Skip to content

Commit 9040bce

Browse files
author
DavidQ
committed
Plan multi-entity rewind and reconciliation support for Level 11
- Defines per-entity timeline buffers - Establishes entity-scoped reconciliation - Introduces selective rewind strategy - Expands debug visualization to multi-entity context Docs-only PR V2
1 parent cc50f8d commit 9040bce

5 files changed

Lines changed: 883 additions & 193 deletions

File tree

games/network_sample_c/debug/networkSampleCDebug.js

Lines changed: 101 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,23 @@ function asNumber(value, fallback = 0) {
2222
return Number.isFinite(numeric) ? numeric : fallback;
2323
}
2424

25+
function formatEntityLine(entity, options = {}) {
26+
const source = asObject(entity);
27+
const limit = Number.isFinite(Number(options.labelLimit))
28+
? Math.max(6, Math.floor(Number(options.labelLimit)))
29+
: 14;
30+
const label = sanitizeText(source.label) || sanitizeText(source.entityId) || "entity";
31+
const compactLabel = label.length > limit
32+
? `${label.slice(0, limit - 1)}.`
33+
: label;
34+
const marker = source.selected === true ? "*" : "-";
35+
const frameDelta = asNumber(source.frameDelta, 0);
36+
const status = sanitizeText(source.divergenceStatus || source.status) || "unknown";
37+
const severity = sanitizeText(source.severity) || "low";
38+
const alignment = sanitizeText(source.alignment) || "unavailable";
39+
return `${marker}${compactLabel} delta=${frameDelta} status=${status} severity=${severity} align=${alignment}`;
40+
}
41+
2542
function toNetworkSnapshot(snapshot) {
2643
return asObject(snapshot?.assets?.networkSampleC);
2744
}
@@ -35,32 +52,60 @@ function toDivergenceLines(snapshot) {
3552
const scenario = asObject(network.scenario);
3653
const divergence = asObject(network.divergence);
3754
const transport = asObject(network.network);
55+
const entities = asArray(divergence.entities);
56+
const selectedEntityId = sanitizeText(divergence.selectedEntityId);
57+
const selectedEntity = entities.find((entity) => sanitizeText(asObject(entity).entityId) === selectedEntityId);
3858

39-
return [
40-
`status=${sanitizeText(divergence.status) || "unknown"}`,
59+
const lines = [
60+
`selectedStatus=${sanitizeText(divergence.status) || "unknown"} overallStatus=${sanitizeText(divergence.overallStatus) || sanitizeText(divergence.status) || "unknown"}`,
4161
`severity=${sanitizeText(divergence.severity) || "low"} correction=${sanitizeText(divergence.correctionMode) || "hold-annotate"}`,
4262
`phase=${sanitizeText(scenario.phaseLabel) || "n/a"} (${sanitizeText(scenario.phaseId) || "n/a"})`,
43-
`frameDelta=${asNumber(divergence.frameDelta, 0)}`,
63+
`selectedEntity=${sanitizeText(divergence.selectedEntityLabel) || selectedEntityId || "n/a"} selectedFrameDelta=${asNumber(divergence.selectedFrameDelta ?? divergence.frameDelta, 0)}`,
64+
`overallFrameDelta=${asNumber(divergence.overallFrameDelta, asNumber(divergence.frameDelta, 0))}`,
4465
`authoritativeFrame=${asNumber(divergence.authoritativeFrame, 0)}`,
4566
`predictedFrame=${asNumber(divergence.predictedFrame, 0)}`,
4667
`alignment=${sanitizeText(divergence.alignment) || "unavailable"}`,
4768
`targetOffsetFrames=${asNumber(divergence.targetOffsetFrames, 0).toFixed(2)}`,
4869
`rttMs=${asNumber(transport.rttMs, 0)} jitterMs=${asNumber(transport.jitterMs, 0)} lossPct=${asNumber(transport.packetLossPct, 0)}`,
4970
`hint=${sanitizeText(divergence.reconciliationHint) || "n/a"}`
5071
];
72+
73+
if (selectedEntity) {
74+
lines.push(`selectedEntityDetail ${formatEntityLine(selectedEntity)}`);
75+
}
76+
if (entities.length > 0) {
77+
lines.push(`entityCount=${entities.length}`);
78+
entities.slice(0, 5).forEach((entity) => {
79+
lines.push(formatEntityLine(entity));
80+
});
81+
}
82+
return lines;
5183
}
5284

5385
function toTimelineLines(snapshot) {
5486
const network = toNetworkSnapshot(snapshot);
5587
const timeline = asObject(network.timeline);
5688
const history = asObject(timeline.history);
89+
const entityHistory = asArray(history.entities);
5790
const events = asArray(timeline.events);
5891
const lines = [
5992
`history=${asNumber(history.size, 0)}/${asNumber(history.limit, 0)}`,
6093
`oldestFrame=${history.oldestFrameId ?? "n/a"} latestFrame=${history.latestFrameId ?? "n/a"}`,
61-
`alignment=${sanitizeText(history.alignment) || "unavailable"} frameGap=${history.frameGap ?? "n/a"}`
94+
`alignment=${sanitizeText(history.alignment) || "unavailable"} frameGap=${history.frameGap ?? "n/a"}`,
95+
`entityHistoryCount=${asNumber(history.entityCount, entityHistory.length)}`
6296
];
6397

98+
if (entityHistory.length > 0) {
99+
entityHistory.slice(0, 5).forEach((entityStatus) => {
100+
const source = asObject(entityStatus);
101+
const marker = source.selected === true ? "*" : "-";
102+
lines.push(
103+
`${marker}${sanitizeText(source.entityId) || "entity"} history=${asNumber(source.historySize, 0)}/${asNumber(source.historyLimit, 0)} `
104+
+ `aligned=${source.alignedFrameId ?? "n/a"} gap=${source.frameGap ?? "n/a"} align=${sanitizeText(source.alignment) || "unavailable"}`
105+
);
106+
});
107+
}
108+
64109
if (events.length === 0) {
65110
lines.push("No timeline events recorded yet.");
66111
return lines;
@@ -97,19 +142,37 @@ function toValidationLines(snapshot) {
97142
function toRewindLines(snapshot) {
98143
const network = toNetworkSnapshot(snapshot);
99144
const rewind = asObject(network.rewindPreparation);
145+
const rewindEntities = asArray(rewind.entities);
146+
const rewindSelected = asArray(rewind.selectedEntityIds);
100147
const correction = asObject(network.correction);
101148
const replay = asObject(network.rewindReplay);
149+
const replaySelected = asArray(replay.selectedEntityIds);
102150

103-
return [
151+
const lines = [
104152
`status=${sanitizeText(rewind.status) || "n/a"}`,
105153
`canPrepare=${Boolean(rewind.canPrepare)}`,
106154
`anchorFrame=${rewind.rewindAnchorFrameId ?? "n/a"} targetFrame=${rewind.rewindTargetFrameId ?? "n/a"}`,
107155
`resimulateFrameCount=${asNumber(rewind.resimulateFrameCount, 0)}`,
108156
`alignment=${sanitizeText(rewind.alignment) || "unavailable"} frameGap=${rewind.frameGap ?? "n/a"}`,
157+
`selectedEntityIds=${rewindSelected.join(",") || "n/a"}`,
109158
`correctionMode=${sanitizeText(correction.mode) || "hold-annotate"} severity=${sanitizeText(correction.severity) || "low"}`,
110-
`lastReplayId=${replay.replayId ?? "none"} replayedFrames=${asNumber(replay.replayedFrameCount, 0)} postReplaySeverity=${sanitizeText(replay.postReplaySeverity) || "n/a"}`,
159+
`lastReplayId=${replay.replayId ?? "none"} replayedFrames=${asNumber(replay.replayedFrameCount, 0)} replayEntityIds=${replaySelected.join(",") || "n/a"}`,
160+
`postReplaySeverity=${sanitizeText(replay.postReplaySeverity) || "n/a"}`,
111161
sanitizeText(rewind.note) || "No rewind preparation note."
112162
];
163+
164+
if (rewindEntities.length > 0) {
165+
lines.push(`rewindEntityCount=${rewindEntities.length}`);
166+
rewindEntities.slice(0, 6).forEach((entityPrep) => {
167+
const source = asObject(entityPrep);
168+
lines.push(
169+
`${sanitizeText(source.entityId) || "entity"} status=${sanitizeText(source.status) || "n/a"} `
170+
+ `canPrepare=${Boolean(source.canPrepare)} anchor=${source.rewindAnchorFrameId ?? "n/a"} `
171+
+ `resim=${asNumber(source.resimulateFrameCount, 0)} severity=${sanitizeText(source.severity) || "low"}`
172+
);
173+
});
174+
}
175+
return lines;
113176
}
114177

115178
function toTraceLines(snapshot, maxLines = 10) {
@@ -139,20 +202,30 @@ function commandLinesForDivergence(context) {
139202
const scenario = asObject(snapshot.scenario);
140203
const divergence = asObject(snapshot.divergence);
141204
const network = asObject(snapshot.network);
205+
const entities = asArray(divergence.entities);
142206

143-
return [
144-
`status=${sanitizeText(divergence.status) || "unknown"}`,
207+
const lines = [
208+
`selectedStatus=${sanitizeText(divergence.status) || "unknown"}`,
209+
`overallStatus=${sanitizeText(divergence.overallStatus) || sanitizeText(divergence.status) || "unknown"}`,
145210
`severity=${sanitizeText(divergence.severity) || "low"}`,
146211
`correctionMode=${sanitizeText(divergence.correctionMode) || "hold-annotate"}`,
147212
`alignment=${sanitizeText(divergence.alignment) || "unavailable"}`,
213+
`selectedEntityId=${sanitizeText(divergence.selectedEntityId) || "n/a"}`,
148214
`phase=${sanitizeText(scenario.phaseId) || "unknown"}`,
149215
`phaseElapsed=${asNumber(scenario.phaseElapsedSeconds, 0).toFixed(2)}s/${asNumber(scenario.phaseDurationSeconds, 0).toFixed(2)}s`,
150216
`cycleCount=${asNumber(scenario.cycleCount, 0)}`,
151-
`frameDelta=${asNumber(divergence.frameDelta, 0)}`,
217+
`selectedFrameDelta=${asNumber(divergence.selectedFrameDelta ?? divergence.frameDelta, 0)}`,
218+
`overallFrameDelta=${asNumber(divergence.overallFrameDelta, asNumber(divergence.frameDelta, 0))}`,
152219
`authoritativeFrame=${asNumber(divergence.authoritativeFrame, 0)}`,
153220
`predictedFrame=${asNumber(divergence.predictedFrame, 0)}`,
154221
`rttMs=${asNumber(network.rttMs, 0)} jitterMs=${asNumber(network.jitterMs, 0)} lossPct=${asNumber(network.packetLossPct, 0)}`
155222
];
223+
224+
if (entities.length > 0) {
225+
lines.push(`entityCount=${entities.length}`);
226+
entities.slice(0, 6).forEach((entity) => lines.push(formatEntityLine(entity)));
227+
}
228+
return lines;
156229
}
157230

158231
function commandLinesForTrace(context, args = []) {
@@ -222,22 +295,39 @@ function commandLinesForReproduction(context) {
222295
function commandLinesForRewind(context) {
223296
const snapshot = getCommandSnapshot(context);
224297
const rewind = asObject(snapshot.rewindPreparation);
298+
const rewindEntities = asArray(rewind.entities);
299+
const rewindSelected = asArray(rewind.selectedEntityIds);
225300
const timeline = asObject(snapshot.timeline);
226301
const history = asObject(timeline.history);
227302
const correction = asObject(snapshot.correction);
228303
const replay = asObject(snapshot.rewindReplay);
304+
const replaySelected = asArray(replay.selectedEntityIds);
229305

230-
return [
306+
const lines = [
231307
`rewindStatus=${sanitizeText(rewind.status) || "n/a"}`,
232308
`canPrepare=${Boolean(rewind.canPrepare)}`,
233309
`rewindAnchorFrame=${rewind.rewindAnchorFrameId ?? "n/a"}`,
234310
`rewindTargetFrame=${rewind.rewindTargetFrameId ?? "n/a"}`,
235311
`resimulateFrameCount=${asNumber(rewind.resimulateFrameCount, 0)}`,
312+
`selectedEntityIds=${rewindSelected.join(",") || "n/a"}`,
236313
`history=${asNumber(history.size, 0)}/${asNumber(history.limit, 0)} alignment=${sanitizeText(history.alignment) || "unavailable"} frameGap=${history.frameGap ?? "n/a"}`,
237314
`correctionMode=${sanitizeText(correction.mode) || "hold-annotate"} severity=${sanitizeText(correction.severity) || "low"}`,
238-
`lastReplayId=${replay.replayId ?? "none"} replayedFrames=${asNumber(replay.replayedFrameCount, 0)} postReplaySeverity=${sanitizeText(replay.postReplaySeverity) || "n/a"}`,
315+
`lastReplayId=${replay.replayId ?? "none"} replayedFrames=${asNumber(replay.replayedFrameCount, 0)} replayEntityIds=${replaySelected.join(",") || "n/a"}`,
316+
`postReplaySeverity=${sanitizeText(replay.postReplaySeverity) || "n/a"}`,
239317
sanitizeText(rewind.note) || "Rewind preparation status unavailable."
240318
];
319+
320+
if (rewindEntities.length > 0) {
321+
lines.push(`rewindEntityCount=${rewindEntities.length}`);
322+
rewindEntities.slice(0, 8).forEach((entityPrep) => {
323+
const source = asObject(entityPrep);
324+
lines.push(
325+
`${sanitizeText(source.entityId) || "entity"} status=${sanitizeText(source.status) || "n/a"} `
326+
+ `canPrepare=${Boolean(source.canPrepare)} anchor=${source.rewindAnchorFrameId ?? "n/a"} resim=${asNumber(source.resimulateFrameCount, 0)}`
327+
);
328+
});
329+
}
330+
return lines;
241331
}
242332

243333
export function createNetworkSampleCDebugPlugin() {

0 commit comments

Comments
 (0)