diff --git a/packages/engine/src/networked-server.ts b/packages/engine/src/networked-server.ts index bea577c..12aae26 100644 --- a/packages/engine/src/networked-server.ts +++ b/packages/engine/src/networked-server.ts @@ -91,7 +91,7 @@ export class NetworkedAuthoritativeEngine< ) { this.game = game; this.historyTicks = options.historyTicks ?? game.tickRateHz * 2; - this.maxPastTicks = options.maxPastTicks ?? 2; + this.maxPastTicks = options.maxPastTicks ?? game.tickRateHz; this.maxFutureTicks = options.maxFutureTicks ?? game.tickRateHz; this.snapshotEveryTicks = game.tickRateHz / game.snapshotRateHz; this.state = game.server.createInitialState(); diff --git a/packages/engine/src/server.ts b/packages/engine/src/server.ts index a21d87d..e17872f 100644 --- a/packages/engine/src/server.ts +++ b/packages/engine/src/server.ts @@ -11,6 +11,10 @@ import type { export interface ServerEngineOptions { historyTicks?: number; + /** + * Maximum transport delay accepted for an input. Late inputs are applied on + * the next simulation step; they never rewrite authoritative history. + */ maxPastTicks?: number; maxFutureTicks?: number; } @@ -63,7 +67,7 @@ export class AuthoritativeEngine { ) { this.game = game; this.historyTicks = options.historyTicks ?? game.tickRateHz * 2; - this.maxPastTicks = options.maxPastTicks ?? 2; + this.maxPastTicks = options.maxPastTicks ?? game.tickRateHz; this.maxFutureTicks = options.maxFutureTicks ?? game.tickRateHz; this.snapshotEveryTicks = game.tickRateHz / game.snapshotRateHz; this.state = game.createInitialState(); diff --git a/packages/engine/test/engine.test.mjs b/packages/engine/test/engine.test.mjs index 2532658..abf4c2e 100644 --- a/packages/engine/test/engine.test.mjs +++ b/packages/engine/test/engine.test.mjs @@ -151,6 +151,34 @@ test("the developer-facing API compiles named sections into a networked game", ( ); server.step(); assert.equal(server.currentState.value, 3); + + // Internet clients can be several ticks behind the server when their first + // input arrives. Accept it within the default one-second transport window + // and apply it on the next authoritative step. + for (let index = 0; index < 6; index++) server.step(); + assert.deepEqual( + server.submitInput(1, { + sequence: 2, + targetTick: 2, + observedTick: 2, + input: { amount: 2 }, + }), + { accepted: true }, + ); + server.step(); + assert.equal(server.currentState.value, 5); + + for (let index = 0; index < 7; index++) server.step(); + assert.deepEqual( + server.submitInput(1, { + sequence: 3, + targetTick: 2, + observedTick: 2, + input: { amount: 2 }, + }), + { accepted: false, reason: "past" }, + ); + assert.deepEqual(codec.decode(codec.encode({ unicode: "flux ⚡" })), { unicode: "flux ⚡", });