sci2-ts 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/apps/viewer/src/session.js +12 -2
- package/apps/viewer/src/session.ts +12 -2
- package/lib/992.sc +173 -0
- package/lib/993.sc +6 -0
- package/lib/994.sc +13 -0
- package/lib/995.sc +6 -0
- package/lib/996.sc +4 -0
- package/lib/998.sc +39 -3
- package/lib/system.sh +16 -0
- package/package.json +1 -1
- package/packages/sci/src/vm/kernels/index.d.ts +1 -1
- package/packages/sci/src/vm/kernels/index.js +1 -1
- package/packages/sci/src/vm/kernels/index.ts +1 -1
- package/packages/sci/src/vm/kernels/saves.d.ts +8 -0
- package/packages/sci/src/vm/kernels/saves.js +24 -3
- package/packages/sci/src/vm/kernels/saves.ts +30 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@ All notable changes to this project. Versions follow [Semantic Versioning](https
|
|
|
4
4
|
and the entries are generated from commit messages
|
|
5
5
|
([Conventional Commits](https://www.conventionalcommits.org/)) when a release is made.
|
|
6
6
|
|
|
7
|
+
## [0.4.0](https://github.com/rlueder/sci-ts/compare/v0.3.0...v0.4.0) (2026-10-01)
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
* **lib:** saving and restoring ([#2](https://github.com/rlueder/sci-ts/issues/2)) ([117ad9d](https://github.com/rlueder/sci-ts/commit/117ad9ddf4ca87d443d8b7143d66eef380dced9a))
|
|
12
|
+
|
|
7
13
|
## [0.3.0](https://github.com/rlueder/sci-ts/compare/v0.2.2...v0.3.0) (2026-10-01)
|
|
8
14
|
|
|
9
15
|
### Added
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EventType, ResourceManager, ResourceType, SciKey, SoundDevice, Vm, allKernels, audio, graphics, hostFiles, input, parseAdLibBank, readingTime, restoreGame, saves, snapshot, } from "../../../packages/sci/src/index.js";
|
|
1
|
+
import { EventType, GLOBAL_NAMES_VOCAB, ResourceManager, ResourceType, SciKey, SoundDevice, Vm, allKernels, audio, graphics, hostFiles, input, parseAdLibBank, readingTime, restoreGame, saves, snapshot, } from "../../../packages/sci/src/index.js";
|
|
2
2
|
import { AdLibOutput } from "./adlib.js";
|
|
3
3
|
import { WebAudioOutput } from "./audio.js";
|
|
4
4
|
import { BASE, httpFiles } from "./files.js";
|
|
@@ -48,7 +48,7 @@ export class GameSession {
|
|
|
48
48
|
const vm = new Vm(rm);
|
|
49
49
|
vm.registerKernels(allKernels);
|
|
50
50
|
// Saved games persist in this browser (IndexedDB) and appear in the game's Restore screen.
|
|
51
|
-
saves(vm).store = await browserSaveStore(options.saveNamespace ??
|
|
51
|
+
saves(vm).store = await browserSaveStore(options.saveNamespace ?? defaultSaveNamespace(vm, rm, options.mods ?? []));
|
|
52
52
|
if (options.debug)
|
|
53
53
|
hostFiles(vm).add("18.scr");
|
|
54
54
|
const sound = new WebAudioOutput(httpFiles, (name) => rm.file(name));
|
|
@@ -279,6 +279,16 @@ export class GameSession {
|
|
|
279
279
|
});
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
|
+
/**
|
|
283
|
+
* Whose saves these are. Games built by sci-ts (they carry their globals' names, vocab 990)
|
|
284
|
+
* each get their own, by the game object's name, so games played from one site don't share
|
|
285
|
+
* save slots; Sierra's games keep the namespace they always had (their mods, if any).
|
|
286
|
+
*/
|
|
287
|
+
function defaultSaveNamespace(vm, rm, mods) {
|
|
288
|
+
if (rm.has({ type: ResourceType.Vocab, number: GLOBAL_NAMES_VOCAB }))
|
|
289
|
+
return `game:${vm.object(vm.exportAddress(0, 0)).name}`;
|
|
290
|
+
return [...mods].sort().join("+");
|
|
291
|
+
}
|
|
282
292
|
const KEY_CODES = {
|
|
283
293
|
Enter: 13, Escape: 27, Backspace: 8, Tab: 9, " ": 32,
|
|
284
294
|
ArrowUp: SciKey.Up, ArrowDown: SciKey.Down, ArrowLeft: SciKey.Left, ArrowRight: SciKey.Right,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
-
EventType, ResourceManager, ResourceType, SciKey, SoundDevice, Vm, allKernels, audio, graphics, hostFiles, input, parseAdLibBank, readingTime, restoreGame, saves, snapshot,
|
|
2
|
+
EventType, GLOBAL_NAMES_VOCAB, ResourceManager, ResourceType, SciKey, SoundDevice, Vm, allKernels, audio, graphics, hostFiles, input, parseAdLibBank, readingTime, restoreGame, saves, snapshot,
|
|
3
3
|
type Frame, type VmSnapshot,
|
|
4
4
|
} from "../../../packages/sci/src/index.ts";
|
|
5
5
|
import { AdLibOutput } from "./adlib.ts";
|
|
@@ -76,7 +76,7 @@ export class GameSession {
|
|
|
76
76
|
const vm = new Vm(rm);
|
|
77
77
|
vm.registerKernels(allKernels);
|
|
78
78
|
// Saved games persist in this browser (IndexedDB) and appear in the game's Restore screen.
|
|
79
|
-
saves(vm).store = await browserSaveStore(options.saveNamespace ??
|
|
79
|
+
saves(vm).store = await browserSaveStore(options.saveNamespace ?? defaultSaveNamespace(vm, rm, options.mods ?? []));
|
|
80
80
|
if (options.debug) hostFiles(vm).add("18.scr");
|
|
81
81
|
|
|
82
82
|
const sound = new WebAudioOutput(httpFiles, (name) => rm.file(name));
|
|
@@ -294,6 +294,16 @@ export class GameSession {
|
|
|
294
294
|
}
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Whose saves these are. Games built by sci-ts (they carry their globals' names, vocab 990)
|
|
299
|
+
* each get their own, by the game object's name, so games played from one site don't share
|
|
300
|
+
* save slots; Sierra's games keep the namespace they always had (their mods, if any).
|
|
301
|
+
*/
|
|
302
|
+
function defaultSaveNamespace(vm: Vm, rm: ResourceManager, mods: string[]): string {
|
|
303
|
+
if (rm.has({ type: ResourceType.Vocab, number: GLOBAL_NAMES_VOCAB })) return `game:${vm.object(vm.exportAddress(0, 0)).name}`;
|
|
304
|
+
return [...mods].sort().join("+");
|
|
305
|
+
}
|
|
306
|
+
|
|
297
307
|
const KEY_CODES: Record<string, number> = {
|
|
298
308
|
Enter: 13, Escape: 27, Backspace: 8, Tab: 9, " ": 32,
|
|
299
309
|
ArrowUp: SciKey.Up, ArrowDown: SciKey.Down, ArrowLeft: SciKey.Left, ArrowRight: SciKey.Right,
|
package/lib/992.sc
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
;;; Saving, restoring and restarting: the game menu (Escape), F5 to save and F7 to restore.
|
|
2
|
+
(script 992)
|
|
3
|
+
(include "system.sh")
|
|
4
|
+
|
|
5
|
+
;; A line to type into, for the interpreter's line editor (EditText): shown over the room
|
|
6
|
+
;; until Enter, Escape or a click outside it, each of which is left for the next event.
|
|
7
|
+
(class EditField of Obj
|
|
8
|
+
(properties
|
|
9
|
+
x 0 y 0 z 0
|
|
10
|
+
view -1 loop 0 cel 0
|
|
11
|
+
priority 300 fixPriority 1
|
|
12
|
+
plane 0
|
|
13
|
+
bitmap 0
|
|
14
|
+
scaleSignal 0 scaleX 128 scaleY 128
|
|
15
|
+
text 0
|
|
16
|
+
font 0
|
|
17
|
+
fore 0
|
|
18
|
+
back 255
|
|
19
|
+
skip 254
|
|
20
|
+
mode 0
|
|
21
|
+
borderColor 0
|
|
22
|
+
nsLeft 0 nsTop 0 nsRight 0 nsBottom 0
|
|
23
|
+
textLeft 3 textTop 3 textRight 0 textBottom 0
|
|
24
|
+
width SAVE_DESCRIPTION) ; the most characters
|
|
25
|
+
|
|
26
|
+
;; At (x, y), w pixels wide and one line of text tall.
|
|
27
|
+
(method (place theX theY w)
|
|
28
|
+
(= x theX)
|
|
29
|
+
(= y theY)
|
|
30
|
+
(= plane uiPlane)
|
|
31
|
+
(= font (textStyle font?))
|
|
32
|
+
(= fore (textStyle fore?))
|
|
33
|
+
(= back (textStyle back?))
|
|
34
|
+
(= borderColor (if (== (textStyle border?) -1) (textStyle fore?) else (textStyle border?)))
|
|
35
|
+
(= nsLeft x)
|
|
36
|
+
(= nsTop y)
|
|
37
|
+
(= nsRight (+ x w -1))
|
|
38
|
+
(= nsBottom (+ y (PointSize font) 5))
|
|
39
|
+
(= textRight (- w 4))
|
|
40
|
+
(= textBottom (- (- nsBottom nsTop) 3))))
|
|
41
|
+
|
|
42
|
+
;; What the game menu, F5 and F7 open; (game save:), (game restore:) and (game showMenu:)
|
|
43
|
+
;; open them from a script. A save is a snapshot of the whole game, named with a line the
|
|
44
|
+
;; player types; only saves made by this build of the game are offered.
|
|
45
|
+
(class SaveRestore of Obj
|
|
46
|
+
(properties
|
|
47
|
+
mode 0 ; what the open menu is for: SR_MENU, SR_SAVE, SR_RESTORE, or SR_NAME
|
|
48
|
+
descriptions 0 ; GetSaveFiles: SAVE_DESCRIPTION characters a save, newest first
|
|
49
|
+
ids 0
|
|
50
|
+
count 0
|
|
51
|
+
field 0 ; the description being typed
|
|
52
|
+
slot 0) ; the save it goes into
|
|
53
|
+
|
|
54
|
+
(method (showMenu &tmp menu)
|
|
55
|
+
(= mode SR_MENU)
|
|
56
|
+
(= menu (Menu new:))
|
|
57
|
+
(menu
|
|
58
|
+
add: SR_SAVE "Save the game"
|
|
59
|
+
add: SR_RESTORE "Restore a game"
|
|
60
|
+
add: SR_RESTART "Start again"
|
|
61
|
+
add: 0 "Carry on"
|
|
62
|
+
show: self))
|
|
63
|
+
|
|
64
|
+
;; The saves there are now.
|
|
65
|
+
(method (load)
|
|
66
|
+
(self release:)
|
|
67
|
+
(= descriptions (String ARRAY_NEW 0))
|
|
68
|
+
(= ids (Array ARRAY_NEW 0 0))
|
|
69
|
+
(= count (GetSaveFiles "" descriptions ids)))
|
|
70
|
+
|
|
71
|
+
(method (release)
|
|
72
|
+
(if descriptions (String ARRAY_FREE descriptions) (= descriptions 0))
|
|
73
|
+
(if ids (Array ARRAY_FREE ids) (= ids 0)))
|
|
74
|
+
|
|
75
|
+
;; The description of the i-th save (a new string).
|
|
76
|
+
(method (description i &tmp s)
|
|
77
|
+
(= s (String ARRAY_NEW SAVE_DESCRIPTION))
|
|
78
|
+
(Array ARRAY_COPY s 0 descriptions (* i SAVE_DESCRIPTION) (- SAVE_DESCRIPTION 1))
|
|
79
|
+
(return s))
|
|
80
|
+
|
|
81
|
+
;; One more than the highest save number.
|
|
82
|
+
(method (nextSlot &tmp i n id)
|
|
83
|
+
(= n 0)
|
|
84
|
+
(for ((= i 0)) (< i count) ((++ i))
|
|
85
|
+
(= id (Array ARRAY_AT ids i))
|
|
86
|
+
(if (>= id n) (= n (+ id 1))))
|
|
87
|
+
(return n))
|
|
88
|
+
|
|
89
|
+
;; Menu values are save numbers plus one (0 is cancel).
|
|
90
|
+
(method (listSaves menu &tmp i)
|
|
91
|
+
(for ((= i 0)) (and (< i count) (< i SAVES_SHOWN)) ((++ i))
|
|
92
|
+
(menu add: (+ (Array ARRAY_AT ids i) 1) (self description: i))))
|
|
93
|
+
|
|
94
|
+
(method (save &tmp menu)
|
|
95
|
+
(game clearText:)
|
|
96
|
+
(self load:)
|
|
97
|
+
(= mode SR_SAVE)
|
|
98
|
+
(= menu ((Menu new:) y: 12 yourself:))
|
|
99
|
+
(menu add: (+ (self nextSlot:) 1) "A new saved game")
|
|
100
|
+
(self listSaves: menu)
|
|
101
|
+
(menu add: 0 "Cancel" show: self))
|
|
102
|
+
|
|
103
|
+
(method (restore &tmp menu)
|
|
104
|
+
(game clearText:)
|
|
105
|
+
(self load:)
|
|
106
|
+
(if (not count)
|
|
107
|
+
(narrator say: "There are no saved games yet.")
|
|
108
|
+
(return))
|
|
109
|
+
(= mode SR_RESTORE)
|
|
110
|
+
(= menu ((Menu new:) y: 12 yourself:))
|
|
111
|
+
(self listSaves: menu)
|
|
112
|
+
(menu add: 0 "Cancel" show: self))
|
|
113
|
+
|
|
114
|
+
(method (choose v)
|
|
115
|
+
(switch mode
|
|
116
|
+
(SR_MENU
|
|
117
|
+
(switch v
|
|
118
|
+
(SR_SAVE (self save:))
|
|
119
|
+
(SR_RESTORE (self restore:))
|
|
120
|
+
(SR_RESTART (RestartGame))))
|
|
121
|
+
(SR_SAVE (if v (self askDescription: (- v 1))))
|
|
122
|
+
(SR_RESTORE
|
|
123
|
+
(if v
|
|
124
|
+
;; Returns only if it couldn't: the game starts again where the save left it.
|
|
125
|
+
(RestoreGame "" (- v 1) "")
|
|
126
|
+
(narrator say: "That saved game couldn't be restored.")))))
|
|
127
|
+
|
|
128
|
+
;; Types the description of save n: the old one, or the room's number to start from.
|
|
129
|
+
(method (askDescription n &tmp i)
|
|
130
|
+
(= slot n)
|
|
131
|
+
(= field (EditField new:))
|
|
132
|
+
(field text: (String ARRAY_NEW SAVE_DESCRIPTION))
|
|
133
|
+
(for ((= i 0)) (< i count) ((++ i))
|
|
134
|
+
(if (== (Array ARRAY_AT ids i) n)
|
|
135
|
+
(Array ARRAY_COPY (field text?) 0 descriptions (* i SAVE_DESCRIPTION) (- SAVE_DESCRIPTION 1))))
|
|
136
|
+
(if (not (String STRING_LENGTH (field text?)))
|
|
137
|
+
(String STRING_FORMAT_INTO (field text?) "Room %d" curRoomNum))
|
|
138
|
+
(field place: 40 80 240)
|
|
139
|
+
(EditText field)
|
|
140
|
+
;; How it ended (Enter, Escape, a click) is the next event: wait for it.
|
|
141
|
+
(= mode SR_NAME)
|
|
142
|
+
(= dialog self))
|
|
143
|
+
|
|
144
|
+
(method (handleEvent event)
|
|
145
|
+
(event claimed: TRUE)
|
|
146
|
+
(if (== mode SR_NAME)
|
|
147
|
+
(cond
|
|
148
|
+
((and (== (event type?) EV_KEY_DOWN) (== (event message?) KEY_ENTER)) (self finishSave:))
|
|
149
|
+
((or (== (event type?) EV_KEY_DOWN) (== (event type?) EV_MOUSE_DOWN)) (self endNaming:))))
|
|
150
|
+
(return TRUE))
|
|
151
|
+
|
|
152
|
+
(method (dismiss)
|
|
153
|
+
(self endNaming:))
|
|
154
|
+
|
|
155
|
+
(method (endNaming)
|
|
156
|
+
(if (== dialog self) (= dialog 0))
|
|
157
|
+
(= mode 0)
|
|
158
|
+
(if field
|
|
159
|
+
(String ARRAY_FREE (field text?))
|
|
160
|
+
(field dispose:)
|
|
161
|
+
(= field 0)))
|
|
162
|
+
|
|
163
|
+
(method (finishSave &tmp ok)
|
|
164
|
+
;; Nothing of this menu goes into the save.
|
|
165
|
+
(if (== dialog self) (= dialog 0))
|
|
166
|
+
(= mode 0)
|
|
167
|
+
(= ok (SaveGame "" slot (field text?) ""))
|
|
168
|
+
(self endNaming: release:)
|
|
169
|
+
(narrator say: (if ok "Saved." else "The game couldn't be saved.")))
|
|
170
|
+
|
|
171
|
+
(method (dispose)
|
|
172
|
+
(self release:)
|
|
173
|
+
(super dispose:)))
|
package/lib/993.sc
CHANGED
|
@@ -79,6 +79,9 @@
|
|
|
79
79
|
(= icons 0))
|
|
80
80
|
(if window (window dispose:) (= window 0)))
|
|
81
81
|
|
|
82
|
+
(method (dismiss)
|
|
83
|
+
(self hide:))
|
|
84
|
+
|
|
82
85
|
(method (delete item)
|
|
83
86
|
(if (== theItem item) (user useItem: 0))
|
|
84
87
|
(super delete: item &rest)))
|
|
@@ -119,6 +122,9 @@
|
|
|
119
122
|
(self close:))
|
|
120
123
|
(return TRUE))
|
|
121
124
|
|
|
125
|
+
(method (dismiss)
|
|
126
|
+
(self close:))
|
|
127
|
+
|
|
122
128
|
(method (close &tmp c)
|
|
123
129
|
(if (== dialog self) (= dialog 0))
|
|
124
130
|
(if picture (picture dispose:) (= picture 0))
|
package/lib/994.sc
CHANGED
|
@@ -42,6 +42,19 @@
|
|
|
42
42
|
(= client 0)
|
|
43
43
|
(if c (c cue:)))))
|
|
44
44
|
|
|
45
|
+
;; After a restore nothing is playing: a sound that loops forever starts again; one that
|
|
46
|
+
;; was playing once is over (and whoever was waiting for it hears so).
|
|
47
|
+
(method (replay &tmp c)
|
|
48
|
+
(if handle
|
|
49
|
+
(= handle 0)
|
|
50
|
+
(= nodePtr 0)
|
|
51
|
+
(if (== loop -1)
|
|
52
|
+
(self play: client)
|
|
53
|
+
else
|
|
54
|
+
(= c client)
|
|
55
|
+
(= client 0)
|
|
56
|
+
(if c (c cue:)))))
|
|
57
|
+
|
|
45
58
|
(method (dispose)
|
|
46
59
|
(DoSound SND_STOP self)
|
|
47
60
|
(DoSound SND_DISPOSE self)
|
package/lib/995.sc
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
;; A game with its own sets them in its init: (user walkCursor: 261 waitCursor: 265 ...).
|
|
21
21
|
;; I or Tab opens the inventory; an item picked there is used like a verb (V_ITEM), with
|
|
22
22
|
;; its own cursor, until right-click goes back to walking.
|
|
23
|
+
;; Escape opens the game menu (save, restore, start again); F5 saves and F7 restores.
|
|
23
24
|
(class User of Obj
|
|
24
25
|
(properties
|
|
25
26
|
verb V_WALK
|
|
@@ -86,6 +87,11 @@
|
|
|
86
87
|
(dialog handleEvent: event)
|
|
87
88
|
(return))
|
|
88
89
|
(if (not canInput) (return))
|
|
90
|
+
(if (== (event type?) EV_KEY_DOWN)
|
|
91
|
+
(switch (event message?)
|
|
92
|
+
(KEY_ESCAPE (event claimed: TRUE) (game showMenu:) (return))
|
|
93
|
+
(KEY_F5 (event claimed: TRUE) (game save:) (return))
|
|
94
|
+
(KEY_F7 (event claimed: TRUE) (game restore:) (return))))
|
|
89
95
|
(if (and (== (event type?) EV_KEY_DOWN)
|
|
90
96
|
(or (== (event message?) KEY_TAB) (== (event message?) KEY_i) (== (event message?) KEY_I)))
|
|
91
97
|
(event claimed: TRUE)
|
package/lib/996.sc
CHANGED
package/lib/998.sc
CHANGED
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
;; The game: sets everything up, then runs one cycle after another until `quit`.
|
|
21
21
|
(class Game of Obj
|
|
22
22
|
(properties
|
|
23
|
-
script 0
|
|
23
|
+
script 0
|
|
24
|
+
saver 0) ; the SaveRestore behind the game menu
|
|
24
25
|
|
|
25
26
|
(method (init)
|
|
26
27
|
(= game self)
|
|
@@ -44,6 +45,38 @@
|
|
|
44
45
|
(while (not quit)
|
|
45
46
|
(self doit:)))
|
|
46
47
|
|
|
48
|
+
;; After a restore (RestoreGame), the interpreter starts the game here, not at play: the
|
|
49
|
+
;; game is as it was saved, but nothing is on screen and nothing is playing. Put the
|
|
50
|
+
;; planes and the cast back and start looping sounds again, then carry on.
|
|
51
|
+
(method (replay)
|
|
52
|
+
(uiPlane init:)
|
|
53
|
+
(if curRoom ((curRoom plane?) init:))
|
|
54
|
+
(cast eachElementDo: #replay)
|
|
55
|
+
(sounds eachElementDo: #replay)
|
|
56
|
+
(user cursor: -1)
|
|
57
|
+
(while (not quit)
|
|
58
|
+
(self doit:)))
|
|
59
|
+
|
|
60
|
+
;; The game menu (Escape): save, restore, start again.
|
|
61
|
+
(method (showMenu)
|
|
62
|
+
(self clearText:)
|
|
63
|
+
((self saveRestore:) showMenu:))
|
|
64
|
+
|
|
65
|
+
(method (save)
|
|
66
|
+
((self saveRestore:) save:))
|
|
67
|
+
|
|
68
|
+
(method (restore)
|
|
69
|
+
((self saveRestore:) restore:))
|
|
70
|
+
|
|
71
|
+
(method (saveRestore)
|
|
72
|
+
(if (not saver) (= saver (SaveRestore new:)))
|
|
73
|
+
(return saver))
|
|
74
|
+
|
|
75
|
+
;; Takes down a line being shown and whatever is open (a menu, the inventory, a close-up).
|
|
76
|
+
(method (clearText)
|
|
77
|
+
(if talking (talking clear:))
|
|
78
|
+
(if dialog (dialog dismiss:)))
|
|
79
|
+
|
|
47
80
|
;; One cycle: scripts, the room, everything on screen, text, drawing, then input.
|
|
48
81
|
(method (doit)
|
|
49
82
|
(++ gameTime)
|
|
@@ -59,8 +92,7 @@
|
|
|
59
92
|
;; Leaves the current room for room n (its script's export 0).
|
|
60
93
|
(method (newRoom n &tmp old)
|
|
61
94
|
(= newRoomNum 0)
|
|
62
|
-
(
|
|
63
|
-
(if dialog (dialog dispose:))
|
|
95
|
+
(self clearText:)
|
|
64
96
|
(if curRoom
|
|
65
97
|
(= old curRoomNum)
|
|
66
98
|
(curRoom dispose:)
|
|
@@ -239,6 +271,10 @@
|
|
|
239
271
|
(method (roomDisposed)
|
|
240
272
|
(self dispose:))
|
|
241
273
|
|
|
274
|
+
;; Back on screen after a restore.
|
|
275
|
+
(method (replay)
|
|
276
|
+
(if (not (& signal SIG_HIDDEN)) (AddScreenItem self)))
|
|
277
|
+
|
|
242
278
|
(method (dispose)
|
|
243
279
|
(if (not (& signal SIG_HIDDEN)) (DeleteScreenItem self))
|
|
244
280
|
(cast delete: self)
|
package/lib/system.sh
CHANGED
|
@@ -24,6 +24,16 @@
|
|
|
24
24
|
(define REMAP_OFF 0)
|
|
25
25
|
(define REMAP_BY_PERCENT 2)
|
|
26
26
|
|
|
27
|
+
;; Saved games: a description is up to SAVE_DESCRIPTION - 1 characters; the menus show the
|
|
28
|
+
;; newest SAVES_SHOWN. What SaveRestore's open menu is for:
|
|
29
|
+
(define SAVE_DESCRIPTION 36)
|
|
30
|
+
(define SAVES_SHOWN 8)
|
|
31
|
+
(define SR_MENU 1)
|
|
32
|
+
(define SR_SAVE 2)
|
|
33
|
+
(define SR_RESTORE 3)
|
|
34
|
+
(define SR_RESTART 4)
|
|
35
|
+
(define SR_NAME 5)
|
|
36
|
+
|
|
27
37
|
;; Event types, as GetEvent reports them.
|
|
28
38
|
(define EV_NULL 0)
|
|
29
39
|
(define EV_MOUSE_DOWN 1)
|
|
@@ -32,6 +42,10 @@
|
|
|
32
42
|
|
|
33
43
|
;; Keys (an event's message).
|
|
34
44
|
(define KEY_TAB 9)
|
|
45
|
+
(define KEY_ENTER 13)
|
|
46
|
+
(define KEY_ESCAPE 27)
|
|
47
|
+
(define KEY_F5 $3f00)
|
|
48
|
+
(define KEY_F7 $4100)
|
|
35
49
|
(define KEY_I 73)
|
|
36
50
|
(define KEY_i 105)
|
|
37
51
|
(define EV_ALL $7fff)
|
|
@@ -53,6 +67,8 @@
|
|
|
53
67
|
(define ARRAY_AT_PUT 3)
|
|
54
68
|
(define ARRAY_FREE 4)
|
|
55
69
|
(define STRING_LENGTH 10)
|
|
70
|
+
(define STRING_FORMAT_INTO 12)
|
|
71
|
+
(define ARRAY_COPY 6)
|
|
56
72
|
|
|
57
73
|
;; Bitmap subfunctions.
|
|
58
74
|
(define BITMAP_DISPOSE 1)
|
package/package.json
CHANGED
|
@@ -3,6 +3,6 @@ export { graphics, Graphics, type Frame, SCREEN_WIDTH, SCREEN_HEIGHT, ShowStyle,
|
|
|
3
3
|
export { input, Input, EventType, SciKey, hostFiles, type SciEvent } from "./system.ts";
|
|
4
4
|
export { audio, AudioState, readingTime } from "./audio.ts";
|
|
5
5
|
export { paletteEffects, PaletteEffects } from "./palette.ts";
|
|
6
|
-
export { saves, restoreGame, MemorySaveStore, type SaveStore, type SaveInfo } from "./saves.ts";
|
|
6
|
+
export { saves, restoreGame, scriptsFingerprint, MemorySaveStore, type SaveStore, type SaveInfo } from "./saves.ts";
|
|
7
7
|
export declare const allKernels: Record<string, KernelFn>;
|
|
8
8
|
export { stringHelpers } from "./arrays.ts";
|
|
@@ -12,7 +12,7 @@ export { graphics, Graphics, SCREEN_WIDTH, SCREEN_HEIGHT, ShowStyle, frameRgb }
|
|
|
12
12
|
export { input, Input, EventType, SciKey, hostFiles } from "./system.js";
|
|
13
13
|
export { audio, AudioState, readingTime } from "./audio.js";
|
|
14
14
|
export { paletteEffects, PaletteEffects } from "./palette.js";
|
|
15
|
-
export { saves, restoreGame, MemorySaveStore } from "./saves.js";
|
|
15
|
+
export { saves, restoreGame, scriptsFingerprint, MemorySaveStore } from "./saves.js";
|
|
16
16
|
export const allKernels = {
|
|
17
17
|
...coreKernels,
|
|
18
18
|
...mathKernels,
|
|
@@ -14,7 +14,7 @@ export { graphics, Graphics, type Frame, SCREEN_WIDTH, SCREEN_HEIGHT, ShowStyle,
|
|
|
14
14
|
export { input, Input, EventType, SciKey, hostFiles, type SciEvent } from "./system.ts";
|
|
15
15
|
export { audio, AudioState, readingTime } from "./audio.ts";
|
|
16
16
|
export { paletteEffects, PaletteEffects } from "./palette.ts";
|
|
17
|
-
export { saves, restoreGame, MemorySaveStore, type SaveStore, type SaveInfo } from "./saves.ts";
|
|
17
|
+
export { saves, restoreGame, scriptsFingerprint, MemorySaveStore, type SaveStore, type SaveInfo } from "./saves.ts";
|
|
18
18
|
|
|
19
19
|
export const allKernels: Record<string, KernelFn> = {
|
|
20
20
|
...coreKernels,
|
|
@@ -5,6 +5,11 @@ export interface SaveInfo {
|
|
|
5
5
|
description: string;
|
|
6
6
|
version: string;
|
|
7
7
|
date: number;
|
|
8
|
+
/**
|
|
9
|
+
* The scripts it was made with (scriptsFingerprint). A save is a snapshot of running
|
|
10
|
+
* scripts, so it only restores into the same ones; saves from before this was kept have none.
|
|
11
|
+
*/
|
|
12
|
+
scripts?: string;
|
|
8
13
|
}
|
|
9
14
|
/** Where saves live. Synchronous because kernels are; hosts preload and write through. */
|
|
10
15
|
export interface SaveStore {
|
|
@@ -32,7 +37,10 @@ export declare class MemorySaveStore implements SaveStore {
|
|
|
32
37
|
declare class SaveState {
|
|
33
38
|
store: SaveStore;
|
|
34
39
|
restarting: number;
|
|
40
|
+
fingerprint: string | undefined;
|
|
35
41
|
}
|
|
42
|
+
/** A hash (FNV-1a) of every script and heap resource: which scripts a save belongs to. */
|
|
43
|
+
export declare function scriptsFingerprint(vm: Vm): string;
|
|
36
44
|
export declare const saves: (vm: Vm) => SaveState;
|
|
37
45
|
/**
|
|
38
46
|
* Loads a saved game the way SSCI's RestoreGame does: host state (planes, screen items,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ResourceType } from "../../resource/types.js";
|
|
1
2
|
import { restore, snapshot } from "../savegame.js";
|
|
2
3
|
import { toSigned } from "../value.js";
|
|
3
4
|
import { stringHelpers } from "./arrays.js";
|
|
@@ -26,7 +27,25 @@ const DESCRIPTION_SIZE = 36;
|
|
|
26
27
|
class SaveState {
|
|
27
28
|
store = new MemorySaveStore();
|
|
28
29
|
restarting = Restarting.None;
|
|
30
|
+
fingerprint;
|
|
29
31
|
}
|
|
32
|
+
/** A hash (FNV-1a) of every script and heap resource: which scripts a save belongs to. */
|
|
33
|
+
export function scriptsFingerprint(vm) {
|
|
34
|
+
const s = saves(vm);
|
|
35
|
+
if (s.fingerprint === undefined) {
|
|
36
|
+
let h = 0x811c9dc5;
|
|
37
|
+
const resources = vm.resources.list().filter((r) => r.type === ResourceType.Script || r.type === ResourceType.Heap);
|
|
38
|
+
resources.sort((a, b) => a.type - b.type || a.number - b.number);
|
|
39
|
+
for (const r of resources) {
|
|
40
|
+
for (const b of [r.type, r.number & 0xff, r.number >> 8, ...vm.resources.loadSync(r).data])
|
|
41
|
+
h = Math.imul(h ^ b, 0x01000193) >>> 0;
|
|
42
|
+
}
|
|
43
|
+
s.fingerprint = h.toString(16).padStart(8, "0");
|
|
44
|
+
}
|
|
45
|
+
return s.fingerprint;
|
|
46
|
+
}
|
|
47
|
+
/** Whether a save can be restored into the scripts running now. */
|
|
48
|
+
const fits = (vm, info) => info.scripts === undefined || info.scripts === scriptsFingerprint(vm);
|
|
30
49
|
const stateOf = new WeakMap();
|
|
31
50
|
export const saves = (vm) => {
|
|
32
51
|
let s = stateOf.get(vm);
|
|
@@ -66,6 +85,7 @@ export const saveKernels = {
|
|
|
66
85
|
description: stringHelpers.str(vm, description),
|
|
67
86
|
version: stringHelpers.str(vm, version),
|
|
68
87
|
date: Date.now(),
|
|
88
|
+
scripts: scriptsFingerprint(vm),
|
|
69
89
|
};
|
|
70
90
|
saves(vm).store.put(info, snapshot(vm));
|
|
71
91
|
return 1;
|
|
@@ -74,7 +94,7 @@ export const saveKernels = {
|
|
|
74
94
|
// the saved state via `theGame replay`, as in the original interpreter.
|
|
75
95
|
RestoreGame: (vm, [, id = 0]) => {
|
|
76
96
|
const save = saves(vm).store.get(toSigned(id));
|
|
77
|
-
if (!save)
|
|
97
|
+
if (!save || !fits(vm, save.info))
|
|
78
98
|
return 0;
|
|
79
99
|
restoreGame(vm, save.snapshot);
|
|
80
100
|
return 1;
|
|
@@ -97,7 +117,8 @@ export const saveKernels = {
|
|
|
97
117
|
// GetSaveFiles(gameName, descriptions, ids): fixed 36-byte description slots and a
|
|
98
118
|
// 0-terminated id list, newest first.
|
|
99
119
|
GetSaveFiles: (vm, [, descriptions = 0, ids = 0]) => {
|
|
100
|
-
|
|
120
|
+
// Only saves these scripts can restore.
|
|
121
|
+
const list = saves(vm).store.list().filter((info) => fits(vm, info));
|
|
101
122
|
const desc = stringHelpers.resolve(vm, descriptions);
|
|
102
123
|
const idArray = stringHelpers.resolve(vm, ids);
|
|
103
124
|
if (desc) {
|
|
@@ -115,7 +136,7 @@ export const saveKernels = {
|
|
|
115
136
|
// CheckSaveGame(gameName, id, version): exists and was made by this game version.
|
|
116
137
|
CheckSaveGame: (vm, [, id = 0, version = 0]) => {
|
|
117
138
|
const save = saves(vm).store.get(toSigned(id));
|
|
118
|
-
return save && save.info.version === stringHelpers.str(vm, version) ? 1 : 0;
|
|
139
|
+
return save && fits(vm, save.info) && save.info.version === stringHelpers.str(vm, version) ? 1 : 0;
|
|
119
140
|
},
|
|
120
141
|
GetSaveDir: (vm) => stringHelpers.newString(vm, "SAVES"),
|
|
121
142
|
MakeSaveCatName: (vm, [dst = 0]) => stringHelpers.writeString(vm, dst, "QG4CDSG.CAT"),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ResourceType } from "../../resource/types.ts";
|
|
1
2
|
import { restore, snapshot, type VmSnapshot } from "../savegame.ts";
|
|
2
3
|
import type { KernelFn, Vm } from "../vm.ts";
|
|
3
4
|
import { toSigned } from "../value.ts";
|
|
@@ -12,6 +13,11 @@ export interface SaveInfo {
|
|
|
12
13
|
description: string;
|
|
13
14
|
version: string;
|
|
14
15
|
date: number;
|
|
16
|
+
/**
|
|
17
|
+
* The scripts it was made with (scriptsFingerprint). A save is a snapshot of running
|
|
18
|
+
* scripts, so it only restores into the same ones; saves from before this was kept have none.
|
|
19
|
+
*/
|
|
20
|
+
scripts?: string;
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
/** Where saves live. Synchronous because kernels are; hosts preload and write through. */
|
|
@@ -47,8 +53,27 @@ const DESCRIPTION_SIZE = 36;
|
|
|
47
53
|
class SaveState {
|
|
48
54
|
store: SaveStore = new MemorySaveStore();
|
|
49
55
|
restarting: number = Restarting.None;
|
|
56
|
+
fingerprint: string | undefined;
|
|
50
57
|
}
|
|
51
58
|
|
|
59
|
+
/** A hash (FNV-1a) of every script and heap resource: which scripts a save belongs to. */
|
|
60
|
+
export function scriptsFingerprint(vm: Vm): string {
|
|
61
|
+
const s = saves(vm);
|
|
62
|
+
if (s.fingerprint === undefined) {
|
|
63
|
+
let h = 0x811c9dc5;
|
|
64
|
+
const resources = vm.resources.list().filter((r) => r.type === ResourceType.Script || r.type === ResourceType.Heap);
|
|
65
|
+
resources.sort((a, b) => a.type - b.type || a.number - b.number);
|
|
66
|
+
for (const r of resources) {
|
|
67
|
+
for (const b of [r.type, r.number & 0xff, r.number >> 8, ...vm.resources.loadSync(r).data]) h = Math.imul(h ^ b, 0x01000193) >>> 0;
|
|
68
|
+
}
|
|
69
|
+
s.fingerprint = h.toString(16).padStart(8, "0");
|
|
70
|
+
}
|
|
71
|
+
return s.fingerprint;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Whether a save can be restored into the scripts running now. */
|
|
75
|
+
const fits = (vm: Vm, info: SaveInfo) => info.scripts === undefined || info.scripts === scriptsFingerprint(vm);
|
|
76
|
+
|
|
52
77
|
const stateOf = new WeakMap<Vm, SaveState>();
|
|
53
78
|
export const saves = (vm: Vm): SaveState => {
|
|
54
79
|
let s = stateOf.get(vm);
|
|
@@ -90,6 +115,7 @@ export const saveKernels: Record<string, KernelFn> = {
|
|
|
90
115
|
description: stringHelpers.str(vm, description),
|
|
91
116
|
version: stringHelpers.str(vm, version),
|
|
92
117
|
date: Date.now(),
|
|
118
|
+
scripts: scriptsFingerprint(vm),
|
|
93
119
|
};
|
|
94
120
|
saves(vm).store.put(info, snapshot(vm));
|
|
95
121
|
return 1;
|
|
@@ -99,7 +125,7 @@ export const saveKernels: Record<string, KernelFn> = {
|
|
|
99
125
|
// the saved state via `theGame replay`, as in the original interpreter.
|
|
100
126
|
RestoreGame: (vm, [, id = 0]) => {
|
|
101
127
|
const save = saves(vm).store.get(toSigned(id));
|
|
102
|
-
if (!save) return 0;
|
|
128
|
+
if (!save || !fits(vm, save.info)) return 0;
|
|
103
129
|
restoreGame(vm, save.snapshot);
|
|
104
130
|
return 1;
|
|
105
131
|
},
|
|
@@ -123,7 +149,8 @@ export const saveKernels: Record<string, KernelFn> = {
|
|
|
123
149
|
// GetSaveFiles(gameName, descriptions, ids): fixed 36-byte description slots and a
|
|
124
150
|
// 0-terminated id list, newest first.
|
|
125
151
|
GetSaveFiles: (vm, [, descriptions = 0, ids = 0]) => {
|
|
126
|
-
|
|
152
|
+
// Only saves these scripts can restore.
|
|
153
|
+
const list = saves(vm).store.list().filter((info) => fits(vm, info));
|
|
127
154
|
const desc = stringHelpers.resolve(vm, descriptions);
|
|
128
155
|
const idArray = stringHelpers.resolve(vm, ids);
|
|
129
156
|
if (desc) {
|
|
@@ -140,7 +167,7 @@ export const saveKernels: Record<string, KernelFn> = {
|
|
|
140
167
|
// CheckSaveGame(gameName, id, version): exists and was made by this game version.
|
|
141
168
|
CheckSaveGame: (vm, [, id = 0, version = 0]) => {
|
|
142
169
|
const save = saves(vm).store.get(toSigned(id));
|
|
143
|
-
return save && save.info.version === stringHelpers.str(vm, version) ? 1 : 0;
|
|
170
|
+
return save && fits(vm, save.info) && save.info.version === stringHelpers.str(vm, version) ? 1 : 0;
|
|
144
171
|
},
|
|
145
172
|
|
|
146
173
|
GetSaveDir: (vm) => stringHelpers.newString(vm, "SAVES"),
|