Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e9ca14e3e | ||
|
|
8fd140f767 |
No files matched your search
@@ -1,54 +0,0 @@
|
||||
# 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
|
||||
saves
|
||||
|
||||
# weird pattern to ignore sharedPlugins except not ignore some core modules
|
||||
!sharedPlugins/
|
||||
sharedPlugins/*
|
||||
!sharedPlugins/inventoryImports/
|
||||
!sharedPlugins/UPSdisplay/
|
||||
!sharedPlugins/remoteMap/
|
||||
!sharedPlugins/remoteCommands/
|
||||
!sharedPlugins/researchSync/
|
||||
!sharedPlugins/globalChat/
|
||||
|
||||
\.cache-require-paths\.json
|
||||
@@ -1,10 +0,0 @@
|
||||
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
|
||||
@@ -1,19 +0,0 @@
|
||||
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
|
||||
@@ -1,21 +0,0 @@
|
||||
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.
|
||||
@@ -1,209 +1,18 @@
|
||||
# factorioClusterio
|
||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2FDanielv123%2FfactorioClusterio?ref=badge_shield)
|
||||
# Plugin page layout screenshots
|
||||
|
||||
| Branch | Coverage | Build |
|
||||
|--------|----------| ----- |
|
||||
| Master | [](https://codecov.io/gh/Danielv123/factorioClusterio) | [](https://travis-ci.org/Danielv123/factorioClusterio) |
|
||||
| Dev | [](https://codecov.io/gh/Danielv123/factorioClusterio) | [](https://travis-ci.org/Danielv123/factorioClusterio) |
|
||||
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.
|
||||
|
||||
If you want to connect to a clusterio cluster, please reffer to the [client](https://github.com/Danielv123/factorioClusterioClient)
|
||||
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.
|
||||
|
||||
### Table of contents
|
||||
The screenshots are kept on a separate evidence branch so they are not part of the code PR's diff.
|
||||
|
||||
* [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:
|
||||
|
||||

|
||||
|
||||
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
|
||||
curl -o factorio.tar.gz -L https://www.factorio.com/get-download/latest/headless/linux64
|
||||
tar -xf factorio.tar.gz
|
||||
npm install
|
||||
node ./lib/npmPostinstall.js
|
||||
|
||||
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.
|
||||
|
||||
*Copy config.json.dist to config.json* - Otherwise it will crash, as you'd expect.
|
||||
|
||||
**Master**
|
||||
|
||||
pm2 start master.js --name master
|
||||
|
||||
**Server Host**
|
||||
|
||||
To download the mod for all its non vanilla features and items, (optional, but very recommended)
|
||||
|
||||
node client.js manage shared mods add clusterio
|
||||
|
||||
Add the master API key, found in the masters install directory (secret-api-token.txt) to your config.json masterAuthToken field
|
||||
|
||||
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 [instancename] 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 8 or 9 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. Copy config.json.dist to config.json
|
||||
|
||||
3. Follow the instructions in the bat file
|
||||
|
||||
4. Run `node client.js manage shared mods add clusterio`
|
||||
|
||||
5. type `node master.js` to start the server
|
||||
|
||||
**Server Host**
|
||||
|
||||
1. download and run https://puu.sh/toFHl/01eebbb333.bat
|
||||
|
||||
2. Copy config.json.dist to config.json
|
||||
|
||||
3. Follow the instructions given
|
||||
|
||||
4. 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)
|
||||
e war
|
||||
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, but is really old so be warned: [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 manage shared mods add clusterio
|
||||
```
|
||||
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.
|
||||
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,14 @@
|
||||
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();
|
||||
@@ -0,0 +1,57 @@
|
||||
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();
|
||||
@@ -1,999 +0,0 @@
|
||||
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('rcon-client').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(config.instanceDirectory)) {
|
||||
fs.mkdirSync(config.instanceDirectory);
|
||||
}
|
||||
if (!fs.existsSync("./sharedPlugins/")) {
|
||||
fs.mkdirSync("sharedPlugins");
|
||||
}
|
||||
if (!fs.existsSync("./sharedMods/")) {
|
||||
fs.mkdirSync("sharedMods");
|
||||
}
|
||||
const instance = process.argv[3];
|
||||
const instancedirectory = config.instanceDirectory + '/' + 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;
|
||||
|
||||
// make sure we have the master access token (can't write to master without it since clusterio 2.0)
|
||||
if(!config.masterAuthToken || typeof config.masterAuthToken !== "string"){
|
||||
console.log("ERROR invalid config!");
|
||||
console.log("Master server now needs an access token for write operations. As clusterio slaves depends \
|
||||
upon this, please add your token to config.js in the field named masterAuthToken. \
|
||||
You can retrieve your auth token from the master in secret-api-token.txt after running it once.");
|
||||
}
|
||||
const needleOptionsWithTokenAuthHeader = {
|
||||
headers: {
|
||||
'x-access-token': config.masterAuthToken
|
||||
},
|
||||
};
|
||||
|
||||
var instanceInfo = {};
|
||||
var commandBuffer=[];
|
||||
// messageInterface Management
|
||||
setInterval(function(){
|
||||
let command=commandBuffer.shift();
|
||||
if(command){
|
||||
messageInterfaceInternal(command[0], command[1], command[2], command[3]);
|
||||
}
|
||||
},config.msBetweenCommands || 50);
|
||||
|
||||
// function to handle sending commands into the game
|
||||
async function messageInterfaceInternal(command, callback, resolve, reject) {
|
||||
// try to save us if you send a buffer instead of string
|
||||
if(typeof command == "object") {
|
||||
command = command.toString('utf8');
|
||||
}
|
||||
|
||||
if(false && 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();
|
||||
}
|
||||
resolve();
|
||||
} else if(typeof command == "string" && client && client.send && typeof client.send == "function") {
|
||||
try {
|
||||
let str = await client.send(command+"\n");
|
||||
if(typeof callback == "function") callback(str)
|
||||
resolve(str)
|
||||
} catch (err) {
|
||||
console.log("RCON failed, server might be paused or shutting down");
|
||||
// console.log(err);
|
||||
// reject(err);
|
||||
if(typeof callback == "function"){
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function messageInterface(command, callback) {
|
||||
return new Promise((resolve,reject) => {
|
||||
commandBuffer.push([command,callback, resolve, reject]);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 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(config.instanceDirectory);
|
||||
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(config.instanceDirectory+"/"+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(0);
|
||||
} 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"){
|
||||
(async function(){try{
|
||||
// allow managing mods
|
||||
if(action == "list"){
|
||||
console.log(await modManager.listMods(instance));
|
||||
} else if(action == "search"){
|
||||
console.log(await modManager.findMods(process.argv[6]));
|
||||
} else if(action == "add" || action == "download"){
|
||||
await modManager.addMod(process.argv[6], instance);
|
||||
} else if(action == "remove" || action == "rm" || action == "delete"){
|
||||
await modManager.removeMod(process.argv[6], instance);
|
||||
} else if(action == "update"){
|
||||
await modManager.updateAllMods();
|
||||
} else {
|
||||
usage(instance, tool);
|
||||
}
|
||||
process.exit(0);
|
||||
}catch(e){
|
||||
console.log("Got error from modManager:")
|
||||
console.log(e);
|
||||
}})();
|
||||
} 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(0);
|
||||
} 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(config.instanceDirectory+"/" + process.argv[3]) && process.argv[3] != "/" && process.argv[3] != "") {
|
||||
fileOps.deleteFolderRecursiveSync(config.instanceDirectory+"/" + process.argv[3]);
|
||||
console.log("Deleted instance " + process.argv[3]);
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.error("Instance not found: " + process.argv[3]);
|
||||
process.exit(0);
|
||||
}
|
||||
} 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);
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
} 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.resolve(config.factorioDirectory, "data") }\r\n
|
||||
write-data=${ path.resolve(config.instanceDirectory, 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!");
|
||||
process.exit(0);
|
||||
} else if (command == "start" && typeof instance == "string" && instance != "/" && fs.existsSync(instancedirectory)) {
|
||||
// Exit if no instance specified (it should be, just a safeguard);
|
||||
if(instancedirectory != config.instanceDirectory+"/undefined"){
|
||||
var instanceconfig = require(path.resolve(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, sending /quit");
|
||||
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.error("ERROR!")
|
||||
console.error("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/\n");
|
||||
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();
|
||||
}
|
||||
// we have to do this to make logs visible on linux and in powershell. Causes log duplication for people with CMD.
|
||||
console.log('Fact: ' + data);
|
||||
});
|
||||
serverprocess.stderr.on('data', (chunk) => {
|
||||
console.log('ERR: ' + chunk);
|
||||
});
|
||||
|
||||
// connect to the server with rcon
|
||||
if(true || process.platform != "linux"){
|
||||
// IP, port, password
|
||||
client = new Rcon();
|
||||
|
||||
// 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({
|
||||
host: 'localhost',
|
||||
port: Number(process.env.RCONPORT) || instanceconfig.clientPort,
|
||||
password: instanceconfig.clientPassword,
|
||||
timeout: 5000
|
||||
});
|
||||
} else {
|
||||
setTimeout(function(){
|
||||
checkRcon();
|
||||
},5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
setTimeout(checkRcon, 5000);
|
||||
|
||||
client.onDidAuthenticate(() => {
|
||||
console.log('Clusterio | RCON Authenticated!');
|
||||
instanceManagement(); // start using rcons
|
||||
});
|
||||
client.onDidConnect(() => {
|
||||
console.log('Clusterio | RCON Connected!');
|
||||
// getID();
|
||||
});
|
||||
client.onDidDisconnect(() => {
|
||||
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"){
|
||||
// don't open an RCON connection and just use stdio instead, does not work on windows.
|
||||
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, async function(data, callback){
|
||||
if(data.toString('utf8')[0] != "/") {
|
||||
log("Stdout: " + data.toString('utf8'));
|
||||
return true;
|
||||
} else {
|
||||
return messageInterface(data.toString('utf8'), callback);
|
||||
}
|
||||
}, { // 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(plugins[I].factorioOutput && typeof plugins[I].factorioOutput === "function"){
|
||||
// when factorio logs a line, send it to the plugin. This includes things like autosaves, chat, errors etc
|
||||
serverprocess.stdout.on("data", data => plugins[I].factorioOutput(data.toString()));
|
||||
}
|
||||
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')) {
|
||||
try{
|
||||
plugins[I].scriptOutput(stuff[i]);
|
||||
}catch(e){console.log(e)}
|
||||
}
|
||||
}
|
||||
},
|
||||
pluginConfig.fileReadDelay || 0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log("Clusterio | Loaded plugin " + pluginDirectories[i]);
|
||||
} else if(pluginConfig.binary != "nodePackage" && pluginConfig.binary){
|
||||
// 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}`);
|
||||
});
|
||||
} else {
|
||||
// This plugin doesn't have a client portion
|
||||
}
|
||||
}
|
||||
|
||||
// world IDs ------------------------------------------------------------------
|
||||
hashMods(instance, function(modHashes){
|
||||
setInterval(getID, 10000);
|
||||
getID();
|
||||
function getID() {
|
||||
messageInterface("/silent-command rcon.print(#game.connected_players)", function(playerCount) {
|
||||
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,
|
||||
instanceName: instance,
|
||||
playerCount:playerCount.replace(/(\r\n\t|\n|\r\t)/gm, ""),
|
||||
}
|
||||
|
||||
function callback(err, mac) {
|
||||
if (err) {
|
||||
mac = "unknown";
|
||||
console.log("##### getMac crashed, but we don't really give a shit because we are probably closing down #####");
|
||||
}
|
||||
payload.mac = mac;
|
||||
console.log("Registered our precense with master "+config.masterIP+" at " + payload.time);
|
||||
needle.post(config.masterIP + ":" + config.masterPort + '/api/getID', payload, needleOptionsWithTokenAuthHeader, 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
// 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, needleOptionsWithTokenAuthHeader, 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 "+modHashes[i].modName);
|
||||
} 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({url: "http://"+config.masterIP + ":" + config.masterPort + '/api/uploadMod',
|
||||
headers: {
|
||||
"x-access-token": config.masterAuthToken,
|
||||
},
|
||||
}, 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(config.instanceDirectory+"/"+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}, needleOptionsWithTokenAuthHeader, 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
|
||||
}, needleOptionsWithTokenAuthHeader, 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]], needleOptionsWithTokenAuthHeader, function (err, response, body) {
|
||||
if (response && response.body && typeof response.body == "object") {
|
||||
// buffer confirmed orders
|
||||
confirmedOrders[confirmedOrders.length] = {name:response.body.name,count: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));
|
||||
//if (!(confirmedOrders.length>0)){return;}
|
||||
let cmd="local t={";
|
||||
for(let i=0;i<confirmedOrders.length;i++)
|
||||
{
|
||||
cmd+='["'+confirmedOrders[i].name+'"]='+confirmedOrders[i].count+',';
|
||||
if(cmd.length>320) // Factorio max packet size is 508
|
||||
{
|
||||
messageInterface("/silent-command remote.call('clusterio', 'runcode', '"+cmd.slice(0, -1)+"}"+ " for k, item in pairs(t) do GiveItemsToStorage(k, item) end')");
|
||||
cmd="local t={";
|
||||
}
|
||||
}
|
||||
if (!(cmd==="local t={")){
|
||||
messageInterface("/silent-command remote.call('clusterio', 'runcode', '"+cmd.slice(0, -1)+"}"+ " for k, item in pairs(t) do GiveItemsToStorage(k, item) end')");
|
||||
}
|
||||
confirmedOrders=[];
|
||||
}
|
||||
}, 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);
|
||||
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));
|
||||
let first = true;
|
||||
let cmd="local s={";
|
||||
for (let key in inventoryFrame)
|
||||
{
|
||||
cmd+='["'+key+'"]='+inventoryFrame[key]+",";
|
||||
if(first && cmd.length>300 || !first && cmd.length > 320) // Factorio max packet size is 508
|
||||
{
|
||||
messageInterface("/silent-command remote.call('clusterio', 'runcode', '"+(first ? 'global.ticksSinceMasterPinged=0 ':'')+cmd.slice(0, -1)+"}"+ " for name,count in pairs(s) do global.invdata[name]=count end')");
|
||||
cmd="local s={";
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
if (!(cmd==="local s={")){
|
||||
messageInterface("/silent-command remote.call('clusterio', 'runcode', '"+(first ? 'global.ticksSinceMasterPinged=0 ':'')+cmd.slice(0, -1)+"}"+ " for name,count in pairs(s) do global.invdata[name]=count end')");
|
||||
}
|
||||
messageInterface("/silent-command remote.call('clusterio', 'runcode', 'UpdateInvCombinators()')");
|
||||
} catch (e){
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 1550);
|
||||
// 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(config.instanceDirectory+"/"+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 = config.instanceDirectory+"/"+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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
{
|
||||
"__comment": "Where factorio folder. Should be in the same folder as this config file.",
|
||||
"factorioDirectory": "factorio",
|
||||
"instanceDirectory": "instances",
|
||||
|
||||
"__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",
|
||||
|
||||
"__commentMasterPort": "HTTP port for the master to listen to, set to 0 or an empty string to not listen for HTTP requests",
|
||||
"masterPort": 8080,
|
||||
|
||||
"__commentsslPort": "HTTPS port for the master to listen to. Set to 0 or an empty string to not use HTTPS.",
|
||||
"sslPort": 443,
|
||||
"sslCert": "database/certificates/cert.crt",
|
||||
"sslPrivKey": "database/certificates/cert.key",
|
||||
|
||||
"__commentMasterAuthToken": "Authentication token to connect to the master server. Find it in secret-api-token.txt on the master.",
|
||||
"masterAuthToken": "",
|
||||
|
||||
"__commentAuthSecret": "This is a long random string used for generating authentication tokens. Do not share. Regenerates when secret-api-token.txt is deleted.",
|
||||
"masterAuthSecret": "",
|
||||
|
||||
"__comment3": "Your factorio.com username for factorio MP services, also username will be admin on new instances",
|
||||
"username": "",
|
||||
"__comment4": "Token from your factorio.com profile page. Required for MP and mod manager services.",
|
||||
"token": "",
|
||||
"__comment7": "Used for communication with the mod portal, not needed if you manage mods by hand",
|
||||
"factorio_version": "0.16.41",
|
||||
|
||||
"__comment5": "General user settings",
|
||||
"verify_user_identity": true,
|
||||
"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,
|
||||
"msBetweenCommands": 10,
|
||||
"allowRemoteCommandExecution": true
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
toc:
|
||||
- name: Master Server API
|
||||
- clusterioMaster
|
||||
- name: Type declarations
|
||||
- itemStack
|
||||
- name: Internal modules
|
||||
description: |
|
||||
Here are some helper functions for navigation.
|
||||
- modManager
|
||||
- getFactorioLocale
|
||||
@@ -0,0 +1,2 @@
|
||||
<!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>
|
||||
@@ -1,15 +0,0 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const config = require("./../config");
|
||||
|
||||
var middleware = function (req, res, next) {
|
||||
var token = req.headers['x-access-token'];
|
||||
if(!token) return res.status(401).send({ auth: false, message: 'No token provided.', endpoint: req.route.path});
|
||||
|
||||
jwt.verify(token, config.masterAuthSecret, function(err, decoded) {
|
||||
if(err) return res.status(500).send({ auth: false, message: 'Failed to authenticate token.', endpoint: req.route.path});
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {middleware};
|
||||
@@ -1,49 +0,0 @@
|
||||
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");
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
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);
|
||||
});
|
||||
@@ -1,53 +0,0 @@
|
||||
/**
|
||||
Module for grabbing factorio locale from a local factorio install
|
||||
@module getFactorioLocale
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
const ini = require("ini");
|
||||
|
||||
const cache = {};
|
||||
|
||||
/**
|
||||
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 new Error("Error: wrong parameters provided");
|
||||
}
|
||||
|
||||
if(!cache[languageCode]){
|
||||
getLocale(factorioDirectory, languageCode, (err, rawLocale) => {
|
||||
if(err){
|
||||
callback(err);
|
||||
} else {
|
||||
let processedLocale = ini.parse(rawLocale);
|
||||
cache[languageCode] = processedLocale;
|
||||
callback(undefined, processedLocale);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
callback(undefined, cache[languageCode]);
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
const fileOps = require("_app/fileOps");
|
||||
const config = require("./../config");
|
||||
|
||||
module.exports = {
|
||||
sync: function(){
|
||||
let instances;
|
||||
try {
|
||||
instances = fileOps.getDirectoriesSync(config.instanceDirectory);
|
||||
} catch (e) {
|
||||
// there are no instances created yet, probably missing the folder
|
||||
instances = [];
|
||||
}
|
||||
return instances;
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/**
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,273 +0,0 @@
|
||||
/**
|
||||
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 query = require('cli-interact').getYesNo;
|
||||
|
||||
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){
|
||||
return new Promise((resolve, reject) => {
|
||||
if(instance && instances.includes(instance)){
|
||||
factorioAPI.init(false, config.instanceDirectory+'/'+instance+'/instanceMods', config.instanceDirectory+'/'+instance+'/saves', config.factorio_version);
|
||||
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");
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
// list shared mods in /sharedMods/
|
||||
factorioAPI.init(false, 'sharedMods/', '', config.factorio_version);
|
||||
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));
|
||||
resolve();
|
||||
});
|
||||
}).catch(e => {
|
||||
reject(new Error(e));
|
||||
});
|
||||
}
|
||||
|
||||
// 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){
|
||||
return new Promise((resolve, reject) => {
|
||||
factorioAPI.init(false);
|
||||
factorioAPI.authenticate({
|
||||
username: config.username,
|
||||
token: config.token,
|
||||
require_ownership: false
|
||||
}).then(token => {
|
||||
factorioAPI.searchMods(searchTerm).then(mods => {
|
||||
let result = 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"),
|
||||
latest_version: mod.latest_release.version,
|
||||
owner:mod.owner,
|
||||
//license: mod.license_name,
|
||||
}
|
||||
}));
|
||||
resolve(result);
|
||||
// console.log(result);
|
||||
});
|
||||
}).catch(e => {
|
||||
if(e.contains("Insufficient information")) console.log("We are missing some parameters required for logging in! Make sure your factorio username and token are specified in config.json.");
|
||||
reject(new Error(e));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports.addMod = function(term, instance){
|
||||
return new Promise((resolve, reject) => {
|
||||
if(instance == "shared"){ // modDirectory has to end in /
|
||||
var modDirectory = "sharedMods/";
|
||||
} else {
|
||||
var modDirectory = config.instanceDirectory+"/"+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, "", config.factorio_version);
|
||||
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 + " in version: " + body[0].latest_release.version);
|
||||
factorioAPI.downloadMod( {name: body[0].name, version: body[0].latest_release.version}).then(()=>{
|
||||
console.log("Mod downloaded");
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
} 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
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// treat it as a search and download the top result
|
||||
factorioAPI.init(false, modDirectory, "", config.factorio_version);
|
||||
factorioAPI.authenticate({
|
||||
username: config.username,
|
||||
token: config.token,
|
||||
require_ownership: false
|
||||
}).then(() => {
|
||||
factorioAPI.searchMods(term).then(body => {
|
||||
console.log(body)
|
||||
console.log("Im gonna download: "+body[0].name + " in version: " + body[0].latest_release.version);
|
||||
factorioAPI.downloadMod( {name: body[0].name, version: body[0].latest_release.version}).then(()=>{
|
||||
console.log("Mod downloaded");
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
reject(new Error("Invalid search term"));
|
||||
}
|
||||
});
|
||||
}
|
||||
module.exports.removeMod = function removeMod(term, instance, force = false){
|
||||
return new Promise((resolve, reject) => {
|
||||
if(instance == "shared"){ // modDirectory has to end in /
|
||||
var modDirectory = "sharedMods/";
|
||||
} else {
|
||||
var modDirectory = config.instanceDirectory+"/"+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");
|
||||
modsMatchingFilter.forEach(mod => console.log(mod));
|
||||
if(query("Delete all mods matching filter?")){
|
||||
let modsToDelete = modsMatchingFilter.length;
|
||||
for(let i = 0; i < modsToDelete; i++){
|
||||
fs.unlinkSync(modDirectory+modsMatchingFilter[i]);
|
||||
console.log("Deleted "+modsMatchingFilter[i]);
|
||||
}
|
||||
} else console.log("Did not delete mods.")
|
||||
}
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error("Invalid search term"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// '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()))
|
||||
}*/
|
||||
@@ -1,136 +0,0 @@
|
||||
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,
|
||||
});
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
/**
|
||||
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;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
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,
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
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")
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
/**
|
||||
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}
|
||||
*/
|
||||
@@ -0,0 +1,63 @@
|
||||
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>);
|
||||
@@ -1,184 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,168 +0,0 @@
|
||||
/**
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
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;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,120 +0,0 @@
|
||||
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,
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
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;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,2 +0,0 @@
|
||||
const fileOps = require("./fileOps");
|
||||
module.exports = fileOps;
|
||||
@@ -1 +0,0 @@
|
||||
module.exports = require("./isFactorioCommand.js");
|
||||
@@ -1,23 +0,0 @@
|
||||
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;
|
||||
@@ -1,18 +0,0 @@
|
||||
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?")
|
||||
});
|
||||
});
|
||||
@@ -1,77 +0,0 @@
|
||||
{
|
||||
"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",
|
||||
"base64url": "^3.0.0",
|
||||
"bcrypt": "^2.0.1",
|
||||
"bcrypt-promise": "^2.0.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"body-parser": "^1.18.2",
|
||||
"cache-require-paths": "^0.3.0",
|
||||
"cli-interact": "^0.1.9",
|
||||
"codecov": "^3.0.1",
|
||||
"compression": "^1.7.2",
|
||||
"deepmerge": "^1.5.2",
|
||||
"ejs": "^2.5.7",
|
||||
"express": "^4.16.2",
|
||||
"express-fileupload": "0.0.6",
|
||||
"follow-redirects": "^1.5.0",
|
||||
"fs-extra": "^6.0.1",
|
||||
"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",
|
||||
"rcon-client": "^3.0.2",
|
||||
"request": "^2.83.0",
|
||||
"right-pad": "^1.0.1",
|
||||
"rmdir-sync": "^1.0.1",
|
||||
"sanitizer": "^0.1.3",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
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");
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
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();
|
||||
@@ -1,17 +0,0 @@
|
||||
/*
|
||||
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",
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
const needle = require("needle");
|
||||
|
||||
|
||||
|
||||
class UPSdisplay {
|
||||
constructor(slaveConfig, msgInterface, extras){
|
||||
this.config = slaveConfig;
|
||||
this.messageInterface = msgInterface;
|
||||
this.socket = extras.socket;
|
||||
|
||||
this.historicalTicks = [];
|
||||
setInterval(() => {
|
||||
this.messageInterface("/silent-command rcon.print(game.tick)", data => {
|
||||
this.tick = Number(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);
|
||||
}
|
||||
});
|
||||
},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, tick:this.tick}}, {headers: {'x-access-token': this.config.masterAuthToken}}, function(err, resp) {
|
||||
// success?
|
||||
});
|
||||
} catch (err){
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
}, 5000);
|
||||
this.messageInterface("/silent-command game.print('UPSdisplay enabled')");
|
||||
}
|
||||
}
|
||||
module.exports = UPSdisplay;
|
||||
@@ -1,10 +0,0 @@
|
||||
/*
|
||||
Clusterio plugin to allow for chat between instances.
|
||||
*/
|
||||
module.exports = {
|
||||
// Name of package. For display somewhere I guess.
|
||||
name: "globalChat",
|
||||
version: "1.0.0",
|
||||
binary: "nodePackage",
|
||||
description: "Clusterio plugin to allow for chat between instances.",
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
const needle = require("needle");
|
||||
|
||||
module.exports = class remoteCommands {
|
||||
constructor(mergedConfig, messageInterface, extras){
|
||||
this.messageInterface = messageInterface;
|
||||
this.config = mergedConfig;
|
||||
this.socket = extras.socket;
|
||||
this.instances = {};
|
||||
|
||||
this.socket.on("hello", () => this.socket.emit("registerChatReciever"));
|
||||
|
||||
this.socket.on("gameChat", async data => {
|
||||
if(data.instanceID && data.instanceID != this.config.unique.toString()){
|
||||
if(data.data.includes("[CHAT]") && (data.data.toLowerCase().includes("/shout") || data.data.toLowerCase().includes("!shout"))){
|
||||
let words = data.data.trim().split(" ");
|
||||
words.shift();
|
||||
words.shift();
|
||||
words.shift();
|
||||
let message = words.join(" ").replace(/[";]/g, " ").replace(/[']/g,"").replace("/shout ", "").replace("!shout", "");
|
||||
|
||||
let instance = await this.getInstanceName(data.instanceID);
|
||||
|
||||
this.messageInterface("/silent-command game.print('[" + instance + "] " + message + "')");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
async factorioOutput(data){
|
||||
try{
|
||||
this.socket.emit("gameChat", {
|
||||
instanceID: this.config.unique.toString(),
|
||||
data,
|
||||
});
|
||||
if(data.includes("[CHAT]") && data.includes("!info")){
|
||||
let infoMessage = [
|
||||
"=== Server info ===",
|
||||
"Name: "+await this.getInstanceName(this.config.unique)+" ("+this.config.unique+")",
|
||||
];
|
||||
infoMessage.forEach((line, i) => {
|
||||
setTimeout(()=>this.messageInterface("/silent-command game.print('"+line+"')"),i*200);
|
||||
});
|
||||
}
|
||||
}catch(e){console.log(e)}
|
||||
}
|
||||
getInstanceName(instanceID){
|
||||
return new Promise((resolve, reject) => {
|
||||
let instance = this.instances[instanceID];
|
||||
if(!instance){
|
||||
needle.get(this.config.masterIP+":"+this.config.masterPort+ '/api/slaves', (err, response) => {
|
||||
if(err || response.statusCode != 200) {
|
||||
console.log("Unable to get JSON master/api/slaves, master might be unaccessible");
|
||||
} else if (response && response.body) {
|
||||
if(Buffer.isBuffer(response.body)) {console.log(response.body.toString("utf-8")); throw new Error();}
|
||||
try {
|
||||
for (let index in response.body)
|
||||
this.instances[index] = response.body[index].instanceName;
|
||||
} catch (e){
|
||||
console.log(e);
|
||||
return null;
|
||||
}
|
||||
instance = this.instances[instanceID]
|
||||
if (!instance) instance = instanceID; //somehow the master doesn't know the instance
|
||||
resolve(instance);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve(instance);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
/*
|
||||
|
||||
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",
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
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;
|
||||
@@ -1,157 +0,0 @@
|
||||
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,headers: {'x-access-token': config.masterAuthToken}}, 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], {headers: {'x-access-token': config.masterAuthToken}}, 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,
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
@@ -1,180 +0,0 @@
|
||||
-- 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
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
/*
|
||||
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.",
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
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?
|
||||
*/
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
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/");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
/*
|
||||
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",
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
-- 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
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
/c
|
||||
local toDelete = game.surfaces[1].find_entities({{3,7},{4,8}})
|
||||
for i, entity in pairs(toDelete) do
|
||||
entity.die()
|
||||
end
|
||||
@@ -1,130 +0,0 @@
|
||||
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;
|
||||
@@ -1,9 +0,0 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
|
||||
{
|
||||
"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?"
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
/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)
|
||||
@@ -1,16 +0,0 @@
|
||||
/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
|
||||
@@ -1,129 +0,0 @@
|
||||
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}
|
||||
}, {headers: {'x-access-token': this.config.masterAuthToken}}, 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;
|
||||
@@ -1,107 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
<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">
|
||||
@@ -1,23 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,355 +0,0 @@
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,185 +0,0 @@
|
||||
/*! 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
|
||||
}())
|
||||
@@ -1,53 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
|
||||
(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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let cachedInstanceNames = {};
|
||||
async function getInstanceName(instanceID){
|
||||
let instance = cachedInstanceNames[instanceID];
|
||||
if(!instance){
|
||||
let slaves = await getJSON("/api/slaves");
|
||||
try {
|
||||
for(id in slaves){
|
||||
cachedInstanceNames[id] = slaves[id].instanceName;
|
||||
}
|
||||
} catch(e){
|
||||
console.log("Error while looking for instance name!");
|
||||
console.log(e);
|
||||
}
|
||||
return cachedInstanceNames[instanceID] || instanceID;
|
||||
} else {
|
||||
return cachedInstanceNames[instanceID];
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// get queryString parameters
|
||||
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, " "));
|
||||
}
|
||||
|
||||
// callback(err, json)
|
||||
function getJSON(url, callback) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', url, true);
|
||||
xhr.responseType = 'json';
|
||||
xhr.onload = function() {
|
||||
var status = xhr.status;
|
||||
if (status == 200) {
|
||||
if(callback) callback(null, xhr.response);
|
||||
resolve(xhr.response);
|
||||
} else {
|
||||
if(callback) callback(status);
|
||||
reject(status);
|
||||
}
|
||||
};
|
||||
// triggers if connection is refused
|
||||
xhr.onerror = function(e){
|
||||
if(callback) callback(e);
|
||||
reject(e);
|
||||
};
|
||||
xhr.send();
|
||||
});
|
||||
};
|
||||
// callback(err, json)
|
||||
function postJSON(url, data, callback) {
|
||||
return new Promise((resolve, reject) => {
|
||||
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) {
|
||||
if(callback) callback(null, xhr.response);
|
||||
resolve(xhr.response);
|
||||
} else {
|
||||
if(callback) callback(status);
|
||||
reject(status);
|
||||
}
|
||||
};
|
||||
// triggers if connection is refused
|
||||
xhr.onerror = function(e){
|
||||
if(callback) callback(e);
|
||||
reject(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",
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,331 +0,0 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<!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>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 968 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |