From 9dac57b36b0ec93c9e74e52f0b196f7aa41b5f59 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sun, 15 Mar 2020 17:41:52 +0100 Subject: [PATCH] Port Research Sync plugin Rewriwe the researchSync plugin into plugins/research_sync. --- plugins/research_sync/info.js | 80 ++++ plugins/research_sync/instance.js | 81 ++++ plugins/research_sync/master.js | 184 +++++++++ plugins/research_sync/module/module.json | 4 + plugins/research_sync/module/sync.lua | 215 ++++++++++ plugins/research_sync/test/missing.js | 5 + sharedPlugins/researchSync/config.js | 12 - sharedPlugins/researchSync/dumpResearch.lua | 24 -- sharedPlugins/researchSync/enableResearch.lua | 33 -- sharedPlugins/researchSync/index.js | 385 ------------------ sharedPlugins/researchSync/test/index.js | 128 ------ sharedPlugins/researchSync/updateProgress.lua | 33 -- test/integration/clusterio.js | 1 + 13 files changed, 570 insertions(+), 615 deletions(-) create mode 100644 plugins/research_sync/info.js create mode 100644 plugins/research_sync/instance.js create mode 100644 plugins/research_sync/master.js create mode 100644 plugins/research_sync/module/module.json create mode 100644 plugins/research_sync/module/sync.lua create mode 100644 plugins/research_sync/test/missing.js delete mode 100644 sharedPlugins/researchSync/config.js delete mode 100644 sharedPlugins/researchSync/dumpResearch.lua delete mode 100644 sharedPlugins/researchSync/enableResearch.lua delete mode 100644 sharedPlugins/researchSync/index.js delete mode 100644 sharedPlugins/researchSync/test/index.js delete mode 100644 sharedPlugins/researchSync/updateProgress.lua diff --git a/plugins/research_sync/info.js b/plugins/research_sync/info.js new file mode 100644 index 00000000..2e184014 --- /dev/null +++ b/plugins/research_sync/info.js @@ -0,0 +1,80 @@ +const link = require("lib/link"); + + +// schema used for syncing technologies to and from the master +const technologies = { + type: "array", + items: { + type: "array", + minItems: 4, + maxItems: 4, + items: [ + { type: "string" }, + { type: "integer" }, + { type: ["null", "number"] }, + { type: "boolean" }, + ] + }, +}; + +module.exports = { + name: "research_sync", + title: "Research Sync", + description: "Synchronises technology research progress between instances.", + version: "2.0.0-alpha", + instanceEntrypoint: "instance", + masterEntrypoint: "master", + + messages: { + contribution: new link.Event({ + type: "research_sync:contribution", + links: ["instance-slave", "slave-master"], + forwardTo: "master", + eventProperties: { + "name": { type: "string" }, + "level": { type: "integer" }, + "contribution": { type: "number" }, + }, + }), + progress: new link.Event({ + type: "research_sync:progress", + links: ["master-slave", "slave-instance"], + broadcastTo: "instance", + eventProperties: { + "technologies": { + type: "array", + items: { + additionalProperties: false, + required: ["name", "level", "progress"], + properties: { + "name": { type: "string" }, + "level": { type: "integer" }, + "progress": { type: "number" }, + }, + }, + }, + }, + }), + finished: new link.Event({ + type: "research_sync:finished", + links: ["instance-slave", "slave-master", "master-slave", "slave-instance"], + forwardTo: "master", + broadcastTo: "instance", + eventProperties: { + "name": { type: "string" }, + "level": { type: "integer" }, + }, + }), + syncTechnologies: new link.Request({ + type: "research_sync:sync_technologies", + links: ["instance-slave", "slave-master"], + forwardTo: "master", + requestProperties: { + "technologies": technologies, + }, + responseProperties: { + "technologies": technologies, + }, + }), + }, +} diff --git a/plugins/research_sync/instance.js b/plugins/research_sync/instance.js new file mode 100644 index 00000000..ffbeb3a0 --- /dev/null +++ b/plugins/research_sync/instance.js @@ -0,0 +1,81 @@ +const needle = require("needle"); + +const plugin = require("lib/plugin"); +const luaTools = require("lib/luaTools"); + + +function unexpectedError(err) { + console.log("Unexpected error in research_sync") + console.log("---------------------------------") + console.log(err); +} + +class InstancePlugin extends plugin.BaseInstancePlugin { + async init() { + this.instance.server.on("ipc-research_sync:contribution", (tech) => { + this.researchContribution(tech).catch(unexpectedError); + }); + this.instance.server.on("ipc-research_sync:finished", (tech) => { + this.researchFinished(tech).catch(unexpectedError); + }); + } + + async researchContribution(tech) { + this.info.messages.contribution.send(this.instance, tech); + } + + async progressEventHandler(message) { + let techsJson = luaTools.escapeString(JSON.stringify(message.data.technologies)); + await this.instance.server.sendRcon(`/sc research_sync.update_progress("${techsJson}")`, true); + } + + async researchFinished(tech) { + this.info.messages.finished.send(this.instance, tech); + } + + async finishedEventHandler(message) { + let { name, level, researched } = message.data; + await this.instance.server.sendRcon( + `/sc research_sync.research_technology("${luaTools.escapeString(name)}", ${level})`, true + ); + } + + async onStart() { + let dumpJson = await this.instance.server.sendRcon("/sc research_sync.dump_technologies()"); + let techsToSend = []; + let instanceTechs = new Map(); + for (let tech of JSON.parse(dumpJson)) { + techsToSend.push([ + tech.name, + tech.level, + tech.progress || null, + tech.researched, + ]); + instanceTechs.set(tech.name, tech); + } + + let response = await this.info.messages.syncTechnologies.send(this.instance, { technologies: techsToSend }); + let techsToSync = []; + for (let masterTech of response.technologies) { + let [name, level, progress, researched] = masterTech; + let instanceTech = instanceTechs.get(name); + if ( + !instanceTech + || instanceTech.level !== level + || (instanceTech.progress || null) !== progress + || instanceTech.researched !== researched + ) { + techsToSync.push(masterTech); + } + } + + if (techsToSync.length) { + let syncJson = luaTools.escapeString(JSON.stringify(techsToSync)); + await this.instance.server.sendRcon(`/sc research_sync.sync_technologies("${syncJson}")`, true); + } + } +} + +module.exports = { + InstancePlugin, +} diff --git a/plugins/research_sync/master.js b/plugins/research_sync/master.js new file mode 100644 index 00000000..deaf10c4 --- /dev/null +++ b/plugins/research_sync/master.js @@ -0,0 +1,184 @@ +const fs = require("fs-extra"); +const path = require("path"); + +const plugin = require("lib/plugin"); + + +class MasterPlugin extends plugin.BaseMasterPlugin { + async init() { + this.technologies = await loadTechnologies(this.master.config); + this.lastProgressBroadcast = Date.now(); + this.progressBroadcastId = null; + this.progressToBroadcast = new Set(); + } + + async onExit() { + if (this.progressBroadcastId) { + clearTimeout(this.progressBroadcastId); + } + await saveTechnologies(this.master.config, this.technologies); + } + + registerProgress() { + // Rate limit progress broadcasts to one per second + if (Date.now() < this.lastProgressBroadcast + 1000) { + if (!this.progressBroadcastId) { + this.progressBroadcastId = setTimeout(() => { + this.broadcastProgress(); + this.progressBroadcastId = null; + }, 2000); + } + + } else { + if (this.progressBroadcastId) { + clearTimeout(this.progressBroadcastId); + } + this.broadcastProgress(); + } + } + + broadcastProgress() { + let techs = [] + for (let name of this.progressToBroadcast) { + let tech = this.technologies.get(name); + if (tech.progress) { + techs.push({ name, level: tech.level, progress: tech.progress }); + } + } + this.progressToBroadcast.clear(); + + this.lastProgressBroadcast = Date.now(); + if (techs.length) { + for (let slaveConnection of this.master.slaveConnections.values()) { + this.info.messages.progress.send(slaveConnection, { technologies: techs }); + } + } + } + + async contributionEventHandler(message) { + let { name, level, contribution } = message.data; + let tech = this.technologies.get(name); + if (!tech) { + tech = { level, progress: 0, researched: false }; + this.technologies.set(name, tech) + + // Ignore contribution to already researched technologies + } else if (tech.level > level || tech.level === level && tech.researched) { + return; + } + + let newProgress = tech.progress + contribution; + if (newProgress < 1) { + tech.progress = newProgress; + this.progressToBroadcast.add(name); + this.registerProgress(); + + } else { + tech.researched = true; + tech.progress = null; + this.progressToBroadcast.delete(name); + + for (let slaveConnection of this.master.slaveConnections.values()) { + this.info.messages.finished.send(slaveConnection, { + name, + level: tech.level + }); + } + } + } + + async finishedEventHandler(message) { + let { name, level } = message.data; + let tech = this.technologies.get(name); + if (!tech || tech.level <= level) { + this.progressToBroadcast.delete(name); + this.technologies.set(name, { level, progress: null, researched: true }) + } + } + + async syncTechnologiesRequestHandler(message) { + function baseLevel(name) { + let match = /-(\d+)$/.exec(name); + if (!match) { + return 1; + } else { + return Number.parseInt(match[1], 10); + } + } + + for (let instanceTech of message.data.technologies) { + let [name, level, progress, researched] = instanceTech; + let tech = this.technologies.get(name); + if (!tech) { + this.technologies.set(name, { level, progress, researched }); + if (progress) { + this.progressToBroadcast.add(name); + } else if (researched || baseLevel(name) != level) { + for (let slaveConnection of this.master.slaveConnections.values()) { + this.info.messages.finished.send(slaveConnection, { name, level }); + } + } + + } else { + if (tech.level > level || tech.level === level && tech.researched) { + continue; + } + + if (tech.level < level || researched) { + tech.level = level; + tech.progress = progress; + tech.researched = researched; + for (let slaveConnection of this.master.slaveConnections.values()) { + this.info.messages.finished.send(slaveConnection, { name, level }); + } + + if (progress) { + this.progressToBroadcast.add(name); + } else { + this.progressToBroadcast.delete(name); + } + + } else if (tech.progress < progress) { + tech.progress = progress; + this.progressToBroadcast.add(name); + } + } + } + this.registerProgress(); + + let technologies = []; + for (let [name, tech] of this.technologies) { + technologies.push([ name, tech.level, tech.progress, tech.researched ]) + } + + return { technologies } + } +} + +async function loadTechnologies(masterConfig) { + let filePath = path.join(masterConfig.get("master.database_directory"), "technologies.json"); + console.log(`Loading ${filePath}`); + try { + return new Map(JSON.parse(await fs.readFile(filePath))); + + } catch (err) { + if (err.code === "ENOENT") { + console.log("Creating new technologies database"); + return new Map(); + + } else { + throw err; + } + } +} + +async function saveTechnologies(masterConfig, technologies) { + let filePath = path.join(masterConfig.get("master.database_directory"), "technologies.json"); + console.log(`writing ${filePath}`); + await fs.outputFile(filePath, JSON.stringify([...technologies.entries()], null, 4)); +} + + +module.exports = { + MasterPlugin +} diff --git a/plugins/research_sync/module/module.json b/plugins/research_sync/module/module.json new file mode 100644 index 00000000..c2b055e9 --- /dev/null +++ b/plugins/research_sync/module/module.json @@ -0,0 +1,4 @@ +{ + "name": "research_sync", + "load": ["sync.lua"] +} diff --git a/plugins/research_sync/module/sync.lua b/plugins/research_sync/module/sync.lua new file mode 100644 index 00000000..f7c88aae --- /dev/null +++ b/plugins/research_sync/module/sync.lua @@ -0,0 +1,215 @@ +local clusterio_api = require("modules/clusterio/api") + +local sync = {} + + +local function get_technology_progress(tech) + if tech == tech.force.current_research then + return tech.force.research_progress + else + return tech.force.get_saved_technology_progress(tech.name) + end +end + +local function set_technology_progress(tech, progress) + if tech == tech.force.current_research then + tech.force.research_progress = progress + else + tech.force.set_saved_technology_progress(tech.name, progress) + end +end + +sync.events = {} +sync.events[clusterio_api.events.on_server_startup] = function(event) + if not global.research_sync then + global.research_sync = { + technologies = {}, + } + end + + -- Used when syncing completed technologies from the master + global.research_sync.ignore_research_finished = false + + local force = game.forces["player"] + for _, tech in pairs(force.technologies) do + if tech.enabled then + local progress = get_technology_progress(tech) + global.research_sync.technologies[tech.name] = { + level = tech.level, + researched = tech.researched, + progress = progress, + } + end + end +end + +local function get_contribution(tech) + local progress = get_technology_progress(tech) + if not progress then + return 0, nil + end + + local prev_tech = global.research_sync.technologies[tech.name] + if prev_tech.progress and prev_tech.level == tech.level then + return progress - prev_tech.progress, progress + else + return progress, progress + end +end + +local function send_contribution(tech) + local contribution, progress = get_contribution(tech) + if contribution ~= 0 then + clusterio_api.send_json("research_sync:contribution", { + name = tech.name, + level = tech.level, + contribution = contribution, + }) + global.research_sync.technologies[tech.name].progress = progress + end +end + +sync.events[defines.events.on_research_started] = function(event) + local tech = event.last_research + if tech then + send_contribution(tech) + end +end + +sync.events[defines.events.on_research_finished] = function(event) + if global.research_sync.ignore_research_finished then + return + end + + local tech = event.research + global.research_sync.technologies[tech.name] = { + level = tech.level, + researched = tech.researched, + } + + local level = tech.level + if not tech.researched then + level = level - 1 + end + + clusterio_api.send_json("research_sync:finished", { + name = tech.name, + level = level, + }) +end + +sync.on_nth_tick = {} +sync.on_nth_tick[79] = function(event) + local tech = game.forces["player"].current_research + if tech then + send_contribution(tech) + end +end + +research_sync = {} +function research_sync.dump_technologies() + local force = game.forces["player"] + + local techs = {} + for _, tech in pairs(force.technologies) do + if tech.enabled then + table.insert(techs, { + name = tech.name, + level = tech.level, + progress = get_technology_progress(tech), + researched = tech.researched, + }) + end + end + + if #techs == 0 then + rcon.print("[]") + else + rcon.print(game.table_to_json(techs)) + end +end + +function research_sync.sync_technologies(data) + local force = game.forces["player"] + + local nameIndex = 1 + local levelIndex = 2 + local progressIndex = 3 + local researchedIndex = 4 + + global.research_sync.ignore_research_finished = true + for _, tech_data in pairs(game.json_to_table(data)) do + local tech = force.technologies[tech_data[nameIndex]] + if tech and tech.enabled and tech.level <= tech_data[levelIndex] then + tech.level = tech_data[levelIndex] + local progress + if tech_data[researchedIndex] then + tech.researched = true + progress = nil + elseif tech_data[progressIndex] then + send_contribution(tech) + progress = tech_data[progressIndex] + set_technology_progress(tech, progress) + else + progress = get_technology_progress(tech) + end + + global.research_sync.technologies[tech.name] = { + level = tech.level, + researched = tech.researched, + progress = progress, + } + end + end + global.research_sync.ignore_research_finished = false +end + +function research_sync.update_progress(data) + local techs = game.json_to_table(data) + local force = game.forces["player"] + + for _, masterTech in ipairs(techs) do + local tech = force.technologies[masterTech.name] + if tech and tech.enabled and tech.level == masterTech.level then + send_contribution(tech) + set_technology_progress(tech, masterTech.progress) + global.research_sync.technologies[tech.name] = { + level = tech.level, + progress = masterTech.progress + } + end + end +end + +function research_sync.research_technology(name, level) + local force = game.forces["player"] + local tech = force.technologies[name] + if not tech or not tech.enabled or tech.level > level then + return + end + + global.research_sync.ignore_research_finished = true + if tech == force.current_research and tech.level == level then + force.research_progress = 1 + + elseif tech.level < level or tech.level == level and not tech.researched then + tech.level = level + tech.researched = true + + if tech.name:find("-%d+$") then + game.print {"", "Researched ", {"technology-name." .. tech.name:gsub("-%d+$", "")}, level} + else + game.print {"", "Researched ", {"technology-name." .. tech.name}} + end + game.play_sound { path = "utility/research_completed" } + end + global.research_sync.ignore_research_finished = false + + global.research_sync.technologies[tech.name] = { + level = tech.level, + researched = tech.researched, + } +end + + +return sync diff --git a/plugins/research_sync/test/missing.js b/plugins/research_sync/test/missing.js new file mode 100644 index 00000000..c0b5b5ab --- /dev/null +++ b/plugins/research_sync/test/missing.js @@ -0,0 +1,5 @@ +// These are libraries that does not have any units test, they are +// required here in order to track coverage for them. + +require('../instance'); +require('../master'); diff --git a/sharedPlugins/researchSync/config.js b/sharedPlugins/researchSync/config.js deleted file mode 100644 index 8d1210dd..00000000 --- a/sharedPlugins/researchSync/config.js +++ /dev/null @@ -1,12 +0,0 @@ - - -module.exports = { - // Name of package. For display somewhere I guess. - name: "researchSync", - version: "1.0.0", - arguments: ["index.js"], - description: "syncs research across servers", - // We'll send everything in this file to your stdin. Beware. - scriptOutputFileSubscription: "researchSync.txt", - fileReadDelay: 50 -} diff --git a/sharedPlugins/researchSync/dumpResearch.lua b/sharedPlugins/researchSync/dumpResearch.lua deleted file mode 100644 index 27ec7946..00000000 --- a/sharedPlugins/researchSync/dumpResearch.lua +++ /dev/null @@ -1,24 +0,0 @@ - -local data = {}; -local force = game.forces["player"]; - -for _, tech in pairs(force.technologies) do - local progress; - if tech == force.current_research then - progress = force.research_progress - else - progress = force.get_saved_technology_progress(tech.name); - end - local infinite = tech.enabled == true and tech.prototype.max_level == 4294967295; - local tech_data = { - tech.name, - tostring(tech.researched), - tostring(tech.level), - tostring(progress), - tostring(infinite), - } - tech_data = table.concat(tech_data, ':') - table.insert(data, tech_data); -end - -game.write_file("researchSync.txt", table.concat(data, "\n"), false, 0) diff --git a/sharedPlugins/researchSync/enableResearch.lua b/sharedPlugins/researchSync/enableResearch.lua deleted file mode 100644 index 8c8b7329..00000000 --- a/sharedPlugins/researchSync/enableResearch.lua +++ /dev/null @@ -1,33 +0,0 @@ - -local function updateTech() - script.raise_event(defines.events.on_research_finished, {research=game.forces['player'].technologies['{tech_name}'], by_script=true}) - if {notify} then - if 1 == {tech_infinite} then - game.print("Infinite technology {tech_name} level {tech_level} unlocked") - else - game.print("Technology {tech_name} researched") - end - game.play_sound({path="utility/research_completed"}) - end -end - -local force = game.forces['player'] -local tech = force.technologies['{tech_name}'] -if tech then - if 1 == {tech_infinite} then - if (tech.level ~= level) then - tech.level = {tech_level} - if tech == force.current_research then - force.research_progress = {tech_progress} - else - force.set_saved_technology_progress(tech.name, {tech_progress}); - end - updateTech() - end - else - if not tech.researched then - tech.researched = true - updateTech() - end - end -end diff --git a/sharedPlugins/researchSync/index.js b/sharedPlugins/researchSync/index.js deleted file mode 100644 index 5e555cac..00000000 --- a/sharedPlugins/researchSync/index.js +++ /dev/null @@ -1,385 +0,0 @@ -const fs = require('fs'); -const path = require('path') -const needle = require("needle"); - - -function time() { - let d = new Date() - return `${d.getUTCHours()}:${d.getUTCMinutes()}:${d.getUTCSeconds()}.${d.getUTCMilliseconds()}` -} - -class ResearchSync { - constructor(slaveConfig, messageInterface, extras = {}){ - this.config = slaveConfig - this.messageInterface = messageInterface - - this.functions = { - dumpResearch: this.loadFunc("dumpResearch.lua"), - enableResearch: this.loadFunc("enableResearch.lua"), - updateProgress: this.loadFunc("updateProgress.lua") - } - - this.log_folder = './logs' - if (!fs.existsSync(this.log_folder)) - fs.mkdirSync(this.log_folder); - this.log_file = path.join(this.log_folder, `${this.config.instanceName}-research.log`) - - this.research = {} - this.prev_research = {} - this.initial_request_own_data( - () => this.setup_sync_task(extras) - ) - } - - log(data) { - try { - console.log(`researchSync: ${data}`) - fs.appendFileSync(this.log_file, `${time()}: ${data}\n`); - } catch (e) { - console.error(e) - } - } - - error(data) { - try { - console.error(`researchSync: ${data}`) - fs.appendFileSync(this.log_file, `${time()}: ${data}\n`); - } catch (e) { - console.error(e) - } - } - - initial_request_own_data(callback) { - const url = `${this.config.masterURL}/api/getSlaveMeta` - const data = { - instanceID: this.config.unique, - password: this.config.clientPassword, - } - const options = { - headers: {'x-access-token': this.config.masterAuthToken}, - json: true, - compressed: true - } - needle.post(url, data, options, (err, res, techs) => { - if (err) { - this.log(`Can't get own slave data:`) - this.error(err) - return - } - if (res.statusCode === 404) { - this.log('slave is not registered yet. Delaying for 5 secs') - setTimeout( - () => this.initial_request_own_data(callback), - 5000 - ) - return - } - if (res.statusCode !== 200) { - this.log(`Can't get own slave data:`) - this.error(`status code ${res.statusCode}, ${res.body}`) - return - } - if(techs.toString()!="") { - techs = JSON.parse(techs) - if (typeof techs.research === 'object') - this.research = techs.research - this.log('techs imported from master') - } else { - this.log('no techs imported from master, since there are none yet') - } - callback() - }) - } - - setup_sync_task(extras) { - const timeout = extras.researchSyncPollInterval || 5000 - setInterval(() => this.sync_task(), timeout); - } - - sync_task() { - this.messageInterface(this.functions.dumpResearch); - setTimeout(this.request_cluster_data.bind(this), 2000); - } - - request_cluster_data() { - const slaves_data_url = `${this.config.masterURL}/api/slaves` - needle.get(slaves_data_url, {compressed:true}, this.sync_researches.bind(this)) - } - - sync_researches(err, resp, slaves_data) { - if (err) { - this.messageInterface("Unable to post JSON master/api/slaves, master might be unreachable"); - return false; - } - if (resp.statusCode !== 200) { - this.messageInterface("got error when calling slaves", resp.statusCode, resp.body); - return; - } - - slaves_data = Object.values(slaves_data) - slaves_data = slaves_data.filter( - slave_data => slave_data.unique !== this.config.unique.toString() - && slave_data.meta && slave_data.meta.research - ) - - this.clear_contribution_to_researched_techs() - let cluster_techs = this.get_cluster_techs(slaves_data) - this.recount_cluster_research_progress(slaves_data, cluster_techs) - - let to_research = this.filter_researched_techs(cluster_techs) - let to_update_progress = this.filter_updated_techs(cluster_techs, to_research) - - this.research_technologies(to_research) - this.update_technologies_progress(to_update_progress) - - this.print_own_contribution() - - needle.post(this.config.masterURL + '/api/editSlaveMeta', { - instanceID: this.config.unique, - password: this.config.clientPassword, - meta: {research: this.research} - }, {headers: {'x-access-token': this.config.masterAuthToken}, json: true, compressed:true}, (err, resp) => { - if (err) - this.error(err) - }) - } - - clear_contribution_to_researched_techs() { - for (let [name, tech] of Object.entries(this.research)) { - if (!Object.hasOwnProperty.call(this.prev_research, name)) { - continue; - } - - let researched; - if (tech.infinite) { - researched = this.prev_research[name].level < tech.level; - } else { - researched = this.prev_research[name].researched < tech.researched; - } - - if (researched) { - tech.contribution = 0; - } - } - } - - get_cluster_techs(slavesData) { - let cluster_techs = {} - for (let slave_data of slavesData) { - let node_researches = slave_data.meta.research - for (let [name, node_tech] of Object.entries(node_researches)) { - if (isNaN(node_tech.researched) || isNaN(node_tech.level) || isNaN(node_tech.infinite)) - continue - - if (Object.hasOwnProperty.call(cluster_techs, name)) { - if (cluster_techs[name].infinite === 1 && cluster_techs[name].level < node_tech.level) { - cluster_techs[name].level = node_tech.level - } else if (node_tech.researched > cluster_techs[name].researched) { - cluster_techs[name].researched = 1 - } - } else { - cluster_techs[name] = node_tech - } - } - } - return cluster_techs - } - - recount_cluster_research_progress(slaves_data, cluster_researches) { - for (let [name, tech] of Object.entries(cluster_researches)) { - if (Object.hasOwnProperty.call(this.research, name)) { - tech.progress = this.research[name].contribution - } else { - tech.progress = 0; - } - } - - for (let slave_data of slaves_data) { - for (let [name, tech] of Object.entries(slave_data.meta.research)) { - if (!Object.hasOwnProperty.call(cluster_researches, name)) { - continue; - } - - if ( - isNaN(cluster_researches[name].progress) - || isNaN(tech.contribution) - || isNaN(tech.level) - || isNaN(cluster_researches[name].level) - ) { - continue; - } - - if (cluster_researches[name].level === tech.level) { - cluster_researches[name].progress += tech.contribution; - } - } - } - - for (let [name, tech] of Object.entries(cluster_researches)) { - if (Object.hasOwnProperty.call(this.research, name) && tech.progress > 1) { - tech.progress = null - tech.researched = 1 - this.research[name].contribution = 0 - tech.contribution = 0 - if (this.research[name].level >= tech.level) - tech.level = this.research[name].level + 1 - } - } - } - - filter_researched_techs(cluster_researches) { - let result = {}; - for (let [name, tech] of Object.entries(this.research)) { - if (!Object.hasOwnProperty.call(cluster_researches, name)) { - continue; - } - - if (isNaN(cluster_researches[name].researched) || isNaN(cluster_researches[name].level)) - continue - - let researched - if (tech.infinite) { - researched = tech.level < cluster_researches[name].level; - } else { - researched = tech.researched < cluster_researches[name].researched; - } - - if (researched) - result[name] = cluster_researches[name] - } - return result; - } - - filter_updated_techs(cluster_techs, to_research) { - let result = {} - for (let [name, tech] of Object.entries(this.research)) { - if (Object.hasOwnProperty.call(to_research, name) || !Object.hasOwnProperty.call(cluster_techs, name)) { - continue; - } - if (isNaN(cluster_techs[name].progress)) - continue - - // Ignore researched techs - if (!tech.infinite && tech.researched) { - continue; - } - - // Do not update progress for infinite techs with a local level that's higher. - if (tech.infinite && tech.level > cluster_techs[name].level) { - continue; - } - - if (tech.progress < cluster_techs[name].progress) - result[name] = cluster_techs[name] - } - return result - } - - research_technologies(to_research) { - for (let name of Object.keys(to_research)) - if (!Object.hasOwnProperty.call(this.research, name)) { - delete to_research[name]; - } - - const notify = Object.keys(to_research).length === 1 - for (let [name, tech] of Object.entries(to_research)) { - this.research[name].contribution = 0 - this.research[name].progress = null - let command = this.functions.enableResearch; - command = command.replace(/{tech_name}/g, name); - command = command.replace(/{tech_level}/g, tech.level); - command = command.replace(/{tech_infinite}/g, tech.infinite); - command = command.replace(/{tech_progress}/g, tech.progress) - command = command.replace(/{notify}/g, notify); - this.messageInterface(command); - let log_message = tech.infinite - ? `Unlocking infinite research ${name} at level ${this.research[name].level}` - : `Unlocking research ${name}` - this.log(log_message); - this.messageInterface(log_message); - this.research[name] = tech; - } - } - - update_technologies_progress(to_update) { - for (let [name, tech] of Object.entries(to_update)) { - if (!Object.hasOwnProperty.call(this.research, name)) { - continue; - } - let progress = this.research[name].progress - if (progress === null) - progress = 'nil' - let command = this.functions.updateProgress - command = command.replace(/{tech_name}/g, name) - command = command.replace(/{last_check_progress}/g, progress) - command = command.replace(/{new_progress}/g, tech.progress) - this.messageInterface(command); - this.log( - `Updating ${name}: ${this.research[name].progress} += ${tech.progress - this.research[name].progress}` - ); - this.research[name].progress = tech.progress - } - } - - print_own_contribution() { - for (let [name, tech] of Object.entries(this.research)) { - if (!Object.hasOwnProperty.call(this.prev_research, name)) { - continue; - } - let diff = this.research[name].contribution - this.prev_research[name].contribution - if (Math.abs(diff) > Number.EPSILON * 1000) - this.log(`Own research ${name}: ${this.research[name].progress} += ${diff}`) - } - } - - loadFunc(path, silent=true) { - let command = fs.readFileSync("sharedPlugins/researchSync/" + path,'utf-8') - command = command.replace(/\r?\n|\r/g,' ') - command = (silent ? '/silent-command ' : '/c ') + command - return command; - } - scriptOutput(data) { - let [name, researched, level, progress, infinite] = data.split(":") - researched = +(researched === 'true') - infinite = +(infinite === 'true') - level = parseInt(level); - if (progress === 'nil') - progress = null - else - progress = parseFloat(progress) - - if (isNaN(level) || isNaN(researched)) - return - - if (Object.hasOwnProperty.call(this.research, name)) { - this.prev_research[name] = this.research[name]; - } else { - this.prev_research[name] = { - researched: null, - level: null, - progress: null, - contribution: 0, - infinite, - } - } - this.research[name] = { - researched, - level, - progress, - contribution: this.prev_research[name].contribution, - infinite - } - if (this.prev_research[name].progress && this.research[name].progress) { - // this.prev_research[name].progress gets updated to overall cluster progress - // therefore contribution should be own research progress change over sync interval - let contribution = this.research[name].progress - this.prev_research[name].progress - this.research[name].contribution += contribution - } - if (Math.abs(this.research[name].contribution) < Number.EPSILON * 1000) { - // if contribution should be 0 but because of floating-point precision is e.g. 2.2564e-18 - this.research[name].contribution = 0 - } - } -} - -module.exports = ResearchSync; diff --git a/sharedPlugins/researchSync/test/index.js b/sharedPlugins/researchSync/test/index.js deleted file mode 100644 index 00f8788f..00000000 --- a/sharedPlugins/researchSync/test/index.js +++ /dev/null @@ -1,128 +0,0 @@ -const assert = require("assert").strict; -const nock = require("nock"); - -const isFactorioCommand = require("lib/isFactorioCommand"); - -const researchSync = require("../index"); - -describe("researchSync/index.js", function() { - it("exports a single class (or at least a function)", function() { - assert.equal(typeof researchSync, "function"); - }); - - const config = { - instanceName: "test", - masterURL: "http://localhost:8080", - unique: 99, - clientPassword: "password", - masterAuthToken: "masterToken", - }; - - const scope = nock('http://localhost:8080') - .post('/api/getSlaveMeta') - .reply(200, '{}') - .persist() - ; - - describe("class researchSync()", function() { - it(".filter_researched_techs(researces) returns research that needs enabling on the local instance", function() { - let reSync = new researchSync(config, function(){}); - - let local = { - a:{infinite: true, researched: true, level: 0}, - b:{infinite: true, researched: true, level: 0}, - c:{infinite: true, researched: false, level: 0}, - d:{infinite: true, researched: false, level: 10}, - }; - - let remote = { - a:{infinite: true, researched: true, level: 0}, - b:{infinite: true, researched: true, level: 1}, - c:{infinite: true, researched: false, level: 0}, - e:{infinite: true, researched: false, level: 50}, - }; - reSync.research = local; - let diffResult = reSync.filter_researched_techs(remote); - console.log(diffResult); - - assert.equal(diffResult.a, undefined); - - assert.notEqual(diffResult.b, undefined); - assert.equal(diffResult.b.researched, true); - assert.equal(diffResult.b.level, 1); - - assert.equal(diffResult.d, undefined); - - // filter_researched_techs is broken - this.skip(); - assert.notEqual(diffResult.e, undefined); - assert.equal(diffResult.e.researched, false); - assert.equal(diffResult.e.level, 50); - }); - it(".pollResearch() dumps a long Lua command", function() { - // pollResearch doesn't exist - this.skip(); - let reSync = new researchSync(config, io); - let ioRecieved; - function io(str){ - ioRecieved = true; - assert(isFactorioCommand(str), "pollResearch should run a command"); - } - - assert(!ioRecieved); - - reSync.pollResearch(); - - assert(ioRecieved); - }); - describe(".scriptOutput(data) handles file writes from factorio", function() { - it("wants a key:value pair of a single research, parses and saves it", function() { - let reSync = new researchSync(config, console.log); - let researches = [ - 'automation:false:0:nil', - 'automation-2:false:0:nil', - 'automation-3:false:0:nil', - 'electronics:false:0:nil', - 'advanced-electronics:false:0:nil', - 'advanced-electronics-2:false:0:nil', - 'circuit-network:false:0:nil', - 'explosives:false:0:nil', - 'logistics:false:0:nil', - 'logistics-2:false:0:nil', - 'logistics-3:false:0:nil', - 'optics:false:0:nil', - 'laser:false:0:nil', - 'solar-energy:false:0:nil', - 'turrets:false:0:nil', - 'laser-turrets:false:0:nil', - 'stone-walls:false:0:nil', - 'gates:false:0:nil', - 'engine:false:0:nil', - ] - researches.forEach(research => { - reSync.scriptOutput(research); - assert(!isFactorioCommand("/c x = "+research)); - }); - assert.equal(Object.keys(reSync.research).length, researches.length); - }); - }); - it("regularily polls and syncs research (configurable delay)", function(done) { - let reSync = new researchSync(config, io, { - researchSyncPollInterval: 100, - }); - let ioRecieved; - function io(str){ - ioRecieved = true; - assert(isFactorioCommand(str), "pollResearch should run a command"); - } - function waitForIo(){ - if(!ioRecieved){ - setTimeout(waitForIo,50); - } else { - done(); - } - } - waitForIo(); - }); - }); -}); diff --git a/sharedPlugins/researchSync/updateProgress.lua b/sharedPlugins/researchSync/updateProgress.lua deleted file mode 100644 index 0d07bb84..00000000 --- a/sharedPlugins/researchSync/updateProgress.lua +++ /dev/null @@ -1,33 +0,0 @@ - -local function get_tech_progress(force, tech) - if tech == force.current_research then - return force.research_progress - else - return force.get_saved_technology_progress(tech.name); - end -end -local function set_tech_progress(force, tech, progress) - if tech == force.current_research then - force.research_progress = progress - else - force.set_saved_technology_progress(tech.name, progress); - end -end - -local force = game.forces['player'] -local tech_name = '{tech_name}' -local tech = force.technologies[tech_name] -local last_check_progress = {last_check_progress} -local new_progress = {new_progress} - -local progress_delta = get_tech_progress(force, tech) -if last_check_progress ~= nil then - progress_delta = progress_delta - last_check_progress -end -if progress_delta ~= nil then - new_progress = new_progress + progress_delta -end -if new_progress > 1 then - new_progress = 1 -end -set_tech_progress(force, tech, new_progress) diff --git a/test/integration/clusterio.js b/test/integration/clusterio.js index 2815856d..ffd5a869 100644 --- a/test/integration/clusterio.js +++ b/test/integration/clusterio.js @@ -89,6 +89,7 @@ describe("Integration of Clusterio", function() { // XXX breaks tests. master._db.instances.get(44).set("subspace_storage.enabled", false); + master._db.instances.get(44).set("research_sync.enabled", false); }); });