@rohal12/spindle 0.51.1 → 0.51.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/pkg/format.js +1 -1
- package/dist/pkg/headless.js +56 -26
- package/package.json +1 -1
- package/src/components/Passage.tsx +9 -3
- package/src/components/macros/PassageDisplay.tsx +1 -0
- package/src/components/macros/SaveManager.tsx +7 -12
- package/src/passage-render-state.ts +9 -8
- package/src/saves/save-manager.ts +10 -5
- package/src/store.ts +54 -23
- package/src/story-api.ts +13 -7
package/dist/pkg/headless.js
CHANGED
|
@@ -2809,18 +2809,23 @@ async function getCurrentPlaythroughId(ifid) {
|
|
|
2809
2809
|
}
|
|
2810
2810
|
/**
|
|
2811
2811
|
* Run a save through the `beforesave` / `aftersave` hooks. `beforesave` fires
|
|
2812
|
-
* before
|
|
2813
|
-
*
|
|
2814
|
-
*
|
|
2812
|
+
* before the payload is captured, so data a hook sets is part of the save;
|
|
2813
|
+
* `aftersave` fires once `write` has stored it. Every user-facing save path
|
|
2814
|
+
* goes through here.
|
|
2815
|
+
*
|
|
2816
|
+
* `beginCapture` is called just before the hooks and returns the function
|
|
2817
|
+
* that captures the payload after them, so the capture can tell which
|
|
2818
|
+
* variables the hooks changed (the store's `beginSave`).
|
|
2815
2819
|
*
|
|
2816
2820
|
* The hook and capture run synchronously, but an error they throw (a failing
|
|
2817
2821
|
* hook, an unserializable payload) rejects the returned promise like a failed
|
|
2818
2822
|
* write rather than escaping to the caller.
|
|
2819
2823
|
*/
|
|
2820
|
-
function saveWithHooks(slot, custom,
|
|
2824
|
+
function saveWithHooks(slot, custom, beginCapture, write) {
|
|
2821
2825
|
try {
|
|
2826
|
+
const capture = beginCapture();
|
|
2822
2827
|
emit("beforesave", slot, custom);
|
|
2823
|
-
return write(
|
|
2828
|
+
return write(capture()).then((result) => {
|
|
2824
2829
|
emit("aftersave", slot);
|
|
2825
2830
|
return result;
|
|
2826
2831
|
});
|
|
@@ -3703,6 +3708,22 @@ function loadedEntryMoment(payload) {
|
|
|
3703
3708
|
if (typeof moment.variables !== "object" || moment.variables === null) return;
|
|
3704
3709
|
return moment;
|
|
3705
3710
|
}
|
|
3711
|
+
/**
|
|
3712
|
+
* Copy the variables that changed between `before` and `after` (the live
|
|
3713
|
+
* variables around the `beforesave` hooks) into the payload's snapshot of the
|
|
3714
|
+
* saved moment. A load restores that snapshot, the state on entering the
|
|
3715
|
+
* passage, and runs the passage again, so data a hook adds to a save would
|
|
3716
|
+
* otherwise be lost on load (#227). Only the payload's copy changes: the live
|
|
3717
|
+
* history keeps the recorded snapshot (#159). Store updates are immutable, so
|
|
3718
|
+
* a value a hook did not touch keeps its identity.
|
|
3719
|
+
*/
|
|
3720
|
+
function keepHookWrites(payload, before, after) {
|
|
3721
|
+
const moment = payload.history[payload.historyIndex];
|
|
3722
|
+
if (!moment) return;
|
|
3723
|
+
const has = (vars, key) => Object.prototype.hasOwnProperty.call(vars, key);
|
|
3724
|
+
for (const key of new Set([...Object.keys(before), ...Object.keys(after)])) if (!has(after, key)) delete moment.variables[key];
|
|
3725
|
+
else if (!has(before, key) || after[key] !== before[key]) moment.variables[key] = deepClone(after[key]);
|
|
3726
|
+
}
|
|
3706
3727
|
/** Restore or reset PRNG from a history moment's snapshot. */
|
|
3707
3728
|
function restorePRNGFromMoment(moment) {
|
|
3708
3729
|
if (moment?.prng) restorePRNG(moment.prng.seed, moment.prng.pull);
|
|
@@ -3985,7 +4006,7 @@ var useStoryStore = create()(immer((set, get) => ({
|
|
|
3985
4006
|
save: (slot, custom) => {
|
|
3986
4007
|
const { storyData, playthroughId } = get();
|
|
3987
4008
|
if (!storyData) return Promise.resolve();
|
|
3988
|
-
return handled(saveWithHooks(slot, custom,
|
|
4009
|
+
return handled(saveWithHooks(slot, custom, get().beginSave, async (payload) => {
|
|
3989
4010
|
set((state) => {
|
|
3990
4011
|
state.saveError = null;
|
|
3991
4012
|
});
|
|
@@ -4127,6 +4148,14 @@ var useStoryStore = create()(immer((set, get) => ({
|
|
|
4127
4148
|
prng: snapshotPRNG()
|
|
4128
4149
|
};
|
|
4129
4150
|
},
|
|
4151
|
+
beginSave: () => {
|
|
4152
|
+
const before = get().variables;
|
|
4153
|
+
return () => {
|
|
4154
|
+
const payload = get().getSavePayload();
|
|
4155
|
+
keepHookWrites(payload, before, get().variables);
|
|
4156
|
+
return payload;
|
|
4157
|
+
};
|
|
4158
|
+
},
|
|
4130
4159
|
loadFromPayload: (payload, slot) => {
|
|
4131
4160
|
if (payload.history.length === 0) {
|
|
4132
4161
|
console.warn("loadFromPayload: rejecting payload with empty history");
|
|
@@ -4825,16 +4854,17 @@ function defineMacro(config, source = "builtin") {
|
|
|
4825
4854
|
//#endregion
|
|
4826
4855
|
//#region src/passage-render-state.ts
|
|
4827
4856
|
/**
|
|
4828
|
-
*
|
|
4829
|
-
* first render, e.g. when the story
|
|
4830
|
-
* Lets Story.waitForActions() tell whether the
|
|
4857
|
+
* Navigation id (the store's `navigationId`) of the passage most recently
|
|
4858
|
+
* mounted by <Passage> (null until the first render, e.g. when the story
|
|
4859
|
+
* interface has no {passage}). Lets Story.waitForActions() tell whether the
|
|
4860
|
+
* current navigation is on screen, also when it revisits the passage shown.
|
|
4831
4861
|
*/
|
|
4832
|
-
var
|
|
4833
|
-
function markPassageRendered(
|
|
4834
|
-
|
|
4862
|
+
var renderedNavigationId = null;
|
|
4863
|
+
function markPassageRendered(navigationId) {
|
|
4864
|
+
renderedNavigationId = navigationId;
|
|
4835
4865
|
}
|
|
4836
|
-
function
|
|
4837
|
-
return
|
|
4866
|
+
function getRenderedNavigationId() {
|
|
4867
|
+
return renderedNavigationId;
|
|
4838
4868
|
}
|
|
4839
4869
|
//#endregion
|
|
4840
4870
|
//#region src/story-api.ts
|
|
@@ -5128,11 +5158,10 @@ function createStoryAPI() {
|
|
|
5128
5158
|
await new Promise((resolve) => {
|
|
5129
5159
|
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
|
|
5130
5160
|
});
|
|
5131
|
-
const
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
if (name === useStoryStore.getState().currentPassage) {
|
|
5161
|
+
const isRendered = () => getRenderedNavigationId() === useStoryStore.getState().navigationId;
|
|
5162
|
+
if (getRenderedNavigationId() !== null && !isRendered() && !useStoryStore.getState().renderDeferred) await new Promise((resolve) => {
|
|
5163
|
+
const off = on("passagerender", () => {
|
|
5164
|
+
if (isRendered()) {
|
|
5136
5165
|
off();
|
|
5137
5166
|
resolve();
|
|
5138
5167
|
}
|
|
@@ -6424,7 +6453,7 @@ function SaveManagerContent() {
|
|
|
6424
6453
|
const closeDialog = useContext(DialogCloseContext);
|
|
6425
6454
|
const storyData = useStoryStore((s) => s.storyData);
|
|
6426
6455
|
const playthroughId = useStoryStore((s) => s.playthroughId);
|
|
6427
|
-
const
|
|
6456
|
+
const beginSave = useStoryStore((s) => s.beginSave);
|
|
6428
6457
|
const loadFromPayload = useStoryStore((s) => s.loadFromPayload);
|
|
6429
6458
|
const ifid = storyData?.ifid ?? "";
|
|
6430
6459
|
const refresh = useCallback(async () => {
|
|
@@ -6464,7 +6493,7 @@ function SaveManagerContent() {
|
|
|
6464
6493
|
const handleNewSave = async () => {
|
|
6465
6494
|
if (!ifid || !playthroughId) return;
|
|
6466
6495
|
try {
|
|
6467
|
-
await saveWithHooks(void 0, void 0,
|
|
6496
|
+
await saveWithHooks(void 0, void 0, beginSave, (payload) => createSave(ifid, playthroughId, payload));
|
|
6468
6497
|
showStatus("Save created");
|
|
6469
6498
|
await refresh();
|
|
6470
6499
|
} catch {
|
|
@@ -6473,7 +6502,7 @@ function SaveManagerContent() {
|
|
|
6473
6502
|
};
|
|
6474
6503
|
const handleOverwrite = async (saveId) => {
|
|
6475
6504
|
try {
|
|
6476
|
-
await saveWithHooks(void 0, void 0,
|
|
6505
|
+
await saveWithHooks(void 0, void 0, beginSave, async (payload) => {
|
|
6477
6506
|
if (!await overwriteSave(saveId, payload)) throw new Error("Save not found");
|
|
6478
6507
|
});
|
|
6479
6508
|
showStatus("Save overwritten");
|
|
@@ -7152,7 +7181,7 @@ var CODE_PASSAGES = new Set([
|
|
|
7152
7181
|
"PassageFooter",
|
|
7153
7182
|
"PassageDone"
|
|
7154
7183
|
]);
|
|
7155
|
-
function Passage({ passage, dataTransition }) {
|
|
7184
|
+
function Passage({ passage, dataTransition, navigationId }) {
|
|
7156
7185
|
const storyData = useStoryStore((s) => s.storyData);
|
|
7157
7186
|
const isCodePassage = CODE_PASSAGES.has(passage.name);
|
|
7158
7187
|
const [doneReady, setDoneReady] = useState(false);
|
|
@@ -7210,9 +7239,9 @@ function Passage({ passage, dataTransition }) {
|
|
|
7210
7239
|
}, [doneReady, donePassage?.content]);
|
|
7211
7240
|
const elRef = useRef(null);
|
|
7212
7241
|
useLayoutEffect(() => {
|
|
7213
|
-
markPassageRendered(
|
|
7242
|
+
if (navigationId !== void 0) markPassageRendered(navigationId);
|
|
7214
7243
|
if (elRef.current) emitFromRender("passagerender", passage.name, elRef.current);
|
|
7215
|
-
}, [passage.name]);
|
|
7244
|
+
}, [passage.name, navigationId]);
|
|
7216
7245
|
const nobr = passage.tags.includes("nobr");
|
|
7217
7246
|
const inner = /* @__PURE__ */ jsxs("div", {
|
|
7218
7247
|
ref: elRef,
|
|
@@ -7458,7 +7487,8 @@ defineMacro({
|
|
|
7458
7487
|
ref: containerRef,
|
|
7459
7488
|
children: effectivePassage && /* @__PURE__ */ jsx(Passage, {
|
|
7460
7489
|
passage: effectivePassage,
|
|
7461
|
-
dataTransition: renderDeferred ? "none" : resolvedTypeRef.current
|
|
7490
|
+
dataTransition: renderDeferred ? "none" : resolvedTypeRef.current,
|
|
7491
|
+
navigationId: renderDeferred ? void 0 : displayed.id
|
|
7462
7492
|
}, renderDeferred ? "loading" : `nav-${displayed.id}`)
|
|
7463
7493
|
})]
|
|
7464
7494
|
});
|
package/package.json
CHANGED
|
@@ -42,6 +42,8 @@ export function renderPassageContent(passage: PassageData) {
|
|
|
42
42
|
interface PassageProps {
|
|
43
43
|
passage: PassageData;
|
|
44
44
|
dataTransition?: string;
|
|
45
|
+
/** The navigation this passage displays; omitted for placeholders. */
|
|
46
|
+
navigationId?: number;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
const CODE_PASSAGES = new Set([
|
|
@@ -51,7 +53,11 @@ const CODE_PASSAGES = new Set([
|
|
|
51
53
|
'PassageDone',
|
|
52
54
|
]);
|
|
53
55
|
|
|
54
|
-
export function Passage({
|
|
56
|
+
export function Passage({
|
|
57
|
+
passage,
|
|
58
|
+
dataTransition,
|
|
59
|
+
navigationId,
|
|
60
|
+
}: PassageProps) {
|
|
55
61
|
const storyData = useStoryStore((s) => s.storyData);
|
|
56
62
|
const isCodePassage = CODE_PASSAGES.has(passage.name);
|
|
57
63
|
const [doneReady, setDoneReady] = useState(false);
|
|
@@ -121,11 +127,11 @@ export function Passage({ passage, dataTransition }: PassageProps) {
|
|
|
121
127
|
// Passage so every navigation that shows a passage mounts a new one.
|
|
122
128
|
const elRef = useRef<HTMLDivElement>(null);
|
|
123
129
|
useLayoutEffect(() => {
|
|
124
|
-
markPassageRendered(
|
|
130
|
+
if (navigationId !== undefined) markPassageRendered(navigationId);
|
|
125
131
|
if (elRef.current) {
|
|
126
132
|
emitFromRender('passagerender', passage.name, elRef.current);
|
|
127
133
|
}
|
|
128
|
-
}, [passage.name]);
|
|
134
|
+
}, [passage.name, navigationId]);
|
|
129
135
|
|
|
130
136
|
const nobr = passage.tags.includes('nobr');
|
|
131
137
|
|
|
@@ -292,6 +292,7 @@ defineMacro({
|
|
|
292
292
|
passage={effectivePassage}
|
|
293
293
|
key={renderDeferred ? 'loading' : `nav-${displayed.id}`}
|
|
294
294
|
dataTransition={renderDeferred ? 'none' : resolvedTypeRef.current}
|
|
295
|
+
navigationId={renderDeferred ? undefined : displayed.id}
|
|
295
296
|
/>
|
|
296
297
|
)}
|
|
297
298
|
</div>
|
|
@@ -62,7 +62,7 @@ export function SaveManagerContent() {
|
|
|
62
62
|
|
|
63
63
|
const storyData = useStoryStore((s) => s.storyData);
|
|
64
64
|
const playthroughId = useStoryStore((s) => s.playthroughId);
|
|
65
|
-
const
|
|
65
|
+
const beginSave = useStoryStore((s) => s.beginSave);
|
|
66
66
|
const loadFromPayload = useStoryStore((s) => s.loadFromPayload);
|
|
67
67
|
const ifid = storyData?.ifid ?? '';
|
|
68
68
|
|
|
@@ -108,7 +108,7 @@ export function SaveManagerContent() {
|
|
|
108
108
|
const handleNewSave = async () => {
|
|
109
109
|
if (!ifid || !playthroughId) return;
|
|
110
110
|
try {
|
|
111
|
-
await saveWithHooks(undefined, undefined,
|
|
111
|
+
await saveWithHooks(undefined, undefined, beginSave, (payload) =>
|
|
112
112
|
createSave(ifid, playthroughId, payload),
|
|
113
113
|
);
|
|
114
114
|
showStatus('Save created');
|
|
@@ -120,16 +120,11 @@ export function SaveManagerContent() {
|
|
|
120
120
|
|
|
121
121
|
const handleOverwrite = async (saveId: string) => {
|
|
122
122
|
try {
|
|
123
|
-
await saveWithHooks(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (!(await overwriteSave(saveId, payload))) {
|
|
129
|
-
throw new Error('Save not found');
|
|
130
|
-
}
|
|
131
|
-
},
|
|
132
|
-
);
|
|
123
|
+
await saveWithHooks(undefined, undefined, beginSave, async (payload) => {
|
|
124
|
+
if (!(await overwriteSave(saveId, payload))) {
|
|
125
|
+
throw new Error('Save not found');
|
|
126
|
+
}
|
|
127
|
+
});
|
|
133
128
|
showStatus('Save overwritten');
|
|
134
129
|
await refresh();
|
|
135
130
|
} catch {
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* first render, e.g. when the story
|
|
4
|
-
* Lets Story.waitForActions() tell whether the
|
|
2
|
+
* Navigation id (the store's `navigationId`) of the passage most recently
|
|
3
|
+
* mounted by <Passage> (null until the first render, e.g. when the story
|
|
4
|
+
* interface has no {passage}). Lets Story.waitForActions() tell whether the
|
|
5
|
+
* current navigation is on screen, also when it revisits the passage shown.
|
|
5
6
|
*/
|
|
6
|
-
let
|
|
7
|
+
let renderedNavigationId: number | null = null;
|
|
7
8
|
|
|
8
|
-
export function markPassageRendered(
|
|
9
|
-
|
|
9
|
+
export function markPassageRendered(navigationId: number): void {
|
|
10
|
+
renderedNavigationId = navigationId;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
export function
|
|
13
|
-
return
|
|
13
|
+
export function getRenderedNavigationId(): number | null {
|
|
14
|
+
return renderedNavigationId;
|
|
14
15
|
}
|
|
@@ -99,9 +99,13 @@ export async function getCurrentPlaythroughId(
|
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
101
|
* Run a save through the `beforesave` / `aftersave` hooks. `beforesave` fires
|
|
102
|
-
* before
|
|
103
|
-
*
|
|
104
|
-
*
|
|
102
|
+
* before the payload is captured, so data a hook sets is part of the save;
|
|
103
|
+
* `aftersave` fires once `write` has stored it. Every user-facing save path
|
|
104
|
+
* goes through here.
|
|
105
|
+
*
|
|
106
|
+
* `beginCapture` is called just before the hooks and returns the function
|
|
107
|
+
* that captures the payload after them, so the capture can tell which
|
|
108
|
+
* variables the hooks changed (the store's `beginSave`).
|
|
105
109
|
*
|
|
106
110
|
* The hook and capture run synchronously, but an error they throw (a failing
|
|
107
111
|
* hook, an unserializable payload) rejects the returned promise like a failed
|
|
@@ -110,12 +114,13 @@ export async function getCurrentPlaythroughId(
|
|
|
110
114
|
export function saveWithHooks<T>(
|
|
111
115
|
slot: string | undefined,
|
|
112
116
|
custom: Record<string, unknown> | undefined,
|
|
113
|
-
|
|
117
|
+
beginCapture: () => () => SavePayload,
|
|
114
118
|
write: (payload: SavePayload) => Promise<T>,
|
|
115
119
|
): Promise<T> {
|
|
116
120
|
try {
|
|
121
|
+
const capture = beginCapture();
|
|
117
122
|
emit('beforesave', slot, custom);
|
|
118
|
-
const payload =
|
|
123
|
+
const payload = capture();
|
|
119
124
|
return write(payload).then((result) => {
|
|
120
125
|
emit('aftersave', slot);
|
|
121
126
|
return result;
|
package/src/store.ts
CHANGED
|
@@ -297,6 +297,33 @@ function loadedEntryMoment(
|
|
|
297
297
|
return moment;
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
+
/**
|
|
301
|
+
* Copy the variables that changed between `before` and `after` (the live
|
|
302
|
+
* variables around the `beforesave` hooks) into the payload's snapshot of the
|
|
303
|
+
* saved moment. A load restores that snapshot, the state on entering the
|
|
304
|
+
* passage, and runs the passage again, so data a hook adds to a save would
|
|
305
|
+
* otherwise be lost on load (#227). Only the payload's copy changes: the live
|
|
306
|
+
* history keeps the recorded snapshot (#159). Store updates are immutable, so
|
|
307
|
+
* a value a hook did not touch keeps its identity.
|
|
308
|
+
*/
|
|
309
|
+
function keepHookWrites(
|
|
310
|
+
payload: SavePayload,
|
|
311
|
+
before: Record<string, unknown>,
|
|
312
|
+
after: Record<string, unknown>,
|
|
313
|
+
): void {
|
|
314
|
+
const moment = payload.history[payload.historyIndex];
|
|
315
|
+
if (!moment) return;
|
|
316
|
+
const has = (vars: Record<string, unknown>, key: string) =>
|
|
317
|
+
Object.prototype.hasOwnProperty.call(vars, key);
|
|
318
|
+
for (const key of new Set([...Object.keys(before), ...Object.keys(after)])) {
|
|
319
|
+
if (!has(after, key)) {
|
|
320
|
+
delete moment.variables[key];
|
|
321
|
+
} else if (!has(before, key) || after[key] !== before[key]) {
|
|
322
|
+
moment.variables[key] = deepClone(after[key]);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
300
327
|
/** Restore or reset PRNG from a history moment's snapshot. */
|
|
301
328
|
function restorePRNGFromMoment(moment: HistoryMoment | undefined): void {
|
|
302
329
|
if (moment?.prng) {
|
|
@@ -384,6 +411,12 @@ export interface StoryState {
|
|
|
384
411
|
clearAllData: () => void;
|
|
385
412
|
deletePlaythrough: (playthroughId: string) => void;
|
|
386
413
|
getSavePayload: () => SavePayload;
|
|
414
|
+
/**
|
|
415
|
+
* Start capturing a save, before the `beforesave` hooks run. The returned
|
|
416
|
+
* function builds the payload after them, and keeps the variables the
|
|
417
|
+
* hooks changed in the saved moment's snapshot, so a load restores them.
|
|
418
|
+
*/
|
|
419
|
+
beginSave: () => () => SavePayload;
|
|
387
420
|
/**
|
|
388
421
|
* Replace the game state with a live (deserialized) payload. `slot` is
|
|
389
422
|
* passed to the `beforeload`/`afterload` events.
|
|
@@ -799,29 +832,18 @@ export const useStoryStore = create<StoryState>()(
|
|
|
799
832
|
if (!storyData) return Promise.resolve();
|
|
800
833
|
|
|
801
834
|
return handled(
|
|
802
|
-
saveWithHooks(
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
slot,
|
|
815
|
-
custom,
|
|
816
|
-
);
|
|
817
|
-
set((state) => {
|
|
818
|
-
state.knownSaves = {
|
|
819
|
-
...state.knownSaves,
|
|
820
|
-
[slot ?? '']: true,
|
|
821
|
-
};
|
|
822
|
-
});
|
|
823
|
-
},
|
|
824
|
-
).catch((err) => {
|
|
835
|
+
saveWithHooks(slot, custom, get().beginSave, async (payload) => {
|
|
836
|
+
set((state) => {
|
|
837
|
+
state.saveError = null;
|
|
838
|
+
});
|
|
839
|
+
await quickSave(storyData.ifid, playthroughId, payload, slot, custom);
|
|
840
|
+
set((state) => {
|
|
841
|
+
state.knownSaves = {
|
|
842
|
+
...state.knownSaves,
|
|
843
|
+
[slot ?? '']: true,
|
|
844
|
+
};
|
|
845
|
+
});
|
|
846
|
+
}).catch((err) => {
|
|
825
847
|
console.error('spindle: failed to save', err);
|
|
826
848
|
set((state) => {
|
|
827
849
|
state.saveError =
|
|
@@ -997,6 +1019,15 @@ export const useStoryStore = create<StoryState>()(
|
|
|
997
1019
|
};
|
|
998
1020
|
},
|
|
999
1021
|
|
|
1022
|
+
beginSave: () => {
|
|
1023
|
+
const before = get().variables;
|
|
1024
|
+
return () => {
|
|
1025
|
+
const payload = get().getSavePayload();
|
|
1026
|
+
keepHookWrites(payload, before, get().variables);
|
|
1027
|
+
return payload;
|
|
1028
|
+
};
|
|
1029
|
+
},
|
|
1030
|
+
|
|
1000
1031
|
loadFromPayload: (payload: SavePayload, slot?: string) => {
|
|
1001
1032
|
if (payload.history.length === 0) {
|
|
1002
1033
|
console.warn('loadFromPayload: rejecting payload with empty history');
|
package/src/story-api.ts
CHANGED
|
@@ -30,7 +30,7 @@ import type { MacroDefinition } from './define-macro';
|
|
|
30
30
|
import { getMacroRegistry as _getMacroRegistry } from './registry';
|
|
31
31
|
import type { MacroMetadata } from './registry';
|
|
32
32
|
import { getActions, getAction, type StoryAction } from './action-registry';
|
|
33
|
-
import {
|
|
33
|
+
import { getRenderedNavigationId } from './passage-render-state';
|
|
34
34
|
import {
|
|
35
35
|
initPRNG,
|
|
36
36
|
isPRNGEnabled,
|
|
@@ -548,13 +548,19 @@ function createStoryAPI(): StoryAPI {
|
|
|
548
548
|
});
|
|
549
549
|
// A navigation can still be rendering: the passage display mounts the
|
|
550
550
|
// new passage from an effect, after a fade-through's outgoing phase.
|
|
551
|
-
// Its actions exist only once it is mounted, so wait for that.
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
551
|
+
// Its actions exist only once it is mounted, so wait for that. Compare
|
|
552
|
+
// navigations, not passage names: revisiting the passage shown mounts
|
|
553
|
+
// it anew (#228).
|
|
554
|
+
const isRendered = () =>
|
|
555
|
+
getRenderedNavigationId() === useStoryStore.getState().navigationId;
|
|
556
|
+
if (
|
|
557
|
+
getRenderedNavigationId() !== null &&
|
|
558
|
+
!isRendered() &&
|
|
559
|
+
!useStoryStore.getState().renderDeferred
|
|
560
|
+
) {
|
|
555
561
|
await new Promise<void>((resolve) => {
|
|
556
|
-
const off = emitterOn('passagerender', (
|
|
557
|
-
if (
|
|
562
|
+
const off = emitterOn('passagerender', () => {
|
|
563
|
+
if (isRendered()) {
|
|
558
564
|
off();
|
|
559
565
|
resolve();
|
|
560
566
|
}
|