Compare commits
2 commits
65c380555f
...
2795f8bbf5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2795f8bbf5 | ||
|
|
7926c55983 |
90 changed files with 3026 additions and 380 deletions
205
PLAN.md
Normal file
205
PLAN.md
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
# JoeyAGI plan: every Sierra AGI game
|
||||||
|
|
||||||
|
The goal is to run every Sierra AGI release. STATUS.md records where each
|
||||||
|
subsystem and command stands; this file is the road there. References:
|
||||||
|
Sierra's own interpreters, run as black boxes in DOSBox (`scripts/oracle.sh`),
|
||||||
|
and, since October 2026, the open-source implementations, ScummVM's AGI engine
|
||||||
|
first. They are read for behavior, never copied: ScummVM is GPL and JoeyAGI is
|
||||||
|
MIT. Where a source and Sierra's interpreter disagree, Sierra's interpreter
|
||||||
|
wins.
|
||||||
|
|
||||||
|
## Decisions (Scott, 2026-10-19)
|
||||||
|
|
||||||
|
| Question | Decision |
|
||||||
|
|----------|----------|
|
||||||
|
| Scope | Every DOS release of the 16 Sierra AGI titles, then the other releases (Phase 5) |
|
||||||
|
| Bugs in Sierra's game scripts that ScummVM patches | Patch them |
|
||||||
|
| Copy protection | Skip it, as ScummVM does for Gold Rush |
|
||||||
|
| Mouse | Add AGI v3's mouse: click-to-walk, the mouse commands, mouse menus |
|
||||||
|
| Save games | Keep JoeyAGI's own format; do not read Sierra's `*SG.n` files |
|
||||||
|
| Random numbers | Match Sierra's generator, so runs can be compared exactly |
|
||||||
|
|
||||||
|
## The games
|
||||||
|
|
||||||
|
Sixteen titles: King's Quest 1 to 4, Space Quest 1 and 2, Police Quest 1,
|
||||||
|
Leisure Suit Larry 1, Black Cauldron, Gold Rush, Manhunter 1 and 2, Mixed-Up
|
||||||
|
Mother Goose, Donald Duck's Playground, the AGI demos, and the 1986 Christmas
|
||||||
|
card. `games/` holds all sixteen and four fan games. ScummVM's detection
|
||||||
|
table (~/claude/scratch/joeyagi/scummvmAgi) names the known releases by
|
||||||
|
their directory files' MD5s.
|
||||||
|
|
||||||
|
On 2026-10-20 Scott's copy of the eXo ScummVM collection
|
||||||
|
(~/Downloads/eXo/eXoScummVM/eXo/eXoScummVM: read, never changed) added nine
|
||||||
|
releases to `games/` and the other machines' releases to `games/machines/`
|
||||||
|
(Phase 5, which test-agi.sh does not run yet): 13 Amiga, 11 Apple IIgs, 10
|
||||||
|
Atari ST, and 10 Macintosh. eXo's folders hold only the data, because eXo plays
|
||||||
|
them in ScummVM. Where a release lacks Sierra's interpreter, one of the same
|
||||||
|
version stands in (the "Interpreter" column), so the oracle can run it.
|
||||||
|
|
||||||
|
Still to obtain (asked of Scott 2026-10-20), each with Sierra's interpreter
|
||||||
|
files:
|
||||||
|
- Sierra's 3.002.086 (any DOS King's Quest IV AGI as shipped);
|
||||||
|
- 2.089 (Space Quest 1 1.0X, 1986-09-24) and 2.230 (the Christmas card's
|
||||||
|
version 1, 1986-10-13);
|
||||||
|
- a stock 2.936 (King's Quest III 2.14 or Space Quest II 2.0D/2.0F as
|
||||||
|
shipped), to confirm the copy in Caitlyn's Destiny;
|
||||||
|
- older releases on interpreters already covered (King's Quest 1 1.0U, King's
|
||||||
|
Quest III 1.01, Space Quest 1 1.1A, King's Quest 2 2.1, Donald Duck's
|
||||||
|
Playground 1.50, Police Quest 2.0E, the demos);
|
||||||
|
- the booter disk images (Phase 5).
|
||||||
|
|
||||||
|
| Folder | Game | Interpreter (AGIDATA.OVL) | Data |
|
||||||
|
|--------|------|---------------------------|------|
|
||||||
|
| bc | Black Cauldron | 2.439 | 386 KB |
|
||||||
|
| kq1 | King's Quest 1 | 2.917 | 344 KB |
|
||||||
|
| kq2 | King's Quest 2 | 2.917 | 488 KB |
|
||||||
|
| kq3 | King's Quest 3 | 2.435 | 651 KB |
|
||||||
|
| lsl1 | Leisure Suit Larry 1 | 2.440 | 455 KB |
|
||||||
|
| mmg | Mixed-Up Mother Goose | 2.915 | 401 KB |
|
||||||
|
| pq1 | Police Quest 1 | 2.903 | 700 KB |
|
||||||
|
| sq1 | Space Quest 1 | 2.426 | 529 KB |
|
||||||
|
| sq2 | Space Quest 2 | 2.912 | 644 KB |
|
||||||
|
| gr | Gold Rush | 3.002.149 (v3) | 1174 KB |
|
||||||
|
| mh1 | Manhunter: New York | 3.002.107 (v3) | 1034 KB |
|
||||||
|
| mh2 | Manhunter 2: San Francisco | 3.002.149 (v3) | 1189 KB |
|
||||||
|
| agidemo1 | AGI Demo 1, 1987-05-20 | 2.425 | 168 KB |
|
||||||
|
| agidemo2a | AGI Demo 2, 1987-11-24, 5.25" version 1 (its VOL.3, the second disk, is agidemo2b's: every entry matches) | 2.915 | 385 KB |
|
||||||
|
| agidemo2b | AGI Demo 2, 1987-11-24, 3.5" | 2.915 | 382 KB |
|
||||||
|
| agidemo2c | AGI Demo 2, 1987-11-25, 5.25" version 2 | 2.917 | 276 KB |
|
||||||
|
| agidemo3 | AGI Demo 3, 1988-09-13 | 3.002.102 (v3, DMDIR) | 247 KB |
|
||||||
|
| kq4gs | King's Quest IV, Apple IIgs 1.0K, 1988-11-22 (three 800K disks; IIgs font AGIFONT and instruments SIERRASTANDARD; Sierra's IIgs interpreter KQ4.SYS16) | IIgs 2.006, as DOS 3.002.086 (v3, KQ4DIR) | 1417 KB |
|
||||||
|
| xmas | The Christmas card, 1986-11-13 (version 2) | 2.272 | 178 KB |
|
||||||
|
| ddp | Donald Duck's Playground: a cracked PC booter image (DONALD.IMG) and its DOS launcher | booter (Phase 5) | 273 KB |
|
||||||
|
| kq6agi | King's Quest VI AGI demake (fan, Brandon Kouri) | 2.917 | 1469 KB |
|
||||||
|
| sqtlc | Space Quest: The Lost Chapter, mouse version (fan; AGI Mouse interpreter) | 2.917 | 1550 KB |
|
||||||
|
| sq0 | Space Quest 0: Replicated (fan; its interpreter is a Windows program) | none (2.435 assumed) | 1032 KB |
|
||||||
|
| kq3v214 | King's Quest III 2.14, 1988-03-15, 3.5" (eXo; its sound 36 shipped cut to 1035 of its 2281 bytes) | 2.936, from caitlyn | 643 KB |
|
||||||
|
| sq2v20f | Space Quest II 2.0F, 1989-01-05, 3.5" (eXo) | 2.936, from caitlyn | 637 KB |
|
||||||
|
| pq1v20g | Police Quest 2.0G, 1987-12-03 (eXo) | 2.917, AGI Tetris's (fan; kq1's quits at once on this game) | 694 KB |
|
||||||
|
| bcv210 | Black Cauldron 2.10, 1988-11-10, 5.25" (eXo; AGI 3) | 3.002.149: gr's, its built-in file prefix changed from GR to BC (a v3 interpreter carries its game's prefix, at 0x60D7) | 300 KB |
|
||||||
|
| mh2v303 | Manhunter 2 3.03, 1989-08-17, 5.25" (eXo) | 3.002.149, mh2's (3.02) | 1183 KB |
|
||||||
|
| grv201 | Gold Rush 2.01, 1988-12-22, 3.5", the original (eXo). gr, which ScummVM lists as pirated, is a cracked copy: its logic 129, the speed test, is the original's logic 73 | 3.002.149 (its own) | 1169 KB |
|
||||||
|
| kq4 | King's Quest IV 2.3, 1988-09-27, DOS (eXo) | none here (3.002.086, by its KQ4 prefix) | 1360 KB |
|
||||||
|
| ddpst | Donald Duck's Playground 1.0A, 1986-08-08: the Atari ST release's files, which eXo files as DOS | none (2.435 assumed; ScummVM takes 2.272) | 165 KB |
|
||||||
|
| caitlyn | Caitlyn's Destiny (fan, eXo): the source of the 2.936 interpreter (its AGI.COM is Sierra's SIERRA.COM; no fan hack files) | 2.936 | 1258 KB |
|
||||||
|
|
||||||
|
Starting point (2026-10-19): every v2 game but Mixed-Up Mother Goose boots
|
||||||
|
through its title into play on the host harness; Mother Goose stays black; the
|
||||||
|
v3 games cannot load yet.
|
||||||
|
|
||||||
|
Progress (2026-10-19, end of day):
|
||||||
|
|
||||||
|
- Phase 0: oracle.sh runs any game's sessions (tests/GAME/*.scr) on that
|
||||||
|
game's own loader and interpreter, and the probes on any v2 interpreter
|
||||||
|
(ORACLE_INTERP). Every v2 game has a start session in test-agi.sh
|
||||||
|
(49/49). The logic trace is AGI_HOST_VM_TRACE. Sierra's random generator
|
||||||
|
is in (read from 2.915; see STATUS.md). The timing and clock probes match
|
||||||
|
on 2.426 and 2.439; the other probes on the other interpreters are still
|
||||||
|
to run.
|
||||||
|
- Phase 1: Mother Goose plays (start, set.simple's pointer list of
|
||||||
|
players, the name, the face, the bedroom, the first room, F5 saving
|
||||||
|
under the name, restoring it). 0xAA to 0xB6 and test 0x13 are in, each
|
||||||
|
in its versions; the version keeps all three parts; the cycle and motion
|
||||||
|
modes share Sierra's four bytes; wander and follow.ego are Sierra's. Six
|
||||||
|
more differences found and fixed (STATUS.md, bugs 21 to 26).
|
||||||
|
- The oracle's shots drifted: each DOSBox screenshot's real cost was added
|
||||||
|
to the script's waits (about 1.5 s over twelve shots). Fixed: dosDrive.py
|
||||||
|
keeps each step at its time from the interpreter's start (DOSBox's log
|
||||||
|
line for the graphics mode), as the host harness's virtual clock does.
|
||||||
|
- Phase 2 (2026-10-20): done but for speed and memory (Phase 4).
|
||||||
|
- The v3 loader (agiRes.c: <prefix>DIR, LZW, nibble pictures, clear
|
||||||
|
messages in packed logics). Gold Rush, Manhunter 1 and 2, Demo 3, Black
|
||||||
|
Cauldron 2.10, and King's Quest IV (DOS and Apple IIgs) load every
|
||||||
|
resource and start. Every DOS v3 release with an interpreter here starts
|
||||||
|
as on Sierra's, run as <prefix>.COM.
|
||||||
|
- 3.002.086's rules for King's Quest IV and v3's draw and new.room rules.
|
||||||
|
- The mouse (agiMouse.c): the pointer, click-to-walk, the mouse commands,
|
||||||
|
and the menus and message windows by pointer (tests/kq3/mouse.scr, on
|
||||||
|
the host).
|
||||||
|
- agiPatch.c: King's Quest IV's zombie distance; its manual question and
|
||||||
|
Gold Rush's quiz skipped; Gold Rush's clock fed the real cycle rates.
|
||||||
|
- The eXo releases (above): King's Quest III 2.14 and Space Quest II 2.0F
|
||||||
|
match Sierra's 2.936 at the start, and Police Quest 2.0G, Black Cauldron
|
||||||
|
2.10, and Manhunter 2 3.03 match theirs. test-agi.sh 94/94.
|
||||||
|
|
||||||
|
## Phase 0: a test bench for every game
|
||||||
|
|
||||||
|
1. `scripts/oracle.sh` for any game, each run on its own interpreter (each game
|
||||||
|
folder ships one, or one of the same version stands in: fourteen versions
|
||||||
|
from 2.272 to 3.002.149).
|
||||||
|
2. The probe games run on every one of those interpreters, to measure what
|
||||||
|
differs between real versions (ScummVM treats every 2.426 to 2.440
|
||||||
|
interpreter as one); v3 interpreters need the probes in v3 format.
|
||||||
|
3. A logic trace (rooms, logics called, commands) for finding hangs.
|
||||||
|
4. Smoke sessions for every game in `scripts/test-agi.sh`, as King's Quest III
|
||||||
|
has.
|
||||||
|
5. Sierra's random number generator, matched (decision 6).
|
||||||
|
|
||||||
|
## Phase 1: every DOS v2 game (interpreters 2.089 to 2.936)
|
||||||
|
|
||||||
|
- Mixed-Up Mother Goose's black screen. Leads: the startup flags, hold.key,
|
||||||
|
set.simple, a window over the menu bar, writing v11.
|
||||||
|
- Commands 0xAA to 0xB6 and test 0x13, each in the versions that have it:
|
||||||
|
set.simple, push.script, pop.script, hold.key, set.pri.base, discard.sound,
|
||||||
|
hide.mouse, allow.menu, show.mouse, fence.mouse, get.mse.posn, release.key,
|
||||||
|
adj.ego.move.to.x.y, in.motion.using.mouse.
|
||||||
|
- Version rules: print.at with 3 arguments below 2.089 and quit with none on
|
||||||
|
2.089; move.obj's first step at once only above 2.272; loop changes that
|
||||||
|
ignore step time at 2.272 and below; save and restore stop the sound from
|
||||||
|
2.272; the 2.230 view mirror format; the note envelopes of 2.900 and 3.000.
|
||||||
|
- What some games rely on: motion and cycling sharing storage (King's Quest 1's
|
||||||
|
eagle, Black Cauldron's witches), strings surviving restart (Space Quest 2),
|
||||||
|
stopping a sound setting its flag (Police Quest 1), a tight clock-polling loop
|
||||||
|
yielding (Police Quest 1's poker).
|
||||||
|
- Sierra's script bugs, patched (decision 2): King's Quest 1's bow to the king,
|
||||||
|
King's Quest III's endless fall, the Rosella staircase and the room 77 talk,
|
||||||
|
Larry's leaving the store without paying, Space Quest 1's spider droid and
|
||||||
|
its exploding Roger (f103 still set when picture 20 is drawn).
|
||||||
|
- The interpreter version keeps all three parts (3.002.086 against 3.002.149).
|
||||||
|
|
||||||
|
## Phase 2: AGI v3 (Gold Rush, Manhunter 1 and 2, King's Quest IV, later Black Cauldron)
|
||||||
|
|
||||||
|
- The loader: `<prefix>DIR` with four section offsets, `<prefix>VOL.n`, the
|
||||||
|
7-byte resource header, LZW (9 to 11 bits, 0x100 resets, 0x101 ends),
|
||||||
|
pictures with 4-bit arguments to F0 and F2, messages not encrypted in
|
||||||
|
compressed logics.
|
||||||
|
- 3.002.086 (King's Quest IV): loops chosen for any loop count, the x <= 0 left
|
||||||
|
border, the priority-to-row glitch, one-argument hide.mouse and hold.key, the
|
||||||
|
zombie's distance (patched).
|
||||||
|
- v3 rules: draw reapplies loop and cel; new.room cancels a mouse walk;
|
||||||
|
allow.menu and release.key from 3.098.
|
||||||
|
- The mouse (decision 4): click-to-walk, the mouse commands, clicking the menu
|
||||||
|
bar and message windows, on every port.
|
||||||
|
- Game rules (done 2026-10-20): Gold Rush's clock fed the real cycle rates
|
||||||
|
(v165 and v167, which its speed test sets for a 1988 PC), and its book quiz
|
||||||
|
skipped (decision 3). ScummVM's Manhunter rule, Enter as Space when the
|
||||||
|
input line is off, is not needed: both games map Enter to Space's
|
||||||
|
controller themselves.
|
||||||
|
- Speed and memory: LZW on the 8 MHz machines, 1.2 MB of data.
|
||||||
|
|
||||||
|
## Phase 3: every game played through
|
||||||
|
|
||||||
|
- A scripted walkthrough per game on the host harness (virtual time): the main
|
||||||
|
path, deaths, the score, the endings, checked at checkpoints against the
|
||||||
|
game's own interpreter.
|
||||||
|
- Copy-protection quizzes skipped (decision 3); Larry's age quiz has its
|
||||||
|
own way past, Alt+X (a set.key that sets f109).
|
||||||
|
|
||||||
|
## Phase 4: every game on every machine
|
||||||
|
|
||||||
|
IIgs, Amiga, ST, X68000, DOS, and the hosted ports: memory, speed at 8 MHz,
|
||||||
|
disks (a CFFA2 volume per game on the IIgs; every game fits in 2 MB), and the
|
||||||
|
four-voice sound by ear.
|
||||||
|
|
||||||
|
## Phase 5: the other releases
|
||||||
|
|
||||||
|
- Amiga, Atari ST, Apple IIgs, Macintosh, and CoCo3 data: 4-byte OBJECT entries
|
||||||
|
on the Amiga, the `dirs` and `vol.N` file names, each machine's palettes and
|
||||||
|
font, and its sound: the IIgs's sampled instruments and MIDI-like music, the
|
||||||
|
CoCo3's and Apple II's own note formats.
|
||||||
|
- The v1 booter disk images: King's Quest 2, Black Cauldron, Donald Duck's
|
||||||
|
Playground (directories on disk sectors, the v1 command set, v1 words and
|
||||||
|
sound).
|
||||||
|
- The Apple II disk images.
|
||||||
|
- The Game Adaptation Language games (King's Quest 1 of 1984, Apple II King's
|
||||||
|
Quest 1), which ScummVM does not run either.
|
||||||
20
README.md
20
README.md
|
|
@ -85,15 +85,17 @@ in `build/tests/out/`.
|
||||||
## Checking against Sierra's interpreter
|
## Checking against Sierra's interpreter
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ scripts/oracle.sh # every probe and KQ3 session
|
$ scripts/oracle.sh # every probe and game session
|
||||||
$ scripts/oracle.sh windows parser # or some of them
|
$ scripts/oracle.sh windows parser # or some of them
|
||||||
```
|
```
|
||||||
|
|
||||||
`scripts/oracle.sh` plays the probes (and `tests/kq3/*.scr`) on Sierra's own
|
`scripts/oracle.sh` plays the probes on Sierra's own interpreter (the copy in
|
||||||
interpreter -- the copy in `games/kq3`, in DOSBox Staging at 386SX-16 speed --
|
`games/kq3`, or the game `ORACLE_INTERP` names) and each game's sessions
|
||||||
and on JoeyAGI, then compares the frames with `tests/tools/compareFrames.py`,
|
(`tests/GAME/*.scr`) on that game's interpreter, in DOSBox Staging as a fast
|
||||||
which reads text with each interpreter's font (JoeyAGI's is its own). The
|
386, and on JoeyAGI, then compares the frames with
|
||||||
probes record what happened into variables and print them, so their frames
|
`tests/tools/compareFrames.py`, which reads text with each interpreter's font
|
||||||
match whatever the timing; the KQ3 sessions run in real time on one side and
|
(JoeyAGI's is its own). The probes record what happened into variables and
|
||||||
virtual time on the other, so moving things differ by a cycle or two. STATUS.md
|
print them, so their frames match whatever the timing. The sessions run in
|
||||||
has the results.
|
real time on one side and virtual time on the other; `tests/tools/dosDrive.py`
|
||||||
|
keeps every step at its time from the interpreter's start, so the two stay in
|
||||||
|
step and moving things differ by a cycle at most. STATUS.md has the results.
|
||||||
|
|
|
||||||
187
STATUS.md
187
STATUS.md
|
|
@ -4,8 +4,10 @@ Where JoeyAGI stands against Sierra's own interpreter, subsystem by subsystem
|
||||||
and command by command, with the evidence. Written from a clean-room deep dive
|
and command by command, with the evidence. Written from a clean-room deep dive
|
||||||
(October 2026): the public AGI specifications for the formats and commands,
|
(October 2026): the public AGI specifications for the formats and commands,
|
||||||
and Sierra's interpreter (King's Quest III 2.00, interpreter 2.435, in DOSBox
|
and Sierra's interpreter (King's Quest III 2.00, interpreter 2.435, in DOSBox
|
||||||
Staging at 386SX-16 speed) run as a black box for everything the
|
Staging at 386SX-16 speed; a fast 386 since 2026-10-19) run as a black box for
|
||||||
specifications leave open. No ScummVM, NAGI or Sarien source was read.
|
everything the specifications leave open. No ScummVM, NAGI or Sarien source
|
||||||
|
was read until 2026-10-19; since then ScummVM's AGI engine is read for
|
||||||
|
behavior, never copied (it is GPL), and Sierra's interpreter still decides.
|
||||||
|
|
||||||
## In short
|
## In short
|
||||||
|
|
||||||
|
|
@ -17,7 +19,8 @@ specifications leave open. No ScummVM, NAGI or Sarien source was read.
|
||||||
teleports all behaving as on Sierra's interpreter. Where frames still differ
|
teleports all behaving as on Sierra's interpreter. Where frames still differ
|
||||||
it is in things that move with time or chance (Manannan's sparkles, the cat,
|
it is in things that move with time or chance (Manannan's sparkles, the cat,
|
||||||
the chickens, wandering), because DOSBox runs in real time and the harness in
|
the chickens, wandering), because DOSBox runs in real time and the harness in
|
||||||
virtual time, and the two random generators differ.
|
virtual time, and each interpreter seeds its random numbers from its own
|
||||||
|
clock (the generator itself is Sierra's: see "Random numbers").
|
||||||
- **Every picture is exact.** All 97 KQ3 pictures (visual and priority, 194
|
- **Every picture is exact.** All 97 KQ3 pictures (visual and priority, 194
|
||||||
frames) match Sierra's pixel for pixel, as does a synthetic picture using
|
frames) match Sierra's pixel for pixel, as does a synthetic picture using
|
||||||
every drawing action and all 16 pens, and a long diagonal that shows
|
every drawing action and all 16 pens, and a long diagonal that shows
|
||||||
|
|
@ -38,8 +41,7 @@ specifications leave open. No ScummVM, NAGI or Sarien source was read.
|
||||||
- **All six ports build;** the IIgs disk boots and draws (it was blank when
|
- **All six ports build;** the IIgs disk boots and draws (it was blank when
|
||||||
the dive started), and the ST, Amiga and X68000 builds draw KQ3 correctly
|
the dive started), and the ST, Amiga and X68000 builds draw KQ3 correctly
|
||||||
(`screenshots/port-*.png`).
|
(`screenshots/port-*.png`).
|
||||||
- **Gates:** `make check` passes: host 28/28 (resources, 23 probes, 4 KQ3
|
- **Gates:** `make check` passes: host 94/94 (see "Gates"), disk PASS.
|
||||||
sessions), disk PASS.
|
|
||||||
|
|
||||||
Not verified: playing KQ3 to the end, death scenes, the later lands (Llewdor,
|
Not verified: playing KQ3 to the end, death scenes, the later lands (Llewdor,
|
||||||
the ship, Daventry) beyond their pictures and a few teleports, audio by ear,
|
the ship, Daventry) beyond their pictures and a few teleports, audio by ear,
|
||||||
|
|
@ -79,22 +81,25 @@ Sierra's interpreter.
|
||||||
| Subsystem | Status | Evidence |
|
| Subsystem | Status | Evidence |
|
||||||
|-----------|--------|----------|
|
|-----------|--------|----------|
|
||||||
| Resources (directories, VOL, WORDS.TOK, OBJECT) | Works. Files read whole into buffers of their own size from the Memory Manager on the IIgs; a missing or empty directory just means none of that type. | `resources-kq3`: 125 logics, 97 pictures, 216 views, 34 sounds load and decode |
|
| Resources (directories, VOL, WORDS.TOK, OBJECT) | Works. Files read whole into buffers of their own size from the Memory Manager on the IIgs; a missing or empty directory just means none of that type. | `resources-kq3`: 125 logics, 97 pictures, 216 views, 34 sounds load and decode |
|
||||||
|
| AGI v3 (2026-10-20) | Works: <prefix>DIR (four directories in one file), <prefix>VOL.n (prefix from the known games: KQ4, GR, MH, MH2, BC, DM), 7-byte headers, LZW (9 to 11 bits) and nibble pictures expanded at load, packed logics' messages not decrypted, OBJECT decrypted only when encrypted. Gold Rush (both copies), Manhunter 1 and 2 (3.02 and 3.03), Demo 3, Black Cauldron 2.10, and King's Quest IV (DOS and Apple IIgs) load and start; on Sierra's interpreter (as <prefix>.COM) the DOS ones with an interpreter here start the same. 3.002.086's rules (loops for any loop count, the x <= 0 left border, one-argument hide.mouse and hold.key) and v3's (draw reapplies loop and cel; new.room ends a walk to a click) are in | resources for all of them; `gr-start`, `grv201-start`, `mh1-start`, `mh2-start`, `mh2v303-start`, `bcv210-start`, `agidemo3-start`, `kq4-start`, `kq4gs-start` |
|
||||||
|
| Mouse (2026-10-20) | Works on the host (the DOS interpreters had none; this follows the Amiga, ST, IIgs, and Mac ones as ScummVM describes them): a JoeyLib sprite pointer drawn at each present and kept to fence.mouse's box; a left click on the menu bar opens the menus, in the picture walks ego to it (its baseline's middle; a direction key, or in v3 a new room, stops it); the menus follow the pointer and a click picks; a click closes a message window | `kq3-mouse` (the pointer, a walk, the bar, a hovered item, a click choosing Help, a click closing it) |
|
||||||
|
| Game patches (agiPatch.c) | King's Quest IV's zombie distance; the manual questions of King's Quest IV (rooms 141 to 143) and Gold Rush (room 125) skipped to where a right answer leads; Gold Rush's clock fed the real cycle rates | `kq4-start`, `kq4gs-start`, `grv201-start`; the clock run at Normal, Fast, and Fastest on the host (10 game seconds in 10 seconds each) |
|
||||||
| PICTURE | Exact: the spec's line (exact half rounds away from the start) with Sierra's byte-sized error term, corners, relative lines, scanline fill (visual, priority-only and both), solid and splatter pens of every size with the texture wrap at bit 254 | 194/194 KQ3 frames pixel-exact; `pictures` probe visual + priority exact (`screenshots/probe-pictures-*.png`); `winedge` probe's long diagonal |
|
| PICTURE | Exact: the spec's line (exact half rounds away from the start) with Sierra's byte-sized error term, corners, relative lines, scanline fill (visual, priority-only and both), solid and splatter pens of every size with the texture wrap at bit 254 | 194/194 KQ3 frames pixel-exact; `pictures` probe visual + priority exact (`screenshots/probe-pictures-*.png`); `winedge` probe's long diagonal |
|
||||||
| VIEW | Works: loops, mirrored loops, cel transparency, descriptions; one allocation per view | `resources-kq3` (every cel decoded), `setview`, `parser` (show.obj) |
|
| VIEW | Works: loops, mirrored loops, cel transparency, descriptions; one allocation per view | `resources-kq3` (every cel decoded), `setview`, `parser` (show.obj) |
|
||||||
| Object drawing | Exact: blits stacked with save-unders, not-updated objects under updated ones, sorted by baseline (or band top for fixed priority), control lines take the priority below them, add.to.pic margins (an outline of the control value from the baseline up to its band's top) | `priority` probe incl. the priority screen exact |
|
| Object drawing | Exact: blits stacked with save-unders, not-updated objects under updated ones, sorted by baseline (or band top for fixed priority), control lines take the priority below them, add.to.pic margins (an outline of the control value from the baseline up to its band's top) | `priority` probe incl. the priority screen exact |
|
||||||
| Motion and cycling | Exact: step and cycle counters, the lost first step after draw/new.room/reposition, end.of.loop and reverse.loop flag timing, move.obj arrival (slack = step size), follow.ego distance | `timing`, `motion`, `counters`, `redraw`, `steering`, `cycling` exact; `follow` deterministic rows exact |
|
| Motion and cycling | Exact: step and cycle counters, the lost first step after draw/new.room/reposition, end.of.loop and reverse.loop flag timing, move.obj arrival (slack = step size), follow.ego distance. wander and follow.ego's detours are Sierra's code (read from 2.915): a heading kept about 255 cycles, a new one when an object's step leaves it standing. The cycle and motion modes share four bytes per object, as in Sierra's, so starting one overwrites what another left | `timing`, `motion`, `counters`, `redraw`, `steering`, `cycling` exact; `follow` deterministic rows exact |
|
||||||
| Ego steering | Exact: v6 steers ego before and after logic 0 under player and program control; set.dir on ego has no effect; arrow keys toggle v6 | `egodir`, `steering`, `ego` exact |
|
| Ego steering | Exact: v6 steers ego before and after logic 0 under player and program control; set.dir on ego has no effect; arrow keys toggle v6 | `egodir`, `steering`, `ego` exact |
|
||||||
| Control lines, collisions, position fixing | Exact: barriers, conditional barriers and ignore.blocks, signal lines (f3), water/land, priority 15 ignoring control, the block rectangle, objects' baselines (touching counts, crossing counts), the spiral position fix | `control`, `collide` exact |
|
| Control lines, collisions, position fixing | Exact: barriers, conditional barriers and ignore.blocks, signal lines (f3), water/land, priority 15 ignoring control, the block rectangle, objects' baselines (touching counts, crossing counts), the spiral position fix | `control`, `collide` exact |
|
||||||
| Windows and text | Exact: print/print.at geometry and Sierra's wrap rule, display wrapping, text attributes in picture and text mode, clear.lines/clear.text.rect, the status line, timed windows (v21); print.at windows at columns 0 and 1 as Sierra's byte arithmetic draws and erases them; the screen under a window put back exactly to the pixel | `windows` (12 frames), `winedge` (18 frames), `textscreen`, `clock` exact |
|
| Windows and text | Exact: print/print.at geometry and Sierra's wrap rule, display wrapping, text attributes in picture and text mode, clear.lines/clear.text.rect, the status line, timed windows (v21); print.at windows at columns 0 and 1 as Sierra's byte arithmetic draws and erases them; the screen under a window put back exactly to the pixel | `windows` (12 frames), `winedge` (18 frames), `textscreen`, `clock` exact |
|
||||||
| Input line | Exact: row 23 unless configure.screen moves it, no cursor until set.cursor.char, typed characters drawn in place, Enter and accept.input redraw it, prevent.input clears it; the echo keeps its own position as Sierra's does, so a cursor changed mid-line behaves as there | `parser` probe exact (17 frames); `cursor` probe exact (20 frames) |
|
| Input line | Exact: row 23 unless configure.screen moves it, no cursor until set.cursor.char, typed characters drawn in place, Enter and accept.input redraw it, prevent.input clears it; the echo keeps its own position as Sierra's does, so a cursor changed mid-line behaves as there | `parser` probe exact (17 frames); `cursor` probe exact (20 frames) |
|
||||||
| shake.screen | Exact: 4 beats per unit of the whole screen moved (8, 4) for 4 frames and back for 4, blocking | `shake` recording: beats, frames and offset identical to Sierra's |
|
| shake.screen | Exact: 4 beats per unit of the whole screen moved (8, 4) for 4 frames and back for 4, blocking | `shake` recording: beats, frames and offset identical to Sierra's |
|
||||||
| Interpreter version | Read from AGIDATA.OVL ("Version 2.435"), else the AGI file, else 2.435; the measured quirks follow it | `resources-kq3` (KQ3: 2435) |
|
| Interpreter version | Read from AGIDATA.OVL ("Version 2.435", "Version 3.002.149" as 3149), else the AGI file, else 2.435 (a v3 game 3149); the measured quirks follow it | `resources-kq3` (KQ3: 2435) |
|
||||||
| Parser and said | Exact: separators, lower case, longest dictionary match incl. multi-word entries, ignored words, unknown words (stop, stay in the list for any-word/rest-of-line, v9 = position), all-ignored lines say nothing, v9 cleared each cycle | `parser` exact; a 10-input x 9-pattern said() matrix matched Sierra |
|
| Parser and said | Exact: separators, lower case, longest dictionary match incl. multi-word entries, ignored words, unknown words (stop, stay in the list for any-word/rest-of-line, v9 = position), all-ignored lines say nothing, v9 cleared each cycle | `parser` exact; a 10-input x 9-pattern said() matrix matched Sierra |
|
||||||
| Menus | Exact: bar, items, disabled items, keyboard navigation; kept across restart | `parser` m01..m07 exact; `screenshots/kq3-menu-after-restart.png` |
|
| Menus | Exact: bar, items, disabled items, keyboard navigation; kept across restart | `parser` m01..m07 exact; `screenshots/kq3-menu-after-restart.png` |
|
||||||
| Inventory (status) and show.obj | Exact layout (Sierra draws the inventory in text mode; compared cell by cell) | `parser` p10, m05; `screenshots/kq3-inventory-tab.png` |
|
| Inventory (status) and show.obj | Exact layout (Sierra draws the inventory in text mode; compared cell by cell) | `parser` p10, m05; `screenshots/kq3-inventory-tab.png` |
|
||||||
| Sound | Flag timing exact: the flag is set when the first voice ends (Sierra's PC interpreter plays only that voice), whatever the other voices do; an empty first voice ends at once; f9 off sets it at once; stop.sound sets it. JoeyAGI plays all four voices on JoeyLib. | `sound` probe exact (tunes of 3 to 240 ticks, 1 to 60 notes, long tails, no first voice); audio itself not compared (PC speaker vs. JoeyLib voices) |
|
| Sound | Flag timing exact: the flag is set when the first voice ends (Sierra's PC interpreter plays only that voice), whatever the other voices do; an empty first voice ends at once; f9 off sets it at once; stop.sound sets it. JoeyAGI plays all four voices on JoeyLib. | `sound` probe exact (tunes of 3 to 240 ticks, 1 to 60 notes, long tails, no first voice); audio itself not compared (PC speaker vs. JoeyLib voices) |
|
||||||
| Clock (v11..v14), timed windows (v21) | Clock exact; v21 windows of 3, 2 and 0.5 seconds measured identical to Sierra's against v11, the 1-second one one cycle longer | `clock` probe; KQ3's once-a-second countdowns traced in step with Sierra's |
|
| Clock (v11..v14), timed windows (v21) | Clock exact; v21 windows of 3, 2 and 0.5 seconds measured identical to Sierra's against v11, the 1-second one one cycle longer | `clock` probe; KQ3's once-a-second countdowns traced in step with Sierra's |
|
||||||
| have.key | Works: a key this cycle that nothing else took; a logic that asks again in the same cycle (KQ3's help screen spins on it) waits a frame per ask and takes any key | `pictures`, `priority`, `textscreen`, `ego` probes; `screenshots/kq3-help-f1.png` |
|
| have.key | Works: a key this cycle that nothing else took, else (as Sierra's 2.915 does) one that arrived since the cycle's keys were handled, taken raw even if set.key maps it; a logic that asks again in the same cycle (KQ3's help screen spins on it) waits a frame per ask and takes any key | `pictures`, `priority`, `textscreen`, `ego` probes; `screenshots/kq3-help-f1.png` |
|
||||||
| Save / restore / restart | Work: restore rebuilds the picture from the script and ends the cycle so logic 0 sees f12 next (KQ3 relies on it); restart keeps menus and set.key mappings (KQ3 builds them only once) | `kq3-saverestore` session; restart + menu matches Sierra |
|
| Save / restore / restart | Work: restore rebuilds the picture from the script and ends the cycle so logic 0 sees f12 next (KQ3 relies on it); restart keeps menus and set.key mappings (KQ3 builds them only once) | `kq3-saverestore` session; restart + menu matches Sierra |
|
||||||
| Memory | Logics other than 0 are freed at new.room and so are the views no object shows, as Sierra frees a room's resources | ASan run of every session clean |
|
| Memory | Logics other than 0 are freed at new.room and so are the views no object shows, as Sierra frees a room's resources | ASan run of every session clean |
|
||||||
| Picture output | Every machine pushes a picture rectangle with one jlSurfacePasteIndexed call, each AGI pixel doubled (color runs through jlFillRect on the planar machines before, two jlDrawPixel calls per AGI pixel before that) | IIgs frame outside a moving object byte-identical to the old code; JoeyLib's UBER indexed checks on every port |
|
| Picture output | Every machine pushes a picture rectangle with one jlSurfacePasteIndexed call, each AGI pixel doubled (color runs through jlFillRect on the planar machines before, two jlDrawPixel calls per AGI pixel before that) | IIgs frame outside a moving object byte-identical to the old code; JoeyLib's UBER indexed checks on every port |
|
||||||
|
|
@ -172,6 +177,66 @@ Sierra's interpreter.
|
||||||
carries the error in a byte, which a line long on both sides overflows
|
carries the error in a byte, which a line long on both sides overflows
|
||||||
(below). No KQ3 picture has such a line.
|
(below). No KQ3 picture has such a line.
|
||||||
|
|
||||||
|
Found 2026-10-19 by playing every v2 game's start on its own interpreter
|
||||||
|
(scripts/oracle.sh GAME-start):
|
||||||
|
|
||||||
|
21. **Mixed-Up Mother Goose stayed black:** logic 0 sets up only on f5
|
||||||
|
(the game starts as a room does), and it needs hold.key and set.simple
|
||||||
|
(0xAA to 0xB6 were unknown commands).
|
||||||
|
22. **display() wrapped at 40 minus its column;** Sierra's wraps at 40
|
||||||
|
counted from the text and goes on at column 0 (King's Quest 1's
|
||||||
|
copyright line at column 1 is 39 characters on one row).
|
||||||
|
23. **%w was lower case;** Sierra's keeps the word as typed (Space Quest 1:
|
||||||
|
I don't understand "Wilco").
|
||||||
|
24. **get.string cleared its field in black;** Sierra's draws the text and
|
||||||
|
cursor in the current attribute (Mother Goose's name, black on its white
|
||||||
|
window).
|
||||||
|
25. **f15 kept every window up;** Sierra's clears it after the one print it
|
||||||
|
covers, and show.pic clears it too (Larry's thanks after Alt+X waited
|
||||||
|
on Sierra's, flashed past on JoeyAGI).
|
||||||
|
26. **A restore drew the input line while input was off,** in the text
|
||||||
|
attribute's background (a white bar under Mother Goose's picture);
|
||||||
|
prevent.input now blanks it in black and nothing draws there until
|
||||||
|
accept.input.
|
||||||
|
|
||||||
|
Found 2026-10-20 with the new games (the demos, the Christmas card, the fan
|
||||||
|
games, and the v3 games):
|
||||||
|
|
||||||
|
27. **A loop of tests alone never ended:** Manhunter 1 waits a second with
|
||||||
|
"v11 = 0; while (v49 > v11)", and the clock moved only between cycles.
|
||||||
|
A backward jump with no command since the last one now lets a frame
|
||||||
|
pass, as Sierra's timer interrupt moves the clock while such a loop
|
||||||
|
spins.
|
||||||
|
28. **The prompt was drawn raw;** Sierra's expands string 0's %-codes
|
||||||
|
(Gold Rush's is "%g69", ">").
|
||||||
|
29. **OBJECT was always decrypted;** the demos' and the Christmas card's
|
||||||
|
are not encrypted (encrypted, the first offset is past the file's end).
|
||||||
|
30. **have.key never saw a key set.key maps:** Sierra's 2.915 reads the
|
||||||
|
keyboard there and takes a key that came in after the cycle's keys were
|
||||||
|
handled (Demo 2's Return at "Press any key", when it lands while the
|
||||||
|
logics run). The first ask now polls; on the host, where keys arrive
|
||||||
|
only between cycles, nothing changes.
|
||||||
|
31. **King's Quest IV's manual question** is skipped (decision 3): a
|
||||||
|
new.room to its question rooms 141 to 143 goes to room 96, where a right
|
||||||
|
answer leads (agiPatch.c, the first game-specific change).
|
||||||
|
|
||||||
|
Found 2026-10-20 with the eXo releases (PLAN.md, "The games"):
|
||||||
|
|
||||||
|
32. **Gold Rush's clock stopped at Fast and Fastest** in the cracked copy
|
||||||
|
(games/gr): the crack put logic 73 in place of logic 129, the speed test
|
||||||
|
that sets v165 and v167, so logic 0 copied 0 into v166. In the original
|
||||||
|
the test counts a second of empty cycles at Fastest and takes a third
|
||||||
|
and a quarter for a 1988 PC's playing rate; JoeyAGI holds Fastest to the
|
||||||
|
frame rate, so that clock would run six times fast. Both copies now get
|
||||||
|
the real rates every cycle (agiPatch.c).
|
||||||
|
33. **Gold Rush's quiz** (the original's claim jumper, rooms 125 to 128) is
|
||||||
|
skipped: logic 129's new.room(125) goes to room 73, where a right answer
|
||||||
|
leads, which is also what the crack does.
|
||||||
|
34. **testResources failed King's Quest III 2.14,** whose sound 36 shipped
|
||||||
|
cut to 1035 of its 2281 bytes (the next resource follows it in VOL.1).
|
||||||
|
Such a sound is now listed as truncated; the player already ends a voice
|
||||||
|
where the resource ends.
|
||||||
|
|
||||||
Fixed earlier in the dive (before the gates existed), all verified against
|
Fixed earlier in the dive (before the gates existed), all verified against
|
||||||
Sierra's interpreter: the picture decoder (line algorithm, fill semantics,
|
Sierra's interpreter: the picture decoder (line algorithm, fill semantics,
|
||||||
pens, splatter textures); window geometry and the wrap rule; display wrapping;
|
pens, splatter textures); window geometry and the wrap rule; display wrapping;
|
||||||
|
|
@ -280,6 +345,38 @@ an exact line).
|
||||||
and ends at (134,167). No KQ3 picture has such a line (all 194 frames are
|
and ends at (134,167). No KQ3 picture has such a line (all 194 frames are
|
||||||
unchanged).
|
unchanged).
|
||||||
|
|
||||||
|
## Random numbers
|
||||||
|
|
||||||
|
Read from the 2.915 interpreter (Mixed-Up Mother Goose's AGI, an
|
||||||
|
unencrypted MZ file; the code segment starts 512 bytes in):
|
||||||
|
|
||||||
|
- **The generator.** seed = seed * 0x7C4D + 1 (16 bits); the number is the
|
||||||
|
seed's two bytes XORed (0 to 255). A seed of 0 is replaced first by the
|
||||||
|
BIOS tick count, so it seeds itself at the first use. JoeyAGI's
|
||||||
|
agiRandom does the same with its own clock (virtual time in the host
|
||||||
|
harness, so runs there repeat exactly).
|
||||||
|
- **random(min, max, v).** The number modulo (max - min + 1) in 16 bits,
|
||||||
|
plus min: a maximum below the minimum takes the number as it is. A span of
|
||||||
|
0 (max = min - 1) divides by zero in Sierra's; JoeyAGI takes the number.
|
||||||
|
- **wander.** Each cycle the count (shared byte 0) runs down; when it had
|
||||||
|
run out, or the object's last step left it standing, a new heading,
|
||||||
|
rand % 9 (0 standing still), and, while the count is under 6, a new one,
|
||||||
|
rand % 51. A count that ran out has wrapped to 255, so a heading lasts
|
||||||
|
about 255 cycles unless the object is stopped.
|
||||||
|
- **follow.ego.** Within the distance (byte 0) of ego's middle it stops and
|
||||||
|
sets its flag (byte 1). The detour count (byte 2) starts at 255, meaning
|
||||||
|
"head straight there". A step that left the object standing gives a
|
||||||
|
heading, rand % 9 drawn until it is not 0, and a count, rand % (half the x
|
||||||
|
and y distance + 1) drawn until it reaches the step size, spent a step
|
||||||
|
size per cycle; Sierra's signed byte compare ends it early when the count
|
||||||
|
is 128 or more.
|
||||||
|
- **Standing.** At each step an object's place is compared with where the
|
||||||
|
last step that moved it (or position, draw) left it; the same place sets
|
||||||
|
its "didn't move" flag.
|
||||||
|
|
||||||
|
The 2.4xx interpreter files (AGI) are encrypted, and their loaders decrypt
|
||||||
|
them; the same generator is assumed there.
|
||||||
|
|
||||||
## What remains, most important first
|
## What remains, most important first
|
||||||
|
|
||||||
1. **A full play-through.** KQ3 has not been finished on JoeyAGI. The rooms,
|
1. **A full play-through.** KQ3 has not been finished on JoeyAGI. The rooms,
|
||||||
|
|
@ -294,8 +391,12 @@ an exact line).
|
||||||
5. **Save/restore look.** JoeyAGI has its own slot window (slots 1-9, a
|
5. **Save/restore look.** JoeyAGI has its own slot window (slots 1-9, a
|
||||||
description) instead of Sierra's path prompt and list, and its own file
|
description) instead of Sierra's path prompt and list, and its own file
|
||||||
format, which is per machine type (byte order and padding).
|
format, which is per machine type (byte order and padding).
|
||||||
6. **Random sequences** differ from Sierra's (wander, follow.ego detours,
|
6. **Random sequences** are made by Sierra's generator and spent as
|
||||||
KQ3's Manannan schedule): expected, not a bug.
|
Sierra's spends them (wander, follow.ego's detours, random), but each run
|
||||||
|
seeds it from its own clock, so a run on Sierra's interpreter and one on
|
||||||
|
JoeyAGI only match by chance. The generator was read from the 2.915
|
||||||
|
interpreter; the 2.4xx interpreter files are encrypted and assumed the
|
||||||
|
same.
|
||||||
7. **Other interpreter versions.** Only 2.435 was available to measure; the
|
7. **Other interpreter versions.** Only 2.435 was available to measure; the
|
||||||
quirks are assumed for every 2.x interpreter (see below).
|
quirks are assumed for every 2.x interpreter (see below).
|
||||||
8. **The font** is JoeyAGI's own, not the PC's ROM font.
|
8. **The font** is JoeyAGI's own, not the PC's ROM font.
|
||||||
|
|
@ -433,14 +534,14 @@ the earlier rewrite touched nearly every command.
|
||||||
| 0x74 | word.to.string | 0 | | works |
|
| 0x74 | word.to.string | 0 | | works |
|
||||||
| 0x75 | parse | 0 | | works |
|
| 0x75 | parse | 0 | | works |
|
||||||
| 0x76 | get.num | 13 | | works |
|
| 0x76 | get.num | 13 | | works |
|
||||||
| 0x77 | prevent.input | 75 | | works; clears the input line |
|
| 0x77 | prevent.input | 75 | | works; blanks the input line in black, and nothing draws there until accept.input: FIXED (a restore drew it in the text attribute's background) |
|
||||||
| 0x78 | accept.input | 45 | yes | works; redraws the input line |
|
| 0x78 | accept.input | 45 | yes | works; redraws the input line |
|
||||||
| 0x79 | set.key | 39 | yes | works |
|
| 0x79 | set.key | 39 | yes | works |
|
||||||
| 0x7A | add.to.pic | 66 | yes | works |
|
| 0x7A | add.to.pic | 66 | yes | works |
|
||||||
| 0x7B | add.to.pic.v | 8 | | works |
|
| 0x7B | add.to.pic.v | 8 | | works |
|
||||||
| 0x7C | status | 2 | yes | works: the inventory screen |
|
| 0x7C | status | 2 | yes | works: the inventory screen |
|
||||||
| 0x7D | save.game | 1 | | works: JoeyAGI's own slot window and file format |
|
| 0x7D | save.game | 1 | | works: JoeyAGI's own slot window and file format; under set.simple, no questions, as Sierra's (the slot with that description, else the first empty one) |
|
||||||
| 0x7E | restore.game | 1 | | works: JoeyAGI's own slot window; replays the picture script; FIXED (cycle ends, f12 reaches the next cycle) |
|
| 0x7E | restore.game | 1 | | works: JoeyAGI's own slot window; replays the picture script; FIXED (cycle ends, f12 reaches the next cycle); under set.simple, Sierra's pointer list of descriptions |
|
||||||
| 0x7F | init.disk | 0 | | no-op (nothing to do on these machines) |
|
| 0x7F | init.disk | 0 | | no-op (nothing to do on these machines) |
|
||||||
| 0x80 | restart.game | 1 | | works; FIXED (keeps menus and set.key) |
|
| 0x80 | restart.game | 1 | | works; FIXED (keeps menus and set.key) |
|
||||||
| 0x81 | show.obj | 61 | yes | works |
|
| 0x81 | show.obj | 61 | yes | works |
|
||||||
|
|
@ -484,6 +585,19 @@ the earlier rewrite touched nearly every command.
|
||||||
| 0xA7 | div.n | 0 | | works |
|
| 0xA7 | div.n | 0 | | works |
|
||||||
| 0xA8 | div.v | 0 | | works |
|
| 0xA8 | div.v | 0 | | works |
|
||||||
| 0xA9 | close.window | 0 | | works |
|
| 0xA9 | close.window | 0 | | works |
|
||||||
|
| 0xAA | set.simple | 0 | | works (2.425 on, not 2.440): save and restore by a description, as Sierra's (Mother Goose names the player's save) |
|
||||||
|
| 0xAB | push.script | 0 | | no-op (as ScummVM; no game uses it) |
|
||||||
|
| 0xAC | pop.script | 0 | | no-op (as ScummVM; no game uses it) |
|
||||||
|
| 0xAD | hold.key | 0 | | works (2.425 on, not 2.440): ego walks only while a direction key is down |
|
||||||
|
| 0xAE | set.pri.base | 0 | | works (2.425, and 2.936 on): a y-to-priority table from the base down |
|
||||||
|
| 0xAF | discard.sound | 0 | | no-op (sounds are not kept) |
|
||||||
|
| 0xB0 | hide.mouse | 0 | | works: the pointer hidden; one argument on 3.002.086 |
|
||||||
|
| 0xB1 | allow.menu | 0 | | works (3.098 on): a requested menu does not open |
|
||||||
|
| 0xB2 | show.mouse | 0 | | works: the pointer shown again |
|
||||||
|
| 0xB3 | fence.mouse | 0 | | works: the pointer kept to a box in picture coordinates |
|
||||||
|
| 0xB4 | get.mse.posn | 0 | | works: the pointer in picture coordinates |
|
||||||
|
| 0xB5 | release.key | 0 | | works (3.098 on): ends hold.key |
|
||||||
|
| 0xB6 | adj.ego.move.to.x.y | 0 | | no-op (the Amiga and ST releases' two-argument form waits for Phase 5) |
|
||||||
|
|
||||||
| # | Test | KQ3 uses | Probe | Status |
|
| # | Test | KQ3 uses | Probe | Status |
|
||||||
|---|------|---------:|:-----:|--------|
|
|---|------|---------:|:-----:|--------|
|
||||||
|
|
@ -505,6 +619,7 @@ the earlier rewrite touched nearly every command.
|
||||||
| 0x10 | obj.in.box | 5 | | works |
|
| 0x10 | obj.in.box | 5 | | works |
|
||||||
| 0x11 | center.posn | 0 | | works |
|
| 0x11 | center.posn | 0 | | works |
|
||||||
| 0x12 | right.posn | 0 | | works |
|
| 0x12 | right.posn | 0 | | works |
|
||||||
|
| 0x13 | in.motion.using.mouse | 0 | | works: ego is walking to a click |
|
||||||
|
|
||||||
## Gates
|
## Gates
|
||||||
|
|
||||||
|
|
@ -512,17 +627,45 @@ the earlier rewrite touched nearly every command.
|
||||||
|
|
||||||
| Gate | Result |
|
| Gate | Result |
|
||||||
|------|--------|
|
|------|--------|
|
||||||
| host (`scripts/test-agi.sh`) | 28/28: resources-kq3 (the interpreter version and 472 lines of resource hashes), 23 probes (shake pins every frame of its recording), 4 KQ3 sessions (opening, play, saverestore, teleport) |
|
| host (`scripts/test-agi.sh`) | 94/94: resources for the 22 v2 and 9 v3 folders in games/ (every resource loads and decodes, a sound shipped cut short listed as truncated; KQ3's interpreter version and picture hashes are checked), 29 probes (shake pins every frame of its recording), 5 KQ3 sessions (opening, play, saverestore, teleport, mouse), and a start session for each of the other 29 games (tests/GAME/start.scr: titles, menus, the first room, typed answers) |
|
||||||
| disk (IIgs `AGI.2mg` on a CFFA2, MAME) | PASS (boots, launches, draws: distinctNibbles=6) |
|
| disk (IIgs `AGI.2mg` on a CFFA2, MAME) | PASS (boots, launches, draws: distinctNibbles=6) |
|
||||||
|
|
||||||
`scripts/oracle.sh` (not part of `make check`; needs Sierra's interpreter in
|
`scripts/oracle.sh` (not part of `make check`; needs Sierra's interpreters in
|
||||||
games/kq3, Xvfb, xdotool): probes 89 of 94 shots identical to Sierra's
|
games/, Xvfb, xdotool), 2026-10-19: probes 97 of 103 shots identical to
|
||||||
(the rest: follow.ego's random detours, and the clock probe's 1-second
|
Sierra's, including the shake recording, `winedge` (18/18) and `cursor`
|
||||||
window one cycle long, shown on all four of its frames), including the
|
(20/20). The rest: follow.ego's random detours, the clock probe's 1-second
|
||||||
shake recording and the new `winedge` (18/18) and `cursor` (20/20) probes.
|
window one cycle long (all four of its frames), and egoblocked's ego shown
|
||||||
Its KQ3 sessions (several shots each) drift from Sierra's with real time
|
in another cel of its walk (where it stands and what it recorded match).
|
||||||
and with the screenshot key reaching the game (see "How it was checked"),
|
|
||||||
which is why the KQ3 checkpoints above were taken one screenshot a run.
|
The sessions (tests/GAME/*.scr, 81 shots) used to drift from Sierra's for
|
||||||
|
two reasons, both fixed on 2026-10-19 in tests/tools/dosDrive.py:
|
||||||
|
|
||||||
|
- Every screenshot's real cost was added to the script's waits (about 1.5 s
|
||||||
|
over twelve shots). Now each step happens at its time from the
|
||||||
|
interpreter's start (DOSBox logs its switch to a graphics mode), as the
|
||||||
|
host harness's virtual clock does.
|
||||||
|
- At 386SX-16 speed Sierra's interpreter spends real seconds drawing and
|
||||||
|
loading, where the harness spends none (Black Cauldron's title put it 1.9
|
||||||
|
s behind). The oracle's DOSBox is now a fast 386 (50000 cycles/ms). The
|
||||||
|
timing probe matches at that speed on 2.426, 2.435, and 2.439.
|
||||||
|
|
||||||
|
What still differs is chance (Manannan's orders, Larry's quiz question, the
|
||||||
|
goose, follow.ego), sprites caught in another cel, Sierra's own save
|
||||||
|
dialogs, and KQ3's opening, which each screenshot hurries along (the
|
||||||
|
screenshot key reaches the game; see "How it was checked").
|
||||||
|
|
||||||
|
The sessions of the demos, the Christmas card, and two fan games (28
|
||||||
|
shots, 2026-10-19): 8 identical; the rest differ in color-cycled logos, the
|
||||||
|
fireplace's flames, other things in motion, and AGI Mouse's pointer in The
|
||||||
|
Lost Chapter (JoeyAGI's own pointer differs).
|
||||||
|
|
||||||
|
The eXo releases' start sessions (2026-10-20, scratch pair.sh): King's
|
||||||
|
Quest III 2.14 and Space Quest II 2.0F on the 2.936 from Caitlyn's Destiny,
|
||||||
|
Police Quest 2.0G on AGI Tetris's 2.917, and Black Cauldron 2.10 and
|
||||||
|
Manhunter 2 3.03 on 3.002.149 all agree with Sierra's but for chance (the
|
||||||
|
Police Quest title's bullet holes). Gold Rush 2.01's original goes to its
|
||||||
|
quiz on Sierra's, which JoeyAGI skips, so its session is marked "JoeyAGI
|
||||||
|
only" and oracle.sh leaves it out.
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
|
|
|
||||||
58
agi.c
58
agi.c
|
|
@ -25,8 +25,6 @@
|
||||||
#include "joey/present.h"
|
#include "joey/present.h"
|
||||||
|
|
||||||
|
|
||||||
#define CYCLE_UNIT_MS 50u
|
|
||||||
#define MS_PER_SECOND 1000u
|
|
||||||
#define SECONDS_PER_MINUTE 60u
|
#define SECONDS_PER_MINUTE 60u
|
||||||
#define MINUTES_PER_HOUR 60u
|
#define MINUTES_PER_HOUR 60u
|
||||||
#define HOURS_PER_DAY 24u
|
#define HOURS_PER_DAY 24u
|
||||||
|
|
@ -39,7 +37,11 @@
|
||||||
#define COMPUTER_IBM_PC 0u
|
#define COMPUTER_IBM_PC 0u
|
||||||
#define SOUND_PC 1u
|
#define SOUND_PC 1u
|
||||||
#define MONITOR_EGA 3u
|
#define MONITOR_EGA 3u
|
||||||
#define RANDOM_SEED 0xA61DA61Du
|
// Sierra's random number generator (read from the 2.915 interpreter):
|
||||||
|
// seed = seed * 0x7C4D + 1 in 16 bits, the number its two bytes XORed.
|
||||||
|
#define RANDOM_MULTIPLIER 0x7C4Du
|
||||||
|
#define RANDOM_BYTE_SHIFT 8u
|
||||||
|
#define RANDOM_BYTE_MASK 0xFFu
|
||||||
#define STATUS_TEXT_MAX 160u
|
#define STATUS_TEXT_MAX 160u
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -65,6 +67,7 @@ static void tickClock(void);
|
||||||
AgiStateT *gAgi;
|
AgiStateT *gAgi;
|
||||||
static uint32_t gClockMs;
|
static uint32_t gClockMs;
|
||||||
static uint32_t gClockLastMs;
|
static uint32_t gClockLastMs;
|
||||||
|
static uint16_t gRandomSeed;
|
||||||
|
|
||||||
|
|
||||||
// ----- Internal helpers (alphabetical) -----
|
// ----- Internal helpers (alphabetical) -----
|
||||||
|
|
@ -109,6 +112,11 @@ static void changeRoom(void) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
gAgi->vars[VAR_EGO_EDGE] = AGI_EDGE_NONE;
|
gAgi->vars[VAR_EGO_EDGE] = AGI_EDGE_NONE;
|
||||||
|
// AGI 3 ends a walk to a click at a new room.
|
||||||
|
if (gAgi->game.v3 && ego->motion == AGI_MOTION_MOUSE) {
|
||||||
|
ego->motion = AGI_MOTION_NORMAL;
|
||||||
|
gAgi->vars[VAR_EGO_DIR] = AGI_DIR_NONE;
|
||||||
|
}
|
||||||
(void)vmLoadLogic(gAgi->vars[VAR_ROOM]);
|
(void)vmLoadLogic(gAgi->vars[VAR_ROOM]);
|
||||||
gAgi->logicLoaded[gAgi->vars[VAR_ROOM]] = true;
|
gAgi->logicLoaded[gAgi->vars[VAR_ROOM]] = true;
|
||||||
gAgi->flags[FLAG_ENTERED] = 0u;
|
gAgi->flags[FLAG_ENTERED] = 0u;
|
||||||
|
|
@ -127,12 +135,15 @@ static void cycle(void) {
|
||||||
gAgi->vars[VAR_BAD_WORD] = 0u;
|
gAgi->vars[VAR_BAD_WORD] = 0u;
|
||||||
if (gAgi->menuRequested) {
|
if (gAgi->menuRequested) {
|
||||||
gAgi->menuRequested = false;
|
gAgi->menuRequested = false;
|
||||||
chosen = textMenuInput();
|
if (!gAgi->menuDenied) {
|
||||||
if (chosen != AGI_NO_CONTROLLER) {
|
chosen = textMenuInput();
|
||||||
gAgi->controllers[chosen] = 1u;
|
if (chosen != AGI_NO_CONTROLLER) {
|
||||||
|
gAgi->controllers[chosen] = 1u;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
inputProcess();
|
inputProcess();
|
||||||
|
agiPatchCycle();
|
||||||
// v6 steers ego, under player and program control alike, unless
|
// v6 steers ego, under player and program control alike, unless
|
||||||
// move.obj, wander or follow.ego has it: before logic 0 and again
|
// move.obj, wander or follow.ego has it: before logic 0 and again
|
||||||
// after it (so v6 set by a logic moves ego this cycle and set.dir on
|
// after it (so v6 set by a logic moves ego this cycle and set.dir on
|
||||||
|
|
@ -184,8 +195,8 @@ static void tickClock(void) {
|
||||||
now = jlMillisElapsed();
|
now = jlMillisElapsed();
|
||||||
gClockMs = gClockMs + (now - gClockLastMs);
|
gClockMs = gClockMs + (now - gClockLastMs);
|
||||||
gClockLastMs = now;
|
gClockLastMs = now;
|
||||||
while (gClockMs >= MS_PER_SECOND) {
|
while (gClockMs >= AGI_MS_PER_SECOND) {
|
||||||
gClockMs -= MS_PER_SECOND;
|
gClockMs -= AGI_MS_PER_SECOND;
|
||||||
gAgi->vars[VAR_SECONDS]++;
|
gAgi->vars[VAR_SECONDS]++;
|
||||||
if (gAgi->vars[VAR_SECONDS] < SECONDS_PER_MINUTE) {
|
if (gAgi->vars[VAR_SECONDS] < SECONDS_PER_MINUTE) {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -216,7 +227,9 @@ void agiIdle(void) {
|
||||||
inputPoll();
|
inputPoll();
|
||||||
soundPump();
|
soundPump();
|
||||||
tickClock();
|
tickClock();
|
||||||
|
mouseDrawPointer();
|
||||||
jlStagePresent();
|
jlStagePresent();
|
||||||
|
mouseErasePointer();
|
||||||
jlAudioFrameTick();
|
jlAudioFrameTick();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -224,7 +237,7 @@ void agiIdle(void) {
|
||||||
// new.room: logic stops; the room changes before logic 0 runs again.
|
// new.room: logic stops; the room changes before logic 0 runs again.
|
||||||
void agiNewRoom(uint8_t room) {
|
void agiNewRoom(uint8_t room) {
|
||||||
gAgi->newRoomPending = true;
|
gAgi->newRoomPending = true;
|
||||||
gAgi->newRoomNumber = room;
|
gAgi->newRoomNumber = agiPatchRoom(room);
|
||||||
gAgi->abortLogic = true;
|
gAgi->abortLogic = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -259,6 +272,18 @@ void agiQuit(bool immediately) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// A random number from 0 to 255, as Sierra's interpreter makes them. Its
|
||||||
|
// seed is the clock when the first is wanted (Sierra's takes the BIOS
|
||||||
|
// tick count), and again whenever the seed comes round to 0.
|
||||||
|
uint8_t agiRandom(void) {
|
||||||
|
if (gRandomSeed == 0u) {
|
||||||
|
gRandomSeed = (uint16_t)jlMillisElapsed();
|
||||||
|
}
|
||||||
|
gRandomSeed = (uint16_t)(gRandomSeed * RANDOM_MULTIPLIER + 1u);
|
||||||
|
return (uint8_t)((gRandomSeed ^ (gRandomSeed >> RANDOM_BYTE_SHIFT)) & RANDOM_BYTE_MASK);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Everything as it is when the game starts -- but the menus and the
|
// Everything as it is when the game starts -- but the menus and the
|
||||||
// set.key mappings, which a game makes once: KQ3 skips both when it is
|
// set.key mappings, which a game makes once: KQ3 skips both when it is
|
||||||
// restarted (f6), and Sierra's interpreter keeps them.
|
// restarted (f6), and Sierra's interpreter keeps them.
|
||||||
|
|
@ -277,6 +302,11 @@ void agiResetState(void) {
|
||||||
gAgi->itemRoom[i] = agiItemStartRoom(&gAgi->game, (uint8_t)i);
|
gAgi->itemRoom[i] = agiItemStartRoom(&gAgi->game, (uint8_t)i);
|
||||||
}
|
}
|
||||||
gAgi->menuRequested = false;
|
gAgi->menuRequested = false;
|
||||||
|
gAgi->menuDenied = false;
|
||||||
|
gAgi->holdKey = false;
|
||||||
|
gAgi->simpleSaveOn = false;
|
||||||
|
gAgi->simpleSaveDesc[0] = '\0';
|
||||||
|
gAgi->mouseHidden = false;
|
||||||
gAgi->horizon = AGI_DEFAULT_HORIZON;
|
gAgi->horizon = AGI_DEFAULT_HORIZON;
|
||||||
gAgi->blockActive = false;
|
gAgi->blockActive = false;
|
||||||
gAgi->playerControl = true;
|
gAgi->playerControl = true;
|
||||||
|
|
@ -302,8 +332,13 @@ void agiResetState(void) {
|
||||||
gAgi->vars[VAR_COMPUTER] = COMPUTER_IBM_PC;
|
gAgi->vars[VAR_COMPUTER] = COMPUTER_IBM_PC;
|
||||||
gAgi->vars[VAR_SOUND_TYPE] = SOUND_PC;
|
gAgi->vars[VAR_SOUND_TYPE] = SOUND_PC;
|
||||||
gAgi->vars[VAR_MONITOR] = MONITOR_EGA;
|
gAgi->vars[VAR_MONITOR] = MONITOR_EGA;
|
||||||
|
// The game starts as a room does (f5): Mixed-Up Mother Goose's logic 0
|
||||||
|
// sets itself up only on f5 with v0 still 0, and without it stays black.
|
||||||
|
gAgi->flags[FLAG_NEW_ROOM] = 1u;
|
||||||
gAgi->flags[FLAG_FIRST_LOGIC0] = 1u;
|
gAgi->flags[FLAG_FIRST_LOGIC0] = 1u;
|
||||||
gAgi->flags[FLAG_SOUND] = 1u;
|
gAgi->flags[FLAG_SOUND] = 1u;
|
||||||
|
memset(gAgi->mouseFence, 0, sizeof(gAgi->mouseFence));
|
||||||
|
agiPriorityReset();
|
||||||
saveScriptClear();
|
saveScriptClear();
|
||||||
inputReset();
|
inputReset();
|
||||||
}
|
}
|
||||||
|
|
@ -339,7 +374,6 @@ int main(int argc, char **argv) {
|
||||||
jlLogReset();
|
jlLogReset();
|
||||||
jlPaletteSet(jlStageGet(), 0u, kAgiPalette);
|
jlPaletteSet(jlStageGet(), 0u, kAgiPalette);
|
||||||
jlSurfaceClear(jlStageGet(), AGI_COLOR_BLACK);
|
jlSurfaceClear(jlStageGet(), AGI_COLOR_BLACK);
|
||||||
jlRandomSeed(RANDOM_SEED);
|
|
||||||
gAgi = (AgiStateT *)jlAlloc(sizeof(AgiStateT));
|
gAgi = (AgiStateT *)jlAlloc(sizeof(AgiStateT));
|
||||||
if (gAgi == NULL) {
|
if (gAgi == NULL) {
|
||||||
jlShutdown();
|
jlShutdown();
|
||||||
|
|
@ -354,6 +388,7 @@ int main(int argc, char **argv) {
|
||||||
}
|
}
|
||||||
gAgi->objectCount = gAgi->game.maxObjects;
|
gAgi->objectCount = gAgi->game.maxObjects;
|
||||||
soundInit();
|
soundInit();
|
||||||
|
mouseInit();
|
||||||
agiResetState();
|
agiResetState();
|
||||||
gClockLastMs = jlMillisElapsed();
|
gClockLastMs = jlMillisElapsed();
|
||||||
lastCycleMs = gClockLastMs;
|
lastCycleMs = gClockLastMs;
|
||||||
|
|
@ -373,9 +408,10 @@ int main(int argc, char **argv) {
|
||||||
do {
|
do {
|
||||||
agiIdle();
|
agiIdle();
|
||||||
now = jlMillisElapsed();
|
now = jlMillisElapsed();
|
||||||
} while (now - lastCycleMs < (uint32_t)gAgi->vars[VAR_CYCLE_DELAY] * CYCLE_UNIT_MS && !gAgi->quitRequested);
|
} while (now - lastCycleMs < (uint32_t)gAgi->vars[VAR_CYCLE_DELAY] * AGI_CYCLE_UNIT_MS && !gAgi->quitRequested);
|
||||||
lastCycleMs = now;
|
lastCycleMs = now;
|
||||||
}
|
}
|
||||||
|
mouseShutdown();
|
||||||
soundShutdown();
|
soundShutdown();
|
||||||
gfxFree();
|
gfxFree();
|
||||||
agiResClose(&gAgi->game);
|
agiResClose(&gAgi->game);
|
||||||
|
|
|
||||||
121
agi.h
121
agi.h
|
|
@ -13,6 +13,8 @@
|
||||||
// agiInput.c the keyboard, the command line and the parser
|
// agiInput.c the keyboard, the command line and the parser
|
||||||
// agiSound.c PCjr sound resources on JoeyLib's tone and noise voices
|
// agiSound.c PCjr sound resources on JoeyLib's tone and noise voices
|
||||||
// agiSave.c save, restore and the picture script restore replays
|
// agiSave.c save, restore and the picture script restore replays
|
||||||
|
// agiMouse.c the pointer, click-to-walk, and the menus' pointer
|
||||||
|
// agiPatch.c changes to particular games: copy protection, script bugs
|
||||||
// agiVm.c the LOGIC interpreter
|
// agiVm.c the LOGIC interpreter
|
||||||
// agi.c start-up and the interpreter cycle
|
// agi.c start-up and the interpreter cycle
|
||||||
|
|
||||||
|
|
@ -45,6 +47,10 @@
|
||||||
#define AGI_TEXT_ROWS 25u
|
#define AGI_TEXT_ROWS 25u
|
||||||
#define AGI_CELL_PX 8
|
#define AGI_CELL_PX 8
|
||||||
|
|
||||||
|
// Time: v10 delays each cycle by twentieths of a second.
|
||||||
|
#define AGI_MS_PER_SECOND 1000u
|
||||||
|
#define AGI_CYCLE_UNIT_MS 50u
|
||||||
|
|
||||||
// Colors. The picture starts white with priority 4 (red) everywhere.
|
// Colors. The picture starts white with priority 4 (red) everywhere.
|
||||||
#define AGI_COLOR_BLACK 0u
|
#define AGI_COLOR_BLACK 0u
|
||||||
#define AGI_COLOR_RED 4u
|
#define AGI_COLOR_RED 4u
|
||||||
|
|
@ -121,6 +127,11 @@
|
||||||
#define AGI_KEY_RIGHT 0x4D00u
|
#define AGI_KEY_RIGHT 0x4D00u
|
||||||
#define AGI_KEY_JOY_BUTTON_1 0x0101u
|
#define AGI_KEY_JOY_BUTTON_1 0x0101u
|
||||||
#define AGI_KEY_JOY_BUTTON_2 0x0201u
|
#define AGI_KEY_JOY_BUTTON_2 0x0201u
|
||||||
|
// The keypad's 5: ego stops (hold.key mode sends it when the direction key
|
||||||
|
// last pressed is let go).
|
||||||
|
#define AGI_KEY_STATIONARY 0x4C00u
|
||||||
|
// A left click, queued with the keys (no key has this code).
|
||||||
|
#define AGI_KEY_CLICK 0xF101u // ScummVM's code for it, so v19 reads alike
|
||||||
|
|
||||||
// A menu.input or inventory answer meaning "nothing chosen".
|
// A menu.input or inventory answer meaning "nothing chosen".
|
||||||
#define AGI_NO_CONTROLLER 0xFFu
|
#define AGI_NO_CONTROLLER 0xFFu
|
||||||
|
|
@ -181,8 +192,19 @@ extern const uint8_t kAgiFont[AGI_FONT_GLYPHS][AGI_FONT_ROWS];
|
||||||
|
|
||||||
// The Sierra interpreter a game shipped with, as major * 1000 + minor
|
// The Sierra interpreter a game shipped with, as major * 1000 + minor
|
||||||
// (2.435 is 2435), read from the game's AGIDATA.OVL (or AGI) when its
|
// (2.435 is 2435), read from the game's AGIDATA.OVL (or AGI) when its
|
||||||
// folder has one; otherwise the version the quirks were measured on.
|
// folder has one; otherwise the version the quirks were measured on, or
|
||||||
|
// for an AGI v3 game whose folder says nothing (a non-DOS release) the last
|
||||||
|
// v3 interpreter, as ScummVM assumes for most.
|
||||||
#define AGI_VERSION_MEASURED 2435u
|
#define AGI_VERSION_MEASURED 2435u
|
||||||
|
#define AGI_VERSION_V3_FIRST 3000u
|
||||||
|
#define AGI_VERSION_V3_ASSUMED 3149u
|
||||||
|
// Interpreters whose rules differ: 2.272 (the Christmas card, Donald
|
||||||
|
// Duck) and 3.002.086 (every King's Quest IV).
|
||||||
|
#define AGI_VERSION_2272 2272u
|
||||||
|
#define AGI_VERSION_KQ4 3086u
|
||||||
|
|
||||||
|
// What distance reports for an object not on the screen.
|
||||||
|
#define AGI_DISTANCE_FAR 255u
|
||||||
|
|
||||||
// Behavior of particular Sierra interpreter versions that games may lean
|
// Behavior of particular Sierra interpreter versions that games may lean
|
||||||
// on; agiQuirk says whether the game's version has one (the version ranges
|
// on; agiQuirk says whether the game's version has one (the version ranges
|
||||||
|
|
@ -218,6 +240,20 @@ typedef struct {
|
||||||
uint16_t offsetLow;
|
uint16_t offsetLow;
|
||||||
} AgiResEntryT;
|
} AgiResEntryT;
|
||||||
|
|
||||||
|
// The longest v3 file-name prefix ("MH2" of MH2DIR and MH2VOL.0).
|
||||||
|
#define AGI_PREFIX_MAX 3u
|
||||||
|
|
||||||
|
// The games agiPatch.c changes, known by their v3 file prefix.
|
||||||
|
typedef enum {
|
||||||
|
AGI_GAME_UNKNOWN = 0,
|
||||||
|
AGI_GAME_KQ4,
|
||||||
|
AGI_GAME_GOLD_RUSH,
|
||||||
|
AGI_GAME_MANHUNTER_1,
|
||||||
|
AGI_GAME_MANHUNTER_2,
|
||||||
|
AGI_GAME_BLACK_CAULDRON,
|
||||||
|
AGI_GAME_DEMO
|
||||||
|
} AgiGameIdE;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
FILE *volFiles[AGI_MAX_VOLUMES];
|
FILE *volFiles[AGI_MAX_VOLUMES];
|
||||||
uint16_t resCount[AGI_RES_COUNT];
|
uint16_t resCount[AGI_RES_COUNT];
|
||||||
|
|
@ -228,6 +264,11 @@ typedef struct {
|
||||||
uint16_t itemCount;
|
uint16_t itemCount;
|
||||||
uint8_t maxObjects;
|
uint8_t maxObjects;
|
||||||
uint16_t interpreterVersion;
|
uint16_t interpreterVersion;
|
||||||
|
// AGI v3: one <prefix>DIR holds the four directories, the volumes are
|
||||||
|
// <prefix>VOL.n, and resources have 7-byte headers and may be packed.
|
||||||
|
bool v3;
|
||||||
|
char prefix[AGI_PREFIX_MAX + 1u];
|
||||||
|
AgiGameIdE id;
|
||||||
} AgiGameT;
|
} AgiGameT;
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -268,7 +309,8 @@ typedef enum {
|
||||||
AGI_MOTION_NORMAL = 0,
|
AGI_MOTION_NORMAL = 0,
|
||||||
AGI_MOTION_WANDER = 1,
|
AGI_MOTION_WANDER = 1,
|
||||||
AGI_MOTION_FOLLOW = 2,
|
AGI_MOTION_FOLLOW = 2,
|
||||||
AGI_MOTION_MOVE = 3
|
AGI_MOTION_MOVE = 3,
|
||||||
|
AGI_MOTION_MOUSE = 4 // ego walking to a click (agiMouse.c)
|
||||||
} AgiMotionE;
|
} AgiMotionE;
|
||||||
|
|
||||||
// Directions: 0 is still, 1..8 run clockwise from north.
|
// Directions: 0 is still, 1..8 run clockwise from north.
|
||||||
|
|
@ -306,6 +348,22 @@ typedef enum {
|
||||||
// after end.of.loop or reverse.loop on an object already drawn.
|
// after end.of.loop or reverse.loop on an object already drawn.
|
||||||
#define OBJ_SKIP_STEP 0x0800u
|
#define OBJ_SKIP_STEP 0x0800u
|
||||||
#define OBJ_SKIP_CYCLE 0x1000u
|
#define OBJ_SKIP_CYCLE 0x1000u
|
||||||
|
// Its last step left it where it was (wander and follow.ego turn aside).
|
||||||
|
#define OBJ_DIDNT_MOVE 0x2000u
|
||||||
|
|
||||||
|
// The four bytes an object's cycle and motion modes share, as in Sierra's
|
||||||
|
// interpreter: starting one mode overwrites what another left there, and
|
||||||
|
// games rely on it (King's Quest 1's eagle, Black Cauldron's witches).
|
||||||
|
#define AGI_OBJ_PARMS 4u
|
||||||
|
#define PARM_LOOP_FLAG 0u // end.of.loop, reverse.loop: the flag
|
||||||
|
#define PARM_WANDER_COUNT 0u // wander: cycles left on this heading
|
||||||
|
#define PARM_FOLLOW_STEP 0u // follow.ego: how near is there
|
||||||
|
#define PARM_FOLLOW_FLAG 1u
|
||||||
|
#define PARM_FOLLOW_COUNT 2u // follow.ego: a detour's steps left
|
||||||
|
#define PARM_MOVE_X 0u // move.obj
|
||||||
|
#define PARM_MOVE_Y 1u
|
||||||
|
#define PARM_MOVE_STEP 2u // move.obj: the step size to restore
|
||||||
|
#define PARM_MOVE_FLAG 3u
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
const AgiViewT *view;
|
const AgiViewT *view;
|
||||||
|
|
@ -328,13 +386,7 @@ typedef struct {
|
||||||
uint8_t cycleCount;
|
uint8_t cycleCount;
|
||||||
uint8_t cycleMode;
|
uint8_t cycleMode;
|
||||||
uint8_t motion;
|
uint8_t motion;
|
||||||
uint8_t loopFlag;
|
uint8_t parm[AGI_OBJ_PARMS];
|
||||||
uint8_t motionFlag;
|
|
||||||
uint8_t targetX;
|
|
||||||
uint8_t targetY;
|
|
||||||
uint8_t savedStepSize;
|
|
||||||
uint8_t randomSteps;
|
|
||||||
uint8_t followDistance;
|
|
||||||
// Blit state: where the object was last drawn and the buffer bytes
|
// Blit state: where the object was last drawn and the buffer bytes
|
||||||
// its cel covered there, restored when it is erased.
|
// its cel covered there, restored when it is erased.
|
||||||
bool blitted;
|
bool blitted;
|
||||||
|
|
@ -445,6 +497,21 @@ typedef struct {
|
||||||
bool menuSubmitted;
|
bool menuSubmitted;
|
||||||
bool menuRequested;
|
bool menuRequested;
|
||||||
uint8_t menuLastMenu;
|
uint8_t menuLastMenu;
|
||||||
|
bool menuDenied; // allow.menu(0): a requested menu does not open
|
||||||
|
|
||||||
|
// The modes of the 2.425-and-later commands: hold.key (ego walks only
|
||||||
|
// while a direction key is down), set.simple (save and restore without
|
||||||
|
// asking, under that description), set.pri.base (a y-to-priority table
|
||||||
|
// of its own instead of the 12-row bands).
|
||||||
|
bool holdKey;
|
||||||
|
bool simpleSaveOn;
|
||||||
|
char simpleSaveDesc[AGI_STRING_LEN + 1u];
|
||||||
|
bool priorityTableSet;
|
||||||
|
uint8_t priorityTable[AGI_HEIGHT];
|
||||||
|
|
||||||
|
// The mouse (AGI 3): hidden by hide.mouse, kept to fence.mouse's box.
|
||||||
|
bool mouseHidden;
|
||||||
|
uint8_t mouseFence[4];
|
||||||
|
|
||||||
// Requests that end the cycle, and a restore that just did.
|
// Requests that end the cycle, and a restore that just did.
|
||||||
bool quitRequested;
|
bool quitRequested;
|
||||||
|
|
@ -455,6 +522,13 @@ typedef struct {
|
||||||
extern AgiStateT *gAgi;
|
extern AgiStateT *gAgi;
|
||||||
|
|
||||||
|
|
||||||
|
// ----- agiPatch.c -----
|
||||||
|
|
||||||
|
void agiPatchCycle(void);
|
||||||
|
uint8_t agiPatchDistance(uint8_t var, uint8_t distance);
|
||||||
|
uint8_t agiPatchRoom(uint8_t room);
|
||||||
|
|
||||||
|
|
||||||
// ----- agiRes.c -----
|
// ----- agiRes.c -----
|
||||||
|
|
||||||
void agiDecrypt(uint8_t *data, uint16_t length);
|
void agiDecrypt(uint8_t *data, uint16_t length);
|
||||||
|
|
@ -464,6 +538,7 @@ bool agiQuirk(AgiQuirkE quirk);
|
||||||
void agiResClose(AgiGameT *game);
|
void agiResClose(AgiGameT *game);
|
||||||
uint8_t *agiResLoad(const AgiGameT *game, AgiResTypeE type, uint16_t index, uint16_t *outLength);
|
uint8_t *agiResLoad(const AgiGameT *game, AgiResTypeE type, uint16_t index, uint16_t *outLength);
|
||||||
bool agiResOpen(AgiGameT *game);
|
bool agiResOpen(AgiGameT *game);
|
||||||
|
bool agiResPacked(const AgiGameT *game, AgiResTypeE type, uint16_t index);
|
||||||
bool agiResPresent(const AgiGameT *game, AgiResTypeE type, uint16_t index);
|
bool agiResPresent(const AgiGameT *game, AgiResTypeE type, uint16_t index);
|
||||||
uint16_t agiWordMatch(const AgiGameT *game, const char *text, uint8_t *outLen);
|
uint16_t agiWordMatch(const AgiGameT *game, const char *text, uint8_t *outLen);
|
||||||
|
|
||||||
|
|
@ -472,6 +547,8 @@ uint16_t agiWordMatch(const AgiGameT *game, const char *text, uint8_t *outLen
|
||||||
|
|
||||||
bool agiPicDecode(uint8_t *visual, uint8_t *priority, const uint8_t *data, uint16_t length);
|
bool agiPicDecode(uint8_t *visual, uint8_t *priority, const uint8_t *data, uint16_t length);
|
||||||
uint8_t agiPriorityForY(int16_t y);
|
uint8_t agiPriorityForY(int16_t y);
|
||||||
|
void agiPriorityReset(void);
|
||||||
|
void agiPrioritySetBase(uint8_t base);
|
||||||
int16_t agiYForPriority(uint8_t priority);
|
int16_t agiYForPriority(uint8_t priority);
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -509,6 +586,8 @@ void objAnimate(uint8_t n);
|
||||||
void objClearAll(void);
|
void objClearAll(void);
|
||||||
uint8_t objDirectionTo(int16_t fromX, int16_t fromY, int16_t toX, int16_t toY, uint8_t slack);
|
uint8_t objDirectionTo(int16_t fromX, int16_t fromY, int16_t toX, int16_t toY, uint8_t slack);
|
||||||
void objFixPosition(uint8_t n);
|
void objFixPosition(uint8_t n);
|
||||||
|
void objFollowEgo(uint8_t n, uint8_t step, uint8_t flag);
|
||||||
|
void objMouseWalk(uint8_t x, uint8_t y);
|
||||||
void objMoveTo(uint8_t n, uint8_t x, uint8_t y, uint8_t step, uint8_t flag);
|
void objMoveTo(uint8_t n, uint8_t x, uint8_t y, uint8_t step, uint8_t flag);
|
||||||
void objPlanMotion(void);
|
void objPlanMotion(void);
|
||||||
void objSetCel(uint8_t n, uint8_t cel);
|
void objSetCel(uint8_t n, uint8_t cel);
|
||||||
|
|
@ -518,6 +597,17 @@ void objUnanimateAll(void);
|
||||||
void objUpdateAll(void);
|
void objUpdateAll(void);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- agiMouse.c -----
|
||||||
|
|
||||||
|
void mouseClick(void);
|
||||||
|
void mouseDrawPointer(void);
|
||||||
|
void mouseErasePointer(void);
|
||||||
|
void mouseInit(void);
|
||||||
|
bool mouseOver(int16_t *col, int16_t *row);
|
||||||
|
void mousePosition(uint8_t *x, uint8_t *y);
|
||||||
|
void mouseShutdown(void);
|
||||||
|
|
||||||
|
|
||||||
// ----- agiText.c -----
|
// ----- agiText.c -----
|
||||||
|
|
||||||
void textClearLines(uint8_t top, uint8_t bottom, uint8_t color);
|
void textClearLines(uint8_t top, uint8_t bottom, uint8_t color);
|
||||||
|
|
@ -535,10 +625,12 @@ void textMenuAddItem(const char *name, uint8_t controller);
|
||||||
uint8_t textMenuInput(void);
|
uint8_t textMenuInput(void);
|
||||||
void textMessageBox(const char *text);
|
void textMessageBox(const char *text);
|
||||||
void textPrint(const char *text, int16_t row, int16_t col, uint8_t width);
|
void textPrint(const char *text, int16_t row, int16_t col, uint8_t width);
|
||||||
|
uint8_t textPrompt(char *out);
|
||||||
void textSetAttribute(uint8_t fg, uint8_t bg);
|
void textSetAttribute(uint8_t fg, uint8_t bg);
|
||||||
void textShowObject(uint8_t viewId);
|
void textShowObject(uint8_t viewId);
|
||||||
void textTextMode(void);
|
void textTextMode(void);
|
||||||
void textUpdateStatus(void);
|
void textUpdateStatus(void);
|
||||||
|
void textWindowPut(uint8_t line, uint8_t col, const char *text);
|
||||||
|
|
||||||
|
|
||||||
// ----- agiInput.c -----
|
// ----- agiInput.c -----
|
||||||
|
|
@ -550,6 +642,7 @@ void inputParse(const char *line);
|
||||||
void inputPoll(void);
|
void inputPoll(void);
|
||||||
void inputProcess(void);
|
void inputProcess(void);
|
||||||
void inputReset(void);
|
void inputReset(void);
|
||||||
|
uint16_t inputPollKey(void);
|
||||||
uint16_t inputWaitKey(void);
|
uint16_t inputWaitKey(void);
|
||||||
bool inputWaitPrintClose(uint16_t timeoutHalfSeconds);
|
bool inputWaitPrintClose(uint16_t timeoutHalfSeconds);
|
||||||
|
|
||||||
|
|
@ -574,6 +667,15 @@ void saveScriptClear(void);
|
||||||
|
|
||||||
// ----- agiVm.c -----
|
// ----- agiVm.c -----
|
||||||
|
|
||||||
|
// The VM's trace, in AGI_TRACE builds (the host harness): gAgiTrace, when
|
||||||
|
// set, hears every command a logic runs (its arguments; result -1) and every
|
||||||
|
// condition (op 0xFF, no arguments; result 0 or 1), with the logic and the
|
||||||
|
// command's offset in it.
|
||||||
|
#if defined(AGI_TRACE)
|
||||||
|
typedef void (*AgiTraceFnT)(uint8_t logic, uint16_t pc, uint8_t op, const uint8_t *args, uint8_t argCount, int8_t result);
|
||||||
|
extern AgiTraceFnT gAgiTrace;
|
||||||
|
#endif
|
||||||
|
|
||||||
void vmCallLogic(uint8_t logicId);
|
void vmCallLogic(uint8_t logicId);
|
||||||
void vmDiscardLogics(void);
|
void vmDiscardLogics(void);
|
||||||
bool vmLoadLogic(uint8_t logicId);
|
bool vmLoadLogic(uint8_t logicId);
|
||||||
|
|
@ -587,6 +689,7 @@ void agiIdle(void);
|
||||||
void agiNewRoom(uint8_t room);
|
void agiNewRoom(uint8_t room);
|
||||||
void agiObjectStatus(uint8_t n);
|
void agiObjectStatus(uint8_t n);
|
||||||
void agiQuit(bool immediately);
|
void agiQuit(bool immediately);
|
||||||
|
uint8_t agiRandom(void);
|
||||||
void agiResetState(void);
|
void agiResetState(void);
|
||||||
void agiRestartGame(void);
|
void agiRestartGame(void);
|
||||||
|
|
||||||
|
|
|
||||||
3
agiGfx.c
3
agiGfx.c
|
|
@ -47,7 +47,6 @@
|
||||||
#define SHAKE_DY 4
|
#define SHAKE_DY 4
|
||||||
#define SHAKE_PHASE_FRAMES 4u
|
#define SHAKE_PHASE_FRAMES 4u
|
||||||
#define SHAKE_BEATS_PER_UNIT 4u
|
#define SHAKE_BEATS_PER_UNIT 4u
|
||||||
#define MS_PER_SECOND 1000u
|
|
||||||
// A chunky surface's byte holds two pixels.
|
// A chunky surface's byte holds two pixels.
|
||||||
#define CHUNKY_PX_PER_BYTE (JL_SURFACE_WIDTH / JL_SURFACE_BYTES_PER_ROW)
|
#define CHUNKY_PX_PER_BYTE (JL_SURFACE_WIDTH / JL_SURFACE_BYTES_PER_ROW)
|
||||||
|
|
||||||
|
|
@ -601,7 +600,7 @@ void gfxShakeScreen(uint8_t times) {
|
||||||
jlSurfaceCopy(normal, jlStageGet());
|
jlSurfaceCopy(normal, jlStageGet());
|
||||||
buildShifted(shifted, normal);
|
buildShifted(shifted, normal);
|
||||||
}
|
}
|
||||||
phaseMs = (uint32_t)SHAKE_PHASE_FRAMES * MS_PER_SECOND / jlFrameHz();
|
phaseMs = (uint32_t)SHAKE_PHASE_FRAMES * AGI_MS_PER_SECOND / jlFrameHz();
|
||||||
beats = (uint16_t)((uint16_t)times * SHAKE_BEATS_PER_UNIT);
|
beats = (uint16_t)((uint16_t)times * SHAKE_BEATS_PER_UNIT);
|
||||||
do {
|
do {
|
||||||
shakePhase(shifted, phaseMs);
|
shakePhase(shifted, phaseMs);
|
||||||
|
|
|
||||||
128
agiInput.c
128
agiInput.c
|
|
@ -69,6 +69,8 @@ static uint16_t gQueue[KEY_QUEUE_SIZE];
|
||||||
static uint8_t gQueueHead;
|
static uint8_t gQueueHead;
|
||||||
static uint8_t gQueueTail;
|
static uint8_t gQueueTail;
|
||||||
static uint8_t gJoyDir;
|
static uint8_t gJoyDir;
|
||||||
|
// The direction key last pressed: under hold.key, letting it go stops ego.
|
||||||
|
static jlKeyE gHoldLastKey;
|
||||||
// A key this cycle that nothing took (no controller, not steering ego,
|
// A key this cycle that nothing took (no controller, not steering ego,
|
||||||
// no command line to type into), left for have.key.
|
// no command line to type into), left for have.key.
|
||||||
static bool gKeyUnused;
|
static bool gKeyUnused;
|
||||||
|
|
@ -124,11 +126,17 @@ static void enqueue(uint16_t key) {
|
||||||
|
|
||||||
|
|
||||||
static void handleKey(uint16_t key) {
|
static void handleKey(uint16_t key) {
|
||||||
|
char prompt[AGI_TEXT_COLS + 1u];
|
||||||
uint8_t i;
|
uint8_t i;
|
||||||
uint8_t dir;
|
uint8_t dir;
|
||||||
uint8_t maxLen;
|
uint8_t maxLen;
|
||||||
uint8_t oldLen;
|
uint8_t oldLen;
|
||||||
|
|
||||||
|
// A click is the mouse's (the menus or a walk), not a key for v19.
|
||||||
|
if (key == AGI_KEY_CLICK) {
|
||||||
|
mouseClick();
|
||||||
|
return;
|
||||||
|
}
|
||||||
gAgi->vars[VAR_KEY] = (uint8_t)key;
|
gAgi->vars[VAR_KEY] = (uint8_t)key;
|
||||||
for (i = 0u; i < gAgi->keyMapCount; i++) {
|
for (i = 0u; i < gAgi->keyMapCount; i++) {
|
||||||
if (gAgi->keyMaps[i].key == key) {
|
if (gAgi->keyMaps[i].key == key) {
|
||||||
|
|
@ -136,10 +144,21 @@ static void handleKey(uint16_t key) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (key == AGI_KEY_STATIONARY) {
|
||||||
|
if (gAgi->playerControl) {
|
||||||
|
gAgi->vars[VAR_EGO_DIR] = AGI_DIR_NONE;
|
||||||
|
gAgi->objects[AGI_EGO].direction = AGI_DIR_NONE;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
dir = keyDirection(key);
|
dir = keyDirection(key);
|
||||||
if (dir != AGI_DIR_NONE) {
|
if (dir != AGI_DIR_NONE) {
|
||||||
// A second press of the direction ego is walking stops it.
|
// A second press of the direction ego is walking stops it; any
|
||||||
|
// stops a walk to a click.
|
||||||
if (gAgi->playerControl) {
|
if (gAgi->playerControl) {
|
||||||
|
if (gAgi->objects[AGI_EGO].motion == AGI_MOTION_MOUSE) {
|
||||||
|
gAgi->objects[AGI_EGO].motion = AGI_MOTION_NORMAL;
|
||||||
|
}
|
||||||
gAgi->vars[VAR_EGO_DIR] = (gAgi->vars[VAR_EGO_DIR] == dir) ? AGI_DIR_NONE : dir;
|
gAgi->vars[VAR_EGO_DIR] = (gAgi->vars[VAR_EGO_DIR] == dir) ? AGI_DIR_NONE : dir;
|
||||||
gAgi->objects[AGI_EGO].direction = gAgi->vars[VAR_EGO_DIR];
|
gAgi->objects[AGI_EGO].direction = gAgi->vars[VAR_EGO_DIR];
|
||||||
}
|
}
|
||||||
|
|
@ -149,7 +168,7 @@ static void handleKey(uint16_t key) {
|
||||||
gKeyUnused = true;
|
gKeyUnused = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
maxLen = (uint8_t)(AGI_TEXT_COLS - 1u - strlen(gAgi->strings[0]));
|
maxLen = (uint8_t)(AGI_TEXT_COLS - 1u - textPrompt(prompt));
|
||||||
if (gAgi->vars[VAR_INPUT_MAX] != 0u && gAgi->vars[VAR_INPUT_MAX] < maxLen) {
|
if (gAgi->vars[VAR_INPUT_MAX] != 0u && gAgi->vars[VAR_INPUT_MAX] < maxLen) {
|
||||||
maxLen = gAgi->vars[VAR_INPUT_MAX];
|
maxLen = gAgi->vars[VAR_INPUT_MAX];
|
||||||
}
|
}
|
||||||
|
|
@ -221,6 +240,8 @@ bool inputGetString(char *dst, uint8_t maxLen, uint8_t row, uint8_t col, const c
|
||||||
char line[AGI_INPUT_MAX + 1u];
|
char line[AGI_INPUT_MAX + 1u];
|
||||||
char shown[AGI_INPUT_MAX + 2u];
|
char shown[AGI_INPUT_MAX + 2u];
|
||||||
uint8_t len;
|
uint8_t len;
|
||||||
|
uint8_t shownLen;
|
||||||
|
uint8_t drawn;
|
||||||
uint16_t key;
|
uint16_t key;
|
||||||
uint8_t fieldCol;
|
uint8_t fieldCol;
|
||||||
bool onInputLine;
|
bool onInputLine;
|
||||||
|
|
@ -237,14 +258,24 @@ bool inputGetString(char *dst, uint8_t maxLen, uint8_t row, uint8_t col, const c
|
||||||
}
|
}
|
||||||
fieldCol = (uint8_t)(col + strlen(prompt));
|
fieldCol = (uint8_t)(col + strlen(prompt));
|
||||||
textDisplay(row, col, prompt);
|
textDisplay(row, col, prompt);
|
||||||
line[0] = '\0';
|
line[0] = '\0';
|
||||||
len = 0u;
|
len = 0u;
|
||||||
done = false;
|
drawn = 0u;
|
||||||
|
done = false;
|
||||||
while (!done) {
|
while (!done) {
|
||||||
|
// The text and the cursor in the current attribute, as Sierra's
|
||||||
|
// draws them (nothing clears the field first: Mother Goose's name
|
||||||
|
// is black on its white window); spaces over what a backspace
|
||||||
|
// left behind.
|
||||||
memcpy(shown, line, len);
|
memcpy(shown, line, len);
|
||||||
shown[len] = gAgi->cursorChar;
|
shown[len] = gAgi->cursorChar;
|
||||||
shown[len + 1u] = '\0';
|
shown[len + 1u] = '\0';
|
||||||
textClearRect(row, fieldCol, row, (uint8_t)(fieldCol + maxLen), AGI_COLOR_BLACK);
|
shownLen = (uint8_t)strlen(shown);
|
||||||
|
while (shownLen < drawn) {
|
||||||
|
shown[shownLen++] = ' ';
|
||||||
|
}
|
||||||
|
shown[shownLen] = '\0';
|
||||||
|
drawn = shownLen;
|
||||||
textDisplay(row, fieldCol, shown);
|
textDisplay(row, fieldCol, shown);
|
||||||
key = inputWaitKey();
|
key = inputWaitKey();
|
||||||
if (key == AGI_KEY_ESCAPE) {
|
if (key == AGI_KEY_ESCAPE) {
|
||||||
|
|
@ -263,10 +294,13 @@ bool inputGetString(char *dst, uint8_t maxLen, uint8_t row, uint8_t col, const c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// have.key: a key this cycle that nothing else took. A logic that asks
|
// have.key: a key this cycle that nothing else took, else (as Sierra's
|
||||||
// again in the same cycle is waiting for a key (KQ3's help screen spins
|
// 2.915 does) a key that arrived since the cycle's keys were handled,
|
||||||
// on have.key), so from then on a frame passes per ask and any key that
|
// taken as it is: one set.key maps to a controller counts too when it comes
|
||||||
// arrives counts, as Sierra's interpreter reads the keyboard there.
|
// in while the logics run (Demo 2's Return at its "Press any key"). A
|
||||||
|
// logic that asks again in the same cycle is waiting for a key (KQ3's help
|
||||||
|
// screen spins on have.key), so from then on a frame passes per ask and any
|
||||||
|
// key that arrives counts.
|
||||||
bool inputHaveKey(void) {
|
bool inputHaveKey(void) {
|
||||||
uint16_t key;
|
uint16_t key;
|
||||||
|
|
||||||
|
|
@ -276,9 +310,10 @@ bool inputHaveKey(void) {
|
||||||
}
|
}
|
||||||
if (!gHaveKeyAsked) {
|
if (!gHaveKeyAsked) {
|
||||||
gHaveKeyAsked = true;
|
gHaveKeyAsked = true;
|
||||||
return false;
|
inputPoll();
|
||||||
|
} else {
|
||||||
|
agiIdle();
|
||||||
}
|
}
|
||||||
agiIdle();
|
|
||||||
key = dequeue();
|
key = dequeue();
|
||||||
if (key == AGI_KEY_NONE) {
|
if (key == AGI_KEY_NONE) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -291,30 +326,33 @@ bool inputHaveKey(void) {
|
||||||
// Parse a typed line into word groups for said().
|
// Parse a typed line into word groups for said().
|
||||||
void inputParse(const char *line) {
|
void inputParse(const char *line) {
|
||||||
char text[AGI_INPUT_MAX + 1u];
|
char text[AGI_INPUT_MAX + 1u];
|
||||||
|
char typed[AGI_INPUT_MAX + 1u];
|
||||||
uint8_t n;
|
uint8_t n;
|
||||||
uint8_t pos;
|
uint8_t pos;
|
||||||
uint8_t len;
|
uint8_t len;
|
||||||
uint16_t id;
|
uint16_t id;
|
||||||
bool space;
|
bool space;
|
||||||
|
|
||||||
// Lower case, punctuation to spaces, runs of spaces to one.
|
// Punctuation to spaces, runs of spaces to one; matched in lower case,
|
||||||
|
// while %w gives each word as it was typed (Sierra's "Wilco").
|
||||||
n = 0u;
|
n = 0u;
|
||||||
space = true;
|
space = true;
|
||||||
while (*line != '\0' && n < AGI_INPUT_MAX) {
|
while (*line != '\0' && n < AGI_INPUT_MAX) {
|
||||||
char c = *line++;
|
char c = *line++;
|
||||||
|
|
||||||
if (c >= 'A' && c <= 'Z') {
|
|
||||||
c = (char)(c - 'A' + 'a');
|
|
||||||
}
|
|
||||||
if (strchr(PARSE_SEPARATORS, c) != NULL) {
|
if (strchr(PARSE_SEPARATORS, c) != NULL) {
|
||||||
if (!space) {
|
if (!space) {
|
||||||
text[n++] = ' ';
|
text[n] = ' ';
|
||||||
|
typed[n] = ' ';
|
||||||
|
n++;
|
||||||
}
|
}
|
||||||
space = true;
|
space = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
text[n++] = c;
|
typed[n] = c;
|
||||||
space = false;
|
text[n] = (c >= 'A' && c <= 'Z') ? (char)(c - 'A' + 'a') : c;
|
||||||
|
n++;
|
||||||
|
space = false;
|
||||||
}
|
}
|
||||||
while (n > 0u && text[n - 1u] == ' ') {
|
while (n > 0u && text[n - 1u] == ' ') {
|
||||||
n--;
|
n--;
|
||||||
|
|
@ -334,7 +372,7 @@ void inputParse(const char *line) {
|
||||||
// its text for %w; v9 says where it was.
|
// its text for %w; v9 says where it was.
|
||||||
len = 0u;
|
len = 0u;
|
||||||
while (text[pos + len] != '\0' && text[pos + len] != ' ' && len < AGI_WORD_MAX_LEN) {
|
while (text[pos + len] != '\0' && text[pos + len] != ' ' && len < AGI_WORD_MAX_LEN) {
|
||||||
gAgi->wordText[gAgi->wordCount][len] = text[pos + len];
|
gAgi->wordText[gAgi->wordCount][len] = typed[pos + len];
|
||||||
len++;
|
len++;
|
||||||
}
|
}
|
||||||
gAgi->wordText[gAgi->wordCount][len] = '\0';
|
gAgi->wordText[gAgi->wordCount][len] = '\0';
|
||||||
|
|
@ -344,7 +382,7 @@ void inputParse(const char *line) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (id != AGI_WORD_IGNORE) {
|
if (id != AGI_WORD_IGNORE) {
|
||||||
memcpy(gAgi->wordText[gAgi->wordCount], &text[pos], len);
|
memcpy(gAgi->wordText[gAgi->wordCount], &typed[pos], len);
|
||||||
gAgi->wordText[gAgi->wordCount][len] = '\0';
|
gAgi->wordText[gAgi->wordCount][len] = '\0';
|
||||||
gAgi->wordIds[gAgi->wordCount] = id;
|
gAgi->wordIds[gAgi->wordCount] = id;
|
||||||
gAgi->wordCount++;
|
gAgi->wordCount++;
|
||||||
|
|
@ -395,17 +433,30 @@ void inputPoll(void) {
|
||||||
enqueue((uint16_t)((uint16_t)(FKEY_SCAN_FIRST + i) << SCAN_SHIFT));
|
enqueue((uint16_t)((uint16_t)(FKEY_SCAN_FIRST + i) << SCAN_SHIFT));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (jlMousePressed(JL_MOUSE_BUTTON_LEFT)) {
|
||||||
|
enqueue(AGI_KEY_CLICK);
|
||||||
|
}
|
||||||
if (jlKeyPressed(JL_KEY_UP)) {
|
if (jlKeyPressed(JL_KEY_UP)) {
|
||||||
enqueue(AGI_KEY_UP);
|
enqueue(AGI_KEY_UP);
|
||||||
|
gHoldLastKey = JL_KEY_UP;
|
||||||
}
|
}
|
||||||
if (jlKeyPressed(JL_KEY_DOWN)) {
|
if (jlKeyPressed(JL_KEY_DOWN)) {
|
||||||
enqueue(AGI_KEY_DOWN);
|
enqueue(AGI_KEY_DOWN);
|
||||||
|
gHoldLastKey = JL_KEY_DOWN;
|
||||||
}
|
}
|
||||||
if (jlKeyPressed(JL_KEY_LEFT)) {
|
if (jlKeyPressed(JL_KEY_LEFT)) {
|
||||||
enqueue(AGI_KEY_LEFT);
|
enqueue(AGI_KEY_LEFT);
|
||||||
|
gHoldLastKey = JL_KEY_LEFT;
|
||||||
}
|
}
|
||||||
if (jlKeyPressed(JL_KEY_RIGHT)) {
|
if (jlKeyPressed(JL_KEY_RIGHT)) {
|
||||||
enqueue(AGI_KEY_RIGHT);
|
enqueue(AGI_KEY_RIGHT);
|
||||||
|
gHoldLastKey = JL_KEY_RIGHT;
|
||||||
|
}
|
||||||
|
// hold.key: ego walks only while the direction key last pressed is
|
||||||
|
// down (letting go of an earlier one changes nothing).
|
||||||
|
if (gAgi->holdKey && gHoldLastKey != JL_KEY_NONE && jlKeyReleased(gHoldLastKey)) {
|
||||||
|
enqueue(AGI_KEY_STATIONARY);
|
||||||
|
gHoldLastKey = JL_KEY_NONE;
|
||||||
}
|
}
|
||||||
if (jlJoystickConnected(JL_JOYSTICK_0)) {
|
if (jlJoystickConnected(JL_JOYSTICK_0)) {
|
||||||
if (jlJoyPressed(JL_JOYSTICK_0, JL_JOY_BUTTON_0)) {
|
if (jlJoyPressed(JL_JOYSTICK_0, JL_JOY_BUTTON_0)) {
|
||||||
|
|
@ -440,23 +491,35 @@ void inputProcess(void) {
|
||||||
|
|
||||||
|
|
||||||
void inputReset(void) {
|
void inputReset(void) {
|
||||||
gQueueHead = 0u;
|
gQueueHead = 0u;
|
||||||
gQueueTail = 0u;
|
gQueueTail = 0u;
|
||||||
gKeyUnused = false;
|
gKeyUnused = false;
|
||||||
|
gHoldLastKey = JL_KEY_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Block until a key, keeping the game's clock and sound going.
|
// Block until a key, keeping the game's clock and sound going.
|
||||||
|
// A key if one is waiting, else one that comes in over the next frame,
|
||||||
|
// else AGI_KEY_NONE (the menus watch the pointer between keys).
|
||||||
|
uint16_t inputPollKey(void) {
|
||||||
|
uint16_t key;
|
||||||
|
|
||||||
|
key = dequeue();
|
||||||
|
if (key == AGI_KEY_NONE) {
|
||||||
|
agiIdle();
|
||||||
|
key = dequeue();
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
uint16_t inputWaitKey(void) {
|
uint16_t inputWaitKey(void) {
|
||||||
uint16_t key;
|
uint16_t key;
|
||||||
|
|
||||||
for (;;) {
|
do {
|
||||||
key = dequeue();
|
key = inputPollKey();
|
||||||
if (key != AGI_KEY_NONE) {
|
} while (key == AGI_KEY_NONE);
|
||||||
return key;
|
return key;
|
||||||
}
|
|
||||||
agiIdle();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -470,7 +533,8 @@ bool inputWaitPrintClose(uint16_t timeoutHalfSeconds) {
|
||||||
start = jlMillisElapsed();
|
start = jlMillisElapsed();
|
||||||
for (;;) {
|
for (;;) {
|
||||||
key = dequeue();
|
key = dequeue();
|
||||||
if (key == AGI_KEY_ENTER) {
|
// A click in a message window closes it as Enter does.
|
||||||
|
if (key == AGI_KEY_ENTER || key == AGI_KEY_CLICK) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (key == AGI_KEY_ESCAPE) {
|
if (key == AGI_KEY_ESCAPE) {
|
||||||
|
|
|
||||||
232
agiMouse.c
Normal file
232
agiMouse.c
Normal file
|
|
@ -0,0 +1,232 @@
|
||||||
|
// The mouse (PLAN.md decision 4). Sierra's DOS interpreters had none; its
|
||||||
|
// Amiga, ST, IIgs, and Macintosh ones did, and this follows them as
|
||||||
|
// ScummVM describes them:
|
||||||
|
//
|
||||||
|
// - A pointer is drawn over the screen at every present while the machine
|
||||||
|
// has a mouse and the game has not hidden it (hide.mouse), kept to the
|
||||||
|
// box fence.mouse gives.
|
||||||
|
// - A left click on the menu bar opens the menus (as menu.input would);
|
||||||
|
// in the picture, with the player in control, ego walks to it: its
|
||||||
|
// baseline's middle to the click (objMouseWalk). A direction key, or in
|
||||||
|
// AGI 3 a new room, stops the walk.
|
||||||
|
// - In the menus the pointer picks the menu and the item, and in a message
|
||||||
|
// window a click is Enter (agiText.c, agiInput.c).
|
||||||
|
|
||||||
|
#include "agi.h"
|
||||||
|
|
||||||
|
#include "joey/core.h"
|
||||||
|
#include "joey/input.h"
|
||||||
|
#include "joey/sprite.h"
|
||||||
|
#include "joey/tile.h"
|
||||||
|
|
||||||
|
|
||||||
|
// The pointer: a 16x16 arrow, its point the hot spot. '.' is
|
||||||
|
// transparent, 'k' its outline (dark grey: color 0, black, is the
|
||||||
|
// sprite's transparent one), 'w' its white.
|
||||||
|
#define POINTER_TILES 2u
|
||||||
|
#define POINTER_PX (POINTER_TILES * JL_TILE_PIXELS_PER_SIDE)
|
||||||
|
#define POINTER_OUTLINE 8u
|
||||||
|
#define POINTER_FILL 15u
|
||||||
|
#define PIXELS_PER_BYTE 2u
|
||||||
|
#define NIBBLE_BITS 4u
|
||||||
|
#define FENCE_X1 0u
|
||||||
|
#define FENCE_Y1 1u
|
||||||
|
#define FENCE_X2 2u
|
||||||
|
#define FENCE_Y2 3u
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Prototypes -----
|
||||||
|
|
||||||
|
static void pointerAt(int16_t *x, int16_t *y);
|
||||||
|
static bool pointerShown(void);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Tables -----
|
||||||
|
|
||||||
|
static const char *const kPointerArt[POINTER_PX] = {
|
||||||
|
"k...............",
|
||||||
|
"kk..............",
|
||||||
|
"kwk.............",
|
||||||
|
"kwwk............",
|
||||||
|
"kwwwk...........",
|
||||||
|
"kwwwwk..........",
|
||||||
|
"kwwwwwk.........",
|
||||||
|
"kwwwwwwk........",
|
||||||
|
"kwwwwwwwk.......",
|
||||||
|
"kwwwwwkkkk......",
|
||||||
|
"kwwkwwk.........",
|
||||||
|
"kwk.kwwk........",
|
||||||
|
"kk..kwwk........",
|
||||||
|
"k....kwwk.......",
|
||||||
|
".....kwwk.......",
|
||||||
|
"......kk........"
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Module state -----
|
||||||
|
|
||||||
|
static uint8_t gPointerTiles[POINTER_TILES * POINTER_TILES * JL_TILE_BYTES];
|
||||||
|
static jlSpriteT *gPointer;
|
||||||
|
static uint16_t gPointerUnder[jlSpriteBackupLength(POINTER_TILES, POINTER_TILES)];
|
||||||
|
static jlSpriteBackupT gPointerSaved;
|
||||||
|
static bool gPointerDrawn;
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Internal helpers (alphabetical) -----
|
||||||
|
|
||||||
|
// Where the pointer is on the screen, kept inside fence.mouse's box (in
|
||||||
|
// picture coordinates; all zero is no fence).
|
||||||
|
static void pointerAt(int16_t *x, int16_t *y) {
|
||||||
|
const uint8_t *f;
|
||||||
|
int16_t left;
|
||||||
|
int16_t top;
|
||||||
|
int16_t right;
|
||||||
|
int16_t bottom;
|
||||||
|
|
||||||
|
*x = jlMouseX();
|
||||||
|
*y = jlMouseY();
|
||||||
|
f = gAgi->mouseFence;
|
||||||
|
if (f[FENCE_X1] == 0u && f[FENCE_Y1] == 0u && f[FENCE_X2] == 0u && f[FENCE_Y2] == 0u) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
left = (int16_t)(f[FENCE_X1] * AGI_SCREEN_PX_PER_AGI);
|
||||||
|
right = (int16_t)(f[FENCE_X2] * AGI_SCREEN_PX_PER_AGI);
|
||||||
|
top = (int16_t)(f[FENCE_Y1] + gAgi->picRow * AGI_CELL_PX);
|
||||||
|
bottom = (int16_t)(f[FENCE_Y2] + gAgi->picRow * AGI_CELL_PX);
|
||||||
|
if (*x < left) {
|
||||||
|
*x = left;
|
||||||
|
} else if (*x > right) {
|
||||||
|
*x = right;
|
||||||
|
}
|
||||||
|
if (*y < top) {
|
||||||
|
*y = top;
|
||||||
|
} else if (*y > bottom) {
|
||||||
|
*y = bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool pointerShown(void) {
|
||||||
|
return gPointer != NULL && jlMousePresent() && !gAgi->mouseHidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Public API (alphabetical) -----
|
||||||
|
|
||||||
|
// A left click in a cycle (inputProcess): the menus on the menu bar, else
|
||||||
|
// a walk for ego.
|
||||||
|
void mouseClick(void) {
|
||||||
|
int16_t x;
|
||||||
|
int16_t y;
|
||||||
|
uint8_t ax;
|
||||||
|
uint8_t ay;
|
||||||
|
|
||||||
|
if (!pointerShown()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pointerAt(&x, &y);
|
||||||
|
if (y < (int16_t)AGI_CELL_PX) {
|
||||||
|
if (gAgi->flags[FLAG_MENU] != 0u) {
|
||||||
|
gAgi->menuRequested = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (gAgi->playerControl && !gAgi->textMode) {
|
||||||
|
mousePosition(&ax, &ay);
|
||||||
|
objMouseWalk(ax, ay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The pointer onto the stage just before a present, what it covers kept.
|
||||||
|
void mouseDrawPointer(void) {
|
||||||
|
int16_t x;
|
||||||
|
int16_t y;
|
||||||
|
|
||||||
|
if (!pointerShown()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pointerAt(&x, &y);
|
||||||
|
jlSpriteSaveAndDraw(jlStageGet(), gPointer, x, y, &gPointerSaved);
|
||||||
|
gPointerDrawn = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// And off again just after, so nothing else ever draws under it.
|
||||||
|
void mouseErasePointer(void) {
|
||||||
|
if (!gPointerDrawn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
jlSpriteRestoreUnder(jlStageGet(), &gPointerSaved);
|
||||||
|
gPointerDrawn = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void mouseInit(void) {
|
||||||
|
uint8_t tx;
|
||||||
|
uint8_t ty;
|
||||||
|
uint8_t r;
|
||||||
|
uint8_t b;
|
||||||
|
uint8_t pixel[PIXELS_PER_BYTE];
|
||||||
|
uint8_t p;
|
||||||
|
char c;
|
||||||
|
|
||||||
|
for (ty = 0u; ty < POINTER_TILES; ty++) {
|
||||||
|
for (tx = 0u; tx < POINTER_TILES; tx++) {
|
||||||
|
uint8_t *tile = &gPointerTiles[(ty * POINTER_TILES + tx) * JL_TILE_BYTES];
|
||||||
|
|
||||||
|
for (r = 0u; r < JL_TILE_PIXELS_PER_SIDE; r++) {
|
||||||
|
for (b = 0u; b < JL_TILE_BYTES_PER_ROW; b++) {
|
||||||
|
for (p = 0u; p < PIXELS_PER_BYTE; p++) {
|
||||||
|
c = kPointerArt[ty * JL_TILE_PIXELS_PER_SIDE + r][tx * JL_TILE_PIXELS_PER_SIDE + b * PIXELS_PER_BYTE + p];
|
||||||
|
pixel[p] = (c == 'k') ? POINTER_OUTLINE : ((c == 'w') ? POINTER_FILL : 0u);
|
||||||
|
}
|
||||||
|
tile[r * JL_TILE_BYTES_PER_ROW + b] = (uint8_t)((pixel[0] << NIBBLE_BITS) | pixel[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gPointerSaved.bytes = (uint8_t *)gPointerUnder;
|
||||||
|
gPointer = jlSpriteCreate(gPointerTiles, POINTER_TILES, POINTER_TILES);
|
||||||
|
if (gPointer != NULL) {
|
||||||
|
(void)jlSpriteCompile(gPointer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The text cell under the pointer, when one is shown.
|
||||||
|
bool mouseOver(int16_t *col, int16_t *row) {
|
||||||
|
int16_t x;
|
||||||
|
int16_t y;
|
||||||
|
|
||||||
|
if (!pointerShown()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
pointerAt(&x, &y);
|
||||||
|
*col = (int16_t)(x / AGI_CELL_PX);
|
||||||
|
*row = (int16_t)(y / AGI_CELL_PX);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// get.mse.posn: the pointer in picture coordinates, kept to the picture.
|
||||||
|
void mousePosition(uint8_t *x, uint8_t *y) {
|
||||||
|
int16_t sx;
|
||||||
|
int16_t sy;
|
||||||
|
int16_t mx;
|
||||||
|
int16_t my;
|
||||||
|
|
||||||
|
pointerAt(&sx, &sy);
|
||||||
|
mx = (int16_t)(sx / AGI_SCREEN_PX_PER_AGI);
|
||||||
|
my = (int16_t)(sy - (int16_t)gAgi->picRow * AGI_CELL_PX);
|
||||||
|
*x = (uint8_t)(mx < 0 ? 0 : (mx > AGI_WIDTH - 1 ? AGI_WIDTH - 1 : mx));
|
||||||
|
*y = (uint8_t)(my < 0 ? 0 : (my > AGI_MAX_Y ? AGI_MAX_Y : my));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void mouseShutdown(void) {
|
||||||
|
if (gPointer != NULL) {
|
||||||
|
jlSpriteDestroy(gPointer);
|
||||||
|
gPointer = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
224
agiObj.c
224
agiObj.c
|
|
@ -14,23 +14,30 @@
|
||||||
#include "agi.h"
|
#include "agi.h"
|
||||||
|
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#include "joey/core.h"
|
#include "joey/core.h"
|
||||||
|
|
||||||
|
|
||||||
// Wandering objects walk a random direction for a random number of steps.
|
// Sierra's wander: a new heading lasts rand % 51 cycles, drawn until it is
|
||||||
#define WANDER_STEPS_MIN 6u
|
// 6 or more. follow.ego's detour count starts at FOLLOW_START, its own
|
||||||
#define WANDER_STEPS_SPAN 45u
|
// mark for "head straight for ego".
|
||||||
#define FOLLOW_DETOUR_MIN 2u
|
#define WANDER_COUNT_MIN 6u
|
||||||
#define FOLLOW_DETOUR_SPAN 8u
|
#define WANDER_COUNT_LIMIT 51u
|
||||||
|
#define FOLLOW_START 0xFFu
|
||||||
|
// A mouse walk's x for "past the left edge" (a click left of half ego's
|
||||||
|
// width): no picture x is 255.
|
||||||
|
#define MOUSE_OFF_LEFT 0xFFu
|
||||||
#define STEP_DEFAULT 1u
|
#define STEP_DEFAULT 1u
|
||||||
#define SPIRAL_LEGS 4u
|
#define SPIRAL_LEGS 4u
|
||||||
#define SPIRAL_MAX (2u * AGI_WIDTH)
|
#define SPIRAL_MAX (2u * AGI_WIDTH)
|
||||||
#define SIGN_STATES 3u
|
#define SIGN_STATES 3u
|
||||||
|
|
||||||
// Automatic loops by direction (specification 4.2): views with two or
|
// Automatic loops by direction (specification 4.2): views with two or
|
||||||
// three loops face right or left; four or more also face up and down.
|
// three loops face right or left, with four also up and down; others keep
|
||||||
|
// their loop, except under King's Quest IV's 3.002.086, which turns any
|
||||||
|
// count as four.
|
||||||
// LOOP_KEEP leaves the loop alone.
|
// LOOP_KEEP leaves the loop alone.
|
||||||
#define LOOP_KEEP 0xFFu
|
#define LOOP_KEEP 0xFFu
|
||||||
#define LOOP_RIGHT 0u
|
#define LOOP_RIGHT 0u
|
||||||
|
|
@ -38,7 +45,11 @@
|
||||||
#define LOOP_DOWN 2u
|
#define LOOP_DOWN 2u
|
||||||
#define LOOP_UP 3u
|
#define LOOP_UP 3u
|
||||||
#define LOOPS_FOUR 4u
|
#define LOOPS_FOUR 4u
|
||||||
|
#define LOOPS_THREE 3u
|
||||||
#define LOOPS_TWO 2u
|
#define LOOPS_TWO 2u
|
||||||
|
// The step count of an object stepping this cycle (objUpdateAll counts it
|
||||||
|
// down to 0 then steps).
|
||||||
|
#define STEP_DUE 1u
|
||||||
|
|
||||||
|
|
||||||
// ----- Prototypes -----
|
// ----- Prototypes -----
|
||||||
|
|
@ -46,12 +57,14 @@
|
||||||
static void advanceCel(uint8_t n);
|
static void advanceCel(uint8_t n);
|
||||||
static bool blockedByObject(uint8_t n, int16_t x, int16_t y);
|
static bool blockedByObject(uint8_t n, int16_t x, int16_t y);
|
||||||
static uint8_t clampToScreen(AgiObjectT *o, int16_t *x, int16_t *y);
|
static uint8_t clampToScreen(AgiObjectT *o, int16_t *x, int16_t *y);
|
||||||
|
static void followEgo(uint8_t n);
|
||||||
static bool legalPosition(uint8_t n, int16_t x, int16_t y);
|
static bool legalPosition(uint8_t n, int16_t x, int16_t y);
|
||||||
static void moveObject(uint8_t n);
|
static void moveObject(uint8_t n);
|
||||||
static bool onScreen(const AgiObjectT *o, int16_t x, int16_t y);
|
static bool onScreen(const AgiObjectT *o, int16_t x, int16_t y);
|
||||||
static void reportEdge(uint8_t n, uint8_t edge);
|
static void reportEdge(uint8_t n, uint8_t edge);
|
||||||
static void selectLoop(AgiObjectT *o);
|
static void selectLoop(AgiObjectT *o);
|
||||||
static void stopSpecialMotion(uint8_t n);
|
static void stopSpecialMotion(uint8_t n);
|
||||||
|
static void wander(AgiObjectT *o);
|
||||||
|
|
||||||
|
|
||||||
// ----- Tables -----
|
// ----- Tables -----
|
||||||
|
|
@ -106,9 +119,9 @@ static void advanceCel(uint8_t n) {
|
||||||
o->cel--;
|
o->cel--;
|
||||||
}
|
}
|
||||||
if ((o->cycleMode == AGI_CYCLE_END_LOOP) ? (o->cel >= last) : (o->cel == 0u)) {
|
if ((o->cycleMode == AGI_CYCLE_END_LOOP) ? (o->cel >= last) : (o->cel == 0u)) {
|
||||||
gAgi->flags[o->loopFlag] = 1u;
|
gAgi->flags[o->parm[PARM_LOOP_FLAG]] = 1u;
|
||||||
o->flags = (uint16_t)(o->flags & (uint16_t)~OBJ_CYCLING);
|
o->flags = (uint16_t)(o->flags & (uint16_t)~OBJ_CYCLING);
|
||||||
o->cycleMode = AGI_CYCLE_NORMAL;
|
o->cycleMode = AGI_CYCLE_NORMAL;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
@ -150,9 +163,13 @@ static bool blockedByObject(uint8_t n, int16_t x, int16_t y) {
|
||||||
// it ran into, if any.
|
// it ran into, if any.
|
||||||
static uint8_t clampToScreen(AgiObjectT *o, int16_t *x, int16_t *y) {
|
static uint8_t clampToScreen(AgiObjectT *o, int16_t *x, int16_t *y) {
|
||||||
uint8_t edge;
|
uint8_t edge;
|
||||||
|
bool left;
|
||||||
|
|
||||||
|
// King's Quest IV's 3.002.086 counts x = 0 as past the left edge too
|
||||||
|
// (its room 135 hen walks off there).
|
||||||
edge = AGI_EDGE_NONE;
|
edge = AGI_EDGE_NONE;
|
||||||
if (*x < 0) {
|
left = (gAgi->game.interpreterVersion == AGI_VERSION_KQ4) ? *x <= 0 : *x < 0;
|
||||||
|
if (left) {
|
||||||
*x = 0;
|
*x = 0;
|
||||||
edge = AGI_EDGE_LEFT;
|
edge = AGI_EDGE_LEFT;
|
||||||
} else if (*x + (int16_t)o->width > AGI_WIDTH) {
|
} else if (*x + (int16_t)o->width > AGI_WIDTH) {
|
||||||
|
|
@ -173,6 +190,57 @@ static uint8_t clampToScreen(AgiObjectT *o, int16_t *x, int16_t *y) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// follow.ego, as Sierra's (2.915): head for ego's middle until within
|
||||||
|
// the distance follow.ego gave; there, stop and set its flag. After a step
|
||||||
|
// that left it standing, a random heading (never 0) and a detour of
|
||||||
|
// rand % (half the x and y distance to ego, + 1) steps, drawn until at
|
||||||
|
// least its step size, worked off a step size at a time; a count Sierra's
|
||||||
|
// signed byte compare finds below the step size ends the detour.
|
||||||
|
static void followEgo(uint8_t n) {
|
||||||
|
AgiObjectT *o;
|
||||||
|
const AgiObjectT *ego;
|
||||||
|
int16_t objX;
|
||||||
|
int16_t egoX;
|
||||||
|
uint16_t span;
|
||||||
|
uint8_t dir;
|
||||||
|
uint8_t *count;
|
||||||
|
|
||||||
|
o = &gAgi->objects[n];
|
||||||
|
ego = &gAgi->objects[AGI_EGO];
|
||||||
|
objX = (int16_t)(o->x + o->width / 2u);
|
||||||
|
egoX = (int16_t)(ego->x + ego->width / 2u);
|
||||||
|
dir = objDirectionTo(objX, o->y, egoX, ego->y, o->parm[PARM_FOLLOW_STEP]);
|
||||||
|
if (dir == AGI_DIR_NONE) {
|
||||||
|
stopSpecialMotion(n);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
count = &o->parm[PARM_FOLLOW_COUNT];
|
||||||
|
if (*count == FOLLOW_START) {
|
||||||
|
*count = 0u;
|
||||||
|
} else if ((o->flags & OBJ_DIDNT_MOVE) != 0u) {
|
||||||
|
do {
|
||||||
|
o->direction = (uint8_t)(agiRandom() % AGI_DIR_COUNT);
|
||||||
|
} while (o->direction == AGI_DIR_NONE);
|
||||||
|
span = (uint16_t)((uint16_t)(abs(o->y - ego->y) + abs(objX - egoX)) / 2u + 1u);
|
||||||
|
if (span <= o->stepSize) {
|
||||||
|
*count = o->stepSize;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
*count = (uint8_t)(agiRandom() % span);
|
||||||
|
} while (*count < o->stepSize);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (*count == 0u) {
|
||||||
|
o->direction = dir;
|
||||||
|
} else if ((int8_t)*count < (int8_t)o->stepSize) {
|
||||||
|
*count = 0u;
|
||||||
|
} else {
|
||||||
|
*count = (uint8_t)(*count - o->stepSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Whether the object may stand at (x, y): what its baseline covers in the
|
// Whether the object may stand at (x, y): what its baseline covers in the
|
||||||
// priority screen, the block rectangle and other objects. Sets f0/f3 for
|
// priority screen, the block rectangle and other objects. Sets f0/f3 for
|
||||||
// ego as a side effect.
|
// ego as a side effect.
|
||||||
|
|
@ -252,14 +320,6 @@ static void moveObject(uint8_t n) {
|
||||||
if ((x != o->x || y != o->y) && legalPosition(n, x, y)) {
|
if ((x != o->x || y != o->y) && legalPosition(n, x, y)) {
|
||||||
o->x = x;
|
o->x = x;
|
||||||
o->y = y;
|
o->y = y;
|
||||||
} else if (o->motion == AGI_MOTION_WANDER) {
|
|
||||||
o->randomSteps = 0u;
|
|
||||||
} else if (o->motion == AGI_MOTION_FOLLOW) {
|
|
||||||
// A follower that runs into something wanders off a few steps
|
|
||||||
// before it heads for ego again (measured: it never settles
|
|
||||||
// against ego's side).
|
|
||||||
o->direction = (uint8_t)(AGI_DIR_N + jlRandomRange(AGI_DIR_COUNT - 1u));
|
|
||||||
o->randomSteps = (uint8_t)(FOLLOW_DETOUR_MIN + jlRandomRange(FOLLOW_DETOUR_SPAN));
|
|
||||||
}
|
}
|
||||||
// Ego's edge is current: a step that touches none clears it.
|
// Ego's edge is current: a step that touches none clears it.
|
||||||
if (edge != AGI_EDGE_NONE) {
|
if (edge != AGI_EDGE_NONE) {
|
||||||
|
|
@ -289,13 +349,24 @@ static void reportEdge(uint8_t n, uint8_t edge) {
|
||||||
|
|
||||||
|
|
||||||
static void selectLoop(AgiObjectT *o) {
|
static void selectLoop(AgiObjectT *o) {
|
||||||
|
uint8_t count;
|
||||||
uint8_t loop;
|
uint8_t loop;
|
||||||
|
uint16_t version;
|
||||||
|
|
||||||
if ((o->flags & OBJ_FIX_LOOP) != 0u || o->view == NULL || o->view->loopCount < LOOPS_TWO) {
|
if ((o->flags & OBJ_FIX_LOOP) != 0u || o->view == NULL) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
loop = (o->view->loopCount >= LOOPS_FOUR) ? kLoopFour[o->direction] : kLoopFew[o->direction];
|
count = o->view->loopCount;
|
||||||
if (loop != LOOP_KEEP && loop != o->loop) {
|
version = gAgi->game.interpreterVersion;
|
||||||
|
if (count == LOOPS_TWO || count == LOOPS_THREE) {
|
||||||
|
loop = kLoopFew[o->direction];
|
||||||
|
} else if (count == LOOPS_FOUR || version == AGI_VERSION_KQ4) {
|
||||||
|
loop = kLoopFour[o->direction];
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// After 2.272 the loop turns only as the object steps.
|
||||||
|
if (loop != LOOP_KEEP && loop != o->loop && (version <= AGI_VERSION_2272 || o->stepCount == STEP_DUE)) {
|
||||||
objSetLoop((uint8_t)(o - gAgi->objects), loop);
|
objSetLoop((uint8_t)(o - gAgi->objects), loop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -307,21 +378,46 @@ static void stopSpecialMotion(uint8_t n) {
|
||||||
AgiObjectT *o;
|
AgiObjectT *o;
|
||||||
|
|
||||||
o = &gAgi->objects[n];
|
o = &gAgi->objects[n];
|
||||||
if (o->motion == AGI_MOTION_MOVE) {
|
if (o->motion == AGI_MOTION_MOVE || o->motion == AGI_MOTION_MOUSE) {
|
||||||
o->stepSize = o->savedStepSize;
|
o->stepSize = o->parm[PARM_MOVE_STEP];
|
||||||
if (n == AGI_EGO) {
|
if (n == AGI_EGO) {
|
||||||
gAgi->playerControl = true;
|
gAgi->playerControl = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
gAgi->flags[o->motionFlag] = 1u;
|
// A mouse walk has no flag to set.
|
||||||
o->motion = AGI_MOTION_NORMAL;
|
if (o->motion == AGI_MOTION_MOVE) {
|
||||||
o->direction = AGI_DIR_NONE;
|
gAgi->flags[o->parm[PARM_MOVE_FLAG]] = 1u;
|
||||||
|
} else if (o->motion == AGI_MOTION_FOLLOW) {
|
||||||
|
gAgi->flags[o->parm[PARM_FOLLOW_FLAG]] = 1u;
|
||||||
|
}
|
||||||
|
o->motion = AGI_MOTION_NORMAL;
|
||||||
|
o->direction = AGI_DIR_NONE;
|
||||||
if (n == AGI_EGO) {
|
if (n == AGI_EGO) {
|
||||||
gAgi->vars[VAR_EGO_DIR] = AGI_DIR_NONE;
|
gAgi->vars[VAR_EGO_DIR] = AGI_DIR_NONE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// wander, as Sierra's (2.915): the count runs down each cycle; when it had
|
||||||
|
// run out, or the last step left the object standing, a new heading
|
||||||
|
// (rand % 9, 0 standing still) and, while the count is under 6, a new
|
||||||
|
// count. A count that ran out has wrapped to 255, so that heading lasts
|
||||||
|
// about 255 cycles unless something stops the object.
|
||||||
|
static void wander(AgiObjectT *o) {
|
||||||
|
uint8_t count;
|
||||||
|
|
||||||
|
count = o->parm[PARM_WANDER_COUNT];
|
||||||
|
o->parm[PARM_WANDER_COUNT] = (uint8_t)(count - 1u);
|
||||||
|
if (count != 0u && (o->flags & OBJ_DIDNT_MOVE) == 0u) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
o->direction = (uint8_t)(agiRandom() % AGI_DIR_COUNT);
|
||||||
|
while (o->parm[PARM_WANDER_COUNT] < WANDER_COUNT_MIN) {
|
||||||
|
o->parm[PARM_WANDER_COUNT] = (uint8_t)(agiRandom() % WANDER_COUNT_LIMIT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ----- Public API (alphabetical) -----
|
// ----- Public API (alphabetical) -----
|
||||||
|
|
||||||
// animate.obj: bring an object under the interpreter's control. Its
|
// animate.obj: bring an object under the interpreter's control. Its
|
||||||
|
|
@ -414,18 +510,48 @@ void objFixPosition(uint8_t n) {
|
||||||
|
|
||||||
// move.obj: head for (x, y) at `step` pixels a step (0 keeps the step
|
// move.obj: head for (x, y) at `step` pixels a step (0 keeps the step
|
||||||
// size); flag is set on arrival.
|
// size); flag is set on arrival.
|
||||||
|
// follow.ego: within step (at least the object's step size) of ego, set
|
||||||
|
// flag. The detour count starts at FOLLOW_START.
|
||||||
|
void objFollowEgo(uint8_t n, uint8_t step, uint8_t flag) {
|
||||||
|
AgiObjectT *o;
|
||||||
|
|
||||||
|
o = &gAgi->objects[n];
|
||||||
|
o->motion = AGI_MOTION_FOLLOW;
|
||||||
|
o->parm[PARM_FOLLOW_STEP] = (step > o->stepSize) ? step : o->stepSize;
|
||||||
|
o->parm[PARM_FOLLOW_FLAG] = flag;
|
||||||
|
o->parm[PARM_FOLLOW_COUNT] = FOLLOW_START;
|
||||||
|
gAgi->flags[flag] = 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// A click's walk (agiMouse.c): ego heads, as for move.obj but with no flag
|
||||||
|
// and the player keeping control, for where its baseline's middle is at
|
||||||
|
// (x, y).
|
||||||
|
void objMouseWalk(uint8_t x, uint8_t y) {
|
||||||
|
AgiObjectT *ego;
|
||||||
|
uint8_t half;
|
||||||
|
|
||||||
|
ego = &gAgi->objects[AGI_EGO];
|
||||||
|
half = (uint8_t)(ego->width / 2u);
|
||||||
|
ego->motion = AGI_MOTION_MOUSE;
|
||||||
|
ego->parm[PARM_MOVE_X] = (x < half) ? MOUSE_OFF_LEFT : (uint8_t)(x - half);
|
||||||
|
ego->parm[PARM_MOVE_Y] = y;
|
||||||
|
ego->parm[PARM_MOVE_STEP] = ego->stepSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void objMoveTo(uint8_t n, uint8_t x, uint8_t y, uint8_t step, uint8_t flag) {
|
void objMoveTo(uint8_t n, uint8_t x, uint8_t y, uint8_t step, uint8_t flag) {
|
||||||
AgiObjectT *o;
|
AgiObjectT *o;
|
||||||
|
|
||||||
o = &gAgi->objects[n];
|
o = &gAgi->objects[n];
|
||||||
o->motion = AGI_MOTION_MOVE;
|
o->motion = AGI_MOTION_MOVE;
|
||||||
o->targetX = x;
|
o->parm[PARM_MOVE_X] = x;
|
||||||
o->targetY = y;
|
o->parm[PARM_MOVE_Y] = y;
|
||||||
o->savedStepSize = o->stepSize;
|
o->parm[PARM_MOVE_STEP] = o->stepSize;
|
||||||
|
o->parm[PARM_MOVE_FLAG] = flag;
|
||||||
if (step != 0u) {
|
if (step != 0u) {
|
||||||
o->stepSize = step;
|
o->stepSize = step;
|
||||||
}
|
}
|
||||||
o->motionFlag = flag;
|
|
||||||
o->direction = objDirectionTo(o->x, o->y, x, y, o->stepSize);
|
o->direction = objDirectionTo(o->x, o->y, x, y, o->stepSize);
|
||||||
gAgi->flags[flag] = 0u;
|
gAgi->flags[flag] = 0u;
|
||||||
if (n == AGI_EGO) {
|
if (n == AGI_EGO) {
|
||||||
|
|
@ -443,9 +569,7 @@ void objMoveTo(uint8_t n, uint8_t x, uint8_t y, uint8_t step, uint8_t flag) {
|
||||||
void objPlanMotion(void) {
|
void objPlanMotion(void) {
|
||||||
uint8_t i;
|
uint8_t i;
|
||||||
AgiObjectT *o;
|
AgiObjectT *o;
|
||||||
AgiObjectT *ego;
|
|
||||||
|
|
||||||
ego = &gAgi->objects[AGI_EGO];
|
|
||||||
for (i = 0u; i < gAgi->objectCount; i++) {
|
for (i = 0u; i < gAgi->objectCount; i++) {
|
||||||
o = &gAgi->objects[i];
|
o = &gAgi->objects[i];
|
||||||
if ((o->flags & (OBJ_ANIMATED | OBJ_UPDATE | OBJ_DRAWN)) != (OBJ_ANIMATED | OBJ_UPDATE | OBJ_DRAWN)) {
|
if ((o->flags & (OBJ_ANIMATED | OBJ_UPDATE | OBJ_DRAWN)) != (OBJ_ANIMATED | OBJ_UPDATE | OBJ_DRAWN)) {
|
||||||
|
|
@ -453,26 +577,14 @@ void objPlanMotion(void) {
|
||||||
}
|
}
|
||||||
switch (o->motion) {
|
switch (o->motion) {
|
||||||
case AGI_MOTION_WANDER:
|
case AGI_MOTION_WANDER:
|
||||||
if (o->randomSteps == 0u) {
|
wander(o);
|
||||||
o->direction = (uint8_t)jlRandomRange(AGI_DIR_COUNT);
|
|
||||||
o->randomSteps = (uint8_t)(WANDER_STEPS_MIN + jlRandomRange(WANDER_STEPS_SPAN));
|
|
||||||
} else {
|
|
||||||
o->randomSteps--;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case AGI_MOTION_FOLLOW:
|
case AGI_MOTION_FOLLOW:
|
||||||
if (o->randomSteps > 0u) {
|
followEgo(i);
|
||||||
o->randomSteps--;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
o->direction = objDirectionTo((int16_t)(o->x + o->width / 2u), o->y,
|
|
||||||
(int16_t)(ego->x + ego->width / 2u), ego->y, o->followDistance);
|
|
||||||
if (o->direction == AGI_DIR_NONE) {
|
|
||||||
stopSpecialMotion(i);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case AGI_MOTION_MOVE:
|
case AGI_MOTION_MOVE:
|
||||||
o->direction = objDirectionTo(o->x, o->y, o->targetX, o->targetY, o->stepSize);
|
case AGI_MOTION_MOUSE:
|
||||||
|
o->direction = objDirectionTo(o->x, o->y, (o->motion == AGI_MOTION_MOUSE && o->parm[PARM_MOVE_X] == MOUSE_OFF_LEFT) ? -1 : (int16_t)o->parm[PARM_MOVE_X], o->parm[PARM_MOVE_Y], o->stepSize);
|
||||||
if (o->direction == AGI_DIR_NONE) {
|
if (o->direction == AGI_DIR_NONE) {
|
||||||
stopSpecialMotion(i);
|
stopSpecialMotion(i);
|
||||||
}
|
}
|
||||||
|
|
@ -596,13 +708,21 @@ void objUpdateAll(void) {
|
||||||
}
|
}
|
||||||
if (o->stepCount == 0u) {
|
if (o->stepCount == 0u) {
|
||||||
o->stepCount = o->stepTime;
|
o->stepCount = o->stepTime;
|
||||||
o->prevX = o->x;
|
|
||||||
o->prevY = o->y;
|
|
||||||
if ((o->flags & OBJ_SKIP_STEP) != 0u) {
|
if ((o->flags & OBJ_SKIP_STEP) != 0u) {
|
||||||
o->flags = (uint16_t)(o->flags & (uint16_t)~OBJ_SKIP_STEP);
|
o->flags = (uint16_t)(o->flags & (uint16_t)~OBJ_SKIP_STEP);
|
||||||
} else {
|
} else {
|
||||||
moveObject(i);
|
moveObject(i);
|
||||||
}
|
}
|
||||||
|
// Where the step left it against where the last step that
|
||||||
|
// moved it (or a position command) did, as Sierra's does at
|
||||||
|
// each step: wander and follow.ego turn aside when it stood.
|
||||||
|
if (o->x == o->prevX && o->y == o->prevY) {
|
||||||
|
o->flags = (uint16_t)(o->flags | OBJ_DIDNT_MOVE);
|
||||||
|
} else {
|
||||||
|
o->prevX = o->x;
|
||||||
|
o->prevY = o->y;
|
||||||
|
o->flags = (uint16_t)(o->flags & (uint16_t)~OBJ_DIDNT_MOVE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
gfxRedrawUpdating();
|
gfxRedrawUpdating();
|
||||||
|
|
|
||||||
105
agiPatch.c
Normal file
105
agiPatch.c
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
// Changes to particular games, as PLAN.md's decisions have them: copy
|
||||||
|
// protection is skipped (decision 3) and the script bugs Sierra shipped are
|
||||||
|
// patched (decision 2). A game is known by its v3 file prefix (agiRes.c).
|
||||||
|
|
||||||
|
#include "agi.h"
|
||||||
|
|
||||||
|
#include "joey/core.h"
|
||||||
|
|
||||||
|
|
||||||
|
// Gold Rush's clock: logic 0 counts v166 cycles to a second. At a change
|
||||||
|
// of speed it copies v166 from v167 (Fastest) or v165 (Fast), which the
|
||||||
|
// speed test in logic 129 sets: it counts a second of empty cycles at
|
||||||
|
// Fastest and takes a third and a quarter of them (at most 20) for the
|
||||||
|
// rate a playing cycle would run at on a 1988 PC. Here Fastest is held to
|
||||||
|
// the frame rate, playing or not, so the clock would run six times too
|
||||||
|
// fast; and the cracked copy (games/gr) lost the test with logic 129, so
|
||||||
|
// its clock stops. Both are filled with the real rates every cycle: Fast
|
||||||
|
// is one cycle a unit, Fastest one a frame (agiIdle waits for each).
|
||||||
|
#define GR_FAST_CPS_VAR 165u
|
||||||
|
#define GR_FASTEST_CPS_VAR 167u
|
||||||
|
#define GR_FAST_CPS (AGI_MS_PER_SECOND / AGI_CYCLE_UNIT_MS)
|
||||||
|
|
||||||
|
// King's Quest IV's graveyards (rooms 16 and 18): three zombies rise at
|
||||||
|
// night, each one's distance from Rosella measured into v221..v223 and its
|
||||||
|
// state kept in v155, v156, and v162 (room 16) or v158 (room 18).
|
||||||
|
#define KQ4_GRAVEYARD_A 16u
|
||||||
|
#define KQ4_GRAVEYARD_B 18u
|
||||||
|
#define KQ4_ZOMBIE_DIST 221u
|
||||||
|
#define KQ4_ZOMBIES 3u
|
||||||
|
#define KQ4_ZOMBIE_CHASING 2u
|
||||||
|
|
||||||
|
|
||||||
|
// The copy protection (decision 3): the rooms in which a game asks a
|
||||||
|
// question from its manual, and the room a right answer goes on to.
|
||||||
|
typedef struct {
|
||||||
|
AgiGameIdE game;
|
||||||
|
uint8_t first;
|
||||||
|
uint8_t last;
|
||||||
|
uint8_t after;
|
||||||
|
} QuestionRoomsT;
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Tables -----
|
||||||
|
|
||||||
|
static const uint8_t kKq4ZombieStateA[KQ4_ZOMBIES] = { 155u, 156u, 162u };
|
||||||
|
static const uint8_t kKq4ZombieStateB[KQ4_ZOMBIES] = { 155u, 156u, 158u };
|
||||||
|
|
||||||
|
static const QuestionRoomsT kQuestionRooms[] = {
|
||||||
|
// King's Quest IV asks a word from its manual in one of rooms 141 to
|
||||||
|
// 143; the title, room 96, follows.
|
||||||
|
{ AGI_GAME_KQ4, 141u, 143u, 96u },
|
||||||
|
// Gold Rush's claim jumper asks about the book "California Gold" on
|
||||||
|
// the way from the title (logic 129 to room 125, which picks one of
|
||||||
|
// rooms 126 to 128); room 73 follows. The cracked copy's logic 129 is
|
||||||
|
// the original's logic 73 and never comes here.
|
||||||
|
{ AGI_GAME_GOLD_RUSH, 125u, 125u, 73u }
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Public API (alphabetical) -----
|
||||||
|
|
||||||
|
// Before each cycle's logic 0.
|
||||||
|
void agiPatchCycle(void) {
|
||||||
|
if (gAgi->game.id != AGI_GAME_GOLD_RUSH) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gAgi->vars[GR_FAST_CPS_VAR] = (uint8_t)GR_FAST_CPS;
|
||||||
|
gAgi->vars[GR_FASTEST_CPS_VAR] = (uint8_t)jlFrameHz();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The distance the distance command reports into var. King's Quest IV's
|
||||||
|
// zombie bug (in Sierra's interpreter too): with the obsidian scarab, a
|
||||||
|
// zombie rising beside Rosella is turned away before it is up, so it
|
||||||
|
// rises and never chases. Patched as ScummVM does: until a zombie chases,
|
||||||
|
// it reads as far away.
|
||||||
|
uint8_t agiPatchDistance(uint8_t var, uint8_t distance) {
|
||||||
|
uint8_t room;
|
||||||
|
uint8_t zombie;
|
||||||
|
|
||||||
|
room = gAgi->vars[VAR_ROOM];
|
||||||
|
if (gAgi->game.id != AGI_GAME_KQ4 || (room != KQ4_GRAVEYARD_A && room != KQ4_GRAVEYARD_B) || var < KQ4_ZOMBIE_DIST || var >= KQ4_ZOMBIE_DIST + KQ4_ZOMBIES) {
|
||||||
|
return distance;
|
||||||
|
}
|
||||||
|
zombie = (uint8_t)(var - KQ4_ZOMBIE_DIST);
|
||||||
|
if (gAgi->vars[(room == KQ4_GRAVEYARD_A) ? kKq4ZombieStateA[zombie] : kKq4ZombieStateB[zombie]] != KQ4_ZOMBIE_CHASING) {
|
||||||
|
return AGI_DISTANCE_FAR;
|
||||||
|
}
|
||||||
|
return distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The room new.room really goes to: a manual question is skipped.
|
||||||
|
uint8_t agiPatchRoom(uint8_t room) {
|
||||||
|
const QuestionRoomsT *q;
|
||||||
|
uint8_t i;
|
||||||
|
|
||||||
|
for (i = 0u; i < sizeof(kQuestionRooms) / sizeof(kQuestionRooms[0]); i++) {
|
||||||
|
q = &kQuestionRooms[i];
|
||||||
|
if (gAgi->game.id == q->game && room >= q->first && room <= q->last) {
|
||||||
|
return q->after;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return room;
|
||||||
|
}
|
||||||
52
agiPic.c
52
agiPic.c
|
|
@ -32,6 +32,14 @@
|
||||||
#define REL_Y_SIGN 0x08u
|
#define REL_Y_SIGN 0x08u
|
||||||
#define REL_MAG_MASK 0x07u
|
#define REL_MAG_MASK 0x07u
|
||||||
|
|
||||||
|
// set.pri.base: priority 4 above the base row, then from 5 in bands of
|
||||||
|
// (168 - base) * 168 / 10 / 168 rows, at most 15 (ScummVM's formula,
|
||||||
|
// graphics.cpp setPriorityTable). Up to 3.002.086 the lowest row of a
|
||||||
|
// priority is always 168 once a table is set (ScummVM, priorityToY: a
|
||||||
|
// glitch of the original that King's Quest IV's room 54 needs).
|
||||||
|
#define PRI_TABLE_BANDS 10
|
||||||
|
#define PRI_GLITCH_LAST AGI_VERSION_KQ4
|
||||||
|
|
||||||
// Pen byte: bit 5 splatter, bit 4 rectangle, bits 0-2 the size.
|
// Pen byte: bit 5 splatter, bit 4 rectangle, bits 0-2 the size.
|
||||||
#define PEN_SPLATTER 0x20u
|
#define PEN_SPLATTER 0x20u
|
||||||
#define PEN_RECTANGLE 0x10u
|
#define PEN_RECTANGLE 0x10u
|
||||||
|
|
@ -517,19 +525,55 @@ bool agiPicDecode(uint8_t *visual, uint8_t *priority, const uint8_t *data, uint1
|
||||||
|
|
||||||
|
|
||||||
uint8_t agiPriorityForY(int16_t y) {
|
uint8_t agiPriorityForY(int16_t y) {
|
||||||
if (y < AGI_PRI_BAND_TOP) {
|
|
||||||
return AGI_PRIORITY_MIN;
|
|
||||||
}
|
|
||||||
if (y > AGI_MAX_Y) {
|
if (y > AGI_MAX_Y) {
|
||||||
y = AGI_MAX_Y;
|
y = AGI_MAX_Y;
|
||||||
}
|
}
|
||||||
|
if (gAgi->priorityTableSet) {
|
||||||
|
return gAgi->priorityTable[y < 0 ? 0 : y];
|
||||||
|
}
|
||||||
|
if (y < AGI_PRI_BAND_TOP) {
|
||||||
|
return AGI_PRIORITY_MIN;
|
||||||
|
}
|
||||||
return (uint8_t)(AGI_PRI_FIRST_BAND + (uint8_t)((y - AGI_PRI_BAND_TOP) / AGI_PRI_BAND_HEIGHT));
|
return (uint8_t)(AGI_PRI_FIRST_BAND + (uint8_t)((y - AGI_PRI_BAND_TOP) / AGI_PRI_BAND_HEIGHT));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The 12-row bands again: a new game, a restart, a restore.
|
||||||
|
void agiPriorityReset(void) {
|
||||||
|
gAgi->priorityTableSet = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// set.pri.base: the table from base down (PRI_TABLE_BANDS).
|
||||||
|
void agiPrioritySetBase(uint8_t base) {
|
||||||
|
int16_t band;
|
||||||
|
int16_t y;
|
||||||
|
int16_t p;
|
||||||
|
|
||||||
|
band = (int16_t)((AGI_HEIGHT - (int16_t)base) * AGI_HEIGHT / PRI_TABLE_BANDS);
|
||||||
|
for (y = 0; y < AGI_HEIGHT; y++) {
|
||||||
|
p = (y < (int16_t)base || band <= 0) ? (int16_t)AGI_PRIORITY_MIN : (int16_t)((y - (int16_t)base) * AGI_HEIGHT / band + AGI_PRI_FIRST_BAND);
|
||||||
|
gAgi->priorityTable[y] = (uint8_t)(p > (int16_t)AGI_PRIORITY_MAX ? (int16_t)AGI_PRIORITY_MAX : p);
|
||||||
|
}
|
||||||
|
gAgi->priorityTableSet = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// The lowest y that has a priority: where an object with that fixed
|
// The lowest y that has a priority: where an object with that fixed
|
||||||
// priority sorts among the others when they are drawn.
|
// priority sorts among the others when they are drawn. With a table, the
|
||||||
|
// row below it, scanned up from the bottom as ScummVM does, or 168 up to
|
||||||
|
// 3.002.086 (PRI_GLITCH_LAST).
|
||||||
int16_t agiYForPriority(uint8_t priority) {
|
int16_t agiYForPriority(uint8_t priority) {
|
||||||
|
int16_t y;
|
||||||
|
|
||||||
|
if (gAgi->priorityTableSet) {
|
||||||
|
if (gAgi->game.interpreterVersion <= PRI_GLITCH_LAST) {
|
||||||
|
return AGI_HEIGHT;
|
||||||
|
}
|
||||||
|
for (y = AGI_MAX_Y; y >= 0 && gAgi->priorityTable[y] >= priority; y--) {
|
||||||
|
}
|
||||||
|
return y;
|
||||||
|
}
|
||||||
if (priority <= AGI_PRIORITY_MIN) {
|
if (priority <= AGI_PRIORITY_MIN) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
395
agiRes.c
395
agiRes.c
|
|
@ -1,14 +1,23 @@
|
||||||
// AGI v2 resources: the four directories, the VOL files, WORDS.TOK and
|
// AGI resources: the four directories, the VOL files, WORDS.TOK and
|
||||||
// OBJECT (AGI specification chapters 5 and 10).
|
// OBJECT (AGI specification chapters 5 and 10).
|
||||||
//
|
//
|
||||||
// A directory is a flat array of 3-byte entries: the high nibble of the
|
// A directory is a flat array of 3-byte entries: the high nibble of the
|
||||||
// first byte is the VOL file, the remaining 20 bits the offset into it;
|
// first byte is the VOL file, the remaining 20 bits the offset into it;
|
||||||
// FF FF FF marks an unused number. A resource in a VOL file starts with a
|
// FF FF FF marks an unused number. A resource in a VOL file starts with a
|
||||||
// 5-byte header: 12 34, the volume, then the little-endian length.
|
// 5-byte header: 12 34, the volume, then the little-endian length.
|
||||||
|
//
|
||||||
|
// AGI v3 keeps the four directories in one file, <prefix>DIR (GRDIR,
|
||||||
|
// MH2DIR), after four little-endian offsets to them, and the volumes in
|
||||||
|
// <prefix>VOL.n. Its headers are 7 bytes: 12 34, the volume, the length,
|
||||||
|
// then the length stored. A stored length short of the length means LZW
|
||||||
|
// (expanded here); a picture whose volume byte has bit 7 set instead keeps
|
||||||
|
// the colors of F0 and F2 in a nibble each (spread back into bytes here),
|
||||||
|
// so the rest of the game sees v2 resources.
|
||||||
|
|
||||||
#include "agi.h"
|
#include "agi.h"
|
||||||
|
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#include "joey/core.h"
|
#include "joey/core.h"
|
||||||
|
|
@ -21,13 +30,42 @@
|
||||||
#define DIR_VOLUME_SHIFT 4u
|
#define DIR_VOLUME_SHIFT 4u
|
||||||
#define DIR_OFFSET_HIGH_MASK 0x0Fu
|
#define DIR_OFFSET_HIGH_MASK 0x0Fu
|
||||||
#define DIR_OFFSET_HIGH_SHIFT 16u
|
#define DIR_OFFSET_HIGH_SHIFT 16u
|
||||||
#define VOL_HEADER_BYTES 5u
|
#define VOL_HEADER_V2_BYTES 5u
|
||||||
|
#define VOL_HEADER_V3_BYTES 7u
|
||||||
|
#define VOL_HEADER_MAX VOL_HEADER_V3_BYTES
|
||||||
#define VOL_SIG_0 0x12u
|
#define VOL_SIG_0 0x12u
|
||||||
#define VOL_SIG_1 0x34u
|
#define VOL_SIG_1 0x34u
|
||||||
#define VOL_COMPRESSED 0x80u
|
#define VOL_VOLUME_BYTE 2u
|
||||||
|
#define VOL_LENGTH_BYTE 3u
|
||||||
|
#define VOL_STORED_BYTE 5u
|
||||||
|
#define VOL_NIBBLE_PICTURE 0x80u
|
||||||
#define VOL_VOLUME_MASK 0x7Fu
|
#define VOL_VOLUME_MASK 0x7Fu
|
||||||
#define VOL_NAME_MAX 8u
|
#define VOL_NAME_MAX (AGI_PREFIX_MAX + sizeof("VOL.15"))
|
||||||
|
#define DIR_NAME_MAX (AGI_PREFIX_MAX + sizeof("DIR"))
|
||||||
#define DECIMAL_BASE 10u
|
#define DECIMAL_BASE 10u
|
||||||
|
#define DIR_V3_OFFSETS_BYTES 8u
|
||||||
|
#define DIR_V3_MAX_BYTES (DIR_V3_OFFSETS_BYTES + AGI_RES_COUNT * AGI_MAX_RESOURCES * DIR_ENTRY_BYTES)
|
||||||
|
#define BYTE_BITS 8u
|
||||||
|
#define NIBBLE_BITS 4u
|
||||||
|
#define LOW_NIBBLE 0x0Fu
|
||||||
|
|
||||||
|
// AGI v3's LZW, as the data has it: codes come least significant bit
|
||||||
|
// first, 9 bits wide and growing a bit (to 11) as the next free code would
|
||||||
|
// need it; 0x100 starts the table over, 0x101 ends, and every stream
|
||||||
|
// starts with 0x100.
|
||||||
|
#define LZW_START_BITS 9u
|
||||||
|
#define LZW_MAX_BITS 11u
|
||||||
|
#define LZW_RESET 0x100u
|
||||||
|
#define LZW_END 0x101u
|
||||||
|
#define LZW_FIRST_FREE 0x102u
|
||||||
|
#define LZW_TABLE_SIZE (1u << LZW_MAX_BITS)
|
||||||
|
#define LZW_LITERAL_MAX 0xFFu
|
||||||
|
#define LZW_NONE 0xFFFFu
|
||||||
|
|
||||||
|
// The picture commands whose colors a v3 nibble picture packs.
|
||||||
|
#define PIC_SET_COLOR 0xF0u
|
||||||
|
#define PIC_SET_PRIORITY 0xF2u
|
||||||
|
#define PIC_END 0xFFu
|
||||||
|
|
||||||
// OBJECT: a 3-byte header (offset of the names, the number of animated
|
// OBJECT: a 3-byte header (offset of the names, the number of animated
|
||||||
// objects) then a 3-byte entry per item (name offset, starting room).
|
// objects) then a 3-byte entry per item (name offset, starting room).
|
||||||
|
|
@ -59,11 +97,17 @@
|
||||||
|
|
||||||
// ----- Prototypes -----
|
// ----- Prototypes -----
|
||||||
|
|
||||||
static void buildVolName(char *out, uint8_t volNum);
|
static void buildVolName(const AgiGameT *game, char *out, uint8_t volNum);
|
||||||
static uint16_t detectVersion(void);
|
static uint16_t detectVersion(void);
|
||||||
|
static uint8_t *expandNibblePicture(const uint8_t *in, uint16_t inLen, uint16_t *outLen);
|
||||||
|
static void fillDirectory(AgiGameT *game, AgiResTypeE type, const uint8_t *bytes, uint16_t length);
|
||||||
static void loadDirectory(AgiGameT *game, const char *fileName, AgiResTypeE type);
|
static void loadDirectory(AgiGameT *game, const char *fileName, AgiResTypeE type);
|
||||||
|
static void loadDirectoryV3(AgiGameT *game);
|
||||||
static uint8_t *loadFile(const char *name, uint16_t maxBytes, uint16_t *outLength);
|
static uint8_t *loadFile(const char *name, uint16_t maxBytes, uint16_t *outLength);
|
||||||
static bool loadObjects(AgiGameT *game);
|
static bool loadObjects(AgiGameT *game);
|
||||||
|
static bool lzwExpand(const uint8_t *in, uint16_t inLen, uint8_t *out, uint16_t outLen);
|
||||||
|
static uint8_t nibbleAt(const uint8_t *data, uint32_t k);
|
||||||
|
static uint16_t readLe16(const uint8_t *p);
|
||||||
static bool resHeaderRead(const AgiGameT *game, AgiResTypeE type, uint16_t index, uint8_t *header);
|
static bool resHeaderRead(const AgiGameT *game, AgiResTypeE type, uint16_t index, uint8_t *header);
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -75,6 +119,27 @@ static const char *const kDirNames[AGI_RES_COUNT] = { "LOGDIR", "PICDIR", "VIEWD
|
||||||
// Where the version string may be, in the order to look.
|
// Where the version string may be, in the order to look.
|
||||||
static const char *const kVersionFiles[] = { "AGIDATA.OVL", "AGI" };
|
static const char *const kVersionFiles[] = { "AGIDATA.OVL", "AGI" };
|
||||||
|
|
||||||
|
// The v3 games' file-name prefixes, which also tell which game it is, and
|
||||||
|
// the interpreter to behave as when the folder names none (another
|
||||||
|
// machine's release: every King's Quest IV is 3.002.086's, the rest are
|
||||||
|
// taken as the last, 3.002.149). ScummVM finds the prefix by listing the
|
||||||
|
// folder for a file ending in DIR; JoeyLib lists no folders, so the known
|
||||||
|
// ones are tried.
|
||||||
|
typedef struct {
|
||||||
|
const char *prefix;
|
||||||
|
AgiGameIdE id;
|
||||||
|
uint16_t version;
|
||||||
|
} V3GameT;
|
||||||
|
|
||||||
|
static const V3GameT kV3Games[] = {
|
||||||
|
{ "KQ4", AGI_GAME_KQ4, AGI_VERSION_KQ4 },
|
||||||
|
{ "GR", AGI_GAME_GOLD_RUSH, AGI_VERSION_V3_ASSUMED },
|
||||||
|
{ "MH", AGI_GAME_MANHUNTER_1, AGI_VERSION_V3_ASSUMED },
|
||||||
|
{ "MH2", AGI_GAME_MANHUNTER_2, AGI_VERSION_V3_ASSUMED },
|
||||||
|
{ "BC", AGI_GAME_BLACK_CAULDRON, AGI_VERSION_V3_ASSUMED },
|
||||||
|
{ "DM", AGI_GAME_DEMO, AGI_VERSION_V3_ASSUMED }
|
||||||
|
};
|
||||||
|
|
||||||
// The interpreter versions each quirk is known in. Measured on 2.435
|
// The interpreter versions each quirk is known in. Measured on 2.435
|
||||||
// (King's Quest III 2.00) only, the one Sierra interpreter at hand; they
|
// (King's Quest III 2.00) only, the one Sierra interpreter at hand; they
|
||||||
// come from the PC interpreter's window, input-line and picture code, so
|
// come from the PC interpreter's window, input-line and picture code, so
|
||||||
|
|
@ -94,19 +159,8 @@ static const QuirkRangeT kQuirkVersions[AGI_QUIRK_COUNT] = {
|
||||||
|
|
||||||
// ----- Internal helpers (alphabetical) -----
|
// ----- Internal helpers (alphabetical) -----
|
||||||
|
|
||||||
static void buildVolName(char *out, uint8_t volNum) {
|
static void buildVolName(const AgiGameT *game, char *out, uint8_t volNum) {
|
||||||
uint8_t n;
|
(void)snprintf(out, VOL_NAME_MAX, "%sVOL.%u", game->prefix, (unsigned)volNum);
|
||||||
|
|
||||||
n = 0u;
|
|
||||||
out[n++] = 'V';
|
|
||||||
out[n++] = 'O';
|
|
||||||
out[n++] = 'L';
|
|
||||||
out[n++] = '.';
|
|
||||||
if (volNum >= DECIMAL_BASE) {
|
|
||||||
out[n++] = (char)('0' + volNum / DECIMAL_BASE);
|
|
||||||
}
|
|
||||||
out[n++] = (char)('0' + volNum % DECIMAL_BASE);
|
|
||||||
out[n] = '\0';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -139,6 +193,14 @@ static uint16_t detectVersion(void) {
|
||||||
for (j = (uint16_t)(j + 2u); j < length && data[j] >= '0' && data[j] <= '9'; j++) {
|
for (j = (uint16_t)(j + 2u); j < length && data[j] >= '0' && data[j] <= '9'; j++) {
|
||||||
minor = (uint16_t)(minor * DECIMAL_BASE + (uint16_t)(data[j] - '0'));
|
minor = (uint16_t)(minor * DECIMAL_BASE + (uint16_t)(data[j] - '0'));
|
||||||
}
|
}
|
||||||
|
// AGI 3 names three parts, 3.002.149: the last one tells them
|
||||||
|
// apart (3149), as ScummVM numbers them.
|
||||||
|
if (j + 1u < length && data[j] == '.' && data[j + 1u] >= '0' && data[j + 1u] <= '9') {
|
||||||
|
minor = 0u;
|
||||||
|
for (j = (uint16_t)(j + 1u); j < length && data[j] >= '0' && data[j] <= '9'; j++) {
|
||||||
|
minor = (uint16_t)(minor * DECIMAL_BASE + (uint16_t)(data[j] - '0'));
|
||||||
|
}
|
||||||
|
}
|
||||||
jlFree(data);
|
jlFree(data);
|
||||||
return (uint16_t)(major * VERSION_MAJOR_SCALE + minor);
|
return (uint16_t)(major * VERSION_MAJOR_SCALE + minor);
|
||||||
}
|
}
|
||||||
|
|
@ -148,19 +210,52 @@ static uint16_t detectVersion(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// A missing or empty directory just means none of that type (a game
|
// A v3 nibble picture spread back into bytes: each byte is two nibbles
|
||||||
// without sounds); the game needs logic 0 (agiResOpen).
|
// from wherever the last one ended, and the color after F0 or F2 one.
|
||||||
static void loadDirectory(AgiGameT *game, const char *fileName, AgiResTypeE type) {
|
static uint8_t *expandNibblePicture(const uint8_t *in, uint16_t inLen, uint16_t *outLen) {
|
||||||
uint8_t *bytes;
|
uint8_t *out;
|
||||||
uint16_t length;
|
uint32_t nibbles;
|
||||||
|
uint32_t k;
|
||||||
|
uint16_t n;
|
||||||
|
uint8_t b;
|
||||||
|
|
||||||
|
// Every output byte uses at least one nibble.
|
||||||
|
nibbles = (uint32_t)inLen * 2u;
|
||||||
|
if (nibbles > UINT16_MAX) {
|
||||||
|
nibbles = UINT16_MAX;
|
||||||
|
}
|
||||||
|
out = (uint8_t *)jlAlloc(nibbles);
|
||||||
|
if (out == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
n = 0u;
|
||||||
|
k = 0u;
|
||||||
|
while (k + 1u < nibbles) {
|
||||||
|
b = (uint8_t)((nibbleAt(in, k) << NIBBLE_BITS) | nibbleAt(in, k + 1u));
|
||||||
|
k += 2u;
|
||||||
|
out[n++] = b;
|
||||||
|
if (b == PIC_END) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ((b == PIC_SET_COLOR || b == PIC_SET_PRIORITY) && k < nibbles) {
|
||||||
|
out[n++] = nibbleAt(in, k);
|
||||||
|
k++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*outLen = n;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The 3-byte entries of one directory.
|
||||||
|
static void fillDirectory(AgiGameT *game, AgiResTypeE type, const uint8_t *bytes, uint16_t length) {
|
||||||
uint16_t count;
|
uint16_t count;
|
||||||
uint16_t i;
|
uint16_t i;
|
||||||
|
|
||||||
bytes = loadFile(fileName, (uint16_t)(AGI_MAX_RESOURCES * DIR_ENTRY_BYTES), &length);
|
|
||||||
if (bytes == NULL) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
count = (uint16_t)(length / DIR_ENTRY_BYTES);
|
count = (uint16_t)(length / DIR_ENTRY_BYTES);
|
||||||
|
if (count > AGI_MAX_RESOURCES) {
|
||||||
|
count = AGI_MAX_RESOURCES;
|
||||||
|
}
|
||||||
for (i = 0u; i < count; i++) {
|
for (i = 0u; i < count; i++) {
|
||||||
const uint8_t *e = &bytes[i * DIR_ENTRY_BYTES];
|
const uint8_t *e = &bytes[i * DIR_ENTRY_BYTES];
|
||||||
AgiResEntryT *d = &game->resDir[type][i];
|
AgiResEntryT *d = &game->resDir[type][i];
|
||||||
|
|
@ -174,10 +269,61 @@ static void loadDirectory(AgiGameT *game, const char *fileName, AgiResTypeE type
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
game->resCount[type] = count;
|
game->resCount[type] = count;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// A missing or empty directory just means none of that type (a game
|
||||||
|
// without sounds); the game needs logic 0 (agiResOpen).
|
||||||
|
static void loadDirectory(AgiGameT *game, const char *fileName, AgiResTypeE type) {
|
||||||
|
uint8_t *bytes;
|
||||||
|
uint16_t length;
|
||||||
|
|
||||||
|
bytes = loadFile(fileName, (uint16_t)(AGI_MAX_RESOURCES * DIR_ENTRY_BYTES), &length);
|
||||||
|
if (bytes == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fillDirectory(game, type, bytes, length);
|
||||||
jlFree(bytes);
|
jlFree(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// An AGI v3 game's <prefix>DIR: four offsets, then the logic, picture,
|
||||||
|
// view and sound directories, each running to the next (the last to the
|
||||||
|
// end of the file).
|
||||||
|
static void loadDirectoryV3(AgiGameT *game) {
|
||||||
|
char name[DIR_NAME_MAX];
|
||||||
|
uint8_t *bytes;
|
||||||
|
uint16_t length;
|
||||||
|
uint16_t start;
|
||||||
|
uint16_t end;
|
||||||
|
uint8_t p;
|
||||||
|
uint8_t t;
|
||||||
|
|
||||||
|
for (p = 0u; p < sizeof(kV3Games) / sizeof(kV3Games[0]); p++) {
|
||||||
|
(void)snprintf(name, sizeof(name), "%sDIR", kV3Games[p].prefix);
|
||||||
|
bytes = loadFile(name, (uint16_t)DIR_V3_MAX_BYTES, &length);
|
||||||
|
if (bytes == NULL) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (length >= DIR_V3_OFFSETS_BYTES) {
|
||||||
|
for (t = 0u; t < AGI_RES_COUNT; t++) {
|
||||||
|
start = readLe16(&bytes[t * 2u]);
|
||||||
|
end = (t + 1u < AGI_RES_COUNT) ? readLe16(&bytes[(t + 1u) * 2u]) : length;
|
||||||
|
if (start <= end && end <= length) {
|
||||||
|
fillDirectory(game, (AgiResTypeE)t, &bytes[start], (uint16_t)(end - start));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
strcpy(game->prefix, kV3Games[p].prefix);
|
||||||
|
game->id = kV3Games[p].id;
|
||||||
|
game->interpreterVersion = kV3Games[p].version;
|
||||||
|
game->v3 = true;
|
||||||
|
}
|
||||||
|
jlFree(bytes);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Read a whole data file, at most maxBytes of it, into a buffer of its
|
// Read a whole data file, at most maxBytes of it, into a buffer of its
|
||||||
// own size (one fread: on the IIgs each call is a GS/OS round trip).
|
// own size (one fread: on the IIgs each call is a GS/OS round trip).
|
||||||
// Returns NULL when it is missing or empty.
|
// Returns NULL when it is missing or empty.
|
||||||
|
|
@ -223,8 +369,13 @@ static bool loadObjects(AgiGameT *game) {
|
||||||
jlFree(data);
|
jlFree(data);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
agiDecrypt(data, length);
|
// Most OBJECT files are XORed with the LOGIC messages' key, a few (the
|
||||||
namesOffset = (uint16_t)(data[0] | ((uint16_t)data[1] << 8));
|
// demos', the Christmas card's) not: encrypted, the first offset falls
|
||||||
|
// past the end of the file.
|
||||||
|
if (readLe16(data) > length) {
|
||||||
|
agiDecrypt(data, length);
|
||||||
|
}
|
||||||
|
namesOffset = readLe16(data);
|
||||||
game->objectData = data;
|
game->objectData = data;
|
||||||
game->itemCount = (uint16_t)(namesOffset / OBJECT_ENTRY_BYTES);
|
game->itemCount = (uint16_t)(namesOffset / OBJECT_ENTRY_BYTES);
|
||||||
if (game->itemCount > AGI_MAX_ITEMS) {
|
if (game->itemCount > AGI_MAX_ITEMS) {
|
||||||
|
|
@ -238,6 +389,120 @@ static bool loadObjects(AgiGameT *game) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// LZW-packed v3 data into exactly outLen bytes (any shortfall zeroed).
|
||||||
|
// False when the work tables cannot be had or the data makes no sense.
|
||||||
|
static bool lzwExpand(const uint8_t *in, uint16_t inLen, uint8_t *out, uint16_t outLen) {
|
||||||
|
uint16_t *prefix;
|
||||||
|
uint8_t *suffix;
|
||||||
|
uint8_t *stack;
|
||||||
|
uint32_t bits;
|
||||||
|
uint8_t bitCount;
|
||||||
|
uint8_t width;
|
||||||
|
uint16_t inPos;
|
||||||
|
uint16_t outPos;
|
||||||
|
uint16_t next;
|
||||||
|
uint16_t prev;
|
||||||
|
uint16_t code;
|
||||||
|
uint16_t cur;
|
||||||
|
uint16_t depth;
|
||||||
|
uint8_t first;
|
||||||
|
bool ok;
|
||||||
|
|
||||||
|
prefix = (uint16_t *)jlAlloc(LZW_TABLE_SIZE * sizeof(uint16_t));
|
||||||
|
suffix = (uint8_t *)jlAlloc(LZW_TABLE_SIZE);
|
||||||
|
stack = (uint8_t *)jlAlloc(LZW_TABLE_SIZE);
|
||||||
|
ok = prefix != NULL && suffix != NULL && stack != NULL;
|
||||||
|
memset(out, 0, outLen);
|
||||||
|
bits = 0u;
|
||||||
|
bitCount = 0u;
|
||||||
|
width = LZW_START_BITS;
|
||||||
|
inPos = 0u;
|
||||||
|
outPos = 0u;
|
||||||
|
next = LZW_FIRST_FREE;
|
||||||
|
prev = LZW_NONE;
|
||||||
|
first = 0u;
|
||||||
|
while (ok && outPos < outLen) {
|
||||||
|
while (bitCount < width && inPos < inLen) {
|
||||||
|
bits |= (uint32_t)in[inPos++] << bitCount;
|
||||||
|
bitCount = (uint8_t)(bitCount + BYTE_BITS);
|
||||||
|
}
|
||||||
|
if (bitCount < width) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
code = (uint16_t)(bits & ((1u << width) - 1u));
|
||||||
|
bits >>= width;
|
||||||
|
bitCount = (uint8_t)(bitCount - width);
|
||||||
|
if (code == LZW_END) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (code == LZW_RESET) {
|
||||||
|
width = LZW_START_BITS;
|
||||||
|
next = LZW_FIRST_FREE;
|
||||||
|
prev = LZW_NONE;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// The first code after a reset is a byte of its own.
|
||||||
|
if (prev == LZW_NONE) {
|
||||||
|
ok = code <= LZW_LITERAL_MAX;
|
||||||
|
if (ok) {
|
||||||
|
out[outPos++] = (uint8_t)code;
|
||||||
|
prev = code;
|
||||||
|
first = (uint8_t)code;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// The code's string, last byte first; the one code not yet in the
|
||||||
|
// table (next) is the previous string and its own first byte.
|
||||||
|
depth = 0u;
|
||||||
|
cur = code;
|
||||||
|
if (code >= next) {
|
||||||
|
ok = code == next;
|
||||||
|
stack[depth++] = first;
|
||||||
|
cur = prev;
|
||||||
|
}
|
||||||
|
while (ok && cur > LZW_LITERAL_MAX) {
|
||||||
|
ok = depth < LZW_TABLE_SIZE - 1u;
|
||||||
|
if (ok) {
|
||||||
|
stack[depth++] = suffix[cur];
|
||||||
|
cur = prefix[cur];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!ok) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
stack[depth++] = (uint8_t)cur;
|
||||||
|
first = (uint8_t)cur;
|
||||||
|
while (depth > 0u && outPos < outLen) {
|
||||||
|
out[outPos++] = stack[--depth];
|
||||||
|
}
|
||||||
|
if (next < LZW_TABLE_SIZE) {
|
||||||
|
if (next >= (1u << width) - 1u && width < LZW_MAX_BITS) {
|
||||||
|
width++;
|
||||||
|
}
|
||||||
|
prefix[next] = prev;
|
||||||
|
suffix[next] = first;
|
||||||
|
next++;
|
||||||
|
}
|
||||||
|
prev = code;
|
||||||
|
}
|
||||||
|
jlFree(prefix);
|
||||||
|
jlFree(suffix);
|
||||||
|
jlFree(stack);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Nibble k of data, the high one of each byte first.
|
||||||
|
static uint8_t nibbleAt(const uint8_t *data, uint32_t k) {
|
||||||
|
return ((k & 1u) != 0u) ? (uint8_t)(data[k >> 1] & LOW_NIBBLE) : (uint8_t)(data[k >> 1] >> NIBBLE_BITS);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static uint16_t readLe16(const uint8_t *p) {
|
||||||
|
return (uint16_t)(p[0] | ((uint16_t)p[1] << 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Seek to resource index's header in its VOL file and read it: false when
|
// Seek to resource index's header in its VOL file and read it: false when
|
||||||
// the number is past its directory or unused, or its entry names a VOL file
|
// the number is past its directory or unused, or its entry names a VOL file
|
||||||
// the game lacks or a place in one that holds no header for that volume
|
// the game lacks or a place in one that holds no header for that volume
|
||||||
|
|
@ -250,6 +515,7 @@ static bool resHeaderRead(const AgiGameT *game, AgiResTypeE type, uint16_t index
|
||||||
const AgiResEntryT *entry;
|
const AgiResEntryT *entry;
|
||||||
FILE *fp;
|
FILE *fp;
|
||||||
long offset;
|
long offset;
|
||||||
|
size_t size;
|
||||||
|
|
||||||
if (index >= game->resCount[type]) {
|
if (index >= game->resCount[type]) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -263,10 +529,11 @@ static bool resHeaderRead(const AgiGameT *game, AgiResTypeE type, uint16_t index
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
offset = ((long)entry->offsetHigh << DIR_OFFSET_HIGH_SHIFT) | (long)entry->offsetLow;
|
offset = ((long)entry->offsetHigh << DIR_OFFSET_HIGH_SHIFT) | (long)entry->offsetLow;
|
||||||
if (fseek(fp, offset, SEEK_SET) != 0 || fread(header, 1, VOL_HEADER_BYTES, fp) != VOL_HEADER_BYTES) {
|
size = game->v3 ? VOL_HEADER_V3_BYTES : VOL_HEADER_V2_BYTES;
|
||||||
|
if (fseek(fp, offset, SEEK_SET) != 0 || fread(header, 1, size, fp) != size) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return header[0] == VOL_SIG_0 && header[1] == VOL_SIG_1 && (header[2] & VOL_VOLUME_MASK) == entry->volume;
|
return header[0] == VOL_SIG_0 && header[1] == VOL_SIG_1 && (header[VOL_VOLUME_BYTE] & VOL_VOLUME_MASK) == entry->volume;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -329,33 +596,45 @@ void agiResClose(AgiGameT *game) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// A resource, as v2 stores it: v3's packed ones expanded (see the top).
|
||||||
uint8_t *agiResLoad(const AgiGameT *game, AgiResTypeE type, uint16_t index, uint16_t *outLength) {
|
uint8_t *agiResLoad(const AgiGameT *game, AgiResTypeE type, uint16_t index, uint16_t *outLength) {
|
||||||
FILE *fp;
|
FILE *fp;
|
||||||
uint8_t header[VOL_HEADER_BYTES];
|
uint8_t header[VOL_HEADER_MAX];
|
||||||
|
uint8_t *stored;
|
||||||
uint8_t *payload;
|
uint8_t *payload;
|
||||||
uint16_t length;
|
uint16_t length;
|
||||||
|
uint16_t storedLength;
|
||||||
|
|
||||||
if (!resHeaderRead(game, type, index, header)) {
|
if (!resHeaderRead(game, type, index, header)) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
// v3 resources are LZW-compressed and flag it in the volume byte.
|
fp = game->volFiles[game->resDir[type][index].volume];
|
||||||
if ((header[2] & VOL_COMPRESSED) != 0u) {
|
length = readLe16(&header[VOL_LENGTH_BYTE]);
|
||||||
|
storedLength = game->v3 ? readLe16(&header[VOL_STORED_BYTE]) : length;
|
||||||
|
if (length == 0u || storedLength == 0u) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
fp = game->volFiles[game->resDir[type][index].volume];
|
stored = (uint8_t *)jlAlloc(storedLength);
|
||||||
length = (uint16_t)(header[3] | ((uint16_t)header[4] << 8));
|
if (stored == NULL) {
|
||||||
if (length == 0u) {
|
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
payload = (uint8_t *)jlAlloc(length);
|
if (fread(stored, 1, storedLength, fp) != storedLength) {
|
||||||
if (payload == NULL) {
|
jlFree(stored);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
if (fread(payload, 1, length, fp) != length) {
|
payload = stored;
|
||||||
jlFree(payload);
|
if (game->v3 && (header[VOL_VOLUME_BYTE] & VOL_NIBBLE_PICTURE) != 0u) {
|
||||||
return NULL;
|
payload = expandNibblePicture(stored, storedLength, &length);
|
||||||
|
jlFree(stored);
|
||||||
|
} else if (storedLength != length) {
|
||||||
|
payload = (uint8_t *)jlAlloc(length);
|
||||||
|
if (payload != NULL && !lzwExpand(stored, storedLength, payload, length)) {
|
||||||
|
jlFree(payload);
|
||||||
|
payload = NULL;
|
||||||
|
}
|
||||||
|
jlFree(stored);
|
||||||
}
|
}
|
||||||
if (outLength != NULL) {
|
if (payload != NULL && outLength != NULL) {
|
||||||
*outLength = length;
|
*outLength = length;
|
||||||
}
|
}
|
||||||
return payload;
|
return payload;
|
||||||
|
|
@ -363,22 +642,26 @@ uint8_t *agiResLoad(const AgiGameT *game, AgiResTypeE type, uint16_t index, uint
|
||||||
|
|
||||||
|
|
||||||
bool agiResOpen(AgiGameT *game) {
|
bool agiResOpen(AgiGameT *game) {
|
||||||
char name[VOL_NAME_MAX];
|
char name[VOL_NAME_MAX];
|
||||||
uint8_t t;
|
uint8_t t;
|
||||||
uint8_t v;
|
uint8_t v;
|
||||||
bool anyVolume;
|
uint16_t version;
|
||||||
|
bool anyVolume;
|
||||||
|
|
||||||
memset(game, 0, sizeof(*game));
|
memset(game, 0, sizeof(*game));
|
||||||
for (t = 0u; t < AGI_RES_COUNT; t++) {
|
for (t = 0u; t < AGI_RES_COUNT; t++) {
|
||||||
loadDirectory(game, kDirNames[t], (AgiResTypeE)t);
|
loadDirectory(game, kDirNames[t], (AgiResTypeE)t);
|
||||||
}
|
}
|
||||||
|
if (game->resCount[AGI_RES_LOGIC] == 0u) {
|
||||||
|
loadDirectoryV3(game);
|
||||||
|
}
|
||||||
if (game->resCount[AGI_RES_LOGIC] == 0u) {
|
if (game->resCount[AGI_RES_LOGIC] == 0u) {
|
||||||
agiResClose(game);
|
agiResClose(game);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
anyVolume = false;
|
anyVolume = false;
|
||||||
for (v = 0u; v < AGI_MAX_VOLUMES; v++) {
|
for (v = 0u; v < AGI_MAX_VOLUMES; v++) {
|
||||||
buildVolName(name, v);
|
buildVolName(game, name, v);
|
||||||
game->volFiles[v] = jlDataOpen(name, "rb");
|
game->volFiles[v] = jlDataOpen(name, "rb");
|
||||||
if (game->volFiles[v] != NULL) {
|
if (game->volFiles[v] != NULL) {
|
||||||
anyVolume = true;
|
anyVolume = true;
|
||||||
|
|
@ -398,15 +681,27 @@ bool agiResOpen(AgiGameT *game) {
|
||||||
if (!loadObjects(game)) {
|
if (!loadObjects(game)) {
|
||||||
game->maxObjects = AGI_MAX_OBJECTS;
|
game->maxObjects = AGI_MAX_OBJECTS;
|
||||||
}
|
}
|
||||||
game->interpreterVersion = detectVersion();
|
version = detectVersion();
|
||||||
|
if (!game->v3 || version >= AGI_VERSION_V3_FIRST) {
|
||||||
|
game->interpreterVersion = version;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Whether resource index of type is LZW-packed (v3), so its LOGIC messages
|
||||||
|
// are not encrypted.
|
||||||
|
bool agiResPacked(const AgiGameT *game, AgiResTypeE type, uint16_t index) {
|
||||||
|
uint8_t header[VOL_HEADER_MAX];
|
||||||
|
|
||||||
|
return game->v3 && resHeaderRead(game, type, index, header) && (header[VOL_VOLUME_BYTE] & VOL_NIBBLE_PICTURE) == 0u && readLe16(&header[VOL_LENGTH_BYTE]) != readLe16(&header[VOL_STORED_BYTE]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Whether resource index of type is in the game's files: its entry points at
|
// Whether resource index of type is in the game's files: its entry points at
|
||||||
// a resource header in its VOL file (resHeaderRead).
|
// a resource header in its VOL file (resHeaderRead).
|
||||||
bool agiResPresent(const AgiGameT *game, AgiResTypeE type, uint16_t index) {
|
bool agiResPresent(const AgiGameT *game, AgiResTypeE type, uint16_t index) {
|
||||||
uint8_t header[VOL_HEADER_BYTES];
|
uint8_t header[VOL_HEADER_MAX];
|
||||||
|
|
||||||
return resHeaderRead(game, type, index, header);
|
return resHeaderRead(game, type, index, header);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
248
agiSave.c
248
agiSave.c
|
|
@ -11,6 +11,13 @@
|
||||||
// The files are JoeyLib save files (SAVES/), AGISAVE1..AGISAVE9, in
|
// The files are JoeyLib save files (SAVES/), AGISAVE1..AGISAVE9, in
|
||||||
// JoeyAGI's own format: the state as the machine holds it, so a save made
|
// JoeyAGI's own format: the state as the machine holds it, so a save made
|
||||||
// on one kind of machine is for that kind only.
|
// on one kind of machine is for that kind only.
|
||||||
|
//
|
||||||
|
// set.simple names the saves itself, as Sierra's interpreter does: save
|
||||||
|
// goes without asking to the slot with that description, else to the
|
||||||
|
// first empty one (with every slot taken by others, a pointer picks one to
|
||||||
|
// overwrite), and restore lists the saves by description for a pointer.
|
||||||
|
// The pointer starts on the newest save (each is numbered one above the
|
||||||
|
// highest on the disk) and wraps at either end of the list.
|
||||||
|
|
||||||
#include "agi.h"
|
#include "agi.h"
|
||||||
|
|
||||||
|
|
@ -26,10 +33,17 @@
|
||||||
#define SAVE_MAGIC_0 'J'
|
#define SAVE_MAGIC_0 'J'
|
||||||
#define SAVE_MAGIC_1 'A'
|
#define SAVE_MAGIC_1 'A'
|
||||||
#define SAVE_MAGIC_2 'G'
|
#define SAVE_MAGIC_2 'G'
|
||||||
#define SAVE_VERSION 1u
|
#define SAVE_VERSION 2u
|
||||||
#define SAVE_SLOTS 9u
|
#define SAVE_SLOTS 9u
|
||||||
#define SAVE_DESC_MAX 30u
|
#define SAVE_DESC_MAX 30u
|
||||||
#define SAVE_NAME_MAX 12u
|
#define SAVE_NAME_MAX 12u
|
||||||
|
// The pointer lists: their longest header, the box's width (as Sierra's,
|
||||||
|
// whatever the text), and the pointer drawn over the first column of an
|
||||||
|
// entry (" - name").
|
||||||
|
#define POINTER_HEADER_LINES 4u
|
||||||
|
#define POINTER_BOX_COLS 34
|
||||||
|
#define POINTER_ON ">"
|
||||||
|
#define POINTER_OFF " "
|
||||||
// The slot window is as wide as the screen allows with its frame.
|
// The slot window is as wide as the screen allows with its frame.
|
||||||
#define PICK_WIDTH (AGI_TEXT_COLS - 4u)
|
#define PICK_WIDTH (AGI_TEXT_COLS - 4u)
|
||||||
|
|
||||||
|
|
@ -51,18 +65,13 @@ typedef struct {
|
||||||
uint8_t cycleCount;
|
uint8_t cycleCount;
|
||||||
uint8_t cycleMode;
|
uint8_t cycleMode;
|
||||||
uint8_t motion;
|
uint8_t motion;
|
||||||
uint8_t loopFlag;
|
uint8_t parm[AGI_OBJ_PARMS];
|
||||||
uint8_t motionFlag;
|
|
||||||
uint8_t targetX;
|
|
||||||
uint8_t targetY;
|
|
||||||
uint8_t savedStepSize;
|
|
||||||
uint8_t randomSteps;
|
|
||||||
uint8_t followDistance;
|
|
||||||
} SavedObjectT;
|
} SavedObjectT;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char magic[3];
|
char magic[3];
|
||||||
uint8_t version;
|
uint8_t version;
|
||||||
|
uint32_t sequence;
|
||||||
char description[SAVE_DESC_MAX + 1u];
|
char description[SAVE_DESC_MAX + 1u];
|
||||||
uint8_t vars[AGI_NUM_VARS];
|
uint8_t vars[AGI_NUM_VARS];
|
||||||
uint8_t flags[AGI_NUM_FLAGS];
|
uint8_t flags[AGI_NUM_FLAGS];
|
||||||
|
|
@ -89,6 +98,11 @@ typedef struct {
|
||||||
uint8_t textFg;
|
uint8_t textFg;
|
||||||
uint8_t textBg;
|
uint8_t textBg;
|
||||||
char cursorChar;
|
char cursorChar;
|
||||||
|
bool holdKey;
|
||||||
|
bool simpleSaveOn;
|
||||||
|
char simpleSaveDesc[AGI_STRING_LEN + 1u];
|
||||||
|
bool priorityTableSet;
|
||||||
|
uint8_t priorityTable[AGI_HEIGHT];
|
||||||
uint8_t scriptCount;
|
uint8_t scriptCount;
|
||||||
AgiScriptEntryT script[AGI_SCRIPT_MAX];
|
AgiScriptEntryT script[AGI_SCRIPT_MAX];
|
||||||
} SaveFileT;
|
} SaveFileT;
|
||||||
|
|
@ -97,11 +111,23 @@ typedef struct {
|
||||||
// ----- Prototypes -----
|
// ----- Prototypes -----
|
||||||
|
|
||||||
static bool fileBuffer(void);
|
static bool fileBuffer(void);
|
||||||
|
static bool isSimpleDesc(const char *desc);
|
||||||
static uint8_t pickSlot(const char *title, bool mustExist);
|
static uint8_t pickSlot(const char *title, bool mustExist);
|
||||||
static bool readDescription(uint8_t slot, char *out);
|
static uint8_t pointerPick(const char *header, bool offerNamed);
|
||||||
|
static uint32_t newestSequence(void);
|
||||||
|
static bool readDescription(uint8_t slot, char *out, uint32_t *sequence);
|
||||||
|
static void simpleDesc(char *out);
|
||||||
|
static uint8_t simpleSlot(void);
|
||||||
static void slotName(uint8_t slot, char *out);
|
static void slotName(uint8_t slot, char *out);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Strings -----
|
||||||
|
|
||||||
|
// set.simple's dialogs, worded as Sierra's.
|
||||||
|
static const char kSimpleRestore[] = "Use the arrow keys to move\n the pointer to your name.\nThen press ENTER\n";
|
||||||
|
static const char kSimpleFull[] = " Sorry, this disk is full.\nPosition pointer and press ENTER\n to overwrite a saved game\nor press ESC to cancel.\n";
|
||||||
|
|
||||||
|
|
||||||
// ----- Module state -----
|
// ----- Module state -----
|
||||||
|
|
||||||
static SaveFileT *gFile;
|
static SaveFileT *gFile;
|
||||||
|
|
@ -119,6 +145,12 @@ static bool fileBuffer(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// A save description that is set.simple's.
|
||||||
|
static bool isSimpleDesc(const char *desc) {
|
||||||
|
return strncmp(desc, gAgi->simpleSaveDesc, SAVE_DESC_MAX) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static uint8_t pickSlot(const char *title, bool mustExist) {
|
static uint8_t pickSlot(const char *title, bool mustExist) {
|
||||||
char text[(SAVE_SLOTS + 3u) * (AGI_TEXT_COLS + 1u)];
|
char text[(SAVE_SLOTS + 3u) * (AGI_TEXT_COLS + 1u)];
|
||||||
char desc[SAVE_DESC_MAX + 1u];
|
char desc[SAVE_DESC_MAX + 1u];
|
||||||
|
|
@ -129,7 +161,7 @@ static uint8_t pickSlot(const char *title, bool mustExist) {
|
||||||
// A window listing the slots; a digit picks one.
|
// A window listing the slots; a digit picks one.
|
||||||
n = (size_t)snprintf(text, sizeof(text), "%s\n\n", title);
|
n = (size_t)snprintf(text, sizeof(text), "%s\n\n", title);
|
||||||
for (slot = 1u; slot <= SAVE_SLOTS && n < sizeof(text); slot++) {
|
for (slot = 1u; slot <= SAVE_SLOTS && n < sizeof(text); slot++) {
|
||||||
if (!readDescription(slot, desc)) {
|
if (!readDescription(slot, desc, NULL)) {
|
||||||
strcpy(desc, "-");
|
strcpy(desc, "-");
|
||||||
}
|
}
|
||||||
n += (size_t)snprintf(text + n, sizeof(text) - n, "%u. %s\n", (unsigned)slot, desc);
|
n += (size_t)snprintf(text + n, sizeof(text) - n, "%u. %s\n", (unsigned)slot, desc);
|
||||||
|
|
@ -146,7 +178,7 @@ static uint8_t pickSlot(const char *title, bool mustExist) {
|
||||||
}
|
}
|
||||||
if (key >= (uint16_t)'1' && key <= (uint16_t)('0' + SAVE_SLOTS)) {
|
if (key >= (uint16_t)'1' && key <= (uint16_t)('0' + SAVE_SLOTS)) {
|
||||||
slot = (uint8_t)(key - '0');
|
slot = (uint8_t)(key - '0');
|
||||||
if (!mustExist || readDescription(slot, desc)) {
|
if (!mustExist || readDescription(slot, desc, NULL)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -156,8 +188,104 @@ static uint8_t pickSlot(const char *title, bool mustExist) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// A slot's description, read from the start of its file.
|
// The highest sequence number of the saves on the disk.
|
||||||
static bool readDescription(uint8_t slot, char *out) {
|
static uint32_t newestSequence(void) {
|
||||||
|
char desc[SAVE_DESC_MAX + 1u];
|
||||||
|
uint32_t newest;
|
||||||
|
uint32_t sequence;
|
||||||
|
uint8_t slot;
|
||||||
|
|
||||||
|
newest = 0u;
|
||||||
|
for (slot = 1u; slot <= SAVE_SLOTS; slot++) {
|
||||||
|
if (readDescription(slot, desc, &sequence) && sequence > newest) {
|
||||||
|
newest = sequence;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newest;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The saves listed under a header, one chosen with a pointer (set.simple's
|
||||||
|
// dialogs), starting on the newest. With offerNamed, set.simple's description is listed too when
|
||||||
|
// no save has it, as slot 0: Sierra's Mother Goose ships a save by that
|
||||||
|
// name ("Start New Character") that starts the game afresh, which
|
||||||
|
// JoeyAGI, keeping its own save files, does by restoring nothing. The
|
||||||
|
// slot, or 0 for that entry or ESC.
|
||||||
|
static uint8_t pointerPick(const char *header, bool offerNamed) {
|
||||||
|
char text[(SAVE_SLOTS + POINTER_HEADER_LINES + 3u) * (AGI_TEXT_COLS + 1u)];
|
||||||
|
char desc[SAVE_DESC_MAX + 1u];
|
||||||
|
uint8_t slots[SAVE_SLOTS + 1u];
|
||||||
|
uint8_t count;
|
||||||
|
uint8_t current;
|
||||||
|
uint8_t first;
|
||||||
|
uint8_t slot;
|
||||||
|
uint16_t key;
|
||||||
|
uint32_t sequence;
|
||||||
|
uint32_t newest;
|
||||||
|
size_t n;
|
||||||
|
bool named;
|
||||||
|
const char *c;
|
||||||
|
|
||||||
|
// The entries start a line below the header's last.
|
||||||
|
first = 1u;
|
||||||
|
for (c = header; *c != '\0'; c++) {
|
||||||
|
if (*c == '\n') {
|
||||||
|
first++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n = (size_t)snprintf(text, sizeof(text), "%s", header);
|
||||||
|
count = 0u;
|
||||||
|
current = 0u;
|
||||||
|
newest = 0u;
|
||||||
|
named = false;
|
||||||
|
for (slot = 1u; slot <= SAVE_SLOTS; slot++) {
|
||||||
|
if (readDescription(slot, desc, &sequence)) {
|
||||||
|
if (sequence > newest) {
|
||||||
|
newest = sequence;
|
||||||
|
current = count;
|
||||||
|
}
|
||||||
|
n += (size_t)snprintf(text + n, sizeof(text) - n, "\n - %s", desc);
|
||||||
|
slots[count++] = slot;
|
||||||
|
named = named || isSimpleDesc(desc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (offerNamed && !named) {
|
||||||
|
simpleDesc(desc);
|
||||||
|
n += (size_t)snprintf(text + n, sizeof(text) - n, "\n - %s", desc);
|
||||||
|
slots[count++] = 0u;
|
||||||
|
}
|
||||||
|
// A blank last line as wide as the box sets its size.
|
||||||
|
(void)snprintf(text + n, sizeof(text) - n, "\n%*s", POINTER_BOX_COLS, "");
|
||||||
|
if (count == 0u) {
|
||||||
|
return 0u;
|
||||||
|
}
|
||||||
|
textPrint(text, -1, -1, PICK_WIDTH);
|
||||||
|
for (;;) {
|
||||||
|
textWindowPut((uint8_t)(first + current), 0u, POINTER_ON);
|
||||||
|
key = inputWaitKey();
|
||||||
|
textWindowPut((uint8_t)(first + current), 0u, POINTER_OFF);
|
||||||
|
if (key == AGI_KEY_ESCAPE) {
|
||||||
|
slot = 0u;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (key == AGI_KEY_ENTER) {
|
||||||
|
slot = slots[current];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (key == AGI_KEY_UP) {
|
||||||
|
current = (uint8_t)((current + count - 1u) % count);
|
||||||
|
} else if (key == AGI_KEY_DOWN) {
|
||||||
|
current = (uint8_t)((current + 1u) % count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
textCloseWindow();
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// A slot's description and sequence number (unless NULL), read from the
|
||||||
|
// start of its file; false for no save of this version.
|
||||||
|
static bool readDescription(uint8_t slot, char *out, uint32_t *sequence) {
|
||||||
char name[SAVE_NAME_MAX];
|
char name[SAVE_NAME_MAX];
|
||||||
FILE *fp;
|
FILE *fp;
|
||||||
char head[offsetof(SaveFileT, description) + SAVE_DESC_MAX + 1u];
|
char head[offsetof(SaveFileT, description) + SAVE_DESC_MAX + 1u];
|
||||||
|
|
@ -167,17 +295,51 @@ static bool readDescription(uint8_t slot, char *out) {
|
||||||
if (fp == NULL) {
|
if (fp == NULL) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (fread(head, 1, sizeof(head), fp) != sizeof(head) || head[0] != SAVE_MAGIC_0 || head[1] != SAVE_MAGIC_1 || head[2] != SAVE_MAGIC_2) {
|
if (fread(head, 1, sizeof(head), fp) != sizeof(head) || head[0] != SAVE_MAGIC_0 || head[1] != SAVE_MAGIC_1 || head[2] != SAVE_MAGIC_2 || (uint8_t)head[offsetof(SaveFileT, version)] != SAVE_VERSION) {
|
||||||
fclose(fp);
|
fclose(fp);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
fclose(fp);
|
fclose(fp);
|
||||||
memcpy(out, &head[offsetof(SaveFileT, description)], SAVE_DESC_MAX + 1u);
|
memcpy(out, &head[offsetof(SaveFileT, description)], SAVE_DESC_MAX + 1u);
|
||||||
out[SAVE_DESC_MAX] = '\0';
|
out[SAVE_DESC_MAX] = '\0';
|
||||||
|
if (sequence != NULL) {
|
||||||
|
memcpy(sequence, &head[offsetof(SaveFileT, sequence)], sizeof(*sequence));
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// set.simple's description, cut to a save's.
|
||||||
|
static void simpleDesc(char *out) {
|
||||||
|
const char *end;
|
||||||
|
|
||||||
|
end = (const char *)memchr(gAgi->simpleSaveDesc, '\0', SAVE_DESC_MAX);
|
||||||
|
memset(out, 0, SAVE_DESC_MAX + 1u);
|
||||||
|
memcpy(out, gAgi->simpleSaveDesc, end != NULL ? (size_t)(end - gAgi->simpleSaveDesc) : SAVE_DESC_MAX);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// set.simple's save slot: the one saved under its description, else the
|
||||||
|
// first empty one; 0 when every slot holds another.
|
||||||
|
static uint8_t simpleSlot(void) {
|
||||||
|
char desc[SAVE_DESC_MAX + 1u];
|
||||||
|
uint8_t slot;
|
||||||
|
uint8_t fresh;
|
||||||
|
|
||||||
|
fresh = 0u;
|
||||||
|
for (slot = 1u; slot <= SAVE_SLOTS; slot++) {
|
||||||
|
if (readDescription(slot, desc, NULL)) {
|
||||||
|
if (isSimpleDesc(desc)) {
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
} else if (fresh == 0u) {
|
||||||
|
fresh = slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fresh;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static void slotName(uint8_t slot, char *out) {
|
static void slotName(uint8_t slot, char *out) {
|
||||||
(void)snprintf(out, SAVE_NAME_MAX, "AGISAVE%u", (unsigned)slot);
|
(void)snprintf(out, SAVE_NAME_MAX, "AGISAVE%u", (unsigned)slot);
|
||||||
}
|
}
|
||||||
|
|
@ -191,7 +353,11 @@ bool restoreGame(void) {
|
||||||
uint8_t i;
|
uint8_t i;
|
||||||
uint16_t r;
|
uint16_t r;
|
||||||
|
|
||||||
slot = pickSlot("Restore which game?", true);
|
if (gAgi->simpleSaveOn) {
|
||||||
|
slot = pointerPick(kSimpleRestore, true);
|
||||||
|
} else {
|
||||||
|
slot = pickSlot("Restore which game?", true);
|
||||||
|
}
|
||||||
if (slot == 0u) {
|
if (slot == 0u) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -234,6 +400,11 @@ bool restoreGame(void) {
|
||||||
gAgi->textFg = gFile->textFg;
|
gAgi->textFg = gFile->textFg;
|
||||||
gAgi->textBg = gFile->textBg;
|
gAgi->textBg = gFile->textBg;
|
||||||
gAgi->cursorChar = gFile->cursorChar;
|
gAgi->cursorChar = gFile->cursorChar;
|
||||||
|
gAgi->holdKey = gFile->holdKey;
|
||||||
|
gAgi->simpleSaveOn = gFile->simpleSaveOn;
|
||||||
|
memcpy(gAgi->simpleSaveDesc, gFile->simpleSaveDesc, sizeof(gAgi->simpleSaveDesc));
|
||||||
|
gAgi->priorityTableSet = gFile->priorityTableSet;
|
||||||
|
memcpy(gAgi->priorityTable, gFile->priorityTable, sizeof(gAgi->priorityTable));
|
||||||
// Rebuild the picture with no objects drawn, then the objects.
|
// Rebuild the picture with no objects drawn, then the objects.
|
||||||
gAgi->scriptCount = gFile->scriptCount;
|
gAgi->scriptCount = gFile->scriptCount;
|
||||||
memcpy(gAgi->script, gFile->script, sizeof(gAgi->script));
|
memcpy(gAgi->script, gFile->script, sizeof(gAgi->script));
|
||||||
|
|
@ -266,13 +437,7 @@ bool restoreGame(void) {
|
||||||
o->cycleCount = s->cycleCount;
|
o->cycleCount = s->cycleCount;
|
||||||
o->cycleMode = s->cycleMode;
|
o->cycleMode = s->cycleMode;
|
||||||
o->motion = s->motion;
|
o->motion = s->motion;
|
||||||
o->loopFlag = s->loopFlag;
|
memcpy(o->parm, s->parm, sizeof(o->parm));
|
||||||
o->motionFlag = s->motionFlag;
|
|
||||||
o->targetX = s->targetX;
|
|
||||||
o->targetY = s->targetY;
|
|
||||||
o->savedStepSize = s->savedStepSize;
|
|
||||||
o->randomSteps = s->randomSteps;
|
|
||||||
o->followDistance = s->followDistance;
|
|
||||||
}
|
}
|
||||||
gfxDrawStatic();
|
gfxDrawStatic();
|
||||||
jlSurfaceClear(jlStageGet(), AGI_COLOR_BLACK);
|
jlSurfaceClear(jlStageGet(), AGI_COLOR_BLACK);
|
||||||
|
|
@ -292,13 +457,24 @@ void saveGame(void) {
|
||||||
uint8_t i;
|
uint8_t i;
|
||||||
uint16_t r;
|
uint16_t r;
|
||||||
|
|
||||||
slot = pickSlot("Save in which slot?", false);
|
if (gAgi->simpleSaveOn) {
|
||||||
if (slot == 0u) {
|
slot = simpleSlot();
|
||||||
return;
|
if (slot == 0u) {
|
||||||
}
|
slot = pointerPick(kSimpleFull, false);
|
||||||
desc[0] = '\0';
|
}
|
||||||
if (!inputGetString(desc, SAVE_DESC_MAX, AGI_NO_POSITION, AGI_NO_POSITION, "Description: ")) {
|
if (slot == 0u) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
simpleDesc(desc);
|
||||||
|
} else {
|
||||||
|
slot = pickSlot("Save in which slot?", false);
|
||||||
|
if (slot == 0u) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
desc[0] = '\0';
|
||||||
|
if (!inputGetString(desc, SAVE_DESC_MAX, AGI_NO_POSITION, AGI_NO_POSITION, "Description: ")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!fileBuffer()) {
|
if (!fileBuffer()) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -308,6 +484,7 @@ void saveGame(void) {
|
||||||
gFile->magic[1] = SAVE_MAGIC_1;
|
gFile->magic[1] = SAVE_MAGIC_1;
|
||||||
gFile->magic[2] = SAVE_MAGIC_2;
|
gFile->magic[2] = SAVE_MAGIC_2;
|
||||||
gFile->version = SAVE_VERSION;
|
gFile->version = SAVE_VERSION;
|
||||||
|
gFile->sequence = newestSequence() + 1u;
|
||||||
memcpy(gFile->description, desc, sizeof(desc));
|
memcpy(gFile->description, desc, sizeof(desc));
|
||||||
memcpy(gFile->vars, gAgi->vars, sizeof(gFile->vars));
|
memcpy(gFile->vars, gAgi->vars, sizeof(gFile->vars));
|
||||||
memcpy(gFile->flags, gAgi->flags, sizeof(gFile->flags));
|
memcpy(gFile->flags, gAgi->flags, sizeof(gFile->flags));
|
||||||
|
|
@ -337,6 +514,11 @@ void saveGame(void) {
|
||||||
gFile->textFg = gAgi->textFg;
|
gFile->textFg = gAgi->textFg;
|
||||||
gFile->textBg = gAgi->textBg;
|
gFile->textBg = gAgi->textBg;
|
||||||
gFile->cursorChar = gAgi->cursorChar;
|
gFile->cursorChar = gAgi->cursorChar;
|
||||||
|
gFile->holdKey = gAgi->holdKey;
|
||||||
|
gFile->simpleSaveOn = gAgi->simpleSaveOn;
|
||||||
|
memcpy(gFile->simpleSaveDesc, gAgi->simpleSaveDesc, sizeof(gFile->simpleSaveDesc));
|
||||||
|
gFile->priorityTableSet = gAgi->priorityTableSet;
|
||||||
|
memcpy(gFile->priorityTable, gAgi->priorityTable, sizeof(gFile->priorityTable));
|
||||||
for (i = 0u; i < AGI_MAX_OBJECTS; i++) {
|
for (i = 0u; i < AGI_MAX_OBJECTS; i++) {
|
||||||
const AgiObjectT *o = &gAgi->objects[i];
|
const AgiObjectT *o = &gAgi->objects[i];
|
||||||
SavedObjectT *s = &gFile->objects[i];
|
SavedObjectT *s = &gFile->objects[i];
|
||||||
|
|
@ -356,13 +538,7 @@ void saveGame(void) {
|
||||||
s->cycleCount = o->cycleCount;
|
s->cycleCount = o->cycleCount;
|
||||||
s->cycleMode = o->cycleMode;
|
s->cycleMode = o->cycleMode;
|
||||||
s->motion = o->motion;
|
s->motion = o->motion;
|
||||||
s->loopFlag = o->loopFlag;
|
memcpy(s->parm, o->parm, sizeof(s->parm));
|
||||||
s->motionFlag = o->motionFlag;
|
|
||||||
s->targetX = o->targetX;
|
|
||||||
s->targetY = o->targetY;
|
|
||||||
s->savedStepSize = o->savedStepSize;
|
|
||||||
s->randomSteps = o->randomSteps;
|
|
||||||
s->followDistance = o->followDistance;
|
|
||||||
}
|
}
|
||||||
gFile->scriptCount = gAgi->scriptCount;
|
gFile->scriptCount = gAgi->scriptCount;
|
||||||
memcpy(gFile->script, gAgi->script, sizeof(gFile->script));
|
memcpy(gFile->script, gAgi->script, sizeof(gFile->script));
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,6 @@
|
||||||
#define NOTE_BYTES 5u
|
#define NOTE_BYTES 5u
|
||||||
#define NOTE_END 0xFFFFu
|
#define NOTE_END 0xFFFFu
|
||||||
#define TICKS_PER_SECOND 60u
|
#define TICKS_PER_SECOND 60u
|
||||||
#define MS_PER_SECOND 1000u
|
|
||||||
#define PSG_CLOCK_HZ 111860u
|
#define PSG_CLOCK_HZ 111860u
|
||||||
#define DIVISOR_HIGH_MASK 0x3Fu
|
#define DIVISOR_HIGH_MASK 0x3Fu
|
||||||
#define DIVISOR_LOW_MASK 0x0Fu
|
#define DIVISOR_LOW_MASK 0x0Fu
|
||||||
|
|
@ -159,7 +158,7 @@ void soundPump(void) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
now = jlMillisElapsed();
|
now = jlMillisElapsed();
|
||||||
due = (uint32_t)((now - gTickStartMs) * TICKS_PER_SECOND / MS_PER_SECOND);
|
due = (uint32_t)((now - gTickStartMs) * TICKS_PER_SECOND / AGI_MS_PER_SECOND);
|
||||||
while (gTicksDone < due && gData != NULL) {
|
while (gTicksDone < due && gData != NULL) {
|
||||||
gTicksDone++;
|
gTicksDone++;
|
||||||
allDone = true;
|
allDone = true;
|
||||||
|
|
|
||||||
212
agiText.c
212
agiText.c
|
|
@ -69,6 +69,8 @@
|
||||||
#define TEXT_ATTR_BG_SHIFT 4u
|
#define TEXT_ATTR_BG_SHIFT 4u
|
||||||
#define MENU_MARGIN 8
|
#define MENU_MARGIN 8
|
||||||
#define MENU_FRAME AGI_COLOR_BLACK
|
#define MENU_FRAME AGI_COLOR_BLACK
|
||||||
|
// No text cell (the pointer's last one before it is seen).
|
||||||
|
#define POINTER_NOWHERE (-1)
|
||||||
#define DOT_MASK_EVEN_ROW 0xAAu
|
#define DOT_MASK_EVEN_ROW 0xAAu
|
||||||
#define DOT_MASK_ODD_ROW 0x55u
|
#define DOT_MASK_ODD_ROW 0x55u
|
||||||
#define SHOW_OBJ_MARGIN 4
|
#define SHOW_OBJ_MARGIN 4
|
||||||
|
|
@ -81,6 +83,14 @@ typedef enum {
|
||||||
SPAN_RESTORE = 2
|
SPAN_RESTORE = 2
|
||||||
} SpanOpE;
|
} SpanOpE;
|
||||||
|
|
||||||
|
// What the pointer is over while the menus are open.
|
||||||
|
typedef enum {
|
||||||
|
MENU_HIT_NONE = 0,
|
||||||
|
MENU_HIT_BAR = 1,
|
||||||
|
MENU_HIT_TITLE = 2,
|
||||||
|
MENU_HIT_ITEM = 3
|
||||||
|
} MenuHitE;
|
||||||
|
|
||||||
|
|
||||||
// ----- Prototypes -----
|
// ----- Prototypes -----
|
||||||
|
|
||||||
|
|
@ -91,9 +101,12 @@ static void copySpan(jlSurfaceT *dst, const jlSurfaceT *src, int16_t col, in
|
||||||
static void drawBox(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t frame);
|
static void drawBox(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t frame);
|
||||||
static uint8_t drawMenuBar(uint8_t current);
|
static uint8_t drawMenuBar(uint8_t current);
|
||||||
static void drawMenuItems(uint8_t menu, uint8_t current);
|
static void drawMenuItems(uint8_t menu, uint8_t current);
|
||||||
|
static uint8_t menuItemsCol(const AgiMenuT *m, uint8_t *outWidth);
|
||||||
|
static MenuHitE menuUnderPointer(uint8_t open, uint8_t *menu, uint8_t *item);
|
||||||
static void drawText(uint8_t row, uint8_t col, const char *text, uint8_t fg, uint8_t bg);
|
static void drawText(uint8_t row, uint8_t col, const char *text, uint8_t fg, uint8_t bg);
|
||||||
static void echoCell(char ch, uint8_t fg, uint8_t bg);
|
static void echoCell(char ch, uint8_t fg, uint8_t bg);
|
||||||
static void expandInto(const char *src, char *dst, uint16_t dstCap, uint16_t *pos, uint8_t depth);
|
static void expandInto(const char *src, char *dst, uint16_t dstCap, uint16_t *pos, uint8_t depth);
|
||||||
|
static uint8_t nextRow(uint8_t row);
|
||||||
static void openWindow(int16_t x, int16_t y, int16_t w, int16_t h);
|
static void openWindow(int16_t x, int16_t y, int16_t w, int16_t h);
|
||||||
static void putCell(uint8_t row, uint8_t col, char ch, uint8_t fg, uint8_t bg);
|
static void putCell(uint8_t row, uint8_t col, char ch, uint8_t fg, uint8_t bg);
|
||||||
static void putGlyph(uint8_t row, uint8_t col, char ch, uint8_t fg, uint8_t bg, bool dotted);
|
static void putGlyph(uint8_t row, uint8_t col, char ch, uint8_t fg, uint8_t bg, bool dotted);
|
||||||
|
|
@ -118,6 +131,9 @@ static int16_t gWinX;
|
||||||
static int16_t gWinY;
|
static int16_t gWinY;
|
||||||
static int16_t gWinW;
|
static int16_t gWinW;
|
||||||
static int16_t gWinH;
|
static int16_t gWinH;
|
||||||
|
// Where textPrint put the window's first line (screen row and column).
|
||||||
|
static uint8_t gWinTextRow;
|
||||||
|
static uint8_t gWinTextCol;
|
||||||
// Wrapped text on its way to the screen (print and display draw it at once).
|
// Wrapped text on its way to the screen (print and display draw it at once).
|
||||||
static char gLines[WINDOW_MAX_LINES][AGI_TEXT_COLS + 1u];
|
static char gLines[WINDOW_MAX_LINES][AGI_TEXT_COLS + 1u];
|
||||||
// Where the command line's echo draws next, as row * 40 + column.
|
// Where the command line's echo draws next, as row * 40 + column.
|
||||||
|
|
@ -247,20 +263,8 @@ static void drawMenuItems(uint8_t menu, uint8_t current) {
|
||||||
uint8_t width;
|
uint8_t width;
|
||||||
uint8_t col;
|
uint8_t col;
|
||||||
|
|
||||||
m = &gAgi->menus[menu];
|
m = &gAgi->menus[menu];
|
||||||
width = 0u;
|
col = menuItemsCol(m, &width);
|
||||||
for (i = 0u; i < m->itemCount; i++) {
|
|
||||||
uint8_t len;
|
|
||||||
|
|
||||||
len = (uint8_t)strlen(gAgi->menuItems[m->firstItem + i].text);
|
|
||||||
if (len > width) {
|
|
||||||
width = len;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
col = m->col;
|
|
||||||
if ((uint16_t)col + width + 1u > AGI_TEXT_COLS) {
|
|
||||||
col = (uint8_t)(AGI_TEXT_COLS - width - 1u);
|
|
||||||
}
|
|
||||||
drawBox((int16_t)((int16_t)col * AGI_CELL_PX - MENU_MARGIN), (int16_t)((int16_t)MENU_ITEM_ROW * AGI_CELL_PX - MENU_MARGIN),
|
drawBox((int16_t)((int16_t)col * AGI_CELL_PX - MENU_MARGIN), (int16_t)((int16_t)MENU_ITEM_ROW * AGI_CELL_PX - MENU_MARGIN),
|
||||||
(uint16_t)((uint16_t)width * AGI_CELL_PX + 2u * MENU_MARGIN), (uint16_t)((uint16_t)m->itemCount * AGI_CELL_PX + 2u * MENU_MARGIN),
|
(uint16_t)((uint16_t)width * AGI_CELL_PX + 2u * MENU_MARGIN), (uint16_t)((uint16_t)m->itemCount * AGI_CELL_PX + 2u * MENU_MARGIN),
|
||||||
MENU_FRAME);
|
MENU_FRAME);
|
||||||
|
|
@ -290,6 +294,63 @@ static void drawText(uint8_t row, uint8_t col, const char *text, uint8_t fg, uin
|
||||||
|
|
||||||
// One character of the command line's echo at gEchoPos, which moves on;
|
// One character of the command line's echo at gEchoPos, which moves on;
|
||||||
// a position off the screen draws nothing.
|
// a position off the screen draws nothing.
|
||||||
|
// The column a menu's items start in (under its title, moved left to fit)
|
||||||
|
// and their width.
|
||||||
|
static uint8_t menuItemsCol(const AgiMenuT *m, uint8_t *outWidth) {
|
||||||
|
uint8_t i;
|
||||||
|
uint8_t width;
|
||||||
|
uint8_t len;
|
||||||
|
uint8_t col;
|
||||||
|
|
||||||
|
width = 0u;
|
||||||
|
for (i = 0u; i < m->itemCount; i++) {
|
||||||
|
len = (uint8_t)strlen(gAgi->menuItems[m->firstItem + i].text);
|
||||||
|
if (len > width) {
|
||||||
|
width = len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
col = m->col;
|
||||||
|
if ((uint16_t)col + width + 1u > AGI_TEXT_COLS) {
|
||||||
|
col = (uint8_t)(AGI_TEXT_COLS - width - 1u);
|
||||||
|
}
|
||||||
|
*outWidth = width;
|
||||||
|
return col;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// What the pointer is over: a menu title (*menu) or an item of the open
|
||||||
|
// menu (*item), or neither.
|
||||||
|
static MenuHitE menuUnderPointer(uint8_t open, uint8_t *menu, uint8_t *item) {
|
||||||
|
const AgiMenuT *m;
|
||||||
|
int16_t col;
|
||||||
|
int16_t row;
|
||||||
|
uint8_t i;
|
||||||
|
uint8_t left;
|
||||||
|
uint8_t width;
|
||||||
|
|
||||||
|
if (!mouseOver(&col, &row)) {
|
||||||
|
return MENU_HIT_NONE;
|
||||||
|
}
|
||||||
|
if (row == (int16_t)MENU_ROW) {
|
||||||
|
for (i = 0u; i < gAgi->menuCount; i++) {
|
||||||
|
m = &gAgi->menus[i];
|
||||||
|
if (col >= (int16_t)m->col && col < (int16_t)(m->col + strlen(m->name))) {
|
||||||
|
*menu = i;
|
||||||
|
return MENU_HIT_TITLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MENU_HIT_BAR;
|
||||||
|
}
|
||||||
|
m = &gAgi->menus[open];
|
||||||
|
left = menuItemsCol(m, &width);
|
||||||
|
if (row >= (int16_t)MENU_ITEM_ROW && row < (int16_t)(MENU_ITEM_ROW + m->itemCount) && col >= (int16_t)left && col < (int16_t)(left + width)) {
|
||||||
|
*item = (uint8_t)(row - (int16_t)MENU_ITEM_ROW);
|
||||||
|
return MENU_HIT_ITEM;
|
||||||
|
}
|
||||||
|
return MENU_HIT_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static void echoCell(char ch, uint8_t fg, uint8_t bg) {
|
static void echoCell(char ch, uint8_t fg, uint8_t bg) {
|
||||||
if (gEchoPos >= 0 && gEchoPos < SCREEN_CELLS) {
|
if (gEchoPos >= 0 && gEchoPos < SCREEN_CELLS) {
|
||||||
putCell((uint8_t)(gEchoPos / AGI_TEXT_COLS), (uint8_t)(gEchoPos % AGI_TEXT_COLS), ch, fg, bg);
|
putCell((uint8_t)(gEchoPos / AGI_TEXT_COLS), (uint8_t)(gEchoPos % AGI_TEXT_COLS), ch, fg, bg);
|
||||||
|
|
@ -380,6 +441,13 @@ static void expandInto(const char *src, char *dst, uint16_t dstCap, uint16_t *po
|
||||||
|
|
||||||
// Save the stage under a window and draw its white box and red frame
|
// Save the stage under a window and draw its white box and red frame
|
||||||
// (x and w in AGI pixels, y and h in screen rows).
|
// (x and w in AGI pixels, y and h in screen rows).
|
||||||
|
// The row below, or the bottom row again at the bottom (display() writes
|
||||||
|
// over it rather than scrolling).
|
||||||
|
static uint8_t nextRow(uint8_t row) {
|
||||||
|
return (row + 1u < AGI_TEXT_ROWS) ? (uint8_t)(row + 1u) : row;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static void openWindow(int16_t x, int16_t y, int16_t w, int16_t h) {
|
static void openWindow(int16_t x, int16_t y, int16_t w, int16_t h) {
|
||||||
int16_t r;
|
int16_t r;
|
||||||
|
|
||||||
|
|
@ -637,47 +705,60 @@ void textCloseWindow(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// display(): text at a row and column in the current attribute, wrapped
|
// display(): text at a row and column in the current attribute. As
|
||||||
// like a window's at the right edge; further lines start at the same
|
// Sierra's, it is wrapped at the screen's width counted from the text, not
|
||||||
// column.
|
// from its column (King's Quest 1's 39-character copyright line at column
|
||||||
|
// 1 stays on one row); a wrapped line and a character past the right edge
|
||||||
|
// go on at column 0 of the next row, and the bottom row is written over
|
||||||
|
// rather than scrolled.
|
||||||
void textDisplay(uint8_t row, uint8_t col, const char *text) {
|
void textDisplay(uint8_t row, uint8_t col, const char *text) {
|
||||||
uint8_t fg;
|
uint8_t fg;
|
||||||
uint8_t bg;
|
uint8_t bg;
|
||||||
uint8_t count;
|
uint8_t count;
|
||||||
uint8_t width;
|
uint8_t width;
|
||||||
uint8_t i;
|
uint8_t i;
|
||||||
|
const char *c;
|
||||||
|
|
||||||
if (col >= AGI_TEXT_COLS) {
|
if (col >= AGI_TEXT_COLS || row >= AGI_TEXT_ROWS) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
attrColors(&fg, &bg);
|
attrColors(&fg, &bg);
|
||||||
count = wrapText(text, (uint8_t)(AGI_TEXT_COLS - col), gLines, WINDOW_MAX_LINES, &width);
|
count = wrapText(text, AGI_TEXT_COLS, gLines, WINDOW_MAX_LINES, &width);
|
||||||
for (i = 0u; i < count && row < AGI_TEXT_ROWS; i++) {
|
for (i = 0u; i < count; i++) {
|
||||||
drawText(row, col, gLines[i], fg, bg);
|
if (i > 0u) {
|
||||||
row++;
|
row = nextRow(row);
|
||||||
|
col = 0u;
|
||||||
|
}
|
||||||
|
for (c = gLines[i]; *c != '\0'; c++) {
|
||||||
|
putCell(row, col, *c, fg, bg);
|
||||||
|
col++;
|
||||||
|
if (col >= AGI_TEXT_COLS) {
|
||||||
|
row = nextRow(row);
|
||||||
|
col = 0u;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// The command line: the prompt (string 0), what has been typed and the
|
// The command line: the prompt (string 0), what has been typed and the
|
||||||
// cursor, on the input row.
|
// cursor, on the input row, while input is on (off, nothing draws there).
|
||||||
void textDrawInputLine(void) {
|
void textDrawInputLine(void) {
|
||||||
|
char prompt[AGI_TEXT_COLS + 1u];
|
||||||
uint8_t fg;
|
uint8_t fg;
|
||||||
uint8_t bg;
|
uint8_t bg;
|
||||||
uint8_t col;
|
uint8_t col;
|
||||||
uint8_t i;
|
uint8_t i;
|
||||||
|
|
||||||
if (gAgi->textMode) {
|
if (gAgi->textMode || !gAgi->inputOn) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
attrColors(&fg, &bg);
|
attrColors(&fg, &bg);
|
||||||
jlFillRect(jlStageGet(), 0, (int16_t)((int16_t)gAgi->inputRow * AGI_CELL_PX), (uint16_t)(AGI_TEXT_COLS * AGI_CELL_PX), AGI_CELL_PX, bg);
|
jlFillRect(jlStageGet(), 0, (int16_t)((int16_t)gAgi->inputRow * AGI_CELL_PX), (uint16_t)(AGI_TEXT_COLS * AGI_CELL_PX), AGI_CELL_PX, bg);
|
||||||
if (!gAgi->inputOn) {
|
(void)textPrompt(prompt);
|
||||||
return;
|
|
||||||
}
|
|
||||||
col = 0u;
|
col = 0u;
|
||||||
for (i = 0u; gAgi->strings[0][i] != '\0' && col < AGI_TEXT_COLS; i++) {
|
for (i = 0u; prompt[i] != '\0' && col < AGI_TEXT_COLS; i++) {
|
||||||
putCell(gAgi->inputRow, col++, gAgi->strings[0][i], fg, bg);
|
putCell(gAgi->inputRow, col++, prompt[i], fg, bg);
|
||||||
}
|
}
|
||||||
for (i = 0u; i < gAgi->inputLen && col < AGI_TEXT_COLS; i++) {
|
for (i = 0u; i < gAgi->inputLen && col < AGI_TEXT_COLS; i++) {
|
||||||
putCell(gAgi->inputRow, col++, gAgi->inputLine[i], fg, bg);
|
putCell(gAgi->inputRow, col++, gAgi->inputLine[i], fg, bg);
|
||||||
|
|
@ -717,6 +798,7 @@ void textDrawStatus(void) {
|
||||||
// out can run back over the prompt); otherwise the echo starts from
|
// out can run back over the prompt); otherwise the echo starts from
|
||||||
// the end of what is typed.
|
// the end of what is typed.
|
||||||
void textEchoInput(uint8_t oldLen) {
|
void textEchoInput(uint8_t oldLen) {
|
||||||
|
char prompt[AGI_TEXT_COLS + 1u];
|
||||||
uint8_t fg;
|
uint8_t fg;
|
||||||
uint8_t bg;
|
uint8_t bg;
|
||||||
bool cursor;
|
bool cursor;
|
||||||
|
|
@ -727,7 +809,7 @@ void textEchoInput(uint8_t oldLen) {
|
||||||
attrColors(&fg, &bg);
|
attrColors(&fg, &bg);
|
||||||
cursor = (gAgi->cursorChar != AGI_NO_CURSOR);
|
cursor = (gAgi->cursorChar != AGI_NO_CURSOR);
|
||||||
if (!agiQuirk(AGI_QUIRK_ECHO_STEPS_BACK)) {
|
if (!agiQuirk(AGI_QUIRK_ECHO_STEPS_BACK)) {
|
||||||
gEchoPos = (int16_t)((int16_t)gAgi->inputRow * AGI_TEXT_COLS + (int16_t)strlen(gAgi->strings[0]) + oldLen + (cursor ? 1 : 0));
|
gEchoPos = (int16_t)((int16_t)gAgi->inputRow * AGI_TEXT_COLS + (int16_t)textPrompt(prompt) + oldLen + (cursor ? 1 : 0));
|
||||||
}
|
}
|
||||||
if (gAgi->inputLen > oldLen) {
|
if (gAgi->inputLen > oldLen) {
|
||||||
if (cursor) {
|
if (cursor) {
|
||||||
|
|
@ -880,8 +962,15 @@ void textMenuAddItem(const char *name, uint8_t controller) {
|
||||||
uint8_t textMenuInput(void) {
|
uint8_t textMenuInput(void) {
|
||||||
uint8_t menu;
|
uint8_t menu;
|
||||||
uint8_t item;
|
uint8_t item;
|
||||||
|
uint8_t hitMenu;
|
||||||
|
uint8_t hitItem;
|
||||||
|
int16_t lastCol;
|
||||||
|
int16_t lastRow;
|
||||||
|
int16_t col;
|
||||||
|
int16_t row;
|
||||||
uint16_t key;
|
uint16_t key;
|
||||||
uint8_t result;
|
uint8_t result;
|
||||||
|
MenuHitE hit;
|
||||||
jlSurfaceT *stage;
|
jlSurfaceT *stage;
|
||||||
|
|
||||||
if (gAgi->menuCount == 0u) {
|
if (gAgi->menuCount == 0u) {
|
||||||
|
|
@ -900,13 +989,43 @@ uint8_t textMenuInput(void) {
|
||||||
if (menu >= gAgi->menuCount) {
|
if (menu >= gAgi->menuCount) {
|
||||||
menu = 0u;
|
menu = 0u;
|
||||||
}
|
}
|
||||||
|
lastCol = POINTER_NOWHERE;
|
||||||
|
lastRow = POINTER_NOWHERE;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
if (gWinSave != NULL) {
|
if (gWinSave != NULL) {
|
||||||
jlSurfaceCopy(stage, gWinSave);
|
jlSurfaceCopy(stage, gWinSave);
|
||||||
}
|
}
|
||||||
(void)drawMenuBar(menu);
|
(void)drawMenuBar(menu);
|
||||||
drawMenuItems(menu, item);
|
drawMenuItems(menu, item);
|
||||||
key = inputWaitKey();
|
// A key, or the pointer moving to another cell: over a title it
|
||||||
|
// opens that menu, over an item it lights it. A click on an item
|
||||||
|
// chooses it, on the bar does nothing more, elsewhere closes.
|
||||||
|
hit = MENU_HIT_NONE;
|
||||||
|
do {
|
||||||
|
key = inputPollKey();
|
||||||
|
hitMenu = menu;
|
||||||
|
hitItem = item;
|
||||||
|
hit = menuUnderPointer(menu, &hitMenu, &hitItem);
|
||||||
|
if (mouseOver(&col, &row) && (col != lastCol || row != lastRow)) {
|
||||||
|
lastCol = col;
|
||||||
|
lastRow = row;
|
||||||
|
if (hit == MENU_HIT_TITLE && hitMenu != menu) {
|
||||||
|
menu = hitMenu;
|
||||||
|
item = 0u;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (hit == MENU_HIT_ITEM && hitItem != item) {
|
||||||
|
item = hitItem;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (key == AGI_KEY_NONE);
|
||||||
|
if (key == AGI_KEY_CLICK) {
|
||||||
|
if (hit == MENU_HIT_NONE) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
key = (hit == MENU_HIT_ITEM) ? AGI_KEY_ENTER : AGI_KEY_NONE;
|
||||||
|
}
|
||||||
if (key == AGI_KEY_ESCAPE) {
|
if (key == AGI_KEY_ESCAPE) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -975,6 +1094,17 @@ void textPrint(const char *text, int16_t row, int16_t col, uint8_t width) {
|
||||||
for (i = 0u; i < count; i++) {
|
for (i = 0u; i < count; i++) {
|
||||||
drawText((uint8_t)(row + i), (uint8_t)col, gLines[i], WINDOW_TEXT_FG, WINDOW_BG);
|
drawText((uint8_t)(row + i), (uint8_t)col, gLines[i], WINDOW_TEXT_FG, WINDOW_BG);
|
||||||
}
|
}
|
||||||
|
gWinTextRow = (uint8_t)row;
|
||||||
|
gWinTextCol = (uint8_t)col;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The command line's prompt into out (AGI_TEXT_COLS + 1 bytes): string 0
|
||||||
|
// with its %-codes expanded, as Sierra's draws it (Gold Rush's is "%g69",
|
||||||
|
// logic 0's ">"). Its length.
|
||||||
|
uint8_t textPrompt(char *out) {
|
||||||
|
textExpand(gAgi->strings[0], out, AGI_TEXT_COLS + 1u);
|
||||||
|
return (uint8_t)strlen(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1027,3 +1157,13 @@ void textUpdateStatus(void) {
|
||||||
textDrawStatus();
|
textDrawStatus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Text over the open window, at a line and column of what textPrint put
|
||||||
|
// there (a pointer moving down a list).
|
||||||
|
void textWindowPut(uint8_t line, uint8_t col, const char *text) {
|
||||||
|
if (!gAgi->windowOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
drawText((uint8_t)(gWinTextRow + line), (uint8_t)(gWinTextCol + col), text, WINDOW_TEXT_FG, WINDOW_BG);
|
||||||
|
}
|
||||||
|
|
|
||||||
216
agiVm.c
216
agiVm.c
|
|
@ -40,7 +40,6 @@
|
||||||
#define CODE_OR 0xFCu
|
#define CODE_OR 0xFCu
|
||||||
#define MAX_ARGS AGI_SCRIPT_ARGS
|
#define MAX_ARGS AGI_SCRIPT_ARGS
|
||||||
#define MESSAGE_MAX 512u
|
#define MESSAGE_MAX 512u
|
||||||
#define DISTANCE_FAR 255u
|
|
||||||
#define DISTANCE_MAX 254u
|
#define DISTANCE_MAX 254u
|
||||||
// Logics calling logics nest only a few deep; a deeper chain is a logic
|
// Logics calling logics nest only a few deep; a deeper chain is a logic
|
||||||
// calling itself round in a loop (KQ3's room 43 entered the wrong way),
|
// calling itself round in a loop (KQ3's room 43 entered the wrong way),
|
||||||
|
|
@ -218,7 +217,32 @@
|
||||||
#define CMD_DIV_N 0xA7u
|
#define CMD_DIV_N 0xA7u
|
||||||
#define CMD_DIV_V 0xA8u
|
#define CMD_DIV_V 0xA8u
|
||||||
#define CMD_CLOSE_WINDOW 0xA9u
|
#define CMD_CLOSE_WINDOW 0xA9u
|
||||||
#define CMD_COUNT 0xAAu
|
// The commands of 2.425 and later, and AGI 3's (ScummVM's opcodes.cpp).
|
||||||
|
#define CMD_SET_SIMPLE 0xAAu
|
||||||
|
#define CMD_PUSH_SCRIPT 0xABu
|
||||||
|
#define CMD_POP_SCRIPT 0xACu
|
||||||
|
#define CMD_HOLD_KEY 0xADu
|
||||||
|
#define CMD_SET_PRI_BASE 0xAEu
|
||||||
|
#define CMD_DISCARD_SOUND 0xAFu
|
||||||
|
#define CMD_HIDE_MOUSE 0xB0u
|
||||||
|
#define CMD_ALLOW_MENU 0xB1u
|
||||||
|
#define CMD_SHOW_MOUSE 0xB2u
|
||||||
|
#define CMD_FENCE_MOUSE 0xB3u
|
||||||
|
#define CMD_GET_MSE_POSN 0xB4u
|
||||||
|
#define CMD_RELEASE_KEY 0xB5u
|
||||||
|
#define CMD_ADJ_EGO_MOVE_TO_XY 0xB6u
|
||||||
|
#define CMD_COUNT 0xB7u
|
||||||
|
|
||||||
|
// The interpreter versions commands differ by (agiRes.c's numbering:
|
||||||
|
// 2.917 is 2917, 3.002.149 is 3149), after ScummVM's opcodes.cpp and
|
||||||
|
// op_cmd.cpp.
|
||||||
|
#define VER_V2_FIRST 2000u // the v2 command set from here
|
||||||
|
#define VER_2089 2089u // quit takes no argument; print.at four from here
|
||||||
|
#define VER_2425 2425u // set.simple, push/pop.script, hold.key, set.pri.base from here ...
|
||||||
|
#define VER_2440 2440u // ... but 2.440 lacks them
|
||||||
|
#define VER_PRI_BASE_AGAIN 2936u // set.pri.base, missing after 2.425, again from here
|
||||||
|
#define VER_3086 AGI_VERSION_KQ4 // hide.mouse and hold.key take an argument
|
||||||
|
#define VER_3098 3098u // allow.menu and release.key from here
|
||||||
|
|
||||||
// Test commands (specification 6.2).
|
// Test commands (specification 6.2).
|
||||||
#define TEST_EQUALN 0x01u
|
#define TEST_EQUALN 0x01u
|
||||||
|
|
@ -239,7 +263,8 @@
|
||||||
#define TEST_OBJ_IN_BOX 0x10u
|
#define TEST_OBJ_IN_BOX 0x10u
|
||||||
#define TEST_CENTER_POSN 0x11u
|
#define TEST_CENTER_POSN 0x11u
|
||||||
#define TEST_RIGHT_POSN 0x12u
|
#define TEST_RIGHT_POSN 0x12u
|
||||||
#define TEST_COUNT 0x13u
|
#define TEST_IN_MOTION_MOUSE 0x13u // AGI 3: ego walks to a click
|
||||||
|
#define TEST_COUNT 0x14u
|
||||||
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|
@ -248,11 +273,20 @@ typedef struct {
|
||||||
uint16_t pc;
|
uint16_t pc;
|
||||||
uint8_t logic;
|
uint8_t logic;
|
||||||
bool done;
|
bool done;
|
||||||
|
bool acted; // a command ran since the last backward jump
|
||||||
} VmFrameT;
|
} VmFrameT;
|
||||||
|
|
||||||
|
#if defined(AGI_TRACE)
|
||||||
|
AgiTraceFnT gAgiTrace = NULL;
|
||||||
|
#define TRACE(_f, _pc, _op, _args, _count, _result) do { if (gAgiTrace != NULL) { gAgiTrace((_f)->logic, (_pc), (_op), (_args), (_count), (_result)); } } while (0)
|
||||||
|
#else
|
||||||
|
#define TRACE(_f, _pc, _op, _args, _count, _result) do { (void)(_pc); } while (0)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
// ----- Prototypes -----
|
// ----- Prototypes -----
|
||||||
|
|
||||||
|
static uint8_t actionArgs(uint8_t op);
|
||||||
static void cmdMisc(VmFrameT *f, uint8_t op, const uint8_t *a);
|
static void cmdMisc(VmFrameT *f, uint8_t op, const uint8_t *a);
|
||||||
static void cmdObject(VmFrameT *f, uint8_t op, const uint8_t *a);
|
static void cmdObject(VmFrameT *f, uint8_t op, const uint8_t *a);
|
||||||
static void cmdText(VmFrameT *f, uint8_t op, const uint8_t *a);
|
static void cmdText(VmFrameT *f, uint8_t op, const uint8_t *a);
|
||||||
|
|
@ -263,6 +297,7 @@ static void decryptMessages(uint8_t *raw, uint16_t length);
|
||||||
static bool evalCondition(VmFrameT *f);
|
static bool evalCondition(VmFrameT *f);
|
||||||
static bool evalTest(VmFrameT *f, uint8_t test);
|
static bool evalTest(VmFrameT *f, uint8_t test);
|
||||||
static void execute(VmFrameT *f);
|
static void execute(VmFrameT *f);
|
||||||
|
static bool has2425Commands(void);
|
||||||
static AgiObjectT *object(uint8_t n);
|
static AgiObjectT *object(uint8_t n);
|
||||||
static bool posnTest(uint8_t test, const uint8_t *a);
|
static bool posnTest(uint8_t test, const uint8_t *a);
|
||||||
static void printMessage(const char *msg, int16_t row, int16_t col, uint8_t width);
|
static void printMessage(const char *msg, int16_t row, int16_t col, uint8_t width);
|
||||||
|
|
@ -287,12 +322,13 @@ static const uint8_t kActionArgs[CMD_COUNT] = {
|
||||||
0, 0, 2, 5, 2, 1, 2, 0, 0, 3, 7, 7, 0, 0, 0, 0, // 0x70
|
0, 0, 2, 5, 2, 1, 2, 0, 0, 3, 7, 7, 0, 0, 0, 0, // 0x70
|
||||||
0, 1, 3, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, // 0x80
|
0, 1, 3, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, // 0x80
|
||||||
1, 0, 0, 3, 3, 0, 3, 4, 4, 1, 5, 2, 1, 2, 0, 1, // 0x90
|
1, 0, 0, 3, 3, 0, 3, 4, 4, 1, 5, 2, 1, 2, 0, 1, // 0x90
|
||||||
1, 0, 1, 0, 0, 2, 2, 2, 2, 0 // 0xA0
|
1, 0, 1, 0, 0, 2, 2, 2, 2, 0, 1, 0, 0, 0, 1, 1, // 0xA0
|
||||||
|
0, 1, 0, 4, 2, 0, 0 // 0xB0
|
||||||
};
|
};
|
||||||
|
|
||||||
// Argument bytes per test command (said is variable).
|
// Argument bytes per test command (said is variable).
|
||||||
static const uint8_t kTestArgs[TEST_COUNT] = {
|
static const uint8_t kTestArgs[TEST_COUNT] = {
|
||||||
0, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 5, 1, 0, 0, 2, 5, 5, 5
|
0, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 5, 1, 0, 0, 2, 5, 5, 5, 0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -304,8 +340,32 @@ static uint8_t gCallDepth;
|
||||||
|
|
||||||
// ----- Internal helpers (alphabetical) -----
|
// ----- Internal helpers (alphabetical) -----
|
||||||
|
|
||||||
// The rest: system, screen, menu and arithmetic commands (0x7C..0xA9).
|
// A command's argument bytes in this game's interpreter: kActionArgs, but
|
||||||
|
// quit takes none on 2.089, print.at three below it, and King's Quest IV's
|
||||||
|
// hide.mouse and hold.key one.
|
||||||
|
static uint8_t actionArgs(uint8_t op) {
|
||||||
|
uint16_t v;
|
||||||
|
|
||||||
|
v = gAgi->game.interpreterVersion;
|
||||||
|
if (op == CMD_QUIT && v == VER_2089) {
|
||||||
|
return 0u;
|
||||||
|
}
|
||||||
|
if ((op == CMD_PRINT_AT || op == CMD_PRINT_AT_V) && v >= VER_V2_FIRST && v < VER_2089) {
|
||||||
|
return 3u;
|
||||||
|
}
|
||||||
|
if ((op == CMD_HIDE_MOUSE || op == CMD_HOLD_KEY) && v == VER_3086) {
|
||||||
|
return 1u;
|
||||||
|
}
|
||||||
|
return kActionArgs[op];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The rest: system, screen, menu, arithmetic, and the later commands
|
||||||
|
// (0x7C..0xB6).
|
||||||
static void cmdMisc(VmFrameT *f, uint8_t op, const uint8_t *a) {
|
static void cmdMisc(VmFrameT *f, uint8_t op, const uint8_t *a) {
|
||||||
|
uint16_t span;
|
||||||
|
uint8_t r;
|
||||||
|
|
||||||
switch (op) {
|
switch (op) {
|
||||||
case CMD_STATUS:
|
case CMD_STATUS:
|
||||||
textInventory();
|
textInventory();
|
||||||
|
|
@ -328,18 +388,22 @@ static void cmdMisc(VmFrameT *f, uint8_t op, const uint8_t *a) {
|
||||||
textShowObject(gAgi->vars[a[0]]);
|
textShowObject(gAgi->vars[a[0]]);
|
||||||
break;
|
break;
|
||||||
case CMD_RANDOM:
|
case CMD_RANDOM:
|
||||||
if (a[1] >= a[0]) {
|
// Sierra's: the number modulo the 16-bit span, so a maximum
|
||||||
gAgi->vars[a[2]] = (uint8_t)(a[0] + jlRandomRange((uint16_t)(a[1] - a[0] + 1u)));
|
// below the minimum takes the number as it is; so does the one
|
||||||
} else {
|
// span of 0 (a maximum one below), which Sierra's divides by.
|
||||||
gAgi->vars[a[2]] = a[0];
|
span = (uint16_t)(a[1] - a[0] + 1u);
|
||||||
}
|
r = agiRandom();
|
||||||
|
gAgi->vars[a[2]] = (uint8_t)(a[0] + ((span == 0u) ? r : r % span));
|
||||||
break;
|
break;
|
||||||
case CMD_PROGRAM_CONTROL:
|
case CMD_PROGRAM_CONTROL:
|
||||||
gAgi->playerControl = false;
|
gAgi->playerControl = false;
|
||||||
break;
|
break;
|
||||||
case CMD_PLAYER_CONTROL:
|
case CMD_PLAYER_CONTROL:
|
||||||
gAgi->playerControl = true;
|
// A walk to a click goes on (as ScummVM keeps it).
|
||||||
gAgi->objects[AGI_EGO].motion = AGI_MOTION_NORMAL;
|
gAgi->playerControl = true;
|
||||||
|
if (gAgi->objects[AGI_EGO].motion != AGI_MOTION_MOUSE) {
|
||||||
|
gAgi->objects[AGI_EGO].motion = AGI_MOTION_NORMAL;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case CMD_OBJ_STATUS_V:
|
case CMD_OBJ_STATUS_V:
|
||||||
agiObjectStatus(gAgi->vars[a[0]]);
|
agiObjectStatus(gAgi->vars[a[0]]);
|
||||||
|
|
@ -443,10 +507,52 @@ static void cmdMisc(VmFrameT *f, uint8_t op, const uint8_t *a) {
|
||||||
case CMD_CLOSE_WINDOW:
|
case CMD_CLOSE_WINDOW:
|
||||||
textCloseWindow();
|
textCloseWindow();
|
||||||
break;
|
break;
|
||||||
|
case CMD_SET_SIMPLE:
|
||||||
|
// Save and restore without asking, under string a[0].
|
||||||
|
if (has2425Commands()) {
|
||||||
|
strncpy(gAgi->simpleSaveDesc, a[0] < AGI_MAX_STRINGS ? gAgi->strings[a[0]] : "", AGI_STRING_LEN);
|
||||||
|
gAgi->simpleSaveDesc[AGI_STRING_LEN] = '\0';
|
||||||
|
gAgi->simpleSaveOn = gAgi->simpleSaveDesc[0] != '\0';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case CMD_HOLD_KEY:
|
||||||
|
if (has2425Commands()) {
|
||||||
|
gAgi->holdKey = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case CMD_RELEASE_KEY:
|
||||||
|
if (gAgi->game.interpreterVersion >= VER_3098) {
|
||||||
|
gAgi->holdKey = false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case CMD_SET_PRI_BASE:
|
||||||
|
if (gAgi->game.interpreterVersion == VER_2425 || gAgi->game.interpreterVersion >= VER_PRI_BASE_AGAIN) {
|
||||||
|
agiPrioritySetBase(a[0]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case CMD_ALLOW_MENU:
|
||||||
|
if (gAgi->game.interpreterVersion >= VER_3098) {
|
||||||
|
gAgi->menuDenied = a[0] == 0u;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case CMD_HIDE_MOUSE:
|
||||||
|
gAgi->mouseHidden = true;
|
||||||
|
break;
|
||||||
|
case CMD_SHOW_MOUSE:
|
||||||
|
gAgi->mouseHidden = false;
|
||||||
|
break;
|
||||||
|
case CMD_FENCE_MOUSE:
|
||||||
|
memcpy(gAgi->mouseFence, a, sizeof(gAgi->mouseFence));
|
||||||
|
break;
|
||||||
|
case CMD_GET_MSE_POSN:
|
||||||
|
mousePosition(&gAgi->vars[a[0]], &gAgi->vars[a[1]]);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// init.disk, init.joy, toggle.monitor, script.size, set.game.id,
|
// init.disk, init.joy, toggle.monitor, script.size, set.game.id,
|
||||||
// log, trace.on, trace.info, set.upper.left, open.dialogue and
|
// log, trace.on, trace.info, set.upper.left, open.dialogue,
|
||||||
// close.dialogue have nothing to do here.
|
// close.dialogue, push.script and pop.script (debugging aids),
|
||||||
|
// discard.sound (the Apple IIgs's) and adj.ego.move.to.x.y
|
||||||
|
// without arguments (no game calls it) have nothing to do here.
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -480,6 +586,12 @@ static void cmdObject(VmFrameT *f, uint8_t op, const uint8_t *a) {
|
||||||
break;
|
break;
|
||||||
case CMD_DRAW:
|
case CMD_DRAW:
|
||||||
if (o != NULL && (o->flags & OBJ_DRAWN) == 0u && o->view != NULL) {
|
if (o != NULL && (o->flags & OBJ_DRAWN) == 0u && o->view != NULL) {
|
||||||
|
// v3 sets the loop and cel again first, so a view set since
|
||||||
|
// draws as its own loop and cel allow.
|
||||||
|
if (gAgi->game.interpreterVersion >= AGI_VERSION_V3_FIRST) {
|
||||||
|
objSetLoop(a[0], o->loop);
|
||||||
|
objSetCel(a[0], o->cel);
|
||||||
|
}
|
||||||
objFixPosition(a[0]);
|
objFixPosition(a[0]);
|
||||||
o->prevX = o->x;
|
o->prevX = o->x;
|
||||||
o->prevY = o->y;
|
o->prevY = o->y;
|
||||||
|
|
@ -631,7 +743,7 @@ static void cmdObject(VmFrameT *f, uint8_t op, const uint8_t *a) {
|
||||||
const AgiObjectT *p = object(a[1]);
|
const AgiObjectT *p = object(a[1]);
|
||||||
uint16_t d;
|
uint16_t d;
|
||||||
|
|
||||||
d = DISTANCE_FAR;
|
d = AGI_DISTANCE_FAR;
|
||||||
if (o != NULL && p != NULL && (o->flags & OBJ_DRAWN) != 0u && (p->flags & OBJ_DRAWN) != 0u) {
|
if (o != NULL && p != NULL && (o->flags & OBJ_DRAWN) != 0u && (p->flags & OBJ_DRAWN) != 0u) {
|
||||||
// From the middle of one baseline to the middle of the
|
// From the middle of one baseline to the middle of the
|
||||||
// other, as Sierra's interpreter measures it.
|
// other, as Sierra's interpreter measures it.
|
||||||
|
|
@ -643,7 +755,7 @@ static void cmdObject(VmFrameT *f, uint8_t op, const uint8_t *a) {
|
||||||
d = DISTANCE_MAX;
|
d = DISTANCE_MAX;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
gAgi->vars[a[2]] = (uint8_t)d;
|
gAgi->vars[a[2]] = agiPatchDistance(a[2], (uint8_t)d);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case CMD_STOP_CYCLING:
|
case CMD_STOP_CYCLING:
|
||||||
|
|
@ -666,10 +778,10 @@ static void cmdObject(VmFrameT *f, uint8_t op, const uint8_t *a) {
|
||||||
case CMD_END_OF_LOOP:
|
case CMD_END_OF_LOOP:
|
||||||
case CMD_REVERSE_LOOP:
|
case CMD_REVERSE_LOOP:
|
||||||
if (o != NULL) {
|
if (o != NULL) {
|
||||||
o->cycleMode = (op == CMD_END_OF_LOOP) ? AGI_CYCLE_END_LOOP : AGI_CYCLE_REVERSE_LOOP;
|
o->cycleMode = (op == CMD_END_OF_LOOP) ? AGI_CYCLE_END_LOOP : AGI_CYCLE_REVERSE_LOOP;
|
||||||
o->loopFlag = a[1];
|
o->parm[PARM_LOOP_FLAG] = a[1];
|
||||||
gAgi->flags[a[1]] = 0u;
|
gAgi->flags[a[1]] = 0u;
|
||||||
o->flags = (uint16_t)(o->flags | OBJ_CYCLING);
|
o->flags = (uint16_t)(o->flags | OBJ_CYCLING);
|
||||||
if ((o->flags & OBJ_DRAWN) != 0u) {
|
if ((o->flags & OBJ_DRAWN) != 0u) {
|
||||||
o->flags = (uint16_t)(o->flags | OBJ_SKIP_CYCLE);
|
o->flags = (uint16_t)(o->flags | OBJ_SKIP_CYCLE);
|
||||||
}
|
}
|
||||||
|
|
@ -723,17 +835,13 @@ static void cmdObject(VmFrameT *f, uint8_t op, const uint8_t *a) {
|
||||||
break;
|
break;
|
||||||
case CMD_FOLLOW_EGO:
|
case CMD_FOLLOW_EGO:
|
||||||
if (o != NULL) {
|
if (o != NULL) {
|
||||||
o->motion = AGI_MOTION_FOLLOW;
|
objFollowEgo(a[0], a[1], a[2]);
|
||||||
o->randomSteps = 0u;
|
|
||||||
o->followDistance = (a[1] > o->stepSize) ? a[1] : o->stepSize;
|
|
||||||
o->motionFlag = a[2];
|
|
||||||
gAgi->flags[a[2]] = 0u;
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case CMD_WANDER:
|
case CMD_WANDER:
|
||||||
|
// The count is what the shared bytes hold, as in Sierra's.
|
||||||
if (o != NULL) {
|
if (o != NULL) {
|
||||||
o->motion = AGI_MOTION_WANDER;
|
o->motion = AGI_MOTION_WANDER;
|
||||||
o->randomSteps = 0u;
|
|
||||||
if (a[0] == AGI_EGO) {
|
if (a[0] == AGI_EGO) {
|
||||||
gAgi->playerControl = false;
|
gAgi->playerControl = false;
|
||||||
}
|
}
|
||||||
|
|
@ -885,8 +993,9 @@ static void cmdText(VmFrameT *f, uint8_t op, const uint8_t *a) {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case CMD_PREVENT_INPUT:
|
case CMD_PREVENT_INPUT:
|
||||||
|
// The line goes black; nothing redraws it until accept.input.
|
||||||
gAgi->inputOn = false;
|
gAgi->inputOn = false;
|
||||||
textDrawInputLine();
|
textClearLines(gAgi->inputRow, gAgi->inputRow, AGI_COLOR_BLACK);
|
||||||
break;
|
break;
|
||||||
case CMD_ACCEPT_INPUT:
|
case CMD_ACCEPT_INPUT:
|
||||||
gAgi->inputOn = true;
|
gAgi->inputOn = true;
|
||||||
|
|
@ -1014,7 +1123,8 @@ static void cmdVars(VmFrameT *f, uint8_t op, const uint8_t *a) {
|
||||||
saveScriptAdd(op, v[a[0]]);
|
saveScriptAdd(op, v[a[0]]);
|
||||||
break;
|
break;
|
||||||
case CMD_SHOW_PIC:
|
case CMD_SHOW_PIC:
|
||||||
gAgi->windowOpen = false;
|
gAgi->flags[FLAG_WINDOW_STAYS] = 0u;
|
||||||
|
gAgi->windowOpen = false;
|
||||||
gfxShowPic();
|
gfxShowPic();
|
||||||
break;
|
break;
|
||||||
case CMD_DISCARD_PIC:
|
case CMD_DISCARD_PIC:
|
||||||
|
|
@ -1171,6 +1281,7 @@ static bool evalTest(VmFrameT *f, uint8_t test) {
|
||||||
case TEST_OBJ_IN_ROOM: return gAgi->itemRoom[a[0]] == gAgi->vars[a[1]];
|
case TEST_OBJ_IN_ROOM: return gAgi->itemRoom[a[0]] == gAgi->vars[a[1]];
|
||||||
case TEST_CONTROLLER: return gAgi->controllers[a[0]] != 0u;
|
case TEST_CONTROLLER: return gAgi->controllers[a[0]] != 0u;
|
||||||
case TEST_HAVE_KEY: return inputHaveKey();
|
case TEST_HAVE_KEY: return inputHaveKey();
|
||||||
|
case TEST_IN_MOTION_MOUSE: return gAgi->objects[AGI_EGO].motion == AGI_MOTION_MOUSE;
|
||||||
case TEST_COMPARE_STRINGS:
|
case TEST_COMPARE_STRINGS:
|
||||||
return a[0] < AGI_MAX_STRINGS && a[1] < AGI_MAX_STRINGS && compareStrings(gAgi->strings[a[0]], gAgi->strings[a[1]]);
|
return a[0] < AGI_MAX_STRINGS && a[1] < AGI_MAX_STRINGS && compareStrings(gAgi->strings[a[0]], gAgi->strings[a[1]]);
|
||||||
default: return posnTest(test, a);
|
default: return posnTest(test, a);
|
||||||
|
|
@ -1182,10 +1293,13 @@ static bool evalTest(VmFrameT *f, uint8_t test) {
|
||||||
static void execute(VmFrameT *f) {
|
static void execute(VmFrameT *f) {
|
||||||
uint8_t op;
|
uint8_t op;
|
||||||
uint8_t a[MAX_ARGS];
|
uint8_t a[MAX_ARGS];
|
||||||
|
uint8_t count;
|
||||||
uint8_t i;
|
uint8_t i;
|
||||||
uint16_t skip;
|
uint16_t skip;
|
||||||
|
uint16_t at;
|
||||||
|
|
||||||
while (!f->done && !gAgi->abortLogic) {
|
while (!f->done && !gAgi->abortLogic) {
|
||||||
|
at = f->pc;
|
||||||
op = readByte(f);
|
op = readByte(f);
|
||||||
if (f->done) {
|
if (f->done) {
|
||||||
break;
|
break;
|
||||||
|
|
@ -1195,6 +1309,7 @@ static void execute(VmFrameT *f) {
|
||||||
|
|
||||||
cond = evalCondition(f);
|
cond = evalCondition(f);
|
||||||
skip = readWord(f);
|
skip = readWord(f);
|
||||||
|
TRACE(f, at, op, NULL, 0u, cond ? 1 : 0);
|
||||||
if (!cond && !f->done) {
|
if (!cond && !f->done) {
|
||||||
f->pc = (uint16_t)(f->pc + skip);
|
f->pc = (uint16_t)(f->pc + skip);
|
||||||
}
|
}
|
||||||
|
|
@ -1203,16 +1318,32 @@ static void execute(VmFrameT *f) {
|
||||||
if (op == CODE_GOTO) {
|
if (op == CODE_GOTO) {
|
||||||
skip = readWord(f);
|
skip = readWord(f);
|
||||||
f->pc = (uint16_t)(f->pc + skip);
|
f->pc = (uint16_t)(f->pc + skip);
|
||||||
|
// A loop of tests alone waits on what only time, keys or sound
|
||||||
|
// change (Manhunter 1's "v11 = 0; while (v49 > v11)"); in
|
||||||
|
// Sierra's interpreter the timer interrupt moves them on as it
|
||||||
|
// spins, here a frame passes per turn.
|
||||||
|
if ((int16_t)skip < 0) {
|
||||||
|
if (!f->acted) {
|
||||||
|
agiIdle();
|
||||||
|
}
|
||||||
|
f->acted = false;
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (op >= CMD_COUNT || kActionArgs[op] > MAX_ARGS) {
|
if (op >= CMD_COUNT || kActionArgs[op] > MAX_ARGS) {
|
||||||
jlLogF("logic %u: unknown command 0x%02X at %u", (unsigned)f->logic, (unsigned)op, (unsigned)(f->pc - 1u));
|
jlLogF("logic %u: unknown command 0x%02X at %u", (unsigned)f->logic, (unsigned)op, (unsigned)(f->pc - 1u));
|
||||||
|
TRACE(f, at, op, NULL, 0u, -1);
|
||||||
f->done = true;
|
f->done = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
for (i = 0u; i < kActionArgs[op]; i++) {
|
// Arguments a version leaves out read as 0 (quit on 2.089).
|
||||||
|
count = actionArgs(op);
|
||||||
|
memset(a, 0, sizeof(a));
|
||||||
|
for (i = 0u; i < count; i++) {
|
||||||
a[i] = readByte(f);
|
a[i] = readByte(f);
|
||||||
}
|
}
|
||||||
|
TRACE(f, at, op, a, count, -1);
|
||||||
|
f->acted = true;
|
||||||
if (op <= CMD_SHOW_PRI_SCREEN) {
|
if (op <= CMD_SHOW_PRI_SCREEN) {
|
||||||
cmdVars(f, op, a);
|
cmdVars(f, op, a);
|
||||||
} else if (op <= CMD_UNBLOCK) {
|
} else if (op <= CMD_UNBLOCK) {
|
||||||
|
|
@ -1226,6 +1357,13 @@ static void execute(VmFrameT *f) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// set.simple, push.script, pop.script, hold.key: 2.425 and later, but not
|
||||||
|
// 2.440 (ScummVM's op_cmd.cpp).
|
||||||
|
static bool has2425Commands(void) {
|
||||||
|
return gAgi->game.interpreterVersion >= VER_2425 && gAgi->game.interpreterVersion != VER_2440;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static AgiObjectT *object(uint8_t n) {
|
static AgiObjectT *object(uint8_t n) {
|
||||||
if (n >= gAgi->objectCount) {
|
if (n >= gAgi->objectCount) {
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
@ -1267,10 +1405,14 @@ static void printMessage(const char *msg, int16_t row, int16_t col, uint8_t widt
|
||||||
|
|
||||||
textExpand(msg, text, sizeof(text));
|
textExpand(msg, text, sizeof(text));
|
||||||
textPrint(text, row, col, width);
|
textPrint(text, row, col, width);
|
||||||
if (gAgi->flags[FLAG_WINDOW_STAYS] == 0u) {
|
// f15 leaves one window up without waiting, then goes back (Larry's
|
||||||
(void)inputWaitPrintClose(gAgi->vars[VAR_WINDOW_TIMER]);
|
// quiz sets it for each question; the thanks after Alt+X waits).
|
||||||
textCloseWindow();
|
if (gAgi->flags[FLAG_WINDOW_STAYS] != 0u) {
|
||||||
|
gAgi->flags[FLAG_WINDOW_STAYS] = 0u;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
(void)inputWaitPrintClose(gAgi->vars[VAR_WINDOW_TIMER]);
|
||||||
|
textCloseWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1385,6 +1527,7 @@ void vmCallLogic(uint8_t logicId) {
|
||||||
frame.pc = gAgi->logicScanStart[logicId];
|
frame.pc = gAgi->logicScanStart[logicId];
|
||||||
frame.logic = logicId;
|
frame.logic = logicId;
|
||||||
frame.done = false;
|
frame.done = false;
|
||||||
|
frame.acted = false;
|
||||||
caller = gAgi->currentLogic;
|
caller = gAgi->currentLogic;
|
||||||
gAgi->currentLogic = logicId;
|
gAgi->currentLogic = logicId;
|
||||||
gCallDepth++;
|
gCallDepth++;
|
||||||
|
|
@ -1431,7 +1574,10 @@ bool vmLoadLogic(uint8_t logicId) {
|
||||||
jlFree(raw);
|
jlFree(raw);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
decryptMessages(raw, length);
|
// An LZW-packed v3 logic keeps its messages in the clear.
|
||||||
|
if (!agiResPacked(&gAgi->game, AGI_RES_LOGIC, logicId)) {
|
||||||
|
decryptMessages(raw, length);
|
||||||
|
}
|
||||||
gAgi->logicRaw[logicId] = raw;
|
gAgi->logicRaw[logicId] = raw;
|
||||||
gAgi->logicLength[logicId] = length;
|
gAgi->logicLength[logicId] = length;
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,21 @@ games/
|
||||||
```
|
```
|
||||||
|
|
||||||
`scripts/test-agi.sh` runs the host tests against every subdirectory that
|
`scripts/test-agi.sh` runs the host tests against every subdirectory that
|
||||||
holds a `LOGDIR`, and `make iigs-game-disk-agi` puts `kq3` (or
|
holds a `LOGDIR` (v2) or a `<prefix>DIR` (v3), and `make
|
||||||
`AGI_GAME=NAME`) on the bootable IIgs volume. Everywhere else the
|
iigs-game-disk-agi` puts `kq3` (or `AGI_GAME=NAME`) on the bootable IIgs
|
||||||
|
volume. Everywhere else the
|
||||||
interpreter reads the game from its `DATA` folder; `joey-run --data
|
interpreter reads the game from its `DATA` folder; `joey-run --data
|
||||||
games/kq3` copies a game there for one run.
|
games/kq3` copies a game there for one run.
|
||||||
|
|
||||||
|
Releases for other machines go one level down, in `machines/` (`kq1amiga`,
|
||||||
|
`kq1gs`, `kq1st`, `kq1mac`, and so on), where the host tests do not look
|
||||||
|
yet (PLAN.md, Phase 5).
|
||||||
|
|
||||||
|
`scripts/oracle.sh` plays a game's sessions on the Sierra interpreter in its
|
||||||
|
folder. A release that came without one can be given another release's of
|
||||||
|
the same version; PLAN.md's games table says which. A v3 interpreter carries
|
||||||
|
its game's file prefix, so it opens only that game's files.
|
||||||
|
|
||||||
## Required files per game
|
## Required files per game
|
||||||
|
|
||||||
For an AGI v2 game directory the loader needs:
|
For an AGI v2 game directory the loader needs:
|
||||||
|
|
@ -31,9 +41,15 @@ For an AGI v2 game directory the loader needs:
|
||||||
- `OBJECT` - inventory table
|
- `OBJECT` - inventory table
|
||||||
- `WORDS.TOK` - parser dictionary
|
- `WORDS.TOK` - parser dictionary
|
||||||
|
|
||||||
and it reads Sierra's interpreter version from `AGIDATA.OVL` when the
|
An AGI v3 game instead has one `<prefix>DIR` holding the four indexes and
|
||||||
|
`<prefix>VOL.0`..`<prefix>VOL.N` (`GRDIR` and `GRVOL.*` for Gold Rush);
|
||||||
|
the loader knows the prefixes of Sierra's v3 games (`KQ4`, `GR`, `MH`,
|
||||||
|
`MH2`, `BC`, `DM`). `OBJECT` and `WORDS.TOK` are the same.
|
||||||
|
|
||||||
|
The loader reads Sierra's interpreter version from `AGIDATA.OVL` when the
|
||||||
folder has it, to behave as that version did (see `agiQuirk`); without
|
folder has it, to behave as that version did (see `agiQuirk`); without
|
||||||
it the interpreter behaves as 2.435, the version it was measured on.
|
it the interpreter behaves as 2.435, the version it was measured on (or,
|
||||||
|
for a v3 game, 3.002.149).
|
||||||
|
|
||||||
File names are uppercase by AGI convention; the loader looks them up
|
File names are uppercase by AGI convention; the loader looks them up
|
||||||
literally so the directory should match.
|
literally so the directory should match.
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
AGI_HOME := $(abspath $(dir $(lastword $(MAKEFILE_LIST))))
|
AGI_HOME := $(abspath $(dir $(lastword $(MAKEFILE_LIST))))
|
||||||
|
|
||||||
PROGRAMS += AGI
|
PROGRAMS += AGI
|
||||||
AGI_SRCS := $(addprefix $(AGI_HOME)/,agi.c agiRes.c agiPic.c agiView.c agiGfx.c agiObj.c agiText.c agiFont.c agiInput.c agiSound.c agiSave.c agiVm.c)
|
AGI_SRCS := $(addprefix $(AGI_HOME)/,agi.c agiRes.c agiPic.c agiView.c agiGfx.c agiObj.c agiText.c agiFont.c agiInput.c agiSound.c agiSave.c agiVm.c agiPatch.c agiMouse.c)
|
||||||
AGI_TITLE := AGI
|
AGI_TITLE := AGI
|
||||||
AGI_HELP := A Sierra AGI interpreter. The files of the game it plays go in its DATA folder.
|
AGI_HELP := A Sierra AGI interpreter. The files of the game it plays go in its DATA folder.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# oracle.sh - play the test probes (tests/probes.py) and the King's Quest
|
# oracle.sh - play the test probes (tests/probes.py) and the game sessions
|
||||||
# III sessions (tests/kq3/*.scr) on Sierra's own interpreter and on
|
# (tests/GAME/*.scr, for each game in games/GAME) on Sierra's own
|
||||||
# JoeyAGI, and compare the frames.
|
# interpreter and on JoeyAGI, and compare the frames.
|
||||||
#
|
#
|
||||||
# Sierra's interpreter is the black box the probes were checked against:
|
# Sierra's interpreter is the black box the probes were checked against:
|
||||||
# each probe game is written beside the interpreter files of the copy of
|
# each probe game is written beside the interpreter files of the copy of
|
||||||
# King's Quest III in games/kq3 (AGI, AGIDATA.OVL, the *.OVL drivers,
|
# King's Quest III in games/kq3 (AGI, AGIDATA.OVL, the *.OVL drivers,
|
||||||
# KQ3.COM -- never distributed), run in DOSBox Staging at 386SX-16 speed
|
# KQ3.COM -- never distributed), or of the game ORACLE_INTERP names. A
|
||||||
|
# session plays a copy of its own game with that game's interpreter,
|
||||||
|
# started by the launcher it shipped with (GAME.COM, SIERRA.COM, or the
|
||||||
|
# game's .EXE). Each runs in DOSBox Staging as a fast 386 (dosDrive.py)
|
||||||
# under a private Xvfb and fed the probe's script with xdotool; JoeyAGI
|
# under a private Xvfb and fed the probe's script with xdotool; JoeyAGI
|
||||||
# plays the same script on the host (build/tests/agiHost, built by
|
# plays the same script on the host (build/tests/agiHost, built by
|
||||||
# scripts/test-agi.sh). tests/tools/compareFrames.py then compares each
|
# scripts/test-agi.sh). tests/tools/compareFrames.py then compares each
|
||||||
|
|
@ -15,38 +18,81 @@
|
||||||
# A probe whose script records (shake) is compared by what
|
# A probe whose script records (shake) is compared by what
|
||||||
# tests/tools/shakeMeasure.py reads out of the two recordings.
|
# tests/tools/shakeMeasure.py reads out of the two recordings.
|
||||||
#
|
#
|
||||||
# DOSBox runs in real time and Sierra's interpreter starts a moment later
|
# DOSBox runs in real time. dosDrive.py keeps every step of a script at
|
||||||
# than the host's virtual clock, so shots of things in motion can be a
|
# its own time from the moment DOSBox switches to the game's graphics mode
|
||||||
# cycle or two apart; the probes that matter record their results into
|
# (the host harness's frame 0), so the two sides stay in step however long
|
||||||
# variables and print them, which does not depend on timing.
|
# the screenshots take; a shot of something in motion can still fall in a
|
||||||
|
# different part of a cycle. The probes that matter record their results
|
||||||
|
# into variables and print them, which does not depend on timing.
|
||||||
#
|
#
|
||||||
# The follow probe's second follower takes random detours, so its x2/y2
|
# The follow probe's second follower takes random detours, so its x2/y2
|
||||||
# rows differ from run to run on Sierra's side; everything else matches.
|
# rows differ from run to run on Sierra's side; everything else matches.
|
||||||
#
|
#
|
||||||
# Usage: scripts/oracle.sh [PROBE | kq3-SESSION ...] every probe and
|
# Usage: scripts/oracle.sh [PROBE | GAME-SESSION ...] every probe and
|
||||||
# session when none is named
|
# session when none is named
|
||||||
|
# Env: ORACLE_INTERP=GAME runs the probes on games/GAME's interpreter
|
||||||
|
# (a v2 game; default kq3)
|
||||||
# Output: build/oracle/PROBE/{oracle,joey}/ and a diff image per shot
|
# Output: build/oracle/PROBE/{oracle,joey}/ and a diff image per shot
|
||||||
# Needs: games/kq3 with Sierra's interpreter, Xvfb, xdotool, python3 +
|
# Needs: games/kq3 (or games/ORACLE_INTERP) with Sierra's interpreter,
|
||||||
# PIL, JoeyLib's DOSBox Staging (./install.sh install emulators)
|
# Xvfb, xdotool, python3 + PIL, JoeyLib's DOSBox Staging
|
||||||
|
# (./install.sh install emulators)
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
JOEYLIB=$(cd "${JOEYLIB:-$repo/../joeylib}" && pwd) || { echo "oracle: no JoeyLib checkout (set JOEYLIB)" >&2; exit 2; }
|
JOEYLIB=$(cd "${JOEYLIB:-$repo/../joeylib}" && pwd) || { echo "oracle: no JoeyLib checkout (set JOEYLIB)" >&2; exit 2; }
|
||||||
export JOEYLIB
|
export JOEYLIB
|
||||||
sierra=$repo/games/kq3
|
games=$repo/games
|
||||||
|
sierra=$games/${ORACLE_INTERP:-kq3}
|
||||||
tools=$repo/tests/tools
|
tools=$repo/tests/tools
|
||||||
build=$repo/build/oracle
|
build=$repo/build/oracle
|
||||||
agiHost=$repo/build/tests/agiHost
|
agiHost=$repo/build/tests/agiHost
|
||||||
parallel=${ORACLE_JOBS:-4}
|
parallel=${ORACLE_JOBS:-4}
|
||||||
|
|
||||||
[[ -f $sierra/AGI && -f $sierra/KQ3.COM ]] || { echo "oracle: needs Sierra's interpreter in games/kq3" >&2; exit 2; }
|
# The launcher Sierra shipped with game $2 (games/$2) in folder $1. A v3
|
||||||
|
# game runs as <prefix>.COM, the name its installer gives SIERRA.COM (the
|
||||||
|
# interpreter takes its file prefix, GR of GRDIR, from it), so in a copy
|
||||||
|
# of one that file is made.
|
||||||
|
launcher() {
|
||||||
|
local dir=$1
|
||||||
|
local id=$2
|
||||||
|
local exe
|
||||||
|
local d
|
||||||
|
|
||||||
|
if [[ ! -f $dir/LOGDIR && -f $dir/SIERRA.COM ]]; then
|
||||||
|
for d in "$dir"/*DIR; do
|
||||||
|
if [[ -f $d ]]; then
|
||||||
|
cp "$dir/SIERRA.COM" "${d%DIR}.COM"
|
||||||
|
basename "${d%DIR}.COM"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
for exe in "$dir/${id^^}.COM" "$dir/SIERRA.COM" "$dir"/*.[Ee][Xx][Ee]; do
|
||||||
|
if [[ -f $exe ]]; then
|
||||||
|
basename "$exe"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ -f $sierra/AGIDATA.OVL ]] && launcher "$sierra" "$(basename "$sierra")" >/dev/null || { echo "oracle: needs Sierra's interpreter in $sierra" >&2; exit 2; }
|
||||||
for tool in Xvfb xdotool python3; do
|
for tool in Xvfb xdotool python3; do
|
||||||
command -v "$tool" >/dev/null || { echo "oracle: needs $tool" >&2; exit 2; }
|
command -v "$tool" >/dev/null || { echo "oracle: needs $tool" >&2; exit 2; }
|
||||||
done
|
done
|
||||||
[[ -x $agiHost ]] || "$repo/scripts/test-agi.sh" font >/dev/null || true
|
[[ -x $agiHost ]] || "$repo/scripts/test-agi.sh" font >/dev/null || true
|
||||||
[[ -x $agiHost ]] || { echo "oracle: build/tests/agiHost missing (scripts/test-agi.sh builds it)" >&2; exit 1; }
|
[[ -x $agiHost ]] || { echo "oracle: build/tests/agiHost missing (scripts/test-agi.sh builds it)" >&2; exit 1; }
|
||||||
|
|
||||||
sessions=$(for f in "$repo"/tests/kq3/*.scr; do [[ -f $f ]] && echo "kq3-$(basename "${f%.scr}")"; done)
|
# Every session of a game that ships Sierra's interpreter (AGIDATA.OVL;
|
||||||
|
# Space Quest 0's is a Windows program and has no part here), but those
|
||||||
|
# that use the mouse, which Sierra's DOS interpreters have none of, and
|
||||||
|
# those marked "# JoeyAGI only" (where JoeyAGI skips copy protection).
|
||||||
|
sessions=$(for f in "$repo"/tests/*/*.scr; do
|
||||||
|
g=$(basename "$(dirname "$f")")
|
||||||
|
[[ -f $f && -f $games/$g/AGIDATA.OVL ]] || continue
|
||||||
|
grep -qE '^((mouse|click)( |$)|# JoeyAGI only)' "$f" && continue
|
||||||
|
echo "$g-$(basename "${f%.scr}")"
|
||||||
|
done)
|
||||||
probes=${*:-$(python3 "$repo/tests/probes.py" list) $sessions}
|
probes=${*:-$(python3 "$repo/tests/probes.py" list) $sessions}
|
||||||
mkdir -p "$build"
|
mkdir -p "$build"
|
||||||
|
|
||||||
|
|
@ -56,19 +102,26 @@ one() {
|
||||||
local game=$build/$name/game
|
local game=$build/$name/game
|
||||||
local run
|
local run
|
||||||
local script
|
local script
|
||||||
|
local exe
|
||||||
|
|
||||||
rm -rf "${build:?}/$name"
|
rm -rf "${build:?}/$name"
|
||||||
mkdir -p "$build/$name/oracle" "$build/$name/joey"
|
mkdir -p "$build/$name/oracle" "$build/$name/joey"
|
||||||
if [[ $name == kq3-* ]]; then
|
script=$repo/tests/${name%%-*}/${name#*-}.scr
|
||||||
cp -r "$sierra" "$game"
|
if [[ $name == *-* && -f $script ]]; then
|
||||||
script=$repo/tests/kq3/${name#kq3-}.scr
|
cp -r "$games/${name%%-*}" "$game"
|
||||||
|
exe=$(launcher "$game" "${name%%-*}")
|
||||||
else
|
else
|
||||||
python3 "$repo/tests/probes.py" build "$build/$name/.probe" "$name" || return 1
|
python3 "$repo/tests/probes.py" build "$build/$name/.probe" "$name" || return 1
|
||||||
mv "$build/$name/.probe/$name" "$game"
|
mv "$build/$name/.probe/$name" "$game"
|
||||||
rmdir "$build/$name/.probe"
|
rmdir "$build/$name/.probe"
|
||||||
cp "$sierra"/AGI "$sierra"/*.OVL "$sierra"/HGC_FONT "$sierra"/KQ3.COM "$game/"
|
exe=$(launcher "$sierra" "$(basename "$sierra")")
|
||||||
|
cp "$sierra"/*.OVL "$sierra"/HGC_FONT "$sierra/$exe" "$game/"
|
||||||
|
if [[ -f $sierra/AGI ]]; then
|
||||||
|
cp "$sierra/AGI" "$game/"
|
||||||
|
fi
|
||||||
script=$game/probe.scr
|
script=$game/probe.scr
|
||||||
fi
|
fi
|
||||||
timeout -s KILL 900 python3 "$tools/dosDrive.py" "$game" KQ3.COM "$build/$name/oracle" "$script" \
|
timeout -s KILL 900 python3 "$tools/dosDrive.py" "$game" "$exe" "$build/$name/oracle" "$script" \
|
||||||
--set joysticktype=none > "$build/$name/oracle.log" 2>&1
|
--set joysticktype=none > "$build/$name/oracle.log" 2>&1
|
||||||
run=$(mktemp -d "$build/run.XXXXXX")
|
run=$(mktemp -d "$build/run.XXXXXX")
|
||||||
ln -s "$game" "$run/DATA"
|
ln -s "$game" "$run/DATA"
|
||||||
|
|
@ -76,8 +129,8 @@ one() {
|
||||||
(cd "$run" && timeout -s KILL 900 "$agiHost" "$script" "$build/$name/joey") > "$build/$name/joey.log" 2>&1
|
(cd "$run" && timeout -s KILL 900 "$agiHost" "$script" "$build/$name/joey") > "$build/$name/joey.log" 2>&1
|
||||||
rm -rf "$run"
|
rm -rf "$run"
|
||||||
}
|
}
|
||||||
export -f one
|
export -f one launcher
|
||||||
export build repo sierra tools agiHost
|
export build repo games sierra tools agiHost
|
||||||
|
|
||||||
# The fonts first: every comparison reads text with them.
|
# The fonts first: every comparison reads text with them.
|
||||||
one font
|
one font
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,17 @@
|
||||||
# subsystem (pictures, windows, motion, control lines, priority,
|
# subsystem (pictures, windows, motion, control lines, priority,
|
||||||
# parser, menus, inventory, sound timing, ...), each played by its
|
# parser, menus, inventory, sound timing, ...), each played by its
|
||||||
# own script. Expected hashes in tests/expected/probes/.
|
# own script. Expected hashes in tests/expected/probes/.
|
||||||
# 3. With games/kq3 present, the KQ3 sessions in tests/kq3/*.scr (the
|
# 3. The game sessions: tests/GAME/*.scr, each played on games/GAME when
|
||||||
# opening, walking, typing, menus, debug teleports). Expected hashes
|
# that game is there (King's Quest III's opening, walking, typing,
|
||||||
# in tests/expected/kq3/.
|
# menus, and debug teleports; the other games' starts). Expected
|
||||||
|
# hashes in tests/expected/GAME/.
|
||||||
#
|
#
|
||||||
# Time inside a run is virtual (one frame per jlWaitVBL), so a run is
|
# Time inside a run is virtual (one frame per jlWaitVBL), so a run is
|
||||||
# exactly reproducible. Every run leaves its frames as PPMs in
|
# exactly reproducible. Every run leaves its frames as PPMs in
|
||||||
# build/tests/out/NAME/.
|
# build/tests/out/NAME/.
|
||||||
#
|
#
|
||||||
# Usage: scripts/test-agi.sh [--update] [NAME...]
|
# Usage: scripts/test-agi.sh [--update] [NAME...]
|
||||||
# NAME is a probe, kq3-SESSION or resources-GAME; --update
|
# NAME is a probe, GAME-SESSION or resources-GAME; --update
|
||||||
# rewrites the expected hashes from this run (check the frames
|
# rewrites the expected hashes from this run (check the frames
|
||||||
# first, e.g. with scripts/oracle.sh).
|
# first, e.g. with scripts/oracle.sh).
|
||||||
# Env: JOEYLIB the JoeyLib checkout (default ../joeylib)
|
# Env: JOEYLIB the JoeyLib checkout (default ../joeylib)
|
||||||
|
|
@ -50,8 +51,10 @@ mkdir -p "$obj" "$out"
|
||||||
# ----- Build: JoeyLib's core and generic code with the host port -----
|
# ----- Build: JoeyLib's core and generic code with the host port -----
|
||||||
|
|
||||||
jlFlags=(-std=c99 -O1 -DJOEYLIB_PLATFORM_BLANK -I "$JOEYLIB/include" -I "$JOEYLIB/include/joey" -I "$JOEYLIB/src/core" -I "$JOEYLIB/src/codegen")
|
jlFlags=(-std=c99 -O1 -DJOEYLIB_PLATFORM_BLANK -I "$JOEYLIB/include" -I "$JOEYLIB/include/joey" -I "$JOEYLIB/src/core" -I "$JOEYLIB/src/codegen")
|
||||||
agiFlags=("${jlFlags[@]}" -Wall -Wextra -Werror -I "$repo" -I "$repo/tests")
|
agiFlags=("${jlFlags[@]}" -Wall -Wextra -Werror -DAGI_TRACE -I "$repo" -I "$repo/tests")
|
||||||
engine=(agi agiRes agiPic agiView agiGfx agiObj agiText agiFont agiInput agiSound agiSave agiVm)
|
# The engine's sources, as program.mk lists them for the ports.
|
||||||
|
engine=($(grep '^AGI_SRCS' "$repo/program.mk" | sed -e 's/.*,//' -e 's/)$//' -e 's/\.c//g'))
|
||||||
|
[[ ${#engine[@]} -gt 0 ]] || { echo "test-agi: no AGI_SRCS in program.mk" >&2; exit 1; }
|
||||||
|
|
||||||
# compile SRC OBJ FLAGS...: when the source, or any header, is newer.
|
# compile SRC OBJ FLAGS...: when the source, or any header, is newer.
|
||||||
compile() {
|
compile() {
|
||||||
|
|
@ -148,7 +151,8 @@ play() {
|
||||||
echo
|
echo
|
||||||
echo "--- resources ---"
|
echo "--- resources ---"
|
||||||
for dir in "$repo"/games/*/; do
|
for dir in "$repo"/games/*/; do
|
||||||
[[ -f $dir/LOGDIR ]] || continue
|
# A v2 game has a LOGDIR, a v3 one a <prefix>DIR.
|
||||||
|
[[ -f $dir/LOGDIR ]] || compgen -G "$dir*DIR" >/dev/null || continue
|
||||||
game=$(basename "$dir")
|
game=$(basename "$dir")
|
||||||
name=resources-$game
|
name=resources-$game
|
||||||
selected "$name" || continue
|
selected "$name" || continue
|
||||||
|
|
@ -178,20 +182,23 @@ for name in $(python3 "$repo/tests/probes.py" list); do
|
||||||
play "$name" "$build/probes/$name" "$build/probes/$name/probe.scr" "$expected/probes/$name.txt"
|
play "$name" "$build/probes/$name" "$build/probes/$name/probe.scr" "$expected/probes/$name.txt"
|
||||||
done
|
done
|
||||||
|
|
||||||
# ----- 3. King's Quest III -----
|
# ----- 3. The games -----
|
||||||
|
|
||||||
echo
|
for dir in "$repo"/tests/*/; do
|
||||||
echo "--- kq3 ---"
|
game=$(basename "$dir")
|
||||||
if [[ -f $repo/games/kq3/LOGDIR ]]; then
|
compgen -G "$dir*.scr" >/dev/null || continue
|
||||||
for scr in "$repo"/tests/kq3/*.scr; do
|
echo
|
||||||
[[ -f $scr ]] || continue
|
echo "--- $game ---"
|
||||||
|
if [[ ! -d $repo/games/$game ]]; then
|
||||||
|
echo " skipped: no games/$game"
|
||||||
|
skipped=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
for scr in "$dir"*.scr; do
|
||||||
session=$(basename "${scr%.scr}")
|
session=$(basename "${scr%.scr}")
|
||||||
play "kq3-$session" "$repo/games/kq3" "$scr" "$expected/kq3/$session.txt"
|
play "$game-$session" "$repo/games/$game" "$scr" "$expected/$game/$session.txt"
|
||||||
done
|
done
|
||||||
else
|
done
|
||||||
echo " skipped: no games/kq3"
|
|
||||||
skipped=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "===== test-agi: $passed passed, $failed failed ====="
|
echo "===== test-agi: $passed passed, $failed failed ====="
|
||||||
|
|
|
||||||
11
tests/agidemo1/start.scr
Normal file
11
tests/agidemo1/start.scr
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# The start: Demo 1's menu, Space Quest picked (1) and run (F9).
|
||||||
|
wait 4
|
||||||
|
shot d01
|
||||||
|
key 1
|
||||||
|
wait 1
|
||||||
|
shot d02
|
||||||
|
key F9
|
||||||
|
wait 6
|
||||||
|
shot d03
|
||||||
|
wait 6
|
||||||
|
shot d04
|
||||||
13
tests/agidemo2a/start.scr
Normal file
13
tests/agidemo2a/start.scr
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# The start: "Press any key" (space: Return is set.key's, and reaches
|
||||||
|
# have.key only by luck of timing on Sierra's), the menu, and demo 1.
|
||||||
|
wait 4
|
||||||
|
shot d01
|
||||||
|
key space
|
||||||
|
wait 4
|
||||||
|
shot d02
|
||||||
|
key 1
|
||||||
|
key Return
|
||||||
|
wait 6
|
||||||
|
shot d03
|
||||||
|
wait 6
|
||||||
|
shot d04
|
||||||
13
tests/agidemo2b/start.scr
Normal file
13
tests/agidemo2b/start.scr
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# The start: "Press any key" (space: Return is set.key's, and reaches
|
||||||
|
# have.key only by luck of timing on Sierra's), the menu, and demo 1.
|
||||||
|
wait 4
|
||||||
|
shot d01
|
||||||
|
key space
|
||||||
|
wait 4
|
||||||
|
shot d02
|
||||||
|
key 1
|
||||||
|
key Return
|
||||||
|
wait 6
|
||||||
|
shot d03
|
||||||
|
wait 6
|
||||||
|
shot d04
|
||||||
13
tests/agidemo2c/start.scr
Normal file
13
tests/agidemo2c/start.scr
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# The start: "Press any key" (space: Return is set.key's, and reaches
|
||||||
|
# have.key only by luck of timing on Sierra's), the menu, and demo 1.
|
||||||
|
wait 4
|
||||||
|
shot d01
|
||||||
|
key space
|
||||||
|
wait 4
|
||||||
|
shot d02
|
||||||
|
key 1
|
||||||
|
key Return
|
||||||
|
wait 6
|
||||||
|
shot d03
|
||||||
|
wait 6
|
||||||
|
shot d04
|
||||||
12
tests/agidemo3/start.scr
Normal file
12
tests/agidemo3/start.scr
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# The start: "Press any key" (space, as for Demo 2), the menu, and demo 1.
|
||||||
|
wait 4
|
||||||
|
shot d01
|
||||||
|
key space
|
||||||
|
wait 4
|
||||||
|
shot d02
|
||||||
|
key 1
|
||||||
|
key Return
|
||||||
|
wait 6
|
||||||
|
shot d03
|
||||||
|
wait 6
|
||||||
|
shot d04
|
||||||
8
tests/bc/start.scr
Normal file
8
tests/bc/start.scr
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# The start: the title and the credits, then Taran by Dallben's cottage.
|
||||||
|
wait 4
|
||||||
|
shot b01
|
||||||
|
wait 8
|
||||||
|
shot b02
|
||||||
|
key Return
|
||||||
|
wait 6
|
||||||
|
shot b03
|
||||||
9
tests/bcv210/start.scr
Normal file
9
tests/bcv210/start.scr
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# The Black Cauldron 2.10 (AGI 3): the title and the credits, then Taran
|
||||||
|
# by Dallben's cottage.
|
||||||
|
wait 4
|
||||||
|
shot b01
|
||||||
|
wait 8
|
||||||
|
shot b02
|
||||||
|
key Return
|
||||||
|
wait 6
|
||||||
|
shot b03
|
||||||
10
tests/ddpst/start.scr
Normal file
10
tests/ddpst/start.scr
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# Donald Duck's Playground 1.0A, the Atari ST release's files (eXo files
|
||||||
|
# them as DOS; no interpreter here, so JoeyAGI's alone): the title, the
|
||||||
|
# nephews, and Duckburg.
|
||||||
|
wait 4
|
||||||
|
shot d1
|
||||||
|
wait 8
|
||||||
|
shot d2
|
||||||
|
key Return
|
||||||
|
wait 6
|
||||||
|
shot d3
|
||||||
4
tests/expected/agidemo1/start.txt
Normal file
4
tests/expected/agidemo1/start.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
d01 25017b21
|
||||||
|
d02 bc5c1dd9
|
||||||
|
d03 4dbe2518
|
||||||
|
d04 04103f6a
|
||||||
4
tests/expected/agidemo2a/start.txt
Normal file
4
tests/expected/agidemo2a/start.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
d01 d1423a29
|
||||||
|
d02 ecc33127
|
||||||
|
d03 9b0d5a7b
|
||||||
|
d04 3392072b
|
||||||
4
tests/expected/agidemo2b/start.txt
Normal file
4
tests/expected/agidemo2b/start.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
d01 d1423a29
|
||||||
|
d02 68780fd3
|
||||||
|
d03 9b0d5a7b
|
||||||
|
d04 3392072b
|
||||||
4
tests/expected/agidemo2c/start.txt
Normal file
4
tests/expected/agidemo2c/start.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
d01 b332a93d
|
||||||
|
d02 26dbae01
|
||||||
|
d03 2687ee4d
|
||||||
|
d04 9fa769c5
|
||||||
4
tests/expected/agidemo3/start.txt
Normal file
4
tests/expected/agidemo3/start.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
d01 556337e1
|
||||||
|
d02 dd7304d5
|
||||||
|
d03 e367472f
|
||||||
|
d04 46e6df19
|
||||||
3
tests/expected/bc/start.txt
Normal file
3
tests/expected/bc/start.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
b01 32386daf
|
||||||
|
b02 abbb679b
|
||||||
|
b03 e6cf7f1a
|
||||||
3
tests/expected/bcv210/start.txt
Normal file
3
tests/expected/bcv210/start.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
b01 32386daf
|
||||||
|
b02 abbb679b
|
||||||
|
b03 e6cf7f1a
|
||||||
3
tests/expected/ddpst/start.txt
Normal file
3
tests/expected/ddpst/start.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
d1 149b9a3d
|
||||||
|
d2 7f427c91
|
||||||
|
d3 e7e063da
|
||||||
4
tests/expected/gr/start.txt
Normal file
4
tests/expected/gr/start.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
s1 5d188ae1
|
||||||
|
s2 f1fbd425
|
||||||
|
s3 31e24c9a
|
||||||
|
s4 e9568d9a
|
||||||
4
tests/expected/grv201/start.txt
Normal file
4
tests/expected/grv201/start.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
s1 6c9d1d93
|
||||||
|
s2 ff68162b
|
||||||
|
s3 5a454ac2
|
||||||
|
s4 a682c8fc
|
||||||
5
tests/expected/kq1/start.txt
Normal file
5
tests/expected/kq1/start.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
k01 b13ad04d
|
||||||
|
k02 3e3e97ad
|
||||||
|
k03 b80a0692
|
||||||
|
k04 f4ada91e
|
||||||
|
k05 a228bc50
|
||||||
5
tests/expected/kq2/start.txt
Normal file
5
tests/expected/kq2/start.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
k01 6c4d72fc
|
||||||
|
k02 1468a291
|
||||||
|
k03 41e7e7b8
|
||||||
|
k04 0194cfae
|
||||||
|
k05 41e7e7b8
|
||||||
7
tests/expected/kq3/mouse.txt
Normal file
7
tests/expected/kq3/mouse.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
m01 9e1e99da
|
||||||
|
m02 c0a82f5f
|
||||||
|
m03 86889a57
|
||||||
|
m04 c08deb47
|
||||||
|
m05 266b8a99
|
||||||
|
m06 787d7b35
|
||||||
|
m07 c354a030
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
o01 fddacd89
|
o01 56c612e7
|
||||||
o02 b2742c3f
|
o02 4db5e753
|
||||||
o03 74541ae3
|
o03 74541ae3
|
||||||
o04 bf373e80
|
o04 bf373e80
|
||||||
o05 bf373e80
|
o05 bf373e80
|
||||||
o06 625d901d
|
o06 b398e4e7
|
||||||
o07 784af2b9
|
o07 f9d34c7d
|
||||||
o08 6e6b8dd5
|
o08 4e8ec131
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@ g01 003016a5
|
||||||
g02 f5361366
|
g02 f5361366
|
||||||
g03 9b841c62
|
g03 9b841c62
|
||||||
g04 975e862c
|
g04 975e862c
|
||||||
g05 af3ac138
|
g05 bd657c8b
|
||||||
g06 2d70e908
|
g06 2d70e908
|
||||||
g07 0aa33f32
|
g07 633446b0
|
||||||
g08 081e7951
|
g08 6e2ff025
|
||||||
g09 287e4cb3
|
g09 534621a8
|
||||||
g10 eb63dfcb
|
g10 eb63dfcb
|
||||||
g11 8f2418db
|
g11 8f2418db
|
||||||
g12 ac9a41f5
|
g12 ac9a41f5
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
s01 c7fa568e
|
s01 4d68f0cc
|
||||||
s02 6275e293
|
s02 1a2917ed
|
||||||
s03 37836207
|
s03 08b7e523
|
||||||
s04 490e452f
|
s04 bfda331b
|
||||||
s05 4e59a416
|
s05 4e59a416
|
||||||
s06 155d1e0e
|
s06 155d1e0e
|
||||||
s07 180b604f
|
s07 54f4988f
|
||||||
s08 ab92c46b
|
s08 0623b111
|
||||||
s09 bb34ec9d
|
s09 bb34ec9d
|
||||||
s10 83ac187a
|
s10 83ac187a
|
||||||
s11 d6a89b3d
|
s11 d6a89b3d
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
r20 0d79701a
|
r20 8a6c383a
|
||||||
r23 e2e3fed9
|
r23 5fddb0c1
|
||||||
r26 7c7ae16c
|
r26 7a6e95bc
|
||||||
r29 616f1479
|
r29 616f1479
|
||||||
|
|
|
||||||
4
tests/expected/kq3v214/start.txt
Normal file
4
tests/expected/kq3v214/start.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
o01 56c612e7
|
||||||
|
o02 4db5e753
|
||||||
|
o03 74541ae3
|
||||||
|
o04 bf373e80
|
||||||
6
tests/expected/kq4/start.txt
Normal file
6
tests/expected/kq4/start.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
s1 b3107bc3
|
||||||
|
s2 2acfb799
|
||||||
|
s3 56f8d2fb
|
||||||
|
s4 ff6daa07
|
||||||
|
s5 b51e0291
|
||||||
|
s6 dfe12332
|
||||||
6
tests/expected/kq4gs/start.txt
Normal file
6
tests/expected/kq4gs/start.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
s1 2e9906b7
|
||||||
|
s2 e9dd505f
|
||||||
|
s3 0a7bdd27
|
||||||
|
s4 ff6daa07
|
||||||
|
s5 b51e0291
|
||||||
|
s6 f7914390
|
||||||
4
tests/expected/kq6agi/start.txt
Normal file
4
tests/expected/kq6agi/start.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
k01 f4117ded
|
||||||
|
k02 4d1d5ced
|
||||||
|
k03 2d6c5bb6
|
||||||
|
k04 1cdc393e
|
||||||
8
tests/expected/lsl1/start.txt
Normal file
8
tests/expected/lsl1/start.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
l01 9961df51
|
||||||
|
l02 e3462831
|
||||||
|
l03 54ac58a5
|
||||||
|
l04 4839f56a
|
||||||
|
l05 13859150
|
||||||
|
l06 76a0e2d2
|
||||||
|
l07 300661d9
|
||||||
|
l08 24e226b9
|
||||||
4
tests/expected/mh1/start.txt
Normal file
4
tests/expected/mh1/start.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
s1 0c9307fd
|
||||||
|
s2 b35b29d9
|
||||||
|
s3 f17f55d5
|
||||||
|
s4 b60478bf
|
||||||
4
tests/expected/mh2/start.txt
Normal file
4
tests/expected/mh2/start.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
m01 ca482697
|
||||||
|
m02 d0672fc9
|
||||||
|
m03 7602973d
|
||||||
|
m04 0cd7da2b
|
||||||
4
tests/expected/mh2v303/start.txt
Normal file
4
tests/expected/mh2v303/start.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
m01 ca482697
|
||||||
|
m02 d0672fc9
|
||||||
|
m03 7602973d
|
||||||
|
m04 0cd7da2b
|
||||||
6
tests/expected/mmg/start.txt
Normal file
6
tests/expected/mmg/start.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
m01 40e67cb7
|
||||||
|
m02 93199e7f
|
||||||
|
m03 34e1c07e
|
||||||
|
m04 c840cad2
|
||||||
|
m05 cbddccdf
|
||||||
|
m06 2814beed
|
||||||
5
tests/expected/pq1/start.txt
Normal file
5
tests/expected/pq1/start.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
p01 f3cc67fd
|
||||||
|
p02 088d7363
|
||||||
|
p03 2dd5daef
|
||||||
|
p04 5f41010f
|
||||||
|
p05 2dd5daef
|
||||||
5
tests/expected/pq1v20g/start.txt
Normal file
5
tests/expected/pq1v20g/start.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
p01 f3cc67fd
|
||||||
|
p02 088d7363
|
||||||
|
p03 2dd5daef
|
||||||
|
p04 5f41010f
|
||||||
|
p05 2dd5daef
|
||||||
|
|
@ -1 +1 @@
|
||||||
a 40833758
|
a 409665a3
|
||||||
|
|
|
||||||
1
tests/expected/probes/loopturn.txt
Normal file
1
tests/expected/probes/loopturn.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
a e933940c
|
||||||
4
tests/expected/sq0/start.txt
Normal file
4
tests/expected/sq0/start.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
s01 ca27402f
|
||||||
|
s02 8e5dbbe1
|
||||||
|
s03 0318b5e2
|
||||||
|
s04 03e81ffe
|
||||||
6
tests/expected/sq1/start.txt
Normal file
6
tests/expected/sq1/start.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
s01 9c9fd51d
|
||||||
|
s02 bae6fb93
|
||||||
|
s03 80d7df3a
|
||||||
|
s04 d23de88e
|
||||||
|
s05 25d119ac
|
||||||
|
s06 66c57e84
|
||||||
5
tests/expected/sq2/start.txt
Normal file
5
tests/expected/sq2/start.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
s01 7c76584e
|
||||||
|
s02 d7838181
|
||||||
|
s03 a47fc7d1
|
||||||
|
s04 3222151b
|
||||||
|
s05 160cfe5d
|
||||||
5
tests/expected/sq2v20f/start.txt
Normal file
5
tests/expected/sq2v20f/start.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
s01 d0cc275a
|
||||||
|
s02 d7838181
|
||||||
|
s03 39b92971
|
||||||
|
s04 2232343b
|
||||||
|
s05 160cfe5d
|
||||||
4
tests/expected/sqtlc/start.txt
Normal file
4
tests/expected/sqtlc/start.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
t01 9caf908f
|
||||||
|
t02 4e25d715
|
||||||
|
t03 1604cfa9
|
||||||
|
t04 69e3b0be
|
||||||
4
tests/expected/xmas/start.txt
Normal file
4
tests/expected/xmas/start.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
x01 89dbf9ef
|
||||||
|
x02 35925cc4
|
||||||
|
x03 799a81e5
|
||||||
|
x04 5fda1127
|
||||||
12
tests/gr/start.scr
Normal file
12
tests/gr/start.scr
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# The start: the titles, then Jerrod in Brooklyn Heights, 1848, a walk
|
||||||
|
# from the opening, and the next screen.
|
||||||
|
wait 4
|
||||||
|
shot s1
|
||||||
|
wait 8
|
||||||
|
shot s2
|
||||||
|
key Return
|
||||||
|
wait 4
|
||||||
|
shot s3
|
||||||
|
key Return
|
||||||
|
wait 6
|
||||||
|
shot s4
|
||||||
15
tests/grv201/start.scr
Normal file
15
tests/grv201/start.scr
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Gold Rush 2.01, 3.5-inch, the original (games/gr is a cracked copy):
|
||||||
|
# logic 129's speed test, then the titles, Brooklyn Heights, and the
|
||||||
|
# next screen.
|
||||||
|
# JoeyAGI only: Sierra's interpreter goes from the speed test to the
|
||||||
|
# claim jumper's quiz (rooms 125 to 128), which JoeyAGI skips (agiPatch.c).
|
||||||
|
wait 4
|
||||||
|
shot s1
|
||||||
|
wait 8
|
||||||
|
shot s2
|
||||||
|
key Return
|
||||||
|
wait 4
|
||||||
|
shot s3
|
||||||
|
key Return
|
||||||
|
wait 6
|
||||||
|
shot s4
|
||||||
|
|
@ -11,6 +11,9 @@
|
||||||
// wait SECONDS let SECONDS of virtual time pass
|
// wait SECONDS let SECONDS of virtual time pass
|
||||||
// key NAME [HOLD] press key NAME (xdotool names) for HOLD seconds
|
// key NAME [HOLD] press key NAME (xdotool names) for HOLD seconds
|
||||||
// down NAME / up NAME hold / release a key
|
// down NAME / up NAME hold / release a key
|
||||||
|
// mouse X Y attach a mouse, its pointer at screen pixel
|
||||||
|
// (X, Y) (the host has none until then)
|
||||||
|
// click press the left button as long as a key
|
||||||
// type TEXT press each character's key in turn, typing it
|
// type TEXT press each character's key in turn, typing it
|
||||||
// say TEXT type TEXT, press Return, give the game a second
|
// say TEXT type TEXT, press Return, give the game a second
|
||||||
// to answer and clear its message windows
|
// to answer and clear its message windows
|
||||||
|
|
@ -175,6 +178,7 @@ static void hostShow(char *things);
|
||||||
static void hostState(const char *name);
|
static void hostState(const char *name);
|
||||||
static void hostStep(void);
|
static void hostStep(void);
|
||||||
static bool hostTermHolds(const HostTermT *t);
|
static bool hostTermHolds(const HostTermT *t);
|
||||||
|
static void hostVmTrace(uint8_t logic, uint16_t pc, uint8_t op, const uint8_t *args, uint8_t argCount, int8_t result);
|
||||||
static bool hostWaitStep(void);
|
static bool hostWaitStep(void);
|
||||||
static bool hostWalkStep(void);
|
static bool hostWalkStep(void);
|
||||||
static void hostWritePpm(const char *path);
|
static void hostWritePpm(const char *path);
|
||||||
|
|
@ -221,6 +225,7 @@ static char gOutDir[HOST_PATH_MAX];
|
||||||
static uint32_t gFrame;
|
static uint32_t gFrame;
|
||||||
static uint32_t gResumeFrame;
|
static uint32_t gResumeFrame;
|
||||||
static uint32_t gHoldUntil[JL_KEY_COUNT];
|
static uint32_t gHoldUntil[JL_KEY_COUNT];
|
||||||
|
static uint32_t gClickUntil;
|
||||||
static bool gHeld[JL_KEY_COUNT];
|
static bool gHeld[JL_KEY_COUNT];
|
||||||
static uint8_t gDisplay[JL_SURFACE_HEIGHT][JL_SURFACE_WIDTH][3];
|
static uint8_t gDisplay[JL_SURFACE_HEIGHT][JL_SURFACE_WIDTH][3];
|
||||||
// Characters still to type for the current 'type' line, one per press.
|
// Characters still to type for the current 'type' line, one per press.
|
||||||
|
|
@ -272,6 +277,9 @@ static bool gWindowSeen;
|
||||||
static uint32_t gWindowSince;
|
static uint32_t gWindowSince;
|
||||||
// AGI_HOST_TRACE in the environment: log every key a walk presses.
|
// AGI_HOST_TRACE in the environment: log every key a walk presses.
|
||||||
static bool gTrace;
|
static bool gTrace;
|
||||||
|
// AGI_HOST_VM_TRACE=FILE: every command and condition the VM runs, one line
|
||||||
|
// each (hostVmTrace).
|
||||||
|
static FILE *gVmTrace;
|
||||||
static uint16_t gGotoCount;
|
static uint16_t gGotoCount;
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -716,6 +724,20 @@ static void hostStep(void) {
|
||||||
if (gHeld[key] && ch != 0u) {
|
if (gHeld[key] && ch != 0u) {
|
||||||
jlInputCharPush(ch);
|
jlInputCharPush(ch);
|
||||||
}
|
}
|
||||||
|
} else if (strcmp(op, "mouse") == 0) {
|
||||||
|
char *yArg;
|
||||||
|
|
||||||
|
yArg = arg + strcspn(arg, " \t");
|
||||||
|
if (*yArg == '\0') {
|
||||||
|
fprintf(stderr, "agiHost: mouse wants X Y in '%s'\n", gOpLine);
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
jlgMouseAttached = true;
|
||||||
|
jlgMouseX = (int16_t)atoi(arg);
|
||||||
|
jlgMouseY = (int16_t)atoi(yArg + 1);
|
||||||
|
} else if (strcmp(op, "click") == 0) {
|
||||||
|
gClickUntil = gFrame + HOST_KEY_HOLD_FRAMES;
|
||||||
|
gResumeFrame = gFrame + HOST_KEY_HOLD_FRAMES + HOST_KEY_GAP_FRAMES;
|
||||||
} else if (strcmp(op, "shot") == 0) {
|
} else if (strcmp(op, "shot") == 0) {
|
||||||
hostDump(arg);
|
hostDump(arg);
|
||||||
} else if (strcmp(op, "record") == 0) {
|
} else if (strcmp(op, "record") == 0) {
|
||||||
|
|
@ -871,6 +893,22 @@ static bool hostTermHolds(const HostTermT *t) {
|
||||||
|
|
||||||
// One frame of an until or a walk: true when it is over. A message
|
// One frame of an until or a walk: true when it is over. A message
|
||||||
// window left open meanwhile is dismissed; the time limit runs on.
|
// window left open meanwhile is dismissed; the time limit runs on.
|
||||||
|
// One line of the VM trace: the time, the logic, the command's offset in
|
||||||
|
// it, the command (FF for a condition, with its result) and its arguments.
|
||||||
|
static void hostVmTrace(uint8_t logic, uint16_t pc, uint8_t op, const uint8_t *args, uint8_t argCount, int8_t result) {
|
||||||
|
uint8_t i;
|
||||||
|
|
||||||
|
fprintf(gVmTrace, "%7.2f L%03u @%05u %02X", (double)gFrame / (double)HOST_FRAME_HZ, (unsigned)logic, (unsigned)pc, (unsigned)op);
|
||||||
|
if (result >= 0) {
|
||||||
|
fprintf(gVmTrace, " = %d", (int)result);
|
||||||
|
}
|
||||||
|
for (i = 0u; i < argCount; i++) {
|
||||||
|
fprintf(gVmTrace, " %u", (unsigned)args[i]);
|
||||||
|
}
|
||||||
|
fputc('\n', gVmTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static bool hostWaitStep(void) {
|
static bool hostWaitStep(void) {
|
||||||
if (gWait == HOST_WAIT_UNTIL && hostCondHolds()) {
|
if (gWait == HOST_WAIT_UNTIL && hostCondHolds()) {
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -1080,6 +1118,7 @@ void jlpInputPoll(void) {
|
||||||
for (k = 0u; k < (uint16_t)JL_KEY_COUNT; k++) {
|
for (k = 0u; k < (uint16_t)JL_KEY_COUNT; k++) {
|
||||||
jlgKeyState[k] = (uint8_t)((gHeld[k] || gFrame < gHoldUntil[k]) ? 1u : 0u);
|
jlgKeyState[k] = (uint8_t)((gHeld[k] || gFrame < gHoldUntil[k]) ? 1u : 0u);
|
||||||
}
|
}
|
||||||
|
jlgMouseButtonState[JL_MOUSE_BUTTON_LEFT] = (uint8_t)((gFrame < gClickUntil) ? 1u : 0u);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1138,6 +1177,12 @@ bool hostScriptOpen(const char *scriptPath, const char *outDir) {
|
||||||
}
|
}
|
||||||
strncpy(gOutDir, outDir, sizeof(gOutDir) - 1u);
|
strncpy(gOutDir, outDir, sizeof(gOutDir) - 1u);
|
||||||
gTrace = getenv("AGI_HOST_TRACE") != NULL;
|
gTrace = getenv("AGI_HOST_TRACE") != NULL;
|
||||||
|
if (getenv("AGI_HOST_VM_TRACE") != NULL) {
|
||||||
|
gVmTrace = fopen(getenv("AGI_HOST_VM_TRACE"), "w");
|
||||||
|
if (gVmTrace != NULL) {
|
||||||
|
gAgiTrace = hostVmTrace;
|
||||||
|
}
|
||||||
|
}
|
||||||
// Line by line, so a run killed by a timeout still shows how far it got.
|
// Line by line, so a run killed by a timeout still shows how far it got.
|
||||||
setvbuf(stdout, NULL, _IOLBF, 0);
|
setvbuf(stdout, NULL, _IOLBF, 0);
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
16
tests/kq1/start.scr
Normal file
16
tests/kq1/start.scr
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# The start: the title and the credits, then Graham by the castle, a look
|
||||||
|
# around, and the window closed.
|
||||||
|
wait 4
|
||||||
|
shot k01
|
||||||
|
wait 8
|
||||||
|
shot k02
|
||||||
|
key Return
|
||||||
|
wait 6
|
||||||
|
shot k03
|
||||||
|
type look
|
||||||
|
key Return
|
||||||
|
wait 2
|
||||||
|
shot k04
|
||||||
|
clear
|
||||||
|
wait 1
|
||||||
|
shot k05
|
||||||
16
tests/kq2/start.scr
Normal file
16
tests/kq2/start.scr
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# The start: the title and the credits, then Graham on the beach, a look
|
||||||
|
# around, and the window closed.
|
||||||
|
wait 4
|
||||||
|
shot k01
|
||||||
|
wait 8
|
||||||
|
shot k02
|
||||||
|
key Return
|
||||||
|
wait 6
|
||||||
|
shot k03
|
||||||
|
type look
|
||||||
|
key Return
|
||||||
|
wait 2
|
||||||
|
shot k04
|
||||||
|
clear
|
||||||
|
wait 1
|
||||||
|
shot k05
|
||||||
28
tests/kq3/mouse.scr
Normal file
28
tests/kq3/mouse.scr
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
# The mouse (decision 4; Sierra's DOS interpreter has none, so this is JoeyAGI's
|
||||||
|
# alone): the pointer, a click to walk, the menu bar clicked, Help lit by
|
||||||
|
# the pointer and clicked, and its screen closed by a click.
|
||||||
|
wait 4
|
||||||
|
key Return
|
||||||
|
wait 4
|
||||||
|
mouse 230 150
|
||||||
|
wait 0.5
|
||||||
|
shot m01
|
||||||
|
click
|
||||||
|
wait 3
|
||||||
|
shot m02
|
||||||
|
mouse 20 3
|
||||||
|
click
|
||||||
|
wait 1
|
||||||
|
shot m03
|
||||||
|
mouse 60 3
|
||||||
|
wait 0.5
|
||||||
|
shot m04
|
||||||
|
mouse 60 24
|
||||||
|
wait 0.5
|
||||||
|
shot m05
|
||||||
|
click
|
||||||
|
wait 2
|
||||||
|
shot m06
|
||||||
|
click
|
||||||
|
wait 1
|
||||||
|
shot m07
|
||||||
10
tests/kq3v214/start.scr
Normal file
10
tests/kq3v214/start.scr
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# The start of King's Quest III 2.14 (interpreter 2.936): the title, the
|
||||||
|
# credits over Manannan's face, and the house.
|
||||||
|
wait 3
|
||||||
|
shot o01
|
||||||
|
wait 7
|
||||||
|
shot o02
|
||||||
|
wait 7
|
||||||
|
shot o03
|
||||||
|
wait 13
|
||||||
|
shot o04
|
||||||
18
tests/kq4/start.scr
Normal file
18
tests/kq4/start.scr
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# The start of King's Quest IV 2.3 for DOS (3.002.086; no interpreter
|
||||||
|
# here, so JoeyAGI's alone): the title, the credits, the throne room, and
|
||||||
|
# Rosella on Tamir's beach (the manual question skipped, agiPatch.c).
|
||||||
|
wait 4
|
||||||
|
shot s1
|
||||||
|
key Return
|
||||||
|
wait 6
|
||||||
|
shot s2
|
||||||
|
wait 8
|
||||||
|
shot s3
|
||||||
|
key Return
|
||||||
|
wait 8
|
||||||
|
shot s4
|
||||||
|
wait 10
|
||||||
|
shot s5
|
||||||
|
key Return
|
||||||
|
wait 8
|
||||||
|
shot s6
|
||||||
18
tests/kq4gs/start.scr
Normal file
18
tests/kq4gs/start.scr
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# The start of the Apple IIgs release: the title, the credits, the
|
||||||
|
# throne room, and Rosella on Tamir's beach (the manual question skipped,
|
||||||
|
# agiPatch.c). Its interpreter is the IIgs one, so this is JoeyAGI's alone.
|
||||||
|
wait 4
|
||||||
|
shot s1
|
||||||
|
key Return
|
||||||
|
wait 6
|
||||||
|
shot s2
|
||||||
|
wait 8
|
||||||
|
shot s3
|
||||||
|
key Return
|
||||||
|
wait 8
|
||||||
|
shot s4
|
||||||
|
wait 10
|
||||||
|
shot s5
|
||||||
|
key Return
|
||||||
|
wait 8
|
||||||
|
shot s6
|
||||||
11
tests/kq6agi/start.scr
Normal file
11
tests/kq6agi/start.scr
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# The fan demake's start: its disclaimers and title.
|
||||||
|
wait 4
|
||||||
|
shot k01
|
||||||
|
wait 4
|
||||||
|
shot k02
|
||||||
|
key Return
|
||||||
|
wait 4
|
||||||
|
shot k03
|
||||||
|
key Return
|
||||||
|
wait 6
|
||||||
|
shot k04
|
||||||
28
tests/lsl1/start.scr
Normal file
28
tests/lsl1/start.scr
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
# The start: the title, the warning, the age, the first quiz question, and
|
||||||
|
# Alt+X, the game's own way past the quiz (a set.key that sets f109), into
|
||||||
|
# Larry outside Lefty's.
|
||||||
|
wait 4
|
||||||
|
shot l01
|
||||||
|
wait 8
|
||||||
|
shot l02
|
||||||
|
key Return
|
||||||
|
wait 4
|
||||||
|
shot l03
|
||||||
|
key Return
|
||||||
|
wait 4
|
||||||
|
shot l04
|
||||||
|
type 30
|
||||||
|
key Return
|
||||||
|
wait 2
|
||||||
|
shot l05
|
||||||
|
key Return
|
||||||
|
wait 2
|
||||||
|
shot l06
|
||||||
|
down alt
|
||||||
|
key x
|
||||||
|
up alt
|
||||||
|
wait 2
|
||||||
|
shot l07
|
||||||
|
key Return
|
||||||
|
wait 6
|
||||||
|
shot l08
|
||||||
12
tests/mh1/start.scr
Normal file
12
tests/mh1/start.scr
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# The start: the Orbs over New York, two years later, and day 1 on the
|
||||||
|
# Upper East Side.
|
||||||
|
wait 4
|
||||||
|
shot s1
|
||||||
|
wait 8
|
||||||
|
shot s2
|
||||||
|
key Return
|
||||||
|
wait 4
|
||||||
|
shot s3
|
||||||
|
key Return
|
||||||
|
wait 6
|
||||||
|
shot s4
|
||||||
13
tests/mh2/start.scr
Normal file
13
tests/mh2/start.scr
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# The start: the date, the skyline after the orbs pass (shots kept off
|
||||||
|
# the text screen's end, which falls anywhere in its last second), the
|
||||||
|
# globe, and the alley.
|
||||||
|
wait 2
|
||||||
|
shot m01
|
||||||
|
wait 8
|
||||||
|
shot m02
|
||||||
|
key Return
|
||||||
|
wait 4
|
||||||
|
shot m03
|
||||||
|
key Return
|
||||||
|
wait 6
|
||||||
|
shot m04
|
||||||
12
tests/mh2v303/start.scr
Normal file
12
tests/mh2v303/start.scr
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# Manhunter 2 3.03, 5.25-inch: the date, the skyline, the globe, and the
|
||||||
|
# alley.
|
||||||
|
wait 2
|
||||||
|
shot m01
|
||||||
|
wait 8
|
||||||
|
shot m02
|
||||||
|
key Return
|
||||||
|
wait 4
|
||||||
|
shot m03
|
||||||
|
key Return
|
||||||
|
wait 6
|
||||||
|
shot m04
|
||||||
19
tests/mmg/start.scr
Normal file
19
tests/mmg/start.scr
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
# The start: the title, the list of players (set.simple's restore; Start
|
||||||
|
# New Character starts afresh), the name, the face, and the bedroom.
|
||||||
|
wait 4
|
||||||
|
shot m01
|
||||||
|
wait 8
|
||||||
|
shot m02
|
||||||
|
key Return
|
||||||
|
wait 6
|
||||||
|
shot m03
|
||||||
|
key Return
|
||||||
|
wait 4
|
||||||
|
shot m04
|
||||||
|
type Scott
|
||||||
|
key Return
|
||||||
|
wait 3
|
||||||
|
shot m05
|
||||||
|
key Return
|
||||||
|
wait 8
|
||||||
|
shot m06
|
||||||
16
tests/pq1/start.scr
Normal file
16
tests/pq1/start.scr
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# The start: the title, then Sonny in the briefing room, a look around,
|
||||||
|
# and the window closed.
|
||||||
|
wait 4
|
||||||
|
shot p01
|
||||||
|
wait 8
|
||||||
|
shot p02
|
||||||
|
key Return
|
||||||
|
wait 6
|
||||||
|
shot p03
|
||||||
|
type look
|
||||||
|
key Return
|
||||||
|
wait 2
|
||||||
|
shot p04
|
||||||
|
clear
|
||||||
|
wait 1
|
||||||
|
shot p05
|
||||||
16
tests/pq1v20g/start.scr
Normal file
16
tests/pq1v20g/start.scr
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Police Quest 2.0G (interpreter 2.917): the title, then Sonny in the
|
||||||
|
# briefing room, a look around, and the window closed.
|
||||||
|
wait 4
|
||||||
|
shot p01
|
||||||
|
wait 8
|
||||||
|
shot p02
|
||||||
|
key Return
|
||||||
|
wait 6
|
||||||
|
shot p03
|
||||||
|
type look
|
||||||
|
key Return
|
||||||
|
wait 2
|
||||||
|
shot p04
|
||||||
|
clear
|
||||||
|
wait 1
|
||||||
|
shot p05
|
||||||
|
|
@ -402,6 +402,61 @@ def probeCounters():
|
||||||
return dict(logics=roomLogic(body), pics={1: EMPTY_PIC}, views=VIEWS), "wait 5\nshot a\n"
|
return dict(logics=roomLogic(body), pics={1: EMPTY_PIC}, views=VIEWS), "wait 5\nshot a\n"
|
||||||
|
|
||||||
|
|
||||||
|
def fiveLoopView():
|
||||||
|
# Five loops of one cel each, a stripe on a different row in each.
|
||||||
|
loops = []
|
||||||
|
for loop in range(5):
|
||||||
|
art = ["1111111"] * 20
|
||||||
|
art[2 + loop * 3] = "ccccccc"
|
||||||
|
loops.append([cel(art, transparent=0)])
|
||||||
|
return view(loops)
|
||||||
|
|
||||||
|
|
||||||
|
def probeLoopTurn():
|
||||||
|
# When a moving object's loop follows its direction: a 4-loop view
|
||||||
|
# stepping every 3 cycles (A) and every cycle (B), and a 5-loop view
|
||||||
|
# (C), turned right, then left on cycle 4, then down on cycle 9.
|
||||||
|
N = 14
|
||||||
|
rows = [("A", 1, 40), ("B", 2, 80), ("C", 3, 120)]
|
||||||
|
|
||||||
|
def body(L):
|
||||||
|
def mk(o, viewNr, y):
|
||||||
|
L.animate_obj(o)
|
||||||
|
L.set_view(o, viewNr)
|
||||||
|
L.set_loop(o, 0)
|
||||||
|
L.set_cel(o, 0)
|
||||||
|
L.position(o, 80, y)
|
||||||
|
L.ignore_objs(o)
|
||||||
|
L.stop_cycling(o)
|
||||||
|
with L.If(("isset", F_NEW_ROOM)):
|
||||||
|
L.assignn(V_CYCLE_DELAY, 2)
|
||||||
|
showPic(L)
|
||||||
|
L.load_view(0)
|
||||||
|
L.load_view(4)
|
||||||
|
for var, val in ((203, DIR_RIGHT), (204, 3), (206, DIR_LEFT), (207, DIR_DOWN)):
|
||||||
|
L.assignn(var, val)
|
||||||
|
mk(1, 0, 40)
|
||||||
|
L.step_time(1, 204)
|
||||||
|
mk(2, 0, 80)
|
||||||
|
mk(3, 4, 120)
|
||||||
|
for o in (1, 2, 3):
|
||||||
|
L.set_dir(o, 203)
|
||||||
|
L.draw(o)
|
||||||
|
L.show_pic()
|
||||||
|
L.increment(V_CYCLE)
|
||||||
|
for when, var in ((4, 206), (9, 207)):
|
||||||
|
with L.If(("equaln", V_CYCLE, when)):
|
||||||
|
for o in (1, 2, 3):
|
||||||
|
L.set_dir(o, var)
|
||||||
|
with L.If(("equaln", V_CYCLE, N + 2)):
|
||||||
|
for t in rows:
|
||||||
|
L.erase(t[1])
|
||||||
|
series(L, rows, N, lambda L, t, v: L.current_loop(t[1], v))
|
||||||
|
views = dict(VIEWS)
|
||||||
|
views[4] = fiveLoopView()
|
||||||
|
return dict(logics=roomLogic(body), pics={1: EMPTY_PIC}, views=views), "wait 5\nshot a\n"
|
||||||
|
|
||||||
|
|
||||||
def probeRedraw():
|
def probeRedraw():
|
||||||
# What erase/draw, set.view, set.loop, set.cel, animate.obj and position
|
# What erase/draw, set.view, set.loop, set.cel, animate.obj and position
|
||||||
# do to a moving object's position and counters.
|
# do to a moving object's position and counters.
|
||||||
|
|
@ -1402,6 +1457,7 @@ PROBES = {
|
||||||
"newroom": probeNewRoom,
|
"newroom": probeNewRoom,
|
||||||
"edgewalk": probeEdgeWalk,
|
"edgewalk": probeEdgeWalk,
|
||||||
"distance": probeDistance,
|
"distance": probeDistance,
|
||||||
|
"loopturn": probeLoopTurn,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
11
tests/sq0/start.scr
Normal file
11
tests/sq0/start.scr
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# The fan game's start: its titles, the station, and the first room. Its
|
||||||
|
# own interpreter is a Windows program, so this one is JoeyAGI's alone.
|
||||||
|
wait 4
|
||||||
|
shot s01
|
||||||
|
wait 8
|
||||||
|
shot s02
|
||||||
|
key Return
|
||||||
|
wait 4
|
||||||
|
shot s03
|
||||||
|
wait 4
|
||||||
|
shot s04
|
||||||
20
tests/sq1/start.scr
Normal file
20
tests/sq1/start.scr
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
# The start: the title, the log-on (first and last name), and the alarm
|
||||||
|
# aboard the Arcada.
|
||||||
|
wait 4
|
||||||
|
shot s01
|
||||||
|
wait 8
|
||||||
|
shot s02
|
||||||
|
key Return
|
||||||
|
wait 4
|
||||||
|
shot s03
|
||||||
|
type Roger
|
||||||
|
key Return
|
||||||
|
wait 2
|
||||||
|
shot s04
|
||||||
|
type Wilco
|
||||||
|
key Return
|
||||||
|
wait 6
|
||||||
|
shot s05
|
||||||
|
clear
|
||||||
|
wait 2
|
||||||
|
shot s06
|
||||||
14
tests/sq2/start.scr
Normal file
14
tests/sq2/start.scr
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# The start: the title, the opening over the planet, and the log-on.
|
||||||
|
wait 4
|
||||||
|
shot s01
|
||||||
|
wait 8
|
||||||
|
shot s02
|
||||||
|
key Return
|
||||||
|
wait 4
|
||||||
|
shot s03
|
||||||
|
wait 8
|
||||||
|
shot s04
|
||||||
|
type Roger
|
||||||
|
key Return
|
||||||
|
wait 4
|
||||||
|
shot s05
|
||||||
15
tests/sq2v20f/start.scr
Normal file
15
tests/sq2v20f/start.scr
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Space Quest II 2.0F (interpreter 2.936): the title, the opening over the
|
||||||
|
# planet, and the log-on.
|
||||||
|
wait 4
|
||||||
|
shot s01
|
||||||
|
wait 8
|
||||||
|
shot s02
|
||||||
|
key Return
|
||||||
|
wait 4
|
||||||
|
shot s03
|
||||||
|
wait 8
|
||||||
|
shot s04
|
||||||
|
type Roger
|
||||||
|
key Return
|
||||||
|
wait 4
|
||||||
|
shot s05
|
||||||
12
tests/sqtlc/start.scr
Normal file
12
tests/sqtlc/start.scr
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# The fan game's start: the title, its two windows, and the station.
|
||||||
|
wait 4
|
||||||
|
shot t01
|
||||||
|
key Return
|
||||||
|
wait 2
|
||||||
|
shot t02
|
||||||
|
key Return
|
||||||
|
wait 2
|
||||||
|
shot t03
|
||||||
|
key Return
|
||||||
|
wait 6
|
||||||
|
shot t04
|
||||||
|
|
@ -27,6 +27,10 @@
|
||||||
#define SOUND_VOICES 4u
|
#define SOUND_VOICES 4u
|
||||||
#define SOUND_NOTE_BYTES 5u
|
#define SOUND_NOTE_BYTES 5u
|
||||||
#define SOUND_END_BYTE 0xFFu
|
#define SOUND_END_BYTE 0xFFu
|
||||||
|
#define SOUND_TYPE_BYTES 2u
|
||||||
|
#define SOUND_IIGS_SAMPLE 1u
|
||||||
|
#define SOUND_IIGS_MIDI 2u
|
||||||
|
#define SOUND_PC 8u
|
||||||
#define RLE_COLOR_SHIFT 4u
|
#define RLE_COLOR_SHIFT 4u
|
||||||
#define RLE_COUNT_MASK 0x0Fu
|
#define RLE_COUNT_MASK 0x0Fu
|
||||||
#define MAX_CEL_PIXELS (255u * 255u)
|
#define MAX_CEL_PIXELS (255u * 255u)
|
||||||
|
|
@ -41,7 +45,7 @@ static const char *const kTypeNames[AGI_RES_COUNT] = { "logic", "picture", "view
|
||||||
static uint32_t fnv(uint32_t hash, const uint8_t *p, uint32_t length);
|
static uint32_t fnv(uint32_t hash, const uint8_t *p, uint32_t length);
|
||||||
static bool testLogic(uint16_t n);
|
static bool testLogic(uint16_t n);
|
||||||
static bool testPicture(uint16_t n, uint8_t *visual, uint8_t *priority);
|
static bool testPicture(uint16_t n, uint8_t *visual, uint8_t *priority);
|
||||||
static bool testSound(uint16_t n);
|
static bool testSound(uint16_t n, uint16_t *truncated);
|
||||||
static bool testView(uint16_t n, uint8_t *pixels);
|
static bool testView(uint16_t n, uint8_t *pixels);
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -95,20 +99,37 @@ static bool testPicture(uint16_t n, uint8_t *visual, uint8_t *priority) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Every voice's notes must end (FF FF) inside the resource.
|
// Every voice's notes should end (FF FF) inside the resource. One that runs
|
||||||
static bool testSound(uint16_t n) {
|
// to the resource's end instead is listed as truncated, not malformed:
|
||||||
|
// King's Quest III 2.14 shipped its sound 36 cut to 1035 of its 2281 bytes,
|
||||||
|
// and the player ends a voice where the resource ends. A PC sound's
|
||||||
|
// first word is 8, its first voice's offset; anything else is another
|
||||||
|
// machine's sound and only listed: an Apple IIgs sample or MIDI (first word 1
|
||||||
|
// or 2; King's Quest IV's IIgs release has nothing else), or an unknown kind
|
||||||
|
// (Demo 3's PC data carries sounds 209 to 225 starting 0x0101, which no
|
||||||
|
// logic plays).
|
||||||
|
static bool testSound(uint16_t n, uint16_t *truncated) {
|
||||||
uint8_t *data;
|
uint8_t *data;
|
||||||
uint16_t length;
|
uint16_t length;
|
||||||
uint16_t pos;
|
uint16_t pos;
|
||||||
uint16_t notes[SOUND_VOICES];
|
uint16_t notes[SOUND_VOICES];
|
||||||
|
uint16_t type;
|
||||||
uint8_t v;
|
uint8_t v;
|
||||||
|
bool cut;
|
||||||
|
|
||||||
data = agiResLoad(&gAgi->game, AGI_RES_SOUND, n, &length);
|
data = agiResLoad(&gAgi->game, AGI_RES_SOUND, n, &length);
|
||||||
|
type = (data != NULL && length >= SOUND_TYPE_BYTES) ? (uint16_t)(data[0] | ((uint16_t)data[1] << 8)) : 0u;
|
||||||
|
if (data != NULL && type != SOUND_PC) {
|
||||||
|
printf("sound %u %s %u bytes\n", (unsigned)n, (type == SOUND_IIGS_SAMPLE) ? "iigs sample" : ((type == SOUND_IIGS_MIDI) ? "iigs midi" : "not a pc sound"), (unsigned)length);
|
||||||
|
jlFree(data);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (data == NULL || length < SOUND_VOICES * 2u) {
|
if (data == NULL || length < SOUND_VOICES * 2u) {
|
||||||
fprintf(stderr, "sound %u: does not load\n", (unsigned)n);
|
fprintf(stderr, "sound %u: does not load\n", (unsigned)n);
|
||||||
jlFree(data);
|
jlFree(data);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
cut = false;
|
||||||
for (v = 0u; v < SOUND_VOICES; v++) {
|
for (v = 0u; v < SOUND_VOICES; v++) {
|
||||||
pos = (uint16_t)(data[v * 2u] | ((uint16_t)data[v * 2u + 1u] << 8));
|
pos = (uint16_t)(data[v * 2u] | ((uint16_t)data[v * 2u + 1u] << 8));
|
||||||
notes[v] = 0u;
|
notes[v] = 0u;
|
||||||
|
|
@ -117,13 +138,15 @@ static bool testSound(uint16_t n) {
|
||||||
notes[v]++;
|
notes[v]++;
|
||||||
}
|
}
|
||||||
if ((uint32_t)pos + 1u >= length) {
|
if ((uint32_t)pos + 1u >= length) {
|
||||||
fprintf(stderr, "sound %u: voice %u runs off the end\n", (unsigned)n, (unsigned)v);
|
fprintf(stderr, "sound %u: voice %u runs to the end of the resource\n", (unsigned)n, (unsigned)v);
|
||||||
jlFree(data);
|
cut = true;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jlFree(data);
|
jlFree(data);
|
||||||
printf("sound %u notes %u %u %u %u\n", (unsigned)n, (unsigned)notes[0], (unsigned)notes[1], (unsigned)notes[2], (unsigned)notes[3]);
|
if (cut) {
|
||||||
|
(*truncated)++;
|
||||||
|
}
|
||||||
|
printf("sound %u%s notes %u %u %u %u\n", (unsigned)n, cut ? " truncated" : "", (unsigned)notes[0], (unsigned)notes[1], (unsigned)notes[2], (unsigned)notes[3]);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -193,6 +216,7 @@ int main(void) {
|
||||||
uint16_t n;
|
uint16_t n;
|
||||||
uint16_t counts[AGI_RES_COUNT];
|
uint16_t counts[AGI_RES_COUNT];
|
||||||
uint16_t absent;
|
uint16_t absent;
|
||||||
|
uint16_t truncated;
|
||||||
uint8_t t;
|
uint8_t t;
|
||||||
bool ok;
|
bool ok;
|
||||||
|
|
||||||
|
|
@ -218,8 +242,9 @@ int main(void) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
memset(counts, 0, sizeof(counts));
|
memset(counts, 0, sizeof(counts));
|
||||||
absent = 0u;
|
absent = 0u;
|
||||||
ok = true;
|
truncated = 0u;
|
||||||
|
ok = true;
|
||||||
for (n = 0u; n < AGI_MAX_RESOURCES; n++) {
|
for (n = 0u; n < AGI_MAX_RESOURCES; n++) {
|
||||||
for (t = 0u; t < AGI_RES_COUNT; t++) {
|
for (t = 0u; t < AGI_RES_COUNT; t++) {
|
||||||
if (n >= gAgi->game.resCount[t] || gAgi->game.resDir[t][n].volume >= AGI_MAX_VOLUMES) {
|
if (n >= gAgi->game.resCount[t] || gAgi->game.resDir[t][n].volume >= AGI_MAX_VOLUMES) {
|
||||||
|
|
@ -243,12 +268,12 @@ int main(void) {
|
||||||
break;
|
break;
|
||||||
case AGI_RES_SOUND:
|
case AGI_RES_SOUND:
|
||||||
default:
|
default:
|
||||||
ok = testSound(n) && ok;
|
ok = testSound(n, &truncated) && ok;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
counts[t]++;
|
counts[t]++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fprintf(stderr, "%u logics, %u pictures, %u views, %u sounds, %u absent%s\n", (unsigned)counts[AGI_RES_LOGIC], (unsigned)counts[AGI_RES_PIC], (unsigned)counts[AGI_RES_VIEW], (unsigned)counts[AGI_RES_SOUND], (unsigned)absent, ok ? "" : " -- MALFORMED (see above)");
|
fprintf(stderr, "%u logics, %u pictures, %u views, %u sounds, %u absent, %u truncated%s\n", (unsigned)counts[AGI_RES_LOGIC], (unsigned)counts[AGI_RES_PIC], (unsigned)counts[AGI_RES_VIEW], (unsigned)counts[AGI_RES_SOUND], (unsigned)absent, (unsigned)truncated, ok ? "" : " -- MALFORMED (see above)");
|
||||||
return ok ? 0 : 1;
|
return ok ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@
|
||||||
# key NAME [HOLD] press xdotool key NAME for HOLD seconds (0.12)
|
# key NAME [HOLD] press xdotool key NAME for HOLD seconds (0.12)
|
||||||
# down NAME / up NAME hold / release a key
|
# down NAME / up NAME hold / release a key
|
||||||
# type TEXT press each character's key in turn
|
# type TEXT press each character's key in turn
|
||||||
|
# say TEXT type TEXT, press Return, give the game a second
|
||||||
|
# to answer, then clear
|
||||||
# shot NAME save the screen as OUTDIR/NAME.png
|
# shot NAME save the screen as OUTDIR/NAME.png
|
||||||
# clear press Return while a message window is showing
|
# clear press Return while a message window is showing
|
||||||
# record NAME start recording every frame (70 Hz) ...
|
# record NAME start recording every frame (70 Hz) ...
|
||||||
|
|
@ -19,7 +21,16 @@
|
||||||
# is bound to F12 by a mapper file, and a mapper file replaces every
|
# is bound to F12 by a mapper file, and a mapper file replaces every
|
||||||
# default binding -- the keyboard and Ctrl+F5 too -- so a script that
|
# default binding -- the keyboard and Ctrl+F5 too -- so a script that
|
||||||
# records can do nothing else.
|
# records can do nothing else.
|
||||||
|
#
|
||||||
|
# Time is the script's, as in tests/hostPort.c's virtual time: every step
|
||||||
|
# happens at its own time since the interpreter started (the moment DOSBox
|
||||||
|
# logs the switch to its graphics mode), a key taking KEY_HOLD + KEY_GAP, a
|
||||||
|
# cleared window CLEAR_GAP and a screenshot no time at all. What a step
|
||||||
|
# costs here (the capture, each xdotool call) delays at most the next one
|
||||||
|
# and never the rest of the script. A recording's stop is the exception:
|
||||||
|
# the schedule starts again once the video is written.
|
||||||
# Env: DOSBOX (default JoeyLib's toolchains/opt/dosbox/dosbox), JOEYLIB.
|
# Env: DOSBOX (default JoeyLib's toolchains/opt/dosbox/dosbox), JOEYLIB.
|
||||||
|
import datetime
|
||||||
import glob
|
import glob
|
||||||
import os
|
import os
|
||||||
import ctypes
|
import ctypes
|
||||||
|
|
@ -36,6 +47,11 @@ import compareFrames
|
||||||
JOEYLIB = os.environ.get("JOEYLIB", os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../../joeylib"))
|
JOEYLIB = os.environ.get("JOEYLIB", os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../../joeylib"))
|
||||||
DOSBOX = os.environ.get("DOSBOX", os.path.join(JOEYLIB, "toolchains/opt/dosbox/dosbox"))
|
DOSBOX = os.environ.get("DOSBOX", os.path.join(JOEYLIB, "toolchains/opt/dosbox/dosbox"))
|
||||||
CONF = os.path.join(JOEYLIB, "scripts/dosbox-386sx16.conf")
|
CONF = os.path.join(JOEYLIB, "scripts/dosbox-386sx16.conf")
|
||||||
|
# JoeyLib's 386SX-16 configuration, but a fast 386: Sierra's interpreter
|
||||||
|
# paces itself by the clock, so speed only shortens the time it spends
|
||||||
|
# drawing and loading, which the host harness spends none of (at 2200
|
||||||
|
# cycles/ms Black Cauldron's title picture alone put it 1.9 s behind).
|
||||||
|
CPU_CYCLES = "50000"
|
||||||
KEY_HOLD = 0.12
|
KEY_HOLD = 0.12
|
||||||
KEY_GAP = 0.08
|
KEY_GAP = 0.08
|
||||||
CLEAR_GAP = 0.6
|
CLEAR_GAP = 0.6
|
||||||
|
|
@ -47,6 +63,14 @@ VIDEO_POLLS = 50
|
||||||
VIDEO_POLL_SECONDS = 0.3
|
VIDEO_POLL_SECONDS = 0.3
|
||||||
SHOT_POLL_SECONDS = 0.1
|
SHOT_POLL_SECONDS = 0.1
|
||||||
WINDOW_POLLS = 50
|
WINDOW_POLLS = 50
|
||||||
|
SAY_SETTLE = 1.0
|
||||||
|
# The interpreter's start: DOSBox's log line for a graphics mode, and the
|
||||||
|
# timestamp at the head of each log line.
|
||||||
|
START_MARK = "graphics mode"
|
||||||
|
START_POLLS = 100
|
||||||
|
START_POLL_SECONDS = 0.1
|
||||||
|
LOG_TIME_FORMAT = "%Y-%m-%d %H:%M:%S.%f"
|
||||||
|
LOG_TIME_LEN = 23
|
||||||
# prctl option: a signal for the child when its parent dies.
|
# prctl option: a signal for the child when its parent dies.
|
||||||
PR_SET_PDEATHSIG = 1
|
PR_SET_PDEATHSIG = 1
|
||||||
KEYMAP = {" ": "space", ".": "period", ",": "comma", "'": "apostrophe", "-": "minus",
|
KEYMAP = {" ": "space", ".": "period", ",": "comma", "'": "apostrophe", "-": "minus",
|
||||||
|
|
@ -82,10 +106,12 @@ class Dos:
|
||||||
with open(mapper, "w") as f:
|
with open(mapper, "w") as f:
|
||||||
f.write('hand_video "key %d"\n' % VIDEO_KEY_SDL_SCANCODE)
|
f.write('hand_video "key %d"\n' % VIDEO_KEY_SDL_SCANCODE)
|
||||||
args += ["--set", "mapperfile=" + mapper]
|
args += ["--set", "mapperfile=" + mapper]
|
||||||
|
args += ["--set", "cpu_cycles=" + CPU_CYCLES, "--set", "cpu_cycles_protected=" + CPU_CYCLES]
|
||||||
for s in (extraSet or []):
|
for s in (extraSet or []):
|
||||||
args += ["--set", s]
|
args += ["--set", s]
|
||||||
args += ["-c", "MOUNT C \"%s\"" % hostDir, "-c", "C:", "-c", command]
|
args += ["-c", "MOUNT C \"%s\"" % hostDir, "-c", "C:", "-c", command]
|
||||||
self.proc = subprocess.Popen(args, env=self.env, stdout=open(os.path.join(self.work, "dosbox.log"), "w"),
|
self.log = os.path.join(self.work, "dosbox.log")
|
||||||
|
self.proc = subprocess.Popen(args, env=self.env, stdout=open(self.log, "w"),
|
||||||
stderr=subprocess.STDOUT, preexec_fn=dieWithParent)
|
stderr=subprocess.STDOUT, preexec_fn=dieWithParent)
|
||||||
self.win = None
|
self.win = None
|
||||||
for _ in range(WINDOW_POLLS):
|
for _ in range(WINDOW_POLLS):
|
||||||
|
|
@ -96,7 +122,29 @@ class Dos:
|
||||||
if not self.win:
|
if not self.win:
|
||||||
self.stop()
|
self.stop()
|
||||||
raise RuntimeError("no DOSBox window")
|
raise RuntimeError("no DOSBox window")
|
||||||
self.t0 = time.time()
|
self.t0 = self.interpreterStart()
|
||||||
|
self.due = 0.0
|
||||||
|
|
||||||
|
def interpreterStart(self):
|
||||||
|
# When the program switched to a graphics mode, from DOSBox's log
|
||||||
|
# (the host harness's frame 0); the window's appearance if it never
|
||||||
|
# says so.
|
||||||
|
for _ in range(START_POLLS):
|
||||||
|
with open(self.log, errors="replace") as f:
|
||||||
|
for line in f:
|
||||||
|
if START_MARK in line:
|
||||||
|
try:
|
||||||
|
return datetime.datetime.strptime(line[:LOG_TIME_LEN], LOG_TIME_FORMAT).timestamp()
|
||||||
|
except ValueError:
|
||||||
|
break
|
||||||
|
time.sleep(START_POLL_SECONDS)
|
||||||
|
print("no graphics mode in DOSBox's log: timing from the window", file=sys.stderr)
|
||||||
|
return time.time()
|
||||||
|
|
||||||
|
def sleepUntil(self, due):
|
||||||
|
delay = self.t0 + due - time.time()
|
||||||
|
if delay > 0:
|
||||||
|
time.sleep(delay)
|
||||||
|
|
||||||
def findWin(self):
|
def findWin(self):
|
||||||
r = subprocess.run(["xdotool", "search", "--name", "."], env=self.env, capture_output=True, text=True)
|
r = subprocess.run(["xdotool", "search", "--name", "."], env=self.env, capture_output=True, text=True)
|
||||||
|
|
@ -110,14 +158,16 @@ class Dos:
|
||||||
subprocess.run(["xdotool"] + list(a), env=self.env, capture_output=True)
|
subprocess.run(["xdotool"] + list(a), env=self.env, capture_output=True)
|
||||||
|
|
||||||
def key(self, name, hold=KEY_HOLD):
|
def key(self, name, hold=KEY_HOLD):
|
||||||
|
self.sleepUntil(self.due)
|
||||||
self.xdo("keydown", "--window", self.win, name)
|
self.xdo("keydown", "--window", self.win, name)
|
||||||
time.sleep(hold)
|
self.sleepUntil(self.due + hold)
|
||||||
self.xdo("keyup", "--window", self.win, name)
|
self.xdo("keyup", "--window", self.win, name)
|
||||||
time.sleep(KEY_GAP)
|
self.due += hold + KEY_GAP
|
||||||
|
|
||||||
def typeText(self, text):
|
def typeText(self, text):
|
||||||
for ch in text:
|
for ch in text:
|
||||||
if ch.isupper():
|
if ch.isupper():
|
||||||
|
self.sleepUntil(self.due)
|
||||||
self.xdo("keydown", "--window", self.win, "shift")
|
self.xdo("keydown", "--window", self.win, "shift")
|
||||||
self.key(ch.lower())
|
self.key(ch.lower())
|
||||||
self.xdo("keyup", "--window", self.win, "shift")
|
self.xdo("keyup", "--window", self.win, "shift")
|
||||||
|
|
@ -125,6 +175,7 @@ class Dos:
|
||||||
self.key(KEYMAP.get(ch, ch))
|
self.key(KEYMAP.get(ch, ch))
|
||||||
|
|
||||||
def shot(self, name):
|
def shot(self, name):
|
||||||
|
self.sleepUntil(self.due)
|
||||||
before = set(glob.glob(os.path.join(self.work, "cap", "*.png")))
|
before = set(glob.glob(os.path.join(self.work, "cap", "*.png")))
|
||||||
self.xdo("key", "--window", self.win, "ctrl+F5")
|
self.xdo("key", "--window", self.win, "ctrl+F5")
|
||||||
for _ in range(SHOT_POLLS):
|
for _ in range(SHOT_POLLS):
|
||||||
|
|
@ -138,6 +189,7 @@ class Dos:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def record(self, name):
|
def record(self, name):
|
||||||
|
self.sleepUntil(self.due)
|
||||||
self.recording = name
|
self.recording = name
|
||||||
self.xdo("key", "--window", self.win, VIDEO_KEY)
|
self.xdo("key", "--window", self.win, VIDEO_KEY)
|
||||||
|
|
||||||
|
|
@ -154,6 +206,7 @@ class Dos:
|
||||||
if size == last:
|
if size == last:
|
||||||
shutil.move(videos[0], os.path.join(self.out, self.recording + ".avi"))
|
shutil.move(videos[0], os.path.join(self.out, self.recording + ".avi"))
|
||||||
self.recording = None
|
self.recording = None
|
||||||
|
self.t0 = time.time() - self.due
|
||||||
return True
|
return True
|
||||||
last = size
|
last = size
|
||||||
print("record %s: no video" % self.recording, file=sys.stderr)
|
print("record %s: no video" % self.recording, file=sys.stderr)
|
||||||
|
|
@ -168,8 +221,10 @@ class Dos:
|
||||||
os.remove(path)
|
os.remove(path)
|
||||||
if not showing:
|
if not showing:
|
||||||
return
|
return
|
||||||
|
# The Return is pressed within the gap, as on the host.
|
||||||
|
start = self.due
|
||||||
self.key("Return")
|
self.key("Return")
|
||||||
time.sleep(CLEAR_GAP)
|
self.due = start + CLEAR_GAP
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
try:
|
try:
|
||||||
|
|
@ -188,12 +243,18 @@ class Dos:
|
||||||
continue
|
continue
|
||||||
op, _, arg = line.partition(" ")
|
op, _, arg = line.partition(" ")
|
||||||
if op == "wait":
|
if op == "wait":
|
||||||
time.sleep(float(arg))
|
self.due += float(arg)
|
||||||
|
self.sleepUntil(self.due)
|
||||||
elif op == "key":
|
elif op == "key":
|
||||||
parts = arg.split()
|
parts = arg.split()
|
||||||
self.key(parts[0], float(parts[1]) if len(parts) > 1 else KEY_HOLD)
|
self.key(parts[0], float(parts[1]) if len(parts) > 1 else KEY_HOLD)
|
||||||
elif op == "type":
|
elif op == "type":
|
||||||
self.typeText(arg)
|
self.typeText(arg)
|
||||||
|
elif op == "say":
|
||||||
|
self.typeText(arg)
|
||||||
|
self.key("Return")
|
||||||
|
self.due += SAY_SETTLE
|
||||||
|
self.clearWindows()
|
||||||
elif op == "shot":
|
elif op == "shot":
|
||||||
self.shot(arg)
|
self.shot(arg)
|
||||||
elif op == "clear":
|
elif op == "clear":
|
||||||
|
|
@ -203,8 +264,10 @@ class Dos:
|
||||||
elif op == "stop":
|
elif op == "stop":
|
||||||
self.stopRecording()
|
self.stopRecording()
|
||||||
elif op == "down":
|
elif op == "down":
|
||||||
|
self.sleepUntil(self.due)
|
||||||
self.xdo("keydown", "--window", self.win, arg)
|
self.xdo("keydown", "--window", self.win, arg)
|
||||||
elif op == "up":
|
elif op == "up":
|
||||||
|
self.sleepUntil(self.due)
|
||||||
self.xdo("keyup", "--window", self.win, arg)
|
self.xdo("keyup", "--window", self.win, arg)
|
||||||
else:
|
else:
|
||||||
raise ValueError("bad script op " + op)
|
raise ValueError("bad script op " + op)
|
||||||
|
|
|
||||||
12
tests/xmas/start.scr
Normal file
12
tests/xmas/start.scr
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# The card: the greeting, the message prompt (Return for none), and the
|
||||||
|
# fireplace.
|
||||||
|
wait 4
|
||||||
|
shot x01
|
||||||
|
key Return
|
||||||
|
wait 2
|
||||||
|
shot x02
|
||||||
|
key Return
|
||||||
|
wait 6
|
||||||
|
shot x03
|
||||||
|
wait 8
|
||||||
|
shot x04
|
||||||
Loading…
Add table
Reference in a new issue