diff --git a/.gitignore b/.gitignore index d12f04bb..a7049f1f 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,6 @@ sharedPlugins/scenario_loader/scenarios sharedPlugins/*/DISABLED \.cache-require-paths\.json + +# IDE +.idea/ diff --git a/master.js b/master.js index cba39301..358e911f 100644 --- a/master.js +++ b/master.js @@ -378,8 +378,13 @@ app.post("/api/getSlaveMeta", function (req, res) { endpointHitCounter.labels(req.route.path).inc(); console.log("body", req.body); if(req.body && req.body.instanceID && req.body.password){ - console.log("returning meta for ", req.body.instanceID); - res.send(JSON.stringify(slaves[req.body.instanceID].meta)) + if (slaves[req.body.instanceID]) { + console.log("returning meta for ", req.body.instanceID); + res.send(JSON.stringify(slaves[req.body.instanceID].meta)) + } else { + res.status(404) + res.send('{"status": 404, "info": "Slave not registered"}') + } } else { res.status(400); res.send('{"INVALID REQUEST":1}'); diff --git a/sharedPlugins/researchSync/dumpResearch.lua b/sharedPlugins/researchSync/dumpResearch.lua index 94b61b0a..27ec7946 100644 --- a/sharedPlugins/researchSync/dumpResearch.lua +++ b/sharedPlugins/researchSync/dumpResearch.lua @@ -1,13 +1,24 @@ -/silent-command local data = {}; +local force = game.forces["player"]; -for _, tech in pairs(game.forces["player"].technologies) do - if tech.enabled == true and tech.prototype.max_level == 4294967295 then - table.insert(data, tech.name .. ":" .. tostring(tech.researched) .. ":" .. tostring(tech.level) .. ":true"); +for _, tech in pairs(force.technologies) do + local progress; + if tech == force.current_research then + progress = force.research_progress else - table.insert(data, tech.name .. ":" .. tostring(tech.researched) .. ":" .. tostring(tech.level) .. ":false"); + 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 index 155a8ecc..4e5af42a 100644 --- a/sharedPlugins/researchSync/enableResearch.lua +++ b/sharedPlugins/researchSync/enableResearch.lua @@ -1,4 +1,4 @@ -/silent-command + if game.forces['player'].technologies['{tech_name}'] then if 1 == {tech_infinite} then game.forces['player'].technologies['{tech_name}'].level = {tech_level} diff --git a/sharedPlugins/researchSync/index.js b/sharedPlugins/researchSync/index.js index adb811d5..d8cb5887 100644 --- a/sharedPlugins/researchSync/index.js +++ b/sharedPlugins/researchSync/index.js @@ -1,162 +1,311 @@ -const fs = require('fs'); +const fs = require('fs'); const needle = require("needle"); + class ResearchSync { - constructor(slaveConfig, messageInterface, extras = {}) { - this.config = slaveConfig; + constructor(slaveConfig, messageInterface, extras = {}){ + this.config = slaveConfig; this.messageInterface = messageInterface; - this.functions = this.loadFunctions(); + this.functions = { + dumpResearch: this.loadFunc("dumpResearch.lua"), + enableResearch: this.loadFunc("enableResearch.lua"), + updateProgress: this.loadFunc("updateProgress.lua") + } this.research = {}; - - setInterval( - () => { - this.pollResearch(); - setTimeout(this.doSync.bind(this), 5000); - }, - extras.researchSyncPollInterval || 30000 - ); + this.prev_research = {}; + this.initial_request_own_data( + () => this.setup_sync_task(extras) + ) } - pollResearch() { - // this.messageInterface("Polling Research\n") - this.messageInterface(this.functions.dumpResearch); - } - - doSync() { - needle.get(this.config.masterIP + ':' + this.config.masterPort + '/api/slaves', (err, resp, slaveData) => { + initial_request_own_data(callback) { + const url = `${this.config.masterIP}:${this.config.masterPort}/api/getSlaveMeta` + const data = { + instanceID: this.config.unique, + password: this.config.clientPassword, + } + const options = { + headers: {'x-access-token': this.config.masterAuthToken}, + json: true + } + needle.post(url, data, options, (err, res, techs) => { if (err) { - this.messageInterface("Unable to post JSON master/api/slaves, master might be unreachable"); - return false; + console.log(`Can't get own slave data:`) + console.error(err) + return } - if (resp.statusCode !== 200) { - this.messageInterface("got error when calling slaves", resp.statusCode, resp.body); - return; + if (res.statusCode === 404) { + console.log('researchSync: slave is not registered yet. Delaying for 5 secs') + setTimeout( + () => this.initial_request_own_data(callback), + 5000 + ) + return } + if (res.statusCode !== 200) { + console.log(`Can't get own slave data:`) + console.error(`status code ${res.statusCode}, ${res.body}`) + return + } + techs = JSON.parse(techs) + if (typeof techs.research === 'object') + this.research = techs.research + console.log('researchSync: techs imported from master') + callback() + }) + } - let needResearch = {}; + setup_sync_task(extras) { + const timeout = extras.researchSyncPollInterval || 5000 + setInterval(() => this.sync_task(), timeout); + } - Object.keys(slaveData).forEach(instanceKey => { - if (slaveData[instanceKey].unique === this.config.unique.toString()) { - return; - } - if (!slaveData[instanceKey].hasOwnProperty('meta') - || !slaveData[instanceKey].meta.hasOwnProperty('research') - ) { - return; - } - let researchList = slaveData[instanceKey].meta.research; - if (researchList) { - Object.keys(researchList).forEach(researchName => { - if ( - isNaN(researchList[researchName].researched) - || isNaN(researchList[researchName].level) - || isNaN(researchList[researchName].infinite)) - { - return; - } - if (needResearch.hasOwnProperty(researchName)) { - if ( - needResearch[researchName].infinite === 1 - && needResearch[researchName].level < parseInt(researchList[researchName].level) - ) { - needResearch[researchName].level = parseInt(researchList[researchName].level); - } else if ( - needResearch[researchName].researched === 0 - && parseInt(researchList[researchName].researched) === 1 - ) { - needResearch[researchName].researched = 1 - } - } else { - needResearch[researchName] = { - researched: parseInt(researchList[researchName].researched), - level : parseInt(researchList[researchName].level), - infinite : parseInt(researchList[researchName].infinite) - }; - } - }); - } - }); + sync_task() { + this.messageInterface(this.functions.dumpResearch); + setTimeout(this.request_cluster_data.bind(this), 2000); + } - let difference = this.filterResearchDiff(this.research, needResearch); + request_cluster_data() { + const slaves_data_url = `${this.config.masterIP}:${this.config.masterPort}/api/slaves` + needle.get(slaves_data_url, this.sync_researches.bind(this)) + } - let count = Object.keys(difference).length; - let counter = 1; - Object.keys(difference).forEach((key) => { - if (this.research[key]) { - let command = this.functions.enableResearch; - while (command.includes("{tech_name}")) { - command = command.replace("{tech_name}", key); - command = command.replace("{tech_researched}", difference[key].researched); - command = command.replace("{tech_level}", difference[key].level); - command = command.replace("{tech_infinite}", difference[key].infinite); - command = command.replace("{notify}", count > 1 || count === counter ? 'false' : 'true'); - } - this.messageInterface(command); - let log_message = ''; - if (difference[key].infinite === 1) { - log_message = 'Unlocking infinite research ' + key + 'at level ' + this.research[key].level; - } else { - log_message = 'Unlocking research ' + key; - } - console.log(log_message); - this.messageInterface(log_message); - this.research[key] = difference[key]; - ++counter; - } - }); + 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; + } - needle.post(this.config.masterIP + ':' + this.config.masterPort + '/api/editSlaveMeta', { - instanceID: this.config.unique, - password : this.config.clientPassword, - meta : {research: this.research} - }, {headers: {'x-access-token': this.config.masterAuthToken}}, function (err, resp) { - // success? - }); + 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.masterIP + ':' + this.config.masterPort + '/api/editSlaveMeta', { + instanceID: this.config.unique, + password: this.config.clientPassword, + meta: {research: this.research} + }, {headers: {'x-access-token': this.config.masterAuthToken}, json: true}, function (err, resp) { + // it doesn't get called + if (err) + console.error(err) + else + console.log('sent') }); } - filterResearchDiff(localResearch, remoteResearch) { - let diff = {}; - Object.keys(localResearch).forEach((key) => { - if (remoteResearch.hasOwnProperty(key)) { - if ( - (localResearch[key].infinite === 0 && localResearch[key].researched === 0 && remoteResearch[key].researched === 1) - || - (localResearch[key].infinite === 1 && localResearch[key].level < remoteResearch[key].level) - ) { - if (!isNaN(remoteResearch[key].researched) && !isNaN(remoteResearch[key].level) && !isNaN(remoteResearch[key].infinite)) { - diff[key] = {researched: remoteResearch[key].researched, level: remoteResearch[key].level, infinite: remoteResearch[key].infinite}; + clear_contribution_to_researched_techs() { + // if between updates tech was researched + if (!this.prev_research) + return + for (let [name, research] of Object.entries(this.research)) { + let researched + if (research.infinite) + researched = this.prev_research[name].level < research.level + else + researched = this.prev_research[name].researched < research.researched + + if (researched) + research.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 (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 diff; + } + return cluster_techs } - loadFunctions() { - return { - dumpResearch : this.loadFunc("dumpResearch.lua"), - enableResearch: this.loadFunc("enableResearch.lua"), - }; + recount_cluster_research_progress(slaves_data, cluster_researches) { + for (let [name, research] of Object.entries(cluster_researches)) + research.progress = this.research[name].contribution + + for (let slave_data of slaves_data) + for (let [name, research] of Object.entries(slave_data.meta.research)) + if (!isNaN(cluster_researches[name].progress) && !isNaN(research.contribution)) + cluster_researches[name].progress += research.contribution + + for (let [name, research] of Object.entries(cluster_researches)) { + if (research.progress > 1) { + research.progress = null + research.researched = 1 + this.research[name].contribution = 0 + research.contribution = 0 + if (this.research[name].level >= research.level) + research.level = this.research[name].level + 1 + } + } } - loadFunc(path) { - return fs.readFileSync("sharedPlugins/researchSync/" + path, 'utf-8').replace(/\r?\n|\r/g, ' '); + filter_researched_techs(cluster_researches) { + let local_researches = this.research + let result = {}; + for (let key in local_researches) { + if (!cluster_researches[key]) + continue + if (isNaN(cluster_researches[key].researched) || isNaN(cluster_researches[key].level)) + continue + + let researched + if (local_researches[key].infinite) + researched = local_researches[key].level < cluster_researches[key].level + else + researched = local_researches[key].researched < cluster_researches[key].researched + + if (researched) + result[key] = cluster_researches[key] + } + return result; } + filter_updated_techs(cluster_techs, to_research) { + let local_techs = this.research + let result = {} + for (let name in local_techs) { + if (name in to_research || !cluster_techs[name]) + continue + if (isNaN(cluster_techs[name].progress)) + continue + if (local_techs[name].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 (!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_researched}/g, tech.researched); + command = command.replace(/{tech_level}/g, tech.level); + command = command.replace(/{tech_infinite}/g, tech.infinite); + 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}` + console.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 (!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); + console.log( + `Updating ${name}: ${this.research[name].progress} += ${tech.progress - this.research[name].progress}` + ); + this.research[name].progress = tech.progress + } + } + + print_own_contribution() { + if (!this.prev_research) + return + for (let [name, tech] of Object.entries(this.research)) { + if (!this.prev_research[name]) + continue + let diff = this.research[name].contribution - this.prev_research[name].contribution + if (Math.abs(diff) > Number.EPSILON * 1000) + console.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 kv = data.split(":"); - let name = kv[0]; - let researched = ('true' !== kv[1] - ? 0 - : 1); - let level = parseInt(kv[2]); - let infinite = ('true' !== kv[3] - ? 0 - : 1); - if (!isNaN(level) && !isNaN(researched)) { - this.research[name] = {researched: researched, level: level, infinite: infinite}; + 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 + this.prev_research[name] = this.research[name] + if (!this.prev_research[name]) { + 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 } } } diff --git a/sharedPlugins/researchSync/index.spec.js b/sharedPlugins/researchSync/index.spec.js index eae51abc..372bf01e 100644 --- a/sharedPlugins/researchSync/index.spec.js +++ b/sharedPlugins/researchSync/index.spec.js @@ -9,7 +9,7 @@ describe("researchSync/index.js", ()=>{ assert.equal(typeof researchSync, "function"); }); describe("class researchSync()", ()=>{ - it(".filterResearchDiff(object1, object2) returns research that needs enabling on the local instance", ()=>{ + it(".filter_researched_techs(object1, object2) returns research that needs enabling on the local instance", ()=>{ let reSync = new researchSync({}, function(){}); let obj1 = { @@ -24,9 +24,9 @@ describe("researchSync/index.js", ()=>{ c:{researched: false, level: 0}, e:{researched: false, level: 50}, }; - assert(typeof reSync.filterResearchDiff == "function"); + assert(typeof reSync.filter_researched_techs == "function"); - let diffResult = reSync.filterResearchDiff(obj1, obj2); + let diffResult = reSync.filter_researched_techs(obj1, obj2); assert(diffResult.a === undefined); diff --git a/sharedPlugins/researchSync/updateProgress.lua b/sharedPlugins/researchSync/updateProgress.lua new file mode 100644 index 00000000..0d07bb84 --- /dev/null +++ b/sharedPlugins/researchSync/updateProgress.lua @@ -0,0 +1,33 @@ + +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)