Optimized a bit better. Examples are a mess of broken crap.
This commit is contained in:
commit
6804baa6c1
112 changed files with 6451 additions and 0 deletions
2134
MECHANICS.md
Normal file
2134
MECHANICS.md
Normal file
File diff suppressed because it is too large
Load diff
141
PLAN.md
Normal file
141
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
VERIFIED.md
Normal file
96
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
assets/font.png
Normal file
BIN
assets/font.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
269
assets/genPlaceholderArt.py
Normal file
269
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
assets/levels/format.md
Normal file
92
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
assets/levels/title.txt
Normal file
142
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
assets/sprites/sprites.png
Normal file
BIN
assets/sprites/sprites.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 437 B |
BIN
assets/tiles/tbank0.png
Normal file
BIN
assets/tiles/tbank0.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
BIN
assets/tiles/tbank1.png
Normal file
BIN
assets/tiles/tbank1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/tiles/tbank2.png
Normal file
BIN
assets/tiles/tbank2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
496
extractFromDump.py
Normal file
496
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
generated/amiga/font.tbk
Normal file
BIN
generated/amiga/font.tbk
Normal file
Binary file not shown.
BIN
generated/amiga/levels/level01.dat
Normal file
BIN
generated/amiga/levels/level01.dat
Normal file
Binary file not shown.
BIN
generated/amiga/levels/level02.dat
Normal file
BIN
generated/amiga/levels/level02.dat
Normal file
Binary file not shown.
BIN
generated/amiga/levels/level03.dat
Normal file
BIN
generated/amiga/levels/level03.dat
Normal file
Binary file not shown.
BIN
generated/amiga/levels/level04.dat
Normal file
BIN
generated/amiga/levels/level04.dat
Normal file
Binary file not shown.
BIN
generated/amiga/levels/level05.dat
Normal file
BIN
generated/amiga/levels/level05.dat
Normal file
Binary file not shown.
BIN
generated/amiga/levels/level06.dat
Normal file
BIN
generated/amiga/levels/level06.dat
Normal file
Binary file not shown.
BIN
generated/amiga/levels/level07.dat
Normal file
BIN
generated/amiga/levels/level07.dat
Normal file
Binary file not shown.
BIN
generated/amiga/levels/level08.dat
Normal file
BIN
generated/amiga/levels/level08.dat
Normal file
Binary file not shown.
BIN
generated/amiga/levels/level09.dat
Normal file
BIN
generated/amiga/levels/level09.dat
Normal file
Binary file not shown.
BIN
generated/amiga/levels/level10.dat
Normal file
BIN
generated/amiga/levels/level10.dat
Normal file
Binary file not shown.
BIN
generated/amiga/levels/level11.dat
Normal file
BIN
generated/amiga/levels/level11.dat
Normal file
Binary file not shown.
BIN
generated/amiga/levels/level12.dat
Normal file
BIN
generated/amiga/levels/level12.dat
Normal file
Binary file not shown.
BIN
generated/amiga/levels/level13.dat
Normal file
BIN
generated/amiga/levels/level13.dat
Normal file
Binary file not shown.
BIN
generated/amiga/levels/level14.dat
Normal file
BIN
generated/amiga/levels/level14.dat
Normal file
Binary file not shown.
BIN
generated/amiga/levels/level15.dat
Normal file
BIN
generated/amiga/levels/level15.dat
Normal file
Binary file not shown.
BIN
generated/amiga/levels/level16.dat
Normal file
BIN
generated/amiga/levels/level16.dat
Normal file
Binary file not shown.
BIN
generated/amiga/levels/level17.dat
Normal file
BIN
generated/amiga/levels/level17.dat
Normal file
Binary file not shown.
BIN
generated/amiga/levels/level18.dat
Normal file
BIN
generated/amiga/levels/level18.dat
Normal file
Binary file not shown.
BIN
generated/amiga/levels/level19.dat
Normal file
BIN
generated/amiga/levels/level19.dat
Normal file
Binary file not shown.
BIN
generated/amiga/levels/level20.dat
Normal file
BIN
generated/amiga/levels/level20.dat
Normal file
Binary file not shown.
BIN
generated/amiga/levels/level21.dat
Normal file
BIN
generated/amiga/levels/level21.dat
Normal file
Binary file not shown.
BIN
generated/amiga/levels/level22.dat
Normal file
BIN
generated/amiga/levels/level22.dat
Normal file
Binary file not shown.
BIN
generated/amiga/levels/level23.dat
Normal file
BIN
generated/amiga/levels/level23.dat
Normal file
Binary file not shown.
BIN
generated/amiga/levels/level24.dat
Normal file
BIN
generated/amiga/levels/level24.dat
Normal file
Binary file not shown.
BIN
generated/amiga/levels/title.dat
Normal file
BIN
generated/amiga/levels/title.dat
Normal file
Binary file not shown.
BIN
generated/amiga/sprites/sprites.spr
Normal file
BIN
generated/amiga/sprites/sprites.spr
Normal file
Binary file not shown.
BIN
generated/amiga/tiles/tbank0.tbk
Normal file
BIN
generated/amiga/tiles/tbank0.tbk
Normal file
Binary file not shown.
BIN
generated/amiga/tiles/tbank1.tbk
Normal file
BIN
generated/amiga/tiles/tbank1.tbk
Normal file
Binary file not shown.
BIN
generated/amiga/tiles/tbank2.tbk
Normal file
BIN
generated/amiga/tiles/tbank2.tbk
Normal file
Binary file not shown.
BIN
generated/atarist/font.tbk
Normal file
BIN
generated/atarist/font.tbk
Normal file
Binary file not shown.
BIN
generated/atarist/levels/level01.dat
Normal file
BIN
generated/atarist/levels/level01.dat
Normal file
Binary file not shown.
BIN
generated/atarist/levels/level02.dat
Normal file
BIN
generated/atarist/levels/level02.dat
Normal file
Binary file not shown.
BIN
generated/atarist/levels/level03.dat
Normal file
BIN
generated/atarist/levels/level03.dat
Normal file
Binary file not shown.
BIN
generated/atarist/levels/level04.dat
Normal file
BIN
generated/atarist/levels/level04.dat
Normal file
Binary file not shown.
BIN
generated/atarist/levels/level05.dat
Normal file
BIN
generated/atarist/levels/level05.dat
Normal file
Binary file not shown.
BIN
generated/atarist/levels/level06.dat
Normal file
BIN
generated/atarist/levels/level06.dat
Normal file
Binary file not shown.
BIN
generated/atarist/levels/level07.dat
Normal file
BIN
generated/atarist/levels/level07.dat
Normal file
Binary file not shown.
BIN
generated/atarist/levels/level08.dat
Normal file
BIN
generated/atarist/levels/level08.dat
Normal file
Binary file not shown.
BIN
generated/atarist/levels/level09.dat
Normal file
BIN
generated/atarist/levels/level09.dat
Normal file
Binary file not shown.
BIN
generated/atarist/levels/level10.dat
Normal file
BIN
generated/atarist/levels/level10.dat
Normal file
Binary file not shown.
BIN
generated/atarist/levels/level11.dat
Normal file
BIN
generated/atarist/levels/level11.dat
Normal file
Binary file not shown.
BIN
generated/atarist/levels/level12.dat
Normal file
BIN
generated/atarist/levels/level12.dat
Normal file
Binary file not shown.
BIN
generated/atarist/levels/level13.dat
Normal file
BIN
generated/atarist/levels/level13.dat
Normal file
Binary file not shown.
BIN
generated/atarist/levels/level14.dat
Normal file
BIN
generated/atarist/levels/level14.dat
Normal file
Binary file not shown.
BIN
generated/atarist/levels/level15.dat
Normal file
BIN
generated/atarist/levels/level15.dat
Normal file
Binary file not shown.
BIN
generated/atarist/levels/level16.dat
Normal file
BIN
generated/atarist/levels/level16.dat
Normal file
Binary file not shown.
BIN
generated/atarist/levels/level17.dat
Normal file
BIN
generated/atarist/levels/level17.dat
Normal file
Binary file not shown.
BIN
generated/atarist/levels/level18.dat
Normal file
BIN
generated/atarist/levels/level18.dat
Normal file
Binary file not shown.
BIN
generated/atarist/levels/level19.dat
Normal file
BIN
generated/atarist/levels/level19.dat
Normal file
Binary file not shown.
BIN
generated/atarist/levels/level20.dat
Normal file
BIN
generated/atarist/levels/level20.dat
Normal file
Binary file not shown.
BIN
generated/atarist/levels/level21.dat
Normal file
BIN
generated/atarist/levels/level21.dat
Normal file
Binary file not shown.
BIN
generated/atarist/levels/level22.dat
Normal file
BIN
generated/atarist/levels/level22.dat
Normal file
Binary file not shown.
BIN
generated/atarist/levels/level23.dat
Normal file
BIN
generated/atarist/levels/level23.dat
Normal file
Binary file not shown.
BIN
generated/atarist/levels/level24.dat
Normal file
BIN
generated/atarist/levels/level24.dat
Normal file
Binary file not shown.
BIN
generated/atarist/levels/title.dat
Normal file
BIN
generated/atarist/levels/title.dat
Normal file
Binary file not shown.
BIN
generated/atarist/sprites/sprites.spr
Normal file
BIN
generated/atarist/sprites/sprites.spr
Normal file
Binary file not shown.
BIN
generated/atarist/tiles/tbank0.tbk
Normal file
BIN
generated/atarist/tiles/tbank0.tbk
Normal file
Binary file not shown.
BIN
generated/atarist/tiles/tbank1.tbk
Normal file
BIN
generated/atarist/tiles/tbank1.tbk
Normal file
Binary file not shown.
BIN
generated/atarist/tiles/tbank2.tbk
Normal file
BIN
generated/atarist/tiles/tbank2.tbk
Normal file
Binary file not shown.
BIN
generated/dos/font.tbk
Normal file
BIN
generated/dos/font.tbk
Normal file
Binary file not shown.
BIN
generated/dos/levels/level01.dat
Normal file
BIN
generated/dos/levels/level01.dat
Normal file
Binary file not shown.
BIN
generated/dos/levels/level02.dat
Normal file
BIN
generated/dos/levels/level02.dat
Normal file
Binary file not shown.
BIN
generated/dos/levels/level03.dat
Normal file
BIN
generated/dos/levels/level03.dat
Normal file
Binary file not shown.
BIN
generated/dos/levels/level04.dat
Normal file
BIN
generated/dos/levels/level04.dat
Normal file
Binary file not shown.
BIN
generated/dos/levels/level05.dat
Normal file
BIN
generated/dos/levels/level05.dat
Normal file
Binary file not shown.
BIN
generated/dos/levels/level06.dat
Normal file
BIN
generated/dos/levels/level06.dat
Normal file
Binary file not shown.
BIN
generated/dos/levels/level07.dat
Normal file
BIN
generated/dos/levels/level07.dat
Normal file
Binary file not shown.
BIN
generated/dos/levels/level08.dat
Normal file
BIN
generated/dos/levels/level08.dat
Normal file
Binary file not shown.
BIN
generated/dos/levels/level09.dat
Normal file
BIN
generated/dos/levels/level09.dat
Normal file
Binary file not shown.
BIN
generated/dos/levels/level10.dat
Normal file
BIN
generated/dos/levels/level10.dat
Normal file
Binary file not shown.
BIN
generated/dos/levels/level11.dat
Normal file
BIN
generated/dos/levels/level11.dat
Normal file
Binary file not shown.
BIN
generated/dos/levels/level12.dat
Normal file
BIN
generated/dos/levels/level12.dat
Normal file
Binary file not shown.
BIN
generated/dos/levels/level13.dat
Normal file
BIN
generated/dos/levels/level13.dat
Normal file
Binary file not shown.
BIN
generated/dos/levels/level14.dat
Normal file
BIN
generated/dos/levels/level14.dat
Normal file
Binary file not shown.
BIN
generated/dos/levels/level15.dat
Normal file
BIN
generated/dos/levels/level15.dat
Normal file
Binary file not shown.
BIN
generated/dos/levels/level16.dat
Normal file
BIN
generated/dos/levels/level16.dat
Normal file
Binary file not shown.
BIN
generated/dos/levels/level17.dat
Normal file
BIN
generated/dos/levels/level17.dat
Normal file
Binary file not shown.
BIN
generated/dos/levels/level18.dat
Normal file
BIN
generated/dos/levels/level18.dat
Normal file
Binary file not shown.
BIN
generated/dos/levels/level19.dat
Normal file
BIN
generated/dos/levels/level19.dat
Normal file
Binary file not shown.
BIN
generated/dos/levels/level20.dat
Normal file
BIN
generated/dos/levels/level20.dat
Normal file
Binary file not shown.
BIN
generated/dos/levels/level21.dat
Normal file
BIN
generated/dos/levels/level21.dat
Normal file
Binary file not shown.
BIN
generated/dos/levels/level22.dat
Normal file
BIN
generated/dos/levels/level22.dat
Normal file
Binary file not shown.
BIN
generated/dos/levels/level23.dat
Normal file
BIN
generated/dos/levels/level23.dat
Normal file
Binary file not shown.
BIN
generated/dos/levels/level24.dat
Normal file
BIN
generated/dos/levels/level24.dat
Normal file
Binary file not shown.
BIN
generated/dos/levels/title.dat
Normal file
BIN
generated/dos/levels/title.dat
Normal file
Binary file not shown.
BIN
generated/dos/sprites/sprites.spr
Normal file
BIN
generated/dos/sprites/sprites.spr
Normal file
Binary file not shown.
BIN
generated/dos/tiles/tbank0.tbk
Normal file
BIN
generated/dos/tiles/tbank0.tbk
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