@yagejs/save 0.1.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/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # @yagejs/save
2
+
3
+ Save and load game state for the [YAGE](https://yage.dev) 2D game engine.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @yagejs/save
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { Engine } from "@yagejs/core";
15
+ import { SavePlugin, SaveServiceKey } from "@yagejs/save";
16
+
17
+ const engine = new Engine();
18
+ engine.use(new SavePlugin());
19
+ ```
20
+
21
+ Mark components as serializable and save the world:
22
+
23
+ ```ts
24
+ import { serializable } from "@yagejs/core";
25
+
26
+ @serializable("player-stats")
27
+ class PlayerStats extends Component {
28
+ constructor(public hp = 100, public xp = 0) { super(); }
29
+ }
30
+
31
+ // Save
32
+ const saveService = engine.context.resolve(SaveServiceKey);
33
+ await saveService.save("slot-1");
34
+
35
+ // Load
36
+ await saveService.load("slot-1");
37
+ ```
38
+
39
+ ## What's in the box
40
+
41
+ - **SavePlugin / SaveService** - snapshot the entire engine state
42
+ - **Pluggable storage** - `LocalStorageSaveStorage` included; implement `SaveStorage` for custom backends (IndexedDB, files, remote, etc.)
43
+ - **@serializable decorator** - opt components into save/load
44
+ - **Typed slots** - multiple save slots with metadata
45
+
46
+ ## Docs
47
+
48
+ Full documentation at [yage.dev](https://yage.dev).
49
+
50
+ ## License
51
+
52
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,364 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ LocalStorageSaveStorage: () => LocalStorageSaveStorage,
25
+ SavePlugin: () => SavePlugin,
26
+ SaveService: () => SaveService,
27
+ SaveServiceKey: () => SaveServiceKey,
28
+ VERSION: () => import_core3.VERSION
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+ var import_core3 = require("@yagejs/core");
32
+
33
+ // src/LocalStorageAdapter.ts
34
+ var LocalStorageSaveStorage = class {
35
+ static {
36
+ __name(this, "LocalStorageSaveStorage");
37
+ }
38
+ load(key) {
39
+ return localStorage.getItem(key);
40
+ }
41
+ save(key, data) {
42
+ localStorage.setItem(key, data);
43
+ }
44
+ delete(key) {
45
+ localStorage.removeItem(key);
46
+ }
47
+ list(prefix) {
48
+ const result = [];
49
+ for (let i = 0; i < localStorage.length; i++) {
50
+ const key = localStorage.key(i);
51
+ if (key !== null && (!prefix || key.startsWith(prefix))) {
52
+ result.push(key);
53
+ }
54
+ }
55
+ return result;
56
+ }
57
+ };
58
+
59
+ // src/SaveService.ts
60
+ var import_core = require("@yagejs/core");
61
+ var SNAPSHOT_VERSION = 3;
62
+ var COMPONENT_ORDER = [
63
+ "Transform",
64
+ "RigidBodyComponent",
65
+ "ColliderComponent",
66
+ "SpriteComponent",
67
+ "GraphicsComponent",
68
+ "AnimatedSpriteComponent",
69
+ "AnimationController",
70
+ "SoundComponent",
71
+ "ParticleEmitterComponent",
72
+ "TilemapComponent"
73
+ ];
74
+ var SaveService = class {
75
+ static {
76
+ __name(this, "SaveService");
77
+ }
78
+ storage;
79
+ context;
80
+ namespace;
81
+ _loading = false;
82
+ constructor(storage, context, namespace = "yage") {
83
+ this.storage = storage;
84
+ this.context = context;
85
+ this.namespace = namespace;
86
+ }
87
+ // ---- Snapshot API ----
88
+ /** Save a snapshot of the current scene stack to the given slot. */
89
+ saveSnapshot(slot) {
90
+ const snapshot = this.buildSnapshot();
91
+ this.storage.save(
92
+ this.key("snapshot", slot),
93
+ JSON.stringify(snapshot)
94
+ );
95
+ }
96
+ /** Load a snapshot from the given slot, rebuilding the scene stack. */
97
+ async loadSnapshot(slot) {
98
+ const snapshot = this.readSnapshot(slot);
99
+ if (!snapshot) {
100
+ throw new Error(`No save found in slot "${slot}".`);
101
+ }
102
+ await this.hydrateSnapshot(snapshot);
103
+ }
104
+ /** Export a previously saved snapshot from the given slot. */
105
+ exportSnapshot(slot) {
106
+ return this.readSnapshot(slot);
107
+ }
108
+ /** Import a snapshot into the given slot and hydrate the scene stack. */
109
+ async importSnapshot(slot, snapshot) {
110
+ if (this._loading) {
111
+ throw new Error("loadSnapshot already in progress.");
112
+ }
113
+ if (snapshot.version !== SNAPSHOT_VERSION) {
114
+ throw new Error(
115
+ `Save version mismatch: expected ${SNAPSHOT_VERSION}, got ${snapshot.version}.`
116
+ );
117
+ }
118
+ this.storage.save(
119
+ this.key("snapshot", slot),
120
+ JSON.stringify(snapshot)
121
+ );
122
+ await this.hydrateSnapshot(snapshot);
123
+ }
124
+ // ---- User Data API ----
125
+ /** Save arbitrary structured data to a named slot. */
126
+ saveData(slot, data) {
127
+ this.storage.save(this.key("data", slot), JSON.stringify(data));
128
+ }
129
+ /** Load structured data from a named slot. Returns null if not found. */
130
+ loadData(slot) {
131
+ const raw = this.storage.load(this.key("data", slot));
132
+ if (raw === null) return null;
133
+ try {
134
+ return JSON.parse(raw);
135
+ } catch {
136
+ return null;
137
+ }
138
+ }
139
+ /** Read data from a slot for external use (cloud upload, file export). Alias for `loadData`. */
140
+ exportData(slot) {
141
+ return this.loadData(slot);
142
+ }
143
+ /** Write externally-sourced data into a slot. Alias for `saveData` — no version check or hydration. */
144
+ importData(slot, data) {
145
+ this.saveData(slot, data);
146
+ }
147
+ // ---- Snapshot management ----
148
+ /** Check if a snapshot exists in the given slot. */
149
+ hasSnapshot(slot) {
150
+ return this.storage.load(this.key("snapshot", slot)) !== null;
151
+ }
152
+ /** Delete a snapshot from the given slot. */
153
+ deleteSnapshot(slot) {
154
+ this.storage.delete(this.key("snapshot", slot));
155
+ }
156
+ // ---- Data management ----
157
+ /** Check if user data exists in the given slot. */
158
+ hasData(slot) {
159
+ return this.storage.load(this.key("data", slot)) !== null;
160
+ }
161
+ /** Delete user data from the given slot. */
162
+ deleteData(slot) {
163
+ this.storage.delete(this.key("data", slot));
164
+ }
165
+ // ---- Private helpers ----
166
+ key(prefix, slot) {
167
+ return `${this.namespace}:${prefix}:${slot}`;
168
+ }
169
+ readSnapshot(slot) {
170
+ const raw = this.storage.load(this.key("snapshot", slot));
171
+ if (raw === null) return null;
172
+ try {
173
+ return JSON.parse(raw);
174
+ } catch {
175
+ return null;
176
+ }
177
+ }
178
+ buildSnapshot() {
179
+ const sceneManager = this.context.resolve(import_core.SceneManagerKey);
180
+ const scenes = [];
181
+ for (const scene of sceneManager.all) {
182
+ if (!(0, import_core.isSerializable)(scene)) continue;
183
+ const type = (0, import_core.getSerializableType)(scene);
184
+ if (!type) continue;
185
+ const entities = [];
186
+ for (const entity of scene.getEntities()) {
187
+ if (!(0, import_core.isSerializable)(entity)) continue;
188
+ entities.push(this.serializeEntity(entity));
189
+ }
190
+ const userData = scene.serialize?.();
191
+ scenes.push({
192
+ type,
193
+ paused: scene.paused,
194
+ entities,
195
+ userData
196
+ });
197
+ }
198
+ return {
199
+ version: SNAPSHOT_VERSION,
200
+ timestamp: Date.now(),
201
+ scenes
202
+ };
203
+ }
204
+ async hydrateSnapshot(snapshot) {
205
+ if (this._loading) {
206
+ throw new Error("loadSnapshot already in progress.");
207
+ }
208
+ this._loading = true;
209
+ try {
210
+ if (snapshot.version !== SNAPSHOT_VERSION) {
211
+ throw new Error(
212
+ `Save version mismatch: expected ${SNAPSHOT_VERSION}, got ${snapshot.version}.`
213
+ );
214
+ }
215
+ const sceneManager = this.context.resolve(import_core.SceneManagerKey);
216
+ sceneManager.clear();
217
+ for (const entry of snapshot.scenes) {
218
+ const SceneClass = import_core.SerializableRegistry.get(entry.type);
219
+ if (!SceneClass) {
220
+ throw new Error(
221
+ `Cannot load scene type "${entry.type}". Ensure the scene class is decorated with @serializable.`
222
+ );
223
+ }
224
+ const scene = new SceneClass();
225
+ scene.onEnter = () => {
226
+ this.restoreSceneEntities(scene, entry);
227
+ };
228
+ await sceneManager.push(scene);
229
+ if (entry.paused) {
230
+ scene.paused = true;
231
+ }
232
+ }
233
+ } finally {
234
+ this._loading = false;
235
+ }
236
+ }
237
+ restoreSceneEntities(scene, entry) {
238
+ const idMap = /* @__PURE__ */ new Map();
239
+ const entityEntries = [];
240
+ for (const entityEntry of entry.entities) {
241
+ const EntityClass = import_core.SerializableRegistry.get(entityEntry.type);
242
+ if (!EntityClass) {
243
+ console.warn(
244
+ `Entity type "${entityEntry.type}" not found in registry \u2014 skipping.`
245
+ );
246
+ continue;
247
+ }
248
+ const entity = new EntityClass();
249
+ scene._addExistingEntity(entity);
250
+ const restoredComponents = this.restoreEntityComponents(
251
+ entity,
252
+ entityEntry.components
253
+ );
254
+ idMap.set(entityEntry.id, entity);
255
+ entityEntries.push({ entity, entry: entityEntry, restoredComponents });
256
+ }
257
+ for (const { entity, entry: entityEntry } of entityEntries) {
258
+ if (entityEntry.parentId != null && entityEntry.childName != null) {
259
+ const parent = idMap.get(entityEntry.parentId);
260
+ if (parent) {
261
+ parent.addChild(entityEntry.childName, entity);
262
+ } else {
263
+ console.warn(
264
+ `Parent entity (saved id ${entityEntry.parentId}) not found for child "${entity.name}" \u2014 restoring as root entity.`
265
+ );
266
+ }
267
+ }
268
+ }
269
+ const resolver = {
270
+ entity(savedId) {
271
+ return idMap.get(savedId) ?? null;
272
+ }
273
+ };
274
+ for (const { entity, entry: entityEntry, restoredComponents } of entityEntries) {
275
+ for (const { component, data } of restoredComponents) {
276
+ component.afterRestore?.(data, resolver);
277
+ }
278
+ entity.afterRestore?.(entityEntry.userData, resolver);
279
+ }
280
+ scene.afterRestore?.(entry.userData, resolver);
281
+ }
282
+ serializeEntity(entity) {
283
+ const type = (0, import_core.getSerializableType)(entity);
284
+ if (!type) throw new Error("Entity is not serializable");
285
+ const components = [];
286
+ for (const component of entity.getAll()) {
287
+ if (typeof component.serialize !== "function") continue;
288
+ const data = component.serialize();
289
+ if (data == null) continue;
290
+ const compType = (0, import_core.getSerializableType)(component) ?? component.constructor.name;
291
+ components.push({ type: compType, data });
292
+ }
293
+ const userData = entity.serialize?.();
294
+ const result = {
295
+ id: entity.id,
296
+ type,
297
+ components,
298
+ userData
299
+ };
300
+ if (entity.parent && (0, import_core.isSerializable)(entity.parent)) {
301
+ result.parentId = entity.parent.id;
302
+ for (const [name, child] of entity.parent.children) {
303
+ if (child === entity) {
304
+ result.childName = name;
305
+ break;
306
+ }
307
+ }
308
+ } else if (entity.parent) {
309
+ console.warn(
310
+ `Entity "${entity.name}" has non-serializable parent "${entity.parent.name}" \u2014 parent/child relationship will not be saved.`
311
+ );
312
+ }
313
+ return result;
314
+ }
315
+ restoreEntityComponents(entity, snapshots) {
316
+ const sorted = [...snapshots].sort((a, b) => {
317
+ const ai = COMPONENT_ORDER.indexOf(a.type);
318
+ const bi = COMPONENT_ORDER.indexOf(b.type);
319
+ return (ai >= 0 ? ai : 999) - (bi >= 0 ? bi : 999);
320
+ });
321
+ const restored = [];
322
+ for (const snap of sorted) {
323
+ const CompClass = import_core.SerializableRegistry.get(snap.type);
324
+ if (!CompClass || typeof CompClass.fromSnapshot !== "function") {
325
+ continue;
326
+ }
327
+ const component = CompClass.fromSnapshot(snap.data);
328
+ entity.add(component);
329
+ restored.push({ component, data: snap.data });
330
+ }
331
+ return restored;
332
+ }
333
+ };
334
+
335
+ // src/keys.ts
336
+ var import_core2 = require("@yagejs/core");
337
+ var SaveServiceKey = new import_core2.ServiceKey("saveService");
338
+
339
+ // src/SavePlugin.ts
340
+ var SavePlugin = class {
341
+ static {
342
+ __name(this, "SavePlugin");
343
+ }
344
+ name = "save";
345
+ version = "1.0.0";
346
+ options;
347
+ constructor(options) {
348
+ this.options = options ?? {};
349
+ }
350
+ install(context) {
351
+ const storage = this.options.storage ?? new LocalStorageSaveStorage();
352
+ const service = new SaveService(storage, context, this.options.namespace);
353
+ context.register(SaveServiceKey, service);
354
+ }
355
+ };
356
+ // Annotate the CommonJS export names for ESM import in node:
357
+ 0 && (module.exports = {
358
+ LocalStorageSaveStorage,
359
+ SavePlugin,
360
+ SaveService,
361
+ SaveServiceKey,
362
+ VERSION
363
+ });
364
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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":";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAAAA,eAAwB;;;ACGjB,IAAM,0BAAN,MAAqD;AAAA,EAH5D,OAG4D;AAAA;AAAA;AAAA,EAC1D,KAAK,KAA4B;AAC/B,WAAO,aAAa,QAAQ,GAAG;AAAA,EACjC;AAAA,EAEA,KAAK,KAAa,MAAoB;AACpC,iBAAa,QAAQ,KAAK,IAAI;AAAA,EAChC;AAAA,EAEA,OAAO,KAAmB;AACxB,iBAAa,WAAW,GAAG;AAAA,EAC7B;AAAA,EAEA,KAAK,QAA2B;AAC9B,UAAM,SAAmB,CAAC;AAC1B,aAAS,IAAI,GAAG,IAAI,aAAa,QAAQ,KAAK;AAC5C,YAAM,MAAM,aAAa,IAAI,CAAC;AAC9B,UAAI,QAAQ,SAAS,CAAC,UAAU,IAAI,WAAW,MAAM,IAAI;AACvD,eAAO,KAAK,GAAG;AAAA,MACjB;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACF;;;ACzBA,kBAKO;AAYP,IAAM,mBAAmB;AAMzB,IAAM,kBAAkB;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGO,IAAM,cAAN,MAA8D;AAAA,EAtCrE,OAsCqE;AAAA;AAAA;AAAA,EAClD;AAAA,EACA;AAAA,EACA;AAAA,EACT,WAAW;AAAA,EAEnB,YAAY,SAAsB,SAAwB,YAAY,QAAQ;AAC5E,SAAK,UAAU;AACf,SAAK,UAAU;AACf,SAAK,YAAY;AAAA,EACnB;AAAA;AAAA;AAAA,EAKA,aAAa,MAAoB;AAC/B,UAAM,WAAW,KAAK,cAAc;AACpC,SAAK,QAAQ;AAAA,MACX,KAAK,IAAI,YAAY,IAAI;AAAA,MACzB,KAAK,UAAU,QAAQ;AAAA,IACzB;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,aAAa,MAA6B;AAC9C,UAAM,WAAW,KAAK,aAAa,IAAI;AACvC,QAAI,CAAC,UAAU;AACb,YAAM,IAAI,MAAM,0BAA0B,IAAI,IAAI;AAAA,IACpD;AACA,UAAM,KAAK,gBAAgB,QAAQ;AAAA,EACrC;AAAA;AAAA,EAGA,eAAe,MAAmC;AAChD,WAAO,KAAK,aAAa,IAAI;AAAA,EAC/B;AAAA;AAAA,EAGA,MAAM,eAAe,MAAc,UAAuC;AACxE,QAAI,KAAK,UAAU;AACjB,YAAM,IAAI,MAAM,mCAAmC;AAAA,IACrD;AACA,QAAI,SAAS,YAAY,kBAAkB;AACzC,YAAM,IAAI;AAAA,QACR,mCAAmC,gBAAgB,SAAS,SAAS,OAAO;AAAA,MAC9E;AAAA,IACF;AACA,SAAK,QAAQ;AAAA,MACX,KAAK,IAAI,YAAY,IAAI;AAAA,MACzB,KAAK,UAAU,QAAQ;AAAA,IACzB;AACA,UAAM,KAAK,gBAAgB,QAAQ;AAAA,EACrC;AAAA;AAAA;AAAA,EAKA,SAA0C,MAAS,MAAuB;AACxE,SAAK,QAAQ,KAAK,KAAK,IAAI,QAAQ,IAAI,GAAG,KAAK,UAAU,IAAI,CAAC;AAAA,EAChE;AAAA;AAAA,EAGA,SAA0C,MAA2B;AACnE,UAAM,MAAM,KAAK,QAAQ,KAAK,KAAK,IAAI,QAAQ,IAAI,CAAC;AACpD,QAAI,QAAQ,KAAM,QAAO;AACzB,QAAI;AACF,aAAO,KAAK,MAAM,GAAG;AAAA,IACvB,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA,EAGA,WAA4C,MAA2B;AACrE,WAAO,KAAK,SAAS,IAAI;AAAA,EAC3B;AAAA;AAAA,EAGA,WAA4C,MAAS,MAAuB;AAC1E,SAAK,SAAS,MAAM,IAAI;AAAA,EAC1B;AAAA;AAAA;AAAA,EAKA,YAAY,MAAuB;AACjC,WAAO,KAAK,QAAQ,KAAK,KAAK,IAAI,YAAY,IAAI,CAAC,MAAM;AAAA,EAC3D;AAAA;AAAA,EAGA,eAAe,MAAoB;AACjC,SAAK,QAAQ,OAAO,KAAK,IAAI,YAAY,IAAI,CAAC;AAAA,EAChD;AAAA;AAAA;AAAA,EAKA,QAAyC,MAAkB;AACzD,WAAO,KAAK,QAAQ,KAAK,KAAK,IAAI,QAAQ,IAAI,CAAC,MAAM;AAAA,EACvD;AAAA;AAAA,EAGA,WAA4C,MAAe;AACzD,SAAK,QAAQ,OAAO,KAAK,IAAI,QAAQ,IAAI,CAAC;AAAA,EAC5C;AAAA;AAAA,EAIQ,IAAI,QAAgB,MAAsB;AAChD,WAAO,GAAG,KAAK,SAAS,IAAI,MAAM,IAAI,IAAI;AAAA,EAC5C;AAAA,EAEQ,aAAa,MAAmC;AACtD,UAAM,MAAM,KAAK,QAAQ,KAAK,KAAK,IAAI,YAAY,IAAI,CAAC;AACxD,QAAI,QAAQ,KAAM,QAAO;AACzB,QAAI;AACF,aAAO,KAAK,MAAM,GAAG;AAAA,IACvB,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEQ,gBAA8B;AACpC,UAAM,eAAe,KAAK,QAAQ,QAAQ,2BAAe;AACzD,UAAM,SAA+B,CAAC;AAEtC,eAAW,SAAS,aAAa,KAAK;AACpC,UAAI,KAAC,4BAAe,KAAK,EAAG;AAC5B,YAAM,WAAO,iCAAoB,KAAK;AACtC,UAAI,CAAC,KAAM;AAEX,YAAM,WAAkC,CAAC;AACzC,iBAAW,UAAU,MAAM,YAAY,GAAG;AACxC,YAAI,KAAC,4BAAe,MAAM,EAAG;AAC7B,iBAAS,KAAK,KAAK,gBAAgB,MAAM,CAAC;AAAA,MAC5C;AAEA,YAAM,WAAW,MAAM,YAAY;AAEnC,aAAO,KAAK;AAAA,QACV;AAAA,QACA,QAAQ,MAAM;AAAA,QACd;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,MACL,SAAS;AAAA,MACT,WAAW,KAAK,IAAI;AAAA,MACpB;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,gBAAgB,UAAuC;AACnE,QAAI,KAAK,UAAU;AACjB,YAAM,IAAI,MAAM,mCAAmC;AAAA,IACrD;AACA,SAAK,WAAW;AAChB,QAAI;AACF,UAAI,SAAS,YAAY,kBAAkB;AACzC,cAAM,IAAI;AAAA,UACR,mCAAmC,gBAAgB,SAAS,SAAS,OAAO;AAAA,QAC9E;AAAA,MACF;AAEA,YAAM,eAAe,KAAK,QAAQ,QAAQ,2BAAe;AACzD,mBAAa,MAAM;AAEnB,iBAAW,SAAS,SAAS,QAAQ;AACnC,cAAM,aAAa,iCAAqB,IAAI,MAAM,IAAI;AAGtD,YAAI,CAAC,YAAY;AACf,gBAAM,IAAI;AAAA,YACR,2BAA2B,MAAM,IAAI;AAAA,UAEvC;AAAA,QACF;AAEA,cAAM,QAAQ,IAAI,WAAW;AAG7B,cAAM,UAAU,MAAM;AACpB,eAAK,qBAAqB,OAAO,KAAK;AAAA,QACxC;AAEA,cAAM,aAAa,KAAK,KAAK;AAE7B,YAAI,MAAM,QAAQ;AAChB,gBAAM,SAAS;AAAA,QACjB;AAAA,MACF;AAAA,IACF,UAAE;AACA,WAAK,WAAW;AAAA,IAClB;AAAA,EACF;AAAA,EAEQ,qBACN,OACA,OACM;AAEN,UAAM,QAAQ,oBAAI,IAAoB;AACtC,UAAM,gBAID,CAAC;AAEN,eAAW,eAAe,MAAM,UAAU;AACxC,YAAM,cAAc,iCAAqB,IAAI,YAAY,IAAI;AAG7D,UAAI,CAAC,aAAa;AAChB,gBAAQ;AAAA,UACN,gBAAgB,YAAY,IAAI;AAAA,QAClC;AACA;AAAA,MACF;AAEA,YAAM,SAAS,IAAI,YAAY;AAC/B,YAAM,mBAAmB,MAAM;AAC/B,YAAM,qBAAqB,KAAK;AAAA,QAC9B;AAAA,QACA,YAAY;AAAA,MACd;AAEA,YAAM,IAAI,YAAY,IAAI,MAAM;AAChC,oBAAc,KAAK,EAAE,QAAQ,OAAO,aAAa,mBAAmB,CAAC;AAAA,IACvE;AAGA,eAAW,EAAE,QAAQ,OAAO,YAAY,KAAK,eAAe;AAC1D,UAAI,YAAY,YAAY,QAAQ,YAAY,aAAa,MAAM;AACjE,cAAM,SAAS,MAAM,IAAI,YAAY,QAAQ;AAC7C,YAAI,QAAQ;AACV,iBAAO,SAAS,YAAY,WAAW,MAAM;AAAA,QAC/C,OAAO;AACL,kBAAQ;AAAA,YACN,2BAA2B,YAAY,QAAQ,0BAA0B,OAAO,IAAI;AAAA,UACtF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAGA,UAAM,WAA6B;AAAA,MACjC,OAAO,SAAiB;AACtB,eAAO,MAAM,IAAI,OAAO,KAAK;AAAA,MAC/B;AAAA,IACF;AAGA,eAAW,EAAE,QAAQ,OAAO,aAAa,mBAAmB,KAAK,eAAe;AAC9E,iBAAW,EAAE,WAAW,KAAK,KAAK,oBAAoB;AACpD,kBAAU,eAAe,MAAM,QAAQ;AAAA,MACzC;AAEA,aAAO,eAAe,YAAY,UAAU,QAAQ;AAAA,IACtD;AAEA,UAAM,eAAe,MAAM,UAAU,QAAQ;AAAA,EAC/C;AAAA,EAEQ,gBAAgB,QAAqC;AAC3D,UAAM,WAAO,iCAAoB,MAAM;AACvC,QAAI,CAAC,KAAM,OAAM,IAAI,MAAM,4BAA4B;AAEvD,UAAM,aAAkC,CAAC;AACzC,eAAW,aAAa,OAAO,OAAO,GAAG;AACvC,UAAI,OAAO,UAAU,cAAc,WAAY;AAC/C,YAAM,OAAO,UAAU,UAAU;AACjC,UAAI,QAAQ,KAAM;AAClB,YAAM,eACJ,iCAAoB,SAAS,KAAK,UAAU,YAAY;AAC1D,iBAAW,KAAK,EAAE,MAAM,UAAU,KAAK,CAAC;AAAA,IAC1C;AAEA,UAAM,WAAW,OAAO,YAAY;AAEpC,UAAM,SAA8B;AAAA,MAClC,IAAI,OAAO;AAAA,MACX;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAGA,QAAI,OAAO,cAAU,4BAAe,OAAO,MAAM,GAAG;AAClD,aAAO,WAAW,OAAO,OAAO;AAEhC,iBAAW,CAAC,MAAM,KAAK,KAAK,OAAO,OAAO,UAAU;AAClD,YAAI,UAAU,QAAQ;AACpB,iBAAO,YAAY;AACnB;AAAA,QACF;AAAA,MACF;AAAA,IACF,WAAW,OAAO,QAAQ;AACxB,cAAQ;AAAA,QACN,WAAW,OAAO,IAAI,kCAAkC,OAAO,OAAO,IAAI;AAAA,MAC5E;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,wBACN,QACA,WACgD;AAChD,UAAM,SAAS,CAAC,GAAG,SAAS,EAAE,KAAK,CAAC,GAAG,MAAM;AAC3C,YAAM,KAAK,gBAAgB,QAAQ,EAAE,IAAI;AACzC,YAAM,KAAK,gBAAgB,QAAQ,EAAE,IAAI;AACzC,cAAQ,MAAM,IAAI,KAAK,QAAQ,MAAM,IAAI,KAAK;AAAA,IAChD,CAAC;AAED,UAAM,WAA2D,CAAC;AAElE,eAAW,QAAQ,QAAQ;AACzB,YAAM,YAAY,iCAAqB,IAAI,KAAK,IAAI;AAMpD,UAAI,CAAC,aAAa,OAAO,UAAU,iBAAiB,YAAY;AAE9D;AAAA,MACF;AAEA,YAAM,YAAY,UAAU,aAAa,KAAK,IAAI;AAClD,aAAO,IAAI,SAAS;AACpB,eAAS,KAAK,EAAE,WAAW,MAAM,KAAK,KAAK,CAAC;AAAA,IAC9C;AAEA,WAAO;AAAA,EACT;AACF;;;ACxXA,IAAAC,eAA2B;AAIpB,IAAM,iBAAiB,IAAI,wBAAwB,aAAa;;;ACWhE,IAAM,aAAN,MAAmC;AAAA,EAf1C,OAe0C;AAAA;AAAA;AAAA,EAC/B,OAAO;AAAA,EACP,UAAU;AAAA,EAEF;AAAA,EAEjB,YAAY,SAA6B;AACvC,SAAK,UAAU,WAAW,CAAC;AAAA,EAC7B;AAAA,EAEA,QAAQ,SAA8B;AACpC,UAAM,UAAU,KAAK,QAAQ,WAAW,IAAI,wBAAwB;AACpE,UAAM,UAAU,IAAI,YAAY,SAAS,SAAS,KAAK,QAAQ,SAAS;AAExE,YAAQ,SAAS,gBAAgB,OAAO;AAAA,EAC1C;AACF;","names":["import_core","import_core"]}
@@ -0,0 +1,120 @@
1
+ import { EngineContext, Plugin, ServiceKey } from '@yagejs/core';
2
+ export { SnapshotResolver, VERSION } from '@yagejs/core';
3
+
4
+ type UntypedSlots = Record<string, any>;
5
+ /** Pluggable storage backend for save data. */
6
+ interface SaveStorage {
7
+ load(key: string): string | null;
8
+ save(key: string, data: string): void;
9
+ delete(key: string): void;
10
+ /** Return all keys, optionally filtered to those starting with `prefix`. */
11
+ list(prefix?: string): string[];
12
+ }
13
+ /** Complete snapshot of the game state. */
14
+ interface GameSnapshot {
15
+ version: number;
16
+ timestamp: number;
17
+ scenes: SceneSnapshotEntry[];
18
+ }
19
+ /** Serialized state for a single scene in the stack. */
20
+ interface SceneSnapshotEntry {
21
+ /** Scene type identifier (from @serializable decorator). */
22
+ type: string;
23
+ /** Whether the scene was paused at save time. */
24
+ paused: boolean;
25
+ /** Auto-collected entity snapshots. */
26
+ entities: EntitySnapshotEntry[];
27
+ /** User-defined scene data (from scene.serialize()). */
28
+ userData?: unknown;
29
+ }
30
+ /** Serialized state for a single entity. */
31
+ interface EntitySnapshotEntry {
32
+ /** Entity runtime ID at save time (used to restore references). */
33
+ id: number;
34
+ /** Entity type identifier (from @serializable decorator). */
35
+ type: string;
36
+ /** Auto-collected component snapshots. */
37
+ components: ComponentSnapshot[];
38
+ /** User-defined entity data (from entity.serialize()). */
39
+ userData?: unknown;
40
+ /** Save-time ID of the parent entity, if this is a child. */
41
+ parentId?: number;
42
+ /** The name this entity was registered under in parent.addChild(). */
43
+ childName?: string;
44
+ }
45
+ /** Serialized state for a single component. */
46
+ interface ComponentSnapshot {
47
+ /** Component class name. */
48
+ type: string;
49
+ /** Data from component.serialize(). */
50
+ data: unknown;
51
+ }
52
+
53
+ /** SaveStorage backed by browser localStorage. */
54
+ declare class LocalStorageSaveStorage implements SaveStorage {
55
+ load(key: string): string | null;
56
+ save(key: string, data: string): void;
57
+ delete(key: string): void;
58
+ list(prefix?: string): string[];
59
+ }
60
+
61
+ /** Orchestrates full game-state serialization and hydration. */
62
+ declare class SaveService<TSlots extends UntypedSlots = UntypedSlots> {
63
+ private readonly storage;
64
+ private readonly context;
65
+ private readonly namespace;
66
+ private _loading;
67
+ constructor(storage: SaveStorage, context: EngineContext, namespace?: string);
68
+ /** Save a snapshot of the current scene stack to the given slot. */
69
+ saveSnapshot(slot: string): void;
70
+ /** Load a snapshot from the given slot, rebuilding the scene stack. */
71
+ loadSnapshot(slot: string): Promise<void>;
72
+ /** Export a previously saved snapshot from the given slot. */
73
+ exportSnapshot(slot: string): GameSnapshot | null;
74
+ /** Import a snapshot into the given slot and hydrate the scene stack. */
75
+ importSnapshot(slot: string, snapshot: GameSnapshot): Promise<void>;
76
+ /** Save arbitrary structured data to a named slot. */
77
+ saveData<K extends keyof TSlots & string>(slot: K, data: TSlots[K]): void;
78
+ /** Load structured data from a named slot. Returns null if not found. */
79
+ loadData<K extends keyof TSlots & string>(slot: K): TSlots[K] | null;
80
+ /** Read data from a slot for external use (cloud upload, file export). Alias for `loadData`. */
81
+ exportData<K extends keyof TSlots & string>(slot: K): TSlots[K] | null;
82
+ /** Write externally-sourced data into a slot. Alias for `saveData` — no version check or hydration. */
83
+ importData<K extends keyof TSlots & string>(slot: K, data: TSlots[K]): void;
84
+ /** Check if a snapshot exists in the given slot. */
85
+ hasSnapshot(slot: string): boolean;
86
+ /** Delete a snapshot from the given slot. */
87
+ deleteSnapshot(slot: string): void;
88
+ /** Check if user data exists in the given slot. */
89
+ hasData<K extends keyof TSlots & string>(slot: K): boolean;
90
+ /** Delete user data from the given slot. */
91
+ deleteData<K extends keyof TSlots & string>(slot: K): void;
92
+ private key;
93
+ private readSnapshot;
94
+ private buildSnapshot;
95
+ private hydrateSnapshot;
96
+ private restoreSceneEntities;
97
+ private serializeEntity;
98
+ private restoreEntityComponents;
99
+ }
100
+
101
+ /** Options for the SavePlugin. */
102
+ interface SavePluginOptions {
103
+ /** Custom storage backend. Defaults to LocalStorageSaveStorage. */
104
+ storage?: SaveStorage;
105
+ /** Namespace for stored keys. Defaults to "yage". */
106
+ namespace?: string;
107
+ }
108
+ /** Plugin that registers SaveService into the engine context. */
109
+ declare class SavePlugin implements Plugin {
110
+ readonly name = "save";
111
+ readonly version = "1.0.0";
112
+ private readonly options;
113
+ constructor(options?: SavePluginOptions);
114
+ install(context: EngineContext): void;
115
+ }
116
+
117
+ /** Service key for the SaveService. */
118
+ declare const SaveServiceKey: ServiceKey<SaveService<UntypedSlots>>;
119
+
120
+ export { type ComponentSnapshot, type EntitySnapshotEntry, type GameSnapshot, LocalStorageSaveStorage, SavePlugin, type SavePluginOptions, SaveService, SaveServiceKey, type SaveStorage, type SceneSnapshotEntry, type UntypedSlots };
@@ -0,0 +1,120 @@
1
+ import { EngineContext, Plugin, ServiceKey } from '@yagejs/core';
2
+ export { SnapshotResolver, VERSION } from '@yagejs/core';
3
+
4
+ type UntypedSlots = Record<string, any>;
5
+ /** Pluggable storage backend for save data. */
6
+ interface SaveStorage {
7
+ load(key: string): string | null;
8
+ save(key: string, data: string): void;
9
+ delete(key: string): void;
10
+ /** Return all keys, optionally filtered to those starting with `prefix`. */
11
+ list(prefix?: string): string[];
12
+ }
13
+ /** Complete snapshot of the game state. */
14
+ interface GameSnapshot {
15
+ version: number;
16
+ timestamp: number;
17
+ scenes: SceneSnapshotEntry[];
18
+ }
19
+ /** Serialized state for a single scene in the stack. */
20
+ interface SceneSnapshotEntry {
21
+ /** Scene type identifier (from @serializable decorator). */
22
+ type: string;
23
+ /** Whether the scene was paused at save time. */
24
+ paused: boolean;
25
+ /** Auto-collected entity snapshots. */
26
+ entities: EntitySnapshotEntry[];
27
+ /** User-defined scene data (from scene.serialize()). */
28
+ userData?: unknown;
29
+ }
30
+ /** Serialized state for a single entity. */
31
+ interface EntitySnapshotEntry {
32
+ /** Entity runtime ID at save time (used to restore references). */
33
+ id: number;
34
+ /** Entity type identifier (from @serializable decorator). */
35
+ type: string;
36
+ /** Auto-collected component snapshots. */
37
+ components: ComponentSnapshot[];
38
+ /** User-defined entity data (from entity.serialize()). */
39
+ userData?: unknown;
40
+ /** Save-time ID of the parent entity, if this is a child. */
41
+ parentId?: number;
42
+ /** The name this entity was registered under in parent.addChild(). */
43
+ childName?: string;
44
+ }
45
+ /** Serialized state for a single component. */
46
+ interface ComponentSnapshot {
47
+ /** Component class name. */
48
+ type: string;
49
+ /** Data from component.serialize(). */
50
+ data: unknown;
51
+ }
52
+
53
+ /** SaveStorage backed by browser localStorage. */
54
+ declare class LocalStorageSaveStorage implements SaveStorage {
55
+ load(key: string): string | null;
56
+ save(key: string, data: string): void;
57
+ delete(key: string): void;
58
+ list(prefix?: string): string[];
59
+ }
60
+
61
+ /** Orchestrates full game-state serialization and hydration. */
62
+ declare class SaveService<TSlots extends UntypedSlots = UntypedSlots> {
63
+ private readonly storage;
64
+ private readonly context;
65
+ private readonly namespace;
66
+ private _loading;
67
+ constructor(storage: SaveStorage, context: EngineContext, namespace?: string);
68
+ /** Save a snapshot of the current scene stack to the given slot. */
69
+ saveSnapshot(slot: string): void;
70
+ /** Load a snapshot from the given slot, rebuilding the scene stack. */
71
+ loadSnapshot(slot: string): Promise<void>;
72
+ /** Export a previously saved snapshot from the given slot. */
73
+ exportSnapshot(slot: string): GameSnapshot | null;
74
+ /** Import a snapshot into the given slot and hydrate the scene stack. */
75
+ importSnapshot(slot: string, snapshot: GameSnapshot): Promise<void>;
76
+ /** Save arbitrary structured data to a named slot. */
77
+ saveData<K extends keyof TSlots & string>(slot: K, data: TSlots[K]): void;
78
+ /** Load structured data from a named slot. Returns null if not found. */
79
+ loadData<K extends keyof TSlots & string>(slot: K): TSlots[K] | null;
80
+ /** Read data from a slot for external use (cloud upload, file export). Alias for `loadData`. */
81
+ exportData<K extends keyof TSlots & string>(slot: K): TSlots[K] | null;
82
+ /** Write externally-sourced data into a slot. Alias for `saveData` — no version check or hydration. */
83
+ importData<K extends keyof TSlots & string>(slot: K, data: TSlots[K]): void;
84
+ /** Check if a snapshot exists in the given slot. */
85
+ hasSnapshot(slot: string): boolean;
86
+ /** Delete a snapshot from the given slot. */
87
+ deleteSnapshot(slot: string): void;
88
+ /** Check if user data exists in the given slot. */
89
+ hasData<K extends keyof TSlots & string>(slot: K): boolean;
90
+ /** Delete user data from the given slot. */
91
+ deleteData<K extends keyof TSlots & string>(slot: K): void;
92
+ private key;
93
+ private readSnapshot;
94
+ private buildSnapshot;
95
+ private hydrateSnapshot;
96
+ private restoreSceneEntities;
97
+ private serializeEntity;
98
+ private restoreEntityComponents;
99
+ }
100
+
101
+ /** Options for the SavePlugin. */
102
+ interface SavePluginOptions {
103
+ /** Custom storage backend. Defaults to LocalStorageSaveStorage. */
104
+ storage?: SaveStorage;
105
+ /** Namespace for stored keys. Defaults to "yage". */
106
+ namespace?: string;
107
+ }
108
+ /** Plugin that registers SaveService into the engine context. */
109
+ declare class SavePlugin implements Plugin {
110
+ readonly name = "save";
111
+ readonly version = "1.0.0";
112
+ private readonly options;
113
+ constructor(options?: SavePluginOptions);
114
+ install(context: EngineContext): void;
115
+ }
116
+
117
+ /** Service key for the SaveService. */
118
+ declare const SaveServiceKey: ServiceKey<SaveService<UntypedSlots>>;
119
+
120
+ export { type ComponentSnapshot, type EntitySnapshotEntry, type GameSnapshot, LocalStorageSaveStorage, SavePlugin, type SavePluginOptions, SaveService, SaveServiceKey, type SaveStorage, type SceneSnapshotEntry, type UntypedSlots };
package/dist/index.js ADDED
@@ -0,0 +1,342 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ // src/index.ts
5
+ import { VERSION } from "@yagejs/core";
6
+
7
+ // src/LocalStorageAdapter.ts
8
+ var LocalStorageSaveStorage = class {
9
+ static {
10
+ __name(this, "LocalStorageSaveStorage");
11
+ }
12
+ load(key) {
13
+ return localStorage.getItem(key);
14
+ }
15
+ save(key, data) {
16
+ localStorage.setItem(key, data);
17
+ }
18
+ delete(key) {
19
+ localStorage.removeItem(key);
20
+ }
21
+ list(prefix) {
22
+ const result = [];
23
+ for (let i = 0; i < localStorage.length; i++) {
24
+ const key = localStorage.key(i);
25
+ if (key !== null && (!prefix || key.startsWith(prefix))) {
26
+ result.push(key);
27
+ }
28
+ }
29
+ return result;
30
+ }
31
+ };
32
+
33
+ // src/SaveService.ts
34
+ import {
35
+ SceneManagerKey,
36
+ SerializableRegistry,
37
+ isSerializable,
38
+ getSerializableType
39
+ } from "@yagejs/core";
40
+ var SNAPSHOT_VERSION = 3;
41
+ var COMPONENT_ORDER = [
42
+ "Transform",
43
+ "RigidBodyComponent",
44
+ "ColliderComponent",
45
+ "SpriteComponent",
46
+ "GraphicsComponent",
47
+ "AnimatedSpriteComponent",
48
+ "AnimationController",
49
+ "SoundComponent",
50
+ "ParticleEmitterComponent",
51
+ "TilemapComponent"
52
+ ];
53
+ var SaveService = class {
54
+ static {
55
+ __name(this, "SaveService");
56
+ }
57
+ storage;
58
+ context;
59
+ namespace;
60
+ _loading = false;
61
+ constructor(storage, context, namespace = "yage") {
62
+ this.storage = storage;
63
+ this.context = context;
64
+ this.namespace = namespace;
65
+ }
66
+ // ---- Snapshot API ----
67
+ /** Save a snapshot of the current scene stack to the given slot. */
68
+ saveSnapshot(slot) {
69
+ const snapshot = this.buildSnapshot();
70
+ this.storage.save(
71
+ this.key("snapshot", slot),
72
+ JSON.stringify(snapshot)
73
+ );
74
+ }
75
+ /** Load a snapshot from the given slot, rebuilding the scene stack. */
76
+ async loadSnapshot(slot) {
77
+ const snapshot = this.readSnapshot(slot);
78
+ if (!snapshot) {
79
+ throw new Error(`No save found in slot "${slot}".`);
80
+ }
81
+ await this.hydrateSnapshot(snapshot);
82
+ }
83
+ /** Export a previously saved snapshot from the given slot. */
84
+ exportSnapshot(slot) {
85
+ return this.readSnapshot(slot);
86
+ }
87
+ /** Import a snapshot into the given slot and hydrate the scene stack. */
88
+ async importSnapshot(slot, snapshot) {
89
+ if (this._loading) {
90
+ throw new Error("loadSnapshot already in progress.");
91
+ }
92
+ if (snapshot.version !== SNAPSHOT_VERSION) {
93
+ throw new Error(
94
+ `Save version mismatch: expected ${SNAPSHOT_VERSION}, got ${snapshot.version}.`
95
+ );
96
+ }
97
+ this.storage.save(
98
+ this.key("snapshot", slot),
99
+ JSON.stringify(snapshot)
100
+ );
101
+ await this.hydrateSnapshot(snapshot);
102
+ }
103
+ // ---- User Data API ----
104
+ /** Save arbitrary structured data to a named slot. */
105
+ saveData(slot, data) {
106
+ this.storage.save(this.key("data", slot), JSON.stringify(data));
107
+ }
108
+ /** Load structured data from a named slot. Returns null if not found. */
109
+ loadData(slot) {
110
+ const raw = this.storage.load(this.key("data", slot));
111
+ if (raw === null) return null;
112
+ try {
113
+ return JSON.parse(raw);
114
+ } catch {
115
+ return null;
116
+ }
117
+ }
118
+ /** Read data from a slot for external use (cloud upload, file export). Alias for `loadData`. */
119
+ exportData(slot) {
120
+ return this.loadData(slot);
121
+ }
122
+ /** Write externally-sourced data into a slot. Alias for `saveData` — no version check or hydration. */
123
+ importData(slot, data) {
124
+ this.saveData(slot, data);
125
+ }
126
+ // ---- Snapshot management ----
127
+ /** Check if a snapshot exists in the given slot. */
128
+ hasSnapshot(slot) {
129
+ return this.storage.load(this.key("snapshot", slot)) !== null;
130
+ }
131
+ /** Delete a snapshot from the given slot. */
132
+ deleteSnapshot(slot) {
133
+ this.storage.delete(this.key("snapshot", slot));
134
+ }
135
+ // ---- Data management ----
136
+ /** Check if user data exists in the given slot. */
137
+ hasData(slot) {
138
+ return this.storage.load(this.key("data", slot)) !== null;
139
+ }
140
+ /** Delete user data from the given slot. */
141
+ deleteData(slot) {
142
+ this.storage.delete(this.key("data", slot));
143
+ }
144
+ // ---- Private helpers ----
145
+ key(prefix, slot) {
146
+ return `${this.namespace}:${prefix}:${slot}`;
147
+ }
148
+ readSnapshot(slot) {
149
+ const raw = this.storage.load(this.key("snapshot", slot));
150
+ if (raw === null) return null;
151
+ try {
152
+ return JSON.parse(raw);
153
+ } catch {
154
+ return null;
155
+ }
156
+ }
157
+ buildSnapshot() {
158
+ const sceneManager = this.context.resolve(SceneManagerKey);
159
+ const scenes = [];
160
+ for (const scene of sceneManager.all) {
161
+ if (!isSerializable(scene)) continue;
162
+ const type = getSerializableType(scene);
163
+ if (!type) continue;
164
+ const entities = [];
165
+ for (const entity of scene.getEntities()) {
166
+ if (!isSerializable(entity)) continue;
167
+ entities.push(this.serializeEntity(entity));
168
+ }
169
+ const userData = scene.serialize?.();
170
+ scenes.push({
171
+ type,
172
+ paused: scene.paused,
173
+ entities,
174
+ userData
175
+ });
176
+ }
177
+ return {
178
+ version: SNAPSHOT_VERSION,
179
+ timestamp: Date.now(),
180
+ scenes
181
+ };
182
+ }
183
+ async hydrateSnapshot(snapshot) {
184
+ if (this._loading) {
185
+ throw new Error("loadSnapshot already in progress.");
186
+ }
187
+ this._loading = true;
188
+ try {
189
+ if (snapshot.version !== SNAPSHOT_VERSION) {
190
+ throw new Error(
191
+ `Save version mismatch: expected ${SNAPSHOT_VERSION}, got ${snapshot.version}.`
192
+ );
193
+ }
194
+ const sceneManager = this.context.resolve(SceneManagerKey);
195
+ sceneManager.clear();
196
+ for (const entry of snapshot.scenes) {
197
+ const SceneClass = SerializableRegistry.get(entry.type);
198
+ if (!SceneClass) {
199
+ throw new Error(
200
+ `Cannot load scene type "${entry.type}". Ensure the scene class is decorated with @serializable.`
201
+ );
202
+ }
203
+ const scene = new SceneClass();
204
+ scene.onEnter = () => {
205
+ this.restoreSceneEntities(scene, entry);
206
+ };
207
+ await sceneManager.push(scene);
208
+ if (entry.paused) {
209
+ scene.paused = true;
210
+ }
211
+ }
212
+ } finally {
213
+ this._loading = false;
214
+ }
215
+ }
216
+ restoreSceneEntities(scene, entry) {
217
+ const idMap = /* @__PURE__ */ new Map();
218
+ const entityEntries = [];
219
+ for (const entityEntry of entry.entities) {
220
+ const EntityClass = SerializableRegistry.get(entityEntry.type);
221
+ if (!EntityClass) {
222
+ console.warn(
223
+ `Entity type "${entityEntry.type}" not found in registry \u2014 skipping.`
224
+ );
225
+ continue;
226
+ }
227
+ const entity = new EntityClass();
228
+ scene._addExistingEntity(entity);
229
+ const restoredComponents = this.restoreEntityComponents(
230
+ entity,
231
+ entityEntry.components
232
+ );
233
+ idMap.set(entityEntry.id, entity);
234
+ entityEntries.push({ entity, entry: entityEntry, restoredComponents });
235
+ }
236
+ for (const { entity, entry: entityEntry } of entityEntries) {
237
+ if (entityEntry.parentId != null && entityEntry.childName != null) {
238
+ const parent = idMap.get(entityEntry.parentId);
239
+ if (parent) {
240
+ parent.addChild(entityEntry.childName, entity);
241
+ } else {
242
+ console.warn(
243
+ `Parent entity (saved id ${entityEntry.parentId}) not found for child "${entity.name}" \u2014 restoring as root entity.`
244
+ );
245
+ }
246
+ }
247
+ }
248
+ const resolver = {
249
+ entity(savedId) {
250
+ return idMap.get(savedId) ?? null;
251
+ }
252
+ };
253
+ for (const { entity, entry: entityEntry, restoredComponents } of entityEntries) {
254
+ for (const { component, data } of restoredComponents) {
255
+ component.afterRestore?.(data, resolver);
256
+ }
257
+ entity.afterRestore?.(entityEntry.userData, resolver);
258
+ }
259
+ scene.afterRestore?.(entry.userData, resolver);
260
+ }
261
+ serializeEntity(entity) {
262
+ const type = getSerializableType(entity);
263
+ if (!type) throw new Error("Entity is not serializable");
264
+ const components = [];
265
+ for (const component of entity.getAll()) {
266
+ if (typeof component.serialize !== "function") continue;
267
+ const data = component.serialize();
268
+ if (data == null) continue;
269
+ const compType = getSerializableType(component) ?? component.constructor.name;
270
+ components.push({ type: compType, data });
271
+ }
272
+ const userData = entity.serialize?.();
273
+ const result = {
274
+ id: entity.id,
275
+ type,
276
+ components,
277
+ userData
278
+ };
279
+ if (entity.parent && isSerializable(entity.parent)) {
280
+ result.parentId = entity.parent.id;
281
+ for (const [name, child] of entity.parent.children) {
282
+ if (child === entity) {
283
+ result.childName = name;
284
+ break;
285
+ }
286
+ }
287
+ } else if (entity.parent) {
288
+ console.warn(
289
+ `Entity "${entity.name}" has non-serializable parent "${entity.parent.name}" \u2014 parent/child relationship will not be saved.`
290
+ );
291
+ }
292
+ return result;
293
+ }
294
+ restoreEntityComponents(entity, snapshots) {
295
+ const sorted = [...snapshots].sort((a, b) => {
296
+ const ai = COMPONENT_ORDER.indexOf(a.type);
297
+ const bi = COMPONENT_ORDER.indexOf(b.type);
298
+ return (ai >= 0 ? ai : 999) - (bi >= 0 ? bi : 999);
299
+ });
300
+ const restored = [];
301
+ for (const snap of sorted) {
302
+ const CompClass = SerializableRegistry.get(snap.type);
303
+ if (!CompClass || typeof CompClass.fromSnapshot !== "function") {
304
+ continue;
305
+ }
306
+ const component = CompClass.fromSnapshot(snap.data);
307
+ entity.add(component);
308
+ restored.push({ component, data: snap.data });
309
+ }
310
+ return restored;
311
+ }
312
+ };
313
+
314
+ // src/keys.ts
315
+ import { ServiceKey } from "@yagejs/core";
316
+ var SaveServiceKey = new ServiceKey("saveService");
317
+
318
+ // src/SavePlugin.ts
319
+ var SavePlugin = class {
320
+ static {
321
+ __name(this, "SavePlugin");
322
+ }
323
+ name = "save";
324
+ version = "1.0.0";
325
+ options;
326
+ constructor(options) {
327
+ this.options = options ?? {};
328
+ }
329
+ install(context) {
330
+ const storage = this.options.storage ?? new LocalStorageSaveStorage();
331
+ const service = new SaveService(storage, context, this.options.namespace);
332
+ context.register(SaveServiceKey, service);
333
+ }
334
+ };
335
+ export {
336
+ LocalStorageSaveStorage,
337
+ SavePlugin,
338
+ SaveService,
339
+ SaveServiceKey,
340
+ VERSION
341
+ };
342
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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":[]}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@yagejs/save",
3
+ "version": "0.1.0",
4
+ "description": "Save and load game state for YAGE",
5
+ "keywords": [
6
+ "yage",
7
+ "game-engine",
8
+ "2d",
9
+ "typescript",
10
+ "save",
11
+ "load",
12
+ "serialization",
13
+ "snapshot"
14
+ ],
15
+ "license": "MIT",
16
+ "author": "Marco Lepore",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/marco-lepore/yage.git",
20
+ "directory": "packages/save"
21
+ },
22
+ "homepage": "https://yage.dev",
23
+ "type": "module",
24
+ "main": "./dist/index.cjs",
25
+ "module": "./dist/index.js",
26
+ "types": "./dist/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "import": {
30
+ "types": "./dist/index.d.ts",
31
+ "default": "./dist/index.js"
32
+ },
33
+ "require": {
34
+ "types": "./dist/index.d.cts",
35
+ "default": "./dist/index.cjs"
36
+ }
37
+ }
38
+ },
39
+ "files": [
40
+ "dist"
41
+ ],
42
+ "scripts": {
43
+ "build": "tsup",
44
+ "dev": "tsup --watch",
45
+ "typecheck": "tsc --noEmit",
46
+ "lint": "eslint src/",
47
+ "test": "vitest run",
48
+ "clean": "rm -rf dist"
49
+ },
50
+ "dependencies": {
51
+ "@yagejs/core": "^0.1.0"
52
+ }
53
+ }