mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-16 07:24:53 +09:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e9ca14e3e | ||
|
|
8fd140f767 |
No files matched your search
-67
@@ -1,67 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directories used by tools like nyc and codecov
|
||||
coverage
|
||||
.nyc_output
|
||||
|
||||
# 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
|
||||
dist/
|
||||
|
||||
node_modules/*
|
||||
|
||||
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/
|
||||
!sharedPlugins/clusterioMod/
|
||||
!sharedPlugins/serverManager/
|
||||
sharedPlugins/serverManager/database/*
|
||||
sharedPlugins/serverManager/config.json
|
||||
sharedPlugins/serverManager/secret-api-token.txt
|
||||
!sharedPlugins/scenario_loader/
|
||||
sharedPlugins/scenario_loader/scenarios
|
||||
!sharedPlugins/statisticsExporter/
|
||||
|
||||
# File used for marking plugins as disabled
|
||||
sharedPlugins/*/DISABLED
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "9"
|
||||
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,3 +0,0 @@
|
||||
{
|
||||
"CurrentProjectSetting": null
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"ExpandedNodes": [
|
||||
"",
|
||||
"\\sharedPlugins"
|
||||
],
|
||||
"SelectedNode": "\\.gitignore",
|
||||
"PreviewInSolutionExplorer": false
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
Binary file not shown.
@@ -1,41 +0,0 @@
|
||||
const needle = require("needle");
|
||||
const cli = require("cli");
|
||||
const config = require("./../config.json");
|
||||
|
||||
let options = cli.parse()
|
||||
cli.setUsage(`node cluster_rcon.js /c game.player.force = "player"`)
|
||||
if(process.argv.length < 3){
|
||||
cli.getUsage()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const masterIP = `${config.masterIP}:${config.masterPort}`;
|
||||
const token = config.masterAuthToken
|
||||
let commandArray = process.argv;
|
||||
commandArray.shift();
|
||||
commandArray.shift();
|
||||
const needleOptionsWithTokenAuthHeader = {
|
||||
headers: {
|
||||
'x-access-token': token,
|
||||
},
|
||||
};
|
||||
|
||||
let command = commandArray.join(" ");
|
||||
let killBiters = `/silent-command local surface=game.surfaces[1] for key, entity in pairs(surface.find_entities_filtered({force="enemy"})) do entity.destroy() end`;
|
||||
|
||||
(async ()=>{
|
||||
needle.post(masterIP+"/api/runCommand", {
|
||||
broadcast:true,
|
||||
command,
|
||||
}, needleOptionsWithTokenAuthHeader, (err, resp) => {
|
||||
console.log(resp.body)
|
||||
});
|
||||
// setInterval(()=>{
|
||||
// needle.post(masterIP+"/api/runCommand", {
|
||||
// broadcast:true,
|
||||
// command: killBiters,
|
||||
// }, needleOptionsWithTokenAuthHeader, (err, resp) => {
|
||||
// console.log(resp.body)
|
||||
// });
|
||||
// }, 1000*60*5);
|
||||
})();
|
||||
@@ -1,19 +0,0 @@
|
||||
const needle = require("needle")
|
||||
const config = require("./../config.json")
|
||||
const cli = require("cli")
|
||||
|
||||
let options = cli.parse({
|
||||
name: ["n", "Name of the player to delete, case sensitive", "string"],
|
||||
token: [false, "Factorio auth token", "string", config.masterAuthToken]
|
||||
})
|
||||
if(!options.name){
|
||||
cli.getUsage()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
needle.post(config.masterIP+":"+config.masterPort+"/api/playerManager/deletePlayer", {
|
||||
name: options.name,
|
||||
token: options.token
|
||||
}, (err, resp) => {
|
||||
console.log(resp.body)
|
||||
});
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
FROM node:9.11.2
|
||||
RUN apt-get update && apt install git curl tar -y
|
||||
RUN mkdir factorioClusterio
|
||||
|
||||
RUN git clone -b master https://github.com/Danielv123/factorioClusterio.git && cd factorioClusterio && npm install --only=production
|
||||
RUN cd factorioClusterio && curl -o factorio.tar.gz -L https://www.factorio.com/get-download/latest/headless/linux64 && tar -xf factorio.tar.gz
|
||||
|
||||
WORKDIR factorioClusterio
|
||||
RUN mkdir instances sharedMods
|
||||
RUN cp config.json.dist config.json
|
||||
|
||||
RUN node client.js download
|
||||
|
||||
LABEL maintainer "Sir3lit@gmail.com"
|
||||
|
||||
EXPOSE 8080 34167
|
||||
VOLUME /factorioClusterio/instances
|
||||
VOLUME /factorioClusterio/sharedMods
|
||||
VOLUME /factorioClusterio/sharedPlugins
|
||||
|
||||
CMD RCONPORT="$RCONPORT" FACTORIOPORT="$FACTORIOPORT" MODE="$MODE" node $MODE\.js start $INSTANCE
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
This product uses the 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.
|
||||
|
||||
The icons for angels modded items located in /static/pictures/ are licensed
|
||||
by https://mods.factorio.com/user/Arch666Angel under CC BY-NC-ND 4.0
|
||||
https://creativecommons.org/licenses/by-nc-nd/4.0/
|
||||
|
||||
@@ -1,280 +1,18 @@
|
||||
<img src="./logo.svg" width="100%" align="right">
|
||||
# Plugin page layout screenshots
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
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.
|
||||
|
||||
# factorioClusterio
|
||||
Before: `61f2b0117263516b592bba0b397e887065de5c4d`.
|
||||
After: `8229db75`.
|
||||
Mobile viewport: 390 x 844 CSS pixels.
|
||||
Desktop viewport: 1440 x 900 CSS pixels.
|
||||
Both use Edge through Playwright, device scale factor 1, dark theme, the same data and default sorting.
|
||||
|
||||
Discord for development/support/play: https://discord.gg/5XuDkje
|
||||
The screenshots are kept on a separate evidence branch so they are not part of the code PR's diff.
|
||||
|
||||
### Ways to support me/the project:
|
||||
|
||||
* Contribute with code/documentation. Always nice to move the project forward
|
||||
|
||||
* Support me monetarily on [patreon](https://www.patreon.com/danielv123) or paypal: danielv@live.no
|
||||
|
||||
### Table of contents
|
||||
|
||||
* [Introduction & methodology](#introduction)
|
||||
|
||||
* [Ubuntu setup](#ubuntu-setup)
|
||||
|
||||
* [Windows setup](#windows-setup)
|
||||
|
||||
* [Optional plugins](#Plugins)
|
||||
|
||||
* [Common problems](#Common-problems)
|
||||
|
||||
* [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 (Also has extensive Prometheus reporting)
|
||||
|
||||
Optional extras (see [Plugins](#Plugins))
|
||||
|
||||
- Have your inventory synchronize across servers
|
||||
|
||||
- Teleport trans from the border of one world to te next
|
||||
|
||||
- Show in-game chat in discord
|
||||
|
||||
|
||||
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, independent on what mods are used. Yes, modded items are fully supported.
|
||||
|
||||
Clusterio can also do a few other neat things, such as giving you access to epoch time, syncing player inventories between servers, keeping track of playtime (playerManager plugin), teleporting trains between servers (trainTeleports) and exporting tons of factorio related statistics to Prometheus for graphing in grafana.
|
||||
|
||||
## 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 --only=production
|
||||
cp config.json.dist config.json
|
||||
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.
|
||||
|
||||
Now you need to edit the `config.json` file. If you skip this step nothing will work.
|
||||
Pretty much all the blank fields should be filled in, except on the master where a few can be omitted.
|
||||
|
||||
* You get the `masterAuthToken` from `secret-api-token.txt` in the master install dir after running the master twice.
|
||||
|
||||
* You get your factorio matchmaking token from factorio.com
|
||||
|
||||
* The `masterAuthSecret` should never be touched unless you want to invalidate everyones authentication tokens
|
||||
|
||||
**Master**
|
||||
|
||||
pm2 start master.js --name master
|
||||
|
||||
OR
|
||||
|
||||
node master.js
|
||||
|
||||
**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
|
||||
|
||||
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 *very* 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
|
||||
|
||||
**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
|
||||
3. Some of the instructions are outdated. If you get stuck somewhere, look at the Ubuntu section.
|
||||
|
||||
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.
|
||||
|
||||
3.5 Some of the instructions are outdated. If you get stuck somewhere, look at the Ubuntu section.
|
||||
|
||||
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)
|
||||
|
||||
Change `masterPort` to something like `8080` (provided by master server owner)
|
||||
|
||||
Change `masterAuthToken` to the value found in `secret-api-token.txt` on the master server
|
||||
|
||||
Repeat step 4 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
|
||||
|
||||
## Plugins
|
||||
Here are the known Clusterio plugins in the wild:
|
||||
1. [Player Manager](https://github.com/Danielv123/playerManager) - Adds player management to the Web UI and shared inventory handling (beta)
|
||||
2. [DiscordChat](https://github.com/jakedraddy/ClusterioDiscordChat) - Logs in-game chat/joins/leave messages on all instances to a Discord webhook.
|
||||
3. [TrainTeleports](https://github.com/Godmave/clusterioTrainTeleports) - Allows you to teleport cargotrains between servers.
|
||||
|
||||
## Common problems
|
||||
|
||||
### Cannot find module: `/../../config`
|
||||
|
||||
Copy your config.json.dist to config.json and configure it.
|
||||
|
||||
### EACCESS [...] LISTEN 443
|
||||
|
||||
Some systems don't let non root processes listen to ports below 1000. Either run with `sudo` or change config.json to use higher port numbers.
|
||||
|
||||
According to [this link](https://askubuntu.com/questions/839520/open-port-443-for-a-node-web-app) if you manually installed node.js following the above instructions, you may need to run the following command to fix this issue:
|
||||
|
||||
sudo setcap 'cap_net_bind_service=+ep' $(readlink -f $(which node))
|
||||
|
||||
### Portforwarding doesn't work on the master server when running under WSL
|
||||
|
||||
If you follow the ubuntu guide on WSL (Windows Subsystem for Linux, Bash on Ubuntu on Windows specifically), you will find that the website works on localhost and on your local ip, but not on the global ip. This is also true when you correctly port-forwarded the correct ports. Even when routing this server through nginx in WSL, the issue persists. Then, on a hunch, I tried to run nginx from windows itself and found that this DID work. It came to me that the only usage difference between the 2 versions of nginx is that I got a Windows Firewall popup.
|
||||
|
||||
TLDR: the tested fix is:
|
||||
|
||||
- open your windows firewall and go to advanced settings
|
||||
|
||||
- click on inbound rules and click on new rule...
|
||||
|
||||
- select port and click next >
|
||||
|
||||
- select TCP and select specific local ports and type in the ports that you want to open (comma separated) and click next > 3 times
|
||||
|
||||
- give the rule a name (like 'web server' or something), give it a description (optionally) and click finish
|
||||
|
||||
### Other fixes for other potential problems:
|
||||
|
||||
Sometimes the install fails. Try `node ./lib/npmPostinstall.js` to complete it.
|
||||
|
||||
|
||||
## 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 --only=production
|
||||
```
|
||||
|
||||
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.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Binary file not shown.
+14
@@ -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,796 +0,0 @@
|
||||
const fs = require('fs-extra');
|
||||
const Tail = require('tail').Tail;
|
||||
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-fork').Rcon;
|
||||
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("lib/fileOps");
|
||||
const stringUtils = require("lib/stringUtils.js");
|
||||
const configManager = require("lib/manager/configManager.js");
|
||||
const hashFile = require('lib/hash').hashFile;
|
||||
|
||||
// argument parsing
|
||||
const args = require('minimist')(process.argv.slice(2));
|
||||
|
||||
// require config.json
|
||||
var config = require(args.config || './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;
|
||||
|
||||
// add better stack traces on promise rejection
|
||||
process.on('unhandledRejection', r => console.log(r));
|
||||
|
||||
// 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.error("ERROR invalid config!");
|
||||
console.error("Master server now needs an access token for write operations. As clusterio slaves depends \
|
||||
upon this, please add your token to config.json 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 = {
|
||||
compressed:true,
|
||||
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();
|
||||
}
|
||||
reject(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
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("To download a clusterio plugin, do");
|
||||
console.error("node client.js manage shared plugins install https://github.com/Danielv123/playerManager");
|
||||
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(path.resolve(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 if(tool && tool == "plugins") {
|
||||
console.log(`node client.js manage ${instance} ${tool} ["list", "add", "remove"]`);
|
||||
} else {
|
||||
console.log('node client.js manage '+(instance || '[instance, "shared"]') +' '+ (tool || '["mods", "config", "plugins"]') + ' ...');
|
||||
}
|
||||
}
|
||||
const tool = process.argv[4] || "";
|
||||
const action = process.argv[5] || "";
|
||||
if(instance){
|
||||
if(tool == "mods"){
|
||||
(async function(){try{
|
||||
// do require down here to reduce master load time
|
||||
const modManager = require("lib/manager/modManager.js")(config);
|
||||
|
||||
// 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 if(tool == "plugins"){
|
||||
(async function(){try{
|
||||
const pluginManager = require("lib/manager/pluginManager.js")(config);
|
||||
if(action == "list"){
|
||||
await pluginManager.listPlugins();
|
||||
} else if(action == "add" || action == "install" || action == "download"){
|
||||
let status = await pluginManager.addPlugin(process.argv[6]);
|
||||
} else if(action == "remove" || action == "uninstall" || action == "delete"){
|
||||
let status = await pluginManager.removePlugin(process.argv[6]);
|
||||
if(status && status.msg) console.log(status.msg);
|
||||
} else if(action == "enable"){
|
||||
let status = await pluginManager.enablePlugin(process.argv[6]);
|
||||
if(status && status.msg) console.log(status.msg);
|
||||
} else if(action == "disable"){
|
||||
let status = await pluginManager.disablePlugin(process.argv[6]);
|
||||
if(status && status.msg) console.log(status.msg);
|
||||
}
|
||||
process.exit(0);
|
||||
}catch(e){
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}})();
|
||||
} 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(path.resolve(config.instanceDirectory, process.argv[3])); // TODO: Check if this can cause i-craft users to format their server by using wrong paths
|
||||
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.on('end', function () {
|
||||
console.log("Downloaded "+name);
|
||||
process.exit(0);
|
||||
});
|
||||
response.pipe(file);
|
||||
}).end();
|
||||
}
|
||||
} else if (command == "start" && instance === undefined) {
|
||||
console.error("ERROR: No instanceName provided!");
|
||||
console.error("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.mkdirSync(instancedirectory + "/scenarios/");
|
||||
ncp("lib/scenarios", path.resolve(instancedirectory, "scenarios"), err => {
|
||||
if (err) console.error(err)
|
||||
});
|
||||
|
||||
// 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', path.join(instancedirectory, "mods"));
|
||||
let instconf = {
|
||||
"factorioPort": args.port || process.env.FACTORIOPORT || Math.floor(Math.random() * 65535),
|
||||
"clientPort": args["rcon-port"] || process.env.RCONPORT || Math.floor(Math.random() * 65535),
|
||||
"__comment_clientPassword": "This is the rcon password. Its also used for making an instanceID. Make sure its unique and not blank.",
|
||||
"clientPassword": args["rcon-password"] || Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 8),
|
||||
"info": {}
|
||||
}
|
||||
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));
|
||||
console.log("Server settings: "+JSON.stringify(serversettings, null, 4));
|
||||
console.log("Creating save .....");
|
||||
let factorio = child_process.spawn(
|
||||
'./' + config.factorioDirectory + '/bin/x64/factorio', [
|
||||
'-c', instancedirectory + '/config.ini',
|
||||
// '--create', instancedirectory + '/saves/save.zip',
|
||||
'--start-server-load-scenario', 'Hotpatch',
|
||||
'--server-settings', instancedirectory + '/server-settings.json',
|
||||
'--rcon-port', Number(process.env.RCONPORT) || instconf.clientPort,
|
||||
'--rcon-password', instconf.clientPassword,
|
||||
], {
|
||||
'stdio': ['pipe', 'pipe', 'pipe']
|
||||
}
|
||||
);
|
||||
factorio.stdout.on("data", data => {
|
||||
data = data.toString("utf8").replace(/(\r\n\t|\n|\r\t)/gm,"");
|
||||
console.log(data);
|
||||
if(data.includes("Starting RCON interface")){
|
||||
let client = new Rcon({
|
||||
packetResponseTimeout: 200000,
|
||||
maxPending: 5
|
||||
});
|
||||
client.connect({
|
||||
host: 'localhost',
|
||||
port: Number(process.env.RCONPORT) || instconf.clientPort,
|
||||
password: instconf.clientPassword
|
||||
});
|
||||
client.onDidAuthenticate(() => {
|
||||
console.log('Clusterio | RCON Authenticated!');
|
||||
});
|
||||
client.onDidConnect(() => {
|
||||
console.log('Clusterio | RCON Connected, starting save');
|
||||
client.send("/c game.server_save('hotpachSave')");
|
||||
});
|
||||
}
|
||||
if(data.includes("Saving finished")){
|
||||
console.log("Map saved as hotpachSave.zip, exiting...");
|
||||
console.log("Instance created!")
|
||||
process.exit(0);
|
||||
}
|
||||
if(data.includes("Downloading from auth server failed")){
|
||||
console.error("Instance creation failed, unable to establish auth server connection.");
|
||||
console.error("Deleting broken instance...");
|
||||
|
||||
fileOps.deleteFolderRecursiveSync(path.resolve(config.instanceDirectory, process.argv[3]));
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
} else if (command == "start" && typeof instance == "string" && instance != "/" && fs.existsSync(instancedirectory)){(async () => {
|
||||
// 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(path.join(instancedirectory,"saves"));
|
||||
for(i = 0; i < savefiles.length; i++){
|
||||
if(savefiles[i].substr(savefiles[i].length - 8, 8) == ".tmp.zip") {
|
||||
fs.unlinkSync(path.resolve(instancedirectory, "saves", savefiles[i]));
|
||||
}
|
||||
}
|
||||
console.log("Clusterio | Rotating old logs...");
|
||||
// clean old log file to avoid crash
|
||||
try{
|
||||
let logPath = path.join(instancedirectory,'factorio-current.log');
|
||||
let stat = await fs.stat(logPath);
|
||||
console.log(stat)
|
||||
console.log(stat.isFile())
|
||||
if(stat.isFile()){
|
||||
let logFilename = `factorio-${Math.floor(Date.parse(stat.mtime)/1000)}.log`;
|
||||
await fs.rename(logPath, path.join(instancedirectory, logFilename));
|
||||
console.log(`Log rotated as ${logFilename}`);
|
||||
}
|
||||
}catch(e){}
|
||||
// Math.floor(Date.now()/1000)
|
||||
// (new Date).toGMTString()
|
||||
|
||||
// move mods from ./sharedMods to the instances mod directory
|
||||
try{fs.mkdirSync(path.join(instancedirectory, "instanceMods"));}catch(e){}
|
||||
try{rmdirSync(path.join(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(path.join(instancedirectory, "mods"));
|
||||
} catch(e){}
|
||||
console.log("Clusterio | Moving shared mods from sharedMods/ to instance/mods...");
|
||||
fs.copySync('sharedMods', path.join(instancedirectory, "mods"));
|
||||
console.log("Clusterio | Moving instance specific mods from instance/instanceMods to instance/mods...");
|
||||
fs.copySync(path.join(instancedirectory, "instanceMods"), path.join(instancedirectory, "mods"));
|
||||
|
||||
// Set paths for factorio so it reads and writes from the correct place even if the instance is imported from somewhere else
|
||||
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
|
||||
`);
|
||||
|
||||
// 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', args["rcon-port"] || Number(process.env.RCONPORT) || instanceconfig.clientPort,
|
||||
'--rcon-password', args["rcon-password"] || instanceconfig.clientPassword,
|
||||
'--server-settings', instancedirectory + '/server-settings.json',
|
||||
'--port', args.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.disconnect();
|
||||
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.toString("utf8").replace("\n", ""));
|
||||
});
|
||||
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({
|
||||
packetResponseTimeout: 200000, // 200s, should allow for commands up to 1250kB in length
|
||||
maxPending: 5
|
||||
});
|
||||
// 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.error(err);
|
||||
if(data && data.indexOf('Starting RCON interface') > 0){
|
||||
client.connect({
|
||||
host: 'localhost',
|
||||
port: args["rcon-port"] || Number(process.env.RCONPORT) || instanceconfig.clientPort,
|
||||
password: args["rcon-password"] || instanceconfig.clientPassword,
|
||||
});
|
||||
} else {
|
||||
setTimeout(function(){
|
||||
checkRcon();
|
||||
},5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
setTimeout(checkRcon, 5000);
|
||||
|
||||
client.onDidAuthenticate(() => {
|
||||
console.log('Clusterio | RCON Authenticated!');
|
||||
instanceManagement(instanceconfig); // 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
|
||||
});
|
||||
process.on('SIGINT', function () {
|
||||
console.log("Caught interrupt signal, disconnecting RCON");
|
||||
client.disconnect().then(()=>{
|
||||
console.log("Rcon disconnected, Sending ^C");
|
||||
// We don't actually do this on windows, because ctrl+c in windows CMD sends it to all subprocesses as well. Doing it twice will abort factorios save.
|
||||
if(process.platform == "linux"){
|
||||
serverprocess.kill("SIGINT");
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if(process.platform == "linux"){
|
||||
// don't open an RCON connection and just use stdio instead, does not work on windows.
|
||||
instanceManagement(instanceconfig);
|
||||
process.on('SIGINT', function () {
|
||||
console.log("Caught interrupt signal, sending ^C");
|
||||
serverprocess.kill("SIGINT");
|
||||
});
|
||||
}
|
||||
|
||||
// set some globals
|
||||
confirmedOrders = [];
|
||||
lastSignalCheck = Date.now();
|
||||
});
|
||||
})()} else {
|
||||
console.error("Invalid arguments, quitting.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ensure instancemanagement only ever runs once
|
||||
var _instanceInitialized;
|
||||
async function instanceManagement(instanceconfig) {
|
||||
if (_instanceInitialized) return;
|
||||
_instanceInitialized = true;
|
||||
|
||||
console.log("Started 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);
|
||||
|
||||
// load plugins and execute onLoad event
|
||||
const pluginManager = require("lib/manager/pluginManager.js")(config);
|
||||
let pluginsToLoad = await pluginManager.getPlugins();
|
||||
let plugins = [];
|
||||
|
||||
for(let i = 0; i < pluginsToLoad.length; i++){
|
||||
let pluginLoadStarted = Date.now();
|
||||
let combinedConfig = deepmerge(instanceconfig,config,{clone:true});
|
||||
let pluginConfig = pluginsToLoad[i];
|
||||
|
||||
if(!global.subscribedFiles) {
|
||||
global.subscribedFiles = {};
|
||||
}
|
||||
if(pluginConfig.binary == "nodePackage" && pluginConfig.enabled){
|
||||
// require plugin class and execute it
|
||||
let pluginClass = require(path.resolve(pluginConfig.pluginPath, "index.js"));
|
||||
plugins[i] = new pluginClass(combinedConfig, async function(data, callback){
|
||||
if(data && data.toString('utf8')[0] != "/") {
|
||||
console.log("Clusterio | "+ pluginsToLoad[i].name + " | " + data.toString('utf8'));
|
||||
return true;
|
||||
} else if (data && data.toString('utf8')[0] == "/"){
|
||||
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 " + pluginConfig.name + " 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);
|
||||
|
||||
|
||||
if(!pluginConfig.fileReadDelay || pluginConfig.fileReadDelay == 0) {
|
||||
// only wipe the file on restart for now, should most likely be rotated during runtime too
|
||||
fs.writeFileSync(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription, "");
|
||||
let tail = new Tail(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription);
|
||||
tail.on("line", function (data) {
|
||||
plugins[i].scriptOutput(data);
|
||||
});
|
||||
} else {
|
||||
fs.watch(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription, fileChangeHandler);
|
||||
// run once in case a plugin wrote out information before the plugin loaded fully
|
||||
// delay, so the socket got enough time to connect
|
||||
setTimeout(() => {
|
||||
fileChangeHandler(false, pluginConfig.scriptOutputFileSubscription);
|
||||
}, 500);
|
||||
|
||||
// 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 o = 0; o < stuff.length; o++) {
|
||||
if (stuff[o] && !stuff[o].includes('\u0000\u0000')) {
|
||||
try {
|
||||
plugins[i].scriptOutput(stuff[o]);
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
pluginConfig.fileReadDelay || 0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(`Clusterio | Loaded plugin ${pluginsToLoad[i].name} in ${Date.now() - pluginLoadStarted}ms`);
|
||||
} else {
|
||||
// this plugin doesn't have a client portion. Maybe it runs on the master only?
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
info: instanceconfig.info,
|
||||
};
|
||||
if(playerCount){
|
||||
payload.playerCount = playerCount.replace(/(\r\n\t|\n|\r\t)/gm, "");
|
||||
} else {
|
||||
payload.playerCount = 0;
|
||||
}
|
||||
|
||||
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 #####");
|
||||
}
|
||||
global.mac = mac;
|
||||
payload.mac = mac;
|
||||
// console.log("Registered our presence 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"+config.masterIP+" at " + payload.time);
|
||||
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.
|
||||
if(response.body !== "ok") {
|
||||
console.log("Got no \"ok\" while registering our precense with master "+config.masterIP+" at " + payload.time);
|
||||
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.error("Unable to contact master server /api/checkMod! 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.error(new Error("Unable to contact master server /api/uploadMod! 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")
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} // 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");
|
||||
}
|
||||
|
||||
let modsDir = config.instanceDirectory+"/"+instanceName+"/mods/";
|
||||
function hashMod(name) {
|
||||
if (path.extname(name) != ".zip") {
|
||||
// Can't hash unzipped mods, return null that's filtered out later
|
||||
return null;
|
||||
} else {
|
||||
return hashFile(modsDir+name).then(hash => (
|
||||
{modName: name, hash: hash}
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let promises = fs.readdirSync(modsDir).map(hashMod);
|
||||
Promise.all(promises).then(hashes => {
|
||||
// Remove null entries from hashMod
|
||||
callback(hashes.filter(entry => entry !== null));
|
||||
});
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
{
|
||||
"__comment": "Where factorio folder. Should be in the same folder as this config file.",
|
||||
"factorioDirectory": "./factorio",
|
||||
"instanceDirectory": "./instances",
|
||||
"databaseDirectory": "./database",
|
||||
|
||||
"__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",
|
||||
"masterPort": 8080,
|
||||
|
||||
"__commentsslPort": "HTTPS port for the master to listen to. Set to 0 or an empty string to not listen for 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,
|
||||
"__comment6.5": "When the master is undersupplied, we have 3 different modes to 'fairly' distribute items. First to ask gets all, dole division using weird formula or experimental neural net tries to make stuff as fair as possible. Feel free to play around.",
|
||||
"disableFairItemDistribution": false,
|
||||
"useNeuralNetDoleDivider": true,
|
||||
"uploadModsToMaster": true,
|
||||
"msBetweenCommands": 10,
|
||||
"allowRemoteCommandExecution": true,
|
||||
"enableCrossServerShout": true,
|
||||
"mirrorAllChat": false,
|
||||
"__comment7": "Autosave interval for items in the master storage in milliseconds. Defaults to one minute when set to 0 or not set.",
|
||||
"autosaveInterval": 600000,
|
||||
"disablePrometheusPushgateway": false,
|
||||
"__comment8": "This setting only affects the master. It makes every import for things that arent 'electricity' return a maximum of 0. Effectively disables teleport chests.",
|
||||
"disableImportsOfEverythingExceptElectricity": false
|
||||
}
|
||||
@@ -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,57 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const NodeInstance_1 = require("./NodeInstance");
|
||||
const Point_1 = require("./Point");
|
||||
const Grid_1 = require("./Grid");
|
||||
/**
|
||||
* Manages all nodes within the cluster
|
||||
*/
|
||||
class ClusterManager {
|
||||
constructor() {
|
||||
this.Grid = new Grid_1.Grid();
|
||||
}
|
||||
/**
|
||||
* Generates a x*y grid of 1x1 nodes
|
||||
* @param width How wide to make the node grid
|
||||
* @param height How high to make the node grid
|
||||
* @param nodeNamePrefix The prefix used for the initial node names. The name will be in the format <prefix>_x_y.
|
||||
*/
|
||||
GenerateBasicGridOfNodes(width, height, nodeNamePrefix) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
for (let y = 0; y < height; y++) {
|
||||
let newNode = new NodeInstance_1.NodeInstance(nodeNamePrefix + "-" + (x + 1) + "-" + (y + 1), new Point_1.Point(x, y), 1, 1);
|
||||
this.Grid.AddNodeToGrid(newNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Assigns all nodes that currently do not have a server assigned to the given server
|
||||
* @param server Server to assign all unassigned nodes to
|
||||
*/
|
||||
AssignAllUnassignedNodesToServer(server) {
|
||||
this.CheckServerArray(server);
|
||||
this.Grid.GetNodes()
|
||||
.filter(node => { node.Server === null; })
|
||||
.forEach(node => {
|
||||
server.AddNodeInstance(node);
|
||||
node.Server = server;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Assigns a node to a server
|
||||
* @param x x co-ordinate of the node
|
||||
* @param y y co-ordinate of the node
|
||||
* @param serverName server name
|
||||
*/
|
||||
AssignNodeToServer(x, y, server) {
|
||||
this._nodeArray[x][y].Server = server;
|
||||
}
|
||||
CheckServerArray(server) {
|
||||
let s = this._servers[server.Name];
|
||||
if (s == null) {
|
||||
this._servers[server.Name] = server;
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.ClusterManager = ClusterManager;
|
||||
//# sourceMappingURL=ClusterManager.js.map
|
||||
@@ -1,112 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const child_process = require("child_process");
|
||||
const needle = require("needle");
|
||||
const Helpers_1 = require("./Helpers");
|
||||
const fs = require("fs");
|
||||
const util = require("util");
|
||||
/**
|
||||
* Contains all interactions between the cluster model, the
|
||||
* Clusterio master.js/client.js infrastructure and the operating system.
|
||||
*/
|
||||
class ClusterioMasterProxy {
|
||||
constructor(masterIP, masterPort, masterAuthToken, serviceHostType) {
|
||||
this._masterIP = `${masterIP}:${masterPort}`;
|
||||
this._needleOptionsWithTokenAuthHeader = {
|
||||
headers: {
|
||||
'x-access-token': masterAuthToken,
|
||||
},
|
||||
};
|
||||
}
|
||||
async RunRconCommand(instanceId, command, logDescription) {
|
||||
console.log("Running command: " + logDescription);
|
||||
let request = { instanceID: instanceId, command };
|
||||
let response = await needle("post", this._masterIP + "/api/runCommand", request, this._needleOptionsWithTokenAuthHeader);
|
||||
}
|
||||
async GetSlaves() {
|
||||
let response = await needle("get", this._masterIP + "/api/slaves", this._needleOptionsWithTokenAuthHeader);
|
||||
let body = response.body;
|
||||
let slaves = new Array();
|
||||
Object.entries(body).forEach((keyValuePair, index, array) => {
|
||||
let slave = keyValuePair[1];
|
||||
slave.id = keyValuePair[0];
|
||||
slaves.push(slave);
|
||||
});
|
||||
return slaves;
|
||||
}
|
||||
async CreateNodeInstancesOnLocalServer(nodes) {
|
||||
await Helpers_1.asyncForEach(nodes, async (node) => {
|
||||
await this.CreateNodeInstanceOnLocalServer(node);
|
||||
});
|
||||
await Helpers_1.asyncForEach(nodes, async (node) => {
|
||||
await this.CreateTeleportZonesForANodeInstance(node);
|
||||
});
|
||||
await this.sleep(1000);
|
||||
await Helpers_1.asyncForEach(nodes, async (node) => {
|
||||
await this.CreateTeleportRestrictionsForANodeInstance(node);
|
||||
await this.sleep(100);
|
||||
});
|
||||
}
|
||||
async CreateNodeInstanceOnLocalServer(node) {
|
||||
console.log(`Creating instance ${node.Name}...`);
|
||||
await this.CreateNodeInstanceOnLocalServerUsingPm2(node);
|
||||
console.log("Waiting for node to connect to cluster master...");
|
||||
while (true) {
|
||||
await this.sleep(1000);
|
||||
//TODO: Use /api/getSlaveMeta/
|
||||
let slaves = await this.GetSlaves();
|
||||
let slave = slaves.find((slave, index, array) => slave.instanceName === node.Name);
|
||||
if (slave != null) {
|
||||
node.ClusterioWorldId = slave.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
async CreateNodeInstanceOnLocalServerUsingPm2(node) {
|
||||
let proc = child_process.spawn("pm2", ['start', '--name', node.Name, 'client.js', '--', 'start', node.Name]);
|
||||
proc.stdout.on('data', (data) => { console.log(`stdout: ${data}`); });
|
||||
proc.stderr.on('data', data => { console.log(`stderr: ${data}`); });
|
||||
proc.on('error', error => { console.log(`error: ${error}`); });
|
||||
}
|
||||
async CreateNodeInstanceOnLocalServerUsingScreen(node) {
|
||||
// first call of client.js start for a new node creates map from HotPatch scenario then exits.
|
||||
let proc = child_process.spawn("screen", ['-dmS', node.Name, '-L', '-Logfile', node.Name + '.log', 'node', 'client.js', 'start', node.Name]);
|
||||
proc.stdout.on('data', (data) => { console.log(`stdout: ${data}`); });
|
||||
proc.stderr.on('data', data => { console.log(`stderr: ${data}`); });
|
||||
proc.on('error', error => { console.log(`error: ${error}`); });
|
||||
console.log("Waiting for creation of new map...");
|
||||
// create awaiter for fs.readFile
|
||||
const readFile = util.promisify(fs.readFile);
|
||||
while (true) {
|
||||
await this.sleep(1000);
|
||||
let fileData = await readFile(node.Name + '.log');
|
||||
if (fileData.includes('Instance created')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// start node
|
||||
proc = child_process.spawn("screen", ['-dmS', node.Name, '-L', '-Logfile', node.Name + '.log', 'node', 'client.js', 'start', node.Name]);
|
||||
proc.stdout.on('data', data => { console.log(`stdout: ${data}`); });
|
||||
proc.stderr.on('data', data => { console.log(`stderr: ${data}`); });
|
||||
proc.on('error', error => { console.log(`error: ${error}`); });
|
||||
}
|
||||
async CreateTeleportZonesForANodeInstance(nodeInstance) {
|
||||
await Helpers_1.asyncForEach(nodeInstance.TeleportZones.values(), async (zone) => {
|
||||
let o = zone.GenerateTeleportZoneInFactorioCoordinates();
|
||||
let cmd = `CreateZone('${o[0]}',${o[1]},${o[2]},${o[3]},${o[4]},${o[5]},true)`;
|
||||
await this.RunRconCommand(nodeInstance.ClusterioWorldId, `/c remote.call('trainTeleports','runCode',\"${cmd}\")`, cmd);
|
||||
});
|
||||
}
|
||||
async CreateTeleportRestrictionsForANodeInstance(nodeInstance) {
|
||||
await Helpers_1.asyncForEach(nodeInstance.TeleportZones.values(), async (zone) => {
|
||||
let o = zone.GenerateZoneRestriction();
|
||||
let cmd = `CreateZoneRestriction(${o[0]},'${o[1]}','${o[2]}')`;
|
||||
await this.RunRconCommand(nodeInstance.ClusterioWorldId, `/c remote.call('trainTeleports','runCode',\"${cmd}\")`, cmd);
|
||||
});
|
||||
}
|
||||
sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
exports.ClusterioMasterProxy = ClusterioMasterProxy;
|
||||
//# sourceMappingURL=ClusterioMasterProxy.js.map
|
||||
@@ -1,8 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
/** The multiplier used to convert from the grid's coordinate system to factorio's.
|
||||
* 1000 means a 1x1 grid node will have a factorio map size of 1000x1000. */
|
||||
exports.GridToFactorioCoordinateSystemMultiplier = 1000;
|
||||
/** The width of teleport zones when they get created. */
|
||||
exports.TeleportZoneWidth = 50;
|
||||
//# sourceMappingURL=Config.js.map
|
||||
@@ -1,177 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const typescript_collections_1 = require("typescript-collections");
|
||||
const Point_1 = require("./Point");
|
||||
const TeleportZone_1 = require("./TeleportZone");
|
||||
const TeleportZoneDirection_1 = require("./TeleportZoneDirection");
|
||||
class Grid {
|
||||
constructor() {
|
||||
this._nodes = new typescript_collections_1.Dictionary();
|
||||
this._minX = 0;
|
||||
this._minY = 0;
|
||||
this._maxX = 0;
|
||||
this._maxY = 0;
|
||||
}
|
||||
GetNodes() {
|
||||
return this._nodes.values();
|
||||
}
|
||||
/**
|
||||
* Adds a node to the grid
|
||||
* @param topLeftCoordinate The top left x,y coordinate of the node
|
||||
* @param nodeInstance
|
||||
*/
|
||||
AddNodeToGrid(nodeInstance) {
|
||||
let topLeftCoordinate = nodeInstance.TopLeftCoordinate;
|
||||
let pointsToSave = new Array();
|
||||
// check all points that make up nodeInstance for existing nodes
|
||||
for (let x = topLeftCoordinate.X; x < topLeftCoordinate.X + nodeInstance.Width; x++) {
|
||||
for (let y = topLeftCoordinate.Y; y < topLeftCoordinate.Y + nodeInstance.Height; y++) {
|
||||
let currentPoint = new Point_1.Point(x, y);
|
||||
let existingNode = this._nodes.getValue(currentPoint);
|
||||
if (existingNode != null) {
|
||||
throw new Error("Cannot add node to grid - collision detected with node '" + existingNode.Name + "'");
|
||||
}
|
||||
pointsToSave.push(currentPoint);
|
||||
}
|
||||
}
|
||||
// no existing nodes found to assign node to these points
|
||||
for (let i = 0; i < pointsToSave.length; i++) {
|
||||
this._nodes.setValue(pointsToSave[i], nodeInstance);
|
||||
}
|
||||
//TODO: Refactor
|
||||
//#region Set up teleport zones
|
||||
// check surrounding points for nodes
|
||||
// check north points
|
||||
let y = topLeftCoordinate.Y - 1;
|
||||
for (let x = topLeftCoordinate.X; x < topLeftCoordinate.X + nodeInstance.Width; x++) {
|
||||
let currentPoint = new Point_1.Point(x, y);
|
||||
let existingNode = this._nodes.getValue(currentPoint);
|
||||
// found a node - set up teleport zone
|
||||
if (existingNode != null) {
|
||||
// Create/Update teleport node in existing node to new node
|
||||
let teleportZoneFromExistingToNew = existingNode.TeleportZones.getValue(nodeInstance.Key);
|
||||
if (teleportZoneFromExistingToNew == null) {
|
||||
teleportZoneFromExistingToNew = new TeleportZone_1.TeleportZone(existingNode, TeleportZoneDirection_1.TeleportZoneDirection.South, currentPoint, currentPoint, existingNode.TeleportZones.size(), nodeInstance);
|
||||
existingNode.TeleportZones.setValue(nodeInstance.Key, teleportZoneFromExistingToNew);
|
||||
}
|
||||
else {
|
||||
teleportZoneFromExistingToNew.BottomRightCoordinate = currentPoint;
|
||||
}
|
||||
// Create/Update teleport node in new node to existing node
|
||||
let teleportZoneFromNewToExisting = nodeInstance.TeleportZones.getValue(existingNode.Key);
|
||||
let pointInNewNode = new Point_1.Point(currentPoint.X, currentPoint.Y + 1);
|
||||
if (teleportZoneFromNewToExisting == null) {
|
||||
teleportZoneFromNewToExisting = new TeleportZone_1.TeleportZone(nodeInstance, TeleportZoneDirection_1.TeleportZoneDirection.North, pointInNewNode, pointInNewNode, nodeInstance.TeleportZones.size(), existingNode);
|
||||
nodeInstance.TeleportZones.setValue(existingNode.Key, teleportZoneFromNewToExisting);
|
||||
}
|
||||
else {
|
||||
teleportZoneFromNewToExisting.BottomRightCoordinate = currentPoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
// check south points
|
||||
y = topLeftCoordinate.Y + nodeInstance.Height + 1;
|
||||
for (let x = topLeftCoordinate.X; x < topLeftCoordinate.X + nodeInstance.Width; x++) {
|
||||
let currentPoint = new Point_1.Point(x, y);
|
||||
let existingNode = this._nodes.getValue(currentPoint);
|
||||
// found a node - set up teleport zone
|
||||
if (existingNode != null) {
|
||||
// Create/Update teleport node in existing node to new node
|
||||
let teleportZoneFromExistingToNew = existingNode.TeleportZones.getValue(nodeInstance.Key);
|
||||
if (teleportZoneFromExistingToNew == null) {
|
||||
teleportZoneFromExistingToNew = new TeleportZone_1.TeleportZone(existingNode, TeleportZoneDirection_1.TeleportZoneDirection.North, currentPoint, currentPoint, existingNode.TeleportZones.size(), nodeInstance);
|
||||
existingNode.TeleportZones.setValue(nodeInstance.Key, teleportZoneFromExistingToNew);
|
||||
}
|
||||
else {
|
||||
teleportZoneFromExistingToNew.BottomRightCoordinate = currentPoint;
|
||||
}
|
||||
// Create/Update teleport node in new node to existing node
|
||||
let teleportZoneFromNewToExisting = nodeInstance.TeleportZones.getValue(existingNode.Key);
|
||||
let pointInNewNode = new Point_1.Point(currentPoint.X, currentPoint.Y - 1);
|
||||
if (teleportZoneFromNewToExisting == null) {
|
||||
teleportZoneFromNewToExisting = new TeleportZone_1.TeleportZone(nodeInstance, TeleportZoneDirection_1.TeleportZoneDirection.South, pointInNewNode, pointInNewNode, nodeInstance.TeleportZones.size(), existingNode);
|
||||
nodeInstance.TeleportZones.setValue(existingNode.Key, teleportZoneFromNewToExisting);
|
||||
}
|
||||
else {
|
||||
teleportZoneFromNewToExisting.BottomRightCoordinate = currentPoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
// check west points
|
||||
let x = topLeftCoordinate.X - 1;
|
||||
for (let y = topLeftCoordinate.Y; y < topLeftCoordinate.Y + nodeInstance.Height; y++) {
|
||||
let currentPoint = new Point_1.Point(x, y);
|
||||
let existingNode = this._nodes.getValue(currentPoint);
|
||||
// found a node - set up teleport zone
|
||||
if (existingNode != null) {
|
||||
// Create/Update teleport node in existing node to new node
|
||||
let teleportZoneFromExistingToNew = existingNode.TeleportZones.getValue(nodeInstance.Key);
|
||||
if (teleportZoneFromExistingToNew == null) {
|
||||
teleportZoneFromExistingToNew = new TeleportZone_1.TeleportZone(existingNode, TeleportZoneDirection_1.TeleportZoneDirection.East, currentPoint, currentPoint, existingNode.TeleportZones.size(), nodeInstance);
|
||||
existingNode.TeleportZones.setValue(nodeInstance.Key, teleportZoneFromExistingToNew);
|
||||
}
|
||||
else {
|
||||
teleportZoneFromExistingToNew.BottomRightCoordinate = currentPoint;
|
||||
}
|
||||
// Create/Update teleport node in new node to existing node
|
||||
let teleportZoneFromNewToExisting = nodeInstance.TeleportZones.getValue(existingNode.Key);
|
||||
let pointInNewNode = new Point_1.Point(currentPoint.X + 1, currentPoint.Y);
|
||||
if (teleportZoneFromNewToExisting == null) {
|
||||
teleportZoneFromNewToExisting = new TeleportZone_1.TeleportZone(nodeInstance, TeleportZoneDirection_1.TeleportZoneDirection.West, pointInNewNode, pointInNewNode, nodeInstance.TeleportZones.size(), existingNode);
|
||||
nodeInstance.TeleportZones.setValue(existingNode.Key, teleportZoneFromNewToExisting);
|
||||
}
|
||||
else {
|
||||
teleportZoneFromNewToExisting.BottomRightCoordinate = currentPoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
// check east points
|
||||
x = topLeftCoordinate.X + nodeInstance.Width + 1;
|
||||
for (let y = topLeftCoordinate.Y; y < topLeftCoordinate.Y + nodeInstance.Height; y++) {
|
||||
let currentPoint = new Point_1.Point(x, y);
|
||||
let existingNode = this._nodes.getValue(currentPoint);
|
||||
// found a node - set up teleport zone
|
||||
if (existingNode != null) {
|
||||
// Create/Update teleport node in existing node to new node
|
||||
let teleportZoneFromExistingToNew = existingNode.TeleportZones.getValue(nodeInstance.Key);
|
||||
if (teleportZoneFromExistingToNew == null) {
|
||||
teleportZoneFromExistingToNew = new TeleportZone_1.TeleportZone(existingNode, TeleportZoneDirection_1.TeleportZoneDirection.West, currentPoint, currentPoint, existingNode.TeleportZones.size(), nodeInstance);
|
||||
existingNode.TeleportZones.setValue(nodeInstance.Key, teleportZoneFromExistingToNew);
|
||||
}
|
||||
else {
|
||||
teleportZoneFromExistingToNew.BottomRightCoordinate = currentPoint;
|
||||
}
|
||||
// Create/Update teleport node in new node to existing node
|
||||
let teleportZoneFromNewToExisting = nodeInstance.TeleportZones.getValue(existingNode.Key);
|
||||
let pointInNewNode = new Point_1.Point(currentPoint.X - 1, currentPoint.Y);
|
||||
if (teleportZoneFromNewToExisting == null) {
|
||||
teleportZoneFromNewToExisting = new TeleportZone_1.TeleportZone(nodeInstance, TeleportZoneDirection_1.TeleportZoneDirection.East, pointInNewNode, pointInNewNode, nodeInstance.TeleportZones.size(), existingNode);
|
||||
nodeInstance.TeleportZones.setValue(existingNode.Key, teleportZoneFromNewToExisting);
|
||||
}
|
||||
else {
|
||||
teleportZoneFromNewToExisting.BottomRightCoordinate = currentPoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
this._nodes.setValue(topLeftCoordinate, nodeInstance);
|
||||
this._minX = Math.min(this._minX, topLeftCoordinate.X);
|
||||
this._minY = Math.min(this._minY, topLeftCoordinate.Y);
|
||||
this._maxX = Math.max(this._maxX, topLeftCoordinate.X + nodeInstance.Width);
|
||||
this._maxY = Math.max(this._maxY, topLeftCoordinate.Y + nodeInstance.Height);
|
||||
}
|
||||
get Width() {
|
||||
return this._maxX - this._minX;
|
||||
}
|
||||
get Height() {
|
||||
return this._maxY - this._minY;
|
||||
}
|
||||
get TopLeftCoordinate() {
|
||||
return new Point_1.Point(this._minX, this._minY);
|
||||
}
|
||||
get BottomRightCoordinate() {
|
||||
return new Point_1.Point(this._maxX, this._maxY);
|
||||
}
|
||||
}
|
||||
exports.Grid = Grid;
|
||||
//# sourceMappingURL=Grid.js.map
|
||||
@@ -1,10 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
//TODO: Make generic
|
||||
async function asyncForEach(array, callback) {
|
||||
for (let index = 0; index < array.length; index++) {
|
||||
await callback(array[index], index, array);
|
||||
}
|
||||
}
|
||||
exports.asyncForEach = asyncForEach;
|
||||
//# sourceMappingURL=Helpers.js.map
|
||||
@@ -1,3 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
//# sourceMappingURL=IApiSlaves.js.map
|
||||
@@ -1,3 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
//# sourceMappingURL=IClusterioMasterProxy.js.map
|
||||
@@ -1,27 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
console.log("started.");
|
||||
const ClusterioMasterProxy_1 = require("./ClusterioMasterProxy");
|
||||
const ClusterManager_1 = require("./ClusterManager");
|
||||
const argv = require('minimist')(process.argv.slice(2));
|
||||
let configPath = argv['configPath'];
|
||||
if (configPath === undefined || configPath === null || configPath === "") {
|
||||
throw new Error("configPath is null.");
|
||||
}
|
||||
;
|
||||
let config = require(configPath);
|
||||
let proxy = new ClusterioMasterProxy_1.ClusterioMasterProxy(config.masterIP, config.masterPort, config.masterAuthToken);
|
||||
let manager = new ClusterManager_1.ClusterManager();
|
||||
manager.GenerateBasicGridOfNodes(2, 2, 'GridLockTest_');
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
// async function test() {
|
||||
// await asyncForEach([1,2,3], async (v, i, a) => {
|
||||
// console.log("test" + i);
|
||||
// await sleep(5000);
|
||||
// });
|
||||
// }
|
||||
// test();
|
||||
proxy.CreateNodeInstancesOnLocalServer(manager.Grid.GetNodes());
|
||||
//# sourceMappingURL=ModelController.js.map
|
||||
@@ -1,24 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
console.log("started.");
|
||||
const ClusterioMasterProxy_1 = require("./ClusterioMasterProxy");
|
||||
const ClusterManager_1 = require("./ClusterManager");
|
||||
const argv = require('minimist')(process.argv.slice(2));
|
||||
let configPath = argv['configPath'];
|
||||
if (configPath === undefined || configPath === null || configPath === "") {
|
||||
throw new Error("configPath is null.");
|
||||
}
|
||||
;
|
||||
let config = require(configPath);
|
||||
let proxy = new ClusterioMasterProxy_1.ClusterioMasterProxy(config.masterIP, config.masterPort, config.masterAuthToken);
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
// main
|
||||
(async () => {
|
||||
let manager = new ClusterManager_1.ClusterManager();
|
||||
manager.GenerateBasicGridOfNodes(2, 2, 'GridLockTest');
|
||||
await proxy.CreateNodeInstancesOnLocalServer(manager.Grid.GetNodes());
|
||||
return;
|
||||
})();
|
||||
//# sourceMappingURL=ModelDemo.js.map
|
||||
@@ -1,61 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
console.log("Started.");
|
||||
const ClusterioMasterProxy_1 = require("./ClusterioMasterProxy");
|
||||
const ClusterManager_1 = require("./ClusterManager");
|
||||
const argv = require('minimist')(process.argv.slice(2));
|
||||
// arg: configPath
|
||||
// description: should point to the clusterio config.json file.
|
||||
let configPath = argv['configPath'];
|
||||
if (configPath === undefined || configPath === null || configPath === "") {
|
||||
configPath = "../../config.json";
|
||||
}
|
||||
;
|
||||
let config = require(configPath);
|
||||
// arg: mode
|
||||
// description: should be GenerateBasicGridOfNodes for now. Can extend in future.
|
||||
let mode = argv['mode'];
|
||||
if (mode === undefined || mode === null || mode === "") {
|
||||
throw new Error("mode is null. mode must be 'GenerateBasicGridOfNodes' (for now).");
|
||||
}
|
||||
;
|
||||
// arg: serviceHostType
|
||||
// description: what program to use to host the client.js instances.
|
||||
let serviceHostType = argv['serviceHostType'];
|
||||
if (serviceHostType === undefined || serviceHostType === null || serviceHostType === "") {
|
||||
throw new Error("serviceHostType is null. serviceHostType must be either 'pm2' or 'screen'.");
|
||||
}
|
||||
;
|
||||
if (serviceHostType != "pm2" && serviceHostType != "screen") {
|
||||
throw new Error("serviceHostType must be either 'pm2' or 'screen'.");
|
||||
}
|
||||
// arg: width
|
||||
// description: width of node grid
|
||||
let width = argv['width'];
|
||||
if (width === undefined || width === null || width === "") {
|
||||
throw new Error("width is null. width must be an int > 0.");
|
||||
}
|
||||
;
|
||||
// arg: height
|
||||
// description: height of node grid
|
||||
let height = argv['height'];
|
||||
if (height === undefined || height === null || height === "") {
|
||||
throw new Error("height is null. height must be an int > 0.");
|
||||
}
|
||||
;
|
||||
// arg: namePrefix
|
||||
// description: namePrefix of node grid
|
||||
let namePrefix = argv['namePrefix'];
|
||||
if (namePrefix === undefined || namePrefix === null || namePrefix === "") {
|
||||
throw new Error("namePrefix is null.");
|
||||
}
|
||||
;
|
||||
let proxy = new ClusterioMasterProxy_1.ClusterioMasterProxy(config.masterIP, config.masterPort, config.masterAuthToken, serviceHostType);
|
||||
// main
|
||||
(async () => {
|
||||
let manager = new ClusterManager_1.ClusterManager();
|
||||
manager.GenerateBasicGridOfNodes(width, height, namePrefix);
|
||||
await proxy.CreateNodeInstancesOnLocalServer(manager.Grid.GetNodes());
|
||||
return;
|
||||
})();
|
||||
//# sourceMappingURL=ModelRunner.js.map
|
||||
@@ -1,29 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const Point_1 = require("./Point");
|
||||
const typescript_collections_1 = require("typescript-collections");
|
||||
/**
|
||||
* An instance of a Factorio server. Multiple nodes can run on the same server.
|
||||
*/
|
||||
class NodeInstance {
|
||||
constructor(name, topLeftCoordinate, width, height) {
|
||||
this.TeleportZones = new typescript_collections_1.Dictionary();
|
||||
this.Name = name;
|
||||
this.Width = width;
|
||||
this.Height = height;
|
||||
this.TopLeftCoordinate = topLeftCoordinate;
|
||||
}
|
||||
get BottomRightCoordinate() {
|
||||
return new Point_1.Point(this.TopLeftCoordinate.X + this.Width, this.TopLeftCoordinate.Y + this.Height);
|
||||
}
|
||||
// public AddTeleportZone(name: string, topLeftCoordinate: Point, bottomRightCoordinate: Point) {
|
||||
// this.TeleportZones.push(new TeleportZone(this, topLeftCoordinate, bottomRightCoordinate, name));
|
||||
// }
|
||||
AddTeleportZonePoint() {
|
||||
}
|
||||
get Key() {
|
||||
return this.Name;
|
||||
}
|
||||
}
|
||||
exports.NodeInstance = NodeInstance;
|
||||
//# sourceMappingURL=NodeInstance.js.map
|
||||
@@ -1,57 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
class Point {
|
||||
/**
|
||||
* Constructs a Point. Will not accept NaNs.
|
||||
* @param x A number. Cannot be NaN.
|
||||
* @param y A number. Cannot be NaN.
|
||||
*/
|
||||
constructor(x, y) {
|
||||
this.toString = () => {
|
||||
return "(" + this.X + "," + this.Y + ")";
|
||||
};
|
||||
if (Number.isNaN(x)) {
|
||||
throw new Error("x is NaN. Will not construct Point.");
|
||||
}
|
||||
if (Number.isNaN(y)) {
|
||||
throw new Error("y is NaN. Will not construct Point.");
|
||||
}
|
||||
this.X = x;
|
||||
this.Y = y;
|
||||
}
|
||||
/**
|
||||
* Constructs a Point from a string representation in the format (x,y). Will not accept NaNs.
|
||||
* @param vectorAsString A string in the format (x,y). Will not accept NaNs.
|
||||
*/
|
||||
static FromString(vectorAsString) {
|
||||
this.CheckChar('(', vectorAsString[0], vectorAsString);
|
||||
this.CheckChar(')', vectorAsString[vectorAsString.length - 1], vectorAsString);
|
||||
if (vectorAsString.indexOf(',') === -1) {
|
||||
throw new Error("vectorAsString is an invalid format. Cannot find ','. fullString: " + vectorAsString);
|
||||
}
|
||||
let splitStr = vectorAsString.substr(1, vectorAsString.length - 2).split(',');
|
||||
if (splitStr.length != 2) {
|
||||
throw new Error("vectorAsString is an invalid format. Expecting string in format (x,y). fullString: " + vectorAsString);
|
||||
}
|
||||
let x = parseInt(splitStr[0]);
|
||||
let y = parseInt(splitStr[1]);
|
||||
let v = new Point(x, y);
|
||||
return v;
|
||||
}
|
||||
static CheckChar(expectedChar, actualChar, fullString) {
|
||||
if (actualChar != expectedChar) {
|
||||
throw new Error("vectorAsString is an invalid format. Expecting: '" + expectedChar + "'. Actual: '" + actualChar + "'. fullString: " + fullString);
|
||||
}
|
||||
}
|
||||
equals(obj) {
|
||||
return this.X === obj.X && this.Y === obj.Y;
|
||||
}
|
||||
Add(p) {
|
||||
return new Point(this.X + p.X, this.Y + p.Y);
|
||||
}
|
||||
Subtract(p) {
|
||||
return new Point(this.X - p.X, this.Y - p.Y);
|
||||
}
|
||||
}
|
||||
exports.Point = Point;
|
||||
//# sourceMappingURL=Point.js.map
|
||||
@@ -1,16 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
/**
|
||||
* A server. Can contain multiple nodes (Factorio server instances).
|
||||
*/
|
||||
class Server {
|
||||
constructor(name) {
|
||||
this.Nodes = new Array();
|
||||
this.Name = name;
|
||||
}
|
||||
AddNodeInstance(node) {
|
||||
this.Nodes.push(node);
|
||||
}
|
||||
}
|
||||
exports.Server = Server;
|
||||
//# sourceMappingURL=Server.js.map
|
||||
@@ -1,8 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var ServiceHostType;
|
||||
(function (ServiceHostType) {
|
||||
ServiceHostType[ServiceHostType["pm2"] = 0] = "pm2";
|
||||
ServiceHostType[ServiceHostType["screen"] = 1] = "screen";
|
||||
})(ServiceHostType = exports.ServiceHostType || (exports.ServiceHostType = {}));
|
||||
//# sourceMappingURL=ServiceHostType.js.map
|
||||
@@ -1,64 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const Point_1 = require("./Point");
|
||||
const TeleportZoneDirection_1 = require("./TeleportZoneDirection");
|
||||
const Config = require("./Config");
|
||||
class TeleportZone {
|
||||
constructor(parentNode, direction, topLeftCoordinate, bottomRightCoordinate, index, targetNode) {
|
||||
this.TopLeftCoordinate = topLeftCoordinate;
|
||||
this.BottomRightCoordinate = bottomRightCoordinate;
|
||||
this.ParentNode = parentNode;
|
||||
this.Direction = direction;
|
||||
this.Index = index;
|
||||
this.TargetNode = targetNode;
|
||||
}
|
||||
get Name() {
|
||||
return TeleportZoneDirection_1.TeleportZoneDirection[this.Direction] + " to " + this.TargetNode.Name;
|
||||
}
|
||||
// public AddRestriction(restrictingTeleportZone: TeleportZone) {
|
||||
// this.Restrictions.push(new TeleportZoneRestriction(this, restrictingTeleportZone));
|
||||
// }
|
||||
/**
|
||||
* Converts this teleport zone into Factorio map co-ordinates
|
||||
* @returns [name, topleft.x, topleft.y, width, height, zoneIndex]
|
||||
*/
|
||||
GenerateTeleportZoneInFactorioCoordinates() {
|
||||
// get where this zone is relative to the node itself
|
||||
let topLeftRelativeCoordinate = this.TopLeftCoordinate.Subtract(this.ParentNode.TopLeftCoordinate);
|
||||
// a grid coordinate point is equivalent to a 1x1 node, so bottomright = topleft.
|
||||
// add (1,1) to translate from area representation to coordinates representing actual points.
|
||||
let bottomRightRelativeCoordinate = this.BottomRightCoordinate
|
||||
.Subtract(this.ParentNode.TopLeftCoordinate)
|
||||
.Add(new Point_1.Point(1, 1));
|
||||
// Factorio map is e.g. (-500,-500) to (500, 500) for a 1000x1000 node.
|
||||
// We therefore need to offset our co-ordinates so that (0,0) is the center and not (width/2, height/2)
|
||||
let m = Config.GridToFactorioCoordinateSystemMultiplier;
|
||||
let factorioTeleportZoneTopLeft = new Point_1.Point(topLeftRelativeCoordinate.X * m - m / 2, topLeftRelativeCoordinate.Y * m - m / 2);
|
||||
let factorioTeleportZoneBottomRight = new Point_1.Point(bottomRightRelativeCoordinate.X * m - m / 2, bottomRightRelativeCoordinate.Y * m - m / 2);
|
||||
let zoneWidth = factorioTeleportZoneBottomRight.X - factorioTeleportZoneTopLeft.X;
|
||||
let zoneHeight = factorioTeleportZoneBottomRight.Y - factorioTeleportZoneTopLeft.Y;
|
||||
let tl = factorioTeleportZoneTopLeft;
|
||||
let br = factorioTeleportZoneBottomRight;
|
||||
let zw = Config.TeleportZoneWidth;
|
||||
switch (this.Direction) {
|
||||
case TeleportZoneDirection_1.TeleportZoneDirection.North:
|
||||
return [this.Name, tl.X, tl.Y, zoneWidth, zw, this.Index + 1];
|
||||
case TeleportZoneDirection_1.TeleportZoneDirection.West:
|
||||
return [this.Name, tl.X, tl.Y, zw, zoneHeight, this.Index + 1];
|
||||
case TeleportZoneDirection_1.TeleportZoneDirection.South:
|
||||
return [this.Name, tl.X, br.Y - zw, zoneWidth, zw, this.Index + 1];
|
||||
case TeleportZoneDirection_1.TeleportZoneDirection.East:
|
||||
return [this.Name, br.X - zw, tl.Y, zw, zoneHeight, this.Index + 1];
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Generates a zone restriction from this teleport zone to target zone
|
||||
* @returns [zoneIndex, targetNodeName, targetZoneName]
|
||||
*/
|
||||
GenerateZoneRestriction() {
|
||||
let targetZone = this.TargetNode.TeleportZones.getValue(this.ParentNode.Key);
|
||||
return [this.Index + 1, this.TargetNode.Name, targetZone.Name];
|
||||
}
|
||||
}
|
||||
exports.TeleportZone = TeleportZone;
|
||||
//# sourceMappingURL=TeleportZone.js.map
|
||||
@@ -1,10 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var TeleportZoneDirection;
|
||||
(function (TeleportZoneDirection) {
|
||||
TeleportZoneDirection[TeleportZoneDirection["North"] = 0] = "North";
|
||||
TeleportZoneDirection[TeleportZoneDirection["East"] = 1] = "East";
|
||||
TeleportZoneDirection[TeleportZoneDirection["South"] = 2] = "South";
|
||||
TeleportZoneDirection[TeleportZoneDirection["West"] = 3] = "West";
|
||||
})(TeleportZoneDirection = exports.TeleportZoneDirection || (exports.TeleportZoneDirection = {}));
|
||||
//# sourceMappingURL=TeleportZoneDirection.js.map
|
||||
@@ -1,10 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
class TeleportZoneRestriction {
|
||||
constructor(parentTeleportZone, restrictingTeleportZone) {
|
||||
this.ParentTeleportZone = parentTeleportZone;
|
||||
this.RestrictingTeleportZone = restrictingTeleportZone;
|
||||
}
|
||||
}
|
||||
exports.TeleportZoneRestriction = TeleportZoneRestriction;
|
||||
//# sourceMappingURL=TeleportZoneRestriction.js.map
|
||||
@@ -1,13 +0,0 @@
|
||||
"use strict";
|
||||
function __export(m) {
|
||||
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
|
||||
}
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
__export(require("./ClusterManager"));
|
||||
__export(require("./NodeInstance"));
|
||||
__export(require("./Server"));
|
||||
__export(require("./TeleportZone"));
|
||||
__export(require("./TeleportZoneRestriction"));
|
||||
__export(require("./Point"));
|
||||
__export(require("./Grid"));
|
||||
//# sourceMappingURL=index.js.map
|
||||
@@ -1,28 +0,0 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
// const config = require("./../config");
|
||||
|
||||
module.exports = function(config){
|
||||
return {
|
||||
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();
|
||||
});
|
||||
},
|
||||
check: async function(token){
|
||||
return new Promise((resolve, reject) => {
|
||||
jwt.verify(token, config.masterAuthSecret, function(err, decoded) {
|
||||
if(err){
|
||||
resolve({ok:false, msg:err})
|
||||
} else {
|
||||
resolve({ok:true, msg:"Successfully authenticated"});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
module.exports = async function checkHotpatchInstallation(messageInterface){
|
||||
let yn = await messageInterface("/silent-command if remote.interfaces['hotpatch'] then rcon.print('true') else rcon.print('false') end");
|
||||
yn = yn.replace(/(\r\n\t|\n|\r\t)/gm, "");
|
||||
if(yn == "true"){
|
||||
return true;
|
||||
} else if(yn == "false"){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
const fs = require("fs-extra");
|
||||
|
||||
const getLua = require("./getLua.js");
|
||||
|
||||
let commandCache;
|
||||
module.exports = async function getCommand(file, COMPRESS = true){
|
||||
commandCache = commandCache || {};
|
||||
if(!commandCache[file]){
|
||||
commandCache[file] = await getLua(file);
|
||||
return commandCache[file];
|
||||
} else if(typeof commandCache[file] == "string"){
|
||||
return commandCache[file];
|
||||
} else {
|
||||
throw new Error("Command not found");
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
const needle = require("needle");
|
||||
|
||||
module.exports = function(config){
|
||||
const instances = [];
|
||||
|
||||
if(config){
|
||||
return function getInstanceName(instanceID){
|
||||
return new Promise((resolve, reject) => {
|
||||
let instance = instances[instanceID];
|
||||
if(!instance){
|
||||
needle.get(config.masterIP+":"+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")); reject(new Error())}
|
||||
try {
|
||||
for (let index in response.body)
|
||||
instances[index] = response.body[index].instanceName;
|
||||
} catch (e){
|
||||
console.log(e);
|
||||
return null;
|
||||
}
|
||||
instance = instances[instanceID]
|
||||
if (!instance) instance = instanceID; //somehow the master doesn't know the instance
|
||||
resolve(instance);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve(instance);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return async function thisAintGonnaWork(){
|
||||
let err = new Error("getInstanceName from lib/clusterTools needs to be called with {config} to work");
|
||||
console.log(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
const fs = require("fs-extra");
|
||||
|
||||
module.exports = function getLua(filePath, COMPRESS_LUA = true){
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(filePath, "utf8", (err, contents) => {
|
||||
if(err){
|
||||
reject(err);
|
||||
} else {
|
||||
// split content into lines
|
||||
contents = contents.split(/\r?\n/);
|
||||
|
||||
// join those lines after making them safe again
|
||||
contents = contents.reduce((acc, val) => {
|
||||
val = val.replace(/\\/g ,'\\\\');
|
||||
// remove leading and trailing spaces
|
||||
val = val.trim();
|
||||
// escape single quotes
|
||||
val = val.replace(/'/g ,'\\\'');
|
||||
|
||||
// remove single line comments
|
||||
let singleLineCommentPosition = val.indexOf("--");
|
||||
let multiLineCommentPosition = val.indexOf("--[[");
|
||||
|
||||
if(multiLineCommentPosition === -1 && singleLineCommentPosition !== -1) {
|
||||
val = val.substr(0, singleLineCommentPosition);
|
||||
}
|
||||
|
||||
return acc + val + '\\n';
|
||||
}, ""); // need the "" or it will not process the first row, potentially leaving a single line comment in that disables the whole code
|
||||
|
||||
// console.log(contents);
|
||||
|
||||
// this takes about 46 ms to minify train_stop_tracking.lua in my tests on an i3 6100u
|
||||
if(COMPRESS_LUA) contents = require("luamin").minify(contents);
|
||||
|
||||
resolve(contents);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
/**
|
||||
|
||||
Functions frequently needed from different places in the cluster. Ex:
|
||||
* Loading Lua scripts
|
||||
* Checking if hotpatch is installed
|
||||
* Getting an instance's displayname from their ID
|
||||
|
||||
*/
|
||||
|
||||
module.exports = ({config} = {}) => ({
|
||||
/**
|
||||
async function to get Lua from a file, remove comments, minify and return.
|
||||
|
||||
arg1 is the file path
|
||||
arg2 is whether or not to compress the Lua. Makes debugging harder, but reduces bandwidth when many players are online. Recommended for polling commands. Defaults to true.
|
||||
Promise resolves with a string of Lua.
|
||||
*/
|
||||
getLua: require("./getLua.js"),
|
||||
/**
|
||||
async function to check whether hotpatch is installed in a server.
|
||||
|
||||
First argument is the servers async messageInterface for rcon return values
|
||||
Promise resolves with boolean.
|
||||
*/
|
||||
checkHotpatchInstallation: require("./checkHotpatchInstallation.js"),
|
||||
/**
|
||||
async function to get an instances displayname from their instanceID
|
||||
|
||||
arg1 is the instanceID
|
||||
Promise resolves with a string, either the displayName or instanceID if not found.
|
||||
*/
|
||||
getInstanceName: require("./getInstanceName.js")(config),
|
||||
/**
|
||||
async function same as getLua, but using an in memory cache to lower FS operations.
|
||||
|
||||
arg1 is the file path
|
||||
arg2 is whether or not to compress, default true
|
||||
Promise resolves with a string of Lua.
|
||||
*/
|
||||
getCommand: require("./getCommand.js"),
|
||||
})
|
||||
@@ -1,119 +0,0 @@
|
||||
//Sigmoids...
|
||||
|
||||
function sigmoid0(x)
|
||||
{
|
||||
return x/(1+Math.abs(x));
|
||||
}
|
||||
function sigmoid1(x)
|
||||
{
|
||||
if (x>=0.0) {
|
||||
return 1.0
|
||||
} else {
|
||||
return 0.0
|
||||
}
|
||||
}
|
||||
function sigmoid2(x)
|
||||
{
|
||||
if (x>=0.0){
|
||||
return x
|
||||
} else {
|
||||
return 0.0
|
||||
}
|
||||
}
|
||||
function sigmoid3(x)
|
||||
{
|
||||
return x;
|
||||
}
|
||||
|
||||
//DOSE - Doses items for item X slave Y
|
||||
//Numreq - number of items that are requested for item X for slave Y
|
||||
//Instore - number of items for item X in master gauge (currently)
|
||||
//store_last_tick - Number of items for item X in master gauge at time of last tick
|
||||
//dole - Current dole for item X
|
||||
//carry - carry for item X slave Y
|
||||
//prev_req - Numreq for previous request for item X slave Y
|
||||
//* doleinfo_last - Object with 2 values for last "tick period": numreq - total number or requests for item, numslave - number of slaves requesting that item
|
||||
//debt - debt for item X slave Y
|
||||
|
||||
//Returns: Array of 3 elements
|
||||
//0.Number of items to give in that dose
|
||||
//1.New dole for item X
|
||||
//2.New carry for item X slave Y
|
||||
//3.New debt
|
||||
function Dose(numreq,instore,store_last_tick,dole,carry,prev_req,numreq_total_adj,debt)
|
||||
{
|
||||
numreq=Number(numreq);
|
||||
instore=Number(instore);
|
||||
var instore_adj=instore;
|
||||
if (instore_adj==0) instore_adj=0.1;
|
||||
|
||||
var b=Math.trunc(prev_req/instore_adj*numreq);
|
||||
var inp1=[numreq,instore_adj,store_last_tick/numreq,dole,carry];//0..4
|
||||
var inp2=[prev_req/numreq,numreq/numreq_total_adj,store_last_tick/numreq_total_adj,prev_req/instore_adj,b,numreq_total_adj,store_last_tick,debt];//5..12
|
||||
var inputs=inp1.concat(inp2);
|
||||
var internal=new Array(20).fill(0);
|
||||
var outputs=[0,0,0,0];
|
||||
//NN: Please don't touch :), code is generated
|
||||
|
||||
outputs[0]=inputs[7]*0.8058582544;
|
||||
outputs[0]=sigmoid2(outputs[0]);
|
||||
outputs[1]=sigmoid0(outputs[1]);
|
||||
outputs[2]=sigmoid0(outputs[2]);
|
||||
outputs[3]=inputs[6]*0.2294657379;
|
||||
outputs[3]=sigmoid0(outputs[3]);
|
||||
|
||||
debt=debt || 0;
|
||||
if (outputs[0]>0.99) outputs[0]=1.0;
|
||||
|
||||
var res1=Number(outputs[0])*numreq+outputs[3]+debt;
|
||||
var res_capped=Math.round(res1);
|
||||
|
||||
if (res_capped>numreq) res_capped=numreq;
|
||||
if (res_capped>instore) res_capped=instore;
|
||||
if (res_capped<0) res_capped=0;
|
||||
|
||||
res1=res1 || 0;
|
||||
res_capped=res_capped || 0;//Safeguard against wrong values
|
||||
debt=res1-res_capped;
|
||||
debt_cap=Math.round(numreq*1.5)+5;
|
||||
if (debt>debt_cap) debt=debt_cap;
|
||||
|
||||
carry=sigmoid0(outputs[2]*carry+outputs[0]) || 0;
|
||||
return [res_capped,dole+outputs[1],carry,debt];
|
||||
}
|
||||
|
||||
//TICK - Updates values for item X, should be called once for each item for each round of all slaves requesting that item
|
||||
//Instore - number of items for item X in master gauge (currently)
|
||||
//dole - Current dole for item X
|
||||
//store_last_tick - Number of items for item X in master gauge at time of last tick (not this one)
|
||||
//* doleinfo_last - Object with 2 values for last "tick period": numreq - total number or requests for item, numslave - number of slaves requesting that item
|
||||
//* doleinfo_new - Object with 2 values for new "tick period": numreq - total number or requests for item, numslave - number of slaves requesting that item
|
||||
|
||||
|
||||
//Returns: Array of 2 elements
|
||||
//0.New dole for item X
|
||||
//1.Guessed percentage average of items supplied in last 10 ticks
|
||||
function Tick(instore,dole,store_last_tick,numreq_total_adj)
|
||||
{
|
||||
var instore_adj=instore;
|
||||
if (instore_adj==0) instore_adj=0.1;
|
||||
var inputs=[instore,dole,store_last_tick/instore_adj,store_last_tick/numreq_total_adj,store_last_tick,numreq_total_adj];
|
||||
var internal=new Array(10).fill(0);
|
||||
var outputs=[0,0];
|
||||
//NN: Please don't touch :), code is generated
|
||||
|
||||
outputs[0]=inputs[3]*0.9364774823;
|
||||
outputs[0]=outputs[0]*1.0*0.4590721726;
|
||||
outputs[0]=sigmoid2(outputs[0]);
|
||||
outputs[1]=outputs[0]*1.828422785;
|
||||
outputs[1]=sigmoid3(outputs[1]);
|
||||
|
||||
avg=outputs[1];
|
||||
if (numreq_total_adj<0.11) avg=1;//When no item's are requested, result is 0.1 ; In that case there is no deficit, so return 1 = 100% demand fulfilled
|
||||
if (avg>1) avg=1;
|
||||
if (avg<0) avg=0;
|
||||
|
||||
return [sigmoid0(outputs[0]),avg];
|
||||
}
|
||||
|
||||
module.exports = {Dose,Tick}
|
||||
@@ -1,167 +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 fileOps = require("lib/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,113 +0,0 @@
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("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 = path.relative('.', __dirname);
|
||||
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 = path.relative('.', __dirname);
|
||||
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,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("lib/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,17 +0,0 @@
|
||||
const fileOps = require("lib/fileOps");
|
||||
// const config = require("./../config");
|
||||
|
||||
module.exports = function(config) {
|
||||
return {
|
||||
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;
|
||||
},
|
||||
}
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves to the SHA1 hash of the file given by path
|
||||
*/
|
||||
exports.hashFile = function(path) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
let hasher = crypto.createHash('sha1')
|
||||
hasher.setEncoding('hex')
|
||||
hasher.on('finish', function() {
|
||||
resolve(hasher.read());
|
||||
});
|
||||
|
||||
let stream = fs.createReadStream(path);
|
||||
stream.on('error', function(error) {
|
||||
// the docs doesn't say anything about what's passed on error
|
||||
reject(error);
|
||||
});
|
||||
stream.pipe(hasher);
|
||||
});
|
||||
}
|
||||
@@ -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,69 +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){
|
||||
console.log(asTable(this.getConfig(instance)));
|
||||
}
|
||||
module.exports.getConfig = async 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]});
|
||||
}
|
||||
});
|
||||
return 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));
|
||||
return {
|
||||
ok:true,
|
||||
msg:`Changed config entry ${entry} from Object to ${JSON.stringify(newValue)}`,
|
||||
}
|
||||
} 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));
|
||||
return {
|
||||
ok:true,
|
||||
msg:`Changed config entry ${entry} from ***** to '${newValue}'`,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
ok:false,
|
||||
msg:`No such config parameter`,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
ok:false,
|
||||
msg:`Editing config for single instances is not yet supported`,
|
||||
}
|
||||
}
|
||||
};
|
||||
Whitespace-only changes.
@@ -1,2 +0,0 @@
|
||||
const fs = require("fs-extra");
|
||||
const needle = require("needle");
|
||||
@@ -1,277 +0,0 @@
|
||||
/**
|
||||
Provides tools to manage mods in a clusterio install
|
||||
@module modManager
|
||||
*/
|
||||
|
||||
const asTable = require("as-table").configure({delimiter: ' | '});
|
||||
const moment = require("moment");
|
||||
const syncRequest = require("sync-request");
|
||||
const fs = require("fs-extra");
|
||||
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("lib/fileOps");
|
||||
// const config = require("./../../config");
|
||||
|
||||
|
||||
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]};
|
||||
|
||||
|
||||
module.exports = function(config){
|
||||
if(!config) config = require("./../../config");
|
||||
const instances = require("./../getInstances.js")(config).sync();
|
||||
return {
|
||||
/**
|
||||
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")(config);
|
||||
modManager.listMods("testInstance");
|
||||
// console.logs table of mods installed on instance
|
||||
modManager.listMods("shared");
|
||||
// console.logs mods located in /sharedMods as a nice table
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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));
|
||||
});
|
||||
});
|
||||
},
|
||||
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"));
|
||||
}
|
||||
});
|
||||
},
|
||||
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
|
||||
*/
|
||||
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()))
|
||||
}*/
|
||||
Whitespace-only changes.
@@ -1,116 +0,0 @@
|
||||
const fs = require("fs-extra");
|
||||
const path = require("path");
|
||||
const git = require("simple-git")("./sharedPlugins");
|
||||
|
||||
|
||||
module.exports = function(config){
|
||||
if(!config) config = require("./../../config");
|
||||
|
||||
return {
|
||||
listPlugins: async function(){
|
||||
let plugins = await this.getPlugins();
|
||||
plugins.forEach(plug => {
|
||||
console.log(`${plug.name}`.padEnd(22).substr(0,22)+` - ${plug.description}`.substr(0,80-22));
|
||||
});
|
||||
},
|
||||
getPlugins: async function(){
|
||||
let directories = await fs.readdir("./sharedPlugins");
|
||||
let returnArray = [];
|
||||
for(let i in directories){
|
||||
let dir = directories[i];
|
||||
let pluginContents = await fs.readdir(path.join("./sharedPlugins", dir));
|
||||
// console.log(pluginContents)
|
||||
if(pluginContents.includes("config.js") || pluginContents.includes("config.json")){
|
||||
try{
|
||||
var pluginConfig = require(path.join("./../../sharedPlugins", dir, "config"));
|
||||
pluginConfig.pluginPath = path.join("./sharedPlugins", dir);
|
||||
}catch(e){
|
||||
console.log(e)
|
||||
}
|
||||
|
||||
// check if plugin is enabled
|
||||
try{
|
||||
await fs.stat(path.join(pluginConfig.pluginPath, "DISABLED"))
|
||||
pluginConfig.enabled = false;
|
||||
}catch(e){
|
||||
pluginConfig.enabled = true;
|
||||
}
|
||||
returnArray.push(pluginConfig);
|
||||
}
|
||||
}
|
||||
return returnArray;
|
||||
},
|
||||
addPlugin: function(path){
|
||||
return new Promise((resolve, reject) => {
|
||||
if(path.includes("git")){ // we are decently sure this is a git repo
|
||||
git.clone(path, data => resolve(data))
|
||||
}
|
||||
});
|
||||
},
|
||||
removePlugin: async function(name){
|
||||
let plugins = await this.getPlugins();
|
||||
for(let i in plugins){
|
||||
let plug = plugins[i];
|
||||
if(plug.name.includes(name)){
|
||||
// console.log(`Removing plugin ${plug.name}`);
|
||||
await fs.remove(plug.pluginPath);
|
||||
return {
|
||||
ok:true,
|
||||
msg:`Deleted plugin ${plug.name}`
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok:false,
|
||||
msg:`Plugin matching "${name}" not found`,
|
||||
}
|
||||
},
|
||||
enablePlugin: async function(name, instance){
|
||||
let plugins = await this.getPlugins();
|
||||
let plugin = plugins[findInArray("name", name, plugins)[0]];
|
||||
if(plugin){
|
||||
let files = await fs.readdir(plugin.pluginPath);
|
||||
if(files.includes("DISABLED")){
|
||||
await fs.unlink(path.join(plugin.pluginPath, "DISABLED"));
|
||||
return {
|
||||
ok:true,
|
||||
msg:`Globally enabled plugin ${name}`,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
ok:true,
|
||||
msg:`Plugin ${name} already enabled`,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
ok:false,
|
||||
msg:`Found no excact match for plugin ${name}`,
|
||||
}
|
||||
}
|
||||
},
|
||||
disablePlugin: async function(name, instance){
|
||||
let plugins = await this.getPlugins();
|
||||
let plugin = plugins[findInArray("name", name, plugins)[0]];
|
||||
if(plugin){
|
||||
await fs.writeFile(path.join(plugin.pluginPath, "DISABLED"), "");
|
||||
return {
|
||||
ok:true,
|
||||
msg:`Plugin ${name} is now disabled on this host`,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
ok:false,
|
||||
msg:`Found no excact match for plugin ${name}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function findInArray(key, value, array){
|
||||
let indexes = [];
|
||||
for(let i in array){
|
||||
if(array[i][key] && array[i][key] === value) indexes.push(i);
|
||||
}
|
||||
return indexes;
|
||||
}
|
||||
@@ -1,135 +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 = [
|
||||
"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,5 +0,0 @@
|
||||
{
|
||||
"name": "lib",
|
||||
"description": "Internal library for factorioClusterio"
|
||||
}
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
--[[
|
||||
|
||||
Copyright 2018 Chrisgbk
|
||||
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.
|
||||
|
||||
]]
|
||||
-- MIT License, https://opensource.org/licenses/MIT
|
||||
|
||||
-- Hotpatchable control.lua for scenarios
|
||||
-- Supports multiple simultaneously loaded softmods
|
||||
-- This is a WIP
|
||||
-- Version 1.1 alpha
|
||||
-- probably some performance improvements to be made
|
||||
|
||||
_ENV.debug_settings = {
|
||||
level = 'info',
|
||||
log_to_console_only = false,
|
||||
log_to_RCON = false,
|
||||
log_on_tick = false,
|
||||
}
|
||||
|
||||
local hotpatch_tools = require 'hotpatch.mod-tools'
|
||||
require 'hotpatch.remote-interface'
|
||||
require 'hotpatch.commands'
|
||||
require 'hotpatch.gui'
|
||||
|
||||
-- mod code goes here
|
||||
|
||||
-- This is the Factorio Freeplay scenario
|
||||
-- This code, and the freeplay locale files in the zip file, is the property of Wube
|
||||
-- As this is part of the base mod, the developers have given permission for modders to use/adapt the assets therein, for modding Factorio
|
||||
-- Any other use would require licensing/permission from Wube
|
||||
-- If you don't want to run this, either comment this out before creating your scenario, OR uninstall freeplay at runtime
|
||||
hotpatch_tools.static_mod('freeplay', '0.17.50', [===[
|
||||
local util = require("util")
|
||||
local silo_script = require("silo-script")
|
||||
|
||||
local created_items = function()
|
||||
return
|
||||
{
|
||||
["iron-plate"] = 8,
|
||||
["wood"] = 1,
|
||||
["pistol"] = 1,
|
||||
["firearm-magazine"] = 10,
|
||||
["burner-mining-drill"] = 1,
|
||||
["stone-furnace"] = 1
|
||||
}
|
||||
end
|
||||
|
||||
local respawn_items = function()
|
||||
return
|
||||
{
|
||||
["pistol"] = 1,
|
||||
["firearm-magazine"] = 10
|
||||
}
|
||||
end
|
||||
|
||||
for k,v in pairs(silo_script.get_events()) do
|
||||
script.on_event(k, v)
|
||||
end
|
||||
|
||||
script.on_event(defines.events.on_player_created, function(event)
|
||||
local player = game.players[event.player_index]
|
||||
util.insert_safe(player, global.created_items)
|
||||
|
||||
local r = global.chart_distance or 200
|
||||
player.force.chart(player.surface, {{player.position.x - r, player.position.y - r}, {player.position.x + r, player.position.y + r}})
|
||||
|
||||
if not global.skip_intro then
|
||||
if game.is_multiplayer() then
|
||||
player.print({"msg-intro"})
|
||||
else
|
||||
game.show_message_dialog{text = {"msg-intro"}}
|
||||
end
|
||||
end
|
||||
|
||||
silo_script.on_event(event)
|
||||
end)
|
||||
|
||||
script.on_event(defines.events.on_player_respawned, function(event)
|
||||
local player = game.players[event.player_index]
|
||||
util.insert_safe(player, global.respawn_items)
|
||||
silo_script.on_event(event)
|
||||
end)
|
||||
|
||||
script.on_configuration_changed(function(event)
|
||||
global.created_items = global.created_items or created_items()
|
||||
global.respawn_items = global.respawn_items or respawn_items()
|
||||
silo_script.on_configuration_changed(event)
|
||||
end)
|
||||
|
||||
script.on_load(function()
|
||||
silo_script.on_load()
|
||||
end)
|
||||
|
||||
script.on_init(function()
|
||||
global.created_items = created_items()
|
||||
global.respawn_items = respawn_items()
|
||||
silo_script.on_init()
|
||||
end)
|
||||
|
||||
silo_script.add_remote_interface()
|
||||
silo_script.add_commands()
|
||||
|
||||
remote.add_interface("freeplay",
|
||||
{
|
||||
get_created_items = function()
|
||||
return global.created_items
|
||||
end,
|
||||
set_created_items = function(map)
|
||||
global.created_items = map
|
||||
end,
|
||||
get_respawn_items = function()
|
||||
return global.respawn_items
|
||||
end,
|
||||
set_respawn_items = function(map)
|
||||
global.respawn_items = map
|
||||
end,
|
||||
set_skip_intro = function(bool)
|
||||
global.skip_intro = bool
|
||||
end,
|
||||
set_chart_distance = function(value)
|
||||
global.chart_distance = tonumber(value)
|
||||
end
|
||||
})
|
||||
|
||||
]===])
|
||||
|
||||
-- Multi-file test
|
||||
--[[
|
||||
local files = {}
|
||||
|
||||
files['testfolder.anothertest.test'] = [===[
|
||||
script.on_event(defines.events.on_player_changed_position, function(e) game.print('position changed') end)
|
||||
]===]
|
||||
|
||||
files['testfolder.test'] = [===[
|
||||
require 'anothertest.test'
|
||||
]===]
|
||||
|
||||
files['test'] = [===[
|
||||
require 'testfolder.test'
|
||||
]===]
|
||||
|
||||
hotpatch_tools.static_mod('require-test', '1.0.0', [===[
|
||||
require 'test'
|
||||
]===], files)
|
||||
--]]
|
||||
|
||||
-- end of mod code
|
||||
@@ -1,135 +0,0 @@
|
||||
local hotpatch_tools = require 'hotpatch.mod-tools'
|
||||
hotpatch_tools.static_mod('hotpatch-commands', '1.0.2', [===[
|
||||
--[[
|
||||
|
||||
Copyright 2018 Chrisgbk
|
||||
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.
|
||||
|
||||
]]
|
||||
-- MIT License, https://opensource.org/licenses/MIT
|
||||
|
||||
local hotpatch_tools = require 'hotpatch.mod-tools'
|
||||
|
||||
--load private API
|
||||
-- mod installation/uninstallation support functions
|
||||
-- These take a mod NAME as a first argument
|
||||
local install_mod = hotpatch_tools.install_mod
|
||||
local find_installed_mod = hotpatch_tools.find_installed_mod
|
||||
local install_mod_file = hotpatch_tools.install_mod_file
|
||||
local uninstall_mod = hotpatch_tools.uninstall_mod
|
||||
|
||||
-- mod interaction functions
|
||||
-- These take a LOADED INDEX as a first argument, except load_mod, which takes an INSTALLED INDEX
|
||||
local load_mod = hotpatch_tools.load_mod
|
||||
local find_loaded_mod = hotpatch_tools.find_loaded_mod
|
||||
local run_mod = hotpatch_tools.run_mod
|
||||
local reset_mod = hotpatch_tools.reset_mod
|
||||
local reset_mod_events = hotpatch_tools.reset_mod_events
|
||||
local register_mod_events = hotpatch_tools.register_mod_events
|
||||
local unload_mod = hotpatch_tools.unload_mod
|
||||
|
||||
-- internal callbacks when a mod registers events
|
||||
local register_event = hotpatch_tools.register_event
|
||||
local register_nth_tick = hotpatch_tools.register_nth_tick
|
||||
local register_on_tick = hotpatch_tools.register_on_tick
|
||||
|
||||
-- mod bootstrap functions
|
||||
-- These take a LOADED INDEX as a first argument
|
||||
local mod_on_init = hotpatch_tools.mod_on_init
|
||||
local mod_on_load = hotpatch_tools.mod_on_load
|
||||
local mod_on_configuration_changed = hotpatch_tools.mod_on_configuration_changed
|
||||
|
||||
local static_mods = hotpatch_tools.static_mods
|
||||
local console = hotpatch_tools.console
|
||||
local debug_log = hotpatch_tools.debug_log
|
||||
local loaded_mods = hotpatch_tools.loaded_mods
|
||||
local installed_mods = hotpatch_tools.installed_mods
|
||||
|
||||
local help_commands = {
|
||||
help = '/hotpatch help\nYou got this far, you probably know all you need to know',
|
||||
list = '/hotpatch list [type]\nLists \'installed\', \'loaded\' or \'running\' mods',
|
||||
}
|
||||
|
||||
local sub_commands
|
||||
sub_commands = {
|
||||
help = function(player_index, param)
|
||||
local caller = (player_index and game.players[player_index]) or console
|
||||
if (not param) or (param == '') then
|
||||
caller.print('Usage: /hotpatch [command] [parameters]')
|
||||
caller.print('Available commands:')
|
||||
for k, _ in pairs(sub_commands) do caller.print(k) end
|
||||
caller.print('Use /hotpatch help [command] for help on a particular command')
|
||||
else
|
||||
local text = help_commands[param]
|
||||
if text then
|
||||
caller.print(text)
|
||||
end
|
||||
end
|
||||
end,
|
||||
list = function(player_index, param)
|
||||
local caller = (player_index and game.players[player_index]) or console
|
||||
if not param or (param == '') then
|
||||
caller.print('Installed mods:')
|
||||
for k, v in pairs(installed_mods) do
|
||||
caller.print(table.concat{'[', k, '] ', v.name, ' ', v.version})
|
||||
end
|
||||
caller.print('Loaded/Running mods:')
|
||||
for k, v in pairs(loaded_mods) do
|
||||
caller.print(table.concat{'[', k, '] ', v.name, ' ', v.version, ' ', (v.loaded and 'loaded') or 'not loaded', ' ', (v.running and 'running') or 'not running'})
|
||||
end
|
||||
else
|
||||
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
local admin_commands = {
|
||||
reinstall = function(player_index, param)
|
||||
local caller = (player_index and game.players[player_index]) or console
|
||||
local mod
|
||||
local installed_index
|
||||
local loaded_index
|
||||
for i = 1, #static_mods do
|
||||
mod = static_mods[i]
|
||||
if mod.name:match('^hotpatch%-.*$') then
|
||||
loaded_index = find_loaded_mod(mod.name)
|
||||
if loaded_index then
|
||||
unload_mod(loaded_index)
|
||||
end
|
||||
install_mod(mod.name, mod.version, mod.code, mod.files)
|
||||
installed_index = find_installed_mod(mod.name)
|
||||
load_mod(installed_index)
|
||||
loaded_index = find_loaded_mod(mod.name)
|
||||
run_mod(loaded_index)
|
||||
mod_on_init(loaded_index)
|
||||
end
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
_ENV.commands.add_command('hotpatch', 'Commands for hotpatch. Run /hotpatch help for details.', function(e)
|
||||
local caller = (e.player_index and game.players[e.player_index]) or console
|
||||
if (not e.parameter) or (e.parameter == '') then
|
||||
sub_commands['help'](e.player_index)
|
||||
return
|
||||
end
|
||||
local sub_command, rest = e.parameter:match('(.-)%s(.*)')
|
||||
if not sub_command then
|
||||
sub_command = e.parameter
|
||||
end
|
||||
|
||||
local f = sub_commands[sub_command]
|
||||
if f then f(e.player_index, rest) end
|
||||
if caller.admin then
|
||||
f = admin_commands[sub_command]
|
||||
if f then f(e.player_index, rest) end
|
||||
end
|
||||
end)
|
||||
|
||||
--]===])
|
||||
|
||||
return true
|
||||
@@ -1,377 +0,0 @@
|
||||
local hotpatch_tools = require 'hotpatch.mod-tools'
|
||||
hotpatch_tools.static_mod('hotpatch-gui', '1.0.6', [===[
|
||||
--[[
|
||||
|
||||
Copyright 2018 Chrisgbk
|
||||
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.
|
||||
|
||||
]]
|
||||
-- MIT License, https://opensource.org/licenses/MIT
|
||||
|
||||
local hotpatch_tools = require 'hotpatch.mod-tools'
|
||||
local mod_gui = require 'mod-gui'
|
||||
|
||||
--load private API
|
||||
-- mod installation/uninstallation support functions
|
||||
-- These take a mod NAME as a first argument
|
||||
local install_mod = hotpatch_tools.install_mod
|
||||
local find_installed_mod = hotpatch_tools.find_installed_mod
|
||||
local install_mod_file = hotpatch_tools.install_mod_file
|
||||
local uninstall_mod = hotpatch_tools.uninstall_mod
|
||||
|
||||
-- mod interaction functions
|
||||
-- These take a LOADED INDEX as a first argument, except load_mod, which takes an INSTALLED INDEX
|
||||
local load_mod = hotpatch_tools.load_mod
|
||||
local find_loaded_mod = hotpatch_tools.find_loaded_mod
|
||||
local run_mod = hotpatch_tools.run_mod
|
||||
local reset_mod = hotpatch_tools.reset_mod
|
||||
local reset_mod_events = hotpatch_tools.reset_mod_events
|
||||
local register_mod_events = hotpatch_tools.register_mod_events
|
||||
local unload_mod = hotpatch_tools.unload_mod
|
||||
|
||||
-- internal callbacks when a mod registers events
|
||||
local register_event = hotpatch_tools.register_event
|
||||
local register_nth_tick = hotpatch_tools.register_nth_tick
|
||||
local register_on_tick = hotpatch_tools.register_on_tick
|
||||
|
||||
-- mod bootstrap functions
|
||||
-- These take a LOADED INDEX as a first argument
|
||||
local mod_on_init = hotpatch_tools.mod_on_init
|
||||
local mod_on_load = hotpatch_tools.mod_on_load
|
||||
local mod_on_configuration_changed = hotpatch_tools.mod_on_configuration_changed
|
||||
|
||||
local static_mods = hotpatch_tools.static_mods
|
||||
local console = hotpatch_tools.console
|
||||
local debug_log = hotpatch_tools.debug_log
|
||||
local loaded_mods = hotpatch_tools.loaded_mods
|
||||
local installed_mods = hotpatch_tools.installed_mods
|
||||
|
||||
script.on_event(defines.events.on_player_joined_game, function(e)
|
||||
local player = game.players[e.player_index]
|
||||
local top = mod_gui.get_button_flow(player)
|
||||
local left = mod_gui.get_frame_flow(player)
|
||||
local center = player.gui.center
|
||||
|
||||
local button = top['hotpatch-button']
|
||||
if button then
|
||||
button.destroy()
|
||||
end
|
||||
local menu = left['hotpatch-menu']
|
||||
if menu then
|
||||
menu.destroy()
|
||||
end
|
||||
|
||||
local IDE = center['hotpatch-IDE']
|
||||
if IDE then
|
||||
IDE.destroy()
|
||||
end
|
||||
|
||||
local main = center['hotpatch-main']
|
||||
if main then
|
||||
main.destroy()
|
||||
end
|
||||
|
||||
button = top.add{type = 'sprite-button', name = 'hotpatch-button', sprite='utility/heat_exchange_indication', tooltip = 'Hotpatch', style = mod_gui.button_style}
|
||||
end)
|
||||
|
||||
local on_gui_click_handlers
|
||||
local on_gui_selection_state_changed_handlers
|
||||
|
||||
on_gui_click_handlers = {
|
||||
['hotpatch-button'] = function(e)
|
||||
--on_gui_click_handlers['hotpatch-menu.IDE'](e)
|
||||
--do return end
|
||||
local player = game.players[e.player_index]
|
||||
local left = mod_gui.get_frame_flow(player)
|
||||
local center = player.gui.center
|
||||
|
||||
local menu = left['hotpatch-menu']
|
||||
if not menu then
|
||||
menu = left.add{type = 'frame', name = 'hotpatch-menu', direction = 'vertical'}
|
||||
menu.add{type = 'button', name = 'hotpatch-menu.IDE', caption = 'Mod IDE', tooltip = 'Open debugging GUI'}
|
||||
menu.add{type = 'button', name = 'hotpatch-menu.console', caption = 'Mod Console', tooltip = 'Open debugging console'}
|
||||
menu.visible = true
|
||||
return
|
||||
end
|
||||
menu.visible = not menu.visible
|
||||
end,
|
||||
['hotpatch-menu'] = function(e)
|
||||
local player = game.players[e.player_index]
|
||||
local left = mod_gui.get_frame_flow(player)
|
||||
|
||||
if e.element.name ~= 'hotpatch-menu' then
|
||||
local menu = left['hotpatch-menu']
|
||||
menu.visible = not menu.visible
|
||||
on_gui_click_handlers[e.element.name](e)
|
||||
end
|
||||
end,
|
||||
['hotpatch-menu.IDE'] = function(e)
|
||||
local player = game.players[e.player_index]
|
||||
local center = player.gui.center
|
||||
|
||||
local IDE = center['hotpatch-IDE']
|
||||
if not IDE then
|
||||
IDE = center.add{type = 'frame', name = 'hotpatch-IDE', direction = 'vertical', caption = 'Hotpatch IDE', style = mod_gui.frame_style}
|
||||
local top_flow = IDE.add{type = 'flow', name = 'hotpatch-IDE-top', direction = 'horizontal'}
|
||||
top_flow.add{type = 'label', name = 'hotpatch-IDE-mod-label', caption = 'Mod: '}
|
||||
local IDE_dropdown = top_flow.add{type = 'drop-down', name = 'hotpatch-IDE-mod-selector'}
|
||||
top_flow.add{type = 'label', name = 'hotpatch-IDE-mod-version', caption = 'Version: No mod selected'}
|
||||
local IDE_table = IDE.add{type = 'table', name = 'hotpatch-IDE-table', column_count = 2}
|
||||
local files = IDE_table.add{type = 'scroll-pane', name = 'hotpatch-IDE-files', direction='vertical'}
|
||||
files.style.height = 600
|
||||
files.vertical_scroll_policy = 'always'
|
||||
files = files.add{type = 'table', name = 'hotpatch-IDE-files-table', column_count = 1}
|
||||
local code = IDE_table.add{type = 'text-box', name = 'hotpatch-IDE-code'}
|
||||
code.word_wrap = true
|
||||
files.style.width = 400
|
||||
files.style.cell_padding = 0
|
||||
files.style.top_padding = 0
|
||||
files.style.bottom_padding = 0
|
||||
code.style.width = 600
|
||||
code.style.height = 600
|
||||
IDE.visible = false
|
||||
end
|
||||
|
||||
local IDE_dropdown = IDE['hotpatch-IDE-top']['hotpatch-IDE-mod-selector']
|
||||
IDE_dropdown.clear_items()
|
||||
for _, v in ipairs(installed_mods) do
|
||||
IDE_dropdown.add_item(v.name)
|
||||
end
|
||||
|
||||
IDE.visible = not IDE.visible
|
||||
if IDE.visible then
|
||||
player.opened = IDE
|
||||
else
|
||||
player.opened = nil
|
||||
end
|
||||
end,
|
||||
['hotpatch-menu.console'] = function(e)
|
||||
local player = game.players[e.player_index]
|
||||
local center = player.gui.center
|
||||
|
||||
local hotpatch_console = center['hotpatch-console']
|
||||
if not hotpatch_console then
|
||||
hotpatch_console = center.add{type = 'frame', name = 'hotpatch-console', direction = 'vertical', caption = 'Hotpatch Console'}
|
||||
local top_flow = hotpatch_console.add{type = 'flow', name = 'hotpatch-console-top', direction = 'horizontal'}
|
||||
top_flow.add{type = 'label', name = 'hotpatch-console-mod-label', caption = 'Mod: '}
|
||||
local console_dropdown = top_flow.add{type = 'drop-down', name = 'hotpatch-console-mod-selector'}
|
||||
top_flow.add{type = 'label', name = 'hotpatch-console-mod-version', caption = 'Version: No mod selected'}
|
||||
local bottom_flow = hotpatch_console.add{type = 'flow', name = 'hotpatch-console-bottom', direction = 'vertical'}
|
||||
local output = bottom_flow.add{type = 'text-box', name = 'hotpatch-console-output'}
|
||||
local bottom_input_flow = bottom_flow.add{type = 'flow', name = 'hotpatch-console-bottom-input', direction = 'horizontal'}
|
||||
local input = bottom_input_flow.add{type = 'textfield', name = 'hotpatch-console-input'}
|
||||
bottom_input_flow.add{type = 'button', name = 'hotpatch-console-run', caption = 'Run', tooltip = 'Run Lua Code'}
|
||||
output.style.width = 800
|
||||
output.style.height = 600
|
||||
input.style.width = 600
|
||||
hotpatch_console.visible = false
|
||||
end
|
||||
|
||||
local console_dropdown = hotpatch_console['hotpatch-console-top']['hotpatch-console-mod-selector']
|
||||
console_dropdown.clear_items()
|
||||
for _, v in ipairs(loaded_mods) do
|
||||
console_dropdown.add_item(v.name)
|
||||
end
|
||||
|
||||
hotpatch_console.visible = not hotpatch_console.visible
|
||||
if hotpatch_console.visible then
|
||||
player.opened = hotpatch_console
|
||||
else
|
||||
player.opened = nil
|
||||
end
|
||||
end,
|
||||
['hotpatch-console-run'] = function(e)
|
||||
local player = game.players[e.player_index]
|
||||
local center = player.gui.center
|
||||
|
||||
local hotpatch_console = center['hotpatch-console']
|
||||
local output = hotpatch_console['hotpatch-console-bottom']['hotpatch-console-output']
|
||||
local input = hotpatch_console['hotpatch-console-bottom']['hotpatch-console-bottom-input']['hotpatch-console-input']
|
||||
local console_dropdown = hotpatch_console['hotpatch-console-top']['hotpatch-console-mod-selector']
|
||||
|
||||
if not player.admin then
|
||||
output.text = table.concat{output.text, '\n', 'Only admins!'}
|
||||
return
|
||||
end
|
||||
|
||||
if not (console_dropdown.selected_index > 0) then
|
||||
output.text = table.concat{output.text, '\n', 'Select a mod!'}
|
||||
return
|
||||
end
|
||||
|
||||
local mod_name = console_dropdown.items[console_dropdown.selected_index]
|
||||
local loaded_index = find_loaded_mod(mod_name)
|
||||
local env = loaded_mods[loaded_index].env
|
||||
local old_print = env.print
|
||||
local old_log = env.log
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
env.print = function(...)
|
||||
--local count = select('#', ...)
|
||||
local t = table.pack(...)
|
||||
for k, v in pairs(t) do
|
||||
t[k] = tostring(v)
|
||||
end
|
||||
local text = table.concat(t, '\t')
|
||||
output.text = table.concat{output.text, '\n', text}
|
||||
end
|
||||
|
||||
env.log = function(text)
|
||||
output.text = table.concat{output.text, '\n', text}
|
||||
end
|
||||
|
||||
local success
|
||||
local code, err = load(input.text, input.text, 't', env)
|
||||
if code then
|
||||
success, err = pcall(code)
|
||||
if not success then
|
||||
output.text = table.concat{output.text, '\n', err}
|
||||
end
|
||||
else
|
||||
output.text = table.concat{output.text, '\n', err}
|
||||
end
|
||||
|
||||
env.print = old_print
|
||||
env.log = old_log
|
||||
end,
|
||||
['hotpatch-IDE-file'] = function(e)
|
||||
local player = game.players[e.player_index]
|
||||
local center = player.gui.center
|
||||
|
||||
local element = e.element
|
||||
local previous
|
||||
for _, v in pairs(center['hotpatch-IDE']['hotpatch-IDE-table']['hotpatch-IDE-files']['hotpatch-IDE-files-table'].children) do
|
||||
if table.compare(v.style.font_color or {}, {r=1.0, g=1.0, b=0.0, a=1.0}) then
|
||||
previous = v
|
||||
break
|
||||
end
|
||||
end
|
||||
if previous then
|
||||
previous.style.font_color = {r=1.0, g=1.0, b=1.0, a=1.0}
|
||||
end
|
||||
element.style.font_color = {r=1.0, g=1.0, b=0.0, a=1.0}
|
||||
local file = element.name:match('.-%.(.*)')
|
||||
local selected = center['hotpatch-IDE']['hotpatch-IDE-top']['hotpatch-IDE-mod-selector']
|
||||
local mod_name = selected.items[selected.selected_index]
|
||||
local code = center['hotpatch-IDE']['hotpatch-IDE-table']['hotpatch-IDE-code']
|
||||
local index = find_installed_mod(mod_name)
|
||||
if index then
|
||||
local mod = installed_mods[index]
|
||||
if file == 'control' then
|
||||
code.text = mod.code
|
||||
else
|
||||
code.text = mod.files[file]
|
||||
end
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
script.on_event(defines.events.on_gui_click, function(e)
|
||||
local element = e.element
|
||||
if element.valid then
|
||||
local name = element.name:match('([^%.]*)%.?.-')
|
||||
local handler = on_gui_click_handlers[name]
|
||||
if handler then handler(e) end
|
||||
end
|
||||
end)
|
||||
|
||||
on_gui_selection_state_changed_handlers = {
|
||||
['hotpatch-console-mod-selector'] = function(e)
|
||||
local player = game.players[e.player_index]
|
||||
local center = player.gui.center
|
||||
|
||||
local element = e.element
|
||||
local name = element.items[element.selected_index]
|
||||
if name then
|
||||
local index = find_loaded_mod(name)
|
||||
if index then
|
||||
local mod = loaded_mods[index]
|
||||
local version_label = center['hotpatch-console']['hotpatch-console-top']['hotpatch-console-mod-version']
|
||||
version_label.caption = 'Version: ' .. mod.version
|
||||
local hotpatch_console = center['hotpatch-console']
|
||||
local output = hotpatch_console['hotpatch-console-bottom']['hotpatch-console-output']
|
||||
output.text = ''
|
||||
end
|
||||
end
|
||||
end,
|
||||
['hotpatch-IDE-mod-selector'] = function(e)
|
||||
local player = game.players[e.player_index]
|
||||
local center = player.gui.center
|
||||
|
||||
local element = e.element
|
||||
|
||||
local name = element.items[element.selected_index]
|
||||
if name then
|
||||
local index = find_installed_mod(name)
|
||||
if index then
|
||||
local mod = installed_mods[index]
|
||||
local version_label = center['hotpatch-IDE']['hotpatch-IDE-top']['hotpatch-IDE-mod-version']
|
||||
version_label.caption = 'Version: ' .. mod.version
|
||||
local list = center['hotpatch-IDE']['hotpatch-IDE-table']['hotpatch-IDE-files']['hotpatch-IDE-files-table']
|
||||
list.clear()
|
||||
local file = list.add{type = 'label', style = 'hoverable_bold_label', caption = 'control', name = 'hotpatch-IDE-file.control'}
|
||||
file.style.bottom_padding = 0
|
||||
file.style.top_padding = 0
|
||||
for k, _ in pairs(mod.files) do
|
||||
file = list.add{type = 'label', style = 'hoverable_bold_label', caption = k, name = 'hotpatch-IDE-file.' .. k}
|
||||
file.style.bottom_padding = 0
|
||||
file.style.top_padding = 0
|
||||
end
|
||||
local code = center['hotpatch-IDE']['hotpatch-IDE-table']['hotpatch-IDE-code']
|
||||
code.text = ''
|
||||
end
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
script.on_event(defines.events.on_gui_selection_state_changed, function(e)
|
||||
local element = e.element
|
||||
local handler = on_gui_selection_state_changed_handlers[element.name]
|
||||
if handler then handler(e) end
|
||||
end)
|
||||
|
||||
script.on_event(defines.events.on_gui_closed, function(e)
|
||||
local player = game.players[e.player_index]
|
||||
local element = e.element
|
||||
if e.gui_type == defines.gui_type.custom then
|
||||
if element and element.valid then
|
||||
if element.name == 'hotpatch-IDE' then
|
||||
if element.visible then
|
||||
player.play_sound{path = "utility/gui_click"}
|
||||
end
|
||||
element.visible = false
|
||||
elseif element.name == 'hotpatch-console' then
|
||||
if element.visible then
|
||||
player.play_sound{path = "utility/gui_click"}
|
||||
end
|
||||
element.visible = false
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
--]===])
|
||||
|
||||
--[===[
|
||||
-- keeping this as a reminder
|
||||
['hotpatch-menu'] = function(e)
|
||||
local player = game.players[e.player_index]
|
||||
local top = mod_gui.get_button_flow(player)
|
||||
local left = mod_gui.get_frame_flow(player)
|
||||
local center = player.gui.center
|
||||
|
||||
local menu = left['hotpatch-menu']
|
||||
menu.visible = not menu.visible
|
||||
|
||||
on_gui_click_handlers[e.element.name](e)
|
||||
end,
|
||||
--]===]
|
||||
|
||||
return true
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,322 +0,0 @@
|
||||
local hotpatch_tools = require 'hotpatch.mod-tools'
|
||||
hotpatch_tools.static_mod('hotpatch-remote-interface', '1.0.3', [===[
|
||||
--[[
|
||||
|
||||
Copyright 2018 Chrisgbk
|
||||
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.
|
||||
|
||||
]]
|
||||
-- MIT License, https://opensource.org/licenses/MIT
|
||||
|
||||
local hotpatch_tools = require 'hotpatch.mod-tools'
|
||||
|
||||
--load private API
|
||||
-- mod installation/uninstallation support functions
|
||||
-- These take a mod NAME as a first argument
|
||||
local install_mod = hotpatch_tools.install_mod
|
||||
local find_installed_mod = hotpatch_tools.find_installed_mod
|
||||
local install_mod_file = hotpatch_tools.install_mod_file
|
||||
local uninstall_mod = hotpatch_tools.uninstall_mod
|
||||
|
||||
-- mod interaction functions
|
||||
-- These take a LOADED INDEX as a first argument, except load_mod, which takes an INSTALLED INDEX
|
||||
local load_mod = hotpatch_tools.load_mod
|
||||
local find_loaded_mod = hotpatch_tools.find_loaded_mod
|
||||
local run_mod = hotpatch_tools.run_mod
|
||||
local reset_mod = hotpatch_tools.reset_mod
|
||||
local reset_mod_events = hotpatch_tools.reset_mod_events
|
||||
local register_mod_events = hotpatch_tools.register_mod_events
|
||||
local unload_mod = hotpatch_tools.unload_mod
|
||||
|
||||
-- internal callbacks when a mod registers events
|
||||
local register_event = hotpatch_tools.register_event
|
||||
local register_nth_tick = hotpatch_tools.register_nth_tick
|
||||
local register_on_tick = hotpatch_tools.register_on_tick
|
||||
|
||||
-- mod bootstrap functions
|
||||
-- These take a LOADED INDEX as a first argument
|
||||
local mod_on_init = hotpatch_tools.mod_on_init
|
||||
local mod_on_load = hotpatch_tools.mod_on_load
|
||||
local mod_on_configuration_changed = hotpatch_tools.mod_on_configuration_changed
|
||||
|
||||
local static_mods = hotpatch_tools.static_mods
|
||||
local console = hotpatch_tools.console
|
||||
local debug_log = hotpatch_tools.debug_log
|
||||
local loaded_mods = hotpatch_tools.loaded_mods
|
||||
local installed_mods = hotpatch_tools.installed_mods
|
||||
|
||||
local remote_interface = {}
|
||||
|
||||
--IMPORTANT: WHEN COPY PASTING CODE TO CONSOLE THE CODE MUST HAVE SINGLE LINE COMMENTS REMOVED
|
||||
-- FACTORIO CONSOLE STRIPS LINEFEEDS WHICH MAKES ALL THE CODE BECOME COMMENTED OUT
|
||||
--TODO: gate these behind admin permissions; if (game.player and game.player.admin) should work for console commands?
|
||||
|
||||
debug_log({'hotpatch-info.remote-installing'})
|
||||
|
||||
remote_interface['install'] = function(mod_name, mod_version, mod_code, mod_files, only_install)
|
||||
-- this installs a new mod and runs on_init, then registers events
|
||||
-- Note that mods may expect that certain events haven't been called yet when their on_init is ran
|
||||
-- This may prevent them from functioning properly, without manually calling the events they expect
|
||||
-- examples: on_player_created
|
||||
local caller = game.player or console
|
||||
if caller.admin then
|
||||
if mod_files then
|
||||
for k,_ in pairs(mod_files) do
|
||||
if k == 'control' then
|
||||
caller.print('ERROR: control.lua loaded twice for mod ' .. mod_name)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
local installed_index = find_installed_mod(mod_name)
|
||||
local loaded_index = find_loaded_mod(mod_name)
|
||||
if installed_index then
|
||||
local old_version = installed_mods[installed_index].version
|
||||
if old_version then
|
||||
debug_log({'hotpatch-warning.already-exists', mod_name, old_version})
|
||||
debug_log({'hotpatch-warning.reinstalling', mod_name, old_version})
|
||||
end
|
||||
if loaded_index then
|
||||
unload_mod(loaded_index)
|
||||
end
|
||||
end
|
||||
|
||||
install_mod(mod_name, mod_version, mod_code, mod_files)
|
||||
if not only_install then
|
||||
installed_index = find_installed_mod(mod_name)
|
||||
if installed_index then
|
||||
if not load_mod(installed_index) then
|
||||
caller.print('compilation failed for mod ' .. mod_name)
|
||||
return
|
||||
end
|
||||
loaded_index = find_loaded_mod(mod_name)
|
||||
if loaded_index then
|
||||
if not run_mod(loaded_index) then
|
||||
caller.print('execution failed for mod ' .. mod_name)
|
||||
unload_mod(loaded_index)
|
||||
return
|
||||
end
|
||||
if not mod_on_init(loaded_index) then
|
||||
caller.print('on_init failed for mod ' .. mod_name)
|
||||
unload_mod(loaded_index)
|
||||
return
|
||||
end
|
||||
-- TODO: notify all mods
|
||||
-- TODO: determine vanilla behaviour and replicate it
|
||||
if not mod_on_configuration_changed(loaded_index, {mod_changes = {mod_name={new_version=mod_version}}}) then
|
||||
caller.print('on_configuration_changed failed for mod ' .. mod_name)
|
||||
unload_mod(loaded_index)
|
||||
return
|
||||
end
|
||||
else
|
||||
end
|
||||
else
|
||||
end
|
||||
end
|
||||
else
|
||||
caller.print('You must be an admin to run this command.')
|
||||
end
|
||||
end
|
||||
|
||||
remote_interface['run'] = function(mod_name)
|
||||
-- this runs on_init, then registers events
|
||||
-- Note that mods may expect that certain events haven't been called yet when their on_init is ran
|
||||
-- This may prevent them from functioning properly, without manually calling the events they expect
|
||||
-- examples: on_player_created
|
||||
local caller = game.player or console
|
||||
if caller.admin then
|
||||
local installed_index = find_installed_mod(mod_name)
|
||||
local loaded_index = find_loaded_mod(mod_name)
|
||||
local mod
|
||||
local version
|
||||
if installed_index and not loaded_index then
|
||||
if not load_mod(installed_index) then
|
||||
caller.print('compilation failed for mod ' .. mod_name)
|
||||
return
|
||||
end
|
||||
loaded_index = find_loaded_mod(mod_name)
|
||||
else
|
||||
if loaded_index then
|
||||
mod = loaded_mods[loaded_index]
|
||||
version = mod.version
|
||||
debug_log({'hotpatch-warning.already-loaded', mod_name, version})
|
||||
debug_log({'hotpatch-warning.reinitializing', mod_name, version})
|
||||
end
|
||||
end
|
||||
|
||||
if loaded_index then
|
||||
if not run_mod(loaded_index) then
|
||||
caller.print('execution failed for mod ' .. mod_name)
|
||||
unload_mod(loaded_index)
|
||||
return
|
||||
end
|
||||
if not mod_on_init(loaded_index) then
|
||||
caller.print('on_init failed for mod ' .. mod_name)
|
||||
unload_mod(loaded_index)
|
||||
return
|
||||
end
|
||||
-- TODO: notify all mods
|
||||
-- TODO: determine vanilla behaviour and replicate it
|
||||
if not mod_on_configuration_changed(loaded_index, {mod_changes = {mod_name={new_version=version}}}) then
|
||||
caller.print('on_configuration_changed failed for mod ' .. mod_name)
|
||||
unload_mod(loaded_index)
|
||||
return
|
||||
end
|
||||
end
|
||||
else
|
||||
caller.print('You must be an admin to run this command.')
|
||||
end
|
||||
end
|
||||
|
||||
remote_interface['update'] = function(mod_name, mod_version, mod_code, mod_files)
|
||||
-- this updates an existing mod
|
||||
-- the current mods events are de-registered, the new code is installed, on_load is triggered, and then events are registered
|
||||
-- finally, the mod is informed of the update, so it can run migrations from the previous version
|
||||
-- TODO: validation
|
||||
local caller = game.player or console
|
||||
if caller.admin then
|
||||
if mod_files then
|
||||
for k,_ in pairs(mod_files) do
|
||||
if k == 'control' then
|
||||
caller.print('ERROR: control.lua loaded twice for mod ' .. mod_name)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
local installed_index = find_installed_mod(mod_name)
|
||||
if installed_index then
|
||||
;
|
||||
end
|
||||
local loaded_index = find_loaded_mod(mod_name)
|
||||
local old_version
|
||||
if loaded_index then
|
||||
old_version = loaded_mods[loaded_index].version
|
||||
unload_mod(loaded_index)
|
||||
end
|
||||
install_mod(mod_name, mod_version, mod_code, mod_files)
|
||||
installed_index = find_installed_mod(mod_name)
|
||||
if not load_mod(installed_index) then
|
||||
caller.print('compilation failed for mod ' .. mod_name)
|
||||
return
|
||||
end
|
||||
loaded_index = find_loaded_mod(mod_name)
|
||||
if not run_mod(loaded_index) then
|
||||
caller.print('execution failed for mod ' .. mod_name)
|
||||
unload_mod(loaded_index)
|
||||
return
|
||||
end
|
||||
if old_version then
|
||||
if not mod_on_load(loaded_index) then
|
||||
caller.print('on_load failed for mod ' .. mod_name)
|
||||
unload_mod(loaded_index)
|
||||
return
|
||||
end
|
||||
-- The mod must do any migrations here
|
||||
-- TODO: notify all mods
|
||||
if not mod_on_configuration_changed(loaded_index, {mod_changes = {mod_name={old_version=old_version, new_version=mod_version}}}) then
|
||||
caller.print('on_configuration_changed failed for mod ' .. mod_name)
|
||||
unload_mod(loaded_index)
|
||||
return
|
||||
end
|
||||
else
|
||||
-- first time install
|
||||
if not mod_on_init(loaded_index) then
|
||||
caller.print('on_init failed for mod ' .. mod_name)
|
||||
unload_mod(loaded_index)
|
||||
return
|
||||
end
|
||||
-- TODO: notify all mods
|
||||
if not mod_on_configuration_changed(loaded_index, {mod_changes = {mod_name={new_version=mod_version}}}) then
|
||||
caller.print('on_configuration_changed failed for mod ' .. mod_name)
|
||||
unload_mod(loaded_index)
|
||||
return
|
||||
end
|
||||
end
|
||||
else
|
||||
caller.print('You must be an admin to run this command.')
|
||||
end
|
||||
end
|
||||
|
||||
remote_interface['install_mod_file'] = function(mod_name, mod_file, mod_file_code)
|
||||
local caller = game.player or console
|
||||
if caller.admin then
|
||||
install_mod_file(mod_name, mod_file, mod_file_code)
|
||||
else
|
||||
caller.print('You must be an admin to run this command.')
|
||||
end
|
||||
end
|
||||
|
||||
remote_interface['exec'] = function(mod_name, exec_code)
|
||||
local caller = game.player or console
|
||||
if caller.admin then
|
||||
local loaded_index = find_loaded_mod(mod_name)
|
||||
local env = loaded_mods[loaded_index].env
|
||||
|
||||
local success
|
||||
local code, err = load(exec_code, exec_code, 't', env)
|
||||
if code then
|
||||
success, err = pcall(code)
|
||||
if not success then
|
||||
debug_log(err)
|
||||
end
|
||||
else
|
||||
debug_log(err)
|
||||
end
|
||||
else
|
||||
caller.print('You must be an admin to run this command.')
|
||||
end
|
||||
end
|
||||
|
||||
--TODO: most of this function
|
||||
remote_interface['clean'] = function(mod_name)
|
||||
-- Removes ALL mod data and reinitializes
|
||||
-- doesn't remove things a mod may add like surfaces, etc
|
||||
local caller = game.player or console
|
||||
if caller.admin then
|
||||
--mod_reset(mod_name)
|
||||
--run_mod(mod_name)
|
||||
--mod_on_init(mod_name)
|
||||
local loaded_index = find_loaded_mod(mod_name)
|
||||
local mod = loaded_mods[loaded_index]
|
||||
local version = mod.version
|
||||
mod_on_configuration_changed(loaded_index, {mod_changes = {mod_name={new_version=version}}})
|
||||
else
|
||||
caller.print('You must be an admin to run this command.')
|
||||
end
|
||||
end
|
||||
|
||||
remote_interface['uninstall'] = function(mod_name)
|
||||
-- Uninstalls a mod
|
||||
local caller = game.player or console
|
||||
if caller.admin then
|
||||
uninstall_mod(mod_name)
|
||||
else
|
||||
caller.print('You must be an admin to run this command.')
|
||||
end
|
||||
end
|
||||
|
||||
remote.add_interface('hotpatch', remote_interface)
|
||||
|
||||
debug_log({'hotpatch-info.complete', {'hotpatch-info.remote-installing'}})
|
||||
|
||||
--]===])
|
||||
|
||||
--[[
|
||||
|
||||
remote.call('hotpatch, 'install', 'test', '1.0.0', [===[
|
||||
script.on_event(defines.events.on_player_changed_position, function(e)
|
||||
game.print('changed position')
|
||||
end
|
||||
]===]
|
||||
|
||||
local updated_code = ....
|
||||
remote.call('hotpatch', 'update', 'test', '1.0.1', updated_code)
|
||||
|
||||
|
||||
]]
|
||||
|
||||
return true
|
||||
@@ -1,3 +0,0 @@
|
||||
msg-intro=Hierdie is Factorio vryspel. Jou taak is om 'n vuurpyl in die ruimte in te stuur. Doen dit deur 'n vuurpyl te bou en hom te lanseer. Jy sal baie gevorderde tegnologiëe moet navors om die Vuurpyl Silo te ontsluit. Begin klein, werk jou pad op met outomatisering en moenie vergeet om jouself te beskerm teen die inboorlinge nie.
|
||||
scenario-name=Vryspel
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
msg-intro=.بكوكلا اذه ناكس م كسفن يمحت نا ىسنت الو ,ةيكيتاموتوألا ربع كقيرط ينبتسو ,رفصلا نم أدبتس .كلت خيراوصلا قالطإ ةطحم حتف عيطتست فيكل ةروطتملا ايجولونكتلل ثاحبأب مايقلا ىلإ جاتحتس .اهنم خوراص قالطإ مث خيراوص قالطإ ةطحم ءانب قيرط نع كلذب مايقلا كنكمي ,ءاضفلا ىلإ اخوراص قلطت نا يه كتمهم ,factorioـل رحلا بعللا روط وه اذه
|
||||
scenario-name=رحلا بعللا
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
msg-intro=Гэта рэжым свабоднай гульні Factorio. Вашая мэта - запусціць ракету ў космас. Дзеля гэтага вам трэба пабудаваць ракетную шахту і саму ракету са спадарожнікам. Вы мусіце даследаваць перадавыя тэхналогіі, каб навучыцца будаваць ракетную шахту. Пачынайце з малога, працуйце над аўтаматызацыяй вытворчасці, даследуйце новыя тэхналогіі і не забывайцеся на абарону ад «тутэйшых».
|
||||
scenario-name=Свабодная гульня
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
msg-intro=Това е режимът на свободна игра. Задачата ви е да изстреляте ракета в космоса. За това ще е нужно да построите ракетна шахта и изстреляте ракетата със спътник. Ще трябва да проучите развити технологии, които ще отключат ракетната шахта. Започнете с малко и постепенно автоматизирайте процеса, като не забравяте да се защитавате от местните форми на живот.
|
||||
scenario-name=Свободна игра
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
msg-intro=Aquest és el mode lliure del Factorio. La vostra tasca és construir un coet i enviar-lo a l'espai. Ho haureu de fer construint un centre de coets espacials i llençant un coet. Necessitareu investigar tecnologies avançades per poder-ho fer. Aneu pas a pas, avanceu cap a l'automatització i no oblideu protegir-vos dels natius.
|
||||
scenario-name=Mode lliure
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
msg-intro=Toto je volná hra Factorio. Tvým úkolem je vypustit raketu do vesmíru. Postav raketové silo a vypusť raketu. Pro odemknutí raketového sila budeš muset vyzkoumat pokročilé technologie. Začni v malém, postupuj s automatizací a nezapomeň se chránit před domorodci.
|
||||
scenario-name=Volná hra
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
msg-intro=Dette er Factorio freeplay. Din opgave er at affyre en raket ud i rummet. Gør dette ved at konstruere en raketsilo og affyr en raket. Du bliver nød til at undersøge avancerede teknologier for at låse op for raketsiloen. Start småt og arbejd din vej op med automatisering og glem ikke at beskytte dig selv fra de indfødte.
|
||||
scenario-name=Fri lej
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
msg-intro=Du befindest dich im freien Spiel Modus von Factorio. Ziel ist es, eine Rakete ins Weltall zu schicken. Baue hierzu das Raketensilo um die Rakete starten zu können. Um das Raketensilo freischalten zu können wirst du fortgeschrittene Technologien erforschen müssen. Fange klein an und erforsche die Automation. Vergiss aber nicht, dich vor den Ureinwohner zu schützen.
|
||||
scenario-name=Freies Spiel
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
msg-intro=Αυτό είναι το ελεύθερο παιχνίδι του Factorio. Ο στόχος σας είναι να εκτοξεύσετε έναν πύραυλο στο διάστημα. Αυτό το κάνετε κατασκευάζοντας ένα σιλό πυραύλου και εκτοξεύοντας έναν πύραυλο. Θα πρέπει να ερευνήσετε προηγμένες τεχνολογίες για να ξεκλειδώσετε το σιλό πυραύλου. Ξεκινήστε χαμηλά, φτάστε ψηλά χρησιμοποιώντας αυτοματισμούς και μην ξεχάσετε να προστατέψετε τον εαυτό σας από τους ντόπιους.
|
||||
scenario-name=Ελεύθερο παιχνίδι
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
msg-intro=This is the Factorio freeplay. Your task is to launch a rocket into space. Do this by constructing a rocket silo and launching a rocket. You will need to research advanced technologies in order to unlock the rocket silo. Start small, work your way up with automation and don't forget to protect yourself from the natives.
|
||||
scenario-name=Freeplay
|
||||
@@ -1,81 +0,0 @@
|
||||
[hotpatch]
|
||||
log=@__1__:__2__] __3__
|
||||
log-mod=@__1__:__2__][__4__] __3__
|
||||
severe=SEVERE ERROR: __1__
|
||||
error=ERROR: __1__
|
||||
warning=WARNING: __1__
|
||||
info=INFO: __1__
|
||||
verbose=INFO: __1__
|
||||
trace=TRACE: __1__
|
||||
|
||||
[hotpatch-info]
|
||||
logging-enabled=Logging enabled
|
||||
metatable-installed=_ENV metatable installed
|
||||
complete=__1__ Complete!
|
||||
on-init=initializing...
|
||||
on-load=loading...
|
||||
on-configuration-changed=mod configuration changed...
|
||||
installing-included-mods=installing included mods...
|
||||
loading-installed-mods=loading installed mods...
|
||||
loading-libs=loading Factorio.data.core.lualib...
|
||||
loading-library=loading library: __1__
|
||||
uninstalling=Uninstalling mod...
|
||||
installing=Installing version __1__...
|
||||
installing-file=Installing file __1__...
|
||||
script-shim=setting up mod script shim...
|
||||
setting-env=setting up mod _ENV...
|
||||
loading=loading...
|
||||
unloading=unloading...
|
||||
running=running...
|
||||
must-be-admin=You must be an admin to run this command
|
||||
remote-installing=installing remote interface
|
||||
|
||||
[hotpatch-trace]
|
||||
nil-var-access=_ENV nil variable access: __1__
|
||||
nil-var-assignment=_ENV variable assignment: __1__
|
||||
event-registering=registering events...
|
||||
on-tick-event-registered=registered on_tick event...
|
||||
on-event-registered=registered event __1__...
|
||||
on-nth-tick-event-registered=registered nth_tick event __1__...
|
||||
event-running=running event __1__...
|
||||
nth-tick-event-running=running nth_event __1__...
|
||||
mod-on-init=running on_init...
|
||||
mod-on-load=running on_load...
|
||||
mod-on-configuration-changed=running on_configuration_changed...
|
||||
adding-event=adding event __1__
|
||||
adding-nth-tick-event=adding nth_tick event __1__
|
||||
caching-event=caching event: __1__...
|
||||
caching-nth-tick-event=caching nth_tick event: __1__...
|
||||
event-processing=processing event __1__...
|
||||
nth-tick-event-processing=processing nth_tick event __1__...
|
||||
nth-tick-handler-added=added nth_tick handler: __1__
|
||||
nth-tick-handler-removed=removed nth_tick handler: __1__
|
||||
on-tick-handler-added=added on_tick handler
|
||||
on-tick-handler-removed=removed on_tick handler
|
||||
on-event-handler-added=added event handler: __1__
|
||||
on-event-handler-removed=removed event handler: __1__
|
||||
cached-load-require=loading cached require'd file: __1__...
|
||||
load-require=loading require'd file: __1__...
|
||||
load-core-lib=loading from Factorio.data.core.lualib: __1__...
|
||||
|
||||
[hotpatch-warning]
|
||||
contains-comments=mod code contains comments!
|
||||
contains-comments-no-lf=mod contains comments and no linefeed!
|
||||
contains-comments-console=comments from console will comment out the entire code!
|
||||
reset-events-not-running=tried to reset events for mod that isn't running!
|
||||
remote-interface-exists=remote interface __1__ already exists, removing...
|
||||
command-exists=command __1__ already exists, removing...
|
||||
already-exists=mod already exists: __1__ __2__
|
||||
reinstalling=reinstalling mod in-place: __1__ __2__
|
||||
already-loaded=mod already loaded: __1__ __2__
|
||||
reinitializing=reinitializing: __1__ __2__
|
||||
|
||||
[hotpatch-error]
|
||||
invalid-API-access=Invalid API access: __1__
|
||||
not-installed=mod not installed, cannot install file for mod that does not exist
|
||||
compilation-failed=compilation failed for mod
|
||||
execution-failed=execution failed for mod
|
||||
path-not-found=path '__1__' not found
|
||||
on-init-failed=on_init failed
|
||||
on-load-failed=on_load failed
|
||||
on-configuration-changed-failed=on_configuration_changed failed
|
||||
@@ -1,2 +0,0 @@
|
||||
scenario-name=Libera Ludado
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
msg-intro=Este es el modo de juego libre de Factorio. Tu tarea es la de lanzar un cohete al espacio. Logra esto construyendo un Silo de Cohetes y lanzando un cohete con un satélite. Necesitarás investigar tecnologías avanzadas para poder desbloquear el Silo de Cohetes. Comienza poco a poco, trabaja en tu sistema de automatización y no te olvides de protegerte de los nativos.
|
||||
scenario-name=Modo libre
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
scenario-name=Vabamäng
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
msg-intro=Tämä on Factorion vapaapelimuoto. Tavoitteesi on lähettää satelliitti ulkoavaruuteen rakentamalla rakettisiilo ja laukaisemalla raketti. Rakettisiilon voit rakentaa vasta, kun olet kehittänyt siihen tarvittavat huipputeknologiat. Aloita pienestä ja automatisoi kaikki tarvittava, äläkä unohda suojella itseäsi alkuasukkailta.
|
||||
scenario-name=Vapaapeli
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
msg-intro=Il s'agit du mode de jeu libre de Factorio. Votre tâche consiste à lancer une fusée dans l'espace. Faites ceci en construisant une fusée et en la lançant dans l'espace. Vous aurez besoin de rechercher des technologies de pointe afin de débloquer le silo. Commencez petit, évoluez en automatisant et n'oubliez pas de vous protéger contre les autochtones.
|
||||
scenario-name=Jeu libre
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
msg-intro=.םיימוקמה םירוציה ינפמ םכמצע לע ןגהל וחכשת לאו ,יטמוטוא ןוכימ תועצמאב םכלש הישעתה תא וביחרה ,ןטקב וליחתה .ןקתמה תיינב תא רשפאל ידכ תומדקתמ תויגולונכט רוקחל םכילע היהי .ליט ותועצמאב רגשלו םיליט רוגיש ןקתמ תונבל םכילע .ללחל ליט רגשל איה םכתמישמ .ויִרֹוטְקַפ לש ישפוחה שיחרתה והז
|
||||
scenario-name=ישפוח קחשמ
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
msg-intro=Ez a Factorio szabad játék módja. A feladatod az, hogy kilőj egy rakétát az űr felderítésére. Ehhez egy Rakétasilót kell építened, melyből aztán egy műholddal felszerelt rakétát kell kilőnöd. A Rakétasiló kifejlesztéséhez különféle technológiákat kell fejlesztened. Kezdj kicsiben és küzdd fel magad az automatizálásban. Ne felejts el védekezni a honos fajok ellen.
|
||||
scenario-name=Szabad játék
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
msg-intro=Questa è la modalità libera di Factorio. Il tuo obiettivo è quello di lanciare un razzo nello spazio e per farlo dovrai costruire un silo per razzi. Avrai bisogno di fare ricerche avanzate per sbloccare il silo. Inizia con poco, lavora sull'automazione e non dimenticarti di difenderti dai nativi.
|
||||
scenario-name=Gioco libero
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
msg-intro=これは、Factorioのフリープレイです。ロケットサイロを建造し、ロケットを宇宙へ打ち上げるのがあなたの任務です。ロケットサイロの建造を行うためには高度な技術研究を行う必要があります。小規模に始め、自動化しつつ進み、原住生物から身を守ることも忘れないでください。
|
||||
scenario-name=フリープレイ
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
msg-intro=Factorio 자유플레이입니다. 당신의 목표는 우주로 로켓을 발사하는 것입니다. 로켓 격납고를 건설하고 로켓을 발사하여 이 목표를 달성하세요. 로켓 격납고의 잠금을 해제하기 위해 고급 기술들을 연구할 필요가 있습니다. 작은 것부터 시작해 자동화로 차근차근 발전하고, 토착 생물로부터 자신을 보호하는 것도 잊지 마세요.
|
||||
scenario-name=자유플레이
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
msg-intro=Tai Factorio laisvas žaidimas. Jūsų užduotis - paleisti raketą į kosmosą. Tam reikia pastatyti Raketos šachtą ir paleisti raketą su palydovu. Reikės atrasti pažangias technologijas kad atrakintumėte Raketos šachtą. Pradėkite palengva, tobulinkite automatiką ir neužmirškite apsisaugoti nuo vietinių.
|
||||
scenario-name=Žaisti laisva forma
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
scenario-name=Brīvspēle
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
msg-intro=Dit is Factorio vrij spel. Je taak is om een raket te lanceren in de ruimte. Bouw hiervoor een raket-silo en lanceer een raket. Je zult geavanceerde technologieën moeten onderzoeken om de raket-silo vrij te spelen. Begin op kleine schaal, breidt uit en automatiseer je fabriek en vergeet niet jezelf te beschermen tegen de inheemse bewoners.
|
||||
scenario-name=Freeplay
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
msg-intro=Dette er Factorio freeplay. Din oppgave er å sende en rakett til verdensrommet. Bygg en rakettsilo og send opp en rakett med en satellitt. Du blir nødt til å utforske avanserte teknologier for å få tilgang til rakettsiloen. Begynn i det små, jobb deg oppover med hjelp av automatisering, men ikke glem å beskytte deg mot de innfødte.
|
||||
scenario-name=Frittspill
|
||||
|
||||
Loaded 100 of 3861 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user