diff --git a/plugins/research_sync/controller.ts b/plugins/research_sync/controller.ts index add05517..a617b2c1 100644 --- a/plugins/research_sync/controller.ts +++ b/plugins/research_sync/controller.ts @@ -86,16 +86,25 @@ export default async function(context: ControllerPluginContext) { return controller.instances.get(src.id)?.config.get("instance.name") ?? String(src.id); } - // Removes one entry per level that finished - function removeFinished(name: string, levels = 1) { + // Removes one entry per level that finished, returns whether any was removed + function removeFinished(name: string, levels = 1): boolean { + let removed = false; for (let i = 0; i < levels; i++) { let index = queue.indexOf(name); if (index === -1) { - return; + break; } queue.splice(index, 1); queueDirty = true; + removed = true; } + return removed; + } + + // Instances only advance the technology on a finish, so they need the queue without it. + // No player or instance: the finished research prints its own chat line. + function broadcastQueueSilently() { + controller.sendTo("allInstances", new QueueEvent(queue, null, null)); } function setQueue(newQueue: string[], player: string | null, src: lib.Address) { @@ -162,9 +171,12 @@ export default async function(context: ControllerPluginContext) { tech.researched = true; tech.progress = null; progressToBroadcast.delete(name); - removeFinished(name); + let removed = removeFinished(name); controller.sendTo("allInstances", new FinishedEvent(name, tech.level)); + if (removed) { + broadcastQueueSilently(); + } } technologiesDirty = true; }); @@ -172,8 +184,9 @@ export default async function(context: ControllerPluginContext) { controller.handle(FinishedEvent, async (event: FinishedEvent) => { let { name, level } = event; let tech = technologies.get(name); + let removed = false; if (!tech || tech.level < level || (tech.level === level && !tech.researched)) { - removeFinished(name); + removed = removeFinished(name); } if (!tech || tech.level <= level) { controller.sendTo("allInstances", event); @@ -181,6 +194,9 @@ export default async function(context: ControllerPluginContext) { technologies.set(name, { level, progress: null, researched: true }); technologiesDirty = true; } + if (removed) { + broadcastQueueSilently(); + } }); controller.handle(SyncTechnologiesRequest, async (request: SyncTechnologiesRequest): Promise => { @@ -192,6 +208,7 @@ export default async function(context: ControllerPluginContext) { return Number.parseInt(match[1], 10); } + let removed = false; for (let instanceTech of request.technologies) { let { name, level, progress, researched } = instanceTech; let tech = technologies.get(name); @@ -203,7 +220,8 @@ export default async function(context: ControllerPluginContext) { } else if (researched || baseLevel(name) !== level) { controller.sendTo("allInstances", new FinishedEvent(name, level)); // One queue entry per level unlocked - removeFinished(name, Math.max(level - Number(!researched) - baseLevel(name) + 1, 1)); + let levels = Math.max(level - Number(!researched) - baseLevel(name) + 1, 1); + removed = removeFinished(name, levels) || removed; } } else { @@ -216,7 +234,7 @@ export default async function(context: ControllerPluginContext) { let unlocked = level - Number(!researched) - (tech.level - Number(!tech.researched)); if (unlocked > 0) { controller.sendTo("allInstances", new FinishedEvent(name, level - Number(!researched))); - removeFinished(name, unlocked); + removed = removeFinished(name, unlocked) || removed; } tech.level = level; tech.progress = progress; @@ -237,6 +255,9 @@ export default async function(context: ControllerPluginContext) { } } progressRateLimiter.activate(); + if (removed) { + broadcastQueueSilently(); + } let result = []; for (let [name, tech] of technologies) { diff --git a/plugins/research_sync/instance.ts b/plugins/research_sync/instance.ts index a7124a51..7e7b5109 100644 --- a/plugins/research_sync/instance.ts +++ b/plugins/research_sync/instance.ts @@ -109,9 +109,10 @@ export default async function(context: InstancePluginContext) { if (!syncStarted || !["starting", "running"].includes(instance.status)) { return; } - let data: { queue: string[], instance: string, player?: string } = { - queue: event.queue, instance: event.instance, - }; + let data: { queue: string[], instance?: string, player?: string } = { queue: event.queue }; + if (event.instance !== null) { + data.instance = event.instance; + } if (event.player !== null) { data.player = event.player; } diff --git a/plugins/research_sync/messages.ts b/plugins/research_sync/messages.ts index b10580c3..58d5a38d 100644 --- a/plugins/research_sync/messages.ts +++ b/plugins/research_sync/messages.ts @@ -170,14 +170,14 @@ export class QueueEvent { constructor( public queue: string[], public player: string | null, - public instance: string, + public instance: string | null, ) { } static jsonSchema = Type.Object({ "queue": Type.Array(Type.String()), "player": Type.Union([Type.String(), Type.Null()]), - "instance": Type.String(), + "instance": Type.Union([Type.String(), Type.Null()]), }); static fromJSON(json: Static): QueueEvent { diff --git a/plugins/research_sync/test/controller.js b/plugins/research_sync/test/controller.js index 83171d9a..5fbbfb91 100644 --- a/plugins/research_sync/test/controller.js +++ b/plugins/research_sync/test/controller.js @@ -110,6 +110,31 @@ describe("research_sync controller", function() { await finished(new FinishedEvent("m", 3), src); assert.deepEqual(await sync(), ["m", "x"]); }); + it("should send the queue silently after a completion behind the current research", async function() { + await update(["automation", "m", "m"]); + sent.length = 0; + await mock.getHandler(controller, ContributionEvent)(new ContributionEvent("m", 3, 1), src); + assert.equal(queueEvents().length, 1); + assert.deepEqual(queueEvents()[0][1], new QueueEvent(["automation", "m"], null, null)); + }); + it("should send the queue after an advancing FinishedEvent only", async function() { + await update(["m", "x"]); + sent.length = 0; + const finished = mock.getHandler(controller, FinishedEvent); + await finished(new FinishedEvent("m", 3), src); + assert.equal(queueEvents().length, 1); + assert.deepEqual(queueEvents()[0][1], new QueueEvent(["x"], null, null)); + sent.length = 0; + await finished(new FinishedEvent("m", 3), src); + assert.equal(queueEvents().length, 0); + }); + it("should send no queue when the finished technology is not queued", async function() { + await update(["x"]); + sent.length = 0; + await mock.getHandler(controller, FinishedEvent)(new FinishedEvent("m", 3), src); + await mock.getHandler(controller, ContributionEvent)(new ContributionEvent("n", 3, 1), src); + assert.equal(queueEvents().length, 0); + }); it("should drop an entry finished through a technology sync", async function() { await update(["m", "x"]); await mock.getHandler(controller, SyncTechnologiesRequest)( @@ -126,8 +151,11 @@ describe("research_sync controller", function() { it("should drop one entry per level unlocked on a known technology", async function() { await techSync("m", 4, true); await update(["m", "m", "m", "x"]); + sent.length = 0; await techSync("m", 7, true); assert.deepEqual(await sync(), ["x"]); + assert.equal(queueEvents().length, 1); + assert.deepEqual(queueEvents()[0][1], new QueueEvent(["x"], null, null)); }); it("should drop one entry per level unlocked on a new technology", async function() { await update(["m", "m", "x"]); diff --git a/plugins/research_sync/test/instance.js b/plugins/research_sync/test/instance.js index 2abab6c8..0ef10c6e 100644 --- a/plugins/research_sync/test/instance.js +++ b/plugins/research_sync/test/instance.js @@ -78,6 +78,10 @@ describe("research_sync instance", function() { await handler()(new QueueEvent(["a"], null, "rs-2")); assert.equal(lastCommand(), applyCommand({ queue: ["a"], instance: "rs-2" })); }); + it("should leave out a null instance", async function() { + await handler()(new QueueEvent(["a"], null, null)); + assert.equal(lastCommand(), applyCommand({ queue: ["a"] })); + }); it("should ignore the event while stopped", async function() { instance.status = "stopped"; await handler()(new QueueEvent(["a"], null, "rs-2"));