@@ -249,6 +249,149 @@ function writeOverlayRuntimePreferencePayloadToStorage(preferenceStorageKey, pay
249249 return true ;
250250}
251251
252+ function validateOverlayRuntimePreferencePayload ( payload ) {
253+ const errors = [ ] ;
254+ if ( ! payload || typeof payload !== 'object' || Array . isArray ( payload ) ) {
255+ return {
256+ valid : false ,
257+ errors : Object . freeze ( [ 'Overlay runtime profile payload must be an object.' ] ) ,
258+ value : null ,
259+ } ;
260+ }
261+
262+ const hasVisibility = Object . prototype . hasOwnProperty . call ( payload , 'visibility' ) ;
263+ const hasLayout = Object . prototype . hasOwnProperty . call ( payload , 'layout' ) ;
264+ const hasKeybindProfile = Object . prototype . hasOwnProperty . call ( payload , 'keybindProfile' ) ;
265+ if ( ! hasVisibility && ! hasLayout && ! hasKeybindProfile ) {
266+ errors . push ( 'Overlay runtime profile payload must include at least one of visibility, layout, or keybindProfile.' ) ;
267+ }
268+
269+ const versionRaw = payload . version ;
270+ if ( versionRaw !== undefined ) {
271+ const version = Number ( versionRaw ) ;
272+ if ( ! Number . isInteger ( version ) || version <= 0 ) {
273+ errors . push ( 'Overlay runtime profile version must be a positive integer when provided.' ) ;
274+ }
275+ }
276+
277+ let visibility = null ;
278+ if ( hasVisibility ) {
279+ if ( payload . visibility === true || payload . visibility === false ) {
280+ visibility = payload . visibility ;
281+ } else {
282+ errors . push ( 'Overlay runtime profile visibility must be a boolean.' ) ;
283+ }
284+ }
285+
286+ let layout = null ;
287+ if ( hasLayout ) {
288+ if ( ! payload . layout || typeof payload . layout !== 'object' || Array . isArray ( payload . layout ) ) {
289+ errors . push ( 'Overlay runtime profile layout must be an object.' ) ;
290+ } else {
291+ layout = { } ;
292+ const entries = Object . entries ( payload . layout ) ;
293+ for ( let i = 0 ; i < entries . length ; i += 1 ) {
294+ const [ layoutKey , layoutRect ] = entries [ i ] ;
295+ const normalizedLayoutKey = String ( layoutKey || '' ) . trim ( ) ;
296+ const normalizedRect = normalizeLayoutOverrideRect ( layoutRect ) ;
297+ if ( ! normalizedLayoutKey || ! normalizedRect ) {
298+ errors . push ( `Overlay runtime profile layout entry "${ normalizedLayoutKey || '<empty>' } " is invalid.` ) ;
299+ continue ;
300+ }
301+ layout [ normalizedLayoutKey ] = normalizedRect ;
302+ }
303+ }
304+ }
305+
306+ let keybindProfile = null ;
307+ if ( hasKeybindProfile ) {
308+ if ( ! payload . keybindProfile || typeof payload . keybindProfile !== 'object' || Array . isArray ( payload . keybindProfile ) ) {
309+ errors . push ( 'Overlay runtime profile keybindProfile must be an object.' ) ;
310+ } else {
311+ keybindProfile = { } ;
312+ if ( Object . prototype . hasOwnProperty . call ( payload . keybindProfile , 'id' ) ) {
313+ keybindProfile . id = String ( payload . keybindProfile . id || '' ) . trim ( ) ;
314+ }
315+ if ( Object . prototype . hasOwnProperty . call ( payload . keybindProfile , 'cycleKey' ) ) {
316+ const cycleKey = String ( payload . keybindProfile . cycleKey || '' ) . trim ( ) ;
317+ if ( ! cycleKey ) {
318+ errors . push ( 'Overlay runtime profile keybindProfile.cycleKey must be a non-empty string when provided.' ) ;
319+ } else {
320+ keybindProfile . cycleKey = cycleKey ;
321+ }
322+ }
323+ if ( Object . prototype . hasOwnProperty . call ( payload . keybindProfile , 'contextInputMap' ) ) {
324+ const contextInputMap = payload . keybindProfile . contextInputMap ;
325+ if ( contextInputMap === null ) {
326+ keybindProfile . contextInputMap = null ;
327+ keybindProfile . contextInputMapSpecified = true ;
328+ } else if ( contextInputMap && typeof contextInputMap === 'object' && ! Array . isArray ( contextInputMap ) ) {
329+ const clonedContextInputMap = cloneJsonCompatibleValue ( contextInputMap ) ;
330+ if ( clonedContextInputMap && typeof clonedContextInputMap === 'object' ) {
331+ keybindProfile . contextInputMap = clonedContextInputMap ;
332+ keybindProfile . contextInputMapSpecified = true ;
333+ } else {
334+ errors . push ( 'Overlay runtime profile keybindProfile.contextInputMap must be JSON-compatible.' ) ;
335+ }
336+ } else {
337+ errors . push ( 'Overlay runtime profile keybindProfile.contextInputMap must be an object or null.' ) ;
338+ }
339+ }
340+ }
341+ }
342+
343+ if ( errors . length > 0 ) {
344+ return {
345+ valid : false ,
346+ errors : Object . freeze ( errors ) ,
347+ value : null ,
348+ } ;
349+ }
350+
351+ return {
352+ valid : true ,
353+ errors : Object . freeze ( [ ] ) ,
354+ value : Object . freeze ( {
355+ version : Number . isInteger ( Number ( payload . version ) ) && Number ( payload . version ) > 0
356+ ? Number ( payload . version )
357+ : 1 ,
358+ hasVisibility,
359+ hasLayout,
360+ hasKeybindProfile,
361+ visibility,
362+ layout : layout || { } ,
363+ keybindProfile : keybindProfile || { } ,
364+ } ) ,
365+ } ;
366+ }
367+
368+ function applyOverlayRuntimePreferencePayload ( runtime , validatedPayload ) {
369+ if ( ! runtime || ! validatedPayload || typeof validatedPayload !== 'object' ) {
370+ return false ;
371+ }
372+ if ( validatedPayload . hasVisibility && ( validatedPayload . visibility === true || validatedPayload . visibility === false ) ) {
373+ runtime . interactionVisible = validatedPayload . visibility ;
374+ }
375+ if ( validatedPayload . hasLayout ) {
376+ applyOverlayRuntimeLayoutPreferences ( runtime , validatedPayload . layout ) ;
377+ }
378+ if ( validatedPayload . hasKeybindProfile ) {
379+ const keybindProfile = validatedPayload . keybindProfile || { } ;
380+ if ( Object . prototype . hasOwnProperty . call ( keybindProfile , 'id' ) ) {
381+ runtime . interactionKeybindProfileId = String ( keybindProfile . id || '' ) . trim ( ) ;
382+ }
383+ if ( Object . prototype . hasOwnProperty . call ( keybindProfile , 'cycleKey' ) ) {
384+ runtime . interactionCycleKey = String ( keybindProfile . cycleKey || '' ) . trim ( ) || LEVEL17_OVERLAY_CYCLE_KEY ;
385+ }
386+ if ( keybindProfile . contextInputMapSpecified === true ) {
387+ runtime . interactionContextInputMap = keybindProfile . contextInputMap === null
388+ ? null
389+ : ( cloneJsonCompatibleValue ( keybindProfile . contextInputMap ) ?? null ) ;
390+ }
391+ }
392+ return true ;
393+ }
394+
252395function buildOverlayRuntimeLayoutPreferenceSnapshot ( runtime ) {
253396 const snapshot = { } ;
254397 const overrides = runtime ?. interactionLayoutOverrides ;
@@ -1439,6 +1582,17 @@ export function getOverlayGameplayRuntimePreferencesSnapshot(runtime) {
14391582 } ) ;
14401583}
14411584
1585+ export function exportOverlayGameplayRuntimeProfile ( runtime , { pretty = false } = { } ) {
1586+ const snapshot = getOverlayGameplayRuntimePreferencesSnapshot ( runtime ) ;
1587+ const payload = {
1588+ version : 1 ,
1589+ visibility : snapshot . visibility ,
1590+ layout : snapshot . layout ,
1591+ keybindProfile : snapshot . keybindProfile ,
1592+ } ;
1593+ return JSON . stringify ( payload , null , pretty ? 2 : 0 ) ;
1594+ }
1595+
14421596export function saveOverlayGameplayRuntimePreferences ( runtime , options = { } ) {
14431597 if ( ! runtime ) {
14441598 return false ;
@@ -1476,28 +1630,51 @@ export function loadOverlayGameplayRuntimePreferences(runtime, options = {}) {
14761630 if ( ! payload || typeof payload !== 'object' ) {
14771631 return false ;
14781632 }
1633+ const validated = validateOverlayRuntimePreferencePayload ( payload ) ;
1634+ if ( ! validated . valid || ! validated . value ) {
1635+ return false ;
1636+ }
1637+ return applyOverlayRuntimePreferencePayload ( runtime , validated . value ) ;
1638+ }
14791639
1480- if ( payload . visibility === true || payload . visibility === false ) {
1481- runtime . interactionVisible = payload . visibility ;
1640+ export function importOverlayGameplayRuntimeProfile ( runtime , profileInput , options = { } ) {
1641+ if ( ! runtime ) {
1642+ return Object . freeze ( {
1643+ success : false ,
1644+ errors : Object . freeze ( [ 'Overlay runtime is required for profile import.' ] ) ,
1645+ } ) ;
14821646 }
1483- applyOverlayRuntimeLayoutPreferences ( runtime , payload . layout ) ;
1484- const keybindProfile = payload . keybindProfile && typeof payload . keybindProfile === 'object'
1485- ? payload . keybindProfile
1486- : null ;
1487- if ( keybindProfile ) {
1488- runtime . interactionKeybindProfileId = String ( keybindProfile . id || '' ) . trim ( ) ;
1489- const cycleKey = String ( keybindProfile . cycleKey || '' ) . trim ( ) ;
1490- if ( cycleKey ) {
1491- runtime . interactionCycleKey = cycleKey ;
1492- }
1493- if ( keybindProfile . contextInputMap && typeof keybindProfile . contextInputMap === 'object' ) {
1494- const clonedContextInputMap = cloneJsonCompatibleValue ( keybindProfile . contextInputMap ) ;
1495- if ( clonedContextInputMap && typeof clonedContextInputMap === 'object' ) {
1496- runtime . interactionContextInputMap = clonedContextInputMap ;
1497- }
1647+
1648+ let parsedInput = null ;
1649+ if ( typeof profileInput === 'string' ) {
1650+ try {
1651+ parsedInput = JSON . parse ( profileInput ) ;
1652+ } catch {
1653+ return Object . freeze ( {
1654+ success : false ,
1655+ errors : Object . freeze ( [ 'Overlay runtime profile JSON is invalid.' ] ) ,
1656+ } ) ;
14981657 }
1658+ } else {
1659+ parsedInput = cloneJsonCompatibleValue ( profileInput ) ;
14991660 }
1500- return true ;
1661+
1662+ const validated = validateOverlayRuntimePreferencePayload ( parsedInput ) ;
1663+ if ( ! validated . valid || ! validated . value ) {
1664+ return Object . freeze ( {
1665+ success : false ,
1666+ errors : validated . errors ,
1667+ } ) ;
1668+ }
1669+
1670+ applyOverlayRuntimePreferencePayload ( runtime , validated . value ) ;
1671+ if ( options ?. persist !== false ) {
1672+ saveOverlayGameplayRuntimePreferences ( runtime , { silent : true } ) ;
1673+ }
1674+ return Object . freeze ( {
1675+ success : true ,
1676+ errors : Object . freeze ( [ ] ) ,
1677+ } ) ;
15011678}
15021679
15031680export function setOverlayGameplayRuntimeContextInputMap ( runtime , contextInputMap ) {
0 commit comments