From 431674eb1f5cea954e7b2864809aa126004a277c Mon Sep 17 00:00:00 2001 From: Patrick Canfield Date: Fri, 10 Nov 2023 13:16:35 -0800 Subject: [PATCH] simple save/load --- src/App.ts | 2 ++ src/Component.test.ts | 56 +++++++++++++++++++++++++++++++++ src/Component.ts | 35 +++++++++++++++++++++ src/Entity.ts | 8 +++++ src/Input.ts | 1 + src/Query.ts | 2 +- src/components/ActLike.ts | 11 +++++++ src/components/Layer.ts | 10 ++++++ src/components/LookLike.ts | 10 ++++++ src/components/PixiAppId.ts | 10 ++++++ src/components/PositionX.ts | 10 ++++++ src/components/PositionY.ts | 10 ++++++ src/components/ShouldSave.ts | 28 +++++++++++++++++ src/functions/loadComponents.ts | 19 +++++++++++ src/functions/saveComponents.ts | 25 +++++++++++++++ src/globals.ts | 12 +++++++ src/systems/EditorSystem.ts | 8 ++++- 17 files changed, 255 insertions(+), 2 deletions(-) create mode 100644 src/Component.test.ts create mode 100644 src/Component.ts create mode 100644 src/components/ShouldSave.ts create mode 100644 src/functions/loadComponents.ts create mode 100644 src/functions/saveComponents.ts diff --git a/src/App.ts b/src/App.ts index 42060112..795a9aca 100644 --- a/src/App.ts +++ b/src/App.ts @@ -32,6 +32,7 @@ import { initializePhysicsSystem, } from "./systems/PhysicsSystem"; import { setPixiAppId } from "./components/PixiAppId"; +import { loadComponents } from "./functions/loadComponents"; if (module.hot) { module.hot.accept((getParents) => { @@ -79,6 +80,7 @@ export function startApp() { addFrameRhythmCallback(() => { TaskSwitcherSystem(TASK_MAP, TASK_CLEANUP_MAP); }); + loadComponents(); } export function stopApp() { diff --git a/src/Component.test.ts b/src/Component.test.ts new file mode 100644 index 00000000..b92c62d8 --- /dev/null +++ b/src/Component.test.ts @@ -0,0 +1,56 @@ +import test, { Mock } from "node:test"; +import { localStorage } from "./globals"; +import { savePartialComponent, loadPartialComponent } from "./Component"; +import assert from "node:assert"; +import { executeFilterQuery, registerEntity } from "./Query"; +import { peekNextEntityId } from "./Entity"; + +function testSave( + key: string, + data: ReadonlyArray, + selectedEntities: ReadonlyArray, + expectedData: ReadonlyArray, +) { + savePartialComponent(key, data, selectedEntities); + const setItemMock = ( + localStorage.setItem as Mock + ).mock; + + const lastCall = setItemMock.calls[setItemMock.calls.length - 1]; + assert(lastCall.arguments[0] === key); + assert(lastCall.arguments[1] === JSON.stringify(expectedData)); +} + +test("savePartialComponent", () => { + testSave("Color", [8, 6, 7, 5, 3], [2, 3, 4], [7, 5, 3]); + testSave("Mass", [8, 6, 7, 5, 3], [2, 3, 4], [7, 5, 3]); +}); + +function testLoad( + key: string, + data: T[], + loadedData: ReadonlyArray, + nextEntityId: number, + expectedData: ReadonlyArray, + expectedQueryResult: ReadonlyArray, +) { + const getItemMock = ( + localStorage.getItem as Mock + ).mock; + getItemMock.mockImplementation(() => JSON.stringify(loadedData)); + loadPartialComponent(key, data, nextEntityId); + + assert.deepEqual(data, expectedData); + + for (let i = 0; i < data.length; i++) { + registerEntity(i); + } + const queryResult = executeFilterQuery((_id) => true, []); + assert.deepEqual(queryResult, expectedQueryResult); + assert.equal(peekNextEntityId(), expectedQueryResult.length); +} + +test("loadPartialComponent", () => { + testLoad("Color", [], [7, 5, 3], 3, [, , , 7, 5, 3], [0, 1, 2, 3, 4, 5]); + testLoad("Mass", [], [7, 5, 3], 3, [, , , 7, 5, 3], [0, 1, 2, 3, 4, 5]); +}); diff --git a/src/Component.ts b/src/Component.ts new file mode 100644 index 00000000..c3834356 --- /dev/null +++ b/src/Component.ts @@ -0,0 +1,35 @@ +import { peekNextEntityId, setNextEntityId } from "./Entity"; +import { registerEntity } from "./Query"; +import { localStorage } from "./globals"; + +export function savePartialComponent( + key: string, + data: ReadonlyArray, + selectedEntities: ReadonlyArray, +) { + const selectedData = selectedEntities.map((entity) => data[entity]); + const item = JSON.stringify(selectedData); + localStorage.setItem(key, item); +} + +export function loadPartialComponent( + key: string, + data: T[], + nextEntityId: number, +) { + const item = localStorage.getItem(key); + if (item) { + const selectedData = JSON.parse(item) as T[]; + for ( + let i = 0, entityId = nextEntityId; + i < selectedData.length; + i++, entityId++ + ) { + data[entityId] = selectedData[i]; + registerEntity(entityId); + } + if (peekNextEntityId() < nextEntityId + selectedData.length) { + setNextEntityId(nextEntityId + selectedData.length); + } + } +} diff --git a/src/Entity.ts b/src/Entity.ts index b9957d70..a3a46a90 100644 --- a/src/Entity.ts +++ b/src/Entity.ts @@ -10,6 +10,14 @@ export function addEntity(): number { return id; } +export function peekNextEntityId(): number { + return _nextId; +} + +export function setNextEntityId(id: number): void { + _nextId = id; +} + export enum EntityName { DEFAULT_PIXI_APP = "DEFAULT_PIXI_APP", FLOOR_IMAGE = "FLOOR_IMAGE", diff --git a/src/Input.ts b/src/Input.ts index 12a0c76d..6d973428 100644 --- a/src/Input.ts +++ b/src/Input.ts @@ -8,6 +8,7 @@ export const enum Key { l = "l", r = "r", w = "w", + W = "W", p = "p", c = "c", Space = " ", diff --git a/src/Query.ts b/src/Query.ts index 069bfaf9..243a5b98 100644 --- a/src/Query.ts +++ b/src/Query.ts @@ -7,7 +7,7 @@ export type ComplexFilter> = { const ALL_ENTITIES: Array = []; export function registerEntity(entityId: number): void { - ALL_ENTITIES.push(entityId); + ALL_ENTITIES[entityId] = entityId; } export function executeFilterQuery( diff --git a/src/components/ActLike.ts b/src/components/ActLike.ts index da2ddffe..ef071ce7 100644 --- a/src/components/ActLike.ts +++ b/src/components/ActLike.ts @@ -1,3 +1,5 @@ +import { loadPartialComponent, savePartialComponent } from "../Component"; + export enum ActLike { PLAYER, PUSHABLE, @@ -5,6 +7,7 @@ export enum ActLike { EDITOR_CURSOR, } +const STORAGE_KEY = "Component:ActLike"; const DATA: Array = []; export function setActLike(entityId: number, value: ActLike) { @@ -14,3 +17,11 @@ export function setActLike(entityId: number, value: ActLike) { export function isActLike(entityId: number, value: ActLike): boolean { return DATA[entityId] === value; } + +export function saveActLike(selectedEntities: ReadonlyArray) { + savePartialComponent(STORAGE_KEY, DATA, selectedEntities); +} + +export function loadActLike(nextEntityId: number) { + loadPartialComponent(STORAGE_KEY, DATA, nextEntityId); +} diff --git a/src/components/Layer.ts b/src/components/Layer.ts index 724f8aa6..c48e8067 100644 --- a/src/components/Layer.ts +++ b/src/components/Layer.ts @@ -1,3 +1,4 @@ +import { loadPartialComponent, savePartialComponent } from "../Component"; import { invariant } from "../Error"; import { setRenderStateDirty } from "../systems/RenderSystem"; @@ -7,6 +8,7 @@ export const enum Layer { USER_INTERFACE, } +const STORAGE_KEY = "Component:Layer"; const DATA: Array = []; export function setLayer(entityId: number, value: Layer) { @@ -30,3 +32,11 @@ export function removeLayer(entityId: number): void { setRenderStateDirty(); delete DATA[entityId]; } + +export function saveLayer(selectedEntities: ReadonlyArray) { + savePartialComponent(STORAGE_KEY, DATA, selectedEntities); +} + +export function loadLayer(nextEntityId: number) { + loadPartialComponent(STORAGE_KEY, DATA, nextEntityId); +} diff --git a/src/components/LookLike.ts b/src/components/LookLike.ts index bc0a1f28..238e85dc 100644 --- a/src/components/LookLike.ts +++ b/src/components/LookLike.ts @@ -1,7 +1,9 @@ +import { loadPartialComponent, savePartialComponent } from "../Component"; import { invariant } from "../Error"; import { setRenderStateDirty } from "../systems/RenderSystem"; import { hasImage } from "./Image"; +const STORAGE_KEY = "Component:LookLike"; const DATA: Array = []; export function setLookLike(entityId: number, imageId: number) { @@ -27,3 +29,11 @@ export function getLookLike(entityId: number): number { ); return DATA[entityId]; } + +export function saveLookLike(selectedEntities: ReadonlyArray) { + savePartialComponent(STORAGE_KEY, DATA, selectedEntities); +} + +export function loadLookLike(nextEntityId: number) { + loadPartialComponent(STORAGE_KEY, DATA, nextEntityId); +} diff --git a/src/components/PixiAppId.ts b/src/components/PixiAppId.ts index 649853b1..a03b69dd 100644 --- a/src/components/PixiAppId.ts +++ b/src/components/PixiAppId.ts @@ -1,7 +1,9 @@ +import { loadPartialComponent, savePartialComponent } from "../Component"; import { invariant } from "../Error"; import { setRenderStateDirty } from "../systems/RenderSystem"; import { hasPixiApp } from "./PixiApp"; +const STORAGE_KEY = "Component:PixiAppId"; const DATA: Array = []; export function setPixiAppId(entityId: number, appId: number) { @@ -23,3 +25,11 @@ export function getPixiAppId(entityId: number): number { ); return DATA[entityId]; } + +export function savePixiAppId(selectedEntities: ReadonlyArray) { + savePartialComponent(STORAGE_KEY, DATA, selectedEntities); +} + +export function loadPixiAppId(nextEntityId: number) { + loadPartialComponent(STORAGE_KEY, DATA, nextEntityId); +} diff --git a/src/components/PositionX.ts b/src/components/PositionX.ts index 1f291387..82606afb 100644 --- a/src/components/PositionX.ts +++ b/src/components/PositionX.ts @@ -1,6 +1,8 @@ +import { loadPartialComponent, savePartialComponent } from "../Component"; import { invariant } from "../Error"; import { setRenderStateDirty } from "../systems/RenderSystem"; +const STORAGE_KEY = "Component:PositionX"; const DATA: Array = []; export function setPositionX(entityId: number, value: number) { @@ -30,3 +32,11 @@ export function removePositionX(entityId: number): void { setRenderStateDirty(); delete DATA[entityId]; } + +export function savePositionX(selectedEntities: ReadonlyArray) { + savePartialComponent(STORAGE_KEY, DATA, selectedEntities); +} + +export function loadPositionX(nextEntityId: number) { + loadPartialComponent(STORAGE_KEY, DATA, nextEntityId); +} diff --git a/src/components/PositionY.ts b/src/components/PositionY.ts index efba9340..6928bb76 100644 --- a/src/components/PositionY.ts +++ b/src/components/PositionY.ts @@ -1,6 +1,8 @@ +import { loadPartialComponent, savePartialComponent } from "../Component"; import { invariant } from "../Error"; import { setRenderStateDirty } from "../systems/RenderSystem"; +const STORAGE_KEY = "Component:PositionY"; const DATA: Array = []; export function setPositionY(entityId: number, value: number) { @@ -30,3 +32,11 @@ export function removePositionY(entityId: number): void { setRenderStateDirty(); delete DATA[entityId]; } + +export function savePositionY(selectedEntities: ReadonlyArray) { + savePartialComponent(STORAGE_KEY, DATA, selectedEntities); +} + +export function loadPositionY(nextEntityId: number) { + loadPartialComponent(STORAGE_KEY, DATA, nextEntityId); +} diff --git a/src/components/ShouldSave.ts b/src/components/ShouldSave.ts new file mode 100644 index 00000000..973b2698 --- /dev/null +++ b/src/components/ShouldSave.ts @@ -0,0 +1,28 @@ +import { loadPartialComponent, savePartialComponent } from "../Component"; +import { invariant } from "../Error"; +const STORAGE_KEY = "Component:ShouldSave"; +const DATA: Array = []; + +export function setShouldSave(entityId: number, value: boolean) { + DATA[entityId] = value; +} + +export function hasShouldSave(entityId: number): boolean { + return DATA[entityId] !== undefined; +} + +export function getShouldSave(entityId: number): boolean { + invariant( + hasShouldSave(entityId), + `Entity ${entityId} does not have a ShouldSave`, + ); + return DATA[entityId]; +} + +export function saveShouldSave(selectedEntities: ReadonlyArray) { + savePartialComponent(STORAGE_KEY, DATA, selectedEntities); +} + +export function loadShouldSave(nextEntityId: number) { + loadPartialComponent(STORAGE_KEY, DATA, nextEntityId); +} diff --git a/src/functions/loadComponents.ts b/src/functions/loadComponents.ts new file mode 100644 index 00000000..86327791 --- /dev/null +++ b/src/functions/loadComponents.ts @@ -0,0 +1,19 @@ +import { peekNextEntityId } from "../Entity"; +import { loadActLike } from "../components/ActLike"; +import { loadLayer } from "../components/Layer"; +import { loadLookLike } from "../components/LookLike"; +import { loadPixiAppId } from "../components/PixiAppId"; +import { loadPositionX } from "../components/PositionX"; +import { loadPositionY } from "../components/PositionY"; +import { loadShouldSave } from "../components/ShouldSave"; + +export function loadComponents() { + const nextEntityId = peekNextEntityId(); + loadShouldSave(nextEntityId); + loadLayer(nextEntityId); + loadPositionX(nextEntityId); + loadPositionY(nextEntityId); + loadLookLike(nextEntityId); + loadActLike(nextEntityId); + loadPixiAppId(nextEntityId); +} diff --git a/src/functions/saveComponents.ts b/src/functions/saveComponents.ts new file mode 100644 index 00000000..ff732ec3 --- /dev/null +++ b/src/functions/saveComponents.ts @@ -0,0 +1,25 @@ +import { executeFilterQuery } from "../Query"; +import { saveActLike } from "../components/ActLike"; +import { saveLayer } from "../components/Layer"; +import { saveLookLike } from "../components/LookLike"; +import { savePixiAppId } from "../components/PixiAppId"; +import { savePositionX } from "../components/PositionX"; +import { savePositionY } from "../components/PositionY"; +import { hasShouldSave, saveShouldSave } from "../components/ShouldSave"; + +const entityIds: number[] = []; +function getSelectedEntities(): ReadonlyArray { + entityIds.length = 0; + return executeFilterQuery(hasShouldSave, entityIds); +} + +export function saveComponents() { + const selectedEntities = getSelectedEntities(); + saveShouldSave(selectedEntities); + saveLayer(selectedEntities); + savePositionX(selectedEntities); + savePositionY(selectedEntities); + saveLookLike(selectedEntities); + saveActLike(selectedEntities); + savePixiAppId(selectedEntities); +} diff --git a/src/globals.ts b/src/globals.ts index 13c6e8bd..3afeae20 100644 --- a/src/globals.ts +++ b/src/globals.ts @@ -33,3 +33,15 @@ export const clearInterval = process.env.NODE_ENV === "test" ? test.mock.fn(clearIntervalMock) : globalThis.clearInterval; + +export const localStorage: Storage = + process.env.NODE_ENV === "test" + ? { + getItem: test.mock.fn(), + setItem: test.mock.fn(), + removeItem: test.mock.fn(), + clear: test.mock.fn(), + key: test.mock.fn(), + length: 0, + } + : globalThis.localStorage; diff --git a/src/systems/EditorSystem.ts b/src/systems/EditorSystem.ts index add960e9..db15e575 100644 --- a/src/systems/EditorSystem.ts +++ b/src/systems/EditorSystem.ts @@ -4,6 +4,7 @@ import { KeyMap, getLastKeyDown, isAnyKeyDown, + isKeyDown, isKeyRepeating, } from "../Input"; import { executeFilterQuery } from "../Query"; @@ -11,13 +12,14 @@ import { ActLike, isActLike, setActLike } from "../components/ActLike"; import { setIsVisible } from "../components/IsVisible"; import { Layer, getLayer, hasLayer, setLayer } from "../components/Layer"; import { setLookLike } from "../components/LookLike"; -import { getPixiApp, setPixiApp } from "../components/PixiApp"; import { setPixiAppId } from "../components/PixiAppId"; import { hasPosition, isPosition, setPosition } from "../components/Position"; import { getPositionX } from "../components/PositionX"; import { getPositionY } from "../components/PositionY"; +import { setShouldSave } from "../components/ShouldSave"; import { SPRITE_SIZE } from "../components/Sprite"; import { getPlayerIfExists } from "../functions/Player"; +import { saveComponents } from "../functions/saveComponents"; import { throttle } from "../util"; if (module.hot) { @@ -71,6 +73,7 @@ function finishCreatingObject(cursorId: number, objectId: number) { setPosition(objectId, x, y); setLayer(objectId, Layer.OBJECT); setPixiAppId(objectId, getNamedEntity(EntityName.DEFAULT_PIXI_APP)); + setShouldSave(objectId, true); } const OBJECT_PREFAB_FACTORY_MAP: Record< @@ -202,6 +205,9 @@ export function EditorSystem() { if (lastKeyDown === Key.r) { enterReplaceMode(cursorId); } + if (isKeyDown(Key.W)) { + saveComponents(); + } break; case EditorMode.REPLACE: if (lastKeyDown === Key.Escape) {