Moved persistence related code to its own file

This commit is contained in:
thepaperpilot 2022-02-27 16:18:13 -06:00
parent a8af84f581
commit a81040d6a2
16 changed files with 129 additions and 147 deletions

View file

@ -19,7 +19,7 @@ import {
createCumulativeConversion,
createExponentialScaling
} from "@/features/conversion";
import { jsx, persistent, showIf, Visibility } from "@/features/feature";
import { jsx, showIf, Visibility } from "@/features/feature";
import { createHotkey } from "@/features/hotkey";
import { createInfobox } from "@/features/infoboxes/infobox";
import { createMilestone } from "@/features/milestones/milestone";
@ -32,6 +32,7 @@ import { createTabButton, createTabFamily } from "@/features/tabs/tabFamily";
import { createTree, createTreeNode, GenericTreeNode, TreeBranch } from "@/features/trees/tree";
import { createUpgrade } from "@/features/upgrades/upgrade";
import { createLayer } from "@/game/layers";
import { persistent } from "@/game/persistence";
import settings from "@/game/settings";
import { DecimalSource } from "@/lib/break_eternity";
import Decimal, { format, formatWhole } from "@/util/bignum";

View file

@ -2,12 +2,13 @@ import { createLayerTreeNode, createResetButton } from "@/data/common";
import { main } from "@/data/mod";
import { createClickable } from "@/features/clickables/clickable";
import { createExponentialScaling, createIndependentConversion } from "@/features/conversion";
import { jsx, persistent } from "@/features/feature";
import { jsx } from "@/features/feature";
import { createInfobox } from "@/features/infoboxes/infobox";
import { createReset } from "@/features/reset";
import MainDisplay from "@/features/resources/MainDisplay.vue";
import { createResource, displayResource } from "@/features/resources/resource";
import { createLayer } from "@/game/layers";
import { persistent } from "@/game/persistence";
import Decimal, { DecimalSource, formatWhole } from "@/util/bignum";
import { render } from "@/util/vue";
import c from "./c";

View file

@ -5,9 +5,6 @@ import {
findFeatures,
GatherProps,
getUniqueID,
makePersistent,
Persistent,
PersistentState,
Replace,
setDefault,
StyleValue,
@ -15,6 +12,7 @@ import {
} from "@/features/feature";
import { globalBus } from "@/game/events";
import "@/game/notifications";
import { Persistent, makePersistent, PersistentState } from "@/game/persistence";
import {
Computable,
GetComputableType,

View file

@ -4,16 +4,13 @@ import {
findFeatures,
GatherProps,
getUniqueID,
makePersistent,
Persistent,
PersistentState,
Replace,
setDefault,
State,
StyleValue,
Visibility
} from "@/features/feature";
import { globalBus } from "@/game/events";
import { State, Persistent, makePersistent, PersistentState } from "@/game/persistence";
import Decimal, { DecimalSource } from "@/lib/break_eternity";
import { isFunction } from "@/util/common";
import {

View file

@ -1,5 +1,6 @@
import ClickableComponent from "@/features/clickables/Clickable.vue";
import { Resource } from "@/features/resources/resource";
import { Persistent, makePersistent, PersistentState } from "@/game/persistence";
import Decimal, { DecimalSource, format, formatWhole } from "@/util/bignum";
import {
Computable,
@ -17,9 +18,6 @@ import {
GatherProps,
getUniqueID,
jsx,
makePersistent,
Persistent,
PersistentState,
Replace,
setDefault,
StyleValue,

View file

@ -4,8 +4,6 @@ import {
Component,
GatherProps,
getUniqueID,
persistent,
PersistentRef,
Replace,
setDefault,
StyleValue,
@ -14,6 +12,7 @@ import {
import { GenericReset } from "@/features/reset";
import { Resource } from "@/features/resources/resource";
import { globalBus } from "@/game/events";
import { PersistentRef, persistent } from "@/game/persistence";
import settings from "@/game/settings";
import Decimal, { DecimalSource } from "@/util/bignum";
import {

View file

@ -1,37 +1,15 @@
import { globalBus } from "@/game/events";
import { GenericLayer } from "@/game/layers";
import Decimal, { DecimalSource } from "@/util/bignum";
import { DefaultValue } from "@/game/persistence";
import Decimal from "@/util/bignum";
import { DoNotCache, ProcessedComputable } from "@/util/computed";
import { ProxyState } from "@/util/proxies";
import { isArray } from "@vue/shared";
import { CSSProperties, DefineComponent, isRef, ref, Ref } from "vue";
import { CSSProperties, DefineComponent, isRef } from "vue";
export const PersistentState = Symbol("PersistentState");
export const DefaultValue = Symbol("DefaultValue");
export const Component = Symbol("Component");
export const GatherProps = Symbol("GatherProps");
// Note: This is a union of things that should be safely stringifiable without needing
// special processes for 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
export type State =
| string
| number
| boolean
| DecimalSource
| { [key: string]: State }
| { [key: number]: State };
export type JSXFunction = (() => JSX.Element) & { [DoNotCache]: true };
export type CoercableComponent = string | DefineComponent | JSXFunction;
export type StyleValue = string | CSSProperties | Array<string | CSSProperties>;
export type Persistent<T extends State = State> = {
[PersistentState]: Ref<T>;
[DefaultValue]: T;
};
export type PersistentRef<T extends State = State> = Ref<T> & Persistent<T>;
// TODO if importing .vue components in .tsx can become type safe,
// this type can probably be safely removed
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -70,27 +48,6 @@ export function showIf(condition: boolean, otherwise = Visibility.None): Visibil
return condition ? Visibility.Visible : otherwise;
}
export function persistent<T extends State>(defaultValue: T | Ref<T>): PersistentRef<T> {
const persistent = (
isRef(defaultValue) ? defaultValue : (ref<T>(defaultValue) as unknown)
) as PersistentRef<T>;
persistent[PersistentState] = persistent;
persistent[DefaultValue] = isRef(defaultValue) ? defaultValue.value : defaultValue;
return persistent as PersistentRef<T>;
}
export function makePersistent<T extends State>(
obj: unknown,
defaultValue: T
): asserts obj is Persistent<T> {
const persistent = obj as Partial<Persistent<T>>;
const state = ref(defaultValue) as Ref<T>;
persistent[PersistentState] = state;
persistent[DefaultValue] = isRef(defaultValue) ? (defaultValue.value as T) : defaultValue;
}
export function setDefault<T, K extends keyof T>(
object: T,
key: K,
@ -118,62 +75,3 @@ export function findFeatures(obj: Record<string, unknown>, type: symbol): unknow
handleObject(obj);
return objects;
}
globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>) => {
const handleObject = (obj: Record<string, unknown>, path: string[] = []): boolean => {
let foundPersistent = false;
Object.keys(obj).forEach(key => {
const value = obj[key];
if (value && typeof value === "object") {
if (PersistentState in value) {
foundPersistent = true;
// 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
persistentState[key] = (value as Persistent)[PersistentState];
// Load previously saved value
if (savedValue != null) {
(persistentState[key] as Ref<unknown>).value = savedValue;
} else {
(persistentState[key] as Ref<unknown>).value = (value as Persistent)[
DefaultValue
];
}
} else if (!(value instanceof Decimal) && !isRef(value)) {
// 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;
};
handleObject(layer);
});

View file

@ -4,12 +4,8 @@ import {
Component,
GatherProps,
getUniqueID,
makePersistent,
Persistent,
PersistentState,
Replace,
setDefault,
State,
StyleValue,
Visibility
} from "@/features/feature";
@ -23,6 +19,7 @@ import {
} from "@/util/computed";
import { createLazyProxy } from "@/util/proxies";
import { computed, Ref, unref } from "vue";
import { State, Persistent, makePersistent, PersistentState } from "@/game/persistence";
export const GridType = Symbol("Grid");

View file

@ -4,9 +4,6 @@ import {
Component,
GatherProps,
getUniqueID,
makePersistent,
Persistent,
PersistentState,
Replace,
setDefault,
StyleValue,
@ -21,6 +18,7 @@ import {
} from "@/util/computed";
import { createLazyProxy } from "@/util/proxies";
import { Ref } from "vue";
import { Persistent, makePersistent, PersistentState } from "@/game/persistence";
export const InfoboxType = Symbol("Infobox");

View file

@ -5,9 +5,6 @@ import {
findFeatures,
GatherProps,
getUniqueID,
makePersistent,
Persistent,
PersistentState,
Replace,
setDefault,
StyleValue,
@ -28,6 +25,7 @@ import { coerceComponent, isCoercableComponent } from "@/util/vue";
import { Unsubscribe } from "nanoevents";
import { computed, Ref, unref } from "vue";
import { useToast } from "vue-toastification";
import { Persistent, makePersistent, PersistentState } from "@/game/persistence";
export const MilestoneType = Symbol("Milestone");

View file

@ -1,7 +1,7 @@
import { persistent, State } from "@/features/feature";
import Decimal, { DecimalSource, format, formatWhole } from "@/util/bignum";
import { computed, ComputedRef, ref, Ref, watch } from "vue";
import { globalBus } from "@/game/events";
import { State, persistent } from "@/game/persistence";
export interface Resource<T = DecimalSource> extends Ref<T> {
displayName: string;

View file

@ -3,9 +3,6 @@ import {
Component,
GatherProps,
getUniqueID,
makePersistent,
Persistent,
PersistentState,
Replace,
setDefault,
StyleValue,
@ -13,6 +10,7 @@ import {
} from "@/features/feature";
import TabButtonComponent from "@/features/tabs/TabButton.vue";
import TabFamilyComponent from "@/features/tabs/TabFamily.vue";
import { Persistent, makePersistent, PersistentState } from "@/game/persistence";
import {
Computable,
GetComputableType,

View file

@ -3,7 +3,6 @@ import {
Component,
GatherProps,
getUniqueID,
persistent,
Replace,
setDefault,
StyleValue,
@ -14,6 +13,7 @@ import { GenericReset } from "@/features/reset";
import { displayResource, Resource } from "@/features/resources/resource";
import { Tooltip } from "@/features/tooltip";
import TreeComponent from "@/features/trees/Tree.vue";
import { persistent } from "@/game/persistence";
import { DecimalSource, format } from "@/util/bignum";
import Decimal, { formatWhole } from "@/util/break_eternity";
import {

View file

@ -5,9 +5,6 @@ import {
findFeatures,
GatherProps,
getUniqueID,
makePersistent,
Persistent,
PersistentState,
Replace,
setDefault,
StyleValue,
@ -26,6 +23,7 @@ import {
} from "@/util/computed";
import { createLazyProxy } from "@/util/proxies";
import { computed, Ref, unref } from "vue";
import { Persistent, makePersistent, PersistentState } from "@/game/persistence";
export const UpgradeType = Symbol("Upgrade");
@ -89,9 +87,9 @@ export function createUpgrade<T extends UpgradeOptions>(
upgrade.type = UpgradeType;
upgrade[Component] = UpgradeComponent;
if (upgrade.canPurchase == null && (upgrade.resource == null || upgrade.cost == null)) {
if (upgrade.canAfford == null && (upgrade.resource == null || upgrade.cost == null)) {
console.warn(
"Error: can't create upgrade without a canPurchase property or a resource and cost property",
"Error: can't create upgrade without a canAfford property or a resource and cost property",
upgrade
);
}

View file

@ -1,11 +1,4 @@
import {
CoercableComponent,
persistent,
PersistentRef,
Replace,
setDefault,
StyleValue
} from "@/features/feature";
import { CoercableComponent, Replace, setDefault, StyleValue } from "@/features/feature";
import { Link } from "@/features/links";
import Decimal from "@/util/bignum";
import {
@ -18,6 +11,7 @@ import {
import { createLazyProxy } from "@/util/proxies";
import { createNanoEvents, Emitter } from "nanoevents";
import { globalBus } from "./events";
import { persistent, PersistentRef } from "./persistence";
import player from "./player";
export interface LayerEvents {

107
src/game/persistence.ts Normal file
View file

@ -0,0 +1,107 @@
import { globalBus } from "@/game/events";
import Decimal, { DecimalSource } from "@/util/bignum";
import { ProxyState } from "@/util/proxies";
import { isArray } from "@vue/shared";
import { isRef, Ref, ref } from "vue";
import { GenericLayer } from "./layers";
export const PersistentState = Symbol("PersistentState");
export const DefaultValue = Symbol("DefaultValue");
// Note: This is a union of things that should be safely stringifiable without needing
// special processes for 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
export type State =
| string
| number
| boolean
| DecimalSource
| { [key: string]: State }
| { [key: number]: State };
export type Persistent<T extends State = State> = {
[PersistentState]: Ref<T>;
[DefaultValue]: T;
};
export type PersistentRef<T extends State = State> = Ref<T> & Persistent<T>;
export function persistent<T extends State>(defaultValue: T | Ref<T>): PersistentRef<T> {
const persistent = (
isRef(defaultValue) ? defaultValue : (ref<T>(defaultValue) as unknown)
) as PersistentRef<T>;
persistent[PersistentState] = persistent;
persistent[DefaultValue] = isRef(defaultValue) ? defaultValue.value : defaultValue;
return persistent as PersistentRef<T>;
}
export function makePersistent<T extends State>(
obj: unknown,
defaultValue: T
): asserts obj is Persistent<T> {
const persistent = obj as Partial<Persistent<T>>;
const state = ref(defaultValue) as Ref<T>;
persistent[PersistentState] = state;
persistent[DefaultValue] = isRef(defaultValue) ? (defaultValue.value as T) : defaultValue;
}
globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>) => {
const handleObject = (obj: Record<string, unknown>, path: string[] = []): boolean => {
let foundPersistent = false;
Object.keys(obj).forEach(key => {
const value = obj[key];
if (value && typeof value === "object") {
if (PersistentState in value) {
foundPersistent = true;
// 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
persistentState[key] = (value as Persistent)[PersistentState];
// Load previously saved value
if (savedValue != null) {
(persistentState[key] as Ref<unknown>).value = savedValue;
} else {
(persistentState[key] as Ref<unknown>).value = (value as Persistent)[
DefaultValue
];
}
} else if (!(value instanceof Decimal) && !isRef(value)) {
// 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;
};
handleObject(layer);
});