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