@rohal12/spindle 0.50.0 → 0.51.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/src/store.ts CHANGED
@@ -17,12 +17,17 @@ import type {
17
17
  import { isSaveExport } from './saves/types';
18
18
  import { executeStoryInit } from './story-init';
19
19
  import { emit } from './event-emitter';
20
- import { resetTriggers } from './triggers';
20
+ import {
21
+ resetTriggers,
22
+ checkTriggersOnNavigation,
23
+ reinitTriggerState,
24
+ } from './triggers';
21
25
  import {
22
26
  initSaveSystem,
23
27
  startNewPlaythrough,
24
28
  getCurrentPlaythroughId,
25
29
  quickSave,
30
+ saveWithHooks,
26
31
  loadQuickSave,
27
32
  populateKnownSaves,
28
33
  getSlotSaveInfo,
@@ -36,7 +41,7 @@ import {
36
41
  clearAllData as smClearAllData,
37
42
  deletePlaythroughData as smDeletePlaythroughData,
38
43
  } from './saves/save-manager';
39
- import { deepClone, serialize, deserialize } from './class-registry';
44
+ import { deepClone, serialize } from './class-registry';
40
45
  import {
41
46
  snapshotPRNG,
42
47
  restorePRNG,
@@ -106,6 +111,19 @@ function computeVarPatches(
106
111
  return { forward: clonePatches(forward), inverse: clonePatches(inverse) };
107
112
  }
108
113
 
114
+ /**
115
+ * Replace the variable snapshot recorded for the newest history moment
116
+ * (e.g. with changes made by watchers reacting to the navigation into it).
117
+ */
118
+ function rerecordNewestMoment(vars: Record<string, unknown>): void {
119
+ const last = patchEntries.length - 1;
120
+ if (last < 0) {
121
+ variableBase = vars;
122
+ } else {
123
+ patchEntries[last] = computeVarPatches(reconstructVarsAt(last), vars);
124
+ }
125
+ }
126
+
109
127
  /** Reconstruct variables at a given history moment by replaying patches. */
110
128
  function reconstructVarsAt(index: number): Record<string, unknown> {
111
129
  let vars: Record<string, unknown> = variableBase;
@@ -133,7 +151,8 @@ function persistSession(get: () => StoryState): void {
133
151
  } = get();
134
152
  if (!storyData) return;
135
153
 
136
- // Trim cache when history was truncated (goBack then navigate)
154
+ // Trim cache when history shrank (navigate() drops discarded forward
155
+ // moments itself, since a replacement branch may keep the same length)
137
156
  if (serializedHistory.length > history.length) {
138
157
  serializedHistory.length = history.length;
139
158
  }
@@ -156,10 +175,9 @@ function persistSession(get: () => StoryState): void {
156
175
  for (let i = 0; i < history.length; i++) {
157
176
  if (i > 0) vars = applyPatches(vars, patchEntries[i - 1]!.forward);
158
177
  if (i >= serializedHistory.length) {
159
- const v = i === history.length - 1 ? variables : vars;
160
178
  serializedHistory[i] = {
161
179
  passage: history[i]!.passage,
162
- variables: serialize(v),
180
+ variables: serialize(vars),
163
181
  timestamp: history[i]!.timestamp,
164
182
  prng: history[i]!.prng,
165
183
  };
@@ -179,6 +197,12 @@ function persistSession(get: () => StoryState): void {
179
197
  });
180
198
  }
181
199
 
200
+ /** True while navigate() lets watchers react to the moment it entered. */
201
+ let navigationTriggerPhase = false;
202
+
203
+ /** Navigations requested during the trigger phase, run once it is over. */
204
+ let deferredNavigations: string[] = [];
205
+
182
206
  /** Reset all module-level state (called on init, restart, loadFromPayload). */
183
207
  function resetModuleState(base: Record<string, unknown>): void {
184
208
  variableBase = base;
@@ -187,6 +211,27 @@ function resetModuleState(base: Record<string, unknown>): void {
187
211
  serializedHistory = [];
188
212
  }
189
213
 
214
+ /**
215
+ * Record the state StoryInit left behind as the start moment: its variable
216
+ * snapshot (the history base) and PRNG state. Called by executeStoryInit()
217
+ * after the StoryInit passage has rendered; init()/restart() record the
218
+ * start moment before StoryInit runs. A no-op once the story has moved on.
219
+ */
220
+ export function recordStoryInitState(): void {
221
+ const { history, historyIndex, variables } = useStoryStore.getState();
222
+ if (history.length !== 1 || historyIndex !== 0) return;
223
+
224
+ variableBase = variables;
225
+ patchEntries = [];
226
+ lastNavigationVars = variables;
227
+ serializedHistory = [];
228
+
229
+ const prng = snapshotPRNG();
230
+ useStoryStore.setState((state) => {
231
+ state.history[0]!.prng = prng;
232
+ });
233
+ }
234
+
190
235
  // ---------------------------------------------------------------------------
191
236
  // Runtime handler cleanup (auto-unsub on restart)
192
237
  // ---------------------------------------------------------------------------
@@ -231,6 +276,25 @@ function normalizeHotkey(key: unknown): string | null {
231
276
  return typeof key === 'string' && key !== '' ? key : null;
232
277
  }
233
278
 
279
+ /**
280
+ * The history moment whose entry snapshot a load restores: the moment at
281
+ * `historyIndex`, provided it is in range and belongs to the saved passage.
282
+ * Otherwise (malformed or foreign payloads) the load falls back to the
283
+ * payload's live variables.
284
+ */
285
+ function loadedEntryMoment(
286
+ payload: SavePayload,
287
+ ): SaveHistoryMoment | undefined {
288
+ const { history, historyIndex } = payload;
289
+ if (!Number.isInteger(historyIndex)) return undefined;
290
+ const moment = history[historyIndex];
291
+ if (!moment || moment.passage !== payload.passage) return undefined;
292
+ if (typeof moment.variables !== 'object' || moment.variables === null) {
293
+ return undefined;
294
+ }
295
+ return moment;
296
+ }
297
+
234
298
  /** Restore or reset PRNG from a history moment's snapshot. */
235
299
  function restorePRNGFromMoment(moment: HistoryMoment | undefined): void {
236
300
  if (moment?.prng) {
@@ -246,9 +310,21 @@ export interface HistoryMoment {
246
310
  prng?: PRNGSnapshot | null;
247
311
  }
248
312
 
313
+ /** The variable namespaces watch conditions and mutations can touch. */
314
+ export interface VariableNamespaces {
315
+ variables: Record<string, unknown>;
316
+ temporary: Record<string, unknown>;
317
+ transient: Record<string, unknown>;
318
+ }
319
+
249
320
  export interface StoryState {
250
321
  storyData: StoryData | null;
251
322
  currentPassage: string;
323
+ /**
324
+ * Incremented on every navigation (navigate, back, forward, restart, load)
325
+ * so the passage display can remount even when the passage name is unchanged.
326
+ */
327
+ navigationId: number;
252
328
  variables: Record<string, unknown>;
253
329
  variableDefaults: Record<string, unknown>;
254
330
  transient: Record<string, unknown>;
@@ -287,21 +363,30 @@ export interface StoryState {
287
363
  deleteTemporary: (name: string) => void;
288
364
  setTransient: (name: string, value: unknown) => void;
289
365
  deleteTransient: (name: string) => void;
366
+ /**
367
+ * Apply changes to several variables as one store update, so subscribers
368
+ * (watchers, components) see them together instead of half-applied.
369
+ */
370
+ updateVariables: (recipe: (draft: VariableNamespaces) => void) => void;
290
371
  trackRender: (passageName: string) => void;
291
372
  restart: () => void;
292
- save: (slot?: string, custom?: Record<string, unknown>) => void;
293
- load: (slot?: string) => void;
373
+ save: (slot?: string, custom?: Record<string, unknown>) => Promise<void>;
374
+ load: (slot?: string) => Promise<void>;
294
375
  hasSave: (slot?: string) => boolean;
295
376
  getSaveInfo: (slot?: string) => Promise<SaveInfo | null>;
296
377
  listSaves: () => Promise<SaveInfo[]>;
297
- deleteSave: (slot?: string) => void;
378
+ deleteSave: (slot?: string) => Promise<void>;
298
379
  exportSave: (slot?: string) => Promise<SaveExport | null>;
299
380
  importSave: (data: unknown, slot?: string) => Promise<SaveInfo>;
300
381
  clearGameData: () => void;
301
382
  clearAllData: () => void;
302
383
  deletePlaythrough: (playthroughId: string) => void;
303
384
  getSavePayload: () => SavePayload;
304
- loadFromPayload: (payload: SavePayload) => void;
385
+ /**
386
+ * Replace the game state with a live (deserialized) payload. `slot` is
387
+ * passed to the `beforeload`/`afterload` events.
388
+ */
389
+ loadFromPayload: (payload: SavePayload, slot?: string) => void;
305
390
  getHistoryVariables: (index: number) => Record<string, unknown>;
306
391
  setTransition: (config: TransitionConfig | null) => void;
307
392
  setNextTransition: (config: TransitionConfig | null) => void;
@@ -310,10 +395,22 @@ export interface StoryState {
310
395
  clearDeferredRender: () => void;
311
396
  }
312
397
 
398
+ /**
399
+ * Return `p` marked as handled: a caller that ignores the result of a
400
+ * fire-and-forget operation (whose failure is already logged) gets no
401
+ * unhandled-rejection report, while a caller that awaits it still sees the
402
+ * error.
403
+ */
404
+ function handled<T>(p: Promise<T>): Promise<T> {
405
+ p.catch(() => {});
406
+ return p;
407
+ }
408
+
313
409
  export const useStoryStore = create<StoryState>()(
314
410
  immer((set, get) => ({
315
411
  storyData: null,
316
412
  currentPassage: '',
413
+ navigationId: 0,
317
414
  variables: {},
318
415
  variableDefaults: {},
319
416
  transient: {},
@@ -371,6 +468,7 @@ export const useStoryStore = create<StoryState>()(
371
468
  set((state) => {
372
469
  state.storyData = storyData as StoryData;
373
470
  state.currentPassage = startPassage.name;
471
+ state.navigationId++;
374
472
  state.variables = initialVars;
375
473
  state.variableDefaults = variableDefaults;
376
474
  state.transient = deepClone(transientDefaults);
@@ -420,7 +518,14 @@ export const useStoryStore = create<StoryState>()(
420
518
  },
421
519
 
422
520
  navigate: (passageName: string) => {
423
- const { storyData, variables: currVars } = get();
521
+ // A watcher (goto action or callback) reacting to a navigation must not
522
+ // start another one before the first has recorded its moment.
523
+ if (navigationTriggerPhase) {
524
+ deferredNavigations.push(passageName);
525
+ return;
526
+ }
527
+
528
+ const { storyData } = get();
424
529
  if (!storyData) return;
425
530
 
426
531
  if (SPECIAL_PASSAGES.has(passageName)) {
@@ -438,16 +543,21 @@ export const useStoryStore = create<StoryState>()(
438
543
  const previousPassage = get().currentPassage;
439
544
  emit('beforenavigate', passageName);
440
545
 
441
- // Compute variable delta before Immer set()
442
- const patchEntry = computeVarPatches(lastNavigationVars, currVars);
546
+ // Compute variable delta before Immer set(). Read the variables after
547
+ // beforenavigate so changes made by its handlers are recorded.
548
+ const patchEntry = computeVarPatches(lastNavigationVars, get().variables);
443
549
 
444
550
  set((state) => {
445
551
  state.temporary = {};
446
552
  state.currentPassage = passageName;
553
+ state.navigationId++;
447
554
 
448
555
  // Truncate forward history if we navigated back then chose a new path
449
556
  state.history = state.history.slice(0, state.historyIndex + 1);
450
557
  patchEntries.length = state.historyIndex;
558
+ if (serializedHistory.length > state.historyIndex + 1) {
559
+ serializedHistory.length = state.historyIndex + 1;
560
+ }
451
561
 
452
562
  // Push new transition and moment
453
563
  patchEntries.push(patchEntry);
@@ -476,32 +586,59 @@ export const useStoryStore = create<StoryState>()(
476
586
  (state.renderCounts[passageName] ?? 0) + 1;
477
587
  });
478
588
 
589
+ // Watchers react to the completed transition (visit counts, cleared
590
+ // temporaries). Like beforenavigate changes, their run actions belong
591
+ // to the entered moment; navigations they request run afterwards.
592
+ const enteredVars = get().variables;
593
+ navigationTriggerPhase = true;
594
+ try {
595
+ checkTriggersOnNavigation();
596
+ } finally {
597
+ navigationTriggerPhase = false;
598
+ }
599
+ const deferred = deferredNavigations;
600
+ deferredNavigations = [];
601
+ if (get().variables !== enteredVars) {
602
+ rerecordNewestMoment(get().variables);
603
+ }
604
+ const prng = snapshotPRNG();
605
+ const recorded = get().history[get().historyIndex]!.prng;
606
+ if (prng?.seed !== recorded?.seed || prng?.pull !== recorded?.pull) {
607
+ set((state) => {
608
+ state.history[state.historyIndex]!.prng = prng;
609
+ });
610
+ }
611
+
479
612
  lastNavigationVars = get().variables;
480
613
  persistSession(get);
481
614
 
482
615
  emit('afternavigate', passageName, previousPassage);
616
+
617
+ for (const next of deferred) get().navigate(next);
483
618
  },
484
619
 
485
620
  goBack: () => {
486
- const { historyIndex, variables } = get();
621
+ const { historyIndex } = get();
487
622
  if (historyIndex <= 0) return;
488
623
 
489
624
  const previousPassage = get().currentPassage;
490
625
  const targetPassage = get().history[historyIndex - 1]!.passage;
491
626
  emit('beforenavigate', targetPassage);
492
627
 
493
- // Apply inverse transition: moment historyIndex → historyIndex−1
494
- const restoredVars = deepClone(
495
- applyPatches(variables, patchEntries[historyIndex - 1]!.inverse),
496
- );
628
+ // Restore the recorded snapshot; live variables may hold edits made
629
+ // since the current moment was recorded.
630
+ const restoredVars = deepClone(reconstructVarsAt(historyIndex - 1));
497
631
 
498
632
  set((state) => {
499
633
  state.historyIndex--;
500
634
  state.currentPassage = state.history[state.historyIndex]!.passage;
635
+ state.navigationId++;
501
636
  state.variables = restoredVars;
502
637
  state.temporary = {};
503
638
  });
504
639
 
640
+ // Restored state is not a change watchers react to
641
+ reinitTriggerState();
505
642
  lastNavigationVars = get().variables;
506
643
  restorePRNGFromMoment(get().history[get().historyIndex]);
507
644
  persistSession(get);
@@ -510,25 +647,27 @@ export const useStoryStore = create<StoryState>()(
510
647
  },
511
648
 
512
649
  goForward: () => {
513
- const { historyIndex, history: hist, variables } = get();
650
+ const { historyIndex, history: hist } = get();
514
651
  if (historyIndex >= hist.length - 1) return;
515
652
 
516
653
  const previousPassage = get().currentPassage;
517
654
  const targetPassage = hist[historyIndex + 1]!.passage;
518
655
  emit('beforenavigate', targetPassage);
519
656
 
520
- // Apply forward transition: moment historyIndex → historyIndex+1
521
- const restoredVars = deepClone(
522
- applyPatches(variables, patchEntries[historyIndex]!.forward),
523
- );
657
+ // Restore the recorded snapshot; live variables may hold edits made
658
+ // since the current moment was recorded.
659
+ const restoredVars = deepClone(reconstructVarsAt(historyIndex + 1));
524
660
 
525
661
  set((state) => {
526
662
  state.historyIndex++;
527
663
  state.currentPassage = state.history[state.historyIndex]!.passage;
664
+ state.navigationId++;
528
665
  state.variables = restoredVars;
529
666
  state.temporary = {};
530
667
  });
531
668
 
669
+ // Restored state is not a change watchers react to
670
+ reinitTriggerState();
532
671
  lastNavigationVars = get().variables;
533
672
  restorePRNGFromMoment(get().history[get().historyIndex]);
534
673
  persistSession(get);
@@ -572,6 +711,12 @@ export const useStoryStore = create<StoryState>()(
572
711
  });
573
712
  },
574
713
 
714
+ updateVariables: (recipe: (draft: VariableNamespaces) => void) => {
715
+ set((state) => {
716
+ recipe(state);
717
+ });
718
+ },
719
+
575
720
  trackRender: (passageName: string) => {
576
721
  set((state) => {
577
722
  state.renderCounts[passageName] =
@@ -606,6 +751,7 @@ export const useStoryStore = create<StoryState>()(
606
751
 
607
752
  set((state) => {
608
753
  state.currentPassage = startPassage.name;
754
+ state.navigationId++;
609
755
  state.variables = initialVars;
610
756
  state.transient = deepClone(transientDefaults);
611
757
  state.temporary = {};
@@ -646,53 +792,64 @@ export const useStoryStore = create<StoryState>()(
646
792
 
647
793
  save: (slot?: string, custom?: Record<string, unknown>) => {
648
794
  const { storyData, playthroughId } = get();
649
- if (!storyData) return;
650
-
651
- emit('beforesave', slot, custom);
652
-
653
- const payload = get().getSavePayload();
654
-
655
- set((state) => {
656
- state.saveError = null;
657
- });
658
- quickSave(storyData.ifid, playthroughId, payload, slot, custom)
659
- .then(() => {
660
- set((state) => {
661
- state.knownSaves = {
662
- ...state.knownSaves,
663
- [slot ?? '']: true,
664
- };
665
- });
666
- emit('aftersave', slot);
667
- })
668
- .catch((err) => {
795
+ if (!storyData) return Promise.resolve();
796
+
797
+ return handled(
798
+ saveWithHooks(
799
+ slot,
800
+ custom,
801
+ () => get().getSavePayload(),
802
+ async (payload) => {
803
+ set((state) => {
804
+ state.saveError = null;
805
+ });
806
+ await quickSave(
807
+ storyData.ifid,
808
+ playthroughId,
809
+ payload,
810
+ slot,
811
+ custom,
812
+ );
813
+ set((state) => {
814
+ state.knownSaves = {
815
+ ...state.knownSaves,
816
+ [slot ?? '']: true,
817
+ };
818
+ });
819
+ },
820
+ ).catch((err) => {
669
821
  console.error('spindle: failed to save', err);
670
822
  set((state) => {
671
823
  state.saveError =
672
824
  err instanceof Error ? err.message : 'Failed to save';
673
825
  });
674
- });
826
+ throw err;
827
+ }),
828
+ );
675
829
  },
676
830
 
677
831
  load: (slot?: string) => {
678
832
  const { storyData } = get();
679
- if (!storyData) return;
833
+ if (!storyData) return Promise.resolve();
680
834
 
681
835
  set((state) => {
682
836
  state.loadError = null;
683
837
  });
684
- loadQuickSave(storyData.ifid, slot)
685
- .then((payload) => {
686
- if (!payload) return;
687
- get().loadFromPayload(payload);
688
- })
689
- .catch((err) => {
690
- console.error('spindle: failed to load save', err);
691
- set((state) => {
692
- state.loadError =
693
- err instanceof Error ? err.message : 'Failed to load';
694
- });
695
- });
838
+ return handled(
839
+ loadQuickSave(storyData.ifid, slot)
840
+ .then((payload) => {
841
+ if (!payload) return;
842
+ get().loadFromPayload(payload, slot);
843
+ })
844
+ .catch((err) => {
845
+ console.error('spindle: failed to load save', err);
846
+ set((state) => {
847
+ state.loadError =
848
+ err instanceof Error ? err.message : 'Failed to load';
849
+ });
850
+ throw err;
851
+ }),
852
+ );
696
853
  },
697
854
 
698
855
  hasSave: (slot?: string) => {
@@ -715,19 +872,22 @@ export const useStoryStore = create<StoryState>()(
715
872
 
716
873
  deleteSave: (slot?: string) => {
717
874
  const { storyData } = get();
718
- if (!storyData) return;
875
+ if (!storyData) return Promise.resolve();
719
876
 
720
- deleteSlotSave(storyData.ifid, slot)
721
- .then(() => {
722
- set((state) => {
723
- const key = slot ?? '';
724
- const { [key]: _, ...rest } = state.knownSaves;
725
- state.knownSaves = rest as Record<string, true>;
726
- });
727
- })
728
- .catch((err) => {
729
- console.error('spindle: failed to delete save', err);
730
- });
877
+ return handled(
878
+ deleteSlotSave(storyData.ifid, slot)
879
+ .then(() => {
880
+ set((state) => {
881
+ const key = slot ?? '';
882
+ const { [key]: _, ...rest } = state.knownSaves;
883
+ state.knownSaves = rest as Record<string, true>;
884
+ });
885
+ })
886
+ .catch((err) => {
887
+ console.error('spindle: failed to delete save', err);
888
+ throw err;
889
+ }),
890
+ );
731
891
  },
732
892
 
733
893
  exportSave: async (slot?: string): Promise<SaveExport | null> => {
@@ -832,41 +992,49 @@ export const useStoryStore = create<StoryState>()(
832
992
  };
833
993
  },
834
994
 
835
- loadFromPayload: (payload: SavePayload) => {
995
+ loadFromPayload: (payload: SavePayload, slot?: string) => {
836
996
  if (payload.history.length === 0) {
837
997
  console.warn('loadFromPayload: rejecting payload with empty history');
838
998
  return;
839
999
  }
840
1000
 
841
- emit('beforeload', undefined);
1001
+ emit('beforeload', slot);
842
1002
 
1003
+ // Restore the state on entering the saved passage, not the payload's
1004
+ // live variables: the passage remounts and runs its {set}/{do} again,
1005
+ // so restoring their results as well would apply them twice. Changes
1006
+ // made after entering it (input, clicks) are not restored, as with
1007
+ // back/forward.
1008
+ const entry = loadedEntryMoment(payload);
1009
+
1010
+ // The payload is already live (deserialized at the storage boundary by
1011
+ // loadSave/loadSession); deserializing again would corrupt built-ins.
843
1012
  // Convert full snapshots to patch entries
844
- const base = deserialize(payload.history[0]?.variables ?? {}) as Record<
845
- string,
846
- unknown
847
- >;
1013
+ const base = deepClone(payload.history[0]?.variables ?? {});
848
1014
  const newPatchEntries: PatchEntry[] = [];
849
1015
 
850
1016
  let prevVars: Record<string, unknown> = base;
851
1017
  for (let i = 1; i < payload.history.length; i++) {
852
- const currVars = deserialize(payload.history[i]!.variables) as Record<
853
- string,
854
- unknown
855
- >;
1018
+ const currVars = deepClone(payload.history[i]!.variables);
856
1019
  newPatchEntries.push(computeVarPatches(prevVars, currVars));
857
1020
  prevVars = currVars;
858
1021
  }
859
1022
 
860
1023
  variableBase = deepClone(base);
861
1024
  patchEntries = newPatchEntries;
862
- serializedHistory = [];
1025
+ // Seed the session cache from the payload's own snapshots, so
1026
+ // persistSession does not rebuild them from the live variables.
1027
+ serializedHistory = payload.history.map((m) => ({
1028
+ passage: m.passage,
1029
+ variables: serialize(m.variables),
1030
+ timestamp: m.timestamp,
1031
+ prng: m.prng,
1032
+ }));
863
1033
 
864
1034
  set((state) => {
865
1035
  state.currentPassage = payload.passage;
866
- state.variables = deserialize(payload.variables) as Record<
867
- string,
868
- unknown
869
- >;
1036
+ state.navigationId++;
1037
+ state.variables = deepClone(entry?.variables ?? payload.variables);
870
1038
  state.history = payload.history.map((m) => ({
871
1039
  passage: m.passage,
872
1040
  timestamp: m.timestamp,
@@ -882,15 +1050,26 @@ export const useStoryStore = create<StoryState>()(
882
1050
  state.transient = deepClone(get().transientDefaults);
883
1051
  });
884
1052
 
885
- lastNavigationVars = get().variables;
1053
+ // Loaded state is not a change watchers react to
1054
+ reinitTriggerState();
1055
+
1056
+ // The next navigate() diffs from the snapshot recorded for the current
1057
+ // moment (the live variables, unless the load fell back to them)
1058
+ lastNavigationVars = reconstructVarsAt(get().historyIndex);
886
1059
 
887
- if (payload.prng) {
888
- restorePRNG(payload.prng.seed, payload.prng.pull);
1060
+ // Replay the passage's random rolls from its entry PRNG state; saves
1061
+ // whose moments predate PRNG snapshots use the payload's.
1062
+ const prng = entry?.prng !== undefined ? entry.prng : payload.prng;
1063
+ if (prng) {
1064
+ restorePRNG(prng.seed, prng.pull);
889
1065
  } else {
890
1066
  resetPRNG();
891
1067
  }
892
1068
 
893
- emit('afterload', undefined);
1069
+ // Write the loaded game to the session so a refresh restores it
1070
+ persistSession(get);
1071
+
1072
+ emit('afterload', slot);
894
1073
  },
895
1074
 
896
1075
  getHistoryVariables: (index: number): Record<string, unknown> => {