fixed redundant costume thumb fetch, empty costume thumb display
[SauerbratenRemote.git] / P2PMud-sauerbraten / src / Plexus.groovy
blob1b6e1e9b0b0f60a01cf5a4cb9d30ca8350883b44
1 import javax.swing.text.html.HTMLEditorKit
2 import javolution.util.FastTable
3 import p2pmud.NoWrapEditorKit
4 import org.stringtree.json.JSONWriter
5 import org.stringtree.json.JSONReader
6 import javax.swing.border.BevelBorder
7 import org.jdesktop.swingx.JXStatusBar
8 import javax.swing.plaf.FontUIResource
9 import org.jdesktop.swingx.painter.GlossPainter
10 import java.awt.Color
11 import org.jdesktop.swingx.border.DropShadowBorder
12 import groovy.swing.SwingXBuilder
13 import java.awt.Font
14 import p2pmud.Tools
15 import static p2pmud.Tools.*
16 import static p2pmud.BasicTools.*
17 import java.awt.event.ItemEvent
18 import java.util.concurrent.Executors
19 import javax.swing.UIManager
20 import p2pmud.CloudProperties
21 import java.text.SimpleDateFormat
22 import rice.p2p.commonapi.IdFactory
23 import rice.Continuation
24 import rice.pastry.Id
25 import p2pmud.P2PMudFile
26 import p2pmud.P2PMudPeer
27 import p2pmud.P2PMudCommandHandler
28 import p2pmud.Player
29 import p2pmud.Dungeon
30 import java.awt.Dimension
31 import java.awt.Component
32 import javax.swing.BoxLayout
33 import javax.swing.border.LineBorder
34 import javax.swing.SpringLayout
35 import javax.swing.BoxLayout
36 import java.awt.BorderLayout
37 import static java.awt.BorderLayout.*
38 import static java.awt.GridBagConstraints.*
39 import java.awt.*
40 import javax.swing.*
41 import javax.swing.border.*
42 import groovy.swing.SwingBuilder
43 import net.miginfocom.swing.MigLayout
44 import DFMapBuilder
45 import GroovyFileFilter
47 public class Plexus {
48 def socket = null
49 def output = null
50 def name
51 def id_index = 1
52 def ids = [:]
53 def names
54 def count = 0
55 def pendingCommands = [:]
56 def sauerCmds = new SauerCmds(this)
57 def pastryCmds = new PastryCmds(this)
58 def sauerSocket
59 def queueRunTime = Long.MAX_VALUE
60 def queueBatchPeriod = 200
61 def swing
62 def gui
63 def fields = [:]
64 def runs = 0
65 def pastryCmd
66 def playerObj = new Player()
67 def peerId
68 def sauerDir
69 def plexusDir
70 def cacheDir
71 def mapDir
72 def mapPrefix = 'packages/dist/storage'
73 def sandbox
74 def peer
75 def mapname
76 def costume
77 def mapTopic
78 def mapProps = [:]
79 def newMapHookBlock
80 def globalProps = [:]
81 def mapIsPrivate
82 /** cloudProperties is the shared properties object for PLEXUS
83 * its keys are path-strings, representing information organized in
84 * a tree
86 def cloudProperties
87 def plexusTopic
88 def presenceLock = new Lock('presence')
89 def playerCount = [:]
90 def triggerLambdas = [:]
91 def portals = [:]
92 def receivedCloudPropertiesHooks = [
93 {updateMyPlayerInfo()}
95 def executor = Executors.newSingleThreadExecutor()
96 def neighborField
97 def playerListeners = [:]
98 def maps
99 def mapCombo
100 def mapPlayers
101 def mapPlayersCombo
102 def launchSauerButton
103 def downloadPanel
104 def downloadProgressBar
105 def uploadCountField
106 def downloadCountField
107 def cloudLight
108 def loadTypeField
109 def followingPlayer
110 def cotumeUploadField
111 def tumes
112 def tumesCombo
113 def fileQ = []
114 def executorThread
115 def pendingDownloads = [] as Set
116 def uploads = 0
117 def downloads = 0
118 def headless = false
119 def cloudFields = [:]
120 def mappingFields = [:]
121 def pastryFields = [:]
122 def cachedPlayerLocations = [:]
123 def cachedLocationDoc
124 def myCachedLocation
125 def costumeUpdating = false
126 def costumeUpdateNeeded = false
128 def static sauerExec
129 def static TIME_STAMP = new SimpleDateFormat("yyyyMMdd-HHmmsszzz")
130 def static final PLEXUS_KEY = "Plexus: main"
131 def static final WORLDS_KEY = "Plexus: worlds"
132 def static final SPHINX_KEY = "Plexus: sphinx"
134 public static continuation(args, parent = null) {
135 Tools.continuation(args, parent)
137 public static void main(String[] a) {
138 if (a.length < 2) {
139 println "Usage: Plexus port name pastryArgs"
140 System.exit(1);
142 new Plexus()._main(a)
144 def static verifySauerdir(dir) {
145 if (!dir) return false
146 def f = dir as File
147 def subs = ['data', 'docs', 'packages/base']
149 for (s in subs) {
150 if (!new File(f, s).isDirectory()) {
151 return false
154 return true
156 def static err(msg, err) {
157 System.err.println(msg)
158 stackTrace(err)
159 System.err.println "UNSANITIZED STACK TRACE FOLLOWS..."
160 err.printStackTrace()
161 System.exit(1)
164 def tst(a, b) {
165 println "TST: $a, $b"
167 def checkExec() {
168 if (executorThread != Thread.currentThread()) {
169 stackTrace(new Exception("Not running in executor thread"))
171 assert executorThread == Thread.currentThread()
174 * Exec a block of code asynchronously, hence exec() does not return a value from the block.
176 def exec(block) {
177 executor.submit({
178 try {
179 block()
180 } catch (Exception ex) {
181 err("", ex)
185 def _main(args) {
186 exec {
187 executorThread = Thread.currentThread()
189 name = args[1]
190 headless = LaunchPlexus.props.headless == '1'
191 if (!headless) {
192 sauerDir = System.getProperty("sauerdir");
193 if (!verifySauerdir(sauerDir)) {
194 usage("sauerdir must be provided")
195 } else if (!name) {
196 usage("name must be provided")
198 sauerDir = new File(sauerDir)
199 } else {
200 sauerDir = new File('duh').getAbsoluteFile().getParentFile()
202 plexusDir = new File(sauerDir, "packages/plexus")
203 cloudProperties = new CloudProperties(this, new File(plexusDir, "cache/$name/cloud.properties"))
204 cloudProperties.persistentPropertyPattern = ~'(map|privateMap|costume)/..*'
205 cloudProperties.privatePropertyPattern = ~'(privateMap)/..*'
206 cacheDir = new File(plexusDir, "cache/$name/files")
207 mapDir = new File(plexusDir, "cache/$name/maps")
208 mapDir.mkdirs()
209 def pastStor = new File(plexusDir, "cache/$name/PAST")
210 if (LaunchPlexus.props.cleanStart) {
211 deleteAll(pastStor)
213 pastStor.mkdirs()
214 System.setProperty('past.storage', pastStor.getAbsolutePath())
215 if (!headless) {
216 cloudProperties.setPropertyHooks[~'player/..*'] = {key, value, oldValue ->
217 if (mapTopic) {
218 def pid = key.substring('player/'.length())
219 def pl = getPlayer(pid)
221 if (pl.map != mapTopic.getId().toStringFull()) {
222 removePlayerFromSauerMap(pl.id)
223 } else if (oldValue) {
224 def oldPl = getPlayer(pid, oldValue)
226 if (oldPl.costume != pl.costume) {
227 println "Costume changed. Loading new costume for $pl"
228 loadCostume(pl)
233 cloudProperties.setPropertyHooks[~'map/..*'] = {key, value, oldValue ->
234 def map = getMap(key.substring('map/'.length()))
236 if (!oldValue) {
237 playerCount[map.id] = 0
239 if (map.id == mapTopic?.getId()?.toStringFull()) {
240 loadMap(map.name, map.dir)
243 cloudProperties.setPropertyHooks[~'privateMap/..*'] = {key, value, oldValue ->
244 def map = getMap(key.substring('privateMap/'.length()))
246 if (!oldValue && pastryCmd) {
247 def player = getPlayer(pastryCmd.from.toStringFull())
249 playerCount[map.id] = 0
250 sauer('private', "tc_msgbox [You received the key to a private world, $map.name: $map.id] [from $player.name ($player.id)]")
252 if (key == mapTopic?.getId()?.toStringFull()) {
253 loadMap(map.name, map.id)
256 cloudProperties.removePropertyHooks[~'player/..*'] = {key, value ->
257 println "CLOUD PROPERTY REMOVE HOOK"
258 removePlayerFromSauerMap(key.substring('player/'.length()))
260 cloudProperties.changedPropertyHooks.add {
261 if (swing) {
262 updatePlayerList()
263 updateMapGui()
264 updateCostumeGui()
265 def props = cloudProperties.properties
266 def data = []
268 new ArrayList(props.keySet()).sort().each {
269 data << ["<b>$it</b>", props[it]]
271 showData(cloudFields.properties, "CURRENT CLOUD PROPERTIES: ${new Date()}", 2, data)
274 //PlasticLookAndFeel.setPlasticTheme(new DesertBlue());
275 try {
276 // UIManager.setLookAndFeel(new Plastic3DLookAndFeel());
277 UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel");
278 } catch (Exception e) {}
279 buildPlexusGui()
280 //start(args[0])
281 } else {
282 cloudProperties.saveFilter = {prop -> !pendingDownloads.contains(prop)}
283 cloudProperties.setPropertyHooks[~'map/..*'] = {key, value, oldValue ->
284 def map = getMap(key.substring('map/'.length()))
286 println "RECEIVED MAP NOTIFICATION, DOWNLODING..."
287 fetchAndSave("map $map.name", key, map.dir, "maps/$map.dir")
289 cloudProperties.setPropertyHooks[~'costume/..*'] = {key, value, oldValue ->
290 def costume = getCostume(key.substring('costume/'.length()))
292 println "RECEIVED MAP NOTIFICATION, DOWNLODING..."
293 fetchAndSave("costume", key, costume.id, "costumes/$costume.id")
296 P2PMudPeer.verboseLogging = LaunchPlexus.props.verbose_log == '1'
297 P2PMudPeer.logFile = new File(plexusDir, "cache/$name/plexus.log")
298 if (P2PMudPeer.verboseLogging) {
299 P2PMudPeer.sauerLogFile = new File(plexusDir, "cache/$name/sauer.log")
300 if (P2PMudPeer.sauerLogFile.exists()) P2PMudPeer.sauerLogFile.delete()
302 try {
303 P2PMudPeer.main(
304 {id, topic, cmd ->
305 try {
306 exec {
307 if (topic == null && cmd == null) {
308 id = id.toStringFull()
309 transmitRemoveCloudProperty("player/$id")
310 println "Going to remove '${ids[id]}' from names"
311 } else {
312 pastryCmd = cmd
313 cmd.msgs.each {line ->
314 pastryCmds.invoke(line)
316 pastryCmd = null
319 } catch (Exception ex) {
320 err("Problem executing command: " + cmd, ex)
322 } as P2PMudCommandHandler,
324 if (P2PMudPeer.test.getNeighborCount() < 2 && !headless) {
325 cloudLight.setIcon(new ImageIcon(getClass().getResource('/resources/disconnected.gif')))
327 //sauer('peers', "peers ${peer.getNeighborCount()}")
328 //dumpCommands()
330 args[2..-1] as String[])
331 } catch (Exception ex) {
332 System.err.println("PROBLEMS CONNECTING TO THE CLOUD. PEER STATE...")
333 System.err.println(P2PMudPeer.test.routeState())
334 err("Could not connect...", ex)
336 peer = P2PMudPeer.test
337 peerId = peer.nodeId.toStringFull()
338 // println "Node ID: $peerId}"
339 println "SAVED NODE ID: $LaunchPlexus.props.nodeId"
340 if (!LaunchPlexus.props.nodeId) {
341 LaunchPlexus.props.nodeId = peerId
342 println "SAVING NEW NODE ID: $LaunchPlexus.props.nodeId"
343 LaunchPlexus.saveProps()
345 names = [p0: peerId]
346 ids = [(peerId): 'p0']
347 plexusTopic = subscribe(peer.buildId(PLEXUS_KEY), null)
348 println "execing init..."
349 exec {
350 if (!headless) {
351 cloudLight.setIcon(new ImageIcon(getClass().getResource('/resources/connected.gif')))
353 if (peer.node.getLeafSet().getUniqueCount() == 1) {
354 println "initBoot"
355 initBoot()
356 } else {
357 println "initJoin"
358 initJoin()
360 if (swing) {
361 updateMappingDiag()
362 updatePastryDiag()
364 if ((LaunchPlexus.props.sauer_mode ?: 'launch') == 'launch') launchSauer();
365 if (launchSauerButton) {
366 launchSauerButton.enabled = true
367 start(LaunchPlexus.props.sauer_port)
371 def showData(field, header, cols, data, prologue = null, epilogue = null) {
372 def buf = ("" << "<html><body>${prologue ?: ''}<table><tr colspan=\"$cols\" style=\"background-color: rgb(192,192,192)\"><td><div>$header</div></td></tr>\n")
373 def count = 0
374 def pane = field.parent.parent
376 data.sort {a, b -> a[0].compareTo(b[0])}
377 data.each {row ->
378 buf << "<tr${count & 1 ? '' : ' style="background-color: rgb(192,255,192)"'}>"
379 row.each {col -> buf << "<td><div>$col</div></td>"}
380 buf << "</tr>\n"
381 count++
383 buf << "</table>${epilogue ?: ''}</body></html>"
384 swing.edt {
385 def horiz = pane.horizontalScrollBar.value
386 def vert = pane.verticalScrollBar.value
388 field.text = buf.toString()
389 swing.doLater {
390 pane.horizontalScrollBar.value = horiz
391 pane.verticalScrollBar.value = vert
395 def fetchAndSave(type, prop, id, location) {
396 pendingDownloads.add(prop)
397 showDownloadProgress(0, 16)
398 fetchDir(id, new File(plexusDir, "cache/$name/$location"), receiveResult: {r ->
399 exec {
400 println "RECEVED ${type.toUpperCase()}, CHECKPOINTING CLOUD PROPS"
401 pendingDownloads.remove(prop)
402 cloudProperties.save()
403 updateDownloads()
404 clearDownloadProgress()
406 }, receiveException: {ex -> err("Could not fetch data for $type: $id -> ${new File(plexusDir, "cache/$name/$location")}", ex)})
408 def die() {
409 if (sauerSocket) {
410 sauerSocket.close()
411 sauerSocket = null
413 exec {
414 swing.edt {
415 gui.visible = false
417 if (plexusTopic) {
418 def node = peer.nodeId.toStringFull()
420 transmitRemoveCloudProperty("player/$node")
422 Thread.start {
423 if (peer) {
424 if (plexusTopic) {
425 Thread.sleep(1000)
426 if (mapTopic) {
427 unsubscribe(mapTopic)
429 unsubscribe(plexusTopic)
430 Thread.sleep(1000)
432 println "destroying node"
433 peer.destroy()
435 Thread.sleep(1000)
436 if (LaunchPlexus.igd) {
437 println "cleaning up UPNP mappings"
438 LaunchPlexus.cleanMappings()
440 println "exiting"
441 System.exit(0)
445 def buildPlexusGui() {
446 swing = new SwingXBuilder()
447 gui = swing.frame(title: "PLEXUS [${LaunchPlexus.props.name}]", size: [500, 500], windowClosing: {die()}, pack: true, show: true) {
448 def makeTitlePainter = {label, pos = null ->
449 compoundPainter() {
450 mattePainter(fillPaint: new Color(0x28, 0x26, 0x19))
451 textPainter(text: label, font: new FontUIResource("SansSerif", Font.BOLD, 12), fillPaint: new Color(0xFF, 0x99, 0x00))
452 glossPainter(paint:new Color(1.0f,1.0f,1.0f,0.2f), position: pos ?: GlossPainter.GlossPosition.TOP)
455 def field = {lbl, key ->
456 label(text: lbl)
457 fields[key] = textField(actionPerformed: {sauerEnt(key)}, focusLost: {sauerEnt(key)}, constraints: 'wrap, growx')
459 titledPanel(title: ' ', titlePainter: makeTitlePainter("PLEXUS [${LaunchPlexus.props.name}]: Killer App of the Future - Here Today!"), border: new DropShadowBorder(Color.BLACK, 15)) {
460 panel(layout: new MigLayout('fill, ins 0')) {
461 panel(layout: new MigLayout(), constraints: '') {
462 label(text: "Node id: ")
463 label(text: LaunchPlexus.props.nodeId ?: "none", constraints: 'wrap, growx')
464 label(text: "Neighbors: ")
465 panel(layout: new MigLayout('fill, ins 0'), constraints: 'spanx,wrap,growx') {
466 button(text: "Update Neighbor List", actionPerformed: {updateNeighborList()})
467 neighborField = label(text: 'none', constraints: 'wrap, growx')
469 label(text: "Command: ")
470 fields.cmd = textField(actionPerformed: {cmd()}, constraints: 'wrap, growx')
472 panel(layout: new MigLayout(), constraints: 'spanx, wrap') {
473 cloudLight = label(icon: imageIcon(resource: '/resources/waiting.gif'), constraints: 'wrap')
475 tabbedPane(constraints: 'spanx,grow,wrap') {
476 scrollPane(name: 'Commands', border: null) {
477 box() {
478 panel(layout: new MigLayout('fillx')) {
479 label(text: 'Generation')
480 panel(layout: new MigLayout('fill, ins 0'), constraints: 'growx,wrap') {
481 launchSauerButton = button(text: "Launch Sauerbraten", actionPerformed: {launchSauer()}, enabled: false)
482 button(text: "Generate Dungeon", actionPerformed: {generateDungeon()})
483 button(text: "Load DF Map", actionPerformed: {loadDFMap()})
484 panel(constraints: 'growx,wrap')
486 label(text: "Current Map: ")
487 mapCombo = comboBox(editable: false, actionPerformed: {
488 exec {
489 if (mapCombo && mapCombo.selectedIndex > -1) {
490 connectWorld(mapCombo.selectedIndex == 0 ? null : maps[mapCombo.selectedIndex - 1].id)
493 }, constraints: 'wrap')
494 label(text: 'Choose Costume')
495 tumesCombo = comboBox(editable: false, actionPerformed: {
496 exec {
497 if (tumesCombo && tumesCombo.selectedIndex > -1) {
498 if (tumesCombo.selectedIndex) {
499 def tume = tumes[tumesCombo.selectedIndex - 1]
501 useCostume(tume.name, tume.id)
502 } else {
503 useCostume('', 0)
507 }, constraints: 'wrap')
508 label(text: "Follow player: ")
509 mapPlayersCombo = comboBox(editable: false, actionPerformed: {
510 exec {
511 if (mapPlayersCombo && mapPlayersCombo.selectedIndex > -1) {
512 followingPlayer = mapPlayersCombo.selectedIndex ? mapPlayers[mapPlayersCombo.selectedIndex - 1] : null
513 println "NOW FOLLOWING: ${followingPlayer?.name}"
514 if (followingPlayer) {
515 def array = cachedPlayerLocations[followingPlayer.id]
516 if (array) {
517 def name = array[0], update = array[1]
518 playerUpdate(followingPlayer.id, name, update)
523 }, constraints: 'wrap')
524 button(text: 'Upload Costume', actionPerformed: {
525 exec {
526 if (costumeUploadField.text) {
527 pushCostumeDir(costumeUploadField.text as File)
531 panel(constraints: 'growx,wrap', layout: new MigLayout('fill,ins 0')) {
532 costumeUploadField = textField(constraints: 'growx', actionPerformed: {
533 exec {pushCostumeDir(costumeUploadField.text as File)}
535 button(text: '...', actionPerformed: {
536 exec {
537 def file = chooseFile("Choose a model to upload", costumeUploadField, "Costumes", "")
539 if (file) {
540 pushCostumeDir(file)
548 scrollPane(name: 'Stats', border: null) {
549 box() {
550 panel(name: 'Stats', layout: new MigLayout('fillx')) {
551 field('x: ', 'x')
552 field('y: ', 'y')
553 field('z: ', 'z')
554 field('vx: ', 'vx')
555 field('vy: ', 'vy')
556 field('vz: ', 'vz')
557 field('fx: ', 'fx')
558 field('fy: ', 'fy')
559 field('fz: ', 'fz')
560 field('roll: ', 'rol')
561 field('pitch: ', 'pit')
562 field('yaw: ', 'yaw')
563 field('strafe: ', 's')
564 field('edit: ', 'e')
565 field('move: ', 'm')
566 field('physics state: ', 'ps')
567 field('max speed: ', 'ms')
568 panel(constraints: 'growy')
572 panel(name: 'Cloud', layout: new MigLayout('fill')) {
573 label(text: 'Neighbors: ')
574 cloudFields.neighbors = label(constraints: 'growx,wrap')
575 label(text: 'Cloud Properties', constraints: 'growx,spanx,wrap')
576 cloudFields.scrollPane = scrollPane(constraints: 'grow,span,wrap', border: null) {
577 cloudFields.properties = textPane(editable: false, editorKit: new NoWrapEditorKit())
580 panel(name: 'ID Mapping', layout: new MigLayout('fill')) {
581 scrollPane(constraints: 'grow,span,wrap', border: null) {
582 mappingFields.ids = textPane(editable: false, editorKit: new NoWrapEditorKit())
584 scrollPane(constraints: 'grow,span,wrap', border: null) {
585 mappingFields.names = textPane(editable: false, editorKit: new NoWrapEditorKit())
588 panel(name: 'Cached Locations', layout: new MigLayout('fill')) {
589 button(text: 'Update', actionPerformed: {exec {updateCachedLocationDoc()}}, constraints: 'wrap')
590 scrollPane(constraints: 'grow,span,wrap', border: null) {
591 cachedLocationDoc = textPane(editable: false, editorKit: new NoWrapEditorKit())
594 panel(name: 'Pastry', layout: new MigLayout('fill')) {
595 button(text: 'Update', actionPerformed: {exec {updatePastryDiag()}}, constraints: 'wrap')
596 scrollPane(constraints: 'grow,span,wrap', border: null) {
597 pastryFields.topics = textPane(editable: false, editorKit: new NoWrapEditorKit())
601 label(minimumSize: [24,24], text: ' ', foregroundPainter: makeTitlePainter('Copyright (C) 2008, TEAM CTHULHU', GlossPainter.GlossPosition.BOTTOM), constraints: 'growx, spanx, height 24, wrap')
604 statusBar(border: new BevelBorder(BevelBorder.LOWERED)) {
605 downloadPanel = panel(layout: new MigLayout('ins 0 2 0 2,fillx'), constraints: new JXStatusBar.Constraint(JXStatusBar.Constraint.ResizeBehavior.FILL)) {
606 label(text: 'Uploads: ')
607 uploadCountField = label(text: '0')
608 label(text: ' Downloads: ')
609 downloadCountField = label(text: '0')
610 label(text: ' Current ')
611 loadTypeField = label(text: 'Upload')
612 label(text: ': ')
613 downloadProgressBar = progressBar(constraints: 'growx', minimum: 0, maximum: 100)
618 def updateCachedLocationDoc() {
619 def buf = "<html>" << "<body><table>"
621 cachedPlayerLocations.each {
622 buf << "<tr><td>$it.key</td><td>${it.value[0]}</td><td><div>${it.value[1].join(' ')}</div></td></tr>"
624 buf << "</table></body></html>"
625 cachedLocationDoc.text = buf.toString()
627 def chooseFile(message, field, filterName, filterRegexp) {
628 def ch = new JFileChooser();
630 if (field.text) {
631 ch.setSelectedFile(field.text as File)
633 ch.setDialogTitle(message);
634 ch.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES)
635 ch.setFileFilter(new GroovyFileFilter(filterName) {it.isDirectory() || it.name ==~ filterRegexp})
636 def result = ch.showOpenDialog(null) == JFileChooser.APPROVE_OPTION ? ch.getSelectedFile() : null
637 if (result) {
638 field.text = result.getAbsolutePath()
640 return result
642 def updateNeighborList() {
643 try {
644 neighborField.text = String.valueOf(peer.getNeighborCount())
645 } catch (Exception ex) {}
647 def launchSauer() {
648 if (sauerExec) {
649 exec {
650 def env = []
651 def winderz = System.getProperty('os.name').toLowerCase() ==~ /.*windows.*/
653 for (vars in System.getenv()) {
654 if (winderz && vars.key.equalsIgnoreCase('path')) {
655 env.add("$vars.key=$vars.value;$sauerDir\\bin")
656 } else {
657 env.add("$vars.key=$vars.value")
661 def commandLine = sauerExec.replaceFirst(/\-l[^\s]+/, '') // remove old -llimbo if present
663 commandLine += ' -l' + buildMapPath()
664 commandLine += " \"-xalias sauerPort $LaunchPlexus.props.sauer_port;alias sauerHost 127.0.0.1\""
665 def t = new StreamTokenizer(new StringReader(commandLine))
666 def args = []
667 t.resetSyntax()
668 t.wordChars(0, 127)
669 t.whitespaceChars(0, (int)' ')
670 t.quoteChar((int)'"')
671 t.quoteChar((int)"'")
672 while (t.nextToken() != StreamTokenizer.TT_EOF) {
673 args << t.sval
675 env = env as String[]
676 println ("Going to exec $commandLine from $sauerDir with env: $env, args: $args")
677 Runtime.getRuntime().exec(args as String[], env, sauerDir)
681 def buildMapPath() {
682 def result = 'plexus/dist/limbo/map'
683 def who = getPlayer(peerId)
684 println who
685 if (who && who.map) {
686 def map = getMap(who.map)
687 if (map && map.dir) {
688 def dir = new File(mapDir, map.dir)
689 def mapPath = subpath(new File(sauerDir, "packages"), dir)
690 println mapPath
691 result = "$mapPath/map"
694 return result
696 def loadDFMap() {
697 def ch = new JFileChooser();
698 ch.setDialogTitle("Please choose the DF map to load");
699 ch.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES)
700 ch.setFileFilter(new GroovyFileFilter("DF ASCII MAPS (*.txt)") {it.isDirectory() || it.name.toLowerCase().endsWith(".txt")})
701 if (ch.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {
702 Thread.start {
703 if (ch.getSelectedFile().isFile()) {
704 def dir = ch.getSelectedFile().getAbsolutePath();
706 def df = new DFMapBuilder()
707 df.buildMap(this, dir)
713 // generate a random dungeon
714 def generateDungeon() {
715 println ("Going to generate dungeon")
716 Thread.start {
717 exec {
718 sauer('newmap', 'if (= 1 $editing) [ edittoggle ]; tc_allowedit 1; thirdperson 0; newmap; musicvol 0')
719 sauer('models', 'mapmodelreset ; mmodel tc_door ')
720 dumpCommands()
722 def dungeon = new Dungeon(6, 6, 3, 1)
724 dungeon.generate_maze();
726 def blocks = dungeon.convertTo3DBlocks()
728 for (def i = 0; i < dungeon.blockRows; ++i) {
729 for (def j = 0; j < dungeon.blockCols; ++j) {
730 def b = blocks[i][j]
731 if (b != 'X') {
732 def x = i * 32
733 def y = j * 32
734 def wx = x - 32, wy = y - 32
735 //println "x: $x y: $y"
736 def h = (b == ' ' || b == 'l') ? 2 : 1
737 exec {
738 if (b == 'z') {
739 sauer('secret', "selcube $x $y 430 1 1 $h 32 5; editmat noclip")
740 } else {
741 sauer('delcube', "selcube $x $y 430 1 1 $h 32 5; delcube")
743 if (b == 'e') {
744 sauer('door', "selcube $x $y 430 1 1 1 32 4; ent.yaw p0 180; entdrop 3; newent mapmodel 0 6")
746 else if (b == 's') {
747 sauer('door', "selcube $x $y 430 1 1 1 32 4; ent.yaw p0 90; entdrop 3; newent mapmodel 0 6")
749 else if (b == 'l') {
750 sauer('light', "selcube $x $y 450 1 1 1 32 4; entdrop 2; newent light 128 255 255 255")
752 dumpCommands()
758 exec {
759 sauer('tex', 'texturereset; setshader stdworld; exec packages/egyptsoc/package.cfg ')
760 sauer("texture", "selcube 0 0 480 2 2 2 512 4; tc_settex 35 1")
761 sauer("texture2", "tc_settex 37 0; selcube 0 0 480 2 2 2 512 5; tc_settex 51 0")
762 sauer("texture3", "selcube 0 0 470 512 512 1 16 5; tc_settex 7 1")
765 for (def i = 0; i < dungeon.blockRows; ++i) {
766 for (def j = 0; j < dungeon.blockCols; ++j) {
767 def b = blocks[i][j]
768 if (b != 'X') {
769 def x = i * 32
770 def y = j * 32
771 def wx = x - 32, wy = y - 32
772 exec {
773 if (b == 'e') {
774 sauer('wall1', "selcube $wx $wy 430 3 3 1 32 0; tc_settex 56 0")
775 sauer('wall2', "selcube $wx $wy 430 3 3 1 32 1; tc_settex 56 0")
777 else if (b == 's') {
778 sauer('wall1', "selcube $wx $wy 430 3 3 1 32 2; tc_settex 56 0")
779 sauer('wall2', "selcube $wx $wy 430 3 3 1 32 3; tc_settex 56 0")
781 dumpCommands()
787 exec {
788 sauer("spawn", "selcube 32 32 416 1 1 1 32 5; ent.yaw p0 135; newent playerstart; tc_respawn p0")
789 sauer('finished', 'remip; calclight 3; tc_allowedit 0; thirdperson 1')
790 dumpCommands()
794 //Plexus.bindLevelTrigger(35, 'remotesend levelTrigger 35 $more $data') {println "duh"} remotesend levelTrigger 35
795 def bindLevelTrigger(trigger, lambda) {
796 if (!lambda) println "Error! Trigger lambda is null!"
797 trigger = Integer.parseInt(trigger)
798 triggerLambdas[trigger] = lambda
799 sauer('trigger', "level_trigger_$trigger = [ remotesend levelTrigger $trigger ]")
800 dumpCommands()
802 def levelTrigger(trigger) {
803 trigger = Integer.parseInt(trigger)
804 println "sauer trigger: $trigger"
805 if (triggerLambdas[trigger]) triggerLambdas[trigger](trigger)
807 def usage(msg) {
808 println msg
809 println "usage: ${getClass().name} port name"
810 println "System property sauerdir must hold your Sauerbraten distribution directory"
812 def start(port) {
813 Thread.startDaemon {
814 // don't enable socket to sauer til after we've joined the ring
815 sauerSocket = new ServerSocket()
816 sauerSocket.setReuseAddress(true)
817 //sauerSocket.setSoTimeout(10);
818 sauerSocket.bind(new java.net.InetSocketAddress(Integer.parseInt(port)))
819 println ("Sauer socket opened at: $port")
821 println "READY"
822 while (sauerSocket != null) {
823 try {
824 socket = sauerSocket.accept {
825 println("Got connection from sauerbraten...")
826 output = it.getOutputStream()
827 launchSauerButton.enabled = false
828 exec {init()}
829 try {
830 it.getInputStream().eachLine {line ->
831 exec {
832 try {
833 exec {sauerCmds.invoke(line)}
834 } catch (Exception ex) {
835 err("Problem executing sauer command: " + it, ex)
839 } catch (Exception ex) {}
840 try {it.shutdownInput()} catch (Exception ex) {}
841 try {it.shutdownOutput()} catch (Exception ex) {}
842 swing.edt {
843 gui.visible = true
844 gui.extendedState = Frame.NORMAL
845 gui.requestFocus()
847 launchSauerButton.enabled = true
848 println "Disconnect"
850 } catch (IOException ex) {}
854 def sauer(key, value) {
855 checkExec()
856 pendingCommands[key] = value
858 def hasSauerConnection() {
859 socket?.isConnected()
861 def dumpCommands() {
862 checkExec()
863 if (!pendingCommands.isEmpty()) {
864 if (socket?.isConnected()) {
865 def out = pendingCommands.collect{it.value}.join(";") + '\n'
866 // println out
867 if (P2PMudPeer?.sauerLogFile) { P2PMudPeer.sauerLogFile << new Date().toString() << ' ' << out; }
868 try {
869 output << out
870 output.flush()
871 } catch (SocketException ex) {
872 try {socket.shutdownInput()}catch(Exception ex2){}
873 try {socket.shutdownOutput()}catch(Exception ex2){}
874 socket = null
877 pendingCommands = [:]
880 def sauerEnt(label) {
881 if (fields[label]?.text && fields[label].text[0]) {
882 def cmd = "ent.$label ${ids[peerId]} ${fields[label].text}"
883 exec {
884 sauer(label, cmd)
885 dumpCommands()
889 def cmd() {
890 if (fields.cmd.text && fields.cmd.text[0]) {
891 def txt = fields.cmd.text
893 fields.cmd.text = ""
894 exec {
895 sauer('cmd', fields.cmd.text)
896 dumpCommands()
900 def computePlayerInfo() {
901 def playerInfo = ""
902 def who = getPlayer(peerId)
903 def costume = who && who.costume ? who.costume : "mrfixit"
904 playerInfo = "playerinfo p0 \"${who?.guild}\" $costume"
905 println "pi $playerInfo"
906 return playerInfo
908 def init() {
909 exec {
910 sauer('init', [
911 "alias p2pname [$name]",
912 "name [$name]",
913 "map " + buildMapPath(),
914 computePlayerInfo(),
915 "cleargui 1",
916 "showgui Plexus",
917 'echo INIT'
918 ].join(';'))
919 dumpCommands()
920 updatePlayerList()
921 updateMapGui()
922 updateCostumeGui()
925 def broadcast(cmds) {
926 checkExec()
927 if (peer) peer.broadcastCmds(mapTopic, cmds as String[])
929 def anycast(cmds) {
930 checkExec()
931 if (peer) peer.anycastCmds(mapTopic, cmds as String[])
933 def send(id, cmds) {
934 checkExec()
935 if (id instanceof String) {
936 id = Id.build(id)
938 if (peer) peer.sendCmds(id, cmds as String[])
940 def cvtNewlines(str) {
941 println "${str.replaceAll(/\n/, ';')}"
942 return str.replaceAll(/\n/, ';')
944 def uniqify(name) {
945 "$name-${TIME_STAMP.format(new Date())}"
947 def loadMap(name, id, cont = null) {
948 def dir = new File(mapDir, id)
950 println "Loading map: ${id}"
951 if (id instanceof String) {
952 id = Id.build(id)
954 fetchDir(id, dir, receiveResult: {result ->
955 def mapPath = subpath(new File(sauerDir, "packages"), dir)
957 if (cont) {
958 if (hasSauerConnection()) {
959 newMapHookBlock = {cont.receiveResult(result)}
960 println "Retrieved map from PAST: $dir, executing: map [$mapPath/map]"
961 sauer('load', "echo loading new map: [$mapPath/map]; tc_loadmsg [$name]; map [$mapPath/map]")
962 dumpCommands()
963 } else {
964 cont.receiveResult(result)
967 }, receiveException: {ex ->
968 if (cont) {
969 cont.receiveException(ex)
970 } else {
971 err("Couldn't load map: $id", ex)
975 def selectMap() {
976 mapCombo.selectedItem = mapTopic ? getMap(mapTopic.getId().toStringFull()).name : ''
978 def initBoot() {
979 def docFile = new File(plexusDir, "cache/$name/cloud.properties")
981 if (docFile.exists()) {
982 cloudProperties.load()
984 updateMyPlayerInfo()
985 if (LaunchPlexus.props.cleanStart) {
986 storeCache()
989 def initJoin() {
990 checkExec()
991 deleteAll(cacheDir)
992 peer.anycastCmds(plexusTopic, "sendCloudProperties")
994 def storeFile(cont, file, mutable = false, cacheOverride = false) {
995 def total = P2PMudFile.estimateChunks(file.length())
996 def chunk = 0
998 uploads++
999 showUploadProgress(0, 16)
1000 updateDownloads()
1001 queueIo(cont, {uploads--; updateDownloads(); clearUploadProgress()}) {chain -> peer.wimpyStoreFile(cacheDir, file, {showUploadProgress(chunk++, total)}, chain, mutable, cacheOverride)}
1003 def storeDir(cont, dir) {
1004 uploads++
1005 showUploadProgress(0, 16)
1006 updateDownloads()
1007 queueIo(cont, {uploads--; updateDownloads(); clearUploadProgress()}) {chain -> P2PMudFile.storeDir(cacheDir, dir, {chunk, total -> showUploadProgress(chunk, total)}, chain)}
1009 def fetchFile(cont, id) {
1010 def chunk = 0
1012 downloads++
1013 showDownloadProgress(0, 16)
1014 updateDownloads()
1015 queueIo(cont, {downloads--; updateDownloads(); clearDownloadProgress()}) {chain -> peer.wimpyGetFile(id, cacheDir, {total -> showDownloadProgress(chunk++, total)}, chain)}
1017 def fetchDir(cont, id, dir, mutable = false) {
1018 downloads++
1019 showDownloadProgress(0, 16)
1020 updateDownloads()
1021 queueIo(cont, {downloads--; updateDownloads(); clearDownloadProgress()}) {chain -> P2PMudFile.fetchDir(id, cacheDir, dir, {chunk, total -> showDownloadProgress(chunk, total)}, chain, mutable)}
1023 def queueIo(cont, completedBlock, block) {
1024 checkExec()
1025 if (fileQ.empty) {
1026 println "EXECUTING"
1027 block(ioContinuation(cont, completedBlock))
1028 } else {
1029 println "QUEUING"
1030 fileQ.add({println "EXECUTING QUEUED"; block(ioContinuation(cont, completedBlock))})
1033 def ioContinuation(cont, completedBlock) {
1034 continuation(receiveResult: {r ->
1035 exec {
1036 println "DONE"
1037 completedBlock()
1038 cont.receiveResult(r)
1039 chainIo()
1041 }, receiveException: {e ->
1042 exec {
1043 println "ERROR"
1044 completedBlock()
1045 cont.receiveException(e)
1046 chainIo()
1050 def chainIo() {
1051 if (!fileQ.empty) {
1052 fileQ.remove(0)()
1055 def storeCache() {
1056 if (cacheDir.exists()) {
1057 def files = []
1059 println "STORING CACHE"
1060 deleteAll(new File(cacheDir, 'download'))
1061 cacheDir.eachFile {subDir ->
1062 subDir.eachFile {file ->
1063 files.add(file)
1066 serialContinuations(files, receiveResult: {
1067 println "FINISHED PUSHING CACHE"
1068 // sauer('msg', 'tc_msgbox Ready [Finished pushing cache in PAST]')
1069 // dumpCommands()
1070 }, receiveException: {
1071 println "FAILED TO PUSH CACHE"
1072 err("Error pushing cache in PAST", ex)
1073 }) {file, chain ->
1074 println "STORING FILE: $file"
1075 storeFile(chain, file, false, true)
1077 } else {
1078 println "NO CACHE TO STORE"
1081 def setCloudProperty(key, value) {
1082 cloudProperties[key] = value
1084 def transmitSetCloudProperty(key, value) {
1085 checkExec()
1086 cloudProperties[key] = value
1087 peer.broadcastCmds(plexusTopic, ["setCloudProperty $key $value"] as String[])
1088 println "BROADCAST PROPERTY: $key=$value"
1090 def transmitRemoveCloudProperty(key) {
1091 checkExec()
1092 println "REMOVING CLOUD PROPERTY"
1093 cloudProperties.removeProperty(key)
1094 if (peer) {
1095 peer.broadcastCmds(plexusTopic, ["removeCloudProperty $key"] as String[])
1097 println "BROADCAST REMOVE PROPERTY: $key"
1099 def removeCloudProperty(key) {
1100 checkExec()
1101 println "REMOVING CLOUD PROPERTY"
1102 cloudProperties.removeProperty(key)
1104 def receiveCloudProperties(props) {
1105 checkExec()
1106 cloudProperties.setProperties(props, true)
1107 receivedCloudPropertiesHooks.each {it()}
1109 def getMap(id, entry = null) {
1110 checkExec()
1111 def map
1112 def privateMap = false
1114 if (!entry) {
1115 entry = cloudProperties["map/$id"]
1117 if (!entry) {
1118 entry = cloudProperties["privateMap/$id"]
1119 privateMap = true
1121 if (entry) {
1122 map = new JSONReader().read(entry)
1123 map.id = id
1124 map.privateMap = privateMap
1126 return [
1127 id: id,
1128 dir: entry[0],
1129 name: entry[1..-1].join(' '),
1130 privateMap: privateMap
1134 return map
1136 def getPlayer(id, entry = null) {
1137 checkExec()
1138 def pl
1140 entry = entry ?: cloudProperties["player/$id"]
1141 if (entry) {
1142 pl = new JSONReader().read(entry)
1143 pl.id = id
1145 pl = [
1146 id: id,
1147 map: entry[0] == 'none' ? null : entry[0],
1148 costume: entry[1] == 'none' ? null : entry[1],
1149 name: entry[2..-1].join(' ')
1153 return pl
1155 def getCostume(id, entry = null) {
1156 checkExec()
1157 def costume
1158 if (!entry) {
1159 entry = cloudProperties["costume/$id"]
1161 if (entry) {
1162 costume = new JSONReader().read(entry)
1163 costume.id = id
1164 return costume
1166 return [
1167 id: id,
1168 thumb: entry[0] == 'none' ? null : entry[0],
1169 type: entry[1],
1170 name: entry[2..-1].join(' ')
1174 return costume
1176 def newMapHook(name) {
1177 println "newmap: $name"
1178 clearPlayers()
1179 sauer("delp", "deleteallplayers")
1180 cachedPlayerLocations.each {
1181 def pname = it.value[0], update = it.value[1]
1182 def id = newPlayer(pname, it.key)
1183 sauer("${id}.update", "tc_setinfo $id ${update.join(' ')}")
1184 dumpCommands()
1187 dumpCommands()
1188 if (newMapHookBlock) {
1189 newMapHookBlock()
1190 newMapHookBlock = null
1192 mapname = name
1193 updateMyPlayerInfo()
1194 def groovyScript = name ==~ '[^/]*' ? new File(sauerDir, "packages/base/${name}.groovy") : new File(sauerDir, "packages/${name}.groovy")
1195 def runScript = false
1196 if (groovyScript.exists()) {
1197 if (mapTopic) {
1198 def map = getMap(mapTopic.getId().toStringFull())
1200 runScript = name ==~ ".*/$map.dir/map"
1201 } else {
1202 runScript = name ==~ '.*/limbo/map(.ogz)?'
1205 if (runScript) {
1206 mapProps = [:]
1207 sandbox = new Sandbox(this, [groovyScript.parent])
1208 try {
1209 sandbox.exec(groovyScript.name)
1210 } catch (Exception ex) {
1211 System.err.println "Error while executing map script..."
1212 stackTrace(ex)
1216 def updateMyPlayerInfo() {
1217 if (!headless) {
1218 def id = mapTopic?.getId()?.toStringFull()
1220 println "UPDATING PLAYER INFO"
1221 // after we get the players list, send ourselves out
1222 def node = peer.nodeId.toStringFull()
1223 //TODO put tume in here and persist in props
1224 transmitSetCloudProperty("player/$node", new JSONWriter().write([map: id, costume: costume, name: name, guild: LaunchPlexus.props.guild]))
1227 def removePlayerFromSauerMap(peerId) {
1228 def sauerId = ids[peerId]
1229 if (sauerId && sauerId != 'p0') {
1230 def who = getPlayer(peerId)
1232 if (who) {
1233 println "Going to remove player $who.name from sauer: $sauerId"
1234 sauer('msgplayer', "echo [Player $who.name has left this world.]")
1236 ids.remove(peerId)
1237 names.remove(sauerId)
1238 cachedPlayerLocations.remove(peerId)
1239 if (swing) {
1240 updateMapGui()
1241 updateMappingDiag()
1243 sauer('delplayer', "deleteplayer $sauerId")
1244 dumpCommands()
1245 } else {
1246 println "WARNING: can't find id for player: $peerId"
1249 def newPlayer(name, node = pastryCmd.from.toStringFull()) {
1250 def who = getPlayer(node)
1251 def sauerid = ids[node]
1252 def guild = who ? who.guild : ""
1253 if (!sauerid) sauerid = "p$id_index" as String
1255 ids[node] = sauerid
1256 names[sauerid] = node
1257 ++id_index
1258 sauer('prep', "echo [Welcome player $name to this world.]; createplayer $sauerid $name; playerinfo $sauerid \"$guild\"")
1259 loadCostume(who)
1260 if (swing) {
1261 updateMappingDiag()
1263 return sauerid
1265 def updateMappingDiag() {
1266 if (!swing) return
1267 def data = []
1269 ids.each {
1270 data << [it.key, it.value]
1272 showData(mappingFields.ids, "IDs", 2, data)
1273 data = []
1274 names.each {
1275 data << [it.key, it.value]
1277 showData(mappingFields.names, "Names", 2, data)
1279 def updatePastryDiag() {
1280 if (!swing) return
1281 def data = []
1283 peer.topics.each {
1284 def map = getMap(it.id.toStringFull())
1286 if (it == plexusTopic) {
1287 map = "PLEXUS"
1289 data << [it, map]
1291 showData(pastryFields.topics, "Topics", 2, data, "", "Neighbors: ${peer.getNeighborCount()}<br>Route State...<br><pre>${peer.routeState()}</pre><br>")
1293 def updatePlayerList() {
1294 if (!peer?.nodeId || !swing) return
1295 synchronized (presenceLock) {
1296 updateMapGui()
1298 def id = peer.nodeId.toStringFull()
1299 def myMap = mapTopic ? getMap(mapTopic.getId().toStringFull()) : [name: 'Limbo', id: '0']
1300 def allPlayers = [], newMapPlayers = []
1302 cloudProperties.each('player/(..*)') {key, value, match ->
1303 def pid = match.group(1)
1305 if (pid != id) {
1306 def who = getPlayer(pid)
1307 allPlayers.add(who)
1308 if (myMap?.id == who.map) {
1309 newMapPlayers.add(who)
1313 newMapPlayers.sort {a, b -> a.name.compareTo(b.name)}
1314 if (newMapPlayers != mapPlayers) {
1315 mapPlayers = newMapPlayers
1316 swing.doLater {
1317 def sel = mapPlayersCombo.selectedIndex
1319 mapPlayersCombo.removeAllItems()
1320 mapPlayersCombo.addItem('')
1321 for (player in mapPlayers) {
1322 mapPlayersCombo.addItem(player.name)
1324 mapPlayersCombo.selectedIndex = sel
1328 dumpPlayersMenu(myMap, allPlayers)
1331 def dumpPlayersMenu(myMap, allPlayers) {
1332 println "DUMPING PLAYERS TO SAUER"
1333 def PlayerGui = 'alias showpcostume [ guibar; guiimage (concatword "packages/plexus/models/thumbs/" (get $pcostumenames $guirollovername)) $guirolloveraction 4 1 "packages/plexus/dist/tc_logo.jpg"];'
1334 PlayerGui += "alias pcostumenames ["
1335 allPlayers.each( {who ->
1336 def c = getCostume(who.costume)
1337 def ct = (c != null && c.thumb) ? "${c.id}.$c.type" : ''
1338 def map = (!who.map || who.map == 'none') ? 'none' : getMap(who.map)?.name ?: 'unknown map'
1339 PlayerGui += " \"$who.name ($map)\" $ct"
1341 PlayerGui += " ];"
1343 PlayerGui += 'newgui Players [ \n guilist [ \n guilist [ \n'
1344 def i = 0, needClose = true, last = allPlayers.size()
1345 def wrapAfter = 12
1346 def cnt = 0
1347 def mapCnt = 0
1348 def mapTab = ''
1349 def mymapid = myMap?.id
1351 allPlayers.each( {who ->
1352 def map = (!who.map || who.map == 'none') ? 'none' : getMap(who.map)?.name ?: 'unknown map'
1354 PlayerGui += "guibutton [$who.name ($map)] [alias tc_whisper $who.id; alias selected_player [$who.name]; alias mapIsPrivate $mapIsPrivate; showgui Player]\n"
1355 ++cnt
1356 if (++i % wrapAfter == 0) {
1357 PlayerGui += '] \n showpcostume \n ] \n'
1358 needClose = false
1359 if (i != last) {
1360 def s = Math.min(i, last - 1), e = Math.min(i + wrapAfter, last - 1)
1361 s = Character.toUpperCase((allPlayers[s].name[0]) as char)
1362 e = Character.toUpperCase((allPlayers[e].name[0]) as char)
1363 PlayerGui += " guitab [More $s-$e] \n guilist [ \n guilist [ \n"; needClose = true
1367 if (mymapid == who.map) {
1368 mapTab += "guibutton [$who.name] [echo $who.id]\n"
1369 ++mapCnt
1372 if (cnt == 0) PlayerGui += 'guitext "Sorry, no players are online!"\n'
1373 //PlayerGui += "guibar\n guibutton Close [cleargui]\n"
1374 if (needClose) PlayerGui += '] \n showpcostume \n ] \n'
1376 if (mymapid) {
1377 PlayerGui += "guitab $myMap.name\n$mapTab\n"
1378 if (mapCnt == 0) PlayerGui += "guitext [Sorry, no players are connected to $myMap.name!]\n"
1379 println "MAPCNT: $mapCnt"
1380 PlayerGui += "guibar\n guibutton Close [cleargui]\n"
1382 // bump counts to include ourself
1383 ++cnt
1384 ++mapCnt
1385 PlayerGui += "]; peers $cnt; tc_mapcount $mapCnt" // ;tc_loadmsg ${allPlayers ? myMap.name : 'none'}"
1386 sauer('Player', cvtNewlines(PlayerGui))
1387 dumpCommands()
1389 def shareMap(id) {
1390 def key = "privateMap/${mapTopic.getId().toStringFull()}"
1391 def value = cloudProperties[key]
1393 peer.sendCmds(Id.build(id), ["setCloudProperty $key $value"] as String[])
1395 def updateMapGui() {
1396 if (!swing) return
1397 def ents = []
1398 def privates = []
1400 playerCount = [:]
1401 cloudProperties.each('map/(.*)') {key, value, match ->
1402 playerCount[match.group(1)] = 0
1404 cloudProperties.each('privateMap/(.*)') {key, value, match ->
1405 playerCount[match.group(1)] = 0
1407 cloudProperties.each('player/(.*)') {key, value, match ->
1408 def player = getPlayer(match.group(1))
1410 if (player.map && (playerCount[player.map] == 0 || playerCount[player.map])) {
1411 playerCount[player.map]++
1415 def newMaps = []
1416 cloudProperties.each('map/(.*)') {key, value, match ->
1417 def map = getMap(match.group(1))
1418 def cnt = playerCount[map.id]
1419 ents.add([map.name + " ($cnt)", map.id, cnt, map.dir])
1420 newMaps.add(map)
1422 ents.sort {a, b -> a[0].compareTo(b[0])}
1424 def mapsGui = 'alias showmapthumb [ guibar; guiimage (concatword "packages/plexus/cache/' + name + '/maps/" (get $mapthumbs $guirollovername) "/map.jpg") $guirolloveraction 4 1 "packages/plexus/dist/tc_logo.jpg"];'
1425 mapsGui += "alias mapthumbs ["
1426 for (trip in ents) {
1427 mapsGui += " \"${trip[0]}\" ${trip[3]}"
1429 mapsGui += " ];"
1430 mapsGui += "newgui Worlds [ \n guilist [ \n guilist [ \n"
1431 def i = 0, needClose = true, last = ents.size()
1432 def wrapAfter = 12
1433 for (world in ents) {
1434 mapsGui += "guibutton [${world[0]}] [remotesend connectWorld ${world[1]}]\n"
1435 if (++i % wrapAfter == 0) {
1436 mapsGui += '] \n showmapthumb \n ] \n'
1437 needClose = false
1438 if (i != last) {
1439 def s = Math.min(i, last - 1), e = Math.min(i + wrapAfter, last - 1)
1440 s = Character.toUpperCase((ents[s][0][0]) as char)
1441 e = Character.toUpperCase((ents[e][0][0]) as char)
1442 mapsGui += " guitab [More $s-$e] \n guilist [ \n guilist [ \n"; needClose = true
1446 if (needClose) mapsGui += '] \n showmapthumb \n ] \n'
1447 newMaps.sort {a, b -> a.name.compareTo(b.name)}
1448 if (newMaps != maps) {
1449 maps = newMaps
1450 swing.doLater {
1451 def sel = mapCombo.selectedItem
1453 mapCombo.removeAllItems()
1454 mapCombo.addItem('')
1455 for (map in maps) {
1456 mapCombo.addItem(map.name)
1458 mapCombo.selectedItem = sel
1461 cloudProperties.each('privateMap/(.*)') {key, value, match ->
1462 def map = getMap(match.group(1))
1464 ents.add([map.name, map.id, playerCount[map.id]])
1465 privates.add([map.name, map.id, playerCount[map.id]])
1467 if (privates) {
1468 privates.sort {a, b -> a[0].compareTo(b[0])}
1469 mapsGui += "guitab [Private Worlds]\n"
1470 privates.each {
1471 mapsGui += "guibutton [${it[0]} (${it[2]})] [remotesend connectWorld ${it[1]}]\n"
1474 mapsGui += "]\nnewgui Portals ["
1475 for (world in ents) {
1476 mapsGui += "guibutton [${world[0]} (${world[2]})] [remotesend createPortal ${world[0]} ${world[1]}]\n"
1478 mapsGui += "]\n"
1479 sauer('maps', cvtNewlines(mapsGui))
1480 dumpCommands()
1482 def loadCostume(who) {
1483 if (who?.costume) {
1484 println "loading costume: $who.costume"
1485 def costume = getCostume(who.costume)
1486 def costumeFile = new File(plexusDir, "models/$who.costume")
1488 if (costumeFile.exists()) {
1489 clothe(who, who.costume)
1490 } else {
1491 fetchDir(who.costume, new File(plexusDir, "models/$who.costume"), receiveResult: {r ->
1492 clothe(who, who.costume)
1493 }, receiveException: {ex ->
1494 System.err.println("Could not fetch data for costume: $who.costume: ex")
1495 stackTrace(ex)
1500 def clothe(who, costumeDir) {
1501 println "Clothing $who.id with costume: $costumeDir"
1502 println "SENDING: playerinfo ${ids[who.id]} [${who.guild ?: ''}] $costumeDir"
1503 sauer('clothe', "playerinfo ${ids[who.id]} [${who.guild ?: ''}] $costumeDir")
1504 dumpCommands()
1506 def pushCostume(name) {
1507 println "PUSHING COSTUME: $name"
1508 pushCostumeDir(name, new File(plexusDir, "models/$name"))
1510 def pushCostumeDir(name = path ? (path as File).getName() : null, path) {
1511 if (!path?.exists()) {
1512 sauer('err', "tc_msgbox [File costume not found] [Could not find costume in directory $path]")
1513 dumpCommands()
1514 } else {
1515 println "STORING COSTUME"
1516 storeDir(path, receiveResult: {result ->
1517 def fileId = result.file.getId().toStringFull()
1518 def type = 'png'
1519 def thumb = result.properties['thumb.png']
1521 if (!thumb) {
1522 type = 'jpg'
1523 thumb = result.properties['thumb.jpg']
1525 try {
1526 println "STORED COSTUME, adding"
1527 transmitSetCloudProperty("costume/$fileId", new JSONWriter().write([thumb: thumb, type: thumb ? type : null, name: name]))
1528 } catch (Exception ex) {
1529 System.err.println "Error pushing costume..."
1530 stackTrace(ex)
1532 }, receiveException: {ex ->
1533 System.err.println("Couldn't store costume in cloud: $path")
1534 stackTrace(ex)
1538 def updateCostumeGui() {
1539 if (costumeUpdating) {
1540 println "\n\n@@@\n@@@ QUEUING COSTUME UPDATE"
1541 costumeUpdateNeeded = true
1542 return
1544 costumeUpdateNeeded = false
1545 if (!swing) return
1546 def costumesDir = new File(plexusDir, 'models')
1547 def tumes = []
1548 def needed = []
1549 def present = [] as Set
1551 cloudProperties.each('costume/(.*)') {key, value, match ->
1552 def tume = getCostume(match.group(1))
1554 tumes.add(tume)
1555 if (tume.thumb && !new File(costumesDir, "thumbs/${tume.id}.$tume.type").exists()) {
1556 needed.add(tume)
1557 } else {
1558 present.add(tume)
1561 if (needed) {
1562 println "Tumes: $tumes, Needed: $needed"
1563 def i = 0;
1565 costumeUpdating = true
1566 println "\n\n@@@\n@@@ UPDATE COSTUME GUI"
1567 serialContinuations(needed, receiveResult: {files ->
1568 costumeUpdating = false
1569 if (costumeUpdateNeeded) {
1570 println "\n\n@@@\n@@@ CHECKING QUEUED COSTUME UPDATE"
1571 updateCostumeGui()
1573 }, receiveException: {ex ->
1574 System.err.println("Error fetching thumbs for costumes: $ex")
1575 stackTrace(ex)
1576 }) {tume, chain ->
1577 //cut out if we have been superceded
1578 fetchFile(continuation(receiveResult: {file ->
1579 println "\n\n###\n### SERIAL COSTUME DOWNLOWD: $i"
1580 def thumbFile = new File(costumesDir, "thumbs/${tume.id}.${tume.type}")
1582 thumbFile.getParentFile().mkdirs()
1583 copyFile(file[0], thumbFile)
1584 present.add(tume)
1585 showTumes(tumes.findAll {present.contains(it)})
1587 chain.receiveResult(file)
1589 receiveException: {ex ->
1590 System.err.println "Error fetching thumb for costume: ${needed[i].name}..."
1591 stackTrace(files[i])
1592 }), Id.build(tume.thumb))
1594 } else {
1595 showTumes(tumes)
1598 def showTumes(tumes) {
1599 def trips = []
1601 tumes.sort {a,b -> a.name.compareTo(b.name)}
1602 if (tumes != this.tumes) {
1603 this.tumes = tumes
1604 swing.doLater {
1605 def sel = tumesCombo.selectedItem
1607 tumesCombo.removeAllItems()
1608 tumesCombo.addItem('')
1609 tumes.each {
1610 tumesCombo.addItem(it.name)
1612 tumesCombo.selectedItem = sel
1615 tumes.each {c-> trips.add([c.name, c.thumb ? "${c.id}.$c.type" : '', c.id])}
1616 dumpCostumeSelections(trips)
1618 //varval = [do [result $@arg1]]
1619 //x3 = hello; v = 3; echo (varval (concatword x $v))
1621 * name, thumb path, costume id
1623 def dumpCostumeSelections(triples) {
1624 def guitext = ''
1626 println "COSTUME SELS: $triples"
1627 guitext += 'showcostumesgui = [showgui Costumes];'
1628 guitext += 'alias showcostume [ guibar; guiimage (concatword "packages/plexus/models/thumbs/" (get $costumenames [[@@guirollovername]])) $guirolloveraction 4 1 "packages/plexus/dist/tc_logo.jpg"];'
1629 guitext += "alias costumenames ["
1630 for (trip in triples) {
1631 guitext += " [${trip[0]}] ${trip[1] ?: '[]'}"
1633 guitext += " ];"
1634 guitext += 'newgui Costumes [ \n guilist [ \n guilist [ \n'
1635 def i = 0, needClose = true, last = triples.size()
1636 def wrapAfter = 12
1637 for (trip in triples) {
1638 guitext += "guibutton [${trip[0]}] [remotesend useCostume ${trip[0]} ${trip[2]}];"
1639 if (++i % wrapAfter == 0) {
1640 guitext += '] \n showcostume \n ] \n'
1641 needClose = false
1642 if (i != last) {
1643 def s = Math.min(i, last - 1), e = Math.min(i + wrapAfter, last - 1)
1644 s = Character.toUpperCase((triples[s][0]) as char)
1645 e = Character.toUpperCase((triples[e][0]) as char)
1646 guitext += " guitab [More $s-$e] \n guilist [ \n guilist [ \n"; needClose = true
1650 if (needClose) guitext += '] \n showcostume \n ] \n'
1651 guitext += 'guitab "Upload"; guifield costume_push_name [] \n guibutton [Import Costume] [remotesend pushCostume $costume_push_name] \n ] '
1652 sauer('gui', cvtNewlines(guitext))
1653 dumpCommands()
1655 def useCostume(name, dirId) {
1656 println "name $name dir $dirId"
1657 if (!name) {
1658 if (costume != null) {
1659 costume = null
1660 updateMyPlayerInfo()
1661 sauer('cost', "playerinfo p0 [${LaunchPlexus.props.guild}] mrfixit")
1662 dumpCommands()
1663 selectCostume()
1665 return
1667 if (costume != dirId) {
1668 def costumeDir = new File(plexusDir, "models/$dirId")
1670 fetchDir(dirId, costumeDir, receiveResult: {
1671 costume = dirId
1672 updateMyPlayerInfo()
1673 sauer('cost', "playerinfo p0 [${LaunchPlexus.props.guild}] ${costumeDir.getName()}")
1674 dumpCommands()
1675 println "USE COSTUME $name ($costumeDir)"
1676 selectCostume()
1677 }, receiveException: {ex ->
1678 System.err.println("Couldn't use costume: $name: ex")
1679 stackTrace(ex)
1683 def selectCostume() {
1684 tumesCombo.selectedItem = costume ? getCostume(costume)?.name ?: '' : ''
1686 def clearPlayers() {
1687 //println "Going to clear players"
1688 names = [p0: peerId]
1689 ids = [(peerId): 'p0']
1690 updateMappingDiag()
1692 def subscribe(topicId, cont) {
1693 def topic = peer.subscribe(topicId, cont)
1695 exec {
1696 updatePastryDiag()
1698 return topic
1700 def unsubscribe(topic) {
1701 peer.unsubscribe(topic)
1702 exec {
1703 updatePastryDiag()
1706 def connectWorld(id) {
1707 if (id) {
1708 def map = getMap(id)
1710 if (!map) {
1711 sauer('entry', "tc_msgbox [Couldn't find map] [Unknown map id: $id]")
1712 } else if (map.id != mapTopic?.getId()?.toStringFull()) {
1713 cachedPlayerLocations = [:]
1714 println "CONNECTING TO WORLD: $map.name ($map.id)"
1715 clearPlayers()
1716 if (mapTopic) {
1717 unsubscribe(mapTopic)
1719 loadMap(map.name, map.dir, continuation(receiveResult: {
1720 subscribe(Id.build(id), continuation(receiveResult: {topic ->
1721 exec {
1722 mapTopic = topic
1723 mapIsPrivate = map.privateMap
1724 updateMyPlayerInfo()
1725 selectMap()
1726 anycast(["sendPlayerLocations"])
1729 receiveException: {exception -> err("Couldn't subscribe to topic: ", exception)}))
1731 receiveException: {ex ->
1732 System.err.println("Trouble loading map: $ex")
1733 stackTrace(ex)
1736 } else {
1737 if (mapTopic) {
1738 cachedPlayerLocations = [:]
1739 unsubscribe(mapTopic)
1740 mapTopic = null
1741 sauer('limbo', "tc_loadmsg Limbo; map plexus/dist/limbo/map")
1742 dumpCommands()
1743 updateMyPlayerInfo()
1747 def receivePlayerLocation(id, update) {
1749 def pushMap(privateMap, String... nameArgs) {
1750 println "pushMap: [$nameArgs]"
1751 def name = nameArgs.join(' ')
1752 def newMap = name?.length()
1754 if (newMap || mapTopic) {
1755 def map = mapTopic ? getMap(mapTopic.getId().toStringFull()) : null
1757 if (!newMap) {
1758 name = map.name
1760 println "1"
1761 def cont = continuation(receiveResult: {result ->
1762 def topic = newMap ? peer.randomId().toStringFull() : map.id
1763 def id = result.file.getId().toStringFull()
1765 transmitSetCloudProperty("${privateMap == '1' ? 'privateMap' : 'map'}/$topic", new JSONWriter().write([dir: id, name: name]))
1767 receiveException: {ex ->
1768 exec {
1769 System.err.println "Error pushing map..."
1770 stackTrace(ex)
1774 if (mapname ==~ 'plexus/.*/map') {
1775 println "PLEXUS"
1776 def mapdir = new File(sauerDir, "packages/$mapname").getParentFile()
1778 println "store"
1779 storeDir(cont, mapdir)
1780 } else {
1781 println "sauer"
1782 def prefix = (new File(mapname).parent ? new File(sauerDir, "packages/$mapname") : new File(sauerDir, "packages/base/$mapname")).getAbsolutePath()
1783 def dirmap = ['map.ogz': new File(prefix + ".ogz")]
1784 def thumbJpg = new File(prefix + ".jpg")
1785 def thumbPng = new File(prefix + ".png")
1786 def cfg = new File(prefix + ".cfg")
1787 def groovyPfx = "${mapname}."
1789 new File(prefix).parentFile.eachFileMatch(~"$mapname\\..*groovy") {
1790 println "FOUND GROOVY FILE: $it"
1791 dirmap["map.${it.name.substring(groovyPfx.length())}"] = it
1793 if (thumbJpg.exists()) {
1794 dirmap['map.jpg'] = thumbJpg
1796 if (thumbPng.exists()) {
1797 dirmap['map.png'] = thumbPng
1799 if (cfg.exists()) {
1800 dirmap['map.cfg'] = cfg
1802 println "store"
1803 storeDir(cont, dirmap)
1805 } else {
1806 sauer('msg', "tc_msgbox [Error] [No current map]")
1809 def activePortals(ids) {
1810 ids = ids as Set
1811 portals.keySet().retainAll(ids)
1812 for (id in ids) {
1813 if (!portals.containsKey(id)) {
1814 portals[id] = ""
1818 def bindPortal(id, topic) {
1819 portals[id] = topic
1820 if (cloudProperties["map/$topic"]) {
1821 println "bindPortal: portal_$id = ${getMapName(getMapEntry(topic))}"
1822 sauer('portal', "portal_$id = ${getMapName(getMapEntry(topic))}")
1823 dumpCommands()
1826 def firePortal(id) {
1827 println "portals: $portals, id: $id, portals[id]: ${portals[id]}"
1828 if (portals[id]) {
1829 connectWorld(portals[id])
1832 def createPortal(name, id) {
1833 def triggers = [] as Set
1835 for (def i = 0; i < portals.size(); i++) {
1836 triggers.add(31000 + i)
1838 triggers.removeAll(portals.keySet())
1839 def trigger = triggers.isEmpty() ? 31000 + portals.size() : triggers.iterator().next()
1840 portals[trigger as String] = id
1841 println "createPortal portal_$trigger = $name; portal $trigger"
1842 sauer('portal', "portal_$trigger = $name; portal $trigger")
1843 dumpCommands()
1844 saveGroovyData()
1846 def saveGroovyData() {
1847 def cfg = new File(mapname ==~ '.*/.*' ? new File(sauerDir, "packages") : new File(sauerDir, "packages/base"), mapname + ".cfg")
1848 def txt = ""
1850 if (cfg.exists()) {
1851 txt = cfg.getText()
1852 def pos = txt =~ '(' + Prep.MARKER + '\n)[^\n]*(\n|$)'
1854 if (pos.find()) {
1855 txt = txt.substring(0, pos.start(1)) + txt.substring(pos.end(2))
1858 if (portals) {
1859 txt += Prep.MARKER
1860 txt += "remotesend bindPortals ["
1861 for (portal in portals) {
1862 txt += "$portal.key $portal.value"
1864 txt += "];findPortals"
1866 cfg.write(txt)
1868 def playerUpdate(id, name, update) {
1869 cachedPlayerLocations[id] = [name, update]
1870 if (id == followingPlayer?.id) {
1871 def values = [:]
1872 def format = []
1874 for (def i = 0; i < update.length; i += 2) {
1875 values[update[i]] = update[i + 1]
1877 values.x = (Double.parseDouble(values.x) - 20) as String
1878 values.y = (Double.parseDouble(values.y) - 20) as String
1879 values.each {
1880 format.add(it.key)
1881 format.add(it.value)
1883 sauer('follow', "tc_setinfo p0 ${format.join(' ')}")
1884 dumpCommands()
1885 broadcast(["update $name ${format.join(' ')}"])
1888 def updateDownloads() {
1889 if (!headless) {
1890 downloadCountField.text = downloads as String
1891 uploadCountField.text = uploads as String
1894 def clearUploadProgress() {
1895 if (!headless) {
1896 if (swing) {
1897 swing.edt {
1898 downloadProgressBar.setValue(0)
1901 exec {
1902 sauer('up', 'tc_piechart_image = ""')
1903 dumpCommands()
1907 def showUploadProgress(cur, total) {
1908 if (!headless) {
1909 if (swing) {
1910 swing.edt {
1911 downloadProgressBar.setMaximum(total)
1912 downloadProgressBar.setValue(cur)
1915 exec {
1916 def x = total > 0 ? Math.round(cur/total*16.0) : 0
1917 sauer('up', 'tc_piechart_image = "packages/plexus/dist/ul_' + x + '.png"')
1918 dumpCommands()
1922 def clearDownloadProgress() {
1923 if (!headless) {
1924 if (swing) {
1925 swing.edt {
1926 downloadProgressBar.setValue(0)
1929 exec {
1930 sauer('up', 'tc_piechart_image = ""')
1931 dumpCommands()
1935 def showDownloadProgress(cur, total) {
1936 if (!headless) {
1937 if (swing) {
1938 swing.edt {
1939 downloadProgressBar.setMaximum(total)
1940 downloadProgressBar.setValue(cur)
1943 exec {
1944 def x = total > 0 ? Math.round(cur/total*16.0) : 0
1945 sauer('up', 'tc_piechart_image = "packages/plexus/dist/dl_' + x + '.png"')
1946 dumpCommands()