@@ -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+
94110function 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+
221334function 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
588702export 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
696812export 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
739856export function renderOverlayGameplayRuntime ( runtime , context = { } ) {
857+ synchronizeOverlayGameplayRuntimeState ( runtime , context ) ;
740858 if (
741859 ! runtime ||
742860 runtime . interactionVisible === false ||
0 commit comments