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
|
||||
stuff/*
|
||||
docs/*
|
||||
|
||||
# AGI game data (Sierra/fan-made AGI games used for testing; never committed)
|
||||
examples/agi/gamedata/
|
||||
|
|
|
|||
218
AGI-SESSION-HANDOFF.md
Normal file
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) |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| surfaceClear | 28 | 2.54x | 1.11x | 638.57x |
|
||||
| paletteSet | 678 | 8.68x | 4.23x | 26.13x |
|
||||
| scbSetRange | 1005 | 3.66x | 1.86x | 11.16x |
|
||||
| drawPixel | 1755 | 1.85x | 1.01x | 9.70x |
|
||||
| drawLine H | 682 | 2.19x | 1.22x | 19.26x |
|
||||
| drawLine V | 90 | 1.67x | 1.11x | 115.30x |
|
||||
| drawLine diag | 35 | 1.14x | 1.06x | 261.49x |
|
||||
| drawRect 100x100 | 75 | 1.57x | 1.04x | 250.77x |
|
||||
| drawCircle r=16 | 232 | 1.21x | **0.65x** | 71.24x |
|
||||
| drawCircle r=80 | 56 | 1.16x | **0.61x** | 310.93x |
|
||||
| fillRect 16x16 | 450 | 1.24x | 1.04x | 39.38x |
|
||||
| fillRect 80x80 | 75 | **0.95x** | 1.28x | 206.73x |
|
||||
| fillRect 320x200 | 60 | **0.93x** | **0.43x** | 184.62x |
|
||||
| fillCircle r=40 | 38 | **0.97x** | 1.39x | 347.24x |
|
||||
| samplePixel | 1916 | 3.48x | 1.92x | 18.04x |
|
||||
| tileFill | 1252 | 1.73x | **0.93x** | 10.44x |
|
||||
| tileCopy | 997 | 1.80x | 1.02x | 16.96x |
|
||||
| tileCopyMasked | 498 | 1.76x | 1.08x | 28.55x |
|
||||
| tilePaste | 1106 | 1.94x | 1.09x | 13.53x |
|
||||
| tileSnap | 1473 | 2.26x | 1.28x | 9.44x |
|
||||
| spriteSaveUnder | 528 | 2.21x | 1.29x | 20.14x |
|
||||
| spriteDraw | 438 | 1.82x | 1.28x | 38.39x |
|
||||
| spriteRestoreUnder | 487 | 2.00x | 1.11x | 36.71x |
|
||||
| spriteSaveAndDraw | 277 | 1.82x | 1.06x | 72.79x |
|
||||
| stagePresent full | 42 | 6.31x | 1.40x | 373.83x |
|
||||
| joeyInputPoll | 273 | 13.90x | 4.19x | 57.15x |
|
||||
| joeyKeyDown | 3382 | 7.82x | 3.76x | 18.93x |
|
||||
| joeyKeyPressed | 3345 | 7.65x | 3.81x | 18.72x |
|
||||
| joeyMouseX | 4170 | 10.26x | 5.22x | 26.27x |
|
||||
| jlSurfaceClear | 28 | 2.54x | 1.11x | 638.57x |
|
||||
| jlPaletteSet | 678 | 8.68x | 4.23x | 26.13x |
|
||||
| jlScbSetRange | 1005 | 3.66x | 1.86x | 11.16x |
|
||||
| jlDrawPixel | 1755 | 1.85x | 1.01x | 9.70x |
|
||||
| jlDrawLine H | 682 | 2.19x | 1.22x | 19.26x |
|
||||
| jlDrawLine V | 90 | 1.67x | 1.11x | 115.30x |
|
||||
| jlDrawLine diag | 35 | 1.14x | 1.06x | 261.49x |
|
||||
| jlDrawRect 100x100 | 75 | 1.57x | 1.04x | 250.77x |
|
||||
| jlDrawCircle r=16 | 232 | 1.21x | **0.65x** | 71.24x |
|
||||
| jlDrawCircle r=80 | 56 | 1.16x | **0.61x** | 310.93x |
|
||||
| jlFillRect 16x16 | 450 | 1.24x | 1.04x | 39.38x |
|
||||
| jlFillRect 80x80 | 75 | **0.95x** | 1.28x | 206.73x |
|
||||
| jlFillRect 320x200 | 60 | **0.93x** | **0.43x** | 184.62x |
|
||||
| jlFillCircle r=40 | 38 | **0.97x** | 1.39x | 347.24x |
|
||||
| jlSamplePixel | 1916 | 3.48x | 1.92x | 18.04x |
|
||||
| jlTileFill | 1252 | 1.73x | **0.93x** | 10.44x |
|
||||
| jlTileCopy | 997 | 1.80x | 1.02x | 16.96x |
|
||||
| jlTileCopyMasked | 498 | 1.76x | 1.08x | 28.55x |
|
||||
| jlTilePaste | 1106 | 1.94x | 1.09x | 13.53x |
|
||||
| jlTileSnap | 1473 | 2.26x | 1.28x | 9.44x |
|
||||
| jlSpriteSaveUnder | 528 | 2.21x | 1.29x | 20.14x |
|
||||
| jlSpriteDraw | 438 | 1.82x | 1.28x | 38.39x |
|
||||
| jlSpriteRestoreUnder | 487 | 2.00x | 1.11x | 36.71x |
|
||||
| jlSpriteSaveAndDraw | 277 | 1.82x | 1.06x | 72.79x |
|
||||
| jlStagePresent full | 42 | 6.31x | 1.40x | 373.83x |
|
||||
| jlInputPoll | 273 | 13.90x | 4.19x | 57.15x |
|
||||
| jlKeyDown | 3382 | 7.82x | 3.76x | 18.93x |
|
||||
| jlKeyPressed | 3345 | 7.65x | 3.81x | 18.72x |
|
||||
| jlMouseX | 4170 | 10.26x | 5.22x | 26.27x |
|
||||
| joeyJoyConnected | 3378 | 7.76x | 3.71x | 18.95x |
|
||||
| joeyAudioFrameTick | 4106 | 7.99x | 3.04x | 12.37x |
|
||||
| joeyAudioIsPlayingMod | 3536 | 9.28x | 4.33x | 26.78x |
|
||||
| surfaceMarkDirtyRect (via fillRect 32x32) | 240 | 1.38x | 1.26x | 58.95x |
|
||||
| jlAudioFrameTick | 4106 | 7.99x | 3.04x | 12.37x |
|
||||
| jlAudioIsPlayingMod | 3536 | 9.28x | 4.33x | 26.78x |
|
||||
| surfaceMarkDirtyRect (via jlFillRect 32x32) | 240 | 1.38x | 1.26x | 58.95x |
|
||||
| c2p full screen | - | - | - | - |
|
||||
|
|
|
|||
274
README.md
274
README.md
|
|
@ -50,7 +50,7 @@ src/core/ portable library code
|
|||
src/codegen/ runtime sprite codegen (per-CPU emitters)
|
||||
src/port/<plat>/ per-platform HAL implementations
|
||||
src/shared68k/ assembly shared by Amiga and Atari ST
|
||||
tools/joeyasset/ sprite and tile asset converter
|
||||
tools/assetbake/ PNG -> native .tbk / .spr baker (Python)
|
||||
tools/joeymod/ Protracker .MOD converter (passthrough or .NTP)
|
||||
examples/ example programs
|
||||
toolchains/ self-contained cross-build tools
|
||||
|
|
@ -79,18 +79,17 @@ typedef struct {
|
|||
uint32_t codegenBytes; // runtime compiled-sprite cache size
|
||||
uint16_t maxSurfaces; // maximum concurrent surfaces
|
||||
uint32_t audioBytes; // audio sample / module RAM pool
|
||||
uint32_t assetBytes; // tileset / sprite / map RAM pool
|
||||
} JoeyConfigT;
|
||||
} jlConfigT;
|
||||
|
||||
bool joeyInit (const JoeyConfigT *config);
|
||||
void joeyShutdown (void);
|
||||
const char *joeyLastError (void);
|
||||
const char *joeyPlatformName (void);
|
||||
const char *joeyVersionString(void);
|
||||
bool jlInit (const jlConfigT *config);
|
||||
void jlShutdown (void);
|
||||
const char *jlLastError (void);
|
||||
const char *jlPlatformName (void);
|
||||
const char *jlVersionString(void);
|
||||
|
||||
void joeyWaitVBL (void); // block until next VBL
|
||||
uint16_t joeyFrameCount (void); // monotonic 16-bit frame counter
|
||||
uint16_t joeyFrameHz (void); // 50 / 60 / 70 depending on port
|
||||
void jlWaitVBL (void); // block until next VBL
|
||||
uint16_t jlFrameCount (void); // monotonic 16-bit frame counter
|
||||
uint16_t jlFrameHz (void); // 50 / 60 / 70 depending on port
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -111,19 +110,19 @@ hides the storage format.
|
|||
#define SURFACE_PALETTE_COUNT 16
|
||||
#define SURFACE_COLORS_PER_PALETTE 16
|
||||
|
||||
typedef struct SurfaceT SurfaceT; // opaque
|
||||
typedef struct jlSurfaceT jlSurfaceT; // opaque
|
||||
|
||||
SurfaceT *surfaceCreate (void);
|
||||
void surfaceDestroy(SurfaceT *s);
|
||||
SurfaceT *stageGet (void); // library back-buffer
|
||||
void surfaceCopy (SurfaceT *dst, const SurfaceT *src);
|
||||
jlSurfaceT *jlSurfaceCreate (void);
|
||||
void jlSurfaceDestroy(jlSurfaceT *s);
|
||||
jlSurfaceT *jlStageGet (void); // library back-buffer
|
||||
void jlSurfaceCopy (jlSurfaceT *dst, const jlSurfaceT *src);
|
||||
|
||||
bool surfaceSaveFile(const SurfaceT *src, const char *path);
|
||||
bool surfaceLoadFile(SurfaceT *dst, const char *path);
|
||||
uint32_t surfaceHash (const SurfaceT *s); // FNV-1a of logical pixels
|
||||
bool jlSurfaceSaveFile(const jlSurfaceT *src, const char *path);
|
||||
bool jlSurfaceLoadFile(jlSurfaceT *dst, const char *path);
|
||||
uint32_t jlSurfaceHash (const jlSurfaceT *s); // FNV-1a of logical pixels
|
||||
```
|
||||
|
||||
`surfaceSaveFile` writes the surface in **target-native** form. Files
|
||||
`jlSurfaceSaveFile` writes the surface in **target-native** form. Files
|
||||
are NOT cross-port portable; the asset pipeline handles conversion.
|
||||
|
||||
|
||||
|
|
@ -134,51 +133,48 @@ no-ops. Color 0 is plotted normally (use the masked variants if you
|
|||
need transparency).
|
||||
|
||||
```c
|
||||
void surfaceClear (SurfaceT *s, uint8_t color);
|
||||
void drawPixel (SurfaceT *s, int16_t x, int16_t y, uint8_t color);
|
||||
uint8_t samplePixel (const SurfaceT *s, int16_t x, int16_t y);
|
||||
void jlSurfaceClear (jlSurfaceT *s, uint8_t color);
|
||||
void jlDrawPixel (jlSurfaceT *s, int16_t x, int16_t y, uint8_t color);
|
||||
uint8_t jlSamplePixel (const jlSurfaceT *s, int16_t x, int16_t y);
|
||||
|
||||
void drawLine (SurfaceT *s, int16_t x0, int16_t y0,
|
||||
void jlDrawLine (jlSurfaceT *s, int16_t x0, int16_t y0,
|
||||
int16_t x1, int16_t y1, uint8_t color);
|
||||
void drawRect (SurfaceT *s, int16_t x, int16_t y,
|
||||
void jlDrawRect (jlSurfaceT *s, int16_t x, int16_t y,
|
||||
uint16_t w, uint16_t h, uint8_t color);
|
||||
void fillRect (SurfaceT *s, int16_t x, int16_t y,
|
||||
void jlFillRect (jlSurfaceT *s, int16_t x, int16_t y,
|
||||
uint16_t w, uint16_t h, uint8_t color);
|
||||
void drawCircle (SurfaceT *s, int16_t cx, int16_t cy,
|
||||
void jlDrawCircle (jlSurfaceT *s, int16_t cx, int16_t cy,
|
||||
uint16_t r, uint8_t color);
|
||||
void fillCircle (SurfaceT *s, int16_t cx, int16_t cy,
|
||||
void jlFillCircle (jlSurfaceT *s, int16_t cx, int16_t cy,
|
||||
uint16_t r, uint8_t color);
|
||||
|
||||
void floodFill (SurfaceT *s, int16_t x, int16_t y, uint8_t newColor);
|
||||
void floodFillBounded (SurfaceT *s, int16_t x, int16_t y,
|
||||
void jlFloodFill (jlSurfaceT *s, int16_t x, int16_t y, uint8_t newColor);
|
||||
void jlFloodFillBounded (jlSurfaceT *s, int16_t x, int16_t y,
|
||||
uint8_t newColor, uint8_t boundaryColor);
|
||||
|
||||
void surfaceBlit (SurfaceT *dst, const JoeyAssetT *src, int16_t x, int16_t y);
|
||||
void surfaceBlitMasked (SurfaceT *dst, const JoeyAssetT *src,
|
||||
int16_t x, int16_t y, uint8_t transparentIndex);
|
||||
```
|
||||
|
||||
|
||||
### Palette and SCB (`joey/palette.h`)
|
||||
|
||||
Colors are 12-bit `$0RGB`. Color 0 of every palette is forced to
|
||||
black on `paletteSet`. Each scanline picks one of the 16 palettes
|
||||
black on `jlPaletteSet`. Each scanline picks one of the 16 palettes
|
||||
via the SCB.
|
||||
|
||||
```c
|
||||
void paletteSet (SurfaceT *s, uint8_t paletteIndex, const uint16_t *colors16);
|
||||
void paletteGet (const SurfaceT *s, uint8_t paletteIndex, uint16_t *out16);
|
||||
void scbSet (SurfaceT *s, uint16_t line, uint8_t paletteIndex);
|
||||
void scbSetRange (SurfaceT *s, uint16_t firstLine, uint16_t lastLine,
|
||||
void jlPaletteSet (jlSurfaceT *s, uint8_t paletteIndex, const uint16_t *colors16);
|
||||
void jlPaletteGet (const jlSurfaceT *s, uint8_t paletteIndex, uint16_t *out16);
|
||||
void jlScbSet (jlSurfaceT *s, uint16_t line, uint8_t paletteIndex);
|
||||
void jlScbSetRange (jlSurfaceT *s, uint16_t firstLine, uint16_t lastLine,
|
||||
uint8_t paletteIndex);
|
||||
uint8_t scbGet (const SurfaceT *s, uint16_t line);
|
||||
uint8_t jlScbGet (const jlSurfaceT *s, uint16_t line);
|
||||
```
|
||||
|
||||
|
||||
### Tiles (`joey/tile.h`)
|
||||
|
||||
A "tile" is just an 8x8-aligned region of any surface. The API moves
|
||||
32-byte chunks between surfaces and provides a small `TileT` value
|
||||
32-byte chunks between surfaces and provides a small `jlTileT` value
|
||||
type so callers can stash a copy without allocating a scratch surface.
|
||||
|
||||
```c
|
||||
|
|
@ -189,19 +185,29 @@ type so callers can stash a copy without allocating a scratch surface.
|
|||
#define TILE_BLOCKS_PER_COL (SURFACE_HEIGHT / TILE_PIXELS_PER_SIDE) // 25
|
||||
#define TILE_NO_GLYPH ((uint16_t)0xFFFFu)
|
||||
|
||||
typedef struct TileT { uint8_t pixels[TILE_BYTES]; } TileT;
|
||||
typedef struct jlTileT { uint8_t pixels[TILE_BYTES]; } jlTileT;
|
||||
|
||||
void tileCopy (SurfaceT *dst, uint8_t dstBx, uint8_t dstBy,
|
||||
const SurfaceT *src, uint8_t srcBx, uint8_t srcBy);
|
||||
void tileCopyMasked (SurfaceT *dst, uint8_t dstBx, uint8_t dstBy,
|
||||
const SurfaceT *src, uint8_t srcBx, uint8_t srcBy,
|
||||
void jlTileCopy (jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy,
|
||||
const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy);
|
||||
void jlTileCopyMasked (jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy,
|
||||
const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy,
|
||||
uint8_t transparentIndex);
|
||||
void tileFill (SurfaceT *s, uint8_t bx, uint8_t by, uint8_t color);
|
||||
void tileSnap (const SurfaceT *src, uint8_t bx, uint8_t by, TileT *out);
|
||||
void tilePaste (SurfaceT *dst, uint8_t bx, uint8_t by, const TileT *in);
|
||||
void jlTileFill (jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t color);
|
||||
void jlTileSnap (const jlSurfaceT *src, uint8_t bx, uint8_t by, jlTileT *out);
|
||||
void jlTilePaste (jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in);
|
||||
void jlTilePasteMono (jlSurfaceT *dst, uint8_t bx, uint8_t by,
|
||||
const jlTileT *in, uint8_t fgColor, uint8_t bgColor);
|
||||
|
||||
void drawText (SurfaceT *dst, uint8_t bx, uint8_t by,
|
||||
const SurfaceT *fontSurface, const uint16_t *asciiMap,
|
||||
// Load up to maxTiles tiles from a baked .tbk file (per-target planar
|
||||
// bytes from tools/assetbake/assetbake.py --type tile --target ...).
|
||||
// Refuses files baked for the wrong target. outValid (optional) marks
|
||||
// each loaded slot; outPalette (optional) receives the embedded
|
||||
// 16-entry $0RGB palette if present.
|
||||
uint16_t jlTileBankLoad(const char *path, jlTileT *outTiles, uint16_t maxTiles,
|
||||
bool *outValid, uint16_t *outPalette);
|
||||
|
||||
void jlDrawText (jlSurfaceT *dst, uint8_t bx, uint8_t by,
|
||||
const jlSurfaceT *fontSurface, const uint16_t *asciiMap,
|
||||
const char *str);
|
||||
```
|
||||
|
||||
|
|
@ -214,78 +220,96 @@ bytes, tile-major 4bpp packed. Sprites can be runtime-compiled
|
|||
into per-shift code variants for fast draws.
|
||||
|
||||
```c
|
||||
typedef struct SpriteT SpriteT; // opaque
|
||||
typedef struct jlSpriteT jlSpriteT; // opaque
|
||||
|
||||
typedef struct {
|
||||
SpriteT *sprite;
|
||||
jlSpriteT *sprite;
|
||||
int16_t x, y;
|
||||
uint16_t width, height; // pixels
|
||||
uint8_t *bytes; // caller-owned save-under buffer
|
||||
uint16_t sizeBytes;
|
||||
} SpriteBackupT;
|
||||
} jlSpriteBackupT;
|
||||
|
||||
SpriteT *spriteCreate (const uint8_t *tileData,
|
||||
jlSpriteT *jlSpriteCreate (const uint8_t *tileData,
|
||||
uint8_t widthTiles, uint8_t heightTiles);
|
||||
SpriteT *spriteCreateFromSurface (const SurfaceT *src, int16_t x, int16_t y,
|
||||
jlSpriteT *jlSpriteCreateFromSurface (const jlSurfaceT *src, int16_t x, int16_t y,
|
||||
uint8_t widthTiles, uint8_t heightTiles);
|
||||
SpriteT *spriteLoadFile (const char *path);
|
||||
SpriteT *spriteFromCompiledMem (const uint8_t *data, uint32_t length);
|
||||
bool spriteSaveFile (SpriteT *sp, const char *path);
|
||||
void spriteDestroy (SpriteT *sp);
|
||||
void jlSpriteDestroy (jlSpriteT *sp);
|
||||
|
||||
bool spriteCompile (SpriteT *sp); // build per-shift fast path
|
||||
void spritePrewarm (SpriteT *sp); // hint: compile if not already
|
||||
// Load up to maxCels cels from a baked .spr file (cross-target chunky
|
||||
// 4bpp blob from tools/assetbake/assetbake.py --type sprite). Each
|
||||
// cel becomes a freshly-allocated jlSpriteT (release with
|
||||
// jlSpriteDestroy). outPalette (optional) receives the embedded
|
||||
// 16-entry $0RGB palette if present.
|
||||
uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels,
|
||||
uint16_t maxCels, uint16_t *outPalette);
|
||||
|
||||
void spriteDraw (SurfaceT *s, SpriteT *sp, int16_t x, int16_t y);
|
||||
void spriteSaveUnder (const SurfaceT *s, SpriteT *sp,
|
||||
int16_t x, int16_t y, SpriteBackupT *backup);
|
||||
void spriteRestoreUnder (SurfaceT *s, const SpriteBackupT *backup);
|
||||
void spriteSaveAndDraw (SurfaceT *s, SpriteT *sp, int16_t x, int16_t y,
|
||||
SpriteBackupT *backup);
|
||||
bool jlSpriteCompile (jlSpriteT *sp); // build per-shift fast path
|
||||
void jlSpritePrewarm (jlSpriteT *sp); // hint: compile if not already
|
||||
|
||||
void spriteCompact (void); // defrag the codegen arena
|
||||
uint32_t spriteCodegenBytesUsed (void);
|
||||
uint32_t spriteCodegenBytesTotal (void);
|
||||
void jlSpriteDraw (jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y);
|
||||
void jlSpriteSaveUnder (const jlSurfaceT *s, jlSpriteT *sp,
|
||||
int16_t x, int16_t y, jlSpriteBackupT *backup);
|
||||
void jlSpriteRestoreUnder (jlSurfaceT *s, const jlSpriteBackupT *backup);
|
||||
void jlSpriteSaveAndDraw (jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y,
|
||||
jlSpriteBackupT *backup);
|
||||
|
||||
void jlSpriteCompact (void); // defrag the codegen arena
|
||||
uint32_t jlSpriteCodegenBytesUsed (void);
|
||||
uint32_t jlSpriteCodegenBytesTotal (void);
|
||||
```
|
||||
|
||||
|
||||
### Assets (`joey/asset.h`)
|
||||
### Assets (baked at build time)
|
||||
|
||||
Small bitmap blits with optional embedded palette, in `.jas` format.
|
||||
Use embedded `const JoeyAssetT` for ship-with-binary art; use the
|
||||
loaders for on-disk assets.
|
||||
Asset PNGs live in each example's `assets/` directory and are baked
|
||||
to native binary blobs at build time by `tools/assetbake/assetbake.py`
|
||||
(Python+PIL). The runtime loads those blobs directly with no
|
||||
conversion -- there is no in-memory chunky-to-planar or palette
|
||||
re-indexing on the device.
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
uint16_t width;
|
||||
uint16_t height;
|
||||
bool hasPalette;
|
||||
uint16_t palette[16]; // valid only if hasPalette
|
||||
const uint8_t *pixels; // 4bpp packed, rowBytes = (width+1)/2
|
||||
} JoeyAssetT;
|
||||
Two blob formats:
|
||||
|
||||
* **`.tbk` (tile bank)** -- one or more 8x8 tiles in per-target
|
||||
planar layout (Amiga plane-major; Atari ST row-major-with-planes-
|
||||
per-row; DOS / IIgs chunky 4bpp). The loader `jlTileBankLoad`
|
||||
rejects files baked for the wrong target.
|
||||
* **`.spr` (sprite cel set)** -- one or more uniform-sized sprite
|
||||
cels in cross-target chunky 4bpp. The Phase 11 walker reads chunky
|
||||
and converts to planar at draw time, so the same blob serves every
|
||||
platform.
|
||||
|
||||
Both formats embed an optional 16-entry `$0RGB` palette.
|
||||
|
||||
JoeyAssetT *joeyAssetLoadFile (const char *path);
|
||||
JoeyAssetT *joeyAssetFromMem (const uint8_t *data, uint32_t length);
|
||||
void joeyAssetFree (JoeyAssetT *asset);
|
||||
void joeyAssetApplyPalette (SurfaceT *dst, uint8_t paletteIndex,
|
||||
const JoeyAssetT *asset);
|
||||
```
|
||||
tools/assetbake/assetbake.py --type tile --target {amiga|atarist|dos|iigs} in.png out.tbk
|
||||
tools/assetbake/assetbake.py --type sprite --cell WxH in.png out.spr
|
||||
```
|
||||
|
||||
The Makefiles (`make/{amiga,atarist,dos,iigs}.mk`) wire bake rules
|
||||
per target so `make <target>` produces baked blobs under
|
||||
`examples/<game>/generated/<target>/` and stages them into the
|
||||
runtime tree at `build/<target>/.../DATA/`.
|
||||
|
||||
For runtime-extracted content (sprites peeled out of a procedural
|
||||
or captured surface), use `jlSpriteCreateFromSurface` -- the
|
||||
on-the-fly path stays alongside the baked load API.
|
||||
|
||||
|
||||
### Present (`joey/present.h`)
|
||||
|
||||
```c
|
||||
void stagePresent(void);
|
||||
void jlStagePresent(void);
|
||||
```
|
||||
|
||||
Flips the dirty rows of the stage to the display, then clears dirty
|
||||
state. Drawing primitives mark dirty as a side effect, so calling
|
||||
`stagePresent` once at end-of-frame is enough.
|
||||
`jlStagePresent` once at end-of-frame is enough.
|
||||
|
||||
|
||||
### Input (`joey/input.h`)
|
||||
|
||||
Call `joeyInputPoll` once per frame, then query the state predicates.
|
||||
Call `jlInputPoll` once per frame, then query the state predicates.
|
||||
Edge predicates (`*Pressed`, `*Released`) fire only in the frame the
|
||||
transition happened.
|
||||
|
||||
|
|
@ -293,35 +317,35 @@ transition happened.
|
|||
typedef enum { /* KEY_NONE, KEY_A..KEY_Z, KEY_0..KEY_9, KEY_SPACE,
|
||||
KEY_ESCAPE, KEY_RETURN, KEY_TAB, KEY_BACKSPACE,
|
||||
KEY_UP/DOWN/LEFT/RIGHT, KEY_LSHIFT/RSHIFT/LCTRL/LALT,
|
||||
KEY_F1..KEY_F10, KEY_COUNT */ } JoeyKeyE;
|
||||
KEY_F1..KEY_F10, KEY_COUNT */ } jlKeyE;
|
||||
typedef enum { MOUSE_BUTTON_NONE, MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT,
|
||||
MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_COUNT } JoeyMouseButtonE;
|
||||
typedef enum { JOYSTICK_0, JOYSTICK_1, JOYSTICK_COUNT } JoeyJoystickE;
|
||||
typedef enum { JOY_BUTTON_0, JOY_BUTTON_1, JOY_BUTTON_COUNT } JoeyJoyButtonE;
|
||||
MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_COUNT } jlMouseButtonE;
|
||||
typedef enum { JOYSTICK_0, JOYSTICK_1, JOYSTICK_COUNT } jlJoystickE;
|
||||
typedef enum { JOY_BUTTON_0, JOY_BUTTON_1, JOY_BUTTON_COUNT } jlJoyButtonE;
|
||||
|
||||
#define JOYSTICK_AXIS_MAX 127
|
||||
#define JOYSTICK_AXIS_MIN (-127)
|
||||
|
||||
void joeyInputPoll (void);
|
||||
void joeyWaitForAnyKey (void);
|
||||
void jlInputPoll (void);
|
||||
void jlWaitForAnyKey (void);
|
||||
|
||||
bool joeyKeyDown (JoeyKeyE key);
|
||||
bool joeyKeyPressed (JoeyKeyE key);
|
||||
bool joeyKeyReleased (JoeyKeyE key);
|
||||
bool jlKeyDown (jlKeyE key);
|
||||
bool jlKeyPressed (jlKeyE key);
|
||||
bool jlKeyReleased (jlKeyE key);
|
||||
|
||||
int16_t joeyMouseX (void);
|
||||
int16_t joeyMouseY (void);
|
||||
bool joeyMouseDown (JoeyMouseButtonE b);
|
||||
bool joeyMousePressed (JoeyMouseButtonE b);
|
||||
bool joeyMouseReleased (JoeyMouseButtonE b);
|
||||
int16_t jlMouseX (void);
|
||||
int16_t jlMouseY (void);
|
||||
bool jlMouseDown (jlMouseButtonE b);
|
||||
bool jlMousePressed (jlMouseButtonE b);
|
||||
bool jlMouseReleased (jlMouseButtonE b);
|
||||
|
||||
bool joeyJoystickConnected(JoeyJoystickE js);
|
||||
int8_t joeyJoystickX (JoeyJoystickE js);
|
||||
int8_t joeyJoystickY (JoeyJoystickE js);
|
||||
bool joeyJoyDown (JoeyJoystickE js, JoeyJoyButtonE b);
|
||||
bool joeyJoyPressed (JoeyJoystickE js, JoeyJoyButtonE b);
|
||||
bool joeyJoyReleased (JoeyJoystickE js, JoeyJoyButtonE b);
|
||||
void joeyJoystickReset (JoeyJoystickE js, uint8_t deadZone);
|
||||
bool jlJoystickConnected(jlJoystickE js);
|
||||
int8_t jlJoystickX (jlJoystickE js);
|
||||
int8_t jlJoystickY (jlJoystickE js);
|
||||
bool jlJoyDown (jlJoystickE js, jlJoyButtonE b);
|
||||
bool jlJoyPressed (jlJoystickE js, jlJoyButtonE b);
|
||||
bool jlJoyReleased (jlJoystickE js, jlJoyButtonE b);
|
||||
void jlJoystickReset (jlJoystickE js, uint8_t deadZone);
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -330,38 +354,38 @@ void joeyJoystickReset (JoeyJoystickE js, uint8_t deadZone);
|
|||
4-channel Protracker-style music plus four one-shot SFX slots. Module
|
||||
data must be the platform-native form produced by `tools/joeymod`
|
||||
(`.mod` for Amiga/DOS/ST; `.ntp` for IIgs; `.amod` if you want
|
||||
loop=false on Amiga). A failed `joeyAudioInit` is non-fatal; the rest
|
||||
loop=false on Amiga). A failed `jlAudioInit` is non-fatal; the rest
|
||||
of the API stays callable as no-ops.
|
||||
|
||||
```c
|
||||
#define JOEY_AUDIO_SFX_SLOTS 4
|
||||
|
||||
bool joeyAudioInit (void);
|
||||
void joeyAudioShutdown (void);
|
||||
bool jlAudioInit (void);
|
||||
void jlAudioShutdown (void);
|
||||
|
||||
void joeyAudioPlayMod (const uint8_t *data, uint32_t length, bool loop);
|
||||
void joeyAudioStopMod (void);
|
||||
bool joeyAudioIsPlayingMod (void);
|
||||
void jlAudioPlayMod (const uint8_t *data, uint32_t length, bool loop);
|
||||
void jlAudioStopMod (void);
|
||||
bool jlAudioIsPlayingMod (void);
|
||||
|
||||
void joeyAudioPlaySfx (uint8_t slot, const uint8_t *sample,
|
||||
void jlAudioPlaySfx (uint8_t slot, const uint8_t *sample,
|
||||
uint32_t length, uint16_t rateHz);
|
||||
void joeyAudioStopSfx (uint8_t slot);
|
||||
void jlAudioStopSfx (uint8_t slot);
|
||||
|
||||
void joeyAudioFrameTick (void);
|
||||
void jlAudioFrameTick (void);
|
||||
```
|
||||
|
||||
|
||||
### Debug logging (`joey/debug.h`)
|
||||
|
||||
Crash-tracing logger. Writes are buffered and durable across normal
|
||||
exit; call `joeyLogFlush` ahead of suspected hang points if you want
|
||||
exit; call `jlLogFlush` ahead of suspected hang points if you want
|
||||
a guaranteed last-line-on-disk.
|
||||
|
||||
```c
|
||||
void joeyLog (const char *msg);
|
||||
void joeyLogF (const char *fmt, ...);
|
||||
void joeyLogFlush(void);
|
||||
void joeyLogReset(void);
|
||||
void jlLog (const char *msg);
|
||||
void jlLogF (const char *fmt, ...);
|
||||
void jlLogFlush(void);
|
||||
void jlLogReset(void);
|
||||
```
|
||||
|
||||
Output goes to `joeylog.txt` in the program's working directory.
|
||||
|
|
|
|||
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
|
||||
// each platform's packaging step).
|
||||
//
|
||||
// On platforms where the audio HAL is still a stub, joeyAudioInit
|
||||
// On platforms where the audio HAL is still a stub, jlAudioInit
|
||||
// returns false, every audio call is a quiet no-op, and the demo runs
|
||||
// as a silent input loop -- you can confirm the build links and the
|
||||
// input plumbing still works without hearing anything.
|
||||
|
|
@ -81,7 +81,7 @@ static bool loadFile(const char *path, uint32_t maxLen, uint8_t **outBytes, uint
|
|||
}
|
||||
|
||||
|
||||
static void buildPalette(SurfaceT *screen) {
|
||||
static void buildPalette(jlSurfaceT *screen) {
|
||||
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
|
||||
uint16_t i;
|
||||
|
||||
|
|
@ -92,24 +92,24 @@ static void buildPalette(SurfaceT *screen) {
|
|||
colors[COLOR_HINT] = 0x0444;
|
||||
colors[COLOR_BAR] = 0x00F0;
|
||||
|
||||
paletteSet(screen, 0, colors);
|
||||
jlPaletteSet(screen, 0, colors);
|
||||
}
|
||||
|
||||
|
||||
// Visual feedback for "audio is running, but you cannot tell on a
|
||||
// stub HAL": pulse a horizontal bar between the hint color and the
|
||||
// active color whenever SFX has fired this frame.
|
||||
static void initialPaint(SurfaceT *screen, bool audioOk) {
|
||||
surfaceClear(screen, COLOR_BG);
|
||||
fillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H,
|
||||
static void initialPaint(jlSurfaceT *screen, bool audioOk) {
|
||||
jlSurfaceClear(screen, COLOR_BG);
|
||||
jlFillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H,
|
||||
audioOk ? COLOR_HINT : COLOR_BG);
|
||||
stagePresent();
|
||||
jlStagePresent();
|
||||
}
|
||||
|
||||
|
||||
int main(void) {
|
||||
JoeyConfigT config;
|
||||
SurfaceT *screen;
|
||||
jlConfigT config;
|
||||
jlSurfaceT *screen;
|
||||
bool audioOk;
|
||||
int16_t flashFrames;
|
||||
uint8_t *modBytes;
|
||||
|
|
@ -120,17 +120,16 @@ int main(void) {
|
|||
config.codegenBytes = 8 * 1024;
|
||||
config.maxSurfaces = 4;
|
||||
config.audioBytes = 64UL * 1024;
|
||||
config.assetBytes = 128UL * 1024;
|
||||
|
||||
if (!joeyInit(&config)) {
|
||||
fprintf(stderr, "joeyInit failed: %s\n", joeyLastError());
|
||||
if (!jlInit(&config)) {
|
||||
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
|
||||
return 1;
|
||||
}
|
||||
|
||||
screen = stageGet();
|
||||
screen = jlStageGet();
|
||||
if (screen == NULL) {
|
||||
fprintf(stderr, "stageGet returned NULL\n");
|
||||
joeyShutdown();
|
||||
fprintf(stderr, "jlStageGet returned NULL\n");
|
||||
jlShutdown();
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
|
@ -139,11 +138,11 @@ int main(void) {
|
|||
modLen = 0;
|
||||
sfxLen = 0;
|
||||
|
||||
audioOk = joeyAudioInit();
|
||||
audioOk = jlAudioInit();
|
||||
if (audioOk) {
|
||||
if (loadFile(TEST_MOD_PATH, 64UL * 1024UL, &modBytes, &modLen)) {
|
||||
joeyAudioPlayMod(modBytes, modLen, true);
|
||||
// joeyAudioPlayMod copies the bytes into the engine's own
|
||||
jlAudioPlayMod(modBytes, modLen, true);
|
||||
// jlAudioPlayMod copies the bytes into the engine's own
|
||||
// buffer; safe to release ours immediately.
|
||||
free(modBytes);
|
||||
modBytes = NULL;
|
||||
|
|
@ -152,40 +151,40 @@ int main(void) {
|
|||
}
|
||||
|
||||
buildPalette(screen);
|
||||
scbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
|
||||
jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
|
||||
initialPaint(screen, audioOk);
|
||||
|
||||
flashFrames = 0;
|
||||
for (;;) {
|
||||
joeyWaitVBL();
|
||||
joeyInputPoll();
|
||||
joeyAudioFrameTick();
|
||||
if (joeyKeyPressed(KEY_ESCAPE)) {
|
||||
jlWaitVBL();
|
||||
jlInputPoll();
|
||||
jlAudioFrameTick();
|
||||
if (jlKeyPressed(KEY_ESCAPE)) {
|
||||
break;
|
||||
}
|
||||
if (joeyKeyPressed(KEY_SPACE) && sfxBytes != NULL) {
|
||||
joeyAudioPlaySfx(SFX_SLOT, sfxBytes, sfxLen, SFX_RATE_HZ);
|
||||
if (jlKeyPressed(KEY_SPACE) && sfxBytes != NULL) {
|
||||
jlAudioPlaySfx(SFX_SLOT, sfxBytes, sfxLen, SFX_RATE_HZ);
|
||||
flashFrames = 8;
|
||||
}
|
||||
|
||||
if (flashFrames > 0) {
|
||||
fillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H, COLOR_BAR);
|
||||
stagePresent();
|
||||
jlFillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H, COLOR_BAR);
|
||||
jlStagePresent();
|
||||
flashFrames--;
|
||||
if (flashFrames == 0) {
|
||||
fillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H, COLOR_HINT);
|
||||
stagePresent();
|
||||
jlFillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H, COLOR_HINT);
|
||||
jlStagePresent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (audioOk) {
|
||||
joeyAudioStopMod();
|
||||
joeyAudioShutdown();
|
||||
jlAudioStopMod();
|
||||
jlAudioShutdown();
|
||||
}
|
||||
if (sfxBytes != NULL) {
|
||||
free(sfxBytes);
|
||||
}
|
||||
joeyShutdown();
|
||||
jlShutdown();
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@
|
|||
// a clear visual signal that the underlying inner loops (C or ASM)
|
||||
// produced the expected pixel pattern.
|
||||
//
|
||||
// TL: drawPixel + drawLine (8-octant fan from cell center; pixel
|
||||
// TL: jlDrawPixel + jlDrawLine (8-octant fan from cell center; pixel
|
||||
// row of all 16 colors along the cell's bottom edge).
|
||||
// TR: drawRect + fillRect (concentric outlines + filled blocks at
|
||||
// TR: jlDrawRect + jlFillRect (concentric outlines + filled blocks at
|
||||
// deliberately odd x / odd width to catch nibble-edge bugs).
|
||||
// BL: drawCircle + fillCircle (concentric outlines + a small filled
|
||||
// BL: jlDrawCircle + jlFillCircle (concentric outlines + a small filled
|
||||
// disk at center).
|
||||
// BR: tileCopy / tileCopyMasked / tileSnap+tilePaste / floodFill.
|
||||
// BR: jlTileCopy / jlTileCopyMasked / jlTileSnap+jlTilePaste / jlFloodFill.
|
||||
//
|
||||
// Holds the frame until the user presses ESC / RETURN / SPACE.
|
||||
|
||||
|
|
@ -32,17 +32,17 @@
|
|||
#define C_ORANGE 8
|
||||
#define C_GRAY 9
|
||||
|
||||
static void buildPalette(SurfaceT *screen);
|
||||
static void drawCellBorder(SurfaceT *screen, int16_t cx, int16_t cy);
|
||||
static void drawAllCellBorders(SurfaceT *screen);
|
||||
static void drawPrimitivesPixelLine(SurfaceT *screen);
|
||||
static void drawPrimitivesRect(SurfaceT *screen);
|
||||
static void drawPrimitivesCircle(SurfaceT *screen);
|
||||
static void drawPrimitivesTileFlood(SurfaceT *screen);
|
||||
static void buildPalette(jlSurfaceT *screen);
|
||||
static void drawCellBorder(jlSurfaceT *screen, int16_t cx, int16_t cy);
|
||||
static void drawAllCellBorders(jlSurfaceT *screen);
|
||||
static void drawPrimitivesPixelLine(jlSurfaceT *screen);
|
||||
static void drawPrimitivesRect(jlSurfaceT *screen);
|
||||
static void drawPrimitivesCircle(jlSurfaceT *screen);
|
||||
static void drawPrimitivesTileFlood(jlSurfaceT *screen);
|
||||
static void waitForKey(void);
|
||||
|
||||
|
||||
static void buildPalette(SurfaceT *screen) {
|
||||
static void buildPalette(jlSurfaceT *screen) {
|
||||
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
|
||||
|
||||
// 16 distinct $0RGB entries. Index 0 is forced to black anyway.
|
||||
|
|
@ -62,17 +62,17 @@ static void buildPalette(SurfaceT *screen) {
|
|||
colors[13] = 0x880;
|
||||
colors[14] = 0x088;
|
||||
colors[15] = 0x808;
|
||||
paletteSet(screen, 0, colors);
|
||||
scbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
|
||||
jlPaletteSet(screen, 0, colors);
|
||||
jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
|
||||
}
|
||||
|
||||
|
||||
static void drawCellBorder(SurfaceT *screen, int16_t cx, int16_t cy) {
|
||||
drawRect(screen, cx, cy, CELL_W, CELL_H, C_BORDER);
|
||||
static void drawCellBorder(jlSurfaceT *screen, int16_t cx, int16_t cy) {
|
||||
jlDrawRect(screen, cx, cy, CELL_W, CELL_H, C_BORDER);
|
||||
}
|
||||
|
||||
|
||||
static void drawAllCellBorders(SurfaceT *screen) {
|
||||
static void drawAllCellBorders(jlSurfaceT *screen) {
|
||||
drawCellBorder(screen, 0, 0);
|
||||
drawCellBorder(screen, CELL_W, 0);
|
||||
drawCellBorder(screen, 0, CELL_H);
|
||||
|
|
@ -80,15 +80,15 @@ static void drawAllCellBorders(SurfaceT *screen) {
|
|||
}
|
||||
|
||||
|
||||
// Top-left cell: drawPixel + drawLine.
|
||||
// Top-left cell: jlDrawPixel + jlDrawLine.
|
||||
//
|
||||
// 8 lines fan out from the cell center (80, 50). Four are diagonal
|
||||
// (the new ASM Bresenham path) and four are axis-aligned (drawLine
|
||||
// routes those to fillRect). A horizontal row of 14 pixels along the
|
||||
// cell's bottom verifies drawPixel: each pixel uses a different color
|
||||
// (the new ASM Bresenham path) and four are axis-aligned (jlDrawLine
|
||||
// routes those to jlFillRect). A horizontal row of 14 pixels along the
|
||||
// cell's bottom verifies jlDrawPixel: each pixel uses a different color
|
||||
// index so the leftmost ones at color 0 are invisible (they are bg)
|
||||
// and 1..13 progress through the palette.
|
||||
static void drawPrimitivesPixelLine(SurfaceT *screen) {
|
||||
static void drawPrimitivesPixelLine(jlSurfaceT *screen) {
|
||||
int16_t cx;
|
||||
int16_t cy;
|
||||
int16_t i;
|
||||
|
|
@ -96,180 +96,179 @@ static void drawPrimitivesPixelLine(SurfaceT *screen) {
|
|||
cx = CELL_W / 2; // 80
|
||||
cy = CELL_H / 2; // 50
|
||||
|
||||
drawLine(screen, cx, cy, cx + 70, cy, C_RED); // E (horizontal)
|
||||
drawLine(screen, cx, cy, cx + 60, cy - 40, C_GREEN); // NE (diagonal)
|
||||
drawLine(screen, cx, cy, cx, cy - 45, C_BLUE); // N (vertical)
|
||||
drawLine(screen, cx, cy, cx - 60, cy - 40, C_YELLOW); // NW
|
||||
drawLine(screen, cx, cy, cx - 70, cy, C_CYAN); // W
|
||||
drawLine(screen, cx, cy, cx - 60, cy + 40, C_MAGENTA); // SW
|
||||
drawLine(screen, cx, cy, cx, cy + 45, C_ORANGE); // S
|
||||
drawLine(screen, cx, cy, cx + 60, cy + 40, C_GRAY); // SE
|
||||
jlDrawLine(screen, cx, cy, cx + 70, cy, C_RED); // E (horizontal)
|
||||
jlDrawLine(screen, cx, cy, cx + 60, cy - 40, C_GREEN); // NE (diagonal)
|
||||
jlDrawLine(screen, cx, cy, cx, cy - 45, C_BLUE); // N (vertical)
|
||||
jlDrawLine(screen, cx, cy, cx - 60, cy - 40, C_YELLOW); // NW
|
||||
jlDrawLine(screen, cx, cy, cx - 70, cy, C_CYAN); // W
|
||||
jlDrawLine(screen, cx, cy, cx - 60, cy + 40, C_MAGENTA); // SW
|
||||
jlDrawLine(screen, cx, cy, cx, cy + 45, C_ORANGE); // S
|
||||
jlDrawLine(screen, cx, cy, cx + 60, cy + 40, C_GRAY); // SE
|
||||
|
||||
// Pixel row: 14 single-pixel writes at consecutive x to exercise
|
||||
// both odd and even nibble paths.
|
||||
for (i = 0; i < 14; i++) {
|
||||
drawPixel(screen, (int16_t)(10 + i * 10), (int16_t)(CELL_H - 6),
|
||||
jlDrawPixel(screen, (int16_t)(10 + i * 10), (int16_t)(CELL_H - 6),
|
||||
(uint8_t)((i + 1) & 0x0F));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Top-right cell: drawRect + fillRect.
|
||||
// Top-right cell: jlDrawRect + jlFillRect.
|
||||
//
|
||||
// Four nested rectangles with deliberately odd x/y/w/h to exercise
|
||||
// the partial-byte (nibble) edge handling in halFastFillRect. The
|
||||
// outermost is filled, the next outline-only, then filled with odd
|
||||
// width, then a 1-pixel-wide vertical bar (drawRect collapses to a
|
||||
// line via fillRect's 1-wide path).
|
||||
static void drawPrimitivesRect(SurfaceT *screen) {
|
||||
// width, then a 1-pixel-wide vertical bar (jlDrawRect collapses to a
|
||||
// line via jlFillRect's 1-wide path).
|
||||
static void drawPrimitivesRect(jlSurfaceT *screen) {
|
||||
int16_t ox;
|
||||
|
||||
ox = CELL_W; // cell origin x
|
||||
|
||||
// Outer fill, even-aligned.
|
||||
fillRect(screen, ox + 8, 8, 144, 84, C_RED);
|
||||
jlFillRect(screen, ox + 8, 8, 144, 84, C_RED);
|
||||
// Inner outline, odd x to test partial-nibble edges.
|
||||
drawRect(screen, ox + 17, 17, 124, 64, C_YELLOW);
|
||||
jlDrawRect(screen, ox + 17, 17, 124, 64, C_YELLOW);
|
||||
// Odd width fill, odd x.
|
||||
fillRect(screen, ox + 25, 25, 35, 48, C_GREEN);
|
||||
// 1-pixel vertical bar (degenerate rect through fillRect 1-wide path).
|
||||
fillRect(screen, ox + 100, 25, 1, 48, C_BORDER);
|
||||
jlFillRect(screen, ox + 25, 25, 35, 48, C_GREEN);
|
||||
// 1-pixel vertical bar (degenerate rect through jlFillRect 1-wide path).
|
||||
jlFillRect(screen, ox + 100, 25, 1, 48, C_BORDER);
|
||||
// Odd-x odd-w narrow bar to specifically hit hasLeading + hasTrailing
|
||||
// in halFastFillRect.
|
||||
fillRect(screen, ox + 75, 25, 7, 48, C_CYAN);
|
||||
jlFillRect(screen, ox + 75, 25, 7, 48, C_CYAN);
|
||||
}
|
||||
|
||||
|
||||
// Bottom-left cell: drawCircle + fillCircle.
|
||||
// Bottom-left cell: jlDrawCircle + jlFillCircle.
|
||||
//
|
||||
// Concentric outlines at decreasing radii, alternating colors, plus a
|
||||
// small filled disk at the center. Center is at the cell midpoint.
|
||||
static void drawPrimitivesCircle(SurfaceT *screen) {
|
||||
static void drawPrimitivesCircle(jlSurfaceT *screen) {
|
||||
int16_t cx;
|
||||
int16_t cy;
|
||||
|
||||
cx = CELL_W / 2;
|
||||
cy = CELL_H + CELL_H / 2;
|
||||
|
||||
drawCircle(screen, cx, cy, 45, C_BORDER);
|
||||
drawCircle(screen, cx, cy, 35, C_GREEN);
|
||||
drawCircle(screen, cx, cy, 25, C_YELLOW);
|
||||
drawCircle(screen, cx, cy, 15, C_CYAN);
|
||||
fillCircle(screen, cx, cy, 8, C_MAGENTA);
|
||||
jlDrawCircle(screen, cx, cy, 45, C_BORDER);
|
||||
jlDrawCircle(screen, cx, cy, 35, C_GREEN);
|
||||
jlDrawCircle(screen, cx, cy, 25, C_YELLOW);
|
||||
jlDrawCircle(screen, cx, cy, 15, C_CYAN);
|
||||
jlFillCircle(screen, cx, cy, 8, C_MAGENTA);
|
||||
}
|
||||
|
||||
|
||||
// Bottom-right cell: tile + flood fill.
|
||||
//
|
||||
// Top portion: a 16x16 colored block, then tileSnap one of its 8x8
|
||||
// quadrants and tilePaste the captured tile to a neighbor block;
|
||||
// also tileCopy the same source quadrant to a third location to
|
||||
// exercise the full surface-to-surface path. Then a tileCopyMasked
|
||||
// Top portion: a 16x16 colored block, then jlTileSnap one of its 8x8
|
||||
// quadrants and jlTilePaste the captured tile to a neighbor block;
|
||||
// also jlTileCopy the same source quadrant to a third location to
|
||||
// exercise the full surface-to-surface path. Then a jlTileCopyMasked
|
||||
// case: paint a 2x1 (16x8) "stripe" containing a transparent color 0
|
||||
// pattern interleaved with color, paste it over a solid backdrop with
|
||||
// transparent=0; the backdrop should show through the transparent
|
||||
// nibbles.
|
||||
//
|
||||
// Bottom portion: drawRect outlines a closed region, floodFillBounded
|
||||
// Bottom portion: jlDrawRect outlines a closed region, jlFloodFillBounded
|
||||
// fills its interior with a different color, stopping at the outline.
|
||||
static void drawPrimitivesTileFlood(SurfaceT *screen) {
|
||||
static void drawPrimitivesTileFlood(jlSurfaceT *screen) {
|
||||
int16_t ox;
|
||||
int16_t oy;
|
||||
int16_t bx;
|
||||
int16_t by;
|
||||
int16_t i;
|
||||
int16_t px;
|
||||
TileT snapBuf;
|
||||
jlTileT snapBuf;
|
||||
|
||||
ox = CELL_W; // 160
|
||||
oy = CELL_H; // 100
|
||||
|
||||
// Source 16x16 block at (168, 108): a 4-quadrant pattern.
|
||||
fillRect(screen, ox + 8, oy + 8, 8, 8, C_RED);
|
||||
fillRect(screen, ox + 16, oy + 8, 8, 8, C_GREEN);
|
||||
fillRect(screen, ox + 8, oy + 16, 8, 8, C_BLUE);
|
||||
fillRect(screen, ox + 16, oy + 16, 8, 8, C_YELLOW);
|
||||
jlFillRect(screen, ox + 8, oy + 8, 8, 8, C_RED);
|
||||
jlFillRect(screen, ox + 16, oy + 8, 8, 8, C_GREEN);
|
||||
jlFillRect(screen, ox + 8, oy + 16, 8, 8, C_BLUE);
|
||||
jlFillRect(screen, ox + 16, oy + 16, 8, 8, C_YELLOW);
|
||||
|
||||
// tileSnap the top-left red quadrant (block bx=21, by=13) and
|
||||
// tilePaste it next to the 16x16 source as the 5th quadrant.
|
||||
// jlTileSnap the top-left red quadrant (block bx=21, by=13) and
|
||||
// jlTilePaste it next to the 16x16 source as the 5th quadrant.
|
||||
bx = (int16_t)((ox + 8) / 8);
|
||||
by = (int16_t)((oy + 8) / 8);
|
||||
tileSnap(screen, (uint8_t)bx, (uint8_t)by, &snapBuf);
|
||||
tilePaste(screen, (uint8_t)(bx + 4), (uint8_t)by, &snapBuf);
|
||||
jlTileSnap(screen, (uint8_t)bx, (uint8_t)by, &snapBuf);
|
||||
jlTilePaste(screen, (uint8_t)(bx + 4), (uint8_t)by, &snapBuf);
|
||||
|
||||
// tileCopy from the green quadrant onto a fresh location below.
|
||||
tileCopy(screen, (uint8_t)(bx + 4), (uint8_t)(by + 1),
|
||||
// jlTileCopy from the green quadrant onto a fresh location below.
|
||||
jlTileCopy(screen, (uint8_t)(bx + 4), (uint8_t)(by + 1),
|
||||
screen, (uint8_t)(bx + 1), (uint8_t)by);
|
||||
|
||||
// tileCopyMasked test: build a "transparent" striped pattern at
|
||||
// jlTileCopyMasked test: build a "transparent" striped pattern at
|
||||
// (208, 132). The tile's source has color 0 in alternating
|
||||
// nibbles. Paste it onto a solid orange backdrop so transparent
|
||||
// nibbles let the orange show through.
|
||||
fillRect(screen, ox + 80, oy + 32, 16, 8, C_ORANGE); // backdrop
|
||||
jlFillRect(screen, ox + 80, oy + 32, 16, 8, C_ORANGE); // backdrop
|
||||
// Build a vertical-stripe source at (240, 132): col-pixel = (px % 2 ? color : 0)
|
||||
for (i = 0; i < 8; i++) {
|
||||
for (px = 0; px < 16; px++) {
|
||||
drawPixel(screen, (int16_t)(ox + 112 + px), (int16_t)(oy + 32 + i),
|
||||
jlDrawPixel(screen, (int16_t)(ox + 112 + px), (int16_t)(oy + 32 + i),
|
||||
(uint8_t)((px & 1) ? C_MAGENTA : 0));
|
||||
}
|
||||
}
|
||||
// tileCopyMasked: source at block (ox+112)/8 = 34..35, by 16
|
||||
// jlTileCopyMasked: source at block (ox+112)/8 = 34..35, by 16
|
||||
// -> dst at backdrop block (ox+80)/8 = 30..31, by 16
|
||||
tileCopyMasked(screen, (uint8_t)((ox + 80) / 8), (uint8_t)((oy + 32) / 8),
|
||||
jlTileCopyMasked(screen, (uint8_t)((ox + 80) / 8), (uint8_t)((oy + 32) / 8),
|
||||
screen, (uint8_t)((ox + 112) / 8), (uint8_t)((oy + 32) / 8),
|
||||
0);
|
||||
|
||||
// Flood-fill region: a small bordered rectangle in the cell's
|
||||
// lower portion. Outline drawn in C_BORDER; floodFillBounded
|
||||
// lower portion. Outline drawn in C_BORDER; jlFloodFillBounded
|
||||
// from a point inside should fill with C_CYAN, stopping at the
|
||||
// border.
|
||||
drawRect(screen, ox + 16, oy + 60, 64, 32, C_BORDER);
|
||||
floodFillBounded(screen, (int16_t)(ox + 48), (int16_t)(oy + 76),
|
||||
jlDrawRect(screen, ox + 16, oy + 60, 64, 32, C_BORDER);
|
||||
jlFloodFillBounded(screen, (int16_t)(ox + 48), (int16_t)(oy + 76),
|
||||
C_CYAN, C_BORDER);
|
||||
|
||||
// Plain floodFill: solid block then re-fill to a new color.
|
||||
fillRect(screen, ox + 96, oy + 60, 48, 32, C_GREEN);
|
||||
floodFill(screen, (int16_t)(ox + 120), (int16_t)(oy + 76), C_GRAY);
|
||||
// Plain jlFloodFill: solid block then re-fill to a new color.
|
||||
jlFillRect(screen, ox + 96, oy + 60, 48, 32, C_GREEN);
|
||||
jlFloodFill(screen, (int16_t)(ox + 120), (int16_t)(oy + 76), C_GRAY);
|
||||
}
|
||||
|
||||
|
||||
static void waitForKey(void) {
|
||||
joeyWaitForAnyKey();
|
||||
jlWaitForAnyKey();
|
||||
}
|
||||
|
||||
|
||||
int main(void) {
|
||||
JoeyConfigT config;
|
||||
SurfaceT *screen;
|
||||
jlConfigT config;
|
||||
jlSurfaceT *screen;
|
||||
|
||||
config.codegenBytes = 8 * 1024;
|
||||
config.maxSurfaces = 4;
|
||||
config.audioBytes = 64UL * 1024;
|
||||
config.assetBytes = 128UL * 1024;
|
||||
|
||||
if (!joeyInit(&config)) {
|
||||
fprintf(stderr, "joeyInit failed: %s\n", joeyLastError());
|
||||
if (!jlInit(&config)) {
|
||||
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
|
||||
return 1;
|
||||
}
|
||||
|
||||
screen = stageGet();
|
||||
screen = jlStageGet();
|
||||
if (screen == NULL) {
|
||||
fprintf(stderr, "stageGet returned NULL\n");
|
||||
joeyShutdown();
|
||||
fprintf(stderr, "jlStageGet returned NULL\n");
|
||||
jlShutdown();
|
||||
return 1;
|
||||
}
|
||||
|
||||
buildPalette(screen);
|
||||
surfaceClear(screen, C_BG);
|
||||
jlSurfaceClear(screen, C_BG);
|
||||
drawAllCellBorders(screen);
|
||||
drawPrimitivesPixelLine(screen);
|
||||
drawPrimitivesRect(screen);
|
||||
drawPrimitivesCircle(screen);
|
||||
drawPrimitivesTileFlood(screen);
|
||||
stagePresent();
|
||||
jlStagePresent();
|
||||
|
||||
waitForKey();
|
||||
|
||||
joeyShutdown();
|
||||
jlShutdown();
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,22 +7,21 @@
|
|||
#include <joey/joey.h>
|
||||
|
||||
int main(void) {
|
||||
JoeyConfigT config;
|
||||
jlConfigT config;
|
||||
|
||||
config.codegenBytes = 8 * 1024;
|
||||
config.maxSurfaces = 4;
|
||||
config.audioBytes = 64UL * 1024;
|
||||
config.assetBytes = 128UL * 1024;
|
||||
|
||||
if (!joeyInit(&config)) {
|
||||
fprintf(stderr, "joeyInit failed: %s\n", joeyLastError());
|
||||
if (!jlInit(&config)) {
|
||||
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("JoeyLib %s\n", joeyVersionString());
|
||||
printf("Platform: %s\n", joeyPlatformName());
|
||||
printf("JoeyLib %s\n", jlVersionString());
|
||||
printf("Platform: %s\n", jlPlatformName());
|
||||
printf("Hello from JoeyLib.\n");
|
||||
|
||||
joeyShutdown();
|
||||
jlShutdown();
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,14 +39,14 @@
|
|||
#define STICK0_LEFT 24
|
||||
#define STICK1_LEFT 216
|
||||
|
||||
static void buildPalette(SurfaceT *screen);
|
||||
static void drawFrame(SurfaceT *screen, int16_t left);
|
||||
static void drawAndPresent(SurfaceT *screen, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t color);
|
||||
static void buildPalette(jlSurfaceT *screen);
|
||||
static void drawFrame(jlSurfaceT *screen, int16_t left);
|
||||
static void drawAndPresent(jlSurfaceT *screen, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t color);
|
||||
static int16_t dotXFor(int16_t left, int8_t ax);
|
||||
static int16_t dotYFor(int8_t ay);
|
||||
static int16_t buttonXFor(int16_t left, int idx);
|
||||
static void initialPaint(SurfaceT *screen);
|
||||
static void updateStick(SurfaceT *screen, JoeyJoystickE js, int16_t left);
|
||||
static void initialPaint(jlSurfaceT *screen);
|
||||
static void updateStick(jlSurfaceT *screen, jlJoystickE js, int16_t left);
|
||||
|
||||
typedef struct {
|
||||
int16_t dotX;
|
||||
|
|
@ -59,7 +59,7 @@ typedef struct {
|
|||
static StickViewT gView[JOYSTICK_COUNT];
|
||||
|
||||
|
||||
static void buildPalette(SurfaceT *screen) {
|
||||
static void buildPalette(jlSurfaceT *screen) {
|
||||
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
|
||||
uint16_t i;
|
||||
|
||||
|
|
@ -75,26 +75,26 @@ static void buildPalette(SurfaceT *screen) {
|
|||
colors[COLOR_CONNECTED] = 0x00F0;
|
||||
colors[COLOR_DISCONNECTED] = 0x0F00; // red
|
||||
|
||||
paletteSet(screen, 0, colors);
|
||||
jlPaletteSet(screen, 0, colors);
|
||||
}
|
||||
|
||||
|
||||
static void drawAndPresent(SurfaceT *screen, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t color) {
|
||||
/* fillRect marks the rect dirty; stagePresent flushes only that
|
||||
static void drawAndPresent(jlSurfaceT *screen, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t color) {
|
||||
/* jlFillRect marks the rect dirty; jlStagePresent flushes only that
|
||||
* dirty band. */
|
||||
fillRect(screen, x, y, (uint16_t)w, (uint16_t)h, color);
|
||||
stagePresent();
|
||||
jlFillRect(screen, x, y, (uint16_t)w, (uint16_t)h, color);
|
||||
jlStagePresent();
|
||||
}
|
||||
|
||||
|
||||
// Draw the static stick frame (just an outlined square -- four edges
|
||||
// with the inset filled background).
|
||||
static void drawFrame(SurfaceT *screen, int16_t left) {
|
||||
static void drawFrame(jlSurfaceT *screen, int16_t left) {
|
||||
int16_t top;
|
||||
|
||||
top = STICK_TOP;
|
||||
fillRect(screen, left, top, FRAME_SIZE, FRAME_SIZE, COLOR_FRAME);
|
||||
fillRect(screen,
|
||||
jlFillRect(screen, left, top, FRAME_SIZE, FRAME_SIZE, COLOR_FRAME);
|
||||
jlFillRect(screen,
|
||||
(int16_t)(left + FRAME_INSET),
|
||||
(int16_t)(top + FRAME_INSET),
|
||||
(uint16_t)(FRAME_SIZE - 2 * FRAME_INSET),
|
||||
|
|
@ -134,10 +134,10 @@ static int16_t buttonXFor(int16_t left, int idx) {
|
|||
}
|
||||
|
||||
|
||||
static void initialPaint(SurfaceT *screen) {
|
||||
static void initialPaint(jlSurfaceT *screen) {
|
||||
int16_t i;
|
||||
|
||||
surfaceClear(screen, COLOR_BACKGROUND);
|
||||
jlSurfaceClear(screen, COLOR_BACKGROUND);
|
||||
for (i = 0; i < JOYSTICK_COUNT; i++) {
|
||||
int16_t left;
|
||||
left = (i == 0) ? STICK0_LEFT : STICK1_LEFT;
|
||||
|
|
@ -145,13 +145,13 @@ static void initialPaint(SurfaceT *screen) {
|
|||
gView[i].valid = false;
|
||||
gView[i].connected = false;
|
||||
}
|
||||
stagePresent();
|
||||
jlStagePresent();
|
||||
}
|
||||
|
||||
|
||||
// Compare current joystick state against gView and redraw / present
|
||||
// only the visual elements that changed.
|
||||
static void updateStick(SurfaceT *screen, JoeyJoystickE js, int16_t left) {
|
||||
static void updateStick(jlSurfaceT *screen, jlJoystickE js, int16_t left) {
|
||||
StickViewT *v;
|
||||
int8_t ax;
|
||||
int8_t ay;
|
||||
|
|
@ -164,13 +164,13 @@ static void updateStick(SurfaceT *screen, JoeyJoystickE js, int16_t left) {
|
|||
int16_t by;
|
||||
|
||||
v = &gView[js];
|
||||
connected = joeyJoystickConnected(js);
|
||||
ax = joeyJoystickX(js);
|
||||
ay = joeyJoystickY(js);
|
||||
connected = jlJoystickConnected(js);
|
||||
ax = jlJoystickX(js);
|
||||
ay = jlJoystickY(js);
|
||||
newDotX = dotXFor(left, ax);
|
||||
newDotY = dotYFor(ay);
|
||||
for (i = 0; i < JOY_BUTTON_COUNT; i++) {
|
||||
newBtn[i] = joeyJoyDown(js, (JoeyJoyButtonE)i);
|
||||
newBtn[i] = jlJoyDown(js, (jlJoyButtonE)i);
|
||||
}
|
||||
|
||||
if (!v->valid || v->connected != connected) {
|
||||
|
|
@ -214,40 +214,39 @@ static void updateStick(SurfaceT *screen, JoeyJoystickE js, int16_t left) {
|
|||
|
||||
|
||||
int main(void) {
|
||||
JoeyConfigT config;
|
||||
SurfaceT *screen;
|
||||
jlConfigT config;
|
||||
jlSurfaceT *screen;
|
||||
|
||||
config.codegenBytes = 8 * 1024;
|
||||
config.maxSurfaces = 4;
|
||||
config.audioBytes = 64UL * 1024;
|
||||
config.assetBytes = 128UL * 1024;
|
||||
|
||||
if (!joeyInit(&config)) {
|
||||
fprintf(stderr, "joeyInit failed: %s\n", joeyLastError());
|
||||
if (!jlInit(&config)) {
|
||||
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
|
||||
return 1;
|
||||
}
|
||||
|
||||
screen = stageGet();
|
||||
screen = jlStageGet();
|
||||
if (screen == NULL) {
|
||||
fprintf(stderr, "stageGet returned NULL\n");
|
||||
joeyShutdown();
|
||||
fprintf(stderr, "jlStageGet returned NULL\n");
|
||||
jlShutdown();
|
||||
return 1;
|
||||
}
|
||||
|
||||
buildPalette(screen);
|
||||
scbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
|
||||
jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
|
||||
initialPaint(screen);
|
||||
joeyInputPoll();
|
||||
jlInputPoll();
|
||||
|
||||
for (;;) {
|
||||
joeyInputPoll();
|
||||
if (joeyKeyPressed(KEY_ESCAPE)) {
|
||||
jlInputPoll();
|
||||
if (jlKeyPressed(KEY_ESCAPE)) {
|
||||
break;
|
||||
}
|
||||
updateStick(screen, JOYSTICK_0, STICK0_LEFT);
|
||||
updateStick(screen, JOYSTICK_1, STICK1_LEFT);
|
||||
}
|
||||
|
||||
joeyShutdown();
|
||||
jlShutdown();
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Visual keyboard + mouse demo: one square per JoeyKeyE, lit when its
|
||||
// Visual keyboard + mouse demo: one square per jlKeyE, lit when its
|
||||
// key is held, and a small pointer drawn at the live mouse position.
|
||||
// Holding the left mouse button while the pointer is over a cell lights
|
||||
// it as if the corresponding key were down. Press ESC to quit.
|
||||
|
|
@ -33,19 +33,19 @@
|
|||
|
||||
#define CELL_NONE ((int16_t)-1)
|
||||
|
||||
static void buildPalette(SurfaceT *screen);
|
||||
static void buildPalette(jlSurfaceT *screen);
|
||||
static void cellAtPoint(int16_t px, int16_t py, int16_t *outCol, int16_t *outRow);
|
||||
static bool cellTargetLit(int16_t col, int16_t row, int16_t cursorCol, int16_t cursorRow);
|
||||
static void drawCell(SurfaceT *screen, int16_t col, int16_t row, bool lit);
|
||||
static void drawCursor(SurfaceT *screen, int16_t x, int16_t y);
|
||||
static void initialPaint(SurfaceT *screen);
|
||||
static void presentChangedCells(SurfaceT *screen, int16_t cursorCol, int16_t cursorRow);
|
||||
static void updateCursor(SurfaceT *screen, int16_t cursorCol, int16_t cursorRow);
|
||||
static void drawCell(jlSurfaceT *screen, int16_t col, int16_t row, bool lit);
|
||||
static void drawCursor(jlSurfaceT *screen, int16_t x, int16_t y);
|
||||
static void initialPaint(jlSurfaceT *screen);
|
||||
static void presentChangedCells(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow);
|
||||
static void updateCursor(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow);
|
||||
|
||||
// Keys laid out row-by-row. KEY_NONE cells stay blank. Shape roughly
|
||||
// resembles a real keyboard (top number row, then QWERTY rows, then a
|
||||
// cluster of modifiers / arrows / function keys).
|
||||
static const JoeyKeyE gKeyGrid[GRID_ROWS][GRID_COLS] = {
|
||||
static const jlKeyE gKeyGrid[GRID_ROWS][GRID_COLS] = {
|
||||
{ KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9, KEY_0 },
|
||||
{ KEY_Q, KEY_W, KEY_E, KEY_R, KEY_T, KEY_Y, KEY_U, KEY_I, KEY_O, KEY_P },
|
||||
{ KEY_A, KEY_S, KEY_D, KEY_F, KEY_G, KEY_H, KEY_J, KEY_K, KEY_L, KEY_BACKSPACE },
|
||||
|
|
@ -61,7 +61,7 @@ static int16_t gLastCursorCol = CELL_NONE;
|
|||
static int16_t gLastCursorRow = CELL_NONE;
|
||||
|
||||
|
||||
static void buildPalette(SurfaceT *screen) {
|
||||
static void buildPalette(jlSurfaceT *screen) {
|
||||
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
|
||||
uint16_t i;
|
||||
|
||||
|
|
@ -73,7 +73,7 @@ static void buildPalette(SurfaceT *screen) {
|
|||
colors[COLOR_LIT] = 0x00F0; // bright green
|
||||
colors[COLOR_CURSOR] = 0x0FFF; // white
|
||||
|
||||
paletteSet(screen, 0, colors);
|
||||
jlPaletteSet(screen, 0, colors);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -100,23 +100,23 @@ static void cellAtPoint(int16_t px, int16_t py, int16_t *outCol, int16_t *outRow
|
|||
|
||||
|
||||
static bool cellTargetLit(int16_t col, int16_t row, int16_t cursorCol, int16_t cursorRow) {
|
||||
JoeyKeyE key;
|
||||
jlKeyE key;
|
||||
|
||||
key = gKeyGrid[row][col];
|
||||
if (key == KEY_NONE) {
|
||||
return false;
|
||||
}
|
||||
if (joeyKeyDown(key)) {
|
||||
if (jlKeyDown(key)) {
|
||||
return true;
|
||||
}
|
||||
if (col == cursorCol && row == cursorRow && joeyMouseDown(MOUSE_BUTTON_LEFT)) {
|
||||
if (col == cursorCol && row == cursorRow && jlMouseDown(MOUSE_BUTTON_LEFT)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
static void drawCell(SurfaceT *screen, int16_t col, int16_t row, bool lit) {
|
||||
static void drawCell(jlSurfaceT *screen, int16_t col, int16_t row, bool lit) {
|
||||
int16_t x;
|
||||
int16_t y;
|
||||
uint8_t color;
|
||||
|
|
@ -124,21 +124,21 @@ static void drawCell(SurfaceT *screen, int16_t col, int16_t row, bool lit) {
|
|||
x = (int16_t)(MARGIN_X + col * (CELL_W + GAP));
|
||||
y = (int16_t)(MARGIN_Y + row * (CELL_H + GAP));
|
||||
color = lit ? COLOR_LIT : COLOR_UNLIT;
|
||||
fillRect(screen, x, y, CELL_W, CELL_H, color);
|
||||
jlFillRect(screen, x, y, CELL_W, CELL_H, color);
|
||||
}
|
||||
|
||||
|
||||
static void drawCursor(SurfaceT *screen, int16_t x, int16_t y) {
|
||||
fillRect(screen, x, y, CURSOR_W, CURSOR_H, COLOR_CURSOR);
|
||||
static void drawCursor(jlSurfaceT *screen, int16_t x, int16_t y) {
|
||||
jlFillRect(screen, x, y, CURSOR_W, CURSOR_H, COLOR_CURSOR);
|
||||
}
|
||||
|
||||
|
||||
static void initialPaint(SurfaceT *screen) {
|
||||
static void initialPaint(jlSurfaceT *screen) {
|
||||
int16_t col;
|
||||
int16_t row;
|
||||
JoeyKeyE key;
|
||||
jlKeyE key;
|
||||
|
||||
surfaceClear(screen, COLOR_BACKGROUND);
|
||||
jlSurfaceClear(screen, COLOR_BACKGROUND);
|
||||
for (row = 0; row < GRID_ROWS; row++) {
|
||||
for (col = 0; col < GRID_COLS; col++) {
|
||||
key = gKeyGrid[row][col];
|
||||
|
|
@ -149,14 +149,14 @@ static void initialPaint(SurfaceT *screen) {
|
|||
gCellLit[row][col] = false;
|
||||
}
|
||||
}
|
||||
stagePresent();
|
||||
jlStagePresent();
|
||||
}
|
||||
|
||||
|
||||
static void presentChangedCells(SurfaceT *screen, int16_t cursorCol, int16_t cursorRow) {
|
||||
static void presentChangedCells(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow) {
|
||||
int16_t col;
|
||||
int16_t row;
|
||||
JoeyKeyE key;
|
||||
jlKeyE key;
|
||||
bool lit;
|
||||
|
||||
for (row = 0; row < GRID_ROWS; row++) {
|
||||
|
|
@ -169,10 +169,10 @@ static void presentChangedCells(SurfaceT *screen, int16_t cursorCol, int16_t cur
|
|||
if (lit == gCellLit[row][col]) {
|
||||
continue;
|
||||
}
|
||||
/* drawCell marks the cell's rect dirty; stagePresent
|
||||
/* drawCell marks the cell's rect dirty; jlStagePresent
|
||||
* flushes that one band. */
|
||||
drawCell(screen, col, row, lit);
|
||||
stagePresent();
|
||||
jlStagePresent();
|
||||
gCellLit[row][col] = lit;
|
||||
}
|
||||
}
|
||||
|
|
@ -183,26 +183,26 @@ static void presentChangedCells(SurfaceT *screen, int16_t cursorCol, int16_t cur
|
|||
// stamp the new cursor at the current mouse position. Both rects are
|
||||
// presented; if the cursor stayed inside the same cell only one rect
|
||||
// pair is touched, so steady-state cost is small.
|
||||
static void updateCursor(SurfaceT *screen, int16_t cursorCol, int16_t cursorRow) {
|
||||
static void updateCursor(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow) {
|
||||
int16_t mouseX;
|
||||
int16_t mouseY;
|
||||
|
||||
mouseX = joeyMouseX();
|
||||
mouseY = joeyMouseY();
|
||||
mouseX = jlMouseX();
|
||||
mouseY = jlMouseY();
|
||||
|
||||
if (gLastCursorX != mouseX || gLastCursorY != mouseY) {
|
||||
if (gLastCursorCol != CELL_NONE) {
|
||||
drawCell(screen, gLastCursorCol, gLastCursorRow, gCellLit[gLastCursorRow][gLastCursorCol]);
|
||||
} else if (gLastCursorX >= 0 && gLastCursorY >= 0) {
|
||||
// Old cursor was in a gap region. Stamp background over it.
|
||||
fillRect(screen, gLastCursorX, gLastCursorY, CURSOR_W, CURSOR_H, COLOR_BACKGROUND);
|
||||
jlFillRect(screen, gLastCursorX, gLastCursorY, CURSOR_W, CURSOR_H, COLOR_BACKGROUND);
|
||||
}
|
||||
}
|
||||
|
||||
drawCursor(screen, mouseX, mouseY);
|
||||
/* All draw calls above marked their rects dirty; one stagePresent
|
||||
/* All draw calls above marked their rects dirty; one jlStagePresent
|
||||
* flushes the union (cursor erase + cursor draw). */
|
||||
stagePresent();
|
||||
jlStagePresent();
|
||||
|
||||
gLastCursorX = mouseX;
|
||||
gLastCursorY = mouseY;
|
||||
|
|
@ -212,43 +212,42 @@ static void updateCursor(SurfaceT *screen, int16_t cursorCol, int16_t cursorRow)
|
|||
|
||||
|
||||
int main(void) {
|
||||
JoeyConfigT config;
|
||||
SurfaceT *screen;
|
||||
jlConfigT config;
|
||||
jlSurfaceT *screen;
|
||||
int16_t cursorCol;
|
||||
int16_t cursorRow;
|
||||
|
||||
config.codegenBytes = 8 * 1024;
|
||||
config.maxSurfaces = 4;
|
||||
config.audioBytes = 64UL * 1024;
|
||||
config.assetBytes = 128UL * 1024;
|
||||
|
||||
if (!joeyInit(&config)) {
|
||||
fprintf(stderr, "joeyInit failed: %s\n", joeyLastError());
|
||||
if (!jlInit(&config)) {
|
||||
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
|
||||
return 1;
|
||||
}
|
||||
|
||||
screen = stageGet();
|
||||
screen = jlStageGet();
|
||||
if (screen == NULL) {
|
||||
fprintf(stderr, "stageGet returned NULL\n");
|
||||
joeyShutdown();
|
||||
fprintf(stderr, "jlStageGet returned NULL\n");
|
||||
jlShutdown();
|
||||
return 1;
|
||||
}
|
||||
|
||||
buildPalette(screen);
|
||||
scbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
|
||||
jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
|
||||
initialPaint(screen);
|
||||
joeyInputPoll();
|
||||
jlInputPoll();
|
||||
|
||||
for (;;) {
|
||||
joeyInputPoll();
|
||||
if (joeyKeyPressed(KEY_ESCAPE)) {
|
||||
jlInputPoll();
|
||||
if (jlKeyPressed(KEY_ESCAPE)) {
|
||||
break;
|
||||
}
|
||||
cellAtPoint(joeyMouseX(), joeyMouseY(), &cursorCol, &cursorRow);
|
||||
cellAtPoint(jlMouseX(), jlMouseY(), &cursorCol, &cursorRow);
|
||||
presentChangedCells(screen, cursorCol, cursorRow);
|
||||
updateCursor(screen, cursorCol, cursorRow);
|
||||
}
|
||||
|
||||
joeyShutdown();
|
||||
jlShutdown();
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// M1 deliverable: visible test pattern exercising surfaces, palettes,
|
||||
// SCBs, fillRect, and present.
|
||||
// SCBs, jlFillRect, and present.
|
||||
//
|
||||
// Screen is divided into 8 horizontal bands, each assigned its own
|
||||
// palette. The pattern draws 16 vertical color-index stripes across
|
||||
|
|
@ -16,50 +16,50 @@
|
|||
#define STRIPE_COUNT 16
|
||||
#define STRIPE_WIDTH (SURFACE_WIDTH / STRIPE_COUNT)
|
||||
|
||||
static void buildPalettes(SurfaceT *screen);
|
||||
static void buildScbs(SurfaceT *screen);
|
||||
static void drawStripes(SurfaceT *screen);
|
||||
static void buildPalettes(jlSurfaceT *screen);
|
||||
static void buildScbs(jlSurfaceT *screen);
|
||||
static void drawStripes(jlSurfaceT *screen);
|
||||
static void makeGradient(uint16_t *out16, int redOn, int greenOn, int blueOn);
|
||||
|
||||
|
||||
static void buildPalettes(SurfaceT *screen) {
|
||||
static void buildPalettes(jlSurfaceT *screen) {
|
||||
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
|
||||
|
||||
// Palette 0: grayscale
|
||||
makeGradient(colors, 1, 1, 1);
|
||||
paletteSet(screen, 0, colors);
|
||||
jlPaletteSet(screen, 0, colors);
|
||||
|
||||
// Palette 1: red
|
||||
makeGradient(colors, 1, 0, 0);
|
||||
paletteSet(screen, 1, colors);
|
||||
jlPaletteSet(screen, 1, colors);
|
||||
|
||||
// Palette 2: yellow
|
||||
makeGradient(colors, 1, 1, 0);
|
||||
paletteSet(screen, 2, colors);
|
||||
jlPaletteSet(screen, 2, colors);
|
||||
|
||||
// Palette 3: green
|
||||
makeGradient(colors, 0, 1, 0);
|
||||
paletteSet(screen, 3, colors);
|
||||
jlPaletteSet(screen, 3, colors);
|
||||
|
||||
// Palette 4: cyan
|
||||
makeGradient(colors, 0, 1, 1);
|
||||
paletteSet(screen, 4, colors);
|
||||
jlPaletteSet(screen, 4, colors);
|
||||
|
||||
// Palette 5: blue
|
||||
makeGradient(colors, 0, 0, 1);
|
||||
paletteSet(screen, 5, colors);
|
||||
jlPaletteSet(screen, 5, colors);
|
||||
|
||||
// Palette 6: magenta
|
||||
makeGradient(colors, 1, 0, 1);
|
||||
paletteSet(screen, 6, colors);
|
||||
jlPaletteSet(screen, 6, colors);
|
||||
|
||||
// Palette 7: white-only (same as grayscale but serves as sanity check)
|
||||
makeGradient(colors, 1, 1, 1);
|
||||
paletteSet(screen, 7, colors);
|
||||
jlPaletteSet(screen, 7, colors);
|
||||
}
|
||||
|
||||
|
||||
static void buildScbs(SurfaceT *screen) {
|
||||
static void buildScbs(jlSurfaceT *screen) {
|
||||
uint16_t band;
|
||||
uint16_t first;
|
||||
uint16_t last;
|
||||
|
|
@ -67,19 +67,19 @@ static void buildScbs(SurfaceT *screen) {
|
|||
for (band = 0; band < BAND_COUNT; band++) {
|
||||
first = (uint16_t)(band * BAND_HEIGHT);
|
||||
last = (uint16_t)(first + BAND_HEIGHT - 1);
|
||||
scbSetRange(screen, first, last, (uint8_t)band);
|
||||
jlScbSetRange(screen, first, last, (uint8_t)band);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void drawStripes(SurfaceT *screen) {
|
||||
static void drawStripes(jlSurfaceT *screen) {
|
||||
uint8_t colorIndex;
|
||||
int16_t x;
|
||||
|
||||
surfaceClear(screen, 0);
|
||||
jlSurfaceClear(screen, 0);
|
||||
for (colorIndex = 0; colorIndex < STRIPE_COUNT; colorIndex++) {
|
||||
x = (int16_t)(colorIndex * STRIPE_WIDTH);
|
||||
fillRect(screen, x, 0, STRIPE_WIDTH, SURFACE_HEIGHT, colorIndex);
|
||||
jlFillRect(screen, x, 0, STRIPE_WIDTH, SURFACE_HEIGHT, colorIndex);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -102,33 +102,32 @@ static void makeGradient(uint16_t *out16, int redOn, int greenOn, int blueOn) {
|
|||
|
||||
|
||||
int main(void) {
|
||||
JoeyConfigT config;
|
||||
SurfaceT *screen;
|
||||
jlConfigT config;
|
||||
jlSurfaceT *screen;
|
||||
|
||||
config.codegenBytes = 8 * 1024;
|
||||
config.maxSurfaces = 4;
|
||||
config.audioBytes = 64UL * 1024;
|
||||
config.assetBytes = 128UL * 1024;
|
||||
|
||||
if (!joeyInit(&config)) {
|
||||
fprintf(stderr, "joeyInit failed: %s\n", joeyLastError());
|
||||
if (!jlInit(&config)) {
|
||||
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
|
||||
return 1;
|
||||
}
|
||||
|
||||
screen = stageGet();
|
||||
screen = jlStageGet();
|
||||
if (screen == NULL) {
|
||||
fprintf(stderr, "stageGet returned NULL\n");
|
||||
joeyShutdown();
|
||||
fprintf(stderr, "jlStageGet returned NULL\n");
|
||||
jlShutdown();
|
||||
return 1;
|
||||
}
|
||||
|
||||
buildPalettes(screen);
|
||||
buildScbs(screen);
|
||||
drawStripes(screen);
|
||||
stagePresent();
|
||||
jlStagePresent();
|
||||
|
||||
joeyWaitForAnyKey();
|
||||
jlWaitForAnyKey();
|
||||
|
||||
joeyShutdown();
|
||||
jlShutdown();
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
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