Skip to content

Commit 04fb84d

Browse files
author
DavidQ
committed
implement authoritative server runtime and input ingestion — BUILD_PR_LEVEL_12_2_AUTHORITATIVE_SERVER_RUNTIME
1 parent c4cc294 commit 04fb84d

9 files changed

Lines changed: 516 additions & 55 deletions

docs/dev/CODEX_COMMANDS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
MODEL: GPT-5.3-codex
22
REASONING: high
33
COMMAND:
4-
Prepare for implementation of authoritative server runtime per PLAN_PR_LEVEL_12_2_AUTHORITATIVE_SERVER_RUNTIME. No engine API breakage.
4+
Implement BUILD_PR_LEVEL_12_2_AUTHORITATIVE_SERVER_RUNTIME exactly as defined. Ensure deterministic server loop and input ingestion.

docs/dev/COMMIT_COMMENT.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
define authoritative server runtime contracts — PLAN_PR_LEVEL_12_2_AUTHORITATIVE_SERVER_RUNTIME
1+
implement authoritative server runtime and input ingestion — BUILD_PR_LEVEL_12_2_AUTHORITATIVE_SERVER_RUNTIME
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
- Server loop defined
2-
- Input ingestion defined
3-
- Ownership boundaries defined
1+
- Server loop working
2+
- Input ingestion working
3+
- Ownership enforced
4+
- No regressions

docs/dev/roadmaps/MASTER_ROADMAP_HIGH_LEVEL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,3 +667,4 @@
667667
[ ] any doc that is a move/rename/etc. should be deleted (verify content is in the correct doc before deleting)
668668
[ ] consolidate PR for easier, one stop, review, no need to look as 6 different docs for one capability (a good example of this is bezel/background), all people care about is what it does.
669669
[ ] some games are actually samples/demo, identify and recomment a phase-xx to move to.
670+
[ ] simulated code (like some of the netword samples) should be coverted to use real networks) understanding, tests may need some moch
Lines changed: 9 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,14 @@
11
# BUILD_PR_LEVEL_12_2_AUTHORITATIVE_SERVER_RUNTIME
22

3-
## Purpose
4-
Implement the first authoritative server runtime slice on top of Level 12.1 transport/session contracts with no engine API breakage.
3+
## Build Scope
4+
Implement authoritative server runtime loop and input ingestion contracts.
55

6-
## Scope
7-
Primary target files:
8-
- `src/engine/network/AuthoritativeServerRuntime.js`
9-
- `src/engine/network/AuthoritativeInputIngestionContract.js`
10-
- `src/engine/network/index.js`
11-
- `tests/final/MultiplayerNetworkingStack.test.mjs`
12-
- `docs/pr/LEVEL_12_2_AUTHORITATIVE_SERVER_RUNTIME_CONTRACTS.md`
13-
14-
Allowed nearby reads:
15-
- `src/engine/network/TransportContract.js`
16-
- `src/engine/network/SessionLifecycleContract.js`
17-
- `src/engine/network/HandshakeSimulator.js`
18-
- `src/engine/network/HostServerBootstrap.js`
19-
- `docs/pr/LEVEL_12_1_REAL_NETWORK_FOUNDATION_CONTRACTS.md`
20-
21-
## Required implementation
22-
- Add an authoritative server runtime contract with an isolated tick loop that can run independently of gameplay code.
23-
- Add a client input ingestion contract that validates and normalizes input envelopes before queueing.
24-
- Enforce server-owned state boundaries so only server runtime code mutates authoritative state snapshots.
25-
- Keep handshake simulation compatible and testable with Level 12.1 foundations.
26-
- Export new runtime/contract symbols additively from `src/engine/network/index.js` only.
27-
- Extend `tests/final/MultiplayerNetworkingStack.test.mjs` with focused assertions for:
28-
- runtime loop start/step/stop behavior
29-
- input ingestion acceptance and rejection paths
30-
- server ownership boundary protection
31-
- existing handshake simulation still passing
32-
33-
## Acceptance criteria
34-
- Server runtime contract documented and implemented.
35-
- Input ingestion contract documented and implemented.
36-
- Authoritative ownership boundaries are enforced by runtime behavior and tests.
37-
- No existing engine network exports are removed or renamed.
38-
- Existing handshake simulation remains green.
6+
## Deliverables
7+
- Server loop execution model
8+
- Input ingestion pipeline
9+
- State ownership enforcement
3910

4011
## Validation
41-
Run only:
42-
- `node --check src/engine/network/AuthoritativeServerRuntime.js`
43-
- `node --check src/engine/network/AuthoritativeInputIngestionContract.js`
44-
- `node --input-type=module -e "import('./tests/final/MultiplayerNetworkingStack.test.mjs').then(async ({ run }) => { await run(); console.log('PASS MultiplayerNetworkingStack'); })"`
45-
- `node --input-type=module -e "import('./tests/production/EnginePublicBarrelImports.test.mjs').then(async ({ run }) => { await run(); console.log('PASS EnginePublicBarrelImports'); })"`
46-
47-
## Non-goals
48-
- no replication implementation
49-
- no gameplay coupling
50-
- no UI/debug expansion
51-
- no engine core API redesign
52-
- no repo-wide cleanup or unrelated refactor
53-
54-
## Working tree rule
55-
If the tree is already dirty, ignore unrelated files and modify only the scoped files for this PR purpose.
12+
- Server loop executes deterministically
13+
- Inputs processed correctly
14+
- No engine API breakage
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
04/15/2026
5+
AuthoritativeInputIngestionContract.js
6+
*/
7+
export const INPUT_INGESTION_REJECTION_CODES = Object.freeze({
8+
ENVELOPE_REQUIRED: 'ENVELOPE_REQUIRED',
9+
SESSION_ID_REQUIRED: 'SESSION_ID_REQUIRED',
10+
SESSION_MISMATCH: 'SESSION_MISMATCH',
11+
CLIENT_ID_REQUIRED: 'CLIENT_ID_REQUIRED',
12+
SEQUENCE_INVALID: 'SEQUENCE_INVALID',
13+
INPUT_TYPE_REQUIRED: 'INPUT_TYPE_REQUIRED',
14+
PAYLOAD_OBJECT_REQUIRED: 'PAYLOAD_OBJECT_REQUIRED',
15+
SENT_AT_INVALID: 'SENT_AT_INVALID',
16+
SERVER_OWNERSHIP_VIOLATION: 'SERVER_OWNERSHIP_VIOLATION',
17+
});
18+
19+
export const SERVER_OWNED_STATE_FIELDS = Object.freeze([
20+
'authoritativeTick',
21+
'runtimePhase',
22+
'connectedClients',
23+
'acceptedInputQueue',
24+
]);
25+
26+
function clone(value) {
27+
return JSON.parse(JSON.stringify(value));
28+
}
29+
30+
function isPlainObject(value) {
31+
return value !== null
32+
&& typeof value === 'object'
33+
&& Object.getPrototypeOf(value) === Object.prototype;
34+
}
35+
36+
function createReject(code, message) {
37+
return {
38+
ok: false,
39+
code,
40+
message,
41+
};
42+
}
43+
44+
export function validateClientInputEnvelope(envelope, { sessionId = null } = {}) {
45+
if (!isPlainObject(envelope)) {
46+
return createReject(
47+
INPUT_INGESTION_REJECTION_CODES.ENVELOPE_REQUIRED,
48+
'Input envelope must be a plain object.',
49+
);
50+
}
51+
52+
if (typeof envelope.sessionId !== 'string' || envelope.sessionId.length === 0) {
53+
return createReject(
54+
INPUT_INGESTION_REJECTION_CODES.SESSION_ID_REQUIRED,
55+
'sessionId must be a non-empty string.',
56+
);
57+
}
58+
59+
if (typeof sessionId === 'string' && sessionId.length > 0 && envelope.sessionId !== sessionId) {
60+
return createReject(
61+
INPUT_INGESTION_REJECTION_CODES.SESSION_MISMATCH,
62+
'Input envelope sessionId does not match runtime session.',
63+
);
64+
}
65+
66+
if (typeof envelope.clientId !== 'string' || envelope.clientId.length === 0) {
67+
return createReject(
68+
INPUT_INGESTION_REJECTION_CODES.CLIENT_ID_REQUIRED,
69+
'clientId must be a non-empty string.',
70+
);
71+
}
72+
73+
if (!Number.isInteger(envelope.sequence) || envelope.sequence < 0) {
74+
return createReject(
75+
INPUT_INGESTION_REJECTION_CODES.SEQUENCE_INVALID,
76+
'sequence must be a non-negative integer.',
77+
);
78+
}
79+
80+
if (typeof envelope.inputType !== 'string' || envelope.inputType.length === 0) {
81+
return createReject(
82+
INPUT_INGESTION_REJECTION_CODES.INPUT_TYPE_REQUIRED,
83+
'inputType must be a non-empty string.',
84+
);
85+
}
86+
87+
if (!isPlainObject(envelope.payload)) {
88+
return createReject(
89+
INPUT_INGESTION_REJECTION_CODES.PAYLOAD_OBJECT_REQUIRED,
90+
'payload must be a plain object.',
91+
);
92+
}
93+
94+
if (!Number.isFinite(envelope.sentAtMs)) {
95+
return createReject(
96+
INPUT_INGESTION_REJECTION_CODES.SENT_AT_INVALID,
97+
'sentAtMs must be a finite number.',
98+
);
99+
}
100+
101+
const violatingKey = SERVER_OWNED_STATE_FIELDS.find(
102+
(serverOwnedField) => Object.prototype.hasOwnProperty.call(envelope.payload, serverOwnedField),
103+
);
104+
if (violatingKey) {
105+
return createReject(
106+
INPUT_INGESTION_REJECTION_CODES.SERVER_OWNERSHIP_VIOLATION,
107+
`payload cannot include server-owned field: ${violatingKey}`,
108+
);
109+
}
110+
111+
return { ok: true };
112+
}
113+
114+
export function normalizeClientInputEnvelope(
115+
envelope,
116+
{ acceptedAtTick = 0, acceptedAtMs = 0 } = {},
117+
) {
118+
return {
119+
sessionId: envelope.sessionId,
120+
clientId: envelope.clientId,
121+
sequence: envelope.sequence,
122+
inputType: envelope.inputType,
123+
payload: clone(envelope.payload),
124+
sentAtMs: Number(envelope.sentAtMs),
125+
acceptedAtTick,
126+
acceptedAtMs,
127+
};
128+
}
129+
130+
export default class AuthoritativeInputIngestionContract {
131+
constructor({ sessionId = 'session' } = {}) {
132+
this.sessionId = sessionId;
133+
this.stats = {
134+
accepted: 0,
135+
rejected: 0,
136+
};
137+
}
138+
139+
validate(envelope, { sessionId = this.sessionId } = {}) {
140+
const result = validateClientInputEnvelope(envelope, { sessionId });
141+
if (result.ok) {
142+
this.stats.accepted += 1;
143+
} else {
144+
this.stats.rejected += 1;
145+
}
146+
return result;
147+
}
148+
149+
normalize(envelope, metadata = {}) {
150+
return normalizeClientInputEnvelope(envelope, metadata);
151+
}
152+
153+
getStats() {
154+
return {
155+
...this.stats,
156+
sessionId: this.sessionId,
157+
};
158+
}
159+
}
160+

0 commit comments

Comments
 (0)