sci2-ts 0.3.0 → 0.5.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 CHANGED
@@ -4,6 +4,18 @@ 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.5.0](https://github.com/rlueder/sci-ts/compare/v0.4.0...v0.5.0) (2026-10-01)
8
+
9
+ ### Added
10
+
11
+ * **lib:** an icon bar for players without a right button ([#3](https://github.com/rlueder/sci-ts/issues/3)) ([a28d5b2](https://github.com/rlueder/sci-ts/commit/a28d5b244417af6985c0c82c825a3af29ad68067))
12
+
13
+ ## [0.4.0](https://github.com/rlueder/sci-ts/compare/v0.3.0...v0.4.0) (2026-10-01)
14
+
15
+ ### Added
16
+
17
+ * **lib:** saving and restoring ([#2](https://github.com/rlueder/sci-ts/issues/2)) ([117ad9d](https://github.com/rlueder/sci-ts/commit/117ad9ddf4ca87d443d8b7143d66eef380dced9a))
18
+
7
19
  ## [0.3.0](https://github.com/rlueder/sci-ts/compare/v0.2.2...v0.3.0) (2026-10-01)
8
20
 
9
21
  ### 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 ?? [...(options.mods ?? [])].sort().join("+"));
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 ?? [...(options.mods ?? [])].sort().join("+"));
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/991.sc ADDED
@@ -0,0 +1,102 @@
1
+ ;;; The icon bar: the verbs, the item, the inventory and the game menu, for players without
2
+ ;;; a right mouse button (touch screens, trackpads).
3
+ (script 991)
4
+ (include "system.sh")
5
+
6
+ ;; An icon: a View hit anywhere in its square, not only where it's drawn. Its `actions`
7
+ ;; says what it stands for.
8
+ (class Icon of View
9
+ (properties
10
+ size ICON_SIZE)
11
+
12
+ (method (onMe theX theY)
13
+ (return (and (>= theX x) (< theX (+ x size)) (>= theY y) (< theY (+ y size))))))
14
+
15
+ ;; Slides down when the pointer reaches the top edge of the screen, or on a tap there. Its
16
+ ;; icons are the cels of `view`, loop 0 as they are and loop 1 picked, each ICON_SIZE
17
+ ;; square and anchored at the top-left corner: walk, look, do, talk, the inventory, the
18
+ ;; game menu (the item in use shows its own icon). A game with its own sets view in its
19
+ ;; init: (iconBar view: 266). Picking one, or a click below the bar, closes it.
20
+ (class IconBar of Obj
21
+ (properties
22
+ view ICON_BAR_VIEW
23
+ box 0
24
+ icons 0
25
+ height 0)
26
+
27
+ (method (show &tmp x)
28
+ (if box (return))
29
+ (= box
30
+ ((TextItem new:)
31
+ text: ""
32
+ width: SCREEN_WIDTH
33
+ height: (+ ICON_SIZE (* 2 ICON_GAP) (* 2 (textStyle margin?)))
34
+ x: 0 y: 0
35
+ yourself:))
36
+ (if (!= (textStyle frame?) -1)
37
+ (box height: (+ (box height?) (CelHigh (textStyle frame?) 0 4) (CelHigh (textStyle frame?) 0 5))))
38
+ (box init:)
39
+ (= height (box height?))
40
+ (= icons (List new:))
41
+ (= x (+ (box textLeft?) ICON_GAP))
42
+ ;; The verbs, then the item, the inventory and the menu.
43
+ (self addIcon: x 0 V_WALK)
44
+ (+= x (+ ICON_SIZE ICON_GAP))
45
+ (self addIcon: x 1 V_LOOK)
46
+ (+= x (+ ICON_SIZE ICON_GAP))
47
+ (self addIcon: x 2 V_DO)
48
+ (+= x (+ ICON_SIZE ICON_GAP))
49
+ (self addIcon: x 3 V_TALK)
50
+ (+= x (+ ICON_SIZE ICON_GAP))
51
+ (self addIcon: x -1 V_ITEM)
52
+ (+= x (+ ICON_SIZE ICON_GAP))
53
+ (self addIcon: x 4 ICON_INVENTORY)
54
+ (+= x (+ ICON_SIZE ICON_GAP))
55
+ (self addIcon: x 5 ICON_MENU)
56
+ (= dialog self))
57
+
58
+ ;; An icon at x: cel c of the bar's view (-1: the item in use), standing for `what`.
59
+ (method (addIcon theX c what &tmp icon)
60
+ (if (and (== c -1) (not theItem)) (return))
61
+ (= icon
62
+ ((Icon new:)
63
+ view: (if (== c -1) (theItem view?) else view)
64
+ loop: (if (and (!= c -1) (== what (user verb?))) 1 else 0)
65
+ cel: (if (== c -1) 0 else c)
66
+ actions: what
67
+ x: theX
68
+ y: (+ (box textTop?) ICON_GAP)
69
+ yourself:))
70
+ (icon init:)
71
+ (icon plane: uiPlane setPri: (+ (box priority?) 1))
72
+ (icons add: icon))
73
+
74
+ (method (hide)
75
+ (if (== dialog self) (= dialog 0))
76
+ (if icons
77
+ (icons eachElementDo: #dispose)
78
+ (icons dispose:)
79
+ (= icons 0))
80
+ (if box (box dispose:) (= box 0)))
81
+
82
+ (method (dismiss)
83
+ (self hide:))
84
+
85
+ ;; While it's open, the pointer going well below it closes it too.
86
+ (method (pointerAt theX theY)
87
+ (if (and box (> theY (+ height ICON_BAR_SLACK))) (self hide:)))
88
+
89
+ (method (handleEvent event &tmp icon what)
90
+ (event claimed: TRUE)
91
+ (if (!= (event type?) EV_MOUSE_DOWN) (return TRUE))
92
+ (= icon (icons firstTrue: #onMe (event x?) (event y?)))
93
+ (if (not icon)
94
+ (self hide:)
95
+ (return TRUE))
96
+ (= what (icon actions?))
97
+ (self hide:)
98
+ (switch what
99
+ (ICON_INVENTORY (inventory showSelf:))
100
+ (ICON_MENU (game showMenu:))
101
+ (else (user setVerb: what)))
102
+ (return TRUE)))
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
@@ -47,7 +47,7 @@
47
47
  (for ((= node (FirstNode elements))) node ((= node (NextNode node)))
48
48
  (= item (NodeValue node))
49
49
  ;; The icon keeps its item in `actions`.
50
- (= icon ((View new:) view: (item view?) loop: 0 cel: 0 actions: item x: x y: (+ (window y?) (window textTop?) INV_GAP) yourself:))
50
+ (= icon ((Icon new:) view: (item view?) loop: 0 cel: 0 actions: item x: x y: (+ (window y?) (window textTop?) INV_GAP) yourself:))
51
51
  (icon init:)
52
52
  (icon plane: uiPlane setPri: (+ (window priority?) 1))
53
53
  (icons add: icon)
@@ -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
@@ -69,7 +70,11 @@
69
70
  (method (doit)
70
71
  (self showCursor:)
71
72
  (while (GetEvent EV_ALL theEvent)
72
- (self handleEvent: theEvent)))
73
+ (self handleEvent: theEvent))
74
+ ;; theEvent now says where the pointer is: at the top edge, the icon bar comes down.
75
+ (cond
76
+ ((== dialog iconBar) (iconBar pointerAt: (theEvent x?) (theEvent y?)))
77
+ ((and canInput (not dialog) (not talking) (<= (theEvent y?) ICON_BAR_EDGE)) (iconBar show:))))
73
78
 
74
79
  ;; The verb right-click goes to: walk, do, look, talk, the item picked (if any), walk.
75
80
  (method (nextVerb)
@@ -86,6 +91,11 @@
86
91
  (dialog handleEvent: event)
87
92
  (return))
88
93
  (if (not canInput) (return))
94
+ (if (== (event type?) EV_KEY_DOWN)
95
+ (switch (event message?)
96
+ (KEY_ESCAPE (event claimed: TRUE) (game showMenu:) (return))
97
+ (KEY_F5 (event claimed: TRUE) (game save:) (return))
98
+ (KEY_F7 (event claimed: TRUE) (game restore:) (return))))
89
99
  (if (and (== (event type?) EV_KEY_DOWN)
90
100
  (or (== (event message?) KEY_TAB) (== (event message?) KEY_i) (== (event message?) KEY_I)))
91
101
  (event claimed: TRUE)
@@ -95,6 +105,11 @@
95
105
  (if (& (event modifiers?) MOD_RIGHT)
96
106
  (self setVerb: (self nextVerb:))
97
107
  (return))
108
+ ;; A tap at the top edge (touch has no pointer to hover there) opens the icon bar.
109
+ (if (<= (event y?) (* 3 ICON_BAR_EDGE))
110
+ (event claimed: TRUE)
111
+ (iconBar show:)
112
+ (return))
98
113
  (= ex (event x?))
99
114
  (= ey (event y?))
100
115
  (if (== verb V_WALK)
package/lib/996.sc CHANGED
@@ -290,6 +290,10 @@
290
290
  (c choose: v))
291
291
  (return TRUE))
292
292
 
293
+ ;; Closed without a choice.
294
+ (method (dismiss)
295
+ (self dispose:))
296
+
293
297
  (method (dispose)
294
298
  (if (== dialog self) (= dialog 0))
295
299
  (if items
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)
@@ -32,6 +33,7 @@
32
33
  (= narrator (Narrator new:))
33
34
  (= textStyle (TextStyle new:))
34
35
  (= inventory (Inventory new:))
36
+ (= iconBar (IconBar new:))
35
37
  (= music (Sound new:))
36
38
  (= sfx (Sound new:))
37
39
  (= uiPlane ((Plane new:) priority: 200 picture: -2 yourself:))
@@ -44,6 +46,38 @@
44
46
  (while (not quit)
45
47
  (self doit:)))
46
48
 
49
+ ;; After a restore (RestoreGame), the interpreter starts the game here, not at play: the
50
+ ;; game is as it was saved, but nothing is on screen and nothing is playing. Put the
51
+ ;; planes and the cast back and start looping sounds again, then carry on.
52
+ (method (replay)
53
+ (uiPlane init:)
54
+ (if curRoom ((curRoom plane?) init:))
55
+ (cast eachElementDo: #replay)
56
+ (sounds eachElementDo: #replay)
57
+ (user cursor: -1)
58
+ (while (not quit)
59
+ (self doit:)))
60
+
61
+ ;; The game menu (Escape): save, restore, start again.
62
+ (method (showMenu)
63
+ (self clearText:)
64
+ ((self saveRestore:) showMenu:))
65
+
66
+ (method (save)
67
+ ((self saveRestore:) save:))
68
+
69
+ (method (restore)
70
+ ((self saveRestore:) restore:))
71
+
72
+ (method (saveRestore)
73
+ (if (not saver) (= saver (SaveRestore new:)))
74
+ (return saver))
75
+
76
+ ;; Takes down a line being shown and whatever is open (a menu, the inventory, a close-up).
77
+ (method (clearText)
78
+ (if talking (talking clear:))
79
+ (if dialog (dialog dismiss:)))
80
+
47
81
  ;; One cycle: scripts, the room, everything on screen, text, drawing, then input.
48
82
  (method (doit)
49
83
  (++ gameTime)
@@ -59,8 +93,7 @@
59
93
  ;; Leaves the current room for room n (its script's export 0).
60
94
  (method (newRoom n &tmp old)
61
95
  (= newRoomNum 0)
62
- (if talking (talking clear:))
63
- (if dialog (dialog dispose:))
96
+ (self clearText:)
64
97
  (if curRoom
65
98
  (= old curRoomNum)
66
99
  (curRoom dispose:)
@@ -239,6 +272,10 @@
239
272
  (method (roomDisposed)
240
273
  (self dispose:))
241
274
 
275
+ ;; Back on screen after a restore.
276
+ (method (replay)
277
+ (if (not (& signal SIG_HIDDEN)) (AddScreenItem self)))
278
+
242
279
  (method (dispose)
243
280
  (if (not (& signal SIG_HIDDEN)) (DeleteScreenItem self))
244
281
  (cast delete: self)
package/lib/999.sc CHANGED
@@ -30,7 +30,8 @@
30
30
  [gameFlags 64] ; story flags, a bit each
31
31
  textStyle ; how text boxes and menus look (a TextStyle)
32
32
  inventory ; what the hero carries (an Inventory)
33
- theItem) ; the item picked to use, or 0
33
+ theItem ; the item picked to use, or 0
34
+ iconBar) ; the icon bar (an IconBar)
34
35
 
35
36
  (procedure (SetFlag n)
36
37
  (|= [gameFlags (/ n 16)] (<< 1 (mod n 16))))
package/lib/resources.js CHANGED
@@ -106,9 +106,50 @@ function shade() {
106
106
  const cel = { width, height, displaceX: width >> 1, displaceY: height - 1, skipColor: Colour.Transparent, pixels: new Uint8Array(width * height).fill(253) };
107
107
  return writeView({ flags: 1, loops: [{ link: -1, mirror: false, cels: [cel] }], palette: undefined });
108
108
  }
109
+ /**
110
+ * The icon bar's icons (ICON_BAR_VIEW): walk, look, do, talk, the inventory and the game
111
+ * menu, each 24 pixels square and anchored at the top-left corner; loop 0 as they are,
112
+ * loop 1 picked (inverted). The verbs reuse their cursors' drawings.
113
+ */
114
+ const ICON_BAR_VIEW = 990;
115
+ const ICON = 24;
116
+ const BAG = [
117
+ "...XXXXX...",
118
+ "..X.....X..",
119
+ "..X.....X..",
120
+ "XXXXXXXXXXX",
121
+ "XWWWWWWWWWX",
122
+ "XWWWWWWWWWX",
123
+ "XWWWXXXWWWX",
124
+ "XWWWWWWWWWX",
125
+ "XWWWWWWWWWX",
126
+ "XXXXXXXXXXX",
127
+ ];
128
+ const MENU = ["XXXXXXXXXXX", "", "", "XXXXXXXXXXX", "", "", "XXXXXXXXXXX"].map((r) => r || "...........");
129
+ function iconBar() {
130
+ const drawings = [CURSORS[3].art, CURSORS[1].art, CURSORS[4].art, CURSORS[2].art, BAG, MENU];
131
+ const icon = (art, picked) => {
132
+ const [ink, paper] = picked ? [Colour.White, Colour.Black] : [Colour.Black, Colour.White];
133
+ const pixels = new Uint8Array(ICON * ICON);
134
+ for (let y = 0; y < ICON; y++)
135
+ for (let x = 0; x < ICON; x++)
136
+ pixels[y * ICON + x] = x === 0 || y === 0 || x === ICON - 1 || y === ICON - 1 ? Colour.Black : paper;
137
+ const top = (ICON - art.length) >> 1, left = (ICON - art[0].length) >> 1;
138
+ art.forEach((row, y) => [...row].forEach((c, x) => {
139
+ if (c === "X")
140
+ pixels[(top + y) * ICON + left + x] = ink;
141
+ else if (c === "W")
142
+ pixels[(top + y) * ICON + left + x] = paper;
143
+ }));
144
+ return { width: ICON, height: ICON, displaceX: ICON >> 1, displaceY: ICON - 1, skipColor: Colour.Transparent, pixels };
145
+ };
146
+ const loop = (picked) => ({ link: -1, mirror: false, cels: drawings.map((art) => icon(art, picked)) });
147
+ return writeView({ flags: 1, loops: [loop(false), loop(true)], palette: undefined });
148
+ }
109
149
  export default function resources() {
110
150
  return [
111
151
  ...Object.entries(CURSORS).map(([verb, c]) => ({ type: ResourceType.View, number: CURSOR_BASE + Number(verb), data: cursor(c.art, c.hotspot) })),
112
152
  { type: ResourceType.View, number: CLOSE_UP_SHADE, data: shade() },
153
+ { type: ResourceType.View, number: ICON_BAR_VIEW, data: iconBar() },
113
154
  ];
114
155
  }
package/lib/resources.ts CHANGED
@@ -111,9 +111,47 @@ function shade() {
111
111
  return writeView({ flags: 1, loops: [{ link: -1, mirror: false, cels: [cel] }], palette: undefined });
112
112
  }
113
113
 
114
+ /**
115
+ * The icon bar's icons (ICON_BAR_VIEW): walk, look, do, talk, the inventory and the game
116
+ * menu, each 24 pixels square and anchored at the top-left corner; loop 0 as they are,
117
+ * loop 1 picked (inverted). The verbs reuse their cursors' drawings.
118
+ */
119
+ const ICON_BAR_VIEW = 990;
120
+ const ICON = 24;
121
+ const BAG = [
122
+ "...XXXXX...",
123
+ "..X.....X..",
124
+ "..X.....X..",
125
+ "XXXXXXXXXXX",
126
+ "XWWWWWWWWWX",
127
+ "XWWWWWWWWWX",
128
+ "XWWWXXXWWWX",
129
+ "XWWWWWWWWWX",
130
+ "XWWWWWWWWWX",
131
+ "XXXXXXXXXXX",
132
+ ];
133
+ const MENU = ["XXXXXXXXXXX", "", "", "XXXXXXXXXXX", "", "", "XXXXXXXXXXX"].map((r) => r || "...........");
134
+ function iconBar() {
135
+ const drawings = [CURSORS[3]!.art, CURSORS[1]!.art, CURSORS[4]!.art, CURSORS[2]!.art, BAG, MENU];
136
+ const icon = (art: string[], picked: boolean) => {
137
+ const [ink, paper] = picked ? [Colour.White, Colour.Black] : [Colour.Black, Colour.White];
138
+ const pixels = new Uint8Array(ICON * ICON);
139
+ for (let y = 0; y < ICON; y++) for (let x = 0; x < ICON; x++) pixels[y * ICON + x] = x === 0 || y === 0 || x === ICON - 1 || y === ICON - 1 ? Colour.Black : paper;
140
+ const top = (ICON - art.length) >> 1, left = (ICON - art[0]!.length) >> 1;
141
+ art.forEach((row, y) => [...row].forEach((c, x) => {
142
+ if (c === "X") pixels[(top + y) * ICON + left + x] = ink;
143
+ else if (c === "W") pixels[(top + y) * ICON + left + x] = paper;
144
+ }));
145
+ return { width: ICON, height: ICON, displaceX: ICON >> 1, displaceY: ICON - 1, skipColor: Colour.Transparent, pixels };
146
+ };
147
+ const loop = (picked: boolean) => ({ link: -1, mirror: false, cels: drawings.map((art) => icon(art, picked)) });
148
+ return writeView({ flags: 1, loops: [loop(false), loop(true)], palette: undefined });
149
+ }
150
+
114
151
  export default function resources(): ResourceData[] {
115
152
  return [
116
153
  ...Object.entries(CURSORS).map(([verb, c]) => ({ type: ResourceType.View, number: CURSOR_BASE + Number(verb), data: cursor(c.art, c.hotspot) })),
117
154
  { type: ResourceType.View, number: CLOSE_UP_SHADE, data: shade() },
155
+ { type: ResourceType.View, number: ICON_BAR_VIEW, data: iconBar() },
118
156
  ];
119
157
  }
package/lib/system.sh CHANGED
@@ -24,6 +24,27 @@
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
+ ;; The icon bar: icons are ICON_SIZE pixels square, ICON_GAP apart; it opens when the
30
+ ;; pointer is within ICON_BAR_EDGE pixels of the top and closes ICON_BAR_SLACK below it.
31
+ ;; Besides verbs, an icon can stand for the inventory or the game menu.
32
+ (define ICON_SIZE 24)
33
+ (define ICON_GAP 4)
34
+ (define ICON_BAR_VIEW 990)
35
+ (define ICON_BAR_EDGE 3)
36
+ (define ICON_BAR_SLACK 12)
37
+ (define ICON_INVENTORY 100)
38
+ (define ICON_MENU 101)
39
+
40
+ (define SAVE_DESCRIPTION 36)
41
+ (define SAVES_SHOWN 8)
42
+ (define SR_MENU 1)
43
+ (define SR_SAVE 2)
44
+ (define SR_RESTORE 3)
45
+ (define SR_RESTART 4)
46
+ (define SR_NAME 5)
47
+
27
48
  ;; Event types, as GetEvent reports them.
28
49
  (define EV_NULL 0)
29
50
  (define EV_MOUSE_DOWN 1)
@@ -32,6 +53,10 @@
32
53
 
33
54
  ;; Keys (an event's message).
34
55
  (define KEY_TAB 9)
56
+ (define KEY_ENTER 13)
57
+ (define KEY_ESCAPE 27)
58
+ (define KEY_F5 $3f00)
59
+ (define KEY_F7 $4100)
35
60
  (define KEY_I 73)
36
61
  (define KEY_i 105)
37
62
  (define EV_ALL $7fff)
@@ -53,6 +78,8 @@
53
78
  (define ARRAY_AT_PUT 3)
54
79
  (define ARRAY_FREE 4)
55
80
  (define STRING_LENGTH 10)
81
+ (define STRING_FORMAT_INTO 12)
82
+ (define ARRAY_COPY 6)
56
83
 
57
84
  ;; Bitmap subfunctions.
58
85
  (define BITMAP_DISPOSE 1)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sci2-ts",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "An SCI2 adventure game engine in TypeScript, and the tools to make games with it",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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
- const list = saves(vm).store.list();
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
- const list = saves(vm).store.list();
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"),