Optimized a bit better. Examples are a mess of broken crap.
This commit is contained in:
commit
2d447f3d95
17 changed files with 8838 additions and 0 deletions
218
AGI-SESSION-HANDOFF.md
Normal file
218
AGI-SESSION-HANDOFF.md
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
# AGI port — session handoff
|
||||||
|
|
||||||
|
This document captures the state of the AGI interpreter port as of
|
||||||
|
2026-05-15. Hand it to a fresh Claude Code session to pick up where
|
||||||
|
the previous one left off.
|
||||||
|
|
||||||
|
## Project context
|
||||||
|
|
||||||
|
JoeyLib is a unified C game-dev library targeting Apple IIgs (perf
|
||||||
|
floor / reference), Amiga, Atari ST, MS-DOS. The AGI port is a
|
||||||
|
from-scratch reimplementation of Sierra's Adventure Game Interpreter
|
||||||
|
v2 on top of JoeyLib's surface / draw / input APIs. Written from
|
||||||
|
public AGI format specs only -- NOT derived from ScummVM / NAGI /
|
||||||
|
Sarien (those are GPL'd; JoeyLib is not).
|
||||||
|
|
||||||
|
Test data: KQ3 v2, locally installed at
|
||||||
|
`examples/agi/gamedata/kq3/`. The user owns the game and is not
|
||||||
|
distributing it; gamedata is gitignored.
|
||||||
|
|
||||||
|
## Current phase
|
||||||
|
|
||||||
|
**Phase 1 sub-D** (callback-driven rendering): verified working.
|
||||||
|
Logic.0 + room 45 init paint KQ3's title screen on every supported
|
||||||
|
port. Ego sprite (VIEW 0, Graham) draws on top with priority masking,
|
||||||
|
WASD/arrow keys move him, ESC quits.
|
||||||
|
|
||||||
|
What's next: **Phase 1 sub-E** (input + animation opcodes). The
|
||||||
|
title screen does not auto-advance because the VM still stubs ~150
|
||||||
|
opcodes including `wait`, `set.menu`, `accept.input`, animation
|
||||||
|
timers, etc. Real KQ3 progression past the title requires implementing
|
||||||
|
enough of those to let logic.45 complete its intro and call new.room.
|
||||||
|
|
||||||
|
Phase 2 sub-A onward: parser, NPC actors, sound (NTP / PSG).
|
||||||
|
|
||||||
|
## What works (verified)
|
||||||
|
|
||||||
|
- Resource loader (LOGDIR / PICDIR / VIEWDIR / SNDDIR / VOL.x)
|
||||||
|
- PIC decoder (visual + priority planes, flood fill, vector draw)
|
||||||
|
- VIEW decoder (cel parse + RLE blit + priority masking + mirror)
|
||||||
|
- LOGIC VM: 256 vars, 256 flags, 8-deep call stack
|
||||||
|
- Real handlers: arithmetic (0x00-0x11), OP_IF (0xFF), OP_GOTO
|
||||||
|
(0xFE), OP_NEW_ROOM (0x12/0x13), OP_LOAD_LOGIC (0x14/0x15),
|
||||||
|
OP_CALL (0x16/0x17), OP_LOAD_PIC (0x18), OP_DRAW_PIC (0x19),
|
||||||
|
OP_SHOW_PIC (0x1A), OP_DISCARD_PIC (0x1B), OP_OVERLAY_PIC (0x1C)
|
||||||
|
- All ~180 other action opcodes are stubs (skip their arg bytes
|
||||||
|
via `kActionArgBytes[256]` table)
|
||||||
|
- Host callbacks: fetchLogic / loadPic / drawPic / showPic /
|
||||||
|
discardPic / overlayPic
|
||||||
|
- Per-platform save-under for the ego sprite (no full-stage memcpy
|
||||||
|
per frame; IIgs uses MVN-based codegen)
|
||||||
|
- Cross-platform binary works on all 4 ports (`AGI.EXE`,
|
||||||
|
`AGI.PRG`, `Agi`, IIgs `AGI`)
|
||||||
|
|
||||||
|
## What does NOT work
|
||||||
|
|
||||||
|
- **Title screen never advances.** Stubs the input / timer opcodes
|
||||||
|
the title uses. Workaround: starting-room override (argv[2]).
|
||||||
|
- **No NPC actors.** animate.obj / set.cel / set.loop are stubs.
|
||||||
|
- **No parser.** said() and the word-tokenize opcodes are stubs.
|
||||||
|
- **No sound.** Phase 3 work.
|
||||||
|
- **IIgs run path for AGI** is not wired. `scripts/run-agi.sh iigs`
|
||||||
|
prints an explicit "not yet wired" message describing the missing
|
||||||
|
disk-packager work (need bigger 2mg volume + case-folded ProDOS
|
||||||
|
names + game-data argument to `package-disk.sh`).
|
||||||
|
- **ST / Amiga can't pass argv.** Hatari `--auto` and Amiga
|
||||||
|
startup-sequence don't forward args, so the starting-room override
|
||||||
|
only works on DOS. ST / Amiga AGI starts at the title screen.
|
||||||
|
Fix needs either a config-file stage (`ROOM.TXT` read at startup)
|
||||||
|
or enough title opcodes for the title to advance naturally.
|
||||||
|
|
||||||
|
## How to test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source toolchains/env.sh
|
||||||
|
|
||||||
|
# Host-side parsers + VM (fast iteration, no emulator)
|
||||||
|
./scripts/test-agi.sh
|
||||||
|
|
||||||
|
# Cross-platform binary in DOSBox
|
||||||
|
./scripts/run-agi.sh dos # title screen
|
||||||
|
./scripts/run-agi.sh dos kq3 1 # jump to room 1
|
||||||
|
./scripts/run-agi.sh dos kq3 3 # jump to room 3
|
||||||
|
|
||||||
|
# Other platforms (no argv support yet, always title)
|
||||||
|
./scripts/run-agi.sh atarist
|
||||||
|
./scripts/run-agi.sh amiga
|
||||||
|
./scripts/run-agi.sh iigs # prints "not wired" message
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files touched (this session)
|
||||||
|
|
||||||
|
New:
|
||||||
|
|
||||||
|
- `src/core/random.c` — xorshift32 PRNG (jlRandom / jlRandomRange / jlRandomSeed)
|
||||||
|
- `examples/agi/*.c` — agi.c, agiRes.c, agiPic.c, agiView.c, agiVm.c
|
||||||
|
- `examples/agi/agi.h` — all AGI types + public API
|
||||||
|
- `tests/agi/*.c` — testAgiRes / testAgiPic / testAgiView / testAgiVm / testAgiPipeline
|
||||||
|
- `scripts/test-agi.sh` — host-build runner for all five tests
|
||||||
|
- `scripts/run-agi.sh` — cross-platform emulator launcher (dos | amiga | atarist | iigs)
|
||||||
|
|
||||||
|
Modified:
|
||||||
|
|
||||||
|
- `include/joey/core.h` — random prototypes
|
||||||
|
- `include/joey/platform.h` — auto-detect from compiler defines
|
||||||
|
- `.gitignore` — examples/agi/gamedata/
|
||||||
|
- All four make/*.mk — added agi to EXAMPLES list
|
||||||
|
|
||||||
|
## Architectural decisions worth remembering
|
||||||
|
|
||||||
|
### Render loop: save-under, not full stage copy
|
||||||
|
|
||||||
|
The naive per-frame `jlSurfaceCopy(stage, gBackdrop)` is 32 KB of
|
||||||
|
memcpy + present overhead -- not acceptable on a 2.8 MHz IIgs. The
|
||||||
|
current code:
|
||||||
|
|
||||||
|
1. `cbShowPic` paints the picture onto the stage ONCE and snapshots
|
||||||
|
it as `gBackdrop`. Sets `gPicReady = true` and invalidates any
|
||||||
|
prior ego save-under.
|
||||||
|
2. Per frame: `jlSpriteRestoreUnder` the last-frame ego rect (puts
|
||||||
|
clean backdrop bytes back), `jlSpriteSaveUnder` the new ego rect,
|
||||||
|
then `agiViewDraw` paints the cel with priority masking.
|
||||||
|
3. Placeholder sprite for save-under is 4x5 tiles = 32x40 stage
|
||||||
|
pixels, sized to cover any AGI v2 ego cel. Compiled via
|
||||||
|
`jlSpriteCompile` so save/restore use the platform's compiled
|
||||||
|
fast path (MVN block-copy on IIgs, etc.) instead of the C
|
||||||
|
interpreter fallback.
|
||||||
|
|
||||||
|
Per-frame memory traffic: ~1.3 KB save+restore vs ~32 KB full copy.
|
||||||
|
|
||||||
|
### VM throttling
|
||||||
|
|
||||||
|
AGI's native cycle is ~6 Hz. Running logic.0 every frame (60 Hz) is
|
||||||
|
6800-byte switch dispatcher × 60 = ~400k opcodes/sec for nothing.
|
||||||
|
`agi.c` now runs runVmCycle once every 10 frames (~6 Hz on 60 Hz
|
||||||
|
refresh, ~5 Hz on 50 Hz PAL). Input polling, ego movement, render
|
||||||
|
still run at full refresh -- only the bytecode interp is throttled.
|
||||||
|
|
||||||
|
Startup pumps the VM until `gPicReady` (or 32 cycles) so the user
|
||||||
|
never sees a flash of COLOR_MISSING before the first room paints.
|
||||||
|
|
||||||
|
### VIEW mirror handling
|
||||||
|
|
||||||
|
Sierra's encoder writes `mirror=1, src=N` on BOTH loops in a mirror
|
||||||
|
pair, where N is the lower-numbered loop. `agiView.c:resolveMirror`
|
||||||
|
must only treat a cel as mirrored when `src != current_loop`. KQ3
|
||||||
|
view 0 hit this directly: both loop 0 (right) and loop 1 (left) had
|
||||||
|
`mirror=1, src=0`, and our original code flipped both -- so pressing
|
||||||
|
left and right both faced left. Fixed.
|
||||||
|
|
||||||
|
### Starting-room override
|
||||||
|
|
||||||
|
`agi.c` argv[2] = starting room number 1..255. When set,
|
||||||
|
`runVmCycle`'s NEW_ROOM handler substitutes the override for
|
||||||
|
`vm.newRoomId` on the FIRST NEW_ROOM halt only (one-shot). Lets the
|
||||||
|
user skip past KQ3's title room (45) into the actual game. Once the
|
||||||
|
override fires, subsequent transitions go through unchanged.
|
||||||
|
|
||||||
|
`scripts/run-agi.sh` accepts `<platform> [game] [room]` and forwards
|
||||||
|
the room to DOS via dosbox `-c "AGI.EXE . <room>"`. ST and Amiga
|
||||||
|
can't pass argv via autostart so their AGI starts at the title.
|
||||||
|
|
||||||
|
### Default game dir is "." not "agidemo"
|
||||||
|
|
||||||
|
`resolveGameDir` defaults to "." (CWD). On platforms whose autostart
|
||||||
|
can't pass argv (Hatari, AmigaDOS), `run-agi.sh` stages the AGI
|
||||||
|
binary plus the v2 game data files (LOGDIR / PICDIR / etc.) flat in
|
||||||
|
a temp directory and points the emulator at it; AGI runs from CWD =
|
||||||
|
that temp dir and finds the data files relative to "./".
|
||||||
|
|
||||||
|
## Likely next-step work
|
||||||
|
|
||||||
|
The user's natural next ask is "implement enough title opcodes to
|
||||||
|
let KQ3's title screen auto-advance." That probably means:
|
||||||
|
|
||||||
|
- `wait` (cycle.time gate)
|
||||||
|
- `input.line` / `accept.input`
|
||||||
|
- Animation timers: `cycle.time`, `current.loop`, etc.
|
||||||
|
- `set.menu` / `disable.item` (KQ3 title shows a menu)
|
||||||
|
|
||||||
|
Alternative path: skip the title work and implement enough core
|
||||||
|
opcodes to make a gameplay room functional. animate.obj,
|
||||||
|
set.view.v, set.cel, set.loop, position, draw, erase. That gets us
|
||||||
|
walking around KQ3 room 1 (Manannan's house) with Graham instead
|
||||||
|
of a static title.
|
||||||
|
|
||||||
|
The user should pick which to prioritize.
|
||||||
|
|
||||||
|
## Memory entries you should know about
|
||||||
|
|
||||||
|
The repo's auto-memory at
|
||||||
|
`~/.claude/projects/-home-scott-claude-joeylib/memory/` includes
|
||||||
|
JoeyLib-wide context (CMake quirks, ORCA-C gotchas, perf
|
||||||
|
constraints) plus `project_joeylib_agi_plan.md` which describes the
|
||||||
|
overall AGI port plan. All persist into the next session
|
||||||
|
automatically; no need to reload.
|
||||||
|
|
||||||
|
Specifically relevant to the AGI port:
|
||||||
|
|
||||||
|
- `project_joeylib_agi_plan.md` — overall plan, 3-4 month estimate
|
||||||
|
- `feedback_orca_c_atoi_segment.md` — don't use atoi in AGI code
|
||||||
|
(it pushes the IIgs ORCA-C stdio cluster past 64 KB; we use a
|
||||||
|
manual parseArgU8 instead)
|
||||||
|
- `project_perf_directive.md` — IIgs is the perf floor; every
|
||||||
|
other target must match or beat it
|
||||||
|
- `feedback_iigs_speed.md` — IIgs must be absolute fastest
|
||||||
|
technically possible; the save-under decision was made under
|
||||||
|
this constraint.
|
||||||
|
|
||||||
|
## Git / user preferences
|
||||||
|
|
||||||
|
- The user manages git themselves -- do NOT commit / push / branch
|
||||||
|
on JoeyLib without explicit ask (see memory
|
||||||
|
`feedback_git.md`).
|
||||||
|
- Always `source toolchains/env.sh` before any `make` -- the user
|
||||||
|
has added that to .claude/settings.local.json allow list.
|
||||||
|
- DJGPP / ORCA / vasm / m68k-atari-mint paths come from env.sh.
|
||||||
|
- The user does NOT need hand-holding -- they're an expert C dev.
|
||||||
|
Skip excessive explanation; describe what was done and move on.
|
||||||
562
agi.h
Normal file
562
agi.h
Normal file
|
|
@ -0,0 +1,562 @@
|
||||||
|
// JoeyLib AGI interpreter (Sierra Adventure Game Interpreter, v2).
|
||||||
|
//
|
||||||
|
// Reimplements the engine that ran Sierra's first wave of graphic
|
||||||
|
// adventures (KQ1-3, SQ1-2, PQ1, LSL1, Manhunter 1-2) on top of
|
||||||
|
// JoeyLib's surface/draw/input APIs. v2 format only for Phase 0; v3
|
||||||
|
// (LZW-compressed) lands in a later phase.
|
||||||
|
//
|
||||||
|
// This is a from-scratch implementation written against the public
|
||||||
|
// AGI format specification, not derived from any GPL'd reference.
|
||||||
|
|
||||||
|
#ifndef AGI_H
|
||||||
|
#define AGI_H
|
||||||
|
|
||||||
|
#include "joey/types.h"
|
||||||
|
#include "joey/surface.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
|
||||||
|
// AGI resource directories are sized by the on-disk file length. The
|
||||||
|
// maximum index any single game references is well under 256 for the
|
||||||
|
// supported game set; oversize directories are rejected at load.
|
||||||
|
#define AGI_MAX_RESOURCES 256
|
||||||
|
|
||||||
|
// AGI VOL.x files are numbered 0..15 (4-bit field in the directory
|
||||||
|
// entry). One open FILE * per volume kept resident for the life of
|
||||||
|
// the game session.
|
||||||
|
#define AGI_MAX_VOLUMES 16
|
||||||
|
|
||||||
|
// AGI's native rendering surface. Visual and priority screens are
|
||||||
|
// each 160 wide; horizontal pixel doubling produces the 320-wide
|
||||||
|
// display. The vertical 168 leaves room above and below for the
|
||||||
|
// status line and command prompt in a 200-line display.
|
||||||
|
#define AGI_PIC_WIDTH 160
|
||||||
|
#define AGI_PIC_HEIGHT 168
|
||||||
|
#define AGI_PIC_PIXELS (AGI_PIC_WIDTH * AGI_PIC_HEIGHT)
|
||||||
|
|
||||||
|
// Flood-fill spread tests: a fill spreads through pixels matching the
|
||||||
|
// background color and stops at anything else. Visual default bg is
|
||||||
|
// 15 (white), priority default bg is 4 (red).
|
||||||
|
#define AGI_VISUAL_BG 15u
|
||||||
|
#define AGI_PRIORITY_BG 4u
|
||||||
|
|
||||||
|
// AGI 16-color CGA-derived palette in JoeyLib's $0RGB format (4-bit
|
||||||
|
// per channel). Entry 6 keeps the classic CGA "brown" tweak (R=A,
|
||||||
|
// G=5, B=0) so the muddy-yellow-on-CGA-monitors look matches the
|
||||||
|
// original render rather than reading as pure yellow-green.
|
||||||
|
extern const uint16_t kAgiPalette[16];
|
||||||
|
|
||||||
|
// Resource directory entry: which VOL.x file holds the resource and
|
||||||
|
// where in that file it starts. A 0xFF marker in volume means the
|
||||||
|
// directory slot is empty (game does not define this resource ID).
|
||||||
|
typedef struct {
|
||||||
|
uint8_t volume;
|
||||||
|
uint32_t offset;
|
||||||
|
} AgiResEntryT;
|
||||||
|
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
AGI_RES_LOGIC = 0,
|
||||||
|
AGI_RES_PIC = 1,
|
||||||
|
AGI_RES_VIEW = 2,
|
||||||
|
AGI_RES_SOUND = 3,
|
||||||
|
AGI_RES_COUNT = 4
|
||||||
|
} AgiResTypeE;
|
||||||
|
|
||||||
|
|
||||||
|
// Open-game state: directory metadata + handles to every VOL.x file.
|
||||||
|
// Loading and freeing individual resource blobs is per-call so the
|
||||||
|
// working-set footprint stays small enough for stock IIgs / Amiga.
|
||||||
|
typedef struct {
|
||||||
|
FILE *volFiles[AGI_MAX_VOLUMES];
|
||||||
|
uint16_t resCount[AGI_RES_COUNT];
|
||||||
|
AgiResEntryT resDir[AGI_RES_COUNT][AGI_MAX_RESOURCES];
|
||||||
|
} AgiGameT;
|
||||||
|
|
||||||
|
|
||||||
|
// Decoded PIC working buffers. Both planes are 160x168 chunky 8bpp;
|
||||||
|
// only the low nibble (4 bits) of each byte carries actual color
|
||||||
|
// information. Buffers are heap-allocated so the binary's BSS
|
||||||
|
// footprint stays under what the IIgs stock heap can satisfy in
|
||||||
|
// one shot.
|
||||||
|
typedef struct {
|
||||||
|
uint8_t *visual;
|
||||||
|
uint8_t *priority;
|
||||||
|
} AgiPicT;
|
||||||
|
|
||||||
|
|
||||||
|
// VIEW resources hold one or more animation loops; each loop is a
|
||||||
|
// sequence of cels (frames). The parser walks the published format
|
||||||
|
// once and records pointers into the raw resource buffer so per-cel
|
||||||
|
// access is O(1) without re-parsing.
|
||||||
|
#define AGI_MAX_LOOPS_PER_VIEW 16u
|
||||||
|
#define AGI_MAX_CELS_PER_LOOP 32u
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t width;
|
||||||
|
uint8_t height;
|
||||||
|
uint8_t transparentColor;
|
||||||
|
uint8_t mirrored;
|
||||||
|
uint8_t mirrorSourceLoop;
|
||||||
|
const uint8_t *rleData;
|
||||||
|
} AgiCelInfoT;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t celCount;
|
||||||
|
AgiCelInfoT cels[AGI_MAX_CELS_PER_LOOP];
|
||||||
|
} AgiLoopInfoT;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t *raw;
|
||||||
|
uint16_t rawLength;
|
||||||
|
uint8_t loopCount;
|
||||||
|
AgiLoopInfoT loops[AGI_MAX_LOOPS_PER_VIEW];
|
||||||
|
} AgiViewT;
|
||||||
|
|
||||||
|
|
||||||
|
// AGI priority for an actor based on its feet (bottom-edge) Y.
|
||||||
|
// 0..47 is the sky band (priority 4); 48..167 is ground divided into
|
||||||
|
// 10 bands at priority 5..14 (12 pixels each). Priority 15 is "always
|
||||||
|
// on top" and isn't reached by the actor's natural Y.
|
||||||
|
uint8_t agiActorPriorityForY(int16_t y);
|
||||||
|
|
||||||
|
|
||||||
|
// LOGIC VM state. AGI's interpreter is a stack-of-logics bytecode
|
||||||
|
// VM with 256 byte-sized variables and 256 single-bit flags.
|
||||||
|
#define AGI_VM_NUM_VARS 256u
|
||||||
|
#define AGI_VM_NUM_FLAGS 256u
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
AGI_VM_HALT_NONE = 0,
|
||||||
|
AGI_VM_HALT_RETURN = 1,
|
||||||
|
AGI_VM_HALT_UNKNOWN_OP = 2,
|
||||||
|
AGI_VM_HALT_TRUNCATED = 3,
|
||||||
|
AGI_VM_HALT_NEW_ROOM = 4,
|
||||||
|
AGI_VM_HALT_NO_LOGIC = 5,
|
||||||
|
AGI_VM_HALT_STACK_OVER = 6,
|
||||||
|
AGI_VM_HALT_PRINT_PENDING = 7, // print() popup waiting for ack
|
||||||
|
AGI_VM_HALT_QUIT = 8 // game logic asked to exit
|
||||||
|
} AgiVmHaltE;
|
||||||
|
|
||||||
|
#define AGI_VM_CALL_STACK_MAX 8u
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const uint8_t *code;
|
||||||
|
uint16_t codeLength;
|
||||||
|
uint16_t pc;
|
||||||
|
uint8_t logicId;
|
||||||
|
} AgiVmFrameT;
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Animated objects -----
|
||||||
|
//
|
||||||
|
// AGI v2 supports up to 16 simultaneous animated objects ("screen
|
||||||
|
// objects"). Object 0 is the player/ego; the rest are NPCs, props,
|
||||||
|
// and incidental animations driven by the game logic. The state
|
||||||
|
// here mirrors the per-object fields the game scripts manipulate
|
||||||
|
// via animate.obj / set.view / set.cel / position / set.dir / etc.
|
||||||
|
#define AGI_MAX_OBJECTS 16u
|
||||||
|
|
||||||
|
#define AGI_OBJ_FLAG_ANIMATED 0x01u // animate.obj called
|
||||||
|
#define AGI_OBJ_FLAG_DRAWN 0x02u // draw() called, sprite is on-screen
|
||||||
|
#define AGI_OBJ_FLAG_CYCLING 0x04u // start.cycling, advances cel
|
||||||
|
#define AGI_OBJ_FLAG_UPDATING 0x08u // start.update, render allowed
|
||||||
|
#define AGI_OBJ_FLAG_LOOP_FIXED 0x10u // fix.loop disables auto-loop choice
|
||||||
|
#define AGI_OBJ_FLAG_PRI_FIXED 0x20u // set.priority overrides Y-band priority
|
||||||
|
#define AGI_OBJ_FLAG_HORIZON_IGN 0x40u // ignore.horizon
|
||||||
|
#define AGI_OBJ_FLAG_OBJS_IGN 0x80u // ignore.objs (collision)
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
AGI_CYCLE_NORMAL = 0, // 0->1->2..->last->0->...
|
||||||
|
AGI_CYCLE_END_LOOP = 1, // play once forward, stop, set flag
|
||||||
|
AGI_CYCLE_REVERSE = 2, // last->..->1->0->last->...
|
||||||
|
AGI_CYCLE_REV_LOOP = 3 // play once backward, stop, set flag
|
||||||
|
} AgiCycleE;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
AGI_MOTION_NORMAL = 0, // moves on direction (vN+6 for ego)
|
||||||
|
AGI_MOTION_WANDER = 1, // random direction every few cycles
|
||||||
|
AGI_MOTION_FOLLOW_EGO = 2, // chases obj 0
|
||||||
|
AGI_MOTION_MOVE_OBJ = 3 // moving toward fixed (x,y)
|
||||||
|
} AgiMotionE;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t viewId;
|
||||||
|
uint8_t loop;
|
||||||
|
uint8_t cel;
|
||||||
|
uint8_t priority; // 4..15; high nibble masking
|
||||||
|
int16_t x; // baseline (bottom-left) AGI x
|
||||||
|
int16_t y; // baseline (bottom) AGI y
|
||||||
|
int16_t prevX;
|
||||||
|
int16_t prevY;
|
||||||
|
uint8_t prevLoop;
|
||||||
|
uint8_t prevCel;
|
||||||
|
uint8_t prevView;
|
||||||
|
uint8_t direction; // 0=stop, 1..8 (up=1, clockwise)
|
||||||
|
uint8_t stepSize; // pixels per step
|
||||||
|
uint8_t stepTime; // VM cycles between steps
|
||||||
|
uint8_t stepTick; // counter toward stepTime
|
||||||
|
uint8_t cycleTime; // VM cycles between cel advances
|
||||||
|
uint8_t cycleTick; // counter toward cycleTime
|
||||||
|
AgiCycleE cycleMode;
|
||||||
|
uint8_t endOfLoopFlag; // flag id to set when end.of.loop fires
|
||||||
|
AgiMotionE motionType;
|
||||||
|
int16_t targetX; // for move.obj
|
||||||
|
int16_t targetY;
|
||||||
|
uint8_t moveStepBackup; // step size before move.obj started
|
||||||
|
uint8_t moveDoneFlag; // flag id set on move.obj completion
|
||||||
|
uint8_t wanderTick; // wander direction-change countdown
|
||||||
|
uint8_t followStep; // step.size override for follow.ego
|
||||||
|
uint8_t followDoneFlag;
|
||||||
|
uint8_t flags; // AGI_OBJ_FLAG_*
|
||||||
|
} AgiObjectT;
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Text plane / status line -----
|
||||||
|
//
|
||||||
|
// AGI's text plane is 40 columns x 25 rows of 8x8 character cells.
|
||||||
|
// Status line (row 0) is reserved for game-managed status text;
|
||||||
|
// display() writes anywhere by row/col; print() pops a modal window.
|
||||||
|
#define AGI_TEXT_COLS 40u
|
||||||
|
#define AGI_TEXT_ROWS 25u
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char text[AGI_TEXT_COLS + 1u]; // NUL-terminated
|
||||||
|
uint8_t fg;
|
||||||
|
uint8_t bg;
|
||||||
|
} AgiTextLineT;
|
||||||
|
|
||||||
|
#define AGI_PRINT_MAX_LEN 240u
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
bool active; // print modal on screen
|
||||||
|
uint16_t length;
|
||||||
|
char message[AGI_PRINT_MAX_LEN + 1u];
|
||||||
|
} AgiPrintModalT;
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Controllers -----
|
||||||
|
//
|
||||||
|
// set.key binds a (key1, key2) pair to a controller id 0..49. When
|
||||||
|
// the host dispatches a key matching a binding, the controller's
|
||||||
|
// "fired this cycle" bit is set. The IF test controller(N) consumes
|
||||||
|
// the bit so the binding only fires once per press.
|
||||||
|
#define AGI_MAX_CONTROLLERS 50u
|
||||||
|
#define AGI_MAX_KEY_BINDINGS 64u
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t key1; // ASCII or extended scancode (low byte)
|
||||||
|
uint8_t key2; // extended high byte (0 for plain ASCII)
|
||||||
|
uint8_t controller;
|
||||||
|
uint8_t active;
|
||||||
|
} AgiKeyBindingT;
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Menus -----
|
||||||
|
//
|
||||||
|
// set.menu / set.menu.item populate a 2D menu from message ids; the
|
||||||
|
// game can later disable individual items. menu.input pops it up;
|
||||||
|
// the user's choice fires the corresponding controller. Stored as
|
||||||
|
// state so menu.input can render and return a controller id.
|
||||||
|
#define AGI_MAX_MENUS 16u
|
||||||
|
#define AGI_MAX_MENU_ITEMS 16u
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t messageId; // logic 0 message id
|
||||||
|
uint8_t controller; // fired when item selected
|
||||||
|
uint8_t enabled;
|
||||||
|
} AgiMenuItemT;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t nameMsgId;
|
||||||
|
uint8_t itemCount;
|
||||||
|
AgiMenuItemT items[AGI_MAX_MENU_ITEMS];
|
||||||
|
} AgiMenuT;
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Strings -----
|
||||||
|
//
|
||||||
|
// set.string / get.string / word.to.string maintain a small pool of
|
||||||
|
// up to 24 game-defined strings. Each is 40 chars max (one screen row).
|
||||||
|
#define AGI_MAX_STRINGS 24u
|
||||||
|
#define AGI_STRING_LEN 40u
|
||||||
|
|
||||||
|
|
||||||
|
// Host callbacks invoked from the VM for side-effecting opcodes the
|
||||||
|
// host owns. The host owns the logic-resource cache and screen
|
||||||
|
// surfaces; the VM never touches either directly. fetchLogic returns
|
||||||
|
// the post-header bytecode pointer and its length (the 2-byte LE
|
||||||
|
// logic header has already been consumed by the host). NULL on
|
||||||
|
// missing logic causes the VM to halt with AGI_VM_HALT_NO_LOGIC.
|
||||||
|
//
|
||||||
|
// haveKey returns true if a keystroke is buffered, consuming it so
|
||||||
|
// the next call returns false until another key arrives. Used by
|
||||||
|
// the AGI test command have.key.
|
||||||
|
//
|
||||||
|
// fetchMessage returns the *decrypted* bytes of message `msgId`
|
||||||
|
// from logic `logicId`, or NULL if the message is missing. The
|
||||||
|
// returned pointer is valid until the next fetchMessage call (host
|
||||||
|
// may use a single shared scratch buffer). Used by display, print,
|
||||||
|
// print.at and friends.
|
||||||
|
typedef struct {
|
||||||
|
const uint8_t *(*fetchLogic)(void *ctx, uint8_t logicId, uint16_t *outBytecodeLength);
|
||||||
|
void (*loadPic)(void *ctx, uint8_t picId);
|
||||||
|
void (*drawPic)(void *ctx, uint8_t picId);
|
||||||
|
void (*showPic)(void *ctx);
|
||||||
|
void (*discardPic)(void *ctx, uint8_t picId);
|
||||||
|
void (*overlayPic)(void *ctx, uint8_t picId);
|
||||||
|
bool (*haveKey)(void *ctx);
|
||||||
|
const char *(*fetchMessage)(void *ctx, uint8_t logicId, uint8_t msgId);
|
||||||
|
void (*addToPic)(void *ctx, uint8_t viewId, uint8_t loop, uint8_t cel,
|
||||||
|
uint8_t x, uint8_t y, uint8_t pri, uint8_t margin);
|
||||||
|
// Return the playback duration of sound `soundId` in milliseconds,
|
||||||
|
// or 0 if the sound is missing/empty. Used only as a fallback
|
||||||
|
// duration cap when the host can't reliably report playback end;
|
||||||
|
// when isPlayingSound is wired, the VM ignores this and trusts
|
||||||
|
// the host's live state instead.
|
||||||
|
uint32_t (*soundDuration)(void *ctx, uint8_t soundId);
|
||||||
|
// Start playback of sound `soundId`. Any currently-playing AGI
|
||||||
|
// sound is replaced. The VM polls isPlayingSound each tick and
|
||||||
|
// sets the sound-done flag on the true->false transition.
|
||||||
|
void (*playSound)(void *ctx, uint8_t soundId);
|
||||||
|
// Stop playback immediately. Called by OP_STOP_SOUND (and any
|
||||||
|
// path that preempts the current sound).
|
||||||
|
void (*stopSound)(void *ctx);
|
||||||
|
// Live host-side playback state. Returns true while audio output
|
||||||
|
// is active for the currently-armed sound, false once the host
|
||||||
|
// has run all SND events to completion (or stopSound was called).
|
||||||
|
// Source of truth for sound completion: matches what the user
|
||||||
|
// actually hears, so the title sequence advances exactly when
|
||||||
|
// the music ends.
|
||||||
|
bool (*isPlayingSound)(void *ctx);
|
||||||
|
// View metadata for accurate cycling. AGI's cycle/loop opcodes
|
||||||
|
// (cycle.normal, end.of.loop, last.cel, number.of.loops...) need
|
||||||
|
// to know the cel count of the current view+loop and the loop
|
||||||
|
// count of the current view. Without these, end.of.loop fires
|
||||||
|
// its flag only at the AGI_MAX_CELS_PER_LOOP boundary -- and a
|
||||||
|
// sparkle that visually has 8 cels but is treated as 32 won't
|
||||||
|
// signal its end until 4x the intended time, breaking title
|
||||||
|
// sequences that arm end.of.loop expecting a tight cycle.
|
||||||
|
//
|
||||||
|
// Returns 0 if the view/loop is unloaded; callers treat 0 as
|
||||||
|
// "still running" so a not-yet-loaded view doesn't terminate
|
||||||
|
// early. Returns the loop count (>=1) or cel count (>=1) when
|
||||||
|
// the view is available.
|
||||||
|
uint8_t (*viewLoopCount)(void *ctx, uint8_t viewId);
|
||||||
|
uint8_t (*viewCelCount)(void *ctx, uint8_t viewId, uint8_t loopId);
|
||||||
|
void *ctx;
|
||||||
|
} AgiVmCallbacksT;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
// ----- core state -----
|
||||||
|
uint8_t vars[AGI_VM_NUM_VARS];
|
||||||
|
uint8_t flags[AGI_VM_NUM_FLAGS];
|
||||||
|
const uint8_t *code;
|
||||||
|
uint16_t codeLength;
|
||||||
|
uint16_t pc;
|
||||||
|
AgiVmHaltE haltReason;
|
||||||
|
uint8_t lastUnknownOp;
|
||||||
|
uint8_t newRoomId;
|
||||||
|
uint8_t currentLogicId;
|
||||||
|
uint8_t callDepth;
|
||||||
|
AgiVmFrameT callStack[AGI_VM_CALL_STACK_MAX];
|
||||||
|
AgiVmCallbacksT callbacks;
|
||||||
|
|
||||||
|
// ----- objects -----
|
||||||
|
AgiObjectT objects[AGI_MAX_OBJECTS];
|
||||||
|
|
||||||
|
// ----- text plane -----
|
||||||
|
AgiTextLineT textRows[AGI_TEXT_ROWS];
|
||||||
|
uint8_t textFg;
|
||||||
|
uint8_t textBg;
|
||||||
|
bool statusLineOn;
|
||||||
|
uint8_t statusLineRow; // usually 0
|
||||||
|
uint8_t horizon; // any obj y < horizon stops
|
||||||
|
bool programControl; // true => program controls ego (false = player)
|
||||||
|
bool acceptInput; // accept.input; toggled by accept/prevent
|
||||||
|
AgiPrintModalT printModal;
|
||||||
|
|
||||||
|
// ----- controllers + key bindings -----
|
||||||
|
AgiKeyBindingT keyBindings[AGI_MAX_KEY_BINDINGS];
|
||||||
|
uint8_t controllerFired[AGI_MAX_CONTROLLERS];
|
||||||
|
|
||||||
|
// ----- menus -----
|
||||||
|
AgiMenuT menus[AGI_MAX_MENUS];
|
||||||
|
uint8_t menuCount;
|
||||||
|
uint8_t menuOpen; // 0xFF = not open
|
||||||
|
|
||||||
|
// ----- strings -----
|
||||||
|
char strings[AGI_MAX_STRINGS][AGI_STRING_LEN + 1u];
|
||||||
|
|
||||||
|
// ----- sound playback state -----
|
||||||
|
// The VM polls callbacks.isPlayingSound() each tick; when it
|
||||||
|
// transitions from true to false, the sound-done flags fire.
|
||||||
|
// soundPlaying tracks whether we are currently waiting on an
|
||||||
|
// armed sound; soundDoneFlag is the per-sound user flag from
|
||||||
|
// OP_SOUND's second arg (0 = no user flag, just engine flag 9).
|
||||||
|
bool soundPlaying;
|
||||||
|
uint8_t soundDoneFlag;
|
||||||
|
} AgiVmT;
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Resource loader (agiRes.c) -----
|
||||||
|
|
||||||
|
// Close all volume files; safe to call after a failed open.
|
||||||
|
void agiResClose(AgiGameT *game);
|
||||||
|
|
||||||
|
// Allocate and return a resource's raw bytes. Caller frees with
|
||||||
|
// free(). Returns NULL on any failure (slot empty, signature bad,
|
||||||
|
// I/O error, out of memory). On success *outLength receives the
|
||||||
|
// resource payload length in bytes.
|
||||||
|
uint8_t *agiResLoad(const AgiGameT *game, AgiResTypeE type, uint16_t index, uint16_t *outLength);
|
||||||
|
|
||||||
|
// Open an AGI v2 game directory. gameDir is a relative or absolute
|
||||||
|
// path to a directory containing LOGDIR/PICDIR/VIEWDIR/SNDDIR plus
|
||||||
|
// VOL.0..VOL.N. Returns true if every directory file and at least
|
||||||
|
// one VOL.* file opened. Partial-success on missing optional
|
||||||
|
// volumes; agiResLoad reports the missing-volume case per resource.
|
||||||
|
bool agiResOpen(AgiGameT *game, const char *gameDir);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Picture decoder (agiPic.c) -----
|
||||||
|
|
||||||
|
// Allocate the two 160x168 working buffers. Caller must agiPicFree
|
||||||
|
// before exit. Returns false if either malloc fails.
|
||||||
|
bool agiPicAlloc(AgiPicT *pic);
|
||||||
|
|
||||||
|
// Pixel-doubled blit of the visual plane onto the stage starting at
|
||||||
|
// (0, destY). 160 source columns become 320 stage columns; the rows
|
||||||
|
// copy 1:1 so the output occupies (0, destY) - (319, destY+167).
|
||||||
|
void agiPicBlit(const AgiPicT *pic, jlSurfaceT *stage, int16_t destY);
|
||||||
|
|
||||||
|
// Reset both planes to their AGI default backgrounds (15 / 4).
|
||||||
|
void agiPicClear(AgiPicT *pic);
|
||||||
|
|
||||||
|
// Decode an AGI v2 PIC resource into the working buffers. The buffers
|
||||||
|
// must already be cleared (agiPicClear) and allocated. Returns false
|
||||||
|
// if the byte stream is malformed (truncated argument, unknown
|
||||||
|
// opcode); on failure the buffers are left in a partial state.
|
||||||
|
bool agiPicDecode(AgiPicT *pic, const uint8_t *data, uint16_t length);
|
||||||
|
|
||||||
|
void agiPicFree(AgiPicT *pic);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- View decoder (agiView.c) -----
|
||||||
|
|
||||||
|
// Parse an AGI VIEW resource. Takes ownership of `rawBytes` -- they
|
||||||
|
// must remain valid until agiViewFree (the view's cel pointers index
|
||||||
|
// into them). Returns false on malformed data; on failure the buffer
|
||||||
|
// is freed and `view` is left zeroed.
|
||||||
|
bool agiViewParse(AgiViewT *view, uint8_t *rawBytes, uint16_t length);
|
||||||
|
|
||||||
|
void agiViewFree(AgiViewT *view);
|
||||||
|
|
||||||
|
// Draw cel (loopIdx, celIdx) of `view` at AGI coordinate (x, y),
|
||||||
|
// where (x, y) is the bottom-left corner of the cel in 160x168 AGI
|
||||||
|
// space. Pixels are masked per-pixel by the priority plane: actor
|
||||||
|
// pixels are skipped where the picture priority exceeds actorPri.
|
||||||
|
// The blit is pixel-doubled horizontally onto the stage at
|
||||||
|
// (0, destY) -- pass the same destY used by agiPicBlit.
|
||||||
|
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);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- LOGIC VM (agiVm.c) -----
|
||||||
|
|
||||||
|
// Reset every variable and flag to 0 and clear the halt state.
|
||||||
|
void agiVmInit(AgiVmT *vm);
|
||||||
|
|
||||||
|
// Point the VM at a logic resource's bytecode segment. AGI v2 logic
|
||||||
|
// resources start with a 2-byte little-endian length for the bytecode
|
||||||
|
// section followed by the bytecode itself (messages live past it).
|
||||||
|
// Returns false if the buffer is too short to contain a header.
|
||||||
|
bool agiVmLoadLogic(AgiVmT *vm, const uint8_t *resourceBytes, uint16_t resourceLength);
|
||||||
|
|
||||||
|
// Reset the VM's program counter to 0 against an already-loaded
|
||||||
|
// bytecode pointer. Used by the host after handling a halt (room
|
||||||
|
// transition, frame end) to re-run a logic from the top without
|
||||||
|
// re-parsing its resource header.
|
||||||
|
void agiVmResetToLogic(AgiVmT *vm, const uint8_t *bytecode, uint16_t bytecodeLength, uint8_t logicId);
|
||||||
|
|
||||||
|
// Run until the VM hits any halt condition (return, unknown opcode,
|
||||||
|
// truncated stream, or pending room transition). Returns the halt
|
||||||
|
// reason; details live in vm->lastUnknownOp / vm->newRoomId / vm->pc.
|
||||||
|
AgiVmHaltE agiVmRun(AgiVmT *vm);
|
||||||
|
|
||||||
|
// Install host callbacks. May be called before or after agiVmInit;
|
||||||
|
// pass NULL for any callback the host doesn't implement (the VM will
|
||||||
|
// treat that opcode as a no-op stub).
|
||||||
|
void agiVmSetCallbacks(AgiVmT *vm, const AgiVmCallbacksT *callbacks);
|
||||||
|
|
||||||
|
// Advance the AGI engine clock by `seconds` wall-clock seconds. Bumps
|
||||||
|
// v11 (seconds 0..59), v12 (minutes 0..59) and v13 (hours 0..23) with
|
||||||
|
// proper roll-over. Game logic uses these to detect per-second ticks
|
||||||
|
// (KQ3's logic.0 fires a per-second update branch when v11 changes,
|
||||||
|
// which sets flag 45 -- the gate the title countdown is waiting on).
|
||||||
|
// Safe to call with seconds=0 (no-op) and with any size up to UINT8_MAX.
|
||||||
|
void agiVmTickSeconds(AgiVmT *vm, uint8_t seconds);
|
||||||
|
|
||||||
|
// Per-VM-cycle ticker. Called by the host once per game-loop cycle
|
||||||
|
// (~6 Hz, the AGI interpreter cadence). Advances every animated
|
||||||
|
// object's cel cycling and motion. Honors per-object stop.cycling /
|
||||||
|
// stop.motion / stop.update flags. Sets end.of.loop / move.obj.done
|
||||||
|
// flags as those events fire.
|
||||||
|
void agiVmTickAnimation(AgiVmT *vm);
|
||||||
|
|
||||||
|
// Notify the VM that the host received key `keyCode` (low 7 bits =
|
||||||
|
// ASCII; high byte = extended scancode for arrow keys / Fn). Walks
|
||||||
|
// the key-binding table; if `keyCode` matches a set.key binding,
|
||||||
|
// the corresponding controller fires. Also dispatches to any open
|
||||||
|
// menu navigation. Called by the host once per pressed key.
|
||||||
|
void agiVmDispatchKey(AgiVmT *vm, uint8_t key1, uint8_t key2);
|
||||||
|
|
||||||
|
// Acknowledge an open print() modal so the VM can resume. Called by
|
||||||
|
// the host when the user presses ENTER / SPACE / etc. while
|
||||||
|
// vm->printModal.active is true. Clears the modal and the
|
||||||
|
// AGI_VM_HALT_PRINT_PENDING halt reason.
|
||||||
|
void agiVmAckPrint(AgiVmT *vm);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Object helpers (agiObj.c) -----
|
||||||
|
|
||||||
|
// Reset every object slot to the default empty state. Called by
|
||||||
|
// agiVmInit and again on every NEW_ROOM transition (matches AGI's
|
||||||
|
// canonical room-change reset).
|
||||||
|
void agiObjResetAll(AgiVmT *vm);
|
||||||
|
|
||||||
|
// Single-object reset (animate.obj reuses this).
|
||||||
|
void agiObjReset(AgiObjectT *obj);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Text helpers (agiText.c) -----
|
||||||
|
|
||||||
|
// Fetch the host font surface (built lazily on first call). Returned
|
||||||
|
// surface contains 96 ASCII glyphs (codes 32..127) laid out 16 wide
|
||||||
|
// by 6 tall. Caller must NOT free.
|
||||||
|
const jlSurfaceT *agiTextFontSurface(void);
|
||||||
|
|
||||||
|
// asciiMap suitable for jlDrawText against the font surface above.
|
||||||
|
const uint16_t *agiTextAsciiMap(void);
|
||||||
|
|
||||||
|
// Render the VM's text plane (status line + display rows + open
|
||||||
|
// print modal) onto `stage`. Called by host after the picture +
|
||||||
|
// objects are drawn.
|
||||||
|
void agiTextRender(const AgiVmT *vm, jlSurfaceT *stage);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- PIC compositing (agiPic.c addendum) -----
|
||||||
|
|
||||||
|
// Bake a VIEW cel into the PIC visual + priority planes (the
|
||||||
|
// implementation of add.to.pic). priColor 4..15 sets the priority
|
||||||
|
// pixels; priColor == 0 means "use the priority of the actor's Y
|
||||||
|
// band" (AGI default). margin 0..3 reserves a control-line band at
|
||||||
|
// the cel's bottom so add.to.pic art can suggest a walkable edge;
|
||||||
|
// margin 4 means "no margin".
|
||||||
|
void agiPicAddView(AgiPicT *pic, const AgiViewT *view,
|
||||||
|
uint8_t loop, uint8_t cel,
|
||||||
|
int16_t x, int16_t y,
|
||||||
|
uint8_t priColor, uint8_t margin);
|
||||||
|
|
||||||
|
#endif
|
||||||
368
agiObj.c
Normal file
368
agiObj.c
Normal file
|
|
@ -0,0 +1,368 @@
|
||||||
|
// AGI v2 animated-object table: state lifecycle + per-cycle ticks.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
#include "agi.h"
|
||||||
|
|
||||||
|
#include "joey/core.h"
|
||||||
|
#include "joey/debug.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||||
|
segment "AGIOBJ";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
|
||||||
|
// 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 };
|
||||||
|
|
||||||
|
|
||||||
|
// ----- 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);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Internal helpers (alphabetical) -----
|
||||||
|
|
||||||
|
static void advanceCycle(AgiVmT *vm, uint8_t objId) {
|
||||||
|
AgiObjectT *obj;
|
||||||
|
uint8_t cels;
|
||||||
|
uint8_t next;
|
||||||
|
|
||||||
|
obj = &vm->objects[objId];
|
||||||
|
if ((obj->flags & AGI_OBJ_FLAG_ANIMATED) == 0u) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((obj->flags & AGI_OBJ_FLAG_CYCLING) == 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) {
|
||||||
|
case AGI_CYCLE_NORMAL:
|
||||||
|
next = (uint8_t)((obj->cel + 1u) % cels);
|
||||||
|
obj->cel = next;
|
||||||
|
break;
|
||||||
|
case AGI_CYCLE_REVERSE:
|
||||||
|
obj->cel = (obj->cel == 0u) ? (uint8_t)(cels - 1u) : (uint8_t)(obj->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++;
|
||||||
|
}
|
||||||
|
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--;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
obj = &vm->objects[objId];
|
||||||
|
if ((obj->flags & AGI_OBJ_FLAG_ANIMATED) == 0u) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
case AGI_MOTION_NORMAL:
|
||||||
|
default:
|
||||||
|
// Direction is whatever the script (or var 6 for ego) set.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
dir = obj->direction;
|
||||||
|
if (dir == AGI_DIR_NONE || dir >= 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;
|
||||||
|
}
|
||||||
|
if (newX > (int16_t)(AGI_PIC_WIDTH - 1)) {
|
||||||
|
newX = (int16_t)(AGI_PIC_WIDTH - 1);
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
dx = (int16_t)(toX - fromX);
|
||||||
|
dy = (int16_t)(toY - fromY);
|
||||||
|
if (dx == 0 && dy == 0) {
|
||||||
|
return AGI_DIR_NONE;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
if (ay > ax * 2) {
|
||||||
|
return (dy > 0) ? AGI_DIR_S : AGI_DIR_N;
|
||||||
|
}
|
||||||
|
if (dx > 0) {
|
||||||
|
return (dy > 0) ? AGI_DIR_SE : AGI_DIR_NE;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void agiObjResetAll(AgiVmT *vm) {
|
||||||
|
uint8_t i;
|
||||||
|
|
||||||
|
for (i = 0u; i < (uint8_t)AGI_MAX_OBJECTS; i++) {
|
||||||
|
agiObjReset(&vm->objects[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
vm->flags[9] = 1u;
|
||||||
|
vm->soundPlaying = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0u; i < (uint8_t)AGI_MAX_OBJECTS; i++) {
|
||||||
|
AgiObjectT *obj;
|
||||||
|
|
||||||
|
obj = &vm->objects[i];
|
||||||
|
if ((obj->flags & AGI_OBJ_FLAG_ANIMATED) == 0u) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
662
agiPic.c
Normal file
662
agiPic.c
Normal file
|
|
@ -0,0 +1,662 @@
|
||||||
|
// AGI v2 PIC (picture / background) decoder.
|
||||||
|
//
|
||||||
|
// Paints to two private 160x168 chunky 8bpp buffers (visual + priority).
|
||||||
|
// The blit step doubles each AGI source column into two stage columns
|
||||||
|
// so the natural 160x168 internal coordinate system can be used by the
|
||||||
|
// drawing primitives, and the 320-wide stage receives the canonical
|
||||||
|
// AGI look.
|
||||||
|
//
|
||||||
|
// All algorithms here -- line interpolation, corner drawing, scanline
|
||||||
|
// flood fill, pen plotting, opcode dispatch -- are re-implementations
|
||||||
|
// written against the published AGI v2 picture-stream specification.
|
||||||
|
// No third-party source is referenced.
|
||||||
|
|
||||||
|
#include "agi.h"
|
||||||
|
|
||||||
|
#include "joey/draw.h"
|
||||||
|
#include "surfaceInternal.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
|
||||||
|
// PIC opcodes. Anything < 0xF0 is an argument byte; >= 0xF0 ends the
|
||||||
|
// current opcode's argument list.
|
||||||
|
#define OP_FIRST 0xF0u
|
||||||
|
#define OP_SET_PIC_COLOR 0xF0u
|
||||||
|
#define OP_DISABLE_PIC 0xF1u
|
||||||
|
#define OP_SET_PRI_COLOR 0xF2u
|
||||||
|
#define OP_DISABLE_PRI 0xF3u
|
||||||
|
#define OP_Y_CORNER 0xF4u
|
||||||
|
#define OP_X_CORNER 0xF5u
|
||||||
|
#define OP_ABS_LINE 0xF6u
|
||||||
|
#define OP_REL_LINE 0xF7u
|
||||||
|
#define OP_FILL 0xF8u
|
||||||
|
#define OP_SET_PEN 0xF9u
|
||||||
|
#define OP_PLOT_PEN 0xFAu
|
||||||
|
#define OP_END 0xFFu
|
||||||
|
|
||||||
|
// Pen control byte bits (set via OP_SET_PEN, consumed by OP_PLOT_PEN).
|
||||||
|
#define PEN_PATTERN_BIT 0x20u
|
||||||
|
|
||||||
|
// Scanline-fill seed stack. Each fill is reentrant-free, so a single
|
||||||
|
// static stack serves both the visual and priority passes. The bound
|
||||||
|
// is a safe over-estimate of typical PIC fill complexity; KQ3 PICs
|
||||||
|
// don't approach it.
|
||||||
|
#define FILL_STACK_MAX 512
|
||||||
|
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t visualEnabled;
|
||||||
|
uint8_t priorityEnabled;
|
||||||
|
uint8_t visualColor;
|
||||||
|
uint8_t priorityColor;
|
||||||
|
uint8_t penPattern;
|
||||||
|
} PicStateT;
|
||||||
|
|
||||||
|
|
||||||
|
// AGI 16-color CGA-derived palette. Entry 6's (R=A, G=5, B=0) keeps the
|
||||||
|
// classic CGA "brown fix" -- pure (R=A, G=A, B=0) would read as a vivid
|
||||||
|
// yellow-green on the host, which is wrong for AGI's intended look.
|
||||||
|
const uint16_t kAgiPalette[16] = {
|
||||||
|
0x000u, 0x00Au, 0x0A0u, 0x0AAu,
|
||||||
|
0xA00u, 0xA0Au, 0xA50u, 0xAAAu,
|
||||||
|
0x555u, 0x55Fu, 0x5F5u, 0x5FFu,
|
||||||
|
0xF55u, 0xF5Fu, 0xFF5u, 0xFFFu
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Prototypes -----
|
||||||
|
|
||||||
|
static void drawCorner(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos, bool yFirst);
|
||||||
|
static void drawLine(AgiPicT *pic, const PicStateT *state, int16_t x0, int16_t y0, int16_t x1, int16_t y1);
|
||||||
|
static void fillPlane(uint8_t *plane, int16_t x, int16_t y, uint8_t bgValue, uint8_t newValue);
|
||||||
|
static bool isOpcode(uint8_t byte);
|
||||||
|
static void opAbsLine(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos);
|
||||||
|
static void opFill(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos);
|
||||||
|
static void opPlotPen(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos);
|
||||||
|
static void opRelLine(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos);
|
||||||
|
static void plot(AgiPicT *pic, const PicStateT *state, int16_t x, int16_t y);
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||||
|
segment "AGIPIC";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// ----- Internal helpers (alphabetical) -----
|
||||||
|
|
||||||
|
static void drawCorner(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos, bool yFirst) {
|
||||||
|
uint16_t p;
|
||||||
|
int16_t curX;
|
||||||
|
int16_t curY;
|
||||||
|
bool readY;
|
||||||
|
|
||||||
|
p = *pos;
|
||||||
|
if (p + 1 >= length) {
|
||||||
|
*pos = length;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
curX = (int16_t)data[p++];
|
||||||
|
curY = (int16_t)data[p++];
|
||||||
|
plot(pic, state, curX, curY);
|
||||||
|
|
||||||
|
readY = yFirst;
|
||||||
|
while (p < length && !isOpcode(data[p])) {
|
||||||
|
if (readY) {
|
||||||
|
int16_t newY = (int16_t)data[p++];
|
||||||
|
drawLine(pic, state, curX, curY, curX, newY);
|
||||||
|
curY = newY;
|
||||||
|
} else {
|
||||||
|
int16_t newX = (int16_t)data[p++];
|
||||||
|
drawLine(pic, state, curX, curY, newX, curY);
|
||||||
|
curX = newX;
|
||||||
|
}
|
||||||
|
readY = !readY;
|
||||||
|
}
|
||||||
|
*pos = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Documented AGI line algorithm. Major axis steps in unit increments;
|
||||||
|
// the minor axis is linearly interpolated with round-half-away-from-
|
||||||
|
// zero. All math stays in 16-bit -- AGI coordinates are 8-bit so the
|
||||||
|
// worst product (dy * i = 167 * 167 = 27889) fits in int16, and the
|
||||||
|
// 65816 build avoids ORCA-C's long-arithmetic helpers entirely.
|
||||||
|
static void drawLine(AgiPicT *pic, const PicStateT *state, int16_t x0, int16_t y0, int16_t x1, int16_t y1) {
|
||||||
|
int16_t dx;
|
||||||
|
int16_t dy;
|
||||||
|
int16_t absDx;
|
||||||
|
int16_t absDy;
|
||||||
|
int16_t steps;
|
||||||
|
int16_t i;
|
||||||
|
int16_t half;
|
||||||
|
int16_t px;
|
||||||
|
int16_t py;
|
||||||
|
int16_t sx;
|
||||||
|
int16_t sy;
|
||||||
|
|
||||||
|
dx = (int16_t)(x1 - x0);
|
||||||
|
dy = (int16_t)(y1 - y0);
|
||||||
|
if (dx == 0 && dy == 0) {
|
||||||
|
plot(pic, state, x0, y0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
absDx = (int16_t)((dx >= 0) ? dx : -dx);
|
||||||
|
absDy = (int16_t)((dy >= 0) ? dy : -dy);
|
||||||
|
sx = (int16_t)((dx >= 0) ? 1 : -1);
|
||||||
|
sy = (int16_t)((dy >= 0) ? 1 : -1);
|
||||||
|
|
||||||
|
if (absDx >= absDy) {
|
||||||
|
steps = absDx;
|
||||||
|
half = (int16_t)(steps / 2);
|
||||||
|
for (i = 0; i <= steps; i++) {
|
||||||
|
px = (int16_t)(x0 + sx * i);
|
||||||
|
py = (int16_t)(y0 + (dy * i + sy * half) / steps);
|
||||||
|
plot(pic, state, px, py);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
steps = absDy;
|
||||||
|
half = (int16_t)(steps / 2);
|
||||||
|
for (i = 0; i <= steps; i++) {
|
||||||
|
py = (int16_t)(y0 + sy * i);
|
||||||
|
px = (int16_t)(x0 + (dx * i + sx * half) / steps);
|
||||||
|
plot(pic, state, px, py);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Scanline flood fill on one plane. Replaces every cell of value
|
||||||
|
// `bgValue` 4-connected to (x, y) with `newValue`. The seed stack is
|
||||||
|
// bounded; runs that don't fit are silently dropped, which mirrors
|
||||||
|
// the original interpreter's behavior under pathological PIC inputs.
|
||||||
|
static void fillPlane(uint8_t *plane, int16_t x, int16_t y, uint8_t bgValue, uint8_t newValue) {
|
||||||
|
static int16_t stackX[FILL_STACK_MAX];
|
||||||
|
static int16_t stackY[FILL_STACK_MAX];
|
||||||
|
uint16_t sp;
|
||||||
|
int16_t cx;
|
||||||
|
int16_t cy;
|
||||||
|
int16_t left;
|
||||||
|
int16_t right;
|
||||||
|
int16_t scanX;
|
||||||
|
bool aboveOpen;
|
||||||
|
bool belowOpen;
|
||||||
|
|
||||||
|
if (x < 0 || x >= AGI_PIC_WIDTH || y < 0 || y >= AGI_PIC_HEIGHT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (plane[y * AGI_PIC_WIDTH + x] != bgValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (bgValue == newValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sp = 0;
|
||||||
|
stackX[sp] = x;
|
||||||
|
stackY[sp] = y;
|
||||||
|
sp++;
|
||||||
|
|
||||||
|
while (sp > 0u) {
|
||||||
|
sp--;
|
||||||
|
cx = stackX[sp];
|
||||||
|
cy = stackY[sp];
|
||||||
|
|
||||||
|
if (plane[cy * AGI_PIC_WIDTH + cx] != bgValue) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand the current row to the bg run's full extent.
|
||||||
|
left = cx;
|
||||||
|
while (left > 0 && plane[cy * AGI_PIC_WIDTH + (left - 1)] == bgValue) {
|
||||||
|
left--;
|
||||||
|
}
|
||||||
|
right = cx;
|
||||||
|
while (right < (int16_t)(AGI_PIC_WIDTH - 1) && plane[cy * AGI_PIC_WIDTH + (right + 1)] == bgValue) {
|
||||||
|
right++;
|
||||||
|
}
|
||||||
|
for (scanX = left; scanX <= right; scanX++) {
|
||||||
|
plane[cy * AGI_PIC_WIDTH + scanX] = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push one seed per bg run on the row above.
|
||||||
|
if (cy > 0) {
|
||||||
|
aboveOpen = false;
|
||||||
|
for (scanX = left; scanX <= right; scanX++) {
|
||||||
|
if (plane[(cy - 1) * AGI_PIC_WIDTH + scanX] == bgValue) {
|
||||||
|
if (!aboveOpen) {
|
||||||
|
if (sp < FILL_STACK_MAX) {
|
||||||
|
stackX[sp] = scanX;
|
||||||
|
stackY[sp] = (int16_t)(cy - 1);
|
||||||
|
sp++;
|
||||||
|
}
|
||||||
|
aboveOpen = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
aboveOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// And one seed per bg run on the row below.
|
||||||
|
if (cy < (int16_t)(AGI_PIC_HEIGHT - 1)) {
|
||||||
|
belowOpen = false;
|
||||||
|
for (scanX = left; scanX <= right; scanX++) {
|
||||||
|
if (plane[(cy + 1) * AGI_PIC_WIDTH + scanX] == bgValue) {
|
||||||
|
if (!belowOpen) {
|
||||||
|
if (sp < FILL_STACK_MAX) {
|
||||||
|
stackX[sp] = scanX;
|
||||||
|
stackY[sp] = (int16_t)(cy + 1);
|
||||||
|
sp++;
|
||||||
|
}
|
||||||
|
belowOpen = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
belowOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool isOpcode(uint8_t byte) {
|
||||||
|
return byte >= OP_FIRST;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void opAbsLine(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos) {
|
||||||
|
uint16_t p;
|
||||||
|
int16_t prevX;
|
||||||
|
int16_t prevY;
|
||||||
|
int16_t curX;
|
||||||
|
int16_t curY;
|
||||||
|
bool have;
|
||||||
|
|
||||||
|
p = *pos;
|
||||||
|
have = false;
|
||||||
|
prevX = 0;
|
||||||
|
prevY = 0;
|
||||||
|
while (p + 1 < length && !isOpcode(data[p])) {
|
||||||
|
curX = (int16_t)data[p++];
|
||||||
|
curY = (int16_t)data[p++];
|
||||||
|
if (!have) {
|
||||||
|
plot(pic, state, curX, curY);
|
||||||
|
have = true;
|
||||||
|
} else {
|
||||||
|
drawLine(pic, state, prevX, prevY, curX, curY);
|
||||||
|
}
|
||||||
|
prevX = curX;
|
||||||
|
prevY = curY;
|
||||||
|
}
|
||||||
|
*pos = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void opFill(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos) {
|
||||||
|
uint16_t p;
|
||||||
|
int16_t x;
|
||||||
|
int16_t y;
|
||||||
|
|
||||||
|
p = *pos;
|
||||||
|
while (p + 1 < length && !isOpcode(data[p])) {
|
||||||
|
x = (int16_t)data[p++];
|
||||||
|
y = (int16_t)data[p++];
|
||||||
|
if (state->visualEnabled) {
|
||||||
|
fillPlane(pic->visual, x, y, AGI_VISUAL_BG, state->visualColor);
|
||||||
|
}
|
||||||
|
if (state->priorityEnabled) {
|
||||||
|
fillPlane(pic->priority, x, y, AGI_PRIORITY_BG, state->priorityColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*pos = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Plot-with-pen. With pen patterning off, each iteration consumes an
|
||||||
|
// X,Y pair and plots a single pixel. With patterning on, each
|
||||||
|
// iteration consumes a texture byte first (we discard it -- proper
|
||||||
|
// patterned brushes are deferred to a later pass). Size and shape
|
||||||
|
// are likewise stubbed to single-pixel; KQ3 PICs use pens sparingly
|
||||||
|
// and dropping the brush footprint just thins occasional grass / sky
|
||||||
|
// dithering.
|
||||||
|
static void opPlotPen(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos) {
|
||||||
|
uint16_t p;
|
||||||
|
int16_t x;
|
||||||
|
int16_t y;
|
||||||
|
|
||||||
|
p = *pos;
|
||||||
|
while (p < length && !isOpcode(data[p])) {
|
||||||
|
if (state->penPattern) {
|
||||||
|
// Skip the texture-selector byte.
|
||||||
|
p++;
|
||||||
|
if (p >= length || isOpcode(data[p])) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (p + 1 >= length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
x = (int16_t)data[p++];
|
||||||
|
y = (int16_t)data[p++];
|
||||||
|
plot(pic, state, x, y);
|
||||||
|
}
|
||||||
|
*pos = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void opRelLine(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos) {
|
||||||
|
uint16_t p;
|
||||||
|
int16_t curX;
|
||||||
|
int16_t curY;
|
||||||
|
int16_t newX;
|
||||||
|
int16_t newY;
|
||||||
|
uint8_t disp;
|
||||||
|
int16_t dx;
|
||||||
|
int16_t dy;
|
||||||
|
|
||||||
|
p = *pos;
|
||||||
|
if (p + 1 >= length) {
|
||||||
|
*pos = p;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
curX = (int16_t)data[p++];
|
||||||
|
curY = (int16_t)data[p++];
|
||||||
|
plot(pic, state, curX, curY);
|
||||||
|
|
||||||
|
while (p < length && !isOpcode(data[p])) {
|
||||||
|
disp = data[p++];
|
||||||
|
// High nibble = X disp (bit 7 sign, bits 4-6 magnitude 0..7).
|
||||||
|
// Low nibble = Y disp (bit 3 sign, bits 0-2 magnitude 0..7).
|
||||||
|
dx = (int16_t)((disp >> 4) & 0x07u);
|
||||||
|
if (disp & 0x80u) {
|
||||||
|
dx = (int16_t)-dx;
|
||||||
|
}
|
||||||
|
dy = (int16_t)(disp & 0x07u);
|
||||||
|
if (disp & 0x08u) {
|
||||||
|
dy = (int16_t)-dy;
|
||||||
|
}
|
||||||
|
newX = (int16_t)(curX + dx);
|
||||||
|
newY = (int16_t)(curY + dy);
|
||||||
|
drawLine(pic, state, curX, curY, newX, newY);
|
||||||
|
curX = newX;
|
||||||
|
curY = newY;
|
||||||
|
}
|
||||||
|
*pos = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void plot(AgiPicT *pic, const PicStateT *state, int16_t x, int16_t y) {
|
||||||
|
uint16_t idx;
|
||||||
|
|
||||||
|
if (x < 0 || x >= (int16_t)AGI_PIC_WIDTH) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (y < 0 || y >= (int16_t)AGI_PIC_HEIGHT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
idx = (uint16_t)(y * (int16_t)AGI_PIC_WIDTH + x);
|
||||||
|
if (state->visualEnabled) {
|
||||||
|
pic->visual[idx] = state->visualColor;
|
||||||
|
}
|
||||||
|
if (state->priorityEnabled) {
|
||||||
|
pic->priority[idx] = state->priorityColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Public API (alphabetical) -----
|
||||||
|
|
||||||
|
bool agiPicAlloc(AgiPicT *pic) {
|
||||||
|
pic->visual = (uint8_t *)malloc(AGI_PIC_PIXELS);
|
||||||
|
pic->priority = (uint8_t *)malloc(AGI_PIC_PIXELS);
|
||||||
|
if (pic->visual == NULL || pic->priority == NULL) {
|
||||||
|
agiPicFree(pic);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void agiPicBlit(const AgiPicT *pic, jlSurfaceT *stage, int16_t destY) {
|
||||||
|
int16_t sy;
|
||||||
|
int16_t sx;
|
||||||
|
int16_t dx;
|
||||||
|
uint8_t color;
|
||||||
|
|
||||||
|
// Fast path: chunky shadow exists -> write packed bytes direct,
|
||||||
|
// mark the whole region dirty once at the end. ~30x faster than
|
||||||
|
// calling jlDrawPixel 53,760 times (each does bounds + 1-pixel
|
||||||
|
// dirty mark). Required for state-2 / state-3 title transitions
|
||||||
|
// to render under a second on period-correct DOSBox CPU cycles.
|
||||||
|
if (stage != NULL && stage->pixels != NULL) {
|
||||||
|
const uint8_t *src = pic->visual;
|
||||||
|
uint8_t *dst;
|
||||||
|
int16_t rowOff;
|
||||||
|
|
||||||
|
for (sy = 0; sy < (int16_t)AGI_PIC_HEIGHT; sy++) {
|
||||||
|
rowOff = (int16_t)((int16_t)(destY + sy) * (int16_t)SURFACE_BYTES_PER_ROW);
|
||||||
|
dst = &stage->pixels[rowOff];
|
||||||
|
for (sx = 0; sx < (int16_t)AGI_PIC_WIDTH; sx++) {
|
||||||
|
color = (uint8_t)(src[sy * AGI_PIC_WIDTH + sx] & 0x0Fu);
|
||||||
|
// Pixel-doubled: each source pixel = 1 dst byte
|
||||||
|
// (both nibbles = same color).
|
||||||
|
dst[sx] = (uint8_t)((color << 4) | color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
surfaceMarkDirtyRect(stage, 0, destY,
|
||||||
|
(int16_t)SURFACE_WIDTH, (int16_t)AGI_PIC_HEIGHT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Planar fallback (s->pixels NULL: Phase-9 Amiga). Goes through
|
||||||
|
// jlDrawPixel so the port's c2p / plane-sync path stays correct.
|
||||||
|
for (sy = 0; sy < (int16_t)AGI_PIC_HEIGHT; sy++) {
|
||||||
|
for (sx = 0; sx < (int16_t)AGI_PIC_WIDTH; sx++) {
|
||||||
|
color = pic->visual[sy * AGI_PIC_WIDTH + sx];
|
||||||
|
dx = (int16_t)(sx << 1);
|
||||||
|
jlDrawPixel(stage, dx, (int16_t)(destY + sy), color);
|
||||||
|
jlDrawPixel(stage, (int16_t)(dx + 1), (int16_t)(destY + sy), color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void agiPicClear(AgiPicT *pic) {
|
||||||
|
if (pic->visual != NULL) {
|
||||||
|
memset(pic->visual, AGI_VISUAL_BG, AGI_PIC_PIXELS);
|
||||||
|
}
|
||||||
|
if (pic->priority != NULL) {
|
||||||
|
memset(pic->priority, AGI_PRIORITY_BG, AGI_PIC_PIXELS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool agiPicDecode(AgiPicT *pic, const uint8_t *data, uint16_t length) {
|
||||||
|
PicStateT state;
|
||||||
|
uint16_t pos;
|
||||||
|
uint8_t op;
|
||||||
|
|
||||||
|
state.visualEnabled = 0u;
|
||||||
|
state.priorityEnabled = 0u;
|
||||||
|
state.visualColor = 0u;
|
||||||
|
state.priorityColor = 0u;
|
||||||
|
state.penPattern = 0u;
|
||||||
|
|
||||||
|
pos = 0u;
|
||||||
|
while (pos < length) {
|
||||||
|
op = data[pos++];
|
||||||
|
switch (op) {
|
||||||
|
case OP_SET_PIC_COLOR:
|
||||||
|
if (pos >= length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
state.visualColor = data[pos++];
|
||||||
|
state.visualEnabled = 1u;
|
||||||
|
break;
|
||||||
|
case OP_DISABLE_PIC:
|
||||||
|
state.visualEnabled = 0u;
|
||||||
|
break;
|
||||||
|
case OP_SET_PRI_COLOR:
|
||||||
|
if (pos >= length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
state.priorityColor = data[pos++];
|
||||||
|
state.priorityEnabled = 1u;
|
||||||
|
break;
|
||||||
|
case OP_DISABLE_PRI:
|
||||||
|
state.priorityEnabled = 0u;
|
||||||
|
break;
|
||||||
|
case OP_Y_CORNER:
|
||||||
|
drawCorner(pic, &state, data, length, &pos, true);
|
||||||
|
break;
|
||||||
|
case OP_X_CORNER:
|
||||||
|
drawCorner(pic, &state, data, length, &pos, false);
|
||||||
|
break;
|
||||||
|
case OP_ABS_LINE:
|
||||||
|
opAbsLine(pic, &state, data, length, &pos);
|
||||||
|
break;
|
||||||
|
case OP_REL_LINE:
|
||||||
|
opRelLine(pic, &state, data, length, &pos);
|
||||||
|
break;
|
||||||
|
case OP_FILL:
|
||||||
|
opFill(pic, &state, data, length, &pos);
|
||||||
|
break;
|
||||||
|
case OP_SET_PEN:
|
||||||
|
if (pos >= length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
state.penPattern = (uint8_t)((data[pos++] & PEN_PATTERN_BIT) ? 1u : 0u);
|
||||||
|
break;
|
||||||
|
case OP_PLOT_PEN:
|
||||||
|
opPlotPen(pic, &state, data, length, &pos);
|
||||||
|
break;
|
||||||
|
case OP_END:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
// Unknown opcode -- stop cleanly rather than misparsing.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Bake a single VIEW cel into the visual + priority planes (the
|
||||||
|
// implementation of add.to.pic). Pixels outside the picture window
|
||||||
|
// or set to the cel's transparent color are skipped. priColor in
|
||||||
|
// 4..15 overwrites the priority pixels where the visual is opaque;
|
||||||
|
// 0 preserves the existing priority. RLE format matches agiViewDraw
|
||||||
|
// (per-row 0-byte EOR marker after a variable run).
|
||||||
|
void agiPicAddView(AgiPicT *pic, const AgiViewT *view,
|
||||||
|
uint8_t loop, uint8_t cel,
|
||||||
|
int16_t baseX, int16_t baseY,
|
||||||
|
uint8_t priColor, uint8_t margin) {
|
||||||
|
const AgiCelInfoT *info;
|
||||||
|
const AgiCelInfoT *resolved;
|
||||||
|
const uint8_t *rle;
|
||||||
|
uint8_t actualLoop;
|
||||||
|
int16_t width;
|
||||||
|
int16_t height;
|
||||||
|
uint8_t trans;
|
||||||
|
bool mirrored;
|
||||||
|
int16_t topY;
|
||||||
|
int16_t cy;
|
||||||
|
int16_t cx;
|
||||||
|
int16_t drawCx;
|
||||||
|
int16_t agiX;
|
||||||
|
int16_t agiY;
|
||||||
|
uint8_t rleByte;
|
||||||
|
uint8_t color;
|
||||||
|
uint8_t runLen;
|
||||||
|
uint8_t r;
|
||||||
|
|
||||||
|
(void)margin;
|
||||||
|
if (view == NULL || pic == NULL || pic->visual == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (loop >= view->loopCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cel >= view->loops[loop].celCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
info = &view->loops[loop].cels[cel];
|
||||||
|
actualLoop = loop;
|
||||||
|
mirrored = false;
|
||||||
|
if (info->mirrored != 0u) {
|
||||||
|
if (info->mirrorSourceLoop != loop && info->mirrorSourceLoop < view->loopCount
|
||||||
|
&& cel < view->loops[info->mirrorSourceLoop].celCount) {
|
||||||
|
actualLoop = info->mirrorSourceLoop;
|
||||||
|
mirrored = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolved = &view->loops[actualLoop].cels[cel];
|
||||||
|
|
||||||
|
width = (int16_t)resolved->width;
|
||||||
|
height = (int16_t)resolved->height;
|
||||||
|
trans = resolved->transparentColor;
|
||||||
|
rle = resolved->rleData;
|
||||||
|
topY = (int16_t)(baseY - height + 1);
|
||||||
|
|
||||||
|
for (cy = 0; cy < height; cy++) {
|
||||||
|
cx = 0;
|
||||||
|
for (;;) {
|
||||||
|
rleByte = *rle++;
|
||||||
|
if (rleByte == 0u) {
|
||||||
|
// End-of-row marker.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
color = (uint8_t)(rleByte >> 4);
|
||||||
|
runLen = (uint8_t)(rleByte & 0x0Fu);
|
||||||
|
for (r = 0u; r < runLen; r++) {
|
||||||
|
if (cx >= width) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (color != trans) {
|
||||||
|
drawCx = mirrored ? (int16_t)(width - 1 - cx) : cx;
|
||||||
|
agiX = (int16_t)(baseX + drawCx);
|
||||||
|
agiY = (int16_t)(topY + cy);
|
||||||
|
if (agiX >= 0 && agiX < (int16_t)AGI_PIC_WIDTH && agiY >= 0 && agiY < (int16_t)AGI_PIC_HEIGHT) {
|
||||||
|
int16_t idx = (int16_t)(agiY * (int16_t)AGI_PIC_WIDTH + agiX);
|
||||||
|
bool paint = true;
|
||||||
|
|
||||||
|
// AGI v2 priority masking: when priColor is a
|
||||||
|
// real priority value (>= AGI_PRIORITY_BG),
|
||||||
|
// skip the pixel if the existing priority is
|
||||||
|
// strictly higher -- the new art is behind
|
||||||
|
// existing higher-priority art. This is how
|
||||||
|
// KQ3's title "III" ends up behind the KQ
|
||||||
|
// logo (III priColor=4 = background, KQ logo
|
||||||
|
// in PIC 45 sits at a higher band).
|
||||||
|
if (priColor >= AGI_PRIORITY_BG && priColor < 16u && pic->priority != NULL) {
|
||||||
|
if (pic->priority[idx] > priColor) {
|
||||||
|
paint = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (paint) {
|
||||||
|
pic->visual[idx] = color;
|
||||||
|
if (priColor >= AGI_PRIORITY_BG && priColor < 16u && pic->priority != NULL) {
|
||||||
|
pic->priority[idx] = priColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void agiPicFree(AgiPicT *pic) {
|
||||||
|
if (pic->visual != NULL) {
|
||||||
|
free(pic->visual);
|
||||||
|
pic->visual = NULL;
|
||||||
|
}
|
||||||
|
if (pic->priority != NULL) {
|
||||||
|
free(pic->priority);
|
||||||
|
pic->priority = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
309
agiRes.c
Normal file
309
agiRes.c
Normal file
|
|
@ -0,0 +1,309 @@
|
||||||
|
// AGI v2 resource loader.
|
||||||
|
//
|
||||||
|
// 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).
|
||||||
|
|
||||||
|
#include "agi.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.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
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// Max directory + path string size. AGI filenames are 7 chars max;
|
||||||
|
// gameDir is application-controlled. 256 leaves comfortable margin.
|
||||||
|
#define AGI_PATH_MAX 256
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
|
||||||
|
// IIgs 64 KB code-bank rule: park this TU in its own load segment
|
||||||
|
// so it doesn't pile onto _ROOT (which still holds main() and the
|
||||||
|
// ORCA-C startup). Cross-platform builds ignore this pragma.
|
||||||
|
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||||
|
segment "AGIRES";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Prototypes -----
|
||||||
|
|
||||||
|
static bool buildPath(char *out, size_t outSize, const char *dir, const char *file);
|
||||||
|
static bool buildVolPath(char *out, size_t outSize, const char *dir, uint8_t volNum);
|
||||||
|
static bool loadDirectory(AgiGameT *game, AgiResTypeE type, const char *gameDir, const char *fileName);
|
||||||
|
static bool openVolumes(AgiGameT *game, const char *gameDir);
|
||||||
|
static const char *resTypeFileName(AgiResTypeE type);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Internal helpers (alphabetical) -----
|
||||||
|
|
||||||
|
static bool buildPath(char *out, size_t outSize, const char *dir, const char *file) {
|
||||||
|
size_t dirLen;
|
||||||
|
size_t fileLen;
|
||||||
|
|
||||||
|
dirLen = strlen(dir);
|
||||||
|
fileLen = strlen(file);
|
||||||
|
if (dirLen + 1 + fileLen + 1 > outSize) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
memcpy(out, dir, dirLen);
|
||||||
|
out[dirLen] = '/';
|
||||||
|
memcpy(out + dirLen + 1, file, fileLen);
|
||||||
|
out[dirLen + 1 + fileLen] = '\0';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool buildVolPath(char *out, size_t outSize, const char *dir, uint8_t volNum) {
|
||||||
|
size_t dirLen;
|
||||||
|
char *p;
|
||||||
|
|
||||||
|
dirLen = strlen(dir);
|
||||||
|
// "/VOL." + up to 2 digits + NUL = 8 chars max.
|
||||||
|
if (dirLen + 8 > outSize) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
memcpy(out, dir, dirLen);
|
||||||
|
p = out + dirLen;
|
||||||
|
*p++ = '/';
|
||||||
|
*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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool loadDirectory(AgiGameT *game, AgiResTypeE type, const char *gameDir, const char *fileName) {
|
||||||
|
char path[AGI_PATH_MAX];
|
||||||
|
FILE *fp;
|
||||||
|
long fileSize;
|
||||||
|
uint16_t entryCount;
|
||||||
|
uint16_t i;
|
||||||
|
uint8_t buf[AGI_DIR_ENTRY_BYTES];
|
||||||
|
uint8_t volNibble;
|
||||||
|
uint32_t offset;
|
||||||
|
|
||||||
|
if (!buildPath(path, AGI_PATH_MAX, gameDir, fileName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fp = fopen(path, "rb");
|
||||||
|
if (fp == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fseek(fp, 0L, SEEK_END) != 0) {
|
||||||
|
fclose(fp);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
fileSize = ftell(fp);
|
||||||
|
if (fileSize < 0 || fileSize > (long)(AGI_MAX_RESOURCES * AGI_DIR_ENTRY_BYTES)) {
|
||||||
|
fclose(fp);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ((fileSize % AGI_DIR_ENTRY_BYTES) != 0) {
|
||||||
|
fclose(fp);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (fseek(fp, 0L, SEEK_SET) != 0) {
|
||||||
|
fclose(fp);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
entryCount = (uint16_t)(fileSize / AGI_DIR_ENTRY_BYTES);
|
||||||
|
for (i = 0; i < entryCount; i++) {
|
||||||
|
if (fread(buf, 1, AGI_DIR_ENTRY_BYTES, fp) != AGI_DIR_ENTRY_BYTES) {
|
||||||
|
fclose(fp);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
volNibble = (uint8_t)(buf[0] >> 4);
|
||||||
|
// 20-bit offset, cast to uint32_t before shift so ORCA-C's
|
||||||
|
// 16-bit int doesn't truncate the (& 0x0F) << 16 term.
|
||||||
|
offset = ((uint32_t)(buf[0] & 0x0F) << 16) | ((uint32_t)buf[1] << 8) | (uint32_t)buf[2];
|
||||||
|
|
||||||
|
if (buf[0] == 0xFF && buf[1] == 0xFF && buf[2] == 0xFF) {
|
||||||
|
game->resDir[type][i].volume = AGI_DIR_EMPTY_VOLUME;
|
||||||
|
game->resDir[type][i].offset = 0u;
|
||||||
|
} else {
|
||||||
|
game->resDir[type][i].volume = volNibble;
|
||||||
|
game->resDir[type][i].offset = offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(fp);
|
||||||
|
|
||||||
|
game->resCount[type] = entryCount;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool openVolumes(AgiGameT *game, const char *gameDir) {
|
||||||
|
char path[AGI_PATH_MAX];
|
||||||
|
uint8_t v;
|
||||||
|
bool anyOpened;
|
||||||
|
|
||||||
|
anyOpened = false;
|
||||||
|
for (v = 0; v < AGI_MAX_VOLUMES; v++) {
|
||||||
|
if (!buildVolPath(path, AGI_PATH_MAX, gameDir, v)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
game->volFiles[v] = fopen(path, "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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Public API (alphabetical) -----
|
||||||
|
|
||||||
|
void agiResClose(AgiGameT *game) {
|
||||||
|
uint8_t v;
|
||||||
|
uint8_t t;
|
||||||
|
|
||||||
|
for (v = 0; 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 *payload;
|
||||||
|
uint16_t length;
|
||||||
|
uint8_t flagsAndVolume;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
fp = game->volFiles[entry->volume];
|
||||||
|
if (fp == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fseek(fp, (long)entry->offset, SEEK_SET) != 0) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (fread(header, 1, AGI_VOL_HEADER_BYTES, fp) != AGI_VOL_HEADER_BYTES) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (header[0] != AGI_VOL_SIG_0 || header[1] != AGI_VOL_SIG_1) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
flagsAndVolume = header[2];
|
||||||
|
if (flagsAndVolume & AGI_VOL_COMPRESSED) {
|
||||||
|
// v3 LZW-compressed resource. Phase 2 work; not handled yet.
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if ((flagsAndVolume & AGI_VOL_VOLUME_MASK) != entry->volume) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
length = (uint16_t)(((uint16_t)header[4] << 8) | (uint16_t)header[3]);
|
||||||
|
if (length == 0u) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = (uint8_t *)malloc(length);
|
||||||
|
if (payload == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (fread(payload, 1, length, fp) != length) {
|
||||||
|
free(payload);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outLength != NULL) {
|
||||||
|
*outLength = length;
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool agiResOpen(AgiGameT *game, const char *gameDir) {
|
||||||
|
uint8_t t;
|
||||||
|
|
||||||
|
memset(game, 0, sizeof(*game));
|
||||||
|
|
||||||
|
for (t = 0; t < AGI_RES_COUNT; t++) {
|
||||||
|
if (!loadDirectory(game, (AgiResTypeE)t, gameDir, resTypeFileName((AgiResTypeE)t))) {
|
||||||
|
agiResClose(game);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!openVolumes(game, gameDir)) {
|
||||||
|
agiResClose(game);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
438
agiText.c
Normal file
438
agiText.c
Normal file
|
|
@ -0,0 +1,438 @@
|
||||||
|
// AGI v2 text plane: status line, display() rows, print() modal.
|
||||||
|
//
|
||||||
|
// Renders directly to the stage with an embedded 8x8 ASCII bitmap
|
||||||
|
// font; bypasses jlDrawText so glyph foreground/background can vary
|
||||||
|
// per-call without rebuilding a font surface for every color combo.
|
||||||
|
//
|
||||||
|
// Layout matches AGI's 40x25 text grid:
|
||||||
|
// Row 0 : status line (when statusLineOn)
|
||||||
|
// Rows 1..21 : picture (handled by agiPicBlit, not here)
|
||||||
|
// Rows 22..24 : message overlay area
|
||||||
|
//
|
||||||
|
// The print() modal centers a window over the picture region with
|
||||||
|
// a 1-character border and waits for user ack (HALT_PRINT_PENDING).
|
||||||
|
|
||||||
|
#include "agi.h"
|
||||||
|
|
||||||
|
#include "joey/draw.h"
|
||||||
|
#include "joey/surface.h"
|
||||||
|
#include "surfaceInternal.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||||
|
segment "AGITXT";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
#define GLYPH_W 8u
|
||||||
|
#define GLYPH_H 8u
|
||||||
|
#define FIRST_CHAR 0x20u // space
|
||||||
|
#define LAST_CHAR 0x7Eu // tilde
|
||||||
|
#define GLYPH_COUNT (LAST_CHAR - FIRST_CHAR + 1u)
|
||||||
|
|
||||||
|
#define STAGE_W SURFACE_WIDTH
|
||||||
|
#define STAGE_H SURFACE_HEIGHT
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Prototypes -----
|
||||||
|
|
||||||
|
static void drawChar(jlSurfaceT *stage, int16_t x, int16_t y, char ch, uint8_t fg, uint8_t bg);
|
||||||
|
static void drawCharTransparent(jlSurfaceT *stage, int16_t x, int16_t y, char ch, uint8_t fg);
|
||||||
|
static void drawString(jlSurfaceT *stage, int16_t x, int16_t y, const char *s, uint8_t fg, uint8_t bg);
|
||||||
|
static void drawWindow(jlSurfaceT *stage, int16_t x, int16_t y, uint8_t cols, uint8_t rows, uint8_t bg, uint8_t border);
|
||||||
|
static void fillRow(jlSurfaceT *stage, uint8_t row, uint8_t bg);
|
||||||
|
|
||||||
|
|
||||||
|
// 8x8 bitmap font for printable ASCII (0x20 = space .. 0x7E = tilde).
|
||||||
|
// Each glyph is 8 bytes; bit 7 is the leftmost pixel of each row.
|
||||||
|
// Lower-case letters map to upper-case via the ASCII normalizer in
|
||||||
|
// drawChar so the table only needs the upper half.
|
||||||
|
//
|
||||||
|
// Hand-pixeled to be readable rather than authentic. Each row is
|
||||||
|
// commented with the visual pattern (X = on, . = off).
|
||||||
|
static const uint8_t kFont[GLYPH_COUNT][GLYPH_H] = {
|
||||||
|
/* 0x20 ' ' */ {0,0,0,0,0,0,0,0},
|
||||||
|
/* 0x21 '!' */ {0x18,0x18,0x18,0x18,0x18,0x00,0x18,0x00},
|
||||||
|
/* 0x22 '"' */ {0x6C,0x6C,0x48,0x00,0x00,0x00,0x00,0x00},
|
||||||
|
/* 0x23 '#' */ {0x36,0x36,0x7F,0x36,0x7F,0x36,0x36,0x00},
|
||||||
|
/* 0x24 '$' */ {0x18,0x3E,0x60,0x3C,0x06,0x7C,0x18,0x00},
|
||||||
|
/* 0x25 '%' */ {0x66,0x66,0x0C,0x18,0x30,0x66,0x66,0x00},
|
||||||
|
/* 0x26 '&' */ {0x38,0x6C,0x38,0x76,0xDC,0xCC,0x76,0x00},
|
||||||
|
/* 0x27 '\''*/ {0x18,0x18,0x10,0x00,0x00,0x00,0x00,0x00},
|
||||||
|
/* 0x28 '(' */ {0x0C,0x18,0x30,0x30,0x30,0x18,0x0C,0x00},
|
||||||
|
/* 0x29 ')' */ {0x30,0x18,0x0C,0x0C,0x0C,0x18,0x30,0x00},
|
||||||
|
/* 0x2A '*' */ {0x00,0x66,0x3C,0xFF,0x3C,0x66,0x00,0x00},
|
||||||
|
/* 0x2B '+' */ {0x00,0x18,0x18,0x7E,0x18,0x18,0x00,0x00},
|
||||||
|
/* 0x2C ',' */ {0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x30},
|
||||||
|
/* 0x2D '-' */ {0x00,0x00,0x00,0x7E,0x00,0x00,0x00,0x00},
|
||||||
|
/* 0x2E '.' */ {0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x00},
|
||||||
|
/* 0x2F '/' */ {0x06,0x0C,0x18,0x30,0x60,0xC0,0x80,0x00},
|
||||||
|
/* 0x30 '0' */ {0x3C,0x66,0x6E,0x76,0x66,0x66,0x3C,0x00},
|
||||||
|
/* 0x31 '1' */ {0x18,0x38,0x18,0x18,0x18,0x18,0x7E,0x00},
|
||||||
|
/* 0x32 '2' */ {0x3C,0x66,0x06,0x1C,0x30,0x66,0x7E,0x00},
|
||||||
|
/* 0x33 '3' */ {0x3C,0x66,0x06,0x1C,0x06,0x66,0x3C,0x00},
|
||||||
|
/* 0x34 '4' */ {0x0C,0x1C,0x3C,0x6C,0x7E,0x0C,0x0C,0x00},
|
||||||
|
/* 0x35 '5' */ {0x7E,0x60,0x7C,0x06,0x06,0x66,0x3C,0x00},
|
||||||
|
/* 0x36 '6' */ {0x1C,0x30,0x60,0x7C,0x66,0x66,0x3C,0x00},
|
||||||
|
/* 0x37 '7' */ {0x7E,0x66,0x0C,0x18,0x18,0x18,0x18,0x00},
|
||||||
|
/* 0x38 '8' */ {0x3C,0x66,0x66,0x3C,0x66,0x66,0x3C,0x00},
|
||||||
|
/* 0x39 '9' */ {0x3C,0x66,0x66,0x3E,0x06,0x0C,0x38,0x00},
|
||||||
|
/* 0x3A ':' */ {0x00,0x18,0x18,0x00,0x00,0x18,0x18,0x00},
|
||||||
|
/* 0x3B ';' */ {0x00,0x18,0x18,0x00,0x00,0x18,0x18,0x30},
|
||||||
|
/* 0x3C '<' */ {0x0C,0x18,0x30,0x60,0x30,0x18,0x0C,0x00},
|
||||||
|
/* 0x3D '=' */ {0x00,0x00,0x7E,0x00,0x7E,0x00,0x00,0x00},
|
||||||
|
/* 0x3E '>' */ {0x30,0x18,0x0C,0x06,0x0C,0x18,0x30,0x00},
|
||||||
|
/* 0x3F '?' */ {0x3C,0x66,0x06,0x0C,0x18,0x00,0x18,0x00},
|
||||||
|
/* 0x40 '@' */ {0x3C,0x66,0x6E,0x6E,0x60,0x62,0x3C,0x00},
|
||||||
|
/* 0x41 'A' */ {0x18,0x3C,0x66,0x66,0x7E,0x66,0x66,0x00},
|
||||||
|
/* 0x42 'B' */ {0x7C,0x66,0x66,0x7C,0x66,0x66,0x7C,0x00},
|
||||||
|
/* 0x43 'C' */ {0x3C,0x66,0x60,0x60,0x60,0x66,0x3C,0x00},
|
||||||
|
/* 0x44 'D' */ {0x78,0x6C,0x66,0x66,0x66,0x6C,0x78,0x00},
|
||||||
|
/* 0x45 'E' */ {0x7E,0x60,0x60,0x78,0x60,0x60,0x7E,0x00},
|
||||||
|
/* 0x46 'F' */ {0x7E,0x60,0x60,0x78,0x60,0x60,0x60,0x00},
|
||||||
|
/* 0x47 'G' */ {0x3C,0x66,0x60,0x6E,0x66,0x66,0x3C,0x00},
|
||||||
|
/* 0x48 'H' */ {0x66,0x66,0x66,0x7E,0x66,0x66,0x66,0x00},
|
||||||
|
/* 0x49 'I' */ {0x3C,0x18,0x18,0x18,0x18,0x18,0x3C,0x00},
|
||||||
|
/* 0x4A 'J' */ {0x1E,0x0C,0x0C,0x0C,0x0C,0x6C,0x38,0x00},
|
||||||
|
/* 0x4B 'K' */ {0x66,0x6C,0x78,0x70,0x78,0x6C,0x66,0x00},
|
||||||
|
/* 0x4C 'L' */ {0x60,0x60,0x60,0x60,0x60,0x60,0x7E,0x00},
|
||||||
|
/* 0x4D 'M' */ {0x63,0x77,0x7F,0x6B,0x63,0x63,0x63,0x00},
|
||||||
|
/* 0x4E 'N' */ {0x66,0x76,0x7E,0x7E,0x6E,0x66,0x66,0x00},
|
||||||
|
/* 0x4F 'O' */ {0x3C,0x66,0x66,0x66,0x66,0x66,0x3C,0x00},
|
||||||
|
/* 0x50 'P' */ {0x7C,0x66,0x66,0x7C,0x60,0x60,0x60,0x00},
|
||||||
|
/* 0x51 'Q' */ {0x3C,0x66,0x66,0x66,0x6A,0x6C,0x36,0x00},
|
||||||
|
/* 0x52 'R' */ {0x7C,0x66,0x66,0x7C,0x6C,0x66,0x66,0x00},
|
||||||
|
/* 0x53 'S' */ {0x3C,0x66,0x60,0x3C,0x06,0x66,0x3C,0x00},
|
||||||
|
/* 0x54 'T' */ {0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x00},
|
||||||
|
/* 0x55 'U' */ {0x66,0x66,0x66,0x66,0x66,0x66,0x3C,0x00},
|
||||||
|
/* 0x56 'V' */ {0x66,0x66,0x66,0x66,0x66,0x3C,0x18,0x00},
|
||||||
|
/* 0x57 'W' */ {0x63,0x63,0x63,0x6B,0x7F,0x77,0x63,0x00},
|
||||||
|
/* 0x58 'X' */ {0x66,0x66,0x3C,0x18,0x3C,0x66,0x66,0x00},
|
||||||
|
/* 0x59 'Y' */ {0x66,0x66,0x66,0x3C,0x18,0x18,0x18,0x00},
|
||||||
|
/* 0x5A 'Z' */ {0x7E,0x06,0x0C,0x18,0x30,0x60,0x7E,0x00},
|
||||||
|
/* 0x5B '[' */ {0x3C,0x30,0x30,0x30,0x30,0x30,0x3C,0x00},
|
||||||
|
/* 0x5C '\\'*/ {0xC0,0x60,0x30,0x18,0x0C,0x06,0x02,0x00},
|
||||||
|
/* 0x5D ']' */ {0x3C,0x0C,0x0C,0x0C,0x0C,0x0C,0x3C,0x00},
|
||||||
|
/* 0x5E '^' */ {0x18,0x3C,0x66,0x00,0x00,0x00,0x00,0x00},
|
||||||
|
/* 0x5F '_' */ {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF},
|
||||||
|
/* 0x60 '`' */ {0x30,0x18,0x0C,0x00,0x00,0x00,0x00,0x00},
|
||||||
|
/* 0x61 'a' */ {0x00,0x00,0x3C,0x06,0x3E,0x66,0x3E,0x00},
|
||||||
|
/* 0x62 'b' */ {0x60,0x60,0x7C,0x66,0x66,0x66,0x7C,0x00},
|
||||||
|
/* 0x63 'c' */ {0x00,0x00,0x3C,0x66,0x60,0x66,0x3C,0x00},
|
||||||
|
/* 0x64 'd' */ {0x06,0x06,0x3E,0x66,0x66,0x66,0x3E,0x00},
|
||||||
|
/* 0x65 'e' */ {0x00,0x00,0x3C,0x66,0x7E,0x60,0x3C,0x00},
|
||||||
|
/* 0x66 'f' */ {0x1C,0x36,0x30,0x78,0x30,0x30,0x30,0x00},
|
||||||
|
/* 0x67 'g' */ {0x00,0x00,0x3E,0x66,0x66,0x3E,0x06,0x3C},
|
||||||
|
/* 0x68 'h' */ {0x60,0x60,0x7C,0x66,0x66,0x66,0x66,0x00},
|
||||||
|
/* 0x69 'i' */ {0x18,0x00,0x38,0x18,0x18,0x18,0x3C,0x00},
|
||||||
|
/* 0x6A 'j' */ {0x06,0x00,0x06,0x06,0x06,0x06,0x66,0x3C},
|
||||||
|
/* 0x6B 'k' */ {0x60,0x60,0x66,0x6C,0x78,0x6C,0x66,0x00},
|
||||||
|
/* 0x6C 'l' */ {0x38,0x18,0x18,0x18,0x18,0x18,0x3C,0x00},
|
||||||
|
/* 0x6D 'm' */ {0x00,0x00,0x66,0x7F,0x7F,0x6B,0x63,0x00},
|
||||||
|
/* 0x6E 'n' */ {0x00,0x00,0x7C,0x66,0x66,0x66,0x66,0x00},
|
||||||
|
/* 0x6F 'o' */ {0x00,0x00,0x3C,0x66,0x66,0x66,0x3C,0x00},
|
||||||
|
/* 0x70 'p' */ {0x00,0x00,0x7C,0x66,0x66,0x7C,0x60,0x60},
|
||||||
|
/* 0x71 'q' */ {0x00,0x00,0x3E,0x66,0x66,0x3E,0x06,0x06},
|
||||||
|
/* 0x72 'r' */ {0x00,0x00,0x7C,0x66,0x60,0x60,0x60,0x00},
|
||||||
|
/* 0x73 's' */ {0x00,0x00,0x3E,0x60,0x3C,0x06,0x7C,0x00},
|
||||||
|
/* 0x74 't' */ {0x18,0x18,0x7E,0x18,0x18,0x18,0x0E,0x00},
|
||||||
|
/* 0x75 'u' */ {0x00,0x00,0x66,0x66,0x66,0x66,0x3E,0x00},
|
||||||
|
/* 0x76 'v' */ {0x00,0x00,0x66,0x66,0x66,0x3C,0x18,0x00},
|
||||||
|
/* 0x77 'w' */ {0x00,0x00,0x63,0x6B,0x7F,0x7F,0x36,0x00},
|
||||||
|
/* 0x78 'x' */ {0x00,0x00,0x66,0x3C,0x18,0x3C,0x66,0x00},
|
||||||
|
/* 0x79 'y' */ {0x00,0x00,0x66,0x66,0x66,0x3E,0x06,0x3C},
|
||||||
|
/* 0x7A 'z' */ {0x00,0x00,0x7E,0x0C,0x18,0x30,0x7E,0x00},
|
||||||
|
/* 0x7B '{' */ {0x0E,0x18,0x18,0x70,0x18,0x18,0x0E,0x00},
|
||||||
|
/* 0x7C '|' */ {0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x00},
|
||||||
|
/* 0x7D '}' */ {0x70,0x18,0x18,0x0E,0x18,0x18,0x70,0x00},
|
||||||
|
/* 0x7E '~' */ {0x76,0xDC,0x00,0x00,0x00,0x00,0x00,0x00}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Internal helpers (alphabetical) -----
|
||||||
|
|
||||||
|
static void drawChar(jlSurfaceT *stage, int16_t x, int16_t y, char ch, uint8_t fg, uint8_t bg) {
|
||||||
|
const uint8_t *glyph;
|
||||||
|
uint8_t row;
|
||||||
|
uint8_t col;
|
||||||
|
uint8_t bits;
|
||||||
|
uint8_t c;
|
||||||
|
uint8_t *stagePixels;
|
||||||
|
|
||||||
|
c = (uint8_t)ch;
|
||||||
|
if (c < FIRST_CHAR || c > LAST_CHAR) {
|
||||||
|
c = FIRST_CHAR; // unknown -> space
|
||||||
|
}
|
||||||
|
glyph = kFont[c - FIRST_CHAR];
|
||||||
|
|
||||||
|
// Chunky fast path: each glyph row is 8 bits == 4 stage bytes
|
||||||
|
// (each byte = 2 pixels in 4bpp packed). Build each byte by
|
||||||
|
// checking 2 bits at a time. Bypasses jlDrawPixel's per-pixel
|
||||||
|
// overhead which dominates text rendering on slow targets.
|
||||||
|
stagePixels = (stage != NULL) ? stage->pixels : NULL;
|
||||||
|
if (stagePixels != NULL && x >= 0 && y >= 0 &&
|
||||||
|
x + (int16_t)GLYPH_W <= (int16_t)SURFACE_WIDTH &&
|
||||||
|
y + (int16_t)GLYPH_H <= (int16_t)SURFACE_HEIGHT &&
|
||||||
|
(x & 1) == 0) {
|
||||||
|
uint16_t rowOff;
|
||||||
|
uint16_t byteX;
|
||||||
|
uint8_t bytePair;
|
||||||
|
uint8_t left;
|
||||||
|
uint8_t right;
|
||||||
|
|
||||||
|
byteX = (uint16_t)(x >> 1);
|
||||||
|
for (row = 0u; row < GLYPH_H; row++) {
|
||||||
|
bits = glyph[row];
|
||||||
|
rowOff = (uint16_t)((y + row) * SURFACE_BYTES_PER_ROW + byteX);
|
||||||
|
for (col = 0u; col < GLYPH_W; col += 2u) {
|
||||||
|
left = (uint8_t)((bits & (uint8_t)(0x80u >> col)) ? fg : bg);
|
||||||
|
right = (uint8_t)((bits & (uint8_t)(0x80u >> (col + 1u))) ? fg : bg);
|
||||||
|
bytePair = (uint8_t)((left << 4) | right);
|
||||||
|
stagePixels[rowOff + (col >> 1)] = bytePair;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
surfaceMarkDirtyRect(stage, x, y, (int16_t)GLYPH_W, (int16_t)GLYPH_H);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Planar / out-of-bounds slow fallback.
|
||||||
|
for (row = 0u; row < GLYPH_H; row++) {
|
||||||
|
bits = glyph[row];
|
||||||
|
for (col = 0u; col < GLYPH_W; col++) {
|
||||||
|
uint8_t pixel;
|
||||||
|
|
||||||
|
pixel = ((bits >> (uint8_t)(7u - col)) & 0x01u) ? fg : bg;
|
||||||
|
jlDrawPixel(stage, (int16_t)(x + (int16_t)col), (int16_t)(y + (int16_t)row), pixel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Draw a char with transparent background: only foreground bits are
|
||||||
|
// written, the surface pixels underneath show through. Chunky fast
|
||||||
|
// path reads each stage byte, modifies set nibbles, writes back.
|
||||||
|
static void drawCharTransparent(jlSurfaceT *stage, int16_t x, int16_t y, char ch, uint8_t fg) {
|
||||||
|
const uint8_t *glyph;
|
||||||
|
uint8_t row;
|
||||||
|
uint8_t col;
|
||||||
|
uint8_t bits;
|
||||||
|
uint8_t c;
|
||||||
|
uint8_t *stagePixels;
|
||||||
|
|
||||||
|
c = (uint8_t)ch;
|
||||||
|
if (c < FIRST_CHAR || c > LAST_CHAR) {
|
||||||
|
return; // unknown -> nothing to draw (transparent)
|
||||||
|
}
|
||||||
|
glyph = kFont[c - FIRST_CHAR];
|
||||||
|
stagePixels = (stage != NULL) ? stage->pixels : NULL;
|
||||||
|
if (stagePixels != NULL && x >= 0 && y >= 0 &&
|
||||||
|
x + (int16_t)GLYPH_W <= (int16_t)SURFACE_WIDTH &&
|
||||||
|
y + (int16_t)GLYPH_H <= (int16_t)SURFACE_HEIGHT &&
|
||||||
|
(x & 1) == 0) {
|
||||||
|
uint16_t rowOff;
|
||||||
|
uint16_t byteX;
|
||||||
|
uint8_t existing;
|
||||||
|
uint8_t leftMask;
|
||||||
|
uint8_t rightMask;
|
||||||
|
uint8_t nibbleFg;
|
||||||
|
|
||||||
|
byteX = (uint16_t)(x >> 1);
|
||||||
|
nibbleFg = (uint8_t)(fg & 0x0Fu);
|
||||||
|
for (row = 0u; row < GLYPH_H; row++) {
|
||||||
|
bits = glyph[row];
|
||||||
|
if (bits == 0u) { continue; } // entire row transparent
|
||||||
|
rowOff = (uint16_t)((y + row) * SURFACE_BYTES_PER_ROW + byteX);
|
||||||
|
for (col = 0u; col < GLYPH_W; col += 2u) {
|
||||||
|
leftMask = (uint8_t)(bits & (uint8_t)(0x80u >> col));
|
||||||
|
rightMask = (uint8_t)(bits & (uint8_t)(0x80u >> (col + 1u)));
|
||||||
|
if (leftMask == 0u && rightMask == 0u) { continue; }
|
||||||
|
existing = stagePixels[rowOff + (col >> 1)];
|
||||||
|
if (leftMask) { existing = (uint8_t)((existing & 0x0Fu) | (nibbleFg << 4)); }
|
||||||
|
if (rightMask) { existing = (uint8_t)((existing & 0xF0u) | nibbleFg); }
|
||||||
|
stagePixels[rowOff + (col >> 1)] = existing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
surfaceMarkDirtyRect(stage, x, y, (int16_t)GLYPH_W, (int16_t)GLYPH_H);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slow fallback (planar or out-of-bounds).
|
||||||
|
for (row = 0u; row < GLYPH_H; row++) {
|
||||||
|
bits = glyph[row];
|
||||||
|
for (col = 0u; col < GLYPH_W; col++) {
|
||||||
|
if (bits & (uint8_t)(0x80u >> col)) {
|
||||||
|
jlDrawPixel(stage, (int16_t)(x + (int16_t)col), (int16_t)(y + (int16_t)row), fg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void drawString(jlSurfaceT *stage, int16_t x, int16_t y, const char *s, uint8_t fg, uint8_t bg) {
|
||||||
|
int16_t cur;
|
||||||
|
|
||||||
|
if (s == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cur = x;
|
||||||
|
while (*s != '\0') {
|
||||||
|
if (cur >= (int16_t)STAGE_W) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
drawChar(stage, cur, y, *s, fg, bg);
|
||||||
|
cur = (int16_t)(cur + (int16_t)GLYPH_W);
|
||||||
|
s++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void drawWindow(jlSurfaceT *stage, int16_t x, int16_t y, uint8_t cols, uint8_t rows, uint8_t bg, uint8_t border) {
|
||||||
|
int16_t w;
|
||||||
|
int16_t h;
|
||||||
|
|
||||||
|
w = (int16_t)((int16_t)cols * (int16_t)GLYPH_W);
|
||||||
|
h = (int16_t)((int16_t)rows * (int16_t)GLYPH_H);
|
||||||
|
jlFillRect(stage, x, y, (uint16_t)(w + 4), (uint16_t)(h + 4), border);
|
||||||
|
jlFillRect(stage, (int16_t)(x + 2), (int16_t)(y + 2), (uint16_t)w, (uint16_t)h, bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void fillRow(jlSurfaceT *stage, uint8_t row, uint8_t bg) {
|
||||||
|
jlFillRect(stage, 0, (int16_t)((int16_t)row * (int16_t)GLYPH_H), (uint16_t)STAGE_W, (uint16_t)GLYPH_H, bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Public API (alphabetical) -----
|
||||||
|
|
||||||
|
const uint16_t *agiTextAsciiMap(void) {
|
||||||
|
// Not used in this impl (we render direct), but expose so callers
|
||||||
|
// who want to use jlDrawText against agiTextFontSurface have the
|
||||||
|
// standard 16x6 layout map. Built lazily on first call.
|
||||||
|
static uint16_t map[256];
|
||||||
|
static bool built;
|
||||||
|
uint16_t i;
|
||||||
|
|
||||||
|
if (built) {
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
for (i = 0u; i < 256u; i++) {
|
||||||
|
if (i < FIRST_CHAR || i > LAST_CHAR) {
|
||||||
|
map[i] = (uint16_t)0xFFFFu;
|
||||||
|
} else {
|
||||||
|
uint8_t off;
|
||||||
|
|
||||||
|
off = (uint8_t)(i - FIRST_CHAR);
|
||||||
|
map[i] = (uint16_t)(((uint16_t)(off / 16u) << 8) | (uint16_t)(off % 16u));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
built = true;
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const jlSurfaceT *agiTextFontSurface(void) {
|
||||||
|
// No surface built in this impl (drawChar walks raw bits direct
|
||||||
|
// to the stage). Reserved for callers that prefer jlDrawText.
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void agiTextRender(const AgiVmT *vm, jlSurfaceT *stage) {
|
||||||
|
uint8_t r;
|
||||||
|
|
||||||
|
if (vm->statusLineOn) {
|
||||||
|
fillRow(stage, vm->statusLineRow, 15u); // white background
|
||||||
|
if (vm->textRows[vm->statusLineRow].text[0] != '\0') {
|
||||||
|
drawString(stage, 0, (int16_t)((int16_t)vm->statusLineRow * (int16_t)GLYPH_H),
|
||||||
|
vm->textRows[vm->statusLineRow].text, 0u, 15u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// display() rows: walk every row that has content. Glyphs draw
|
||||||
|
// transparently (no bg fill) so any PIC art behind shows through
|
||||||
|
// where glyph pixels are 0. Filling the whole row's bg would
|
||||||
|
// obscure picture elements that overlap the text band (e.g.
|
||||||
|
// KQ3's "King's Quest III" title art).
|
||||||
|
for (r = 0u; r < AGI_TEXT_ROWS; r++) {
|
||||||
|
const char *p;
|
||||||
|
int16_t x;
|
||||||
|
int16_t y;
|
||||||
|
uint8_t fg;
|
||||||
|
|
||||||
|
if (r == vm->statusLineRow && vm->statusLineOn) {
|
||||||
|
continue; // status line handled above
|
||||||
|
}
|
||||||
|
if (vm->textRows[r].text[0] == '\0') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
y = (int16_t)((int16_t)r * (int16_t)GLYPH_H);
|
||||||
|
x = 0;
|
||||||
|
fg = vm->textRows[r].fg;
|
||||||
|
p = vm->textRows[r].text;
|
||||||
|
while (*p != '\0' && x < (int16_t)STAGE_W) {
|
||||||
|
if (*p != ' ') {
|
||||||
|
drawCharTransparent(stage, x, y, *p, fg);
|
||||||
|
}
|
||||||
|
x = (int16_t)(x + (int16_t)GLYPH_W);
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vm->printModal.active) {
|
||||||
|
const char *p;
|
||||||
|
int16_t lineW;
|
||||||
|
int16_t maxW;
|
||||||
|
int16_t lineCount;
|
||||||
|
int16_t winX;
|
||||||
|
int16_t winY;
|
||||||
|
int16_t lineH;
|
||||||
|
int16_t textY;
|
||||||
|
int16_t curX;
|
||||||
|
|
||||||
|
// Measure: width = longest line in chars; height = lines.
|
||||||
|
lineW = 0;
|
||||||
|
maxW = 1;
|
||||||
|
lineCount = 1;
|
||||||
|
p = vm->printModal.message;
|
||||||
|
while (*p != '\0') {
|
||||||
|
if (*p == '\n') {
|
||||||
|
if (lineW > maxW) {
|
||||||
|
maxW = lineW;
|
||||||
|
}
|
||||||
|
lineW = 0;
|
||||||
|
lineCount++;
|
||||||
|
} else {
|
||||||
|
lineW++;
|
||||||
|
}
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
if (lineW > maxW) {
|
||||||
|
maxW = lineW;
|
||||||
|
}
|
||||||
|
if (maxW > (int16_t)AGI_TEXT_COLS) {
|
||||||
|
maxW = (int16_t)AGI_TEXT_COLS;
|
||||||
|
}
|
||||||
|
lineH = (int16_t)GLYPH_H;
|
||||||
|
winX = (int16_t)((STAGE_W - (maxW * (int16_t)GLYPH_W + 4)) / 2);
|
||||||
|
winY = (int16_t)((STAGE_H - (lineCount * lineH + 4)) / 2);
|
||||||
|
if (winX < 0) { winX = 0; }
|
||||||
|
if (winY < 0) { winY = 0; }
|
||||||
|
drawWindow(stage, winX, winY, (uint8_t)maxW, (uint8_t)lineCount, 15u, 4u);
|
||||||
|
|
||||||
|
textY = (int16_t)(winY + 2);
|
||||||
|
curX = (int16_t)(winX + 2);
|
||||||
|
p = vm->printModal.message;
|
||||||
|
while (*p != '\0') {
|
||||||
|
if (*p == '\n') {
|
||||||
|
textY = (int16_t)(textY + lineH);
|
||||||
|
curX = (int16_t)(winX + 2);
|
||||||
|
} else {
|
||||||
|
drawChar(stage, curX, textY, *p, 0u, 15u);
|
||||||
|
curX = (int16_t)(curX + (int16_t)GLYPH_W);
|
||||||
|
}
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
340
agiView.c
Normal file
340
agiView.c
Normal file
|
|
@ -0,0 +1,340 @@
|
||||||
|
// AGI v2 VIEW (sprite/animation) decoder.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
#include "agi.h"
|
||||||
|
|
||||||
|
#include "joey/draw.h"
|
||||||
|
#include "surfaceInternal.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.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
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||||
|
segment "AGIVIEW";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Prototypes -----
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Internal helpers (alphabetical) -----
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cel;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Public API (alphabetical) -----
|
||||||
|
|
||||||
|
uint8_t agiActorPriorityForY(int16_t y) {
|
||||||
|
int16_t capped;
|
||||||
|
int16_t band;
|
||||||
|
|
||||||
|
if (y < AGI_PRI_SKY_TOP) {
|
||||||
|
return 4u;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
return (uint8_t)band;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||||
|
segment "AGIDRAW";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
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;
|
||||||
|
const uint8_t *rle;
|
||||||
|
bool mirrored;
|
||||||
|
int16_t width;
|
||||||
|
int16_t height;
|
||||||
|
uint8_t transparent;
|
||||||
|
int16_t topY;
|
||||||
|
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 packed;
|
||||||
|
uint8_t runLen;
|
||||||
|
uint8_t r;
|
||||||
|
uint8_t picPri;
|
||||||
|
uint8_t *stagePixels;
|
||||||
|
|
||||||
|
cel = resolveMirror(view, loopIdx, celIdx, &mirrored);
|
||||||
|
if (cel == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
width = (int16_t)cel->width;
|
||||||
|
height = (int16_t)cel->height;
|
||||||
|
transparent = cel->transparentColor;
|
||||||
|
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 minStageX = (int16_t)(x << 1);
|
||||||
|
int16_t maxStageX = (int16_t)((x + width) << 1);
|
||||||
|
int16_t minStageY = (int16_t)(destY + topY);
|
||||||
|
int16_t maxStageY = (int16_t)(destY + topY + height);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
cx = 0;
|
||||||
|
for (;;) {
|
||||||
|
rleByte = *rle++;
|
||||||
|
if (rleByte == 0u) { break; }
|
||||||
|
color = (uint8_t)(rleByte >> 4);
|
||||||
|
runLen = (uint8_t)(rleByte & 0x0Fu);
|
||||||
|
if (color == transparent) {
|
||||||
|
cx = (int16_t)(cx + runLen);
|
||||||
|
if (cx > width) { cx = width; }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (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 = mirrored ? (int16_t)(width - 1 - cx) : cx;
|
||||||
|
agiX = (int16_t)(x + drawCx);
|
||||||
|
if (agiX >= 0 && agiX < (int16_t)AGI_PIC_WIDTH) {
|
||||||
|
if (priRow[agiX] <= actorPri) {
|
||||||
|
stageRow[agiX] = packed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Dirty-rect tracking at the cel level only: we know the
|
||||||
|
// bbox a priori (the cel's stage rect), and per-pixel tight
|
||||||
|
// bounds aren't worth the inner-loop cost on a 386. Clip to
|
||||||
|
// the stage so the surface marker doesn't reject the whole
|
||||||
|
// rect for an off-screen edge.
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Planar fallback (s->pixels NULL: Amiga Phase 9). Goes through
|
||||||
|
// jlDrawPixel so the port's c2p / plane-sync path stays correct.
|
||||||
|
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 != transparent) {
|
||||||
|
drawCx = mirrored ? (int16_t)(width - 1 - cx) : cx;
|
||||||
|
agiX = (int16_t)(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 = picPriority[agiY * (int16_t)AGI_PIC_WIDTH + agiX];
|
||||||
|
if (picPri <= 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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
277
scripts/run-agi.sh
Executable file
277
scripts/run-agi.sh
Executable file
|
|
@ -0,0 +1,277 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Launch the AGI interpreter in the named platform's emulator with a
|
||||||
|
# staged AGI v2 game directory. The game's data files are copied flat
|
||||||
|
# alongside the binary so AGI's CWD-relative default ("./LOGDIR" etc.)
|
||||||
|
# works on platforms whose autostart can't pass argv.
|
||||||
|
#
|
||||||
|
# scripts/run-agi.sh <platform> [game] [room]
|
||||||
|
# platform : dos | amiga | atarist | iigs
|
||||||
|
# game : subdirectory under examples/agi/gamedata/ (default kq3)
|
||||||
|
# room : starting room number 1..255. Skips logic.0's hardcoded
|
||||||
|
# title-room target and jumps straight to the named room.
|
||||||
|
# Only forwarded to DOS today -- the Atari ST / Amiga
|
||||||
|
# autostart paths can't pass argv to the binary; their
|
||||||
|
# AGI starts at the title screen until we wire a stage-
|
||||||
|
# time config-file fallback or implement enough title
|
||||||
|
# opcodes for the title to advance on its own.
|
||||||
|
#
|
||||||
|
# Required files in the game directory (others are ignored):
|
||||||
|
# LOGDIR PICDIR VIEWDIR SNDDIR VOL.0 [VOL.1 ...] [OBJECT] [WORDS.TOK]
|
||||||
|
#
|
||||||
|
# IIgs is not yet wired (needs the disk packager extended to a larger
|
||||||
|
# 2mg volume plus case-folded ProDOS file names); the script reports
|
||||||
|
# what's missing rather than launching.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ $# -lt 1 || $# -gt 3 ]]; then
|
||||||
|
echo "usage: $0 <dos|amiga|atarist|iigs> [game-name] [starting-room]" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
platform=$1
|
||||||
|
game=${2:-kq3}
|
||||||
|
room=${3:-0}
|
||||||
|
|
||||||
|
if ! [[ $room =~ ^[0-9]+$ ]] || (( room > 255 )); then
|
||||||
|
echo "starting-room must be 0..255 (got '$room')" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
gamedata=$repo/examples/agi/gamedata/$game
|
||||||
|
support=$repo/toolchains/emulators/support
|
||||||
|
|
||||||
|
if [[ ! -d $gamedata ]]; then
|
||||||
|
echo "$gamedata: not an AGI v2 game directory" >&2
|
||||||
|
if [[ -d $repo/examples/agi/gamedata ]]; then
|
||||||
|
echo "available games:" >&2
|
||||||
|
find "$repo/examples/agi/gamedata" -maxdepth 1 -mindepth 1 -type d -printf ' %f\n' >&2
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ ! -f $gamedata/LOGDIR ]]; then
|
||||||
|
echo "$gamedata: no LOGDIR -- not an AGI v2 game directory" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# stageGameData WORKDIR
|
||||||
|
# Copies the subset of files AGI v2 reads (DIR files, VOL.x, OBJECT,
|
||||||
|
# WORDS.TOK) into WORKDIR. Leaves Sierra's original AGI binary, OVL
|
||||||
|
# graphics drivers, and platform-specific launchers behind so a flat
|
||||||
|
# stage doesn't clash with our binary's name on case-insensitive
|
||||||
|
# filesystems (e.g. Amiga FFS: "AGI" vs our "Agi").
|
||||||
|
stageGameData() {
|
||||||
|
local destDir=$1
|
||||||
|
local f
|
||||||
|
|
||||||
|
for f in LOGDIR PICDIR VIEWDIR SNDDIR OBJECT WORDS.TOK; do
|
||||||
|
if [[ -f $gamedata/$f ]]; then
|
||||||
|
cp "$gamedata/$f" "$destDir/$f"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
for f in "$gamedata"/VOL.*; do
|
||||||
|
[[ -f $f ]] && cp "$f" "$destDir/$(basename "$f")"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
runDos() {
|
||||||
|
local bin=$repo/build/dos/bin/AGI.EXE
|
||||||
|
local conf=$repo/scripts/dosbox-386sx16.conf
|
||||||
|
local work
|
||||||
|
|
||||||
|
if [[ ! -f $bin ]]; then
|
||||||
|
echo "$bin not built. Run 'make -f make/dos.mk EXAMPLE=agi' first." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sweep any leftover staging dirs from previous runs that
|
||||||
|
# didn't reach their trap (DOSBox window close, SIGKILL, etc.).
|
||||||
|
# We're about to make a new one anyway; freshly leftover dirs
|
||||||
|
# serve no purpose and just leak ~1 MB each.
|
||||||
|
rm -rf /tmp/joeylib-agi-dos.* 2>/dev/null || true
|
||||||
|
rm -f /tmp/joeylib-agi-dos-last.log 2>/dev/null || true
|
||||||
|
|
||||||
|
work=$(mktemp -d -t joeylib-agi-dos.XXXXXX)
|
||||||
|
# Cleanup trap: copy DJGPP's diagnostic log out (DOS uppercases
|
||||||
|
# so the file lands as JOEYLOG.TXT, not joeylog.txt), then nuke
|
||||||
|
# the staging dir. Hook EXIT plus the common interrupt signals
|
||||||
|
# so the cleanup fires even when the user closes the DOSBox
|
||||||
|
# window or Ctrl-Cs the terminal -- both bypass plain EXIT in
|
||||||
|
# some bash configurations.
|
||||||
|
cleanupDos() {
|
||||||
|
local log
|
||||||
|
for log in "$work/JOEYLOG.TXT" "$work/joeylog.txt"; do
|
||||||
|
if [[ -f $log ]]; then
|
||||||
|
cp "$log" /tmp/joeylib-agi-dos-last.log
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
rm -rf "$work"
|
||||||
|
}
|
||||||
|
trap cleanupDos EXIT INT TERM HUP
|
||||||
|
|
||||||
|
cp "$bin" "$work/AGI.EXE"
|
||||||
|
stageGameData "$work"
|
||||||
|
|
||||||
|
# AGI.EXE reads game data from its CWD (we mount $work as C: and
|
||||||
|
# cd there first), so argv[1] is "." -- the starting-room override
|
||||||
|
# lives in argv[2]. Skip the argv when room=0 so the binary
|
||||||
|
# exercises its default code path.
|
||||||
|
local agi_cmd="AGI.EXE"
|
||||||
|
if (( room != 0 )); then
|
||||||
|
agi_cmd="AGI.EXE . $room"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
DOSBox: mounting $work as C:, running '$agi_cmd' for $game.
|
||||||
|
ESC quits the demo, then 'pause' waits for a keypress in the shell.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
local dosboxArgs=(
|
||||||
|
-conf "$conf"
|
||||||
|
-set "mouse_capture=seamless"
|
||||||
|
)
|
||||||
|
# CYCLES=max (or any other DOSBox cycles value) overrides the conf's
|
||||||
|
# period-correct 386SX-16 cap. Use this to A/B test whether perf
|
||||||
|
# ceilings are coming from the simulated CPU or the code itself.
|
||||||
|
if [[ -n "${CYCLES:-}" ]]; then
|
||||||
|
dosboxArgs+=(-set "cpu cycles=$CYCLES")
|
||||||
|
fi
|
||||||
|
dosboxArgs+=(
|
||||||
|
-c "C:"
|
||||||
|
-c "$agi_cmd"
|
||||||
|
-c "pause"
|
||||||
|
--exit "$work"
|
||||||
|
)
|
||||||
|
# Avoid `exec`: we need bash to stay alive so the EXIT trap above
|
||||||
|
# actually runs and removes the staged $work dir. With `exec`,
|
||||||
|
# dosbox replaces the shell and the trap never fires, leaking
|
||||||
|
# ~1 MB of game data into /tmp on every launch.
|
||||||
|
dosbox "${dosboxArgs[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
runAtariST() {
|
||||||
|
local bin=$repo/build/atarist/bin/AGI.PRG
|
||||||
|
local tos=$support/emutos-512k.img
|
||||||
|
local work
|
||||||
|
|
||||||
|
if [[ ! -f $bin ]]; then
|
||||||
|
echo "$bin not built. Run 'make -f make/atarist.mk EXAMPLE=agi' first." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ ! -f $tos ]]; then
|
||||||
|
echo "TOS ROM missing: $tos" >&2
|
||||||
|
echo "Run ./toolchains/install.sh (EmuTOS should have been staged)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf /tmp/joeylib-agi-atarist.* 2>/dev/null || true
|
||||||
|
work=$(mktemp -d -t joeylib-agi-atarist.XXXXXX)
|
||||||
|
trap 'rm -rf "$work"' EXIT INT TERM HUP
|
||||||
|
|
||||||
|
cp "$bin" "$work/AGI.PRG"
|
||||||
|
stageGameData "$work"
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
Hatari: mounting $work as GEMDOS C:, autostarting AGI.PRG for $game.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Run hatari as a child so the EXIT trap removes the staged
|
||||||
|
# game data dir; `exec` would have hatari replace bash and
|
||||||
|
# leak $work into /tmp on every launch.
|
||||||
|
hatari \
|
||||||
|
--tos "$tos" \
|
||||||
|
--harddrive "$work" \
|
||||||
|
--gemdos-drive C \
|
||||||
|
--auto "C:\\AGI.PRG"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
runAmiga() {
|
||||||
|
local bin=$repo/build/amiga/bin/Agi
|
||||||
|
local kickstart=$support/kickstart.rom
|
||||||
|
local workbench=$support/workbench.adf
|
||||||
|
local work
|
||||||
|
|
||||||
|
if [[ ! -f $bin ]]; then
|
||||||
|
echo "$bin not built. Run 'make -f make/amiga.mk EXAMPLE=agi' first." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf /tmp/joeylib-agi-amiga.* 2>/dev/null || true
|
||||||
|
work=$(mktemp -d -t joeylib-agi-amiga.XXXXXX)
|
||||||
|
trap 'rm -rf "$work"' EXIT INT TERM HUP
|
||||||
|
|
||||||
|
mkdir -p "$work/s"
|
||||||
|
cp "$bin" "$work/Agi"
|
||||||
|
stageGameData "$work"
|
||||||
|
# ':' prefix anchors to volume root so AmigaDOS doesn't search C:.
|
||||||
|
echo ":Agi" > "$work/s/startup-sequence"
|
||||||
|
|
||||||
|
local fsargs=(
|
||||||
|
--amiga_model=A500
|
||||||
|
--fast_memory=2048
|
||||||
|
--hard_drive_0="$work"
|
||||||
|
--hard_drive_0_label=JOEYLIB
|
||||||
|
--automatic_input_grab=0
|
||||||
|
--middle_click_ungrab=1
|
||||||
|
--mouse_integration=1
|
||||||
|
--floppy_drive_speed=800
|
||||||
|
)
|
||||||
|
if [[ -f $kickstart ]]; then
|
||||||
|
fsargs+=(--kickstart_file="$kickstart")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f $workbench ]]; then
|
||||||
|
# If a Workbench floppy is staged it boots from DF0: instead of
|
||||||
|
# the HD, so the startup-sequence never fires. Skip Workbench
|
||||||
|
# for AGI to keep the launch one-shot; user can still launch
|
||||||
|
# manually with run-amiga.sh if they want the GUI.
|
||||||
|
echo "(skipping workbench.adf so the JOEYLIB HD boots Agi directly)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
FS-UAE booting JOEYLIB hard drive ($work) and auto-running Agi for $game.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
fs-uae "${fsargs[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
runIigs() {
|
||||||
|
cat <<EOF >&2
|
||||||
|
IIgs AGI run is not wired up yet.
|
||||||
|
|
||||||
|
The existing IIgs disk packager (toolchains/iigs/package-disk.sh) builds
|
||||||
|
joey.2mg from build/iigs/bin/* only; it doesn't accept arbitrary game
|
||||||
|
data, the disk size is fixed at 800K (KQ3's ~1.5 MB of resources won't
|
||||||
|
fit), and ProDOS case-folding for game-data file names has not been
|
||||||
|
audited.
|
||||||
|
|
||||||
|
To wire this up the package-disk.sh tool needs a larger volume option
|
||||||
|
and an "extra data" path argument, and run-iigs.sh needs an AGI mode
|
||||||
|
that points GSplus at the resulting 2mg. Tracked separately.
|
||||||
|
|
||||||
|
Workaround for now: build with 'make -f make/iigs.mk EXAMPLE=agi' to
|
||||||
|
prove the IIgs link still works, but launch via run-iigs.sh and pick
|
||||||
|
JOEYLIB:AGI in Finder -- it will fail to find LOGDIR (no game data on
|
||||||
|
disk), which is the expected current state.
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
case "$platform" in
|
||||||
|
dos) runDos ;;
|
||||||
|
atarist) runAtariST ;;
|
||||||
|
amiga) runAmiga ;;
|
||||||
|
iigs) runIigs ;;
|
||||||
|
*)
|
||||||
|
echo "$platform: unknown platform (expected dos, amiga, atarist, or iigs)" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
169
scripts/test-agi.sh
Executable file
169
scripts/test-agi.sh
Executable file
|
|
@ -0,0 +1,169 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Host-side regression tests for the AGI port.
|
||||||
|
#
|
||||||
|
# Compiles tests/agi/testAgiRes.c + examples/agi/agiRes.c with the
|
||||||
|
# host gcc and runs it against every game directory in
|
||||||
|
# examples/agi/gamedata/. Each game is exercised in isolation; the
|
||||||
|
# script aggregates pass/fail counts and exits non-zero if any test
|
||||||
|
# fails.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/test-agi.sh # test every subdir of gamedata/
|
||||||
|
# scripts/test-agi.sh kq3 agidemo # test only the named subdirs
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
gamedata=$repo/examples/agi/gamedata
|
||||||
|
build=$repo/build/tests
|
||||||
|
testRes=$build/testAgiRes
|
||||||
|
testPic=$build/testAgiPic
|
||||||
|
testView=$build/testAgiView
|
||||||
|
testVm=$build/testAgiVm
|
||||||
|
testPipeline=$build/testAgiPipeline
|
||||||
|
|
||||||
|
mkdir -p "$build"
|
||||||
|
|
||||||
|
# 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 "$repo/include" -I "$repo/examples/agi" -I "$repo/src/core")
|
||||||
|
stubSrc=$build/hostStubs.c
|
||||||
|
cat > "$stubSrc" <<'EOF'
|
||||||
|
#include "joey/draw.h"
|
||||||
|
#include "joey/core.h"
|
||||||
|
#include "surfaceInternal.h"
|
||||||
|
#include <stdlib.h>
|
||||||
|
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 surfaceMarkDirtyRect(const jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h) { (void)s; (void)x; (void)y; (void)w; (void)h; }
|
||||||
|
void surfaceMarkDirtyAll(const jlSurfaceT *s) { (void)s; }
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Building host tests..."
|
||||||
|
gcc "${hostCflags[@]}" \
|
||||||
|
"$repo/tests/agi/testAgiRes.c" \
|
||||||
|
"$repo/examples/agi/agiRes.c" \
|
||||||
|
-o "$testRes"
|
||||||
|
|
||||||
|
gcc "${hostCflags[@]}" \
|
||||||
|
"$repo/tests/agi/testAgiPic.c" \
|
||||||
|
"$repo/examples/agi/agiRes.c" \
|
||||||
|
"$repo/examples/agi/agiPic.c" \
|
||||||
|
"$stubSrc" \
|
||||||
|
-o "$testPic"
|
||||||
|
|
||||||
|
gcc "${hostCflags[@]}" \
|
||||||
|
"$repo/tests/agi/testAgiView.c" \
|
||||||
|
"$repo/examples/agi/agiRes.c" \
|
||||||
|
"$repo/examples/agi/agiPic.c" \
|
||||||
|
"$repo/examples/agi/agiView.c" \
|
||||||
|
"$stubSrc" \
|
||||||
|
-o "$testView"
|
||||||
|
|
||||||
|
gcc "${hostCflags[@]}" \
|
||||||
|
"$repo/tests/agi/testAgiVm.c" \
|
||||||
|
"$repo/examples/agi/agiRes.c" \
|
||||||
|
"$repo/examples/agi/agiVm.c" \
|
||||||
|
"$repo/examples/agi/agiObj.c" \
|
||||||
|
"$stubSrc" \
|
||||||
|
-o "$testVm"
|
||||||
|
|
||||||
|
gcc "${hostCflags[@]}" \
|
||||||
|
"$repo/tests/agi/testAgiPipeline.c" \
|
||||||
|
"$repo/examples/agi/agiRes.c" \
|
||||||
|
"$repo/examples/agi/agiVm.c" \
|
||||||
|
"$repo/examples/agi/agiObj.c" \
|
||||||
|
"$stubSrc" \
|
||||||
|
-o "$testPipeline"
|
||||||
|
|
||||||
|
# Pick the game directories to exercise. With no args, every
|
||||||
|
# subdirectory of examples/agi/gamedata 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
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${#games[@]} -eq 0 ]]; then
|
||||||
|
echo
|
||||||
|
echo "No AGI game directories found under $gamedata"
|
||||||
|
echo "Drop an AGI v2 game in examples/agi/gamedata/<name>/ and rerun."
|
||||||
|
echo "See examples/agi/gamedata/README.md for the required files."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
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"
|
||||||
|
failed=$((failed + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
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
|
||||||
|
passed=$((passed + 1))
|
||||||
|
else
|
||||||
|
failed=$((failed + 1))
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
done
|
||||||
|
|
||||||
|
total=$((passed + failed))
|
||||||
|
echo "===== summary: $passed/$total passed ====="
|
||||||
|
if [[ $failed -gt 0 ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
413
tests/disasmAgi.c
Normal file
413
tests/disasmAgi.c
Normal file
|
|
@ -0,0 +1,413 @@
|
||||||
|
// 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, argv[1])) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
191
tests/testAgiPic.c
Normal file
191
tests/testAgiPic.c
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
// Host-side test for examples/agi/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, argv[1])) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
284
tests/testAgiPipeline.c
Normal file
284
tests/testAgiPipeline.c
Normal file
|
|
@ -0,0 +1,284 @@
|
||||||
|
// 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, argv[1])) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
101
tests/testAgiRes.c
Normal file
101
tests/testAgiRes.c
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
// Host-side regression test for examples/agi/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, argv[1])) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
198
tests/testAgiView.c
Normal file
198
tests/testAgiView.c
Normal file
|
|
@ -0,0 +1,198 @@
|
||||||
|
// Host-side test for examples/agi/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, argv[1])) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
135
tests/testAgiVm.c
Normal file
135
tests/testAgiVm.c
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
// Host-side test for examples/agi/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, argv[1])) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue