From 5a59aaf4fcee12f4f740b890cf209681ca6be82f Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Tue, 5 Apr 2022 22:16:40 -0500 Subject: [PATCH] Persistence rework - Removed makePersistent - Removed Persistent, renamed PersistentRef to Persistent - createLazyProxy now takes optional base object - For use where previously makePersistent would be used - Added warnings when creating refs outside a layer - Added warnings when persistent refs aren't included in their layer object - createLayer now takes id as a parameter rather than an option --- src/features/achievements/achievement.tsx | 11 +- src/features/boards/board.ts | 261 +++++++++++----------- src/features/buyable.tsx | 11 +- src/features/challenges/challenge.tsx | 12 +- src/features/grids/grid.ts | 11 +- src/features/infoboxes/infobox.ts | 11 +- src/features/milestones/milestone.tsx | 11 +- src/features/tabs/tabFamily.ts | 54 +++-- src/features/trees/tree.ts | 6 +- src/features/upgrades/upgrade.ts | 11 +- src/game/layers.tsx | 22 +- src/game/persistence.ts | 74 ++++-- src/util/proxies.ts | 15 +- 13 files changed, 296 insertions(+), 214 deletions(-) diff --git a/src/features/achievements/achievement.tsx b/src/features/achievements/achievement.tsx index 930850c..31140ff 100644 --- a/src/features/achievements/achievement.tsx +++ b/src/features/achievements/achievement.tsx @@ -10,7 +10,7 @@ import { Visibility } from "features/feature"; import "game/notifications"; -import { Persistent, makePersistent, PersistentState } from "game/persistence"; +import { Persistent, PersistentState, persistent } from "game/persistence"; import { Computable, GetComputableType, @@ -69,9 +69,10 @@ export type GenericAchievement = Replace< export function createAchievement( optionsFunc: () => T & ThisType> ): Achievement { - return createLazyProxy(() => { - const achievement: T & Partial = optionsFunc(); - makePersistent(achievement, false); + return createLazyProxy(persistent => { + // Create temp literally just to avoid explicitly assigning types + const temp = Object.assign(persistent, optionsFunc()); + const achievement: Partial & typeof temp = temp; achievement.id = getUniqueID("achievement-"); achievement.type = AchievementType; achievement[Component] = AchievementComponent; @@ -122,5 +123,5 @@ export function createAchievement( } return achievement as unknown as Achievement; - }); + }, persistent(false)); } diff --git a/src/features/boards/board.ts b/src/features/boards/board.ts index 93e1089..0328c77 100644 --- a/src/features/boards/board.ts +++ b/src/features/boards/board.ts @@ -10,7 +10,7 @@ import { Visibility } from "features/feature"; import { globalBus } from "game/events"; -import { State, Persistent, makePersistent, PersistentState } from "game/persistence"; +import { State, Persistent, PersistentState, persistent } from "game/persistence"; import { isFunction } from "util/common"; import { Computable, @@ -199,135 +199,142 @@ export type GenericBoard = Replace< export function createBoard( optionsFunc: () => T & ThisType> ): Board { - return createLazyProxy(() => { - const board: T & Partial = optionsFunc(); - makePersistent(board, { + return createLazyProxy( + persistent => { + // Create temp literally just to avoid explicitly assigning types + const temp = Object.assign(persistent, optionsFunc()); + const board: Partial & typeof temp = temp; + board.id = getUniqueID("board-"); + board.type = BoardType; + board[Component] = BoardComponent; + + board.nodes = computed(() => processedBoard[PersistentState].value.nodes); + board.selectedNode = computed( + () => + processedBoard.nodes.value.find( + node => node.id === board[PersistentState].value.selectedNode + ) || null + ); + board.selectedAction = computed(() => { + const selectedNode = processedBoard.selectedNode.value; + if (selectedNode == null) { + return null; + } + const type = processedBoard.types[selectedNode.type]; + if (type.actions == null) { + return null; + } + return ( + type.actions.find( + action => action.id === processedBoard[PersistentState].value.selectedAction + ) || null + ); + }); + board.links = computed(() => { + if (processedBoard.selectedAction.value == null) { + return null; + } + if ( + processedBoard.selectedAction.value.links && + processedBoard.selectedNode.value + ) { + return getNodeProperty( + processedBoard.selectedAction.value.links, + processedBoard.selectedNode.value + ); + } + return null; + }); + processComputable(board as T, "visibility"); + setDefault(board, "visibility", Visibility.Visible); + processComputable(board as T, "width"); + setDefault(board, "width", "100%"); + processComputable(board as T, "height"); + setDefault(board, "height", "400px"); + processComputable(board as T, "classes"); + processComputable(board as T, "style"); + + for (const type in board.types) { + const nodeType: NodeTypeOptions & Partial = board.types[type]; + + processComputable(nodeType as NodeTypeOptions, "title"); + processComputable(nodeType as NodeTypeOptions, "label"); + processComputable(nodeType as NodeTypeOptions, "size"); + setDefault(nodeType, "size", 50); + processComputable(nodeType as NodeTypeOptions, "draggable"); + setDefault(nodeType, "draggable", false); + processComputable(nodeType as NodeTypeOptions, "shape"); + setDefault(nodeType, "shape", Shape.Circle); + processComputable(nodeType as NodeTypeOptions, "canAccept"); + setDefault(nodeType, "canAccept", false); + processComputable(nodeType as NodeTypeOptions, "progress"); + processComputable(nodeType as NodeTypeOptions, "progressDisplay"); + setDefault(nodeType, "progressDisplay", ProgressDisplay.Fill); + processComputable(nodeType as NodeTypeOptions, "progressColor"); + setDefault(nodeType, "progressColor", "none"); + processComputable(nodeType as NodeTypeOptions, "fillColor"); + processComputable(nodeType as NodeTypeOptions, "outlineColor"); + processComputable(nodeType as NodeTypeOptions, "titleColor"); + processComputable(nodeType as NodeTypeOptions, "actionDistance"); + setDefault(nodeType, "actionDistance", Math.PI / 6); + nodeType.nodes = computed(() => + board[PersistentState].value.nodes.filter(node => node.type === type) + ); + setDefault(nodeType, "onClick", function (node: BoardNode) { + board[PersistentState].value.selectedNode = node.id; + }); + + if (nodeType.actions) { + for (const action of nodeType.actions) { + processComputable(action, "visibility"); + setDefault(action, "visibility", Visibility.Visible); + processComputable(action, "icon"); + processComputable(action, "fillColor"); + processComputable(action, "tooltip"); + processComputable(action, "links"); + } + } + } + + board[GatherProps] = function (this: GenericBoard) { + const { + nodes, + types, + [PersistentState]: state, + visibility, + width, + height, + style, + classes, + links, + selectedAction, + selectedNode + } = this; + return { + nodes, + types, + [PersistentState]: state, + visibility, + width, + height, + style: unref(style), + classes, + links, + selectedAction, + selectedNode + }; + }; + + // This is necessary because board.types is different from T and Board + const processedBoard = board as unknown as Board; + return processedBoard; + }, + persistent({ nodes: [], selectedNode: null, selectedAction: null - }); - board.id = getUniqueID("board-"); - board.type = BoardType; - board[Component] = BoardComponent; - - board.nodes = computed(() => processedBoard[PersistentState].value.nodes); - board.selectedNode = computed( - () => - processedBoard.nodes.value.find( - node => node.id === board[PersistentState].value.selectedNode - ) || null - ); - board.selectedAction = computed(() => { - const selectedNode = processedBoard.selectedNode.value; - if (selectedNode == null) { - return null; - } - const type = processedBoard.types[selectedNode.type]; - if (type.actions == null) { - return null; - } - return ( - type.actions.find( - action => action.id === processedBoard[PersistentState].value.selectedAction - ) || null - ); - }); - board.links = computed(() => { - if (processedBoard.selectedAction.value == null) { - return null; - } - if (processedBoard.selectedAction.value.links && processedBoard.selectedNode.value) { - return getNodeProperty( - processedBoard.selectedAction.value.links, - processedBoard.selectedNode.value - ); - } - return null; - }); - processComputable(board as T, "visibility"); - setDefault(board, "visibility", Visibility.Visible); - processComputable(board as T, "width"); - setDefault(board, "width", "100%"); - processComputable(board as T, "height"); - setDefault(board, "height", "400px"); - processComputable(board as T, "classes"); - processComputable(board as T, "style"); - - for (const type in board.types) { - const nodeType: NodeTypeOptions & Partial = board.types[type]; - - processComputable(nodeType as NodeTypeOptions, "title"); - processComputable(nodeType as NodeTypeOptions, "label"); - processComputable(nodeType as NodeTypeOptions, "size"); - setDefault(nodeType, "size", 50); - processComputable(nodeType as NodeTypeOptions, "draggable"); - setDefault(nodeType, "draggable", false); - processComputable(nodeType as NodeTypeOptions, "shape"); - setDefault(nodeType, "shape", Shape.Circle); - processComputable(nodeType as NodeTypeOptions, "canAccept"); - setDefault(nodeType, "canAccept", false); - processComputable(nodeType as NodeTypeOptions, "progress"); - processComputable(nodeType as NodeTypeOptions, "progressDisplay"); - setDefault(nodeType, "progressDisplay", ProgressDisplay.Fill); - processComputable(nodeType as NodeTypeOptions, "progressColor"); - setDefault(nodeType, "progressColor", "none"); - processComputable(nodeType as NodeTypeOptions, "fillColor"); - processComputable(nodeType as NodeTypeOptions, "outlineColor"); - processComputable(nodeType as NodeTypeOptions, "titleColor"); - processComputable(nodeType as NodeTypeOptions, "actionDistance"); - setDefault(nodeType, "actionDistance", Math.PI / 6); - nodeType.nodes = computed(() => - board[PersistentState].value.nodes.filter(node => node.type === type) - ); - setDefault(nodeType, "onClick", function (node: BoardNode) { - board[PersistentState].value.selectedNode = node.id; - }); - - if (nodeType.actions) { - for (const action of nodeType.actions) { - processComputable(action, "visibility"); - setDefault(action, "visibility", Visibility.Visible); - processComputable(action, "icon"); - processComputable(action, "fillColor"); - processComputable(action, "tooltip"); - processComputable(action, "links"); - } - } - } - - board[GatherProps] = function (this: GenericBoard) { - const { - nodes, - types, - [PersistentState]: state, - visibility, - width, - height, - style, - classes, - links, - selectedAction, - selectedNode - } = this; - return { - nodes, - types, - [PersistentState]: state, - visibility, - width, - height, - style: unref(style), - classes, - links, - selectedAction, - selectedNode - }; - }; - - // This is necessary because board.types is different from T and Board - const processedBoard = board as unknown as Board; - return processedBoard; - }); + }) + ); } export function getNodeProperty(property: NodeComputable, node: BoardNode): T { diff --git a/src/features/buyable.tsx b/src/features/buyable.tsx index 48d786a..c831c28 100644 --- a/src/features/buyable.tsx +++ b/src/features/buyable.tsx @@ -1,6 +1,6 @@ import ClickableComponent from "features/clickables/Clickable.vue"; import { Resource } from "features/resources/resource"; -import { Persistent, makePersistent, PersistentState } from "game/persistence"; +import { Persistent, PersistentState, persistent } from "game/persistence"; import Decimal, { DecimalSource, format, formatWhole } from "util/bignum"; import { Computable, @@ -89,8 +89,10 @@ export type GenericBuyable = Replace< export function createBuyable( optionsFunc: () => T & ThisType> ): Buyable { - return createLazyProxy(() => { - const buyable: T & Partial = optionsFunc(); + return createLazyProxy(persistent => { + // Create temp literally just to avoid explicitly assigning types + const temp = Object.assign(persistent, optionsFunc()); + const buyable: Partial & typeof temp = temp; if (buyable.canPurchase == null && (buyable.resource == null || buyable.cost == null)) { console.warn( @@ -100,7 +102,6 @@ export function createBuyable( throw "Cannot create buyable without a canPurchase property or a resource and cost property"; } - makePersistent(buyable, 0); buyable.id = getUniqueID("buyable-"); buyable.type = BuyableType; buyable[Component] = ClickableComponent; @@ -239,5 +240,5 @@ export function createBuyable( }; return buyable as unknown as Buyable; - }); + }, persistent(0)); } diff --git a/src/features/challenges/challenge.tsx b/src/features/challenges/challenge.tsx index 9ec22e3..71231c8 100644 --- a/src/features/challenges/challenge.tsx +++ b/src/features/challenges/challenge.tsx @@ -15,7 +15,7 @@ import { import { GenericReset } from "features/reset"; import { Resource } from "features/resources/resource"; import { globalBus } from "game/events"; -import { persistent, PersistentRef } from "game/persistence"; +import { Persistent, persistent } from "game/persistence"; import settings, { registerSettingField } from "game/settings"; import Decimal, { DecimalSource } from "util/bignum"; import { @@ -58,10 +58,10 @@ export interface ChallengeOptions { export interface BaseChallenge { id: string; - completions: PersistentRef; + completions: Persistent; completed: Ref; maxed: Ref; - active: PersistentRef; + active: Persistent; toggle: VoidFunction; complete: (remainInChallenge?: boolean) => void; type: typeof ChallengeType; @@ -98,6 +98,8 @@ export type GenericChallenge = Replace< export function createChallenge( optionsFunc: () => T & ThisType> ): Challenge { + const completions = persistent(0); + const active = persistent(false); return createLazyProxy(() => { const challenge: T & Partial = optionsFunc(); @@ -116,8 +118,8 @@ export function createChallenge( challenge.type = ChallengeType; challenge[Component] = ChallengeComponent; - challenge.completions = persistent(0); - challenge.active = persistent(false); + challenge.completions = completions; + challenge.active = active; challenge.completed = computed(() => Decimal.gt((challenge as GenericChallenge).completions.value, 0) ); diff --git a/src/features/grids/grid.ts b/src/features/grids/grid.ts index 12b0c30..97033cb 100644 --- a/src/features/grids/grid.ts +++ b/src/features/grids/grid.ts @@ -19,7 +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"; +import { State, Persistent, PersistentState, persistent } from "game/persistence"; export const GridType = Symbol("Grid"); @@ -243,9 +243,10 @@ export type GenericGrid = Replace< export function createGrid( optionsFunc: () => T & ThisType> ): Grid { - return createLazyProxy(() => { - const grid: T & Partial = optionsFunc(); - makePersistent(grid, {}); + return createLazyProxy(persistent => { + // Create temp literally just to avoid explicitly assigning types + const temp = Object.assign(persistent, optionsFunc()); + const grid: Partial & typeof temp = temp; grid.id = getUniqueID("grid-"); grid[Component] = GridComponent; @@ -301,5 +302,5 @@ export function createGrid( }; return grid as unknown as Grid; - }); + }, persistent({})); } diff --git a/src/features/infoboxes/infobox.ts b/src/features/infoboxes/infobox.ts index bc8081b..55580de 100644 --- a/src/features/infoboxes/infobox.ts +++ b/src/features/infoboxes/infobox.ts @@ -18,7 +18,7 @@ import { } from "util/computed"; import { createLazyProxy } from "util/proxies"; import { Ref, unref } from "vue"; -import { Persistent, makePersistent, PersistentState } from "game/persistence"; +import { Persistent, PersistentState, persistent } from "game/persistence"; export const InfoboxType = Symbol("Infobox"); @@ -65,9 +65,10 @@ export type GenericInfobox = Replace< export function createInfobox( optionsFunc: () => T & ThisType> ): Infobox { - return createLazyProxy(() => { - const infobox: T & Partial = optionsFunc(); - makePersistent(infobox, false); + return createLazyProxy(persistent => { + // Create temp literally just to avoid explicitly assigning types + const temp = Object.assign(persistent, optionsFunc()); + const infobox: Partial & typeof temp = temp; infobox.id = getUniqueID("infobox-"); infobox.type = InfoboxType; infobox[Component] = InfoboxComponent; @@ -112,5 +113,5 @@ export function createInfobox( }; return infobox as unknown as Infobox; - }); + }, persistent(false)); } diff --git a/src/features/milestones/milestone.tsx b/src/features/milestones/milestone.tsx index d5bfcfd..28c32a0 100644 --- a/src/features/milestones/milestone.tsx +++ b/src/features/milestones/milestone.tsx @@ -13,7 +13,7 @@ import { import MilestoneComponent from "features/milestones/Milestone.vue"; import { globalBus } from "game/events"; import "game/notifications"; -import { makePersistent, Persistent, PersistentState } from "game/persistence"; +import { persistent, Persistent, PersistentState } from "game/persistence"; import settings, { registerSettingField } from "game/settings"; import { camelToTitle } from "util/common"; import { @@ -85,9 +85,10 @@ export type GenericMilestone = Replace< export function createMilestone( optionsFunc: () => T & ThisType> ): Milestone { - return createLazyProxy(() => { - const milestone: T & Partial = optionsFunc(); - makePersistent(milestone, false); + return createLazyProxy(persistent => { + // Create temp literally just to avoid explicitly assigning types + const temp = Object.assign(persistent, optionsFunc()); + const milestone: Partial & typeof temp = temp; milestone.id = getUniqueID("milestone-"); milestone.type = MilestoneType; milestone[Component] = MilestoneComponent; @@ -168,7 +169,7 @@ export function createMilestone( } return milestone as unknown as Milestone; - }); + }, persistent(false)); } declare module "game/settings" { diff --git a/src/features/tabs/tabFamily.ts b/src/features/tabs/tabFamily.ts index bfcfdfb..13ec76b 100644 --- a/src/features/tabs/tabFamily.ts +++ b/src/features/tabs/tabFamily.ts @@ -10,7 +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 { Persistent, PersistentState, persistent } from "game/persistence"; import { Computable, GetComputableType, @@ -60,13 +60,13 @@ export type GenericTabButton = Replace< export interface TabFamilyOptions { visibility?: Computable; - tabs: Record; classes?: Computable>; style?: Computable; } export interface BaseTabFamily extends Persistent { id: string; + tabs: Record; activeTab: Ref; selected: Ref; type: typeof TabFamilyType; @@ -90,21 +90,41 @@ export type GenericTabFamily = Replace< >; export function createTabFamily( + tabs: Record TabButtonOptions>, optionsFunc: () => T & ThisType> ): TabFamily { - return createLazyProxy(() => { - const tabFamily: T & Partial = optionsFunc(); + if (Object.keys(tabs).length === 0) { + console.warn("Cannot create tab family with 0 tabs"); + throw "Cannot create tab family with 0 tabs"; + } - if (Object.keys(tabFamily.tabs).length === 0) { - console.warn("Cannot create tab family with 0 tabs", tabFamily); - throw "Cannot create tab family with 0 tabs"; - } + return createLazyProxy(persistent => { + // Create temp literally just to avoid explicitly assigning types + const temp = Object.assign(persistent, optionsFunc()); + const tabFamily: Partial & typeof temp = temp; tabFamily.id = getUniqueID("tabFamily-"); tabFamily.type = TabFamilyType; tabFamily[Component] = TabFamilyComponent; - makePersistent(tabFamily, Object.keys(tabFamily.tabs)[0]); + tabFamily.tabs = Object.keys(tabs).reduce>( + (parsedTabs, tab) => { + const tabButton: TabButtonOptions & Partial = tabs[tab](); + tabButton.type = TabButtonType; + tabButton[Component] = TabButtonComponent; + + processComputable(tabButton as TabButtonOptions, "visibility"); + setDefault(tabButton, "visibility", Visibility.Visible); + processComputable(tabButton as TabButtonOptions, "tab"); + processComputable(tabButton as TabButtonOptions, "display"); + processComputable(tabButton as TabButtonOptions, "classes"); + processComputable(tabButton as TabButtonOptions, "style"); + processComputable(tabButton as TabButtonOptions, "glowColor"); + parsedTabs[tab] = tabButton as GenericTabButton; + return parsedTabs; + }, + {} + ); tabFamily.selected = tabFamily[PersistentState]; tabFamily.activeTab = computed(() => { const tabs = unref(processedTabFamily.tabs); @@ -129,20 +149,6 @@ export function createTabFamily( processComputable(tabFamily as T, "classes"); processComputable(tabFamily as T, "style"); - for (const tab in tabFamily.tabs) { - const tabButton: TabButtonOptions & Partial = tabFamily.tabs[tab]; - tabButton.type = TabButtonType; - tabButton[Component] = TabButtonComponent; - - processComputable(tabButton as TabButtonOptions, "visibility"); - setDefault(tabButton, "visibility", Visibility.Visible); - processComputable(tabButton as TabButtonOptions, "tab"); - processComputable(tabButton as TabButtonOptions, "display"); - processComputable(tabButton as TabButtonOptions, "classes"); - processComputable(tabButton as TabButtonOptions, "style"); - processComputable(tabButton as TabButtonOptions, "glowColor"); - } - tabFamily[GatherProps] = function (this: GenericTabFamily) { const { visibility, activeTab, selected, tabs, style, classes } = this; return { visibility, activeTab, selected, tabs, style: unref(style), classes }; @@ -151,5 +157,5 @@ export function createTabFamily( // This is necessary because board.types is different from T and TabFamily const processedTabFamily = tabFamily as unknown as TabFamily; return processedTabFamily; - }); + }, persistent(Object.keys(tabs)[0])); } diff --git a/src/features/trees/tree.ts b/src/features/trees/tree.ts index 04b337f..5d00863 100644 --- a/src/features/trees/tree.ts +++ b/src/features/trees/tree.ts @@ -13,7 +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 { deletePersistent, persistent } from "game/persistence"; import Decimal, { DecimalSource, format, formatWhole } from "util/bignum"; import { Computable, @@ -76,16 +76,18 @@ export type GenericTreeNode = Replace< export function createTreeNode( optionsFunc: () => T & ThisType> ): TreeNode { + const forceTooltip = persistent(false); return createLazyProxy(() => { const treeNode: T & Partial = optionsFunc(); treeNode.id = getUniqueID("treeNode-"); treeNode.type = TreeNodeType; if (treeNode.tooltip) { - treeNode.forceTooltip = persistent(false); + treeNode.forceTooltip = forceTooltip; } else { // If we don't have a tooltip, no point in making this persistent treeNode.forceTooltip = ref(false); + deletePersistent(forceTooltip); } processComputable(treeNode as T, "visibility"); diff --git a/src/features/upgrades/upgrade.ts b/src/features/upgrades/upgrade.ts index aa05c9a..6b47574 100644 --- a/src/features/upgrades/upgrade.ts +++ b/src/features/upgrades/upgrade.ts @@ -23,7 +23,7 @@ import { } from "util/computed"; import { createLazyProxy } from "util/proxies"; import { computed, Ref, unref } from "vue"; -import { Persistent, makePersistent, PersistentState } from "game/persistence"; +import { persistent, Persistent, PersistentState } from "game/persistence"; export const UpgradeType = Symbol("Upgrade"); @@ -80,9 +80,10 @@ export type GenericUpgrade = Replace< export function createUpgrade( optionsFunc: () => T & ThisType> ): Upgrade { - return createLazyProxy(() => { - const upgrade: T & Partial = optionsFunc(); - makePersistent(upgrade, false); + return createLazyProxy(persistent => { + // Create temp literally just to avoid explicitly assigning types + const temp = Object.assign(persistent, optionsFunc()); + const upgrade: Partial & typeof temp = temp; upgrade.id = getUniqueID("upgrade-"); upgrade.type = UpgradeType; upgrade[Component] = UpgradeComponent; @@ -167,7 +168,7 @@ export function createUpgrade( }; return upgrade as unknown as Upgrade; - }); + }, persistent(false)); } export function setupAutoPurchase( diff --git a/src/game/layers.tsx b/src/game/layers.tsx index ce99ecf..e059eec 100644 --- a/src/game/layers.tsx +++ b/src/game/layers.tsx @@ -18,7 +18,7 @@ import { createLazyProxy } from "util/proxies"; import { createNanoEvents, Emitter } from "nanoevents"; import { InjectionKey, Ref, ref, unref } from "vue"; import { globalBus } from "./events"; -import { persistent, PersistentRef } from "./persistence"; +import { Persistent, persistent } from "./persistence"; import player from "./player"; export interface FeatureNode { @@ -58,7 +58,6 @@ export interface Position { } export interface LayerOptions { - id: string; color?: Computable; display: Computable; classes?: Computable>; @@ -70,7 +69,8 @@ export interface LayerOptions { } export interface BaseLayer { - minimized: PersistentRef; + id: string; + minimized: Persistent; emitter: Emitter; on: OmitThisParameter["on"]>; emit: (event: K, ...args: Parameters) => void; @@ -84,7 +84,7 @@ export type Layer = Replace< display: GetComputableType; classes: GetComputableType; style: GetComputableType; - name: GetComputableTypeWithDefault; + name: GetComputableTypeWithDefault; minWidth: GetComputableTypeWithDefault; minimizable: GetComputableTypeWithDefault; forceHideGoBack: GetComputableType; @@ -100,7 +100,10 @@ export type GenericLayer = Replace< } >; +export const persistentRefs: Record> = {}; +export const addingLayers: string[] = []; export function createLayer( + id: string, optionsFunc: (() => T) & ThisType ): Layer { return createLazyProxy(() => { @@ -109,10 +112,19 @@ export function createLayer( layer.on = emitter.on.bind(emitter); layer.emit = emitter.emit.bind(emitter); layer.nodes = ref({}); + layer.id = id; + addingLayers.push(id); + persistentRefs[id] = new Set(); layer.minimized = persistent(false); - Object.assign(layer, optionsFunc.call(layer)); + if ( + addingLayers[addingLayers.length - 1] == null || + addingLayers[addingLayers.length - 1] !== id + ) { + throw `Adding layers stack in invalid state. This should not happen\nStack: ${addingLayers}\nTrying to pop ${layer.id}`; + } + addingLayers.pop(); processComputable(layer as T, "color"); processComputable(layer as T, "display"); diff --git a/src/game/persistence.ts b/src/game/persistence.ts index 6be595d..021298c 100644 --- a/src/game/persistence.ts +++ b/src/game/persistence.ts @@ -3,10 +3,12 @@ import Decimal, { DecimalSource } from "util/bignum"; import { ProxyState } from "util/proxies"; import { isArray } from "@vue/shared"; import { isReactive, isRef, Ref, ref } from "vue"; -import { GenericLayer } from "./layers"; +import { addingLayers, GenericLayer, persistentRefs } from "./layers"; export const PersistentState = Symbol("PersistentState"); export const DefaultValue = Symbol("DefaultValue"); +export const StackTrace = Symbol("StackTrace"); +export const Deleted = Symbol("Deleted"); // Note: This is a union of things that should be safely stringifiable without needing // special processes for knowing what to load them in as @@ -20,31 +22,52 @@ export type State = | { [key: string]: State } | { [key: number]: State }; -export type Persistent = { +export type Persistent = Ref & { [PersistentState]: Ref; [DefaultValue]: T; + [StackTrace]: string; + [Deleted]: boolean; }; -export type PersistentRef = Ref & Persistent; -export function persistent(defaultValue: T | Ref): PersistentRef { +function getStackTrace() { + return ( + new Error().stack + ?.split("\n") + .slice(3, 5) + .map(line => line.trim()) + .join("\n") || "" + ); +} + +export function persistent(defaultValue: T | Ref): Persistent { const persistent = ( isRef(defaultValue) ? defaultValue : (ref(defaultValue) as unknown) - ) as PersistentRef; + ) as Persistent; persistent[PersistentState] = persistent; persistent[DefaultValue] = isRef(defaultValue) ? defaultValue.value : defaultValue; - return persistent as PersistentRef; + persistent[StackTrace] = getStackTrace(); + persistent[Deleted] = false; + + 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); + } + + return persistent as Persistent; } -export function makePersistent( - obj: unknown, - defaultValue: T -): asserts obj is Persistent { - const persistent = obj as Partial>; - const state = ref(defaultValue) as Ref; - - persistent[PersistentState] = state; - persistent[DefaultValue] = isRef(defaultValue) ? (defaultValue.value as T) : defaultValue; +export function deletePersistent(persistent: Persistent) { + if (addingLayers.length === 0) { + console.warn("Deleting a persistent ref outside of a layer. Ignoring...", persistent); + } + persistentRefs[addingLayers[addingLayers.length - 1]].delete(persistent); + persistent[Deleted] = true; } globalBus.on("addLayer", (layer: GenericLayer, saveData: Record) => { @@ -56,6 +79,19 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record if (value && typeof value === "object") { if (PersistentState in value) { foundPersistent = true; + if ((value as Persistent)[Deleted]) { + console.warn( + "Deleted persistent ref present in returned object. Ignoring...", + value, + "\nCreated at:\n" + (value as Persistent)[StackTrace] + ); + return; + } + persistentRefs[layer.id].delete( + ProxyState in value + ? ((value as any)[ProxyState] as Persistent) + : (value as Persistent) + ); // Construct save path if it doesn't exist const persistentState = path.reduce>((acc, curr) => { @@ -122,4 +158,12 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record return foundPersistent; }; handleObject(layer); + 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(); }); diff --git a/src/util/proxies.ts b/src/util/proxies.ts index 575779b..4724259 100644 --- a/src/util/proxies.ts +++ b/src/util/proxies.ts @@ -17,15 +17,18 @@ export type ProxiedWithState = NonNullable extends Record(objectFunc: () => T): T { - const obj: T | Record = {}; +export function createLazyProxy( + objectFunc: (baseObject: S) => T & S, + baseObject: S = {} as S +): T { + const obj: S & Partial = baseObject; let calculated = false; function calculateObj(): T { if (!calculated) { - Object.assign(obj, objectFunc()); + Object.assign(obj, objectFunc(obj)); calculated = true; } - return obj as T; + return obj as S & T; } return new Proxy(obj, { @@ -51,10 +54,10 @@ export function createLazyProxy(objectFunc: () => T): T { }, getOwnPropertyDescriptor(target, key) { if (!calculated) { - Object.assign(obj, objectFunc()); + Object.assign(obj, objectFunc(obj)); calculated = true; } return Object.getOwnPropertyDescriptor(target, key); } - }) as T; + }) as S & T; }