From dd216318b04bf9c0ccbe2b2e03ac9146bcc48bcb Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Thu, 20 May 2021 21:33:54 +0200 Subject: [PATCH] Add option to pass map settings to Factorio Add options to pass map seed, map exchange string, map gen settings and/or map settings when creating a save either through the create save request or the load scenario request. --- packages/ctl/ctl.js | 48 +++++- packages/lib/factorio/server.js | 35 ++++- packages/lib/link/messages.js | 9 ++ packages/slave/slave.js | 15 +- packages/web_ui/package.json | 3 + .../web_ui/src/components/CreateSaveModal.jsx | 140 ++++++++++++++++++ .../src/components/InstanceViewPage.jsx | 19 +-- .../src/components/LoadScenarioModal.jsx | 140 ++++++++++++++++++ packages/web_ui/webpack.common.js | 5 + test/integration/clusterio.js | 30 +++- test/lib/factorio/exchange_string.js | 39 +---- test/lib/factorio/test_strings.js | 38 +++++ 12 files changed, 462 insertions(+), 59 deletions(-) create mode 100644 packages/web_ui/src/components/CreateSaveModal.jsx create mode 100644 packages/web_ui/src/components/LoadScenarioModal.jsx create mode 100644 test/lib/factorio/test_strings.js diff --git a/packages/ctl/ctl.js b/packages/ctl/ctl.js index b4051db2..7f464d22 100755 --- a/packages/ctl/ctl.js +++ b/packages/ctl/ctl.js @@ -25,6 +25,7 @@ const libSharedCommands = require("@clusterio/lib/shared_commands"); const { ConsoleTransport, levels, logger } = require("@clusterio/lib/logging"); const libLoggingUtils = require("@clusterio/lib/logging_utils"); const libHelpers = require("@clusterio/lib/helpers"); +const libFactorio = require("@clusterio/lib/factorio"); function print(...content) { @@ -314,15 +315,50 @@ instanceCommands.add(new libCommand.Command({ }, })); +async function loadMapSettings(args) { + let seed = args.seed !== undefined ? args.seed : null; + let mapGenSettings = null; + let mapSettings = null; + if (args.mapExchangeString) { + let parsed = libFactorio.readMapExchangeString(args.mapExchangeString); + mapGenSettings = parsed.map_gen_settings; + mapSettings = parsed.map_settings; + } + if (args.mapGenSettings) { + mapGenSettings = JSON.parse(await fs.readFile(args.mapGenSettings)); + } + if (args.mapSettings) { + mapSettings = JSON.parse(await fs.readFile(args.mapSettings)); + } + + return { + seed, + mapGenSettings, + mapSettings, + }; +} + instanceCommands.add(new libCommand.Command({ - definition: ["create-save ", "Create a new save on an instance", (yargs) => { + definition: ["create-save [name]", "Create a new save on an instance", (yargs) => { yargs.positional("instance", { describe: "Instance to create on", type: "string" }); + yargs.positional("name", { describe: "Name of save to create.", type: "string", default: "world.zip" }); + yargs.options({ + "seed": { describe: "Seed to use, takes precedence over map-gen-settings", nargs: 1, type: "number" }, + "map-exchange-string": { describe: "Map exchange string to use for the save", nargs: 1, type: "string" }, + "map-gen-settings": { describe: "path to file to use for map-gen-settings", nargs: 1, type: "string" }, + "map-settings": { describe: "path to file to use for map-settings", nargs: 1, type: "string" }, + }); }], handler: async function(args, control) { let instanceId = await libCommand.resolveInstance(control, args.instance); + let { seed, mapGenSettings, mapSettings } = await loadMapSettings(args); await control.setLogSubscriptions({ instance_ids: [instanceId] }); let response = await libLink.messages.createSave.send(control, { instance_id: instanceId, + name: args.name, + seed, + map_gen_settings: mapGenSettings, + map_settings: mapSettings, }); }, })); @@ -364,15 +400,23 @@ instanceCommands.add(new libCommand.Command({ yargs.positional("instance", { describe: "Instance to start", type: "string" }); yargs.positional("scenario", { describe: "Scenario to load", type: "string" }); yargs.options({ + "seed": { describe: "Seed to use, takes precedence over map-gen-settings", nargs: 1, type: "number" }, + "map-exchange-string": { describe: "Map exchange string to use for the save", nargs: 1, type: "string" }, + "map-gen-settings": { describe: "path to file to use for map-gen-settings", nargs: 1, type: "string" }, + "map-settings": { describe: "path to file to use for map-settings", nargs: 1, type: "string" }, "keep-open": { describe: "Keep console open", nargs: 0, type: "boolean", default: false }, }); }], handler: async function(args, control) { let instanceId = await libCommand.resolveInstance(control, args.instance); await control.setLogSubscriptions({ instance_ids: [instanceId] }); + let { seed, mapGenSettings, mapSettings } = await loadMapSettings(args); let response = await libLink.messages.loadScenario.send(control, { instance_id: instanceId, - scenario: args.scenario || null, + scenario: args.scenario, + seed, + map_gen_settings: mapGenSettings, + map_settings: mapSettings, }); control.keepOpen = args.keepOpen; }, diff --git a/packages/lib/factorio/server.js b/packages/lib/factorio/server.js index 9ba551d3..d61b49c9 100644 --- a/packages/lib/factorio/server.js +++ b/packages/lib/factorio/server.js @@ -829,17 +829,26 @@ class FactorioServer extends events.EventEmitter { * * @param {string} name - * Name of the save to create. Should end with ".zip". + * @param {?number=} seed - Seed to pass via --map-gen-seed + * @param {?object=} mapGenSettings - + * Map get settings to pass via --map-gen-settings. + * @param {?object=} mapSettings - + * Map setting to pass via --map-settings. */ - async create(name) { + async create(name, seed = null, mapGenSettings = null, mapSettings = null) { this._check(["init"]); this._state = "create"; await this._writeConfigIni(); + await this._writeMapSettings(mapGenSettings, mapSettings); this._server = child_process.spawn( this.binaryPath(), [ "--config", this.writePath("config.ini"), "--create", this.writePath("saves", name), + ...(seed !== null ? ["--map-gen-seed", seed] : []), + ...(mapGenSettings !== null ? ["--map-gen-settings", this.writePath("map-gen-settings.json")] : []), + ...(mapSettings !== null ? ["--map-settings", this.writePath("map-settings.json")] : []), ...(this.verboseLogging ? ["--verbose"] : []), ], { @@ -903,17 +912,26 @@ class FactorioServer extends events.EventEmitter { * argument to start the given scenario. * * @param {string} scenario - Name of the scenario to run. + * @param {?number=} seed - Seed to pass via --map-gen-seed + * @param {?object=} mapGenSettings - + * Map get settings to pass via --map-gen-settings. + * @param {?object=} mapSettings - + * Map setting to pass via --map-settings. */ - async startScenario(scenario) { + async startScenario(scenario, seed = null, mapGenSettings = null, mapSettings = null) { this._check(["init"]); this._state = "running"; await this._writeConfigIni(); + await this._writeMapSettings(mapGenSettings, mapSettings); this._server = child_process.spawn( this.binaryPath(), [ "--config", this.writePath("config.ini"), "--start-server-load-scenario", scenario, + ...(seed !== null ? ["--map-gen-seed", seed] : []), + ...(mapGenSettings !== null ? ["--map-gen-settings", this.writePath("map-gen-settings.json")] : []), + ...(mapSettings !== null ? ["--map-settings", this.writePath("map-settings.json")] : []), "--port", this.gamePort, "--rcon-port", this.rconPort, "--rcon-password", this.rconPassword, @@ -1153,6 +1171,19 @@ class FactorioServer extends events.EventEmitter { }); await fs.outputFile(this.writePath("config.ini"), content); } + + async _writeMapSettings(mapGenSettings, mapSettings) { + if (mapGenSettings) { + await fs.outputFile( + this.writePath("map-gen-settings.json"), JSON.stringify(mapGenSettings, null, 4) + ); + } + if (mapSettings) { + await fs.outputFile( + this.writePath("map-settings.json"), JSON.stringify(mapSettings, null, 4) + ); + } + } } diff --git a/packages/lib/link/messages.js b/packages/lib/link/messages.js index 8586ce39..672777b8 100644 --- a/packages/lib/link/messages.js +++ b/packages/lib/link/messages.js @@ -498,6 +498,12 @@ messages.createSave = new Request({ links: ["control-master", "master-slave", "slave-instance"], permission: "core.instance.create_save", forwardTo: "instance", + requestProperties: { + "name": { type: "string" }, + "seed": { type: ["integer", "null"] }, + "map_gen_settings": { type: ["object", "null"] }, + "map_settings": { type: ["object", "null"] }, + }, }); messages.loadScenario = new Request({ @@ -507,6 +513,9 @@ messages.loadScenario = new Request({ forwardTo: "instance", requestProperties: { "scenario": { type: "string" }, + "seed": { type: ["integer", "null"] }, + "map_gen_settings": { type: ["object", "null"] }, + "map_settings": { type: ["object", "null"] }, }, }); diff --git a/packages/slave/slave.js b/packages/slave/slave.js index 93f3eb79..c1c0028e 100755 --- a/packages/slave/slave.js +++ b/packages/slave/slave.js @@ -516,14 +516,17 @@ class Instance extends libLink.Link { * scenario. * * @param {String} scenario - Name of scenario to load. + * @param {?number} seed - seed to use. + * @param {?object} mapGenSettings - MapGenSettings to use. + * @param {?object} mapSettings - MapSettings to use. */ - async startScenario(scenario) { + async startScenario(scenario, seed, mapGenSettings, mapSettings) { this.server.on("rcon-ready", () => { this.logger.verbose("RCON connection established"); }); this.server.on("exit", () => this.notifyExit()); - await this.server.startScenario(scenario); + await this.server.startScenario(scenario, seed, mapGenSettings, mapSettings); await libPlugin.invokeHook(this.plugins, "onStart"); @@ -678,15 +681,16 @@ class Instance extends libLink.Link { throw err; } + let { scenario, seed, map_gen_settings, map_settings } = message.data; try { - await this.startScenario(message.data.scenario); + await this.startScenario(scenario, seed, map_gen_settings, map_settings); } catch (err) { await this.stop(); throw err; } } - async createSaveRequestHandler() { + async createSaveRequestHandler(message) { this.notifyStatus("creating_save"); try { this.logger.verbose("Writing server-settings.json"); @@ -702,7 +706,8 @@ class Instance extends libLink.Link { } this.server.on("exit", () => this.notifyExit()); - await this.server.create("world"); + let { name, seed, map_gen_settings, map_settings } = message.data; + await this.server.create(name, seed, map_gen_settings, map_settings); this.logger.info("Successfully created save"); } diff --git a/packages/web_ui/package.json b/packages/web_ui/package.json index 7a64620d..fa302af5 100644 --- a/packages/web_ui/package.json +++ b/packages/web_ui/package.json @@ -20,7 +20,9 @@ "@babel/preset-react": "^7.12.13", "@clusterio/lib": "^2.0.0-alpha.6", "antd": "^4.6.5", + "assert": "^2.0.0", "babel-loader": "^8.2.2", + "browserify-zlib": "^0.2.0", "buffer": "^6.0.2", "clean-webpack-plugin": "^3.0.0", "css-loader": "^5.1.0", @@ -33,6 +35,7 @@ "react": "^16.13.1", "react-dom": "^16.13.1", "react-router-dom": "^5.2.0", + "stream-browserify": "^3.0.0", "style-loader": "^2.0.0", "util": "^0.12.3", "webpack": "^5.36.2" diff --git a/packages/web_ui/src/components/CreateSaveModal.jsx b/packages/web_ui/src/components/CreateSaveModal.jsx new file mode 100644 index 00000000..f067635b --- /dev/null +++ b/packages/web_ui/src/components/CreateSaveModal.jsx @@ -0,0 +1,140 @@ +import React, { useContext, useState } from "react"; +import { Button, Col, Form, Input, Modal, Row, Typography } from "antd"; + +import libLink from "@clusterio/lib/link"; +import { readMapExchangeString } from "@clusterio/lib/factorio/exchange_string"; + +import ControlContext from "./ControlContext"; +import { notifyErrorHandler } from "../util/notify"; + + +export default function CreateSaveModal(props) { + let [visible, setVisible] = useState(false); + let [creatingSave, setCreatingSave] = useState(false); + let [form] = Form.useForm(); + let control = useContext(ControlContext); + + function createSave() { + let values = form.getFieldsValue(); + let name = values.saveName || "world.zip"; + let seed = values.seed === undefined ? null : Number.parseInt(values.seed, 10); + let mapGenSettings = null; + let mapSettings = null; + if (values.exchangeString && values.exchangeString.trim()) { + let result; + try { + result = readMapExchangeString(values.exchangeString); + } catch (err) { + form.setFields([{ name: "exchangeString", errors: [err.message] }]); + return; + } + form.setFields([{ name: "exchangeString", errors: [] }]); + mapGenSettings = result.map_gen_settings; + mapSettings = result.map_settings; + } + if (values.mapGenSettings && values.mapGenSettings.trim()) { + try { + mapGenSettings = JSON.parse(values.mapGenSettings); + } catch (err) { + form.setFields([{ name: "mapGenSettings", errors: [err.message] }]); + return; + } + form.setFields([{ name: "mapGenSettings", errors: [] }]); + } + if (values.mapSettings && values.mapSettings.trim()) { + try { + mapSettings = JSON.parse(values.mapGenSettings); + } catch (err) { + form.setFields([{ name: "mapSettings", errors: [err.message] }]); + return; + } + form.setFields([{ name: "mapSettings", errors: [] }]); + } + + setCreatingSave(true); + libLink.messages.createSave.send(control, { + instance_id: props.instance.id, + name, + seed, + map_gen_settings: mapGenSettings, + map_settings: mapSettings, + }).then(() => { + setVisible(false); + }).catch( + notifyErrorHandler("Error creating save") + ).finally(() => { + setCreatingSave(false); + }); + } + + function convertExchangeString() { + let exchangeString = form.getFieldValue("exchangeString"); + let result; + try { + result = readMapExchangeString(exchangeString); + } catch (err) { + form.setFields([{ name: "exchangeString", errors: [err.message] }]); + return; + } + form.setFields([{ name: "exchangeString", errors: [], value: "" }]); + form.setFieldsValue({ + mapGenSettings: JSON.stringify(result.map_gen_settings, null, 4), + mapSettings: JSON.stringify(result.map_settings, null, 4), + }); + } + + function handleCancel() { + setVisible(false); + } + + return <> + + +
+ + + + + + + + + +
+ +
+ + + + + + +
+
+ ; +} diff --git a/packages/web_ui/src/components/InstanceViewPage.jsx b/packages/web_ui/src/components/InstanceViewPage.jsx index cc0a55d2..4b7cd077 100644 --- a/packages/web_ui/src/components/InstanceViewPage.jsx +++ b/packages/web_ui/src/components/InstanceViewPage.jsx @@ -12,6 +12,8 @@ import InstanceConsole from "./InstanceConsole"; import InstanceRcon from "./InstanceRcon"; import AssignInstanceModal from "./AssignInstanceModal"; import StartStopInstanceButton from "./StartStopInstanceButton"; +import CreateSaveModal from "./CreateSaveModal"; +import LoadScenarioModal from "./LoadScenarioModal"; import { notifyErrorHandler } from "../util/notify"; import { useInstance } from "../model/instance"; import { useSlave } from "../model/slave"; @@ -29,8 +31,6 @@ export default function InstanceViewPage(props) { let [instance] = useInstance(instanceId); let [slave] = useSlave(Number(instance["assigned_slave"])); - let [creatingSave, setCreatingSave] = useState(false); - let nav = [{ name: "Instances", path: "/instances" }, { name: instance.name || "Unknown" }]; if (instance.loading) { return ; @@ -45,19 +45,8 @@ export default function InstanceViewPage(props) { let instanceButtons = - {instance.status === "stopped" && } + + { + setVisible(false); + }).catch( + notifyErrorHandler("Error creating save") + ).finally(() => { + setLoadingScenario(false); + }); + } + + function convertExchangeString() { + let exchangeString = form.getFieldValue("exchangeString"); + let result; + try { + result = readMapExchangeString(exchangeString); + } catch (err) { + form.setFields([{ name: "exchangeString", errors: [err.message] }]); + return; + } + form.setFields([{ name: "exchangeString", errors: [], value: "" }]); + form.setFieldsValue({ + mapGenSettings: JSON.stringify(result.map_gen_settings, null, 4), + mapSettings: JSON.stringify(result.map_settings, null, 4), + }); + } + + function handleCancel() { + setVisible(false); + } + + return <> + + +
+ + + + + + + + + +
+ +
+ + + + + + +
+
+ ; +} diff --git a/packages/web_ui/webpack.common.js b/packages/web_ui/webpack.common.js index df0fac39..6a850827 100644 --- a/packages/web_ui/webpack.common.js +++ b/packages/web_ui/webpack.common.js @@ -92,8 +92,13 @@ module.exports = (env = {}) => ({ "util": require.resolve("util/"), "os": require.resolve("os-browserify/browser"), + // Required for zlib + "assert": require.resolve("assert"), + "stream": require.resolve("stream-browserify"), + "events": require.resolve("events/"), "path": require.resolve("path-browserify"), + "zlib": require.resolve("browserify-zlib"), }, }, }); diff --git a/test/integration/clusterio.js b/test/integration/clusterio.js index a8f80793..042c9979 100644 --- a/test/integration/clusterio.js +++ b/test/integration/clusterio.js @@ -7,6 +7,7 @@ const events = require("events"); const libLink = require("@clusterio/lib/link"); const libUsers = require("@clusterio/lib/users"); +const testStrings = require("../lib/factorio/test_strings"); const { TestControl, TestControlConnector, url, controlToken, slowTest, get, exec, execCtl, sendRcon, getControl, instancesDir, @@ -184,7 +185,7 @@ describe("Integration of Clusterio", function() { "afk-auto-kick", "Kick if AFK for more than 2 minutes.", ], [ - "allow_commands", true, + "allow_commands", "true", "allow-commands", "Allow Lua commands: Yes.", ], [ @@ -315,6 +316,33 @@ describe("Integration of Clusterio", function() { }); }); + describe("instance load-scenario", function() { + it("starts the instance with the given settings", async function() { + slowTest(this); + await execCtl("instance config set 44 factorio.enable_save_patching false"); + await execCtl("instance config set 44 player_auth.load_plugin false"); + await execCtl("instance config set 44 research_sync.load_plugin false"); + await execCtl("instance config set 44 statistics_exporter.load_plugin false"); + await execCtl("instance config set 44 subspace_storage.load_plugin false"); + try { + let exchangeString = testStrings.modified.replace(/[\n\r]+/g, ""); + let args = `base/freeplay --seed 1234 --map-exchange-string "${exchangeString}"`; + await execCtl(`instance load-scenario test ${args}`); + await checkInstanceStatus(44, "running"); + await sendRcon(44, '/c game.print("disable achievements")'); + await sendRcon(44, '/c game.print("disable achievements")'); + assert.equal(await sendRcon(44, "/c rcon.print(game.default_map_gen_settings.seed)"), "1234\n"); + assert.equal(await sendRcon(44, "/c rcon.print(game.map_settings.pollution.ageing)"), "1.5\n"); + assert.equal( + await sendRcon(44, "/c rcon.print(game.difficulty_settings.research_queue_setting)"), "never\n" + ); + + } finally { + await execCtl("instance stop test"); + } + }); + }); + describe("instance delete", function() { it("deletes the instance", async function() { slowTest(this); diff --git a/test/lib/factorio/exchange_string.js b/test/lib/factorio/exchange_string.js index 854a6893..8164a241 100644 --- a/test/lib/factorio/exchange_string.js +++ b/test/lib/factorio/exchange_string.js @@ -4,37 +4,8 @@ const zlib = require("zlib"); const libFactorio = require("@clusterio/lib/factorio"); +const testStrings = require("./test_strings"); -let defaultString = ` ->>>eNpjZGBkUGQAgwZ7EOZgSc5PzIHxQJgrOb+gILVIN78oFVmYM -7moNCVVNz8TVXFqXmpupW5SYjGKYo7Movw8dBNYi0vy81BFSopSU -4uRRbhLixLzMktz0fUyMF5imubZ0CLHAML/6xkU/v8HYSDrAdAvI -MzA2ABRCRSDAdbknMy0NAYGBUcgdgJJMzIyVousc39YNcWeEaJGz -wHK+AAVOZAEE/GEMfwccEqpwBgmSOYYg8FnJAbE0hKgFVBVHA4IB -kSyBSTJyNj7duuC78cu2DH+Wfnxkm9Sgj2joavIuw9G6+yAkuwgf -zLBiVkzQWAnzCsMMDMf2EOlbtoznj0DAm/sGVlBOkRAhIMFkDjgz -czAKMAHZC3oARIKMgwwp9nBjBFxYEwDg28wnzyGMS7bo/sDGBA2I -MPlQMQJEAG2EO4yRgjTod+B0UEeJiuJUALUb8SA7IYUhA9Pwqw9j -GQ/mkMwIwLZH2giKg5YooELZGEKnHjBDHcNMDwvsMN4DvMdGJlBD -JCqL0AxCA8kAzMKQgs4gIObmQEBgGmjrO/lHgDxa6G4<<< -`; - -let testString = ` ->>>eNp1UzFs00AUvZ8mbRIEypCBSlCCyFAhOQqBKULxlQUxMCCxgnCc -S2rh2On5LAgMZOjAgISEurQLXSmCBTGwRWKhEkgIJraiMgBiaGmFOiC -FO5/PsUz6pfv+9/6/99+/kwEBOo2k6c83NhrZtOkaNkIDXa286fZ6hG -ouJaJIwTmT+i2iuZYdQ3GeOKTb15qGJ4oxQsMglbWo60iGYcSQ8Zjrh -GUhwighHpcRSBHIEZ8ajuV35dmBwIPK1DRxmMX6ADnqmre1Rb/D86UF -hNYX1lZncZpZNkGpmQ41PE87J6RFTe4YjND4hPDq943NwfIcEmv0AJV -GI7F4tMXViYVgECgCjoWWPm66DqOurXmEMcvp1A3/br1pGV5Oq1ZqVW -Hzk0ralCz5xDH79a5vM6tnW4Rma5XgQPVk8kTXtTzmU5Jg1g6tm0hfr -ZwPLGPaVrstL6p0SYwEAPeLLy9/vbeigxysgsPgIESGTYVcCYMnL/Bh -qbRKoQsq2NFBdt+LBbIp4y3CqiweBzK5LJIAu4vbD18f7Dfg77PdT1e -bt3S4/q2w5J39IbQfFW+TitzaqrA3ahSkOLf0MPVFhw/vhf3SYUqcOC -PczkoKQfXaNILCMb5df8Rd6QRS0hqKpoihHdgfNcm2Cj7ryTnKGC4K8 -jnh3gmXQRElVwYyxI8x4FMqOzsu4edrKK6hNZ5wU7V9G+ufEFL+7yHi -cySQMp7wDHnRsBW571ORGn6fH2fUDj/FwV0iUbXPMbmTP7akkt8ChiL -/BPceEs3j1M3iz71/ObT9tA==<<<`; // Simple linear congruential random number generator class Random { @@ -52,10 +23,10 @@ class Random { describe("lib/factorio/exchange_string", function() { describe("readMapExchangeString", function() { it("should parse a valid string", function() { - let result = libFactorio.readMapExchangeString(defaultString); + let result = libFactorio.readMapExchangeString(testStrings.default); assert.equal(result.map_gen_settings.seed, 1234567890); - result = libFactorio.readMapExchangeString(testString); + result = libFactorio.readMapExchangeString(testStrings.modified); assert.equal(result.checksum, 4092204126); }); @@ -66,7 +37,7 @@ describe("lib/factorio/exchange_string", function() { ); assert.throws( - () => libFactorio.readMapExchangeString(defaultString.slice(0, 100)), + () => libFactorio.readMapExchangeString(testStrings.default.slice(0, 100)), new Error("Not a map exchange string") ); @@ -90,7 +61,7 @@ describe("lib/factorio/exchange_string", function() { ); } - let pastEnd = Buffer.from(defaultString.replace(/><\n/g, ""), "base64"); + let pastEnd = Buffer.from(testStrings.default.replace(/><\n/g, ""), "base64"); // eslint-disable-next-line node/no-sync pastEnd = zlib.deflateSync(Buffer.concat([zlib.inflateSync(pastEnd), Buffer.from("junk")])); assert.throws( diff --git a/test/lib/factorio/test_strings.js b/test/lib/factorio/test_strings.js new file mode 100644 index 00000000..243d4a01 --- /dev/null +++ b/test/lib/factorio/test_strings.js @@ -0,0 +1,38 @@ +"use strict"; +// Exchange strings for testing with + +let default_ = ` +>>>eNpjZGBkUGQAgwZ7EOZgSc5PzIHxQJgrOb+gILVIN78oFVmYM +7moNCVVNz8TVXFqXmpupW5SYjGKYo7Movw8dBNYi0vy81BFSopSU +4uRRbhLixLzMktz0fUyMF5imubZ0CLHAML/6xkU/v8HYSDrAdAvI +MzA2ABRCRSDAdbknMy0NAYGBUcgdgJJMzIyVousc39YNcWeEaJGz +wHK+AAVOZAEE/GEMfwccEqpwBgmSOYYg8FnJAbE0hKgFVBVHA4IB +kSyBSTJyNj7duuC78cu2DH+Wfnxkm9Sgj2joavIuw9G6+yAkuwgf +zLBiVkzQWAnzCsMMDMf2EOlbtoznj0DAm/sGVlBOkRAhIMFkDjgz +czAKMAHZC3oARIKMgwwp9nBjBFxYEwDg28wnzyGMS7bo/sDGBA2I +MPlQMQJEAG2EO4yRgjTod+B0UEeJiuJUALUb8SA7IYUhA9Pwqw9j +GQ/mkMwIwLZH2giKg5YooELZGEKnHjBDHcNMDwvsMN4DvMdGJlBD +JCqL0AxCA8kAzMKQgs4gIObmQEBgGmjrO/lHgDxa6G4<<< +`; + +let modified = ` +>>>eNp1UzFs00AUvZ8mbRIEypCBSlCCyFAhOQqBKULxlQUxMCCxgnCc +S2rh2On5LAgMZOjAgISEurQLXSmCBTGwRWKhEkgIJraiMgBiaGmFOiC +FO5/PsUz6pfv+9/6/99+/kwEBOo2k6c83NhrZtOkaNkIDXa286fZ6hG +ouJaJIwTmT+i2iuZYdQ3GeOKTb15qGJ4oxQsMglbWo60iGYcSQ8Zjrh +GUhwighHpcRSBHIEZ8ajuV35dmBwIPK1DRxmMX6ADnqmre1Rb/D86UF +hNYX1lZncZpZNkGpmQ41PE87J6RFTe4YjND4hPDq943NwfIcEmv0AJV +GI7F4tMXViYVgECgCjoWWPm66DqOurXmEMcvp1A3/br1pGV5Oq1ZqVW +Hzk0ralCz5xDH79a5vM6tnW4Rma5XgQPVk8kTXtTzmU5Jg1g6tm0hfr +ZwPLGPaVrstL6p0SYwEAPeLLy9/vbeigxysgsPgIESGTYVcCYMnL/Bh +qbRKoQsq2NFBdt+LBbIp4y3CqiweBzK5LJIAu4vbD18f7Dfg77PdT1e +bt3S4/q2w5J39IbQfFW+TitzaqrA3ahSkOLf0MPVFhw/vhf3SYUqcOC +PczkoKQfXaNILCMb5df8Rd6QRS0hqKpoihHdgfNcm2Cj7ryTnKGC4K8 +jnh3gmXQRElVwYyxI8x4FMqOzsu4edrKK6hNZ5wU7V9G+ufEFL+7yHi +cySQMp7wDHnRsBW571ORGn6fH2fUDj/FwV0iUbXPMbmTP7akkt8ChiL +/BPceEs3j1M3iz71/ObT9tA==<<<`; + +module.exports = { + default: default_, + modified, +};