Optimized a bit better. Examples are a mess of broken crap.

This commit is contained in:
Scott Duensing 2026-05-27 20:22:24 -05:00
parent ad06ee59b7
commit 087bc77a6b
215 changed files with 23190 additions and 3746 deletions

3
.gitignore vendored
View file

@ -45,3 +45,6 @@ Thumbs.db
# Crap I added
stuff/*
docs/*
# AGI game data (Sierra/fan-made AGI games used for testing; never committed)
examples/agi/gamedata/

218
AGI-SESSION-HANDOFF.md Normal file
View 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.

64
PERF.md
View file

@ -1,36 +1,36 @@
| Op | IIgs (ops/sec) | Amiga (vs IIGS) | Atari ST (vs IIGS) | DOS (vs IIGS) |
| --- | --- | --- | --- | --- |
| surfaceClear | 28 | 2.54x | 1.11x | 638.57x |
| paletteSet | 678 | 8.68x | 4.23x | 26.13x |
| scbSetRange | 1005 | 3.66x | 1.86x | 11.16x |
| drawPixel | 1755 | 1.85x | 1.01x | 9.70x |
| drawLine H | 682 | 2.19x | 1.22x | 19.26x |
| drawLine V | 90 | 1.67x | 1.11x | 115.30x |
| drawLine diag | 35 | 1.14x | 1.06x | 261.49x |
| drawRect 100x100 | 75 | 1.57x | 1.04x | 250.77x |
| drawCircle r=16 | 232 | 1.21x | **0.65x** | 71.24x |
| drawCircle r=80 | 56 | 1.16x | **0.61x** | 310.93x |
| fillRect 16x16 | 450 | 1.24x | 1.04x | 39.38x |
| fillRect 80x80 | 75 | **0.95x** | 1.28x | 206.73x |
| fillRect 320x200 | 60 | **0.93x** | **0.43x** | 184.62x |
| fillCircle r=40 | 38 | **0.97x** | 1.39x | 347.24x |
| samplePixel | 1916 | 3.48x | 1.92x | 18.04x |
| tileFill | 1252 | 1.73x | **0.93x** | 10.44x |
| tileCopy | 997 | 1.80x | 1.02x | 16.96x |
| tileCopyMasked | 498 | 1.76x | 1.08x | 28.55x |
| tilePaste | 1106 | 1.94x | 1.09x | 13.53x |
| tileSnap | 1473 | 2.26x | 1.28x | 9.44x |
| spriteSaveUnder | 528 | 2.21x | 1.29x | 20.14x |
| spriteDraw | 438 | 1.82x | 1.28x | 38.39x |
| spriteRestoreUnder | 487 | 2.00x | 1.11x | 36.71x |
| spriteSaveAndDraw | 277 | 1.82x | 1.06x | 72.79x |
| stagePresent full | 42 | 6.31x | 1.40x | 373.83x |
| joeyInputPoll | 273 | 13.90x | 4.19x | 57.15x |
| joeyKeyDown | 3382 | 7.82x | 3.76x | 18.93x |
| joeyKeyPressed | 3345 | 7.65x | 3.81x | 18.72x |
| joeyMouseX | 4170 | 10.26x | 5.22x | 26.27x |
| jlSurfaceClear | 28 | 2.54x | 1.11x | 638.57x |
| jlPaletteSet | 678 | 8.68x | 4.23x | 26.13x |
| jlScbSetRange | 1005 | 3.66x | 1.86x | 11.16x |
| jlDrawPixel | 1755 | 1.85x | 1.01x | 9.70x |
| jlDrawLine H | 682 | 2.19x | 1.22x | 19.26x |
| jlDrawLine V | 90 | 1.67x | 1.11x | 115.30x |
| jlDrawLine diag | 35 | 1.14x | 1.06x | 261.49x |
| jlDrawRect 100x100 | 75 | 1.57x | 1.04x | 250.77x |
| jlDrawCircle r=16 | 232 | 1.21x | **0.65x** | 71.24x |
| jlDrawCircle r=80 | 56 | 1.16x | **0.61x** | 310.93x |
| jlFillRect 16x16 | 450 | 1.24x | 1.04x | 39.38x |
| jlFillRect 80x80 | 75 | **0.95x** | 1.28x | 206.73x |
| jlFillRect 320x200 | 60 | **0.93x** | **0.43x** | 184.62x |
| jlFillCircle r=40 | 38 | **0.97x** | 1.39x | 347.24x |
| jlSamplePixel | 1916 | 3.48x | 1.92x | 18.04x |
| jlTileFill | 1252 | 1.73x | **0.93x** | 10.44x |
| jlTileCopy | 997 | 1.80x | 1.02x | 16.96x |
| jlTileCopyMasked | 498 | 1.76x | 1.08x | 28.55x |
| jlTilePaste | 1106 | 1.94x | 1.09x | 13.53x |
| jlTileSnap | 1473 | 2.26x | 1.28x | 9.44x |
| jlSpriteSaveUnder | 528 | 2.21x | 1.29x | 20.14x |
| jlSpriteDraw | 438 | 1.82x | 1.28x | 38.39x |
| jlSpriteRestoreUnder | 487 | 2.00x | 1.11x | 36.71x |
| jlSpriteSaveAndDraw | 277 | 1.82x | 1.06x | 72.79x |
| jlStagePresent full | 42 | 6.31x | 1.40x | 373.83x |
| jlInputPoll | 273 | 13.90x | 4.19x | 57.15x |
| jlKeyDown | 3382 | 7.82x | 3.76x | 18.93x |
| jlKeyPressed | 3345 | 7.65x | 3.81x | 18.72x |
| jlMouseX | 4170 | 10.26x | 5.22x | 26.27x |
| joeyJoyConnected | 3378 | 7.76x | 3.71x | 18.95x |
| joeyAudioFrameTick | 4106 | 7.99x | 3.04x | 12.37x |
| joeyAudioIsPlayingMod | 3536 | 9.28x | 4.33x | 26.78x |
| surfaceMarkDirtyRect (via fillRect 32x32) | 240 | 1.38x | 1.26x | 58.95x |
| jlAudioFrameTick | 4106 | 7.99x | 3.04x | 12.37x |
| jlAudioIsPlayingMod | 3536 | 9.28x | 4.33x | 26.78x |
| surfaceMarkDirtyRect (via jlFillRect 32x32) | 240 | 1.38x | 1.26x | 58.95x |
| c2p full screen | - | - | - | - |

274
README.md
View file

@ -50,7 +50,7 @@ src/core/ portable library code
src/codegen/ runtime sprite codegen (per-CPU emitters)
src/port/<plat>/ per-platform HAL implementations
src/shared68k/ assembly shared by Amiga and Atari ST
tools/joeyasset/ sprite and tile asset converter
tools/assetbake/ PNG -> native .tbk / .spr baker (Python)
tools/joeymod/ Protracker .MOD converter (passthrough or .NTP)
examples/ example programs
toolchains/ self-contained cross-build tools
@ -79,18 +79,17 @@ typedef struct {
uint32_t codegenBytes; // runtime compiled-sprite cache size
uint16_t maxSurfaces; // maximum concurrent surfaces
uint32_t audioBytes; // audio sample / module RAM pool
uint32_t assetBytes; // tileset / sprite / map RAM pool
} JoeyConfigT;
} jlConfigT;
bool joeyInit (const JoeyConfigT *config);
void joeyShutdown (void);
const char *joeyLastError (void);
const char *joeyPlatformName (void);
const char *joeyVersionString(void);
bool jlInit (const jlConfigT *config);
void jlShutdown (void);
const char *jlLastError (void);
const char *jlPlatformName (void);
const char *jlVersionString(void);
void joeyWaitVBL (void); // block until next VBL
uint16_t joeyFrameCount (void); // monotonic 16-bit frame counter
uint16_t joeyFrameHz (void); // 50 / 60 / 70 depending on port
void jlWaitVBL (void); // block until next VBL
uint16_t jlFrameCount (void); // monotonic 16-bit frame counter
uint16_t jlFrameHz (void); // 50 / 60 / 70 depending on port
```
@ -111,19 +110,19 @@ hides the storage format.
#define SURFACE_PALETTE_COUNT 16
#define SURFACE_COLORS_PER_PALETTE 16
typedef struct SurfaceT SurfaceT; // opaque
typedef struct jlSurfaceT jlSurfaceT; // opaque
SurfaceT *surfaceCreate (void);
void surfaceDestroy(SurfaceT *s);
SurfaceT *stageGet (void); // library back-buffer
void surfaceCopy (SurfaceT *dst, const SurfaceT *src);
jlSurfaceT *jlSurfaceCreate (void);
void jlSurfaceDestroy(jlSurfaceT *s);
jlSurfaceT *jlStageGet (void); // library back-buffer
void jlSurfaceCopy (jlSurfaceT *dst, const jlSurfaceT *src);
bool surfaceSaveFile(const SurfaceT *src, const char *path);
bool surfaceLoadFile(SurfaceT *dst, const char *path);
uint32_t surfaceHash (const SurfaceT *s); // FNV-1a of logical pixels
bool jlSurfaceSaveFile(const jlSurfaceT *src, const char *path);
bool jlSurfaceLoadFile(jlSurfaceT *dst, const char *path);
uint32_t jlSurfaceHash (const jlSurfaceT *s); // FNV-1a of logical pixels
```
`surfaceSaveFile` writes the surface in **target-native** form. Files
`jlSurfaceSaveFile` writes the surface in **target-native** form. Files
are NOT cross-port portable; the asset pipeline handles conversion.
@ -134,51 +133,48 @@ no-ops. Color 0 is plotted normally (use the masked variants if you
need transparency).
```c
void surfaceClear (SurfaceT *s, uint8_t color);
void drawPixel (SurfaceT *s, int16_t x, int16_t y, uint8_t color);
uint8_t samplePixel (const SurfaceT *s, int16_t x, int16_t y);
void jlSurfaceClear (jlSurfaceT *s, uint8_t color);
void jlDrawPixel (jlSurfaceT *s, int16_t x, int16_t y, uint8_t color);
uint8_t jlSamplePixel (const jlSurfaceT *s, int16_t x, int16_t y);
void drawLine (SurfaceT *s, int16_t x0, int16_t y0,
void jlDrawLine (jlSurfaceT *s, int16_t x0, int16_t y0,
int16_t x1, int16_t y1, uint8_t color);
void drawRect (SurfaceT *s, int16_t x, int16_t y,
void jlDrawRect (jlSurfaceT *s, int16_t x, int16_t y,
uint16_t w, uint16_t h, uint8_t color);
void fillRect (SurfaceT *s, int16_t x, int16_t y,
void jlFillRect (jlSurfaceT *s, int16_t x, int16_t y,
uint16_t w, uint16_t h, uint8_t color);
void drawCircle (SurfaceT *s, int16_t cx, int16_t cy,
void jlDrawCircle (jlSurfaceT *s, int16_t cx, int16_t cy,
uint16_t r, uint8_t color);
void fillCircle (SurfaceT *s, int16_t cx, int16_t cy,
void jlFillCircle (jlSurfaceT *s, int16_t cx, int16_t cy,
uint16_t r, uint8_t color);
void floodFill (SurfaceT *s, int16_t x, int16_t y, uint8_t newColor);
void floodFillBounded (SurfaceT *s, int16_t x, int16_t y,
void jlFloodFill (jlSurfaceT *s, int16_t x, int16_t y, uint8_t newColor);
void jlFloodFillBounded (jlSurfaceT *s, int16_t x, int16_t y,
uint8_t newColor, uint8_t boundaryColor);
void surfaceBlit (SurfaceT *dst, const JoeyAssetT *src, int16_t x, int16_t y);
void surfaceBlitMasked (SurfaceT *dst, const JoeyAssetT *src,
int16_t x, int16_t y, uint8_t transparentIndex);
```
### Palette and SCB (`joey/palette.h`)
Colors are 12-bit `$0RGB`. Color 0 of every palette is forced to
black on `paletteSet`. Each scanline picks one of the 16 palettes
black on `jlPaletteSet`. Each scanline picks one of the 16 palettes
via the SCB.
```c
void paletteSet (SurfaceT *s, uint8_t paletteIndex, const uint16_t *colors16);
void paletteGet (const SurfaceT *s, uint8_t paletteIndex, uint16_t *out16);
void scbSet (SurfaceT *s, uint16_t line, uint8_t paletteIndex);
void scbSetRange (SurfaceT *s, uint16_t firstLine, uint16_t lastLine,
void jlPaletteSet (jlSurfaceT *s, uint8_t paletteIndex, const uint16_t *colors16);
void jlPaletteGet (const jlSurfaceT *s, uint8_t paletteIndex, uint16_t *out16);
void jlScbSet (jlSurfaceT *s, uint16_t line, uint8_t paletteIndex);
void jlScbSetRange (jlSurfaceT *s, uint16_t firstLine, uint16_t lastLine,
uint8_t paletteIndex);
uint8_t scbGet (const SurfaceT *s, uint16_t line);
uint8_t jlScbGet (const jlSurfaceT *s, uint16_t line);
```
### Tiles (`joey/tile.h`)
A "tile" is just an 8x8-aligned region of any surface. The API moves
32-byte chunks between surfaces and provides a small `TileT` value
32-byte chunks between surfaces and provides a small `jlTileT` value
type so callers can stash a copy without allocating a scratch surface.
```c
@ -189,19 +185,29 @@ type so callers can stash a copy without allocating a scratch surface.
#define TILE_BLOCKS_PER_COL (SURFACE_HEIGHT / TILE_PIXELS_PER_SIDE) // 25
#define TILE_NO_GLYPH ((uint16_t)0xFFFFu)
typedef struct TileT { uint8_t pixels[TILE_BYTES]; } TileT;
typedef struct jlTileT { uint8_t pixels[TILE_BYTES]; } jlTileT;
void tileCopy (SurfaceT *dst, uint8_t dstBx, uint8_t dstBy,
const SurfaceT *src, uint8_t srcBx, uint8_t srcBy);
void tileCopyMasked (SurfaceT *dst, uint8_t dstBx, uint8_t dstBy,
const SurfaceT *src, uint8_t srcBx, uint8_t srcBy,
void jlTileCopy (jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy,
const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy);
void jlTileCopyMasked (jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy,
const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy,
uint8_t transparentIndex);
void tileFill (SurfaceT *s, uint8_t bx, uint8_t by, uint8_t color);
void tileSnap (const SurfaceT *src, uint8_t bx, uint8_t by, TileT *out);
void tilePaste (SurfaceT *dst, uint8_t bx, uint8_t by, const TileT *in);
void jlTileFill (jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t color);
void jlTileSnap (const jlSurfaceT *src, uint8_t bx, uint8_t by, jlTileT *out);
void jlTilePaste (jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in);
void jlTilePasteMono (jlSurfaceT *dst, uint8_t bx, uint8_t by,
const jlTileT *in, uint8_t fgColor, uint8_t bgColor);
void drawText (SurfaceT *dst, uint8_t bx, uint8_t by,
const SurfaceT *fontSurface, const uint16_t *asciiMap,
// Load up to maxTiles tiles from a baked .tbk file (per-target planar
// bytes from tools/assetbake/assetbake.py --type tile --target ...).
// Refuses files baked for the wrong target. outValid (optional) marks
// each loaded slot; outPalette (optional) receives the embedded
// 16-entry $0RGB palette if present.
uint16_t jlTileBankLoad(const char *path, jlTileT *outTiles, uint16_t maxTiles,
bool *outValid, uint16_t *outPalette);
void jlDrawText (jlSurfaceT *dst, uint8_t bx, uint8_t by,
const jlSurfaceT *fontSurface, const uint16_t *asciiMap,
const char *str);
```
@ -214,78 +220,96 @@ bytes, tile-major 4bpp packed. Sprites can be runtime-compiled
into per-shift code variants for fast draws.
```c
typedef struct SpriteT SpriteT; // opaque
typedef struct jlSpriteT jlSpriteT; // opaque
typedef struct {
SpriteT *sprite;
jlSpriteT *sprite;
int16_t x, y;
uint16_t width, height; // pixels
uint8_t *bytes; // caller-owned save-under buffer
uint16_t sizeBytes;
} SpriteBackupT;
} jlSpriteBackupT;
SpriteT *spriteCreate (const uint8_t *tileData,
jlSpriteT *jlSpriteCreate (const uint8_t *tileData,
uint8_t widthTiles, uint8_t heightTiles);
SpriteT *spriteCreateFromSurface (const SurfaceT *src, int16_t x, int16_t y,
jlSpriteT *jlSpriteCreateFromSurface (const jlSurfaceT *src, int16_t x, int16_t y,
uint8_t widthTiles, uint8_t heightTiles);
SpriteT *spriteLoadFile (const char *path);
SpriteT *spriteFromCompiledMem (const uint8_t *data, uint32_t length);
bool spriteSaveFile (SpriteT *sp, const char *path);
void spriteDestroy (SpriteT *sp);
void jlSpriteDestroy (jlSpriteT *sp);
bool spriteCompile (SpriteT *sp); // build per-shift fast path
void spritePrewarm (SpriteT *sp); // hint: compile if not already
// Load up to maxCels cels from a baked .spr file (cross-target chunky
// 4bpp blob from tools/assetbake/assetbake.py --type sprite). Each
// cel becomes a freshly-allocated jlSpriteT (release with
// jlSpriteDestroy). outPalette (optional) receives the embedded
// 16-entry $0RGB palette if present.
uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels,
uint16_t maxCels, uint16_t *outPalette);
void spriteDraw (SurfaceT *s, SpriteT *sp, int16_t x, int16_t y);
void spriteSaveUnder (const SurfaceT *s, SpriteT *sp,
int16_t x, int16_t y, SpriteBackupT *backup);
void spriteRestoreUnder (SurfaceT *s, const SpriteBackupT *backup);
void spriteSaveAndDraw (SurfaceT *s, SpriteT *sp, int16_t x, int16_t y,
SpriteBackupT *backup);
bool jlSpriteCompile (jlSpriteT *sp); // build per-shift fast path
void jlSpritePrewarm (jlSpriteT *sp); // hint: compile if not already
void spriteCompact (void); // defrag the codegen arena
uint32_t spriteCodegenBytesUsed (void);
uint32_t spriteCodegenBytesTotal (void);
void jlSpriteDraw (jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y);
void jlSpriteSaveUnder (const jlSurfaceT *s, jlSpriteT *sp,
int16_t x, int16_t y, jlSpriteBackupT *backup);
void jlSpriteRestoreUnder (jlSurfaceT *s, const jlSpriteBackupT *backup);
void jlSpriteSaveAndDraw (jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y,
jlSpriteBackupT *backup);
void jlSpriteCompact (void); // defrag the codegen arena
uint32_t jlSpriteCodegenBytesUsed (void);
uint32_t jlSpriteCodegenBytesTotal (void);
```
### Assets (`joey/asset.h`)
### Assets (baked at build time)
Small bitmap blits with optional embedded palette, in `.jas` format.
Use embedded `const JoeyAssetT` for ship-with-binary art; use the
loaders for on-disk assets.
Asset PNGs live in each example's `assets/` directory and are baked
to native binary blobs at build time by `tools/assetbake/assetbake.py`
(Python+PIL). The runtime loads those blobs directly with no
conversion -- there is no in-memory chunky-to-planar or palette
re-indexing on the device.
```c
typedef struct {
uint16_t width;
uint16_t height;
bool hasPalette;
uint16_t palette[16]; // valid only if hasPalette
const uint8_t *pixels; // 4bpp packed, rowBytes = (width+1)/2
} JoeyAssetT;
Two blob formats:
* **`.tbk` (tile bank)** -- one or more 8x8 tiles in per-target
planar layout (Amiga plane-major; Atari ST row-major-with-planes-
per-row; DOS / IIgs chunky 4bpp). The loader `jlTileBankLoad`
rejects files baked for the wrong target.
* **`.spr` (sprite cel set)** -- one or more uniform-sized sprite
cels in cross-target chunky 4bpp. The Phase 11 walker reads chunky
and converts to planar at draw time, so the same blob serves every
platform.
Both formats embed an optional 16-entry `$0RGB` palette.
JoeyAssetT *joeyAssetLoadFile (const char *path);
JoeyAssetT *joeyAssetFromMem (const uint8_t *data, uint32_t length);
void joeyAssetFree (JoeyAssetT *asset);
void joeyAssetApplyPalette (SurfaceT *dst, uint8_t paletteIndex,
const JoeyAssetT *asset);
```
tools/assetbake/assetbake.py --type tile --target {amiga|atarist|dos|iigs} in.png out.tbk
tools/assetbake/assetbake.py --type sprite --cell WxH in.png out.spr
```
The Makefiles (`make/{amiga,atarist,dos,iigs}.mk`) wire bake rules
per target so `make <target>` produces baked blobs under
`examples/<game>/generated/<target>/` and stages them into the
runtime tree at `build/<target>/.../DATA/`.
For runtime-extracted content (sprites peeled out of a procedural
or captured surface), use `jlSpriteCreateFromSurface` -- the
on-the-fly path stays alongside the baked load API.
### Present (`joey/present.h`)
```c
void stagePresent(void);
void jlStagePresent(void);
```
Flips the dirty rows of the stage to the display, then clears dirty
state. Drawing primitives mark dirty as a side effect, so calling
`stagePresent` once at end-of-frame is enough.
`jlStagePresent` once at end-of-frame is enough.
### Input (`joey/input.h`)
Call `joeyInputPoll` once per frame, then query the state predicates.
Call `jlInputPoll` once per frame, then query the state predicates.
Edge predicates (`*Pressed`, `*Released`) fire only in the frame the
transition happened.
@ -293,35 +317,35 @@ transition happened.
typedef enum { /* KEY_NONE, KEY_A..KEY_Z, KEY_0..KEY_9, KEY_SPACE,
KEY_ESCAPE, KEY_RETURN, KEY_TAB, KEY_BACKSPACE,
KEY_UP/DOWN/LEFT/RIGHT, KEY_LSHIFT/RSHIFT/LCTRL/LALT,
KEY_F1..KEY_F10, KEY_COUNT */ } JoeyKeyE;
KEY_F1..KEY_F10, KEY_COUNT */ } jlKeyE;
typedef enum { MOUSE_BUTTON_NONE, MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT,
MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_COUNT } JoeyMouseButtonE;
typedef enum { JOYSTICK_0, JOYSTICK_1, JOYSTICK_COUNT } JoeyJoystickE;
typedef enum { JOY_BUTTON_0, JOY_BUTTON_1, JOY_BUTTON_COUNT } JoeyJoyButtonE;
MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_COUNT } jlMouseButtonE;
typedef enum { JOYSTICK_0, JOYSTICK_1, JOYSTICK_COUNT } jlJoystickE;
typedef enum { JOY_BUTTON_0, JOY_BUTTON_1, JOY_BUTTON_COUNT } jlJoyButtonE;
#define JOYSTICK_AXIS_MAX 127
#define JOYSTICK_AXIS_MIN (-127)
void joeyInputPoll (void);
void joeyWaitForAnyKey (void);
void jlInputPoll (void);
void jlWaitForAnyKey (void);
bool joeyKeyDown (JoeyKeyE key);
bool joeyKeyPressed (JoeyKeyE key);
bool joeyKeyReleased (JoeyKeyE key);
bool jlKeyDown (jlKeyE key);
bool jlKeyPressed (jlKeyE key);
bool jlKeyReleased (jlKeyE key);
int16_t joeyMouseX (void);
int16_t joeyMouseY (void);
bool joeyMouseDown (JoeyMouseButtonE b);
bool joeyMousePressed (JoeyMouseButtonE b);
bool joeyMouseReleased (JoeyMouseButtonE b);
int16_t jlMouseX (void);
int16_t jlMouseY (void);
bool jlMouseDown (jlMouseButtonE b);
bool jlMousePressed (jlMouseButtonE b);
bool jlMouseReleased (jlMouseButtonE b);
bool joeyJoystickConnected(JoeyJoystickE js);
int8_t joeyJoystickX (JoeyJoystickE js);
int8_t joeyJoystickY (JoeyJoystickE js);
bool joeyJoyDown (JoeyJoystickE js, JoeyJoyButtonE b);
bool joeyJoyPressed (JoeyJoystickE js, JoeyJoyButtonE b);
bool joeyJoyReleased (JoeyJoystickE js, JoeyJoyButtonE b);
void joeyJoystickReset (JoeyJoystickE js, uint8_t deadZone);
bool jlJoystickConnected(jlJoystickE js);
int8_t jlJoystickX (jlJoystickE js);
int8_t jlJoystickY (jlJoystickE js);
bool jlJoyDown (jlJoystickE js, jlJoyButtonE b);
bool jlJoyPressed (jlJoystickE js, jlJoyButtonE b);
bool jlJoyReleased (jlJoystickE js, jlJoyButtonE b);
void jlJoystickReset (jlJoystickE js, uint8_t deadZone);
```
@ -330,38 +354,38 @@ void joeyJoystickReset (JoeyJoystickE js, uint8_t deadZone);
4-channel Protracker-style music plus four one-shot SFX slots. Module
data must be the platform-native form produced by `tools/joeymod`
(`.mod` for Amiga/DOS/ST; `.ntp` for IIgs; `.amod` if you want
loop=false on Amiga). A failed `joeyAudioInit` is non-fatal; the rest
loop=false on Amiga). A failed `jlAudioInit` is non-fatal; the rest
of the API stays callable as no-ops.
```c
#define JOEY_AUDIO_SFX_SLOTS 4
bool joeyAudioInit (void);
void joeyAudioShutdown (void);
bool jlAudioInit (void);
void jlAudioShutdown (void);
void joeyAudioPlayMod (const uint8_t *data, uint32_t length, bool loop);
void joeyAudioStopMod (void);
bool joeyAudioIsPlayingMod (void);
void jlAudioPlayMod (const uint8_t *data, uint32_t length, bool loop);
void jlAudioStopMod (void);
bool jlAudioIsPlayingMod (void);
void joeyAudioPlaySfx (uint8_t slot, const uint8_t *sample,
void jlAudioPlaySfx (uint8_t slot, const uint8_t *sample,
uint32_t length, uint16_t rateHz);
void joeyAudioStopSfx (uint8_t slot);
void jlAudioStopSfx (uint8_t slot);
void joeyAudioFrameTick (void);
void jlAudioFrameTick (void);
```
### Debug logging (`joey/debug.h`)
Crash-tracing logger. Writes are buffered and durable across normal
exit; call `joeyLogFlush` ahead of suspected hang points if you want
exit; call `jlLogFlush` ahead of suspected hang points if you want
a guaranteed last-line-on-disk.
```c
void joeyLog (const char *msg);
void joeyLogF (const char *fmt, ...);
void joeyLogFlush(void);
void joeyLogReset(void);
void jlLog (const char *msg);
void jlLogF (const char *fmt, ...);
void jlLogFlush(void);
void jlLogReset(void);
```
Output goes to `joeylog.txt` in the program's working directory.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

1873
examples/agi/agi.c Normal file

File diff suppressed because it is too large Load diff

562
examples/agi/agi.h Normal file
View 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
examples/agi/agiObj.c Normal file
View 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
examples/agi/agiPic.c Normal file
View 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
examples/agi/agiRes.c Normal file
View 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
examples/agi/agiText.c Normal file
View 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
examples/agi/agiView.c Normal file
View 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;
}

2300
examples/agi/agiVm.c Normal file

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,7 @@
// the DATA folder shipped on the disk image (see make/<plat>.mk for
// each platform's packaging step).
//
// On platforms where the audio HAL is still a stub, joeyAudioInit
// On platforms where the audio HAL is still a stub, jlAudioInit
// returns false, every audio call is a quiet no-op, and the demo runs
// as a silent input loop -- you can confirm the build links and the
// input plumbing still works without hearing anything.
@ -81,7 +81,7 @@ static bool loadFile(const char *path, uint32_t maxLen, uint8_t **outBytes, uint
}
static void buildPalette(SurfaceT *screen) {
static void buildPalette(jlSurfaceT *screen) {
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
uint16_t i;
@ -92,24 +92,24 @@ static void buildPalette(SurfaceT *screen) {
colors[COLOR_HINT] = 0x0444;
colors[COLOR_BAR] = 0x00F0;
paletteSet(screen, 0, colors);
jlPaletteSet(screen, 0, colors);
}
// Visual feedback for "audio is running, but you cannot tell on a
// stub HAL": pulse a horizontal bar between the hint color and the
// active color whenever SFX has fired this frame.
static void initialPaint(SurfaceT *screen, bool audioOk) {
surfaceClear(screen, COLOR_BG);
fillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H,
static void initialPaint(jlSurfaceT *screen, bool audioOk) {
jlSurfaceClear(screen, COLOR_BG);
jlFillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H,
audioOk ? COLOR_HINT : COLOR_BG);
stagePresent();
jlStagePresent();
}
int main(void) {
JoeyConfigT config;
SurfaceT *screen;
jlConfigT config;
jlSurfaceT *screen;
bool audioOk;
int16_t flashFrames;
uint8_t *modBytes;
@ -120,17 +120,16 @@ int main(void) {
config.codegenBytes = 8 * 1024;
config.maxSurfaces = 4;
config.audioBytes = 64UL * 1024;
config.assetBytes = 128UL * 1024;
if (!joeyInit(&config)) {
fprintf(stderr, "joeyInit failed: %s\n", joeyLastError());
if (!jlInit(&config)) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
return 1;
}
screen = stageGet();
screen = jlStageGet();
if (screen == NULL) {
fprintf(stderr, "stageGet returned NULL\n");
joeyShutdown();
fprintf(stderr, "jlStageGet returned NULL\n");
jlShutdown();
return 1;
}
@ -139,11 +138,11 @@ int main(void) {
modLen = 0;
sfxLen = 0;
audioOk = joeyAudioInit();
audioOk = jlAudioInit();
if (audioOk) {
if (loadFile(TEST_MOD_PATH, 64UL * 1024UL, &modBytes, &modLen)) {
joeyAudioPlayMod(modBytes, modLen, true);
// joeyAudioPlayMod copies the bytes into the engine's own
jlAudioPlayMod(modBytes, modLen, true);
// jlAudioPlayMod copies the bytes into the engine's own
// buffer; safe to release ours immediately.
free(modBytes);
modBytes = NULL;
@ -152,40 +151,40 @@ int main(void) {
}
buildPalette(screen);
scbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
initialPaint(screen, audioOk);
flashFrames = 0;
for (;;) {
joeyWaitVBL();
joeyInputPoll();
joeyAudioFrameTick();
if (joeyKeyPressed(KEY_ESCAPE)) {
jlWaitVBL();
jlInputPoll();
jlAudioFrameTick();
if (jlKeyPressed(KEY_ESCAPE)) {
break;
}
if (joeyKeyPressed(KEY_SPACE) && sfxBytes != NULL) {
joeyAudioPlaySfx(SFX_SLOT, sfxBytes, sfxLen, SFX_RATE_HZ);
if (jlKeyPressed(KEY_SPACE) && sfxBytes != NULL) {
jlAudioPlaySfx(SFX_SLOT, sfxBytes, sfxLen, SFX_RATE_HZ);
flashFrames = 8;
}
if (flashFrames > 0) {
fillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H, COLOR_BAR);
stagePresent();
jlFillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H, COLOR_BAR);
jlStagePresent();
flashFrames--;
if (flashFrames == 0) {
fillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H, COLOR_HINT);
stagePresent();
jlFillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H, COLOR_HINT);
jlStagePresent();
}
}
}
if (audioOk) {
joeyAudioStopMod();
joeyAudioShutdown();
jlAudioStopMod();
jlAudioShutdown();
}
if (sfxBytes != NULL) {
free(sfxBytes);
}
joeyShutdown();
jlShutdown();
return 0;
}

View file

@ -3,13 +3,13 @@
// a clear visual signal that the underlying inner loops (C or ASM)
// produced the expected pixel pattern.
//
// TL: drawPixel + drawLine (8-octant fan from cell center; pixel
// TL: jlDrawPixel + jlDrawLine (8-octant fan from cell center; pixel
// row of all 16 colors along the cell's bottom edge).
// TR: drawRect + fillRect (concentric outlines + filled blocks at
// TR: jlDrawRect + jlFillRect (concentric outlines + filled blocks at
// deliberately odd x / odd width to catch nibble-edge bugs).
// BL: drawCircle + fillCircle (concentric outlines + a small filled
// BL: jlDrawCircle + jlFillCircle (concentric outlines + a small filled
// disk at center).
// BR: tileCopy / tileCopyMasked / tileSnap+tilePaste / floodFill.
// BR: jlTileCopy / jlTileCopyMasked / jlTileSnap+jlTilePaste / jlFloodFill.
//
// Holds the frame until the user presses ESC / RETURN / SPACE.
@ -32,17 +32,17 @@
#define C_ORANGE 8
#define C_GRAY 9
static void buildPalette(SurfaceT *screen);
static void drawCellBorder(SurfaceT *screen, int16_t cx, int16_t cy);
static void drawAllCellBorders(SurfaceT *screen);
static void drawPrimitivesPixelLine(SurfaceT *screen);
static void drawPrimitivesRect(SurfaceT *screen);
static void drawPrimitivesCircle(SurfaceT *screen);
static void drawPrimitivesTileFlood(SurfaceT *screen);
static void buildPalette(jlSurfaceT *screen);
static void drawCellBorder(jlSurfaceT *screen, int16_t cx, int16_t cy);
static void drawAllCellBorders(jlSurfaceT *screen);
static void drawPrimitivesPixelLine(jlSurfaceT *screen);
static void drawPrimitivesRect(jlSurfaceT *screen);
static void drawPrimitivesCircle(jlSurfaceT *screen);
static void drawPrimitivesTileFlood(jlSurfaceT *screen);
static void waitForKey(void);
static void buildPalette(SurfaceT *screen) {
static void buildPalette(jlSurfaceT *screen) {
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
// 16 distinct $0RGB entries. Index 0 is forced to black anyway.
@ -62,17 +62,17 @@ static void buildPalette(SurfaceT *screen) {
colors[13] = 0x880;
colors[14] = 0x088;
colors[15] = 0x808;
paletteSet(screen, 0, colors);
scbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
jlPaletteSet(screen, 0, colors);
jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
}
static void drawCellBorder(SurfaceT *screen, int16_t cx, int16_t cy) {
drawRect(screen, cx, cy, CELL_W, CELL_H, C_BORDER);
static void drawCellBorder(jlSurfaceT *screen, int16_t cx, int16_t cy) {
jlDrawRect(screen, cx, cy, CELL_W, CELL_H, C_BORDER);
}
static void drawAllCellBorders(SurfaceT *screen) {
static void drawAllCellBorders(jlSurfaceT *screen) {
drawCellBorder(screen, 0, 0);
drawCellBorder(screen, CELL_W, 0);
drawCellBorder(screen, 0, CELL_H);
@ -80,15 +80,15 @@ static void drawAllCellBorders(SurfaceT *screen) {
}
// Top-left cell: drawPixel + drawLine.
// Top-left cell: jlDrawPixel + jlDrawLine.
//
// 8 lines fan out from the cell center (80, 50). Four are diagonal
// (the new ASM Bresenham path) and four are axis-aligned (drawLine
// routes those to fillRect). A horizontal row of 14 pixels along the
// cell's bottom verifies drawPixel: each pixel uses a different color
// (the new ASM Bresenham path) and four are axis-aligned (jlDrawLine
// routes those to jlFillRect). A horizontal row of 14 pixels along the
// cell's bottom verifies jlDrawPixel: each pixel uses a different color
// index so the leftmost ones at color 0 are invisible (they are bg)
// and 1..13 progress through the palette.
static void drawPrimitivesPixelLine(SurfaceT *screen) {
static void drawPrimitivesPixelLine(jlSurfaceT *screen) {
int16_t cx;
int16_t cy;
int16_t i;
@ -96,180 +96,179 @@ static void drawPrimitivesPixelLine(SurfaceT *screen) {
cx = CELL_W / 2; // 80
cy = CELL_H / 2; // 50
drawLine(screen, cx, cy, cx + 70, cy, C_RED); // E (horizontal)
drawLine(screen, cx, cy, cx + 60, cy - 40, C_GREEN); // NE (diagonal)
drawLine(screen, cx, cy, cx, cy - 45, C_BLUE); // N (vertical)
drawLine(screen, cx, cy, cx - 60, cy - 40, C_YELLOW); // NW
drawLine(screen, cx, cy, cx - 70, cy, C_CYAN); // W
drawLine(screen, cx, cy, cx - 60, cy + 40, C_MAGENTA); // SW
drawLine(screen, cx, cy, cx, cy + 45, C_ORANGE); // S
drawLine(screen, cx, cy, cx + 60, cy + 40, C_GRAY); // SE
jlDrawLine(screen, cx, cy, cx + 70, cy, C_RED); // E (horizontal)
jlDrawLine(screen, cx, cy, cx + 60, cy - 40, C_GREEN); // NE (diagonal)
jlDrawLine(screen, cx, cy, cx, cy - 45, C_BLUE); // N (vertical)
jlDrawLine(screen, cx, cy, cx - 60, cy - 40, C_YELLOW); // NW
jlDrawLine(screen, cx, cy, cx - 70, cy, C_CYAN); // W
jlDrawLine(screen, cx, cy, cx - 60, cy + 40, C_MAGENTA); // SW
jlDrawLine(screen, cx, cy, cx, cy + 45, C_ORANGE); // S
jlDrawLine(screen, cx, cy, cx + 60, cy + 40, C_GRAY); // SE
// Pixel row: 14 single-pixel writes at consecutive x to exercise
// both odd and even nibble paths.
for (i = 0; i < 14; i++) {
drawPixel(screen, (int16_t)(10 + i * 10), (int16_t)(CELL_H - 6),
jlDrawPixel(screen, (int16_t)(10 + i * 10), (int16_t)(CELL_H - 6),
(uint8_t)((i + 1) & 0x0F));
}
}
// Top-right cell: drawRect + fillRect.
// Top-right cell: jlDrawRect + jlFillRect.
//
// Four nested rectangles with deliberately odd x/y/w/h to exercise
// the partial-byte (nibble) edge handling in halFastFillRect. The
// outermost is filled, the next outline-only, then filled with odd
// width, then a 1-pixel-wide vertical bar (drawRect collapses to a
// line via fillRect's 1-wide path).
static void drawPrimitivesRect(SurfaceT *screen) {
// width, then a 1-pixel-wide vertical bar (jlDrawRect collapses to a
// line via jlFillRect's 1-wide path).
static void drawPrimitivesRect(jlSurfaceT *screen) {
int16_t ox;
ox = CELL_W; // cell origin x
// Outer fill, even-aligned.
fillRect(screen, ox + 8, 8, 144, 84, C_RED);
jlFillRect(screen, ox + 8, 8, 144, 84, C_RED);
// Inner outline, odd x to test partial-nibble edges.
drawRect(screen, ox + 17, 17, 124, 64, C_YELLOW);
jlDrawRect(screen, ox + 17, 17, 124, 64, C_YELLOW);
// Odd width fill, odd x.
fillRect(screen, ox + 25, 25, 35, 48, C_GREEN);
// 1-pixel vertical bar (degenerate rect through fillRect 1-wide path).
fillRect(screen, ox + 100, 25, 1, 48, C_BORDER);
jlFillRect(screen, ox + 25, 25, 35, 48, C_GREEN);
// 1-pixel vertical bar (degenerate rect through jlFillRect 1-wide path).
jlFillRect(screen, ox + 100, 25, 1, 48, C_BORDER);
// Odd-x odd-w narrow bar to specifically hit hasLeading + hasTrailing
// in halFastFillRect.
fillRect(screen, ox + 75, 25, 7, 48, C_CYAN);
jlFillRect(screen, ox + 75, 25, 7, 48, C_CYAN);
}
// Bottom-left cell: drawCircle + fillCircle.
// Bottom-left cell: jlDrawCircle + jlFillCircle.
//
// Concentric outlines at decreasing radii, alternating colors, plus a
// small filled disk at the center. Center is at the cell midpoint.
static void drawPrimitivesCircle(SurfaceT *screen) {
static void drawPrimitivesCircle(jlSurfaceT *screen) {
int16_t cx;
int16_t cy;
cx = CELL_W / 2;
cy = CELL_H + CELL_H / 2;
drawCircle(screen, cx, cy, 45, C_BORDER);
drawCircle(screen, cx, cy, 35, C_GREEN);
drawCircle(screen, cx, cy, 25, C_YELLOW);
drawCircle(screen, cx, cy, 15, C_CYAN);
fillCircle(screen, cx, cy, 8, C_MAGENTA);
jlDrawCircle(screen, cx, cy, 45, C_BORDER);
jlDrawCircle(screen, cx, cy, 35, C_GREEN);
jlDrawCircle(screen, cx, cy, 25, C_YELLOW);
jlDrawCircle(screen, cx, cy, 15, C_CYAN);
jlFillCircle(screen, cx, cy, 8, C_MAGENTA);
}
// Bottom-right cell: tile + flood fill.
//
// Top portion: a 16x16 colored block, then tileSnap one of its 8x8
// quadrants and tilePaste the captured tile to a neighbor block;
// also tileCopy the same source quadrant to a third location to
// exercise the full surface-to-surface path. Then a tileCopyMasked
// Top portion: a 16x16 colored block, then jlTileSnap one of its 8x8
// quadrants and jlTilePaste the captured tile to a neighbor block;
// also jlTileCopy the same source quadrant to a third location to
// exercise the full surface-to-surface path. Then a jlTileCopyMasked
// case: paint a 2x1 (16x8) "stripe" containing a transparent color 0
// pattern interleaved with color, paste it over a solid backdrop with
// transparent=0; the backdrop should show through the transparent
// nibbles.
//
// Bottom portion: drawRect outlines a closed region, floodFillBounded
// Bottom portion: jlDrawRect outlines a closed region, jlFloodFillBounded
// fills its interior with a different color, stopping at the outline.
static void drawPrimitivesTileFlood(SurfaceT *screen) {
static void drawPrimitivesTileFlood(jlSurfaceT *screen) {
int16_t ox;
int16_t oy;
int16_t bx;
int16_t by;
int16_t i;
int16_t px;
TileT snapBuf;
jlTileT snapBuf;
ox = CELL_W; // 160
oy = CELL_H; // 100
// Source 16x16 block at (168, 108): a 4-quadrant pattern.
fillRect(screen, ox + 8, oy + 8, 8, 8, C_RED);
fillRect(screen, ox + 16, oy + 8, 8, 8, C_GREEN);
fillRect(screen, ox + 8, oy + 16, 8, 8, C_BLUE);
fillRect(screen, ox + 16, oy + 16, 8, 8, C_YELLOW);
jlFillRect(screen, ox + 8, oy + 8, 8, 8, C_RED);
jlFillRect(screen, ox + 16, oy + 8, 8, 8, C_GREEN);
jlFillRect(screen, ox + 8, oy + 16, 8, 8, C_BLUE);
jlFillRect(screen, ox + 16, oy + 16, 8, 8, C_YELLOW);
// tileSnap the top-left red quadrant (block bx=21, by=13) and
// tilePaste it next to the 16x16 source as the 5th quadrant.
// jlTileSnap the top-left red quadrant (block bx=21, by=13) and
// jlTilePaste it next to the 16x16 source as the 5th quadrant.
bx = (int16_t)((ox + 8) / 8);
by = (int16_t)((oy + 8) / 8);
tileSnap(screen, (uint8_t)bx, (uint8_t)by, &snapBuf);
tilePaste(screen, (uint8_t)(bx + 4), (uint8_t)by, &snapBuf);
jlTileSnap(screen, (uint8_t)bx, (uint8_t)by, &snapBuf);
jlTilePaste(screen, (uint8_t)(bx + 4), (uint8_t)by, &snapBuf);
// tileCopy from the green quadrant onto a fresh location below.
tileCopy(screen, (uint8_t)(bx + 4), (uint8_t)(by + 1),
// jlTileCopy from the green quadrant onto a fresh location below.
jlTileCopy(screen, (uint8_t)(bx + 4), (uint8_t)(by + 1),
screen, (uint8_t)(bx + 1), (uint8_t)by);
// tileCopyMasked test: build a "transparent" striped pattern at
// jlTileCopyMasked test: build a "transparent" striped pattern at
// (208, 132). The tile's source has color 0 in alternating
// nibbles. Paste it onto a solid orange backdrop so transparent
// nibbles let the orange show through.
fillRect(screen, ox + 80, oy + 32, 16, 8, C_ORANGE); // backdrop
jlFillRect(screen, ox + 80, oy + 32, 16, 8, C_ORANGE); // backdrop
// Build a vertical-stripe source at (240, 132): col-pixel = (px % 2 ? color : 0)
for (i = 0; i < 8; i++) {
for (px = 0; px < 16; px++) {
drawPixel(screen, (int16_t)(ox + 112 + px), (int16_t)(oy + 32 + i),
jlDrawPixel(screen, (int16_t)(ox + 112 + px), (int16_t)(oy + 32 + i),
(uint8_t)((px & 1) ? C_MAGENTA : 0));
}
}
// tileCopyMasked: source at block (ox+112)/8 = 34..35, by 16
// jlTileCopyMasked: source at block (ox+112)/8 = 34..35, by 16
// -> dst at backdrop block (ox+80)/8 = 30..31, by 16
tileCopyMasked(screen, (uint8_t)((ox + 80) / 8), (uint8_t)((oy + 32) / 8),
jlTileCopyMasked(screen, (uint8_t)((ox + 80) / 8), (uint8_t)((oy + 32) / 8),
screen, (uint8_t)((ox + 112) / 8), (uint8_t)((oy + 32) / 8),
0);
// Flood-fill region: a small bordered rectangle in the cell's
// lower portion. Outline drawn in C_BORDER; floodFillBounded
// lower portion. Outline drawn in C_BORDER; jlFloodFillBounded
// from a point inside should fill with C_CYAN, stopping at the
// border.
drawRect(screen, ox + 16, oy + 60, 64, 32, C_BORDER);
floodFillBounded(screen, (int16_t)(ox + 48), (int16_t)(oy + 76),
jlDrawRect(screen, ox + 16, oy + 60, 64, 32, C_BORDER);
jlFloodFillBounded(screen, (int16_t)(ox + 48), (int16_t)(oy + 76),
C_CYAN, C_BORDER);
// Plain floodFill: solid block then re-fill to a new color.
fillRect(screen, ox + 96, oy + 60, 48, 32, C_GREEN);
floodFill(screen, (int16_t)(ox + 120), (int16_t)(oy + 76), C_GRAY);
// Plain jlFloodFill: solid block then re-fill to a new color.
jlFillRect(screen, ox + 96, oy + 60, 48, 32, C_GREEN);
jlFloodFill(screen, (int16_t)(ox + 120), (int16_t)(oy + 76), C_GRAY);
}
static void waitForKey(void) {
joeyWaitForAnyKey();
jlWaitForAnyKey();
}
int main(void) {
JoeyConfigT config;
SurfaceT *screen;
jlConfigT config;
jlSurfaceT *screen;
config.codegenBytes = 8 * 1024;
config.maxSurfaces = 4;
config.audioBytes = 64UL * 1024;
config.assetBytes = 128UL * 1024;
if (!joeyInit(&config)) {
fprintf(stderr, "joeyInit failed: %s\n", joeyLastError());
if (!jlInit(&config)) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
return 1;
}
screen = stageGet();
screen = jlStageGet();
if (screen == NULL) {
fprintf(stderr, "stageGet returned NULL\n");
joeyShutdown();
fprintf(stderr, "jlStageGet returned NULL\n");
jlShutdown();
return 1;
}
buildPalette(screen);
surfaceClear(screen, C_BG);
jlSurfaceClear(screen, C_BG);
drawAllCellBorders(screen);
drawPrimitivesPixelLine(screen);
drawPrimitivesRect(screen);
drawPrimitivesCircle(screen);
drawPrimitivesTileFlood(screen);
stagePresent();
jlStagePresent();
waitForKey();
joeyShutdown();
jlShutdown();
return 0;
}

View file

@ -7,22 +7,21 @@
#include <joey/joey.h>
int main(void) {
JoeyConfigT config;
jlConfigT config;
config.codegenBytes = 8 * 1024;
config.maxSurfaces = 4;
config.audioBytes = 64UL * 1024;
config.assetBytes = 128UL * 1024;
if (!joeyInit(&config)) {
fprintf(stderr, "joeyInit failed: %s\n", joeyLastError());
if (!jlInit(&config)) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
return 1;
}
printf("JoeyLib %s\n", joeyVersionString());
printf("Platform: %s\n", joeyPlatformName());
printf("JoeyLib %s\n", jlVersionString());
printf("Platform: %s\n", jlPlatformName());
printf("Hello from JoeyLib.\n");
joeyShutdown();
jlShutdown();
return 0;
}

View file

@ -39,14 +39,14 @@
#define STICK0_LEFT 24
#define STICK1_LEFT 216
static void buildPalette(SurfaceT *screen);
static void drawFrame(SurfaceT *screen, int16_t left);
static void drawAndPresent(SurfaceT *screen, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t color);
static void buildPalette(jlSurfaceT *screen);
static void drawFrame(jlSurfaceT *screen, int16_t left);
static void drawAndPresent(jlSurfaceT *screen, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t color);
static int16_t dotXFor(int16_t left, int8_t ax);
static int16_t dotYFor(int8_t ay);
static int16_t buttonXFor(int16_t left, int idx);
static void initialPaint(SurfaceT *screen);
static void updateStick(SurfaceT *screen, JoeyJoystickE js, int16_t left);
static void initialPaint(jlSurfaceT *screen);
static void updateStick(jlSurfaceT *screen, jlJoystickE js, int16_t left);
typedef struct {
int16_t dotX;
@ -59,7 +59,7 @@ typedef struct {
static StickViewT gView[JOYSTICK_COUNT];
static void buildPalette(SurfaceT *screen) {
static void buildPalette(jlSurfaceT *screen) {
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
uint16_t i;
@ -75,26 +75,26 @@ static void buildPalette(SurfaceT *screen) {
colors[COLOR_CONNECTED] = 0x00F0;
colors[COLOR_DISCONNECTED] = 0x0F00; // red
paletteSet(screen, 0, colors);
jlPaletteSet(screen, 0, colors);
}
static void drawAndPresent(SurfaceT *screen, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t color) {
/* fillRect marks the rect dirty; stagePresent flushes only that
static void drawAndPresent(jlSurfaceT *screen, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t color) {
/* jlFillRect marks the rect dirty; jlStagePresent flushes only that
* dirty band. */
fillRect(screen, x, y, (uint16_t)w, (uint16_t)h, color);
stagePresent();
jlFillRect(screen, x, y, (uint16_t)w, (uint16_t)h, color);
jlStagePresent();
}
// Draw the static stick frame (just an outlined square -- four edges
// with the inset filled background).
static void drawFrame(SurfaceT *screen, int16_t left) {
static void drawFrame(jlSurfaceT *screen, int16_t left) {
int16_t top;
top = STICK_TOP;
fillRect(screen, left, top, FRAME_SIZE, FRAME_SIZE, COLOR_FRAME);
fillRect(screen,
jlFillRect(screen, left, top, FRAME_SIZE, FRAME_SIZE, COLOR_FRAME);
jlFillRect(screen,
(int16_t)(left + FRAME_INSET),
(int16_t)(top + FRAME_INSET),
(uint16_t)(FRAME_SIZE - 2 * FRAME_INSET),
@ -134,10 +134,10 @@ static int16_t buttonXFor(int16_t left, int idx) {
}
static void initialPaint(SurfaceT *screen) {
static void initialPaint(jlSurfaceT *screen) {
int16_t i;
surfaceClear(screen, COLOR_BACKGROUND);
jlSurfaceClear(screen, COLOR_BACKGROUND);
for (i = 0; i < JOYSTICK_COUNT; i++) {
int16_t left;
left = (i == 0) ? STICK0_LEFT : STICK1_LEFT;
@ -145,13 +145,13 @@ static void initialPaint(SurfaceT *screen) {
gView[i].valid = false;
gView[i].connected = false;
}
stagePresent();
jlStagePresent();
}
// Compare current joystick state against gView and redraw / present
// only the visual elements that changed.
static void updateStick(SurfaceT *screen, JoeyJoystickE js, int16_t left) {
static void updateStick(jlSurfaceT *screen, jlJoystickE js, int16_t left) {
StickViewT *v;
int8_t ax;
int8_t ay;
@ -164,13 +164,13 @@ static void updateStick(SurfaceT *screen, JoeyJoystickE js, int16_t left) {
int16_t by;
v = &gView[js];
connected = joeyJoystickConnected(js);
ax = joeyJoystickX(js);
ay = joeyJoystickY(js);
connected = jlJoystickConnected(js);
ax = jlJoystickX(js);
ay = jlJoystickY(js);
newDotX = dotXFor(left, ax);
newDotY = dotYFor(ay);
for (i = 0; i < JOY_BUTTON_COUNT; i++) {
newBtn[i] = joeyJoyDown(js, (JoeyJoyButtonE)i);
newBtn[i] = jlJoyDown(js, (jlJoyButtonE)i);
}
if (!v->valid || v->connected != connected) {
@ -214,40 +214,39 @@ static void updateStick(SurfaceT *screen, JoeyJoystickE js, int16_t left) {
int main(void) {
JoeyConfigT config;
SurfaceT *screen;
jlConfigT config;
jlSurfaceT *screen;
config.codegenBytes = 8 * 1024;
config.maxSurfaces = 4;
config.audioBytes = 64UL * 1024;
config.assetBytes = 128UL * 1024;
if (!joeyInit(&config)) {
fprintf(stderr, "joeyInit failed: %s\n", joeyLastError());
if (!jlInit(&config)) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
return 1;
}
screen = stageGet();
screen = jlStageGet();
if (screen == NULL) {
fprintf(stderr, "stageGet returned NULL\n");
joeyShutdown();
fprintf(stderr, "jlStageGet returned NULL\n");
jlShutdown();
return 1;
}
buildPalette(screen);
scbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
initialPaint(screen);
joeyInputPoll();
jlInputPoll();
for (;;) {
joeyInputPoll();
if (joeyKeyPressed(KEY_ESCAPE)) {
jlInputPoll();
if (jlKeyPressed(KEY_ESCAPE)) {
break;
}
updateStick(screen, JOYSTICK_0, STICK0_LEFT);
updateStick(screen, JOYSTICK_1, STICK1_LEFT);
}
joeyShutdown();
jlShutdown();
return 0;
}

View file

@ -1,4 +1,4 @@
// Visual keyboard + mouse demo: one square per JoeyKeyE, lit when its
// Visual keyboard + mouse demo: one square per jlKeyE, lit when its
// key is held, and a small pointer drawn at the live mouse position.
// Holding the left mouse button while the pointer is over a cell lights
// it as if the corresponding key were down. Press ESC to quit.
@ -33,19 +33,19 @@
#define CELL_NONE ((int16_t)-1)
static void buildPalette(SurfaceT *screen);
static void buildPalette(jlSurfaceT *screen);
static void cellAtPoint(int16_t px, int16_t py, int16_t *outCol, int16_t *outRow);
static bool cellTargetLit(int16_t col, int16_t row, int16_t cursorCol, int16_t cursorRow);
static void drawCell(SurfaceT *screen, int16_t col, int16_t row, bool lit);
static void drawCursor(SurfaceT *screen, int16_t x, int16_t y);
static void initialPaint(SurfaceT *screen);
static void presentChangedCells(SurfaceT *screen, int16_t cursorCol, int16_t cursorRow);
static void updateCursor(SurfaceT *screen, int16_t cursorCol, int16_t cursorRow);
static void drawCell(jlSurfaceT *screen, int16_t col, int16_t row, bool lit);
static void drawCursor(jlSurfaceT *screen, int16_t x, int16_t y);
static void initialPaint(jlSurfaceT *screen);
static void presentChangedCells(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow);
static void updateCursor(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow);
// Keys laid out row-by-row. KEY_NONE cells stay blank. Shape roughly
// resembles a real keyboard (top number row, then QWERTY rows, then a
// cluster of modifiers / arrows / function keys).
static const JoeyKeyE gKeyGrid[GRID_ROWS][GRID_COLS] = {
static const jlKeyE gKeyGrid[GRID_ROWS][GRID_COLS] = {
{ KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9, KEY_0 },
{ KEY_Q, KEY_W, KEY_E, KEY_R, KEY_T, KEY_Y, KEY_U, KEY_I, KEY_O, KEY_P },
{ KEY_A, KEY_S, KEY_D, KEY_F, KEY_G, KEY_H, KEY_J, KEY_K, KEY_L, KEY_BACKSPACE },
@ -61,7 +61,7 @@ static int16_t gLastCursorCol = CELL_NONE;
static int16_t gLastCursorRow = CELL_NONE;
static void buildPalette(SurfaceT *screen) {
static void buildPalette(jlSurfaceT *screen) {
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
uint16_t i;
@ -73,7 +73,7 @@ static void buildPalette(SurfaceT *screen) {
colors[COLOR_LIT] = 0x00F0; // bright green
colors[COLOR_CURSOR] = 0x0FFF; // white
paletteSet(screen, 0, colors);
jlPaletteSet(screen, 0, colors);
}
@ -100,23 +100,23 @@ static void cellAtPoint(int16_t px, int16_t py, int16_t *outCol, int16_t *outRow
static bool cellTargetLit(int16_t col, int16_t row, int16_t cursorCol, int16_t cursorRow) {
JoeyKeyE key;
jlKeyE key;
key = gKeyGrid[row][col];
if (key == KEY_NONE) {
return false;
}
if (joeyKeyDown(key)) {
if (jlKeyDown(key)) {
return true;
}
if (col == cursorCol && row == cursorRow && joeyMouseDown(MOUSE_BUTTON_LEFT)) {
if (col == cursorCol && row == cursorRow && jlMouseDown(MOUSE_BUTTON_LEFT)) {
return true;
}
return false;
}
static void drawCell(SurfaceT *screen, int16_t col, int16_t row, bool lit) {
static void drawCell(jlSurfaceT *screen, int16_t col, int16_t row, bool lit) {
int16_t x;
int16_t y;
uint8_t color;
@ -124,21 +124,21 @@ static void drawCell(SurfaceT *screen, int16_t col, int16_t row, bool lit) {
x = (int16_t)(MARGIN_X + col * (CELL_W + GAP));
y = (int16_t)(MARGIN_Y + row * (CELL_H + GAP));
color = lit ? COLOR_LIT : COLOR_UNLIT;
fillRect(screen, x, y, CELL_W, CELL_H, color);
jlFillRect(screen, x, y, CELL_W, CELL_H, color);
}
static void drawCursor(SurfaceT *screen, int16_t x, int16_t y) {
fillRect(screen, x, y, CURSOR_W, CURSOR_H, COLOR_CURSOR);
static void drawCursor(jlSurfaceT *screen, int16_t x, int16_t y) {
jlFillRect(screen, x, y, CURSOR_W, CURSOR_H, COLOR_CURSOR);
}
static void initialPaint(SurfaceT *screen) {
static void initialPaint(jlSurfaceT *screen) {
int16_t col;
int16_t row;
JoeyKeyE key;
jlKeyE key;
surfaceClear(screen, COLOR_BACKGROUND);
jlSurfaceClear(screen, COLOR_BACKGROUND);
for (row = 0; row < GRID_ROWS; row++) {
for (col = 0; col < GRID_COLS; col++) {
key = gKeyGrid[row][col];
@ -149,14 +149,14 @@ static void initialPaint(SurfaceT *screen) {
gCellLit[row][col] = false;
}
}
stagePresent();
jlStagePresent();
}
static void presentChangedCells(SurfaceT *screen, int16_t cursorCol, int16_t cursorRow) {
static void presentChangedCells(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow) {
int16_t col;
int16_t row;
JoeyKeyE key;
jlKeyE key;
bool lit;
for (row = 0; row < GRID_ROWS; row++) {
@ -169,10 +169,10 @@ static void presentChangedCells(SurfaceT *screen, int16_t cursorCol, int16_t cur
if (lit == gCellLit[row][col]) {
continue;
}
/* drawCell marks the cell's rect dirty; stagePresent
/* drawCell marks the cell's rect dirty; jlStagePresent
* flushes that one band. */
drawCell(screen, col, row, lit);
stagePresent();
jlStagePresent();
gCellLit[row][col] = lit;
}
}
@ -183,26 +183,26 @@ static void presentChangedCells(SurfaceT *screen, int16_t cursorCol, int16_t cur
// stamp the new cursor at the current mouse position. Both rects are
// presented; if the cursor stayed inside the same cell only one rect
// pair is touched, so steady-state cost is small.
static void updateCursor(SurfaceT *screen, int16_t cursorCol, int16_t cursorRow) {
static void updateCursor(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow) {
int16_t mouseX;
int16_t mouseY;
mouseX = joeyMouseX();
mouseY = joeyMouseY();
mouseX = jlMouseX();
mouseY = jlMouseY();
if (gLastCursorX != mouseX || gLastCursorY != mouseY) {
if (gLastCursorCol != CELL_NONE) {
drawCell(screen, gLastCursorCol, gLastCursorRow, gCellLit[gLastCursorRow][gLastCursorCol]);
} else if (gLastCursorX >= 0 && gLastCursorY >= 0) {
// Old cursor was in a gap region. Stamp background over it.
fillRect(screen, gLastCursorX, gLastCursorY, CURSOR_W, CURSOR_H, COLOR_BACKGROUND);
jlFillRect(screen, gLastCursorX, gLastCursorY, CURSOR_W, CURSOR_H, COLOR_BACKGROUND);
}
}
drawCursor(screen, mouseX, mouseY);
/* All draw calls above marked their rects dirty; one stagePresent
/* All draw calls above marked their rects dirty; one jlStagePresent
* flushes the union (cursor erase + cursor draw). */
stagePresent();
jlStagePresent();
gLastCursorX = mouseX;
gLastCursorY = mouseY;
@ -212,43 +212,42 @@ static void updateCursor(SurfaceT *screen, int16_t cursorCol, int16_t cursorRow)
int main(void) {
JoeyConfigT config;
SurfaceT *screen;
jlConfigT config;
jlSurfaceT *screen;
int16_t cursorCol;
int16_t cursorRow;
config.codegenBytes = 8 * 1024;
config.maxSurfaces = 4;
config.audioBytes = 64UL * 1024;
config.assetBytes = 128UL * 1024;
if (!joeyInit(&config)) {
fprintf(stderr, "joeyInit failed: %s\n", joeyLastError());
if (!jlInit(&config)) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
return 1;
}
screen = stageGet();
screen = jlStageGet();
if (screen == NULL) {
fprintf(stderr, "stageGet returned NULL\n");
joeyShutdown();
fprintf(stderr, "jlStageGet returned NULL\n");
jlShutdown();
return 1;
}
buildPalette(screen);
scbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
initialPaint(screen);
joeyInputPoll();
jlInputPoll();
for (;;) {
joeyInputPoll();
if (joeyKeyPressed(KEY_ESCAPE)) {
jlInputPoll();
if (jlKeyPressed(KEY_ESCAPE)) {
break;
}
cellAtPoint(joeyMouseX(), joeyMouseY(), &cursorCol, &cursorRow);
cellAtPoint(jlMouseX(), jlMouseY(), &cursorCol, &cursorRow);
presentChangedCells(screen, cursorCol, cursorRow);
updateCursor(screen, cursorCol, cursorRow);
}
joeyShutdown();
jlShutdown();
return 0;
}

View file

@ -1,5 +1,5 @@
// M1 deliverable: visible test pattern exercising surfaces, palettes,
// SCBs, fillRect, and present.
// SCBs, jlFillRect, and present.
//
// Screen is divided into 8 horizontal bands, each assigned its own
// palette. The pattern draws 16 vertical color-index stripes across
@ -16,50 +16,50 @@
#define STRIPE_COUNT 16
#define STRIPE_WIDTH (SURFACE_WIDTH / STRIPE_COUNT)
static void buildPalettes(SurfaceT *screen);
static void buildScbs(SurfaceT *screen);
static void drawStripes(SurfaceT *screen);
static void buildPalettes(jlSurfaceT *screen);
static void buildScbs(jlSurfaceT *screen);
static void drawStripes(jlSurfaceT *screen);
static void makeGradient(uint16_t *out16, int redOn, int greenOn, int blueOn);
static void buildPalettes(SurfaceT *screen) {
static void buildPalettes(jlSurfaceT *screen) {
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
// Palette 0: grayscale
makeGradient(colors, 1, 1, 1);
paletteSet(screen, 0, colors);
jlPaletteSet(screen, 0, colors);
// Palette 1: red
makeGradient(colors, 1, 0, 0);
paletteSet(screen, 1, colors);
jlPaletteSet(screen, 1, colors);
// Palette 2: yellow
makeGradient(colors, 1, 1, 0);
paletteSet(screen, 2, colors);
jlPaletteSet(screen, 2, colors);
// Palette 3: green
makeGradient(colors, 0, 1, 0);
paletteSet(screen, 3, colors);
jlPaletteSet(screen, 3, colors);
// Palette 4: cyan
makeGradient(colors, 0, 1, 1);
paletteSet(screen, 4, colors);
jlPaletteSet(screen, 4, colors);
// Palette 5: blue
makeGradient(colors, 0, 0, 1);
paletteSet(screen, 5, colors);
jlPaletteSet(screen, 5, colors);
// Palette 6: magenta
makeGradient(colors, 1, 0, 1);
paletteSet(screen, 6, colors);
jlPaletteSet(screen, 6, colors);
// Palette 7: white-only (same as grayscale but serves as sanity check)
makeGradient(colors, 1, 1, 1);
paletteSet(screen, 7, colors);
jlPaletteSet(screen, 7, colors);
}
static void buildScbs(SurfaceT *screen) {
static void buildScbs(jlSurfaceT *screen) {
uint16_t band;
uint16_t first;
uint16_t last;
@ -67,19 +67,19 @@ static void buildScbs(SurfaceT *screen) {
for (band = 0; band < BAND_COUNT; band++) {
first = (uint16_t)(band * BAND_HEIGHT);
last = (uint16_t)(first + BAND_HEIGHT - 1);
scbSetRange(screen, first, last, (uint8_t)band);
jlScbSetRange(screen, first, last, (uint8_t)band);
}
}
static void drawStripes(SurfaceT *screen) {
static void drawStripes(jlSurfaceT *screen) {
uint8_t colorIndex;
int16_t x;
surfaceClear(screen, 0);
jlSurfaceClear(screen, 0);
for (colorIndex = 0; colorIndex < STRIPE_COUNT; colorIndex++) {
x = (int16_t)(colorIndex * STRIPE_WIDTH);
fillRect(screen, x, 0, STRIPE_WIDTH, SURFACE_HEIGHT, colorIndex);
jlFillRect(screen, x, 0, STRIPE_WIDTH, SURFACE_HEIGHT, colorIndex);
}
}
@ -102,33 +102,32 @@ static void makeGradient(uint16_t *out16, int redOn, int greenOn, int blueOn) {
int main(void) {
JoeyConfigT config;
SurfaceT *screen;
jlConfigT config;
jlSurfaceT *screen;
config.codegenBytes = 8 * 1024;
config.maxSurfaces = 4;
config.audioBytes = 64UL * 1024;
config.assetBytes = 128UL * 1024;
if (!joeyInit(&config)) {
fprintf(stderr, "joeyInit failed: %s\n", joeyLastError());
if (!jlInit(&config)) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
return 1;
}
screen = stageGet();
screen = jlStageGet();
if (screen == NULL) {
fprintf(stderr, "stageGet returned NULL\n");
joeyShutdown();
fprintf(stderr, "jlStageGet returned NULL\n");
jlShutdown();
return 1;
}
buildPalettes(screen);
buildScbs(screen);
drawStripes(screen);
stagePresent();
jlStagePresent();
joeyWaitForAnyKey();
jlWaitForAnyKey();
joeyShutdown();
jlShutdown();
return 0;
}

File diff suppressed because it is too large Load diff

141
examples/spacetaxi/PLAN.md Normal file
View file

@ -0,0 +1,141 @@
# Space Taxi port plan
Roadmap for bringing the JoeyLib port to functional parity with the
C64 original. Sequenced so each phase delivers something testable on
its own and unblocks the next. Each phase ends with a user-test
checkpoint -- regressions must be caught before moving on.
## Workflow per subsystem
Same loop every time, so it stays cheap to iterate:
1. **Find** -- grep the live RAM dump for relevant register writes
(`$DC00`, `$D015`, `$D400-$D418`, `$07F8-$07FF`, etc.) or use
`callGraph.py` to find routines touching specific addresses.
2. **Read** -- pull the routine into a window via `disLive.py
<dump> <out> <start> <end>`, study the annotated listing.
3. **Document** -- append behavior summary to `MECHANICS.md` in
plain English. Add new labels/vars to `stuff/spacetaxi/labels.txt`.
4. **Reannotate** -- `python3 disLive.py mem0000-level1.bin
live-level1.lst` to regenerate the (annotated) listing from
the dump + labels.txt in one step. The .lst file is the only
readable form -- labels are baked in directly.
5. **Port** -- update the matching JoeyLib C file
(`stEngine.c`, `stPassenger.c`, `stRender.c`, `stAudio.c`,
`stLevel.c`, `spacetaxi.c`).
6. **Build + ship** -- `make -f make/dos.mk EXAMPLE=spacetaxi`
first, then once stable also amiga / atarist / iigs.
7. **User test** -- run `scripts/run-dos.sh staxi`, report
behavior. Do not start the next phase before this.
## Phase 0 -- foundation (DONE)
- Disassembly toolchain in `stuff/spacetaxi/`
- Full live-RAM listing + annotated variant
- `MECHANICS.md` baseline
- Asset pipeline (`tools/assetbake/assetbake.py`, extractor realigned)
- Bugfixes: sprite size in tile units, palette clobber,
tile-bank load clamp, fire-not-thrust, palette/sheet collision
- Template-velocity physics in `stEngine.c`
## Phase 1 -- physics + collision (NEXT)
Make the cab fly correctly. Highest impact: nothing else works
until the player can hover, land, and crash.
| Task | Investigate | Update |
| ---- | ----------- | ------------------------------------- |
| 1.1 | Trace `$7D95/96` gravity word writers -- when is gravity on? | `stEngine.c`: gate gravity on the same conditions |
| 1.2 | Find writers of `$721D` (player state byte). High bit = "dying". What sets it? | `stEngine.c`: crash trigger logic |
| 1.3 | Trace phase-2 handler at `$6B24` -- this is what runs after `$6A72` advances. Likely the crash / fall-to-ground sequence | New `stEngine.c` crash-anim path |
| 1.4 | Find pad-vs-wall predicate. Hardware `$D01F` says "we hit bg"; some code looks at char codes near taxi feet to decide PAD vs WALL | `stLevel.c`: pad detection by tile code |
| 1.5 | Confirm screen-edge wrap at `$6AED` -- when X-velocity carries the taxi off the right edge, does it wrap or stop? | `stEngine.c`: edge behavior |
**Checkpoint**: cab flies smoothly, can land on a pad, crashes hard.
## Phase 2 -- visuals + animation
Make the screen match the original's look frame-by-frame.
| Task | Investigate | Update |
| ---- | ----------- | ------------------------------------- |
| 2.1 | Confirm taxi has one cel only. The 2 writes to `$07F8` (taxi sprite ptr) -- are they per-level (color swap) or per-frame? Read those sites | `stRender.c`: reduce `ST_TAXI_CEL_COUNT` to 1, kill `thrustFrame` |
| 2.2 | What renders the engine flame under the taxi during thrust? Either a second sprite or a tile overwrite | `stRender.c`: flame overlay |
| 2.3 | Passenger walk: trace updates to passenger sprite Y/X in the sprite state table (NOT `$D004-$D00D`; the table memory) | `stPassenger.c`: walk frame timing |
| 2.4 | Title-screen layout: the C64 title positions logo / credits / instructions at specific char rows. The extractor pulls the screen layout; check the .txt against the disassembly's title-init routine to confirm we render the same layout | `assets/levels/title.txt`, `stRender.c` title overlay |
**Checkpoint**: the title screen, gameplay, and game-over screens
each look like the C64.
## Phase 3 -- audio
Authentic feel needs the SID engine. JoeyLib audio is per-platform,
so the abstraction is "SFX event id" -- the SID details only land in
the DOS / Amiga / ST tone-generator backend.
| Task | Investigate | Update |
| ---- | ------------------------------------------ | ------ |
| 3.1 | Music engine: voice 1+2 melodic table. Find where tunes are stored, how the row pointer advances, how note-to-freq lookup works | `stAudio.c`: music playback state machine |
| 3.2 | Thrust SFX: confirmed voice 1 freq sweep from `$721B`. Find writer of `$721B` (start of thrust) and what triggers it to stop | `stAudioSfxThrust` envelope |
| 3.3 | Crash SFX: voice 3 noise burst. `LDA #$81 STA $D412 ... LDA #$80`. Find duration / pitch envelope | `stAudioSfxCrash` |
| 3.4 | Land SFX: a successful landing must trigger some audible cue. Trace from collision dispatch when sprite-bg collision is at low vy | `stAudioSfxLand` |
| 3.5 | Door / passenger SFX: passenger enters/exits the cab on a pad. Probably also voice 3 noise or a brief tone | new SFX |
**Checkpoint**: thrust, land, crash, and at least one music loop
all play audibly on DOS. Approximate timbres OK; pitch + envelope
should roughly match.
## Phase 4 -- game logic
Make the game progress. State machine, scoring, lives.
| Task | Investigate | Update |
| ---- | ---------------------------------------- | ------ |
| 4.1 | Passenger spawn: when does fare N+1 appear after fare N is delivered? Look for writes to the passenger-active flags in the state table | `stPassenger.c` |
| 4.2 | Patience timer: a passenger has a countdown. Find the per-frame decrement and what happens at zero (passenger leaves? lose pad?) | `stPassenger.c` |
| 4.3 | Scoring: base fare from `$7D...`, tip math for time-on-board. BCD arithmetic on a 4-byte score | `stHud.c`, score storage |
| 4.4 | Level transitions: `$7215 == $19` is the "level complete" sentinel. What writes that, and how does the game pick the next level? | `spacetaxi.c` main switch |
| 4.5 | Lives: where is the lives counter? How does the game decide game-over? | `stEngine.c` crash branch, `spacetaxi.c` |
**Checkpoint**: can complete a level, advance, lose all lives,
see game-over, restart from title.
## Phase 5 -- level data
Match the original's 25 levels (or however many ship in the disk
image; verify from `SPACETAX.D64`).
| Task | Investigate | Update |
| ---- | ---------------------------------------- | ------ |
| 5.1 | Level table layout in RAM: where are per-level pad coords stored? Per-level music ID? Per-level taxi spawn? Per-level fare list? | `stLevel.c` STL1 format |
| 5.2 | Extract every level from the D64 image (we already have level1 / level2 / title dumps; need a way to dump every level state). Either replay through the game in VICE and snapshot each level, or read the level table once and reconstruct | extractor: bulk-export |
| 5.3 | Verify the title screen extraction matches the original byte-for-byte (chars + colors) | `title.dat` regenerate |
**Checkpoint**: all original levels playable, named correctly,
ordered correctly.
## Phase 6 -- cross-platform parity
DOS lands first. Then bring the other three up.
| Target | Specific work |
| ------ | ------------------------------------------------------------ |
| Amiga | Verify build still runs; PT replayer for music; joystick |
| Atari ST | Same; YM2149 for SFX, MOD for music |
| IIgs | Resolve ROOT-segment limit so the binary fits; wire asset |
| | pipeline into the disk packager (3 options in earlier note); |
| | Ensoniq sound match |
**Checkpoint**: each of the four targets boots, shows title,
plays at least one level end-to-end.
## Working notes
- All chat-side analysis must stay in plain English -- disassembly
excerpts go in local files (`MECHANICS.md`, `labels.txt`), not
pasted back into the session.
- One subsystem per session is the right cadence. Trying to "do
them all at once" without user-test feedback compounds errors.
- Keep this plan up to date: as each task lands, mark it DONE
with the commit SHA or date, and add new tasks discovered
during the work.

View file

@ -0,0 +1,96 @@
# Space Taxi disassembly: verified claims
Each row below references a claim in `MECHANICS.md` and the trace
that verifies it. Trace scripts live in `stuff/spacetaxi/trace.py`
plus the inline scripts run during verification sessions.
## VERIFIED via emulator trace
| Claim | Evidence |
| ----- | -------- |
| `$4354` BCD-add with blob `$43B9` modifies HUD position 3 with digit '5' | Run `$4354` against all-blank `$07C2`. Result: `$07C2-$07C8 = $66 $66 $66 $6F $66 $74 $74`. Only one write to `$07C5`. |
| Fare blob `$43C1` = +95 (positions 2,3) | Same harness, different blob. Result has digits '9' at pos 2, '5' at pos 3. |
| Fare blob `$43C9` = +50 (position 2) | Same harness. Result has digit '5' at pos 2. |
| Stage 6 (`$6742`) writes only to `$07C2`, never `$07E0` | Memory-write trace shows the only HUD writes target `$07C5`. |
| `$07E0` has no in-game writers except `$43AA` (hudInit template copy) | grep across `live-raw.lst` finds only `$43AA` and `$635F` (sceneLoad restore). |
| `$6032` physics: X-accel suppressed when `$7197 & 1 == 1` via `$6190` | Run physics with RIGHT-held and `$7197 = $C1`: `ax = 0`. With `$7197 = $C0`: `ax = +14`. |
| `$6032` physics: Y-accel NOT gated by parity | Same test with UP/DOWN held + various `$7197`: Y accel always = +/-yAccel regardless of parity. |
| Parity bit `$7197 & 1` toggle sites are death-phase-1 (`$6A93/95`) and FIRE-button rising edge (`$63FF/$6401`) | grep `EOR #$01 ... STA $7197` across all listings -- no other writers. |
| `$619B` (gated dispatch) preserves the parity bit | Code at `$619B-$61BC`: ORA with captured parity `$61` before STA `$7197`. |
| `$645C` pad detection requires EXACT row match | At pad-table row 204 with cab at (115, 204): `$7150 = 0 -> 1`. At row 203 (1 off): stays 0. |
| `$645C` pad detection X-bounds work | At (100, 204): stays 0 (X<110). At (208, 204): stays 0 (X>198). At (115, 204): `$7150 -> 1`. |
| `$6966` collision dispatch: bg-collision triggers DEATH | With `$D01F & 1 = 1`, phase advances 0->1, sprite-0 cel $C1->$CC. |
| `$6966`: sprite-0 + passenger collision (bits 0+3 in `$D01E`) ALSO triggers death (in raw.bin state) | With `$D01E = $09`: phase->1, `$721D=1` (trampoline RTS preserved A=1). |
| `$6966`: sprite-sprite without sprite-0 bit does NOT trigger death | With `$D01E = $08` (only bit 3): phase stays 0. |
| `$7D75` trampoline is STATIC in raw.bin and level1.bin | Direct read: bytes `$4C $9A $7D` in both dumps. No writers anywhere in the code. |
| `$7D75` trampoline returns A unchanged in default state | Patched the operand to a `LDA #$00; RTS` stub: passenger collision no longer triggers death (phase stays 0). With default RTS: phase advances. |
| `$65C1` death-stage dispatch JMP table verified for stages 1-9 | Instrumented run of `$65C1` with each `$7163` value; first JMP captured matches the documented target ($665F, $66B7, $66DA, $66DD, $6739, $6742, $67A3, $67A6, $67C6). Stage 0 is fall-through (no JMP). |
| Per-level X-accel and Y-accel can DIFFER | Levels C, D, E, F, G, K, M, N, T, V have `$7D91 != $7D8F` after decompress. |
| Per-level X-gravity is non-zero on levels P and U | Levels P (`$7D95 = $04`) and U (`$7D95 = $02`); all others = 0. |
| Level K is anti-gravity (`$7D93 = $F9`) | After 10 ticks of physics with no input, cab Y position DECREASES (moves up), vy = -70. |
| Level P side-wind drifts cab right at $4/frame | After 20 ticks: vx = +80, x position drifted +840 sub-units. |
| `.dat` files preserve all per-level fields | Parse STL2 header for all 24 levels; xAccel/yAccel/xGrav/yGrav/border/bg match emulator extraction byte-for-byte. |
| `$CC6C` music tick is a NO-OP when track pointers are zero | Run with `$CB80/$CB81 = $00`, X = 0/7/14: 4 instructions executed, 0 SID writes. Confirms "gameplay is silent" because no tracks are loaded. |
| In post-decompress state for level A, ALL THREE voice track pointers are zero | Direct read of `$CB80/81`, `$CB87/88`, `$CB8E/8F` after `run_scene(0)`: all `$00 $00`. So even if the IRQ runs (which my emulator can't simulate due to the KERNAL `$EA31` exit being absent), there's nothing for it to play. Gameplay is silent. |
| `$5FBB` draws "GAME OVER!" to row 11 of screen RAM | Direct trace of `$5FBB` writes the chars `$47 $41 $4D $45 $20 $4F $56 $45 $52 $21` to $0400+11*40+15 through $0400+11*40+24 -- byte values matching the ASCII string "GAME OVER!" (using the game's custom charset at `$2800`). |
| `$5FBB` is called via `$6BD0` after every death-fall (NOT just final game-over) | Only caller of `$6BD0` is `$5C29` (end of `$5BD1+` template-restore path). NOT called from every death. CORRECTION: `$5C29` is at the end of `$5BD1`'s fall-through path (reached only when `$5CF1 >= 3`). |
| `$5CF1` is a 4-phase animation state machine (`$5BBB` dispatcher) | Verified by tracing all `$5CF1` writers: `$5B2F` sets to 0; `$5C6F` `$5C9C` `$5CDF` each INC at the end of phase 0/1/2 finalizers. So a fare sequence cycles 0->1->2->3. |
| `$5C2C` is the **drop-off animation** ($5CF1=0), not death-fall | Verified by emulator trace at row $D3 -> 642 instructions -> cab at $D4, BCD-add (+50 via $43C9) and hudInit called. Routine RTS-es cleanly, does NOT JMP to $6BD0 from here. |
| `$5B07` is "game-over reset", NOT "deathTuneSetup" | The `JSR $6283 with A=$24` decompresses **scene 24 (title screen)**, not a song. My earlier "song 24" label was wrong: `$6283` is the bank-switch wrapper around `$9656` (the scene decompressor), and `$24` is the scene index. Verified by checking the scene pointer table at `$9600` -- scene $24 is the title. |
| `$71CE` (fares done) INCs ONLY at `$6BD6` | grep finds only one INC (`$6BD6`) and one STA (`$5EE9 = 0` at game init). |
| `$7213` (fare target) is player-selectable 1..4 at title | Writers: `$48AF` sets to 1 (default); `$5310-$5328` is a title-screen joystick UI -- RIGHT INCs, LEFT DECs, ANDed with `#$03` to clamp 0..3 (= 1..4 with +1 baseline). |
| Game-end goes to `$5EC4` (gameInitContinue, restarts to title) | `$6BE1: JMP $602F`, `$602F: JMP $5EC4`. So Space Taxi's "level complete" just resets the game -- no inter-level progression in the original. |
| `romToLevel.py` bug: levels A/O/R had fareCount=0 (uncompletable) | Verified by parsing all 24 .dat files. Fixed: single-pad levels now emit `(0, 0)` self-loop fare. |
| Full `$5F40` main-loop body verified: 25 JSRs in exact order matching MECHANICS.md | With stubs for KERNAL `$FFE1`, `$$404B` (vblWait), `$44EF` (waitTickSpin), `$44D2` (waitVoicesIdle), and `$7D65 = $80` so `$6BE7` doesn't early-exit: 716 instructions, 25 depth-1 JSRs captured in documented order. |
| `$4BF8` (CIA1 DDR setup) is called every main-loop iteration, not "once" | Verified by trace: appears in the depth-1 chain at PC `$5FB5`. Setting CIA DDRs each frame is wasteful but harmless. |
| Emulator now covers all opcodes used by the main-loop body | Added INC/DEC absx/zpx, ASL/LSR/ROL/ROR zpx/absx, LDA/STA/AND/ORA/EOR/ADC/SBC/CMP indx, CPX/CPY zp. The main loop runs to completion through these instructions. |
| Passenger-spawn flow `$660B` advances state | Verified by tracing `$660B` with `$71CC = 1`: `$715C` 0->1 (fare slot incremented), `$7163` 0->1 (death stage advances to "passenger appearing"), `$7176` (sprite-1 passenger X) changes. JSR chain shows `$4080` (RNG) and `$6537` (padHoverSetup) called. |
| Flame is **sprite 2**, NOT sprite 1 (correction) | `$6D6A` flameSpriteUpdate writes `$7177` (= `$7175` + 2 = sprite 2 X) and `$7199` (sprite-2 ptr). Sprite 1 (`$7176`, `$7198`) is the active passenger sprite, set by `$660B` and other passenger-spawn paths. Earlier MECHANICS.md sprite-role table was wrong. |
| `$619B` cab cel selection: LEFT/RIGHT only writes when held, otherwise preserves $7197 | 7 input/parity combos tested: LEFT held -> base $DC (or $DD with parity); RIGHT held only when NO LEFT -> base $C0 (or $C1); no L/R held -> $7197 unchanged. Default-RIGHT facing. |
| Pad table byte 5/6 are PASSENGER spawn coords (not "anchor X" as earlier) | `$662E: LDA $7D0F,X -> $7186` (passenger X-frac); `$6634: LDA $7D10,X -> $7176` (passenger X col). My "second X for cab-stop snap" reading was wrong; these are the spawn position for the passenger sprite on the pad. |
| Pad table byte 7 has NO runtime readers | `grep -nE "11 7D"` finds zero readers in the live disassembly. The `$C2/$C4` observed values are dead. My earlier "ground vs elevated pad style" interpretation was speculation the disassembly doesn't support. |
| Stage 5 `$6739` is pad-color-alt + zero-death-flag | Verified by direct call: 11 instructions. `$7167` stays 0. `$DBDC: $FF -> $0B` and `$DBB4: $D9 -> $07` -- color RAM writes via JSR $6877 (padColor_passOff). |
| Custom charset at `$2800` is ASCII-compatible | Decoded the 8x8 bitmap glyphs for `$47/$41/$4D/$45/$4F/$56/$52/$21`: each renders as the corresponding Latin letter / '!'. So bytes spelling "GAME OVER!" in ASCII really do show up as readable "GAME OVER!" on screen. |
| Level K extended physics: cab rises ~50 px in 60 ticks no input | Y position: `$A000 -> $6DF6` over 60 ticks. Net -12810 sub-units = -50 px (moves UP, anti-gravity confirmed at scale). Expected per gravity-integration math: -12390. The 420-unit discrepancy = exactly one frame's vy of -420 (integration-order detail; vy applied before update on first tick). |
| `$4253` sprite-shadow marshal: writes X,Y interleaved to `$71A7-$71B6` | Tested with per-sprite X = `$A0..$A7`, Y = `$50..$57`. Result at `$71A7` = `A0 50 A1 51 A2 52 ... A7 57`. Matches MECHANICS.md format. |
| `$4293` sprite-shadow flush: writes 5 register blocks to VIC | Verified per-register: `$D000-$D00F = $10-$1F` (pos), `$07F8-$07FF = $D0-$D7` (ptrs), `$D027-$D02E = $E0-$E7` (colors), `$D010 = $55` (X-MSB), `$D015 = $FF` (enable). |
| `$42E9` SFX-load takes a **9-byte** program, not 7 | Verified with 9-byte program: byte 8 is voice index (read FIRST), bytes 0-6 written to `$D400+vi*7..+6`, byte 7 stored to `$7216+vi` (release timer), byte 4 ALSO stored to `$7218+vi` (ctrl mask). Earlier MECHANICS.md "7-byte program, voice index then 7 bytes" was wrong. |
| `$4080` RNG with X=100 produces non-uniform distribution | 100 trials: all in [0..99], mean ~61, ~16-cycle repeat pattern. Biased toward higher values. C64 raster-register based, not uniform. |
| `$63DD` fireButtonEdge fires SFX only on rising edge | 4 cases tested: on-pad RTS; airborne+no-fire RTS; airborne+fire-held no-op; airborne+fire-rising = 8 SID writes ($42E9 SFX load) + `$7197` parity toggled + `$71BD` latch set. |
| `$4FCB` runStopWatcher branches verified | idle (no key): 12-step RTS. RUN/STOP key ($028D bit 0): kills SID voices + `$7222 = $80`. post-mortem flag: 6-step early RTS. game-mode flag: enters wait loop at `$5006` checking joystick. |
| Stage 1 `$665F` death branch: `$7163 1->8`, DEC `$715C` | Verified: with `$7164=1`, walked `$7198` down to `$C7`, `$7163` went 1->8 (jumps to stage 8), `$715C` decremented (5->4). |
| Stage 6 `$6742` scoring: `$07C2 += 5` and `$7163 6->7` | After 2 ticks with `$7198 $C8 -> $C7`: HUD `$07C2 = $66 $66 $66 $6F $77 $74 $74` (= "___5. " = +5 at pos 3), `$7163 6 -> 7`. Score confirmed +5 per fare. |
| `$6F18` takeoffSetup gates on `$7215 >= 2` | `$7215 < 2`: 7-step early RTS, zeros `$7163`. `$7215 = 2`: 304 steps, sets `$7163 = 5` (takeoff stage), `$7D8E = 1`, `$716D = 1`, `$71CD = 1`. |
| `$6888` taxiSpawnInit: copies `$7D5B-$7D5F` -> taxi pos, zeros vels, sets cel | Per-level spawn at `$7D5B-$7D5F` (`$80 $40 $00 $30 $60`) copied to `$7D61-$7D65`. Velocities cleared. `$7197 = $C0` (default cab cel). `$71C7 = $7D60 = $AA` (fuel/max copy). |
| Animation main loop at `$5B85` is DEAD CODE in gameplay | Original .prg has `JMP $5B85` at `$FCB5` (per full-disasm.lst). raw.bin (live runtime) has `$08 $AB $28` at `$FCB5` -- DIFFERENT bytes, meaning runtime patched/overwrote that JMP. So the `$5B07-$5BBA` animation main loop was load-time / startup code that got replaced. NEVER reached in active gameplay. |
| Custom charset is ASCII-compatible | Decoded the 8x8 bitmap glyphs at `$2800 + char*8` for "GAME OVER!": each renders as recognizable Latin letter. ASCII text in screen RAM displays correctly. |
| `$704E` hudArchive copies 7 bytes from `$07C2/$07E0` to `$71D3,X*8` / `$71F3,X*8` | With `$7214 = 2` (fare idx -> X = 16), `$07C2 = AA BB CC DD EE FF 11` and `$07E0 = 22 33 44 55 66 77 88`: after call, `$71E3 = AA BB CC DD EE FF 11 00` and `$7203 = 22 33 44 55 66 77 88 00`. Per-fare 8-byte slot stride confirmed; only 7 bytes written, 8th stays at prior value. |
| `$43A5` hudInit copies template `$43B1-$43B7 -> $07E0-$07E6` | After clobbering `$07E0` with `$FF * 7`, calling `$43A5`: `$07E0 = $66 $66 $66 $74 $77 $74 $74` matching template byte-for-byte. |
## UNVERIFIED (acknowledged, not yet proven)
| Claim | Why unverified |
| ----- | -------------- |
| `$715C` is "fare slot count" not lives | INC at `$6559`, DEC at `$6B7B` etc., scene-load zeroes it. Semantics still unclear; my "fare slot" guess hasn't been confirmed against gameplay observation. |
| Bonus score blobs `$43C1` (+95) and `$43C9` (+50) fire on specific gameplay events | The call sites (`$5C82`, `$5D03`) are in the `$5BBB`-dispatched anim and a per-passenger-counter decrement; the gameplay scenarios that put `$5CF1` and `$71CF,X` in the necessary states haven't been replayed in the emulator. |
| In-cab score `$07C2` ever gets reset / committed to a persistent score | Only writers are BCD-add (during stage 6) and sceneLoad restore (from `$71D3` backup). The accumulation logic is in the backup tables, not a global score. The port simplifies this to a `uint32_t game->score`. |
| Passenger pickup mechanism in the C64 | The RTS-without-death path at `$69B3` requires `$71CA & 2` (sprite-1 bit) ALSO set, which is a 3-way sprite collision. Not yet reproduced. |
| `$7D75` trampoline ever gets patched at runtime | No writers found via grep. The "patches on pickup" theory was wrong. Possibly the trampoline is set ONCE at game-load via the bank-switched code at `$9656` (under-ROM), which my emulator can run but I haven't watched specifically for `$7D76/$7D77` writes during decompress. |
| Initial parity bit value during normal gameplay | The state on level entry isn't traced; depends on what code sets `$7197` between title and gameplay. |
## TRACE INFRASTRUCTURE
- `stuff/spacetaxi/trace.py`: instrumented wrapper around `cpu6502.Cpu6502`. `Tracer.from_dump('raw.bin').call(addr)` runs a subroutine and snapshots memory + JSR chain.
- `stuff/spacetaxi/cpu6502.py`: minimal 6502 emulator. Sufficient for all routines tested above; throws `NotImplementedError` for unsupported opcodes (none hit so far in gameplay paths).
- `stuff/spacetaxi/dumpScenes.py` + `dumpAllLevels.py`: pre-existing scene-decompress drivers.
- `stuff/spacetaxi/romToLevel.py`: emits the `.dat` files from raw.bin via the same emulator.
## METHODOLOGY (for future verification sessions)
For each new claim I want to commit to MECHANICS.md:
1. **Set up the pre-state explicitly.** Don't assume registers/memory — `poke_byte` everything that affects the routine's behavior, plus document why each pre-state value matters.
2. **Call the routine via `Tracer.call(addr)`.** Catch failures (NotImplementedError, runaway loops) and reduce scope if it doesn't return cleanly.
3. **Diff memory.** Use `tracer.memory_writes()` to see exactly what changed. Don't trust label names.
4. **Verify expected branches taken.** Use `instr_log = True` for routines where control flow matters.
5. **Document the trace command in this file** so the claim can be re-verified later.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,269 @@
#!/usr/bin/env python3
# genPlaceholderArt.py -- generate generic geometric placeholder art
# for the Space Taxi port. Output is 320x200 indexed PNG, 16-color.
# Replace these with hand-authored art any time; the build pipeline
# picks up whichever PNG sits in assets/.
import os
from PIL import Image, ImageDraw
ROOT = os.path.dirname(os.path.abspath(__file__))
# 16-color palette matching the engine's kDefaultPalette (see stRender.c).
# Each entry is (R, G, B) in 0..255. The .jas format will store the
# 4-bit-per-channel quantization automatically via joeyasset.
PALETTE_RGB = [
(0x00, 0x00, 0x00), # 0 black
(0x00, 0x00, 0x77), # 1 dark blue
(0xCC, 0x00, 0x00), # 2 red
(0x00, 0x77, 0x00), # 3 dark green
(0x66, 0x44, 0x00), # 4 brown
(0xEE, 0xEE, 0x00), # 5 yellow
(0x88, 0x88, 0x88), # 6 mid gray
(0xBB, 0xBB, 0xBB), # 7 light gray
(0x44, 0x66, 0xFF), # 8 sky blue
(0x77, 0x88, 0xFF), # 9 light blue
(0xFF, 0x88, 0x88), # 10 light red
(0x88, 0xFF, 0x44), # 11 light green
(0xFF, 0x88, 0x00), # 12 orange
(0xFF, 0x00, 0xFF), # 13 magenta
(0x00, 0xFF, 0xFF), # 14 cyan
(0xFF, 0xFF, 0xFF), # 15 white
]
def new_canvas():
"""Returns a fresh 320x200 indexed PIL image with the palette installed."""
img = Image.new('P', (320, 200), 0)
flat = []
for r, g, b in PALETTE_RGB:
flat.extend([r, g, b])
flat.extend([0] * (768 - len(flat))) # pad to 256 entries
img.putpalette(flat)
return img
def write_indexed_png(img, path):
"""Save with mode='P' so PNG output is indexed (joeyasset wants this
when converted through ppm; ImageMagick's `convert` preserves
indexed mode when writing PPM)."""
img.save(path, 'PNG', optimize=True)
print(f" wrote {path}")
def gen_tilebank():
"""tilebank0.png: 320x200, 40x25 cells of 8x8 each (= 1000 tile
slots). The engine convention is:
index 0 = empty (non-solid)
index 1..63 = solid walls
index 64..127 = landing-pad surfaces
index 128..255 = decorative non-solid
We fill a few representative slots; the rest stay black so they
show as "empty" if a level references them.
"""
img = new_canvas()
px = img.load()
def draw_tile(idx, painter):
bx = (idx % 40) * 8
by = (idx // 40) * 8
painter(bx, by)
def block(color):
return lambda x, y: [px.__setitem__((x+dx, y+dy), color)
for dx in range(8) for dy in range(8)]
def bricks(color_a, color_b):
def paint(x, y):
for dy in range(8):
row_offset = 4 if (y // 8 + dy // 4) % 2 == 0 else 0
for dx in range(8):
is_grout = (dx == (row_offset % 8)) or (dy == 3 or dy == 7)
px[x + dx, y + dy] = color_b if is_grout else color_a
return paint
def stripes(color_a, color_b):
def paint(x, y):
for dy in range(8):
c = color_a if (dy + (x // 8)) % 2 == 0 else color_b
for dx in range(8):
px[x + dx, y + dy] = c
return paint
def landing_pad(color_a, color_b):
# diagonal stripes signal a landing-pad surface
def paint(x, y):
for dy in range(8):
for dx in range(8):
px[x + dx, y + dy] = color_a if ((dx + dy) // 2) % 2 == 0 else color_b
return paint
# tile 0 -> already black (empty); leave as-is
# walls 1..7: a variety of brick/stripe patterns
draw_tile(1, bricks(6, 0)) # gray brick
draw_tile(2, bricks(4, 0)) # brown brick
draw_tile(3, block(6)) # solid gray
draw_tile(4, block(4)) # solid brown
draw_tile(5, stripes(6, 7)) # gray stripes (ceiling)
draw_tile(6, stripes(1, 8)) # blue stripes (sky deco)
draw_tile(7, block(7)) # solid light gray
# landing pad tiles 64..67
draw_tile(64, landing_pad(5, 12)) # yellow/orange pad
draw_tile(65, landing_pad(11, 3)) # green pad
draw_tile(66, landing_pad(9, 8)) # blue pad
draw_tile(67, landing_pad(15, 7)) # white pad
write_indexed_png(img, os.path.join(ROOT, 'tiles', 'tilebank0.png'))
def gen_sprites():
"""sprites.png: 320x200, layout:
y= 0..23 : taxi cels (4 frames of 24x24 starting at x=0)
y= 24..39 : passenger walk cels (4 frames of 16x16 at x=0)
Remaining rows free for future content.
"""
img = new_canvas()
d = ImageDraw.Draw(img)
# ---- Taxi cels: simple side-view "pod with thruster" ----
# Cel 0 = idle (thruster off)
# Cel 1..3 = thrust-on, alternating flame frames
for cel in range(4):
x = cel * 24
# Pod body: rounded rectangle in gray
d.rectangle([x+2, 4, x+21, 16], fill=7, outline=15)
# Cockpit window in blue
d.rectangle([x+13, 6, x+19, 10], fill=8, outline=15)
# Landing skid
d.line([(x+3, 17), (x+20, 17)], fill=6)
d.line([(x+3, 17), (x+3, 19)], fill=6)
d.line([(x+20,17), (x+20, 19)], fill=6)
# Thruster flame (cels 1..3 only, alternating shape)
if cel >= 1:
flame_color = [2, 12, 5][(cel - 1) % 3]
flame_top = 18
flame_bot = 22 if cel != 2 else 23
d.polygon([(x+8, flame_top),
(x+15, flame_top),
(x+12, flame_bot)], fill=flame_color)
# ---- Passenger cels: small stick figure, walk cycle ----
# Centered in 16x16, walking right.
for cel in range(4):
x = cel * 16
y = 24
# Head
d.ellipse([x+5, y+1, x+10, y+6], fill=10, outline=15)
# Body
d.line([(x+7, y+7), (x+7, y+12)], fill=15)
# Arms (swing alternately based on cel)
if cel == 0 or cel == 2:
d.line([(x+4, y+10), (x+10, y+10)], fill=15)
else:
d.line([(x+3, y+9), (x+11, y+11)], fill=15)
# Legs (alternating stride)
if cel == 0 or cel == 2:
d.line([(x+7, y+12), (x+4, y+15)], fill=15)
d.line([(x+7, y+12), (x+10,y+15)], fill=15)
else:
d.line([(x+7, y+12), (x+5, y+15)], fill=15)
d.line([(x+7, y+12), (x+9, y+15)], fill=15)
write_indexed_png(img, os.path.join(ROOT, 'sprites', 'sprites.png'))
def gen_font():
"""font.png: 320x200 (40x25 grid of 8x8 ASCII glyphs).
Authored manually for the printable subset (0x20..0x7E).
Each glyph cell is at (ascii%40, ascii/40) when ascii<128.
Unfilled cells stay black (treated as TILE_NO_GLYPH at runtime).
"""
# A compact 5-pixel-wide bitmap font, drawn in 7 rows with 1px
# padding on all sides (so 5x7 active inside an 8x8 cell, baseline
# at row 6). The font supports digits, uppercase letters, space,
# and basic punctuation -- enough for HUD text. Lowercase letters
# map to the same glyphs as uppercase (room to add later).
GLYPHS = {
' ': [],
'!': ['..#..', '..#..', '..#..', '..#..', '.....', '..#..', '.....'],
'"': ['.#.#.', '.#.#.', '.....', '.....', '.....', '.....', '.....'],
'0': ['.###.', '#...#', '#..##', '#.#.#', '##..#', '#...#', '.###.'],
'1': ['..#..', '.##..', '..#..', '..#..', '..#..', '..#..', '.###.'],
'2': ['.###.', '#...#', '....#', '...#.', '..#..', '.#...', '#####'],
'3': ['.###.', '#...#', '....#', '..##.', '....#', '#...#', '.###.'],
'4': ['...#.', '..##.', '.#.#.', '#..#.', '#####', '...#.', '...#.'],
'5': ['#####', '#....', '####.', '....#', '....#', '#...#', '.###.'],
'6': ['.###.', '#....', '#....', '####.', '#...#', '#...#', '.###.'],
'7': ['#####', '....#', '...#.', '..#..', '..#..', '..#..', '..#..'],
'8': ['.###.', '#...#', '#...#', '.###.', '#...#', '#...#', '.###.'],
'9': ['.###.', '#...#', '#...#', '.####', '....#', '....#', '.###.'],
':': ['.....', '..#..', '.....', '.....', '.....', '..#..', '.....'],
'.': ['.....', '.....', '.....', '.....', '.....', '..#..', '.....'],
'-': ['.....', '.....', '.....', '#####', '.....', '.....', '.....'],
'/': ['....#', '...#.', '...#.', '..#..', '.#...', '.#...', '#....'],
'A': ['.###.', '#...#', '#...#', '#####', '#...#', '#...#', '#...#'],
'B': ['####.', '#...#', '#...#', '####.', '#...#', '#...#', '####.'],
'C': ['.###.', '#...#', '#....', '#....', '#....', '#...#', '.###.'],
'D': ['####.', '#...#', '#...#', '#...#', '#...#', '#...#', '####.'],
'E': ['#####', '#....', '#....', '####.', '#....', '#....', '#####'],
'F': ['#####', '#....', '#....', '####.', '#....', '#....', '#....'],
'G': ['.###.', '#...#', '#....', '#..##', '#...#', '#...#', '.###.'],
'H': ['#...#', '#...#', '#...#', '#####', '#...#', '#...#', '#...#'],
'I': ['.###.', '..#..', '..#..', '..#..', '..#..', '..#..', '.###.'],
'J': ['..###', '...#.', '...#.', '...#.', '...#.', '#..#.', '.##..'],
'K': ['#...#', '#..#.', '#.#..', '##...', '#.#..', '#..#.', '#...#'],
'L': ['#....', '#....', '#....', '#....', '#....', '#....', '#####'],
'M': ['#...#', '##.##', '#.#.#', '#.#.#', '#...#', '#...#', '#...#'],
'N': ['#...#', '##..#', '#.#.#', '#.#.#', '#..##', '#...#', '#...#'],
'O': ['.###.', '#...#', '#...#', '#...#', '#...#', '#...#', '.###.'],
'P': ['####.', '#...#', '#...#', '####.', '#....', '#....', '#....'],
'Q': ['.###.', '#...#', '#...#', '#...#', '#.#.#', '#..#.', '.##.#'],
'R': ['####.', '#...#', '#...#', '####.', '#.#..', '#..#.', '#...#'],
'S': ['.###.', '#...#', '#....', '.###.', '....#', '#...#', '.###.'],
'T': ['#####', '..#..', '..#..', '..#..', '..#..', '..#..', '..#..'],
'U': ['#...#', '#...#', '#...#', '#...#', '#...#', '#...#', '.###.'],
'V': ['#...#', '#...#', '#...#', '#...#', '.#.#.', '.#.#.', '..#..'],
'W': ['#...#', '#...#', '#...#', '#.#.#', '#.#.#', '##.##', '#...#'],
'X': ['#...#', '#...#', '.#.#.', '..#..', '.#.#.', '#...#', '#...#'],
'Y': ['#...#', '#...#', '.#.#.', '..#..', '..#..', '..#..', '..#..'],
'Z': ['#####', '....#', '...#.', '..#..', '.#...', '#....', '#####'],
}
img = new_canvas()
px = img.load()
def paint_glyph(ascii_code, glyph_rows):
col = ascii_code % 40
row = ascii_code // 40
bx = col * 8
by = row * 8
# active 5x7 area: cell-x 1..5, cell-y 0..6 (top row, 1px left pad)
for ry, line in enumerate(glyph_rows):
for rx, ch in enumerate(line):
if ch == '#':
px[bx + 1 + rx, by + ry] = 15 # white
for ascii_code in range(32, 127):
c = chr(ascii_code)
if c in GLYPHS:
paint_glyph(ascii_code, GLYPHS[c])
elif 'a' <= c <= 'z':
paint_glyph(ascii_code, GLYPHS[c.upper()])
write_indexed_png(img, os.path.join(ROOT, 'font.png'))
def main():
os.makedirs(os.path.join(ROOT, 'tiles'), exist_ok=True)
os.makedirs(os.path.join(ROOT, 'sprites'), exist_ok=True)
print("genPlaceholderArt: emitting 320x200 indexed PNGs...")
gen_tilebank()
gen_sprites()
gen_font()
print("done.")
if __name__ == '__main__':
main()

View file

@ -0,0 +1,92 @@
# Space Taxi level .dat format (STL2)
A small custom binary format that `stLevelLoad()` reads at scene
boot. Output files land in `examples/spacetaxi/generated/levels/`
(from `romToLevel.py` for the 24 canonical C64 levels, or from
`mkstlevel` for hand-authored levels in `assets/levels/*.txt`). The
build copies them under `DATA/levels/levelNN.dat` in the runtime
asset bundle. All multi-byte fields are little-endian. No
compression; ~2 KB per level.
| Offset | Type | Field |
|-------:|:----------|:-----------------------------------------------|
| 0 | 4 bytes | magic `S T L 2` |
| 4 | u8 | nameLen (0..23) |
| 5 | char[N] | name (no NUL; max 23 chars) |
| 5+N | u8 | tileBankId |
| ... | u8 | musicId (UNUSED -- C64 gameplay is silent; see VERIFIED.md) |
| ... | u8 | bgColor (palette slot) |
| ... | u8 | borderColor (palette slot, also HUD-band fill) |
| ... | u8 | taxiSpawnTileX |
| ... | u8 | taxiSpawnTileY |
| ... | u8 | xAccel (horizontal thrust magnitude / frame) |
| ... | u8 | yAccel (vertical thrust magnitude / frame) |
| ... | i8 | xGrav (constant horizontal accel; side-wind on levels P, U) |
| ... | i8 | yGrav (constant vertical accel; anti-gravity on level K = -7) |
| ... | u8 | bgColor1 ($D022, VIC multicolor -- UNUSED) |
| ... | u8 | bgColor2 ($D023, VIC multicolor -- UNUSED) |
| ... | u8 | bgColor3 ($D024, VIC multicolor -- UNUSED) |
| ... | u8 | spriteMc0 ($D025, sprite multicolor -- UNUSED) |
| ... | u8 | spriteMc1 ($D026, sprite multicolor -- UNUSED) |
| ... | u8 | sprite0Color ($D027, cab fallback color) |
| ... | u8 | sprite1Color ($D028, flame fallback color) |
| ... | u8 | padCount (0..10) |
| ... | pad[] | padCount * 4 bytes: letter, tileX, tileY, tileW |
| ... | u8 | fareCount (0..16) |
| ... | fare[] | fareCount * 2 bytes: spawnPad, destPad |
| ... | u8[40*25] | tilemap (row-major, full 25 rows) |
| ... | u8[40*25] | colormap (row-major, palette slot per cell) |
Notes:
- `xAccel/yAccel/xGrav/yGrav` mirror the C64 per-level templates at
`$7D8F-$7D96`. Side-wind levels P (`xGrav = +4`) and U (`+2`); the
anti-gravity level K (`yGrav = -7`) makes the cab drift upward
without input. See `VERIFIED.md` for the emulator-traced extracts.
- `musicId` is preserved for format symmetry only; the C64 original
has no gameplay music (only title/score-screen jingles).
- The five VIC multicolor fields are preserved so the .dat is a
faithful byte-for-byte capture of the C64 `$7D00-$7D08` block. The
port renders in single-color mode and ignores them at runtime.
- Pads are 4 bytes (letter + 3 tile coords); there is no patience
byte on fares -- Space Taxi proper has no patience timeout, and the
emulator trace at `$71CE/$7213` confirmed the only gating is the
player-selectable fare target at the title screen.
## Tile-index conventions (in the tilemap byte)
Indexes 0..255 reference the level's active tile bank. Reserved
ranges:
| Range | Meaning |
|----------|---------------------------------------------------------|
| 0 | empty (sky / interior space; non-solid) |
| 1..63 | solid (walls, ceilings, support structures) |
| 64..127 | landing-pad surfaces (also solid; pad collisions live here) |
| 128..255 | decorative non-solid (lights, signs, animation frames) |
The engine's `isSolidAt()` uses this convention. If you redesign a
tile bank, keep the indexing consistent so the physics keeps working
without changes.
## Authoring pipeline
1. Author a 256-tile (or smaller) tile sheet as an indexed PNG. Each
tile is 8x8 px. Arrange in a grid; the bank loader assumes row-
major, tile-index = `ty * tilesPerRow + tx`. Drop the PNG at
`examples/spacetaxi/assets/tiles/tbank<N>.png`.
2. The Makefile bakes per-target via `tools/assetbake/assetbake.py
--type tile --target <port> tbank<N>.png tbank<N>.tbk`. Output
lands in `examples/spacetaxi/generated/<port>/tiles/` and is
staged into the runtime tree at `build/<port>/.../DATA/tiles/`.
3. Author each level layout in a text editor or grid tool, save as a
.dat per this spec. The helper `examples/spacetaxi/mkstlevel`
converts from a human-readable text grid to .dat. The 24 canonical
C64 levels are emitted by `stuff/spacetaxi/romToLevel.py` directly
from a raw VICE dump of the original .prg.
## Naming
Use the original game's level names as the `name` field (uppercase
ASCII, max 23 chars). The HUD displays this at the bottom-right of
the screen.

View file

@ -0,0 +1,142 @@
# Extracted from VICE memory dump. Edit at your own risk;
# regenerating will overwrite. Keep the @tile mappings in
# sync with the tilebank PNG (extracted from $2800).
@name TITLE
@tilebank 0
@music 0
@bgColor 0
@borderColor 0
@taxiSpawn 18 2
# Pads auto-detected from landable surfaces (>= 3 wide,
# approachable from above). Verify and adjust as needed.
@pad A 6 3 3
@pad B 17 3 5
@pad C 5 5 4
@pad D 24 5 4
@pad E 29 5 5
@pad F 10 7 5
@pad G 27 7 3
@pad H 15 9 5
@fare A B
@fare B C
@fare C D
@fare D E
@fare E F
@fare F G
@fare G H
@fare H A
# Screen-code -> alphabet-char mapping (one per unique code).
# @tile '.' = $C64_screencode_20
# @tile '1' = $C64_screencode_84
# @tile '2' = $C64_screencode_66
# @tile '3' = $C64_screencode_62
# @tile '4' = $C64_screencode_5D
# @tile '5' = $C64_screencode_40
# @tile '6' = $C64_screencode_5E
# @tile '7' = $C64_screencode_09
# @tile '8' = $C64_screencode_0F
# @tile '9' = $C64_screencode_13
# @tile 'A' = $C64_screencode_05
# @tile 'B' = $C64_screencode_08
# @tile 'C' = $C64_screencode_12
# @tile 'D' = $C64_screencode_14
# @tile 'E' = $C64_screencode_02
# @tile 'F' = $C64_screencode_03
# @tile 'G' = $C64_screencode_07
# @tile 'H' = $C64_screencode_0E
# @tile 'I' = $C64_screencode_19
# @tile 'J' = $C64_screencode_52
# @tile 'K' = $C64_screencode_0C
# @tile 'L' = $C64_screencode_15
# @tile 'M' = $C64_screencode_46
# @tile 'N' = $C64_screencode_4F
# @tile 'O' = $C64_screencode_54
# @tile 'P' = $C64_screencode_04
# @tile 'Q' = $C64_screencode_06
# @tile 'R' = $C64_screencode_2C
# @tile 'S' = $C64_screencode_2E
# @tile 'T' = $C64_screencode_41
# @tile 'U' = $C64_screencode_45
# @tile 'V' = $C64_screencode_4A
# @tile 'W' = $C64_screencode_4E
# @tile 'X' = $C64_screencode_50
# @tile 'Y' = $C64_screencode_55
# @tile 'Z' = $C64_screencode_57
# @tile 'a' = $C64_screencode_0B
# @tile 'b' = $C64_screencode_10
# @tile 'c' = $C64_screencode_16
# @tile 'd' = $C64_screencode_26
# @tile 'e' = $C64_screencode_31
# @tile 'f' = $C64_screencode_34
# @tile 'g' = $C64_screencode_38
# @tile 'h' = $C64_screencode_39
# @tile 'i' = $C64_screencode_3A
# @tile 'j' = $C64_screencode_42
# @tile 'k' = $C64_screencode_43
# @tile 'l' = $C64_screencode_44
# @tile 'm' = $C64_screencode_49
# @tile 'n' = $C64_screencode_4B
# @tile 'o' = $C64_screencode_53
# @tile 'p' = $C64_screencode_5C
# @tile 'q' = $C64_screencode_5F
# @tile 'r' = $C64_screencode_60
# @tile 's' = $C64_screencode_61
# @tile 't' = $C64_screencode_BC
@tilemap
r33333333333333333333333333333333333333s
4.....1111.1111....1....1111.11111.....6
4....1.....1...1..1.1..1.....1.........6
4.....111..1111..11111.1.....1111......6
4........1.1.....1...1.1.....1.........6
4....1111..1.....1...1..1111.11111.....6
4......................................6
4.........11111..1...1...1.111.........6
4...........1...1.1...1.1...1..........6
4...........1..11111...1....1..........6
4...........1..1...1..1.1...1..........6
4...........1..1...1.1...1.111.........6
2222222222222222222222222222222222222222
4...........EI.V8BH.MS.nLDFBAC.........6
4......................................6
4............d.k8bIC7GBD.ehgf..........6
4..........TKK.J7GBD9.JA9ACcAP.........6
4......................................6
4..V8I9D7Fai.YX.Q8C.B7GB.9F8CA9R.......6
4............lNZW.Q8C.7H9DCLFD78H9R....6
4............MmJU.jYOONW.D8.EAG7HS.....6
4.XLEK79BAP...................555555555q
4.......EI..................t...........
4.......................................
p555555555555.................oNMOZTJU..
@colormap
1111111111111111111111111111111111111111
1666666666666666666666666666666666666691
1666666666666666666666666666666666666691
1666666666666666666666666666666666666691
1666666666666666666666666666666666666691
1666666666666666666666666666666666666691
1666666666666666666666666666666666666691
1666666666666666666666666666666666666691
1666666666666666666666666666666666666691
1666666666666666666666666666666666666691
1666666666666666666666666666666666666691
1666666666666666666666666666666666666691
CCCCCCCBBBBBBBBBBBBBBBBBBBBBBBBBBCCCCCCC
199999999999CCCCCCCCCCCCCCCCCC0199999991
1999999999999900000000000000000999999991
1999999999999C1CCCCCCCCCCCCCC00099999991
19999999991CCCCCCCCCCCCCCCCCCC0999999991
1999990000000000000000000000000099999991
1CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC00999991
1000CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC09991
109CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC009991
10CCCCCCCCCCC009000000000000001111111111
10000000CCC00099999999999990109999999990
1999999999999999999999999990000000000000
1111111111111999999999999999301111111100

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,496 @@
#!/usr/bin/env python3
# extractFromDump.py -- convert a VICE C64 memory dump into JoeyLib
# Space Taxi assets.
#
# Reads: /tmp/spacetaxi/mem0000-<labelN>.bin (VICE `save` output;
# 2-byte LE start-addr header + 64 KB RAM).
# Writes: examples/spacetaxi/assets/tiles/tilebank<N>.png
# examples/spacetaxi/assets/levels/level<NN>.txt
# examples/spacetaxi/assets/sprites/sprites.png (level1 only)
# examples/spacetaxi/assets/font.png (level1 only)
#
# Per-level:
# - Custom charset at $2800-$2FFF (2 KB, 256 chars * 8 bytes mono)
# becomes a tile bank PNG: 40 cols x 25 rows of 8x8 cells, with
# tile index N at cell (N%40, N/40). The PNG uses the EGA-ish
# 16-color palette we already install on stage.
# - Screen RAM at $0400-$07E7 (1000 bytes = 40x25 char codes) and
# color RAM at $D800-$DBE7 (1000 bytes = 40x25 palette nybbles)
# become the tilemap+colormap in the level .txt source.
#
# Sprite data at $3000-$37FF + sprite pointer table at $07F8-$07FF
# is decoded once (it's the same across levels for the active set):
# - sprite 0 / 7: the taxi cab body & shadow
# - sprite 1..6: passenger frames / animation cels
# 64 bytes per sprite, 24x21 1-bit, MSB-leftmost. Color comes from
# $D027-$D02E (one color per sprite).
#
# Usage:
# python3 extractFromDump.py stuff/spacetaxi/mem0000-level1.bin level01 1
# python3 extractFromDump.py stuff/spacetaxi/mem0000-level2.bin level02 2
# args: <dump-path> <level-name-without-extension> <tilebank-id>
#
# Dumps live in stuff/spacetaxi/ (committed-ish, repo-local) rather
# than /tmp so they survive reboots and stay with the project.
import os
import struct
import sys
from collections import Counter
from PIL import Image
# C64 standard color palette (EGA-ish 16-color JoeyLib mapping).
# Indices 0..15 match the C64 VIC-II color register order so a value
# read from $D8xx or $D02x maps directly to a JoeyLib palette slot.
# Colors below approximate Pepto's standard PAL palette in $RGB form.
C64_PALETTE_RGB = [
(0x00, 0x00, 0x00), # 0 black
(0xFF, 0xFF, 0xFF), # 1 white
(0x88, 0x39, 0x32), # 2 red
(0x67, 0xB6, 0xBD), # 3 cyan
(0x8B, 0x3F, 0x96), # 4 purple
(0x55, 0xA0, 0x49), # 5 green
(0x40, 0x31, 0x8D), # 6 blue
(0xBF, 0xCE, 0x72), # 7 yellow
(0x8B, 0x54, 0x29), # 8 orange
(0x57, 0x42, 0x00), # 9 brown
(0xB8, 0x69, 0x62), # 10 light red
(0x50, 0x50, 0x50), # 11 dark gray
(0x78, 0x78, 0x78), # 12 mid gray
(0x94, 0xE0, 0x89), # 13 light green
(0x78, 0x69, 0xC4), # 14 light blue
(0x9F, 0x9F, 0x9F), # 15 light gray
]
ROOT = os.path.dirname(os.path.abspath(__file__))
def load_dump(path):
with open(path, "rb") as f:
raw = f.read()
start = raw[0] | (raw[1] << 8)
if start != 0:
raise SystemExit(f"{path}: unexpected start ${start:04X}, want $0000")
if len(raw) - 2 != 0x10000:
raise SystemExit(f"{path}: not a 64K dump (got {len(raw) - 2} bytes)")
return raw[2:]
def new_canvas(w, h):
img = Image.new("P", (w, h), 0)
flat = bytearray()
for r, g, b in C64_PALETTE_RGB:
flat.extend((r, g, b))
flat.extend(b"\x00" * (768 - len(flat)))
img.putpalette(bytes(flat))
return img
def paint_charset_glyph(px, mem, src_code, bx, by):
"""Plot the 8x8 glyph for C64 screen code `src_code` at pixel
(bx, by) of the indexed-palette PIL image. Foreground = palette
index 1, background untouched (already index 0 from new_canvas).
"""
src = 0x2800 + src_code * 8
for r in range(8):
byte = mem[src + r]
for c in range(8):
if byte & (0x80 >> c):
px[bx + c, by + r] = 1
def extract_tilebank(mem, out_path, code_to_slot=None):
"""Render the custom charset at $2800-$2FFF as a 320x200 PNG.
When `code_to_slot` is None, lays out all 256 chars at their raw
C64 positions (40 cols x 25 rows). When `code_to_slot` is given,
writes each charset glyph at the slot the level loader expects
-- i.e. the bank tile at position N is the glyph for screen-code
`code_for_slot[N]`. That keeps level data and tilebank aligned
after the level extractor's frequency-sorted code remapping.
"""
img = new_canvas(320, 200)
px = img.load()
if code_to_slot is None:
for ch in range(256):
paint_charset_glyph(px, mem, ch, (ch % 40) * 8, (ch // 40) * 8)
else:
# Invert: slot -> code. Multiple codes can collide into slot 0
# (background); only the first one we see "owns" that slot for
# painting purposes -- the rest just fall through to the same
# blank tile. Highest priority is the explicit space mapping.
slot_to_code = {}
for code, slot in code_to_slot.items():
slot_to_code.setdefault(slot, code)
for slot, code in slot_to_code.items():
if slot >= 256:
continue
paint_charset_glyph(px, mem, code, (slot % 40) * 8, (slot // 40) * 8)
img.save(out_path, "PNG", optimize=True)
def build_code_to_slot(mem):
"""Build the screen-code -> alphabet-slot map shared between the
tilebank emission and the level emission. The slot index is what
the runtime sees in level.tilemap[]; the tilebank must mirror this
ordering so a level-data slot-N cell renders the charset glyph
that was actually used in the original screen RAM at that code.
"""
screen = mem[0x0400:0x0400 + 1000]
codes = sorted(Counter(screen).items(), key=lambda kv: (-kv[1], kv[0]))
code_to_slot = {}
next_slot = 0
for code, _count in codes:
if code == 0x20: # space -> always index 0
code_to_slot[code] = 0
continue
if code in code_to_slot:
continue
if next_slot >= len(ALPHABET):
code_to_slot[code] = 0
continue
# Reserve slot 0 for background (space). The very first
# non-space code claims slot 1, etc.
if next_slot == 0:
next_slot = 1
code_to_slot[code] = next_slot
next_slot += 1
return code_to_slot
def screencode_to_char(code):
"""Convert a C64 screen code into a printable tilemap character
using the encoding the .txt format already understands:
0 -> '.' (treated as 0)
1..9 -> '1'..'9' (digits encode the lower 10 indices)
but we shift to use up most of the index space below.
For arbitrary 0..255 we need a 256-symbol alphabet -- not feasible
in a single character per cell. So we encode as TWO chars per cell
using base-16 hex would still only cover 0..255. Decision: keep
.txt cells one-char and map screen codes via a custom 64-char
alphabet plus an @char directive table emitted into the file so
the loader knows exactly which screen code each char represents.
"""
# This function is unused as a one-char encoder; we encode via the
# generic alphabet below instead.
return code
# Generic 1-char alphabet (62 codes); the level loader's index map
# was: '.' or ' '=0, '0'..'9'=0..9, 'A'..'Z'=10..35, 'a'..'z'=36..61.
# That's only 62 codes, not 256. The original C64 levels use ~30
# distinct screen codes per level so most levels fit. We map the N
# most-frequent screen codes to those alphabet slots.
ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
def extract_level(mem, level_name, tilebank_id, out_path, code_to_alpha):
"""Render screen RAM + color RAM as an STL1-format .txt source
using the prebuilt screen-code -> alphabet-slot mapping. Caller
must pass the same mapping that was used to reorder the tilebank
PNG; otherwise the level tilemap and tilebank diverge.
"""
screen = mem[0x0400:0x0400 + 1000] # 40x25
color = mem[0xD800:0xD800 + 1000] # 40x25, low nybble = palette slot
codes = sorted(Counter(screen).items(), key=lambda kv: (-kv[1], kv[0]))
# Full 25-row C64 screen. The title screen uses all 25 rows
# (PUBLISHED ... SOFTWARE credit wraps across rows 21, 22, 24
# with frame chars interleaved). Gameplay HUD draws over the
# bottom 3 rows at runtime.
playfield_rows = 25
lines = []
lines.append(f"# Extracted from VICE memory dump. Edit at your own risk;")
lines.append(f"# regenerating will overwrite. Keep the @tile mappings in")
lines.append(f"# sync with the tilebank PNG (extracted from $2800).")
lines.append(f"")
lines.append(f"@name {level_name.upper()}")
lines.append(f"@tilebank {tilebank_id}")
lines.append(f"@music 0")
bg = mem[0xD021] & 0x0F
border = mem[0xD020] & 0x0F
lines.append(f"@bgColor {bg}")
lines.append(f"@borderColor {border}")
# Best-effort taxi spawn: middle-top of playfield. The original
# script-driven spawn position would need to be lifted out of
# game state at dump time; for now hand-edit if you want it
# to match the cracker's preferred spawn.
lines.append(f"@taxiSpawn 18 2")
lines.append(f"")
# Pads: scan the playfield for "landable surfaces" -- runs of
# >= 3 identical non-background tile cells with two or more
# empty rows directly above (so the taxi can approach from the
# top). Per-row scanning runs top-to-bottom; for the same
# surface tile across rows we only take the topmost contiguous
# platform edge.
bg_code = codes[0][0] if codes else 0x20
def is_bg(code):
return code == bg_code or code == 0x20
pads = []
seen_used = [False] * (playfield_rows * 40)
for row in range(2, playfield_rows):
line = screen[row * 40:(row + 1) * 40]
prev = screen[(row - 1) * 40:row * 40]
prev2 = screen[(row - 2) * 40:(row - 1) * 40]
col = 0
while col < 40 and len(pads) < 8:
if is_bg(line[col]):
col += 1
continue
j = col
while j < 40 and line[j] == line[col] and not seen_used[row * 40 + j]:
j += 1
width = j - col
# Approachable from above? At least 2/3 of cells in the
# 2 rows above this stretch must be background.
above_clear = 0
for k in range(col, j):
if is_bg(prev[k]):
above_clear += 1
if is_bg(prev2[k]):
above_clear += 1
if width >= 3 and above_clear >= width:
pads.append((row, col, width))
for k in range(col, j):
seen_used[row * 40 + k] = True
col = j
lines.append("# Pads auto-detected from landable surfaces (>= 3 wide,")
lines.append("# approachable from above). Verify and adjust as needed.")
pad_letter = ord("A")
for (row, col, width) in pads:
if pad_letter > ord("H"):
break
lines.append(f"@pad {chr(pad_letter)} {col} {row} {width}")
pad_letter += 1
if pad_letter == ord("A"):
lines.append("# (No landable surfaces auto-detected; emitting placeholders.)")
lines.append("@pad A 4 10 4")
lines.append("@pad B 32 10 4")
pad_letter = ord("C")
lines.append("")
# Fare list: simple round-robin between detected pads.
pad_count = pad_letter - ord("A")
if pad_count >= 2:
for k in range(pad_count):
src = chr(ord("A") + k)
dst = chr(ord("A") + ((k + 1) % pad_count))
lines.append(f"@fare {src} {dst} 45")
else:
lines.append("@fare A B 45")
lines.append("")
# Emit the @tile mapping for round-trip clarity (and so the
# author can read a level .txt and tell which screen code maps
# to which char). Format: @tile <char> <screencode> -- the
# mkstlevel parser ignores these today but they're useful
# documentation, and a future loader could honor them.
lines.append("# Screen-code -> alphabet-char mapping (one per unique code).")
inv = {v: k for k, v in code_to_alpha.items()}
for idx in sorted(inv):
ch = '.' if idx == 0 else ALPHABET[idx]
code = inv[idx]
lines.append(f"# @tile '{ch}' = $C64_screencode_{code:02X}")
lines.append("")
# Tilemap (22 rows of 40 chars each).
lines.append("@tilemap")
for r in range(playfield_rows):
row = screen[r * 40:(r + 1) * 40]
s = ""
for code in row:
idx = code_to_alpha.get(code, 0)
s += '.' if idx == 0 else ALPHABET[idx]
lines.append(s)
lines.append("")
lines.append("@colormap")
for r in range(playfield_rows):
row = color[r * 40:(r + 1) * 40]
s = "".join(
('0' if (c & 0x0F) == 0
else ALPHABET[(c & 0x0F)]
if (c & 0x0F) < len(ALPHABET) else '0')
for c in row
)
lines.append(s)
with open(out_path, "w") as fp:
fp.write("\n".join(lines) + "\n")
def extract_sprites(mem, out_path):
"""Render C64 hardware sprites into the JoeyLib-expected sprite
sheet layout (320x200, 16-color indexed PNG):
y= 0..23 : 1 taxi cel (24x24, x = 0)
sprite 0 ptr (in level1 dump: $DC -> $3700)
The adjacent slot $DD is the landing-gear-down
pose, not a thrust frame -- ignored for now.
y= 24..47 : 4 passenger cels (16x16 each, x = 0, 16, 32, 48)
Sprite 3 ptr ($07FB) is the passenger data; sprites 3-6 share it.
"""
img = new_canvas(320, 200)
px = img.load()
# Shared multicolor sprite colors from VIC registers.
mcm_color0 = mem[0xD025] & 0x0F # 2-bit value 01
mcm_color1 = mem[0xD026] & 0x0F # 2-bit value 11
mcm_mask = mem[0xD01C] # bit per sprite: 1 = multicolor
def paint_sprite_at(data_addr, sprite_color, dst_x, dst_y, dst_w, dst_h, multicolor=False):
# Mono C64 sprite is 24x21, 1 bit per pixel, 3 bytes per row.
# Multicolor C64 sprite is 12 "double-wide pixels" x 21 rows,
# 2 bits per pixel, still 3 bytes per row. Color mapping:
# 00 -> transparent
# 01 -> mcm_color0 ($D025)
# 10 -> sprite_color (sprite's own $D027+sp)
# 11 -> mcm_color1 ($D026)
if sprite_color == 0:
sprite_color = 1
sx_max = min(24, dst_w)
sy_max = min(21, dst_h)
for r in range(sy_max):
if multicolor:
for byte_idx in range(3):
byte = mem[data_addr + r * 3 + byte_idx]
# 4 multicolor pixels per byte; each spans 2
# hardware pixels wide.
for pp in range(4):
bits = (byte >> ((3 - pp) * 2)) & 0x03
if bits == 0:
continue
if bits == 1:
col = mcm_color0
elif bits == 2:
col = sprite_color
else:
col = mcm_color1
sx0 = byte_idx * 8 + pp * 2
if sx0 + 1 >= sx_max:
break
px[dst_x + sx0, dst_y + r] = col
px[dst_x + sx0 + 1, dst_y + r] = col
else:
for byte_idx in range(3):
byte = mem[data_addr + r * 3 + byte_idx]
for bit in range(8):
sx = byte_idx * 8 + bit
if sx >= sx_max:
break
if byte & (0x80 >> bit):
px[dst_x + sx, dst_y + r] = sprite_color
def paint_c64_sprite(sp_index, dst_x, dst_y, dst_w, dst_h):
ptr = mem[0x07F8 + sp_index]
if ptr == 0:
return False
sprite_color = mem[0xD027 + sp_index] & 0x0F
is_multicolor = bool(mcm_mask & (1 << sp_index))
paint_sprite_at(ptr * 64, sprite_color, dst_x, dst_y, dst_w, dst_h,
multicolor=is_multicolor)
return True
# Single taxi cel from sprite 0's actual pointer.
paint_c64_sprite(0, 0, 0, 24, 24)
# 4 passenger cels = the shared C64 sprite 3 (sprites 3..6 all
# point to the same data in the level-1 dump).
for cel in range(4):
paint_c64_sprite(3, cel * 16, 24, 16, 16)
img.save(out_path, "PNG", optimize=True)
def extract_font(mem, out_path):
"""Render the custom charset (which IS the in-game font in C64
text-mode games) as the 320x200 font.png. Same layout as
tilebank, but here we want each glyph at cell (ascii%40,
ascii/40) for jlDrawText's ASCII map.
For Space Taxi specifically, the charset doubles as the level
tile bank AND any in-screen text Sierra-style. We map the C64
screen codes used for ASCII display (codes 1..26 = A..Z,
32..63 = punctuation/digits) directly to their ASCII positions
in the JoeyLib font sheet.
"""
img = new_canvas(320, 200)
px = img.load()
def render_at(ascii_code, charset_idx):
if ascii_code < 32 or ascii_code >= 128:
return
src = 0x2800 + (charset_idx & 0xFF) * 8
col = ascii_code % 40
row = ascii_code // 40
bx = col * 8
by = row * 8
for r in range(8):
byte = mem[src + r]
for c in range(8):
if byte & (0x80 >> c):
px[bx + c, by + r] = 1
# C64 upper-only screen codes: 1..26 = A..Z, 27=[ 28=£ 29=] 30=↑ 31=←
# 32 = space, 33..63 = !"# ... ?, etc. We map ASCII directly.
for c in range(32, 64):
render_at(c, c) # symbols + digits 1:1
for c, code in enumerate("ABCDEFGHIJKLMNOPQRSTUVWXYZ"):
render_at(ord(code), 1 + c) # screen code 1..26 -> A..Z
img.save(out_path, "PNG", optimize=True)
def main():
if len(sys.argv) != 4:
print("usage: extractFromDump.py <dump-path> <level-name> <tilebank-id>", file=sys.stderr)
sys.exit(2)
dump_path = sys.argv[1]
level_name = sys.argv[2]
tilebank_id = int(sys.argv[3])
mem = load_dump(dump_path)
print(f"loaded {dump_path}: 64 KB OK")
tiles_dir = os.path.join(ROOT, "assets", "tiles")
sprites_dir = os.path.join(ROOT, "assets", "sprites")
levels_dir = os.path.join(ROOT, "assets", "levels")
os.makedirs(tiles_dir, exist_ok=True)
os.makedirs(sprites_dir, exist_ok=True)
os.makedirs(levels_dir, exist_ok=True)
# Build the screen-code -> bank-slot mapping once and feed it to
# both extractors so the tilemap and tilebank stay aligned.
code_to_slot = build_code_to_slot(mem)
# 8.3 DOS naming -- "tilebank0" is 9 chars and fopen fails under
# DOSBox strict 8.3 mode. Keep the short form across the pipeline.
tilebank_path = os.path.join(tiles_dir, f"tbank{tilebank_id}.png")
extract_tilebank(mem, tilebank_path, code_to_slot)
print(f" wrote {tilebank_path}")
level_path = os.path.join(levels_dir, f"{level_name}.txt")
extract_level(mem, level_name, tilebank_id, level_path, code_to_slot)
print(f" wrote {level_path}")
# Sprites + font come from level 1 only (sprite pointer table is
# script-driven so it differs by scene; we use the first dump's
# set as the canonical sheet, plus its charset as the font).
if tilebank_id == 1:
sprite_path = os.path.join(sprites_dir, "sprites.png")
extract_sprites(mem, sprite_path)
print(f" wrote {sprite_path}")
font_path = os.path.join(ROOT, "assets", "font.png")
extract_font(mem, font_path)
print(f" wrote {font_path}")
if __name__ == "__main__":
main()

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show more