JoeyLib AGI now it's own project.
This commit is contained in:
parent
96c72ca4ba
commit
cfda4c52db
68 changed files with 11221 additions and 8142 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -6,6 +6,10 @@ build/
|
|||
games/*
|
||||
!games/README.md
|
||||
|
||||
# Oracle and JoeyAGI frames of the games (STATUS.md): pictures of Sierra's
|
||||
# games, never committed.
|
||||
screenshots/
|
||||
|
||||
__pycache__/
|
||||
|
||||
# Editor / OS cruft
|
||||
|
|
|
|||
22
README.md
22
README.md
|
|
@ -71,9 +71,29 @@ $ scripts/verify.sh host # or name the ones to run
|
|||
|
||||
| Gate | What it proves |
|
||||
|------|----------------|
|
||||
| `host` | The resource loader, the PICTURE and VIEW decoders, the LOGIC VM and the VM pipeline, built for the host against JoeyLib's headers and run on every game in `games/` (`scripts/test-agi.sh`) |
|
||||
| `host` | The whole interpreter, built for the build machine on JoeyLib's headless port, plays scripted sessions and every frame asked for must hash as expected: every resource of every game in `games/` loads and decodes (KQ3's pictures hash as Sierra's interpreter draws them); twenty probe games built from scratch by `tests/probes.py` (pictures, windows, motion, control lines, priority, the parser, menus, inventory, sound timing, ...); and, with `games/kq3`, four KQ3 sessions (the opening, play, save/restore/restart, debug teleports) (`scripts/test-agi.sh`) |
|
||||
| `disk` | `AGI.2mg` with King's Quest III boots on a IIgs, launches the interpreter and draws |
|
||||
|
||||
`scripts/verify.sh` exits 0 when every gate passed, 1 when one failed, and 3
|
||||
when none failed but one was skipped (no game in `games/`, or the IIgs
|
||||
toolchain or emulator missing).
|
||||
|
||||
The expected hashes live in `tests/expected/`; `scripts/test-agi.sh --update`
|
||||
records new ones once a change has been checked. Every run leaves its frames
|
||||
in `build/tests/out/`.
|
||||
|
||||
## Checking against Sierra's interpreter
|
||||
|
||||
```console
|
||||
$ scripts/oracle.sh # every probe and KQ3 session
|
||||
$ scripts/oracle.sh windows parser # or some of them
|
||||
```
|
||||
|
||||
`scripts/oracle.sh` plays the probes (and `tests/kq3/*.scr`) on Sierra's own
|
||||
interpreter -- the copy in `games/kq3`, in DOSBox Staging at 386SX-16 speed --
|
||||
and on JoeyAGI, then compares the frames with `tests/tools/compareFrames.py`,
|
||||
which reads text with each interpreter's font (JoeyAGI's is its own). The
|
||||
probes record what happened into variables and print them, so their frames
|
||||
match whatever the timing; the KQ3 sessions run in real time on one side and
|
||||
virtual time on the other, so moving things differ by a cycle or two. STATUS.md
|
||||
has the results.
|
||||
|
|
|
|||
527
STATUS.md
Normal file
527
STATUS.md
Normal file
|
|
@ -0,0 +1,527 @@
|
|||
# JoeyAGI status
|
||||
|
||||
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
|
||||
(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
|
||||
Staging at 386SX-16 speed) run as a black box for everything the
|
||||
specifications leave open. No ScummVM, NAGI or Sarien source was read.
|
||||
|
||||
## In short
|
||||
|
||||
- **King's Quest III plays.** The opening runs in step with Sierra's (title,
|
||||
credits over Manannan, the house, the story windows: checked with single
|
||||
screenshots at 9, 17 and 84 seconds); play starts in the hall with walking,
|
||||
typing, the parser and its unknown-word replies, the menus, the inventory
|
||||
screen, the F1 help screen, save, restore, restart and KQ3's debug mode and
|
||||
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,
|
||||
the chickens, wandering), because DOSBox runs in real time and the harness in
|
||||
virtual time, and the two random generators differ.
|
||||
- **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
|
||||
every drawing action and all 16 pens, and a long diagonal that shows
|
||||
Sierra's byte-sized line error (see "Interpreter versions and quirks").
|
||||
- **shake.screen is Sierra's,** measured frame by frame from a recording of
|
||||
Sierra's interpreter and reproduced beat for beat with JoeyLib's existing
|
||||
API (see "shake.screen").
|
||||
- **Sierra's window and input-line quirks are reproduced:** a print.at
|
||||
window at column 0 or 1 is drawn and only partly erased as Sierra's
|
||||
byte arithmetic does it, and set.cursor.char changed mid-line moves the
|
||||
echo as on Sierra's. Both, and the picture-line quirk, are gated on the
|
||||
interpreter version read from the game's AGIDATA.OVL.
|
||||
- **23 probe games, 93 frames and one recording: 89 match Sierra
|
||||
exactly.** The others are follow.ego's random detours (its deterministic
|
||||
half matches) and the clock probe's four frames, which all show one
|
||||
measurement: a 1-second timed window that closes one cycle (50 ms) later
|
||||
than Sierra's (the 3-, 2- and half-second ones match).
|
||||
- **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
|
||||
(`screenshots/port-*.png`).
|
||||
- **Gates:** `make check` passes: host 28/28 (resources, 23 probes, 4 KQ3
|
||||
sessions), disk PASS.
|
||||
|
||||
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,
|
||||
and speed on the 8 MHz machines.
|
||||
|
||||
## How it was checked
|
||||
|
||||
| Tool | What it does |
|
||||
|------|--------------|
|
||||
| `tests/hostPort.c`, `tests/agiHost.c` | The whole interpreter on the build machine (JoeyLib's BLANK port): virtual 60 Hz time, a scripted keyboard, every frame resolved through the palette and hashed (FNV-1a) or saved as PPM. Fully reproducible. |
|
||||
| `tests/agiGame.py`, `tests/probes.py` | Builds AGI games from scratch (LOGIC assembler from the spec's command table, PICTURE byte lists, VIEW from text art, SOUND, WORDS.TOK, OBJECT). Each probe records what the interpreter did cycle by cycle into variables and prints them, so its frames do not depend on timing. |
|
||||
| `scripts/oracle.sh` | Runs a probe (or a KQ3 session) on Sierra's interpreter in DOSBox under a private Xvfb, driven by xdotool, and on JoeyAGI, then compares every shot with `tests/tools/compareFrames.py` (EGA colours, text read with each interpreter's own font -- JoeyAGI's font is its own -- and Sierra's 40-column text mode compared cell by cell). A probe whose script says `record` is recorded instead: DOSBox's video capture (every 70 Hz frame) on Sierra's side, every frame on JoeyAGI's, compared by `tests/tools/shakeMeasure.py`. |
|
||||
| `scripts/test-agi.sh` | The host gate: resources, probes and KQ3 sessions against `tests/expected/`. |
|
||||
|
||||
A caution learned on the way: DOSBox's screenshot key (Ctrl+F5) reaches the
|
||||
game as a key press, and KQ3's opening hurries along when it sees one. The KQ3
|
||||
checkpoints below were each taken with a single screenshot.
|
||||
|
||||
## The state found
|
||||
|
||||
The interpreter at the start (commit 96c72ca) was a VM/host split with many
|
||||
pieces stubbed: `sound` set its flag at once; `save.game`, `restore.game`,
|
||||
`status`, `show.obj`, `menu.input`, `obj.status.v` did nothing;
|
||||
`restart.game` quit; object collisions and `block` were not modelled;
|
||||
`posn` tests were approximations; ego stepped a fixed 2 pixels; and an argv
|
||||
"starting room" let you skip "a title screen the VM can't yet advance
|
||||
through". The IIgs disk gate passed, but on the IIgs the old engine was the
|
||||
one drawing.
|
||||
|
||||
Nearly every module was rewritten (agi.c, agiRes, agiPic, agiView, agiObj,
|
||||
agiText, agiVm) or written new (agiGfx, agiFont, agiInput, agiSound,
|
||||
agiSave), each behaviour settled against the specifications and then against
|
||||
Sierra's interpreter.
|
||||
|
||||
## Subsystems
|
||||
|
||||
| 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 |
|
||||
| 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) |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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) |
|
||||
| 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) |
|
||||
| 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` |
|
||||
| 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) |
|
||||
| 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` |
|
||||
| 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 |
|
||||
| Display on planar machines | Picture updates go out as colour runs through jlFillRect (two jlDrawPixel calls per AGI pixel before) | host frames identical either way; ST/Amiga/X68000 snapshots |
|
||||
|
||||
## Bugs found and fixed in this dive, with causes
|
||||
|
||||
1. **IIgs: blank screen.** Every resource came from `malloc`, and on the IIgs
|
||||
the C heap is the small window left in the entry bank (1926 bytes with 22 KB
|
||||
of BSS). Logic 0 could not be allocated, so nothing ran. All interpreter
|
||||
memory now comes from jlAlloc (the Memory Manager); files are read into
|
||||
buffers of their exact size; a view is one allocation; BSS fell from 22 KB
|
||||
to 4 KB (byte-sized fill stack, one shared line buffer, the picture script
|
||||
moved into the jlAlloc'd state).
|
||||
2. **ST, Amiga, X68000: picture output.** Planar stages have no chunky bytes,
|
||||
and the fallback drew two jlDrawPixel calls per AGI pixel. Now runs of one
|
||||
colour per row through jlFillRect. (An 8x8 tile path was tried first and
|
||||
came out striped: jlTileT's bytes are in each port's own layout.)
|
||||
3. **Text attributes.** set.text.attribute in picture mode stores a background
|
||||
of white (or black); text.screen clears to that; on the text screen the
|
||||
colours are used as given; clear.lines and clear.text.rect on the text
|
||||
screen take the colour as a PC attribute byte (so 0..15 clear to black).
|
||||
The old rule (background | 8) was wrong.
|
||||
4. **Sound on at start.** Sierra's interpreter sets f9 itself (its status line
|
||||
reads "Sound:on" in a game that never touches f9).
|
||||
5. **Input line.** Default row 23, not 22; no cursor before set.cursor.char;
|
||||
typing draws only the changed cell, so text displayed on the input row stays
|
||||
until Enter or accept.input redraws the line.
|
||||
6. **Unknown words.** said() failed whenever v9 was set; Sierra keeps the
|
||||
unknown word in the list, where any-word and rest-of-line match it, so
|
||||
"open the door now" matches said(open, rol). A line of only ignored words
|
||||
no longer sets f2, and v9 is cleared each cycle.
|
||||
7. **Ego steering.** v6 is copied to ego's direction before and after logic 0
|
||||
in both control modes (the old code copied ego's direction into v6 under
|
||||
player control, so set.dir moved ego); v6 set by a logic now moves ego that
|
||||
same cycle.
|
||||
8. **reposition / reposition.to** skip the object's step in that cycle.
|
||||
9. **Sound flag** came after the longest voice; Sierra's comes after the first
|
||||
voice. This alone put KQ3's opening seconds behind (its title waits on the
|
||||
tune's flag).
|
||||
10. **Restore** let the rest of the cycle run (objects stepped once past the
|
||||
saved state) and cleared f12 before any logic saw it; KQ3's logic 0 tests
|
||||
f12 after a restore to fix up its menu.
|
||||
11. **Restart** wiped the menus and set.key mappings, but KQ3 builds both only
|
||||
on a first start (f6 clear), so after a restart Esc and the function keys
|
||||
did nothing.
|
||||
12. **Resource memory** grew without bound (every logic and view ever loaded
|
||||
stayed); now freed at new.room.
|
||||
13. **Games without SNDDIR** (or any empty directory) failed to start.
|
||||
14. **Frame order.** Each idle frame polled the keys, pumped the sound and
|
||||
ticked the clock *before* waiting for the vertical blank, so a cycle that
|
||||
started right after the wait saw them a frame stale: a short sound's flag
|
||||
arrived a cycle late. The frame now passes first.
|
||||
15. **have.key** let a frame pass every time a logic asked, and took any key
|
||||
that arrived then -- so a probe asking once a cycle swallowed the arrow
|
||||
key meant to steer ego. Now only a logic asking twice in one cycle
|
||||
waits.
|
||||
16. **shake.screen** was a pause. It now shakes as Sierra's does (below).
|
||||
17. **print.at at column 0 or 1** drew a clipped window and erased it
|
||||
whole; Sierra's interpreter draws and erases it displaced (below).
|
||||
18. **Window save-under** went through jlSurfaceCopyRect, which widens a
|
||||
copy to 16-pixel groups, so closing a window also put back up to 15
|
||||
stale pixels on each side: harmless while nothing beside an open window
|
||||
changed, wrong for the column-1 window, whose own frame and text sit
|
||||
there. Now exact: a chunky surface copies bytes (an AGI pixel each), a
|
||||
planar one copies the whole groups and the pixels at the ends one by
|
||||
one. Checked on all five machine ports against the host frame (0
|
||||
pixels differ on ST, Amiga, X68000, DOS and IIgs).
|
||||
19. **set.cursor.char mid-line** echoed from the end of the typed text;
|
||||
Sierra's echo keeps its own position (below).
|
||||
20. **Picture lines** were exact to the fraction; Sierra's interpreter
|
||||
carries the error in a byte, which a line long on both sides overflows
|
||||
(below). No KQ3 picture has such a line.
|
||||
|
||||
Fixed earlier in the dive (before the gates existed), all verified against
|
||||
Sierra's interpreter: the picture decoder (line algorithm, fill semantics,
|
||||
pens, splatter textures); window geometry and the wrap rule; display wrapping;
|
||||
the status line; menus; the inventory layout; show.obj placement; add.to.pic
|
||||
and its margin boxes; sprite priority masking; control-line blocking; object
|
||||
collisions; the position-fixing spiral; motion and cycle counter timing;
|
||||
move.obj arrival; follow.ego; set.view/set.loop resets of missing loops and
|
||||
cels; animate.obj leaving step and cycle settings alone; %-codes in messages
|
||||
(%v with |width, %s, %w, %0, %m, %g); the parser's longest match; the intro's
|
||||
timing; v21 window timeouts; a guard for KQ3's room 43 call.v(0) recursion
|
||||
(Sierra hangs there); Alt and Ctrl chords no longer type characters.
|
||||
|
||||
## shake.screen
|
||||
|
||||
Measured on Sierra's interpreter from DOSBox's video capture, every 70 Hz
|
||||
VGA frame of probe games calling shake.screen(1), (2) and (3) (the `shake`
|
||||
probe) and (64), (65) and (0) (one-off probes); `tests/tools/shakeMeasure.py`
|
||||
reads the shakes out of a recording:
|
||||
|
||||
- **What moves:** the whole screen -- picture, status line, displayed text
|
||||
and the input line -- by exactly 8 screen pixels right (4 AGI pixels) and
|
||||
4 rows down, a plain shift; the uncovered strips are black. No other
|
||||
offset or direction occurs.
|
||||
- **A beat** is the moved screen for 4 frames, then the screen in place for
|
||||
4, locked to the vertical blank (114 ms at 70 Hz).
|
||||
- **Beats:** 4 per unit: 1, 2, 3 gave 4, 8, 12 beats; 64 gave 256 and 65
|
||||
gave 260, so the count is 16 bits, and shake.screen(0) ran past 1090
|
||||
beats before the recording stopped (65536 beats, about two hours).
|
||||
- **It blocks:** no cycles run and nothing animates until it ends.
|
||||
|
||||
JoeyAGI (`gfxShakeScreen` in agiGfx.c, JoeyLib's existing API, no per-port
|
||||
code): it copies the stage to one surface and builds a second with that
|
||||
screen moved (8, 4) over black (a chunky surface moves its bytes; a planar
|
||||
one draws the screen as one 40x25-tile sprite at (8, 4)); each beat copies
|
||||
the moved one to the stage, presents and lets 4 display frames pass
|
||||
(timed with jlMillisElapsed against jlFrameHz, so a slow copy is not added
|
||||
on top), then the same with the screen in place. shake.screen(0) is 65536
|
||||
beats, as on Sierra's. If the two surfaces cannot be allocated the shake is
|
||||
a pause of the same length. The host's recording reads the same as
|
||||
Sierra's: 4, 8 and 12 beats, moved 4 frames, in place 4 frames, offset
|
||||
(8, 4); the host gate pins every frame of it.
|
||||
|
||||
**IIgs cost:** a phase is a 32 KB jlSurfaceCopy into the stage (about
|
||||
108 ms) and a full-screen present (about 74 ms): 11 frames (183 ms) where
|
||||
Sierra's takes 4, so a beat is 367 ms against Sierra's 133 ms at 60 Hz and
|
||||
a shake lasts 2.75 times as long. The other ports were not timed.
|
||||
|
||||
**A JoeyLib addition that would make it much better (not made):** a
|
||||
display offset -- show the stage moved by (dx, dy), uncovered edges black
|
||||
-- as one call (say `jlStagePresentOffset(dx, dy)`). Where the hardware
|
||||
scrolls it is free: Amiga bitplane pointers plus BPLCON1, the X68000's
|
||||
graphic scroll registers, the VGA CRTC start address; the ST (an STF's
|
||||
video base moves in 256-byte steps) and the IIgs (no hardware scroll) would
|
||||
present the stage shifted, one present's worth of copying instead of a
|
||||
32 KB copy and a present -- about halving the IIgs's beat.
|
||||
|
||||
## Interpreter versions and quirks
|
||||
|
||||
KQ3's AGIDATA.OVL names its interpreter ("Adventure Game Interpreter",
|
||||
"Version 2.435"); the AGI executable carries no readable version (2.00 is
|
||||
KQ3's own version, shown by the game). At startup `detectVersion`
|
||||
(agiRes.c) reads the version from AGIDATA.OVL, else from the AGI file, else
|
||||
takes 2.435, the version measured, into `game.interpreterVersion`;
|
||||
`agiQuirk()` answers from one table of version ranges (`kQuirkVersions`),
|
||||
the one place per-version behaviour is decided. The IIgs disk now carries
|
||||
AGIDATA.OVL, and `resources-kq3` checks KQ3 reads as 2435.
|
||||
|
||||
Only one Sierra interpreter, 2.435, was at hand, so whether the quirks
|
||||
differ between versions could not be measured: all three are on for 2.000
|
||||
to 2.999 (every version JoeyAGI plays), and narrowing a range is a one-line
|
||||
change once another interpreter is measured. Off, each falls back to the
|
||||
plain behaviour (a clipped window, an echo from the end of the typed text,
|
||||
an exact line).
|
||||
|
||||
- **AGI_QUIRK_WINDOW_BYTE_X -- print.at at column 0 or 1.** A window's box
|
||||
is x = 4 * col - 5 AGI pixels, y = 8 * row - 5 screen rows, 4 * width + 10
|
||||
by 8 * lines + 10, the red frame one pixel in; its sides stop a row short
|
||||
of the top and bottom lines. Saving the screen under it, the white fill,
|
||||
the frame and putting the screen back all address the screen as one line
|
||||
of 160-pixel rows with x in a byte (row * 160 + (x & 255)), each span
|
||||
running on into the next row; the text is drawn where it belongs. At
|
||||
column 1 (x = -1, so 255) the save, fill and restore land a row down at
|
||||
x 95 and spill into the row below from x 0, while the frame (x + 1 = 0)
|
||||
and the text are in place: the window opens with its white running off to
|
||||
the right, and closing it erases only its leftmost 4 * width - 55 pixels,
|
||||
leaving the rest of the frame and text on screen. At column 0 (x = 251)
|
||||
the box and its top, bottom and left lines are drawn a row down, 91 in,
|
||||
and the right side and the text stay in place. Columns 2 and on are not
|
||||
affected. `winedge` probe: nine windows at columns 0 to 3, one to three
|
||||
lines, on a coloured picture, open and closed: 18/18 frames identical.
|
||||
- **AGI_QUIRK_ECHO_STEPS_BACK -- set.cursor.char mid-line.** The input line
|
||||
echo keeps its own position: a redraw (Enter, accept.input) leaves it
|
||||
after the prompt, the text and the cursor; with a cursor set, a typed
|
||||
character first steps back one -- whether or not a cursor was drawn there
|
||||
-- then draws itself and the cursor; Backspace steps back two and draws
|
||||
the cursor and a blank (without a cursor: back one, a blank). So a cursor
|
||||
set mid-line makes the next character overwrite the last one typed
|
||||
(">w", then '*' set and "yz" typed, shows ">yz*" while the line holds
|
||||
"wyz"), a cursor taken away stays on the line (">pq_" then "r" shows
|
||||
">pq_r"), and rubbing that line out with the cursor set runs back over
|
||||
the prompt (three Backspaces leave just "*"). `cursor` probe: 20/20
|
||||
frames identical.
|
||||
- **AGI_QUIRK_LINE_BYTE_ERROR -- picture lines** (found by the `winedge`
|
||||
picture): the line's error term is a byte, so a line long on both sides
|
||||
loses steps when it passes 255: (0,0) to (159,167) stalls two rows in ten
|
||||
and ends at (134,167). No KQ3 picture has such a line (all 194 frames are
|
||||
unchanged).
|
||||
|
||||
## What remains, most important first
|
||||
|
||||
1. **A full play-through.** KQ3 has not been finished on JoeyAGI. The rooms,
|
||||
puzzles, spells, deaths and endings past the house are untested beyond
|
||||
their resources decoding and a few teleports.
|
||||
2. **Speed on the 8 MHz machines.** Not measured. The run-length output is
|
||||
much faster than before; picture decoding and flood fill run in C.
|
||||
3. **Audio by ear.** Note timing and the flag are exact; how the four voices
|
||||
and noise sound on each port's JoeyLib audio has not been judged.
|
||||
4. **The IIgs beyond booting.** The disk gate proves it boots and draws; the
|
||||
opening was checked by framebuffer dumps, play on the IIgs was not.
|
||||
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
|
||||
format, which is per machine type (byte order and padding).
|
||||
6. **Random sequences** differ from Sierra's (wander, follow.ego detours,
|
||||
KQ3's Manannan schedule): expected, not a bug.
|
||||
7. **Other interpreter versions.** Only 2.435 was available to measure; the
|
||||
quirks are assumed for every 2.x interpreter (see below).
|
||||
8. **The font** is JoeyAGI's own, not the PC's ROM font.
|
||||
9. **shake.screen on the IIgs** takes 2.75 times as long as on Sierra's
|
||||
(see below); a JoeyLib display offset would fix it.
|
||||
10. **The flood fill's seed stack** (1024 seeds) would truncate a fill that
|
||||
needs more; no KQ3 picture does.
|
||||
|
||||
## Commands
|
||||
|
||||
Every AGI v2 command is implemented. "KQ3 uses" counts the command in KQ3's
|
||||
logics; "Probe" marks commands the probe games exercise against Sierra's
|
||||
interpreter (everything KQ3 uses is also exercised by the KQ3 sessions or
|
||||
checkpoints). FIXED marks behaviour corrected in this dive's later half;
|
||||
the earlier rewrite touched nearly every command.
|
||||
|
||||
| # | Command | KQ3 uses | Probe | Status |
|
||||
|---|---------|---------:|:-----:|--------|
|
||||
| 0x00 | return | 262 | yes | works |
|
||||
| 0x01 | increment | 186 | yes | works |
|
||||
| 0x02 | decrement | 144 | | works |
|
||||
| 0x03 | assignn | 2659 | yes | works |
|
||||
| 0x04 | assignv | 111 | yes | works |
|
||||
| 0x05 | addn | 106 | | works |
|
||||
| 0x06 | addv | 10 | | works |
|
||||
| 0x07 | subn | 29 | yes | works |
|
||||
| 0x08 | subv | 7 | | works |
|
||||
| 0x09 | lindirectv | 1 | | works |
|
||||
| 0x0A | rindirect | 1 | | works |
|
||||
| 0x0B | lindirectn | 1 | | works |
|
||||
| 0x0C | set | 923 | yes | works |
|
||||
| 0x0D | reset | 737 | yes | works |
|
||||
| 0x0E | toggle | 12 | | works |
|
||||
| 0x0F | set.v | 4 | | works |
|
||||
| 0x10 | reset.v | 5 | | works |
|
||||
| 0x11 | toggle.v | 0 | | works |
|
||||
| 0x12 | new.room | 263 | | works |
|
||||
| 0x13 | new.room.v | 14 | | works |
|
||||
| 0x14 | load.logics | 135 | | works |
|
||||
| 0x15 | load.logics.v | 1 | | works |
|
||||
| 0x16 | call | 253 | | works |
|
||||
| 0x17 | call.v | 5 | | works |
|
||||
| 0x18 | load.pic | 98 | yes | no-op (draw.pic loads) |
|
||||
| 0x19 | draw.pic | 102 | yes | works |
|
||||
| 0x1A | show.pic | 109 | yes | works |
|
||||
| 0x1B | discard.pic | 91 | yes | no-op (pictures are not kept) |
|
||||
| 0x1C | overlay.pic | 9 | | works |
|
||||
| 0x1D | show.pri.screen | 1 | yes | works |
|
||||
| 0x1E | load.view | 375 | yes | works |
|
||||
| 0x1F | load.view.v | 84 | | works |
|
||||
| 0x20 | discard.view | 11 | | no-op: views go at new.room |
|
||||
| 0x21 | animate.obj | 200 | yes | works |
|
||||
| 0x22 | unanimate.all | 2 | | works |
|
||||
| 0x23 | draw | 318 | yes | works |
|
||||
| 0x24 | erase | 153 | yes | works |
|
||||
| 0x25 | position | 306 | yes | works |
|
||||
| 0x26 | position.v | 112 | | works |
|
||||
| 0x27 | get.posn | 48 | yes | works |
|
||||
| 0x28 | reposition | 12 | yes | works; FIXED (skips that cycle's step) |
|
||||
| 0x29 | set.view | 477 | yes | works |
|
||||
| 0x2A | set.view.v | 84 | | works |
|
||||
| 0x2B | set.loop | 227 | yes | works |
|
||||
| 0x2C | set.loop.v | 7 | | works |
|
||||
| 0x2D | fix.loop | 44 | yes | works |
|
||||
| 0x2E | release.loop | 19 | | works |
|
||||
| 0x2F | set.cel | 219 | yes | works |
|
||||
| 0x30 | set.cel.v | 18 | | works |
|
||||
| 0x31 | last.cel | 15 | | works |
|
||||
| 0x32 | current.cel | 23 | yes | works |
|
||||
| 0x33 | current.loop | 11 | yes | works |
|
||||
| 0x34 | current.view | 8 | yes | works |
|
||||
| 0x35 | number.of.loops | 0 | | works |
|
||||
| 0x36 | set.priority | 246 | yes | works |
|
||||
| 0x37 | set.priority.v | 11 | | works |
|
||||
| 0x38 | release.priority | 59 | | works |
|
||||
| 0x39 | get.priority | 10 | | works |
|
||||
| 0x3A | stop.update | 83 | yes | works |
|
||||
| 0x3B | start.update | 72 | | works |
|
||||
| 0x3C | force.update | 19 | | works |
|
||||
| 0x3D | ignore.horizon | 59 | | works |
|
||||
| 0x3E | observe.horizon | 5 | | works |
|
||||
| 0x3F | set.horizon | 57 | | works |
|
||||
| 0x40 | object.on.water | 6 | yes | works |
|
||||
| 0x41 | object.on.land | 2 | yes | works |
|
||||
| 0x42 | object.on.anything | 1 | | works |
|
||||
| 0x43 | ignore.objs | 126 | yes | works |
|
||||
| 0x44 | observe.objs | 15 | | works |
|
||||
| 0x45 | distance | 23 | | works |
|
||||
| 0x46 | stop.cycling | 150 | yes | works |
|
||||
| 0x47 | start.cycling | 100 | yes | works |
|
||||
| 0x48 | normal.cycle | 1 | | works |
|
||||
| 0x49 | end.of.loop | 100 | yes | works |
|
||||
| 0x4A | reverse.cycle | 0 | | works |
|
||||
| 0x4B | reverse.loop | 23 | yes | works |
|
||||
| 0x4C | cycle.time | 139 | yes | works |
|
||||
| 0x4D | stop.motion | 95 | yes | works |
|
||||
| 0x4E | start.motion | 72 | yes | works |
|
||||
| 0x4F | step.size | 19 | yes | works |
|
||||
| 0x50 | step.time | 8 | yes | works |
|
||||
| 0x51 | move.obj | 155 | yes | works |
|
||||
| 0x52 | move.obj.v | 59 | | works |
|
||||
| 0x53 | follow.ego | 30 | yes | works |
|
||||
| 0x54 | wander | 39 | | works |
|
||||
| 0x55 | normal.motion | 35 | | works |
|
||||
| 0x56 | set.dir | 6 | yes | works; FIXED (no effect on ego: v6 steers it) |
|
||||
| 0x57 | get.dir | 4 | yes | works |
|
||||
| 0x58 | ignore.blocks | 127 | yes | works |
|
||||
| 0x59 | observe.blocks | 52 | | works |
|
||||
| 0x5A | block | 10 | | works |
|
||||
| 0x5B | unblock | 6 | | works |
|
||||
| 0x5C | get | 155 | yes | works |
|
||||
| 0x5D | get.v | 2 | | works |
|
||||
| 0x5E | drop | 80 | | works |
|
||||
| 0x5F | put | 30 | | works |
|
||||
| 0x60 | put.v | 10 | | works |
|
||||
| 0x61 | get.room.v | 12 | | works |
|
||||
| 0x62 | load.sound | 111 | yes | no-op (sound loads) |
|
||||
| 0x63 | sound | 125 | yes | works; FIXED (flag when the first voice ends) |
|
||||
| 0x64 | stop.sound | 19 | yes | works; flag set |
|
||||
| 0x65 | print | 1695 | yes | works |
|
||||
| 0x66 | print.v | 27 | | works |
|
||||
| 0x67 | display | 70 | yes | works |
|
||||
| 0x68 | display.v | 5 | | works |
|
||||
| 0x69 | clear.lines | 17 | yes | works; FIXED (text-screen colour is an attribute byte) |
|
||||
| 0x6A | text.screen | 3 | yes | works; FIXED (clears to the attribute background) |
|
||||
| 0x6B | graphics | 3 | yes | works |
|
||||
| 0x6C | set.cursor.char | 1 | yes | works (no cursor before it: FIXED; changed mid-line moves the echo as Sierra's: FIXED) |
|
||||
| 0x6D | set.text.attribute | 31 | yes | works; FIXED (picture-mode background means white) |
|
||||
| 0x6E | shake.screen | 8 | yes | works; FIXED (measured: 4 beats per unit, (8, 4) for 4 frames and back for 4) |
|
||||
| 0x6F | configure.screen | 1 | | works |
|
||||
| 0x70 | status.line.on | 6 | yes | works |
|
||||
| 0x71 | status.line.off | 6 | | works |
|
||||
| 0x72 | set.string | 51 | | works |
|
||||
| 0x73 | get.string | 56 | | works |
|
||||
| 0x74 | word.to.string | 0 | | works |
|
||||
| 0x75 | parse | 0 | | works |
|
||||
| 0x76 | get.num | 13 | | works |
|
||||
| 0x77 | prevent.input | 75 | | works; clears the input line |
|
||||
| 0x78 | accept.input | 45 | yes | works; redraws the input line |
|
||||
| 0x79 | set.key | 39 | yes | works |
|
||||
| 0x7A | add.to.pic | 66 | yes | works |
|
||||
| 0x7B | add.to.pic.v | 8 | | works |
|
||||
| 0x7C | status | 2 | yes | works: the inventory screen |
|
||||
| 0x7D | save.game | 1 | | works: JoeyAGI's own slot window and file format |
|
||||
| 0x7E | restore.game | 1 | | works: JoeyAGI's own slot window; replays the picture script; FIXED (cycle ends, f12 reaches the next cycle) |
|
||||
| 0x7F | init.disk | 0 | | no-op (nothing to do on these machines) |
|
||||
| 0x80 | restart.game | 1 | | works; FIXED (keeps menus and set.key) |
|
||||
| 0x81 | show.obj | 61 | yes | works |
|
||||
| 0x82 | random | 200 | | works |
|
||||
| 0x83 | program.control | 80 | yes | works |
|
||||
| 0x84 | player.control | 44 | yes | works |
|
||||
| 0x85 | obj.status.v | 2 | | works: a debug window |
|
||||
| 0x86 | quit | 3 | | works |
|
||||
| 0x87 | show.mem | 1 | | works: a JoeyAGI message |
|
||||
| 0x88 | pause | 1 | | works: a message window |
|
||||
| 0x89 | echo.line | 1 | | works |
|
||||
| 0x8A | cancel.line | 1 | | works |
|
||||
| 0x8B | init.joy | 1 | | no-op (nothing to do on these machines) |
|
||||
| 0x8C | toggle.monitor | 1 | | no-op (nothing to do on these machines) |
|
||||
| 0x8D | version | 2 | | works: a JoeyAGI message |
|
||||
| 0x8E | script.size | 1 | | no-op (nothing to do on these machines) |
|
||||
| 0x8F | set.game.id | 1 | | no-op (nothing to do on these machines) |
|
||||
| 0x90 | log | 8 | | no-op (nothing to do on these machines) |
|
||||
| 0x91 | set.scan.start | 22 | | works |
|
||||
| 0x92 | reset.scan.start | 18 | | works |
|
||||
| 0x93 | reposition.to | 49 | yes | works; FIXED (skips that cycle's step) |
|
||||
| 0x94 | reposition.to.v | 58 | | works; FIXED (as reposition.to) |
|
||||
| 0x95 | trace.on | 0 | | no-op (nothing to do on these machines) |
|
||||
| 0x96 | trace.info | 1 | | no-op (nothing to do on these machines) |
|
||||
| 0x97 | print.at | 31 | yes | works; FIXED (columns 0 and 1 drawn and erased as Sierra's byte arithmetic does) |
|
||||
| 0x98 | print.at.v | 2 | | works (as print.at) |
|
||||
| 0x99 | discard.view.v | 0 | | no-op: views go at new.room |
|
||||
| 0x9A | clear.text.rect | 0 | | works; FIXED (as clear.lines) |
|
||||
| 0x9B | set.upper.left | 0 | | no-op (nothing to do on these machines) |
|
||||
| 0x9C | set.menu | 5 | yes | works |
|
||||
| 0x9D | set.menu.item | 21 | yes | works |
|
||||
| 0x9E | submit.menu | 1 | yes | works |
|
||||
| 0x9F | enable.item | 1 | | works |
|
||||
| 0xA0 | disable.item | 3 | yes | works |
|
||||
| 0xA1 | menu.input | 1 | yes | works |
|
||||
| 0xA2 | show.obj.v | 1 | | works |
|
||||
| 0xA3 | open.dialogue | 16 | | no-op (nothing to do on these machines) |
|
||||
| 0xA4 | close.dialogue | 7 | | no-op (nothing to do on these machines) |
|
||||
| 0xA5 | mul.n | 0 | | works |
|
||||
| 0xA6 | mul.v | 0 | | works |
|
||||
| 0xA7 | div.n | 0 | | works |
|
||||
| 0xA8 | div.v | 0 | | works |
|
||||
| 0xA9 | close.window | 0 | | works |
|
||||
|
||||
| # | Test | KQ3 uses | Probe | Status |
|
||||
|---|------|---------:|:-----:|--------|
|
||||
| 0x01 | equaln | 2281 | yes | works |
|
||||
| 0x02 | equalv | 35 | | works |
|
||||
| 0x03 | lessn | 157 | yes | works |
|
||||
| 0x04 | lessv | 12 | | works |
|
||||
| 0x05 | greatern | 449 | | works |
|
||||
| 0x06 | greaterv | 10 | | works |
|
||||
| 0x07 | isset | 1631 | yes | works |
|
||||
| 0x08 | isset.v | 1 | | works |
|
||||
| 0x09 | has | 362 | | works |
|
||||
| 0x0A | obj.in.room | 144 | | works |
|
||||
| 0x0B | posn | 307 | | works |
|
||||
| 0x0C | controller | 39 | yes | works |
|
||||
| 0x0D | have.key | 4 | yes | works |
|
||||
| 0x0E | said | 2991 | yes | works; FIXED (an unknown word only matches any-word and rest-of-line; all-ignored lines say nothing) |
|
||||
| 0x0F | compare.strings | 28 | | works |
|
||||
| 0x10 | obj.in.box | 5 | | works |
|
||||
| 0x11 | center.posn | 0 | | works |
|
||||
| 0x12 | right.posn | 0 | | works |
|
||||
|
||||
## Gates
|
||||
|
||||
`make check` (build every port, then `scripts/verify.sh`):
|
||||
|
||||
| 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) |
|
||||
| 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
|
||||
games/kq3, Xvfb, xdotool): probes 89 of 94 shots identical to Sierra's
|
||||
(the rest: follow.ego's random detours, and the clock probe's 1-second
|
||||
window one cycle long, shown on all four of its frames), including the
|
||||
shake recording and the new `winedge` (18/18) and `cursor` (20/20) probes.
|
||||
Its KQ3 sessions (several shots each) drift from Sierra's with real time
|
||||
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.
|
||||
|
||||
## Screenshots
|
||||
|
||||
`screenshots/` (git-ignored: they show Sierra's game) holds Sierra's frame and
|
||||
JoeyAGI's side by side for the probes (`probe-*.png`) and the KQ3 checkpoints
|
||||
(`kq3-*.png`), and the ports running KQ3 (`port-*.png`).
|
||||
104
agiFont.c
Normal file
104
agiFont.c
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
// The text font: an 8x8 cell per character from space (0x20) to tilde
|
||||
// (0x7E), one byte per row with the leftmost pixel in bit 7. Drawn for
|
||||
// JoeyAGI (it is not the IBM PC's ROM font Sierra's interpreter shows).
|
||||
|
||||
#include "agi.h"
|
||||
|
||||
|
||||
const uint8_t kAgiFont[AGI_FONT_GLYPHS][AGI_FONT_ROWS] = {
|
||||
{ 0, 0, 0, 0, 0, 0, 0, 0 }, // 0x20 ' '
|
||||
{ 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x18, 0x00 }, // 0x21 '!'
|
||||
{ 0x6C, 0x6C, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x22 '"'
|
||||
{ 0x36, 0x36, 0x7F, 0x36, 0x7F, 0x36, 0x36, 0x00 }, // 0x23 '#'
|
||||
{ 0x18, 0x3E, 0x60, 0x3C, 0x06, 0x7C, 0x18, 0x00 }, // 0x24 '$'
|
||||
{ 0x66, 0x66, 0x0C, 0x18, 0x30, 0x66, 0x66, 0x00 }, // 0x25 '%'
|
||||
{ 0x38, 0x6C, 0x38, 0x76, 0xDC, 0xCC, 0x76, 0x00 }, // 0x26 '&'
|
||||
{ 0x18, 0x18, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x27 '\''
|
||||
{ 0x0C, 0x18, 0x30, 0x30, 0x30, 0x18, 0x0C, 0x00 }, // 0x28 '('
|
||||
{ 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x18, 0x30, 0x00 }, // 0x29 ')'
|
||||
{ 0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00 }, // 0x2A '*'
|
||||
{ 0x00, 0x18, 0x18, 0x7E, 0x18, 0x18, 0x00, 0x00 }, // 0x2B '+'
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x30 }, // 0x2C ','
|
||||
{ 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x00 }, // 0x2D '-'
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00 }, // 0x2E '.'
|
||||
{ 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC0, 0x80, 0x00 }, // 0x2F '/'
|
||||
{ 0x3C, 0x66, 0x6E, 0x76, 0x66, 0x66, 0x3C, 0x00 }, // 0x30 '0'
|
||||
{ 0x18, 0x38, 0x18, 0x18, 0x18, 0x18, 0x7E, 0x00 }, // 0x31 '1'
|
||||
{ 0x3C, 0x66, 0x06, 0x1C, 0x30, 0x66, 0x7E, 0x00 }, // 0x32 '2'
|
||||
{ 0x3C, 0x66, 0x06, 0x1C, 0x06, 0x66, 0x3C, 0x00 }, // 0x33 '3'
|
||||
{ 0x0C, 0x1C, 0x3C, 0x6C, 0x7E, 0x0C, 0x0C, 0x00 }, // 0x34 '4'
|
||||
{ 0x7E, 0x60, 0x7C, 0x06, 0x06, 0x66, 0x3C, 0x00 }, // 0x35 '5'
|
||||
{ 0x1C, 0x30, 0x60, 0x7C, 0x66, 0x66, 0x3C, 0x00 }, // 0x36 '6'
|
||||
{ 0x7E, 0x66, 0x0C, 0x18, 0x18, 0x18, 0x18, 0x00 }, // 0x37 '7'
|
||||
{ 0x3C, 0x66, 0x66, 0x3C, 0x66, 0x66, 0x3C, 0x00 }, // 0x38 '8'
|
||||
{ 0x3C, 0x66, 0x66, 0x3E, 0x06, 0x0C, 0x38, 0x00 }, // 0x39 '9'
|
||||
{ 0x00, 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x00 }, // 0x3A ':'
|
||||
{ 0x00, 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x30 }, // 0x3B ';'
|
||||
{ 0x0C, 0x18, 0x30, 0x60, 0x30, 0x18, 0x0C, 0x00 }, // 0x3C '<'
|
||||
{ 0x00, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0x00, 0x00 }, // 0x3D '='
|
||||
{ 0x30, 0x18, 0x0C, 0x06, 0x0C, 0x18, 0x30, 0x00 }, // 0x3E '>'
|
||||
{ 0x3C, 0x66, 0x06, 0x0C, 0x18, 0x00, 0x18, 0x00 }, // 0x3F '?'
|
||||
{ 0x3C, 0x66, 0x6E, 0x6E, 0x60, 0x62, 0x3C, 0x00 }, // 0x40 '@'
|
||||
{ 0x18, 0x3C, 0x66, 0x66, 0x7E, 0x66, 0x66, 0x00 }, // 0x41 'A'
|
||||
{ 0x7C, 0x66, 0x66, 0x7C, 0x66, 0x66, 0x7C, 0x00 }, // 0x42 'B'
|
||||
{ 0x3C, 0x66, 0x60, 0x60, 0x60, 0x66, 0x3C, 0x00 }, // 0x43 'C'
|
||||
{ 0x78, 0x6C, 0x66, 0x66, 0x66, 0x6C, 0x78, 0x00 }, // 0x44 'D'
|
||||
{ 0x7E, 0x60, 0x60, 0x78, 0x60, 0x60, 0x7E, 0x00 }, // 0x45 'E'
|
||||
{ 0x7E, 0x60, 0x60, 0x78, 0x60, 0x60, 0x60, 0x00 }, // 0x46 'F'
|
||||
{ 0x3C, 0x66, 0x60, 0x6E, 0x66, 0x66, 0x3C, 0x00 }, // 0x47 'G'
|
||||
{ 0x66, 0x66, 0x66, 0x7E, 0x66, 0x66, 0x66, 0x00 }, // 0x48 'H'
|
||||
{ 0x3C, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00 }, // 0x49 'I'
|
||||
{ 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x6C, 0x38, 0x00 }, // 0x4A 'J'
|
||||
{ 0x66, 0x6C, 0x78, 0x70, 0x78, 0x6C, 0x66, 0x00 }, // 0x4B 'K'
|
||||
{ 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x7E, 0x00 }, // 0x4C 'L'
|
||||
{ 0x63, 0x77, 0x7F, 0x6B, 0x63, 0x63, 0x63, 0x00 }, // 0x4D 'M'
|
||||
{ 0x66, 0x76, 0x7E, 0x7E, 0x6E, 0x66, 0x66, 0x00 }, // 0x4E 'N'
|
||||
{ 0x3C, 0x66, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x00 }, // 0x4F 'O'
|
||||
{ 0x7C, 0x66, 0x66, 0x7C, 0x60, 0x60, 0x60, 0x00 }, // 0x50 'P'
|
||||
{ 0x3C, 0x66, 0x66, 0x66, 0x6A, 0x6C, 0x36, 0x00 }, // 0x51 'Q'
|
||||
{ 0x7C, 0x66, 0x66, 0x7C, 0x6C, 0x66, 0x66, 0x00 }, // 0x52 'R'
|
||||
{ 0x3C, 0x66, 0x60, 0x3C, 0x06, 0x66, 0x3C, 0x00 }, // 0x53 'S'
|
||||
{ 0x7E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00 }, // 0x54 'T'
|
||||
{ 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x00 }, // 0x55 'U'
|
||||
{ 0x66, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x18, 0x00 }, // 0x56 'V'
|
||||
{ 0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00 }, // 0x57 'W'
|
||||
{ 0x66, 0x66, 0x3C, 0x18, 0x3C, 0x66, 0x66, 0x00 }, // 0x58 'X'
|
||||
{ 0x66, 0x66, 0x66, 0x3C, 0x18, 0x18, 0x18, 0x00 }, // 0x59 'Y'
|
||||
{ 0x7E, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x7E, 0x00 }, // 0x5A 'Z'
|
||||
{ 0x3C, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3C, 0x00 }, // 0x5B '['
|
||||
{ 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x02, 0x00 }, // 0x5C '\\'
|
||||
{ 0x3C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x3C, 0x00 }, // 0x5D ']'
|
||||
{ 0x18, 0x3C, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x5E '^'
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF }, // 0x5F '_'
|
||||
{ 0x30, 0x18, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x60 '`'
|
||||
{ 0x00, 0x00, 0x3C, 0x06, 0x3E, 0x66, 0x3E, 0x00 }, // 0x61 'a'
|
||||
{ 0x60, 0x60, 0x7C, 0x66, 0x66, 0x66, 0x7C, 0x00 }, // 0x62 'b'
|
||||
{ 0x00, 0x00, 0x3C, 0x66, 0x60, 0x66, 0x3C, 0x00 }, // 0x63 'c'
|
||||
{ 0x06, 0x06, 0x3E, 0x66, 0x66, 0x66, 0x3E, 0x00 }, // 0x64 'd'
|
||||
{ 0x00, 0x00, 0x3C, 0x66, 0x7E, 0x60, 0x3C, 0x00 }, // 0x65 'e'
|
||||
{ 0x1C, 0x36, 0x30, 0x78, 0x30, 0x30, 0x30, 0x00 }, // 0x66 'f'
|
||||
{ 0x00, 0x00, 0x3E, 0x66, 0x66, 0x3E, 0x06, 0x3C }, // 0x67 'g'
|
||||
{ 0x60, 0x60, 0x7C, 0x66, 0x66, 0x66, 0x66, 0x00 }, // 0x68 'h'
|
||||
{ 0x18, 0x00, 0x38, 0x18, 0x18, 0x18, 0x3C, 0x00 }, // 0x69 'i'
|
||||
{ 0x06, 0x00, 0x06, 0x06, 0x06, 0x06, 0x66, 0x3C }, // 0x6A 'j'
|
||||
{ 0x60, 0x60, 0x66, 0x6C, 0x78, 0x6C, 0x66, 0x00 }, // 0x6B 'k'
|
||||
{ 0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00 }, // 0x6C 'l'
|
||||
{ 0x00, 0x00, 0x66, 0x7F, 0x7F, 0x6B, 0x63, 0x00 }, // 0x6D 'm'
|
||||
{ 0x00, 0x00, 0x7C, 0x66, 0x66, 0x66, 0x66, 0x00 }, // 0x6E 'n'
|
||||
{ 0x00, 0x00, 0x3C, 0x66, 0x66, 0x66, 0x3C, 0x00 }, // 0x6F 'o'
|
||||
{ 0x00, 0x00, 0x7C, 0x66, 0x66, 0x7C, 0x60, 0x60 }, // 0x70 'p'
|
||||
{ 0x00, 0x00, 0x3E, 0x66, 0x66, 0x3E, 0x06, 0x06 }, // 0x71 'q'
|
||||
{ 0x00, 0x00, 0x7C, 0x66, 0x60, 0x60, 0x60, 0x00 }, // 0x72 'r'
|
||||
{ 0x00, 0x00, 0x3E, 0x60, 0x3C, 0x06, 0x7C, 0x00 }, // 0x73 's'
|
||||
{ 0x18, 0x18, 0x7E, 0x18, 0x18, 0x18, 0x0E, 0x00 }, // 0x74 't'
|
||||
{ 0x00, 0x00, 0x66, 0x66, 0x66, 0x66, 0x3E, 0x00 }, // 0x75 'u'
|
||||
{ 0x00, 0x00, 0x66, 0x66, 0x66, 0x3C, 0x18, 0x00 }, // 0x76 'v'
|
||||
{ 0x00, 0x00, 0x63, 0x6B, 0x7F, 0x7F, 0x36, 0x00 }, // 0x77 'w'
|
||||
{ 0x00, 0x00, 0x66, 0x3C, 0x18, 0x3C, 0x66, 0x00 }, // 0x78 'x'
|
||||
{ 0x00, 0x00, 0x66, 0x66, 0x66, 0x3E, 0x06, 0x3C }, // 0x79 'y'
|
||||
{ 0x00, 0x00, 0x7E, 0x0C, 0x18, 0x30, 0x7E, 0x00 }, // 0x7A 'z'
|
||||
{ 0x0E, 0x18, 0x18, 0x70, 0x18, 0x18, 0x0E, 0x00 }, // 0x7B '{'
|
||||
{ 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00 }, // 0x7C '|'
|
||||
{ 0x70, 0x18, 0x18, 0x0E, 0x18, 0x18, 0x70, 0x00 }, // 0x7D '}'
|
||||
{ 0x76, 0xDC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x7E '~'
|
||||
};
|
||||
696
agiGfx.c
Normal file
696
agiGfx.c
Normal file
|
|
@ -0,0 +1,696 @@
|
|||
// The picture buffers and the screen objects drawn into them.
|
||||
//
|
||||
// Like Sierra's interpreter, JoeyAGI keeps one 160x168 picture buffer pair
|
||||
// -- a colour and a priority byte per AGI pixel -- that holds the picture
|
||||
// with the visible objects blitted into it. An object's blit saves the
|
||||
// bytes under its cel, then draws the cel's pixels whose priority is at
|
||||
// least the buffer's, writing the object's priority with the colour, so a
|
||||
// lower object drawn later stays hidden behind it. Erasing puts the saved
|
||||
// bytes back, so blits stack: the buffer is always erased in the reverse
|
||||
// order it was drawn. Objects that are not updated (stop.update) are drawn
|
||||
// first, sorted by depth, and the updated ones on top of them.
|
||||
//
|
||||
// Changed areas are copied to the stage, each AGI pixel two screen
|
||||
// pixels wide, below the status line (configure.screen's picture row).
|
||||
// display() text written over the picture lives only on the stage, so an
|
||||
// object moving across it wipes it, as in the original.
|
||||
|
||||
#include "agi.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "joey/core.h"
|
||||
#include "joey/draw.h"
|
||||
#include "joey/input.h"
|
||||
#include "joey/present.h"
|
||||
#include "joey/sprite.h"
|
||||
#include "joey/tile.h"
|
||||
#include "surfaceInternal.h"
|
||||
|
||||
|
||||
// Room for the save-unders of every drawn object: two bytes per pixel.
|
||||
#define SAVE_ARENA_BYTES 65535u
|
||||
// Areas waiting to be copied to the stage; more are merged into one.
|
||||
#define DIRTY_MAX 24u
|
||||
#define NIBBLE_SHIFT 4u
|
||||
// A cel run byte: the colour in the high nibble, the length in the low.
|
||||
#define RLE_COUNT_MASK 0x0Fu
|
||||
// add.to.pic's margin box: margins 0..3 are control values; 4 and above
|
||||
// mean no box.
|
||||
#define MARGIN_NONE 4u
|
||||
// shake.screen, measured on Sierra's interpreter: each beat shows the
|
||||
// whole screen moved 8 pixels right and 4 down (the uncovered edges
|
||||
// black) for 4 frames, then in place for 4; a shake is 4 beats per unit
|
||||
// counted in 16 bits, so shake.screen(0) is 65536 beats.
|
||||
#define SHAKE_DX 8
|
||||
#define SHAKE_DY 4
|
||||
#define SHAKE_PHASE_FRAMES 4u
|
||||
#define SHAKE_BEATS_PER_UNIT 4u
|
||||
#define MS_PER_SECOND 1000u
|
||||
// A chunky surface's byte holds two pixels.
|
||||
#define CHUNKY_PX_PER_BYTE (SURFACE_WIDTH / SURFACE_BYTES_PER_ROW)
|
||||
|
||||
|
||||
typedef struct {
|
||||
int16_t x;
|
||||
int16_t y;
|
||||
int16_t w;
|
||||
int16_t h;
|
||||
} GfxRectT;
|
||||
|
||||
|
||||
// ----- Prototypes -----
|
||||
|
||||
static void blitObject(uint8_t n);
|
||||
static void buildShifted(jlSurfaceT *shifted, const jlSurfaceT *normal);
|
||||
static void drawCel(const AgiCelT *cel, bool mirror, int16_t left, int16_t top, uint8_t priority, bool keepPriority);
|
||||
static void drawList(bool updating);
|
||||
static uint8_t effectivePriority(int16_t x, int16_t y);
|
||||
static void markDirty(int16_t x, int16_t y, int16_t w, int16_t h);
|
||||
static void pushRect(const GfxRectT *r);
|
||||
static void pushRuns(jlSurfaceT *stage, int16_t x0, int16_t y0, int16_t x1, int16_t y1, int16_t screenY);
|
||||
static void restoreRect(const uint8_t *save, int16_t x, int16_t y, uint8_t w, uint8_t h);
|
||||
static void saveRect(uint8_t *save, int16_t x, int16_t y, uint8_t w, uint8_t h);
|
||||
static void shakePhase(const jlSurfaceT *show, uint32_t phaseMs);
|
||||
static int16_t sortKey(const AgiObjectT *o);
|
||||
static void unblitTop(void);
|
||||
|
||||
|
||||
// ----- Module state -----
|
||||
|
||||
static uint8_t *gVis;
|
||||
static uint8_t *gPri;
|
||||
static uint8_t *gSaveArena;
|
||||
static uint16_t gSaveTop;
|
||||
// Objects in the order they were blitted; the first gStaticBlits of them
|
||||
// are the not-updated batch.
|
||||
static uint8_t gBlitStack[AGI_MAX_OBJECTS];
|
||||
static uint8_t gBlitCount;
|
||||
static uint8_t gStaticBlits;
|
||||
static GfxRectT gDirty[DIRTY_MAX];
|
||||
static uint8_t gDirtyCount;
|
||||
static bool gPicShown;
|
||||
|
||||
|
||||
// ----- Internal helpers (alphabetical) -----
|
||||
|
||||
static void blitObject(uint8_t n) {
|
||||
AgiObjectT *o;
|
||||
const AgiCelT *cel;
|
||||
bool mirror;
|
||||
int16_t left;
|
||||
int16_t top;
|
||||
uint16_t need;
|
||||
|
||||
o = &gAgi->objects[n];
|
||||
cel = agiViewCel(o->view, o->loop, o->cel, &mirror);
|
||||
if (cel == NULL || cel->width == 0u || cel->height == 0u) {
|
||||
return;
|
||||
}
|
||||
left = o->x;
|
||||
top = (int16_t)(o->y - (int16_t)cel->height + 1);
|
||||
// Clip the save rectangle to the buffer.
|
||||
o->blitX = left;
|
||||
o->blitY = (top < 0) ? 0 : top;
|
||||
o->blitW = cel->width;
|
||||
if (left + (int16_t)cel->width > AGI_WIDTH) {
|
||||
o->blitW = (uint8_t)(AGI_WIDTH - left);
|
||||
}
|
||||
o->blitH = (uint8_t)(o->y - o->blitY + 1);
|
||||
if (left < 0 || left >= AGI_WIDTH || o->y < 0 || o->y > AGI_MAX_Y || o->blitW == 0u) {
|
||||
return;
|
||||
}
|
||||
need = (uint16_t)((uint16_t)o->blitW * o->blitH * 2u);
|
||||
if ((uint32_t)gSaveTop + need > SAVE_ARENA_BYTES) {
|
||||
return;
|
||||
}
|
||||
o->saveUnder = &gSaveArena[gSaveTop];
|
||||
saveRect(o->saveUnder, left, o->blitY, o->blitW, o->blitH);
|
||||
gSaveTop = (uint16_t)(gSaveTop + need);
|
||||
o->blitted = true;
|
||||
gBlitStack[gBlitCount] = n;
|
||||
gBlitCount++;
|
||||
if ((o->flags & OBJ_FIX_PRIORITY) == 0u) {
|
||||
o->priority = agiPriorityForY(o->y);
|
||||
}
|
||||
drawCel(cel, mirror, left, top, o->priority, false);
|
||||
markDirty(o->blitX, o->blitY, o->blitW, o->blitH);
|
||||
}
|
||||
|
||||
|
||||
// The screen as a shake beat shows it: moved SHAKE_DX right and SHAKE_DY
|
||||
// down over black. Chunky surfaces move their bytes; planar ones draw the
|
||||
// screen as one big sprite (colour 0 is transparent, and the surface is
|
||||
// black beneath).
|
||||
static void buildShifted(jlSurfaceT *shifted, const jlSurfaceT *normal) {
|
||||
jlSpriteT *sprite;
|
||||
uint16_t y;
|
||||
|
||||
jlSurfaceCopy(shifted, normal);
|
||||
jlSurfaceClear(shifted, AGI_COLOR_BLACK);
|
||||
if (shifted->pixels != NULL && normal->pixels != NULL) {
|
||||
for (y = SHAKE_DY; y < SURFACE_HEIGHT; y++) {
|
||||
memcpy(&shifted->pixels[y * SURFACE_BYTES_PER_ROW + SHAKE_DX / CHUNKY_PX_PER_BYTE],
|
||||
&normal->pixels[(uint16_t)(y - SHAKE_DY) * SURFACE_BYTES_PER_ROW],
|
||||
SURFACE_BYTES_PER_ROW - SHAKE_DX / CHUNKY_PX_PER_BYTE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
sprite = jlSpriteCreateFromSurface(normal, 0, 0, TILE_BLOCKS_PER_ROW, TILE_BLOCKS_PER_COL);
|
||||
if (sprite != NULL) {
|
||||
jlSpriteDraw(shifted, sprite, SHAKE_DX, SHAKE_DY);
|
||||
jlSpriteDestroy(sprite);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Draw a cel's opaque pixels into the buffers where its priority is at
|
||||
// least the buffer's (control lines take the priority below them).
|
||||
// keepPriority leaves the priority buffer alone (add.to.pic writes it).
|
||||
static void drawCel(const AgiCelT *cel, bool mirror, int16_t left, int16_t top, uint8_t priority, bool keepPriority) {
|
||||
const uint8_t *rle;
|
||||
uint8_t row;
|
||||
int16_t col;
|
||||
int16_t y;
|
||||
int16_t x;
|
||||
uint8_t color;
|
||||
uint8_t run;
|
||||
uint16_t idx;
|
||||
|
||||
rle = cel->rle;
|
||||
for (row = 0u; row < cel->height; row++) {
|
||||
y = (int16_t)(top + row);
|
||||
col = 0;
|
||||
while (*rle != 0u) {
|
||||
color = (uint8_t)(*rle >> NIBBLE_SHIFT);
|
||||
run = (uint8_t)(*rle & RLE_COUNT_MASK);
|
||||
rle++;
|
||||
if (color == cel->transparent || y < 0 || y > AGI_MAX_Y) {
|
||||
col = (int16_t)(col + run);
|
||||
continue;
|
||||
}
|
||||
while (run > 0u) {
|
||||
x = mirror ? (int16_t)(left + (int16_t)cel->width - 1 - col) : (int16_t)(left + col);
|
||||
if (x >= 0 && x < AGI_WIDTH && col < (int16_t)cel->width) {
|
||||
idx = (uint16_t)((uint16_t)y * AGI_WIDTH + (uint16_t)x);
|
||||
if (priority >= effectivePriority(x, y)) {
|
||||
gVis[idx] = color;
|
||||
if (!keepPriority) {
|
||||
gPri[idx] = priority;
|
||||
}
|
||||
}
|
||||
}
|
||||
col++;
|
||||
run--;
|
||||
}
|
||||
}
|
||||
rle++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Blit one batch, sorted by depth: the not-updated objects or the updated
|
||||
// ones.
|
||||
static void drawList(bool updating) {
|
||||
uint8_t order[AGI_MAX_OBJECTS];
|
||||
uint8_t count;
|
||||
uint8_t i;
|
||||
uint8_t j;
|
||||
uint16_t want;
|
||||
|
||||
want = updating ? (uint16_t)(OBJ_ANIMATED | OBJ_DRAWN | OBJ_UPDATE) : (uint16_t)(OBJ_ANIMATED | OBJ_DRAWN);
|
||||
count = 0u;
|
||||
for (i = 0u; i < gAgi->objectCount; i++) {
|
||||
const AgiObjectT *o = &gAgi->objects[i];
|
||||
|
||||
if ((o->flags & (OBJ_ANIMATED | OBJ_DRAWN | OBJ_UPDATE)) != want || o->blitted || o->view == NULL) {
|
||||
continue;
|
||||
}
|
||||
// Insertion sort: by depth, then by object number.
|
||||
j = count;
|
||||
while (j > 0u && sortKey(&gAgi->objects[order[j - 1u]]) > sortKey(o)) {
|
||||
order[j] = order[j - 1u];
|
||||
j--;
|
||||
}
|
||||
order[j] = i;
|
||||
count++;
|
||||
}
|
||||
for (i = 0u; i < count; i++) {
|
||||
blitObject(order[i]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// The priority a pixel has for drawing: control lines (0..3) take the
|
||||
// priority of the first non-control pixel below them.
|
||||
static uint8_t effectivePriority(int16_t x, int16_t y) {
|
||||
uint8_t p;
|
||||
|
||||
for (;;) {
|
||||
p = gPri[(uint16_t)y * AGI_WIDTH + (uint16_t)x];
|
||||
if (p > AGI_CTL_WATER) {
|
||||
return p;
|
||||
}
|
||||
y++;
|
||||
if (y > AGI_MAX_Y) {
|
||||
return AGI_PRIORITY_MAX;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void markDirty(int16_t x, int16_t y, int16_t w, int16_t h) {
|
||||
GfxRectT *r;
|
||||
int16_t right;
|
||||
int16_t bottom;
|
||||
|
||||
if (w <= 0 || h <= 0) {
|
||||
return;
|
||||
}
|
||||
if (gDirtyCount < DIRTY_MAX) {
|
||||
r = &gDirty[gDirtyCount];
|
||||
gDirtyCount++;
|
||||
r->x = x;
|
||||
r->y = y;
|
||||
r->w = w;
|
||||
r->h = h;
|
||||
return;
|
||||
}
|
||||
// Out of slots: grow the last one to cover this one too.
|
||||
r = &gDirty[DIRTY_MAX - 1u];
|
||||
right = (int16_t)(r->x + r->w);
|
||||
bottom = (int16_t)(r->y + r->h);
|
||||
if (x + w > right) {
|
||||
right = (int16_t)(x + w);
|
||||
}
|
||||
if (y + h > bottom) {
|
||||
bottom = (int16_t)(y + h);
|
||||
}
|
||||
if (x < r->x) {
|
||||
r->x = x;
|
||||
}
|
||||
if (y < r->y) {
|
||||
r->y = y;
|
||||
}
|
||||
r->w = (int16_t)(right - r->x);
|
||||
r->h = (int16_t)(bottom - r->y);
|
||||
}
|
||||
|
||||
|
||||
// Copy a buffer rectangle to the stage, each AGI pixel two screen pixels:
|
||||
// straight into a chunky stage's bytes, or as runs on a planar one.
|
||||
static void pushRect(const GfxRectT *r) {
|
||||
jlSurfaceT *stage;
|
||||
int16_t x0;
|
||||
int16_t y0;
|
||||
int16_t x1;
|
||||
int16_t y1;
|
||||
int16_t x;
|
||||
int16_t y;
|
||||
int16_t screenY;
|
||||
uint8_t c;
|
||||
|
||||
x0 = (r->x < 0) ? 0 : r->x;
|
||||
y0 = (r->y < 0) ? 0 : r->y;
|
||||
x1 = (int16_t)(r->x + r->w);
|
||||
y1 = (int16_t)(r->y + r->h);
|
||||
if (x1 > AGI_WIDTH) {
|
||||
x1 = AGI_WIDTH;
|
||||
}
|
||||
if (y1 > AGI_HEIGHT) {
|
||||
y1 = AGI_HEIGHT;
|
||||
}
|
||||
if (x0 >= x1 || y0 >= y1) {
|
||||
return;
|
||||
}
|
||||
stage = jlStageGet();
|
||||
screenY = (int16_t)((int16_t)gAgi->picRow * AGI_CELL_PX);
|
||||
if (stage->pixels == NULL) {
|
||||
pushRuns(stage, x0, y0, x1, y1, screenY);
|
||||
return;
|
||||
}
|
||||
for (y = y0; y < y1; y++) {
|
||||
const uint8_t *src = &gVis[(uint16_t)y * AGI_WIDTH];
|
||||
uint8_t *dst = &stage->pixels[(uint16_t)(screenY + y) * SURFACE_BYTES_PER_ROW];
|
||||
|
||||
for (x = x0; x < x1; x++) {
|
||||
c = src[x];
|
||||
dst[x] = (uint8_t)((c << NIBBLE_SHIFT) | c);
|
||||
}
|
||||
}
|
||||
surfaceMarkDirtyRect(stage, (int16_t)(x0 * AGI_SCREEN_PX_PER_AGI), (int16_t)(screenY + y0),
|
||||
(int16_t)((x1 - x0) * AGI_SCREEN_PX_PER_AGI), (int16_t)(y1 - y0));
|
||||
}
|
||||
|
||||
|
||||
// Planar stages (Amiga, ST, X68000) have no bytes to write: each row goes
|
||||
// out as runs of one colour through jlFillRect, which the ports do fast.
|
||||
static void pushRuns(jlSurfaceT *stage, int16_t x0, int16_t y0, int16_t x1, int16_t y1, int16_t screenY) {
|
||||
const uint8_t *src;
|
||||
int16_t x;
|
||||
int16_t y;
|
||||
int16_t run;
|
||||
uint8_t c;
|
||||
|
||||
for (y = y0; y < y1; y++) {
|
||||
src = &gVis[(uint16_t)y * AGI_WIDTH];
|
||||
x = x0;
|
||||
while (x < x1) {
|
||||
c = src[x];
|
||||
run = 1;
|
||||
while (x + run < x1 && src[x + run] == c) {
|
||||
run++;
|
||||
}
|
||||
jlFillRect(stage, (int16_t)(x * AGI_SCREEN_PX_PER_AGI), (int16_t)(screenY + y), (uint16_t)(run * AGI_SCREEN_PX_PER_AGI), 1u, c);
|
||||
x = (int16_t)(x + run);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Put back a rectangle of both buffers that saveRect kept.
|
||||
static void restoreRect(const uint8_t *save, int16_t x, int16_t y, uint8_t w, uint8_t h) {
|
||||
uint8_t row;
|
||||
uint16_t dst;
|
||||
|
||||
for (row = 0u; row < h; row++) {
|
||||
dst = (uint16_t)((uint16_t)(y + row) * AGI_WIDTH + (uint16_t)x);
|
||||
memcpy(&gVis[dst], save, w);
|
||||
save += w;
|
||||
memcpy(&gPri[dst], save, w);
|
||||
save += w;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Keep a rectangle of both buffers, a row of colours then a row of
|
||||
// priorities at a time (2 * w * h bytes).
|
||||
static void saveRect(uint8_t *save, int16_t x, int16_t y, uint8_t w, uint8_t h) {
|
||||
uint8_t row;
|
||||
uint16_t src;
|
||||
|
||||
for (row = 0u; row < h; row++) {
|
||||
src = (uint16_t)((uint16_t)(y + row) * AGI_WIDTH + (uint16_t)x);
|
||||
memcpy(save, &gVis[src], w);
|
||||
save += w;
|
||||
memcpy(save, &gPri[src], w);
|
||||
save += w;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// One phase of a shake beat: `show` on the screen for phaseMs (NULL, for
|
||||
// want of memory, is only the wait).
|
||||
static void shakePhase(const jlSurfaceT *show, uint32_t phaseMs) {
|
||||
uint32_t start;
|
||||
|
||||
if (show != NULL) {
|
||||
jlSurfaceCopy(jlStageGet(), show);
|
||||
}
|
||||
start = jlMillisElapsed();
|
||||
do {
|
||||
agiIdle();
|
||||
} while (jlMillisElapsed() - start < phaseMs);
|
||||
}
|
||||
|
||||
|
||||
// Objects sort by their baseline, or by the top of their band when their
|
||||
// priority is fixed.
|
||||
static int16_t sortKey(const AgiObjectT *o) {
|
||||
if ((o->flags & OBJ_FIX_PRIORITY) != 0u) {
|
||||
return agiYForPriority(o->priority);
|
||||
}
|
||||
return o->y;
|
||||
}
|
||||
|
||||
|
||||
static void unblitTop(void) {
|
||||
AgiObjectT *o;
|
||||
|
||||
gBlitCount--;
|
||||
o = &gAgi->objects[gBlitStack[gBlitCount]];
|
||||
restoreRect(o->saveUnder, o->blitX, o->blitY, o->blitW, o->blitH);
|
||||
gSaveTop = (uint16_t)(o->saveUnder - gSaveArena);
|
||||
o->blitted = false;
|
||||
markDirty(o->blitX, o->blitY, o->blitW, o->blitH);
|
||||
}
|
||||
|
||||
|
||||
// ----- Public API (alphabetical) -----
|
||||
|
||||
// add.to.pic: draw a cel into the picture itself with a fixed priority
|
||||
// (0 means the priority of its baseline) and, for margins 0..3, a box of
|
||||
// that control value along its base.
|
||||
void gfxAddToPic(uint8_t viewId, uint8_t loop, uint8_t cel, int16_t x, int16_t y, uint8_t priority, uint8_t margin) {
|
||||
const AgiViewT *view;
|
||||
const AgiCelT *c;
|
||||
bool mirror;
|
||||
int16_t top;
|
||||
int16_t boxTop;
|
||||
int16_t i;
|
||||
|
||||
view = agiViewGet(viewId);
|
||||
c = agiViewCel(view, loop, cel, &mirror);
|
||||
if (c == NULL) {
|
||||
return;
|
||||
}
|
||||
if (priority < AGI_PRIORITY_MIN) {
|
||||
priority = agiPriorityForY(y);
|
||||
}
|
||||
gfxEraseAll();
|
||||
top = (int16_t)(y - (int16_t)c->height + 1);
|
||||
drawCel(c, mirror, x, top, priority, false);
|
||||
// The margin box (measured): an outline of the control value, as wide
|
||||
// as the cel, from its baseline up to the top of the baseline's band.
|
||||
if (margin < MARGIN_NONE) {
|
||||
boxTop = agiYForPriority(agiPriorityForY(y));
|
||||
for (i = 0; i < (int16_t)c->width; i++) {
|
||||
int16_t yy;
|
||||
|
||||
if (x + i < 0 || x + i > AGI_MAX_X) {
|
||||
continue;
|
||||
}
|
||||
for (yy = boxTop; yy <= y; yy++) {
|
||||
if (yy == y || yy == boxTop || i == 0 || i == (int16_t)c->width - 1) {
|
||||
gPri[(uint16_t)yy * AGI_WIDTH + (uint16_t)(x + i)] = margin;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
markDirty(x, top, c->width, c->height);
|
||||
gfxRedrawAll();
|
||||
}
|
||||
|
||||
|
||||
// The priority or control value at an AGI pixel, for collision tests.
|
||||
uint8_t gfxControlAt(int16_t x, int16_t y) {
|
||||
if (x < 0 || x > AGI_MAX_X || y < 0 || y > AGI_MAX_Y) {
|
||||
return AGI_PRIORITY_MAX;
|
||||
}
|
||||
return gPri[(uint16_t)y * AGI_WIDTH + (uint16_t)x];
|
||||
}
|
||||
|
||||
|
||||
// draw.pic (clear first) and overlay.pic: draw a picture into the buffers
|
||||
// under the objects. The screen keeps the old picture until show.pic.
|
||||
void gfxDrawPic(uint8_t picId, bool clearFirst) {
|
||||
uint8_t *data;
|
||||
uint16_t length;
|
||||
|
||||
data = agiResLoad(&gAgi->game, AGI_RES_PIC, picId, &length);
|
||||
if (data == NULL) {
|
||||
return;
|
||||
}
|
||||
gfxEraseAll();
|
||||
if (clearFirst) {
|
||||
memset(gVis, AGI_PIC_BG_COLOR, AGI_PIXELS);
|
||||
memset(gPri, AGI_PIC_BG_PRIORITY, AGI_PIXELS);
|
||||
}
|
||||
(void)agiPicDecode(gVis, gPri, data, length);
|
||||
jlFree(data);
|
||||
gDirtyCount = 0u;
|
||||
gPicShown = false;
|
||||
drawList(false);
|
||||
gStaticBlits = gBlitCount;
|
||||
drawList(true);
|
||||
gDirtyCount = 0u;
|
||||
}
|
||||
|
||||
|
||||
// Draw the objects that are drawn but not yet blitted (after a change),
|
||||
// keeping the not-updated ones under the updated ones.
|
||||
void gfxDrawStatic(void) {
|
||||
drawList(false);
|
||||
gStaticBlits = gBlitCount;
|
||||
drawList(true);
|
||||
}
|
||||
|
||||
|
||||
void gfxEraseAll(void) {
|
||||
while (gBlitCount > 0u) {
|
||||
unblitTop();
|
||||
}
|
||||
gStaticBlits = 0u;
|
||||
}
|
||||
|
||||
|
||||
void gfxEraseUpdating(void) {
|
||||
while (gBlitCount > gStaticBlits) {
|
||||
unblitTop();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void gfxFree(void) {
|
||||
jlFree(gVis);
|
||||
jlFree(gPri);
|
||||
jlFree(gSaveArena);
|
||||
gVis = NULL;
|
||||
gPri = NULL;
|
||||
gSaveArena = NULL;
|
||||
}
|
||||
|
||||
|
||||
bool gfxInit(void) {
|
||||
gVis = (uint8_t *)jlAlloc(AGI_PIXELS);
|
||||
gPri = (uint8_t *)jlAlloc(AGI_PIXELS);
|
||||
gSaveArena = (uint8_t *)jlAlloc(SAVE_ARENA_BYTES);
|
||||
if (gVis == NULL || gPri == NULL || gSaveArena == NULL) {
|
||||
gfxFree();
|
||||
return false;
|
||||
}
|
||||
memset(gVis, AGI_COLOR_BLACK, AGI_PIXELS);
|
||||
memset(gPri, AGI_PIC_BG_PRIORITY, AGI_PIXELS);
|
||||
gSaveTop = 0u;
|
||||
gBlitCount = 0u;
|
||||
gStaticBlits = 0u;
|
||||
gDirtyCount = 0u;
|
||||
gPicShown = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Copy every area that changed since the last call to the stage.
|
||||
void gfxPresentDirty(void) {
|
||||
uint8_t i;
|
||||
|
||||
if (gPicShown && !gAgi->textMode) {
|
||||
for (i = 0u; i < gDirtyCount; i++) {
|
||||
pushRect(&gDirty[i]);
|
||||
}
|
||||
}
|
||||
gDirtyCount = 0u;
|
||||
}
|
||||
|
||||
|
||||
// After a change to which objects are drawn or updated: take them all off
|
||||
// and put them back in the right order.
|
||||
void gfxRedrawAll(void) {
|
||||
gfxEraseAll();
|
||||
gfxDrawStatic();
|
||||
gfxPresentDirty();
|
||||
}
|
||||
|
||||
|
||||
void gfxRedrawUpdating(void) {
|
||||
drawList(true);
|
||||
}
|
||||
|
||||
|
||||
// Copy the whole picture area to the stage (after text mode or a window).
|
||||
void gfxRestorePicture(void) {
|
||||
GfxRectT r;
|
||||
|
||||
if (!gPicShown || gAgi->textMode) {
|
||||
return;
|
||||
}
|
||||
r.x = 0;
|
||||
r.y = 0;
|
||||
r.w = AGI_WIDTH;
|
||||
r.h = AGI_HEIGHT;
|
||||
pushRect(&r);
|
||||
}
|
||||
|
||||
|
||||
// shake.screen: the whole screen jumps and comes back, beat after beat,
|
||||
// as long as the game waits (keys, sound and the clock carry on). Each
|
||||
// phase lasts SHAKE_PHASE_FRAMES of this display's frames, counted from
|
||||
// when it was put up, so a machine that takes longer than that to change
|
||||
// the whole screen (the IIgs) shows each phase for as long as that takes.
|
||||
// Without the memory for the two screens it is just as long a pause.
|
||||
void gfxShakeScreen(uint8_t times) {
|
||||
jlSurfaceT *normal;
|
||||
jlSurfaceT *shifted;
|
||||
uint16_t beats;
|
||||
uint32_t phaseMs;
|
||||
|
||||
normal = jlSurfaceCreate();
|
||||
shifted = jlSurfaceCreate();
|
||||
if (normal == NULL || shifted == NULL) {
|
||||
jlSurfaceDestroy(shifted);
|
||||
jlSurfaceDestroy(normal);
|
||||
normal = NULL;
|
||||
shifted = NULL;
|
||||
} else {
|
||||
jlSurfaceCopy(normal, jlStageGet());
|
||||
buildShifted(shifted, normal);
|
||||
}
|
||||
phaseMs = (uint32_t)SHAKE_PHASE_FRAMES * MS_PER_SECOND / jlFrameHz();
|
||||
beats = (uint16_t)((uint16_t)times * SHAKE_BEATS_PER_UNIT);
|
||||
do {
|
||||
shakePhase(shifted, phaseMs);
|
||||
shakePhase(normal, phaseMs);
|
||||
beats--;
|
||||
} while (beats != 0u);
|
||||
jlSurfaceDestroy(shifted);
|
||||
jlSurfaceDestroy(normal);
|
||||
}
|
||||
|
||||
|
||||
// show.obj: a view's first cel at the bottom middle of the picture, on
|
||||
// the stage only: drawn into the buffers over everything, copied out and
|
||||
// the buffers put back (the caller restores the screen after).
|
||||
void gfxShowObjectCel(uint8_t viewId) {
|
||||
const AgiCelT *cel;
|
||||
bool mirror;
|
||||
GfxRectT r;
|
||||
uint8_t *save;
|
||||
|
||||
cel = agiViewCel(agiViewGet(viewId), 0u, 0u, &mirror);
|
||||
if (cel == NULL || cel->width > AGI_WIDTH || cel->height > AGI_HEIGHT) {
|
||||
return;
|
||||
}
|
||||
r.x = (int16_t)((AGI_MAX_X - (int16_t)cel->width) / 2);
|
||||
r.y = (int16_t)(AGI_HEIGHT - (int16_t)cel->height);
|
||||
r.w = cel->width;
|
||||
r.h = cel->height;
|
||||
if ((uint32_t)gSaveTop + 2u * (uint32_t)r.w * (uint32_t)r.h > SAVE_ARENA_BYTES) {
|
||||
return;
|
||||
}
|
||||
save = &gSaveArena[gSaveTop];
|
||||
saveRect(save, r.x, r.y, cel->width, cel->height);
|
||||
drawCel(cel, mirror, r.x, r.y, AGI_PRIORITY_MAX, true);
|
||||
pushRect(&r);
|
||||
restoreRect(save, r.x, r.y, cel->width, cel->height);
|
||||
}
|
||||
|
||||
|
||||
void gfxShowPic(void) {
|
||||
gPicShown = true;
|
||||
gDirtyCount = 0u;
|
||||
gfxRestorePicture();
|
||||
}
|
||||
|
||||
|
||||
// show.pri.screen: the priority buffer as colours until a key.
|
||||
void gfxShowPriority(void) {
|
||||
uint8_t *saved;
|
||||
|
||||
saved = gVis;
|
||||
gVis = gPri;
|
||||
gfxRestorePicture();
|
||||
gVis = saved;
|
||||
(void)inputWaitKey();
|
||||
gfxRestorePicture();
|
||||
}
|
||||
484
agiInput.c
Normal file
484
agiInput.c
Normal file
|
|
@ -0,0 +1,484 @@
|
|||
// The keyboard (and joystick) as AGI sees it, the command line and the
|
||||
// parser (AGI specification 3.8 and 4.2's input commands).
|
||||
//
|
||||
// Keys become the 16-bit codes Sierra's interpreter uses: the character
|
||||
// in the low byte, or for keys without one the PC scan code in the high
|
||||
// byte (F1 is 0x3B00, Alt+D 0x2000, the arrows 0x48/0x50/0x4B/0x4D00).
|
||||
// Each cycle every queued key goes, in order, to a set.key controller
|
||||
// that names it, then to steering ego, then to the command line. Enter
|
||||
// on the command line parses it: punctuation removed, lower case, the
|
||||
// longest dictionary entries matched word by word, ignored words (group
|
||||
// 0) dropped; f2 is set and f4 cleared. Parsing stops at an unknown word,
|
||||
// which stays in the list (any-word and rest-of-line still match it) with
|
||||
// its position in v9; a line of nothing but ignored words says nothing.
|
||||
|
||||
#include "agi.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "joey/core.h"
|
||||
#include "joey/input.h"
|
||||
#include "joey/present.h"
|
||||
|
||||
|
||||
#define KEY_QUEUE_SIZE 16u
|
||||
#define ASCII_FIRST_PRINT 0x20u
|
||||
#define ASCII_LAST_PRINT 0x7Eu
|
||||
#define ASCII_CTRL_FIRST 1u
|
||||
#define SCAN_SHIFT 8u
|
||||
#define FKEY_SCAN_FIRST 0x3Bu
|
||||
#define FKEY_COUNT 10u
|
||||
#define JOY_DEADZONE 48
|
||||
#define HALF_SECOND_MS 500u
|
||||
#define NUMBER_MAX_DIGITS 3u
|
||||
#define NUMBER_MAX 255u
|
||||
#define DECIMAL_BASE 10u
|
||||
#define LETTER_COUNT 26u
|
||||
|
||||
// Characters the parser treats as word breaks.
|
||||
#define PARSE_SEPARATORS " ,.?!();:[]{}"
|
||||
|
||||
|
||||
// ----- Prototypes -----
|
||||
|
||||
static uint16_t dequeue(void);
|
||||
static bool editLine(char *line, uint8_t *len, uint8_t maxLen, uint16_t key);
|
||||
static void enqueue(uint16_t key);
|
||||
static void handleKey(uint16_t key);
|
||||
static uint8_t joystickDirection(void);
|
||||
static uint8_t keyDirection(uint16_t key);
|
||||
|
||||
|
||||
// ----- Tables -----
|
||||
|
||||
// The scan code of each letter, for Alt+letter.
|
||||
static const uint8_t kLetterScan[LETTER_COUNT] = {
|
||||
0x1E, 0x30, 0x2E, 0x20, 0x12, 0x21, 0x22, 0x23, 0x17, 0x24, 0x25, 0x26, 0x32,
|
||||
0x31, 0x18, 0x19, 0x10, 0x13, 0x1F, 0x14, 0x16, 0x2F, 0x11, 0x2D, 0x15, 0x2C
|
||||
};
|
||||
|
||||
static const jlKeyE kFunctionKeys[FKEY_COUNT] = {
|
||||
KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6, KEY_F7, KEY_F8, KEY_F9, KEY_F10
|
||||
};
|
||||
|
||||
|
||||
// ----- Module state -----
|
||||
|
||||
static uint16_t gQueue[KEY_QUEUE_SIZE];
|
||||
static uint8_t gQueueHead;
|
||||
static uint8_t gQueueTail;
|
||||
static uint8_t gJoyDir;
|
||||
// A key this cycle that nothing took (no controller, not steering ego,
|
||||
// no command line to type into), left for have.key.
|
||||
static bool gKeyUnused;
|
||||
// have.key has been asked this cycle.
|
||||
static bool gHaveKeyAsked;
|
||||
|
||||
|
||||
// ----- Internal helpers (alphabetical) -----
|
||||
|
||||
static uint16_t dequeue(void) {
|
||||
uint16_t key;
|
||||
|
||||
if (gQueueHead == gQueueTail) {
|
||||
return AGI_KEY_NONE;
|
||||
}
|
||||
key = gQueue[gQueueTail];
|
||||
gQueueTail = (uint8_t)((gQueueTail + 1u) % KEY_QUEUE_SIZE);
|
||||
return key;
|
||||
}
|
||||
|
||||
|
||||
// Apply one key to a line being typed. Returns true when it was Enter.
|
||||
static bool editLine(char *line, uint8_t *len, uint8_t maxLen, uint16_t key) {
|
||||
if (key == AGI_KEY_ENTER) {
|
||||
return true;
|
||||
}
|
||||
if (key == AGI_KEY_BACKSPACE) {
|
||||
if (*len > 0u) {
|
||||
(*len)--;
|
||||
line[*len] = '\0';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (key >= ASCII_FIRST_PRINT && key <= ASCII_LAST_PRINT && *len < maxLen) {
|
||||
line[*len] = (char)key;
|
||||
(*len)++;
|
||||
line[*len] = '\0';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
static void enqueue(uint16_t key) {
|
||||
uint8_t next;
|
||||
|
||||
next = (uint8_t)((gQueueHead + 1u) % KEY_QUEUE_SIZE);
|
||||
if (next == gQueueTail) {
|
||||
return;
|
||||
}
|
||||
gQueue[gQueueHead] = key;
|
||||
gQueueHead = next;
|
||||
}
|
||||
|
||||
|
||||
static void handleKey(uint16_t key) {
|
||||
uint8_t i;
|
||||
uint8_t dir;
|
||||
uint8_t maxLen;
|
||||
uint8_t oldLen;
|
||||
|
||||
gAgi->vars[VAR_KEY] = (uint8_t)key;
|
||||
for (i = 0u; i < gAgi->keyMapCount; i++) {
|
||||
if (gAgi->keyMaps[i].key == key) {
|
||||
gAgi->controllers[gAgi->keyMaps[i].controller] = 1u;
|
||||
return;
|
||||
}
|
||||
}
|
||||
dir = keyDirection(key);
|
||||
if (dir != AGI_DIR_NONE) {
|
||||
// A second press of the direction ego is walking stops it.
|
||||
if (gAgi->playerControl) {
|
||||
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];
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!gAgi->inputOn) {
|
||||
gKeyUnused = true;
|
||||
return;
|
||||
}
|
||||
maxLen = (uint8_t)(AGI_TEXT_COLS - 1u - strlen(gAgi->strings[0]));
|
||||
if (gAgi->vars[VAR_INPUT_MAX] != 0u && gAgi->vars[VAR_INPUT_MAX] < maxLen) {
|
||||
maxLen = gAgi->vars[VAR_INPUT_MAX];
|
||||
}
|
||||
if (maxLen > AGI_INPUT_MAX) {
|
||||
maxLen = AGI_INPUT_MAX;
|
||||
}
|
||||
oldLen = gAgi->inputLen;
|
||||
if (!editLine(gAgi->inputLine, &gAgi->inputLen, maxLen, key)) {
|
||||
textEchoInput(oldLen);
|
||||
return;
|
||||
}
|
||||
if (gAgi->inputLen > 0u) {
|
||||
strcpy(gAgi->lastLine, gAgi->inputLine);
|
||||
inputParse(gAgi->inputLine);
|
||||
gAgi->inputLine[0] = '\0';
|
||||
gAgi->inputLen = 0u;
|
||||
}
|
||||
textDrawInputLine();
|
||||
}
|
||||
|
||||
|
||||
// The joystick, read as one of the eight directions.
|
||||
static uint8_t joystickDirection(void) {
|
||||
return objDirectionTo(0, 0, jlJoystickX(JOYSTICK_0), jlJoystickY(JOYSTICK_0), JOY_DEADZONE);
|
||||
}
|
||||
|
||||
|
||||
static uint8_t keyDirection(uint16_t key) {
|
||||
switch (key) {
|
||||
case AGI_KEY_UP: return AGI_DIR_N;
|
||||
case AGI_KEY_RIGHT: return AGI_DIR_E;
|
||||
case AGI_KEY_DOWN: return AGI_DIR_S;
|
||||
case AGI_KEY_LEFT: return AGI_DIR_W;
|
||||
default: return AGI_DIR_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ----- Public API (alphabetical) -----
|
||||
|
||||
// get.num: a number typed after a prompt on the input line.
|
||||
bool inputGetNumber(const char *prompt, uint8_t *outValue) {
|
||||
char buf[NUMBER_MAX_DIGITS + 1u];
|
||||
uint8_t len;
|
||||
uint16_t value;
|
||||
uint8_t i;
|
||||
|
||||
buf[0] = '\0';
|
||||
if (!inputGetString(buf, NUMBER_MAX_DIGITS, AGI_NO_POSITION, AGI_NO_POSITION, prompt)) {
|
||||
return false;
|
||||
}
|
||||
value = 0u;
|
||||
len = (uint8_t)strlen(buf);
|
||||
for (i = 0u; i < len; i++) {
|
||||
if (buf[i] < '0' || buf[i] > '9') {
|
||||
break;
|
||||
}
|
||||
value = (uint16_t)(value * DECIMAL_BASE + (uint16_t)(buf[i] - '0'));
|
||||
}
|
||||
*outValue = (uint8_t)((value > NUMBER_MAX) ? NUMBER_MAX : value);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// get.string (and get.num): a line typed at (row, col) after a prompt;
|
||||
// without a position, on the input line. Escape cancels. The typed text
|
||||
// is left in dst.
|
||||
bool inputGetString(char *dst, uint8_t maxLen, uint8_t row, uint8_t col, const char *prompt) {
|
||||
char line[AGI_INPUT_MAX + 1u];
|
||||
char shown[AGI_INPUT_MAX + 2u];
|
||||
uint8_t len;
|
||||
uint16_t key;
|
||||
uint8_t fieldCol;
|
||||
bool onInputLine;
|
||||
bool done;
|
||||
|
||||
if (maxLen > AGI_INPUT_MAX) {
|
||||
maxLen = AGI_INPUT_MAX;
|
||||
}
|
||||
onInputLine = (row == AGI_NO_POSITION);
|
||||
if (onInputLine) {
|
||||
row = gAgi->inputRow;
|
||||
col = 0u;
|
||||
textClearLines(row, row, AGI_COLOR_BLACK);
|
||||
}
|
||||
fieldCol = (uint8_t)(col + strlen(prompt));
|
||||
textDisplay(row, col, prompt);
|
||||
line[0] = '\0';
|
||||
len = 0u;
|
||||
done = false;
|
||||
while (!done) {
|
||||
memcpy(shown, line, len);
|
||||
shown[len] = gAgi->cursorChar;
|
||||
shown[len + 1u] = '\0';
|
||||
textClearRect(row, fieldCol, row, (uint8_t)(fieldCol + maxLen), AGI_COLOR_BLACK);
|
||||
textDisplay(row, fieldCol, shown);
|
||||
key = inputWaitKey();
|
||||
if (key == AGI_KEY_ESCAPE) {
|
||||
break;
|
||||
}
|
||||
done = editLine(line, &len, maxLen, key);
|
||||
}
|
||||
if (onInputLine) {
|
||||
textDrawInputLine();
|
||||
}
|
||||
if (!done) {
|
||||
return false;
|
||||
}
|
||||
strcpy(dst, line);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// have.key: a key this cycle that nothing else took. 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, as Sierra's interpreter reads the keyboard there.
|
||||
bool inputHaveKey(void) {
|
||||
uint16_t key;
|
||||
|
||||
if (gKeyUnused) {
|
||||
gKeyUnused = false;
|
||||
return true;
|
||||
}
|
||||
if (!gHaveKeyAsked) {
|
||||
gHaveKeyAsked = true;
|
||||
return false;
|
||||
}
|
||||
agiIdle();
|
||||
key = dequeue();
|
||||
if (key == AGI_KEY_NONE) {
|
||||
return false;
|
||||
}
|
||||
gAgi->vars[VAR_KEY] = (uint8_t)key;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Parse a typed line into word groups for said().
|
||||
void inputParse(const char *line) {
|
||||
char text[AGI_INPUT_MAX + 1u];
|
||||
uint8_t n;
|
||||
uint8_t pos;
|
||||
uint8_t len;
|
||||
uint16_t id;
|
||||
bool space;
|
||||
|
||||
// Lower case, punctuation to spaces, runs of spaces to one.
|
||||
n = 0u;
|
||||
space = true;
|
||||
while (*line != '\0' && n < AGI_INPUT_MAX) {
|
||||
char c = *line++;
|
||||
|
||||
if (c >= 'A' && c <= 'Z') {
|
||||
c = (char)(c - 'A' + 'a');
|
||||
}
|
||||
if (strchr(PARSE_SEPARATORS, c) != NULL) {
|
||||
if (!space) {
|
||||
text[n++] = ' ';
|
||||
}
|
||||
space = true;
|
||||
continue;
|
||||
}
|
||||
text[n++] = c;
|
||||
space = false;
|
||||
}
|
||||
while (n > 0u && text[n - 1u] == ' ') {
|
||||
n--;
|
||||
}
|
||||
text[n] = '\0';
|
||||
gAgi->wordCount = 0u;
|
||||
gAgi->vars[VAR_BAD_WORD] = 0u;
|
||||
if (n == 0u) {
|
||||
return;
|
||||
}
|
||||
pos = 0u;
|
||||
while (text[pos] != '\0' && gAgi->wordCount < AGI_MAX_WORDS) {
|
||||
id = agiWordMatch(&gAgi->game, &text[pos], &len);
|
||||
if (id == AGI_WORD_NONE) {
|
||||
// Parsing stops at an unknown word, which stays in the list
|
||||
// (any word and the rest of the line still match it) with
|
||||
// its text for %w; v9 says where it was.
|
||||
len = 0u;
|
||||
while (text[pos + len] != '\0' && text[pos + len] != ' ' && len < AGI_WORD_MAX_LEN) {
|
||||
gAgi->wordText[gAgi->wordCount][len] = text[pos + len];
|
||||
len++;
|
||||
}
|
||||
gAgi->wordText[gAgi->wordCount][len] = '\0';
|
||||
gAgi->wordIds[gAgi->wordCount] = AGI_WORD_NONE;
|
||||
gAgi->wordCount++;
|
||||
gAgi->vars[VAR_BAD_WORD] = gAgi->wordCount;
|
||||
break;
|
||||
}
|
||||
if (id != AGI_WORD_IGNORE) {
|
||||
memcpy(gAgi->wordText[gAgi->wordCount], &text[pos], len);
|
||||
gAgi->wordText[gAgi->wordCount][len] = '\0';
|
||||
gAgi->wordIds[gAgi->wordCount] = id;
|
||||
gAgi->wordCount++;
|
||||
}
|
||||
pos = (uint8_t)(pos + len);
|
||||
if (text[pos] == ' ') {
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
// A line of nothing but ignored words says nothing.
|
||||
if (gAgi->wordCount == 0u) {
|
||||
return;
|
||||
}
|
||||
gAgi->flags[FLAG_ENTERED] = 1u;
|
||||
gAgi->flags[FLAG_SAID_OK] = 0u;
|
||||
}
|
||||
|
||||
|
||||
// Gather this frame's key presses into the queue as AGI key codes.
|
||||
void inputPoll(void) {
|
||||
int ch;
|
||||
uint8_t i;
|
||||
bool alt;
|
||||
bool ctrl;
|
||||
uint8_t dir;
|
||||
|
||||
jlInputPoll();
|
||||
alt = jlKeyDown(KEY_LALT);
|
||||
ctrl = jlKeyDown(KEY_LCTRL);
|
||||
// A Ctrl or Alt chord is a command, not typing, whatever the port
|
||||
// delivers as a character.
|
||||
while ((ch = jlInputGetChar()) >= 0) {
|
||||
if (!alt && !ctrl) {
|
||||
enqueue((uint16_t)ch);
|
||||
}
|
||||
}
|
||||
for (i = 0u; i < (uint8_t)(KEY_Z - KEY_A + 1); i++) {
|
||||
if (jlKeyPressed((jlKeyE)(KEY_A + i))) {
|
||||
if (alt) {
|
||||
enqueue((uint16_t)((uint16_t)kLetterScan[i] << SCAN_SHIFT));
|
||||
} else if (ctrl) {
|
||||
enqueue((uint16_t)(ASCII_CTRL_FIRST + i));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (i = 0u; i < FKEY_COUNT; i++) {
|
||||
if (jlKeyPressed(kFunctionKeys[i])) {
|
||||
enqueue((uint16_t)((uint16_t)(FKEY_SCAN_FIRST + i) << SCAN_SHIFT));
|
||||
}
|
||||
}
|
||||
if (jlKeyPressed(KEY_UP)) {
|
||||
enqueue(AGI_KEY_UP);
|
||||
}
|
||||
if (jlKeyPressed(KEY_DOWN)) {
|
||||
enqueue(AGI_KEY_DOWN);
|
||||
}
|
||||
if (jlKeyPressed(KEY_LEFT)) {
|
||||
enqueue(AGI_KEY_LEFT);
|
||||
}
|
||||
if (jlKeyPressed(KEY_RIGHT)) {
|
||||
enqueue(AGI_KEY_RIGHT);
|
||||
}
|
||||
if (jlJoystickConnected(JOYSTICK_0)) {
|
||||
if (jlJoyPressed(JOYSTICK_0, JOY_BUTTON_0)) {
|
||||
enqueue(AGI_KEY_JOY_BUTTON_1);
|
||||
}
|
||||
if (jlJoyPressed(JOYSTICK_0, JOY_BUTTON_1)) {
|
||||
enqueue(AGI_KEY_JOY_BUTTON_2);
|
||||
}
|
||||
dir = joystickDirection();
|
||||
if (dir != gJoyDir) {
|
||||
gJoyDir = dir;
|
||||
if (gAgi->playerControl) {
|
||||
gAgi->objects[AGI_EGO].direction = dir;
|
||||
gAgi->vars[VAR_EGO_DIR] = dir;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// The start of a cycle: hand every queued key to the game.
|
||||
void inputProcess(void) {
|
||||
uint16_t key;
|
||||
|
||||
gKeyUnused = false;
|
||||
gHaveKeyAsked = false;
|
||||
gAgi->vars[VAR_KEY] = 0u;
|
||||
while ((key = dequeue()) != AGI_KEY_NONE) {
|
||||
handleKey(key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void inputReset(void) {
|
||||
gQueueHead = 0u;
|
||||
gQueueTail = 0u;
|
||||
gKeyUnused = false;
|
||||
}
|
||||
|
||||
|
||||
// Block until a key, keeping the game's clock and sound going.
|
||||
uint16_t inputWaitKey(void) {
|
||||
uint16_t key;
|
||||
|
||||
for (;;) {
|
||||
key = dequeue();
|
||||
if (key != AGI_KEY_NONE) {
|
||||
return key;
|
||||
}
|
||||
agiIdle();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Wait for a message window to be dismissed: Enter or Escape, or after
|
||||
// timeoutHalfSeconds half-seconds when that is not 0. Returns false on
|
||||
// Escape.
|
||||
bool inputWaitPrintClose(uint16_t timeoutHalfSeconds) {
|
||||
uint32_t start;
|
||||
uint16_t key;
|
||||
|
||||
start = jlMillisElapsed();
|
||||
for (;;) {
|
||||
key = dequeue();
|
||||
if (key == AGI_KEY_ENTER) {
|
||||
return true;
|
||||
}
|
||||
if (key == AGI_KEY_ESCAPE) {
|
||||
return false;
|
||||
}
|
||||
if (timeoutHalfSeconds != 0u && jlMillisElapsed() - start >= (uint32_t)timeoutHalfSeconds * HALF_SECOND_MS) {
|
||||
return true;
|
||||
}
|
||||
agiIdle();
|
||||
}
|
||||
}
|
||||
818
agiObj.c
818
agiObj.c
|
|
@ -1,363 +1,609 @@
|
|||
// AGI v2 animated-object table: state lifecycle + per-cycle ticks.
|
||||
// Screen objects: cel cycling, motion and what stops it (AGI
|
||||
// specification 4.2's object commands and 2.7's control lines).
|
||||
//
|
||||
// The VM stores an AgiObjectT[16] inside AgiVmT; opcode handlers in
|
||||
// agiVm.c mutate fields directly (set.view / position / step.size /
|
||||
// etc.) and the host renders from the table each frame. This file
|
||||
// owns the housekeeping the dispatcher would otherwise duplicate
|
||||
// across many opcodes:
|
||||
//
|
||||
// * reset on construction and on NEW_ROOM
|
||||
// * per-cycle cel cycling honoring start/stop.cycling, end.of.loop
|
||||
// * per-cycle motion: normal / wander / follow.ego / move.obj
|
||||
//
|
||||
// Every algorithm here is a re-implementation against the published
|
||||
// AGI v2 actor-cycle / actor-motion spec; no third-party source is
|
||||
// referenced.
|
||||
// Each interpreter cycle, before logic 0 runs, objPlanMotion turns the
|
||||
// special motions (wander, follow.ego, move.obj) into a direction; after
|
||||
// it, objUpdateAll erases the updated objects, steps their cels and moves
|
||||
// them a step in their direction, then blits them again. A step is
|
||||
// refused by an unconditional barrier (control 0), a conditional one
|
||||
// (control 1) unless the object ignores blocks, water rules, the block
|
||||
// rectangle and other objects' baselines; objects at fixed priority 15
|
||||
// ignore control lines. Leaving the screen or crossing the horizon stops
|
||||
// the object at the edge and reports it in v2 (ego) or v4/v5.
|
||||
|
||||
#include "agi.h"
|
||||
|
||||
#include "joey/core.h"
|
||||
#include "joey/debug.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
|
||||
// AGI direction encoding. 0 means "no motion this cycle"; 1..8
|
||||
// circle clockwise starting at north.
|
||||
// 1=N 2=NE 3=E 4=SE
|
||||
// 5=S 6=SW 7=W 8=NW
|
||||
#define AGI_DIR_NONE 0u
|
||||
#define AGI_DIR_N 1u
|
||||
#define AGI_DIR_NE 2u
|
||||
#define AGI_DIR_E 3u
|
||||
#define AGI_DIR_SE 4u
|
||||
#define AGI_DIR_S 5u
|
||||
#define AGI_DIR_SW 6u
|
||||
#define AGI_DIR_W 7u
|
||||
#define AGI_DIR_NW 8u
|
||||
#define AGI_DIR_COUNT 9u
|
||||
|
||||
#define WANDER_TICKS_MIN 4u
|
||||
#define WANDER_TICKS_MAX 24u
|
||||
#define MOVE_REACH_PX 2
|
||||
#include "joey/core.h"
|
||||
|
||||
|
||||
// dx, dy per direction (subscript 0 unused; AGI directions are 1..8).
|
||||
static const int8_t kDirDx[AGI_DIR_COUNT] = { 0, 0, +1, +1, +1, 0, -1, -1, -1 };
|
||||
static const int8_t kDirDy[AGI_DIR_COUNT] = { 0, -1, -1, 0, +1, +1, +1, 0, -1 };
|
||||
// Wandering objects walk a random direction for a random number of steps.
|
||||
#define WANDER_STEPS_MIN 6u
|
||||
#define WANDER_STEPS_SPAN 45u
|
||||
#define FOLLOW_DETOUR_MIN 2u
|
||||
#define FOLLOW_DETOUR_SPAN 8u
|
||||
#define STEP_DEFAULT 1u
|
||||
#define SPIRAL_LEGS 4u
|
||||
#define SPIRAL_MAX (2u * AGI_WIDTH)
|
||||
#define SIGN_STATES 3u
|
||||
|
||||
// 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.
|
||||
// LOOP_KEEP leaves the loop alone.
|
||||
#define LOOP_KEEP 0xFFu
|
||||
#define LOOP_RIGHT 0u
|
||||
#define LOOP_LEFT 1u
|
||||
#define LOOP_DOWN 2u
|
||||
#define LOOP_UP 3u
|
||||
#define LOOPS_FOUR 4u
|
||||
#define LOOPS_TWO 2u
|
||||
|
||||
|
||||
// ----- Prototypes -----
|
||||
|
||||
static void advanceCycle(AgiVmT *vm, uint8_t objId);
|
||||
static void advanceMotion(AgiVmT *vm, uint8_t objId);
|
||||
static uint8_t celCount(const AgiVmT *vm, const AgiObjectT *obj);
|
||||
static uint8_t directionToward(int16_t fromX, int16_t fromY, int16_t toX, int16_t toY);
|
||||
static void advanceCel(uint8_t n);
|
||||
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 bool legalPosition(uint8_t n, int16_t x, int16_t y);
|
||||
static void moveObject(uint8_t n);
|
||||
static bool onScreen(const AgiObjectT *o, int16_t x, int16_t y);
|
||||
static void reportEdge(uint8_t n, uint8_t edge);
|
||||
static void selectLoop(AgiObjectT *o);
|
||||
static void stopSpecialMotion(uint8_t n);
|
||||
|
||||
|
||||
// ----- Tables -----
|
||||
|
||||
// A step in each direction.
|
||||
static const int8_t kDirDx[AGI_DIR_COUNT] = { 0, 0, 1, 1, 1, 0, -1, -1, -1 };
|
||||
static const int8_t kDirDy[AGI_DIR_COUNT] = { 0, -1, -1, 0, 1, 1, 1, 0, -1 };
|
||||
|
||||
static const uint8_t kLoopFew[AGI_DIR_COUNT] = { LOOP_KEEP, LOOP_KEEP, LOOP_RIGHT, LOOP_RIGHT, LOOP_RIGHT, LOOP_KEEP, LOOP_LEFT, LOOP_LEFT, LOOP_LEFT };
|
||||
static const uint8_t kLoopFour[AGI_DIR_COUNT] = { LOOP_KEEP, LOOP_UP, LOOP_RIGHT, LOOP_RIGHT, LOOP_RIGHT, LOOP_DOWN, LOOP_LEFT, LOOP_LEFT, LOOP_LEFT };
|
||||
|
||||
// Direction for each (sign of dx, sign of dy), indexed [dy + 1][dx + 1].
|
||||
static const uint8_t kDirFromSigns[SIGN_STATES][SIGN_STATES] = {
|
||||
{ AGI_DIR_NW, AGI_DIR_N, AGI_DIR_NE },
|
||||
{ AGI_DIR_W, AGI_DIR_NONE, AGI_DIR_E },
|
||||
{ AGI_DIR_SW, AGI_DIR_S, AGI_DIR_SE }
|
||||
};
|
||||
|
||||
// objFixPosition's spiral: left, down, right, up.
|
||||
static const int8_t kSpiralDx[SPIRAL_LEGS] = { -1, 0, 1, 0 };
|
||||
static const int8_t kSpiralDy[SPIRAL_LEGS] = { 0, 1, 0, -1 };
|
||||
|
||||
|
||||
// ----- Internal helpers (alphabetical) -----
|
||||
|
||||
static void advanceCycle(AgiVmT *vm, uint8_t objId) {
|
||||
AgiObjectT *obj;
|
||||
uint8_t cels;
|
||||
uint8_t next;
|
||||
static void advanceCel(uint8_t n) {
|
||||
AgiObjectT *o;
|
||||
uint8_t last;
|
||||
|
||||
obj = &vm->objects[objId];
|
||||
if ((obj->flags & AGI_OBJ_FLAG_ANIMATED) == 0u) {
|
||||
o = &gAgi->objects[n];
|
||||
if (o->view == NULL || o->loop >= o->view->loopCount) {
|
||||
return;
|
||||
}
|
||||
if ((obj->flags & AGI_OBJ_FLAG_CYCLING) == 0u) {
|
||||
last = o->view->loops[o->loop].celCount;
|
||||
if (last == 0u) {
|
||||
return;
|
||||
}
|
||||
// Erased objects don't cycle. KQ3 LOGIC.45 sparkle pattern depends
|
||||
// on this: erase(1..4) at wizard-scene start leaves their CYCLING
|
||||
// flag set (LOGIC.45 never explicitly stops it), but Sierra's
|
||||
// interpreter freezes the cel until the next draw(). Without this
|
||||
// gate, end.of.loop fires for invisible objs, sets flag 221..224,
|
||||
// and the sparkle loop kicks back in on the wizard pic.
|
||||
if ((obj->flags & AGI_OBJ_FLAG_DRAWN) == 0u) {
|
||||
return;
|
||||
}
|
||||
// stop.update freezes the obj's displayed cel until start.update.
|
||||
// Canonical AGI cycles cels internally regardless, but with no
|
||||
// display refresh the visual stays put. Since our host renderer
|
||||
// always paints from obj->cel, the easiest faithful approximation
|
||||
// is to also stop advancing cel while !UPDATING -- otherwise
|
||||
// poses cycle visibly (e.g. Manannan's body in room 46, which is
|
||||
// stop.update'd at init and only released to start.update at a
|
||||
// specific state, would otherwise wave its hand continuously).
|
||||
if ((obj->flags & AGI_OBJ_FLAG_UPDATING) == 0u) {
|
||||
return;
|
||||
}
|
||||
if (obj->cycleTime == 0u) {
|
||||
return;
|
||||
}
|
||||
obj->cycleTick++;
|
||||
if (obj->cycleTick < obj->cycleTime) {
|
||||
return;
|
||||
}
|
||||
obj->cycleTick = 0u;
|
||||
|
||||
cels = celCount(vm, obj);
|
||||
if (cels <= 1u) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (obj->cycleMode) {
|
||||
last--;
|
||||
switch (o->cycleMode) {
|
||||
case AGI_CYCLE_NORMAL:
|
||||
next = (uint8_t)((obj->cel + 1u) % cels);
|
||||
obj->cel = next;
|
||||
o->cel = (o->cel >= last) ? 0u : (uint8_t)(o->cel + 1u);
|
||||
break;
|
||||
case AGI_CYCLE_REVERSE:
|
||||
obj->cel = (obj->cel == 0u) ? (uint8_t)(cels - 1u) : (uint8_t)(obj->cel - 1u);
|
||||
o->cel = (o->cel == 0u || o->cel > last) ? last : (uint8_t)(o->cel - 1u);
|
||||
break;
|
||||
case AGI_CYCLE_END_LOOP:
|
||||
if (obj->cel + 1u >= cels) {
|
||||
// reached last frame, fire end-of-loop
|
||||
obj->flags &= (uint8_t)~AGI_OBJ_FLAG_CYCLING;
|
||||
if (obj->endOfLoopFlag != 0u) {
|
||||
vm->flags[obj->endOfLoopFlag] = 1u;
|
||||
}
|
||||
} else {
|
||||
obj->cel++;
|
||||
case AGI_CYCLE_REVERSE_LOOP:
|
||||
// The flag is set as the end cel is reached.
|
||||
if (o->cycleMode == AGI_CYCLE_END_LOOP && o->cel < last) {
|
||||
o->cel++;
|
||||
} else if (o->cycleMode == AGI_CYCLE_REVERSE_LOOP && o->cel > 0u) {
|
||||
o->cel--;
|
||||
}
|
||||
break;
|
||||
case AGI_CYCLE_REV_LOOP:
|
||||
if (obj->cel == 0u) {
|
||||
obj->flags &= (uint8_t)~AGI_OBJ_FLAG_CYCLING;
|
||||
if (obj->endOfLoopFlag != 0u) {
|
||||
vm->flags[obj->endOfLoopFlag] = 1u;
|
||||
}
|
||||
} else {
|
||||
obj->cel--;
|
||||
if ((o->cycleMode == AGI_CYCLE_END_LOOP) ? (o->cel >= last) : (o->cel == 0u)) {
|
||||
gAgi->flags[o->loopFlag] = 1u;
|
||||
o->flags = (uint16_t)(o->flags & (uint16_t)~OBJ_CYCLING);
|
||||
o->cycleMode = AGI_CYCLE_NORMAL;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
objSetCel(n, o->cel);
|
||||
}
|
||||
|
||||
|
||||
static void advanceMotion(AgiVmT *vm, uint8_t objId) {
|
||||
AgiObjectT *obj;
|
||||
int16_t newX;
|
||||
int16_t newY;
|
||||
uint8_t dir;
|
||||
int8_t dx;
|
||||
int8_t dy;
|
||||
// Another object's baseline in the way: the two baselines overlap (or
|
||||
// touch) and are on the same line, or this step crosses the other's.
|
||||
static bool blockedByObject(uint8_t n, int16_t x, int16_t y) {
|
||||
const AgiObjectT *o;
|
||||
const AgiObjectT *other;
|
||||
uint8_t i;
|
||||
|
||||
obj = &vm->objects[objId];
|
||||
if ((obj->flags & AGI_OBJ_FLAG_ANIMATED) == 0u) {
|
||||
return;
|
||||
o = &gAgi->objects[n];
|
||||
if ((o->flags & OBJ_IGNORE_OBJS) != 0u) {
|
||||
return false;
|
||||
}
|
||||
if ((obj->flags & AGI_OBJ_FLAG_DRAWN) == 0u) {
|
||||
return;
|
||||
}
|
||||
if (obj->stepSize == 0u) {
|
||||
return;
|
||||
}
|
||||
if (obj->stepTime == 0u) {
|
||||
return;
|
||||
}
|
||||
obj->stepTick++;
|
||||
if (obj->stepTick < obj->stepTime) {
|
||||
return;
|
||||
}
|
||||
obj->stepTick = 0u;
|
||||
|
||||
switch (obj->motionType) {
|
||||
case AGI_MOTION_WANDER:
|
||||
if (obj->wanderTick == 0u) {
|
||||
obj->direction = (uint8_t)(jlRandomRange((uint16_t)AGI_DIR_COUNT));
|
||||
obj->wanderTick = (uint8_t)(WANDER_TICKS_MIN + jlRandomRange((uint16_t)(WANDER_TICKS_MAX - WANDER_TICKS_MIN)));
|
||||
} else {
|
||||
obj->wanderTick--;
|
||||
}
|
||||
break;
|
||||
case AGI_MOTION_FOLLOW_EGO:
|
||||
obj->direction = directionToward(obj->x, obj->y, vm->objects[0].x, vm->objects[0].y);
|
||||
break;
|
||||
case AGI_MOTION_MOVE_OBJ: {
|
||||
int16_t reachDx;
|
||||
int16_t reachDy;
|
||||
|
||||
reachDx = (int16_t)(obj->targetX - obj->x);
|
||||
reachDy = (int16_t)(obj->targetY - obj->y);
|
||||
if (reachDx < 0) {
|
||||
reachDx = (int16_t)(-reachDx);
|
||||
}
|
||||
if (reachDy < 0) {
|
||||
reachDy = (int16_t)(-reachDy);
|
||||
}
|
||||
if (reachDx <= MOVE_REACH_PX && reachDy <= MOVE_REACH_PX) {
|
||||
obj->x = obj->targetX;
|
||||
obj->y = obj->targetY;
|
||||
obj->direction = AGI_DIR_NONE;
|
||||
obj->motionType = AGI_MOTION_NORMAL;
|
||||
obj->stepSize = obj->moveStepBackup;
|
||||
if (obj->moveDoneFlag != 0u) {
|
||||
vm->flags[obj->moveDoneFlag] = 1u;
|
||||
}
|
||||
return;
|
||||
}
|
||||
obj->direction = directionToward(obj->x, obj->y, obj->targetX, obj->targetY);
|
||||
break;
|
||||
for (i = 0u; i < gAgi->objectCount; i++) {
|
||||
other = &gAgi->objects[i];
|
||||
if (i == n || (other->flags & (OBJ_ANIMATED | OBJ_DRAWN)) != (OBJ_ANIMATED | OBJ_DRAWN) ||
|
||||
(other->flags & OBJ_IGNORE_OBJS) != 0u) {
|
||||
continue;
|
||||
}
|
||||
if (x + (int16_t)o->width < other->x || other->x + (int16_t)other->width < x) {
|
||||
continue;
|
||||
}
|
||||
if (y == other->y || (o->y < other->y && y > other->y) || (o->y > other->y && y < other->y)) {
|
||||
return true;
|
||||
}
|
||||
case AGI_MOTION_NORMAL:
|
||||
default:
|
||||
// Direction is whatever the script (or var 6 for ego) set.
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
dir = obj->direction;
|
||||
if (dir == AGI_DIR_NONE || dir >= AGI_DIR_COUNT) {
|
||||
|
||||
// Keep a position on the screen and below the horizon; returns the edge
|
||||
// it ran into, if any.
|
||||
static uint8_t clampToScreen(AgiObjectT *o, int16_t *x, int16_t *y) {
|
||||
uint8_t edge;
|
||||
|
||||
edge = AGI_EDGE_NONE;
|
||||
if (*x < 0) {
|
||||
*x = 0;
|
||||
edge = AGI_EDGE_LEFT;
|
||||
} else if (*x + (int16_t)o->width > AGI_WIDTH) {
|
||||
*x = (int16_t)(AGI_WIDTH - (int16_t)o->width);
|
||||
edge = AGI_EDGE_RIGHT;
|
||||
}
|
||||
if (*y > AGI_MAX_Y) {
|
||||
*y = AGI_MAX_Y;
|
||||
edge = AGI_EDGE_BOTTOM;
|
||||
} else if ((o->flags & OBJ_IGNORE_HORIZON) == 0u && *y <= (int16_t)gAgi->horizon) {
|
||||
*y = (int16_t)(gAgi->horizon + 1u);
|
||||
edge = AGI_EDGE_TOP;
|
||||
} else if (*y - (int16_t)o->height + 1 < 0) {
|
||||
*y = (int16_t)(o->height - 1u);
|
||||
edge = AGI_EDGE_TOP;
|
||||
}
|
||||
return edge;
|
||||
}
|
||||
|
||||
|
||||
// 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
|
||||
// ego as a side effect.
|
||||
static bool legalPosition(uint8_t n, int16_t x, int16_t y) {
|
||||
const AgiObjectT *o;
|
||||
int16_t i;
|
||||
uint8_t c;
|
||||
bool anyWater;
|
||||
bool allWater;
|
||||
bool signal;
|
||||
bool insideNow;
|
||||
bool insideNew;
|
||||
|
||||
o = &gAgi->objects[n];
|
||||
anyWater = false;
|
||||
allWater = true;
|
||||
signal = false;
|
||||
if (!((o->flags & OBJ_FIX_PRIORITY) != 0u && o->priority == AGI_PRIORITY_MAX)) {
|
||||
for (i = 0; i < (int16_t)o->width; i++) {
|
||||
c = gfxControlAt((int16_t)(x + i), y);
|
||||
if (c == AGI_CTL_BARRIER) {
|
||||
return false;
|
||||
}
|
||||
if (c == AGI_CTL_BLOCK && (o->flags & OBJ_IGNORE_BLOCKS) == 0u) {
|
||||
return false;
|
||||
}
|
||||
if (c == AGI_CTL_WATER) {
|
||||
anyWater = true;
|
||||
} else {
|
||||
allWater = false;
|
||||
}
|
||||
if (c == AGI_CTL_SIGNAL) {
|
||||
signal = true;
|
||||
}
|
||||
}
|
||||
if ((o->flags & OBJ_ON_WATER) != 0u && !allWater) {
|
||||
return false;
|
||||
}
|
||||
if ((o->flags & OBJ_ON_LAND) != 0u && anyWater) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
allWater = false;
|
||||
}
|
||||
if (gAgi->blockActive && (o->flags & OBJ_IGNORE_BLOCKS) == 0u) {
|
||||
insideNow = o->x >= gAgi->blockX1 && o->x <= gAgi->blockX2 && o->y >= gAgi->blockY1 && o->y <= gAgi->blockY2;
|
||||
insideNew = x >= gAgi->blockX1 && x <= gAgi->blockX2 && y >= gAgi->blockY1 && y <= gAgi->blockY2;
|
||||
if (insideNow != insideNew) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (blockedByObject(n, x, y)) {
|
||||
return false;
|
||||
}
|
||||
if (n == AGI_EGO) {
|
||||
gAgi->flags[FLAG_EGO_WATER] = allWater ? 1u : 0u;
|
||||
gAgi->flags[FLAG_EGO_SIGNAL] = signal ? 1u : 0u;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// One step in the object's direction, if the way is clear.
|
||||
static void moveObject(uint8_t n) {
|
||||
AgiObjectT *o;
|
||||
int16_t x;
|
||||
int16_t y;
|
||||
uint8_t edge;
|
||||
|
||||
o = &gAgi->objects[n];
|
||||
if (o->direction == AGI_DIR_NONE || o->direction >= AGI_DIR_COUNT) {
|
||||
return;
|
||||
}
|
||||
dx = kDirDx[dir];
|
||||
dy = kDirDy[dir];
|
||||
newX = (int16_t)(obj->x + (int16_t)((int16_t)dx * (int16_t)obj->stepSize));
|
||||
newY = (int16_t)(obj->y + (int16_t)((int16_t)dy * (int16_t)obj->stepSize));
|
||||
|
||||
// Clamp to AGI's canonical 160x168 picture window. The horizon
|
||||
// applies only to objects observing it.
|
||||
if (newX < 0) {
|
||||
newX = 0;
|
||||
x = (int16_t)(o->x + kDirDx[o->direction] * (int16_t)o->stepSize);
|
||||
y = (int16_t)(o->y + kDirDy[o->direction] * (int16_t)o->stepSize);
|
||||
edge = clampToScreen(o, &x, &y);
|
||||
if ((x != o->x || y != o->y) && legalPosition(n, x, y)) {
|
||||
o->x = x;
|
||||
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));
|
||||
}
|
||||
if (newX > (int16_t)(AGI_PIC_WIDTH - 1)) {
|
||||
newX = (int16_t)(AGI_PIC_WIDTH - 1);
|
||||
// Ego's edge is current: a step that touches none clears it.
|
||||
if (edge != AGI_EDGE_NONE) {
|
||||
reportEdge(n, edge);
|
||||
} else if (n == AGI_EGO) {
|
||||
gAgi->vars[VAR_EGO_EDGE] = AGI_EDGE_NONE;
|
||||
}
|
||||
if (newY > (int16_t)(AGI_PIC_HEIGHT - 1)) {
|
||||
newY = (int16_t)(AGI_PIC_HEIGHT - 1);
|
||||
}
|
||||
if ((obj->flags & AGI_OBJ_FLAG_HORIZON_IGN) == 0u && newY < (int16_t)vm->horizon) {
|
||||
newY = (int16_t)vm->horizon;
|
||||
}
|
||||
if (newY < 0) {
|
||||
newY = 0;
|
||||
}
|
||||
obj->x = newX;
|
||||
obj->y = newY;
|
||||
}
|
||||
|
||||
|
||||
// Without a view-id-to-loop-cel-count map in the VM, fall back to an
|
||||
// Asks the host for the actual cel count of obj's view+loop via
|
||||
// the viewCelCount callback. Falls back to AGI_MAX_CELS_PER_LOOP
|
||||
// only when no callback is wired (legacy host) -- but cycling
|
||||
// is broken in that mode because end.of.loop won't fire at the
|
||||
// correct cel. A 0 return from the callback means "view not
|
||||
// loaded yet"; we report 0 and the caller's `cels <= 1u` guard
|
||||
// pauses cycling until the load completes.
|
||||
static uint8_t celCount(const AgiVmT *vm, const AgiObjectT *obj) {
|
||||
if (vm->callbacks.viewCelCount != NULL) {
|
||||
return vm->callbacks.viewCelCount(vm->callbacks.ctx, obj->viewId, obj->loop);
|
||||
}
|
||||
return AGI_MAX_CELS_PER_LOOP;
|
||||
// The object's cel is wholly on the screen, below the horizon unless it
|
||||
// ignores it.
|
||||
static bool onScreen(const AgiObjectT *o, int16_t x, int16_t y) {
|
||||
return x >= 0 && x + (int16_t)o->width <= AGI_WIDTH && y <= AGI_MAX_Y && y - (int16_t)o->height + 1 >= 0 &&
|
||||
((o->flags & OBJ_IGNORE_HORIZON) != 0u || y > (int16_t)gAgi->horizon);
|
||||
}
|
||||
|
||||
|
||||
// 8-direction snap by quadrant + axis-dominance.
|
||||
static uint8_t directionToward(int16_t fromX, int16_t fromY, int16_t toX, int16_t toY) {
|
||||
int16_t dx;
|
||||
int16_t dy;
|
||||
int16_t ax;
|
||||
int16_t ay;
|
||||
static void reportEdge(uint8_t n, uint8_t edge) {
|
||||
if (n == AGI_EGO) {
|
||||
gAgi->vars[VAR_EGO_EDGE] = edge;
|
||||
} else {
|
||||
gAgi->vars[VAR_OBJ_EDGE_OBJ] = n;
|
||||
gAgi->vars[VAR_OBJ_EDGE] = edge;
|
||||
}
|
||||
}
|
||||
|
||||
dx = (int16_t)(toX - fromX);
|
||||
dy = (int16_t)(toY - fromY);
|
||||
if (dx == 0 && dy == 0) {
|
||||
return AGI_DIR_NONE;
|
||||
|
||||
static void selectLoop(AgiObjectT *o) {
|
||||
uint8_t loop;
|
||||
|
||||
if ((o->flags & OBJ_FIX_LOOP) != 0u || o->view == NULL || o->view->loopCount < LOOPS_TWO) {
|
||||
return;
|
||||
}
|
||||
ax = (dx < 0) ? (int16_t)(-dx) : dx;
|
||||
ay = (dy < 0) ? (int16_t)(-dy) : dy;
|
||||
if (ax > ay * 2) {
|
||||
return (dx > 0) ? AGI_DIR_E : AGI_DIR_W;
|
||||
loop = (o->view->loopCount >= LOOPS_FOUR) ? kLoopFour[o->direction] : kLoopFew[o->direction];
|
||||
if (loop != LOOP_KEEP && loop != o->loop) {
|
||||
objSetLoop((uint8_t)(o - gAgi->objects), loop);
|
||||
}
|
||||
if (ay > ax * 2) {
|
||||
return (dy > 0) ? AGI_DIR_S : AGI_DIR_N;
|
||||
}
|
||||
|
||||
|
||||
// A special motion is over: set its flag, stop, and give back what it
|
||||
// took (the step size, and for ego the keyboard).
|
||||
static void stopSpecialMotion(uint8_t n) {
|
||||
AgiObjectT *o;
|
||||
|
||||
o = &gAgi->objects[n];
|
||||
if (o->motion == AGI_MOTION_MOVE) {
|
||||
o->stepSize = o->savedStepSize;
|
||||
if (n == AGI_EGO) {
|
||||
gAgi->playerControl = true;
|
||||
}
|
||||
}
|
||||
if (dx > 0) {
|
||||
return (dy > 0) ? AGI_DIR_SE : AGI_DIR_NE;
|
||||
gAgi->flags[o->motionFlag] = 1u;
|
||||
o->motion = AGI_MOTION_NORMAL;
|
||||
o->direction = AGI_DIR_NONE;
|
||||
if (n == AGI_EGO) {
|
||||
gAgi->vars[VAR_EGO_DIR] = AGI_DIR_NONE;
|
||||
}
|
||||
return (dy > 0) ? AGI_DIR_SW : AGI_DIR_NW;
|
||||
}
|
||||
|
||||
|
||||
// ----- Public API (alphabetical) -----
|
||||
|
||||
void agiObjReset(AgiObjectT *obj) {
|
||||
memset(obj, 0, sizeof(*obj));
|
||||
obj->stepSize = 1u;
|
||||
obj->stepTime = 1u;
|
||||
obj->cycleTime = 1u;
|
||||
obj->priority = 0u; // 0 = "auto-Y-band" (canonical AGI)
|
||||
obj->direction = AGI_DIR_NONE;
|
||||
obj->cycleMode = AGI_CYCLE_NORMAL;
|
||||
obj->motionType = AGI_MOTION_NORMAL;
|
||||
obj->endOfLoopFlag = 0u;
|
||||
obj->moveDoneFlag = 0u;
|
||||
obj->followDoneFlag = 0u;
|
||||
}
|
||||
// animate.obj: bring an object under the interpreter's control. Its
|
||||
// step and cycle settings are left as they are: new.room (unanimate.all)
|
||||
// is what sets them back to the defaults, so before the first new.room
|
||||
// they are all 0 (measured: such objects neither cycle nor move).
|
||||
void objAnimate(uint8_t n) {
|
||||
AgiObjectT *o;
|
||||
|
||||
|
||||
void agiObjResetAll(AgiVmT *vm) {
|
||||
uint8_t i;
|
||||
|
||||
for (i = 0u; i < (uint8_t)AGI_MAX_OBJECTS; i++) {
|
||||
agiObjReset(&vm->objects[i]);
|
||||
o = &gAgi->objects[n];
|
||||
if ((o->flags & OBJ_ANIMATED) == 0u) {
|
||||
o->flags = (uint16_t)((o->flags & OBJ_SKIP_STEP) | OBJ_ANIMATED | OBJ_UPDATE | OBJ_CYCLING);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Called by VM core after each VM cycle returns to the host (or by
|
||||
// the host directly each game-loop cycle). Walks the object table
|
||||
// and advances cycling + motion. Also polls the host for sound
|
||||
// completion so the sound-done flag fires on the natural-end of
|
||||
// playback rather than a pre-computed deadline.
|
||||
void agiVmTickAnimation(AgiVmT *vm) {
|
||||
// The objects as the interpreter starts: all zero, each yet to make its
|
||||
// first step.
|
||||
void objClearAll(void) {
|
||||
uint8_t i;
|
||||
|
||||
// Sound poll: fire done-flags on the host's playback->silent
|
||||
// edge. Runs every VM cycle so OP_IF wait loops see the flag
|
||||
// within one cycle of actual audio end.
|
||||
if (vm->soundPlaying && vm->callbacks.isPlayingSound != NULL) {
|
||||
if (!vm->callbacks.isPlayingSound(vm->callbacks.ctx)) {
|
||||
jlLogF("sound-done flag fires: flag=%u (v11=%u)",
|
||||
(unsigned)vm->soundDoneFlag, (unsigned)vm->vars[11]);
|
||||
jlLogFlush();
|
||||
if (vm->soundDoneFlag != 0u) {
|
||||
vm->flags[vm->soundDoneFlag] = 1u;
|
||||
memset(gAgi->objects, 0, sizeof(gAgi->objects));
|
||||
for (i = 0u; i < AGI_MAX_OBJECTS; i++) {
|
||||
gAgi->objects[i].flags = OBJ_SKIP_STEP;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// The direction from one point toward another, treating offsets smaller
|
||||
// than `slack` as none (measured: move.obj with step 2 goes the last 2
|
||||
// pixels, with step 3 stops 2 short).
|
||||
uint8_t objDirectionTo(int16_t fromX, int16_t fromY, int16_t toX, int16_t toY, uint8_t slack) {
|
||||
int16_t dx;
|
||||
int16_t dy;
|
||||
int16_t s;
|
||||
uint8_t sx;
|
||||
uint8_t sy;
|
||||
|
||||
s = (int16_t)((slack == 0u) ? 1u : slack);
|
||||
dx = (int16_t)(toX - fromX);
|
||||
dy = (int16_t)(toY - fromY);
|
||||
sx = (dx <= -s) ? 0u : (dx >= s) ? 2u : 1u;
|
||||
sy = (dy <= -s) ? 0u : (dy >= s) ? 2u : 1u;
|
||||
return kDirFromSigns[sy][sx];
|
||||
}
|
||||
|
||||
|
||||
// Put an object somewhere it may stand (measured from Sierra's
|
||||
// interpreter): below the horizon, then, if it may not stand there,
|
||||
// spiralling out a pixel at a time -- left 1, down 1, right 2, up 2,
|
||||
// left 3, ... -- to the first place on the screen clear of barriers and
|
||||
// other objects.
|
||||
void objFixPosition(uint8_t n) {
|
||||
AgiObjectT *o;
|
||||
int16_t x;
|
||||
int16_t y;
|
||||
uint16_t len;
|
||||
uint16_t step;
|
||||
uint8_t leg;
|
||||
|
||||
o = &gAgi->objects[n];
|
||||
x = o->x;
|
||||
y = o->y;
|
||||
if ((o->flags & OBJ_IGNORE_HORIZON) == 0u && y <= (int16_t)gAgi->horizon) {
|
||||
y = (int16_t)(gAgi->horizon + 1u);
|
||||
}
|
||||
leg = 0u;
|
||||
len = 1u;
|
||||
while (!(onScreen(o, x, y) && legalPosition(n, x, y)) && len <= SPIRAL_MAX) {
|
||||
for (step = 0u; step < len; step++) {
|
||||
x = (int16_t)(x + kSpiralDx[leg]);
|
||||
y = (int16_t)(y + kSpiralDy[leg]);
|
||||
if (onScreen(o, x, y) && legalPosition(n, x, y)) {
|
||||
break;
|
||||
}
|
||||
vm->flags[9] = 1u;
|
||||
vm->soundPlaying = false;
|
||||
}
|
||||
if (step < len) {
|
||||
break;
|
||||
}
|
||||
// The legs lengthen every second turn.
|
||||
leg = (uint8_t)((leg + 1u) % SPIRAL_LEGS);
|
||||
if ((leg & 1u) == 0u) {
|
||||
len++;
|
||||
}
|
||||
}
|
||||
o->x = x;
|
||||
o->y = y;
|
||||
}
|
||||
|
||||
for (i = 0u; i < (uint8_t)AGI_MAX_OBJECTS; i++) {
|
||||
AgiObjectT *obj;
|
||||
|
||||
obj = &vm->objects[i];
|
||||
if ((obj->flags & AGI_OBJ_FLAG_ANIMATED) == 0u) {
|
||||
// move.obj: head for (x, y) at `step` pixels a step (0 keeps the step
|
||||
// size); flag is set on arrival.
|
||||
void objMoveTo(uint8_t n, uint8_t x, uint8_t y, uint8_t step, uint8_t flag) {
|
||||
AgiObjectT *o;
|
||||
|
||||
o = &gAgi->objects[n];
|
||||
o->motion = AGI_MOTION_MOVE;
|
||||
o->targetX = x;
|
||||
o->targetY = y;
|
||||
o->savedStepSize = o->stepSize;
|
||||
if (step != 0u) {
|
||||
o->stepSize = step;
|
||||
}
|
||||
o->motionFlag = flag;
|
||||
o->direction = objDirectionTo(o->x, o->y, x, y, o->stepSize);
|
||||
gAgi->flags[flag] = 0u;
|
||||
if (n == AGI_EGO) {
|
||||
gAgi->playerControl = false;
|
||||
gAgi->vars[VAR_EGO_DIR] = o->direction;
|
||||
}
|
||||
if (o->direction == AGI_DIR_NONE) {
|
||||
stopSpecialMotion(n);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Before logic 0: work out where wandering, following and moving objects
|
||||
// head this cycle.
|
||||
void objPlanMotion(void) {
|
||||
uint8_t i;
|
||||
AgiObjectT *o;
|
||||
AgiObjectT *ego;
|
||||
|
||||
ego = &gAgi->objects[AGI_EGO];
|
||||
for (i = 0u; i < gAgi->objectCount; i++) {
|
||||
o = &gAgi->objects[i];
|
||||
if ((o->flags & (OBJ_ANIMATED | OBJ_UPDATE | OBJ_DRAWN)) != (OBJ_ANIMATED | OBJ_UPDATE | OBJ_DRAWN)) {
|
||||
continue;
|
||||
}
|
||||
// Snapshot current state so the host renderer can restore the
|
||||
// last frame's pixels under each obj before re-drawing this
|
||||
// frame. Done before the tick so prevX/prevY reflect what was
|
||||
// actually painted last frame.
|
||||
obj->prevX = obj->x;
|
||||
obj->prevY = obj->y;
|
||||
obj->prevLoop = obj->loop;
|
||||
obj->prevCel = obj->cel;
|
||||
obj->prevView = obj->viewId;
|
||||
|
||||
advanceCycle(vm, i);
|
||||
advanceMotion(vm, i);
|
||||
}
|
||||
// Mirror ego state into AGI's well-known engine vars so game
|
||||
// scripts that probe v6 (ego dir) / current.view / get.posn
|
||||
// see the right values without a per-opcode getter call.
|
||||
if ((vm->objects[0].flags & AGI_OBJ_FLAG_ANIMATED) != 0u) {
|
||||
vm->vars[6] = vm->objects[0].direction;
|
||||
switch (o->motion) {
|
||||
case AGI_MOTION_WANDER:
|
||||
if (o->randomSteps == 0u) {
|
||||
o->direction = (uint8_t)jlRandomRange(AGI_DIR_COUNT);
|
||||
o->randomSteps = (uint8_t)(WANDER_STEPS_MIN + jlRandomRange(WANDER_STEPS_SPAN));
|
||||
} else {
|
||||
o->randomSteps--;
|
||||
}
|
||||
break;
|
||||
case AGI_MOTION_FOLLOW:
|
||||
if (o->randomSteps > 0u) {
|
||||
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;
|
||||
case AGI_MOTION_MOVE:
|
||||
o->direction = objDirectionTo(o->x, o->y, o->targetX, o->targetY, o->stepSize);
|
||||
if (o->direction == AGI_DIR_NONE) {
|
||||
stopSpecialMotion(i);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (i == AGI_EGO) {
|
||||
gAgi->vars[VAR_EGO_DIR] = o->direction;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void objSetCel(uint8_t n, uint8_t cel) {
|
||||
AgiObjectT *o;
|
||||
const AgiCelT *c;
|
||||
bool mirror;
|
||||
|
||||
o = &gAgi->objects[n];
|
||||
o->cel = cel;
|
||||
if (o->view == NULL || o->loop >= o->view->loopCount) {
|
||||
return;
|
||||
}
|
||||
// A cel (or loop) the new view or loop lacks becomes 0 (measured).
|
||||
if (cel >= o->view->loops[o->loop].celCount) {
|
||||
o->cel = 0u;
|
||||
}
|
||||
c = agiViewCel(o->view, o->loop, o->cel, &mirror);
|
||||
if (c != NULL) {
|
||||
o->width = c->width;
|
||||
o->height = c->height;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void objSetLoop(uint8_t n, uint8_t loop) {
|
||||
AgiObjectT *o;
|
||||
|
||||
o = &gAgi->objects[n];
|
||||
if (o->view == NULL) {
|
||||
o->loop = loop;
|
||||
return;
|
||||
}
|
||||
o->loop = (loop >= o->view->loopCount) ? 0u : loop;
|
||||
objSetCel(n, o->cel);
|
||||
}
|
||||
|
||||
|
||||
bool objSetView(uint8_t n, uint8_t viewId) {
|
||||
AgiObjectT *o;
|
||||
const AgiViewT *view;
|
||||
|
||||
view = agiViewGet(viewId);
|
||||
if (view == NULL) {
|
||||
return false;
|
||||
}
|
||||
o = &gAgi->objects[n];
|
||||
o->view = view;
|
||||
o->viewId = viewId;
|
||||
if (n == AGI_EGO) {
|
||||
gAgi->vars[VAR_EGO_VIEW] = viewId;
|
||||
}
|
||||
objSetLoop(n, o->loop);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// unanimate.all (and new.room): every object off the screen and out of
|
||||
// the interpreter's hands, with the default step and cycle settings. Its
|
||||
// view, loop, cel and position stay (new.room places ego from them).
|
||||
void objUnanimateAll(void) {
|
||||
uint8_t i;
|
||||
AgiObjectT *o;
|
||||
|
||||
gfxEraseAll();
|
||||
for (i = 0u; i < AGI_MAX_OBJECTS; i++) {
|
||||
o = &gAgi->objects[i];
|
||||
o->flags = OBJ_SKIP_STEP;
|
||||
o->direction = AGI_DIR_NONE;
|
||||
o->motion = AGI_MOTION_NORMAL;
|
||||
o->cycleMode = AGI_CYCLE_NORMAL;
|
||||
o->stepSize = STEP_DEFAULT;
|
||||
o->stepTime = STEP_DEFAULT;
|
||||
o->stepCount = STEP_DEFAULT;
|
||||
o->cycleTime = STEP_DEFAULT;
|
||||
o->cycleCount = STEP_DEFAULT;
|
||||
}
|
||||
gfxPresentDirty();
|
||||
}
|
||||
|
||||
|
||||
// After logic 0: step every updated object's cel and move it, then draw
|
||||
// them all again.
|
||||
void objUpdateAll(void) {
|
||||
uint8_t i;
|
||||
AgiObjectT *o;
|
||||
|
||||
gfxEraseUpdating();
|
||||
for (i = 0u; i < gAgi->objectCount; i++) {
|
||||
o = &gAgi->objects[i];
|
||||
if ((o->flags & (OBJ_ANIMATED | OBJ_UPDATE | OBJ_DRAWN)) != (OBJ_ANIMATED | OBJ_UPDATE | OBJ_DRAWN)) {
|
||||
continue;
|
||||
}
|
||||
selectLoop(o);
|
||||
// A cycle count of 0 (an object never given a cycle time) never
|
||||
// comes due; a step count of 0 is due every cycle.
|
||||
if ((o->flags & OBJ_CYCLING) != 0u && o->cycleCount != 0u) {
|
||||
o->cycleCount--;
|
||||
if (o->cycleCount == 0u) {
|
||||
o->cycleCount = o->cycleTime;
|
||||
if ((o->flags & OBJ_SKIP_CYCLE) != 0u) {
|
||||
o->flags = (uint16_t)(o->flags & (uint16_t)~OBJ_SKIP_CYCLE);
|
||||
} else {
|
||||
advanceCel(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (o->stepCount > 0u) {
|
||||
o->stepCount--;
|
||||
}
|
||||
if (o->stepCount == 0u) {
|
||||
o->stepCount = o->stepTime;
|
||||
o->prevX = o->x;
|
||||
o->prevY = o->y;
|
||||
if ((o->flags & OBJ_SKIP_STEP) != 0u) {
|
||||
o->flags = (uint16_t)(o->flags & (uint16_t)~OBJ_SKIP_STEP);
|
||||
} else {
|
||||
moveObject(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
gfxRedrawUpdating();
|
||||
}
|
||||
|
|
|
|||
579
agiRes.c
579
agiRes.c
|
|
@ -1,267 +1,312 @@
|
|||
// AGI v2 resource loader.
|
||||
// AGI v2 resources: the four directories, the VOL files, WORDS.TOK and
|
||||
// OBJECT (AGI specification chapters 5 and 10).
|
||||
//
|
||||
// Parses LOGDIR/PICDIR/VIEWDIR/SNDDIR into in-RAM index tables, keeps
|
||||
// every VOL.x file open for the session, and exposes a single
|
||||
// resource-by-id load primitive that hands back a freshly-allocated
|
||||
// payload buffer.
|
||||
//
|
||||
// AGI v2 directory entry format (3 bytes, big-endian within the byte
|
||||
// stream): byte0 high nibble is the volume number, the 20-bit value
|
||||
// formed from (byte0 & 0x0F):byte1:byte2 is the file offset. All-FF
|
||||
// means the slot is empty (no resource with that ID).
|
||||
//
|
||||
// VOL.x record header (5 bytes at the directory's offset): 0x12 0x34
|
||||
// signature, then the volume number, then a little-endian 16-bit
|
||||
// payload length. v2 payloads are stored raw; v3 sets bit 7 of the
|
||||
// volume byte to flag LZW compression (not handled here).
|
||||
// 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;
|
||||
// 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.
|
||||
|
||||
#include "agi.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "joey/core.h"
|
||||
#include "joey/file.h"
|
||||
|
||||
|
||||
// AGI directory files are flat arrays of 3-byte entries. The maximum
|
||||
// directory length the loader will accept is bounded by AGI_MAX_-
|
||||
// RESOURCES; larger files are reported as bad rather than truncated.
|
||||
#define AGI_DIR_ENTRY_BYTES 3
|
||||
#define DIR_ENTRY_BYTES 3u
|
||||
#define DIR_EMPTY_BYTE 0xFFu
|
||||
#define DIR_EMPTY_VOLUME 0xFFu
|
||||
#define DIR_VOLUME_SHIFT 4u
|
||||
#define DIR_OFFSET_HIGH_MASK 0x0Fu
|
||||
#define DIR_OFFSET_HIGH_SHIFT 16u
|
||||
#define VOL_HEADER_BYTES 5u
|
||||
#define VOL_SIG_0 0x12u
|
||||
#define VOL_SIG_1 0x34u
|
||||
#define VOL_COMPRESSED 0x80u
|
||||
#define VOL_VOLUME_MASK 0x7Fu
|
||||
#define VOL_NAME_MAX 8u
|
||||
#define DECIMAL_BASE 10u
|
||||
|
||||
// VOL.x resource records start with a 5-byte header.
|
||||
#define AGI_VOL_HEADER_BYTES 5
|
||||
#define AGI_VOL_SIG_0 0x12
|
||||
#define AGI_VOL_SIG_1 0x34
|
||||
#define AGI_VOL_COMPRESSED 0x80
|
||||
#define AGI_VOL_VOLUME_MASK 0x7F
|
||||
// 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).
|
||||
// Offsets count from the first entry. The file is XORed with the same
|
||||
// key as LOGIC messages.
|
||||
#define OBJECT_HEADER_BYTES 3u
|
||||
#define OBJECT_ENTRY_BYTES 3u
|
||||
#define OBJECT_ROOM_BYTE 2u
|
||||
#define OBJECT_MAX_BYTES 8192u
|
||||
#define OBJECT_MAX_OBJS_BYTE 2u
|
||||
|
||||
// Resource file names are opened relative to DATA/ via jlDataOpen (see
|
||||
// joey/file.h); the longest name AGI forms is "VOL.<n>". 16 is ample.
|
||||
#define AGI_VOL_NAME_MAX 16
|
||||
// WORDS.TOK: 26 big-endian letter offsets, then entries of a prefix
|
||||
// count, characters XORed with 0x7F (bit 7 marks the last) and a
|
||||
// big-endian group number.
|
||||
#define WORD_CHAR_XOR 0x7Fu
|
||||
#define WORD_LAST_CHAR 0x80u
|
||||
#define WORD_INDEX_BYTES 2u
|
||||
|
||||
// Empty-slot sentinel: must be >= AGI_MAX_VOLUMES so agiResLoad
|
||||
// rejects empty entries. The on-disk empty marker is 0xFFFFFF (high
|
||||
// nibble 0xF = volume 15); 15 is a legitimate volume number, so we
|
||||
// don't reuse it as the sentinel.
|
||||
#define AGI_DIR_EMPTY_VOLUME 0xFF
|
||||
#define CRYPT_KEY "Avis Durgan"
|
||||
#define CRYPT_KEY_LEN 11u
|
||||
|
||||
// The interpreter's version string, "Version 2.435", in AGIDATA.OVL (or
|
||||
// in the AGI executable of interpreters without one).
|
||||
#define VERSION_TAG "Version "
|
||||
#define VERSION_FILE_MAX 65535u
|
||||
#define VERSION_MINOR_DIGITS 3u
|
||||
#define VERSION_MAJOR_SCALE 1000u
|
||||
|
||||
|
||||
// ----- Prototypes -----
|
||||
|
||||
static bool buildVolName(char *out, size_t outSize, uint8_t volNum);
|
||||
static bool loadDirectory(AgiGameT *game, const char *fileName, AgiResTypeE type);
|
||||
static bool loadWords(AgiGameT *game);
|
||||
static bool openVolumes(AgiGameT *game);
|
||||
static const char *resTypeFileName(AgiResTypeE type);
|
||||
static bool wordEqCI(const char *a, const char *b);
|
||||
static void buildVolName(char *out, uint8_t volNum);
|
||||
static uint16_t detectVersion(void);
|
||||
static void loadDirectory(AgiGameT *game, const char *fileName, AgiResTypeE type);
|
||||
static uint8_t *loadFile(const char *name, uint16_t maxBytes, uint16_t *outLength);
|
||||
static bool loadObjects(AgiGameT *game);
|
||||
|
||||
|
||||
// ----- Tables -----
|
||||
|
||||
// The directory files, by AgiResTypeE.
|
||||
static const char *const kDirNames[AGI_RES_COUNT] = { "LOGDIR", "PICDIR", "VIEWDIR", "SNDDIR" };
|
||||
|
||||
// Where the version string may be, in the order to look.
|
||||
static const char *const kVersionFiles[] = { "AGIDATA.OVL", "AGI" };
|
||||
|
||||
// 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
|
||||
// come from the PC interpreter's window, input-line and picture code, so
|
||||
// they are assumed for every 2.x interpreter until one is measured
|
||||
// otherwise.
|
||||
typedef struct {
|
||||
uint16_t first;
|
||||
uint16_t last;
|
||||
} QuirkRangeT;
|
||||
|
||||
static const QuirkRangeT kQuirkVersions[AGI_QUIRK_COUNT] = {
|
||||
{ 2000u, 2999u }, // AGI_QUIRK_WINDOW_BYTE_X
|
||||
{ 2000u, 2999u }, // AGI_QUIRK_ECHO_STEPS_BACK
|
||||
{ 2000u, 2999u } // AGI_QUIRK_LINE_BYTE_ERROR
|
||||
};
|
||||
|
||||
|
||||
// ----- Internal helpers (alphabetical) -----
|
||||
|
||||
static bool buildVolName(char *out, size_t outSize, uint8_t volNum) {
|
||||
char *p;
|
||||
static void buildVolName(char *out, uint8_t volNum) {
|
||||
uint8_t n;
|
||||
|
||||
// "VOL." + up to 2 digits + NUL = 7 chars max.
|
||||
if (outSize < 8u) {
|
||||
return false;
|
||||
n = 0u;
|
||||
out[n++] = 'V';
|
||||
out[n++] = 'O';
|
||||
out[n++] = 'L';
|
||||
out[n++] = '.';
|
||||
if (volNum >= DECIMAL_BASE) {
|
||||
out[n++] = (char)('0' + volNum / DECIMAL_BASE);
|
||||
}
|
||||
p = out;
|
||||
*p++ = 'V';
|
||||
*p++ = 'O';
|
||||
*p++ = 'L';
|
||||
*p++ = '.';
|
||||
if (volNum >= 10u) {
|
||||
*p++ = (char)('0' + (volNum / 10u));
|
||||
*p++ = (char)('0' + (volNum % 10u));
|
||||
} else {
|
||||
*p++ = (char)('0' + volNum);
|
||||
}
|
||||
*p = '\0';
|
||||
return true;
|
||||
out[n++] = (char)('0' + volNum % DECIMAL_BASE);
|
||||
out[n] = '\0';
|
||||
}
|
||||
|
||||
|
||||
static bool loadDirectory(AgiGameT *game, const char *fileName, AgiResTypeE type) {
|
||||
// One directory is resident at a time; a file-scope buffer keeps this
|
||||
// ~768-byte read off the shallow IIgs soft stack.
|
||||
static uint8_t dirBytes[AGI_MAX_RESOURCES * AGI_DIR_ENTRY_BYTES];
|
||||
FILE *fp;
|
||||
uint16_t entryCount;
|
||||
uint16_t i;
|
||||
uint8_t volNibble;
|
||||
uint32_t offset;
|
||||
size_t got;
|
||||
// The version Sierra's interpreter files in the game folder name, or
|
||||
// AGI_VERSION_MEASURED when there are none.
|
||||
static uint16_t detectVersion(void) {
|
||||
uint8_t *data;
|
||||
uint16_t length;
|
||||
uint16_t i;
|
||||
uint16_t j;
|
||||
uint16_t major;
|
||||
uint16_t minor;
|
||||
uint8_t f;
|
||||
|
||||
// fileName is a bare name relative to DATA/ (e.g. "LOGDIR").
|
||||
fp = jlDataOpen(fileName, "rb");
|
||||
if (fp == NULL) {
|
||||
return false;
|
||||
for (f = 0u; f < sizeof(kVersionFiles) / sizeof(kVersionFiles[0]); f++) {
|
||||
data = loadFile(kVersionFiles[f], VERSION_FILE_MAX, &length);
|
||||
if (data == NULL) {
|
||||
continue;
|
||||
}
|
||||
for (i = 0u; (uint32_t)i + sizeof(VERSION_TAG) + VERSION_MINOR_DIGITS + 1u < length; i++) {
|
||||
if (memcmp(&data[i], VERSION_TAG, sizeof(VERSION_TAG) - 1u) != 0) {
|
||||
continue;
|
||||
}
|
||||
j = (uint16_t)(i + sizeof(VERSION_TAG) - 1u);
|
||||
if (data[j] < '0' || data[j] > '9' || data[j + 1u] != '.') {
|
||||
continue;
|
||||
}
|
||||
major = (uint16_t)(data[j] - '0');
|
||||
minor = 0u;
|
||||
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'));
|
||||
}
|
||||
jlFree(data);
|
||||
return (uint16_t)(major * VERSION_MAJOR_SCALE + minor);
|
||||
}
|
||||
jlFree(data);
|
||||
}
|
||||
return AGI_VERSION_MEASURED;
|
||||
}
|
||||
|
||||
// Read the whole flat 3-byte-entry array in one call rather than one
|
||||
// read per entry: on the IIgs each fread is a GS/OS round trip through
|
||||
// SmartPort firmware, and a directory never exceeds AGI_MAX_RESOURCES
|
||||
// entries, so the fixed buffer holds any valid file. A non-multiple-of-3
|
||||
// length means the file is malformed.
|
||||
got = fread(dirBytes, 1, sizeof(dirBytes), fp);
|
||||
fclose(fp);
|
||||
if (got == 0u || (got % AGI_DIR_ENTRY_BYTES) != 0u) {
|
||||
return false;
|
||||
|
||||
// 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;
|
||||
uint16_t count;
|
||||
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);
|
||||
for (i = 0u; i < count; i++) {
|
||||
const uint8_t *e = &bytes[i * DIR_ENTRY_BYTES];
|
||||
AgiResEntryT *d = &game->resDir[type][i];
|
||||
|
||||
entryCount = (uint16_t)(got / AGI_DIR_ENTRY_BYTES);
|
||||
for (i = 0; i < entryCount; i++) {
|
||||
const uint8_t *e = &dirBytes[(uint32_t)i * AGI_DIR_ENTRY_BYTES];
|
||||
|
||||
volNibble = (uint8_t)(e[0] >> 4);
|
||||
// 20-bit offset, cast to uint32_t before shift so a
|
||||
// 16-bit int doesn't truncate the (& 0x0F) << 16 term.
|
||||
offset = ((uint32_t)(e[0] & 0x0F) << 16) | ((uint32_t)e[1] << 8) | (uint32_t)e[2];
|
||||
|
||||
if (e[0] == 0xFF && e[1] == 0xFF && e[2] == 0xFF) {
|
||||
game->resDir[type][i].volume = AGI_DIR_EMPTY_VOLUME;
|
||||
game->resDir[type][i].offset = 0u;
|
||||
if (e[0] == DIR_EMPTY_BYTE && e[1] == DIR_EMPTY_BYTE && e[2] == DIR_EMPTY_BYTE) {
|
||||
d->volume = DIR_EMPTY_VOLUME;
|
||||
} else {
|
||||
game->resDir[type][i].volume = volNibble;
|
||||
game->resDir[type][i].offset = offset;
|
||||
d->volume = (uint8_t)(e[0] >> DIR_VOLUME_SHIFT);
|
||||
d->offsetHigh = (uint8_t)(e[0] & DIR_OFFSET_HIGH_MASK);
|
||||
d->offsetLow = (uint16_t)(((uint16_t)e[1] << 8) | e[2]);
|
||||
}
|
||||
}
|
||||
|
||||
game->resCount[type] = entryCount;
|
||||
return true;
|
||||
game->resCount[type] = count;
|
||||
jlFree(bytes);
|
||||
}
|
||||
|
||||
|
||||
static bool loadWords(AgiGameT *game) {
|
||||
// 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).
|
||||
// Returns NULL when it is missing or empty.
|
||||
static uint8_t *loadFile(const char *name, uint16_t maxBytes, uint16_t *outLength) {
|
||||
FILE *fp;
|
||||
uint8_t *buf;
|
||||
size_t got;
|
||||
long size;
|
||||
|
||||
game->words = NULL;
|
||||
game->wordsLen = 0u;
|
||||
|
||||
// The dictionary is optional: a game with no WORDS.TOK simply can't
|
||||
// match said()/parse (no crash), so a missing file is not an error.
|
||||
fp = jlDataOpen("WORDS.TOK", "rb");
|
||||
fp = jlDataOpen(name, "rb");
|
||||
if (fp == NULL) {
|
||||
return true;
|
||||
return NULL;
|
||||
}
|
||||
buf = (uint8_t *)malloc(AGI_WORDS_MAX);
|
||||
if (buf == NULL) {
|
||||
fclose(fp);
|
||||
buf = NULL;
|
||||
if (fseek(fp, 0L, SEEK_END) == 0) {
|
||||
size = ftell(fp);
|
||||
if (size > (long)maxBytes) {
|
||||
size = (long)maxBytes;
|
||||
}
|
||||
if (size > 0L && fseek(fp, 0L, SEEK_SET) == 0) {
|
||||
buf = (uint8_t *)jlAlloc((uint32_t)size);
|
||||
}
|
||||
if (buf != NULL && fread(buf, 1, (size_t)size, fp) != (size_t)size) {
|
||||
jlFree(buf);
|
||||
buf = NULL;
|
||||
}
|
||||
*outLength = (uint16_t)size;
|
||||
}
|
||||
fclose(fp);
|
||||
return buf;
|
||||
}
|
||||
|
||||
|
||||
static bool loadObjects(AgiGameT *game) {
|
||||
uint8_t *data;
|
||||
uint16_t length;
|
||||
uint16_t namesOffset;
|
||||
|
||||
data = loadFile("OBJECT", OBJECT_MAX_BYTES, &length);
|
||||
if (data == NULL) {
|
||||
return false;
|
||||
}
|
||||
got = fread(buf, 1, AGI_WORDS_MAX, fp);
|
||||
fclose(fp);
|
||||
// Anything shorter than the letter-index header can't hold a word.
|
||||
if (got <= AGI_WORDS_HEADER_BYTES) {
|
||||
free(buf);
|
||||
return true;
|
||||
if (length < OBJECT_HEADER_BYTES) {
|
||||
jlFree(data);
|
||||
return false;
|
||||
}
|
||||
agiDecrypt(data, length);
|
||||
namesOffset = (uint16_t)(data[0] | ((uint16_t)data[1] << 8));
|
||||
game->objectData = data;
|
||||
game->itemCount = (uint16_t)(namesOffset / OBJECT_ENTRY_BYTES);
|
||||
if (game->itemCount > AGI_MAX_ITEMS) {
|
||||
game->itemCount = AGI_MAX_ITEMS;
|
||||
}
|
||||
game->maxObjects = data[OBJECT_MAX_OBJS_BYTE];
|
||||
if (game->maxObjects == 0u || game->maxObjects > AGI_MAX_OBJECTS) {
|
||||
game->maxObjects = AGI_MAX_OBJECTS;
|
||||
}
|
||||
game->words = buf;
|
||||
game->wordsLen = (uint32_t)got;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static bool openVolumes(AgiGameT *game) {
|
||||
char name[AGI_VOL_NAME_MAX];
|
||||
uint8_t v;
|
||||
bool anyOpened;
|
||||
|
||||
anyOpened = false;
|
||||
for (v = 0; v < AGI_MAX_VOLUMES; v++) {
|
||||
if (!buildVolName(name, sizeof(name), v)) {
|
||||
return false;
|
||||
}
|
||||
game->volFiles[v] = jlDataOpen(name, "rb");
|
||||
if (game->volFiles[v] != NULL) {
|
||||
anyOpened = true;
|
||||
}
|
||||
}
|
||||
return anyOpened;
|
||||
}
|
||||
|
||||
|
||||
static const char *resTypeFileName(AgiResTypeE type) {
|
||||
switch (type) {
|
||||
case AGI_RES_LOGIC: return "LOGDIR";
|
||||
case AGI_RES_PIC: return "PICDIR";
|
||||
case AGI_RES_VIEW: return "VIEWDIR";
|
||||
case AGI_RES_SOUND: return "SNDDIR";
|
||||
default: return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Case-insensitive ASCII equality. Both dictionary words and (lowercased)
|
||||
// typed words are ASCII; fold A-Z so a stray uppercase still matches.
|
||||
static bool wordEqCI(const char *a, const char *b) {
|
||||
while (*a != '\0' && *b != '\0') {
|
||||
char ca;
|
||||
char cb;
|
||||
|
||||
ca = *a;
|
||||
cb = *b;
|
||||
if (ca >= 'A' && ca <= 'Z') {
|
||||
ca = (char)(ca + ('a' - 'A'));
|
||||
}
|
||||
if (cb >= 'A' && cb <= 'Z') {
|
||||
cb = (char)(cb + ('a' - 'A'));
|
||||
}
|
||||
if (ca != cb) {
|
||||
return false;
|
||||
}
|
||||
a++;
|
||||
b++;
|
||||
}
|
||||
return *a == '\0' && *b == '\0';
|
||||
}
|
||||
|
||||
|
||||
// ----- Public API (alphabetical) -----
|
||||
|
||||
// Undo Sierra's XOR with "Avis Durgan" (OBJECT, and LOGIC messages from
|
||||
// the start of their text).
|
||||
void agiDecrypt(uint8_t *data, uint16_t length) {
|
||||
uint16_t i;
|
||||
|
||||
for (i = 0u; i < length; i++) {
|
||||
data[i] = (uint8_t)(data[i] ^ (uint8_t)CRYPT_KEY[i % CRYPT_KEY_LEN]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const char *agiItemName(const AgiGameT *game, uint8_t item) {
|
||||
const uint8_t *e;
|
||||
uint16_t off;
|
||||
|
||||
if (game->objectData == NULL || item >= game->itemCount) {
|
||||
return "";
|
||||
}
|
||||
e = &game->objectData[OBJECT_HEADER_BYTES + (uint16_t)item * OBJECT_ENTRY_BYTES];
|
||||
off = (uint16_t)(e[0] | ((uint16_t)e[1] << 8));
|
||||
return (const char *)&game->objectData[OBJECT_HEADER_BYTES + off];
|
||||
}
|
||||
|
||||
|
||||
uint8_t agiItemStartRoom(const AgiGameT *game, uint8_t item) {
|
||||
if (game->objectData == NULL || item >= game->itemCount) {
|
||||
return 0u;
|
||||
}
|
||||
return game->objectData[OBJECT_HEADER_BYTES + (uint16_t)item * OBJECT_ENTRY_BYTES + OBJECT_ROOM_BYTE];
|
||||
}
|
||||
|
||||
|
||||
// Whether the game's interpreter version has a quirk.
|
||||
bool agiQuirk(AgiQuirkE quirk) {
|
||||
uint16_t v;
|
||||
|
||||
v = gAgi->game.interpreterVersion;
|
||||
return quirk < AGI_QUIRK_COUNT && v >= kQuirkVersions[quirk].first && v <= kQuirkVersions[quirk].last;
|
||||
}
|
||||
|
||||
|
||||
void agiResClose(AgiGameT *game) {
|
||||
uint8_t v;
|
||||
uint8_t t;
|
||||
|
||||
for (v = 0; v < AGI_MAX_VOLUMES; v++) {
|
||||
for (v = 0u; v < AGI_MAX_VOLUMES; v++) {
|
||||
if (game->volFiles[v] != NULL) {
|
||||
fclose(game->volFiles[v]);
|
||||
game->volFiles[v] = NULL;
|
||||
}
|
||||
}
|
||||
for (t = 0; t < AGI_RES_COUNT; t++) {
|
||||
game->resCount[t] = 0u;
|
||||
}
|
||||
if (game->words != NULL) {
|
||||
free(game->words);
|
||||
game->words = NULL;
|
||||
game->wordsLen = 0u;
|
||||
}
|
||||
jlFree(game->words);
|
||||
game->words = NULL;
|
||||
jlFree(game->objectData);
|
||||
game->objectData = NULL;
|
||||
}
|
||||
|
||||
|
||||
uint8_t *agiResLoad(const AgiGameT *game, AgiResTypeE type, uint16_t index, uint16_t *outLength) {
|
||||
const AgiResEntryT *entry;
|
||||
FILE *fp;
|
||||
uint8_t header[AGI_VOL_HEADER_BYTES];
|
||||
uint8_t header[VOL_HEADER_BYTES];
|
||||
uint8_t *payload;
|
||||
uint16_t length;
|
||||
uint8_t flagsAndVolume;
|
||||
long offset;
|
||||
|
||||
if (outLength != NULL) {
|
||||
*outLength = 0u;
|
||||
}
|
||||
if (type >= AGI_RES_COUNT) {
|
||||
return NULL;
|
||||
}
|
||||
if (index >= game->resCount[type]) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
entry = &game->resDir[type][index];
|
||||
if (entry->volume >= AGI_MAX_VOLUMES) {
|
||||
return NULL;
|
||||
|
|
@ -270,40 +315,32 @@ uint8_t *agiResLoad(const AgiGameT *game, AgiResTypeE type, uint16_t index, uint
|
|||
if (fp == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (fseek(fp, (long)entry->offset, SEEK_SET) != 0) {
|
||||
offset = ((long)entry->offsetHigh << DIR_OFFSET_HIGH_SHIFT) | (long)entry->offsetLow;
|
||||
if (fseek(fp, offset, SEEK_SET) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
if (fread(header, 1, AGI_VOL_HEADER_BYTES, fp) != AGI_VOL_HEADER_BYTES) {
|
||||
if (fread(header, 1, VOL_HEADER_BYTES, fp) != VOL_HEADER_BYTES) {
|
||||
return NULL;
|
||||
}
|
||||
if (header[0] != AGI_VOL_SIG_0 || header[1] != AGI_VOL_SIG_1) {
|
||||
if (header[0] != VOL_SIG_0 || header[1] != VOL_SIG_1) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
flagsAndVolume = header[2];
|
||||
if (flagsAndVolume & AGI_VOL_COMPRESSED) {
|
||||
// v3 LZW-compressed resource. Phase 2 work; not handled yet.
|
||||
// v3 resources are LZW-compressed and flag it in the volume byte.
|
||||
if ((header[2] & VOL_COMPRESSED) != 0u || (header[2] & VOL_VOLUME_MASK) != entry->volume) {
|
||||
return NULL;
|
||||
}
|
||||
if ((flagsAndVolume & AGI_VOL_VOLUME_MASK) != entry->volume) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
length = (uint16_t)(((uint16_t)header[4] << 8) | (uint16_t)header[3]);
|
||||
length = (uint16_t)(header[3] | ((uint16_t)header[4] << 8));
|
||||
if (length == 0u) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
payload = (uint8_t *)malloc(length);
|
||||
payload = (uint8_t *)jlAlloc(length);
|
||||
if (payload == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
if (fread(payload, 1, length, fp) != length) {
|
||||
free(payload);
|
||||
jlFree(payload);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (outLength != NULL) {
|
||||
*outLength = length;
|
||||
}
|
||||
|
|
@ -312,89 +349,107 @@ uint8_t *agiResLoad(const AgiGameT *game, AgiResTypeE type, uint16_t index, uint
|
|||
|
||||
|
||||
bool agiResOpen(AgiGameT *game) {
|
||||
char name[VOL_NAME_MAX];
|
||||
uint8_t t;
|
||||
uint8_t v;
|
||||
bool anyVolume;
|
||||
|
||||
memset(game, 0, sizeof(*game));
|
||||
|
||||
// Resource files are opened by bare name relative to DATA/ (jlDataOpen);
|
||||
// the game's data is whatever the packager staged there, so there is no
|
||||
// runtime game-directory to thread through.
|
||||
for (t = 0; t < AGI_RES_COUNT; t++) {
|
||||
if (!loadDirectory(game, resTypeFileName((AgiResTypeE)t), (AgiResTypeE)t)) {
|
||||
agiResClose(game);
|
||||
return false;
|
||||
for (t = 0u; t < AGI_RES_COUNT; t++) {
|
||||
loadDirectory(game, kDirNames[t], (AgiResTypeE)t);
|
||||
}
|
||||
if (game->resCount[AGI_RES_LOGIC] == 0u) {
|
||||
agiResClose(game);
|
||||
return false;
|
||||
}
|
||||
anyVolume = false;
|
||||
for (v = 0u; v < AGI_MAX_VOLUMES; v++) {
|
||||
buildVolName(name, v);
|
||||
game->volFiles[v] = jlDataOpen(name, "rb");
|
||||
if (game->volFiles[v] != NULL) {
|
||||
anyVolume = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!openVolumes(game)) {
|
||||
if (!anyVolume) {
|
||||
agiResClose(game);
|
||||
return false;
|
||||
}
|
||||
|
||||
// The dictionary is optional (loadWords only fails on OOM); a game
|
||||
// without WORDS.TOK still runs, just with no parser matches.
|
||||
if (!loadWords(game)) {
|
||||
agiResClose(game);
|
||||
return false;
|
||||
// A game without WORDS.TOK just never matches said(); without OBJECT
|
||||
// it has no inventory.
|
||||
game->words = loadFile("WORDS.TOK", AGI_WORDS_MAX, &game->wordsLen);
|
||||
if (game->words != NULL && game->wordsLen <= AGI_WORDS_HEADER_BYTES) {
|
||||
jlFree(game->words);
|
||||
game->words = NULL;
|
||||
}
|
||||
|
||||
if (!loadObjects(game)) {
|
||||
game->maxObjects = AGI_MAX_OBJECTS;
|
||||
}
|
||||
game->interpreterVersion = detectVersion();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
uint16_t agiWordId(const AgiGameT *game, const char *word) {
|
||||
// Find the longest dictionary entry that starts `text` and ends at a word
|
||||
// boundary (the end of text or a space). Entries can hold several words
|
||||
// ("pick up"). text is lower case with single spaces. Returns the word's
|
||||
// group, or AGI_WORD_NONE, and its length in *outLen.
|
||||
uint16_t agiWordMatch(const AgiGameT *game, const char *text, uint8_t *outLen) {
|
||||
char cur[AGI_WORD_MAX_LEN + 1u];
|
||||
const uint8_t *p;
|
||||
const uint8_t *end;
|
||||
char cur[AGI_WORD_MAX_LEN + 1u];
|
||||
uint16_t best;
|
||||
uint8_t bestLen;
|
||||
uint8_t len;
|
||||
uint8_t letter;
|
||||
uint16_t start;
|
||||
|
||||
if (game->words == NULL || word == NULL) {
|
||||
return AGI_WORD_UNKNOWN;
|
||||
*outLen = 0u;
|
||||
if (game->words == NULL || text[0] < 'a' || text[0] > 'z') {
|
||||
return AGI_WORD_NONE;
|
||||
}
|
||||
|
||||
// Word entries follow the fixed 52-byte letter-index header. Each entry:
|
||||
// 1 byte prefixLen -- chars shared with the previous word
|
||||
// n bytes suffix -- each (c & 0x7F) ^ 0x7F; the char with bit 7
|
||||
// set is the last one of the word
|
||||
// 2 bytes groupId -- big-endian
|
||||
// `cur` persists across entries so prefixLen chars carry forward.
|
||||
p = game->words + AGI_WORDS_HEADER_BYTES;
|
||||
end = game->words + game->wordsLen;
|
||||
len = 0u;
|
||||
letter = (uint8_t)(text[0] - 'a');
|
||||
start = (uint16_t)(((uint16_t)game->words[letter * WORD_INDEX_BYTES] << 8) | game->words[letter * WORD_INDEX_BYTES + 1u]);
|
||||
if (start == 0u || start >= game->wordsLen) {
|
||||
return AGI_WORD_NONE;
|
||||
}
|
||||
best = AGI_WORD_NONE;
|
||||
bestLen = 0u;
|
||||
p = game->words + start;
|
||||
end = game->words + game->wordsLen;
|
||||
while (p < end) {
|
||||
uint8_t prefixLen;
|
||||
uint8_t c;
|
||||
uint16_t id;
|
||||
|
||||
prefixLen = *p++;
|
||||
if (prefixLen > AGI_WORD_MAX_LEN) {
|
||||
break; // malformed dictionary
|
||||
len = *p++;
|
||||
if (len > AGI_WORD_MAX_LEN) {
|
||||
break;
|
||||
}
|
||||
len = prefixLen;
|
||||
for (;;) {
|
||||
uint8_t c;
|
||||
|
||||
do {
|
||||
if (p >= end) {
|
||||
return AGI_WORD_UNKNOWN;
|
||||
*outLen = bestLen;
|
||||
return best;
|
||||
}
|
||||
c = *p++;
|
||||
if (len < AGI_WORD_MAX_LEN) {
|
||||
cur[len] = (char)((c & 0x7Fu) ^ 0x7Fu);
|
||||
cur[len] = (char)((c & (uint8_t)~WORD_LAST_CHAR) ^ WORD_CHAR_XOR);
|
||||
len++;
|
||||
}
|
||||
if ((c & 0x80u) != 0u) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
cur[len] = '\0';
|
||||
|
||||
} while ((c & WORD_LAST_CHAR) == 0u);
|
||||
if (p + 1 >= end) {
|
||||
return AGI_WORD_UNKNOWN;
|
||||
break;
|
||||
}
|
||||
id = (uint16_t)(((uint16_t)p[0] << 8) | (uint16_t)p[1]);
|
||||
id = (uint16_t)(((uint16_t)p[0] << 8) | p[1]);
|
||||
p += 2;
|
||||
if (wordEqCI(cur, word)) {
|
||||
return id;
|
||||
// Entries are sorted, so once the first letter changes nothing
|
||||
// further can match.
|
||||
if (cur[0] != text[0]) {
|
||||
break;
|
||||
}
|
||||
if (len > bestLen && strncmp(cur, text, len) == 0 && (text[len] == '\0' || text[len] == ' ')) {
|
||||
best = id;
|
||||
bestLen = len;
|
||||
}
|
||||
}
|
||||
return AGI_WORD_UNKNOWN;
|
||||
*outLen = bestLen;
|
||||
return best;
|
||||
}
|
||||
|
|
|
|||
402
agiSave.c
Normal file
402
agiSave.c
Normal file
|
|
@ -0,0 +1,402 @@
|
|||
// save.game and restore.game.
|
||||
//
|
||||
// A save holds the interpreter state that is not rebuilt from the game
|
||||
// files: variables, flags, strings, the inventory, the screen objects,
|
||||
// the screen layout and the logics in memory. The picture is not saved;
|
||||
// as in Sierra's interpreter, the commands that built the current room's
|
||||
// picture (draw.pic, overlay.pic, add.to.pic, and the loads) are kept in
|
||||
// a script, which is saved and played back on restore. new.room empties
|
||||
// the script; f7 stops commands being added.
|
||||
//
|
||||
// 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
|
||||
// on one kind of machine is for that kind only.
|
||||
|
||||
#include "agi.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "joey/core.h"
|
||||
#include "joey/draw.h"
|
||||
#include "joey/file.h"
|
||||
|
||||
|
||||
#define SAVE_MAGIC_0 'J'
|
||||
#define SAVE_MAGIC_1 'A'
|
||||
#define SAVE_MAGIC_2 'G'
|
||||
#define SAVE_VERSION 1u
|
||||
#define SAVE_SLOTS 9u
|
||||
#define SAVE_DESC_MAX 30u
|
||||
#define SAVE_NAME_MAX 12u
|
||||
// The slot window is as wide as the screen allows with its frame.
|
||||
#define PICK_WIDTH (AGI_TEXT_COLS - 4u)
|
||||
|
||||
|
||||
// One saved object: AgiObjectT without its pointers and blit state.
|
||||
typedef struct {
|
||||
uint8_t viewId;
|
||||
uint8_t loop;
|
||||
uint8_t cel;
|
||||
uint8_t priority;
|
||||
int16_t x;
|
||||
int16_t y;
|
||||
uint16_t flags;
|
||||
uint8_t direction;
|
||||
uint8_t stepSize;
|
||||
uint8_t stepTime;
|
||||
uint8_t stepCount;
|
||||
uint8_t cycleTime;
|
||||
uint8_t cycleCount;
|
||||
uint8_t cycleMode;
|
||||
uint8_t motion;
|
||||
uint8_t loopFlag;
|
||||
uint8_t motionFlag;
|
||||
uint8_t targetX;
|
||||
uint8_t targetY;
|
||||
uint8_t savedStepSize;
|
||||
uint8_t randomSteps;
|
||||
uint8_t followDistance;
|
||||
} SavedObjectT;
|
||||
|
||||
typedef struct {
|
||||
char magic[3];
|
||||
uint8_t version;
|
||||
char description[SAVE_DESC_MAX + 1u];
|
||||
uint8_t vars[AGI_NUM_VARS];
|
||||
uint8_t flags[AGI_NUM_FLAGS];
|
||||
char strings[AGI_MAX_STRINGS][AGI_STRING_LEN + 1u];
|
||||
uint8_t itemRoom[AGI_MAX_ITEMS];
|
||||
AgiKeyMapT keyMaps[AGI_MAX_KEYMAPS];
|
||||
uint8_t keyMapCount;
|
||||
SavedObjectT objects[AGI_MAX_OBJECTS];
|
||||
bool logicLoaded[AGI_MAX_RESOURCES];
|
||||
uint16_t logicScanStart[AGI_MAX_RESOURCES];
|
||||
bool menuEnabled[AGI_MAX_MENU_ITEMS];
|
||||
uint8_t horizon;
|
||||
bool blockActive;
|
||||
uint8_t blockX1;
|
||||
uint8_t blockY1;
|
||||
uint8_t blockX2;
|
||||
uint8_t blockY2;
|
||||
bool playerControl;
|
||||
uint8_t picRow;
|
||||
uint8_t inputRow;
|
||||
uint8_t statusRow;
|
||||
bool statusOn;
|
||||
bool inputOn;
|
||||
uint8_t textFg;
|
||||
uint8_t textBg;
|
||||
char cursorChar;
|
||||
uint8_t scriptCount;
|
||||
AgiScriptEntryT script[AGI_SCRIPT_MAX];
|
||||
} SaveFileT;
|
||||
|
||||
|
||||
// ----- Prototypes -----
|
||||
|
||||
static bool fileBuffer(void);
|
||||
static uint8_t pickSlot(const char *title, bool mustExist);
|
||||
static bool readDescription(uint8_t slot, char *out);
|
||||
static void slotName(uint8_t slot, char *out);
|
||||
|
||||
|
||||
// ----- Module state -----
|
||||
|
||||
static SaveFileT *gFile;
|
||||
|
||||
|
||||
// ----- Internal helpers (alphabetical) -----
|
||||
|
||||
// The save file's image, allocated on first use (it is too big for the
|
||||
// IIgs's bank 0).
|
||||
static bool fileBuffer(void) {
|
||||
if (gFile == NULL) {
|
||||
gFile = (SaveFileT *)jlAlloc(sizeof(SaveFileT));
|
||||
}
|
||||
return gFile != NULL;
|
||||
}
|
||||
|
||||
|
||||
static uint8_t pickSlot(const char *title, bool mustExist) {
|
||||
char text[(SAVE_SLOTS + 3u) * (AGI_TEXT_COLS + 1u)];
|
||||
char desc[SAVE_DESC_MAX + 1u];
|
||||
uint8_t slot;
|
||||
uint16_t key;
|
||||
size_t n;
|
||||
|
||||
// A window listing the slots; a digit picks one.
|
||||
n = (size_t)snprintf(text, sizeof(text), "%s\n\n", title);
|
||||
for (slot = 1u; slot <= SAVE_SLOTS && n < sizeof(text); slot++) {
|
||||
if (!readDescription(slot, desc)) {
|
||||
strcpy(desc, "-");
|
||||
}
|
||||
n += (size_t)snprintf(text + n, sizeof(text) - n, "%u. %s\n", (unsigned)slot, desc);
|
||||
}
|
||||
if (n < sizeof(text)) {
|
||||
(void)snprintf(text + n, sizeof(text) - n, "\nPress 1-9, or ESC to cancel.");
|
||||
}
|
||||
textPrint(text, -1, -1, PICK_WIDTH);
|
||||
for (;;) {
|
||||
key = inputWaitKey();
|
||||
if (key == AGI_KEY_ESCAPE) {
|
||||
slot = 0u;
|
||||
break;
|
||||
}
|
||||
if (key >= (uint16_t)'1' && key <= (uint16_t)('0' + SAVE_SLOTS)) {
|
||||
slot = (uint8_t)(key - '0');
|
||||
if (!mustExist || readDescription(slot, desc)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
textCloseWindow();
|
||||
return slot;
|
||||
}
|
||||
|
||||
|
||||
// A slot's description, read from the start of its file.
|
||||
static bool readDescription(uint8_t slot, char *out) {
|
||||
char name[SAVE_NAME_MAX];
|
||||
FILE *fp;
|
||||
char head[offsetof(SaveFileT, description) + SAVE_DESC_MAX + 1u];
|
||||
|
||||
slotName(slot, name);
|
||||
fp = jlSaveOpen(name, "rb");
|
||||
if (fp == NULL) {
|
||||
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) {
|
||||
fclose(fp);
|
||||
return false;
|
||||
}
|
||||
fclose(fp);
|
||||
memcpy(out, &head[offsetof(SaveFileT, description)], SAVE_DESC_MAX + 1u);
|
||||
out[SAVE_DESC_MAX] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static void slotName(uint8_t slot, char *out) {
|
||||
(void)snprintf(out, SAVE_NAME_MAX, "AGISAVE%u", (unsigned)slot);
|
||||
}
|
||||
|
||||
|
||||
// ----- Public API (alphabetical) -----
|
||||
|
||||
bool restoreGame(void) {
|
||||
char name[SAVE_NAME_MAX];
|
||||
uint8_t slot;
|
||||
uint8_t i;
|
||||
uint16_t r;
|
||||
|
||||
slot = pickSlot("Restore which game?", true);
|
||||
if (slot == 0u) {
|
||||
return false;
|
||||
}
|
||||
if (!fileBuffer()) {
|
||||
return false;
|
||||
}
|
||||
slotName(slot, name);
|
||||
if (jlSaveRead(name, gFile, sizeof(SaveFileT)) != sizeof(SaveFileT) || gFile->version != SAVE_VERSION) {
|
||||
textMessageBox("Error in restoring game.");
|
||||
return false;
|
||||
}
|
||||
soundStop();
|
||||
gfxEraseAll();
|
||||
agiResetState();
|
||||
memcpy(gAgi->vars, gFile->vars, sizeof(gAgi->vars));
|
||||
memcpy(gAgi->flags, gFile->flags, sizeof(gAgi->flags));
|
||||
memcpy(gAgi->strings, gFile->strings, sizeof(gAgi->strings));
|
||||
memcpy(gAgi->itemRoom, gFile->itemRoom, sizeof(gAgi->itemRoom));
|
||||
memcpy(gAgi->keyMaps, gFile->keyMaps, sizeof(gAgi->keyMaps));
|
||||
gAgi->keyMapCount = gFile->keyMapCount;
|
||||
for (r = 0u; r < AGI_MAX_RESOURCES; r++) {
|
||||
gAgi->logicLoaded[r] = gFile->logicLoaded[r];
|
||||
gAgi->logicScanStart[r] = gFile->logicScanStart[r];
|
||||
}
|
||||
for (i = 0u; i < gAgi->menuItemCount && i < AGI_MAX_MENU_ITEMS; i++) {
|
||||
gAgi->menuItems[i].enabled = gFile->menuEnabled[i];
|
||||
}
|
||||
gAgi->horizon = gFile->horizon;
|
||||
gAgi->blockActive = gFile->blockActive;
|
||||
gAgi->blockX1 = gFile->blockX1;
|
||||
gAgi->blockY1 = gFile->blockY1;
|
||||
gAgi->blockX2 = gFile->blockX2;
|
||||
gAgi->blockY2 = gFile->blockY2;
|
||||
gAgi->playerControl = gFile->playerControl;
|
||||
gAgi->picRow = gFile->picRow;
|
||||
gAgi->inputRow = gFile->inputRow;
|
||||
gAgi->statusRow = gFile->statusRow;
|
||||
gAgi->statusOn = gFile->statusOn;
|
||||
gAgi->inputOn = gFile->inputOn;
|
||||
gAgi->textFg = gFile->textFg;
|
||||
gAgi->textBg = gFile->textBg;
|
||||
gAgi->cursorChar = gFile->cursorChar;
|
||||
// Rebuild the picture with no objects drawn, then the objects.
|
||||
gAgi->scriptCount = gFile->scriptCount;
|
||||
memcpy(gAgi->script, gFile->script, sizeof(gAgi->script));
|
||||
for (i = 0u; i < gAgi->scriptCount; i++) {
|
||||
vmReplay(&gAgi->script[i]);
|
||||
}
|
||||
for (i = 0u; i < AGI_MAX_OBJECTS; i++) {
|
||||
const SavedObjectT *s = &gFile->objects[i];
|
||||
AgiObjectT *o = &gAgi->objects[i];
|
||||
|
||||
memset(o, 0, sizeof(*o));
|
||||
if ((s->flags & OBJ_ANIMATED) != 0u) {
|
||||
(void)objSetView(i, s->viewId);
|
||||
}
|
||||
o->loop = s->loop;
|
||||
o->cel = s->cel;
|
||||
objSetLoop(i, s->loop);
|
||||
objSetCel(i, s->cel);
|
||||
o->priority = s->priority;
|
||||
o->x = s->x;
|
||||
o->y = s->y;
|
||||
o->prevX = s->x;
|
||||
o->prevY = s->y;
|
||||
o->flags = s->flags;
|
||||
o->direction = s->direction;
|
||||
o->stepSize = s->stepSize;
|
||||
o->stepTime = s->stepTime;
|
||||
o->stepCount = s->stepCount;
|
||||
o->cycleTime = s->cycleTime;
|
||||
o->cycleCount = s->cycleCount;
|
||||
o->cycleMode = s->cycleMode;
|
||||
o->motion = s->motion;
|
||||
o->loopFlag = s->loopFlag;
|
||||
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();
|
||||
jlSurfaceClear(jlStageGet(), AGI_COLOR_BLACK);
|
||||
gfxShowPic();
|
||||
textDrawStatus();
|
||||
textDrawInputLine();
|
||||
gAgi->flags[FLAG_RESTORED] = 1u;
|
||||
gAgi->restored = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void saveGame(void) {
|
||||
char name[SAVE_NAME_MAX];
|
||||
char desc[SAVE_DESC_MAX + 1u];
|
||||
uint8_t slot;
|
||||
uint8_t i;
|
||||
uint16_t r;
|
||||
|
||||
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()) {
|
||||
return;
|
||||
}
|
||||
memset(gFile, 0, sizeof(SaveFileT));
|
||||
gFile->magic[0] = SAVE_MAGIC_0;
|
||||
gFile->magic[1] = SAVE_MAGIC_1;
|
||||
gFile->magic[2] = SAVE_MAGIC_2;
|
||||
gFile->version = SAVE_VERSION;
|
||||
memcpy(gFile->description, desc, sizeof(desc));
|
||||
memcpy(gFile->vars, gAgi->vars, sizeof(gFile->vars));
|
||||
memcpy(gFile->flags, gAgi->flags, sizeof(gFile->flags));
|
||||
memcpy(gFile->strings, gAgi->strings, sizeof(gFile->strings));
|
||||
memcpy(gFile->itemRoom, gAgi->itemRoom, sizeof(gFile->itemRoom));
|
||||
memcpy(gFile->keyMaps, gAgi->keyMaps, sizeof(gFile->keyMaps));
|
||||
gFile->keyMapCount = gAgi->keyMapCount;
|
||||
for (r = 0u; r < AGI_MAX_RESOURCES; r++) {
|
||||
gFile->logicLoaded[r] = gAgi->logicLoaded[r];
|
||||
gFile->logicScanStart[r] = gAgi->logicScanStart[r];
|
||||
}
|
||||
for (i = 0u; i < gAgi->menuItemCount && i < AGI_MAX_MENU_ITEMS; i++) {
|
||||
gFile->menuEnabled[i] = gAgi->menuItems[i].enabled;
|
||||
}
|
||||
gFile->horizon = gAgi->horizon;
|
||||
gFile->blockActive = gAgi->blockActive;
|
||||
gFile->blockX1 = gAgi->blockX1;
|
||||
gFile->blockY1 = gAgi->blockY1;
|
||||
gFile->blockX2 = gAgi->blockX2;
|
||||
gFile->blockY2 = gAgi->blockY2;
|
||||
gFile->playerControl = gAgi->playerControl;
|
||||
gFile->picRow = gAgi->picRow;
|
||||
gFile->inputRow = gAgi->inputRow;
|
||||
gFile->statusRow = gAgi->statusRow;
|
||||
gFile->statusOn = gAgi->statusOn;
|
||||
gFile->inputOn = gAgi->inputOn;
|
||||
gFile->textFg = gAgi->textFg;
|
||||
gFile->textBg = gAgi->textBg;
|
||||
gFile->cursorChar = gAgi->cursorChar;
|
||||
for (i = 0u; i < AGI_MAX_OBJECTS; i++) {
|
||||
const AgiObjectT *o = &gAgi->objects[i];
|
||||
SavedObjectT *s = &gFile->objects[i];
|
||||
|
||||
s->viewId = o->viewId;
|
||||
s->loop = o->loop;
|
||||
s->cel = o->cel;
|
||||
s->priority = o->priority;
|
||||
s->x = o->x;
|
||||
s->y = o->y;
|
||||
s->flags = o->flags;
|
||||
s->direction = o->direction;
|
||||
s->stepSize = o->stepSize;
|
||||
s->stepTime = o->stepTime;
|
||||
s->stepCount = o->stepCount;
|
||||
s->cycleTime = o->cycleTime;
|
||||
s->cycleCount = o->cycleCount;
|
||||
s->cycleMode = o->cycleMode;
|
||||
s->motion = o->motion;
|
||||
s->loopFlag = o->loopFlag;
|
||||
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;
|
||||
memcpy(gFile->script, gAgi->script, sizeof(gFile->script));
|
||||
slotName(slot, name);
|
||||
if (!jlSaveWrite(name, gFile, sizeof(SaveFileT))) {
|
||||
textMessageBox("The game could not be saved.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Remember a picture-building command for restore (unless f7 blocks it).
|
||||
void saveScriptAdd(uint8_t op, uint8_t arg) {
|
||||
uint8_t args[AGI_SCRIPT_ARGS];
|
||||
|
||||
memset(args, 0, sizeof(args));
|
||||
args[0] = arg;
|
||||
saveScriptAddArgs(op, args);
|
||||
}
|
||||
|
||||
|
||||
void saveScriptAddArgs(uint8_t op, const uint8_t *args) {
|
||||
AgiScriptEntryT *e;
|
||||
|
||||
if (gAgi->flags[FLAG_NO_SCRIPT] != 0u || gAgi->scriptCount >= AGI_SCRIPT_MAX) {
|
||||
return;
|
||||
}
|
||||
e = &gAgi->script[gAgi->scriptCount];
|
||||
e->op = op;
|
||||
memcpy(e->args, args, AGI_SCRIPT_ARGS);
|
||||
gAgi->scriptCount++;
|
||||
}
|
||||
|
||||
|
||||
// new.room: the new room's picture starts from nothing.
|
||||
void saveScriptClear(void) {
|
||||
gAgi->scriptCount = 0u;
|
||||
}
|
||||
262
agiSound.c
Normal file
262
agiSound.c
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
// SOUND resources on JoeyLib's three tone voices and noise voice (AGI
|
||||
// specification 9.2 and 9.5).
|
||||
//
|
||||
// A sound is four voices -- three tones and the noise -- each a list of
|
||||
// 5-byte notes: a 16-bit duration in 1/60 s ticks, then the PCjr sound
|
||||
// chip bytes in reverse order: the high six bits of the tone's 10-bit
|
||||
// divisor, a command byte with its low four bits (or, for the noise
|
||||
// voice, the noise type and rate), and the attenuation (0 loudest, 15
|
||||
// off). The tone is 111860 / divisor Hz. A voice ends at an FF FF
|
||||
// duration. The sound's flag is set when the first voice ends, as in
|
||||
// Sierra's PC interpreter, whose speaker plays only that voice (measured:
|
||||
// longer second, third and noise voices do not delay it); the others play
|
||||
// on here until they end or another sound starts. With sound turned off
|
||||
// (f9 clear) the flag is set at once.
|
||||
//
|
||||
// soundPump runs the notes from the millisecond clock every frame, and
|
||||
// re-asserts the voices (the IIgs's voices are one-shots that must be
|
||||
// re-triggered about once a frame).
|
||||
|
||||
#include "agi.h"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include "joey/audio.h"
|
||||
#include "joey/core.h"
|
||||
|
||||
|
||||
#define SOUND_VOICES 4u
|
||||
#define SOUND_TONES 3u
|
||||
#define SOUND_NOISE 3u
|
||||
#define SOUND_HEADER_BYTES 8u
|
||||
#define NOTE_BYTES 5u
|
||||
#define NOTE_END 0xFFFFu
|
||||
#define TICKS_PER_SECOND 60u
|
||||
#define MS_PER_SECOND 1000u
|
||||
#define PSG_CLOCK_HZ 111860u
|
||||
#define DIVISOR_HIGH_MASK 0x3Fu
|
||||
#define DIVISOR_LOW_MASK 0x0Fu
|
||||
#define DIVISOR_HIGH_SHIFT 4u
|
||||
#define ATTEN_MASK 0x0Fu
|
||||
#define ATTEN_OFF 15u
|
||||
#define NOISE_RATE_MASK 0x03u
|
||||
#define NOISE_RATE_TONE3 3u
|
||||
// JoeyLib's noise pitch runs 0 (brightest) to 31: the chip's three fixed
|
||||
// rates an octave apart, and the third tone's divisor scaled down.
|
||||
#define NOISE_PITCH_FIRST 4u
|
||||
#define NOISE_PITCH_MAX 31u
|
||||
#define NOISE_DIVISOR_SHIFT 5u
|
||||
|
||||
|
||||
typedef struct {
|
||||
uint16_t pos;
|
||||
uint16_t ticksLeft;
|
||||
bool done;
|
||||
} VoiceT;
|
||||
|
||||
|
||||
// ----- Prototypes -----
|
||||
|
||||
static void playNote(uint8_t voice, const uint8_t *note);
|
||||
static void raiseFlag(void);
|
||||
static void release(void);
|
||||
static void silence(void);
|
||||
|
||||
|
||||
// ----- Module state -----
|
||||
|
||||
static uint8_t *gData;
|
||||
static uint16_t gLength;
|
||||
static VoiceT gVoices[SOUND_VOICES];
|
||||
static uint16_t gToneHz[SOUND_TONES];
|
||||
static uint8_t gToneAtten[SOUND_TONES];
|
||||
static uint16_t gToneDivisor[SOUND_TONES];
|
||||
static uint8_t gNoisePitch;
|
||||
static uint8_t gNoiseAtten;
|
||||
static uint32_t gTickStartMs;
|
||||
static uint32_t gTicksDone;
|
||||
// The flag sound() named, still to be set while the first voice plays.
|
||||
static uint8_t gFlag;
|
||||
static bool gFlagPending;
|
||||
|
||||
|
||||
// ----- Internal helpers (alphabetical) -----
|
||||
|
||||
static void playNote(uint8_t voice, const uint8_t *note) {
|
||||
uint16_t divisor;
|
||||
uint8_t atten;
|
||||
|
||||
atten = (uint8_t)(note[4] & ATTEN_MASK);
|
||||
if (voice == SOUND_NOISE) {
|
||||
uint8_t rate;
|
||||
|
||||
rate = (uint8_t)(note[3] & NOISE_RATE_MASK);
|
||||
if (rate == NOISE_RATE_TONE3) {
|
||||
gNoisePitch = (uint8_t)(gToneDivisor[SOUND_TONES - 1u] >> NOISE_DIVISOR_SHIFT);
|
||||
} else {
|
||||
gNoisePitch = (uint8_t)(NOISE_PITCH_FIRST << rate);
|
||||
}
|
||||
if (gNoisePitch > NOISE_PITCH_MAX) {
|
||||
gNoisePitch = NOISE_PITCH_MAX;
|
||||
}
|
||||
gNoiseAtten = atten;
|
||||
jlAudioNoise(gNoisePitch, gNoiseAtten);
|
||||
return;
|
||||
}
|
||||
divisor = (uint16_t)(((uint16_t)(note[2] & DIVISOR_HIGH_MASK) << DIVISOR_HIGH_SHIFT) | (note[3] & DIVISOR_LOW_MASK));
|
||||
gToneDivisor[voice] = divisor;
|
||||
gToneHz[voice] = (divisor == 0u) ? 0u : (uint16_t)(PSG_CLOCK_HZ / divisor);
|
||||
gToneAtten[voice] = atten;
|
||||
jlAudioVoice(voice, gToneHz[voice], atten);
|
||||
}
|
||||
|
||||
|
||||
static void raiseFlag(void) {
|
||||
if (gFlagPending) {
|
||||
gFlagPending = false;
|
||||
gAgi->flags[gFlag] = 1u;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// The sound's data goes and every voice falls silent.
|
||||
static void release(void) {
|
||||
silence();
|
||||
jlFree(gData);
|
||||
gData = NULL;
|
||||
}
|
||||
|
||||
|
||||
static void silence(void) {
|
||||
uint8_t v;
|
||||
|
||||
for (v = 0u; v < SOUND_TONES; v++) {
|
||||
gToneHz[v] = 0u;
|
||||
gToneAtten[v] = ATTEN_OFF;
|
||||
jlAudioVoice(v, 0u, ATTEN_OFF);
|
||||
}
|
||||
gNoiseAtten = ATTEN_OFF;
|
||||
jlAudioNoise(0u, ATTEN_OFF);
|
||||
}
|
||||
|
||||
|
||||
// ----- Public API (alphabetical) -----
|
||||
|
||||
void soundInit(void) {
|
||||
(void)jlAudioInit();
|
||||
silence();
|
||||
}
|
||||
|
||||
|
||||
// Advance the notes to the clock; called every frame.
|
||||
void soundPump(void) {
|
||||
uint32_t now;
|
||||
uint32_t due;
|
||||
uint8_t v;
|
||||
bool allDone;
|
||||
|
||||
if (gData == NULL) {
|
||||
return;
|
||||
}
|
||||
now = jlMillisElapsed();
|
||||
due = (uint32_t)((now - gTickStartMs) * TICKS_PER_SECOND / MS_PER_SECOND);
|
||||
while (gTicksDone < due && gData != NULL) {
|
||||
gTicksDone++;
|
||||
allDone = true;
|
||||
for (v = 0u; v < SOUND_VOICES; v++) {
|
||||
VoiceT *vc = &gVoices[v];
|
||||
|
||||
while (!vc->done && vc->ticksLeft == 0u) {
|
||||
const uint8_t *note;
|
||||
uint16_t dur;
|
||||
|
||||
if ((uint32_t)vc->pos + NOTE_BYTES > gLength) {
|
||||
vc->done = true;
|
||||
break;
|
||||
}
|
||||
note = &gData[vc->pos];
|
||||
dur = (uint16_t)(note[0] | ((uint16_t)note[1] << 8));
|
||||
if (dur == NOTE_END) {
|
||||
vc->done = true;
|
||||
break;
|
||||
}
|
||||
playNote(v, note);
|
||||
vc->pos = (uint16_t)(vc->pos + NOTE_BYTES);
|
||||
vc->ticksLeft = dur;
|
||||
}
|
||||
if (!vc->done) {
|
||||
vc->ticksLeft--;
|
||||
allDone = false;
|
||||
} else if (v < SOUND_TONES) {
|
||||
jlAudioVoice(v, 0u, ATTEN_OFF);
|
||||
gToneHz[v] = 0u;
|
||||
} else {
|
||||
gNoiseAtten = ATTEN_OFF;
|
||||
jlAudioNoise(0u, ATTEN_OFF);
|
||||
}
|
||||
}
|
||||
if (gVoices[0].done) {
|
||||
raiseFlag();
|
||||
}
|
||||
if (allDone) {
|
||||
release();
|
||||
}
|
||||
}
|
||||
#if defined(JOEYLIB_PLATFORM_IIGS)
|
||||
// The IIgs voices are one-shots: hold the notes by re-triggering.
|
||||
if (gData != NULL) {
|
||||
for (v = 0u; v < SOUND_TONES; v++) {
|
||||
if (gToneHz[v] != 0u && gToneAtten[v] < ATTEN_OFF) {
|
||||
jlAudioVoice(v, gToneHz[v], gToneAtten[v]);
|
||||
}
|
||||
}
|
||||
if (gNoiseAtten < ATTEN_OFF) {
|
||||
jlAudioNoise(gNoisePitch, gNoiseAtten);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
void soundShutdown(void) {
|
||||
release();
|
||||
jlAudioShutdown();
|
||||
}
|
||||
|
||||
|
||||
// sound(n, f): start a sound; f is set when it ends (or is stopped).
|
||||
void soundStart(uint8_t soundId, uint8_t flag) {
|
||||
uint8_t v;
|
||||
|
||||
soundStop();
|
||||
gFlag = flag;
|
||||
gAgi->flags[flag] = 0u;
|
||||
if (gAgi->flags[FLAG_SOUND] == 0u) {
|
||||
gAgi->flags[flag] = 1u;
|
||||
return;
|
||||
}
|
||||
gData = agiResLoad(&gAgi->game, AGI_RES_SOUND, soundId, &gLength);
|
||||
if (gData == NULL || gLength < SOUND_HEADER_BYTES) {
|
||||
jlFree(gData);
|
||||
gData = NULL;
|
||||
gAgi->flags[flag] = 1u;
|
||||
return;
|
||||
}
|
||||
for (v = 0u; v < SOUND_VOICES; v++) {
|
||||
gVoices[v].pos = (uint16_t)(gData[v * 2u] | ((uint16_t)gData[v * 2u + 1u] << 8));
|
||||
gVoices[v].ticksLeft = 0u;
|
||||
gVoices[v].done = gVoices[v].pos == 0u || gVoices[v].pos >= gLength;
|
||||
}
|
||||
gFlagPending = true;
|
||||
gTickStartMs = jlMillisElapsed();
|
||||
gTicksDone = 0u;
|
||||
}
|
||||
|
||||
|
||||
// stop.sound: silence, and the playing sound's flag is set.
|
||||
void soundStop(void) {
|
||||
raiseFlag();
|
||||
if (gData != NULL) {
|
||||
release();
|
||||
}
|
||||
}
|
||||
511
agiView.c
511
agiView.c
|
|
@ -1,395 +1,198 @@
|
|||
// AGI v2 VIEW (sprite/animation) decoder.
|
||||
// AGI v2 VIEW parsing (AGI specification chapter 8).
|
||||
//
|
||||
// Parses an in-RAM VIEW resource into a structured loop/cel table
|
||||
// once at load time, then renders any cel against the priority plane
|
||||
// for per-pixel actor-vs-picture masking. Re-implements the public
|
||||
// AGI VIEW byte layout from scratch.
|
||||
//
|
||||
// Pixel-doubled blit goes straight to the stage so animation cycles
|
||||
// don't require an intermediate back-buffer. The 160x168 AGI surface
|
||||
// maps onto stage columns (2 * agiX, 2 * agiX + 1) at the same destY
|
||||
// agiPicBlit uses.
|
||||
// A view is a header (loop count, description offset, a loop offset per
|
||||
// loop), each loop a cel count and cel offsets relative to the loop, each
|
||||
// cel a width, height and a byte holding the transparent colour (low
|
||||
// nibble) and the mirroring bits (high nibble), then run-length rows: a
|
||||
// byte of colour (high nibble) and run length (low nibble), 0 ending a
|
||||
// row. Loops that mirror each other share their cels; a cel whose
|
||||
// mirroring bit is set is drawn flipped in every loop except the one its
|
||||
// loop field names.
|
||||
|
||||
#include "agi.h"
|
||||
|
||||
#include "joey/draw.h"
|
||||
#include "surfaceInternal.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "joey/core.h"
|
||||
|
||||
|
||||
// VIEW header byte offsets (publicly documented v2 layout).
|
||||
#define AGI_VIEW_HDR_LOOP_COUNT 2u
|
||||
#define AGI_VIEW_HDR_LOOP_TABLE 5u
|
||||
|
||||
// Cel header bits (publicly documented v2 layout).
|
||||
#define AGI_CEL_TRANSPARENT_MASK 0x0Fu
|
||||
#define AGI_CEL_MIRROR_FLAG 0x80u
|
||||
#define AGI_CEL_MIRROR_SRC_SHIFT 4u
|
||||
#define AGI_CEL_MIRROR_SRC_MASK 0x07u
|
||||
|
||||
// Priority bands. Y 0..47 is sky (priority 4); Y 48..167 splits into
|
||||
// 10 ground bands of 12 pixels each at priorities 5..14. Priority 15
|
||||
// is "always on top" and reserved for HUD/overlay; an actor's natural
|
||||
// Y never reaches it.
|
||||
#define AGI_PRI_SKY_TOP 48
|
||||
#define AGI_PRI_GROUND_FIRST 5u
|
||||
#define AGI_PRI_GROUND_BAND_PX 12
|
||||
|
||||
|
||||
// ----- File-local types -----
|
||||
|
||||
// Per-draw invariants for one cel blit, grouped so they live in memory
|
||||
// rather than competing for the w65816's tiny register file. Passing a
|
||||
// pointer to this keeps the hot blit helpers under the backend's
|
||||
// register-allocation ceiling.
|
||||
typedef struct {
|
||||
const uint8_t *picPriority;
|
||||
int16_t x;
|
||||
int16_t width;
|
||||
uint8_t actorPri;
|
||||
uint8_t transparent;
|
||||
bool mirrored;
|
||||
} AgiCelBlitT;
|
||||
#define VIEW_LOOP_COUNT_BYTE 2u
|
||||
#define VIEW_DESC_OFFSET_BYTE 3u
|
||||
#define VIEW_LOOP_TABLE_BYTE 5u
|
||||
#define CEL_HEADER_BYTES 3u
|
||||
#define CEL_TRANSPARENT_MASK 0x0Fu
|
||||
#define CEL_MIRRORED 0x80u
|
||||
#define CEL_MIRROR_LOOP_SHIFT 4u
|
||||
#define CEL_MIRROR_LOOP_MASK 0x07u
|
||||
|
||||
|
||||
// ----- Prototypes -----
|
||||
|
||||
static void blitPlanar(const AgiCelBlitT *blit, const uint8_t *rle, int16_t height, int16_t topY, jlSurfaceT *stage, int16_t destY);
|
||||
static const uint8_t *blitRowChunky(const AgiCelBlitT *blit, const uint8_t *rle, const uint8_t *priRow, uint8_t *stageRow);
|
||||
static void markCelDirty(jlSurfaceT *stage, int16_t x, int16_t width, int16_t destY, int16_t topY, int16_t height);
|
||||
static bool parseLoop(AgiLoopInfoT *loop, const uint8_t *loopStart, uint16_t maxBytes);
|
||||
static const AgiCelInfoT *resolveMirror(const AgiViewT *view, uint8_t loopIdx, uint8_t celIdx, bool *outMirrored);
|
||||
static bool celCountOf(const uint8_t *raw, uint16_t length, uint8_t loopCount, uint16_t *outCels);
|
||||
static bool inUse(const AgiViewT *view);
|
||||
static AgiViewT *load(const AgiGameT *game, uint8_t viewId);
|
||||
static uint16_t readLe16(const uint8_t *p);
|
||||
|
||||
|
||||
// ----- Internal helpers (alphabetical) -----
|
||||
|
||||
// Planar fallback row walker (stage->pixels == NULL). Routes every
|
||||
// opaque, visible pixel through jlDrawPixel so the port's c2p /
|
||||
// plane-sync path stays correct. Split out of agiViewDraw so its
|
||||
// locals don't pile onto the chunky path's register budget.
|
||||
static void blitPlanar(const AgiCelBlitT *blit, const uint8_t *rle, int16_t height, int16_t topY, jlSurfaceT *stage, int16_t destY) {
|
||||
int16_t width;
|
||||
int16_t cy;
|
||||
int16_t cx;
|
||||
int16_t drawCx;
|
||||
int16_t agiX;
|
||||
int16_t agiY;
|
||||
int16_t stageX;
|
||||
int16_t stageY;
|
||||
uint8_t rleByte;
|
||||
uint8_t color;
|
||||
uint8_t runLen;
|
||||
uint8_t r;
|
||||
uint8_t picPri;
|
||||
// Check the loop table and every cel header against the resource's length
|
||||
// and count the cels, so the view can be built in one allocation.
|
||||
static bool celCountOf(const uint8_t *raw, uint16_t length, uint8_t loopCount, uint16_t *outCels) {
|
||||
uint8_t loop;
|
||||
uint8_t cel;
|
||||
uint8_t count;
|
||||
uint16_t loopOffset;
|
||||
uint16_t celOffset;
|
||||
|
||||
width = blit->width;
|
||||
for (cy = 0; cy < height; cy++) {
|
||||
cx = 0;
|
||||
for (;;) {
|
||||
rleByte = *rle++;
|
||||
if (rleByte == 0u) { break; }
|
||||
color = (uint8_t)(rleByte >> 4);
|
||||
runLen = (uint8_t)(rleByte & 0x0Fu);
|
||||
for (r = 0u; r < runLen; r++) {
|
||||
if (cx >= width) { break; }
|
||||
if (color != blit->transparent) {
|
||||
drawCx = blit->mirrored ? (int16_t)(width - 1 - cx) : cx;
|
||||
agiX = (int16_t)(blit->x + drawCx);
|
||||
agiY = (int16_t)(topY + cy);
|
||||
if (agiX >= 0 && agiX < (int16_t)AGI_PIC_WIDTH && agiY >= 0 && agiY < (int16_t)AGI_PIC_HEIGHT) {
|
||||
picPri = blit->picPriority[agiY * (int16_t)AGI_PIC_WIDTH + agiX];
|
||||
if (picPri <= blit->actorPri) {
|
||||
stageX = (int16_t)(agiX << 1);
|
||||
stageY = (int16_t)(destY + agiY);
|
||||
jlDrawPixel(stage, stageX, stageY, color);
|
||||
jlDrawPixel(stage, (int16_t)(stageX + 1), stageY, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
cx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Chunky fast-path walker for one source row. Consumes the cel's RLE
|
||||
// stream from `rle`, writing pixel-doubled bytes into `stageRow` where
|
||||
// the picture priority allows it, and returns the advanced RLE pointer
|
||||
// so the caller can hand it to the next row. A NULL priRow / stageRow
|
||||
// means the row is clipped out vertically: the RLE is still walked (so
|
||||
// the pointer lands correctly for later rows) but nothing is drawn.
|
||||
static const uint8_t *blitRowChunky(const AgiCelBlitT *blit, const uint8_t *rle, const uint8_t *priRow, uint8_t *stageRow) {
|
||||
int16_t width;
|
||||
int16_t cx;
|
||||
int16_t drawCx;
|
||||
int16_t agiX;
|
||||
uint8_t rleByte;
|
||||
uint8_t color;
|
||||
uint8_t runLen;
|
||||
uint8_t packed;
|
||||
uint8_t r;
|
||||
|
||||
width = blit->width;
|
||||
cx = 0;
|
||||
for (;;) {
|
||||
rleByte = *rle++;
|
||||
if (rleByte == 0u) { break; }
|
||||
color = (uint8_t)(rleByte >> 4);
|
||||
runLen = (uint8_t)(rleByte & 0x0Fu);
|
||||
// Transparent runs and vertically-clipped rows both just skip
|
||||
// ahead by runLen without touching the stage.
|
||||
if (color == blit->transparent || priRow == NULL) {
|
||||
cx = (int16_t)(cx + runLen);
|
||||
if (cx > width) { cx = width; }
|
||||
continue;
|
||||
}
|
||||
packed = (uint8_t)((color << 4) | color);
|
||||
for (r = 0u; r < runLen; r++) {
|
||||
if (cx >= width) { break; }
|
||||
drawCx = blit->mirrored ? (int16_t)(width - 1 - cx) : cx;
|
||||
agiX = (int16_t)(blit->x + drawCx);
|
||||
if (agiX >= 0 && agiX < (int16_t)AGI_PIC_WIDTH) {
|
||||
if (priRow[agiX] <= blit->actorPri) {
|
||||
stageRow[agiX] = packed;
|
||||
}
|
||||
}
|
||||
cx++;
|
||||
}
|
||||
}
|
||||
return rle;
|
||||
}
|
||||
|
||||
|
||||
// Mark the cel's stage bounding box dirty once after a chunky blit.
|
||||
// Computes the pixel-doubled bbox from the source rect, clips it to the
|
||||
// stage so the surface marker doesn't reject the whole rect for an
|
||||
// off-screen edge, and records it if any of it is on-screen.
|
||||
static void markCelDirty(jlSurfaceT *stage, int16_t x, int16_t width, int16_t destY, int16_t topY, int16_t height) {
|
||||
int16_t minStageX;
|
||||
int16_t maxStageX;
|
||||
int16_t minStageY;
|
||||
int16_t maxStageY;
|
||||
|
||||
minStageX = (int16_t)(x << 1);
|
||||
maxStageX = (int16_t)((x + width) << 1);
|
||||
minStageY = (int16_t)(destY + topY);
|
||||
maxStageY = (int16_t)(destY + topY + height);
|
||||
if (minStageX < 0) { minStageX = 0; }
|
||||
if (minStageY < 0) { minStageY = 0; }
|
||||
if (maxStageX > (int16_t)SURFACE_WIDTH) { maxStageX = (int16_t)SURFACE_WIDTH; }
|
||||
if (maxStageY > (int16_t)SURFACE_HEIGHT) { maxStageY = (int16_t)SURFACE_HEIGHT; }
|
||||
if (maxStageX > minStageX && maxStageY > minStageY) {
|
||||
surfaceMarkDirtyRect(stage, minStageX, minStageY,
|
||||
(int16_t)(maxStageX - minStageX),
|
||||
(int16_t)(maxStageY - minStageY));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static bool parseLoop(AgiLoopInfoT *loop, const uint8_t *loopStart, uint16_t maxBytes) {
|
||||
uint8_t celCount;
|
||||
uint8_t i;
|
||||
uint16_t celOffset;
|
||||
const uint8_t *celStart;
|
||||
uint8_t flags;
|
||||
|
||||
if (maxBytes < 1u) {
|
||||
return false;
|
||||
}
|
||||
celCount = loopStart[0];
|
||||
if ((uint16_t)celCount > AGI_MAX_CELS_PER_LOOP) {
|
||||
return false;
|
||||
}
|
||||
if (maxBytes < (uint16_t)(1u + (uint16_t)celCount * 2u)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
loop->celCount = celCount;
|
||||
for (i = 0u; i < celCount; i++) {
|
||||
celOffset = (uint16_t)(loopStart[1u + (uint16_t)i * 2u] |
|
||||
((uint16_t)loopStart[2u + (uint16_t)i * 2u] << 8));
|
||||
if ((uint16_t)(celOffset + 3u) > maxBytes) {
|
||||
*outCels = 0u;
|
||||
for (loop = 0u; loop < loopCount; loop++) {
|
||||
loopOffset = readLe16(&raw[VIEW_LOOP_TABLE_BYTE + (uint16_t)loop * 2u]);
|
||||
if (loopOffset >= length) {
|
||||
return false;
|
||||
}
|
||||
celStart = loopStart + celOffset;
|
||||
flags = celStart[2];
|
||||
loop->cels[i].width = celStart[0];
|
||||
loop->cels[i].height = celStart[1];
|
||||
loop->cels[i].transparentColor = (uint8_t)(flags & AGI_CEL_TRANSPARENT_MASK);
|
||||
loop->cels[i].mirrored = (uint8_t)((flags & AGI_CEL_MIRROR_FLAG) ? 1u : 0u);
|
||||
loop->cels[i].mirrorSourceLoop = (uint8_t)((flags >> AGI_CEL_MIRROR_SRC_SHIFT) & AGI_CEL_MIRROR_SRC_MASK);
|
||||
loop->cels[i].rleData = celStart + 3;
|
||||
count = raw[loopOffset];
|
||||
if ((uint32_t)loopOffset + 1u + (uint32_t)count * 2u > length) {
|
||||
return false;
|
||||
}
|
||||
for (cel = 0u; cel < count; cel++) {
|
||||
celOffset = (uint16_t)(loopOffset + readLe16(&raw[loopOffset + 1u + (uint16_t)cel * 2u]));
|
||||
if ((uint32_t)celOffset + CEL_HEADER_BYTES > length) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
*outCels = (uint16_t)(*outCels + count);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// If the cel is a mirror, swap to the source loop's same-index cel and
|
||||
// flag the caller to flip the RLE walk horizontally. Mirror chains
|
||||
// don't recurse in valid AGI data; we don't follow more than one hop.
|
||||
//
|
||||
// Sierra's encoder marks every cel in BOTH halves of a mirror pair
|
||||
// with the mirror flag, with the source-loop field pointing to the
|
||||
// lower-numbered loop. The cel is only an actual mirror when the
|
||||
// source-loop field points somewhere ELSE; when source == this loop,
|
||||
// this IS the source data and must be drawn unflipped (otherwise both
|
||||
// loops in a pair render mirrored and e.g. Graham faces left whether
|
||||
// the player presses left or right).
|
||||
static const AgiCelInfoT *resolveMirror(const AgiViewT *view, uint8_t loopIdx, uint8_t celIdx, bool *outMirrored) {
|
||||
const AgiCelInfoT *cel;
|
||||
uint8_t sourceLoop;
|
||||
static bool inUse(const AgiViewT *view) {
|
||||
uint8_t i;
|
||||
|
||||
if (loopIdx >= view->loopCount) {
|
||||
return NULL;
|
||||
}
|
||||
if (celIdx >= view->loops[loopIdx].celCount) {
|
||||
return NULL;
|
||||
}
|
||||
cel = &view->loops[loopIdx].cels[celIdx];
|
||||
*outMirrored = false;
|
||||
if (cel->mirrored) {
|
||||
sourceLoop = cel->mirrorSourceLoop;
|
||||
if (sourceLoop != loopIdx && sourceLoop < view->loopCount && celIdx < view->loops[sourceLoop].celCount) {
|
||||
cel = &view->loops[sourceLoop].cels[celIdx];
|
||||
*outMirrored = true;
|
||||
for (i = 0u; i < gAgi->objectCount; i++) {
|
||||
if (gAgi->objects[i].view == view) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return cel;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// The view, its loop table and every cel header share one allocation (on
|
||||
// the IIgs each is a Memory Manager handle); the cels point into raw.
|
||||
static AgiViewT *load(const AgiGameT *game, uint8_t viewId) {
|
||||
AgiViewT *view;
|
||||
AgiCelT *cels;
|
||||
uint8_t *raw;
|
||||
uint16_t length;
|
||||
uint16_t celTotal;
|
||||
uint16_t loopOffset;
|
||||
uint16_t celOffset;
|
||||
uint16_t descOffset;
|
||||
uint8_t loopCount;
|
||||
uint8_t loop;
|
||||
uint8_t cel;
|
||||
|
||||
raw = agiResLoad(game, AGI_RES_VIEW, viewId, &length);
|
||||
if (raw == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
loopCount = (length < VIEW_LOOP_TABLE_BYTE) ? 0u : raw[VIEW_LOOP_COUNT_BYTE];
|
||||
if (length < VIEW_LOOP_TABLE_BYTE || (uint32_t)VIEW_LOOP_TABLE_BYTE + (uint32_t)loopCount * 2u > length ||
|
||||
!celCountOf(raw, length, loopCount, &celTotal)) {
|
||||
jlFree(raw);
|
||||
return NULL;
|
||||
}
|
||||
view = (AgiViewT *)jlAlloc((uint32_t)sizeof(AgiViewT) + (uint32_t)loopCount * sizeof(AgiLoopT) + (uint32_t)celTotal * sizeof(AgiCelT));
|
||||
if (view == NULL) {
|
||||
jlFree(raw);
|
||||
return NULL;
|
||||
}
|
||||
view->raw = raw;
|
||||
view->rawLength = length;
|
||||
view->loopCount = loopCount;
|
||||
view->loops = (AgiLoopT *)(view + 1);
|
||||
view->description = NULL;
|
||||
descOffset = readLe16(&raw[VIEW_DESC_OFFSET_BYTE]);
|
||||
if (descOffset != 0u && descOffset < length) {
|
||||
view->description = (const char *)&raw[descOffset];
|
||||
}
|
||||
cels = (AgiCelT *)(view->loops + loopCount);
|
||||
for (loop = 0u; loop < loopCount; loop++) {
|
||||
loopOffset = readLe16(&raw[VIEW_LOOP_TABLE_BYTE + (uint16_t)loop * 2u]);
|
||||
view->loops[loop].celCount = raw[loopOffset];
|
||||
view->loops[loop].cels = cels;
|
||||
for (cel = 0u; cel < view->loops[loop].celCount; cel++) {
|
||||
celOffset = (uint16_t)(loopOffset + readLe16(&raw[loopOffset + 1u + (uint16_t)cel * 2u]));
|
||||
cels->width = raw[celOffset];
|
||||
cels->height = raw[celOffset + 1u];
|
||||
cels->transparent = (uint8_t)(raw[celOffset + 2u] & CEL_TRANSPARENT_MASK);
|
||||
cels->mirrorFlags = (uint8_t)(raw[celOffset + 2u] & (uint8_t)~CEL_TRANSPARENT_MASK);
|
||||
cels->rle = &raw[celOffset + CEL_HEADER_BYTES];
|
||||
cels++;
|
||||
}
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
|
||||
static uint16_t readLe16(const uint8_t *p) {
|
||||
return (uint16_t)(p[0] | ((uint16_t)p[1] << 8));
|
||||
}
|
||||
|
||||
|
||||
// ----- Public API (alphabetical) -----
|
||||
|
||||
uint8_t agiActorPriorityForY(int16_t y) {
|
||||
int16_t capped;
|
||||
int16_t band;
|
||||
// Look up a cel, clamping the loop and cel to the view's range. *outMirror
|
||||
// says whether it is drawn flipped in this loop.
|
||||
const AgiCelT *agiViewCel(const AgiViewT *view, uint8_t loop, uint8_t cel, bool *outMirror) {
|
||||
const AgiCelT *c;
|
||||
|
||||
if (y < AGI_PRI_SKY_TOP) {
|
||||
return 4u;
|
||||
*outMirror = false;
|
||||
if (view == NULL || view->loopCount == 0u) {
|
||||
return NULL;
|
||||
}
|
||||
capped = (y > (int16_t)(AGI_PIC_HEIGHT - 1)) ? (int16_t)(AGI_PIC_HEIGHT - 1) : y;
|
||||
band = (int16_t)(AGI_PRI_GROUND_FIRST + (capped - AGI_PRI_SKY_TOP) / AGI_PRI_GROUND_BAND_PX);
|
||||
if (band > 14) {
|
||||
band = 14;
|
||||
if (loop >= view->loopCount) {
|
||||
loop = (uint8_t)(view->loopCount - 1u);
|
||||
}
|
||||
return (uint8_t)band;
|
||||
if (view->loops[loop].celCount == 0u) {
|
||||
return NULL;
|
||||
}
|
||||
if (cel >= view->loops[loop].celCount) {
|
||||
cel = (uint8_t)(view->loops[loop].celCount - 1u);
|
||||
}
|
||||
c = &view->loops[loop].cels[cel];
|
||||
if ((c->mirrorFlags & CEL_MIRRORED) != 0u &&
|
||||
((c->mirrorFlags >> CEL_MIRROR_LOOP_SHIFT) & CEL_MIRROR_LOOP_MASK) != loop) {
|
||||
*outMirror = true;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
|
||||
void agiViewDraw(const AgiViewT *view, uint8_t loopIdx, uint8_t celIdx,
|
||||
int16_t x, int16_t y, uint8_t actorPri,
|
||||
const uint8_t *picPriority,
|
||||
jlSurfaceT *stage, int16_t destY) {
|
||||
const AgiCelInfoT *cel;
|
||||
AgiCelBlitT blit;
|
||||
const uint8_t *rle;
|
||||
bool mirrored;
|
||||
int16_t height;
|
||||
int16_t topY;
|
||||
uint8_t *stagePixels;
|
||||
// new.room: free the views no object still shows (an unanimated object
|
||||
// keeps its view for the next room, as ego does), as Sierra's interpreter
|
||||
// frees the old room's resources.
|
||||
void agiViewDiscardUnused(void) {
|
||||
uint16_t i;
|
||||
|
||||
cel = resolveMirror(view, loopIdx, celIdx, &mirrored);
|
||||
if (cel == NULL) {
|
||||
return;
|
||||
}
|
||||
height = (int16_t)cel->height;
|
||||
blit.picPriority = picPriority;
|
||||
blit.x = x;
|
||||
blit.width = (int16_t)cel->width;
|
||||
blit.actorPri = actorPri;
|
||||
blit.transparent = cel->transparentColor;
|
||||
blit.mirrored = mirrored;
|
||||
rle = cel->rleData;
|
||||
topY = (int16_t)(y - height + 1);
|
||||
stagePixels = (stage != NULL) ? stage->pixels : NULL;
|
||||
|
||||
// Per-pixel chunky fast path: pixel-doubled output means each
|
||||
// source pixel maps to exactly one stage byte (both nibbles =
|
||||
// same color). We can skip jlDrawPixel and write the byte
|
||||
// direct, marking the cel bbox dirty once at the end. ~80x
|
||||
// faster on DOSBox @ 386SX-equivalent CPU cycles.
|
||||
if (stagePixels != NULL) {
|
||||
int16_t cy;
|
||||
int16_t agiY;
|
||||
const uint8_t *priRow;
|
||||
uint8_t *stageRow;
|
||||
|
||||
for (cy = 0; cy < height; cy++) {
|
||||
agiY = (int16_t)(topY + cy);
|
||||
// Hoist per-row work out of the per-pixel inner loop.
|
||||
// For rows entirely outside the pic clip we still have to
|
||||
// walk the RLE so the byte pointer ends in the right place
|
||||
// for subsequent rows.
|
||||
if (agiY < 0 || agiY >= (int16_t)AGI_PIC_HEIGHT) {
|
||||
priRow = NULL;
|
||||
stageRow = NULL;
|
||||
} else {
|
||||
priRow = picPriority + (int32_t)agiY * (int32_t)AGI_PIC_WIDTH;
|
||||
stageRow = stagePixels +
|
||||
(int32_t)(destY + agiY) * (int32_t)SURFACE_BYTES_PER_ROW;
|
||||
}
|
||||
rle = blitRowChunky(&blit, rle, priRow, stageRow);
|
||||
}
|
||||
markCelDirty(stage, x, blit.width, destY, topY, height);
|
||||
return;
|
||||
}
|
||||
|
||||
// Planar fallback (s->pixels NULL: Amiga Phase 9). Goes through
|
||||
// jlDrawPixel so the port's c2p / plane-sync path stays correct.
|
||||
blitPlanar(&blit, rle, height, topY, stage, destY);
|
||||
}
|
||||
|
||||
|
||||
void agiViewFree(AgiViewT *view) {
|
||||
if (view->raw != NULL) {
|
||||
free(view->raw);
|
||||
view->raw = NULL;
|
||||
}
|
||||
view->rawLength = 0u;
|
||||
view->loopCount = 0u;
|
||||
}
|
||||
|
||||
|
||||
bool agiViewParse(AgiViewT *view, uint8_t *rawBytes, uint16_t length) {
|
||||
uint8_t loopCount;
|
||||
uint8_t i;
|
||||
uint16_t loopOffset;
|
||||
|
||||
memset(view, 0, sizeof(*view));
|
||||
if (length < AGI_VIEW_HDR_LOOP_TABLE) {
|
||||
free(rawBytes);
|
||||
return false;
|
||||
}
|
||||
|
||||
loopCount = rawBytes[AGI_VIEW_HDR_LOOP_COUNT];
|
||||
if ((uint16_t)loopCount > AGI_MAX_LOOPS_PER_VIEW) {
|
||||
free(rawBytes);
|
||||
return false;
|
||||
}
|
||||
if ((uint16_t)(AGI_VIEW_HDR_LOOP_TABLE + (uint16_t)loopCount * 2u) > length) {
|
||||
free(rawBytes);
|
||||
return false;
|
||||
}
|
||||
|
||||
view->raw = rawBytes;
|
||||
view->rawLength = length;
|
||||
view->loopCount = loopCount;
|
||||
|
||||
for (i = 0u; i < loopCount; i++) {
|
||||
loopOffset = (uint16_t)(rawBytes[AGI_VIEW_HDR_LOOP_TABLE + (uint16_t)i * 2u] |
|
||||
((uint16_t)rawBytes[AGI_VIEW_HDR_LOOP_TABLE + 1u + (uint16_t)i * 2u] << 8));
|
||||
if (loopOffset >= length) {
|
||||
agiViewFree(view);
|
||||
return false;
|
||||
}
|
||||
if (!parseLoop(&view->loops[i], rawBytes + loopOffset, (uint16_t)(length - loopOffset))) {
|
||||
agiViewFree(view);
|
||||
return false;
|
||||
for (i = 0u; i < AGI_MAX_RESOURCES; i++) {
|
||||
if (gAgi->views[i] != NULL && !inUse(gAgi->views[i])) {
|
||||
jlFree(gAgi->views[i]->raw);
|
||||
jlFree(gAgi->views[i]);
|
||||
gAgi->views[i] = NULL;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// A view from the cache, loaded on first use; NULL when the game has none
|
||||
// by that number. Within a room views stay loaded (discard.view is a
|
||||
// no-op).
|
||||
const AgiViewT *agiViewGet(uint8_t viewId) {
|
||||
if (gAgi->views[viewId] == NULL) {
|
||||
gAgi->views[viewId] = load(&gAgi->game, viewId);
|
||||
}
|
||||
return gAgi->views[viewId];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ For an AGI v2 game directory the loader needs:
|
|||
- `OBJECT` - inventory table
|
||||
- `WORDS.TOK` - parser dictionary
|
||||
|
||||
and it reads Sierra's interpreter version from `AGIDATA.OVL` when the
|
||||
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.
|
||||
|
||||
File names are uppercase by AGI convention; the loader looks them up
|
||||
literally so the directory should match.
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
AGI_HOME := $(abspath $(dir $(lastword $(MAKEFILE_LIST))))
|
||||
|
||||
PROGRAMS += AGI
|
||||
AGI_SRCS := $(addprefix $(AGI_HOME)/,agi.c agiRes.c agiPic.c agiView.c agiVm.c agiObj.c agiText.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)
|
||||
AGI_TITLE := AGI
|
||||
AGI_HELP := A Sierra AGI interpreter. The files of the game it plays go in its DATA folder.
|
||||
|
||||
|
|
|
|||
|
|
@ -56,10 +56,11 @@ cp "$agiBin" "$work/AGI#B30000"
|
|||
"$CADIUS" CREATEFOLDER "$out" "/$VOL/DATA" >/dev/null
|
||||
|
||||
# Game resources as plain ProDOS BIN ($06). Only the subset AGI v2 reads
|
||||
# (the four DIR indexes, the VOL.x archives, OBJECT, WORDS.TOK) is copied;
|
||||
# Sierra's original binary / OVL drivers are left behind.
|
||||
# (the four DIR indexes, the VOL.x archives, OBJECT, WORDS.TOK, and
|
||||
# AGIDATA.OVL for the interpreter version its quirks follow) is copied;
|
||||
# Sierra's original binary and the other OVL drivers are left behind.
|
||||
added=0
|
||||
for f in LOGDIR PICDIR VIEWDIR SNDDIR OBJECT WORDS.TOK; do
|
||||
for f in LOGDIR PICDIR VIEWDIR SNDDIR OBJECT WORDS.TOK AGIDATA.OVL; do
|
||||
if [ -f "$gamedata/$f" ]; then
|
||||
cp "$gamedata/$f" "$work/${f}#060000"
|
||||
"$CADIUS" ADDFILE "$out" "/$VOL/DATA" "$work/${f}#060000" >/dev/null
|
||||
|
|
|
|||
124
scripts/oracle.sh
Executable file
124
scripts/oracle.sh
Executable file
|
|
@ -0,0 +1,124 @@
|
|||
#!/usr/bin/env bash
|
||||
# oracle.sh - play the test probes (tests/probes.py) and the King's Quest
|
||||
# III sessions (tests/kq3/*.scr) on Sierra's own interpreter and on
|
||||
# JoeyAGI, and compare the frames.
|
||||
#
|
||||
# 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
|
||||
# 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
|
||||
# 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
|
||||
# scripts/test-agi.sh). tests/tools/compareFrames.py then compares each
|
||||
# shot, reading text with each interpreter's own font.
|
||||
#
|
||||
# A probe whose script records (shake) is compared by what
|
||||
# tests/tools/shakeMeasure.py reads out of the two recordings.
|
||||
#
|
||||
# DOSBox runs in real time and Sierra's interpreter starts a moment later
|
||||
# than the host's virtual clock, so shots of things in motion can be a
|
||||
# cycle or two apart; 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
|
||||
# rows differ from run to run on Sierra's side; everything else matches.
|
||||
#
|
||||
# Usage: scripts/oracle.sh [PROBE | kq3-SESSION ...] every probe and
|
||||
# session when none is named
|
||||
# Output: build/oracle/PROBE/{oracle,joey}/ and a diff image per shot
|
||||
# Needs: games/kq3 with Sierra's interpreter, Xvfb, xdotool, python3 +
|
||||
# PIL, JoeyLib's DOSBox Staging (./install.sh install emulators)
|
||||
set -uo pipefail
|
||||
|
||||
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
JOEYLIB=$(cd "${JOEYLIB:-$repo/../joeylib}" && pwd) || { echo "oracle: no JoeyLib checkout (set JOEYLIB)" >&2; exit 2; }
|
||||
export JOEYLIB
|
||||
sierra=$repo/games/kq3
|
||||
tools=$repo/tests/tools
|
||||
build=$repo/build/oracle
|
||||
agiHost=$repo/build/tests/agiHost
|
||||
parallel=${ORACLE_JOBS:-4}
|
||||
|
||||
[[ -f $sierra/AGI && -f $sierra/KQ3.COM ]] || { echo "oracle: needs Sierra's interpreter in games/kq3" >&2; exit 2; }
|
||||
for tool in Xvfb xdotool python3; do
|
||||
command -v "$tool" >/dev/null || { echo "oracle: needs $tool" >&2; exit 2; }
|
||||
done
|
||||
[[ -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; }
|
||||
|
||||
sessions=$(for f in "$repo"/tests/kq3/*.scr; do [[ -f $f ]] && echo "kq3-$(basename "${f%.scr}")"; done)
|
||||
probes=${*:-$(python3 "$repo/tests/probes.py" list) $sessions}
|
||||
mkdir -p "$build"
|
||||
|
||||
# One probe on both interpreters.
|
||||
one() {
|
||||
local name=$1
|
||||
local game=$build/$name/game
|
||||
local run
|
||||
local script
|
||||
rm -rf "${build:?}/$name"
|
||||
mkdir -p "$build/$name/oracle" "$build/$name/joey"
|
||||
if [[ $name == kq3-* ]]; then
|
||||
cp -r "$sierra" "$game"
|
||||
script=$repo/tests/kq3/${name#kq3-}.scr
|
||||
else
|
||||
python3 "$repo/tests/probes.py" build "$build/$name/.probe" "$name" || return 1
|
||||
mv "$build/$name/.probe/$name" "$game"
|
||||
rmdir "$build/$name/.probe"
|
||||
cp "$sierra"/AGI "$sierra"/*.OVL "$sierra"/HGC_FONT "$sierra"/KQ3.COM "$game/"
|
||||
script=$game/probe.scr
|
||||
fi
|
||||
timeout -s KILL 900 python3 "$tools/dosDrive.py" "$game" KQ3.COM "$build/$name/oracle" "$script" \
|
||||
--set joysticktype=none > "$build/$name/oracle.log" 2>&1
|
||||
run=$(mktemp -d "$build/run.XXXXXX")
|
||||
ln -s "$game" "$run/DATA"
|
||||
mkdir "$run/SAVES"
|
||||
(cd "$run" && timeout -s KILL 900 "$agiHost" "$script" "$build/$name/joey") > "$build/$name/joey.log" 2>&1
|
||||
rm -rf "$run"
|
||||
}
|
||||
export -f one
|
||||
export build repo sierra tools agiHost
|
||||
|
||||
# The fonts first: every comparison reads text with them.
|
||||
one font
|
||||
printf '%s\n' $probes | grep -vx font | xargs -P "$parallel" -I{} bash -c 'one {}'
|
||||
|
||||
fontArgs=(--fonts "$build/font/oracle/font.png" "$build/font/joey/font.ppm")
|
||||
total=0
|
||||
differ=0
|
||||
for name in $probes; do
|
||||
echo "=== $name"
|
||||
for shot in "$build/$name"/oracle/*.png; do
|
||||
[[ -f $shot ]] || continue
|
||||
base=$(basename "${shot%.png}")
|
||||
joey=$build/$name/joey/$base.ppm
|
||||
total=$((total + 1))
|
||||
if [[ ! -f $joey ]]; then
|
||||
echo " $base: JoeyAGI took no such shot"
|
||||
differ=$((differ + 1))
|
||||
continue
|
||||
fi
|
||||
if result=$(python3 "$tools/compareFrames.py" "$shot" "$joey" "$build/$name/diff-$base.png" "${fontArgs[@]}"); then
|
||||
echo " $base: same"
|
||||
else
|
||||
echo " $base: DIFFERS $result"
|
||||
differ=$((differ + 1))
|
||||
fi
|
||||
done
|
||||
# Recordings (shake.screen): the shakes read out of both.
|
||||
for video in "$build/$name"/oracle/*.avi; do
|
||||
[[ -f $video ]] || continue
|
||||
base=$(basename "${video%.avi}")
|
||||
total=$((total + 1))
|
||||
if result=$(python3 "$tools/shakeMeasure.py" "$video" "$build/$name/joey/$base.frames"); then
|
||||
echo " $base (recording): same"
|
||||
else
|
||||
echo " $base (recording): DIFFERS"
|
||||
differ=$((differ + 1))
|
||||
fi
|
||||
echo "$result" | sed 's/^/ /'
|
||||
done
|
||||
done
|
||||
echo
|
||||
echo "oracle: $((total - differ)) of $total shots match (diff images in $build/<probe>/)"
|
||||
[[ $differ == 0 ]]
|
||||
|
|
@ -1,205 +1,204 @@
|
|||
#!/usr/bin/env bash
|
||||
# Host-side regression tests for the AGI port.
|
||||
# test-agi.sh - JoeyAGI's host gate: the whole interpreter, built for the
|
||||
# build machine on JoeyLib's headless BLANK port (tests/hostPort.c), plays
|
||||
# scripted sessions and every frame it is asked for must hash as expected.
|
||||
#
|
||||
# Compiles the tests in tests/ with the interpreter sources they cover,
|
||||
# using the host gcc and JoeyLib's headers, and runs them against every
|
||||
# game directory in games/. Each game is exercised in isolation; the
|
||||
# script aggregates pass/fail counts and exits non-zero if any test
|
||||
# fails.
|
||||
# 1. testResources: every resource of every game in games/ loads, every
|
||||
# PICTURE decodes and every VIEW parses; KQ3's pictures (visual and
|
||||
# priority) must hash as tests/expected/resources-kq3.txt says --
|
||||
# those frames were checked pixel for pixel against Sierra's
|
||||
# interpreter.
|
||||
# 2. The probes: small games tests/probes.py builds from scratch, one per
|
||||
# subsystem (pictures, windows, motion, control lines, priority,
|
||||
# parser, menus, inventory, sound timing, ...), each played by its
|
||||
# own script. Expected hashes in tests/expected/probes/.
|
||||
# 3. With games/kq3 present, the KQ3 sessions in tests/kq3/*.scr (the
|
||||
# opening, walking, typing, menus, debug teleports). Expected hashes
|
||||
# in tests/expected/kq3/.
|
||||
#
|
||||
# Why a host build instead of running the emulator binaries: the AGI
|
||||
# resource loader is plain stdio C with no platform dependencies, so
|
||||
# the parsing logic is identical on every JoeyLib target. Validating
|
||||
# it on the build host means each iteration takes a second instead
|
||||
# of the 15-30s of an emulator boot, and reproducible failures don't
|
||||
# depend on capturing video / log output through DOSBox or MAME.
|
||||
# Time inside a run is virtual (one frame per jlWaitVBL), so a run is
|
||||
# exactly reproducible. Every run leaves its frames as PPMs in
|
||||
# build/tests/out/NAME/.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/test-agi.sh # test every subdir of games/
|
||||
# scripts/test-agi.sh kq3 agidemo # test only the named subdirs
|
||||
# Env:
|
||||
# JOEYLIB the JoeyLib checkout (default ../joeylib)
|
||||
|
||||
set -euo pipefail
|
||||
# Usage: scripts/test-agi.sh [--update] [NAME...]
|
||||
# NAME is a probe, kq3-SESSION or resources-GAME; --update
|
||||
# rewrites the expected hashes from this run (check the frames
|
||||
# first, e.g. with scripts/oracle.sh).
|
||||
# Env: JOEYLIB the JoeyLib checkout (default ../joeylib)
|
||||
# Exit: 0 pass, 1 fail, 2 skipped (no JoeyLib / host compiler / python3)
|
||||
set -uo pipefail
|
||||
|
||||
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
JOEYLIB=${JOEYLIB:-$repo/../joeylib}
|
||||
[[ -f $JOEYLIB/include/joey/joey.h ]] || { echo "test-agi: no JoeyLib checkout at $JOEYLIB (set JOEYLIB)" >&2; exit 2; }
|
||||
gamedata=$repo/games
|
||||
command -v cc >/dev/null || { echo "test-agi: no host C compiler" >&2; exit 2; }
|
||||
command -v python3 >/dev/null || { echo "test-agi: no python3" >&2; exit 2; }
|
||||
|
||||
update=0
|
||||
if [[ ${1:-} == --update ]]; then
|
||||
update=1
|
||||
shift
|
||||
fi
|
||||
only=" $* "
|
||||
|
||||
build=$repo/build/tests
|
||||
testRes=$build/testAgiRes
|
||||
testPic=$build/testAgiPic
|
||||
testView=$build/testAgiView
|
||||
testVm=$build/testAgiVm
|
||||
testPipeline=$build/testAgiPipeline
|
||||
obj=$build/obj
|
||||
out=$build/out
|
||||
expected=$repo/tests/expected
|
||||
mkdir -p "$obj" "$out"
|
||||
|
||||
mkdir -p "$build"
|
||||
# ----- Build: JoeyLib's core and generic code with the host port -----
|
||||
|
||||
# Shared host compile flags. agiPic.c calls jlDrawPixel in the blit
|
||||
# step, which the host test never invokes; we stub it as an empty
|
||||
# function via the link-line shim below so the host build can resolve
|
||||
# the symbol without dragging in the JoeyLib runtime.
|
||||
# Pick a JoeyLib platform for the host build. None of the platform-
|
||||
# specific code paths are exercised by the host test (it only touches
|
||||
# the AGI parsers, never the JoeyLib HAL), but joey/platform.h #errors
|
||||
# unless exactly one is defined. DOS is the closest fit for a Linux
|
||||
# host (chunky, little-endian, x86 lineage).
|
||||
hostCflags=(-std=c99 -Wall -Wextra -Werror -O2 -DJOEYLIB_PLATFORM_DOS -I "$JOEYLIB/include" -I "$repo" -I "$JOEYLIB/src/core")
|
||||
stubSrc=$build/hostStubs.c
|
||||
cat > "$stubSrc" <<'EOF'
|
||||
#include "joey/draw.h"
|
||||
#include "joey/core.h"
|
||||
#include "joey/file.h"
|
||||
#include "surfaceInternal.h"
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
// Host jlDataOpen: the real one (assetLoad.c) forces a DATA/ prefix, but
|
||||
// the test gamedata dirs hold their files flat. Open <AGI_DATA_DIR>/<name>
|
||||
// so agiRes.c's bare-name jlDataOpen calls resolve against the game dir the
|
||||
// harness points us at.
|
||||
FILE *jlDataOpen(const char *name, const char *mode) {
|
||||
const char *base = getenv("AGI_DATA_DIR");
|
||||
char path[1024];
|
||||
if (base == NULL) { base = "."; }
|
||||
if ((size_t)snprintf(path, sizeof(path), "%s/%s", base, name) >= sizeof(path)) { return NULL; }
|
||||
return fopen(path, mode);
|
||||
}
|
||||
void jlDrawPixel(jlSurfaceT *s, int16_t x, int16_t y, uint8_t c) { (void)s; (void)x; (void)y; (void)c; }
|
||||
void jlFillRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t c) { (void)s; (void)x; (void)y; (void)w; (void)h; (void)c; }
|
||||
uint32_t jlRandom(void) { return (uint32_t)rand(); }
|
||||
uint16_t jlRandomRange(uint16_t bound) { return (bound == 0u) ? 0u : (uint16_t)((unsigned)rand() % bound); }
|
||||
void jlRandomSeed(uint32_t seed) { srand((unsigned)seed); }
|
||||
void *jlAlloc(uint32_t bytes) { return malloc((size_t)bytes); }
|
||||
void jlFree(void *p) { free(p); }
|
||||
void jlLogF(const char *fmt, ...) { (void)fmt; }
|
||||
void jlLogFlush(void) { }
|
||||
void jlLog(const char *msg) { (void)msg; }
|
||||
/* surfaceMarkDirtyRect is a MACRO since the Phase-6 mark rework -- the
|
||||
* host TUs (agiPic/agiView) expand it inline, so the stubs provide the
|
||||
* band state it references instead of a function. gStage stays NULL,
|
||||
* so every expansion takes the s != gStage no-op branch; the symbols
|
||||
* only need to exist to link. */
|
||||
jlSurfaceT *gStage = 0;
|
||||
uint8_t gStageMinWord[SURFACE_HEIGHT];
|
||||
uint8_t gStageMaxWord[SURFACE_HEIGHT];
|
||||
void surfaceMarkDirtyRows(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord) { (void)y; (void)yEnd; (void)minWord; (void)maxWord; }
|
||||
void surfaceMarkDirtyAll(const jlSurfaceT *s) { (void)s; }
|
||||
EOF
|
||||
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")
|
||||
engine=(agi agiRes agiPic agiView agiGfx agiObj agiText agiFont agiInput agiSound agiSave agiVm)
|
||||
|
||||
echo "Building host tests..."
|
||||
gcc "${hostCflags[@]}" \
|
||||
"$repo/tests/testAgiRes.c" \
|
||||
"$repo/agiRes.c" \
|
||||
"$stubSrc" \
|
||||
-o "$testRes"
|
||||
|
||||
gcc "${hostCflags[@]}" \
|
||||
"$repo/tests/testAgiPic.c" \
|
||||
"$repo/agiRes.c" \
|
||||
"$repo/agiPic.c" \
|
||||
"$stubSrc" \
|
||||
-o "$testPic"
|
||||
|
||||
gcc "${hostCflags[@]}" \
|
||||
"$repo/tests/testAgiView.c" \
|
||||
"$repo/agiRes.c" \
|
||||
"$repo/agiPic.c" \
|
||||
"$repo/agiView.c" \
|
||||
"$stubSrc" \
|
||||
-o "$testView"
|
||||
|
||||
gcc "${hostCflags[@]}" \
|
||||
"$repo/tests/testAgiVm.c" \
|
||||
"$repo/agiRes.c" \
|
||||
"$repo/agiVm.c" \
|
||||
"$repo/agiObj.c" \
|
||||
"$stubSrc" \
|
||||
-o "$testVm"
|
||||
|
||||
gcc "${hostCflags[@]}" \
|
||||
"$repo/tests/testAgiPipeline.c" \
|
||||
"$repo/agiRes.c" \
|
||||
"$repo/agiVm.c" \
|
||||
"$repo/agiObj.c" \
|
||||
"$stubSrc" \
|
||||
-o "$testPipeline"
|
||||
|
||||
# Pick the game directories to exercise. With no args, every
|
||||
# subdirectory of games/ that contains LOGDIR is a candidate (skips
|
||||
# stray README files etc).
|
||||
if [[ $# -gt 0 ]]; then
|
||||
games=("$@")
|
||||
else
|
||||
games=()
|
||||
if [[ -d $gamedata ]]; then
|
||||
for dir in "$gamedata"/*/; do
|
||||
[[ -d $dir ]] || continue
|
||||
name=$(basename "$dir")
|
||||
if [[ -f $dir/LOGDIR ]]; then
|
||||
games+=("$name")
|
||||
fi
|
||||
done
|
||||
# compile SRC OBJ FLAGS...: when the source, or any header, is newer.
|
||||
compile() {
|
||||
local src=$1 o=$2
|
||||
shift 2
|
||||
if [[ -f $o && ! $src -nt $o ]] && [[ -z $(find "$repo" "$repo/tests" "$JOEYLIB/include" "$JOEYLIB/src/core" -maxdepth 2 -name '*.h' -newer "$o" -print -quit) ]]; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
cc "$@" -c "$src" -o "$o"
|
||||
}
|
||||
|
||||
if [[ ${#games[@]} -eq 0 ]]; then
|
||||
echo
|
||||
echo "No AGI game directories found under $gamedata"
|
||||
echo "Drop an AGI v2 game in games/<name>/ and rerun."
|
||||
echo "See games/README.md for the required files."
|
||||
# Nothing was tested: not a pass (a gate runner reads 2 as skipped).
|
||||
exit 2
|
||||
fi
|
||||
echo "Building the host interpreter..."
|
||||
ok=1
|
||||
jlObjs=()
|
||||
for f in "$JOEYLIB"/src/core/*.c "$JOEYLIB"/src/generic/*.c; do
|
||||
o=$obj/jl_$(basename "${f%.c}").o
|
||||
compile "$f" "$o" "${jlFlags[@]}" -w || ok=0
|
||||
jlObjs+=("$o")
|
||||
done
|
||||
agiObjs=()
|
||||
for f in "${engine[@]}"; do
|
||||
o=$obj/$f.o
|
||||
if [[ $f == agi ]]; then
|
||||
compile "$repo/$f.c" "$o" "${agiFlags[@]}" -Dmain=agiMain || ok=0
|
||||
else
|
||||
compile "$repo/$f.c" "$o" "${agiFlags[@]}" || ok=0
|
||||
fi
|
||||
agiObjs+=("$o")
|
||||
done
|
||||
for f in hostPort agiHost testResources; do
|
||||
compile "$repo/tests/$f.c" "$obj/$f.o" "${agiFlags[@]}" || ok=0
|
||||
done
|
||||
[[ $ok == 1 ]] || { echo "test-agi: FAIL (build)" >&2; exit 1; }
|
||||
cc -o "$build/agiHost" "$obj/agiHost.o" "$obj/hostPort.o" "${agiObjs[@]}" "${jlObjs[@]}" -lm || exit 1
|
||||
cc -o "$build/testResources" "$obj/testResources.o" "$obj/hostPort.o" "${agiObjs[@]}" "${jlObjs[@]}" -lm || exit 1
|
||||
|
||||
passed=0
|
||||
failed=0
|
||||
echo
|
||||
for game in "${games[@]}"; do
|
||||
dir=$gamedata/$game
|
||||
echo "===== $game ====="
|
||||
if [[ ! -d $dir ]]; then
|
||||
echo " FAIL: $dir does not exist"
|
||||
skipped=0
|
||||
|
||||
# check NAME EXPECTFILE ACTUALFILE: compare, or with --update record.
|
||||
check() {
|
||||
local name=$1 want=$2 got=$3
|
||||
if [[ $update == 1 ]]; then
|
||||
mkdir -p "$(dirname "$want")"
|
||||
cp "$got" "$want"
|
||||
echo " $name: recorded $(wc -l < "$got") lines"
|
||||
passed=$((passed + 1))
|
||||
return
|
||||
fi
|
||||
if [[ ! -f $want ]]; then
|
||||
echo " $name: FAIL (no $want; record it with --update once the frames are checked)"
|
||||
failed=$((failed + 1))
|
||||
continue
|
||||
return
|
||||
fi
|
||||
|
||||
# agiRes.c opens data by bare name via jlDataOpen; the host stub
|
||||
# resolves those against AGI_DATA_DIR (see stubSrc above).
|
||||
export AGI_DATA_DIR="$dir"
|
||||
gameOk=1
|
||||
echo "--- resource loader ---"
|
||||
if ! "$testRes" "$dir"; then
|
||||
gameOk=0
|
||||
fi
|
||||
echo "--- PIC decoder ---"
|
||||
if ! "$testPic" "$dir"; then
|
||||
gameOk=0
|
||||
fi
|
||||
echo "--- VIEW decoder ---"
|
||||
if ! "$testView" "$dir"; then
|
||||
gameOk=0
|
||||
fi
|
||||
echo "--- LOGIC VM (logic.0) ---"
|
||||
if ! "$testVm" "$dir"; then
|
||||
gameOk=0
|
||||
fi
|
||||
echo "--- VM pipeline (callbacks + room transitions) ---"
|
||||
if ! "$testPipeline" "$dir"; then
|
||||
gameOk=0
|
||||
fi
|
||||
|
||||
if [[ $gameOk -eq 1 ]]; then
|
||||
if diff -q "$want" "$got" >/dev/null; then
|
||||
echo " $name: PASS ($(wc -l < "$got") checked)"
|
||||
passed=$((passed + 1))
|
||||
else
|
||||
echo " $name: FAIL -- what differs (< expected, > got); frames in $out/$name/"
|
||||
diff "$want" "$got" | grep '^[<>]' | head -20 | sed 's/^/ /'
|
||||
failed=$((failed + 1))
|
||||
fi
|
||||
echo
|
||||
}
|
||||
|
||||
# selected NAME: was it asked for (everything is when nothing was named)?
|
||||
selected() {
|
||||
[[ $only == " " || $only == *" $1 "* ]]
|
||||
}
|
||||
|
||||
# play NAME GAMEDIR SCRIPT EXPECTFILE: one scripted session.
|
||||
play() {
|
||||
local name=$1 game=$2 script=$3 want=$4 run
|
||||
selected "$name" || return 0
|
||||
run=$(mktemp -d "$build/run.XXXXXX")
|
||||
ln -s "$game" "$run/DATA"
|
||||
# The host port cannot make directories; saving needs SAVES/ there.
|
||||
mkdir "$run/SAVES"
|
||||
rm -rf "${out:?}/$name"
|
||||
mkdir -p "$out/$name"
|
||||
(cd "$run" && timeout -s KILL 600 "$build/agiHost" "$script" "$out/$name") > "$out/$name/log.txt" 2>&1
|
||||
rm -rf "$run"
|
||||
sed -n 's/^\[ *[0-9.]*\] shot \([^ ]*\) \([0-9a-f]*\)$/\1 \2/p' "$out/$name/log.txt" > "$out/$name/hashes.txt"
|
||||
if ! grep -q 'script done' "$out/$name/log.txt"; then
|
||||
echo " $name: FAIL (the run stopped before its script ended; see $out/$name/log.txt)"
|
||||
failed=$((failed + 1))
|
||||
return
|
||||
fi
|
||||
check "$name" "$want" "$out/$name/hashes.txt"
|
||||
}
|
||||
|
||||
# ----- 1. Resources -----
|
||||
|
||||
echo
|
||||
echo "--- resources ---"
|
||||
for dir in "$repo"/games/*/; do
|
||||
[[ -f $dir/LOGDIR ]] || continue
|
||||
game=$(basename "$dir")
|
||||
name=resources-$game
|
||||
selected "$name" || continue
|
||||
run=$(mktemp -d "$build/run.XXXXXX")
|
||||
ln -s "$dir" "$run/DATA"
|
||||
mkdir -p "$out/$name"
|
||||
(cd "$run" && timeout -s KILL 600 "$build/testResources") > "$out/$name/hashes.txt" 2> "$out/$name/log.txt"
|
||||
status=$?
|
||||
rm -rf "$run"
|
||||
if [[ $status != 0 ]]; then
|
||||
echo " $name: FAIL ($(tail -1 "$out/$name/log.txt"))"
|
||||
failed=$((failed + 1))
|
||||
elif [[ -f $expected/$name.txt || $update == 1 ]]; then
|
||||
check "$name" "$expected/$name.txt" "$out/$name/hashes.txt"
|
||||
else
|
||||
echo " $name: PASS ($(tail -1 "$out/$name/log.txt"); no expected picture hashes for this game)"
|
||||
passed=$((passed + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
total=$((passed + failed))
|
||||
echo "===== summary: $passed/$total passed ====="
|
||||
# ----- 2. Probes -----
|
||||
|
||||
echo
|
||||
echo "--- probes ---"
|
||||
python3 "$repo/tests/probes.py" build "$build/probes" || { echo "test-agi: FAIL (probes.py)" >&2; exit 1; }
|
||||
for name in $(python3 "$repo/tests/probes.py" list); do
|
||||
play "$name" "$build/probes/$name" "$build/probes/$name/probe.scr" "$expected/probes/$name.txt"
|
||||
done
|
||||
|
||||
# ----- 3. King's Quest III -----
|
||||
|
||||
echo
|
||||
echo "--- kq3 ---"
|
||||
if [[ -f $repo/games/kq3/LOGDIR ]]; then
|
||||
for scr in "$repo"/tests/kq3/*.scr; do
|
||||
[[ -f $scr ]] || continue
|
||||
session=$(basename "${scr%.scr}")
|
||||
play "kq3-$session" "$repo/games/kq3" "$scr" "$expected/kq3/$session.txt"
|
||||
done
|
||||
else
|
||||
echo " skipped: no games/kq3"
|
||||
skipped=1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "===== test-agi: $passed passed, $failed failed ====="
|
||||
if [[ $failed -gt 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
if [[ $skipped -gt 0 && $passed -eq 0 ]]; then
|
||||
exit 2
|
||||
fi
|
||||
exit 0
|
||||
|
|
|
|||
489
tests/agiGame.py
Normal file
489
tests/agiGame.py
Normal file
|
|
@ -0,0 +1,489 @@
|
|||
#!/usr/bin/env python3
|
||||
# agiGame.py - build small AGI v2 games from scratch for JoeyAGI's tests:
|
||||
# LOGIC code from a Python description, PICTURE command lists, VIEWs from
|
||||
# text art, SOUNDs, WORDS.TOK, OBJECT, the four directories and VOL.0.
|
||||
#
|
||||
# Everything here follows the public AGI specifications (the LOGIC command
|
||||
# table, chapter 5's directory and VOL formats, chapters 6-10). Nothing is
|
||||
# copied from a Sierra game, so the games it writes can live in the repo's
|
||||
# tests and be fed to Sierra's own interpreter as well (scripts/oracle.sh).
|
||||
#
|
||||
# L = Logic()
|
||||
# with L.If(("isset", 5)):
|
||||
# L.load_pic(...) # commands by name, '.' written as '_'
|
||||
# L.return_()
|
||||
# writeGame(dir, logics={0: L.build()}, pics={...}, views={...})
|
||||
|
||||
import os
|
||||
import struct
|
||||
|
||||
# Action commands in number order, with their argument counts.
|
||||
ACTIONS = """return 0
|
||||
increment 1
|
||||
decrement 1
|
||||
assignn 2
|
||||
assignv 2
|
||||
addn 2
|
||||
addv 2
|
||||
subn 2
|
||||
subv 2
|
||||
lindirectv 2
|
||||
rindirect 2
|
||||
lindirectn 2
|
||||
set 1
|
||||
reset 1
|
||||
toggle 1
|
||||
set.v 1
|
||||
reset.v 1
|
||||
toggle.v 1
|
||||
new.room 1
|
||||
new.room.v 1
|
||||
load.logics 1
|
||||
load.logics.v 1
|
||||
call 1
|
||||
call.v 1
|
||||
load.pic 1
|
||||
draw.pic 1
|
||||
show.pic 0
|
||||
discard.pic 1
|
||||
overlay.pic 1
|
||||
show.pri.screen 0
|
||||
load.view 1
|
||||
load.view.v 1
|
||||
discard.view 1
|
||||
animate.obj 1
|
||||
unanimate.all 0
|
||||
draw 1
|
||||
erase 1
|
||||
position 3
|
||||
position.v 3
|
||||
get.posn 3
|
||||
reposition 3
|
||||
set.view 2
|
||||
set.view.v 2
|
||||
set.loop 2
|
||||
set.loop.v 2
|
||||
fix.loop 1
|
||||
release.loop 1
|
||||
set.cel 2
|
||||
set.cel.v 2
|
||||
last.cel 2
|
||||
current.cel 2
|
||||
current.loop 2
|
||||
current.view 2
|
||||
number.of.loops 2
|
||||
set.priority 2
|
||||
set.priority.v 2
|
||||
release.priority 1
|
||||
get.priority 2
|
||||
stop.update 1
|
||||
start.update 1
|
||||
force.update 1
|
||||
ignore.horizon 1
|
||||
observe.horizon 1
|
||||
set.horizon 1
|
||||
object.on.water 1
|
||||
object.on.land 1
|
||||
object.on.anything 1
|
||||
ignore.objs 1
|
||||
observe.objs 1
|
||||
distance 3
|
||||
stop.cycling 1
|
||||
start.cycling 1
|
||||
normal.cycle 1
|
||||
end.of.loop 2
|
||||
reverse.cycle 1
|
||||
reverse.loop 2
|
||||
cycle.time 2
|
||||
stop.motion 1
|
||||
start.motion 1
|
||||
step.size 2
|
||||
step.time 2
|
||||
move.obj 5
|
||||
move.obj.v 5
|
||||
follow.ego 3
|
||||
wander 1
|
||||
normal.motion 1
|
||||
set.dir 2
|
||||
get.dir 2
|
||||
ignore.blocks 1
|
||||
observe.blocks 1
|
||||
block 4
|
||||
unblock 0
|
||||
get 1
|
||||
get.v 1
|
||||
drop 1
|
||||
put 2
|
||||
put.v 2
|
||||
get.room.v 2
|
||||
load.sound 1
|
||||
sound 2
|
||||
stop.sound 0
|
||||
print 1
|
||||
print.v 1
|
||||
display 3
|
||||
display.v 3
|
||||
clear.lines 3
|
||||
text.screen 0
|
||||
graphics 0
|
||||
set.cursor.char 1
|
||||
set.text.attribute 2
|
||||
shake.screen 1
|
||||
configure.screen 3
|
||||
status.line.on 0
|
||||
status.line.off 0
|
||||
set.string 2
|
||||
get.string 5
|
||||
word.to.string 2
|
||||
parse 1
|
||||
get.num 2
|
||||
prevent.input 0
|
||||
accept.input 0
|
||||
set.key 3
|
||||
add.to.pic 7
|
||||
add.to.pic.v 7
|
||||
status 0
|
||||
save.game 0
|
||||
restore.game 0
|
||||
init.disk 0
|
||||
restart.game 0
|
||||
show.obj 1
|
||||
random 3
|
||||
program.control 0
|
||||
player.control 0
|
||||
obj.status.v 1
|
||||
quit 1
|
||||
show.mem 0
|
||||
pause 0
|
||||
echo.line 0
|
||||
cancel.line 0
|
||||
init.joy 0
|
||||
toggle.monitor 0
|
||||
version 0
|
||||
script.size 1
|
||||
set.game.id 1
|
||||
log 1
|
||||
set.scan.start 0
|
||||
reset.scan.start 0
|
||||
reposition.to 3
|
||||
reposition.to.v 3
|
||||
trace.on 0
|
||||
trace.info 3
|
||||
print.at 4
|
||||
print.at.v 4
|
||||
discard.view.v 1
|
||||
clear.text.rect 5
|
||||
set.upper.left 2
|
||||
set.menu 1
|
||||
set.menu.item 2
|
||||
submit.menu 0
|
||||
enable.item 1
|
||||
disable.item 1
|
||||
menu.input 0
|
||||
show.obj.v 1
|
||||
open.dialogue 0
|
||||
close.dialogue 0
|
||||
mul.n 2
|
||||
mul.v 2
|
||||
div.n 2
|
||||
div.v 2
|
||||
close.window 0"""
|
||||
|
||||
# Test commands in number order (said takes a word list).
|
||||
TESTS = """_ 0
|
||||
equaln 2
|
||||
equalv 2
|
||||
lessn 2
|
||||
lessv 2
|
||||
greatern 2
|
||||
greaterv 2
|
||||
isset 1
|
||||
isset.v 1
|
||||
has 1
|
||||
obj.in.room 2
|
||||
posn 5
|
||||
controller 1
|
||||
have.key 0
|
||||
said -1
|
||||
compare.strings 2
|
||||
obj.in.box 5
|
||||
center.posn 5
|
||||
right.posn 5"""
|
||||
|
||||
ACT = {}
|
||||
for _i, _line in enumerate(ACTIONS.splitlines()):
|
||||
_n, _c = _line.split()
|
||||
ACT[_n] = (_i, int(_c))
|
||||
TST = {}
|
||||
for _i, _line in enumerate(TESTS.splitlines()):
|
||||
_n, _c = _line.split()
|
||||
TST[_n] = (_i, int(_c))
|
||||
|
||||
CRYPT_KEY = b"Avis Durgan"
|
||||
COND_START = 0xFF
|
||||
COND_END = 0xFF
|
||||
COND_OR = 0xFC
|
||||
COND_NOT = 0xFD
|
||||
CODE_GOTO = 0xFE
|
||||
VOL_SIGNATURE = b"\x12\x34"
|
||||
DIR_EMPTY = b"\xff\xff\xff"
|
||||
LETTERS = 26
|
||||
WORD_CHAR_XOR = 0x7F
|
||||
WORD_LAST_CHAR = 0x80
|
||||
VOICES = 4
|
||||
SOUND_END = b"\xff\xff"
|
||||
|
||||
# PICTURE commands (specification 7.2).
|
||||
PIC_COLOR = 0xF0
|
||||
PIC_NO_COLOR = 0xF1
|
||||
PIC_PRIORITY = 0xF2
|
||||
PIC_NO_PRIORITY = 0xF3
|
||||
PIC_Y_CORNER = 0xF4
|
||||
PIC_X_CORNER = 0xF5
|
||||
PIC_LINE = 0xF6
|
||||
PIC_REL_LINE = 0xF7
|
||||
PIC_FILL = 0xF8
|
||||
PIC_PEN = 0xF9
|
||||
PIC_PLOT = 0xFA
|
||||
PIC_END = 0xFF
|
||||
|
||||
|
||||
def crypt(data):
|
||||
return bytes(c ^ CRYPT_KEY[i % len(CRYPT_KEY)] for i, c in enumerate(data))
|
||||
|
||||
|
||||
class Logic:
|
||||
def __init__(self):
|
||||
self.code = bytearray()
|
||||
self.msgs = []
|
||||
|
||||
def m(self, text):
|
||||
# The message number for text (from 1), added on first use.
|
||||
if text not in self.msgs:
|
||||
self.msgs.append(text)
|
||||
return self.msgs.index(text) + 1
|
||||
|
||||
def __getattr__(self, name):
|
||||
cmd = name.rstrip("_").replace("_", ".")
|
||||
if cmd not in ACT:
|
||||
raise AttributeError(name)
|
||||
|
||||
def emit(*args):
|
||||
op, n = ACT[cmd]
|
||||
if len(args) != n:
|
||||
raise ValueError("%s takes %d arguments" % (cmd, n))
|
||||
self.code.append(op)
|
||||
for a in args:
|
||||
self.code.append(a & 0xFF)
|
||||
return emit
|
||||
|
||||
def cond(self, tests):
|
||||
# A term is ("name", args...), ("!", "name", args...) or
|
||||
# ("or", [terms]); said takes word numbers.
|
||||
out = bytearray([COND_START])
|
||||
|
||||
def term(t):
|
||||
b = bytearray()
|
||||
if t[0] == "or":
|
||||
b.append(COND_OR)
|
||||
for s in t[1]:
|
||||
b += term(s)
|
||||
b.append(COND_OR)
|
||||
return b
|
||||
if t[0] == "!":
|
||||
b.append(COND_NOT)
|
||||
t = t[1:]
|
||||
num, n = TST[t[0]]
|
||||
b.append(num)
|
||||
if t[0] == "said":
|
||||
b.append(len(t) - 1)
|
||||
for w in t[1:]:
|
||||
b += struct.pack("<H", w)
|
||||
else:
|
||||
if len(t) - 1 != n:
|
||||
raise ValueError("%s takes %d arguments" % (t[0], n))
|
||||
for a in t[1:]:
|
||||
b.append(a & 0xFF)
|
||||
return b
|
||||
for t in tests:
|
||||
out += term(t)
|
||||
out.append(COND_END)
|
||||
return out
|
||||
|
||||
def If(self, *tests):
|
||||
return _IfBlock(self, list(tests))
|
||||
|
||||
def build(self):
|
||||
# The code, then the message section: the count, the end of the
|
||||
# text, an offset per message and the encrypted text.
|
||||
code = bytes(self.code)
|
||||
tableLen = 2 + 2 * len(self.msgs)
|
||||
texts = b""
|
||||
offsets = []
|
||||
for t in self.msgs:
|
||||
offsets.append(tableLen + len(texts))
|
||||
texts += t.encode("ascii") + b"\0"
|
||||
section = bytes([len(self.msgs)]) + struct.pack("<H", tableLen + len(texts))
|
||||
for o in offsets:
|
||||
section += struct.pack("<H", o)
|
||||
return struct.pack("<H", len(code)) + code + section + crypt(texts)
|
||||
|
||||
|
||||
class _IfBlock:
|
||||
def __init__(self, logic, tests):
|
||||
self.logic = logic
|
||||
self.tests = tests
|
||||
|
||||
def __enter__(self):
|
||||
self.logic.code += self.logic.cond(self.tests)
|
||||
self.pos = len(self.logic.code)
|
||||
self.logic.code += b"\0\0"
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc):
|
||||
struct.pack_into("<H", self.logic.code, self.pos, len(self.logic.code) - (self.pos + 2))
|
||||
|
||||
|
||||
def _rle(row, transparent):
|
||||
# Runs of up to 15 pixels; a trailing transparent run is left out.
|
||||
out = bytearray()
|
||||
while row and row[-1] == transparent:
|
||||
row = row[:-1]
|
||||
i = 0
|
||||
while i < len(row):
|
||||
c = row[i]
|
||||
n = 1
|
||||
while i + n < len(row) and row[i + n] == c and n < 15:
|
||||
n += 1
|
||||
out.append((c << 4) | n)
|
||||
i += n
|
||||
out.append(0)
|
||||
return out
|
||||
|
||||
|
||||
def cel(art, transparent=0):
|
||||
# A cel from rows of hex digits; '.' is the transparent colour.
|
||||
rows = [[transparent if ch == "." else int(ch, 16) for ch in line] for line in art]
|
||||
return (rows, transparent)
|
||||
|
||||
|
||||
def view(loops, description=None):
|
||||
# loops: a list whose items are a list of cels, or ("mirror", n) for a
|
||||
# loop that shows loop n flipped. Mirrored cels are padded so the
|
||||
# flipped run data fits where the original was (spec 8.2).
|
||||
mirrors = {}
|
||||
for i, lp in enumerate(loops):
|
||||
if isinstance(lp, tuple):
|
||||
mirrors[lp[1]] = i
|
||||
header = bytearray([1, 1, len(loops), 0, 0]) + bytes(2 * len(loops))
|
||||
body = bytearray()
|
||||
loopAt = {}
|
||||
base = len(header)
|
||||
for i, lp in enumerate(loops):
|
||||
if isinstance(lp, tuple):
|
||||
continue
|
||||
loopAt[i] = base + len(body)
|
||||
start = len(body)
|
||||
body += bytes([len(lp)]) + bytes(2 * len(lp))
|
||||
for c, (rows, transparent) in enumerate(lp):
|
||||
struct.pack_into("<H", body, start + 1 + 2 * c, len(body) - start)
|
||||
flags = transparent
|
||||
if i in mirrors:
|
||||
flags |= 0x80 | (i << 4)
|
||||
data = bytearray()
|
||||
flipped = bytearray()
|
||||
for r in rows:
|
||||
data += _rle(r, transparent)
|
||||
flipped += _rle(r[::-1], transparent)
|
||||
data += bytes(max(0, len(flipped) - len(data)))
|
||||
body += bytes([len(rows[0]), len(rows), flags]) + data
|
||||
for i, lp in enumerate(loops):
|
||||
target = loopAt[lp[1]] if isinstance(lp, tuple) else loopAt[i]
|
||||
struct.pack_into("<H", header, 5 + 2 * i, target)
|
||||
out = header + body
|
||||
if description is not None:
|
||||
struct.pack_into("<H", out, 3, len(out))
|
||||
out += description.encode("ascii") + b"\0"
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def sound(voices):
|
||||
# voices: up to four lists of (duration, divisor, attenuation) notes;
|
||||
# the fourth is the noise channel, whose divisor byte is the noise
|
||||
# control value. Durations are 1/60 s ticks.
|
||||
voices = list(voices) + [[]] * (VOICES - len(voices))
|
||||
header = bytearray(2 * VOICES)
|
||||
body = bytearray()
|
||||
for v, notes in enumerate(voices):
|
||||
struct.pack_into("<H", header, 2 * v, 2 * VOICES + len(body))
|
||||
for duration, divisor, atten in notes:
|
||||
if v == VOICES - 1:
|
||||
freq = bytes([0, divisor & 0xFF])
|
||||
else:
|
||||
freq = bytes([(divisor >> 4) & 0x3F, 0x80 | (v << 5) | (divisor & 0x0F)])
|
||||
body += struct.pack("<H", duration) + freq + bytes([0x90 | (v << 5) | (atten & 0x0F)])
|
||||
body += SOUND_END
|
||||
return bytes(header + body)
|
||||
|
||||
|
||||
def words(table):
|
||||
# WORDS.TOK from {word: number}: the letter index (big-endian), then
|
||||
# the words in order, each sharing a prefix with the one before.
|
||||
out = bytearray(2 * LETTERS)
|
||||
prev = ""
|
||||
for w in sorted(table):
|
||||
letter = ord(w[0]) - ord("a")
|
||||
if struct.unpack_from(">H", out, 2 * letter)[0] == 0:
|
||||
struct.pack_into(">H", out, 2 * letter, len(out))
|
||||
prev = ""
|
||||
share = 0
|
||||
limit = min(len(prev), len(w) - 1)
|
||||
while share < limit and prev[share] == w[share]:
|
||||
share += 1
|
||||
out.append(share)
|
||||
tail = w[share:]
|
||||
for k, ch in enumerate(tail):
|
||||
b = ord(ch) ^ WORD_CHAR_XOR
|
||||
if k == len(tail) - 1:
|
||||
b |= WORD_LAST_CHAR
|
||||
out.append(b)
|
||||
out += struct.pack(">H", table[w])
|
||||
prev = w
|
||||
out.append(0)
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def objects(items, maxObjects=16):
|
||||
# OBJECT from [(name, room)]: the header, an entry per item, the
|
||||
# names; offsets count from the first entry. Encrypted.
|
||||
entries = bytearray()
|
||||
names = bytearray()
|
||||
tableLen = 3 * len(items)
|
||||
for name, room in items:
|
||||
entries += struct.pack("<H", tableLen + len(names)) + bytes([room])
|
||||
names += name.encode("ascii") + b"\0"
|
||||
data = struct.pack("<H", tableLen) + bytes([maxObjects]) + entries + names
|
||||
return crypt(data)
|
||||
|
||||
|
||||
def writeGame(path, logics, pics=None, views=None, sounds=None, vocabulary=None, items=None, maxObjects=16):
|
||||
# The directories and one VOL.0 holding every resource.
|
||||
os.makedirs(path, exist_ok=True)
|
||||
vol = bytearray()
|
||||
for dirName, table in (("LOGDIR", logics), ("PICDIR", pics or {}), ("VIEWDIR", views or {}), ("SNDDIR", sounds or {})):
|
||||
d = bytearray()
|
||||
for num in sorted(table):
|
||||
data = table[num]
|
||||
off = len(vol)
|
||||
vol += VOL_SIGNATURE + b"\x00" + struct.pack("<H", len(data)) + data
|
||||
while len(d) < 3 * (num + 1):
|
||||
d += DIR_EMPTY
|
||||
d[3 * num:3 * num + 3] = bytes([(off >> 16) & 0x0F, (off >> 8) & 0xFF, off & 0xFF])
|
||||
with open(os.path.join(path, dirName), "wb") as f:
|
||||
f.write(d)
|
||||
with open(os.path.join(path, "VOL.0"), "wb") as f:
|
||||
f.write(vol)
|
||||
with open(os.path.join(path, "WORDS.TOK"), "wb") as f:
|
||||
f.write(words(vocabulary or {"a": 0}))
|
||||
with open(os.path.join(path, "OBJECT"), "wb") as f:
|
||||
f.write(objects(items or [("?", 0)], maxObjects))
|
||||
28
tests/agiHost.c
Normal file
28
tests/agiHost.c
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// agiHost: JoeyAGI on the build machine, played by an input script.
|
||||
//
|
||||
// agiHost SCRIPT OUTDIR run from a folder holding DATA/ with the game;
|
||||
// OUTDIR "-" prints the shots' hashes only
|
||||
//
|
||||
// The interpreter's own main is compiled as agiMain (-Dmain=agiMain).
|
||||
|
||||
#include "hostPort.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
|
||||
int agiMain(int argc, char **argv);
|
||||
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
char *agiArgv[1];
|
||||
|
||||
if (argc != 3) {
|
||||
fprintf(stderr, "usage: agiHost SCRIPT OUTDIR (run from a folder holding DATA/)\n");
|
||||
return 2;
|
||||
}
|
||||
if (!hostScriptOpen(argv[1], argv[2])) {
|
||||
return 2;
|
||||
}
|
||||
agiArgv[0] = argv[0];
|
||||
return agiMain(1, agiArgv);
|
||||
}
|
||||
|
|
@ -1,413 +0,0 @@
|
|||
// AGI v2 LOGIC disassembler.
|
||||
//
|
||||
// Walks one LOGIC resource and prints the bytecode in a human-readable
|
||||
// form, with names for action opcodes, test commands, common variables
|
||||
// and flags. Intended as a debugging aid while filling out the VM:
|
||||
// dump KQ3's logic.0 + logic.45 to identify which stub opcodes are
|
||||
// gating title-screen progression.
|
||||
//
|
||||
// Usage:
|
||||
// disasmAgi <game-directory> <logic-id> [end-pc]
|
||||
//
|
||||
// `end-pc` caps the walk at a given offset; useful for spelunking
|
||||
// through huge logic.0 without dumping the whole thing.
|
||||
|
||||
#include "agi.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
|
||||
#define AGI_LOGIC_HDR_BYTES 2u
|
||||
#define ACTION_UNKNOWN 0xFFu
|
||||
#define MAX_TEST 0x12u
|
||||
|
||||
|
||||
// ----- Prototypes -----
|
||||
|
||||
static const char *actionName(uint8_t op);
|
||||
static uint16_t disasmAction(const uint8_t *code, uint16_t pc, uint16_t length, uint8_t op);
|
||||
static uint16_t disasmIf(const uint8_t *code, uint16_t pc, uint16_t length);
|
||||
static uint16_t disasmTest(const uint8_t *code, uint16_t pc, uint16_t length);
|
||||
static const char *testName(uint8_t op);
|
||||
static int walk(const uint8_t *code, uint16_t length, uint16_t endPc);
|
||||
|
||||
|
||||
// AGI v2 action-opcode names. Indexed by opcode id; NULL means "no
|
||||
// such opcode" (matches the VM's ACTION_UNKNOWN sentinel).
|
||||
static const char *kActionNames[256] = {
|
||||
/* 0x00 */ "return", "increment", "decrement", "assignn",
|
||||
/* 0x04 */ "assignv", "addn", "addv", "subn",
|
||||
/* 0x08 */ "subv", "lindirectv", "rindirect", "lindirectn",
|
||||
/* 0x0C */ "set", "reset", "toggle", "setv",
|
||||
/* 0x10 */ "resetv", "togglev", "new.room", "new.room.v",
|
||||
/* 0x14 */ "load.logic", "load.logic.v", "call", "call.v",
|
||||
/* 0x18 */ "load.pic", "draw.pic", "show.pic", "discard.pic",
|
||||
/* 0x1C */ "overlay.pic", "show.pri.screen", "load.view", "load.view.v",
|
||||
/* 0x20 */ "discard.view", "animate.obj", "unanimate.all", "draw",
|
||||
/* 0x24 */ "erase", "position", "position.v", "get.posn",
|
||||
/* 0x28 */ "reposition", "set.view", "set.view.v", "set.loop",
|
||||
/* 0x2C */ "set.loop.v", "fix.loop", "release.loop", "set.cel",
|
||||
/* 0x30 */ "set.cel.v", "last.cel", "current.cel", "current.loop",
|
||||
/* 0x34 */ "current.view", "number.of.loops", "set.priority", "set.priority.v",
|
||||
/* 0x38 */ "release.priority", "get.priority", "stop.update", "start.update",
|
||||
/* 0x3C */ "force.update", "ignore.horizon", "observe.horizon", "set.horizon",
|
||||
/* 0x40 */ "object.on.water", "object.on.land", "object.on.anything", "ignore.objs",
|
||||
/* 0x44 */ "observe.objs", "distance", "stop.cycling", "start.cycling",
|
||||
/* 0x48 */ "normal.cycle", "end.of.loop", "reverse.cycle", "reverse.loop",
|
||||
/* 0x4C */ "cycle.time", "stop.motion", "start.motion", "step.size",
|
||||
/* 0x50 */ "step.time", "move.obj", "move.obj.v", "follow.ego",
|
||||
/* 0x54 */ "wander", "normal.motion", "set.dir", "get.dir",
|
||||
/* 0x58 */ "ignore.blocks", "observe.blocks", "block", "unblock",
|
||||
/* 0x5C */ "get", "get.v", "drop", "put",
|
||||
/* 0x60 */ "put.v", "get.room.v", "load.sound", "sound",
|
||||
/* 0x64 */ "stop.sound", "print", "print.v", "display",
|
||||
/* 0x68 */ "display.v", "clear.lines", "text.screen", "graphics",
|
||||
/* 0x6C */ "set.cursor.char", "set.text.attribute", "shake.screen", "configure.screen",
|
||||
/* 0x70 */ "status.line.on", "status.line.off", "set.string", "get.string",
|
||||
/* 0x74 */ "word.to.string", "parse", "get.num", "prevent.input",
|
||||
/* 0x78 */ "accept.input", "set.key", "add.to.pic", "add.to.pic.v",
|
||||
/* 0x7C */ "status", "save.game", "restore.game", "init.disk",
|
||||
/* 0x80 */ "restart.game", "show.obj", "random", "program.control",
|
||||
/* 0x84 */ "player.control", "obj.status.v", "quit", "show.mem",
|
||||
/* 0x88 */ "pause", "echo.line", "cancel.line", "init.joy",
|
||||
/* 0x8C */ "toggle.monitor", "version", "script.size", "set.game.id",
|
||||
/* 0x90 */ "log", "set.scan.start", "reset.scan.start", "reposition.to",
|
||||
/* 0x94 */ "reposition.to.v", "trace.on", "trace.info", "print.at",
|
||||
/* 0x98 */ "print.at.v", "discard.view.v", "clear.text.rect", "set.upper.left",
|
||||
/* 0x9C */ "set.menu", "set.menu.item", "submit.menu", "enable.item",
|
||||
/* 0xA0 */ "disable.item", "menu.input", "show.obj.v", "open.dialogue",
|
||||
/* 0xA4 */ "close.dialogue", "mul.n", "mul.v", "div.n",
|
||||
/* 0xA8 */ "div.v", "close.window", "set.simple", "push.script",
|
||||
/* 0xAC */ "pop.script", "hold.key", "set.pri.base", "discard.sound",
|
||||
/* 0xB0 */ "hide.mouse", "allow.menu", "show.mouse", "fence.mouse",
|
||||
/* 0xB4 */ "mouse.posn", "release.key", "adj.ego.move.to.x.y",
|
||||
/* 0xB7..0xFD */ NULL
|
||||
// 0xFE = goto, 0xFF = if -- handled separately
|
||||
};
|
||||
|
||||
|
||||
// AGI v2 action arg byte counts (matches kActionArgBytes in agiVm.c).
|
||||
static const uint8_t kActionArgBytes[256] = {
|
||||
/* 0x00 */ 0u, 1u, 1u, 2u, 2u, 2u, 2u, 2u, 2u, 2u, 2u, 2u, 1u, 1u, 1u, 1u,
|
||||
/* 0x10 */ 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 0u, 1u, 1u, 0u, 1u, 1u,
|
||||
/* 0x20 */ 1u, 1u, 0u, 1u, 1u, 3u, 3u, 3u, 3u, 2u, 2u, 2u, 2u, 1u, 1u, 2u,
|
||||
/* 0x30 */ 2u, 2u, 2u, 2u, 2u, 2u, 2u, 2u, 1u, 2u, 1u, 1u, 1u, 1u, 1u, 1u,
|
||||
/* 0x40 */ 1u, 1u, 1u, 1u, 1u, 3u, 1u, 1u, 1u, 2u, 1u, 2u, 2u, 1u, 1u, 2u,
|
||||
/* 0x50 */ 2u, 5u, 5u, 3u, 1u, 1u, 2u, 2u, 1u, 1u, 4u, 0u, 1u, 1u, 1u, 2u,
|
||||
/* 0x60 */ 2u, 2u, 1u, 2u, 0u, 1u, 1u, 3u, 3u, 3u, 0u, 0u, 1u, 2u, 1u, 3u,
|
||||
/* 0x70 */ 0u, 0u, 2u, 5u, 2u, 1u, 2u, 0u, 0u, 3u, 7u, 7u, 0u, 0u, 0u, 0u,
|
||||
/* 0x80 */ 0u, 1u, 3u, 0u, 0u, 1u, 1u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 1u, 1u,
|
||||
/* 0x90 */ 1u, 0u, 0u, 3u, 3u, 0u, 3u, 4u, 4u, 1u, 4u, 2u, 1u, 2u, 0u, 1u,
|
||||
/* 0xA0 */ 1u, 0u, 1u, 0u, 0u, 2u, 2u, 2u, 2u, 0u, 1u, 0u, 0u, 0u, 1u, 1u,
|
||||
/* 0xB0 */ 0u, 1u, 0u, 4u, 2u, 0u, 0u,
|
||||
ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN,
|
||||
/* 0xBA */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN,
|
||||
/* 0xC0 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN,
|
||||
/* 0xC8 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN,
|
||||
/* 0xD0 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN,
|
||||
/* 0xD8 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN,
|
||||
/* 0xE0 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN,
|
||||
/* 0xE8 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN,
|
||||
/* 0xF0 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN,
|
||||
/* 0xF8 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN
|
||||
};
|
||||
|
||||
|
||||
static const char *kTestNames[MAX_TEST + 1u] = {
|
||||
/* 0x00 */ NULL,
|
||||
/* 0x01 */ "equaln",
|
||||
/* 0x02 */ "equalv",
|
||||
/* 0x03 */ "lessn",
|
||||
/* 0x04 */ "lessv",
|
||||
/* 0x05 */ "greatern",
|
||||
/* 0x06 */ "greaterv",
|
||||
/* 0x07 */ "isset",
|
||||
/* 0x08 */ "issetv",
|
||||
/* 0x09 */ "has",
|
||||
/* 0x0A */ "obj.in.room",
|
||||
/* 0x0B */ "posn",
|
||||
/* 0x0C */ "controller",
|
||||
/* 0x0D */ "have.key",
|
||||
/* 0x0E */ "said",
|
||||
/* 0x0F */ "compare.strings",
|
||||
/* 0x10 */ "obj.in.box",
|
||||
/* 0x11 */ "center.posn",
|
||||
/* 0x12 */ "right.posn"
|
||||
};
|
||||
|
||||
|
||||
static const uint8_t kTestArgBytes[MAX_TEST + 1u] = {
|
||||
0u, 2u, 2u, 2u, 2u, 2u, 2u, 1u, 1u, 1u, 2u, 5u, 1u, 0u, 0u /*said*/, 2u, 5u, 5u, 5u
|
||||
};
|
||||
|
||||
|
||||
// ----- Helpers (alphabetical) -----
|
||||
|
||||
static const char *actionName(uint8_t op) {
|
||||
if (op == 0xFFu) {
|
||||
return "if";
|
||||
}
|
||||
if (op == 0xFEu) {
|
||||
return "goto";
|
||||
}
|
||||
if (kActionNames[op] != NULL) {
|
||||
return kActionNames[op];
|
||||
}
|
||||
return "???";
|
||||
}
|
||||
|
||||
|
||||
static uint16_t disasmAction(const uint8_t *code, uint16_t pc, uint16_t length, uint8_t op) {
|
||||
uint8_t args;
|
||||
uint8_t i;
|
||||
uint16_t here;
|
||||
|
||||
here = (uint16_t)(pc - 1u);
|
||||
args = kActionArgBytes[op];
|
||||
if (args == ACTION_UNKNOWN) {
|
||||
printf(" %04X: %02X ???\n", (unsigned)here, (unsigned)op);
|
||||
return length;
|
||||
}
|
||||
if ((uint32_t)pc + (uint32_t)args > (uint32_t)length) {
|
||||
printf(" %04X: %02X truncated args\n", (unsigned)here, (unsigned)op);
|
||||
return length;
|
||||
}
|
||||
printf(" %04X: %s", (unsigned)here, actionName(op));
|
||||
if (args == 0u) {
|
||||
printf("()\n");
|
||||
} else {
|
||||
printf("(");
|
||||
for (i = 0u; i < args; i++) {
|
||||
if (i > 0u) {
|
||||
printf(", ");
|
||||
}
|
||||
printf("%u", (unsigned)code[pc + i]);
|
||||
}
|
||||
printf(")\n");
|
||||
}
|
||||
return (uint16_t)(pc + args);
|
||||
}
|
||||
|
||||
|
||||
static uint16_t disasmIf(const uint8_t *code, uint16_t pc, uint16_t length) {
|
||||
uint16_t here;
|
||||
uint8_t testOp;
|
||||
bool first;
|
||||
bool notNext;
|
||||
bool inOr;
|
||||
uint16_t skip;
|
||||
uint16_t target;
|
||||
|
||||
here = (uint16_t)(pc - 1u);
|
||||
printf(" %04X: if (", (unsigned)here);
|
||||
first = true;
|
||||
notNext = false;
|
||||
inOr = false;
|
||||
for (;;) {
|
||||
if (pc >= length) {
|
||||
printf(" <truncated>)\n");
|
||||
return length;
|
||||
}
|
||||
testOp = code[pc];
|
||||
pc = (uint16_t)(pc + 1u);
|
||||
if (testOp == 0xFFu) {
|
||||
// End of test list.
|
||||
break;
|
||||
}
|
||||
if (testOp == 0xFCu) {
|
||||
if (inOr) {
|
||||
printf(")");
|
||||
inOr = false;
|
||||
} else {
|
||||
if (!first) {
|
||||
printf(" && ");
|
||||
}
|
||||
printf("(");
|
||||
inOr = true;
|
||||
first = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (testOp == 0xFDu) {
|
||||
notNext = true;
|
||||
continue;
|
||||
}
|
||||
if (!first) {
|
||||
printf(inOr ? " || " : " && ");
|
||||
}
|
||||
first = false;
|
||||
if (notNext) {
|
||||
printf("!");
|
||||
notNext = false;
|
||||
}
|
||||
pc = disasmTest(code, pc, length);
|
||||
}
|
||||
if (inOr) {
|
||||
printf(")");
|
||||
}
|
||||
if ((uint32_t)pc + 2u > (uint32_t)length) {
|
||||
printf(") <truncated skip>\n");
|
||||
return length;
|
||||
}
|
||||
skip = (uint16_t)(code[pc] | ((uint16_t)code[pc + 1u] << 8));
|
||||
target = (uint16_t)(pc + 2u + skip);
|
||||
printf(") -> %04X\n", (unsigned)target);
|
||||
return (uint16_t)(pc + 2u);
|
||||
}
|
||||
|
||||
|
||||
static uint16_t disasmTest(const uint8_t *code, uint16_t pc, uint16_t length) {
|
||||
uint8_t args;
|
||||
uint8_t i;
|
||||
uint8_t testOp;
|
||||
uint8_t wordCount;
|
||||
uint16_t word;
|
||||
|
||||
testOp = code[pc - 1u];
|
||||
if (testOp == 0u || testOp > MAX_TEST) {
|
||||
printf("???_test_0x%02X(", (unsigned)testOp);
|
||||
return pc;
|
||||
}
|
||||
if (testOp == 0x0Eu) {
|
||||
// said(word, word, ...)
|
||||
if (pc >= length) {
|
||||
printf("said(<truncated>");
|
||||
return length;
|
||||
}
|
||||
wordCount = code[pc];
|
||||
pc = (uint16_t)(pc + 1u);
|
||||
printf("said(");
|
||||
for (i = 0u; i < wordCount; i++) {
|
||||
if (i > 0u) {
|
||||
printf(", ");
|
||||
}
|
||||
if ((uint32_t)pc + 2u > (uint32_t)length) {
|
||||
printf("<truncated>");
|
||||
return length;
|
||||
}
|
||||
word = (uint16_t)(code[pc] | ((uint16_t)code[pc + 1u] << 8));
|
||||
printf("w%u", (unsigned)word);
|
||||
pc = (uint16_t)(pc + 2u);
|
||||
}
|
||||
printf(")");
|
||||
return pc;
|
||||
}
|
||||
args = kTestArgBytes[testOp];
|
||||
printf("%s(", testName(testOp));
|
||||
if ((uint32_t)pc + (uint32_t)args > (uint32_t)length) {
|
||||
printf("<truncated>)");
|
||||
return length;
|
||||
}
|
||||
for (i = 0u; i < args; i++) {
|
||||
if (i > 0u) {
|
||||
printf(", ");
|
||||
}
|
||||
printf("%u", (unsigned)code[pc + i]);
|
||||
}
|
||||
printf(")");
|
||||
return (uint16_t)(pc + args);
|
||||
}
|
||||
|
||||
|
||||
static const char *testName(uint8_t op) {
|
||||
if (op > MAX_TEST || kTestNames[op] == NULL) {
|
||||
return "???";
|
||||
}
|
||||
return kTestNames[op];
|
||||
}
|
||||
|
||||
|
||||
static int walk(const uint8_t *code, uint16_t length, uint16_t endPc) {
|
||||
uint16_t pc;
|
||||
uint8_t op;
|
||||
int16_t gotoOff;
|
||||
uint16_t target;
|
||||
|
||||
pc = 0u;
|
||||
while (pc < length && pc < endPc) {
|
||||
op = code[pc];
|
||||
pc = (uint16_t)(pc + 1u);
|
||||
if (op == 0xFFu) {
|
||||
pc = disasmIf(code, pc, length);
|
||||
continue;
|
||||
}
|
||||
if (op == 0xFEu) {
|
||||
if ((uint32_t)pc + 2u > (uint32_t)length) {
|
||||
printf(" %04X: goto <truncated>\n", (unsigned)(pc - 1u));
|
||||
return 1;
|
||||
}
|
||||
gotoOff = (int16_t)(code[pc] | ((uint16_t)code[pc + 1u] << 8));
|
||||
target = (uint16_t)((int32_t)pc + 2 + (int32_t)gotoOff);
|
||||
printf(" %04X: goto %04X\n", (unsigned)(pc - 1u), (unsigned)target);
|
||||
pc = (uint16_t)(pc + 2u);
|
||||
continue;
|
||||
}
|
||||
pc = disasmAction(code, pc, length, op);
|
||||
if (op == 0x00u) {
|
||||
// return -- end of logic. Keep walking past it; some
|
||||
// logics have unreachable handlers below the top-level
|
||||
// return that callers jump into via goto.
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
// ----- main -----
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
AgiGameT game;
|
||||
uint16_t logicId;
|
||||
uint8_t *bytes;
|
||||
uint16_t length;
|
||||
uint16_t bytecodeLen;
|
||||
uint16_t endPc;
|
||||
int ret;
|
||||
|
||||
if (argc < 3 || argc > 4) {
|
||||
fprintf(stderr, "usage: %s <game-directory> <logic-id> [end-pc]\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
if (!agiResOpen(&game)) {
|
||||
fprintf(stderr, "FAIL: agiResOpen rejected '%s'\n", argv[1]);
|
||||
return 1;
|
||||
}
|
||||
logicId = (uint16_t)atoi(argv[2]);
|
||||
if (logicId >= game.resCount[AGI_RES_LOGIC]) {
|
||||
fprintf(stderr, "FAIL: no LOGIC at id %u\n", (unsigned)logicId);
|
||||
agiResClose(&game);
|
||||
return 1;
|
||||
}
|
||||
bytes = agiResLoad(&game, AGI_RES_LOGIC, logicId, &length);
|
||||
if (bytes == NULL) {
|
||||
fprintf(stderr, "FAIL: LOGIC %u failed to load\n", (unsigned)logicId);
|
||||
agiResClose(&game);
|
||||
return 1;
|
||||
}
|
||||
if (length < AGI_LOGIC_HDR_BYTES) {
|
||||
fprintf(stderr, "FAIL: LOGIC %u truncated header\n", (unsigned)logicId);
|
||||
free(bytes);
|
||||
agiResClose(&game);
|
||||
return 1;
|
||||
}
|
||||
bytecodeLen = (uint16_t)(bytes[0] | ((uint16_t)bytes[1] << 8));
|
||||
if ((uint32_t)bytecodeLen + AGI_LOGIC_HDR_BYTES > (uint32_t)length) {
|
||||
fprintf(stderr, "FAIL: LOGIC %u bytecode length exceeds resource\n", (unsigned)logicId);
|
||||
free(bytes);
|
||||
agiResClose(&game);
|
||||
return 1;
|
||||
}
|
||||
endPc = (argc == 4) ? (uint16_t)atoi(argv[3]) : bytecodeLen;
|
||||
if (endPc > bytecodeLen) {
|
||||
endPc = bytecodeLen;
|
||||
}
|
||||
|
||||
printf("LOGIC %u: %u bytes resource, %u bytes bytecode\n",
|
||||
(unsigned)logicId, (unsigned)length, (unsigned)bytecodeLen);
|
||||
ret = walk(bytes + AGI_LOGIC_HDR_BYTES, bytecodeLen, endPc);
|
||||
free(bytes);
|
||||
agiResClose(&game);
|
||||
return ret;
|
||||
}
|
||||
8
tests/expected/kq3/opening.txt
Normal file
8
tests/expected/kq3/opening.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
o01 fddacd89
|
||||
o02 b2742c3f
|
||||
o03 74541ae3
|
||||
o04 bf373e80
|
||||
o05 bf373e80
|
||||
o06 625d901d
|
||||
o07 784af2b9
|
||||
o08 6e6b8dd5
|
||||
15
tests/expected/kq3/play.txt
Normal file
15
tests/expected/kq3/play.txt
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
g01 003016a5
|
||||
g02 f5361366
|
||||
g03 9b841c62
|
||||
g04 975e862c
|
||||
g05 af3ac138
|
||||
g06 2d70e908
|
||||
g07 0aa33f32
|
||||
g08 081e7951
|
||||
g09 287e4cb3
|
||||
g10 eb63dfcb
|
||||
g11 8f2418db
|
||||
g12 ac9a41f5
|
||||
g13 53bc8d5c
|
||||
g14 82b40f94
|
||||
g15 fd1788e9
|
||||
11
tests/expected/kq3/saverestore.txt
Normal file
11
tests/expected/kq3/saverestore.txt
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
s01 c7fa568e
|
||||
s02 6275e293
|
||||
s03 37836207
|
||||
s04 490e452f
|
||||
s05 4e59a416
|
||||
s06 155d1e0e
|
||||
s07 180b604f
|
||||
s08 ab92c46b
|
||||
s09 bb34ec9d
|
||||
s10 83ac187a
|
||||
s11 d6a89b3d
|
||||
4
tests/expected/kq3/teleport.txt
Normal file
4
tests/expected/kq3/teleport.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
r20 0d79701a
|
||||
r23 e2e3fed9
|
||||
r26 7c7ae16c
|
||||
r29 616f1479
|
||||
4
tests/expected/probes/clock.txt
Normal file
4
tests/expected/probes/clock.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
k0 dd984418
|
||||
k1 dd984418
|
||||
k2 dd984418
|
||||
k3 656d758f
|
||||
1
tests/expected/probes/collide.txt
Normal file
1
tests/expected/probes/collide.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
a e8f6a1e9
|
||||
1
tests/expected/probes/control.txt
Normal file
1
tests/expected/probes/control.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
a 2cb92e07
|
||||
1
tests/expected/probes/counters.txt
Normal file
1
tests/expected/probes/counters.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
a e17a096c
|
||||
20
tests/expected/probes/cursor.txt
Normal file
20
tests/expected/probes/cursor.txt
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
c1 edbd2e2f
|
||||
c2 6c93f989
|
||||
c3 39d21a45
|
||||
c4 fcbe1f49
|
||||
c5 9a26e107
|
||||
c6 6b40fbeb
|
||||
c7 e9e6dae6
|
||||
c8 6b40fbeb
|
||||
c9 e913a603
|
||||
d1 d5f7bf0e
|
||||
d2 c9292279
|
||||
d3 a943ceb7
|
||||
d4 bdb0338c
|
||||
d5 0c21e71f
|
||||
d6 c9089eb8
|
||||
d7 4efb8b66
|
||||
d8 15f8f7ca
|
||||
d9 47749070
|
||||
e1 575bb647
|
||||
e2 1c028ded
|
||||
1
tests/expected/probes/cycling.txt
Normal file
1
tests/expected/probes/cycling.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
a 092d9b57
|
||||
1
tests/expected/probes/ego.txt
Normal file
1
tests/expected/probes/ego.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
a 98c7cd7c
|
||||
1
tests/expected/probes/egodir.txt
Normal file
1
tests/expected/probes/egodir.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
a 3e55f201
|
||||
1
tests/expected/probes/follow.txt
Normal file
1
tests/expected/probes/follow.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
a 40833758
|
||||
1
tests/expected/probes/font.txt
Normal file
1
tests/expected/probes/font.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
font dd6d62b9
|
||||
1
tests/expected/probes/motion.txt
Normal file
1
tests/expected/probes/motion.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
a c110f209
|
||||
17
tests/expected/probes/parser.txt
Normal file
17
tests/expected/probes/parser.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
p01 6abc8cfa
|
||||
p02 4fd5e76d
|
||||
p03 f5c57fdb
|
||||
p04 ed8aab2b
|
||||
p05 dbe8213f
|
||||
p06 ccf3d84d
|
||||
p07 001f8601
|
||||
p08 b874cfa6
|
||||
p09 23b4f464
|
||||
p10 cd735a97
|
||||
m01 6abc8cfa
|
||||
m02 6abc8cfa
|
||||
m03 6abc8cfa
|
||||
m04 6abc8cfa
|
||||
m05 6abc8cfa
|
||||
m06 6abc8cfa
|
||||
m07 6abc8cfa
|
||||
2
tests/expected/probes/pictures.txt
Normal file
2
tests/expected/probes/pictures.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
visual fa0d1f87
|
||||
priority 6aa74fb7
|
||||
3
tests/expected/probes/priority.txt
Normal file
3
tests/expected/probes/priority.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
a 1ac4084c
|
||||
b 64ac8145
|
||||
p 87906dfd
|
||||
1
tests/expected/probes/redraw.txt
Normal file
1
tests/expected/probes/redraw.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
a f74cc61f
|
||||
1
tests/expected/probes/setview.txt
Normal file
1
tests/expected/probes/setview.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
a 123ccc90
|
||||
1
tests/expected/probes/shake.txt
Normal file
1
tests/expected/probes/shake.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
shake 00d23305
|
||||
1
tests/expected/probes/sound.txt
Normal file
1
tests/expected/probes/sound.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
a 8997da11
|
||||
1
tests/expected/probes/steering.txt
Normal file
1
tests/expected/probes/steering.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
a f4c646c2
|
||||
3
tests/expected/probes/textscreen.txt
Normal file
3
tests/expected/probes/textscreen.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
g1 0222bfaa
|
||||
t1 a3bbcf30
|
||||
g2 0222bfaa
|
||||
1
tests/expected/probes/timing.txt
Normal file
1
tests/expected/probes/timing.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
a 4d9e1ce8
|
||||
12
tests/expected/probes/windows.txt
Normal file
12
tests/expected/probes/windows.txt
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
w01 cf195eef
|
||||
w02 8ed9f248
|
||||
w03 d0bd77b8
|
||||
w04 35a76d64
|
||||
w05 50ffa273
|
||||
w06 0c468875
|
||||
w07 46c4e29b
|
||||
w08 5574f954
|
||||
w09 2e2fac4e
|
||||
w10 a5d70bc1
|
||||
w11 c05a5ca4
|
||||
w12 3bb497ec
|
||||
18
tests/expected/probes/winedge.txt
Normal file
18
tests/expected/probes/winedge.txt
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
e0_open 00b962e1
|
||||
e0_closed 1f699b25
|
||||
e1_open 2c877b3d
|
||||
e1_closed aa90b6d5
|
||||
e2_open 897ee79f
|
||||
e2_closed d5cd02e5
|
||||
e3_open 990e2fcd
|
||||
e3_closed e84d57f5
|
||||
e4_open 8f1e26b8
|
||||
e4_closed f1744bc9
|
||||
e5_open 19be3045
|
||||
e5_closed e84d57f5
|
||||
e6_open e7f7bac9
|
||||
e6_closed 295c31f1
|
||||
e7_open a6227fcd
|
||||
e7_closed f48e2ce5
|
||||
e8_open 1034ed4d
|
||||
e8_closed 102ecf6b
|
||||
473
tests/expected/resources-kq3.txt
Normal file
473
tests/expected/resources-kq3.txt
Normal file
|
|
@ -0,0 +1,473 @@
|
|||
interpreter 2435
|
||||
logic 0 code 6778 messages 144
|
||||
view 0 loops 4 cels 28 pixels 0f681f64
|
||||
logic 1 code 895 messages 27
|
||||
picture 1 visual 329e9052 priority 7c218c8f
|
||||
view 1 loops 4 cels 16 pixels cd1bd77d
|
||||
sound 1 notes 51 9 48 5
|
||||
logic 2 code 2488 messages 42
|
||||
picture 2 visual 4ad3a933 priority 1a05f607
|
||||
view 2 loops 4 cels 20 pixels bd49a3c6
|
||||
sound 2 notes 59 74 1 1
|
||||
logic 3 code 443 messages 8
|
||||
picture 3 visual d42e7f3a priority b004a114
|
||||
view 3 loops 4 cels 28 pixels bb849240
|
||||
sound 3 notes 8 1 1 2
|
||||
logic 4 code 3782 messages 38
|
||||
picture 4 visual 929dd5c7 priority 317d182b
|
||||
view 4 loops 1 cels 10 pixels 46049c68
|
||||
sound 4 notes 10 1 1 4
|
||||
logic 5 code 2336 messages 36
|
||||
picture 5 visual 72cb08ea priority 9498253b
|
||||
view 5 loops 1 cels 1 pixels 38dada04
|
||||
sound 5 notes 14 14 1 1
|
||||
logic 6 code 2122 messages 63
|
||||
picture 6 visual 05336461 priority 75113374
|
||||
view 6 loops 1 cels 4 pixels 52fdd842
|
||||
sound 6 notes 4 6 7 1
|
||||
logic 7 code 915 messages 11
|
||||
picture 7 visual 9146dd38 priority c9fa84a5
|
||||
view 7 loops 4 cels 28 pixels 0dee3f03
|
||||
sound 7 notes 10 1 1 4
|
||||
logic 8 code 3164 messages 36
|
||||
picture 8 visual 7f3449b2 priority 3ea16cf2
|
||||
view 8 loops 2 cels 18 pixels af423bc5
|
||||
sound 8 notes 17 19 1 1
|
||||
logic 9 code 432 messages 8
|
||||
picture 9 visual 36203a15 priority 97584b96
|
||||
view 9 loops 1 cels 7 pixels 93bb2aa1
|
||||
sound 9 notes 1 1 1 21
|
||||
logic 10 code 3837 messages 72
|
||||
picture 10 visual f7541608 priority e56e5c16
|
||||
view 10 loops 4 cels 24 pixels 71961597
|
||||
sound 10 notes 10 1 1 4
|
||||
logic 11 code 318 messages 11
|
||||
picture 11 visual 39b4474d priority 955293d1
|
||||
view 11 loops 1 cels 8 pixels 3707aacf
|
||||
sound 11 notes 4 1 1 4
|
||||
logic 12 code 185 messages 0
|
||||
picture 12 visual d3cff1df priority 331ca9c5
|
||||
view 12 loops 2 cels 14 pixels b60f62dd
|
||||
sound 12 notes 13 13 1 1
|
||||
logic 13 code 710 messages 5
|
||||
picture 13 visual ac5ca216 priority 795b5a59
|
||||
view 13 loops 1 cels 9 pixels 81b43ddc
|
||||
sound 13 notes 12 1 1 1
|
||||
logic 14 code 1334 messages 16
|
||||
picture 14 visual e41998f2 priority 3d6efd35
|
||||
view 14 loops 1 cels 10 pixels 2568ef4a
|
||||
sound 14 notes 6 1 1 6
|
||||
logic 15 code 413 messages 0
|
||||
picture 15 visual e063ac9d priority dc9b5348
|
||||
view 15 loops 1 cels 3 pixels 98d9e7c4
|
||||
sound 15 notes 6 1 1 4
|
||||
logic 16 code 139 messages 0
|
||||
picture 16 visual 9199ea13 priority 2b073fe7
|
||||
view 16 loops 2 cels 9 pixels 3a288ff4
|
||||
logic 17 code 252 messages 0
|
||||
picture 17 visual 247be7cb priority 05368fd5
|
||||
view 17 loops 2 cels 10 pixels 1436da12
|
||||
logic 18 code 552 messages 5
|
||||
picture 18 visual 66ffd417 priority e1470d06
|
||||
view 18 loops 1 cels 6 pixels a0d463e2
|
||||
logic 19 code 320 messages 3
|
||||
picture 19 visual 1fd4b8d2 priority c6f4d7ee
|
||||
view 19 loops 1 cels 8 pixels c0be60de
|
||||
logic 20 code 695 messages 3
|
||||
picture 20 visual 28f3414c priority 1d9f0394
|
||||
view 20 loops 4 cels 28 pixels 65ec367c
|
||||
logic 21 code 350 messages 4
|
||||
picture 21 visual 13716309 priority a639bc3a
|
||||
view 21 loops 3 cels 10 pixels e4d55e4f
|
||||
sound 21 notes 62 30 30 0
|
||||
logic 22 code 1485 messages 19
|
||||
picture 22 visual a688ed69 priority 371ba442
|
||||
sound 22 notes 32 36 30 0
|
||||
logic 23 code 312 messages 5
|
||||
picture 23 visual 841169dd priority a434a281
|
||||
view 23 loops 1 cels 10 pixels f112f931
|
||||
sound 23 notes 67 45 95 0
|
||||
logic 24 code 1312 messages 18
|
||||
picture 24 visual 805ce07b priority 42437aa7
|
||||
view 24 loops 1 cels 1 pixels 4550a884
|
||||
sound 24 notes 121 142 115 0
|
||||
logic 25 code 966 messages 6
|
||||
picture 25 visual da6aa39c priority fd9bd8a4
|
||||
view 25 loops 1 cels 10 pixels 9dba8865
|
||||
sound 25 notes 18 15 11 0
|
||||
logic 26 code 145 messages 0
|
||||
picture 26 visual d4600f5b priority 116d8fee
|
||||
view 26 loops 2 cels 14 pixels 8056da53
|
||||
sound 26 notes 10 9 10 0
|
||||
logic 27 code 216 messages 0
|
||||
picture 27 visual 24bba7b1 priority 3114b1cf
|
||||
view 27 loops 4 cels 8 pixels 1695f82d
|
||||
sound 27 notes 10 8 8 0
|
||||
logic 28 code 2406 messages 40
|
||||
picture 28 visual 212f56b9 priority 442dc510
|
||||
view 28 loops 4 cels 28 pixels 02fe61cf
|
||||
sound 28 notes 55 68 50 0
|
||||
logic 29 code 858 messages 17
|
||||
picture 29 visual 10be702f priority 6fb1a208
|
||||
view 29 loops 4 cels 25 pixels 373f70db
|
||||
sound 29 notes 54 55 28 0
|
||||
logic 30 code 602 messages 4
|
||||
picture 30 visual f22f602a priority a82217f4
|
||||
view 30 loops 2 cels 16 pixels 6b7de209
|
||||
sound 30 notes 102 42 54 0
|
||||
logic 31 code 1239 messages 22
|
||||
picture 31 visual 74d532c5 priority 34fcede5
|
||||
view 31 loops 2 cels 8 pixels d6a5bd5d
|
||||
sound 31 notes 41 0 0 0
|
||||
logic 32 code 697 messages 12
|
||||
picture 32 visual c8383685 priority 2e20d5c5
|
||||
view 32 loops 2 cels 8 pixels fa83658d
|
||||
sound 32 notes 12 12 12 0
|
||||
logic 33 code 682 messages 6
|
||||
picture 33 visual 7fd6392f priority a70ba4b7
|
||||
view 33 loops 2 cels 14 pixels 095ffbf5
|
||||
sound 33 notes 66 44 42 0
|
||||
logic 34 code 2197 messages 43
|
||||
picture 34 visual ebe094d2 priority 2b57ea18
|
||||
view 34 loops 1 cels 9 pixels 48e09f12
|
||||
sound 34 notes 69 32 32 0
|
||||
logic 35 code 94 messages 6
|
||||
picture 35 visual 486e3be6 priority 8649ad2f
|
||||
view 35 loops 1 cels 4 pixels 39d3b961
|
||||
sound 35 notes 15 26 26 0
|
||||
logic 36 code 739 messages 18
|
||||
picture 36 visual fc3945a3 priority 071f283d
|
||||
view 36 loops 4 cels 12 pixels 9e3f127e
|
||||
sound 36 notes 151 153 149 0
|
||||
logic 37 code 661 messages 13
|
||||
picture 37 visual 3c19d37c priority a763d473
|
||||
view 37 loops 4 cels 20 pixels 09eb3089
|
||||
sound 37 notes 25 4 4 0
|
||||
logic 38 code 1099 messages 24
|
||||
picture 38 visual 813dd175 priority 1618c2ed
|
||||
view 38 loops 4 cels 17 pixels 7c0352fb
|
||||
sound 38 notes 17 0 0 0
|
||||
logic 39 code 1366 messages 51
|
||||
picture 39 visual 33dd4b05 priority faa88819
|
||||
view 39 loops 3 cels 15 pixels 6088ef6b
|
||||
sound 39 notes 99 57 58 0
|
||||
logic 40 code 1826 messages 54
|
||||
picture 40 visual 29219083 priority 0612fd59
|
||||
view 40 loops 4 cels 24 pixels 93437817
|
||||
logic 41 code 1310 messages 36
|
||||
picture 41 visual ff908fac priority 1dd84d29
|
||||
view 41 loops 2 cels 12 pixels cc45b4b1
|
||||
logic 42 code 1208 messages 29
|
||||
picture 42 visual d4cfe5ba priority 955c5346
|
||||
view 42 loops 2 cels 16 pixels de8554ef
|
||||
logic 43 code 602 messages 18
|
||||
picture 43 visual d73a6f45 priority 2e20d5c5
|
||||
view 43 loops 2 cels 18 pixels 7fa345dd
|
||||
view 44 loops 2 cels 19 pixels a228f386
|
||||
logic 45 code 1759 messages 42
|
||||
picture 45 visual 0ff62c5f priority 2ef2c44e
|
||||
view 45 loops 4 cels 39 pixels 063e3f54
|
||||
logic 46 code 526 messages 5
|
||||
picture 46 visual 0e924d07 priority 9589e1ce
|
||||
view 46 loops 3 cels 14 pixels 872cfd84
|
||||
picture 47 visual 6b7a8375 priority 2e20d5c5
|
||||
view 47 loops 2 cels 12 pixels bd1835db
|
||||
logic 48 code 601 messages 11
|
||||
picture 48 visual a4a35a34 priority e9a54109
|
||||
view 48 loops 3 cels 14 pixels 0cf25c53
|
||||
logic 49 code 802 messages 16
|
||||
picture 49 visual 95112fa7 priority 6002128f
|
||||
view 49 loops 1 cels 4 pixels aec33377
|
||||
logic 50 code 425 messages 7
|
||||
picture 50 visual f2adf287 priority d1bc6197
|
||||
logic 51 code 547 messages 7
|
||||
picture 51 visual 24222e55 priority 5f4eddc6
|
||||
view 51 loops 4 cels 24 pixels bc097266
|
||||
logic 52 code 432 messages 7
|
||||
picture 52 visual cdad2aa1 priority a5b7e996
|
||||
view 52 loops 4 cels 36 pixels 432ab78b
|
||||
logic 53 code 671 messages 12
|
||||
picture 53 visual efa174b8 priority af27e66d
|
||||
logic 54 code 636 messages 11
|
||||
picture 54 visual e803b77d priority bf6eb7f6
|
||||
view 54 loops 1 cels 12 pixels 85a73127
|
||||
logic 55 code 536 messages 13
|
||||
picture 55 visual 232fde88 priority 24331781
|
||||
logic 56 code 571 messages 17
|
||||
picture 56 visual 5d93290d priority f293db2f
|
||||
view 56 loops 4 cels 17 pixels 55998cc7
|
||||
logic 57 code 999 messages 25
|
||||
picture 57 visual 6035cafa priority 34391afb
|
||||
view 57 loops 1 cels 8 pixels 24571dcf
|
||||
logic 58 code 349 messages 7
|
||||
picture 58 visual 93f9bfb2 priority 6aec7c73
|
||||
view 58 loops 2 cels 8 pixels 2ea41665
|
||||
logic 59 code 319 messages 7
|
||||
picture 59 visual 6b9d445d priority 8a30ead0
|
||||
view 59 loops 2 cels 10 pixels 8c3dc255
|
||||
logic 60 code 417 messages 17
|
||||
picture 60 visual beb70e28 priority 0adf81fe
|
||||
view 60 loops 3 cels 3 pixels 69567fb6
|
||||
logic 61 code 582 messages 12
|
||||
picture 61 visual af233d3e priority c7c19e7f
|
||||
view 61 loops 3 cels 3 pixels d16e78ab
|
||||
logic 62 code 277 messages 5
|
||||
picture 62 visual be854141 priority e99a9740
|
||||
view 62 loops 4 cels 10 pixels 751e9a0e
|
||||
logic 63 code 254 messages 5
|
||||
picture 63 visual 22995dfc priority f5f2f366
|
||||
view 63 loops 1 cels 1 pixels 47387319
|
||||
logic 64 code 312 messages 5
|
||||
picture 64 visual 9947b0f5 priority 7457e6cb
|
||||
view 64 loops 1 cels 1 pixels de156123
|
||||
logic 65 code 247 messages 7
|
||||
picture 65 visual 298f1769 priority 388124ef
|
||||
view 65 loops 2 cels 10 pixels a5ee8429
|
||||
logic 66 code 1390 messages 24
|
||||
picture 66 visual 49ec136f priority 283923d7
|
||||
view 66 loops 1 cels 4 pixels 061e86e7
|
||||
logic 67 code 327 messages 12
|
||||
picture 67 visual 27cc7783 priority 3979bf85
|
||||
view 67 loops 1 cels 7 pixels 5b3201f5
|
||||
logic 68 code 437 messages 12
|
||||
picture 68 visual 5ada2325 priority d53d5088
|
||||
view 68 loops 1 cels 2 pixels af4f991d
|
||||
logic 69 code 623 messages 25
|
||||
picture 69 visual 26b880f3 priority cd27f03a
|
||||
view 69 loops 4 cels 18 pixels bc5a6a50
|
||||
logic 70 code 226 messages 2
|
||||
picture 70 visual 9a51c920 priority bc2c5485
|
||||
view 70 loops 4 cels 15 pixels ee55e881
|
||||
logic 71 code 556 messages 18
|
||||
picture 71 visual 4b18a836 priority e5f0b48b
|
||||
view 71 loops 2 cels 16 pixels 858cb785
|
||||
logic 72 code 306 messages 18
|
||||
picture 72 visual 43a78d32 priority 5e55fbf2
|
||||
view 72 loops 2 cels 8 pixels 51b5e48d
|
||||
logic 73 code 188 messages 3
|
||||
picture 73 visual 68f1a471 priority 19f9fb51
|
||||
view 73 loops 4 cels 16 pixels 345bfc51
|
||||
logic 74 code 915 messages 16
|
||||
picture 74 visual da80a511 priority 55b69b73
|
||||
view 74 loops 1 cels 8 pixels 204192c8
|
||||
logic 75 code 1465 messages 27
|
||||
picture 75 visual 32e469aa priority a5031158
|
||||
view 75 loops 1 cels 4 pixels db72bcb4
|
||||
logic 76 code 683 messages 6
|
||||
picture 76 visual b77a72a8 priority 67e4e005
|
||||
view 76 loops 4 cels 20 pixels ac6a428f
|
||||
logic 77 code 154 messages 2
|
||||
view 77 loops 2 cels 4 pixels ab947e32
|
||||
logic 78 code 853 messages 15
|
||||
picture 78 visual 9e85475d priority 351609c1
|
||||
view 78 loops 4 cels 19 pixels 8a37240a
|
||||
logic 79 code 443 messages 12
|
||||
picture 79 visual ddbf5114 priority b398034b
|
||||
view 79 loops 1 cels 4 pixels 47e418f2
|
||||
logic 80 code 992 messages 13
|
||||
picture 80 visual 500553dc priority 7bcd1510
|
||||
view 80 loops 4 cels 20 pixels 0493437d
|
||||
logic 81 code 908 messages 13
|
||||
picture 81 visual 63ce560e priority 8bcb477a
|
||||
logic 82 code 707 messages 10
|
||||
picture 82 visual 08b24a4f priority 661a2af3
|
||||
view 82 loops 2 cels 12 pixels e9e9d68d
|
||||
logic 83 code 1683 messages 27
|
||||
picture 83 visual 12345f5e priority 953b713c
|
||||
view 83 loops 2 cels 6 pixels 66e3fb3f
|
||||
logic 84 code 778 messages 18
|
||||
picture 84 visual 30c39096 priority f1f01db0
|
||||
view 84 loops 2 cels 16 pixels 6da07225
|
||||
logic 85 code 561 messages 11
|
||||
picture 85 visual efd9fe0a priority a0f1d19a
|
||||
view 85 loops 1 cels 3 pixels c38ddf34
|
||||
logic 86 code 478 messages 26
|
||||
picture 86 visual c2b99d27 priority bb68a175
|
||||
view 86 loops 1 cels 4 pixels 90657743
|
||||
logic 87 code 192 messages 2
|
||||
picture 87 visual 9aaa9785 priority 54680636
|
||||
picture 88 visual 558d9555 priority 28e20f45
|
||||
view 88 loops 1 cels 4 pixels d12f33bb
|
||||
logic 89 code 1834 messages 8
|
||||
view 90 loops 1 cels 4 pixels b22ec0c1
|
||||
logic 92 code 137 messages 29
|
||||
view 92 loops 1 cels 4 pixels 45b62afb
|
||||
logic 93 code 40 messages 1
|
||||
view 93 loops 1 cels 5 pixels d56c88b1
|
||||
logic 94 code 157 messages 1
|
||||
view 94 loops 2 cels 10 pixels 1f444d33
|
||||
logic 95 code 67 messages 1
|
||||
view 95 loops 1 cels 4 pixels 5c540485
|
||||
logic 96 code 1 messages 178
|
||||
view 96 loops 2 cels 11 pixels e5826d31
|
||||
logic 97 code 82 messages 1
|
||||
view 97 loops 1 cels 3 pixels 24b8bf18
|
||||
logic 98 code 5 messages 32
|
||||
view 98 loops 1 cels 4 pixels e876b3e5
|
||||
logic 99 code 748 messages 33
|
||||
logic 100 code 187 messages 0
|
||||
logic 101 code 272 messages 3
|
||||
view 101 loops 1 cels 1 pixels 72f6fdfe
|
||||
logic 102 code 81 messages 15
|
||||
view 102 loops 1 cels 1 pixels 4e2c487e
|
||||
logic 103 code 417 messages 4
|
||||
view 103 loops 1 cels 1 pixels 7aeff556
|
||||
logic 104 code 810 messages 27
|
||||
view 104 loops 1 cels 1 pixels 2373d0ab
|
||||
logic 105 code 922 messages 25
|
||||
view 105 loops 1 cels 1 pixels 42d6dee1
|
||||
logic 106 code 1107 messages 29
|
||||
view 106 loops 1 cels 1 pixels 5b4ecc1a
|
||||
logic 107 code 277 messages 0
|
||||
view 107 loops 1 cels 1 pixels 187679e9
|
||||
logic 108 code 533 messages 9
|
||||
view 108 loops 1 cels 1 pixels 080c61d8
|
||||
logic 109 code 1004 messages 19
|
||||
view 109 loops 1 cels 1 pixels 456223a4
|
||||
logic 110 code 299 messages 12
|
||||
view 110 loops 1 cels 1 pixels d8c808d2
|
||||
logic 111 code 1000 messages 44
|
||||
view 111 loops 1 cels 1 pixels da310b48
|
||||
logic 112 code 285 messages 2
|
||||
view 112 loops 1 cels 1 pixels a6a4324f
|
||||
logic 113 code 106 messages 1
|
||||
view 113 loops 1 cels 1 pixels 9601e9be
|
||||
logic 114 code 443 messages 8
|
||||
view 114 loops 1 cels 1 pixels e217a2d9
|
||||
logic 115 code 844 messages 10
|
||||
view 115 loops 1 cels 1 pixels fad25d04
|
||||
logic 116 code 42 messages 3
|
||||
view 116 loops 1 cels 1 pixels ff79e3d1
|
||||
logic 117 code 549 messages 12
|
||||
view 117 loops 1 cels 1 pixels adf8caa1
|
||||
logic 118 code 674 messages 17
|
||||
view 118 loops 1 cels 1 pixels be7457f7
|
||||
logic 119 code 395 messages 21
|
||||
view 119 loops 1 cels 2 pixels 8a67c378
|
||||
logic 120 code 318 messages 12
|
||||
view 120 loops 1 cels 1 pixels 299ea209
|
||||
logic 121 code 1164 messages 29
|
||||
view 121 loops 1 cels 1 pixels 828fc6ec
|
||||
logic 122 code 429 messages 10
|
||||
picture 122 visual 89455a65 priority a4351d16
|
||||
view 122 loops 1 cels 1 pixels 60dc5f7f
|
||||
logic 123 code 602 messages 17
|
||||
view 123 loops 1 cels 1 pixels ce05e564
|
||||
logic 124 code 946 messages 26
|
||||
view 124 loops 1 cels 1 pixels 775be74b
|
||||
logic 125 code 633 messages 15
|
||||
picture 125 visual 3530172d priority 662cd747
|
||||
view 125 loops 1 cels 1 pixels 775be74b
|
||||
logic 126 code 1030 messages 16
|
||||
view 126 loops 1 cels 1 pixels b500bf35
|
||||
logic 127 code 856 messages 18
|
||||
view 127 loops 1 cels 1 pixels a546227c
|
||||
logic 128 code 657 messages 24
|
||||
view 128 loops 1 cels 1 pixels 3375b3cf
|
||||
logic 129 code 131 messages 11
|
||||
view 129 loops 1 cels 1 pixels 514249b1
|
||||
view 130 loops 1 cels 2 pixels ee35a0fa
|
||||
picture 131 visual 3b883fc9 priority 090b0331
|
||||
view 131 loops 1 cels 1 pixels ed9a66a8
|
||||
picture 132 visual 4e9bfd15 priority 2e20d5c5
|
||||
view 132 loops 1 cels 2 pixels 3ef657d2
|
||||
picture 133 visual 79cd2ea2 priority 090b0331
|
||||
view 133 loops 1 cels 1 pixels 8de948a4
|
||||
picture 134 visual bc63ce47 priority 090b0331
|
||||
view 134 loops 1 cels 2 pixels 231a12b4
|
||||
view 136 loops 1 cels 2 pixels 1f21ce54
|
||||
view 137 loops 1 cels 2 pixels 83b667f0
|
||||
view 138 loops 1 cels 2 pixels 24d17d54
|
||||
view 139 loops 1 cels 1 pixels a6a4324f
|
||||
view 140 loops 1 cels 1 pixels c10b9d0a
|
||||
view 141 loops 1 cels 1 pixels cae29458
|
||||
view 142 loops 1 cels 1 pixels 911979d4
|
||||
picture 143 visual 24a5937f priority 2e20d5c5
|
||||
view 143 loops 1 cels 1 pixels 105686f7
|
||||
view 147 loops 1 cels 2 pixels 5997882e
|
||||
picture 148 visual 62b0b8dc priority 102c7c76
|
||||
view 148 loops 1 cels 2 pixels 09a4e7e6
|
||||
view 149 loops 1 cels 1 pixels 4c905dd3
|
||||
view 150 loops 1 cels 2 pixels fa954041
|
||||
view 151 loops 1 cels 2 pixels 56c14092
|
||||
view 152 loops 1 cels 2 pixels 247b403b
|
||||
view 153 loops 1 cels 2 pixels 9629a73a
|
||||
view 155 loops 1 cels 7 pixels cb9826f5
|
||||
view 156 loops 2 cels 12 pixels c3cc9229
|
||||
view 157 loops 2 cels 12 pixels d4e2a085
|
||||
view 158 loops 2 cels 16 pixels 2cbe9fa5
|
||||
view 159 loops 2 cels 10 pixels d3b4fc85
|
||||
view 160 loops 1 cels 4 pixels ae76ea18
|
||||
view 161 loops 2 cels 16 pixels 2b2bd9a7
|
||||
view 162 loops 1 cels 9 pixels 0340ba7f
|
||||
view 163 loops 2 cels 12 pixels 68edcd2d
|
||||
view 164 loops 3 cels 11 pixels f7006733
|
||||
view 165 loops 1 cels 6 pixels 0ad94d18
|
||||
picture 166 visual 586fa489 priority 5a8a037b
|
||||
view 166 loops 3 cels 14 pixels f2b024e3
|
||||
view 167 loops 1 cels 2 pixels 327251f7
|
||||
view 168 loops 1 cels 1 pixels 5c464f93
|
||||
view 169 loops 1 cels 1 pixels e5ca925f
|
||||
view 170 loops 3 cels 4 pixels 6e64c617
|
||||
view 171 loops 4 cels 14 pixels 024958f9
|
||||
view 172 loops 2 cels 6 pixels 986ecd7f
|
||||
view 173 loops 2 cels 14 pixels 3b6d535b
|
||||
view 174 loops 1 cels 1 pixels 060bc2ef
|
||||
picture 175 visual 247f168a priority ed392918
|
||||
view 175 loops 1 cels 4 pixels 8b3c2393
|
||||
picture 176 visual b5b9f822 priority ea6867a1
|
||||
view 176 loops 1 cels 1 pixels d1532cee
|
||||
view 177 loops 1 cels 1 pixels 4a955462
|
||||
view 178 loops 1 cels 4 pixels cd8f29cd
|
||||
view 179 loops 1 cels 4 pixels 80913381
|
||||
view 180 loops 1 cels 4 pixels 41c7d7e4
|
||||
view 182 loops 1 cels 4 pixels 7d2651eb
|
||||
view 185 loops 1 cels 5 pixels 57b0732b
|
||||
view 186 loops 2 cels 14 pixels 466c2ca7
|
||||
view 187 loops 3 cels 21 pixels d309c2aa
|
||||
view 190 loops 4 cels 8 pixels 6e8fe275
|
||||
view 191 loops 1 cels 2 pixels 72cbb584
|
||||
view 192 loops 1 cels 10 pixels 3e2b5ee1
|
||||
view 193 loops 2 cels 10 pixels 63ed6b4f
|
||||
view 194 loops 1 cels 7 pixels 508c952c
|
||||
view 195 loops 1 cels 2 pixels 82f7e047
|
||||
view 196 loops 1 cels 10 pixels c4086043
|
||||
view 197 loops 1 cels 10 pixels 2e12ab7e
|
||||
view 199 loops 1 cels 9 pixels fa90f4d0
|
||||
view 200 loops 1 cels 1 pixels 11295cc5
|
||||
view 201 loops 1 cels 1 pixels 4bcd4dd2
|
||||
view 203 loops 4 cels 16 pixels 8ef700c1
|
||||
view 204 loops 2 cels 16 pixels 013880d9
|
||||
view 205 loops 2 cels 8 pixels 5405b91d
|
||||
view 206 loops 2 cels 16 pixels 7e017ab9
|
||||
view 207 loops 1 cels 1 pixels 22940efd
|
||||
view 208 loops 1 cels 8 pixels 9dc7946a
|
||||
view 209 loops 2 cels 19 pixels 279d312c
|
||||
view 210 loops 4 cels 24 pixels f4b5514d
|
||||
view 211 loops 4 cels 24 pixels 3766f515
|
||||
view 214 loops 1 cels 2 pixels 20be3280
|
||||
view 216 loops 1 cels 1 pixels fc50c3e3
|
||||
view 217 loops 1 cels 5 pixels 9d7e80de
|
||||
view 220 loops 2 cels 2 pixels 6fd93d2b
|
||||
view 221 loops 3 cels 10 pixels 76ccbee8
|
||||
view 222 loops 1 cels 3 pixels e5276203
|
||||
view 223 loops 1 cels 3 pixels 676f5aee
|
||||
view 225 loops 4 cels 19 pixels 9de7ebda
|
||||
view 226 loops 1 cels 3 pixels ecd03070
|
||||
view 227 loops 1 cels 3 pixels 2ef18662
|
||||
view 229 loops 1 cels 5 pixels 65abca39
|
||||
view 230 loops 1 cels 3 pixels e0d19e69
|
||||
view 231 loops 1 cels 6 pixels 343af315
|
||||
view 232 loops 4 cels 17 pixels 9d7e8581
|
||||
view 236 loops 2 cels 4 pixels f7081d15
|
||||
view 240 loops 4 cels 16 pixels 2e91cc99
|
||||
view 241 loops 1 cels 9 pixels 3b3fa27e
|
||||
view 242 loops 1 cels 1 pixels 591d9bf1
|
||||
view 243 loops 1 cels 5 pixels 78eb7fc9
|
||||
view 244 loops 1 cels 3 pixels 53485ecd
|
||||
view 245 loops 1 cels 3 pixels 47368ac5
|
||||
view 246 loops 1 cels 1 pixels 38e43d15
|
||||
view 248 loops 2 cels 11 pixels ae2412bc
|
||||
view 249 loops 2 cels 10 pixels bc310549
|
||||
view 250 loops 2 cels 12 pixels 7949f929
|
||||
view 251 loops 1 cels 10 pixels 7f960f3b
|
||||
462
tests/hostPort.c
Normal file
462
tests/hostPort.c
Normal file
|
|
@ -0,0 +1,462 @@
|
|||
// Headless host port of JoeyLib for running the whole interpreter on the
|
||||
// build machine: JoeyLib's BLANK platform with these services filled in.
|
||||
//
|
||||
// Time is virtual. Every jlWaitVBL is one 1/60 s frame, so a run is exactly
|
||||
// reproducible: the same script gives the same frames, independent of how
|
||||
// fast the host is. The display is the stage itself, resolved through each
|
||||
// row's palette at every present.
|
||||
//
|
||||
// The input script is the one scripts/dosDrive.py feeds DOSBox, so the same
|
||||
// file drives Sierra's interpreter, JoeyAGI in DOSBox and this harness:
|
||||
// wait SECONDS let SECONDS of virtual time pass
|
||||
// key NAME [HOLD] press key NAME (xdotool names) for HOLD seconds
|
||||
// down NAME / up NAME hold / release a key
|
||||
// type TEXT press each character's key in turn, typing it
|
||||
// shot NAME write the displayed frame to OUTDIR/NAME.ppm
|
||||
// clear press Return until no message window is open
|
||||
// record NAME note every frame from here ...
|
||||
// stop ... to here: OUTDIR/NAME.frames lists each
|
||||
// frame's hash and OUTDIR/NAME-HASH.ppm holds each
|
||||
// different frame once
|
||||
// '#' starts a comment. When the script ends the run stops.
|
||||
//
|
||||
// Every shot prints its frame's FNV-1a hash (the gate compares those) and,
|
||||
// unless the output folder is "-", writes a PPM; a recording prints one
|
||||
// hash for its whole sequence of frames, as a shot named after it.
|
||||
|
||||
#include "hostPort.h"
|
||||
|
||||
#include "port.h"
|
||||
#include "surfaceInternal.h"
|
||||
#include "inputInternal.h"
|
||||
#include "agi.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
|
||||
#define HOST_FRAME_HZ 60u
|
||||
#define HOST_LINE_MAX 512
|
||||
#define HOST_PATH_MAX 1024
|
||||
// A file path: the output folder, a name from the script and a suffix.
|
||||
#define HOST_FILE_MAX (HOST_PATH_MAX + HOST_LINE_MAX + 64)
|
||||
#define HOST_KEY_HOLD_FRAMES 7u // 0.12 s, dosDrive.py's default press
|
||||
#define HOST_KEY_GAP_FRAMES 5u // 0.08 s, dosDrive.py's gap after a press
|
||||
#define HOST_CHANNEL_MAX 255u
|
||||
#define HOST_NIBBLE_SCALE 17u // $0RGB nibble -> 8-bit channel
|
||||
#define HOST_PIXELS_PER_BYTE 2u
|
||||
#define HOST_CLEAR_GAP_FRAMES 36u // 0.6 s, dosDrive.py's pause between Returns
|
||||
#define HOST_CLEAR_TRIES 8u // dosDrive.py's limit on Returns per clear
|
||||
#define HOST_NO_FILES "-"
|
||||
#define FNV_OFFSET_BASIS 2166136261u
|
||||
#define FNV_PRIME 16777619u
|
||||
#define HASH_BYTES 4u
|
||||
#define BYTE_BITS 8u
|
||||
#define BYTE_MASK 0xFFu
|
||||
#define RECORD_DISTINCT_MAX 64u
|
||||
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
jlKeyE key;
|
||||
uint8_t ch;
|
||||
} HostKeyNameT;
|
||||
|
||||
|
||||
// ----- Prototypes -----
|
||||
|
||||
static void hostDump(const char *name);
|
||||
static uint32_t hostFrameHash(void);
|
||||
static bool hostKeyLookup(const char *name, jlKeyE *outKey, uint8_t *outCh);
|
||||
static void hostPress(jlKeyE key, uint8_t ch, uint32_t holdFrames);
|
||||
static void hostRecordFrame(void);
|
||||
static void hostStep(void);
|
||||
static void hostWritePpm(const char *path);
|
||||
|
||||
|
||||
// ----- Module state -----
|
||||
|
||||
static const HostKeyNameT kKeyNames[] = {
|
||||
{ "Return", KEY_RETURN, JL_CHAR_RETURN },
|
||||
{ "Escape", KEY_ESCAPE, JL_CHAR_ESCAPE },
|
||||
{ "Tab", KEY_TAB, JL_CHAR_TAB },
|
||||
{ "BackSpace", KEY_BACKSPACE, JL_CHAR_BACKSPACE },
|
||||
{ "space", KEY_SPACE, ' ' },
|
||||
{ "Up", KEY_UP, 0u },
|
||||
{ "Down", KEY_DOWN, 0u },
|
||||
{ "Left", KEY_LEFT, 0u },
|
||||
{ "Right", KEY_RIGHT, 0u },
|
||||
{ "F1", KEY_F1, 0u },
|
||||
{ "F2", KEY_F2, 0u },
|
||||
{ "F3", KEY_F3, 0u },
|
||||
{ "F4", KEY_F4, 0u },
|
||||
{ "F5", KEY_F5, 0u },
|
||||
{ "F6", KEY_F6, 0u },
|
||||
{ "F7", KEY_F7, 0u },
|
||||
{ "F8", KEY_F8, 0u },
|
||||
{ "F9", KEY_F9, 0u },
|
||||
{ "F10", KEY_F10, 0u },
|
||||
{ "ctrl", KEY_LCTRL, 0u },
|
||||
{ "alt", KEY_LALT, 0u },
|
||||
{ "shift", KEY_LSHIFT, 0u },
|
||||
{ "period", KEY_NONE, '.' },
|
||||
{ "comma", KEY_NONE, ',' },
|
||||
{ "apostrophe", KEY_NONE, '\'' },
|
||||
{ "minus", KEY_NONE, '-' },
|
||||
{ "question", KEY_NONE, '?' },
|
||||
{ "exclam", KEY_NONE, '!' }
|
||||
};
|
||||
|
||||
static FILE *gScript;
|
||||
static char gOutDir[HOST_PATH_MAX];
|
||||
static uint32_t gFrame;
|
||||
static uint32_t gResumeFrame;
|
||||
static uint32_t gHoldUntil[KEY_COUNT];
|
||||
static bool gHeld[KEY_COUNT];
|
||||
static uint8_t gDisplay[SURFACE_HEIGHT][SURFACE_WIDTH][3];
|
||||
// Characters still to type for the current 'type' line, one per press.
|
||||
static char gTypeBuf[HOST_LINE_MAX];
|
||||
static long gLinePos;
|
||||
static uint16_t gTypePos;
|
||||
static uint8_t gClearTries;
|
||||
// A recording: its name, its frame list, the hash of its frame sequence
|
||||
// and the frames written so far.
|
||||
static char gRecordName[HOST_LINE_MAX];
|
||||
static FILE *gRecordLog;
|
||||
static bool gRecording;
|
||||
static uint32_t gRecordHash;
|
||||
static uint32_t gRecordFrames;
|
||||
static uint32_t gRecordSeen[RECORD_DISTINCT_MAX];
|
||||
static uint8_t gRecordSeenCount;
|
||||
|
||||
|
||||
// ----- Internal helpers (alphabetical) -----
|
||||
|
||||
static void hostDump(const char *name) {
|
||||
char path[HOST_FILE_MAX];
|
||||
|
||||
printf("[%6.1f] shot %s %08x\n", (double)gFrame / (double)HOST_FRAME_HZ, name, (unsigned)hostFrameHash());
|
||||
if (strcmp(gOutDir, HOST_NO_FILES) == 0) {
|
||||
return;
|
||||
}
|
||||
snprintf(path, sizeof(path), "%s/%s.ppm", gOutDir, name);
|
||||
hostWritePpm(path);
|
||||
}
|
||||
|
||||
|
||||
static uint32_t hostFrameHash(void) {
|
||||
const uint8_t *p;
|
||||
uint32_t hash;
|
||||
size_t i;
|
||||
|
||||
hash = FNV_OFFSET_BASIS;
|
||||
p = &gDisplay[0][0][0];
|
||||
for (i = 0; i < sizeof(gDisplay); i++) {
|
||||
hash = (hash ^ p[i]) * FNV_PRIME;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
|
||||
// Map an xdotool key name (or a single character) to a JoeyLib key and
|
||||
// the character it types.
|
||||
static bool hostKeyLookup(const char *name, jlKeyE *outKey, uint8_t *outCh) {
|
||||
size_t i;
|
||||
char c;
|
||||
|
||||
for (i = 0; i < sizeof(kKeyNames) / sizeof(kKeyNames[0]); i++) {
|
||||
if (strcmp(kKeyNames[i].name, name) == 0) {
|
||||
*outKey = kKeyNames[i].key;
|
||||
*outCh = kKeyNames[i].ch;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (name[0] == '\0' || name[1] != '\0') {
|
||||
return false;
|
||||
}
|
||||
c = name[0];
|
||||
if (c >= 'a' && c <= 'z') {
|
||||
*outKey = (jlKeyE)(KEY_A + (c - 'a'));
|
||||
*outCh = (uint8_t)c;
|
||||
return true;
|
||||
}
|
||||
if (c >= 'A' && c <= 'Z') {
|
||||
*outKey = (jlKeyE)(KEY_A + (c - 'A'));
|
||||
*outCh = (uint8_t)c;
|
||||
return true;
|
||||
}
|
||||
if (c >= '0' && c <= '9') {
|
||||
*outKey = (jlKeyE)(KEY_0 + (c - '0'));
|
||||
*outCh = (uint8_t)c;
|
||||
return true;
|
||||
}
|
||||
*outKey = KEY_NONE;
|
||||
*outCh = (uint8_t)c;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static void hostPress(jlKeyE key, uint8_t ch, uint32_t holdFrames) {
|
||||
if (key != KEY_NONE) {
|
||||
gHoldUntil[key] = gFrame + holdFrames;
|
||||
}
|
||||
// Like the real keyboards, a Ctrl or Alt chord types no character.
|
||||
if (ch != 0u && !gHeld[KEY_LALT] && !gHeld[KEY_LCTRL]) {
|
||||
jlInputCharPush(ch);
|
||||
}
|
||||
gResumeFrame = gFrame + holdFrames + HOST_KEY_GAP_FRAMES;
|
||||
}
|
||||
|
||||
|
||||
// One frame of a recording: into the sequence hash and the frame list,
|
||||
// and written out the first time it is seen.
|
||||
static void hostRecordFrame(void) {
|
||||
char path[HOST_FILE_MAX];
|
||||
uint32_t hash;
|
||||
uint8_t i;
|
||||
|
||||
hash = hostFrameHash();
|
||||
for (i = 0u; i < HASH_BYTES; i++) {
|
||||
gRecordHash = (gRecordHash ^ ((hash >> (i * BYTE_BITS)) & BYTE_MASK)) * FNV_PRIME;
|
||||
}
|
||||
gRecordFrames++;
|
||||
if (gRecordLog == NULL) {
|
||||
return;
|
||||
}
|
||||
fprintf(gRecordLog, "%u %08x\n", (unsigned)gRecordFrames, (unsigned)hash);
|
||||
for (i = 0u; i < gRecordSeenCount; i++) {
|
||||
if (gRecordSeen[i] == hash) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (gRecordSeenCount >= RECORD_DISTINCT_MAX) {
|
||||
return;
|
||||
}
|
||||
gRecordSeen[gRecordSeenCount++] = hash;
|
||||
snprintf(path, sizeof(path), "%s/%s-%08x.ppm", gOutDir, gRecordName, (unsigned)hash);
|
||||
hostWritePpm(path);
|
||||
}
|
||||
|
||||
|
||||
// Run script lines until one asks for time to pass.
|
||||
static void hostStep(void) {
|
||||
char line[HOST_LINE_MAX];
|
||||
char *op;
|
||||
char *arg;
|
||||
jlKeyE key;
|
||||
uint8_t ch;
|
||||
double secs;
|
||||
|
||||
while (gScript != NULL && gFrame >= gResumeFrame) {
|
||||
if (gTypeBuf[gTypePos] != '\0') {
|
||||
char one[2];
|
||||
|
||||
one[0] = gTypeBuf[gTypePos];
|
||||
one[1] = '\0';
|
||||
gTypePos++;
|
||||
if (one[0] == ' ') {
|
||||
hostPress(KEY_SPACE, ' ', HOST_KEY_HOLD_FRAMES);
|
||||
} else if (hostKeyLookup(one, &key, &ch)) {
|
||||
hostPress(key, ch, HOST_KEY_HOLD_FRAMES);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
gLinePos = ftell(gScript);
|
||||
if (fgets(line, sizeof(line), gScript) == NULL) {
|
||||
printf("[%6.1f] script done\n", (double)gFrame / (double)HOST_FRAME_HZ);
|
||||
exit(0);
|
||||
}
|
||||
line[strcspn(line, "\r\n")] = '\0';
|
||||
op = line;
|
||||
while (*op == ' ' || *op == '\t') {
|
||||
op++;
|
||||
}
|
||||
if (strncmp(op, "type ", 5) == 0) {
|
||||
strncpy(gTypeBuf, op + 5, sizeof(gTypeBuf) - 1u);
|
||||
gTypeBuf[sizeof(gTypeBuf) - 1u] = '\0';
|
||||
gTypePos = 0u;
|
||||
continue;
|
||||
}
|
||||
op[strcspn(op, "#")] = '\0';
|
||||
arg = op + strcspn(op, " \t");
|
||||
if (*arg != '\0') {
|
||||
*arg = '\0';
|
||||
arg++;
|
||||
while (*arg == ' ' || *arg == '\t') {
|
||||
arg++;
|
||||
}
|
||||
}
|
||||
if (*op == '\0') {
|
||||
continue;
|
||||
}
|
||||
if (strcmp(op, "wait") == 0) {
|
||||
secs = atof(arg);
|
||||
gResumeFrame = gFrame + (uint32_t)(secs * HOST_FRAME_HZ + 0.5);
|
||||
} else if (strcmp(op, "key") == 0) {
|
||||
char *holdArg;
|
||||
double hold;
|
||||
|
||||
holdArg = arg + strcspn(arg, " \t");
|
||||
hold = 0.0;
|
||||
if (*holdArg != '\0') {
|
||||
*holdArg = '\0';
|
||||
hold = atof(holdArg + 1);
|
||||
}
|
||||
if (!hostKeyLookup(arg, &key, &ch)) {
|
||||
fprintf(stderr, "agiHost: unknown key '%s'\n", arg);
|
||||
exit(2);
|
||||
}
|
||||
hostPress(key, ch, (hold > 0.0) ? (uint32_t)(hold * HOST_FRAME_HZ + 0.5) : HOST_KEY_HOLD_FRAMES);
|
||||
} else if (strcmp(op, "down") == 0 || strcmp(op, "up") == 0) {
|
||||
if (!hostKeyLookup(arg, &key, &ch) || key == KEY_NONE) {
|
||||
fprintf(stderr, "agiHost: unknown key '%s'\n", arg);
|
||||
exit(2);
|
||||
}
|
||||
gHeld[key] = (op[0] == 'd');
|
||||
if (gHeld[key] && ch != 0u) {
|
||||
jlInputCharPush(ch);
|
||||
}
|
||||
} else if (strcmp(op, "shot") == 0) {
|
||||
hostDump(arg);
|
||||
} else if (strcmp(op, "record") == 0) {
|
||||
char path[HOST_FILE_MAX];
|
||||
|
||||
strncpy(gRecordName, arg, sizeof(gRecordName) - 1u);
|
||||
gRecording = true;
|
||||
gRecordHash = FNV_OFFSET_BASIS;
|
||||
gRecordFrames = 0u;
|
||||
gRecordSeenCount = 0u;
|
||||
if (strcmp(gOutDir, HOST_NO_FILES) != 0) {
|
||||
snprintf(path, sizeof(path), "%s/%s.frames", gOutDir, gRecordName);
|
||||
gRecordLog = fopen(path, "w");
|
||||
}
|
||||
} else if (strcmp(op, "stop") == 0) {
|
||||
gRecording = false;
|
||||
if (gRecordLog != NULL) {
|
||||
fclose(gRecordLog);
|
||||
gRecordLog = NULL;
|
||||
}
|
||||
printf("[%6.1f] shot %s %08x\n", (double)gFrame / (double)HOST_FRAME_HZ, gRecordName, (unsigned)gRecordHash);
|
||||
} else if (strcmp(op, "clear") == 0) {
|
||||
// Dismiss message windows: Return while one is open, a few
|
||||
// times at most (a window that wants another key stays).
|
||||
if (gAgi != NULL && gAgi->windowOpen && gClearTries < HOST_CLEAR_TRIES) {
|
||||
gClearTries++;
|
||||
hostPress(KEY_RETURN, JL_CHAR_RETURN, HOST_KEY_HOLD_FRAMES);
|
||||
gResumeFrame = gFrame + HOST_CLEAR_GAP_FRAMES;
|
||||
fseek(gScript, gLinePos, SEEK_SET);
|
||||
} else {
|
||||
gClearTries = 0u;
|
||||
}
|
||||
} else {
|
||||
fprintf(stderr, "agiHost: bad script op '%s'\n", op);
|
||||
exit(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void hostWritePpm(const char *path) {
|
||||
FILE *fp;
|
||||
|
||||
fp = fopen(path, "wb");
|
||||
if (fp == NULL) {
|
||||
fprintf(stderr, "agiHost: cannot write %s\n", path);
|
||||
return;
|
||||
}
|
||||
fprintf(fp, "P6\n%d %d\n%u\n", SURFACE_WIDTH, SURFACE_HEIGHT, HOST_CHANNEL_MAX);
|
||||
fwrite(gDisplay, 1, sizeof(gDisplay), fp);
|
||||
fclose(fp);
|
||||
}
|
||||
|
||||
|
||||
// ----- Port services -----
|
||||
|
||||
uint16_t jlpFrameCount(void) {
|
||||
return (uint16_t)gFrame;
|
||||
}
|
||||
|
||||
|
||||
uint16_t jlpFrameHz(void) {
|
||||
return HOST_FRAME_HZ;
|
||||
}
|
||||
|
||||
|
||||
bool jlpInit(const jlConfigT *config) {
|
||||
(void)config;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void jlpInputInit(void) {
|
||||
}
|
||||
|
||||
|
||||
void jlpInputPoll(void) {
|
||||
uint16_t k;
|
||||
|
||||
for (k = 0u; k < (uint16_t)KEY_COUNT; k++) {
|
||||
gKeyState[k] = (uint8_t)((gHeld[k] || gFrame < gHoldUntil[k]) ? 1u : 0u);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void jlpInputShutdown(void) {
|
||||
}
|
||||
|
||||
|
||||
void jlpJoystickReset(jlJoystickE js) {
|
||||
(void)js;
|
||||
}
|
||||
|
||||
|
||||
void jlpPresent(const jlSurfaceT *src) {
|
||||
uint16_t x;
|
||||
uint16_t y;
|
||||
const uint16_t *pal;
|
||||
uint8_t byte;
|
||||
uint8_t nib;
|
||||
uint16_t rgb;
|
||||
|
||||
for (y = 0u; y < SURFACE_HEIGHT; y++) {
|
||||
pal = src->palette[src->scb[y]];
|
||||
for (x = 0u; x < SURFACE_WIDTH; x++) {
|
||||
byte = src->pixels[(uint32_t)y * SURFACE_BYTES_PER_ROW + x / HOST_PIXELS_PER_BYTE];
|
||||
nib = (uint8_t)((x & 1u) ? (byte & 0x0Fu) : (byte >> 4));
|
||||
rgb = pal[nib];
|
||||
gDisplay[y][x][0] = (uint8_t)(((rgb >> 8) & 0x0Fu) * HOST_NIBBLE_SCALE);
|
||||
gDisplay[y][x][1] = (uint8_t)(((rgb >> 4) & 0x0Fu) * HOST_NIBBLE_SCALE);
|
||||
gDisplay[y][x][2] = (uint8_t)((rgb & 0x0Fu) * HOST_NIBBLE_SCALE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void jlpShutdown(void) {
|
||||
}
|
||||
|
||||
|
||||
void jlpWaitVBL(void) {
|
||||
gFrame++;
|
||||
if (gRecording) {
|
||||
hostRecordFrame();
|
||||
}
|
||||
hostStep();
|
||||
}
|
||||
|
||||
|
||||
// ----- Harness -----
|
||||
|
||||
// Read the input script from scriptPath; shots go to outDir ("-" for none).
|
||||
bool hostScriptOpen(const char *scriptPath, const char *outDir) {
|
||||
gScript = fopen(scriptPath, "r");
|
||||
if (gScript == NULL) {
|
||||
fprintf(stderr, "agiHost: cannot read %s\n", scriptPath);
|
||||
return false;
|
||||
}
|
||||
strncpy(gOutDir, outDir, sizeof(gOutDir) - 1u);
|
||||
// Line by line, so a run killed by a timeout still shows how far it got.
|
||||
setvbuf(stdout, NULL, _IOLBF, 0);
|
||||
return true;
|
||||
}
|
||||
11
tests/hostPort.h
Normal file
11
tests/hostPort.h
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// The headless host port's harness (tests/hostPort.c): the input script
|
||||
// that drives a run and where its shots go.
|
||||
|
||||
#ifndef HOST_PORT_H
|
||||
#define HOST_PORT_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
bool hostScriptOpen(const char *scriptPath, const char *outDir);
|
||||
|
||||
#endif
|
||||
20
tests/kq3/opening.scr
Normal file
20
tests/kq3/opening.scr
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# The opening, untouched: the title, the credits over Manannan's face, the
|
||||
# house and the story windows. Shots are sparse: a DOSBox screenshot
|
||||
# (Ctrl+F5) reaches Sierra's interpreter as a key press and can hurry the
|
||||
# opening along when scripts/oracle.sh runs it.
|
||||
wait 3
|
||||
shot o01
|
||||
wait 7
|
||||
shot o02
|
||||
wait 7
|
||||
shot o03
|
||||
wait 13
|
||||
shot o04
|
||||
wait 15
|
||||
shot o05
|
||||
wait 15
|
||||
shot o06
|
||||
wait 15
|
||||
shot o07
|
||||
wait 15
|
||||
shot o08
|
||||
57
tests/kq3/play.scr
Normal file
57
tests/kq3/play.scr
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# The first minutes of play: the opening skipped with Enter, Gwydion
|
||||
# walked about his room, things looked at and typed (an unknown word too),
|
||||
# the inventory, and the menus opened and walked.
|
||||
wait 4
|
||||
key Return
|
||||
wait 4
|
||||
shot g01
|
||||
key Left
|
||||
wait 2
|
||||
shot g02
|
||||
key Left
|
||||
wait 1
|
||||
shot g03
|
||||
key Down
|
||||
wait 2
|
||||
shot g04
|
||||
key Down
|
||||
wait 1
|
||||
type look
|
||||
wait 0.5
|
||||
shot g05
|
||||
key Return
|
||||
wait 2
|
||||
shot g06
|
||||
key Return
|
||||
wait 1
|
||||
shot g07
|
||||
type look at bed
|
||||
key Return
|
||||
wait 2
|
||||
shot g08
|
||||
key Return
|
||||
wait 1
|
||||
type xyzzy
|
||||
key Return
|
||||
wait 2
|
||||
shot g09
|
||||
key Return
|
||||
wait 1
|
||||
key Tab
|
||||
wait 2
|
||||
shot g10
|
||||
key Return
|
||||
wait 2
|
||||
shot g11
|
||||
key Escape
|
||||
wait 2
|
||||
shot g12
|
||||
key Right
|
||||
wait 1
|
||||
shot g13
|
||||
key Down
|
||||
wait 1
|
||||
shot g14
|
||||
key Escape
|
||||
wait 2
|
||||
shot g15
|
||||
58
tests/kq3/saverestore.scr
Normal file
58
tests/kq3/saverestore.scr
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# Save (F5) to slot 3, walk away, then restore (F7) slot 3: the room,
|
||||
# Gwydion's position and the status line come back, and the menus still
|
||||
# work. Then restart (F9): the game starts again in the hall, menus and
|
||||
# all (KQ3 builds its menus once and not on a restart).
|
||||
wait 4
|
||||
key Return
|
||||
wait 6
|
||||
clear
|
||||
wait 6
|
||||
clear
|
||||
key Left
|
||||
wait 2
|
||||
key Left
|
||||
wait 0.5
|
||||
shot s01
|
||||
key F5
|
||||
wait 1
|
||||
shot s02
|
||||
key 3
|
||||
wait 1
|
||||
shot s03
|
||||
type before the walk
|
||||
key Return
|
||||
wait 1
|
||||
clear
|
||||
wait 1
|
||||
shot s04
|
||||
key Down
|
||||
wait 3
|
||||
key Down
|
||||
wait 0.5
|
||||
shot s05
|
||||
key F7
|
||||
wait 1
|
||||
shot s06
|
||||
key 3
|
||||
wait 2
|
||||
clear
|
||||
wait 1
|
||||
shot s07
|
||||
key Escape
|
||||
wait 1
|
||||
shot s08
|
||||
key Escape
|
||||
wait 1
|
||||
key F9
|
||||
wait 1
|
||||
shot s09
|
||||
key Return
|
||||
wait 6
|
||||
clear
|
||||
wait 2
|
||||
shot s10
|
||||
key Escape
|
||||
wait 1
|
||||
shot s11
|
||||
key Escape
|
||||
wait 1
|
||||
52
tests/kq3/teleport.scr
Normal file
52
tests/kq3/teleport.scr
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# KQ3's own debug mode (Alt+D) and its "tp" command, from the hall to the
|
||||
# beach and on, until Manannan comes home, finds Gwydion outside and takes
|
||||
# him back (tp does nothing in some rooms, on Sierra's interpreter too).
|
||||
wait 4
|
||||
key Return
|
||||
wait 6
|
||||
clear
|
||||
wait 6
|
||||
clear
|
||||
wait 6
|
||||
clear
|
||||
down alt
|
||||
key d
|
||||
up alt
|
||||
wait 1
|
||||
clear
|
||||
type tp
|
||||
key Return
|
||||
wait 0.5
|
||||
type 20
|
||||
key Return
|
||||
wait 3
|
||||
shot r20
|
||||
clear
|
||||
wait 1
|
||||
type tp
|
||||
key Return
|
||||
wait 0.5
|
||||
type 23
|
||||
key Return
|
||||
wait 3
|
||||
shot r23
|
||||
clear
|
||||
wait 1
|
||||
type tp
|
||||
key Return
|
||||
wait 0.5
|
||||
type 26
|
||||
key Return
|
||||
wait 3
|
||||
shot r26
|
||||
clear
|
||||
wait 1
|
||||
type tp
|
||||
key Return
|
||||
wait 0.5
|
||||
type 29
|
||||
key Return
|
||||
wait 3
|
||||
shot r29
|
||||
clear
|
||||
wait 1
|
||||
1257
tests/probes.py
Executable file
1257
tests/probes.py
Executable file
File diff suppressed because it is too large
Load diff
|
|
@ -1,191 +0,0 @@
|
|||
// Host-side test for agiPic.c.
|
||||
//
|
||||
// Loads a single PIC resource from a game directory, decodes it, and
|
||||
// writes two image files: a color PPM for the visual plane (palette
|
||||
// expanded to 8-bit RGB) and a grayscale PGM for the priority plane.
|
||||
// Also reports the byte count and pixel coverage so a regression in
|
||||
// the decoder shows up as a numeric diff.
|
||||
//
|
||||
// Usage:
|
||||
// testAgiPic <game-directory> [pic-id]
|
||||
//
|
||||
// The default pic-id is the first non-empty entry in PICDIR; KQ3 has
|
||||
// id 0 unused so the loader hops forward until it finds one.
|
||||
//
|
||||
// Exit codes:
|
||||
// 0 decoded; outputs written
|
||||
// 1 game directory missing, PIC slot empty, or decode failed
|
||||
// 2 invalid command line
|
||||
|
||||
#include "agi.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
|
||||
// 4-bit channel -> 8-bit channel scaling: x * 17 maps 0..15 to 0..255
|
||||
// without bias (15 * 17 = 255 exactly).
|
||||
#define CHANNEL_SCALE 17
|
||||
|
||||
|
||||
// ----- Prototypes -----
|
||||
|
||||
static int16_t firstNonEmptyPic(const AgiGameT *game);
|
||||
static bool writePgm(const char *path, const uint8_t *plane);
|
||||
static bool writePpm(const char *path, const uint8_t *plane);
|
||||
|
||||
|
||||
// ----- Helpers (alphabetical) -----
|
||||
|
||||
static int16_t firstNonEmptyPic(const AgiGameT *game) {
|
||||
uint16_t count;
|
||||
uint16_t i;
|
||||
|
||||
count = game->resCount[AGI_RES_PIC];
|
||||
for (i = 0u; i < count; i++) {
|
||||
if (game->resDir[AGI_RES_PIC][i].volume < AGI_MAX_VOLUMES) {
|
||||
return (int16_t)i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
static bool writePgm(const char *path, const uint8_t *plane) {
|
||||
FILE *fp;
|
||||
uint32_t i;
|
||||
uint8_t gray;
|
||||
|
||||
fp = fopen(path, "wb");
|
||||
if (fp == NULL) {
|
||||
return false;
|
||||
}
|
||||
fprintf(fp, "P5\n%d %d\n255\n", AGI_PIC_WIDTH, AGI_PIC_HEIGHT);
|
||||
for (i = 0u; i < (uint32_t)AGI_PIC_PIXELS; i++) {
|
||||
gray = (uint8_t)((plane[i] & 0x0Fu) * CHANNEL_SCALE);
|
||||
fputc(gray, fp);
|
||||
}
|
||||
fclose(fp);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static bool writePpm(const char *path, const uint8_t *plane) {
|
||||
FILE *fp;
|
||||
uint32_t i;
|
||||
uint16_t rgb;
|
||||
uint8_t r;
|
||||
uint8_t g;
|
||||
uint8_t b;
|
||||
|
||||
fp = fopen(path, "wb");
|
||||
if (fp == NULL) {
|
||||
return false;
|
||||
}
|
||||
fprintf(fp, "P6\n%d %d\n255\n", AGI_PIC_WIDTH, AGI_PIC_HEIGHT);
|
||||
for (i = 0u; i < (uint32_t)AGI_PIC_PIXELS; i++) {
|
||||
rgb = kAgiPalette[plane[i] & 0x0Fu];
|
||||
r = (uint8_t)(((rgb >> 8) & 0x0Fu) * CHANNEL_SCALE);
|
||||
g = (uint8_t)(((rgb >> 4) & 0x0Fu) * CHANNEL_SCALE);
|
||||
b = (uint8_t)((rgb & 0x0Fu) * CHANNEL_SCALE);
|
||||
fputc(r, fp);
|
||||
fputc(g, fp);
|
||||
fputc(b, fp);
|
||||
}
|
||||
fclose(fp);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// ----- main -----
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
AgiGameT game;
|
||||
AgiPicT pic;
|
||||
int16_t picId;
|
||||
uint8_t *bytes;
|
||||
uint16_t length;
|
||||
uint32_t nonBgVisual;
|
||||
uint32_t nonBgPriority;
|
||||
uint32_t i;
|
||||
char visPath[256];
|
||||
char priPath[256];
|
||||
|
||||
if (argc < 2 || argc > 3) {
|
||||
fprintf(stderr, "usage: %s <game-directory> [pic-id]\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (!agiResOpen(&game)) {
|
||||
fprintf(stderr, "FAIL: agiResOpen rejected '%s'\n", argv[1]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (argc == 3) {
|
||||
picId = (int16_t)atoi(argv[2]);
|
||||
} else {
|
||||
picId = firstNonEmptyPic(&game);
|
||||
}
|
||||
if (picId < 0 || (uint16_t)picId >= game.resCount[AGI_RES_PIC]) {
|
||||
fprintf(stderr, "FAIL: no PIC at id %d\n", (int)picId);
|
||||
agiResClose(&game);
|
||||
return 1;
|
||||
}
|
||||
|
||||
bytes = agiResLoad(&game, AGI_RES_PIC, (uint16_t)picId, &length);
|
||||
if (bytes == NULL) {
|
||||
fprintf(stderr, "FAIL: PIC %d failed to load\n", (int)picId);
|
||||
agiResClose(&game);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!agiPicAlloc(&pic)) {
|
||||
fprintf(stderr, "FAIL: agiPicAlloc out of memory\n");
|
||||
free(bytes);
|
||||
agiResClose(&game);
|
||||
return 1;
|
||||
}
|
||||
agiPicClear(&pic);
|
||||
|
||||
if (!agiPicDecode(&pic, bytes, length)) {
|
||||
fprintf(stderr, "FAIL: PIC %d decode rejected (malformed stream?)\n", (int)picId);
|
||||
agiPicFree(&pic);
|
||||
free(bytes);
|
||||
agiResClose(&game);
|
||||
return 1;
|
||||
}
|
||||
|
||||
nonBgVisual = 0u;
|
||||
nonBgPriority = 0u;
|
||||
for (i = 0u; i < (uint32_t)AGI_PIC_PIXELS; i++) {
|
||||
if (pic.visual[i] != AGI_VISUAL_BG) {
|
||||
nonBgVisual++;
|
||||
}
|
||||
if (pic.priority[i] != AGI_PRIORITY_BG) {
|
||||
nonBgPriority++;
|
||||
}
|
||||
}
|
||||
|
||||
snprintf(visPath, sizeof(visPath), "build/tests/pic%d_visual.ppm", (int)picId);
|
||||
snprintf(priPath, sizeof(priPath), "build/tests/pic%d_priority.pgm", (int)picId);
|
||||
if (!writePpm(visPath, pic.visual) || !writePgm(priPath, pic.priority)) {
|
||||
fprintf(stderr, "FAIL: could not write output images\n");
|
||||
agiPicFree(&pic);
|
||||
free(bytes);
|
||||
agiResClose(&game);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("PIC %d: %u bytes, visual %u/%u px painted, priority %u/%u px painted\n",
|
||||
(int)picId, (unsigned)length,
|
||||
(unsigned)nonBgVisual, (unsigned)AGI_PIC_PIXELS,
|
||||
(unsigned)nonBgPriority, (unsigned)AGI_PIC_PIXELS);
|
||||
printf(" wrote %s\n", visPath);
|
||||
printf(" wrote %s\n", priPath);
|
||||
|
||||
agiPicFree(&pic);
|
||||
free(bytes);
|
||||
agiResClose(&game);
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,284 +0,0 @@
|
|||
// Host-side AGI pipeline test (Phase 1 sub-D).
|
||||
//
|
||||
// Wires up a real logic-resource cache + trace callbacks and runs
|
||||
// the VM in a simulated game-cycle loop:
|
||||
// * agiVmRun()
|
||||
// * NEW_ROOM -> update var[0]/var[1], reset flags, restart logic.0
|
||||
// * RETURN -> count one cycle, restart logic.0
|
||||
// * UNKNOWN_OP -> print and exit
|
||||
// Capped at MAX_CYCLES so a logic that always returns without
|
||||
// painting can't loop forever.
|
||||
//
|
||||
// What this lets us verify before touching any emulator:
|
||||
// - logic.0's prologue + IF branching against fresh game state
|
||||
// - room transition handling (new.room(N) loads logic N)
|
||||
// - room logic execution (load.pic/draw.pic/show.pic callbacks)
|
||||
// - call.logic / RETURN frame management
|
||||
//
|
||||
// Usage:
|
||||
// testAgiPipeline <game-directory> [max-cycles]
|
||||
|
||||
#include "agi.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
|
||||
#define MAX_CYCLES_DEFAULT 20u
|
||||
#define AGI_LOGIC_HDR_BYTES 2u
|
||||
|
||||
|
||||
typedef struct {
|
||||
uint8_t *raw;
|
||||
uint16_t rawLength;
|
||||
const uint8_t *bytecode;
|
||||
uint16_t bytecodeLength;
|
||||
} CachedLogicT;
|
||||
|
||||
|
||||
static AgiGameT gGame;
|
||||
static CachedLogicT gLogicCache[AGI_MAX_RESOURCES];
|
||||
static uint32_t gTraceLoadPic;
|
||||
static uint32_t gTraceDrawPic;
|
||||
static uint32_t gTraceShowPic;
|
||||
static uint32_t gTraceDiscardPic;
|
||||
static uint32_t gTraceOverlayPic;
|
||||
static uint32_t gTraceFetchLogic;
|
||||
|
||||
|
||||
static void cbDiscardPic(void *ctx, uint8_t picId);
|
||||
static void cbDrawPic(void *ctx, uint8_t picId);
|
||||
static const uint8_t *cbFetchLogic(void *ctx, uint8_t logicId, uint16_t *outLength);
|
||||
static void cbLoadPic(void *ctx, uint8_t picId);
|
||||
static void cbOverlayPic(void *ctx, uint8_t picId);
|
||||
static void cbShowPic(void *ctx);
|
||||
static const char *haltName(AgiVmHaltE r);
|
||||
static void releaseCache(void);
|
||||
|
||||
|
||||
static void cbDiscardPic(void *ctx, uint8_t picId) {
|
||||
(void)ctx;
|
||||
gTraceDiscardPic++;
|
||||
printf(" [host] discardPic(%u)\n", (unsigned)picId);
|
||||
}
|
||||
|
||||
|
||||
static void cbDrawPic(void *ctx, uint8_t picId) {
|
||||
(void)ctx;
|
||||
gTraceDrawPic++;
|
||||
printf(" [host] drawPic(%u)\n", (unsigned)picId);
|
||||
}
|
||||
|
||||
|
||||
static const uint8_t *cbFetchLogic(void *ctx, uint8_t logicId, uint16_t *outLength) {
|
||||
CachedLogicT *slot;
|
||||
uint8_t *raw;
|
||||
uint16_t rawLength;
|
||||
uint16_t bcLen;
|
||||
|
||||
(void)ctx;
|
||||
slot = &gLogicCache[logicId];
|
||||
if (slot->bytecode != NULL) {
|
||||
*outLength = slot->bytecodeLength;
|
||||
return slot->bytecode;
|
||||
}
|
||||
|
||||
if (logicId >= gGame.resCount[AGI_RES_LOGIC]) {
|
||||
return NULL;
|
||||
}
|
||||
raw = agiResLoad(&gGame, AGI_RES_LOGIC, (uint16_t)logicId, &rawLength);
|
||||
if (raw == NULL || rawLength < AGI_LOGIC_HDR_BYTES) {
|
||||
if (raw != NULL) {
|
||||
free(raw);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
bcLen = (uint16_t)(raw[0] | ((uint16_t)raw[1] << 8));
|
||||
if ((uint16_t)(AGI_LOGIC_HDR_BYTES + bcLen) > rawLength) {
|
||||
free(raw);
|
||||
return NULL;
|
||||
}
|
||||
slot->raw = raw;
|
||||
slot->rawLength = rawLength;
|
||||
slot->bytecode = raw + AGI_LOGIC_HDR_BYTES;
|
||||
slot->bytecodeLength = bcLen;
|
||||
gTraceFetchLogic++;
|
||||
printf(" [host] fetchLogic(%u) -> %u bytes (first load)\n",
|
||||
(unsigned)logicId, (unsigned)bcLen);
|
||||
|
||||
*outLength = bcLen;
|
||||
return slot->bytecode;
|
||||
}
|
||||
|
||||
|
||||
static void cbLoadPic(void *ctx, uint8_t picId) {
|
||||
(void)ctx;
|
||||
gTraceLoadPic++;
|
||||
printf(" [host] loadPic(%u)\n", (unsigned)picId);
|
||||
}
|
||||
|
||||
|
||||
static void cbOverlayPic(void *ctx, uint8_t picId) {
|
||||
(void)ctx;
|
||||
gTraceOverlayPic++;
|
||||
printf(" [host] overlayPic(%u)\n", (unsigned)picId);
|
||||
}
|
||||
|
||||
|
||||
static void cbShowPic(void *ctx) {
|
||||
(void)ctx;
|
||||
gTraceShowPic++;
|
||||
printf(" [host] showPic\n");
|
||||
}
|
||||
|
||||
|
||||
static const char *haltName(AgiVmHaltE r) {
|
||||
switch (r) {
|
||||
case AGI_VM_HALT_NONE: return "NONE";
|
||||
case AGI_VM_HALT_RETURN: return "RETURN";
|
||||
case AGI_VM_HALT_UNKNOWN_OP: return "UNKNOWN_OP";
|
||||
case AGI_VM_HALT_TRUNCATED: return "TRUNCATED";
|
||||
case AGI_VM_HALT_NEW_ROOM: return "NEW_ROOM";
|
||||
case AGI_VM_HALT_NO_LOGIC: return "NO_LOGIC";
|
||||
case AGI_VM_HALT_STACK_OVER: return "STACK_OVER";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void releaseCache(void) {
|
||||
uint16_t i;
|
||||
|
||||
for (i = 0u; i < AGI_MAX_RESOURCES; i++) {
|
||||
if (gLogicCache[i].raw != NULL) {
|
||||
free(gLogicCache[i].raw);
|
||||
gLogicCache[i].raw = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
AgiVmT vm;
|
||||
AgiVmCallbacksT cb;
|
||||
uint16_t length;
|
||||
const uint8_t *logic0;
|
||||
uint32_t maxCycles;
|
||||
uint32_t cycles;
|
||||
AgiVmHaltE reason;
|
||||
uint8_t prevRoom;
|
||||
uint8_t i;
|
||||
|
||||
if (argc < 2 || argc > 3) {
|
||||
fprintf(stderr, "usage: %s <game-directory> [max-cycles]\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
maxCycles = (argc == 3) ? (uint32_t)atoi(argv[2]) : (uint32_t)MAX_CYCLES_DEFAULT;
|
||||
if (maxCycles == 0u) {
|
||||
maxCycles = (uint32_t)MAX_CYCLES_DEFAULT;
|
||||
}
|
||||
|
||||
if (!agiResOpen(&gGame)) {
|
||||
fprintf(stderr, "FAIL: agiResOpen rejected '%s'\n", argv[1]);
|
||||
return 1;
|
||||
}
|
||||
memset(gLogicCache, 0, sizeof(gLogicCache));
|
||||
|
||||
cb.fetchLogic = cbFetchLogic;
|
||||
cb.loadPic = cbLoadPic;
|
||||
cb.drawPic = cbDrawPic;
|
||||
cb.showPic = cbShowPic;
|
||||
cb.discardPic = cbDiscardPic;
|
||||
cb.overlayPic = cbOverlayPic;
|
||||
cb.haveKey = NULL;
|
||||
cb.fetchMessage = NULL;
|
||||
cb.addToPic = NULL;
|
||||
cb.soundDuration = NULL;
|
||||
cb.playSound = NULL;
|
||||
cb.stopSound = NULL;
|
||||
cb.ctx = NULL;
|
||||
|
||||
agiVmInit(&vm);
|
||||
agiVmSetCallbacks(&vm, &cb);
|
||||
|
||||
length = 0u;
|
||||
logic0 = cbFetchLogic(NULL, 0u, &length);
|
||||
if (logic0 == NULL) {
|
||||
fprintf(stderr, "FAIL: could not load logic.0\n");
|
||||
releaseCache();
|
||||
agiResClose(&gGame);
|
||||
return 1;
|
||||
}
|
||||
agiVmResetToLogic(&vm, logic0, length, 0u);
|
||||
|
||||
cycles = 0u;
|
||||
for (;;) {
|
||||
if (cycles >= maxCycles) {
|
||||
printf(" (capped at %u cycles)\n", (unsigned)maxCycles);
|
||||
break;
|
||||
}
|
||||
cycles++;
|
||||
// Simulate one wall-clock second per cycle so per-second
|
||||
// game logic (countdowns, state machines, animation cadence)
|
||||
// advances under the host test without waiting in real time.
|
||||
// KQ3's title state machine in particular is gated entirely
|
||||
// on var 11 changing; without this tick logic.45's countdowns
|
||||
// never fire and the title appears to stall at 20+ cycles.
|
||||
agiVmTickSeconds(&vm, 1u);
|
||||
printf(" cycle %u: pc=%u/%u logic=%u callDepth=%u\n",
|
||||
(unsigned)cycles, (unsigned)vm.pc, (unsigned)vm.codeLength,
|
||||
(unsigned)vm.currentLogicId, (unsigned)vm.callDepth);
|
||||
|
||||
reason = agiVmRun(&vm);
|
||||
|
||||
printf(" halt=%s at pc=%u logic=%u",
|
||||
haltName(reason), (unsigned)vm.pc, (unsigned)vm.currentLogicId);
|
||||
if (reason == AGI_VM_HALT_UNKNOWN_OP) {
|
||||
printf(" opcode=0x%02X", (unsigned)vm.lastUnknownOp);
|
||||
}
|
||||
if (reason == AGI_VM_HALT_NEW_ROOM) {
|
||||
printf(" newRoom=%u", (unsigned)vm.newRoomId);
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
if (reason == AGI_VM_HALT_NEW_ROOM) {
|
||||
prevRoom = vm.vars[0];
|
||||
vm.vars[0] = vm.newRoomId;
|
||||
vm.vars[1] = prevRoom;
|
||||
// Reset most flags, then set flag 5 ("new room") to
|
||||
// signal the room logic that its init branch should
|
||||
// fire. In the real interpreter the engine also resets
|
||||
// a handful of cycle vars (input, signal, etc.) but for
|
||||
// the sub-D pipeline test the new-room flag is the only
|
||||
// one room logic actually checks.
|
||||
for (i = 0u; i < 32u; i++) {
|
||||
vm.flags[i] = 0u;
|
||||
}
|
||||
vm.flags[5] = 1u;
|
||||
agiVmResetToLogic(&vm, logic0, length, 0u);
|
||||
continue;
|
||||
}
|
||||
if (reason == AGI_VM_HALT_RETURN) {
|
||||
// End-of-cycle housekeeping the real interpreter would
|
||||
// perform: clear flag 5 (new.room) so subsequent cycles
|
||||
// don't re-run the room-init branch.
|
||||
vm.flags[5] = 0u;
|
||||
agiVmResetToLogic(&vm, logic0, length, 0u);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
printf("===== trace =====\n");
|
||||
printf(" fetchLogic: %u (first-loads only)\n", (unsigned)gTraceFetchLogic);
|
||||
printf(" loadPic: %u\n", (unsigned)gTraceLoadPic);
|
||||
printf(" drawPic: %u\n", (unsigned)gTraceDrawPic);
|
||||
printf(" showPic: %u\n", (unsigned)gTraceShowPic);
|
||||
printf(" discardPic: %u\n", (unsigned)gTraceDiscardPic);
|
||||
printf(" overlayPic: %u\n", (unsigned)gTraceOverlayPic);
|
||||
|
||||
releaseCache();
|
||||
agiResClose(&gGame);
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
// Host-side regression test for agiRes.c.
|
||||
//
|
||||
// The AGI resource loader is plain stdio C with no JoeyLib runtime
|
||||
// dependencies, so we can compile and exercise it on the build host
|
||||
// without touching any emulator. This keeps the Phase 0 iteration
|
||||
// loop fast: edit agiRes.c, rerun scripts/test-agi.sh, get a pass
|
||||
// or fail against KQ3 (or any AGI v2 directory) in under a second.
|
||||
//
|
||||
// Usage: testAgiRes <game-directory>
|
||||
//
|
||||
// Exit codes:
|
||||
// 0 resource directories parsed and at least one resource per
|
||||
// type loaded successfully (or the type was empty)
|
||||
// 1 game directory missing or malformed
|
||||
// 2 invalid command line
|
||||
|
||||
#include "agi.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
|
||||
// ----- Prototypes -----
|
||||
|
||||
static const char *resTypeName(AgiResTypeE type);
|
||||
static int testFirstResource(AgiGameT *game, AgiResTypeE type);
|
||||
|
||||
|
||||
// ----- Helpers (alphabetical) -----
|
||||
|
||||
static const char *resTypeName(AgiResTypeE type) {
|
||||
switch (type) {
|
||||
case AGI_RES_LOGIC: return "LOGIC";
|
||||
case AGI_RES_PIC: return "PIC ";
|
||||
case AGI_RES_VIEW: return "VIEW ";
|
||||
case AGI_RES_SOUND: return "SOUND";
|
||||
default: return "?????";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Locate the first non-empty resource of `type`, load it, print its
|
||||
// length. Returns 0 on success or empty type, 1 on load failure.
|
||||
static int testFirstResource(AgiGameT *game, AgiResTypeE type) {
|
||||
uint16_t i;
|
||||
uint16_t length;
|
||||
uint8_t *payload;
|
||||
uint16_t count;
|
||||
|
||||
count = game->resCount[type];
|
||||
for (i = 0; i < count; i++) {
|
||||
if (game->resDir[type][i].volume >= AGI_MAX_VOLUMES) {
|
||||
continue;
|
||||
}
|
||||
payload = agiResLoad(game, type, i, &length);
|
||||
if (payload == NULL) {
|
||||
fprintf(stderr, "FAIL: %s id %u failed to load\n", resTypeName(type), (unsigned)i);
|
||||
return 1;
|
||||
}
|
||||
printf(" %s: %u resources, first non-empty id %u is %u bytes\n",
|
||||
resTypeName(type), (unsigned)count, (unsigned)i, (unsigned)length);
|
||||
free(payload);
|
||||
return 0;
|
||||
}
|
||||
printf(" %s: %u directory slots, all empty\n", resTypeName(type), (unsigned)count);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
// ----- main -----
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
AgiGameT game;
|
||||
uint8_t t;
|
||||
int rc;
|
||||
|
||||
if (argc != 2) {
|
||||
fprintf(stderr, "usage: %s <game-directory>\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
|
||||
printf("Opening AGI game directory: %s\n", argv[1]);
|
||||
if (!agiResOpen(&game)) {
|
||||
fprintf(stderr, "FAIL: agiResOpen rejected '%s' (missing files? wrong format?)\n", argv[1]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
rc = 0;
|
||||
for (t = 0; t < AGI_RES_COUNT; t++) {
|
||||
if (testFirstResource(&game, (AgiResTypeE)t) != 0) {
|
||||
rc = 1;
|
||||
}
|
||||
}
|
||||
|
||||
agiResClose(&game);
|
||||
if (rc == 0) {
|
||||
printf("OK\n");
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
|
@ -1,198 +0,0 @@
|
|||
// Host-side test for agiView.c.
|
||||
//
|
||||
// Loads a single VIEW resource, parses it, dumps the loop/cel
|
||||
// structure, and writes cel 0 of every loop as a PPM image. The
|
||||
// transparent color is rendered as bright magenta so the cel's
|
||||
// silhouette stands out from the actual painted pixels.
|
||||
//
|
||||
// Usage:
|
||||
// testAgiView <game-directory> [view-id]
|
||||
//
|
||||
// Exit codes:
|
||||
// 0 parsed; outputs written
|
||||
// 1 game directory missing, view slot empty, or parse failed
|
||||
// 2 invalid command line
|
||||
|
||||
#include "agi.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
|
||||
#define CHANNEL_SCALE 17u
|
||||
|
||||
// Pseudo-color used for "transparent" pixels in the output image.
|
||||
#define TRANS_R 0xFFu
|
||||
#define TRANS_G 0x00u
|
||||
#define TRANS_B 0xFFu
|
||||
|
||||
|
||||
// ----- Prototypes -----
|
||||
|
||||
static int16_t firstNonEmptyView(const AgiGameT *game);
|
||||
static bool writeCelPpm(const char *path, const AgiCelInfoT *cel);
|
||||
|
||||
|
||||
// ----- Helpers (alphabetical) -----
|
||||
|
||||
static int16_t firstNonEmptyView(const AgiGameT *game) {
|
||||
uint16_t count;
|
||||
uint16_t i;
|
||||
|
||||
count = game->resCount[AGI_RES_VIEW];
|
||||
for (i = 0u; i < count; i++) {
|
||||
if (game->resDir[AGI_RES_VIEW][i].volume < AGI_MAX_VOLUMES) {
|
||||
return (int16_t)i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
// Walk the cel's RLE data, paint into a chunky 8bpp buffer, then
|
||||
// emit a PPM with transparent pixels mapped to magenta. Same RLE
|
||||
// rules as agiViewDraw: each byte = (color << 4) | runLen, runLen == 0
|
||||
// ends the row.
|
||||
static bool writeCelPpm(const char *path, const AgiCelInfoT *cel) {
|
||||
FILE *fp;
|
||||
uint8_t *buf;
|
||||
uint16_t w;
|
||||
uint16_t h;
|
||||
uint16_t cx;
|
||||
uint16_t cy;
|
||||
uint16_t i;
|
||||
const uint8_t *rle;
|
||||
uint8_t rleByte;
|
||||
uint8_t color;
|
||||
uint8_t runLen;
|
||||
uint8_t r;
|
||||
uint8_t transparent;
|
||||
uint16_t rgb;
|
||||
|
||||
w = (uint16_t)cel->width;
|
||||
h = (uint16_t)cel->height;
|
||||
if (w == 0u || h == 0u) {
|
||||
return false;
|
||||
}
|
||||
|
||||
buf = (uint8_t *)malloc((size_t)w * (size_t)h);
|
||||
if (buf == NULL) {
|
||||
return false;
|
||||
}
|
||||
transparent = cel->transparentColor;
|
||||
memset(buf, transparent, (size_t)w * (size_t)h);
|
||||
|
||||
rle = cel->rleData;
|
||||
for (cy = 0u; cy < h; cy++) {
|
||||
cx = 0u;
|
||||
for (;;) {
|
||||
rleByte = *rle++;
|
||||
if (rleByte == 0u) {
|
||||
break;
|
||||
}
|
||||
color = (uint8_t)(rleByte >> 4);
|
||||
runLen = (uint8_t)(rleByte & 0x0Fu);
|
||||
for (r = 0u; r < runLen; r++) {
|
||||
if (cx >= w) {
|
||||
break;
|
||||
}
|
||||
buf[cy * w + cx] = color;
|
||||
cx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fp = fopen(path, "wb");
|
||||
if (fp == NULL) {
|
||||
free(buf);
|
||||
return false;
|
||||
}
|
||||
fprintf(fp, "P6\n%u %u\n255\n", (unsigned)w, (unsigned)h);
|
||||
for (i = 0u; i < (uint16_t)(w * h); i++) {
|
||||
if (buf[i] == transparent) {
|
||||
fputc(TRANS_R, fp);
|
||||
fputc(TRANS_G, fp);
|
||||
fputc(TRANS_B, fp);
|
||||
} else {
|
||||
rgb = kAgiPalette[buf[i] & 0x0Fu];
|
||||
fputc((int)(((rgb >> 8) & 0x0Fu) * CHANNEL_SCALE), fp);
|
||||
fputc((int)(((rgb >> 4) & 0x0Fu) * CHANNEL_SCALE), fp);
|
||||
fputc((int)((rgb & 0x0Fu) * CHANNEL_SCALE), fp);
|
||||
}
|
||||
}
|
||||
fclose(fp);
|
||||
free(buf);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// ----- main -----
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
AgiGameT game;
|
||||
AgiViewT view;
|
||||
int16_t viewId;
|
||||
uint8_t *bytes;
|
||||
uint16_t length;
|
||||
uint8_t li;
|
||||
uint8_t ci;
|
||||
const AgiCelInfoT *cel;
|
||||
char path[256];
|
||||
|
||||
if (argc < 2 || argc > 3) {
|
||||
fprintf(stderr, "usage: %s <game-directory> [view-id]\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (!agiResOpen(&game)) {
|
||||
fprintf(stderr, "FAIL: agiResOpen rejected '%s'\n", argv[1]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (argc == 3) {
|
||||
viewId = (int16_t)atoi(argv[2]);
|
||||
} else {
|
||||
viewId = firstNonEmptyView(&game);
|
||||
}
|
||||
if (viewId < 0 || (uint16_t)viewId >= game.resCount[AGI_RES_VIEW]) {
|
||||
fprintf(stderr, "FAIL: no VIEW at id %d\n", (int)viewId);
|
||||
agiResClose(&game);
|
||||
return 1;
|
||||
}
|
||||
|
||||
bytes = agiResLoad(&game, AGI_RES_VIEW, (uint16_t)viewId, &length);
|
||||
if (bytes == NULL) {
|
||||
fprintf(stderr, "FAIL: VIEW %d failed to load\n", (int)viewId);
|
||||
agiResClose(&game);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!agiViewParse(&view, bytes, length)) {
|
||||
fprintf(stderr, "FAIL: VIEW %d parse rejected\n", (int)viewId);
|
||||
agiResClose(&game);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("VIEW %d: %u bytes, %u loops\n", (int)viewId, (unsigned)length, (unsigned)view.loopCount);
|
||||
for (li = 0u; li < view.loopCount; li++) {
|
||||
printf(" loop %u: %u cels\n", (unsigned)li, (unsigned)view.loops[li].celCount);
|
||||
for (ci = 0u; ci < view.loops[li].celCount; ci++) {
|
||||
cel = &view.loops[li].cels[ci];
|
||||
printf(" cel %u: %ux%u trans=%u mirror=%u (src loop %u)\n",
|
||||
(unsigned)ci, (unsigned)cel->width, (unsigned)cel->height,
|
||||
(unsigned)cel->transparentColor,
|
||||
(unsigned)cel->mirrored, (unsigned)cel->mirrorSourceLoop);
|
||||
}
|
||||
if (view.loops[li].celCount > 0u) {
|
||||
snprintf(path, sizeof(path), "build/tests/view%d_loop%u_cel0.ppm", (int)viewId, (unsigned)li);
|
||||
if (!writeCelPpm(path, &view.loops[li].cels[0])) {
|
||||
fprintf(stderr, " warn: could not write %s\n", path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
agiViewFree(&view);
|
||||
agiResClose(&game);
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
// Host-side test for agiVm.c (Phase 1 sub-A).
|
||||
//
|
||||
// Loads a LOGIC resource (default 0), runs the VM until it halts,
|
||||
// and prints the halt reason plus any non-zero vars/flags. Since
|
||||
// only the arithmetic and flag opcodes (0x00-0x11) are implemented
|
||||
// this pass, expectation is one of:
|
||||
// - HALT_RETURN at offset N (logic was entirely sub-A opcodes)
|
||||
// - HALT_UNKNOWN_OP 0xXX at offset N (hit IF / new.room / etc.)
|
||||
// - HALT_TRUNCATED at offset N (malformed resource)
|
||||
//
|
||||
// Usage:
|
||||
// testAgiVm <game-directory> [logic-id]
|
||||
|
||||
#include "agi.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
|
||||
// ----- Prototypes -----
|
||||
|
||||
static const char *haltName(AgiVmHaltE r);
|
||||
static void printNonZeroState(const AgiVmT *vm);
|
||||
|
||||
|
||||
// ----- Helpers (alphabetical) -----
|
||||
|
||||
static const char *haltName(AgiVmHaltE r) {
|
||||
switch (r) {
|
||||
case AGI_VM_HALT_NONE: return "NONE";
|
||||
case AGI_VM_HALT_RETURN: return "RETURN";
|
||||
case AGI_VM_HALT_UNKNOWN_OP: return "UNKNOWN_OP";
|
||||
case AGI_VM_HALT_TRUNCATED: return "TRUNCATED";
|
||||
case AGI_VM_HALT_NEW_ROOM: return "NEW_ROOM";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void printNonZeroState(const AgiVmT *vm) {
|
||||
uint16_t i;
|
||||
uint16_t nonZeroVars;
|
||||
uint16_t nonZeroFlags;
|
||||
|
||||
nonZeroVars = 0u;
|
||||
nonZeroFlags = 0u;
|
||||
for (i = 0u; i < AGI_VM_NUM_VARS; i++) {
|
||||
if (vm->vars[i] != 0u) {
|
||||
if (nonZeroVars == 0u) {
|
||||
printf(" vars:");
|
||||
}
|
||||
printf(" v%u=%u", (unsigned)i, (unsigned)vm->vars[i]);
|
||||
nonZeroVars++;
|
||||
}
|
||||
}
|
||||
if (nonZeroVars > 0u) {
|
||||
printf("\n");
|
||||
}
|
||||
for (i = 0u; i < AGI_VM_NUM_FLAGS; i++) {
|
||||
if (vm->flags[i] != 0u) {
|
||||
if (nonZeroFlags == 0u) {
|
||||
printf(" flags:");
|
||||
}
|
||||
printf(" f%u", (unsigned)i);
|
||||
nonZeroFlags++;
|
||||
}
|
||||
}
|
||||
if (nonZeroFlags > 0u) {
|
||||
printf("\n");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ----- main -----
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
AgiGameT game;
|
||||
AgiVmT vm;
|
||||
uint16_t logicId;
|
||||
uint8_t *bytes;
|
||||
uint16_t length;
|
||||
AgiVmHaltE reason;
|
||||
|
||||
if (argc < 2 || argc > 3) {
|
||||
fprintf(stderr, "usage: %s <game-directory> [logic-id]\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (!agiResOpen(&game)) {
|
||||
fprintf(stderr, "FAIL: agiResOpen rejected '%s'\n", argv[1]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
logicId = (argc == 3) ? (uint16_t)atoi(argv[2]) : 0u;
|
||||
if (logicId >= game.resCount[AGI_RES_LOGIC]) {
|
||||
fprintf(stderr, "FAIL: no LOGIC at id %u\n", (unsigned)logicId);
|
||||
agiResClose(&game);
|
||||
return 1;
|
||||
}
|
||||
|
||||
bytes = agiResLoad(&game, AGI_RES_LOGIC, logicId, &length);
|
||||
if (bytes == NULL) {
|
||||
fprintf(stderr, "FAIL: LOGIC %u failed to load\n", (unsigned)logicId);
|
||||
agiResClose(&game);
|
||||
return 1;
|
||||
}
|
||||
|
||||
agiVmInit(&vm);
|
||||
if (!agiVmLoadLogic(&vm, bytes, length)) {
|
||||
fprintf(stderr, "FAIL: agiVmLoadLogic rejected LOGIC %u (truncated header?)\n", (unsigned)logicId);
|
||||
free(bytes);
|
||||
agiResClose(&game);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("LOGIC %u: %u bytes resource, %u bytes bytecode\n",
|
||||
(unsigned)logicId, (unsigned)length, (unsigned)vm.codeLength);
|
||||
|
||||
reason = agiVmRun(&vm);
|
||||
|
||||
printf(" halt=%s at pc=%u/%u",
|
||||
haltName(reason), (unsigned)vm.pc, (unsigned)vm.codeLength);
|
||||
if (reason == AGI_VM_HALT_UNKNOWN_OP) {
|
||||
printf(" opcode=0x%02X", (unsigned)vm.lastUnknownOp);
|
||||
}
|
||||
if (reason == AGI_VM_HALT_NEW_ROOM) {
|
||||
printf(" newRoom=%u", (unsigned)vm.newRoomId);
|
||||
}
|
||||
printf("\n");
|
||||
printNonZeroState(&vm);
|
||||
|
||||
free(bytes);
|
||||
agiResClose(&game);
|
||||
return 0;
|
||||
}
|
||||
233
tests/testResources.c
Normal file
233
tests/testResources.c
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
// testResources: every resource of the game in DATA/ loads and decodes.
|
||||
//
|
||||
// Prints the interpreter version the game's files name, then one line per
|
||||
// resource to stdout -- a picture's visual and priority screens as FNV-1a
|
||||
// hashes, a view's loop and cel counts with a hash of its decoded cels, a
|
||||
// logic's code and message counts, a sound's note counts -- for
|
||||
// scripts/test-agi.sh to compare, and a summary line on stderr. Exits 1
|
||||
// when a resource is malformed.
|
||||
//
|
||||
// Run on the host (BLANK port) from a folder holding DATA/.
|
||||
|
||||
#include "agi.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "joey/core.h"
|
||||
|
||||
|
||||
#define FNV_OFFSET_BASIS 2166136261u
|
||||
#define FNV_PRIME 16777619u
|
||||
#define LOGIC_HEADER_BYTES 2u
|
||||
#define SOUND_VOICES 4u
|
||||
#define SOUND_NOTE_BYTES 5u
|
||||
#define SOUND_END_BYTE 0xFFu
|
||||
#define RLE_COLOR_SHIFT 4u
|
||||
#define RLE_COUNT_MASK 0x0Fu
|
||||
#define MAX_CEL_PIXELS (255u * 255u)
|
||||
|
||||
|
||||
// ----- Prototypes -----
|
||||
|
||||
static uint32_t fnv(uint32_t hash, const uint8_t *p, uint32_t length);
|
||||
static bool testLogic(uint16_t n);
|
||||
static bool testPicture(uint16_t n, uint8_t *visual, uint8_t *priority);
|
||||
static bool testSound(uint16_t n);
|
||||
static bool testView(uint16_t n, uint8_t *pixels);
|
||||
|
||||
|
||||
// ----- Internal helpers (alphabetical) -----
|
||||
|
||||
static uint32_t fnv(uint32_t hash, const uint8_t *p, uint32_t length) {
|
||||
uint32_t i;
|
||||
|
||||
for (i = 0u; i < length; i++) {
|
||||
hash = (hash ^ p[i]) * FNV_PRIME;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
|
||||
static bool testLogic(uint16_t n) {
|
||||
const uint8_t *raw;
|
||||
uint16_t codeLen;
|
||||
|
||||
if (!vmLoadLogic((uint8_t)n)) {
|
||||
fprintf(stderr, "logic %u: does not load\n", (unsigned)n);
|
||||
return false;
|
||||
}
|
||||
raw = gAgi->logicRaw[n];
|
||||
codeLen = (uint16_t)(raw[0] | ((uint16_t)raw[1] << 8));
|
||||
printf("logic %u code %u messages %u\n", (unsigned)n, (unsigned)codeLen, (unsigned)raw[LOGIC_HEADER_BYTES + codeLen]);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static bool testPicture(uint16_t n, uint8_t *visual, uint8_t *priority) {
|
||||
uint8_t *data;
|
||||
uint16_t length;
|
||||
|
||||
data = agiResLoad(&gAgi->game, AGI_RES_PIC, n, &length);
|
||||
if (data == NULL) {
|
||||
fprintf(stderr, "picture %u: does not load\n", (unsigned)n);
|
||||
return false;
|
||||
}
|
||||
memset(visual, AGI_PIC_BG_COLOR, AGI_PIXELS);
|
||||
memset(priority, AGI_PIC_BG_PRIORITY, AGI_PIXELS);
|
||||
if (!agiPicDecode(visual, priority, data, length)) {
|
||||
fprintf(stderr, "picture %u: does not decode\n", (unsigned)n);
|
||||
jlFree(data);
|
||||
return false;
|
||||
}
|
||||
jlFree(data);
|
||||
printf("picture %u visual %08x priority %08x\n", (unsigned)n,
|
||||
(unsigned)fnv(FNV_OFFSET_BASIS, visual, AGI_PIXELS), (unsigned)fnv(FNV_OFFSET_BASIS, priority, AGI_PIXELS));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Every voice's notes must end (FF FF) inside the resource.
|
||||
static bool testSound(uint16_t n) {
|
||||
uint8_t *data;
|
||||
uint16_t length;
|
||||
uint16_t pos;
|
||||
uint16_t notes[SOUND_VOICES];
|
||||
uint8_t v;
|
||||
|
||||
data = agiResLoad(&gAgi->game, AGI_RES_SOUND, n, &length);
|
||||
if (data == NULL || length < SOUND_VOICES * 2u) {
|
||||
fprintf(stderr, "sound %u: does not load\n", (unsigned)n);
|
||||
jlFree(data);
|
||||
return false;
|
||||
}
|
||||
for (v = 0u; v < SOUND_VOICES; v++) {
|
||||
pos = (uint16_t)(data[v * 2u] | ((uint16_t)data[v * 2u + 1u] << 8));
|
||||
notes[v] = 0u;
|
||||
while ((uint32_t)pos + 1u < length && !(data[pos] == SOUND_END_BYTE && data[pos + 1u] == SOUND_END_BYTE)) {
|
||||
pos = (uint16_t)(pos + SOUND_NOTE_BYTES);
|
||||
notes[v]++;
|
||||
}
|
||||
if ((uint32_t)pos + 1u >= length) {
|
||||
fprintf(stderr, "sound %u: voice %u runs off the end\n", (unsigned)n, (unsigned)v);
|
||||
jlFree(data);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
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]);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Decode every cel's runs: no row may run past the cel's width or the
|
||||
// resource's end.
|
||||
static bool testView(uint16_t n, uint8_t *pixels) {
|
||||
const AgiViewT *view;
|
||||
const AgiCelT *c;
|
||||
const uint8_t *rle;
|
||||
const uint8_t *end;
|
||||
uint32_t hash;
|
||||
uint16_t cels;
|
||||
uint8_t loop;
|
||||
uint8_t i;
|
||||
uint8_t row;
|
||||
uint8_t x;
|
||||
uint8_t run;
|
||||
|
||||
view = agiViewGet((uint8_t)n);
|
||||
if (view == NULL) {
|
||||
fprintf(stderr, "view %u: does not load\n", (unsigned)n);
|
||||
return false;
|
||||
}
|
||||
end = view->raw + view->rawLength;
|
||||
hash = FNV_OFFSET_BASIS;
|
||||
cels = 0u;
|
||||
for (loop = 0u; loop < view->loopCount; loop++) {
|
||||
for (i = 0u; i < view->loops[loop].celCount; i++) {
|
||||
c = &view->loops[loop].cels[i];
|
||||
rle = c->rle;
|
||||
memset(pixels, c->transparent, (size_t)c->width * c->height);
|
||||
for (row = 0u; row < c->height; row++) {
|
||||
x = 0u;
|
||||
while (rle < end && *rle != 0u) {
|
||||
run = (uint8_t)(*rle & RLE_COUNT_MASK);
|
||||
if ((uint16_t)x + run > c->width) {
|
||||
fprintf(stderr, "view %u loop %u cel %u: row %u is too wide\n", (unsigned)n, (unsigned)loop, (unsigned)i, (unsigned)row);
|
||||
return false;
|
||||
}
|
||||
memset(&pixels[(uint16_t)row * c->width + x], *rle >> RLE_COLOR_SHIFT, run);
|
||||
x = (uint8_t)(x + run);
|
||||
rle++;
|
||||
}
|
||||
if (rle >= end) {
|
||||
fprintf(stderr, "view %u loop %u cel %u: runs off the end\n", (unsigned)n, (unsigned)loop, (unsigned)i);
|
||||
return false;
|
||||
}
|
||||
rle++;
|
||||
}
|
||||
hash = fnv(hash, pixels, (uint32_t)c->width * c->height);
|
||||
cels++;
|
||||
}
|
||||
}
|
||||
printf("view %u loops %u cels %u pixels %08x\n", (unsigned)n, (unsigned)view->loopCount, (unsigned)cels, (unsigned)hash);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// ----- Entry point -----
|
||||
|
||||
int main(void) {
|
||||
jlConfigT cfg;
|
||||
uint8_t *visual;
|
||||
uint8_t *priority;
|
||||
uint8_t *pixels;
|
||||
uint16_t n;
|
||||
uint16_t counts[AGI_RES_COUNT];
|
||||
bool ok;
|
||||
|
||||
memset(&cfg, 0, sizeof(cfg));
|
||||
if (!jlInit(&cfg)) {
|
||||
fprintf(stderr, "jlInit failed\n");
|
||||
return 1;
|
||||
}
|
||||
gAgi = (AgiStateT *)calloc(1u, sizeof(AgiStateT));
|
||||
if (gAgi == NULL || !agiResOpen(&gAgi->game)) {
|
||||
fprintf(stderr, "no game in DATA/\n");
|
||||
return 1;
|
||||
}
|
||||
// The Sierra interpreter version the game's files name (its quirks
|
||||
// follow it).
|
||||
printf("interpreter %u\n", (unsigned)gAgi->game.interpreterVersion);
|
||||
visual = (uint8_t *)malloc(AGI_PIXELS);
|
||||
priority = (uint8_t *)malloc(AGI_PIXELS);
|
||||
pixels = (uint8_t *)malloc(MAX_CEL_PIXELS);
|
||||
if (visual == NULL || priority == NULL || pixels == NULL) {
|
||||
fprintf(stderr, "out of memory\n");
|
||||
return 1;
|
||||
}
|
||||
memset(counts, 0, sizeof(counts));
|
||||
ok = true;
|
||||
for (n = 0u; n < AGI_MAX_RESOURCES; n++) {
|
||||
if (n < gAgi->game.resCount[AGI_RES_LOGIC] && gAgi->game.resDir[AGI_RES_LOGIC][n].volume < AGI_MAX_VOLUMES) {
|
||||
ok = testLogic(n) && ok;
|
||||
counts[AGI_RES_LOGIC]++;
|
||||
}
|
||||
if (n < gAgi->game.resCount[AGI_RES_PIC] && gAgi->game.resDir[AGI_RES_PIC][n].volume < AGI_MAX_VOLUMES) {
|
||||
ok = testPicture(n, visual, priority) && ok;
|
||||
counts[AGI_RES_PIC]++;
|
||||
}
|
||||
if (n < gAgi->game.resCount[AGI_RES_VIEW] && gAgi->game.resDir[AGI_RES_VIEW][n].volume < AGI_MAX_VOLUMES) {
|
||||
ok = testView(n, pixels) && ok;
|
||||
counts[AGI_RES_VIEW]++;
|
||||
}
|
||||
if (n < gAgi->game.resCount[AGI_RES_SOUND] && gAgi->game.resDir[AGI_RES_SOUND][n].volume < AGI_MAX_VOLUMES) {
|
||||
ok = testSound(n) && ok;
|
||||
counts[AGI_RES_SOUND]++;
|
||||
}
|
||||
}
|
||||
fprintf(stderr, "%u logics, %u pictures, %u views, %u sounds%s\n", (unsigned)counts[AGI_RES_LOGIC], (unsigned)counts[AGI_RES_PIC],
|
||||
(unsigned)counts[AGI_RES_VIEW], (unsigned)counts[AGI_RES_SOUND], ok ? "" : " -- MALFORMED (see above)");
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
200
tests/tools/compareFrames.py
Executable file
200
tests/tools/compareFrames.py
Executable file
|
|
@ -0,0 +1,200 @@
|
|||
#!/usr/bin/env python3
|
||||
# compareFrames.py - compare a frame from Sierra's interpreter with one
|
||||
# from JoeyAGI. Both are mapped to the 16 EGA colours, then compared one
|
||||
# 8x8 text cell at a time. JoeyAGI draws text in its own font, so a cell
|
||||
# where both frames hold a character -- read with each interpreter's font,
|
||||
# captured from the "font" probe -- counts as equal when the characters
|
||||
# and colours match. Every other differing pixel is a real difference.
|
||||
#
|
||||
# Sierra shows its text screen (text.screen, the inventory) in the PC's
|
||||
# 40-column text mode, which DOSBox captures at 360x400 (9x16 cells).
|
||||
# JoeyAGI draws the same screen on the bitmap, so those frames are
|
||||
# compared cell by cell: blank or not, and the colours.
|
||||
#
|
||||
# compareFrames.py ORACLE JOEY [DIFF.png] [--fonts ORACLEFONT JOEYFONT]
|
||||
# prints "real=N text=M" and the cells that differ; exit 1 if any do.
|
||||
import sys
|
||||
from collections import Counter
|
||||
|
||||
from PIL import Image
|
||||
|
||||
EGA = [(0, 0, 0), (0, 0, 170), (0, 170, 0), (0, 170, 170), (170, 0, 0), (170, 0, 170), (170, 85, 0),
|
||||
(170, 170, 170), (85, 85, 85), (85, 85, 255), (85, 255, 85), (85, 255, 255), (255, 85, 85),
|
||||
(255, 85, 255), (255, 255, 85), (255, 255, 255)]
|
||||
WIDTH = 320
|
||||
HEIGHT = 200
|
||||
TEXT_MODE_SIZE = (360, 400)
|
||||
TEXT_CELL_W = 9
|
||||
TEXT_CELL_H = 16
|
||||
CELL = 8
|
||||
COLS = WIDTH // CELL
|
||||
ROWS = HEIGHT // CELL
|
||||
WINDOW_FRAME = 4
|
||||
WINDOW_BG = 15
|
||||
WINDOW_MIN_RUN = 16
|
||||
UNKNOWN = "?"
|
||||
# The font probe displays these rows from row 0, column 0.
|
||||
FONT_ROWS = ['!"#$&\'()*+,-./0123456789:;<=>?', '@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^', '_`abcdefghijklmnopqrstuvwxyz{|', '}~']
|
||||
|
||||
_nearest = {}
|
||||
|
||||
|
||||
def isTextMode(path):
|
||||
return Image.open(path).size == TEXT_MODE_SIZE
|
||||
|
||||
|
||||
def egaGrid(im):
|
||||
px = im.load()
|
||||
grid = []
|
||||
for y in range(im.size[1]):
|
||||
row = []
|
||||
for x in range(im.size[0]):
|
||||
c = px[x, y]
|
||||
if c not in _nearest:
|
||||
_nearest[c] = min(range(16), key=lambda i: sum((a - b) ** 2 for a, b in zip(EGA[i], c)))
|
||||
row.append(_nearest[c])
|
||||
grid.append(row)
|
||||
return grid
|
||||
|
||||
|
||||
def load(path):
|
||||
# A frame as rows of EGA colour numbers, scaled to 320x200 if need be.
|
||||
im = Image.open(path).convert("RGB")
|
||||
if im.size != (WIDTH, HEIGHT):
|
||||
im = im.resize((WIDTH, HEIGHT), Image.NEAREST)
|
||||
return egaGrid(im)
|
||||
|
||||
|
||||
def textCells(grid, cellW, cellH):
|
||||
# Each cell as (blank?, foreground, background): the background is
|
||||
# the cell's commonest colour.
|
||||
cells = []
|
||||
for cy in range(ROWS):
|
||||
row = []
|
||||
for cx in range(COLS):
|
||||
colours = Counter(grid[cy * cellH + y][cx * cellW + x] for y in range(cellH) for x in range(cellW))
|
||||
common = colours.most_common()
|
||||
if len(common) == 1:
|
||||
row.append((True, None, common[0][0]))
|
||||
else:
|
||||
row.append((False, common[1][0], common[0][0]))
|
||||
cells.append(row)
|
||||
return cells
|
||||
|
||||
|
||||
def compareTextMode(oraclePath, joeyPath):
|
||||
a = textCells(egaGrid(Image.open(oraclePath).convert("RGB")), TEXT_CELL_W, TEXT_CELL_H)
|
||||
b = textCells(load(joeyPath), CELL, CELL)
|
||||
cells = [(cx, cy) for cy in range(ROWS) for cx in range(COLS) if a[cy][cx] != b[cy][cx]]
|
||||
return 0, len(cells), cells
|
||||
|
||||
|
||||
def cellBits(g, cx, cy, bg):
|
||||
return tuple(tuple(1 if g[cy * CELL + y][cx * CELL + x] != bg else 0 for x in range(CELL)) for y in range(CELL))
|
||||
|
||||
|
||||
def cellColours(g, cx, cy):
|
||||
return Counter(g[cy * CELL + y][cx * CELL + x] for y in range(CELL) for x in range(CELL))
|
||||
|
||||
|
||||
def fontTemplates(path):
|
||||
g = load(path)
|
||||
t = {}
|
||||
for r, s in enumerate(FONT_ROWS):
|
||||
for i, ch in enumerate(s):
|
||||
t[cellBits(g, i, r, 0)] = ch
|
||||
return t
|
||||
|
||||
|
||||
def readCell(g, cx, cy, templates):
|
||||
# (character, foreground, background) or None when it is not text.
|
||||
colours = cellColours(g, cx, cy)
|
||||
if len(colours) > 2:
|
||||
return None
|
||||
if len(colours) == 1:
|
||||
return (" ", None, next(iter(colours)))
|
||||
(a, na), (b, nb) = colours.most_common()
|
||||
for bg, fg in ((a, b), (b, a)):
|
||||
ch = templates.get(cellBits(g, cx, cy, bg))
|
||||
if ch is not None:
|
||||
return (ch, fg, bg)
|
||||
return None
|
||||
|
||||
|
||||
def compare(a, b, fontA=None, fontB=None):
|
||||
real = 0
|
||||
text = 0
|
||||
cells = []
|
||||
for cy in range(ROWS):
|
||||
for cx in range(COLS):
|
||||
diff = sum(1 for y in range(cy * CELL, cy * CELL + CELL) for x in range(cx * CELL, cx * CELL + CELL)
|
||||
if a[y][x] != b[y][x])
|
||||
if diff == 0:
|
||||
continue
|
||||
if fontA and fontB:
|
||||
ta = readCell(a, cx, cy, fontA)
|
||||
tb = readCell(b, cx, cy, fontB)
|
||||
if ta and tb:
|
||||
if ta[0] == tb[0] and (ta[0] == " " or ta[1:] == tb[1:]) and ta[2] == tb[2]:
|
||||
continue
|
||||
text += 1
|
||||
cells.append((cx, cy, ta[0], tb[0]))
|
||||
continue
|
||||
real += diff
|
||||
cells.append((cx, cy))
|
||||
return real, text, cells
|
||||
|
||||
|
||||
def messageWindows(g):
|
||||
# Message windows: a red frame run of at least 16 pixels below white.
|
||||
found = []
|
||||
for y in range(1, HEIGHT - 1):
|
||||
x = 0
|
||||
while x < WIDTH:
|
||||
if g[y][x] == WINDOW_FRAME and g[y - 1][x] == WINDOW_BG:
|
||||
x2 = x
|
||||
while x2 < WIDTH and g[y][x2] == WINDOW_FRAME:
|
||||
x2 += 1
|
||||
if x2 - x >= WINDOW_MIN_RUN:
|
||||
found.append((x, y, x2 - 1))
|
||||
x = x2
|
||||
else:
|
||||
x += 1
|
||||
return found
|
||||
|
||||
|
||||
def diffImage(a, b, cells, path):
|
||||
marked = set((c[0], c[1]) for c in cells)
|
||||
out = Image.new("RGB", (WIDTH * 3, HEIGHT))
|
||||
for y in range(HEIGHT):
|
||||
for x in range(WIDTH):
|
||||
out.putpixel((x, y), EGA[a[y][x]])
|
||||
out.putpixel((WIDTH + x, y), EGA[b[y][x]])
|
||||
if a[y][x] != b[y][x] and (x // CELL, y // CELL) in marked:
|
||||
d = (255, 0, 0)
|
||||
else:
|
||||
d = tuple(v // 4 for v in EGA[a[y][x]])
|
||||
out.putpixel((2 * WIDTH + x, y), d)
|
||||
out.resize((WIDTH * 6, HEIGHT * 2), Image.NEAREST).save(path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = sys.argv[1:]
|
||||
fonts = (None, None)
|
||||
if "--fonts" in args:
|
||||
i = args.index("--fonts")
|
||||
fonts = (fontTemplates(args[i + 1]), fontTemplates(args[i + 2]))
|
||||
del args[i:i + 3]
|
||||
if len(args) < 2:
|
||||
sys.exit("usage: compareFrames.py ORACLE JOEY [DIFF.png] [--fonts ORACLEFONT JOEYFONT]")
|
||||
if isTextMode(args[0]):
|
||||
real, text, cells = compareTextMode(args[0], args[1])
|
||||
print("text mode: cells=%d %s" % (text, cells[:16]))
|
||||
else:
|
||||
a = load(args[0])
|
||||
b = load(args[1])
|
||||
real, text, cells = compare(a, b, fonts[0], fonts[1])
|
||||
print("real=%d text=%d %s" % (real, text, cells[:16]))
|
||||
if len(args) > 2:
|
||||
diffImage(a, b, cells, args[2])
|
||||
sys.exit(1 if real or text else 0)
|
||||
231
tests/tools/dosDrive.py
Executable file
231
tests/tools/dosDrive.py
Executable file
|
|
@ -0,0 +1,231 @@
|
|||
#!/usr/bin/env python3
|
||||
# dosDrive.py - run a DOS program in DOSBox Staging under a private Xvfb,
|
||||
# type at it from an input script and take raw 320x200 screenshots
|
||||
# (Ctrl+F5). scripts/oracle.sh drives Sierra's interpreter with it, using
|
||||
# the same scripts tests/hostPort.c plays.
|
||||
#
|
||||
# dosDrive.py HOSTDIR COMMAND OUTDIR SCRIPT [--set section.key=value ...]
|
||||
#
|
||||
# Script lines ('#' starts a comment, except in a type line):
|
||||
# wait SECONDS let SECONDS pass
|
||||
# key NAME [HOLD] press xdotool key NAME for HOLD seconds (0.12)
|
||||
# down NAME / up NAME hold / release a key
|
||||
# type TEXT press each character's key in turn
|
||||
# shot NAME save the screen as OUTDIR/NAME.png
|
||||
# clear press Return while a message window is showing
|
||||
# record NAME start recording every frame (70 Hz) ...
|
||||
# stop ... and stop, leaving OUTDIR/NAME.avi
|
||||
# A shot is Ctrl+F5, which the game also sees as a key press. Recording
|
||||
# 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
|
||||
# records can do nothing else.
|
||||
# Env: DOSBOX (default JoeyLib's toolchains/opt/dosbox/dosbox), JOEYLIB.
|
||||
import glob
|
||||
import os
|
||||
import ctypes
|
||||
import random
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import compareFrames
|
||||
|
||||
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"))
|
||||
CONF = os.path.join(JOEYLIB, "scripts/dosbox-386sx16.conf")
|
||||
KEY_HOLD = 0.12
|
||||
KEY_GAP = 0.08
|
||||
CLEAR_GAP = 0.6
|
||||
CLEAR_TRIES = 8
|
||||
SHOT_POLLS = 40
|
||||
VIDEO_KEY = "F12"
|
||||
VIDEO_KEY_SDL_SCANCODE = 69
|
||||
VIDEO_POLLS = 50
|
||||
VIDEO_POLL_SECONDS = 0.3
|
||||
SHOT_POLL_SECONDS = 0.1
|
||||
WINDOW_POLLS = 50
|
||||
# prctl option: a signal for the child when its parent dies.
|
||||
PR_SET_PDEATHSIG = 1
|
||||
KEYMAP = {" ": "space", ".": "period", ",": "comma", "'": "apostrophe", "-": "minus",
|
||||
"?": "question", "!": "exclam", "\n": "Return"}
|
||||
|
||||
|
||||
def dieWithParent():
|
||||
# Runs in each child before it starts: Xvfb and DOSBox die with the
|
||||
# driver, so a driver killed by timeout -s KILL leaves neither behind.
|
||||
ctypes.CDLL(None).prctl(PR_SET_PDEATHSIG, signal.SIGKILL)
|
||||
|
||||
|
||||
class Dos:
|
||||
def __init__(self, hostDir, command, outDir, extraSet=None, recording=False):
|
||||
self.out = outDir
|
||||
os.makedirs(outDir, exist_ok=True)
|
||||
self.work = os.path.join(outDir, ".work")
|
||||
shutil.rmtree(self.work, ignore_errors=True)
|
||||
os.makedirs(os.path.join(self.work, "cap"))
|
||||
os.makedirs(os.path.join(self.work, "xdg"))
|
||||
self.recording = None
|
||||
self.disp = ":%d" % random.randint(300, 799)
|
||||
self.xvfb = subprocess.Popen(["Xvfb", self.disp, "-screen", "0", "1024x768x24", "-nolisten", "tcp"],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, preexec_fn=dieWithParent)
|
||||
time.sleep(1)
|
||||
self.env = dict(os.environ, DISPLAY=self.disp, XDG_CONFIG_HOME=os.path.join(self.work, "xdg"),
|
||||
SDL_AUDIODRIVER="dummy")
|
||||
args = [DOSBOX, "--noprimaryconf", "--nolocalconf", "--conf", CONF,
|
||||
"--set", "capture_dir=" + os.path.join(self.work, "cap"),
|
||||
"--set", "default_image_capture_formats=raw"]
|
||||
if recording:
|
||||
mapper = os.path.join(self.work, "video.map")
|
||||
with open(mapper, "w") as f:
|
||||
f.write('hand_video "key %d"\n' % VIDEO_KEY_SDL_SCANCODE)
|
||||
args += ["--set", "mapperfile=" + mapper]
|
||||
for s in (extraSet or []):
|
||||
args += ["--set", s]
|
||||
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"),
|
||||
stderr=subprocess.STDOUT, preexec_fn=dieWithParent)
|
||||
self.win = None
|
||||
for _ in range(WINDOW_POLLS):
|
||||
time.sleep(0.2)
|
||||
self.win = self.findWin()
|
||||
if self.win:
|
||||
break
|
||||
if not self.win:
|
||||
self.stop()
|
||||
raise RuntimeError("no DOSBox window")
|
||||
self.t0 = time.time()
|
||||
|
||||
def findWin(self):
|
||||
r = subprocess.run(["xdotool", "search", "--name", "."], env=self.env, capture_output=True, text=True)
|
||||
for w in r.stdout.split():
|
||||
p = subprocess.run(["xdotool", "getwindowpid", w], env=self.env, capture_output=True, text=True)
|
||||
if p.stdout.strip() == str(self.proc.pid):
|
||||
return w
|
||||
return None
|
||||
|
||||
def xdo(self, *a):
|
||||
subprocess.run(["xdotool"] + list(a), env=self.env, capture_output=True)
|
||||
|
||||
def key(self, name, hold=KEY_HOLD):
|
||||
self.xdo("keydown", "--window", self.win, name)
|
||||
time.sleep(hold)
|
||||
self.xdo("keyup", "--window", self.win, name)
|
||||
time.sleep(KEY_GAP)
|
||||
|
||||
def typeText(self, text):
|
||||
for ch in text:
|
||||
if ch.isupper():
|
||||
self.xdo("keydown", "--window", self.win, "shift")
|
||||
self.key(ch.lower())
|
||||
self.xdo("keyup", "--window", self.win, "shift")
|
||||
else:
|
||||
self.key(KEYMAP.get(ch, ch))
|
||||
|
||||
def shot(self, name):
|
||||
before = set(glob.glob(os.path.join(self.work, "cap", "*.png")))
|
||||
self.xdo("key", "--window", self.win, "ctrl+F5")
|
||||
for _ in range(SHOT_POLLS):
|
||||
time.sleep(SHOT_POLL_SECONDS)
|
||||
now = set(glob.glob(os.path.join(self.work, "cap", "*.png"))) - before
|
||||
if now:
|
||||
time.sleep(0.2)
|
||||
shutil.move(now.pop(), os.path.join(self.out, name + ".png"))
|
||||
return True
|
||||
print("shot %s: no capture" % name, file=sys.stderr)
|
||||
return False
|
||||
|
||||
def record(self, name):
|
||||
self.recording = name
|
||||
self.xdo("key", "--window", self.win, VIDEO_KEY)
|
||||
|
||||
def stopRecording(self):
|
||||
# The file is finished when it stops growing.
|
||||
self.xdo("key", "--window", self.win, VIDEO_KEY)
|
||||
last = -1
|
||||
for _ in range(VIDEO_POLLS):
|
||||
time.sleep(VIDEO_POLL_SECONDS)
|
||||
videos = glob.glob(os.path.join(self.work, "cap", "*.avi"))
|
||||
if not videos:
|
||||
continue
|
||||
size = os.path.getsize(videos[0])
|
||||
if size == last:
|
||||
shutil.move(videos[0], os.path.join(self.out, self.recording + ".avi"))
|
||||
self.recording = None
|
||||
return True
|
||||
last = size
|
||||
print("record %s: no video" % self.recording, file=sys.stderr)
|
||||
return False
|
||||
|
||||
def clearWindows(self):
|
||||
for _ in range(CLEAR_TRIES):
|
||||
path = os.path.join(self.out, "_clear.png")
|
||||
self.shot("_clear")
|
||||
showing = os.path.exists(path) and compareFrames.messageWindows(compareFrames.load(path))
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
if not showing:
|
||||
return
|
||||
self.key("Return")
|
||||
time.sleep(CLEAR_GAP)
|
||||
|
||||
def stop(self):
|
||||
try:
|
||||
self.proc.kill()
|
||||
except OSError:
|
||||
pass
|
||||
self.proc.wait()
|
||||
self.xvfb.kill()
|
||||
self.xvfb.wait()
|
||||
shutil.rmtree(self.work, ignore_errors=True)
|
||||
|
||||
def run(self, script):
|
||||
for raw in script.splitlines():
|
||||
line = raw.strip() if raw.strip().startswith("type ") else raw.split("#", 1)[0].strip()
|
||||
if not line:
|
||||
continue
|
||||
op, _, arg = line.partition(" ")
|
||||
if op == "wait":
|
||||
time.sleep(float(arg))
|
||||
elif op == "key":
|
||||
parts = arg.split()
|
||||
self.key(parts[0], float(parts[1]) if len(parts) > 1 else KEY_HOLD)
|
||||
elif op == "type":
|
||||
self.typeText(arg)
|
||||
elif op == "shot":
|
||||
self.shot(arg)
|
||||
elif op == "clear":
|
||||
self.clearWindows()
|
||||
elif op == "record":
|
||||
self.record(arg)
|
||||
elif op == "stop":
|
||||
self.stopRecording()
|
||||
elif op == "down":
|
||||
self.xdo("keydown", "--window", self.win, arg)
|
||||
elif op == "up":
|
||||
self.xdo("keyup", "--window", self.win, arg)
|
||||
else:
|
||||
raise ValueError("bad script op " + op)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 5:
|
||||
sys.exit("usage: dosDrive.py HOSTDIR COMMAND OUTDIR SCRIPT [--set section.key=value ...]")
|
||||
hostDir, command, outDir, scriptFile = sys.argv[1:5]
|
||||
sets = []
|
||||
rest = sys.argv[5:]
|
||||
while rest:
|
||||
if rest[0] == "--set" and len(rest) > 1:
|
||||
sets.append(rest[1])
|
||||
rest = rest[2:]
|
||||
else:
|
||||
rest = rest[1:]
|
||||
script = open(scriptFile).read()
|
||||
recording = any(line.split()[:1] == ["record"] for line in script.splitlines())
|
||||
dos = Dos(hostDir, command, outDir, sets, recording)
|
||||
try:
|
||||
dos.run(script)
|
||||
finally:
|
||||
dos.stop()
|
||||
140
tests/tools/shakeMeasure.py
Normal file
140
tests/tools/shakeMeasure.py
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
#!/usr/bin/env python3
|
||||
# shakeMeasure.py - read shake.screen's effect out of a recording: Sierra's
|
||||
# (a DOSBox AVI, every 70 Hz frame) or JoeyAGI's (agiHost's NAME.frames
|
||||
# list with a NAME-HASH.ppm per different frame).
|
||||
#
|
||||
# Each frame is compared with the last frame that was in place: the same
|
||||
# picture moved by (dx, dy) is a shaken frame, anything else a new
|
||||
# in-place frame. Consecutive shaken frames make a beat; beats less than
|
||||
# GAP_MAX frames apart make one shake.
|
||||
#
|
||||
# shakeMeasure.py RECORDING [OTHER]
|
||||
# prints one line per shake (beats, frames moved / in place per beat, the
|
||||
# offset); with OTHER it compares the two and exits 1 if they differ.
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from PIL import Image
|
||||
|
||||
WIDTH = 320
|
||||
HEIGHT = 200
|
||||
SEARCH_X = 16
|
||||
SEARCH_Y = 8
|
||||
SAMPLE = 3
|
||||
GAP_MAX = 20
|
||||
|
||||
|
||||
def framesFromAvi(path):
|
||||
proc = subprocess.Popen(["ffmpeg", "-loglevel", "error", "-i", path, "-f", "rawvideo",
|
||||
"-pix_fmt", "rgb24", "-"], stdout=subprocess.PIPE)
|
||||
size = WIDTH * HEIGHT * 3
|
||||
while True:
|
||||
buf = proc.stdout.read(size)
|
||||
if len(buf) < size:
|
||||
break
|
||||
yield buf
|
||||
proc.wait()
|
||||
|
||||
|
||||
def framesFromHost(path):
|
||||
base = path[:-len(".frames")]
|
||||
cache = {}
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
_, h = line.split()
|
||||
if h not in cache:
|
||||
cache[h] = Image.open("%s-%s.ppm" % (base, h)).convert("RGB").tobytes()
|
||||
yield cache[h]
|
||||
|
||||
|
||||
def pixel(buf, x, y):
|
||||
i = (y * WIDTH + x) * 3
|
||||
return buf[i:i + 3]
|
||||
|
||||
|
||||
def offset(frame, ref):
|
||||
# The (dx, dy) that maps ref onto frame exactly, or None.
|
||||
for dy in range(-SEARCH_Y, SEARCH_Y + 1):
|
||||
for dx in range(-SEARCH_X, SEARCH_X + 1):
|
||||
ok = True
|
||||
for y in range(max(0, dy), min(HEIGHT, HEIGHT + dy), SAMPLE):
|
||||
for x in range(max(0, dx), min(WIDTH, WIDTH + dx), SAMPLE):
|
||||
if pixel(frame, x, y) != pixel(ref, x - dx, y - dy):
|
||||
ok = False
|
||||
break
|
||||
if not ok:
|
||||
break
|
||||
if ok:
|
||||
return (dx, dy)
|
||||
return None
|
||||
|
||||
|
||||
def measure(frames):
|
||||
# A list of (moved?, offset) per frame.
|
||||
ref = None
|
||||
known = {}
|
||||
marks = []
|
||||
for frame in frames:
|
||||
if ref is None or frame == ref:
|
||||
ref = frame
|
||||
marks.append(None)
|
||||
continue
|
||||
key = hash(frame)
|
||||
if key not in known:
|
||||
known[key] = offset(frame, ref)
|
||||
o = known[key]
|
||||
if o is None or o == (0, 0):
|
||||
ref = frame
|
||||
known.clear()
|
||||
marks.append(None)
|
||||
else:
|
||||
marks.append(o)
|
||||
return marks
|
||||
|
||||
|
||||
def shakes(marks):
|
||||
beats = []
|
||||
i = 0
|
||||
while i < len(marks):
|
||||
if marks[i] is None:
|
||||
i += 1
|
||||
continue
|
||||
start = i
|
||||
while i < len(marks) and marks[i] is not None:
|
||||
i += 1
|
||||
beats.append((start, i - start, marks[start]))
|
||||
groups = []
|
||||
for b in beats:
|
||||
if groups and b[0] - (groups[-1][-1][0] + groups[-1][-1][1]) <= GAP_MAX:
|
||||
groups[-1].append(b)
|
||||
else:
|
||||
groups.append([b])
|
||||
lines = []
|
||||
for g in groups:
|
||||
moved = sorted(set(b[1] for b in g))
|
||||
still = sorted(set(g[k + 1][0] - g[k][0] - g[k][1] for k in range(len(g) - 1)))
|
||||
offsets = sorted(set(b[2] for b in g))
|
||||
lines.append("%d beats, moved %s frames, in place %s frames, offset %s" % (len(g), moved, still, offsets))
|
||||
return lines
|
||||
|
||||
|
||||
def load(path):
|
||||
if path.endswith(".frames"):
|
||||
return framesFromHost(path)
|
||||
return framesFromAvi(path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
sys.exit("usage: shakeMeasure.py RECORDING [OTHER]")
|
||||
first = shakes(measure(load(sys.argv[1])))
|
||||
print("%s:" % os.path.basename(sys.argv[1]))
|
||||
for line in first:
|
||||
print(" " + line)
|
||||
if len(sys.argv) > 2:
|
||||
second = shakes(measure(load(sys.argv[2])))
|
||||
print("%s:" % os.path.basename(sys.argv[2]))
|
||||
for line in second:
|
||||
print(" " + line)
|
||||
sys.exit(0 if first == second else 1)
|
||||
Loading…
Add table
Reference in a new issue