Send the research queue to followers after a finished technology is removed

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ILLISISandClaude Opus 5.5 committed 2026-10-19 08:54:04 +11:00
1 parent bedfe0873b
commit 2035f5bc2c
5 files changed
+66 -12

No files matched your search

+28 -7
View File
@@ -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<TechnologySync[]> => {
@@ -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) {
+4 -3
View File
@@ -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;
}
+2 -2
View File
@@ -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<typeof QueueEvent.jsonSchema>): QueueEvent {
+28
View File
@@ -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"]);
+4
View File
@@ -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"));