Skip to content

Commit a28bb8a

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

7 files changed

Lines changed: 197 additions & 22 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 context-aware overlays:
6-
- Detect gameplay state
7-
- Adapt overlay behavior
5+
Implement overlay state synchronization:
6+
- Sync with gameplay state
7+
- Prevent desync
88
- Update roadmap status only

docs/dev/COMMIT_COMMENT.txt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
Add context-aware overlays.
1+
Add overlay state synchronization.
22

33
PR Details:
4-
- Enables dynamic overlay behavior
5-
4+
- Ensures consistent overlay behavior
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
[ ] Context detection works
2-
[ ] Overlay adapts
3-
[ ] No regression
1+
[ ] State sync works
2+
[ ] No desync
3+
[ ] Stable behavior
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
@@ -826,5 +826,5 @@
826826
### Track H — Final Stability Gate
827827
- [ ] full-repo validation sweep
828828
- [x] zero regression requirement
829-
- [.] contract freeze readiness
829+
- [x] contract freeze readiness
830830
- [.] 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_2_CONTEXT_AWARE_OVERLAYS
1+
# BUILD_PR_LEVEL_21_3_OVERLAY_STATE_SYNCHRONIZATION
22

33
## Purpose
4-
Add context-aware overlays that respond to gameplay state.
4+
Ensure overlay state is synchronized with gameplay systems.
55

66
## Roadmap Improvement
7-
Advances Level 21 with dynamic overlay behavior.
7+
Advances Level 21 with consistent state behavior across overlays.
88

99
## Scope
10-
- Detect gameplay context
11-
- Show/hide or modify overlays dynamically
12-
- Validate behavior
10+
- Sync overlay state with gameplay state
11+
- Prevent desync issues
12+
- Validate across multiple overlays
1313

1414
## Test Steps
15-
1. Change gameplay state
16-
2. Verify overlay adapts
17-
3. Confirm stability
15+
1. Change gameplay state rapidly
16+
2. Verify overlays stay in sync
17+
3. Confirm no desync artifacts
1818

1919
## Expected
20-
- Context-driven overlays
21-
- No regression
20+
- Consistent overlay state
21+
- No desync

samples/phase-17/shared/overlayGameplayRuntime.js

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,22 @@ function normalizeInteractionIndex(runtime) {
9191
return normalized;
9292
}
9393

94+
function findRuntimeExtensionIndexByOverlayId(runtime, overlayId) {
95+
if (!runtime || !Array.isArray(runtime.runtimeExtensions)) {
96+
return -1;
97+
}
98+
const requestedOverlayId = String(overlayId || '').trim();
99+
if (!requestedOverlayId) {
100+
return -1;
101+
}
102+
for (let i = 0; i < runtime.runtimeExtensions.length; i += 1) {
103+
if (String(runtime.runtimeExtensions[i]?.overlayId || '').trim() === requestedOverlayId) {
104+
return i;
105+
}
106+
}
107+
return -1;
108+
}
109+
94110
function normalizeSafeZoneEntry(entry) {
95111
if (!entry || typeof entry !== 'object') {
96112
return null;
@@ -218,6 +234,103 @@ function resolveRuntimeExtensionContextBehavior(extension, context = {}) {
218234
}
219235
}
220236

237+
function resolveOverlayRuntimeSyncStateContainer(context = {}, gameplayState = null) {
238+
if (context?.overlayRuntimeState && typeof context.overlayRuntimeState === 'object') {
239+
return context.overlayRuntimeState;
240+
}
241+
if (gameplayState?.overlayRuntimeState && typeof gameplayState.overlayRuntimeState === 'object') {
242+
return gameplayState.overlayRuntimeState;
243+
}
244+
if (gameplayState && typeof gameplayState === 'object') {
245+
try {
246+
gameplayState.overlayRuntimeState = {};
247+
if (gameplayState.overlayRuntimeState && typeof gameplayState.overlayRuntimeState === 'object') {
248+
return gameplayState.overlayRuntimeState;
249+
}
250+
} catch {
251+
// Sync state creation is best effort only.
252+
}
253+
}
254+
return null;
255+
}
256+
257+
function writeOverlayRuntimeSyncSnapshot(container, snapshot) {
258+
if (!container || typeof container !== 'object' || !snapshot || typeof snapshot !== 'object') {
259+
return false;
260+
}
261+
try {
262+
container.visible = snapshot.visible;
263+
container.interactionIndex = snapshot.interactionIndex;
264+
container.activeOverlayId = snapshot.activeOverlayId;
265+
container.count = snapshot.count;
266+
container.cycleKey = snapshot.cycleKey;
267+
container.desyncCorrected = snapshot.desyncCorrected;
268+
return true;
269+
} catch {
270+
return false;
271+
}
272+
}
273+
274+
export function synchronizeOverlayGameplayRuntimeState(runtime, context = {}) {
275+
if (!runtime) {
276+
return null;
277+
}
278+
279+
const gameplayState = resolveOverlayGameplayState(context);
280+
const syncState = resolveOverlayRuntimeSyncStateContainer(context, gameplayState);
281+
const runtimeExtensions = Array.isArray(runtime.runtimeExtensions) ? runtime.runtimeExtensions : [];
282+
const count = runtimeExtensions.length;
283+
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;
296+
}
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;
309+
}
310+
}
311+
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+
}
318+
}
319+
320+
const interactionIndex = normalizeInteractionIndex(runtime);
321+
const active = runtimeExtensions[interactionIndex] || null;
322+
const snapshot = {
323+
visible: runtime.interactionVisible !== false,
324+
interactionIndex,
325+
activeOverlayId: active?.overlayId || '',
326+
count,
327+
cycleKey: String(runtime.interactionCycleKey || LEVEL17_OVERLAY_CYCLE_KEY),
328+
desyncCorrected,
329+
};
330+
writeOverlayRuntimeSyncSnapshot(syncState, snapshot);
331+
return snapshot;
332+
}
333+
221334
function getComposedRuntimeFrames(runtime, activeOverlayId, context = {}) {
222335
if (!runtime || !Array.isArray(runtime.runtimeExtensions) || runtime.runtimeExtensions.length === 0) {
223336
return [];
@@ -570,7 +683,8 @@ export function setOverlayGameplayRuntimeVisible(runtime, visible) {
570683
return true;
571684
}
572685

573-
export function getOverlayGameplayRuntimeInteractionSnapshot(runtime) {
686+
export function getOverlayGameplayRuntimeInteractionSnapshot(runtime, context = {}) {
687+
synchronizeOverlayGameplayRuntimeState(runtime, context);
574688
const extensions = Array.isArray(runtime?.runtimeExtensions) ? runtime.runtimeExtensions : [];
575689
const index = normalizeInteractionIndex(runtime);
576690
const active = extensions[index] || null;
@@ -586,6 +700,7 @@ export function getOverlayGameplayRuntimeInteractionSnapshot(runtime) {
586700
}
587701

588702
export function getOverlayGameplayRuntimeCompositionSnapshot(runtime, context = {}) {
703+
synchronizeOverlayGameplayRuntimeState(runtime, context);
589704
const activeOverlayId = String(context?.activeOverlayId || '').trim();
590705
const safeZones = resolveLayoutSafeZones(context);
591706
const frames = deriveRenderHierarchy(attachCompositionSlots(
@@ -618,6 +733,7 @@ export function stepOverlayGameplayRuntimeControls(runtime, input, options = {})
618733
if (!runtime) {
619734
return false;
620735
}
736+
synchronizeOverlayGameplayRuntimeState(runtime, options);
621737

622738
const dtSeconds = Math.max(0, Math.min(0.25, Number(options?.dtSeconds) || 0));
623739
if (runtime.interactionCooldownRemainingSeconds > 0 && dtSeconds > 0) {
@@ -694,6 +810,7 @@ export function stepOverlayGameplayRuntimeControls(runtime, input, options = {})
694810
}
695811

696812
export function stepOverlayGameplayRuntime(runtime, context = {}) {
813+
synchronizeOverlayGameplayRuntimeState(runtime, context);
697814
if (
698815
!runtime ||
699816
runtime.interactionVisible === false ||
@@ -737,6 +854,7 @@ export function stepOverlayGameplayRuntime(runtime, context = {}) {
737854
}
738855

739856
export function renderOverlayGameplayRuntime(runtime, context = {}) {
857+
synchronizeOverlayGameplayRuntimeState(runtime, context);
740858
if (
741859
!runtime ||
742860
runtime.interactionVisible === false ||

tests/runtime/Phase19OverlayExpansionFramework.test.mjs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import assert from 'node:assert/strict';
88
import { LEVEL17_OVERLAY_CYCLE_KEY } from '../../samples/phase-17/shared/overlayCycleInput.js';
99
import {
1010
getOverlayGameplayRuntimeCompositionSnapshot,
11+
getOverlayGameplayRuntimeInteractionSnapshot,
1112
renderOverlayGameplayRuntime,
1213
stepOverlayGameplayRuntime,
1314
} from '../../samples/phase-17/shared/overlayGameplayRuntime.js';
@@ -238,9 +239,66 @@ function assertContextAwareOverlayBehavior() {
238239
assert.equal(visibleSnapshot[0].slot.height, 104, 'Context-aware runtime should adapt panel height from gameplay state behavior.');
239240
}
240241

242+
function assertOverlayStateSynchronizationAndDesyncRecovery() {
243+
const framework = createPhase19OverlayExpansionFramework();
244+
framework.registerExtension(definePhase19OverlayExtension({
245+
id: 'phase19-overlay-sync',
246+
overlays: [
247+
{ id: 'ui', label: 'UI' },
248+
{ id: 'runtime-a', label: 'Runtime A' },
249+
{ id: 'runtime-b', label: 'Runtime B' },
250+
],
251+
initialOverlayId: 'ui',
252+
runtimeExtensions: [
253+
{ overlayId: 'runtime-a', onRender() {} },
254+
{ overlayId: 'runtime-b', onRender() {} },
255+
],
256+
}));
257+
258+
const runtime = framework.createRuntimeForExtension('phase19-overlay-sync');
259+
const gameplayState = {
260+
overlayRuntimeState: {
261+
visible: true,
262+
interactionIndex: 99,
263+
activeOverlayId: 'runtime-b',
264+
},
265+
};
266+
267+
const initialInteraction = getOverlayGameplayRuntimeInteractionSnapshot(runtime, { gameplayState });
268+
assert.equal(initialInteraction.index, 1, 'Sync should correct out-of-range index to the overlay requested by gameplay state.');
269+
assert.equal(initialInteraction.activeOverlayId, 'runtime-b', 'Sync should align active overlay id with gameplay state request.');
270+
assert.equal(gameplayState.overlayRuntimeState.desyncCorrected, true, 'Sync should flag corrected desync state.');
271+
assert.equal(gameplayState.overlayRuntimeState.count, 2, 'Sync snapshot should expose runtime extension count.');
272+
273+
const renderRuntimeB = renderOverlayGameplayRuntime(runtime, {
274+
activeOverlayId: 'runtime-b',
275+
renderer: createRendererProbe(),
276+
gameplayState,
277+
});
278+
assert.equal(renderRuntimeB, 1, 'Synced runtime should render requested gameplay overlay.');
279+
280+
gameplayState.overlayRuntimeState.visible = false;
281+
gameplayState.overlayRuntimeState.activeOverlayId = 'runtime-a';
282+
const renderHidden = renderOverlayGameplayRuntime(runtime, {
283+
activeOverlayId: 'runtime-a',
284+
renderer: createRendererProbe(),
285+
gameplayState,
286+
});
287+
assert.equal(renderHidden, 0, 'Gameplay visibility sync should prevent overlay render while hidden.');
288+
289+
gameplayState.overlayRuntimeState.visible = true;
290+
const renderRuntimeA = renderOverlayGameplayRuntime(runtime, {
291+
activeOverlayId: 'runtime-a',
292+
renderer: createRendererProbe(),
293+
gameplayState,
294+
});
295+
assert.equal(renderRuntimeA, 1, 'Gameplay sync should recover to visible overlay rendering without desync.');
296+
}
297+
241298
export function run() {
242299
assertExpansionRegistrationAndCompatibility();
243300
assertExtensionLifecycleMutations();
244301
assertDynamicPanelSizingCapability();
245302
assertContextAwareOverlayBehavior();
303+
assertOverlayStateSynchronizationAndDesyncRecovery();
246304
}

0 commit comments

Comments
 (0)