@yagejs/save 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -213,7 +213,7 @@ var SaveService = class {
213
213
  );
214
214
  }
215
215
  const sceneManager = this.context.resolve(import_core.SceneManagerKey);
216
- sceneManager.clear();
216
+ await sceneManager.popAll();
217
217
  for (const entry of snapshot.scenes) {
218
218
  const SceneClass = import_core.SerializableRegistry.get(entry.type);
219
219
  if (!SceneClass) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/LocalStorageAdapter.ts","../src/SaveService.ts","../src/keys.ts","../src/SavePlugin.ts"],"sourcesContent":["export { VERSION } from \"@yagejs/core\";\n\n// Types\nexport type {\n SaveStorage,\n UntypedSlots,\n GameSnapshot,\n SceneSnapshotEntry,\n EntitySnapshotEntry,\n ComponentSnapshot,\n} from \"./types.js\";\nexport type { SnapshotResolver } from \"@yagejs/core\";\n\n// Storage\nexport { LocalStorageSaveStorage } from \"./LocalStorageAdapter.js\";\n\n// Service\nexport { SaveService } from \"./SaveService.js\";\nexport { SavePlugin } from \"./SavePlugin.js\";\nexport type { SavePluginOptions } from \"./SavePlugin.js\";\nexport { SaveServiceKey } from \"./keys.js\";\n","import type { SaveStorage } from \"./types.js\";\n\n/** SaveStorage backed by browser localStorage. */\nexport class LocalStorageSaveStorage implements SaveStorage {\n load(key: string): string | null {\n return localStorage.getItem(key);\n }\n\n save(key: string, data: string): void {\n localStorage.setItem(key, data);\n }\n\n delete(key: string): void {\n localStorage.removeItem(key);\n }\n\n list(prefix?: string): string[] {\n const result: string[] = [];\n for (let i = 0; i < localStorage.length; i++) {\n const key = localStorage.key(i);\n if (key !== null && (!prefix || key.startsWith(prefix))) {\n result.push(key);\n }\n }\n return result;\n }\n}\n","import type { Scene, Entity, Component, SnapshotResolver } from \"@yagejs/core\";\nimport {\n SceneManagerKey,\n SerializableRegistry,\n isSerializable,\n getSerializableType,\n} from \"@yagejs/core\";\nimport type { EngineContext } from \"@yagejs/core\";\nimport type {\n SaveStorage,\n UntypedSlots,\n GameSnapshot,\n SceneSnapshotEntry,\n EntitySnapshotEntry,\n ComponentSnapshot,\n} from \"./types.js\";\n\n/** Current snapshot format version. */\nconst SNAPSHOT_VERSION = 3;\n\n/**\n * Component restoration priority. Components listed here are added first\n * (in order) to satisfy onAdd() dependencies.\n */\nconst COMPONENT_ORDER = [\n \"Transform\",\n \"RigidBodyComponent\",\n \"ColliderComponent\",\n \"SpriteComponent\",\n \"GraphicsComponent\",\n \"AnimatedSpriteComponent\",\n \"AnimationController\",\n \"SoundComponent\",\n \"ParticleEmitterComponent\",\n \"TilemapComponent\",\n];\n\n/** Orchestrates full game-state serialization and hydration. */\nexport class SaveService<TSlots extends UntypedSlots = UntypedSlots> {\n private readonly storage: SaveStorage;\n private readonly context: EngineContext;\n private readonly namespace: string;\n private _loading = false;\n\n constructor(storage: SaveStorage, context: EngineContext, namespace = \"yage\") {\n this.storage = storage;\n this.context = context;\n this.namespace = namespace;\n }\n\n // ---- Snapshot API ----\n\n /** Save a snapshot of the current scene stack to the given slot. */\n saveSnapshot(slot: string): void {\n const snapshot = this.buildSnapshot();\n this.storage.save(\n this.key(\"snapshot\", slot),\n JSON.stringify(snapshot),\n );\n }\n\n /** Load a snapshot from the given slot, rebuilding the scene stack. */\n async loadSnapshot(slot: string): Promise<void> {\n const snapshot = this.readSnapshot(slot);\n if (!snapshot) {\n throw new Error(`No save found in slot \"${slot}\".`);\n }\n await this.hydrateSnapshot(snapshot);\n }\n\n /** Export a previously saved snapshot from the given slot. */\n exportSnapshot(slot: string): GameSnapshot | null {\n return this.readSnapshot(slot);\n }\n\n /** Import a snapshot into the given slot and hydrate the scene stack. */\n async importSnapshot(slot: string, snapshot: GameSnapshot): Promise<void> {\n if (this._loading) {\n throw new Error(\"loadSnapshot already in progress.\");\n }\n if (snapshot.version !== SNAPSHOT_VERSION) {\n throw new Error(\n `Save version mismatch: expected ${SNAPSHOT_VERSION}, got ${snapshot.version}.`,\n );\n }\n this.storage.save(\n this.key(\"snapshot\", slot),\n JSON.stringify(snapshot),\n );\n await this.hydrateSnapshot(snapshot);\n }\n\n // ---- User Data API ----\n\n /** Save arbitrary structured data to a named slot. */\n saveData<K extends keyof TSlots & string>(slot: K, data: TSlots[K]): void {\n this.storage.save(this.key(\"data\", slot), JSON.stringify(data));\n }\n\n /** Load structured data from a named slot. Returns null if not found. */\n loadData<K extends keyof TSlots & string>(slot: K): TSlots[K] | null {\n const raw = this.storage.load(this.key(\"data\", slot));\n if (raw === null) return null;\n try {\n return JSON.parse(raw) as TSlots[K];\n } catch {\n return null;\n }\n }\n\n /** Read data from a slot for external use (cloud upload, file export). Alias for `loadData`. */\n exportData<K extends keyof TSlots & string>(slot: K): TSlots[K] | null {\n return this.loadData(slot);\n }\n\n /** Write externally-sourced data into a slot. Alias for `saveData` — no version check or hydration. */\n importData<K extends keyof TSlots & string>(slot: K, data: TSlots[K]): void {\n this.saveData(slot, data);\n }\n\n // ---- Snapshot management ----\n\n /** Check if a snapshot exists in the given slot. */\n hasSnapshot(slot: string): boolean {\n return this.storage.load(this.key(\"snapshot\", slot)) !== null;\n }\n\n /** Delete a snapshot from the given slot. */\n deleteSnapshot(slot: string): void {\n this.storage.delete(this.key(\"snapshot\", slot));\n }\n\n // ---- Data management ----\n\n /** Check if user data exists in the given slot. */\n hasData<K extends keyof TSlots & string>(slot: K): boolean {\n return this.storage.load(this.key(\"data\", slot)) !== null;\n }\n\n /** Delete user data from the given slot. */\n deleteData<K extends keyof TSlots & string>(slot: K): void {\n this.storage.delete(this.key(\"data\", slot));\n }\n\n // ---- Private helpers ----\n\n private key(prefix: string, slot: string): string {\n return `${this.namespace}:${prefix}:${slot}`;\n }\n\n private readSnapshot(slot: string): GameSnapshot | null {\n const raw = this.storage.load(this.key(\"snapshot\", slot));\n if (raw === null) return null;\n try {\n return JSON.parse(raw) as GameSnapshot;\n } catch {\n return null;\n }\n }\n\n private buildSnapshot(): GameSnapshot {\n const sceneManager = this.context.resolve(SceneManagerKey);\n const scenes: SceneSnapshotEntry[] = [];\n\n for (const scene of sceneManager.all) {\n if (!isSerializable(scene)) continue;\n const type = getSerializableType(scene);\n if (!type) continue;\n\n const entities: EntitySnapshotEntry[] = [];\n for (const entity of scene.getEntities()) {\n if (!isSerializable(entity)) continue;\n entities.push(this.serializeEntity(entity));\n }\n\n const userData = scene.serialize?.();\n\n scenes.push({\n type,\n paused: scene.paused,\n entities,\n userData,\n });\n }\n\n return {\n version: SNAPSHOT_VERSION,\n timestamp: Date.now(),\n scenes,\n };\n }\n\n private async hydrateSnapshot(snapshot: GameSnapshot): Promise<void> {\n if (this._loading) {\n throw new Error(\"loadSnapshot already in progress.\");\n }\n this._loading = true;\n try {\n if (snapshot.version !== SNAPSHOT_VERSION) {\n throw new Error(\n `Save version mismatch: expected ${SNAPSHOT_VERSION}, got ${snapshot.version}.`,\n );\n }\n\n const sceneManager = this.context.resolve(SceneManagerKey);\n sceneManager.clear();\n\n for (const entry of snapshot.scenes) {\n const SceneClass = SerializableRegistry.get(entry.type) as\n | (new () => Scene)\n | undefined;\n if (!SceneClass) {\n throw new Error(\n `Cannot load scene type \"${entry.type}\". ` +\n `Ensure the scene class is decorated with @serializable.`,\n );\n }\n\n const scene = new SceneClass();\n\n // Instance-patch onEnter: restore entities + call afterRestore instead\n scene.onEnter = () => {\n this.restoreSceneEntities(scene, entry);\n };\n\n await sceneManager.push(scene);\n\n if (entry.paused) {\n scene.paused = true;\n }\n }\n } finally {\n this._loading = false;\n }\n }\n\n private restoreSceneEntities(\n scene: Scene,\n entry: SceneSnapshotEntry,\n ): void {\n // Phase 1: Create all entities, add to scene, add components\n const idMap = new Map<number, Entity>();\n const entityEntries: Array<{\n entity: Entity;\n entry: EntitySnapshotEntry;\n restoredComponents: Array<{ component: Component; data: unknown }>;\n }> = [];\n\n for (const entityEntry of entry.entities) {\n const EntityClass = SerializableRegistry.get(entityEntry.type) as\n | (new () => Entity)\n | undefined;\n if (!EntityClass) {\n console.warn(\n `Entity type \"${entityEntry.type}\" not found in registry — skipping.`,\n );\n continue;\n }\n\n const entity = new EntityClass();\n scene._addExistingEntity(entity);\n const restoredComponents = this.restoreEntityComponents(\n entity,\n entityEntry.components,\n );\n\n idMap.set(entityEntry.id, entity);\n entityEntries.push({ entity, entry: entityEntry, restoredComponents });\n }\n\n // Phase 2: Rewire parent/child relationships\n for (const { entity, entry: entityEntry } of entityEntries) {\n if (entityEntry.parentId != null && entityEntry.childName != null) {\n const parent = idMap.get(entityEntry.parentId);\n if (parent) {\n parent.addChild(entityEntry.childName, entity);\n } else {\n console.warn(\n `Parent entity (saved id ${entityEntry.parentId}) not found for child \"${entity.name}\" — restoring as root entity.`,\n );\n }\n }\n }\n\n // Build resolver for afterRestore hooks\n const resolver: SnapshotResolver = {\n entity(savedId: number) {\n return idMap.get(savedId) ?? null;\n },\n };\n\n // Phase 3: afterRestore hooks (components, then entities, then scene)\n for (const { entity, entry: entityEntry, restoredComponents } of entityEntries) {\n for (const { component, data } of restoredComponents) {\n component.afterRestore?.(data, resolver);\n }\n\n entity.afterRestore?.(entityEntry.userData, resolver);\n }\n\n scene.afterRestore?.(entry.userData, resolver);\n }\n\n private serializeEntity(entity: Entity): EntitySnapshotEntry {\n const type = getSerializableType(entity);\n if (!type) throw new Error(\"Entity is not serializable\");\n\n const components: ComponentSnapshot[] = [];\n for (const component of entity.getAll()) {\n if (typeof component.serialize !== \"function\") continue;\n const data = component.serialize();\n if (data == null) continue;\n const compType =\n getSerializableType(component) ?? component.constructor.name;\n components.push({ type: compType, data });\n }\n\n const userData = entity.serialize?.();\n\n const result: EntitySnapshotEntry = {\n id: entity.id,\n type,\n components,\n userData,\n };\n\n // Capture parent/child relationship\n if (entity.parent && isSerializable(entity.parent)) {\n result.parentId = entity.parent.id;\n // Find the name this entity is registered under\n for (const [name, child] of entity.parent.children) {\n if (child === entity) {\n result.childName = name;\n break;\n }\n }\n } else if (entity.parent) {\n console.warn(\n `Entity \"${entity.name}\" has non-serializable parent \"${entity.parent.name}\" — parent/child relationship will not be saved.`,\n );\n }\n\n return result;\n }\n\n private restoreEntityComponents(\n entity: Entity,\n snapshots: ComponentSnapshot[],\n ): Array<{ component: Component; data: unknown }> {\n const sorted = [...snapshots].sort((a, b) => {\n const ai = COMPONENT_ORDER.indexOf(a.type);\n const bi = COMPONENT_ORDER.indexOf(b.type);\n return (ai >= 0 ? ai : 999) - (bi >= 0 ? bi : 999);\n });\n\n const restored: Array<{ component: Component; data: unknown }> = [];\n\n for (const snap of sorted) {\n const CompClass = SerializableRegistry.get(snap.type) as\n | ({ fromSnapshot?(data: unknown): Component } & (new (\n ...args: unknown[]\n ) => Component))\n | undefined;\n\n if (!CompClass || typeof CompClass.fromSnapshot !== \"function\") {\n // Not in registry or no fromSnapshot — entity handles in afterRestore\n continue;\n }\n\n const component = CompClass.fromSnapshot(snap.data);\n entity.add(component);\n restored.push({ component, data: snap.data });\n }\n\n return restored;\n }\n}\n","import { ServiceKey } from \"@yagejs/core\";\nimport type { SaveService } from \"./SaveService.js\";\n\n/** Service key for the SaveService. */\nexport const SaveServiceKey = new ServiceKey<SaveService>(\"saveService\");\n","import type { EngineContext, Plugin } from \"@yagejs/core\";\nimport type { SaveStorage } from \"./types.js\";\nimport { LocalStorageSaveStorage } from \"./LocalStorageAdapter.js\";\nimport { SaveService } from \"./SaveService.js\";\nimport { SaveServiceKey } from \"./keys.js\";\n\n/** Options for the SavePlugin. */\nexport interface SavePluginOptions {\n /** Custom storage backend. Defaults to LocalStorageSaveStorage. */\n storage?: SaveStorage;\n /** Namespace for stored keys. Defaults to \"yage\". */\n namespace?: string;\n}\n\n/** Plugin that registers SaveService into the engine context. */\nexport class SavePlugin implements Plugin {\n readonly name = \"save\";\n readonly version = \"1.0.0\";\n\n private readonly options: SavePluginOptions;\n\n constructor(options?: SavePluginOptions) {\n this.options = options ?? {};\n }\n\n install(context: EngineContext): void {\n const storage = this.options.storage ?? new LocalStorageSaveStorage();\n const service = new SaveService(storage, context, this.options.namespace);\n\n context.register(SaveServiceKey, service);\n }\n}\n"],"mappings":"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","names":["import_core","import_core"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/LocalStorageAdapter.ts","../src/SaveService.ts","../src/keys.ts","../src/SavePlugin.ts"],"sourcesContent":["export { VERSION } from \"@yagejs/core\";\n\n// Types\nexport type {\n SaveStorage,\n UntypedSlots,\n GameSnapshot,\n SceneSnapshotEntry,\n EntitySnapshotEntry,\n ComponentSnapshot,\n} from \"./types.js\";\nexport type { SnapshotResolver } from \"@yagejs/core\";\n\n// Storage\nexport { LocalStorageSaveStorage } from \"./LocalStorageAdapter.js\";\n\n// Service\nexport { SaveService } from \"./SaveService.js\";\nexport { SavePlugin } from \"./SavePlugin.js\";\nexport type { SavePluginOptions } from \"./SavePlugin.js\";\nexport { SaveServiceKey } from \"./keys.js\";\n","import type { SaveStorage } from \"./types.js\";\n\n/** SaveStorage backed by browser localStorage. */\nexport class LocalStorageSaveStorage implements SaveStorage {\n load(key: string): string | null {\n return localStorage.getItem(key);\n }\n\n save(key: string, data: string): void {\n localStorage.setItem(key, data);\n }\n\n delete(key: string): void {\n localStorage.removeItem(key);\n }\n\n list(prefix?: string): string[] {\n const result: string[] = [];\n for (let i = 0; i < localStorage.length; i++) {\n const key = localStorage.key(i);\n if (key !== null && (!prefix || key.startsWith(prefix))) {\n result.push(key);\n }\n }\n return result;\n }\n}\n","import type { Scene, Entity, Component, SnapshotResolver } from \"@yagejs/core\";\nimport {\n SceneManagerKey,\n SerializableRegistry,\n isSerializable,\n getSerializableType,\n} from \"@yagejs/core\";\nimport type { EngineContext } from \"@yagejs/core\";\nimport type {\n SaveStorage,\n UntypedSlots,\n GameSnapshot,\n SceneSnapshotEntry,\n EntitySnapshotEntry,\n ComponentSnapshot,\n} from \"./types.js\";\n\n/** Current snapshot format version. */\nconst SNAPSHOT_VERSION = 3;\n\n/**\n * Component restoration priority. Components listed here are added first\n * (in order) to satisfy onAdd() dependencies.\n */\nconst COMPONENT_ORDER = [\n \"Transform\",\n \"RigidBodyComponent\",\n \"ColliderComponent\",\n \"SpriteComponent\",\n \"GraphicsComponent\",\n \"AnimatedSpriteComponent\",\n \"AnimationController\",\n \"SoundComponent\",\n \"ParticleEmitterComponent\",\n \"TilemapComponent\",\n];\n\n/** Orchestrates full game-state serialization and hydration. */\nexport class SaveService<TSlots extends UntypedSlots = UntypedSlots> {\n private readonly storage: SaveStorage;\n private readonly context: EngineContext;\n private readonly namespace: string;\n private _loading = false;\n\n constructor(storage: SaveStorage, context: EngineContext, namespace = \"yage\") {\n this.storage = storage;\n this.context = context;\n this.namespace = namespace;\n }\n\n // ---- Snapshot API ----\n\n /** Save a snapshot of the current scene stack to the given slot. */\n saveSnapshot(slot: string): void {\n const snapshot = this.buildSnapshot();\n this.storage.save(\n this.key(\"snapshot\", slot),\n JSON.stringify(snapshot),\n );\n }\n\n /** Load a snapshot from the given slot, rebuilding the scene stack. */\n async loadSnapshot(slot: string): Promise<void> {\n const snapshot = this.readSnapshot(slot);\n if (!snapshot) {\n throw new Error(`No save found in slot \"${slot}\".`);\n }\n await this.hydrateSnapshot(snapshot);\n }\n\n /** Export a previously saved snapshot from the given slot. */\n exportSnapshot(slot: string): GameSnapshot | null {\n return this.readSnapshot(slot);\n }\n\n /** Import a snapshot into the given slot and hydrate the scene stack. */\n async importSnapshot(slot: string, snapshot: GameSnapshot): Promise<void> {\n if (this._loading) {\n throw new Error(\"loadSnapshot already in progress.\");\n }\n if (snapshot.version !== SNAPSHOT_VERSION) {\n throw new Error(\n `Save version mismatch: expected ${SNAPSHOT_VERSION}, got ${snapshot.version}.`,\n );\n }\n this.storage.save(\n this.key(\"snapshot\", slot),\n JSON.stringify(snapshot),\n );\n await this.hydrateSnapshot(snapshot);\n }\n\n // ---- User Data API ----\n\n /** Save arbitrary structured data to a named slot. */\n saveData<K extends keyof TSlots & string>(slot: K, data: TSlots[K]): void {\n this.storage.save(this.key(\"data\", slot), JSON.stringify(data));\n }\n\n /** Load structured data from a named slot. Returns null if not found. */\n loadData<K extends keyof TSlots & string>(slot: K): TSlots[K] | null {\n const raw = this.storage.load(this.key(\"data\", slot));\n if (raw === null) return null;\n try {\n return JSON.parse(raw) as TSlots[K];\n } catch {\n return null;\n }\n }\n\n /** Read data from a slot for external use (cloud upload, file export). Alias for `loadData`. */\n exportData<K extends keyof TSlots & string>(slot: K): TSlots[K] | null {\n return this.loadData(slot);\n }\n\n /** Write externally-sourced data into a slot. Alias for `saveData` — no version check or hydration. */\n importData<K extends keyof TSlots & string>(slot: K, data: TSlots[K]): void {\n this.saveData(slot, data);\n }\n\n // ---- Snapshot management ----\n\n /** Check if a snapshot exists in the given slot. */\n hasSnapshot(slot: string): boolean {\n return this.storage.load(this.key(\"snapshot\", slot)) !== null;\n }\n\n /** Delete a snapshot from the given slot. */\n deleteSnapshot(slot: string): void {\n this.storage.delete(this.key(\"snapshot\", slot));\n }\n\n // ---- Data management ----\n\n /** Check if user data exists in the given slot. */\n hasData<K extends keyof TSlots & string>(slot: K): boolean {\n return this.storage.load(this.key(\"data\", slot)) !== null;\n }\n\n /** Delete user data from the given slot. */\n deleteData<K extends keyof TSlots & string>(slot: K): void {\n this.storage.delete(this.key(\"data\", slot));\n }\n\n // ---- Private helpers ----\n\n private key(prefix: string, slot: string): string {\n return `${this.namespace}:${prefix}:${slot}`;\n }\n\n private readSnapshot(slot: string): GameSnapshot | null {\n const raw = this.storage.load(this.key(\"snapshot\", slot));\n if (raw === null) return null;\n try {\n return JSON.parse(raw) as GameSnapshot;\n } catch {\n return null;\n }\n }\n\n private buildSnapshot(): GameSnapshot {\n const sceneManager = this.context.resolve(SceneManagerKey);\n const scenes: SceneSnapshotEntry[] = [];\n\n for (const scene of sceneManager.all) {\n if (!isSerializable(scene)) continue;\n const type = getSerializableType(scene);\n if (!type) continue;\n\n const entities: EntitySnapshotEntry[] = [];\n for (const entity of scene.getEntities()) {\n if (!isSerializable(entity)) continue;\n entities.push(this.serializeEntity(entity));\n }\n\n const userData = scene.serialize?.();\n\n scenes.push({\n type,\n paused: scene.paused,\n entities,\n userData,\n });\n }\n\n return {\n version: SNAPSHOT_VERSION,\n timestamp: Date.now(),\n scenes,\n };\n }\n\n private async hydrateSnapshot(snapshot: GameSnapshot): Promise<void> {\n if (this._loading) {\n throw new Error(\"loadSnapshot already in progress.\");\n }\n this._loading = true;\n try {\n if (snapshot.version !== SNAPSHOT_VERSION) {\n throw new Error(\n `Save version mismatch: expected ${SNAPSHOT_VERSION}, got ${snapshot.version}.`,\n );\n }\n\n const sceneManager = this.context.resolve(SceneManagerKey);\n await sceneManager.popAll();\n\n for (const entry of snapshot.scenes) {\n const SceneClass = SerializableRegistry.get(entry.type) as\n | (new () => Scene)\n | undefined;\n if (!SceneClass) {\n throw new Error(\n `Cannot load scene type \"${entry.type}\". ` +\n `Ensure the scene class is decorated with @serializable.`,\n );\n }\n\n const scene = new SceneClass();\n\n // Instance-patch onEnter: restore entities + call afterRestore instead\n scene.onEnter = () => {\n this.restoreSceneEntities(scene, entry);\n };\n\n await sceneManager.push(scene);\n\n if (entry.paused) {\n scene.paused = true;\n }\n }\n } finally {\n this._loading = false;\n }\n }\n\n private restoreSceneEntities(\n scene: Scene,\n entry: SceneSnapshotEntry,\n ): void {\n // Phase 1: Create all entities, add to scene, add components\n const idMap = new Map<number, Entity>();\n const entityEntries: Array<{\n entity: Entity;\n entry: EntitySnapshotEntry;\n restoredComponents: Array<{ component: Component; data: unknown }>;\n }> = [];\n\n for (const entityEntry of entry.entities) {\n const EntityClass = SerializableRegistry.get(entityEntry.type) as\n | (new () => Entity)\n | undefined;\n if (!EntityClass) {\n console.warn(\n `Entity type \"${entityEntry.type}\" not found in registry — skipping.`,\n );\n continue;\n }\n\n const entity = new EntityClass();\n scene._addExistingEntity(entity);\n const restoredComponents = this.restoreEntityComponents(\n entity,\n entityEntry.components,\n );\n\n idMap.set(entityEntry.id, entity);\n entityEntries.push({ entity, entry: entityEntry, restoredComponents });\n }\n\n // Phase 2: Rewire parent/child relationships\n for (const { entity, entry: entityEntry } of entityEntries) {\n if (entityEntry.parentId != null && entityEntry.childName != null) {\n const parent = idMap.get(entityEntry.parentId);\n if (parent) {\n parent.addChild(entityEntry.childName, entity);\n } else {\n console.warn(\n `Parent entity (saved id ${entityEntry.parentId}) not found for child \"${entity.name}\" — restoring as root entity.`,\n );\n }\n }\n }\n\n // Build resolver for afterRestore hooks\n const resolver: SnapshotResolver = {\n entity(savedId: number) {\n return idMap.get(savedId) ?? null;\n },\n };\n\n // Phase 3: afterRestore hooks (components, then entities, then scene)\n for (const { entity, entry: entityEntry, restoredComponents } of entityEntries) {\n for (const { component, data } of restoredComponents) {\n component.afterRestore?.(data, resolver);\n }\n\n entity.afterRestore?.(entityEntry.userData, resolver);\n }\n\n scene.afterRestore?.(entry.userData, resolver);\n }\n\n private serializeEntity(entity: Entity): EntitySnapshotEntry {\n const type = getSerializableType(entity);\n if (!type) throw new Error(\"Entity is not serializable\");\n\n const components: ComponentSnapshot[] = [];\n for (const component of entity.getAll()) {\n if (typeof component.serialize !== \"function\") continue;\n const data = component.serialize();\n if (data == null) continue;\n const compType =\n getSerializableType(component) ?? component.constructor.name;\n components.push({ type: compType, data });\n }\n\n const userData = entity.serialize?.();\n\n const result: EntitySnapshotEntry = {\n id: entity.id,\n type,\n components,\n userData,\n };\n\n // Capture parent/child relationship\n if (entity.parent && isSerializable(entity.parent)) {\n result.parentId = entity.parent.id;\n // Find the name this entity is registered under\n for (const [name, child] of entity.parent.children) {\n if (child === entity) {\n result.childName = name;\n break;\n }\n }\n } else if (entity.parent) {\n console.warn(\n `Entity \"${entity.name}\" has non-serializable parent \"${entity.parent.name}\" — parent/child relationship will not be saved.`,\n );\n }\n\n return result;\n }\n\n private restoreEntityComponents(\n entity: Entity,\n snapshots: ComponentSnapshot[],\n ): Array<{ component: Component; data: unknown }> {\n const sorted = [...snapshots].sort((a, b) => {\n const ai = COMPONENT_ORDER.indexOf(a.type);\n const bi = COMPONENT_ORDER.indexOf(b.type);\n return (ai >= 0 ? ai : 999) - (bi >= 0 ? bi : 999);\n });\n\n const restored: Array<{ component: Component; data: unknown }> = [];\n\n for (const snap of sorted) {\n const CompClass = SerializableRegistry.get(snap.type) as\n | ({ fromSnapshot?(data: unknown): Component } & (new (\n ...args: unknown[]\n ) => Component))\n | undefined;\n\n if (!CompClass || typeof CompClass.fromSnapshot !== \"function\") {\n // Not in registry or no fromSnapshot — entity handles in afterRestore\n continue;\n }\n\n const component = CompClass.fromSnapshot(snap.data);\n entity.add(component);\n restored.push({ component, data: snap.data });\n }\n\n return restored;\n }\n}\n","import { ServiceKey } from \"@yagejs/core\";\nimport type { SaveService } from \"./SaveService.js\";\n\n/** Service key for the SaveService. */\nexport const SaveServiceKey = new ServiceKey<SaveService>(\"saveService\");\n","import type { EngineContext, Plugin } from \"@yagejs/core\";\nimport type { SaveStorage } from \"./types.js\";\nimport { LocalStorageSaveStorage } from \"./LocalStorageAdapter.js\";\nimport { SaveService } from \"./SaveService.js\";\nimport { SaveServiceKey } from \"./keys.js\";\n\n/** Options for the SavePlugin. */\nexport interface SavePluginOptions {\n /** Custom storage backend. Defaults to LocalStorageSaveStorage. */\n storage?: SaveStorage;\n /** Namespace for stored keys. Defaults to \"yage\". */\n namespace?: string;\n}\n\n/** Plugin that registers SaveService into the engine context. */\nexport class SavePlugin implements Plugin {\n readonly name = \"save\";\n readonly version = \"1.0.0\";\n\n private readonly options: SavePluginOptions;\n\n constructor(options?: SavePluginOptions) {\n this.options = options ?? {};\n }\n\n install(context: EngineContext): void {\n const storage = this.options.storage ?? new LocalStorageSaveStorage();\n const service = new SaveService(storage, context, this.options.namespace);\n\n context.register(SaveServiceKey, service);\n }\n}\n"],"mappings":"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","names":["import_core","import_core"]}
package/dist/index.js CHANGED
@@ -192,7 +192,7 @@ var SaveService = class {
192
192
  );
193
193
  }
194
194
  const sceneManager = this.context.resolve(SceneManagerKey);
195
- sceneManager.clear();
195
+ await sceneManager.popAll();
196
196
  for (const entry of snapshot.scenes) {
197
197
  const SceneClass = SerializableRegistry.get(entry.type);
198
198
  if (!SceneClass) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/LocalStorageAdapter.ts","../src/SaveService.ts","../src/keys.ts","../src/SavePlugin.ts"],"sourcesContent":["export { VERSION } from \"@yagejs/core\";\n\n// Types\nexport type {\n SaveStorage,\n UntypedSlots,\n GameSnapshot,\n SceneSnapshotEntry,\n EntitySnapshotEntry,\n ComponentSnapshot,\n} from \"./types.js\";\nexport type { SnapshotResolver } from \"@yagejs/core\";\n\n// Storage\nexport { LocalStorageSaveStorage } from \"./LocalStorageAdapter.js\";\n\n// Service\nexport { SaveService } from \"./SaveService.js\";\nexport { SavePlugin } from \"./SavePlugin.js\";\nexport type { SavePluginOptions } from \"./SavePlugin.js\";\nexport { SaveServiceKey } from \"./keys.js\";\n","import type { SaveStorage } from \"./types.js\";\n\n/** SaveStorage backed by browser localStorage. */\nexport class LocalStorageSaveStorage implements SaveStorage {\n load(key: string): string | null {\n return localStorage.getItem(key);\n }\n\n save(key: string, data: string): void {\n localStorage.setItem(key, data);\n }\n\n delete(key: string): void {\n localStorage.removeItem(key);\n }\n\n list(prefix?: string): string[] {\n const result: string[] = [];\n for (let i = 0; i < localStorage.length; i++) {\n const key = localStorage.key(i);\n if (key !== null && (!prefix || key.startsWith(prefix))) {\n result.push(key);\n }\n }\n return result;\n }\n}\n","import type { Scene, Entity, Component, SnapshotResolver } from \"@yagejs/core\";\nimport {\n SceneManagerKey,\n SerializableRegistry,\n isSerializable,\n getSerializableType,\n} from \"@yagejs/core\";\nimport type { EngineContext } from \"@yagejs/core\";\nimport type {\n SaveStorage,\n UntypedSlots,\n GameSnapshot,\n SceneSnapshotEntry,\n EntitySnapshotEntry,\n ComponentSnapshot,\n} from \"./types.js\";\n\n/** Current snapshot format version. */\nconst SNAPSHOT_VERSION = 3;\n\n/**\n * Component restoration priority. Components listed here are added first\n * (in order) to satisfy onAdd() dependencies.\n */\nconst COMPONENT_ORDER = [\n \"Transform\",\n \"RigidBodyComponent\",\n \"ColliderComponent\",\n \"SpriteComponent\",\n \"GraphicsComponent\",\n \"AnimatedSpriteComponent\",\n \"AnimationController\",\n \"SoundComponent\",\n \"ParticleEmitterComponent\",\n \"TilemapComponent\",\n];\n\n/** Orchestrates full game-state serialization and hydration. */\nexport class SaveService<TSlots extends UntypedSlots = UntypedSlots> {\n private readonly storage: SaveStorage;\n private readonly context: EngineContext;\n private readonly namespace: string;\n private _loading = false;\n\n constructor(storage: SaveStorage, context: EngineContext, namespace = \"yage\") {\n this.storage = storage;\n this.context = context;\n this.namespace = namespace;\n }\n\n // ---- Snapshot API ----\n\n /** Save a snapshot of the current scene stack to the given slot. */\n saveSnapshot(slot: string): void {\n const snapshot = this.buildSnapshot();\n this.storage.save(\n this.key(\"snapshot\", slot),\n JSON.stringify(snapshot),\n );\n }\n\n /** Load a snapshot from the given slot, rebuilding the scene stack. */\n async loadSnapshot(slot: string): Promise<void> {\n const snapshot = this.readSnapshot(slot);\n if (!snapshot) {\n throw new Error(`No save found in slot \"${slot}\".`);\n }\n await this.hydrateSnapshot(snapshot);\n }\n\n /** Export a previously saved snapshot from the given slot. */\n exportSnapshot(slot: string): GameSnapshot | null {\n return this.readSnapshot(slot);\n }\n\n /** Import a snapshot into the given slot and hydrate the scene stack. */\n async importSnapshot(slot: string, snapshot: GameSnapshot): Promise<void> {\n if (this._loading) {\n throw new Error(\"loadSnapshot already in progress.\");\n }\n if (snapshot.version !== SNAPSHOT_VERSION) {\n throw new Error(\n `Save version mismatch: expected ${SNAPSHOT_VERSION}, got ${snapshot.version}.`,\n );\n }\n this.storage.save(\n this.key(\"snapshot\", slot),\n JSON.stringify(snapshot),\n );\n await this.hydrateSnapshot(snapshot);\n }\n\n // ---- User Data API ----\n\n /** Save arbitrary structured data to a named slot. */\n saveData<K extends keyof TSlots & string>(slot: K, data: TSlots[K]): void {\n this.storage.save(this.key(\"data\", slot), JSON.stringify(data));\n }\n\n /** Load structured data from a named slot. Returns null if not found. */\n loadData<K extends keyof TSlots & string>(slot: K): TSlots[K] | null {\n const raw = this.storage.load(this.key(\"data\", slot));\n if (raw === null) return null;\n try {\n return JSON.parse(raw) as TSlots[K];\n } catch {\n return null;\n }\n }\n\n /** Read data from a slot for external use (cloud upload, file export). Alias for `loadData`. */\n exportData<K extends keyof TSlots & string>(slot: K): TSlots[K] | null {\n return this.loadData(slot);\n }\n\n /** Write externally-sourced data into a slot. Alias for `saveData` — no version check or hydration. */\n importData<K extends keyof TSlots & string>(slot: K, data: TSlots[K]): void {\n this.saveData(slot, data);\n }\n\n // ---- Snapshot management ----\n\n /** Check if a snapshot exists in the given slot. */\n hasSnapshot(slot: string): boolean {\n return this.storage.load(this.key(\"snapshot\", slot)) !== null;\n }\n\n /** Delete a snapshot from the given slot. */\n deleteSnapshot(slot: string): void {\n this.storage.delete(this.key(\"snapshot\", slot));\n }\n\n // ---- Data management ----\n\n /** Check if user data exists in the given slot. */\n hasData<K extends keyof TSlots & string>(slot: K): boolean {\n return this.storage.load(this.key(\"data\", slot)) !== null;\n }\n\n /** Delete user data from the given slot. */\n deleteData<K extends keyof TSlots & string>(slot: K): void {\n this.storage.delete(this.key(\"data\", slot));\n }\n\n // ---- Private helpers ----\n\n private key(prefix: string, slot: string): string {\n return `${this.namespace}:${prefix}:${slot}`;\n }\n\n private readSnapshot(slot: string): GameSnapshot | null {\n const raw = this.storage.load(this.key(\"snapshot\", slot));\n if (raw === null) return null;\n try {\n return JSON.parse(raw) as GameSnapshot;\n } catch {\n return null;\n }\n }\n\n private buildSnapshot(): GameSnapshot {\n const sceneManager = this.context.resolve(SceneManagerKey);\n const scenes: SceneSnapshotEntry[] = [];\n\n for (const scene of sceneManager.all) {\n if (!isSerializable(scene)) continue;\n const type = getSerializableType(scene);\n if (!type) continue;\n\n const entities: EntitySnapshotEntry[] = [];\n for (const entity of scene.getEntities()) {\n if (!isSerializable(entity)) continue;\n entities.push(this.serializeEntity(entity));\n }\n\n const userData = scene.serialize?.();\n\n scenes.push({\n type,\n paused: scene.paused,\n entities,\n userData,\n });\n }\n\n return {\n version: SNAPSHOT_VERSION,\n timestamp: Date.now(),\n scenes,\n };\n }\n\n private async hydrateSnapshot(snapshot: GameSnapshot): Promise<void> {\n if (this._loading) {\n throw new Error(\"loadSnapshot already in progress.\");\n }\n this._loading = true;\n try {\n if (snapshot.version !== SNAPSHOT_VERSION) {\n throw new Error(\n `Save version mismatch: expected ${SNAPSHOT_VERSION}, got ${snapshot.version}.`,\n );\n }\n\n const sceneManager = this.context.resolve(SceneManagerKey);\n sceneManager.clear();\n\n for (const entry of snapshot.scenes) {\n const SceneClass = SerializableRegistry.get(entry.type) as\n | (new () => Scene)\n | undefined;\n if (!SceneClass) {\n throw new Error(\n `Cannot load scene type \"${entry.type}\". ` +\n `Ensure the scene class is decorated with @serializable.`,\n );\n }\n\n const scene = new SceneClass();\n\n // Instance-patch onEnter: restore entities + call afterRestore instead\n scene.onEnter = () => {\n this.restoreSceneEntities(scene, entry);\n };\n\n await sceneManager.push(scene);\n\n if (entry.paused) {\n scene.paused = true;\n }\n }\n } finally {\n this._loading = false;\n }\n }\n\n private restoreSceneEntities(\n scene: Scene,\n entry: SceneSnapshotEntry,\n ): void {\n // Phase 1: Create all entities, add to scene, add components\n const idMap = new Map<number, Entity>();\n const entityEntries: Array<{\n entity: Entity;\n entry: EntitySnapshotEntry;\n restoredComponents: Array<{ component: Component; data: unknown }>;\n }> = [];\n\n for (const entityEntry of entry.entities) {\n const EntityClass = SerializableRegistry.get(entityEntry.type) as\n | (new () => Entity)\n | undefined;\n if (!EntityClass) {\n console.warn(\n `Entity type \"${entityEntry.type}\" not found in registry — skipping.`,\n );\n continue;\n }\n\n const entity = new EntityClass();\n scene._addExistingEntity(entity);\n const restoredComponents = this.restoreEntityComponents(\n entity,\n entityEntry.components,\n );\n\n idMap.set(entityEntry.id, entity);\n entityEntries.push({ entity, entry: entityEntry, restoredComponents });\n }\n\n // Phase 2: Rewire parent/child relationships\n for (const { entity, entry: entityEntry } of entityEntries) {\n if (entityEntry.parentId != null && entityEntry.childName != null) {\n const parent = idMap.get(entityEntry.parentId);\n if (parent) {\n parent.addChild(entityEntry.childName, entity);\n } else {\n console.warn(\n `Parent entity (saved id ${entityEntry.parentId}) not found for child \"${entity.name}\" — restoring as root entity.`,\n );\n }\n }\n }\n\n // Build resolver for afterRestore hooks\n const resolver: SnapshotResolver = {\n entity(savedId: number) {\n return idMap.get(savedId) ?? null;\n },\n };\n\n // Phase 3: afterRestore hooks (components, then entities, then scene)\n for (const { entity, entry: entityEntry, restoredComponents } of entityEntries) {\n for (const { component, data } of restoredComponents) {\n component.afterRestore?.(data, resolver);\n }\n\n entity.afterRestore?.(entityEntry.userData, resolver);\n }\n\n scene.afterRestore?.(entry.userData, resolver);\n }\n\n private serializeEntity(entity: Entity): EntitySnapshotEntry {\n const type = getSerializableType(entity);\n if (!type) throw new Error(\"Entity is not serializable\");\n\n const components: ComponentSnapshot[] = [];\n for (const component of entity.getAll()) {\n if (typeof component.serialize !== \"function\") continue;\n const data = component.serialize();\n if (data == null) continue;\n const compType =\n getSerializableType(component) ?? component.constructor.name;\n components.push({ type: compType, data });\n }\n\n const userData = entity.serialize?.();\n\n const result: EntitySnapshotEntry = {\n id: entity.id,\n type,\n components,\n userData,\n };\n\n // Capture parent/child relationship\n if (entity.parent && isSerializable(entity.parent)) {\n result.parentId = entity.parent.id;\n // Find the name this entity is registered under\n for (const [name, child] of entity.parent.children) {\n if (child === entity) {\n result.childName = name;\n break;\n }\n }\n } else if (entity.parent) {\n console.warn(\n `Entity \"${entity.name}\" has non-serializable parent \"${entity.parent.name}\" — parent/child relationship will not be saved.`,\n );\n }\n\n return result;\n }\n\n private restoreEntityComponents(\n entity: Entity,\n snapshots: ComponentSnapshot[],\n ): Array<{ component: Component; data: unknown }> {\n const sorted = [...snapshots].sort((a, b) => {\n const ai = COMPONENT_ORDER.indexOf(a.type);\n const bi = COMPONENT_ORDER.indexOf(b.type);\n return (ai >= 0 ? ai : 999) - (bi >= 0 ? bi : 999);\n });\n\n const restored: Array<{ component: Component; data: unknown }> = [];\n\n for (const snap of sorted) {\n const CompClass = SerializableRegistry.get(snap.type) as\n | ({ fromSnapshot?(data: unknown): Component } & (new (\n ...args: unknown[]\n ) => Component))\n | undefined;\n\n if (!CompClass || typeof CompClass.fromSnapshot !== \"function\") {\n // Not in registry or no fromSnapshot — entity handles in afterRestore\n continue;\n }\n\n const component = CompClass.fromSnapshot(snap.data);\n entity.add(component);\n restored.push({ component, data: snap.data });\n }\n\n return restored;\n }\n}\n","import { ServiceKey } from \"@yagejs/core\";\nimport type { SaveService } from \"./SaveService.js\";\n\n/** Service key for the SaveService. */\nexport const SaveServiceKey = new ServiceKey<SaveService>(\"saveService\");\n","import type { EngineContext, Plugin } from \"@yagejs/core\";\nimport type { SaveStorage } from \"./types.js\";\nimport { LocalStorageSaveStorage } from \"./LocalStorageAdapter.js\";\nimport { SaveService } from \"./SaveService.js\";\nimport { SaveServiceKey } from \"./keys.js\";\n\n/** Options for the SavePlugin. */\nexport interface SavePluginOptions {\n /** Custom storage backend. Defaults to LocalStorageSaveStorage. */\n storage?: SaveStorage;\n /** Namespace for stored keys. Defaults to \"yage\". */\n namespace?: string;\n}\n\n/** Plugin that registers SaveService into the engine context. */\nexport class SavePlugin implements Plugin {\n readonly name = \"save\";\n readonly version = \"1.0.0\";\n\n private readonly options: SavePluginOptions;\n\n constructor(options?: SavePluginOptions) {\n this.options = options ?? {};\n }\n\n install(context: EngineContext): void {\n const storage = this.options.storage ?? new LocalStorageSaveStorage();\n const service = new SaveService(storage, context, this.options.namespace);\n\n context.register(SaveServiceKey, service);\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/LocalStorageAdapter.ts","../src/SaveService.ts","../src/keys.ts","../src/SavePlugin.ts"],"sourcesContent":["export { VERSION } from \"@yagejs/core\";\n\n// Types\nexport type {\n SaveStorage,\n UntypedSlots,\n GameSnapshot,\n SceneSnapshotEntry,\n EntitySnapshotEntry,\n ComponentSnapshot,\n} from \"./types.js\";\nexport type { SnapshotResolver } from \"@yagejs/core\";\n\n// Storage\nexport { LocalStorageSaveStorage } from \"./LocalStorageAdapter.js\";\n\n// Service\nexport { SaveService } from \"./SaveService.js\";\nexport { SavePlugin } from \"./SavePlugin.js\";\nexport type { SavePluginOptions } from \"./SavePlugin.js\";\nexport { SaveServiceKey } from \"./keys.js\";\n","import type { SaveStorage } from \"./types.js\";\n\n/** SaveStorage backed by browser localStorage. */\nexport class LocalStorageSaveStorage implements SaveStorage {\n load(key: string): string | null {\n return localStorage.getItem(key);\n }\n\n save(key: string, data: string): void {\n localStorage.setItem(key, data);\n }\n\n delete(key: string): void {\n localStorage.removeItem(key);\n }\n\n list(prefix?: string): string[] {\n const result: string[] = [];\n for (let i = 0; i < localStorage.length; i++) {\n const key = localStorage.key(i);\n if (key !== null && (!prefix || key.startsWith(prefix))) {\n result.push(key);\n }\n }\n return result;\n }\n}\n","import type { Scene, Entity, Component, SnapshotResolver } from \"@yagejs/core\";\nimport {\n SceneManagerKey,\n SerializableRegistry,\n isSerializable,\n getSerializableType,\n} from \"@yagejs/core\";\nimport type { EngineContext } from \"@yagejs/core\";\nimport type {\n SaveStorage,\n UntypedSlots,\n GameSnapshot,\n SceneSnapshotEntry,\n EntitySnapshotEntry,\n ComponentSnapshot,\n} from \"./types.js\";\n\n/** Current snapshot format version. */\nconst SNAPSHOT_VERSION = 3;\n\n/**\n * Component restoration priority. Components listed here are added first\n * (in order) to satisfy onAdd() dependencies.\n */\nconst COMPONENT_ORDER = [\n \"Transform\",\n \"RigidBodyComponent\",\n \"ColliderComponent\",\n \"SpriteComponent\",\n \"GraphicsComponent\",\n \"AnimatedSpriteComponent\",\n \"AnimationController\",\n \"SoundComponent\",\n \"ParticleEmitterComponent\",\n \"TilemapComponent\",\n];\n\n/** Orchestrates full game-state serialization and hydration. */\nexport class SaveService<TSlots extends UntypedSlots = UntypedSlots> {\n private readonly storage: SaveStorage;\n private readonly context: EngineContext;\n private readonly namespace: string;\n private _loading = false;\n\n constructor(storage: SaveStorage, context: EngineContext, namespace = \"yage\") {\n this.storage = storage;\n this.context = context;\n this.namespace = namespace;\n }\n\n // ---- Snapshot API ----\n\n /** Save a snapshot of the current scene stack to the given slot. */\n saveSnapshot(slot: string): void {\n const snapshot = this.buildSnapshot();\n this.storage.save(\n this.key(\"snapshot\", slot),\n JSON.stringify(snapshot),\n );\n }\n\n /** Load a snapshot from the given slot, rebuilding the scene stack. */\n async loadSnapshot(slot: string): Promise<void> {\n const snapshot = this.readSnapshot(slot);\n if (!snapshot) {\n throw new Error(`No save found in slot \"${slot}\".`);\n }\n await this.hydrateSnapshot(snapshot);\n }\n\n /** Export a previously saved snapshot from the given slot. */\n exportSnapshot(slot: string): GameSnapshot | null {\n return this.readSnapshot(slot);\n }\n\n /** Import a snapshot into the given slot and hydrate the scene stack. */\n async importSnapshot(slot: string, snapshot: GameSnapshot): Promise<void> {\n if (this._loading) {\n throw new Error(\"loadSnapshot already in progress.\");\n }\n if (snapshot.version !== SNAPSHOT_VERSION) {\n throw new Error(\n `Save version mismatch: expected ${SNAPSHOT_VERSION}, got ${snapshot.version}.`,\n );\n }\n this.storage.save(\n this.key(\"snapshot\", slot),\n JSON.stringify(snapshot),\n );\n await this.hydrateSnapshot(snapshot);\n }\n\n // ---- User Data API ----\n\n /** Save arbitrary structured data to a named slot. */\n saveData<K extends keyof TSlots & string>(slot: K, data: TSlots[K]): void {\n this.storage.save(this.key(\"data\", slot), JSON.stringify(data));\n }\n\n /** Load structured data from a named slot. Returns null if not found. */\n loadData<K extends keyof TSlots & string>(slot: K): TSlots[K] | null {\n const raw = this.storage.load(this.key(\"data\", slot));\n if (raw === null) return null;\n try {\n return JSON.parse(raw) as TSlots[K];\n } catch {\n return null;\n }\n }\n\n /** Read data from a slot for external use (cloud upload, file export). Alias for `loadData`. */\n exportData<K extends keyof TSlots & string>(slot: K): TSlots[K] | null {\n return this.loadData(slot);\n }\n\n /** Write externally-sourced data into a slot. Alias for `saveData` — no version check or hydration. */\n importData<K extends keyof TSlots & string>(slot: K, data: TSlots[K]): void {\n this.saveData(slot, data);\n }\n\n // ---- Snapshot management ----\n\n /** Check if a snapshot exists in the given slot. */\n hasSnapshot(slot: string): boolean {\n return this.storage.load(this.key(\"snapshot\", slot)) !== null;\n }\n\n /** Delete a snapshot from the given slot. */\n deleteSnapshot(slot: string): void {\n this.storage.delete(this.key(\"snapshot\", slot));\n }\n\n // ---- Data management ----\n\n /** Check if user data exists in the given slot. */\n hasData<K extends keyof TSlots & string>(slot: K): boolean {\n return this.storage.load(this.key(\"data\", slot)) !== null;\n }\n\n /** Delete user data from the given slot. */\n deleteData<K extends keyof TSlots & string>(slot: K): void {\n this.storage.delete(this.key(\"data\", slot));\n }\n\n // ---- Private helpers ----\n\n private key(prefix: string, slot: string): string {\n return `${this.namespace}:${prefix}:${slot}`;\n }\n\n private readSnapshot(slot: string): GameSnapshot | null {\n const raw = this.storage.load(this.key(\"snapshot\", slot));\n if (raw === null) return null;\n try {\n return JSON.parse(raw) as GameSnapshot;\n } catch {\n return null;\n }\n }\n\n private buildSnapshot(): GameSnapshot {\n const sceneManager = this.context.resolve(SceneManagerKey);\n const scenes: SceneSnapshotEntry[] = [];\n\n for (const scene of sceneManager.all) {\n if (!isSerializable(scene)) continue;\n const type = getSerializableType(scene);\n if (!type) continue;\n\n const entities: EntitySnapshotEntry[] = [];\n for (const entity of scene.getEntities()) {\n if (!isSerializable(entity)) continue;\n entities.push(this.serializeEntity(entity));\n }\n\n const userData = scene.serialize?.();\n\n scenes.push({\n type,\n paused: scene.paused,\n entities,\n userData,\n });\n }\n\n return {\n version: SNAPSHOT_VERSION,\n timestamp: Date.now(),\n scenes,\n };\n }\n\n private async hydrateSnapshot(snapshot: GameSnapshot): Promise<void> {\n if (this._loading) {\n throw new Error(\"loadSnapshot already in progress.\");\n }\n this._loading = true;\n try {\n if (snapshot.version !== SNAPSHOT_VERSION) {\n throw new Error(\n `Save version mismatch: expected ${SNAPSHOT_VERSION}, got ${snapshot.version}.`,\n );\n }\n\n const sceneManager = this.context.resolve(SceneManagerKey);\n await sceneManager.popAll();\n\n for (const entry of snapshot.scenes) {\n const SceneClass = SerializableRegistry.get(entry.type) as\n | (new () => Scene)\n | undefined;\n if (!SceneClass) {\n throw new Error(\n `Cannot load scene type \"${entry.type}\". ` +\n `Ensure the scene class is decorated with @serializable.`,\n );\n }\n\n const scene = new SceneClass();\n\n // Instance-patch onEnter: restore entities + call afterRestore instead\n scene.onEnter = () => {\n this.restoreSceneEntities(scene, entry);\n };\n\n await sceneManager.push(scene);\n\n if (entry.paused) {\n scene.paused = true;\n }\n }\n } finally {\n this._loading = false;\n }\n }\n\n private restoreSceneEntities(\n scene: Scene,\n entry: SceneSnapshotEntry,\n ): void {\n // Phase 1: Create all entities, add to scene, add components\n const idMap = new Map<number, Entity>();\n const entityEntries: Array<{\n entity: Entity;\n entry: EntitySnapshotEntry;\n restoredComponents: Array<{ component: Component; data: unknown }>;\n }> = [];\n\n for (const entityEntry of entry.entities) {\n const EntityClass = SerializableRegistry.get(entityEntry.type) as\n | (new () => Entity)\n | undefined;\n if (!EntityClass) {\n console.warn(\n `Entity type \"${entityEntry.type}\" not found in registry — skipping.`,\n );\n continue;\n }\n\n const entity = new EntityClass();\n scene._addExistingEntity(entity);\n const restoredComponents = this.restoreEntityComponents(\n entity,\n entityEntry.components,\n );\n\n idMap.set(entityEntry.id, entity);\n entityEntries.push({ entity, entry: entityEntry, restoredComponents });\n }\n\n // Phase 2: Rewire parent/child relationships\n for (const { entity, entry: entityEntry } of entityEntries) {\n if (entityEntry.parentId != null && entityEntry.childName != null) {\n const parent = idMap.get(entityEntry.parentId);\n if (parent) {\n parent.addChild(entityEntry.childName, entity);\n } else {\n console.warn(\n `Parent entity (saved id ${entityEntry.parentId}) not found for child \"${entity.name}\" — restoring as root entity.`,\n );\n }\n }\n }\n\n // Build resolver for afterRestore hooks\n const resolver: SnapshotResolver = {\n entity(savedId: number) {\n return idMap.get(savedId) ?? null;\n },\n };\n\n // Phase 3: afterRestore hooks (components, then entities, then scene)\n for (const { entity, entry: entityEntry, restoredComponents } of entityEntries) {\n for (const { component, data } of restoredComponents) {\n component.afterRestore?.(data, resolver);\n }\n\n entity.afterRestore?.(entityEntry.userData, resolver);\n }\n\n scene.afterRestore?.(entry.userData, resolver);\n }\n\n private serializeEntity(entity: Entity): EntitySnapshotEntry {\n const type = getSerializableType(entity);\n if (!type) throw new Error(\"Entity is not serializable\");\n\n const components: ComponentSnapshot[] = [];\n for (const component of entity.getAll()) {\n if (typeof component.serialize !== \"function\") continue;\n const data = component.serialize();\n if (data == null) continue;\n const compType =\n getSerializableType(component) ?? component.constructor.name;\n components.push({ type: compType, data });\n }\n\n const userData = entity.serialize?.();\n\n const result: EntitySnapshotEntry = {\n id: entity.id,\n type,\n components,\n userData,\n };\n\n // Capture parent/child relationship\n if (entity.parent && isSerializable(entity.parent)) {\n result.parentId = entity.parent.id;\n // Find the name this entity is registered under\n for (const [name, child] of entity.parent.children) {\n if (child === entity) {\n result.childName = name;\n break;\n }\n }\n } else if (entity.parent) {\n console.warn(\n `Entity \"${entity.name}\" has non-serializable parent \"${entity.parent.name}\" — parent/child relationship will not be saved.`,\n );\n }\n\n return result;\n }\n\n private restoreEntityComponents(\n entity: Entity,\n snapshots: ComponentSnapshot[],\n ): Array<{ component: Component; data: unknown }> {\n const sorted = [...snapshots].sort((a, b) => {\n const ai = COMPONENT_ORDER.indexOf(a.type);\n const bi = COMPONENT_ORDER.indexOf(b.type);\n return (ai >= 0 ? ai : 999) - (bi >= 0 ? bi : 999);\n });\n\n const restored: Array<{ component: Component; data: unknown }> = [];\n\n for (const snap of sorted) {\n const CompClass = SerializableRegistry.get(snap.type) as\n | ({ fromSnapshot?(data: unknown): Component } & (new (\n ...args: unknown[]\n ) => Component))\n | undefined;\n\n if (!CompClass || typeof CompClass.fromSnapshot !== \"function\") {\n // Not in registry or no fromSnapshot — entity handles in afterRestore\n continue;\n }\n\n const component = CompClass.fromSnapshot(snap.data);\n entity.add(component);\n restored.push({ component, data: snap.data });\n }\n\n return restored;\n }\n}\n","import { ServiceKey } from \"@yagejs/core\";\nimport type { SaveService } from \"./SaveService.js\";\n\n/** Service key for the SaveService. */\nexport const SaveServiceKey = new ServiceKey<SaveService>(\"saveService\");\n","import type { EngineContext, Plugin } from \"@yagejs/core\";\nimport type { SaveStorage } from \"./types.js\";\nimport { LocalStorageSaveStorage } from \"./LocalStorageAdapter.js\";\nimport { SaveService } from \"./SaveService.js\";\nimport { SaveServiceKey } from \"./keys.js\";\n\n/** Options for the SavePlugin. */\nexport interface SavePluginOptions {\n /** Custom storage backend. Defaults to LocalStorageSaveStorage. */\n storage?: SaveStorage;\n /** Namespace for stored keys. Defaults to \"yage\". */\n namespace?: string;\n}\n\n/** Plugin that registers SaveService into the engine context. */\nexport class SavePlugin implements Plugin {\n readonly name = \"save\";\n readonly version = \"1.0.0\";\n\n private readonly options: SavePluginOptions;\n\n constructor(options?: SavePluginOptions) {\n this.options = options ?? {};\n }\n\n install(context: EngineContext): void {\n const storage = this.options.storage ?? new LocalStorageSaveStorage();\n const service = new SaveService(storage, context, this.options.namespace);\n\n context.register(SaveServiceKey, service);\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yagejs/save",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Save and load game state for YAGE",
5
5
  "keywords": [
6
6
  "yage",
@@ -48,6 +48,6 @@
48
48
  "clean": "rm -rf dist"
49
49
  },
50
50
  "dependencies": {
51
- "@yagejs/core": "^0.1.0"
51
+ "@yagejs/core": "^0.3.0"
52
52
  }
53
53
  }