Keep auto paused instances paused

Every RCON command makes an auto paused Factorio server run one tick, so
subspace_storage's 1 s inventory update and 5 s ping, plus a
statistics_exporter gather on every Prometheus scrape, kept a server with
nobody online running at about 1.3 ticks per second.

The instance now records the auto_pause server setting it was started
with and exposes autoPaused, true while that setting is on and no players
are online, along with an autoPauseChanged hook. subspace_storage skips
its timers while paused and sends a ping and inventory as soon as a player
joins. statistics_exporter gathers once after the game pauses and keeps
serving those values until it resumes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bbassieandClaude Opus 5.5 committed 2026-10-28 23:40:30 +00:00
1 parent ebdbb1fe56
commit 11e067a933
11 files changed
+227 -19

No files matched your search

+36
View File
@@ -418,6 +418,42 @@ describe("class Instance", function() {
});
});
describe(".autoPaused", function() {
let changes;
beforeEach(function() {
changes = [];
instance.hooks.autoPauseChanged.attach("test", async paused => { changes.push(paused); });
});
it("should be false when auto_pause is off", function() {
assert.equal(instance.autoPaused, false);
instance._recordPlayerJoin("player");
instance._recordPlayerLeave("player", "quit");
assert.equal(instance.autoPaused, false);
assert.deepEqual(changes, []);
});
it("should follow players online when auto_pause is on", function() {
instance._autoPause = true;
assert.equal(instance.autoPaused, true);
instance._recordPlayerJoin("a");
instance._recordPlayerJoin("b");
assert.equal(instance.autoPaused, false);
instance._recordPlayerLeave("a", "quit");
assert.equal(instance.autoPaused, false);
instance._recordPlayerLeave("b", "quit");
assert.equal(instance.autoPaused, true);
assert.deepEqual(changes, [false, true]);
});
it("should not invoke the hook on exit", function() {
instance._autoPause = true;
instance._recordPlayerJoin("player");
instance._saveStats = async () => {};
instance.notifyExit();
assert.equal(instance.autoPaused, false);
assert.deepEqual(changes, [false]);
});
});
describe("._checkOnlinePlayers()", function() {
it("should do nothing on empty server", async function() {
await instance._checkOnlinePlayers();
+49
View File
@@ -1332,6 +1332,55 @@ describe("Integration of Clusterio", function() {
});
});
describe("auto pause", function() {
requiresFactorio(this);
async function getTick() {
return Number(await getControl().sendTo(
{ instanceId: 44 }, new lib.InstanceSendRconRequest("/sc rcon.print(game.tick)")
));
}
async function sendPlayerEvent(type) {
const json = JSON.stringify({ type, name: "auto_pause_player", reason: "quit" });
await getControl().sendTo(
{ instanceId: 44 },
new lib.InstanceSendRconRequest(`/sc print("\\f$ipc:player_event?j${lib.escapeString(json)}")`),
);
}
let started = false;
before(async function() {
slowTest(this);
this.timeout(60000);
started = true;
await execCtl("instance stop test");
await execCtl("instance config set-prop test factorio.settings auto_pause true");
await execCtl("instance start test");
});
after(async function() {
if (!started) {
return;
}
this.timeout(60000);
await execCtl("instance stop test");
await execCtl("instance config set-prop test factorio.settings auto_pause false");
await execCtl("instance start test");
});
it("does not tick the game while no players are online", async function() {
slowTest(this);
const start = await getTick();
await wait(6000);
assert.equal(await getTick() - start, 1);
});
it("resumes plugin work when a player joins", async function() {
slowTest(this);
const start = await getTick();
await sendPlayerEvent("join");
await wait(6000);
const ticks = await getTick() - start;
await sendPlayerEvent("leave");
assert(ticks > 3, `game ran ${ticks} ticks`);
});
});
describe("instance stop", function() {
it("stops the instance", async function() {
slowTest(this);
+1
View File
@@ -129,6 +129,7 @@ export class MockInstance extends lib.Link {
this.name = "test";
this.id = 7357;
this.status = "running";
this.autoPaused = false;
this.hooks = new InstanceHooks(this.logger);
this.loadedPlugins = new Set();
this.mockConfigEntries = new Map([