2022-06-27 00:17:22 +00:00
import { isArray } from "@vue/shared" ;
2022-03-04 03:39:48 +00:00
import { globalBus } from "game/events" ;
2022-06-27 00:17:22 +00:00
import type { GenericLayer } from "game/layers" ;
import { addingLayers , persistentRefs } from "game/layers" ;
import type { DecimalSource } from "util/bignum" ;
import Decimal from "util/bignum" ;
2022-03-04 03:39:48 +00:00
import { ProxyState } from "util/proxies" ;
2022-12-06 04:53:46 +00:00
import type { Ref , WritableComputedRef } from "vue" ;
import { computed , isReactive , isRef , ref } from "vue" ;
2022-02-27 22:18:13 +00:00
2022-07-15 05:55:36 +00:00
/ * *
* A symbol used in { @link Persistent } objects .
* @see { @link Persistent [ PersistentState ] }
* /
2022-02-27 22:18:13 +00:00
export const PersistentState = Symbol ( "PersistentState" ) ;
2022-07-15 05:55:36 +00:00
/ * *
* A symbol used in { @link Persistent } objects .
* @see { @link Persistent [ DefaultValue ] }
* /
2022-02-27 22:18:13 +00:00
export const DefaultValue = Symbol ( "DefaultValue" ) ;
2022-07-15 05:55:36 +00:00
/ * *
* A symbol used in { @link Persistent } objects .
* @see { @link Persistent [ StackTrace ] }
* /
2022-04-06 03:16:40 +00:00
export const StackTrace = Symbol ( "StackTrace" ) ;
2022-07-15 05:55:36 +00:00
/ * *
* A symbol used in { @link Persistent } objects .
* @see { @link Persistent [ Deleted ] }
* /
2022-04-06 03:16:40 +00:00
export const Deleted = Symbol ( "Deleted" ) ;
2022-12-06 04:53:46 +00:00
/ * *
* A symbol used in { @link Persistent } objects .
* @see { @link Persistent [ NonPersistent ] }
* /
export const NonPersistent = Symbol ( "NonPersistent" ) ;
/ * *
* A symbol used in { @link Persistent } objects .
* @see { @link Persistent [ SaveDataPath ] }
* /
export const SaveDataPath = Symbol ( "SaveDataPath" ) ;
2022-02-27 22:18:13 +00:00
2022-07-15 05:55:36 +00:00
/ * *
* This is a union of things that should be safely stringifiable without needing special processes or knowing what to load them in as .
* - Decimals aren 't allowed because we' d need to know to parse them back .
* - DecimalSources are allowed because the string is a valid value for them
* /
2022-02-27 22:18:13 +00:00
export type State =
| string
| number
| boolean
| DecimalSource
| { [ key : string ] : State }
| { [ key : number ] : State } ;
2022-07-15 05:55:36 +00:00
/ * *
* A { @link Ref } that has been augmented with properties to allow it to be saved and loaded within the player save data object .
* /
2022-04-06 03:16:40 +00:00
export type Persistent < T extends State = State > = Ref < T > & {
2022-07-15 05:55:36 +00:00
/** A flag that this is a persistent property. Typically a circular reference. */
2022-02-27 22:18:13 +00:00
[ PersistentState ] : Ref < T > ;
2022-07-15 05:55:36 +00:00
/** The value the ref should be set to in a fresh save, or when updating an old save to the current version. */
2022-02-27 22:18:13 +00:00
[ DefaultValue ] : T ;
2022-07-15 05:55:36 +00:00
/** The stack trace of where the persistent ref was created. This is used for debugging purposes when a persistent ref is created but not placed in its layer object. */
2022-04-06 03:16:40 +00:00
[ StackTrace ] : string ;
2022-07-15 05:55:36 +00:00
/ * *
* This is a flag that can be set once the option func is evaluated , to mark that a persistent ref should _not_ be saved to the player save data object .
* @see { @link deletePersistent } for marking a persistent ref as deleted .
* /
2022-04-06 03:16:40 +00:00
[ Deleted ] : boolean ;
2022-12-06 04:53:46 +00:00
/ * *
* A non - persistent ref that just reads and writes ot the persistent ref . Used for passing to other features without duplicating the persistent ref in the constructed save data object .
* /
2022-12-06 13:14:42 +00:00
[ NonPersistent ] : NonPersistent < T > ;
2022-12-06 04:53:46 +00:00
/ * *
* The path this persistent appears in within the save data object . Predominantly used to ensure it ' s only placed in there one time .
* /
[ SaveDataPath ] : string [ ] | undefined ;
2022-02-27 22:18:13 +00:00
} ;
2022-12-06 13:14:42 +00:00
export type NonPersistent < T extends State = State > = WritableComputedRef < T > & { [ DefaultValue ] : T } ;
2022-04-06 03:16:40 +00:00
function getStackTrace() {
return (
new Error ( ) . stack
? . split ( "\n" )
. slice ( 3 , 5 )
. map ( line = > line . trim ( ) )
2022-12-21 03:26:25 +00:00
. join ( "\n" ) ? ? ""
2022-04-06 03:16:40 +00:00
) ;
}
2022-07-15 05:55:36 +00:00
/ * *
* Create a persistent ref , which can be saved and loaded .
* All ( non - deleted ) persistent refs must be included somewhere within the layer object returned by that layer ' s options func .
* @param defaultValue The value the persistent ref should start at on fresh saves or when reset .
* /
2022-04-06 03:16:40 +00:00
export function persistent < T extends State > ( defaultValue : T | Ref < T > ) : Persistent < T > {
2022-02-27 22:18:13 +00:00
const persistent = (
isRef ( defaultValue ) ? defaultValue : ( ref < T > ( defaultValue ) as unknown )
2022-04-06 03:16:40 +00:00
) as Persistent < T > ;
2022-02-27 22:18:13 +00:00
persistent [ PersistentState ] = persistent ;
persistent [ DefaultValue ] = isRef ( defaultValue ) ? defaultValue.value : defaultValue ;
2022-04-06 03:16:40 +00:00
persistent [ StackTrace ] = getStackTrace ( ) ;
persistent [ Deleted ] = false ;
2022-12-06 13:14:42 +00:00
const nonPersistent : Partial < NonPersistent < T > > = computed ( {
2022-12-06 04:53:46 +00:00
get ( ) {
return persistent . value ;
} ,
set ( value ) {
persistent . value = value ;
}
} ) ;
2022-12-06 13:09:21 +00:00
nonPersistent [ DefaultValue ] = persistent [ DefaultValue ] ;
2022-12-06 13:14:42 +00:00
persistent [ NonPersistent ] = nonPersistent as NonPersistent < T > ;
2022-12-06 04:53:46 +00:00
persistent [ SaveDataPath ] = undefined ;
2022-02-27 22:18:13 +00:00
2022-04-06 03:16:40 +00:00
if ( addingLayers . length === 0 ) {
console . warn (
"Creating a persistent ref outside of a layer. This is not officially supported" ,
persistent ,
"\nCreated at:\n" + persistent [ StackTrace ]
) ;
} else {
persistentRefs [ addingLayers [ addingLayers . length - 1 ] ] . add ( persistent ) ;
}
2022-02-27 22:18:13 +00:00
2022-04-06 03:16:40 +00:00
return persistent as Persistent < T > ;
}
2022-12-06 04:53:46 +00:00
/ * *
* Type guard for whether an arbitrary value is a persistent ref
* @param value The value that may or may not be a persistent ref
* /
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function isPersistent ( value : any ) : value is Persistent {
2022-12-21 03:26:25 +00:00
return value != null && typeof value === "object" && PersistentState in value ;
2022-12-06 04:53:46 +00:00
}
/ * *
* Unwraps the non - persistent ref inside of persistent refs , to be passed to other features without duplicating values in the save data object .
* @param persistent The persistent ref to unwrap
* /
export function noPersist < T extends Persistent < S > , S extends State > (
persistent : T
) : T [ typeof NonPersistent ] {
return persistent [ NonPersistent ] ;
}
2022-07-15 05:55:36 +00:00
/ * *
* Mark a { @link Persistent } as deleted , so it won ' t be saved and loaded .
* Since persistent refs must be created during a layer ' s options func , features can not create persistent refs after evaluating their own options funcs .
* As a result , it must create any persistent refs it _might_ need .
* This function can then be called after the options func is evaluated to mark the persistent ref to not be saved or loaded .
* /
2022-04-06 03:16:40 +00:00
export function deletePersistent ( persistent : Persistent ) {
if ( addingLayers . length === 0 ) {
console . warn ( "Deleting a persistent ref outside of a layer. Ignoring..." , persistent ) ;
2022-04-10 23:56:32 +00:00
} else {
persistentRefs [ addingLayers [ addingLayers . length - 1 ] ] . delete ( persistent ) ;
2022-04-06 03:16:40 +00:00
}
persistent [ Deleted ] = true ;
2022-02-27 22:18:13 +00:00
}
globalBus . on ( "addLayer" , ( layer : GenericLayer , saveData : Record < string , unknown > ) = > {
2022-02-28 00:07:21 +00:00
const features : { type : typeof Symbol } [ ] = [ ] ;
2022-02-27 22:18:13 +00:00
const handleObject = ( obj : Record < string , unknown > , path : string [ ] = [ ] ) : boolean = > {
let foundPersistent = false ;
Object . keys ( obj ) . forEach ( key = > {
2022-12-06 04:53:46 +00:00
let value = obj [ key ] ;
2022-12-21 03:26:25 +00:00
if ( value != null && typeof value === "object" ) {
2022-12-06 04:53:46 +00:00
if ( ProxyState in value ) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value = ( value as any ) [ ProxyState ] as object ;
}
if ( isPersistent ( value ) ) {
2022-02-27 22:18:13 +00:00
foundPersistent = true ;
2022-12-06 04:53:46 +00:00
if ( value [ Deleted ] ) {
2022-04-06 03:16:40 +00:00
console . warn (
"Deleted persistent ref present in returned object. Ignoring..." ,
value ,
2022-12-06 04:53:46 +00:00
"\nCreated at:\n" + value [ StackTrace ]
2022-04-06 03:16:40 +00:00
) ;
return ;
}
2022-12-06 04:53:46 +00:00
persistentRefs [ layer . id ] . delete ( value ) ;
// Handle SaveDataPath
const newPath = [ layer . id , . . . path , key ] ;
if (
value [ SaveDataPath ] != undefined &&
JSON . stringify ( newPath ) !== JSON . stringify ( value [ SaveDataPath ] )
) {
console . error (
` Persistent ref is being saved to \` ${ newPath . join (
"."
) } \ ` when it's already present at \` ${ value [ SaveDataPath ] . join (
"."
) } \ ` . This can cause unexpected behavior when loading saves between updates. ` ,
value
) ;
}
value [ SaveDataPath ] = newPath ;
2022-02-27 22:18:13 +00:00
// Construct save path if it doesn't exist
const persistentState = path . reduce < Record < string , unknown > > ( ( acc , curr ) = > {
if ( ! ( curr in acc ) ) {
acc [ curr ] = { } ;
}
return acc [ curr ] as Record < string , unknown > ;
} , saveData ) ;
// Cache currently saved value
const savedValue = persistentState [ key ] ;
// Add ref to save data
2022-12-06 04:53:46 +00:00
persistentState [ key ] = value [ PersistentState ] ;
2022-02-27 22:18:13 +00:00
// Load previously saved value
2022-04-03 23:41:52 +00:00
if ( isReactive ( persistentState ) ) {
if ( savedValue != null ) {
persistentState [ key ] = savedValue ;
} else {
2022-12-06 04:53:46 +00:00
persistentState [ key ] = value [ DefaultValue ] ;
2022-04-03 23:41:52 +00:00
}
2022-02-27 22:18:13 +00:00
} else {
2022-04-03 23:41:52 +00:00
if ( savedValue != null ) {
( persistentState [ key ] as Ref < unknown > ) . value = savedValue ;
} else {
2022-12-06 04:53:46 +00:00
( persistentState [ key ] as Ref < unknown > ) . value = value [ DefaultValue ] ;
2022-04-03 23:41:52 +00:00
}
2022-02-27 22:18:13 +00:00
}
2022-02-28 00:07:21 +00:00
} else if (
! ( value instanceof Decimal ) &&
! isRef ( value ) &&
// eslint-disable-next-line @typescript-eslint/no-explicit-any
! features . includes ( value as { type : typeof Symbol } )
) {
if ( typeof ( value as { type : typeof Symbol } ) . type === "symbol" ) {
features . push ( value as { type : typeof Symbol } ) ;
}
2022-02-27 22:18:13 +00:00
// Continue traversing
const foundPersistentInChild = handleObject ( value as Record < string , unknown > , [
. . . path ,
key
] ) ;
// Show warning for persistent values inside arrays
// TODO handle arrays better
if ( foundPersistentInChild ) {
if ( isArray ( value ) && ! isArray ( obj ) ) {
console . warn (
"Found array that contains persistent values when adding layer. Keep in mind changing the order of elements in the array will mess with existing player saves." ,
ProxyState in obj
? ( obj as Record < PropertyKey , unknown > ) [ ProxyState ]
: obj ,
key
) ;
} else {
foundPersistent = true ;
}
}
}
}
} ) ;
return foundPersistent ;
} ;
2022-12-06 04:53:46 +00:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any
handleObject ( ( layer as any ) [ ProxyState ] ) ;
2022-04-06 03:16:40 +00:00
persistentRefs [ layer . id ] . forEach ( persistent = > {
console . error (
` Created persistent ref in ${ layer . id } without registering it to the layer! Make sure to include everything persistent in the returned object ` ,
persistent ,
"\nCreated at:\n" + persistent [ StackTrace ]
) ;
} ) ;
persistentRefs [ layer . id ] . clear ( ) ;
2022-02-27 22:18:13 +00:00
} ) ;