Compare commits

..
Author SHA1 Message Date
Daniel 4b7ad9b8b8 Split install command onto multiple lines 2018-04-25 09:28:21 +02:00
Daniel f63fd20179 Merge pull request #76 from psihius/dev
README.md update
2018-04-23 10:53:08 +02:00
psihius 8cc52663e5 Adjusted readme install string - nodejs 9 and added python-dev and build-essential packages as those can be missing on some cloud images 2018-04-23 10:56:52 +03:00
Danielv123 8f2d22cbd8 Only wrote files on the server, might resolve some crashes 2018-04-21 08:05:40 +02:00
psihius bf9886d7ee Merge branch 'dev' of https://github.com/Danielv123/factorioClusterio into fork-dev 2018-04-19 23:33:27 +03:00
psihius 03511fc240 Hardened NaN checks, added file read delay for research-sync so factorio has time to finish writing to the file 2018-04-19 23:33:09 +03:00
Danielv123 2111e337c9 Fix crash when starting client.js without master 2018-04-19 18:41:30 +02:00
Danielv123 0672004cbd researchSync: Partially fixed failing test (it still fails) 2018-04-19 17:51:46 +02:00
psihius ed9b4bbafa Changed research sync to work with /api/slaves so the instance can ignore it's own research tree and enhanced research enabling with proper messages and sound alert, added checks so we don't flip values that haven't changed 2018-04-19 18:29:05 +03:00
psihius 9a66542270 Fixed so local research is never disabled 2018-04-18 23:27:53 +03:00
psihius 1c8deea2dc Fixed research sync for levels and repeated execution 2018-04-18 10:38:44 +03:00
psihius 19913af056 Changed research transfer data from array to objects 2018-04-17 23:37:34 +03:00
psihius 718315849c Made sure data is converted to integers 2018-04-17 22:44:56 +03:00
psihius 1731426f49 Removed unnesecesary json parsing and adjusted the expression for better style 2018-04-17 22:07:18 +03:00
psihius bb06723918 Changed some research fields and added a check remoteResearch key existence 2018-04-17 21:48:34 +03:00
psihius 0edb4caff9 Typo fix 2018-04-17 14:26:09 +03:00
psihius c7cd922541 Updated research dump and enabling with a fix for infinite-research 2018-04-17 13:57:36 +03:00
Danielv123 0dee243bf9 crash fix 2018-04-16 22:16:22 +02:00
Danielv123 2ab812d5a9 prometheus track count of items in inventory 2018-04-16 10:49:09 +02:00
Danielv123 5076b4e8e1 Increased slave database limits due to demands from researchSync 2018-04-16 08:48:54 +02:00
Danielv123 b7b0dd9d85 prometheus track playercounts 2018-04-16 08:35:10 +02:00
Danielv123 cf7af19370 Don't send corrupt factorio file outputs to plugins 2018-04-16 06:43:22 +02:00
Danielv123 4d5b3b855c Export production stats to prometheus 2018-04-15 23:04:35 +02:00
Danielv123 2b5cbf00e4 Fix error ion /nodeDetails sometimes making the display weird 2018-04-15 21:25:19 +02:00
Danielv123 aeeb09a975 Fixed weird thing about rebinding 'this' 2018-04-15 21:15:58 +02:00
Danielv123 7915b71e10 typo 2018-04-15 21:07:49 +02:00
Danielv123 2c2bbb71b4 inventoryImports: Fixed chat errors when players are dead 2018-04-15 12:02:46 +02:00
Danielv123 6c966e2c29 fixed issue with server browser token not being applied 2018-04-14 11:18:06 +02:00
Danielv123 6bbd6b8717 researchSync: Fixed crash when master contact is lost 2018-04-14 11:17:38 +02:00
Danielv123 1d0d9765bd researchSync: Fixed importing research 2018-04-13 20:18:47 +02:00
Danielv123 c9bd26a01e researchSync: Fixed sending commands with newlines 2018-04-13 18:31:01 +02:00
Danielv123 2d5c0c3bbf More tests for remoteMap 2018-04-13 16:37:57 +02:00
Danielv123 f131a83103 moved isFactorioCommand, more tests, researchSync stability 2018-04-13 12:32:09 +02:00
Danielv123 181cb1179c Fixed stack size exceeded bug 2018-04-13 10:35:55 +02:00
Danielv123 a8dfd22eeb . 2018-04-13 09:12:46 +02:00
Danielv123 23d31435d7 Fixed bug in remoteCommands 2018-04-13 09:08:02 +02:00
Danielv123 c8f2a67845 researchSync: Tried to avoid errors when research doesn't exist 2018-04-12 07:49:40 +02:00
Danielv123 7a4ea69cbb researchSync removed node-lua dependency 2018-04-11 21:38:28 +02:00
Danielv123 12bf445579 Added endpoint docs for endpoints introduced with researchSync 2018-04-11 15:39:25 +02:00
Danielv123 51e41835ee Merge branch 'dev' into pr/71 2018-04-11 15:22:42 +02:00
Danielv123 3af343c5a0 Merged researchSync 2018-04-11 15:20:15 +02:00
Danielv123 2daddde511 Improved auto generated endpoint documentation 2018-04-09 11:40:39 +02:00
Danielv123 43c1f2ae93 remoteCommands added plugin and master API endpoint for executing things 2018-04-09 11:25:57 +02:00
Danielv123 be49deb170 Changed import delay from 3 to 1 second 2018-04-09 09:14:15 +02:00
Danielv123 6b29b2a430 Autodownload latest clusterio on npm install 2018-04-09 09:13:17 +02:00
Danielv123 fe2bf75b89 Fixed issue where oldest version of mod is downloaded thanks to changes in undocumented factorio API 2018-04-04 16:32:51 +02:00
Danielv123 26483d63d3 remoteMap draw better trees 2018-04-02 18:27:36 +02:00
Danielv123 50d14394a6 Fixed error occasionally crashing client when plugins use lots of IO 2018-04-02 14:59:58 +02:00
Danielv123 cd4887e5cf Fixed getMac and launching instance with no mods 2018-04-02 12:14:06 +02:00
Danielv123 cbee07e451 No more external dependencies in web interface 2018-03-30 15:48:37 +02:00
Danielv123 f697d2a74c Multithreaded createMap, improved isValidInstance reliablity 2018-03-30 14:39:39 +02:00
Danielv123 507c0afa98 factorioServer relative paths instead of absolute 2018-03-30 11:07:26 +02:00
Danielv123 d9d8905d02 , 2018-03-29 19:42:19 +02:00
Danielv123 dfa5d9ad16 still trying to fix paths 2018-03-29 19:31:56 +02:00
Danielv123 9a0ce97746 factorioServer: Tried adding function for map creation, failed. 2018-03-29 14:40:07 +02:00
Danielv123 e55bf71431 fileOps: Fixed crash on invalid filenames, added test for it 2018-03-29 14:39:39 +02:00
Danielv123 e1ab5b4d91 More functionality in factorioServer and added unit tests 2018-03-28 21:00:38 +02:00
Danielv123 fff1989897 Moved fileOps to _app/fileOps 2018-03-28 20:57:57 +02:00
Danielv123 802c21f41e started work on extracting server launching and IO systems 2018-03-27 20:50:10 +02:00
Danielv123 e694593fdb remoteMap yellow transport belts 2018-03-26 16:15:26 +02:00
Danielv123 0aa25544b0 remoteMap prescan spawn and also scan newly generated chunks 2018-03-26 16:11:56 +02:00
Danielv123 2ab163ab68 remoteMap don't error on broken image links 2018-03-26 16:09:15 +02:00
Danielv123 486890480d Improved reliablity of plugins that burst write 2018-03-26 15:27:52 +02:00
Danielv123 47637f5511 remoteMap animated radar 2018-03-26 14:03:57 +02:00
Danielv123 fb3d27abef remoteMap added grass background 2018-03-26 13:25:03 +02:00
Danielv123 d0444ae00d remoteMap fixed animated belt alignements (not yellow) 2018-03-26 12:37:16 +02:00
Danielv123 8a1f440485 Added more prometheus monitoring to websockets 2018-03-26 11:03:28 +02:00
Danielv123 465ab9aeb3 master Added performance and usage logging for prometheus 2018-03-04 20:00:55 +01:00
Danielv123 dc62e714d6 remoteMap animated belts (yellows are bugged) 2018-03-03 21:58:31 +01:00
Danielv123 0b07698b78 remoteMap More high res entities 2018-03-02 00:03:42 +01:00
Danielv123 d1c2816669 remoteMap chem plant, oil refinery, splitters, underneathies, inserters 2018-02-28 23:50:57 +01:00
Danielv123 3499411290 remoteMap stone/steel furnace scaling, inserter rotations 2018-02-28 21:51:59 +01:00
Danielv123 e6d734d4eb remoteMap place rotated items, fix selection box offset 2018-02-28 18:01:34 +01:00
Danielv123 f5eef7c1bb remoteMap placing and removing entities 2018-02-26 23:28:51 +01:00
Danielv123 4b286f7b08 remoteMap mouse selection overlay 2018-02-26 11:26:29 +01:00
Danielv123 7f6f1d07d7 remoteMap resolve issue with shrinking viewport when having low FPS 2018-02-25 21:58:00 +01:00
Danielv123 619b7972b9 remoteMap move speed setting and wasd controls 2018-02-25 19:42:56 +01:00
Danielv123 909fc79bd2 remoteMap smooth and diagonal scrolling 2018-02-25 18:26:46 +01:00
Danielv123 6476c9e68a Ignored steam in /nodeDetails prod graph 2018-02-25 14:22:42 +01:00
Danielv123 eae1a76341 remoteMap: 60fps, option to pause when window is deselected to save CPU 2018-02-25 14:08:30 +01:00
Danielv123 551a2ea6d7 Fixed failing tests and clutter generated by chunkStore test 2018-02-25 13:16:59 +01:00
Danielv123 cde0d3265b Fixed remoteMap not deleting things properly 2018-02-25 12:41:15 +01:00
Danielv123 72d5f06381 Nuking forrests with remoteMap no longer produces lagspikes 2018-02-25 12:40:52 +01:00
Danielv123 dd9130dc8b Fixed remoteMap perf issues by making map and tilesize config (and tiny) 2018-02-25 00:09:24 +01:00
theCatsPaw d1a295ae4e researchsync: every server can now research 2018-02-23 23:02:44 +01:00
Danielv123 718c807699 Updated gitignore 2018-02-22 21:43:17 +01:00
Danielv123 c4fb313aef Added time estimate etc to the github mod downloading 2018-02-22 21:37:31 +01:00
Danielv123 1fcd91342f Made progress bar for github mod downloads 2018-02-22 21:00:35 +01:00
Danielv123 29bffa472b Fixed modManager add and remove functions 2018-02-22 19:13:47 +01:00
theCatsPaw 8935d03b4a researchSync: client 2018-02-21 22:26:33 +01:00
theCatsPaw e64db6b236 researchSync: master 2018-02-21 20:54:02 +01:00
Danielv123 64a29c004c Fixed modManager to work in 0.16 2018-02-18 19:50:35 +01:00
Danielv123 9e63086d6d Fixed more remoteMap things, I think it was rotations or something? 2018-02-18 18:17:20 +01:00
Danielv123 6f2262de52 Using spritesheets for more entities 2018-02-06 13:11:35 +01:00
Danielv123 d5d02aa80d Using real textures of belts from spritesheet instead of icons 2018-02-05 17:51:06 +01:00
Danielv123 1a434b0537 Made combinator worldID setting silent 2018-02-05 17:49:12 +01:00
Danielv123 fc874b4e4e Send worldID to circuits 2018-01-30 07:53:28 +01:00
Danielv123 f5fa8a4ca9 Cache getMac 2018-01-30 07:53:11 +01:00
Danielv123 7b661781f2 ws combinators now working 2018-01-19 22:02:50 +01:00
Danielv123 1ab3c49772 Attempted to fix combinator transmission 2018-01-19 20:30:15 +01:00
Danielv123 501807884f Fixed rebroadcasting every frame up to 500 times 2018-01-18 18:00:33 +01:00
Danielv123 b2afea666e initial attempt at converting combinators to use websockets 2018-01-18 17:42:22 +01:00
Danielv123 fdd8a8299b Fixed SSL test failing 2018-01-15 17:16:14 +01:00
Danielv123 0688184aaf Made sure all web dependencies load over HTTPS 2018-01-14 22:20:53 +01:00
Danielv123 864395c8aa Fixed private key generation 2018-01-14 22:17:25 +01:00
Danielv123 a307cd63a3 Auto generate ssl cert and download spritesheet on npm install 2018-01-14 21:50:47 +01:00
Danielv123 071a2e6dbe Added function to generate SSL certificate (unused, as of yet) 2018-01-14 21:39:45 +01:00
Danielv123 322a1711ab Cleaned up remoteMap drawing code using ES6 modules 2018-01-14 00:14:44 +01:00
Danielv123 ced0aa4522 remoteMap proper size rendering for chemical-plant 2018-01-13 15:04:18 +01:00
Danielv123 712b33b4a7 #59 improved helptext for new features 2018-01-11 22:07:18 +01:00
Danielv123 4b0bedbacf #59 fixed downloading from github repos using 'github releases' 2018-01-11 21:58:17 +01:00
Danielv123 5d72832402 #59 added node client.js manage mods shared/instance remove modname 2018-01-11 21:44:23 +01:00
Danielv123 5eaa94f4ac remoteMap added proper scaling to radar 2018-01-10 11:13:18 +01:00
Danielv123 1d92fe21c9 Added solar-panel and accumulator to remoteMap with correct sizes 2018-01-09 11:55:56 +01:00
Danielv123 9ce7cf5f28 Fixed typo (maybe) 2018-01-09 11:33:14 +01:00
Danielv123 68b0d3b75a Untested code to draw entities with correct sizes (hardcoded sizes) 2018-01-09 11:27:46 +01:00
Danielv123 b5a9ac3212 Fixed remoteMap trying to connect to localhost 2018-01-08 15:36:50 +01:00
Danielv123 625f88c63e node client.js manage [instancename] mods download [modname] 2018-01-08 15:01:10 +01:00
Danielv123 4d3924b339 . 2018-01-07 13:49:34 +01:00
Danielv123 a182a1525d protected nodePackage type plugins against missing script-output files 2018-01-07 13:48:24 +01:00
Danielv123 605d58443d Removed unused dependencies 2018-01-07 11:38:19 +01:00
Danielv123 2efe339fef Fixed some folders not getting created on fresh installs, throwing a few errors 2018-01-07 10:57:12 +01:00
Danielv123 618abb0ab9 1.7.1 2018-01-05 21:33:06 +01:00
Danielv123 66a583ad29 removed logging from remoteMap 2018-01-05 21:32:21 +01:00
Danielv123 894d5c467f 1.7.0 2018-01-05 21:28:58 +01:00
Danielv123 0f246ca9d3 #59 added error handling when not having put token in config.json 2018-01-05 21:26:22 +01:00
Danielv123 52cc98cc95 #61 fixed remoteMap deletions 2018-01-05 21:25:26 +01:00
Danielv123 a78a80f395 Prevented race condition in test from affecting result 2018-01-04 19:58:42 +01:00
Danielv123 8029067cb2 #61 un-inverted map movement keys 2018-01-04 19:46:10 +01:00
Danielv123 618e62de6e Fixed client typo causing crash and tests failing on CI 2018-01-04 19:28:03 +01:00
Danielv123 3f8a685f19 resolves #66 added setting to not upload mods to master 2018-01-04 18:54:30 +01:00
Danielv123 5dc71ad46e Setting to reduce verbosity of logs (don't log item transfers) 2018-01-04 18:50:09 +01:00
Danielv123 df60455d5e #61 improved master performance and socket cleanup 2018-01-04 18:42:26 +01:00
Danielv123 b9912f09f9 #61 Link to remoteMap on /slaves and /nodeDetails if LUAmod is detected 2018-01-04 18:22:37 +01:00
Danielv123 84cded0cf9 #61 dynamically load more map when moving around 2018-01-04 15:08:45 +01:00
Danielv123 0b00f065f7 #61 Pan map with WASD 2018-01-04 14:43:25 +01:00
Danielv123 0c365c0090 #61 Render entities with rotations
This code didn't even error once, it just worked. Which is nice.
2018-01-04 14:43:01 +01:00
Danielv123 51bb9f4d71 Live updates to map as things are placed or removed 2018-01-03 18:34:27 +01:00
Danielv123 9ae20886b3 Entity based mapping has gotten to the renderer 2018-01-03 14:17:33 +01:00
Danielv123 1b405b8701 Refactored chunkStore to use linvodb3, broke mapping (ofc) 2018-01-02 17:00:50 +01:00
Danielv123 cd2d00d224 Merge branch 'master' into dev 2017-12-28 11:58:37 +01:00
Danielv123 0feaab8314 Cleaned up readme, added TOC, introduction and command cheatsheet 2017-12-28 11:58:14 +01:00
Danielv123 be5dfdf38b Merge branch 'master' into dev 2017-12-28 11:46:04 +01:00
Danielv123 853b7d1026 Attempted to fix remoteMap race condition, not working yet 2017-12-16 14:26:20 +01:00
Danielv123 f9db49734c Cleaner test output, better promise error logging 2017-12-16 14:25:38 +01:00
Danielv123 2c38c37c25 Fixed tests failing without good reason 2017-12-14 10:58:51 +01:00
Danielv123 0df3c0c14f Better error handling in remoteMap 2017-12-12 21:43:03 +01:00
Danielv123 51c644203e #61 Basic canvas rendering is now working 2017-12-11 17:44:11 +01:00
Danielv123 eba982836c #61 Remote map chunk storage and equesting working, waiting for renderer 2017-12-10 20:55:53 +01:00
Danielv123 8fce9a2cfe #61 chunkStore is getting filled/emptied on demand 2017-12-10 14:07:37 +01:00
Danielv123 b1f35c9b72 Fixed UPSdisplay 2017-12-10 11:38:56 +01:00
Danielv123 44b68c92d5 Big changes to plugin system, fixed inventoryImports 2017-12-10 11:27:02 +01:00
Danielv123 36ccf22e02 More informative error when master is unavailable 2017-12-09 14:49:11 +01:00
Danielv123 7682a51437 More informative error when launching instance without savefile 2017-12-09 14:25:56 +01:00
Daniel 1d790388c2 Updated install command 2017-12-09 14:21:37 +01:00
Danielv123 f522f19cb5 fixed #65 2017-12-09 14:05:17 +01:00
Danielv123 9496fa70e0 #61 without ES6 features to support node 6.10 2017-12-03 18:51:31 +01:00
Danielv123 132c7999f3 Merge remote-tracking branch 'origin/master' into dev 2017-12-03 18:37:28 +01:00
Daniel e3e255bbb0 #61 might be fixed now 2017-12-03 18:30:40 +01:00
Danielv123 566d61755e Fixed inventoryCombinator attempting to output 2360791303, resolves #62
Because it was above 2^31
2017-11-29 09:07:21 +01:00
Danielv123 ffe2df55c4 #61 added webpage to master, some more plugin work 2017-11-29 09:02:11 +01:00
Danielv123 227ac87018 housekeeping 2017-11-29 09:01:27 +01:00
Danielv123 cdddfe6e30 #61 Completed chunkStore for now, time to work on filling it 2017-11-22 18:03:20 +01:00
Danielv123 e2a795528f Change name from slaveID to instanceID for consistency accross endpoints 2017-11-21 21:39:41 +01:00
Danielv123 7ece1af073 #61 start of map store/retrieve, communication channel using websockets 2017-11-21 11:06:57 +01:00
Danielv123 2bcacb2513 #61 extracting place data from game
Still need to extract remove data in the LUA portion and store it
2017-11-20 10:29:52 +01:00
Danielv123 dc0a722e3d More docs 2017-11-19 13:02:24 +01:00
Danielv123 6ecdde8cbc resolves #60 sharedMods doesn't affect instance mods
Might have broken backwards compat???
2017-11-15 12:55:49 +01:00
Danielv123 682856c0a5 Implemented editing configs using node client.js manager #59 2017-11-14 13:06:57 +01:00
Danielv123 1dd58c27e0 undotted 2017-11-14 12:54:22 +01:00
Danielv123 e007ffd30a . 2017-11-13 21:57:18 +01:00
Danielv123 8146a313f3 #59 interface for editing config + more docs 2017-11-11 23:51:52 +01:00
Danielv123 906b192683 More work on command line tools #59 2017-11-11 16:40:19 +01:00
Danielv123 bd1304365c 1.6.0 2017-11-10 16:51:30 +01:00
Danielv123 eca8723506 Sorted documentation better, "npm run livedocs" to browse the docs 2017-11-09 17:09:59 +01:00
Danielv123 9662df356a Fix crash due to leftover code
F
2017-11-08 22:30:47 +01:00
Danielv123 7d7c2515f4 minor locale fix 2017-11-08 22:26:28 +01:00
Danielv123 490f039d83 Yes, its more JSDOCs 2017-11-08 16:47:32 +01:00
Danielv123 02dc688161 Documented more things with JSDOC 2017-11-07 16:29:43 +01:00
Danielv123 96d60e5428 Attempted to add some JSDOC docs, npm run docs/livedocs 2017-11-06 23:37:21 +01:00
Danielv123 2fd47434bb Added search functionality to mod manager #59 2017-11-06 21:12:25 +01:00
Danielv123 88ff90262d Updated default config 2017-11-06 21:09:19 +01:00
Danielv123 a3195ad8ce More details in mod list 2017-11-06 20:00:19 +01:00
Danielv123 8d63bb896a Big charting performance increase for master 2017-11-06 19:53:12 +01:00
Danielv123 b016f36667 Fixed graph on /nodeDetails 2017-11-05 21:18:59 +01:00
Danielv123 7046ba58d3 Import/export line on same graph on storage page to save space 2017-11-05 21:11:12 +01:00
Danielv123 0735aa2db4 More work on listing mods nicely with node client.js manager 2017-11-05 17:50:14 +01:00
Danielv123 c901275f38 'node client.js manage shared list' now lists shared mods 2017-11-04 20:07:25 +01:00
Danielv123 f507b503e9 Updated packages 2017-11-03 23:05:19 +01:00
Danielv123 8cf85f3ecc More mod manager stuff, node client.js list is more informative
Now shows nice table with name and port to connect to
2017-11-03 22:54:52 +01:00
Danielv123 1eeb289669 Coverage % now includes new untested modManager 2017-11-02 21:29:21 +01:00
Danielv123 c3b1e66646 #59 Starting work on the mod manager 2017-11-01 21:48:55 +01:00
Danielv123 b847b1a934 1.5.0 2017-11-01 20:06:25 +01:00
Danielv123 be7fdb692f Cleaned up logs on slave to be more informative 2017-11-01 18:59:08 +01:00
Danielv123 e75931c4f3 Improved place stats identification 2017-11-01 18:53:39 +01:00
Danielv123 01b317006c Made some slow tests run in parallel 2017-11-01 16:56:57 +01:00
Danielv123 2319ce01a2 Fixed UPSdisplay 2017-11-01 16:56:37 +01:00
Danielv123 cfbf281b42 #58 added even more tests for /api/remove 2017-10-31 20:48:06 +01:00
Danielv123 7185bd4fe1 More consistent return values for /api/remove 2017-10-31 20:47:42 +01:00
Danielv123 ea603c61fd #58 added test for /api/remove 2017-10-31 20:04:59 +01:00
Danielv123 0dd6315abc Fixed export statistics being logged under "unknown" 2017-10-29 23:37:46 +01:00
Danielv123 bfbe7e7d1c Added tests for /api/inventory and /api/place 2017-10-29 23:09:37 +01:00
Danielv123 42e2816b56 Better title for storage graphs 2017-10-29 12:29:16 +01:00
Danielv123 fe704b46e9 Itemstats on storage page respects offlineSlaves setting 2017-10-29 12:22:50 +01:00
Danielv123 a7474fdb86 Give processes a better name, clusterioMaster and clusterioClient 2017-10-29 09:37:13 +01:00
Danielv123 869b5f9ced sentItemStats for master is now working as well 2017-10-28 22:13:40 +02:00
Danielv123 3d4c788788 recievedItemStats for master is working 2017-10-27 09:23:18 +02:00
Danielv123 04aba1eef1 Added debug tools for adding/removing items 2017-10-26 21:17:59 +02:00
Danielv123 f886fe3e95 Updated averaged-timeseries module 2017-10-26 21:07:30 +02:00
Danielv123 1510c86944 1.4.0 2017-10-26 09:01:06 +02:00
Danielv123 15f3e88408 Merge remote-tracking branch 'origin/master' into storageIOstats 2017-10-25 11:25:05 +02:00
Danielv123 a1db24bd91 Added the first tests for master.js 2017-10-25 11:24:57 +02:00
Danielv123 0e1a7cdb4a Made index confirm to HTML5 spec 2017-10-25 11:24:35 +02:00
Daniel 598c2961a8 Updated badges 2017-10-24 22:00:13 +02:00
Danielv123 e91c25557e Data collection related to storage IO and graphing 2017-10-22 15:03:29 +02:00
Danielv123 994bd09fa7 Increased statistics performance by lowering rate, untested :'( 2017-10-22 15:00:48 +02:00
Danielv123 d858f1da69 Final fix for crash on closing slave related to getMac 2017-09-19 19:51:00 +02:00
Danielv123 c7744b5189 Gather more information about who adds stuff to the cluster 2017-09-19 19:48:50 +02:00
Danielv123 339f38c1b0 Moved string hashing function away from main files 2017-09-19 08:35:55 +02:00
Danielv123 27665b8e93 More travis stuff 2017-09-18 16:15:22 +02:00
Danielv123 43f1e89e89 Tell travis to download factorio 2017-09-18 14:41:44 +02:00
Danielv123 bb1eef93c2 Changed test commands 2017-09-18 14:37:10 +02:00
Danielv123 1538235e0f Added travis-ci builds maybe? 2017-09-18 14:36:52 +02:00
Danielv123 1b0da523ca Added coverage badge 2017-09-18 09:28:45 +02:00
Danielv123 f9a0703725 Fixed inventory combinator 2017-09-17 16:45:06 +02:00
Danielv123 201912fdea Use linvodb3 for storing flows to save memory 2017-09-17 08:47:57 +02:00
Danielv123 43656f931e Added fluid barrel graphics to inventory display 2017-09-17 07:52:18 +02:00
Danielv123 b06c61b5a9 . 2017-09-17 07:44:01 +02:00
Danielv123 d6ce94176b Someone changed the name of the power armor mk2 to MK2
These wiki guys, can't they ever decide?
2017-09-17 07:42:23 +02:00
Danielv123 6a251be318 Attempted to fix imports 2017-09-16 23:51:24 +02:00
Danielv123 10e585b307 New item database working 2017-09-16 16:47:33 +02:00
Danielv123 8742e34d9f Tried to fix occasional crash when saving related to getMac 2017-09-16 16:47:16 +02:00
Danielv123 08143b7b84 Fixed database corruption problems 2017-09-16 15:55:28 +02:00
Danielv123 2a07591173 Stop throwing errors 2017-09-16 15:55:14 +02:00
Danielv123 39462e925c Changed masters item storage engine 2017-09-16 10:57:11 +02:00
Daniel 2aa3900ceb Update README.md 2017-09-12 21:51:35 +02:00
Danielv123 dcd46acdf2 resolves #57, issue was related to the way DDRs dole factor works.
I hope it works at least.
2017-09-12 09:13:52 +02:00
Daniel a881f4713d Merge pull request #56 from DDR0/dole-factor
Another branch, built on top of my dev branch, which contains a scarce resource sharing implementation.
2017-08-30 11:18:58 +02:00
Daniel c52fd3f966 Merge pull request #55 from DDR0/dev
ui improvements for the web interface storage tab
2017-08-30 11:12:01 +02:00
David Roberts ce6ab38a72 Initial stab at making the master server dole out resources more fairly when running a deficit. 2017-08-29 23:35:50 -07:00
David Roberts 49887012f7 Tweaked slaveDisplay.js to use 92% less data to draw a chart. This is projected to speed up chart loading significantly, from 3.4s to about 0.5s.
- Added copper-cable to the list of hidables on the chart, and removed copper-wire which isn't a thing.
- Don't display items as hidable that aren't being produced. It's confusing.
- Added /getTimelineStats, because /getStats is used by another view that I don't feel like converting to not require the 0-production data points.
2017-08-29 16:25:33 -07:00
David Roberts 000b2fc147 Added search functionality back in. It's much snappier now.
- Is now case-insensitive.
- And regex.
2017-08-29 13:57:13 -07:00
David Roberts bdf4225dec Clean up web interface storage tab.
- Use diffing algorithm so you can select text and images.
	- This uses textcontent, plugs a vulnerability where you could inject arbitrary javascript if you could upload a modded item with html in the name, eg, "iron-plate<img src=\"\" onerror=\"console.log('hi')\">".
- Fall back to transparent png if item image not found.
	- This stops the flashing box when the image does not exist.
- (internal) Simplify `sortByKey(array, key)`.
2017-08-29 13:25:15 -07:00
Danielv123 d01914f7b2 BUGFIX: unknown symbol near "/"
2017-08-21 11:00:57 Cannot execute command. Error: [string "local a='{"players":{'local b=false;for c,d i..."]:2: unexpected symbol near '/' kept appearing in chat, lagging the server and occasionally causing dropouts. I believe this was magically fixed
2017-08-21 11:16:22 +02:00
Danielv123 512b3c66a5 Added fake terminal to /nodeDetails
It doesn't do anything yet, but it should allow sending commands to servers and watching their output.
2017-07-13 11:59:38 +02:00
Danielv123 e3b8926e35 Housekeeping 2017-07-11 19:48:00 +02:00
Danielv123 2fc7de3983 DDMMYY is better than MMDDYY 2017-07-11 19:47:06 +02:00
Danielv123 6334ceb252 Fix last seen date on /slaves 2017-07-11 18:16:11 +02:00
Danielv123 a350b39e2f . 2017-07-11 13:15:56 +02:00
Danielv123 cb1071fc33 fix race condition 2017-07-11 13:14:13 +02:00
Danielv123 53af3849cc Fix ddosing wiki.factorio.com with 404 picture requests 2017-07-11 13:10:58 +02:00
Danielv123 8acbd707f9 GET /api/getFactorioLocale now returns entire base en locale 2017-07-11 12:02:47 +02:00
Danielv123 5d294c3a62 Change extension fron .ejs to .html 2017-07-10 14:57:48 +02:00
Danielv123 39dcb43199 Don't open rcon port on linux because its not needed 2017-07-10 14:33:07 +02:00
Danielv123 12fd4f28a2 Updated readme to use correct tar flags 2017-07-10 13:41:03 +02:00
Danielv123 d394426958 critical bugfix 2017-07-10 13:33:18 +02:00
Danielv123 dd9e8d5029 No comment. 2017-07-09 17:18:54 +02:00
Danielv123 7ce788e146 Typo. This is unproffessional. 2017-07-09 17:16:27 +02:00
Danielv123 cddbc547f8 Fixed. Again. 2017-07-09 17:15:14 +02:00
Danielv123 ba389859ba Merge branch 'master' into dev 2017-07-09 17:12:10 +02:00
Danielv123 492d678f94 Fixed icons after wiki links changed 2017-07-09 17:03:41 +02:00
Danielv123 4047f66144 Merge branch 'dev' 2017-07-09 17:02:12 +02:00
Danielv123 14bec5d225 Fixed icons after wiki links changed 2017-07-09 17:02:07 +02:00
Danielv123 f3bb8c58e5 Merge branch 'dev' 2017-06-20 20:10:51 +02:00
Danielv123 5de0b6319c Error message when started without instanceName
And some test coverage
2017-06-20 20:05:54 +02:00
Danielv123 e6da33582c Cleanup, more coverage 2017-06-17 17:40:46 +02:00
Danielv123 d2f122936e Tests for handleInventory(), 93% avg coverage 2017-06-17 17:24:24 +02:00
Danielv123 2a1badf7cb Lots of tests 2017-06-15 17:14:29 +02:00
Danielv123 8ee3109fbc NPM5.0.1, housekeeping 2017-06-15 17:14:01 +02:00
Danielv123 fdf8a4b4b1 Housekeeping 2017-06-14 21:52:38 +02:00
Danielv123 c4ead8c699 Working inventory imports resolves #50 2017-06-14 21:46:30 +02:00
Danielv123 f9a2e8519b Hide steam from production graph 2017-06-14 15:08:34 +02:00
Danielv123 c24cb51f13 . 2017-06-09 06:58:06 +02:00
Danielv123 0ada84e153 #50 nearing completion, needs taping together and testing
Includes great unit test coverage (npm run cover reports ~85%)
2017-06-07 20:40:03 +02:00
Danielv123 e5b6a94524 less clutter on npm run cover 2017-06-07 20:33:58 +02:00
Danielv123 00dd22e59f Slight improvements to existing tests 2017-06-07 20:33:20 +02:00
Danielv123 913887fd2c Removed \n from UPSdisplay 2017-06-07 07:26:14 +02:00
Danielv123 dbf209de61 Tests, 90% coverage 2017-06-06 11:12:05 +02:00
Danielv123 a7fcde60b8 Reporting for multiple players 2017-06-06 10:45:44 +02:00
Danielv123 6466e12504 working on multi player reports 2017-06-05 12:52:40 +02:00
Danielv123 53cd6c846a inventoryImport now reads inventory successfully 2017-06-03 15:07:26 +02:00
Danielv123 a09cdfb1de possibly fixed rare crash on connect in UPSdisplay 2017-06-03 15:06:27 +02:00
Danielv123 ee133761f7 fixed slaves not registering on windows 2017-06-03 11:48:07 +02:00
Danielv123 18a40ca806 More lua experimenting 2017-06-02 22:29:28 +02:00
Danielv123 103f01b6a2 inventory reports, work on #50 2017-06-01 13:25:59 +02:00
Danielv123 0930000246 Merge branch 'dev' 2017-05-31 18:04:25 +02:00
Danielv123 a0facf9ea8 Hover text for online indicator 2017-05-30 09:30:05 +02:00
Danielv123 18b8b16f02 decreased logging on master 2017-05-29 12:39:35 +02:00
Danielv123 4155ea7916 Save slaves between master reboots 2017-05-29 11:23:53 +02:00
Danielv123 21d87a8cd1 Housekeeping 2017-05-26 21:17:24 +02:00
Danielv123 844bfebcd7 UPS display plugin that shows on master 2017-05-26 19:50:33 +02:00
Danielv123 960cd79be3 fix /api/editSlaveMeta creating new slaves when it shouldn't 2017-05-26 15:25:49 +02:00
Danielv123 bafc8dfc44 /inventory/ now has a search field 2017-05-26 09:25:07 +02:00
Danielv123 b520d9611b Red/green/yellow dot to tell slave status on /nodeDetails/ 2017-05-26 09:06:04 +02:00
Danielv123 74348be0a8 slaveDetails prod chart, playerCount, last online date 2017-05-24 22:34:03 +02:00
Danielv123 70ecdf010c Renamed lib because thats reasonable 2017-05-24 22:33:15 +02:00
Danielv123 f8749fb8d4 Made tests linux compatible 2017-05-23 15:45:34 +02:00
Danielv123 4dee4ed84e Extended gitignore, removed database 2017-05-23 15:35:17 +02:00
Danielv123 4969c6041b Fixed style that broke on other pages 2017-05-23 15:28:07 +02:00
Danielv123 7536469649 Link to nodeDetails from /slaves/ 2017-05-23 14:22:01 +02:00
Danielv123 5c13e3107d /nodeDetails?slaveID=xxx page for detailed info about a single slave
and massive UI overhaul which looks like shit.
2017-05-23 11:56:24 +02:00
Danielv123 ecc5b1c6ce tests for fileOps.deleteFolder[...], istanbul for code coverage 2017-05-22 13:28:00 +02:00
Danielv123 3d4fbfb3fa tests for fileOps.getNewestFile() 2017-05-22 10:59:39 +02:00
Danielv123 2695f0a443 Suppressed NPM errors 2017-05-22 10:59:16 +02:00
Danielv123 4f77c1b473 housekeeping 2017-05-21 22:20:20 +02:00
Danielv123 5f3a160b75 Moved file functions to internal lib with some tests as well 2017-05-21 22:00:30 +02:00
Danielv123 d9e897836b test cleanup 2017-05-20 20:30:25 +02:00
Danielv123 6e95c86fb1 moved libs to ./lib/ 2017-05-20 20:17:02 +02:00
Danielv123 cd44b3572b Added tests using mocha 2017-05-20 19:40:47 +02:00
Danielv123 336eb07cab /api/editSlaveMeta allows you to attach arbitrary metadata to slaves
You will need its unique ID and rcon password though.
2017-05-19 11:05:36 +02:00
Danielv123 ba626295a5 Only pipe plugin stdout if it starts with /, else log to console 2017-05-19 09:09:33 +02:00
Danielv123 930d3352cc Fixed problem 2017-05-19 07:24:09 +02:00
Danielv123 924edc7af7 Allow starting plugins with arguments 2017-05-19 07:22:36 +02:00
Danielv123 efbc4fbb87 Semicolons for consistency 2017-05-18 21:16:11 +02:00
Danielv123 85d7246bca Add semicolons because they should be there 2017-05-18 21:13:13 +02:00
Danielv123 625f0eec3b Merge branch 'dev' 2017-04-24 19:50:14 +02:00
Danielv123 4a2e62386f Fixed missing callback :S 2017-04-23 10:30:09 +02:00
Danielv123 5d51e9e80a stdIn mode for linuxes to solve problem in rcon lib resolves #42 2017-04-22 21:47:11 +02:00
Danielv123 31e4bab159 Less logs 2017-04-21 10:21:47 +02:00
Danielv123 78334fc7e0 Less logs 2017-04-21 10:08:02 +02:00
Danielv123 538c4f575b ; not : 2017-04-21 10:05:14 +02:00
Danielv123 1c91727c8a docker 2017-04-21 10:02:39 +02:00
Danielv123 6449fef1e1 Merge branch 'master' into dev 2017-04-21 09:22:48 +02:00
Danielv123 4db55a8857 Add reconnect when factorio complains about payload size
Sometimes the rcon lib messes up and delivers a 4x package that makes factorio stop responding to rcon and throw an error. We catch that error and attempt to reconnect.

Couldn't parse RCON data: Maximum payload size exceeded
2017-04-21 09:20:22 +02:00
Danielv123 9971b9f5b3 Add more logging to isolate case where slave stops reporting to master 2017-04-13 13:45:49 +02:00
Danielv123 8e0abf5e2d or is || not | 2017-04-08 16:52:33 +02:00
Danielv123 1719ff9b07 . 2017-04-08 16:37:38 +02:00
Danielv123 f208540856 Clone dev branch instead of master
This caused me a lot of headaches. Sigh.
2017-04-08 16:13:59 +02:00
Danielv123 c9d56b8940 Debug 2017-04-08 10:32:21 +02:00
Danielv123 8613c1deef Fixed dockerfile 2017-04-07 10:35:24 +02:00
Danielv123 ae72814527 More docker trying to fix environent variables 2017-04-07 10:17:46 +02:00
Danielv123 af345fb2ae More docker 2017-04-07 08:40:46 +02:00
Danielv123 80251eacae Log factorio stdout if on linux 2017-04-06 21:13:06 +02:00
Danielv123 2e4eb9332d Report instance name when asking for items 2017-04-06 20:57:15 +02:00
Danielv123 432c4e462d Setting to disable automatic storage screen updates 2017-04-06 20:56:42 +02:00
Daniel e18bc13abe Add more pictures to storage page 2017-04-04 09:35:10 +02:00
Danielv123 41a6e88006 Show how many are online 2017-04-03 13:31:09 +02:00
Danielv123 8979fd28b3 Merge branch 'dev' 2017-04-02 15:59:21 +02:00
Danielv123 e8d8217dc5 Show friendly instance name instead of ID 2017-04-02 15:59:10 +02:00
Danielv123 a21df015a8 Merge branch 'dev' 2017-04-02 15:17:27 +02:00
Danielv123 2278176608 Suppressed error 2017-04-02 15:17:17 +02:00
Danielv123 0e10e2ebfa . 2017-04-02 15:16:54 +02:00
Danielv123 06e431c01a Fixed markdown headers in readme 2017-03-31 22:36:24 +02:00
Danielv123 ff9fac842e Added docker for running master server 2017-03-31 22:34:39 +02:00
Danielv123 e90bbb745a Merge branch 'dev' 2017-03-31 16:25:47 +02:00
Danielv123 f5516891e3 Fixed missing sync-request 2017-03-31 16:25:25 +02:00
Danielv123 93b163f3a4 adding lots of let instead of globals to see what happens with the leaks
#48 is the one
2017-03-30 21:50:24 +02:00
Danielv123 9fe4ebdacd Stop overloading the legend by using a whitelist 2017-03-17 16:00:03 +01:00
Danielv123 3186fa8fb7 Button to show all datasets
Also #46
2017-03-17 10:55:34 +01:00
Danielv123 8967189fd2 Setting to disable production graph for performance
Related to #45
2017-03-17 09:24:53 +01:00
Danielv123 3f699e7ab5 Button to hide all datasets
resolves #46
2017-03-17 09:20:09 +01:00
Danielv123 dc2c04741a Fixed inventory using dead link to API 2017-03-16 17:56:10 +01:00
Danielv123 bd5956c9ed Add mod icons by link 2017-03-16 17:55:15 +01:00
Danielv123 e156864ccd Setting to display offline slaves 2017-03-16 13:32:08 +01:00
Danielv123 1e287502f8 Fix messy graph bug due to unsorted x axis 2017-03-16 13:31:49 +01:00
Danielv123 e801be3c77 Added fancy settings menu 2017-03-16 13:11:19 +01:00
Danielv123 fedf9f6df1 Show legends and toggle display of datasets 2017-03-16 09:25:28 +01:00
Danielv123 5abeb8c9a6 Fixed bug with negative production 2017-03-16 09:24:39 +01:00
Danielv123 a243181856 Moved API endpoints to /api/ 2017-03-16 09:23:59 +01:00
Danielv123 7b71185bb8 Avoid extreme negatives in stat reporting 2017-03-13 18:46:43 +01:00
Danielv123 5cf91e7a3d Production logging from 5 to 60 seconds 2017-03-13 18:21:51 +01:00
Danielv123 480a176fc3 Zoomable graphs 2017-03-12 19:14:33 +01:00
Danielv123 cd49d2582f Show disconnected slaves for up to 2 minutes 2017-03-12 17:04:04 +01:00
Danielv123 71753c29a5 Production graph display on website
resolves #35
2017-03-12 17:01:05 +01:00
Danielv123 14b9ca0068 Merge remote-tracking branch 'origin/master' into dev 2017-03-12 09:33:20 +01:00
Danielv123 1fc2de231b Show material type in graph 2017-03-10 21:14:20 +01:00
Danielv123 1f06d3eb48 Display of all graphs working
Also added proper clock to the X axis so its readable
#35
2017-03-10 21:08:04 +01:00
Danielv123 11e3bf7879 Working chart for single item #35 2017-03-10 20:36:21 +01:00
Danielv123 548436cbde Non working canvasjs chart for #35 2017-03-10 11:15:46 +01:00
Danielv123 dcc17e2fa8 Master has POST endpoint for flows data
#35 still not sure how to do the graphs on clientside though
2017-03-10 09:27:36 +01:00
Daniel 153aba3e0f Update README.md 2017-03-08 12:54:34 +01:00
Danielv123 dc103d1bed Fixed inventory
Yeah, I broke that in last commit. So it did change *something*
2017-03-08 12:46:58 +01:00
Danielv123 9edf4dc18e Overhauled entire website with 0 change in looks
So I can finally work on implementing #35 but it feels like I did nothing :(
2017-03-08 11:32:01 +01:00
Danielv123 cc6ff973b5 Production graph DB for master
#35
2017-03-07 11:19:40 +01:00
Danielv123 03bb6a8063 Client.js now gathers production graph information 2017-03-06 20:37:02 +01:00
Danielv123 adff48d612 Merged dev into master 2017-03-02 15:01:06 +01:00
Danielv123 1f5c2a1977 Refix because last fix was moot 2017-03-02 14:19:27 +01:00
Danielv123 d4968e0894 Fix getting trolled by partial autosaves (.tmp.zip) 2017-03-02 13:47:02 +01:00
Danielv123 36bfd6450b Autosave loading fixed again 2017-03-02 13:31:51 +01:00
Danielv123 e21332591f Load autosaves but bug with factorio-current.log
resoves #44
2017-03-02 12:37:34 +01:00
Danielv123 3a04019241 Reduce logs 2017-03-02 11:50:18 +01:00
Danielv123 e6b3d55e70 Merged dev into master 2017-03-02 00:18:17 +01:00
Danielv123 77d7def6ef Fixed instance creation, server reports playercount 2017-03-02 00:18:04 +01:00
Danielv123 88238ead27 Merged branch master into dev 2017-02-28 16:31:02 +01:00
Danielv123 dd0213b4d1 Removed plugins from main repo 2017-02-28 16:29:41 +01:00
Daniel b7b8d7d254 Added server browser password for fox 2017-02-27 23:06:44 +01:00
Daniel 0264037731 Update README.md 2017-02-27 21:29:00 +01:00
Danielv123 dda2538578 Plugins can now listen to files, resolves #41 2017-02-27 08:42:08 +01:00
Danielv123 ccf54bec18 Removed junk, added comments 2017-02-26 18:31:58 +01:00
Danielv123 d73bfbc1eb #41 Basic plugins can now send commands to game 2017-02-26 16:48:58 +01:00
Danielv123 27148053ae Basic plugin implementation 2017-01-29 23:34:56 +01:00
Danielv123 b37e158774 Merged dev into master 2017-01-28 17:43:23 +01:00
Danielv123 f435a792de Avoid crashing on every 2nd launch due to #40 fix 2017-01-28 17:43:02 +01:00
Danielv123 4f1cfce39c Fix crash on slow startup, resolves #40 2017-01-28 17:24:30 +01:00
Danielv123 f1284264fe Merged dev into master 2017-01-16 23:24:38 +01:00
Danielv123 e610d23525 asdasdas readme 2017-01-16 23:23:05 +01:00
Danielv123 1655b4b689 Windows readme updated to use bat installer 2017-01-16 23:17:48 +01:00
Danielv123 bc2cfb1cb1 Fixed missing dependency 2017-01-16 23:13:37 +01:00
Danielv123 8a29276f58 Merged branch master into dev 2017-01-16 22:16:09 +01:00
Daniel 0288e9b2c2 Update README.md 2017-01-16 21:22:29 +01:00
Danielv123 22a1372e32 Better mod loading, service auth 2017-01-16 21:21:13 +01:00
Daniel a6ec00bb18 Added notice about not working on old ubuntus 2017-01-16 20:38:21 +01:00
Danielv123 a718dbeb53 Merged dev into master 2017-01-10 11:11:45 +01:00
Danielv123 91438c32d9 Client can download mods from master 2017-01-08 22:18:04 +01:00
Danielv123 fd577054c5 Instances uploads their mods to master 2017-01-08 21:00:24 +01:00
Danielv123 e961ca6d56 Fixed mod hashing to avoid dupes 2017-01-07 23:37:10 +01:00
Danielv123 a49f98b228 Stop sending rcon passwords because security and stuff 2017-01-05 13:19:45 +01:00
Danielv123 0f7916229a Function to get a hash from an instances mods 2017-01-05 13:19:21 +01:00
Danielv123 e16d7b43ed Gitignore and require moved to top 2017-01-04 10:19:17 +01:00
Danielv123 12d1631922 Fixed downloading of mods with https 2017-01-03 20:21:15 +01:00
Danielv123 f589273c32 Mod seperated from slave, node client.js download 2017-01-03 17:44:35 +01:00
Danielv123 c33ca4abec Client.js now transmits IP needed to connect to slaves 2016-12-27 11:15:53 +01:00
Danielv123 26a9c4f1ef Fixed virtual signals 2016-12-27 11:14:53 +01:00
Danielv123 a10fa69def Inventory combinator can now transmit all virtuals 2016-12-27 10:47:02 +01:00
Danielv123 21cbc098f7 Merged dev into master 2016-12-26 23:26:12 +01:00
Danielv123 a70342fd82 Added unixtime signal to inventory combinator 2016-12-26 23:25:59 +01:00
Daniel 7c1caebccc Finalized readme 2016-12-22 17:01:47 +01:00
Danielv123 b3ee475a6c Missing , 2016-12-22 16:57:41 +01:00
Danielv123 c9d5803e26 Update to support 0.14.10+ 2016-12-22 16:53:12 +01:00
Danielv123 ceff284ba5 Changed slashes to fix linux support 2016-12-22 16:40:49 +01:00
Danielv123 de1a51610d Merged dev into master 2016-12-22 16:23:59 +01:00
Daniel ace440dbef Removed .exe to allow launching on linuxes 2016-12-22 16:21:14 +01:00
Daniel af92716c77 Ubuntu install instructions added 2016-12-22 16:09:20 +01:00
Danielv123 aac101bad7 prettify 2016-12-21 14:40:43 +01:00
Danielv123 cb77717650 Starting instances now require "start" as second param, instance 3rd 2016-12-21 14:33:34 +01:00
Danielv123 ba05b14e6f Maintainability, avoid bugs when not exiting after special commands 2016-12-21 14:16:29 +01:00
Danielv123 a896b032aa Added list and delete commands to client.js for managing instances
client.js list
client.js delete [instance]
also adde --help to show usage
2016-12-21 14:08:21 +01:00
Danielv123 964bc76b53 Merged branch master into dev 2016-12-21 11:08:32 +01:00
Danielv123 1c5bb9e8ab Consistent styling 2016-12-19 13:23:53 +01:00
Danielv123 187c05640f Argument 2016-12-19 13:21:36 +01:00
Daniel de95872773 Licensed as MIT, resolves #39 2016-12-02 23:16:22 +01:00
Danielv123 eb786ff146 Slave display now only shows connected slaves, autoupdates, trangartime
Added hidden option to show dates in trangartime y-ymd-ymd-y instead of ISO 6801

Ctrl+shift+i
g.trangarTime = true
2016-11-07 13:33:15 +01:00
Danielv123 37fdbb68d1 Remove currently non functioning graph from nav, start on storage page 2016-11-07 11:14:25 +01:00
Danielv123 afb00ab3fb Production chart partially added and hidden 2016-11-07 09:26:58 +01:00
Danielv123 b66df4d233 Merged branch master into dev 2016-10-28 16:19:16 +02:00
Daniel 1e6a46d8b3 Update README.md 2016-10-28 16:15:43 +02:00
Danielv123 dd0a216cb5 Merged dev into master 2016-10-28 16:01:14 +02:00
Danielv123 09d511d0c8 No longer displays all subpages by default, performance on tab inactive 2016-10-28 10:51:46 +02:00
Danielv123 85274220e7 Webpage now shows recently connected slaves, resolves #33 2016-10-28 09:51:42 +02:00
Danielv123 a90d78213a Basic serverside slaveawareness 2016-10-27 22:41:25 +02:00
Danielv123 f6de6e026b Decreased logging, automatic configuring of multiple instances
When creating a new instance, it will now generate a random port for the instance, RCON and an RCON password.

Current problems are that its not checking of the port is already taken by something else; if it is, the erver either won't start or RCON will fuck up.
2016-10-27 09:21:45 +02:00
Danielv123 64009a7e16 Valid JSON config file 2016-10-26 09:05:43 +02:00
Danielv123 656c05a30b Changed default config 2016-10-26 08:57:50 +02:00
Danielv123 2234d41655 Housekeeping, more config settings 2016-10-24 09:31:30 +02:00
Danielv123 234e6eba4c Merged branch instances into dev 2016-10-24 08:59:47 +02:00
Thomas Deal 0927bec070 client.js now launches it's own server instance 2016-10-23 17:38:57 -04:00
Danielv123 8fd13a7d04 Merged dev into master 2016-10-21 16:58:48 +02:00
Danielv123 12ca57af61 More icon exceptions 2016-10-21 16:00:40 +02:00
Danielv123 82f52c6e21 Item list sort after amount 2016-10-21 15:39:39 +02:00
Danielv123 8562bccd5f Betterer icons 2016-10-21 11:14:04 +02:00
Danielv123 3ea2530dab Moar icons :D 2016-10-21 11:04:48 +02:00
Danielv123 59ddb289a8 Removed jumping related to icons 2016-10-21 09:22:35 +02:00
Danielv123 6b6ffe5904 #8 Working on icons in masterGUI 2016-10-20 22:47:36 +02:00
Danielv123 3d38bba455 Merged origin/master into master 2016-10-20 22:45:59 +02:00
Danielv123 0133e36f8a Merged dev into master 2016-10-20 22:45:47 +02:00
Danielv123 b30d9fa190 Merged branch master into dev 2016-10-20 19:34:12 +02:00
Thomas Deal 854ed8fdd0 Report fluids correctly in Inventory Combinator 2016-10-20 13:31:33 -04:00
Daniel 528165367c Merge pull request #31 from Danielv123/dev
Resolves #30 adds inventory combinator
2016-10-20 13:27:07 +02:00
Danielv123 67fef2e543 Resolves #30 2016-10-20 13:25:52 +02:00
Danielv123 52b3e48eb8 Merged branch master into dev 2016-10-20 09:17:11 +02:00
Danielv123 cf93377625 Merged branch master into dev 2016-10-20 09:17:01 +02:00
Thomas Deal 67e5916790 Remove Lost bracket 2016-10-19 22:26:56 -04:00
Thomas Deal cc65d8c3c2 Locale updates 2016-10-19 21:45:24 -04:00
Thomas Deal bf2d16669d Support setting world ID, and if set, output it on the Inventory Combinator, and include it in transmitted frames. 2016-10-19 21:42:09 -04:00
Thomas Deal 16ec26984a Typo Fixes, Uncomment virtual signals. 2016-10-19 21:20:12 -04:00
Thomas Deal 67e192a78f More consistent handling of creation/destruction of entities. Uses entity.unit_number instead of HashPosition. Rebuilds all tables on initial install and on mod version bump. 2016-10-19 20:41:10 -04:00
Thomas Deal 5e65fa5208 Add Inventory Combinator which reports the last inventory reported by the external code. 2016-10-19 20:11:54 -04:00
Thomas Deal 09feb085ef Spelling fix 2016-10-19 16:54:54 -04:00
Thomas Deal 7b44e830f8 Write files only to the server by default, changeable via setFilePlayer() 2016-10-19 16:52:55 -04:00
Daniel 2013eb5449 Update README.md 2016-10-19 18:19:58 +02:00
Danielv123 f896e0dc74 Merged dev into master 2016-10-15 15:35:27 +02:00
Daniel 23e0aef34e Merge pull request #29 from TheAIBot/master
Fixed bug where you couldn't make lubricant and sul acid.

closes #28
2016-10-15 15:17:00 +02:00
TheAIBot 5abaed00aa fixed bug where you couldn't make lubricant and sul acid. 2016-10-15 08:43:45 +02:00
TheAIBot 3bea31cd45 Merge pull request #4 from Danielv123/dev
Dev
2016-10-14 16:43:14 +02:00
Danielv123 9d80769a12 MasterGUI basic production chart
Work on #8
2016-10-13 13:25:45 +02:00
Danielv123 6b35a3eb47 MasterGUI navigation added 2016-10-11 09:30:34 +02:00
TheAIBot 2c66b25162 Merge pull request #3 from Danielv123/master
update
2016-10-10 00:47:19 +02:00
TheAIBot 851fbe28e7 Merge remote-tracking branch 'refs/remotes/Danielv123/master' 2016-10-10 00:44:47 +02:00
Danielv123 4b3e5b0917 Merged dev into master 2016-10-09 22:28:53 +02:00
Danielv123 640a8d4d21 Removed argument to improve compat with older node versions
Resolves #25
2016-10-09 22:28:20 +02:00
Daniel e5314f6ace Merge pull request #24 from Danielv123/dev
Circuit network support, closes #22
2016-10-09 11:39:15 +02:00
Danielv123 511e31f24f Merge branch 'master'
Conflicts:
	README.md
2016-10-09 11:37:24 +02:00
Danielv123 8f426a1362 Updated readme 2016-10-09 11:02:50 +02:00
Danielv123 a2d970fad6 Use the many option for signals instead 2016-10-07 23:59:15 +02:00
Danielv123 87cfe1bc46 Sending signals over Clusterio now works
Closes #19
2016-10-07 20:29:35 +02:00
Danielv123 a3aafd8b1d JS part of signals kinda finished but it aint working 2016-10-07 17:54:23 +02:00
Danielv123 20c591bf71 Removed DB from repo 2016-10-06 17:49:16 +02:00
Danielv123 b123d485d2 Slave sending combinator signals finished
So working on #22.
2016-10-06 17:47:03 +02:00
Daniel dfa8b573df Closes #21 2016-10-03 21:07:58 +02:00
Daniel 3fde2be222 Solved #21 2016-10-03 21:06:31 +02:00
Danielv123 9011665a80 Merged branch master into dev 2016-10-02 11:41:45 +02:00
Daniel 134c8e4d92 Merge pull request #20 from Danielv123/dev
The end of all desync problems and chat spamming
2016-10-02 10:21:56 +02:00
Danielv123 9b14da470e Use silent-command to avoid spamming chat. Thanks HanziQ! 2016-10-02 10:20:05 +02:00
Thomas Deal f9d6d9e001 Add Transmit/Receive Combinators. Transmit combinators are Deciders which also output into txbuffer.txt. Receive Combinators are Constants which replay frames received from external sources via remote calls to receiveFrame and recieveMany. 2016-10-01 21:20:18 -04:00
Daniel 4f8ad5eec6 Merge pull request #18 from Danielv123/dev
MP desync issues fixed, prerealease 1
2016-10-01 23:35:36 +02:00
Danielv123 6d12bac32a Merged branch master into dev 2016-10-01 19:03:02 +02:00
TheAIBot 090910aeef Merge remote-tracking branch 'refs/remotes/Danielv123/master' 2016-10-01 02:46:06 +02:00
TheAIBot 7e67ae5068 fixed up the last things about the output tank 2016-10-01 02:44:45 +02:00
TheAIBot 9ca013590d fixed merge error 2016-10-01 02:27:08 +02:00
TheAIBot 72cef84df5 Merge remote-tracking branch 'refs/remotes/origin/json-parser'
# Conflicts:
#	clusterio_0.0.2/control.lua
2016-10-01 02:26:29 +02:00
TheAIBot d22b7e3de4 multiplayer fixed 2016-10-01 02:26:08 +02:00
Danielv123 5670bdb77a Working on adding charts to master GUI 2016-09-30 23:23:01 +02:00
Danielv123 379f8398c7 Merged branch dev into dev 2016-09-30 19:27:40 +02:00
Danielv123 e33109703d Minor minor 2016-09-30 19:27:10 +02:00
Danielv123 181e570972 Master webpage syntax 2016-09-30 19:16:46 +02:00
TheAIBot 9391b33cc9 Merge remote-tracking branch 'refs/remotes/Danielv123/master' 2016-09-30 18:24:44 +02:00
Danielv123 aa93af6d06 More cleanup 2016-09-29 09:16:01 +02:00
Danielv123 db47320951 Removed outdated script 2016-09-29 09:15:42 +02:00
Daniel d820c483e6 Merge pull request #15 from Danielv123/dev
Prerelease: Solved #10 and solved #14
2016-09-27 11:26:01 +02:00
Danielv123 da9d2c14dc Solved #14 and #10, preparing for prerelease 2016-09-27 11:24:55 +02:00
Danielv123 1e65eb40c5 Changed up the tranfer format, everything works! 2016-09-26 08:07:19 +02:00
Danielv123 06a4d49884 WIP, still not working 2016-09-09 08:25:39 +02:00
Danielv123 129ef8d2f8 Tried to cache rcon requests, ended up with dupe machine
Me needs help, I don't understand where the game is getting free items from.
2016-08-25 20:50:00 +02:00
Daniel e326ae093a Merge pull request #13 from TheAIBot/json-parser
Added Json parser for remote call ImportMany
2016-08-25 13:02:47 +02:00
TheAIBot f3c6fdaa3b added json parser for importMany 2016-08-23 22:51:54 +02:00
TheAIBot b3aa96cddf Merge remote-tracking branch 'refs/remotes/Danielv123/master' into json-parser 2016-08-23 22:49:27 +02:00
Daniel a12d818cdb Revert "Merge pull request #11 from TheAIBot/master"
This reverts commit 3252eefcfc.
2016-08-23 21:45:08 +02:00
Daniel 3252eefcfc Merge pull request #11 from TheAIBot/master
fix desync
2016-08-23 21:32:56 +02:00
TheAIBot 11ff85ca74 fixed desync 2016-08-23 21:12:02 +02:00
TheAIBot 62c07c77f2 initial tank chose fluid 2016-08-23 20:31:43 +02:00
TheAIBot c9d5509c85 Merge pull request #2 from Danielv123/master
update
2016-08-23 20:15:47 +02:00
Danielv123 258946fe51 Added sample server-settings.json to repo 2016-08-23 19:42:34 +02:00
Danielv123 410b3e5e53 Added webpage to display network contents 2016-08-23 19:31:27 +02:00
Danielv123 8ea74821b4 webserver and API endpoint for getting network contents 2016-08-23 14:16:24 +02:00
TheAIBot f2f832333c Merge pull request #1 from Danielv123/master
update
2016-08-21 10:48:03 +02:00
TheAIBot 2fe0844fd8 Merge remote-tracking branch 'refs/remotes/Danielv123/master' 2016-08-21 10:43:48 +02:00
Danielv123 fcb9c5add5 Changed name of mod and descriptions, updated readme.md 2016-08-19 16:42:30 +02:00
Danielv123 0e9fb267c4 Working on #7 configs 2016-08-17 22:52:44 +02:00
Danielv123 601d5edb27 Cleanup, readme 2016-08-17 18:31:43 +02:00
Danielv123 44e311feea Fixed js, items are now created correctly and dupe/loss glitches removed 2016-08-17 09:20:14 +02:00
Danielv123 440ecfad30 Everything works! (using rcon) 2016-08-16 12:41:55 +02:00
Daniel 74253908d5 Merge pull request #6 from TheAIBot/master
added UPDATE_RATE to config
2016-08-16 11:54:10 +02:00
TheAIBot 4897f019f6 added UPDATE_RATE to config 2016-08-16 11:43:18 +02:00
Daniel a3f931fbec Merge pull request #5 from TheAIBot/master
added tank, made code faster and added another remote call
2016-08-16 10:36:02 +02:00
TheAIBot 7beef11f08 added tank, made code faster and added another remote call 2016-08-16 02:03:11 +02:00
Daniel 1a9fc933d4 Merge pull request #3 from TheAIBot/master
Only use as many resources as given
2016-08-14 11:06:25 +02:00
TheAIBot ba98a0cfe0 removed uneeded print statements 2016-08-14 10:59:31 +02:00
TheAIBot e5366ac431 Mod will now only use as many resources as it has been given 2016-08-14 10:55:53 +02:00
TheAIBot 1f58458302 Merge remote-tracking branch 'refs/remotes/Danielv123/master' 2016-08-13 22:21:53 +02:00
Danielv123 5bbb961f54 Integration of last merges changes 2016-08-13 15:54:01 +02:00
Daniel a25d3be98d Merge pull request #1 from TheAIBot/master
Fits perfectly, thank you so much!
2016-08-13 11:28:15 +02:00
TheAIBot 9b0349d518 using chests 2016-08-13 10:26:55 +02:00
Danielv123 ba3d01e2fe Added my bat file for launching everything (most things) 2016-08-12 21:15:37 +02:00
Danielv123 d80619bab8 added the lua mod too 2016-08-12 21:02:44 +02:00
Danielv123 1f707d0aad Lua side does not work, JS seems to be fine 2016-08-12 21:01:50 +02:00
Danielv123 95a04c1221 Initial commit 2016-08-12 20:54:14 +02:00
108 changed files with 17745 additions and 174 deletions

No files matched your search

+50
View File
@@ -0,0 +1,50 @@
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
node_modules/*
!node_modules/_app
Factorio*
/factorio
/instances
/sharedMods
/database
database/items.db
config.json
package-lock.json
secret-api-token.txt
mod_cache.json
static/pictures/spritesheet.js
static/pictures/spritesheet.png
static/external
# weird pattern to ignore sharedPlugins except not ignore some core modules
!sharedPlugins/
sharedPlugins/*
!sharedPlugins/inventoryImports/
!sharedPlugins/UPSdisplay/
!sharedPlugins/remoteMap/
!sharedPlugins/remoteCommands/
!sharedPlugins/researchSync/
+10
View File
@@ -0,0 +1,10 @@
language: node_js
node_js:
- "node"
before_install:
- sudo apt-get -qq update
- sudo apt-get install -y curl git tar
- curl -o factorio.tar.gz -L https://www.factorio.com/get-download/latest/headless/linux64
- tar -xvf factorio.tar.gz
script:
- npm run cover
+19
View File
@@ -0,0 +1,19 @@
FROM node:latest
RUN apt-get update && apt install git -y
RUN mkdir factorioClusterio
RUN git clone -b dev https://github.com/Danielv123/factorioClusterio.git && cd factorioClusterio && npm install && curl -o factorio.tar.gz -L https://www.factorio.com/get-download/latest/headless/linux64 && tar -xvzf factorio.tar.gz
WORKDIR factorioClusterio
RUN mkdir instances sharedMods sharedPlugins
RUN node client.js download
LABEL maintainer "danielv@live.no"
EXPOSE 8080 34167
VOLUME /factorioClusterio/instances
VOLUME /factorioClusterio/sharedMods
VOLUME /factorioClusterio/sharedPlugins
CMD RCONPORT="$RCONPORT" FACTORIOPORT="$FACTORIOPORT" MODE="$MODE" node $MODE\.js start $INSTANCE
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 Daniel Vestøl
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+194 -14
View File
@@ -1,18 +1,198 @@
# Plugin page layout screenshots
# factorioClusterio
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FDanielv123%2FfactorioClusterio.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FDanielv123%2FfactorioClusterio?ref=badge_shield)
These screenshots render the real `PluginsPage`, `PluginVersions`, and `SiteLayout` components with synthetic controller/host responses.
They are layout evidence, not a live cluster installation or restart test.
The fixture has two machines, a pending awf_oarc update and web load error, an Auto Restart plugin without a host entrypoint, a Global Chat update, and an up-to-date Research Sync plugin.
| Branch | Coverage | Build |
|--------|----------| ----- |
| Master | [![codecov](https://codecov.io/gh/Danielv123/factorioClusterio/branch/master/graph/badge.svg)](https://codecov.io/gh/Danielv123/factorioClusterio) | [![Build Status](https://travis-ci.org/Danielv123/factorioClusterio.svg?branch=master)](https://travis-ci.org/Danielv123/factorioClusterio) |
| Dev | [![codecov](https://codecov.io/gh/Danielv123/factorioClusterio/branch/dev/graph/badge.svg)](https://codecov.io/gh/Danielv123/factorioClusterio) | [![Build Status](https://travis-ci.org/Danielv123/factorioClusterio.svg?branch=dev)](https://travis-ci.org/Danielv123/factorioClusterio) |
Before: `61f2b0117263516b592bba0b397e887065de5c4d`.
After: `8229db75`.
Mobile viewport: 390 x 844 CSS pixels.
Desktop viewport: 1440 x 900 CSS pixels.
Both use Edge through Playwright, device scale factor 1, dark theme, the same data and default sorting.
If you want to connect to a clusterio cluster, please reffer to the [client](https://github.com/Danielv123/factorioClusterioClient)
The screenshots are kept on a separate evidence branch so they are not part of the code PR's diff.
### Table of contents
To reproduce, put `main.tsx`, `index.html`, `serve.mjs`, `capture.mjs` and `check.mjs` in `temp/plugin-layout/` of a Clusterio checkout.
Run `pnpm install --no-frozen-lockfile`, `npm install --prefix temp/plugin-layout --no-package-lock --no-save playwright`, and `node temp/plugin-layout/serve.mjs` from the repository root.
With Microsoft Edge installed, run `node temp/plugin-layout/capture.mjs before` at the baseline or `node temp/plugin-layout/capture.mjs after` at the fixed commit.
Run `node temp/plugin-layout/check.mjs` at the fixed commit for viewport bounds, search, sorting, navigation, account menu and checklist checks.
* [Introduction & methodology](#introduction)
* [Ubuntu setup](#ubuntu-setup)
* [Windows setup](#windows-setup)
* [Command cheatsheet](#cheatsheet)
## Introduction
Features:
- Entities to send/recieve items
- Cross dimensional storage
- Sending of liquids
- Sending of circuit network signals
- Inventory combinator to display item levels in the cluster (and epoch time)
- Reporting of graphs and UPS on master interface
Connection diagram:
![http://i.imgur.com/7FdVfgB.png](http://i.imgur.com/7FdVfgB.png)
There can be any number of clients connected to each slave, and any number of slaves connected to a master but there can only be one master server in each cluster.
**How does it work?**
Traditional factorio mods have always been limited by the games deterministic design. This gives us a very bug free and predictable game, but doesn't allow us cool stuff such as internet communication.
Clusterio exploits one of the games logging features, game.write_file and RCON to communicate between servers. Sending an item from one server to another takes this path:
1. server1: Chest has stuff in it, write the contents to a file and delete them from the game world
2. client.js on server1: File has stuff in it, parse and send to the master for storage
3. master: server1 gave us stuff, store it in the storage and write some statistics
4. server2: get-chest is empty, write a request to file
5. client.js on server2: Request file has stuff in it, parse and send a request to master for more items of that type
6. master: server2 asked for stuff, check if we have enough and how much demand there is, then send however much is appropriate back
7. client.js on server2: We were allowed to import x of item y, run command /c remote.call("clusterio", "importMany", "{'copper-plate':120}")
This process works the same for both items and liquids, independant on what mods are used. Yes, modded items are fully supported.
Clusterio also handles a few other neat things, such as giving you access to epoch time, transmitting combinator signals between worlds (and any other application who wants to) and
creating graphs on the master web interface.
## Ubuntu setup
NodeJS does not support EOL ubuntu releases. Make sure you are on the most recent LTS release or newer.
Master and all slaves:
sudo curl -sL https://deb.nodesource.com/setup_9.x | sudo -E bash - \
sudo apt install -y nodejs python-dev git wget curl tar build-essential \
sudo npm install pm2 -g \
git clone https://github.com/Danielv123/factorioClusterio.git \
cd factorioClusterio \
npm install \
curl -o factorio.tar.gz -L https://www.factorio.com/get-download/latest/headless/linux64 \
tar -xf factorio.tar.gz
downloads and installs nodejs, pm2, git and clusterio. To specify a version, change "latest" in the link to a version number like 0.14.21.
**Master**
pm2 start master.js --name master
**Server Host**
To download the mod for all its non vanilla features and items, (optional)
node client.js download
To create a new instance (its own save, set of mods and config files)
node client.js start [instancename]
To launch an instance with pm2
pm2 start --name slave client.js -- start [instancename]
use `nano config.json` to change settings.
**Ubuntu with Docker**
Clusterio has limited support for using docker.
sudo docker build -t clusterio --no-cache --force-rm factorioClusterio
sudo docker run --name master -e MODE=master -p 1234:8080 -d -it --restart=unless-stopped danielvestol/clusterio
sudo docker run --name slave -e MODE=client -e INSTANCE=world1 -v /srv/clusterio/instances:/factorioClusterio/instances -p 1235:34167 -it --restart=unless-stopped danielvestol/clusterio
The -v flag is used to specify the instance directory. Your instances (save files etc) will be stored there.
## Windows setup
Clusterio is built up of multiple parts. Here is a quick guide:
Master = master.js
Server host (Slave) = client.js + factorio server
Game Client = The people connecting to the server
All Server Hosts (Slaves) AND Game Clients need to be running the clusterio mod located at [github](https://github.com/Danielv123/factorioClusterioMod) Install it by dropping it into either the sharedMods folder or instances/[instanceName]/instanceMods folder.
There are no requirements for other mods, they can be ran in any configuration allowed by the base game.
**Requirements**
download and install nodeJS 6+ from http://nodejs.org
download and install git from https://git-scm.com/
reboot when you are done, then proceed to the next steps. *reboots matter*
**Master**
1. download and run https://puu.sh/toFHl/01eebbb333.bat
2. You do not *need* to follow the given instructions, but you should
3. type `node master.js`
**Server Host**
1. download and run https://puu.sh/toFHl/01eebbb333.bat
2. Follow the instructions given
3. Type `node client.js start [instancename]` to create a new instance.
To connect to a master server running on a remote machine, open config.json with your favourite text editor (notepad++). You can also set it up to use the official server browser.
Change masterIP to something like 31.152.123.14 (provided by master server owner)
Change masterPort to something like 8080 (provided by master server owner)
Repeat step 3 for more servers on one machine. You should be able to find its port by looking at the slave section on master:8080 (the web interface)
**GameClient**
Fancy game client that does the following steps automatically: [clusterioClient](https://github.com/Danielv123/factorioClusterioClient)
1. Download the same version of the mod as the slave is running from [the mod portal](https://mods.factorio.com/mods/Danielv123/clusterio) or [github](https://github.com/Danielv123/factorioClusterioMod
2. Drop it into ./factorio/mods
3. Run factorio and connect to slave as a normal MP game. You will find the port number to connect to at http://[masterAddress]:8080
## Cheatsheet
**To create a new instance/start it**
node client.js start [instanceName]
**Other instance management tools:**
```
node client.js delete [instanceName]
node client.js list
```
**To update clusterio to the latest version:**
1. Download the latest zip version of factorio for your platform manually from factorio.com. Place it in the project root folder and call it "factorio" (folder name is specified in config.json)
2. Grab the latest version of the repo
```
git pull
npm install
```
3. Download the latest version of the factorioClusterioMod from its github repo
```
node client.js download
```
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

-14
View File
@@ -1,14 +0,0 @@
import { chromium } from "playwright";
const browser = await chromium.launch({ channel: "msedge", headless: true });
const page = await browser.newPage();
page.on("pageerror", error => console.error(error));
for (const [label, width, height] of [["desktop", 1440, 900], ["mobile", 390, 844]]) {
await page.setViewportSize({ width, height });
await page.goto("http://127.0.0.1:5173/plugins");
await page.getByRole("button", { name: "Update to 0.3.1", exact: true }).waitFor();
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: `temp/plugin-layout/${process.argv[2]}-${label}.png` });
console.log(label, await page.evaluate(() => ({ width: innerWidth, scrollWidth: document.documentElement.scrollWidth,
rowHeights: [...document.querySelectorAll(".ant-table-tbody > tr")].map(row => row.getBoundingClientRect().height) })));
}
await browser.close();
-57
View File
@@ -1,57 +0,0 @@
import assert from "node:assert/strict";
import { chromium } from "playwright";
const browser = await chromium.launch({ channel: "msedge", headless: true });
const page = await browser.newPage();
const errors = [];
page.on("pageerror", error => errors.push(error.message));
for (const width of [320, 390, 575, 767, 768, 1024, 1440]) {
await page.setViewportSize({ width, height: 900 });
await page.goto("http://127.0.0.1:5173/plugins");
await page.getByRole("button", { name: "Update to 0.3.1", exact: true }).waitFor();
const dimensions = await page.evaluate(() => ({
width: innerWidth, scrollWidth: document.documentElement.scrollWidth,
columns: document.querySelectorAll("thead th").length,
brand: document.querySelector(".site-layout-brand").getBoundingClientRect().height,
overflow: [...document.querySelectorAll(".plugin-mobile-details, .plugin-mobile-details .ant-btn")]
.filter(element => element.scrollWidth > element.clientWidth + 1).length,
}));
assert.equal(dimensions.scrollWidth, width);
assert.equal(dimensions.columns, width < 768 ? 1 : 3);
assert.equal(dimensions.overflow, 0);
assert.ok(dimensions.brand < 50);
console.log("Layout", width, dimensions);
}
for (const width of [390, 1440]) {
await page.setViewportSize({ width, height: 900 });
await page.goto("http://127.0.0.1:5173/plugins");
const update = page.getByRole("button", { name: "Update to 2.1.0", exact: true }).filter({ hasNot: page.locator("[disabled]") });
await page.locator(".ant-table-row").filter({ hasText: "Global Chat" }).getByRole("button").click();
await page.getByRole("dialog", { name: "Update Global Chat 2.1.0" }).waitFor();
assert.equal(new URL(page.url()).pathname, "/plugins");
await page.getByRole("button", { name: "Cancel", exact: true }).click();
await page.getByRole("button", { name: "Restart pending (2)" }).click();
await page.getByRole("dialog", { name: "Restart machines with a pending restart" }).waitFor();
await page.getByRole("button", { name: "Cancel", exact: true }).click();
await page.getByRole("button", { name: "Account menu for Cooldude2606" }).click();
await page.getByRole("menuitem", { name: "Cooldude2606" }).waitFor();
await page.keyboard.press("Escape");
await page.locator(".page-header-title").click();
await page.locator(".ant-table-filter-trigger").click();
await page.getByPlaceholder("Search plugins").fill("Global");
assert.equal(await page.locator(".ant-table-row").count(), 1);
await page.getByPlaceholder("Search plugins").press("Enter");
await page.reload();
await page.getByRole("link", { name: "Global Chat", exact: true }).waitFor();
assert.equal(await page.locator(".ant-table-row").count(), 1);
await page.getByRole("link", { name: "Global Chat", exact: true }).click();
await page.getByText("Plugin navigation target").waitFor();
await page.goto("http://127.0.0.1:5173/plugins");
await page.getByRole("columnheader", { name: /Name/ }).click();
await page.waitForFunction(() => document.querySelector(".ant-table-row")?.textContent.startsWith("Research Sync"));
assert.match(await page.locator(".ant-table-row").first().innerText(), /Research Sync/);
await page.locator(".ant-table-row").first().click();
await page.getByText("Plugin navigation target").waitFor();
console.log("Interactions passed", width);
}
assert.deepEqual(errors, []);
await browser.close();
+945
View File
@@ -0,0 +1,945 @@
const fs = require('fs-extra');
const https = require('follow-redirects').https;
const needle = require("needle");
const child_process = require('child_process');
const path = require('path');
const syncRequest = require('sync-request');
const request = require("request");
const ncp = require('ncp').ncp;
const Rcon = require('simple-rcon');
const hashFiles = require('hash-files');
const _ = require('underscore');
const deepmerge = require("deepmerge");
const getMac = require('getmac').getMac;
const rmdirSync = require('rmdir-sync');
const ioClient = require("socket.io-client");
// internal libraries
const objectOps = require("./lib/objectOps.js");
const fileOps = require("_app/fileOps");
const stringUtils = require("./lib/stringUtils.js");
const modManager = require("./lib/manager/modManager.js");
const configManager = require("./lib/manager/configManager.js");
// require config.json
var config = require('./config');
var global = {};
if (!fs.existsSync("./instances/")) {
fs.mkdirSync("instances");
}
if (!fs.existsSync("./sharedPlugins/")) {
fs.mkdirSync("sharedPlugins");
}
if (!fs.existsSync("./sharedMods/")) {
fs.mkdirSync("sharedMods");
}
const instance = process.argv[3];
const instancedirectory = './instances/' + instance;
const command = process.argv[2];
// Set the process title, shows up as the title of the CMD window on windows
// and as the process name in ps/top on linux.
process.title = "clusterioClient "+instance;
var instanceInfo = {};
// function to handle sending commands into the game
function messageInterface(command, callback) {
// try to save us if you send a buffer instead of string
if(typeof command == "object") {
command = command.toString('utf8');
}
if(process.platform == "linux" && typeof command == "string" && serverprocess) {
/*
to send to stdin, use:
serverprocess.stdin.write("/c command;\n")
*/
serverprocess.stdin.write(command+"\n");
if(typeof callback == "function"){
callback();
}
} else if(typeof command == "string" && client && client.exec && typeof client.exec == "function") {
try {
client.exec(command+"\n", callback);
} catch (err) {
console.log(err);
}
if(typeof callback == "function"){
callback();
}
}
}
// handle commandline parameters
if (!command || command == "help" || command == "--help") {
console.error("Usage: ");
console.error("node client.js start [instance name]");
console.error("node client.js list");
console.error("node client.js delete [instance]");
console.error("To download the latest version of the Clusterio lua mod, do");
console.error("node client.js manage shared mods download clusterio");
console.error("For more management options, do");
console.error("node client.js manage");
process.exit(1);
} else if (command == "list") {
let instanceNames = fileOps.getDirectoriesSync("./instances/");
instanceNames.unshift("Name:");
let longestInstanceName = 0;
// determine longest instance name
instanceNames.forEach(function(instance){
if(instance.length > longestInstanceName) longestInstanceName = instance.length;
});
let displayLines = [];
// push name coloumn to array
instanceNames.forEach(function(instance){
while(instance.length < longestInstanceName+1){
instance += " ";
}
displayLines.push("| "+ instance + "|");
});
// create port colloumn
let factorioPorts = [];
instanceNames.forEach(function(instance){
let factorioPort
if(instance.includes("Name:")){
factorioPort = "Port:"
} else {
factorioPort = require("./instances/"+instance+"/config").factorioPort;
}
factorioPorts.push(factorioPort);
});
factorioPorts.forEach((port, index) => {
let longestPort = 0;
factorioPorts.forEach((port, index) => {
if(port.toString().length > longestPort) longestPort = port.toString().length;
});
while(port.toString().length < longestPort){
port += " ";
}
factorioPorts[index] = port;
});
instanceNames.forEach(function(instance, index){
displayLines[index] += " " + factorioPorts[index] + " |";
});
displayLines.forEach(line => console.log(line));
process.exit(1);
} else if (command == "manage"){
// console.log("Launching mod manager");
//const fullUsage = 'node client.js manage [instance, "shared"] ["mods", "config"] ...';
function usage(instance, tool, action){
if(tool && tool == "mods"){
console.log('node client.js manage '+instance+' '+tool+' ["list", "search", "add", "remove", "update"]');
} else if(tool && tool == "config") {
console.log('node client.js manage '+instance+' '+tool+' ["list", "edit"]');
} else {
console.log('node client.js manage '+(instance || '[instance, "shared"]') +' '+ (tool || '["mods", "config"]') + ' ...');
}
}
const tool = process.argv[4] || "";
const action = process.argv[5] || "";
if(instance){
if(tool == "mods"){
// allow managing mods
if(action == "list"){
modManager.listMods(instance);
} else if(action == "search"){
modManager.findMods(process.argv[6]);
} else if(action == "add" || action == "download"){
modManager.addMod(process.argv[6], instance);
} else if(action == "remove" || action == "rm" || action == "delete"){
modManager.removeMod(process.argv[6], instance);
} else if(action == "update"){
modManager.updateAllMods();
} else {
usage(instance, tool);
}
} else if(tool == "config"){
// allow managing the config
if(action == "list" || action == "show" || action == "display"){
configManager.displayConfig(instance);
} else if(action == "edit"){
let newConfigValue = "";
process.argv.forEach((arg, i)=>{
if(i >= 8){
newConfigValue += " "+arg;
} else if(i >= 7){
newConfigValue += arg;
}
});
configManager.editConfig(instance, process.argv[6], newConfigValue);
} else {
usage(instance, tool);
}
} else {
usage(instance);
}
} else {
console.log('Usage:');
usage(instance);
}
// process.exit(1);
} else if (command == "delete") {
if (!process.argv[3]) {
console.error("Usage: node client.js delete [instance]");
process.exit(1);
} else if (typeof process.argv[3] == "string" && fs.existsSync("./instances/" + process.argv[3]) && process.argv[3] != "/" && process.argv[3] != "") {
fileOps.deleteFolderRecursiveSync("./instances/" + process.argv[3]);
console.log("Deleted instance " + process.argv[3]);
process.exit(1);
} else {
console.error("Instance not found: " + process.argv[3]);
process.exit(1);
}
} else if (command == "download") {
console.log("Downloading mods...");
// get JSON data about releases
let res = syncRequest('GET', 'https://api.github.com/repos/Danielv123/factorioClusterioMod/releases', {"headers":{"User-Agent":"Fuck you for requiring user agents!"}});
let url = JSON.parse(res.getBody())[0].assets[0].browser_download_url;
let name = JSON.parse(res.getBody())[0].assets[0].name;
if(url) {
console.log(url);
let file = fs.createWriteStream("sharedMods/"+name);
https.get(url, function(response) {
response.pipe(file);
console.log("Downloaded "+name);
});
}
} else if (command == "start" && instance === undefined) {
console.log("ERROR: No instanceName provided!");
console.log("Usage: node client.js start [instanceName]");
process.exit(0);
} else if (command == "start" && typeof instance == "string" && instance != "/" && !fs.existsSync(instancedirectory)) {
// if instance does not exist, create it
console.log("Creating instance...");
fs.mkdirSync(instancedirectory);
fs.mkdirSync(instancedirectory + "/script-output/");
fs.mkdirSync(instancedirectory + "/saves/");
fs.writeFileSync(instancedirectory + "/script-output/output.txt", "");
fs.writeFileSync(instancedirectory + "/script-output/orders.txt", "");
fs.writeFileSync(instancedirectory + "/script-output/txbuffer.txt", "");
fs.mkdirSync(instancedirectory + "/mods/");
fs.mkdirSync(instancedirectory + "/instanceMods/");
// fs.symlinkSync('../../../sharedMods', instancedirectory + "/mods", 'junction') // This is broken because it can only take a file as first argument, not a folder
fs.writeFileSync(instancedirectory + "/config.ini", "[path]\r\n\
read-data=__PATH__executable__/../../data\r\n\
write-data=__PATH__executable__/../../../instances/" + instance + "\r\n\
");
// this line is probably not needed anymore but Im not gonna remove it
fs.copySync('sharedMods', instancedirectory + "/mods");
let instconf = {
"factorioPort": process.env.FACTORIOPORT || Math.floor(Math.random() * 65535),
"clientPort": process.env.RCONPORT || Math.floor(Math.random() * 65535),
"clientPassword": Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 8),
}
console.log("Clusterio | Created instance with settings:")
console.log(instconf);
// create instance config
fs.writeFileSync(instancedirectory + "/config.json", JSON.stringify(instconf, null, 4));
let name = "Clusterio instance: " + instance;
if (config.username) {
name = config.username + "'s clusterio " + instance;
}
let serversettings = {
"name": name,
"description": config.description,
"tags": ["clusterio"],
"max_players": "20",
"visibility": config.visibility,
"username": config.username,
"token": config.token,
"game_password": config.game_password,
"verify_user_identity": config.verify_user_identity,
"admins": [config.username],
"allow_commands": config.allow_commands,
"autosave_interval": 10,
"autosave_slots": 5,
"afk_autokick_interval": 0,
"auto_pause": config.auto_pause,
}
fs.writeFileSync(instancedirectory + "/server-settings.json", JSON.stringify(serversettings, null, 4));
let createSave = child_process.spawnSync(
'./' + config.factorioDirectory + '/bin/x64/factorio', [
'-c', instancedirectory + '/config.ini',
'--create', instancedirectory + '/saves/save.zip',
]
);
console.log("Instance created!");
} else if (command == "start" && typeof instance == "string" && instance != "/" && fs.existsSync(instancedirectory)) {
// Exit if no instance specified (it should be, just a safeguard);
if(instancedirectory != "./instances/undefined"){
var instanceconfig = require(instancedirectory + '/config');
instanceconfig.unique = stringUtils.hashCode(instanceconfig.clientPassword);
if(process.env.FACTORIOPORT){
instanceconfig.factorioPort = process.env.FACTORIOPORT;
}
if(process.env.RCONPORT){
instanceconfig.rconPort = process.env.RCONPORT;
}
} else {
process.exit(1);
}
console.log("Deleting .tmp.zip files");
let savefiles = fs.readdirSync(instancedirectory + "/saves/");
for(i = 0; i < savefiles.length; i++){
if(savefiles[i].substr(savefiles[i].length - 8, 8) == ".tmp.zip") {
fs.unlinkSync(instancedirectory + "/saves/" + savefiles[i]);
}
}
console.log("Deleting logs");
// clean old log file to avoid crash
// file exists, delete so we don't get in trouble
try {
fs.unlinkSync(instancedirectory+'/factorio-current.log');
} catch (err){
if(err){
console.log(err);
} else {
console.log("Clusterio | Deleting old logs...");
}
}
// move mods from ./sharedMods to the instances mod directory
try {
fs.mkdirSync(instancedirectory + "/instanceMods/");
} catch(e){}
try{rmdirSync(instancedirectory + "/mods");}catch(e){}
try {
// mods directory that will be emptied (deleted) when closing the server to facilitate seperation of instanceMods and sharedMods
fs.mkdirSync(instancedirectory + "/mods/");
} catch(e){}
console.log("Clusterio | Moving shared mods from sharedMods/ to instance/mods...");
fs.copySync('sharedMods', instancedirectory + "/mods");
console.log("Clusterio | Moving instance specific mods from instance/instanceMods to instance/mods...");
fs.copySync(instancedirectory + "/instanceMods", instancedirectory + "/mods");
process.on('SIGINT', function () {
console.log("Caught interrupt signal");
messageInterface("/quit");
});
// Spawn factorio server
//var serverprocess = child_process.exec(commandline);
fileOps.getNewestFile(instancedirectory + "/saves/", fs.readdirSync(instancedirectory + "/saves/"),function(err, latestSave) {
if(err) {
console.log("Your savefile seems to be missing. This might because you created an instance without having factorio\
installed and configured properly. Try installing factorio and adding your savefile to instances/[instancename]/saves/");
throw err;
}
// implicit global
serverprocess = child_process.spawn(
'./' + config.factorioDirectory + '/bin/x64/factorio', [
'-c', instancedirectory + '/config.ini',
'--start-server', latestSave.file,
'--rcon-port', Number(process.env.RCONPORT) || instanceconfig.clientPort,
'--rcon-password', instanceconfig.clientPassword,
'--server-settings', instancedirectory + '/server-settings.json',
'--port', Number(process.env.FACTORIOPORT) || instanceconfig.factorioPort
], {
'stdio': ['pipe', 'pipe', 'pipe']
}
);
serverprocess.on('close', (code) => {
console.log(`child process exited with code ${code}`);
process.exit();
});
serverprocess.stdout.on("data", (data) => {
// log("Stdout: " + data);
if(data.toString('utf8').includes("Couldn't parse RCON data: Maximum payload size exceeded")){
console.error("ERROR: RCON CONNECTION BROKE DUE TO TOO LARGE PACKET!");
console.error("Attempting reconnect...");
client.close();
client.connect();
}
if(process.platform == "linux"){
// we have to log on linux because linux only shows stdout of the first process launched
console.log('Factorio: ' + data);
}
});
serverprocess.stderr.on('data', (chunk) => {
console.log('ERR: ' + chunk);
});
// connect to the server with rcon
if(process.platform != "linux"){
// IP, port, password
client = new Rcon({
host: 'localhost',
port: Number(process.env.RCONPORT) || instanceconfig.clientPort,
password: instanceconfig.clientPassword,
timeout: 0
});
// check the logfile to see if the RCON interface is running as there is no way to continue without it
// we read the log every 2 seconds and stop looping when we start connecting to factorio
function checkRcon() {
fs.readFile(instancedirectory+"/factorio-current.log", function (err, data) {
// if (err) console.log(err);
if(data && data.indexOf('Starting RCON interface') > 0){
client.connect();
} else {
setTimeout(function(){
checkRcon();
},2000);
}
});
}
setTimeout(checkRcon, 5000);
client.on('authenticated', function () {
console.log('Clusterio | RCON Authenticated!');
instanceManagement(); // start using rcons
}).on('connected', function () {
console.log('Clusterio | RCON Connected!');
// getID();
}).on('disconnected', function () {
console.log('Clusterio | RCON Disconnected!');
process.exit(0); // exit because RCON disconnecting is undefined behaviour and we rather just wanna restart now
});
} else if(process.platform == "linux"){
// TODO: Check if this works fine on linux or if it has to be delayed until the server starts properly
instanceManagement();
}
// set some globals
confirmedOrders = [];
lastSignalCheck = Date.now();
});
}
// ensure instancemanagement only ever runs once
_.once(instanceManagement);
function instanceManagement() {
/* Open websocket connection to master */
var socket = ioClient("http://"+config.masterIP+":"+config.masterPort);
socket.on("hello", data => {
console.log("SOCKET | registering slave!");
socket.emit("registerSlave", {
instanceID: instanceconfig.unique,
});
setInterval(B=> socket.emit("heartbeat"), 10000);
});
console.log("Started instanceManagement();");
// load plugins and execute onLoad event
let pluginDirectories = fileOps.getDirectoriesSync("./sharedPlugins/");
let plugins = [];
for(let i=0; i<pluginDirectories.length; i++) {
let I = i
let log = function(message) {
console.log("Clusterio | "+ pluginDirectories[I] + " | " + message);
}
// these are our two config files. We need to send these in case plugin
// wants to contact master or know something.
let combinedConfig = deepmerge(instanceconfig,config,{clone:true})
let pluginConfig = require("./sharedPlugins/" + pluginDirectories[i] + "/config.js");
if(!global.subscribedFiles) {
global.subscribedFiles = {};
}
if(pluginConfig.binary == "nodePackage"){
// require index.js.main() of plugin and execute it as a class
let pluginClass = require("./sharedPlugins/" + pluginDirectories[I] + "/index.js");
plugins[I] = new pluginClass(combinedConfig, function(data){
if(data.toString('utf8')[0] != "/") {
log("Stdout: " + data.toString('utf8'));
} else {
messageInterface(data.toString('utf8'));
}
}, { // extra functions to pass in object. Should have done it like this from the start, but won't break backwards compat.
socket, // socket.io connection to master (and ES6 destructuring, yay)
});
if(pluginConfig.scriptOutputFileSubscription && typeof pluginConfig.scriptOutputFileSubscription == "string"){
if(global.subscribedFiles[pluginConfig.scriptOutputFileSubscription]) {
// please choose a unique file to subscribe to. If you need plugins to share this interface, set up a direct communication
// between those plugins instead.
throw "FATAL ERROR IN " + pluginDirectories[i] + " FILE ALREADY SUBSCRIBED " + pluginConfig.scriptOutputFileSubscription;
}
if (!fs.existsSync(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription)) {
// Do something
fs.writeFileSync(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription, "");
}
global.subscribedFiles[pluginConfig.scriptOutputFileSubscription] = true;
console.log("Clusterio | Registered file subscription on "+instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription);
fs.watch(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription, fileChangeHandler);
// run once in case a plugin wrote out information before the plugin loaded fully
fileChangeHandler(false, pluginConfig.scriptOutputFileSubscription);
// send file contents to plugin for processing
function fileChangeHandler(eventType, filename) {
if(filename != null){
setTimeout(
()=>{
// get array of lines in file
let stuff = fs.readFileSync(instancedirectory + "/script-output/" + filename, "utf8").split("\n");
// if you found anything, reset the file
if (stuff[0]) {
fs.writeFileSync(instancedirectory + "/script-output/" + filename, "");
}
for(let i = 0; i < stuff.length; i++) {
if(stuff[i] && !stuff[i].includes('\u0000\u0000')) {
plugins[I].scriptOutput(stuff[i]);
}
}
},
pluginConfig.fileReadDelay || 0
);
}
}
}
console.log("Clusterio | Loaded plugin " + pluginDirectories[i]);
} else if(pluginConfig.binary != "nodePackage"){
// handle as fragile executable plugin
let args = pluginConfig.args || [];
plugins[I]=child_process.spawn(pluginConfig.binary, args, {
cwd: "./sharedPlugins/"+pluginDirectories[i],
stdio: ['pipe', 'pipe', 'pipe'],
});
/*
to send to stdin, use:
spawn.stdin.write("text\n");
*/
// If plugin has subscribed to a file, send any text appearing in that file to stdin
if(pluginConfig.scriptOutputFileSubscription && typeof pluginConfig.scriptOutputFileSubscription == "string") {
if(global.subscribedFiles[pluginConfig.scriptOutputFileSubscription]) {
// please choose a unique file to subscribe to. If you need plugins to share this interface, set up a direct communication
// between those plugins instead.
throw "FATAL ERROR IN " + pluginDirectories[i] + " FILE ALREADY SUBSCRIBED " + pluginConfig.scriptOutputFileSubscription;
}
if (!fs.existsSync(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription)) {
// Do something
fs.writeFileSync(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription, "");
}
global.subscribedFiles[pluginConfig.scriptOutputFileSubscription] = true;
fs.watch(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription, function (eventType, filename) {
// get array of lines in file
// TODO async
let stuff = fs.readFileSync(instancedirectory + "/script-output/" + filename, "utf8").split("\n");
// if you found anything, reset the file
if (stuff[0]) {
fs.writeFileSync(instancedirectory + "/script-output/" + filename, "");
}
for (let i = 0; i < stuff.length; i++) {
if (stuff[i]) {
plugins[I].stdin.write(stuff[i]);
}
}
});
}
// these are our two config files. We need to send these in case plugin
// wants to contact master or know something.
// send through script-output file, maybe more compat?
fs.writeFileSync(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription, JSON.stringify(combinedConfig)+"\r\n");
// send directly through stdin
// plugins[i].stdin.write(JSON.stringify(combinedConfig)+"\n");
console.log("Clusterio | Loaded plugin " + pluginDirectories[i]);
plugins[i].stdout.on("data", (data) => {
if(data.toString('utf8')[0] != "/") {
log("Stdout: " + data.toString('utf8'))
} else {
messageInterface(data.toString('utf8'));
}
});
plugins[i].stderr.on("data", (data) => {
log("STDERR: " + data);
});
plugins[i].on('close', (code) => {
log(`child process exited with code ${code}`);
});
}
}
// world IDs ------------------------------------------------------------------
hashMods(instance, function(modHashes){
setInterval(getID, 10000);
getID();
function getID() {
messageInterface("/silent-command game.write_file('tempfile.txt', 'connected_players ' .. #game.connected_players .. '\\n', true, 0)", function(err) {setTimeout(function(){
// get array of lines in file
if(fs.existsSync(instancedirectory + "/script-output/tempfile.txt")) {
var data = fs.readFileSync(instancedirectory + "/script-output/tempfile.txt", "utf8").split("\n");
// delete when we are done
fs.unlink(instancedirectory + "/script-output/tempfile.txt", function(){});
}
// if we actually got anything from the file, proceed to categorize it
if (data && data[0]) {
while (data[0]) {
let q = data[0].split(" ");
// delete array element
data.splice(0,1);
if(q[0] == "connected_players" && Number(q[1]) != NaN) {
instanceInfo.playerCount = q[1];
}
}
} else {
instanceInfo.playerCount = 0;
}
var payload = {
time: Date.now(),
rconPort: instanceconfig.clientPort,
rconPassword: instanceconfig.clientPassword,
serverPort: instanceconfig.factorioPort,
unique: instanceconfig.unique,
publicIP: config.publicIP, // IP of the server should be global for all instances, so we pull that straight from the config
mods:modHashes,
playerCount: instanceInfo.playerCount || 0,
instanceName: instance,
}
function callback(err, mac) {
if (err) {
console.log("##### getMac crashed, but we don't really give a shit because we are probably closing down #####");
} else {
payload.mac = mac
console.log("Registered our precense with master "+config.masterIP+" at " + payload.time);
needle.post(config.masterIP + ":" + config.masterPort + '/api/getID', payload, function (err, response, body) {
if (err && err.code != "ECONNRESET"){
console.error("We got problems, something went wrong when contacting master");
console.error(err);
} else if (response && response.body) {
// In the future we might be interested in whether or not we actually manage to send it, but honestly I don't care.
console.log(response.body);
}
});
}
}
if(global.mac){
callback(undefined, global.mac);
} else {
getMac(callback);
}
},1000)});
}
});
// Mod uploading and management -----------------------------------------------
// get mod names and hashes
// string: instance, function: callback
setTimeout(function(){hashMods(instance, uploadMods)}, 5000);
function uploadMods(modHashes) {
// [{modName:string,hash:string}, ... ]
for(let i=0;i<modHashes.length;i++){
let payload = {
modName: modHashes[i].modName,
hash: modHashes[i].hash,
}
needle.post(config.masterIP + ":" + config.masterPort + '/api/checkMod', payload, function (err, response, body) {
if(err) console.log(new Error("Unable to contact master server! Please check your config.json."));
if(response && body && body == "found") {
console.log("master has mod");
} else if (response && body && typeof body == "string") {
let mod = response.body;
if(config.uploadModsToMaster){
console.log("Sending mod: " + mod);
// Send mods master says it wants
// response.body is a string which is a modName.zip
var req = request.post("http://"+config.masterIP + ":" + config.masterPort + '/api/uploadMod', function (err, resp, body) {
if (err) {
console.log(new Error("Unable to contact master server! Please check your config.json."));
} else {
console.log('URL: ' + body);
}
});
var form = req.form();
form.append('file', fs.createReadStream("./instances/"+instance+"/mods/"+mod));
} else {
console.log("Not sending mod: " + mod + " to master because config.uploadModsToMaster is not enabled")
}
}
});
}
}
// flow/production statistics ------------------------------------------------------------
oldFlowStats = false
setInterval(function(){
fs.readFile(instancedirectory + "/script-output/flows.txt", {encoding: "utf8"}, function(err, data) {
if(!err && data) {
let timestamp = Date.now();
data = data.split("\n");
let flowStats = [];
for(let i = 0; i < data.length; i++) {
// try catch to remove any invalid json
try{
flowStats[flowStats.length] = JSON.parse(data[i]);
} catch (e) {
// console.log(" invalid json: " + i);
// some lines of JSON are invalid but don't worry, we just filter em out
}
}
// fluids
let flowStat1 = flowStats[flowStats.length-1].flows.player.input_counts
// items
let flowStat2 = flowStats[flowStats.length-2].flows.player.input_counts
// merge fluid and item flows
let totalFlows = {};
for(let key in flowStat1) totalFlows[key] = flowStat1[key];
for(let key in flowStat2) totalFlows[key] = flowStat2[key];
if(oldFlowStats && totalFlows && oldTimestamp) {
let payload = objectOps.deepclone(totalFlows);
// change from total reported to per time unit
for(let key in oldFlowStats) {
// get production per minute
payload[key] = Math.floor((payload[key] - oldFlowStats[key])/(timestamp - oldTimestamp)*60000);
if(payload[key] < 0) {
payload[key] = 0;
}
}
for(let key in payload) {
if(payload[key] == '0') {
delete payload[key];
}
}
console.log("Recorded flows, copper plate since last time: " + payload["copper-plate"]);
needle.post(config.masterIP + ":" + config.masterPort + '/api/logStats', {timestamp: timestamp, instanceID: instanceconfig.unique,data: payload}, function (err, response, body) {
// we did it, keep going
});
}
oldTimestamp = timestamp;
oldFlowStats = totalFlows;
fs.writeFileSync(instancedirectory + "/script-output/flows.txt", "");
}
});
// we don't need to update stats quickly as that could be expensive
}, 60000*5);
// provide items --------------------------------------------------------------
// trigger when something happens to output.txt
fs.watch(instancedirectory + "/script-output/output.txt", function (eventType, filename) {
// get array of lines in file
let items = fs.readFileSync(instancedirectory + "/script-output/output.txt", "utf8").split("\n");
// if you found anything, reset the file
if (items[0]) {
fs.writeFileSync(instancedirectory + "/script-output/output.txt", "");
}
for (let i = 0; i < items.length; i++) {
if (items[i]) {
let g = items[i].split(" ");
g[0] = g[0].replace("\u0000", "");
// console.log("exporting " + JSON.stringify(g));
// send our entity and count to the master for him to keep track of
needle.post(config.masterIP + ":" + config.masterPort + '/api/place', {
name: g[0],
count: g[1],
instanceName: instance, // name of instance
instanceID: instanceconfig.unique, // a hash computed from the randomly generated rcon password
}, function (err, resp, body) {
if(body == "failure") console.error("#### Export failed! Lost: "+g[1]+" "+g[0]);
if(config.logItemTransfers){
if(body == "success") console.log(`Exported ${g[1]} ${g[0]} to master`);
}
});
}
}
});
// request items --------------------------------------------------------------
setInterval(function () {
// get array of lines in file
let items = fs.readFileSync(instancedirectory + "/script-output/orders.txt", "utf8").split("\n");
// if we actually got anything from the file, proceed and reset file
if (items[0]) {
fs.writeFileSync(instancedirectory + "/script-output/orders.txt", "");
// prepare a package of all our requested items in a more tranfer friendly format
var preparedPackage = {};
for (let i = 0; i < items.length; i++) {
(function (i) {
if (items[i]) {
items[i] = items[i].split(" ");
items[i][0] = items[i][0].replace("\u0000", "");
items[i][0] = items[i][0].replace(",", "");
if (preparedPackage[items[i][0]]) {
// if we have buffered some already, sum the new items
if (typeof Number(preparedPackage[items[i][0]].count) == "number" && typeof Number(items[i][1]) == "number") {
preparedPackage[items[i][0]] = {
"name": items[i][0],
"count": Number(preparedPackage[items[i][0]].count) + Number(items[i][1]),
"instanceName":instance,
"instanceID":instanceconfig.unique,
};
// else just add em in without summing
} else if (typeof Number(items[i][1]) == "number") {
preparedPackage[items[i][0]] = {
"name": items[i][0],
"count": Number(items[i][1]),
"instanceName":instance,
"instanceID":instanceconfig.unique,
};
}
// this condition will NEVER be triggered but we know how that goes
} else if (typeof Number(items[i][1]) == "number") {
preparedPackage[items[i][0]] = {
"name": items[i][0],
"count": Number(items[i][1]),
"instanceName":instance,
"instanceID":instanceconfig.unique,
};
}
}
})(i);
}
// request our items, one item at a time
for (let i = 0; i < Object.keys(preparedPackage).length; i++) {
// console.log(preparedPackage[Object.keys(preparedPackage)[i]]);
needle.post(config.masterIP + ":" + config.masterPort + '/api/remove', preparedPackage[Object.keys(preparedPackage)[i]], function (err, response, body) {
if (response && response.body && typeof response.body == "object") {
// buffer confirmed orders
confirmedOrders[confirmedOrders.length] = {
[response.body.name]: response.body.count,
}
if(config.logItemTransfers){
console.log(`Imported ${response.body.count} ${response.body.name} from master`);
}
}
});
}
// if we got some confirmed orders
// console.log("Importing " + confirmedOrders.length + " items! " + JSON.stringify(confirmedOrders));
sadas = JSON.stringify(confirmedOrders);
confirmedOrders = [];
// send our RCON command with whatever we got
messageInterface("/silent-command remote.call('clusterio', 'importMany', '" + sadas + "')");
sadas = null;
}
}, 1000);
// COMBINATOR SIGNALS ---------------------------------------------------------
// get inventory from Master and RCON it to our slave
setInterval(function () {
needle.get(config.masterIP + ":" + config.masterPort + '/api/inventory', function (err, response, body) {
if(err){
console.log("Unable to get JSON master/api/inventory, master might be unaccessible");
} else if (response && response.body) {
// Take the inventory we (hopefully) got and turn it into the format LUA accepts
if(Buffer.isBuffer(response.body)) {console.log(response.body.toString("utf-8")); throw new Error();} // We are probably contacting the wrong webserver
try {
var inventory = JSON.parse(response.body);
} catch (e){
console.log(e);
}
var inventoryFrame = {};
for (let i = 0; i < inventory.length; i++) {
inventoryFrame[inventory[i].name] = Number(inventory[i].count);
if(inventoryFrame[inventory[i].name] >= Math.pow(2, 31)){
inventoryFrame[inventory[i].name] = Math.pow(2, 30); // set it waaay lower, 31 -1 would probably suffice
}
}
inventoryFrame["signal-unixtime"] = Math.floor(Date.now()/1000);
// console.log("RCONing inventory! " + JSON.stringify(inventoryFrame));
messageInterface("/silent-command remote.call('clusterio', 'receiveInventory', '" + JSON.stringify(inventoryFrame) + "')");
}
});
}, 1000);
// Make sure world has its worldID
setTimeout(function(){
messageInterface("/silent-command remote.call('clusterio','setWorldID',"+instanceconfig.unique+")")
}, 20000);
/* REMOTE SIGNALLING
* send any signals the slave has been told to send
* Fetch combinator signals from the server
*/
socket.on("processCombinatorSignal", circuitFrameWithMeta => {
if(circuitFrameWithMeta && typeof circuitFrameWithMeta == "object" && circuitFrameWithMeta.frame && Array.isArray(circuitFrameWithMeta.frame)){
messageInterface("/silent-command remote.call('clusterio', 'receiveFrame', '"+JSON.stringify(circuitFrameWithMeta.frame)+"')");
}
});
// get outbound frames from file and send to master
// get array of lines in file, each line should correspond to a JSON encoded frame
let signals = fs.readFileSync(instancedirectory + "/script-output/txbuffer.txt", "utf8").split("\n");
// if we actually got anything from the file, proceed and reset file
let readingTxBufferSoon = false;
let txBufferClearCounter = 0;
fs.watch(instancedirectory + "/script-output/txbuffer.txt", "utf-8", (eventType, filename) => {
if(!readingTxBufferSoon){ // use a 100ms delay to avoid messing with rapid sequential writes from factorio (I think that might be a problem maybe?)
readingTxBufferSoon = true;
setTimeout(()=>{
txBufferClearCounter++;
fs.readFile(instancedirectory + "/script-output/txbuffer.txt", "utf-8", (err, signals) => {
signals = signals.split("\n");
if (signals[0]) {
//if(txBufferClearCounter > 500){
fs.writeFileSync(instancedirectory + "/script-output/txbuffer.txt", "");
// txBufferClearCounter = 0;
//}
// loop through all our frames
for (let i = 0; i < signals.length; i++) {
if (signals[i] && objectOps.isJSON(signals[i])) {
// signals[i] is a JSON array called a "frame" of signals. We timestamp it for storage on master
// then we unpack and RCON in this.frame to the game later.
let framepart = JSON.parse(signals[i]);
let doneframe = {
time: Date.now(),
frame: framepart, // thats our array of objects(single signals);
}
// send to master using socket.io, opened at the top of instanceManagement()
socket.emit("combinatorSignal", doneframe);
} else {
// console.log("Invalid jsony: "+typeof signals[i])
}
}
}
});
readingTxBufferSoon = false;
},100);
}
});
} // END OF INSTANCE START ---------------------------------------------------------------------
// string, function
// returns [{modName:string,hash:string}, ... ]
function hashMods(instanceName, callback) {
if(!callback) {
throw new Error("ERROR in function hashMods NO CALLBACK");
}
function callback2(hash, modName){
hashedMods[hashedMods.length] = {
modName: modName,
hash: hash,
}
// check if this callback has ran once for each mod
if(hashedMods.length == instanceMods.length) {
callback(hashedMods);
}
//console.log(modname);
}
let hashedMods = [];
/*let mods = fs.readdirSync("./sharedMods/")*/
let instanceMods = fs.readdirSync("./instances/"+instanceName+"/mods/");
if(instanceMods.length == 0){
callback({}); // there are no mods installed, return an empty object
}
for(let o=0;o<instanceMods.length;o++) {
if(path.extname(instanceMods[o]) != ".zip") {
instanceMods.splice(instanceMods.indexOf(instanceMods[o]), 1); // remove element from array, we can't hash unzipped mods
}
}
for(let i=0; i<instanceMods.length; i++){
let path = "./instances/"+instanceName+"/mods/"+instanceMods[i];
let name = instanceMods[i];
let options = {
files:path,
}
// options {files:[array of paths]}
hashFiles(options, function(error, hash) {
// hash will be a string if no error occurred
if(!error){
callback2(hash, name);
} else {
throw error;
}
});
}
}
+39
View File
@@ -0,0 +1,39 @@
{
"__comment": "Where factorio folder. Should be in the same folder as this config file.",
"factorioDirectory": "factorio",
"__comment1": "IP players should connect to to join your servers, should be portforwarded and stuff",
"publicIP": "localhost",
"__comment2": "IP the master web server runs at, can be ignored on the master system",
"masterIP": "localhost",
"masterPort": 8080,
"masterAuthToken": "",
"__commentAuthSecret": "This is a long random string generated by keyboard mashing. Please mash you own string for security.",
"masterAuthSecret": "anfeåpvunuennuusnvzunsrevunåzeufnewnufasiojfåqwoiuajknwkjfdabewfkjadfnewåfundjkcvnewmfqewrmn123",
"__comment3": "Your factorio.com username for factorio MP services, also username will be admin",
"username": "",
"__comment4": "Token from your factorio.com profile page. Required for MP and mod manager services.",
"token": "",
"__comment5": "General user settings",
"verify_user_identity": false,
"auto_pause": false,
"allow_commands": "admins-only",
"game_password": "",
"description": "This server runs Clusterio",
"visibility": {
"public": true,
"lan": true
},
"__comment6": "Statistic gathering statistics, may affect server performance (master memory usage especially)",
"itemStats": {
"maxEntries":60,
"entriesPerSecond": 0.01666666667
},
"logItemTransfers": false,
"uploadModsToMaster": true,
"allowRemoteCommandExecution": false
}
+10
View File
@@ -0,0 +1,10 @@
toc:
- name: Master Server API
- clusterioMaster
- name: Type declarations
- itemStack
- name: Internal modules
description: |
Here are some helper functions for navigation.
- modManager
- getFactorioLocale
-2
View File
@@ -1,2 +0,0 @@
<!doctype html>
<html><head><meta name="viewport" content="width=device-width, initial-scale=1"><title>Plugin layout fixture</title></head><body><div id="root"></div><script>window.webRoot = "/";</script><script type="module" src="/temp/plugin-layout/main.tsx"></script></body></html>
+49
View File
@@ -0,0 +1,49 @@
var forge = require("node-forge");
forge.options.usePureJavaScript = true;
var fs = require("fs");
var mkdirp = require("mkdirp");
var pki = forge.pki;
var rsa = forge.pki.rsa;
var startTime, endTime;
module.exports = function({
bits = 2048,
certificatePath = "database/certificate",
doLogging = false,
} = {}){
function log(t){
if(doLogging) console.log("lib/generateSSLcert.js | "+t);
}
log("Generating SSL certificate with "+bits+" bits at "+certificatePath);
startTime = Date.now();
var keypair = rsa.generateKeyPair({bits: bits, e: 0x10001});
endTime = Date.now();
log("Generated 2048bit keypair in: "+(endTime-startTime)+" ms");
startTime = Date.now();
var cert = pki.createCertificate();
cert.publicKey = keypair.publicKey;
cert.serialNumber = "01";
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date(Date.now()+(1000*60*60*24*365)); // valid for one year
// self-sign certificate
cert.sign(keypair.privateKey);
// Convert certificate to pem type and write it to a file
if(certificatePath[certificatePath.length-1] != "/") certificatePath += "/";
if(!fs.existsSync(certificatePath)) {
mkdirp.sync(certificatePath);
console.log("Created directory: "+certificatePath);
}
fs.writeFileSync(certificatePath+ "cert.key", pki.privateKeyToPem(keypair.privateKey));
fs.writeFileSync(certificatePath+ "cert.crt", pki.certificateToPem(cert));
endTime = Date.now();
log("Generated certificate in: "+(endTime-startTime)+" ms");
}
+25
View File
@@ -0,0 +1,25 @@
var assert = require("assert");
var fs = require("fs");
var fileOps = require("_app/fileOps");
var generateSSLcert = require("./generateSSLcert.js")
describe("generateSSLcert.js(options)", ()=>{
it("Creates a folder with a .crt and .key file in it", ()=>{
let certPath = "lib/certTestPlsDelete";
assert(!fs.existsSync(certPath)); // if it already exsits, the test might pass because of leftover data (which is bad)
generateSSLcert({
// bits:512, // supported, but I just use the default of 2048 for this test
certificatePath:certPath,
doLogging: false,
});
assert(fs.existsSync(certPath));
assert(fs.existsSync(certPath+"/cert.key")); // private key
assert(fs.existsSync(certPath+"/cert.crt")); // certificate file
// remove certificates and data from the test
fileOps.deleteFolderRecursiveSync(certPath);
}).timeout(10000);
});
+45
View File
@@ -0,0 +1,45 @@
/**
Module for grabbing factorio locale from a local factorio install
@module getFactorioLocale
*/
const fs = require("fs");
const ini = require("ini");
/**
Gets raw factorio locale from the .ini file.
Not available outside of module through exports.
@memberof getFactorioLocale
@inner
@param {string} factorioDirectory
@param {string} languageCode
@param {function} callback
*/
function getLocale(factorioDirectory, languageCode, callback){
var localeFile = factorioDirectory + '/data/base/locale/' + languageCode + '/base.cfg';
fs.readFile(localeFile, "utf8", (err, rawLocale) => {
callback(err, rawLocale);
});
}
/**
Gets factorios locale as an object, does not respect mods.
@param {string} factorioDirectory
@param {string} languageCode
@param {function} callback
*/
module.exports.asObject = function getLocaleAsObject(factorioDirectory, languageCode, callback){
if(typeof factorioDirectory != "string" || typeof languageCode != "string" || typeof callback != "function"){
throw "Error: wrong parameters provided";
}
getLocale(factorioDirectory, languageCode, (err, rawLocale) => {
if(err){
callback(err);
} else {
let processedLocale = ini.parse(rawLocale)
callback(undefined, processedLocale);
}
});
}
+74
View File
@@ -0,0 +1,74 @@
const assert = require("assert");
const getFactorioLocale = require("./getFactorioLocale");
describe("getFactorioLocale.js", () => {
describe("asObject()", () => {
it("requires 2 strings and a function as parameters", () => {
let validate = (err) => {
return err == "Error: wrong parameters provided"
}
let message = "getFactorioLocale is responding incorrectly to invalid arguments"
let callback = (err,object) => true;
assert.throws(() => getFactorioLocale.asObject(), validate, message);
assert.throws(() => getFactorioLocale.asObject("hello", "I forgot my callback"), validate, message);
assert.throws(() => getFactorioLocale.asObject(undefined, undefined, callback), validate, message);
// the proper way to do it
assert.doesNotThrow(() => getFactorioLocale.asObject("factorio_0.15.27", "en", callback), validate, message);
});
it("returns a nested object with base game locale information", (done) => {
getFactorioLocale.asObject("factorio", "en", callback);
function callback(err, object){
assert.equal(typeof object, "object");
assert.equal(object["entity-name"]["fish"], "Fish");
assert.equal(object["entity-name"]["small-lamp"], "Lamp");
Object.keys(object).forEach(key => {
// first level of the nested object is always an object
assert.equal(typeof object[key], "object");
Object.keys(object[key]).forEach(key2 => {
// second level of the nested object is always a string, nearly always truthy
if(key2 != "so-long-and-thanks-for-all-the-fish"){
assert.ok(object[key][key2]);
assert.equal(typeof object[key][key2], "string");
}
});
});
done();
}
});
it("has some duplicate keys", (done) => {
getFactorioLocale.asObject("factorio", "en", callback);
function callback(err, object){
let arrayOfKeys = [];
// collect all keys in a single array
Object.keys(object).forEach(key => {
arrayOfKeys.push(key);
Object.keys(object[key]).forEach(key2 => {
arrayOfKeys.push(key2);
});
});
// check for duplicates
let duplicateKeys = [];
let i = 0;
arrayOfKeys.forEach(key => {
let o = 0;
arrayOfKeys.forEach(key2 => {
if(i != o && key == key2){
duplicateKeys.push(key);
}
o++
});
i++;
});
// we got 704 but this is a very unexact number
assert(duplicateKeys.length > 10)
done();
}
});
});
});
+14
View File
@@ -0,0 +1,14 @@
const fileOps = require("_app/fileOps");
module.exports = {
sync: function(){
let instances;
try {
instances = fileOps.getDirectoriesSync("./instances/");
} catch (e) {
// there are no instances created yet, probably missing the folder
instances = [];
}
return instances;
}
}
+48
View File
@@ -0,0 +1,48 @@
/**
Provides tools for managing both masters and slaves config files.
@module configManager
*/
// require modules
const asTable = require("as-table").configure({delimiter: ' | '});
const fs = require("fs");
// internal requires
var config = require("./../../config");
const objectOps = require("./../objectOps.js");
/**
console.log()s each entry in the config
@param {string} instance Either undefined, "shared" or the instance to show the config for.
*/
module.exports.displayConfig = function(instance){
if(!instance || instance == "shared"){
let configArray = [];
Object.keys(config).forEach(key=>{
if(key != "__comment"){
if(typeof config[key] == "object"){
configArray.push({entry: key, value: JSON.stringify(config[key])});
} else configArray.push({entry: key, value: config[key]});
}
});
console.log(asTable(configArray));
}
}
module.exports.editConfig = function(instance, entry, newValue){
if(!instance || instance == "shared"){
if(config[entry] && newValue){
if(objectOps.isJSON(newValue)){
config[entry] = JSON.parse(newValue);
console.log("Changed config entry "+entry+" from "+JSON.stringify(config[entry])+" to "+JSON.stringify(newValue));
console.log("new config file:");
console.log(JSON.stringify(config, null, " "))
fs.writeFileSync("config.json", JSON.stringify(config, null, 4));
} else {
config[entry] = newValue;
console.log("Changed config entry "+entry+" from "+config[entry]+" to '"+newValue+"'");
console.log("new config file:");
console.log(JSON.stringify(config, null, " "))
fs.writeFileSync("config.json", JSON.stringify(config, null, 4));
}
}
}
};
View File
Whitespace-only changes.
+238
View File
@@ -0,0 +1,238 @@
/**
Provides tools to manage mods in a clusterio install
@module modManager
*/
const rightpad = require("right-pad");
const asTable = require("as-table").configure({delimiter: ' | '});
const moment = require("moment");
const syncRequest = require("sync-request");
const fs = require("fs");
const https = require('follow-redirects').https;
const factorioAPI = require("node-factorio-api");
const sLog = require("single-line-log").stdout;
const fileOps = require("_app/fileOps");
const config = require("./../../config");
const instances = require("./../getInstances.js").sync();
function ellipse(str, max){
return str.length > (max - 3) ? str.substring(0,max-3) + '...' : str;
}
// https://stackoverflow.com/questions/15900485/correct-way-to-convert-size-in-bytes-to-kb-mb-gb-in-javascript
function formatBytes(a,b){if(0==a)return"0 Bytes";var c=1024,d=b||2,e=["Bytes","KB","MB","GB","TB","PB","EB","ZB","YB"],f=Math.floor(Math.log(a)/Math.log(c));return parseFloat((a/Math.pow(c,f)).toFixed(d))+" "+e[f]};
/**
Lists mods for an instance as a table. Warning, handles printing internally.
@param {string} instance - Name of the instance or "shared"
@example
var modManager = require("modManager");
modManager.listMods("testInstance");
// console.logs table of mods installed on instance
modManager.listMods("shared");
// console.logs mods located in /sharedMods as a nice table
*/
module.exports.listMods = function(instance){
if(instance && instances.includes(instance)){
factorioAPI.init(false, 'instances/'+instance+'/instanceMods', 'instances/'+instance+'/saves', '0.16.15');
factorioAPI.authenticate({
username: config.username,
token: config.token,
require_ownership: false
}).then(token => {
console.log("* Currently installed:");
factorioAPI.readModZips().then(list => {
console.log(asTable(list.map(mod => {
return {
name:(mod.title || mod.name),
version:mod.version,
date:mod.date,
factorio_ver:mod.factorio_version,
author:mod.author,
}
})));
});
// Had issues earlier due to bug in node-factorio-api, see https://github.com/Danacus/node-factorio-api/issues/2
// bug is resolved, thanks to @Danacus and @brotherpot
factorioAPI.getModsFromSaves().then(list => {
console.log("* Mods used in save:");
console.log(asTable(list[0].mods))
});
}).catch(reason => {
console.log("Failed to authenticate to factorio.com! Please check your connection and the username+token used for authentication in config.json");
});
} else {
// list shared mods in /sharedMods/
factorioAPI.init(false, 'sharedMods/', '', '0.16.15');
factorioAPI.authenticate({
username: config.username,
token: config.token,
require_ownership: false
}).then(() => {
console.log("\r\n")
console.log("* Mods shared by all instances:")
factorioAPI.getInstalledMods().then(mods => {
console.log(asTable(mods));
});
});
}
// get JSON data about GH releases
/*let res = syncRequest('GET', 'https://api.github.com/repos/Danielv123/factorioClusterioMod/releases', {"headers":{"User-Agent":"Fuck you for requiring user agents!"}});
let url = JSON.parse(res.getBody())[0].assets[0].browser_download_url;
let name = JSON.parse(res.getBody())[0].assets[0].name;
console.log(JSON.parse(res.getBody()))
*/
}
/**
Searches mods.factorio.com for mods matching a search term and prints
the results to console as a neat looking table
@param {string} searchTerm What to search for
*/
module.exports.findMods = function(searchTerm){
factorioAPI.init(false);
factorioAPI.authenticate({
username: config.username,
token: config.token,
require_ownership: false
}).then(token => {
factorioAPI.searchMods(searchTerm).then(mods => {
console.log(asTable(mods.reverse().map(mod => {
return {
title:ellipse(mod.title, 50),
name:ellipse(mod.name, 30),
downloads:mod.downloads_count,
updated: moment(mod.updated_at).format("DD.MM.YY"),
owner:mod.owner,
//license: mod.license_name,
}
})))
})
});
}
module.exports.addMod = function(term, instance){
if(instance == "shared"){ // modDirectory has to end in /
var modDirectory = "sharedMods/";
} else {
var modDirectory = "instances/"+instance+"/instanceMods/";
}
if(term && term.length > 0){
if(term.includes("mods.factorio.com/mods/")){ // https://mods.factorio.com/mods/Danielv123/ChooChoo
// treat it as a link download
factorioAPI.init(false, modDirectory, "", "0.16.15");
factorioAPI.authenticate({
username: config.username,
token: config.token,
require_ownership: false
}).then(() => {
factorioAPI.searchMods(term.split("/")[term.split("/").length-1]).then(body => {
console.log("Im gonna download: "+body[0].name);
factorioAPI.downloadMod(body[0]).then(()=>{
console.log("Mod downloaded");
});
});
});
} else if(term.includes("github.com")){ // https://github.com/Danielv123/factorioClusterioMod
// treat it as a github releases download
let apiURL = "https://api.github.com/repos/" + term.split("/")[3] + "/" + term.split("/")[4] + "/releases"; // https://api.github.com/repos/Danielv123/factorioClusterioMod/releases
let res = syncRequest('GET', apiURL, {"headers":{"User-Agent":"Fuck you for requiring user agents!"}});
let url = JSON.parse(res.getBody())[0].assets[0].browser_download_url;
let name = JSON.parse(res.getBody())[0].assets[0].name;
if(url) {
console.log("Downloading " + url);
let file = fs.createWriteStream(modDirectory + name);
let bytesDownloaded = 0;
let downloadStartTime = Date.now();
https.get(url, function(response) {
let bytesToDownload = response.headers['content-length'];
response.pipe(file);
response.on("data", chunk => {
bytesDownloaded += chunk.length;
let downloadTimeElapsed = Date.now() - downloadStartTime;
let bytesPerSecond = bytesDownloaded / (downloadTimeElapsed/1000);
let timeToComplete = Math.floor((bytesToDownload - bytesDownloaded)/bytesPerSecond);
sLog("Downloading "+name+" "+(bytesDownloaded/bytesToDownload*100).toPrecision(3)+"% ("+formatBytes(bytesDownloaded)+" / "+formatBytes(bytesToDownload)+")\r\n"+
"about "+timeToComplete+" seconds remaining ("+formatBytes(bytesPerSecond)+"/s)");
}).on("end", x => {
sLog.clear();
console.log(" Downloaded in "+moment.utc(moment.duration(Date.now()-downloadStartTime).as('milliseconds')).format('mm:ss')); // because sLog.clear() doesn't seem to do the job of ending the line properly
});
});
}
} else {
// treat it as a search and download the top result
factorioAPI.init(false, modDirectory, "", "0.16.15");
factorioAPI.authenticate({
username: config.username,
token: config.token,
require_ownership: false
}).then(() => {
factorioAPI.searchMods(term).then(body => {
console.log("Im gonna download: "+body[0].name);
factorioAPI.downloadMod(body[0]).then(()=>{
console.log("Mod downloaded");
});
});
});
}
}
}
module.exports.removeMod = function removeMod(term, instance, force = false){
if(instance == "shared"){ // modDirectory has to end in /
var modDirectory = "sharedMods/";
} else {
var modDirectory = "instances/"+instance+"/instanceMods/";
}
if(term && typeof term == "string" && term.length > 0){
let mods = fs.readdirSync(modDirectory);
let modsMatchingFilter = mods.filter(mod => mod.toLowerCase().includes(term.toLowerCase()));
if(modsMatchingFilter.length < 1){
console.log("No mods found matching search");
} else if(modsMatchingFilter.length == 1){
console.log("Deleting "+modsMatchingFilter[0]);
fs.unlinkSync(modDirectory+modsMatchingFilter[0]);
console.log("Deleted.");
} else if(modsMatchingFilter.length > 1){
console.log("Found multiple mods, not doing anything");
modsMatchingFilter.forEach(mod => console.log(mod));
}
}
}
// 'https://api.github.com/repos/Danielv123/factorioClusterioMod/releases'
/**
Download mod from github repo by looking for the first binary download in
the latest release, saving it to /sharedMods
@param {string} url a github repo URL, for example https://api.github.com/repos/Danielv123/factorioClusterioMod/releases. Must link to /releases
*/
module.exports.downloadFromGitURL = function(url){
console.log("Downloading mod from "+url);
// get JSON data about releases
let res = syncRequest('GET', url, {"headers":{"User-Agent":"Please don't require setting a user agent"}});
let downloadUrl = JSON.parse(res.getBody())[0].assets[0].browser_download_url;
let name = JSON.parse(res.getBody())[0].assets[0].name;
console.log(JSON.parse(res.getBody()))
if(downloadUrl) {
console.log(downloadUrl);
let file = fs.createWriteStream("sharedMods/"+name);
https.get(downloadUrl, function(response) {
response.pipe(file);
console.log("Downloaded "+name);
});
}
}
/*
module.exports.findUpdates = function(){
console.log("Downloading mods...");
// get JSON data about releases
let res = syncRequest('GET', 'https://api.github.com/repos/Danielv123/factorioClusterioMod/releases', {"headers":{"User-Agent":"Fuck you for requiring user agents!"}});
let url = JSON.parse(res.getBody())[0].assets[0].browser_download_url;
let name = JSON.parse(res.getBody())[0].assets[0].name;
console.log(JSON.parse(res.getBody()))
}*/
View File
Whitespace-only changes.
+136
View File
@@ -0,0 +1,136 @@
var mkdirp = require("mkdirp");
var http = require('http');
var https = require('https');
var fs = require('fs');
var modManager = require("./manager/modManager.js");
modManager.addMod("clusterio", "shared");
function makeDirectory(path){
return new Promise((resolve, reject) => {
mkdirp(path, function(err){
if(err){
console.error("Unable to mkdirp "+path);
console.error(err);
resolve(false);
} else {
console.log("mkdirp "+path);
resolve(true);
}
});
});
}
function download(url, file, callback){
return new Promise((resolve, reject) => {
let pathArray = file.split("/");
pathArray.pop(); // remove filename from the end
if(pathArray.length >= 1){
// this might be an invalid path!
fs.stat(pathArray.join("/"), (err, stat) => {
if(err){
makeDirectory(pathArray.join("/")).then(()=>{
doDownload(url, file, callback);
});
} else {
doDownload(url, file, callback);
}
});
} else {
doDownload(url, file, callback);
}
function doDownload(url, file, callback){
var writeStream = fs.createWriteStream(file);
let protocol = http;
if(url.split(":")[0] == "https") protocol = https;
var request = protocol.get(url, function(response) {
response.pipe(writeStream);
response.on("end", () => {
if(callback && typeof callback == "function") callback(file);
console.log("Downloaded "+file+" from "+url);
resolve();
});
});
}
});
}
var directories = [
"database/linvodb",
"sharedMods",
"instances",
];
let downloadList = [
{
url: "http://fbpviewer.trakos.pl/images/spritesheet.json",
file: "static/pictures/spritesheet.js",
callback: function(file){
fs.writeFileSync("static/pictures/spritesheet.js", "export default\n"+fs.readFileSync("static/pictures/spritesheet.js"));
},
},
{
url: "http://fbpviewer.trakos.pl/images/spritesheet.png",
file: "static/pictures/spritesheet.png",
},
{
url: "https://code.jquery.com/jquery.js",
file: "static/external/jquery.js",
},
{
url: "https://code.jquery.com/ui/1.12.1/jquery-ui.js",
file: "static/external/jquery-ui.js",
},
{
url: "https://cdnjs.cloudflare.com/ajax/libs/canvasjs/1.7.0/canvasjs.js",
file: "static/external/canvasjs.js",
},
{
url: "https://momentjs.com/downloads/moment.min.js",
file: "static/external/moment.min.js",
},
{
url: "https://craig.global.ssl.fastly.net/js/mousetrap/mousetrap.min.js",
file: "static/external/mousetrap.min.js",
},
{
url: "https://fonts.googleapis.com/css?family=Roboto",
file: "static/external/css/Roboto.css",
},
{
url: "https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.4/socket.io.js",
file: "static/external/socket.io.js",
},
]
// initiate folder creations and downloads
processMkdirRecursive(processDownloadRecursive);
function processMkdirRecursive(callback){
let item = directories.shift();
makeDirectory(item).then(()=>{
if(directories.length == 0){
if(callback) callback();
} else {
processMkdirRecursive(callback);
}
});
}
function processDownloadRecursive(callback){
let item = downloadList.shift();
download(item.url, item.file, item.callback).then(()=>{
if(downloadList.length == 0){
if(callback) callback();
} else {
processDownloadRecursive(callback);
}
});
}
// generate SSL certificates if they are missing
if(!fs.existsSync("database/certificates") || !fs.existsSync("database/certificates/cert.crt") || !fs.existsSync("database/certificates/cert.key")){
var generateSSLcert = require("./generateSSLcert.js");
generateSSLcert({
certificatePath: "database/certificates",
doLogging: true,
});
}
+44
View File
@@ -0,0 +1,44 @@
/**
Helper functions related to objects
@module objectOps
*/
/**
Deep clones an object, is actually just JSON.parse(JSON.stringify()) but looks cleaner.
@param {object} obj to clone
@returns {object}
@example
a = {"thingy":"with a value"};
b = a;
a == b; // true
c = objectOps.deepclone(a);
a == c; // false
*/
module.exports.deepclone = function(obj){
return JSON.parse(JSON.stringify(obj));
}
/**
Checks if a string is valid JSON using try/catch. If passed an object it will always return true.
@param {string} string to verify
@returns {boolean}
@example
objectOps.isJSON("99 bottles of beer"); // false
objectOps.isJSON("99"); // true
objectOps.isJSON({"thingy":"that is an object"}); // true
objectOps.isJSON(JSON.stringify({"thingy":"that is an object"})); // true
*/
module.exports.isJSON = function(string){
let stringIsJson = false;
let x
try {
x = JSON.parse(string);
} catch (e){
return false;
}
if(typeof x == "object" || typeof string == "object"){
stringIsJson = true;
}
return stringIsJson;
}
+40
View File
@@ -0,0 +1,40 @@
var assert = require('assert');
var objectOps = require('./objectOps');
describe("objectOps.js", function(){
/*describe("objectOps.clone()", function(){
it("clones javascript objects", function(){
var object1 = {hello:"world", cat:{legs:4, name:"Kitty", colors:["brown", "yellow", "purple"]}};
var object2 = {};
object2 = objectOps.clone(object1);
assert.equal(object2.cat.name, object1.cat.name)
assert.equal(object1.hello, object2.hello)
});
it("does not deep clone objects", function(){
var obj1 = {hello:"world", cat:{legs:4, name:"Kitty", colors:["brown", "yellow", "purple"]}};
var obj2 = {};
obj2 = objectOps.clone(obj1);
obj1.cat.colors = "black";
assert.equal(obj1.cat.colors, "black");
assert.notEqual(obj2.cat.colors[1], "yellow");
});
});*/
describe("objectOps.deepclone()", function(){
it("deep clones javascript objects", function(){
var obj1 = {hello:"world", cat:{legs:4, name:"Kitty", colors:["brown", "yellow", "purple"]}};
var obj2 = {};
obj2 = objectOps.deepclone(obj1);
obj1.cat.colors = "black";
assert.equal(obj1.cat.colors, "black");
assert.equal(obj2.cat.colors[1], "yellow");
});
it("throws on non JSON parameters", function(){
assert.throws(function(){
let y = objectOps.deepclone(objectOps.deepclone);
});
});
});
});
+14
View File
@@ -0,0 +1,14 @@
function hashCode(string){
if(typeof string !== "string") throw "ERROR stringUtils: Not a string";
var hash = 0;
if (string.length == 0) return hash;
for (let i = 0; i < string.length; i++) {
char = string.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32bit integer
}
return hash;
}
module.exports = {
hashCode: hashCode,
}
+48
View File
@@ -0,0 +1,48 @@
const assert = require("assert");
const stringUtils = require("./stringUtils");
describe("stringUtils.js", function(){
describe("hashCode", function(){
it("Produces a 32bit signed int hash from a string", function(){
let x = stringUtils.hashCode("whatever");
let y = stringUtils.hashCode(" a much longer string to prove this works for long strings as well and also special characters &!¤(#)¤");
assert(typeof x == "number", "hashCode should be returning a 32 bit signed int");
assert(x.toString() == "1934383586");
assert(y.toString() == "1648886132");
});
let permutations = 1000;
it("Rarely produces same hash for different strings ("+permutations+" hashes with collision check)", function(){
let keyHashPairs = [];
for(let i = 0; i < permutations; i++){
let string = Math.random().toString();
keyHashPairs.push({
key:string,
hash:stringUtils.hashCode(string),
});
}
for(let i = 0; i < permutations; i++){
for(let o = 0; o < permutations; o++){
assert(i == o || keyHashPairs[o].key != keyHashPairs[i].key, "Woha, Math.random() generated 2 equal numbers???");
assert(i == o || keyHashPairs[o].hash != keyHashPairs[i].hash, "Hash collision, as long at it doesn't happen again its fine.");
}
}
});
it("Returns 0 for 0 length strings", function(){
let x = stringUtils.hashCode("");
assert.deepEqual(x, 0);
});
it("Throws when supplied with anything other than a string", function(){
assert.throws(function(){
stringUtils.hashCode([]);
}, "ERROR: Not a string")
assert.throws(function(){
stringUtils.hashCode({});
}, "ERROR: Not a string")
assert.throws(function(){
stringUtils.hashCode(0);
}, "ERROR: Not a string")
});
});
});
+11
View File
@@ -0,0 +1,11 @@
/**
An item with a count specified
@typedef {object} itemStack
@property {string} name The name of the item in the stack
@property {(number|string)} count The amount of items in the stack
@example
itemStack = {name:"iron-plate", count:"12"}
itemStack = {name:"copper-plate", count:972}
*/
-63
View File
@@ -1,63 +0,0 @@
import React from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { ConfigProvider, theme } from "antd";
import { PluginDetails } from "@clusterio/lib";
import SiteLayout from "../../packages/web_ui/src/components/SiteLayout.tsx";
import PluginsPage from "../../packages/web_ui/src/components/PluginsPage.tsx";
import ControlContext from "../../packages/web_ui/src/components/ControlContext.tsx";
import "../../packages/web_ui/src/style.css";
const specs = [
["awf_oarc", "awf_oarc", "0.2.16", "0.3.1"],
["auto_restart", "ExpGaming - Auto Restart", "7.0.5", "7.0.5"],
["global_chat", "Global Chat", "2.0.0", "2.1.0"],
["research_sync", "Research Sync", "2.1.0", "2.1.0"],
];
const plugins = specs.map(([name, title, version, latest]) => new PluginDetails(
name, title, version, "loaded", undefined, `@fixture/${name}`,
name === "awf_oarc" ? latest : version, `@fixture/${name}`,
));
const hostPlugins = plugins.map(plugin => plugin.name === "auto_restart"
? new PluginDetails(plugin.name, plugin.title, plugin.version, "noEntrypoint", undefined,
plugin.npmPackage, plugin.version, plugin.dependencyName) : plugin);
const store = entries => {
const snapshot = [new Map(entries), true, null];
return { subscribe: () => () => {}, getSnapshot: () => snapshot };
};
const meta = plugins.map(p => ({ name: p.name, version: p.version, npmPackage: p.npmPackage,
enabled: true, web: p.name === "awf_oarc" ? { error: "Fixture: web module could not load" } : {} }));
const originalFetch = window.fetch;
window.fetch = (url, ...rest) => String(url).endsWith("api/plugins")
? Promise.resolve(new Response(JSON.stringify(meta))) : originalFetch(url, ...rest);
const permissions = ["core.plugin.list", "core.plugin.search", "core.plugin.update", "core.plugin.install",
"core.controller.restart", "core.host.restart"];
const control = {
accountName: "Cooldude2606", accountRoles: [{ permissions }],
onAccountUpdate() {}, offAccountUpdate() {}, connector: {},
plugins: new Map(),
pluginInfos: new Map(specs.map(([name, title]) => [name, { name, title }])),
hosts: store([[1, { id: 1, name: "Host 1", connected: true }]]),
systems: store([["controller", { plugins, canRestart: true }], [1, { plugins: hostPlugins, canRestart: true }]]),
instances: store([]), users: store([]),
pages: [
...["Controller", "Hosts", "Instances", "Users"].map(title => ({ path: `/${title.toLowerCase()}`,
sidebarName: title, content: <div>{title}</div> })),
{ path: "/plugins", sidebarName: "Plugins", content: <PluginsPage /> },
{ path: "/plugins/:name/view", content: <div>Plugin navigation target</div> },
],
async send(request) {
if (request.constructor.name === "PluginLatestVersionsRequest") {
return { versions: Object.fromEntries(specs.map(([name, , , latest]) => [`@fixture/${name}`, latest])) };
}
throw new Error(`Unexpected fixture request: ${request.constructor.name}`);
},
async sendTo(address, request) {
if (request.constructor.name === "PluginListRequest") { return address === "controller" ? plugins : hostPlugins; }
throw new Error(`Unexpected fixture request: ${request.constructor.name}`);
},
};
window.fixtureControl = control;
createRoot(document.getElementById("root")).render(<ConfigProvider theme={{ algorithm: theme.darkAlgorithm }}>
<ControlContext.Provider value={control}><BrowserRouter><SiteLayout /></BrowserRouter></ControlContext.Provider>
</ConfigProvider>);
+990
View File
@@ -0,0 +1,990 @@
/**
Clusterio master server. Facilitates communication between slaves through
a webserver, storing data related to slaves like production graphs and
combinator signals.
@module clusterioMaster
@author Danielv123
@example
node master.js
*/
// Set the process title, shows up as the title of the CMD window on windows
// and as the process name in ps/top on linux.
process.title = "clusterioMaster";
// add better stack traces on promise rejection
process.on('unhandledRejection', r => console.log(r));
// configgy stuff
debug = false;
// constants
const masterModFolder = "./database/masterMods/";
var config = require('./config');
// homebrew modules
const getFactorioLocale = require("./lib/getFactorioLocale");
const stringUtils = require("./lib/stringUtils");
// Library for create folder recursively if it does not exist
const mkdirp = require("mkdirp");
mkdirp.sync("./database");
mkdirp.sync(masterModFolder);
const averagedTimeSeries = require("averaged-timeseries");
const deepmerge = require("deepmerge");
const path = require("path");
const fs = require("fs");
const nedb = require("nedb");
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const express = require("express");
const ejs = require("ejs");
// Required for express post requests
const bodyParser = require("body-parser");
const fileUpload = require('express-fileupload');
var app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(fileUpload());
// dynamic HTML generations with EJS
app.set('views', path.join(__dirname, 'static'));
app.set('view engine', 'html');
app.engine('html', ejs.renderFile);
var routes = require("./routes.js");
routes(app);
// Set folder to serve static content from (the website)
app.use(express.static('static'));
// mod downloads
app.use(express.static(masterModFolder));
// set up logging software
const prometheusPrefix = "clusterio_";
const Prometheus = require('prom-client');
Prometheus.collectDefaultMetrics({ timeout: 10000 }); // collects RAM usage etc every 10 s
const httpRequestDurationMilliseconds = new Prometheus.Histogram({
name: prometheusPrefix+'http_request_duration_ms',
help: 'Duration of HTTP requests in ms',
labelNames: ['route'],
// buckets for response time from 0.1ms to 500ms
buckets: [0.10, 5, 15, 50, 100, 200, 300, 400, 500]
});
const endpointHitCounter = new Prometheus.Gauge({
name: prometheusPrefix+'endpoint_hit_gauge',
help: "How many requests a particular endpoint has gotten",
labelNames: ['route'],
});
const prometheusConnectedInstancesCounter = new Prometheus.Gauge({
name: prometheusPrefix+'connected_instaces_gauge',
help: "How many instances are currently connected to this master server",
});
const prometheusWsUsageCounter = new Prometheus.Counter({
name: prometheusPrefix+'websocket_usage_counter',
help: 'Websocket traffic',
labelNames: ["connectionType"],
});
const prometheusProductionGauge = new Prometheus.Gauge({
name: prometheusPrefix+'production_gauge',
help: 'Items produced by instance',
labelNames: ["instanceID", "itemName"],
});
const prometheusPlayerCountGauge = new Prometheus.Gauge({
name: prometheusPrefix+'player_count_gauge',
help: 'Amount of players connected to this cluster',
labelNames: ["instanceID", "instanceName"],
});
const prometheusMasterInventoryGauge = new Prometheus.Gauge({
name: prometheusPrefix+'master_inventory_gauge',
help: 'Amount of items stored on master',
labelNames: ["itemName"],
});
setInterval(()=>{
let numberOfActiveSlaves = 0;
for(let instance in slaves){
if(Date.now() - Number(slaves[instance].time) < 1000 * 30) numberOfActiveSlaves++;
}
prometheusConnectedInstancesCounter.set(numberOfActiveSlaves);
},10000);
/**
GET Prometheus metrics endpoint. Returns performance and usage metrics in a prometheus readable format.
@memberof clusterioMaster
@instance
@alias /metrics
*/
app.get('/metrics', (req, res) => {
endpointHitCounter.labels(req.route.path).inc();
let reqStartTime = Date.now();
res.set('Content-Type', Prometheus.register.contentType);
/// gather some static metrics
// playercount
for(let instanceID in slaves){try{
prometheusPlayerCountGauge.labels(instanceID, slaves[instanceID].instanceName).set(Number(slaves[instanceID].playerCount) || 0);
}catch(e){}}
// inventory
for(let key in db.items){
if(typeof db.items[key] == "number" || typeof db.items[key] == "string"){
prometheusMasterInventoryGauge.labels(key).set(Number(db.items[key]) || 0);
}
}
res.end(Prometheus.register.metrics());
httpRequestDurationMilliseconds.labels(req.route.path).observe(Date.now()-reqStartTime);
});
// set up database
var Datastore = require('nedb');
db = {};
var LinvoDB = require("linvodb3");
LinvoDB.dbPath = "./database/linvodb/";
// database for items in system
// db.items = new Datastore({ filename: 'database/items.db', autoload: true });
// in memory database for combinator signals
db.signals = new Datastore({ filename: 'database/signals.db', autoload: true, inMemoryOnly: true});
db.signals.ensureIndex({ fieldName: 'time', expireAfterSeconds: 3600 }, function (err) {});
// production chart database
db.flows = new LinvoDB("flows", {}, {});
(function(){
try{
let x = fs.statSync("database/slaves.json");
console.log("loading slaves from database/slaves.json");
slaves = JSON.parse(fs.readFileSync("database/slaves.json"));
} catch (e){
slaves = {};
}
try{
x = fs.statSync("database/items.json");
console.log("loading items from database/items.json");
db.items = JSON.parse(fs.readFileSync("database/items.json"));
} catch (e){
db.items = {};
}
})()
db.items.addItem = function(object) {
if(object.name == "addItem" || object.name == "removeItem") {
console.error("Fuck you, that would screw everything up if you named your item that.");
return false;
} else {
if(this[object.name] && Number(this[object.name]) != NaN){
this[object.name] = Number(this[object.name]) + Number(object.count);
} else {
this[object.name] = object.count;
}
return true;
}
}
db.items.removeItem = function(object) {
if(object.name == "addItem" || object.name == "removeItem") {
console.error("Fuck you, that would screw everything up if you named your item that.");
return false;
} else {
if(this[object.name] && Number(this[object.name]) != NaN){
this[object.name] = Number(this[object.name]) - Number(object.count);
} else {
this[object.name] = 0;
}
return true;
}
}
// store slaves and inventory in a .json full of JSON data
process.on('SIGINT', function () {
console.log('Ctrl-C...');
// set insane limit to slave length, if its longer than this we are probably being ddosed or something
if(slaves && Object.keys(slaves).length < 2000000){
console.log("saving to slaves.json");
fs.writeFileSync("database/slaves.json", JSON.stringify(slaves));
} else if(slaves) {
console.log("Slave database too large, not saving ("+Object.keys(slaves).length+")");
}
if(db.items && Object.keys(db.items).length < 50000){
console.log("saving to items.json");
fs.writeFileSync("database/items.json", JSON.stringify(db.items));
} else if(slaves) {
console.log("Item database too large, not saving ("+Object.keys(slaves).length+")");
}
process.exit(2);
});
/**
POST World ID management. Slaves post here to tell the server they exist
@memberof clusterioMaster
@instance
@alias /api/getID
*/
app.post("/api/getID", function(req,res) {
endpointHitCounter.labels(req.route.path).inc();
let reqStartTime = Date.now();
// request.body should be an object
// {rconPort, rconPassword, serverPort, mac, time}
// time us a unix timestamp we can use to check for how long the server has been unresponsive
// we should save that somewhere and give appropriate response
if(req.body){
if(debug){
console.log(req.body)
}
if(!slaves[req.body.unique]){
slaves[req.body.unique] = {};
}
for(k in req.body){
if(k != "meta" && req.body.hasOwnProperty(k)){
slaves[req.body.unique][k] = req.body[k];
}
}
console.log("Slave registered: " + slaves[req.body.unique].mac + " : " + slaves[req.body.unique].serverPort+" at " + slaves[req.body.unique].publicIP + " with name " + slaves[req.body.unique].instanceName);
}
httpRequestDurationMilliseconds.labels(req.route.path).observe(Date.now()-reqStartTime);
});
/**
POST Allows you to add metadata related to slaves for other tools, like owner data, descriptions or statistics.
@memberof clusterioMaster
@instance
@alias /api/editSlaveMeta
*/
app.post("/api/editSlaveMeta", function(req,res) {
endpointHitCounter.labels(req.route.path).inc();
let reqStartTime = Date.now();
// request.body should be an object
// {instanceID, pass, meta:{x,y,z}}
if(req.body && req.body.instanceID && req.body.password && req.body.meta){
// check for editing permissions
if(slaves[req.body.instanceID] && slaves[req.body.instanceID].rconPassword == req.body.password){
if(!slaves[req.body.instanceID].meta){
slaves[req.body.instanceID].meta = {};
}
slaves[req.body.instanceID].meta = deepmerge(slaves[req.body.instanceID].meta, req.body.meta, {clone:true});
let metaPortion = JSON.stringify(req.body.meta);
if(metaPortion.length > 50) metaPortion = metaPortion.substring(0,20) + "...";
console.log("Updating slave "+slaves[req.body.instanceID].instanceName+": " + slaves[req.body.instanceID].mac + " : " + slaves[req.body.instanceID].serverPort+" at " + slaves[req.body.instanceID].publicIP, metaPortion);
} else {
res.send("ERROR: Invalid instanceID or password")
}
}
httpRequestDurationMilliseconds.labels(req.route.path).observe(Date.now()-reqStartTime);
});
/**
POST Get metadata from a single slave. For whenever you find the data returned by /api/slaves overkill.
@memberof clusterioMaster
@instance
@alias /api/getSlaveMeta
*/
app.post("/api/getSlaveMeta", function (req, res) {
console.log("body", req.body);
if(req.body && req.body.instanceID && req.body.password){
console.log("returning meta for ", req.body.instanceID);
res.send(JSON.stringify(slaves[req.body.instanceID].meta))
} else {
res.status(400);
res.send('{"INVALID REQUEST":1}');
}
});
// TODO: Remove deprecated function, also update researchSync to not use it
/**
POST DEPRECATED Get metadata from all slaves. This is already covered by /api/slaves, please use that one instead.
@memberof clusterioMaster
@instance
@alias /api/getSlavesMeta
*/
app.post("/api/getSlavesMeta", function (req, res) {
if(req.body.password){
let metas = {};
res.send(JSON.stringify(Object.keys(slaves).map(key => metas[key] = slaves[key].meta)));
} else {
res.status(400);
res.send('{"INVALID REQUEST":1}');
}
});
// mod management
// should handle uploading and checking if mods are uploaded
/**
POST Check if a mod has been uploaded to the master before. Only checks against filename, not hash.
@memberof clusterioMaster
@instance
@alias /api/checkMod
*/
app.post("/api/checkMod", function(req,res) {
endpointHitCounter.labels(req.route.path).inc();
let reqStartTime = Date.now();
let files = fs.readdirSync(masterModFolder);
let found = false;
files.forEach(file => {
if(file == req.body.modName) {
found = true;
}
});
if(!found) {
// we don't have mod, plz send
res.send(req.body.modName);
} else {
res.send("found");
}
res.end();
httpRequestDurationMilliseconds.labels(req.route.path).observe(Date.now()-reqStartTime);
});
/**
POST endpoint for uploading mods to the master server. Required for automatic mod downloads with factorioClusterioClient (electron app, see seperate repo)
@memberof clusterioMaster
@instance
@alias /api/uploadMod
*/
app.post("/api/uploadMod", function(req,res) {
endpointHitCounter.labels(req.route.path).inc();
let reqStartTime = Date.now();
if (!req.files) {
res.send('No files were uploaded.');
return;
} else {
console.log(req.files.file);
req.files.file.mv('./database/masterMods/'+req.files.file.name, function(err) {
if (err) {
res.status(500).send(err);
} else {
res.send('File uploaded!');
console.log("Uploaded mod: " + req.files.file.name);
}
httpRequestDurationMilliseconds.labels(req.route.path).observe(Date.now()-reqStartTime);
});
}
});
/**
GET endpoint for getting information about all our slaves
@memberof clusterioMaster
@instance
@alias /api/slaves
*/
app.get("/api/slaves", function(req, res) {
endpointHitCounter.labels(req.route.path).inc();
let reqStartTime = Date.now();
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
let copyOfSlaves = JSON.parse(JSON.stringify(slaves));
// filter out the rcon password because thats kindof not a safe thing to share
for(key in copyOfSlaves){
copyOfSlaves[key].rconPassword = "hidden";
}
res.send(copyOfSlaves);
httpRequestDurationMilliseconds.labels(req.route.path).observe(Date.now()-reqStartTime);
});
var recievedItemStatisticsBySlaveID = {};
var sentItemStatisticsBySlaveID = {};
/*
var recievedItemStatistics = new averagedTimeSeries({
maxEntries: config.itemStats.maxEntries,
entriesPerSecond: config.itemStats.entriesPerSecond,
}, console.log);*/
//
/**
POST endpoint for storing items in master's inventory.
@memberof clusterioMaster
@instance
@alias api/place
@param {itemStack} itemStack the number and type of items to store (see typedef)
@param {string} [itemStack.instanceID="unknown"] the unique/instanceID which is a numerical value for an instance
@param {string} [itemStack.instanceName="unknown"] the name of an instance for identification in statistics, as provided when launching it. ex node client.js start [name]
@returns {string} status either "success" or "failure"
*/
app.post("/api/place", function(req, res) {
endpointHitCounter.labels(req.route.path).inc();
let reqStartTime = Date.now();
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
let x;
try {
x = JSON.parse(req.body);
} catch (e) {
x = req.body;
}
if(!x.instanceName) {
x.instanceName = "unknown";
}
if(!x.instanceID) {
x.instanceID = "unknown";
Object.keys(slaves).forEach(instanceID => {
if(slaves[instanceID].instanceName == req.body.instanceName) {
x.instanceID = instanceID;
}
});
}
if( x.instanceID
&& x.instanceName
&& !isNaN(Number(x.count))// This is in no way a proper authentication or anything, its just to make sure everybody are registered as slaves before modifying the cluster (or not, to maintain backwards compat)
/*&& stringUtils.hashCode(slaves[x.unique].rconPassword) == x.unique*/){
if(config.logItemTransfers){
console.log("added: " + req.body.name + " " + req.body.count+" from "+x.instanceName+" ("+x.instanceID+")");
}
// gather statistics
let recievedItemStatistics = recievedItemStatisticsBySlaveID[x.instanceID];
if(recievedItemStatistics === undefined){
recievedItemStatistics = new averagedTimeSeries({
maxEntries: config.itemStats.maxEntries,
entriesPerSecond: config.itemStats.entriesPerSecond,
mergeMode: "add",
}, console.log);
recievedItemStatisticsBySlaveID[x.instanceID] = recievedItemStatistics;
}
recievedItemStatistics.add({
key:req.body.name,
value:req.body.count,
});
// save items we get
db.items.addItem({
name:req.body.name,
count:req.body.count
});
// Attempt confirming
res.send("success");
} else {
res.send("failure");
}
httpRequestDurationMilliseconds.labels(req.route.path).observe(Date.now()-reqStartTime);
});
/**
POST endpoint to remove items from DB when client orders items.
@memberof clusterioMaster
@instance
@alias api/remove
@param {itemStack} itemStack the name of and the number of items to remove (see typedef)
@param {string} [itemStack.instanceID="unknown"] the unique/instanceID which is a numerical value for an instance
@param {string} [itemStack.instanceName="unknown"] the name of an instance for identification in statistics, as provided when launching it. ex node client.js start [name]
@returns {itemStack} the number of items actually removed, may be lower than what was asked for due to shortages.
*/
_doleDivisionFactor = {}; //If the server regularly can't fulfill requests, this number grows until it can. Then it slowly shrinks back down.
app.post("/api/remove", function(req, res) {
endpointHitCounter.labels(req.route.path).inc();
let reqStartTime = Date.now();
const doleDivisionRetardation = 10; //lower rates will equal more dramatic swings
const maxDoleDivision = 250; //a higher cap will divide the store more ways, but will take longer to recover as supplies increase
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
// save items we get
var object = req.body;
if(!object.instanceID) {
object.instanceID = "unknown"
}
if(!object.instanceName) {
object.instanceName = "unknown";
}
if(slaves[object.instanceID]) object.instanceName = slaves[object.instanceID].instanceName;
let item = db.items[object.name]
// console.dir(doc);
if (!item) {
if(config.logItemTransfers){
console.log('failure could not find ' + object.name);
}
res.send({name:object.name, count:0});
} else {
const originalCount = Number(object.count) || 0;
object.count /= ((_doleDivisionFactor[object.name]||0)+doleDivisionRetardation)/doleDivisionRetardation;
object.count = Math.round(object.count);
if(item.length > 40) console.info(`Serving ${object.count}/${originalCount} ${object.name} from ${item} ${object.name} with dole division factor ${(_doleDivisionFactor[object.name]||0)} (real=${((_doleDivisionFactor[object.name]||0)+doleDivisionRetardation)/doleDivisionRetardation}), item is ${Number(item) > Number(object.count)?'stocked':'short'}.`);
// Update existing items if item name already exists
if(Number(item) > Number(object.count)) {
//If successful, increase dole
_doleDivisionFactor[object.name] = Math.max((_doleDivisionFactor[object.name]||0)||1, 1) - 1;
if(config.logItemTransfers){
console.log("removed: " + object.name + " " + object.count + " . " + item + " and sent to " + object.instanceID + " | " + object.instanceName);
}
if(db.items.removeItem({count: object.count, name: object.name})){
let sentItemStatistics = sentItemStatisticsBySlaveID[object.instanceID];
if(sentItemStatistics === undefined){
sentItemStatistics = new averagedTimeSeries({
maxEntries: config.itemStats.maxEntries,
entriesPerSecond: config.itemStats.entriesPerSecond,
mergeMode: "add",
}, console.log);
}
sentItemStatistics.add({
key:object.name,
value:object.count,
});
//console.log(sentItemStatistics.data)
sentItemStatisticsBySlaveID[object.instanceID] = sentItemStatistics;
}
res.send({count: object.count, name: object.name});
httpRequestDurationMilliseconds.labels(req.route.path).observe(Date.now()-reqStartTime);
} else {
// if we didn't have enough, attempt giving out a smaller amount next time
_doleDivisionFactor[object.name] = Math.min(maxDoleDivision, Math.max((_doleDivisionFactor[object.name]||0)||1, 1) * 2);
res.send({name:object.name, count:0});
httpRequestDurationMilliseconds.labels(req.route.path).observe(Date.now()-reqStartTime);
//console.log('failure out of ' + object.name + " | " + object.count + " from " + object.instanceID + " ("+object.instanceName+")");
}
}
});
// circuit stuff
/**
POST endpoint to send and store circuit frames on the master.
Gives no response
@memberof clusterioMaster
@instance
@alias api/setSignal
@param {object} circuitFrame
@param {number} circuitFrame.time
*/
app.post("/api/setSignal", function(req,res) {
endpointHitCounter.labels(req.route.path).inc();
let reqStartTime = Date.now();
if(typeof req.body == "object" && req.body.time){
db.signals.insert(req.body);
httpRequestDurationMilliseconds.labels(req.route.path).observe(Date.now()-reqStartTime);
// console.log("signal set");
}
});
/**
POST endpoint to read database of circuit signals sent to master
@memberof clusterioMaster
@instance
@alias api/readSignal
@returns {object} circuitFrame
*/
app.post("/api/readSignal", function(req,res) {
endpointHitCounter.labels(req.route.path).inc();
let reqStartTime = Date.now();
// request.body should be an object
// {since:UNIXTIMESTAMP,}
// we should send an array of all signals since then
db.signals.find({time:{$gte: req.body.since}}, function (err, docs) {
// $gte means greater than or equal to, meaning we only get entries newer than the timestamp
res.send(docs);
httpRequestDurationMilliseconds.labels(req.route.path).observe(Date.now()-reqStartTime);
// console.log(docs);
});
});
/**
GET endpoint to read the masters current inventory of items.
@memberof clusterioMaster
@instance
@alias api/inventory
@returns {object[]} JSON [{name:"iron-plate", count:100},{name:"copper-plate",count:5}]
*/
// endpoint for getting an inventory of what we got
app.get("/api/inventory", function(req, res) {
endpointHitCounter.labels(req.route.path).inc();
let reqStartTime = Date.now();
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
// Check it and send it
var inventory = [];
for(let key in db.items){
if(typeof db.items[key] == "number" || typeof db.items[key] == "string"){
inventory.push({name:key, count:db.items[key]});
}
}
res.send(JSON.stringify(inventory));
httpRequestDurationMilliseconds.labels(req.route.path).observe(Date.now()-reqStartTime);
});
/**
GET endpoint to read the masters inventory as an object with key:value pairs
@memberof clusterioMaster
@instance
@alias api/inventoryAsObject
@returns {object} JSON {"iron-plate":100, "copper-plate":5}
*/
app.get("/api/inventoryAsObject", function(req, res) {
endpointHitCounter.labels(req.route.path).inc();
let reqStartTime = Date.now();
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
// Check it and send it
res.send(JSON.stringify(db.items));
httpRequestDurationMilliseconds.labels(req.route.path).observe(Date.now()-reqStartTime);
});
// post flowstats here for production graphs
// {timestamp: Date, instanceID: string, data: {"item":number}}
/**
POST endpoint to log production graph statistics. Should contain a timestamp
gathered from Date.now(), a instanceID (also reffered to as "unique") and of
course the timeSeries data.
@memberof clusterioMaster
@instance
@alias api/logStats
@param {object} JSON {timestamp: Date.now(), instanceID: "string", data: {"item": number}}
@returns {string} failure
*/
app.post("/api/logStats", function(req,res) {
endpointHitCounter.labels(req.route.path).inc();
let reqStartTime = Date.now();
if(typeof req.body == "object" && req.body.instanceID && req.body.timestamp && req.body.data) {
if(Number(req.body.timestamp) != NaN){
req.body.timestamp = Number(req.body.timestamp);
db.flows.insert({
instanceID: req.body.instanceID,
timestamp: req.body.timestamp,
data: req.body.data,
});
try{
Object.keys(req.body.data).forEach(itemName => {
prometheusProductionGauge.labels(req.body.instanceID, itemName).inc(Number(req.body.data[itemName]) || 0);
});
}catch(e){console.log(e)};
console.log("inserted: " + req.body.instanceID + " | " + req.body.timestamp);
} else {
console.log("error invalid timestamp " + req.body.timestamp);
res.send("failure");
}
} else {
res.send("failure");
}
httpRequestDurationMilliseconds.labels(req.route.path).observe(Date.now()-reqStartTime);
});
// {instanceID: string, fromTime: Date, toTime, Date}
/**
POST endpoint to get timeSeries statistics stored on the master. Can give production
graphs and other IO statistics.
@memberof clusterioMaster
@instance
@alias api/getStats
@param {object} JSON
@returns {object} - with statistics
*/
app.post("/api/getStats", function(req,res) {
endpointHitCounter.labels(req.route.path).inc();
let reqStartTime = Date.now();
if(typeof req.body == "string"){
req.body = JSON.parse(req.body);
}
// console.log(req.body);
if(typeof req.body == "object" && req.body.instanceID && req.body.statistic === undefined) {
// if not specified, get stats for last 24 hours
if(!req.body.fromTime) {
req.body.fromTime = Date.now() - 86400000 // 24 hours in MS
}
if(!req.body.toTime) {
req.body.toTime = Date.now();
}
console.log("Looking... " + JSON.stringify(req.body));
db.flows.find({
instanceID: req.body.instanceID,
}, function(err, docs) {
let entries = docs.filter(function (el) {
return el.timestamp <= req.body.toTime && el.timestamp >= req.body.fromTime;
});
// console.log(entries);
res.send(entries);
});
} else if(typeof req.body == "object" && req.body.instanceID && typeof req.body.instanceID == "string" && req.body.itemName){
if(req.body.statistic == "place"){
console.log(`sending place data for instanceID ${req.body.instanceID} ${req.body.itemName}`);
// Gather data
//console.log(recievedItemStatisticsBySlaveID)
let itemStats = recievedItemStatisticsBySlaveID[req.body.instanceID];
if(itemStats === undefined){
res.send({statusForDebugging:"no data available"});
return false;
}
let data = itemStats.get(config.itemStats.maxEntries, req.body.itemName);
res.send({
maxEntries:config.itemStats.maxEntries,
entriesPerSecond: config.itemStats.entriesPerSecond,
data: data,
});
} else if(req.body.statistic == "remove"){
console.log(`sending remove data for instanceID ${req.body.instanceID} ${req.body.itemName}`);
// Gather data
console.log(sentItemStatisticsBySlaveID)
let itemStats = sentItemStatisticsBySlaveID[req.body.instanceID];
console.log(itemStats)
if(typeof itemStats == "object"){
let data = itemStats.get(config.itemStats.maxEntries, req.body.itemName);
//console.log(itemStats.get(config.itemStats.maxEntries, req.body.itemName));
res.send({
maxEntries:config.itemStats.maxEntries,
entriesPerSecond: config.itemStats.entriesPerSecond,
data: data,
});
} else {
res.send({statusForDebugging:"no data available"});
}
}
}
httpRequestDurationMilliseconds.labels(req.route.path).observe(Date.now()-reqStartTime);
});
/**
POST endpoint. Modified version of api/getStats which feeds the web interface production graphs
at /nodes. It is like getStats but it does not report items which were not
produced at the moment of recording. (This is to save space, the 0-items were
making up about 92% of the response body weight.)
@memberof clusterioMaster
@instance
@alias api/getTimelineStats
@param {object} JSON {instanceID:1941029, fromTime: ???, toTime: ???}
@returns {object[]} timeseries where each entry is a set point in time.
*/
app.post("/api/getTimelineStats", function(req,res) {
endpointHitCounter.labels(req.route.path).inc();
let reqStartTime = Date.now();
console.log(req.body);
if(typeof req.body == "object" && req.body.instanceID) {
// if not specified, get stats for last 24 hours
if(!req.body.fromTime) {
req.body.fromTime = Date.now() - 86400000 // 24 hours in MS
}
if(!req.body.toTime) {
req.body.toTime = Date.now();
}
console.log("Looking... " + JSON.stringify(req.body));
db.flows.find({
instanceID: req.body.instanceID,
}, function(err, docs) {
if(err) {
console.error(err);
return;
}
//Filter out all the entries outside the time range.
docs = docs.filter(function (el) {
return el.timestamp <= req.body.toTime && el.timestamp >= req.body.fromTime;
});
//Filter out all the elements that weren't produced.
docs.forEach(el => {
for(let key in el.data) {
if(el.data[key] === '0') {
// Set value to undefined instead of using the delete keyword.
// This is because the delete keyword is super duper slow.
// https://stackoverflow.com/questions/208105/how-do-i-remove-a-property-from-a-javascript-object
//delete el.data[key];
el.data[key] = undefined;
}
}
});
res.send(docs);
httpRequestDurationMilliseconds.labels(req.route.path).observe(Date.now()-reqStartTime);
});
}
});
/**
POST endpoint for running commands on slaves.
Requires x-access-token header to be set. Find you api token in secret-api-token.txt on the master (after running it once)
@memberof clusterioMaster
@instance
@alias api/runCommand
@param {object} JSON {instanceID:19412312, command:"/c game.print('hello')"}
@returns {object} Status {auth: bool, message: "Informative error", data:{}}
*/
// write an auth token to file
fs.writeFileSync("secret-api-token.txt", jwt.sign({ id: "api" }, config.masterAuthSecret, {
expiresIn: 86400*365 // expires in 1 year
}));
app.post("/api/runCommand", (req,res) => {
var token = req.headers['x-access-token'];
if(!token) return res.status(401).send({ auth: false, message: 'No token provided.' });
jwt.verify(token, config.masterAuthSecret, function(err, decoded) {
if(err) return res.status(500).send({ auth: false, message: 'Failed to authenticate token.' });
// validate request.body
let body = req.body;
if(body.instanceID
&& Object.keys(wsSlaves).includes(body.instanceID)
&& body.command
&& typeof body.command == "string"
&& body.command[0] == "/"){
// execute command
wsSlaves[body.instanceID].runCommand(body.command, data => {
res.status(200).send({auth: true, message: "success", data});
});
} else {
res.status(400).send({auth: true, message: "Error: invalid request.body"});
}
});
});
/**
GET endpoint. Returns factorio's base locale as a JSON object.
@memberof clusterioMaster
@instance
@alias api/getFactorioLocale
@returns {object{}} 2 deep nested object with base game factorio locale as key:value pairs
*/
app.get("/api/getFactorioLocale", function(req,res){
endpointHitCounter.labels(req.route.path).inc();
let reqStartTime = Date.now();
getFactorioLocale.asObject(config.factorioDirectory, "en", (err, factorioLocale) => {
res.send(factorioLocale);
httpRequestDurationMilliseconds.labels(req.route.path).observe(Date.now()-reqStartTime);
});
});
var server = require("http").Server(app);
server.listen(config.masterPort || 8080, function () {
console.log("Listening on port %s...", server.address().port);
});
/* Websockets for remoteMap */
var io = require("socket.io")(server);
var slaveMappers = {};
class slaveMapper {
constructor(instanceID, socket) {
this.instanceID = instanceID;
this.socket = socket;
this.lastBeat = Date.now();
this.socket.on("heartbeat", () => {
// we aren't ready to die yet apparently
this.lastBeat = Date.now();
});
this.socket.on("sendChunk", function(data){
mapRequesters[data.requesterID].socket.emit("displayChunk", data);
});
// slaveMapper sent us an entity update, process
this.socket.on("sendEntity", entity => {
Object.keys(mapRequesters).forEach(requesterName => {
let requester = mapRequesters[requesterName];
if(requester.instanceID == this.instanceID){
// this mapRequester is listening to this slaveMapper, so we send it updates
requester.socket.emit("displayEntity", entity);
}
});
});
}
}
var mapRequesters = {};
class mapRequester {
constructor(requesterID, socket, instanceID){
this.requesterID = requesterID;
this.socket = socket;
this.instanceID = instanceID;
this.lastBeat = Date.now();
this.socket.on("heartbeat", () => {
prometheusWsUsageCounter.labels('heartbeat').inc();
// we aren't ready to die yet apparently
this.lastBeat = Date.now();
});
this.socket.on("requestChunk", loc => {
prometheusWsUsageCounter.labels('requestChunk').inc();
loc.requesterID = this.requesterID;
let instanceID = loc.instanceID || this.instanceID;
if(slaveMappers[instanceID] && typeof loc.x == "number" && typeof loc.y == "number"){
slaveMappers[instanceID].socket.emit("getChunk", loc);
}
});
this.socket.on("requestEntity", req => {
prometheusWsUsageCounter.labels('requestEntity').inc();
req.requesterID = this.requesterID;
let instanceID = req.instanceID || this.instanceID;
if(slaveMappers[instanceID] && typeof req.x == "number" && typeof req.y == "number"){
slaveMappers[instanceID].socket.emit("getEntity", req);
}
});
this.socket.on("placeEntity", req => {
prometheusWsUsageCounter.labels('placeEntity').inc();
req.requesterID = this.requesterID;
let instanceID = req.instanceID || this.instanceID;
if(slaveMappers[instanceID]){
slaveMappers[instanceID].socket.emit("placeEntity", req);
}
});
}
}
/* Websockets for send and recieve combinators */
var wsSlaves = {};
class wsSlave {
constructor(instanceID, socket){
this.instanceID = instanceID;
this.socket = socket;
this.lastBeat = Date.now();
this.socket.on("heartbeat", () => {
prometheusWsUsageCounter.labels('heartbeat').inc();
this.lastBeat = Date.now();
});
this.socket.on("combinatorSignal", circuitFrameWithMeta => {
prometheusWsUsageCounter.labels('combinatorSignal').inc();
if(circuitFrameWithMeta && typeof circuitFrameWithMeta == "object"){
Object.keys(wsSlaves).forEach(instanceID => {
wsSlaves[instanceID].socket.emit("processCombinatorSignal", circuitFrameWithMeta);
});
}
});
// handle command return values
this.commandsWaitingForReturn = {};
this.socket.on("runCommandReturnValue", resp => {
if(resp.commandID && resp.body && this.commandsWaitingForReturn[resp.commandID] && this.commandsWaitingForReturn[resp.commandID].callback && typeof this.commandsWaitingForReturn[resp.commandID].callback == "function"){
this.commandsWaitingForReturn[resp.commandID].callback(resp.body);
delete this.commandsWaitingForReturn[resp.commandID];
}
});
}
runCommand(command, callback){
let commandID = Math.random().toString();
this.socket.emit("runCommand", {command, commandID});
this.commandsWaitingForReturn[commandID] = {callback, timestamp: Date.now()};
}
}
io.on('connection', function (socket) {
// cleanup dead sockets from disconnected people
let terminatedConnections = 0;
let currentConnections = Object.keys(mapRequesters).length + Object.keys(slaveMappers).length + Object.keys(wsSlaves).length;
[mapRequesters, slaveMappers, wsSlaves].forEach(list => {
Object.keys(list).forEach(connectionID => {
let connection = list[connectionID];
if(connection.lastBeat < (Date.now() - 30000)){
terminatedConnections++;
delete list[connectionID];
}
});
});
if(terminatedConnections > 0) console.log("SOCKET | There are currently "+currentConnections+" websocket connections, deleting "+terminatedConnections+" on timeout");
// tell our friend that we are listening
socket.emit('hello', { hello: 'world' });
/* initial processing for remoteMap */
socket.on('registerSlaveMapper', function (data) {
prometheusWsUsageCounter.labels('registerSlaveMapper').inc();
slaveMappers[data.instanceID] = new slaveMapper(data.instanceID, socket);
console.log("remoteMap | SOCKET registered map provider for "+data.instanceID);
});
socket.on('registerMapRequester', function(data){
// data {instanceID:""}
prometheusWsUsageCounter.labels('registerMapRequester').inc();
let requesterID = Math.random().toString();
mapRequesters[requesterID] = new mapRequester(requesterID, socket, data.instanceID);
socket.emit("mapRequesterReady", true);
console.log("remoteMap | SOCKET registered map requester for "+data.instanceID);
});
/* Websockets for send and recieve combinators */
socket.on("registerSlave", function(data) {
prometheusWsUsageCounter.labels('registerSlave').inc();
if(data && data.instanceID){
wsSlaves[data.instanceID] = new wsSlave(data.instanceID, socket);
console.log("SOCKET | Created new wsSlave: "+ data.instanceID);
}
});
});
module.exports = app;
+184
View File
@@ -0,0 +1,184 @@
var app = require('./master.js'),
assert = require('assert'),
request = require('supertest');
const validateHTML = require('html5-validator');
var parallel = require('mocha.parallel');
describe('Master server endpoint testing', function() {
describe('#GET /api/getFactorioLocale', function() {
it('should get the basegame factorio locale', function(done) {
request(app).get('/api/getFactorioLocale').end(function(err, res) {
assert.equal(res.statusCode, 200);
let object = res.body;
// test that it is actual factorio locale, copied from getFactorioLocale.spec.js
assert.equal(typeof object, "object");
assert.equal(object["entity-name"]["fish"], "Fish");
assert.equal(object["entity-name"]["small-lamp"], "Lamp");
Object.keys(object).forEach(key => {
// first level of the nested object is always an object
assert.equal(typeof object[key], "object");
Object.keys(object[key]).forEach(key2 => {
// second level of the nested object is always a string, nearly always truthy
if(key2 != "so-long-and-thanks-for-all-the-fish"){
assert.ok(object[key][key2]);
assert.equal(typeof object[key][key2], "string");
}
});
});
done();
});
});
});
// describe("#GET /api/")
parallel("#GET static website data", function() {
this.timeout(6000);
it("sends some HTML when accessing /", function(done){
this.timeout(6000);
request(app).get("/").end(function(err,res){
assert.equal(res.statusCode, 200);
validateHTML(res.text).then(result => {
assert(result.messages.length === 1, "there are HTML errors on the page, please fix: "+JSON.stringify(result.messages));
done();
});
});
});
it("sends some HTML when accessing /nodes",function(done){
this.timeout(6000);
request(app).get("/nodes").end(function(err,res){
assert.equal(res.statusCode, 200);
validateHTML(res.text).then(result => {
assert(result.messages.length === 1, "there are HTML errors on the page, please fix: "+JSON.stringify(result.messages));
done();
});
});
});
it("sends some HTML when accessing /settings",function(done){
this.timeout(6000);
request(app).get("/settings").end(function(err,res){
assert.equal(res.statusCode, 200);
validateHTML(res.text).then(result => {
assert(result.messages.length === 1, "there are HTML errors on the page, please fix: "+JSON.stringify(result.messages));
done();
});
});
});
it("sends some HTML when accessing /nodeDetails",function(done){
this.timeout(6000);
request(app).get("/nodeDetails").end(function(err,res){
assert.equal(res.statusCode, 200);
validateHTML(res.text).then(result => {
assert(result.messages.length === 1, "there are HTML errors on the page, please fix: "+JSON.stringify(result.messages));
done();
});
});
});
it("sends some HTML when accessing /remoteMap",function(done){
this.timeout(6000);
request(app).get("/remoteMap").end(function(err,res){
assert.equal(res.statusCode, 200);
validateHTML(res.text).then(result => {
// there should be 1 error regarding complaining about me using ES6 modules before they are fully supported
assert(result.messages.length === 2, "there are HTML errors on the page, please fix: "+JSON.stringify(result.messages));
done();
});
});
});
});
let persistentMaster = request(app);
describe("#POST /api/place", function(){
it("adds an itemStack to the masters inventory", function(done){
persistentMaster.post("/api/place")
.send({
name:"steel-plate",
count:20,
instanceName:"unitTest"
})
.end(function(err,res){
assert(!err)
assert.equal(res.text, "success", "something went wrong with the request")
done();
});
});
});
describe("#GET /api/inventory", function(){
it("returns the masters current inventory", function(done){
persistentMaster.get("/api/inventory").end(function(err,res){
assert.equal(res.statusCode, 200);
let inventory = JSON.parse(res.text);
assert.equal(typeof inventory, "object", "Inventory should be an object");
assert(inventory.length >= 1, "There should be at least 1 entry in the inventory");
let contains20SteelPlate = false;
inventory.forEach(itemStack => {
if(itemStack.name == "steel-plate" && itemStack.count >= 20) contains20SteelPlate = true;
});
assert(contains20SteelPlate, "Please ensure there are at least 20 steel plate in the inventory")
done();
});
});
});
parallel("#POST /api/remove", function(){
it("returns an itemStack of how many items were removed", function(done){
persistentMaster.post("/api/remove")
.send({
name:"steel-plate",
count:10
})
.end(function(err,res){
assert(!err);
assert.equal(res.body.count, 10, "Something is wrong with the response, maybe the format changed slightly or you didn't have enough steel?");
done();
});
});
it("returns an empty itemStack if you try to request addItem or removeItem", function(done){
let casesRan = 0;
persistentMaster.post("/api/remove")
.send({
name:"addItem",
count:10
})
.end(function(err,res){
assert(!err);
assert.equal(res.text, '{"name":"addItem","count":0}', "When there are none of the item, you should get 0 back. addItem and removeItem should always return 0");
isDone();
});
persistentMaster.post("/api/remove")
.send({
name:"removeItem",
count:10
})
.end(function(err,res){
assert(!err);
assert.equal(res.text, '{"name":"removeItem","count":0}', "When there are none of the item, you should get 0 back. addItem and removeItem should always return 0");
isDone();
});
function isDone(){
if(++casesRan == 2) done();
}
});
it("returns an empty itemStack if you don't have any of the item you request", function(done){
persistentMaster.post("/api/remove")
.send({
name:"imaginaryItem",
count:999999
})
.end(function(err,res){
assert(!err);//{"name":"imaginaryItem","count":0}
assert.equal(res.body.name, "imaginaryItem", "Make sure body.name is the item we asked for");
assert.equal(res.body.count, 0, "Count should be 0 since we are asking for something that does not exist anywhere");
assert.equal(Object.keys(res.body).length, 2, "name and count should be the only keys on this object");
done();
});
});
});
});
+168
View File
@@ -0,0 +1,168 @@
/**
class factorioServer
This class has the responsiblity of :
* spawning a factorio server
* Handling input via stdin or RCON
*/
const fs = require("fs");
const child_process = require("child_process");
const path = require("path");
const mkdirp = require("mkdirp");
const appRootPath = require("app-root-path");
const fileOps = require("_app/fileOps");
module.exports = class factorioServer {
constructor({
factorioDirectory = "factorio",
instanceDirectory = "instances/data",
instanceName = "Default server name",
factorioPort = Math.floor(Math.random() * 65535),
serversettings = {},
onProgress = function(type, a, b, c){
if(type == "progress") console.log("Progress: ",a," of ",b," unit: ",c);
if(type == "log") console.log(a);
},
} = {}){
this.factorioDirectory = factorioDirectory;
this.instanceDirectory = instanceDirectory;
this.instanceName = instanceName;
this.factorioPort = factorioPort;
this.serversettings = Object.assign({
"name": "Clusterio instance: " + this.instanceName,
"description": "Public factorio server running clusterio",
"tags": ["clusterio"],
"max_players": "20",
"visibility": "public",
"username": "",
"token": "",
"game_password": "",
"verify_user_identity": true,
"admins": ["Danielv123"],
"allow_commands": true,
"autosave_interval": 10,
"autosave_slots": 5,
"afk_autokick_interval": 0,
"auto_pause": false,
}, serversettings);
this.onProgress = (type, a, b, c) => {
if(typeof onProgress == "function") onProgress(type, a, b, c);
};
}
async initialize(){
if(!await this.isValidInstance()){
await this.createInstance();
}
return true;
}
async createInstance(path = this.instanceDirectory){
if(await this.isValidInstance(path)){
throw new Error("Instance already exists!");
}
// create folders
let folders = [
"/script-output/",
"/saves/",
"/mods/",
"/instanceMods/",
];
mkdirp.sync(path);
folders.forEach(folder => mkdirp.sync(path + folder));
// create files
let files = [
{name: "/script-output/output.txt", content: ""},
{name: "/script-output/orders.txt", content: ""},
{name: "/script-output/txbuffer.txt", content: ""},
{name: "/config.ini", content: "[path]\r\n\ read-data=__PATH__executable__/../../data\r\n\ write-data=__PATH__executable__/../../../" + this.instanceDirectory + "\r\n\ "},
{name: "/config.json", content: JSON.stringify({
factorioPort: this.factorioPort,
clientPassword: Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 8),
}, null, 4)},
{name: "/server-settings.json", content: JSON.stringify(this.serversettings, null, 4)},
];
files.forEach(file => {
fs.writeFile(path + file.name, file.content, doneWritingHandler);
// console.log("Created: ",path+file.name);
});
let filesToWrite = files.length;
function doneWritingHandler(){
if(!--filesToWrite){
return path;
}
}
}
async deleteInstance(path = this.instanceDirectory){
if(await this.isValidInstance(path)){
await fileOps.deleteFolderRecursive(path);
return true;
} else {
return false;
}
}
isValidInstance(path = this.instanceDirectory){
return new Promise((resolve, reject) => {
fs.stat(path, (err, stat) => {
if(err){
// console.log(err)
resolve(false);
} else {
// the instance folder exists, do some more checks
fs.readdir(path, (err, files) => {
if(files && Array.isArray(files)){
let foldersThatShouldBeThere = [
"script-output",
"saves",
"mods",
"instanceMods",
];
let instanceValidity = true;
foldersThatShouldBeThere.forEach(folder => {
if(!files.includes(folder)){
instanceValidity = false;
}
});
resolve(instanceValidity);
}
});
}
});
});
}
createMap(name = "map.zip"){
return new Promise((resolve, reject) => {
this.isValidInstance().then(isValid => {
if(!isValid){
return createInstance();
}
}).then(()=>{
this.onProgress("log", "Starting map creation");
// console.log(this.factorioDirectory + '/bin/x64/factorio')
let proc = child_process.spawn(
path.join(this.factorioDirectory, 'bin', 'x64', 'factorio'), [
'-c', path.join(this.instanceDirectory,'config.ini'),
'--create', path.join(this.instanceDirectory, 'saves',name),
'--server-settings', path.join(this.instanceDirectory, 'server-settings.json'),
],{
cwd:process.cwd(),
}
);
// console.log(proc.stdout.toString("utf8"));
proc.stdout.on("data", data => this.onProgress("log", data.toString("utf8")));
proc.on("close", code => {
this.onProgress("log", "Map created! "+code);
resolve();
});
});
});
}
}
+114
View File
@@ -0,0 +1,114 @@
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const appRootPath = require("app-root-path");
const parallel = require("mocha.parallel");
const mkdirp = require("mkdirp");
const factorioServer = require("./index.js");
describe("class factorioServer/index.js", ()=>{
it("is a class (or at least a function)", ()=>{
assert.equal(typeof factorioServer, "function");
});
describe(".deleteInstance(instanceName)", ()=>{
it("deletes an instance, defaulting to the classes instanceDirectory", async () => {
let server = new factorioServer({
instanceDirectory: path.resolve(__dirname, "_testData_deleteInstance"),
instanceName: "Unit testing instance",
});
await server.initialize();
await server.deleteInstance();
assert.equal(await server.isValidInstance(), false);
return true;
});
});
describe(".isValidInstance(instanceName)", ()=>{
it("tells us whether an instance exists or not", async () => {
let server = new factorioServer({
instanceDirectory: path.join(__dirname, "_testData_isValidInstance"),
instanceName: "Unit testing instance",
});
await server.deleteInstance();
let status = await server.isValidInstance();
assert.equal(status, false);
let x = await server.initialize(); // creates our instance
let status2 = await server.isValidInstance();
assert.equal(status2, true);
await server.deleteInstance(); // clean up folder
return true;
});
it("does not get tricked by an empty folder", async () => {
let server = new factorioServer({
instanceDirectory: path.join(__dirname, "_testData_isValidInstance_2"),
instanceName: "Unit testing instance",
});
assert.equal(await server.isValidInstance(), false);
mkdirp.sync(path.join(__dirname, "_testData_isValidInstance_2")); // create empty folder simulating instance
assert.equal(await server.isValidInstance(), false);
await server.initialize(); // create a proper instance
assert.equal(await server.isValidInstance(), true);
await server.deleteInstance(); // delete the instance and ensure we see that it is in fact deleted
assert.equal(await server.isValidInstance(), false);
return true;
}).timeout(5000);
});
describe(".initialize()", ()=>{
it("creates an instance", done => {
let server = new factorioServer({
instanceDirectory: path.resolve(__dirname, "_testData_initialize"),
instanceName: "Unit testing instance",
});
server.initialize().then(()=>{
return server.deleteInstance();
}).then(() => {
done();
}).catch(e => console.log(e));
});
});
describe(".createMap(mapName.zip)", ()=>{
it("creates a map file as instanceDirectory/saves/map.zip", async function(){
this.timeout(20000); // these test often takes a lot of time in the map creation step (4500 ms on i3 dev system)
let relativePath = __dirname.replace(appRootPath, "").substr(1);
let server = new factorioServer({
instanceDirectory: path.join(relativePath, "_testData_createMap"),
instanceName: "Unit testing instance",
onProgress: false,
});
await server.initialize();
let maps = fs.readdirSync(path.join(relativePath, "_testData_createMap", "saves"));
assert.equal(maps.length, 0);
await server.createMap();
let maps2 = fs.readdirSync(path.join(relativePath, "_testData_createMap", "saves"));
assert(maps2.includes("map.zip"));
await server.deleteInstance();
return true;
});
it("supports custom filenames", async function(){
this.timeout(20000);
let relativePath = __dirname.replace(appRootPath, "").substr(1);
let server = new factorioServer({
instanceDirectory: path.join(relativePath, "_testData_createMap_2"),
instanceName: "Unit testing instance",
onProgress: false,
});
await server.initialize();
let maps = fs.readdirSync(path.join(relativePath, "_testData_createMap_2", "saves"));
assert.equal(maps.length, 0);
await server.createMap("mapWithCustomName.zip");
let maps2 = fs.readdirSync(path.join(relativePath, "_testData_createMap_2", "saves"));
assert(maps2.includes("mapWithCustomName.zip"));
await server.deleteInstance();
return true;
});
});
});
+120
View File
@@ -0,0 +1,120 @@
const fs = require("fs");
const path = require("path");
// gets newest file in a directory
// dir is directory
// files is array of filenames
// callback(err, filename string);
var getNewestFile = function(dir, files, callback) {
if (!callback) throw new Error("Missing callback");
if (!files || (files && files.length === 0)) {
callback(new Error("No files"));
return;
}
// we are appending the file name to the directory, without the
// "/" we would get paths like /folderfile.txt instead of /folder/file.txt
if(dir[dir.length-1] != "/"){
dir += "/";
}
var newest = { file: files[0] };
var checked = 0;
fs.stat(dir + newest.file, function(err, stats) {
if(err) {
callback(err);
} else {
newest.mtime = stats.mtime;
for (var i = 0; i < files.length; i++) {
var file = files[i];
(function(file) {
fs.stat(file, function(err, stats) {
++checked;
if (stats.mtime.getTime() > newest.mtime.getTime()) {
newest = { file : file, mtime : stats.mtime };
}
if (checked == files.length) {
callback(undefined,newest);
}
});
})(dir + file);
}
}
});
}
// get names of all directories in directory
var getDirectoriesSync = function(srcpath) {
return fs.readdirSync(srcpath).filter(function (file) {
return fs.statSync(path.join(srcpath, file)).isDirectory();
});
}
// get names of all files in a directory
var getFileNamesSync = function(srcpath) {
return fs.readdirSync(srcpath).filter(function (file) {
return !fs.statSync(path.join(srcpath, file)).isDirectory();
});
}
// does what it says on the tin
var deleteFolderRecursiveSync = function(path) {
if (fs.existsSync(path)) {
fs.readdirSync(path).forEach(function (file, index) {
var curPath = path + "/" + file;
if (fs.lstatSync(curPath).isDirectory()) { // recurse
deleteFolderRecursiveSync(curPath);
} else { // delete file
fs.unlinkSync(curPath);
}
});
fs.rmdirSync(path);
return true;
} else {
return false;
}
};
var deleteFolderRecursive = function(path){
return new Promise((resolve, reject) => {
fs.readdir(path, (err, files) => {
if(err){
reject(err);
} else {
let thingsToDelete = files.length;
files.forEach(file => {
let curPath = path + "/" + file;
fs.stat(curPath, (err, stat) => {
if(!err){
if(stat.isDirectory()){
// recurse
deleteFolderRecursive(curPath).then(() => {
whenFilesDeleted();
}).catch(e => console.log(e));
} else {
fs.unlink(curPath, () => {
// console.log("Deleted "+curPath);
whenFilesDeleted();
});
}
} else {
resolve(false);
}
});
});
whenFilesDeleted();
function whenFilesDeleted(){
if(!thingsToDelete--){// var-- instead of --var because we are calling the function once too much and need the delay
fs.rmdir(path, () => {
resolve();
// console.log("Deleted "+path);
});
}
}
}
});
});
}
module.exports = {
getNewestFile,
getDirectoriesSync,
deleteFolderRecursiveSync,
deleteFolderRecursive,
getFileNamesSync,
}
+135
View File
@@ -0,0 +1,135 @@
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const fileOps = require('./fileOps');
// create folders for testing
function setupTestingEnv(){
if (!fs.existsSync(path.resolve(__dirname, "database"))){
fs.mkdirSync(path.resolve(__dirname, "database"));
}
if (!fs.existsSync(path.resolve(__dirname, "test"))){
fs.mkdirSync(path.resolve(__dirname, "test"));
}
if (!fs.existsSync(path.resolve(__dirname, "test/folder"))){
fs.mkdirSync(path.resolve(__dirname, "test/folder"));
}
if (!fs.existsSync(path.resolve(__dirname, "test/another folder"))){
fs.mkdirSync(path.resolve(__dirname, "test/another folder"));
}
if (!fs.existsSync(path.resolve(__dirname, "test/file.txt"))){
fs.writeFileSync(path.resolve(__dirname, "test/file.txt"), "contents");
}
if (!fs.existsSync(path.resolve(__dirname, "test/another file.txt"))){
fs.writeFileSync(path.resolve(__dirname, "test/another file.txt"), "more contents");
}
// we need this xfile to test a branch in fileOps.getNewestFile()s propagation
if (!fs.existsSync(path.resolve(__dirname, "test/xfile.txt"))){
fs.writeFileSync(path.resolve(__dirname, "test/xfile.txt"), "contents");
}
}
setupTestingEnv();
setupTestingEnv(); // run twice to get branch coverage
describe("fileOps.js", function(){
describe("fileOps.getDirectoriesSync()", function(){
it("gets an array of directory names from a path", function(){
let directories = fileOps.getDirectoriesSync(path.resolve(__dirname, "test"));
// because there are no arrays for some reason
assert(typeof directories == "object");
// they are sorted alphabetically because thats the way it is
assert(directories[0] == "another folder" && directories[1] == "folder");
});
it("does not return any filenames", function(){
let directories = fileOps.getDirectoriesSync(path.resolve(__dirname, "test"));
// this is a function to return directories, not files. So yeah.
assert(!directories.includes("file.txt") && !directories.includes("another file.txt"));
});
it("throws when lacking first argument", function(){
assert.throws(function(){
let files = fileOps.getDirectoriesSync();
}, Error);
});
});
describe("fileOps.getFileNamesSync(path)", function(){
it("gets names of files in directory", function(){
let files = fileOps.getFileNamesSync(path.resolve(__dirname, "test"));
assert(files.length > 0);
assert(files.includes("file.txt") && files.includes("another file.txt"));
});
it("throws when lacking first argument", function(){
assert.throws(function(){
let files = fileOps.getFileNamesSync();
}, Error);
});
});
describe("fileOps.getNewestFile()", function(){
it("gets the newest file in a directory async", function(done){
fileOps.getNewestFile(path.resolve(__dirname, "test/"), fileOps.getFileNamesSync(path.resolve(__dirname, "test")), function(err, newest){
assert(newest && typeof newest.file == "string");
done();
});
});
it("throws if callback is missing", function(){
assert.throws(function(){
fileOps.getNewestFile("test", fileOps.getFileNamesSync(path.resolve(__dirname, "test")));
}, Error);
});
it("executes callback(err) if no files are provided", function(done){
fileOps.getNewestFile(path.resolve(__dirname, "test"), [], function(err, newest){
assert(err && typeof err == "object");
done();
});
});
it("executes callback(err) if launched without a directory", function(done){
fileOps.getNewestFile("", fileOps.getFileNamesSync(path.resolve(__dirname, "test")), function(err, newest){
assert(err);
done();
});
});
it("executes callback(err) when you do stupid shit", function(done){
fileOps.getNewestFile({thisWontWork: "but not crash either"}, {lolz:"yeah..."}, function(err, newest){
assert(err);
// ENOENT: no such file or directory, stat 'C:\**\factorioClusterio\lib\test\undefined'
done();
});
});
});
describe("fileOps.deleteFolderRecursiveSync(path)", function(){
it("deletes a folder and all files and folders in it", function(){
let x = fileOps.deleteFolderRecursiveSync(path.resolve(__dirname, "test"));
assert.throws(function(){
fs.statSync(path.resolve(__dirname, "test"));
}, Error);
assert(x);
// give it some extra time because its sometimes very slow
}).timeout(5000);
it("returns false if you give it an invalid path", function(){
let x = fileOps.deleteFolderRecursiveSync("This isn't a real path");
assert(!x);
});
});
describe("fileOps.deleteFolderRecursive(path)", function(){
it("deletes a folder and all files and folders in it, just like sync version", async function(){
setupTestingEnv();
assert(fs.statSync(path.resolve(__dirname, "test")));
let x = await fileOps.deleteFolderRecursive(path.resolve(__dirname, "test"));
assert.throws(function(){
fs.statSync(path.resolve(__dirname, "test"));
}, Error);
assert(x === undefined, "Promise version of this function does not have a return value upon success");
return true;
}).timeout(5000);
it("throws like a promise does if you give it an invalid path", async function(){
let x = await fileOps.deleteFolderRecursive("Look this path is fake!")
.catch(e => {
assert(e);
assert.equal(e.code, "ENOENT");
return true;
});
});
});
});
+2
View File
@@ -0,0 +1,2 @@
const fileOps = require("./fileOps");
module.exports = fileOps;
+1
View File
@@ -0,0 +1 @@
module.exports = require("./isFactorioCommand.js");
+23
View File
@@ -0,0 +1,23 @@
const luamin = require("luamin");
function isFactorioCommand(command){
if(typeof command != "string"){
return false;
} else if(!command.includes("/silent-command ") && !command.includes("/c ")){
return false;
} else if(command[0] != "/"){
return false;
} else {
// check that what follows /c is valid LUA
let x = command.replace("/silent-command ", "").replace("/c ","");
let y
try{
y = luamin.minify(x);
} catch (e) {
return false;
}
// by this point it is surely a LUA command
return true;
}
}
module.exports = isFactorioCommand;
+18
View File
@@ -0,0 +1,18 @@
const isFactorioCommand = require("./isFactorioCommand");
const assert = require("assert");
describe("inventoryImports/isFactorioCommand.js()", function(){
it("Returns false if input is not a factorio command", function(){
assert(isFactorioCommand({}) == false, "This is not even a string so should return false");
assert(isFactorioCommand("Howdy boys!") == false, "This is not a command so should return false");
assert(isFactorioCommand("/c this ain't a command because its invalid LUA") == false, "This is not valid LUA so it should return false");
assert(isFactorioCommand("Trying to trick the system /c game.print('Hello world!')") == false, "this is still not a command");
});
it("Returns true if it is a valid command", function(){
assert(isFactorioCommand("/c game.print('hello world')"), "This is a valid command, should output true not false")
});
it("Fails to understand that leading spaces are valid", function(){
assert(isFactorioCommand(" /c game.print('Hello world!')") == false, "This should be true but often returns false unintentionally. If you see this you probably fixed the bug so change this test, OK?")
});
});
+9638
View File
File diff suppressed because it is too large. Load diff
+70
View File
@@ -0,0 +1,70 @@
{
"name": "clusterio",
"version": "1.7.0",
"description": "Interserver communication for factorio",
"repository": "https://github.com/Danielv123/factorioClusterio",
"main": "master.js",
"scripts": {
"test": "./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha -- \"./{{,node_modules/_app/**/},!(node_modules)/**/}*.spec.js\" -R spec || echo .",
"start": "echo Run node client.js start [instance] to start slaves && node master.js",
"cover": "./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha -- \"./{{,node_modules/_app/**/},!(node_modules)/**/}*.spec.js\" -R spec || echo . && codecov -t f7b38085-36eb-4d72-84e6-64721910171e",
"docs": "documentation build index.js --config documentation.yml -f html -o docs",
"livedocs": "documentation serve --watch *.js lib/** --config documentation.yml",
"install": "node lib/npmPostinstall.js"
},
"keywords": [
"factorio"
],
"author": "Danielv123",
"license": "ISC",
"dependencies": {
"app-root-path": "^2.0.1",
"as-table": "^1.0.31",
"averaged-timeseries": "^1.4.0",
"bcryptjs": "^2.4.3",
"body-parser": "^1.18.2",
"codecov": "^2.3.1",
"deepmerge": "^1.5.2",
"ejs": "^2.5.7",
"express": "^4.16.2",
"express-fileupload": "0.0.6",
"follow-redirects": "^1.4.1",
"fs-extra": "^1.0.0",
"getmac": "^1.4.0",
"hash-files": "^1.1.1",
"html5-validator": "^1.0.7",
"ini": "^1.3.5",
"jsonwebtoken": "^8.2.1",
"leveldown": "^1.9.0",
"linvodb3": "^3.25.1",
"luamin": "^1.0.4",
"mkdirp": "^0.5.1",
"mkdirp-promise": "^5.0.1",
"moment": "^2.20.1",
"ncp": "^2.0.0",
"nedb": "^1.8.0",
"needle": "^1.1.0",
"node-factorio-api": "^0.3.1-alpha",
"node-forge": "^0.7.1",
"prom-client": "^10.2.3",
"request": "^2.83.0",
"right-pad": "^1.0.1",
"rmdir-sync": "^1.0.1",
"simple-rcon": "^0.3.1",
"single-line-log": "^1.1.2",
"socket.io": "^2.0.4",
"socket.io-client": "^2.0.4",
"sync-request": "^4.0.1",
"underscore": "^1.8.3"
},
"devDependencies": {
"documentation": "^5.4.0",
"istanbul": "^0.4.5",
"mocha": "^3.5.3",
"mocha-sinon": "^2.0.0",
"mocha.parallel": "^0.15.5",
"nock": "^9.1.6",
"sinon": "^2.4.1",
"supertest": "^3.0.0"
}
}
+17
View File
@@ -0,0 +1,17 @@
module.exports = function(app) {
app.get("/", function(req, res) {
res.render("index");
});
app.get("/nodes", function(req, res) {
res.render("nodes");
});
app.get("/settings", function(req, res) {
res.render("settings");
});
app.get("/nodeDetails", function(req, res) {
res.render("nodeDetails");
});
app.get("/remoteMap", function(req, res) {
res.render("remoteMap");
});
};
-24
View File
@@ -1,24 +0,0 @@
import { createServer } from "../../packages/web_ui/node_modules/vite/dist/node/index.js";
import { clusterioApp } from "../../packages/web_ui/vite.mjs";
import fs from "node:fs/promises";
import path from "node:path";
import { createRequire } from "node:module";
const require = createRequire(path.resolve("packages/web_ui/package.json"));
const server = await createServer({
configFile: false,
root: process.cwd(),
resolve: { alias: ["react", "react/jsx-runtime", "react/jsx-dev-runtime", "react-dom", "react-dom/client", "react-router", "react-router/dom", "react-router-dom", "antd", "@sinclair/typebox"].map(name => ({ find: new RegExp(`^${name}$`), replacement: require.resolve(name) })) },
plugins: [clusterioApp({ entry: "temp/plugin-layout/main.tsx" }), {
name: "fixture-html", configureServer(server) {
server.middlewares.use(async (req, res, next) => {
if (req.url?.startsWith("/plugins")) {
res.setHeader("Content-Type", "text/html");
res.end(await server.transformIndexHtml(req.url, await fs.readFile("temp/plugin-layout/index.html", "utf8")));
} else { next(); }
});
},
}],
server: { host: "127.0.0.1", port: 5173, strictPort: true },
});
await server.listen();
server.printUrls();
+17
View File
@@ -0,0 +1,17 @@
/*
Example config file for example Clusterio plugin
Binary will be spawned using child_process.spawn and get input from stdin and text from stdout will be ran as
commands in the factorio server.
*/
module.exports = {
// Name of package. For display somewhere I guess.
name: "UPSdisplay",
version: "2.0.0",
// Binary entrypoint for plugin. Don't let it crash. Stdout is sent to game as server chat (run /c commands from here for interface)
// Make sure its cross platform somehow.
binary: "nodePackage",
description: "Internal clusterio plugin to get the worlds UPS and display it on master",
// We'll send everything in this file to your stdin. Beware.
scriptOutputFileSubscription: "UPSdisplay.txt",
}
+40
View File
@@ -0,0 +1,40 @@
const needle = require("needle");
class UPSdisplay {
constructor(slaveConfig, msgInterface){
this.config = slaveConfig;
this.messageInterface = msgInterface;
this.historicalTicks = [];
setInterval(() => {
this.messageInterface("/silent-command game.write_file('UPSdisplay.txt', game.tick, true, 0)");
},1000);
setInterval(() => {
if(this.historicalTicks && this.historicalTicks[0]){
let timePeriod = this.historicalTicks[this.historicalTicks.length-1].timestamp - this.historicalTicks[0].timestamp;
let ticksInPeriod = this.historicalTicks[this.historicalTicks.length-1].tick - this.historicalTicks[0].tick;
let UPS = Math.round(ticksInPeriod / (timePeriod/1000));
// console.log("UPS: " + UPS);
try{
needle.post(this.config.masterIP+':'+this.config.masterPort+'/api/editSlaveMeta', {instanceID: this.config.unique, password: this.config.clientPassword, meta: {UPS:UPS}}, function(err, resp) {
// success?
});
} catch (err){
console.log(err);
}
}
}, 10000);
this.messageInterface("/silent-command game.print('UPSdisplay enabled')");
}
scriptOutput(data){
if (data !== null && !isNaN(data)){
// console.log("Tick: " + data);
this.historicalTicks[this.historicalTicks.length] = {tick:Number(data), timestamp:Date.now()};
if(this.historicalTicks.length > 30){
this.historicalTicks.shift(); // delete last element in array (position 0);
}
}
}
}
module.exports = UPSdisplay;
+20
View File
@@ -0,0 +1,20 @@
/*
This clusterio plugin is tasked to read your inventory and logistic request slots
it will then attempt to fulfill those requests using items provided by the master
server. There *should* be an in game GUI to toggle this.
*/
module.exports = {
// Name of package. For display somewhere I guess.
name: "inventoryImports",
version: "1.0.0",
// Binary entrypoint for plugin. Don't let it crash. Stdout is sent to game as server chat (run /c commands from here for interface)
// Make sure its cross platform somehow.
binary: "nodePackage",
arguments: ["index.js"],
description: "Clusterio plugin to fill character logistic slots with master imports",
// We'll send everything in this file to your stdin. Beware.
scriptOutputFileSubscription: "inventoryImports.txt",
}
+27
View File
@@ -0,0 +1,27 @@
const needle = require("needle");
const isJson = require("./isJson.js");
const pluginConfig = require("./config.js");
const functions = require("./interfacingFunctions");
// poll inventories
class inventoryImports {
constructor(slaveConfig, messageInterface){
this.config = slaveConfig;
this.messageInterface = messageInterface;
messageInterface("/silent-command game.print('InventoryImports enabled')");
setInterval(function(){
messageInterface("Polling inventories\n")
messageInterface(functions.pollInventories(pluginConfig.scriptOutputFileSubscription));
}, 10000);
}
scriptOutput(data){
if(data !== null && isJson(data)){
let inventory = JSON.parse(data);
// process inventory of players and ask master for more stuff
functions.handleInventory(inventory, this.config, this.messageInterface);
}
}
}
module.exports = inventoryImports;
@@ -0,0 +1,157 @@
const objectOps = require("../../lib/objectOps.js");
const needle = require("needle");
const pluginConfig = require("./config")
const isJson = require("./isJson")
// returns JS object, throws if there are syntax errors in input string
function parseJsString(string){
// eval because my lua is writing a JS object instead of traditional JSON
// this was done because I hate escaping strings and there are more quotes
// in JSON than JS objects.
// since eval is unsafe, maybe use https://www.npmjs.com/package/eval-sanitizer
if(string.includes("require") || string.includes(";") || string.includes("eval")){
throw "parseJsString might have gotten something that could be a xss attempt";
}
let inventory = "";
eval("inventory = " + string);
/*console.log(string)
console.log(inventory)*/
return inventory;
}
function handleInventory(json, config, callback){
/*
json is one of these two samples
{"players":{"1":{"inventory":{"stone":12,"iron-ore":120,"raw-wood":10,"iron-plate":8,"steel-plate":480},"requestSlots":{}}}}
{"exports":{"1":[{"name":"iron-ore","count":0},{"name":"steel-plate","count":0}]}}
we want to seperate players, parse their json and compare to request slots
then ask master for the difference
we must then prepare it all and inject it into players inventories with lua before
the next json report arrives to avoid duplicating orders
*/
if(json && typeof json != "string" && typeof json == "object"){
let exports = {};
// this is an inventory report, check whats missing and request from master
// {"players":{"1":{"inventory":{"stone":12,"iron-ore":120,"raw-wood":10,"iron-plate":8,"steel-plate":480},"requestSlots":{}}}}
if(json.players){
let playerNames = Object.keys(json.players);
for(let i = 0; i < playerNames.length; i++) {
// haha, this turned into bit of a mess didn't it?
let playerInv = json.players[playerNames[i]];
// create a copy of player.requestSlots
let toRequest = objectOps.deepclone(playerInv.requestSlots);
let inventory = playerInv.inventory;
let itemsToConfirm = Object.keys(toRequest).length;
// create a place to store the object we have confirmed for importing
let confirmedItems = {};
let collectStacks = function(stack){
itemsToConfirm--;
if(stack){
// because of the way we collect data in a keyed JSON object, this first part of the IF cannot happen.
/* istanbul ignore next */
if(confirmedItems[stack.name] && confirmedItems[stack.name] > 0){
confirmedItems[stack.name] += stack.count;
} else {
confirmedItems[stack.name] = stack.count;
}
}
if(itemsToConfirm == 0 && Object.keys(confirmedItems).length > 0){
// return LUA
callback(insertItemsFromObject(confirmedItems, playerNames[i]));
}
}
// subtract items already in inventory
Object.keys(playerInv.requestSlots).forEach(function(key){
// if this item in inventory is also in a request slot, subtract invCount from requestCount
if(inventory[key]){
toRequest[key] -= inventory[key];
}
// if the difference is more than 0, request item from master
if(toRequest[key] > 0){
let request = {
name:key,
count:toRequest[key],
}
//console.log("Requesting: "+JSON.stringify(request));
// request the difference from master
needle.post(config.masterIP + ":" + config.masterPort + '/api/remove', request, {json:true}, function (err, response, body) {
if (!err && response && response.body && (typeof response.body == "object" || isJson(response.body))) {
// confirmed orders, already removed from master inventory
//response.body.name: response.body.count
if(typeof response.body == "string") response.body = JSON.parse(response.body);
let stack = {name:response.body.name,count:Number(response.body.count)}
collectStacks(stack);
} else {
collectStacks(false);
}
});
} else {
collectStacks(false);
}
});
}
}
// {"exports":{"1":[{"name":"iron-ore","count":0},{"name":"steel-plate","count":0}]}}
// this is a report of stuff we didn't have space for, send back to master
if(json.exports){
// loop over players (normally only one, but whatever)
Object.keys(json.exports).forEach(function(playerName){
// loop over simpleItemStacks to export
let stackArray = json.exports[playerName];
for(let i = 0; i < stackArray.length; i++){
if(stackArray[i].count > 0){
// console.log("Returning overflow: " + JSON.stringify(stackArray[i]));
needle.post(config.masterIP + ":" + config.masterPort + '/api/place', stackArray[i], function (err, resp, body) {
// console.log(body);
});
}
}
});
}
} else return false; // inventory is falsey or a string
}
function insertItemsFromObject(confirmedItems, playerName){
// expeced input
// {["iron-plate"]:100, coal:20}
// make lua table
// we gotta construct a string because I can't find any nice library for this
if(playerName && confirmedItems){
if(typeof playerName != "string") {
throw "playerName is: '"+typeof playerName+"' instead of string!";
}
let itemTable = "{";
Object.keys(confirmedItems).forEach(function(name){
let count = confirmedItems[name];
if(count > 0){
itemTable += '["'+name+'"]='+Math.floor(count)+',';
}
});
itemTable += "}";
let outputFile = pluginConfig.scriptOutputFileSubscription;
// a="'+players[i]+'"
// b='+itemTable+'
return '/silent-command local a="'+playerName+'"local b='+itemTable+'local c={}if game.players[a]and game.players[a].connected then local d='+"'"+'{"exports":{"'+"'"+'..a..'+"'"+'":['+"'"+'local e=false;for f,g in pairs(b)do local h=game.players[a].insert{name=f,count=g}if e then d=d..'+"'"+','+"'"+'else e=true end;d=d..'+"'"+'{"name":"'+"'"+'..f..'+"'"+'","count":'+"'"+'..g-h..'+"'"+'}'+"'"+'end;d=d..'+"'"+']}}'+"'"+'game.write_file("'+outputFile+'",d,true,0)end';
}
}
function pollInventories(outputFile){
// console.log("/silent-command "+'local a=game.players["Danielv123"].get_inventory(defines.inventory.player_main).get_contents()local b=game.players["Danielv123"].get_quickbar().get_contents()local c={}local d="{inventory:{"for e,f in pairs(a)do d=d.."['+"'"+'"..e.."'+"'"+']:"..f..","end;for e,f in pairs(b)do d=d.."['+"'"+'"..e.."'+"'"+']:"..f..","end;d=d.."},requestSlots:{"for e,f in pairs(c)do d=d.."['+"'"+'"..e.."'+"'"+']:"..f..","end;d=d.."}}"game.write_file("'+pluginConfig.scriptOutputFileSubscription+'",d)');
// console.log("Running inventory LUA")
//console.log("/silent-command "+'local a=game.players["Danielv123"].get_inventory(defines.inventory.player_main).get_contents()local b=game.players["Danielv123"].get_quickbar().get_contents()local c={}for d=1,game.players["Danielv123"].force.character_logistic_slot_count do c[d]=game.players["Danielv123"].character.get_request_slot(d)end;local e="{inventory:{"for f,g in pairs(a)do e=e.."['+"'"+'"..f.."'+"'"+']:"..g..","end;for f,g in pairs(b)do e=e.."['+"'"+'"..f.."'+"'"+']:"..g..","end;e=e.."},requestSlots:{"for f,g in pairs(c)do e=e.."['+"'"+'"..g["name"].."'+"'"+']:"..g["count"]..", "end;e=e.."}}"game.write_file("'+pluginConfig.scriptOutputFileSubscription+'",e)')
// console.log("/silent-command "+'local a="{"for b,c in pairs(game.players)do local d=game.players[b].get_inventory(defines.inventory.player_main).get_contents()local e=game.players[b].get_quickbar().get_contents()local f={}for g=1,game.players[b].force.character_logistic_slot_count do f[g]=game.players[b].character.get_request_slot(g)end;a=a..b..":{inventory:{"for h,i in pairs(d)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;for h,i in pairs(e)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;a=a.."},requestSlots:{"for h,i in pairs(f)do a=a.."['+"'"+'"..i["name"].."'+"'"+']:"..i["count"]..", "end;a=a.."}},"end;a=a.."}"game.write_file("'+pluginConfig.scriptOutputFileSubscription+'",a)')
//return "/silent-command "+'local a="{"for b,c in pairs(game.players)do if c.connected then local d=game.players[b].get_inventory(defines.inventory.player_main).get_contents()local e=game.players[b].get_quickbar().get_contents()local f={}for g=1,game.players[b].force.character_logistic_slot_count do f[g]=game.players[b].character.get_request_slot(g)end;a=a..b..":{inventory:{"for h,i in pairs(d)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;for h,i in pairs(e)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;a=a.."},requestSlots:{"for h,i in pairs(f)do a=a.."['+"'"+'"..i["name"].."'+"'"+']:"..i["count"]..", "end;a=a.."}},"end end;a=a.."}"game.write_file("'+outputFile+'",a)'
return '/silent-command '+'local a='+"'"+'{"players":{'+"'"+'local b=false;for c,d in pairs(game.players)do if d.connected and d.character then if b then a=a..'+"'"+','+"'"+'else b=true end;local e=game.players[c].get_inventory(defines.inventory.player_main).get_contents()local f=game.players[c].get_quickbar().get_contents()local g={}for h=1,game.players[c].force.character_logistic_slot_count do g[h]=game.players[c].character.get_request_slot(h)end;a=a..'+"'"+'"'+"'"+'..c..'+"'"+'":{"inventory":{'+"'"+'local i=false;for j,k in pairs(e)do if i then a=a..'+"'"+','+"'"+'else i=true end;a=a..'+"'"+'"'+"'"+'..j..'+"'"+'":'+"'"+'..k end;for j,k in pairs(f)do if i then a=a..'+"'"+','+"'"+'else i=true end;a=a..'+"'"+'"'+"'"+'..j..'+"'"+'":'+"'"+'..k end;a=a..'+"'"+'},"requestSlots":{'+"'"+'i=false;for j,k in pairs(g)do if i then a=a..'+"'"+','+"'"+'else i=true end;a=a..'+"'"+'"'+"'"+'..k['+"'"+'name'+"'"+']..'+"'"+'":'+"'"+'..k['+"'"+'count'+"'"+']end;a=a..'+"'"+'}}'+"'"+'end end;a=a.."}}"game.write_file("'+outputFile+'",a,true,0)'
/*
needle.post(config.masterIP+':'+config.masterPort+'/api/editSlaveMeta', {instanceID: config.unique, password: config.clientPassword, meta: {UPS:UPS}}, function(err, resp) {
// success?
});
*/
}
module.exports = {
handleInventory: handleInventory,
parseJsString: parseJsString,
pollInventories: pollInventories,
insertItemsFromObject: insertItemsFromObject,
}
@@ -0,0 +1,178 @@
const assert = require("assert");
const child_process = require("child_process");
const functions = require("./interfacingFunctions");
const luamin = require("luamin");
const isFactorioCommand = require("_app/isFactorioCommand");
const nock = require("nock");
// sinon does weird prototype manipulation and so doen't need to be saved to anything.
require("mocha-sinon");
const sinon = require("sinon");
describe("inventoryImports/interfacingFunctions.js", function(){
describe("handleInventory(inventory, config)", function(){
let config = {
masterIP:"localhost",
masterPort:8080,
}
it("returns false if given invalid input", function(){
assert(functions.handleInventory("this is a string") === false, "when given strings it should return false and do nothing");
});
it("Sends post requests and outputs factorio commands", function(done){
let inventory = {
"players":{
"Danielv123":{
"inventory":{
"stone":12,
"iron-ore":120,
"raw-wood":10,
"iron-plate":8,
"steel-plate":480
},
"requestSlots":{
"iron-plate":100,
"steel-plate":480,
"raw-wood":20,
"bullshit":0,
}
}
}
}
var scope = nock('http://localhost:8080')
.post('/api/remove', function(body){
// check that we are requesting the right things, () to convert to boolean
// 92 because 100 requested and 8 already in inventory
if(body.name == "iron-plate") assert(body.count == "92");
// nock wants us to return true if this is the correct body and we should reply
// if we return false we won't send a reply.
return true;
})
// nock destroys the endpoints after being hit once, this changes the number of hits required for destruction
.times(10)
// respond with JSON object instead of url query parameters
.reply(200,function(uri, req){
return req
},{
'Content-Type': 'application/json',
'Accept': 'application/json'
})
// nock logging statement .log((data) => console.log(data));
functions.handleInventory(inventory, config, callback);
function callback(x){
assert(x.includes(92) && x.includes("iron-plate"), "it should have included the items it was going to import");
assert(!x.includes("steel-plate"), "Inventory is already filled with steel so shouldn't be importing that");
assert(isFactorioCommand(x), "it should have returned a valid factorio command");
assert(x.includes('"Danielv123"'), "it should include the name of the player that will recieve the items");
done();
}
});
it("posts leftovers to master", function(done){
let inventory2 = {
"exports":{
"1":[
{"name":"iron-ore","count":1200},
{"name":"steel-plate","count":1337},
{"name":"copper-plate","count":0},
]
}
}
let timesRan = 0;
let callback = function(){
timesRan++
assert(timesRan <= 2, "placed more than 2 items, unexpeceted test result");
if(timesRan == 2){
done();
}
}
var scope = nock('http://localhost:8080')
.post('/api/place', function(body){
// check that we are requesting the right things
assert(body.name != "copper-plate", "copper plate should not be placed because it is 0");
assert(body.count > 0, "no need for placing 0 things");
assert(Number(body.count) != NaN, "count should be a number");
callback();
return true;
})
// nock destroys the endpoints after being hit once, this changes the number of hits required for destruction
.times(10)
.reply(200,"affirmative")
// if it tries using the nonexisting callback (argv 3) it will throw. It shoudln't.
assert.doesNotThrow(function(){
functions.handleInventory(inventory2, config);
}, "this here threw for some reason, remove assert.doesNotThrow for easier debugging of this issue");
});
});
describe("pollInventories(outputfile)", function(){
it("Returns a big factorio command as string", function(){
let x = functions.pollInventories("output.txt");
assert(typeof x == "string", "pollInventories() should always return a string");
assert(isFactorioCommand(x), "This is not a command factorio will accept, please prepend /c");
assert(x.length > 20, "LUA string seems to short, this ain't good");
});
it("Returns valid LUA", function(){
let x = functions.pollInventories("output.txt");
// remove string added to make factorio interpret as command
x = x.replace("/silent-command ","").replace("/c ","");
let y = false;
// luamin throws when it recieves something that is invalid LUA
assert.doesNotThrow(function(){
y = luamin.minify(x);
}, "Invalid LUA supplied");
assert(typeof y == "string", "luamin does not return string???");
assert(y.length > 20, "Minified LUA too short, something is up");
});
});
describe("insertItemsFromObject(object)", function(){
let testObject = {"copper-plate":100, "iron-ore":1337}
let playerName = "Danielv123"
it("Returns undefined if any of the parameters are missing", function(){
let x = functions.insertItemsFromObject();
assert(x === undefined);
let y = functions.insertItemsFromObject(testObject);
assert(y === undefined);
let z = functions.insertItemsFromObject(undefined, playerName);
assert(z === undefined);
});
it("Throws if player name is not a string", function(){
assert.throws(function(){
let x = functions.insertItemsFromObject(testObject, testObject);
}, "playerName is: 'object' instead of string!");
});
it("Returns a big factorio command as string", function(){
let x = functions.insertItemsFromObject(testObject, playerName);
assert(typeof x == "string", "LUA returned should be a string");
assert(x.length > 20, "LUA strings are quite long");
assert(isFactorioCommand(x), "This is not a command factorio will accept, please prepend /c");
});
it("The returned string is a valid factorio command", function(){
let x = functions.insertItemsFromObject(testObject, playerName);
assert(isFactorioCommand(x) == true);
});
});
describe("parseJsString(string) => object", function(){
it("converts a string with JS object notation to object", function(){
let x = functions.parseJsString("{hello:'thisAString!', yes:function(){return 'JS string!'}, not:'json'}");
assert(!!x == true, "Should under no circumstance return a falsey value, even !!{} is truthy");
assert(typeof x == "object", "parseJsString should return an object in this case");
assert(typeof x.yes == "function", "parseJsString is not JSON, so it should be able to contain functions");
});
it("*Tries* to avoid XSS attempts by throwing on certain symbols", function(){
assert.throws(function(){
let x = functions.parseJsString("{};throw 'fail'");
}, function(err) {
let error = err+""
/* istanbul ignore else */
if(error.includes('parseJsString might have gotten something that could be a xss attempt')){
return true;
}
}, "should throw when ; are passed because those are the simplest form of xss");
});
});
});
+11
View File
@@ -0,0 +1,11 @@
module.exports = function(x){
if(x === false || x === true || x === null) return true; // === false is valid JSON, so is true and null
var isNotJson = false
try{
z = JSON.parse(x);
} catch (e) {
isNotJson = true;
}
return !isNotJson;
}
@@ -0,0 +1,20 @@
const isJson = require("./isJson");
const assert = require("assert");
describe("inventoryImports/isJson.js()", function(){
it("checks if a thing is valid JSON", function(){
assert(isJson("string") === false);
assert(isJson("{js_object_notation='very false'}") === false);
assert(isJson('{ "name":"John", "age":31, "city":"New York" }') === true, "This is valid JSON, lib disagrees");
});
it("recognizes that true and false are both valid JSON", function(){
assert(isJson(true) === true);
assert(isJson(false) === true);
});
it("Handles null as valid JSON (after spec)", function(){
assert(isJson(null) === true);
});
it("Recognizes that JSON doesn't allow trailing commas (,)", function(){
assert(isJson('["a", "b", ]') === false, "JSON should not allow trailing commas");
});
});
@@ -0,0 +1,180 @@
-- Lua cheatsheet
/c game.write_file("test.txt",serpent.block(game.players["Danielv123"].get_quickbar().get_contents()))
/c game.write_file("test.txt",serpent.block(game.players["Danielv123"].get_inventory(defines.inventory.player_main).get_contents()))
-- print JSON of entire inventory and request slots to file
-- TODO: Get requester slot data as well
-- requester slot data is game.players.character.get_request_slot(slot)
-- returns {name="copper-plate", count=47} or nil if nothing is set
-- maybe game.players.character.item_requests? http://lua-api.factorio.com/latest/LuaEntity.html#LuaEntity.item_requests
/c
local requests = {}
for i = 1, game.player.force.character_logistic_slot_count do
requests[i] = game.players["Danielv123"].character.get_request_slot(i)
end
local o = "Requests: "
for k,v in pairs(requests) do
o = o..v["name"]..": "..v["count"]..", "
end
game.print(o)
-- sum up our inventory and request slots and print it as valid JSON
-- turns out the valid part is important
/c
local o = '{"players":{'
local notFirst = false
for a,b in pairs(game.players) do
if b.connected and b.character then
if notFirst then
o = o .. ','
else
notFirst = true
end
local inventory = game.players[a].get_inventory(defines.inventory.player_main).get_contents()
local quickbar = game.players[a].get_quickbar().get_contents()
--[[ Get contents of requester slots into a table ]]
local requests = {}
for i = 1, game.players[a].force.character_logistic_slot_count do
requests[i] = game.players[a].character.get_request_slot(i)
end
o = o..'"'..a..'":{"inventory":{'
--[[ generate JSON table ]]
local addComma = false
for k,v in pairs(inventory) do
if addComma then
o = o..','
else
addComma = true
end
o = o..'"'..k..'":'..v
end
for k,v in pairs(quickbar) do
if addComma then
o = o..','
else
addComma = true
end
o = o..'"'..k..'":'..v
end
o = o..'},"requestSlots":{'
addComma = false
for k,v in pairs(requests) do
if addComma then
o = o..','
else
addComma = true
end
o = o..'"'..v['name']..'":'..v['count']
end
o = o..'}}' --[[ We need a comma after these two if there is more than 1 player, handled up top by notFirst]]
end
end
o = o.."}}"
game.write_file("t.txt", o, true, 0)
game.print(o)
--[[
Example output
{"players":{"1":{"inventory":{"stone":12,"iron-ore":120,"raw-wood":10,"iron-plate":8,"steel-plate":480},"requestSlots":{}}}}
Minification procedure:
Run through https://mothereff.in/lua-minifier
Replace ' with '+"'"+'
Prepend /silent-command
remove game.print(o)
replace "t.txt" with "'+outputFile+'"
enclose in ''
]]
-- minified strings (newest on the top)
local a='+"'"+'{"players":{'+"'"+'local b=false;for c,d in pairs(game.players)do if d.connected then if b then a=a..'+"'"+','+"'"+'else b=true end;local e=game.players[c].get_inventory(defines.inventory.player_main).get_contents()local f=game.players[c].get_quickbar().get_contents()local g={}for h=1,game.players[c].force.character_logistic_slot_count do g[h]=game.players[c].character.get_request_slot(h)end;a=a..'+"'"+'"'+"'"+'..c..'+"'"+'":{"inventory":{'+"'"+'local i=false;for j,k in pairs(e)do if i then a=a..'+"'"+','+"'"+'else i=true end;a=a..'+"'"+'"'+"'"+'..j..'+"'"+'":'+"'"+'..k end;for j,k in pairs(f)do if i then a=a..'+"'"+','+"'"+'else i=true end;a=a..'+"'"+'"'+"'"+'..j..'+"'"+'":'+"'"+'..k end;a=a..'+"'"+'},"requestSlots":{'+"'"+'i=false;for j,k in pairs(g)do if i then a=a..'+"'"+','+"'"+'else i=true end;a=a..'+"'"+'"'+"'"+'..k['+"'"+'name'+"'"+']..'+"'"+'":'+"'"+'..k['+"'"+'count'+"'"+']end;a=a..'+"'"+'}}'+"'"+'end end;a=a.."}}"game.write_file("'+outputFile+'",a,true,0)
-- offline playes
local a="{"for b,c in pairs(game.players)do if c.connected then local d=game.players[b].get_inventory(defines.inventory.player_main).get_contents()local e=game.players[b].get_quickbar().get_contents()local f={}for g=1,game.players[b].force.character_logistic_slot_count do f[g]=game.players[b].character.get_request_slot(g)end;a=a..b..":{inventory:{"for h,i in pairs(d)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;for h,i in pairs(e)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;a=a.."},requestSlots:{"for h,i in pairs(f)do a=a.."['+"'"+'"..i["name"].."'+"'"+']:"..i["count"]..", "end;a=a.."}},"end end;a=a.."}"game.print(a)
-- multiple players
local a="{"for b,c in pairs(game.players)do local d=game.players[b].get_inventory(defines.inventory.player_main).get_contents()local e=game.players[b].get_quickbar().get_contents()local f={}for g=1,game.players[b].force.character_logistic_slot_count do f[g]=game.players[b].character.get_request_slot(g)end;a=a..b..":{inventory:{"for h,i in pairs(d)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;for h,i in pairs(e)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;a=a.."},requestSlots:{"for h,i in pairs(f)do a=a.."['+"'"+'"..i["name"].."'+"'"+']:"..i["count"]..", "end;a=a.."}},"end;a=a.."}"game.print(a)
local a=game.players["Danielv123"].get_inventory(defines.inventory.player_main).get_contents()local b=game.players["Danielv123"].get_quickbar().get_contents()local c={}for d=1,game.players["Danielv123"].force.character_logistic_slot_count do c[d]=game.players["Danielv123"].character.get_request_slot(d)end;local e="{inventory:{"for f,g in pairs(a)do e=e.."['+"'"+'"..f.."'+"'"+']:"..g..","end;for f,g in pairs(b)do e=e.."['+"'"+'"..f.."'+"'"+']:"..g..","end;e=e.."},requestSlots:{"for f,g in pairs(c)do e=e.."['+"'"+'"..g["name"].."'+"'"+']:"..g["count"]..", "end;e=e.."}}"game.print(e)
local inventory = game.players["Danielv123"].get_inventory(defines.inventory.player_main).get_contents()local quickbar = game.players["Danielv123"].get_quickbar().get_contents()local requests = {}for i = 1, game.player.force.character_logistic_slot_count do requests[i] = game.players["Danielv123"].character.get_request_slot(i) end local o = "{inventory:{" for k,v in pairs(inventory) do o = o.."['+"'"+'"..k.."'+"'"+']:"..v.."," end for k,v in pairs(quickbar) do o = o.."['+"'"+'"..k.."'+"'"+']:"..v.."," end o = o.."},requestSlots:{" for k,v in pairs(requests) do o = o.."['+"'"+'"..v["name"].."'+"'"+']:"..v["count"]..", " end o = o.."}}" game.write_file("test.txt", o,true,0)
-- insert items in players inventory and export leftovers
-- name should be players[i]
/c
local name = "1"
local items = {["iron-ore"]=30, ["steel-plate"]=120}
local items_failed = {}
if(game.players[name] and game.players[name].connected) then
for item,count in pairs(items) do
local i = game.players[name].insert{name=item,count=count}
if not items_failed[name] then
items_failed[name] = {}
end
if items_failed[name][item] then
items_failed[name][item] = items_failed[name][item] + count - i
else
items_failed[name][item] = count - i
end
end
game.write_file("t.txt", serpent.line({["exports"]=items_failed}, {["comment"]=false, ["compact"]=true}),true,0)
end
--[[
Writes something like
{exports={["1"]={["iron-ore"]=0,["steel-plate"]=0}}}
converting to JS would be
replace "[" and "]" with ""???
replace "=" with ":"
]]
--[[
JSON edition
{"exports":{"1":[{"name":"iron-ore","count":0},{"name":"steel-plate","count":0}]}}
Should always return valid JSON
]]
/c
local name = "1"
local items = {["iron-ore"]=30, ["steel-plate"]=120}
if(game.players[name] and game.players[name].connected) then
local o = '{"exports":{"'..name..'":['
local addComma = false
for item,count in pairs(items) do
local i = game.players[name].insert{name=item,count=count}
if addComma then
o = o..','
else
addComma = true
end
o = o .. '{"name":"'..item..'","count":'..count - i..'}'
end
o = o .. ']}}'
game.write_file("t.txt", o,true,0)
end
--[[
Minification procedure:
Run through https://mothereff.in/lua-minifier
Replace ' with '+"'"+'
Prepend /silent-command
replace "t.txt" with "'+outputFile+'"
enclose in ''
]]
-- JSON outputFile
local a="1"local b={["iron-ore"]=30,["steel-plate"]=120}local c={}if game.players[a]and game.players[a].connected then local d='+"'"+'{"exports":{"'+"'"+'..a..'+"'"+'":['+"'"+'local e=false;for f,g in pairs(b)do local h=game.players[a].insert{name=f,count=g}if e then d=d..'+"'"+','+"'"+'else e=true end;d=d..'+"'"+'{"name":"'+"'"+'..f..'+"'"+'","count":'+"'"+'..g-h..'+"'"+'}'+"'"+'end;d=d..'+"'"+']}}'+"'"+'game.write_file("'+outputFile+'",d,true,0)end
-- output to serpented LUA table
local a="Danielv123"local b={["iron-ore"]=30,["steel-plate"]=120}local c={}if game.players[a]and game.players[a].connected then for d,e in pairs(b)do local f=game.players[a].insert{name=d,count=e}if not c[a]then c[a]={}end;if c[a][d]then c[a][d]=c[a][d]+e-f else c[a][d]=e-f end end;game.write_file("'+outputFile+'",serpent.line(c,{["comment"]=false,["compact"]=true}),true,0)end
+13
View File
@@ -0,0 +1,13 @@
/*
Config for clusterio remote command plugin.
Allows you to execute commands on slaves with a web API.
Careful, may allow for RCE vulnerabilities.
*/
module.exports = {
// Name of package. For display somewhere I guess.
name: "remoteCommands",
version: "1.0.0",
binary: "nodePackage",
description: "Config for clusterio remote command plugin. Allows you to execute commands on slaves with a web API.",
}
+25
View File
@@ -0,0 +1,25 @@
module.exports = class remoteCommands {
constructor(mergedConfig, messageInterface, extras){
this.messageInterface = messageInterface;
this.config = mergedConfig;
this.socket = extras.socket;
if(this.config.allowRemoteCommandExecution){
this.socket.on("runCommand", data => {
let {commandID} = data;
this.messageInterface(data.command);
if(commandID){
this.socket.emit("runCommandReturnValue", {
commandID,
body: {
info: "Command was sent to factorio",
},
});
}
});
}
}
scriptOutput(data){
// we don't return anything yet
}
}
+98
View File
@@ -0,0 +1,98 @@
var mkdirp = require("mkdirp-promise");
var mkdirpSync = require("mkdirp").sync;
var fs = require("fs");
var events = require("events");
var objectOps = require("./../../lib/objectOps");
var LinvoDB = require("linvodb3");
LinvoDB.dbPath = "./database/linvodb/";
let allowLogging = true;
let log = string => (allowLogging)? console.log(string):""
module.exports = class chunkStore {
constructor(name, chunkSize = 32, path = "./chunkStore/"){
if(!name || (typeof name != "string" && typeof name != "number")) throw new Error("chunkstore requires a name string as first param");
name = name.toString();
this.name = name;
this.chunkSize = chunkSize;
this.path = (path[path.length] == "/") ? path : path+"/";
mkdirpSync(path+name);
this.db = new LinvoDB(name, {}, {});
this.eventEmitter = new events.EventEmitter();
}
setEntity(x/**{number}*/, y/**{number}*/, newEntity/**{object}*/){
return new Promise((resolve, reject) => {
x = Math.floor(x);
y = Math.floor(y);
this.db.find({x, y}, (err, docs) => {
if(typeof newEntity == "object"){
if(err) reject(err);
let entityDoc = {x, y, entity: newEntity};
if(docs && docs[0] && docs[0]._id){
entityDoc._id = docs[0]._id;
}
this.db.save(entityDoc, (err,docs) => {
if(err) reject(err);
resolve(docs);
this.eventEmitter.emit("change", docs)
});
} else {
docs.forEach(doc => {
doc.remove(()=>{
// removed :)
});
});
resolve();
this.eventEmitter.emit("change", {x,y});
}
});
});
}
getEntity(x, y){
return new Promise((resolve, reject) => {
this.db.find({x:Math.floor(x), y:Math.floor(y)}, (err, docs) => {
if(err) reject(err);
resolve(docs);
});
});
}
onEntityChange(eventhandler/**function*/){
if(typeof eventhandler == "function") {
this.eventEmitter.on("change", eventhandler);
}
}
getChunk(x, y){
return new Promise((resolve, reject) => {
/*
// make new chunk in case we can't find an existing one
let chunk = {
dataObject: {},
position:{x,y}, // fancy ES6 feature that generates to {x:x, y:y}
chunkSize:this.chunkSize
};
fs.readFile(this.path+this.name+"/"+x+"/"+y, (err, chunkFile) => {
if(err){
log(err)
resolve(chunk);
} else {
if(objectOps.isJSON(chunkFile)) {
log("returned from file")
let chunk = JSON.parse(chunkFile);
resolve(chunk);
} else {
log("file failed")
resolve(chunk);
}
}
}); // add x and y values as well and we are done?
*/
});
}
}
@@ -0,0 +1,64 @@
var assert = require("assert");
var chunkStore = require("./chunkStore.js");
describe("remoteMap/chunkStore.js", () => {
// define some values for our test
const inserterX = 14.5; // rounds to 15
const inserterY = -17.5; // rounds to -17, +64 would be 47
const inserterName = "inserter";
let store // make our store elevated
it("is a constructor", ()=>{
assert.equal(typeof chunkStore, "function");
// create a new store and elevate it
store = new chunkStore("910239"/* instanceID, use defaults for the rest */, undefined, "./database/unitTests/chunkStore/");
assert.equal(store.name, "910239");
});
it("allows you to store the locations of entities", done => {
store.setEntity(inserterX, inserterY, {name:inserterName, otherData:"can be arbitrary"}).then(() => {
done();
// nothing to do here really
});
});
it("returns entities at positions you query for", done => {
setTimeout(()=>{
store.getEntity(inserterX, inserterY).then(entities => {
entity = entities[0]
// console.log(entity)
assert.equal(entity.x, 14);
assert.equal(entity.y, -18);
assert.equal(entity.entity.name, "inserter");
assert.equal(entity.entity.otherData, "can be arbitrary");
done();
});
},300);
});
it("deletes entities when not provided with an object as 3rd arg", done => {
store.setEntity(inserterX, inserterY).then(entity => {
assert(entity === undefined);
setTimeout(()=>{
store.getEntity(inserterX, inserterY).then(entities => {
// console.log(entities)
assert.equal(entities.length, 0, "Why are we getting results, it should be empty?");
done();
});
},300);
});
});
describe("validates input by throwing when it is invalid", ()=>{
it("throws if called without a string as the first parameter", ()=>{
assert.throws(()=>{
let throwingChunkStore = new chunkStore(undefined, undefined, "asdasda");
});
assert.throws(()=>{
let throwingChunkStore = new chunkStore({}, undefined, "asdasdas");
});
});
it("converts numbers to strings automatically if they are provided as the first parameter", ()=>{
assert.ok(()=>{
let nonThrowingChunkStore = new chunkStore(42, undefined, "./database/unitTests/chunkStore/");
});
});
});
});
+16
View File
@@ -0,0 +1,16 @@
/*
Config for clusterio remote mapping plugin. Will handle writing out new map data, aggregating it and sending it to the master (or something) for
display/interaction.
*/
var modInfo = require("./info.json");
module.exports = {
// Name of package. For display somewhere I guess.
name: modInfo.name,
version: modInfo.version,
// Binary entrypoint for plugin. Don't let it crash. Stdout is sent to game as server chat (run /c commands from here for interface)
// Make sure its cross platform somehow.
binary: "nodePackage",
description: modInfo.description,
// We'll send everything in this file to your stdin. Beware.
scriptOutputFileSubscription: "remoteMap.txt",
}
+145
View File
@@ -0,0 +1,145 @@
-- MIT License
--
-- Copyright (c) 2017 Florian Jung
--
-- Permission is hereby granted, free of charge, to any person obtaining a
-- copy of this factorio lua stub and associated
-- documentation files (the "Software"), to deal in the Software without
-- restriction, including without limitation the rights to use, copy, modify,
-- merge, publish, distribute, sublicense, and/or sell copies of the
-- Software, and to permit persons to whom the Software is furnished to do
-- so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in
-- all copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
-- THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-- DEALINGS IN THE SOFTWARE.
function write_file(data)
stringsToWriteNextTick = stringsToWriteNextTick .. data .. '\n'
-- game.write_file("remoteMap.txt", data, true)
end
function dump_cached_writes()
game.write_file("remoteMap.txt", stringsToWriteNextTick, true, 0)
end
function complain(text)
if false then -- only print during development
print(text)
game.forces["player"].print(text)
end
end
local todo_next_tick = {}
must_write_initstuff = true
function on_tick(event)
dump_cached_writes()
stringsToWriteNextTick = ''
if must_write_initstuff then
must_write_initstuff = false
log("I must write init stuff!")
writeout_initial_stuff()
end
if #todo_next_tick > 0 then
complain("on_tick executing "..#todo_next_tick.." stored callbacks")
for _,func in ipairs(todo_next_tick) do
func()
end
todo_next_tick = {}
end
end
function writeout_initial_stuff()
log("Writing out initial stuff...")
writeout_objects(game.surfaces['nauvis'], {left_top={x=-64,y=-64},right_bottom={x=64,y=64}})
end
function writeout_objects(surface, area)
--if my_client_id ~= 1 then return end
for idx, ent in pairs(surface.find_entities(area)) do
if area.left_top.x <= ent.position.x and ent.position.x < area.right_bottom.x and area.left_top.y <= ent.position.y and ent.position.y < area.right_bottom.y then
if ent.prototype.collision_mask ~= nil and ent.prototype.collision_mask['player-layer'] then
-- line = line .. ent.name.." "..ent.position.x.." "..ent.position.y.."\n"
-- local line = ent.name..","..ent.position.x..","..ent.position.y
-- if ent.supports_direction then
-- line = line .. ",rot="..ent.direction
-- end
-- write_file(line)
on_some_entity_created(nil, ent)
end
end
end
--write_file(header..table.concat(lines,"").."\n")
--write_file(table.concat(lines,"").."\n")
end
function on_chunk_generated(event)
local area = event.area
local surface = event.surface
--print("chunk generated at ("..area.left_top.x..","..area.left_top.y..") -- ("..area.right_bottom.x..","..area.right_bottom.y..")")
if surface ~= game.surfaces['nauvis'] then -- we only support one surface
return
end
-- writeout_resources(surface, area)
writeout_objects(surface, area)
-- writeout_tiles(surface, area)
end
function on_some_entity_created(event, entp)
local ent
if entp then
ent = entp
else
ent = event.entity or event.created_entity or nil
end
if ent == nil then
complain("wtf, on_some_entity_created has nil entity")
return
end
local entityData = ent.name..","..ent.position.x..","..ent.position.y
if ent.supports_direction then
entityData = entityData .. ",rot="..ent.direction
end
-- log(entityData)
write_file(entityData)
--writeout_objects(ent.surface, {left_top={x=math.floor(ent.position.x), y=math.floor(ent.position.y)}, right_bottom={x=math.floor(ent.position.x)+1, y=math.floor(ent.position.y)+1}})
--complain("on_some_entity_created: "..ent.name.." at "..ent.position.x..","..ent.position.y)
end
function on_some_entity_deleted(event)
local ent = event.entity
if ent == nil then
complain("wtf, on_some_entity_deleted has nil entity")
return
end
-- we can't do this now, because the entity still exists at this point. instead, we schedule the writeout for the next tick
--local surface = ent.surface
--local area = {left_top={x=math.floor(ent.position.x), y=math.floor(ent.position.y)}, right_bottom={x=math.floor(ent.position.x)+1, y=math.floor(ent.position.y)+1}}
--table.insert(todo_next_tick, function () writeout_objects(surface, area ) end)
write_file("deleted,"..ent.position.x..","..ent.position.y)
--complain("on_some_entity_deleted: "..ent.name.." at "..ent.position.x..","..ent.position.y)
end
script.on_event(defines.events.on_tick, on_tick)
script.on_event(defines.events.on_chunk_generated, on_chunk_generated)
script.on_event(defines.events.on_biter_base_built, on_some_entity_created) --entity
script.on_event(defines.events.on_built_entity, on_some_entity_created) --created_entity
script.on_event(defines.events.on_robot_built_entity, on_some_entity_created) --created_entity
script.on_event(defines.events.on_player_rotated_entity, on_some_entity_created) --created_entity
script.on_event(defines.events.on_entity_died, on_some_entity_deleted) --entity
script.on_event(defines.events.on_player_mined_entity, on_some_entity_deleted) --entity
script.on_event(defines.events.on_robot_mined_entity, on_some_entity_deleted) --entity
script.on_event(defines.events.on_resource_depleted, on_some_entity_deleted) --entity
@@ -0,0 +1,5 @@
/c
local toDelete = game.surfaces[1].find_entities({{3,7},{4,8}})
for i, entity in pairs(toDelete) do
entity.die()
end
+130
View File
@@ -0,0 +1,130 @@
const needle = require("needle");
const ioClient = require("socket.io-client");
const chunkStore = require("./chunkStore.js");
const moduleConfig = require("./config"); // not to be confused with clusterio config. This config is private to this plugin.
class remoteMap {
constructor(slaveConfig, messageInterface){
this.config = slaveConfig;
this.messageInterface = messageInterface;
// initialize chunk database
this.chunkMap = new chunkStore(this.config.unique, 64, "./database/chunkStore/");
this.chunkMap.onEntityChange(entity => {
// this.messageInterface("Entity changed: ")
// this.messageInterface(entity);
// check valid coordinates (dunno what could go wrong, but can never be too sure)
if(entity && entity.x !== undefined && !isNaN(Number(entity.x)) && entity.y !== undefined && !isNaN(Number(entity.y))){
this.socket.emit("sendEntity", entity);
}
});
// set up websocket communication and handle requests from web interface users (which are going through master)
// socket should be a global
this.socket = ioClient("http://"+this.config.masterIP+":"+this.config.masterPort);
this.socket.on("hello", data => {
this.socket.emit("registerSlaveMapper", {instanceID: this.config.unique});
setInterval(()=>{
this.socket.emit("heartbeat"); // send our heartbeat to prevent being assumed dead
},10000);
});
this.socket.on("getChunk", req => {
this.messageInterface("getChunk is not supported!");
/*this.chunkMap.getChunk(req.x, req.y).then(chunk => {
chunk.requesterID = req.requesterID;
this.socket.emit("sendChunk", chunk);
});*/
});
this.socket.on("getEntity", req => {
// check for valid coordinates
if(req.x !== undefined && !isNaN(Number(req.x)) && req.y !== undefined && !isNaN(Number(req.y))){
this.chunkMap.getEntity(req.x, req.y).then(entities => {
if(entities && entities.length > 0){
entities.forEach(entity => {
this.socket.emit("sendEntity", entity);
});
}
});
} else {
this.messageInterface("socket.getEntity triggered called with invalid parameters");
}
});
// both placing and deleting entities via the web interface
this.socket.on("placeEntity", req => {
// game.player.surface.create_entity{name="small-ship-wreck", position={game.player.position.x-2, game.player.position.y+1}, direction=..., force=...}
this.messageInterface("placing "+JSON.stringify(req));
if(req.name && typeof req.name == "string" && req.position && !isNaN(Number(req.position.x)) && !isNaN(Number(req.position.y))){
if(req.name == "deleted"){ // {{'+req.position.x+','+req.position.y+'},{'+req.position.x+','+req.position.y+'}}
// delete entity from game world using LUA command
this.messageInterface('/silent-command local toDelete = game.surfaces[1].find_entities({{'+req.position.x+','+req.position.y+'},{'+(req.position.x+1)+','+(req.position.y+1)+'}}) for i, entity in pairs(toDelete) do entity.die() end');
// this position is now empty, delete whatever was there from DB
this.deleteEntity(req.position.x, req.position.y);
} else {
let command = '/silent-command game.surfaces[1].create_entity{name="'+req.name+'", position={'+Number(req.position.x)+', '+Number(req.position.y)+'}, force="neutral"'
if(req.direction) command += ', direction='+req.direction;
command += '}';
this.messageInterface(command); // execute command
// add entity to the chunkStore database because create_entity does not trigger any events
let name = req.name;
let entity = {name};
if(typeof req.direction == "string"){
entity.rot = req.direction;
}
this.addEntity(req.position.x, req.position.y, entity);
}
}
});
this.messageInterface("/silent-command game.print('"+moduleConfig.name+" version "+moduleConfig.version+" enabled')");
}
addEntity(x,y,entity){ // add entity to database
this.chunkMap.setEntity(Number(x), Number(y), entity).then(data => {
this.messageInterface("Added "+entity.name+" to data "+Number(x)+", "+Number(y));
}).catch((err)=>this.messageInterface(err));
}
deleteEntity(x,y){ // remove entity from database
// this position is now empty, delete whatever was there from DB
this.chunkMap.setEntity(x, y, "delete this entity").then(() => {
this.messageInterface("Deleted entity at "+x+", "+y);
}).catch((err)=>this.messageInterface(err));
}
scriptOutput(data){
if(data){
this.messageInterface(data);
// express-transport-belt,-35.5,-14.5,[...]
data = data.split(",");
if(data && data[0] == undefined){
this.messageInterface("empty: ");
this.messageInterface(data);
return
}
let name = data[0];
let xPos = data[1];
let yPos = data[2];
if(xPos && yPos && name){
if(name == "deleted"){
// this position is now empty, delete whatever was there from DB
this.deleteEntity(xPos, yPos);
} else {
let entity = {name};
for(let i = 3; i < data.length; i++){
if(data[i].includes("=")){
let kv = data[i].split("=");
entity[kv[0]] = kv[1];
}
}
// save this entity in the database
this.addEntity(xPos, yPos, entity);
/* this.chunkMap.setEntity(xPos, yPos, entity).then(data => {
this.messageInterface("Added "+name+" to data "+xPos+", "+yPos);
}).catch((err)=>this.messageInterface(err)); */
}
}
}
}
}
module.exports = remoteMap;
+9
View File
@@ -0,0 +1,9 @@
var assert = require("assert");
var remoteMap = require("./index.js");
describe("remoteMap/index.js", () => {
it("exports a single class (or at least a function)", ()=>{
assert.equal(typeof remoteMap, "function");
});
});
+11
View File
@@ -0,0 +1,11 @@
{
"name": "remoteMap",
"title": "Clusterio remote map",
"version": "2.2.0",
"date": "18-11-2017",
"author": "Danielv123",
"dependencies": ["base >= 0.15"],
"factorio_version": "0.16",
"description": "Clusterio plugin for mapping and building through the browser. No more need to start game, download and catching up for every game, also mobile factorio confirmed?"
}
+15
View File
@@ -0,0 +1,15 @@
module.exports = {
// Name of package. For display somewhere I guess.
name: "researchSync",
version: "1.0.0",
// Binary entrypoint for plugin. Don't let it crash. Stdout is sent to game as server chat (run /c commands from here for interface)
// Make sure its cross platform somehow.
binary: "nodePackage",
arguments: ["index.js"],
description: "syncs research across servers",
// We'll send everything in this file to your stdin. Beware.
scriptOutputFileSubscription: "researchSync.txt",
fileReadDelay: 50
}
@@ -0,0 +1,9 @@
/silent-command
local data = {};
for _, tech in pairs(game.forces["player"].technologies) do
table.insert(data, tech.name .. ":" .. tostring(tech.researched) .. ":" .. tostring(tech.level));
end
game.write_file("researchSync.txt", table.concat(data, "\n"), false, 0)
@@ -0,0 +1,16 @@
/silent-command
if game.forces['player'].technologies['{tech_name}'] then
if game.forces['player'].technologies['{tech_name}'].researched ~= {tech_researched} then
game.forces['player'].technologies['{tech_name}'].researched = {tech_researched}
end
if game.forces['player'].technologies['{tech_name}'].level ~= {tech_level} then
game.forces['player'].technologies['{tech_name}'].level = {tech_level}
end
script.raise_event(defines.events.on_research_finished, {research=game.forces['player'].technologies['{tech_name}'], by_script=true})
game.play_sound({path="utility/research_completed"})
if game.forces['player'].technologies['{tech_name}'].researched == false then
game.print("Technology {tech_name} synced at level {tech_level}")
else
game.print("Technology {tech_name} synced")
end
end
+129
View File
@@ -0,0 +1,129 @@
const fs = require('fs');
const needle = require("needle");
class ResearchSync {
constructor(slaveConfig, messageInterface, extras = {}){
this.config = slaveConfig;
this.messageInterface = messageInterface;
this.functions = this.loadFunctions();
this.research = {};
setInterval(() => {
this.pollResearch();
setTimeout(this.doSync.bind(this), 2000);
}, extras.researchSyncPollInterval || 5000);
}
pollResearch() {
// this.messageInterface("Polling Research\n")
this.messageInterface(this.functions.dumpResearch);
}
doSync() {
needle.get(this.config.masterIP + ':' + this.config.masterPort + '/api/slaves', (err, resp, slaveData) => {
if (err){
this.messageInterface("Unable to post JSON master/api/slaves, master might be unreachable");
return false;
}
if (resp.statusCode !== 200){
this.messageInterface("got error when calling slaves", resp.statusCode, resp.body);
return;
}
let needResearch = {};
Object.keys(slaveData).forEach(instanceKey => {
if (slaveData[instanceKey].unique === this.config.unique.toString()) {
return;
}
if (!slaveData[instanceKey].hasOwnProperty('meta') || !slaveData[instanceKey].meta.hasOwnProperty('research')) {
return;
}
let researchList = slaveData[instanceKey].meta.research;
if(researchList){
Object.keys(researchList).forEach(researchName => {
if (isNaN(researchList[researchName].researched) || isNaN(researchList[researchName].level)) {
return;
}
if (needResearch.hasOwnProperty(researchName)) {
if (needResearch[researchName].researched === 0) {
needResearch[researchName].researched = parseInt(researchList[researchName].researched);
}
if (needResearch[researchName].level < parseInt(researchList[researchName].level)) {
needResearch[researchName].level = parseInt(researchList[researchName].level);
}
}
else {
needResearch[researchName] = {researched: parseInt(researchList[researchName].researched), level: parseInt(researchList[researchName].level)};
}
});
}
});
let difference = this.filterResearchDiff(this.research, needResearch);
Object.keys(difference).forEach((key) => {
if (this.research[key]) {
let command = this.functions.enableResearch;
while(command.includes("{tech_name}")){
command = command.replace("{tech_name}", key);
command = command.replace("{tech_researched}", difference[key].researched);
command = command.replace("{tech_level}", difference[key].level);
}
this.messageInterface(command);
console.log('Unlocking '+ key + ': ' + (difference[key].researched === 0 ? 'false' : 'true') + ' and level ' + difference[key].level + ', was '+ (this.research[key].researched === 0 ? 'false' : 'true') + ' at level ' + this.research[key].level);
this.messageInterface("Unlocking research: " + key + " at research state = " + (difference[key].researched === 0 ? 'false' : 'true') + ' and level ' + difference[key].level);
this.research[key] = difference[key];
}
});
needle.post(this.config.masterIP + ':' + this.config.masterPort + '/api/editSlaveMeta', {
instanceID: this.config.unique,
password: this.config.clientPassword,
meta: {research: this.research}
}, function (err, resp) {
// success?
});
});
}
filterResearchDiff(localResearch, remoteResearch) {
let diff = {};
Object.keys(localResearch).forEach((key) => {
if (remoteResearch.hasOwnProperty(key)) {
if ((localResearch[key].researched === 0 && localResearch[key].researched !== remoteResearch[key].researched) || localResearch[key].level < remoteResearch[key].level) {
if (!isNaN(remoteResearch[key].researched) && !isNaN(remoteResearch[key].level)) {
diff[key] = {researched: remoteResearch[key].researched, level: remoteResearch[key].level};
}
}
}
});
return diff;
}
loadFunctions() {
return {
dumpResearch: this.loadFunc("dumpResearch.lua"),
enableResearch: this.loadFunc("enableResearch.lua"),
};
}
loadFunc(path) {
return fs.readFileSync("sharedPlugins/researchSync/" + path,'utf-8').replace(/\r?\n|\r/g,' ');
}
scriptOutput(data){
let kv = data.split(":");
let name = kv[0];
let researched = ('true' !== kv[1]
? 0
: 1);
let level = parseInt(kv[2]);
if (!isNaN(level) && !isNaN(researched)) {
this.research[name] = {researched: researched, level: level};
}
}
}
module.exports = ResearchSync;
+107
View File
@@ -0,0 +1,107 @@
const assert = require("assert");
const isFactorioCommand = require("_app/isFactorioCommand");
const researchSync = require("./index.js");
describe("researchSync/index.js", ()=>{
it("exports a single class (or at least a function)", ()=>{
assert.equal(typeof researchSync, "function");
});
describe("class researchSync()", ()=>{
it(".filterResearchDiff(object1, object2) returns research that needs enabling on the local instance", ()=>{
let reSync = new researchSync({}, function(){});
let obj1 = {
a:{researched: true, level: 0},
b:{researched: true, level: 0},
c:{researched: false, level: 0},
d:{researched: false, level: 10},
};
let obj2 = {
a:{researched: true, level: 0},
b:{researched: true, level: 1},
c:{researched: false, level: 0},
e:{researched: false, level: 50},
};
assert(typeof reSync.filterResearchDiff == "function");
let diffResult = reSync.filterResearchDiff(obj1, obj2);
assert(diffResult.a === undefined);
assert(diffResult.b !== undefined);
assert.equal(diffResult.b.researched, true);
assert.equal(diffResult.b.level, 1);
assert(diffResult.d === undefined);
assert(diffResult.e !== undefined);
assert.equal(diffResult.e.researched, false);
assert.equal(diffResult.e.level, 50);
});
it(".pollResearch() dumps a long Lua command", ()=>{
let reSync = new researchSync({}, io);
let ioRecieved;
function io(str){
ioRecieved = true;
assert(isFactorioCommand(str), "pollResearch should run a command");
}
assert(!ioRecieved);
reSync.pollResearch();
assert(ioRecieved);
});
describe(".scriptOutput(data) handles file writes from factorio", ()=>{
it("wants a key:value pair of a single research, parses and saves it", ()=>{
let reSync = new researchSync({hi:"hello"}, console.log);
let researches = [
'automation:false',
'automation-2:false',
'automation-3:false',
'electronics:false',
'advanced-electronics:false',
'advanced-electronics-2:false',
'circuit-network:false',
'explosives:false',
'logistics:false',
'logistics-2:false',
'logistics-3:false',
'optics:false',
'laser:false',
'solar-energy:false',
'turrets:false',
'laser-turrets:false',
'stone-walls:false',
'gates:false',
'engine:false',
]
researches.forEach(research => {
reSync.scriptOutput(research);
assert(!isFactorioCommand("/c x = "+research));
});
assert(Object.keys(reSync.research).length == researches.length);
});
});
it("regularily polls and syncs research (configurable delay)", (done)=>{
let reSync = new researchSync({}, io, {
researchSyncPollInterval: 100,
});
let ioRecieved;
function io(str){
ioRecieved = true;
assert(isFactorioCommand(str), "pollResearch should run a command");
}
function waitForIo(){
if(!ioRecieved){
setTimeout(waitForIo,50);
} else {
done();
}
}
waitForIo();
});
});
});
+9
View File
@@ -0,0 +1,9 @@
<title>Clusterio</title>
<link rel="stylesheet" type="text/css" href="style.css">
<script src="external/jquery.js"></script>
<script src="external/jquery-ui.js"></script>
<script src="external/canvasjs.js"></script>
<script src="external/moment.min.js"></script>
<script src="external/mousetrap.min.js"></script>
<script src="main.js"></script>
<link href="external/css/Roboto.css" rel="stylesheet">
+23
View File
@@ -0,0 +1,23 @@
<!doctype HTML>
<head>
<%- include("head.html") %>
<script defer src="storage.js"></script>
</head>
<body>
<%- include("sidebar.html") %>
<div id="body">
<div id="storage">
<h2>Storage</h2>
<input type="text" placeholder="search" id="search">
<div id="contents">
<table><tbody><!-- populated by js --></tbody></table>
</div>
</div>
<div id="graphDisplay">
<div id="storageGraphContainer">
<div id="contentGraph" class="storageGraph" style="width: 100%;"></div>
</div>
</div>
</div>
</body>
+355
View File
@@ -0,0 +1,355 @@
import spritesheetJson from "./../pictures/spritesheet.js";
export var entityDrawRules = {
"big-electric-pole": {
sizeInTiles: {
x:2,
y:2,
},
positionOffset: {
x:-1,
y:-1,
}
},
"accumulator": {
spritesheet: spritesheetJson.frames["images/factorio/entity/accumulator/accumulator.png"],
sizeInTiles: {
x:3.5,
y:3.5,
},
positionOffset: {
x:-1,
y:-2,
}
},
"centrifuge": {
spritesheet: spritesheetJson.frames["images/factorio/entity/centrifuge/centrifuge-A.png.0"],
sizeInTiles: {
x:4,
y:4,
},
positionOffset: {
x:-1.5,
y:-1,
},
},
"stone-furnace": {
spritesheet: spritesheetJson.frames["images/factorio/entity/stone-furnace/stone-furnace.png"],
sizeInTiles: {
x:3,
y:2.2,
},
positionOffset: {
x:-0.9,
y:-1,
},
},
"steel-furnace": {
spritesheet: spritesheetJson.frames["images/factorio/entity/steel-furnace/steel-furnace.png"],
sizeInTiles: {
x:2.5,
y:2.5,
},
positionOffset: {
x:-1.2,
y:-1.1,
},
},
"roboport": {
spritesheet: spritesheetJson.frames["images/factorio/entity/roboport/roboport-base.png"],
sizeInTiles: {
x:5,
y:4.5,
},
positionOffset: {
x:-2,
y:-2.2,
},
},
"substation": {
spritesheet: spritesheetJson.frames["images/factorio/entity/substation/substation.png.0"],
sizeInTiles: {
x:3,
y:3,
},
positionOffset: {
x:-1,
y:-1.8,
},
},
"big-electric-pole": {
spritesheet: spritesheetJson.frames["images/factorio/entity/big-electric-pole/big-electric-pole.png.0"],
sizeInTiles: {
x:5,
y:5,
},
positionOffset: {
x:-1,
y:-3.5,
},
},
"electric-mining-drill": {
spritesheet: [{
spritesheet: spritesheetJson.frames["images/factorio/entity/electric-mining-drill/electric-mining-drill-N.png.0"],
},{
spritesheet: spritesheetJson.frames["images/factorio/entity/electric-mining-drill/electric-mining-drill-E.png.0"],
},{
spritesheet: spritesheetJson.frames["images/factorio/entity/electric-mining-drill/electric-mining-drill-S.png.0"],
},{
spritesheet: spritesheetJson.frames["images/factorio/entity/electric-mining-drill/electric-mining-drill-W.png.0"],
},
],
sizeInTiles: {
x:3,
y:3,
},
positionOffset: {
x:-1,
y:-1,
},
},
"straight-rail": {
sizeInTiles: {
x:2,
y:2,
},
positionOffset: {
x:-1,
y:-1,
}
},
};
let template3x3entity = {
sizeInTiles: {
x:3,
y:3,
},
positionOffset: {
x:-1,
y:-1,
},
};
for(let i = 1; i <= 3; i++){
let name = "assembling-machine-"+i;
// entityDrawRules[name] =
entityDrawRules[name] = {
spritesheet: spritesheetJson.frames["images/factorio/entity/"+name+"/"+name+".png.0"],
sizeInTiles: {
x:3.9,
y:3.9,
},
positionOffset: {
x:-1.4,
y:-1.2,
},
}
// template3x3entity;
};
entityDrawRules["radar"] = {
spritesheet: [],
sizeInTiles: {
x:4,
y:4,
},
positionOffset: {
x:-0.8,
y:-1.7,
},
}
for(let i = 0; i < 64; i++){
// animate the radar
entityDrawRules.radar.spritesheet.push({spritesheet: spritesheetJson.frames["images/factorio/entity/radar/radar.png."+i]});
}
["beacon", "electric-furnace"].forEach(name => {
entityDrawRules[name] = template3x3entity;
});
entityDrawRules["lab"] = {
spritesheet: spritesheetJson.frames["images/factorio/entity/lab/lab.png.0"],
sizeInTiles: {
x:3.5,
y:3,
},
positionOffset: {
x:-1,
y:-0.9,
},
}
entityDrawRules["chemical-plant"] = {
spritesheet: [{
spritesheet: spritesheetJson.frames["images/factorio/entity/chemical-plant/chemical-plant.png.0"],
sizeInTiles: {
x:3.5,
y:3.5,
},
positionOffset: {
x:-1.3,
y:-1,
},
},{
spritesheet: spritesheetJson.frames["images/factorio/entity/chemical-plant/chemical-plant.png.1"],
sizeInTiles: {
x:3.7,
y:4,
},
positionOffset: {
x:-1.3,
y:-1.5,
},
},{
spritesheet: spritesheetJson.frames["images/factorio/entity/chemical-plant/chemical-plant.png.2"],
sizeInTiles: {
x:3.5,
y:3.2,
},
positionOffset: {
x:-1.3,
y:-1,
},
},{
spritesheet: spritesheetJson.frames["images/factorio/entity/chemical-plant/chemical-plant.png.3"],
sizeInTiles: {
x:3.7,
y:3.6,
},
positionOffset: {
x:-1.45,
y:-1.3,
},
},
],
};
[{name: "solar-panel", ref:"images/factorio/entity/solar-panel/solar-panel.png"}].forEach(ent => {
entityDrawRules[ent.name] = {
spritesheet: spritesheetJson.frames[ent.ref],
sizeInTiles: {
x:3,
y:3,
},
positionOffset: {
x:-1,
y:-1,
},
}
});
["transport-belt", "fast-transport-belt", "express-transport-belt"].forEach(name => {
entityDrawRules[name] = {
spritesheet: [{
spritesheet: [ // Up
// spritesheetJson.frames["images/factorio/entity/"+name+"/"+name+".png.0"], // 0 to 31
],
sizeInTiles: {
x:1.2,y:1.4,
},
positionOffset: {
x:-0.1,y:-0,
}
},{
spritesheet: [ // Right
// spritesheetJson.frames["images/factorio/entity/"+name+"/"+name+".png.0"], // 0 to 31
],
sizeInTiles: {
x:1.2,y:1.4,
},
positionOffset: {
x:-0.1,y:-0.2,
}
},{
spritesheet: [ // Down
// spritesheetJson.frames["images/factorio/entity/"+name+"/"+name+".png.27"], // 32 to 63
],
flip: "y",
sizeInTiles: {
x:1.3,y:1.6,
},
positionOffset: {
x:-0.15,y:-1.6,
}
},{
spritesheet: [ // Left
// spritesheetJson.frames["images/factorio/entity/"+name+"/"+name+".png.27"], // 32 to 63
],
flip: "x",
sizeInTiles: {
x:1.3,y:1.6,
},
positionOffset: {
x:-1.35,y:-0.25,
}
}],
}
for(let i = 0; i < 16; i++){
if(name == "transport-belt"){
entityDrawRules[name].spritesheet[3].spritesheet.push(spritesheetJson.frames["images/factorio/entity/"+name+"/"+name+".png."+i]);
entityDrawRules[name].spritesheet[2].spritesheet.push(spritesheetJson.frames["images/factorio/entity/"+name+"/"+name+".png."+(i+16)]);
entityDrawRules[name].spritesheet[1].spritesheet.push(spritesheetJson.frames["images/factorio/entity/"+name+"/"+name+".png."+i]);
entityDrawRules[name].spritesheet[0].spritesheet.push(spritesheetJson.frames["images/factorio/entity/"+name+"/"+name+".png."+(i+16)]);
} else {
entityDrawRules[name].spritesheet[3].spritesheet.push(spritesheetJson.frames["images/factorio/entity/"+name+"/"+name+".png."+i]);
entityDrawRules[name].spritesheet[2].spritesheet.push(spritesheetJson.frames["images/factorio/entity/"+name+"/"+name+".png."+(i+32)]);
entityDrawRules[name].spritesheet[1].spritesheet.push(spritesheetJson.frames["images/factorio/entity/"+name+"/"+name+".png."+i]);
entityDrawRules[name].spritesheet[0].spritesheet.push(spritesheetJson.frames["images/factorio/entity/"+name+"/"+name+".png."+(i+32)]);
}
}
});
["splitter", "fast-splitter", "express-splitter"].forEach(splitter => {
entityDrawRules[splitter] = {
rotOffset:180,
}
});
["underground-belt", "fast-underground-belt", "express-underground-belt"].forEach(underneathie => {
entityDrawRules[underneathie] = {
rotOffset: 270,
}
});
["inserter", "fast-inserter", "stack-inserter", "filter-inserter", "burner-inserter", 'long-handed-inserter', 'stack-filter-inserter'].forEach(inserter => {
entityDrawRules[inserter] = {
rotOffset:90,
}
});
entityDrawRules["oil-refinery"] = {
spritesheet: [
{
spritesheet: spritesheetJson.frames["images/factorio/entity/oil-refinery/oil-refinery.png.0"],
}, {
spritesheet: spritesheetJson.frames["images/factorio/entity/oil-refinery/oil-refinery.png.1"],
}, {
spritesheet: spritesheetJson.frames["images/factorio/entity/oil-refinery/oil-refinery.png.2"],
}, {
spritesheet: spritesheetJson.frames["images/factorio/entity/oil-refinery/oil-refinery.png.3"],
}],
}
// correct image size for the oil refinery frames (same for all of them on this one)
entityDrawRules["oil-refinery"].spritesheet.forEach(object => {
object.sizeInTiles = {
x:10,
y:7.5,
}
object.positionOffset = {
x:-2,
y:-2.7,
}
});
// tree sizes
["tree-02",
"tree-02-red",
"tree-03",
"tree-04",
"tree-05",
"tree-07",
"tree-09",
"tree-09-brown",
"tree-09-red"].forEach(tree => {
entityDrawRules[tree] = {
sizeInTiles: {
x: 3,
y: 4,
},
positionOffset: {
x: -0.6,
y: -2.6,
}
}
});
+185
View File
@@ -0,0 +1,185 @@
/*! terminal.js v2.0 | (c) 2014 Erik Österberg | https://github.com/eosterberg/terminaljs */
var Terminal = (function () {
// PROMPT_TYPE
var PROMPT_INPUT = 1, PROMPT_PASSWORD = 2, PROMPT_CONFIRM = 3
var fireCursorInterval = function (inputField, terminalObj) {
var cursor = terminalObj._cursor
setTimeout(function () {
if (inputField.parentElement && terminalObj._shouldBlinkCursor) {
cursor.style.visibility = cursor.style.visibility === 'visible' ? 'hidden' : 'visible'
fireCursorInterval(inputField, terminalObj)
} else {
cursor.style.visibility = 'visible'
}
}, 500)
}
var firstPrompt = true;
promptInput = function (terminalObj, message, PROMPT_TYPE, callback) {
var shouldDisplayInput = (PROMPT_TYPE === PROMPT_INPUT)
var inputField = document.createElement('input')
inputField.style.position = 'absolute'
inputField.style.zIndex = '-100'
inputField.style.outline = 'none'
inputField.style.border = 'none'
inputField.style.opacity = '0'
inputField.style.fontSize = '0.2em'
terminalObj._inputLine.textContent = ''
terminalObj._input.style.display = 'block'
terminalObj.html.appendChild(inputField)
fireCursorInterval(inputField, terminalObj)
if (message.length) terminalObj.print(PROMPT_TYPE === PROMPT_CONFIRM ? message + ' (y/n)' : message)
inputField.onblur = function () {
terminalObj._cursor.style.display = 'none'
}
inputField.onfocus = function () {
inputField.value = terminalObj._inputLine.textContent
terminalObj._cursor.style.display = 'inline'
}
terminalObj.html.onclick = function () {
inputField.focus()
}
inputField.onkeydown = function (e) {
if (e.which === 37 || e.which === 39 || e.which === 38 || e.which === 40 || e.which === 9) {
e.preventDefault()
} else if (shouldDisplayInput && e.which !== 13) {
setTimeout(function () {
terminalObj._inputLine.textContent = inputField.value
}, 1)
}
}
inputField.onkeyup = function (e) {
if (PROMPT_TYPE === PROMPT_CONFIRM || e.which === 13) {
terminalObj._input.style.display = 'none'
var inputValue = inputField.value
if (shouldDisplayInput) terminalObj.print(inputValue)
terminalObj.html.removeChild(inputField)
if (typeof(callback) === 'function') {
if (PROMPT_TYPE === PROMPT_CONFIRM) {
callback(inputValue.toUpperCase()[0] === 'Y' ? true : false)
} else callback(inputValue)
}
}
}
if (firstPrompt) {
firstPrompt = false
setTimeout(function () { inputField.focus() }, 50)
} else {
inputField.focus()
}
}
var terminalBeep
var TerminalConstructor = function (id) {
if (!terminalBeep) {
terminalBeep = document.createElement('audio')
var source = '<source src="http://www.erikosterberg.com/terminaljs/beep.'
terminalBeep.innerHTML = source + 'mp3" type="audio/mpeg">' + source + 'ogg" type="audio/ogg">'
terminalBeep.volume = 0.05
}
this.html = document.createElement('div')
this.html.className = 'Terminal'
if (typeof(id) === 'string') { this.html.id = id }
this._innerWindow = document.createElement('div')
this._output = document.createElement('p')
this._inputLine = document.createElement('span') //the span element where the users input is put
this._cursor = document.createElement('span')
this._input = document.createElement('p') //the full element administering the user input, including cursor
this._shouldBlinkCursor = true
this.beep = function () {
terminalBeep.load()
terminalBeep.play()
}
this.print = function (message) {
var newLine = document.createElement('div')
newLine.textContent = message
this._output.appendChild(newLine)
}
this.input = function (message, callback) {
promptInput(this, message, PROMPT_INPUT, callback)
}
this.password = function (message, callback) {
promptInput(this, message, PROMPT_PASSWORD, callback)
}
this.confirm = function (message, callback) {
promptInput(this, message, PROMPT_CONFIRM, callback)
}
this.clear = function () {
this._output.innerHTML = ''
}
this.sleep = function (milliseconds, callback) {
setTimeout(callback, milliseconds)
}
this.setTextSize = function (size) {
this._output.style.fontSize = size
this._input.style.fontSize = size
}
this.setTextColor = function (col) {
this.html.style.color = col
this._cursor.style.background = col
}
this.setBackgroundColor = function (col) {
this.html.style.background = col
}
this.setWidth = function (width) {
this.html.style.width = width
}
this.setHeight = function (height) {
this.html.style.height = height
}
this.blinkingCursor = function (bool) {
bool = bool.toString().toUpperCase()
this._shouldBlinkCursor = (bool === 'TRUE' || bool === '1' || bool === 'YES')
}
this._input.appendChild(this._inputLine)
this._input.appendChild(this._cursor)
this._innerWindow.appendChild(this._output)
this._innerWindow.appendChild(this._input)
this.html.appendChild(this._innerWindow)
this.setBackgroundColor('black')
this.setTextColor('white')
this.setTextSize('1em')
this.setWidth('100%')
this.setHeight('100%')
this.html.style.fontFamily = 'Monaco, Courier'
this.html.style.margin = '0'
this._innerWindow.style.padding = '10px'
this._input.style.margin = '0'
this._output.style.margin = '0'
this._cursor.style.background = 'white'
this._cursor.innerHTML = 'C' //put something in the cursor..
this._cursor.style.display = 'none' //then hide it
this._input.style.display = 'none'
}
return TerminalConstructor
}())
+53
View File
@@ -0,0 +1,53 @@
export function getParameterByName(name, url) {
if(!url) url = window.location.href;
name = name.replace(/[\[\]]/g, "\\$&");
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
results = regex.exec(url);
if(!results) return null;
if(!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
// ctx, entityImages[name].img, xPos, yPos, size.x, size.y, rotation, sprWidth, sprHeight, offLeft, offTop
export function drawImageWithRotation(ctx, image, x, y, w, h, degrees, sprWidth, sprHeight, offLeft, offTop, flipX, flipY){
ctx.save();
ctx.translate(x+w/2, y+h/2);
ctx.rotate(degrees*Math.PI/180.0);
ctx.translate(-x-w/2, -y-h/2);
if(flipX) {
x -= x*2+sprWidth;
ctx.scale(-1, 1);
}
if(flipY){
y -= y*2+sprHeight;
ctx.scale(1, -1);
}
if(isImageOk(image) && sprWidth != undefined && sprHeight != undefined && offLeft != undefined && offTop != undefined){
// console.log(sprWidth+" "+sprHeight+" "+offLeft+" "+offTop+" "+w+" "+h)
ctx.drawImage(image, offLeft, offTop, sprWidth, sprHeight, x, y, w, h);
} else if(isImageOk(image)){
ctx.drawImage(image, x, y, w, h);
} else {
// console.log("Image is not OK! "+image.src);
}
ctx.restore();
}
export function isImageOk(img) {
// During the onload event, IE correctly identifies any images that
// weren't downloaded as not complete. Others should too. Gecko-based
// browsers act like NS4 in that they report this incorrectly.
if (!img.complete) {
return false;
}
// However, they do have two very useful properties: naturalWidth and
// naturalHeight. These give the true size of the image. If it failed
// to load, either of these should be zero.
if (typeof img.naturalWidth != "undefined" && img.naturalWidth == 0) {
return false;
}
// No other way of checking: assume it's ok.
return true;
}
+216
View File
@@ -0,0 +1,216 @@
(function (){
ensureSetting("Display offline slaves", "boolean", false);
ensureSetting("Enable production graphs", "boolean", true);
ensureSetting("Periodically update storage screen", "boolean", true);
}())
function ensureSetting(setting, type, defaultSetting) {
let settings
try {
settings = JSON.parse(localStorage.settings);
} catch (e) {
}
if(!settings) settings = {};
if(typeof settings[setting] != type) {
settings[setting] = defaultSetting;
console.log("Fixed setting: " + setting + " | " + defaultSetting)
}
localStorage.settings = JSON.stringify(settings)
}
// debug tools
debugTools = {
addItem: function(name, count){
console.log(`Attempting to add ${count} ${name}`)
postJSON("api/place", {
instanceName: "unknown",
instanceID: "unknown",
unique: "unknown",
name: name,
count: count,
}, function(data){
console.log(data);
});
},
removeItem: function(name, count){
console.log(`Attempting to remove ${count} ${name}`)
postJSON("api/remove", {
instanceName: "unknown",
instanceID: "unknown",
unique: "unknown",
name: name,
count: count,
}, function(data){
console.log(data);
});
}
}
var g = {}
contents = {
"iron-plate":100,
"copper-plate":7312,
}
// nice functions
// hash a string to a hash
function djb2(str){
var hash = 5381;
for (var i = 0; i < str.length; i++) {
hash = ((hash << 5) + hash) + str.charCodeAt(i); /* hash * 33 + c */
}
return hash;
}
// callback(err, json)
function getJSON(url, callback) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'json';
xhr.onload = function() {
var status = xhr.status;
if (status == 200) {
callback(null, xhr.response);
} else {
callback(status);
}
};
// triggers if connection is refused
xhr.onerror = function(e){
callback(e);
};
xhr.send();
};
// callback(err, json)
function postJSON(url, data, callback) {
var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
xhr.responseType = 'json';
xhr.onload = function() {
var status = xhr.status;
if (status == 200) {
callback(null, xhr.response);
} else {
callback(status);
}
};
// triggers if connection is refused
xhr.onerror = function(e){
callback(e);
};
xhr.send(JSON.stringify(data));
};
// return Boolean
function isJSON(string){
let stringIsJson = false;
let x
try {
x = JSON.parse(string);
} catch (e){
return false;
}
if(typeof x == "object" || typeof string == "object"){
stringIsJson = true;
}
return stringIsJson;
}
// hash a string to a color
function hashColor(str) {
var hash = djb2(str);
var r = (hash & 0xFF0000) >> 16;
var g = (hash & 0x00FF00) >> 8;
var b = hash & 0x0000FF;
return "#" + ("0" + r.toString(16)).substr(-2) + ("0" + g.toString(16)).substr(-2) + ("0" + b.toString(16)).substr(-2);
}
// exactly what you would expect it to, returns String
function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
// hide the HTML object passed as argument #1
function hideThis(object) {
object.style.visibility = "hidden";
}
Date.prototype.yyyymmdd = function(time) { // http://stackoverflow.com/questions/3066586/get-string-in-yyyymmdd-format-from-js-date-object
var mm = this.getMonth() + 1; // getMonth() is zero-based
var dd = this.getDate();
if(mm<10)mm = "0"+mm
if(dd<10)dd = "0"+dd
console.log(this.getFullYear())
return this.getFullYear()+""+mm+""+dd+ ""; // padding
};
var date = new Date();
function replaceAll(target, search, replacement) {
return target.split(search).join(replacement);
};
function getImageFromName(name){
if(imagelinks[name]){
return imagelinks[name];
} else if(imagedata[name]) {
return "https://wiki.factorio.com/images/" + imagedata[name] + ".png";
} else if(factorioLocale["entity-name"] && factorioLocale["entity-name"][name]){
return "https://wiki.factorio.com/images/" + capitalizeFirstLetter(replaceAll(factorioLocale["entity-name"][name], ' ', '_').toLowerCase()) + ".png";
} else if(factorioLocale["item-name"] && factorioLocale["item-name"][name]){
return "https://wiki.factorio.com/images/" + capitalizeFirstLetter(replaceAll(factorioLocale["item-name"][name], ' ', '_').toLowerCase()) + ".png";
} else if(factorioLocale["fluid-name"] && factorioLocale["fluid-name"][name]){
return "https://wiki.factorio.com/images/" + capitalizeFirstLetter(replaceAll(factorioLocale["fluid-name"][name], ' ', '_').toLowerCase()) + ".png";
} else if(factorioLocale["equipment-name"] && factorioLocale["equipment-name"][name]){
return "https://wiki.factorio.com/images/" + capitalizeFirstLetter(replaceAll(factorioLocale["equipment-name"][name], ' ', '_').toLowerCase()) + ".png";
} else return "/pictures/unknown-item.png";
}
window.factorioLocale = {};
$.getJSON("/api/getFactorioLocale", (locale) => {
window.factorioLocale = locale;
});
// image data
// key is the name of the item in the database, value is the name of the image on wiki.factorio.com/images/*
// this list has to include all entities that doesn't just follow the simple capitalize first letter convention
/* Notice from bilka: All icon images hosted on this Wiki have been renamed to use their in-game names, with underscores. If your external tool references these icons, please correct your links. */
var imagedata = {
"put-combinator": "Decider_combinator",
"smart-train-stop": "Train_stop",
"raw-fish": "Fish",
"solar-panel-equipment": "Portable_solar_panel",
"diesel-locomotive": "Locomotive", // pre 0.15 loco name
"small-lamp": "Lamp",
"tree-02": "Green_tree",
"tree-02-red": "Green_tree",
"tree-03": "Green_tree",
"tree-04": "Green_tree",
"tree-05": "Green_tree",
"tree-07": "Green_tree",
"tree-09": "Green_tree",
"tree-09-brown": "Green_tree",
"tree-09-red": "Green_tree",
"dead-tree-desert": "Dead_tree_desert",
"sand-rock-big": "Rock_big",
// "biter-spawner" missing, but can't find png for it on wiki
// "spitter-spawner"
}
// value:link pairs where you can add modded item icons that are missing
var imagelinks = {
"crude-oil-barrel": "/pictures/Crude_oil_barrel.png",
"heavy-oil-barrel": "/pictures/Heavy_oil_barrel.png",
"light-oil-barrel": "/pictures/Light_oil_barrel.png",
"petroleum-gas-barrel": "/pictures/Petroleum_gas_barrel.png",
"sulfuric-acid-barrel": "/pictures/Sulfuric_acid_barrel.png",
"water-barrel": "/pictures/Water_barrel.png",
"lubricant-barrel": "/pictures/Lubricant_barrel.png",
"power-armor-mk2": "/pictures/Power_armor_MK2.png",
"fictional-modded-item": "/pictures/fictional-modded-item.png",
"raw-fish": "/pictures/Fish.png",
"rail": "/pictures/rail.png",
"effectivity-module-1": "/pictures/green_module_1.png",
"effectivity-module-2": "/pictures/green_module_2.png",
"effectivity-module-3": "/pictures/green_module_3.png",
"piercing-shotgun-shell": "/pictures/piercing_shotgun_shells.png",
"energy-shield-mk2-equipment": "/pictures/Energy_shield_MK2.png",
"battery-mk2-equipment": "/pictures/Battery_MK2.png",
"personal-roboport-mk2-equipment": "/pictures/Personal_roboport_MK2.png",
}
+58
View File
@@ -0,0 +1,58 @@
#header .subbar h6 {
display:inline;
font-weight:normal;
color: #BBB
}
#header {
background-color: #222;
color: #DDD;
font-family: 'Roboto', sans-serif;
padding-left:16px;
}
#displayBody {
margin-left: 16px;
}
#hero {
background-color: #444;
height:400px;
}
#hero .subtitle {
margin-left:8px;
}
#header h1 {
margin-left: 10px;
display:inline-block;
}
#header .subbar h6 {
margin-right: 16px;
}
#onlineIndicator {
background-color:red;
height:20px;
width:20px;
display:inline-block;
border-radius:16px;
}
.productionGraph {
background-color:black;
}
#leftHeroContainer {
float:left;
}
ul {
width:300px;
list-style:none;
max-height:400px;
min-height:100px;
margin:0;
overflow:auto;
padding:0;
text-indent:10px;
}
li{
line-height:25px;
}
li:nth-child(even){
background:#ccc;
}
+33
View File
@@ -0,0 +1,33 @@
<!doctype HTML>
<head>
<%- include("head.html") %>
<script src="lib/terminal.js"></script>
<script src="nodeDetails.js"></script>
<link rel="stylesheet" type="text/css" href="nodeDetails.css">
</head>
<body>
<%- include("sidebar.html") %>
<div id="body">
<div id="hero">
<h2>Slave details</h2>
<div id="display">
</div>
</div>
<div id="details">
</div>
<div id="terminal">
<div class="titleBar" style="display:none;">
<div class="title">
<p>Clusterio RCON shell</p>
</div>
<div class="titleButton close" onclick="minimizeTerminal();"></div>
<div class="titleButton maximize" onclick="maximizeTerminal();"></div>
<div class="titleButton minimize" onclick="minimizeTerminal();"></div>
</div>
<!-- JS generates a div called .Terminal that appears here with the display and typing things -->
</div>
</div>
</body>
+331
View File
@@ -0,0 +1,331 @@
// string, object, function(object)
function post(url, data, callback) {
console.log("POST " + url + JSON.stringify(data))
var xhr = new XMLHttpRequest();
xhr.open("POST", url, true);
xhr.setRequestHeader("Content-type", "application/json");
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
var json = JSON.parse(xhr.responseText);
callback(json);
}
}
xhr.send(JSON.stringify(data));
}
// function to sort arrays of objects after a keys value
function sortByKey(array, key) {
return array.sort(function(a, b) {
var x = Number(a[key]); var y = Number(b[key]);
return ((x < y) ? 1 : ((x > y) ? -1 : 0));
});
}
function getParameterByName(name, url) {
if (!url) url = window.location.href;
name = name.replace(/[\[\]]/g, "\\$&");
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
/// code below here
// declaring globals
var chartsByID = {};
// update the online indicator dot
setInterval(function(){
getJSON("/api/slaves", function(err, slaveData){
let indicator = document.querySelector("#onlineIndicator");
if(err){
// our request threw an error, most likely master is unavailable so we show yellow indicator
indicator.style.backgroundColor = "yellow";
indicator.title = "Master server unavailable";
// compare with Math.floor(x/100000) allows us to check if they are within 10s of each other
} else if(Math.floor(slaveData[getParameterByName("instanceID")].time/100000) == Math.floor(Date.now()/100000)){
indicator.style.backgroundColor = "green";
indicator.title = "Slave is online";
} else {
indicator.style.backgroundColor = "red";
indicator.title = "Slave is offline";
}
});
}, 1000);
populateSlaveInfo();
function populateSlaveInfo(){
let instanceID = getParameterByName("instanceID");
if(!instanceID){
throw "We need instanceID! Also, *funny joke* but it doesn't work after I standardized on calling it instanceID";
}
getJSON("/api/slaves", function(err, slaveData){
/*
{"-1530507550":{
"time":"1495475926375",
"rconPort":"8714",
"rconPassword":"hidden",
"serverPort":"14345",
"unique":"-1530507550",
"publicIP":"localhost",
"mods":[
{
"modName":"clusterio_1.0.0.zip",
"hash":"341feb6c60918c83f7f3a6a14a4a308aef18dda6"
},{
"modName":"clusterio_1.0.1.zip",
"hash":"aee7a4b67ece19bddf236312fe408a5d4b67248d"
},{
"modName":"clusterio_1.1.0.zip",
"hash":"18e2f7e402d716e3dd1c1458b7fab2d0bb29eccf"
}],
"playerCount":"0",
"instanceName":"t",
"mac":"44-85-00-D2-7E-D6"}
}
*/
let slave = slaveData[instanceID];
let HTML = "<div id='header'><div id='onlineIndicator'></div><h1>Name: " + slave.instanceName+"</h1>"
HTML += "<div class='subbar'><h6>Host: "+slave.mac+" </h6><h6>Unique: "+slave.unique+" </h6>"
if(slave.publicIP != "localhost"){
HTML += "<h6>IP: "+slave.publicIP+":"+slave.serverPort+"</h6>";
} else {
HTML += "<h6>This server is not configured for incoming connections</h6>";
}
HTML += "</div></div>" // end of header
// left container
HTML += "<div id='leftHeroContainer'>";
HTML += "<div id='displayBody'><p>Last seen: <span id='lastSeenDate'>"+moment(Number(slave.time)).fromNow()+"</span></p>";
HTML += "<p>Online players: "+slave.playerCount+"</p>";
// detect if remoteMap mod is installed, if it is we want to show the link for it
let hasRemoteMap = false;
slave.mods.forEach(mod => {
console.log(mod.modName)
if(mod.modName.includes("remoteMap")){
hasRemoteMap = true; // we are still doing the logic on the outside, in case there are multiple instances of remoteMap installed....
}
});
if(hasRemoteMap){
HTML += "<a href='/remoteMap?instanceID="+getParameterByName("instanceID")+"'>Remote map</a>";
}
HTML += "</div>" // end of displayBody
// list mods and other metadata
HTML += "<h2 class='subtitle'>Mods</h2><ul id='modlist'>"
for(let i = 0; i < slave.mods.length; i++){
HTML += "<li>"+slave.mods[i].modName+"</li>"
}
HTML += "</ul>"
HTML += "</div>" // end of left container
// chart
HTML += '<div id="' + slave.unique + '" class="productionGraph" style="width: calc(100% - 300px);"></div>';
// terminal
// HTML += '<div id="terminal"></div>';
document.querySelector("#hero").innerHTML = HTML;
// slaveDetails
HTML = "";
if(slave.meta)
for(let key in slave.meta){
let t = slave.meta[key];
if(typeof t == "string"){
HTML += "<p>"+key+": "+t+"</p>";
}
}
document.querySelector("#body > #details").innerHTML = HTML;
// make production graph
makeGraph(slave.unique, slave.unique)
// makeTerminal();
});
}
var slaveLogin = {};
function makeTerminal(){
myTerminal = new Terminal();
document.querySelector("#terminal").appendChild(myTerminal.html);
// use jQuery to allow the user to drag the window around
$("#terminal").draggable({
handle:".titleBar",
});
myTerminal.print('Welcome to Clusterio rcon!');
myTerminal.input('', handleTerminalInput);
if(localStorage.terminalMinimized && localStorage.terminalMinimized == "true"){
minimizeTerminal(true);
}
function print(string){
myTerminal.print(string);
}
function handleTerminalInput(inputString) {
// show clearly that it was a command that was typed, not terminal output
let lastLine = document.querySelector("#terminal div > p:nth-child(1) > div:last-child");
lastLine.innerHTML = "> " + lastLine.innerHTML;
argv = inputString.split(' ');
if(argv[0][0] == '/'){
if(slaveLogin && slaveLogin.name && slaveLogin.pass){
print('Running: '+inputString);
} else {
print('Not identified to communicate with any slave! Run "help" for more information.');
}
} else if(argv[0] == 'help'){
if(argv[1] == 'login'){
print('To send commands to a slave, you are required to identify yourself. You can do this with the login command. The name will be the name of the slave as displayed on master. The password is the rcon password of the slave, as per /instances/[name]/config.json');
} else if(argv[1] == "/c"){
print('To run a command, start with /. You can for example do /c game.print("hello world!")');
} else if(argv[1] == "issues"){
print("Issues are reported to Danielv123 on github or espernet IRC.");
print(" - http://github.com/Danielv123/factorioClusterio/issues");
print(" - EsperNet #factorio Danielv123");
} else {
print('To identify yourself to a slave, use "login [name] [password]"');
print(' - /c [command] - Run command');
print(' - login [name] [passs] - Connect to remote slave');
print(' - issues - Report an issue');
}
} else if(argv[0] == 'issues'){
print("Issues are reported to Danielv123 on github or espernet IRC.");
print(" - http://github.com/Danielv123/factorioClusterio/issues");
print(" - EsperNet #factorio Danielv123");
} else {
print('Invalid command!');
}
// keep handling input (unless something is seriously wrong)
myTerminal.input('',handleTerminalInput);
}
}
function minimizeTerminal(instant){
localStorage.terminalMinimized = true;
$('#terminal').draggable('disable');
if(!instant){
$('#terminal')[0].style.transition = 'left 0.2s linear, top 0.2s linear';
}
document.querySelector('.Terminal').style.height = 0;
let ter = $('#terminal')[0];
// if the window just spawned in and we are doing the first minimize, our position won't be set yet. We also don't want to save this blank position.
if(ter.style.left && ter.style.top){
localStorage.oldTerminalPosition = JSON.stringify({x:ter.style.left, y:ter.style.top});
}
ter.style.left = (window.innerWidth - ter.offsetWidth)+'px';
ter.style.top = (window.innerHeight - 25)+'px';
// if we did it instant we still want to set this property for after.
if(instant){
setTimeout(() => $('#terminal')[0].style.transition = 'left 0.2s linear, top 0.2s linear',200);
}
}
function maximizeTerminal(instant){
localStorage.terminalMinimized = false;
document.querySelector('.Terminal').style.height = '400px';
let ter = $('#terminal')[0];
// recover our old saved position
let oldTerminalPosition = JSON.parse(localStorage.oldTerminalPosition);
ter.style.left = oldTerminalPosition.x;
ter.style.top = oldTerminalPosition.y;
// remove our animation once its finished
// since instant is a true/false value, Number will convert it to 1 or 0. Since 0 is falsy, || will turn it into 200.
setTimeout(() => $('#terminal')[0].style.transition = 'none', (Number(instant) || 200));
$('#terminal').draggable('enable');
}
// ID of slave, ID of canvasjs div without #
function makeGraph(instanceID, selector) {
let chartIgnoreList = [
"water",
"steam"
]
post("api/getStats", {instanceID: instanceID}, function(data){
//console.log("Building chart " + instanceID + " with this data:")
//console.log(data)
if(data.length > 0) {
// find keys
let itemNames = [];
for(let key in data[data.length - 1].data) {
itemNames[itemNames.length] = key
}
let chartData = [];
for(let o = 0; o < itemNames.length; o++) {
if(!chartIgnoreList.includes(itemNames[o])){
chartData[chartData.length] = generateLineChartArray(data, itemNames[o]);
}
}
//console.log(chartData)
drawChart(selector, chartData)
}
// callback(data);
})
}
function generateLineChartArray(data, nameKey) {
let chartData = [];
for(let i = 0; i < data.length; i++) {
// only show recent data
if(data[i].timestamp > Date.now() - (24*60*60*1000)){
let y = data[i].data[nameKey]
if(!data[i].data[nameKey]) {
y = 0;
} else if(y < 0) {
y = 0;
}
chartData[chartData.length] = {
x: new Date(data[i].timestamp),
y: Number(y)
}
}
}
chartData = sortByKey(chartData, "x");
let xyz = {};
xyz.name = nameKey;
xyz.type = "line";
if(nameKey == "copper-wire"||nameKey == "iron-plate"||nameKey == "copper-plate"||nameKey == "electronic-circuit"||nameKey == "steel-plate"||nameKey == "advanced-circuit"||nameKey == "crude-oil"||nameKey == "petroleum-gas"){
xyz.showInLegend = true;
}
xyz.dataPoints = chartData;
return xyz;
}
function drawChart(selector, chartData, title) {
// selector is ID of element, ex "chartContainer" or "-123199123"
console.log(chartData)
chartsByID[selector] = new CanvasJS.Chart(selector, {
title:{
text: title || "Production graph"
},
toolTip:{
content: "{name}: {y}"
},
zoomEnabled: true,
axisY:{
includeZero: true,
},
legend: {
cursor: "pointer",
itemclick: function (e) {
//console.log("legend click: " + e.dataPointIndex);
//console.log(e);
if (typeof (e.dataSeries.visible) === "undefined" || e.dataSeries.visible) {
e.dataSeries.visible = false;
} else {
e.dataSeries.visible = true;
}
e.chart.render();
}
},
data: chartData
});
chart = chartsByID[selector];
// console.log(chart)
chart.render();
return chart;
}
+17
View File
@@ -0,0 +1,17 @@
<!doctype HTML>
<head>
<%- include("head.html") %>
<script src="slaveDisplay.js"></script>
</head>
<body>
<%- include("sidebar.html") %>
<div id="body">
<div id="slaves">
<h2>Slaves</h2>
<div id="display">
</div>
</div>
</div>
</body>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 968 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

+28
View File
@@ -0,0 +1,28 @@
<!doctype HTML>
<head>
<%- include("head.html") %>
<script src="external/socket.io.js"></script>
<script type="module" src="remoteMap.js"></script>
</head>
<body>
<%- include("sidebar.html") %>
<div id="body">
<h2>remoteMap</h2>
<div id="remoteMapContainer" style="float:left">
<canvas class="remoteMap" id="remoteMapBackground" width="512" height="512" style="border:1px solid #000000;position:absolute;"></canvas>
<canvas class="remoteMap" id="remoteMap" width="512" height="512" style="border:1px solid #000000;position:absolute;"></canvas>
<canvas class="remoteMap" id="remoteMapSelection" width="512" height="512" style="border:1px solid #000000;position:absolute;"></canvas>
</div>
<div id="remoteMapItemSelector" style="float:left">
<!-- Generated by script, don't edit -->
</div>
<div id="remoteMapSettings" style="float:left">
<p>Map size: <input type="number" id="mapSize" style="color:black;"> (default 32)</p>
<p>Tile size: <input type="number" id="tileSize" style="color:black;"> (default 8)</p>
<p>FPS: <input type="number" id="fpsCounter" style="color:black;"></p>
<p>Pause when window is deselected (saves CPU) <input type="checkbox" id="pauseOnBlur"></p>
<p>Movement speed: <input type="number" id="movementSpeed" style="color:black;"> (default 0.2)</p>
</div>
</div>
</body>
Loaded 100 of 108 files, more files were not shown because too many files have changed in this diff. Show more