@yagejs/save 0.2.0 → 0.4.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
@@ -58,7 +58,7 @@ var LocalStorageSaveStorage = class {
58
58
 
59
59
  // src/SaveService.ts
60
60
  var import_core = require("@yagejs/core");
61
- var SNAPSHOT_VERSION = 3;
61
+ var SNAPSHOT_VERSION = 4;
62
62
  var COMPONENT_ORDER = [
63
63
  "Transform",
64
64
  "RigidBodyComponent",
@@ -78,12 +78,28 @@ var SaveService = class {
78
78
  storage;
79
79
  context;
80
80
  namespace;
81
+ contributors = /* @__PURE__ */ new Map();
81
82
  _loading = false;
82
83
  constructor(storage, context, namespace = "yage") {
83
84
  this.storage = storage;
84
85
  this.context = context;
85
86
  this.namespace = namespace;
86
87
  }
88
+ // ---- Extension API ----
89
+ /**
90
+ * Register a plugin to contribute extra data to every snapshot under
91
+ * `key`. The contributor's `serialize()` is invoked during `saveSnapshot`,
92
+ * and its `restore(data)` runs after every scene + entity in the snapshot
93
+ * has been hydrated. Re-registering an existing key replaces the previous
94
+ * contributor.
95
+ */
96
+ registerSnapshotExtra(key, contributor) {
97
+ this.contributors.set(key, contributor);
98
+ }
99
+ /** Remove a previously registered contributor. */
100
+ unregisterSnapshotExtra(key) {
101
+ this.contributors.delete(key);
102
+ }
87
103
  // ---- Snapshot API ----
88
104
  /** Save a snapshot of the current scene stack to the given slot. */
89
105
  saveSnapshot(slot) {
@@ -195,10 +211,27 @@ var SaveService = class {
195
211
  userData
196
212
  });
197
213
  }
214
+ const extras = {};
215
+ let hasExtras = false;
216
+ for (const [key, contributor] of this.contributors) {
217
+ try {
218
+ const data = contributor.serialize();
219
+ if (data !== void 0) {
220
+ extras[key] = data;
221
+ hasExtras = true;
222
+ }
223
+ } catch (err) {
224
+ console.error(
225
+ `SaveService: contributor "${key}" serialize failed; omitting its extras from this snapshot.`,
226
+ err
227
+ );
228
+ }
229
+ }
198
230
  return {
199
231
  version: SNAPSHOT_VERSION,
200
232
  timestamp: Date.now(),
201
- scenes
233
+ scenes,
234
+ ...hasExtras ? { extras } : {}
202
235
  };
203
236
  }
204
237
  async hydrateSnapshot(snapshot) {
@@ -230,6 +263,24 @@ var SaveService = class {
230
263
  scene.paused = true;
231
264
  }
232
265
  }
266
+ const extras = snapshot.extras ?? {};
267
+ for (const [key, contributor] of this.contributors) {
268
+ try {
269
+ await contributor.restore(extras[key]);
270
+ } catch (err) {
271
+ console.error(
272
+ `SaveService: contributor "${key}" restore failed; continuing.`,
273
+ err
274
+ );
275
+ }
276
+ }
277
+ for (const key of Object.keys(extras)) {
278
+ if (!this.contributors.has(key)) {
279
+ console.warn(
280
+ `SaveService: snapshot contains extras for "${key}" but no contributor is registered \u2014 skipping.`
281
+ );
282
+ }
283
+ }
233
284
  } finally {
234
285
  this._loading = false;
235
286
  }
@@ -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 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"]}
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 SnapshotContributor,\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 SnapshotContributor,\n} from \"./types.js\";\n\n/** Current snapshot format version. */\nconst SNAPSHOT_VERSION = 4;\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 readonly contributors = new Map<string, SnapshotContributor>();\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 // ---- Extension API ----\n\n /**\n * Register a plugin to contribute extra data to every snapshot under\n * `key`. The contributor's `serialize()` is invoked during `saveSnapshot`,\n * and its `restore(data)` runs after every scene + entity in the snapshot\n * has been hydrated. Re-registering an existing key replaces the previous\n * contributor.\n */\n registerSnapshotExtra(key: string, contributor: SnapshotContributor): void {\n this.contributors.set(key, contributor);\n }\n\n /** Remove a previously registered contributor. */\n unregisterSnapshotExtra(key: string): void {\n this.contributors.delete(key);\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 const extras: Record<string, unknown> = {};\n let hasExtras = false;\n for (const [key, contributor] of this.contributors) {\n // Isolate each contributor — a single failing one (e.g. a third-party\n // plugin throwing during serialize) shouldn't poison the rest of the\n // snapshot. Log and continue.\n try {\n const data = contributor.serialize();\n if (data !== undefined) {\n extras[key] = data;\n hasExtras = true;\n }\n } catch (err) {\n console.error(\n `SaveService: contributor \"${key}\" serialize failed; ` +\n `omitting its extras from this snapshot.`,\n err,\n );\n }\n }\n\n return {\n version: SNAPSHOT_VERSION,\n timestamp: Date.now(),\n scenes,\n ...(hasExtras ? { extras } : {}),\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\n // Run snapshot contributors after every scene is pushed and entities\n // restored. By this point any layer/scene render trees the renderer\n // contributor needs to find by name are live, and component-scope\n // afterRestore hooks have run, so contributors see consistent state.\n //\n // Iterate every REGISTERED contributor — not just keys present in\n // `extras` — so contributors get a chance to clear stale baseline\n // state (e.g. a screen-scope vignette enabled after the save) when\n // the snapshot has no entry for them. Contributors must treat\n // `data === undefined` as \"reset to empty\".\n const extras = snapshot.extras ?? {};\n for (const [key, contributor] of this.contributors) {\n // Isolate each contributor — one bad restore (e.g. a third-party\n // plugin choking on a partially-compatible payload) shouldn't abort\n // the whole load and leave the engine in a half-restored state.\n try {\n await contributor.restore(extras[key]);\n } catch (err) {\n console.error(\n `SaveService: contributor \"${key}\" restore failed; continuing.`,\n err,\n );\n }\n }\n for (const key of Object.keys(extras)) {\n if (!this.contributors.has(key)) {\n console.warn(\n `SaveService: snapshot contains extras for \"${key}\" but no ` +\n `contributor is registered — skipping.`,\n );\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.d.cts CHANGED
@@ -15,6 +15,23 @@ interface GameSnapshot {
15
15
  version: number;
16
16
  timestamp: number;
17
17
  scenes: SceneSnapshotEntry[];
18
+ /**
19
+ * Extension data contributed by plugins outside the entity/component model
20
+ * — e.g. layer/scene/screen-scope effects from the renderer. Keyed by the
21
+ * string passed to `SaveService.registerSnapshotExtra`.
22
+ */
23
+ extras?: Record<string, unknown>;
24
+ }
25
+ /**
26
+ * Plugin-supplied snapshot data that doesn't fit the per-entity/component
27
+ * model. The renderer registers one of these for layer/scene/screen-scope
28
+ * effects.
29
+ */
30
+ interface SnapshotContributor {
31
+ /** Build the snapshot fragment. Return `undefined` to omit the extra. */
32
+ serialize(): unknown;
33
+ /** Apply the snapshot fragment back onto live state. */
34
+ restore(data: unknown): void | Promise<void>;
18
35
  }
19
36
  /** Serialized state for a single scene in the stack. */
20
37
  interface SceneSnapshotEntry {
@@ -63,8 +80,19 @@ declare class SaveService<TSlots extends UntypedSlots = UntypedSlots> {
63
80
  private readonly storage;
64
81
  private readonly context;
65
82
  private readonly namespace;
83
+ private readonly contributors;
66
84
  private _loading;
67
85
  constructor(storage: SaveStorage, context: EngineContext, namespace?: string);
86
+ /**
87
+ * Register a plugin to contribute extra data to every snapshot under
88
+ * `key`. The contributor's `serialize()` is invoked during `saveSnapshot`,
89
+ * and its `restore(data)` runs after every scene + entity in the snapshot
90
+ * has been hydrated. Re-registering an existing key replaces the previous
91
+ * contributor.
92
+ */
93
+ registerSnapshotExtra(key: string, contributor: SnapshotContributor): void;
94
+ /** Remove a previously registered contributor. */
95
+ unregisterSnapshotExtra(key: string): void;
68
96
  /** Save a snapshot of the current scene stack to the given slot. */
69
97
  saveSnapshot(slot: string): void;
70
98
  /** Load a snapshot from the given slot, rebuilding the scene stack. */
@@ -117,4 +145,4 @@ declare class SavePlugin implements Plugin {
117
145
  /** Service key for the SaveService. */
118
146
  declare const SaveServiceKey: ServiceKey<SaveService<UntypedSlots>>;
119
147
 
120
- export { type ComponentSnapshot, type EntitySnapshotEntry, type GameSnapshot, LocalStorageSaveStorage, SavePlugin, type SavePluginOptions, SaveService, SaveServiceKey, type SaveStorage, type SceneSnapshotEntry, type UntypedSlots };
148
+ export { type ComponentSnapshot, type EntitySnapshotEntry, type GameSnapshot, LocalStorageSaveStorage, SavePlugin, type SavePluginOptions, SaveService, SaveServiceKey, type SaveStorage, type SceneSnapshotEntry, type SnapshotContributor, type UntypedSlots };
package/dist/index.d.ts CHANGED
@@ -15,6 +15,23 @@ interface GameSnapshot {
15
15
  version: number;
16
16
  timestamp: number;
17
17
  scenes: SceneSnapshotEntry[];
18
+ /**
19
+ * Extension data contributed by plugins outside the entity/component model
20
+ * — e.g. layer/scene/screen-scope effects from the renderer. Keyed by the
21
+ * string passed to `SaveService.registerSnapshotExtra`.
22
+ */
23
+ extras?: Record<string, unknown>;
24
+ }
25
+ /**
26
+ * Plugin-supplied snapshot data that doesn't fit the per-entity/component
27
+ * model. The renderer registers one of these for layer/scene/screen-scope
28
+ * effects.
29
+ */
30
+ interface SnapshotContributor {
31
+ /** Build the snapshot fragment. Return `undefined` to omit the extra. */
32
+ serialize(): unknown;
33
+ /** Apply the snapshot fragment back onto live state. */
34
+ restore(data: unknown): void | Promise<void>;
18
35
  }
19
36
  /** Serialized state for a single scene in the stack. */
20
37
  interface SceneSnapshotEntry {
@@ -63,8 +80,19 @@ declare class SaveService<TSlots extends UntypedSlots = UntypedSlots> {
63
80
  private readonly storage;
64
81
  private readonly context;
65
82
  private readonly namespace;
83
+ private readonly contributors;
66
84
  private _loading;
67
85
  constructor(storage: SaveStorage, context: EngineContext, namespace?: string);
86
+ /**
87
+ * Register a plugin to contribute extra data to every snapshot under
88
+ * `key`. The contributor's `serialize()` is invoked during `saveSnapshot`,
89
+ * and its `restore(data)` runs after every scene + entity in the snapshot
90
+ * has been hydrated. Re-registering an existing key replaces the previous
91
+ * contributor.
92
+ */
93
+ registerSnapshotExtra(key: string, contributor: SnapshotContributor): void;
94
+ /** Remove a previously registered contributor. */
95
+ unregisterSnapshotExtra(key: string): void;
68
96
  /** Save a snapshot of the current scene stack to the given slot. */
69
97
  saveSnapshot(slot: string): void;
70
98
  /** Load a snapshot from the given slot, rebuilding the scene stack. */
@@ -117,4 +145,4 @@ declare class SavePlugin implements Plugin {
117
145
  /** Service key for the SaveService. */
118
146
  declare const SaveServiceKey: ServiceKey<SaveService<UntypedSlots>>;
119
147
 
120
- export { type ComponentSnapshot, type EntitySnapshotEntry, type GameSnapshot, LocalStorageSaveStorage, SavePlugin, type SavePluginOptions, SaveService, SaveServiceKey, type SaveStorage, type SceneSnapshotEntry, type UntypedSlots };
148
+ export { type ComponentSnapshot, type EntitySnapshotEntry, type GameSnapshot, LocalStorageSaveStorage, SavePlugin, type SavePluginOptions, SaveService, SaveServiceKey, type SaveStorage, type SceneSnapshotEntry, type SnapshotContributor, type UntypedSlots };
package/dist/index.js CHANGED
@@ -37,7 +37,7 @@ import {
37
37
  isSerializable,
38
38
  getSerializableType
39
39
  } from "@yagejs/core";
40
- var SNAPSHOT_VERSION = 3;
40
+ var SNAPSHOT_VERSION = 4;
41
41
  var COMPONENT_ORDER = [
42
42
  "Transform",
43
43
  "RigidBodyComponent",
@@ -57,12 +57,28 @@ var SaveService = class {
57
57
  storage;
58
58
  context;
59
59
  namespace;
60
+ contributors = /* @__PURE__ */ new Map();
60
61
  _loading = false;
61
62
  constructor(storage, context, namespace = "yage") {
62
63
  this.storage = storage;
63
64
  this.context = context;
64
65
  this.namespace = namespace;
65
66
  }
67
+ // ---- Extension API ----
68
+ /**
69
+ * Register a plugin to contribute extra data to every snapshot under
70
+ * `key`. The contributor's `serialize()` is invoked during `saveSnapshot`,
71
+ * and its `restore(data)` runs after every scene + entity in the snapshot
72
+ * has been hydrated. Re-registering an existing key replaces the previous
73
+ * contributor.
74
+ */
75
+ registerSnapshotExtra(key, contributor) {
76
+ this.contributors.set(key, contributor);
77
+ }
78
+ /** Remove a previously registered contributor. */
79
+ unregisterSnapshotExtra(key) {
80
+ this.contributors.delete(key);
81
+ }
66
82
  // ---- Snapshot API ----
67
83
  /** Save a snapshot of the current scene stack to the given slot. */
68
84
  saveSnapshot(slot) {
@@ -174,10 +190,27 @@ var SaveService = class {
174
190
  userData
175
191
  });
176
192
  }
193
+ const extras = {};
194
+ let hasExtras = false;
195
+ for (const [key, contributor] of this.contributors) {
196
+ try {
197
+ const data = contributor.serialize();
198
+ if (data !== void 0) {
199
+ extras[key] = data;
200
+ hasExtras = true;
201
+ }
202
+ } catch (err) {
203
+ console.error(
204
+ `SaveService: contributor "${key}" serialize failed; omitting its extras from this snapshot.`,
205
+ err
206
+ );
207
+ }
208
+ }
177
209
  return {
178
210
  version: SNAPSHOT_VERSION,
179
211
  timestamp: Date.now(),
180
- scenes
212
+ scenes,
213
+ ...hasExtras ? { extras } : {}
181
214
  };
182
215
  }
183
216
  async hydrateSnapshot(snapshot) {
@@ -209,6 +242,24 @@ var SaveService = class {
209
242
  scene.paused = true;
210
243
  }
211
244
  }
245
+ const extras = snapshot.extras ?? {};
246
+ for (const [key, contributor] of this.contributors) {
247
+ try {
248
+ await contributor.restore(extras[key]);
249
+ } catch (err) {
250
+ console.error(
251
+ `SaveService: contributor "${key}" restore failed; continuing.`,
252
+ err
253
+ );
254
+ }
255
+ }
256
+ for (const key of Object.keys(extras)) {
257
+ if (!this.contributors.has(key)) {
258
+ console.warn(
259
+ `SaveService: snapshot contains extras for "${key}" but no contributor is registered \u2014 skipping.`
260
+ );
261
+ }
262
+ }
212
263
  } finally {
213
264
  this._loading = false;
214
265
  }
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 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":[]}
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 SnapshotContributor,\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 SnapshotContributor,\n} from \"./types.js\";\n\n/** Current snapshot format version. */\nconst SNAPSHOT_VERSION = 4;\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 readonly contributors = new Map<string, SnapshotContributor>();\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 // ---- Extension API ----\n\n /**\n * Register a plugin to contribute extra data to every snapshot under\n * `key`. The contributor's `serialize()` is invoked during `saveSnapshot`,\n * and its `restore(data)` runs after every scene + entity in the snapshot\n * has been hydrated. Re-registering an existing key replaces the previous\n * contributor.\n */\n registerSnapshotExtra(key: string, contributor: SnapshotContributor): void {\n this.contributors.set(key, contributor);\n }\n\n /** Remove a previously registered contributor. */\n unregisterSnapshotExtra(key: string): void {\n this.contributors.delete(key);\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 const extras: Record<string, unknown> = {};\n let hasExtras = false;\n for (const [key, contributor] of this.contributors) {\n // Isolate each contributor — a single failing one (e.g. a third-party\n // plugin throwing during serialize) shouldn't poison the rest of the\n // snapshot. Log and continue.\n try {\n const data = contributor.serialize();\n if (data !== undefined) {\n extras[key] = data;\n hasExtras = true;\n }\n } catch (err) {\n console.error(\n `SaveService: contributor \"${key}\" serialize failed; ` +\n `omitting its extras from this snapshot.`,\n err,\n );\n }\n }\n\n return {\n version: SNAPSHOT_VERSION,\n timestamp: Date.now(),\n scenes,\n ...(hasExtras ? { extras } : {}),\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\n // Run snapshot contributors after every scene is pushed and entities\n // restored. By this point any layer/scene render trees the renderer\n // contributor needs to find by name are live, and component-scope\n // afterRestore hooks have run, so contributors see consistent state.\n //\n // Iterate every REGISTERED contributor — not just keys present in\n // `extras` — so contributors get a chance to clear stale baseline\n // state (e.g. a screen-scope vignette enabled after the save) when\n // the snapshot has no entry for them. Contributors must treat\n // `data === undefined` as \"reset to empty\".\n const extras = snapshot.extras ?? {};\n for (const [key, contributor] of this.contributors) {\n // Isolate each contributor — one bad restore (e.g. a third-party\n // plugin choking on a partially-compatible payload) shouldn't abort\n // the whole load and leave the engine in a half-restored state.\n try {\n await contributor.restore(extras[key]);\n } catch (err) {\n console.error(\n `SaveService: contributor \"${key}\" restore failed; continuing.`,\n err,\n );\n }\n }\n for (const key of Object.keys(extras)) {\n if (!this.contributors.has(key)) {\n console.warn(\n `SaveService: snapshot contains extras for \"${key}\" but no ` +\n `contributor is registered — skipping.`,\n );\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.2.0",
3
+ "version": "0.4.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.2.0"
51
+ "@yagejs/core": "^0.4.0"
52
52
  }
53
53
  }