mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-14 23:24:52 +09:00
Resync research after a new controller session
While the host has no controller session, events between the instance and the controller are dropped in both directions, and research_sync only synced technologies on instance start. A technology finished during the outage stayed unresearched on the controller and on every other instance until the instance restarted. On a new session the instance now runs the same technology sync as on start, before taking the cluster queue. Contributions made while the controller is unreachable are summed per technology and sent on connect or resume, since progress from several instances can't be rebuilt from a state sync that keeps the highest progress. A finish made while the connection was dropped triggers the sync on resume too. sync_technologies now runs while the game is running, so it skips writing progress to a technology that finished after the dump. Fixes #1123 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fa2883d74e
commit
5589609b1b
4 files changed
+246
-44
No files matched your search
@@ -24,9 +24,13 @@ type IpcFinished = {
|
||||
type IpcQueue = { queue: unknown, player?: string };
|
||||
|
||||
export default async function(context: InstancePluginContext) {
|
||||
const { instance, logger, plugin } = context;
|
||||
const { instance, host, logger, plugin } = context;
|
||||
let syncStarted = false;
|
||||
let queueEventsApplied = 0;
|
||||
// Contributions made while the controller is unreachable, summed per technology.
|
||||
// Unlike finished research they can't be recovered from the game state.
|
||||
const pendingContributions = new Map<string, IpcContribution>();
|
||||
let finishedWhileDisconnected = false;
|
||||
|
||||
// Commands are sent one at a time so they execute in order
|
||||
let rconQueue: Promise<unknown> = Promise.resolve();
|
||||
@@ -69,11 +73,68 @@ export default async function(context: InstancePluginContext) {
|
||||
}
|
||||
|
||||
async function researchContribution(tech: IpcContribution) {
|
||||
instance.sendTo("controller", new ContributionEvent(tech.name, tech.level, tech.contribution));
|
||||
if (host.connector.connected) {
|
||||
instance.sendTo("controller", new ContributionEvent(tech.name, tech.level, tech.contribution));
|
||||
return;
|
||||
}
|
||||
let pending = pendingContributions.get(tech.name);
|
||||
if (pending && pending.level === tech.level) {
|
||||
pending.contribution += tech.contribution;
|
||||
} else if (!pending || pending.level < tech.level) {
|
||||
// A lower level finished here, the technology sync reports it
|
||||
pendingContributions.set(tech.name, { ...tech });
|
||||
}
|
||||
}
|
||||
|
||||
async function researchFinished(tech: IpcFinished) {
|
||||
instance.sendTo("controller", new FinishedEvent(tech.name, tech.level));
|
||||
if (host.connector.connected) {
|
||||
instance.sendTo("controller", new FinishedEvent(tech.name, tech.level));
|
||||
return;
|
||||
}
|
||||
finishedWhileDisconnected = true;
|
||||
}
|
||||
|
||||
function sendPendingContributions() {
|
||||
for (let tech of pendingContributions.values()) {
|
||||
instance.sendTo("controller", new ContributionEvent(tech.name, tech.level, tech.contribution));
|
||||
}
|
||||
pendingContributions.clear();
|
||||
}
|
||||
|
||||
async function syncTechnologies() {
|
||||
let dumpJson = await sendOrderedRcon("/sc research_sync.dump_technologies()");
|
||||
let techsToSend = [];
|
||||
let instanceTechs = new Map();
|
||||
for (let tech of JSON.parse(dumpJson)) {
|
||||
techsToSend.push(new TechnologySync(
|
||||
tech.name,
|
||||
tech.level,
|
||||
tech.progress || null,
|
||||
tech.researched,
|
||||
));
|
||||
instanceTechs.set(tech.name, tech);
|
||||
}
|
||||
|
||||
let controllerTechs = await instance.sendTo("controller", new SyncTechnologiesRequest(techsToSend));
|
||||
syncStarted = true;
|
||||
let techsToSync = [];
|
||||
for (let controllerTech of controllerTechs) {
|
||||
let { name, level, progress, researched } = controllerTech;
|
||||
let instanceTech = instanceTechs.get(name);
|
||||
if (
|
||||
!instanceTech
|
||||
|| instanceTech.level !== level
|
||||
|| (instanceTech.progress || null) !== progress
|
||||
|| instanceTech.researched !== researched
|
||||
) {
|
||||
techsToSync.push(controllerTech);
|
||||
}
|
||||
}
|
||||
|
||||
if (techsToSync.length) {
|
||||
let syncJson = lib.escapeString(JSON.stringify(techsToSync));
|
||||
await sendOrderedRcon(`/sc research_sync.sync_technologies("${syncJson}")`, true);
|
||||
}
|
||||
}
|
||||
|
||||
instance.server.on("ipc-research_sync:contribution", (tech: IpcContribution) => {
|
||||
@@ -128,52 +189,35 @@ export default async function(context: InstancePluginContext) {
|
||||
});
|
||||
|
||||
instance.hooks.start.attach(plugin.name, async () => {
|
||||
let dumpJson = await sendOrderedRcon("/sc research_sync.dump_technologies()");
|
||||
let techsToSend = [];
|
||||
let instanceTechs = new Map();
|
||||
for (let tech of JSON.parse(dumpJson)) {
|
||||
techsToSend.push(new TechnologySync(
|
||||
tech.name,
|
||||
tech.level,
|
||||
tech.progress || null,
|
||||
tech.researched,
|
||||
));
|
||||
instanceTechs.set(tech.name, tech);
|
||||
}
|
||||
|
||||
let controllerTechs = await instance.sendTo("controller", new SyncTechnologiesRequest(techsToSend));
|
||||
syncStarted = true;
|
||||
let techsToSync = [];
|
||||
for (let controllerTech of controllerTechs) {
|
||||
let { name, level, progress, researched } = controllerTech;
|
||||
let instanceTech = instanceTechs.get(name);
|
||||
if (
|
||||
!instanceTech
|
||||
|| instanceTech.level !== level
|
||||
|| (instanceTech.progress || null) !== progress
|
||||
|| instanceTech.researched !== researched
|
||||
) {
|
||||
techsToSync.push(controllerTech);
|
||||
}
|
||||
}
|
||||
|
||||
if (techsToSync.length) {
|
||||
let syncJson = lib.escapeString(JSON.stringify(techsToSync));
|
||||
await sendOrderedRcon(`/sc research_sync.sync_technologies("${syncJson}")`, true);
|
||||
}
|
||||
await syncTechnologies();
|
||||
|
||||
// A first start counts as turning following on, which seeds an empty cluster queue
|
||||
await syncQueue(true);
|
||||
});
|
||||
|
||||
// QueueEvents sent while the controller session was down are lost, so take the cluster queue again.
|
||||
// An edit made on this instance during the outage was never sent and is replaced: the cluster queue wins.
|
||||
// Events are lost both ways while the controller session is down. Contributions are kept above, finished
|
||||
// research comes from the technology sync, and the cluster queue replaces queue edits made meanwhile.
|
||||
instance.hooks.controllerConnectionEvent.attach(plugin.name, async (event) => {
|
||||
if (event !== "connect" || !syncStarted || !["starting", "running"].includes(instance.status)) {
|
||||
if (event !== "connect" && event !== "resume") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await syncQueue(false);
|
||||
sendPendingContributions();
|
||||
if (!syncStarted || !["starting", "running"].includes(instance.status)) {
|
||||
return;
|
||||
}
|
||||
if (event === "connect" || finishedWhileDisconnected) {
|
||||
finishedWhileDisconnected = false;
|
||||
try {
|
||||
await syncTechnologies();
|
||||
} catch (err) {
|
||||
finishedWhileDisconnected = true;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
if (event === "connect") {
|
||||
await syncQueue(false);
|
||||
}
|
||||
} catch (err: any) {
|
||||
unexpectedError(err);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user