Optimized a bit better. Examples are a mess of broken crap.
This commit is contained in:
parent
ad06ee59b7
commit
087bc77a6b
215 changed files with 23190 additions and 3746 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -45,3 +45,6 @@ Thumbs.db
|
||||||
# Crap I added
|
# Crap I added
|
||||||
stuff/*
|
stuff/*
|
||||||
docs/*
|
docs/*
|
||||||
|
|
||||||
|
# AGI game data (Sierra/fan-made AGI games used for testing; never committed)
|
||||||
|
examples/agi/gamedata/
|
||||||
|
|
|
||||||
218
AGI-SESSION-HANDOFF.md
Normal file
218
AGI-SESSION-HANDOFF.md
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
# AGI port — session handoff
|
||||||
|
|
||||||
|
This document captures the state of the AGI interpreter port as of
|
||||||
|
2026-05-15. Hand it to a fresh Claude Code session to pick up where
|
||||||
|
the previous one left off.
|
||||||
|
|
||||||
|
## Project context
|
||||||
|
|
||||||
|
JoeyLib is a unified C game-dev library targeting Apple IIgs (perf
|
||||||
|
floor / reference), Amiga, Atari ST, MS-DOS. The AGI port is a
|
||||||
|
from-scratch reimplementation of Sierra's Adventure Game Interpreter
|
||||||
|
v2 on top of JoeyLib's surface / draw / input APIs. Written from
|
||||||
|
public AGI format specs only -- NOT derived from ScummVM / NAGI /
|
||||||
|
Sarien (those are GPL'd; JoeyLib is not).
|
||||||
|
|
||||||
|
Test data: KQ3 v2, locally installed at
|
||||||
|
`examples/agi/gamedata/kq3/`. The user owns the game and is not
|
||||||
|
distributing it; gamedata is gitignored.
|
||||||
|
|
||||||
|
## Current phase
|
||||||
|
|
||||||
|
**Phase 1 sub-D** (callback-driven rendering): verified working.
|
||||||
|
Logic.0 + room 45 init paint KQ3's title screen on every supported
|
||||||
|
port. Ego sprite (VIEW 0, Graham) draws on top with priority masking,
|
||||||
|
WASD/arrow keys move him, ESC quits.
|
||||||
|
|
||||||
|
What's next: **Phase 1 sub-E** (input + animation opcodes). The
|
||||||
|
title screen does not auto-advance because the VM still stubs ~150
|
||||||
|
opcodes including `wait`, `set.menu`, `accept.input`, animation
|
||||||
|
timers, etc. Real KQ3 progression past the title requires implementing
|
||||||
|
enough of those to let logic.45 complete its intro and call new.room.
|
||||||
|
|
||||||
|
Phase 2 sub-A onward: parser, NPC actors, sound (NTP / PSG).
|
||||||
|
|
||||||
|
## What works (verified)
|
||||||
|
|
||||||
|
- Resource loader (LOGDIR / PICDIR / VIEWDIR / SNDDIR / VOL.x)
|
||||||
|
- PIC decoder (visual + priority planes, flood fill, vector draw)
|
||||||
|
- VIEW decoder (cel parse + RLE blit + priority masking + mirror)
|
||||||
|
- LOGIC VM: 256 vars, 256 flags, 8-deep call stack
|
||||||
|
- Real handlers: arithmetic (0x00-0x11), OP_IF (0xFF), OP_GOTO
|
||||||
|
(0xFE), OP_NEW_ROOM (0x12/0x13), OP_LOAD_LOGIC (0x14/0x15),
|
||||||
|
OP_CALL (0x16/0x17), OP_LOAD_PIC (0x18), OP_DRAW_PIC (0x19),
|
||||||
|
OP_SHOW_PIC (0x1A), OP_DISCARD_PIC (0x1B), OP_OVERLAY_PIC (0x1C)
|
||||||
|
- All ~180 other action opcodes are stubs (skip their arg bytes
|
||||||
|
via `kActionArgBytes[256]` table)
|
||||||
|
- Host callbacks: fetchLogic / loadPic / drawPic / showPic /
|
||||||
|
discardPic / overlayPic
|
||||||
|
- Per-platform save-under for the ego sprite (no full-stage memcpy
|
||||||
|
per frame; IIgs uses MVN-based codegen)
|
||||||
|
- Cross-platform binary works on all 4 ports (`AGI.EXE`,
|
||||||
|
`AGI.PRG`, `Agi`, IIgs `AGI`)
|
||||||
|
|
||||||
|
## What does NOT work
|
||||||
|
|
||||||
|
- **Title screen never advances.** Stubs the input / timer opcodes
|
||||||
|
the title uses. Workaround: starting-room override (argv[2]).
|
||||||
|
- **No NPC actors.** animate.obj / set.cel / set.loop are stubs.
|
||||||
|
- **No parser.** said() and the word-tokenize opcodes are stubs.
|
||||||
|
- **No sound.** Phase 3 work.
|
||||||
|
- **IIgs run path for AGI** is not wired. `scripts/run-agi.sh iigs`
|
||||||
|
prints an explicit "not yet wired" message describing the missing
|
||||||
|
disk-packager work (need bigger 2mg volume + case-folded ProDOS
|
||||||
|
names + game-data argument to `package-disk.sh`).
|
||||||
|
- **ST / Amiga can't pass argv.** Hatari `--auto` and Amiga
|
||||||
|
startup-sequence don't forward args, so the starting-room override
|
||||||
|
only works on DOS. ST / Amiga AGI starts at the title screen.
|
||||||
|
Fix needs either a config-file stage (`ROOM.TXT` read at startup)
|
||||||
|
or enough title opcodes for the title to advance naturally.
|
||||||
|
|
||||||
|
## How to test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source toolchains/env.sh
|
||||||
|
|
||||||
|
# Host-side parsers + VM (fast iteration, no emulator)
|
||||||
|
./scripts/test-agi.sh
|
||||||
|
|
||||||
|
# Cross-platform binary in DOSBox
|
||||||
|
./scripts/run-agi.sh dos # title screen
|
||||||
|
./scripts/run-agi.sh dos kq3 1 # jump to room 1
|
||||||
|
./scripts/run-agi.sh dos kq3 3 # jump to room 3
|
||||||
|
|
||||||
|
# Other platforms (no argv support yet, always title)
|
||||||
|
./scripts/run-agi.sh atarist
|
||||||
|
./scripts/run-agi.sh amiga
|
||||||
|
./scripts/run-agi.sh iigs # prints "not wired" message
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files touched (this session)
|
||||||
|
|
||||||
|
New:
|
||||||
|
|
||||||
|
- `src/core/random.c` — xorshift32 PRNG (jlRandom / jlRandomRange / jlRandomSeed)
|
||||||
|
- `examples/agi/*.c` — agi.c, agiRes.c, agiPic.c, agiView.c, agiVm.c
|
||||||
|
- `examples/agi/agi.h` — all AGI types + public API
|
||||||
|
- `tests/agi/*.c` — testAgiRes / testAgiPic / testAgiView / testAgiVm / testAgiPipeline
|
||||||
|
- `scripts/test-agi.sh` — host-build runner for all five tests
|
||||||
|
- `scripts/run-agi.sh` — cross-platform emulator launcher (dos | amiga | atarist | iigs)
|
||||||
|
|
||||||
|
Modified:
|
||||||
|
|
||||||
|
- `include/joey/core.h` — random prototypes
|
||||||
|
- `include/joey/platform.h` — auto-detect from compiler defines
|
||||||
|
- `.gitignore` — examples/agi/gamedata/
|
||||||
|
- All four make/*.mk — added agi to EXAMPLES list
|
||||||
|
|
||||||
|
## Architectural decisions worth remembering
|
||||||
|
|
||||||
|
### Render loop: save-under, not full stage copy
|
||||||
|
|
||||||
|
The naive per-frame `jlSurfaceCopy(stage, gBackdrop)` is 32 KB of
|
||||||
|
memcpy + present overhead -- not acceptable on a 2.8 MHz IIgs. The
|
||||||
|
current code:
|
||||||
|
|
||||||
|
1. `cbShowPic` paints the picture onto the stage ONCE and snapshots
|
||||||
|
it as `gBackdrop`. Sets `gPicReady = true` and invalidates any
|
||||||
|
prior ego save-under.
|
||||||
|
2. Per frame: `jlSpriteRestoreUnder` the last-frame ego rect (puts
|
||||||
|
clean backdrop bytes back), `jlSpriteSaveUnder` the new ego rect,
|
||||||
|
then `agiViewDraw` paints the cel with priority masking.
|
||||||
|
3. Placeholder sprite for save-under is 4x5 tiles = 32x40 stage
|
||||||
|
pixels, sized to cover any AGI v2 ego cel. Compiled via
|
||||||
|
`jlSpriteCompile` so save/restore use the platform's compiled
|
||||||
|
fast path (MVN block-copy on IIgs, etc.) instead of the C
|
||||||
|
interpreter fallback.
|
||||||
|
|
||||||
|
Per-frame memory traffic: ~1.3 KB save+restore vs ~32 KB full copy.
|
||||||
|
|
||||||
|
### VM throttling
|
||||||
|
|
||||||
|
AGI's native cycle is ~6 Hz. Running logic.0 every frame (60 Hz) is
|
||||||
|
6800-byte switch dispatcher × 60 = ~400k opcodes/sec for nothing.
|
||||||
|
`agi.c` now runs runVmCycle once every 10 frames (~6 Hz on 60 Hz
|
||||||
|
refresh, ~5 Hz on 50 Hz PAL). Input polling, ego movement, render
|
||||||
|
still run at full refresh -- only the bytecode interp is throttled.
|
||||||
|
|
||||||
|
Startup pumps the VM until `gPicReady` (or 32 cycles) so the user
|
||||||
|
never sees a flash of COLOR_MISSING before the first room paints.
|
||||||
|
|
||||||
|
### VIEW mirror handling
|
||||||
|
|
||||||
|
Sierra's encoder writes `mirror=1, src=N` on BOTH loops in a mirror
|
||||||
|
pair, where N is the lower-numbered loop. `agiView.c:resolveMirror`
|
||||||
|
must only treat a cel as mirrored when `src != current_loop`. KQ3
|
||||||
|
view 0 hit this directly: both loop 0 (right) and loop 1 (left) had
|
||||||
|
`mirror=1, src=0`, and our original code flipped both -- so pressing
|
||||||
|
left and right both faced left. Fixed.
|
||||||
|
|
||||||
|
### Starting-room override
|
||||||
|
|
||||||
|
`agi.c` argv[2] = starting room number 1..255. When set,
|
||||||
|
`runVmCycle`'s NEW_ROOM handler substitutes the override for
|
||||||
|
`vm.newRoomId` on the FIRST NEW_ROOM halt only (one-shot). Lets the
|
||||||
|
user skip past KQ3's title room (45) into the actual game. Once the
|
||||||
|
override fires, subsequent transitions go through unchanged.
|
||||||
|
|
||||||
|
`scripts/run-agi.sh` accepts `<platform> [game] [room]` and forwards
|
||||||
|
the room to DOS via dosbox `-c "AGI.EXE . <room>"`. ST and Amiga
|
||||||
|
can't pass argv via autostart so their AGI starts at the title.
|
||||||
|
|
||||||
|
### Default game dir is "." not "agidemo"
|
||||||
|
|
||||||
|
`resolveGameDir` defaults to "." (CWD). On platforms whose autostart
|
||||||
|
can't pass argv (Hatari, AmigaDOS), `run-agi.sh` stages the AGI
|
||||||
|
binary plus the v2 game data files (LOGDIR / PICDIR / etc.) flat in
|
||||||
|
a temp directory and points the emulator at it; AGI runs from CWD =
|
||||||
|
that temp dir and finds the data files relative to "./".
|
||||||
|
|
||||||
|
## Likely next-step work
|
||||||
|
|
||||||
|
The user's natural next ask is "implement enough title opcodes to
|
||||||
|
let KQ3's title screen auto-advance." That probably means:
|
||||||
|
|
||||||
|
- `wait` (cycle.time gate)
|
||||||
|
- `input.line` / `accept.input`
|
||||||
|
- Animation timers: `cycle.time`, `current.loop`, etc.
|
||||||
|
- `set.menu` / `disable.item` (KQ3 title shows a menu)
|
||||||
|
|
||||||
|
Alternative path: skip the title work and implement enough core
|
||||||
|
opcodes to make a gameplay room functional. animate.obj,
|
||||||
|
set.view.v, set.cel, set.loop, position, draw, erase. That gets us
|
||||||
|
walking around KQ3 room 1 (Manannan's house) with Graham instead
|
||||||
|
of a static title.
|
||||||
|
|
||||||
|
The user should pick which to prioritize.
|
||||||
|
|
||||||
|
## Memory entries you should know about
|
||||||
|
|
||||||
|
The repo's auto-memory at
|
||||||
|
`~/.claude/projects/-home-scott-claude-joeylib/memory/` includes
|
||||||
|
JoeyLib-wide context (CMake quirks, ORCA-C gotchas, perf
|
||||||
|
constraints) plus `project_joeylib_agi_plan.md` which describes the
|
||||||
|
overall AGI port plan. All persist into the next session
|
||||||
|
automatically; no need to reload.
|
||||||
|
|
||||||
|
Specifically relevant to the AGI port:
|
||||||
|
|
||||||
|
- `project_joeylib_agi_plan.md` — overall plan, 3-4 month estimate
|
||||||
|
- `feedback_orca_c_atoi_segment.md` — don't use atoi in AGI code
|
||||||
|
(it pushes the IIgs ORCA-C stdio cluster past 64 KB; we use a
|
||||||
|
manual parseArgU8 instead)
|
||||||
|
- `project_perf_directive.md` — IIgs is the perf floor; every
|
||||||
|
other target must match or beat it
|
||||||
|
- `feedback_iigs_speed.md` — IIgs must be absolute fastest
|
||||||
|
technically possible; the save-under decision was made under
|
||||||
|
this constraint.
|
||||||
|
|
||||||
|
## Git / user preferences
|
||||||
|
|
||||||
|
- The user manages git themselves -- do NOT commit / push / branch
|
||||||
|
on JoeyLib without explicit ask (see memory
|
||||||
|
`feedback_git.md`).
|
||||||
|
- Always `source toolchains/env.sh` before any `make` -- the user
|
||||||
|
has added that to .claude/settings.local.json allow list.
|
||||||
|
- DJGPP / ORCA / vasm / m68k-atari-mint paths come from env.sh.
|
||||||
|
- The user does NOT need hand-holding -- they're an expert C dev.
|
||||||
|
Skip excessive explanation; describe what was done and move on.
|
||||||
64
PERF.md
64
PERF.md
|
|
@ -1,36 +1,36 @@
|
||||||
| Op | IIgs (ops/sec) | Amiga (vs IIGS) | Atari ST (vs IIGS) | DOS (vs IIGS) |
|
| Op | IIgs (ops/sec) | Amiga (vs IIGS) | Atari ST (vs IIGS) | DOS (vs IIGS) |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| surfaceClear | 28 | 2.54x | 1.11x | 638.57x |
|
| jlSurfaceClear | 28 | 2.54x | 1.11x | 638.57x |
|
||||||
| paletteSet | 678 | 8.68x | 4.23x | 26.13x |
|
| jlPaletteSet | 678 | 8.68x | 4.23x | 26.13x |
|
||||||
| scbSetRange | 1005 | 3.66x | 1.86x | 11.16x |
|
| jlScbSetRange | 1005 | 3.66x | 1.86x | 11.16x |
|
||||||
| drawPixel | 1755 | 1.85x | 1.01x | 9.70x |
|
| jlDrawPixel | 1755 | 1.85x | 1.01x | 9.70x |
|
||||||
| drawLine H | 682 | 2.19x | 1.22x | 19.26x |
|
| jlDrawLine H | 682 | 2.19x | 1.22x | 19.26x |
|
||||||
| drawLine V | 90 | 1.67x | 1.11x | 115.30x |
|
| jlDrawLine V | 90 | 1.67x | 1.11x | 115.30x |
|
||||||
| drawLine diag | 35 | 1.14x | 1.06x | 261.49x |
|
| jlDrawLine diag | 35 | 1.14x | 1.06x | 261.49x |
|
||||||
| drawRect 100x100 | 75 | 1.57x | 1.04x | 250.77x |
|
| jlDrawRect 100x100 | 75 | 1.57x | 1.04x | 250.77x |
|
||||||
| drawCircle r=16 | 232 | 1.21x | **0.65x** | 71.24x |
|
| jlDrawCircle r=16 | 232 | 1.21x | **0.65x** | 71.24x |
|
||||||
| drawCircle r=80 | 56 | 1.16x | **0.61x** | 310.93x |
|
| jlDrawCircle r=80 | 56 | 1.16x | **0.61x** | 310.93x |
|
||||||
| fillRect 16x16 | 450 | 1.24x | 1.04x | 39.38x |
|
| jlFillRect 16x16 | 450 | 1.24x | 1.04x | 39.38x |
|
||||||
| fillRect 80x80 | 75 | **0.95x** | 1.28x | 206.73x |
|
| jlFillRect 80x80 | 75 | **0.95x** | 1.28x | 206.73x |
|
||||||
| fillRect 320x200 | 60 | **0.93x** | **0.43x** | 184.62x |
|
| jlFillRect 320x200 | 60 | **0.93x** | **0.43x** | 184.62x |
|
||||||
| fillCircle r=40 | 38 | **0.97x** | 1.39x | 347.24x |
|
| jlFillCircle r=40 | 38 | **0.97x** | 1.39x | 347.24x |
|
||||||
| samplePixel | 1916 | 3.48x | 1.92x | 18.04x |
|
| jlSamplePixel | 1916 | 3.48x | 1.92x | 18.04x |
|
||||||
| tileFill | 1252 | 1.73x | **0.93x** | 10.44x |
|
| jlTileFill | 1252 | 1.73x | **0.93x** | 10.44x |
|
||||||
| tileCopy | 997 | 1.80x | 1.02x | 16.96x |
|
| jlTileCopy | 997 | 1.80x | 1.02x | 16.96x |
|
||||||
| tileCopyMasked | 498 | 1.76x | 1.08x | 28.55x |
|
| jlTileCopyMasked | 498 | 1.76x | 1.08x | 28.55x |
|
||||||
| tilePaste | 1106 | 1.94x | 1.09x | 13.53x |
|
| jlTilePaste | 1106 | 1.94x | 1.09x | 13.53x |
|
||||||
| tileSnap | 1473 | 2.26x | 1.28x | 9.44x |
|
| jlTileSnap | 1473 | 2.26x | 1.28x | 9.44x |
|
||||||
| spriteSaveUnder | 528 | 2.21x | 1.29x | 20.14x |
|
| jlSpriteSaveUnder | 528 | 2.21x | 1.29x | 20.14x |
|
||||||
| spriteDraw | 438 | 1.82x | 1.28x | 38.39x |
|
| jlSpriteDraw | 438 | 1.82x | 1.28x | 38.39x |
|
||||||
| spriteRestoreUnder | 487 | 2.00x | 1.11x | 36.71x |
|
| jlSpriteRestoreUnder | 487 | 2.00x | 1.11x | 36.71x |
|
||||||
| spriteSaveAndDraw | 277 | 1.82x | 1.06x | 72.79x |
|
| jlSpriteSaveAndDraw | 277 | 1.82x | 1.06x | 72.79x |
|
||||||
| stagePresent full | 42 | 6.31x | 1.40x | 373.83x |
|
| jlStagePresent full | 42 | 6.31x | 1.40x | 373.83x |
|
||||||
| joeyInputPoll | 273 | 13.90x | 4.19x | 57.15x |
|
| jlInputPoll | 273 | 13.90x | 4.19x | 57.15x |
|
||||||
| joeyKeyDown | 3382 | 7.82x | 3.76x | 18.93x |
|
| jlKeyDown | 3382 | 7.82x | 3.76x | 18.93x |
|
||||||
| joeyKeyPressed | 3345 | 7.65x | 3.81x | 18.72x |
|
| jlKeyPressed | 3345 | 7.65x | 3.81x | 18.72x |
|
||||||
| joeyMouseX | 4170 | 10.26x | 5.22x | 26.27x |
|
| jlMouseX | 4170 | 10.26x | 5.22x | 26.27x |
|
||||||
| joeyJoyConnected | 3378 | 7.76x | 3.71x | 18.95x |
|
| joeyJoyConnected | 3378 | 7.76x | 3.71x | 18.95x |
|
||||||
| joeyAudioFrameTick | 4106 | 7.99x | 3.04x | 12.37x |
|
| jlAudioFrameTick | 4106 | 7.99x | 3.04x | 12.37x |
|
||||||
| joeyAudioIsPlayingMod | 3536 | 9.28x | 4.33x | 26.78x |
|
| jlAudioIsPlayingMod | 3536 | 9.28x | 4.33x | 26.78x |
|
||||||
| surfaceMarkDirtyRect (via fillRect 32x32) | 240 | 1.38x | 1.26x | 58.95x |
|
| surfaceMarkDirtyRect (via jlFillRect 32x32) | 240 | 1.38x | 1.26x | 58.95x |
|
||||||
| c2p full screen | - | - | - | - |
|
| c2p full screen | - | - | - | - |
|
||||||
|
|
|
||||||
274
README.md
274
README.md
|
|
@ -50,7 +50,7 @@ src/core/ portable library code
|
||||||
src/codegen/ runtime sprite codegen (per-CPU emitters)
|
src/codegen/ runtime sprite codegen (per-CPU emitters)
|
||||||
src/port/<plat>/ per-platform HAL implementations
|
src/port/<plat>/ per-platform HAL implementations
|
||||||
src/shared68k/ assembly shared by Amiga and Atari ST
|
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)
|
tools/joeymod/ Protracker .MOD converter (passthrough or .NTP)
|
||||||
examples/ example programs
|
examples/ example programs
|
||||||
toolchains/ self-contained cross-build tools
|
toolchains/ self-contained cross-build tools
|
||||||
|
|
@ -79,18 +79,17 @@ typedef struct {
|
||||||
uint32_t codegenBytes; // runtime compiled-sprite cache size
|
uint32_t codegenBytes; // runtime compiled-sprite cache size
|
||||||
uint16_t maxSurfaces; // maximum concurrent surfaces
|
uint16_t maxSurfaces; // maximum concurrent surfaces
|
||||||
uint32_t audioBytes; // audio sample / module RAM pool
|
uint32_t audioBytes; // audio sample / module RAM pool
|
||||||
uint32_t assetBytes; // tileset / sprite / map RAM pool
|
} jlConfigT;
|
||||||
} JoeyConfigT;
|
|
||||||
|
|
||||||
bool joeyInit (const JoeyConfigT *config);
|
bool jlInit (const jlConfigT *config);
|
||||||
void joeyShutdown (void);
|
void jlShutdown (void);
|
||||||
const char *joeyLastError (void);
|
const char *jlLastError (void);
|
||||||
const char *joeyPlatformName (void);
|
const char *jlPlatformName (void);
|
||||||
const char *joeyVersionString(void);
|
const char *jlVersionString(void);
|
||||||
|
|
||||||
void joeyWaitVBL (void); // block until next VBL
|
void jlWaitVBL (void); // block until next VBL
|
||||||
uint16_t joeyFrameCount (void); // monotonic 16-bit frame counter
|
uint16_t jlFrameCount (void); // monotonic 16-bit frame counter
|
||||||
uint16_t joeyFrameHz (void); // 50 / 60 / 70 depending on port
|
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_PALETTE_COUNT 16
|
||||||
#define SURFACE_COLORS_PER_PALETTE 16
|
#define SURFACE_COLORS_PER_PALETTE 16
|
||||||
|
|
||||||
typedef struct SurfaceT SurfaceT; // opaque
|
typedef struct jlSurfaceT jlSurfaceT; // opaque
|
||||||
|
|
||||||
SurfaceT *surfaceCreate (void);
|
jlSurfaceT *jlSurfaceCreate (void);
|
||||||
void surfaceDestroy(SurfaceT *s);
|
void jlSurfaceDestroy(jlSurfaceT *s);
|
||||||
SurfaceT *stageGet (void); // library back-buffer
|
jlSurfaceT *jlStageGet (void); // library back-buffer
|
||||||
void surfaceCopy (SurfaceT *dst, const SurfaceT *src);
|
void jlSurfaceCopy (jlSurfaceT *dst, const jlSurfaceT *src);
|
||||||
|
|
||||||
bool surfaceSaveFile(const SurfaceT *src, const char *path);
|
bool jlSurfaceSaveFile(const jlSurfaceT *src, const char *path);
|
||||||
bool surfaceLoadFile(SurfaceT *dst, const char *path);
|
bool jlSurfaceLoadFile(jlSurfaceT *dst, const char *path);
|
||||||
uint32_t surfaceHash (const SurfaceT *s); // FNV-1a of logical pixels
|
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.
|
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).
|
need transparency).
|
||||||
|
|
||||||
```c
|
```c
|
||||||
void surfaceClear (SurfaceT *s, uint8_t color);
|
void jlSurfaceClear (jlSurfaceT *s, uint8_t color);
|
||||||
void drawPixel (SurfaceT *s, int16_t x, int16_t y, uint8_t color);
|
void jlDrawPixel (jlSurfaceT *s, int16_t x, int16_t y, uint8_t color);
|
||||||
uint8_t samplePixel (const SurfaceT *s, int16_t x, int16_t y);
|
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);
|
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);
|
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);
|
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);
|
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);
|
uint16_t r, uint8_t color);
|
||||||
|
|
||||||
void floodFill (SurfaceT *s, int16_t x, int16_t y, uint8_t newColor);
|
void jlFloodFill (jlSurfaceT *s, int16_t x, int16_t y, uint8_t newColor);
|
||||||
void floodFillBounded (SurfaceT *s, int16_t x, int16_t y,
|
void jlFloodFillBounded (jlSurfaceT *s, int16_t x, int16_t y,
|
||||||
uint8_t newColor, uint8_t boundaryColor);
|
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`)
|
### Palette and SCB (`joey/palette.h`)
|
||||||
|
|
||||||
Colors are 12-bit `$0RGB`. Color 0 of every palette is forced to
|
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.
|
via the SCB.
|
||||||
|
|
||||||
```c
|
```c
|
||||||
void paletteSet (SurfaceT *s, uint8_t paletteIndex, const uint16_t *colors16);
|
void jlPaletteSet (jlSurfaceT *s, uint8_t paletteIndex, const uint16_t *colors16);
|
||||||
void paletteGet (const SurfaceT *s, uint8_t paletteIndex, uint16_t *out16);
|
void jlPaletteGet (const jlSurfaceT *s, uint8_t paletteIndex, uint16_t *out16);
|
||||||
void scbSet (SurfaceT *s, uint16_t line, uint8_t paletteIndex);
|
void jlScbSet (jlSurfaceT *s, uint16_t line, uint8_t paletteIndex);
|
||||||
void scbSetRange (SurfaceT *s, uint16_t firstLine, uint16_t lastLine,
|
void jlScbSetRange (jlSurfaceT *s, uint16_t firstLine, uint16_t lastLine,
|
||||||
uint8_t paletteIndex);
|
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`)
|
### Tiles (`joey/tile.h`)
|
||||||
|
|
||||||
A "tile" is just an 8x8-aligned region of any surface. The API moves
|
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.
|
type so callers can stash a copy without allocating a scratch surface.
|
||||||
|
|
||||||
```c
|
```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_BLOCKS_PER_COL (SURFACE_HEIGHT / TILE_PIXELS_PER_SIDE) // 25
|
||||||
#define TILE_NO_GLYPH ((uint16_t)0xFFFFu)
|
#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,
|
void jlTileCopy (jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy,
|
||||||
const SurfaceT *src, uint8_t srcBx, uint8_t srcBy);
|
const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy);
|
||||||
void tileCopyMasked (SurfaceT *dst, uint8_t dstBx, uint8_t dstBy,
|
void jlTileCopyMasked (jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy,
|
||||||
const SurfaceT *src, uint8_t srcBx, uint8_t srcBy,
|
const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy,
|
||||||
uint8_t transparentIndex);
|
uint8_t transparentIndex);
|
||||||
void tileFill (SurfaceT *s, uint8_t bx, uint8_t by, uint8_t color);
|
void jlTileFill (jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t color);
|
||||||
void tileSnap (const SurfaceT *src, uint8_t bx, uint8_t by, TileT *out);
|
void jlTileSnap (const jlSurfaceT *src, uint8_t bx, uint8_t by, jlTileT *out);
|
||||||
void tilePaste (SurfaceT *dst, uint8_t bx, uint8_t by, const TileT *in);
|
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,
|
// Load up to maxTiles tiles from a baked .tbk file (per-target planar
|
||||||
const SurfaceT *fontSurface, const uint16_t *asciiMap,
|
// 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);
|
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.
|
into per-shift code variants for fast draws.
|
||||||
|
|
||||||
```c
|
```c
|
||||||
typedef struct SpriteT SpriteT; // opaque
|
typedef struct jlSpriteT jlSpriteT; // opaque
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
SpriteT *sprite;
|
jlSpriteT *sprite;
|
||||||
int16_t x, y;
|
int16_t x, y;
|
||||||
uint16_t width, height; // pixels
|
uint16_t width, height; // pixels
|
||||||
uint8_t *bytes; // caller-owned save-under buffer
|
uint8_t *bytes; // caller-owned save-under buffer
|
||||||
uint16_t sizeBytes;
|
uint16_t sizeBytes;
|
||||||
} SpriteBackupT;
|
} jlSpriteBackupT;
|
||||||
|
|
||||||
SpriteT *spriteCreate (const uint8_t *tileData,
|
jlSpriteT *jlSpriteCreate (const uint8_t *tileData,
|
||||||
uint8_t widthTiles, uint8_t heightTiles);
|
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);
|
uint8_t widthTiles, uint8_t heightTiles);
|
||||||
SpriteT *spriteLoadFile (const char *path);
|
void jlSpriteDestroy (jlSpriteT *sp);
|
||||||
SpriteT *spriteFromCompiledMem (const uint8_t *data, uint32_t length);
|
|
||||||
bool spriteSaveFile (SpriteT *sp, const char *path);
|
|
||||||
void spriteDestroy (SpriteT *sp);
|
|
||||||
|
|
||||||
bool spriteCompile (SpriteT *sp); // build per-shift fast path
|
// Load up to maxCels cels from a baked .spr file (cross-target chunky
|
||||||
void spritePrewarm (SpriteT *sp); // hint: compile if not already
|
// 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);
|
bool jlSpriteCompile (jlSpriteT *sp); // build per-shift fast path
|
||||||
void spriteSaveUnder (const SurfaceT *s, SpriteT *sp,
|
void jlSpritePrewarm (jlSpriteT *sp); // hint: compile if not already
|
||||||
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);
|
|
||||||
|
|
||||||
void spriteCompact (void); // defrag the codegen arena
|
void jlSpriteDraw (jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y);
|
||||||
uint32_t spriteCodegenBytesUsed (void);
|
void jlSpriteSaveUnder (const jlSurfaceT *s, jlSpriteT *sp,
|
||||||
uint32_t spriteCodegenBytesTotal (void);
|
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.
|
Asset PNGs live in each example's `assets/` directory and are baked
|
||||||
Use embedded `const JoeyAssetT` for ship-with-binary art; use the
|
to native binary blobs at build time by `tools/assetbake/assetbake.py`
|
||||||
loaders for on-disk assets.
|
(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
|
Two blob formats:
|
||||||
typedef struct {
|
|
||||||
uint16_t width;
|
* **`.tbk` (tile bank)** -- one or more 8x8 tiles in per-target
|
||||||
uint16_t height;
|
planar layout (Amiga plane-major; Atari ST row-major-with-planes-
|
||||||
bool hasPalette;
|
per-row; DOS / IIgs chunky 4bpp). The loader `jlTileBankLoad`
|
||||||
uint16_t palette[16]; // valid only if hasPalette
|
rejects files baked for the wrong target.
|
||||||
const uint8_t *pixels; // 4bpp packed, rowBytes = (width+1)/2
|
* **`.spr` (sprite cel set)** -- one or more uniform-sized sprite
|
||||||
} JoeyAssetT;
|
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`)
|
### Present (`joey/present.h`)
|
||||||
|
|
||||||
```c
|
```c
|
||||||
void stagePresent(void);
|
void jlStagePresent(void);
|
||||||
```
|
```
|
||||||
|
|
||||||
Flips the dirty rows of the stage to the display, then clears dirty
|
Flips the dirty rows of the stage to the display, then clears dirty
|
||||||
state. Drawing primitives mark dirty as a side effect, so calling
|
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`)
|
### 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
|
Edge predicates (`*Pressed`, `*Released`) fire only in the frame the
|
||||||
transition happened.
|
transition happened.
|
||||||
|
|
||||||
|
|
@ -293,35 +317,35 @@ transition happened.
|
||||||
typedef enum { /* KEY_NONE, KEY_A..KEY_Z, KEY_0..KEY_9, KEY_SPACE,
|
typedef enum { /* KEY_NONE, KEY_A..KEY_Z, KEY_0..KEY_9, KEY_SPACE,
|
||||||
KEY_ESCAPE, KEY_RETURN, KEY_TAB, KEY_BACKSPACE,
|
KEY_ESCAPE, KEY_RETURN, KEY_TAB, KEY_BACKSPACE,
|
||||||
KEY_UP/DOWN/LEFT/RIGHT, KEY_LSHIFT/RSHIFT/LCTRL/LALT,
|
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,
|
typedef enum { MOUSE_BUTTON_NONE, MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT,
|
||||||
MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_COUNT } JoeyMouseButtonE;
|
MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_COUNT } jlMouseButtonE;
|
||||||
typedef enum { JOYSTICK_0, JOYSTICK_1, JOYSTICK_COUNT } JoeyJoystickE;
|
typedef enum { JOYSTICK_0, JOYSTICK_1, JOYSTICK_COUNT } jlJoystickE;
|
||||||
typedef enum { JOY_BUTTON_0, JOY_BUTTON_1, JOY_BUTTON_COUNT } JoeyJoyButtonE;
|
typedef enum { JOY_BUTTON_0, JOY_BUTTON_1, JOY_BUTTON_COUNT } jlJoyButtonE;
|
||||||
|
|
||||||
#define JOYSTICK_AXIS_MAX 127
|
#define JOYSTICK_AXIS_MAX 127
|
||||||
#define JOYSTICK_AXIS_MIN (-127)
|
#define JOYSTICK_AXIS_MIN (-127)
|
||||||
|
|
||||||
void joeyInputPoll (void);
|
void jlInputPoll (void);
|
||||||
void joeyWaitForAnyKey (void);
|
void jlWaitForAnyKey (void);
|
||||||
|
|
||||||
bool joeyKeyDown (JoeyKeyE key);
|
bool jlKeyDown (jlKeyE key);
|
||||||
bool joeyKeyPressed (JoeyKeyE key);
|
bool jlKeyPressed (jlKeyE key);
|
||||||
bool joeyKeyReleased (JoeyKeyE key);
|
bool jlKeyReleased (jlKeyE key);
|
||||||
|
|
||||||
int16_t joeyMouseX (void);
|
int16_t jlMouseX (void);
|
||||||
int16_t joeyMouseY (void);
|
int16_t jlMouseY (void);
|
||||||
bool joeyMouseDown (JoeyMouseButtonE b);
|
bool jlMouseDown (jlMouseButtonE b);
|
||||||
bool joeyMousePressed (JoeyMouseButtonE b);
|
bool jlMousePressed (jlMouseButtonE b);
|
||||||
bool joeyMouseReleased (JoeyMouseButtonE b);
|
bool jlMouseReleased (jlMouseButtonE b);
|
||||||
|
|
||||||
bool joeyJoystickConnected(JoeyJoystickE js);
|
bool jlJoystickConnected(jlJoystickE js);
|
||||||
int8_t joeyJoystickX (JoeyJoystickE js);
|
int8_t jlJoystickX (jlJoystickE js);
|
||||||
int8_t joeyJoystickY (JoeyJoystickE js);
|
int8_t jlJoystickY (jlJoystickE js);
|
||||||
bool joeyJoyDown (JoeyJoystickE js, JoeyJoyButtonE b);
|
bool jlJoyDown (jlJoystickE js, jlJoyButtonE b);
|
||||||
bool joeyJoyPressed (JoeyJoystickE js, JoeyJoyButtonE b);
|
bool jlJoyPressed (jlJoystickE js, jlJoyButtonE b);
|
||||||
bool joeyJoyReleased (JoeyJoystickE js, JoeyJoyButtonE b);
|
bool jlJoyReleased (jlJoystickE js, jlJoyButtonE b);
|
||||||
void joeyJoystickReset (JoeyJoystickE js, uint8_t deadZone);
|
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
|
4-channel Protracker-style music plus four one-shot SFX slots. Module
|
||||||
data must be the platform-native form produced by `tools/joeymod`
|
data must be the platform-native form produced by `tools/joeymod`
|
||||||
(`.mod` for Amiga/DOS/ST; `.ntp` for IIgs; `.amod` if you want
|
(`.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.
|
of the API stays callable as no-ops.
|
||||||
|
|
||||||
```c
|
```c
|
||||||
#define JOEY_AUDIO_SFX_SLOTS 4
|
#define JOEY_AUDIO_SFX_SLOTS 4
|
||||||
|
|
||||||
bool joeyAudioInit (void);
|
bool jlAudioInit (void);
|
||||||
void joeyAudioShutdown (void);
|
void jlAudioShutdown (void);
|
||||||
|
|
||||||
void joeyAudioPlayMod (const uint8_t *data, uint32_t length, bool loop);
|
void jlAudioPlayMod (const uint8_t *data, uint32_t length, bool loop);
|
||||||
void joeyAudioStopMod (void);
|
void jlAudioStopMod (void);
|
||||||
bool joeyAudioIsPlayingMod (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);
|
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`)
|
### Debug logging (`joey/debug.h`)
|
||||||
|
|
||||||
Crash-tracing logger. Writes are buffered and durable across normal
|
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.
|
a guaranteed last-line-on-disk.
|
||||||
|
|
||||||
```c
|
```c
|
||||||
void joeyLog (const char *msg);
|
void jlLog (const char *msg);
|
||||||
void joeyLogF (const char *fmt, ...);
|
void jlLogF (const char *fmt, ...);
|
||||||
void joeyLogFlush(void);
|
void jlLogFlush(void);
|
||||||
void joeyLogReset(void);
|
void jlLogReset(void);
|
||||||
```
|
```
|
||||||
|
|
||||||
Output goes to `joeylog.txt` in the program's working directory.
|
Output goes to `joeylog.txt` in the program's working directory.
|
||||||
|
|
|
||||||
2328
examples/adventure/adventure.c
Normal file
2328
examples/adventure/adventure.c
Normal file
File diff suppressed because it is too large
Load diff
1450
examples/adventure2/adventure2.c
Normal file
1450
examples/adventure2/adventure2.c
Normal file
File diff suppressed because it is too large
Load diff
1873
examples/agi/agi.c
Normal file
1873
examples/agi/agi.c
Normal file
File diff suppressed because it is too large
Load diff
562
examples/agi/agi.h
Normal file
562
examples/agi/agi.h
Normal file
|
|
@ -0,0 +1,562 @@
|
||||||
|
// JoeyLib AGI interpreter (Sierra Adventure Game Interpreter, v2).
|
||||||
|
//
|
||||||
|
// Reimplements the engine that ran Sierra's first wave of graphic
|
||||||
|
// adventures (KQ1-3, SQ1-2, PQ1, LSL1, Manhunter 1-2) on top of
|
||||||
|
// JoeyLib's surface/draw/input APIs. v2 format only for Phase 0; v3
|
||||||
|
// (LZW-compressed) lands in a later phase.
|
||||||
|
//
|
||||||
|
// This is a from-scratch implementation written against the public
|
||||||
|
// AGI format specification, not derived from any GPL'd reference.
|
||||||
|
|
||||||
|
#ifndef AGI_H
|
||||||
|
#define AGI_H
|
||||||
|
|
||||||
|
#include "joey/types.h"
|
||||||
|
#include "joey/surface.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
|
||||||
|
// AGI resource directories are sized by the on-disk file length. The
|
||||||
|
// maximum index any single game references is well under 256 for the
|
||||||
|
// supported game set; oversize directories are rejected at load.
|
||||||
|
#define AGI_MAX_RESOURCES 256
|
||||||
|
|
||||||
|
// AGI VOL.x files are numbered 0..15 (4-bit field in the directory
|
||||||
|
// entry). One open FILE * per volume kept resident for the life of
|
||||||
|
// the game session.
|
||||||
|
#define AGI_MAX_VOLUMES 16
|
||||||
|
|
||||||
|
// AGI's native rendering surface. Visual and priority screens are
|
||||||
|
// each 160 wide; horizontal pixel doubling produces the 320-wide
|
||||||
|
// display. The vertical 168 leaves room above and below for the
|
||||||
|
// status line and command prompt in a 200-line display.
|
||||||
|
#define AGI_PIC_WIDTH 160
|
||||||
|
#define AGI_PIC_HEIGHT 168
|
||||||
|
#define AGI_PIC_PIXELS (AGI_PIC_WIDTH * AGI_PIC_HEIGHT)
|
||||||
|
|
||||||
|
// Flood-fill spread tests: a fill spreads through pixels matching the
|
||||||
|
// background color and stops at anything else. Visual default bg is
|
||||||
|
// 15 (white), priority default bg is 4 (red).
|
||||||
|
#define AGI_VISUAL_BG 15u
|
||||||
|
#define AGI_PRIORITY_BG 4u
|
||||||
|
|
||||||
|
// AGI 16-color CGA-derived palette in JoeyLib's $0RGB format (4-bit
|
||||||
|
// per channel). Entry 6 keeps the classic CGA "brown" tweak (R=A,
|
||||||
|
// G=5, B=0) so the muddy-yellow-on-CGA-monitors look matches the
|
||||||
|
// original render rather than reading as pure yellow-green.
|
||||||
|
extern const uint16_t kAgiPalette[16];
|
||||||
|
|
||||||
|
// Resource directory entry: which VOL.x file holds the resource and
|
||||||
|
// where in that file it starts. A 0xFF marker in volume means the
|
||||||
|
// directory slot is empty (game does not define this resource ID).
|
||||||
|
typedef struct {
|
||||||
|
uint8_t volume;
|
||||||
|
uint32_t offset;
|
||||||
|
} AgiResEntryT;
|
||||||
|
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
AGI_RES_LOGIC = 0,
|
||||||
|
AGI_RES_PIC = 1,
|
||||||
|
AGI_RES_VIEW = 2,
|
||||||
|
AGI_RES_SOUND = 3,
|
||||||
|
AGI_RES_COUNT = 4
|
||||||
|
} AgiResTypeE;
|
||||||
|
|
||||||
|
|
||||||
|
// Open-game state: directory metadata + handles to every VOL.x file.
|
||||||
|
// Loading and freeing individual resource blobs is per-call so the
|
||||||
|
// working-set footprint stays small enough for stock IIgs / Amiga.
|
||||||
|
typedef struct {
|
||||||
|
FILE *volFiles[AGI_MAX_VOLUMES];
|
||||||
|
uint16_t resCount[AGI_RES_COUNT];
|
||||||
|
AgiResEntryT resDir[AGI_RES_COUNT][AGI_MAX_RESOURCES];
|
||||||
|
} AgiGameT;
|
||||||
|
|
||||||
|
|
||||||
|
// Decoded PIC working buffers. Both planes are 160x168 chunky 8bpp;
|
||||||
|
// only the low nibble (4 bits) of each byte carries actual color
|
||||||
|
// information. Buffers are heap-allocated so the binary's BSS
|
||||||
|
// footprint stays under what the IIgs stock heap can satisfy in
|
||||||
|
// one shot.
|
||||||
|
typedef struct {
|
||||||
|
uint8_t *visual;
|
||||||
|
uint8_t *priority;
|
||||||
|
} AgiPicT;
|
||||||
|
|
||||||
|
|
||||||
|
// VIEW resources hold one or more animation loops; each loop is a
|
||||||
|
// sequence of cels (frames). The parser walks the published format
|
||||||
|
// once and records pointers into the raw resource buffer so per-cel
|
||||||
|
// access is O(1) without re-parsing.
|
||||||
|
#define AGI_MAX_LOOPS_PER_VIEW 16u
|
||||||
|
#define AGI_MAX_CELS_PER_LOOP 32u
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t width;
|
||||||
|
uint8_t height;
|
||||||
|
uint8_t transparentColor;
|
||||||
|
uint8_t mirrored;
|
||||||
|
uint8_t mirrorSourceLoop;
|
||||||
|
const uint8_t *rleData;
|
||||||
|
} AgiCelInfoT;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t celCount;
|
||||||
|
AgiCelInfoT cels[AGI_MAX_CELS_PER_LOOP];
|
||||||
|
} AgiLoopInfoT;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t *raw;
|
||||||
|
uint16_t rawLength;
|
||||||
|
uint8_t loopCount;
|
||||||
|
AgiLoopInfoT loops[AGI_MAX_LOOPS_PER_VIEW];
|
||||||
|
} AgiViewT;
|
||||||
|
|
||||||
|
|
||||||
|
// AGI priority for an actor based on its feet (bottom-edge) Y.
|
||||||
|
// 0..47 is the sky band (priority 4); 48..167 is ground divided into
|
||||||
|
// 10 bands at priority 5..14 (12 pixels each). Priority 15 is "always
|
||||||
|
// on top" and isn't reached by the actor's natural Y.
|
||||||
|
uint8_t agiActorPriorityForY(int16_t y);
|
||||||
|
|
||||||
|
|
||||||
|
// LOGIC VM state. AGI's interpreter is a stack-of-logics bytecode
|
||||||
|
// VM with 256 byte-sized variables and 256 single-bit flags.
|
||||||
|
#define AGI_VM_NUM_VARS 256u
|
||||||
|
#define AGI_VM_NUM_FLAGS 256u
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
AGI_VM_HALT_NONE = 0,
|
||||||
|
AGI_VM_HALT_RETURN = 1,
|
||||||
|
AGI_VM_HALT_UNKNOWN_OP = 2,
|
||||||
|
AGI_VM_HALT_TRUNCATED = 3,
|
||||||
|
AGI_VM_HALT_NEW_ROOM = 4,
|
||||||
|
AGI_VM_HALT_NO_LOGIC = 5,
|
||||||
|
AGI_VM_HALT_STACK_OVER = 6,
|
||||||
|
AGI_VM_HALT_PRINT_PENDING = 7, // print() popup waiting for ack
|
||||||
|
AGI_VM_HALT_QUIT = 8 // game logic asked to exit
|
||||||
|
} AgiVmHaltE;
|
||||||
|
|
||||||
|
#define AGI_VM_CALL_STACK_MAX 8u
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const uint8_t *code;
|
||||||
|
uint16_t codeLength;
|
||||||
|
uint16_t pc;
|
||||||
|
uint8_t logicId;
|
||||||
|
} AgiVmFrameT;
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Animated objects -----
|
||||||
|
//
|
||||||
|
// AGI v2 supports up to 16 simultaneous animated objects ("screen
|
||||||
|
// objects"). Object 0 is the player/ego; the rest are NPCs, props,
|
||||||
|
// and incidental animations driven by the game logic. The state
|
||||||
|
// here mirrors the per-object fields the game scripts manipulate
|
||||||
|
// via animate.obj / set.view / set.cel / position / set.dir / etc.
|
||||||
|
#define AGI_MAX_OBJECTS 16u
|
||||||
|
|
||||||
|
#define AGI_OBJ_FLAG_ANIMATED 0x01u // animate.obj called
|
||||||
|
#define AGI_OBJ_FLAG_DRAWN 0x02u // draw() called, sprite is on-screen
|
||||||
|
#define AGI_OBJ_FLAG_CYCLING 0x04u // start.cycling, advances cel
|
||||||
|
#define AGI_OBJ_FLAG_UPDATING 0x08u // start.update, render allowed
|
||||||
|
#define AGI_OBJ_FLAG_LOOP_FIXED 0x10u // fix.loop disables auto-loop choice
|
||||||
|
#define AGI_OBJ_FLAG_PRI_FIXED 0x20u // set.priority overrides Y-band priority
|
||||||
|
#define AGI_OBJ_FLAG_HORIZON_IGN 0x40u // ignore.horizon
|
||||||
|
#define AGI_OBJ_FLAG_OBJS_IGN 0x80u // ignore.objs (collision)
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
AGI_CYCLE_NORMAL = 0, // 0->1->2..->last->0->...
|
||||||
|
AGI_CYCLE_END_LOOP = 1, // play once forward, stop, set flag
|
||||||
|
AGI_CYCLE_REVERSE = 2, // last->..->1->0->last->...
|
||||||
|
AGI_CYCLE_REV_LOOP = 3 // play once backward, stop, set flag
|
||||||
|
} AgiCycleE;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
AGI_MOTION_NORMAL = 0, // moves on direction (vN+6 for ego)
|
||||||
|
AGI_MOTION_WANDER = 1, // random direction every few cycles
|
||||||
|
AGI_MOTION_FOLLOW_EGO = 2, // chases obj 0
|
||||||
|
AGI_MOTION_MOVE_OBJ = 3 // moving toward fixed (x,y)
|
||||||
|
} AgiMotionE;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t viewId;
|
||||||
|
uint8_t loop;
|
||||||
|
uint8_t cel;
|
||||||
|
uint8_t priority; // 4..15; high nibble masking
|
||||||
|
int16_t x; // baseline (bottom-left) AGI x
|
||||||
|
int16_t y; // baseline (bottom) AGI y
|
||||||
|
int16_t prevX;
|
||||||
|
int16_t prevY;
|
||||||
|
uint8_t prevLoop;
|
||||||
|
uint8_t prevCel;
|
||||||
|
uint8_t prevView;
|
||||||
|
uint8_t direction; // 0=stop, 1..8 (up=1, clockwise)
|
||||||
|
uint8_t stepSize; // pixels per step
|
||||||
|
uint8_t stepTime; // VM cycles between steps
|
||||||
|
uint8_t stepTick; // counter toward stepTime
|
||||||
|
uint8_t cycleTime; // VM cycles between cel advances
|
||||||
|
uint8_t cycleTick; // counter toward cycleTime
|
||||||
|
AgiCycleE cycleMode;
|
||||||
|
uint8_t endOfLoopFlag; // flag id to set when end.of.loop fires
|
||||||
|
AgiMotionE motionType;
|
||||||
|
int16_t targetX; // for move.obj
|
||||||
|
int16_t targetY;
|
||||||
|
uint8_t moveStepBackup; // step size before move.obj started
|
||||||
|
uint8_t moveDoneFlag; // flag id set on move.obj completion
|
||||||
|
uint8_t wanderTick; // wander direction-change countdown
|
||||||
|
uint8_t followStep; // step.size override for follow.ego
|
||||||
|
uint8_t followDoneFlag;
|
||||||
|
uint8_t flags; // AGI_OBJ_FLAG_*
|
||||||
|
} AgiObjectT;
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Text plane / status line -----
|
||||||
|
//
|
||||||
|
// AGI's text plane is 40 columns x 25 rows of 8x8 character cells.
|
||||||
|
// Status line (row 0) is reserved for game-managed status text;
|
||||||
|
// display() writes anywhere by row/col; print() pops a modal window.
|
||||||
|
#define AGI_TEXT_COLS 40u
|
||||||
|
#define AGI_TEXT_ROWS 25u
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char text[AGI_TEXT_COLS + 1u]; // NUL-terminated
|
||||||
|
uint8_t fg;
|
||||||
|
uint8_t bg;
|
||||||
|
} AgiTextLineT;
|
||||||
|
|
||||||
|
#define AGI_PRINT_MAX_LEN 240u
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
bool active; // print modal on screen
|
||||||
|
uint16_t length;
|
||||||
|
char message[AGI_PRINT_MAX_LEN + 1u];
|
||||||
|
} AgiPrintModalT;
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Controllers -----
|
||||||
|
//
|
||||||
|
// set.key binds a (key1, key2) pair to a controller id 0..49. When
|
||||||
|
// the host dispatches a key matching a binding, the controller's
|
||||||
|
// "fired this cycle" bit is set. The IF test controller(N) consumes
|
||||||
|
// the bit so the binding only fires once per press.
|
||||||
|
#define AGI_MAX_CONTROLLERS 50u
|
||||||
|
#define AGI_MAX_KEY_BINDINGS 64u
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t key1; // ASCII or extended scancode (low byte)
|
||||||
|
uint8_t key2; // extended high byte (0 for plain ASCII)
|
||||||
|
uint8_t controller;
|
||||||
|
uint8_t active;
|
||||||
|
} AgiKeyBindingT;
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Menus -----
|
||||||
|
//
|
||||||
|
// set.menu / set.menu.item populate a 2D menu from message ids; the
|
||||||
|
// game can later disable individual items. menu.input pops it up;
|
||||||
|
// the user's choice fires the corresponding controller. Stored as
|
||||||
|
// state so menu.input can render and return a controller id.
|
||||||
|
#define AGI_MAX_MENUS 16u
|
||||||
|
#define AGI_MAX_MENU_ITEMS 16u
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t messageId; // logic 0 message id
|
||||||
|
uint8_t controller; // fired when item selected
|
||||||
|
uint8_t enabled;
|
||||||
|
} AgiMenuItemT;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t nameMsgId;
|
||||||
|
uint8_t itemCount;
|
||||||
|
AgiMenuItemT items[AGI_MAX_MENU_ITEMS];
|
||||||
|
} AgiMenuT;
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Strings -----
|
||||||
|
//
|
||||||
|
// set.string / get.string / word.to.string maintain a small pool of
|
||||||
|
// up to 24 game-defined strings. Each is 40 chars max (one screen row).
|
||||||
|
#define AGI_MAX_STRINGS 24u
|
||||||
|
#define AGI_STRING_LEN 40u
|
||||||
|
|
||||||
|
|
||||||
|
// Host callbacks invoked from the VM for side-effecting opcodes the
|
||||||
|
// host owns. The host owns the logic-resource cache and screen
|
||||||
|
// surfaces; the VM never touches either directly. fetchLogic returns
|
||||||
|
// the post-header bytecode pointer and its length (the 2-byte LE
|
||||||
|
// logic header has already been consumed by the host). NULL on
|
||||||
|
// missing logic causes the VM to halt with AGI_VM_HALT_NO_LOGIC.
|
||||||
|
//
|
||||||
|
// haveKey returns true if a keystroke is buffered, consuming it so
|
||||||
|
// the next call returns false until another key arrives. Used by
|
||||||
|
// the AGI test command have.key.
|
||||||
|
//
|
||||||
|
// fetchMessage returns the *decrypted* bytes of message `msgId`
|
||||||
|
// from logic `logicId`, or NULL if the message is missing. The
|
||||||
|
// returned pointer is valid until the next fetchMessage call (host
|
||||||
|
// may use a single shared scratch buffer). Used by display, print,
|
||||||
|
// print.at and friends.
|
||||||
|
typedef struct {
|
||||||
|
const uint8_t *(*fetchLogic)(void *ctx, uint8_t logicId, uint16_t *outBytecodeLength);
|
||||||
|
void (*loadPic)(void *ctx, uint8_t picId);
|
||||||
|
void (*drawPic)(void *ctx, uint8_t picId);
|
||||||
|
void (*showPic)(void *ctx);
|
||||||
|
void (*discardPic)(void *ctx, uint8_t picId);
|
||||||
|
void (*overlayPic)(void *ctx, uint8_t picId);
|
||||||
|
bool (*haveKey)(void *ctx);
|
||||||
|
const char *(*fetchMessage)(void *ctx, uint8_t logicId, uint8_t msgId);
|
||||||
|
void (*addToPic)(void *ctx, uint8_t viewId, uint8_t loop, uint8_t cel,
|
||||||
|
uint8_t x, uint8_t y, uint8_t pri, uint8_t margin);
|
||||||
|
// Return the playback duration of sound `soundId` in milliseconds,
|
||||||
|
// or 0 if the sound is missing/empty. Used only as a fallback
|
||||||
|
// duration cap when the host can't reliably report playback end;
|
||||||
|
// when isPlayingSound is wired, the VM ignores this and trusts
|
||||||
|
// the host's live state instead.
|
||||||
|
uint32_t (*soundDuration)(void *ctx, uint8_t soundId);
|
||||||
|
// Start playback of sound `soundId`. Any currently-playing AGI
|
||||||
|
// sound is replaced. The VM polls isPlayingSound each tick and
|
||||||
|
// sets the sound-done flag on the true->false transition.
|
||||||
|
void (*playSound)(void *ctx, uint8_t soundId);
|
||||||
|
// Stop playback immediately. Called by OP_STOP_SOUND (and any
|
||||||
|
// path that preempts the current sound).
|
||||||
|
void (*stopSound)(void *ctx);
|
||||||
|
// Live host-side playback state. Returns true while audio output
|
||||||
|
// is active for the currently-armed sound, false once the host
|
||||||
|
// has run all SND events to completion (or stopSound was called).
|
||||||
|
// Source of truth for sound completion: matches what the user
|
||||||
|
// actually hears, so the title sequence advances exactly when
|
||||||
|
// the music ends.
|
||||||
|
bool (*isPlayingSound)(void *ctx);
|
||||||
|
// View metadata for accurate cycling. AGI's cycle/loop opcodes
|
||||||
|
// (cycle.normal, end.of.loop, last.cel, number.of.loops...) need
|
||||||
|
// to know the cel count of the current view+loop and the loop
|
||||||
|
// count of the current view. Without these, end.of.loop fires
|
||||||
|
// its flag only at the AGI_MAX_CELS_PER_LOOP boundary -- and a
|
||||||
|
// sparkle that visually has 8 cels but is treated as 32 won't
|
||||||
|
// signal its end until 4x the intended time, breaking title
|
||||||
|
// sequences that arm end.of.loop expecting a tight cycle.
|
||||||
|
//
|
||||||
|
// Returns 0 if the view/loop is unloaded; callers treat 0 as
|
||||||
|
// "still running" so a not-yet-loaded view doesn't terminate
|
||||||
|
// early. Returns the loop count (>=1) or cel count (>=1) when
|
||||||
|
// the view is available.
|
||||||
|
uint8_t (*viewLoopCount)(void *ctx, uint8_t viewId);
|
||||||
|
uint8_t (*viewCelCount)(void *ctx, uint8_t viewId, uint8_t loopId);
|
||||||
|
void *ctx;
|
||||||
|
} AgiVmCallbacksT;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
// ----- core state -----
|
||||||
|
uint8_t vars[AGI_VM_NUM_VARS];
|
||||||
|
uint8_t flags[AGI_VM_NUM_FLAGS];
|
||||||
|
const uint8_t *code;
|
||||||
|
uint16_t codeLength;
|
||||||
|
uint16_t pc;
|
||||||
|
AgiVmHaltE haltReason;
|
||||||
|
uint8_t lastUnknownOp;
|
||||||
|
uint8_t newRoomId;
|
||||||
|
uint8_t currentLogicId;
|
||||||
|
uint8_t callDepth;
|
||||||
|
AgiVmFrameT callStack[AGI_VM_CALL_STACK_MAX];
|
||||||
|
AgiVmCallbacksT callbacks;
|
||||||
|
|
||||||
|
// ----- objects -----
|
||||||
|
AgiObjectT objects[AGI_MAX_OBJECTS];
|
||||||
|
|
||||||
|
// ----- text plane -----
|
||||||
|
AgiTextLineT textRows[AGI_TEXT_ROWS];
|
||||||
|
uint8_t textFg;
|
||||||
|
uint8_t textBg;
|
||||||
|
bool statusLineOn;
|
||||||
|
uint8_t statusLineRow; // usually 0
|
||||||
|
uint8_t horizon; // any obj y < horizon stops
|
||||||
|
bool programControl; // true => program controls ego (false = player)
|
||||||
|
bool acceptInput; // accept.input; toggled by accept/prevent
|
||||||
|
AgiPrintModalT printModal;
|
||||||
|
|
||||||
|
// ----- controllers + key bindings -----
|
||||||
|
AgiKeyBindingT keyBindings[AGI_MAX_KEY_BINDINGS];
|
||||||
|
uint8_t controllerFired[AGI_MAX_CONTROLLERS];
|
||||||
|
|
||||||
|
// ----- menus -----
|
||||||
|
AgiMenuT menus[AGI_MAX_MENUS];
|
||||||
|
uint8_t menuCount;
|
||||||
|
uint8_t menuOpen; // 0xFF = not open
|
||||||
|
|
||||||
|
// ----- strings -----
|
||||||
|
char strings[AGI_MAX_STRINGS][AGI_STRING_LEN + 1u];
|
||||||
|
|
||||||
|
// ----- sound playback state -----
|
||||||
|
// The VM polls callbacks.isPlayingSound() each tick; when it
|
||||||
|
// transitions from true to false, the sound-done flags fire.
|
||||||
|
// soundPlaying tracks whether we are currently waiting on an
|
||||||
|
// armed sound; soundDoneFlag is the per-sound user flag from
|
||||||
|
// OP_SOUND's second arg (0 = no user flag, just engine flag 9).
|
||||||
|
bool soundPlaying;
|
||||||
|
uint8_t soundDoneFlag;
|
||||||
|
} AgiVmT;
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Resource loader (agiRes.c) -----
|
||||||
|
|
||||||
|
// Close all volume files; safe to call after a failed open.
|
||||||
|
void agiResClose(AgiGameT *game);
|
||||||
|
|
||||||
|
// Allocate and return a resource's raw bytes. Caller frees with
|
||||||
|
// free(). Returns NULL on any failure (slot empty, signature bad,
|
||||||
|
// I/O error, out of memory). On success *outLength receives the
|
||||||
|
// resource payload length in bytes.
|
||||||
|
uint8_t *agiResLoad(const AgiGameT *game, AgiResTypeE type, uint16_t index, uint16_t *outLength);
|
||||||
|
|
||||||
|
// Open an AGI v2 game directory. gameDir is a relative or absolute
|
||||||
|
// path to a directory containing LOGDIR/PICDIR/VIEWDIR/SNDDIR plus
|
||||||
|
// VOL.0..VOL.N. Returns true if every directory file and at least
|
||||||
|
// one VOL.* file opened. Partial-success on missing optional
|
||||||
|
// volumes; agiResLoad reports the missing-volume case per resource.
|
||||||
|
bool agiResOpen(AgiGameT *game, const char *gameDir);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Picture decoder (agiPic.c) -----
|
||||||
|
|
||||||
|
// Allocate the two 160x168 working buffers. Caller must agiPicFree
|
||||||
|
// before exit. Returns false if either malloc fails.
|
||||||
|
bool agiPicAlloc(AgiPicT *pic);
|
||||||
|
|
||||||
|
// Pixel-doubled blit of the visual plane onto the stage starting at
|
||||||
|
// (0, destY). 160 source columns become 320 stage columns; the rows
|
||||||
|
// copy 1:1 so the output occupies (0, destY) - (319, destY+167).
|
||||||
|
void agiPicBlit(const AgiPicT *pic, jlSurfaceT *stage, int16_t destY);
|
||||||
|
|
||||||
|
// Reset both planes to their AGI default backgrounds (15 / 4).
|
||||||
|
void agiPicClear(AgiPicT *pic);
|
||||||
|
|
||||||
|
// Decode an AGI v2 PIC resource into the working buffers. The buffers
|
||||||
|
// must already be cleared (agiPicClear) and allocated. Returns false
|
||||||
|
// if the byte stream is malformed (truncated argument, unknown
|
||||||
|
// opcode); on failure the buffers are left in a partial state.
|
||||||
|
bool agiPicDecode(AgiPicT *pic, const uint8_t *data, uint16_t length);
|
||||||
|
|
||||||
|
void agiPicFree(AgiPicT *pic);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- View decoder (agiView.c) -----
|
||||||
|
|
||||||
|
// Parse an AGI VIEW resource. Takes ownership of `rawBytes` -- they
|
||||||
|
// must remain valid until agiViewFree (the view's cel pointers index
|
||||||
|
// into them). Returns false on malformed data; on failure the buffer
|
||||||
|
// is freed and `view` is left zeroed.
|
||||||
|
bool agiViewParse(AgiViewT *view, uint8_t *rawBytes, uint16_t length);
|
||||||
|
|
||||||
|
void agiViewFree(AgiViewT *view);
|
||||||
|
|
||||||
|
// Draw cel (loopIdx, celIdx) of `view` at AGI coordinate (x, y),
|
||||||
|
// where (x, y) is the bottom-left corner of the cel in 160x168 AGI
|
||||||
|
// space. Pixels are masked per-pixel by the priority plane: actor
|
||||||
|
// pixels are skipped where the picture priority exceeds actorPri.
|
||||||
|
// The blit is pixel-doubled horizontally onto the stage at
|
||||||
|
// (0, destY) -- pass the same destY used by agiPicBlit.
|
||||||
|
void agiViewDraw(const AgiViewT *view, uint8_t loopIdx, uint8_t celIdx,
|
||||||
|
int16_t x, int16_t y, uint8_t actorPri,
|
||||||
|
const uint8_t *picPriority,
|
||||||
|
jlSurfaceT *stage, int16_t destY);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- LOGIC VM (agiVm.c) -----
|
||||||
|
|
||||||
|
// Reset every variable and flag to 0 and clear the halt state.
|
||||||
|
void agiVmInit(AgiVmT *vm);
|
||||||
|
|
||||||
|
// Point the VM at a logic resource's bytecode segment. AGI v2 logic
|
||||||
|
// resources start with a 2-byte little-endian length for the bytecode
|
||||||
|
// section followed by the bytecode itself (messages live past it).
|
||||||
|
// Returns false if the buffer is too short to contain a header.
|
||||||
|
bool agiVmLoadLogic(AgiVmT *vm, const uint8_t *resourceBytes, uint16_t resourceLength);
|
||||||
|
|
||||||
|
// Reset the VM's program counter to 0 against an already-loaded
|
||||||
|
// bytecode pointer. Used by the host after handling a halt (room
|
||||||
|
// transition, frame end) to re-run a logic from the top without
|
||||||
|
// re-parsing its resource header.
|
||||||
|
void agiVmResetToLogic(AgiVmT *vm, const uint8_t *bytecode, uint16_t bytecodeLength, uint8_t logicId);
|
||||||
|
|
||||||
|
// Run until the VM hits any halt condition (return, unknown opcode,
|
||||||
|
// truncated stream, or pending room transition). Returns the halt
|
||||||
|
// reason; details live in vm->lastUnknownOp / vm->newRoomId / vm->pc.
|
||||||
|
AgiVmHaltE agiVmRun(AgiVmT *vm);
|
||||||
|
|
||||||
|
// Install host callbacks. May be called before or after agiVmInit;
|
||||||
|
// pass NULL for any callback the host doesn't implement (the VM will
|
||||||
|
// treat that opcode as a no-op stub).
|
||||||
|
void agiVmSetCallbacks(AgiVmT *vm, const AgiVmCallbacksT *callbacks);
|
||||||
|
|
||||||
|
// Advance the AGI engine clock by `seconds` wall-clock seconds. Bumps
|
||||||
|
// v11 (seconds 0..59), v12 (minutes 0..59) and v13 (hours 0..23) with
|
||||||
|
// proper roll-over. Game logic uses these to detect per-second ticks
|
||||||
|
// (KQ3's logic.0 fires a per-second update branch when v11 changes,
|
||||||
|
// which sets flag 45 -- the gate the title countdown is waiting on).
|
||||||
|
// Safe to call with seconds=0 (no-op) and with any size up to UINT8_MAX.
|
||||||
|
void agiVmTickSeconds(AgiVmT *vm, uint8_t seconds);
|
||||||
|
|
||||||
|
// Per-VM-cycle ticker. Called by the host once per game-loop cycle
|
||||||
|
// (~6 Hz, the AGI interpreter cadence). Advances every animated
|
||||||
|
// object's cel cycling and motion. Honors per-object stop.cycling /
|
||||||
|
// stop.motion / stop.update flags. Sets end.of.loop / move.obj.done
|
||||||
|
// flags as those events fire.
|
||||||
|
void agiVmTickAnimation(AgiVmT *vm);
|
||||||
|
|
||||||
|
// Notify the VM that the host received key `keyCode` (low 7 bits =
|
||||||
|
// ASCII; high byte = extended scancode for arrow keys / Fn). Walks
|
||||||
|
// the key-binding table; if `keyCode` matches a set.key binding,
|
||||||
|
// the corresponding controller fires. Also dispatches to any open
|
||||||
|
// menu navigation. Called by the host once per pressed key.
|
||||||
|
void agiVmDispatchKey(AgiVmT *vm, uint8_t key1, uint8_t key2);
|
||||||
|
|
||||||
|
// Acknowledge an open print() modal so the VM can resume. Called by
|
||||||
|
// the host when the user presses ENTER / SPACE / etc. while
|
||||||
|
// vm->printModal.active is true. Clears the modal and the
|
||||||
|
// AGI_VM_HALT_PRINT_PENDING halt reason.
|
||||||
|
void agiVmAckPrint(AgiVmT *vm);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Object helpers (agiObj.c) -----
|
||||||
|
|
||||||
|
// Reset every object slot to the default empty state. Called by
|
||||||
|
// agiVmInit and again on every NEW_ROOM transition (matches AGI's
|
||||||
|
// canonical room-change reset).
|
||||||
|
void agiObjResetAll(AgiVmT *vm);
|
||||||
|
|
||||||
|
// Single-object reset (animate.obj reuses this).
|
||||||
|
void agiObjReset(AgiObjectT *obj);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Text helpers (agiText.c) -----
|
||||||
|
|
||||||
|
// Fetch the host font surface (built lazily on first call). Returned
|
||||||
|
// surface contains 96 ASCII glyphs (codes 32..127) laid out 16 wide
|
||||||
|
// by 6 tall. Caller must NOT free.
|
||||||
|
const jlSurfaceT *agiTextFontSurface(void);
|
||||||
|
|
||||||
|
// asciiMap suitable for jlDrawText against the font surface above.
|
||||||
|
const uint16_t *agiTextAsciiMap(void);
|
||||||
|
|
||||||
|
// Render the VM's text plane (status line + display rows + open
|
||||||
|
// print modal) onto `stage`. Called by host after the picture +
|
||||||
|
// objects are drawn.
|
||||||
|
void agiTextRender(const AgiVmT *vm, jlSurfaceT *stage);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- PIC compositing (agiPic.c addendum) -----
|
||||||
|
|
||||||
|
// Bake a VIEW cel into the PIC visual + priority planes (the
|
||||||
|
// implementation of add.to.pic). priColor 4..15 sets the priority
|
||||||
|
// pixels; priColor == 0 means "use the priority of the actor's Y
|
||||||
|
// band" (AGI default). margin 0..3 reserves a control-line band at
|
||||||
|
// the cel's bottom so add.to.pic art can suggest a walkable edge;
|
||||||
|
// margin 4 means "no margin".
|
||||||
|
void agiPicAddView(AgiPicT *pic, const AgiViewT *view,
|
||||||
|
uint8_t loop, uint8_t cel,
|
||||||
|
int16_t x, int16_t y,
|
||||||
|
uint8_t priColor, uint8_t margin);
|
||||||
|
|
||||||
|
#endif
|
||||||
368
examples/agi/agiObj.c
Normal file
368
examples/agi/agiObj.c
Normal file
|
|
@ -0,0 +1,368 @@
|
||||||
|
// AGI v2 animated-object table: state lifecycle + per-cycle ticks.
|
||||||
|
//
|
||||||
|
// The VM stores an AgiObjectT[16] inside AgiVmT; opcode handlers in
|
||||||
|
// agiVm.c mutate fields directly (set.view / position / step.size /
|
||||||
|
// etc.) and the host renders from the table each frame. This file
|
||||||
|
// owns the housekeeping the dispatcher would otherwise duplicate
|
||||||
|
// across many opcodes:
|
||||||
|
//
|
||||||
|
// * reset on construction and on NEW_ROOM
|
||||||
|
// * per-cycle cel cycling honoring start/stop.cycling, end.of.loop
|
||||||
|
// * per-cycle motion: normal / wander / follow.ego / move.obj
|
||||||
|
//
|
||||||
|
// Every algorithm here is a re-implementation against the published
|
||||||
|
// AGI v2 actor-cycle / actor-motion spec; no third-party source is
|
||||||
|
// referenced.
|
||||||
|
|
||||||
|
#include "agi.h"
|
||||||
|
|
||||||
|
#include "joey/core.h"
|
||||||
|
#include "joey/debug.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||||
|
segment "AGIOBJ";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
// AGI direction encoding. 0 means "no motion this cycle"; 1..8
|
||||||
|
// circle clockwise starting at north.
|
||||||
|
// 1=N 2=NE 3=E 4=SE
|
||||||
|
// 5=S 6=SW 7=W 8=NW
|
||||||
|
#define AGI_DIR_NONE 0u
|
||||||
|
#define AGI_DIR_N 1u
|
||||||
|
#define AGI_DIR_NE 2u
|
||||||
|
#define AGI_DIR_E 3u
|
||||||
|
#define AGI_DIR_SE 4u
|
||||||
|
#define AGI_DIR_S 5u
|
||||||
|
#define AGI_DIR_SW 6u
|
||||||
|
#define AGI_DIR_W 7u
|
||||||
|
#define AGI_DIR_NW 8u
|
||||||
|
#define AGI_DIR_COUNT 9u
|
||||||
|
|
||||||
|
#define WANDER_TICKS_MIN 4u
|
||||||
|
#define WANDER_TICKS_MAX 24u
|
||||||
|
#define MOVE_REACH_PX 2
|
||||||
|
|
||||||
|
|
||||||
|
// dx, dy per direction (subscript 0 unused; AGI directions are 1..8).
|
||||||
|
static const int8_t kDirDx[AGI_DIR_COUNT] = { 0, 0, +1, +1, +1, 0, -1, -1, -1 };
|
||||||
|
static const int8_t kDirDy[AGI_DIR_COUNT] = { 0, -1, -1, 0, +1, +1, +1, 0, -1 };
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Prototypes -----
|
||||||
|
|
||||||
|
static void advanceCycle(AgiVmT *vm, uint8_t objId);
|
||||||
|
static void advanceMotion(AgiVmT *vm, uint8_t objId);
|
||||||
|
static uint8_t celCount(const AgiVmT *vm, const AgiObjectT *obj);
|
||||||
|
static uint8_t directionToward(int16_t fromX, int16_t fromY, int16_t toX, int16_t toY);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Internal helpers (alphabetical) -----
|
||||||
|
|
||||||
|
static void advanceCycle(AgiVmT *vm, uint8_t objId) {
|
||||||
|
AgiObjectT *obj;
|
||||||
|
uint8_t cels;
|
||||||
|
uint8_t next;
|
||||||
|
|
||||||
|
obj = &vm->objects[objId];
|
||||||
|
if ((obj->flags & AGI_OBJ_FLAG_ANIMATED) == 0u) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((obj->flags & AGI_OBJ_FLAG_CYCLING) == 0u) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Erased objects don't cycle. KQ3 LOGIC.45 sparkle pattern depends
|
||||||
|
// on this: erase(1..4) at wizard-scene start leaves their CYCLING
|
||||||
|
// flag set (LOGIC.45 never explicitly stops it), but Sierra's
|
||||||
|
// interpreter freezes the cel until the next draw(). Without this
|
||||||
|
// gate, end.of.loop fires for invisible objs, sets flag 221..224,
|
||||||
|
// and the sparkle loop kicks back in on the wizard pic.
|
||||||
|
if ((obj->flags & AGI_OBJ_FLAG_DRAWN) == 0u) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// stop.update freezes the obj's displayed cel until start.update.
|
||||||
|
// Canonical AGI cycles cels internally regardless, but with no
|
||||||
|
// display refresh the visual stays put. Since our host renderer
|
||||||
|
// always paints from obj->cel, the easiest faithful approximation
|
||||||
|
// is to also stop advancing cel while !UPDATING -- otherwise
|
||||||
|
// poses cycle visibly (e.g. Manannan's body in room 46, which is
|
||||||
|
// stop.update'd at init and only released to start.update at a
|
||||||
|
// specific state, would otherwise wave its hand continuously).
|
||||||
|
if ((obj->flags & AGI_OBJ_FLAG_UPDATING) == 0u) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (obj->cycleTime == 0u) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
obj->cycleTick++;
|
||||||
|
if (obj->cycleTick < obj->cycleTime) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
obj->cycleTick = 0u;
|
||||||
|
|
||||||
|
cels = celCount(vm, obj);
|
||||||
|
if (cels <= 1u) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (obj->cycleMode) {
|
||||||
|
case AGI_CYCLE_NORMAL:
|
||||||
|
next = (uint8_t)((obj->cel + 1u) % cels);
|
||||||
|
obj->cel = next;
|
||||||
|
break;
|
||||||
|
case AGI_CYCLE_REVERSE:
|
||||||
|
obj->cel = (obj->cel == 0u) ? (uint8_t)(cels - 1u) : (uint8_t)(obj->cel - 1u);
|
||||||
|
break;
|
||||||
|
case AGI_CYCLE_END_LOOP:
|
||||||
|
if (obj->cel + 1u >= cels) {
|
||||||
|
// reached last frame, fire end-of-loop
|
||||||
|
obj->flags &= (uint8_t)~AGI_OBJ_FLAG_CYCLING;
|
||||||
|
if (obj->endOfLoopFlag != 0u) {
|
||||||
|
vm->flags[obj->endOfLoopFlag] = 1u;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
obj->cel++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case AGI_CYCLE_REV_LOOP:
|
||||||
|
if (obj->cel == 0u) {
|
||||||
|
obj->flags &= (uint8_t)~AGI_OBJ_FLAG_CYCLING;
|
||||||
|
if (obj->endOfLoopFlag != 0u) {
|
||||||
|
vm->flags[obj->endOfLoopFlag] = 1u;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
obj->cel--;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void advanceMotion(AgiVmT *vm, uint8_t objId) {
|
||||||
|
AgiObjectT *obj;
|
||||||
|
int16_t newX;
|
||||||
|
int16_t newY;
|
||||||
|
uint8_t dir;
|
||||||
|
int8_t dx;
|
||||||
|
int8_t dy;
|
||||||
|
|
||||||
|
obj = &vm->objects[objId];
|
||||||
|
if ((obj->flags & AGI_OBJ_FLAG_ANIMATED) == 0u) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((obj->flags & AGI_OBJ_FLAG_DRAWN) == 0u) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (obj->stepSize == 0u) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (obj->stepTime == 0u) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
obj->stepTick++;
|
||||||
|
if (obj->stepTick < obj->stepTime) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
obj->stepTick = 0u;
|
||||||
|
|
||||||
|
switch (obj->motionType) {
|
||||||
|
case AGI_MOTION_WANDER:
|
||||||
|
if (obj->wanderTick == 0u) {
|
||||||
|
obj->direction = (uint8_t)(jlRandomRange((uint16_t)AGI_DIR_COUNT));
|
||||||
|
obj->wanderTick = (uint8_t)(WANDER_TICKS_MIN + jlRandomRange((uint16_t)(WANDER_TICKS_MAX - WANDER_TICKS_MIN)));
|
||||||
|
} else {
|
||||||
|
obj->wanderTick--;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case AGI_MOTION_FOLLOW_EGO:
|
||||||
|
obj->direction = directionToward(obj->x, obj->y, vm->objects[0].x, vm->objects[0].y);
|
||||||
|
break;
|
||||||
|
case AGI_MOTION_MOVE_OBJ: {
|
||||||
|
int16_t reachDx;
|
||||||
|
int16_t reachDy;
|
||||||
|
|
||||||
|
reachDx = (int16_t)(obj->targetX - obj->x);
|
||||||
|
reachDy = (int16_t)(obj->targetY - obj->y);
|
||||||
|
if (reachDx < 0) {
|
||||||
|
reachDx = (int16_t)(-reachDx);
|
||||||
|
}
|
||||||
|
if (reachDy < 0) {
|
||||||
|
reachDy = (int16_t)(-reachDy);
|
||||||
|
}
|
||||||
|
if (reachDx <= MOVE_REACH_PX && reachDy <= MOVE_REACH_PX) {
|
||||||
|
obj->x = obj->targetX;
|
||||||
|
obj->y = obj->targetY;
|
||||||
|
obj->direction = AGI_DIR_NONE;
|
||||||
|
obj->motionType = AGI_MOTION_NORMAL;
|
||||||
|
obj->stepSize = obj->moveStepBackup;
|
||||||
|
if (obj->moveDoneFlag != 0u) {
|
||||||
|
vm->flags[obj->moveDoneFlag] = 1u;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
obj->direction = directionToward(obj->x, obj->y, obj->targetX, obj->targetY);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AGI_MOTION_NORMAL:
|
||||||
|
default:
|
||||||
|
// Direction is whatever the script (or var 6 for ego) set.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
dir = obj->direction;
|
||||||
|
if (dir == AGI_DIR_NONE || dir >= AGI_DIR_COUNT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dx = kDirDx[dir];
|
||||||
|
dy = kDirDy[dir];
|
||||||
|
newX = (int16_t)(obj->x + (int16_t)((int16_t)dx * (int16_t)obj->stepSize));
|
||||||
|
newY = (int16_t)(obj->y + (int16_t)((int16_t)dy * (int16_t)obj->stepSize));
|
||||||
|
|
||||||
|
// Clamp to AGI's canonical 160x168 picture window. The horizon
|
||||||
|
// applies only to objects observing it.
|
||||||
|
if (newX < 0) {
|
||||||
|
newX = 0;
|
||||||
|
}
|
||||||
|
if (newX > (int16_t)(AGI_PIC_WIDTH - 1)) {
|
||||||
|
newX = (int16_t)(AGI_PIC_WIDTH - 1);
|
||||||
|
}
|
||||||
|
if (newY > (int16_t)(AGI_PIC_HEIGHT - 1)) {
|
||||||
|
newY = (int16_t)(AGI_PIC_HEIGHT - 1);
|
||||||
|
}
|
||||||
|
if ((obj->flags & AGI_OBJ_FLAG_HORIZON_IGN) == 0u && newY < (int16_t)vm->horizon) {
|
||||||
|
newY = (int16_t)vm->horizon;
|
||||||
|
}
|
||||||
|
if (newY < 0) {
|
||||||
|
newY = 0;
|
||||||
|
}
|
||||||
|
obj->x = newX;
|
||||||
|
obj->y = newY;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Without a view-id-to-loop-cel-count map in the VM, fall back to an
|
||||||
|
// Asks the host for the actual cel count of obj's view+loop via
|
||||||
|
// the viewCelCount callback. Falls back to AGI_MAX_CELS_PER_LOOP
|
||||||
|
// only when no callback is wired (legacy host) -- but cycling
|
||||||
|
// is broken in that mode because end.of.loop won't fire at the
|
||||||
|
// correct cel. A 0 return from the callback means "view not
|
||||||
|
// loaded yet"; we report 0 and the caller's `cels <= 1u` guard
|
||||||
|
// pauses cycling until the load completes.
|
||||||
|
static uint8_t celCount(const AgiVmT *vm, const AgiObjectT *obj) {
|
||||||
|
if (vm->callbacks.viewCelCount != NULL) {
|
||||||
|
return vm->callbacks.viewCelCount(vm->callbacks.ctx, obj->viewId, obj->loop);
|
||||||
|
}
|
||||||
|
return AGI_MAX_CELS_PER_LOOP;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 8-direction snap by quadrant + axis-dominance.
|
||||||
|
static uint8_t directionToward(int16_t fromX, int16_t fromY, int16_t toX, int16_t toY) {
|
||||||
|
int16_t dx;
|
||||||
|
int16_t dy;
|
||||||
|
int16_t ax;
|
||||||
|
int16_t ay;
|
||||||
|
|
||||||
|
dx = (int16_t)(toX - fromX);
|
||||||
|
dy = (int16_t)(toY - fromY);
|
||||||
|
if (dx == 0 && dy == 0) {
|
||||||
|
return AGI_DIR_NONE;
|
||||||
|
}
|
||||||
|
ax = (dx < 0) ? (int16_t)(-dx) : dx;
|
||||||
|
ay = (dy < 0) ? (int16_t)(-dy) : dy;
|
||||||
|
if (ax > ay * 2) {
|
||||||
|
return (dx > 0) ? AGI_DIR_E : AGI_DIR_W;
|
||||||
|
}
|
||||||
|
if (ay > ax * 2) {
|
||||||
|
return (dy > 0) ? AGI_DIR_S : AGI_DIR_N;
|
||||||
|
}
|
||||||
|
if (dx > 0) {
|
||||||
|
return (dy > 0) ? AGI_DIR_SE : AGI_DIR_NE;
|
||||||
|
}
|
||||||
|
return (dy > 0) ? AGI_DIR_SW : AGI_DIR_NW;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Public API (alphabetical) -----
|
||||||
|
|
||||||
|
void agiObjReset(AgiObjectT *obj) {
|
||||||
|
memset(obj, 0, sizeof(*obj));
|
||||||
|
obj->stepSize = 1u;
|
||||||
|
obj->stepTime = 1u;
|
||||||
|
obj->cycleTime = 1u;
|
||||||
|
obj->priority = 0u; // 0 = "auto-Y-band" (canonical AGI)
|
||||||
|
obj->direction = AGI_DIR_NONE;
|
||||||
|
obj->cycleMode = AGI_CYCLE_NORMAL;
|
||||||
|
obj->motionType = AGI_MOTION_NORMAL;
|
||||||
|
obj->endOfLoopFlag = 0u;
|
||||||
|
obj->moveDoneFlag = 0u;
|
||||||
|
obj->followDoneFlag = 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void agiObjResetAll(AgiVmT *vm) {
|
||||||
|
uint8_t i;
|
||||||
|
|
||||||
|
for (i = 0u; i < (uint8_t)AGI_MAX_OBJECTS; i++) {
|
||||||
|
agiObjReset(&vm->objects[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Called by VM core after each VM cycle returns to the host (or by
|
||||||
|
// the host directly each game-loop cycle). Walks the object table
|
||||||
|
// and advances cycling + motion. Also polls the host for sound
|
||||||
|
// completion so the sound-done flag fires on the natural-end of
|
||||||
|
// playback rather than a pre-computed deadline.
|
||||||
|
void agiVmTickAnimation(AgiVmT *vm) {
|
||||||
|
uint8_t i;
|
||||||
|
|
||||||
|
// Sound poll: fire done-flags on the host's playback->silent
|
||||||
|
// edge. Runs every VM cycle so OP_IF wait loops see the flag
|
||||||
|
// within one cycle of actual audio end.
|
||||||
|
if (vm->soundPlaying && vm->callbacks.isPlayingSound != NULL) {
|
||||||
|
if (!vm->callbacks.isPlayingSound(vm->callbacks.ctx)) {
|
||||||
|
jlLogF("sound-done flag fires: flag=%u (v11=%u)",
|
||||||
|
(unsigned)vm->soundDoneFlag, (unsigned)vm->vars[11]);
|
||||||
|
jlLogFlush();
|
||||||
|
if (vm->soundDoneFlag != 0u) {
|
||||||
|
vm->flags[vm->soundDoneFlag] = 1u;
|
||||||
|
}
|
||||||
|
vm->flags[9] = 1u;
|
||||||
|
vm->soundPlaying = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0u; i < (uint8_t)AGI_MAX_OBJECTS; i++) {
|
||||||
|
AgiObjectT *obj;
|
||||||
|
|
||||||
|
obj = &vm->objects[i];
|
||||||
|
if ((obj->flags & AGI_OBJ_FLAG_ANIMATED) == 0u) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Snapshot current state so the host renderer can restore the
|
||||||
|
// last frame's pixels under each obj before re-drawing this
|
||||||
|
// frame. Done before the tick so prevX/prevY reflect what was
|
||||||
|
// actually painted last frame.
|
||||||
|
obj->prevX = obj->x;
|
||||||
|
obj->prevY = obj->y;
|
||||||
|
obj->prevLoop = obj->loop;
|
||||||
|
obj->prevCel = obj->cel;
|
||||||
|
obj->prevView = obj->viewId;
|
||||||
|
|
||||||
|
advanceCycle(vm, i);
|
||||||
|
advanceMotion(vm, i);
|
||||||
|
}
|
||||||
|
// Mirror ego state into AGI's well-known engine vars so game
|
||||||
|
// scripts that probe v6 (ego dir) / current.view / get.posn
|
||||||
|
// see the right values without a per-opcode getter call.
|
||||||
|
if ((vm->objects[0].flags & AGI_OBJ_FLAG_ANIMATED) != 0u) {
|
||||||
|
vm->vars[6] = vm->objects[0].direction;
|
||||||
|
}
|
||||||
|
}
|
||||||
662
examples/agi/agiPic.c
Normal file
662
examples/agi/agiPic.c
Normal file
|
|
@ -0,0 +1,662 @@
|
||||||
|
// AGI v2 PIC (picture / background) decoder.
|
||||||
|
//
|
||||||
|
// Paints to two private 160x168 chunky 8bpp buffers (visual + priority).
|
||||||
|
// The blit step doubles each AGI source column into two stage columns
|
||||||
|
// so the natural 160x168 internal coordinate system can be used by the
|
||||||
|
// drawing primitives, and the 320-wide stage receives the canonical
|
||||||
|
// AGI look.
|
||||||
|
//
|
||||||
|
// All algorithms here -- line interpolation, corner drawing, scanline
|
||||||
|
// flood fill, pen plotting, opcode dispatch -- are re-implementations
|
||||||
|
// written against the published AGI v2 picture-stream specification.
|
||||||
|
// No third-party source is referenced.
|
||||||
|
|
||||||
|
#include "agi.h"
|
||||||
|
|
||||||
|
#include "joey/draw.h"
|
||||||
|
#include "surfaceInternal.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
|
||||||
|
// PIC opcodes. Anything < 0xF0 is an argument byte; >= 0xF0 ends the
|
||||||
|
// current opcode's argument list.
|
||||||
|
#define OP_FIRST 0xF0u
|
||||||
|
#define OP_SET_PIC_COLOR 0xF0u
|
||||||
|
#define OP_DISABLE_PIC 0xF1u
|
||||||
|
#define OP_SET_PRI_COLOR 0xF2u
|
||||||
|
#define OP_DISABLE_PRI 0xF3u
|
||||||
|
#define OP_Y_CORNER 0xF4u
|
||||||
|
#define OP_X_CORNER 0xF5u
|
||||||
|
#define OP_ABS_LINE 0xF6u
|
||||||
|
#define OP_REL_LINE 0xF7u
|
||||||
|
#define OP_FILL 0xF8u
|
||||||
|
#define OP_SET_PEN 0xF9u
|
||||||
|
#define OP_PLOT_PEN 0xFAu
|
||||||
|
#define OP_END 0xFFu
|
||||||
|
|
||||||
|
// Pen control byte bits (set via OP_SET_PEN, consumed by OP_PLOT_PEN).
|
||||||
|
#define PEN_PATTERN_BIT 0x20u
|
||||||
|
|
||||||
|
// Scanline-fill seed stack. Each fill is reentrant-free, so a single
|
||||||
|
// static stack serves both the visual and priority passes. The bound
|
||||||
|
// is a safe over-estimate of typical PIC fill complexity; KQ3 PICs
|
||||||
|
// don't approach it.
|
||||||
|
#define FILL_STACK_MAX 512
|
||||||
|
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t visualEnabled;
|
||||||
|
uint8_t priorityEnabled;
|
||||||
|
uint8_t visualColor;
|
||||||
|
uint8_t priorityColor;
|
||||||
|
uint8_t penPattern;
|
||||||
|
} PicStateT;
|
||||||
|
|
||||||
|
|
||||||
|
// AGI 16-color CGA-derived palette. Entry 6's (R=A, G=5, B=0) keeps the
|
||||||
|
// classic CGA "brown fix" -- pure (R=A, G=A, B=0) would read as a vivid
|
||||||
|
// yellow-green on the host, which is wrong for AGI's intended look.
|
||||||
|
const uint16_t kAgiPalette[16] = {
|
||||||
|
0x000u, 0x00Au, 0x0A0u, 0x0AAu,
|
||||||
|
0xA00u, 0xA0Au, 0xA50u, 0xAAAu,
|
||||||
|
0x555u, 0x55Fu, 0x5F5u, 0x5FFu,
|
||||||
|
0xF55u, 0xF5Fu, 0xFF5u, 0xFFFu
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Prototypes -----
|
||||||
|
|
||||||
|
static void drawCorner(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos, bool yFirst);
|
||||||
|
static void drawLine(AgiPicT *pic, const PicStateT *state, int16_t x0, int16_t y0, int16_t x1, int16_t y1);
|
||||||
|
static void fillPlane(uint8_t *plane, int16_t x, int16_t y, uint8_t bgValue, uint8_t newValue);
|
||||||
|
static bool isOpcode(uint8_t byte);
|
||||||
|
static void opAbsLine(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos);
|
||||||
|
static void opFill(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos);
|
||||||
|
static void opPlotPen(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos);
|
||||||
|
static void opRelLine(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos);
|
||||||
|
static void plot(AgiPicT *pic, const PicStateT *state, int16_t x, int16_t y);
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||||
|
segment "AGIPIC";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// ----- Internal helpers (alphabetical) -----
|
||||||
|
|
||||||
|
static void drawCorner(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos, bool yFirst) {
|
||||||
|
uint16_t p;
|
||||||
|
int16_t curX;
|
||||||
|
int16_t curY;
|
||||||
|
bool readY;
|
||||||
|
|
||||||
|
p = *pos;
|
||||||
|
if (p + 1 >= length) {
|
||||||
|
*pos = length;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
curX = (int16_t)data[p++];
|
||||||
|
curY = (int16_t)data[p++];
|
||||||
|
plot(pic, state, curX, curY);
|
||||||
|
|
||||||
|
readY = yFirst;
|
||||||
|
while (p < length && !isOpcode(data[p])) {
|
||||||
|
if (readY) {
|
||||||
|
int16_t newY = (int16_t)data[p++];
|
||||||
|
drawLine(pic, state, curX, curY, curX, newY);
|
||||||
|
curY = newY;
|
||||||
|
} else {
|
||||||
|
int16_t newX = (int16_t)data[p++];
|
||||||
|
drawLine(pic, state, curX, curY, newX, curY);
|
||||||
|
curX = newX;
|
||||||
|
}
|
||||||
|
readY = !readY;
|
||||||
|
}
|
||||||
|
*pos = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Documented AGI line algorithm. Major axis steps in unit increments;
|
||||||
|
// the minor axis is linearly interpolated with round-half-away-from-
|
||||||
|
// zero. All math stays in 16-bit -- AGI coordinates are 8-bit so the
|
||||||
|
// worst product (dy * i = 167 * 167 = 27889) fits in int16, and the
|
||||||
|
// 65816 build avoids ORCA-C's long-arithmetic helpers entirely.
|
||||||
|
static void drawLine(AgiPicT *pic, const PicStateT *state, int16_t x0, int16_t y0, int16_t x1, int16_t y1) {
|
||||||
|
int16_t dx;
|
||||||
|
int16_t dy;
|
||||||
|
int16_t absDx;
|
||||||
|
int16_t absDy;
|
||||||
|
int16_t steps;
|
||||||
|
int16_t i;
|
||||||
|
int16_t half;
|
||||||
|
int16_t px;
|
||||||
|
int16_t py;
|
||||||
|
int16_t sx;
|
||||||
|
int16_t sy;
|
||||||
|
|
||||||
|
dx = (int16_t)(x1 - x0);
|
||||||
|
dy = (int16_t)(y1 - y0);
|
||||||
|
if (dx == 0 && dy == 0) {
|
||||||
|
plot(pic, state, x0, y0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
absDx = (int16_t)((dx >= 0) ? dx : -dx);
|
||||||
|
absDy = (int16_t)((dy >= 0) ? dy : -dy);
|
||||||
|
sx = (int16_t)((dx >= 0) ? 1 : -1);
|
||||||
|
sy = (int16_t)((dy >= 0) ? 1 : -1);
|
||||||
|
|
||||||
|
if (absDx >= absDy) {
|
||||||
|
steps = absDx;
|
||||||
|
half = (int16_t)(steps / 2);
|
||||||
|
for (i = 0; i <= steps; i++) {
|
||||||
|
px = (int16_t)(x0 + sx * i);
|
||||||
|
py = (int16_t)(y0 + (dy * i + sy * half) / steps);
|
||||||
|
plot(pic, state, px, py);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
steps = absDy;
|
||||||
|
half = (int16_t)(steps / 2);
|
||||||
|
for (i = 0; i <= steps; i++) {
|
||||||
|
py = (int16_t)(y0 + sy * i);
|
||||||
|
px = (int16_t)(x0 + (dx * i + sx * half) / steps);
|
||||||
|
plot(pic, state, px, py);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Scanline flood fill on one plane. Replaces every cell of value
|
||||||
|
// `bgValue` 4-connected to (x, y) with `newValue`. The seed stack is
|
||||||
|
// bounded; runs that don't fit are silently dropped, which mirrors
|
||||||
|
// the original interpreter's behavior under pathological PIC inputs.
|
||||||
|
static void fillPlane(uint8_t *plane, int16_t x, int16_t y, uint8_t bgValue, uint8_t newValue) {
|
||||||
|
static int16_t stackX[FILL_STACK_MAX];
|
||||||
|
static int16_t stackY[FILL_STACK_MAX];
|
||||||
|
uint16_t sp;
|
||||||
|
int16_t cx;
|
||||||
|
int16_t cy;
|
||||||
|
int16_t left;
|
||||||
|
int16_t right;
|
||||||
|
int16_t scanX;
|
||||||
|
bool aboveOpen;
|
||||||
|
bool belowOpen;
|
||||||
|
|
||||||
|
if (x < 0 || x >= AGI_PIC_WIDTH || y < 0 || y >= AGI_PIC_HEIGHT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (plane[y * AGI_PIC_WIDTH + x] != bgValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (bgValue == newValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sp = 0;
|
||||||
|
stackX[sp] = x;
|
||||||
|
stackY[sp] = y;
|
||||||
|
sp++;
|
||||||
|
|
||||||
|
while (sp > 0u) {
|
||||||
|
sp--;
|
||||||
|
cx = stackX[sp];
|
||||||
|
cy = stackY[sp];
|
||||||
|
|
||||||
|
if (plane[cy * AGI_PIC_WIDTH + cx] != bgValue) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand the current row to the bg run's full extent.
|
||||||
|
left = cx;
|
||||||
|
while (left > 0 && plane[cy * AGI_PIC_WIDTH + (left - 1)] == bgValue) {
|
||||||
|
left--;
|
||||||
|
}
|
||||||
|
right = cx;
|
||||||
|
while (right < (int16_t)(AGI_PIC_WIDTH - 1) && plane[cy * AGI_PIC_WIDTH + (right + 1)] == bgValue) {
|
||||||
|
right++;
|
||||||
|
}
|
||||||
|
for (scanX = left; scanX <= right; scanX++) {
|
||||||
|
plane[cy * AGI_PIC_WIDTH + scanX] = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push one seed per bg run on the row above.
|
||||||
|
if (cy > 0) {
|
||||||
|
aboveOpen = false;
|
||||||
|
for (scanX = left; scanX <= right; scanX++) {
|
||||||
|
if (plane[(cy - 1) * AGI_PIC_WIDTH + scanX] == bgValue) {
|
||||||
|
if (!aboveOpen) {
|
||||||
|
if (sp < FILL_STACK_MAX) {
|
||||||
|
stackX[sp] = scanX;
|
||||||
|
stackY[sp] = (int16_t)(cy - 1);
|
||||||
|
sp++;
|
||||||
|
}
|
||||||
|
aboveOpen = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
aboveOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// And one seed per bg run on the row below.
|
||||||
|
if (cy < (int16_t)(AGI_PIC_HEIGHT - 1)) {
|
||||||
|
belowOpen = false;
|
||||||
|
for (scanX = left; scanX <= right; scanX++) {
|
||||||
|
if (plane[(cy + 1) * AGI_PIC_WIDTH + scanX] == bgValue) {
|
||||||
|
if (!belowOpen) {
|
||||||
|
if (sp < FILL_STACK_MAX) {
|
||||||
|
stackX[sp] = scanX;
|
||||||
|
stackY[sp] = (int16_t)(cy + 1);
|
||||||
|
sp++;
|
||||||
|
}
|
||||||
|
belowOpen = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
belowOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool isOpcode(uint8_t byte) {
|
||||||
|
return byte >= OP_FIRST;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void opAbsLine(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos) {
|
||||||
|
uint16_t p;
|
||||||
|
int16_t prevX;
|
||||||
|
int16_t prevY;
|
||||||
|
int16_t curX;
|
||||||
|
int16_t curY;
|
||||||
|
bool have;
|
||||||
|
|
||||||
|
p = *pos;
|
||||||
|
have = false;
|
||||||
|
prevX = 0;
|
||||||
|
prevY = 0;
|
||||||
|
while (p + 1 < length && !isOpcode(data[p])) {
|
||||||
|
curX = (int16_t)data[p++];
|
||||||
|
curY = (int16_t)data[p++];
|
||||||
|
if (!have) {
|
||||||
|
plot(pic, state, curX, curY);
|
||||||
|
have = true;
|
||||||
|
} else {
|
||||||
|
drawLine(pic, state, prevX, prevY, curX, curY);
|
||||||
|
}
|
||||||
|
prevX = curX;
|
||||||
|
prevY = curY;
|
||||||
|
}
|
||||||
|
*pos = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void opFill(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos) {
|
||||||
|
uint16_t p;
|
||||||
|
int16_t x;
|
||||||
|
int16_t y;
|
||||||
|
|
||||||
|
p = *pos;
|
||||||
|
while (p + 1 < length && !isOpcode(data[p])) {
|
||||||
|
x = (int16_t)data[p++];
|
||||||
|
y = (int16_t)data[p++];
|
||||||
|
if (state->visualEnabled) {
|
||||||
|
fillPlane(pic->visual, x, y, AGI_VISUAL_BG, state->visualColor);
|
||||||
|
}
|
||||||
|
if (state->priorityEnabled) {
|
||||||
|
fillPlane(pic->priority, x, y, AGI_PRIORITY_BG, state->priorityColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*pos = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Plot-with-pen. With pen patterning off, each iteration consumes an
|
||||||
|
// X,Y pair and plots a single pixel. With patterning on, each
|
||||||
|
// iteration consumes a texture byte first (we discard it -- proper
|
||||||
|
// patterned brushes are deferred to a later pass). Size and shape
|
||||||
|
// are likewise stubbed to single-pixel; KQ3 PICs use pens sparingly
|
||||||
|
// and dropping the brush footprint just thins occasional grass / sky
|
||||||
|
// dithering.
|
||||||
|
static void opPlotPen(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos) {
|
||||||
|
uint16_t p;
|
||||||
|
int16_t x;
|
||||||
|
int16_t y;
|
||||||
|
|
||||||
|
p = *pos;
|
||||||
|
while (p < length && !isOpcode(data[p])) {
|
||||||
|
if (state->penPattern) {
|
||||||
|
// Skip the texture-selector byte.
|
||||||
|
p++;
|
||||||
|
if (p >= length || isOpcode(data[p])) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (p + 1 >= length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
x = (int16_t)data[p++];
|
||||||
|
y = (int16_t)data[p++];
|
||||||
|
plot(pic, state, x, y);
|
||||||
|
}
|
||||||
|
*pos = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void opRelLine(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos) {
|
||||||
|
uint16_t p;
|
||||||
|
int16_t curX;
|
||||||
|
int16_t curY;
|
||||||
|
int16_t newX;
|
||||||
|
int16_t newY;
|
||||||
|
uint8_t disp;
|
||||||
|
int16_t dx;
|
||||||
|
int16_t dy;
|
||||||
|
|
||||||
|
p = *pos;
|
||||||
|
if (p + 1 >= length) {
|
||||||
|
*pos = p;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
curX = (int16_t)data[p++];
|
||||||
|
curY = (int16_t)data[p++];
|
||||||
|
plot(pic, state, curX, curY);
|
||||||
|
|
||||||
|
while (p < length && !isOpcode(data[p])) {
|
||||||
|
disp = data[p++];
|
||||||
|
// High nibble = X disp (bit 7 sign, bits 4-6 magnitude 0..7).
|
||||||
|
// Low nibble = Y disp (bit 3 sign, bits 0-2 magnitude 0..7).
|
||||||
|
dx = (int16_t)((disp >> 4) & 0x07u);
|
||||||
|
if (disp & 0x80u) {
|
||||||
|
dx = (int16_t)-dx;
|
||||||
|
}
|
||||||
|
dy = (int16_t)(disp & 0x07u);
|
||||||
|
if (disp & 0x08u) {
|
||||||
|
dy = (int16_t)-dy;
|
||||||
|
}
|
||||||
|
newX = (int16_t)(curX + dx);
|
||||||
|
newY = (int16_t)(curY + dy);
|
||||||
|
drawLine(pic, state, curX, curY, newX, newY);
|
||||||
|
curX = newX;
|
||||||
|
curY = newY;
|
||||||
|
}
|
||||||
|
*pos = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void plot(AgiPicT *pic, const PicStateT *state, int16_t x, int16_t y) {
|
||||||
|
uint16_t idx;
|
||||||
|
|
||||||
|
if (x < 0 || x >= (int16_t)AGI_PIC_WIDTH) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (y < 0 || y >= (int16_t)AGI_PIC_HEIGHT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
idx = (uint16_t)(y * (int16_t)AGI_PIC_WIDTH + x);
|
||||||
|
if (state->visualEnabled) {
|
||||||
|
pic->visual[idx] = state->visualColor;
|
||||||
|
}
|
||||||
|
if (state->priorityEnabled) {
|
||||||
|
pic->priority[idx] = state->priorityColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Public API (alphabetical) -----
|
||||||
|
|
||||||
|
bool agiPicAlloc(AgiPicT *pic) {
|
||||||
|
pic->visual = (uint8_t *)malloc(AGI_PIC_PIXELS);
|
||||||
|
pic->priority = (uint8_t *)malloc(AGI_PIC_PIXELS);
|
||||||
|
if (pic->visual == NULL || pic->priority == NULL) {
|
||||||
|
agiPicFree(pic);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void agiPicBlit(const AgiPicT *pic, jlSurfaceT *stage, int16_t destY) {
|
||||||
|
int16_t sy;
|
||||||
|
int16_t sx;
|
||||||
|
int16_t dx;
|
||||||
|
uint8_t color;
|
||||||
|
|
||||||
|
// Fast path: chunky shadow exists -> write packed bytes direct,
|
||||||
|
// mark the whole region dirty once at the end. ~30x faster than
|
||||||
|
// calling jlDrawPixel 53,760 times (each does bounds + 1-pixel
|
||||||
|
// dirty mark). Required for state-2 / state-3 title transitions
|
||||||
|
// to render under a second on period-correct DOSBox CPU cycles.
|
||||||
|
if (stage != NULL && stage->pixels != NULL) {
|
||||||
|
const uint8_t *src = pic->visual;
|
||||||
|
uint8_t *dst;
|
||||||
|
int16_t rowOff;
|
||||||
|
|
||||||
|
for (sy = 0; sy < (int16_t)AGI_PIC_HEIGHT; sy++) {
|
||||||
|
rowOff = (int16_t)((int16_t)(destY + sy) * (int16_t)SURFACE_BYTES_PER_ROW);
|
||||||
|
dst = &stage->pixels[rowOff];
|
||||||
|
for (sx = 0; sx < (int16_t)AGI_PIC_WIDTH; sx++) {
|
||||||
|
color = (uint8_t)(src[sy * AGI_PIC_WIDTH + sx] & 0x0Fu);
|
||||||
|
// Pixel-doubled: each source pixel = 1 dst byte
|
||||||
|
// (both nibbles = same color).
|
||||||
|
dst[sx] = (uint8_t)((color << 4) | color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
surfaceMarkDirtyRect(stage, 0, destY,
|
||||||
|
(int16_t)SURFACE_WIDTH, (int16_t)AGI_PIC_HEIGHT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Planar fallback (s->pixels NULL: Phase-9 Amiga). Goes through
|
||||||
|
// jlDrawPixel so the port's c2p / plane-sync path stays correct.
|
||||||
|
for (sy = 0; sy < (int16_t)AGI_PIC_HEIGHT; sy++) {
|
||||||
|
for (sx = 0; sx < (int16_t)AGI_PIC_WIDTH; sx++) {
|
||||||
|
color = pic->visual[sy * AGI_PIC_WIDTH + sx];
|
||||||
|
dx = (int16_t)(sx << 1);
|
||||||
|
jlDrawPixel(stage, dx, (int16_t)(destY + sy), color);
|
||||||
|
jlDrawPixel(stage, (int16_t)(dx + 1), (int16_t)(destY + sy), color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void agiPicClear(AgiPicT *pic) {
|
||||||
|
if (pic->visual != NULL) {
|
||||||
|
memset(pic->visual, AGI_VISUAL_BG, AGI_PIC_PIXELS);
|
||||||
|
}
|
||||||
|
if (pic->priority != NULL) {
|
||||||
|
memset(pic->priority, AGI_PRIORITY_BG, AGI_PIC_PIXELS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool agiPicDecode(AgiPicT *pic, const uint8_t *data, uint16_t length) {
|
||||||
|
PicStateT state;
|
||||||
|
uint16_t pos;
|
||||||
|
uint8_t op;
|
||||||
|
|
||||||
|
state.visualEnabled = 0u;
|
||||||
|
state.priorityEnabled = 0u;
|
||||||
|
state.visualColor = 0u;
|
||||||
|
state.priorityColor = 0u;
|
||||||
|
state.penPattern = 0u;
|
||||||
|
|
||||||
|
pos = 0u;
|
||||||
|
while (pos < length) {
|
||||||
|
op = data[pos++];
|
||||||
|
switch (op) {
|
||||||
|
case OP_SET_PIC_COLOR:
|
||||||
|
if (pos >= length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
state.visualColor = data[pos++];
|
||||||
|
state.visualEnabled = 1u;
|
||||||
|
break;
|
||||||
|
case OP_DISABLE_PIC:
|
||||||
|
state.visualEnabled = 0u;
|
||||||
|
break;
|
||||||
|
case OP_SET_PRI_COLOR:
|
||||||
|
if (pos >= length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
state.priorityColor = data[pos++];
|
||||||
|
state.priorityEnabled = 1u;
|
||||||
|
break;
|
||||||
|
case OP_DISABLE_PRI:
|
||||||
|
state.priorityEnabled = 0u;
|
||||||
|
break;
|
||||||
|
case OP_Y_CORNER:
|
||||||
|
drawCorner(pic, &state, data, length, &pos, true);
|
||||||
|
break;
|
||||||
|
case OP_X_CORNER:
|
||||||
|
drawCorner(pic, &state, data, length, &pos, false);
|
||||||
|
break;
|
||||||
|
case OP_ABS_LINE:
|
||||||
|
opAbsLine(pic, &state, data, length, &pos);
|
||||||
|
break;
|
||||||
|
case OP_REL_LINE:
|
||||||
|
opRelLine(pic, &state, data, length, &pos);
|
||||||
|
break;
|
||||||
|
case OP_FILL:
|
||||||
|
opFill(pic, &state, data, length, &pos);
|
||||||
|
break;
|
||||||
|
case OP_SET_PEN:
|
||||||
|
if (pos >= length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
state.penPattern = (uint8_t)((data[pos++] & PEN_PATTERN_BIT) ? 1u : 0u);
|
||||||
|
break;
|
||||||
|
case OP_PLOT_PEN:
|
||||||
|
opPlotPen(pic, &state, data, length, &pos);
|
||||||
|
break;
|
||||||
|
case OP_END:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
// Unknown opcode -- stop cleanly rather than misparsing.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Bake a single VIEW cel into the visual + priority planes (the
|
||||||
|
// implementation of add.to.pic). Pixels outside the picture window
|
||||||
|
// or set to the cel's transparent color are skipped. priColor in
|
||||||
|
// 4..15 overwrites the priority pixels where the visual is opaque;
|
||||||
|
// 0 preserves the existing priority. RLE format matches agiViewDraw
|
||||||
|
// (per-row 0-byte EOR marker after a variable run).
|
||||||
|
void agiPicAddView(AgiPicT *pic, const AgiViewT *view,
|
||||||
|
uint8_t loop, uint8_t cel,
|
||||||
|
int16_t baseX, int16_t baseY,
|
||||||
|
uint8_t priColor, uint8_t margin) {
|
||||||
|
const AgiCelInfoT *info;
|
||||||
|
const AgiCelInfoT *resolved;
|
||||||
|
const uint8_t *rle;
|
||||||
|
uint8_t actualLoop;
|
||||||
|
int16_t width;
|
||||||
|
int16_t height;
|
||||||
|
uint8_t trans;
|
||||||
|
bool mirrored;
|
||||||
|
int16_t topY;
|
||||||
|
int16_t cy;
|
||||||
|
int16_t cx;
|
||||||
|
int16_t drawCx;
|
||||||
|
int16_t agiX;
|
||||||
|
int16_t agiY;
|
||||||
|
uint8_t rleByte;
|
||||||
|
uint8_t color;
|
||||||
|
uint8_t runLen;
|
||||||
|
uint8_t r;
|
||||||
|
|
||||||
|
(void)margin;
|
||||||
|
if (view == NULL || pic == NULL || pic->visual == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (loop >= view->loopCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cel >= view->loops[loop].celCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
info = &view->loops[loop].cels[cel];
|
||||||
|
actualLoop = loop;
|
||||||
|
mirrored = false;
|
||||||
|
if (info->mirrored != 0u) {
|
||||||
|
if (info->mirrorSourceLoop != loop && info->mirrorSourceLoop < view->loopCount
|
||||||
|
&& cel < view->loops[info->mirrorSourceLoop].celCount) {
|
||||||
|
actualLoop = info->mirrorSourceLoop;
|
||||||
|
mirrored = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolved = &view->loops[actualLoop].cels[cel];
|
||||||
|
|
||||||
|
width = (int16_t)resolved->width;
|
||||||
|
height = (int16_t)resolved->height;
|
||||||
|
trans = resolved->transparentColor;
|
||||||
|
rle = resolved->rleData;
|
||||||
|
topY = (int16_t)(baseY - height + 1);
|
||||||
|
|
||||||
|
for (cy = 0; cy < height; cy++) {
|
||||||
|
cx = 0;
|
||||||
|
for (;;) {
|
||||||
|
rleByte = *rle++;
|
||||||
|
if (rleByte == 0u) {
|
||||||
|
// End-of-row marker.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
color = (uint8_t)(rleByte >> 4);
|
||||||
|
runLen = (uint8_t)(rleByte & 0x0Fu);
|
||||||
|
for (r = 0u; r < runLen; r++) {
|
||||||
|
if (cx >= width) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (color != trans) {
|
||||||
|
drawCx = mirrored ? (int16_t)(width - 1 - cx) : cx;
|
||||||
|
agiX = (int16_t)(baseX + drawCx);
|
||||||
|
agiY = (int16_t)(topY + cy);
|
||||||
|
if (agiX >= 0 && agiX < (int16_t)AGI_PIC_WIDTH && agiY >= 0 && agiY < (int16_t)AGI_PIC_HEIGHT) {
|
||||||
|
int16_t idx = (int16_t)(agiY * (int16_t)AGI_PIC_WIDTH + agiX);
|
||||||
|
bool paint = true;
|
||||||
|
|
||||||
|
// AGI v2 priority masking: when priColor is a
|
||||||
|
// real priority value (>= AGI_PRIORITY_BG),
|
||||||
|
// skip the pixel if the existing priority is
|
||||||
|
// strictly higher -- the new art is behind
|
||||||
|
// existing higher-priority art. This is how
|
||||||
|
// KQ3's title "III" ends up behind the KQ
|
||||||
|
// logo (III priColor=4 = background, KQ logo
|
||||||
|
// in PIC 45 sits at a higher band).
|
||||||
|
if (priColor >= AGI_PRIORITY_BG && priColor < 16u && pic->priority != NULL) {
|
||||||
|
if (pic->priority[idx] > priColor) {
|
||||||
|
paint = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (paint) {
|
||||||
|
pic->visual[idx] = color;
|
||||||
|
if (priColor >= AGI_PRIORITY_BG && priColor < 16u && pic->priority != NULL) {
|
||||||
|
pic->priority[idx] = priColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void agiPicFree(AgiPicT *pic) {
|
||||||
|
if (pic->visual != NULL) {
|
||||||
|
free(pic->visual);
|
||||||
|
pic->visual = NULL;
|
||||||
|
}
|
||||||
|
if (pic->priority != NULL) {
|
||||||
|
free(pic->priority);
|
||||||
|
pic->priority = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
309
examples/agi/agiRes.c
Normal file
309
examples/agi/agiRes.c
Normal file
|
|
@ -0,0 +1,309 @@
|
||||||
|
// AGI v2 resource loader.
|
||||||
|
//
|
||||||
|
// Parses LOGDIR/PICDIR/VIEWDIR/SNDDIR into in-RAM index tables, keeps
|
||||||
|
// every VOL.x file open for the session, and exposes a single
|
||||||
|
// resource-by-id load primitive that hands back a freshly-allocated
|
||||||
|
// payload buffer.
|
||||||
|
//
|
||||||
|
// AGI v2 directory entry format (3 bytes, big-endian within the byte
|
||||||
|
// stream): byte0 high nibble is the volume number, the 20-bit value
|
||||||
|
// formed from (byte0 & 0x0F):byte1:byte2 is the file offset. All-FF
|
||||||
|
// means the slot is empty (no resource with that ID).
|
||||||
|
//
|
||||||
|
// VOL.x record header (5 bytes at the directory's offset): 0x12 0x34
|
||||||
|
// signature, then the volume number, then a little-endian 16-bit
|
||||||
|
// payload length. v2 payloads are stored raw; v3 sets bit 7 of the
|
||||||
|
// volume byte to flag LZW compression (not handled here).
|
||||||
|
|
||||||
|
#include "agi.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
|
||||||
|
// AGI directory files are flat arrays of 3-byte entries. The maximum
|
||||||
|
// directory length the loader will accept is bounded by AGI_MAX_-
|
||||||
|
// RESOURCES; larger files are reported as bad rather than truncated.
|
||||||
|
#define AGI_DIR_ENTRY_BYTES 3
|
||||||
|
|
||||||
|
// VOL.x resource records start with a 5-byte header.
|
||||||
|
#define AGI_VOL_HEADER_BYTES 5
|
||||||
|
#define AGI_VOL_SIG_0 0x12
|
||||||
|
#define AGI_VOL_SIG_1 0x34
|
||||||
|
#define AGI_VOL_COMPRESSED 0x80
|
||||||
|
#define AGI_VOL_VOLUME_MASK 0x7F
|
||||||
|
|
||||||
|
// Max directory + path string size. AGI filenames are 7 chars max;
|
||||||
|
// gameDir is application-controlled. 256 leaves comfortable margin.
|
||||||
|
#define AGI_PATH_MAX 256
|
||||||
|
|
||||||
|
// Empty-slot sentinel: must be >= AGI_MAX_VOLUMES so agiResLoad
|
||||||
|
// rejects empty entries. The on-disk empty marker is 0xFFFFFF (high
|
||||||
|
// nibble 0xF = volume 15); 15 is a legitimate volume number, so we
|
||||||
|
// don't reuse it as the sentinel.
|
||||||
|
#define AGI_DIR_EMPTY_VOLUME 0xFF
|
||||||
|
|
||||||
|
|
||||||
|
// IIgs 64 KB code-bank rule: park this TU in its own load segment
|
||||||
|
// so it doesn't pile onto _ROOT (which still holds main() and the
|
||||||
|
// ORCA-C startup). Cross-platform builds ignore this pragma.
|
||||||
|
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||||
|
segment "AGIRES";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Prototypes -----
|
||||||
|
|
||||||
|
static bool buildPath(char *out, size_t outSize, const char *dir, const char *file);
|
||||||
|
static bool buildVolPath(char *out, size_t outSize, const char *dir, uint8_t volNum);
|
||||||
|
static bool loadDirectory(AgiGameT *game, AgiResTypeE type, const char *gameDir, const char *fileName);
|
||||||
|
static bool openVolumes(AgiGameT *game, const char *gameDir);
|
||||||
|
static const char *resTypeFileName(AgiResTypeE type);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Internal helpers (alphabetical) -----
|
||||||
|
|
||||||
|
static bool buildPath(char *out, size_t outSize, const char *dir, const char *file) {
|
||||||
|
size_t dirLen;
|
||||||
|
size_t fileLen;
|
||||||
|
|
||||||
|
dirLen = strlen(dir);
|
||||||
|
fileLen = strlen(file);
|
||||||
|
if (dirLen + 1 + fileLen + 1 > outSize) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
memcpy(out, dir, dirLen);
|
||||||
|
out[dirLen] = '/';
|
||||||
|
memcpy(out + dirLen + 1, file, fileLen);
|
||||||
|
out[dirLen + 1 + fileLen] = '\0';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool buildVolPath(char *out, size_t outSize, const char *dir, uint8_t volNum) {
|
||||||
|
size_t dirLen;
|
||||||
|
char *p;
|
||||||
|
|
||||||
|
dirLen = strlen(dir);
|
||||||
|
// "/VOL." + up to 2 digits + NUL = 8 chars max.
|
||||||
|
if (dirLen + 8 > outSize) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
memcpy(out, dir, dirLen);
|
||||||
|
p = out + dirLen;
|
||||||
|
*p++ = '/';
|
||||||
|
*p++ = 'V';
|
||||||
|
*p++ = 'O';
|
||||||
|
*p++ = 'L';
|
||||||
|
*p++ = '.';
|
||||||
|
if (volNum >= 10u) {
|
||||||
|
*p++ = (char)('0' + (volNum / 10u));
|
||||||
|
*p++ = (char)('0' + (volNum % 10u));
|
||||||
|
} else {
|
||||||
|
*p++ = (char)('0' + volNum);
|
||||||
|
}
|
||||||
|
*p = '\0';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool loadDirectory(AgiGameT *game, AgiResTypeE type, const char *gameDir, const char *fileName) {
|
||||||
|
char path[AGI_PATH_MAX];
|
||||||
|
FILE *fp;
|
||||||
|
long fileSize;
|
||||||
|
uint16_t entryCount;
|
||||||
|
uint16_t i;
|
||||||
|
uint8_t buf[AGI_DIR_ENTRY_BYTES];
|
||||||
|
uint8_t volNibble;
|
||||||
|
uint32_t offset;
|
||||||
|
|
||||||
|
if (!buildPath(path, AGI_PATH_MAX, gameDir, fileName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fp = fopen(path, "rb");
|
||||||
|
if (fp == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fseek(fp, 0L, SEEK_END) != 0) {
|
||||||
|
fclose(fp);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
fileSize = ftell(fp);
|
||||||
|
if (fileSize < 0 || fileSize > (long)(AGI_MAX_RESOURCES * AGI_DIR_ENTRY_BYTES)) {
|
||||||
|
fclose(fp);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ((fileSize % AGI_DIR_ENTRY_BYTES) != 0) {
|
||||||
|
fclose(fp);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (fseek(fp, 0L, SEEK_SET) != 0) {
|
||||||
|
fclose(fp);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
entryCount = (uint16_t)(fileSize / AGI_DIR_ENTRY_BYTES);
|
||||||
|
for (i = 0; i < entryCount; i++) {
|
||||||
|
if (fread(buf, 1, AGI_DIR_ENTRY_BYTES, fp) != AGI_DIR_ENTRY_BYTES) {
|
||||||
|
fclose(fp);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
volNibble = (uint8_t)(buf[0] >> 4);
|
||||||
|
// 20-bit offset, cast to uint32_t before shift so ORCA-C's
|
||||||
|
// 16-bit int doesn't truncate the (& 0x0F) << 16 term.
|
||||||
|
offset = ((uint32_t)(buf[0] & 0x0F) << 16) | ((uint32_t)buf[1] << 8) | (uint32_t)buf[2];
|
||||||
|
|
||||||
|
if (buf[0] == 0xFF && buf[1] == 0xFF && buf[2] == 0xFF) {
|
||||||
|
game->resDir[type][i].volume = AGI_DIR_EMPTY_VOLUME;
|
||||||
|
game->resDir[type][i].offset = 0u;
|
||||||
|
} else {
|
||||||
|
game->resDir[type][i].volume = volNibble;
|
||||||
|
game->resDir[type][i].offset = offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(fp);
|
||||||
|
|
||||||
|
game->resCount[type] = entryCount;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool openVolumes(AgiGameT *game, const char *gameDir) {
|
||||||
|
char path[AGI_PATH_MAX];
|
||||||
|
uint8_t v;
|
||||||
|
bool anyOpened;
|
||||||
|
|
||||||
|
anyOpened = false;
|
||||||
|
for (v = 0; v < AGI_MAX_VOLUMES; v++) {
|
||||||
|
if (!buildVolPath(path, AGI_PATH_MAX, gameDir, v)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
game->volFiles[v] = fopen(path, "rb");
|
||||||
|
if (game->volFiles[v] != NULL) {
|
||||||
|
anyOpened = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return anyOpened;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static const char *resTypeFileName(AgiResTypeE type) {
|
||||||
|
switch (type) {
|
||||||
|
case AGI_RES_LOGIC: return "LOGDIR";
|
||||||
|
case AGI_RES_PIC: return "PICDIR";
|
||||||
|
case AGI_RES_VIEW: return "VIEWDIR";
|
||||||
|
case AGI_RES_SOUND: return "SNDDIR";
|
||||||
|
default: return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Public API (alphabetical) -----
|
||||||
|
|
||||||
|
void agiResClose(AgiGameT *game) {
|
||||||
|
uint8_t v;
|
||||||
|
uint8_t t;
|
||||||
|
|
||||||
|
for (v = 0; v < AGI_MAX_VOLUMES; v++) {
|
||||||
|
if (game->volFiles[v] != NULL) {
|
||||||
|
fclose(game->volFiles[v]);
|
||||||
|
game->volFiles[v] = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (t = 0; t < AGI_RES_COUNT; t++) {
|
||||||
|
game->resCount[t] = 0u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
uint8_t *agiResLoad(const AgiGameT *game, AgiResTypeE type, uint16_t index, uint16_t *outLength) {
|
||||||
|
const AgiResEntryT *entry;
|
||||||
|
FILE *fp;
|
||||||
|
uint8_t header[AGI_VOL_HEADER_BYTES];
|
||||||
|
uint8_t *payload;
|
||||||
|
uint16_t length;
|
||||||
|
uint8_t flagsAndVolume;
|
||||||
|
|
||||||
|
if (outLength != NULL) {
|
||||||
|
*outLength = 0u;
|
||||||
|
}
|
||||||
|
if (type >= AGI_RES_COUNT) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (index >= game->resCount[type]) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry = &game->resDir[type][index];
|
||||||
|
if (entry->volume >= AGI_MAX_VOLUMES) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
fp = game->volFiles[entry->volume];
|
||||||
|
if (fp == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fseek(fp, (long)entry->offset, SEEK_SET) != 0) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (fread(header, 1, AGI_VOL_HEADER_BYTES, fp) != AGI_VOL_HEADER_BYTES) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (header[0] != AGI_VOL_SIG_0 || header[1] != AGI_VOL_SIG_1) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
flagsAndVolume = header[2];
|
||||||
|
if (flagsAndVolume & AGI_VOL_COMPRESSED) {
|
||||||
|
// v3 LZW-compressed resource. Phase 2 work; not handled yet.
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if ((flagsAndVolume & AGI_VOL_VOLUME_MASK) != entry->volume) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
length = (uint16_t)(((uint16_t)header[4] << 8) | (uint16_t)header[3]);
|
||||||
|
if (length == 0u) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = (uint8_t *)malloc(length);
|
||||||
|
if (payload == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (fread(payload, 1, length, fp) != length) {
|
||||||
|
free(payload);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outLength != NULL) {
|
||||||
|
*outLength = length;
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool agiResOpen(AgiGameT *game, const char *gameDir) {
|
||||||
|
uint8_t t;
|
||||||
|
|
||||||
|
memset(game, 0, sizeof(*game));
|
||||||
|
|
||||||
|
for (t = 0; t < AGI_RES_COUNT; t++) {
|
||||||
|
if (!loadDirectory(game, (AgiResTypeE)t, gameDir, resTypeFileName((AgiResTypeE)t))) {
|
||||||
|
agiResClose(game);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!openVolumes(game, gameDir)) {
|
||||||
|
agiResClose(game);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
438
examples/agi/agiText.c
Normal file
438
examples/agi/agiText.c
Normal file
|
|
@ -0,0 +1,438 @@
|
||||||
|
// AGI v2 text plane: status line, display() rows, print() modal.
|
||||||
|
//
|
||||||
|
// Renders directly to the stage with an embedded 8x8 ASCII bitmap
|
||||||
|
// font; bypasses jlDrawText so glyph foreground/background can vary
|
||||||
|
// per-call without rebuilding a font surface for every color combo.
|
||||||
|
//
|
||||||
|
// Layout matches AGI's 40x25 text grid:
|
||||||
|
// Row 0 : status line (when statusLineOn)
|
||||||
|
// Rows 1..21 : picture (handled by agiPicBlit, not here)
|
||||||
|
// Rows 22..24 : message overlay area
|
||||||
|
//
|
||||||
|
// The print() modal centers a window over the picture region with
|
||||||
|
// a 1-character border and waits for user ack (HALT_PRINT_PENDING).
|
||||||
|
|
||||||
|
#include "agi.h"
|
||||||
|
|
||||||
|
#include "joey/draw.h"
|
||||||
|
#include "joey/surface.h"
|
||||||
|
#include "surfaceInternal.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||||
|
segment "AGITXT";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
#define GLYPH_W 8u
|
||||||
|
#define GLYPH_H 8u
|
||||||
|
#define FIRST_CHAR 0x20u // space
|
||||||
|
#define LAST_CHAR 0x7Eu // tilde
|
||||||
|
#define GLYPH_COUNT (LAST_CHAR - FIRST_CHAR + 1u)
|
||||||
|
|
||||||
|
#define STAGE_W SURFACE_WIDTH
|
||||||
|
#define STAGE_H SURFACE_HEIGHT
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Prototypes -----
|
||||||
|
|
||||||
|
static void drawChar(jlSurfaceT *stage, int16_t x, int16_t y, char ch, uint8_t fg, uint8_t bg);
|
||||||
|
static void drawCharTransparent(jlSurfaceT *stage, int16_t x, int16_t y, char ch, uint8_t fg);
|
||||||
|
static void drawString(jlSurfaceT *stage, int16_t x, int16_t y, const char *s, uint8_t fg, uint8_t bg);
|
||||||
|
static void drawWindow(jlSurfaceT *stage, int16_t x, int16_t y, uint8_t cols, uint8_t rows, uint8_t bg, uint8_t border);
|
||||||
|
static void fillRow(jlSurfaceT *stage, uint8_t row, uint8_t bg);
|
||||||
|
|
||||||
|
|
||||||
|
// 8x8 bitmap font for printable ASCII (0x20 = space .. 0x7E = tilde).
|
||||||
|
// Each glyph is 8 bytes; bit 7 is the leftmost pixel of each row.
|
||||||
|
// Lower-case letters map to upper-case via the ASCII normalizer in
|
||||||
|
// drawChar so the table only needs the upper half.
|
||||||
|
//
|
||||||
|
// Hand-pixeled to be readable rather than authentic. Each row is
|
||||||
|
// commented with the visual pattern (X = on, . = off).
|
||||||
|
static const uint8_t kFont[GLYPH_COUNT][GLYPH_H] = {
|
||||||
|
/* 0x20 ' ' */ {0,0,0,0,0,0,0,0},
|
||||||
|
/* 0x21 '!' */ {0x18,0x18,0x18,0x18,0x18,0x00,0x18,0x00},
|
||||||
|
/* 0x22 '"' */ {0x6C,0x6C,0x48,0x00,0x00,0x00,0x00,0x00},
|
||||||
|
/* 0x23 '#' */ {0x36,0x36,0x7F,0x36,0x7F,0x36,0x36,0x00},
|
||||||
|
/* 0x24 '$' */ {0x18,0x3E,0x60,0x3C,0x06,0x7C,0x18,0x00},
|
||||||
|
/* 0x25 '%' */ {0x66,0x66,0x0C,0x18,0x30,0x66,0x66,0x00},
|
||||||
|
/* 0x26 '&' */ {0x38,0x6C,0x38,0x76,0xDC,0xCC,0x76,0x00},
|
||||||
|
/* 0x27 '\''*/ {0x18,0x18,0x10,0x00,0x00,0x00,0x00,0x00},
|
||||||
|
/* 0x28 '(' */ {0x0C,0x18,0x30,0x30,0x30,0x18,0x0C,0x00},
|
||||||
|
/* 0x29 ')' */ {0x30,0x18,0x0C,0x0C,0x0C,0x18,0x30,0x00},
|
||||||
|
/* 0x2A '*' */ {0x00,0x66,0x3C,0xFF,0x3C,0x66,0x00,0x00},
|
||||||
|
/* 0x2B '+' */ {0x00,0x18,0x18,0x7E,0x18,0x18,0x00,0x00},
|
||||||
|
/* 0x2C ',' */ {0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x30},
|
||||||
|
/* 0x2D '-' */ {0x00,0x00,0x00,0x7E,0x00,0x00,0x00,0x00},
|
||||||
|
/* 0x2E '.' */ {0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x00},
|
||||||
|
/* 0x2F '/' */ {0x06,0x0C,0x18,0x30,0x60,0xC0,0x80,0x00},
|
||||||
|
/* 0x30 '0' */ {0x3C,0x66,0x6E,0x76,0x66,0x66,0x3C,0x00},
|
||||||
|
/* 0x31 '1' */ {0x18,0x38,0x18,0x18,0x18,0x18,0x7E,0x00},
|
||||||
|
/* 0x32 '2' */ {0x3C,0x66,0x06,0x1C,0x30,0x66,0x7E,0x00},
|
||||||
|
/* 0x33 '3' */ {0x3C,0x66,0x06,0x1C,0x06,0x66,0x3C,0x00},
|
||||||
|
/* 0x34 '4' */ {0x0C,0x1C,0x3C,0x6C,0x7E,0x0C,0x0C,0x00},
|
||||||
|
/* 0x35 '5' */ {0x7E,0x60,0x7C,0x06,0x06,0x66,0x3C,0x00},
|
||||||
|
/* 0x36 '6' */ {0x1C,0x30,0x60,0x7C,0x66,0x66,0x3C,0x00},
|
||||||
|
/* 0x37 '7' */ {0x7E,0x66,0x0C,0x18,0x18,0x18,0x18,0x00},
|
||||||
|
/* 0x38 '8' */ {0x3C,0x66,0x66,0x3C,0x66,0x66,0x3C,0x00},
|
||||||
|
/* 0x39 '9' */ {0x3C,0x66,0x66,0x3E,0x06,0x0C,0x38,0x00},
|
||||||
|
/* 0x3A ':' */ {0x00,0x18,0x18,0x00,0x00,0x18,0x18,0x00},
|
||||||
|
/* 0x3B ';' */ {0x00,0x18,0x18,0x00,0x00,0x18,0x18,0x30},
|
||||||
|
/* 0x3C '<' */ {0x0C,0x18,0x30,0x60,0x30,0x18,0x0C,0x00},
|
||||||
|
/* 0x3D '=' */ {0x00,0x00,0x7E,0x00,0x7E,0x00,0x00,0x00},
|
||||||
|
/* 0x3E '>' */ {0x30,0x18,0x0C,0x06,0x0C,0x18,0x30,0x00},
|
||||||
|
/* 0x3F '?' */ {0x3C,0x66,0x06,0x0C,0x18,0x00,0x18,0x00},
|
||||||
|
/* 0x40 '@' */ {0x3C,0x66,0x6E,0x6E,0x60,0x62,0x3C,0x00},
|
||||||
|
/* 0x41 'A' */ {0x18,0x3C,0x66,0x66,0x7E,0x66,0x66,0x00},
|
||||||
|
/* 0x42 'B' */ {0x7C,0x66,0x66,0x7C,0x66,0x66,0x7C,0x00},
|
||||||
|
/* 0x43 'C' */ {0x3C,0x66,0x60,0x60,0x60,0x66,0x3C,0x00},
|
||||||
|
/* 0x44 'D' */ {0x78,0x6C,0x66,0x66,0x66,0x6C,0x78,0x00},
|
||||||
|
/* 0x45 'E' */ {0x7E,0x60,0x60,0x78,0x60,0x60,0x7E,0x00},
|
||||||
|
/* 0x46 'F' */ {0x7E,0x60,0x60,0x78,0x60,0x60,0x60,0x00},
|
||||||
|
/* 0x47 'G' */ {0x3C,0x66,0x60,0x6E,0x66,0x66,0x3C,0x00},
|
||||||
|
/* 0x48 'H' */ {0x66,0x66,0x66,0x7E,0x66,0x66,0x66,0x00},
|
||||||
|
/* 0x49 'I' */ {0x3C,0x18,0x18,0x18,0x18,0x18,0x3C,0x00},
|
||||||
|
/* 0x4A 'J' */ {0x1E,0x0C,0x0C,0x0C,0x0C,0x6C,0x38,0x00},
|
||||||
|
/* 0x4B 'K' */ {0x66,0x6C,0x78,0x70,0x78,0x6C,0x66,0x00},
|
||||||
|
/* 0x4C 'L' */ {0x60,0x60,0x60,0x60,0x60,0x60,0x7E,0x00},
|
||||||
|
/* 0x4D 'M' */ {0x63,0x77,0x7F,0x6B,0x63,0x63,0x63,0x00},
|
||||||
|
/* 0x4E 'N' */ {0x66,0x76,0x7E,0x7E,0x6E,0x66,0x66,0x00},
|
||||||
|
/* 0x4F 'O' */ {0x3C,0x66,0x66,0x66,0x66,0x66,0x3C,0x00},
|
||||||
|
/* 0x50 'P' */ {0x7C,0x66,0x66,0x7C,0x60,0x60,0x60,0x00},
|
||||||
|
/* 0x51 'Q' */ {0x3C,0x66,0x66,0x66,0x6A,0x6C,0x36,0x00},
|
||||||
|
/* 0x52 'R' */ {0x7C,0x66,0x66,0x7C,0x6C,0x66,0x66,0x00},
|
||||||
|
/* 0x53 'S' */ {0x3C,0x66,0x60,0x3C,0x06,0x66,0x3C,0x00},
|
||||||
|
/* 0x54 'T' */ {0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x00},
|
||||||
|
/* 0x55 'U' */ {0x66,0x66,0x66,0x66,0x66,0x66,0x3C,0x00},
|
||||||
|
/* 0x56 'V' */ {0x66,0x66,0x66,0x66,0x66,0x3C,0x18,0x00},
|
||||||
|
/* 0x57 'W' */ {0x63,0x63,0x63,0x6B,0x7F,0x77,0x63,0x00},
|
||||||
|
/* 0x58 'X' */ {0x66,0x66,0x3C,0x18,0x3C,0x66,0x66,0x00},
|
||||||
|
/* 0x59 'Y' */ {0x66,0x66,0x66,0x3C,0x18,0x18,0x18,0x00},
|
||||||
|
/* 0x5A 'Z' */ {0x7E,0x06,0x0C,0x18,0x30,0x60,0x7E,0x00},
|
||||||
|
/* 0x5B '[' */ {0x3C,0x30,0x30,0x30,0x30,0x30,0x3C,0x00},
|
||||||
|
/* 0x5C '\\'*/ {0xC0,0x60,0x30,0x18,0x0C,0x06,0x02,0x00},
|
||||||
|
/* 0x5D ']' */ {0x3C,0x0C,0x0C,0x0C,0x0C,0x0C,0x3C,0x00},
|
||||||
|
/* 0x5E '^' */ {0x18,0x3C,0x66,0x00,0x00,0x00,0x00,0x00},
|
||||||
|
/* 0x5F '_' */ {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF},
|
||||||
|
/* 0x60 '`' */ {0x30,0x18,0x0C,0x00,0x00,0x00,0x00,0x00},
|
||||||
|
/* 0x61 'a' */ {0x00,0x00,0x3C,0x06,0x3E,0x66,0x3E,0x00},
|
||||||
|
/* 0x62 'b' */ {0x60,0x60,0x7C,0x66,0x66,0x66,0x7C,0x00},
|
||||||
|
/* 0x63 'c' */ {0x00,0x00,0x3C,0x66,0x60,0x66,0x3C,0x00},
|
||||||
|
/* 0x64 'd' */ {0x06,0x06,0x3E,0x66,0x66,0x66,0x3E,0x00},
|
||||||
|
/* 0x65 'e' */ {0x00,0x00,0x3C,0x66,0x7E,0x60,0x3C,0x00},
|
||||||
|
/* 0x66 'f' */ {0x1C,0x36,0x30,0x78,0x30,0x30,0x30,0x00},
|
||||||
|
/* 0x67 'g' */ {0x00,0x00,0x3E,0x66,0x66,0x3E,0x06,0x3C},
|
||||||
|
/* 0x68 'h' */ {0x60,0x60,0x7C,0x66,0x66,0x66,0x66,0x00},
|
||||||
|
/* 0x69 'i' */ {0x18,0x00,0x38,0x18,0x18,0x18,0x3C,0x00},
|
||||||
|
/* 0x6A 'j' */ {0x06,0x00,0x06,0x06,0x06,0x06,0x66,0x3C},
|
||||||
|
/* 0x6B 'k' */ {0x60,0x60,0x66,0x6C,0x78,0x6C,0x66,0x00},
|
||||||
|
/* 0x6C 'l' */ {0x38,0x18,0x18,0x18,0x18,0x18,0x3C,0x00},
|
||||||
|
/* 0x6D 'm' */ {0x00,0x00,0x66,0x7F,0x7F,0x6B,0x63,0x00},
|
||||||
|
/* 0x6E 'n' */ {0x00,0x00,0x7C,0x66,0x66,0x66,0x66,0x00},
|
||||||
|
/* 0x6F 'o' */ {0x00,0x00,0x3C,0x66,0x66,0x66,0x3C,0x00},
|
||||||
|
/* 0x70 'p' */ {0x00,0x00,0x7C,0x66,0x66,0x7C,0x60,0x60},
|
||||||
|
/* 0x71 'q' */ {0x00,0x00,0x3E,0x66,0x66,0x3E,0x06,0x06},
|
||||||
|
/* 0x72 'r' */ {0x00,0x00,0x7C,0x66,0x60,0x60,0x60,0x00},
|
||||||
|
/* 0x73 's' */ {0x00,0x00,0x3E,0x60,0x3C,0x06,0x7C,0x00},
|
||||||
|
/* 0x74 't' */ {0x18,0x18,0x7E,0x18,0x18,0x18,0x0E,0x00},
|
||||||
|
/* 0x75 'u' */ {0x00,0x00,0x66,0x66,0x66,0x66,0x3E,0x00},
|
||||||
|
/* 0x76 'v' */ {0x00,0x00,0x66,0x66,0x66,0x3C,0x18,0x00},
|
||||||
|
/* 0x77 'w' */ {0x00,0x00,0x63,0x6B,0x7F,0x7F,0x36,0x00},
|
||||||
|
/* 0x78 'x' */ {0x00,0x00,0x66,0x3C,0x18,0x3C,0x66,0x00},
|
||||||
|
/* 0x79 'y' */ {0x00,0x00,0x66,0x66,0x66,0x3E,0x06,0x3C},
|
||||||
|
/* 0x7A 'z' */ {0x00,0x00,0x7E,0x0C,0x18,0x30,0x7E,0x00},
|
||||||
|
/* 0x7B '{' */ {0x0E,0x18,0x18,0x70,0x18,0x18,0x0E,0x00},
|
||||||
|
/* 0x7C '|' */ {0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x00},
|
||||||
|
/* 0x7D '}' */ {0x70,0x18,0x18,0x0E,0x18,0x18,0x70,0x00},
|
||||||
|
/* 0x7E '~' */ {0x76,0xDC,0x00,0x00,0x00,0x00,0x00,0x00}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Internal helpers (alphabetical) -----
|
||||||
|
|
||||||
|
static void drawChar(jlSurfaceT *stage, int16_t x, int16_t y, char ch, uint8_t fg, uint8_t bg) {
|
||||||
|
const uint8_t *glyph;
|
||||||
|
uint8_t row;
|
||||||
|
uint8_t col;
|
||||||
|
uint8_t bits;
|
||||||
|
uint8_t c;
|
||||||
|
uint8_t *stagePixels;
|
||||||
|
|
||||||
|
c = (uint8_t)ch;
|
||||||
|
if (c < FIRST_CHAR || c > LAST_CHAR) {
|
||||||
|
c = FIRST_CHAR; // unknown -> space
|
||||||
|
}
|
||||||
|
glyph = kFont[c - FIRST_CHAR];
|
||||||
|
|
||||||
|
// Chunky fast path: each glyph row is 8 bits == 4 stage bytes
|
||||||
|
// (each byte = 2 pixels in 4bpp packed). Build each byte by
|
||||||
|
// checking 2 bits at a time. Bypasses jlDrawPixel's per-pixel
|
||||||
|
// overhead which dominates text rendering on slow targets.
|
||||||
|
stagePixels = (stage != NULL) ? stage->pixels : NULL;
|
||||||
|
if (stagePixels != NULL && x >= 0 && y >= 0 &&
|
||||||
|
x + (int16_t)GLYPH_W <= (int16_t)SURFACE_WIDTH &&
|
||||||
|
y + (int16_t)GLYPH_H <= (int16_t)SURFACE_HEIGHT &&
|
||||||
|
(x & 1) == 0) {
|
||||||
|
uint16_t rowOff;
|
||||||
|
uint16_t byteX;
|
||||||
|
uint8_t bytePair;
|
||||||
|
uint8_t left;
|
||||||
|
uint8_t right;
|
||||||
|
|
||||||
|
byteX = (uint16_t)(x >> 1);
|
||||||
|
for (row = 0u; row < GLYPH_H; row++) {
|
||||||
|
bits = glyph[row];
|
||||||
|
rowOff = (uint16_t)((y + row) * SURFACE_BYTES_PER_ROW + byteX);
|
||||||
|
for (col = 0u; col < GLYPH_W; col += 2u) {
|
||||||
|
left = (uint8_t)((bits & (uint8_t)(0x80u >> col)) ? fg : bg);
|
||||||
|
right = (uint8_t)((bits & (uint8_t)(0x80u >> (col + 1u))) ? fg : bg);
|
||||||
|
bytePair = (uint8_t)((left << 4) | right);
|
||||||
|
stagePixels[rowOff + (col >> 1)] = bytePair;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
surfaceMarkDirtyRect(stage, x, y, (int16_t)GLYPH_W, (int16_t)GLYPH_H);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Planar / out-of-bounds slow fallback.
|
||||||
|
for (row = 0u; row < GLYPH_H; row++) {
|
||||||
|
bits = glyph[row];
|
||||||
|
for (col = 0u; col < GLYPH_W; col++) {
|
||||||
|
uint8_t pixel;
|
||||||
|
|
||||||
|
pixel = ((bits >> (uint8_t)(7u - col)) & 0x01u) ? fg : bg;
|
||||||
|
jlDrawPixel(stage, (int16_t)(x + (int16_t)col), (int16_t)(y + (int16_t)row), pixel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Draw a char with transparent background: only foreground bits are
|
||||||
|
// written, the surface pixels underneath show through. Chunky fast
|
||||||
|
// path reads each stage byte, modifies set nibbles, writes back.
|
||||||
|
static void drawCharTransparent(jlSurfaceT *stage, int16_t x, int16_t y, char ch, uint8_t fg) {
|
||||||
|
const uint8_t *glyph;
|
||||||
|
uint8_t row;
|
||||||
|
uint8_t col;
|
||||||
|
uint8_t bits;
|
||||||
|
uint8_t c;
|
||||||
|
uint8_t *stagePixels;
|
||||||
|
|
||||||
|
c = (uint8_t)ch;
|
||||||
|
if (c < FIRST_CHAR || c > LAST_CHAR) {
|
||||||
|
return; // unknown -> nothing to draw (transparent)
|
||||||
|
}
|
||||||
|
glyph = kFont[c - FIRST_CHAR];
|
||||||
|
stagePixels = (stage != NULL) ? stage->pixels : NULL;
|
||||||
|
if (stagePixels != NULL && x >= 0 && y >= 0 &&
|
||||||
|
x + (int16_t)GLYPH_W <= (int16_t)SURFACE_WIDTH &&
|
||||||
|
y + (int16_t)GLYPH_H <= (int16_t)SURFACE_HEIGHT &&
|
||||||
|
(x & 1) == 0) {
|
||||||
|
uint16_t rowOff;
|
||||||
|
uint16_t byteX;
|
||||||
|
uint8_t existing;
|
||||||
|
uint8_t leftMask;
|
||||||
|
uint8_t rightMask;
|
||||||
|
uint8_t nibbleFg;
|
||||||
|
|
||||||
|
byteX = (uint16_t)(x >> 1);
|
||||||
|
nibbleFg = (uint8_t)(fg & 0x0Fu);
|
||||||
|
for (row = 0u; row < GLYPH_H; row++) {
|
||||||
|
bits = glyph[row];
|
||||||
|
if (bits == 0u) { continue; } // entire row transparent
|
||||||
|
rowOff = (uint16_t)((y + row) * SURFACE_BYTES_PER_ROW + byteX);
|
||||||
|
for (col = 0u; col < GLYPH_W; col += 2u) {
|
||||||
|
leftMask = (uint8_t)(bits & (uint8_t)(0x80u >> col));
|
||||||
|
rightMask = (uint8_t)(bits & (uint8_t)(0x80u >> (col + 1u)));
|
||||||
|
if (leftMask == 0u && rightMask == 0u) { continue; }
|
||||||
|
existing = stagePixels[rowOff + (col >> 1)];
|
||||||
|
if (leftMask) { existing = (uint8_t)((existing & 0x0Fu) | (nibbleFg << 4)); }
|
||||||
|
if (rightMask) { existing = (uint8_t)((existing & 0xF0u) | nibbleFg); }
|
||||||
|
stagePixels[rowOff + (col >> 1)] = existing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
surfaceMarkDirtyRect(stage, x, y, (int16_t)GLYPH_W, (int16_t)GLYPH_H);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slow fallback (planar or out-of-bounds).
|
||||||
|
for (row = 0u; row < GLYPH_H; row++) {
|
||||||
|
bits = glyph[row];
|
||||||
|
for (col = 0u; col < GLYPH_W; col++) {
|
||||||
|
if (bits & (uint8_t)(0x80u >> col)) {
|
||||||
|
jlDrawPixel(stage, (int16_t)(x + (int16_t)col), (int16_t)(y + (int16_t)row), fg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void drawString(jlSurfaceT *stage, int16_t x, int16_t y, const char *s, uint8_t fg, uint8_t bg) {
|
||||||
|
int16_t cur;
|
||||||
|
|
||||||
|
if (s == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cur = x;
|
||||||
|
while (*s != '\0') {
|
||||||
|
if (cur >= (int16_t)STAGE_W) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
drawChar(stage, cur, y, *s, fg, bg);
|
||||||
|
cur = (int16_t)(cur + (int16_t)GLYPH_W);
|
||||||
|
s++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void drawWindow(jlSurfaceT *stage, int16_t x, int16_t y, uint8_t cols, uint8_t rows, uint8_t bg, uint8_t border) {
|
||||||
|
int16_t w;
|
||||||
|
int16_t h;
|
||||||
|
|
||||||
|
w = (int16_t)((int16_t)cols * (int16_t)GLYPH_W);
|
||||||
|
h = (int16_t)((int16_t)rows * (int16_t)GLYPH_H);
|
||||||
|
jlFillRect(stage, x, y, (uint16_t)(w + 4), (uint16_t)(h + 4), border);
|
||||||
|
jlFillRect(stage, (int16_t)(x + 2), (int16_t)(y + 2), (uint16_t)w, (uint16_t)h, bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void fillRow(jlSurfaceT *stage, uint8_t row, uint8_t bg) {
|
||||||
|
jlFillRect(stage, 0, (int16_t)((int16_t)row * (int16_t)GLYPH_H), (uint16_t)STAGE_W, (uint16_t)GLYPH_H, bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Public API (alphabetical) -----
|
||||||
|
|
||||||
|
const uint16_t *agiTextAsciiMap(void) {
|
||||||
|
// Not used in this impl (we render direct), but expose so callers
|
||||||
|
// who want to use jlDrawText against agiTextFontSurface have the
|
||||||
|
// standard 16x6 layout map. Built lazily on first call.
|
||||||
|
static uint16_t map[256];
|
||||||
|
static bool built;
|
||||||
|
uint16_t i;
|
||||||
|
|
||||||
|
if (built) {
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
for (i = 0u; i < 256u; i++) {
|
||||||
|
if (i < FIRST_CHAR || i > LAST_CHAR) {
|
||||||
|
map[i] = (uint16_t)0xFFFFu;
|
||||||
|
} else {
|
||||||
|
uint8_t off;
|
||||||
|
|
||||||
|
off = (uint8_t)(i - FIRST_CHAR);
|
||||||
|
map[i] = (uint16_t)(((uint16_t)(off / 16u) << 8) | (uint16_t)(off % 16u));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
built = true;
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const jlSurfaceT *agiTextFontSurface(void) {
|
||||||
|
// No surface built in this impl (drawChar walks raw bits direct
|
||||||
|
// to the stage). Reserved for callers that prefer jlDrawText.
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void agiTextRender(const AgiVmT *vm, jlSurfaceT *stage) {
|
||||||
|
uint8_t r;
|
||||||
|
|
||||||
|
if (vm->statusLineOn) {
|
||||||
|
fillRow(stage, vm->statusLineRow, 15u); // white background
|
||||||
|
if (vm->textRows[vm->statusLineRow].text[0] != '\0') {
|
||||||
|
drawString(stage, 0, (int16_t)((int16_t)vm->statusLineRow * (int16_t)GLYPH_H),
|
||||||
|
vm->textRows[vm->statusLineRow].text, 0u, 15u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// display() rows: walk every row that has content. Glyphs draw
|
||||||
|
// transparently (no bg fill) so any PIC art behind shows through
|
||||||
|
// where glyph pixels are 0. Filling the whole row's bg would
|
||||||
|
// obscure picture elements that overlap the text band (e.g.
|
||||||
|
// KQ3's "King's Quest III" title art).
|
||||||
|
for (r = 0u; r < AGI_TEXT_ROWS; r++) {
|
||||||
|
const char *p;
|
||||||
|
int16_t x;
|
||||||
|
int16_t y;
|
||||||
|
uint8_t fg;
|
||||||
|
|
||||||
|
if (r == vm->statusLineRow && vm->statusLineOn) {
|
||||||
|
continue; // status line handled above
|
||||||
|
}
|
||||||
|
if (vm->textRows[r].text[0] == '\0') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
y = (int16_t)((int16_t)r * (int16_t)GLYPH_H);
|
||||||
|
x = 0;
|
||||||
|
fg = vm->textRows[r].fg;
|
||||||
|
p = vm->textRows[r].text;
|
||||||
|
while (*p != '\0' && x < (int16_t)STAGE_W) {
|
||||||
|
if (*p != ' ') {
|
||||||
|
drawCharTransparent(stage, x, y, *p, fg);
|
||||||
|
}
|
||||||
|
x = (int16_t)(x + (int16_t)GLYPH_W);
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vm->printModal.active) {
|
||||||
|
const char *p;
|
||||||
|
int16_t lineW;
|
||||||
|
int16_t maxW;
|
||||||
|
int16_t lineCount;
|
||||||
|
int16_t winX;
|
||||||
|
int16_t winY;
|
||||||
|
int16_t lineH;
|
||||||
|
int16_t textY;
|
||||||
|
int16_t curX;
|
||||||
|
|
||||||
|
// Measure: width = longest line in chars; height = lines.
|
||||||
|
lineW = 0;
|
||||||
|
maxW = 1;
|
||||||
|
lineCount = 1;
|
||||||
|
p = vm->printModal.message;
|
||||||
|
while (*p != '\0') {
|
||||||
|
if (*p == '\n') {
|
||||||
|
if (lineW > maxW) {
|
||||||
|
maxW = lineW;
|
||||||
|
}
|
||||||
|
lineW = 0;
|
||||||
|
lineCount++;
|
||||||
|
} else {
|
||||||
|
lineW++;
|
||||||
|
}
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
if (lineW > maxW) {
|
||||||
|
maxW = lineW;
|
||||||
|
}
|
||||||
|
if (maxW > (int16_t)AGI_TEXT_COLS) {
|
||||||
|
maxW = (int16_t)AGI_TEXT_COLS;
|
||||||
|
}
|
||||||
|
lineH = (int16_t)GLYPH_H;
|
||||||
|
winX = (int16_t)((STAGE_W - (maxW * (int16_t)GLYPH_W + 4)) / 2);
|
||||||
|
winY = (int16_t)((STAGE_H - (lineCount * lineH + 4)) / 2);
|
||||||
|
if (winX < 0) { winX = 0; }
|
||||||
|
if (winY < 0) { winY = 0; }
|
||||||
|
drawWindow(stage, winX, winY, (uint8_t)maxW, (uint8_t)lineCount, 15u, 4u);
|
||||||
|
|
||||||
|
textY = (int16_t)(winY + 2);
|
||||||
|
curX = (int16_t)(winX + 2);
|
||||||
|
p = vm->printModal.message;
|
||||||
|
while (*p != '\0') {
|
||||||
|
if (*p == '\n') {
|
||||||
|
textY = (int16_t)(textY + lineH);
|
||||||
|
curX = (int16_t)(winX + 2);
|
||||||
|
} else {
|
||||||
|
drawChar(stage, curX, textY, *p, 0u, 15u);
|
||||||
|
curX = (int16_t)(curX + (int16_t)GLYPH_W);
|
||||||
|
}
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
340
examples/agi/agiView.c
Normal file
340
examples/agi/agiView.c
Normal file
|
|
@ -0,0 +1,340 @@
|
||||||
|
// AGI v2 VIEW (sprite/animation) decoder.
|
||||||
|
//
|
||||||
|
// Parses an in-RAM VIEW resource into a structured loop/cel table
|
||||||
|
// once at load time, then renders any cel against the priority plane
|
||||||
|
// for per-pixel actor-vs-picture masking. Re-implements the public
|
||||||
|
// AGI VIEW byte layout from scratch.
|
||||||
|
//
|
||||||
|
// Pixel-doubled blit goes straight to the stage so animation cycles
|
||||||
|
// don't require an intermediate back-buffer. The 160x168 AGI surface
|
||||||
|
// maps onto stage columns (2 * agiX, 2 * agiX + 1) at the same destY
|
||||||
|
// agiPicBlit uses.
|
||||||
|
|
||||||
|
#include "agi.h"
|
||||||
|
|
||||||
|
#include "joey/draw.h"
|
||||||
|
#include "surfaceInternal.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
|
||||||
|
// VIEW header byte offsets (publicly documented v2 layout).
|
||||||
|
#define AGI_VIEW_HDR_LOOP_COUNT 2u
|
||||||
|
#define AGI_VIEW_HDR_LOOP_TABLE 5u
|
||||||
|
|
||||||
|
// Cel header bits (publicly documented v2 layout).
|
||||||
|
#define AGI_CEL_TRANSPARENT_MASK 0x0Fu
|
||||||
|
#define AGI_CEL_MIRROR_FLAG 0x80u
|
||||||
|
#define AGI_CEL_MIRROR_SRC_SHIFT 4u
|
||||||
|
#define AGI_CEL_MIRROR_SRC_MASK 0x07u
|
||||||
|
|
||||||
|
// Priority bands. Y 0..47 is sky (priority 4); Y 48..167 splits into
|
||||||
|
// 10 ground bands of 12 pixels each at priorities 5..14. Priority 15
|
||||||
|
// is "always on top" and reserved for HUD/overlay; an actor's natural
|
||||||
|
// Y never reaches it.
|
||||||
|
#define AGI_PRI_SKY_TOP 48
|
||||||
|
#define AGI_PRI_GROUND_FIRST 5u
|
||||||
|
#define AGI_PRI_GROUND_BAND_PX 12
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||||
|
segment "AGIVIEW";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Prototypes -----
|
||||||
|
|
||||||
|
static bool parseLoop(AgiLoopInfoT *loop, const uint8_t *loopStart, uint16_t maxBytes);
|
||||||
|
static const AgiCelInfoT *resolveMirror(const AgiViewT *view, uint8_t loopIdx, uint8_t celIdx, bool *outMirrored);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Internal helpers (alphabetical) -----
|
||||||
|
|
||||||
|
static bool parseLoop(AgiLoopInfoT *loop, const uint8_t *loopStart, uint16_t maxBytes) {
|
||||||
|
uint8_t celCount;
|
||||||
|
uint8_t i;
|
||||||
|
uint16_t celOffset;
|
||||||
|
const uint8_t *celStart;
|
||||||
|
uint8_t flags;
|
||||||
|
|
||||||
|
if (maxBytes < 1u) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
celCount = loopStart[0];
|
||||||
|
if ((uint16_t)celCount > AGI_MAX_CELS_PER_LOOP) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (maxBytes < (uint16_t)(1u + (uint16_t)celCount * 2u)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
loop->celCount = celCount;
|
||||||
|
for (i = 0u; i < celCount; i++) {
|
||||||
|
celOffset = (uint16_t)(loopStart[1u + (uint16_t)i * 2u] |
|
||||||
|
((uint16_t)loopStart[2u + (uint16_t)i * 2u] << 8));
|
||||||
|
if ((uint16_t)(celOffset + 3u) > maxBytes) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
celStart = loopStart + celOffset;
|
||||||
|
flags = celStart[2];
|
||||||
|
loop->cels[i].width = celStart[0];
|
||||||
|
loop->cels[i].height = celStart[1];
|
||||||
|
loop->cels[i].transparentColor = (uint8_t)(flags & AGI_CEL_TRANSPARENT_MASK);
|
||||||
|
loop->cels[i].mirrored = (uint8_t)((flags & AGI_CEL_MIRROR_FLAG) ? 1u : 0u);
|
||||||
|
loop->cels[i].mirrorSourceLoop = (uint8_t)((flags >> AGI_CEL_MIRROR_SRC_SHIFT) & AGI_CEL_MIRROR_SRC_MASK);
|
||||||
|
loop->cels[i].rleData = celStart + 3;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// If the cel is a mirror, swap to the source loop's same-index cel and
|
||||||
|
// flag the caller to flip the RLE walk horizontally. Mirror chains
|
||||||
|
// don't recurse in valid AGI data; we don't follow more than one hop.
|
||||||
|
//
|
||||||
|
// Sierra's encoder marks every cel in BOTH halves of a mirror pair
|
||||||
|
// with the mirror flag, with the source-loop field pointing to the
|
||||||
|
// lower-numbered loop. The cel is only an actual mirror when the
|
||||||
|
// source-loop field points somewhere ELSE; when source == this loop,
|
||||||
|
// this IS the source data and must be drawn unflipped (otherwise both
|
||||||
|
// loops in a pair render mirrored and e.g. Graham faces left whether
|
||||||
|
// the player presses left or right).
|
||||||
|
static const AgiCelInfoT *resolveMirror(const AgiViewT *view, uint8_t loopIdx, uint8_t celIdx, bool *outMirrored) {
|
||||||
|
const AgiCelInfoT *cel;
|
||||||
|
uint8_t sourceLoop;
|
||||||
|
|
||||||
|
if (loopIdx >= view->loopCount) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (celIdx >= view->loops[loopIdx].celCount) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
cel = &view->loops[loopIdx].cels[celIdx];
|
||||||
|
*outMirrored = false;
|
||||||
|
if (cel->mirrored) {
|
||||||
|
sourceLoop = cel->mirrorSourceLoop;
|
||||||
|
if (sourceLoop != loopIdx && sourceLoop < view->loopCount && celIdx < view->loops[sourceLoop].celCount) {
|
||||||
|
cel = &view->loops[sourceLoop].cels[celIdx];
|
||||||
|
*outMirrored = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cel;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Public API (alphabetical) -----
|
||||||
|
|
||||||
|
uint8_t agiActorPriorityForY(int16_t y) {
|
||||||
|
int16_t capped;
|
||||||
|
int16_t band;
|
||||||
|
|
||||||
|
if (y < AGI_PRI_SKY_TOP) {
|
||||||
|
return 4u;
|
||||||
|
}
|
||||||
|
capped = (y > (int16_t)(AGI_PIC_HEIGHT - 1)) ? (int16_t)(AGI_PIC_HEIGHT - 1) : y;
|
||||||
|
band = (int16_t)(AGI_PRI_GROUND_FIRST + (capped - AGI_PRI_SKY_TOP) / AGI_PRI_GROUND_BAND_PX);
|
||||||
|
if (band > 14) {
|
||||||
|
band = 14;
|
||||||
|
}
|
||||||
|
return (uint8_t)band;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||||
|
segment "AGIDRAW";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void agiViewDraw(const AgiViewT *view, uint8_t loopIdx, uint8_t celIdx,
|
||||||
|
int16_t x, int16_t y, uint8_t actorPri,
|
||||||
|
const uint8_t *picPriority,
|
||||||
|
jlSurfaceT *stage, int16_t destY) {
|
||||||
|
const AgiCelInfoT *cel;
|
||||||
|
const uint8_t *rle;
|
||||||
|
bool mirrored;
|
||||||
|
int16_t width;
|
||||||
|
int16_t height;
|
||||||
|
uint8_t transparent;
|
||||||
|
int16_t topY;
|
||||||
|
int16_t cy;
|
||||||
|
int16_t cx;
|
||||||
|
int16_t drawCx;
|
||||||
|
int16_t agiX;
|
||||||
|
int16_t agiY;
|
||||||
|
int16_t stageX;
|
||||||
|
int16_t stageY;
|
||||||
|
uint8_t rleByte;
|
||||||
|
uint8_t color;
|
||||||
|
uint8_t packed;
|
||||||
|
uint8_t runLen;
|
||||||
|
uint8_t r;
|
||||||
|
uint8_t picPri;
|
||||||
|
uint8_t *stagePixels;
|
||||||
|
|
||||||
|
cel = resolveMirror(view, loopIdx, celIdx, &mirrored);
|
||||||
|
if (cel == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
width = (int16_t)cel->width;
|
||||||
|
height = (int16_t)cel->height;
|
||||||
|
transparent = cel->transparentColor;
|
||||||
|
rle = cel->rleData;
|
||||||
|
topY = (int16_t)(y - height + 1);
|
||||||
|
stagePixels = (stage != NULL) ? stage->pixels : NULL;
|
||||||
|
|
||||||
|
// Per-pixel chunky fast path: pixel-doubled output means each
|
||||||
|
// source pixel maps to exactly one stage byte (both nibbles =
|
||||||
|
// same color). We can skip jlDrawPixel and write the byte
|
||||||
|
// direct, marking the cel bbox dirty once at the end. ~80x
|
||||||
|
// faster on DOSBox @ 386SX-equivalent CPU cycles.
|
||||||
|
if (stagePixels != NULL) {
|
||||||
|
int16_t minStageX = (int16_t)(x << 1);
|
||||||
|
int16_t maxStageX = (int16_t)((x + width) << 1);
|
||||||
|
int16_t minStageY = (int16_t)(destY + topY);
|
||||||
|
int16_t maxStageY = (int16_t)(destY + topY + height);
|
||||||
|
const uint8_t *priRow;
|
||||||
|
uint8_t *stageRow;
|
||||||
|
|
||||||
|
for (cy = 0; cy < height; cy++) {
|
||||||
|
agiY = (int16_t)(topY + cy);
|
||||||
|
// Hoist per-row work out of the per-pixel inner loop.
|
||||||
|
// For rows entirely outside the pic clip we still have to
|
||||||
|
// walk the RLE so the byte pointer ends in the right place
|
||||||
|
// for subsequent rows.
|
||||||
|
if (agiY < 0 || agiY >= (int16_t)AGI_PIC_HEIGHT) {
|
||||||
|
priRow = NULL;
|
||||||
|
stageRow = NULL;
|
||||||
|
} else {
|
||||||
|
priRow = picPriority + (int32_t)agiY * (int32_t)AGI_PIC_WIDTH;
|
||||||
|
stageRow = stagePixels +
|
||||||
|
(int32_t)(destY + agiY) * (int32_t)SURFACE_BYTES_PER_ROW;
|
||||||
|
}
|
||||||
|
cx = 0;
|
||||||
|
for (;;) {
|
||||||
|
rleByte = *rle++;
|
||||||
|
if (rleByte == 0u) { break; }
|
||||||
|
color = (uint8_t)(rleByte >> 4);
|
||||||
|
runLen = (uint8_t)(rleByte & 0x0Fu);
|
||||||
|
if (color == transparent) {
|
||||||
|
cx = (int16_t)(cx + runLen);
|
||||||
|
if (cx > width) { cx = width; }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (priRow == NULL) {
|
||||||
|
cx = (int16_t)(cx + runLen);
|
||||||
|
if (cx > width) { cx = width; }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
packed = (uint8_t)((color << 4) | color);
|
||||||
|
for (r = 0u; r < runLen; r++) {
|
||||||
|
if (cx >= width) { break; }
|
||||||
|
drawCx = mirrored ? (int16_t)(width - 1 - cx) : cx;
|
||||||
|
agiX = (int16_t)(x + drawCx);
|
||||||
|
if (agiX >= 0 && agiX < (int16_t)AGI_PIC_WIDTH) {
|
||||||
|
if (priRow[agiX] <= actorPri) {
|
||||||
|
stageRow[agiX] = packed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Dirty-rect tracking at the cel level only: we know the
|
||||||
|
// bbox a priori (the cel's stage rect), and per-pixel tight
|
||||||
|
// bounds aren't worth the inner-loop cost on a 386. Clip to
|
||||||
|
// the stage so the surface marker doesn't reject the whole
|
||||||
|
// rect for an off-screen edge.
|
||||||
|
if (minStageX < 0) { minStageX = 0; }
|
||||||
|
if (minStageY < 0) { minStageY = 0; }
|
||||||
|
if (maxStageX > (int16_t)SURFACE_WIDTH) { maxStageX = (int16_t)SURFACE_WIDTH; }
|
||||||
|
if (maxStageY > (int16_t)SURFACE_HEIGHT) { maxStageY = (int16_t)SURFACE_HEIGHT; }
|
||||||
|
if (maxStageX > minStageX && maxStageY > minStageY) {
|
||||||
|
surfaceMarkDirtyRect(stage, minStageX, minStageY,
|
||||||
|
(int16_t)(maxStageX - minStageX),
|
||||||
|
(int16_t)(maxStageY - minStageY));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Planar fallback (s->pixels NULL: Amiga Phase 9). Goes through
|
||||||
|
// jlDrawPixel so the port's c2p / plane-sync path stays correct.
|
||||||
|
for (cy = 0; cy < height; cy++) {
|
||||||
|
cx = 0;
|
||||||
|
for (;;) {
|
||||||
|
rleByte = *rle++;
|
||||||
|
if (rleByte == 0u) { break; }
|
||||||
|
color = (uint8_t)(rleByte >> 4);
|
||||||
|
runLen = (uint8_t)(rleByte & 0x0Fu);
|
||||||
|
for (r = 0u; r < runLen; r++) {
|
||||||
|
if (cx >= width) { break; }
|
||||||
|
if (color != transparent) {
|
||||||
|
drawCx = mirrored ? (int16_t)(width - 1 - cx) : cx;
|
||||||
|
agiX = (int16_t)(x + drawCx);
|
||||||
|
agiY = (int16_t)(topY + cy);
|
||||||
|
if (agiX >= 0 && agiX < (int16_t)AGI_PIC_WIDTH && agiY >= 0 && agiY < (int16_t)AGI_PIC_HEIGHT) {
|
||||||
|
picPri = picPriority[agiY * (int16_t)AGI_PIC_WIDTH + agiX];
|
||||||
|
if (picPri <= actorPri) {
|
||||||
|
stageX = (int16_t)(agiX << 1);
|
||||||
|
stageY = (int16_t)(destY + agiY);
|
||||||
|
jlDrawPixel(stage, stageX, stageY, color);
|
||||||
|
jlDrawPixel(stage, (int16_t)(stageX + 1), stageY, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void agiViewFree(AgiViewT *view) {
|
||||||
|
if (view->raw != NULL) {
|
||||||
|
free(view->raw);
|
||||||
|
view->raw = NULL;
|
||||||
|
}
|
||||||
|
view->rawLength = 0u;
|
||||||
|
view->loopCount = 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool agiViewParse(AgiViewT *view, uint8_t *rawBytes, uint16_t length) {
|
||||||
|
uint8_t loopCount;
|
||||||
|
uint8_t i;
|
||||||
|
uint16_t loopOffset;
|
||||||
|
|
||||||
|
memset(view, 0, sizeof(*view));
|
||||||
|
if (length < AGI_VIEW_HDR_LOOP_TABLE) {
|
||||||
|
free(rawBytes);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
loopCount = rawBytes[AGI_VIEW_HDR_LOOP_COUNT];
|
||||||
|
if ((uint16_t)loopCount > AGI_MAX_LOOPS_PER_VIEW) {
|
||||||
|
free(rawBytes);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ((uint16_t)(AGI_VIEW_HDR_LOOP_TABLE + (uint16_t)loopCount * 2u) > length) {
|
||||||
|
free(rawBytes);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
view->raw = rawBytes;
|
||||||
|
view->rawLength = length;
|
||||||
|
view->loopCount = loopCount;
|
||||||
|
|
||||||
|
for (i = 0u; i < loopCount; i++) {
|
||||||
|
loopOffset = (uint16_t)(rawBytes[AGI_VIEW_HDR_LOOP_TABLE + (uint16_t)i * 2u] |
|
||||||
|
((uint16_t)rawBytes[AGI_VIEW_HDR_LOOP_TABLE + 1u + (uint16_t)i * 2u] << 8));
|
||||||
|
if (loopOffset >= length) {
|
||||||
|
agiViewFree(view);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!parseLoop(&view->loops[i], rawBytes + loopOffset, (uint16_t)(length - loopOffset))) {
|
||||||
|
agiViewFree(view);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
2300
examples/agi/agiVm.c
Normal file
2300
examples/agi/agiVm.c
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -4,7 +4,7 @@
|
||||||
// the DATA folder shipped on the disk image (see make/<plat>.mk for
|
// the DATA folder shipped on the disk image (see make/<plat>.mk for
|
||||||
// each platform's packaging step).
|
// 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
|
// 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
|
// as a silent input loop -- you can confirm the build links and the
|
||||||
// input plumbing still works without hearing anything.
|
// 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 colors[SURFACE_COLORS_PER_PALETTE];
|
||||||
uint16_t i;
|
uint16_t i;
|
||||||
|
|
||||||
|
|
@ -92,24 +92,24 @@ static void buildPalette(SurfaceT *screen) {
|
||||||
colors[COLOR_HINT] = 0x0444;
|
colors[COLOR_HINT] = 0x0444;
|
||||||
colors[COLOR_BAR] = 0x00F0;
|
colors[COLOR_BAR] = 0x00F0;
|
||||||
|
|
||||||
paletteSet(screen, 0, colors);
|
jlPaletteSet(screen, 0, colors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Visual feedback for "audio is running, but you cannot tell on a
|
// Visual feedback for "audio is running, but you cannot tell on a
|
||||||
// stub HAL": pulse a horizontal bar between the hint color and the
|
// stub HAL": pulse a horizontal bar between the hint color and the
|
||||||
// active color whenever SFX has fired this frame.
|
// active color whenever SFX has fired this frame.
|
||||||
static void initialPaint(SurfaceT *screen, bool audioOk) {
|
static void initialPaint(jlSurfaceT *screen, bool audioOk) {
|
||||||
surfaceClear(screen, COLOR_BG);
|
jlSurfaceClear(screen, COLOR_BG);
|
||||||
fillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H,
|
jlFillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H,
|
||||||
audioOk ? COLOR_HINT : COLOR_BG);
|
audioOk ? COLOR_HINT : COLOR_BG);
|
||||||
stagePresent();
|
jlStagePresent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
int main(void) {
|
int main(void) {
|
||||||
JoeyConfigT config;
|
jlConfigT config;
|
||||||
SurfaceT *screen;
|
jlSurfaceT *screen;
|
||||||
bool audioOk;
|
bool audioOk;
|
||||||
int16_t flashFrames;
|
int16_t flashFrames;
|
||||||
uint8_t *modBytes;
|
uint8_t *modBytes;
|
||||||
|
|
@ -120,17 +120,16 @@ int main(void) {
|
||||||
config.codegenBytes = 8 * 1024;
|
config.codegenBytes = 8 * 1024;
|
||||||
config.maxSurfaces = 4;
|
config.maxSurfaces = 4;
|
||||||
config.audioBytes = 64UL * 1024;
|
config.audioBytes = 64UL * 1024;
|
||||||
config.assetBytes = 128UL * 1024;
|
|
||||||
|
|
||||||
if (!joeyInit(&config)) {
|
if (!jlInit(&config)) {
|
||||||
fprintf(stderr, "joeyInit failed: %s\n", joeyLastError());
|
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
screen = stageGet();
|
screen = jlStageGet();
|
||||||
if (screen == NULL) {
|
if (screen == NULL) {
|
||||||
fprintf(stderr, "stageGet returned NULL\n");
|
fprintf(stderr, "jlStageGet returned NULL\n");
|
||||||
joeyShutdown();
|
jlShutdown();
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -139,11 +138,11 @@ int main(void) {
|
||||||
modLen = 0;
|
modLen = 0;
|
||||||
sfxLen = 0;
|
sfxLen = 0;
|
||||||
|
|
||||||
audioOk = joeyAudioInit();
|
audioOk = jlAudioInit();
|
||||||
if (audioOk) {
|
if (audioOk) {
|
||||||
if (loadFile(TEST_MOD_PATH, 64UL * 1024UL, &modBytes, &modLen)) {
|
if (loadFile(TEST_MOD_PATH, 64UL * 1024UL, &modBytes, &modLen)) {
|
||||||
joeyAudioPlayMod(modBytes, modLen, true);
|
jlAudioPlayMod(modBytes, modLen, true);
|
||||||
// joeyAudioPlayMod copies the bytes into the engine's own
|
// jlAudioPlayMod copies the bytes into the engine's own
|
||||||
// buffer; safe to release ours immediately.
|
// buffer; safe to release ours immediately.
|
||||||
free(modBytes);
|
free(modBytes);
|
||||||
modBytes = NULL;
|
modBytes = NULL;
|
||||||
|
|
@ -152,40 +151,40 @@ int main(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
buildPalette(screen);
|
buildPalette(screen);
|
||||||
scbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
|
jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
|
||||||
initialPaint(screen, audioOk);
|
initialPaint(screen, audioOk);
|
||||||
|
|
||||||
flashFrames = 0;
|
flashFrames = 0;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
joeyWaitVBL();
|
jlWaitVBL();
|
||||||
joeyInputPoll();
|
jlInputPoll();
|
||||||
joeyAudioFrameTick();
|
jlAudioFrameTick();
|
||||||
if (joeyKeyPressed(KEY_ESCAPE)) {
|
if (jlKeyPressed(KEY_ESCAPE)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (joeyKeyPressed(KEY_SPACE) && sfxBytes != NULL) {
|
if (jlKeyPressed(KEY_SPACE) && sfxBytes != NULL) {
|
||||||
joeyAudioPlaySfx(SFX_SLOT, sfxBytes, sfxLen, SFX_RATE_HZ);
|
jlAudioPlaySfx(SFX_SLOT, sfxBytes, sfxLen, SFX_RATE_HZ);
|
||||||
flashFrames = 8;
|
flashFrames = 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flashFrames > 0) {
|
if (flashFrames > 0) {
|
||||||
fillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H, COLOR_BAR);
|
jlFillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H, COLOR_BAR);
|
||||||
stagePresent();
|
jlStagePresent();
|
||||||
flashFrames--;
|
flashFrames--;
|
||||||
if (flashFrames == 0) {
|
if (flashFrames == 0) {
|
||||||
fillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H, COLOR_HINT);
|
jlFillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H, COLOR_HINT);
|
||||||
stagePresent();
|
jlStagePresent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audioOk) {
|
if (audioOk) {
|
||||||
joeyAudioStopMod();
|
jlAudioStopMod();
|
||||||
joeyAudioShutdown();
|
jlAudioShutdown();
|
||||||
}
|
}
|
||||||
if (sfxBytes != NULL) {
|
if (sfxBytes != NULL) {
|
||||||
free(sfxBytes);
|
free(sfxBytes);
|
||||||
}
|
}
|
||||||
joeyShutdown();
|
jlShutdown();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,13 @@
|
||||||
// a clear visual signal that the underlying inner loops (C or ASM)
|
// a clear visual signal that the underlying inner loops (C or ASM)
|
||||||
// produced the expected pixel pattern.
|
// 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).
|
// 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).
|
// 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).
|
// 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.
|
// Holds the frame until the user presses ESC / RETURN / SPACE.
|
||||||
|
|
||||||
|
|
@ -32,17 +32,17 @@
|
||||||
#define C_ORANGE 8
|
#define C_ORANGE 8
|
||||||
#define C_GRAY 9
|
#define C_GRAY 9
|
||||||
|
|
||||||
static void buildPalette(SurfaceT *screen);
|
static void buildPalette(jlSurfaceT *screen);
|
||||||
static void drawCellBorder(SurfaceT *screen, int16_t cx, int16_t cy);
|
static void drawCellBorder(jlSurfaceT *screen, int16_t cx, int16_t cy);
|
||||||
static void drawAllCellBorders(SurfaceT *screen);
|
static void drawAllCellBorders(jlSurfaceT *screen);
|
||||||
static void drawPrimitivesPixelLine(SurfaceT *screen);
|
static void drawPrimitivesPixelLine(jlSurfaceT *screen);
|
||||||
static void drawPrimitivesRect(SurfaceT *screen);
|
static void drawPrimitivesRect(jlSurfaceT *screen);
|
||||||
static void drawPrimitivesCircle(SurfaceT *screen);
|
static void drawPrimitivesCircle(jlSurfaceT *screen);
|
||||||
static void drawPrimitivesTileFlood(SurfaceT *screen);
|
static void drawPrimitivesTileFlood(jlSurfaceT *screen);
|
||||||
static void waitForKey(void);
|
static void waitForKey(void);
|
||||||
|
|
||||||
|
|
||||||
static void buildPalette(SurfaceT *screen) {
|
static void buildPalette(jlSurfaceT *screen) {
|
||||||
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
|
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
|
||||||
|
|
||||||
// 16 distinct $0RGB entries. Index 0 is forced to black anyway.
|
// 16 distinct $0RGB entries. Index 0 is forced to black anyway.
|
||||||
|
|
@ -62,17 +62,17 @@ static void buildPalette(SurfaceT *screen) {
|
||||||
colors[13] = 0x880;
|
colors[13] = 0x880;
|
||||||
colors[14] = 0x088;
|
colors[14] = 0x088;
|
||||||
colors[15] = 0x808;
|
colors[15] = 0x808;
|
||||||
paletteSet(screen, 0, colors);
|
jlPaletteSet(screen, 0, colors);
|
||||||
scbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
|
jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static void drawCellBorder(SurfaceT *screen, int16_t cx, int16_t cy) {
|
static void drawCellBorder(jlSurfaceT *screen, int16_t cx, int16_t cy) {
|
||||||
drawRect(screen, cx, cy, CELL_W, CELL_H, C_BORDER);
|
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, 0, 0);
|
||||||
drawCellBorder(screen, CELL_W, 0);
|
drawCellBorder(screen, CELL_W, 0);
|
||||||
drawCellBorder(screen, 0, CELL_H);
|
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
|
// 8 lines fan out from the cell center (80, 50). Four are diagonal
|
||||||
// (the new ASM Bresenham path) and four are axis-aligned (drawLine
|
// (the new ASM Bresenham path) and four are axis-aligned (jlDrawLine
|
||||||
// routes those to fillRect). A horizontal row of 14 pixels along the
|
// routes those to jlFillRect). A horizontal row of 14 pixels along the
|
||||||
// cell's bottom verifies drawPixel: each pixel uses a different color
|
// cell's bottom verifies jlDrawPixel: each pixel uses a different color
|
||||||
// index so the leftmost ones at color 0 are invisible (they are bg)
|
// index so the leftmost ones at color 0 are invisible (they are bg)
|
||||||
// and 1..13 progress through the palette.
|
// and 1..13 progress through the palette.
|
||||||
static void drawPrimitivesPixelLine(SurfaceT *screen) {
|
static void drawPrimitivesPixelLine(jlSurfaceT *screen) {
|
||||||
int16_t cx;
|
int16_t cx;
|
||||||
int16_t cy;
|
int16_t cy;
|
||||||
int16_t i;
|
int16_t i;
|
||||||
|
|
@ -96,180 +96,179 @@ static void drawPrimitivesPixelLine(SurfaceT *screen) {
|
||||||
cx = CELL_W / 2; // 80
|
cx = CELL_W / 2; // 80
|
||||||
cy = CELL_H / 2; // 50
|
cy = CELL_H / 2; // 50
|
||||||
|
|
||||||
drawLine(screen, cx, cy, cx + 70, cy, C_RED); // E (horizontal)
|
jlDrawLine(screen, cx, cy, cx + 70, cy, C_RED); // E (horizontal)
|
||||||
drawLine(screen, cx, cy, cx + 60, cy - 40, C_GREEN); // NE (diagonal)
|
jlDrawLine(screen, cx, cy, cx + 60, cy - 40, C_GREEN); // NE (diagonal)
|
||||||
drawLine(screen, cx, cy, cx, cy - 45, C_BLUE); // N (vertical)
|
jlDrawLine(screen, cx, cy, cx, cy - 45, C_BLUE); // N (vertical)
|
||||||
drawLine(screen, cx, cy, cx - 60, cy - 40, C_YELLOW); // NW
|
jlDrawLine(screen, cx, cy, cx - 60, cy - 40, C_YELLOW); // NW
|
||||||
drawLine(screen, cx, cy, cx - 70, cy, C_CYAN); // W
|
jlDrawLine(screen, cx, cy, cx - 70, cy, C_CYAN); // W
|
||||||
drawLine(screen, cx, cy, cx - 60, cy + 40, C_MAGENTA); // SW
|
jlDrawLine(screen, cx, cy, cx - 60, cy + 40, C_MAGENTA); // SW
|
||||||
drawLine(screen, cx, cy, cx, cy + 45, C_ORANGE); // S
|
jlDrawLine(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 + 60, cy + 40, C_GRAY); // SE
|
||||||
|
|
||||||
// Pixel row: 14 single-pixel writes at consecutive x to exercise
|
// Pixel row: 14 single-pixel writes at consecutive x to exercise
|
||||||
// both odd and even nibble paths.
|
// both odd and even nibble paths.
|
||||||
for (i = 0; i < 14; i++) {
|
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));
|
(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
|
// Four nested rectangles with deliberately odd x/y/w/h to exercise
|
||||||
// the partial-byte (nibble) edge handling in halFastFillRect. The
|
// the partial-byte (nibble) edge handling in halFastFillRect. The
|
||||||
// outermost is filled, the next outline-only, then filled with odd
|
// outermost is filled, the next outline-only, then filled with odd
|
||||||
// width, then a 1-pixel-wide vertical bar (drawRect collapses to a
|
// width, then a 1-pixel-wide vertical bar (jlDrawRect collapses to a
|
||||||
// line via fillRect's 1-wide path).
|
// line via jlFillRect's 1-wide path).
|
||||||
static void drawPrimitivesRect(SurfaceT *screen) {
|
static void drawPrimitivesRect(jlSurfaceT *screen) {
|
||||||
int16_t ox;
|
int16_t ox;
|
||||||
|
|
||||||
ox = CELL_W; // cell origin x
|
ox = CELL_W; // cell origin x
|
||||||
|
|
||||||
// Outer fill, even-aligned.
|
// 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.
|
// 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.
|
// Odd width fill, odd x.
|
||||||
fillRect(screen, ox + 25, 25, 35, 48, C_GREEN);
|
jlFillRect(screen, ox + 25, 25, 35, 48, C_GREEN);
|
||||||
// 1-pixel vertical bar (degenerate rect through fillRect 1-wide path).
|
// 1-pixel vertical bar (degenerate rect through jlFillRect 1-wide path).
|
||||||
fillRect(screen, ox + 100, 25, 1, 48, C_BORDER);
|
jlFillRect(screen, ox + 100, 25, 1, 48, C_BORDER);
|
||||||
// Odd-x odd-w narrow bar to specifically hit hasLeading + hasTrailing
|
// Odd-x odd-w narrow bar to specifically hit hasLeading + hasTrailing
|
||||||
// in halFastFillRect.
|
// 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
|
// Concentric outlines at decreasing radii, alternating colors, plus a
|
||||||
// small filled disk at the center. Center is at the cell midpoint.
|
// 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 cx;
|
||||||
int16_t cy;
|
int16_t cy;
|
||||||
|
|
||||||
cx = CELL_W / 2;
|
cx = CELL_W / 2;
|
||||||
cy = CELL_H + CELL_H / 2;
|
cy = CELL_H + CELL_H / 2;
|
||||||
|
|
||||||
drawCircle(screen, cx, cy, 45, C_BORDER);
|
jlDrawCircle(screen, cx, cy, 45, C_BORDER);
|
||||||
drawCircle(screen, cx, cy, 35, C_GREEN);
|
jlDrawCircle(screen, cx, cy, 35, C_GREEN);
|
||||||
drawCircle(screen, cx, cy, 25, C_YELLOW);
|
jlDrawCircle(screen, cx, cy, 25, C_YELLOW);
|
||||||
drawCircle(screen, cx, cy, 15, C_CYAN);
|
jlDrawCircle(screen, cx, cy, 15, C_CYAN);
|
||||||
fillCircle(screen, cx, cy, 8, C_MAGENTA);
|
jlFillCircle(screen, cx, cy, 8, C_MAGENTA);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Bottom-right cell: tile + flood fill.
|
// Bottom-right cell: tile + flood fill.
|
||||||
//
|
//
|
||||||
// Top portion: a 16x16 colored block, then tileSnap one of its 8x8
|
// Top portion: a 16x16 colored block, then jlTileSnap one of its 8x8
|
||||||
// quadrants and tilePaste the captured tile to a neighbor block;
|
// quadrants and jlTilePaste the captured tile to a neighbor block;
|
||||||
// also tileCopy the same source quadrant to a third location to
|
// also jlTileCopy the same source quadrant to a third location to
|
||||||
// exercise the full surface-to-surface path. Then a tileCopyMasked
|
// exercise the full surface-to-surface path. Then a jlTileCopyMasked
|
||||||
// case: paint a 2x1 (16x8) "stripe" containing a transparent color 0
|
// case: paint a 2x1 (16x8) "stripe" containing a transparent color 0
|
||||||
// pattern interleaved with color, paste it over a solid backdrop with
|
// pattern interleaved with color, paste it over a solid backdrop with
|
||||||
// transparent=0; the backdrop should show through the transparent
|
// transparent=0; the backdrop should show through the transparent
|
||||||
// nibbles.
|
// 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.
|
// 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 ox;
|
||||||
int16_t oy;
|
int16_t oy;
|
||||||
int16_t bx;
|
int16_t bx;
|
||||||
int16_t by;
|
int16_t by;
|
||||||
int16_t i;
|
int16_t i;
|
||||||
int16_t px;
|
int16_t px;
|
||||||
TileT snapBuf;
|
jlTileT snapBuf;
|
||||||
|
|
||||||
ox = CELL_W; // 160
|
ox = CELL_W; // 160
|
||||||
oy = CELL_H; // 100
|
oy = CELL_H; // 100
|
||||||
|
|
||||||
// Source 16x16 block at (168, 108): a 4-quadrant pattern.
|
// Source 16x16 block at (168, 108): a 4-quadrant pattern.
|
||||||
fillRect(screen, ox + 8, oy + 8, 8, 8, C_RED);
|
jlFillRect(screen, ox + 8, oy + 8, 8, 8, C_RED);
|
||||||
fillRect(screen, ox + 16, oy + 8, 8, 8, C_GREEN);
|
jlFillRect(screen, ox + 16, oy + 8, 8, 8, C_GREEN);
|
||||||
fillRect(screen, ox + 8, oy + 16, 8, 8, C_BLUE);
|
jlFillRect(screen, ox + 8, oy + 16, 8, 8, C_BLUE);
|
||||||
fillRect(screen, ox + 16, oy + 16, 8, 8, C_YELLOW);
|
jlFillRect(screen, ox + 16, oy + 16, 8, 8, C_YELLOW);
|
||||||
|
|
||||||
// tileSnap the top-left red quadrant (block bx=21, by=13) and
|
// jlTileSnap the top-left red quadrant (block bx=21, by=13) and
|
||||||
// tilePaste it next to the 16x16 source as the 5th quadrant.
|
// jlTilePaste it next to the 16x16 source as the 5th quadrant.
|
||||||
bx = (int16_t)((ox + 8) / 8);
|
bx = (int16_t)((ox + 8) / 8);
|
||||||
by = (int16_t)((oy + 8) / 8);
|
by = (int16_t)((oy + 8) / 8);
|
||||||
tileSnap(screen, (uint8_t)bx, (uint8_t)by, &snapBuf);
|
jlTileSnap(screen, (uint8_t)bx, (uint8_t)by, &snapBuf);
|
||||||
tilePaste(screen, (uint8_t)(bx + 4), (uint8_t)by, &snapBuf);
|
jlTilePaste(screen, (uint8_t)(bx + 4), (uint8_t)by, &snapBuf);
|
||||||
|
|
||||||
// tileCopy from the green quadrant onto a fresh location below.
|
// jlTileCopy from the green quadrant onto a fresh location below.
|
||||||
tileCopy(screen, (uint8_t)(bx + 4), (uint8_t)(by + 1),
|
jlTileCopy(screen, (uint8_t)(bx + 4), (uint8_t)(by + 1),
|
||||||
screen, (uint8_t)(bx + 1), (uint8_t)by);
|
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
|
// (208, 132). The tile's source has color 0 in alternating
|
||||||
// nibbles. Paste it onto a solid orange backdrop so transparent
|
// nibbles. Paste it onto a solid orange backdrop so transparent
|
||||||
// nibbles let the orange show through.
|
// 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)
|
// Build a vertical-stripe source at (240, 132): col-pixel = (px % 2 ? color : 0)
|
||||||
for (i = 0; i < 8; i++) {
|
for (i = 0; i < 8; i++) {
|
||||||
for (px = 0; px < 16; px++) {
|
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));
|
(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
|
// -> 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),
|
screen, (uint8_t)((ox + 112) / 8), (uint8_t)((oy + 32) / 8),
|
||||||
0);
|
0);
|
||||||
|
|
||||||
// Flood-fill region: a small bordered rectangle in the cell's
|
// 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
|
// from a point inside should fill with C_CYAN, stopping at the
|
||||||
// border.
|
// border.
|
||||||
drawRect(screen, ox + 16, oy + 60, 64, 32, C_BORDER);
|
jlDrawRect(screen, ox + 16, oy + 60, 64, 32, C_BORDER);
|
||||||
floodFillBounded(screen, (int16_t)(ox + 48), (int16_t)(oy + 76),
|
jlFloodFillBounded(screen, (int16_t)(ox + 48), (int16_t)(oy + 76),
|
||||||
C_CYAN, C_BORDER);
|
C_CYAN, C_BORDER);
|
||||||
|
|
||||||
// Plain floodFill: solid block then re-fill to a new color.
|
// Plain jlFloodFill: solid block then re-fill to a new color.
|
||||||
fillRect(screen, ox + 96, oy + 60, 48, 32, C_GREEN);
|
jlFillRect(screen, ox + 96, oy + 60, 48, 32, C_GREEN);
|
||||||
floodFill(screen, (int16_t)(ox + 120), (int16_t)(oy + 76), C_GRAY);
|
jlFloodFill(screen, (int16_t)(ox + 120), (int16_t)(oy + 76), C_GRAY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static void waitForKey(void) {
|
static void waitForKey(void) {
|
||||||
joeyWaitForAnyKey();
|
jlWaitForAnyKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
int main(void) {
|
int main(void) {
|
||||||
JoeyConfigT config;
|
jlConfigT config;
|
||||||
SurfaceT *screen;
|
jlSurfaceT *screen;
|
||||||
|
|
||||||
config.codegenBytes = 8 * 1024;
|
config.codegenBytes = 8 * 1024;
|
||||||
config.maxSurfaces = 4;
|
config.maxSurfaces = 4;
|
||||||
config.audioBytes = 64UL * 1024;
|
config.audioBytes = 64UL * 1024;
|
||||||
config.assetBytes = 128UL * 1024;
|
|
||||||
|
|
||||||
if (!joeyInit(&config)) {
|
if (!jlInit(&config)) {
|
||||||
fprintf(stderr, "joeyInit failed: %s\n", joeyLastError());
|
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
screen = stageGet();
|
screen = jlStageGet();
|
||||||
if (screen == NULL) {
|
if (screen == NULL) {
|
||||||
fprintf(stderr, "stageGet returned NULL\n");
|
fprintf(stderr, "jlStageGet returned NULL\n");
|
||||||
joeyShutdown();
|
jlShutdown();
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildPalette(screen);
|
buildPalette(screen);
|
||||||
surfaceClear(screen, C_BG);
|
jlSurfaceClear(screen, C_BG);
|
||||||
drawAllCellBorders(screen);
|
drawAllCellBorders(screen);
|
||||||
drawPrimitivesPixelLine(screen);
|
drawPrimitivesPixelLine(screen);
|
||||||
drawPrimitivesRect(screen);
|
drawPrimitivesRect(screen);
|
||||||
drawPrimitivesCircle(screen);
|
drawPrimitivesCircle(screen);
|
||||||
drawPrimitivesTileFlood(screen);
|
drawPrimitivesTileFlood(screen);
|
||||||
stagePresent();
|
jlStagePresent();
|
||||||
|
|
||||||
waitForKey();
|
waitForKey();
|
||||||
|
|
||||||
joeyShutdown();
|
jlShutdown();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,22 +7,21 @@
|
||||||
#include <joey/joey.h>
|
#include <joey/joey.h>
|
||||||
|
|
||||||
int main(void) {
|
int main(void) {
|
||||||
JoeyConfigT config;
|
jlConfigT config;
|
||||||
|
|
||||||
config.codegenBytes = 8 * 1024;
|
config.codegenBytes = 8 * 1024;
|
||||||
config.maxSurfaces = 4;
|
config.maxSurfaces = 4;
|
||||||
config.audioBytes = 64UL * 1024;
|
config.audioBytes = 64UL * 1024;
|
||||||
config.assetBytes = 128UL * 1024;
|
|
||||||
|
|
||||||
if (!joeyInit(&config)) {
|
if (!jlInit(&config)) {
|
||||||
fprintf(stderr, "joeyInit failed: %s\n", joeyLastError());
|
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
printf("JoeyLib %s\n", joeyVersionString());
|
printf("JoeyLib %s\n", jlVersionString());
|
||||||
printf("Platform: %s\n", joeyPlatformName());
|
printf("Platform: %s\n", jlPlatformName());
|
||||||
printf("Hello from JoeyLib.\n");
|
printf("Hello from JoeyLib.\n");
|
||||||
|
|
||||||
joeyShutdown();
|
jlShutdown();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,14 +39,14 @@
|
||||||
#define STICK0_LEFT 24
|
#define STICK0_LEFT 24
|
||||||
#define STICK1_LEFT 216
|
#define STICK1_LEFT 216
|
||||||
|
|
||||||
static void buildPalette(SurfaceT *screen);
|
static void buildPalette(jlSurfaceT *screen);
|
||||||
static void drawFrame(SurfaceT *screen, int16_t left);
|
static void drawFrame(jlSurfaceT *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 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 dotXFor(int16_t left, int8_t ax);
|
||||||
static int16_t dotYFor(int8_t ay);
|
static int16_t dotYFor(int8_t ay);
|
||||||
static int16_t buttonXFor(int16_t left, int idx);
|
static int16_t buttonXFor(int16_t left, int idx);
|
||||||
static void initialPaint(SurfaceT *screen);
|
static void initialPaint(jlSurfaceT *screen);
|
||||||
static void updateStick(SurfaceT *screen, JoeyJoystickE js, int16_t left);
|
static void updateStick(jlSurfaceT *screen, jlJoystickE js, int16_t left);
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
int16_t dotX;
|
int16_t dotX;
|
||||||
|
|
@ -59,7 +59,7 @@ typedef struct {
|
||||||
static StickViewT gView[JOYSTICK_COUNT];
|
static StickViewT gView[JOYSTICK_COUNT];
|
||||||
|
|
||||||
|
|
||||||
static void buildPalette(SurfaceT *screen) {
|
static void buildPalette(jlSurfaceT *screen) {
|
||||||
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
|
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
|
||||||
uint16_t i;
|
uint16_t i;
|
||||||
|
|
||||||
|
|
@ -75,26 +75,26 @@ static void buildPalette(SurfaceT *screen) {
|
||||||
colors[COLOR_CONNECTED] = 0x00F0;
|
colors[COLOR_CONNECTED] = 0x00F0;
|
||||||
colors[COLOR_DISCONNECTED] = 0x0F00; // red
|
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) {
|
static void drawAndPresent(jlSurfaceT *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
|
/* jlFillRect marks the rect dirty; jlStagePresent flushes only that
|
||||||
* dirty band. */
|
* dirty band. */
|
||||||
fillRect(screen, x, y, (uint16_t)w, (uint16_t)h, color);
|
jlFillRect(screen, x, y, (uint16_t)w, (uint16_t)h, color);
|
||||||
stagePresent();
|
jlStagePresent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Draw the static stick frame (just an outlined square -- four edges
|
// Draw the static stick frame (just an outlined square -- four edges
|
||||||
// with the inset filled background).
|
// with the inset filled background).
|
||||||
static void drawFrame(SurfaceT *screen, int16_t left) {
|
static void drawFrame(jlSurfaceT *screen, int16_t left) {
|
||||||
int16_t top;
|
int16_t top;
|
||||||
|
|
||||||
top = STICK_TOP;
|
top = STICK_TOP;
|
||||||
fillRect(screen, left, top, FRAME_SIZE, FRAME_SIZE, COLOR_FRAME);
|
jlFillRect(screen, left, top, FRAME_SIZE, FRAME_SIZE, COLOR_FRAME);
|
||||||
fillRect(screen,
|
jlFillRect(screen,
|
||||||
(int16_t)(left + FRAME_INSET),
|
(int16_t)(left + FRAME_INSET),
|
||||||
(int16_t)(top + FRAME_INSET),
|
(int16_t)(top + FRAME_INSET),
|
||||||
(uint16_t)(FRAME_SIZE - 2 * 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;
|
int16_t i;
|
||||||
|
|
||||||
surfaceClear(screen, COLOR_BACKGROUND);
|
jlSurfaceClear(screen, COLOR_BACKGROUND);
|
||||||
for (i = 0; i < JOYSTICK_COUNT; i++) {
|
for (i = 0; i < JOYSTICK_COUNT; i++) {
|
||||||
int16_t left;
|
int16_t left;
|
||||||
left = (i == 0) ? STICK0_LEFT : STICK1_LEFT;
|
left = (i == 0) ? STICK0_LEFT : STICK1_LEFT;
|
||||||
|
|
@ -145,13 +145,13 @@ static void initialPaint(SurfaceT *screen) {
|
||||||
gView[i].valid = false;
|
gView[i].valid = false;
|
||||||
gView[i].connected = false;
|
gView[i].connected = false;
|
||||||
}
|
}
|
||||||
stagePresent();
|
jlStagePresent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Compare current joystick state against gView and redraw / present
|
// Compare current joystick state against gView and redraw / present
|
||||||
// only the visual elements that changed.
|
// 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;
|
StickViewT *v;
|
||||||
int8_t ax;
|
int8_t ax;
|
||||||
int8_t ay;
|
int8_t ay;
|
||||||
|
|
@ -164,13 +164,13 @@ static void updateStick(SurfaceT *screen, JoeyJoystickE js, int16_t left) {
|
||||||
int16_t by;
|
int16_t by;
|
||||||
|
|
||||||
v = &gView[js];
|
v = &gView[js];
|
||||||
connected = joeyJoystickConnected(js);
|
connected = jlJoystickConnected(js);
|
||||||
ax = joeyJoystickX(js);
|
ax = jlJoystickX(js);
|
||||||
ay = joeyJoystickY(js);
|
ay = jlJoystickY(js);
|
||||||
newDotX = dotXFor(left, ax);
|
newDotX = dotXFor(left, ax);
|
||||||
newDotY = dotYFor(ay);
|
newDotY = dotYFor(ay);
|
||||||
for (i = 0; i < JOY_BUTTON_COUNT; i++) {
|
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) {
|
if (!v->valid || v->connected != connected) {
|
||||||
|
|
@ -214,40 +214,39 @@ static void updateStick(SurfaceT *screen, JoeyJoystickE js, int16_t left) {
|
||||||
|
|
||||||
|
|
||||||
int main(void) {
|
int main(void) {
|
||||||
JoeyConfigT config;
|
jlConfigT config;
|
||||||
SurfaceT *screen;
|
jlSurfaceT *screen;
|
||||||
|
|
||||||
config.codegenBytes = 8 * 1024;
|
config.codegenBytes = 8 * 1024;
|
||||||
config.maxSurfaces = 4;
|
config.maxSurfaces = 4;
|
||||||
config.audioBytes = 64UL * 1024;
|
config.audioBytes = 64UL * 1024;
|
||||||
config.assetBytes = 128UL * 1024;
|
|
||||||
|
|
||||||
if (!joeyInit(&config)) {
|
if (!jlInit(&config)) {
|
||||||
fprintf(stderr, "joeyInit failed: %s\n", joeyLastError());
|
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
screen = stageGet();
|
screen = jlStageGet();
|
||||||
if (screen == NULL) {
|
if (screen == NULL) {
|
||||||
fprintf(stderr, "stageGet returned NULL\n");
|
fprintf(stderr, "jlStageGet returned NULL\n");
|
||||||
joeyShutdown();
|
jlShutdown();
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildPalette(screen);
|
buildPalette(screen);
|
||||||
scbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
|
jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
|
||||||
initialPaint(screen);
|
initialPaint(screen);
|
||||||
joeyInputPoll();
|
jlInputPoll();
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
joeyInputPoll();
|
jlInputPoll();
|
||||||
if (joeyKeyPressed(KEY_ESCAPE)) {
|
if (jlKeyPressed(KEY_ESCAPE)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
updateStick(screen, JOYSTICK_0, STICK0_LEFT);
|
updateStick(screen, JOYSTICK_0, STICK0_LEFT);
|
||||||
updateStick(screen, JOYSTICK_1, STICK1_LEFT);
|
updateStick(screen, JOYSTICK_1, STICK1_LEFT);
|
||||||
}
|
}
|
||||||
|
|
||||||
joeyShutdown();
|
jlShutdown();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
// 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
|
// 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.
|
// it as if the corresponding key were down. Press ESC to quit.
|
||||||
|
|
@ -33,19 +33,19 @@
|
||||||
|
|
||||||
#define CELL_NONE ((int16_t)-1)
|
#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 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 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 drawCell(jlSurfaceT *screen, int16_t col, int16_t row, bool lit);
|
||||||
static void drawCursor(SurfaceT *screen, int16_t x, int16_t y);
|
static void drawCursor(jlSurfaceT *screen, int16_t x, int16_t y);
|
||||||
static void initialPaint(SurfaceT *screen);
|
static void initialPaint(jlSurfaceT *screen);
|
||||||
static void presentChangedCells(SurfaceT *screen, int16_t cursorCol, int16_t cursorRow);
|
static void presentChangedCells(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow);
|
||||||
static void updateCursor(SurfaceT *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
|
// 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
|
// resembles a real keyboard (top number row, then QWERTY rows, then a
|
||||||
// cluster of modifiers / arrows / function keys).
|
// 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_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_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 },
|
{ 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 int16_t gLastCursorRow = CELL_NONE;
|
||||||
|
|
||||||
|
|
||||||
static void buildPalette(SurfaceT *screen) {
|
static void buildPalette(jlSurfaceT *screen) {
|
||||||
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
|
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
|
||||||
uint16_t i;
|
uint16_t i;
|
||||||
|
|
||||||
|
|
@ -73,7 +73,7 @@ static void buildPalette(SurfaceT *screen) {
|
||||||
colors[COLOR_LIT] = 0x00F0; // bright green
|
colors[COLOR_LIT] = 0x00F0; // bright green
|
||||||
colors[COLOR_CURSOR] = 0x0FFF; // white
|
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) {
|
static bool cellTargetLit(int16_t col, int16_t row, int16_t cursorCol, int16_t cursorRow) {
|
||||||
JoeyKeyE key;
|
jlKeyE key;
|
||||||
|
|
||||||
key = gKeyGrid[row][col];
|
key = gKeyGrid[row][col];
|
||||||
if (key == KEY_NONE) {
|
if (key == KEY_NONE) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (joeyKeyDown(key)) {
|
if (jlKeyDown(key)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (col == cursorCol && row == cursorRow && joeyMouseDown(MOUSE_BUTTON_LEFT)) {
|
if (col == cursorCol && row == cursorRow && jlMouseDown(MOUSE_BUTTON_LEFT)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
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 x;
|
||||||
int16_t y;
|
int16_t y;
|
||||||
uint8_t color;
|
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));
|
x = (int16_t)(MARGIN_X + col * (CELL_W + GAP));
|
||||||
y = (int16_t)(MARGIN_Y + row * (CELL_H + GAP));
|
y = (int16_t)(MARGIN_Y + row * (CELL_H + GAP));
|
||||||
color = lit ? COLOR_LIT : COLOR_UNLIT;
|
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) {
|
static void drawCursor(jlSurfaceT *screen, int16_t x, int16_t y) {
|
||||||
fillRect(screen, x, y, CURSOR_W, CURSOR_H, COLOR_CURSOR);
|
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 col;
|
||||||
int16_t row;
|
int16_t row;
|
||||||
JoeyKeyE key;
|
jlKeyE key;
|
||||||
|
|
||||||
surfaceClear(screen, COLOR_BACKGROUND);
|
jlSurfaceClear(screen, COLOR_BACKGROUND);
|
||||||
for (row = 0; row < GRID_ROWS; row++) {
|
for (row = 0; row < GRID_ROWS; row++) {
|
||||||
for (col = 0; col < GRID_COLS; col++) {
|
for (col = 0; col < GRID_COLS; col++) {
|
||||||
key = gKeyGrid[row][col];
|
key = gKeyGrid[row][col];
|
||||||
|
|
@ -149,14 +149,14 @@ static void initialPaint(SurfaceT *screen) {
|
||||||
gCellLit[row][col] = false;
|
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 col;
|
||||||
int16_t row;
|
int16_t row;
|
||||||
JoeyKeyE key;
|
jlKeyE key;
|
||||||
bool lit;
|
bool lit;
|
||||||
|
|
||||||
for (row = 0; row < GRID_ROWS; row++) {
|
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]) {
|
if (lit == gCellLit[row][col]) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
/* drawCell marks the cell's rect dirty; stagePresent
|
/* drawCell marks the cell's rect dirty; jlStagePresent
|
||||||
* flushes that one band. */
|
* flushes that one band. */
|
||||||
drawCell(screen, col, row, lit);
|
drawCell(screen, col, row, lit);
|
||||||
stagePresent();
|
jlStagePresent();
|
||||||
gCellLit[row][col] = lit;
|
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
|
// stamp the new cursor at the current mouse position. Both rects are
|
||||||
// presented; if the cursor stayed inside the same cell only one rect
|
// presented; if the cursor stayed inside the same cell only one rect
|
||||||
// pair is touched, so steady-state cost is small.
|
// 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 mouseX;
|
||||||
int16_t mouseY;
|
int16_t mouseY;
|
||||||
|
|
||||||
mouseX = joeyMouseX();
|
mouseX = jlMouseX();
|
||||||
mouseY = joeyMouseY();
|
mouseY = jlMouseY();
|
||||||
|
|
||||||
if (gLastCursorX != mouseX || gLastCursorY != mouseY) {
|
if (gLastCursorX != mouseX || gLastCursorY != mouseY) {
|
||||||
if (gLastCursorCol != CELL_NONE) {
|
if (gLastCursorCol != CELL_NONE) {
|
||||||
drawCell(screen, gLastCursorCol, gLastCursorRow, gCellLit[gLastCursorRow][gLastCursorCol]);
|
drawCell(screen, gLastCursorCol, gLastCursorRow, gCellLit[gLastCursorRow][gLastCursorCol]);
|
||||||
} else if (gLastCursorX >= 0 && gLastCursorY >= 0) {
|
} else if (gLastCursorX >= 0 && gLastCursorY >= 0) {
|
||||||
// Old cursor was in a gap region. Stamp background over it.
|
// 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);
|
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). */
|
* flushes the union (cursor erase + cursor draw). */
|
||||||
stagePresent();
|
jlStagePresent();
|
||||||
|
|
||||||
gLastCursorX = mouseX;
|
gLastCursorX = mouseX;
|
||||||
gLastCursorY = mouseY;
|
gLastCursorY = mouseY;
|
||||||
|
|
@ -212,43 +212,42 @@ static void updateCursor(SurfaceT *screen, int16_t cursorCol, int16_t cursorRow)
|
||||||
|
|
||||||
|
|
||||||
int main(void) {
|
int main(void) {
|
||||||
JoeyConfigT config;
|
jlConfigT config;
|
||||||
SurfaceT *screen;
|
jlSurfaceT *screen;
|
||||||
int16_t cursorCol;
|
int16_t cursorCol;
|
||||||
int16_t cursorRow;
|
int16_t cursorRow;
|
||||||
|
|
||||||
config.codegenBytes = 8 * 1024;
|
config.codegenBytes = 8 * 1024;
|
||||||
config.maxSurfaces = 4;
|
config.maxSurfaces = 4;
|
||||||
config.audioBytes = 64UL * 1024;
|
config.audioBytes = 64UL * 1024;
|
||||||
config.assetBytes = 128UL * 1024;
|
|
||||||
|
|
||||||
if (!joeyInit(&config)) {
|
if (!jlInit(&config)) {
|
||||||
fprintf(stderr, "joeyInit failed: %s\n", joeyLastError());
|
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
screen = stageGet();
|
screen = jlStageGet();
|
||||||
if (screen == NULL) {
|
if (screen == NULL) {
|
||||||
fprintf(stderr, "stageGet returned NULL\n");
|
fprintf(stderr, "jlStageGet returned NULL\n");
|
||||||
joeyShutdown();
|
jlShutdown();
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildPalette(screen);
|
buildPalette(screen);
|
||||||
scbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
|
jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
|
||||||
initialPaint(screen);
|
initialPaint(screen);
|
||||||
joeyInputPoll();
|
jlInputPoll();
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
joeyInputPoll();
|
jlInputPoll();
|
||||||
if (joeyKeyPressed(KEY_ESCAPE)) {
|
if (jlKeyPressed(KEY_ESCAPE)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
cellAtPoint(joeyMouseX(), joeyMouseY(), &cursorCol, &cursorRow);
|
cellAtPoint(jlMouseX(), jlMouseY(), &cursorCol, &cursorRow);
|
||||||
presentChangedCells(screen, cursorCol, cursorRow);
|
presentChangedCells(screen, cursorCol, cursorRow);
|
||||||
updateCursor(screen, cursorCol, cursorRow);
|
updateCursor(screen, cursorCol, cursorRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
joeyShutdown();
|
jlShutdown();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// M1 deliverable: visible test pattern exercising surfaces, palettes,
|
// 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
|
// Screen is divided into 8 horizontal bands, each assigned its own
|
||||||
// palette. The pattern draws 16 vertical color-index stripes across
|
// palette. The pattern draws 16 vertical color-index stripes across
|
||||||
|
|
@ -16,50 +16,50 @@
|
||||||
#define STRIPE_COUNT 16
|
#define STRIPE_COUNT 16
|
||||||
#define STRIPE_WIDTH (SURFACE_WIDTH / STRIPE_COUNT)
|
#define STRIPE_WIDTH (SURFACE_WIDTH / STRIPE_COUNT)
|
||||||
|
|
||||||
static void buildPalettes(SurfaceT *screen);
|
static void buildPalettes(jlSurfaceT *screen);
|
||||||
static void buildScbs(SurfaceT *screen);
|
static void buildScbs(jlSurfaceT *screen);
|
||||||
static void drawStripes(SurfaceT *screen);
|
static void drawStripes(jlSurfaceT *screen);
|
||||||
static void makeGradient(uint16_t *out16, int redOn, int greenOn, int blueOn);
|
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];
|
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
|
||||||
|
|
||||||
// Palette 0: grayscale
|
// Palette 0: grayscale
|
||||||
makeGradient(colors, 1, 1, 1);
|
makeGradient(colors, 1, 1, 1);
|
||||||
paletteSet(screen, 0, colors);
|
jlPaletteSet(screen, 0, colors);
|
||||||
|
|
||||||
// Palette 1: red
|
// Palette 1: red
|
||||||
makeGradient(colors, 1, 0, 0);
|
makeGradient(colors, 1, 0, 0);
|
||||||
paletteSet(screen, 1, colors);
|
jlPaletteSet(screen, 1, colors);
|
||||||
|
|
||||||
// Palette 2: yellow
|
// Palette 2: yellow
|
||||||
makeGradient(colors, 1, 1, 0);
|
makeGradient(colors, 1, 1, 0);
|
||||||
paletteSet(screen, 2, colors);
|
jlPaletteSet(screen, 2, colors);
|
||||||
|
|
||||||
// Palette 3: green
|
// Palette 3: green
|
||||||
makeGradient(colors, 0, 1, 0);
|
makeGradient(colors, 0, 1, 0);
|
||||||
paletteSet(screen, 3, colors);
|
jlPaletteSet(screen, 3, colors);
|
||||||
|
|
||||||
// Palette 4: cyan
|
// Palette 4: cyan
|
||||||
makeGradient(colors, 0, 1, 1);
|
makeGradient(colors, 0, 1, 1);
|
||||||
paletteSet(screen, 4, colors);
|
jlPaletteSet(screen, 4, colors);
|
||||||
|
|
||||||
// Palette 5: blue
|
// Palette 5: blue
|
||||||
makeGradient(colors, 0, 0, 1);
|
makeGradient(colors, 0, 0, 1);
|
||||||
paletteSet(screen, 5, colors);
|
jlPaletteSet(screen, 5, colors);
|
||||||
|
|
||||||
// Palette 6: magenta
|
// Palette 6: magenta
|
||||||
makeGradient(colors, 1, 0, 1);
|
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)
|
// Palette 7: white-only (same as grayscale but serves as sanity check)
|
||||||
makeGradient(colors, 1, 1, 1);
|
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 band;
|
||||||
uint16_t first;
|
uint16_t first;
|
||||||
uint16_t last;
|
uint16_t last;
|
||||||
|
|
@ -67,19 +67,19 @@ static void buildScbs(SurfaceT *screen) {
|
||||||
for (band = 0; band < BAND_COUNT; band++) {
|
for (band = 0; band < BAND_COUNT; band++) {
|
||||||
first = (uint16_t)(band * BAND_HEIGHT);
|
first = (uint16_t)(band * BAND_HEIGHT);
|
||||||
last = (uint16_t)(first + BAND_HEIGHT - 1);
|
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;
|
uint8_t colorIndex;
|
||||||
int16_t x;
|
int16_t x;
|
||||||
|
|
||||||
surfaceClear(screen, 0);
|
jlSurfaceClear(screen, 0);
|
||||||
for (colorIndex = 0; colorIndex < STRIPE_COUNT; colorIndex++) {
|
for (colorIndex = 0; colorIndex < STRIPE_COUNT; colorIndex++) {
|
||||||
x = (int16_t)(colorIndex * STRIPE_WIDTH);
|
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) {
|
int main(void) {
|
||||||
JoeyConfigT config;
|
jlConfigT config;
|
||||||
SurfaceT *screen;
|
jlSurfaceT *screen;
|
||||||
|
|
||||||
config.codegenBytes = 8 * 1024;
|
config.codegenBytes = 8 * 1024;
|
||||||
config.maxSurfaces = 4;
|
config.maxSurfaces = 4;
|
||||||
config.audioBytes = 64UL * 1024;
|
config.audioBytes = 64UL * 1024;
|
||||||
config.assetBytes = 128UL * 1024;
|
|
||||||
|
|
||||||
if (!joeyInit(&config)) {
|
if (!jlInit(&config)) {
|
||||||
fprintf(stderr, "joeyInit failed: %s\n", joeyLastError());
|
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
screen = stageGet();
|
screen = jlStageGet();
|
||||||
if (screen == NULL) {
|
if (screen == NULL) {
|
||||||
fprintf(stderr, "stageGet returned NULL\n");
|
fprintf(stderr, "jlStageGet returned NULL\n");
|
||||||
joeyShutdown();
|
jlShutdown();
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildPalettes(screen);
|
buildPalettes(screen);
|
||||||
buildScbs(screen);
|
buildScbs(screen);
|
||||||
drawStripes(screen);
|
drawStripes(screen);
|
||||||
stagePresent();
|
jlStagePresent();
|
||||||
|
|
||||||
joeyWaitForAnyKey();
|
jlWaitForAnyKey();
|
||||||
|
|
||||||
joeyShutdown();
|
jlShutdown();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2134
examples/spacetaxi/MECHANICS.md
Normal file
2134
examples/spacetaxi/MECHANICS.md
Normal file
File diff suppressed because it is too large
Load diff
141
examples/spacetaxi/PLAN.md
Normal file
141
examples/spacetaxi/PLAN.md
Normal 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.
|
||||||
96
examples/spacetaxi/VERIFIED.md
Normal file
96
examples/spacetaxi/VERIFIED.md
Normal 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.
|
||||||
BIN
examples/spacetaxi/assets/font.png
Normal file
BIN
examples/spacetaxi/assets/font.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
269
examples/spacetaxi/assets/genPlaceholderArt.py
Normal file
269
examples/spacetaxi/assets/genPlaceholderArt.py
Normal 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()
|
||||||
92
examples/spacetaxi/assets/levels/format.md
Normal file
92
examples/spacetaxi/assets/levels/format.md
Normal 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.
|
||||||
142
examples/spacetaxi/assets/levels/title.txt
Normal file
142
examples/spacetaxi/assets/levels/title.txt
Normal 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
|
||||||
BIN
examples/spacetaxi/assets/sprites/sprites.png
Normal file
BIN
examples/spacetaxi/assets/sprites/sprites.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 437 B |
BIN
examples/spacetaxi/assets/tiles/tbank0.png
Normal file
BIN
examples/spacetaxi/assets/tiles/tbank0.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
BIN
examples/spacetaxi/assets/tiles/tbank1.png
Normal file
BIN
examples/spacetaxi/assets/tiles/tbank1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
examples/spacetaxi/assets/tiles/tbank2.png
Normal file
BIN
examples/spacetaxi/assets/tiles/tbank2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
496
examples/spacetaxi/extractFromDump.py
Normal file
496
examples/spacetaxi/extractFromDump.py
Normal 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()
|
||||||
BIN
examples/spacetaxi/generated/amiga/font.tbk
Normal file
BIN
examples/spacetaxi/generated/amiga/font.tbk
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/amiga/levels/level01.dat
Normal file
BIN
examples/spacetaxi/generated/amiga/levels/level01.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/amiga/levels/level02.dat
Normal file
BIN
examples/spacetaxi/generated/amiga/levels/level02.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/amiga/levels/level03.dat
Normal file
BIN
examples/spacetaxi/generated/amiga/levels/level03.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/amiga/levels/level04.dat
Normal file
BIN
examples/spacetaxi/generated/amiga/levels/level04.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/amiga/levels/level05.dat
Normal file
BIN
examples/spacetaxi/generated/amiga/levels/level05.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/amiga/levels/level06.dat
Normal file
BIN
examples/spacetaxi/generated/amiga/levels/level06.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/amiga/levels/level07.dat
Normal file
BIN
examples/spacetaxi/generated/amiga/levels/level07.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/amiga/levels/level08.dat
Normal file
BIN
examples/spacetaxi/generated/amiga/levels/level08.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/amiga/levels/level09.dat
Normal file
BIN
examples/spacetaxi/generated/amiga/levels/level09.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/amiga/levels/level10.dat
Normal file
BIN
examples/spacetaxi/generated/amiga/levels/level10.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/amiga/levels/level11.dat
Normal file
BIN
examples/spacetaxi/generated/amiga/levels/level11.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/amiga/levels/level12.dat
Normal file
BIN
examples/spacetaxi/generated/amiga/levels/level12.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/amiga/levels/level13.dat
Normal file
BIN
examples/spacetaxi/generated/amiga/levels/level13.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/amiga/levels/level14.dat
Normal file
BIN
examples/spacetaxi/generated/amiga/levels/level14.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/amiga/levels/level15.dat
Normal file
BIN
examples/spacetaxi/generated/amiga/levels/level15.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/amiga/levels/level16.dat
Normal file
BIN
examples/spacetaxi/generated/amiga/levels/level16.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/amiga/levels/level17.dat
Normal file
BIN
examples/spacetaxi/generated/amiga/levels/level17.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/amiga/levels/level18.dat
Normal file
BIN
examples/spacetaxi/generated/amiga/levels/level18.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/amiga/levels/level19.dat
Normal file
BIN
examples/spacetaxi/generated/amiga/levels/level19.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/amiga/levels/level20.dat
Normal file
BIN
examples/spacetaxi/generated/amiga/levels/level20.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/amiga/levels/level21.dat
Normal file
BIN
examples/spacetaxi/generated/amiga/levels/level21.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/amiga/levels/level22.dat
Normal file
BIN
examples/spacetaxi/generated/amiga/levels/level22.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/amiga/levels/level23.dat
Normal file
BIN
examples/spacetaxi/generated/amiga/levels/level23.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/amiga/levels/level24.dat
Normal file
BIN
examples/spacetaxi/generated/amiga/levels/level24.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/amiga/levels/title.dat
Normal file
BIN
examples/spacetaxi/generated/amiga/levels/title.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/amiga/sprites/sprites.spr
Normal file
BIN
examples/spacetaxi/generated/amiga/sprites/sprites.spr
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/amiga/tiles/tbank0.tbk
Normal file
BIN
examples/spacetaxi/generated/amiga/tiles/tbank0.tbk
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/amiga/tiles/tbank1.tbk
Normal file
BIN
examples/spacetaxi/generated/amiga/tiles/tbank1.tbk
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/amiga/tiles/tbank2.tbk
Normal file
BIN
examples/spacetaxi/generated/amiga/tiles/tbank2.tbk
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/atarist/font.tbk
Normal file
BIN
examples/spacetaxi/generated/atarist/font.tbk
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/atarist/levels/level01.dat
Normal file
BIN
examples/spacetaxi/generated/atarist/levels/level01.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/atarist/levels/level02.dat
Normal file
BIN
examples/spacetaxi/generated/atarist/levels/level02.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/atarist/levels/level03.dat
Normal file
BIN
examples/spacetaxi/generated/atarist/levels/level03.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/atarist/levels/level04.dat
Normal file
BIN
examples/spacetaxi/generated/atarist/levels/level04.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/atarist/levels/level05.dat
Normal file
BIN
examples/spacetaxi/generated/atarist/levels/level05.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/atarist/levels/level06.dat
Normal file
BIN
examples/spacetaxi/generated/atarist/levels/level06.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/atarist/levels/level07.dat
Normal file
BIN
examples/spacetaxi/generated/atarist/levels/level07.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/atarist/levels/level08.dat
Normal file
BIN
examples/spacetaxi/generated/atarist/levels/level08.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/atarist/levels/level09.dat
Normal file
BIN
examples/spacetaxi/generated/atarist/levels/level09.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/atarist/levels/level10.dat
Normal file
BIN
examples/spacetaxi/generated/atarist/levels/level10.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/atarist/levels/level11.dat
Normal file
BIN
examples/spacetaxi/generated/atarist/levels/level11.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/atarist/levels/level12.dat
Normal file
BIN
examples/spacetaxi/generated/atarist/levels/level12.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/atarist/levels/level13.dat
Normal file
BIN
examples/spacetaxi/generated/atarist/levels/level13.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/atarist/levels/level14.dat
Normal file
BIN
examples/spacetaxi/generated/atarist/levels/level14.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/atarist/levels/level15.dat
Normal file
BIN
examples/spacetaxi/generated/atarist/levels/level15.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/atarist/levels/level16.dat
Normal file
BIN
examples/spacetaxi/generated/atarist/levels/level16.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/atarist/levels/level17.dat
Normal file
BIN
examples/spacetaxi/generated/atarist/levels/level17.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/atarist/levels/level18.dat
Normal file
BIN
examples/spacetaxi/generated/atarist/levels/level18.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/atarist/levels/level19.dat
Normal file
BIN
examples/spacetaxi/generated/atarist/levels/level19.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/atarist/levels/level20.dat
Normal file
BIN
examples/spacetaxi/generated/atarist/levels/level20.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/atarist/levels/level21.dat
Normal file
BIN
examples/spacetaxi/generated/atarist/levels/level21.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/atarist/levels/level22.dat
Normal file
BIN
examples/spacetaxi/generated/atarist/levels/level22.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/atarist/levels/level23.dat
Normal file
BIN
examples/spacetaxi/generated/atarist/levels/level23.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/atarist/levels/level24.dat
Normal file
BIN
examples/spacetaxi/generated/atarist/levels/level24.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/atarist/levels/title.dat
Normal file
BIN
examples/spacetaxi/generated/atarist/levels/title.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/atarist/sprites/sprites.spr
Normal file
BIN
examples/spacetaxi/generated/atarist/sprites/sprites.spr
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/atarist/tiles/tbank0.tbk
Normal file
BIN
examples/spacetaxi/generated/atarist/tiles/tbank0.tbk
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/atarist/tiles/tbank1.tbk
Normal file
BIN
examples/spacetaxi/generated/atarist/tiles/tbank1.tbk
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/atarist/tiles/tbank2.tbk
Normal file
BIN
examples/spacetaxi/generated/atarist/tiles/tbank2.tbk
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/dos/font.tbk
Normal file
BIN
examples/spacetaxi/generated/dos/font.tbk
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/dos/levels/level01.dat
Normal file
BIN
examples/spacetaxi/generated/dos/levels/level01.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/dos/levels/level02.dat
Normal file
BIN
examples/spacetaxi/generated/dos/levels/level02.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/dos/levels/level03.dat
Normal file
BIN
examples/spacetaxi/generated/dos/levels/level03.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/dos/levels/level04.dat
Normal file
BIN
examples/spacetaxi/generated/dos/levels/level04.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/dos/levels/level05.dat
Normal file
BIN
examples/spacetaxi/generated/dos/levels/level05.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/dos/levels/level06.dat
Normal file
BIN
examples/spacetaxi/generated/dos/levels/level06.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/dos/levels/level07.dat
Normal file
BIN
examples/spacetaxi/generated/dos/levels/level07.dat
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue