Skip to content

Commit afa6d05

Browse files
author
DavidQ
committed
Add event-driven overlay updates.
PR Details: - Improves responsiveness and efficiency
1 parent a28bb8a commit afa6d05

7 files changed

Lines changed: 152 additions & 58 deletions

File tree

docs/dev/CODEX_COMMANDS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ MODEL: GPT-5.4
22
REASONING: medium
33

44
COMMAND:
5-
Implement overlay state synchronization:
6-
- Sync with gameplay state
7-
- Prevent desync
5+
Implement event-driven overlay updates:
6+
- Use events instead of polling
7+
- Improve responsiveness
88
- Update roadmap status only

docs/dev/COMMIT_COMMENT.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Add overlay state synchronization.
1+
Add event-driven overlay updates.
22

33
PR Details:
4-
- Ensures consistent overlay behavior
4+
- Improves responsiveness and efficiency
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
[ ] State sync works
2-
[ ] No desync
3-
[ ] Stable behavior
1+
[ ] Events trigger updates
2+
[ ] No polling overhead
3+
[ ] Performance improved
44
[ ] Roadmap updated

docs/dev/roadmaps/MASTER_ROADMAP_HIGH_LEVEL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -827,4 +827,4 @@
827827
- [ ] full-repo validation sweep
828828
- [x] zero regression requirement
829829
- [x] contract freeze readiness
830-
- [.] readiness for long-term maintenance mode
830+
- [x] readiness for long-term maintenance mode

docs/pr/BUILD_PR.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
1-
# BUILD_PR_LEVEL_21_3_OVERLAY_STATE_SYNCHRONIZATION
1+
# BUILD_PR_LEVEL_21_4_OVERLAY_EVENT_DRIVEN_UPDATES
22

33
## Purpose
4-
Ensure overlay state is synchronized with gameplay systems.
4+
Move overlay updates to an event-driven model.
55

66
## Roadmap Improvement
7-
Advances Level 21 with consistent state behavior across overlays.
7+
Improves efficiency and responsiveness of overlays.
88

99
## Scope
10-
- Sync overlay state with gameplay state
11-
- Prevent desync issues
12-
- Validate across multiple overlays
10+
- Trigger overlay updates via events
11+
- Reduce unnecessary polling
12+
- Validate responsiveness
1313

1414
## Test Steps
15-
1. Change gameplay state rapidly
16-
2. Verify overlays stay in sync
17-
3. Confirm no desync artifacts
15+
1. Trigger gameplay events
16+
2. Verify overlay updates instantly
17+
3. Confirm no extra processing
1818

1919
## Expected
20-
- Consistent overlay state
21-
- No desync
20+
- Event-driven updates working
21+
- Improved performance

samples/phase-17/shared/overlayGameplayRuntime.js

Lines changed: 110 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,93 @@ function resolveOverlayRuntimeSyncStateContainer(context = {}, gameplayState = n
254254
return null;
255255
}
256256

257+
function resolveOverlayRuntimeEventQueue(context = {}, gameplayState = null, syncState = null) {
258+
if (Array.isArray(context?.overlayRuntimeEvents)) {
259+
return context.overlayRuntimeEvents;
260+
}
261+
if (Array.isArray(gameplayState?.overlayRuntimeEvents)) {
262+
return gameplayState.overlayRuntimeEvents;
263+
}
264+
if (Array.isArray(syncState?.events)) {
265+
return syncState.events;
266+
}
267+
return null;
268+
}
269+
270+
export function enqueueOverlayGameplayRuntimeSyncEvent(target, event = {}) {
271+
if (!target || typeof target !== 'object') {
272+
return false;
273+
}
274+
if (!Array.isArray(target.overlayRuntimeEvents)) {
275+
target.overlayRuntimeEvents = [];
276+
}
277+
if (!Array.isArray(target.overlayRuntimeEvents)) {
278+
return false;
279+
}
280+
target.overlayRuntimeEvents.push({
281+
type: String(event?.type || 'overlay-runtime-sync').trim() || 'overlay-runtime-sync',
282+
...(event || {}),
283+
});
284+
return true;
285+
}
286+
287+
function normalizeOverlayRuntimeSyncEvent(event) {
288+
if (!event || typeof event !== 'object') {
289+
return null;
290+
}
291+
const normalizedType = String(event.type || 'overlay-runtime-sync').trim() || 'overlay-runtime-sync';
292+
const hasVisible = event.visible === true || event.visible === false;
293+
const hasInteractionIndex = Number.isFinite(Number(event.interactionIndex));
294+
const hasActiveOverlayId = String(event.activeOverlayId || '').trim().length > 0;
295+
if (!hasVisible && !hasInteractionIndex && !hasActiveOverlayId && normalizedType === 'overlay-runtime-sync') {
296+
return null;
297+
}
298+
return {
299+
type: normalizedType,
300+
visible: hasVisible ? event.visible : undefined,
301+
interactionIndex: hasInteractionIndex ? Number(event.interactionIndex) : undefined,
302+
activeOverlayId: hasActiveOverlayId ? String(event.activeOverlayId || '').trim() : '',
303+
};
304+
}
305+
306+
function applyOverlayRuntimeSyncPatch(runtime, runtimeExtensions, patch = {}) {
307+
const count = runtimeExtensions.length;
308+
let desyncCorrected = false;
309+
if (patch.visible === true || patch.visible === false) {
310+
runtime.interactionVisible = patch.visible;
311+
}
312+
313+
if (Number.isFinite(patch.interactionIndex) && count > 0) {
314+
const incomingIndexRaw = Number(patch.interactionIndex);
315+
const incomingIndex = Math.trunc(incomingIndexRaw);
316+
if (incomingIndex < 0 || incomingIndex >= count || incomingIndex !== incomingIndexRaw) {
317+
desyncCorrected = true;
318+
}
319+
runtime.interactionIndex = ((incomingIndex % count) + count) % count;
320+
}
321+
322+
const requestedOverlayId = String(patch.activeOverlayId || '').trim();
323+
const resolvedOverlayIndex = findRuntimeExtensionIndexByOverlayId(runtime, requestedOverlayId);
324+
if (requestedOverlayId) {
325+
if (resolvedOverlayIndex >= 0) {
326+
if (runtime.interactionIndex !== resolvedOverlayIndex) {
327+
runtime.interactionIndex = resolvedOverlayIndex;
328+
}
329+
} else {
330+
desyncCorrected = true;
331+
}
332+
}
333+
334+
if (Number.isFinite(patch.interactionIndex) && requestedOverlayId && resolvedOverlayIndex >= 0 && count > 0) {
335+
const incomingIndex = ((Math.trunc(Number(patch.interactionIndex)) % count) + count) % count;
336+
if (incomingIndex !== resolvedOverlayIndex) {
337+
desyncCorrected = true;
338+
}
339+
}
340+
341+
return desyncCorrected;
342+
}
343+
257344
function writeOverlayRuntimeSyncSnapshot(container, snapshot) {
258345
if (!container || typeof container !== 'object' || !snapshot || typeof snapshot !== 'object') {
259346
return false;
@@ -265,6 +352,8 @@ function writeOverlayRuntimeSyncSnapshot(container, snapshot) {
265352
container.count = snapshot.count;
266353
container.cycleKey = snapshot.cycleKey;
267354
container.desyncCorrected = snapshot.desyncCorrected;
355+
container.eventsProcessed = snapshot.eventsProcessed;
356+
container.syncMode = snapshot.syncMode;
268357
return true;
269358
} catch {
270359
return false;
@@ -278,43 +367,32 @@ export function synchronizeOverlayGameplayRuntimeState(runtime, context = {}) {
278367

279368
const gameplayState = resolveOverlayGameplayState(context);
280369
const syncState = resolveOverlayRuntimeSyncStateContainer(context, gameplayState);
370+
const eventQueue = resolveOverlayRuntimeEventQueue(context, gameplayState, syncState);
281371
const runtimeExtensions = Array.isArray(runtime.runtimeExtensions) ? runtime.runtimeExtensions : [];
282372
const count = runtimeExtensions.length;
283373
let desyncCorrected = false;
284-
285-
if (syncState) {
286-
if (syncState.visible === true || syncState.visible === false) {
287-
runtime.interactionVisible = syncState.visible;
288-
}
289-
290-
const incomingIndexRaw = Number(syncState.interactionIndex);
291-
const hasIncomingIndex = Number.isFinite(incomingIndexRaw);
292-
if (hasIncomingIndex && count > 0) {
293-
const incomingIndex = Math.trunc(incomingIndexRaw);
294-
if (incomingIndex < 0 || incomingIndex >= count || incomingIndex !== incomingIndexRaw) {
295-
desyncCorrected = true;
374+
let eventsProcessed = 0;
375+
376+
if (Array.isArray(eventQueue) && eventQueue.length > 0) {
377+
const pending = eventQueue.splice(0, eventQueue.length);
378+
for (let i = 0; i < pending.length; i += 1) {
379+
const event = normalizeOverlayRuntimeSyncEvent(pending[i]);
380+
if (!event) {
381+
continue;
296382
}
297-
runtime.interactionIndex = ((incomingIndex % count) + count) % count;
298-
}
299-
300-
const requestedOverlayId = String(syncState.activeOverlayId || '').trim();
301-
const resolvedOverlayIndex = findRuntimeExtensionIndexByOverlayId(runtime, requestedOverlayId);
302-
if (requestedOverlayId) {
303-
if (resolvedOverlayIndex >= 0) {
304-
if (runtime.interactionIndex !== resolvedOverlayIndex) {
305-
runtime.interactionIndex = resolvedOverlayIndex;
306-
}
307-
} else {
308-
desyncCorrected = true;
383+
if (event.type !== 'overlay-runtime-sync' && event.type !== 'overlay-state-sync') {
384+
continue;
309385
}
386+
const corrected = applyOverlayRuntimeSyncPatch(runtime, runtimeExtensions, event);
387+
desyncCorrected = desyncCorrected || corrected;
388+
eventsProcessed += 1;
310389
}
390+
}
311391

312-
if (hasIncomingIndex && requestedOverlayId && resolvedOverlayIndex >= 0 && count > 0) {
313-
const incomingIndex = ((Math.trunc(incomingIndexRaw) % count) + count) % count;
314-
if (incomingIndex !== resolvedOverlayIndex) {
315-
desyncCorrected = true;
316-
}
317-
}
392+
// Compatibility fallback for pre-event producers.
393+
if (eventsProcessed === 0 && syncState) {
394+
const corrected = applyOverlayRuntimeSyncPatch(runtime, runtimeExtensions, syncState);
395+
desyncCorrected = desyncCorrected || corrected;
318396
}
319397

320398
const interactionIndex = normalizeInteractionIndex(runtime);
@@ -326,6 +404,8 @@ export function synchronizeOverlayGameplayRuntimeState(runtime, context = {}) {
326404
count,
327405
cycleKey: String(runtime.interactionCycleKey || LEVEL17_OVERLAY_CYCLE_KEY),
328406
desyncCorrected,
407+
eventsProcessed,
408+
syncMode: eventsProcessed > 0 ? 'events' : 'compat',
329409
};
330410
writeOverlayRuntimeSyncSnapshot(syncState, snapshot);
331411
return snapshot;

tests/runtime/Phase19OverlayExpansionFramework.test.mjs

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Phase19OverlayExpansionFramework.test.mjs
77
import assert from 'node:assert/strict';
88
import { LEVEL17_OVERLAY_CYCLE_KEY } from '../../samples/phase-17/shared/overlayCycleInput.js';
99
import {
10+
enqueueOverlayGameplayRuntimeSyncEvent,
1011
getOverlayGameplayRuntimeCompositionSnapshot,
1112
getOverlayGameplayRuntimeInteractionSnapshot,
1213
renderOverlayGameplayRuntime,
@@ -257,18 +258,25 @@ function assertOverlayStateSynchronizationAndDesyncRecovery() {
257258

258259
const runtime = framework.createRuntimeForExtension('phase19-overlay-sync');
259260
const gameplayState = {
260-
overlayRuntimeState: {
261-
visible: true,
262-
interactionIndex: 99,
263-
activeOverlayId: 'runtime-b',
264-
},
261+
overlayRuntimeState: {},
265262
};
263+
enqueueOverlayGameplayRuntimeSyncEvent(gameplayState, {
264+
visible: true,
265+
interactionIndex: 99,
266+
activeOverlayId: 'runtime-b',
267+
});
266268

267269
const initialInteraction = getOverlayGameplayRuntimeInteractionSnapshot(runtime, { gameplayState });
268270
assert.equal(initialInteraction.index, 1, 'Sync should correct out-of-range index to the overlay requested by gameplay state.');
269271
assert.equal(initialInteraction.activeOverlayId, 'runtime-b', 'Sync should align active overlay id with gameplay state request.');
270272
assert.equal(gameplayState.overlayRuntimeState.desyncCorrected, true, 'Sync should flag corrected desync state.');
271273
assert.equal(gameplayState.overlayRuntimeState.count, 2, 'Sync snapshot should expose runtime extension count.');
274+
assert.equal(gameplayState.overlayRuntimeState.syncMode, 'events', 'Sync should process queued gameplay events in event-driven mode.');
275+
assert.equal(gameplayState.overlayRuntimeState.eventsProcessed, 1, 'Sync should process exactly one overlay event.');
276+
277+
const idleInteraction = getOverlayGameplayRuntimeInteractionSnapshot(runtime, { gameplayState });
278+
assert.equal(idleInteraction.activeOverlayId, 'runtime-b', 'No-event sync should keep prior synchronized state stable.');
279+
assert.equal(gameplayState.overlayRuntimeState.eventsProcessed, 0, 'No queued events should avoid extra event processing.');
272280

273281
const renderRuntimeB = renderOverlayGameplayRuntime(runtime, {
274282
activeOverlayId: 'runtime-b',
@@ -277,16 +285,22 @@ function assertOverlayStateSynchronizationAndDesyncRecovery() {
277285
});
278286
assert.equal(renderRuntimeB, 1, 'Synced runtime should render requested gameplay overlay.');
279287

280-
gameplayState.overlayRuntimeState.visible = false;
281-
gameplayState.overlayRuntimeState.activeOverlayId = 'runtime-a';
288+
enqueueOverlayGameplayRuntimeSyncEvent(gameplayState, {
289+
visible: false,
290+
activeOverlayId: 'runtime-a',
291+
});
282292
const renderHidden = renderOverlayGameplayRuntime(runtime, {
283293
activeOverlayId: 'runtime-a',
284294
renderer: createRendererProbe(),
285295
gameplayState,
286296
});
287297
assert.equal(renderHidden, 0, 'Gameplay visibility sync should prevent overlay render while hidden.');
298+
assert.equal(gameplayState.overlayRuntimeState.syncMode, 'events', 'Render sync should remain event-driven after hide event.');
288299

289-
gameplayState.overlayRuntimeState.visible = true;
300+
enqueueOverlayGameplayRuntimeSyncEvent(gameplayState, {
301+
visible: true,
302+
activeOverlayId: 'runtime-a',
303+
});
290304
const renderRuntimeA = renderOverlayGameplayRuntime(runtime, {
291305
activeOverlayId: 'runtime-a',
292306
renderer: createRendererProbe(),

0 commit comments

Comments
 (0)