From a334d2dd135f567297410072d868845cbc7ff6d7 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Mon, 5 Oct 2026 18:49:14 -0500 Subject: [PATCH] Split Space Taxi and AGI into their own repositories. --- .gitignore | 19 +- LICENSES.md | 3 - Makefile | 36 +- README.md | 42 +- docs/Assets.adoc | 34 +- docs/BuildIntegration.adoc | 335 ++- docs/GettingStarted.adoc | 40 +- docs/Overview.adoc | 4 +- examples/agi/agi.c | 2086 --------------- examples/agi/agi.h | 606 ----- examples/agi/agiObj.c | 363 --- examples/agi/agiPic.c | 659 ----- examples/agi/agiRes.c | 400 --- examples/agi/agiText.c | 433 ---- examples/agi/agiView.c | 395 --- examples/agi/agiVm.c | 2415 ------------------ examples/audio/audio.c | 301 ++- examples/spacetaxi/21_!.wav | Bin 33460 -> 0 bytes examples/spacetaxi/31_1.wav | Bin 12986 -> 0 bytes examples/spacetaxi/32_2.wav | Bin 21446 -> 0 bytes examples/spacetaxi/33_3.wav | Bin 25554 -> 0 bytes examples/spacetaxi/34_4.wav | Bin 17962 -> 0 bytes examples/spacetaxi/35_5.wav | Bin 20456 -> 0 bytes examples/spacetaxi/36_6.wav | Bin 28952 -> 0 bytes examples/spacetaxi/37_7.wav | Bin 18852 -> 0 bytes examples/spacetaxi/38_8.wav | Bin 23360 -> 0 bytes examples/spacetaxi/39_9.wav | Bin 14764 -> 0 bytes examples/spacetaxi/3F_x.wav | Bin 24876 -> 0 bytes examples/spacetaxi/48_H.wav | Bin 18072 -> 0 bytes examples/spacetaxi/50_P.wav | Bin 24446 -> 0 bytes examples/spacetaxi/54_T.wav | Bin 25694 -> 0 bytes examples/spacetaxi/55_U.wav | Bin 25264 -> 0 bytes examples/spacetaxi/MECHANICS.md | 2217 ---------------- examples/spacetaxi/PLAN.md | 141 - examples/spacetaxi/VERIFIED.md | 97 - examples/spacetaxi/assets/RawTitle.png | 3 - examples/spacetaxi/assets/TitleShr.png | 3 - examples/spacetaxi/assets/cpu6502.py | 560 ---- examples/spacetaxi/assets/depackSpeechRam.py | 78 - examples/spacetaxi/assets/dumpScenes.py | 115 - examples/spacetaxi/assets/extractSpeech.py | 225 -- examples/spacetaxi/assets/genC64Data.py | 192 -- examples/spacetaxi/assets/genDemoStreams.py | 74 - examples/spacetaxi/assets/genSongData.py | 76 - examples/spacetaxi/assets/genSpeechPcm.py | 113 - examples/spacetaxi/assets/levels/format.md | 92 - examples/spacetaxi/assets/levels/title.txt | 142 - examples/spacetaxi/assets/romToLevel.py | 205 -- examples/spacetaxi/assets/sidCapture.py | 57 - examples/spacetaxi/assets/sidToSong.py | 111 - examples/spacetaxi/assets/songs/song0.song | 94 - examples/spacetaxi/assets/songs/song1.song | 94 - examples/spacetaxi/assets/songs/song2.song | 86 - examples/spacetaxi/assets/songs/song3.song | 86 - examples/spacetaxi/assets/songs/song4.song | 126 - examples/spacetaxi/assets/songs/song5.song | 736 ------ examples/spacetaxi/assets/songs/song6.song | 840 ------ examples/spacetaxi/assets/songs/song7.song | 852 ------ examples/spacetaxi/assets/songs/song8.song | 1508 ----------- examples/spacetaxi/mkstlevel/mkstlevel.c | 452 ---- examples/spacetaxi/per-level-data.txt | 26 - examples/spacetaxi/spacetaxi.c | 1191 --------- examples/spacetaxi/spacetaxi.h | 78 - examples/spacetaxi/stAudio.c | 669 ----- examples/spacetaxi/stC64Data.c | 1356 ---------- examples/spacetaxi/stC64Data.h | 56 - examples/spacetaxi/stCels.c | 103 - examples/spacetaxi/stCels.h | 48 - examples/spacetaxi/stDemoStreams.h | 230 -- examples/spacetaxi/stFare.c | 424 --- examples/spacetaxi/stHookTables.h | 85 - examples/spacetaxi/stHooks.c | 1778 ------------- examples/spacetaxi/stLevel.c | 21 - examples/spacetaxi/stLevelCels.c | 114 - examples/spacetaxi/stLevelFile.c | 90 - examples/spacetaxi/stRender.c | 1286 ---------- examples/spacetaxi/stSim.c | 1713 ------------- examples/spacetaxi/stSim.h | 677 ----- examples/spacetaxi/stSongData.h | 512 ---- examples/spacetaxi/stSpeechData.c | 490 ---- examples/spacetaxi/stSpeechData.h | 26 - examples/spacetaxi/stTitle.c | 354 --- install.sh | 4 +- installer/components/staxiData.sh | 30 - installer/files.sha1 | 1 - joey-run | 42 +- make/amiga.mk | 257 +- make/atarist.mk | 278 +- make/common.mk | 125 +- make/dos.mk | 313 +-- make/dvx.mk | 238 +- make/examples.mk | 164 ++ make/iigs.mk | 511 ++-- make/programs.mk | 67 + make/project.mk | 97 + make/support/iigs/clang-build.sh | 5 +- make/tools.mk | 33 +- make/x68000.mk | 225 +- scripts/bench-staxi-title.sh | 164 -- scripts/diag-staxi-screen.sh | 147 -- scripts/lib/dvxGate.sh | 13 +- scripts/lib/joeyEnv.sh | 37 +- scripts/make-agi-iigs-disk.sh | 75 - scripts/make-iigs-disk.sh | 159 +- scripts/make-iigs-game-disk.sh | 2 +- scripts/profile-x68000.sh | 17 +- scripts/test-agi.sh | 200 -- scripts/verify-all.sh | 18 +- scripts/verify-atarist-audio.sh | 12 +- scripts/verify-dos-audio.sh | 3 +- scripts/verify-iigs-audio.sh | 30 +- scripts/verify-iigs-tilebank.sh | 13 +- scripts/verify-x68000-audio.sh | 27 +- src/core/debug.c | 10 +- src/generic/genericSurface.c | 4 +- tests/agi/disasmAgi.c | 413 --- tests/agi/testAgiPic.c | 191 -- tests/agi/testAgiPipeline.c | 284 -- tests/agi/testAgiRes.c | 101 - tests/agi/testAgiView.c | 198 -- tests/agi/testAgiVm.c | 135 - tools/iigsStart/iigsStart.c | 6 +- tools/staxibake/staxibake.c | 124 - tools/staxiharness/Makefile | 36 - tools/staxiharness/harness.c | 182 -- tools/stsim/Makefile | 17 - tools/stsim/celGate.sh | 34 - tools/stsim/compare.py | 79 - tools/stsim/gate.sh | 75 - tools/stsim/gateHashes.txt | 21 - tools/stsim/gateStreams.h | 460 ---- tools/stsim/recorder.c | 195 -- tools/stsim/stsim.c | 238 -- 133 files changed, 1647 insertions(+), 33336 deletions(-) delete mode 100644 examples/agi/agi.c delete mode 100644 examples/agi/agi.h delete mode 100644 examples/agi/agiObj.c delete mode 100644 examples/agi/agiPic.c delete mode 100644 examples/agi/agiRes.c delete mode 100644 examples/agi/agiText.c delete mode 100644 examples/agi/agiView.c delete mode 100644 examples/agi/agiVm.c delete mode 100644 examples/spacetaxi/21_!.wav delete mode 100644 examples/spacetaxi/31_1.wav delete mode 100644 examples/spacetaxi/32_2.wav delete mode 100644 examples/spacetaxi/33_3.wav delete mode 100644 examples/spacetaxi/34_4.wav delete mode 100644 examples/spacetaxi/35_5.wav delete mode 100644 examples/spacetaxi/36_6.wav delete mode 100644 examples/spacetaxi/37_7.wav delete mode 100644 examples/spacetaxi/38_8.wav delete mode 100644 examples/spacetaxi/39_9.wav delete mode 100644 examples/spacetaxi/3F_x.wav delete mode 100644 examples/spacetaxi/48_H.wav delete mode 100644 examples/spacetaxi/50_P.wav delete mode 100644 examples/spacetaxi/54_T.wav delete mode 100644 examples/spacetaxi/55_U.wav delete mode 100644 examples/spacetaxi/MECHANICS.md delete mode 100644 examples/spacetaxi/PLAN.md delete mode 100644 examples/spacetaxi/VERIFIED.md delete mode 100644 examples/spacetaxi/assets/RawTitle.png delete mode 100644 examples/spacetaxi/assets/TitleShr.png delete mode 100644 examples/spacetaxi/assets/cpu6502.py delete mode 100755 examples/spacetaxi/assets/depackSpeechRam.py delete mode 100644 examples/spacetaxi/assets/dumpScenes.py delete mode 100755 examples/spacetaxi/assets/extractSpeech.py delete mode 100644 examples/spacetaxi/assets/genC64Data.py delete mode 100644 examples/spacetaxi/assets/genDemoStreams.py delete mode 100644 examples/spacetaxi/assets/genSongData.py delete mode 100755 examples/spacetaxi/assets/genSpeechPcm.py delete mode 100644 examples/spacetaxi/assets/levels/format.md delete mode 100644 examples/spacetaxi/assets/levels/title.txt delete mode 100644 examples/spacetaxi/assets/romToLevel.py delete mode 100644 examples/spacetaxi/assets/sidCapture.py delete mode 100644 examples/spacetaxi/assets/sidToSong.py delete mode 100644 examples/spacetaxi/assets/songs/song0.song delete mode 100644 examples/spacetaxi/assets/songs/song1.song delete mode 100644 examples/spacetaxi/assets/songs/song2.song delete mode 100644 examples/spacetaxi/assets/songs/song3.song delete mode 100644 examples/spacetaxi/assets/songs/song4.song delete mode 100644 examples/spacetaxi/assets/songs/song5.song delete mode 100644 examples/spacetaxi/assets/songs/song6.song delete mode 100644 examples/spacetaxi/assets/songs/song7.song delete mode 100644 examples/spacetaxi/assets/songs/song8.song delete mode 100644 examples/spacetaxi/mkstlevel/mkstlevel.c delete mode 100644 examples/spacetaxi/per-level-data.txt delete mode 100644 examples/spacetaxi/spacetaxi.c delete mode 100644 examples/spacetaxi/spacetaxi.h delete mode 100644 examples/spacetaxi/stAudio.c delete mode 100644 examples/spacetaxi/stC64Data.c delete mode 100644 examples/spacetaxi/stC64Data.h delete mode 100644 examples/spacetaxi/stCels.c delete mode 100644 examples/spacetaxi/stCels.h delete mode 100644 examples/spacetaxi/stDemoStreams.h delete mode 100644 examples/spacetaxi/stFare.c delete mode 100644 examples/spacetaxi/stHookTables.h delete mode 100644 examples/spacetaxi/stHooks.c delete mode 100644 examples/spacetaxi/stLevel.c delete mode 100644 examples/spacetaxi/stLevelCels.c delete mode 100644 examples/spacetaxi/stLevelFile.c delete mode 100644 examples/spacetaxi/stRender.c delete mode 100644 examples/spacetaxi/stSim.c delete mode 100644 examples/spacetaxi/stSim.h delete mode 100644 examples/spacetaxi/stSongData.h delete mode 100644 examples/spacetaxi/stSpeechData.c delete mode 100644 examples/spacetaxi/stSpeechData.h delete mode 100644 examples/spacetaxi/stTitle.c delete mode 100644 installer/components/staxiData.sh create mode 100644 make/examples.mk create mode 100644 make/programs.mk create mode 100644 make/project.mk delete mode 100755 scripts/bench-staxi-title.sh delete mode 100755 scripts/diag-staxi-screen.sh delete mode 100755 scripts/make-agi-iigs-disk.sh delete mode 100755 scripts/test-agi.sh delete mode 100644 tests/agi/disasmAgi.c delete mode 100644 tests/agi/testAgiPic.c delete mode 100644 tests/agi/testAgiPipeline.c delete mode 100644 tests/agi/testAgiRes.c delete mode 100644 tests/agi/testAgiView.c delete mode 100644 tests/agi/testAgiVm.c delete mode 100644 tools/staxibake/staxibake.c delete mode 100644 tools/staxiharness/Makefile delete mode 100644 tools/staxiharness/harness.c delete mode 100644 tools/stsim/Makefile delete mode 100755 tools/stsim/celGate.sh delete mode 100644 tools/stsim/compare.py delete mode 100755 tools/stsim/gate.sh delete mode 100644 tools/stsim/gateHashes.txt delete mode 100644 tools/stsim/gateStreams.h delete mode 100644 tools/stsim/recorder.c delete mode 100644 tools/stsim/stsim.c diff --git a/.gitignore b/.gitignore index c98f64e..b999f5c 100644 --- a/.gitignore +++ b/.gitignore @@ -45,16 +45,12 @@ Thumbs.db *.SND *.ANI -# Baked assets: sprite banks (.spr), precompiled sprite banks (.spc), -# tile banks (.tbk) and level files -- all regenerated by the platform -# makefiles from the PNG/ROM sources +# Baked assets: sprite banks (.spr), precompiled sprite banks (.spc) and +# tile banks (.tbk) -- all regenerated by the platform makefiles from the +# PNG sources *.spr *.spc *.tbk -examples/spacetaxi/generated/ - -# Host tool binaries built from source -tools/stsim/stsim # MAME droppings (written next to the work dir by the verify scripts) cfg/ @@ -68,12 +64,3 @@ screenshots/ # Claude's working notes (design scratch, port write-ups, session state). # NOT the user manual -- that lives in docs/ and IS tracked. claudeDocs/* - -# AGI game data (Sierra/fan-made AGI games used for testing; never committed) -examples/agi/gamedata/ - -# Space Taxi behaviour-gate captures (tools/stsim/gate.sh capture). Large -# and regenerable from the tree; gateHashes.txt is the committed reference. -tools/stsim/golden/ -tools/stsim/stsim -tools/stsim/recorder diff --git a/LICENSES.md b/LICENSES.md index 4279090..b193b47 100644 --- a/LICENSES.md +++ b/LICENSES.md @@ -243,9 +243,6 @@ project hosts (`JOEY_BUNDLE_URL`); matched by the checksums in a Workbench `.adf`. Component `amigaSerial`. - **X68000 CG (font) ROM**: not part of Sharp's free release. MAME's x68000 driver needs it. Component `x68kCgrom`. -- **Space Taxi C64 memory image** (`stuff/spacetaxi/raw.bin`): the original - game's data, from which the example's levels are extracted. Optional. - Component `staxiData`. --- diff --git a/Makefile b/Makefile index a8212c0..8d21c26 100644 --- a/Makefile +++ b/Makefile @@ -18,38 +18,12 @@ # toolchains/config.mk; every makefile reads that file itself, so no # environment setup is needed first. -REPO_DIR := $(abspath $(dir $(firstword $(MAKEFILE_LIST)))) +# The toolchain paths and which ports are installed (HAVE_, +# JOEY_PORTS_INSTALLED) come from make/common.mk, shared with every port's +# makefile and with make/project.mk. +include $(dir $(firstword $(MAKEFILE_LIST)))make/common.mk --include $(REPO_DIR)/toolchains/config.mk - -# A port is available when its real compiler binary is installed. -HAVE_IIGS := $(if $(wildcard $(LLVM816_ROOT)/tools/llvm-mos-build/bin/clang),1) -HAVE_AMIGA := $(if $(wildcard $(AMIGA_CC)),1) -HAVE_ATARIST := $(if $(wildcard $(ST_CC)),1) -HAVE_DOS := $(if $(wildcard $(DOS_CC)),1) -HAVE_X68000 := $(if $(wildcard $(X68K_ROOT)/bin/m68k-xelf-gcc),1) -# DVX needs the DOS compiler and the DVX SDK the installer copies. -HAVE_DVX := $(if $(and $(wildcard $(DOS_CC)),$(wildcard $(DVX_SDK)/sdk.mk)),1) - -ALL_TARGETS := -ifeq ($(HAVE_IIGS),1) - ALL_TARGETS += iigs -endif -ifeq ($(HAVE_AMIGA),1) - ALL_TARGETS += amiga -endif -ifeq ($(HAVE_ATARIST),1) - ALL_TARGETS += atarist -endif -ifeq ($(HAVE_DOS),1) - ALL_TARGETS += dos -endif -ifeq ($(HAVE_X68000),1) - ALL_TARGETS += x68000 -endif -ifeq ($(HAVE_DVX),1) - ALL_TARGETS += dvx -endif +ALL_TARGETS := $(JOEY_PORTS_INSTALLED) .PHONY: all iigs iigs-disk iigs-verify amiga atarist dos x68000 dvx blank-check tools clean help status verify verify-audio verify-serial docs diff --git a/README.md b/README.md index f53fc40..a473ffa 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ hand, `source toolchains/env.sh` sets the same variables. `make` builds `libjoey.a` for every port whose toolchain is installed, plus the example programs (`hello`, `pattern`, `draw`, `keys`, `serial`, `sertest`, `save`, `tilebank`, `joy`, `sprite`, `audio`, `uber`, `adventure`, -`adventure2`, `agi`, `spacetaxi`) for each. The IIgs is the exception: +`adventure2`) for each. The IIgs is the exception: `make iigs` builds the library, and `make iigs-disk` builds its examples and packages them onto a disk image. DVX needs the `dvx` installer component, which unpacks DVX and its SDK from a DVX tree's distribution, `dist/dvx.zip` @@ -55,9 +55,15 @@ and `dist/sdk.zip` (`./install.sh --dvx-dir PATH install dvx`; the default is `../dvxgui`); its examples land in `build/dvx/bin/apps/joeylib//`, laid out as they install in DVX. -Space Taxi's levels come from the original Commodore 64 game, which is not in -the repository. Without `stuff/spacetaxi/raw.bin` the build compiles the -STAXI program, skips only its data, and prints a note saying so. +Your own program does not go into this tree: it lives in a directory of its +own whose Makefile describes it and includes `make/project.mk` and +`make/programs.mk` from a JoeyLib checkout (found through `JOEYLIB`, by +default `../joeylib`). `make` there builds it for every installed target with +the same rules, flags and packaging as the examples, into the project's own +`build/`; the manual's "Building Your Program" chapter +(`docs/BuildIntegration.adoc`) has the details. Two complete games are built +this way as separate projects: Space Taxi, a port of the 1984 Commodore 64 +game, and JoeyAGI, an interpreter for Sierra's AGI adventures. ## Running programs @@ -70,12 +76,12 @@ the emulators and their firmware: ./joey-run build/dos/bin/DRAW.EXE ./joey-run --port amiga draw ./joey-run --headless --seconds 30 --snapshot draw.png --port atarist draw -./joey-run --port dvx staxi # DVX in DOSBox, booting into the program +./joey-run --port dvx draw # DVX in DOSBox, booting into the program ``` -Four files are never downloaded -- the Amiga Kickstart, the Amiga -`serial.device` from Workbench 3.1, the X68000 font ROM and Space Taxi's C64 -memory image -- so supply your copies once with `./install.sh import PATH` +Three files are never downloaded -- the Amiga Kickstart, the Amiga +`serial.device` from Workbench 3.1 and the X68000 font ROM -- so supply your +copies once with `./install.sh import PATH` (files, folders, `.zip` archives or Amiga `.adf` disk images). The manual's Getting Started chapter covers `joey-run`'s options and the private-file bundle. @@ -158,7 +164,11 @@ install.sh installs toolchains, emulators and firmware installer/ the installer's component recipes, patches, lists toolchains/ everything install.sh downloads and builds (not in git; delete it any time and re-run install.sh) -make/ per-target Makefile fragments (make/support: build helpers) +make/ per-target Makefile fragments: each port's library and + program rules (make/.mk), the shared program + rules (programs.mk), JoeyLib's examples (examples.mk), + the entry point for outside projects (project.mk); + make/support: build helpers build// per-target build outputs ``` @@ -423,14 +433,12 @@ tools/assetbake/assetbake.py --type tile --target {amiga|atarist|dos|iigs|x680 tools/assetbake/assetbake.py --type sprite --cell WxH in.png out.spr ``` -The Makefiles (`make/{amiga,atarist,dos}.mk`) wire bake rules -per target so `make ` produces baked blobs under -`examples//generated//` and stages them into the -runtime tree at `build//.../DATA/`. `make/iigs.mk` runs no -`assetbake.py`: its `generated/iigs` tree is produced out-of-band and the -fragment only bakes `.spc` from the committed `.spr`. `make/x68000.mk` -builds only what Space Taxi loads there: its levels and pre-compiled cel -banks. +A program's description wires its bake rules once for every target +(`make/examples.mk`, or a project's own Makefile), so `make ` +produces the baked blobs and stages them into the +runtime tree at `build//.../DATA/` -- on the IIgs and DVX into +`build//data//DATA/`, which the disk packer and the DVX SDK +package with the program. For runtime-extracted content (sprites peeled out of a procedural or captured surface), use `jlSpriteCreateFromSurface` -- the diff --git a/docs/Assets.adoc b/docs/Assets.adoc index 04c3521..45eb793 100644 --- a/docs/Assets.adoc +++ b/docs/Assets.adoc @@ -269,16 +269,17 @@ shipped artwork, bake a tile bank and compose the screen with === Staging Assets into the Build -Baked files must end up in `DATA/` next to the executable. Each port's makefile -does this with an explicit rule. The simple copy case, from `make/atarist.mk`: +Baked files must end up in `DATA/` next to the executable. A program's +Makefile lists them in its `ID_DATA` and makes them with rules of its own, +written once for every target (<<_data_files>>). The simple copy case, from +JoeyLib's `make/examples.mk`: [source,makefile] ---- -DATA_DIR := $(BINDIR)/DATA -DATA_FILES := $(DATA_DIR)/test.mod $(DATA_DIR)/test.sfx +AUDIO_DATA := $(addprefix $(call programDataDir,AUDIO)/,test.mod test.sfx) -$(DATA_DIR)/test.mod: $(REPO_DIR)/assets/test.mod - @mkdir -p $(DATA_DIR) +$(call programDataDir,AUDIO)/%: $(REPO_DIR)/assets/% + @mkdir -p $(dir $@) cp $< $@ ---- @@ -286,20 +287,23 @@ And a bake rule looks like: [source,makefile] ---- -$(DATA_DIR)/font.tbk: $(MY_ASSETS)/font.png - @mkdir -p $(DATA_DIR) - python3 $(REPO_DIR)/tools/assetbake/assetbake.py --type tile --target atarist $< $@ +$(call programDataDir,MYGAME)/font.tbk: assets/font.png + @mkdir -p $(dir $@) + python3 $(REPO_DIR)/tools/assetbake/assetbake.py --type tile --target $(ASSET_PLATFORM) $< $@ ---- -Add the baked files to the port's `all` target so they are built alongside the -binaries. Remember that tile banks and `.spc` files need one rule *per -platform*, while `.spr`, `.jym` and `.sfx` files are shared. +Tile banks and `.spc` files differ per platform, while `.spr`, `.jym` and +`.sfx` files are shared: `ASSET_PLATFORM` names the target a bake is for (DVX +takes the DOS bakes) and `SPRITEBAKE` the matching host `spritebake`. -`examples/tilebank` is a working reference: every port's makefile bakes the -same PNG for its own target into `DATA/tilebank` (its `TILEBANK_DATA` rule), +`examples/tilebank` is a working reference: one rule in `make/examples.mk` +bakes the same PNG for each target into its `DATA/tilebank` (`TILEBANK_DATA`), and the program loads that bank both ways and shows the loader refusing the other targets' bakes. Its `scripts/verify--tilebank.sh` gates run it on -every port. +every port. Space Taxi, a game built on JoeyLib in a repository of its own, +has the full-sized one in its `program.mk`: levels extracted from a C64 image, +a cel bank generated by a host tool of its own and pre-compiled per target, +per-level banks marked optional, and speech baked with per-target flags. === Loading Assets at Run Time diff --git a/docs/BuildIntegration.adoc b/docs/BuildIntegration.adoc index d81ff75..f197f6f 100644 --- a/docs/BuildIntegration.adoc +++ b/docs/BuildIntegration.adoc @@ -1,88 +1,210 @@ -== Adding Your Program to the Build +== Building Your Program -JoeyLib does not install as a system library. Programs are built from inside -the repository by the same makefiles that build the library, because each -target needs its own cross compiler, link script, packaging step and disk -image. Adding a program means adding it to each port's makefile. +JoeyLib does not install as a system library: each target needs its own cross +compiler, link step, packaging and disk image, and JoeyLib's makefiles own all +of that. Your program lives in a directory of its own -- its own repository, +usually -- next to a JoeyLib checkout. Its Makefile describes the program and +includes two of JoeyLib's makefiles, and that is enough to build it for every +target with exactly the flags, link order and packaging JoeyLib's own examples +get. JoeyLib's examples are described the same way and built by the same +rules; there is one set of them. -=== The Shape of a Program Directory - -Put your sources under `examples/`, one directory per program: +=== A Project ---- -examples/mygame/ - mygame.c - mygame.h - assets/ - tiles.png - sprites.png - music.song +games/ + joeylib/ a JoeyLib checkout, set up with ./install.sh + mygame/ + Makefile + mygame.c + world.c + assets/ + tiles.png ---- -Keep the authoring assets next to the code and let the makefiles bake them into -`DATA/`, as described in <<_creating_assets>>. - -=== Adding It to One Port - -Each port's makefile declares programs explicitly. The pattern in -`make/atarist.mk`, for a single-source program: +The whole Makefile for a program with no data files: [source,makefile] ---- -MYGAME_SRC := $(EXAMPLES)/mygame/mygame.c -MYGAME_BIN := $(BINDIR)/MYGAME.PRG +JOEYLIB ?= ../joeylib +include $(JOEYLIB)/make/project.mk + +PROGRAMS := MYGAME +MYGAME_SRCS := mygame.c world.c +MYGAME_TITLE := My Game +MYGAME_HELP := One line on what it is, for its DVX help file. + +include $(JOEYLIB)/make/programs.mk ---- -Add the binary to the port's `all` target so it gets built: +`make/project.mk` comes first and sets up the target being built; the +description comes next and may use what it set up (see <<_data_files>>); +`make/programs.mk` comes last and makes the rules. Run make in the project's +directory -- the paths in the Makefile are relative to it -- or with `make -C`: -[source,makefile] +[source,console] ---- -all atarist: $(LIB) ... $(MYGAME_BIN) $(DATA_FILES) +$ make # every target whose toolchain is installed +$ make dos # one target: iigs amiga atarist dos x68000 dvx +$ make clean # the project's build/ (clean-dos: one target's) +$ make help ---- -And give it a build rule, copying the shape of an existing example in that -file. Do not invent your own compiler invocation: the flags, include paths and -link order in those rules encode real constraints, several of them the result -of long debugging. +A target without its toolchain is skipped with a note. `JOEYLIB=PATH` on the +command line points at a checkout somewhere else. -For a multi-file program, list the sources: +==== Where things go -[source,makefile] +The project's outputs land in its own `build/`, laid out like JoeyLib's: + +[cols="1,4", options="header"] +|=== +|Target |Output + +|Apple IIgs |`build/iigs/bin/MYGAME` (an S16 OMF), its data staged in +`build/iigs/data/MYGAME/`, and with `MYGAME_IIGS_GAME_DISK` the disks +`build/iigs/bin/MYGAME.2mg` and `MYGAME-data.2mg` +|Amiga |`build/amiga/bin/Mygame` +|Atari ST |`build/atarist/bin/MYGAME.PRG` +|MS-DOS |`build/dos/bin/MYGAME.EXE`, with the DPMI server built in +|Sharp X68000 |`build/x68000/bin/MYGAME.X` +|DVX |`build/dvx/bin/apps/joeylib/mygame/`: `mygame.app`, `mygame.hlp` and its +`DATA/` +|=== + +A program with `MYGAME_FOLDER` set ships in a folder of its own with its +`DATA/` beside it (`build/dos/bin/MYGAME/MYGAME.EXE`); otherwise programs share +`bin/DATA/`. + +JoeyLib's library for each target, and the host tools such as `spritebake`, are +built on demand where JoeyLib keeps them, in the checkout's `build/`, and shared +with JoeyLib's own build and every other project: the first project to need +the DOS library builds it, the rest reuse it. That is the only thing a project +writes into the JoeyLib checkout. To keep even that out, give it another place: + +[source,console] ---- -MYGAME_SRCS := $(EXAMPLES)/mygame/mygame.c \ - $(EXAMPLES)/mygame/world.c \ - $(EXAMPLES)/mygame/render.c +$ make JOEY_BUILD=$PWD/build/joeylib ---- -Repeat for each port you want to support: `make/iigs.mk`, `make/amiga.mk`, -`make/atarist.mk`, `make/dos.mk`, `make/x68000.mk` and `make/dvx.mk`. - -`make/dvx.mk` is the one exception to the pattern. A DVX program is a DVX SDK -project built by the SDK's own `sdk.mk`, which `make/dvx.mk` writes for every -name in its `APPS` list from three variables, plus a fourth for a program -with data files: - -[source,makefile] ----- -mygame_SRCS := $(EXAMPLES)/mygame/mygame.c $(EXAMPLES)/mygame/world.c -mygame_TITLE := My Game -mygame_HELP := One line on what the program is, for its help file. -mygame_DATA := $(DATADIR)/mygame/DATA # optional: staged by a rule of yours - -APPS := hello pattern ... mygame ----- +`PROJECT_BUILD=DIR` likewise moves the project's own `build/`. Both belong on +the command line (or before the `include` of `make/project.mk`). [NOTE] ==== -Yes, that is six makefile edits for one program. The ports genuinely differ: -the IIgs needs OMF segmentation and a disk image, the Amiga a startup-sequence -entry, DOS a DPMI stub, the X68000 a Human68k floppy layout, and DVX a module -with a help file. There is no single rule that covers them all. +How it works: every target has its own compiler and its own make variables, so +one make cannot hold two of them. `make` in a project only reads the +description, then runs the same Makefile again once per target with +`JOEY_PORT` set, and in that run `make/project.mk` includes the target's +makefile, `make/.mk`. That makefile holds JoeyLib's library rules and +`programRules`, the target's rules for one program, which `make/programs.mk` +makes for every program. So any target of one port can be built directly: +`make JOEY_PORT=dos build/dos/bin/MYGAME.EXE`, and `make iigs-game-disk` is +passed to the IIgs run for you. ==== +=== Describing a Program + +A program is a set of variables named after its id: an upper-case name of up +to eight letters and digits, which is also its file name unless `_NAME` says +otherwise. `PROGRAMS` lists the ids. A project may describe several programs. + +[cols="2,5", options="header"] +|=== +|Variable |Meaning + +|`PROGRAMS` |The ids that `make` builds. +|`TEST_PROGRAMS` |Ids with rules that are built only when asked for (test +probes for gates). +|`ID_SRCS` |Its C sources, in link order. +|`ID_CFLAGS` |Extra compiler flags: `-I` folders, `-D` defines. +|`ID_LDFLAGS` |Extra link flags (`-s` to strip). +|`ID_PORTS` |The targets it builds for. Every target when unset. +|`ID_NAME` |Its file name without the extension (see <<_naming>>). +|`ID_FOLDER` |Set: it ships in a folder of its own with its `DATA/` (Amiga, +ST, DOS, X68000; on the IIgs and DVX every program is packaged apart anyway). +|`ID_BINDIR` |Build it there instead of `bin/`. +|`ID_DATA` |Its data files (<<_data_files>>). +|`ID_TITLE` |DVX: its window title. +|`ID_HELP` |DVX: the line its help file says about it. No apostrophes. +|`ID_IIGS_OVERFLOW_CAP` |IIgs: link large overflow segments (`0xF000`), which +a big program needs to leave the Memory Manager room (see +`make/support/iigs/clang-build.sh`). +|`ID_IIGS_GAME_DISK` |IIgs: set: `make` also builds its bootable volume. +|=== + +Any variable can be given for one target only as `ID__`, which +wins there even when empty: + +[source,makefile] +---- +MYGAME_AMIGA_NAME := MyGame +MYGAME_X68000_LDFLAGS := -s +MYGAME_IIGS_DATA := # nothing staged on the IIgs +---- + +DVX's help and resource texts call a program "a `$(DVX_KIND)` program" and +name `$(DVX_AUTHOR)` as its author (both `JoeyLib` by default); `DVX_VENDOR` +(`joeylib`) is the folder it installs in under `APPS\`. Set them in the +description. + +=== Data Files + +A program opens its files through `DATA/` (`jlDataOpen`), and its Makefile +puts them there. `ID_DATA` lists the staged files, and the project writes the +rules that make them, below its description, using what `make/project.mk` +set up: + +[cols="2,5", options="header"] +|=== +|Name |Meaning + +|`$(call programDataDir,ID)` |Where its `DATA/` files go on this target. +|`$(call programOptionalDir,ID)` |Where the files go that it can run without +(<<_disk_images_and_packaging>>). +|`PLATFORM` |The target: `iigs`, `amiga`, `atarist`, `dos`, `x68000`, `dvx`. +|`ASSET_PLATFORM` |The target its bakes are for. DVX shows the DOS stage and +runs x86 code, so it takes the DOS bakes. +|`SPRITEBAKE` |The host `spritebake` for this target (its rule is +JoeyLib's). +|`PROJECT_DIR`, `PROJECT_BUILD` |The project and its `build/`. +|`REPO_DIR` |The JoeyLib checkout, for its tools: `$(REPO_DIR)/tools/assetbake/assetbake.py`. +|`HOST_CC`, `HOST_CFLAGS` |For a host tool of the project's own. +|=== + +A copy, a tile-bank bake and a pre-compiled sprite bank: + +[source,makefile] +---- +MYGAME_RUN := $(call programDataDir,MYGAME) +MYGAME_GEN := $(PROJECT_BUILD)/generated/$(ASSET_PLATFORM) +MYGAME_DATA := $(MYGAME_RUN)/theme.jym $(MYGAME_RUN)/tiles.tbk $(MYGAME_RUN)/player.spc + +$(MYGAME_RUN)/theme.jym: assets/theme.jym + @mkdir -p $(dir $@) + cp $< $@ + +$(MYGAME_RUN)/tiles.tbk: assets/tiles.png + @mkdir -p $(dir $@) + python3 $(REPO_DIR)/tools/assetbake/assetbake.py --type tile --target $(ASSET_PLATFORM) $< $@ + +$(MYGAME_GEN)/player.spr: assets/player.png + @mkdir -p $(dir $@) + python3 $(REPO_DIR)/tools/assetbake/assetbake.py --type sprite --cell 16x16 $< $@ + +$(MYGAME_RUN)/player.spc: $(MYGAME_GEN)/player.spr $(SPRITEBAKE) + @mkdir -p $(dir $@) + $(SPRITEBAKE) $< $@ +---- + +One rule serves every target: tile banks and `.spc` files differ per target, +and `ASSET_PLATFORM` and `SPRITEBAKE` say which. The Assets chapter +(<<_creating_assets>>) covers the formats and tools. + === Naming -File naming conventions differ per platform and the makefiles reflect that: +File naming conventions differ per platform, and the rules apply them to the +program's id: [cols="2,2,4", options="header"] |=== @@ -91,65 +213,59 @@ File naming conventions differ per platform and the makefiles reflect that: |Apple IIgs |Uppercase, 8 characters |`MYGAME` |Atari ST |Uppercase 8.3, `.PRG` |`MYGAME.PRG` |MS-DOS |Uppercase 8.3, `.EXE` |`MYGAME.EXE` -|Amiga |Mixed case, no extension |`MyGame` +|Amiga |Mixed case, no extension: the id Capitalised unless `ID_AMIGA_NAME` +says otherwise |`Mygame` |Sharp X68000 |Uppercase 8.3, `.X` |`MYGAME.X` |DVX |Lowercase, 8 characters, a folder of its own |`apps/joeylib/mygame/mygame.app` |=== -Keep the base name to 8 characters and letters and digits only, so the same -name works everywhere. +Keep the id to 8 characters and letters and digits only, so the same name works +everywhere. === Disk Images and Packaging -Some ports need a packaging step beyond compiling. +*Apple IIgs.* Programs run from ProDOS volumes. With `MYGAME_IIGS_GAME_DISK := +yes`, `make iigs` also builds a self-booting volume, `build/iigs/bin/MYGAME.2mg`: +a stripped GS/OS that launches the program straight from its own folder, which +can be copied to a hard drive and run from there. It is an 800 KB floppy when +the program fits beside the system, otherwise a 2048 KB volume that MAME boots +off a CFFA2 card. `make iigs-game-disk GAME=NAME` builds one for any program, +`GAME_DISK_SIZE=2048KB` forces the size, and `make iigs-verify-game-disk` +boots it headlessly and checks it draws. The data the program needs goes on with +it; files listed under `programOptionalDir` (a program's optional data, which +it can run without) go on last, only while the volume has room. -*Apple IIgs.* Programs are placed on an 800 KB ProDOS disk image: +*Sharp X68000.* Programs ship on a Human68k floppy; `joey-run` builds one for +each run from the program and its `DATA/`. A floppy is 1232 KB, so a large +program strips its binary (`ID_X68000_LDFLAGS := -s`) and ships in a folder of +its own (`ID_FOLDER`), so its data never rides onto another program's disk. A +stale image can hide hundreds of kilobytes in subdirectories that a directory +listing does not show; rebuild from empty rather than deleting files if space +seems to vanish. -[source,console] ----- -$ make iigs-disk -$ JOEY_DISK_EXAMPLES="MYGAME" make iigs-disk # just yours ----- - -The floppy cannot hold every example at once, so the packager drops some. If -your program is missing from the image, build it with `JOEY_DISK_EXAMPLES` -naming it explicitly, and check the `added:` line the packager prints. A -program plus large assets may not fit 800 KB at all, in which case use the -bigger game-disk image on a CFFA2 card: - -[source,console] ----- -$ JOEY_GAME_DISK_SIZE=2048KB scripts/make-iigs-game-disk.sh \ - build/iigs/bin/mygame.2mg MYGAME ----- - -*Sharp X68000.* Programs ship on a Human68k floppy. Be aware that a stale image -can hide hundreds of kilobytes in subdirectories that a directory listing does -not show; rebuild from empty rather than deleting files if space seems to -vanish. +*MS-DOS.* The CWSDPMI DPMI server is built into every `.EXE`, so nothing else +needs to be on the disk. *Amiga.* `joey-run` writes an `s/startup-sequence` that invokes your binary. *DVX.* Every program is a folder laid out as it installs in DVX: `build/dvx/bin/apps/joeylib/mygame/` holds `mygame.app`, its help file -`mygame.hlp` (DVX gives every program one; `make/dvx.mk` writes a one-topic -help from `mygame_HELP`) and its `DATA/`. Copy that folder into a DVX -installation's `APPS\JOEYLIB\` and start it from DVX, or let `joey-run` do it +`mygame.hlp` (DVX gives every program one; the rules write a one-topic help +from `MYGAME_HELP`) and its `DATA/`. Copy that folder into a DVX installation's +`APPS\JOEYLIB\` and start it from DVX, or let `joey-run` do it (<<_dvx_programs>>). === Verifying It Actually Runs Build success is weak evidence on retro targets. Run it with `joey-run` (see -<>), on every machine: +<>) on every machine, by its path or by its name in +the project: [source,console] ---- -$ ./joey-run --port iigs mygame -$ ./joey-run --port amiga mygame -$ ./joey-run --port atarist mygame -$ ./joey-run --port dos mygame -$ ./joey-run --port x68000 mygame -$ ./joey-run --port dvx mygame +$ ../joeylib/joey-run build/dos/bin/MYGAME.EXE +$ ../joeylib/joey-run --project . --port amiga mygame +$ ../joeylib/joey-run build/iigs/bin/MYGAME.2mg ---- Without a window, which is what you want while iterating, run it headless and @@ -157,14 +273,36 @@ look at the snapshot; the program's log lands next to it as usual: [source,console] ---- -$ ./joey-run --headless --seconds 90 --snapshot mygame.png --port atarist mygame +$ ../joeylib/joey-run --headless --seconds 90 --snapshot mygame.png --project . --port atarist mygame ---- +=== JoeyLib's Own Examples + +JoeyLib's examples are programs like any other: `make/examples.mk` describes +them, and each target's makefile includes it and `make/programs.mk`. Built from +the JoeyLib checkout, everything lands in its `build/`: + +[source,console] +---- +$ make # the library and the examples, every installed target +$ make iigs-disk # the IIgs examples on build/iigs/bin/joey.2mg +$ make -f make/iigs.mk iigs-game-disk GAME=DRAW +---- + +`make iigs` builds only the IIgs library; `make iigs-disk` builds the examples +and packs them. Space Taxi and JoeyAGI, two complete games, are projects of +their own built exactly as this chapter describes: each repository's Makefile +includes its `program.mk` description between JoeyLib's two makefiles. + === The Gates The repository carries automated checks. They exist because several classes of bug on these machines are invisible until measured, and they are worth running -against your own program too. +against your own program too: `scripts/verify-iigs-game-disk.sh IMAGE` boots any +game disk, `scripts/verify-iigs-audio.sh` and `scripts/verify-x68000-audio.sh` +read the sound chips under any program given its path, `joey-run --headless` +runs any program, and the emulator setup in `scripts/lib/` (DVX staging, +Human68k floppies, environment checks) takes paths. [cols="2,4", options="header"] |=== @@ -178,6 +316,9 @@ against your own program too. |`make iigs-verify` |The IIgs actually reached the program and drew something. +|`make iigs-verify-game-disk` +|A program's bootable IIgs volume boots, launches the program and draws. + |`scripts/verify--tilebank.sh` |The port's own tile-bank bake loads pixel-exact both ways, with the same surface hash as the host check `scripts/check-tilebank.sh`, and banks baked diff --git a/docs/GettingStarted.adoc b/docs/GettingStarted.adoc index 20286cc..aa7fab7 100644 --- a/docs/GettingStarted.adoc +++ b/docs/GettingStarted.adoc @@ -122,9 +122,9 @@ firmware. | Every port's emulator. | `all` -| Everything except optional components (`staxiData`, Space Taxi's level -source, and `dvx`, which unpacks DVX from a DVX tree of yours), which are -installed by name. `toolchains` and `emulators` leave them out too. +| Everything except optional components (`dvx`, which unpacks DVX from a DVX +tree of yours), which are installed by name. `toolchains` and `emulators` +leave them out too. |=== [cols="1,3"] @@ -213,15 +213,6 @@ produces a bootable disk image: $ make iigs-disk # build/iigs/bin/joey.2mg ---- -Space Taxi's levels come from the original Commodore 64 game, which is not -part of the repository. Without `stuff/spacetaxi/raw.bin` the build still -compiles the STAXI program, skips only its data files, and says so: - -[source,console] ----- -note: Space Taxi data skipped: stuff/spacetaxi/raw.bin is not present ----- - [TIP] ==== Never judge a retro build by its exit status alone. Check that the file you @@ -342,8 +333,8 @@ Point `joey-run` at a program, or name an example together with `--port`: $ ./joey-run build/dos/bin/DRAW.EXE $ ./joey-run build/amiga/bin/Draw $ ./joey-run --port atarist draw -$ ./joey-run build/iigs/bin/STAXI.2mg -$ ./joey-run --port dvx staxi +$ ./joey-run build/iigs/bin/DRAW.2mg +$ ./joey-run --port dvx draw ---- It recognises the machine from the file itself. Interactive runs use the @@ -359,7 +350,7 @@ the next. Nothing is written to your home directory. | `--data DIR` | Copy `DIR`'s files into the program's `DATA` folder for this run, for -example an AGI game's files. +example the game an interpreter plays. | `--headless` | No window and no sound, for scripts and continuous integration. Stops after @@ -375,7 +366,7 @@ example an AGI game's files. | Apple IIgs: the ROM 01 machine instead of ROM 03. | `--ram SIZE` -| Apple IIgs: MAME's memory size. The default is `4M`; AGI wants `8M`. +| Apple IIgs: MAME's memory size. The default is `4M`. | `--debug` | Apple IIgs and X68000: start MAME's debugger. @@ -495,12 +486,11 @@ connects late sees only the echo. Almost everything the emulators need is downloaded: EmuTOS for the Atari ST, the Apple IIgs ROMs, GS/OS 6.0.4 and the CFFA2 card's firmware, and Sharp's own -free release of the X68000 IPL-ROM and Human68k 3.02. Four files are not, +free release of the X68000 IPL-ROM and Human68k 3.02. Three files are not, because they were never released for free: the Amiga Kickstart 3.1 ROM, the Amiga `serial.device` from Workbench 3.1 (Kickstart 3.1 has none in ROM, and -a program cannot open the serial port without it), the X68000 CG (font) ROM, -and the memory image of the original Space Taxi that its levels are extracted -from. Supply your copies once: +a program cannot open the serial port without it) and the X68000 CG (font) +ROM. Supply your copies once: [source,console] ---- @@ -532,15 +522,19 @@ built for every platform: |`keys` |Typed-character input and the key predicates. |`joy` |Joystick axes, buttons and recalibration. |`sprite` |Sprite loading, compiling, save-under and restore. -|`audio` |Tracker music, `.MOD` playback and digital sound effects. +|`audio` |Tracker music, `.MOD` playback, digital sound effects, a streamed sound and the noise generator. |`save` |Save files and the disk-space query. |`tilebank` |Loading a baked tile bank both ways, and a bank for another machine refused. |`serial` |The optional RS-232 add-on. |`adventure` |A small complete game. -|`agi` |A Sierra AGI interpreter, showing the library under real load. -|`spacetaxi` |A full port of Space Taxi: the largest example. |`uber` |The validation and benchmark harness. |=== +Two complete games built on JoeyLib live in repositories of their own and +show the library under real load: Space Taxi, a port of the 1984 Commodore 64 +game, and JoeyAGI, an interpreter for Sierra's AGI adventures. Each builds +from its own directory against a JoeyLib checkout, as any program of yours +would (<<_building_your_program>>). + On DVX every example is a folder of its own under `build/dvx/bin/apps/joeylib/` with its help file (<<_dvx_programs>>). diff --git a/docs/Overview.adoc b/docs/Overview.adoc index 341e361..df0f8a6 100644 --- a/docs/Overview.adoc +++ b/docs/Overview.adoc @@ -118,8 +118,8 @@ chapters are the whole programming model. <<_creating_assets>> covers getting graphics, music and sound into a form the library can load. -<<_adding_your_program_to_the_build>> explains how to get your own program -compiling for every target. +<<_building_your_program>> explains how to get your own program, in a +directory of its own, compiling for every target. The API Reference documents every public function: its exact signature, each parameter, what it returns, and any per-platform behaviour you need to know diff --git a/examples/agi/agi.c b/examples/agi/agi.c deleted file mode 100644 index 13e2f8b..0000000 --- a/examples/agi/agi.c +++ /dev/null @@ -1,2086 +0,0 @@ -// JoeyLib AGI interpreter, Phase 2 host. The VM in agiVm.c owns all -// game state (vars, flags, objects, text, menus); the host renders -// the VM's snapshot every frame and bridges VM callbacks to the -// platform (resource I/O, picture decoding, text rendering, key -// dispatch). One VM cycle runs every VM_FRAMES_PER_CYCLE display -// frames; per-real-second the host pumps agiVmTickSeconds so the -// engine clock advances and per-second logic.0 branches fire. -// -// Argv layout: -// argv[1] = game directory (defaults to ".") -// argv[2] = starting room (optional; 1..255 overrides logic.0's -// hardcoded new.room target so you can skip past a title -// screen the VM can't yet advance through. 0 means "use -// whatever logic.0 picks", same as omitting the argument.) - - -#include "agi.h" - -#include "joey/audio.h" -#include "joey/core.h" -#include "joey/debug.h" -#include "joey/draw.h" -#include "joey/input.h" -#include "joey/palette.h" -#include "joey/platform.h" -#include "joey/present.h" -#include "joey/sprite.h" -#include "joey/surface.h" -#include "surfaceInternal.h" - -#include -#include -#include - - -#define AGI_LOGIC_HDR_BYTES 2u -// Border fill color (rows above/below the AGI picture region and any -// area not painted by show.pic). AGI canonical: black (palette 0). -#define COLOR_MISSING 0u -#define CYCLE_HALT_GUARD 16u -#define EGO_START_X 80 -#define EGO_START_Y 140 -#define EGO_STEP_PX 2 - -// AGI v2 EGA layout on a 320x200 display: pic occupies y=0..167 -// (160x168 doubled horizontally to 320 wide), and y=168..199 is the -// 4-text-row band below the picture. Text rows always use y = row*8, -// so pic at y=0 keeps the in-picture rows (0..20) visually aligned -// with the text rendered into them. Status line, when on, overlays -// the picture at its assigned row -- that's Sierra's canonical -// behaviour. -// Canonical AGI: 1 jiffy = 50ms, VM cycle dispatch every v10 -// jiffies. KQ3 sets v10=1 -> 20 cycles/sec; default is 2 -> -// 10 cycles/sec. We drive the cycle off wall-clock so the -// rate stays correct regardless of host frame rate. -#define AGI_JIFFY_MS 50u -// Renders are wall-clock paced too. AGI canonical runs at ~20 Hz -// (one VM cycle = one rendered frame in Sierra's interpreter); we -// render slightly above that for smoother sparkle/cel cycling but -// not the full 70 Hz VBL rate, which on emulated 386 in DOSBox -// burns enough CPU on backdrop-restore + cel redraw that audio -// refill misses its 92 ms half-buffer deadline. -#define AGI_RENDER_PERIOD_MS 33u -#define STARTUP_CYCLE_LIMIT 32u -// Per Sierra KQ3 v2 EGA reference: pic starts 8 pixels down from -// the top of the 320x200 display (one 8-pixel text-row band reserved -// for the status line). Pic occupies y=8..175. Text rows use the -// row*8 absolute Y -- no pic offset added -- so they deliberately -// overlap pic art where the script wants captions inside the picture. -#define PIC_DEST_Y 8 - -// (Per-object save-under removed: we now restore each object's prev -// rect by memcpy from the gBackdrop surface, sized to the actual -// cel bbox at draw time. That handles arbitrary cel sizes without -// any per-object backup buffers and is faster on chunky platforms.) - -// Canonical AGI ego loop indices. -#define LOOP_RIGHT 0 -#define LOOP_LEFT 1 -#define LOOP_DOWN 2 -#define LOOP_UP 3 - -// Encryption key applied per-message in the AGI v2 message section. -// Same string in every supported game; XOR cycles every 11 bytes. -#define MSG_KEY "Avis Durgan" -#define MSG_KEY_LEN 11u - -// Largest decrypted message we'll surface to the VM. Game scripts -// stay well under this (most under 80 chars); the cap protects the -// scratch buffer from a corrupt resource. -#define MSG_BUFFER_BYTES 256u - - -// ----- Types ----- - -typedef struct { - uint8_t *raw; - uint16_t rawLength; - const uint8_t *bytecode; - uint16_t bytecodeLength; -} LogicCacheSlotT; - -typedef struct { - AgiViewT view; - bool loaded; -} ViewCacheSlotT; - - -// ----- Prototypes ----- - -static void agiSoundReset(void); -static uint32_t agiSynthFill(void *ctx, int8_t *dst, uint32_t count); -static void agiTickAdvance(void); -static void agiVoiceTick(uint8_t voice); -static void psgNoiseApply(void); -static void psgRecompute(uint8_t chipCh); -static void psgWriteData(uint8_t val); -static void cbAddToPic(void *ctx, uint8_t viewId, uint8_t loop, uint8_t cel, - uint8_t x, uint8_t y, uint8_t pri, uint8_t margin); -static void cbDiscardPic(void *ctx, uint8_t picId); -static void cbDrawPic(void *ctx, uint8_t picId); -static const uint8_t *cbFetchLogic(void *ctx, uint8_t logicId, uint16_t *outLength); -static const char *cbFetchMessage(void *ctx, uint8_t logicId, uint8_t msgId); -static bool cbHaveKey(void *ctx); -static bool cbIsPlayingSound(void *ctx); -static void cbLoadPic(void *ctx, uint8_t picId); -static void cbOverlayPic(void *ctx, uint8_t picId); -static void cbPlaySound(void *ctx, uint8_t soundId); -static void cbShowPic(void *ctx); -static uint32_t cbSoundDuration(void *ctx, uint8_t soundId); -static void cbStopSound(void *ctx); -static uint8_t cbViewCelCount(void *ctx, uint8_t viewId, uint8_t loopId); -static uint8_t cbViewLoopCount(void *ctx, uint8_t viewId); -static bool decodePicInto(uint8_t picId, bool clearFirst); -static void agiInputCapture(void); -static void agiParseCommand(void); -static void dispatchAllPressedKeys(void); -static void drawObjects(jlSurfaceT *stage); -static const AgiViewT *fetchView(uint8_t viewId); -static void invalidateObjectSaves(void); -static uint8_t loopForDirection(uint8_t direction, uint8_t loopCount); -static uint8_t parseArgU8(const char *s); -static void releaseLogicCache(void); -static void releaseViewCache(void); -static void renderFrame(jlSurfaceT *stage); -static uint8_t resolveStartingRoom(int argc, char **argv); -static void runVmCycle(void); -static void updateEgoFromInput(void); - - -// ----- Module state ----- - -static AgiGameT gGame; -static AgiPicT gPic; -static jlSurfaceT *gBackdrop; -static jlSurfaceT *gAgiStage; -static AgiVmT gVm; -static LogicCacheSlotT gLogicCache[AGI_MAX_RESOURCES]; -// View cache: 256 slots of ~5 KB each (~1.3 MB). Far too large for the -// IIgs bank-0 BSS, so it is jlAlloc'd at startup (Memory Manager handle -// on the IIgs, malloc elsewhere) rather than declared as a static array. -static ViewCacheSlotT *gViewCache; -static bool gPicReady; -static uint8_t gOverrideRoom; -static bool gOverrideRoomActive; -static bool gAnyKeyHit; -// Per-object previously-drawn stage rect. After each draw we record -// the cel's exact stage bbox here; next frame's pass 1 restores that -// rect from gBackdrop, then pass 2 redraws. valid=false means the -// object wasn't drawn last frame (skip restore). -typedef struct { - int16_t x; - int16_t y; - int16_t w; - int16_t h; - bool valid; -} ObjPrevRectT; - -static ObjPrevRectT gObjPrev[AGI_MAX_OBJECTS]; -// Per-text-row "had content last frame" flag. We restore each row's -// stripe of the backdrop at the start of every frame if it was -// dirtied last frame, so the old text disappears when the VM clears -// it (clear.lines) or moves to a different row. -static bool gTextRowDirty[AGI_TEXT_ROWS]; -// One static scratch for the most recently decrypted message. The -// fetchMessage callback contract guarantees its return pointer is -// only valid until the next call -- the VM's expandMessage copies -// the bytes it needs before calling fetchMessage again. -static char gMessageScratch[MSG_BUFFER_BYTES]; - -// SN76489 PSG clock divisor base. AGI v2 uses the TI PSG's tone -// generator: tone_hz = 111860 / divisor (3.579545 MHz / 32). -#define PSG_CLOCK_HZ 111860u - -// AGI sound is software-synthesized at 22050 Hz mono then pushed -// through the existing Sound Blaster auto-init DMA path via the -// jlAudioPlaySfxStream API. Three independent squarewave voices -// (16-bit phase, 8-bit amplitude) sum into the output stream; AGI -// tick logic advances every 22050/60 = 367 output samples, matching -// ScummVM's PCjr/Tandy sound model (which is also how Sierra's own -// SB drivers shipped the AGI sound after 1988). -// -// This is the "software synth to PCM" model. Pros: works on any -// SB-equipped DOS box, no hardware-IRQ headaches, sample-accurate -// note timing because we're rendering audio at 22050 Hz with the -// AGI ticks embedded. Cons: requires SB-compat audio (true for -// every target since 1989). The PCjr/Tandy native PSG path (cheaper, -// older) is a separate problem we can revisit later. -// -// SND v2 layout (per channel): -// bytes 0..7 : 4 channel offsets (LE16 each) -// per channel : sequence of 5-byte events. Bytes 0..1 = duration -// LE16 in 1/60s units, terminator 0xFFFF; bytes 2/3 -// = raw SN76489 latch+data pair encoding the 10-bit -// frequency divisor (low 4 bits in byte 3, high 6 -// bits in byte 2); byte 4 = 4-bit attenuation -// (0 = loudest, 15 = silent). -// freq_hz = 111860 / divisor (PSG clock 3.579545 MHz / 32). -// -// Sample rate must match the SB DMA setup (see MIX_RATE in -// src/port/dos/audio.c). 22050 keeps 256-sample stream chunks at -// ~12 ms each, well over an AGI tick. -typedef struct { - uint16_t chOffset; // cursor in this channel's event stream - uint16_t eventFramesRem; // ticks until current event ends - bool done; // channel hit 0xFFFF terminator -} AgiVoiceSchedT; - -// Per-SN76489-channel state. The chip has 3 tone channels + 1 noise -// channel. Each event in the SND stream is 3 raw chip-command bytes -// (b4=atten, b3=latch-or-noise-control, b2=data) that may target -// any chip channel, not necessarily the AGI channel that owns the -// stream. We dispatch the bytes through psgWriteData (mirrors -// ScummVM's writeData) and let the chip-state machine update the -// right register. The synth then reads each tone channel's derived -// (amplitude, phaseInc) and produces samples. -typedef enum { - PSG_GEN_TONE = 0, - PSG_GEN_NOISE_PERIOD = 1, - PSG_GEN_NOISE_WHITE = 2 -} PsgGenE; - -typedef struct { - uint16_t freqCount; // 10-bit tone period; noise uses small values per ScummVM - uint8_t attenuation; // 0=loud, 15=silent - uint8_t genType; // PsgGenE - // Derived cache (recomputed when freqCount/attenuation change): - uint16_t phaseInc; - int8_t amplitude; - // Synth state: - uint16_t phase; -} PsgChannelT; - -// AGI v2 SND has 4 sub-channels: 3 tone + 1 noise. The "AGI -// channel" indexes into the event-stream offsets table at the -// start of the file; it does NOT correspond 1:1 to the chip's 4 -// channels. Each AGI channel ticks an event stream and fires raw -// chip writes; the chip state is shared across all AGI channels. -#define AGI_SOUND_CHANNELS 4u -#define AGI_SOUND_TONE_VOICES 3u -#define AGI_SOUND_NOISE_CHANNEL 3u -#define AGI_SOUND_END_MARKER 0xFFFFu -// AGI sound tick rate. Sierra's PCjr/Tandy SOUND.DRV ran at 120 Hz -// (vs ScummVM's 60 Hz). All script-driven timing (v11 seconds -// counter, animation cycling, motion stepping) is derived from -// this same tick rate, NOT from wall clock -- so doubling the -// tick rate makes both the music AND v11 advance 2x faster, which -// keeps script-internal wait conditions consistent. -#define AGI_SOUND_TICKS_HZ 120u -#define AGI_SYNTH_SAMPLE_RATE 22050u -#define AGI_SYNTH_SAMPLES_PER_TICK (AGI_SYNTH_SAMPLE_RATE / AGI_SOUND_TICKS_HZ) - -// SN76489 attenuation -> int8 amplitude lookup. 2 dB per step -// (so atten=1 is 0.794x atten=0, atten=2 is 0.794x atten=1, etc.). -// Linear (15-atten)/15 mapping makes mid-volume notes far too loud -// and turns fade-outs into harsh cliffs; this table matches -// ScummVM's int16 PSG table scaled down to our 40-peak headroom -// for safe 3-voice sums into int8 PCM. -static const int8_t kAgiAttenAmp[16] = { - 40, 32, 25, 20, 16, 13, 10, 8, - 6, 5, 4, 3, 3, 2, 2, 0 -}; - -// All synth + scheduler state runs entirely on the main thread now -// (the stream fill callback is invoked from halAudioFrameTick, not -// from any IRQ). No critical sections needed. -static uint8_t *gSoundBytes; -static uint16_t gSoundLength; -static AgiVoiceSchedT gVoiceSched[AGI_SOUND_CHANNELS]; -static PsgChannelT gPsg[4]; // 3 tone + 1 noise -static uint8_t gPsgReg; // last register selected by a LATCH or ATTEN byte -static bool gSoundActive; -// AGI ticks are driven from wall-clock ms, NOT from samples -// produced. The previous countdown-per-sample approach assumed -// the synth's call rate matched SB DMA's drain rate (22050/sec). -// In practice DOSBox's SB Pro emulation at cycles=2200 drains -// the DMA buffer roughly 2.1x faster than the requested time -// constant (or fires refill IRQs at half the block-size cadence, -// or some combination -- empirically observed 47-50 kHz -// production rate when we asked for 22050). Pitch is unaffected -// because phaseInc still matches the actual DMA rate, but tempo -// (ticks/sec) was running 2.1x fast. Wall-clock tick gating -// locks tempo to 60 Hz exactly regardless of DMA quirks. -#define AGI_TICK_PERIOD_US (1000000u / AGI_SOUND_TICKS_HZ) // 8333 us at 120 Hz -// Tick counter advanced by agiTickAdvance() from the host loop. -// All AGI script-driven timing keys off this, NOT off wall clock, -// so changing AGI_SOUND_TICKS_HZ uniformly speeds up music + v11 -// + animation + script waits together. v11 advances every 60 AGI -// ticks (= 1 "AGI sec"); at AGI_SOUND_TICKS_HZ=120 that's twice -// per wall second, matching Sierra's PCjr/Tandy game-tick model. -static uint32_t gAgiTickAccumUs; -static uint32_t gAgiTickLastWallMs; -static uint32_t gAgiV11SubTickAccum; // counts AGI ticks toward next v11++ - - -// ----- Internal helpers (alphabetical) ----- - -static void cbAddToPic(void *ctx, uint8_t viewId, uint8_t loop, uint8_t cel, - uint8_t x, uint8_t y, uint8_t pri, uint8_t margin) { - const AgiViewT *view; - - (void)ctx; - view = fetchView(viewId); - if (view == NULL) { - return; - } - // Mutate the in-memory pic buffer only. The companion show.pic - // call (which always follows an add.to.pic batch in canonical - // AGI logic) is what blits the updated buffer to the stage and - // republishes the backdrop. Per-add.to.pic blits would compound - // into 100s of ms of redundant pixel work on slow targets. - agiPicAddView(&gPic, view, loop, cel, (int16_t)x, (int16_t)y, pri, margin); -} - - -// Accumulate typed characters into gVm.inputLine while the command line is -// active (accept.input). Printable keys append, backspace deletes, and Enter -// hands the finished line to agiParseCommand. Called every frame from the -// main loop; the on-screen echo is drawn in renderFrame. -static void agiInputCapture(void) { - uint8_t k; - - if (jlKeyPressed(KEY_BACKSPACE)) { - if (gVm.inputLen > 0u) { - gVm.inputLen--; - gVm.inputLine[gVm.inputLen] = '\0'; - } - } - if (jlKeyPressed(KEY_SPACE) && gVm.inputLen < AGI_INPUT_MAX) { - gVm.inputLine[gVm.inputLen] = ' '; - gVm.inputLen++; - gVm.inputLine[gVm.inputLen] = '\0'; - } - for (k = 0u; k < 26u; k++) { - if (jlKeyPressed((jlKeyE)((uint8_t)KEY_A + k)) && gVm.inputLen < AGI_INPUT_MAX) { - gVm.inputLine[gVm.inputLen] = (char)('a' + k); - gVm.inputLen++; - gVm.inputLine[gVm.inputLen] = '\0'; - } - } - for (k = 0u; k < 10u; k++) { - if (jlKeyPressed((jlKeyE)((uint8_t)KEY_0 + k)) && gVm.inputLen < AGI_INPUT_MAX) { - gVm.inputLine[gVm.inputLen] = (char)('0' + k); - gVm.inputLen++; - gVm.inputLine[gVm.inputLen] = '\0'; - } - } - if (jlKeyPressed(KEY_RETURN)) { - if (gVm.inputLen > 0u) { - agiParseCommand(); - } - gVm.inputLine[0] = '\0'; - gVm.inputLen = 0u; - } -} - - -// Tokenize gVm.inputLine into dictionary group IDs (gVm.parsedWords), using -// greedy longest-match so multi-word entries ("crystal ball", "get in") beat -// their prefixes. Ignore-words (group 0) drop out; a word absent from the -// dictionary records its 1-based position in var 9 and voids the command -// (no parsedWords, so said() won't match and the room can report the miss). -// Sets flag 2 so said() tests fire this cycle. -static void agiParseCommand(void) { - static char toks[AGI_MAX_PARSED][AGI_WORD_MAX_LEN + 1u]; - static uint8_t tokLen[AGI_MAX_PARSED]; - char phrase[AGI_WORD_MAX_LEN + 1u]; - uint8_t tokCount; - uint8_t ti; - uint16_t i; - uint8_t pos; - bool unknown; - - // Split into lowercase alphanumeric tokens (cap at AGI_MAX_PARSED). - tokCount = 0u; - ti = 0u; - for (i = 0u; i <= gVm.inputLen; i++) { - char c; - - c = (i < gVm.inputLen) ? gVm.inputLine[i] : ' '; - if (c >= 'A' && c <= 'Z') { - c = (char)(c + ('a' - 'A')); - } - if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) { - if (tokCount < AGI_MAX_PARSED && ti < AGI_WORD_MAX_LEN) { - toks[tokCount][ti] = c; - ti++; - } - continue; - } - if (ti > 0u && tokCount < AGI_MAX_PARSED) { - toks[tokCount][ti] = '\0'; - tokLen[tokCount] = ti; - tokCount++; - } - ti = 0u; - } - - gVm.parsedWordCount = 0u; - gVm.vars[AGI_VAR_UNKNOWN_WORD] = 0u; - unknown = false; - pos = 0u; - while (pos < tokCount) { - uint16_t bestId; - uint8_t best; - uint8_t span; - - best = 0u; - bestId = AGI_WORD_UNKNOWN; - // Longest join first: try tokens[pos..end], then shorter, down to one. - for (span = (uint8_t)(tokCount - pos); span >= 1u; span--) { - uint8_t pl; - uint8_t s; - uint16_t id; - - pl = 0u; - for (s = 0u; s < span; s++) { - uint8_t j; - - if (s > 0u && pl < AGI_WORD_MAX_LEN) { - phrase[pl] = ' '; - pl++; - } - for (j = 0u; j < tokLen[pos + s] && pl < AGI_WORD_MAX_LEN; j++) { - phrase[pl] = toks[pos + s][j]; - pl++; - } - } - phrase[pl] = '\0'; - id = agiWordId(&gGame, phrase); - if (id != AGI_WORD_UNKNOWN) { - best = span; - bestId = id; - break; - } - } - if (best == 0u) { - if (!unknown) { - gVm.vars[AGI_VAR_UNKNOWN_WORD] = (uint8_t)(pos + 1u); - unknown = true; - } - pos++; - continue; - } - if (bestId != AGI_WORD_IGNORE && gVm.parsedWordCount < AGI_MAX_PARSED) { - gVm.parsedWords[gVm.parsedWordCount] = bestId; - gVm.parsedWordCount++; - } - pos = (uint8_t)(pos + best); - } - - if (unknown) { - gVm.parsedWordCount = 0u; - } - gVm.flags[AGI_FLAG_INPUT] = 1u; -} - - -static void cbDiscardPic(void *ctx, uint8_t picId) { - (void)ctx; - (void)picId; -} - - -static void cbDrawPic(void *ctx, uint8_t picId) { - uint8_t i; - - (void)ctx; - jlLogF("cbDrawPic id=%u v11=%u", (unsigned)picId, (unsigned)gVm.vars[11]); - // Dump active objects so we can see whether scene leftovers - // (e.g. sparkle objects from previous scene) are still flagged - // animated/drawn at scene-change time. - for (i = 0u; i < (uint8_t)AGI_MAX_OBJECTS; i++) { - const AgiObjectT *o = &gVm.objects[i]; - if (o->flags != 0u) { - jlLogF(" obj[%u] flags=0x%02x view=%u loop=%u cel=%u x=%d y=%d", - (unsigned)i, (unsigned)o->flags, - (unsigned)o->viewId, (unsigned)o->loop, (unsigned)o->cel, - (int)o->x, (int)o->y); - } - } - // Dump set flags so we can compare flag state across scene - // transitions and identify which flag gates a wrong-path branch. - { - uint16_t fi; - char line[128]; - size_t used; - line[0] = '\0'; - used = 0u; - for (fi = 0u; fi < AGI_VM_NUM_FLAGS; fi++) { - if (gVm.flags[fi] != 0u) { - int n = snprintf(line + used, sizeof(line) - used, "%u ", (unsigned)fi); - if (n < 0 || (size_t)n >= sizeof(line) - used) { - jlLogF(" flagsSet=%s...(trunc)", line); - line[0] = '\0'; - used = 0u; - } else { - used += (size_t)n; - } - } - } - jlLogF(" flagsSet=%s", line); - } - jlLogFlush(); - decodePicInto(picId, true); -} - - -static const uint8_t *cbFetchLogic(void *ctx, uint8_t logicId, uint16_t *outLength) { - LogicCacheSlotT *slot; - uint8_t *raw; - uint16_t rawLength; - uint16_t bcLen; - - (void)ctx; - slot = &gLogicCache[logicId]; - if (slot->bytecode != NULL) { - *outLength = slot->bytecodeLength; - return slot->bytecode; - } - if (logicId >= gGame.resCount[AGI_RES_LOGIC]) { - return NULL; - } - raw = agiResLoad(&gGame, AGI_RES_LOGIC, (uint16_t)logicId, &rawLength); - if (raw == NULL || rawLength < AGI_LOGIC_HDR_BYTES) { - if (raw != NULL) { - free(raw); - } - return NULL; - } - bcLen = (uint16_t)(raw[0] | ((uint16_t)raw[1] << 8)); - if ((uint16_t)(AGI_LOGIC_HDR_BYTES + bcLen) > rawLength) { - free(raw); - return NULL; - } - slot->raw = raw; - slot->rawLength = rawLength; - slot->bytecode = raw + AGI_LOGIC_HDR_BYTES; - slot->bytecodeLength = bcLen; - *outLength = bcLen; - return slot->bytecode; -} - - -// Look up message `msgId` in logic `logicId`'s message table, decrypt -// it via the cycling "Avis Durgan" XOR, write the result to the -// shared scratch buffer, and return a pointer to it. Returns NULL -// on missing message; returns "" for an empty slot. -// -// AGI v2 quirks the spec requires: -// 1. The 2-byte offsets stored in the table are relative to byte 1 -// of the message section (i.e. they exclude the count byte), so -// the actual byte position is stored_offset + 1. -// 2. The XOR key cycles continuously across the whole strings -// block, not per-message. Key index for byte K = (K - strings -// start byte) mod 11. -static const char *cbFetchMessage(void *ctx, uint8_t logicId, uint8_t msgId) { - LogicCacheSlotT *slot; - const uint8_t *raw; - const uint8_t *msgSection; - uint16_t msgSectionOff; - uint16_t rawLength; - uint16_t msgCount; - uint16_t msgOff; - uint16_t stringsStartOff; - uint16_t k; - uint16_t i; - - (void)ctx; - slot = &gLogicCache[logicId]; - if (slot->raw == NULL) { - uint16_t ignore = 0u; - if (cbFetchLogic(NULL, logicId, &ignore) == NULL) { - return NULL; - } - } - raw = slot->raw; - rawLength = slot->rawLength; - msgSectionOff = (uint16_t)(AGI_LOGIC_HDR_BYTES + slot->bytecodeLength); - if (msgSectionOff >= rawLength) { - return NULL; - } - msgSection = raw + msgSectionOff; - msgCount = (uint16_t)msgSection[0]; - if (msgId == 0u || msgId > msgCount) { - return NULL; - } - // Find the lowest non-zero stored offset across all message - // slots. That's where the encrypted strings block begins. - stringsStartOff = 0xFFFFu; - for (k = 1u; k <= msgCount; k++) { - uint16_t off; - uint16_t tOff = (uint16_t)(3u + (uint16_t)((k - 1u) * 2u)); - if ((uint16_t)(msgSectionOff + tOff + 1u) >= rawLength) { - continue; - } - off = (uint16_t)(msgSection[tOff] | ((uint16_t)msgSection[tOff + 1u] << 8)); - if (off != 0u && off < stringsStartOff) { - stringsStartOff = off; - } - } - if (stringsStartOff == 0xFFFFu) { - gMessageScratch[0] = '\0'; - return gMessageScratch; - } - { - uint16_t tableOff; - uint16_t msgStart; - uint16_t keyBase; - - tableOff = (uint16_t)(3u + (uint16_t)((msgId - 1u) * 2u)); - if ((uint16_t)(msgSectionOff + tableOff + 1u) >= rawLength) { - return NULL; - } - msgOff = (uint16_t)(msgSection[tableOff] | ((uint16_t)msgSection[tableOff + 1u] << 8)); - if (msgOff == 0u) { - gMessageScratch[0] = '\0'; - return gMessageScratch; - } - // actual byte = section_off + stored_offset + 1. - msgStart = (uint16_t)(msgSectionOff + msgOff + 1u); - if (msgStart >= rawLength) { - return NULL; - } - // continuous key index = (msgOff - stringsStartOff) at byte 0 - // of this message, then increments per byte. - keyBase = (uint16_t)(msgOff - stringsStartOff); - i = 0u; - while (msgStart + i < rawLength && i < (uint16_t)(MSG_BUFFER_BYTES - 1u)) { - uint8_t enc; - uint8_t dec; - - enc = raw[msgStart + i]; - dec = (uint8_t)(enc ^ (uint8_t)MSG_KEY[(keyBase + i) % MSG_KEY_LEN]); - if (dec == 0u) { - break; - } - gMessageScratch[i] = (char)dec; - i++; - } - gMessageScratch[i] = '\0'; - } - return gMessageScratch; -} - - -static bool cbHaveKey(void *ctx) { - bool hit; - - (void)ctx; - hit = gAnyKeyHit; - gAnyKeyHit = false; - return hit; -} - - -static void cbLoadPic(void *ctx, uint8_t picId) { - (void)ctx; - (void)picId; -} - - -static void cbOverlayPic(void *ctx, uint8_t picId) { - (void)ctx; - jlLogF("cbOverlayPic id=%u v11=%u", (unsigned)picId, (unsigned)gVm.vars[11]); - decodePicInto(picId, false); -} - - -static void cbShowPic(void *ctx) { - (void)ctx; - jlLogF("cbShowPic v11=%u", (unsigned)gVm.vars[11]); - if (gAgiStage == NULL) { - return; - } - // Clear the WHOLE stage first so the rows above/below the AGI - // picture region (y=0..15 and y=184..199) don't retain pixels - // from the previous scene. Without this, text drawn during the - // logo phase at rows 22..24 ends up captured by the backdrop - // copy below and gets restored over every subsequent frame -- - // so clear.lines never visually erases anything that sat in - // the border bands. - jlSurfaceClear(gAgiStage, COLOR_MISSING); - agiPicBlit(&gPic, gAgiStage, PIC_DEST_Y); - if (gBackdrop != NULL) { - jlSurfaceCopy(gBackdrop, gAgiStage); - } - gPicReady = true; - // The stage just got repainted; any prior save-under refers to - // pre-paint pixels and must NOT be restored next frame (it would - // leak the old picture over the new one). - invalidateObjectSaves(); -} - - -// Push the chip's noise-channel state (gPsg[3]) to the JoeyLib noise -// voice. The SN76489 freqCount maps onto the portable 0..31 pitch as -// freqCount >> 3 (rate codes 0/1/2 -> 4/8/16, preserving the chip's -// octave-per-step; mode 3 tracks tone channel 2's snapshot). The -// white/periodic bit has no portable representation and renders as -// plain hiss. -static void psgNoiseApply(void) { - uint16_t count; - uint8_t pitch; - - if (gPsg[3].attenuation >= 15u || gPsg[3].freqCount == 0u) { - jlAudioNoise(0u, 15u); - return; - } - count = gPsg[3].freqCount; - pitch = (uint8_t)(count >> 3); - if (pitch > 31u) { - pitch = 31u; - } - jlAudioNoise(pitch, gPsg[3].attenuation); -} - - -// Recompute derived (phaseInc, amplitude) for chip channel -// `chipCh` from its current freqCount + attenuation + genType. -// Called whenever the chip state for a tone channel changes; the -// noise channel isn't synthesized into the PCM stream -- it goes to -// the hardware noise voice instead. -static void psgRecompute(uint8_t chipCh) { - PsgChannelT *p; - uint32_t freqHz; - - if (chipCh >= 3u) { - psgNoiseApply(); - return; - } - p = &gPsg[chipCh]; - if (p->genType != PSG_GEN_TONE || p->freqCount == 0u || p->attenuation >= 15u) { - p->phaseInc = 0u; - p->amplitude = 0; - return; - } - freqHz = (uint32_t)PSG_CLOCK_HZ / (uint32_t)p->freqCount; - p->phaseInc = (uint16_t)((freqHz * 65536u) / AGI_SYNTH_SAMPLE_RATE); - p->amplitude = kAgiAttenAmp[p->attenuation]; -} - - -// SN76489 byte dispatcher, byte-for-byte equivalent to ScummVM's -// SoundGenPCJr::writeData. The chip has 4 channels (3 tone + 1 -// noise) and a single "last register" latch used by DATA bytes -// to know which channel to update. -// -// Byte forms (per the SN76489 datasheet): -// 1rrr1xxx (atten): bits 6-5 = chip channel, bits 3-0 = atten. -// Updates _reg and the channel's attenuation. -// 11100xxx (noise): noise channel (3); bit 2 = white/period, -// bits 1-0 = period code. _reg NOT updated -- -// the DATA byte that follows still targets -// whatever channel was last selected by a tone -// latch or atten command. -// 1rrr0xxx (tone latch): bits 6-5 = chip channel, bits 3-0 = -// LOW 4 bits of freqCount. Sets _reg, fully -// replaces low nibble, marks gen as tone. -// 0xxxxxxx (data): ORs (val & 0x3F) << 4 into freqCount of -// whichever channel _reg points to (the high -// 6 bits of the 10-bit divisor). The OR is -// key: an event without a fresh tone latch -// keeps its previous low nibble, so two -// events can share a low-nibble setup and a -// noise-control byte in between leaves the -// tone channel's freqCount mostly intact. -static void psgWriteData(uint8_t val) { - uint8_t reg; - - if ((val & 0x90u) == 0x90u) { - gPsgReg = (uint8_t)((val >> 5) & 0x03u); - gPsg[gPsgReg].attenuation = (uint8_t)(val & 0x0Fu); - psgRecompute(gPsgReg); - } else if ((val & 0xF0u) == 0xE0u) { - gPsg[3].genType = (val & 0x04u) ? PSG_GEN_NOISE_WHITE : PSG_GEN_NOISE_PERIOD; - switch (val & 0x03u) { - case 0u: gPsg[3].freqCount = 32u; break; - case 1u: gPsg[3].freqCount = 64u; break; - case 2u: gPsg[3].freqCount = 128u; break; - default: gPsg[3].freqCount = (uint16_t)(gPsg[2].freqCount * 2u); break; - } - // No _reg update -- the DATA byte that follows still targets - // the previously latched channel. - psgNoiseApply(); - } else if (val & 0x80u) { - reg = (uint8_t)((val >> 5) & 0x03u); - gPsgReg = reg; - gPsg[reg].freqCount = (uint16_t)(val & 0x0Fu); - gPsg[reg].genType = PSG_GEN_TONE; - psgRecompute(reg); - } else { - gPsg[gPsgReg].freqCount |= (uint16_t)((val & 0x3Fu) << 4); - psgRecompute(gPsgReg); - } -} - - -// Silence every channel and free the owned SND buffer. Called from -// main thread; stops the SFX-stream slot first so the synth -// callback can't be invoked again against a freed buffer. -static void agiSoundReset(void) { - uint8_t v; - - jlAudioStopSfx(0u); - // The noise voice is live hardware, separate from the PCM stream - // slot -- without an explicit off it latches on forever. - jlAudioNoise(0u, 15u); - for (v = 0u; v < AGI_SOUND_CHANNELS; v++) { - gVoiceSched[v].chOffset = 0u; - gVoiceSched[v].eventFramesRem = 0u; - gVoiceSched[v].done = true; - } - for (v = 0u; v < 4u; v++) { - gPsg[v].freqCount = 0u; - gPsg[v].attenuation = 15u; // silent - gPsg[v].genType = PSG_GEN_TONE; - gPsg[v].phaseInc = 0u; - gPsg[v].amplitude = 0; - gPsg[v].phase = 0u; - } - gPsgReg = 0u; - gSoundActive = false; - gAgiTickAccumUs = 0u; - gAgiTickLastWallMs = 0u; - gAgiV11SubTickAccum = 0u; - if (gSoundBytes != NULL) { - free(gSoundBytes); - gSoundBytes = NULL; - } - gSoundLength = 0u; -} - - -// Advance one AGI channel by one tick. When the channel's -// duration counter rolls over, parse the 5-byte event header -// (dur LE16 + 3 chip-command bytes) and feed the 3 chip bytes -// through psgWriteData in ScummVM's order: byte 4 (atten), byte -// 3 (latch or noise control), byte 2 (data). The chip state is -// shared across all AGI channels, so an event in AGI channel N -// can update chip-channel state for any chip register, including -// the noise channel and other tone channels' attenuation. -// -// dur=0 events fire-then-skip in the same tick (AGI uses them for -// legato note transitions). Inner-loop guard caps iterations so -// a malformed channel can't lock the synth. -static void agiVoiceTick(uint8_t v) { - AgiVoiceSchedT *sch; - uint16_t dur; - uint8_t guard; - - sch = &gVoiceSched[v]; - if (sch->done) { - return; - } - guard = 16u; - while (sch->eventFramesRem == 0u && guard != 0u) { - guard--; - if ((uint32_t)sch->chOffset + 4u >= (uint32_t)gSoundLength) { - sch->done = true; - return; - } - dur = (uint16_t)(gSoundBytes[sch->chOffset] | - ((uint16_t)gSoundBytes[sch->chOffset + 1u] << 8)); - if (dur == AGI_SOUND_END_MARKER) { - sch->done = true; - return; - } - // ScummVM order: attenuation byte first, then latch, then - // data. Critical because the noise-control byte (0xE0-0xEF) - // does NOT update _reg, so the data byte that follows must - // target whatever channel the atten byte selected. - psgWriteData(gSoundBytes[sch->chOffset + 4u]); - psgWriteData(gSoundBytes[sch->chOffset + 3u]); - psgWriteData(gSoundBytes[sch->chOffset + 2u]); - sch->chOffset = (uint16_t)(sch->chOffset + 5u); - sch->eventFramesRem = dur; - // Light per-event trace (no flush; buffered). Useful when - // diagnosing whether a "weird" note is from the SND data - // itself or from our chip emulator. - jlLogF(" agiV%u @%u dur=%u b234=%02x %02x %02x | psg ch0/1/2/N freq=%u/%u/%u/%u atten=%u/%u/%u/%u _reg=%u", - (unsigned)v, (unsigned)(sch->chOffset - 5u), (unsigned)dur, - (unsigned)gSoundBytes[sch->chOffset - 3u], - (unsigned)gSoundBytes[sch->chOffset - 2u], - (unsigned)gSoundBytes[sch->chOffset - 1u], - (unsigned)gPsg[0].freqCount, (unsigned)gPsg[1].freqCount, - (unsigned)gPsg[2].freqCount, (unsigned)gPsg[3].freqCount, - (unsigned)gPsg[0].attenuation, (unsigned)gPsg[1].attenuation, - (unsigned)gPsg[2].attenuation, (unsigned)gPsg[3].attenuation, - (unsigned)gPsgReg); - } - if (sch->eventFramesRem != 0u) { - sch->eventFramesRem = (uint16_t)(sch->eventFramesRem - 1u); - } -} - - -// Host-loop tick generator. Drives ALL AGI-time-sensitive things -// (music event advancement, v11 seconds counter increment) off the -// same wall-clock-paced tick stream. Sierra's interpreter does the -// same: one tick = one PSG event step AND one 60th of an "AGI -// second", so v11 stays in lockstep with the music regardless of -// what AGI_SOUND_TICKS_HZ is set to. Called once per host frame. -static void agiTickAdvance(void) { - static uint32_t gLastObjDumpMs = 0; - uint32_t nowMs = jlMillisElapsed(); - uint32_t deltaMs; - uint8_t v; - - // Periodic dump of currently-drawn objects so we can see what's - // visible at any moment without having to log per-opcode. - if (nowMs - gLastObjDumpMs >= 2000u || gLastObjDumpMs == 0u) { - uint8_t i; - jlLogF("--- obj dump @wallMs=%u v11=%u v36=%u v221=%u v222=%u v223=%u v224=%u v225=%u v226=%u ---", - (unsigned)nowMs, (unsigned)gVm.vars[11], - (unsigned)gVm.vars[36], - (unsigned)gVm.vars[221], (unsigned)gVm.vars[222], - (unsigned)gVm.vars[223], (unsigned)gVm.vars[224], - (unsigned)gVm.vars[225], (unsigned)gVm.vars[226]); - for (i = 0u; i < (uint8_t)AGI_MAX_OBJECTS; i++) { - const AgiObjectT *o = &gVm.objects[i]; - if ((o->flags & (AGI_OBJ_FLAG_ANIMATED | AGI_OBJ_FLAG_DRAWN)) - == (AGI_OBJ_FLAG_ANIMATED | AGI_OBJ_FLAG_DRAWN)) { - jlLogF(" drawn obj[%u] view=%u loop=%u cel=%u x=%d y=%d flags=0x%02x", - (unsigned)i, (unsigned)o->viewId, (unsigned)o->loop, - (unsigned)o->cel, (int)o->x, (int)o->y, - (unsigned)o->flags); - } - } - gLastObjDumpMs = nowMs; - } - - if (gAgiTickLastWallMs == 0u) { - gAgiTickLastWallMs = nowMs; - // Prime one tick so events load immediately on song start. - gAgiTickAccumUs = AGI_TICK_PERIOD_US; - return; - } - deltaMs = nowMs - gAgiTickLastWallMs; - gAgiTickLastWallMs = nowMs; - gAgiTickAccumUs = (uint32_t)(gAgiTickAccumUs + deltaMs * 1000u); - - while (gAgiTickAccumUs >= AGI_TICK_PERIOD_US) { - gAgiTickAccumUs -= AGI_TICK_PERIOD_US; - - // Advance music if any. - if (gSoundActive) { - bool allDone; - for (v = 0u; v < AGI_SOUND_CHANNELS; v++) { - agiVoiceTick(v); - } - allDone = true; - for (v = 0u; v < AGI_SOUND_TONE_VOICES; v++) { - if (!gVoiceSched[v].done) { - allDone = false; - break; - } - } - if (allDone) { - gSoundActive = false; - jlAudioNoise(0u, 15u); // percussion must not outlive the song - jlLogF("synth EOS wallMs=%u v0done=%u v1done=%u v2done=%u v3done=%u", - (unsigned)nowMs, - (unsigned)gVoiceSched[0].done, - (unsigned)gVoiceSched[1].done, - (unsigned)gVoiceSched[2].done, - (unsigned)gVoiceSched[3].done); - jlLogFlush(); - } - } - - // v11 advances every 60 AGI ticks (= 1 AGI sec). At - // AGI_SOUND_TICKS_HZ=120 this is 2 v11 ticks per wall - // sec, which is what the KQ3 script expects (Sierra - // game-tick rate, not wall clock). - gAgiV11SubTickAccum++; - if (gAgiV11SubTickAccum >= 60u) { - gAgiV11SubTickAccum = 0u; - agiVmTickSeconds(&gVm, 1u); - } - } -} - - -// Stream-fill callback for the SB DMA path. Pure sample generator -// now -- all tick advancement is done by agiTickAdvance() from the -// host loop. The synth just reads the latest chip state cached by -// psgRecompute and emits signed 8-bit PCM. -static uint32_t agiSynthFill(void *ctx, int8_t *dst, uint32_t count) { - uint32_t i; - uint8_t v; - int sum; - int8_t voiceSample; - - (void)ctx; - if (!gSoundActive) { - return 0u; - } - for (i = 0u; i < count; i++) { - sum = 0; - for (v = 0u; v < AGI_SOUND_TONE_VOICES; v++) { - voiceSample = (gPsg[v].phase & 0x8000u) - ? gPsg[v].amplitude - : (int8_t)(-gPsg[v].amplitude); - sum += voiceSample; - gPsg[v].phase = (uint16_t)(gPsg[v].phase + gPsg[v].phaseInc); - } - if (sum > 127) { sum = 127; } - if (sum < -128) { sum = -128; } - dst[i] = (int8_t)sum; - } - return count; -} - - -static void cbPlaySound(void *ctx, uint8_t soundId) { - uint8_t *bytes; - uint16_t length; - uint8_t v; - uint16_t off; - - (void)ctx; - agiSoundReset(); - if ((uint16_t)soundId >= gGame.resCount[AGI_RES_SOUND]) { - return; - } - bytes = agiResLoad(&gGame, AGI_RES_SOUND, (uint16_t)soundId, &length); - if (bytes == NULL) { - return; - } - if (length < 8u) { - free(bytes); - return; - } - gSoundBytes = bytes; - gSoundLength = length; - jlLogF("cbPlaySound id=%u len=%u offs=%u/%u/%u/%u wallMs=%u", - (unsigned)soundId, (unsigned)length, - (unsigned)(bytes[0] | (bytes[1] << 8)), - (unsigned)(bytes[2] | (bytes[3] << 8)), - (unsigned)(bytes[4] | (bytes[5] << 8)), - (unsigned)(bytes[6] | (bytes[7] << 8)), - (unsigned)jlMillisElapsed()); - jlLogFlush(); - for (v = 0u; v < AGI_SOUND_CHANNELS; v++) { - off = (uint16_t)(bytes[v * 2u] | ((uint16_t)bytes[v * 2u + 1u] << 8)); - gVoiceSched[v].chOffset = off; - gVoiceSched[v].eventFramesRem = 0u; - gVoiceSched[v].done = (off == 0u) || ((uint32_t)off + 4u >= (uint32_t)length); - } - // Chip state was zeroed by agiSoundReset above; nothing more - // to init here. First agiVoiceTick will fire the events that - // configure freqCount / attenuation / genType. - // Wall-clock-driven tick scheduler: cleared in agiSoundReset - // above; agiSynthFill's first invocation primes the timestamp. - gSoundActive = true; - // Push the synth into SFX slot 0 of the SB DMA mixer at the - // exact output rate so no resampling happens between the synth - // and the chip. - jlAudioPlaySfxStream(0u, agiSynthFill, NULL, AGI_SYNTH_SAMPLE_RATE); -} - - -// VM polls this each cycle to detect natural sound completion. -// True while any tone voice still has events left; false once -// every voice has run off its 0xFFFF terminator (or stopSound -// was called). -static bool cbIsPlayingSound(void *ctx) { - (void)ctx; - return gSoundActive; -} - - -static void cbStopSound(void *ctx) { - (void)ctx; - jlLog("cbStopSound"); - jlLogFlush(); - agiSoundReset(); -} - - -// View metadata callbacks for the VM's cycle / last.cel / -// number.of.loops opcodes. fetchView walks the view-cache, faulting -// the view in from VOL on a cold hit, so the first call after a -// view is referenced may decode the resource. Returns 0 if the -// view cannot be loaded; the VM treats 0 as "still loading" (no -// premature end.of.loop fire) and the host renderer's own clamps -// keep the screen from drawing garbage cels. -static uint8_t cbViewCelCount(void *ctx, uint8_t viewId, uint8_t loopId) { - const AgiViewT *view; - - (void)ctx; - view = fetchView(viewId); - if (view == NULL || loopId >= AGI_MAX_LOOPS_PER_VIEW) { - return 0u; - } - return view->loops[loopId].celCount; -} - - -static uint8_t cbViewLoopCount(void *ctx, uint8_t viewId) { - const AgiViewT *view; - - (void)ctx; - view = fetchView(viewId); - if (view == NULL) { - return 0u; - } - return view->loopCount; -} - - -// (Pre-render synthesizeSnd path was replaced by the streaming -// generator agiSoundFill above; see that for the SND event format -// and PSG synthesis details.) -#if 0 -static bool synthesizeSnd_removed(const uint8_t *bytes, uint16_t length, uint8_t **outPcm, uint32_t *outLen) { - uint32_t maxTicks; - uint32_t durSamples; - uint8_t *pcm; - uint8_t ch; - - if (length < 8u) { - return false; - } - maxTicks = 0u; - for (ch = 0u; ch < 4u; ch++) { - uint16_t off; - uint16_t i; - uint32_t ticks; - - off = (uint16_t)(bytes[ch * 2u] | ((uint16_t)bytes[ch * 2u + 1u] << 8)); - if (off >= length) { - continue; - } - ticks = 0u; - i = off; - while ((uint32_t)i + 4u < (uint32_t)length) { - uint16_t dur; - - dur = (uint16_t)(bytes[i] | ((uint16_t)bytes[i + 1u] << 8)); - if (dur == 0xFFFFu) { - break; - } - ticks = (uint32_t)(ticks + dur); - i = (uint16_t)(i + 5u); - } - if (ticks > maxTicks) { - maxTicks = ticks; - } - } - if (maxTicks == 0u) { - return false; - } - durSamples = (maxTicks * (uint32_t)SOUND_SAMPLE_RATE) / 60u; - if (durSamples == 0u) { - return false; - } - // audioSfxOverlayMix uses a 16.16 fixed-point position counter; - // input samples beyond 2^16 - 1 cause pos to wrap and the slot - // to loop the buffer forever instead of completing. Truncate - // here so longer sounds at least play their opening bars. - if (durSamples > SOUND_MAX_SAMPLES) { - durSamples = SOUND_MAX_SAMPLES; - } - pcm = (uint8_t *)malloc(durSamples); - if (pcm == NULL) { - return false; - } - memset(pcm, 0, durSamples); // signed silence = 0 - - for (ch = 0u; ch < SOUND_SYNTH_CHANNELS; ch++) { - uint16_t off; - uint16_t i; - uint32_t sampleIdx; - // 16.16 phase accumulator: 0..0xFFFF = one square-wave - // cycle. Stepping by phaseStep per output sample yields - // sub-sample precision so 717 Hz, 466 Hz, etc. stay at the - // correct frequency instead of quantizing to the nearest - // integer half-period (which scrambled the melody at - // lower sample rates). - uint32_t phaseAcc; - - off = (uint16_t)(bytes[ch * 2u] | ((uint16_t)bytes[ch * 2u + 1u] << 8)); - if (off >= length) { - continue; - } - sampleIdx = 0u; - phaseAcc = 0u; - i = off; - while ((uint32_t)i + 4u < (uint32_t)length && sampleIdx < durSamples) { - uint16_t dur; - uint16_t divisor; - uint8_t atten; - int8_t amp; - uint32_t durSamps; - uint32_t phaseStep; - uint32_t s; - - dur = (uint16_t)(bytes[i] | ((uint16_t)bytes[i + 1u] << 8)); - if (dur == 0xFFFFu) { - break; - } - divisor = (uint16_t)(((bytes[i + 2u] & 0x3Fu) << 4) | (bytes[i + 3u] & 0x0Fu)); - atten = (uint8_t)(bytes[i + 4u] & 0x0Fu); - amp = kAgiSoundAmp[atten]; - durSamps = ((uint32_t)dur * (uint32_t)SOUND_SAMPLE_RATE) / 60u; - if (divisor == 0u || amp == 0) { - // Silent event -- just advance sampleIdx without - // touching pcm (already zeroed) or stepping phase. - if (sampleIdx + durSamps > durSamples) { - sampleIdx = durSamples; - } else { - sampleIdx = sampleIdx + durSamps; - } - i = (uint16_t)(i + 5u); - continue; - } - // freq_hz = PSG_CLOCK_HZ / divisor. - // phaseStep (16.16, one cycle = 0x10000) per output sample - // = (freq_hz << 16) / SOUND_SAMPLE_RATE - // = (PSG_CLOCK_HZ << 16) / (divisor * SOUND_SAMPLE_RATE) - phaseStep = ((uint32_t)PSG_CLOCK_HZ << 16) / ((uint32_t)divisor * SOUND_SAMPLE_RATE); - for (s = 0u; s < durSamps && sampleIdx < durSamples; s++, sampleIdx++) { - int8_t sval; - int16_t mixed; - - sval = (phaseAcc & 0x8000u) ? (int8_t)(-amp) : amp; - mixed = (int16_t)((int8_t)pcm[sampleIdx]) + sval; - if (mixed > 127) { mixed = 127; } - if (mixed < -128) { mixed = -128; } - pcm[sampleIdx] = (uint8_t)mixed; - phaseAcc = (uint32_t)((phaseAcc + phaseStep) & 0xFFFFu); - } - i = (uint16_t)(i + 5u); - } - } - - *outPcm = pcm; - *outLen = durSamples; - return true; -} - - -static void cbPlaySound(void *ctx, uint8_t soundId) { - uint8_t *bytes; - uint16_t length; - uint8_t *pcm; - uint32_t pcmLen; - - (void)ctx; - if (!gAudioReady) { - return; - } - // Cache hit: same sound replayed -- just restart the slot, - // skip the file load + synth (instant restart, no freeze). - if (gSoundPcmValid && gSoundPcm != NULL && gSoundPcmId == soundId) { - jlAudioStopSfx(0u); - jlAudioPlaySfx(0u, gSoundPcm, gSoundPcmLen, (uint16_t)SOUND_SAMPLE_RATE); - return; - } - // Cache miss: tear down prior buffer, synth a new one. - if (gSoundPcm != NULL) { - jlAudioStopSfx(0u); - free(gSoundPcm); - gSoundPcm = NULL; - gSoundPcmLen = 0u; - } - gSoundPcmValid = false; - if ((uint16_t)soundId >= gGame.resCount[AGI_RES_SOUND]) { - return; - } - bytes = agiResLoad(&gGame, AGI_RES_SOUND, (uint16_t)soundId, &length); - if (bytes == NULL) { - return; - } - pcm = NULL; - pcmLen = 0u; - if (synthesizeSnd(bytes, length, &pcm, &pcmLen)) { - gSoundPcm = pcm; - gSoundPcmLen = pcmLen; - gSoundPcmId = soundId; - gSoundPcmValid = true; - jlAudioPlaySfx(0u, gSoundPcm, gSoundPcmLen, (uint16_t)SOUND_SAMPLE_RATE); - } - free(bytes); -} - - -static void cbStopSound(void *ctx) { - (void)ctx; - if (!gAudioReady) { - return; - } - // Stop the slot but keep the PCM cached -- a quick stop/start - // (very common in AGI v2 state machines) then replays without - // a re-synth freeze. - jlAudioStopSfx(0u); -} -#endif - - -// Parse an AGI v2 SND resource and return its playback duration in -// milliseconds (max across the 4 PSG channels). Used to defer the -// sound-done flag firing until the music would naturally end -- -// drives KQ3's title state-1 / state-3 shortcut which advances the -// countdown to 2 sec once flag 9 (sound finished) is set. -// -// SND v2 layout: -// bytes 0..7 : 4 channel offsets (2-byte LE each) -// per channel : sequence of 5-byte events (duration LE16, freq LE16, -// attenuation), terminated by duration = 0xFFFF. -// Duration units are 1/60 sec (NTSC vblank ticks). -static uint32_t cbSoundDuration(void *ctx, uint8_t soundId) { - uint8_t *bytes; - uint16_t length; - uint32_t maxTicks; - uint8_t ch; - - (void)ctx; - if ((uint16_t)soundId >= gGame.resCount[AGI_RES_SOUND]) { - return 0u; - } - bytes = agiResLoad(&gGame, AGI_RES_SOUND, (uint16_t)soundId, &length); - if (bytes == NULL) { - return 0u; - } - if (length < 8u) { - free(bytes); - return 0u; - } - maxTicks = 0u; - for (ch = 0u; ch < 4u; ch++) { - uint16_t off; - uint16_t i; - uint32_t ticks; - - off = (uint16_t)(bytes[ch * 2u] | ((uint16_t)bytes[ch * 2u + 1u] << 8)); - ticks = 0u; - i = off; - while ((uint32_t)i + 4u < (uint32_t)length) { - uint16_t dur; - - dur = (uint16_t)(bytes[i] | ((uint16_t)bytes[i + 1u] << 8)); - if (dur == 0xFFFFu) { - break; - } - ticks = (uint32_t)(ticks + dur); - i = (uint16_t)(i + 5u); - } - if (ticks > maxTicks) { - maxTicks = ticks; - } - } - free(bytes); - // 60 ticks / sec -> ms = ticks * 1000 / 60 = ticks * 50 / 3 - return (maxTicks * 1000u) / 60u; -} - - -static bool decodePicInto(uint8_t picId, bool clearFirst) { - uint8_t *bytes; - uint16_t length; - bool ok; - - if ((uint16_t)picId >= gGame.resCount[AGI_RES_PIC]) { - return false; - } - bytes = agiResLoad(&gGame, AGI_RES_PIC, (uint16_t)picId, &length); - if (bytes == NULL) { - return false; - } - if (clearFirst) { - agiPicClear(&gPic); - } - ok = agiPicDecode(&gPic, bytes, length); - free(bytes); - return ok; -} - - -// Walk every key the JoeyLib enum knows about; for each one that -// went down this frame, look it up against vm->keyBindings via -// agiVmDispatchKey. Function keys map to the AGI extended scancode -// pair (0, 59..68); printable ASCII keys map to (asciiCode, 0). -static void dispatchAllPressedKeys(void) { - static const uint8_t fnScancodes[10] = { 59, 60, 61, 62, 63, 64, 65, 66, 67, 68 }; - static const jlKeyE fnKeys[10] = { - KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, - KEY_F6, KEY_F7, KEY_F8, KEY_F9, KEY_F10 - }; - uint8_t k; - uint8_t i; - bool captureText; - - // While the command line is live (and no modal is up), printable keys, - // Enter, Space and Backspace belong to the input line (agiInputCapture), - // not to controller/key-binding dispatch. Tab, function keys and the - // joystick stay as controllers so status/menu/action keys still work. - captureText = gVm.acceptInput && !gVm.printModal.active; - - if (!captureText) { - if (jlKeyPressed(KEY_RETURN)) { - agiVmDispatchKey(&gVm, 13u, 0u); - gAnyKeyHit = true; - } - if (jlKeyPressed(KEY_SPACE)) { - agiVmDispatchKey(&gVm, 32u, 0u); - gAnyKeyHit = true; - } - if (jlKeyPressed(KEY_BACKSPACE)) { - agiVmDispatchKey(&gVm, 8u, 0u); - gAnyKeyHit = true; - } - for (k = 0u; k < 26u; k++) { - if (jlKeyPressed((jlKeyE)((uint8_t)KEY_A + k))) { - agiVmDispatchKey(&gVm, (uint8_t)('a' + k), 0u); - gAnyKeyHit = true; - } - } - for (k = 0u; k < 10u; k++) { - if (jlKeyPressed((jlKeyE)((uint8_t)KEY_0 + k))) { - agiVmDispatchKey(&gVm, (uint8_t)('0' + k), 0u); - gAnyKeyHit = true; - } - } - } - if (jlKeyPressed(KEY_TAB)) { - agiVmDispatchKey(&gVm, 9u, 0u); - gAnyKeyHit = true; - } - for (i = 0u; i < 10u; i++) { - if (jlKeyPressed(fnKeys[i])) { - agiVmDispatchKey(&gVm, 0u, fnScancodes[i]); - gAnyKeyHit = true; - } - } - if (jlJoystickConnected(JOYSTICK_0)) { - if (jlJoyPressed(JOYSTICK_0, JOY_BUTTON_0) || jlJoyPressed(JOYSTICK_0, JOY_BUTTON_1)) { - agiVmDispatchKey(&gVm, 13u, 0u); // joystick fire == ENTER - gAnyKeyHit = true; - } - } -} - - -// Restore the exact stage rect `(x, y, w, h)` from the gBackdrop -// surface. Chunky-fast: a single memcpy per row from backdrop pixels -// to stage pixels via the library's rect copy (hunt-3 rank 3). The -// old body hand-rolled a chunky memcpy loop and, on the planar 68k -// ports (stage->pixels NULL post-Phase-9/10), fell back to a FULL -// 32KB jlSurfaceCopy per call -- 3-5 times per frame -- which also -// re-marked the whole stage AND the SCB/palette dirty, defeating the -// group-skip presents. jlSurfaceCopyRect does everything this needs: -// clips, widens x to the 16-px group grid (safe -- the erase-all- -// then-redraw-all object pass repaints anything the widening grazes), -// routes to each port's fast copy (ST move.l spans, Amiga blitter -// arm, IIgs inline word, DOS memcpy), and marks exactly the rect. -static void restoreRectFromBackdrop(jlSurfaceT *stage, int16_t x, int16_t y, int16_t w, int16_t h) { - if (stage == NULL || gBackdrop == NULL || w <= 0 || h <= 0) { - return; - } - jlSurfaceCopyRect(stage, gBackdrop, x, y, (uint16_t)w, (uint16_t)h); -} - - -// Walk vm->objects[] and (re)draw every animated+drawn object. Per -// frame: -// pass 1: restore the prev-frame rect of every previously-drawn -// obj from the backdrop. Erases the prev draw without -// needing per-object save buffers. -// pass 2: for each visible obj, compute its cel's stage bbox, -// draw the cel, and record the bbox as its new prev rect. -// -// Two passes are required so an obj's restore can't accidentally -// erase another obj's draw -- all restores happen before any draws. -static void drawObjects(jlSurfaceT *stage) { - uint8_t i; - - // Pass 1: restore prev-frame rect from backdrop for every obj - // that was drawn last frame. - for (i = 0u; i < (uint8_t)AGI_MAX_OBJECTS; i++) { - if (gObjPrev[i].valid) { - restoreRectFromBackdrop(stage, gObjPrev[i].x, gObjPrev[i].y, - gObjPrev[i].w, gObjPrev[i].h); - gObjPrev[i].valid = false; - } - } - - // Pass 2: draw current frame's visible objects. - for (i = 0u; i < (uint8_t)AGI_MAX_OBJECTS; i++) { - const AgiObjectT *obj; - const AgiViewT *view; - uint8_t loop; - uint8_t cel; - uint8_t actorPri; - int16_t stageX; - int16_t stageY; - int16_t stageW; - int16_t stageH; - - obj = &gVm.objects[i]; - if ((obj->flags & AGI_OBJ_FLAG_ANIMATED) == 0u) { - continue; - } - if ((obj->flags & AGI_OBJ_FLAG_DRAWN) == 0u) { - continue; - } - view = fetchView(obj->viewId); - if (view == NULL || view->loopCount == 0u) { - continue; - } - loop = obj->loop; - // Auto-loop from direction only applies to MOVING objects. - // A stopped obj keeps whatever loop the script (e.g. - // set.loop) assigned -- otherwise KQ3's title-screen wizard - // eyes (obj 0, view=65 loop=1, direction=0) get clobbered - // to loop 0 (the body view) by the stopped-default branch. - if ((obj->flags & AGI_OBJ_FLAG_LOOP_FIXED) == 0u && - obj->direction != 0u) { - uint8_t autoLoop = loopForDirection(obj->direction, view->loopCount); - if (autoLoop < view->loopCount) { - loop = autoLoop; - } - } - if (loop >= view->loopCount) { - loop = 0u; - } - cel = obj->cel; - if (cel >= view->loops[loop].celCount) { - cel = 0u; - } - if ((obj->flags & AGI_OBJ_FLAG_PRI_FIXED) != 0u && obj->priority >= AGI_PRIORITY_BG) { - actorPri = obj->priority; - } else { - actorPri = agiActorPriorityForY(obj->y); - } - agiViewDraw(view, loop, cel, - (int16_t)obj->x, (int16_t)obj->y, - actorPri, gPic.priority, - stage, PIC_DEST_Y); - // Record the cel's exact stage bbox so next frame's restore - // covers only that region (works for cels of any size, fixes - // the trailing-pixel artifact wider/taller cels suffered with - // the previous fixed-size save-under). - { - uint8_t cw = view->loops[loop].cels[cel].width; - uint8_t ch = view->loops[loop].cels[cel].height; - stageX = (int16_t)(obj->x * 2); - stageY = (int16_t)(PIC_DEST_Y + obj->y - (int16_t)ch + 1); - stageW = (int16_t)(cw * 2); - stageH = (int16_t)ch; - gObjPrev[i].x = stageX; - gObjPrev[i].y = stageY; - gObjPrev[i].w = stageW; - gObjPrev[i].h = stageH; - gObjPrev[i].valid = true; - } - } -} - - -// Invalidate every object's prev-rect entry. Called when the picture -// changes (show.pic / add.to.pic) so we don't restore stale pixels -// over the new backdrop on the next frame. -static void invalidateObjectSaves(void) { - uint8_t i; - - for (i = 0u; i < (uint8_t)AGI_MAX_OBJECTS; i++) { - gObjPrev[i].valid = false; - } -} - - -static const AgiViewT *fetchView(uint8_t viewId) { - ViewCacheSlotT *slot; - uint8_t *bytes; - uint16_t length; - - slot = &gViewCache[viewId]; - if (slot->loaded) { - return &slot->view; - } - if (viewId >= gGame.resCount[AGI_RES_VIEW]) { - return NULL; - } - bytes = agiResLoad(&gGame, AGI_RES_VIEW, (uint16_t)viewId, &length); - if (bytes == NULL) { - return NULL; - } - if (!agiViewParse(&slot->view, bytes, length)) { - return NULL; - } - slot->loaded = true; - return &slot->view; -} - - -// AGI's canonical loop assignment from direction (idx 0 = right): -// right=0, left=1, down=2, up=3 -// Diagonal directions reuse the dominant axis loop. -static uint8_t loopForDirection(uint8_t direction, uint8_t loopCount) { - uint8_t loop; - - if (loopCount <= 1u) { - return 0u; - } - switch (direction) { - case 1u: loop = 3u; break; // N - case 2u: loop = 0u; break; // NE -> right - case 3u: loop = 0u; break; // E - case 4u: loop = 0u; break; // SE -> right - case 5u: loop = 2u; break; // S - case 6u: loop = 1u; break; // SW -> left - case 7u: loop = 1u; break; // W - case 8u: loop = 1u; break; // NW -> left - default: loop = 2u; break; // stopped -> down - } - if (loop >= loopCount) { - return 0u; - } - return loop; -} - - -static uint8_t parseArgU8(const char *s) { - uint16_t val; - uint8_t digit; - - if (s == NULL) { - return 0u; - } - val = 0u; - while (*s != '\0') { - if (*s < '0' || *s > '9') { - return 0u; - } - digit = (uint8_t)(*s - '0'); - if (val > 25u || (val == 25u && digit > 5u)) { - return 0u; - } - val = (uint16_t)(val * 10u + digit); - s++; - } - return (uint8_t)val; -} - - -static void releaseLogicCache(void) { - uint16_t i; - - for (i = 0u; i < AGI_MAX_RESOURCES; i++) { - if (gLogicCache[i].raw != NULL) { - free(gLogicCache[i].raw); - gLogicCache[i].raw = NULL; - gLogicCache[i].bytecode = NULL; - } - } -} - - -static void releaseViewCache(void) { - uint16_t i; - - for (i = 0u; i < AGI_MAX_RESOURCES; i++) { - if (gViewCache[i].loaded) { - agiViewFree(&gViewCache[i].view); - gViewCache[i].loaded = false; - } - } -} - - -static void renderFrame(jlSurfaceT *stage) { - uint8_t r; - - if (!gPicReady) { - jlStagePresent(); - return; - } - // Step 1: restore text rows that had content last frame so a VM - // that cleared them this frame (clear.lines / row reassign) sees - // them disappear from the stage. - for (r = 0u; r < (uint8_t)AGI_TEXT_ROWS; r++) { - if (gTextRowDirty[r]) { - restoreRectFromBackdrop(stage, 0, (int16_t)((int16_t)r * 8), - (int16_t)SURFACE_WIDTH, 8); - gTextRowDirty[r] = false; - } - } - // Step 2: restore + draw animated/drawn objects. - drawObjects(stage); - // Command line: echo "> " + the typed text (with a cursor) on the input - // row while accept.input is live; blank our prompt when it turns off so - // the line disappears. This reuses the text-row plane so agiTextRender - // and the dirty-row bookkeeping below draw and wipe it like any message. - { - char *dst = gVm.textRows[AGI_INPUT_ROW].text; - - if (gVm.acceptInput) { - uint8_t n; - uint8_t i; - - n = 0u; - dst[n++] = '>'; - dst[n++] = ' '; - for (i = 0u; i < gVm.inputLen && n < (uint8_t)AGI_TEXT_COLS; i++) { - dst[n++] = gVm.inputLine[i]; - } - if (n < (uint8_t)AGI_TEXT_COLS) { - dst[n++] = '_'; - } - dst[n] = '\0'; - } else if (dst[0] == '>') { - dst[0] = '\0'; - } - } - // Step 3: draw current text rows on top. Mark dirty as we go so - // step 1 of the next frame knows to wipe them. - { - bool statusOn = gVm.statusLineOn; - uint8_t statusRow = gVm.statusLineRow; - - if (statusOn) { - gTextRowDirty[statusRow] = true; - } - for (r = 0u; r < (uint8_t)AGI_TEXT_ROWS; r++) { - if (gVm.textRows[r].text[0] != '\0') { - gTextRowDirty[r] = true; - } - } - } - agiTextRender(&gVm, stage); - jlStagePresent(); -} - - -static uint8_t resolveStartingRoom(int argc, char **argv) { -#if defined(JOEYLIB_PLATFORM_IIGS) - // GS/OS launches an S16 app with no usable argv (the crt0 leaves - // argc/argv uninitialized -- garbage), so always start at the title - // room. Game data comes from DATA/ regardless of argv now. - (void)argc; - (void)argv; - return 0u; -#else - if (argc <= 1 || argv[1] == NULL) { - return 0u; - } - return parseArgU8(argv[1]); -#endif -} - - -static void runVmCycle(void) { - AgiVmHaltE reason; - const uint8_t *logic0; - uint16_t length; - uint8_t prevRoom; - uint8_t i; - uint8_t guard; - - // Block the cycle entirely while a print() modal is up; the - // host's input loop dismisses it on the next ack key. - if (gVm.printModal.active) { - return; - } - - guard = CYCLE_HALT_GUARD; - for (;;) { - if (guard == 0u) { - return; - } - guard--; - reason = agiVmRun(&gVm); - if (reason == AGI_VM_HALT_PRINT_PENDING) { - return; - } - if (reason == AGI_VM_HALT_QUIT) { - return; - } - if (reason == AGI_VM_HALT_RETURN) { - agiVmTickAnimation(&gVm); - gVm.flags[5] = 0u; - // The player's command was offered to every logic this cycle; - // consume it so said() can't re-fire it on later cycles. (A - // matching said() already cleared it; this covers the no-match - // case.) - gVm.flags[AGI_FLAG_INPUT] = 0u; - length = 0u; - logic0 = cbFetchLogic(NULL, 0u, &length); - if (logic0 != NULL) { - agiVmResetToLogic(&gVm, logic0, length, 0u); - } - return; - } - if (reason == AGI_VM_HALT_NEW_ROOM) { - prevRoom = gVm.vars[0]; - if (gOverrideRoomActive) { - gVm.vars[0] = gOverrideRoom; - gOverrideRoomActive = false; - } else { - gVm.vars[0] = gVm.newRoomId; - } - gVm.vars[1] = prevRoom; - for (i = 0u; i < 32u; i++) { - gVm.flags[i] = 0u; - } - gVm.flags[5] = 1u; - // Clear the object table on room change so leftover NPCs - // don't bleed into the new scene. Ego (object 0) stays - // animated/drawn; the new room's init re-runs animate.obj - // with the same setup if it wants ego on screen. - for (i = 1u; i < (uint8_t)AGI_MAX_OBJECTS; i++) { - agiObjReset(&gVm.objects[i]); - } - // Clear text rows. Sierra's new.room expects a fresh text - // band -- without this, v226=3 credits from LOGIC.45 stay - // visible after the room.46 transition. - for (i = 0u; i < (uint8_t)AGI_TEXT_ROWS; i++) { - gVm.textRows[i].text[0] = '\0'; - } - length = 0u; - logic0 = cbFetchLogic(NULL, 0u, &length); - if (logic0 != NULL) { - agiVmResetToLogic(&gVm, logic0, length, 0u); - } - continue; - } - // Unknown opcode, truncated stream, missing logic, stack - // overflow: stop cycling this frame and let the host render - // what we have. - return; - } -} - - -// Player-control input mapping for ego (when programControl is false). -// Sets var 6 (ego direction) so the engine's motion tick moves the -// player normally. Diagonals not bound; AGI canonical games rarely -// use them via keyboard. -static void updateEgoFromInput(void) { - uint8_t newDir; - - if (gVm.programControl) { - return; - } - newDir = 0u; - if (jlKeyDown(KEY_UP)) { newDir = 1u; } - if (jlKeyDown(KEY_RIGHT)) { newDir = 3u; } - if (jlKeyDown(KEY_DOWN)) { newDir = 5u; } - if (jlKeyDown(KEY_LEFT)) { newDir = 7u; } - if (newDir != 0u || jlKeyPressed(KEY_UP) || jlKeyPressed(KEY_DOWN) || jlKeyPressed(KEY_LEFT) || jlKeyPressed(KEY_RIGHT)) { - gVm.objects[0].direction = newDir; - gVm.vars[6] = newDir; - } else if (!jlKeyDown(KEY_UP) && !jlKeyDown(KEY_DOWN) && !jlKeyDown(KEY_LEFT) && !jlKeyDown(KEY_RIGHT)) { - gVm.objects[0].direction = 0u; - gVm.vars[6] = 0u; - } -} - - -// ----- Entry point ----- - -int main(int argc, char **argv) { - jlConfigT cfg; - AgiVmCallbacksT cb; - const uint8_t *logic0; - uint16_t length; - uint8_t startingRoom; - bool picOk; - bool vmReady; - - cfg.codegenBytes = 0u; // no sprites needed -- backdrop-restore path is direct memcpy - // AGI sound is software-synthesized PSG squarewaves pushed - // through the Sound Blaster auto-init DMA path (see agiSynthFill - // and src/port/dos/audio.c). The audio pool covers the 4 KB - // DMA buffer + libxmp-lite SMIX state. - if (!jlInit(&cfg)) { - return 1; - } - - jlLogReset(); - jlLogF("AGI TRACE build=%s %s", __DATE__, __TIME__); - jlLogFlush(); - gAgiStage = jlStageGet(); - jlPaletteSet(gAgiStage, 0u, kAgiPalette); - jlRandomSeed(0xA61DA61Du); - - // Bring up the SB DMA + mixer. AGI music feeds the SFX stream - // path inside jlAudioFrameTick, which depends on a successful - // init here. - (void)jlAudioInit(); - - { - uint8_t i; - for (i = 0u; i < (uint8_t)AGI_MAX_OBJECTS; i++) { - gObjPrev[i].valid = false; - } - } - - - // The view cache is too large for bank-0 BSS on the IIgs; pull it - // from the platform's large-allocator. jlAlloc does not zero its - // result, so clear it so every slot starts with loaded == false. - gViewCache = (ViewCacheSlotT *)jlAlloc(sizeof(ViewCacheSlotT) * AGI_MAX_RESOURCES); - if (gViewCache == NULL) { - jlShutdown(); - return 1; - } - memset(gViewCache, 0, sizeof(ViewCacheSlotT) * AGI_MAX_RESOURCES); - - startingRoom = resolveStartingRoom(argc, argv); - gOverrideRoom = startingRoom; - gOverrideRoomActive = (startingRoom != 0u); - picOk = false; - vmReady = false; - gBackdrop = NULL; - gPicReady = false; - gAnyKeyHit = false; - - if (agiResOpen(&gGame)) { - picOk = agiPicAlloc(&gPic); - if (picOk) { - agiPicClear(&gPic); - gBackdrop = jlSurfaceCreate(); - - agiVmInit(&gVm); - cb.fetchLogic = cbFetchLogic; - cb.loadPic = cbLoadPic; - cb.drawPic = cbDrawPic; - cb.showPic = cbShowPic; - cb.discardPic = cbDiscardPic; - cb.overlayPic = cbOverlayPic; - cb.haveKey = cbHaveKey; - cb.fetchMessage = cbFetchMessage; - cb.addToPic = cbAddToPic; - cb.soundDuration = cbSoundDuration; - cb.playSound = cbPlaySound; - cb.stopSound = cbStopSound; - cb.isPlayingSound = cbIsPlayingSound; - cb.viewCelCount = cbViewCelCount; - cb.viewLoopCount = cbViewLoopCount; - cb.ctx = NULL; - agiVmSetCallbacks(&gVm, &cb); - - length = 0u; - logic0 = cbFetchLogic(NULL, 0u, &length); - if (logic0 != NULL) { - agiVmResetToLogic(&gVm, logic0, length, 0u); - vmReady = true; - } - } - } - - jlSurfaceClear(gAgiStage, COLOR_MISSING); - if (gBackdrop != NULL) { - jlSurfaceClear(gBackdrop, COLOR_MISSING); - } - - if (vmReady) { - uint8_t pump; - - for (pump = 0u; pump < STARTUP_CYCLE_LIMIT; pump++) { - if (gPicReady) { - break; - } - runVmCycle(); - } - } - - { - uint32_t lastVmCycleMs = 0u; - uint32_t lastRenderMs = 0u; - - for (;;) { - jlInputPoll(); - if (jlKeyPressed(KEY_ESCAPE)) { - break; - } - gAnyKeyHit = false; - dispatchAllPressedKeys(); - - // When the command line is live (and no modal is up), typed - // characters build the input line instead of firing controllers. - if (gVm.acceptInput && !gVm.printModal.active) { - agiInputCapture(); - } - - // Pump the AGI tick generator EVERY frame, even with a - // print modal up. This drives music event playback AND - // the v11 seconds counter -- both keyed to the same - // AGI tick rate so script timing stays consistent with - // music tempo. Pausing this would freeze the AGI clock. - agiTickAdvance(); - - // If a print() modal is up, any key dismisses it and resumes - // the VM next cycle. Don't run the VM or update inputs while - // the modal is alive. - if (gVm.printModal.active) { - if (gAnyKeyHit) { - agiVmAckPrint(&gVm); - } - } else { - uint32_t nowMs = jlMillisElapsed(); - uint8_t delayJiffies; - uint32_t cyclePeriodMs; - - updateEgoFromInput(); - delayJiffies = gVm.vars[10]; - if (delayJiffies == 0u) { delayJiffies = 1u; } - cyclePeriodMs = (uint32_t)delayJiffies * (uint32_t)AGI_JIFFY_MS; - if (vmReady && (nowMs - lastVmCycleMs) >= cyclePeriodMs) { - runVmCycle(); - lastVmCycleMs = nowMs; - } - if (gVm.haltReason == AGI_VM_HALT_QUIT) { - break; - } - } - - // Wall-clock-paced render. Audio refill still runs every - // loop iteration so DMA half-buffers get serviced even - // when we're skipping the visual update. - { - uint32_t nowMs2 = jlMillisElapsed(); - if (nowMs2 - lastRenderMs >= AGI_RENDER_PERIOD_MS) { - renderFrame(gAgiStage); - lastRenderMs = nowMs2; - } else { - jlStagePresent(); - } - } - // Pump the SB DMA mixer. Refills any half-buffer the - // ISR flagged for refill since last call, which in turn - // pulls the next chunk from agiSynthFill (when music - // is armed) or libxmp + SFX overlay (when not). - jlAudioFrameTick(); -#if defined(JOEYLIB_PLATFORM_IIGS) - // The IIgs noise voice is an ~80 ms one-shot; re-assert - // held noise once per frame so it sustains (same model as - // the music sequencer's per-pump re-assert). - if (gSoundActive && gPsg[3].attenuation < 15u) { - psgNoiseApply(); - } -#endif - jlWaitVBL(); - } - } - - agiSoundReset(); - jlAudioShutdown(); - releaseLogicCache(); - releaseViewCache(); - jlFree(gViewCache); - gViewCache = NULL; - if (gBackdrop != NULL) { - jlSurfaceDestroy(gBackdrop); - } - if (picOk) { - agiPicFree(&gPic); - } - agiResClose(&gGame); - jlShutdown(); - return 0; -} diff --git a/examples/agi/agi.h b/examples/agi/agi.h deleted file mode 100644 index 0b5c99e..0000000 --- a/examples/agi/agi.h +++ /dev/null @@ -1,606 +0,0 @@ -// 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 - - -// 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]; - // WORDS.TOK dictionary, loaded whole. Prefix-compressed entries are - // walked on demand by agiWordId (parse is infrequent -- once per typed - // command -- so a flat scan beats building a hashed table). - uint8_t *words; - uint32_t wordsLen; -} AgiGameT; - -// Reserved AGI word group IDs. -#define AGI_WORD_IGNORE 0u // "a", "the", ... -- dropped by parse -#define AGI_WORD_ANYWORD 1u // said() wildcard: matches any one word -#define AGI_WORD_REST 9999u // said() wildcard: matches rest of line -#define AGI_WORD_UNKNOWN 0xFFFFu // agiWordId: typed word not in dictionary - -// WORDS.TOK layout + limits. The header is 26 big-endian 2-byte offsets -// (one per initial letter); word entries follow. AGI v2 dictionaries are a -// few KB (KQ3 is ~5.7 KB); 16 KB is ample headroom. -#define AGI_WORDS_HEADER_BYTES 52u -#define AGI_WORDS_MAX 16384u -#define AGI_WORD_MAX_LEN 40u - -// Command-line parser limits + the reserved flag/var the interpreter uses -// to talk to logic scripts about input: -// flag 2 -- set when the player has entered a command this cycle; said() -// matches only while it is set and clears it on a match so only -// the first matching said() in the logic scan fires. -// var 9 -- 1-based position of a typed word that isn't in the dictionary. -#define AGI_INPUT_MAX 40u // chars in the on-screen command line -#define AGI_MAX_PARSED 12u // word IDs kept from one command -#define AGI_INPUT_ROW 24u // bottom text row shows the command line -#define AGI_FLAG_INPUT 2u -#define AGI_VAR_UNKNOWN_WORD 9u - - -// 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]; - - // ----- parser / command input ----- - // inputLine is the raw text the player is typing; on Enter agi.c - // tokenizes it against the dictionary into parsedWords[] (group IDs, - // ignore-words dropped) and sets flag 2 for said() to test. - char inputLine[AGI_INPUT_MAX + 1u]; - uint8_t inputLen; - uint16_t parsedWords[AGI_MAX_PARSED]; - uint8_t parsedWordCount; - - // ----- 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 the AGI v2 game whose LOGDIR/PICDIR/VIEWDIR/SNDDIR and VOL.0..VOL.N -// files are staged under DATA/ (opened by bare name via jlDataOpen). -// 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); - -// Look up a word in the loaded WORDS.TOK dictionary (case-insensitive) and -// return its AGI group ID, or AGI_WORD_UNKNOWN if the word is absent (or no -// dictionary loaded). Synonyms share a group ID; group 0 = ignore-words. -uint16_t agiWordId(const AgiGameT *game, const char *word); - - -// ----- 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 diff --git a/examples/agi/agiObj.c b/examples/agi/agiObj.c deleted file mode 100644 index 24629a7..0000000 --- a/examples/agi/agiObj.c +++ /dev/null @@ -1,363 +0,0 @@ -// 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 -#include - - -// 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; - } -} diff --git a/examples/agi/agiPic.c b/examples/agi/agiPic.c deleted file mode 100644 index ae92566..0000000 --- a/examples/agi/agiPic.c +++ /dev/null @@ -1,659 +0,0 @@ -// 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/core.h" -#include "joey/draw.h" -#include "surfaceInternal.h" - -#include -#include -#include - - -// 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); - - -// ----- 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 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 *)jlAlloc(AGI_PIC_PIXELS); - pic->priority = (uint8_t *)jlAlloc(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) { - jlFree(pic->visual); - pic->visual = NULL; - } - if (pic->priority != NULL) { - jlFree(pic->priority); - pic->priority = NULL; - } -} diff --git a/examples/agi/agiRes.c b/examples/agi/agiRes.c deleted file mode 100644 index 32da2cf..0000000 --- a/examples/agi/agiRes.c +++ /dev/null @@ -1,400 +0,0 @@ -// 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 -#include -#include -#include - -#include "joey/file.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 - -// Resource file names are opened relative to DATA/ via jlDataOpen (see -// joey/file.h); the longest name AGI forms is "VOL.". 16 is ample. -#define AGI_VOL_NAME_MAX 16 - -// 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 - - -// ----- Prototypes ----- - -static bool buildVolName(char *out, size_t outSize, uint8_t volNum); -static bool loadDirectory(AgiGameT *game, const char *fileName, AgiResTypeE type); -static bool loadWords(AgiGameT *game); -static bool openVolumes(AgiGameT *game); -static const char *resTypeFileName(AgiResTypeE type); -static bool wordEqCI(const char *a, const char *b); - - -// ----- Internal helpers (alphabetical) ----- - -static bool buildVolName(char *out, size_t outSize, uint8_t volNum) { - char *p; - - // "VOL." + up to 2 digits + NUL = 7 chars max. - if (outSize < 8u) { - return false; - } - p = out; - *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, const char *fileName, AgiResTypeE type) { - // One directory is resident at a time; a file-scope buffer keeps this - // ~768-byte read off the shallow IIgs soft stack. - static uint8_t dirBytes[AGI_MAX_RESOURCES * AGI_DIR_ENTRY_BYTES]; - FILE *fp; - uint16_t entryCount; - uint16_t i; - uint8_t volNibble; - uint32_t offset; - size_t got; - - // fileName is a bare name relative to DATA/ (e.g. "LOGDIR"). - fp = jlDataOpen(fileName, "rb"); - if (fp == NULL) { - return false; - } - - // Read the whole flat 3-byte-entry array in one call rather than one - // read per entry: on the IIgs each fread is a GS/OS round trip through - // SmartPort firmware, and a directory never exceeds AGI_MAX_RESOURCES - // entries, so the fixed buffer holds any valid file. A non-multiple-of-3 - // length means the file is malformed. - got = fread(dirBytes, 1, sizeof(dirBytes), fp); - fclose(fp); - if (got == 0u || (got % AGI_DIR_ENTRY_BYTES) != 0u) { - return false; - } - - entryCount = (uint16_t)(got / AGI_DIR_ENTRY_BYTES); - for (i = 0; i < entryCount; i++) { - const uint8_t *e = &dirBytes[(uint32_t)i * AGI_DIR_ENTRY_BYTES]; - - volNibble = (uint8_t)(e[0] >> 4); - // 20-bit offset, cast to uint32_t before shift so a - // 16-bit int doesn't truncate the (& 0x0F) << 16 term. - offset = ((uint32_t)(e[0] & 0x0F) << 16) | ((uint32_t)e[1] << 8) | (uint32_t)e[2]; - - if (e[0] == 0xFF && e[1] == 0xFF && e[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; - } - } - - game->resCount[type] = entryCount; - return true; -} - - -static bool loadWords(AgiGameT *game) { - FILE *fp; - uint8_t *buf; - size_t got; - - game->words = NULL; - game->wordsLen = 0u; - - // The dictionary is optional: a game with no WORDS.TOK simply can't - // match said()/parse (no crash), so a missing file is not an error. - fp = jlDataOpen("WORDS.TOK", "rb"); - if (fp == NULL) { - return true; - } - buf = (uint8_t *)malloc(AGI_WORDS_MAX); - if (buf == NULL) { - fclose(fp); - return false; - } - got = fread(buf, 1, AGI_WORDS_MAX, fp); - fclose(fp); - // Anything shorter than the letter-index header can't hold a word. - if (got <= AGI_WORDS_HEADER_BYTES) { - free(buf); - return true; - } - game->words = buf; - game->wordsLen = (uint32_t)got; - return true; -} - - -static bool openVolumes(AgiGameT *game) { - char name[AGI_VOL_NAME_MAX]; - uint8_t v; - bool anyOpened; - - anyOpened = false; - for (v = 0; v < AGI_MAX_VOLUMES; v++) { - if (!buildVolName(name, sizeof(name), v)) { - return false; - } - game->volFiles[v] = jlDataOpen(name, "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; - } -} - - -// Case-insensitive ASCII equality. Both dictionary words and (lowercased) -// typed words are ASCII; fold A-Z so a stray uppercase still matches. -static bool wordEqCI(const char *a, const char *b) { - while (*a != '\0' && *b != '\0') { - char ca; - char cb; - - ca = *a; - cb = *b; - if (ca >= 'A' && ca <= 'Z') { - ca = (char)(ca + ('a' - 'A')); - } - if (cb >= 'A' && cb <= 'Z') { - cb = (char)(cb + ('a' - 'A')); - } - if (ca != cb) { - return false; - } - a++; - b++; - } - return *a == '\0' && *b == '\0'; -} - - -// ----- 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; - } - if (game->words != NULL) { - free(game->words); - game->words = NULL; - game->wordsLen = 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) { - uint8_t t; - - memset(game, 0, sizeof(*game)); - - // Resource files are opened by bare name relative to DATA/ (jlDataOpen); - // the game's data is whatever the packager staged there, so there is no - // runtime game-directory to thread through. - for (t = 0; t < AGI_RES_COUNT; t++) { - if (!loadDirectory(game, resTypeFileName((AgiResTypeE)t), (AgiResTypeE)t)) { - agiResClose(game); - return false; - } - } - - if (!openVolumes(game)) { - agiResClose(game); - return false; - } - - // The dictionary is optional (loadWords only fails on OOM); a game - // without WORDS.TOK still runs, just with no parser matches. - if (!loadWords(game)) { - agiResClose(game); - return false; - } - - return true; -} - - -uint16_t agiWordId(const AgiGameT *game, const char *word) { - const uint8_t *p; - const uint8_t *end; - char cur[AGI_WORD_MAX_LEN + 1u]; - uint8_t len; - - if (game->words == NULL || word == NULL) { - return AGI_WORD_UNKNOWN; - } - - // Word entries follow the fixed 52-byte letter-index header. Each entry: - // 1 byte prefixLen -- chars shared with the previous word - // n bytes suffix -- each (c & 0x7F) ^ 0x7F; the char with bit 7 - // set is the last one of the word - // 2 bytes groupId -- big-endian - // `cur` persists across entries so prefixLen chars carry forward. - p = game->words + AGI_WORDS_HEADER_BYTES; - end = game->words + game->wordsLen; - len = 0u; - while (p < end) { - uint8_t prefixLen; - uint16_t id; - - prefixLen = *p++; - if (prefixLen > AGI_WORD_MAX_LEN) { - break; // malformed dictionary - } - len = prefixLen; - for (;;) { - uint8_t c; - - if (p >= end) { - return AGI_WORD_UNKNOWN; - } - c = *p++; - if (len < AGI_WORD_MAX_LEN) { - cur[len] = (char)((c & 0x7Fu) ^ 0x7Fu); - len++; - } - if ((c & 0x80u) != 0u) { - break; - } - } - cur[len] = '\0'; - - if (p + 1 >= end) { - return AGI_WORD_UNKNOWN; - } - id = (uint16_t)(((uint16_t)p[0] << 8) | (uint16_t)p[1]); - p += 2; - if (wordEqCI(cur, word)) { - return id; - } - } - return AGI_WORD_UNKNOWN; -} diff --git a/examples/agi/agiText.c b/examples/agi/agiText.c deleted file mode 100644 index 744e355..0000000 --- a/examples/agi/agiText.c +++ /dev/null @@ -1,433 +0,0 @@ -// 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 -#include - - -#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++; - } - } -} diff --git a/examples/agi/agiView.c b/examples/agi/agiView.c deleted file mode 100644 index 137d0d3..0000000 --- a/examples/agi/agiView.c +++ /dev/null @@ -1,395 +0,0 @@ -// 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 -#include -#include - - -// 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 - - -// ----- File-local types ----- - -// Per-draw invariants for one cel blit, grouped so they live in memory -// rather than competing for the w65816's tiny register file. Passing a -// pointer to this keeps the hot blit helpers under the backend's -// register-allocation ceiling. -typedef struct { - const uint8_t *picPriority; - int16_t x; - int16_t width; - uint8_t actorPri; - uint8_t transparent; - bool mirrored; -} AgiCelBlitT; - - -// ----- Prototypes ----- - -static void blitPlanar(const AgiCelBlitT *blit, const uint8_t *rle, int16_t height, int16_t topY, jlSurfaceT *stage, int16_t destY); -static const uint8_t *blitRowChunky(const AgiCelBlitT *blit, const uint8_t *rle, const uint8_t *priRow, uint8_t *stageRow); -static void markCelDirty(jlSurfaceT *stage, int16_t x, int16_t width, int16_t destY, int16_t topY, int16_t height); -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) ----- - -// Planar fallback row walker (stage->pixels == NULL). Routes every -// opaque, visible pixel through jlDrawPixel so the port's c2p / -// plane-sync path stays correct. Split out of agiViewDraw so its -// locals don't pile onto the chunky path's register budget. -static void blitPlanar(const AgiCelBlitT *blit, const uint8_t *rle, int16_t height, int16_t topY, jlSurfaceT *stage, int16_t destY) { - int16_t width; - 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 runLen; - uint8_t r; - uint8_t picPri; - - width = blit->width; - 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 != blit->transparent) { - drawCx = blit->mirrored ? (int16_t)(width - 1 - cx) : cx; - agiX = (int16_t)(blit->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 = blit->picPriority[agiY * (int16_t)AGI_PIC_WIDTH + agiX]; - if (picPri <= blit->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++; - } - } - } -} - - -// Chunky fast-path walker for one source row. Consumes the cel's RLE -// stream from `rle`, writing pixel-doubled bytes into `stageRow` where -// the picture priority allows it, and returns the advanced RLE pointer -// so the caller can hand it to the next row. A NULL priRow / stageRow -// means the row is clipped out vertically: the RLE is still walked (so -// the pointer lands correctly for later rows) but nothing is drawn. -static const uint8_t *blitRowChunky(const AgiCelBlitT *blit, const uint8_t *rle, const uint8_t *priRow, uint8_t *stageRow) { - int16_t width; - int16_t cx; - int16_t drawCx; - int16_t agiX; - uint8_t rleByte; - uint8_t color; - uint8_t runLen; - uint8_t packed; - uint8_t r; - - width = blit->width; - cx = 0; - for (;;) { - rleByte = *rle++; - if (rleByte == 0u) { break; } - color = (uint8_t)(rleByte >> 4); - runLen = (uint8_t)(rleByte & 0x0Fu); - // Transparent runs and vertically-clipped rows both just skip - // ahead by runLen without touching the stage. - if (color == blit->transparent || 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 = blit->mirrored ? (int16_t)(width - 1 - cx) : cx; - agiX = (int16_t)(blit->x + drawCx); - if (agiX >= 0 && agiX < (int16_t)AGI_PIC_WIDTH) { - if (priRow[agiX] <= blit->actorPri) { - stageRow[agiX] = packed; - } - } - cx++; - } - } - return rle; -} - - -// Mark the cel's stage bounding box dirty once after a chunky blit. -// Computes the pixel-doubled bbox from the source rect, clips it to the -// stage so the surface marker doesn't reject the whole rect for an -// off-screen edge, and records it if any of it is on-screen. -static void markCelDirty(jlSurfaceT *stage, int16_t x, int16_t width, int16_t destY, int16_t topY, int16_t height) { - int16_t minStageX; - int16_t maxStageX; - int16_t minStageY; - int16_t maxStageY; - - minStageX = (int16_t)(x << 1); - maxStageX = (int16_t)((x + width) << 1); - minStageY = (int16_t)(destY + topY); - maxStageY = (int16_t)(destY + topY + height); - 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)); - } -} - - -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; -} - - -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; - AgiCelBlitT blit; - const uint8_t *rle; - bool mirrored; - int16_t height; - int16_t topY; - uint8_t *stagePixels; - - cel = resolveMirror(view, loopIdx, celIdx, &mirrored); - if (cel == NULL) { - return; - } - height = (int16_t)cel->height; - blit.picPriority = picPriority; - blit.x = x; - blit.width = (int16_t)cel->width; - blit.actorPri = actorPri; - blit.transparent = cel->transparentColor; - blit.mirrored = mirrored; - 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 cy; - int16_t agiY; - 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; - } - rle = blitRowChunky(&blit, rle, priRow, stageRow); - } - markCelDirty(stage, x, blit.width, destY, topY, height); - return; - } - - // Planar fallback (s->pixels NULL: Amiga Phase 9). Goes through - // jlDrawPixel so the port's c2p / plane-sync path stays correct. - blitPlanar(&blit, rle, height, topY, stage, destY); -} - - -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; -} diff --git a/examples/agi/agiVm.c b/examples/agi/agiVm.c deleted file mode 100644 index 4e7ec40..0000000 --- a/examples/agi/agiVm.c +++ /dev/null @@ -1,2415 +0,0 @@ -// AGI v2 LOGIC VM, Phase 1 sub-A: bytecode dispatch skeleton. -// -// This pass implements the variable / flag opcodes (0x00-0x11) plus -// the basic header parser. Control flow (IF / ELSE / GOTO), test -// commands (equaln, isset, said, ...), and side-effecting opcodes -// (new.room, load.pic, animate.obj, ...) come in later sub-phases. -// Unknown opcodes halt cleanly with the offending byte recorded so -// host tests can report exactly where execution stopped. -// -// All algorithms here are re-implementations written against the -// published AGI v2 LOGIC bytecode specification; no third-party -// interpreter source is referenced. - -#include "agi.h" - -#include "joey/core.h" -#include "joey/debug.h" - -#include -#include - - -// LOGIC resource layout: bytes 0-1 are the little-endian length of -// the bytecode section, followed by `bytecodeLen` bytes of bytecode, -// followed by the message table. The VM walks only the bytecode. -#define AGI_LOGIC_HDR_BYTES 2u - -// Implemented action opcodes (sub-A: arithmetic and flags). -#define OP_RETURN 0x00u -#define OP_INCREMENT 0x01u -#define OP_DECREMENT 0x02u -#define OP_ASSIGNN 0x03u -#define OP_ASSIGNV 0x04u -#define OP_ADDN 0x05u -#define OP_ADDV 0x06u -#define OP_SUBN 0x07u -#define OP_SUBV 0x08u -#define OP_LINDIRECTV 0x09u -#define OP_RINDIRECT 0x0Au -#define OP_LINDIRECTN 0x0Bu -#define OP_SET 0x0Cu -#define OP_RESET 0x0Du -#define OP_TOGGLE 0x0Eu -#define OP_SETV 0x0Fu -#define OP_RESETV 0x10u -#define OP_TOGGLEV 0x11u - -// Control flow (sub-B). 0xFF as an action opcode introduces an IF -// expression; the test list is terminated by an inner 0xFF, followed -// by a 2-byte LE skip offset that jumps past the IF body when the -// test is false. 0xFE is an unconditional relative GOTO (signed 16 -// bit), used by the compiler to skip ELSE bodies and to form loops. -#define OP_GOTO 0xFEu -#define OP_IF 0xFFu - -// Test command IDs (sub-B). Only valid inside an IF test list. -#define TEST_EQUALN 0x01u -#define TEST_EQUALV 0x02u -#define TEST_LESSN 0x03u -#define TEST_LESSV 0x04u -#define TEST_GREATERN 0x05u -#define TEST_GREATERV 0x06u -#define TEST_ISSET 0x07u -#define TEST_ISSETV 0x08u -#define TEST_HAS 0x09u -#define TEST_OBJ_IN_ROOM 0x0Au -#define TEST_POSN 0x0Bu -#define TEST_CONTROLLER 0x0Cu -#define TEST_HAVE_KEY 0x0Du -#define TEST_SAID 0x0Eu -#define TEST_COMPARE_STRINGS 0x0Fu -#define TEST_OBJ_IN_BOX 0x10u -#define TEST_CENTER_POSN 0x11u -#define TEST_RIGHT_POSN 0x12u -#define TEST_MAX 0x13u - -// Markers inside an IF test list. -#define TEST_END_LIST 0xFFu -#define TEST_OR_TOGGLE 0xFCu -#define TEST_NOT 0xFDu - -// Side-effecting opcodes wired to host callbacks in sub-D. Each has -// a real case in the dispatch switch; the host's callback table -// decides what (if anything) actually happens for the call. -#define OP_NEW_ROOM 0x12u -#define OP_NEW_ROOM_V 0x13u -#define OP_LOAD_LOGIC 0x14u -#define OP_LOAD_LOGIC_V 0x15u -#define OP_CALL 0x16u -#define OP_CALL_V 0x17u -#define OP_LOAD_PIC 0x18u -#define OP_DRAW_PIC 0x19u -#define OP_SHOW_PIC 0x1Au -#define OP_DISCARD_PIC 0x1Bu -#define OP_OVERLAY_PIC 0x1Cu - -// Object-table opcodes (animate.obj, set.view, ..., release.priority). -// These mutate the per-object slots in vm->objects[N]; per-frame -// rendering and per-cycle animation tick are handled outside the VM. -#define OP_LOAD_VIEW 0x1Eu -#define OP_LOAD_VIEW_V 0x1Fu -#define OP_DISCARD_VIEW 0x20u -#define OP_ANIMATE_OBJ 0x21u -#define OP_UNANIMATE_ALL 0x22u -#define OP_DRAW 0x23u -#define OP_ERASE 0x24u -#define OP_POSITION 0x25u -#define OP_POSITION_V 0x26u -#define OP_GET_POSN 0x27u -#define OP_REPOSITION 0x28u -#define OP_SET_VIEW 0x29u -#define OP_SET_VIEW_V 0x2Au -#define OP_SET_LOOP 0x2Bu -#define OP_SET_LOOP_V 0x2Cu -#define OP_FIX_LOOP 0x2Du -#define OP_RELEASE_LOOP 0x2Eu -#define OP_SET_CEL 0x2Fu -#define OP_SET_CEL_V 0x30u -#define OP_LAST_CEL 0x31u -#define OP_CURRENT_CEL 0x32u -#define OP_CURRENT_LOOP 0x33u -#define OP_CURRENT_VIEW 0x34u -#define OP_NUMBER_OF_LOOPS 0x35u -#define OP_SET_PRIORITY 0x36u -#define OP_SET_PRIORITY_V 0x37u -#define OP_RELEASE_PRIORITY 0x38u -#define OP_GET_PRIORITY 0x39u -#define OP_STOP_UPDATE 0x3Au -#define OP_START_UPDATE 0x3Bu -#define OP_FORCE_UPDATE 0x3Cu -#define OP_IGNORE_HORIZON 0x3Du -#define OP_OBSERVE_HORIZON 0x3Eu -#define OP_SET_HORIZON 0x3Fu -#define OP_OBJECT_ON_WATER 0x40u -#define OP_OBJECT_ON_LAND 0x41u -#define OP_OBJECT_ON_ANYTHING 0x42u -#define OP_IGNORE_OBJS 0x43u -#define OP_OBSERVE_OBJS 0x44u -#define OP_DISTANCE 0x45u - -// Animation cycling. -#define OP_STOP_CYCLING 0x46u -#define OP_START_CYCLING 0x47u -#define OP_NORMAL_CYCLE 0x48u -#define OP_END_OF_LOOP 0x49u -#define OP_REVERSE_CYCLE 0x4Au -#define OP_REVERSE_LOOP 0x4Bu -#define OP_CYCLE_TIME 0x4Cu - -// Motion. -#define OP_STOP_MOTION 0x4Du -#define OP_START_MOTION 0x4Eu -#define OP_STEP_SIZE 0x4Fu -#define OP_STEP_TIME 0x50u -#define OP_MOVE_OBJ 0x51u -#define OP_MOVE_OBJ_V 0x52u -#define OP_FOLLOW_EGO 0x53u -#define OP_WANDER 0x54u -#define OP_NORMAL_MOTION 0x55u -#define OP_SET_DIR 0x56u -#define OP_GET_DIR 0x57u - -// Object collision (most stub no-op; engine doesn't model blocking). -#define OP_IGNORE_BLOCKS 0x58u -#define OP_OBSERVE_BLOCKS 0x59u -#define OP_BLOCK 0x5Au -#define OP_UNBLOCK 0x5Bu - -// Inventory. -#define OP_GET 0x5Cu -#define OP_GET_V 0x5Du -#define OP_DROP 0x5Eu -#define OP_PUT 0x5Fu -#define OP_PUT_V 0x60u -#define OP_GET_ROOM_V 0x61u - -// Sound (stub for now; satisfies "sound finished" flag immediately). -#define OP_LOAD_SOUND 0x62u -#define OP_SOUND 0x63u -#define OP_STOP_SOUND 0x64u - -// Text. -#define OP_PRINT 0x65u -#define OP_PRINT_V 0x66u -#define OP_DISPLAY 0x67u -#define OP_DISPLAY_V 0x68u -#define OP_CLEAR_LINES 0x69u -#define OP_TEXT_SCREEN 0x6Au -#define OP_GRAPHICS 0x6Bu -#define OP_SET_CURSOR_CHAR 0x6Cu -#define OP_SET_TEXT_ATTRIBUTE 0x6Du -#define OP_SHAKE_SCREEN 0x6Eu -#define OP_CONFIGURE_SCREEN 0x6Fu -#define OP_STATUS_LINE_ON 0x70u -#define OP_STATUS_LINE_OFF 0x71u -#define OP_SET_STRING 0x72u -#define OP_GET_STRING 0x73u -#define OP_WORD_TO_STRING 0x74u -#define OP_PARSE 0x75u -#define OP_GET_NUM 0x76u - -// Input. -#define OP_PREVENT_INPUT 0x77u -#define OP_ACCEPT_INPUT 0x78u -#define OP_SET_KEY 0x79u - -// Picture composition. -#define OP_ADD_TO_PIC 0x7Au -#define OP_ADD_TO_PIC_V 0x7Bu - -// Misc. -#define OP_STATUS 0x7Cu -#define OP_SAVE_GAME 0x7Du -#define OP_RESTORE_GAME 0x7Eu -#define OP_INIT_DISK 0x7Fu -#define OP_RESTART_GAME 0x80u -#define OP_SHOW_OBJ 0x81u -#define OP_RANDOM 0x82u -#define OP_PROGRAM_CONTROL 0x83u -#define OP_PLAYER_CONTROL 0x84u -#define OP_OBJ_STATUS_V 0x85u -#define OP_QUIT 0x86u -#define OP_SHOW_MEM 0x87u -#define OP_PAUSE 0x88u -#define OP_ECHO_LINE 0x89u -#define OP_CANCEL_LINE 0x8Au -#define OP_INIT_JOY 0x8Bu -#define OP_TOGGLE_MONITOR 0x8Cu -#define OP_VERSION 0x8Du -#define OP_SCRIPT_SIZE 0x8Eu -#define OP_SET_GAME_ID 0x8Fu -#define OP_LOG 0x90u -#define OP_SET_SCAN_START 0x91u -#define OP_RESET_SCAN_START 0x92u -#define OP_REPOSITION_TO 0x93u -#define OP_REPOSITION_TO_V 0x94u -#define OP_TRACE_ON 0x95u -#define OP_TRACE_INFO 0x96u -#define OP_PRINT_AT 0x97u -#define OP_PRINT_AT_V 0x98u -#define OP_DISCARD_VIEW_V 0x99u -#define OP_CLEAR_TEXT_RECT 0x9Au -#define OP_SET_UPPER_LEFT 0x9Bu -#define OP_SET_MENU 0x9Cu -#define OP_SET_MENU_ITEM 0x9Du -#define OP_SUBMIT_MENU 0x9Eu -#define OP_ENABLE_ITEM 0x9Fu -#define OP_DISABLE_ITEM 0xA0u -#define OP_MENU_INPUT 0xA1u -#define OP_SHOW_OBJ_V 0xA2u -#define OP_OPEN_DIALOGUE 0xA3u -#define OP_CLOSE_DIALOGUE 0xA4u -#define OP_MUL_N 0xA5u -#define OP_MUL_V 0xA6u -#define OP_DIV_N 0xA7u -#define OP_DIV_V 0xA8u -#define OP_CLOSE_WINDOW 0xA9u -#define OP_SET_SIMPLE 0xAAu - -// Special vars touched by the engine outside script control. -#define ENG_VAR_EGO_DIR 6u -#define ENG_VAR_EGO_VIEW 16u - -// Inventory state lives in vm->vars[88..127] (40 slots) — each slot -// stores the room id where item N is currently (255 = ego inventory). -// AGI v2 fixes the inventory length to about 40 in most games; we -// reserve more (88..255) but cap at 64 items in practice. -#define INV_ROOM_INVENTORY 255u - -// Sentinel value in kActionArgBytes meaning "no v2 opcode at this -// index"; the dispatcher halts when it sees this in the table. -#define ACTION_UNKNOWN 0xFFu - - -// Result of one opcode-group handler in the split agiVmRun dispatch. -// VM_DISPATCH_CONTINUE: opcode handled, advance to the next. RETURN: -// handled, stop and let agiVmRun return vm->haltReason. UNHANDLED: opcode -// is owned by a later group's handler. -typedef enum { - VM_DISPATCH_CONTINUE = 0, - VM_DISPATCH_RETURN = 1, - VM_DISPATCH_UNHANDLED = 2 -} VmDispatchE; - - -// ----- Prototypes ----- - -static bool enterLogic(AgiVmT *vm, uint8_t logicId); -static bool evalOneTest(AgiVmT *vm, uint8_t testOp); -static bool evalTests(AgiVmT *vm); -static bool executeGoto(AgiVmT *vm); -static bool executeIf(AgiVmT *vm); -static void expandMessage(AgiVmT *vm, const char *src, char *dst, uint16_t dstCap); -static const char *fetchMessageOrEmpty(AgiVmT *vm, uint8_t logicId, uint8_t msgId); -static void openPrintModal(AgiVmT *vm, const char *expanded); -static bool popFrame(AgiVmT *vm); -static bool pushFrame(AgiVmT *vm); -static bool readByte(AgiVmT *vm, uint8_t *out); -static bool readU16(AgiVmT *vm, uint16_t *out); -static bool saidMatch(const AgiVmT *vm, const uint16_t *pat, uint8_t patCount); -static VmDispatchE vmStepCore(AgiVmT *vm, uint8_t op); -static VmDispatchE vmStepMisc(AgiVmT *vm, uint8_t op); -static VmDispatchE vmStepObject(AgiVmT *vm, uint8_t op); -static VmDispatchE vmStepText(AgiVmT *vm, uint8_t op); -static void writeTextRow(AgiVmT *vm, uint8_t row, uint8_t col, const char *expanded); - - -// Per-action-opcode argument byte counts, indexed by opcode id. The -// real handlers (arithmetic / flags / IF / GOTO / new.room) consult -// this only as a sanity check; unrecognized opcodes (entries set to -// ACTION_UNKNOWN) halt the VM with AGI_VM_HALT_UNKNOWN_OP. Every other -// entry is treated as a stub: its argument bytes are consumed and no -// side effect is taken. This lets the VM walk arbitrary v2 logic -// without halting at every yet-to-be-implemented side-effecting -// command (load.pic / animate.obj / sound / ...). -static const uint8_t kActionArgBytes[256] = { - /* 0x00 */ 0u, 1u, 1u, 2u, 2u, 2u, 2u, 2u, 2u, 2u, 2u, 2u, 1u, 1u, 1u, 1u, - /* 0x10 */ 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 0u, 1u, 1u, 0u, 1u, 1u, - /* 0x20 */ 1u, 1u, 0u, 1u, 1u, 3u, 3u, 3u, 3u, 2u, 2u, 2u, 2u, 1u, 1u, 2u, - /* 0x30 */ 2u, 2u, 2u, 2u, 2u, 2u, 2u, 2u, 1u, 2u, 1u, 1u, 1u, 1u, 1u, 1u, - /* 0x40 */ 1u, 1u, 1u, 1u, 1u, 3u, 1u, 1u, 1u, 2u, 1u, 2u, 2u, 1u, 1u, 2u, - /* 0x50 */ 2u, 5u, 5u, 3u, 1u, 1u, 2u, 2u, 1u, 1u, 4u, 0u, 1u, 1u, 1u, 2u, - /* 0x60 */ 2u, 2u, 1u, 2u, 0u, 1u, 1u, 3u, 3u, 3u, 0u, 0u, 1u, 2u, 1u, 3u, - /* 0x70 */ 0u, 0u, 2u, 5u, 2u, 1u, 2u, 0u, 0u, 3u, 7u, 7u, 0u, 0u, 0u, 0u, - /* 0x80 */ 0u, 1u, 3u, 0u, 0u, 1u, 1u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 1u, 1u, - /* 0x90 */ 1u, 0u, 0u, 3u, 3u, 0u, 3u, 4u, 4u, 1u, 4u, 2u, 1u, 2u, 0u, 1u, - /* 0xA0 */ 1u, 0u, 1u, 0u, 0u, 2u, 2u, 2u, 2u, 0u, 1u, 0u, 0u, 0u, 1u, 1u, - /* 0xB0 */ 0u, 1u, 0u, 4u, 2u, 0u, 0u, - ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, - /* 0xBA */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, - /* 0xC0 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, - /* 0xC8 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, - /* 0xD0 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, - /* 0xD8 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, - /* 0xE0 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, - /* 0xE8 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, - /* 0xF0 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, - /* 0xF8 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN -}; - - -// Per-test-command argument byte counts, indexed by test opcode id. -// 0x0E (said) is variable-length and gets special-cased rather than -// indexed via this table. -static const uint8_t kTestArgBytes[TEST_MAX] = { - /* 0x00 */ 0u, - /* 0x01 equaln */ 2u, - /* 0x02 equalv */ 2u, - /* 0x03 lessn */ 2u, - /* 0x04 lessv */ 2u, - /* 0x05 greatern */ 2u, - /* 0x06 greaterv */ 2u, - /* 0x07 isset */ 1u, - /* 0x08 issetv */ 1u, - /* 0x09 has */ 1u, - /* 0x0A obj.in.room */ 2u, - /* 0x0B posn */ 5u, - /* 0x0C controller */ 1u, - /* 0x0D have.key */ 0u, - /* 0x0E said */ 0u, - /* 0x0F compare.strings */ 2u, - /* 0x10 obj.in.box */ 5u, - /* 0x11 center.posn */ 5u, - /* 0x12 right.posn */ 5u -}; - - -// ----- Internal helpers (alphabetical) ----- - -// Switch the VM's running logic to `logicId`. Used by OP_CALL and -// OP_CALL_V after pushFrame has saved the caller's position. Returns -// false (and sets haltReason) if the host has no bytecode for the -// requested id. -static bool enterLogic(AgiVmT *vm, uint8_t logicId) { - const uint8_t *bytecode; - uint16_t length; - - if (vm->callbacks.fetchLogic == NULL) { - vm->haltReason = AGI_VM_HALT_NO_LOGIC; - return false; - } - length = 0u; - bytecode = vm->callbacks.fetchLogic(vm->callbacks.ctx, logicId, &length); - if (bytecode == NULL || length == 0u) { - vm->haltReason = AGI_VM_HALT_NO_LOGIC; - return false; - } - vm->code = bytecode; - vm->codeLength = length; - vm->pc = 0u; - vm->currentLogicId = logicId; - return true; -} - - -// Evaluate a single test command, with pc currently positioned at -// the first argument byte. Always consumes the test's full argument -// list so the caller's stream position stays in sync, even when the -// test is stubbed to a constant false. Tests this sub-phase doesn't -// implement (objects, parser, controllers, key state) read their -// args and return false; the VM walks past the IF body as if the -// test evaluated against fresh game state. -static bool evalOneTest(AgiVmT *vm, uint8_t testOp) { - uint8_t args[5]; - uint8_t i; - uint8_t argBytes; - - if (testOp == 0u || testOp >= TEST_MAX) { - vm->lastUnknownOp = testOp; - vm->haltReason = AGI_VM_HALT_UNKNOWN_OP; - return false; - } - - if (testOp == TEST_SAID) { - // Variable-length: 1-byte word count, then that many 16-bit - // little-endian word IDs. Read the pattern (always, to keep pc in - // sync) then match it against what the player typed this cycle. - uint16_t pat[AGI_MAX_PARSED]; - uint8_t count; - uint8_t lo; - uint8_t hi; - uint8_t k; - bool matched; - - if (!readByte(vm, &count)) { - return false; - } - for (k = 0u; k < count; k++) { - if (!readByte(vm, &lo) || !readByte(vm, &hi)) { - return false; - } - if (k < AGI_MAX_PARSED) { - pat[k] = (uint16_t)((uint16_t)lo | ((uint16_t)hi << 8)); - } - } - // said() fires only on the cycle input was entered; a match consumes - // that input (clears flag 2) so no later said() double-handles it. - if (vm->flags[AGI_FLAG_INPUT] == 0u || count > AGI_MAX_PARSED) { - return false; - } - matched = saidMatch(vm, pat, count); - if (matched) { - vm->flags[AGI_FLAG_INPUT] = 0u; - } - return matched; - } - - argBytes = kTestArgBytes[testOp]; - for (i = 0u; i < argBytes; i++) { - if (!readByte(vm, &args[i])) { - return false; - } - } - - switch (testOp) { - case TEST_EQUALN: return vm->vars[args[0]] == args[1]; - case TEST_EQUALV: return vm->vars[args[0]] == vm->vars[args[1]]; - case TEST_LESSN: return vm->vars[args[0]] < args[1]; - case TEST_LESSV: return vm->vars[args[0]] < vm->vars[args[1]]; - case TEST_GREATERN: return vm->vars[args[0]] > args[1]; - case TEST_GREATERV: return vm->vars[args[0]] > vm->vars[args[1]]; - case TEST_ISSET: return vm->flags[args[0]] != 0u; - case TEST_ISSETV: return vm->flags[vm->vars[args[0]]] != 0u; - case TEST_HAVE_KEY: - if (vm->callbacks.haveKey != NULL) { - return vm->callbacks.haveKey(vm->callbacks.ctx); - } - return false; - case TEST_CONTROLLER: { - uint8_t c = args[0]; - if (c < AGI_MAX_CONTROLLERS && vm->controllerFired[c] != 0u) { - vm->controllerFired[c] = 0u; - return true; - } - return false; - } - case TEST_POSN: { - uint8_t n = args[0]; - int16_t x1 = (int16_t)args[1]; - int16_t y1 = (int16_t)args[2]; - int16_t x2 = (int16_t)args[3]; - int16_t y2 = (int16_t)args[4]; - const AgiObjectT *o; - - if (n >= AGI_MAX_OBJECTS) return false; - o = &vm->objects[n]; - return (o->x >= x1 && o->x <= x2 && o->y >= y1 && o->y <= y2); - } - case TEST_OBJ_IN_BOX: { - // Same shape as posn but checks the *entire* cel bbox is - // inside the box. Without view dimensions in the VM we - // approximate with point containment (== posn). - uint8_t n = args[0]; - int16_t x1 = (int16_t)args[1]; - int16_t y1 = (int16_t)args[2]; - int16_t x2 = (int16_t)args[3]; - int16_t y2 = (int16_t)args[4]; - const AgiObjectT *o; - - if (n >= AGI_MAX_OBJECTS) return false; - o = &vm->objects[n]; - return (o->x >= x1 && o->x <= x2 && o->y >= y1 && o->y <= y2); - } - case TEST_CENTER_POSN: { - uint8_t n = args[0]; - int16_t x1 = (int16_t)args[1]; - int16_t y1 = (int16_t)args[2]; - int16_t x2 = (int16_t)args[3]; - int16_t y2 = (int16_t)args[4]; - const AgiObjectT *o; - - if (n >= AGI_MAX_OBJECTS) return false; - o = &vm->objects[n]; - return (o->x >= x1 && o->x <= x2 && o->y >= y1 && o->y <= y2); - } - case TEST_RIGHT_POSN: { - uint8_t n = args[0]; - int16_t x1 = (int16_t)args[1]; - int16_t y1 = (int16_t)args[2]; - int16_t x2 = (int16_t)args[3]; - int16_t y2 = (int16_t)args[4]; - const AgiObjectT *o; - - if (n >= AGI_MAX_OBJECTS) return false; - o = &vm->objects[n]; - return (o->x >= x1 && o->x <= x2 && o->y >= y1 && o->y <= y2); - } - case TEST_HAS: - // Inventory: var[88+arg] is the item room (255 = held). - // Without OBJECT-table parsing, default to "not held". - return false; - case TEST_OBJ_IN_ROOM: - // Same: defaults to false until OBJECT support lands. - return false; - default: return false; - } -} - - -// Walk the IF test list at pc. Default operator is AND; 0xFC toggles -// in/out of an OR group whose result is then ANDed into the running -// AND. 0xFD prefixes the next test with NOT. 0xFF terminates the -// list. Leaves pc positioned right after the terminator so the -// caller can read the 2-byte skip offset. -static bool evalTests(AgiVmT *vm) { - bool andResult; - bool orResult; - bool inOrGroup; - bool notNext; - bool testResult; - uint8_t op; - - andResult = true; - orResult = false; - inOrGroup = false; - notNext = false; - - for (;;) { - if (!readByte(vm, &op)) { - return false; - } - if (op == TEST_END_LIST) { - if (inOrGroup) { - andResult = (bool)(andResult && orResult); - } - return andResult; - } - if (op == TEST_OR_TOGGLE) { - if (inOrGroup) { - andResult = (bool)(andResult && orResult); - inOrGroup = false; - } else { - orResult = false; - inOrGroup = true; - } - continue; - } - if (op == TEST_NOT) { - notNext = (bool)!notNext; - continue; - } - - testResult = evalOneTest(vm, op); - if (vm->haltReason != AGI_VM_HALT_NONE) { - return false; - } - if (notNext) { - testResult = (bool)!testResult; - notNext = false; - } - if (inOrGroup) { - orResult = (bool)(orResult || testResult); - } else { - andResult = (bool)(andResult && testResult); - } - } -} - - -// Unconditional relative jump. Offset is signed 16-bit; negative -// values allow backward jumps for loops. 16-bit arithmetic with -// separate positive / negative paths avoids dragging int32 into the -// 65816 build. -// Substitute AGI v2 message placeholders into an output buffer. -// Recognizes %v (decimal var value), %0 (decimal var with -// leading zero pad), %s (game string slot), %m (recursive -// message lookup in current logic). Unknown sequences are passed -// through verbatim. Always NUL-terminates within dstCap. -static void expandMessage(AgiVmT *vm, const char *src, char *dst, uint16_t dstCap) { - uint16_t out; - uint8_t pad; - uint8_t varIdx; - uint8_t value; - uint8_t tmp[4]; - int8_t digit; - char ch; - - out = 0u; - if (dstCap == 0u) { - return; - } - while (src != NULL && *src != '\0' && out + 1u < dstCap) { - ch = *src++; - if (ch != '%') { - dst[out++] = ch; - continue; - } - if (*src == '\0') { - dst[out++] = '%'; - break; - } - { - char tag = *src++; - uint8_t arg = 0u; - uint8_t digitsRead = 0u; - - while (*src >= '0' && *src <= '9' && digitsRead < 3u) { - arg = (uint8_t)(arg * 10u + (uint8_t)(*src - '0')); - src++; - digitsRead++; - } - if (digitsRead == 0u) { - if (out + 2u < dstCap) { - dst[out++] = '%'; - dst[out++] = tag; - } - continue; - } - switch (tag) { - case 'v': case '0': - pad = (tag == '0') ? 1u : 0u; - varIdx = arg; - value = vm->vars[varIdx]; - digit = 0; - do { - tmp[digit++] = (uint8_t)(value % 10u); - value = (uint8_t)(value / 10u); - } while (value != 0u && digit < 3); - while (pad && digit < 3) { - tmp[digit++] = 0u; - } - while (digit > 0 && out + 1u < dstCap) { - digit--; - dst[out++] = (char)('0' + tmp[digit]); - } - break; - case 's': - if (arg < AGI_MAX_STRINGS) { - const char *s = vm->strings[arg]; - while (*s != '\0' && out + 1u < dstCap) { - dst[out++] = *s++; - } - } - break; - case 'm': { - const char *nested = fetchMessageOrEmpty(vm, vm->currentLogicId, arg); - char scratch[AGI_PRINT_MAX_LEN + 1u]; - - expandMessage(vm, nested, scratch, sizeof(scratch)); - { - const char *s = scratch; - while (*s != '\0' && out + 1u < dstCap) { - dst[out++] = *s++; - } - } - break; - } - case 'g': - case 'w': - // %g (logic 0 message — global) and %w (parser word) - // — fall back to no-op so the script doesn't see - // garbage substitutions. - break; - default: - if (out + 2u < dstCap) { - dst[out++] = '%'; - dst[out++] = tag; - } - break; - } - } - } - dst[out] = '\0'; -} - - -static bool executeGoto(AgiVmT *vm) { - uint16_t raw; - int16_t signedOffset; - uint16_t magnitude; - - if (!readU16(vm, &raw)) { - return false; - } - signedOffset = (int16_t)raw; - if (signedOffset >= 0) { - magnitude = (uint16_t)signedOffset; - if (magnitude > (uint16_t)(vm->codeLength - vm->pc)) { - vm->haltReason = AGI_VM_HALT_TRUNCATED; - return false; - } - vm->pc = (uint16_t)(vm->pc + magnitude); - } else { - magnitude = (uint16_t)(-signedOffset); - if (magnitude > vm->pc) { - vm->haltReason = AGI_VM_HALT_TRUNCATED; - return false; - } - vm->pc = (uint16_t)(vm->pc - magnitude); - } - return true; -} - - -static bool executeIf(AgiVmT *vm) { - bool cond; - uint16_t skip; - - cond = evalTests(vm); - if (vm->haltReason != AGI_VM_HALT_NONE) { - return false; - } - if (!readU16(vm, &skip)) { - return false; - } - if (!cond) { - if (skip > (uint16_t)(vm->codeLength - vm->pc)) { - vm->haltReason = AGI_VM_HALT_TRUNCATED; - return false; - } - vm->pc = (uint16_t)(vm->pc + skip); - } - return true; -} - - -static const char *fetchMessageOrEmpty(AgiVmT *vm, uint8_t logicId, uint8_t msgId) { - const char *msg; - - if (vm->callbacks.fetchMessage == NULL) { - return ""; - } - msg = vm->callbacks.fetchMessage(vm->callbacks.ctx, logicId, msgId); - return (msg != NULL) ? msg : ""; -} - - -// AGI print() default text-window width: ~30 columns. Sierra's -// interpreter word-wraps the message at this width before measuring -// the window. Without wrapping a long single-line message (e.g. -// KQ3's "Gwydion is a lonely lad...") overflows the screen edge. -#define AGI_PRINT_WRAP_COLS 30u - - -static void openPrintModal(AgiVmT *vm, const char *expanded) { - uint16_t i; - uint16_t out; - uint16_t cap; - uint16_t lineStart; // index in printModal.message where current line begins - uint16_t lastSpace; // index of last space char in current line (0 = none yet) - uint16_t col; // visible columns on current line - - cap = AGI_PRINT_MAX_LEN; - if (expanded == NULL) { - vm->printModal.message[0] = '\0'; - vm->printModal.length = 0u; - vm->printModal.active = true; - vm->haltReason = AGI_VM_HALT_PRINT_PENDING; - return; - } - - // Stream characters from expanded into printModal.message, breaking - // at AGI_PRINT_WRAP_COLS by replacing the last space on the line - // with '\n'. Preserves explicit '\n' from the source message. - out = 0u; - lineStart = 0u; - lastSpace = 0u; - col = 0u; - i = 0u; - while (expanded[i] != '\0' && out < cap - 1u) { - char c = expanded[i++]; - if (c == '\n') { - vm->printModal.message[out++] = '\n'; - lineStart = out; - lastSpace = 0u; - col = 0u; - continue; - } - vm->printModal.message[out++] = c; - if (c == ' ') { - lastSpace = (uint16_t)(out - 1u); - } - col++; - if (col >= AGI_PRINT_WRAP_COLS) { - if (lastSpace != 0u && lastSpace >= lineStart) { - // Replace the last space on this line with '\n' so - // the partial word that followed wraps to the next. - vm->printModal.message[lastSpace] = '\n'; - col = (uint16_t)(out - lastSpace - 1u); - lineStart = (uint16_t)(lastSpace + 1u); - lastSpace = 0u; - } else { - // No space to break on: hard-wrap mid-token. - if (out < cap - 1u) { - vm->printModal.message[out++] = '\n'; - } - lineStart = out; - lastSpace = 0u; - col = 0u; - } - } - } - vm->printModal.message[out] = '\0'; - vm->printModal.length = out; - vm->printModal.active = true; - vm->haltReason = AGI_VM_HALT_PRINT_PENDING; -} - - -// Restore the most recently pushed frame as the active context. -// Caller (OP_RETURN at depth > 0) has already confirmed callDepth > 0. -static bool popFrame(AgiVmT *vm) { - const AgiVmFrameT *f; - - vm->callDepth--; - f = &vm->callStack[vm->callDepth]; - vm->code = f->code; - vm->codeLength = f->codeLength; - vm->pc = f->pc; - vm->currentLogicId = f->logicId; - return true; -} - - -// Save the active context onto the call stack so OP_CALL can switch -// to a different logic. Returns false (and halts) if the stack is -// already full -- AGI v2 games keep call depth shallow so a small -// fixed-size stack is sufficient. -static bool pushFrame(AgiVmT *vm) { - AgiVmFrameT *f; - - if (vm->callDepth >= AGI_VM_CALL_STACK_MAX) { - vm->haltReason = AGI_VM_HALT_STACK_OVER; - return false; - } - f = &vm->callStack[vm->callDepth]; - f->code = vm->code; - f->codeLength = vm->codeLength; - f->pc = vm->pc; - f->logicId = vm->currentLogicId; - vm->callDepth++; - return true; -} - - -static bool readByte(AgiVmT *vm, uint8_t *out) { - if (vm->pc >= vm->codeLength) { - vm->haltReason = AGI_VM_HALT_TRUNCATED; - return false; - } - *out = vm->code[vm->pc]; - vm->pc++; - return true; -} - - -static bool readU16(AgiVmT *vm, uint16_t *out) { - uint8_t lo; - uint8_t hi; - - if (!readByte(vm, &lo)) { - return false; - } - if (!readByte(vm, &hi)) { - return false; - } - *out = (uint16_t)(lo | ((uint16_t)hi << 8)); - return true; -} - - -// Match the parsed player command against a said() word pattern. -// AGI_WORD_REST (9999) matches the rest of the line (including nothing); -// AGI_WORD_ANYWORD (1) matches exactly one arbitrary word; -// any other id matches that exact group id. -// An exact match requires every typed word to be consumed by the pattern. -static bool saidMatch(const AgiVmT *vm, const uint16_t *pat, uint8_t patCount) { - uint8_t pi; - uint8_t wi; - - pi = 0u; - wi = 0u; - while (pi < patCount) { - uint16_t pw; - - pw = pat[pi]; - if (pw == AGI_WORD_REST) { - return true; - } - if (wi >= vm->parsedWordCount) { - return false; - } - if (pw != AGI_WORD_ANYWORD && pw != vm->parsedWords[wi]) { - return false; - } - wi++; - pi++; - } - return wi == vm->parsedWordCount; -} - - -static void writeTextRow(AgiVmT *vm, uint8_t row, uint8_t col, const char *expanded) { - uint16_t i; - uint16_t off; - - if (row >= AGI_TEXT_ROWS || col >= AGI_TEXT_COLS) { - return; - } - if (expanded == NULL) { - return; - } - // Pad text[0..col-1] with spaces. Otherwise the row starts with - // '\0' and agiTextRender skips the whole row -- display(r,c,m) - // with c>0 produces no visible text. - for (i = 0u; i < (uint16_t)col; i++) { - if (vm->textRows[row].text[i] == '\0') { - vm->textRows[row].text[i] = ' '; - } - } - off = col; - i = 0u; - while (expanded[i] != '\0' && off < AGI_TEXT_COLS) { - if (expanded[i] == '\n') { - // newline — clear rest of row, advance to next row col 0 - while (off < AGI_TEXT_COLS) { - vm->textRows[row].text[off++] = ' '; - } - vm->textRows[row].text[AGI_TEXT_COLS] = '\0'; - row++; - off = 0u; - if (row >= AGI_TEXT_ROWS) { - return; - } - } else { - vm->textRows[row].text[off++] = expanded[i]; - } - i++; - } - vm->textRows[row].text[AGI_TEXT_COLS] = '\0'; - vm->textRows[row].fg = vm->textFg; - vm->textRows[row].bg = vm->textBg; -} - - -// ----- Public API (alphabetical) ----- - -void agiVmAckPrint(AgiVmT *vm) { - vm->printModal.active = false; - vm->printModal.length = 0u; - vm->printModal.message[0] = '\0'; - if (vm->haltReason == AGI_VM_HALT_PRINT_PENDING) { - vm->haltReason = AGI_VM_HALT_NONE; - } -} - - -void agiVmDispatchKey(AgiVmT *vm, uint8_t key1, uint8_t key2) { - uint8_t i; - - for (i = 0u; i < (uint8_t)AGI_MAX_KEY_BINDINGS; i++) { - AgiKeyBindingT *b; - - b = &vm->keyBindings[i]; - if (b->active == 0u) { - continue; - } - if (b->key1 == key1 && b->key2 == key2) { - if (b->controller < AGI_MAX_CONTROLLERS) { - vm->controllerFired[b->controller] = 1u; - } - } - } -} - - -void agiVmInit(AgiVmT *vm) { - uint8_t i; - - memset(vm, 0, sizeof(*vm)); - vm->haltReason = AGI_VM_HALT_NONE; - vm->statusLineOn = false; - vm->statusLineRow = 0u; - vm->textFg = 15u; - vm->textBg = 0u; - vm->horizon = 36u; // canonical AGI default - vm->programControl = false; - vm->acceptInput = false; - vm->menuOpen = 0xFFu; - for (i = 0u; i < (uint8_t)AGI_TEXT_ROWS; i++) { - vm->textRows[i].text[0] = '\0'; - vm->textRows[i].fg = 15u; - vm->textRows[i].bg = 0u; - } - agiObjResetAll(vm); -} - - -bool agiVmLoadLogic(AgiVmT *vm, const uint8_t *resourceBytes, uint16_t resourceLength) { - uint16_t bytecodeLen; - - if (resourceLength < AGI_LOGIC_HDR_BYTES) { - return false; - } - bytecodeLen = (uint16_t)(resourceBytes[0] | ((uint16_t)resourceBytes[1] << 8)); - if ((uint16_t)(AGI_LOGIC_HDR_BYTES + bytecodeLen) > resourceLength) { - return false; - } - vm->code = resourceBytes + AGI_LOGIC_HDR_BYTES; - vm->codeLength = bytecodeLen; - vm->pc = 0u; - vm->haltReason = AGI_VM_HALT_NONE; - vm->lastUnknownOp = 0u; - vm->newRoomId = 0u; - vm->currentLogicId = 0u; - vm->callDepth = 0u; - return true; -} - - -void agiVmResetToLogic(AgiVmT *vm, const uint8_t *bytecode, uint16_t bytecodeLength, uint8_t logicId) { - vm->code = bytecode; - vm->codeLength = bytecodeLength; - vm->pc = 0u; - vm->haltReason = AGI_VM_HALT_NONE; - vm->lastUnknownOp = 0u; - vm->newRoomId = 0u; - vm->currentLogicId = logicId; - vm->callDepth = 0u; -} - - -AgiVmHaltE agiVmRun(AgiVmT *vm) { - uint8_t op; - VmDispatchE res; - - while (vm->haltReason == AGI_VM_HALT_NONE) { - if (!readByte(vm, &op)) { - // readByte sets haltReason = TRUNCATED on out-of-range. - return vm->haltReason; - } - res = vmStepCore(vm, op); - if (res == VM_DISPATCH_UNHANDLED) { - res = vmStepObject(vm, op); - } - if (res == VM_DISPATCH_UNHANDLED) { - res = vmStepText(vm, op); - } - if (res == VM_DISPATCH_UNHANDLED) { - res = vmStepMisc(vm, op); - } - if (res == VM_DISPATCH_RETURN) { - return vm->haltReason; - } - } - return vm->haltReason; -} - - -// Opcode dispatch is partitioned across four __attribute__((noinline)) -// handlers so the llvm-mos w65816 register allocator is never asked to -// colour the whole 1200-line switch at once (it ran out of registers when -// it was a single function). Each handler returns VM_DISPATCH_CONTINUE -// (advance to the next opcode), VM_DISPATCH_RETURN (stop; agiVmRun returns -// vm->haltReason), or VM_DISPATCH_UNHANDLED (opcode is owned by a later -// group). vmStepMisc owns the default fallback and never returns UNHANDLED. -// Behaviour is byte-for-byte identical to the original monolithic switch. -// -// Group 1: RETURN, arithmetic/flags (0x00-0x11), IF/GOTO, new.room, -// load.logic, call, and the picture-cache opcodes (0x12-0x1C). -static VmDispatchE __attribute__((noinline)) vmStepCore(AgiVmT *vm, uint8_t op) { - uint8_t a; - uint8_t b; - - switch (op) { - case OP_RETURN: - if (vm->callDepth > 0u) { - popFrame(vm); - } else { - vm->haltReason = AGI_VM_HALT_RETURN; - } - break; - - case OP_INCREMENT: - if (!readByte(vm, &a)) { - return VM_DISPATCH_RETURN; - } - // AGI spec: increment clamps at 255. - if (vm->vars[a] != 0xFFu) { - vm->vars[a] = (uint8_t)(vm->vars[a] + 1u); - } - break; - - case OP_DECREMENT: - if (!readByte(vm, &a)) { - return VM_DISPATCH_RETURN; - } - // AGI spec: decrement clamps at 0 (does NOT wrap). - // Sparkle counters like v221..v224 stay at 0 once - // assigned 0, which is how the wizard scene suppresses - // the logo-sparkle loop until sound 22 ends. - if (vm->vars[a] != 0u) { - vm->vars[a] = (uint8_t)(vm->vars[a] - 1u); - } - break; - - case OP_ASSIGNN: - if (!readByte(vm, &a) || !readByte(vm, &b)) { - return VM_DISPATCH_RETURN; - } - vm->vars[a] = b; - break; - - case OP_ASSIGNV: - if (!readByte(vm, &a) || !readByte(vm, &b)) { - return VM_DISPATCH_RETURN; - } - vm->vars[a] = vm->vars[b]; - break; - - case OP_ADDN: - if (!readByte(vm, &a) || !readByte(vm, &b)) { - return VM_DISPATCH_RETURN; - } - vm->vars[a] = (uint8_t)(vm->vars[a] + b); - break; - - case OP_ADDV: - if (!readByte(vm, &a) || !readByte(vm, &b)) { - return VM_DISPATCH_RETURN; - } - vm->vars[a] = (uint8_t)(vm->vars[a] + vm->vars[b]); - break; - - case OP_SUBN: - if (!readByte(vm, &a) || !readByte(vm, &b)) { - return VM_DISPATCH_RETURN; - } - vm->vars[a] = (uint8_t)(vm->vars[a] - b); - break; - - case OP_SUBV: - if (!readByte(vm, &a) || !readByte(vm, &b)) { - return VM_DISPATCH_RETURN; - } - vm->vars[a] = (uint8_t)(vm->vars[a] - vm->vars[b]); - break; - - case OP_LINDIRECTV: - // var[a] = var[var[b]] - if (!readByte(vm, &a) || !readByte(vm, &b)) { - return VM_DISPATCH_RETURN; - } - vm->vars[a] = vm->vars[vm->vars[b]]; - break; - - case OP_RINDIRECT: - // var[var[a]] = var[b] - if (!readByte(vm, &a) || !readByte(vm, &b)) { - return VM_DISPATCH_RETURN; - } - vm->vars[vm->vars[a]] = vm->vars[b]; - break; - - case OP_LINDIRECTN: - // var[var[a]] = b (immediate) - if (!readByte(vm, &a) || !readByte(vm, &b)) { - return VM_DISPATCH_RETURN; - } - vm->vars[vm->vars[a]] = b; - break; - - case OP_SET: - if (!readByte(vm, &a)) { - return VM_DISPATCH_RETURN; - } - vm->flags[a] = 1u; - break; - - case OP_RESET: - if (!readByte(vm, &a)) { - return VM_DISPATCH_RETURN; - } - vm->flags[a] = 0u; - break; - - case OP_TOGGLE: - if (!readByte(vm, &a)) { - return VM_DISPATCH_RETURN; - } - vm->flags[a] = (uint8_t)(vm->flags[a] ? 0u : 1u); - break; - - case OP_SETV: - if (!readByte(vm, &a)) { - return VM_DISPATCH_RETURN; - } - vm->flags[vm->vars[a]] = 1u; - break; - - case OP_RESETV: - if (!readByte(vm, &a)) { - return VM_DISPATCH_RETURN; - } - vm->flags[vm->vars[a]] = 0u; - break; - - case OP_TOGGLEV: - if (!readByte(vm, &a)) { - return VM_DISPATCH_RETURN; - } - vm->flags[vm->vars[a]] = (uint8_t)(vm->flags[vm->vars[a]] ? 0u : 1u); - break; - - case OP_IF: - if (!executeIf(vm)) { - return VM_DISPATCH_RETURN; - } - break; - - case OP_GOTO: - if (!executeGoto(vm)) { - return VM_DISPATCH_RETURN; - } - break; - - case OP_NEW_ROOM: - if (!readByte(vm, &a)) { - return VM_DISPATCH_RETURN; - } - vm->newRoomId = a; - vm->haltReason = AGI_VM_HALT_NEW_ROOM; - jlLogF("OP_NEW_ROOM -> %u (v11=%u v12=%u)", - (unsigned)a, (unsigned)vm->vars[11], (unsigned)vm->vars[12]); - jlLogFlush(); - break; - - case OP_NEW_ROOM_V: - if (!readByte(vm, &a)) { - return VM_DISPATCH_RETURN; - } - vm->newRoomId = vm->vars[a]; - vm->haltReason = AGI_VM_HALT_NEW_ROOM; - jlLogF("OP_NEW_ROOM_V -> %u (v11=%u v12=%u)", - (unsigned)vm->newRoomId, (unsigned)vm->vars[11], (unsigned)vm->vars[12]); - jlLogFlush(); - break; - - case OP_LOAD_LOGIC: - case OP_LOAD_LOGIC_V: - // load.logic just primes the host's cache; the actual - // bytecode swap happens at OP_CALL time. We touch the - // callback so callers that prefetch can react. - if (!readByte(vm, &a)) { - return VM_DISPATCH_RETURN; - } - if (vm->callbacks.fetchLogic != NULL) { - uint8_t logicId; - uint16_t ignore; - - logicId = (op == OP_LOAD_LOGIC) ? a : vm->vars[a]; - ignore = 0u; - (void)vm->callbacks.fetchLogic(vm->callbacks.ctx, logicId, &ignore); - } - break; - - case OP_CALL: - case OP_CALL_V: - if (!readByte(vm, &a)) { - return VM_DISPATCH_RETURN; - } - { - uint8_t logicId; - - logicId = (op == OP_CALL) ? a : vm->vars[a]; - if (!pushFrame(vm)) { - return VM_DISPATCH_RETURN; - } - if (!enterLogic(vm, logicId)) { - // Roll back the pushed frame so RETURN doesn't - // pop garbage state. - vm->callDepth--; - return VM_DISPATCH_RETURN; - } - } - break; - - case OP_LOAD_PIC: - if (!readByte(vm, &a)) { - return VM_DISPATCH_RETURN; - } - if (vm->callbacks.loadPic != NULL) { - vm->callbacks.loadPic(vm->callbacks.ctx, vm->vars[a]); - } - break; - - case OP_DRAW_PIC: - if (!readByte(vm, &a)) { - return VM_DISPATCH_RETURN; - } - if (vm->callbacks.drawPic != NULL) { - vm->callbacks.drawPic(vm->callbacks.ctx, vm->vars[a]); - } - break; - - case OP_SHOW_PIC: - if (vm->callbacks.showPic != NULL) { - vm->callbacks.showPic(vm->callbacks.ctx); - } - break; - - case OP_DISCARD_PIC: - if (!readByte(vm, &a)) { - return VM_DISPATCH_RETURN; - } - if (vm->callbacks.discardPic != NULL) { - vm->callbacks.discardPic(vm->callbacks.ctx, vm->vars[a]); - } - break; - - case OP_OVERLAY_PIC: - if (!readByte(vm, &a)) { - return VM_DISPATCH_RETURN; - } - if (vm->callbacks.overlayPic != NULL) { - vm->callbacks.overlayPic(vm->callbacks.ctx, vm->vars[a]); - } - break; - - default: - return VM_DISPATCH_UNHANDLED; - } - return VM_DISPATCH_CONTINUE; -} - - -// Group 2: object table (set.view/loop/cel/priority, draw/erase, get.posn, -// etc.), the lumped view-cache and reposition aliases (0x94/0x99), -// object-collision/block opcodes, animation cycling, and motion (0x1E-0x5B). -static VmDispatchE __attribute__((noinline)) vmStepObject(AgiVmT *vm, uint8_t op) { - uint8_t a; - uint8_t b; - uint8_t c; - uint8_t d; - uint8_t e; - - switch (op) { - // ----- view cache hints (no-op; host loads on demand) ----- - case OP_LOAD_VIEW: - case OP_LOAD_VIEW_V: - case OP_DISCARD_VIEW: - case OP_DISCARD_VIEW_V: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - break; - - // ----- object table ----- - case OP_ANIMATE_OBJ: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - jlLogF("OP_ANIMATE_OBJ %u", (unsigned)a); - if (a < AGI_MAX_OBJECTS) { - agiObjReset(&vm->objects[a]); - vm->objects[a].flags |= AGI_OBJ_FLAG_ANIMATED | AGI_OBJ_FLAG_UPDATING | AGI_OBJ_FLAG_CYCLING; - } - break; - - case OP_UNANIMATE_ALL: - jlLog("OP_UNANIMATE_ALL"); - agiObjResetAll(vm); - break; - - case OP_DRAW: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - jlLogF("OP_DRAW %u", (unsigned)a); - if (a < AGI_MAX_OBJECTS) { - vm->objects[a].flags |= AGI_OBJ_FLAG_DRAWN; - } - break; - - case OP_ERASE: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - jlLogF("OP_ERASE %u", (unsigned)a); - if (a < AGI_MAX_OBJECTS) { - vm->objects[a].flags &= (uint8_t)~AGI_OBJ_FLAG_DRAWN; - } - break; - - case OP_POSITION: - if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return VM_DISPATCH_RETURN; - } - jlLogF("OP_POSITION %u (%u,%u)", (unsigned)a, (unsigned)b, (unsigned)c); - if (a < AGI_MAX_OBJECTS) { - vm->objects[a].x = (int16_t)b; - vm->objects[a].y = (int16_t)c; - } - break; - - case OP_POSITION_V: - case OP_REPOSITION_TO_V: - if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return VM_DISPATCH_RETURN; - } - if (a < AGI_MAX_OBJECTS) { - vm->objects[a].x = (int16_t)vm->vars[b]; - vm->objects[a].y = (int16_t)vm->vars[c]; - jlLogF("OP_POSITION_V/RTO_V %u <- v%u/v%u = (%d,%d)", - (unsigned)a, (unsigned)b, (unsigned)c, - (int)vm->objects[a].x, (int)vm->objects[a].y); - } - break; - - case OP_GET_POSN: - if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return VM_DISPATCH_RETURN; - } - if (a < AGI_MAX_OBJECTS) { - vm->vars[b] = (uint8_t)vm->objects[a].x; - vm->vars[c] = (uint8_t)vm->objects[a].y; - } - break; - - case OP_REPOSITION: - // reposition(N, vdx, vdy) — apply signed deltas (vars hold int8_t-style) - if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return VM_DISPATCH_RETURN; - } - if (a < AGI_MAX_OBJECTS) { - int8_t dx = (int8_t)vm->vars[b]; - int8_t dy = (int8_t)vm->vars[c]; - vm->objects[a].x = (int16_t)(vm->objects[a].x + dx); - vm->objects[a].y = (int16_t)(vm->objects[a].y + dy); - } - break; - - case OP_REPOSITION_TO: - if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return VM_DISPATCH_RETURN; - } - jlLogF("OP_REPOSITION_TO %u (%u,%u)", (unsigned)a, (unsigned)b, (unsigned)c); - if (a < AGI_MAX_OBJECTS) { - vm->objects[a].x = (int16_t)b; - vm->objects[a].y = (int16_t)c; - } - break; - - case OP_SET_VIEW: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - jlLogF("OP_SET_VIEW obj=%u view=%u", (unsigned)a, (unsigned)b); - if (a < AGI_MAX_OBJECTS) { - vm->objects[a].viewId = b; - vm->objects[a].cel = 0u; - } - break; - - case OP_SET_VIEW_V: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - jlLogF("OP_SET_VIEW_V obj=%u view=v%u (=%u)", - (unsigned)a, (unsigned)b, (unsigned)vm->vars[b]); - if (a < AGI_MAX_OBJECTS) { - vm->objects[a].viewId = vm->vars[b]; - vm->objects[a].cel = 0u; - } - break; - - case OP_SET_LOOP: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - jlLogF("OP_SET_LOOP obj=%u loop=%u", (unsigned)a, (unsigned)b); - if (a < AGI_MAX_OBJECTS) { - vm->objects[a].loop = b; - vm->objects[a].cel = 0u; - } - break; - - case OP_SET_LOOP_V: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - jlLogF("OP_SET_LOOP_V obj=%u loop=v%u (=%u)", - (unsigned)a, (unsigned)b, (unsigned)vm->vars[b]); - if (a < AGI_MAX_OBJECTS) { - vm->objects[a].loop = vm->vars[b]; - vm->objects[a].cel = 0u; - } - break; - - case OP_FIX_LOOP: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags |= AGI_OBJ_FLAG_LOOP_FIXED; } - break; - - case OP_RELEASE_LOOP: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags &= (uint8_t)~AGI_OBJ_FLAG_LOOP_FIXED; } - break; - - case OP_SET_CEL: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - if (a < AGI_MAX_OBJECTS) { vm->objects[a].cel = b; vm->objects[a].cycleTick = 0u; } - break; - - case OP_SET_CEL_V: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - if (a < AGI_MAX_OBJECTS) { vm->objects[a].cel = vm->vars[b]; vm->objects[a].cycleTick = 0u; } - break; - - case OP_LAST_CEL: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - if (a < AGI_MAX_OBJECTS && vm->callbacks.viewCelCount != NULL) { - uint8_t c = vm->callbacks.viewCelCount(vm->callbacks.ctx, - vm->objects[a].viewId, - vm->objects[a].loop); - vm->vars[b] = (c > 0u) ? (uint8_t)(c - 1u) : 0u; - } else { - vm->vars[b] = 0u; - } - break; - - case OP_CURRENT_CEL: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - vm->vars[b] = (a < AGI_MAX_OBJECTS) ? vm->objects[a].cel : 0u; - break; - - case OP_CURRENT_LOOP: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - vm->vars[b] = (a < AGI_MAX_OBJECTS) ? vm->objects[a].loop : 0u; - break; - - case OP_CURRENT_VIEW: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - vm->vars[b] = (a < AGI_MAX_OBJECTS) ? vm->objects[a].viewId : 0u; - break; - - case OP_NUMBER_OF_LOOPS: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - if (a < AGI_MAX_OBJECTS && vm->callbacks.viewLoopCount != NULL) { - uint8_t c = vm->callbacks.viewLoopCount(vm->callbacks.ctx, - vm->objects[a].viewId); - vm->vars[b] = (c > 0u) ? c : 1u; - } else { - vm->vars[b] = 1u; - } - break; - - case OP_SET_PRIORITY: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - if (a < AGI_MAX_OBJECTS) { - vm->objects[a].priority = b; - vm->objects[a].flags |= AGI_OBJ_FLAG_PRI_FIXED; - } - break; - - case OP_SET_PRIORITY_V: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - if (a < AGI_MAX_OBJECTS) { - vm->objects[a].priority = vm->vars[b]; - vm->objects[a].flags |= AGI_OBJ_FLAG_PRI_FIXED; - } - break; - - case OP_RELEASE_PRIORITY: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags &= (uint8_t)~AGI_OBJ_FLAG_PRI_FIXED; } - break; - - case OP_GET_PRIORITY: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - vm->vars[b] = (a < AGI_MAX_OBJECTS) ? vm->objects[a].priority : 0u; - break; - - case OP_STOP_UPDATE: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags &= (uint8_t)~AGI_OBJ_FLAG_UPDATING; } - break; - - case OP_START_UPDATE: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags |= AGI_OBJ_FLAG_UPDATING; } - break; - - case OP_FORCE_UPDATE: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - // Force-redraw hint; we always redraw, so no-op apart from arg consume. - break; - - case OP_IGNORE_HORIZON: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags |= AGI_OBJ_FLAG_HORIZON_IGN; } - break; - - case OP_OBSERVE_HORIZON: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags &= (uint8_t)~AGI_OBJ_FLAG_HORIZON_IGN; } - break; - - case OP_SET_HORIZON: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - vm->horizon = a; - break; - - case OP_OBJECT_ON_WATER: - case OP_OBJECT_ON_LAND: - case OP_OBJECT_ON_ANYTHING: - case OP_IGNORE_BLOCKS: - case OP_OBSERVE_BLOCKS: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - break; - - case OP_IGNORE_OBJS: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags |= AGI_OBJ_FLAG_OBJS_IGN; } - break; - - case OP_OBSERVE_OBJS: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags &= (uint8_t)~AGI_OBJ_FLAG_OBJS_IGN; } - break; - - case OP_DISTANCE: { - int16_t dx; - int16_t dy; - - if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return VM_DISPATCH_RETURN; - } - if (a < AGI_MAX_OBJECTS && b < AGI_MAX_OBJECTS) { - dx = (int16_t)(vm->objects[a].x - vm->objects[b].x); - dy = (int16_t)(vm->objects[a].y - vm->objects[b].y); - if (dx < 0) { dx = (int16_t)(-dx); } - if (dy < 0) { dy = (int16_t)(-dy); } - vm->vars[c] = (uint8_t)((dx + dy > 255) ? 255 : (dx + dy)); - } else { - vm->vars[c] = 255u; - } - break; - } - - case OP_BLOCK: - // 4 args: (x1, y1, x2, y2) - if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d)) { - return VM_DISPATCH_RETURN; - } - break; - - case OP_UNBLOCK: - break; - - // ----- animation cycling ----- - case OP_STOP_CYCLING: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags &= (uint8_t)~AGI_OBJ_FLAG_CYCLING; } - break; - - case OP_START_CYCLING: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags |= AGI_OBJ_FLAG_CYCLING; } - break; - - case OP_NORMAL_CYCLE: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - if (a < AGI_MAX_OBJECTS) { - vm->objects[a].cycleMode = AGI_CYCLE_NORMAL; - vm->objects[a].endOfLoopFlag = 0u; - vm->objects[a].flags |= AGI_OBJ_FLAG_CYCLING; - } - break; - - case OP_END_OF_LOOP: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - if (a < AGI_MAX_OBJECTS) { - vm->objects[a].cycleMode = AGI_CYCLE_END_LOOP; - vm->objects[a].endOfLoopFlag = b; - vm->objects[a].flags |= AGI_OBJ_FLAG_CYCLING; - vm->flags[b] = 0u; - } - break; - - case OP_REVERSE_CYCLE: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - if (a < AGI_MAX_OBJECTS) { - vm->objects[a].cycleMode = AGI_CYCLE_REVERSE; - vm->objects[a].endOfLoopFlag = 0u; - vm->objects[a].flags |= AGI_OBJ_FLAG_CYCLING; - } - break; - - case OP_REVERSE_LOOP: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - if (a < AGI_MAX_OBJECTS) { - vm->objects[a].cycleMode = AGI_CYCLE_REV_LOOP; - vm->objects[a].endOfLoopFlag = b; - vm->objects[a].flags |= AGI_OBJ_FLAG_CYCLING; - vm->flags[b] = 0u; - } - break; - - case OP_CYCLE_TIME: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - if (a < AGI_MAX_OBJECTS) { - vm->objects[a].cycleTime = vm->vars[b]; - if (vm->objects[a].cycleTime == 0u) { vm->objects[a].cycleTime = 1u; } - vm->objects[a].cycleTick = 0u; - } - break; - - // ----- motion ----- - case OP_STOP_MOTION: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - if (a < AGI_MAX_OBJECTS) { vm->objects[a].direction = 0u; } - break; - - case OP_START_MOTION: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - if (a < AGI_MAX_OBJECTS) { - if (a == 0u) { - vm->objects[0].direction = vm->vars[ENG_VAR_EGO_DIR]; - } else { - // Re-allow motion; direction stays whatever it was. - } - } - break; - - case OP_STEP_SIZE: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - if (a < AGI_MAX_OBJECTS) { - vm->objects[a].stepSize = vm->vars[b]; - if (vm->objects[a].stepSize == 0u) { vm->objects[a].stepSize = 1u; } - } - break; - - case OP_STEP_TIME: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - if (a < AGI_MAX_OBJECTS) { - vm->objects[a].stepTime = vm->vars[b]; - if (vm->objects[a].stepTime == 0u) { vm->objects[a].stepTime = 1u; } - vm->objects[a].stepTick = 0u; - } - break; - - case OP_MOVE_OBJ: - // n, x, y, vstep, flag - if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d) || !readByte(vm, &e)) { - return VM_DISPATCH_RETURN; - } - jlLogF("OP_MOVE_OBJ %u -> (%u,%u) step=%u doneFlag=%u", - (unsigned)a, (unsigned)b, (unsigned)c, (unsigned)d, (unsigned)e); - if (a < AGI_MAX_OBJECTS) { - vm->objects[a].targetX = (int16_t)b; - vm->objects[a].targetY = (int16_t)c; - vm->objects[a].moveStepBackup = vm->objects[a].stepSize; - vm->objects[a].stepSize = (d == 0u) ? vm->objects[a].stepSize : d; - vm->objects[a].moveDoneFlag = e; - vm->objects[a].motionType = AGI_MOTION_MOVE_OBJ; - vm->flags[e] = 0u; - } - break; - - case OP_MOVE_OBJ_V: - if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d) || !readByte(vm, &e)) { - return VM_DISPATCH_RETURN; - } - jlLogF("OP_MOVE_OBJ_V %u -> v%u/v%u=(%u,%u) vstep=v%u=%u doneFlag=%u", - (unsigned)a, (unsigned)b, (unsigned)c, - (unsigned)vm->vars[b], (unsigned)vm->vars[c], - (unsigned)d, (unsigned)vm->vars[d], (unsigned)e); - if (a < AGI_MAX_OBJECTS) { - vm->objects[a].targetX = (int16_t)vm->vars[b]; - vm->objects[a].targetY = (int16_t)vm->vars[c]; - vm->objects[a].moveStepBackup = vm->objects[a].stepSize; - vm->objects[a].stepSize = (vm->vars[d] == 0u) ? vm->objects[a].stepSize : vm->vars[d]; - vm->objects[a].moveDoneFlag = e; - vm->objects[a].motionType = AGI_MOTION_MOVE_OBJ; - vm->flags[e] = 0u; - } - break; - - case OP_FOLLOW_EGO: - if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return VM_DISPATCH_RETURN; - } - if (a < AGI_MAX_OBJECTS) { - vm->objects[a].followStep = b; - vm->objects[a].followDoneFlag = c; - vm->objects[a].motionType = AGI_MOTION_FOLLOW_EGO; - } - break; - - case OP_WANDER: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - if (a < AGI_MAX_OBJECTS) { - vm->objects[a].motionType = AGI_MOTION_WANDER; - vm->objects[a].wanderTick = 0u; - } - break; - - case OP_NORMAL_MOTION: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - if (a < AGI_MAX_OBJECTS) { vm->objects[a].motionType = AGI_MOTION_NORMAL; } - break; - - case OP_SET_DIR: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - jlLogF("OP_SET_DIR obj=%u dir=v%u (=%u)", - (unsigned)a, (unsigned)b, (unsigned)vm->vars[b]); - if (a < AGI_MAX_OBJECTS) { - vm->objects[a].direction = vm->vars[b]; - if (a == 0u) { vm->vars[ENG_VAR_EGO_DIR] = vm->objects[0].direction; } - } - break; - - case OP_GET_DIR: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - vm->vars[b] = (a < AGI_MAX_OBJECTS) ? vm->objects[a].direction : 0u; - break; - - default: - return VM_DISPATCH_UNHANDLED; - } - return VM_DISPATCH_CONTINUE; -} - - -// Group 3: inventory state (0x5C-0x61), sound (0x62-0x64), and the text -// opcodes (0x65-0x76, plus the 0x9A clear.text.rect alias lumped here). -static VmDispatchE __attribute__((noinline)) vmStepText(AgiVmT *vm, uint8_t op) { - uint8_t a; - uint8_t b; - uint8_t c; - uint8_t d; - uint8_t e; - - switch (op) { - // ----- inventory (state only; reads OBJECT data via host later) ----- - case OP_GET: - case OP_DROP: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - break; - - case OP_GET_V: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - break; - - case OP_PUT: - case OP_PUT_V: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - break; - - case OP_GET_ROOM_V: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - vm->vars[b] = 0u; - break; - - // ----- sound (no-op; complete callback fires immediately) ----- - case OP_LOAD_SOUND: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - break; - - case OP_SOUND: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - jlLogF("OP_SOUND id=%u flag=%u (prev playing=%u prevFlag=%u)", - (unsigned)a, (unsigned)b, - (unsigned)vm->soundPlaying, (unsigned)vm->soundDoneFlag); - jlLogFlush(); - // AGI v2 sound(N, F): both args are LITERAL. - // (No _V variant exists for sound; first arg is - // always the sound resource id, second is the - // completion flag id.) Don't index through vars. - // - // If a previous sound is still armed, fire its - // done flag now (the script's about to wait on the - // new sound; the old one's completion is the new - // sound's start, not whenever playback ended). - if (vm->soundPlaying) { - if (vm->soundDoneFlag != 0u) { - vm->flags[vm->soundDoneFlag] = 1u; - } - vm->flags[9] = 1u; - vm->soundPlaying = false; - } - if (vm->callbacks.playSound != NULL && vm->callbacks.isPlayingSound != NULL) { - vm->soundDoneFlag = b; - vm->flags[9] = 0u; - // ScummVM startSound does setFlagOrVar(_endflag, - // false) here -- without it the script's - // `while (!flag[b]) wait` loop sees a stale 1 - // from a previous sound() and skips waiting. - vm->flags[b] = 0u; - vm->callbacks.playSound(vm->callbacks.ctx, a); - vm->soundPlaying = true; - } else { - // Legacy fire-immediately when the host can't - // report playback state. Keeps non-AGI uses of - // this VM (and any test harness without audio) - // from blocking forever on a wait-for-sound flag. - vm->flags[b] = 1u; - vm->flags[9] = 1u; - } - break; - - case OP_STOP_SOUND: - jlLogF("OP_STOP_SOUND (playing=%u flag=%u)", - (unsigned)vm->soundPlaying, - (unsigned)vm->soundDoneFlag); - jlLogFlush(); - if (vm->callbacks.stopSound != NULL) { - vm->callbacks.stopSound(vm->callbacks.ctx); - } - if (vm->soundPlaying) { - if (vm->soundDoneFlag != 0u) { - vm->flags[vm->soundDoneFlag] = 1u; - } - vm->flags[9] = 1u; - vm->soundPlaying = false; - } - break; - - // ----- text ----- - case OP_PRINT: { - char expanded[AGI_PRINT_MAX_LEN + 1u]; - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - expandMessage(vm, fetchMessageOrEmpty(vm, vm->currentLogicId, a), expanded, sizeof(expanded)); - openPrintModal(vm, expanded); - return VM_DISPATCH_RETURN; - } - - case OP_PRINT_V: { - char expanded[AGI_PRINT_MAX_LEN + 1u]; - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - expandMessage(vm, fetchMessageOrEmpty(vm, vm->currentLogicId, vm->vars[a]), expanded, sizeof(expanded)); - openPrintModal(vm, expanded); - return VM_DISPATCH_RETURN; - } - - case OP_DISPLAY: { - char expanded[AGI_PRINT_MAX_LEN + 1u]; - if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return VM_DISPATCH_RETURN; - } - expandMessage(vm, fetchMessageOrEmpty(vm, vm->currentLogicId, c), expanded, sizeof(expanded)); - jlLogF("OP_DISPLAY row=%u col=%u msg=%u text=\"%s\"", - (unsigned)a, (unsigned)b, (unsigned)c, expanded); - writeTextRow(vm, a, b, expanded); - break; - } - - case OP_DISPLAY_V: { - char expanded[AGI_PRINT_MAX_LEN + 1u]; - if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return VM_DISPATCH_RETURN; - } - expandMessage(vm, fetchMessageOrEmpty(vm, vm->currentLogicId, vm->vars[c]), expanded, sizeof(expanded)); - writeTextRow(vm, vm->vars[a], vm->vars[b], expanded); - break; - } - - case OP_CLEAR_LINES: { - uint8_t r; - if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return VM_DISPATCH_RETURN; - } - jlLogF("OP_CLEAR_LINES rows=%u..%u bg=%u", - (unsigned)a, (unsigned)b, (unsigned)c); - if (a >= AGI_TEXT_ROWS) a = AGI_TEXT_ROWS - 1u; - if (b >= AGI_TEXT_ROWS) b = AGI_TEXT_ROWS - 1u; - for (r = a; r <= b; r++) { - vm->textRows[r].text[0] = '\0'; - vm->textRows[r].bg = c; - } - break; - } - - case OP_CLEAR_TEXT_RECT: { - uint8_t r; - if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d) || !readByte(vm, &e)) { - return VM_DISPATCH_RETURN; - } - if (c >= AGI_TEXT_ROWS) c = AGI_TEXT_ROWS - 1u; - for (r = a; r <= c; r++) { - vm->textRows[r].text[0] = '\0'; - vm->textRows[r].bg = e; - } - break; - } - - case OP_TEXT_SCREEN: - case OP_GRAPHICS: - break; - - case OP_SET_CURSOR_CHAR: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - break; - - case OP_SET_TEXT_ATTRIBUTE: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - vm->textFg = a; - vm->textBg = b; - break; - - case OP_SHAKE_SCREEN: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - break; - - case OP_CONFIGURE_SCREEN: - if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return VM_DISPATCH_RETURN; - } - break; - - case OP_STATUS_LINE_ON: - vm->statusLineOn = true; - break; - - case OP_STATUS_LINE_OFF: - vm->statusLineOn = false; - break; - - case OP_SET_STRING: { - char expanded[AGI_STRING_LEN + 1u]; - uint8_t i; - - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - if (a < AGI_MAX_STRINGS) { - expandMessage(vm, fetchMessageOrEmpty(vm, vm->currentLogicId, b), expanded, sizeof(expanded)); - for (i = 0u; i < (uint8_t)AGI_STRING_LEN; i++) { - vm->strings[a][i] = expanded[i]; - if (expanded[i] == '\0') break; - } - vm->strings[a][AGI_STRING_LEN] = '\0'; - } - break; - } - - case OP_GET_STRING: - // get.string(s, msgId, row, col, max) — parser input; - // no parser yet, just consume args. - if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d) || !readByte(vm, &e)) { - return VM_DISPATCH_RETURN; - } - break; - - case OP_WORD_TO_STRING: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - break; - - case OP_PARSE: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - break; - - case OP_GET_NUM: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - vm->vars[b] = 0u; - break; - - default: - return VM_DISPATCH_UNHANDLED; - } - return VM_DISPATCH_CONTINUE; -} - - -// Group 4: input (0x77-0x79), add.to.pic (0x7A-0x7B), the misc/system -// opcodes, print.at aliases, menus, and mul/div (0x7C-0xAA). Owns the -// default fallback (kActionArgBytes stub-consume / unknown-op halt), so it -// never returns VM_DISPATCH_UNHANDLED. -static VmDispatchE __attribute__((noinline)) vmStepMisc(AgiVmT *vm, uint8_t op) { - uint8_t a; - uint8_t b; - uint8_t c; - uint8_t d; - uint8_t e; - uint8_t f; - uint8_t g; - - switch (op) { - // ----- input ----- - case OP_PREVENT_INPUT: - vm->acceptInput = false; - break; - - case OP_ACCEPT_INPUT: - vm->acceptInput = true; - break; - - case OP_SET_KEY: { - uint8_t i; - - if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return VM_DISPATCH_RETURN; - } - for (i = 0u; i < (uint8_t)AGI_MAX_KEY_BINDINGS; i++) { - if (vm->keyBindings[i].active == 0u) { - vm->keyBindings[i].key1 = a; - vm->keyBindings[i].key2 = b; - vm->keyBindings[i].controller = c; - vm->keyBindings[i].active = 1u; - break; - } - } - break; - } - - // ----- picture composition ----- - case OP_ADD_TO_PIC: - if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d) || !readByte(vm, &e) || !readByte(vm, &f) || !readByte(vm, &g)) { - return VM_DISPATCH_RETURN; - } - if (vm->callbacks.addToPic != NULL) { - vm->callbacks.addToPic(vm->callbacks.ctx, a, b, c, d, e, f, g); - } - break; - - case OP_ADD_TO_PIC_V: - if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d) || !readByte(vm, &e) || !readByte(vm, &f) || !readByte(vm, &g)) { - return VM_DISPATCH_RETURN; - } - if (vm->callbacks.addToPic != NULL) { - vm->callbacks.addToPic(vm->callbacks.ctx, - vm->vars[a], vm->vars[b], vm->vars[c], - vm->vars[d], vm->vars[e], vm->vars[f], vm->vars[g]); - } - break; - - // ----- misc state / no-ops ----- - case OP_STATUS: - break; - - case OP_SAVE_GAME: - case OP_RESTORE_GAME: - break; - - case OP_INIT_DISK: - break; - - case OP_RESTART_GAME: - vm->haltReason = AGI_VM_HALT_QUIT; - return VM_DISPATCH_RETURN; - - case OP_SHOW_OBJ: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - break; - - case OP_RANDOM: - if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return VM_DISPATCH_RETURN; - } - { - uint16_t span; - span = (uint16_t)((b > a) ? (b - a + 1u) : 1u); - vm->vars[c] = (uint8_t)(a + (uint8_t)(jlRandomRange(span))); - } - break; - - case OP_PROGRAM_CONTROL: - vm->programControl = true; - break; - - case OP_PLAYER_CONTROL: - vm->programControl = false; - break; - - case OP_OBJ_STATUS_V: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - break; - - case OP_QUIT: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - vm->haltReason = AGI_VM_HALT_QUIT; - return VM_DISPATCH_RETURN; - - case OP_SHOW_MEM: - break; - - case OP_PAUSE: - openPrintModal(vm, "Game Paused.\n\nPress ENTER to continue."); - return VM_DISPATCH_RETURN; - - case OP_ECHO_LINE: - case OP_CANCEL_LINE: - break; - - case OP_INIT_JOY: - break; - - case OP_TOGGLE_MONITOR: - break; - - case OP_VERSION: - break; - - case OP_SCRIPT_SIZE: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - break; - - case OP_SET_GAME_ID: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - break; - - case OP_LOG: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - break; - - case OP_SET_SCAN_START: - case OP_RESET_SCAN_START: - break; - - case OP_TRACE_ON: - break; - - case OP_TRACE_INFO: - if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return VM_DISPATCH_RETURN; - } - break; - - case OP_PRINT_AT: { - char expanded[AGI_PRINT_MAX_LEN + 1u]; - if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d)) { - return VM_DISPATCH_RETURN; - } - expandMessage(vm, fetchMessageOrEmpty(vm, vm->currentLogicId, a), expanded, sizeof(expanded)); - openPrintModal(vm, expanded); - return VM_DISPATCH_RETURN; - } - - case OP_PRINT_AT_V: { - char expanded[AGI_PRINT_MAX_LEN + 1u]; - if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d)) { - return VM_DISPATCH_RETURN; - } - expandMessage(vm, fetchMessageOrEmpty(vm, vm->currentLogicId, vm->vars[a]), expanded, sizeof(expanded)); - openPrintModal(vm, expanded); - return VM_DISPATCH_RETURN; - } - - case OP_SET_UPPER_LEFT: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - break; - - // ----- menus ----- - case OP_SET_MENU: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - if (vm->menuCount < AGI_MAX_MENUS) { - vm->menus[vm->menuCount].nameMsgId = a; - vm->menus[vm->menuCount].itemCount = 0u; - vm->menuCount++; - } - break; - - case OP_SET_MENU_ITEM: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - if (vm->menuCount > 0u) { - AgiMenuT *m = &vm->menus[vm->menuCount - 1u]; - if (m->itemCount < AGI_MAX_MENU_ITEMS) { - m->items[m->itemCount].messageId = a; - m->items[m->itemCount].controller = b; - m->items[m->itemCount].enabled = 1u; - m->itemCount++; - } - } - break; - - case OP_SUBMIT_MENU: - break; - - case OP_ENABLE_ITEM: - case OP_DISABLE_ITEM: { - uint8_t m; - uint8_t i; - uint8_t enable; - - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - enable = (op == OP_ENABLE_ITEM) ? 1u : 0u; - for (m = 0u; m < vm->menuCount; m++) { - for (i = 0u; i < vm->menus[m].itemCount; i++) { - if (vm->menus[m].items[i].controller == a) { - vm->menus[m].items[i].enabled = enable; - } - } - } - break; - } - - case OP_MENU_INPUT: - // Real menu UI is not wired; just no-op so scripts don't stall. - break; - - case OP_SHOW_OBJ_V: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - break; - - case OP_OPEN_DIALOGUE: - case OP_CLOSE_DIALOGUE: - break; - - case OP_MUL_N: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - vm->vars[a] = (uint8_t)(vm->vars[a] * b); - break; - - case OP_MUL_V: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - vm->vars[a] = (uint8_t)(vm->vars[a] * vm->vars[b]); - break; - - case OP_DIV_N: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - if (b != 0u) { - vm->vars[a] = (uint8_t)(vm->vars[a] / b); - } - break; - - case OP_DIV_V: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } - if (vm->vars[b] != 0u) { - vm->vars[a] = (uint8_t)(vm->vars[a] / vm->vars[b]); - } - break; - - case OP_CLOSE_WINDOW: - vm->printModal.active = false; - break; - - case OP_SET_SIMPLE: - if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } - break; - - default: { - uint8_t argBytes; - uint8_t i; - uint8_t scratch; - - argBytes = kActionArgBytes[op]; - if (argBytes == ACTION_UNKNOWN) { - vm->lastUnknownOp = op; - vm->haltReason = AGI_VM_HALT_UNKNOWN_OP; - vm->pc = (uint16_t)(vm->pc - 1u); - break; - } - for (i = 0u; i < argBytes; i++) { - if (!readByte(vm, &scratch)) { - return VM_DISPATCH_RETURN; - } - } - break; - } - } - return VM_DISPATCH_CONTINUE; -} - - -void agiVmSetCallbacks(AgiVmT *vm, const AgiVmCallbacksT *callbacks) { - if (callbacks == NULL) { - memset(&vm->callbacks, 0, sizeof(vm->callbacks)); - } else { - vm->callbacks = *callbacks; - } -} - - -void agiVmTickSeconds(AgiVmT *vm, uint8_t seconds) { - uint16_t s; - uint16_t m; - uint16_t h; - uint16_t carry; - - if (seconds == 0u) { - return; - } - // Sound completion is no longer driven from here -- the per-cycle - // poll in agiVmTickAnimation reads callbacks.isPlayingSound and - // fires the done flags on the live host->silent edge. Keeps the - // VM in lock-step with what the user actually hears. - - s = (uint16_t)((uint16_t)vm->vars[11] + (uint16_t)seconds); - carry = (uint16_t)(s / 60u); - vm->vars[11] = (uint8_t)(s % 60u); - if (carry == 0u) { - return; - } - m = (uint16_t)((uint16_t)vm->vars[12] + carry); - carry = (uint16_t)(m / 60u); - vm->vars[12] = (uint8_t)(m % 60u); - if (carry == 0u) { - return; - } - h = (uint16_t)((uint16_t)vm->vars[13] + carry); - vm->vars[13] = (uint8_t)(h % 24u); -} diff --git a/examples/audio/audio.c b/examples/audio/audio.c index 3398933..8817dea 100644 --- a/examples/audio/audio.c +++ b/examples/audio/audio.c @@ -1,11 +1,22 @@ // Audio demo: starts the baked chip-tracker song (demoSong.song -> -// demoSong.h via tools/songbake) on entry, triggers a short digital -// SFX every time SPACE is tapped, and exits on ESC. M switches to the -// .MOD loaded from the DATA folder on the disk image (see -// make/.mk for each platform's packaging step); T switches back -// to the tracker song; P pauses/resumes tracker playback in place. -// The two layers share output hardware on the ST/Amiga, so the demo -// always stops one before starting the other. +// demoSong.h via tools/songbake) on entry and exits on ESC. The keys: +// SPACE a short digital SFX, test.sfx from the DATA folder +// S a falling tone, streamed: a callback writes each chunk as the +// engine asks for it, already in the port's own sample format +// (jlAudioPlaySfxStreamRaw), so it needs no sample data and could +// run as long as it liked +// N the noise generator swept through its 32 pitches, bright to +// dark (jlAudioNoise); the song pauses for it +// M the .MOD loaded from the DATA folder (make/examples.mk says what +// each platform stages) +// T back to the tracker song +// P pause / resume the tracker song in place +// The tracker and the MOD share output hardware on the ST/Amiga, so the +// demo always stops one before starting the other. +// +// Built with AUDIO_PROBE (the AUDIOPRB test program) it presses its own +// keys on a timetable -- N once, S every 1.5 seconds -- for the IIgs and +// X68000 audio gates, which read the sound chips but cannot type. // // 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 @@ -33,11 +44,52 @@ # define TEST_MOD_NAME "test.mod" #endif #define TEST_SFX_NAME "test.sfx" +#define DATA_FILE_MAX (64UL * 1024UL) // Slot 3: the tracker's three voices ride SFX slots 0-2 on the IIgs, -// so the demo's one-shot SFX stays out of their way. +// so the demo's one-shot SFX stays out of their way. The stream takes +// slot 4, so it never cuts the one-shot off. #define SFX_SLOT 3 #define SFX_RATE_HZ 8000 +#define STREAM_SLOT 4 + +// The streamed tone: STREAM_BLOCKS blocks of STREAM_BLOCK samples, each +// at one pitch and one level, falling STREAM_DROP_HZ from its first pitch +// while it fades from STREAM_PEAK to STREAM_FLOOR. Each press starts +// STREAM_TAKE_HZ higher than the last, over STREAM_TAKES takes, so two +// takes in a row never sound alike. The square wave's phase is a 16-bit +// accumulator: one cycle per 2^STREAM_PHASE_BITS. +#define STREAM_RATE_HZ 8000u +#define STREAM_BLOCK 256u +#define STREAM_BLOCKS 48u +#define STREAM_SAMPLES (STREAM_BLOCK * STREAM_BLOCKS) +#define STREAM_START_HZ 330u +#define STREAM_DROP_HZ 220u +#define STREAM_TAKE_HZ 55u +#define STREAM_TAKES 4u +#define STREAM_PEAK 96 +#define STREAM_FLOOR 16 +#define STREAM_PHASE_BITS 16u +#define STREAM_PHASE_HALF (1u << (STREAM_PHASE_BITS - 1u)) +// Signed sample -> the unsigned form a port that streams unsigned bytes +// wants (JL_SFX_RAW_UNSIGNED8_NOZERO). |level| <= STREAM_PEAK keeps every +// byte off 0x00, the IIgs DOC's stop byte. +#define RAW_BIAS 0x80 + +// The noise sweep: every pitch in turn for NOISE_STEP_FRAMES frames. +#define NOISE_PITCHES 32u +#define NOISE_STEP_FRAMES 6u +#define NOISE_ATTEN 4u +#define ATTEN_SILENT 15u + +// AUDIO_PROBE's timetable, in frames of the main loop. A new stream starts +// about when the last one ends (STREAM_SAMPLES at STREAM_RATE_HZ is 1.5 s), +// so whenever a gate looks, one is playing. +#define PROBE_NOISE_FRAME 60u +#define PROBE_STREAM_FIRST 30u +#define PROBE_STREAM_FRAMES 90u + +#define FLASH_FRAMES 8 #define COLOR_BG 0 #define COLOR_HINT 1 @@ -48,6 +100,62 @@ #define BAR_W (SURFACE_WIDTH - 32) #define BAR_H 16 +// The streamed tone's state, which streamFill advances. +typedef struct { + uint32_t pos; // samples delivered so far + uint16_t phase; // square-wave phase accumulator + uint16_t startHz; // this take's first pitch +} StreamT; + + +static void buildPalette(jlSurfaceT *screen); +static void initialPaint(jlSurfaceT *screen, bool audioOk); +static bool keyPressed(jlKeyE key, uint16_t frame); +static bool loadFile(const char *name, uint32_t maxLen, uint8_t **outBytes, uint32_t *outLen); +static bool noiseStep(uint16_t sweepFrame); +static jlKeyE probeKey(uint16_t frame); +static uint8_t rawSample(int16_t level); +static uint32_t streamFill(void *ctx, int8_t *dst, uint32_t count); +static void streamStart(void); + + +static uint8_t gRawFormat = JL_SFX_RAW_SIGNED8; +static StreamT gStream; +static uint8_t gStreamTake = 0; + + +static void buildPalette(jlSurfaceT *screen) { + uint16_t colors[SURFACE_COLORS_PER_PALETTE]; + uint16_t i; + + for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) { + colors[i] = 0x0000; + } + colors[COLOR_BG] = 0x0000; + colors[COLOR_HINT] = 0x0444; + colors[COLOR_BAR] = 0x00F0; + + 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 a sound has fired this frame. +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); + jlStagePresent(); +} + + +// True when the player pressed key this frame -- or, in the probe build, +// when the timetable presses it. +static bool keyPressed(jlKeyE key, uint16_t frame) { + return jlKeyPressed(key) || probeKey(frame) == key; +} + // Read an entire DATA/ file into a freshly allocated buffer. Caller must // free(*outBytes) once the audio engine has consumed the data. @@ -92,42 +200,119 @@ static bool loadFile(const char *name, uint32_t maxLen, uint8_t **outBytes, uint } -static void buildPalette(jlSurfaceT *screen) { - uint16_t colors[SURFACE_COLORS_PER_PALETTE]; - uint16_t i; +// One frame of the noise sweep, sweepFrame frames in: this frame's pitch, +// or silence once every pitch has had its turn. Re-asserted every frame, +// because the IIgs holds a noise burst for only ~80 ms. Returns false when +// the sweep is over. +static bool noiseStep(uint16_t sweepFrame) { + uint16_t pitch; - for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) { - colors[i] = 0x0000; + pitch = (uint16_t)(sweepFrame / NOISE_STEP_FRAMES); + if (pitch >= NOISE_PITCHES) { + jlAudioNoise(0, ATTEN_SILENT); + return false; } - colors[COLOR_BG] = 0x0000; - colors[COLOR_HINT] = 0x0444; - colors[COLOR_BAR] = 0x00F0; - - jlPaletteSet(screen, 0, colors); + jlAudioNoise((uint8_t)pitch, NOISE_ATTEN); + return true; } -// 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(jlSurfaceT *screen, bool audioOk) { - jlSurfaceClear(screen, COLOR_BG); - jlFillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H, - audioOk ? COLOR_HINT : COLOR_BG); - jlStagePresent(); +#if defined(AUDIO_PROBE) +// The key the probe build presses this frame: N once, then S over and over, +// so a gate finds the noise sweep and a stream running. +static jlKeyE probeKey(uint16_t frame) { + if (frame == PROBE_NOISE_FRAME) { + return KEY_N; + } + if (frame % PROBE_STREAM_FRAMES == PROBE_STREAM_FIRST) { + return KEY_S; + } + return KEY_NONE; +} +#else +static jlKeyE probeKey(uint16_t frame) { + (void)frame; + return KEY_NONE; +} +#endif + + +// One sample of the streamed tone in this port's raw stream format. +static uint8_t rawSample(int16_t level) { + if (gRawFormat == JL_SFX_RAW_UNSIGNED8_NOZERO) { + return (uint8_t)(level + RAW_BIAS); + } + return (uint8_t)(int8_t)level; +} + + +// The stream's fill callback: up to count samples of the falling tone, +// written straight in the raw format; 0 once it has all been delivered, +// which ends the stream. A block's pitch and level are worked out once, +// so each sample costs an add and a store -- it may run in an interrupt. +static uint32_t streamFill(void *ctx, int8_t *dst, uint32_t count) { + StreamT *stream; + uint8_t *out; + uint32_t done; + uint16_t run; + uint16_t i; + uint16_t block; + uint16_t hz; + uint16_t step; + int16_t level; + uint8_t hi; + uint8_t lo; + + stream = (StreamT *)ctx; + out = (uint8_t *)dst; + done = 0; + while (done < count && stream->pos < STREAM_SAMPLES) { + block = (uint16_t)(stream->pos / STREAM_BLOCK); + run = (uint16_t)(STREAM_BLOCK - stream->pos % STREAM_BLOCK); + if (run > count - done) { + run = (uint16_t)(count - done); + } + hz = (uint16_t)(stream->startHz - (STREAM_DROP_HZ * block) / STREAM_BLOCKS); + step = (uint16_t)(((uint32_t)hz << STREAM_PHASE_BITS) / STREAM_RATE_HZ); + level = (int16_t)(STREAM_PEAK - ((STREAM_PEAK - STREAM_FLOOR) * (int16_t)block) / (int16_t)STREAM_BLOCKS); + hi = rawSample(level); + lo = rawSample((int16_t)-level); + for (i = 0; i < run; i++) { + stream->phase = (uint16_t)(stream->phase + step); + *out++ = (stream->phase & STREAM_PHASE_HALF) ? hi : lo; + } + done += run; + stream->pos += run; + } + return done; +} + + +// Start the falling tone over, a take higher than the last one. +static void streamStart(void) { + jlAudioStopSfx(STREAM_SLOT); + gStream.pos = 0; + gStream.phase = 0; + gStream.startHz = (uint16_t)(STREAM_START_HZ + STREAM_TAKE_HZ * gStreamTake); + gStreamTake = (uint8_t)((gStreamTake + 1u) % STREAM_TAKES); + jlAudioPlaySfxStreamRaw(STREAM_SLOT, streamFill, &gStream, STREAM_RATE_HZ); } int main(void) { - jlConfigT config; - jlSurfaceT *screen; - bool audioOk; - bool musicPaused; - int16_t flashFrames; - uint8_t *modBytes; - uint32_t modLen; - uint8_t *sfxBytes; - uint32_t sfxLen; + jlConfigT config; + jlSurfaceT *screen; + bool audioOk; + bool musicPaused; + bool noiseOn; + bool noiseResumes; + uint16_t noiseFrame; + uint16_t frame; + int16_t flashFrames; + uint8_t *modBytes; + uint32_t modLen; + uint8_t *sfxBytes; + uint32_t sfxLen; config.codegenBytes = 8 * 1024; @@ -152,8 +337,9 @@ int main(void) { if (audioOk) { // Keep the MOD bytes resident so M can (re)start it later; // jlAudioPlayMod copies them at play time. - (void)loadFile(TEST_MOD_NAME, 64UL * 1024UL, &modBytes, &modLen); - (void)loadFile(TEST_SFX_NAME, 64UL * 1024UL, &sfxBytes, &sfxLen); + (void)loadFile(TEST_MOD_NAME, DATA_FILE_MAX, &modBytes, &modLen); + (void)loadFile(TEST_SFX_NAME, DATA_FILE_MAX, &sfxBytes, &sfxLen); + gRawFormat = jlAudioSfxRawFormat(); } jlMusicPlay(gDemoSong, (uint32_t)sizeof(gDemoSong), true); @@ -161,28 +347,43 @@ int main(void) { jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0); initialPaint(screen, audioOk); - musicPaused = false; - flashFrames = 0; - for (;;) { + musicPaused = false; + noiseOn = false; + noiseResumes = false; + noiseFrame = 0; + flashFrames = 0; + for (frame = 0; ; frame++) { jlWaitVBL(); jlInputPoll(); jlAudioFrameTick(); - if (jlKeyPressed(KEY_ESCAPE)) { + if (keyPressed(KEY_ESCAPE, frame)) { break; } - if (jlKeyPressed(KEY_SPACE) && sfxBytes != NULL) { + if (keyPressed(KEY_SPACE, frame) && sfxBytes != NULL) { jlAudioPlaySfx(SFX_SLOT, sfxBytes, sfxLen, SFX_RATE_HZ); - flashFrames = 8; + flashFrames = FLASH_FRAMES; } - if (jlKeyPressed(KEY_M) && modBytes != NULL) { + if (keyPressed(KEY_S, frame) && audioOk) { + streamStart(); + flashFrames = FLASH_FRAMES; + } + if (keyPressed(KEY_N, frame) && !noiseOn) { + // The song's own drums would fight the sweep for the noise + // generator, so it pauses and picks up again afterwards. + noiseResumes = jlMusicIsPlaying() && !musicPaused; + jlMusicPause(); + noiseOn = true; + noiseFrame = 0; + } + if (keyPressed(KEY_M, frame) && modBytes != NULL) { jlMusicStop(); jlAudioPlayMod(modBytes, modLen, true); } - if (jlKeyPressed(KEY_T)) { + if (keyPressed(KEY_T, frame)) { jlAudioStopMod(); jlMusicPlay(gDemoSong, (uint32_t)sizeof(gDemoSong), true); } - if (jlKeyPressed(KEY_P)) { + if (keyPressed(KEY_P, frame)) { if (musicPaused) { jlMusicResume(); } else { @@ -191,6 +392,14 @@ int main(void) { musicPaused = !musicPaused; } + if (noiseOn) { + noiseOn = noiseStep(noiseFrame); + noiseFrame++; + if (!noiseOn && noiseResumes && !musicPaused) { + jlMusicResume(); + } + } + if (flashFrames > 0) { jlFillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H, COLOR_BAR); jlStagePresent(); diff --git a/examples/spacetaxi/21_!.wav b/examples/spacetaxi/21_!.wav deleted file mode 100644 index 3772b5ef2adbfaee060b9cdb69ff22f936954551..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33460 zcmeI2&8<~O5QMKC0U;m$Y}bIzQ5-@|}f zNbbL{tLN0kJO23n&6`hu-)?{Y`u4ZKK74%n;&!|J^ySYlZ*RBPpKiCGZ!d1|-+g@d z&ll$9zxB`OSN*3QXb0MXcAy<-2ik#lpdDxj+JSbU9cTyIfp(xBXb0MXcAy<-2ik#l zpdDxj+JSbU9cTyIfp(xBXb0MXcAy<-2ik%A9jN~{aaHeLSnX&3Oyy|%k=@XJZ2xTg z^Q+a{@849#V-+QlA8)Lt*p9fvD1Vf%w4y)m9np`>#!Jyr#W~^38L#uqU3HwJqa1o3 zl|X(@e69E6xAYfjg*P+mG;&Tfs}yQwm}APw88Xf^%c*v1j(x^+BG^i0O%=~Zn)7jn z4w==xAeD+7+ZpQ=wNtDx_sDrON~y z?2Q>Is5_(c%zchZIrM6LD#l%!t7Mb3@t8mGh}iG%6Y6fEdg`8?uHBc*)IVQ6RdN!-S=pHlP#0`%#H78JwBqod_6P>fU5@hSIpcCe^ zi<~l>aoHnNFRpj$L^U$6MARb{on3OqWlwe2)X_v0kk5F`YTRRIRQ6PFO~Jtxp(>fr z$dN*4M&l-!hgDvlR1%}@n9cOBV9qngt?0{uVW>u>~&Fc<+ zwaR6G?Cy8+5_w+sj6`NWMO7)S!D!UE=SrRZO1{ZTHQ240Qp|(Os`6fQgRf-fEGq7x z^Jt`^W~r*3njhUIdql;Iq-JMZnClL6Olu@DX2H(8Rb{76C6KC%o<}7#cdAw(wU6Ey zmBP1152Vk}s@OVXR&!kRxT25xu8`H52c5(jz1HDAm>n%#KaNm*?ub{!ot@XO-=o`5 z^}idaIK7^&@B8Gs@@&I3f7ELB<>)=#H;1CDjUAsqMTPJ1I9GNit2x=-cAy>jJ{_3- zUw))Lv)rWSu0N~S@4jSw!TBa$i3qRGH;)KaTYWtkME+jrAl6IlGb4k0oS{SJD8n8i z-=$e9kbxpQV-_9$Mp@y%*nd3YM!pvMHO3t!aaTF?zRjpFWnRDQ+HZf)m#T}aYh>=b pkXuyI%x9R?7{B(_+t)X)UU+^U-vB?>7xzik2;J46p#a4^11*3 diff --git a/examples/spacetaxi/31_1.wav b/examples/spacetaxi/31_1.wav deleted file mode 100644 index d17740d077bd72bea121b05c125a419e64c54aea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12986 zcmeH`&21Gy5QG;Za0nIw_&^3oV2LXSbGC%IadR9*!ih}Cz@gyLs8m(`v%634J;}0G zK1_G@SJks)``g$1`@bKT<@=|HFF$`h-QFzA^6us5!^5(C{bN3CTqmD=%qMe@BbieCF&_8w0sR@&#t?AW6ch==BL`bbkZj%#k%Lmgj`p3 z{!{sx)45ddHdeRo*?s$JzupFQO-2=Te@4gbYrbYxt$kBfj`f;j{qJ*Rth+mZ<>HO{ zs`3r3a@1@^c3PbPwRU0ZXU(eWJUS&3hR=JiPqgy{cFOGew&8S7?Q(|uQ29J};BaQ5Qb6N$2I`(uotf8Z{{FS;oNXm~ zf_Ly@Z#^Y7=IHcX_YKmCn9@yE*L^uJld82+t&ZH8vI6)N&??L%V#`SnJzjx=NKC76?RGp|xb<%_ACu61=sMj2m zabk4v#(n*Sj+_~1NQJ((zi0F=pw`a3h065t%Q>Nj>^VcnlOAfLqV6|Vy{fYjaCn2!&m2su)R+PWUQft$ z&W6&no;@~X7WdE-$*Rp76VIttxSbv9IuBpms5%p32pmx9jO098$FA zlu143^yuiC6};7qY)z?J)%5$fuydF4eLX$Q801&PotnF+p1XVUGIvA% z`DsXs=lRKd65>|x_aq$5@dC#ST=E6{fAE^!C2xIX9WU@UU*MYmf#+nu&DTD@>Ue?U L1&$YZ>o4#hN7}rM diff --git a/examples/spacetaxi/32_2.wav b/examples/spacetaxi/32_2.wav deleted file mode 100644 index b3b74acfbf8b47517d80638655d7630149fd10fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21446 zcmeHH+ig@)47^Z5N{A?c9*{yLenCpUBcc59mujei8t8!zD1}(oX#6_op1au zI)P506X*mwfliI)P506X*mwfm0Ir{a!z!l}`h?ao(SDeeSrwJ#tgrj_~DQ(K;XQsI_vwRgG}?seLMR#oOwR z@~t-a%xdP#?}aWq%@C-nI@2B7Rqlcvb2&E~ukyY?|Ni^%F8iWc55Ck@p4I&O{p?%& z-TA+(`m3+Mm8Z-SWW>vFUHRqM_t3ra5~kwz1sSp1l|J8k3Bp&3%ijISiSu0ll6Q`^ z`KEFehc2_m2sCx|zRavY%q3N@75y64e`a&OMaa#5un+BhYAt zlUBiqfJwx<5v`oK)ec_pkF#~=4xMVGODfC>+A+SyfEWkoTUQ`ew>xZIlf&tbOVz}cDENTFZajXp~oBWR?a{?(@(+% zQokK2S=w2hAiRq`5U*4e7&ua+j+kfr$+PGOmQ+Q@2vwfxC#(0p)$<-u&(%G#>IRfj zXH36WXK0~|b4&yKSiKv!@by*q6&5#f_8OgV4ZC{AwyIDdKhD}&O+?}>T(4I(u;^uH z%OqZ1;FcXOuv))}az_We+#-dLH{W1kss`I&m)MCuN_(YIF>S>Ra&SbbrH!eO*04kqsq zL8oj?QmL;y<@#NvCuX@oEhhb*t9@7I!j+Yt#Hud7V}Gt(ES4SYcGPFp92xl#rydv) zE>geLqPgtgqPd zc*gb{L6J`%@9uto-L^09K7aWB2~+}=!2gxN`d<#upUe8KrhNX3#}^#^7S)fZ`VKr~S95_gdpRvfnP-K|r(KTq zHyoSmy{px+9jB=ElDS{i$MJmr`3is4Y@Syit7fycZ*yhm-ezbV~)i$K4@9jOl z-gCB|SND~<$!619S9z*EQq2f^M?ZPw0w3LEq6Un;Q8~)S`?89&kW6GUpXb!mV=B=h zV+cGh7<~=<{RGsU6cIH0Z}fSFZt~4u(6 zLtsz4=iCX{j+;=gywL+Dze>NAU#lLEaRXT}?TsuMZX&nu#Y9!jcC2s7ly({Rt=&mg zLu+2Bvae}9oeAwe2bFh|Rrl;l?l-^p#)?~xAe*BOoeVzXcwhD<>LA8Ecv*v?R*~u= zrjk8yWUwJJrZpbE_EMR#s~o2gsdtR2k4RnCZ47nRXWfu`BIXqKGS0S86SrmzIQn=S zxX8d2Z-;j5g(7<4!3!nod}~ut(GYy#Sx2N!>-LP+n4NLWt-S6so{Z{jW@P-l%nOcQ zFe@)zJuNVm2fTH}k1n_u8{m^AlM0Z;x?H%IvdWydpmmDb2)?ADHVjw*F{JaT~d z>ir6y(RS3Y&Ex7fcGGI~p6wVq*4iD=|C<*55|czlsv zgbHtCU&fcJZpLRXv%AHMfy$fn`B~S@e0+m1|Pzh85l|Us>2~+}=KqXKK ZR05SiB~S@e0+m1|Pzh85mB8f^_y-I*bcg@| diff --git a/examples/spacetaxi/34_4.wav b/examples/spacetaxi/34_4.wav deleted file mode 100644 index ae9d522ab381a775e1fed59cb30fe3cf07b1f569..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17962 zcmeH|O==uL423H>fsbH(fZQPv8?w#9SsP4t+4)$xOm2{qgrq2h{@zpfv^yRGV!)D~ z-lwY8{P@e~hljg|F}{9$y#Mz7<@RQb@$T>I?s1GyKgM`JZpQP|%hT`sF>ZhMt2fXa z=neD+dIPQFt^agqZy@B39Z=g4D?G60;Fkjcc zMW5dr=nb5?f%Uy+f6qJ#%|hoQAJ!0!HO%i6xn|_0|4H$je4Z%S&s3p!WaQL26+383^u!u5Sc_z?F{f%K zkfMH@A*X8AS?9{ItmgN>A9xNsRdh@tRR|UlqZ*^Ku8hyJQx)&5E2UUg@r*$_nuB~+ zLk!j|g>puuF@x2568^=PNn5P#3uB}0quhGy9MN8@c<-z$G^Zx&9maX} ztmyW2&DyT#q_U2ji)&BJbX541=&WlrrzXyPKlrSzJH$SEj;;SbnBi(X)7Hl*J3(?h zNxC>F|^ zyeM1b(I=I;a!!oW_c6Ly?I~U{UUWuEKSt)ydL(DP&lSl`;rHZ-&z|LZ+mYNmRwcds zblS05o?*KzkJMhD+ik#BUB^>fbO(**- z`c~~dxw@3XeyrxxM8W>4iX%tPNGCI*h$BaFC>W;xHy%64O(-Ks=yuGOnqc6mjensJBisDEX)SLMgJ zNY#7P^D|c|%FV2D&YpMMsv@mV^NO%NC46Q*EBAd=c?LPM=9yV#k5!`I_Rh`YNyq9l y?{w@2HFf6wu0QL4c-H!kynFrazSRxz4z^FnoL9G}yrsk6ZC3y8^nBLaD}Mk6uJ_Xb diff --git a/examples/spacetaxi/35_5.wav b/examples/spacetaxi/35_5.wav deleted file mode 100644 index ef53f4f90e7e9aada6a93fd4ee421d63544f4bb8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20456 zcmeH|OKy}w41|9;L5>h{fZPEIBC$eZ5!R6qJ9ar1N8l29v=dY*Ro8vg%z5ac8IF3)hkK_GubNqOGdi?Xs+&kJ`25JMff!aWApf*q&s14KxY6G=_+CXifHc%U=4b%o|1GRzLKyBbUHt=%i z|1L9^f5qQ-UiF@ryOk?CdS~v-T{BPa;BMw-J*zX%?Q=$Q@0_ld#o1Zy-^&>7dB^yb zj6_dUg@(CtM>RvS;uEupU)izyr;9xN5*%M*Q=GT{F+`9ni#)_mF{0?_qoTb z@lDNK^O1<1Q!>>PKbh*QH}c*vSGwQ4$1|#XHQtqPH)W!pb=0%^Rhd3_ z%~hQfGpP&xUa8t{p6I#0t3I#lEXY(B`prMnGTrn2u9GmM%J{~fR(sOx@jiE`8r`D~ z`#jS%?K0nLRpaN1gm-w4>TiF~JF+S9Mn?A@m|l6(>)D+>US}@mF=7-lu?zFS5v?_a*g`U zIn$8rNmYD`X4?FDH}i73x>%XbG;^tD)A?kd?m3^r_nyU_R^x@SvU*jwWr><+T|4J$ ze9?IG_tU!hX{@T&ZKo={jXa;dnSYkItMMD`;l9Rnl#(`$Oy zocv~vTy0$=`q??|sgo6*RXMxu^J`KG-0ZE<335C!GrdL{H1v8kQmpTJ-F8&DV;TKq zh3h?v9jDg%5^kD@eqJ%{3D#l_I%?c`J2(DS^jsv)NTzztQ^h3aBk4P1nw8mnEpg2% z(Qkg1-MeLhw+>Edd9_P|YgL|wF4O2*#VXgP<75>z^sL(DcD}8ak36ojm#?9R8E4ag zT7Q?L9L~tZ5I_q(if?8Z~4<=bLqlh*e+J3i0c?|v5@VCF1%vfiF2;+Bl=SJYt}5vb3( z2ihL{{LXo9#BZAYv}>&QTvuTi;;DOQ&r$LG$<13Hvpf3bJM-*X^sE<7!A6?WIGqYBane)nwb!O%&@^YSexpJmsSrt3h zuQia7s=wE9>sZm}N6l(m=Jg8utb;s9Xn>-g@jH(SW?+pYdCbO4qGXOUGDjISquv^W zvd5LKvw^HstIv+>FlF!E;Uw%kT}cHqOhaU+W?&o>IX%xaJR^K^c4q3ik_u#}a7R~F z{bOp}r9{-Y` u(Seztk#m{cF7wUIE_2CzD=tHvGu4p;azGBq0XZNCJJT0pS>8hq z8J|^n5;c9M&~!8w>mGCUsv{@QsPE;k-TN(=)5S9{oa?P~?l*FUba8XM^ezx$WZ#za zeqwd=4d>s+pLgp`y8XerSoL{3b|&WT^;Mm`evz}I#&UhQ2r<6_UEPj1D3VnszK-KZ z)Za|jc|WqczSJ|O;kFr3itHCFULw!ft+`la<19Lw4l;79zt@qU5%J*&sZ3s08LHkRsF7v<}&crim`F@KCjH!5=O zn#i4Lz@sAS;*cw%!d)&A$?Va~xxN|SaW}^*;;ydsx%pS3lc%q$akDtHD)DDK z;&=m?au~Vl)l6z-m9wf*F_T(*uGH%uUNxc`aS#~#<*dUmsBX=~fy=M9>Rj(qng6Bs zP3(L8n4kCc@)2=e$2h+a)6OfU7~gRYk<(*j*Y9?Do4MCRYUfk?&TJR{H~33(b!Hou z5&b<1k8_1y%A#tOn-Bf$3F^^%5gXYfQu)-GSu0qx2*2yEUW1M(JqXb{8xDg);;ClWvGZ9r#iwV&NTFEG`?Z-%?_0x>&&bUk%@n&Ff4PaMmC*! z%o^j3NUzR3sxw^IL~#B^ zyvSYKl1zExFS>!^smoJW@D)(5@Bl_D}x*BCjP zv1U0>%1dl9>jZV&L$zhmH+NzcSuu~C>K1wVS7WNwE3R1MgxGVBh%m=G5g(axK1?ub zi(q4nEb$_mHL+9ko(Vc<(}9^^O;-_~5X(SOs|JBjhdOG#r_Pw6COU&E&k`zJSoH4D zHHM>DAo$XuuJ07yWvGeHp*lB9{=HtPe6PjwUYfLit+_|8<>SN=LE{~&td+O)Vro*v z#O%~{SFzE`|C_G(wf9tS z+H>~>_62UeK>ct2QR-XYbwB<3BKMnfi&MYjEskfk{RR;|>-c}?$J=!IbAEsK9>?zS zs<}IzdrWy44>mj(@4L&T+4r6HFmCfVJj}e_b-u-!PVfD)(KGkU+?Rc~{rY`@^B2hf tK~Gto-+e3g1@;B@1@;B@1@;B@1@;B@1@;B@1@;B@1@;B@1zySp{spRnkvISV diff --git a/examples/spacetaxi/38_8.wav b/examples/spacetaxi/38_8.wav deleted file mode 100644 index 22c374505e9c7243faed0ae57272f47fdcc98e8b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23360 zcmeHHJ#Lgy4E*2(DL6pH0aDWC984=NX7d=C z`^d<$9L$)bmt)IBol&_0rivMPURCMl-ds0hQO!8OSTtN)&$ulkC{_fJ!{l4K4#3J+P=<_K8-Bk`)DY3 z!9kZbG2@KrD&sq_(NSYPn?T7?xvH6l;RFnJy+g!2i~LdU3VXT{MZMV#@ioL5H||A6 zM3r&lD2tlRE3N#ynd{Yj>PPkr%UNVpdDLc3e$GsRC{`I&Rm6_3sCejAtk^T0*UqpU z+;NNwIUjS{N#wP&x{eIb4Tr9Ctg6}Uf>B4})>%|oqYV)meknSfu9TeND6p%z(%GGl z_n87VGhgc(C!(OyM@O~z$nEo9W!#}YE^=(Iy4M+P;+{PnLzxzNIq&!IH{r0g^V4@kCF-J!Sm|Omjuq>i9yd7K z>pnk6n;dRAH>1|y$VRVj`|I7t0P38#f}3MhwMndFo>{AE=RR!1tDNJ?+{|pgGj6Pu z=j=R%TUjW-+VAdr!}2dsefZZ7Yq-%;_NBV0Xm)GIw2mD?F&9-&fDPd-)f< z?h9t#+3a;+uj}N19FPNYV9$ZofBV-m-+PTvazGBq0XZNCR^{41^z?AV-KeK=v$=AQD>^$vP5Z$1VrpFf6$YH^@U1-ygT_@zcYXA3vXOZ?4lxvT#!`*r#D^{Zd*a^3x-tlw51EYm|7^zB%Ep*r`lCMw#}vO#5Po(){AAQwiUAX53Ax_FeRv0ZaGu8Lo(h(=jFua zF^%)Vl%Q@ASXQs6?4t13Xl|NrXb#Y+9}|9+YLvjMn$xp5c{b1I1uB*alqQfix z&53nn6m^T-3tw>5)UVe;qmEgfx9i<>nr@-WdB&TJCW7zhsVcbT_fXRGnNDB5OUdtJ zN3QRhrxOo-oo=CS)g)yz@8+I;r?O6W%?Q_dgI)XQI-AQ>_u!1XFuPURg*7d9r4p~g z0h}4Cx+!+hDW+wO#qf)yW}y9yI0Z9uqSINCYm~5N-c39A$#`yy-6ayQ``5YW{`I z=DeSGwQt-DR0--9%`#O*ow}SZ(=B)o6Rzv;RSC14#J6ISYEZXG+3B)uYF4*RnCWI9 zSv|fCik!@PY2SVs?_;sLD9srY)^I()W3I?G>U_#9+m7qm-Qr%XiR{vHyhS%h-J&tm zD*lyc>#^_c<+wuJ4J6*Dk=TC;xxT5-@P>+6mvbl(HHLf~xx-xd$b%h5{FF7GjaVmU zb?v(T40nkC&&0RScSdEF!!hf4D|O$r-^U17y))Li=lHQtJ-+k*IQkZPk=_A)ebjef zW5wTnjc+^toBUe4epknzyd2jB&U*p;)<@BKcUjZc1=a=D1=a=D1=a=D1?mg@0|Wrm A?EnA( diff --git a/examples/spacetaxi/3F_x.wav b/examples/spacetaxi/3F_x.wav deleted file mode 100644 index 181300750175b4d64dd66f959e9a10715c0253a8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24876 zcmeHJOKub~5H*}2M;Ia2xdReJV#^{~kA&E<%dxmjZjhspmSuI@U9bGA>>g%9X%=l) zyfoLEaIIaQo|Icxb^Oey%?B}E2dqs9K>g?yU319F19+}BIEyz)~ zQ9hsca!zZeQBOaA&7ht9`kAR|`*!w3oKLOm;nWJ+jZ?oKl>9Eed@`#y?Wz8o-zfc5 zHbk!ldCz#rOnHj*WVAk&v=BqWwll=-&08IG*L4uf`t8Fy>t>&1-Qm-o#Z0i?c7v6+ zsQnr9>dl@xb5eU*Zh8ta$EA}@c( z+1QOYGpk?Yylxt=c9?b39P!TLj#cJuEO|7nH<6}^cV4%ahaE;YtrLH@I`hgB((2~L zOWysN-L0=FA?!X|MpoQT^s|(Jo$1Q$mQ}^LR_z44C1~gB9_{>(%nGgMHI#Tkcjj zi?)unIx~3P5pNl7gu$xz4f3$e?XHDkamm=iJmP6jQy9K9h0Q;GK01qcM@U^CZ;>9J zV=eCndodfY>*DYR9|_E*z^69t?u~wGCBM% z4Aa*Fql_@TBT}%;c{c`Wajvn9dX1_1_FJts>t@ffGWr=-toPLS@FKj479Qg{qK~#Cqo3@kHmj9N}MxQTU3nF{wXSwUl_qWx0FFhUc)}{Np{gUPg zM~`O7Q@$^Y#yZ0%kJpi$Hfi=MKR@epJz3HemW5MemwY8to7Hn)wQ3*f#_3DU_dUzK naNl>m!`taee-CbZJ8v6Fqk(838i)p>foLEahz6p8+iu`56anwq diff --git a/examples/spacetaxi/48_H.wav b/examples/spacetaxi/48_H.wav deleted file mode 100644 index 173de91f23867961f1f83bfba4553326517a3a54..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18072 zcmeH{&8?nQ41`~(ASFZ;Ko3X|i7kt;j)YjjE`?AC&Cmg*5X&0PjO}y2AHE0_9bGB5 z$Isa3b#C(0kMG|7@$R~Q{_gz`zyAL5?VIbmzWVg@&HL;6{?F_B`g(Ky^5Nr$-##&K z|2jYC0_Otf0_Otf0_Otf0_Otf0_Otf0_Otf0_Otf0_Otf0_Otf0_Otf0_Otf0_Otf z0-xsse}DVm|9qY=J5$dE&IO*iz$^YocxLabJ-hGN?W(>abC}~Er6X6$b0@v0m2+N!n>Hz#Tvy=NsMIu9+2`i2 zT)8>AzE@v_F14K2|0Gefd)UrE!L+PVMFhd$Au7~B&Q5hiu9W7LV?9x!k;p7L&Ec$? zJ6BZS03RGRqTi#E?54_kbsZ9T6JmBaz1Gfg&T3OkQp$7g*-`Y6Pc7@n>mHEGxyGpz z1Gdg}mr|ZPhl-5ujJ)n~l2zp}88e(jrIhC&GG-5&litJMm->|V8K@#E<+)OxJExU% ztdv1syR7|H(9?%1BWBg;lvb5w4T{Px1D$V06^_W&sXhO9sg&k$^r%$LK|`-KbVT(1 z3ql1E5maiZGAgIJQ(7tIIVtE+P}k^vR1uYg4h?nX(2uB$=Sq1_j{0tgZb=6x=oe7k zQQX-nPSCmcI@X>#7xij#e_16_V^WVGdh9He&O!kZF*MZZGiC=hI9Jil=nN~Tydve9 zQ(8@2YD#79x;i@xMJdhEpQ3WiCz2Yu$Wl*@3^VOaPLjx+Vuk)3$swD$P(gT1KDF)z z+x?x@N$_c0Ir{9?YjlE6My`{T&0Hy7qjRe8C2%t;S94OHRZeS?eBHnKO@mi=M4o$6 zyUHM#JH>Z|NjR}Z%_>pt!IgwCcyhx4db zzR=I$eCkB@uy4t9Ody;b^? z=BznW%ID8~Ro&xNWnWeMmV2w*4toEVp!UQgUF`ID?}X=E_fF@(9p97H?J1KB+ATM@ zRowe$*WUAdci!ge^V7d9r(fk(^<{ngtH0*f^`if(o%if|oO#vj&YW|B|KA1v0SwI- ATmS$7 diff --git a/examples/spacetaxi/50_P.wav b/examples/spacetaxi/50_P.wav deleted file mode 100644 index e9ea5a4a352b030283761af9e43a7fe3cda80b06..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24446 zcmeH`L5dw`41_y5fsbHvfZQRF7_#-kSsFvgE<2M` zRr;jApRnKm{_fpBACKdYUq1Zy*WaJtzB!KLhp&G>eK?L^|2>W$k2lAkpFV$j|CM?B zW&id9dx5>cUSKb<7uXBz1@;1afxW<9U@x#2*bD3h_5yo>y}({zFR&Nb3+x5<0(*hI zz+PZ4uou`1>;?7$dx5>cUSKb<7uXBz1@;1afxW<9U@!2RF7V&a-}3J@{qk?|txvt( z^d@&q-Pf7%H$7Fo&foj!tLG~`ntI&Vy^rZ6a^^9uyF*`1RQ8&_zv+s6`=!4+f9GA! zU&EBA-GO6OhUuh~Og&MVb=D`)4;uOO(tCS2=nzu1K6KpMaZq^s+h;nI_JvQ_8hczHR1^ zat;Y^$fPPx;VmZ4dQOKq(AjZE8hNdifUn$@#|*wGeN2SX$x5DG3LIwP>pt|el0uFI zey){*k2+6eN)A*{ly$bWQIR8qXGMo> z^q6*4XRGfMcXxA>t7#)LC&QsLzY3mID5RLK-syB?LV-{{ah$@$R7r6&xw+e4{+7Rs zucy*)Q0<&{R69j?htXf9{B)_<>`Xeo=&G~cPuHnP?vfKX?FBdOp1)xJhS=jgHhXmj z&Y8XoT-44YQR<4*FU{GU#GbQ?{g3*OwqegyxqIHinIz#kM-@G%;E|F=Quo2@B(vwM z`ZB%lM4A~KD#+^fThT+sOjOU{qAI0YnRAL0$}9cCaifkOp%<}>Hs@R-3{WTwPt z@y=AamwT9DA`>{&yBzqeoZ^f5`;;Ta6d4lusK}USOt$mj#%0hE{S77+{o$24psjlwNE7|ujA(vV8Bvp|L zr5)8y(J9k4xZXhm0u?zcI#Dytgannc)a-XfN>$~wr@C_VO5wZNRpsQ&R%WJhtF!#~ zs&95(r#cadQXrTid1~c`v)`q}q#wPAndIQkUc)wWRe|W$$=;t6pQ4*-q~xkfLQ_pJ z&k9nh%*a%QnWf@6GFrsjucbjpSH;z+PZ4uou`1>;?7$&%40?&koL2 diff --git a/examples/spacetaxi/54_T.wav b/examples/spacetaxi/54_T.wav deleted file mode 100644 index 2614012419a8ab6266c73d84ded321c3b22a56d6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25694 zcmeHLOHLe74D93t90BD3EOLWL2~xIMkTnv@E<28u%WwmZ5-h7!{=NQ8_YfvnBVoH- zF8fu85d8RldwcgVO+UZfef#zM@%n0-rVlTlPj}Pw_0KeYoUW#a`^WpwFP!VA{0(M9Gv;L@J`NSl8DXFor}Wv{gHxaS{6Hs zQZKT6tvrim#CA2O`Y4MpvcB3G(Tg?rX$LiUsH<3WypGOgEY70#QKo?)$0fjX1cOmI zP+&!d&fUx7t5BfK9yM48fD=Ot$bmXF+o%=fiIuZrMTJz(HS1g5vLlFagK&YtqA zV_81;$|LQ_^0g{@%vap$abJ-YGvNVa&I~x?Sq|JF0Gtao1cwvTUvS_^armi(GdB zMI@@3{R)mm;5G-wSWU3289D4^j7ZF63K;UJtbOQ9g`5!@?D;7&H6x!Bk;BW#;hbd? z^%*#VB1g1LkD)@0Ow{Q6h^|K8vsLC18S}{Div+V1);HhtmCjHR8bDA>^pjx3y>SA( zh}+s zci`yOEx2B>{*HAo^zYqT`D?H-M*gevy}AiZzgJQAn0elw3Yrr275**Tv`?AU&Dg$e zpE`xOh-pu7kzHV;Mk-Z~t z9&4R?@mTSFcGmm&KK)veSHv;*xxJMcCf*z;52sM)vS QL|@Skv;*xxJFv}xzXGWd>i_@% diff --git a/examples/spacetaxi/55_U.wav b/examples/spacetaxi/55_U.wav deleted file mode 100644 index 427a765959e600d204e1c887805250b0b6b84cbf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25264 zcmeHFOKufG3>-K?j_||*xC2s%#Fj-^Cqmh=%MmyX3oerza1>%$maA+}zZqqPD7{77 z_3NI}`}*bX?$_hCefxC(`RT{=_uF>+d)wY`H{17z=ZBxKn#;?dkM(zX zS^w7h%I@v`j!;%6a6g%4Zhu?5kmU?yrmk3R8#{b+Rb{%jyC+S>IG5Sv80VWRXZ!3- z)F*dVv^G`w>AsFtS555>eD$pEnT*WeQ*W9)RnzzDE>`5$vm8kUPpD_6x57L+^Nx;k z)0Q!5Du#De&UWN+MlIs?R^}=j`Ir`Sb#`wk&!0WJBb-`!W_{}~tgfzz22wdCcg*#M zr*k!NLRB(5ZPju8OQw{L_1r{EotsVUXRhsm{rjxyQr_f%?guZEHN-)ff2W zMsDphWmV*xFlSOS-*dW$9sTY#k1XDXc+ZT6S;ZTsbv?7TxtnoL?C)6f$nNDbp6{q$ z+~XZ_x~qHlvKgO}Q^Bll$@!EDWD;FD)?Bc8E2hSbx5k_fL_E)Zf#(@{-5Z*!z`eW9 zv93PyFO1jhpYaxa!K6k$y6!qw#nyH4_E6Z_RmgUQv*yaFXG{p@9^v=%F=OJ9^BQ*_l%ObhWX0&?_j-e=wg1K;WkZ&PV_O;Z>&6eG1KN1 z$E+*Uk4~GDIX7X> z)BR`ssKqSAaYoJg8I%5{xqj*V_NwB2_6Su2YCsLB0X3io)PNdL18P7Gr~x&g2GoEW zPy=c}4X6P%pa#@{8c+jjKn`qlKE=1 diff --git a/examples/spacetaxi/MECHANICS.md b/examples/spacetaxi/MECHANICS.md deleted file mode 100644 index 24165ea..0000000 --- a/examples/spacetaxi/MECHANICS.md +++ /dev/null @@ -1,2217 +0,0 @@ -# Space Taxi Mechanics (research notes from C64 disassembly) - -Source: VICE memory dumps in `stuff/spacetaxi/` decoded via the -`dis6502.py` / `disLive.py` helpers. Full live-RAM listings are at -`stuff/spacetaxi/live-level1.lst` (40,069 instructions) and -`live-raw.lst` (titlescreen snapshot). This doc captures observed -behavior in plain English to drive the JoeyLib port without dragging -the original 6502 source into chat or comments. - -## Memory map (post-load runtime) - -| Range | Contents | -| ------------------- | ------------------------------------------------- | -| `$0801-$1AFC` | Loader + initial data (looks data-like, not code) | -| `$2800-$2FFF` | Custom charset (256 chars x 8 bytes mono) | -| `$3000-$37FF` | Sprite bitmaps (referenced by $07F8-$07FF ptrs) | -| `$4000-$6FFF` | Game code (physics, input, level logic) | -| `$7000-$7DFF` | Game state globals + per-level data table | -| `$CB00-$CFFF` | Music engine + tracker per-voice state | -| `$CF52-$CFC0` | Custom IRQ handler | -| `$0400-$07E7` | Screen RAM (40x25 char codes) | -| `$D800-$DBE7` | Color RAM | - -## Input handling - -- Per-frame routine starts at `$6032` (zeros velocity), reads - `$DC00` at `$6040`, applies `EOR #$FF` so bits = 1 when pressed, - stores processed mask to global `$7169`. -- Bits in `$7169` after EOR: `0=UP 1=DOWN 2=LEFT 3=RIGHT 4=FIRE`. -- At `$6056-$6065`, if screen-RAM sentinel `$07A6 == $7B` the - dispatcher masks `$7169` to bit-4 only (menu mode, only fire - matters). Otherwise gameplay path runs. -- Gameplay path tests LEFT/RIGHT (bits 2+3) and UP/DOWN (bits 0+1) - separately. **Fire is never tested in the gameplay path.** -- The **fire button** is consumed by `$63DD` (a post-tick handler): - edge-detect via `$71BD` latch. On press, toggles `$7197 bit 0` - (cab anim parity) and triggers SFX from pointer `$6410` via - `$42E9`. Likely the bonus-press to advance score. - -## Velocity model (CORRECTED -- earlier "template velocity" note was wrong) - -The physics is **acceleration-based**, not constant-velocity. Stick -input sets an instant per-frame *acceleration*. That acceleration -adds to a persistent *velocity accumulator*, which adds to *position* -each frame. Release the stick and velocity persists -- the cab -drifts. Push opposite to decelerate. Gravity is a constant -acceleration always applied to Y. - -### Variables (named for what they actually hold) - -| Var | Meaning | -| ------------ | ------------------------------------------------------------- | -| `$7148/49` | Per-frame X acceleration (the "instant velocity" from input) | -| `$714A/4B` | Per-frame Y acceleration | -| `$714C/4D` | X velocity accumulator (16-bit signed) -- persists across frames | -| `$714E/4F` | Y velocity accumulator | -| `$7D91/92` | X-accel magnitude template (= 14 in level 1) | -| `$7D8F/90` | Y-accel magnitude template (= 14) | -| `$7D95/96` | X gravity (= 0 -- no horizontal drift) | -| `$7D93/94` | Y gravity (= 1 -- constant downward acceleration) | -| `$7D61/62` | Persistent X position, 16-bit fixed-point | -| `$7D64/65` | Persistent Y position | -| `$7D63` | 1-bit X fractional carry | -| `$7185` | Mirror of `$7D63` | -| `$7175` | Cached high byte of Xpos (= pixel column) | -| `$717D` | Cached high byte of Ypos (= pixel row) | - -### Per-frame algorithm (`$6032`, full trace) -- VERIFIED via emulator - -Verified by running `$6032` through the 6502 emulator with various -joystick / sprite-ptr states. See `stuff/spacetaxi/trace.py`. - -``` -1. Clear instant accel: $7148/49 = 0; $714A/4B = 0 [$6032-$603F] - -2. Read input: $7169 = $DC00 EOR $FF [$6040-$6045] - -3. Menu-mode test: if $07A6 == $7B: $7169 &= $10 (FIRE only); jump to vy [$6056-$6065] - else: fall through - -4. Horizontal accel gate (this branch was missing from my earlier write-up): - if $7197 & 1 == 0: [$606F-$6076] - (a) $7148/49 = $7D91/92 (xAccel template, default +) - (b) if $7169 & $08 (RIGHT) : keep positive [$6088-$608A] - (c) else (LEFT) : two's-complement negate [$608C-$60A2] - if $7197 & 1 == 1: [$6190-$6198] - $7169 &= $13 (mask out L/R bits) -- horizontal accel SUPPRESSED this tick - -5. Vertical accel: [$60A4-$60D3] - if $7169 & $03 (U/D): - $714A/4B = $7D8F/90 (yAccel template) - if UP bit set: negate - -6. Integrate accel -> velocity: [$60D6-...] - $714C/4D += $7148/49 + $7D95/96 (X vel += X accel + X grav) - $714E/4F += $714A/4B + $7D93/94 (Y vel += Y accel + Y grav) - -7. Integrate velocity -> position: - $7D61/62 += $714C/4D (Xpos += X vel) - $7D64/65 += $714E/4F (Ypos += Y vel) - -8. Update caches: - $7175 = high byte of $7D61/62 (pixel col) - $717D = high byte of $7D64/65 (pixel row) -``` - -Verified single-tick results from emulator runs with level-A -templates (xAccel=$0E=14, yAccel=$0E=14, xGrav=0, yGrav=1) and a -zeroed velocity accumulator: - -| Input | $7197 | ax | ay | vx after | vy after | -| ------ | ------ | --- | --- | -------- | -------- | -| none | $C0 | 0 | 0 | 0 | +1 | -| RIGHT | $C0 | +14 | 0 | +14 | +1 | -| LEFT | $C0 | -14 | 0 | -14 | +1 | -| UP | $C0 | 0 | -14 | 0 | -13 | -| DOWN | $C0 | 0 | +14 | 0 | +15 | -| RIGHT | $C1 | 0 | 0 | 0 | +1 | <- parity 1 suppresses X -| RIGHT+UP | $C0 | +14 | -14 | +14 | -13 | - -**Asymmetric gating**: only X accel is gated by `$7197 & 1`. Y accel -fires regardless. So when the sprite-ptr flicker bit is 1, the cab -can still go up/down but not left/right. - -**When does the parity bit flip during gameplay?** Hot-grep for -`EOR #$01 ... STA $7197`: -- `$6A93/$6A95` -- only runs in collision-phase 1 (during death) -- `$63FF/$6401` -- `fireButtonEdge`, fires on FIRE button rising edge - -`$619B` (called every frame in gated dispatch) WRITES `$7197` but -PRESERVES the parity bit (`ORA $61` where `$61` holds the captured -LSB). So during normal hover-fly gameplay, `$7197` parity changes -only when the player presses FIRE. - -This means: in stretches of gameplay where FIRE isn't pressed, the -parity bit is fixed -- so the cab either has horizontal accel every -frame (parity = 0) or never (parity = 1). Effective horizontal -acceleration rate depends on the live state of `$7197 & 1`. I have -not yet traced what sets the initial parity bit on level entry, so -the steady-state value during ordinary play is currently unknown. - -The 16-bit add helper at `$4072` is the integration step, called -12 times across the routine (vx+grav, +dx_accum, +Xpos and the Y mirror). -Inputs at zero-page `$61-$64`, output back to `$61/62`. - -The fractional carry at `$6124-$6132` adds an extra +1 to the X step -every other frame when the high byte's carry chain dictates -- -gives sub-pixel granularity without floating point. - -### Why the dump showed `dx_accum = 84`, `dy_accum = -66` - -At dump capture time the cab was mid-flight, drifting right (vel +84 -sub-pixel-units/frame) and rising (vel -66 sub-pixel-units/frame in -screen-coords where +Y is down). Confirms the velocities accumulate -and persist across frames. - -### Templates and gravity per level - -Per-level writers re-set `$7D8F-$7D96` at level init (`$5BD4`) so -different levels can have different gravity / handling. Level 1 uses -{X-accel 14, Y-accel 14, X-grav 0, Y-grav 1}. Different levels may -have stronger gravity, sluggish accel, etc. - -The templates are saved on death (`$5A9E-$5ABC` copies `$7D8F-$7D96` -into a cache at `$5CE9-$5CF0`) and restored on respawn (`$5BD1-$5BFE` -copies the cache back). This is part of the trampoline disable/enable -flow described in "Trampoline patching" below. - -### Physics is called from 3 places - -`$6032` is a generic motion subroutine called from: - -- `$5F45` -- main gameplay (gated by `$7164 == 0` = no collision dispatch) -- `$5C2C` -- death/fall animation loop (exits when `$717D == $D4` = row 212) -- `$5C91` -- takeoff animation loop (exits when `$717D < $87` = row 135) - -Same motion math is reused; only the exit condition differs. - -### Screen-edge behavior -- BOUNCE, not wrap (`$6AED`) - -`$6AED` reflects X-velocity at the screen edges instead of wrapping -or stopping. Two branches: - -``` -$6AED if $7185 == 0 (low fractional, going left): - if vx-hi ($714D) < 0 AND col ($7175) < $17 (= 23): - negate ($714C/$714D) ; EOR #$FF, INC for two's complement -$6B17 else if $7185 != 0 (going right): - if vx-hi ($714D) >= 0 AND col ($7175) >= $41 (= 65): - negate ($714C/$714D) -``` - -So crossing column 23 going left, or column 65 going right, inverts -vx. The cab bounces off the screen edges like a bumper. JoeyLib port -currently clamps with vx=0 -- needs updating to match. - -## Per-level data table (`$7D00-$7D60`) - -Per-level data lives in this 96-byte region, written by the level -loader (the `$6283` bank-switch wrapper calls into `$9656` which -runs in all-RAM mode and reads level data from RAM beneath the -Kernal/I/O area). Layout derived from readers: - -| Offset | Use | -| ------------ | ---------------------------------------------------- | -| `$7D09` | Special-pad sentinel: $6E2E branches when active | -| | pad index ($7150) matches this | -| `$7D0A,X*8` | Pad table base. Each slot is 8 bytes: | -| `,X+0` | pad data E (read by $645C / $67E3 setup) | -| `,X+1` | pad data F | -| `,X+2` | pad-X bound2 high | -| `,X+3` | pad-X bound2 low | -| `,X+4` | pad-X bound1 high | -| `,X+5` | pad-X bound1 low | -| `,X+6` | pad row (compared with $717D = taxi row) | -| `,X+7` | (reserved / decorative?) | -| `$7D5A` | Pad count (number of slots used in the table) | -| `$7D5B-$5D` | Taxi spawn X position: lo, hi, frac | -| `$7D5E-$5F` | Taxi spawn Y position: lo, hi | -| `$7D60` | Fuel max (or initial pad-cell count) | - -### Pad detection (`$645C`) - -Gated routine, called every frame from the post-tick chain. Only -runs when `$7150 == 0` (airborne) and the anim-parity bit is set -and Y velocity high byte is zero (slow descent). Then: - -``` -X = $7D5A (pad count); decrement X -loop: - if $717D (taxi row) != $7D0E,X*8 (pad row): try next pad - set $61/$62 = $7D62/$7D63 (taxi Xpos) - set $63/$64 = $7D0B,X*8 / $7D0A,X*8 (pad X bound) - JSR $6565 (16-bit compare); if Xpos < bound1, skip - set $63/$64 = $7D62/$7D63 again (taxi Xpos) - set $61/$62 = $7D0D,X*8 / $7D0C,X*8 (pad X bound2) - JSR $6565; if bound2 < Xpos, skip - $7150 = $7151 = X+1 ; mark on-pad - $71CD = 0 - compare X+1 to $7D8E (active sprite idx); if equal, jump $64F8 - ; (continuation handles death-counter, music, etc.) -``` - -So pad landing is detected by **row match** + **column bounds -overlap**, indexed off the per-level pad table. The result is -stashed in `$7150` (1-based pad index, 0 = airborne). - -## Game state sentinel - -`$07A6` is the top-right cell of screen RAM. Used as a one-byte -state tag distinguishing menu mode (`$7B`) from gameplay. Many -input/handler sites branch on this value. Title/instruction/score -screens write `$7B` there; level start writes something else. - -## Crash / pad / passenger systems (mostly traced) - -The original game leans entirely on VIC-II hardware collision -registers, no per-cell screen-RAM walking: - -- `$D01E` = sprite-to-sprite collision latch -- `$D01F` = sprite-to-background collision latch (reading either - also clears the latched bits) - -Three call sites use them: - -| Site | Role | -| ------- | ---------------------------------------------------- | -| `$6920` | clear-only (reads to reset latches, JSRs anim tick) | -| `$6946` | death/transition: `STA #$00 -> $D015` (sprites off), | -| | clear collision/menu/animation state | -| `$6966` | per-frame dispatch: latch both regs, then jump to | -| | `$6A72`/`$6B24`/... based on `$7164` phase | - -The phase byte `$7164` is the dispatcher; `$7165/$7166` form a -frame-rate gate (count down each frame, reload when zero) so each -phase handler runs every N frames, not every frame. - -### Collision dispatch trace (VERIFIED via emulator) - -Verified by running `$6966` from `Tracer.from_dump('raw.bin')` with -hand-set `$D01E`/`$D01F` values and observing the resulting `$7164` -phase and `$7197` cel transitions. - -| `$D01E` (ss) | `$D01F` (bg) | Phase after | `$7197` | `$721D` | Outcome | -| ------------ | ------------ | ----------- | ------- | ------- | ----------------- | -| $00 | $00 | 0 | $C1 | - | no change | -| $00 | $01 | **1** | **$CC** | $01 | DEATH | -| $09 | $00 | **1** | **$CC** | $01 | DEATH (passenger 3 + sprite 0 collision) | -| $09 | $01 | **1** | **$CC** | $01 | DEATH (bg wins) | -| $08 | $00 | 0 | $C1 | - | no change (sprite-sprite without sprite 0 bit) | - -**The $7D75 trampoline is STATIC** in both `raw.bin` (under-ROM -capture) and `mem0000-level1.bin`. The bytes are `$4C $9A $7D` -(JMP `$7D9A`) where `$7D9A` is `$60` (RTS). So the trampoline -ALWAYS returns A unchanged. Grep across the entire disassembly -finds NO writers to `$7D76` or `$7D77`. The "trampoline gets patched -to allow pickup" theory I had earlier (in an earlier draft of this -doc) was wrong -- the operand is fixed. - -Demonstrated mechanism via emulator: manually plant `LDA #$00; RTS` -at `$7DA0` and patch `$7D76/$7D77` to point there. After: - -| Trampoline target | After taxi+passenger collision | -| ------------------ | ----------------------------------- | -| `$7D9A` (RTS) | phase=$01, sprite $C1->$CC, DEATH | -| `$7DA0` (LDA #$00) | phase=$00, sprite $C1, no death | - -So the mechanism EXISTS but isn't used in any state we've captured. -The RTS-without-death path at `$69B3` requires `$71CA` bit 1 -(sprite 1 = flame) ALSO set, which is a multi-sprite collision -involving cab + flame + passenger. Possibly that's how pickup is -detected -- the C64 game arranges for the flame sprite to be -involved in a 3-way collision when boarding. Not yet verified. - -For the JoeyLib port: my explicit pickup logic ("if cab is landed -on the passenger's pad, board them") is a clean simplification. -The C64's mechanism via 3-way sprite collision is too entangled -with hardware-specific behavior to reproduce directly. - -### Phase 1 handler (`$6A72`) -- fall + thrust SFX - -This handles in-flight gameplay. Each tick it: - -1. Reads `$721D` -- if high bit set (BMI taken), jumps to the - phase-advance / clamp branch (player is dying). -2. Otherwise decrements `$721B` (thrust freq sweep accumulator), - shifts it right one bit, writes both copies to `$D400/$D401` - (voice 1 frequency lo/hi). This is the **thrust hum** -- voice - 1 freq decrements over the course of a thrust burst. -3. Toggles `$7197` bit 0 -- frame-parity flag for visual sync. -4. Integrates `$714C/$714D` (per-frame dx) into `$714E/$714F` - (sub-pixel row accumulator), +40 per frame. -5. Adds the integrated row delta into `$717D` (taxi pixel row). -6. If `$717D >= $DA` (218), clamps it and **INCs `$7164`** -- - advances to phase 2 (the floor/pad has been reached). -7. Falls through to a screen-edge wrap routine at `$6AED` that - inverts `$714C` when the taxi crosses column 23 (`$17`). - -### Phase 2 handler (`$6B24`) -- death sprite-cel walk - -After phase 1 clamps and INCs `$7164` to 2, this handler runs: - -``` -DEC $7165, BEQ continue, else RTS ; frame-rate gate -INC $7166 ; slow the rate over time -$7166 -> $7165 ; reload gate -INC $7197 ; advance sprite-0 cel ptr -LDA $7197; CMP #$D1; BEQ advance ; reached final death cel? -RTS ; otherwise stay in phase 2 - -advance: - $718E = 0 - INC $7164 ; -> phase 3 - $7165 = $46 (70 frames) - $68/$69 = $6DDF (death art pointer) - JSR $42E9 (load death SFX/art) -``` - -So phase 2 walks the sprite-0 pointer from `$CC` through `$D0` (death -animation cels at `$3300, $3340, ... $3400`), one cel per N frames -where N grows over time (slowing-down "fall apart" effect). When it -hits `$D1`, transitions to phase 3. - -### Phase 3 handler (`$6B4C`) -- pause, lives--, respawn or game-over - -``` -DEC $7165, BEQ continue, else RTS ; 70-frame pause -LDA $721C -BEQ $6B64 ; not in menu state, continue -PLA PLA ; drop our return + caller's return -LDA $5E9D -BNE $4955 (exitToTitle) -JMP $5EC4 (gameInitContinue) ; restart gameplay -``` - -Otherwise (gameplay state): -``` -LDA $7163; CMP #$05; BNE $6BBA ; death-stage gate -JSR $43A5 (hudInit) -LDA $715C (lives); BEQ $6BA1 ; out of lives -> game-over -LDX $716D (active dying slot) -$7152,X = 0 ; blank slot -DEC $715C (lives--) -LDA $7D8E; CMP #$0B ; was it the "main" slot? -BNE $6B96 ; not -> continue alt branch -$0412/$0413/$0414/$0415 = $67 ; rewrite HUD chars -JMP $6BA1 (game-over path) -``` - -The respawn path at `$6BBA` decrements `$71CF,X` (per-passenger -counter), if zero jumps to `$6BD0` (post-death-transition); else -patches HUD char and JMPs `$5F27` (mid-prelude re-entry). - -`$6BD0` (post-death-transition): JSR `$704E`, JSR `$5FBB`, -INC `$71CE` (fare counter), compare to `$7213`; if all fares -delivered, JMP `$602F` (level complete); else JMP `$5F02` -(main loop re-entry). - -So the death pipeline is: -- Sprite-bg hit -> $6A30 starts phase 1 -- Phase 1 -> cab continues physics, BMI on $721D drives to phase 2 -- Phase 2 -> animates sprite-0 cel forward $CC..$D1 (death anim) -- Phase 3 -> 70-frame pause, then DEC $715C, blank slot, transition - -## Main game loop (`$5F18-$5FB8`) - -Falls through from gameInit (`$5EC1`) on level entry via -`$6FDC: JMP $5F18`. Tight loop, **no per-frame wait** in the JMP -back -- music IRQ ticks asynchronously, but `$5F94`/`$5FA3` calls -to `$404B` (VBL wait) provide explicit raster sync mid-loop. - -``` -$5F18 prelude 8 JSRs (level-entry one-time setup): - $61FB advanceFareSlot - $4248 spriteShadowInit - $6946 spritesOffReset - $7D66 stateVec0 (RTS stub on level1) - $6F18 takeoffSetup - $6888 taxiSpawnInit - $7D69 stateVec1 (RTS stub on level1) - $6906 framePresent - -$5F40 loop top Gated dispatch: 9 JSRs guarded by - LDA $7164 (collisionPhase); BNE skip. - Skipped during death animation: - $6032 physicsTick - $63DD fireButtonEdge - $6D6A flameSpriteUpdate - $645C padDetect - $619B taxiSpriteCelSelect - $6BE7 levelEndCheck - $6DFF padLandingBob - $6E23 padPaymentAnim - $61BD passengerEventDraw - -$5F88 post-work Unconditional JSRs (animation, sprite - updates, scoring, etc.): - $67E3 postTickStateGate - $65C1 deathStageDispatch - $7D6C stateVec2 - $4253 spriteShadowMarshal - $404B vblWait <- explicit VBL - $4293 spriteShadowFlush <- now in vblank - $63D0 bitScrollRight - $7D6F stateVec3 - $6419 fuelBarHud - $404B vblWait <- second VBL sync - $6966 collisionDispatch - $43E5 hudDraw - $4320 sfxEnvelopeTick - $4FCB runStopWatcher - $70B1 passengerArrTick - $4BF8 (dead -- single-shot DDR setup, fall-through) - -$5FB8 JMP $5F40 loop close -- repeats dispatch + post-work. - Prelude is one-shot on entry. -``` - -The two `$404B` VBL waits straddle the sprite flush so screen tear -doesn't happen during the shadow-to-hardware copy. - -The single entry to `$5F18` is `$6FDC JMP $5F18`. Three sites in -the init chain jump to `$48AD` instead -- that's the **menu / title -state** alternate path. - -### State-vector dispatch (`$7D66-$7D75`) - -Six adjacent 3-byte slots, each holding `JMP abs`. The main loop -calls into them via `JSR`, so the slot's `JMP abs` operand acts as -a function pointer. - -**Trampoline patching mechanism**: state transitions don't patch -the JMP operand bytes (`$7D67/$7D68`, etc.) -- those are static. -What gets patched is the OPCODE byte (`$7D66`, `$7D69`, etc.): - -- Writing `$60` (RTS) to the opcode byte = trampoline disabled, - whole slot is a no-op `JSR `. -- Writing `$4C` (JMP abs) = trampoline enabled, jumps to whatever - pre-baked operand follows. - -`$5AC1-$5AD0` is the **trampolines-off** patcher (writes #$60 to -all six opcode bytes), `$5C03-$5C12` is **trampolines-on** (#$4C). -These bookend the death-and-respawn sequence: shut everything off, -play the death anim with most main-loop work neutered, then -re-enable for respawn. - -The JMP targets in level 1's gameplay snapshot (`mem0000-level1.bin`): - -| Slot | Called from main loop | Target | Role | -| ------ | --------------------- | --------------- | --------------- | -| `$7D66`| prelude ($5F21) | `$7D9C` (RTS) | post-prelude 0 | -| `$7D69`| prelude ($5F2A) | `$7D9B` (RTS) | post-prelude 1 | -| `$7D6C`| post-work ($5F8E) | `$7D9D` (RTS) | per-tick 2 | -| `$7D6F`| post-work ($5F9D) | `$7D98` (RTS) | per-tick 3 | -| `$7D72`| `$6283` decompress | `$7D99` (RTS) | level-init hook | -| `$7D75`| hit dispatch ($6A03) | `$7D9A` (RTS) | passenger-hit verdict | - -On the title screen (`mem0000.bin`/`mem0000-title.bin`), some of -the operands point at real routines (`$7E5A` for slot 2, -`$7E57` for slot 5). Those operands are loaded as static data -during the title-screen boot, not patched by code we've found. - -### The `$7D75` hit-dispatch trampoline (resolved) - -Called from `$6A03` with `A=#$01`: - -``` -$6A01 LDA #$01 -$6A03 JSR $7D75 ; trampoline -- may modify A -$6A06 STA $721D ; player-state byte -$6A09 LDA $71CB -$6A0C AND #$01 ; taxi-vs-background? -$6A0E BNE $6A1E ; -> death start -$6A10 LDA $5569 -$6A13 BEQ $6A16 -$6A15 RTS ; menu-mode-ish gate -$6A16 LDA $721D -$6A19 CMP #$00 -$6A1B BNE $6A1E ; -> death start if trampoline said die -$6A1D RTS -``` - -So the trampoline's role is to **decide whether a passenger contact -should kill the cab**. The default `$7D9A` target (`RTS`) leaves -A=1, so $721D=1, so the BNE at $6A1B IS taken -- passenger contact -without further state would crash the cab. That's actually consistent -with the original behavior: random passenger collisions DO kill you; -you have to be on a pad in the right state for them to board. - -When the per-level setup wires a different target (e.g. `$7E57`, -`LDA #$00; RTS`), A=0 after the trampoline, $721D=0, BNE not taken, -passenger contact is benign. This must be triggered during the -pickup window (passenger on top of cab while landed on right pad). - -JoeyLib port doesn't need this trampoline scheme -- our pickup logic -lives explicitly in `stPassenger.c`. - -### `$7D72` level-init hook - -Called only via the `$6283` bank-switch wrapper. Probably loaded -with a per-level "post-load setup" target by whatever generates the -per-level table. On gameplay-state dumps it's `RTS`. - -## Title / menu state (`$48AD`) - -Entry from `gameInit` (`$5EC1`) when `$5E9D != 0` (post-mortem) or -`$721C != 0` (menu mode requested). - -``` -$48AD $7213 = 1 ; one fare needed - $721C = 1 ; menu mode - $7214 = 0 - $7171/$7172 = 1 ; menu timers - $3F/$40 = $0900 ; script buffer - $5569 = 0 ; clear paused flag - $595C = $20 - $5975 = $2A - if $5E9D: - $0900/$0901 = $00 $FF - JMP $5F02 (mainEntry) - else: - $3F = 2 - JMP $5F02 -``` - -After the JMP to `$5F02`, the main loop runs with `$721C=1`, which -causes the post-tick chain to behave differently (menu-only paths -in $4FCB, $6BE7). The actual title-screen DRAWING happens via -`$4525` (title sprite setup) and `$4861` (logo color cycle). - -`$4525` puts all 7 sprites at the same screen corner ($AA, $E4 / $8C), -sets their ptr to `$DA` ($3680), color to 7, then triggers song 8 -via `$CB02`. This is the **idle title state**. - -`$4955` is the reverse: gate-off voice 1+2, noise off, then load a -disk file selected by `$6F75,X` (X = `$716B`) via the `$9936` -fast-loader, set $5E9D=0, JMP $5EC4. **Exit-to-title cleanup**. - -CORRECTED 2026-07-20 (demo-subsystem trace): the `$6F75,X` table and -the `$9936` loader do NOT load music. `$6F75` holds a FILENAME DIGIT -patched into the reversed "S0:SPACETAXI 0_/N0S" string, and the four -"SPACETAXI 01..04" disk files are the **attract-demo recorded input -streams** (RLE `[joystick mask][tick count]` pairs; H/T/W/X screen -rotation, `$6F75` maps H=01, T=02, W=03, X=04; playback substitutes -the recorded mask for `$DC00` input via the `$48F2` hook). `$716B` -is the demo rotation index, not a music index. The music engine -proper ($CB00-$CFFF, per-voice streams, freq tables) is unrelated to -these files. - -## Lives + death state - -| Var | Meaning | -| --------- | ------------------------------------------------ | -| `$715C` | **Fare-slot count** -- INC at `$6559` on each new pad-hover setup, DEC at `$69CA`/`$6B7B`/`$6B9E` on death-finalize, zeroed at `$634A` on scene load. Earlier draft labeled this "lives counter" based only on the DEC sites; the INC and scene-load-zero behavior rule that interpretation out. Probably tracks "fares currently in play". The port's own lives mechanic does not derive from this address. | -| `$7163` | 10-stage death-anim state machine ($65C1 dispatch) | -| `$7164` | 4-phase collision dispatch (0/1/2/3) | -| `$7167` | Death dispatch in-progress gate | -| `$7152,X` | Per-sprite slot table; cleared on death | -| `$7D8E` | Index into `$7152` for the active death subject | -| `$71CE` | Successful-fare counter; compared with `$7213` | - -## Death-stage dispatcher (`$65C1`) - -Called every frame from post-tick. 10-way jump on `$7163`: - -| Stage | Target | Purpose | -| ----- | ------- | ---------------------------------------- | -| 0 | $660B | idle RNG-gated stage advance ticker | -| 1 | $665F | stage-1 handler | -| 2 | $66B7 | stage-2 handler | -| 3 | $66DA | stage-3 handler | -| 4 | $66DD | stage-4 handler | -| 5 | $6739 | "takeoff" -- pad hover with input | -| 6 | $6742 | stage-6 handler | -| 7 | $67A3 | stage-7 handler | -| 8 | $67A6 | stage-8 handler | -| 9 | $67C6 | stage-9 handler | - -Stage 0 idle ticker: `$71CC` decrements every frame; on hitting 0, -runs `$6537` (probability gate), reads pad-table entry at -`$7D0E,X*8` (the X-of-pad table) into hover positions -`$7186/$7176/$717E`, sets `$718F=1` (hover active flag), -`$7163=1` (advance to stage 1), `$715D = $715E`. - -So death-stage 0 is the **between-level pause** ticker that picks -a random next pad and transitions into the takeoff sequence. - -## Per-tick handlers detailed - -### `$67E3` -- postTickStateGate - -State-dependent setup that runs every frame. Reads `$7150` (active -pad). If 0 (airborne) RTS. If non-zero, dispatch by `$7163`: - -| Stage | Action | -| ----- | ----------------------------------------------------- | -| 0,1,3,6,7 | RTS (idle) | -| 2 | INC `$7163`, save current col/frac to `$7160/$715F`, fall to $6808 | -| 5 | If $716F=0 (bob timer done): INC `$7163`, `$718F=1`, | -| | save col/row, `$7198=$CB`, $715D=$715E, build | -| | takeoff art pointer from `$7D0F,X*8`/`$7D10,X*8` | -| $6808| `$7167=1`, JSR `$6866` (pad-passenger-on colors) | - -The "$6866 vs $6877" pair is the **pad indicator color flash**: -$6866 writes `$0B` to `$DBB4/$DBB5` (left edge) and `$02` to -`$DBDC/$DBDD` (right edge); $6877 swaps them. Toggled by stages. - -### `$619B` -- taxiSpriteCelSelect - -``` -LDA $7197; AND #$01 -> $61 ; isolate frame-parity bit -LDA $716A; AND #$04 -> Z? ; right input? -BNE -> A=$DC ; flame-on base ptr -LDA $716A; AND #$08 ; left input? -BEQ RTS ; no input -> keep current ptr -A=$C0 ; (some other base ptr) -ORA $61 (parity bit) ; mix in the flicker bit -STA $7197 ; new sprite-0 ptr -``` - -So the cab's sprite ptr is selected as base ($C0 or $DC) OR'd with -the per-frame parity bit. Base $DC = "flame-on" cel (with engine -exhaust visible), base $C0 = "flame-off" cel. The +1 flicker is -the EOR #$01 from phase-1 handler at $6A95, which alternates the -exact cel address. NOTE: the C-port doc said "2 cels"; that's -actually 4 cels organized as 2 bases x 2 flicker variants. - -### `$61BD` -- passengerEventDraw - -If `$716F != 0` (a passenger event just happened): peek `$71CD` -(event type) and dispatch: - -| `$71CD` | Event | Pointer source for $6C7F | -| ------- | ----------- | ---------------------------------- | -| 1 | (drop-off?) | $6A/$6B = $6C6C | -| 2 | (pickup?) | $6A/$6B = $6C7A | -| other | (takeoff?) | $6A/$6B = $6C7D | - -`$6C7F` is "render text/scoreboard": marshals shadow, flushes, -then draws indirect text. So this routine surfaces a HUD blurb -based on what just happened. - -### `$63DD` -- fireButtonEdge - -``` -LDA $7150; BNE RTS ; only when airborne -LDA $71BD; BNE -> store fire ; if previously fired, latch update -LDA $7169; AND #$10 ; FIRE bit -BEQ RTS ; not pressed -> done -STA $71BD ; new latch = $10 -EOR $7197 #$01 ; flicker cab sprite -$68/$69 = $6410 ; SFX pointer -JSR $42E9 ; play SFX -``` - -So fire-button is debounced; on the rising edge, plays a SFX from -`$6410` and flickers the cab sprite. This is likely the **bonus-fare -or quick-stop bell**. - -### `$6D6A` -- flameSpriteUpdate - -``` -LDA $716A; BEQ -> turn off ; no input held -> noise off -LDA #$81; STA $D412 ; voice 3 noise ON -CLC -LDA $7175; ADC #$FE -> $7177 ; flame col = taxi col - 2 -LDA $7185; ADC #$FF -> $7187 ; flame col frac with borrow -LDA $717D; STA $717F ; flame row = taxi row -LDA $716C; EOR #$01 -> $716C ; parity toggle -BEQ -> $6DAA ; on every other frame -LDA $716A; TAX -LDA $6DB0,X -> $7199 ; sprite-1 ptr = (per-direction cel) -$7190 = 1 ; flame visible -RTS - -$6DA5 (no input): -A=$80; STA $D412 ; voice 3 OFF -$7190 = 0 ; flame hidden -``` - -So while input is held, sprite 1 is repositioned to (taxi col - 2, -taxi row) and pointed at a per-direction cel from table `$6DB0`. -Plus voice 3 plays noise (jet sound). When input released, sprite 1 -disappears and noise gates off. **Flame is a separate sprite (not -just a color flash).** - -The `$6DB0` table is the **engine-flame cel pointer table** -(direction-indexed, 8 entries). - -### `$6E23` -- padPaymentAnim - -Per-frame check of pad payment HUD: - -``` -LDA $7150 -CMP $7D5A ; on last/sentinel pad? -BEQ $6EAA -CMP $7D09 ; on special pad? -BNE $6E36 -LDA $07B1 ; HUD char at $07B1 -CMP #$66 -BNE RTS ; only animate if char is currently $66 -$68/$69 = $07C2 (HUD area) -JSR $440B ; draw -> sets carry on overflow -BCC RTS -INC $71C8 ; tick mod-8 -AND #$07 -STA $71C8 -BNE -> check stage ; not at boundary -$68/$69 = $6DBB ; SFX ptr -JSR $42E9 (SFX) -JMP $6E77 -``` - -So $6E23 cycles the **fare-payment HUD chars** at $07C2 area every -8 frames, plays a "ka-ching" SFX from $6DBB on each boundary. - -### `$6DFF` -- padLandingBob - -Misaligned in the disassembly (the byte at $6DFE eats it), but the -real entry is at $6DFF: - -``` -LDA $716F -BEQ RTS ; timer idle -DEC $716F -LDA $716F -AND #$01 -BNE up ; odd value -> row up -INC $717D ; row down (even) -LDA $716F -BEQ end-of-bob -RTS - -end-of-bob: -$7167 = 0 ; clear death-in-progress -JSR $6877 (pad color alt) -RTS - -up: -DEC $717D ; row up -RTS -``` - -So `$716F` is the **pad landing bob timer**. While it counts down, -the row alternates +1/-1 every other tick. On reaching 0, clears -the death-in-progress gate and swaps pad colors. - -### `$6EF0` -- padLightingGate - -Only acts during `$7163` stages 3, 7, or 8. If `$7150 != 0` (on -a pad): - -``` -LDA $71CA; AND #$02 -STA $7167 ; persistent "current bg-coll" flag -BEQ $6F14 -> $6877 (pad color "off") -JSR $6866 -> pad color "on" -``` - -So when a passenger sprite touches the cab and the cab is on a pad, -the pad lights up. This is the **passenger boarding indicator**. - -### `$6BE7` -- levelEndCheck - -``` -LDA $028D (keyboard scancode) -CMP #$04 -BNE $6BF3 -LDA $5569 (paused flag) -BNE $6BFB (force end) -$6BF3: -LDA $7D65 (Y position hi byte) -CMP #$1B -BCC $6BFB ; cab is at/above spawn row -> end level -RTS -$6BFB: -PLA PLA ; abandon return chain -$D412 = $80 ; voice 3 OFF -$D404 = $D40B = 0 ; voice 1+2 control off -$7216 = $7217 = 0 ; envelope timers cleared -JSR $704E -LDA $721C -BNE -> JMP $6B52 (level transition) -JMP $5A11 -``` - -`$028D` is the C64 keyboard scancode buffer. Bit pattern $04 might -be the F1 key or Q. So this is a **manual abort path** plus a -**successful-level-finish check** (cab reaches the top of the -screen, row < $1B = 27). - -### `$70B1` -- passengerArrTick - -``` -LDX $7214 (active fare idx) -LDA $7223,X; BNE RTS ; per-passenger arrival timer not zero -LDA $07C3 (HUD char); CMP #$6D; BCC RTS -LDA $71CF,X; ADC #$01 ; advance per-passenger counter -STA $71CF,X -STA $7223,X -TAX -LDA #$C8 -> $0798,X ; mark slot complete -LDA #$04; JSR $44CF ; trigger anim -RTS -``` - -So per-passenger HUD slot animation; when the counter reaches a -threshold, marks the slot complete and triggers a sprite anim tick. - -### `$4FCB` -- runStopWatcher - -Per-frame keyboard check: - -``` -LDA $028D -AND #$01 ; RUN/STOP bit -BEQ $4FED ; not held -$D412 = $80 ; voice 3 off -$7222 = $80 ; mark thrust ended -$D40B = $D404 = 0 ; voice 1+2 ctrl off -LDA $CB81 ; voice 1 track ptr -BEQ -> JMP $4FCB ; spin until music done -JSR $44E1 ; wait IRQ tick -JMP $4FCB - -$4FED (no RUN/STOP): -LDA $5E9D; BEQ -> further check -RTS - -(check $721C and $DC00 for menu interactions) -$4FF8: JSR $FFE1 (kernal RUN/STOP check) -- if pressed, transition -``` - -So $4FCB is the **abort to title / pause** handler. RUN/STOP key -kills all audio, then either loops waiting for release or transitions. - -### `$6419` -- fuelBarHud - -``` -LDA #$0B; STA $DBA2,$DBA3,$DBCA,$DBCB ; pad color cells -INC $71BE; AND #$07; STA $71BE ; mod-8 ticker -CMP #$05; BPL RTS ; only first 5 of 8 frames - -LDA $714F (Y-vel hi); BMI down ; sign-test -BEQ check_X ; Y vel = 0 -A=$02, X=$28; JMP write -check_X: -LDA $714E -BMI down -A=$03, X=$00; JMP write -down: -A=$07, X=$00 -write: STA $DBA2,X; STA $DBA3,X; RTS -``` - -So the fuel bar's color depends on cab Y-velocity sign (rising vs -descending vs hovering). The `$DBA2-$DBCB` cells form the in-flight -indicator strip. - -## IRQ handler (`$CF52`) -- music only, no animation - -The IRQ does **music ticks only**. Sprite multiplexing, screen-cell -animation, elevator updates, etc. happen in the main game loop, not -the IRQ. The IRQ structure: - -``` -$CF52: save A/X/Y via self-mod into the LDA/LDX/LDY immediates - at $CF9C-$CFA1 (cheaper than stack push) -$CF5B: BIT $CBFF -- pull bits 7 and 6 into N and V flags -$CF5E: BMI / BVS branching: - bit 6 set -> skip music (frame-done guard) - bit 7 set, V=0 -> $CC28 (full silence/reinit all voices) - bit 7 clear, V=0 -> $CC58 (soft gate-off) -$CF6D: save $FC/$FD into $CBD4/$CBD5 -$CF77: loop X = 0, 7, 14 (the three SID voices) -$CF7D: JSR $CC6C (per-voice tracker tick) -$CF80: $CBD6 += 7; if < 21, loop -$CF8A: restore $FC/$FD -$CF94: OR #$40 into $CBFF (mark frame done) -$CF9C: LDA/LDX/LDY (self-modified) restore regs -$CFA2: JMP $EA31 (KERNAL standard IRQ exit) -``` - -### Music tracker per-voice tick (`$CC6C`) - -The 3-voice loop driver. Each call gets `X = 0` (voice 1), `7` -(voice 2), or `14` (voice 3) so the same code services all three -SID voices. Per-voice state lives in `$CB80+X` slots: - -| Var | Role | -| ------------ | -------------------------------------------- | -| `$CB80,X` | track pointer lo | -| `$CB81,X` | track pointer hi (0 = inactive) | -| `$CB83,X` | per-voice "active note" flag (0 = on) | -| `$CB84,X` | per-voice timer (compared with `$A2` tempo) | -| `$CB86,X` | per-voice transpose offset | -| `$CB9B,X` | per-voice default note duration | -| `$CBAB,X` | per-voice filter mask | -| `$CBBF-C2,X` | per-voice ADSR + pulse-width state | -| `$CBC3,X` | per-voice waveform / control byte mask | -| `$CBC4,X` | per-voice filter routing flag | -| `$A2` | global tempo (cycles per tracker step) | - -Each tick: -- if `$CB84,X != $A2`, RTS (not time yet for this voice) -- else if `$CB83,X != 0`: advance timer (`$CB84,X += $A2`), clear - active flag, gate-off the SID voice (`$CBC3,X & $FE -> $D404,X`) -- else: fall to `$CC8D` for **note-start** (now traced below) - -### Note start (`$CC8D`) -- full sequence - -``` -LDA $CB81,X ; track ptr hi -BEQ gate_off ; track inactive -STA $FD; LDA $CB80,X; STA $FC ; zp $FC/$FD = ptr -LDY = 0 -LDA ($FC),Y ; read next byte -BEQ gate_off ; $00 = end of track -CMP #$80 -BNE check_note ; $80 = end of song -> clear ptr + gate off -gate_off: ; $D404,X &= $FE -JMP advance_ptr - -check_note: -AND #$7F ; mask high bit (effect flag) -CMP #$61 ; note count = 97 -BCC normal_note -JMP $CD56 ; effect command (traced -- see "Effect commands" below) - -normal_note: -CLC; ADC $CB86,X ; transpose -CMP #$61; BCS gate_off ; clamp -- past end of freq table -TAY -LDA $CEF1,Y -> $D400,X ; voice freq LO -LDA $CE90,Y -> $D401,X ; voice freq HI -LDA $CBBF,X -> $D405,X ; attack/decay -LDA $CBC0,X -> $D406,X ; sustain/release -LDA $CBC1,X -> $D403,X ; pulse-width HI ($D403 is the PW hi register) -LDA $CBC2,X -> $D402,X ; pulse-width LO -LDA $CBC4,X ; filter flag -PHP -LDA $D417; EOR #$FF; ORA $CBAB,X; EOR #$FF -PLP; BEQ skip_filter -ORA $CBAB,X -skip_filter: -STA $D417 ; filter routing -LDA $CBC3,X; ORA #$01 -> $D404,X ; waveform + gate-on - -advance_ptr: -LDY = 0 -LDA ($FC),Y -BMI explicit_duration -LDA $CB9B,X; SEC; ADC $A2; STA $CB84,X ; default duration -A=0; STA $CB83,X ; active flag = 0 -BEQ continue -``` - -Frequency tables (CORRECTED 2026-07-20 against the trace above: -the earlier version of this line had lo/hi swapped): `$CEF1-CF51` -holds the 97 freq LO bytes (written to `$D400,X`), `$CE90-CEF0` -holds the 97 freq HI bytes (written to `$D401,X`). Note codes are -1..97 indexing both tables. Verified against raw.bin bytes: -`$CE90: 00 01 01 01 ...` (small = hi bytes of low notes), -`$CEF1: 00 0C 1C 2D 3F 51 ...` (lo bytes). - -CORRECTED 2026-07-20 (full byte-level trace of `$CD0E-$CD55`): the -stream is **one byte per note event**, NOT (note, duration) pairs. -After the note plays, `$CD0E` re-reads the SAME byte at `($FC)` -- -the pointer has not advanced yet -- and `BMI $CD24` tests the NOTE -byte's own bit 7 (the bit masked off by `AND #$7F` before the table -lookup): - -- bit 7 CLEAR: default duration. `$CB84,X = $CB9B,X + $A2 + 1` - (next event = now + default duration + 1 jiffy), `$CB83,X = 0`. -- bit 7 SET: staccato/gap mode using `$CBC5,X`: if `$CBC5,X` is - negative it is first added to `$CB9B,X` (clamped at 0). The result - is stored BOTH into `$CB84,X` (+`$A2`+1, the note-on time) AND via - self-modifying code into the `SBC` immediate at `$CD3F`, and - `$CB83,X = $CB9B,X - 2 - (that value)`... i.e. `$CB83,X` receives - the GAP length. `$CB83,X` is therefore a gap counter, not a plain - flag: when the tick at `$CC74` finds it nonzero, it gates the voice - OFF and schedules the next (note-start) event at `now + $CB83,X` - (`CLC ADC $A2`). "0 = on" in the state table above really means - "no pending gap". - -Then `$CD43` advances the track pointer by ONE byte (the note byte) -and refreshes zp `$FC/$FD`. Flat stream, terminated by `$00` (end of -track: gate off, pointer stays) or `$80` (end of song: pointer set to -`$FFFF`, gate off). - -### Effect commands (`$CD56`) -- traced 2026-07-20, previously "out of scope" - -A stream byte whose masked value `(byte & $7F) >= $61` is an effect. -`$CD56` re-reads the raw byte and transforms it with an -ASL/PHP/ASL/ASL/PLP/ROR/LSR/LSR dance that computes -`cmd = (bit7 << 5) | (byte & $1F)` -- so `$61-$7F` map to cmd -`$01-$1F` and `$E1-$FF` map to cmd `$21-$3F`. Dispatch (compare -chain, byte-exact; interpretive names are inference from the code -shapes, marked *): - -| cmd range | bytes | action | -| --------- | ----- | ------ | -| `> $30` | 3 | skip two operand bytes (unused command class in this build) | -| `= $30` | 1+14 | *instrument load: copy the next 14 stream bytes into `$CBBF+`(voice window) -- ADSR, pulse width, waveform mask, filter state (`$CD71`, self-modified X operands) | -| `$20-$2F` | 1 | *set repeat counter: `$CBAA+Y = cmd & $0F` where `Y = $CB85,X + $CBD6` (`$CE84` slot indexer) | -| `$18` | 2 | set per-voice transpose `$CB86,X` from the next byte (`$CDB2`) | -| `$10-$17` (not `$18`) | 2 | set per-voice default duration `$CB9B,X` from the next byte (`$CDC3`) | -| `$0C-$0F` | 1 | decrement repeat counter (`SBC 1, AND $0F`) (`$CDD4`) | -| `$08-$0B` | 1 | *loop-back: condition via `$CE68` (cmd&3: 2 = if counter zero, 3 = if counter nonzero, else always); taken -> pop `$CB95/96,Y` saved pointer (`DEC $CB85,X` twice) and continue there; a negative slot index instead resets `$CB85,X` and ends the song (`$CDE8`) | -| `$04-$07` | 1 or 3 | *loop-start: condition false -> skip 2 operand bytes; true -> push `ptr+3` onto the `$CB95/96` pointer stack (`INC $CB85,X` twice) then fall into the jump below (`$CE17`) | -| `$01-$03` | 3 | *relative jump: add the 16-bit LE operand (bytes at ptr+1/+2) to the track pointer (`$CE4A`) | - -After most commands control returns to `$CC99` to process the next -stream byte in the same tick (so command chains execute -"immediately", consuming no time). - -### Song loading -- `$4955` is a patch stub, the real loader is `$9936` - -`$4955` only silences the SID (`$D404 = 0`, `$D40B = 0`, -`$D412 = $80`), then: `LDX $716B` (music index), `LDA $6F75,X`, -`STA $99A7`, `JSR $9936`. **`$6F75` is not a pointer table -- it -holds a FILENAME CHARACTER** (observed bytes: index 7 -> `$31` '1', -19 -> `$32` '2', 22 -> `$33` '3', 23 -> `$34` '4'), and `$99A7` is a -character slot inside a REVERSED filename string at `$99A3+` -("S0N/_0 IXATECAPS:0S" = "S0:SPACETAXI 0_/N0S" reversed; a second -string at `$9990` is "SPACETAXI HI"). `$9936` is a self-modifying -IEC fast-loader (raw `$FFA8` CIOUT / `$FFAE` UNLSN serial traffic) -that pulls the song file off the disk. **The music DATA is not in -the resident program at all: each song ships as its own disk file** --- the `SPACETAXI 01..04` / `SPACETAXI HI` PRGs (present in -`stuff/spacetaxi/extract/`). Whatever song was resident when the -raw.bin dump was taken is what sits in its load area there. - -Timing base: `$A2` in the tick compare is the KERNAL jiffy clock low -byte, so all durations are in PAL jiffies (50 Hz) -- `$CB84,X` is a -"next event at this jiffy" target, not a countdown. - -### Two reset variants - -| Routine | When called | Effect | -| ------- | -------------------------------------- | ------ | -| `$CC28` | IRQ when `$CBFF` bit 7 SET, bit 6 clear | full SID reset: write $08 (test bit) then 0 to each voice ctrl, prime per-voice timer to `$A2+1`, clear active flag | -| `$CC58` | IRQ when `$CBFF` bit 7 CLEAR, bit 6 clear | soft release: `STA $D404,X` with bit 0 cleared on each voice (release current note's gate, leave timer/state alone) | - -### What the IRQ does NOT do - -- Sprite X/Y register writes (those land in `$4293`, called - from main loop -- see "Sprite system" below) -- Screen RAM animation (elevators, blinking lights, etc.) -- Game logic (taxi physics, pad detection, scoring) -- Color RAM cycling for the title (`$4861` is called from - the main loop, not the IRQ) - -All of those run between IRQ ticks at main-loop speed. The IRQ is -strictly the **music heartbeat**. - -## Sound (SID) - -**Gameplay is silent except for SFX.** Music tracks only load via -`JSR $CB02` (musicInit), and that's called from exactly 5 sites, -none of them in gameplay code paths: - -| Site | Song | Role | -| ------- | ---- | ----------------------------------------------- | -| `$459C` | 8 | title sprite setup (start title music) | -| `$46BC` | 8 | title demo loop restart | -| `$477C` | 25 | title init via `titleEnterContinue` ($4741) | -| `$4C29` | 7 | score-screen draw (between-level jingle) | -| `$4EB6` | 6 | second score-screen variant | - -The 8 gameplay-loop preludes at `$5F18` (`$61FB`, `$4248`, `$6946`, -`$6F18`, `$6888`, `$6906`, and the two state-vec trampolines) do -not load any songs. The IRQ music engine (`$CF52` → `$CC6C` per -voice) still ticks every frame during gameplay, but all three -voice track pointers (`$CB80/$CB81,X`) are zero so the engine -walks-and-does-nothing. Audio during gameplay is purely event- -driven SFX (thrust freq sweep, crash noise, etc). - -There is no per-level music. The port's `StLevelT.musicId` field -is dormant scaffolding; loading songs at level transitions is -non-authentic and was removed. - -Mapping of voice usage confirmed by site-by-site survey: - -| Voice | Register | Role | -| ------------- | ------------- | --------------------------- | -| 1 (triangle) | `$D400-$D406` | jingle music (non-gameplay) + thrust freq sweep + crash scream | -| 2 | `$D407-$D40D` | jingle melodic support voice (non-gameplay) | -| 3 (noise) | `$D40E-$D412` | crash burst + jet-engine continuous noise | - -Patterns observed: - -- **Thrust hum**: phase-1 handler decrements `$721B`, writes the - shifted value to `$D400/$D401`. Continuous while `$721B > 0`. -- **Jet noise**: `$6D6A` writes `$81` (noise + gate ON) to `$D412` - while direction input is held; `$80` (gate off) when released. -- **Crash noise**: `LDA #$81 STA $D412` (noise waveform + gate on) - followed by `LDA #$80 STA $D412` (gate off) after a short - duration -- this is the noise-burst envelope. -- **Note release**: per-voice envelope tick at `$4320` clears the - gate bit of `$D404` / `$D40B` when timers `$7216` / `$7217` hit - zero. The timer is loaded from `$7218` / `$7219` (the SFX program - byte that holds the wave|gate mask). -- **SFX load**: `$42E9` reads a **9-byte program** from `($68/$69)` - (VERIFIED via emulator): - - ``` - byte 0: SID freq lo -> $D400+vi*7 - byte 1: SID freq hi -> $D401+vi*7 - byte 2: SID pulse lo -> $D402+vi*7 - byte 3: SID pulse hi -> $D403+vi*7 - byte 4: SID ctrl byte -> $D404+vi*7 AND stored to $7218+vi - byte 5: SID AD -> $D405+vi*7 - byte 6: SID SR -> $D406+vi*7 - byte 7: release-timer -> $7216+vi - byte 8: voice index (0,1,2 = SID voice 1,2,3) -- read FIRST - ``` - - Routine reads byte 8 first to determine voice, gates off the - selected voice ($D404+X = 0), then writes the 7 SID register - values. Earlier MECHANICS draft said "voice index then 7 bytes" - (wrong order, wrong byte count) -- corrected after trace. - -## Sprite system (table-driven, IRQ flushed) - -Game code never writes the VIC sprite registers directly. It only -updates **shadow tables** in main memory; a per-frame copy routine -at `$4293` (called via `$5F97`) flushes them to hardware: - -| Shadow | Hardware | Width | Purpose | -| -------------- | ---------------- | ------- | ---------------------- | -| `$71A7-$71B6` | `$D000-$D00F` | 16 byt. | Sprite X/Y positions | -| `$7197-$719E` | `$07F8-$07FF` | 8 byt. | Sprite-data pointers | -| `$719F-$71A6` | `$D027-$D02E` | 8 byt. | Sprite colors | -| `$718D` | `$D010` | 1 byt. | Sprite X MSB mask | -| `$7196` | `$D015` | 1 byt. | Sprite enable mask | - -### Position storage and marshaling - -Per-sprite positions are kept in **two parallel 8-byte tables**: - -- `$7175-$717C`: per-sprite pixel column (high byte of position) -- `$717D-$7184`: per-sprite pixel row (Y, simple byte) - -`$4253` (called every frame from `$5F91`) is the **marshal**: - -1. Packs the boolean array `$718E-$7195` (which holds per-sprite - "feet hidden" flags) into byte at `$7196` via 8x ROL through - carry. Same for `$7185-$718C` packed into the X-MSB shadow. -2. Bulk copies the two pos tables into the interleaved hardware - shadow `$71A7-$71B6` (X,Y,X,Y,... order matches the hardware - register layout). - -The flush at `$4293` is then a single tight loop: `LDA shadow,X; -STA reg,X; DEX; BPL`. So the IRQ does no sprite work directly -- -the per-frame routines `$4253` then `$4293` are called from the -main loop, bracketed by `$404B` VBL waits to avoid tearing. - -## Taxi animation (confirmed) - -The taxi has **two base cels** with a per-frame flicker variant, -giving 4 distinct cel ptr values: - -- Base $C0 / $C1: cab (state A) -- Base $DC / $DD: cab (state B, "flame-on") - -The cel-select logic in `$619B`: - -``` -ptr_base = (input has bit-2) ? $DC : ($C0 if bit-3 else ) -parity = $7197 & 0x01 ; flickers each frame in $6A95 -ptr = ptr_base | parity -``` - -Each phase-handler tick (`$6A95`), the taxi pointer shadow `$7197` -gets `EOR #$01`, alternating the parity bit. JoeyLib port has -`ST_TAXI_CEL_COUNT = 2`; should probably be 4 to fully replicate. - -The 9 writers of `$7197` (sprite-0 ptr shadow) reveal state- -dependent base cels: - -| Writer site | Value | Likely state | -| ----------- | ------------ | ------------------------- | -| `$4A56`, `$5B82`, `$6894` | `#$C0` -> `$3000` | Menu / title cab | -| `$5CA1` | `#$E2` -> `$3880` | Different state cel | -| `$6A2D` | `#$CC` -> `$3300` | Phase-2 (death anim start) | -| `$6A95` | `EOR #$01` | In-flight flicker | -| `$6B33` | `INC` | Phase-2 cel walk | -| `$619B`, `$6401`, `$65B2` | computed (in handler) | Various dispatch | - -## Engine flame -- sprite 2 (VERIFIED CORRECTION) - -CORRECTION: the flame is **sprite 2** (not sprite 1 as I had earlier). -Verified by checking `$6D6A`: it writes to `$7177` (sprite-2 X) and -`$7199` (sprite-2 ptr shadow). Sprite 1 (`$7176` X, `$7198` ptr) is -the **active passenger sprite** set by `$660B` / `$6650` etc. - -Sprite role assignment in Space Taxi: -- sprite 0 (taxi): `$7175` X, `$7197` ptr -- sprite 1 (active passenger): `$7176` X, `$7198` ptr -- sprite 2 (flame): `$7177` X, `$7199` ptr -- sprites 3-7: additional passengers / level decoration - -`$6D6A` (called every frame from the gated dispatch) positions -sprite 2 two pixels left of the taxi and one row above (in -Y-frac terms), pointed at a direction-indexed cel from table -`$6DB0,X` (X = $716A direction mask). Sprite 2 is hidden by -zeroing `$7190` when no input is held. - -So the engine flame is: -- A separate sprite (sprite 1) -- Positioned (taxi_x - 2, taxi_y) when active -- Cel ptr depends on direction held -- Hidden when no input - -JoeyLib port currently overlays flame as a tile/sprite under the -cab; needs to be reworked to match the C64's per-frame -direction-cycling 8-entry cel table at $6DB0. - -## Title screen layout - -The title dump's screen RAM (taken with the title displayed) -shows borders at rows 0, 12, 24 (40 non-space chars each) and -content in rows 1-11 (logo + credits) and rows 13-23 (instruction -text / animated demo area). Our extractor preserves this layout -verbatim, so the port renders the same character placement once -the tile bank for `title.txt` is loaded. - -### Logo color cycle (`$4861-$489B`) - -Routine at `$4861` advances a counter at `$48A4` mod 8, looks up -a color from the 8-entry table at `$489C-$48A3`, and paints that -color into 11 rows x 37 columns of color RAM -- the logo area. - -The 8-color cycle table (C64 codes): - -| Slot | Value | Color | -| ---- | ----- | ----------- | -| 0 | `$02` | red | -| 1 | `$08` | orange | -| 2 | `$07` | yellow | -| 3 | `$05` | green | -| 4 | `$06` | blue | -| 5 | `$0E` | light blue | -| 6 | `$03` | cyan | -| 7 | `$04` | purple | - -Inner loop at `$4872-$4896` writes the color to `$D828+X`, -`$D850+X`, `$D878+X`, ..., `$D9B8+X` -- 11 row offsets, each 40 -bytes apart, covering screen rows 1-10 (logo area). X goes 1..37. -Then a follow-up `JSR $49F8` writes A to `$719F+X` for X=7..3 -- -**bulk-sets sprites 2..7 color to the same logo color** so the -title sprites pulse in sync with the logo. - -## Bank switching for under-Kernal data (`$6283`) - -``` -PHP / SEI / TAX -LDA $01 ; save current memory config -STA $6293 ; stash via self-mod (operand of LDA #$37 below) -LDA #$38 ; bit0=0 (BASIC out), bit1=0 (Kernal out), bit2=0 (Char ROM @ D000) -STA $01 ; -> all-RAM mode -JSR $9656 ; data-load routine, can now read $A000-FFFF as RAM -LDA #$37 ; restore (self-mod operand was the saved $01) -STA $01 -PLP / RTS -``` - -So `$6283` is the **bank-switch wrapper** for accessing per-level -data stored in RAM beneath the BASIC/Kernal/I/O ROMs. NOT a -decompressor (earlier MECHANICS draft was wrong). The actual -data-load happens in `$9656`; everything in $8000-$FFFF is open -RAM during the call. - -This is called from `$5B34` (with A = song index, level-end death -tune setup), `$621F` (round-end reset), and `$5F` segment of init. - -## Mapping to JoeyLib port - -| C64 var/route | JoeyLib equivalent | -| -------------------- | ----------------------------------------------- | -| `$DC00` joystick | `jlJoystickX/Y` + `jlJoyDown` polling | -| `$7169` input mask | `applyInput()` produces `thrustDx/Dy/thrusting` | -| `$7148-4B` accel | `StTaxiT.ax, .ay` (currently inlined locals) | -| `$714C-4F` velocity | `StTaxiT.vx, .vy` | -| `$7D8F-92` templates | hard-coded constants in `stEngine.c` | -| `$7D61-63` position | `StTaxiT.x, .y` (32-bit subpixel) | -| `$07A6` state sentinel | `StGameT.state` enum | -| `$7164` collision phase | `StTaxiT.state` (airborne/landing/dying/etc) | -| `$7163` death stage | derived from above + frame counter | -| `$7150` active pad | `StTaxiT.onPad` | -| `$7D75` hit trampoline | explicit pickup check in `stPassenger.c` | -| `$6AED` edge bounce | `clampToField()` -- needs to bounce, not stop | -| `$4253` shadow marshal | implicit in `stRender.c` per-sprite draws | -| `$4293` shadow flush | jlStagePresent | -| `$404B` VBL wait | jlStagePresent (sync-on-present) | -| `$42E9` SFX load | `stAudioSfx*()` per-event functions | -| `$4320` SFX envelope | jlAudioFrameTick / per-platform mixer | -| `$6D6A` flame sprite | needs sprite-1 overlay, direction-indexed cel | - -## Drop-off / death state machine (`$65C1` dispatch, all stages traced) - -The dispatcher at `$65C1` is a 10-way `JMP table` on `$7163`. Despite -"death" in earlier notes, this is actually the **fare-success + -death + game-over** state machine. Same machine handles both -outcomes; the branch on `$7164` (collision phase) inside stage 1 -decides. - -| Stage | Addr | Role | -| ----- | ------ | --------------------------------------------------- | -| 0 | $660B | Idle RNG ticker between transitions | -| 1 | $665F | Branch: success-text OR death-finalize | -| 2 | $66B7 | Sprite-1 cel cycling animation | -| 3 | $66DA | (entry via indirect jump; see "Self-mod" below) | -| 4 | $66DD | Cel ptr walk up to $CC | -| 5 | $6739 | Pad-lighting alt color, clear death-in-progress | -| 6 | $6742 | Score update: BCD add to HUD | -| 7 | $67A3 | -> JMP $6CE8 (HUD finalization) | -| 8 | $67A6 | Sprite-1 ptr advance to $CC | -| 9 | $67C6 | Final cleanup, draw game-over screen text | - -### Stage 0 (`$660B`) -- idle RNG ticker - -Each frame: -- LDA #$64 (100); JSR `$4080` (random 0..A-1) -- if result >= 3, RTS (97% chance per frame to do nothing) -- DEC `$71CC` (100-tick countdown); if not zero, RTS -- $7164 == 0 (gate) -- $71CC := $64 (reload) -- JSR `$6537` -- read pad-spawn table, prep a target pad -- Save pad data into `$7186/$7176/$717E` (hover position) -- `$718F = $7163 = 1` -- advance to stage 1 -- Reload `$715D = $715E` - -So stage 0 is a **probabilistic delay**: roughly every (100*100)/3 -frames (~83 seconds at 60Hz) it ticks a passenger arrival, picks a -pad, and transitions to stage 1. - -### Stage 1 (`$665F`) -- success-text OR death-finalize - -``` -DEC $715D, gate -$715D = $715E (reload) -DEC $7198 (sprite-1 ptr) -- animates down toward $C7 -LDA $7198; CMP #$C7; BEQ continue, else RTS -LDA $7164 ; collision phase -BEQ success ; phase 0 -> success path -death: - $7163 = 8 ; jump to stage 8 - $7198 = $C7 ; reset cel - $7152,$7D8E = 0 ; blank dying slot - $7D8E = 0 ; clear active idx - DEC $715C (lives--) ; lose a life - RTS -success: - INC $7163 (-> stage 2) - Draw text at $6C45 via $41C2 (probably "FARE!" or "PASSENGER!") - JSR $6C7F (scoreboard refresh) - JSR $43D1 (HUD redraw) - RTS -``` - -So the cab gets a chance to recover during stage 1; if collision -phase is non-zero (we crashed), death finalizes; if zero, success -text draws and stages 2-7 run the scoring animation. - -### Stage 2 (`$66B7`) -- sprite-1 cel cycling (sparkle) - -``` -DEC $715D, gate -$715D = $715E -INC $716E mod 4 -> X -$7198 = $66D6,X ; lookup from 4-entry table -RTS -``` - -Table at `$66D6`: `C6 C7 D9 C7` -- cycles sprite-1 ptr through -4 cels for a sparkle/flash effect. - -### Stage 3 (`$66DA`) -- self-modifying indirect - -Stage 3 entry is `INX; JMP ($5DCE)`. The indirect vector at `$5DCE` -is loaded per-stage to select a sub-handler. This is a runtime -state-machine override -- whatever the level's "success" sequence -needs gets pointed at via `$5DCE`. Both writers of $5DCE and the -exact sub-handlers depend on per-level state we haven't fully -probed. In level 1, $5DCE points back into the success-text drawer. - -### Stage 4 (`$66DD`) -- "FARE COLLECTED" art - -``` -DEC $715D, gate -$715D = $715E -INC $7198 ; walk sprite-1 ptr up -CMP #$CC; BEQ continue, else RTS -$718F = 0 ; clear pad hover flag -INC $7163 (-> stage 5) -$716D = $7150 ; save active pad -JSR $6537 ; pad-table read -LDA $7D8E; CMP #$0B -BNE -> draw the next-fare art -... draw text at $6C1E via $41C2, A=1 -... patch HUD chars at $07D2/$6C73/$6C7B with ($7D8E + $30) -JSR $6C7F (scoreboard) -RTS -``` - -So stage 4 walks the cel pointer up to $CC, then triggers the -"FARE!" banner draw + scoreboard update. - -### Stage 5 (`$6739`) -- pad color alt - -Trivial: `$7167 = 0`; JSR `$6877` (alt color); RTS. - -### Stage 6 (`$6742`) -- score update via BCD add - -``` -DEC $715D, gate -$715D = $715E -DEC $7198 ; back down to $C7 -CMP #$C7; BEQ continue, else RTS -INC $7163 (-> stage 7) -draw text at $6C5F via $41C2 with A=1 ; "ETA" or similar -JSR $6C7F (scoreboard) -$68/$69 = $43B9; JSR $4354 ; BCD-add fare value to $07C2,Y -$68/$69 = $07E0; JSR $4354 ; second BCD add to a different field -JSR $43A5 (hudInit) -LDA $715C; BEQ -> one more $4354 -RTS -``` - -So stage 6 performs **two BCD-add operations**: one onto the -in-cab display at `$07C2`, one at the global score field `$07E0`. -A third add fires when lives reach zero (the "final tally"). - -### Stage 7-9 -- HUD finalization + game-over - -Stage 7 is just `JMP $6CE8` -- a HUD-flush routine. Stage 8 walks -the sprite ptr up to $CC again (a second cel-walk pass). Stage 9 -zeroes `$7167` and `$7163` (clearing the state machine), runs -`$6877` (pad color alt), and draws the **game-over text** at -`$6C38` via `$41C2` with A=1. - -So the full success animation flows: 0 -> 1 -> 2 -> 3 -> 4 -> 5 -> -6 -> 7 -> 8 -> 9 -> 0. The death path short-circuits from stage 1 -straight to stage 8 to skip the scoring animation. - -## Score storage and BCD math (`$4354`) - -The 4-byte BCD score is rendered in screen-RAM at the HUD -positions. The `$4354` BCD-add helper: - -``` -$4354: Y = 6 ; $721A = 0 (carry scratch) -$4357: LDA ($68),Y ; source: fare value - JSR $4345 ; validate ASCII digit -> A (0..9 or special) - STA $6A ; per-digit value - LDA $07C2,Y ; current HUD char - JSR $4345 ; validate - CLC; ADC $6A; ADC $721A ; digit + source + carry - PHA; $721A = 0 - PLA; CMP #$0A; BMI no_carry - INC $721A; SEC; SBC #$0A ; subtract 10, set carry -no_carry: - CLC; ADC #$6A ; offset back to char code ($6A = '0' code) - CMP #$6A; BNE store ; if result == $6A ('0'), use $74 (space) - LDA #$74 ; leading-zero suppression -store: STA $07C2,Y - DEY; BPL $438F ; next digit - ... -``` - -So the BCD score is stored as **ASCII chars at $07C2-$07C8** (7 -digits), with leading-zero suppression. The `$4345` helper validates -that source and dest chars are digits (or special $66 = leading -'0' tag, $6A = literal '0', $74 = space). The score lives directly -in screen RAM, not in a separate variable -- the chars displayed -ARE the score. - -The fare-value blob at `$43B9` (or wherever $68/$69 points) is a -7-byte ASCII string representing the points to award; one BCD add -into the HUD adds those points. Different fares can have different -values by pointing $68/$69 at different blobs. - -## Per-level data variations (cross-dump diff) - -Comparing the four available dumps: - -| Field | mem0000 | level1 | title | level2 | -| --------------------- | --------- | ---------- | ---------- | ---------- | -| `$7D8F` Y-accel | $1E (30) | $0E (14) | $19 (25) | $11 (17) | -| `$7D91` X-accel | $1E (30) | $0E (14) | $19 (25) | $11 (17) | -| `$7D93` Y-gravity | $03 ( 3) | $01 ( 1) | $06 ( 6) | $01 ( 1) | -| `$7D95` X-gravity | $00 ( 0) | $00 ( 0) | $00 ( 0) | $00 ( 0) | -| `$7D09` special pad | $03 | $01 | $05 | $03 | -| `$7D5A` (active pad?) | $03 | $01 | $05 | $03 | - -Observations: - -- **X gravity is always 0** -- no horizontal drift across all - observed dumps. Reasonable; no level has wind in the C64 game. -- **Y gravity varies**: level 1 has $01 (light), level 2 also $01, - the "title-demo" state ($19) has $06 (heavy). The mem0000 dump - ($03) is likely an intermediate state during initialization. -- **Accel tracks gravity** roughly: heavier gravity needs more - thrust to fight it, hence $19 / $1E accel paired with $06 / $03 - gravity. Level 1's gentle 14/1 is the most playable starting - point. -- `$7D09` and `$7D5A` correlate strongly -- both look like - pad-count + special-pad-index combinations. - -Per-pad data starting at `$7D0A` has 8-byte stride. Title screen's -5 active pads ($7D5A=5) means a 40-byte pad-table region used. -This is the per-level fixed data that the C64 game ships with; -in the JoeyLib port, the equivalent comes from -`DATA/levels/level??.dat` files (24 levels, A..X). - -### Per-pad byte layout (VERIFIED via reader-grep) - -Pad table entries at `$7D0A,X*8`. Eight bytes per pad. Updated -after tracing the readers in `$645C` (padDetect) and `$6620` -(newFareTransition): - -``` -byte 0,1: cab-landing X start (16-bit, hi/lo) -byte 2,3: cab-landing X end (16-bit, hi/lo) -byte 4: cab-landing row Y ($7D0E,X) -byte 5: passenger spawn X fractional byte ($7D0F,X -> $7186) -byte 6: passenger spawn X column ($7D10,X -> $7176) -byte 7: UNUSED -- no code reads $7D11,X. The $C2/$C4 values - observed in dumps are dead bytes. My earlier "ground vs - elevated pad" interpretation was speculation. -``` - -Verified via `grep -nE "[0-7] 7D"` to find direct addressing modes -hitting `$7D0B` through `$7D11`. Only bytes 1, 4, 5, 6 are read at -runtime. Bytes 0, 2, 3 are accessed via the X-indexed pair with -byte 1 in the 16-bit compare. Byte 7 has no readers anywhere. - -Level 2 has 3 pads at rows $44, $BC, $5C with style codes $C2/$C2/ -$C4. Title-screen demo has 5 pads -- the "press start" art lays -out a logical city with that many landing spots. - -For the JoeyLib port these decode to `StPadT.tileX/tileW/tileY` -- -the high-byte difference (01 vs 00) maps onto the C64's 320-pixel -horizontal range, so converting to JoeyLib's 40-tile-wide field -just divides by tile_pixels. - -## Frame budget summary - -Total per-frame work from main-loop trace: - -| Phase | Calls | -| -------- | ---------------------------------------------------- | -| Gated | 9 JSRs (physics + sprite-cel + pad-detect + audio) | -| Post-work | 16 JSRs (anim + flush + HUD + SFX envelope) | -| VBL syncs | 2x $404B between marshal and flush, second at end | -| IRQ | Music tick (~50us) | - -The two `$404B` VBL waits are the explicit frame sync points. The -order is: gameplay state → animation → marshal sprite shadow → -WAIT VBL → flush shadow to hardware → continue post-work → WAIT -VBL → top of loop. So screen updates happen during vblank, but -game logic runs full-tilt between vblanks. - -This is the "no fixed framerate" pattern: the music IRQ + raster -sync provide timing, but the main loop can run as many iterations -as it has time for between vblanks. On a stock C64 that's ~1 frame -per vblank; on accelerated hardware (or in JoeyLib's port) it -could be more. - -## Per-level header (`$7D00-$7D08`) -- VIC colors - -The bytes BEFORE the pad table are a VIC-II color block. The -scene-load routine at `$62F0` (called via `$621C` at round-end) -does: - -``` -JSR $4523 ; title-style sprite setup -JSR $42BC (memcpy) ; $7530 -> $0400, 1000 bytes (screen RAM) -JSR $42BC ; $7918 -> $D800, ~1000 bytes (color RAM) -LDX #$06 - LDA $7D00,X -> $D020,X ; per-level header -> VIC color regs - DEX, BPL -LDA $7D07 -> $719F ; sprite-0 color shadow -LDA $7D08 -> $71A0 ; sprite-1 color shadow -LDX #$09; STA $7152,X (zero), DEX, BPL ; clear sprite slot table -STA $715C (zero!) ; lives counter cleared at scene-load -``` - -So `$7D00-$7D06` maps onto VIC registers `$D020-$D026`: -- `$7D00`: border color -- `$7D01`: background color (BG #0) -- `$7D02-04`: BG colors 1, 2, 3 (multi-color mode) -- `$7D05-06`: sprite multicolor 0, 1 -- `$7D07-08`: sprite-0/1 individual colors - -The decompressed scene data lives at `$7530-$7929` (screen) and -`$7918+` (color), staged before being copied into screen RAM. -These are output buffers of the decompressor at `$9656`. - -## Stage 3 indirect vector (`$5DCE` -- runtime patched) - -The dispatcher at `$66DA` (stage 3) does `INX; JMP ($5DCE)`. In -all four available dumps, bytes at `$5DCE/$5DCF` are `00 00`, -which means JMP indirect to `$0000` (= BRK -- a crash). Since -gameplay clearly works, the vector must be **patched at level-init -time** by the bank-switched decompressor at `$9656`. - -Each level's compressed data (under the Kernal ROM) likely includes -a per-level "stage 3 hook" address that gets written into `$5DCE` -when the scene loads. Different levels can have different "success" -animations by pointing the vector at level-specific code. - -Without a VICE dump that captures the under-Kernal RAM (BASIC out, -Kernal out before saving), we can't see the decompressor or the -per-level patches. The `dumpAllLevels.py` script crashes at PC -`$9735` because the dumps we have show ROM contents at $A000+ -instead of the game's hidden code (opcode `$02` is invalid 6502 = -KIL, occurs when we try to run code from inside the BASIC ROM). - -**Workaround for the port**: in JoeyLib, stage 3 isn't reproduced -verbatim. Our `stEngine.c` death-anim is a simpler state machine -that doesn't need this indirection. - -## Music engine effect commands (`$CD56`) - -Reached from `$CC8D` when the track byte has its high bit set AND -the masked value is >= $61. The effect byte is bit-shuffled at -`$CD58-$CD61` (ASL ASL ASL with C save/restore via PHP/PLP, then -ROR LSR LSR) -- this rearranges the byte to extract the effect -index. The result is then compared against thresholds: - -| A value | Target | Effect | -| ------- | ------- | --------------------------------------------------- | -| < $04 | $CE45 | "track-call from arg ptr" (push current, jump) | -| < $08 | $CE13 | "track-return" (pop saved ptr from $CB95/96,Y) | -| < $0C | $CDE4 | decrement repeat counter at $CB85,X | -| < $10 | $CDD0 | decrement filter mask at $CBAA+Y | -| < $18 | $CDBF | set per-voice default duration ($CB9B,X) | -| < $20 | $CDAE | set per-voice transpose ($CB86,X) | -| < $30 | $CD9A | set per-voice filter mask byte ($CBAA+Y) | -| == $30 | $CD71 | bulk-set 14 bytes of ADSR/pulse-width ($CBBF+X..) | -| > $30 | $CD68 | "skip two bytes" silent advance | - -The `JSR $CD43` helper advances the track pointer by 1 byte; it's -used to consume the effect byte + any arg bytes. After processing -an effect, control returns via `JMP $CDA8` (continue advancing -track) or `JMP $CC99` (restart note-start with new track ptr after -a track-call). - -So the supported effects are roughly: track-call, track-return, -repeat-count, set transpose, set duration, set filter routing, -load instrument (14-byte ADSR/pulse-width block), and "skip". -This is a competent multi-pattern tracker with subroutine support. - -For the JoeyLib port: approximate timbres are fine. We don't need -to reproduce the full effect set; the simple "note + duration" -stream covers most of the music with reasonable fidelity. - -## Title-screen sprite animator (`$4523-$45A7`) - -Entry via `JSR $4523` from `$62F0` (scene-load). The 4-byte LDA -#$C0 / STA $719E sets sprite-7 ptr; then setup at `$4525` for the -title sprite cloud. Falls through to the pseudo-random demo -animator at `$4666`: - -``` -$4666 loop top: - X = $68 (current sprite index) - if $44F8,X (per-sprite timer) == 0: - JMP $4711 ; "exit demo" -- only when all timers zero - DEC $44F8,X - if non-zero, skip respawn -> $46A0 -respawn at $4675: - $718E,X = 1 ; mark sprite "visible" - $7185,X = 0 ; clear X-frac - new X = ($717D & 7) + $A6 ; columns $A6..$AD (8 wide) - $7175,X = new X - new Y = (($717D & $30) >> 4) + $89 ; 4 vertical bands at $89/$8A/$8B/$8C - $717D,X = new Y - $4500,X = $20 ; reload counter -$46A0: - DEC $68; BPL $4666 -- loop X = 6 down to 0 -$46A4: JSR $4253 (shadow marshal) - JSR $404B (VBL wait) - JSR $4293 (shadow flush) - $7222 = 0 - JSR $4FCB (runStopWatcher) - if $7222 != 0: JSR $CB02 with A=$08 (restart music) -$46BF: INC $4508 (loop counter); ... -``` - -So the title-demo is **per-sprite countdown timers**: each of the -7 demo sprites has a counter at `$44F8,X` and a respawn counter at -`$4500,X`. When the per-sprite timer hits zero, the sprite respawns -at a pseudo-random position derived from the taxi row (`$717D`) -LSBs -- so the "randomness" is actually driven by the demo cab's -own motion. Once all sprites have zero timer, JMP $4711 transitions -to gameplay. - -The per-direction respawn position is reproducible (deterministic -given the same $717D sequence) so the original always shows the -same demo, frame-perfect. - -For the JoeyLib port: render the title screen as a static image, -or implement the same per-sprite-timer-respawn pattern with -positions derived from a frame counter. Either works; the latter -matches the original's quirky-but-charming flicker effect. - -## Score / fare-value blobs (`$43B1` table) -- VERIFIED via emulator trace - -Four pre-defined 7-byte fare-value blobs at `$43B1, $43B9, $43C1, -$43C9`. Each blob is a 7-char screen-RAM template: - -``` -$43B1: 66 66 66 74 77 74 74 ; HUD init (all blanks + separator) -$43B9: 66 66 66 6F 77 74 74 ; basic fare = +5 -$43C1: 66 66 73 6F 77 74 74 ; alt fare A = +95 -$43C9: 66 66 6F 74 77 74 74 ; alt fare B = +50 -``` - -Char-code interpretation (via $4345 validation): -- `$66`: leading blank (treated as 0 in BCD add) -- `$74`: trailing space (treated as 0) -- `$6A-$73`: digits 0..9 ($6A = '0', $73 = '9') -- `$77`: separator (position 4 -- BCD-add SKIPS this position so - the separator char is preserved across adds) -- Other chars: undefined behavior - -Verified by running `$4354` through the 6502 emulator (see -`stuff/spacetaxi/trace.py`) with each blob against an all-blank -HUD. The actual byte-level result for each: - -| Blob | HUD after BCD-add (chars) | Numeric effect | -| ------ | ------------------------- | --------------------------- | -| $43B1 | `___ _ ` | identity (no digits) | -| $43B9 | `___5_ ` | +5 in pos 3 (ones) | -| $43C1 | `__95_ ` | +95 in pos 2 (tens) + pos 3 | -| $43C9 | `__5 _ ` | +50 in pos 2 (tens) | - -If the C64 HUD layout is `DDDD.DD` (4 integer + separator + 2 -decimal), then position 3 is the ones-digit of integer. So: -- `$43B9` = $5 basic fare -- `$43C9` = $50 bonus -- `$43C1` = $95 jackpot - -Callers (verified in disassembly): - -| Site | Blob | Game event | -| ------- | ------ | ------------------------------------------------ | -| `$6780` `$43B9` | Stage 6 ($6742) basic fare delivered | -| `$5C82` `$43C9` | After death-fall reaches floor row $D4 (untraced precise trigger) | -| `$5D03` `$43C1` | After per-passenger counter decrement hits zero (untraced precise trigger) | - -The `$5C82` and `$5D03` precise game-state triggers are not yet -fully traced -- they fire during animation/state-transition flows -whose state pre-conditions aren't fully mapped. - -For the JoeyLib port: `ST_FARE_SCORE = 5` per delivered fare -(matches verified `$43B9` BCD effect). The +50 and +95 bonuses -aren't wired up since their trigger conditions are unverified. - -**Previously I claimed "flat 500 per fare" without ever running -the BCD math; that was wrong.** - -## Game-end condition VERIFIED: $71CE == $7213 (fare target) - -`$71CE` ("fares done") increments only at `$6BD6` inside `$6BD0`, -which is reached only when `$5CF1 >= 3` (the rare end-of-sequence -state). When `$71CE == $7213`, `$6BE1: JMP $602F` fires, which -`JMP $5EC4` -- the **gameInitContinue** entry, effectively -restarting the game (zeroes `$71CE`, `$7215`, runs title path). - -`$7213` (fare target) is **player-selectable on the title screen**: -- `$48AF` sets it to `$01` default -- `$5310-$5328` is a title-screen UI loop: - - RIGHT held (`$7169 & $08`): INC `$7213`, mask to 0..3 (= 0..3 + 1 baseline = 1..4) - - LEFT held (`$7169 & $04`): DEC `$7213` - - Other input: continue - -So Space Taxi's "difficulty" = how many fares per game. Default 1, -selectable 1..4 at title via joystick. After that many deliveries, -the game ends and returns to title. - -The JoeyLib port's per-level fare progression (24 sequential -levels with their own fare counts) is a port-side invention -- -the C64 game restarts at title after the player-chosen fare count, -with no inter-level progression. - -## `$5C2C` drop-off animation (NOT death-fall -- VERIFIED) - -Earlier I mis-labeled `$5C2C` as "deathFallAnim". Re-tracing -shows: when `$5CF1 = 0` (the default state), `$5BBB` jumps to -`$5C2C` which runs physics each tick until the cab's row reaches -`$D4` (row 212 = near screen bottom). Then it executes the -"finalize" block: - -``` -$5C77: $68=$E0; $69=$07 ; source pointer = $07E0 (HUD blank) -$5C7F: JSR $4354 ; BCD-add $07E0 to $07C2 -- noop (source is blanks) -$5C82: $68=$C9; $69=$43 ; source pointer = $43C9 (+50 fare blob) -$5C8A: JSR $4354 ; BCD-add: $07C2 += 50 -$5C8D: JSR $43A5 ; hudInit -- re-template $07E0 -$5C90: RTS ; back to caller -``` - -Verified by emulator trace: starting cab at row `$D3`, running -`$5C2C` directly. 642 instructions executed. Cab ends at `$D4`. -JSR chain captured: $4354 (twice) and $43A5 (hudInit). Score at -$07C2 advances by +50 (the `$43C9` blob delta verified earlier). - -So **`$5C2C` is the drop-off animation**, awarding +50 (the bonus -fare blob) once the cab reaches the destination pad floor. NOT a -death sequence -- the cab moves to its target, gets +50 score, the -fare is delivered. - -The `$5CF1` state-selector ranges in `$5BBB`: - -| $5CF1 | Path | Role | -| ----- | -------------- | ------------------------------------- | -| 0 | `$5C2C` | drop-off animation (+50 bonus on finalize) | -| 1 | `$5C91` | takeoff animation | -| 2 | `$5CB9` | pad-to-pad transition | -| 3+ | fall through to `$5BD1` | restore physics templates, re-enable trampolines, then `JMP $6BD0` | - -So `$6BD0` is reached ONLY when `$5CF1 >= 3` -- a rare state that's -probably the actual game-over path. The "GAME OVER!" text drawn by -`$5FBB` therefore does NOT display on every drop-off as I claimed -earlier; only on the specific `$5CF1 >= 3` transition. - -## Level-end / game-over flow - -After phase-3 finishes (`$6B4C`), control falls to `$6BD0` -(reached via the `$5BD1` template-restore path, not every death): - -``` -$6BD0: JSR $704E ; archive HUD row + score row to backup tables -$6BD3: JSR $5FBB ; (if game-over) draw "GAME OVER" + delay -$6BD6: INC $71CE ; bump fares-completed counter -$6BD9: CMP $7213 ; compare to fares-needed -$6BDF: BNE $6BE4 -> JMP $5F02 ; not done, main loop -$6BE1: JMP $602F ; all fares done, level complete -$6BE4: JMP $5F02 -``` - -`$704E` archives the current HUD: copies the 7 chars at `$07C2` -(in-cab text) and 7 chars at `$07E0` (score row) into per-passenger -backup tables at `$71D3,X*8` and `$71F3,X*8` (X = current fare idx -$7214). This lets a respawn restore the per-fare HUD state. - -`$5FBB` (drawn only via `$6BD3` on game-over path) renders the -**"GAME OVER" screen**: -- Three lines at column 14, rows 10/11/12 drawn via `$41C2` from - source pointers `$6C38`, `$6C52`, `$6C38` -- `$D01B = $FF` (push all sprites BEHIND the background) -- Kill voice 1+2 control + voice 3 gate-off -- Four sequential busywait loops at `$4101` with X=$FF each - (~80ms total visual pause) - -`$5010` is the **level-end dispatcher** called from `$61FB` round-end: - -| State | Action | -| ---------------------------------- | -------------------- | -| `$5E9D` non-zero (post-mortem) | JMP $505A (full game-over screen) | -| `$721C` non-zero (menu mode) | JMP $50C6 | -| `$7215 == $18` | JMP $5167 (level-complete pause) | -| `$7215 == $19` | JMP $602F (next-level intro) | -| `$7221 == 1/2/3` | JMP $517D (sub-paths) | -| `$7221 == 4` | JMP $519C | -| `$7221 == 5` | JMP $51A6 | -| else | JMP $5EC4 (gameInitContinue) | - -So `$7215` is the level-progression counter ($18 = "level done, show -score"; $19 = "advance to next level") and `$7221` is a sub-state -selector for finer-grained transitions. - -`$5BBB` is the **respawn-state dispatcher** that branches on `$5CF1`: - -| `$5CF1` | Path | -| ------- | ------------------------------------------------------ | -| 0 | JMP $5C2C (death fall anim: $6032 physics + clamp $D4) | -| 1 | JMP $5C91 (takeoff anim: $6032 physics + clamp <$87) | -| 2 | JMP $5CB9 (pad-to-pad fly-by sequence) | -| else | Falls through to $5BD1 (level template restore) | - -So `$5CF1` (earlier labeled gCacheUnknown) is the **transition-type -selector**: 0=falling, 1=taking off, 2=moving between pads, -else=normal level init. - -## Music index cycling (`$6279`) - -`$6279` decrements `$716B` (music song index) by `$41`. Called from -`$621F` round-end-reset. Each fare completion rotates the song: -each level isn't tied to one song; the music shifts as the player -progresses through fares within a level. - -## Game-init self-popping trampoline (`$4741`) - -``` -$4741: PLA / STA $473C ; save caller's return-addr LO -$4745: PLA / STA $473D ; save HI -$4749: TSX / STX $473E ; save stack pointer -$474D: JSR $477A ; load song 25 via $6283 + JSR $6F95 -$4750: A=$80 -> $D412 (voice 3 off) - D015=0, D020=D021=0, $C6=0 - JSR $40CA (fillScreenAndColor) - if $721C != 0: JSR $5295 (score-screen draw) -$476D: TSX/TXS restore ; restore stack pointer - Push saved return addr back -$4779: RTS ; return to caller as if nothing happened -``` - -This is a clever **JSR-but-don't-consume-stack** pattern. The -routine pops its own return address, does setup with possibly -modified call chains, then pushes the original return back. The -net effect is that the caller never knows $4741 ran -- but $4741 -did get to insert title-screen song setup + buffer fill. - -Called from `$5EF7` in gameInit. So gameInit goes through: -``` -$5EC1 gameInit - -> $CC00 install IRQ - -> ... clear buffers - -> $5EEC JSR $528A (zero $5254 buffer) - -> if $5E9D set: JMP $48AD (title) - -> $5EF7 JSR $4741 (transparent song-25 + clear setup) - -> if $721C set: JMP $48AD (title) - -> $5F02 main entry... -``` - -## Under-ROM data (from raw.bin) - -The `raw.bin` capture was taken with `bank ram` set in VICE so the -RAM hidden beneath the BASIC/Kernal/I/O ROMs is preserved. With it -we can see: - -- The decompressor at `$9656` and its helper bit-stream reader at - `$96A3/$964A` -- The scene pointer table at `$9600` (28 entries, 2 bytes each) -- The compressed scene data at $D000-$FCFF and $8100-$9418 -- The music engine code at $CB00-$CFFF and frequency tables at - $CE90 (lo bytes) / $CEF1 (hi bytes) - -### Scene pointer table (`$9600`) - -24 game levels (A..X = scenes 0..23) + special scenes: - -| Scene | Letter | Data addr | -| ----- | ------ | --------- | -| 0 | A | $D000 | -| 1 | B | $D1E4 | -| 2 | C | $D3E0 | -| 3 | D | $D700 | -| 4 | E | $D8F5 | -| 5 | F | $DB15 | -| 6 | G | $DD77 | -| 7 | H | $E075 | -| 8 | I | $E416 | -| 9 | J | $E6F6 | -| 10 | K | $E9BF | -| 11 | L | $EBCE | -| 12 | M | $EE79 | -| 13 | N | $F05F | -| 14 | O | $F251 | -| 15 | P | $F46D | -| 16 | Q | $F767 | -| 17 | R | $FA61 | -| 18 | S | $FC9F | -| 19 | T | $8100 | -| 20 | U | $8340 | -| 21 | V | $86A7 | -| 22 | W | $8943 | -| 23 | X | $8C7B | -| 24 | title | $8F7D | -| 25 | ? | $9418 | -| 26+ | invalid | (data garbage) | - -### Decompressor (`$9656`) - -``` -$9656: TXA / ASL A / TAX ; X = scene*2 (word index) -$9659: LDA $9600,X ; load LO of pointer -$965C: LDY $9601,X ; load HI -$965F: STA $964B / STY $964C ; self-mod the LDA at $964A -$9665: LDY=0; LDX=3 -$9669: JSR $964A ; read 1 bit from stream - STA $93,X; DEX; BNE ; build 4-bit token -$9672: TYA; AND #$0F - BEQ $968B ; skip if zero -$9677: ... shift/copy table-build ... -$968B: STA $039C,Y ; write to output table - STA $0368,Y ; output a second table too - JSR $96A3 (read 4 bits) -> $0334,Y -$969A: INY; CPY #$34; BNE $9671 - ... walk output ... -``` - -It's a per-scene **bitstream decoder** that builds three tables at -$0334, $0368, $039C from a compressed stream. Each scene blob is -roughly 250-700 bytes that expands to 1000 cells of screen RAM, -1000 cells of color RAM, plus the per-level data table at $7D00. - -The decompressor uses self-modification for the read pointer -($964B/$964C are operand bytes of the LDA at $964A), enabling -the same code to walk any scene's pointer. - -### Per-level data table for all 24 levels (VERIFIED via emulator) - -Extracted by running the decompressor for each scene through -`cpu6502.py` and reading `$7D00-$7D9F` after. X-accel and Y-accel -are stored separately (`$7D91/92` X, `$7D8F/90` Y) and DIFFER on -several levels. X-gravity (`$7D95`) is non-zero on levels P and U -(side-wind). Y-gravity (`$7D93`) is signed -- level K = `$F9` = -7. - -| Lvl | Pads | SpawnX | SpawnY | X-acc | Y-acc | X-grav | Y-grav | Bord/BG | -| --- | ---- | ------ | ------ | ----- | ----- | ------ | ------ | -------- | -| A | 1 | $0A00 | $4A00 | $0E | $0E | $00 | $01 | $00/$00 | -| B | 3 | $8800 | $8800 | $11 | $11 | $00 | $01 | $00/$00 | -| C | 5 | $AA00 | $4400 | $12 | $10 | $00 | $02 | $00/$00 | -| D | 10 | $8800 | $8800 | $11 | $14 | $00 | $06 | $00/$00 | -| E | 9 | $FA00 | $C800 | $11 | $15 | $00 | $06 | $09/$00 | -| F | 3 | $AA00 | $6400 | $15 | $13 | $00 | $03 | $00/$00 | -| G | 5 | $AF00 | $5600 | $14 | $18 | $00 | $06 | $0C/$00 | -| H | 5 | $9600 | $3E00 | $19 | $19 | $00 | $06 | $00/$00 | -| I | 7 | $AA00 | $6E00 | $15 | $13 | $00 | $03 | $00/$00 | -| J | 6 | $1E00 | $3E00 | $19 | $19 | $00 | $05 | $09/$00 | -| K | 5 | $B400 | $5F00 | $17 | $15 | $00 | $F9 | $00/$00 | <- anti-gravity -| L | 6 | $6E00 | $4600 | $1A | $1A | $00 | $03 | $0B/$0B | -| M | 10 | $AF00 | $3E00 | $40 | $46 | $00 | $06 | $06/$00 | <- highest accel -| N | 8 | $AF00 | $3E00 | $12 | $16 | $00 | $06 | $00/$00 | -| O | 1 | $2E00 | $3400 | $15 | $13 | $00 | $03 | $00/$00 | -| P | 3 | $2400 | $4A00 | $15 | $15 | **$04**| $06 | $0B/$00 | <- side wind -| Q | 8 | $B400 | $4600 | $19 | $19 | $00 | $05 | $0B/$00 | -| R | 1 | $E600 | $3800 | $10 | $10 | $00 | $01 | $00/$00 | -| S | 5 | $AA00 | $4400 | $12 | $10 | $00 | $03 | $07/$08 | -| T | 8 | $AF00 | $C800 | $1B | $26 | $00 | $06 | $06/$00 | -| U | 5 | $AA00 | $4A00 | $18 | $18 | **$02**| $02 | $00/$00 | <- side wind -| V | 7 | $AF00 | $3E00 | $1E | $20 | $00 | $06 | $06/$00 | -| W | 3 | $3200 | $4600 | $1E | $1E | $00 | $03 | $00/$00 | -| X | 8 | $AF00 | $4600 | $17 | $17 | $00 | $06 | $0B/$00 | - -Sprite colors (`$7D07`/`$7D08`) verified `$06` (blue) for EVERY -level -- so the cab color is constant across the game; only the -border/BG palette changes. - -Key gameplay-affecting observations: - -- **Levels P and U have side-wind**: non-zero X-gravity (`$04` and - `$02` respectively). Cab drifts horizontally without input. -- **Level K has anti-gravity** (`Y-grav = $F9` = -7 signed). The - cab is pulled UP each frame. -- **X-accel != Y-accel on most levels**. The cab is more responsive - on one axis. E.g., level T: xAcc=$1B but yAcc=$26 (38% more - vertical thrust). Level M is the extreme: x=$40, y=$46. -- **Pad count** grows from 1 (A, O, R) to 10 (D, M). -- **Border/BG colors** vary; most levels are black (0/0). Level S - has yellow/orange (`$07/$08`), level L is dark gray on dark gray. - -The .dat files in `examples/spacetaxi/generated/levels/` carry the -same xAccel/yAccel/xGrav/yGrav values verbatim (verified by parsing -the binary headers). - -**Pad style bytes (byte 7 of each 8-byte pad slot):** - -Level | Style bytes ---- | --- - A | $C2 - B | $C2 $C2 $C4 - C | $C2 $C2 $C4 $C2 $C4 - D | $FF $FF $FF $FF $FF $FF $FF $FF $FF $FF (uninitialized?) - E | $C2 $C2 $C4 $C2 $C4 $C2 $C4 $C2 - F | $C2 $C4 $C4 - G | $C2 $C2 $C2 $C2 $C4 - H | $C4 $C4 $C2 $C2 $C2 - I | $C2 $C2 $C2 $C2 $C4 $C4 $C4 - J | $C2 $C2 $C4 $C2 $C4 $C4 - K | $C2 $C2 $C2 $C4 $C4 - L | $C2 $C4 $C2 $C2 $C4 $C4 - M | $C2 $C2 $C4 $C4 $C4 $C4 $C4 $C2 - N | $C4 $C2 $C2 $C4 $C4 $C4 $C2 $C2 - O | $C2 - P | $C2 $C4 $C2 - Q | $C4 $C2 $C2 $C4 $C2 $C4 $C2 $C4 - R | $C4 - S | $C2 $C2 $C4 $C2 $C4 - T | $C2 $C4 $C2 $C2 $C4 $C4 $C2 $C4 - U | $C4 $C2 $C2 $C4 $C2 - V | $C2 $C2 $C2 $C2 $C2 $C4 $C4 - W | $C4 $C2 $C2 - X | $C4 $C4 $C4 $C4 $C2 $C2 $C2 $C2 - -Pad-style codes only vary between `$C2` (mostly) and `$C4`. Level -D's all-`$FF` is suspicious -- either uninitialized scene data or -a sentinel meaning "use default style". - -## Stage 3 is dead code (the `$5DCE` mystery resolved) - -Empirically confirmed via grep across BOTH the visible RAM listing -AND the under-ROM listing: **no code writes to `$5DCE` or `$5DCF`**. -The bytes stay at $00 $00 throughout execution, so `JMP ($5DCE)` -at stage 3's `$66DA` would jump to address $0000 = BRK = crash. - -This is fine because **stage 3 of the `$65C1` dispatcher is never -reached during normal play**: stage 2 of $65C1 (`$66B7`) is just -a cel-cycle loop with no `INC $7163`, so the state machine sticks -at stage 2 and never tries to advance to 3. - -Stages 1 through 9 ARE reached, but via a **parallel state machine -at `$4A03`** (driven from the score-screen / level-intro path at -`$47C9`+ that sets `$7163 = 2` directly). $4A03's dispatch handles -stages 2/3/4/5 by calling specific subroutines (e.g. stage 2 calls -`$66B7` as a SUBROUTINE rather than via the $65C1 dispatch). Stage -3 of $4A03 (`$4A24`) is a hover-style routine that doesn't use -$5DCE at all. - -The `INX / JMP ($5DCE)` at `$66DA` is **legacy code from an earlier -version** where the state machines were unified. Now it's just an -unreachable trap. - -### Parallel state machine (`$4A03`) - -Dispatcher with its own jump table on `$7163`: - -| `$7163` | Target | Role | -| ------- | ------- | ----------------------------------------------- | -| 2 | $4A17 | sparkle-cel cycle ($66B7) + 90-frame wait | -| 3 | $4A24 | hover positioning (uses $6D0D pad-lookup) | -| 4 | $4A45 | JMP $67A6 (sprite ptr walk to $CC) | -| 5 | $4A48 | final hover + noise gate-off | - -This runs as a **sequence** during the score-screen / "press fire -to start" transition, called from `$47E4: JSR $4A03`. Caller sets -`$7163 = 2` and `$473F = $5A` (90-frame counter), then runs the -machine via JSRs (not main-loop dispatch). - -So Space Taxi has **TWO state machines on $7163**: -- `$65C1` runs every frame from post-tick (stages 0-9, but only - 0/1/2/4-9 are reachable; the 2->3 path is broken) -- `$4A03` runs from the intro/transition path (stages 2-5, fully - reachable) - -## Music frequency tables (under-ROM) - -- `$CE90-$CEF0`: 97 entries, **freq LO byte per note** -- `$CEF1-$CF51`: 97 entries, **freq HI byte per note** - -The note indices 0..96 cover roughly 8 octaves. Note 0 = $0000 -(silence), notes 1..96 give the SID 16-bit frequency code for each -chromatic step. Looking up: at index 13 (octave 1, note D) the -freq is $0238 = 568, which translates via the SID formula to -~33 Hz -- low D. The table goes up to ~3.5 kHz at the top. - -These tables are normal 12-tone equal temperament with the C64 -SID's clock-divider scaling. For the JoeyLib port: any standard -PSG / FM / sample-based pitch table will sound musically correct. - -## Open work (truly remaining) - -1. **Bonus / tip score blobs**: blobs at `$43C1` and `$43C9` are - not yet traced to their callers. The flat-rate 500-point blob - ($43B9) is the primary; the other two are likely triggered by - specific event scenarios we haven't observed. -2. **Music engine effect arg-byte semantics**: the major effect - types are mapped, but the exact byte-layout of each effect's - args (how many bytes after the opcode, what they mean) is - still case-by-case. Approximate playback works without this. -3. **Menu-state script interpreter** at `$48F2-$494F`: consumes a - byte stream from the `$0900` buffer. The opcode table for the - script bytes (timing, text display, animation triggers) needs - to be traced opcode-by-opcode by stepping through the title - demo in VICE. -4. **Level D's `$FF` pad-style placeholder**: byte-7 of all 10 pad - slots is `$FF` in level D. Either a level data bug in the - original game or the game substitutes a default style when it - sees $FF. Worth confirming by playing level D in VICE. diff --git a/examples/spacetaxi/PLAN.md b/examples/spacetaxi/PLAN.md deleted file mode 100644 index 250346f..0000000 --- a/examples/spacetaxi/PLAN.md +++ /dev/null @@ -1,141 +0,0 @@ -# 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 - `, 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 `./joey-run --port dos 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. diff --git a/examples/spacetaxi/VERIFIED.md b/examples/spacetaxi/VERIFIED.md deleted file mode 100644 index 274f1a1..0000000 --- a/examples/spacetaxi/VERIFIED.md +++ /dev/null @@ -1,97 +0,0 @@ -# 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` `fareCount` and the `(0,0)` self-loop were BOTH fabricated | Deep disassembly (padHoverSetup `$6537`, padDetect `$64CC`, `$6CB8`) proves there is NO static fare list: spawn pad AND destination are RNG-chosen at runtime (`$4080`) in `[1..padCount]`; when all pads are used the destination is the `$0B` "UP PLEASE!" transporter sentinel. Fare count = `$7213` (the GAME-VARIATIONS "cabbies" setting), global not per-level. FIX: `romToLevel` emits `fareCount=0`; the port (`stPassenger.c`) ports the runtime RNG selection. Empirically verified in DOSBox: land->board->"up please!"->fly up through the top-wall transporter->fare delivered (+5)->advance to level B. | -| `romToLevel.py` pad tileX/tileY had NO VIC origin subtraction (pads off-field) | Pad slot bytes are VIC sprite-register coords. FIX (verified exact on 4 pads across scenes A/B, matched to live screen RAM): `tileX=(x1-24)//8`, `tileY=(padRow-36)//8` (36 = sprite-Y origin 50 - 14px landing-gear). Level A pad: was tileX13/tileY25(off-field), now tileX10/tileY21 (on the rendered pad). Also the taxi spawn was hardcoded `(18,2)`; real spawn is `$7D5B-5F` (level A = tile 30,3). | -| 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. -- `examples/spacetaxi/assets/cpu6502.py`: minimal 6502 emulator. Sufficient for all routines tested above; throws `NotImplementedError` for unsupported opcodes (none hit so far in gameplay paths). -- `examples/spacetaxi/assets/dumpScenes.py` + `stuff/spacetaxi/dumpAllLevels.py`: pre-existing scene-decompress drivers. -- `examples/spacetaxi/assets/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. diff --git a/examples/spacetaxi/assets/RawTitle.png b/examples/spacetaxi/assets/RawTitle.png deleted file mode 100644 index 2710f32..0000000 --- a/examples/spacetaxi/assets/RawTitle.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:037ff3028d1e5e1e4022109199d53664812f21c258f2b95f1945792b99cbccdc -size 109832 diff --git a/examples/spacetaxi/assets/TitleShr.png b/examples/spacetaxi/assets/TitleShr.png deleted file mode 100644 index 627e4a0..0000000 --- a/examples/spacetaxi/assets/TitleShr.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6846cf0cc6fc16da5d290a2f395ed44cab1f98f38e652877744ebbd44c3429b8 -size 17887 diff --git a/examples/spacetaxi/assets/cpu6502.py b/examples/spacetaxi/assets/cpu6502.py deleted file mode 100644 index 2ab6af5..0000000 --- a/examples/spacetaxi/assets/cpu6502.py +++ /dev/null @@ -1,560 +0,0 @@ -#!/usr/bin/env python3 -# Minimal 6502 CPU emulator -- just enough to run the Space Taxi -# level decompressor at $6283 against a captured 64KB RAM dump. -# -# Scope: implements the opcodes used by $6283 / $9656 / $964A / $96A3 -# and their helpers. Not a general-purpose 6502 emulator; it has no -# decimal mode, no BRK handling, no NMI/IRQ -- the routine runs with -# SEI in effect and never returns control. -# -# Flags tracked: N Z C. V (overflow) is computed for ADC/SBC since -# those instructions set it, even if no branch tests it. D (decimal) -# is ignored. - -class Cpu6502: - def __init__(self, ram): - # ram is a bytearray of 65536 bytes. Caller pre-loaded the dump. - self.ram = ram - self.a = 0 - self.x = 0 - self.y = 0 - self.sp = 0xFF - self.pc = 0 - self.n = self.z = self.c = self.v = False - # Stop tracking when RTS unwinds to this depth (set by run()). - self._depth_floor = 0 - - # ---- memory ---- - def read8(self, addr): - return self.ram[addr & 0xFFFF] - - def write8(self, addr, val): - self.ram[addr & 0xFFFF] = val & 0xFF - - def read16(self, addr): - return self.ram[addr & 0xFFFF] | (self.ram[(addr + 1) & 0xFFFF] << 8) - - # ---- stack ---- - def push8(self, val): - self.ram[0x100 + self.sp] = val & 0xFF - self.sp = (self.sp - 1) & 0xFF - - def pop8(self): - self.sp = (self.sp + 1) & 0xFF - return self.ram[0x100 + self.sp] - - def push16(self, val): - self.push8((val >> 8) & 0xFF) - self.push8(val & 0xFF) - - def pop16(self): - lo = self.pop8() - hi = self.pop8() - return (hi << 8) | lo - - # ---- flag helpers ---- - def set_nz(self, val): - val &= 0xFF - self.z = (val == 0) - self.n = (val & 0x80) != 0 - return val - - def pack_p(self): - # B and U are usually set when pushed by PHP/BRK. - p = 0x20 - if self.n: p |= 0x80 - if self.v: p |= 0x40 - if self.z: p |= 0x02 - if self.c: p |= 0x01 - return p | 0x10 # B = 1 when pushed by PHP - - def unpack_p(self, p): - self.n = (p & 0x80) != 0 - self.v = (p & 0x40) != 0 - self.z = (p & 0x02) != 0 - self.c = (p & 0x01) != 0 - - # ---- addressing helpers (return effective address) ---- - def addr_imm(self): - a = self.pc; self.pc = (self.pc + 1) & 0xFFFF; return a - - def addr_zp(self): - a = self.read8(self.pc); self.pc = (self.pc + 1) & 0xFFFF; return a - - def addr_zpx(self): - a = (self.read8(self.pc) + self.x) & 0xFF; self.pc = (self.pc + 1) & 0xFFFF; return a - - def addr_abs(self): - a = self.read16(self.pc); self.pc = (self.pc + 2) & 0xFFFF; return a - - def addr_absx(self): - a = (self.read16(self.pc) + self.x) & 0xFFFF; self.pc = (self.pc + 2) & 0xFFFF; return a - - def addr_absy(self): - a = (self.read16(self.pc) + self.y) & 0xFFFF; self.pc = (self.pc + 2) & 0xFFFF; return a - - def addr_indy(self): - zp = self.read8(self.pc); self.pc = (self.pc + 1) & 0xFFFF - base = self.ram[zp] | (self.ram[(zp + 1) & 0xFF] << 8) - return (base + self.y) & 0xFFFF - - # ---- branch helper ---- - def branch(self, cond): - off = self.read8(self.pc); self.pc = (self.pc + 1) & 0xFFFF - if off >= 0x80: off -= 0x100 - if cond: - self.pc = (self.pc + off) & 0xFFFF - - # ---- step ---- - def step(self): - op = self.read8(self.pc) - self.pc = (self.pc + 1) & 0xFFFF - f = _DISPATCH.get(op) - if f is None: - raise NotImplementedError(f"opcode ${op:02X} at PC ${(self.pc-1)&0xFFFF:04X}") - f(self) - - def call(self, target, max_steps=10_000_000): - """Call subroutine at `target` as if via JSR. Returns when the - matching RTS pops the stack back below the original SP. Hard - cap on steps to avoid runaway.""" - # Push a sentinel return address. When RTS pops to this, stop. - # Use $FFFF as sentinel: subsequent step would attempt to fetch - # from $FFFF; we detect via depth floor instead. - original_sp = self.sp - self.push16(0xFFFE) # RTS will pop this, then step will land at $FFFF - self.pc = target & 0xFFFF - for _ in range(max_steps): - if self.pc == 0xFFFF: - return - self.step() - raise RuntimeError(f"6502 call exceeded {max_steps} steps; PC=${self.pc:04X}") - - -# ---- opcode implementations ---- -# Each takes the cpu and reads remaining operand bytes via cpu.addr_*. - -def _NOP(cpu): pass -def _SEI(cpu): pass # we don't model interrupts -def _CLD(cpu): pass # no decimal mode -def _SEC(cpu): cpu.c = True -def _CLC(cpu): cpu.c = False -def _PHP(cpu): cpu.push8(cpu.pack_p()) -def _PLP(cpu): cpu.unpack_p(cpu.pop8()) -def _PHA(cpu): cpu.push8(cpu.a) -def _PLA(cpu): cpu.a = cpu.set_nz(cpu.pop8()) -def _TXA(cpu): cpu.a = cpu.set_nz(cpu.x) -def _TAX(cpu): cpu.x = cpu.set_nz(cpu.a) -def _TYA(cpu): cpu.a = cpu.set_nz(cpu.y) -def _TAY(cpu): cpu.y = cpu.set_nz(cpu.a) -def _INX(cpu): cpu.x = cpu.set_nz((cpu.x + 1) & 0xFF) -def _INY(cpu): cpu.y = cpu.set_nz((cpu.y + 1) & 0xFF) -def _DEX(cpu): cpu.x = cpu.set_nz((cpu.x - 1) & 0xFF) -def _DEY(cpu): cpu.y = cpu.set_nz((cpu.y - 1) & 0xFF) - -def _LDA_imm(cpu): cpu.a = cpu.set_nz(cpu.read8(cpu.addr_imm())) -def _LDA_zp(cpu): cpu.a = cpu.set_nz(cpu.read8(cpu.addr_zp())) -def _LDA_zpx(cpu): cpu.a = cpu.set_nz(cpu.read8(cpu.addr_zpx())) -def _LDA_abs(cpu): cpu.a = cpu.set_nz(cpu.read8(cpu.addr_abs())) -def _LDA_absx(cpu): cpu.a = cpu.set_nz(cpu.read8(cpu.addr_absx())) -def _LDA_absy(cpu): cpu.a = cpu.set_nz(cpu.read8(cpu.addr_absy())) -def _LDA_indy(cpu): cpu.a = cpu.set_nz(cpu.read8(cpu.addr_indy())) - -def _LDX_imm(cpu): cpu.x = cpu.set_nz(cpu.read8(cpu.addr_imm())) -def _LDX_abs(cpu): cpu.x = cpu.set_nz(cpu.read8(cpu.addr_abs())) -def _LDX_absy(cpu): cpu.x = cpu.set_nz(cpu.read8(cpu.addr_absy())) -def _LDX_zp(cpu): cpu.x = cpu.set_nz(cpu.read8(cpu.addr_zp())) - -def _LDY_imm(cpu): cpu.y = cpu.set_nz(cpu.read8(cpu.addr_imm())) -def _LDY_abs(cpu): cpu.y = cpu.set_nz(cpu.read8(cpu.addr_abs())) -def _LDY_absx(cpu): cpu.y = cpu.set_nz(cpu.read8(cpu.addr_absx())) -def _LDY_zp(cpu): cpu.y = cpu.set_nz(cpu.read8(cpu.addr_zp())) - -def _STA_zp(cpu): cpu.write8(cpu.addr_zp(), cpu.a) -def _STA_zpx(cpu): cpu.write8(cpu.addr_zpx(), cpu.a) -def _STA_abs(cpu): cpu.write8(cpu.addr_abs(), cpu.a) -def _STA_absx(cpu): cpu.write8(cpu.addr_absx(), cpu.a) -def _STA_absy(cpu): cpu.write8(cpu.addr_absy(), cpu.a) -def _STA_indy(cpu): cpu.write8(cpu.addr_indy(), cpu.a) - -def _STX_zp(cpu): cpu.write8(cpu.addr_zp(), cpu.x) -def _STX_abs(cpu): cpu.write8(cpu.addr_abs(), cpu.x) - -def _STY_zp(cpu): cpu.write8(cpu.addr_zp(), cpu.y) -def _STY_abs(cpu): cpu.write8(cpu.addr_abs(), cpu.y) - -def _AND_imm(cpu): cpu.a = cpu.set_nz(cpu.a & cpu.read8(cpu.addr_imm())) -def _AND_zp(cpu): cpu.a = cpu.set_nz(cpu.a & cpu.read8(cpu.addr_zp())) -def _AND_abs(cpu): cpu.a = cpu.set_nz(cpu.a & cpu.read8(cpu.addr_abs())) -def _ORA_imm(cpu): cpu.a = cpu.set_nz(cpu.a | cpu.read8(cpu.addr_imm())) -def _ORA_zp(cpu): cpu.a = cpu.set_nz(cpu.a | cpu.read8(cpu.addr_zp())) -def _EOR_imm(cpu): cpu.a = cpu.set_nz(cpu.a ^ cpu.read8(cpu.addr_imm())) -def _EOR_abs(cpu): cpu.a = cpu.set_nz(cpu.a ^ cpu.read8(cpu.addr_abs())) - -def _adc(cpu, m): - r = cpu.a + m + (1 if cpu.c else 0) - cpu.c = r > 0xFF - cpu.v = ((cpu.a ^ r) & (m ^ r) & 0x80) != 0 - cpu.a = cpu.set_nz(r & 0xFF) - -def _ADC_imm(cpu): _adc(cpu, cpu.read8(cpu.addr_imm())) -def _ADC_zp(cpu): _adc(cpu, cpu.read8(cpu.addr_zp())) -def _ADC_abs(cpu): _adc(cpu, cpu.read8(cpu.addr_abs())) -def _ADC_absy(cpu): _adc(cpu, cpu.read8(cpu.addr_absy())) - -def _sbc(cpu, m): - _adc(cpu, m ^ 0xFF) - -def _SBC_imm(cpu): _sbc(cpu, cpu.read8(cpu.addr_imm())) -def _SBC_zp(cpu): _sbc(cpu, cpu.read8(cpu.addr_zp())) -def _SBC_abs(cpu): _sbc(cpu, cpu.read8(cpu.addr_abs())) -def _SBC_absx(cpu): _sbc(cpu, cpu.read8(cpu.addr_absx())) -def _SBC_absy(cpu): _sbc(cpu, cpu.read8(cpu.addr_absy())) - -def _AND_absx(cpu): cpu.a = cpu.set_nz(cpu.a & cpu.read8(cpu.addr_absx())) -def _AND_absy(cpu): cpu.a = cpu.set_nz(cpu.a & cpu.read8(cpu.addr_absy())) -def _AND_indy(cpu): cpu.a = cpu.set_nz(cpu.a & cpu.read8(cpu.addr_indy())) -def _ORA_absx(cpu): cpu.a = cpu.set_nz(cpu.a | cpu.read8(cpu.addr_absx())) -def _ORA_abs(cpu): cpu.a = cpu.set_nz(cpu.a | cpu.read8(cpu.addr_abs())) -def _ORA_zpx(cpu): cpu.a = cpu.set_nz(cpu.a | cpu.read8(cpu.addr_zpx())) -def _ORA_indy(cpu): cpu.a = cpu.set_nz(cpu.a | cpu.read8(cpu.addr_indy())) -def _EOR_zp(cpu): cpu.a = cpu.set_nz(cpu.a ^ cpu.read8(cpu.addr_zp())) -def _EOR_zpx(cpu): cpu.a = cpu.set_nz(cpu.a ^ cpu.read8(cpu.addr_zpx())) -def _EOR_absx(cpu): cpu.a = cpu.set_nz(cpu.a ^ cpu.read8(cpu.addr_absx())) -def _EOR_absy(cpu): cpu.a = cpu.set_nz(cpu.a ^ cpu.read8(cpu.addr_absy())) -def _ADC_zpx(cpu): _adc(cpu, cpu.read8(cpu.addr_zpx())) -def _ADC_absx(cpu): _adc(cpu, cpu.read8(cpu.addr_absx())) -def _CMP_absx(cpu): _cmp_with(cpu, cpu.a, cpu.read8(cpu.addr_absx())) -def _CMP_absy(cpu): _cmp_with(cpu, cpu.a, cpu.read8(cpu.addr_absy())) -def _CMP_zpx(cpu): _cmp_with(cpu, cpu.a, cpu.read8(cpu.addr_zpx())) - -def _JMP_ind(cpu): - addr = cpu.addr_abs() - cpu.pc = cpu.read8(addr) | (cpu.read8((addr & 0xFF00) | ((addr + 1) & 0xFF)) << 8) - -def _BIT_zp(cpu): - v = cpu.read8(cpu.addr_zp()) - cpu.z = (cpu.a & v) == 0 - cpu.n = (v & 0x80) != 0 - cpu.v = (v & 0x40) != 0 - -def _BIT_abs(cpu): - v = cpu.read8(cpu.addr_abs()) - cpu.z = (cpu.a & v) == 0 - cpu.n = (v & 0x80) != 0 - cpu.v = (v & 0x40) != 0 - -def _ASL_zp(cpu): - addr = cpu.addr_zp() - v = cpu.read8(addr) - cpu.c = (v & 0x80) != 0 - cpu.write8(addr, cpu.set_nz((v << 1) & 0xFF)) - -def _ASL_abs(cpu): - addr = cpu.addr_abs() - v = cpu.read8(addr) - cpu.c = (v & 0x80) != 0 - cpu.write8(addr, cpu.set_nz((v << 1) & 0xFF)) - -def _LSR_abs(cpu): - addr = cpu.addr_abs() - v = cpu.read8(addr) - cpu.c = (v & 0x01) != 0 - cpu.write8(addr, cpu.set_nz(v >> 1)) - -def _ROL_abs(cpu): - addr = cpu.addr_abs() - v = cpu.read8(addr) - new_c = (v & 0x80) != 0 - v = ((v << 1) | (1 if cpu.c else 0)) & 0xFF - cpu.write8(addr, cpu.set_nz(v)) - cpu.c = new_c - -def _ROR_abs(cpu): - addr = cpu.addr_abs() - v = cpu.read8(addr) - new_c = (v & 0x01) != 0 - v = ((v >> 1) | (0x80 if cpu.c else 0)) & 0xFF - cpu.write8(addr, cpu.set_nz(v)) - cpu.c = new_c - -def _ROR_zp(cpu): - addr = cpu.addr_zp() - v = cpu.read8(addr) - new_c = (v & 0x01) != 0 - v = ((v >> 1) | (0x80 if cpu.c else 0)) & 0xFF - cpu.write8(addr, cpu.set_nz(v)) - cpu.c = new_c - -def _STA_zpx(cpu_): cpu_.write8(cpu_.addr_zpx(), cpu_.a) -def _TSX(cpu): cpu.x = cpu.set_nz(cpu.sp) -def _TXS(cpu): cpu.sp = cpu.x -def _CLI(cpu): pass -def _CLV(cpu): cpu.v = False -def _SED(cpu): pass # decimal mode ignored -def _STX_zpy(cpu): - a = (cpu.read8(cpu.pc) + cpu.y) & 0xFF; cpu.pc = (cpu.pc + 1) & 0xFFFF - cpu.write8(a, cpu.x) -def _STY_zpx(cpu): cpu.write8(cpu.addr_zpx(), cpu.y) -def _LDX_zpy(cpu): - a = (cpu.read8(cpu.pc) + cpu.y) & 0xFF; cpu.pc = (cpu.pc + 1) & 0xFFFF - cpu.x = cpu.set_nz(cpu.read8(a)) -def _LDY_zpx(cpu): cpu.y = cpu.set_nz(cpu.read8(cpu.addr_zpx())) - -def _cmp_with(cpu, lhs, m): - r = (lhs - m) & 0x1FF - cpu.c = lhs >= m - cpu.set_nz(r & 0xFF) - -def _CMP_imm(cpu): _cmp_with(cpu, cpu.a, cpu.read8(cpu.addr_imm())) -def _CMP_zp(cpu): _cmp_with(cpu, cpu.a, cpu.read8(cpu.addr_zp())) -def _CMP_abs(cpu): _cmp_with(cpu, cpu.a, cpu.read8(cpu.addr_abs())) -def _CPX_imm(cpu): _cmp_with(cpu, cpu.x, cpu.read8(cpu.addr_imm())) -def _CPX_abs(cpu): _cmp_with(cpu, cpu.x, cpu.read8(cpu.addr_abs())) -def _CPY_imm(cpu): _cmp_with(cpu, cpu.y, cpu.read8(cpu.addr_imm())) - -def _ASL_a(cpu): - cpu.c = (cpu.a & 0x80) != 0 - cpu.a = cpu.set_nz((cpu.a << 1) & 0xFF) - -def _LSR_a(cpu): - cpu.c = (cpu.a & 0x01) != 0 - cpu.a = cpu.set_nz(cpu.a >> 1) - -def _LSR_zp(cpu): - addr = cpu.addr_zp() - v = cpu.read8(addr) - cpu.c = (v & 0x01) != 0 - cpu.write8(addr, cpu.set_nz(v >> 1)) - -def _ROL_a(cpu): - new_c = (cpu.a & 0x80) != 0 - cpu.a = cpu.set_nz(((cpu.a << 1) | (1 if cpu.c else 0)) & 0xFF) - cpu.c = new_c - -def _ROL_zp(cpu): - addr = cpu.addr_zp() - v = cpu.read8(addr) - new_c = (v & 0x80) != 0 - v = ((v << 1) | (1 if cpu.c else 0)) & 0xFF - cpu.write8(addr, cpu.set_nz(v)) - cpu.c = new_c - -def _ROR_a(cpu): - new_c = (cpu.a & 0x01) != 0 - cpu.a = cpu.set_nz(((cpu.a >> 1) | (0x80 if cpu.c else 0)) & 0xFF) - cpu.c = new_c - -def _INC_abs(cpu): - addr = cpu.addr_abs() - cpu.write8(addr, cpu.set_nz((cpu.read8(addr) + 1) & 0xFF)) - -def _INC_absx(cpu): - addr = cpu.addr_absx() - cpu.write8(addr, cpu.set_nz((cpu.read8(addr) + 1) & 0xFF)) - -def _INC_zp(cpu): - addr = cpu.addr_zp() - cpu.write8(addr, cpu.set_nz((cpu.read8(addr) + 1) & 0xFF)) - -def _INC_zpx(cpu): - addr = cpu.addr_zpx() - cpu.write8(addr, cpu.set_nz((cpu.read8(addr) + 1) & 0xFF)) - -def _DEC_abs(cpu): - addr = cpu.addr_abs() - cpu.write8(addr, cpu.set_nz((cpu.read8(addr) - 1) & 0xFF)) - -def _DEC_absx(cpu): - addr = cpu.addr_absx() - cpu.write8(addr, cpu.set_nz((cpu.read8(addr) - 1) & 0xFF)) - -def _DEC_zp(cpu): - addr = cpu.addr_zp() - cpu.write8(addr, cpu.set_nz((cpu.read8(addr) - 1) & 0xFF)) - -def _DEC_zpx(cpu): - addr = cpu.addr_zpx() - cpu.write8(addr, cpu.set_nz((cpu.read8(addr) - 1) & 0xFF)) - -def _ASL_zpx(cpu): - addr = cpu.addr_zpx() - v = cpu.read8(addr) - cpu.c = (v & 0x80) != 0 - cpu.write8(addr, cpu.set_nz((v << 1) & 0xFF)) - -def _ASL_absx(cpu): - addr = cpu.addr_absx() - v = cpu.read8(addr) - cpu.c = (v & 0x80) != 0 - cpu.write8(addr, cpu.set_nz((v << 1) & 0xFF)) - -def _LSR_zpx(cpu): - addr = cpu.addr_zpx() - v = cpu.read8(addr) - cpu.c = (v & 0x01) != 0 - cpu.write8(addr, cpu.set_nz(v >> 1)) - -def _LSR_absx(cpu): - addr = cpu.addr_absx() - v = cpu.read8(addr) - cpu.c = (v & 0x01) != 0 - cpu.write8(addr, cpu.set_nz(v >> 1)) - -def _ROL_zpx(cpu): - addr = cpu.addr_zpx() - v = cpu.read8(addr) - new_c = (v & 0x80) != 0 - v = ((v << 1) | (1 if cpu.c else 0)) & 0xFF - cpu.write8(addr, cpu.set_nz(v)) - cpu.c = new_c - -def _ROL_absx(cpu): - addr = cpu.addr_absx() - v = cpu.read8(addr) - new_c = (v & 0x80) != 0 - v = ((v << 1) | (1 if cpu.c else 0)) & 0xFF - cpu.write8(addr, cpu.set_nz(v)) - cpu.c = new_c - -def _ROR_zpx(cpu): - addr = cpu.addr_zpx() - v = cpu.read8(addr) - new_c = (v & 0x01) != 0 - v = ((v >> 1) | (0x80 if cpu.c else 0)) & 0xFF - cpu.write8(addr, cpu.set_nz(v)) - cpu.c = new_c - -def _ROR_absx(cpu): - addr = cpu.addr_absx() - v = cpu.read8(addr) - new_c = (v & 0x01) != 0 - v = ((v >> 1) | (0x80 if cpu.c else 0)) & 0xFF - cpu.write8(addr, cpu.set_nz(v)) - cpu.c = new_c - -def _LDA_indx(cpu): - zp = cpu.read8(cpu.pc); cpu.pc = (cpu.pc + 1) & 0xFFFF - addr_ptr = (zp + cpu.x) & 0xFF - addr = cpu.ram[addr_ptr] | (cpu.ram[(addr_ptr + 1) & 0xFF] << 8) - cpu.a = cpu.set_nz(cpu.read8(addr)) - -def _STA_indx(cpu): - zp = cpu.read8(cpu.pc); cpu.pc = (cpu.pc + 1) & 0xFFFF - addr_ptr = (zp + cpu.x) & 0xFF - addr = cpu.ram[addr_ptr] | (cpu.ram[(addr_ptr + 1) & 0xFF] << 8) - cpu.write8(addr, cpu.a) - -def _AND_indx(cpu): - zp = cpu.read8(cpu.pc); cpu.pc = (cpu.pc + 1) & 0xFFFF - addr_ptr = (zp + cpu.x) & 0xFF - addr = cpu.ram[addr_ptr] | (cpu.ram[(addr_ptr + 1) & 0xFF] << 8) - cpu.a = cpu.set_nz(cpu.a & cpu.read8(addr)) - -def _ORA_indx(cpu): - zp = cpu.read8(cpu.pc); cpu.pc = (cpu.pc + 1) & 0xFFFF - addr_ptr = (zp + cpu.x) & 0xFF - addr = cpu.ram[addr_ptr] | (cpu.ram[(addr_ptr + 1) & 0xFF] << 8) - cpu.a = cpu.set_nz(cpu.a | cpu.read8(addr)) - -def _EOR_indx(cpu): - zp = cpu.read8(cpu.pc); cpu.pc = (cpu.pc + 1) & 0xFFFF - addr_ptr = (zp + cpu.x) & 0xFF - addr = cpu.ram[addr_ptr] | (cpu.ram[(addr_ptr + 1) & 0xFF] << 8) - cpu.a = cpu.set_nz(cpu.a ^ cpu.read8(addr)) - -def _ADC_indx(cpu): - zp = cpu.read8(cpu.pc); cpu.pc = (cpu.pc + 1) & 0xFFFF - addr_ptr = (zp + cpu.x) & 0xFF - addr = cpu.ram[addr_ptr] | (cpu.ram[(addr_ptr + 1) & 0xFF] << 8) - _adc(cpu, cpu.read8(addr)) - -def _SBC_indx(cpu): - zp = cpu.read8(cpu.pc); cpu.pc = (cpu.pc + 1) & 0xFFFF - addr_ptr = (zp + cpu.x) & 0xFF - addr = cpu.ram[addr_ptr] | (cpu.ram[(addr_ptr + 1) & 0xFF] << 8) - _sbc(cpu, cpu.read8(addr)) - -def _CMP_indx(cpu): - zp = cpu.read8(cpu.pc); cpu.pc = (cpu.pc + 1) & 0xFFFF - addr_ptr = (zp + cpu.x) & 0xFF - addr = cpu.ram[addr_ptr] | (cpu.ram[(addr_ptr + 1) & 0xFF] << 8) - _cmp_with(cpu, cpu.a, cpu.read8(addr)) - -def _CPX_zp(cpu): - _cmp_with(cpu, cpu.x, cpu.read8(cpu.addr_zp())) - -def _CPY_zp(cpu): - _cmp_with(cpu, cpu.y, cpu.read8(cpu.addr_zp())) - -def _BPL(cpu): cpu.branch(not cpu.n) -def _BMI(cpu): cpu.branch(cpu.n) -def _BNE(cpu): cpu.branch(not cpu.z) -def _BEQ(cpu): cpu.branch(cpu.z) -def _BCC(cpu): cpu.branch(not cpu.c) -def _BCS(cpu): cpu.branch(cpu.c) -def _BVC(cpu): cpu.branch(not cpu.v) -def _BVS(cpu): cpu.branch(cpu.v) - -def _JMP_abs(cpu): cpu.pc = cpu.addr_abs() -def _JSR(cpu): - target = cpu.addr_abs() - cpu.push16((cpu.pc - 1) & 0xFFFF) - cpu.pc = target -def _RTS(cpu): - cpu.pc = (cpu.pop16() + 1) & 0xFFFF - - -# Build dispatch table -_DISPATCH = { - 0x00: _NOP, # treat BRK as NOP (we never expect it) - 0x18: _CLC, 0x38: _SEC, 0x58: _NOP, 0x78: _SEI, 0xD8: _CLD, - 0x08: _PHP, 0x28: _PLP, 0x48: _PHA, 0x68: _PLA, - 0x8A: _TXA, 0xAA: _TAX, 0x98: _TYA, 0xA8: _TAY, - 0xE8: _INX, 0xC8: _INY, 0xCA: _DEX, 0x88: _DEY, - 0xA9: _LDA_imm, 0xA5: _LDA_zp, 0xB5: _LDA_zpx, 0xAD: _LDA_abs, - 0xBD: _LDA_absx, 0xB9: _LDA_absy, 0xB1: _LDA_indy, - 0xA2: _LDX_imm, 0xAE: _LDX_abs, 0xBE: _LDX_absy, 0xA6: _LDX_zp, - 0xA0: _LDY_imm, 0xAC: _LDY_abs, 0xBC: _LDY_absx, 0xA4: _LDY_zp, - 0x85: _STA_zp, 0x95: _STA_zpx, 0x8D: _STA_abs, - 0x9D: _STA_absx, 0x99: _STA_absy, 0x91: _STA_indy, - 0x86: _STX_zp, 0x8E: _STX_abs, - 0x84: _STY_zp, 0x8C: _STY_abs, - 0x29: _AND_imm, 0x25: _AND_zp, 0x2D: _AND_abs, - 0x3D: _AND_absx, 0x39: _AND_absy, 0x31: _AND_indy, - 0x09: _ORA_imm, 0x05: _ORA_zp, 0x0D: _ORA_abs, 0x15: _ORA_zpx, - 0x1D: _ORA_absx, 0x11: _ORA_indy, - 0x49: _EOR_imm, 0x4D: _EOR_abs, 0x45: _EOR_zp, 0x55: _EOR_zpx, - 0x5D: _EOR_absx, 0x59: _EOR_absy, - 0x69: _ADC_imm, 0x65: _ADC_zp, 0x6D: _ADC_abs, 0x79: _ADC_absy, - 0x75: _ADC_zpx, 0x7D: _ADC_absx, - 0xE9: _SBC_imm, 0xE5: _SBC_zp, 0xED: _SBC_abs, 0xFD: _SBC_absx, 0xF9: _SBC_absy, - 0xC9: _CMP_imm, 0xC5: _CMP_zp, 0xCD: _CMP_abs, 0xD5: _CMP_zpx, - 0xDD: _CMP_absx, 0xD9: _CMP_absy, - 0xE0: _CPX_imm, 0xEC: _CPX_abs, - 0xC0: _CPY_imm, - 0x24: _BIT_zp, 0x2C: _BIT_abs, - 0x0A: _ASL_a, 0x06: _ASL_zp, 0x0E: _ASL_abs, - 0x4A: _LSR_a, 0x46: _LSR_zp, 0x4E: _LSR_abs, - 0x2A: _ROL_a, 0x26: _ROL_zp, 0x2E: _ROL_abs, - 0x6A: _ROR_a, 0x66: _ROR_zp, 0x6E: _ROR_abs, - 0xEE: _INC_abs, 0xFE: _INC_absx, 0xE6: _INC_zp, 0xF6: _INC_zpx, - 0xCE: _DEC_abs, 0xDE: _DEC_absx, 0xC6: _DEC_zp, 0xD6: _DEC_zpx, - 0x16: _ASL_zpx, 0x1E: _ASL_absx, - 0x56: _LSR_zpx, 0x5E: _LSR_absx, - 0x36: _ROL_zpx, 0x3E: _ROL_absx, - 0x76: _ROR_zpx, 0x7E: _ROR_absx, - 0xA1: _LDA_indx, 0x81: _STA_indx, 0x21: _AND_indx, - 0x01: _ORA_indx, 0x41: _EOR_indx, - 0x61: _ADC_indx, 0xE1: _SBC_indx, 0xC1: _CMP_indx, - 0xE4: _CPX_zp, 0xC4: _CPY_zp, - 0x6C: _JMP_ind, - 0x9A: _TXS, 0xBA: _TSX, - 0x58: _CLI, 0xB8: _CLV, 0xF8: _SED, - 0x96: _STX_zpy, 0x94: _STY_zpx, - 0xB6: _LDX_zpy, 0xB4: _LDY_zpx, - 0x10: _BPL, 0x30: _BMI, 0xD0: _BNE, 0xF0: _BEQ, - 0x90: _BCC, 0xB0: _BCS, 0x50: _BVC, 0x70: _BVS, - 0x4C: _JMP_abs, 0x20: _JSR, 0x60: _RTS, - 0xEA: _NOP, -} diff --git a/examples/spacetaxi/assets/depackSpeechRam.py b/examples/spacetaxi/assets/depackSpeechRam.py deleted file mode 100755 index 22752b0..0000000 --- a/examples/spacetaxi/assets/depackSpeechRam.py +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env python3 -"""Run Space Taxi's depacker to produce a FLAT 64K RAM image. - -Why this exists: the disk's "SPACE TAXI+" is a crunched single file (it -matches the running game's memory in 0.6% of bytes), so the speech -samples cannot be read out of it statically. And a VICE memory dump is -no good either -- it captures the banked-IN view, so $A000-$BFFF comes -back as BASIC ROM and 13 of the 15 utterances are missing. Every -mem0000*.bin in this tree has that problem. - -The way out needs no emulator install: cpu6502.py (next to this script) is a -plain 6502 core over a flat 64K bytearray with no banking and no I/O, -which is all a depacker needs. Load the .prg, run from the BASIC SYS -entry, and the depacked image IS the RAM -- $A000-$BFFF included. - -Feed the result to extractSpeech.py. - -Usage: depackSpeechRam.py [spaceTaxiPlus.prg] [out.bin] - Defaults resolve inside stuff/spacetaxi/ (gitignored research - area, same as genC64Data.py's inputs). -""" - -import os -import sys - -HERE = os.path.dirname(os.path.abspath(__file__)) -STUFF = os.path.join(HERE, "..", "..", "..", "stuff", "spacetaxi") - -# The BASIC stub is `0B08 D307 9E "2059" 00` -- SYS 2059. -ENTRY = 0x080B -MAX_STEPS = 60000000 -CHECK_MASK = 0xFFFF # how often to test for completion - -# Completion test: the $9B00 index table is populated and the first -# utterance's $FE lead-in is in place. -TABLE_ADDR = 0x9B00 -FIRST_UTT = 0x9B40 - - -def main(): - prgPath = sys.argv[1] if len(sys.argv) > 1 else os.path.join(STUFF, "extract", "SPACE TAXI+.prg") - outPath = sys.argv[2] if len(sys.argv) > 2 else os.path.join(STUFF, "staxi-ram.bin") - - sys.path.insert(0, HERE) - from cpu6502 import Cpu6502 - - prg = open(prgPath, "rb").read() - load = prg[0] | (prg[1] << 8) - ram = bytearray(65536) - ram[load:load + len(prg) - 2] = prg[2:] - print("loaded %s at $%04X..$%04X" % (os.path.basename(prgPath), load, load + len(prg) - 3)) - - cpu = Cpu6502(ram) - cpu.pc = ENTRY - steps = 0 - done = False - try: - while steps < MAX_STEPS: - cpu.step() - steps += 1 - if (steps & CHECK_MASK) == 0: - if ram[TABLE_ADDR] != 0 and ram[FIRST_UTT] == 0xFE: - done = True - break - except Exception as exc: - # A depacker that runs off into I/O or an undocumented opcode - # still usually gets the data down first, so report and carry on - # to the write rather than throwing the work away. - print("6502 stopped after %d steps at PC=$%04X: %s" % (steps, cpu.pc, exc)) - - open(outPath, "wb").write(bytes(ram)) - print("%d steps, depack %s -> %s" % (steps, "detected" if done else "NOT detected", outPath)) - if not done: - print("WARNING: completion check never fired; the image may be partial.") - - -if __name__ == "__main__": - main() diff --git a/examples/spacetaxi/assets/dumpScenes.py b/examples/spacetaxi/assets/dumpScenes.py deleted file mode 100644 index 77a32be..0000000 --- a/examples/spacetaxi/assets/dumpScenes.py +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env python3 -# Drive the captured Space Taxi RAM through the 6502 emulator to -# decompress every scene the game knows how to load. -# -# Approach: -# 1. Load a VICE memory dump as flat 64KB RAM. The dump captured at -# runtime contains the decompressor at $9656, the scene pointer -# table at $9600, and (we hope) the compressed data the pointers -# reach into. -# 2. Sanity check: run scene ID 1 (the captured live level) through -# $6283 and compare the resulting screen buffer at $7530-$7929 -# with the live screen RAM at $0400-$07E7 already in the dump. -# If they match, the emulator + dump together can reproduce any -# scene. If not, we know something's missing (likely hidden RAM -# under ROMs that VICE didn't capture). -# 3. If sanity passes: iterate scene IDs 0..23 (lettered levels) -# plus known special IDs (title=$24, level-complete=$19) and -# dump each scene's screen + color buffer. - -import sys -import os -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) - -from cpu6502 import Cpu6502 - - -def load_dump(path): - with open(path, 'rb') as f: - raw = f.read() - if len(raw) == 0x10000: - return bytearray(raw) - if len(raw) == 0x10002: - # 2-byte LE load-addr header - return bytearray(raw[2:]) - raise SystemExit(f'{path}: unexpected dump size {len(raw)}') - - -# Memory map landmarks (from MECHANICS.md): -ADDR_DECOMP_ENTRY = 0x6283 # bank-switch wrapper around $9656 -SCREEN_BUF_BASE = 0x7530 # decompressed screen RAM target -SCREEN_BUF_SIZE = 0x400 # 4 pages = 1024 bytes -COLOR_BUF_BASE = 0x7918 # decompressed color RAM target -COLOR_BUF_SIZE = 0x400 - - -def run_scene(ram, scene_id): - """Run $6283(X=scene_id) on a fresh copy of `ram`. Returns a tuple - (screen_buf, color_buf, final_ram) so the caller can compare or - examine state.""" - work = bytearray(ram) # copy so callers can keep the original - cpu = Cpu6502(work) - cpu.a = scene_id - cpu.x = scene_id - cpu.y = 0 - cpu.call(ADDR_DECOMP_ENTRY) - screen = bytes(work[SCREEN_BUF_BASE:SCREEN_BUF_BASE + SCREEN_BUF_SIZE]) - color = bytes(work[COLOR_BUF_BASE:COLOR_BUF_BASE + COLOR_BUF_SIZE]) - return screen, color, work - - -def compare_screen(decomp_screen, live_screen, label): - """Compare 1024-byte decompressed screen vs live screen RAM. - Only the first 1000 bytes are visible (40x25). Bytes 1000..1023 - are unused tail / overlap with color RAM, so we ignore them.""" - a = decomp_screen[:1000] - b = live_screen[:1000] - if a == b: - print(f' {label}: MATCH') - return True - # Show how many cells differ and a sample - diffs = [(i, a[i], b[i]) for i in range(1000) if a[i] != b[i]] - print(f' {label}: {len(diffs)} / 1000 cells differ') - for i, av, bv in diffs[:6]: - row, col = divmod(i, 40) - print(f' cell ({col:2d},{row:2d}): decomp=${av:02X} live=${bv:02X}') - return False - - -def main(): - if len(sys.argv) < 2: - print('usage: dumpScenes.py [scene-id]', file=sys.stderr) - sys.exit(2) - ram = load_dump(sys.argv[1]) - - # Figure out what the captured state is, then pick the scene to - # test against. $7215 = current level number (0..23 = A..X), - # but the title screen / specials don't update it -- the - # most-recent JSR $6283 used scene $24 (title) or $19 (other - # special). Default to the level-number scene, but allow override. - state_level = ram[0x7215] - if len(sys.argv) >= 3: - scene = int(sys.argv[2], 0) - else: - # Heuristic: if state sentinel $07A6 says we're on the title, - # the live screen matches scene 36 ($24). Otherwise we're - # mid-gameplay and the live screen matches scene = level. - # Title state in this game: top-left tile is the inner frame - # corner $5D / $5C. For now just try scene 36 then scene N. - scene = 36 - print(f'=== Sanity test: scene {scene} (${scene:02X}) vs live screen RAM ===') - print(f' captured state: $7215={state_level} $07A6=${ram[0x07A6]:02X}') - try: - scr, col, _ = run_scene(ram, scene) - except Exception as e: - print(f' emulator raised: {type(e).__name__}: {e}') - return - live_scr = bytes(ram[0x0400:0x0400 + 1024]) - live_col = bytes(b & 0x0F for b in ram[0xD800:0xD800 + 1024]) - compare_screen(scr, live_scr, 'screen') - masked_col = bytes(b & 0x0F for b in col) - compare_screen(masked_col, live_col, 'color ') - - -if __name__ == '__main__': - main() diff --git a/examples/spacetaxi/assets/extractSpeech.py b/examples/spacetaxi/assets/extractSpeech.py deleted file mode 100755 index e666e7f..0000000 --- a/examples/spacetaxi/assets/extractSpeech.py +++ /dev/null @@ -1,225 +0,0 @@ -#!/usr/bin/env python3 -"""Extract Space Taxi's digitised speech from a C64 memory dump. - -FORMAT (reverse-engineered from bankedSfxLoad at $9802, see -stuff/spacetaxi/live-level1.lst). It is NOT 4-bit $D418 digi and it is -NOT the SID speech synth. It is 1-BIT RUN-LENGTH audio: - - * An index table at $9B00 holds [code, ptrLo, ptrHi] triples, - terminated by a zero code. `code` is the PETSCII character the game - asks for -- 'H' 'T' '!' spell "Hey Taxi!", 'P' + digit + '?' spell - "Pad two, please", 'U' is "up". - * Each utterance is a byte stream. For every byte the player busy-waits - that many passes of a fixed delay loop and then TOGGLES bit 0 of - $D417, which flips voice 1 in and out of the filter -- a 1-bit - speaker. So each byte is the length of one half-cycle. - * $FE is a raster sync (the player waits on $D011), $FF ends the - utterance. - - The delay loop at $98A1..$98AA costs 27 cycles per pass and the - toggle/advance/fetch tail ($98AC..$987F) another 33, hence CYCLES(). - -LIMITATION: a plain VICE memory dump captures the BANKED-IN view, so -$A000-$BFFF comes back as BASIC ROM ("CBMBASIC" at $A004) and most of the -speech is missing -- the player banks BASIC out ($01 = $06) precisely -because the samples live in the RAM underneath. Dump it with the ROMs -banked out, e.g. in the VICE monitor: - - bank ram - save "staxi-ram.bin" 0 0000 ffff - -Usage: extractSpeech.py [outDir] - The dump may have a 2-byte load address or not; both are handled. -""" - -import os -import struct -import sys -import wave - -NTSC_HZ = 1022727.0 # the port already uses this (ST_SID_HZ_NUM) -OUT_RATE = 22050 -TABLE_ADDR = 0x9B00 -END_MARK = 0xFF -SYNC_MARK = 0xFE - - -def cyclesFor(count): - """Half-cycle length in CPU cycles for one stream byte.""" - return 27 * count + 33 - - -def loadDump(path): - """Return a 64K-addressable bytes object from a dump with or without - a load-address header.""" - raw = open(path, "rb").read() - if len(raw) in (65538, 65536 + 2): - return raw[2:] - if len(raw) == 65536: - return raw - # Headerless dump starting at $0801 (what mem0801.prg turned out to be). - pad = bytearray(65536) - pad[0x0801:0x0801 + len(raw)] = raw[:65536 - 0x0801] - return bytes(pad) - - -def readTable(mem): - """[(code, pointer)] from the $9B00 index.""" - out = [] - y = 0 - while y < 0x300: - code = mem[TABLE_ADDR + y] - if code == 0 or code == 0xFF: - break - out.append((code, mem[TABLE_ADDR + y + 1] | (mem[TABLE_ADDR + y + 2] << 8))) - y += 3 - return out - - -def decode(mem, ptr, limit=0x4000): - """Return (runs, byteCount, sawEnd). runs is [(level, cycles)].""" - runs = [] - level = 0 - addr = ptr - while addr - ptr < limit: - b = mem[addr] - addr += 1 - if b == END_MARK: - return runs, addr - ptr, True - if b == SYNC_MARK: - runs.append((level, int(NTSC_HZ / 60.0))) - continue - runs.append((level, cyclesFor(b))) - level ^= 1 - return runs, addr - ptr, False - - -def writeWav(runs, path): - samples = bytearray() - carry = 0.0 - perOut = NTSC_HZ / OUT_RATE - for level, cyc in runs: - carry += cyc - n = int(carry / perOut) - carry -= n * perOut - samples += struct.pack("= romLo: - continue - runs, nbytes, sawEnd = decode(mem, ptr) - nxt = next((q for _, q in sorted(table, key=lambda e: e[1]) if q > ptr), None) - if not sawEnd or (nxt is not None and nbytes > nxt - ptr): - continue - index.append((code, sum(len(b) for b in blobs), nbytes)) - blobs.append(bytes(mem[ptr:ptr + nbytes])) - data = b"".join(blobs) - - h = os.path.join(outDir, "stSpeechData.h") - c = os.path.join(outDir, "stSpeechData.c") - with open(h, "w") as fp: - fp.write("// Generated by assets/extractSpeech.py. Do not hand-edit.\n" - "//\n" - "// Space Taxi's digitised speech as the C64 stores it: 1-bit\n" - "// run-length streams. Each byte is one half-cycle length; the\n" - "// player toggles its output after each. $FE is a raster sync\n" - "// and $FF ends an utterance. stAudio.c expands these to PCM\n" - "// through the jlAudioPlaySfxStream fill callback.\n\n" - "#ifndef ST_SPEECH_DATA_H\n#define ST_SPEECH_DATA_H\n\n" - "#include \n\n" - "typedef struct {\n" - " uint8_t code; // PETSCII: 'H' 'T' '!' 'P' '1'..'9' '?' 'U'\n" - " uint16_t offset; // into kStSpeechRle\n" - " uint16_t length;\n" - "} StSpeechEntryT;\n\n" - "#define ST_SPEECH_COUNT %du\n" - "#define ST_SPEECH_RLE_BYTES %du\n\n" - "extern const StSpeechEntryT kStSpeechIndex[ST_SPEECH_COUNT];\n" - "extern const uint8_t kStSpeechRle[ST_SPEECH_RLE_BYTES];\n\n" - "#endif\n" % (len(index), len(data))) - with open(c, "w") as fp: - fp.write('// Generated by assets/extractSpeech.py. Do not hand-edit.\n\n' - '#include "stSpeechData.h"\n\n' - 'const StSpeechEntryT kStSpeechIndex[ST_SPEECH_COUNT] = {\n') - for code, off, ln in index: - fp.write(" { 0x%02Xu, %5du, %5du }, // '%s'\n" - % (code, off, ln, chr(code) if 33 <= code < 127 else "?")) - fp.write("};\n\nconst uint8_t kStSpeechRle[ST_SPEECH_RLE_BYTES] = {\n") - for i in range(0, len(data), 16): - fp.write(" " + " ".join("0x%02X," % b for b in data[i:i + 16]) + "\n") - fp.write("};\n") - print("\nemitted %s (%d entries) and %s (%d bytes)" % (h, len(index), c, len(data))) - - -def main(): - if len(sys.argv) < 2: - sys.exit(__doc__) - emit = "--c" in sys.argv - args = [a for a in sys.argv[1:] if a != "--c"] - mem = loadDump(args[0]) - outDir = args[1] if len(args) > 1 else "speech" - os.makedirs(outDir, exist_ok=True) - - if mem[0xA004:0xA00C] == b"CBMBASIC": - sys.stderr.write("WARNING: $A000 holds BASIC ROM, not the RAM under it.\n" - " Utterances at $A000 and above will not decode.\n" - " Re-dump with `bank ram` (see the module docstring).\n\n") - - table = readTable(mem) - romLo = 0xA000 if mem[0xA004:0xA00C] == b"CBMBASIC" else 0x10000 - ordered = sorted(p for _, p in table) - good = 0 - - print("%-4s %-6s %7s %7s %s" % ("code", "ptr", "bytes", "dur(s)", "file")) - for code, ptr in table: - label = chr(code) if 33 <= code < 127 else "?" - # Utterances are stored back to back, so the next pointer is the - # expected end. Both checks matter: BASIC ROM is full of $FF, so - # "found a terminator" alone happily reports success on garbage -- - # it claimed all 15 decoded from a ROM-shadowed dump. - nxt = next((q for q in ordered if q > ptr), None) - expected = (nxt - ptr) if nxt else None - if ptr >= romLo: - print("%-4s $%04X %7s %7s -- SKIPPED (shadowed by BASIC ROM in this dump)" - % (label, ptr, "-", "-")) - continue - runs, nbytes, sawEnd = decode(mem, ptr) - if not sawEnd: - print("%-4s $%04X %7d %7s -- SKIPPED (no $FF terminator)" % (label, ptr, nbytes, "-")) - continue - # The terminator must land BEFORE the next utterance starts. - # (Entries are back to back but may carry a pad byte, so this is - # <= rather than ==.) Overrunning means the $FF we found was not - # ours -- which is exactly what happens against a ROM shadow. - if expected is not None and nbytes > expected: - print("%-4s $%04X %7d %7s -- SKIPPED (ran %d bytes, next entry is %d away)" - % (label, ptr, nbytes, "-", nbytes, expected)) - continue - path = os.path.join(outDir, "%02X_%s.wav" % (code, label if label != "?" else "x")) - dur = writeWav(runs, path) - good += 1 - print("%-4s $%04X %7d %7.2f %s" % (label, ptr, nbytes, dur, os.path.basename(path))) - print("\n%d of %d utterances decoded into %s" % (good, len(table), outDir)) - if emit: - emitC(mem, table, romLo, outDir) - - -if __name__ == "__main__": - main() diff --git a/examples/spacetaxi/assets/genC64Data.py b/examples/spacetaxi/assets/genC64Data.py deleted file mode 100644 index 76b25bf..0000000 --- a/examples/spacetaxi/assets/genC64Data.py +++ /dev/null @@ -1,192 +0,0 @@ -#!/usr/bin/env python3 -# genC64Data.py -- emit the C64 game data the JoeyLib Space Taxi port -# needs at runtime as C source: the custom charset (rendering AND the -# exact sprite-to-background collision bits), the standard sprite -# bitmaps (cab, passenger, flame, warp, death, intro star, leaving -# cels), the demo-mode RNG table ($446A), the SID SFX programs (the -# 9-byte blocks fed to $42E9) and the engine-flame pointer table ($6DB0). -# (The level-intro star velocity tables, $450C/$4513, went with the moving -# warp; claudeDocs/spacetaxiSpriteWarp.patch puts them back.) -# -# Everything comes straight out of stuff/spacetaxi/raw.bin (a VICE -# "bank ram" dump, 2-byte load-address header) so the port has one -# source of truth: the original bytes. Output: -# examples/spacetaxi/stC64Data.c / stC64Data.h (committed, do not edit) - -import os -import sys - -HERE = os.path.dirname(os.path.abspath(__file__)) -RAW = os.path.join(HERE, "..", "..", "..", "stuff", "spacetaxi", "raw.bin") -OUT_C = os.path.join(HERE, "..", "stC64Data.c") -OUT_H = os.path.join(HERE, "..", "stC64Data.h") - -SPRITE_PTR_FIRST = 0xC0 -SPRITE_PTR_LAST = 0xEC # $E9-$EC = passengers-leaving cels -SPRITE_BYTES = 63 - -# (enum name, address of the 9-byte program, comment) -SFX = [ - ("ST_SFX_GEAR", 0x6410, "landing gear toggle ($63DD)"), - ("ST_SFX_CASH", 0x6DBB, "refuel ka-ching ($6E5B)"), - ("ST_SFX_FUEL_LOW", 0x6DC4, "fuel below 3 cells ($6EBD)"), - ("ST_SFX_FUEL_FULL", 0x6DCD, "tank full ($6E9E)"), - ("ST_SFX_LAND_HARD", 0x6DD6, "hard landing ($6520)"), - ("ST_SFX_IMPACT", 0x6DDF, "wreck hits the floor ($6AE1)"), - ("ST_SFX_LAND_SOFT", 0x6DE8, "soft landing ($650F)"), - ("ST_SFX_CRASH", 0x6DF1, "crash start ($6A56)"), -] - - -def load_raw(): - with open(RAW, "rb") as fp: - raw = fp.read() - if len(raw) == 0x10002: - raw = raw[2:] - if len(raw) != 0x10000: - sys.exit(f"{RAW}: unexpected size {len(raw)}") - return raw - - -def c_bytes(data, per_line=16, indent=" "): - lines = [] - for i in range(0, len(data), per_line): - chunk = data[i:i + per_line] - lines.append(indent + ", ".join(f"0x{b:02X}" for b in chunk) + ",") - return "\n".join(lines) - - -# The cross-segment accessors the IIgs needs (see the block this emits -# into stC64Data.c). These used to be hand-added to the GENERATED file, -# which meant re-running this script silently deleted them and broke the -# IIgs build -- the header kept declaring them and nothing defined them. -# Prototype and return expression per accessor; the header prints the -# prototypes aligned, so keep the spacing. -ACCESSORS = [ - ("const uint8_t *stC64Charset(void);", "&kStCharset[0][0]"), - ("const uint8_t *stC64SpriteBitmap(uint8_t idx);", "kStSpriteBitmaps[idx]"), - ("uint8_t stC64RngByte(uint8_t idx);", "kStRngTable[idx]"), - ("const uint8_t *stC64SfxProgram(uint8_t idx);", "kStSfxPrograms[idx]"), - ("uint8_t stC64FlameCel(uint8_t dirMask);", "kStFlameCelByDirMask[dirMask]"), -] - - -def main(): - raw = load_raw() - sprite_count = SPRITE_PTR_LAST - SPRITE_PTR_FIRST + 1 - - h = [] - h.append("// Generated by assets/genC64Data.py from stuff/spacetaxi/raw.bin.") - h.append("// Do not hand-edit; re-run the generator.") - h.append("") - h.append("#ifndef ST_C64_DATA_H") - h.append("#define ST_C64_DATA_H") - h.append("") - h.append("#include ") - h.append("") - h.append("// The game's custom character set ($2800-$2FFF): 256 glyphs x 8 rows,") - h.append("// bit 7 = leftmost pixel. Used to paint the screen AND for the") - h.append("// sprite-to-background collision test (a set bit is background data).") - h.append("#define ST_CHARSET_CHARS 256u") - h.append("extern const uint8_t kStCharset[ST_CHARSET_CHARS][8];") - h.append("") - h.append("// Standard sprite bitmaps (VIC bank 0, block ptr * 64). 21 rows x 3") - h.append("// bytes. Index = ptr - ST_SPRITE_PTR_FIRST.") - h.append(f"#define ST_SPRITE_PTR_FIRST 0x{SPRITE_PTR_FIRST:02X}u") - h.append(f"#define ST_SPRITE_PTR_LAST 0x{SPRITE_PTR_LAST:02X}u") - h.append(f"#define ST_SPRITE_COUNT {sprite_count}u") - h.append(f"#define ST_SPRITE_BYTES {SPRITE_BYTES}u") - h.append("extern const uint8_t kStSpriteBitmaps[ST_SPRITE_COUNT][ST_SPRITE_BYTES];") - h.append("") - h.append("// Demo-mode RNG lookup ($446A): rnd = table[t1] + t2 (see stSim.c).") - h.append("#define ST_RNG_TABLE_SIZE 64u") - h.append("extern const uint8_t kStRngTable[ST_RNG_TABLE_SIZE];") - h.append("") - h.append("// SID SFX programs, 9 bytes each, in $42E9 order: freq lo, freq hi,") - h.append("// pulse lo, pulse hi, control, AD, SR, release ticks, voice index.") - h.append("typedef enum {") - for name, addr, comment in SFX: - h.append(f" {name}, // ${addr:04X} {comment}") - h.append(" ST_SFX_COUNT") - h.append("} StSfxE;") - h.append("#define ST_SFX_PROGRAM_BYTES 9u") - h.append("extern const uint8_t kStSfxPrograms[ST_SFX_COUNT][ST_SFX_PROGRAM_BYTES];") - h.append("") - h.append("// Engine-flame cel pointer by direction mask ($6DB0, 1=UP 2=DOWN 4=LEFT") - h.append("// 8=RIGHT); 0 = no cel for that combination.") - h.append("extern const uint8_t kStFlameCelByDirMask[16];") - h.append("") - h.append("// Cross-segment-safe accessors (see stC64Data.c): reach the tables above") - h.append("// by an inter-segment call on the IIgs, where a direct 2-byte data") - h.append("// reference from another bank is not relocatable.") - for decl in ACCESSORS: - h.append(decl[0]) - h.append("") - h.append("#endif") - - c = [] - c.append("// Generated by assets/genC64Data.py from stuff/spacetaxi/raw.bin.") - c.append("// Do not hand-edit; re-run the generator.") - c.append("") - c.append('#include "stC64Data.h"') - c.append("") - c.append("const uint8_t kStCharset[ST_CHARSET_CHARS][8] = {") - for ch in range(256): - rows = raw[0x2800 + ch * 8:0x2800 + ch * 8 + 8] - c.append(" { " + ", ".join(f"0x{b:02X}" for b in rows) + " }, // $" + f"{ch:02X}") - c.append("};") - c.append("") - c.append("const uint8_t kStSpriteBitmaps[ST_SPRITE_COUNT][ST_SPRITE_BYTES] = {") - for ptr in range(SPRITE_PTR_FIRST, SPRITE_PTR_LAST + 1): - base = ptr * 64 - c.append(f" {{ // ptr ${ptr:02X}") - for row in range(21): - b = raw[base + row * 3:base + row * 3 + 3] - c.append(" " + ", ".join(f"0x{x:02X}" for x in b) + ",") - c.append(" },") - c.append("};") - c.append("") - c.append("const uint8_t kStRngTable[ST_RNG_TABLE_SIZE] = {") - c.append(c_bytes(raw[0x446A:0x446A + 64])) - c.append("};") - c.append("") - c.append("const uint8_t kStSfxPrograms[ST_SFX_COUNT][ST_SFX_PROGRAM_BYTES] = {") - for name, addr, comment in SFX: - b = raw[addr:addr + 9] - c.append(" { " + ", ".join(f"0x{x:02X}" for x in b) + " }, // " + name) - c.append("};") - c.append("") - flame = list(raw[0x6DB0:0x6DB0 + 11]) + [0] * 5 - c.append("const uint8_t kStFlameCelByDirMask[16] = {") - c.append(c_bytes(flame)) - c.append("};") - c.append("") - c.append("") - c.append("// ---------------------------------------------------------------------------") - c.append("// Accessors. On the IIgs the program is split across banks and a 2-byte") - c.append("// data reference cannot cross a segment (only a 3-byte JSL/JML can). These") - c.append("// tiny functions live in the same segment as the tables, so their reads") - c.append("// are intra-segment; callers in other segments reach them by a supported") - c.append("// inter-segment call. On the other ports they inline away to nothing.") - c.append("// ---------------------------------------------------------------------------") - first = True - for decl, body in ACCESSORS: - c.append("") - if not first: - c.append("") - first = False - # The header pads return types to align its prototypes; the - # definitions here must not inherit that padding. - c.append(" ".join(decl[:-1].split()) + " {") - c.append(" return " + body + ";") - c.append("}") - - with open(OUT_H, "w", encoding="ascii") as fp: - fp.write("\n".join(h) + "\n") - with open(OUT_C, "w", encoding="ascii") as fp: - fp.write("\n".join(c) + "\n") - print(f"wrote {OUT_H} and {OUT_C}") - - -if __name__ == "__main__": - main() diff --git a/examples/spacetaxi/assets/genDemoStreams.py b/examples/spacetaxi/assets/genDemoStreams.py deleted file mode 100644 index 5095393..0000000 --- a/examples/spacetaxi/assets/genDemoStreams.py +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env python3 -# Embed the C64 attract-demo recorded input streams (the four -# "SPACETAXI 0x" disk files -- RLE [joystick mask][tick count] pairs; -# mask bit0=UP bit1=DOWN bit2=LEFT bit3=RIGHT bit4=FIRE, bit7 always -# set) as C arrays for stDemoStreams.h. -# -# Every stream file is odd-length: the $9936 saver appends one -# dangling byte past the last complete pair. It is kept: the C64 reads -# it (and whatever follows in the buffer) when a ride outlasts its -# recording. -# -# Rotation order is H -> W -> T -> X ($6FE2/$50C1 trace) with file -# mapping H=01, T=02, W=03, X=04 and demo levels H=8, W=23, T=20, -# X=24 (1-based). - -import os - -HERE = os.path.dirname(os.path.abspath(__file__)) -EXTRACT = os.path.join(HERE, "..", "..", "..", "stuff", "spacetaxi", "extract") -OUT = os.path.join(HERE, "..", "stDemoStreams.h") - -# (file, symbol, 0-based level index, screen letter) in ROTATION order. -STREAMS = [ - ("SPACETAXI 01_N0S.prg", "kDemoStreamH", 7, "H"), - ("SPACETAXI 03_N0S.prg", "kDemoStreamW", 22, "W"), - ("SPACETAXI 02_N0S.prg", "kDemoStreamT", 19, "T"), - ("SPACETAXI 04_N0S.prg", "kDemoStreamX", 23, "X"), -] - - -def main(): - with open(OUT, "w", encoding="ascii") as f: - f.write("// Generated by assets/genDemoStreams.py -- do not hand-edit.\n") - f.write("// C64 attract-demo recorded input streams (see the generator\n") - f.write("// for format notes). Rotation order H, W, T, X.\n\n") - f.write("#ifndef ST_DEMO_STREAMS_H\n#define ST_DEMO_STREAMS_H\n\n") - f.write("#include \n\n") - entries = [] - for fname, sym, level, letter in STREAMS: - data = open(os.path.join(EXTRACT, fname), "rb").read() - # The whole file, dangling byte included: the C64 plays past - # the last pair into whatever the buffer held before. - f.write(f"// {letter}: {fname} ({len(data)} bytes)\n") - f.write(f"static const uint8_t {sym}[{len(data)}] = {{\n") - for i in range(0, len(data), 12): - row = ", ".join(f"0x{b:02X}" for b in data[i:i + 12]) - f.write(f" {row},\n") - f.write("};\n\n") - entries.append((sym, len(data), level)) - # The playback buffer at $0902 as the boot leaves it (demo H - # loaded over older RAM): recordings shorter than the previous one - # leave its tail in place, and the game reads on into it. - raw = open(os.path.join(HERE, "..", "..", "..", "stuff", "spacetaxi", "raw.bin"), "rb").read()[2:] - buf = raw[0x0902:0x0902 + 640] - f.write("#define ST_DEMO_BUFFER_BYTES 640u\n") - f.write("static const uint8_t kDemoBufferInit[ST_DEMO_BUFFER_BYTES] = {\n") - for i in range(0, len(buf), 12): - row = ", ".join(f"0x{b:02X}" for b in buf[i:i + 12]) - f.write(f" {row},\n") - f.write("};\n\n") - f.write("typedef struct {\n") - f.write(" const uint8_t *stream;\n") - f.write(" uint16_t length;\n") - f.write(" uint8_t levelIndex; // 0-based\n") - f.write("} StDemoEntryT;\n\n") - f.write(f"static const StDemoEntryT kDemoRotation[{len(entries)}] = {{\n") - for sym, ln, level in entries: - f.write(f" {{ {sym}, {ln}u, {level}u }},\n") - f.write("};\n\n#endif\n") - print(f"wrote {OUT}") - - -if __name__ == "__main__": - main() diff --git a/examples/spacetaxi/assets/genSongData.py b/examples/spacetaxi/assets/genSongData.py deleted file mode 100644 index 8610eab..0000000 --- a/examples/spacetaxi/assets/genSongData.py +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env python3 -# Bake examples/spacetaxi/assets/songs/song*.song into stSongData.h. -# -# python3 examples/spacetaxi/assets/genSongData.py -# -# The .song sources come from the game's own player run under a 6502 -# emulator: assets/sidCapture.py logs every SID write per PAL frame and -# assets/sidToSong.py turns that into songbake text. -import os -import subprocess -import sys -import tempfile - -here = os.path.dirname(os.path.abspath(__file__)) -repo = os.path.abspath(os.path.join(here, "..", "..", "..")) -songbake = os.path.join(repo, "tools", "songbake", "songbake.py") -out = os.path.join(here, "..", "stSongData.h") -count = 9 - -header = """// Space Taxi (C64) tunes as JoeyLib JYM1 streams. GENERATED by -// assets/genSongData.py -- do not hand-edit. The songs were extracted by -// running the game's own music player under a 6502 emulator -// (assets/sidCapture.py logs every SID write per PAL frame; -// assets/sidToSong.py turns that into songbake text in assets/songs/). -// -// Where the C64 plays them: -// BLOCKING via $44CF (the game spins until the tune ends; the port -// parks in ST_STATE_JINGLE with the sim frozen): -// 0..3 start of every screen, rotating on $5E9B ($6906) -// 4 level 25 start ($6935) and the bonus cab ($70B1) -// 5 game over ($5C1F), after the two-second GAME OVER! wait -// NON-BLOCKING via $CB02 (started and left to the IRQ player): -// 6 the high-score name entry screen ($4E35) -- no port screen yet -// 7 the high-score table ($4C1F, joystick UP on the title) -// 8 the level intro while the cab flies in ($459C); endless, so -// the source is trimmed to 30 s and looped -#ifndef ST_SONG_DATA_H -#define ST_SONG_DATA_H - -#include - -""" - -footer = """typedef struct { - const uint8_t *data; - uint16_t length; -} StSongT; - -#define ST_SONG_COUNT %du -#define ST_SONG_BONUS 4u -#define ST_SONG_GAME_OVER 5u -#define ST_SONG_NAME_ENTRY 6u -#define ST_SONG_SCORES 7u -#define ST_SONG_INTRO 8u - -static const StSongT kStSongs[ST_SONG_COUNT] = { -%s}; - -#endif // ST_SONG_DATA_H -""" - -parts = [] -for n in range(count): - src = os.path.join(here, "songs", "song%d.song" % n) - with tempfile.NamedTemporaryFile(suffix=".h", delete=False) as tmp: - path = tmp.name - subprocess.run([sys.executable, songbake, "-c", "gStSong%d" % n, src, path], check=True, stderr=subprocess.DEVNULL) - body = open(path).read() - os.unlink(path) - body = "\n".join(l for l in body.split("\n") if l.startswith("static") or l.startswith(" ") or l.startswith("}")) - parts.append(body.rstrip() + "\n\n") - -table = "".join(" { gStSong%d, (uint16_t)sizeof(gStSong%d) },\n" % (n, n) for n in range(count)) -with open(out, "w") as fp: - fp.write(header + "".join(parts) + footer % (count, table)) -print("wrote", os.path.normpath(out)) diff --git a/examples/spacetaxi/assets/genSpeechPcm.py b/examples/spacetaxi/assets/genSpeechPcm.py deleted file mode 100755 index 8943a2e..0000000 --- a/examples/spacetaxi/assets/genSpeechPcm.py +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env python3 -"""genSpeechPcm.py - bake Space Taxi's speech to 8-bit PCM. -Reads the C64 run-length speech streams from stSpeechData.c (the output -of extractSpeech.py) and writes speech.bin: every utterance decoded to -5 kHz sample bytes for each passenger pitch the game can select, in BOTH -polarities. The decode is the exact algorithm stAudio.c used to run per -sample at play time; on the 65816 that took 2.4 s of CPU per second of -speech (three run bytes per sample, each a multiply) and stalled the ride -for half a second on every "HEY TAXI". A 1-bit intermediate format came -next, but expanding bits still needed a per-byte loop (and an asm copy of -it on the IIgs); with sample bytes the player is a block copy. -Polarity: an utterance is decoded from a +100 start. The C64 keeps the -output level running from one utterance into the next, so a phrase whose -first word ends on the low level plays its second word inverted. Both -versions are stored and the player picks by the running parity. -speech.bin (little-endian): - "STSP" u8 version(3) u8 pitchBase u8 pitchCount u8 count - u8 format (0 = signed +/-100, 1 = 0x80-biased for the IIgs raw stream) - u8 polarities (1 or 2) u8 pad[2] u32 dataBytes - entries[pitchCount * count * polarities]: u8 code, u8 parity, - u16 samples, u32 offset -- polarity is the innermost index - (0 = as decoded, 1 = inverted). parity = 1 when the utterance - ends on the opposite level from the one it started on. - data: sample bytes -With --single only polarity 0 is written, halving the file; the player -negates the samples itself when the running parity calls for it. The -X68000 needs that: its Human68k floppy cannot spare the other 60 KB. -usage: genSpeechPcm.py stSpeechData.c speech.bin [--biased] [--single] -""" -import re -import struct -import sys - -SID_HZ = 1022727 # ST_SID_HZ_NUM -RATE = 5000 # ST_SPEECH_RATE -CYC_PER_SAMPLE = SID_HZ // RATE -SYNC_CYCLES = SID_HZ // 60 // 8 -CYCLE_TAIL = 33 -SYNC = 0xFE -END = 0xFF -PITCH_BASE = 2 # stFare.c: stAudioSpeech(rng(3) + 2) -PITCH_COUNT = 3 - - -def parseSource(path): - text = open(path).read() - idx = re.search(r"kStSpeechIndex\[[^\]]*\]\s*=\s*\{(.*?)\};", text, re.S).group(1) - entries = [(int(c, 16), int(o), int(n)) for c, o, n in - re.findall(r"\{\s*0x([0-9A-Fa-f]+)u,\s*(\d+)u,\s*(\d+)u\s*\}", idx)] - rle = re.search(r"kStSpeechRle\[[^\]]*\]\s*=\s*\{(.*?)\};", text, re.S).group(1) - data = bytes(int(x, 16) for x in re.findall(r"0x([0-9A-Fa-f]{2})", rle)) - return entries, data - - -def decode(rle, perOn): - """speechDecode() for one utterance from a +100 start, remain 0.""" - remain = 0 - level = 100 - out = [] - i = 0 - n = len(rle) - while True: - while remain <= 0: - if i >= n: - return out, level - b = rle[i] - i += 1 - if b == END: - i = n - continue - if b == SYNC: - remain += SYNC_CYCLES - continue - remain += perOn * b + CYCLE_TAIL - level = -100 if level > 0 else 100 - out.append(level) - remain -= CYC_PER_SAMPLE - - -def main(): - args = [a for a in sys.argv[1:] if not a.startswith("--")] - biased = "--biased" in sys.argv[1:] - single = "--single" in sys.argv[1:] - polarities = 1 if single else 2 - src, dst = args[0], args[1] - entries, data = parseSource(src) - records = [] - blob = bytearray() - for p in range(PITCH_COUNT): - perOn = 5 * (PITCH_BASE + p) + 12 - for code, off, length in entries: - samples, level = decode(data[off:off + length], perOn) - parity = 1 if level < 0 else 0 - for pol in range(polarities): - pcm = bytearray() - for s in samples: - v = -s if pol else s - pcm.append((v + 0x80) & 0xFF if biased else v & 0xFF) - records.append((code, parity, len(samples), len(blob))) - blob += pcm - with open(dst, "wb") as f: - f.write(b"STSP" + struct.pack(".png`. -2. The Makefile bakes per-target via `tools/assetbake/assetbake.py - --type tile --target tbank.png tbank.tbk`. Output - lands in `examples/spacetaxi/generated//tiles/` and is - staged into the runtime tree at `build//.../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 `examples/spacetaxi/assets/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. diff --git a/examples/spacetaxi/assets/levels/title.txt b/examples/spacetaxi/assets/levels/title.txt deleted file mode 100644 index 4d80f7b..0000000 --- a/examples/spacetaxi/assets/levels/title.txt +++ /dev/null @@ -1,142 +0,0 @@ -# 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 diff --git a/examples/spacetaxi/assets/romToLevel.py b/examples/spacetaxi/assets/romToLevel.py deleted file mode 100644 index 7c101c9..0000000 --- a/examples/spacetaxi/assets/romToLevel.py +++ /dev/null @@ -1,205 +0,0 @@ -#!/usr/bin/env python3 -# Convert ROM-extracted Space Taxi scenes directly into the runtime -# STL1 binary format (see examples/spacetaxi/stLevel.c). Uses the -# 6502 emulator + raw RAM dump to decompress each scene, then writes -# screen + color RAM bytes straight into the .dat layout the JoeyLib -# port loads at runtime. -# -# Replaces the .txt -> mkstlevel pipeline for canonical levels. The C -# mkstlevel tool stays in tree for hand-authored .txt levels. -# -# Usage: -# romToLevel.py # all 24 levels -# romToLevel.py # one specific -# -# Writes: -# /level01.dat ... level24.dat (scene 0 = "A" = level01) -# /tbank0.png (charset extracted from $2800) - -import os -import struct -import sys -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) - -from cpu6502 import Cpu6502 -from dumpScenes import load_dump, run_scene - - -# STL2 binary layout (see examples/spacetaxi/stLevel.c): -# "STL2" -# nameLen + name -# tileBank, music, bgColor, borderColor, taxiSpawnX, taxiSpawnY -# xAccel, yAccel, xGrav (int8), yGrav (int8) -# bgColor1, bgColor2, bgColor3, spriteMc0, spriteMc1, sprite0Color, sprite1Color -# padCount + padCount * (letter, tileX, tileY, tileW) -# fareCount + fareCount * (spawnPad, destPad) -# tilemap[40 * 25] -# colormap[40 * 25] - -ST_TILEMAP_W = 40 -ST_PLAYFIELD_ROWS = 25 -ST_TILE_PX = 8 - -# VIC-II sprite-register origins: sprite-X $18 (24) is display column 0, -# sprite-Y $32 (50) is the first visible raster line. All in-game pad/taxi -# positions are stored in this sprite-register space, so tile conversion -# subtracts these origins. PAD_SURFACE_ORIGIN folds in the ~14px landing- -# gear offset (50-14=36) so a pad's row byte maps to its contact-surface -# tile row rather than the sprite-top row. -VIC_X_ORIGIN = 24 -VIC_Y_ORIGIN = 50 -PAD_SURFACE_ORIGIN = 36 -LEVEL_LETTERS = [chr(ord('A') + i) for i in range(24)] - - -def write_level(path, level_index, after, screen, colormap, sprite_blocks): - """Write an STL4 level .dat straight from post-decompression RAM. - - Everything stays in the C64's own units so the runtime can mirror the - original routines byte for byte: - name[22] $7D78-$7D8D level name (screen codes, ASCII-compatible) - header[9] $7D00-$7D08 VIC colours: border, bg, bg1-3, mc0, mc1, - sprite-0, sprite-1 - padCount $7D5A pads in the table - specialPad $7D09 fare pads (== padCount, or padCount-1 when - the last pad is the fuel station) - fuelRate $7D60 ticks of flight per half fuel cell - spawn[5] $7D5B-$7D5F X frac, X lo, X hi, Y frac, Y hi (sprite coords) - templates[8] $7D8F-$7D96 Y accel, X accel, Y gravity, X gravity - (16-bit little-endian each) - pads[10][8] $7D0A-$7D59 raw slots: x1 hi/lo, x2 hi/lo, row, - passenger X frac, passenger X col, unused - tilemap[1000] screen RAM (all 25 rows, the HUD rows included) - colormap[1000] colour RAM low nibbles - levelIndex 0..23 = A..X, 24 = title - spriteCount N, then N x (ptr, 63 bytes): the per-level sprite - bitmaps the level's extra scene loads at $2000+ - hookData[616] $7D98-$7FFF: the level's hook code and tables - """ - cells = ST_TILEMAP_W * ST_PLAYFIELD_ROWS - assert len(screen) == cells, f'screen is {len(screen)}, need {cells}' - assert len(colormap) == cells, f'colormap is {len(colormap)}, need {cells}' - with open(path, 'wb') as fp: - fp.write(b'STL4') - fp.write(bytes(after[0x7D78:0x7D78 + 22])) - fp.write(bytes(after[0x7D00:0x7D09])) - fp.write(bytes([after[0x7D5A], after[0x7D09], after[0x7D60]])) - fp.write(bytes(after[0x7D5B:0x7D60])) - fp.write(bytes(after[0x7D8F:0x7D97])) - pad_count = min(after[0x7D5A], 10) - for pad_idx in range(10): - if pad_idx < pad_count: - off = 0x7D0A + pad_idx * 8 - fp.write(bytes(after[off:off + 8])) - else: - fp.write(bytes(8)) - fp.write(bytes(screen)) - fp.write(bytes(colormap)) - fp.write(bytes([level_index])) - fp.write(bytes([len(sprite_blocks)])) - for ptr, data in sprite_blocks: - fp.write(bytes([ptr])) - fp.write(bytes(data)) - fp.write(bytes(after[0x7D98:0x8000])) - - -# Levels whose data blob flags an extra scene ($7D97 != 0) load a second -# scene from this table ($6F55, indexed by level) right after the level -# itself. Those extra scenes only write sprite bitmaps into $2000-$27FF -# (block pointers $80-$9F) for the level's hook code to animate. -_EXTRA_SCENE = [0, 0, 0, 0, 0, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0, 0x1F, 0, 0, 0, - 0x20, 0x21, 0, 0, 0, 0x22, 0, 0, 0, 0x23] - - -def extract_one_level(ram, scene_id, out_dir): - """Decompress `scene_id` and write out_dir/level.dat (NN = scene+1).""" - letter = LEVEL_LETTERS[scene_id] - screen_buf, color_buf, after = run_scene(ram, scene_id) - sprite_blocks = [] - if after[0x7D97] != 0 and _EXTRA_SCENE[scene_id] != 0: - # Start the extra scene from a zeroed sprite window so the blocks - # it writes are exactly the ones the level ships with. - work = bytearray(after) - for i in range(0x2000, 0x2800): - work[i] = 0 - _s, _c, after2 = run_scene(bytes(work), _EXTRA_SCENE[scene_id]) - for ptr in range(0x80, 0xA0): - block = bytes(after2[ptr * 64:ptr * 64 + 63]) - if any(block): - sprite_blocks.append((ptr, block)) - # The level data itself (pads, hooks) is untouched by the extra - # scene; keep reading it from the level's own image. - cells = ST_TILEMAP_W * ST_PLAYFIELD_ROWS - screen = list(screen_buf[:cells]) - colormap = [(b & 0x0F) for b in color_buf[:cells]] - level_num = scene_id + 1 - out_path = os.path.join(out_dir, f'level{level_num:02d}.dat') - write_level(out_path, scene_id, after, screen, colormap, sprite_blocks) - return out_path, after[0x7D5A], len(sprite_blocks) - - -def extract_screen_y(ram, out_dir): - """Screen Y, the 25th screen ("MYSTERY SCREEN !!!"), is scene 24. - - $5010 dispatches on the screens-completed counter: at 24 it jumps to - $5167, which loads #$59 ('Y') as the pending screen; the shift only - ends when the counter reaches 25. So a full 24-hour shift plays A..X - and then Y -- the 25 screens the intro advertises. - """ - screen_buf, color_buf, after = run_scene(ram, 24) - cells = ST_TILEMAP_W * ST_PLAYFIELD_ROWS - screen = list(screen_buf[:cells]) - colormap = [(b & 0x0F) for b in color_buf[:cells]] - out_path = os.path.join(out_dir, 'level25.dat') - write_level(out_path, 24, after, screen, colormap, []) - return out_path - - -def extract_title(ram, out_dir): - """The title screen is scene 25 (what $477A decompresses). - Pads, spawn and templates are dummies.""" - screen_buf, color_buf, after = run_scene(ram, 25) - cells = ST_TILEMAP_W * ST_PLAYFIELD_ROWS - screen = list(screen_buf[:cells]) - colormap = [(b & 0x0F) for b in color_buf[:cells]] - out_path = os.path.join(out_dir, 'title.dat') - # The title scene writes no level name, so $7D78 still holds whatever - # the previously decompressed scene left there (PUZZLER, level H). - # The title never draws the name; ship it blank rather than stale. - work = bytearray(after) - work[0x7D78:0x7D78 + 22] = b' ' * 22 - write_level(out_path, 25, work, screen, colormap, []) - return out_path - - -def main(): - if len(sys.argv) < 3: - print('usage: romToLevel.py [scene_id]', file=sys.stderr) - sys.exit(2) - ram = load_dump(sys.argv[1]) - out_dir = sys.argv[2] - os.makedirs(out_dir, exist_ok=True) - - if len(sys.argv) >= 4: - scene_ids = [int(sys.argv[3], 0)] - else: - # Levels A..X = scenes 0..23. The title comes from screen RAM - # at $0400 (the dump was taken on the title screen), not from - # a decompressed scene. - scene_ids = list(range(24)) - - for sid in scene_ids: - path, pad_count, nspr = extract_one_level(ram, sid, out_dir) - size = os.path.getsize(path) - print(f' scene {sid:2d} (level {LEVEL_LETTERS[sid]}): pads={pad_count} sprites={nspr} size={size} -> {path}') - - # Screen Y and the title are not part of the A..X sweep. - if len(sys.argv) < 4: - y_path = extract_screen_y(ram, out_dir) - print(f' scene 24 (screen Y) : -> {y_path} ({os.path.getsize(y_path)} bytes)') - title_path = extract_title(ram, out_dir) - print(f' title (screen RAM $0400) : -> {title_path} ({os.path.getsize(title_path)} bytes)') - - -if __name__ == '__main__': - main() diff --git a/examples/spacetaxi/assets/sidCapture.py b/examples/spacetaxi/assets/sidCapture.py deleted file mode 100644 index 76befb8..0000000 --- a/examples/spacetaxi/assets/sidCapture.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env python3 -"""Run the C64 Space Taxi music player and log every SID write per frame. - -The player lives in the TITLE memory image at $CB00-$CFFF (its own relocated -copy; the $7500 copy is gameplay-side). Rather than hand-decode its byte -code, drive the real routines under cpu6502.py -- the same trick that -extracted the speech: - $CB00 select song ($CB01 = song number) and init (hooks the IRQ vector) - $CF52 the IRQ body: first frame calls start ($CC28), then ticks the three - voices via $CC6C, then JMPs to the KERNAL IRQ ($EA31) which is what - advances the jiffy clock $A2. We poke an RTS at $EA31 and bump $A2 - ourselves, once per frame. -A song is over when all three voice stream pointers' high bytes are zero -(that is exactly what the game's play-and-wait loop at $44D5 spins on). -""" -import json, os, sys -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from cpu6502 import Cpu6502 - -RAM = bytearray(open(sys.argv[1], "rb").read()[:65536]) -OUT = sys.argv[2] -MAX_FRAMES = 4000 - - -class SidTrap(Cpu6502): - def __init__(self, ram): - super().__init__(ram) - self.log = [] - self.frame = 0 - - def write8(self, addr, val): - a = addr & 0xFFFF - if 0xD400 <= a <= 0xD418: - self.log.append((self.frame, a - 0xD400, val & 0xFF)) - super().write8(addr, val) - - -songs = {} -for song in range(0, 9): - ram = bytearray(RAM) - ram[0xEA31] = 0x60 # KERNAL IRQ tail -> RTS - cpu = SidTrap(ram) - cpu.call(0xCC00) # full reset first - ram[0xCB01] = song - cpu.call(0xCB00) # select + arm - for frame in range(MAX_FRAMES): - cpu.frame = frame - ram[0xA2] = (ram[0xA2] + 1) & 0xFF - cpu.call(0xCF52) - if ram[0xCB81] == 0 and ram[0xCB88] == 0 and ram[0xCB8F] == 0: - break - songs[song] = {"frames": frame + 1, "writes": cpu.log, - "ptr": ram[0xCB40 + song * 2] | (ram[0xCB41 + song * 2] << 8)} - print(f"song {song}: ptr=${songs[song]['ptr']:04X} frames={frame+1} " - f"({(frame+1)/50:.1f}s PAL) sidWrites={len(cpu.log)}") - -json.dump(songs, open(OUT, "w")) diff --git a/examples/spacetaxi/assets/sidToSong.py b/examples/spacetaxi/assets/sidToSong.py deleted file mode 100644 index 43ede3f..0000000 --- a/examples/spacetaxi/assets/sidToSong.py +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env python3 -"""SID register log (sidCapture.py) -> JoeyLib .song text for songbake. - -One row per PAL frame (tickms 20). Per voice, a note-on is a gate rising -edge or a pitch change while gated; gate falling is 'off'. SID frequency -> -Hz exactly (@Hz cells), so no equal-temperament rounding. Attenuation comes -from the sustain nibble. A voice whose waveform is NOISE goes to the noise -column instead of a tone column, with a pitch scaled from its frequency. -Loop detection: the player is deterministic, so once the (frame-relative) -event rows repeat with a fixed period, that period is the loop. -""" -import json, sys - -PAL_CLOCK = 985248.0 -songs = json.load(open(sys.argv[1])) -outDir = sys.argv[2] - - -def sidHz(lo, hi): - return ((hi << 8) | lo) * PAL_CLOCK / 16777216.0 - - -def rowsFor(writes, nframes): - # replay register writes frame by frame, producing per-frame voice state - regs = [0] * 0x19 - state = [] - byFrame = {} - for f, r, v in writes: - byFrame.setdefault(f, []).append((r, v)) - for f in range(nframes): - for r, v in byFrame.get(f, []): - regs[r] = v - vs = [] - for vc in range(3): - b = vc * 7 - vs.append(dict(hz=sidHz(regs[b], regs[b + 1]), gate=regs[b + 4] & 1, - wave=regs[b + 4] & 0xF0, sus=regs[b + 6] >> 4)) - state.append(vs) - return state - - -def atten(sus): - # SID sustain 0..15 -> AGI attenuation 14..0 (0 loudest) - return max(0, min(14, round(14 - sus * 14 / 15))) - - -def noisePitch(hz): - # brighter hiss for higher SID "frequency"; 0..31, 0 = brightest - p = int(31 - min(31, hz / 400.0)) - return max(0, min(31, p)) - - -def toRows(state): - rows = [] - prev = [dict(hz=None, gate=0, wave=0, sus=0) for _ in range(3)] - for vs in state: - tone = ["---", "---", "---"] - noise = "---" - for vc in range(3): - cur, was = vs[vc], prev[vc] - isNoise = (cur["wave"] & 0x80) != 0 - on = cur["gate"] and (not was["gate"] or abs(cur["hz"] - was["hz"]) > 0.5 - or cur["wave"] != was["wave"]) - off = was["gate"] and not cur["gate"] - if on: - if isNoise: - noise = f"N{noisePitch(cur['hz'])}:{atten(cur['sus'])}" - else: - hz = max(1, min(65535, int(round(cur["hz"])))) - tone[vc] = f"@{hz}:{atten(cur['sus'])}" - elif off: - if (was["wave"] & 0x80): - noise = "off" - else: - tone[vc] = "off" - prev[vc] = dict(cur) - rows.append((tone, noise)) - return rows - - -def findLoop(rows): - # smallest (offset, period) with rows[o+k] == rows[o+p+k] for the tail - n = len(rows) - for p in range(8, n // 2): - for o in range(0, n - 2 * p): - if rows[o:o + p] == rows[o + p:o + 2 * p] and rows[o:n - p] == rows[o + p:n]: - return o, p - return None - - -for num, s in sorted(songs.items(), key=lambda kv: int(kv[0])): - num = int(num) - st = rowsFor(s["writes"], s["frames"]) - rows = toRows(st) - loop = findLoop(rows) if s["frames"] >= 4000 else None - if loop: - o, p = loop - rows = rows[:o + p] - lines = ["; Space Taxi (C64) song %d, extracted by running the game's own" % num, - "; player ($CB00/$CF52) under cpu6502.py and logging SID writes.", - "; One row = one PAL frame.", "tickms 20"] - for i, (tone, noise) in enumerate(rows): - if loop and i == loop[0]: - lines.append("loop") - cells = tone + ([noise] if noise != "---" or True else []) - lines.append(" ".join(cells)) - open(f"{outDir}/song{num}.song", "w").write("\n".join(lines) + "\n") - events = sum(1 for t, n in rows for c in t + [n] if c != "---") - waves = sorted({hex(v["wave"]) for vs in st for v in vs if v["gate"]}) - print(f"song {num}: {len(rows)} rows, {events} events, waveforms {waves}" - + (f", LOOP at row {loop[0]} period {loop[1]}" if loop else "")) diff --git a/examples/spacetaxi/assets/songs/song0.song b/examples/spacetaxi/assets/songs/song0.song deleted file mode 100644 index 7e6036a..0000000 --- a/examples/spacetaxi/assets/songs/song0.song +++ /dev/null @@ -1,94 +0,0 @@ -; Space Taxi (C64) song 0, extracted by running the game's own -; player ($CB00/$CF52) under cpu6502.py and logging SID writes. -; One row = one PAL frame. -tickms 20 ---- --- --- --- -@336:2 @424:2 --- --- ---- --- --- --- ---- --- --- --- -off off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@378:2 @424:2 --- --- ---- --- --- --- ---- --- --- --- -off off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@336:2 @424:2 --- --- ---- --- --- --- ---- --- --- --- -off off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@378:2 @476:2 --- --- ---- --- --- --- ---- --- --- --- -off off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@318:2 @252:2 --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @476:2 --- --- ---- --- --- --- ---- --- --- --- -off off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@283:2 @252:2 --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @476:2 --- --- ---- --- --- --- ---- --- --- --- -off off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@252:2 @252:2 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- diff --git a/examples/spacetaxi/assets/songs/song1.song b/examples/spacetaxi/assets/songs/song1.song deleted file mode 100644 index e067e38..0000000 --- a/examples/spacetaxi/assets/songs/song1.song +++ /dev/null @@ -1,94 +0,0 @@ -; Space Taxi (C64) song 1, extracted by running the game's own -; player ($CB00/$CF52) under cpu6502.py and logging SID writes. -; One row = one PAL frame. -tickms 20 ---- --- --- --- -@252:7 @159:7 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off off --- --- -@283:7 @168:7 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off off --- --- -@252:7 @189:7 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off off --- --- -@283:7 @212:7 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off off --- --- -@318:7 @189:7 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off off --- --- -@336:7 @168:7 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off off --- --- -@318:7 @159:7 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off off --- --- -@336:7 @168:7 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off off --- --- -@378:7 @189:7 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- --- --- diff --git a/examples/spacetaxi/assets/songs/song2.song b/examples/spacetaxi/assets/songs/song2.song deleted file mode 100644 index fe58aa5..0000000 --- a/examples/spacetaxi/assets/songs/song2.song +++ /dev/null @@ -1,86 +0,0 @@ -; Space Taxi (C64) song 2, extracted by running the game's own -; player ($CB00/$CF52) under cpu6502.py and logging SID writes. -; One row = one PAL frame. -tickms 20 ---- --- --- --- -@126:0 @159:0 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- -off --- --- --- -@126:0 @168:0 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- -off --- --- --- -@141:0 @189:0 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- -off --- --- --- -@159:0 @159:0 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- -off --- --- --- -@159:0 @168:0 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- -off --- --- --- -@168:0 @189:0 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- -off --- --- --- -@168:0 @159:0 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- -off --- --- --- -@189:0 @168:0 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- -off --- --- --- ---- @159:0 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- @126:0 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- diff --git a/examples/spacetaxi/assets/songs/song3.song b/examples/spacetaxi/assets/songs/song3.song deleted file mode 100644 index 20a3f8d..0000000 --- a/examples/spacetaxi/assets/songs/song3.song +++ /dev/null @@ -1,86 +0,0 @@ -; Space Taxi (C64) song 3, extracted by running the game's own -; player ($CB00/$CF52) under cpu6502.py and logging SID writes. -; One row = one PAL frame. -tickms 20 ---- --- --- --- -@252:0 @336:2 @336:2 --- ---- --- --- --- ---- --- --- --- -off off off --- ---- --- --- --- -@283:0 --- --- --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- --- --- -@318:0 @336:2 @336:2 --- ---- --- --- --- ---- --- --- --- -off off off --- ---- --- --- --- -@252:0 --- --- --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- --- --- -@283:0 @336:2 @336:2 --- ---- --- --- --- ---- --- --- --- -off off off --- ---- --- --- --- -@252:0 --- --- --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- --- --- -@283:0 @336:2 @336:2 --- ---- --- --- --- ---- --- --- --- -off off off --- ---- --- --- --- -@476:0 --- --- --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- --- --- ---- @318:2 @378:2 --- ---- --- --- --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @336:2 @378:2 --- ---- --- --- --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @283:2 @252:2 --- ---- --- --- --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @252:2 @476:2 --- ---- --- --- --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- diff --git a/examples/spacetaxi/assets/songs/song4.song b/examples/spacetaxi/assets/songs/song4.song deleted file mode 100644 index e32f538..0000000 --- a/examples/spacetaxi/assets/songs/song4.song +++ /dev/null @@ -1,126 +0,0 @@ -; Space Taxi (C64) song 4, extracted by running the game's own -; player ($CB00/$CF52) under cpu6502.py and logging SID writes. -; One row = one PAL frame. -tickms 20 ---- --- --- --- -@504:5 @449:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@504:5 @449:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off off --- --- -@673:5 @504:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@504:5 @424:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@673:5 @504:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off off --- --- -@848:5 @673:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @635:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @673:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off off --- --- ---- --- --- --- diff --git a/examples/spacetaxi/assets/songs/song5.song b/examples/spacetaxi/assets/songs/song5.song deleted file mode 100644 index ddf5ada..0000000 --- a/examples/spacetaxi/assets/songs/song5.song +++ /dev/null @@ -1,736 +0,0 @@ -; Space Taxi (C64) song 5, extracted by running the game's own -; player ($CB00/$CF52) under cpu6502.py and logging SID writes. -; One row = one PAL frame. -tickms 20 ---- --- --- --- -@504:2 @159:2 @106:10 --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- @212:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @159:2 @106:10 --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- @212:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @159:2 @106:10 --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- @212:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @159:2 @106:10 --- ---- --- --- --- ---- --- --- --- ---- off --- --- -off --- --- --- ---- --- @212:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@504:2 @159:2 @106:10 --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- @212:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @159:2 @106:10 --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- @212:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @159:2 @106:10 --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- @212:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @159:2 @106:10 --- ---- --- --- --- ---- --- --- --- ---- off --- --- -off --- --- --- ---- --- @212:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@504:2 @159:2 @106:10 --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- @212:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @159:2 @106:10 --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- @212:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @159:2 @106:10 --- ---- --- --- --- ---- --- --- --- ---- off --- --- -off --- --- --- ---- --- @212:10 --- ---- --- --- --- ---- --- --- --- ---- --- off --- ---- --- --- --- -@566:2 @141:2 @100:10 --- ---- --- --- --- ---- --- --- --- ---- off --- --- -off --- --- --- ---- --- @200:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@566:2 @141:2 @100:10 --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- @200:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @141:2 @100:10 --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- @200:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @141:2 @100:10 --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- @200:10 --- ---- --- --- --- ---- --- --- --- ---- --- off --- ---- --- --- --- ---- @141:2 @100:10 --- ---- --- --- --- ---- --- --- --- ---- off --- --- -off --- --- --- ---- --- @200:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@635:2 @504:2 @94:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off off --- --- ---- --- @189:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- @94:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- @189:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@673:2 @566:2 @84:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off off --- --- ---- --- @168:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- @84:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- @168:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@755:2 @635:2 @79:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off off --- --- ---- --- @159:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- @79:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- @159:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@800:2 @635:2 @71:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off off --- --- ---- --- @141:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- @71:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- @141:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@848:2 @635:2 @67:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- @133:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- @67:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- @133:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- @67:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- @133:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- @67:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- @133:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@424:2 @318:2 @67:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off off --- --- ---- --- @133:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@848:2 @336:2 @252:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- off --- ---- --- --- --- -@848:2 @336:2 @252:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- off --- ---- --- --- --- -@848:2 @336:2 @252:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- off --- ---- --- --- --- -@755:2 @318:2 @225:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- off --- ---- --- --- --- -@755:2 @318:2 @225:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- off --- ---- --- --- --- -@673:2 @283:2 @212:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- off --- ---- --- --- --- -@635:2 @252:2 @189:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@566:2 @283:2 @168:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@755:2 @318:2 @126:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- @189:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- @126:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- @189:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- @126:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- @189:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- @126:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- @189:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- @126:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- @189:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@635:2 --- @126:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- @189:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@504:2 --- @126:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- @189:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@635:2 --- @126:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off off --- --- ---- --- @189:10 --- ---- --- --- --- ---- --- --- --- ---- --- off --- ---- --- --- --- -@755:2 @283:2 @119:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- @189:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- @119:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- @189:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@848:2 --- @119:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- @189:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- @119:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- @189:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@755:2 --- @119:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- @189:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@673:2 --- @119:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- @189:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@635:2 --- @119:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- @189:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@566:2 --- @119:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off off --- --- ---- --- @189:10 --- ---- --- --- --- ---- --- --- --- ---- --- off --- ---- --- --- --- -@504:2 @318:2 @126:10 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- off --- ---- --- --- --- ---- --- --- --- diff --git a/examples/spacetaxi/assets/songs/song6.song b/examples/spacetaxi/assets/songs/song6.song deleted file mode 100644 index f26cc4d..0000000 --- a/examples/spacetaxi/assets/songs/song6.song +++ /dev/null @@ -1,840 +0,0 @@ -; Space Taxi (C64) song 6, extracted by running the game's own -; player ($CB00/$CF52) under cpu6502.py and logging SID writes. -; One row = one PAL frame. -tickms 20 ---- --- --- --- ---- @378:1 @504:1 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @378:1 @504:1 --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @378:1 @504:1 --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @378:1 @504:1 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@94:0 --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@126:0 --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@159:0 --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@189:0 @336:1 @449:1 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @300:1 @400:1 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @336:1 @449:1 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @378:1 @504:1 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @378:1 @504:1 --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off @378:1 @504:1 --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @378:1 @504:1 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@212:0 --- --- --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@189:0 --- --- --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@159:0 --- --- --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- --- --- ---- --- --- --- ---- @378:1 @504:1 --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @378:1 @504:1 --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@141:0 @424:1 @566:1 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @378:1 @504:1 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @424:1 @566:1 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@119:0 @476:1 @635:1 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @476:1 @635:1 --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @476:1 @635:1 --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @476:1 @635:1 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@59:0 --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @476:1 @635:1 --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @476:1 @635:1 --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @476:1 @635:1 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @424:1 @566:1 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @476:1 @635:1 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@126:0 @504:1 @673:1 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @504:1 @673:1 --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @504:1 @673:1 --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @504:1 @673:1 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@63:0 --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @504:1 @673:1 --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off @504:1 @673:1 --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@126:0 @566:1 @755:1 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @504:1 @673:1 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @566:1 @755:1 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@141:0 @635:1 @848:1 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- --- --- ---- --- off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@71:0 --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @635:1 @848:1 --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off @635:1 @848:1 --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@141:0 @566:1 @848:1 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @566:1 @848:1 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @566:1 @848:1 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- --- --- ---- off off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@159:0 @534:1 @848:1 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off --- off --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- diff --git a/examples/spacetaxi/assets/songs/song7.song b/examples/spacetaxi/assets/songs/song7.song deleted file mode 100644 index fea5ac3..0000000 --- a/examples/spacetaxi/assets/songs/song7.song +++ /dev/null @@ -1,852 +0,0 @@ -; Space Taxi (C64) song 7, extracted by running the game's own -; player ($CB00/$CF52) under cpu6502.py and logging SID writes. -; One row = one PAL frame. -tickms 20 ---- --- --- --- -@378:0 @1270:5 @126:7 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1346:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1270:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- @1132:5 @119:7 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1270:5 off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1346:5 --- --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@252:0 @1270:5 @106:7 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1346:5 off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1510:5 --- --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@283:0 @1008:5 @94:7 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@318:0 --- off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@336:0 --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1008:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@378:0 --- @126:7 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @952:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @848:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @755:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1132:5 @119:7 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1270:5 off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1346:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1510:5 @126:7 --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1510:5 off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1510:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@424:0 @1132:5 @94:7 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- -@378:0 --- off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@336:0 @1132:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@378:0 @1270:5 @126:7 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- off --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@336:0 @952:5 @119:7 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- -@318:0 --- off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@283:0 @952:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@318:0 @1008:5 @106:7 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- off --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@283:0 @755:5 @94:7 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- -@252:0 --- off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@238:0 @755:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- -@252:0 @1008:5 @126:7 --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @952:5 off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@318:0 @1008:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- -@189:0 @1132:5 @119:7 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1008:5 off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1132:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1270:5 @106:7 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1132:5 off --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1270:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@238:0 @1346:5 @94:7 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@252:0 @1270:5 off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@283:0 @1346:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- -@252:0 @1270:5 @126:7 --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1270:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@238:0 @1510:5 @119:7 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@252:0 --- off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@283:0 --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@318:0 --- @106:7 --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@283:0 @1510:5 @94:7 --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- -@283:0 --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@378:0 @1132:5 @141:7 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1008:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @952:5 @119:7 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1008:5 off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1132:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @755:5 @106:7 --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@378:0 @1132:5 --- --- -off --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@378:0 --- --- --- -off --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@378:0 @1199:5 @94:7 --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@378:0 --- off --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- -@378:0 --- --- --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@378:0 @1270:5 @126:7 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1510:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1270:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1510:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1270:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- @94:7 --- ---- @1510:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1270:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1510:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1270:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1510:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- @126:7 --- ---- --- --- --- ---- @1270:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1510:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1270:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1510:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1270:5 --- --- ---- --- --- --- ---- --- @94:7 --- ---- --- --- --- ---- --- --- --- ---- @1510:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1270:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1510:5 --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1270:5 --- --- ---- --- off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- @126:7 --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -off --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- off --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- diff --git a/examples/spacetaxi/assets/songs/song8.song b/examples/spacetaxi/assets/songs/song8.song deleted file mode 100644 index 93a08c5..0000000 --- a/examples/spacetaxi/assets/songs/song8.song +++ /dev/null @@ -1,1508 +0,0 @@ -; Space Taxi (C64) song 8, extracted by running the game's own -; player ($CB00/$CF52) under cpu6502.py and logging SID writes. -; One row = one PAL frame. -; The C64 tune never ends (no state repeat in 12000 frames); the level -; intro it underscores lasts a few seconds, so the first 1500 rows (30 s) -; are kept and looped back to the start. -tickms 20 -loop ---- --- --- --- -@31:7 @126:1 --- N28:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @178:1 --- --- ---- --- --- --- ---- off --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- @212:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @252:1 --- N30:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @336:1 --- --- ---- --- --- --- ---- off --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@33:7 --- --- N30:10 ---- --- --- --- ---- @449:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @504:1 --- N30:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @599:1 --- --- ---- --- --- --- ---- off --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- @755:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @898:1 --- N30:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@35:7 --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1008:1 --- --- ---- --- --- --- ---- off --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- @1346:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1585:1 --- N29:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1270:1 --- --- ---- --- --- --- ---- off --- N28:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- @1008:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- -@33:7 --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @800:1 --- N29:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @673:1 --- --- ---- --- --- --- ---- off --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- @566:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @504:1 --- N30:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @449:1 --- --- ---- --- --- --- -@31:7 off --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- @378:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @318:1 --- N30:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @252:1 --- --- ---- --- --- --- ---- off --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- @212:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@33:7 @159:1 --- N29:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @126:1 --- --- ---- --- --- --- ---- off --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- @178:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N28:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @212:1 --- N29:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @252:1 --- --- ---- --- --- --- ---- off --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@35:7 --- --- N29:10 ---- --- --- --- ---- @336:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @449:1 --- N30:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @504:1 --- --- ---- --- --- --- ---- off --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- @599:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @755:1 --- N30:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@33:7 --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @898:1 --- --- ---- --- --- --- ---- off --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- @1008:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1346:1 --- N30:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1585:1 --- --- ---- --- --- --- ---- off --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- @1270:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- -@31:7 --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1008:1 --- N29:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N28:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @800:1 --- --- ---- --- --- --- ---- off --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- @673:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @566:1 --- N29:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @504:1 --- --- ---- --- --- --- -@33:7 off --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- @449:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @378:1 --- N30:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @318:1 --- --- ---- --- --- --- ---- off --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- @252:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@35:7 @212:1 --- N30:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @159:1 --- --- ---- --- --- --- ---- off --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- @126:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @178:1 --- N29:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @212:1 --- --- ---- --- --- --- ---- off --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@33:7 --- --- N28:10 ---- --- --- --- ---- @252:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @336:1 --- N29:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @449:1 --- --- ---- --- --- --- ---- off --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- @504:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @599:1 --- N30:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@31:7 --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @755:1 --- --- ---- --- --- --- ---- off --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- @898:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1008:1 --- N30:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1346:1 --- --- ---- --- --- --- ---- off --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- @1585:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- -@33:7 --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1270:1 --- N30:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1008:1 --- --- ---- --- --- --- ---- off --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- @800:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @673:1 --- N28:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @566:1 --- --- ---- --- --- --- -@35:7 off --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- @504:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @449:1 --- N30:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @378:1 --- --- ---- --- --- --- ---- off --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- @318:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@33:7 @252:1 --- N30:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @212:1 --- --- ---- --- --- --- ---- off --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- @159:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @126:1 --- N30:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @178:1 --- --- ---- --- --- --- ---- off --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@31:7 --- --- N29:10 ---- --- --- --- ---- @212:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @252:1 --- N29:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @336:1 --- --- ---- --- --- --- ---- off --- N28:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- @449:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @504:1 --- N29:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@33:7 --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @599:1 --- --- ---- --- --- --- ---- off --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- @755:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @898:1 --- N30:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1008:1 --- --- ---- --- --- --- ---- off --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- @1346:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- -@35:7 --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1585:1 --- N30:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1270:1 --- --- ---- --- --- --- ---- off --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- @1008:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @800:1 --- N29:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @673:1 --- --- ---- --- --- --- -@33:7 off --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- @566:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N28:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @504:1 --- N29:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @449:1 --- --- ---- --- --- --- ---- off --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- @378:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@31:7 @318:1 --- N30:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @252:1 --- --- ---- --- --- --- ---- off --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- @212:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @159:1 --- N30:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @126:1 --- --- ---- --- --- --- ---- off --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@33:7 --- --- N30:10 ---- --- --- --- ---- @178:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @212:1 --- N30:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @252:1 --- --- ---- --- --- --- ---- off --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- @336:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @449:1 --- N29:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@35:7 --- --- N28:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @504:1 --- --- ---- --- --- --- ---- off --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- @599:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @755:1 --- N29:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @898:1 --- --- ---- --- --- --- ---- off --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- @1008:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- -@33:7 --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1346:1 --- N30:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1585:1 --- --- ---- --- --- --- ---- off --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- @1270:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1008:1 --- N30:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @800:1 --- --- ---- --- --- --- -@31:7 off --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- @673:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @566:1 --- N29:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @504:1 --- --- ---- --- --- --- ---- off --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N28:10 ---- --- --- --- ---- @449:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@33:7 @378:1 --- N29:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @318:1 --- --- ---- --- --- --- ---- off --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- @252:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @212:1 --- N30:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @159:1 --- --- ---- --- --- --- ---- off --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@35:7 --- --- N30:10 ---- --- --- --- ---- @126:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @178:1 --- N30:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @212:1 --- --- ---- --- --- --- ---- off --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- @252:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @336:1 --- N30:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@33:7 --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @449:1 --- --- ---- --- --- --- ---- off --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- @504:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @599:1 --- N28:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @755:1 --- --- ---- --- --- --- ---- off --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- @898:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- -@31:7 --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1008:1 --- N30:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1346:1 --- --- ---- --- --- --- ---- off --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- @1585:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1270:1 --- N30:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @1008:1 --- --- ---- --- --- --- -@33:7 off --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- @800:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @673:1 --- N30:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @566:1 --- --- ---- --- --- --- ---- off --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- @504:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@35:7 @449:1 --- N29:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @378:1 --- --- ---- --- --- --- ---- off --- N28:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- @318:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @252:1 --- N29:10 ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- N29:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- @212:1 --- --- ---- --- --- --- ---- off --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- -@33:7 --- --- N30:10 ---- --- --- --- ---- @159:1 --- --- ---- --- --- --- ---- off --- --- ---- --- --- --- ---- --- --- N30:10 ---- --- --- --- ---- --- --- --- ---- --- --- --- ---- --- --- --- diff --git a/examples/spacetaxi/mkstlevel/mkstlevel.c b/examples/spacetaxi/mkstlevel/mkstlevel.c deleted file mode 100644 index 57c0f65..0000000 --- a/examples/spacetaxi/mkstlevel/mkstlevel.c +++ /dev/null @@ -1,452 +0,0 @@ -// mkstlevel: convert a human-readable text level definition into the -// STL3 binary format read by examples/spacetaxi/stLevel.c. -// -// Usage: -// mkstlevel input.txt output.dat -// -// Input format (.txt, line-oriented, comments start with '#'): -// -// @name -// @tilebank <0..255> -// @music <0..255> -// @bgColor -// @borderColor -// @taxiSpawn -// @accel [optional, default 14 14] -// @gravity [optional, default 0 1; yGrav signed] -// @vicColor [optional, default all 0] -// @pad [standX] [up to 10 of these; -// letter is a readability label only (not stored); -// standX = passenger stand column in pixels, optional -// (defaults to the pad's left edge, tileX*8)] -// @tilemap -- followed by exactly 25 lines of 40 chars each -// @colormap -- followed by exactly 25 lines of 40 chars each -// -// Tilemap / colormap character encoding (chars are tile/palette indices): -// '.' or ' ' -> 0 (background) -// '0'..'9' -> 0..9 -// 'A'..'Z' -> 10..35 -// 'a'..'z' -> 36..61 -// -// Per the spacetaxi engine convention: -// tile index 0 = empty (non-solid) -// tile index 1..63 = solid (walls) -// tile index 64..127 = landing-pad surface -// tile index 128..255 = decorative non-solid -// -// Color cells use the same character->index decoding; cell values are -// palette slots 0..15. Use '0'..'9','a'..'f' for the standard 16 -// surface palettes. - -#include -#include -#include -#include -#include - -#define ST_TILEMAP_W 40 -#define ST_PLAYFIELD_ROWS 25 -#define ST_MAX_PADS 10 -#define ST_NAME_MAX 23 -#define LINE_MAX 256 - - -typedef struct { - uint8_t tileX; - uint8_t tileY; - uint8_t tileW; - uint8_t standX; // passenger stand column, screen pixels -} PadT; - - -typedef struct { - char name[ST_NAME_MAX + 1]; - uint8_t tileBank; - uint8_t music; - uint8_t bgColor; - uint8_t borderColor; - uint8_t taxiSpawnX; - uint8_t taxiSpawnY; - uint8_t xAccel; - uint8_t yAccel; - int8_t xGrav; - int8_t yGrav; - uint8_t bgColor1; - uint8_t bgColor2; - uint8_t bgColor3; - uint8_t spriteMc0; - uint8_t spriteMc1; - uint8_t sprite0Color; - uint8_t sprite1Color; - uint8_t padCount; - PadT pads[ST_MAX_PADS]; - uint8_t tilemap[ST_TILEMAP_W * ST_PLAYFIELD_ROWS]; - uint8_t colormap[ST_TILEMAP_W * ST_PLAYFIELD_ROWS]; -} LevelT; - - -static int charToIndex(int c, int *outIdx); -static int parseGrid(FILE *fp, const char *gridName, uint8_t *outCells, int *outLine); -static int parseLevel(FILE *fp, LevelT *out); -static int readLine(FILE *fp, char *buf, int n); -static int stripLine(char *s); -static int writeOut(const LevelT *L, const char *path); - - -static int charToIndex(int c, int *outIdx) { - if (c == '.' || c == ' ') { - *outIdx = 0; - return 1; - } - if (c >= '0' && c <= '9') { *outIdx = c - '0'; return 1; } - if (c >= 'A' && c <= 'Z') { *outIdx = 10 + (c - 'A'); return 1; } - if (c >= 'a' && c <= 'z') { *outIdx = 36 + (c - 'a'); return 1; } - return 0; -} - - -// Reads one line; returns >0 on success, 0 on EOF, -1 on error. -// Strips trailing newline. Lines starting with '#' are returned with -// the leading '#' so the caller can decide whether to skip them. -static int readLine(FILE *fp, char *buf, int n) { - int c; - int k = 0; - - while (k < n - 1) { - c = fgetc(fp); - if (c == EOF) { - buf[k] = '\0'; - return (k == 0) ? 0 : 1; - } - if (c == '\n') { - break; - } - if (c == '\r') { - continue; // tolerate CRLF - } - buf[k++] = (char)c; - } - buf[k] = '\0'; - return 1; -} - - -// Strip leading whitespace; return 1 if blank-or-comment, 0 if data. -static int stripLine(char *s) { - int i = 0; - int j = 0; - while (s[i] == ' ' || s[i] == '\t') { - i++; - } - if (s[i] == '\0' || s[i] == '#') { - return 1; - } - while (s[i] != '\0') { - s[j++] = s[i++]; - } - s[j] = '\0'; - return 0; -} - - -static int parseGrid(FILE *fp, const char *gridName, uint8_t *outCells, int *outLine) { - char line[LINE_MAX]; - int row; - int col; - int idx; - - for (row = 0; row < ST_PLAYFIELD_ROWS; row++) { - if (readLine(fp, line, sizeof(line)) <= 0) { - fprintf(stderr, "mkstlevel: %s: unexpected EOF on row %d\n", - gridName, row); - return 0; - } - (*outLine)++; - // Strip CRLF/trailing space but DON'T treat indent as significant; - // do NOT strip leading whitespace within the grid (chars are literal). - { - int len = (int)strlen(line); - while (len > 0 && (line[len-1] == ' ' || line[len-1] == '\t')) { - line[--len] = '\0'; - } - // Allow a leading '#' comment line inside the grid only if it - // appears in the first column; otherwise treat as data error. - if (line[0] == '#') { - row--; // re-run this slot - continue; - } - if (len == 0) { - row--; // skip blank lines silently - continue; - } - if (len != ST_TILEMAP_W) { - fprintf(stderr, - "mkstlevel: %s line %d: expected %d chars, got %d (\"%s\")\n", - gridName, *outLine, ST_TILEMAP_W, len, line); - return 0; - } - } - for (col = 0; col < ST_TILEMAP_W; col++) { - if (!charToIndex((unsigned char)line[col], &idx)) { - fprintf(stderr, - "mkstlevel: %s line %d col %d: bad char '%c' (0x%02X)\n", - gridName, *outLine, col + 1, line[col], - (unsigned)(unsigned char)line[col]); - return 0; - } - outCells[row * ST_TILEMAP_W + col] = (uint8_t)idx; - } - } - return 1; -} - - -static int parseLevel(FILE *fp, LevelT *out) { - char line[LINE_MAX]; - int lineNo = 0; - int gotTilemap = 0; - int gotColormap = 0; - - memset(out, 0, sizeof(*out)); - out->bgColor = 0; - out->borderColor = 0; - // Physics defaults match level 1's templates so hand-authored - // levels without an @accel/@gravity directive still play sanely. - out->xAccel = 14; - out->yAccel = 14; - out->xGrav = 0; - out->yGrav = 1; - - while (readLine(fp, line, sizeof(line)) > 0) { - lineNo++; - if (stripLine(line)) { - continue; - } - if (line[0] != '@') { - fprintf(stderr, - "mkstlevel: line %d: expected @directive or comment, got \"%s\"\n", - lineNo, line); - return 0; - } - - if (strncmp(line, "@name ", 6) == 0) { - int len = (int)strlen(line + 6); - if (len > ST_NAME_MAX) { - len = ST_NAME_MAX; - } - memcpy(out->name, line + 6, (size_t)len); - out->name[len] = '\0'; - } else if (strncmp(line, "@tilebank ", 10) == 0) { - out->tileBank = (uint8_t)atoi(line + 10); - } else if (strncmp(line, "@music ", 7) == 0) { - out->music = (uint8_t)atoi(line + 7); - } else if (strncmp(line, "@bgColor ", 9) == 0) { - out->bgColor = (uint8_t)atoi(line + 9); - } else if (strncmp(line, "@borderColor ", 13) == 0) { - out->borderColor = (uint8_t)atoi(line + 13); - } else if (strncmp(line, "@taxiSpawn ", 11) == 0) { - int x = 0; - int y = 0; - if (sscanf(line + 11, "%d %d", &x, &y) != 2) { - fprintf(stderr, "mkstlevel: line %d: bad taxiSpawn\n", lineNo); - return 0; - } - out->taxiSpawnX = (uint8_t)x; - out->taxiSpawnY = (uint8_t)y; - } else if (strncmp(line, "@accel ", 7) == 0) { - int ax = 0; - int ay = 0; - if (sscanf(line + 7, "%d %d", &ax, &ay) != 2) { - fprintf(stderr, "mkstlevel: line %d: bad accel\n", lineNo); - return 0; - } - out->xAccel = (uint8_t)ax; - out->yAccel = (uint8_t)ay; - } else if (strncmp(line, "@gravity ", 9) == 0) { - int gx = 0; - int gy = 0; - if (sscanf(line + 9, "%d %d", &gx, &gy) != 2) { - fprintf(stderr, "mkstlevel: line %d: bad gravity\n", lineNo); - return 0; - } - out->xGrav = (int8_t)gx; - out->yGrav = (int8_t)gy; - } else if (strncmp(line, "@vicColor ", 10) == 0) { - int b1 = 0; - int b2 = 0; - int b3 = 0; - int mc0 = 0; - int mc1 = 0; - int sc0 = 0; - int sc1 = 0; - if (sscanf(line + 10, "%d %d %d %d %d %d %d", - &b1, &b2, &b3, &mc0, &mc1, &sc0, &sc1) != 7) { - fprintf(stderr, "mkstlevel: line %d: bad vicColor (need 7 values)\n", lineNo); - return 0; - } - out->bgColor1 = (uint8_t)b1; - out->bgColor2 = (uint8_t)b2; - out->bgColor3 = (uint8_t)b3; - out->spriteMc0 = (uint8_t)mc0; - out->spriteMc1 = (uint8_t)mc1; - out->sprite0Color = (uint8_t)sc0; - out->sprite1Color = (uint8_t)sc1; - } else if (strncmp(line, "@pad ", 5) == 0) { - char letter = '\0'; // kept in the syntax for readability only - int px = 0; - int py = 0; - int pw = 0; - int sx = -1; // optional passenger stand X (pixels) - int got = sscanf(line + 5, " %c %d %d %d %d", - &letter, &px, &py, &pw, &sx); - if (got != 4 && got != 5) { - fprintf(stderr, "mkstlevel: line %d: bad pad\n", lineNo); - return 0; - } - if (out->padCount >= ST_MAX_PADS) { - fprintf(stderr, "mkstlevel: line %d: too many pads (max %d)\n", - lineNo, ST_MAX_PADS); - return 0; - } - out->pads[out->padCount].tileX = (uint8_t)px; - out->pads[out->padCount].tileY = (uint8_t)py; - out->pads[out->padCount].tileW = (uint8_t)pw; - // Default the passenger stand column to the pad's left edge. - out->pads[out->padCount].standX = - (uint8_t)((sx >= 0) ? sx : (px * 8)); - out->padCount++; - } else if (strcmp(line, "@tilemap") == 0) { - if (!parseGrid(fp, "tilemap", out->tilemap, &lineNo)) { - return 0; - } - gotTilemap = 1; - } else if (strcmp(line, "@colormap") == 0) { - if (!parseGrid(fp, "colormap", out->colormap, &lineNo)) { - return 0; - } - gotColormap = 1; - } else { - fprintf(stderr, "mkstlevel: line %d: unknown directive \"%s\"\n", - lineNo, line); - return 0; - } - } - - if (out->name[0] == '\0') { - fprintf(stderr, "mkstlevel: missing @name\n"); - return 0; - } - if (!gotTilemap) { - fprintf(stderr, "mkstlevel: missing @tilemap\n"); - return 0; - } - if (!gotColormap) { - fprintf(stderr, "mkstlevel: missing @colormap\n"); - return 0; - } - if (out->padCount == 0) { - fprintf(stderr, "mkstlevel: at least one @pad required\n"); - return 0; - } - return 1; -} - - -static int writeOut(const LevelT *L, const char *path) { - FILE *fp; - uint8_t nameLen; - uint8_t i; - size_t cells; - - fp = fopen(path, "wb"); - if (fp == NULL) { - fprintf(stderr, "mkstlevel: cannot open %s for write\n", path); - return 0; - } - if (fputc('S', fp) == EOF || fputc('T', fp) == EOF || - fputc('L', fp) == EOF || fputc('3', fp) == EOF) { - goto fail; - } - nameLen = (uint8_t)strlen(L->name); - if (fputc(nameLen, fp) == EOF) { - goto fail; - } - if (fwrite(L->name, 1, nameLen, fp) != nameLen) { - goto fail; - } - if (fputc(L->tileBank, fp) == EOF || - fputc(L->music, fp) == EOF || - fputc(L->bgColor, fp) == EOF || - fputc(L->borderColor, fp) == EOF || - fputc(L->taxiSpawnX, fp) == EOF || - fputc(L->taxiSpawnY, fp) == EOF) { - goto fail; - } - if (fputc(L->xAccel, fp) == EOF || - fputc(L->yAccel, fp) == EOF || - fputc((uint8_t)L->xGrav, fp) == EOF || - fputc((uint8_t)L->yGrav, fp) == EOF) { - goto fail; - } - if (fputc(L->bgColor1, fp) == EOF || - fputc(L->bgColor2, fp) == EOF || - fputc(L->bgColor3, fp) == EOF || - fputc(L->spriteMc0, fp) == EOF || - fputc(L->spriteMc1, fp) == EOF || - fputc(L->sprite0Color, fp) == EOF || - fputc(L->sprite1Color, fp) == EOF) { - goto fail; - } - if (fputc(L->padCount, fp) == EOF) { - goto fail; - } - for (i = 0; i < L->padCount; i++) { - if (fputc(L->pads[i].tileX, fp) == EOF || - fputc(L->pads[i].tileY, fp) == EOF || - fputc(L->pads[i].tileW, fp) == EOF || - fputc(L->pads[i].standX, fp) == EOF) { - goto fail; - } - } - // STL3 stores no fares -- fares are RNG-chosen at runtime. - cells = (size_t)ST_TILEMAP_W * ST_PLAYFIELD_ROWS; - if (fwrite(L->tilemap, 1, cells, fp) != cells) { - goto fail; - } - if (fwrite(L->colormap, 1, cells, fp) != cells) { - goto fail; - } - fclose(fp); - return 1; -fail: - fclose(fp); - fprintf(stderr, "mkstlevel: write error\n"); - return 0; -} - - -int main(int argc, char **argv) { - FILE *fp; - LevelT L; - - if (argc != 3) { - fprintf(stderr, "usage: %s input.txt output.dat\n", argv[0]); - return 2; - } - fp = fopen(argv[1], "r"); - if (fp == NULL) { - fprintf(stderr, "mkstlevel: cannot open %s\n", argv[1]); - return 1; - } - if (!parseLevel(fp, &L)) { - fclose(fp); - return 1; - } - fclose(fp); - if (!writeOut(&L, argv[2])) { - return 1; - } - printf("mkstlevel: wrote %s (name=\"%s\", pads=%u)\n", - argv[2], L.name, (unsigned)L.padCount); - return 0; -} diff --git a/examples/spacetaxi/per-level-data.txt b/examples/spacetaxi/per-level-data.txt deleted file mode 100644 index f04d86f..0000000 --- a/examples/spacetaxi/per-level-data.txt +++ /dev/null @@ -1,26 +0,0 @@ -Lvl Pads SpawnY SpawnX X-acc Y-grav Border/BG Pad-style-bytes (byte 7 of each 8-byte slot) --------------------------------------------------------------------------------------------------- -A 1 $4A00 $0A00 $0E $01 $00/$00 $C2 -B 3 $8800 $8800 $11 $01 $00/$00 $C2 $C2 $C4 -C 5 $4400 $AA00 $12 $02 $00/$00 $C2 $C2 $C4 $C2 $C4 -D 10 $8800 $8800 $11 $06 $00/$00 $FF $FF $FF $FF $FF $FF $FF $FF $FF $FF -E 9 $C800 $FA00 $11 $06 $09/$00 $C2 $C2 $C4 $C2 $C4 $C2 $C4 $C2 $C4 -F 3 $6400 $AA00 $15 $03 $00/$00 $C2 $C4 $C4 -G 5 $5600 $AF00 $14 $06 $0C/$00 $C2 $C2 $C2 $C2 $C4 -H 5 $3E00 $9600 $19 $06 $00/$00 $C4 $C4 $C2 $C2 $C2 -I 7 $6E00 $AA00 $15 $03 $00/$00 $C2 $C2 $C2 $C2 $C4 $C4 $C4 -J 6 $3E00 $1E00 $19 $05 $09/$00 $C2 $C2 $C4 $C2 $C4 $C4 -K 5 $5F00 $B400 $17 $F9 $00/$00 $C2 $C2 $C2 $C4 $C4 -L 6 $4600 $6E00 $1A $03 $0B/$0B $C2 $C4 $C2 $C2 $C4 $C4 -M 10 $3E00 $AF00 $40 $06 $06/$00 $C2 $C2 $C4 $C4 $C4 $C4 $C4 $C2 $C2 $C2 -N 8 $3E00 $AF00 $12 $06 $00/$00 $C4 $C2 $C2 $C4 $C4 $C4 $C2 $C2 -O 1 $3400 $2E00 $15 $03 $00/$00 $C2 -P 3 $4A00 $2400 $15 $06 $0B/$00 $C2 $C4 $C2 -Q 8 $4600 $B400 $19 $05 $0B/$00 $C4 $C2 $C2 $C4 $C2 $C4 $C2 $C4 -R 1 $3800 $E600 $10 $01 $00/$00 $C4 -S 5 $4400 $AA00 $12 $03 $07/$08 $C2 $C2 $C4 $C2 $C4 -T 8 $C800 $AF00 $1B $06 $06/$00 $C2 $C4 $C2 $C2 $C4 $C4 $C2 $C4 -U 5 $4A00 $AA00 $18 $02 $00/$00 $C4 $C2 $C2 $C4 $C2 -V 7 $3E00 $AF00 $1E $06 $06/$00 $C2 $C2 $C2 $C2 $C2 $C4 $C4 -W 3 $4600 $3200 $1E $03 $00/$00 $C4 $C2 $C2 -X 8 $4600 $AF00 $17 $06 $0B/$00 $C4 $C4 $C4 $C4 $C2 $C2 $C2 $C2 diff --git a/examples/spacetaxi/spacetaxi.c b/examples/spacetaxi/spacetaxi.c deleted file mode 100644 index d67d548..0000000 --- a/examples/spacetaxi/spacetaxi.c +++ /dev/null @@ -1,1191 +0,0 @@ -// Space Taxi (JoeyLib port) -- front-end flow and frame pacing. -// -// Build: see make/{dos,amiga,atarist,iigs}.mk -- target is -// `EXAMPLE=spacetaxi`. -// -// The game runs in the host-independent simulation (stSim.c and -// friends); this file is the C64's outer flow around it: the title -// with its intro, the attract-demo rotation, the two GAME VARIATIONS -// menus, the level-name intro screen, the players' turns, game over -// and the return to the title ($5EC4 / $48AD / $4741 / $5010 / $61FB). -// -// Pacing: the C64 runs one game tick per two video frames (its main -// loop waits on the raster twice), 30 Hz on NTSC. Every port steps the -// simulation from its own vertical blank count: exactly one tick per -// two frames on 60 Hz and 50 Hz displays (the latter is the PAL C64's -// own cadence), and three ticks per seven frames on the 70 Hz VGA -// mode, so motion is a steady multiple of the display and never -// keyed to a wall clock. - -#include - -#include - -#include "spacetaxi.h" -#include "stSongData.h" -#include "stDemoStreams.h" - -#define ST_TICK_HZ 30u -#define ST_MAX_CATCHUP_TICKS 4u -// The C64 runs the game every other frame but the level intro ($4666) -// every frame. -#define ST_C64_FRAMES_PER_TICK 2u -// The intro's loop count ($4508) runs 0..3 and the cab's judder ($470D) -// steps -2 into count 0; batches that cannot show every frame end on the -// counts either side of that jump (runIntro). -#define ST_INTRO_LOOP_MASK 3u -#define ST_INTRO_SHOW_BEFORE 3u -#define ST_INTRO_SHOW_AFTER 0u -// Screens A..X, then screen Y -- the 25th, " MYSTERY SCREEN !!! " -- which -// $5167 loads once the 24-hour shifts have cleared all of A..X. Files are -// levels/level01.dat .. level25.dat, so Y is index 24. -#define ST_LEVEL_COUNT 24u // A..X, the screens a shift sweeps -#define ST_SCREEN_Y 24u // the 25th screen ($5171 LDA #$59) -#define ST_LEVEL_FILES 25u // level01.dat .. level25.dat -// "GET READY" shows over the old screen while the C64 cycles the -// leaving-passenger sprites (three busy waits, about half a second). -#define ST_GET_READY_TICKS 15u -// $5FBB: the GAME OVER pause is six full busy waits, about two seconds. -#define ST_GAME_OVER_TICKS 60u -// Menu joystick repeat delay ($4101 with X = $82). -#define ST_MENU_REPEAT_TICKS 8u -// How long a fatal load message stays up, in display frames, and how many -// must pass before a joystick press can cut it short (a button held from -// before must not skip the message). -#define ST_FATAL_HOLD_FRAMES 600u -#define ST_FATAL_ARM_FRAMES 30u - -// The immortal cabbies ($4A89 image, $4E35 screen). -#define ST_HS_ENTRIES 10u -#define ST_HS_NAME_CHARS 15u -#define ST_HS_ENTRY_BYTES 24u // score[7] 0 name[15] shift -#define ST_HS_POINT_AT 4u // "dddd.dd" -#define ST_HS_GREET_DIGIT_AT 23u // "CONGRATULATIONS CABBIE n," -#define ST_HS_NAME_COL 12u // $0664: row 15, column 12 -#define ST_HS_NAME_ROW 15u -#define ST_HS_CURSOR '%' // $4F4D flips it against blank -#define ST_HS_BLINK_TICKS 3u // $4101 x $1E between flips, ~55 ms -#define ST_HS_SAVE_NAME "IMMORTAL" - -typedef enum { - ST_STATE_TITLE = 0, - ST_STATE_HIGHSCORES, - ST_STATE_ABOUT, - ST_STATE_CABBIES, - ST_STATE_SHIFT, - ST_STATE_GET_READY, - ST_STATE_INTRO, - ST_STATE_PLAYING, - ST_STATE_GAME_OVER, - ST_STATE_JINGLE, // a $44CF tune sounding; sim frozen until it ends - ST_STATE_NAME_ENTRY // $4E35: a top-ten cabbie types a name -} StStateE; - -typedef struct { - StStateE state; - bool demo; // the attract demo owns PLAYING - uint8_t demoRotation; // $5143 - uint8_t variation; // $7221: 1..5 - uint8_t usedLevels[ST_LEVEL_COUNT]; // $5254 (random shift) - uint8_t pendingLevel; // $716B as an index - uint16_t waitTicks; - uint8_t menuSel; // cabbies 0..3 / shift 0..4 - uint8_t menuRepeat; - bool fireArmed; // $445D: FIRE must be released first - uint8_t lastFrame; - uint16_t paceAcc; - uint16_t frameAcc; // C64 frames owed, in paceAcc's units - int8_t introOwed; // intro frames owed to (+) or run ahead of (-) the next batch - StStateE afterJingle; // where ST_STATE_JINGLE returns to - bool gameOverTune; // $5C24: song 5 started after the wait - uint8_t hsSlot; // $4D51 table slot being named - uint8_t hsLen; // $4D2C characters typed - uint8_t hsCursor; // $4F4D the blinking cursor's current glyph - uint8_t hsBlinkTicks; // ticks toward the next cursor flip - bool paused; // $4FCB: SHIFT is holding the game - bool fatal; // an asset on the disk would not load -} StGameT; - -// The immortal cabbies ($4A89): ten entries of a right-aligned "dddd.dd" -// score, a name of up to fifteen letters and the shift it was earned on. -// The C64 keeps this table on its own disk (the $9948 IEC routines load -// it at boot and save it after every entry); the port keeps the same -// 240-byte image in a save file. Shipped contents as the dump has them. -typedef struct { - uint8_t score[ST_NUMBER_CHARS + 1u]; // ASCII, NUL-terminated - uint8_t name[ST_HS_NAME_CHARS + 1u]; // ASCII, space-padded, NUL-terminated - uint8_t shift; // 1..5 -} StHighScoreT; - -static StHighScoreT gHighScores[ST_HS_ENTRIES] = { - { "3877.56", "MICHAEL PLATE ", 4u }, - { "1809.51", "MICHAEL PLATE ", 2u }, - { "1179.87", "ANDREAS PLATE ", 4u }, - { " 892.65", "MICHAEL PLATE ", 1u }, - { " 685.37", "ANDREAS PLATE ", 1u }, - { " 629.45", "MICHAEL PLATE ", 4u }, - { " 569.50", "MICHAEL PLATE ", 1u }, - { " 504.97", "ANDREAS PLATE ", 2u }, - { " 498.77", "ANDREAS PLATE ", 5u }, - { " 490.85", "ANDREAS PLATE ", 2u }, -}; - -// Menu texts ($5347..$545D) in screen codes. -static const uint8_t kTextGameVariations[] = "GAME VARIATIONS"; -static const uint8_t kTextCabbies[] = "SELECT NUMBER OF CABBIES:"; -static const uint8_t kTextDigits[] = "1 2 3 4"; -static const uint8_t kTextLeftRight[] = "USE JOYSTICK: LEFT OR RIGHT,"; -static const uint8_t kTextFire[] = "FIRE TO SELECT"; -static const uint8_t kTextSelectShift[] = "SELECT SHIFT:"; -static const uint8_t *kTextShifts[5] = { - (const uint8_t *)"1. MORNING SHIFT - BEGINNER", - (const uint8_t *)"2. DAY SHIFT - INTERMEDIATE", - (const uint8_t *)"3. NIGHT SHIFT - EXPERT", - (const uint8_t *)"4. STANDARD 24 HOUR SHIFT", - (const uint8_t *)"5. RANDOM 24 HOUR SHIFT", -}; -static const uint8_t kTextUpDown[] = "USE JOYSTICK: UP OR DOWN,"; -static const uint8_t kTextGetReady[] = " GET READY FOR A RIDE "; -static const uint8_t kTextOnTheTaxi[] = " ON THE SPACE TAXI! "; -static const uint8_t kTextGameOver[] = " GAME OVER! "; -static const uint8_t kTextBlank12[] = " "; -// $4C1F (the table) and $4E35 (the name entry) screens. -static const uint8_t kTextImmortal[] = "THE IMMORTAL CABBIES"; -static const uint8_t kTextShift[] = "SHIFT"; -static const uint8_t kTextReturnTitle[] = "PRESS FIRE TO RETURN TO TITLE PAGE"; -static const uint8_t kTextCongrats[] = "CONGRATULATIONS CABBIE ,"; -static const uint8_t kTextRanks[] = "YOUR SCORE RANKS YOU AS ONE OF"; -static const uint8_t kTextTopTen[] = "THE TOP TEN CABBIES OF ALL TIME"; -static const uint8_t kTextEnterName[] = "ENTER YOUR NAME BELOW"; -static const uint8_t *kAboutLines[7] = { - (const uint8_t *)"THIS PROGRAM DEVELOPED AND WRITTEN BY", - (const uint8_t *)"JOHN F. KUTCHER", - (const uint8_t *)"11/21/65", - (const uint8_t *)"CURRENTLY, AS OF JANUARY 1984,", - (const uint8_t *)"HE IS ATTENDING JOHNS HOPKINS", - (const uint8_t *)"UNIVERSITY IN BALTIMORE, MD", - (const uint8_t *)"ALSO TRY RESCUE SQUAD BY JOHN KUTCHER", -}; -static const uint8_t kTextReturn[] = "PRESS FIRE TO RETURN"; -// Fatal load messages. Two short centred lines in the game's own charset, -// upper case because the C64 font has no lower case in this bank. -static const char kTextFatalMemory[] = "OUT OF MEMORY"; -static const char kTextFatalSpeech[] = "SPEECH WILL NOT LOAD"; -static const char kTextFatalSprites[] = "SPRITES WILL NOT COMPILE"; -static const char kTextFatalLoad[] = "DISK ERROR"; -static const char kTextFatalTitle[] = "CANNOT LOAD TITLE SCREEN"; -static const char kTextFatalLevel[] = "CANNOT LOAD LEVEL"; - -static StGameT gGame; -static StSimT gSim; -static jlSurfaceT *gStage; -// One level buffer serves both gameplay and the title screen: the title -// is another scene, and gameplay never needs it at the same time, so it -// is loaded into gLevel on each return to the title. (Keeping a second -// StLevelT cost ~3.7 KB of static state the IIgs bank-0 BSS can't spare.) -static StLevelT gLevel; - - -static void blankScreen(StSimT *sim, uint8_t border, uint8_t bg); -static void enterTitle(void); -static void fatalHold(void); -static void fatalStop(const char *line1, const char *line2); -static void gameOverEnter(void); -static void gameOverFinish(void); -static void highScoreDraw(void); -static uint8_t highScoreInsert(const uint8_t *score); -static void highScoreLoad(void); -static void highScoreSave(void); -static bool joyFire(void); -static uint8_t joyMask(void); -static bool loadLevel(uint8_t index); -static uint8_t nextLevelForShift(void); -static uint8_t paceTicks(uint8_t *c64Frames); -static bool serviceJingle(void); -static void runCabbiesMenu(uint8_t ticks); -static void runDemoEnd(void); -static void runIntro(uint8_t c64Frames); -static void runNameEntry(uint8_t ticks); -static bool nameEntryStart(void); -static void runPlaying(uint8_t ticks); -static void runShiftMenu(uint8_t ticks); -static void runTitle(uint8_t ticks); -static void shiftMenuColor(uint8_t row, uint8_t color); -static void showCabbiesMenu(void); -static void showShiftMenu(void); -static void startDemoScreen(void); -static void startIntro(void); -static void startNewScreen(void); - - -// $40CA -- spaces everywhere, black. -static void blankScreen(StSimT *sim, uint8_t border, uint8_t bg) { - memset(sim->screen, ST_CHAR_SPACE, ST_SCREEN_CELLS); - memset(sim->color, 0, ST_SCREEN_CELLS); - stSimDirtyAll(sim); - sim->bgColor = bg; - sim->borderColor = border; - sim->frame.enableMask = 0u; - stRenderSceneChanged(sim); -} - - -// $477A -- back to the title, intro from the top. -static void enterTitle(void) { - gSim.demoMode = 0u; - if (!stLevelLoad(&gLevel, "levels/title.dat")) { - fatalStop(kTextFatalLoad, kTextFatalTitle); - return; - } - (void)stRenderLevelCels(gStage, 0); // the last level's cels go - stTitleEnter(&gSim, &gLevel); - stRenderSceneChanged(&gSim); - stRenderTitleLogo(&gSim); - stAudioNoise(false); - stAudioSilence(); - jlMusicStop(); - gGame.state = ST_STATE_TITLE; - gGame.fireArmed = false; -} - - -// $6BD0 -- one more player done; everybody done -> title. -static void gameOverFinish(void) { - gSim.playersDone++; - if (gSim.playersDone == gSim.playerCount) { - enterTitle(); - } else { - startNewScreen(); - } -} - - -// $4C1F -- the immortal cabbies: ten rows of shift, "$", score and name, -// each entry drawn only while its last score digit is a digit (a blank -// row is an unused slot). Song 7 plays under it until FIRE ($4D04). -static void highScoreDraw(void) { - uint8_t i; - - blankScreen(&gSim, 3u, 14u); - stSimDrawText(&gSim, 9u, 1u, kTextImmortal, 6u); - stSimDrawText(&gSim, 32u, 3u, kTextShift, 15u); - for (i = 0u; i < ST_HS_ENTRIES; i++) { - const StHighScoreT *e = &gHighScores[i]; - uint8_t row = (uint8_t)(4u + i * 2u); - if (e->score[ST_NUMBER_CHARS - 1u] < '0') { - continue; - } - stSimPutChar(&gSim, 34u, row, (uint8_t)('0' + e->shift)); - stSimPutColor(&gSim, 34u, row, 15u); - stSimPutChar(&gSim, 4u, row, '$'); - stSimPutColor(&gSim, 4u, row, 1u); - stSimDrawText(&gSim, 5u, row, e->score, 1u); - stSimDrawText(&gSim, 15u, row, e->name, 1u); - } - stSimDrawText(&gSim, 3u, 24u, kTextReturnTitle, 0u); - stAudioMusic(ST_SONG_SCORES, false); // $4C29 -} - - -// $4D92 -- where a score belongs in the table, or ST_HS_ENTRIES when it -// does not make it. The table is only opened to scores of $50.00 and up -// (second character a digit, or third character 5 or more). The scores -// are right-aligned text, so a character compare orders them; a tie goes -// above the older entry. The new entry is opened with a blank name. -static uint8_t highScoreInsert(const uint8_t *score) { - uint8_t slot; - uint8_t k; - - if (score[1] < '0' && score[2] < '5') { - return ST_HS_ENTRIES; - } - for (slot = 0u; slot < ST_HS_ENTRIES; slot++) { - const uint8_t *old = gHighScores[slot].score; - for (k = 0u; k < ST_NUMBER_CHARS; k++) { - if (score[k] != old[k]) { - break; - } - } - if (k == ST_NUMBER_CHARS || score[k] > old[k]) { - break; - } - } - if (slot == ST_HS_ENTRIES) { - return ST_HS_ENTRIES; - } - for (k = (uint8_t)(ST_HS_ENTRIES - 1u); k > slot; k--) { - gHighScores[k] = gHighScores[k - 1u]; - } - memcpy(gHighScores[slot].score, score, ST_NUMBER_CHARS + 1u); - memset(gHighScores[slot].name, ' ', ST_HS_NAME_CHARS); - gHighScores[slot].name[ST_HS_NAME_CHARS] = 0u; - gHighScores[slot].shift = 0u; - return slot; -} - - -// $23E5 -> $9948 at boot: the table from disk, when there is one. The -// image is the C64's own: per entry score[7], 0, name[15], shift. -static void highScoreLoad(void) { - uint8_t image[ST_HS_ENTRIES * ST_HS_ENTRY_BYTES]; - uint8_t i; - - if (jlSaveRead(ST_HS_SAVE_NAME, image, sizeof(image)) != sizeof(image)) { - return; - } - for (i = 0u; i < ST_HS_ENTRIES; i++) { - const uint8_t *e = &image[i * ST_HS_ENTRY_BYTES]; - if (e[ST_NUMBER_CHARS] != 0u) { - ST_LOG("spacetaxi: ! %s is not a score table", ST_HS_SAVE_NAME); - return; - } - } - for (i = 0u; i < ST_HS_ENTRIES; i++) { - const uint8_t *e = &image[i * ST_HS_ENTRY_BYTES]; - memcpy(gHighScores[i].score, e, ST_NUMBER_CHARS); - gHighScores[i].score[ST_NUMBER_CHARS] = 0u; - memcpy(gHighScores[i].name, &e[ST_NUMBER_CHARS + 1u], ST_HS_NAME_CHARS); - gHighScores[i].name[ST_HS_NAME_CHARS] = 0u; - gHighScores[i].shift = e[ST_HS_ENTRY_BYTES - 1u]; - } -} - - -// $4E11 -> $9948 after an entry: the table back to disk. -static void highScoreSave(void) { - uint8_t image[ST_HS_ENTRIES * ST_HS_ENTRY_BYTES]; - uint8_t i; - - for (i = 0u; i < ST_HS_ENTRIES; i++) { - uint8_t *e = &image[i * ST_HS_ENTRY_BYTES]; - memcpy(e, gHighScores[i].score, ST_NUMBER_CHARS); - e[ST_NUMBER_CHARS] = 0u; - memcpy(&e[ST_NUMBER_CHARS + 1u], gHighScores[i].name, ST_HS_NAME_CHARS); - e[ST_HS_ENTRY_BYTES - 1u] = gHighScores[i].shift; - } - if (!jlSaveWrite(ST_HS_SAVE_NAME, image, sizeof(image))) { - ST_LOG("spacetaxi: ! could not save %s", ST_HS_SAVE_NAME); - } -} - - -// $4D52 -- after a player's GAME OVER: their score as "dddd.dd" text, and -// if it ranks, the $4E35 screen: red border, grey field, the cabbie's -// number in the greeting, the score at the top, song 6, and a blinking -// cursor where the name goes. Returns false when the score did not rank -// and the game moves straight on. -static bool nameEntryStart(void) { - uint8_t score[ST_NUMBER_CHARS + 1u]; - uint8_t greet[sizeof(kTextCongrats)]; - uint8_t k; - - for (k = 0u; k < ST_NUMBER_CHARS; k++) { - uint8_t ch = gSim.scoreBackup[gSim.player][k]; - if (ch == ST_CHAR_BLANK) { - score[k] = ' '; - } else { - uint8_t d = (uint8_t)(ch - ST_CHAR_DIGIT_BASE); // $4345 - score[k] = (uint8_t)('0' + ((d == 10u) ? 0u : d)); - } - } - score[ST_HS_POINT_AT] = '.'; - score[ST_NUMBER_CHARS] = 0u; - gGame.hsSlot = highScoreInsert(score); - if (gGame.hsSlot == ST_HS_ENTRIES) { - return false; - } - blankScreen(&gSim, 2u, 15u); - while (jlInputGetChar() >= 0) { // $0277 = 0: nothing typed earlier counts - } - memcpy(greet, kTextCongrats, sizeof(greet)); - greet[ST_HS_GREET_DIGIT_AT] = (uint8_t)('1' + gSim.player); - stSimDrawText(&gSim, 7u, 3u, greet, 0u); - stSimDrawText(&gSim, 5u, 5u, kTextRanks, 0u); - stSimDrawText(&gSim, 4u, 7u, kTextTopTen, 0u); - stSimDrawText(&gSim, 9u, 9u, kTextEnterName, 0u); - stSimDrawText(&gSim, 16u, 0u, score, 6u); - stSimPutChar(&gSim, 15u, 0u, '$'); - stSimPutColor(&gSim, 15u, 0u, 6u); - stAudioMusic(ST_SONG_NAME_ENTRY, false); // $4EB6 - gGame.hsLen = 0u; - gGame.hsCursor = ST_HS_CURSOR; - gGame.hsBlinkTicks = 0u; - gGame.state = ST_STATE_NAME_ENTRY; - return true; -} - - -// $4ED8 -- the entry loop: the cursor flips between "%" and blank, then -// the keyboard: letters, space, ".", "!" and "," go in (fifteen at most), -// DEL takes one back, RETURN keeps the name (a "*" when none was typed), -// stamps the shift, stops the tune and saves the table. -static void runNameEntry(uint8_t ticks) { - StHighScoreT *e = &gHighScores[gGame.hsSlot]; - uint8_t col = (uint8_t)(ST_HS_NAME_COL + gGame.hsLen); - int c; - - gGame.hsBlinkTicks = (uint8_t)(gGame.hsBlinkTicks + ticks); - if (gGame.hsBlinkTicks >= ST_HS_BLINK_TICKS) { - gGame.hsBlinkTicks = 0u; - gGame.hsCursor ^= ST_HS_CURSOR ^ ' '; - stSimPutChar(&gSim, col, ST_HS_NAME_ROW, gGame.hsCursor); - stSimPutColor(&gSim, col, ST_HS_NAME_ROW, 6u); - } - while ((c = jlInputGetChar()) >= 0) { - if (c >= 'a' && c <= 'z') { - c = c - 'a' + 'A'; - } - if (c == JL_CHAR_RETURN) { - stSimPutChar(&gSim, col, ST_HS_NAME_ROW, ' '); - if (gGame.hsLen == 0u) { - e->name[0] = '*'; - } - e->shift = gGame.variation; - jlMusicStop(); // $44E1 - highScoreSave(); // $4E11 - gameOverFinish(); - return; - } - if (c == JL_CHAR_BACKSPACE || c == JL_CHAR_DELETE) { - if (gGame.hsLen != 0u) { - stSimPutChar(&gSim, col, ST_HS_NAME_ROW, ' '); - gGame.hsLen--; - col--; - e->name[gGame.hsLen] = ' '; - } - continue; - } - if (c != ' ' && c != '.' && c != '!' && c != ',' && (c < 'A' || c > 'Z')) { - continue; - } - if (gGame.hsLen == ST_HS_NAME_CHARS) { - continue; - } - e->name[gGame.hsLen] = (uint8_t)c; - stSimPutChar(&gSim, col, ST_HS_NAME_ROW, (uint8_t)c); - stSimPutColor(&gSim, col, ST_HS_NAME_ROW, 6u); - gGame.hsLen++; - col++; - } -} - - -// Hold a fatal message on screen long enough to be read, then let the -// caller shut down. FIRE or a key cuts the wait short. -static void fatalHold(void) { - uint8_t last = (uint8_t)jlFrameCount(); - uint16_t held = 0u; - - // Count display frames, not loop turns: presenting an unchanged - // screen costs almost nothing, so a turn count would run out in well - // under a second on the faster ports. - while (held < ST_FATAL_HOLD_FRAMES) { - uint8_t now; - - jlInputPoll(); - if (held > ST_FATAL_ARM_FRAMES && (joyMask() & 0x1Fu) != 0u) { - return; - } - now = (uint8_t)jlFrameCount(); - held = (uint16_t)(held + (uint8_t)(now - last)); - last = now; - jlStagePresent(); - } -} - - -// An asset the game ships with will not load. Say so on screen and stop: -// a mute game, or one running every sprite through the interpreter, reads -// as a slow machine rather than a broken install, and that is worse than -// not starting. -static void fatalStop(const char *line1, const char *line2) { - ST_LOG("spacetaxi: ! FATAL -- %s / %s", line1, line2); - ST_LOG_FLUSH(); - stAudioSilence(); - stAudioNoise(false); - jlMusicStop(); - stRenderFatal(gStage, line1, line2); - gGame.fatal = true; -} - - -// $5FBB -- "GAME OVER!" over the play field, sprites hidden. -static void gameOverEnter(void) { - stSimDrawText(&gSim, 14u, 10u, kTextBlank12, 1u); - stSimDrawText(&gSim, 14u, 11u, kTextGameOver, 1u); - stSimDrawText(&gSim, 14u, 12u, kTextBlank12, 1u); - gSim.frame.enableMask = 0u; - stAudioNoise(false); - stAudioSilence(); - gGame.waitTicks = ST_GAME_OVER_TICKS; - gGame.gameOverTune = false; - gGame.state = ST_STATE_GAME_OVER; -} - - -static bool joyFire(void) { - return jlKeyDown(KEY_SPACE) || jlJoyDown(JOYSTICK_0, JOY_BUTTON_0); -} - - -// The C64's EOR'd $DC00: bit0 UP, 1 DOWN, 2 LEFT, 3 RIGHT, 4 FIRE, bit 7 set. -static uint8_t joyMask(void) { - int8_t jx = jlJoystickX(JOYSTICK_0); - int8_t jy = jlJoystickY(JOYSTICK_0); - uint8_t mask = 0x80u; - - if (jlKeyDown(KEY_UP) || jy < -32) { - mask |= 0x01u; - } - if (jlKeyDown(KEY_DOWN) || jy > 32) { - mask |= 0x02u; - } - if (jlKeyDown(KEY_LEFT) || jx < -32) { - mask |= 0x04u; - } - if (jlKeyDown(KEY_RIGHT) || jx > 32) { - mask |= 0x08u; - } - if (joyFire()) { - mask |= 0x10u; - } - return mask; -} - - -static bool loadLevel(uint8_t index) { - char path[32]; - - if (index >= ST_LEVEL_FILES) { - return false; - } - // Hand-rolled "levels/levelNN.dat" (NN = index+1, 01..24) so the - // binary does not drag in the whole formatted-output machinery. - { - uint8_t n = (uint8_t)(index + 1u); - memcpy(path, "levels/level", 12u); - path[12] = (char)('0' + n / 10u); - path[13] = (char)('0' + n % 10u); - memcpy(path + 14, ".dat", 5u); - } - if (!stLevelLoad(&gLevel, path)) { - ST_LOG("spacetaxi: ! cannot load %s", path); - return false; - } - return stRenderLevelCels(gStage, &gLevel); -} - - -// $5010 -- which screen comes next for the chosen shift. Returns 0xFF -// when the shift is over. -static uint8_t nextLevelForShift(void) { - StSimT *sim = &gSim; - - if (gGame.variation <= 3u) { - // $517D: eight screens per shift, A..H / I..P / Q..X. - if (sim->levelState == 8u) { - return 0xFFu; - } - return (uint8_t)((gGame.variation - 1u) * 8u + sim->levelState); - } - // $5022/$5167: the 24-hour shifts play screen Y after A..X, and only - // then is the shift over -- which is also what lights the "finished" - // marker in the screens-completed field (levelState reaches 25). - if (sim->levelState == ST_LEVEL_COUNT) { - return ST_SCREEN_Y; - } - if (sim->levelState > ST_LEVEL_COUNT) { - return 0xFFu; - } - if (gGame.variation == 4u) { - return sim->levelState; - } - // $521A: random order without repeats. - { - uint8_t x = (uint8_t)(stSimRng(sim, ST_LEVEL_COUNT) - 1u); - while (gGame.usedLevels[x] != 0u) { - x = (x == 0u) ? (uint8_t)(ST_LEVEL_COUNT - 1u) : (uint8_t)(x - 1u); - } - gGame.usedLevels[x] = 1u; - return x; - } -} - - -// Game ticks due this rendered frame, from the vertical blank count: -// one tick per two frames on 50/60 Hz displays, three per seven on -// the 70 Hz VGA mode. jlFrameCount is 8-bit-wrapped so a stall of any -// length just clamps to the catch-up cap. *c64Frames gets the C64 frames -// due as well, ST_C64_FRAMES_PER_TICK a tick: one per display frame at -// 50/60 Hz, so the level intro shows every frame the C64 does -- the -// cab's up-and-down judder ($470D) is a one-frame pattern that stepping -// two frames at a time cancels out. -static uint8_t paceTicks(uint8_t *c64Frames) { - uint16_t hz = jlFrameHz(); - uint8_t now = (uint8_t)jlFrameCount(); - uint8_t frames = (uint8_t)(now - gGame.lastFrame); - uint8_t ticks = 0u; - uint16_t perTick = ST_TICK_HZ; - - gGame.lastFrame = now; - if (frames > 16u) { - frames = 16u; - } - if (hz == 50u || hz == 60u) { - perTick = 35u; - hz = 70u; - } - gGame.paceAcc += (uint16_t)(frames * perTick); - gGame.frameAcc += (uint16_t)(frames * perTick * ST_C64_FRAMES_PER_TICK); - *c64Frames = 0u; - while (gGame.frameAcc >= hz && *c64Frames < ST_MAX_CATCHUP_TICKS * ST_C64_FRAMES_PER_TICK) { - gGame.frameAcc = (uint16_t)(gGame.frameAcc - hz); - (*c64Frames)++; - } - if (gGame.frameAcc > (uint16_t)(hz * ST_MAX_CATCHUP_TICKS * ST_C64_FRAMES_PER_TICK)) { - gGame.frameAcc = (uint16_t)(hz * ST_MAX_CATCHUP_TICKS * ST_C64_FRAMES_PER_TICK); - } - // Ticks not run this frame stay owed (a slow frame catches up over - // the next ones); a stall longer than the cap's worth is forgiven. - while (gGame.paceAcc >= hz && ticks < ST_MAX_CATCHUP_TICKS) { - gGame.paceAcc = (uint16_t)(gGame.paceAcc - hz); - ticks++; - } - if (gGame.paceAcc > (uint16_t)(hz * ST_MAX_CATCHUP_TICKS)) { - gGame.paceAcc = (uint16_t)(hz * ST_MAX_CATCHUP_TICKS); - } - return ticks; -} - - -// $52F7 -- LEFT/RIGHT cycle 1..4, FIRE confirms. -static void runCabbiesMenu(uint8_t ticks) { - uint8_t mask = joyMask(); - - if (!gGame.fireArmed) { - if ((mask & 0x10u) == 0u) { - gGame.fireArmed = true; - } - return; - } - if ((mask & 0x10u) != 0u) { - stSimPutColor(&gSim, (uint8_t)(28u + gGame.menuSel * 2u), 2u, 1u); - gSim.playerCount = (uint8_t)(gGame.menuSel + 1u); - showShiftMenu(); - return; - } - if (gGame.menuRepeat > 0u) { - gGame.menuRepeat = (uint8_t)(gGame.menuRepeat - ((ticks < gGame.menuRepeat) ? ticks : gGame.menuRepeat)); - return; - } - if ((mask & 0x0Cu) != 0u) { - stSimPutColor(&gSim, (uint8_t)(28u + gGame.menuSel * 2u), 2u, 6u); - if ((mask & 0x08u) != 0u) { - gGame.menuSel = (uint8_t)((gGame.menuSel + 1u) & 3u); - } else { - gGame.menuSel = (uint8_t)((gGame.menuSel - 1u) & 3u); - } - stSimPutColor(&gSim, (uint8_t)(28u + gGame.menuSel * 2u), 2u, 3u); - gGame.menuRepeat = ST_MENU_REPEAT_TICKS; - } -} - - -// The demo crashed, flew out or was interrupted: $5EC4 -> title. -static void runDemoEnd(void) { - gGame.demo = false; - enterTitle(); -} - - -// $4666 loop: once per C64 frame; joystick ends a demo. A port that shows -// every frame shows the cab's one-frame judder ($470D) as the C64 does. One -// that shows only some would sample it at a fixed phase -- every second frame -// shows -1, 0, -1, 0, a smooth climb -- so when two or more frames are due -// the batch is cut or stretched by one (evened out on the next batch) to end -// on the loop counts either side of the -2 jump: the frames shown then cross -// it and the cab judders, at the same average speed. -static void runIntro(uint8_t c64Frames) { - static const int8_t kTry[] = { 0, -1, 1 }; - int16_t due; - uint8_t steps; - uint8_t end; - uint8_t t; - uint8_t k; - - if (gGame.demo && (joyMask() & 0x1Fu) != 0u) { - runDemoEnd(); - return; - } - due = (int16_t)(c64Frames + gGame.introOwed); - gGame.introOwed = 0; - steps = (uint8_t)((due > 0) ? due : 0); - if (c64Frames >= ST_C64_FRAMES_PER_TICK) { - for (t = 0u; t < (uint8_t)sizeof(kTry); t++) { - if (due + kTry[t] < 1) { - continue; - } - end = (uint8_t)((gSim.introLoopCount + due + kTry[t]) & ST_INTRO_LOOP_MASK); - if (end == ST_INTRO_SHOW_BEFORE || end == ST_INTRO_SHOW_AFTER) { - steps = (uint8_t)(due + kTry[t]); - gGame.introOwed = (int8_t)(-kTry[t]); - break; - } - } - } - for (k = 0u; k < steps; k++) { - if (stIntroStep(&gSim)) { - jlMusicStop(); // $4709 - stSimEnterLevel(&gSim, &gLevel); - stRenderSceneChanged(&gSim); - if (gGame.demo) { - const StDemoEntryT *e = &kDemoRotation[(uint8_t)(gGame.demoRotation - 1u) & 3u]; - stSimSetDemoStream(&gSim, e->stream, e->length); - } - gGame.state = ST_STATE_PLAYING; - serviceJingle(); - return; - } - } -} - - -// The main loop proper, one $5F40 iteration per tick. -static void runPlaying(uint8_t ticks) { - uint8_t k; - - gSim.rawInput = joyMask(); - for (k = 0u; k < ticks; k++) { - StTickResultE r = stSimTick(&gSim); - stAudioTick(); - if (r == ST_TICK_CONTINUE) { - if (serviceJingle()) { - return; // $70D4 bonus cab: the tune blocks - } - continue; - } - if (gGame.demo) { - runDemoEnd(); - return; - } - switch (r) { - case ST_TICK_LEVEL_EXIT: - startNewScreen(); - return; - case ST_TICK_LIFE_LOST: - stSimRespawn(&gSim); - stRenderSceneChanged(&gSim); - if (serviceJingle()) { - return; // $6906 on the new cab - } - break; - case ST_TICK_PLAYER_OUT: - gameOverEnter(); - return; - default: - break; - } - } -} - - -// Start a tune the sim asked for and park the loop in ST_STATE_JINGLE -// until it ends. Returns true when a tune was started, so callers can -// stop ticking the sim for this frame -- the C64's $44CF blocks there. -static bool serviceJingle(void) { - uint8_t song; - - if (gSim.jingleRequest == 0u) { - return false; - } - song = (uint8_t)(gSim.jingleRequest - 1u); - gSim.jingleRequest = 0u; - stAudioJingle(song); - if (!stAudioJingleActive()) { - return false; // no audio: nothing to wait for - } - gGame.afterJingle = gGame.state; - gGame.state = ST_STATE_JINGLE; - return true; -} - - -// $54F8 -- UP/DOWN pick the shift, FIRE confirms and starts the game. -static void runShiftMenu(uint8_t ticks) { - uint8_t mask = joyMask(); - - if (!gGame.fireArmed) { - if ((mask & 0x10u) == 0u) { - gGame.fireArmed = true; - } - return; - } - if ((mask & 0x10u) != 0u) { - shiftMenuColor(gGame.menuSel, 1u); - gGame.variation = (uint8_t)(gGame.menuSel + 1u); - // $5EC4 + $5333: the game starts; the first advance wraps to - // player 0 and loads the first screen. - stSimNewGame(&gSim, gSim.playerCount, false); - gSim.player = (uint8_t)(gSim.playerCount - 1u); - memset(gGame.usedLevels, 0, sizeof(gGame.usedLevels)); - gGame.demo = false; - startNewScreen(); - return; - } - if (gGame.menuRepeat > 0u) { - gGame.menuRepeat = (uint8_t)(gGame.menuRepeat - ((ticks < gGame.menuRepeat) ? ticks : gGame.menuRepeat)); - return; - } - if ((mask & 0x03u) != 0u) { - shiftMenuColor(gGame.menuSel, 6u); - if ((mask & 0x02u) != 0u) { - gGame.menuSel = (uint8_t)((gGame.menuSel + 1u) % 5u); - } else { - gGame.menuSel = (uint8_t)((gGame.menuSel + 4u) % 5u); - } - shiftMenuColor(gGame.menuSel, 3u); - gGame.menuRepeat = ST_MENU_REPEAT_TICKS; - } -} - - -// $47E4 -- the title loop: intro, then FIRE / UP / DOWN, else the demo. -static void runTitle(uint8_t ticks) { - uint8_t mask = joyMask(); - uint8_t k; - - if (!gGame.fireArmed) { - if ((mask & 0x10u) == 0u) { - gGame.fireArmed = true; - } - } - for (k = 0u; k < ticks; k++) { - if (stTitleTick(&gSim)) { - // $4818: intro over -> the attract demo (demo mode on). - startDemoScreen(); - return; - } - } - if ((mask & 0x10u) != 0u && gGame.fireArmed) { - showCabbiesMenu(); - return; - } - if ((mask & 0x01u) != 0u) { - highScoreDraw(); - gGame.state = ST_STATE_HIGHSCORES; - gGame.fireArmed = false; - return; - } - if ((mask & 0x02u) != 0u) { - static const uint8_t kAboutCols[7] = { 1u, 12u, 15u, 4u, 5u, 6u, 1u }; - static const uint8_t kAboutRows[7] = { 3u, 5u, 7u, 10u, 12u, 14u, 18u }; - uint8_t i; - blankScreen(&gSim, 12u, 15u); // $558E: grey border, light-grey field - for (i = 0u; i < 7u; i++) { - stSimDrawText(&gSim, kAboutCols[i], kAboutRows[i], kAboutLines[i], 1u); - } - stSimDrawText(&gSim, 10u, 22u, kTextReturn, 1u); - gGame.state = ST_STATE_ABOUT; - gGame.fireArmed = false; - } -} - - -// $554F -- colour one shift line (row 10 + 2*sel, 35 cells from col 2). -static void shiftMenuColor(uint8_t sel, uint8_t color) { - uint8_t col; - - for (col = 2u; col < 37u; col++) { - stSimPutColor(&gSim, col, (uint8_t)(10u + sel * 2u), color); - } -} - - -// $5295 -- GAME VARIATIONS page 1. -static void showCabbiesMenu(void) { - blankScreen(&gSim, 0u, 0u); - stSimDrawText(&gSim, 13u, 0u, kTextGameVariations, 5u); - stSimDrawText(&gSim, 1u, 2u, kTextCabbies, 7u); - stSimDrawText(&gSim, 5u, 4u, kTextLeftRight, 11u); - stSimDrawText(&gSim, 19u, 5u, kTextFire, 11u); - stSimDrawText(&gSim, 28u, 2u, kTextDigits, 6u); - gGame.menuSel = 0u; - stSimPutColor(&gSim, 28u, 2u, 3u); - gGame.state = ST_STATE_CABBIES; - gGame.fireArmed = false; - gGame.menuRepeat = 0u; -} - - -// $545E -- GAME VARIATIONS page 2. -static void showShiftMenu(void) { - uint8_t i; - - blankScreen(&gSim, 0u, 0u); - stSimDrawText(&gSim, 1u, 8u, kTextSelectShift, 7u); - for (i = 0u; i < 5u; i++) { - stSimDrawText(&gSim, 2u, (uint8_t)(10u + i * 2u), kTextShifts[i], 6u); - } - stSimDrawText(&gSim, 5u, 21u, kTextUpDown, 11u); - stSimDrawText(&gSim, 19u, 22u, kTextFire, 11u); - gGame.menuSel = 0u; - shiftMenuColor(0u, 3u); - gGame.state = ST_STATE_SHIFT; - gGame.fireArmed = false; - gGame.menuRepeat = 0u; -} - - -// $48AD + $61FB + $50C6 -- the next demo screen: "GET READY" over the -// title, then the level intro. -static void startDemoScreen(void) { - const StDemoEntryT *e = &kDemoRotation[gGame.demoRotation & 3u]; - - gGame.demoRotation++; - gGame.demo = true; - stSimNewGame(&gSim, 1u, true); - gSim.player = 0u; - (void)stSimAdvancePlayer(&gSim); - gSim.levelState++; - if (!loadLevel(e->levelIndex)) { - fatalStop(kTextFatalLoad, kTextFatalLevel); - return; - } - // $40CA: blank the character screen first. Without this the two - // green lines land on top of the title, which is still up -- logo, - // byline, copyright and all -- where the original shows them on a - // cleared screen (observed on real hardware, 2026-09-23). Same - // clear stIntroEnter does, and it leaves the sprite mask alone so - // the line below still keeps sprites 0..2. - memset(gSim.screen, ST_CHAR_SPACE, ST_SCREEN_CELLS); - memset(gSim.color, 0, ST_SCREEN_CELLS); - stSimDirtyAll(&gSim); - stSimGlyphReset(&gSim); - gSim.charDirtyAll = true; - gSim.charDirtyCount = 0u; - stSimDrawText(&gSim, 10u, 8u, kTextGetReady, 5u); - stSimDrawText(&gSim, 10u, 10u, kTextOnTheTaxi, 5u); - gSim.frame.enableMask &= 0x07u; - stRenderSceneChanged(&gSim); - gGame.waitTicks = ST_GET_READY_TICKS; - gGame.state = ST_STATE_GET_READY; -} - - -// $62F0 -> $4523 -- the level-name screen for gLevel. -static void startIntro(void) { - stIntroEnter(&gSim, &gLevel); - stRenderSceneChanged(&gSim); - // After the scene change, which turns it back off. - stRenderIntroStars(); - // $459C: the intro's tune plays under the fly-in, not blocking; the - // C64 leaves it to the IRQ player and stops it at $4709. - stAudioMusic(ST_SONG_INTRO, true); - gGame.introOwed = 0; - gGame.state = ST_STATE_INTRO; -} - - -// $61FB -- the next player's turn on this screen, or the next screen -// once everybody has had it; players out of cabs are skipped. -static void startNewScreen(void) { - uint8_t guard = 0u; - - for (;;) { - if (stSimAdvancePlayer(&gSim)) { - uint8_t next = nextLevelForShift(); - if (next == 0xFFu) { - // $602F: the shift is over. - enterTitle(); - return; - } - gGame.pendingLevel = next; - if (!loadLevel(next)) { - fatalStop(kTextFatalLoad, kTextFatalLevel); - return; - } - gSim.levelState++; - } - if (gSim.cabs[gSim.player] != 0u) { - break; - } - guard++; - if (guard > (uint8_t)(ST_MAX_PLAYERS * 2u)) { - enterTitle(); - return; - } - } - startIntro(); -} - - -int main(void) { - jlConfigT config; - jlSurfaceT *stage; - - // The codegen arena holds every compiled cel, the baked ones too: the - // 37 cab/exhaust/passenger cels plus a level's hook cels (level I has - // 30). On the IIgs that is ~230 KB of 65816 code in 64 KB banks, and - // the arena grants as many banks as the machine has (a 1 MB machine - // gets one; whatever does not fit draws interpreted). The 68k ports' - // cels are far larger (9-13 KB each) and their JIT is fast, so they - // keep a modest arena and compile at level entry. -#if defined(JOEYLIB_PLATFORM_IIGS) || defined(JOEYLIB_CPU_X86) - config.codegenBytes = 256UL * 1024; -#else - config.codegenBytes = 160UL * 1024; -#endif - if (!jlInit(&config)) { - return 1; - } - ST_LOG_RESET(); - ST_LOG("spacetaxi: build=%s %s", __DATE__, __TIME__); - stage = jlStageGet(); - if (stage == NULL) { - jlShutdown(); - return 1; - } - gStage = stage; - memset(&gGame, 0, sizeof(gGame)); - memset(&gSim, 0, sizeof(gSim)); - stRenderInit(stage); - stRenderLoading(stage); - if (!stAudioInit()) { - fatalStop(kTextFatalMemory, kTextFatalSpeech); - } - // Boot: the playback buffer as the C64 leaves it, and the state a - // fresh machine has when the first title intro starts ($4092 + the - // title's own sparkle/walk history from the dump). - stSimNewGame(&gSim, 1u, false); - stSimSeedDemoBuffer(&gSim, kDemoBufferInit, ST_DEMO_BUFFER_BYTES); - gSim.waveIdx = 3u; - if (!gGame.fatal && !stRenderPrewarm(stage, &gSim)) { - fatalStop(kTextFatalMemory, kTextFatalSprites); - } - highScoreLoad(); // $23E5 - if (!gGame.fatal) { - enterTitle(); - } - gGame.lastFrame = (uint8_t)jlFrameCount(); - - for (;;) { - uint8_t ticks; - uint8_t c64Frames; - - if (gGame.fatal) { - fatalHold(); - break; - } - jlInputPoll(); - // $4FCB runStopWatcher, on the game tick, the intro and the menus: - // SHIFT held freezes everything with the sound gated off and any - // tune stopped (the intro's restarts on release); RUN/STOP -- ESC - // here -- abandons the game for the title, and on the title it - // leaves the program (the C64 had nowhere else to go). - if (gGame.state != ST_STATE_JINGLE && gGame.state != ST_STATE_GAME_OVER && gGame.state != ST_STATE_NAME_ENTRY) { - if (jlKeyDown(KEY_LSHIFT) || jlKeyDown(KEY_RSHIFT)) { - if (!gGame.paused) { - gGame.paused = true; - stAudioSilence(); - stAudioNoise(false); - jlMusicStop(); - } - stRenderFrame(stage, &gSim); - jlAudioFrameTick(); - continue; - } - if (gGame.paused) { - gGame.paused = false; - gGame.lastFrame = (uint8_t)jlFrameCount(); // the held time is not owed - gGame.paceAcc = 0u; - gGame.frameAcc = 0u; - if (gGame.state == ST_STATE_INTRO) { - stAudioMusic(ST_SONG_INTRO, true); // $46B5 - } - } - if (jlKeyPressed(KEY_ESCAPE)) { - if (gGame.state == ST_STATE_TITLE) { - break; - } - jlMusicStop(); // $5000 - gGame.demo = false; - enterTitle(); - } - } - ticks = paceTicks(&c64Frames); - switch (gGame.state) { - case ST_STATE_TITLE: - runTitle(ticks); - break; - case ST_STATE_HIGHSCORES: - case ST_STATE_ABOUT: - if (!gGame.fireArmed) { - gGame.fireArmed = !joyFire(); - } else if (joyFire()) { - enterTitle(); - } - break; - case ST_STATE_CABBIES: - runCabbiesMenu(ticks); - break; - case ST_STATE_SHIFT: - runShiftMenu(ticks); - break; - case ST_STATE_GET_READY: - if (gGame.demo && (joyMask() & 0x1Fu) != 0u) { - runDemoEnd(); - break; - } - if (gGame.waitTicks > ticks) { - gGame.waitTicks = (uint16_t)(gGame.waitTicks - ticks); - } else { - gGame.waitTicks = 0u; - startIntro(); - } - break; - case ST_STATE_INTRO: - runIntro(c64Frames); - break; - case ST_STATE_PLAYING: - runPlaying(ticks); - break; - case ST_STATE_JINGLE: - // The C64 spins in $44D5 until the tune ends. A joystick - // press ends the attract demo here too ($4FE2 stops the - // music on input). - if (gGame.demo && (joyMask() & 0x1Fu) != 0u) { - jlMusicStop(); - runDemoEnd(); - } else if (!stAudioJingleActive()) { - gGame.state = gGame.afterJingle; - } - break; - case ST_STATE_GAME_OVER: - if (gGame.waitTicks > ticks) { - gGame.waitTicks = (uint16_t)(gGame.waitTicks - ticks); - } else if (!gGame.gameOverTune) { - // $5C24: after the GAME OVER! wait, song 5 plays out. - gGame.gameOverTune = true; - stAudioJingle(ST_SONG_GAME_OVER); - } else if (stAudioJingleActive()) { - // still sounding - } else if (!nameEntryStart()) { - gameOverFinish(); - } - break; - case ST_STATE_NAME_ENTRY: - runNameEntry(ticks); - break; - default: - enterTitle(); - break; - } - stRenderFrame(stage, &gSim); - jlAudioFrameTick(); - } - - stAudioShutdown(); - stRenderShutdown(); - jlShutdown(); - return gGame.fatal ? 1 : 0; -} diff --git a/examples/spacetaxi/spacetaxi.h b/examples/spacetaxi/spacetaxi.h deleted file mode 100644 index 75fd710..0000000 --- a/examples/spacetaxi/spacetaxi.h +++ /dev/null @@ -1,78 +0,0 @@ -// Space Taxi (JoeyLib port) -- host-side declarations. -// -// The game itself lives in the host-independent simulation (stSim.h): -// physics, collision, the fare machine, the level hooks, the title -// intro and the level intro all run there in the C64's own units and -// are verified tick-for-tick against a VICE trace of the original. -// This header is the JoeyLib side: the renderer that paints the -// simulation's screen RAM and sprite frame, the audio sink, the -// level loader and the front-end flow in spacetaxi.c. - -#ifndef SPACETAXI_H -#define SPACETAXI_H - -#include - -#include "stSim.h" - -// Diagnostics. The logger drags in vsnprintf (with newlib that pulls the -// whole formatted-output and float-conversion family, ~45 KB) plus a -// multi-KB ring buffer, and the game has no need of either. That is -// unaffordable on the IIgs, whose bank-0 BSS budget cannot spare the -// ring, and on the X68000, which ships on a Human68k floppy with about -// 330 KB of usable space for the binary AND its levels. Both compile it -// out; the ports with room keep it. -#if defined(__W65816__) || defined(JOEYLIB_PLATFORM_X68000) - #define ST_LOG_RESET() ((void)0) - #define ST_LOG(...) ((void)0) - #define ST_LOG_FLUSH() ((void)0) -#else - #define ST_LOG_RESET() jlLogReset() - #define ST_LOG(...) jlLogF(__VA_ARGS__) - #define ST_LOG_FLUSH() jlLogFlush() -#endif - -// Level loading through the JoeyLib data path (DATA/levels/...). -bool stLevelLoad(StLevelT *out, const char *path); - -// Renderer: paints dirty screen cells with the live charset and draws -// the eight sprites of the last marshaled frame with save-under. -void stRenderInit(jlSurfaceT *stage); -// A word on the stage before the slow part of startup: loading the cel -// bank and the first level takes long enough that the host's desktop -// would otherwise sit there with nothing to look at. -void stRenderLoading(jlSurfaceT *stage); -void stRenderShutdown(void); -void stRenderFrame(jlSurfaceT *stage, StSimT *sim); -// Drop the sprite cache and force a full repaint (new scene). Also -// leaves the title's logo mode (below). -void stRenderSceneChanged(StSimT *sim); -// Title screen only: draw the logo's four flip-book frames from one -// tile held in reserved palette slots, so the flip and the colour -// cycle are palette writes instead of repainting 103 (and 407) cells. -// Call after stRenderSceneChanged, which turns it back off. -void stRenderTitleLogo(StSimT *sim); -// Level intro only: cycle the starfield's ring palette slots so the warp -// animates without a single pixel moving. Call after -// stRenderSceneChanged, which turns it back off. -void stRenderIntroStars(void); -// Fill the stage with a two-line message and present it. Used when an -// asset the game ships with will not load, so a player sees why the -// game stopped instead of a silently degraded ride. -void stRenderFatal(jlSurfaceT *stage, const char *line1, const char *line2); -// Compile every always-on cel. False when a shipped asset is present but -// unusable, so main can stop with a message instead of running the game -// on interpreted cels. -bool stRenderPrewarm(jlSurfaceT *stage, StSimT *sim); -// Ready a level's hook cels before its ride (evicts the previous level's). -// False only when this level's baked bank exists and will not load; a -// port that ships no per-level banks compiles them and returns true. -bool stRenderLevelCels(jlSurfaceT *stage, const StLevelT *level); - -// Audio sink setup and the per-tick release timers ($4320). -// False when the shipped speech blob will not load. -bool stAudioInit(void); -void stAudioShutdown(void); -void stAudioTick(void); - -#endif diff --git a/examples/spacetaxi/stAudio.c b/examples/spacetaxi/stAudio.c deleted file mode 100644 index 1967593..0000000 --- a/examples/spacetaxi/stAudio.c +++ /dev/null @@ -1,669 +0,0 @@ -// Space Taxi -- audio sink for the simulation's SID events. -// -// The C64 drives three SID voices: voice 1 carries the SFX programs -// (landing, gear, crash, fuel) and the thrust/crash frequency sweep, -// voice 2 the level gimmick tones (laser hum, puzzle chime), voice 3 -// the jet noise. JoeyLib's audio HAL is a PSG-style tone/noise -// interface, so each SID program is mapped to its pitch and release -// time; envelopes and waveforms are approximated with an attenuation -// ramp. The speech samples of the original are not extracted; each -// spoken character becomes a short chirp so the cues still land. -// -// UPDATE: the speech IS extracted now (assets/extractSpeech.py), so the -// chirp is gone. stSpeechData.c carries the C64's own 1-bit run-length -// streams; assets/genSpeechPcm.py bakes them to sample bytes -// (DATA/speech.bin) and speechFill below streams those through -// jlAudioPlaySfxStream. - -#include - -#include "spacetaxi.h" -#include -#include "stSongData.h" - - -// SID voice index in a program's byte 8 -> JoeyLib voice slot. -#define ST_VOICE_SFX 1u // SID voice 1 -#define ST_VOICE_TONE 2u // SID voice 2 -#define ST_VOICE_NOISE 3u // SID voice 3 (noise) - -#define ST_ATTEN_LOUD 4u -#define ST_ATTEN_SOFT 8u -// The jet's noise pitch: jlAudioNoise takes 0..31 in YM noise-period -// style, where 31 is the lowest rumble (0 is the brightest hiss). -#define ST_NOISE_JET_PITCH 31u -// Noise sound effects (the crash and the wreck's impact) use the same -// deep end of the range. The C64 takes the rate from the program's -// frequency word; this is the one simplification in that mapping. -#define ST_NOISE_SFX_PITCH 31u -#define ST_ATTEN_OFF 15u - -// NTSC SID: Hz = word * 1022727 / 16777216 = word * 0.06096. -#define ST_SID_HZ_NUM 1022727UL -#define ST_SID_HZ_DEN 16777216UL - -// ---- speech ---- -// -// The C64 player ($9802) busy-waits a delay loop per stream byte and then -// toggles its 1-bit output, so a byte is one half-cycle. The loop costs -// (5k + 12) cycles per pass plus a 33-cycle tail, where k is the inner -// count at $98A2 -- normally 3, giving 27. A code below $09 does not -// speak: it REWRITES k, which is how the original gives each fare a -// slightly different voice pitch. stAudioSpeech honours that. -#define ST_SPEECH_SLOT 0u // slots 1..3 are the SID voices -#define ST_SPEECH_RATE 5000u // enough for a 1-bit source -#define ST_SPEECH_PITCH_BASE 3u // $98A2's power-on value -#define ST_SPEECH_PITCH_MAX 15u // the player's AND #$0F -// Longest phrase is "PX?" -- pad, digit, please. -#define ST_SPEECH_QUEUE 4u -// The speech is baked to sample bytes per pitch, in both polarities, by -// assets/genSpeechPcm.py (DATA/speech.bin, format in that script), so -// playing it is a block copy. Decoding the C64's run-length stream at -// play time cost the 65816 2.4 s of CPU per second of speech and stalled -// the ride half a second on every "HEY TAXI"; the 1-bit intermediate -// format that followed still needed a per-byte expansion loop. -#define ST_SPEECH_FILE "speech.bin" -#define ST_SPEECH_VERSION 3u -#define ST_SPEECH_HEADER_BYTES 16u -#define ST_SPEECH_ENTRY_BYTES 8u -// speech.bin carries each utterance in one polarity or two. Two is the -// cheap case -- the player just picks the baked variant -- but it doubles -// the file, and the X68000's Human68k floppy cannot spare the other -// 60 KB, so that port bakes one and the player negates the samples -// itself. Negation is the same byte arithmetic in both sample formats: -// signed 0 - v, and 0x80-biased 0x100 - v, are one operation. -#define ST_SPEECH_POLARITIES_MAX 2u -#define ST_SPEECH_FORMAT_SIGNED 0u -#define ST_SPEECH_FORMAT_BIASED 1u -#define ST_SPEECH_LOAD_CHUNK 16384u -#define ST_SPEECH_STAGE_MASK (ST_SPEECH_STAGE_BYTES - 1u) -// Decode-ahead staging: a whole engine chunk (7168 at 5 kHz, 1.4 s) plus -// slack. The per-tick slice covers playback (5000/s is ~330 a tick) -// with room to spare and is cheap enough to never need a burst; a -// starved pull still copies ST_SPEECH_STARVED_SLICE on the spot rather -// than ending the stream. -#define ST_SPEECH_STAGE_BYTES 8192u -#define ST_SPEECH_TICK_SLICE 384u -#define ST_SPEECH_STARVED_SLICE 1024u -#define ST_SPEECH_ARM_SLICE 1024u - -// Thrust sweep register value -> Hz: the C64 writes the same byte to -// both frequency bytes, so the word is value * 257. -#define ST_SWEEP_WORD_SCALE 257u - - -typedef struct { - uint16_t ticksLeft; // release countdown (game ticks); 0 = idle - uint8_t voice; -} StSfxSlotT; - - -static StSfxSlotT gSfx; // SID voice 1 (programs + sweep) -static StSfxSlotT gTone; // SID voice 2 -static bool gNoiseOn; -// Release countdown for a NOISE sound effect, in game ticks, mirroring -// the two tone slots' ticksLeft. 0 = no burst running. -static uint16_t gNoiseTicks; - -// Speech playback state. The fill callback walks a queue of utterances so -// a phrase like "HT" ("Hey" + "Taxi") runs as one continuous stream -// instead of each character cutting off the last. -static uint8_t gSpeechQueue[ST_SPEECH_QUEUE]; -static uint8_t gSpeechHead; -static uint8_t gSpeechTail; -static bool gSpeechActive; -static uint8_t gSpeechPitch = ST_SPEECH_PITCH_BASE; -// DATA/speech.bin, whole: header, entry table, sample bytes. The bytes -// are already in the port's raw stream format (jlAudioSfxRawFormat): -// signed +/-100, or 0x80-biased on the IIgs so a chunk needs no -// conversion; speechLoad refuses a file baked for the other one. -static uint8_t *gSpeechBlob; -static uint8_t gSpeechPitchBase; -static uint8_t gSpeechPitchCount; -static uint8_t gSpeechCount; // utterances per pitch -static const uint8_t *gSpeechPcm; // current utterance's samples -static uint16_t gSpeechSample; // next sample index in it -static uint16_t gSpeechSamples; // its length -static uint8_t gSpeechParity; // it ends on the opposite level -static uint8_t gSpeechPol; // 0 / 1: the polarity the next utterance plays in -static uint8_t gSpeechPolarities; // how many the file carries, 1 or 2 -static bool gSpeechInvert; // negate on the way out (1-polarity file, odd polarity) -// Copied-ahead PCM ring for the stream (see speechFill). Allocated by -// stAudioInit rather than static: 8 KB of BSS is a fifth of the IIgs -// entry bank, which text, rodata, BSS and the C heap all share. -static uint8_t *gSpeechStage; -static uint16_t gSpeechStageRead; // byte index of the next unread sample -static uint16_t gSpeechStageWrite; // byte index of the next free byte -static uint16_t gSpeechStageCount; // bytes staged and unread -static bool gSpeechDecoding; // the decoder still has stream left - - -static uint16_t sidHz(uint16_t word); -static bool speechLoad(void); -static bool speechNext(void); -static void speechArm(void); -static uint16_t speechDecode(uint8_t *dst, uint16_t count); -static uint32_t speechFill(void *ctx, int8_t *dst, uint32_t count); -static void speechPumpSlice(uint16_t samples); -static bool speechQueue(uint8_t ch); -static void voiceOff(StSfxSlotT *slot); - - -static uint16_t sidHz(uint16_t word) { - uint32_t hz = ((uint32_t)word * ST_SID_HZ_NUM) / ST_SID_HZ_DEN; - - if (hz > 20000u) { - hz = 20000u; - } - return (uint16_t)hz; -} - - -// Point the decoder at the next queued utterance. False when the queue -// is empty, which ends the stream. -// DATA/speech.bin into one jlAlloc block; without it the game is mute -// but otherwise fine. -static bool speechLoad(void) { - FILE *fp = jlDataOpen(ST_SPEECH_FILE, "rb"); - uint8_t head[ST_SPEECH_HEADER_BYTES]; - uint8_t format; - uint32_t dataBytes; - uint32_t total; - uint32_t done; - - gSpeechBlob = 0; - if (fp == 0) { - ST_LOG("staudio: ! cannot open DATA/%s", ST_SPEECH_FILE); - return false; - } - format = (jlAudioSfxRawFormat() == JL_SFX_RAW_UNSIGNED8_NOZERO) ? ST_SPEECH_FORMAT_BIASED : ST_SPEECH_FORMAT_SIGNED; - if (fread(head, 1u, sizeof(head), fp) != sizeof(head) || head[0] != 'S' || head[1] != 'T' || head[2] != 'S' || head[3] != 'P' || head[4] != ST_SPEECH_VERSION || head[8] != format - || head[9] == 0u || head[9] > ST_SPEECH_POLARITIES_MAX) { - ST_LOG("staudio: ! speech.bin rejected (want v%u fmt%u)", ST_SPEECH_VERSION, format); - fclose(fp); - return false; - } - gSpeechPolarities = head[9]; - gSpeechPitchBase = head[5]; - gSpeechPitchCount = head[6]; - gSpeechCount = head[7]; - dataBytes = (uint32_t)head[12] | ((uint32_t)head[13] << 8) | ((uint32_t)head[14] << 16) | ((uint32_t)head[15] << 24); - total = ST_SPEECH_HEADER_BYTES + (uint32_t)gSpeechPitchCount * gSpeechCount * gSpeechPolarities * ST_SPEECH_ENTRY_BYTES + dataBytes; - gSpeechBlob = (uint8_t *)jlAlloc(total); - if (gSpeechBlob == 0) { - // The blob is ~120 KB of sample bytes, so on the tighter - // machines this is the failure most likely to bite. It stops - // the game: a silent Space Taxi is a broken Space Taxi. - ST_LOG("staudio: ! speech alloc FAILED (%lu bytes)", (unsigned long)total); - fclose(fp); - return false; - } - memcpy(gSpeechBlob, head, sizeof(head)); - // The block spans banks on the IIgs; read it in bank-safe pieces. - done = ST_SPEECH_HEADER_BYTES; - while (done < total) { - uint32_t want = total - done; - - if (want > ST_SPEECH_LOAD_CHUNK) { - want = ST_SPEECH_LOAD_CHUNK; - } - if (fread(gSpeechBlob + done, 1u, (size_t)want, fp) != (size_t)want) { - ST_LOG("staudio: ! speech read FAILED at %lu of %lu", (unsigned long)done, (unsigned long)total); - jlFree(gSpeechBlob); - gSpeechBlob = 0; - break; - } - done += want; - } - fclose(fp); - if (gSpeechBlob == 0) { - return false; - } - ST_LOG("staudio: speech %lu bytes, %u utterances x %u pitches x %u polarities", (unsigned long)total, gSpeechCount, gSpeechPitchCount, gSpeechPolarities); - return true; -} - - -static bool speechNext(void) { - uint8_t row = 0u; - - if (gSpeechBlob == 0) { - return false; - } - if (gSpeechPitch > gSpeechPitchBase) { - row = (uint8_t)(gSpeechPitch - gSpeechPitchBase); - if (row >= gSpeechPitchCount) { - row = (uint8_t)(gSpeechPitchCount - 1u); - } - } - while (gSpeechHead != gSpeechTail) { - uint8_t code = gSpeechQueue[gSpeechHead]; - const uint8_t *e = gSpeechBlob + ST_SPEECH_HEADER_BYTES + (uint16_t)row * gSpeechCount * gSpeechPolarities * ST_SPEECH_ENTRY_BYTES; - uint8_t k; - - gSpeechHead = (uint8_t)((gSpeechHead + 1u) % ST_SPEECH_QUEUE); - for (k = 0u; k < gSpeechCount; k++, e += gSpeechPolarities * ST_SPEECH_ENTRY_BYTES) { - if (e[0] == code) { - // Two polarities baked: take the variant. One: take the - // only one and negate it on the way out when the running - // polarity is the odd one. - uint8_t pol = (gSpeechPolarities == 2u) ? gSpeechPol : 0u; - const uint8_t *v = e + (uint16_t)pol * ST_SPEECH_ENTRY_BYTES; - uint32_t offset = (uint32_t)v[4] | ((uint32_t)v[5] << 8) | ((uint32_t)v[6] << 16) | ((uint32_t)v[7] << 24); - - gSpeechInvert = (gSpeechPolarities == 1u && gSpeechPol != 0u); - gSpeechParity = v[1]; - gSpeechSamples = (uint16_t)(v[2] | ((uint16_t)v[3] << 8)); - gSpeechSample = 0u; - gSpeechPcm = gSpeechBlob + ST_SPEECH_HEADER_BYTES + (uint32_t)gSpeechPitchCount * gSpeechCount * gSpeechPolarities * ST_SPEECH_ENTRY_BYTES + offset; - return true; - } - } - // Unknown code: skip it rather than dropping the whole phrase. - } - return false; -} - - -// Copy up to `count` samples of the queued utterances into `dst`, fewer -// when they run out (0 = none left). jlMemCopy is an MVN on the IIgs; -// the utterance bytes are already in the stream's format, so this is -// the whole of the play-time speech work. -static uint16_t speechDecode(uint8_t *dst, uint16_t count) { - uint16_t n = 0u; - - while (n < count) { - uint16_t left = (uint16_t)(gSpeechSamples - gSpeechSample); - uint16_t take = (uint16_t)(count - n); - - if (left == 0u) { - // The next utterance carries on from the level this one left. - if (gSpeechParity != 0u) { - gSpeechPol ^= 1u; - } - if (!speechNext()) { - return n; - } - continue; - } - if (take > left) { - take = left; - } - if (gSpeechInvert) { - const uint8_t *from = gSpeechPcm + gSpeechSample; - uint8_t *to = dst + n; - uint16_t i; - - for (i = 0u; i < take; i++) { - to[i] = (uint8_t)(0u - (uint16_t)from[i]); - } - } else { - jlMemCopy(dst + n, gSpeechPcm + gSpeechSample, take); - } - gSpeechSample = (uint16_t)(gSpeechSample + take); - n = (uint16_t)(n + take); - } - return n; -} - - -// The audio engine's chunk pull. It used to decode the whole chunk right -// here -- 7168 samples, ~1.5 s of speech, in ONE frame: every "HEY -// TAXI" froze the ride for over a second on the IIgs. Now the chunk is -// copied out of the staging buffer that speechPumpSlice fills a slice at -// a time across the ticks before it is due, and only a stream that has -// outrun the pump decodes here, a bounded slice, so it never starves. -static uint32_t speechFill(void *ctx, int8_t *dst, uint32_t count) { - uint16_t want = (count > ST_SPEECH_STAGE_BYTES) ? (uint16_t)ST_SPEECH_STAGE_BYTES : (uint16_t)count; - uint16_t n; - - (void)ctx; - if (gSpeechStageCount == 0u && gSpeechDecoding) { - speechPumpSlice(ST_SPEECH_STARVED_SLICE); - } - n = (gSpeechStageCount < want) ? gSpeechStageCount : want; - if (n != 0u) { - // jlMemCopy (MVN on the IIgs), in two runs when the unread bytes - // wrap the ring's end. - uint8_t *to = (uint8_t *)dst; - uint16_t left = n; - - while (left != 0u) { - uint16_t run = (uint16_t)(ST_SPEECH_STAGE_BYTES - gSpeechStageRead); - - if (run > left) { - run = left; - } - jlMemCopy(to, gSpeechStage + gSpeechStageRead, run); - to += run; - gSpeechStageRead = (uint16_t)((gSpeechStageRead + run) & ST_SPEECH_STAGE_MASK); - left = (uint16_t)(left - run); - } - gSpeechStageCount = (uint16_t)(gSpeechStageCount - n); - } - if (n == 0u) { - // Out of queued utterances. Only END the stream if this chunk - // produced nothing at all -- a phrase is fed one character at a - // time ("HT" is "Hey" then "Taxi"), and the first arm decodes - // all of 'H' before 'T' has even been queued. Ending here would - // let the 'T' arm a NEW stream that cut 'H' off, so the player - // only ever heard the last character. Returning a short chunk - // keeps the slot armed and the next pull picks up whatever - // arrived since. - gSpeechActive = false; - } - return n; -} - - -// Decode up to `samples` more into the staging buffer. Once the decoder -// reports nothing left it stays quiet until the next speechArm. Reads -// and writes stay even so the chunk copy can move words. -static void speechPumpSlice(uint16_t samples) { - uint16_t room = (uint16_t)(ST_SPEECH_STAGE_BYTES - gSpeechStageCount); - - if (!gSpeechDecoding || room == 0u) { - return; - } - if (samples > room) { - samples = room; - } - // Fill towards the ring's end, then from its start. - while (samples != 0u) { - uint16_t run = (uint16_t)(ST_SPEECH_STAGE_BYTES - gSpeechStageWrite); - uint16_t got; - - if (run > samples) { - run = samples; - } - got = speechDecode(gSpeechStage + gSpeechStageWrite, run); - if (got < run) { - gSpeechDecoding = false; - } - gSpeechStageWrite = (uint16_t)((gSpeechStageWrite + got) & ST_SPEECH_STAGE_MASK); - gSpeechStageCount = (uint16_t)(gSpeechStageCount + got); - samples = (uint16_t)(samples - got); - if (!gSpeechDecoding) { - return; - } - } -} - - -static void voiceOff(StSfxSlotT *slot) { - if (slot->ticksLeft != 0u) { - jlAudioVoice(slot->voice, 0u, ST_ATTEN_OFF); - slot->ticksLeft = 0u; - } -} - - -// Once per game tick: run the release timers ($4320 sfxEnvelopeTick). -void stAudioTick(void) { - if (gSpeechActive) { - speechPumpSlice(ST_SPEECH_TICK_SLICE); - } - if (gSfx.ticksLeft != 0u) { - gSfx.ticksLeft--; - if (gSfx.ticksLeft == 0u) { - jlAudioVoice(gSfx.voice, 0u, ST_ATTEN_OFF); - } - } - if (gTone.ticksLeft != 0u) { - gTone.ticksLeft--; - if (gTone.ticksLeft == 0u) { - jlAudioVoice(gTone.voice, 0u, ST_ATTEN_OFF); - } - } - if (gNoiseTicks != 0u) { - gNoiseTicks--; - if (gNoiseTicks == 0u) { - gNoiseOn = false; - jlAudioNoise(0u, ST_ATTEN_OFF); - } else { - // Re-assert: one call buys about 80 ms on the IIgs. - jlAudioNoise(ST_NOISE_SFX_PITCH, ST_ATTEN_LOUD); - } - } -} - - -bool stAudioInit(void) { - bool ok; - - (void)jlAudioInit(); - gSpeechStage = (uint8_t *)jlAlloc(ST_SPEECH_STAGE_BYTES); - ok = (gSpeechStage != 0) && speechLoad(); - gSfx.voice = ST_VOICE_SFX; - gNoiseTicks = 0u; - gSfx.ticksLeft = 0u; - gTone.voice = ST_VOICE_TONE; - gTone.ticksLeft = 0u; - gNoiseOn = false; - gSpeechHead = 0u; - gSpeechTail = 0u; - gSpeechActive = false; - gSpeechPitch = ST_SPEECH_PITCH_BASE; - gSpeechPol = 0u; - gSpeechInvert = false; - return ok; -} - - -void stAudioShutdown(void) { - voiceOff(&gSfx); - voiceOff(&gTone); - jlAudioNoise(0u, ST_ATTEN_OFF); - jlAudioShutdown(); - jlFree(gSpeechStage); - gSpeechStage = 0; - jlFree(gSpeechBlob); - gSpeechBlob = 0; -} - - -// $6D6A / $6A3B -- voice 3 jet noise gate. -// $6D6A's voice-3 gate. The C64 holds SID voice 3 open for as long as a -// direction is held, so the jet has to keep sounding for the whole burn. -// -// This re-asserts on EVERY call rather than only on the rising edge, -// because jlAudioNoise is a per-frame assertion and not a latch: the -// IIgs plays one ~80 ms one-shot per call and expects the caller to keep -// asking, exactly as jlMusicPlay's sequencer does for a held noise note -// (include/joey/audio.h). Latching it on the edge is why the thruster -// used to sound one short burst and then die with the taxi still -// climbing. The ports that DO latch take the repeat harmlessly: the ST -// rewrites the same YM registers, and Paula's redundant DMA set is -// click-free by design. -void stAudioNoise(bool on) { - if (on && gNoiseTicks != 0u) { - // A sound effect owns the voice until its release runs out, so - // the jet does not talk over the crash. The C64 shares SID - // voice 3 between them the same way and silences the jet before - // firing one. An explicit silence below still wins, which - // matters because stAudioMusic clears this voice before a tune. - return; - } - if (on) { - gNoiseOn = true; - jlAudioNoise(ST_NOISE_JET_PITCH, ST_ATTEN_SOFT); - return; - } - if (!gNoiseOn && gNoiseTicks == 0u) { - return; - } - gNoiseOn = false; - gNoiseTicks = 0u; - jlAudioNoise(0u, ST_ATTEN_OFF); -} - - -// $42E9 -- a 9-byte SID program: freq lo/hi, pulse lo/hi, control, -// AD, SR, release ticks, voice. -void stAudioSfx(const uint8_t *program9) { - uint16_t word = (uint16_t)(program9[0] | ((uint16_t)program9[1] << 8)); - uint8_t ctrl = program9[4]; - uint8_t ticks = program9[7]; - StSfxSlotT *slot = (program9[8] == 1u) ? &gTone : &gSfx; - - if ((ctrl & 0x80u) != 0u) { - // Noise waveform: a burst on the noise generator, held for the - // program's release time exactly as the tone slots below hold - // theirs. It used to fire one jlAudioNoise and return, which is - // not a burst at all: that call is a per-frame ASSERTION, not a - // latch (include/joey/audio.h), so on the IIgs the wreck's - // explosion lasted the ~80 ms of one one-shot and was easy to - // miss entirely, while on Paula it would have hung until some - // later stAudioNoise(false). stAudioTick re-asserts it. - gNoiseTicks = (uint16_t)(ticks + 1u); - gNoiseOn = true; - jlAudioNoise(ST_NOISE_SFX_PITCH, ST_ATTEN_LOUD); - return; - } - jlAudioVoice(slot->voice, sidHz(word), ST_ATTEN_LOUD); - slot->ticksLeft = (uint16_t)(ticks + 1u); -} - - -// $6C7F / $6CAA -- voices 1 and 2 gated off. -void stAudioSilence(void) { - voiceOff(&gSfx); - voiceOff(&gTone); -} - - -// $44CF -- play one of the game's tunes and block. The C64 silences the -// SID first ($5C1A / $6906) and then spins until the song ends; the host -// does the spinning (ST_STATE_JINGLE), so here it is just the start. -void stAudioJingle(uint8_t song) { - stAudioMusic(song, false); -} - - -// $CB02 -- start a tune and leave it to the player: the level intro's -// song and the high-score table's. Speech is cancelled too: it streams -// on the slot the tracker's voice 0 rides. -void stAudioMusic(uint8_t song, bool loop) { - if (song >= ST_SONG_COUNT) { - return; - } - stAudioSilence(); - stAudioNoise(false); - gSpeechActive = false; - gSpeechDecoding = false; - gSpeechStageRead = 0u; - gSpeechStageWrite = 0u; - gSpeechStageCount = 0u; - gSpeechHead = gSpeechTail; - jlAudioStopSfx(ST_SPEECH_SLOT); - jlMusicPlay(kStSongs[song].data, kStSongs[song].length, loop); -} - - -bool stAudioJingleActive(void) { - return jlMusicIsPlaying(); -} - - -// Queue one utterance code. Returns false when the code is not speech (a -// pitch change) or the queue is full. -static bool speechQueue(uint8_t ch) { - uint8_t next; - - if (ch < 9u) { - // Below $09 the original does not speak: it rewrites the delay - // loop's inner count, re-pitching every later utterance. That is - // where the passenger's voice variation comes from, so keep it. - gSpeechPitch = (uint8_t)(ch & ST_SPEECH_PITCH_MAX); - if (gSpeechPitch == 0u) { - gSpeechPitch = ST_SPEECH_PITCH_BASE; - } - return false; - } - next = (uint8_t)((gSpeechTail + 1u) % ST_SPEECH_QUEUE); - if (next == gSpeechHead) { - return false; // queue full; drop rather than stall - } - gSpeechQueue[gSpeechTail] = ch; - gSpeechTail = next; - return true; -} - - -// Start the stream if it is idle. Call this ONCE, after the whole phrase is -// queued: arming decodes everything queued so far into a single chunk, and -// each extra chunk costs a refill deadline of dead air, because the refill -// only happens when the game next calls jlAudioFrameTick -- once per rendered -// frame. Arming per character is what broke phrases into words with silence -// between them. -static void speechArm(void) { - if (gSpeechActive || gSpeechStage == 0 || gSpeechBlob == 0) { - return; - } - if (!speechNext()) { - return; - } - gSpeechPol = 0u; // a phrase starts on +100 - gSpeechActive = true; - gSpeechDecoding = true; - gSpeechStageRead = 0u; - gSpeechStageWrite = 0u; - gSpeechStageCount = 0u; - // A first slice now, so the opening chunk sounds this tick. - speechPumpSlice(ST_SPEECH_ARM_SLICE); - jlAudioPlaySfxStreamRaw(ST_SPEECH_SLOT, speechFill, (void *)0, ST_SPEECH_RATE); -} - - -// $9802 -- one spoken character. Speaks immediately; prefer -// stAudioSpeechPhrase for anything longer than a single code. -void stAudioSpeech(uint8_t ch) { - if (speechQueue(ch)) { - speechArm(); - } -} - - -// A whole phrase: queue every code FIRST, then arm once, so the phrase -// decodes into as few chunks as it fits into. -void stAudioSpeechPhrase(const uint8_t *speech) { - bool any = false; - - while (*speech != 0u) { - if (speechQueue(*speech)) { - any = true; - } - speech++; - } - if (any) { - speechArm(); - } -} - - -// $6A8A -- the crash scream: voice 1 frequency written from the sweep. -void stAudioThrustSweep(uint8_t value) { - jlAudioVoice(ST_VOICE_SFX, sidHz((uint16_t)(value * ST_SWEEP_WORD_SCALE)), ST_ATTEN_LOUD); - gSfx.ticksLeft = 3u; -} - - -// $6E71 -- the fuel pump's rising note on voice 1. -void stAudioVoice1Freq(uint8_t value) { - jlAudioVoice(ST_VOICE_SFX, sidHz((uint16_t)(value * ST_SWEEP_WORD_SCALE)), ST_ATTEN_LOUD); - gSfx.ticksLeft = 2u; -} - - -// Level hooks poke voice 2 directly (laser hum, puzzle chime). -void stAudioVoice2(uint8_t freqLo, uint8_t freqHi, uint8_t ctrl) { - uint16_t word = (uint16_t)(freqLo | ((uint16_t)freqHi << 8)); - - if ((ctrl & 0x80u) != 0u) { - jlAudioNoise((uint8_t)(freqHi & 31u), ST_ATTEN_SOFT); - gNoiseOn = true; - return; - } - jlAudioVoice(ST_VOICE_TONE, sidHz(word), ST_ATTEN_SOFT); - gTone.ticksLeft = 2u; -} diff --git a/examples/spacetaxi/stC64Data.c b/examples/spacetaxi/stC64Data.c deleted file mode 100644 index b8fa792..0000000 --- a/examples/spacetaxi/stC64Data.c +++ /dev/null @@ -1,1356 +0,0 @@ -// Generated by assets/genC64Data.py from stuff/spacetaxi/raw.bin. -// Do not hand-edit; re-run the generator. - -#include "stC64Data.h" - -const uint8_t kStCharset[ST_CHARSET_CHARS][8] = { - { 0x3C, 0x66, 0x6E, 0x6E, 0x60, 0x62, 0x3C, 0x00 }, // $00 - { 0x00, 0x00, 0x3C, 0x06, 0x3E, 0x66, 0x3E, 0x00 }, // $01 - { 0x00, 0x60, 0x60, 0x7C, 0x66, 0x66, 0x7C, 0x00 }, // $02 - { 0x00, 0x00, 0x3C, 0x60, 0x60, 0x60, 0x3C, 0x00 }, // $03 - { 0x00, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x3E, 0x00 }, // $04 - { 0x00, 0x00, 0x3C, 0x66, 0x7E, 0x60, 0x3C, 0x00 }, // $05 - { 0x00, 0x0E, 0x18, 0x3E, 0x18, 0x18, 0x18, 0x00 }, // $06 - { 0x00, 0x00, 0x3E, 0x66, 0x66, 0x3E, 0x06, 0x7C }, // $07 - { 0x00, 0x60, 0x60, 0x7C, 0x66, 0x66, 0x66, 0x00 }, // $08 - { 0x00, 0x18, 0x00, 0x38, 0x18, 0x18, 0x3C, 0x00 }, // $09 - { 0x00, 0x06, 0x00, 0x06, 0x06, 0x06, 0x06, 0x3C }, // $0A - { 0x00, 0x60, 0x60, 0x6C, 0x78, 0x6C, 0x66, 0x00 }, // $0B - { 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00 }, // $0C - { 0x00, 0x00, 0x66, 0x7F, 0x7F, 0x6B, 0x63, 0x00 }, // $0D - { 0x00, 0x00, 0x7C, 0x66, 0x66, 0x66, 0x66, 0x00 }, // $0E - { 0x00, 0x00, 0x3C, 0x66, 0x66, 0x66, 0x3C, 0x00 }, // $0F - { 0x00, 0x00, 0x7C, 0x66, 0x66, 0x7C, 0x60, 0x60 }, // $10 - { 0x00, 0x00, 0x3E, 0x66, 0x66, 0x3E, 0x06, 0x06 }, // $11 - { 0x00, 0x00, 0x7C, 0x66, 0x60, 0x60, 0x60, 0x00 }, // $12 - { 0x00, 0x00, 0x3E, 0x60, 0x3C, 0x06, 0x7C, 0x00 }, // $13 - { 0x00, 0x18, 0x7E, 0x18, 0x18, 0x18, 0x0E, 0x00 }, // $14 - { 0x00, 0x00, 0x66, 0x66, 0x66, 0x66, 0x3E, 0x00 }, // $15 - { 0x00, 0x00, 0x66, 0x66, 0x66, 0x3C, 0x18, 0x00 }, // $16 - { 0x00, 0x00, 0x63, 0x6B, 0x7F, 0x3E, 0x36, 0x00 }, // $17 - { 0x00, 0x00, 0x66, 0x3C, 0x18, 0x3C, 0x66, 0x00 }, // $18 - { 0x00, 0x00, 0x66, 0x66, 0x66, 0x3E, 0x0C, 0x78 }, // $19 - { 0x00, 0x00, 0x7E, 0x0C, 0x18, 0x30, 0x7E, 0x00 }, // $1A - { 0x3C, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3C, 0x00 }, // $1B - { 0x0C, 0x12, 0x30, 0x7C, 0x30, 0x62, 0xFC, 0x00 }, // $1C - { 0x3C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x3C, 0x00 }, // $1D - { 0x00, 0x18, 0x3C, 0x7E, 0x18, 0x18, 0x18, 0x18 }, // $1E - { 0x00, 0x10, 0x30, 0x7F, 0x7F, 0x30, 0x10, 0x00 }, // $1F - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // $20 - { 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x18, 0x00 }, // $21 - { 0x66, 0x66, 0x22, 0x44, 0x00, 0x00, 0x00, 0x00 }, // $22 - { 0xF8, 0x30, 0x30, 0xE9, 0x1B, 0x1E, 0x1B, 0x1B }, // $23 - { 0x18, 0x3E, 0x60, 0x3C, 0x06, 0x7C, 0x18, 0x00 }, // $24 - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF }, // $25 - { 0x3C, 0x42, 0x9D, 0xB1, 0xB1, 0x9D, 0x42, 0x3C }, // $26 - { 0x06, 0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00 }, // $27 - { 0x0C, 0x18, 0x30, 0x30, 0x30, 0x18, 0x0C, 0x00 }, // $28 - { 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x18, 0x30, 0x00 }, // $29 - { 0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00 }, // $2A - { 0x00, 0x18, 0x18, 0x7E, 0x18, 0x18, 0x00, 0x00 }, // $2B - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x30 }, // $2C - { 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x00 }, // $2D - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00 }, // $2E - { 0x00, 0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x00 }, // $2F - { 0x3C, 0x66, 0x6E, 0x76, 0x66, 0x66, 0x3C, 0x00 }, // $30 - { 0x18, 0x18, 0x38, 0x18, 0x18, 0x18, 0x7E, 0x00 }, // $31 - { 0x3C, 0x66, 0x06, 0x0C, 0x30, 0x60, 0x7E, 0x00 }, // $32 - { 0x3C, 0x66, 0x06, 0x1C, 0x06, 0x66, 0x3C, 0x00 }, // $33 - { 0x06, 0x0E, 0x1E, 0x66, 0x7F, 0x06, 0x06, 0x00 }, // $34 - { 0x7E, 0x60, 0x7C, 0x06, 0x06, 0x66, 0x3C, 0x00 }, // $35 - { 0x3C, 0x66, 0x60, 0x7C, 0x66, 0x66, 0x3C, 0x00 }, // $36 - { 0x7E, 0x66, 0x0C, 0x18, 0x18, 0x18, 0x18, 0x00 }, // $37 - { 0x3C, 0x66, 0x66, 0x3C, 0x66, 0x66, 0x3C, 0x00 }, // $38 - { 0x3C, 0x66, 0x66, 0x3E, 0x06, 0x66, 0x3C, 0x00 }, // $39 - { 0x00, 0x00, 0x18, 0x00, 0x00, 0x18, 0x00, 0x00 }, // $3A - { 0x00, 0x00, 0x18, 0x00, 0x00, 0x18, 0x18, 0x30 }, // $3B - { 0x0E, 0x18, 0x30, 0x60, 0x30, 0x18, 0x0E, 0x00 }, // $3C - { 0x00, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0x00, 0x00 }, // $3D - { 0x70, 0x18, 0x0C, 0x06, 0x0C, 0x18, 0x70, 0x00 }, // $3E - { 0x3C, 0x66, 0x06, 0x0C, 0x18, 0x00, 0x18, 0x00 }, // $3F - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF }, // $40 - { 0x18, 0x3C, 0x66, 0x7E, 0x66, 0x66, 0x66, 0x00 }, // $41 - { 0x7C, 0x66, 0x66, 0x7C, 0x66, 0x66, 0x7C, 0x00 }, // $42 - { 0x3C, 0x66, 0x60, 0x60, 0x60, 0x66, 0x3C, 0x00 }, // $43 - { 0x78, 0x6C, 0x66, 0x66, 0x66, 0x6C, 0x78, 0x00 }, // $44 - { 0x7E, 0x60, 0x60, 0x78, 0x60, 0x60, 0x7E, 0x00 }, // $45 - { 0x7E, 0x60, 0x60, 0x78, 0x60, 0x60, 0x60, 0x00 }, // $46 - { 0x3C, 0x66, 0x60, 0x6E, 0x66, 0x66, 0x3C, 0x00 }, // $47 - { 0x66, 0x66, 0x66, 0x7E, 0x66, 0x66, 0x66, 0x00 }, // $48 - { 0x3C, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00 }, // $49 - { 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x6C, 0x38, 0x00 }, // $4A - { 0x66, 0x6C, 0x78, 0x70, 0x78, 0x6C, 0x66, 0x00 }, // $4B - { 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x7E, 0x00 }, // $4C - { 0x63, 0x77, 0x7F, 0x6B, 0x63, 0x63, 0x63, 0x00 }, // $4D - { 0x66, 0x76, 0x7E, 0x7E, 0x6E, 0x66, 0x66, 0x00 }, // $4E - { 0x3C, 0x66, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x00 }, // $4F - { 0x7C, 0x66, 0x66, 0x7C, 0x60, 0x60, 0x60, 0x00 }, // $50 - { 0x3C, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x0E, 0x00 }, // $51 - { 0x7C, 0x66, 0x66, 0x7C, 0x78, 0x6C, 0x66, 0x00 }, // $52 - { 0x3C, 0x66, 0x60, 0x3C, 0x06, 0x66, 0x3C, 0x00 }, // $53 - { 0x7E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00 }, // $54 - { 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x00 }, // $55 - { 0x66, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x18, 0x00 }, // $56 - { 0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00 }, // $57 - { 0x66, 0x66, 0x3C, 0x18, 0x3C, 0x66, 0x66, 0x00 }, // $58 - { 0x66, 0x66, 0x66, 0x3C, 0x18, 0x18, 0x18, 0x00 }, // $59 - { 0x7E, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x7E, 0x00 }, // $5A - { 0x18, 0x18, 0x18, 0xFF, 0xFF, 0x18, 0x18, 0x18 }, // $5B - { 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFF, 0xFF }, // $5C - { 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0 }, // $5D - { 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03 }, // $5E - { 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0xFF, 0xFF }, // $5F - { 0xFF, 0xFF, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0 }, // $60 - { 0xFF, 0xFF, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03 }, // $61 - { 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // $62 - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x79, 0xFF }, // $63 - { 0xFE, 0xFC, 0xF8, 0xF0, 0xE0, 0xC0, 0x80, 0x00 }, // $64 - { 0x7F, 0x3F, 0x1F, 0x0F, 0x07, 0x03, 0x01, 0x00 }, // $65 - { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, // $66 - { 0x88, 0x44, 0x22, 0x11, 0x22, 0x44, 0x88, 0x44 }, // $67 - { 0xFF, 0x33, 0xCC, 0x33, 0xCC, 0x33, 0xCC, 0xFF }, // $68 - { 0x00, 0x80, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC, 0xFE }, // $69 - { 0x00, 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F }, // $6A - { 0xFF, 0xE7, 0xC7, 0xE7, 0xE7, 0xE7, 0x81, 0xFF }, // $6B - { 0xFF, 0xE7, 0x99, 0xF9, 0xE7, 0x9F, 0x81, 0xFF }, // $6C - { 0xFF, 0x83, 0xF9, 0xE3, 0xF9, 0xF9, 0x83, 0xFF }, // $6D - { 0xFF, 0x99, 0x99, 0x81, 0xF9, 0xF9, 0xF9, 0xFF }, // $6E - { 0xFF, 0x81, 0x9F, 0x87, 0xF9, 0xF9, 0x87, 0xFF }, // $6F - { 0xFF, 0xC1, 0x9F, 0x83, 0x99, 0x99, 0xC3, 0xFF }, // $70 - { 0xFF, 0x81, 0xF9, 0xF3, 0xF3, 0xF3, 0xF3, 0xFF }, // $71 - { 0xFF, 0xC3, 0x99, 0xC3, 0x99, 0x99, 0xC3, 0xFF }, // $72 - { 0xFF, 0xC3, 0x99, 0x99, 0xC1, 0xF9, 0x83, 0xFF }, // $73 - { 0xFF, 0xC3, 0x99, 0x99, 0x99, 0x99, 0xC3, 0xFF }, // $74 - { 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00 }, // $75 - { 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18 }, // $76 - { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC7, 0xC7, 0xFF }, // $77 - { 0xF7, 0xC1, 0x97, 0x97, 0xE1, 0xE9, 0x83, 0xEF }, // $78 - { 0xFF, 0x81, 0x9F, 0x87, 0x9F, 0x9F, 0x9F, 0xFF }, // $79 - { 0xFF, 0x00, 0x00, 0x30, 0x30, 0x30, 0x30, 0xFF }, // $7A - { 0xFF, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xFF }, // $7B - { 0x20, 0x50, 0x50, 0x58, 0x89, 0x0A, 0x0A, 0x04 }, // $7C - { 0x03, 0x0F, 0x3F, 0xFF, 0xFF, 0x3F, 0x0F, 0x03 }, // $7D - { 0x0E, 0x1B, 0x18, 0x7E, 0xC3, 0xCF, 0xC3, 0xCF }, // $7E - { 0x00, 0xC0, 0xE0, 0xF0, 0xF0, 0xE0, 0xC0, 0x00 }, // $7F - { 0xFF, 0xD3, 0xD3, 0xFF, 0xD3, 0xD3, 0xD3, 0xFF }, // $80 - { 0x00, 0x03, 0x07, 0x0F, 0x0F, 0x07, 0x03, 0x00 }, // $81 - { 0xC0, 0xF0, 0xFC, 0xFF, 0xFF, 0xFC, 0xF0, 0xC0 }, // $82 - { 0x30, 0x79, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, // $83 - { 0x00, 0x00, 0x7E, 0xFF, 0x7E, 0x00, 0x00, 0x00 }, // $84 - { 0x7E, 0x7E, 0x7E, 0xFF, 0x7E, 0x7E, 0x7E, 0x00 }, // $85 - { 0x00, 0x7E, 0x7E, 0xFF, 0x7E, 0x7E, 0x00, 0x00 }, // $86 - { 0x00, 0x00, 0x7E, 0xFF, 0x7E, 0x00, 0x00, 0x00 }, // $87 - { 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00 }, // $88 - { 0x00, 0x03, 0x03, 0x03, 0x0F, 0x0F, 0x3F, 0xFF }, // $89 - { 0x00, 0xC0, 0xC0, 0xC0, 0xF0, 0xF0, 0xFC, 0xFF }, // $8A - { 0xFF, 0xFC, 0xF0, 0xF0, 0xC0, 0xC0, 0xC0, 0x00 }, // $8B - { 0xFF, 0x3F, 0x0F, 0x0F, 0x03, 0x03, 0x03, 0x00 }, // $8C - { 0x9F, 0x9F, 0x00, 0xF3, 0xF3, 0xF3, 0x00, 0x9F }, // $8D - { 0xFE, 0xFD, 0xFB, 0xF7, 0xEF, 0xDF, 0xBF, 0x7F }, // $8E - { 0x7F, 0xBF, 0xDF, 0xEF, 0xF7, 0xFB, 0xFD, 0xFE }, // $8F - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF }, // $90 - { 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C }, // $91 - { 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C }, // $92 - { 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C }, // $93 - { 0x22, 0x22, 0x46, 0x65, 0x95, 0x91, 0x90, 0x08 }, // $94 - { 0x22, 0x41, 0x61, 0x92, 0x4A, 0x4A, 0x55, 0x05 }, // $95 - { 0xFF, 0x3C, 0x5A, 0x5A, 0x42, 0x42, 0xE7, 0x42 }, // $96 - { 0x00, 0x3C, 0x7E, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C }, // $97 - { 0x10, 0x10, 0x20, 0x20, 0x40, 0x40, 0x80, 0x80 }, // $98 - { 0x4C, 0x61, 0x88, 0x4C, 0xC7, 0x19, 0x21, 0x91 }, // $99 - { 0x80, 0x80, 0x40, 0xC0, 0x60, 0xB0, 0x6C, 0xBB }, // $9A - { 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C }, // $9B - { 0x01, 0x02, 0x03, 0x06, 0x05, 0x0E, 0x32, 0xD5 }, // $9C - { 0x3C, 0x5A, 0x99, 0x3C, 0x5A, 0x99, 0x3C, 0x5A }, // $9D - { 0x00, 0x00, 0x77, 0x99, 0x99, 0x77, 0x00, 0x00 }, // $9E - { 0x00, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x7E, 0xFF }, // $9F - { 0x0F, 0x1F, 0x3F, 0x7F, 0x7F, 0xFF, 0xFF, 0xFF }, // $A0 - { 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x07, 0x0F }, // $A1 - { 0x04, 0x08, 0x30, 0x60, 0x38, 0x0C, 0x78, 0xF0 }, // $A2 - { 0xC0, 0xC0, 0x80, 0x00, 0x00, 0x80, 0xE0, 0xF0 }, // $A3 - { 0xE0, 0xC0, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00 }, // $A4 - { 0x00, 0x01, 0x07, 0x0E, 0x3E, 0x7C, 0x7C, 0xFE }, // $A5 - { 0x00, 0x03, 0x0F, 0x3C, 0x70, 0x38, 0x1F, 0x07 }, // $A6 - { 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0 }, // $A7 - { 0xFC, 0xFC, 0xFC, 0xFC, 0xFC, 0xFC, 0xFC, 0xFC }, // $A8 - { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xE0 }, // $A9 - { 0xFE, 0xFE, 0xFC, 0xFC, 0xF8, 0xF0, 0xE0, 0xC0 }, // $AA - { 0xFF, 0xFE, 0xFC, 0xF8, 0xE0, 0x80, 0x00, 0x00 }, // $AB - { 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03 }, // $AC - { 0x03, 0x03, 0x03, 0xFF, 0xFF, 0x03, 0x03, 0x03 }, // $AD - { 0x03, 0x03, 0x03, 0xFF, 0xFF, 0x00, 0x00, 0x00 }, // $AE - { 0x00, 0x00, 0x33, 0x66, 0xFF, 0x66, 0xFF, 0xFF }, // $AF - { 0x54, 0x54, 0x04, 0x04, 0x00, 0x00, 0x00, 0xFF }, // $B0 - { 0xFF, 0x00, 0xFF, 0x55, 0x41, 0x41, 0x55, 0x55 }, // $B1 - { 0x00, 0x00, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55 }, // $B2 - { 0x00, 0x00, 0x54, 0x54, 0x54, 0x54, 0x54, 0x15 }, // $B3 - { 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x01 }, // $B4 - { 0x00, 0x00, 0x01, 0x05, 0x05, 0x05, 0x05, 0x05 }, // $B5 - { 0xFF, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55 }, // $B6 - { 0x55, 0x55, 0x55, 0xD5, 0xD5, 0xD5, 0xD5, 0xD5 }, // $B7 - { 0xD5, 0xD5, 0xD5, 0xD5, 0xD5, 0xD5, 0xD5, 0xD5 }, // $B8 - { 0x99, 0x65, 0x49, 0x00, 0x00, 0x00, 0x00, 0x00 }, // $B9 - { 0x66, 0x59, 0x55, 0x95, 0x84, 0x00, 0x00, 0x00 }, // $BA - { 0x65, 0x55, 0x96, 0x59, 0x56, 0x95, 0x11, 0x00 }, // $BB - { 0x3C, 0x42, 0xB9, 0xA5, 0xB9, 0x95, 0x42, 0x3C }, // $BC - { 0x3C, 0x42, 0x9D, 0xA1, 0xA1, 0x9D, 0x42, 0x3C }, // $BD - { 0x03, 0x0C, 0x18, 0x28, 0x68, 0x68, 0xC8, 0xC8 }, // $BE - { 0xC8, 0xC8, 0x68, 0x68, 0x38, 0x18, 0x0C, 0x03 }, // $BF - { 0x00, 0x00, 0x00, 0x00, 0x03, 0x0F, 0x1B, 0xE3 }, // $C0 - { 0xF3, 0x1E, 0x0E, 0x06, 0x06, 0x08, 0x00, 0x00 }, // $C1 - { 0xC0, 0x60, 0x30, 0x30, 0x30, 0x3F, 0x32, 0x32 }, // $C2 - { 0x32, 0x32, 0x32, 0x32, 0x1A, 0x0E, 0x06, 0x03 }, // $C3 - { 0x30, 0x30, 0x30, 0x30, 0x30, 0x78, 0xCC, 0x83 }, // $C4 - { 0x00, 0x00, 0x00, 0x60, 0x30, 0xFF, 0x30, 0x30 }, // $C5 - { 0x60, 0x30, 0x18, 0x18, 0x18, 0xF8, 0x18, 0x18 }, // $C6 - { 0x18, 0x18, 0x18, 0x18, 0x30, 0x60, 0xC0, 0x80 }, // $C7 - { 0xFF, 0xFF, 0xF1, 0xC0, 0x80, 0xDD, 0xFF, 0xFF }, // $C8 - { 0xF8, 0x30, 0x30, 0xE9, 0x1B, 0x1E, 0x1B, 0x1B }, // $C9 - { 0xFF, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, 0xE7, 0xFF }, // $CA - { 0x03, 0x98, 0x9C, 0x83, 0x9C, 0x9C, 0x01, 0xFF }, // $CB - { 0xC3, 0x99, 0x99, 0x99, 0x99, 0x99, 0xC3, 0xFF }, // $CC - { 0x18, 0x89, 0x81, 0x81, 0x91, 0x99, 0x18, 0xFF }, // $CD - { 0x18, 0x99, 0x99, 0x99, 0x99, 0x81, 0xC3, 0xFF }, // $CE - { 0xC2, 0x99, 0x9F, 0xC3, 0xF9, 0x99, 0x43, 0xFF }, // $CF - { 0xFB, 0xFD, 0xFD, 0xFB, 0xFD, 0xB9, 0xCB, 0xFF }, // $D0 - { 0xDD, 0xB9, 0xDB, 0xA7, 0xDB, 0xB9, 0xDE, 0xFF }, // $D1 - { 0xBD, 0xBF, 0xB7, 0xBB, 0xDD, 0xED, 0xFD, 0xBD }, // $D2 - { 0x00, 0x00, 0x00, 0x00, 0x38, 0x78, 0xC0, 0x00 }, // $D3 - { 0x00, 0x0E, 0x0F, 0x01, 0x00, 0x00, 0x00, 0x00 }, // $D4 - { 0x00, 0xC7, 0x3F, 0xDF, 0x3F, 0x07, 0x01, 0x00 }, // $D5 - { 0x56, 0x55, 0x6B, 0x3E, 0xBF, 0x7C, 0xFE, 0xFF }, // $D6 - { 0x00, 0x85, 0xF5, 0xFF, 0xFE, 0xF5, 0x86, 0x00 }, // $D7 - { 0x99, 0x99, 0xC3, 0xE7, 0xC3, 0x99, 0x99, 0xFF }, // $D8 - { 0x99, 0x99, 0x99, 0xC3, 0xE7, 0xE7, 0xE7, 0xFF }, // $D9 - { 0x81, 0xF9, 0xF3, 0xE7, 0xCF, 0x9F, 0x81, 0xFF }, // $DA - { 0xE7, 0xE7, 0xE7, 0x00, 0x00, 0xE7, 0xE7, 0xE7 }, // $DB - { 0x3F, 0x3F, 0xCF, 0xCF, 0x3F, 0x3F, 0xCF, 0xCF }, // $DC - { 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7 }, // $DD - { 0xCC, 0xCC, 0x33, 0x33, 0xCC, 0xCC, 0x33, 0x33 }, // $DE - { 0xCC, 0x66, 0x33, 0x99, 0xCC, 0x66, 0x33, 0x99 }, // $DF - { 0x00, 0x3C, 0x7E, 0x5A, 0x66, 0x3C, 0x18, 0x3C }, // $E0 - { 0x00, 0x3E, 0x7F, 0x6F, 0x3F, 0x7E, 0x18, 0x3C }, // $E1 - { 0x00, 0x3E, 0x7F, 0x7B, 0x7E, 0x3F, 0x18, 0x3C }, // $E2 - { 0x00, 0x3C, 0x7E, 0x7E, 0x7E, 0x3C, 0x18, 0x3C }, // $E3 - { 0x3C, 0x42, 0x42, 0xFF, 0x5A, 0x66, 0x18, 0x3C }, // $E4 - { 0x3C, 0x42, 0xF2, 0x5A, 0x3F, 0x7C, 0x18, 0x3C }, // $E5 - { 0x3C, 0x42, 0x4F, 0x5A, 0xFC, 0x3E, 0x18, 0x3C }, // $E6 - { 0x3C, 0x42, 0x42, 0x42, 0xFF, 0x3C, 0x18, 0x3C }, // $E7 - { 0x66, 0x66, 0x66, 0x66, 0x66, 0x24, 0x66, 0xE7 }, // $E8 - { 0x6C, 0x6C, 0x6C, 0x6C, 0x68, 0x2C, 0x6E, 0xE0 }, // $E9 - { 0x36, 0x36, 0x36, 0x36, 0x16, 0x34, 0x76, 0x07 }, // $EA - { 0xF8, 0xD8, 0xD8, 0xCC, 0xCF, 0x8C, 0xC0, 0xE0 }, // $EB - { 0xFF, 0xDB, 0xDB, 0xDB, 0xDB, 0xDB, 0xDB, 0xFF }, // $EC - { 0xFF, 0x8D, 0xED, 0x81, 0xB7, 0xB1, 0xFF, 0xFF }, // $ED - { 0xFF, 0xC9, 0x93, 0xC9, 0x93, 0xFF, 0xFF, 0xFF }, // $EE - { 0x1F, 0x1B, 0x1B, 0x33, 0xF3, 0x31, 0x03, 0x07 }, // $EF - { 0x6C, 0x6C, 0x6C, 0x58, 0x58, 0x70, 0x78, 0x7C }, // $F0 - { 0x6C, 0x6C, 0x38, 0x30, 0xF0, 0xD0, 0x98, 0x1C }, // $F1 - { 0x36, 0x36, 0x1C, 0x0C, 0x0F, 0x0B, 0x19, 0x38 }, // $F2 - { 0x36, 0x36, 0x36, 0x1A, 0x1A, 0x0E, 0x1E, 0x3E }, // $F3 - { 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00 }, // $F4 - { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, // $F5 - { 0x03, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06 }, // $F6 - { 0xC0, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60 }, // $F7 - { 0xC0, 0x60, 0x60, 0x30, 0xF0, 0x70, 0x30, 0x30 }, // $F8 - { 0xC0, 0x60, 0x30, 0x30, 0x18, 0x3C, 0x66, 0x03 }, // $F9 - { 0xC0, 0x60, 0x60, 0x60, 0x3F, 0x1C, 0x18, 0x00 }, // $FA - { 0xC3, 0x66, 0x6C, 0x3C, 0x0E, 0x02, 0x00, 0x00 }, // $FB - { 0xCC, 0xEC, 0x6E, 0x6F, 0x3C, 0x18, 0x00, 0x00 }, // $FC - { 0xC3, 0x66, 0x36, 0x3C, 0x70, 0x40, 0x00, 0x00 }, // $FD - { 0x03, 0x06, 0x06, 0x0E, 0xFC, 0x38, 0x18, 0x00 }, // $FE - { 0x03, 0x06, 0x0C, 0x0C, 0x18, 0x3C, 0x66, 0x57 }, // $FF -}; - -const uint8_t kStSpriteBitmaps[ST_SPRITE_COUNT][ST_SPRITE_BYTES] = { - { // ptr $C0 - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x07, 0xE0, 0x00, - 0x1E, 0xB3, 0x80, - 0x7E, 0xB2, 0x00, - 0xBF, 0xDF, 0xC0, - 0xA1, 0xDC, 0xF0, - 0x6D, 0xF3, 0x70, - 0x0C, 0x03, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $C1 - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x07, 0xE0, 0x00, - 0x1E, 0xB3, 0x80, - 0x7E, 0xB2, 0x00, - 0xBF, 0xDF, 0xC0, - 0xA1, 0xDC, 0xF0, - 0x6D, 0xF3, 0x70, - 0x0C, 0x03, 0x00, - 0x1C, 0x03, 0x80, - 0x38, 0x01, 0xC0, - 0x30, 0x00, 0xC0, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $C2 - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x28, 0x00, - 0x00, 0xB8, 0x00, - 0x00, 0x20, 0x00, - 0x00, 0xA8, 0x00, - 0x00, 0xA8, 0x00, - 0x00, 0xF8, 0x00, - 0x00, 0x38, 0x00, - 0x00, 0x3C, 0x00, - 0x00, 0xCC, 0x00, - 0x00, 0xCE, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $C3 - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x28, 0x00, - 0x00, 0xB8, 0x00, - 0x00, 0x20, 0x00, - 0x00, 0xA8, 0x00, - 0x00, 0xAA, 0x00, - 0x00, 0xAB, 0x00, - 0x00, 0x70, 0x00, - 0x00, 0x3C, 0x00, - 0x00, 0x30, 0x00, - 0x00, 0xF0, 0x00, - 0x00, 0x38, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $C4 - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0xA0, 0x00, - 0x00, 0xE8, 0x00, - 0x00, 0x20, 0x00, - 0x00, 0xA8, 0x00, - 0x00, 0xA8, 0x00, - 0x00, 0x3C, 0x00, - 0x00, 0xB0, 0x00, - 0x00, 0xF0, 0x00, - 0x00, 0xCC, 0x00, - 0x02, 0xCC, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $C5 - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0xA0, 0x00, - 0x00, 0xE8, 0x00, - 0x00, 0x20, 0x00, - 0x00, 0xA8, 0x00, - 0x02, 0xA8, 0x00, - 0x03, 0x28, 0x00, - 0x00, 0x34, 0x00, - 0x00, 0xF0, 0x00, - 0x00, 0x30, 0x00, - 0x00, 0x3C, 0x00, - 0x00, 0xB0, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $C6 - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x20, 0x00, - 0x00, 0xB8, 0x00, - 0x00, 0x20, 0x00, - 0x00, 0x9A, 0x00, - 0x02, 0x23, 0x00, - 0x03, 0x20, 0x00, - 0x00, 0x74, 0x00, - 0x00, 0xCC, 0x00, - 0x00, 0xCC, 0x00, - 0x01, 0xCE, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $C7 - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x20, 0x00, - 0x00, 0xB8, 0x00, - 0x00, 0x20, 0x00, - 0x00, 0x9B, 0x00, - 0x02, 0x20, 0x00, - 0x03, 0x20, 0x00, - 0x00, 0x74, 0x00, - 0x00, 0xCC, 0x00, - 0x00, 0xCE, 0x00, - 0x01, 0xCC, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $C8 - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x20, 0x00, - 0x02, 0x00, 0x00, - 0x00, 0x04, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x98, 0x00, - 0x02, 0x22, 0x00, - 0x00, 0x20, 0x00, - 0x00, 0xFC, 0x00, - 0x00, 0xCC, 0x00, - 0x00, 0xCC, 0x00, - 0x01, 0xCE, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $C9 - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x08, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x20, 0x00, - 0x00, 0x00, 0x00, - 0x02, 0x22, 0x00, - 0x00, 0x20, 0x00, - 0x00, 0x3C, 0x00, - 0x00, 0xCC, 0x00, - 0x00, 0xCC, 0x00, - 0x01, 0xCE, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $CA - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x20, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x81, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x20, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0xC0, 0x00, - 0x00, 0xCC, 0x00, - 0x01, 0xCE, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $CB - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x04, 0x00, - 0x00, 0x20, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x40, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x0C, 0x00, - 0x00, 0x00, 0x00, - 0x01, 0xCE, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $CC - 0x00, 0x00, 0x00, - 0x00, 0x11, 0xC0, - 0x0C, 0x03, 0x00, - 0x42, 0x02, 0x00, - 0x02, 0x03, 0x00, - 0x02, 0x01, 0x00, - 0x01, 0x01, 0x30, - 0x7D, 0x1D, 0xF0, - 0x7D, 0xDE, 0x98, - 0x1F, 0xCA, 0x70, - 0x02, 0x6A, 0x40, - 0x0A, 0x2A, 0xC0, - 0x1C, 0x2F, 0x80, - 0x02, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $CD - 0x00, 0x10, 0x40, - 0x00, 0x00, 0x04, - 0x08, 0x03, 0x00, - 0x40, 0x01, 0xC0, - 0x03, 0x00, 0x80, - 0x00, 0x80, 0x80, - 0x01, 0x00, 0xB0, - 0x1D, 0x1D, 0xF0, - 0x35, 0xDA, 0x98, - 0x77, 0xCA, 0x50, - 0x40, 0x7A, 0x70, - 0x00, 0x18, 0xC1, - 0x04, 0x1C, 0x80, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $CE - 0x00, 0x00, 0x00, - 0x10, 0x03, 0x00, - 0x1C, 0x03, 0x00, - 0x00, 0x00, 0x08, - 0x02, 0x00, 0x10, - 0x00, 0x00, 0x00, - 0x23, 0xC0, 0x40, - 0x90, 0x2C, 0x70, - 0x84, 0x06, 0x20, - 0x2F, 0xC0, 0xE0, - 0x04, 0x14, 0x80, - 0x00, 0x06, 0x00, - 0x60, 0x1C, 0x60, - 0x00, 0x04, 0x00, - 0x00, 0x00, 0x02, - 0x20, 0x08, 0x08, - 0x10, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $CF - 0x00, 0x18, 0x00, - 0x10, 0x01, 0xC0, - 0x10, 0x00, 0x40, - 0x04, 0x00, 0x48, - 0x0C, 0x00, 0x00, - 0x20, 0x00, 0x00, - 0x00, 0xE1, 0xC0, - 0x18, 0xE1, 0x80, - 0xB0, 0x04, 0x02, - 0x00, 0x0C, 0x00, - 0x01, 0x00, 0x00, - 0x00, 0x00, 0xC0, - 0x08, 0x02, 0x40, - 0x00, 0x07, 0x10, - 0x00, 0x00, 0x00, - 0xC0, 0x08, 0x00, - 0x80, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $D0 - 0x00, 0x00, 0x00, - 0x10, 0x01, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x08, - 0x00, 0x10, 0x00, - 0x00, 0x04, 0x00, - 0x30, 0x05, 0x80, - 0x60, 0x00, 0x00, - 0x00, 0x00, 0x20, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x04, 0x00, 0x00, - 0x00, 0x00, 0xD0, - 0x00, 0x00, 0x00, - 0x00, 0x08, 0x00, - 0x10, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $D1 - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x10, 0x00, - 0x00, 0x14, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x01, - 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $D2 - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x01, - 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, - 0x00, 0x04, 0x00, - 0x00, 0x14, 0x00, - 0x00, 0x1C, 0x00, - 0x00, 0x04, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $D3 - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x40, 0x00, 0x00, - 0x40, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x04, 0x00, - 0x00, 0x14, 0x00, - 0x00, 0x1C, 0x00, - 0x00, 0x04, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $D4 - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x10, 0x00, - 0x00, 0x14, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $D5 - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x01, - 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $D6 - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x10, 0x00, - 0x00, 0x14, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x40, 0x00, 0x00, - 0x40, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $D7 - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x40, 0x00, 0x00, - 0x40, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $D8 - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x04, 0x00, - 0x00, 0x14, 0x00, - 0x00, 0x1C, 0x00, - 0x00, 0x04, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $D9 - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x20, 0x00, - 0x00, 0xBB, 0x00, - 0x00, 0x22, 0x00, - 0x00, 0x98, 0x00, - 0x02, 0x20, 0x00, - 0x03, 0x20, 0x00, - 0x00, 0x74, 0x00, - 0x00, 0xCC, 0x00, - 0x00, 0xCC, 0x00, - 0x01, 0xCE, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $DA - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x18, 0x00, - 0x00, 0x7C, 0x00, - 0x00, 0x30, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $DB - 0xFE, 0x00, 0x07, - 0x3E, 0x00, 0x07, - 0x3F, 0x00, 0x0D, - 0x3F, 0x00, 0x0D, - 0x37, 0x80, 0x19, - 0x37, 0x80, 0x19, - 0x33, 0xC0, 0x31, - 0x33, 0xC0, 0x31, - 0x31, 0xE0, 0x61, - 0x31, 0xE0, 0x61, - 0x30, 0xF0, 0xC1, - 0x30, 0xF0, 0xC1, - 0x30, 0x79, 0x81, - 0x30, 0x79, 0x81, - 0x30, 0x3F, 0x01, - 0x30, 0x3F, 0x01, - 0x30, 0x1E, 0x01, - 0x30, 0x1E, 0x01, - 0x30, 0x0C, 0x01, - 0x30, 0x0C, 0x01, - 0xFC, 0x00, 0x07, - }, - { // ptr $DC - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0xBD, 0x00, - 0x2C, 0xEB, 0x40, - 0x08, 0xEB, 0xD0, - 0x3F, 0x7F, 0xE0, - 0xF3, 0x74, 0xA0, - 0xDF, 0xF7, 0x90, - 0x0C, 0x03, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $DD - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0xBD, 0x00, - 0x2C, 0xEB, 0x40, - 0x08, 0xEB, 0xD0, - 0x3F, 0x7F, 0xE0, - 0xF3, 0x74, 0xA0, - 0xDF, 0xF7, 0x90, - 0x0C, 0x03, 0x00, - 0x1C, 0x03, 0x80, - 0x38, 0x01, 0xC0, - 0x30, 0x00, 0xC0, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $DE - 0xF8, 0x00, 0x00, - 0xE0, 0x00, 0x00, - 0xE0, 0x00, 0x00, - 0xE0, 0x00, 0x00, - 0xE0, 0x00, 0x00, - 0xE0, 0x00, 0x00, - 0xE0, 0x00, 0x00, - 0xE0, 0x00, 0x00, - 0xE0, 0x00, 0x00, - 0xE0, 0x00, 0x00, - 0xE0, 0x00, 0x00, - 0xE0, 0x00, 0x00, - 0xE0, 0x00, 0x00, - 0xE0, 0x00, 0x00, - 0xE0, 0x00, 0x00, - 0xE0, 0x00, 0x00, - 0xE0, 0x00, 0x00, - 0xE0, 0x00, 0x00, - 0xE0, 0x00, 0x00, - 0xE0, 0x00, 0x00, - 0xF8, 0x00, 0x00, - }, - { // ptr $DF - 0xFF, 0x00, 0x3F, - 0x3C, 0x00, 0x0C, - 0x3C, 0x00, 0x0C, - 0x3C, 0x00, 0x0C, - 0x3C, 0x00, 0x0C, - 0x3C, 0x00, 0x0C, - 0x3C, 0x00, 0x0C, - 0x3C, 0x00, 0x0C, - 0x3C, 0x00, 0x0C, - 0x3C, 0x00, 0x0C, - 0x3C, 0x00, 0x0C, - 0x3C, 0x00, 0x0C, - 0x3C, 0x00, 0x0C, - 0x3C, 0x00, 0x0C, - 0x3C, 0x00, 0x0C, - 0x3E, 0x00, 0x18, - 0x1E, 0x00, 0x18, - 0x1F, 0x80, 0x70, - 0x07, 0xF3, 0xF0, - 0x01, 0xFF, 0xC0, - 0x00, 0x3E, 0x00, - }, - { // ptr $E0 - 0x00, 0xFF, 0x03, - 0x0F, 0xFF, 0xFF, - 0x3F, 0x00, 0xFF, - 0x7C, 0x00, 0x0F, - 0xF0, 0x00, 0x03, - 0xF0, 0x00, 0x00, - 0xF0, 0x00, 0x00, - 0x7C, 0x00, 0x00, - 0x3F, 0x00, 0x00, - 0x0F, 0xF0, 0x00, - 0x00, 0xFF, 0x00, - 0x00, 0x0F, 0xF0, - 0x00, 0x00, 0xFC, - 0x00, 0x00, 0x3E, - 0x00, 0x00, 0x0F, - 0x00, 0x00, 0x0F, - 0xC0, 0x00, 0x0F, - 0xF0, 0x00, 0x3E, - 0xFF, 0x00, 0xFC, - 0xFF, 0xFF, 0xF0, - 0xC0, 0xFF, 0x00, - }, - { // ptr $E1 - 0xFF, 0xFF, 0xFF, - 0x3C, 0x00, 0x07, - 0x3C, 0x00, 0x03, - 0x3C, 0x00, 0x00, - 0x3C, 0x00, 0x00, - 0x3C, 0x00, 0x00, - 0x3C, 0x00, 0x00, - 0x3C, 0x00, 0x00, - 0x3C, 0x00, 0x60, - 0x3C, 0x00, 0xE0, - 0x3F, 0xFF, 0xE0, - 0x3C, 0x00, 0xE0, - 0x3C, 0x00, 0x60, - 0x3C, 0x00, 0x00, - 0x3C, 0x00, 0x00, - 0x3C, 0x00, 0x00, - 0x3C, 0x00, 0x00, - 0x3C, 0x00, 0x00, - 0x3C, 0x00, 0x03, - 0x3C, 0x00, 0x07, - 0xFF, 0xFF, 0xFF, - }, - { // ptr $E2 - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x07, 0xE0, 0x00, - 0x1E, 0xB3, 0x80, - 0x7E, 0xB2, 0x00, - 0xBF, 0xDF, 0xC0, - 0xA1, 0xDC, 0xC0, - 0x6D, 0xF3, 0xC0, - 0x0C, 0x03, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $E3 - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x07, 0xE0, 0x00, - 0x06, 0xBE, 0x00, - 0x1E, 0xB8, 0x00, - 0x7F, 0xDF, 0x00, - 0x61, 0xDF, 0x00, - 0x6D, 0xF7, 0x00, - 0x0C, 0x0C, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $E4 - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x07, 0xE0, 0x00, - 0x06, 0xB8, 0x00, - 0x1E, 0xB8, 0x00, - 0x7F, 0xFC, 0x00, - 0xD7, 0xD7, 0x00, - 0x7D, 0xBC, 0x00, - 0x3C, 0x3C, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $E5 - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x03, 0xE0, 0x00, - 0x0E, 0xB0, 0x00, - 0x0F, 0xF0, 0x00, - 0x37, 0xDC, 0x00, - 0x3D, 0xBC, 0x00, - 0x0C, 0x30, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $E6 - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x03, 0xC0, 0x00, - 0x06, 0xB0, 0x00, - 0x0D, 0xD0, 0x00, - 0x0F, 0xF0, 0x00, - 0x06, 0xC0, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $E7 - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x03, 0x00, 0x00, - 0x0E, 0xC0, 0x00, - 0x0F, 0xC0, 0x00, - 0x06, 0xC0, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $E8 - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x01, 0x80, 0x00, - 0x03, 0xC0, 0x00, - 0x01, 0x80, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $E9 - 0x00, 0x00, 0x00, - 0x0C, 0x00, 0x00, - 0x3E, 0x38, 0x80, - 0x3A, 0x28, 0x00, - 0x0E, 0xEE, 0x00, - 0x00, 0x82, 0x00, - 0x01, 0xE3, 0xC0, - 0x67, 0x60, 0x80, - 0x06, 0x79, 0x80, - 0x03, 0x91, 0x00, - 0x00, 0xBB, 0x20, - 0x00, 0xEE, 0x00, - 0x0C, 0x60, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x18, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $EA - 0x00, 0x00, 0x00, - 0x0C, 0x00, 0x00, - 0x26, 0x38, 0x00, - 0x38, 0x00, 0x00, - 0x08, 0xB8, 0x00, - 0x00, 0x92, 0x00, - 0x00, 0x1B, 0x40, - 0x07, 0x20, 0x00, - 0x18, 0x09, 0x80, - 0x03, 0x45, 0x00, - 0x00, 0x21, 0x00, - 0x01, 0xAC, 0x00, - 0x00, 0x20, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $EB - 0x00, 0x00, 0x00, - 0x0C, 0x00, 0x00, - 0x20, 0x28, 0x00, - 0x28, 0x00, 0x00, - 0x08, 0x08, 0x00, - 0x00, 0x80, 0x00, - 0x00, 0x11, 0x40, - 0x05, 0x00, 0x00, - 0x18, 0x49, 0x80, - 0x00, 0x00, 0x00, - 0x00, 0x21, 0x00, - 0x01, 0x84, 0x00, - 0x00, 0x20, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - { // ptr $EC - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, -}; - -const uint8_t kStRngTable[ST_RNG_TABLE_SIZE] = { - 0x23, 0x25, 0x76, 0xC1, 0x75, 0xE7, 0x7B, 0x2E, 0x13, 0x6A, 0xAC, 0xD4, 0xA6, 0xAD, 0xE9, 0x7A, - 0x74, 0xE8, 0xEE, 0x76, 0xAC, 0x37, 0x27, 0xDF, 0x81, 0xE7, 0x1C, 0xC0, 0x91, 0xE3, 0x96, 0x4A, - 0x75, 0x59, 0xE0, 0x7A, 0x11, 0xE9, 0x5E, 0xF9, 0x76, 0x29, 0x93, 0x6C, 0xEE, 0x6C, 0xA8, 0xC3, - 0xE0, 0xA5, 0x81, 0xE3, 0x3D, 0xDB, 0xA3, 0x73, 0xF7, 0x16, 0x5F, 0x9B, 0xD8, 0xDC, 0xC4, 0x69, -}; - -const uint8_t kStSfxPrograms[ST_SFX_COUNT][ST_SFX_PROGRAM_BYTES] = { - { 0x20, 0x20, 0xFF, 0x03, 0x61, 0x00, 0xF6, 0x07, 0x00 }, // ST_SFX_GEAR - { 0x04, 0x04, 0xFF, 0x07, 0x41, 0x46, 0x58, 0x05, 0x00 }, // ST_SFX_CASH - { 0x78, 0x78, 0x00, 0x00, 0x21, 0x00, 0xF9, 0x06, 0x00 }, // ST_SFX_FUEL_LOW - { 0x5A, 0x5A, 0x00, 0x00, 0x21, 0x10, 0xF9, 0x0A, 0x00 }, // ST_SFX_FUEL_FULL - { 0x80, 0x01, 0x96, 0x02, 0x41, 0x20, 0xF8, 0x0A, 0x00 }, // ST_SFX_LAND_HARD - { 0x22, 0x22, 0x00, 0x00, 0x81, 0x30, 0xFB, 0x03, 0x00 }, // ST_SFX_IMPACT - { 0x02, 0x02, 0xFF, 0x03, 0x41, 0x00, 0xF7, 0x03, 0x00 }, // ST_SFX_LAND_SOFT - { 0x50, 0x50, 0xFF, 0x06, 0x41, 0x50, 0xFB, 0x06, 0x00 }, // ST_SFX_CRASH -}; - -const uint8_t kStFlameCelByDirMask[16] = { - 0x00, 0xD8, 0xD4, 0x00, 0xD5, 0xD2, 0xD1, 0x00, 0xD7, 0xD3, 0xD6, 0x00, 0x00, 0x00, 0x00, 0x00, -}; - - -// --------------------------------------------------------------------------- -// Accessors. On the IIgs the program is split across banks and a 2-byte -// data reference cannot cross a segment (only a 3-byte JSL/JML can). These -// tiny functions live in the same segment as the tables, so their reads -// are intra-segment; callers in other segments reach them by a supported -// inter-segment call. On the other ports they inline away to nothing. -// --------------------------------------------------------------------------- - -const uint8_t *stC64Charset(void) { - return &kStCharset[0][0]; -} - - -const uint8_t *stC64SpriteBitmap(uint8_t idx) { - return kStSpriteBitmaps[idx]; -} - - -uint8_t stC64RngByte(uint8_t idx) { - return kStRngTable[idx]; -} - - -const uint8_t *stC64SfxProgram(uint8_t idx) { - return kStSfxPrograms[idx]; -} - - -uint8_t stC64FlameCel(uint8_t dirMask) { - return kStFlameCelByDirMask[dirMask]; -} diff --git a/examples/spacetaxi/stC64Data.h b/examples/spacetaxi/stC64Data.h deleted file mode 100644 index c30c083..0000000 --- a/examples/spacetaxi/stC64Data.h +++ /dev/null @@ -1,56 +0,0 @@ -// Generated by assets/genC64Data.py from stuff/spacetaxi/raw.bin. -// Do not hand-edit; re-run the generator. - -#ifndef ST_C64_DATA_H -#define ST_C64_DATA_H - -#include - -// The game's custom character set ($2800-$2FFF): 256 glyphs x 8 rows, -// bit 7 = leftmost pixel. Used to paint the screen AND for the -// sprite-to-background collision test (a set bit is background data). -#define ST_CHARSET_CHARS 256u -extern const uint8_t kStCharset[ST_CHARSET_CHARS][8]; - -// Standard sprite bitmaps (VIC bank 0, block ptr * 64). 21 rows x 3 -// bytes. Index = ptr - ST_SPRITE_PTR_FIRST. -#define ST_SPRITE_PTR_FIRST 0xC0u -#define ST_SPRITE_PTR_LAST 0xECu -#define ST_SPRITE_COUNT 45u -#define ST_SPRITE_BYTES 63u -extern const uint8_t kStSpriteBitmaps[ST_SPRITE_COUNT][ST_SPRITE_BYTES]; - -// Demo-mode RNG lookup ($446A): rnd = table[t1] + t2 (see stSim.c). -#define ST_RNG_TABLE_SIZE 64u -extern const uint8_t kStRngTable[ST_RNG_TABLE_SIZE]; - -// SID SFX programs, 9 bytes each, in $42E9 order: freq lo, freq hi, -// pulse lo, pulse hi, control, AD, SR, release ticks, voice index. -typedef enum { - ST_SFX_GEAR, // $6410 landing gear toggle ($63DD) - ST_SFX_CASH, // $6DBB refuel ka-ching ($6E5B) - ST_SFX_FUEL_LOW, // $6DC4 fuel below 3 cells ($6EBD) - ST_SFX_FUEL_FULL, // $6DCD tank full ($6E9E) - ST_SFX_LAND_HARD, // $6DD6 hard landing ($6520) - ST_SFX_IMPACT, // $6DDF wreck hits the floor ($6AE1) - ST_SFX_LAND_SOFT, // $6DE8 soft landing ($650F) - ST_SFX_CRASH, // $6DF1 crash start ($6A56) - ST_SFX_COUNT -} StSfxE; -#define ST_SFX_PROGRAM_BYTES 9u -extern const uint8_t kStSfxPrograms[ST_SFX_COUNT][ST_SFX_PROGRAM_BYTES]; - -// Engine-flame cel pointer by direction mask ($6DB0, 1=UP 2=DOWN 4=LEFT -// 8=RIGHT); 0 = no cel for that combination. -extern const uint8_t kStFlameCelByDirMask[16]; - -// Cross-segment-safe accessors (see stC64Data.c): reach the tables above -// by an inter-segment call on the IIgs, where a direct 2-byte data -// reference from another bank is not relocatable. -const uint8_t *stC64Charset(void); -const uint8_t *stC64SpriteBitmap(uint8_t idx); -uint8_t stC64RngByte(uint8_t idx); -const uint8_t *stC64SfxProgram(uint8_t idx); -uint8_t stC64FlameCel(uint8_t dirMask); - -#endif diff --git a/examples/spacetaxi/stCels.c b/examples/spacetaxi/stCels.c deleted file mode 100644 index 207f887..0000000 --- a/examples/spacetaxi/stCels.c +++ /dev/null @@ -1,103 +0,0 @@ -// Space Taxi -- the cel bank definition + the shared cel expansion. -// See stCels.h. Ordered exactly as stRenderPrewarm built the cels: the -// cab set (flying, wreck and the intro decorations) and passenger set in -// multicolour at the constant level colours (cab/passenger 6, shared -// $D025/$D026 = 2/7), then the flame set in multicolour colour 7. No -// intro star: the level intro paints its starfield into the character -// screen (stTitle.c), so the star sprite is never drawn. - -#include "stCels.h" - -// mc0/mc1 are the level-wide multicolour registers, constant across all -// 24 screens and the title (verified from the level headers): 2 and 7. -#define MC0 0x02u -#define MC1 0x07u -#define CAB 0x06u // cab and passenger per-sprite colour -#define FLM 0x07u // flame colour - -const StCelDefT kStCels[] = { - // Cab: flying (gear up/down x left/right), wreck cels, intro cabs. - { 0xC0, CAB, 1u, MC0, MC1 }, { 0xC1, CAB, 1u, MC0, MC1 }, - { 0xDC, CAB, 1u, MC0, MC1 }, { 0xDD, CAB, 1u, MC0, MC1 }, - { 0xCC, CAB, 1u, MC0, MC1 }, { 0xCD, CAB, 1u, MC0, MC1 }, - { 0xCE, CAB, 1u, MC0, MC1 }, { 0xCF, CAB, 1u, MC0, MC1 }, - { 0xD0, CAB, 1u, MC0, MC1 }, { 0xD1, CAB, 1u, MC0, MC1 }, - { 0xE2, CAB, 1u, MC0, MC1 }, { 0xE3, CAB, 1u, MC0, MC1 }, - { 0xE4, CAB, 1u, MC0, MC1 }, { 0xE5, CAB, 1u, MC0, MC1 }, - { 0xE6, CAB, 1u, MC0, MC1 }, { 0xE7, CAB, 1u, MC0, MC1 }, - { 0xE8, CAB, 1u, MC0, MC1 }, - // Passenger: walk cels and the boarding cel. - { 0xC2, CAB, 1u, MC0, MC1 }, { 0xC3, CAB, 1u, MC0, MC1 }, - { 0xC4, CAB, 1u, MC0, MC1 }, { 0xC5, CAB, 1u, MC0, MC1 }, - { 0xC6, CAB, 1u, MC0, MC1 }, { 0xC7, CAB, 1u, MC0, MC1 }, - { 0xC8, CAB, 1u, MC0, MC1 }, { 0xC9, CAB, 1u, MC0, MC1 }, - { 0xCA, CAB, 1u, MC0, MC1 }, { 0xCB, CAB, 1u, MC0, MC1 }, - { 0xD9, CAB, 1u, MC0, MC1 }, - // Flame (exhaust) cels, colour 7. - { 0xD8, FLM, 1u, MC0, MC1 }, { 0xD4, FLM, 1u, MC0, MC1 }, - { 0xD5, FLM, 1u, MC0, MC1 }, { 0xD2, FLM, 1u, MC0, MC1 }, - { 0xD1, FLM, 1u, MC0, MC1 }, { 0xD7, FLM, 1u, MC0, MC1 }, - { 0xD3, FLM, 1u, MC0, MC1 }, { 0xD6, FLM, 1u, MC0, MC1 }, -}; - -const uint16_t kStCelCount = (uint16_t)(sizeof(kStCels) / sizeof(kStCels[0])); - - -// Cels the title screen draws that are deliberately NOT pre-compiled. -// -// Its five decoration sprites are recoloured by logoColorCycle ($4861), -// which rewrites sprites 3..7 with the next logo colour every cycle -// step, so each is seen in all eight of kLogoColor's values -- forty -// combinations. The parked cab's jet is a forty-first, colour 6 rather -// than the flame list's 7. None of them are worth a bank slot: on the -// 65816 a compile costs about half a second, so baking the forty would -// add some twenty seconds to startup, and raising kStCelCount at all is -// risky -- 37 made the runtime reject the whole precompiled bank (see -// the note in the project memory). They draw interpreted, which -// a static title screen can well afford. stRender consults this list so -// the mid-ride warning stays about real misses. -// A zero colour means "in any colour": the decorations cycle through all -// eight. The jet is listed for the cab colour alone, because the flame -// list above already carries it in colour 7 for the ride itself. -const StCelDefT kStInterpretedCels[] = { - { 0xDB, 0u, 1u, MC0, MC1 }, { 0xDE, 0u, 1u, MC0, MC1 }, - { 0xDF, 0u, 1u, MC0, MC1 }, { 0xE0, 0u, 1u, MC0, MC1 }, - { 0xE1, 0u, 1u, MC0, MC1 }, - { 0xD8, CAB, 1u, MC0, MC1 }, -}; -const uint16_t kStInterpretedCount = (uint16_t)(sizeof(kStInterpretedCels) / sizeof(kStInterpretedCels[0])); - - -void stCelBlob(const uint8_t *bm, uint8_t multi, uint8_t color, uint8_t mc0, uint8_t mc1, uint8_t *out) { - uint8_t pal[4]; - uint8_t tx; - uint8_t ty; - uint8_t row; - uint8_t k; - - pal[0] = 0u; - pal[1] = mc0; - pal[2] = color; - pal[3] = mc1; - for (ty = 0u; ty < ST_CEL_TILES; ty++) { - for (tx = 0u; tx < ST_CEL_TILES; tx++) { - uint8_t *tile = &out[(ty * ST_CEL_TILES + tx) * 32u]; - for (row = 0u; row < 8u; row++) { - uint8_t y = (uint8_t)(ty * 8u + row); - uint8_t *p = &tile[row * 4u]; - uint8_t b = 0u; - if (bm != 0 && y < ST_CEL_ROWS) { - b = bm[y * 3u + tx]; - } - for (k = 0u; k < 4u; k++) { - uint8_t pair = (uint8_t)((b >> (6u - k * 2u)) & 3u); - if (multi != 0u) { - p[k] = (uint8_t)((pal[pair] << 4) | pal[pair]); - } else { - p[k] = (uint8_t)((((pair & 2u) != 0u) ? (uint8_t)(color << 4) : 0u) | (((pair & 1u) != 0u) ? color : 0u)); - } - } - } - } - } -} diff --git a/examples/spacetaxi/stCels.h b/examples/spacetaxi/stCels.h deleted file mode 100644 index f10f959..0000000 --- a/examples/spacetaxi/stCels.h +++ /dev/null @@ -1,48 +0,0 @@ -// Space Taxi -- the sprite cels the renderer needs, as a fixed-order -// bank. The offline baker (tools/staxibake) emits a .spr in this order, -// spritebake compiles it to a per-platform .spc, and stRenderPrewarm -// loads that with jlSpriteBankLoadPrecompiled -- so the moving cels -// arrive already compiled and the ~0.4 s-per-cel 65816 JIT never runs at -// boot. The game maps loaded cel i back to its (ptr, colour, mode) key -// from kStCels[i]. stCelBlob is the single expansion both the baker and -// the interpreter fallback use, so the compiled and built cels agree. - -#ifndef ST_CELS_H -#define ST_CELS_H - -#include - -#include "stSim.h" - -// 3x3 tiles = 24x24 px; 32 bytes per 8x8 tile, tile-major (matches -// jlSpriteCreateFromSurface / the .spr cel layout). -#define ST_CEL_TILES 3u -#define ST_CEL_BYTES (ST_CEL_TILES * ST_CEL_TILES * 32u) -#define ST_CEL_ROWS 21u // a VIC sprite is 21 rows tall - -typedef struct { - uint8_t ptr; // VIC sprite block pointer - uint8_t color; // per-sprite colour (the multicolour "10" pair) - uint8_t multi; // 1 = multicolour, 0 = hires - uint8_t mc0; // shared multicolour 0 ($D025); 0 when hires - uint8_t mc1; // shared multicolour 1 ($D026); 0 when hires -} StCelDefT; - -extern const StCelDefT kStCels[]; -extern const uint16_t kStCelCount; -// Cels the title screen draws interpreted by design; a zero colour -// entry matches any colour. See the note beside the table in stCels.c. -extern const StCelDefT kStInterpretedCels[]; -extern const uint16_t kStInterpretedCount; - -// The cels a level's hook program can show (stLevelCels.c): at most `max` -// into `out`, no duplicates. 0 for a level without hook sprites. -#define ST_LEVEL_CELS_MAX 40u -uint8_t stLevelCelList(const StLevelT *level, StCelDefT *out, uint8_t max); - -// Expand a VIC bitmap (63 bytes, 21 rows x 3, or NULL) into the 288-byte -// tile-major 4bpp-packed cel blob. Colours as in the C64 multicolour -// rules; hires uses `color` for a set bit. -void stCelBlob(const uint8_t *bm, uint8_t multi, uint8_t color, uint8_t mc0, uint8_t mc1, uint8_t *out); - -#endif diff --git a/examples/spacetaxi/stDemoStreams.h b/examples/spacetaxi/stDemoStreams.h deleted file mode 100644 index a4baf41..0000000 --- a/examples/spacetaxi/stDemoStreams.h +++ /dev/null @@ -1,230 +0,0 @@ -// Generated by assets/genDemoStreams.py -- do not hand-edit. -// C64 attract-demo recorded input streams (see the generator -// for format notes). Rotation order H, W, T, X. - -#ifndef ST_DEMO_STREAMS_H -#define ST_DEMO_STREAMS_H - -#include - -// H: SPACETAXI 01_N0S.prg (297 bytes) -static const uint8_t kDemoStreamH[297] = { - 0x80, 0x17, 0x88, 0x0A, 0x80, 0x0A, 0x84, 0x04, 0x85, 0x03, 0x81, 0x03, - 0x80, 0x0A, 0x88, 0x04, 0x80, 0x01, 0x81, 0x0E, 0x80, 0x0D, 0x82, 0x02, - 0x80, 0x0A, 0x81, 0x09, 0x85, 0x07, 0x84, 0x05, 0x80, 0x02, 0x88, 0x05, - 0x8A, 0x04, 0x82, 0x03, 0x80, 0x05, 0x85, 0x10, 0x81, 0x08, 0x89, 0x02, - 0x88, 0x0E, 0x8A, 0x03, 0x88, 0x06, 0x81, 0x07, 0x80, 0x12, 0x82, 0x05, - 0x80, 0x04, 0x81, 0x11, 0x80, 0x04, 0x82, 0x04, 0x86, 0x09, 0x84, 0x02, - 0x95, 0x07, 0x85, 0x04, 0x81, 0x01, 0x80, 0x43, 0x81, 0x0A, 0x84, 0x05, - 0x86, 0x03, 0x82, 0x06, 0x80, 0x07, 0x81, 0x09, 0x80, 0x04, 0x88, 0x0B, - 0x80, 0x09, 0x84, 0x04, 0x80, 0x09, 0x81, 0x01, 0x89, 0x04, 0x80, 0x08, - 0x85, 0x04, 0x81, 0x01, 0x80, 0x05, 0x81, 0x15, 0x80, 0x0C, 0x82, 0x02, - 0x86, 0x07, 0x80, 0x05, 0x81, 0x09, 0x80, 0x13, 0x84, 0x01, 0x85, 0x03, - 0x80, 0x06, 0x88, 0x0B, 0x80, 0x16, 0x81, 0x0A, 0x80, 0x01, 0x88, 0x04, - 0x80, 0x0C, 0x81, 0x03, 0x85, 0x02, 0x84, 0x03, 0x85, 0x0C, 0x81, 0x01, - 0x80, 0x0D, 0x82, 0x07, 0x80, 0x06, 0x81, 0x02, 0x89, 0x0C, 0x88, 0x02, - 0x80, 0x05, 0x82, 0x04, 0x80, 0x0A, 0x81, 0x04, 0x85, 0x01, 0x84, 0x04, - 0x80, 0x0D, 0x81, 0x05, 0x89, 0x0E, 0x81, 0x01, 0x80, 0x11, 0x84, 0x17, - 0x80, 0x05, 0x81, 0x04, 0x80, 0x19, 0x81, 0x03, 0x89, 0x01, 0x88, 0x0E, - 0x80, 0x07, 0x84, 0x05, 0x80, 0x0C, 0x81, 0x0D, 0x80, 0x19, 0x88, 0x01, - 0x80, 0x02, 0x88, 0x04, 0x80, 0x1B, 0x88, 0x0A, 0x89, 0x0B, 0x81, 0x14, - 0x80, 0x02, 0x82, 0x03, 0x80, 0x05, 0x84, 0x03, 0x85, 0x09, 0x84, 0x05, - 0x85, 0x05, 0x84, 0x02, 0x86, 0x08, 0x84, 0x01, 0x80, 0x01, 0x81, 0x03, - 0x89, 0x12, 0x81, 0x08, 0x89, 0x01, 0x88, 0x07, 0x8A, 0x04, 0x82, 0x0F, - 0x80, 0x07, 0x81, 0x04, 0x80, 0x03, 0x82, 0x03, 0x86, 0x09, 0x84, 0x08, - 0x81, 0x03, 0x89, 0x06, 0x80, 0x01, 0x86, 0x08, 0x82, 0x01, 0x80, 0x03, - 0x88, 0x03, 0x80, 0x04, 0x95, 0x07, 0x85, 0x0A, 0x80, -}; - -// W: SPACETAXI 03_N0S.prg (335 bytes) -static const uint8_t kDemoStreamW[335] = { - 0x80, 0x0E, 0x82, 0x05, 0x8A, 0x01, 0x88, 0x0F, 0x89, 0x06, 0x81, 0x06, - 0x85, 0x0D, 0x84, 0x07, 0x86, 0x0C, 0x82, 0x01, 0x88, 0x09, 0x89, 0x02, - 0x81, 0x02, 0x80, 0x05, 0x88, 0x04, 0x80, 0x06, 0x84, 0x04, 0x80, 0x02, - 0x88, 0x13, 0x81, 0x01, 0x80, 0x01, 0x84, 0x0B, 0x85, 0x07, 0x80, 0x08, - 0x88, 0x14, 0x80, 0x02, 0x84, 0x0F, 0x80, 0x0D, 0x84, 0x05, 0x85, 0x03, - 0x80, 0x04, 0x88, 0x16, 0x81, 0x01, 0x80, 0x03, 0x84, 0x02, 0x86, 0x12, - 0x80, 0x09, 0x81, 0x18, 0x80, 0x0B, 0x81, 0x05, 0x85, 0x06, 0x84, 0x08, - 0x86, 0x0A, 0x84, 0x01, 0x80, 0x01, 0x81, 0x02, 0x89, 0x0B, 0x88, 0x0C, - 0x80, 0x17, 0x82, 0x01, 0x86, 0x0B, 0x84, 0x02, 0x85, 0x09, 0x81, 0x01, - 0x89, 0x01, 0x88, 0x1E, 0x80, 0x03, 0x84, 0x02, 0x85, 0x06, 0x95, 0x02, - 0x94, 0x04, 0x90, 0x01, 0x80, 0x09, 0x82, 0x06, 0x80, 0x07, 0x81, 0x04, - 0x80, 0x0E, 0x81, 0x01, 0x80, 0x38, 0x81, 0x07, 0x89, 0x01, 0x88, 0x05, - 0x80, 0x0C, 0x84, 0x01, 0x85, 0x05, 0x81, 0x01, 0x80, 0x10, 0x88, 0x04, - 0x8A, 0x0E, 0x88, 0x02, 0x89, 0x04, 0x81, 0x04, 0x85, 0x01, 0x84, 0x22, - 0x81, 0x03, 0x89, 0x0A, 0x81, 0x01, 0x80, 0x0E, 0x82, 0x08, 0x8A, 0x08, - 0x88, 0x09, 0x89, 0x02, 0x81, 0x07, 0x85, 0x03, 0x84, 0x03, 0x85, 0x0F, - 0x81, 0x02, 0x89, 0x06, 0x88, 0x04, 0x80, 0x02, 0x84, 0x06, 0x86, 0x03, - 0x82, 0x05, 0x80, 0x01, 0x89, 0x07, 0x88, 0x02, 0x80, 0x04, 0x86, 0x16, - 0x84, 0x02, 0x85, 0x03, 0x81, 0x04, 0x89, 0x09, 0x88, 0x05, 0x80, 0x11, - 0x88, 0x02, 0x8A, 0x0A, 0x82, 0x01, 0x80, 0x04, 0x84, 0x08, 0x85, 0x12, - 0x84, 0x06, 0x81, 0x01, 0x89, 0x07, 0x88, 0x07, 0x8A, 0x14, 0x82, 0x02, - 0x86, 0x01, 0x84, 0x03, 0x85, 0x07, 0x95, 0x03, 0x91, 0x02, 0x80, 0x0E, - 0x98, 0x04, 0x99, 0x01, 0x89, 0x06, 0x81, 0x03, 0x85, 0x02, 0x84, 0x03, - 0x86, 0x01, 0x82, 0x04, 0x8A, 0x01, 0x88, 0x03, 0x80, 0x05, 0x90, 0x01, - 0x94, 0x05, 0x84, 0x01, 0x80, 0x08, 0x84, 0x05, 0x80, 0x78, 0x81, 0x0D, - 0x80, 0x07, 0x82, 0x06, 0x80, 0x2D, 0x88, 0x08, 0x8A, 0x02, 0x88, 0x0C, - 0x80, 0x02, 0x84, 0x06, 0x85, 0x0F, 0x81, 0x02, 0x89, 0x04, 0x88, 0x07, - 0x8A, 0x0B, 0x82, 0x05, 0x86, 0x03, 0x84, 0x07, 0x85, 0x03, 0x04, -}; - -// T: SPACETAXI 02_N0S.prg (375 bytes) -static const uint8_t kDemoStreamT[375] = { - 0x89, 0x0B, 0x88, 0x03, 0x80, 0x04, 0x86, 0x05, 0x84, 0x07, 0x80, 0x01, - 0x81, 0x0A, 0x80, 0x06, 0x86, 0x02, 0x84, 0x02, 0x80, 0x09, 0x84, 0x04, - 0x80, 0x04, 0x81, 0x01, 0x89, 0x04, 0x81, 0x01, 0x80, 0x0B, 0x84, 0x02, - 0x80, 0x18, 0x81, 0x06, 0x80, 0x09, 0x88, 0x04, 0x80, 0x09, 0x81, 0x04, - 0x80, 0x0C, 0x81, 0x07, 0x80, 0x0E, 0x88, 0x05, 0x80, 0x07, 0x81, 0x05, - 0x85, 0x02, 0x81, 0x05, 0x85, 0x01, 0x84, 0x05, 0x80, 0x04, 0x81, 0x0B, - 0x80, 0x0B, 0x81, 0x0B, 0x80, 0x0A, 0x81, 0x09, 0x80, 0x06, 0x88, 0x0C, - 0x80, 0x0D, 0x84, 0x05, 0x80, 0x0C, 0x81, 0x07, 0x80, 0x05, 0x84, 0x03, - 0x80, 0x0D, 0x85, 0x04, 0x81, 0x0C, 0x80, 0x0C, 0x84, 0x03, 0x80, 0x06, - 0x81, 0x0C, 0x80, 0x0A, 0x81, 0x0A, 0x80, 0x06, 0x81, 0x03, 0x85, 0x04, - 0x81, 0x01, 0x80, 0x07, 0x88, 0x05, 0x8A, 0x04, 0x80, 0x04, 0x88, 0x05, - 0x89, 0x0B, 0x81, 0x06, 0x80, 0x07, 0x82, 0x05, 0x80, 0x03, 0x81, 0x08, - 0x80, 0x02, 0x84, 0x05, 0x86, 0x04, 0x82, 0x01, 0x80, 0x08, 0x85, 0x12, - 0x81, 0x09, 0x89, 0x04, 0x81, 0x03, 0x85, 0x09, 0x81, 0x02, 0x80, 0x13, - 0x81, 0x04, 0x80, 0x0D, 0x81, 0x02, 0x80, 0x06, 0x84, 0x04, 0x80, 0x04, - 0x81, 0x03, 0x80, 0x03, 0x81, 0x03, 0x80, 0x05, 0x82, 0x04, 0x80, 0x07, - 0x81, 0x0B, 0x80, 0x03, 0x86, 0x03, 0x82, 0x01, 0x80, 0x14, 0x81, 0x03, - 0x80, 0x06, 0x81, 0x05, 0x80, 0x0C, 0x82, 0x03, 0x80, 0x04, 0x81, 0x06, - 0x80, 0x07, 0x82, 0x02, 0x80, 0x06, 0x81, 0x04, 0x80, 0x07, 0x88, 0x05, - 0x80, 0x08, 0x88, 0x04, 0x80, 0x06, 0x91, 0x02, 0x95, 0x03, 0x85, 0x01, - 0x84, 0x02, 0x80, 0x04, 0x82, 0x05, 0x80, 0x41, 0x81, 0x05, 0x80, 0x02, - 0x84, 0x06, 0x80, 0x07, 0x82, 0x04, 0x80, 0x05, 0x81, 0x08, 0x80, 0x10, - 0x81, 0x02, 0x80, 0x06, 0x82, 0x01, 0x80, 0x0D, 0x88, 0x07, 0x80, 0x0C, - 0x84, 0x04, 0x80, 0x14, 0x88, 0x03, 0x80, 0x1E, 0x81, 0x0C, 0x89, 0x01, - 0x88, 0x05, 0x80, 0x08, 0x84, 0x05, 0x80, 0x09, 0x88, 0x08, 0x89, 0x0F, - 0x88, 0x03, 0x8A, 0x05, 0x82, 0x01, 0x80, 0x02, 0x81, 0x07, 0x85, 0x0C, - 0x81, 0x01, 0x80, 0x01, 0x88, 0x01, 0x8A, 0x02, 0x82, 0x02, 0x80, 0x03, - 0x84, 0x0C, 0x86, 0x04, 0x84, 0x02, 0x80, 0x17, 0x91, 0x05, 0x80, 0x07, - 0x82, 0x03, 0x80, 0x5D, 0x81, 0x08, 0x80, 0x05, 0x82, 0x05, 0x80, 0x04, - 0x81, 0x01, 0x85, 0x02, 0x84, 0x07, 0x81, 0x04, 0x80, 0x06, 0x88, 0x06, - 0x80, 0x0E, 0x81, 0x06, 0x80, 0x0D, 0x81, 0x0D, 0x84, 0x05, 0x85, 0x0F, - 0x81, 0x06, 0x44, -}; - -// X: SPACETAXI 04_N0S.prg (565 bytes) -static const uint8_t kDemoStreamX[565] = { - 0x84, 0x07, 0x85, 0x06, 0x84, 0x06, 0x80, 0x0A, 0x81, 0x05, 0x80, 0x0C, - 0x88, 0x07, 0x81, 0x05, 0x88, 0x08, 0x81, 0x07, 0x80, 0x07, 0x88, 0x04, - 0x8A, 0x02, 0x88, 0x01, 0x80, 0x0A, 0x81, 0x07, 0x80, 0x08, 0x81, 0x07, - 0x85, 0x01, 0x84, 0x06, 0x80, 0x0E, 0x81, 0x06, 0x80, 0x0C, 0x88, 0x01, - 0x80, 0x0C, 0x81, 0x07, 0x80, 0x0D, 0x88, 0x07, 0x80, 0x01, 0x81, 0x06, - 0x80, 0x06, 0x84, 0x03, 0x80, 0x08, 0x81, 0x02, 0x80, 0x08, 0x81, 0x05, - 0x80, 0x0B, 0x88, 0x04, 0x80, 0x05, 0x91, 0x06, 0x80, 0x44, 0x81, 0x07, - 0x85, 0x01, 0x84, 0x0C, 0x80, 0x0D, 0x81, 0x05, 0x80, 0x0C, 0x81, 0x05, - 0x80, 0x1A, 0x88, 0x0C, 0x80, 0x16, 0x89, 0x06, 0x81, 0x0A, 0x80, 0x07, - 0x81, 0x05, 0x80, 0x0C, 0x81, 0x01, 0x89, 0x02, 0x88, 0x01, 0x89, 0x08, - 0x88, 0x03, 0x8A, 0x07, 0x88, 0x03, 0x80, 0x05, 0x81, 0x09, 0x84, 0x09, - 0x85, 0x02, 0x81, 0x03, 0x80, 0x12, 0x81, 0x05, 0x80, 0x04, 0x84, 0x04, - 0x80, 0x06, 0x81, 0x05, 0x85, 0x01, 0x84, 0x08, 0x86, 0x04, 0x84, 0x03, - 0x94, 0x04, 0x95, 0x01, 0x85, 0x07, 0x81, 0x05, 0x85, 0x02, 0x84, 0x01, - 0x80, 0x02, 0x82, 0x02, 0x8A, 0x07, 0x82, 0x01, 0x80, 0x07, 0x90, 0x01, - 0x91, 0x04, 0x80, 0x0B, 0x81, 0x04, 0x80, 0x44, 0x81, 0x09, 0x89, 0x05, - 0x88, 0x07, 0x8A, 0x05, 0x82, 0x01, 0x80, 0x11, 0x81, 0x03, 0x80, 0x0A, - 0x81, 0x08, 0x80, 0x06, 0x81, 0x04, 0x80, 0x0C, 0x81, 0x05, 0x80, 0x05, - 0x84, 0x03, 0x86, 0x01, 0x82, 0x01, 0x80, 0x07, 0x84, 0x06, 0x80, 0x05, - 0x84, 0x0A, 0x80, 0x08, 0x89, 0x04, 0x81, 0x01, 0x80, 0x06, 0x84, 0x01, - 0x85, 0x06, 0x81, 0x01, 0x80, 0x0B, 0x81, 0x05, 0x80, 0x04, 0x81, 0x07, - 0x80, 0x07, 0x84, 0x03, 0x85, 0x04, 0x84, 0x02, 0x80, 0x06, 0x81, 0x04, - 0x88, 0x02, 0x80, 0x05, 0x88, 0x06, 0x80, 0x08, 0x81, 0x03, 0x80, 0x08, - 0x89, 0x05, 0x88, 0x03, 0x89, 0x01, 0x80, 0x0F, 0x84, 0x04, 0x80, 0x03, - 0x81, 0x02, 0x91, 0x03, 0x80, 0x22, 0x85, 0x0C, 0x81, 0x05, 0x80, 0x02, - 0x82, 0x07, 0x80, 0x02, 0x82, 0x04, 0x80, 0x07, 0x81, 0x03, 0x84, 0x08, - 0x85, 0x05, 0x81, 0x03, 0x80, 0x0E, 0x81, 0x06, 0x88, 0x02, 0x80, 0x05, - 0x88, 0x01, 0x89, 0x08, 0x88, 0x01, 0x80, 0x06, 0x82, 0x06, 0x80, 0x05, - 0x81, 0x05, 0x80, 0x09, 0x81, 0x04, 0x80, 0x06, 0x81, 0x07, 0x80, 0x08, - 0x81, 0x03, 0x80, 0x09, 0x82, 0x02, 0x8A, 0x01, 0x88, 0x02, 0x80, 0x07, - 0x88, 0x09, 0x80, 0x11, 0x89, 0x02, 0x81, 0x02, 0x80, 0x04, 0x81, 0x0A, - 0x80, 0x05, 0x81, 0x01, 0x89, 0x08, 0x81, 0x04, 0x88, 0x01, 0x8A, 0x01, - 0x82, 0x06, 0x80, 0x05, 0x81, 0x08, 0x80, 0x0B, 0x81, 0x05, 0x88, 0x04, - 0x89, 0x03, 0x81, 0x03, 0x80, 0x22, 0x84, 0x06, 0x85, 0x07, 0x84, 0x0A, - 0x85, 0x01, 0x81, 0x03, 0x91, 0x02, 0x90, 0x01, 0x80, 0x01, 0x84, 0x04, - 0x80, 0x85, 0x81, 0x05, 0x85, 0x01, 0x84, 0x06, 0x85, 0x05, 0x84, 0x0B, - 0x80, 0x1B, 0x89, 0x0D, 0x80, 0x08, 0x82, 0x04, 0x80, 0x0D, 0x81, 0x0B, - 0x80, 0x06, 0x88, 0x01, 0x89, 0x09, 0x81, 0x07, 0x84, 0x04, 0x80, 0x02, - 0x81, 0x03, 0x89, 0x05, 0x80, 0x05, 0x82, 0x09, 0x8A, 0x01, 0x88, 0x0D, - 0x8A, 0x03, 0x88, 0x0B, 0x89, 0x08, 0x81, 0x03, 0x85, 0x06, 0x81, 0x01, - 0x80, 0x03, 0x82, 0x05, 0x86, 0x06, 0x84, 0x07, 0x95, 0x07, 0x85, 0x04, - 0x80, 0x36, 0x81, 0x02, 0x85, 0x07, 0x84, 0x0F, 0x80, 0x13, 0x81, 0x04, - 0x89, 0x04, 0x88, 0x03, 0x80, 0x05, 0x81, 0x02, 0x80, 0x14, 0x81, 0x06, - 0x80, 0x08, 0x88, 0x0A, 0x80, 0x04, 0x88, 0x01, 0x89, 0x09, 0x80, 0x06, - 0x84, 0x04, 0x80, 0x0C, 0x84, 0x03, 0x80, 0x02, 0x81, 0x06, 0x80, 0x07, - 0x81, 0x08, 0x89, 0x02, 0x88, 0x05, 0x80, 0x07, 0x84, 0x02, 0x86, 0x05, - 0x80, 0x0D, 0x81, 0x0B, 0x80, 0x08, 0x88, 0x01, 0x89, 0x05, 0x80, 0x04, - 0xFF, -}; - -#define ST_DEMO_BUFFER_BYTES 640u -static const uint8_t kDemoBufferInit[ST_DEMO_BUFFER_BYTES] = { - 0x80, 0x17, 0x88, 0x0A, 0x80, 0x0A, 0x84, 0x04, 0x85, 0x03, 0x81, 0x03, - 0x80, 0x0A, 0x88, 0x04, 0x80, 0x01, 0x81, 0x0E, 0x80, 0x0D, 0x82, 0x02, - 0x80, 0x0A, 0x81, 0x09, 0x85, 0x07, 0x84, 0x05, 0x80, 0x02, 0x88, 0x05, - 0x8A, 0x04, 0x82, 0x03, 0x80, 0x05, 0x85, 0x10, 0x81, 0x08, 0x89, 0x02, - 0x88, 0x0E, 0x8A, 0x03, 0x88, 0x06, 0x81, 0x07, 0x80, 0x12, 0x82, 0x05, - 0x80, 0x04, 0x81, 0x11, 0x80, 0x04, 0x82, 0x04, 0x86, 0x09, 0x84, 0x02, - 0x95, 0x07, 0x85, 0x04, 0x81, 0x01, 0x80, 0x43, 0x81, 0x0A, 0x84, 0x05, - 0x86, 0x03, 0x82, 0x06, 0x80, 0x07, 0x81, 0x09, 0x80, 0x04, 0x88, 0x0B, - 0x80, 0x09, 0x84, 0x04, 0x80, 0x09, 0x81, 0x01, 0x89, 0x04, 0x80, 0x08, - 0x85, 0x04, 0x81, 0x01, 0x80, 0x05, 0x81, 0x15, 0x80, 0x0C, 0x82, 0x02, - 0x86, 0x07, 0x80, 0x05, 0x81, 0x09, 0x80, 0x13, 0x84, 0x01, 0x85, 0x03, - 0x80, 0x06, 0x88, 0x0B, 0x80, 0x16, 0x81, 0x0A, 0x80, 0x01, 0x88, 0x04, - 0x80, 0x0C, 0x81, 0x03, 0x85, 0x02, 0x84, 0x03, 0x85, 0x0C, 0x81, 0x01, - 0x80, 0x0D, 0x82, 0x07, 0x80, 0x06, 0x81, 0x02, 0x89, 0x0C, 0x88, 0x02, - 0x80, 0x05, 0x82, 0x04, 0x80, 0x0A, 0x81, 0x04, 0x85, 0x01, 0x84, 0x04, - 0x80, 0x0D, 0x81, 0x05, 0x89, 0x0E, 0x81, 0x01, 0x80, 0x11, 0x84, 0x17, - 0x80, 0x05, 0x81, 0x04, 0x80, 0x19, 0x81, 0x03, 0x89, 0x01, 0x88, 0x0E, - 0x80, 0x07, 0x84, 0x05, 0x80, 0x0C, 0x81, 0x0D, 0x80, 0x19, 0x88, 0x01, - 0x80, 0x02, 0x88, 0x04, 0x80, 0x1B, 0x88, 0x0A, 0x89, 0x0B, 0x81, 0x14, - 0x80, 0x02, 0x82, 0x03, 0x80, 0x05, 0x84, 0x03, 0x85, 0x09, 0x84, 0x05, - 0x85, 0x05, 0x84, 0x02, 0x86, 0x08, 0x84, 0x01, 0x80, 0x01, 0x81, 0x03, - 0x89, 0x12, 0x81, 0x08, 0x89, 0x01, 0x88, 0x07, 0x8A, 0x04, 0x82, 0x0F, - 0x80, 0x07, 0x81, 0x04, 0x80, 0x03, 0x82, 0x03, 0x86, 0x09, 0x84, 0x08, - 0x81, 0x03, 0x89, 0x06, 0x80, 0x01, 0x86, 0x08, 0x82, 0x01, 0x80, 0x03, - 0x88, 0x03, 0x80, 0x04, 0x95, 0x07, 0x85, 0x0A, 0x80, 0xA9, 0x01, 0x85, - 0xF8, 0x60, 0xA5, 0xF9, 0x38, 0xE5, 0xF8, 0x85, 0xF9, 0x30, 0x01, 0x60, - 0xE9, 0xF7, 0x85, 0xF9, 0xA2, 0x00, 0xBD, 0x81, 0x06, 0x9D, 0x80, 0x06, - 0xE8, 0xE0, 0x26, 0xD0, 0xF5, 0xAD, 0x00, 0x0B, 0xD0, 0x06, 0x20, 0x21, - 0x0A, 0x4C, 0x4B, 0x0A, 0xC9, 0x80, 0x90, 0x06, 0x29, 0x07, 0x85, 0xF8, - 0xA9, 0x20, 0x29, 0x7F, 0x8D, 0xA6, 0x06, 0xEE, 0x4C, 0x0A, 0xD0, 0x03, - 0xEE, 0x4D, 0x0A, 0x60, 0x00, 0x80, 0x6F, 0xCF, 0x00, 0x04, 0x20, 0x21, - 0x22, 0x0F, 0x6F, 0x7F, 0x9F, 0x7F, 0xCF, 0x7F, 0x18, 0xE0, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B, 0x10, 0x00, 0x00, 0x0F, - 0xC8, 0x07, 0x16, 0x0F, 0xF0, 0x00, 0x07, 0x07, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x0D, 0x05, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, - 0x00, 0xFF, 0x00, 0x88, 0x88, 0x41, 0xF1, 0xA9, 0xFF, 0x02, 0x81, 0x87, - 0x41, 0xF0, 0xAA, 0x00, 0x03, 0x00, 0x00, 0x15, 0xF1, 0x69, 0x00, 0x11, - 0xF3, 0x1F, 0x01, 0xC6, 0x40, 0x01, 0x29, 0x40, 0x01, 0xCF, 0x40, 0x01, - 0x09, 0x40, 0x01, 0x09, 0x70, 0x01, 0x2E, 0x73, 0x01, 0xA4, 0x84, 0x01, - 0xE4, 0x64, 0x01, 0x64, 0x14, 0x01, 0x24, 0xE3, 0x09, 0x09, 0x02, 0x08, - 0x07, 0x01, 0x01, 0x07, 0x08, 0x02, 0x09, 0x09, 0x00, 0x00, 0x0B, 0x0B, - 0x05, 0x0F, 0x0D, 0x01, 0x01, 0x0D, 0x0F, 0x05, 0x0B, 0x0B, 0x00, 0x80, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x82, 0x23, 0x31, 0x37, 0x36, - 0x20, 0x2D, 0x20, 0x20, 0x53, 0x50, 0x41, 0x43, 0x45, 0x20, 0x54, 0x41, - 0x58, 0x49, 0x20, 0x2B, 0x39, 0x44, 0x48, 0x20, 0x28, 0x43, 0x29, 0x20, - 0x31, 0x39, 0x38, 0x34, 0x20, 0x4D, 0x55, 0x53, 0x45, 0x20, 0x53, 0x4F, - 0x46, 0x54, 0x57, 0x41, 0x52, 0x45, 0x2E, 0x20, 0x20, 0x43, 0x12, 0x01, - 0x03, 0x0B, 0x05, 0x04, 0x2C, 0x20, 0x4F, 0x0E, 0x05, 0x06, 0x09, 0x0C, - 0x05, 0x04, 0x20, 0x26, 0x20, 0x54, 0x12, 0x01, 0x09, 0x0E, 0x05, 0x04, - 0x20, 0x02, 0x19, 0x20, 0x46, 0x15, 0x0E, 0x07, 0x15, 0x13, 0x20, 0x26, - 0x20, 0x36, 0x52, 0x36, 0x2E, 0x20, 0x20, 0x20, 0x20, 0x44, 0x09, 0x13, - 0x0B, 0x20, 0x0F, 0x12, 0x09, 0x07, 0x09, 0x0E, 0x01, 0x0C, 0x20, 0x01, - 0x0E, 0x04, 0x20, 0x04, 0x0F, 0x03, 0x15, 0x0D, 0x05, 0x0E, 0x14, 0x13, - 0x20, 0x13, 0x15, 0x10, -}; - -typedef struct { - const uint8_t *stream; - uint16_t length; - uint8_t levelIndex; // 0-based -} StDemoEntryT; - -static const StDemoEntryT kDemoRotation[4] = { - { kDemoStreamH, 297u, 7u }, - { kDemoStreamW, 335u, 22u }, - { kDemoStreamT, 375u, 19u }, - { kDemoStreamX, 565u, 23u }, -}; - -#endif diff --git a/examples/spacetaxi/stFare.c b/examples/spacetaxi/stFare.c deleted file mode 100644 index 508f2c1..0000000 --- a/examples/spacetaxi/stFare.c +++ /dev/null @@ -1,424 +0,0 @@ -// Space Taxi -- the passenger / fare state machine ($65C1 dispatch on -// $7163, the $67E3 post-tick gate and the pad reservation table). -// -// One passenger at a time: they beam in on a free pad, wave, walk to a -// cab parked on their pad, shrink into it, ride, and reverse the whole -// sequence at the destination. Every step runs on the shared 3-tick -// decay timer. Pads get reserved as fares use them; once every fare -// pad has been used the destination becomes "UP PLEASE" and the top -// hatch opens. - -#include "stSim.h" - -// Screen-code strings ($6C1E..$6C7D). All ASCII-compatible glyphs. -static const uint8_t kTextPadPlease[] = "PAD PLEASE"; // $6C1E -static const uint8_t kTextUpPlease[] = " UP PLEASE! "; // $6C2B -static const uint8_t kTextHeyTaxi[] = " HEY, TAXI! "; // $6C45 -static const uint8_t kTextThanks[] = " THANKS "; // $6C5F -// Speech cue strings fed to the sample player one character at a time. -static const uint8_t kSpeechHeyTaxi[] = "HT"; // $6C6C -static const uint8_t kSpeechUp[] = "U?"; // $6C6F -static const uint8_t kSpeechPad[] = "PX?"; // $6C72 (X = digit) -static const uint8_t kSpeechThanks[] = "!"; // $6C76 -static const uint8_t kSpeechHey[] = "H"; // $6C78 -// Waiting wave cycle ($66D6) and the base-fare blob ($43B9 = +5.00). -static const uint8_t kWaveCels[4] = { 0xC6, 0xC7, 0xD9, 0xC7 }; -static const uint8_t kFareBlob[7] = { 0x66, 0x66, 0x66, 0x6F, 0x77, 0x74, 0x74 }; - - -static void beamIn(StSimT *sim); -static bool decayStep(StSimT *sim); -static void idleTicker(StSimT *sim); -static void lightOnAndLatch(StSimT *sim); -static void newFare(StSimT *sim); -static void stageBoard(StSimT *sim); -static void stageCleanup(StSimT *sim); -static void stageDropIn(StSimT *sim); -static void walkToTarget(StSimT *sim); - - -// $665F -- the passenger grows in ($CB down to $C7); a crash during -// it cancels the fare. -static void beamIn(StSimT *sim) { - if (!decayStep(sim)) { - return; - } - sim->spr[1].ptr--; - if (sim->spr[1].ptr != ST_SPRITE_PASS_STAND) { - return; - } - if (sim->collisionPhase != 0u) { - sim->stage = ST_STAGE_LEAVE; - sim->spr[1].ptr = ST_SPRITE_PASS_STAND; - sim->spriteSlots[sim->activeSpriteIdx] = 0u; - sim->activeSpriteIdx = 0u; - sim->fareSlotCount--; - return; - } - sim->stage++; - stSimDrawText(sim, 14u, 24u, kTextHeyTaxi, 1u); - stFareBoardingSound(sim, kSpeechHeyTaxi); - // $43D1: the meter starts at $10.00..$19.00. - stSimHudInit(sim); - stSimPutChar(sim, 35u, 24u, (uint8_t)(ST_CHAR_DIGIT_BASE + stSimRng(sim, 10u))); - stSimPutChar(sim, 34u, 24u, (uint8_t)(ST_CHAR_DIGIT_BASE + 1u)); -} - - -// The shared 3-tick cadence: true on the tick a step is due. -static bool decayStep(StSimT *sim) { - sim->decayTimer--; - if (sim->decayTimer != 0u) { - return false; - } - sim->decayTimer = sim->decayReload; - return true; -} - - -// $660B -- a new fare turns up on a 3% roll per tick or when the -// 100-tick timer runs out, never mid-crash. -static void idleTicker(StSimT *sim) { - if (stSimRng(sim, 100u) >= 3u) { - sim->stage0Rng--; - if (sim->stage0Rng != 0u) { - return; - } - } - if (sim->collisionPhase != 0u) { - return; - } - newFare(sim); -} - - -// $6808 -- the pad lights up and stays "busy". -static void lightOnAndLatch(StSimT *sim) { - sim->deathInProgress = 1u; - stSimPadLight(sim, true); -} - - -// $6620 -- pick a free pad, stand the passenger there, start beaming. -static void newFare(StSimT *sim) { - uint8_t x; - const StPadT *pad; - - sim->stage0Rng = 0x64u; - x = stFareChooseDestination(sim); - // $6628: X-1 selects the slot; a garbage X (all pads used) reads - // past the table on the C64 -- clamp to the last pad here. - if (x == 0u || x > ST_MAX_PADS) { - x = ST_MAX_PADS; - } - pad = &sim->pads[x - 1u]; - sim->spr[1].msb = pad->passMsb; - sim->spr[1].col = pad->passCol; - sim->spr[1].row = pad->row; - sim->spr[1].enable = 1u; - sim->stage = ST_STAGE_BEAM_IN; - sim->decayTimer = sim->decayReload; - sim->spr[1].ptr = ST_SPRITE_PASS_BEAM; - stAudioSpeech((uint8_t)(stSimRng(sim, 3u) + 2u)); -} - - -// $66DD -- the passenger shrinks into the cab, then picks where to go. -static void stageBoard(StSimT *sim) { - if (!decayStep(sim)) { - return; - } - sim->spr[1].ptr++; - if (sim->spr[1].ptr != ST_SPRITE_PASS_GONE) { - return; - } - sim->spr[1].enable = 0u; - sim->stage++; - sim->activeDyingSlot = sim->activePad; - (void)stFareChooseDestination(sim); - if (sim->activeSpriteIdx == ST_FARE_DEST_UP) { - return; - } - stSimDrawText(sim, 14u, 24u, kTextPadPlease, 1u); - stSimPutChar(sim, 18u, 24u, (uint8_t)(sim->activeSpriteIdx + 0x30u)); - { - uint8_t speech[4]; - speech[0] = kSpeechPad[0]; - speech[1] = (uint8_t)(sim->activeSpriteIdx + 0x30u); - speech[2] = kSpeechPad[2]; - speech[3] = 0u; - stFareBoardingSound(sim, speech); - } -} - - -// $67C6 -- back to idle. -static void stageCleanup(StSimT *sim) { - sim->deathInProgress = 0u; - sim->stage = ST_STAGE_IDLE; - stSimPadLight(sim, false); - stSimClearMessage(sim); -} - - -// $6742 -- the passenger materialises beside the cab; the fare pays -// out: $5.00 plus whatever is left on the meter (twice the base when -// no pads are reserved). -static void stageDropIn(StSimT *sim) { - if (!decayStep(sim)) { - return; - } - sim->spr[1].ptr--; - if (sim->spr[1].ptr != ST_SPRITE_PASS_STAND) { - return; - } - sim->stage++; - stSimDrawText(sim, 14u, 24u, kTextThanks, 1u); - stFareBoardingSound(sim, kSpeechThanks); - stSimBcdAdd(sim, ST_CELL_SCORE, kFareBlob); - stSimBcdAdd(sim, ST_CELL_SCORE, &sim->screen[ST_CELL_FARE]); - stSimHudInit(sim); - if (sim->fareSlotCount != 0u) { - return; - } - stSimBcdAdd(sim, ST_CELL_SCORE, kFareBlob); -} - - -// $67A6 -- the passenger shrinks away at the stand (also intro stage 4). -void stFareLeaveStep(StSimT *sim) { - if (!decayStep(sim)) { - return; - } - sim->spr[1].ptr++; - if (sim->spr[1].ptr != ST_SPRITE_PASS_GONE) { - return; - } - sim->spr[1].enable = 0u; - sim->stage++; -} - - -// $66B7 -- waving on the pad (also the title's sparkle cycle). -void stFareWaveStep(StSimT *sim) { - if (!decayStep(sim)) { - return; - } - sim->waveIdx = (uint8_t)((sim->waveIdx + 1u) & 3u); - sim->spr[1].ptr = kWaveCels[sim->waveIdx]; -} - - -// $6D0D -- one two-pixel stride toward the hover target, cel -// alternating each step (also the title's walk-in). -void stFareWalkStep(StSimT *sim) { - int16_t passX; - int16_t targetX; - - sim->walkParity ^= 1u; - passX = (int16_t)stSimSpriteX(sim, 1u); - targetX = (int16_t)(((int16_t)sim->hoverXFrac << 8) | sim->hoverXCol); - if ((int16_t)(passX - targetX) < 0) { - // $6D30: walk right, cels $C2/$C3. - sim->spr[1].ptr = (uint8_t)(ST_SPRITE_PASS_WALK_R + sim->walkParity); - stSimSpriteAddX(sim, 1u, 2u); - return; - } - // $6D4B: walk left, cels $C4/$C5. - sim->spr[1].ptr = (uint8_t)(ST_SPRITE_PASS_WALK_L + sim->walkParity); - stSimSpriteAddX(sim, 1u, 0xFEu); -} - - -// $6CE8 -- stages 3 and 7: walk until the column matches (even -// pixels), then stand and advance. -static void walkToTarget(StSimT *sim) { - if (!decayStep(sim)) { - return; - } - if ((sim->spr[1].col & 0xFEu) == (sim->hoverXCol & 0xFEu)) { - sim->spr[1].ptr = ST_SPRITE_PASS_STAND; - sim->stage++; - return; - } - stFareWalkStep(sim); -} - - -// --------------------------------------------------------------------------- -// Public -// --------------------------------------------------------------------------- - -// $6C7F -- the passenger speaks: one sample per character. -void stFareBoardingSound(StSimT *sim, const uint8_t *speech) { - (void)sim; - stAudioSilence(); - stAudioNoise(false); - stAudioSpeechPhrase(speech); -} - - -// $6537 -- reserve a free fare pad by RNG (linear probe on collision), -// or "UP PLEASE" once every fare pad is taken. Returns the pad number. -uint8_t stFareChooseDestination(StSimT *sim) { - uint8_t special = sim->specialPad; - uint8_t x; - - if (sim->fareSlotCount == special) { - stFareUpPlease(sim); - return special; - } - x = stSimRng(sim, special); - while (sim->spriteSlots[x] != 0u) { - if (x == special) { - x = 1u; - } else { - x++; - } - } - sim->fareSlotCount++; - sim->activeSpriteIdx = x; - sim->spriteSlots[x] = 1u; - return x; -} - - -// $6EF0 -- while the passenger walks (to or from the cab) the pad stays -// lit only while the two sprites touch. -void stFarePadLightingGate(StSimT *sim) { - if (sim->stage != ST_STAGE_WALK_TO_CAB && sim->stage != ST_STAGE_WALK_TO_PAD && sim->stage != ST_STAGE_LEAVE) { - return; - } - if (sim->activePad == 0u) { - return; - } - sim->deathInProgress = (uint8_t)(sim->spriteSpriteColl & 2u); - stSimPadLight(sim, sim->deathInProgress != 0u); -} - - -// $67E3 -- landed on the fare's pad: hand the passenger over. -void stFarePostTickGate(StSimT *sim) { - if (sim->activePad == 0u) { - return; - } - if (sim->activePad != sim->activeSpriteIdx) { - return; - } - switch (sim->stage) { - case ST_STAGE_IDLE: - case ST_STAGE_BEAM_IN: - case ST_STAGE_WALK_TO_CAB: - case ST_STAGE_WALK_TO_PAD: - return; - case ST_STAGE_WAIT: - // $6811: remember where the cab parked. - sim->stage++; - sim->hoverXFrac = sim->spr[0].msb; - sim->hoverXCol = sim->spr[0].col; - lightOnAndLatch(sim); - return; - case ST_STAGE_RIDING: { - const StPadT *pad; - if (sim->bobTimer != 0u) { - return; - } - sim->stage++; - sim->spr[1].enable = 1u; - sim->spr[1].msb = sim->spr[0].msb; - sim->spr[1].col = sim->spr[0].col; - sim->spr[1].row = sim->spr[0].row; - sim->spr[1].ptr = ST_SPRITE_PASS_BEAM; - sim->decayTimer = sim->decayReload; - pad = &sim->pads[sim->activeSpriteIdx - 1u]; - sim->hoverXFrac = pad->passMsb; - sim->hoverXCol = pad->passCol; - lightOnAndLatch(sim); - return; - } - default: - lightOnAndLatch(sim); - return; - } -} - - -// $69B4 -- the cab touched the passenger: he vanishes, the fare is -// lost and the meter is docked ten dollars. -void stFareSquashed(StSimT *sim) { - if (sim->stage >= ST_STAGE_LEAVE) { - return; - } - if (sim->stage < ST_STAGE_DROP_IN) { - sim->spriteSlots[sim->activeSpriteIdx] = 0u; - sim->activeSpriteIdx = 0u; - sim->fareSlotCount--; - stSimClearMessage(sim); - } - sim->stage = ST_STAGE_LEAVE; - stFareBoardingSound(sim, kSpeechHey); - // $43F3: a hundred dime decrements. - { - uint8_t k; - for (k = 0u; k < 100u; k++) { - (void)stSimDecrementNumber(sim, ST_CELL_SCORE, 5u); - } - // $43F3 counts down in the input-mask byte and leaves it zero. - sim->inputMask = 0u; - } - sim->spr[1].ptr = ST_SPRITE_PASS_STAND; - stSimHudInit(sim); -} - - -// $65C1 -- run the current stage. -void stFareStageDispatch(StSimT *sim) { - switch (sim->stage) { - case ST_STAGE_IDLE: - idleTicker(sim); - break; - case ST_STAGE_BEAM_IN: - beamIn(sim); - break; - case ST_STAGE_WAIT: - stFareWaveStep(sim); - break; - case ST_STAGE_WALK_TO_CAB: - case ST_STAGE_WALK_TO_PAD: - walkToTarget(sim); - break; - case ST_STAGE_BOARD: - stageBoard(sim); - break; - case ST_STAGE_RIDING: - sim->deathInProgress = 0u; - stSimPadLight(sim, false); - break; - case ST_STAGE_DROP_IN: - stageDropIn(sim); - break; - case ST_STAGE_LEAVE: - stFareLeaveStep(sim); - break; - case ST_STAGE_CLEANUP: - stageCleanup(sim); - break; - default: - sim->stage = ST_STAGE_IDLE; - break; - } -} - - -// $6CB8 -- no fare pad left: the passenger wants to leave the screen, -// so the top hatch opens. -void stFareUpPlease(StSimT *sim) { - uint8_t k; - - sim->activeSpriteIdx = ST_FARE_DEST_UP; - stSimDrawText(sim, 14u, 24u, kTextUpPlease, 1u); - for (k = 0u; k < 4u; k++) { - stSimPutChar(sim, (uint8_t)(18u + k), 0u, ST_CHAR_SPACE); - } - stFareBoardingSound(sim, kSpeechUp); -} diff --git a/examples/spacetaxi/stHookTables.h b/examples/spacetaxi/stHookTables.h deleted file mode 100644 index 3b203cd..0000000 --- a/examples/spacetaxi/stHookTables.h +++ /dev/null @@ -1,85 +0,0 @@ -// Space Taxi -- where each level's hook program keeps its tables. -// -// Every address is a byte in the level's shipped hook blob ($7D98..$7FFF, -// StLevelT.hookData), so the same numbers serve the hook programs -// (stHooks.c, reading through the live simulation) and the cel manifest -// (stLevelCels.c, reading the level file alone -- the offline cel baker -// has no simulation). One list, two readers. -#ifndef ST_HOOK_TABLES_H -#define ST_HOOK_TABLES_H - -// Level indices (0 = A). -#define ST_LEVEL_E 4u -#define ST_LEVEL_G 6u -#define ST_LEVEL_H 7u -#define ST_LEVEL_I 8u -#define ST_LEVEL_J 9u -#define ST_LEVEL_K 10u -#define ST_LEVEL_L 11u -#define ST_LEVEL_O 14u -#define ST_LEVEL_P 15u -#define ST_LEVEL_Q 16u -#define ST_LEVEL_R 17u -#define ST_LEVEL_S 18u -#define ST_LEVEL_T 19u -#define ST_LEVEL_U 20u -#define ST_LEVEL_V 21u -#define ST_LEVEL_W 22u -#define ST_LEVEL_X 23u - -// Hardware sprites 3..7 are the hook sprites on every level that has any. -#define ST_HOOK_FIRST_SPRITE 3u - -// ---- level G "TELEPORTS" ---- -#define ST_HK_G_CEL 0x7DD4u // 6 cels the orbs cycle through -#define ST_HK_G_CEL_PHASES 6u -#define ST_HK_G_ORB_COL 0x7DDAu // per sprite: start column, msb, row -#define ST_HK_G_ORB_MSB 0x7DE2u -#define ST_HK_G_ORB_ROW 0x7DEAu -#define ST_HK_G_SPOT_COL 0x7EF3u // 5 landing spots: column, msb, row -#define ST_HK_G_SPOT_MSB 0x7EF8u -#define ST_HK_G_SPOT_ROW 0x7EFDu -#define ST_HK_G_SFX_HOP 0x7F03u -#define ST_HK_G_ORB_COLOR 4u - -// ---- level H "PUZZLER" ---- -#define ST_HK_H_CEL 0x80u // the one sprite cel -#define ST_HK_H_COLOR_IDLE 0x01u -#define ST_HK_H_COLOR_LIT 0x0Au -#define ST_HK_H_COLOR_ACTIVE 0x05u - -// ---- level I "CROSSFIRE" ---- -#define ST_HK_I_CEL_RISE 0x7DD8u // 5 cels -#define ST_HK_I_RISE_PHASES 5u -#define ST_HK_I_CEL_BURST 0x7DE5u // 8 cels -#define ST_HK_I_BURST_PHASES 8u -#define ST_HK_I_COLOR 0x7E7Fu // 3 colours -#define ST_HK_I_COLORS 3u -#define ST_HK_I_COLOR_IDLE 0x02u - -// ---- levels J "SHOOTING STARS" and P "BLIZZARD" (the falling hazards) ---- -#define ST_HK_FALL_CEL 0x80u // falling: $80/$81 -#define ST_HK_FALL_CEL_BURST 0x82u // then the burst run from the table -#define ST_HK_J_CEL_BURST 0x7EC9u // phases 1..9 -#define ST_HK_J_BURST_PHASES 10u -#define ST_HK_J_COLOR 7u -#define ST_HK_P_CEL_BURST 0x7F09u // phases 1..4 -#define ST_HK_P_BURST_PHASES 5u -#define ST_HK_P_COLOR 3u - -// ---- level L "BLACK HOLE" ---- -#define ST_HK_L_CEL_FIRST 0x80u // $80..$82 spin -#define ST_HK_L_CEL_END 0x83u -#define ST_HK_L_COLOR 0u - -// ---- level Q "INTERFERENCE" ---- -#define ST_HK_Q_CEL 0x7DF2u // 12 cels, multicolour -#define ST_HK_Q_CEL_PHASES 12u -#define ST_HK_Q_COLOR 6u - -// ---- level U "REBOUND" ---- -#define ST_HK_U_CEL 0x7F4Bu // 4-entry walk over $80..$82 -#define ST_HK_U_CEL_PHASES 4u -#define ST_HK_U_COLOR 7u - -#endif // ST_HOOK_TABLES_H diff --git a/examples/spacetaxi/stHooks.c b/examples/spacetaxi/stHooks.c deleted file mode 100644 index ce3768d..0000000 --- a/examples/spacetaxi/stHooks.c +++ /dev/null @@ -1,1778 +0,0 @@ -// Space Taxi -- per-level hook programs. -// -// Every level's data blob ends with six trampolines ($7D66..$7D77) -// and a small program at $7D98..$7FFF that the main loop calls at -// fixed points: two prelude hooks, two per-tick hooks (before and -// after the sprite flush), an input filter and the sprite-contact -// verdict. Those programs are the level gimmicks -- the puzzle -// switches, lasers, moving pads, trap doors. The port keeps the -// original bytes in the level blob and re-expresses each program here, -// naming the tables it reads at the addresses the original used, so -// nothing has to be transcribed by hand. Only the bytes a hook WRITES -// are held in the port's own typed scratch (StHookScratchT). - -#include - -#include "stSim.h" -#include "stHookTables.h" - -// Keeping the two per-tick dispatch halves out of line is what lets the -// 65816 build use the default register allocator. Inlined back together -// they are one twelve-way switch whose live values exceed what greedy -// allocation can place on a single-accumulator machine, and the build -// then falls back to the basic allocator for this entire file. The call -// costs one JSL per game tick. -#if defined(__GNUC__) || defined(__clang__) - #define ST_NOINLINE __attribute__((noinline)) -#else - #define ST_NOINLINE -#endif - -// A level's read-only tables, straight out of the shipped level blob. -// Nothing writes these, so there is no working copy and no second source -// of truth: the .dat is the only place the values live. Every byte a -// hook WRITES lives in the typed per-level scratch (StHookScratchT), so -// the 616-byte C64-addressed working copy is gone. -#define HKT(sim, addr) ((sim)->level->hookData[(addr) - ST_HOOK_BASE]) -#define HKTP(sim, addr) (&(sim)->level->hookData[(addr) - ST_HOOK_BASE]) - -// ---- level H "PUZZLER" ($7E15/$7E4C/$7E53..$7E56 scratch) ---- -// Five parallel 11-entry segment tables sit back to back in the blob, -// then the pad and switch segment lists (four ids each, $80 = end). -#define H_SPR_COL(sim, k) HKT(sim, 0x7DCF + (k)) -#define H_SPR_MSB(sim, k) HKT(sim, 0x7DD7 + (k)) -#define H_SPR_ROW(sim, k) HKT(sim, 0x7DDF + (k)) -#define H_SEG_COL(sim, id) HKT(sim, 0x7DE7 + (id)) -#define H_SEG_ROW(sim, id) HKT(sim, 0x7DF2 + (id)) -#define H_SEG_SENSE(sim, id) HKT(sim, 0x7DFD + (id)) -#define H_SEG_GLYPH(sim, id) HKT(sim, 0x7E08 + (id)) -#define H_PAD_SEGS(sim, i) HKT(sim, 0x7E24 + (i)) -#define H_SFX_CHIME(sim) HKTP(sim, 0x7ED0) -// These two index PAST the table they name and must see live scratch, so -// they go through the byte-addressed view below -- see hookHByte. -#define H_SEG_FLAG(sim, id) hookHByte(sim, (uint16_t)(0x7E15u + (id))) -#define H_SWITCH_SEGS(sim, i) hookHByte(sim, (uint16_t)(0x7E38u + (i))) - -// ---- level G "TELEPORTS" ($7DD1..$7DD3/$7E00/$7F02 scratch) ---- -#define G_CEL(sim, ph) HKT(sim, ST_HK_G_CEL + (ph)) -#define G_ORB_COL(sim, x) HKT(sim, ST_HK_G_ORB_COL + (x)) -#define G_ORB_MSB(sim, x) HKT(sim, ST_HK_G_ORB_MSB + (x)) -#define G_ORB_ROW(sim, x) HKT(sim, ST_HK_G_ORB_ROW + (x)) -#define G_SPOT_COL(sim, i) HKT(sim, ST_HK_G_SPOT_COL + (i)) -#define G_SPOT_MSB(sim, i) HKT(sim, ST_HK_G_SPOT_MSB + (i)) -#define G_SPOT_ROW(sim, i) HKT(sim, ST_HK_G_SPOT_ROW + (i)) -#define G_SFX_HOP(sim) HKTP(sim, ST_HK_G_SFX_HOP) - -// ---- level I "CROSSFIRE" ($7DB8/$7DC0/$7DC8/$7DD0 scratch) ---- -#define I_CEL_RISE(sim, ph) HKT(sim, ST_HK_I_CEL_RISE + (ph)) -#define I_START_COL(sim, d) HKT(sim, 0x7DDD + (d)) -#define I_DX(sim, d) HKT(sim, 0x7DE1 + (d)) -#define I_CEL_BURST(sim, ph) HKT(sim, ST_HK_I_CEL_BURST + (ph)) -#define I_COLOR(sim, i) HKT(sim, ST_HK_I_COLOR + (i)) -#define I_SFX_FIRE(sim) HKTP(sim, 0x7E82) - -// ---- level Q "INTERFERENCE" ($7E83 scratch) ---- -#define Q_SPR_COL(sim, x) HKT(sim, 0x7DD2 + (x)) -#define Q_SPR_MSB(sim, x) HKT(sim, 0x7DDA + (x)) -#define Q_SPR_ROW(sim, x) HKT(sim, 0x7DE2 + (x)) -#define Q_INIT_PHASE(sim, x) HKT(sim, 0x7DEA + (x)) -#define Q_CEL(sim, ph) HKT(sim, ST_HK_Q_CEL + (ph)) -#define Q_SCRAMBLE(sim, i) HKT(sim, 0x7E7F + (i)) - -// ---- level T "FAST BREAK" ($7D9C scratch) ---- -#define T_SFX_BOUNCE(sim) HKTP(sim, 0x7E56) -#define T_SFX_OPEN(sim) HKTP(sim, 0x7E5F) -#define T_SFX_SHUT(sim) HKTP(sim, 0x7E68) - -// ---- level U "REBOUND" ($7F4F/$7F57/$7F5F/$7F6D/$7F6E scratch) ---- -#define U_ENTRY(sim, d) HKT(sim, 0x7F3B + (d)) -#define U_ENTRY_MSB(sim, d) HKT(sim, 0x7F3F + (d)) -#define U_DX(sim, d) HKT(sim, 0x7F43 + (d)) -#define U_DY(sim, d) HKT(sim, 0x7F47 + (d)) -#define U_CEL(sim, ph) HKT(sim, ST_HK_U_CEL + (ph)) -#define U_LIFE(sim, d) HKT(sim, 0x7F69 + (d)) -#define U_SFX_BOUNCE(sim) HKTP(sim, 0x7F76) - -// ---- level W "LASERS" ($7DAB/$7DAC/$7DB5/$7DE5 scratch) ---- -#define W_BEAM_GLYPH(sim, i) HKT(sim, 0x7DBD + (i)) -#define W_TOP_ROW(sim, i) HKT(sim, 0x7DC5 + (i)) -#define W_COL(sim, i) HKT(sim, 0x7DCD + (i)) -#define W_START_ROW(sim, i) HKT(sim, 0x7DD5 + (i)) -#define W_ROW_STEP(sim, i) HKT(sim, 0x7DDD + (i)) -#define W_FADE_COLOR(sim, ph) HKT(sim, 0x7DE6 + (ph)) -#define W_END_ROW(sim, i) HKT(sim, 0x7DE9 + (i)) -#define W_SFX_HUM(sim) HKTP(sim, 0x7DA2) - -// ---- level X "MOVING PADS" ($7D9C..$7D9E scratch) ---- -#define X_GLYPH(sim, col) HKT(sim, 0x7D9F + (col)) -#define X_PAD_SLOT(sim, k) HKTP(sim, 0x7FB8 + (k) * 8u) - -// ---- level E "BEANSTALK" ($7D98..$7D9B scratch) ---- -#define E_SFX_GROW(sim) HKTP(sim, 0x7EA0) - -// ---- level J "SHOOTING STARS" ($7ED3/$7EDB scratch) ---- -#define J_SFX_BURST(sim) HKTP(sim, 0x7DF8) - -// ---- level P "BLIZZARD" ($7F0E/$7F16/$7F1F scratch) ---- -#define P_SFX_WIND(sim) HKTP(sim, 0x7E3C) - -// ---- level S "THE SWITCH" ---- -#define S_DIR_MAP(sim, d) HKT(sim, 0x7DAC + (d)) - -// Levels J and P run the same falling-hazard program over hardware -// sprites 3..7 with their own tables: the per-sprite X drift, the -// per-sprite spawn column base and the burst (J) or melt (P) cel run. -typedef struct { - uint16_t dxTable; // blob address, indexed by sprite - uint16_t startTable; // blob address, indexed by sprite - uint16_t celTable; // blob address, indexed by phase 1..phaseEnd-1 - uint8_t phaseEnd; // the phase that ends the burst - bool avoidMiddle; // J rerolls a spawn over the middle band -} StHookFallT; - -static const StHookFallT kFallJ = { 0x7EB9u, 0x7EC1u, ST_HK_J_CEL_BURST, ST_HK_J_BURST_PHASES, true }; -static const StHookFallT kFallP = { 0x7EF9u, 0x7F01u, ST_HK_P_CEL_BURST, ST_HK_P_BURST_PHASES, false }; - -// Each level H switch drives four wall segments ($7E24 stride, $7E38 -// list), animated over four steps. -#define ST_HOOK_H_SEGMENTS 4u -#define ST_HOOK_H_STEPS 4u - -// Level U: hardware sprites 3..7 are the drifting orbs, $80 is the first -// of their three cels, and a bounce holds off for twelve ticks ($7E0E). -#define ST_HOOK_U_COOLDOWN 0x0Cu - -// Level E: pad 1 alone at first; while the fare wants UP a leaf cell -// sprouts every $57 ticks (the count starts at $1E, or $46 on the -// second screen), four to a side from columns 20/19, and the fifth -// step hangs the next pair of pads at the leaf ends, opens them and -// moves the growth four rows up, until pad 9 is out. -#define ST_HOOK_E_GROW_TICKS 0x57u -#define ST_HOOK_E_TICK_START 0x1Eu -#define ST_HOOK_E_TICK_SECOND 0x46u -#define ST_HOOK_E_LEAVES 5u -#define ST_HOOK_E_LAST_PAD 9u -#define ST_HOOK_E_FIRST_ROW 0x13u -#define ST_HOOK_E_ROW_STEP 4u -#define ST_HOOK_E_STALK_COL 20u -#define ST_HOOK_E_PAD_COL_RIGHT 25u -#define ST_HOOK_E_PAD_COL_LEFT 15u -#define ST_HOOK_E_CHAR_LEAF 0xD2u -#define ST_HOOK_E_CHAR_TIP_R 0xD7u -#define ST_HOOK_E_CHAR_TIP_L 0xD5u -#define ST_HOOK_E_COLOR 5u - -// Levels J and P: hardware sprites 3..7 are the falling hazards, cels -// $80/$81 falling and $82 the burst. An idle sprite spawns on rng($3C) -// < 3, on row $17; a scenery hit counts from row $38 down and drops -// the sprite four rows onto it. J rerolls a spawn column in $91..$CC. -#define ST_HOOK_FALL_SPRITES 0xF8u // $D01F bits 3..7 -#define ST_HOOK_FALL_SPAWN_ODDS 0x3Cu -#define ST_HOOK_FALL_SPAWN_HIT 3u -#define ST_HOOK_FALL_TOP_ROW 0x17u -#define ST_HOOK_FALL_HIT_ROW 0x38u -#define ST_HOOK_FALL_HIT_DROP 4u -#define ST_HOOK_FALL_BAND_LO 0x91u -#define ST_HOOK_FALL_BAND_HI 0xCDu - -// Level G: the five orbs (sprites 3..7) cycle a cel every fourth tick; -// the cab touching one hides it for $14 ticks under a falling tone that -// starts at $5A and drops 4 a tick, then lands it on one of five spots: -// the fare's destination pad half the time, else a random spot other -// than the last one used. -#define ST_HOOK_G_HOP_TICKS 0x14u -#define ST_HOOK_G_SWEEP_START 0x5Au -#define ST_HOOK_G_SWEEP_STEP 4u -#define ST_HOOK_G_SPOTS 5u -#define ST_HOOK_G_PAD_SPOTS 6u // a destination of 6+ (UP) lands on spot 5 - -// Level L: hardware sprite 3 is the hole at ($AC, $82), cels $80..$82. -// Gravity pulls the cab toward it: $10 (Y) and $0C (X) less an eighth -// of the distance, X measured in halved sprite units so it fits a -// byte. Touching it returns verdict $FF: a kill with no falling wreck. -#define ST_HOOK_L_HOLE 3u -#define ST_HOOK_L_HOLE_COL 0xACu -#define ST_HOOK_L_HOLE_ROW 0x82u -#define ST_HOOK_L_HOLE_X_HALF 0x5Bu -#define ST_HOOK_L_PULL_Y 0x10u -#define ST_HOOK_L_PULL_X 0x0Cu -#define ST_HOOK_L_VERDICT 0xFFu - -// Level P: the wind turns on rng($78) < 3 per tick, flipping the three -// windsock cells (glyph ^ 6) and the sign of the X gravity. -#define ST_HOOK_P_WIND_ODDS 0x78u -#define ST_HOOK_P_WIND_HIT 3u -#define ST_HOOK_P_SOCK_FLIP 0x06u - -// Level S: FIRE passes, the four direction bits go through a swap table. -#define ST_HOOK_S_FIRE 0x10u -#define ST_HOOK_S_DIRS 0x0Fu - - -static void fillDownChar(StSimT *sim, uint8_t col, uint8_t row, uint8_t ch, uint8_t rows); -static void fillDownColor(StSimT *sim, uint8_t col, uint8_t row, uint8_t color, uint8_t rows); -static uint8_t hookHByte(const StSimT *sim, uint16_t addr); -static void hookHPerTick(StSimT *sim); -static void hookHPrelude0(StSimT *sim); -static void hookHSegment(StSimT *sim, uint8_t id); -static void hookTGate(StSimT *sim, uint8_t ch); -static void hookTPerTick(StSimT *sim); -static void hookTPrelude1(StSimT *sim); -static void hookWLaserUpdate(StSimT *sim); -static void hookWPerTick(StSimT *sim); -static void hookXMovePads(StSimT *sim); -static void hookXPerTick(StSimT *sim); -static void hookXPrelude0(StSimT *sim); -static void hookXShiftRow(StSimT *sim, uint16_t rowCell, uint8_t from, uint8_t to, int8_t dir); -static void setSpriteColors(StSimT *sim, uint8_t color); -static void hookKPerTick(StSimT *sim); -static void hookOPerTick(StSimT *sim); -ST_NOINLINE static void hookPerTickHigh(StSimT *sim); -ST_NOINLINE static void hookPerTickLow(StSimT *sim); -static void hookRMaze(StSimT *sim, uint8_t ch); -static void hookRPerTick(StSimT *sim); -static void hookVPerTick(StSimT *sim); -static void hookVScrollLeft(StSimT *sim, uint8_t row); -static void hookVScrollRight(StSimT *sim, uint8_t row); -static void hookQPrelude(StSimT *sim); -static void hookQPerTick(StSimT *sim); -static uint8_t hookQInput(StSimT *sim, uint8_t input); -static void hookIPrelude(StSimT *sim); -static void hookIPerTick(StSimT *sim); -static void hookGHop(StSimT *sim); -static void hookGPerTick(StSimT *sim); -static void hookGPrelude0(StSimT *sim); -static void hookUBounce(StSimT *sim); -static void hookUOrbs(StSimT *sim); -static void hookUPerTick(StSimT *sim); -static void hookUPrelude0(StSimT *sim); -static void hookEPerTick(StSimT *sim); -static void hookEPrelude0(StSimT *sim); -static void hookEPut(StSimT *sim, uint8_t col, uint8_t row, uint8_t ch); -static void hookETips(StSimT *sim); -static void hookFallBgHits(StSimT *sim, uint8_t *state, uint8_t *phase, const uint8_t *sfx); -static void hookFallTick(StSimT *sim, uint8_t *state, uint8_t *phase, const StHookFallT *d, uint8_t *parity); -static void hookJPerTick(StSimT *sim); -static void hookJPrelude0(StSimT *sim); -static void hookLPerTick(StSimT *sim); -static void hookLPrelude0(StSimT *sim); -static uint16_t hookLPull(uint8_t base, uint8_t dist); -static void hookPFlipSock(StSimT *sim, uint16_t cell); -static void hookPPerTick(StSimT *sim); -static void hookPPrelude0(StSimT *sim); -static uint8_t hookSInput(StSimT *sim, uint8_t input); - - -// $41AD after $401B -- the same character into the `rows` cells below. -static void fillDownChar(StSimT *sim, uint8_t col, uint8_t row, uint8_t ch, uint8_t rows) { - uint8_t k; - - for (k = 1u; k <= rows; k++) { - if ((uint8_t)(row + k) < ST_SCREEN_ROWS) { - stSimPutChar(sim, col, (uint8_t)(row + k), ch); - } - } -} - - -// $41AD after $401E -- the same colour into the `rows` cells below. -static void fillDownColor(StSimT *sim, uint8_t col, uint8_t row, uint8_t color, uint8_t rows) { - uint8_t k; - - for (k = 1u; k <= rows; k++) { - if ((uint8_t)(row + k) < ST_SCREEN_ROWS) { - stSimPutColor(sim, col, (uint8_t)(row + k), color); - } - } -} - - -// Level H's byte-addressed view of its own data block. -// -// Two of its reads index past the end of the table they name and land on -// bytes the level itself writes, so H cannot simply split into "const -// tables" plus "named scratch" the way the other levels do: -// -// * the pad lists ($7E24) end with $FF, and the landing check indexes -// the segment flags with that terminator unguarded -- $7E15 + $FF is -// $7F14, 380 bytes into the blob, holding $30. Non-zero, so the -// "no segment here" case reports a match, with switch id $FF. -// * `active` is therefore 6 on every landing, so the switch-segment -// read is always $7E38 + 20 = $7E4C -- which is where the id was just -// stored. The level writes a value and reads it back as table data. -// ($7E4D..$7E4F ship as $FF, so the $80 guard skips them and only the -// stored id is ever animated. That also bounds every write to ids -// 0..10, so the flags array is never indexed out of range.) -// -// Scratch where the level has written, the shipped blob everywhere else. -static uint8_t hookHByte(const StSimT *sim, uint16_t addr) { - const StHookHT *h = &sim->hookScratch.h; - - if (addr >= 0x7E15u && addr < 0x7E15u + ST_HOOK_H_SEGS) { - return h->segFlag[addr - 0x7E15u]; - } - switch (addr) { - case 0x7E4Cu: - return h->switchId; - case 0x7E53u: - return h->tick; - case 0x7E54u: - return h->active; - case 0x7E55u: - return h->step; - case 0x7E56u: - return h->tone; - default: - break; - } - return HKT(sim, addr); -} - - -// Level H "PUZZLER" per-tick ($7E5A): touching a switch sprite, or -// landing on a pad whose list holds an active segment, animates the -// switch's four wall segments over four 8-tick steps with a rising -// tone, then toggles their flags. -static void hookHPerTick(StSimT *sim) { - if (sim->hookScratch.h.active == 0u) { - if (sim->collisionPhase != 0u) { - return; - } - if (sim->activePad != 0u) { - uint8_t x = (uint8_t)((sim->activePad - 1u) * 4u); - uint8_t n; - bool found = false; - for (n = 0u; n < 4u; n++) { - uint8_t y = H_PAD_SEGS(sim, x + n); - if (H_SEG_FLAG(sim, y) != 0u) { - sim->hookScratch.h.switchId = y; - sim->hookScratch.h.active = 6u; - found = true; - break; - } - } - if (!found) { - return; - } - } else { - uint8_t a = sim->spriteSpriteColl; - uint8_t y = 5u; - while (y != 0u) { - bool hit = (a & 0x80u) != 0u; - a = (uint8_t)(a << 1); - if (hit) { - break; - } - y--; - } - if (y == 0u) { - return; - } - sim->hookScratch.h.active = y; - setSpriteColors(sim, 0x0Au); - sim->spr[2u + y].color = 0x05u; - } - // $7EB2: start the animation. - sim->hookScratch.h.tick = 0u; - sim->hookScratch.h.step = 0xFFu; - sim->hookScratch.h.tone = 0x12u; - stAudioSfx(H_SFX_CHIME(sim)); - } - // $7ED9 - sim->hookScratch.h.tone++; - stAudioVoice2(sim->hookScratch.h.tone, sim->hookScratch.h.tone, 0xFFu); - sim->hookScratch.h.tick++; - if ((sim->hookScratch.h.tick & 7u) != 0u) { - return; - } - sim->hookScratch.h.step++; - { - uint8_t idx = (uint8_t)((sim->hookScratch.h.active - 1u) * ST_HOOK_H_SEGMENTS); - bool last = (sim->hookScratch.h.step == ST_HOOK_H_STEPS); - uint8_t n; - - if (last) { - // $7F7C: finished -- flip the segment flags. - sim->hookScratch.h.active = 0u; - setSpriteColors(sim, 0x01u); - } - for (n = 0u; n < ST_HOOK_H_SEGMENTS; n++) { - uint8_t id = H_SWITCH_SEGS(sim, idx + n); - if ((id & 0x80u) != 0u) { - continue; - } - if (last) { - sim->hookScratch.h.segFlag[id] ^= 1u; - } else { - hookHSegment(sim, id); - } - } - } -} - - -// Level H prelude 0 ($7D9B): clear the segment flags, park the five -// switch sprites (3..7) from the position tables. -static void hookHPrelude0(StSimT *sim) { - uint8_t k; - - for (k = 0u; k < ST_HOOK_H_SEGS; k++) { - sim->hookScratch.h.segFlag[k] = 0u; - } - sim->hookScratch.h.active = 0u; - for (k = 7u; k >= 3u; k--) { - sim->spr[k].color = 1u; - sim->spr[k].enable = 1u; - sim->spr[k].ptr = 0x80u; - sim->spr[k].col = H_SPR_COL(sim, k); - sim->spr[k].msb = H_SPR_MSB(sim, k); - sim->spr[k].row = H_SPR_ROW(sim, k); - } -} - - -// $7F22 -- draw (or erase) one cell of segment `id` for the current -// animation step, growing from whichever end the tables say. -static void hookHSegment(StSimT *sim, uint8_t id) { - uint8_t step; - uint8_t col; - uint8_t row; - uint8_t ch; - - if ((H_SEG_FLAG(sim, id) ^ H_SEG_SENSE(sim, id)) == 0u) { - step = sim->hookScratch.h.step; - } else { - step = (uint8_t)(3u - sim->hookScratch.h.step); - } - if (H_SEG_GLYPH(sim, id) == 0x9Bu) { - row = (uint8_t)(step + H_SEG_ROW(sim, id)); - col = H_SEG_COL(sim, id); - } else { - col = (uint8_t)(step + H_SEG_COL(sim, id)); - row = H_SEG_ROW(sim, id); - } - ch = (H_SEG_FLAG(sim, id) != 0u) ? H_SEG_GLYPH(sim, id) : ST_CHAR_SPACE; - stSimPutChar(sim, col, row, ch); -} - - - -// Level T "FAST BREAK" $7D9D -- the barrier on row 3: `ch` into the -// centre opening (cols 18..21), its complement into the side gaps. -static void hookTGate(StSimT *sim, uint8_t ch) { - uint8_t k; - - for (k = 0u; k < 4u; k++) { - stSimPutChar(sim, (uint8_t)(18u + k), 3u, ch); - } - ch ^= 0x55u; - for (k = 0u; k < 4u; k++) { - stSimPutChar(sim, (uint8_t)(1u + k), 3u, ch); - stSimPutChar(sim, (uint8_t)(35u + k), 3u, ch); - } -} - - -// Level T per-tick ($7DDA): a slow cab up on the right bounces off -// the ceiling; a fast climb into the top rows slams the centre gate -// shut and opens the sides until the cab drops back down. -static void hookTPerTick(StSimT *sim) { - if (sim->hookScratch.t.gateShut != 0u) { - if (sim->spr[0].row >= 0x5Fu) { - hookTPrelude1(sim); - } - return; - } - if (sim->spr[0].row >= 0x4Fu) { - return; - } - if ((uint8_t)((uint16_t)sim->velY >> 8) >= 0xFCu) { - if (sim->spr[0].msb != 0u) { - return; - } - if (sim->spr[0].col < 0x96u) { - return; - } - sim->velY = (int16_t)(-sim->velY); - stAudioSfx(T_SFX_BOUNCE(sim)); - return; - } - if (sim->spr[0].row >= 0x3Fu) { - return; - } - sim->velY = 0; - sim->velX = 0; - hookTGate(sim, 0x75u); - sim->hookScratch.t.gateShut++; - stAudioSfx(T_SFX_SHUT(sim)); -} - - -// Level T prelude 1 ($7DC4): centre open, sides barred. -static void hookTPrelude1(StSimT *sim) { - hookTGate(sim, ST_CHAR_SPACE); - sim->hookScratch.t.gateShut = 0u; - stAudioSfx(T_SFX_OPEN(sim)); -} - - -// Level W "LASERS" $7E46 -- clear the beam glyphs, then step each of -// the eight lasers: idle ones fire on a 2-in-12 roll, extending ones -// grow a cell per pass until their end row, retracting ones fade -// through two colours and vanish. -static void hookWLaserUpdate(StSimT *sim) { - uint8_t i; - uint8_t k; - - for (k = 0u; k < 16u; k++) { - stSimGlyphMut(sim, (uint8_t)(0x92u + (k >> 3)))[k & 7u] = 0u; - } - stSimMarkChar(sim, 0x92u); - stSimMarkChar(sim, 0x93u); - for (i = 0u; i < 8u; i++) { - uint8_t state = sim->hookScratch.w.state[i]; - uint8_t col = W_COL(sim, i); - if (state == 0u) { - if (stSimRng(sim, 12u) >= 3u) { - continue; - } - sim->hookScratch.w.state[i] = 1u; - sim->hookScratch.w.row[i] = W_START_ROW(sim, i); - stSimPutChar(sim, col, sim->hookScratch.w.row[i], W_BEAM_GLYPH(sim, i)); - stSimPutColor(sim, col, sim->hookScratch.w.row[i], 2u); - continue; - } - if (state == 1u) { - stSimPutChar(sim, col, sim->hookScratch.w.row[i], 0x91u); - if (sim->hookScratch.w.row[i] == W_END_ROW(sim, i)) { - sim->hookScratch.w.state[i] = 2u; - sim->hookScratch.w.row[i] = 0u; - state = 2u; - } else { - sim->hookScratch.w.row[i] = (uint8_t)(sim->hookScratch.w.row[i] + W_ROW_STEP(sim, i)); - stSimPutChar(sim, col, sim->hookScratch.w.row[i], W_BEAM_GLYPH(sim, i)); - stSimPutColor(sim, col, sim->hookScratch.w.row[i], 2u); - continue; - } - } - if (state == 2u) { - uint8_t phase; - sim->hookScratch.w.row[i]++; - phase = sim->hookScratch.w.row[i]; - if (phase == 3u) { - sim->hookScratch.w.state[i] = 0u; - stSimPutChar(sim, col, W_TOP_ROW(sim, i), ST_CHAR_SPACE); - fillDownChar(sim, col, W_TOP_ROW(sim, i), ST_CHAR_SPACE, 7u); - } else { - uint8_t color = W_FADE_COLOR(sim, phase); - stSimPutColor(sim, col, W_TOP_ROW(sim, i), color); - fillDownColor(sim, col, W_TOP_ROW(sim, i), color, 7u); - } - continue; - } - sim->hookScratch.w.state[i] = 0u; - } -} - - -// Level W per-tick ($7DF1): a random-pitched hum on voice 2, the beam -// glyph animation on the odd ticks and the laser step on every 8th. -static void hookWPerTick(StSimT *sim) { - for (;;) { - uint8_t c; - if (sim->hookScratch.w.started != 0u) { - stAudioSfx(W_SFX_HUM(sim)); - sim->hookScratch.w.started = 0u; - } - c = (uint8_t)(stSimRng(sim, 0x6Eu) + 6u); - stAudioVoice2(c, c, 0x81u); - sim->hookScratch.w.colorPhase = (uint8_t)((sim->hookScratch.w.colorPhase + 1u) & 7u); - c = sim->hookScratch.w.colorPhase; - if (c == 0u) { - hookWLaserUpdate(sim); - return; - } - // $2C8F + c (char $91 row 7 for c = 0, else char $92 rows 0..6) - // and $2C98 + (8 - c) (char $93 rows 7..1) get a beam bar. - stSimGlyphMut(sim, (uint8_t)(0x91u + ((7u + c) >> 3)))[(7u + c) & 7u] = 0x3Cu; - stSimGlyphMut(sim, 0x93u)[8u - c] = 0x3Cu; - stSimMarkChar(sim, 0x92u); - stSimMarkChar(sim, 0x93u); - if ((c & 1u) == 0u) { - return; - } - } -} - - -// Level X "ON THE MOVE" $7F53 -- shift every pad's X bounds and stand -// column, the waiting passenger, and a parked cab, by the step. -static void hookXMovePads(StSimT *sim) { - uint8_t dir = sim->hookScratch.x.dir; - uint8_t off = 0u; - uint8_t phase = 0u; - - stSimSpriteAddX(sim, 1u, dir); - while (off < 0x48u) { - uint8_t pad = (uint8_t)(off >> 3); - uint8_t field = (uint8_t)(off & 7u); - uint8_t *hi; - uint8_t *lo; - uint16_t v; - if (field == 0u) { - hi = &sim->pads[pad].x1Hi; - lo = &sim->pads[pad].x1Lo; - } else if (field == 2u) { - hi = &sim->pads[pad].x2Hi; - lo = &sim->pads[pad].x2Lo; - } else { - hi = &sim->pads[pad].passMsb; - lo = &sim->pads[pad].passCol; - } - v = (uint16_t)(((uint16_t)*hi << 8) | *lo); - v = (uint16_t)(v + (uint16_t)(int16_t)(int8_t)dir); - *lo = (uint8_t)v; - *hi = (uint8_t)(v >> 8); - // Offsets step +2, +3, +3 repeating: x1, x2, stand X per pad. - if (phase == 0u) { - off = (uint8_t)(off + 2u); - } else { - off = (uint8_t)(off + 3u); - } - phase = (uint8_t)((phase + 1u) % 3u); - } - if (sim->activePad == 0u) { - return; - } - stSimSpriteAddX(sim, 0u, dir); - sim->posXmsb = sim->spr[0].msb; - sim->hoverXFrac = sim->spr[0].msb; - sim->posXcol = sim->spr[0].col; - sim->hoverXCol = sim->spr[0].col; -} - - -// Level X per-tick ($7DE1): every 16 ticks slide the two pad columns -// one cell, bouncing between columns 1 and 21. -static void hookXPerTick(StSimT *sim) { - uint8_t col; - - sim->hookScratch.x.tick++; - if (sim->hookScratch.x.tick != 0x10u) { - return; - } - sim->hookScratch.x.tick = 0u; - col = sim->hookScratch.x.col; - if ((sim->hookScratch.x.dir & 0x80u) == 0u) { - // $7DF9: rightward. - hookXShiftRow(sim, ST_CELL(5, 0), (uint8_t)(col + 17u), col, 1); - hookXShiftRow(sim, ST_CELL(10, 0), (uint8_t)(col + 17u), col, 1); - hookXShiftRow(sim, ST_CELL(15, 0), (uint8_t)(col + 17u), col, 1); - hookXShiftRow(sim, ST_CELL(20, 0), (uint8_t)(col + 17u), col, 1); - stSimPutChar(sim, col, 5u, X_GLYPH(sim, col)); - stSimPutChar(sim, col, 10u, X_GLYPH(sim, col)); - stSimPutChar(sim, col, 15u, X_GLYPH(sim, col)); - stSimPutChar(sim, col, 20u, X_GLYPH(sim, col)); - stSimPutColor(sim, col, 5u, 0x0Cu); - stSimPutColor(sim, col, 10u, 0x0Cu); - stSimPutColor(sim, col, 15u, 0x0Cu); - stSimPutColor(sim, col, 20u, 0x0Cu); - // $7E56: the row-14 cell of the right group moves with it. - stSimPutChar(sim, (uint8_t)(col + 12u), 14u, sim->screen[ST_CELL(14, col + 11u)]); - stSimPutChar(sim, (uint8_t)(col + 11u), 14u, ST_CHAR_SPACE); - stSimPutColor(sim, (uint8_t)(col + 12u), 14u, 7u); - stSimPutChar(sim, (uint8_t)(col + 11u), 5u, X_GLYPH(sim, col + 11u)); - stSimPutChar(sim, (uint8_t)(col + 11u), 10u, X_GLYPH(sim, col + 11u)); - stSimPutChar(sim, (uint8_t)(col + 11u), 15u, X_GLYPH(sim, col + 11u)); - stSimPutChar(sim, (uint8_t)(col + 11u), 20u, X_GLYPH(sim, col + 11u)); - stSimPutColor(sim, (uint8_t)(col + 11u), 5u, 0x0Cu); - stSimPutColor(sim, (uint8_t)(col + 11u), 10u, 0x0Cu); - stSimPutColor(sim, (uint8_t)(col + 11u), 15u, 0x0Cu); - stSimPutColor(sim, (uint8_t)(col + 11u), 20u, 0x0Cu); - hookXMovePads(sim); - sim->hookScratch.x.col++; - if (sim->hookScratch.x.col == 0x15u) { - sim->hookScratch.x.dir = 0xF8u; - } - return; - } - // $7E9E: leftward. - hookXShiftRow(sim, ST_CELL(5, 0), col, (uint8_t)(col + 17u), -1); - hookXShiftRow(sim, ST_CELL(10, 0), col, (uint8_t)(col + 17u), -1); - hookXShiftRow(sim, ST_CELL(15, 0), col, (uint8_t)(col + 17u), -1); - hookXShiftRow(sim, ST_CELL(20, 0), col, (uint8_t)(col + 17u), -1); - stSimPutChar(sim, (uint8_t)(col + 6u), 5u, X_GLYPH(sim, col + 6u)); - stSimPutChar(sim, (uint8_t)(col + 6u), 10u, X_GLYPH(sim, col + 6u)); - stSimPutChar(sim, (uint8_t)(col + 6u), 15u, X_GLYPH(sim, col + 6u)); - stSimPutChar(sim, (uint8_t)(col + 6u), 20u, X_GLYPH(sim, col + 6u)); - stSimPutColor(sim, (uint8_t)(col + 6u), 5u, 0x0Cu); - stSimPutColor(sim, (uint8_t)(col + 6u), 10u, 0x0Cu); - stSimPutColor(sim, (uint8_t)(col + 6u), 15u, 0x0Cu); - stSimPutColor(sim, (uint8_t)(col + 6u), 20u, 0x0Cu); - stSimPutChar(sim, (uint8_t)(col + 10u), 14u, sim->screen[ST_CELL(14, col + 11u)]); - stSimPutChar(sim, (uint8_t)(col + 11u), 14u, ST_CHAR_SPACE); - stSimPutColor(sim, (uint8_t)(col + 10u), 14u, 7u); - stSimPutChar(sim, (uint8_t)(col + 17u), 5u, X_GLYPH(sim, col + 17u)); - stSimPutChar(sim, (uint8_t)(col + 17u), 10u, X_GLYPH(sim, col + 17u)); - stSimPutChar(sim, (uint8_t)(col + 17u), 15u, X_GLYPH(sim, col + 17u)); - stSimPutChar(sim, (uint8_t)(col + 17u), 20u, X_GLYPH(sim, col + 17u)); - stSimPutColor(sim, (uint8_t)(col + 17u), 5u, 0x0Cu); - stSimPutColor(sim, (uint8_t)(col + 17u), 10u, 0x0Cu); - stSimPutColor(sim, (uint8_t)(col + 17u), 15u, 0x0Cu); - stSimPutColor(sim, (uint8_t)(col + 17u), 20u, 0x0Cu); - hookXMovePads(sim); - sim->hookScratch.x.col--; - if (sim->hookScratch.x.col == 1u) { - sim->hookScratch.x.dir = 0x08u; - } -} - - -// Level X prelude 0 ($7DC6): reset the sweep and restore the pad table -// from the level's backup copy ($7FB8). -static void hookXPrelude0(StSimT *sim) { - uint8_t k; - - sim->hookScratch.x.dir = 0xF8u; - sim->hookScratch.x.col = 0x0Bu; - sim->hookScratch.x.tick = 0u; - for (k = 0u; k < 8u; k++) { - const uint8_t *src = X_PAD_SLOT(sim, k); - sim->pads[k].x1Hi = src[0]; - sim->pads[k].x1Lo = src[1]; - sim->pads[k].x2Hi = src[2]; - sim->pads[k].x2Lo = src[3]; - sim->pads[k].row = src[4]; - sim->pads[k].passMsb = src[5]; - sim->pads[k].passCol = src[6]; - sim->pads[k].unused = src[7]; - } -} - - -// Copy cells of one screen row (char + colour) one column along, from -// column `from` to column `to` inclusive, in the direction that keeps -// the copy from overwriting its own source. -static void hookXShiftRow(StSimT *sim, uint16_t rowCell, uint8_t from, uint8_t to, int8_t dir) { - uint8_t x = from; - - for (;;) { - uint16_t src = (uint16_t)(rowCell + x); - uint16_t dst = (uint16_t)(src + (uint16_t)(int16_t)dir); - stSimPutChar(sim, (uint8_t)(dst % ST_SCREEN_COLS), (uint8_t)(dst / ST_SCREEN_COLS), sim->screen[src]); - stSimPutColor(sim, (uint8_t)(dst % ST_SCREEN_COLS), (uint8_t)(dst / ST_SCREEN_COLS), sim->color[src]); - if (x == to) { - break; - } - x = (uint8_t)(x - (uint8_t)dir); - } -} - - -// Sprites 3..7 share one colour in the puzzle level. -static void setSpriteColors(StSimT *sim, uint8_t color) { - uint8_t k; - - for (k = 3u; k < ST_HW_SPRITES; k++) { - sim->spr[k].color = color; - } -} - - -// Extra-level gimmicks (magnets, electroids, maze, shift-o-rama, -// interference, crossfire, teleports, rebound). Compiled out on the -// IIgs, whose bank-0 BSS/text budget is too tight for them; those -// levels play without their hazard there (the 4 attract-demo levels -// H/T/W/X keep their gimmicks everywhere). -// Level K "MAGNETS" per-tick ($7D9D): the level is globally anti-gravity -// (its yGrav template is -7); the hook only animates the six magnet -// poles by flipping bit 0 of their glyphs every other tick. -static void hookKPerTick(StSimT *sim) { - static const uint8_t kPole[6] = { 0x04u, 0x0Bu, 0x10u, 0x17u, 0x1Cu, 0x23u }; - uint8_t k; - - sim->hookScratch.k.parity ^= 1u; - if ((sim->hookScratch.k.parity & 1u) != 0u) { - return; - } - for (k = 0u; k < 6u; k++) { - uint16_t cell = (uint16_t)(0xA0u + kPole[k]); // $04A0 + off - stSimPutChar(sim, (uint8_t)(cell % ST_SCREEN_COLS), (uint8_t)(cell / ST_SCREEN_COLS), (uint8_t)(sim->screen[cell] ^ 1u)); - } -} - - -// Level O "ELECTROIDS" per-tick ($7DBE): four electric barriers (rows -// 5, 14, 22, 26 of screen RAM in cell terms) scroll left one column, -// the leftmost cell wrapping to the right, at a rate gated by a mod-8 -// tick (rows 1-2 every tick, row 3 every 4th). -static void hookOScrollRow(StSimT *sim, uint16_t base) { - uint8_t chSave = sim->screen[base]; - uint8_t colSave = sim->color[base]; - uint8_t x; - uint16_t cell; - - for (x = 1u; x < 0x26u; x++) { - cell = (uint16_t)(base + x); - stSimPutChar(sim, (uint8_t)((base + x - 1u) % ST_SCREEN_COLS), (uint8_t)((base + x - 1u) / ST_SCREEN_COLS), sim->screen[cell]); - stSimPutColor(sim, (uint8_t)((base + x - 1u) % ST_SCREEN_COLS), (uint8_t)((base + x - 1u) / ST_SCREEN_COLS), sim->color[cell]); - } - cell = (uint16_t)(base + 0x25u); - stSimPutChar(sim, (uint8_t)(cell % ST_SCREEN_COLS), (uint8_t)(cell / ST_SCREEN_COLS), chSave); - stSimPutColor(sim, (uint8_t)(cell % ST_SCREEN_COLS), (uint8_t)(cell / ST_SCREEN_COLS), colSave); -} - - -static void hookOPerTick(StSimT *sim) { - stSimRotateGlyph(sim, ST_CHAR_ELECTROID); // $7E6E bitScroll of the barrier glyph - sim->hookScratch.o.tick = (uint8_t)((sim->hookScratch.o.tick + 1u) & 7u); - if ((sim->hookScratch.o.tick & 3u) != 0u) { - return; - } - hookOScrollRow(sim, 0xC9u); // $04C9 row 5 col 1 - hookOScrollRow(sim, 0x209u); // $0609 row 12 col 1 - if (sim->hookScratch.o.tick != 4u) { - return; - } - hookOScrollRow(sim, 0x169u); // $0569 row 9 col 1 - hookOScrollRow(sim, 0x2A9u); // $06A9 row 17 col 1 -} - - -// Level R "TAXI MAZE" wall program ($7D9C): put `ch` in the vertical -// bar cells ($043E/$0466/$048E) and its complement (ch^$46) in the -// gate cells; called with $66 to close, with the gate's own char to -// flip. Toggling on pickup opens a path. -static void hookRMaze(StSimT *sim, uint8_t ch) { - static const uint16_t kBar[3] = { 0x3Eu, 0x66u, 0x8Eu }; - static const uint16_t kGate[10] = { 0x39u, 0x61u, 0x89u, 0xA1u, 0xA2u, 0xA3u, 0xA4u, 0x309u, 0x30Au, 0x30Bu }; - uint8_t k; - uint8_t alt = (uint8_t)(ch ^ 0x46u); - - for (k = 0u; k < 3u; k++) { - stSimPutChar(sim, (uint8_t)(kBar[k] % ST_SCREEN_COLS), (uint8_t)(kBar[k] / ST_SCREEN_COLS), ch); - } - for (k = 0u; k < 10u; k++) { - stSimPutChar(sim, (uint8_t)(kGate[k] % ST_SCREEN_COLS), (uint8_t)(kGate[k] / ST_SCREEN_COLS), alt); - } -} - - -// Level R per-tick ($7DD2): when the passenger boards (stage 4) and the -// gate is open ($0439 == space) the maze flips. -static void hookRPerTick(StSimT *sim) { - if (sim->stage != ST_STAGE_BOARD) { - return; - } - if (sim->screen[0x39u] != ST_CHAR_SPACE) { - return; - } - hookRMaze(sim, sim->screen[0x39u]); -} - - -// Level V "SHIFT-O-RAMA" per-tick ($7D9D): every screen tick, the 12 -// rows 6..17 each scroll one column, alternating direction in bands of -// two (bit 1 of the row index): bands with the bit set go left, the -// others right, the edge cell wrapping around. -static void hookVScrollLeft(StSimT *sim, uint8_t row) { - uint16_t base = ST_CELL(row, 0); - uint8_t chSave = sim->screen[base]; - uint8_t colSave = sim->color[base]; - uint8_t x; - - for (x = 1u; x < 0x27u; x++) { - stSimPutChar(sim, (uint8_t)(x - 1u), row, sim->screen[base + x]); - stSimPutColor(sim, (uint8_t)(x - 1u), row, sim->color[base + x]); - } - stSimPutChar(sim, 0x26u, row, chSave); - stSimPutColor(sim, 0x26u, row, colSave); -} - - -static void hookVScrollRight(StSimT *sim, uint8_t row) { - uint16_t base = ST_CELL(row, 0); - uint8_t chSave = sim->screen[base + 0x26u]; - uint8_t colSave = sim->color[base + 0x26u]; - uint8_t x; - - for (x = 0x26u; x > 0u; x--) { - stSimPutChar(sim, x, row, sim->screen[base + x - 1u]); - stSimPutColor(sim, x, row, sim->color[base + x - 1u]); - } - stSimPutChar(sim, 0u, row, chSave); - stSimPutColor(sim, 0u, row, colSave); -} - - -static void hookVPerTick(StSimT *sim) { - uint8_t row; - - for (row = 6u; row < 0x12u; row++) { - if ((row & 2u) != 0u) { - hookVScrollLeft(sim, row); - } else { - hookVScrollRight(sim, row); - } - } -} - - -// Level Q "INTERFERENCE" ($7DA1 prelude0, $7E0D per-tick2, $7E2E input): -// five interference sprites (hw 3..7) sit at fixed spots flickering -// through cels $80..$8B, and while the cab is mid-screen the joystick -// is scrambled 20% of ticks. -static void hookQPrelude(StSimT *sim) { - int8_t x; - - sim->multiColorMask = 0xFFu; - for (x = 7; x >= 3; x--) { - sim->hookScratch.q.phase[x] = Q_INIT_PHASE(sim, x); - sim->spr[x].ptr = Q_CEL(sim, sim->hookScratch.q.phase[x]); - sim->spr[x].color = 0x06u; - sim->spr[x].col = Q_SPR_COL(sim, x); - sim->spr[x].msb = Q_SPR_MSB(sim, x); - sim->spr[x].row = Q_SPR_ROW(sim, x); - sim->spr[x].enable = 1u; - } -} - - -static void hookQPerTick(StSimT *sim) { - int8_t x; - - for (x = 7; x >= 3; x--) { - sim->hookScratch.q.phase[x]++; - if (sim->hookScratch.q.phase[x] == 0x0Cu) { - sim->hookScratch.q.phase[x] = 0u; - } - sim->spr[x].ptr = Q_CEL(sim, sim->hookScratch.q.phase[x]); - } -} - - -static uint8_t hookQInput(StSimT *sim, uint8_t input) { - if (sim->activePad != 0u) { - return input; - } - if (sim->spr[0].row < 0x5Au || sim->spr[0].row >= 0xB4u) { - return input; - } - if (stSimRng(sim, 10u) < 8u) { - return input; - } - return Q_SCRAMBLE(sim, (uint8_t)(stSimRng(sim, 4u) - 1u)); -} - - -// Level I "CROSSFIRE" ($7D9C prelude0, $7DEF per-tick2, $7DB1 prelude1): -// bullets (hw 3..7) fly up from the floor, cel-walk, then burst. -static void hookIPrelude(StSimT *sim) { - int8_t x; - - for (x = 7; x >= 3; x--) { - sim->hookScratch.i.state[x] = 0u; // state - sim->spr[x].enable = 0u; - sim->spr[x].color = 0x02u; - } -} - - -static void hookIPerTick(StSimT *sim) { - int8_t s; - - for (s = 3; s <= 7; s++) { - uint8_t x = (uint8_t)s; - uint8_t state = sim->hookScratch.i.state[x]; - if (state == 0u) { - uint8_t y; - if (stSimRng(sim, 0x75u) >= 3u) { - continue; - } - y = (uint8_t)(stSimRng(sim, 4u) - 1u); // 0..3 direction - sim->hookScratch.i.dx[x] = I_DX(sim, y); // dx - sim->spr[x].col = I_START_COL(sim, y); // start col - sim->spr[x].msb = 0u; - sim->spr[x].row = 0xD1u; - sim->hookScratch.i.dy[x] = (uint8_t)(stSimRng(sim, 2u) + 0xFDu); // dy = rng(2)-3 - sim->hookScratch.i.phase[x] = 0u; // cel phase - sim->hookScratch.i.state[x] = 1u; - sim->spr[x].enable = 1u; - sim->spr[x].ptr = 0x84u; - stAudioSfx(I_SFX_FIRE(sim)); - sim->spr[x].color = I_COLOR(sim, (uint8_t)(stSimRng(sim, 3u) - 1u)); - } else if (state == 1u) { - uint8_t ph = (uint8_t)(sim->hookScratch.i.phase[x] + 1u); - sim->hookScratch.i.phase[x] = ph; - if (ph == 5u) { - sim->hookScratch.i.phase[x] = 0u; - sim->spr[x].ptr = 0x85u; - sim->hookScratch.i.state[x] = 2u; - } else { - if (ph >= 3u) { - stSimSpriteAddX(sim, x, sim->hookScratch.i.dx[x]); - sim->spr[x].row = (uint8_t)(sim->spr[x].row + sim->hookScratch.i.dy[x]); - } - sim->spr[x].ptr = I_CEL_RISE(sim, ph); - } - } else { - uint8_t ph = (uint8_t)((sim->hookScratch.i.phase[x] + 1u) & 7u); - sim->hookScratch.i.phase[x] = ph; - sim->spr[x].ptr = I_CEL_BURST(sim, ph); - stSimSpriteAddX(sim, x, sim->hookScratch.i.dx[x]); - sim->spr[x].row = (uint8_t)(sim->spr[x].row + sim->hookScratch.i.dy[x]); - if (sim->spr[x].row < 0x25u) { - sim->spr[x].enable = 0u; - sim->hookScratch.i.state[x] = 0u; - } - } - } -} - - -// Level G "TELEPORTS" ($7DA0 prelude0, $7D9A prelude1, $7E01 per-tick2, -// $7DF2 verdict): five orbs on sprites 3..7 sit where the blob puts them -// and cycle their cel every fourth tick. The cab touching one starts a -// hop: it vanishes for $14 ticks with the stick dead and a falling tone, -// then reappears on a landing spot (hookGHop). Touching an orb is never -// a kill -- the verdict is 0 unless the scenery was hit, and a scenery -// hit also puts the cab sprite back. -static void hookGPrelude0(StSimT *sim) { - uint8_t x; - - for (x = ST_HOOK_FIRST_SPRITE; x < ST_HW_SPRITES; x++) { - sim->spr[x].color = ST_HK_G_ORB_COLOR; - sim->spr[x].ptr = HKT(sim, ST_HK_G_CEL); - sim->spr[x].col = G_ORB_COL(sim, x); - sim->spr[x].msb = G_ORB_MSB(sim, x); - sim->spr[x].row = G_ORB_ROW(sim, x); - sim->spr[x].enable = 1u; - } - sim->hookScratch.g.celPhase = 0u; - sim->hookScratch.g.hop = 0u; -} - - -// $7E4A -- land: rng(2) odd with a fare aboard sends the cab to its -// destination pad's spot (UP and beyond = spot 5); otherwise a random -// spot, stepped down if it repeats the last one (wrapping 1 -> 5). -static void hookGHop(StSimT *sim) { - StHookGT *g = &sim->hookScratch.g; - uint8_t spot; - uint8_t i; - - if ((stSimRng(sim, 2u) & 1u) != 0u && sim->activeSpriteIdx != 0u) { - spot = sim->activeSpriteIdx; - if (spot >= ST_HOOK_G_PAD_SPOTS) { - spot = ST_HOOK_G_SPOTS; - } - } else { - spot = stSimRng(sim, ST_HOOK_G_SPOTS); - if (spot == g->lastSpot) { - spot--; - if (spot == 0u) { - spot = ST_HOOK_G_SPOTS; - } - } - } - g->lastSpot = spot; - i = (uint8_t)(spot - 1u); - sim->spr[0].col = G_SPOT_COL(sim, i); - sim->posXcol = sim->spr[0].col; - sim->spr[0].msb = G_SPOT_MSB(sim, i); - sim->posXmsb = sim->spr[0].msb; - sim->spr[0].row = G_SPOT_ROW(sim, i); - sim->posYrow = sim->spr[0].row; - sim->velX = 0; - sim->velY = 0; -} - - -static void hookGPerTick(StSimT *sim) { - StHookGT *g = &sim->hookScratch.g; - uint8_t x; - - g->tick++; - if ((g->tick & 3u) == 0u) { - uint8_t cel; - g->celPhase++; - if (g->celPhase == ST_HK_G_CEL_PHASES) { - g->celPhase = 0u; - } - cel = G_CEL(sim, g->celPhase); - for (x = ST_HOOK_FIRST_SPRITE; x < ST_HW_SPRITES; x++) { - sim->spr[x].ptr = cel; - } - } - if (g->hop != 0u) { - // $7ECB: the tone falls, the stick is dead, and at zero the cab - // is back and lands. - g->sweep = (uint8_t)(g->sweep - ST_HOOK_G_SWEEP_STEP); - stAudioVoice1Freq(g->sweep); - sim->dirMask = 0u; - g->hop--; - if (g->hop == 0u) { - sim->spr[0].enable = 1u; - hookGHop(sim); - } - return; - } - if ((sim->spriteSpriteColl & ST_HOOK_FALL_SPRITES) == 0u) { - return; - } - // $7E3D: an orb took the cab. - sim->spr[0].enable = 0u; - g->hop = ST_HOOK_G_HOP_TICKS; - stAudioSfx(G_SFX_HOP(sim)); - g->sweep = ST_HOOK_G_SWEEP_START; - sim->velX = 0; - sim->velY = 0; -} - - -// Level U "REBOUND" ($7D9B prelude0, $7DAD per-tick2). Five orbs drift -// in from the four edges on their own cel walk; touching one bounces the -// cab by negating its velocity. The orb cels are the level's own sprite -// blocks $80..$82, and every table below is read from the shipped hook -// blob at the address the original indexes. -// -// $7D9B -- clear the orb states and colour the five hazard sprites. -static void hookUPrelude0(StSimT *sim) { - uint8_t x; - - for (x = (uint8_t)(ST_HW_SPRITES - 1u); x >= ST_HOOK_FIRST_SPRITE; x--) { - sim->hookScratch.u.state[x] = 0u; - sim->spr[x].color = 0x07u; - } -} - - -// $7DDD -- a cab/orb contact bounces the cab, then holds off for twelve -// ticks. Which axes flip is a 1-in-3 roll: 1 negates both, 2 negates -// neither and 3 negates Y alone -- the original has no X-only case. -static void hookUBounce(StSimT *sim) { - uint8_t coll; - uint8_t hits = 0u; - uint8_t b; - uint8_t pick; - - if (sim->hookScratch.u.cooldown != 0u) { - sim->hookScratch.u.cooldown--; - return; - } - if ((sim->spriteSpriteColl & 1u) == 0u) { - return; - } - // $7DF8: count the orb sprites in the latch (bits 7..3). - coll = sim->spriteSpriteColl; - for (b = (uint8_t)(ST_HW_SPRITES - 1u); b >= ST_HOOK_FIRST_SPRITE; b--) { - if ((coll & 0x80u) != 0u) { - hits++; - } - coll = (uint8_t)(coll << 1); - } - if (hits == 0u) { - return; - } - sim->hookScratch.u.cooldown = ST_HOOK_U_COOLDOWN; - stAudioSfx(U_SFX_BOUNCE(sim)); - pick = stSimRng(sim, 3u); - if (pick == 1u) { - sim->velX = (int16_t)(-sim->velX); - } - if (pick == 2u) { - return; - } - sim->velY = (int16_t)(-sim->velY); -} - - -// $7E60 -- the orbs themselves: an idle one appears on a 2-in-30 roll -// from one of the four edges (0 up from the floor, 1 down from the -// ceiling, 2 in from the left, 3 in from the right), an active one -// drifts by its direction's step, walks its cel and expires. -static void hookUOrbs(StSimT *sim) { - uint8_t x; - - for (x = (uint8_t)(ST_HW_SPRITES - 1u); x >= ST_HOOK_FIRST_SPRITE; x--) { - uint8_t state = sim->hookScratch.u.state[x]; - uint8_t dir; - - if (state == 0u) { - // $7E84: nothing here yet -- maybe start one. - if (stSimRng(sim, 0x1Eu) >= 3u) { - continue; - } - dir = (uint8_t)(stSimRng(sim, 4u) - 1u); - sim->hookScratch.u.dir[x] = dir; - sim->hookScratch.u.life[x] = U_LIFE(sim, dir); - if ((dir & 2u) == 0u) { - // $7EA8: fixed row, random column. - uint16_t col = (uint16_t)(((uint16_t)stSimRng(sim, 0x90u) + 0x11u) << 1); - sim->spr[x].row = U_ENTRY(sim, dir); - sim->spr[x].col = (uint8_t)col; - sim->spr[x].msb = (uint8_t)(col >> 8); - } else { - // $7ECF: fixed column, random row. - sim->spr[x].col = U_ENTRY(sim, dir); - sim->spr[x].msb = U_ENTRY_MSB(sim, dir); - sim->spr[x].row = (uint8_t)((uint8_t)(stSimRng(sim, 0x4Fu) + 0x1Du) << 1); - } - sim->hookScratch.u.state[x] = 1u; - sim->spr[x].enable = 1u; - sim->spr[x].ptr = ST_HK_FALL_CEL; - sim->hookScratch.u.celPhase[x] = 0u; - continue; - } - if (state != 1u) { - sim->hookScratch.u.state[x] = 0u; - continue; - } - // $7F02: drift until the lifetime runs out. - sim->hookScratch.u.life[x]--; - if (sim->hookScratch.u.life[x] == 0u) { - sim->spr[x].enable = 0u; - sim->hookScratch.u.state[x] = 0u; - continue; - } - dir = sim->hookScratch.u.dir[x]; - stSimSpriteAddX(sim, x, U_DX(sim, dir)); - sim->spr[x].row = (uint8_t)(sim->spr[x].row + U_DY(sim, dir)); - sim->hookScratch.u.celPhase[x] = (uint8_t)((sim->hookScratch.u.celPhase[x] + 1u) & 3u); - sim->spr[x].ptr = U_CEL(sim, sim->hookScratch.u.celPhase[x]); - } -} - - -static void hookUPerTick(StSimT *sim) { - // $7DAD: the original opens with a ceiling reflect that only runs - // while a crash is in progress and only above row $11. The wreck - // sequence moves sprite 0 downward to $DA and never back up, so this - // cannot fire on the real machine either; it is kept as shipped. - if (sim->collisionPhase != 0u && sim->spr[0].row < 0x11u) { - sim->velY = (int16_t)(-sim->velY); - sim->posYrow = (uint8_t)(sim->posYrow + 2u); - } - hookUBounce(sim); - hookUOrbs(sim); -} - - - - -// --------------------------------------------------------------------------- -// Dispatch -// --------------------------------------------------------------------------- - -// $62F0 -- a converted level's scratch starts from the bytes the shipped -// blob holds at the addresses the original used, so the first tick sees -// exactly what the C64's did. -// Level E "BEANSTALK" ($7DA0 prelude0, $7DCA per-tick2): the screen -// opens with pad 1 only. While the fare aboard wants UP the stalk grows: -// every $57 ticks a leaf cell sprouts right along row `row` and left -// along row `row`-2, the green tips one cell beyond; the fifth step -// hangs pads N and N+1 at the leaf ends instead, opens them (pad count -// +2) and moves the growth four rows up, until pad 9 is out. $7DA0 runs -// from the respawn prelude, so a lost cab restarts the count from pad 1 -// while the leaves already drawn stay. -static void hookEPrelude0(StSimT *sim) { - StHookET *e = &sim->hookScratch.e; - - sim->specialPad = 1u; - sim->padCount = 1u; - e->seg = 0u; - e->padSlot = 2u; - e->row = ST_HOOK_E_FIRST_ROW; - e->tick = ST_HOOK_E_TICK_START; - if (sim->levelState == 1u) { - e->tick = ST_HOOK_E_TICK_SECOND; - } -} - - -// $7E89 -- one green cell. -static void hookEPut(StSimT *sim, uint8_t col, uint8_t row, uint8_t ch) { - stSimPutChar(sim, col, row, ch); - stSimPutColor(sim, col, row, ST_HOOK_E_COLOR); -} - - -// $7E39 -- the tips, one cell beyond this step's leaves. -static void hookETips(StSimT *sim) { - const StHookET *e = &sim->hookScratch.e; - - hookEPut(sim, (uint8_t)(ST_HOOK_E_STALK_COL + 1u + e->seg), e->row, ST_HOOK_E_CHAR_TIP_R); - hookEPut(sim, (uint8_t)(ST_HOOK_E_STALK_COL - 1u - e->seg), (uint8_t)(e->row - 2u), ST_HOOK_E_CHAR_TIP_L); -} - - -static void hookEPerTick(StSimT *sim) { - StHookET *e = &sim->hookScratch.e; - - if (sim->specialPad == ST_HOOK_E_LAST_PAD || sim->activeSpriteIdx != ST_FARE_DEST_UP) { - return; - } - e->tick++; - if (e->tick != ST_HOOK_E_GROW_TICKS) { - return; - } - e->tick = 0u; - e->seg++; - if (e->seg != ST_HOOK_E_LEAVES) { - hookEPut(sim, (uint8_t)(ST_HOOK_E_STALK_COL + e->seg), e->row, ST_HOOK_E_CHAR_LEAF); - hookEPut(sim, (uint8_t)(ST_HOOK_E_STALK_COL - e->seg), (uint8_t)(e->row - 2u), ST_HOOK_E_CHAR_LEAF); - hookETips(sim); - return; - } - // $7E15: the pads hang from the leaf ends, then the stalk climbs. - hookEPut(sim, ST_HOOK_E_PAD_COL_RIGHT, e->row, (uint8_t)(ST_CHAR_DIGIT_BASE + e->padSlot)); - hookEPut(sim, ST_HOOK_E_PAD_COL_LEFT, (uint8_t)(e->row - 2u), (uint8_t)(ST_CHAR_DIGIT_BASE + e->padSlot + 1u)); - hookETips(sim); - sim->specialPad = (uint8_t)(sim->specialPad + 2u); - sim->padCount = (uint8_t)(sim->padCount + 2u); - e->seg = 0u; - e->row = (uint8_t)(e->row - ST_HOOK_E_ROW_STEP); - e->padSlot = (uint8_t)(e->padSlot + 2u); - stAudioSfx(E_SFX_GROW(sim)); -} - - -// $7DAF (J) / $7DFE (P) -- a hazard that touched the scenery below row -// $38 bursts: cel $82, phase 0, dropped four rows onto what it hit. -// Sprites 7 down to 3, every one that hit. -static void hookFallBgHits(StSimT *sim, uint8_t *state, uint8_t *phase, const uint8_t *sfx) { - uint8_t x; - - for (x = (uint8_t)(ST_HW_SPRITES - 1u); x >= ST_HOOK_FIRST_SPRITE; x--) { - if ((sim->spriteBgColl & kStBit[x]) == 0u || sim->spr[x].row < ST_HOOK_FALL_HIT_ROW || state[x] == 2u) { - continue; - } - state[x] = 2u; - sim->spr[x].ptr = ST_HK_FALL_CEL_BURST; - phase[x] = 0u; - sim->spr[x].row = (uint8_t)(sim->spr[x].row + ST_HOOK_FALL_HIT_DROP); - if (sfx != 0) { - stAudioSfx(sfx); - } - } -} - - -// $7E01 (J) / $7E45 (P) -- the per-sprite machine, sprites 3..7. Idle: -// spawn on rng($3C) < 3 at rng($FF) + the sprite's column base (J -// rerolls one over the middle band), cel $80 on row $17. Falling: one -// row per tick, cel flipping $80/$81 and drifting sideways on the odd -// cel. Bursting: walk the cel run (P only every other count of a -// shared parity) and go idle at its end. -static void hookFallTick(StSimT *sim, uint8_t *state, uint8_t *phase, const StHookFallT *d, uint8_t *parity) { - uint8_t x; - - for (x = ST_HOOK_FIRST_SPRITE; x < ST_HW_SPRITES; x++) { - if (state[x] == 0u) { - uint16_t col; - if (stSimRng(sim, ST_HOOK_FALL_SPAWN_ODDS) >= ST_HOOK_FALL_SPAWN_HIT) { - continue; - } - do { - col = (uint16_t)(stSimRng(sim, 0xFFu) + HKT(sim, d->startTable + x)); - } while (d->avoidMiddle && col >= ST_HOOK_FALL_BAND_LO && col < ST_HOOK_FALL_BAND_HI); - sim->spr[x].col = (uint8_t)col; - sim->spr[x].msb = (uint8_t)(col >> 8); - sim->spr[x].ptr = ST_HK_FALL_CEL; - sim->spr[x].row = ST_HOOK_FALL_TOP_ROW; - sim->spr[x].enable = 1u; - state[x] = 1u; - } else if (state[x] == 1u) { - sim->spr[x].ptr ^= 1u; - if ((sim->spr[x].ptr & 1u) != 0u) { - stSimSpriteAddX(sim, x, HKT(sim, d->dxTable + x)); - } - sim->spr[x].row++; - } else if (state[x] == 2u) { - if (parity != 0) { - (*parity)++; - if ((*parity & 1u) != 0u) { - continue; - } - } - phase[x]++; - if (phase[x] == d->phaseEnd) { - state[x] = 0u; - sim->spr[x].enable = 0u; - } else { - sim->spr[x].ptr = HKT(sim, d->celTable + phase[x]); - } - } else { - state[x] = 0u; // $7E1C: anything else resets - } - } -} - - -// Level J "SHOOTING STARS" ($7D9D prelude0, $7DAF per-tick2): stars fall -// from the top and burst on the scenery (hookFall*). The hook reads the -// stars' own $D01F bits, so the prelude opts them into the collision -// test. -static void hookJPrelude0(StSimT *sim) { - uint8_t x; - - for (x = ST_HOOK_FIRST_SPRITE; x < ST_HW_SPRITES; x++) { - sim->hookScratch.j.state[x] = 0u; - sim->spr[x].color = ST_HK_J_COLOR; - } - sim->bgCollSprites = ST_HOOK_FALL_SPRITES; -} - - -static void hookJPerTick(StSimT *sim) { - StHookJT *j = &sim->hookScratch.j; - - hookFallBgHits(sim, j->state, j->phase, J_SFX_BURST(sim)); - hookFallTick(sim, j->state, j->phase, &kFallJ, 0); -} - - -// Level L "BLACK HOLE" ($7D9F prelude0, $7DBC per-tick2, $7D9B verdict): -// hardware sprite 3 spins through cels $80..$82 at ($AC, $82) and pulls -// the cab in -- each tick the gravity templates become the pull toward -// it, weaker with distance. -static void hookLPrelude0(StSimT *sim) { - StSpriteT *hole = &sim->spr[ST_HOOK_L_HOLE]; - - hole->color = ST_HK_L_COLOR; - hole->msb = 0u; - hole->col = ST_HOOK_L_HOLE_COL; - hole->row = ST_HOOK_L_HOLE_ROW; - hole->ptr = ST_HK_L_CEL_FIRST; - hole->enable = 1u; -} - - -// $7DCB / $7E01 -- base less |dist|/8 ($7E42), signed by the direction. -static uint16_t hookLPull(uint8_t base, uint8_t dist) { - uint8_t mag = dist; - uint16_t pull; - - if ((mag & 0x80u) != 0u) { - mag = (uint8_t)(0u - mag); - } - pull = (uint8_t)(base - (uint8_t)(mag >> 3)); - if ((dist & 0x80u) != 0u) { - pull = (uint16_t)(0u - pull); - } - return pull; -} - - -static void hookLPerTick(StSimT *sim) { - StSpriteT *hole = &sim->spr[ST_HOOK_L_HOLE]; - uint8_t dy; - uint8_t dx; - - hole->ptr++; - if (hole->ptr == ST_HK_L_CEL_END) { - hole->ptr = ST_HK_L_CEL_FIRST; - } - dy = (uint8_t)(ST_HOOK_L_HOLE_ROW - sim->spr[0].row); - sim->gravTemplateY = hookLPull(ST_HOOK_L_PULL_Y, dy); - dx = (uint8_t)(ST_HOOK_L_HOLE_X_HALF - (uint8_t)(stSimSpriteX(sim, 0u) >> 1)); - sim->gravTemplateX = hookLPull(ST_HOOK_L_PULL_X, dx); -} - - -// Level P "BLIZZARD" ($7DA8 prelude0, $7D9C prelude1, $7DBA per-tick2): -// snow falls and melts on the scenery (hookFall*), and now and then the -// wind turns: the windsocks flip and the X gravity changes sign. -static void hookPPrelude0(StSimT *sim) { - uint8_t x; - - for (x = ST_HOOK_FIRST_SPRITE; x < ST_HW_SPRITES; x++) { - sim->hookScratch.p.state[x] = 0u; - sim->spr[x].color = ST_HK_P_COLOR; - } - sim->bgCollSprites = ST_HOOK_FALL_SPRITES; -} - - -static void hookPFlipSock(StSimT *sim, uint16_t cell) { - stSimPutChar(sim, (uint8_t)(cell % ST_SCREEN_COLS), (uint8_t)(cell / ST_SCREEN_COLS), (uint8_t)(sim->screen[cell] ^ ST_HOOK_P_SOCK_FLIP)); -} - - -static void hookPPerTick(StSimT *sim) { - StHookPT *p = &sim->hookScratch.p; - - if (stSimRng(sim, ST_HOOK_P_WIND_ODDS) < ST_HOOK_P_WIND_HIT) { - stAudioSfx(P_SFX_WIND(sim)); - sim->gravTemplateX = (uint16_t)(0u - sim->gravTemplateX); - hookPFlipSock(sim, ST_CELL(7, 6)); // $051E - hookPFlipSock(sim, ST_CELL(9, 21)); // $057D - hookPFlipSock(sim, ST_CELL(15, 31)); // $0677 - } - hookFallBgHits(sim, p->state, p->phase, 0); - hookFallTick(sim, p->state, p->phase, &kFallP, &p->parity); -} - - -// Level S "THE SWITCH" ($7D9D input filter): the stick's direction bits -// come back swapped through the level's table; FIRE passes. -static uint8_t hookSInput(StSimT *sim, uint8_t input) { - return (uint8_t)((input & ST_HOOK_S_FIRE) | S_DIR_MAP(sim, input & ST_HOOK_S_DIRS)); -} - - -void stHookSceneLoad(StSimT *sim) { - uint8_t k; - - switch (sim->hookLevel) { - case ST_LEVEL_G: - sim->hookScratch.g.lastSpot = HKT(sim, 0x7DD1); - sim->hookScratch.g.celPhase = HKT(sim, 0x7DD2); - sim->hookScratch.g.hop = HKT(sim, 0x7DD3); - sim->hookScratch.g.tick = HKT(sim, 0x7E00); - sim->hookScratch.g.sweep = HKT(sim, 0x7F02); - break; - case ST_LEVEL_H: - sim->hookScratch.h.switchId = HKT(sim, 0x7E4C); - sim->hookScratch.h.tick = HKT(sim, 0x7E53); - sim->hookScratch.h.active = HKT(sim, 0x7E54); - sim->hookScratch.h.step = HKT(sim, 0x7E55); - sim->hookScratch.h.tone = HKT(sim, 0x7E56); - for (k = 0u; k < ST_HOOK_H_SEGS; k++) { - sim->hookScratch.h.segFlag[k] = HKT(sim, 0x7E15 + k); - } - break; - case ST_LEVEL_I: - for (k = 0u; k < ST_HW_SPRITES; k++) { - sim->hookScratch.i.state[k] = HKT(sim, 0x7DB8 + k); - sim->hookScratch.i.phase[k] = HKT(sim, 0x7DC0 + k); - sim->hookScratch.i.dx[k] = HKT(sim, 0x7DC8 + k); - sim->hookScratch.i.dy[k] = HKT(sim, 0x7DD0 + k); - } - break; - case ST_LEVEL_K: - sim->hookScratch.k.parity = HKT(sim, 0x7DBD); - break; - case ST_LEVEL_O: - sim->hookScratch.o.tick = HKT(sim, 0x7DBD); - break; - case ST_LEVEL_P: - sim->hookScratch.p.parity = HKT(sim, 0x7F1F); - break; - case ST_LEVEL_Q: - for (k = 0u; k < ST_HW_SPRITES; k++) { - sim->hookScratch.q.phase[k] = HKT(sim, 0x7E83 + k); - } - break; - case ST_LEVEL_T: - sim->hookScratch.t.gateShut = HKT(sim, 0x7D9C); - break; - case ST_LEVEL_U: - sim->hookScratch.u.cooldown = HKT(sim, 0x7F6D); - for (k = 0u; k < ST_HW_SPRITES; k++) { - sim->hookScratch.u.celPhase[k] = HKT(sim, 0x7F4F + k); - sim->hookScratch.u.state[k] = HKT(sim, 0x7F57 + k); - sim->hookScratch.u.dir[k] = HKT(sim, 0x7F5F + k); - sim->hookScratch.u.life[k] = HKT(sim, 0x7F6E + k); - } - break; - case ST_LEVEL_W: - sim->hookScratch.w.colorPhase = HKT(sim, 0x7DAB); - sim->hookScratch.w.started = HKT(sim, 0x7DE5); - for (k = 0u; k < ST_HOOK_W_LASERS; k++) { - sim->hookScratch.w.state[k] = HKT(sim, 0x7DAC + k); - sim->hookScratch.w.row[k] = HKT(sim, 0x7DB5 + k); - } - break; - case ST_LEVEL_X: - sim->hookScratch.x.col = HKT(sim, 0x7D9C); - sim->hookScratch.x.dir = HKT(sim, 0x7D9D); - sim->hookScratch.x.tick = HKT(sim, 0x7D9E); - break; - default: - break; - } -} - - -// $7D75 -- 0 means sprite contact (passenger, level sprites) is safe. -uint8_t stHookHitVerdict(StSimT *sim) { - switch (sim->hookLevel) { - case ST_LEVEL_G: - // $7DF2: only the scenery kills; that also unhides the cab. - if ((sim->spriteBgColl & 1u) != 0u) { - sim->spr[0].enable = 1u; - return 1u; - } - return 0u; - case ST_LEVEL_H: - return 0u; - case ST_LEVEL_L: - return ST_HOOK_L_VERDICT; - default: - return 1u; - } -} - - -// $7D72 -- the input filter. -uint8_t stHookInput(StSimT *sim, uint8_t input) { - switch (sim->hookLevel) { - case ST_LEVEL_Q: - return hookQInput(sim, input); - case ST_LEVEL_S: - return hookSInput(sim, input); - default: - break; - } - return input; -} - - -// $7D6C -- before the sprite flush. Split in two: one switch over all -// sixteen levels left the 65816's single accumulator with more live -// values than greedy register allocation can place, and the build then -// falls back to the basic allocator for this whole file. -ST_NOINLINE static void hookPerTickLow(StSimT *sim) { - switch (sim->hookLevel) { - case ST_LEVEL_E: - hookEPerTick(sim); - break; - case ST_LEVEL_G: - hookGPerTick(sim); - break; - case ST_LEVEL_H: - hookHPerTick(sim); - break; - case ST_LEVEL_I: - hookIPerTick(sim); - break; - case ST_LEVEL_J: - hookJPerTick(sim); - break; - case ST_LEVEL_K: - hookKPerTick(sim); - break; - case ST_LEVEL_L: - hookLPerTick(sim); - break; - default: - break; - } -} - - -ST_NOINLINE static void hookPerTickHigh(StSimT *sim) { - switch (sim->hookLevel) { - case ST_LEVEL_O: - hookOPerTick(sim); - break; - case ST_LEVEL_P: - hookPPerTick(sim); - break; - case ST_LEVEL_Q: - hookQPerTick(sim); - break; - case ST_LEVEL_R: - hookRPerTick(sim); - break; - case ST_LEVEL_T: - hookTPerTick(sim); - break; - case ST_LEVEL_U: - hookUPerTick(sim); - break; - case ST_LEVEL_V: - hookVPerTick(sim); - break; - case ST_LEVEL_W: - hookWPerTick(sim); - break; - case ST_LEVEL_X: - hookXPerTick(sim); - break; - default: - break; - } -} - - -void stHookPerTick2(StSimT *sim) { - if (sim->hookLevel <= ST_LEVEL_L) { - hookPerTickLow(sim); - return; - } - hookPerTickHigh(sim); -} - - -// $7D6F -- after the sprite flush. -void stHookPerTick3(StSimT *sim) { - (void)sim; -} - - -// $7D66 -- first prelude hook. -void stHookPrelude0(StSimT *sim) { - switch (sim->hookLevel) { - case ST_LEVEL_H: - hookHPrelude0(sim); - break; - case ST_LEVEL_X: - hookXPrelude0(sim); - break; - case ST_LEVEL_U: - hookUPrelude0(sim); - break; - case ST_LEVEL_Q: - hookQPrelude(sim); - break; - case ST_LEVEL_I: - hookIPrelude(sim); - break; - case ST_LEVEL_W: - memset(sim->hookScratch.w.state, 0, sizeof(sim->hookScratch.w.state)); - break; - case ST_LEVEL_E: - hookEPrelude0(sim); - break; - case ST_LEVEL_G: - hookGPrelude0(sim); - break; - case ST_LEVEL_J: - hookJPrelude0(sim); - break; - case ST_LEVEL_L: - hookLPrelude0(sim); - break; - case ST_LEVEL_P: - hookPPrelude0(sim); - break; - default: - break; - } -} - - -// $7D69 -- second prelude hook. -void stHookPrelude1(StSimT *sim) { - switch (sim->hookLevel) { - case ST_LEVEL_T: - hookTPrelude1(sim); - break; - case ST_LEVEL_W: - sim->hookScratch.w.started = 1u; - break; - case ST_LEVEL_R: - hookRMaze(sim, ST_CHAR_BLANK); - break; - case ST_LEVEL_P: - stAudioSfx(P_SFX_WIND(sim)); - break; - case ST_LEVEL_G: - sim->hookScratch.g.lastSpot = ST_HOOK_G_SPOTS; // $7D9A - break; - default: - break; - } -} diff --git a/examples/spacetaxi/stLevel.c b/examples/spacetaxi/stLevel.c deleted file mode 100644 index fb23c27..0000000 --- a/examples/spacetaxi/stLevel.c +++ /dev/null @@ -1,21 +0,0 @@ -// Space Taxi -- level loader (JoeyLib data path + the shared STL4 parser). - -#include - -#include "joey/file.h" -#include "spacetaxi.h" - - -bool stLevelLoad(StLevelT *out, const char *path) { - FILE *fp; - bool ok; - - // path is a name relative to DATA/ (e.g. "levels/title.dat"). - fp = jlDataOpen(path, "rb"); - if (fp == NULL) { - return false; - } - ok = stLevelParse(out, fp); - fclose(fp); - return ok; -} diff --git a/examples/spacetaxi/stLevelCels.c b/examples/spacetaxi/stLevelCels.c deleted file mode 100644 index bb916e7..0000000 --- a/examples/spacetaxi/stLevelCels.c +++ /dev/null @@ -1,114 +0,0 @@ -// Space Taxi -- the cels a level's hook program can put on screen. -// -// Hook sprites are built from the level's own VIC bitmaps in whatever -// colour the hook gives them, and compiling one on the 65816 costs about -// half a second -- far too long to do the first time a cel appears mid- -// ride. So every level lists its cels up front, from the same hook -// tables the hook program reads (stHookTables.h): the offline baker turns -// the list into a pre-compiled bank (sprites/levelNN.spc) and the game -// compiles whatever the bank does not supply BEFORE the ride starts. -// -// Only the level file is needed, no simulation, so the baker can link -// this on the host without the rest of the game. - -#include "stCels.h" -#include "stHookTables.h" - -#define HKL(level, addr) ((level)->hookData[(addr) - ST_HOOK_BASE]) - -static void celAdd(StCelDefT *out, uint8_t *count, uint8_t max, uint8_t ptr, uint8_t color, uint8_t multi, uint8_t mc0, uint8_t mc1); - - -// Append one cel unless the list already has it. -static void celAdd(StCelDefT *out, uint8_t *count, uint8_t max, uint8_t ptr, uint8_t color, uint8_t multi, uint8_t mc0, uint8_t mc1) { - uint8_t k; - - for (k = 0u; k < *count; k++) { - if (out[k].ptr == ptr && out[k].color == color && out[k].multi == multi && out[k].mc0 == mc0 && out[k].mc1 == mc1) { - return; - } - } - if (*count == max) { - return; - } - out[*count].ptr = ptr; - out[*count].color = color; - out[*count].multi = multi; - out[*count].mc0 = mc0; - out[*count].mc1 = mc1; - (*count)++; -} - - -uint8_t stLevelCelList(const StLevelT *level, StCelDefT *out, uint8_t max) { - uint8_t n = 0u; - uint8_t k; - uint8_t c; - - switch (level->levelIndex) { - case ST_LEVEL_G: - for (k = 0u; k < ST_HK_G_CEL_PHASES; k++) { - celAdd(out, &n, max, HKL(level, ST_HK_G_CEL + k), ST_HK_G_ORB_COLOR, 0u, 0u, 0u); - } - break; - case ST_LEVEL_H: - celAdd(out, &n, max, ST_HK_H_CEL, ST_HK_H_COLOR_IDLE, 0u, 0u, 0u); - celAdd(out, &n, max, ST_HK_H_CEL, ST_HK_H_COLOR_LIT, 0u, 0u, 0u); - celAdd(out, &n, max, ST_HK_H_CEL, ST_HK_H_COLOR_ACTIVE, 0u, 0u, 0u); - break; - case ST_LEVEL_I: - // Every cel in the idle colour and in each of the three the - // launcher picks at random. - for (c = 0u; c <= ST_HK_I_COLORS; c++) { - uint8_t color = (c == 0u) ? ST_HK_I_COLOR_IDLE : HKL(level, ST_HK_I_COLOR + c - 1u); - for (k = 0u; k < ST_HK_I_RISE_PHASES; k++) { - celAdd(out, &n, max, HKL(level, ST_HK_I_CEL_RISE + k), color, 0u, 0u, 0u); - } - for (k = 0u; k < ST_HK_I_BURST_PHASES; k++) { - celAdd(out, &n, max, HKL(level, ST_HK_I_CEL_BURST + k), color, 0u, 0u, 0u); - } - } - break; - case ST_LEVEL_J: - celAdd(out, &n, max, ST_HK_FALL_CEL, ST_HK_J_COLOR, 0u, 0u, 0u); - celAdd(out, &n, max, (uint8_t)(ST_HK_FALL_CEL + 1u), ST_HK_J_COLOR, 0u, 0u, 0u); - celAdd(out, &n, max, ST_HK_FALL_CEL_BURST, ST_HK_J_COLOR, 0u, 0u, 0u); - for (k = 1u; k < ST_HK_J_BURST_PHASES; k++) { - celAdd(out, &n, max, HKL(level, ST_HK_J_CEL_BURST + k), ST_HK_J_COLOR, 0u, 0u, 0u); - } - break; - case ST_LEVEL_L: - for (k = ST_HK_L_CEL_FIRST; k < ST_HK_L_CEL_END; k++) { - celAdd(out, &n, max, k, ST_HK_L_COLOR, 0u, 0u, 0u); - } - break; - case ST_LEVEL_P: - celAdd(out, &n, max, ST_HK_FALL_CEL, ST_HK_P_COLOR, 0u, 0u, 0u); - celAdd(out, &n, max, (uint8_t)(ST_HK_FALL_CEL + 1u), ST_HK_P_COLOR, 0u, 0u, 0u); - celAdd(out, &n, max, ST_HK_FALL_CEL_BURST, ST_HK_P_COLOR, 0u, 0u, 0u); - for (k = 1u; k < ST_HK_P_BURST_PHASES; k++) { - celAdd(out, &n, max, HKL(level, ST_HK_P_CEL_BURST + k), ST_HK_P_COLOR, 0u, 0u, 0u); - } - break; - case ST_LEVEL_Q: - // The hook's cels show HIRES in colour 6 for the whole recorded - // ride (tools/stsim STSIM_CELS census, 2026-09-22); the - // multicolour variant (the level's own $D025/$D026) stays - // listed for the phases the census did not reach. - for (k = 0u; k < ST_HK_Q_CEL_PHASES; k++) { - celAdd(out, &n, max, HKL(level, ST_HK_Q_CEL + k), ST_HK_Q_COLOR, 0u, 0u, 0u); - } - for (k = 0u; k < ST_HK_Q_CEL_PHASES; k++) { - celAdd(out, &n, max, HKL(level, ST_HK_Q_CEL + k), ST_HK_Q_COLOR, 1u, level->header[5], level->header[6]); - } - break; - case ST_LEVEL_U: - for (k = 0u; k < ST_HK_U_CEL_PHASES; k++) { - celAdd(out, &n, max, HKL(level, ST_HK_U_CEL + k), ST_HK_U_COLOR, 0u, 0u, 0u); - } - break; - default: - break; - } - return n; -} diff --git a/examples/spacetaxi/stLevelFile.c b/examples/spacetaxi/stLevelFile.c deleted file mode 100644 index 5ca896e..0000000 --- a/examples/spacetaxi/stLevelFile.c +++ /dev/null @@ -1,90 +0,0 @@ -// Space Taxi -- STL4 level file parser (plain stdio, no JoeyLib), shared -// by the game and the host-side simulation harness. -// -// Layout (all bytes, written by examples/spacetaxi/assets/romToLevel.py): -// "STL4" -// name[22] $7D78 level name in screen codes -// header[9] $7D00 VIC colour block -// padCount, specialPad, fuelRate -// spawn[5] $7D5B -// templates[8] $7D8F Y accel, X accel, Y gravity, X gravity (LE words) -// pads[10][8] $7D0A raw slots -// screen[1000], color[1000] -// levelIndex -// spriteCount, then spriteCount x (ptr, 63 bytes) -// hookData[616] $7D98..$7FFF - -#include -#include - -#include "stSim.h" - - -static bool readBlock(FILE *fp, void *dst, size_t n); - - -static bool readBlock(FILE *fp, void *dst, size_t n) { - return fread(dst, 1, n, fp) == n; -} - - -bool stLevelParse(StLevelT *out, FILE *fp) { - uint8_t hdr[4]; - uint8_t counts[3]; - uint8_t templates[8]; - uint8_t slots[ST_MAX_PADS][8]; - uint8_t k; - - memset(out, 0, sizeof(*out)); - if (!readBlock(fp, hdr, 4u) || hdr[0] != 'S' || hdr[1] != 'T' || hdr[2] != 'L' || hdr[3] != '4') { - return false; - } - if (!readBlock(fp, out->name, ST_LEVEL_NAME_CHARS)) { - return false; - } - out->name[ST_LEVEL_NAME_CHARS] = 0u; - if (!readBlock(fp, out->header, 9u) || !readBlock(fp, counts, 3u) || !readBlock(fp, out->spawn, 5u)) { - return false; - } - out->padCount = counts[0]; - out->specialPad = counts[1]; - out->fuelRate = counts[2]; - if (!readBlock(fp, templates, 8u)) { - return false; - } - out->accelY = (uint16_t)(templates[0] | ((uint16_t)templates[1] << 8)); - out->accelX = (uint16_t)(templates[2] | ((uint16_t)templates[3] << 8)); - out->gravY = (uint16_t)(templates[4] | ((uint16_t)templates[5] << 8)); - out->gravX = (uint16_t)(templates[6] | ((uint16_t)templates[7] << 8)); - if (!readBlock(fp, slots, sizeof(slots))) { - return false; - } - for (k = 0u; k < ST_MAX_PADS; k++) { - out->pads[k].x1Hi = slots[k][0]; - out->pads[k].x1Lo = slots[k][1]; - out->pads[k].x2Hi = slots[k][2]; - out->pads[k].x2Lo = slots[k][3]; - out->pads[k].row = slots[k][4]; - out->pads[k].passMsb = slots[k][5]; - out->pads[k].passCol = slots[k][6]; - out->pads[k].unused = slots[k][7]; - } - if (!readBlock(fp, out->screen, ST_SCREEN_CELLS) || !readBlock(fp, out->color, ST_SCREEN_CELLS)) { - return false; - } - if (!readBlock(fp, &out->levelIndex, 1u) || !readBlock(fp, &out->spriteCount, 1u)) { - return false; - } - if (out->spriteCount > ST_MAX_LEVEL_SPRITES) { - return false; - } - for (k = 0u; k < out->spriteCount; k++) { - if (!readBlock(fp, &out->sprites[k].ptr, 1u) || !readBlock(fp, out->sprites[k].bitmap, ST_SPRITE_BYTES)) { - return false; - } - } - if (!readBlock(fp, out->hookData, ST_HOOK_BYTES)) { - return false; - } - return true; -} diff --git a/examples/spacetaxi/stRender.c b/examples/spacetaxi/stRender.c deleted file mode 100644 index 3d41935..0000000 --- a/examples/spacetaxi/stRender.c +++ /dev/null @@ -1,1286 +0,0 @@ -// Space Taxi -- renderer: the simulation's screen RAM, colour RAM, -// charset and sprite frame onto the JoeyLib stage. -// -// The screen is 40x25 character cells painted from the live charset -// (the game edits glyphs at runtime: the transporter hatch, the title -// logo flip-book, the laser beams). A cell goes to the stage in one -// call: jlTilePasteGlyph takes the character's eight 1bpp charset rows -// as they stand and colours them from the cell's colour RAM, so an -// edited glyph needs no rebuild and a colour change costs nothing. -// Only the cells the simulation listed as dirty are -// repainted; a changed glyph dirties the cells showing it, found -// inside the cell range each character was last painted in. The eight -// VIC sprites are drawn from their bitmaps through a small cache of -// JoeyLib sprites keyed on (pointer, colour, multicolour mode), with -// save-under and LIFO restore. Sprite 0 has VIC priority, so the draw -// order is sprite 7 first, sprite 0 last, and a sprite that has not -// moved (and has nothing repainted under it) simply stays on the -// stage: only it and everything drawn after it are undrawn and redrawn -// when it changes. -// -// The title screen gets a special path (stRenderTitleLogo): its logo is -// 103 cells of one character that the flip-book animates by copying one -// of four frame glyphs over it, and a colour cycle that used to rewrite -// the colour of 407 cells. Both were pure repaint cost. Instead the four -// frames collapse into ONE tile whose pixels are drawn in reserved -// palette slots -- one slot per set of frames a pixel belongs to -- so a -// flip and a recolour are both a single palette write and no cell is -// ever repainted. The title's decoration sprites, which cycle colour -// with the logo, share one more reserved slot for the same reason. - -#include - -#include "spacetaxi.h" -#include "stCels.h" - -#define ST_SPRITE_TILES 3u -// The IIgs reaches its globals DBR-relative, so all of BSS must fit the -// entry bank below the I/O window; the caches are sized down there. The -// sprite cache must still hold every cel stRenderPrewarm builds (the -// cab, exhaust and passenger sets) or a cel change mid-play rebuilds -// and recompiles a sprite. Glyphs are direct-mapped by character, so -// the count is a power of two. -// Room for the baked cab/passenger/exhaust set (36) plus the largest -// level's hook cels (I: 30) at once. -#define ST_SPRITE_CACHE 80u -#define ST_SPRITE_BACKUP_BYTES JOEY_SPRITE_BACKUP_BYTES(ST_SPRITE_TILES, ST_SPRITE_TILES) -#define ST_SPRITE_PX (8 * ST_SPRITE_TILES) -// Palette slots the title logo animation owns. The title screen draws -// in C64 colours 0, 1, 2, 6, 7, 11 and 12 only (plus colour 5 for the -// "GET READY" banner that appears over it), so these are free while it -// is up and the whole trick needs no per-scanline SCB band. -#define ST_PAL_LOGO_SPR 3u // the decoration sprites' cycling colour -#define ST_LOGO_PAL_SLOTS 7u -// The first hardware sprite that cycles colour with the logo. -#define ST_LOGO_SPR_FIRST 3u - -// Where the startup message sits, and its C64 colour (1 = white). -#define ST_LOADING_ROW 12u -#define ST_LOADING_COLOR 1u - -typedef struct { - jlSpriteT *sprite; - uint8_t ptr; - uint8_t color; - uint8_t multi; - uint8_t mc0; - uint8_t mc1; - uint8_t level; // level index the bitmap came from (level sprites) - uint8_t offX; // where the (cropped) cel sits in the 24x24 - uint8_t offY; // VIC frame, in pixels -- see celCreate - uint8_t w; // the cel's size in pixels - uint8_t h; - bool used; - bool pinned; // prewarmed moving cel: never evicted (its - // compiled code is costly to re-emit on the 65816) - bool compiled; // jlSpriteCompile succeeded for this cel - uint32_t lastUse; // for least-recently-used eviction - uint32_t retryAt; // cacheStamp at which a failed compile may retry -} StSpriteCacheT; - - -// What is drawn in a draw slot (slot 0 = sprite 7 ... slot 7 = sprite 0). -typedef struct { - bool drawn; - bool trusted; // backed up by the compiled, unclipped save - uint16_t x; - uint8_t y; - uint8_t ptr; - uint8_t color; - uint8_t multi; -} StDrawnT; - -typedef struct { - jlSurfaceT *stage; - jlSurfaceT *scratch; - // Title logo (stRenderTitleLogo): all four flip-book frames in one - // tile, animated by rewriting logoSlotMask[]'s palette slots. - bool logoMode; - bool starMode; // level intro: cycling the starfield - uint8_t starPhase; // which step of every flight is lit - bool logoTileOk; // the live glyph is one of the frames - uint8_t logoSlotCount; - uint8_t logoSlotMask[ST_LOGO_PAL_SLOTS]; // frames each slot is lit in - uint8_t logoFrame; // frame the palette currently shows - uint8_t logoColor; // logo colour the palette currently shows - uint8_t logoSprColor; // decoration-sprite colour it shows - jlTileT logoTile; - // Cell range (inclusive) each character has been painted in since - // the last full repaint: where a changed glyph's cells can be. - // Column band of the cells about to be repainted, per character row - // (min > max = the row is clean), the same shape as the library's - // own per-row dirty bands. - uint8_t dirtyColMin[ST_SCREEN_ROWS]; - uint8_t dirtyColMax[ST_SCREEN_ROWS]; - uint32_t cacheStamp; - StSpriteCacheT cache[ST_SPRITE_CACHE]; - StSpriteCacheT *lastHit[ST_HW_SPRITES]; // the entry each hardware sprite used last - jlSpriteBackupT backup[ST_HW_SPRITES]; - // ST_HW_SPRITES backups of ST_SPRITE_BACKUP_BYTES each, from jlAlloc: - // 4.5 KB that as BSS would not fit the IIgs entry bank (text, rodata, - // BSS and the C heap share its 64 KB). - uint8_t *backupMem; - StDrawnT slot[ST_HW_SPRITES]; - uint8_t lastPresentFrame; -} StRenderStateT; - - -static StRenderStateT gRender; -// Set across stRenderPrewarm so a cel built there is pinned in the cache. -static bool gPrewarming; - -// The VIC-II palette in register order, $0RGB. -static const uint16_t kC64Palette[16] = { - 0x0000, 0x0FFF, 0x0833, 0x06BB, 0x0839, 0x05A4, 0x0438, 0x0BC7, - 0x0852, 0x0540, 0x0B66, 0x0555, 0x0777, 0x09E8, 0x076C, 0x09AA -}; - -// Palette slots the logo tile's pixels are drawn in, one per distinct -// set of flip-book frames a pixel belongs to. The four frames nest, so -// four of these are used; the rest are headroom if the frames change. -static const uint8_t kLogoPalSlot[ST_LOGO_PAL_SLOTS] = { 4u, 8u, 9u, 10u, 13u, 14u, 15u }; - -// Level-intro warp: how a star step looks at each distance BEHIND the -// lit step, as C64 colour indices; further behind is unlit. One yellow -// star per ray, as the C64's star sprites (colour 7, $4523) fly with no -// trail. -static const uint8_t kStarRamp[] = { 7u }; -#define ST_STAR_RAMP_LEN (sizeof(kStarRamp) / sizeof(kStarRamp[0])) - - -static StSpriteCacheT *cachedSprite(StSimT *sim, uint8_t idx); -static void cacheCompile(StSpriteCacheT *e); -static StSpriteCacheT *cacheFreeSlot(void); -static bool celCreate(StSpriteCacheT *e, const uint8_t *bm, uint8_t multi, uint8_t color, uint8_t mc0, uint8_t mc1); -static void levelCelsEvict(void); -static const uint8_t *levelSpriteBitmap(const StLevelT *level, uint8_t ptr); -static void loadingBar(jlSurfaceT *stage, uint8_t done, uint8_t total); -static bool celDrawsInterpreted(uint8_t ptr, uint8_t color); -static uint8_t cellCol(uint16_t cell, uint8_t row); -static uint8_t cellRow(uint16_t cell); -static void collectGlyphs(StSimT *sim); -static void dirtyBands(const StSimT *sim); -static bool dirtyUnderSprite(int16_t px, int16_t py); -static uint8_t drawColor(const StSimT *sim, uint8_t idx); -static void dropCache(void); -static bool loadPrecompiledCels(void); -static bool logoBuildTile(const StSimT *sim); -static uint8_t logoFrameIndex(const StSimT *sim); -static void logoPaletteApply(const StSimT *sim, uint8_t frame); -static void paintCells(jlSurfaceT *stage, StSimT *sim); -static void pasteCell(jlSurfaceT *stage, const StSimT *sim, uint16_t cell, uint8_t bx, uint8_t by); -static bool prewarmReport(uint16_t expect); -static bool spriteOnScreen(int16_t px, int16_t py); -static void starPaletteApply(void); -static void tileFromChunky(jlTileT *out, const uint8_t *chunky); -static bool tileHasPixels(const uint8_t *chunky); - - -// Compile a cached cel if it is not already, backing off after a failure -// (arena full) so a cel that cannot fit is not re-staged every frame -- -// jlSpriteCompile's failure path runs the whole emitter twice. -#define ST_COMPILE_RETRY_STAMPS 4096u // ~8 frames' worth of cachedSprite calls per frame -> ~25 s -static void cacheCompile(StSpriteCacheT *e) { - if (e->compiled || gRender.cacheStamp < e->retryAt) { - return; - } - e->compiled = jlSpriteCompile(e->sprite); - if (!e->compiled) { - e->retryAt = gRender.cacheStamp + ST_COMPILE_RETRY_STAMPS; - } -} - - -// The cached cel for hardware sprite `idx` of the current frame, built -// on first use from its VIC bitmap and colours; NULL when there is -// nothing to draw. -static StSpriteCacheT *cachedSprite(StSimT *sim, uint8_t idx) { - uint8_t ptr = sim->frame.ptr[idx]; - uint8_t color = drawColor(sim, idx); - uint8_t multi = (uint8_t)((sim->frame.multiMask & kStBit[idx]) != 0u ? 1u : 0u); - uint8_t mc0 = multi ? sim->spriteMc0 : 0u; - uint8_t mc1 = multi ? sim->spriteMc1 : 0u; - uint8_t level = (ptr < ST_SPRITE_PTR_FIRST && sim->level != 0) ? sim->level->levelIndex : 0xFFu; - StSpriteCacheT *slot = 0; - StSpriteCacheT *e = gRender.lastHit[idx]; - uint8_t k; - - gRender.cacheStamp++; - // Most frames a hardware sprite shows the cel it showed last time. - if (e != 0 && e->used && e->ptr == ptr && e->color == color && e->multi == multi && e->mc0 == mc0 && e->mc1 == mc1 && e->level == level) { - e->lastUse = gRender.cacheStamp; - return e; - } - for (k = 0u; k < ST_SPRITE_CACHE; k++) { - e = &gRender.cache[k]; - if (e->used && e->ptr == ptr && e->color == color && e->multi == multi && e->mc0 == mc0 && e->mc1 == mc1 && e->level == level) { - e->lastUse = gRender.cacheStamp; - gRender.lastHit[idx] = e; - return e; - } - // Pinned entries (prewarmed cab/exhaust/passenger cels) are the - // costly-to-recompile ones and are never evicted; the least- - // recently-used unpinned slot is the victim, a free slot first. - if (e->pinned) { - continue; - } - if (slot == 0 || !e->used || (slot->used && e->lastUse < slot->lastUse)) { - slot = e; - } - } - if (slot == 0) { - // Every slot is pinned (never happens with the prewarm set sized - // below the cache): reuse the queried entry's own slot is unsafe, - // so fail into the interpreter path. - return 0; - } - // No bitmap for this pointer (hooks enable a sprite a tick before - // they aim it, leaving it at block 0): nothing to draw, and no cel - // to build and evict for. - if (stSimSpriteBitmap(sim, ptr) == 0) { - return 0; - } - if (slot->used) { - jlSpriteDestroy(slot->sprite); - slot->sprite = 0; - slot->used = false; - slot->compiled = false; - } - if (!celCreate(slot, stSimSpriteBitmap(sim, ptr), multi, color, mc0, mc1)) { - return 0; - } - // Nothing compiles mid-ride: a compile is ~0.5 s on the 65816, a - // visible hitch. The cab, exhaust and passenger cels come baked - // (stRenderPrewarm), a level's hook cels come baked or are compiled - // at level entry (stRenderLevelCels), and a cel that still gets - // built here draws interpreted -- and is logged, because it means - // the level's cel list (stLevelCels.c) missed one. - slot->compiled = false; - slot->retryAt = 0u; - if (idx <= 2u && gPrewarming) { - cacheCompile(slot); - } - if (!gPrewarming && !celDrawsInterpreted(ptr, color)) { - ST_LOG("spacetaxi: ! cel $%02X colour %u (level %u) built mid-ride -- add it to stLevelCelList", ptr, color, level); - } - slot->lastUse = gRender.cacheStamp; - slot->ptr = ptr; - slot->color = color; - slot->multi = multi; - slot->mc0 = mc0; - slot->mc1 = mc1; - slot->level = level; - slot->used = true; - slot->pinned = gPrewarming; - gRender.lastHit[idx] = slot; - return slot; -} - - -// Block row of a screen cell: cell / 40, read from the 125 distinct -// results of (cell >> 3) / 5. The old (x * 205) >> 10 identity avoided a -// division but still called the 65816's software multiply, and this runs -// twice for every repainted cell. -static const uint8_t kCellRow[125] = { - 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, - 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 9, - 9, 9, 9, 9, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 12, 12, 12, 12, - 12, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 16, - 16, 16, 16, 16, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 19, 19, 19, - 19, 19, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, - 23, 23, 23, 23, 23, 24, 24, 24, 24, 24 -}; - - -// Build a VIC bitmap's cel into a cache entry from the shared cel -// expansion (stCels.c, the same one the offline baker uses), cropped to -// the tiles that hold pixels. Most VIC sprites leave whole tiles of their -// 24x21 frame empty, and every one of those would otherwise be saved, -// drawn, restored and presented each time the sprite moves. A bitmap -// with no pixels keeps the whole frame. -// -// The tiles go onto the scratch surface last to first: on the planar -// ports tileFromChunky converts through the scratch's top-left tile, so -// that tile has to be the one written last. -static bool celCreate(StSpriteCacheT *e, const uint8_t *bm, uint8_t multi, uint8_t color, uint8_t mc0, uint8_t mc1) { - uint8_t blob[ST_CEL_BYTES]; - jlTileT tile; - uint8_t txMin = ST_SPRITE_TILES; - uint8_t txMax = 0u; - uint8_t tyMin = ST_SPRITE_TILES; - uint8_t tyMax = 0u; - uint8_t tx; - uint8_t ty; - - stCelBlob(bm, multi, color, mc0, mc1, blob); - ty = ST_SPRITE_TILES; - while (ty > 0u) { - ty--; - tx = ST_SPRITE_TILES; - while (tx > 0u) { - const uint8_t *chunk; - - tx--; - chunk = &blob[(ty * ST_SPRITE_TILES + tx) * TILE_BYTES]; - tileFromChunky(&tile, chunk); - jlTilePaste(gRender.scratch, tx, ty, &tile); - if (tileHasPixels(chunk)) { - txMin = (tx < txMin) ? tx : txMin; - txMax = (tx > txMax) ? tx : txMax; - tyMin = (ty < tyMin) ? ty : tyMin; - tyMax = (ty > tyMax) ? ty : tyMax; - } - } - } - if (txMin > txMax) { - txMin = 0u; - tyMin = 0u; - txMax = ST_SPRITE_TILES - 1u; - tyMax = ST_SPRITE_TILES - 1u; - } - e->offX = (uint8_t)(txMin * TILE_PIXELS_PER_SIDE); - e->offY = (uint8_t)(tyMin * TILE_PIXELS_PER_SIDE); - e->w = (uint8_t)((txMax - txMin + 1u) * TILE_PIXELS_PER_SIDE); - e->h = (uint8_t)((tyMax - tyMin + 1u) * TILE_PIXELS_PER_SIDE); - e->sprite = jlSpriteCreateFromSurface(gRender.scratch, (int16_t)e->offX, (int16_t)e->offY, (uint8_t)(e->w / TILE_PIXELS_PER_SIDE), (uint8_t)(e->h / TILE_PIXELS_PER_SIDE)); - return e->sprite != 0; -} - - -// True for a cel that is MEANT to be built here rather than pre-listed, -// so the warning below stays about real misses. Only the title screen's -// decorations qualify: logoColorCycle recolours them every cycle step, -// which is forty ptr/colour pairs that would each cost a compile (see -// the note beside kStTitleDecorCels in stCels.c). -static bool celDrawsInterpreted(uint8_t ptr, uint8_t color) { - uint16_t k; - - for (k = 0u; k < kStInterpretedCount; k++) { - if (kStInterpretedCels[k].ptr != ptr) { - continue; - } - if (kStInterpretedCels[k].color == 0u || kStInterpretedCels[k].color == color) { - return true; - } - } - return false; -} - - -// Column of a cell whose row is known: cell - row * 40 through a table, -// because the two-shift form of the multiply came back as a __mulhi3 -// library call on the 65816. -static uint8_t cellCol(uint16_t cell, uint8_t row) { - static const uint16_t kRowBase[ST_SCREEN_ROWS] = { - 0, 40, 80, 120, 160, 200, 240, 280, 320, 360, 400, 440, 480, - 520, 560, 600, 640, 680, 720, 760, 800, 840, 880, 920, 960 - }; - return (uint8_t)(cell - kRowBase[row]); -} - - - -static uint8_t cellRow(uint16_t cell) { - return kCellRow[cell >> 3]; -} - - -// A changed glyph (the hatch, the logo flip, the laser beams) makes -// every cell that shows it dirty. The simulation hands over a short list -// of the characters it edited, so nothing scans the charset; the cell -// search covers only the range the character was last painted in. The -// glyph itself needs no invalidation -- the paste reads the live charset -// every time. -static void collectGlyphs(StSimT *sim) { - uint8_t k; - uint16_t cell; - uint16_t next; - - if (sim->charDirtyAll) { - sim->charDirtyAll = false; - sim->charDirtyCount = 0u; - return; - } - for (k = 0u; k < sim->charDirtyCount; k++) { - uint8_t ch = sim->charDirtyList[k]; - if (sim->dirtyAll) { - continue; - } - // Exactly the cells showing `ch`, straight off the reverse index - // the sim maintains (StSimT in stSim.h). No scan, no screen[] - // test: a cell is in this list if and only if it shows `ch`. - // - // This replaced a walk of a per-glyph [first, last] span. That - // span only ever widened -- pasteCell pushed it out and nothing - // pulled it back -- so a glyph that once appeared in transient - // text kept a screen-wide span for the rest of the level and was - // rescanned on every animation frame, at 24-bit far-pointer cost - // per cell. It measured 22% of the whole program on level H. - // - // `next` is read before the body because the overflow arm below - // stops the walk, and because keeping the read up here makes the - // loop independent of anything the body does to the cell. - cell = sim->glyphHead[ch]; - while (cell != ST_GLYPH_CELL_NONE) { - next = sim->cellNext[cell]; - if (sim->cellDirty[cell] == 0u) { - sim->cellDirty[cell] = 1u; - if (sim->dirtyCount < (uint16_t)(sizeof(sim->dirtyList) / sizeof(sim->dirtyList[0]))) { - sim->dirtyList[sim->dirtyCount++] = cell; - } else { - sim->dirtyAll = true; - break; - } - } - cell = next; - } - } - sim->charDirtyCount = 0u; -} - - -// Note where the cells about to be repainted are, as one column band -// per character row. This used to be a single bounding box over every -// dirty cell, and that box was wildly too coarse: the status row's -// score and fare meters sit at opposite ends of the screen and one of -// them moves on nearly every tick, so the box spanned the picture and -// every sprite counted as sitting over repainted ground -- all eight -// were undrawn and redrawn on about a third of all frames while only -// the cab and its exhaust had actually moved. Per row the bands are -// tight, and the rows a sprite covers are the only ones it tests. -static void dirtyBands(const StSimT *sim) { - uint16_t k; - - uint8_t row; - - if (sim->dirtyAll) { - for (row = 0u; row < ST_SCREEN_ROWS; row++) { - gRender.dirtyColMin[row] = 0u; - gRender.dirtyColMax[row] = ST_SCREEN_COLS - 1u; - } - return; - } - // One loop over both 25-entry arrays. - for (row = 0u; row < ST_SCREEN_ROWS; row++) { - gRender.dirtyColMin[row] = 0xFFu; - gRender.dirtyColMax[row] = 0u; - } - for (k = 0u; k < sim->dirtyCount; k++) { - uint16_t cell = sim->dirtyList[k]; - uint8_t r = cellRow(cell); - uint8_t col = cellCol(cell, r); - if (col < gRender.dirtyColMin[r]) { - gRender.dirtyColMin[r] = col; - } - if (col > gRender.dirtyColMax[r]) { - gRender.dirtyColMax[r] = col; - } - } -} - - -// True when a cell about to be repainted lies under the sprite drawn at -// (px, py): its save-under backup would hold stale background, so it -// has to be undrawn before the repaint and drawn again after. -static bool dirtyUnderSprite(int16_t px, int16_t py) { - int16_t x1 = (int16_t)(px + ST_SPRITE_W); - int16_t y1 = (int16_t)(py + ST_SPRITE_PX); - uint8_t row; - uint8_t rowEnd; - uint8_t colFirst; - uint8_t colLast; - - if (x1 > SURFACE_WIDTH) { - x1 = SURFACE_WIDTH; - } - if (y1 > SURFACE_HEIGHT) { - y1 = SURFACE_HEIGHT; - } - // spriteOnScreen has already rejected anything fully off the stage, - // so only the top/left overhang needs clamping to row/column 0. - row = (uint8_t)(py < 0 ? 0 : (py >> 3)); - rowEnd = (uint8_t)((y1 - 1) >> 3); - colFirst = (uint8_t)(px < 0 ? 0 : (px >> 3)); - colLast = (uint8_t)((x1 - 1) >> 3); - for (; row <= rowEnd; row++) { - if (gRender.dirtyColMin[row] <= colLast && gRender.dirtyColMax[row] >= colFirst) { - return true; - } - } - return false; -} - - -// The palette index a hardware sprite is drawn in. On the title screen -// sprites 3..7 all follow the logo's colour cycle, so they share one -// reserved slot: the cycle then recolours them with a palette write -// instead of rebuilding five cels, and -- because their drawn state -// stops changing -- without redrawing the sprites stacked above them. -static uint8_t drawColor(const StSimT *sim, uint8_t idx) { - if (gRender.logoMode && idx >= ST_LOGO_SPR_FIRST) { - return ST_PAL_LOGO_SPR; - } - return sim->frame.color[idx]; -} - - -static void dropCache(void) { - uint8_t k; - - for (k = 0u; k < ST_SPRITE_CACHE; k++) { - if (gRender.cache[k].used) { - jlSpriteDestroy(gRender.cache[k].sprite); - gRender.cache[k].sprite = 0; - gRender.cache[k].used = false; - } - } -} - - -// Collapse the four flip-book frame glyphs into one tile: every pixel -// is drawn in the reserved palette slot that stands for the set of -// frames it is set in (no frame = colour 0, the background). Animating -// the logo is then a palette write per flip instead of a repaint of -// every cell showing it. Returns false if the frames need more distinct -// sets than there are reserved slots, leaving the ordinary per-cell -// repaint to draw the flip book. -static bool logoBuildTile(const StSimT *sim) { - uint8_t slotOfMask[1u << ST_LOGO_FRAMES]; - uint8_t chunky[TILE_BYTES]; - uint8_t row; - uint8_t col; - - memset(slotOfMask, 0, sizeof(slotOfMask)); - gRender.logoSlotCount = 0u; - for (row = 0u; row < TILE_PIXELS_PER_SIDE; row++) { - uint8_t *out = &chunky[row * TILE_BYTES_PER_ROW]; - for (col = 0u; col < TILE_PIXELS_PER_SIDE; col++) { - uint8_t mask = 0u; - uint8_t f; - uint8_t slot; - for (f = 0u; f < ST_LOGO_FRAMES; f++) { - if (((stSimGlyphRow(sim, (uint8_t)(ST_LOGO_FRAME_FIRST + f), row) >> (7u - col)) & 1u) != 0u) { - mask |= (uint8_t)(1u << f); - } - } - if (mask != 0u && slotOfMask[mask] == 0u) { - if (gRender.logoSlotCount == ST_LOGO_PAL_SLOTS) { - return false; - } - slotOfMask[mask] = kLogoPalSlot[gRender.logoSlotCount]; - gRender.logoSlotMask[gRender.logoSlotCount] = mask; - gRender.logoSlotCount++; - } - slot = slotOfMask[mask]; - if ((col & 1u) == 0u) { - out[col >> 1] = (uint8_t)(slot << 4); - } else { - out[col >> 1] |= slot; - } - } - } - tileFromChunky(&gRender.logoTile, chunky); - return true; -} - - -// Which flip-book frame the live logo glyph holds, or ST_LOGO_FRAMES if -// it matches none of them (then the logo tile does not stand for what -// the simulation is showing and the cells must be repainted normally). -static uint8_t logoFrameIndex(const StSimT *sim) { - uint8_t f; - - for (f = 0u; f < ST_LOGO_FRAMES; f++) { - if (memcmp(stSimGlyph(sim, ST_LOGO_CHAR), - stSimGlyph(sim, (uint8_t)(ST_LOGO_FRAME_FIRST + f)), 8u) == 0) { - return f; - } - } - return ST_LOGO_FRAMES; -} - - -// Show flip-book frame `frame` in the logo's current colour: every -// reserved slot lights up in the colour if that frame is in its set and -// goes to the background colour if it is not. The decoration sprites' -// slot follows their own colour, which the cycle sets one step behind -// the logo's (the title's sprite table overwrites it on entry). -static void logoPaletteApply(const StSimT *sim, uint8_t frame) { - uint16_t pal[SURFACE_COLORS_PER_PALETTE]; - uint16_t on = kC64Palette[sim->logoColor & 15u]; - uint16_t off = kC64Palette[sim->bgColor & 15u]; - uint8_t k; - - jlPaletteGet(gRender.stage, 0u, pal); - for (k = 0u; k < gRender.logoSlotCount; k++) { - pal[kLogoPalSlot[k]] = (((gRender.logoSlotMask[k] >> frame) & 1u) != 0u) ? on : off; - } - pal[ST_PAL_LOGO_SPR] = kC64Palette[sim->frame.color[ST_LOGO_SPR_FIRST] & 15u]; - jlPaletteSet(gRender.stage, 0u, pal); - gRender.logoFrame = frame; - gRender.logoColor = sim->logoColor; - gRender.logoSprColor = sim->frame.color[ST_LOGO_SPR_FIRST]; -} - - -// Repaint the dirty cells: the list, or everything after an overflow -// or a scene change (which also restarts the per-character cell ranges). -static void paintCells(jlSurfaceT *stage, StSimT *sim) { - uint16_t cell; - uint16_t k; - uint8_t bx; - uint8_t by; - - if (sim->dirtyAll) { - cell = 0u; - for (by = 0u; by < ST_SCREEN_ROWS; by++) { - for (bx = 0u; bx < ST_SCREEN_COLS; bx++) { - pasteCell(stage, sim, cell, bx, by); - cell++; - } - } - memset(sim->cellDirty, 0, ST_SCREEN_CELLS); - sim->dirtyCount = 0u; - sim->dirtyAll = false; - return; - } - for (k = 0u; k < sim->dirtyCount; k++) { - cell = sim->dirtyList[k]; - by = cellRow(cell); - bx = cellCol(cell, by); - pasteCell(stage, sim, cell, bx, by); - sim->cellDirty[cell] = 0u; - } - sim->dirtyCount = 0u; -} - - -// Paint one cell: the character's charset rows coloured from colour RAM -// over the background. Also notes the cell in the character's range, -// which is where a changed glyph's cells are looked for. -static void pasteCell(jlSurfaceT *stage, const StSimT *sim, uint16_t cell, uint8_t bx, uint8_t by) { - uint8_t chr = sim->screen[cell]; - - // On the title the logo's cells carry all four flip-book frames at - // once; the palette decides which one shows, so they are pasted as - // they are and never touched again. - if (gRender.logoTileOk && chr == ST_LOGO_CHAR) { - jlTilePaste(stage, bx, by, &gRender.logoTile); - return; - } - jlTilePasteGlyph(stage, bx, by, stSimGlyph(sim, chr), sim->color[cell], sim->bgColor); -} - - -// Light the starfield's ring colours for the current phase: the ring -// the band is on takes kStarRamp[0], the ones just inside it the rest -// of the ramp, and everything else the background. One palette write -// per step and not a single pixel moves -- that is the whole point of -// ST_STAR_RING_FIRST (stSim.h). -static void starPaletteApply(void) { - uint16_t pal[SURFACE_COLORS_PER_PALETTE]; - uint16_t color; - uint8_t behind; - uint8_t ring; - - jlPaletteGet(gRender.stage, 0u, pal); - for (ring = 0u; ring < ST_STAR_RING_COUNT; ring++) { - // How far this ring sits behind the band, wrapping so the band - // re-enters at the centre as it leaves the edge. - behind = (uint8_t)((gRender.starPhase - ring) & (ST_STAR_RING_COUNT - 1u)); - color = (behind < ST_STAR_RAMP_LEN) ? kC64Palette[kStarRamp[behind]] : kC64Palette[0]; - pal[ST_STAR_RING_FIRST + ring] = color; - } - jlPaletteSet(gRender.stage, 0u, pal); -} - - -static bool spriteOnScreen(int16_t px, int16_t py) { - return px < SURFACE_WIDTH && py < SURFACE_HEIGHT && px > -ST_SPRITE_W && py > -(int16_t)ST_SPRITE_PX; -} - - -// A chunky 8x8 tile (four bytes per row, high nibble = left pixel) as -// this port's jlTileT: a straight copy on the chunky ports, drawn and -// snapped through the scratch surface on the planar ones. -static void tileFromChunky(jlTileT *out, const uint8_t *chunky) { -#if defined(JOEYLIB_NATIVE_CHUNKY) - memcpy(out->pixels, chunky, TILE_BYTES); -#else - uint8_t row; - uint8_t k; - - for (row = 0u; row < TILE_PIXELS_PER_SIDE; row++) { - for (k = 0u; k < TILE_BYTES_PER_ROW; k++) { - uint8_t b = chunky[row * TILE_BYTES_PER_ROW + k]; - jlDrawPixel(gRender.scratch, (int16_t)(k * 2u), (int16_t)row, (uint8_t)(b >> 4)); - jlDrawPixel(gRender.scratch, (int16_t)(k * 2u + 1u), (int16_t)row, (uint8_t)(b & 0x0Fu)); - } - } - jlTileSnap(gRender.scratch, 0u, 0u, out); -#endif -} - - -// True when a chunky 8x8 tile has any non-transparent (non-zero) pixel. -static bool tileHasPixels(const uint8_t *chunky) { - uint8_t k; - - for (k = 0u; k < TILE_BYTES; k++) { - if (chunky[k] != 0u) { - return true; - } - } - return false; -} - - -// --------------------------------------------------------------------------- -// Public -// --------------------------------------------------------------------------- - -void stRenderFrame(jlSurfaceT *stage, StSimT *sim) { - uint8_t first = ST_HW_SPRITES; - uint8_t k; - - // Find the first draw slot whose sprite changed, vanished, or sits - // over cells about to be repainted; it and every later slot are - // undrawn (last first) and redrawn below, the rest stay put. - if (gRender.logoMode) { - // The flip book only ever swaps the logo glyph for one of the - // four frames the logo tile already carries: recolour the - // reserved slots and drop the glyph's repaint on the floor. - uint8_t frame = logoFrameIndex(sim); - gRender.logoTileOk = (frame < ST_LOGO_FRAMES); - if (gRender.logoTileOk) { - // The flip book only swapped in a frame the logo tile already - // carries, so drop the repaint it asked for. - if (sim->charDirtyCount == 1u && sim->charDirtyList[0] == ST_LOGO_CHAR) { - sim->charDirtyCount = 0u; - } - if (frame != gRender.logoFrame || sim->logoColor != gRender.logoColor - || sim->frame.color[ST_LOGO_SPR_FIRST] != gRender.logoSprColor) { - logoPaletteApply(sim, frame); - } - } - } - if (gRender.starMode) { - // The warp animates in the palette: no cells change and no sprite - // moves for it. The lit step follows the intro's own frame count, - // so the stars keep the C64's speed whatever the frame rate. - uint8_t phase = (uint8_t)((sim->introStepCount / ST_STAR_FRAMES_PER_STEP) & (ST_STAR_RING_COUNT - 1u)); - - if (phase != gRender.starPhase) { - gRender.starPhase = phase; - starPaletteApply(); - } - } - collectGlyphs(sim); - dirtyBands(sim); - for (k = 0u; k < ST_HW_SPRITES; k++) { - uint8_t idx = (uint8_t)(ST_HW_SPRITES - 1u - k); - StDrawnT *d = &gRender.slot[k]; - uint8_t multi = (uint8_t)((sim->frame.multiMask & kStBit[idx]) != 0u ? 1u : 0u); - int16_t px = (int16_t)((int16_t)sim->frame.x[idx] - ST_SPRITE_X_ORIGIN); - int16_t py = (int16_t)((int16_t)sim->frame.y[idx] - ST_SPRITE_Y_ORIGIN); - bool want = (sim->frame.enableMask & kStBit[idx]) != 0u && spriteOnScreen(px, py); - bool same = (want == d->drawn); - if (same && want) { - same = (d->x == sim->frame.x[idx] && d->y == sim->frame.y[idx] && d->ptr == sim->frame.ptr[idx] && d->color == drawColor(sim, idx) && d->multi == multi); - } - if (same && want && dirtyUnderSprite(px, py)) { - same = false; - } - if (!same) { - first = k; - break; - } - } - for (k = ST_HW_SPRITES; k > first; k--) { - StDrawnT *d = &gRender.slot[k - 1u]; - if (d->drawn) { - if (d->trusted) { - jlSpriteRestoreUnderTrusted(stage, &gRender.backup[k - 1u]); - } else { - jlSpriteRestoreUnder(stage, &gRender.backup[k - 1u]); - } - d->drawn = false; - } - } - paintCells(stage, sim); - for (k = first; k < ST_HW_SPRITES; k++) { - uint8_t idx = (uint8_t)(ST_HW_SPRITES - 1u - k); - StDrawnT *d = &gRender.slot[k]; - StSpriteCacheT *e; - int16_t px; - int16_t py; - if ((sim->frame.enableMask & kStBit[idx]) == 0u) { - continue; - } - px = (int16_t)((int16_t)sim->frame.x[idx] - ST_SPRITE_X_ORIGIN); - py = (int16_t)((int16_t)sim->frame.y[idx] - ST_SPRITE_Y_ORIGIN); - if (!spriteOnScreen(px, py)) { - continue; - } - e = cachedSprite(sim, idx); - if (e == 0) { - continue; - } - px = (int16_t)(px + e->offX); - py = (int16_t)(py + e->offY); - // A compiled cel wholly on the stage takes the trusted calls, - // which skip the per-call geometry checks; one that is clipped - // or interpreted needs the checked ones, and so does its restore. - d->trusted = e->compiled && px >= 0 && py >= 0 && px <= (int16_t)(SURFACE_WIDTH - e->w) && py <= (int16_t)(SURFACE_HEIGHT - e->h); - if (d->trusted) { - jlSpriteSaveAndDrawTrusted(stage, e->sprite, px, py, &gRender.backup[k]); - } else { - jlSpriteSaveAndDraw(stage, e->sprite, px, py, &gRender.backup[k]); - } - d->drawn = true; - d->x = sim->frame.x[idx]; - d->y = sim->frame.y[idx]; - d->ptr = sim->frame.ptr[idx]; - d->color = drawColor(sim, idx); - d->multi = (uint8_t)((sim->frame.multiMask & kStBit[idx]) != 0u ? 1u : 0u); - } - { - // Sync to the retrace unless the frame already spans more than - // one: a late frame goes out at once rather than waiting again. - uint8_t now = (uint8_t)jlFrameCount(); - if ((uint8_t)(now - gRender.lastPresentFrame) < 2u) { - jlWaitVBL(); - } - jlStagePresent(); - gRender.lastPresentFrame = (uint8_t)jlFrameCount(); - } -} - - -void stRenderInit(jlSurfaceT *stage) { - uint8_t *backupMem = gRender.backupMem; - uint8_t k; - - memset(&gRender, 0, sizeof(gRender)); - if (backupMem == 0) { - backupMem = (uint8_t *)jlAlloc((uint32_t)ST_HW_SPRITES * ST_SPRITE_BACKUP_BYTES); - if (backupMem == 0) { - ST_LOG("stRenderInit: no memory for sprite backups"); - } - } - gRender.backupMem = backupMem; - gRender.stage = stage; - for (k = 0u; k < ST_HW_SPRITES; k++) { - gRender.backup[k].bytes = (backupMem == 0) ? 0 : backupMem + (uint16_t)k * ST_SPRITE_BACKUP_BYTES; - } - jlPaletteSet(stage, 0u, kC64Palette); - jlScbSetRange(stage, 0u, (uint16_t)(SURFACE_HEIGHT - 1u), 0u); - jlSurfaceClear(stage, 0u); - gRender.scratch = jlSurfaceCreate(); - if (gRender.scratch != 0) { - jlPaletteSet(gRender.scratch, 0u, kC64Palette); - jlScbSetRange(gRender.scratch, 0u, (uint16_t)(SURFACE_HEIGHT - 1u), 0u); - jlSurfaceClear(gRender.scratch, 0u); - } -} - - -// The first unused cache slot, or NULL when the cache is full. -static StSpriteCacheT *cacheFreeSlot(void) { - uint8_t k; - - for (k = 0u; k < ST_SPRITE_CACHE; k++) { - if (!gRender.cache[k].used) { - return &gRender.cache[k]; - } - } - return 0; -} - - -// Drop every cel that belongs to a level (the baked set is level 0xFF and -// stays), freeing its arena code, and forget any hardware sprite's last -// hit on it. -static void levelCelsEvict(void) { - uint8_t k; - - for (k = 0u; k < ST_SPRITE_CACHE; k++) { - StSpriteCacheT *e = &gRender.cache[k]; - uint8_t h; - if (!e->used || e->level == 0xFFu) { - continue; - } - for (h = 0u; h < ST_HW_SPRITES; h++) { - if (gRender.lastHit[h] == e) { - gRender.lastHit[h] = 0; - } - } - jlSpriteDestroy(e->sprite); - e->sprite = 0; - e->used = false; - e->pinned = false; - e->compiled = false; - } -} - - -// A level's own sprite block, or NULL when the level does not ship it. -static const uint8_t *levelSpriteBitmap(const StLevelT *level, uint8_t ptr) { - uint8_t k; - - for (k = 0u; k < level->spriteCount; k++) { - if (level->sprites[k].ptr == ptr) { - return level->sprites[k].bitmap; - } - } - return 0; -} - - -static void loadingBar(jlSurfaceT *stage, uint8_t done, uint8_t total) { - jlFillRect(stage, 60, 110, (uint16_t)((uint16_t)done * 200u / total), 6u, 1u); - jlStagePresent(); -} - - -// Ready a level's hook cels BEFORE the ride: the previous level's cels -// go, then the level's list (stLevelCelList) is filled from its baked -// bank (sprites/levelNN.spc) or, when there is none, built and compiled -// here behind the LOADING bar. Either way nothing compiles once the -// level is running. Keyed exactly as cachedSprite keys them, so every -// lookup during the ride is a hit. -bool stRenderLevelCels(jlSurfaceT *stage, const StLevelT *level) { - static StCelDefT list[ST_LEVEL_CELS_MAX]; - jlSpriteT *cels[ST_LEVEL_CELS_MAX]; - char path[24]; - uint8_t n; - uint8_t got; - uint8_t ready = 0u; - uint8_t k; - - levelCelsEvict(); - if (level == 0) { - return true; - } - n = stLevelCelList(level, list, ST_LEVEL_CELS_MAX); - if (n == 0u) { - return true; - } - memcpy(path, "sprites/level", 13u); - path[13] = (char)('0' + (level->levelIndex + 1u) / 10u); - path[14] = (char)('0' + (level->levelIndex + 1u) % 10u); - memcpy(path + 15, ".spc", 5u); - got = (uint8_t)jlSpriteBankLoadPrecompiled(path, cels, n, 0); - if (got == n) { - ST_LOG("spacetaxi: level %u cels from %s (%u)", level->levelIndex, path, n); - } else { - for (k = 0u; k < got; k++) { - jlSpriteDestroy(cels[k]); - } - ST_LOG("spacetaxi: level %u: no usable %s -- compiling %u cels at entry", level->levelIndex, path, n); - stRenderLoading(stage); - } - gPrewarming = true; - for (k = 0u; k < n; k++) { - StSpriteCacheT *slot = cacheFreeSlot(); - if (slot == 0) { - ST_LOG("spacetaxi: ! sprite cache full at level cel %u", k); - break; - } - if (got == n) { - slot->sprite = cels[k]; - slot->offX = 0u; - slot->offY = 0u; - slot->w = ST_SPRITE_PX; - slot->h = ST_SPRITE_PX; - slot->compiled = (jlSpriteCompiledSize(cels[k]) != 0u); - } else { - const uint8_t *bm = levelSpriteBitmap(level, list[k].ptr); - if (bm == 0) { - continue; - } - if (!celCreate(slot, bm, list[k].multi, list[k].color, list[k].mc0, list[k].mc1)) { - continue; - } - slot->compiled = false; - slot->retryAt = 0u; - cacheCompile(slot); - loadingBar(stage, (uint8_t)(k + 1u), n); - } - slot->ptr = list[k].ptr; - slot->color = list[k].color; - slot->multi = list[k].multi; - slot->mc0 = list[k].mc0; - slot->mc1 = list[k].mc1; - slot->level = level->levelIndex; - slot->used = true; - slot->pinned = true; - slot->lastUse = gRender.cacheStamp; - ready++; - } - gPrewarming = false; - if (got != n) { - jlFillRect(stage, 60, 110, 200u, 6u, 0u); - } - if (ready < n) { - ST_LOG("spacetaxi: ! level %u has %u of %u cels", level->levelIndex, ready, n); - return false; - } - return true; -} - - -// Load the offline-baked, already-compiled cel bank into the pinned -// cache so no cel is JIT-compiled at boot (each 65816 compile costs -// ~0.4 s). Every kStCels entry becomes a pinned cache slot keyed exactly -// as the play lookups: system ptr (level 0xFF), the level-wide colours. -// Returns false if the .spc is missing or built for another target/shift -// count (a stale bake), so the caller falls back to the runtime JIT -// prewarm below. Every port stages its own bake (make/.mk). -static bool loadPrecompiledCels(void) { - jlSpriteT *cels[64]; - uint16_t n; - uint16_t i; - - if (kStCelCount > (uint16_t)(sizeof(cels) / sizeof(cels[0])) || kStCelCount > ST_SPRITE_CACHE) { - return false; - } - n = jlSpriteBankLoadPrecompiled("sprites/staxicels.spc", cels, kStCelCount, 0); - if (n < kStCelCount) { - for (i = 0u; i < n; i++) { - jlSpriteDestroy(cels[i]); - } - return false; - } - for (i = 0u; i < n; i++) { - StSpriteCacheT *e = &gRender.cache[i]; - e->sprite = cels[i]; - e->ptr = kStCels[i].ptr; - e->color = kStCels[i].color; - e->multi = kStCels[i].multi; - e->mc0 = kStCels[i].mc0; - e->mc1 = kStCels[i].mc1; - e->level = 0xFFu; - e->offX = 0u; - e->offY = 0u; - e->w = ST_SPRITE_PX; - e->h = ST_SPRITE_PX; - e->used = true; - e->pinned = true; - e->lastUse = 0u; - // The bank's cels arrive compiled ONLY if the arena had room for - // them: jlSpriteBankLoadPrecompiled loads a cel either way and - // leaves the ones that did not fit to the interpreter. So ask - // the sprite instead of assuming, or the flag lies and any - // future cacheCompile skips a cel that really does need work. - e->compiled = (jlSpriteCompiledSize(e->sprite) != 0u); - } - return true; -} - - -// A fatal message, centred, in the game's own charset. Used when an -// asset that IS on the disk will not load: the game then stops instead -// of playing on in a degraded state, because a mute game or one running -// every sprite through the interpreter looks like a slow machine rather -// than a broken install. -void stRenderFatal(jlSurfaceT *stage, const char *line1, const char *line2) { - const uint8_t *charset = stC64Charset(); - const char *lines[2]; - uint8_t i; - uint8_t k; - - if (stage == 0) { - return; - } - lines[0] = line1; - lines[1] = line2; - jlSurfaceClear(stage, 0u); - for (i = 0u; i < 2u; i++) { - uint8_t len; - uint8_t col; - - if (lines[i] == 0) { - continue; - } - len = 0u; - while (lines[i][len] != '\0' && len < ST_SCREEN_COLS) { - len++; - } - col = (uint8_t)((ST_SCREEN_COLS - len) / 2u); - for (k = 0u; k < len; k++) { - jlTilePasteGlyph(stage, (uint8_t)(col + k), (uint8_t)(ST_LOADING_ROW + i * 2u), - &charset[(uint8_t)lines[i][k] * 8u], ST_LOADING_COLOR, 0u); - } - } - jlStagePresent(); -} - - -// "LOADING..." centred on an otherwise black stage. The charset is the -// game's own, indexed by ASCII (the C64 font's letters sit at their -// ASCII codes), so this needs no font surface and no simulation state. -void stRenderLoading(jlSurfaceT *stage) { - static const char kLoading[] = "LOADING..."; - const uint8_t *charset = stC64Charset(); - uint8_t col = (uint8_t)((ST_SCREEN_COLS - (sizeof(kLoading) - 1u)) / 2u); - uint8_t k; - - jlSurfaceClear(stage, 0u); - for (k = 0u; k < (uint8_t)(sizeof(kLoading) - 1u); k++) { - jlTilePasteGlyph(stage, (uint8_t)(col + k), ST_LOADING_ROW, - &charset[(uint8_t)kLoading[k] * 8u], ST_LOADING_COLOR, 0u); - } - jlStagePresent(); -} - - -// Build (and compile) the cels the game draws every screen -- cab, -// exhaust, passenger, wreck and intro cab -- up front, so no frame pays -// for a sprite build mid-play. A bar on the stage shows progress on the -// slower ports. -bool stRenderPrewarm(jlSurfaceT *stage, StSimT *sim) { - static const uint8_t kCabPtrs[] = { 0xC0, 0xC1, 0xDC, 0xDD, 0xCC, 0xCD, 0xCE, 0xCF, 0xD0, 0xD1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8 }; - static const uint8_t kPassPtrs[] = { 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xD9 }; - static const uint8_t kFlamePtrs[] = { 0xD8, 0xD4, 0xD5, 0xD2, 0xD1, 0xD7, 0xD3, 0xD6 }; - uint8_t total = (uint8_t)(sizeof(kCabPtrs) + sizeof(kPassPtrs) + sizeof(kFlamePtrs)); - uint8_t done = 0u; - bool ok; - uint8_t k; - StFrameT saved; - - // Precompiled bank: instant, no per-cel JIT. Falls through to the - // runtime build below on any port without a matching .spc. - if (loadPrecompiledCels()) { - ST_LOG("spacetaxi: cels from sprites/staxicels.spc"); - return prewarmReport(kStCelCount); - } - ST_LOG("spacetaxi: ! no usable sprites/staxicels.spc -- JIT prewarm"); - saved = sim->frame; - gPrewarming = true; - sim->frame.multiMask = 0x07u; - sim->spriteMc0 = 0x02u; - sim->spriteMc1 = 0x07u; - for (k = 0u; k < sizeof(kCabPtrs); k++) { - sim->frame.ptr[0] = kCabPtrs[k]; - sim->frame.color[0] = 0x06u; - (void)cachedSprite(sim, 0u); - done++; - loadingBar(stage, done, total); - } - for (k = 0u; k < sizeof(kPassPtrs); k++) { - sim->frame.ptr[1] = kPassPtrs[k]; - sim->frame.color[1] = 0x06u; - (void)cachedSprite(sim, 1u); - done++; - loadingBar(stage, done, total); - } - for (k = 0u; k < sizeof(kFlamePtrs); k++) { - sim->frame.ptr[2] = kFlamePtrs[k]; - sim->frame.color[2] = 0x07u; - (void)cachedSprite(sim, 2u); - done++; - loadingBar(stage, done, total); - } - sim->frame = saved; - gPrewarming = false; - ok = prewarmReport(total); - jlFillRect(stage, 60, 110, 200u, 6u, 0u); - return ok; -} - - -// Say how the prewarmed cels came out, and judge them. A cel past the end -// of the codegen arena still DRAWS, interpreted, so a port whose arena -// holds only some of the cels is slower but sound -- that is the ST at -// 2 MB, 12 of 37 compiled, and it is not an error. Zero compiled is: -// it means the arena itself never came, which on a 1 MB ST is the speech -// blob having taken the heap, and every sprite then runs about forty -// times slower. The caller stops on a false rather than let that pass -// for a slow machine. -static bool prewarmReport(uint16_t expect) { - uint32_t bytes = 0u; - uint8_t used = 0u; - uint8_t hot = 0u; - uint8_t k; - - for (k = 0u; k < ST_SPRITE_CACHE; k++) { - uint32_t n; - - if (!gRender.cache[k].used) { - continue; - } - n = jlSpriteCompiledSize(gRender.cache[k].sprite); - used++; - if (n != 0u) { - hot++; - bytes += n; - } - } - ST_LOG("spacetaxi: prewarm %u cels: %u compiled, %u interpreted, %lu arena bytes", used, hot, (uint8_t)(used - hot), (unsigned long)bytes); - if (used < expect) { - ST_LOG("spacetaxi: ! only %u of %u cels built", used, expect); - return false; - } - return hot != 0u; -} - - -// A new scene: nothing drawn is valid any more. -void stRenderSceneChanged(StSimT *sim) { - uint8_t k; - - if (gRender.logoMode || gRender.starMode) { - // The borrowed slots go back to being C64 colours. - gRender.logoMode = false; - gRender.logoTileOk = false; - gRender.starMode = false; - jlPaletteSet(gRender.stage, 0u, kC64Palette); - } - for (k = 0u; k < ST_HW_SPRITES; k++) { - gRender.slot[k].drawn = false; - } - stSimDirtyCells(sim); - sim->charDirtyAll = true; - sim->charDirtyCount = 0u; -} - - -// Turn on the title's palette-animated logo. Call it after -// stRenderSceneChanged (which turns it back off) on entry to the title -// screen; from then on a flip-book step and a colour cycle both cost -// one palette write and no cell repaint at all. -void stRenderTitleLogo(StSimT *sim) { - uint8_t frame; - - if (!logoBuildTile(sim)) { - return; - } - frame = logoFrameIndex(sim); - gRender.logoMode = true; - gRender.logoTileOk = (frame < ST_LOGO_FRAMES); - logoPaletteApply(sim, gRender.logoTileOk ? frame : 0u); - stSimDirtyCells(sim); -} - - -// Turn on the level intro's palette-cycled starfield. Call it after -// stRenderSceneChanged (which turns it back off) on entry to the level -// intro. The cells were painted by stIntroEnter; from here the warp is -// one palette write every ST_STAR_FRAMES_PER_STEP intro frames. -void stRenderIntroStars(void) { - gRender.starMode = true; - gRender.starPhase = 0u; - starPaletteApply(); -} - - -void stRenderShutdown(void) { - dropCache(); - if (gRender.scratch != 0) { - jlSurfaceDestroy(gRender.scratch); - gRender.scratch = 0; - } - jlFree(gRender.backupMem); - gRender.backupMem = 0; -} diff --git a/examples/spacetaxi/stSim.c b/examples/spacetaxi/stSim.c deleted file mode 100644 index de974ac..0000000 --- a/examples/spacetaxi/stSim.c +++ /dev/null @@ -1,1713 +0,0 @@ -// Space Taxi -- the game tick ($5F40 main loop), physics, landing, -// collision, crash sequence, fuel and HUD arithmetic. -// -// Every function names the original routine it mirrors. The order of -// calls inside stSimTick is the order of the JSRs in the C64 main -// loop; the two raster waits that split it are where the VIC latched -// the frame, so the sprite snapshot (marshal) and the collision test -// sit at the same places. - -#include - -#include "stSim.h" - -// Jingle selection ($6935 / $70D4). Song numbers index kStSongs. -#define ST_JINGLE_SONG_BONUS 4u // level-25 start and the bonus cab -#define ST_JINGLE_LEVEL_25 0x19u // $7215 compared against #$19 - -// Screen-code strings the engine writes (all ASCII-compatible glyphs). -// $6C38 -- the 11-character blank that clears the message row. -static const uint8_t kTextBlank[] = " "; -// $43B1 -- the fare meter's blank template: three leading blanks, then -// "0.00" in the inverse-video digit glyphs. -static const uint8_t kHudTemplate[ST_NUMBER_CHARS] = { 0x66, 0x66, 0x66, 0x74, 0x77, 0x74, 0x74 }; -// Fuel-pump chirp frequencies while refuelling ($6DFA, indexed by the -// mod-8 pump tick 1..4). -static const uint8_t kPumpFreq[5] = { 0x04, 0x05, 0x07, 0x0A, 0x10 }; -const uint8_t kStBit[ST_HW_SPRITES] = { 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80 }; - - -static void applyVelocityX(StSimT *sim); -static void applyVelocityY(StSimT *sim); -static bool backgroundRowHits(const StSimT *sim, int16_t firstCol, int16_t py, const uint8_t *w); -static bool cabRowsTouchBackground(StSimT *sim, int16_t firstCol, int16_t py, uint8_t phase); -static const uint8_t *cabWindows(StSimT *sim, uint8_t s); -static int16_t cellOfPixel(int16_t px); -static void collisionDispatch(StSimT *sim); -static void collisionPhase1(StSimT *sim); -static void collisionPhase2(StSimT *sim); -static StTickResultE collisionPhase3(StSimT *sim); -static void computeCollisions(StSimT *sim); -static bool coveringCellsSolid(const StSimT *sim, int16_t firstCol, int16_t topRow, int16_t botRow); -static void crashStart(StSimT *sim); -static void drawCabIcons(StSimT *sim); -static void drawScreensCount(StSimT *sim); -static void edgeReflect(StSimT *sim); -static void fireButtonEdge(StSimT *sim); -static void fuelBarHud(StSimT *sim); -static void fuelTick(StSimT *sim); -static void hudDraw(StSimT *sim); -static void landedHandler(StSimT *sim); -static bool levelEndCheck(const StSimT *sim); -static void markCell(StSimT *sim, uint16_t cell); -static void padDetect(StSimT *sim); -static void padLandingBob(StSimT *sim); -static void passengerArrTick(StSimT *sim); -static void physicsTick(StSimT *sim); -static uint8_t readInput(StSimT *sim); -static void rowWindow(const uint8_t *row, uint8_t s, uint8_t *w); -static void setCell(StSimT *sim, uint16_t cell, uint8_t ch); -static void setColorCell(StSimT *sim, uint16_t cell, uint8_t color); -static void spriteMasks(StSimT *sim, uint8_t idx); -static bool spriteRowsTouchBackground(const StSimT *sim, uint8_t idx, int16_t firstCol, int16_t py, uint8_t phase); -static bool spriteTouchesBackground(StSimT *sim, uint8_t idx); -static void spritesOffReset(StSimT *sim); -static void taxiSpawnInit(StSimT *sim); -static void taxiSpriteCelSelect(StSimT *sim); -static uint8_t validateDigit(uint8_t ch); - - -// $6112 -- add the X velocity to the 17-bit X position (msb:col:frac) -// and refresh the sprite-0 column shadow. -static void applyVelocityX(StSimT *sim) { - uint16_t pos = (uint16_t)(((uint16_t)sim->posXcol << 8) | sim->posXlo); - uint32_t sum = (uint32_t)pos + (uint16_t)sim->velX; - uint8_t msb = (uint8_t)(sim->posXmsb + (uint8_t)(sum >> 16)); - - if (sim->velX < 0) { - msb++; - } - sim->posXmsb = (uint8_t)(msb & 1u); - sim->posXlo = (uint8_t)sum; - sim->posXcol = (uint8_t)(sum >> 8); - sim->spr[0].msb = sim->posXmsb; - sim->spr[0].col = sim->posXcol; -} - - -// $6145 -- Y: velocity += accel + gravity, position += velocity. -static void applyVelocityY(StSimT *sim) { - uint16_t pos; - - sim->velY = (int16_t)((uint16_t)sim->accelY + sim->gravTemplateY + (uint16_t)sim->velY); - pos = (uint16_t)(((uint16_t)sim->posYrow << 8) | sim->posYlo); - pos = (uint16_t)(pos + (uint16_t)sim->velY); - sim->posYlo = (uint8_t)pos; - sim->posYrow = (uint8_t)(pos >> 8); - sim->spr[0].row = sim->posYrow; -} - - -// Whether a 4-byte row window (rowWindow: byte j covers character -// column firstCol + j) meets character graphics on screen pixel row py. -static bool backgroundRowHits(const StSimT *sim, int16_t firstCol, int16_t py, const uint8_t *w) { - uint8_t glyphRow; - uint8_t j; - - if (py < 0 || py >= (int16_t)(ST_SCREEN_ROWS * 8u)) { - return false; - } - glyphRow = (uint8_t)(py & 7); - for (j = 0u; j < 4u; j++) { - int16_t col = (int16_t)(firstCol + (int16_t)j); - if (w[j] != 0u && col >= 0 && col < (int16_t)ST_SCREEN_COLS) { - if ((stSimGlyphRow(sim, sim->screen[ST_CELL((uint16_t)py >> 3, (uint16_t)col)], glyphRow) & w[j]) != 0u) { - return true; - } - } - } - return false; -} - - -// The cab's 21 cached row windows at grid phase `phase` against the -// background rows under it. -static bool cabRowsTouchBackground(StSimT *sim, int16_t firstCol, int16_t py, uint8_t phase) { - uint8_t first = sim->collRowFirst[0]; - uint8_t left = (uint8_t)(sim->collRowLast[0] + 1u - first); - const uint8_t *win; - - if (first == ST_SPRITE_H) { - return false; - } - win = cabWindows(sim, phase) + (uint16_t)first * 4u; - py = (int16_t)(py + first); - // Counted down to zero: `dec a / bne` needs no compare. (It also - // dodged the llvm816 bug, fixed 2026-09-22, where the up-counting form - // compiled to `cmp #end / inc a / bne` with the inc trampling the - // compare's Z, so the loop ran past row 20 into the next phase's - // windows -- the false landing crash in the IIgs "Puzzles" demo.) - do { - if (backgroundRowHits(sim, firstCol, py, win)) { - return true; - } - win += 4; - py++; - left--; - } while (left != 0u); - return false; -} - - -// The cab's 21 row windows at grid phase s, 4 bytes per row, built on -// first use after the cab's mask changed and cached until it changes -// again. The cab is one side of every collision test, so no test -// shifts a row (a variable 32-bit shift is a library bit loop on the -// 65816 and this was the collision test's whole cost). -static const uint8_t *cabWindows(StSimT *sim, uint8_t s) { - uint8_t (*win)[4] = sim->cabWin[s]; - uint8_t r; - - if ((sim->cabWinValid & (uint8_t)(1u << s)) == 0u) { - for (r = 0u; r < ST_SPRITE_H; r++) { - rowWindow(sim->collMask[0][r], s, win[r]); - } - sim->cabWinValid |= (uint8_t)(1u << s); - } - return &win[0][0]; -} - - -// Which character column (or row) a pixel coordinate falls in: floor -// division by 8. A shift, not a divide -- the 65816 has no divider and -// the compiler's signed-division helper cost more than the collision -// test that calls it. The +256/-32 bias makes the shift exact for any -// coordinate down to -256, well past the furthest a sprite can hang off -// the left or top edge. -static int16_t cellOfPixel(int16_t px) { - return (int16_t)(((px + 256) >> 3) - 32); -} - - -// $6966 -- latch the frame's collisions and run the crash phases. -static void collisionDispatch(StSimT *sim) { - computeCollisions(sim); - if (sim->collisionPhase != 0u) { - // Phases 1..3 dispatch elsewhere (stSimTick handles 3's exit). - return; - } - stFarePadLightingGate(sim); - if ((sim->spriteBgColl & 1u) == 0u) { - if ((sim->spriteSpriteColl & 1u) == 0u) { - return; - } - if (sim->deathInProgress != 0u) { - return; - } - if ((sim->spriteSpriteColl & 2u) != 0u) { - if ((sim->spriteSpriteColl & 0xF8u) != 0u) { - return; - } - stFareSquashed(sim); - } - if ((sim->spriteSpriteColl & 0xF8u) == 0u) { - return; - } - } - // $6A01: the level's verdict on sprite contact; background contact - // always kills. - sim->hitDispatchResult = stHookHitVerdict(sim); - if ((sim->spriteBgColl & 1u) == 0u) { - if (sim->hitDispatchResult == 0u) { - return; - } - } - crashStart(sim); -} - - -// $6A72 -- the wreck falls: every second tick sweep the scream, flip -// the debris cel, jitter, reflect off the side walls, drift with the -// old X velocity and accelerate down until row $DA. -static void collisionPhase1(StSimT *sim) { - uint8_t jitter; - uint16_t sum; - - if ((sim->hitDispatchResult & 0x80u) == 0u) { - sim->phaseTimer--; - if (sim->phaseTimer != 0u) { - return; - } - sim->phaseTimer = sim->phaseReload; - sim->thrustSweep--; - stAudioThrustSweep((uint8_t)(sim->thrustSweep >> 1)); - sim->spr[0].ptr ^= 1u; - // $6A98: rng(5) - 3 added to the sprite column (msb follows). - jitter = (uint8_t)(stSimRng(sim, 5u) - 3u); - stSimSpriteAddX(sim, 0u, jitter); - edgeReflect(sim); - applyVelocityX(sim); - // vy += 40 (low byte with carry into the high byte), row += vy hi. - sum = (uint16_t)((uint16_t)(sim->velY & 0xFFu) + 0x28u); - sim->velY = (int16_t)((uint16_t)(((uint16_t)sim->velY & 0xFF00u) + (sum & 0x100u)) | (sum & 0xFFu)); - sim->spr[0].row = (uint8_t)(sim->spr[0].row + (uint8_t)((uint16_t)sim->velY >> 8)); - if (sim->spr[0].row < 0xDAu) { - return; - } - sim->spr[0].row = 0xDAu; - } - // $6AD6: hit the floor -> phase 2 with a fresh 2-tick cadence. - sim->collisionPhase++; - sim->phaseReload = 2u; - sim->phaseTimer = 2u; - stAudioSfx(stC64SfxProgram(ST_SFX_IMPACT)); -} - - -// $6B24 -- walk the wreck cels $CC..$D1 at a slowing rate, then hide. -static void collisionPhase2(StSimT *sim) { - sim->phaseTimer--; - if (sim->phaseTimer != 0u) { - return; - } - sim->phaseReload++; - sim->phaseTimer = sim->phaseReload; - sim->spr[0].ptr++; - if (sim->spr[0].ptr != ST_SPRITE_WRECK_LAST) { - return; - } - sim->spr[0].enable = 0u; - sim->collisionPhase++; - sim->phaseTimer = 0x46u; -} - - -// $6B4C -- 70-tick pause, then the life bookkeeping. -static StTickResultE collisionPhase3(StSimT *sim) { - uint8_t p; - - sim->phaseTimer--; - if (sim->phaseTimer != 0u) { - return ST_TICK_CONTINUE; - } - if (sim->demoMode != 0u) { - return ST_TICK_CRASH_DONE; - } - if (sim->stage == ST_STAGE_RIDING) { - stSimHudInit(sim); - if (sim->fareSlotCount != 0u) { - sim->spriteSlots[sim->activeDyingSlot] = 0u; - sim->fareSlotCount--; - if (sim->activeSpriteIdx == ST_FARE_DEST_UP) { - uint8_t k; - for (k = 0u; k < 4u; k++) { - setCell(sim, (uint16_t)(ST_CELL_TRANSPORTER + k), ST_CHAR_TRANSPORTER); - } - } else { - sim->spriteSlots[sim->activeSpriteIdx] = 0u; - sim->fareSlotCount--; - } - } - sim->activeSpriteIdx = 0u; - sim->stage = ST_STAGE_IDLE; - stSimClearMessage(sim); - } - // $6BBA - p = sim->player; - sim->cabs[p]--; - if (sim->cabs[p] == 0u) { - return ST_TICK_PLAYER_OUT; - } - setCell(sim, (uint16_t)(ST_CELL_CAB_ICONS + sim->cabs[p]), ST_CHAR_BLANK); - return ST_TICK_LIFE_LOST; -} - - - -// The C64 reads two VIC collision registers each frame: $D01F (a sprite -// touched the background) and $D01E (two sprites touched). Only the cab -// (sprite 0) can drive a gameplay outcome from them: the crash dispatch -// returns unless the cab or the background is touched, and the hooks -// read a bit set by cab contact -- except levels J and P, which watch -// their own hazards hit the scenery and opt those sprites in through -// bgCollSprites. So the full all-pairs register is not reproduced -- -// only sprite 0 versus the background and versus the other sprites, -// plus the opted-in sprites versus the background, and the exhaust -// flame (sprite 2, the cab's own, always overlapping it) is skipped. -// Non-cab pairs (hazard vs hazard, passenger vs hazard with the cab -// clear) never change the dispatch outcome, so their bits are left -// unset. Proven against all four demo traces: the observable state -// (positions, crash phase, score) is byte-identical. -static void computeCollisions(StSimT *sim) { - uint8_t ss = 0u; - uint8_t bg = 0u; - uint8_t b; - uint8_t r; - - for (b = 1u; b < ST_HW_SPRITES; b++) { - if ((sim->bgCollSprites & kStBit[b]) == 0u || (sim->frame.enableMask & kStBit[b]) == 0u) { - continue; - } - spriteMasks(sim, b); - if (spriteTouchesBackground(sim, b)) { - bg |= kStBit[b]; - } - } - if ((sim->frame.enableMask & 1u) == 0u) { - sim->spriteSpriteColl = 0u; - sim->spriteBgColl = bg; - return; - } - spriteMasks(sim, 0u); - if (spriteTouchesBackground(sim, 0u)) { - bg = 1u; - } - for (b = 1u; b < ST_HW_SPRITES; b++) { - int16_t dx; - int16_t dy; - uint8_t s; - int16_t q; - uint8_t kLo; - uint8_t kHi; - uint8_t rLo; - uint8_t rHi; - int16_t lo; - int16_t hi; - const uint8_t *win; - const uint8_t *bRow; - // Skip the exhaust flame (sprite 2) and any disabled sprite. - if (b == 2u || (sim->frame.enableMask & kStBit[b]) == 0u) { - continue; - } - dx = (int16_t)((int16_t)sim->frame.x[b] - (int16_t)sim->frame.x[0]); - dy = (int16_t)((int16_t)sim->frame.y[b] - (int16_t)sim->frame.y[0]); - if (dx <= -ST_SPRITE_W || dx >= ST_SPRITE_W || dy <= -ST_SPRITE_H || dy >= ST_SPRITE_H) { - continue; - } - spriteMasks(sim, b); - // Sprite b sits dx pixels right of the cab; with dx = 8q - s - // (s in 0..7) its row byte k lines up with byte k + q of the - // cab's phase-s windows, and only 0 <= k + q <= 3 can overlap. - s = (uint8_t)((0 - dx) & (ST_ROW_PHASES - 1)); - q = (int16_t)(((dx + (int16_t)s + 32) >> 3) - 4); - kLo = (q < 0) ? (uint8_t)(0 - q) : 0u; - kHi = (q > 1) ? (uint8_t)(3 - q) : 2u; - // Rows r of the cab meet rows r - dy of sprite b; walk only the - // rows where both have pixels, both pointers advancing in step. - if (sim->collRowFirst[b] == ST_SPRITE_H || sim->collRowFirst[0] == ST_SPRITE_H) { - continue; - } - lo = (int16_t)sim->collRowFirst[0]; - hi = (int16_t)(sim->collRowLast[0] + 1); - if ((int16_t)sim->collRowFirst[b] + dy > lo) { - lo = (int16_t)((int16_t)sim->collRowFirst[b] + dy); - } - if ((int16_t)sim->collRowLast[b] + 1 + dy < hi) { - hi = (int16_t)((int16_t)sim->collRowLast[b] + 1 + dy); - } - if (lo >= hi) { - continue; - } - rLo = (uint8_t)lo; - rHi = (uint8_t)hi; - win = cabWindows(sim, s) + (uint16_t)rLo * 4u; - bRow = sim->collMask[b][(uint8_t)((int16_t)rLo - dy)]; - for (r = rLo; r < rHi; r++, win += 4, bRow += 3) { - bool hit = false; - uint8_t k; - for (k = kLo; k <= kHi; k++) { - if ((bRow[k] & win[(uint8_t)((int16_t)k + q)]) != 0u) { - hit = true; - break; - } - } - if (hit) { - ss |= (uint8_t)(1u | kStBit[b]); - break; - } - } - } - sim->spriteSpriteColl = ss; - sim->spriteBgColl = bg; -} - - -// Whether any character cell in rows topRow..botRow, columns firstCol.. -// firstCol+3 (the cells a sprite at that spot covers) is non-blank. -static bool coveringCellsSolid(const StSimT *sim, int16_t firstCol, int16_t topRow, int16_t botRow) { - int16_t rr; - int16_t cc; - - for (rr = topRow; rr <= botRow; rr++) { - for (cc = firstCol; cc <= (int16_t)(firstCol + 3); cc++) { - if (cc >= 0 && cc < (int16_t)ST_SCREEN_COLS && sim->screen[ST_CELL((uint16_t)rr, (uint16_t)cc)] != ST_CHAR_SPACE) { - return true; - } - } - } - return false; -} - - -// $6A2B -- start the wreck sequence. -static void crashStart(StSimT *sim) { - sim->spr[0].ptr = ST_SPRITE_WRECK_FIRST; - sim->collisionPhase++; - sim->phaseTimer = 2u; - sim->phaseReload = 2u; - stAudioNoise(false); - sim->eventDispatchType = 0u; - sim->activePad = 0u; - sim->spr[2].enable = 0u; - sim->spr[2].row = 0u; - sim->velY = 0x0303; - stAudioSfx(stC64SfxProgram(ST_SFX_CRASH)); - sim->thrustSweep = 0xA0u; - if (sim->stage == ST_STAGE_WALK_TO_CAB) { - sim->stage--; - } -} - - -// $6372 -- one cab icon per spare cab (cabs - 1 of them). -static void drawCabIcons(StSimT *sim) { - uint8_t k = sim->cabs[sim->player]; - - while (k > 1u) { - k--; - setCell(sim, (uint16_t)(ST_CELL_CAB_ICONS + k), ST_CHAR_CAB_ICON); - } -} - - -// $6384 -- screens completed, or the finished marker past 25. -static void drawScreensCount(StSimT *sim) { - uint8_t v = sim->levelState; - uint8_t tens = 0u; - - if (v >= 0x19u) { - setCell(sim, (uint16_t)(ST_CELL_SCREENS + 0u), 0xCBu); - setCell(sim, (uint16_t)(ST_CELL_SCREENS + 1u), 0xCCu); - setCell(sim, (uint16_t)(ST_CELL_SCREENS + 2u), 0xCDu); - setCell(sim, (uint16_t)(ST_CELL_SCREENS + 3u), 0xCEu); - setCell(sim, (uint16_t)(ST_CELL_SCREENS + 4u), 0xCFu); - return; - } - setCell(sim, (uint16_t)(ST_CELL_SCREENS + 2u), 0xCAu); - setCell(sim, (uint16_t)(ST_CELL_SCREENS + 3u), ST_CHAR_ZERO); - setCell(sim, (uint16_t)(ST_CELL_SCREENS + 4u), ST_CHAR_ZERO); - while (v >= 10u) { - v = (uint8_t)(v - 10u); - tens++; - } - setCell(sim, (uint16_t)(ST_CELL_SCREENS + 1u), (v == 0u) ? ST_CHAR_ZERO : (uint8_t)(ST_CHAR_DIGIT_BASE + v)); - setCell(sim, (uint16_t)(ST_CELL_SCREENS + 0u), (tens == 0u) ? ST_CHAR_BLANK : (uint8_t)(ST_CHAR_DIGIT_BASE + tens)); -} - - -// $6AED -- negate the X velocity when the wreck crosses column 23 -// leftward or column 65 (in the high half) rightward. -static void edgeReflect(StSimT *sim) { - bool reflect = false; - - if (sim->spr[0].msb == 0u) { - if (sim->velX < 0 && sim->spr[0].col < 0x17u) { - reflect = true; - } - } else { - if (sim->velX >= 0 && sim->spr[0].col >= 0x41u) { - reflect = true; - } - } - if (reflect) { - sim->velX = (int16_t)(-sim->velX); - } -} - - -// $63DD -- FIRE press edge while airborne toggles the landing gear. -static void fireButtonEdge(StSimT *sim) { - if (sim->activePad != 0u) { - return; - } - if (sim->fireWasHeld != 0u) { - sim->fireWasHeld = (uint8_t)(sim->inputMask & 0x10u); - return; - } - if ((sim->inputMask & 0x10u) == 0u) { - return; - } - sim->fireWasHeld = 0x10u; - sim->spr[0].ptr ^= 1u; - stAudioSfx(stC64SfxProgram(ST_SFX_GEAR)); -} - - -// $6D6A -- sprite 2 is the exhaust: two columns left of the cab, -// visible every other tick while a direction is held. -void stSimFlameUpdate(StSimT *sim) { - uint16_t x; - - if (sim->dirMask == 0u) { - stAudioNoise(false); - sim->spr[2].enable = 0u; - return; - } - stAudioNoise(true); - x = (uint16_t)(stSimSpriteX(sim, 0u) - 2u); - sim->spr[2].col = (uint8_t)x; - sim->spr[2].msb = (uint8_t)(x >> 8); - sim->spr[2].row = sim->spr[0].row; - sim->flameParity ^= 1u; - if (sim->flameParity == 0u) { - sim->spr[2].enable = 0u; - return; - } - sim->spr[2].ptr = stC64FlameCel(sim->dirMask & 0x0Fu); - sim->spr[2].enable = 1u; -} - - -// $6419 -- the climb/descend indicator: colour RAM cells beside the -// fuel gauge flash red (rising), cyan (level) or yellow (falling). -static void fuelBarHud(StSimT *sim) { - uint16_t cell; - uint8_t color; - - setColorCell(sim, ST_CELL(23, 10), 0x0Bu); - setColorCell(sim, ST_CELL(23, 11), 0x0Bu); - setColorCell(sim, ST_CELL(24, 10), 0x0Bu); - setColorCell(sim, ST_CELL(24, 11), 0x0Bu); - sim->fuelBarTick = (uint8_t)((sim->fuelBarTick + 1u) & 7u); - if (sim->fuelBarTick >= 5u) { - return; - } - if (sim->velY < 0) { - color = 0x02u; - cell = ST_CELL(24, 10); - } else if (sim->velY == 0) { - color = 0x03u; - cell = ST_CELL(23, 10); - } else { - color = 0x07u; - cell = ST_CELL(23, 10); - } - setColorCell(sim, cell, color); - setColorCell(sim, (uint16_t)(cell + 1u), color); -} - - -// $6E23 -- the fuel gauge: half a cell burns every fuelRate ticks in -// the air; on the fuel pad (the extra last pad) it refills at ten -// cents per half cell. -static void fuelTick(StSimT *sim) { - const StLevelT *L = sim->level; - uint8_t x; - - if (sim->activePad == sim->padCount && sim->activePad != sim->specialPad) { - if (sim->screen[ST_CELL_FUEL_LAST] == ST_CHAR_BLANK) { - return; - } - if (!stSimDecrementNumber(sim, ST_CELL_SCORE, 5u)) { - return; - } - sim->padAnimTick = (uint8_t)((sim->padAnimTick + 1u) & 7u); - if (sim->padAnimTick == 0u) { - stAudioSfx(stC64SfxProgram(ST_SFX_CASH)); - } else if (sim->padAnimTick < 5u) { - stAudioVoice1Freq(kPumpFreq[sim->padAnimTick]); - } - if (sim->padAnimTick != 0u) { - return; - } - x = sim->fuelCells; - if (sim->screen[ST_CELL_FUEL + x] == ST_CHAR_BLANK) { - x++; - sim->fuelCells = x; - setCell(sim, (uint16_t)(ST_CELL_FUEL + x), ST_CHAR_FUEL_HALF); - return; - } - setCell(sim, (uint16_t)(ST_CELL_FUEL + x), ST_CHAR_BLANK); - if (sim->screen[ST_CELL_FUEL_LAST] == ST_CHAR_BLANK) { - stAudioSfx(stC64SfxProgram(ST_SFX_FUEL_FULL)); - } - return; - } - // $6EAA - if (sim->activePad != 0u) { - return; - } - sim->fuelCountdown--; - if (sim->fuelCountdown != 0u) { - return; - } - if (sim->fuelCells < 3u) { - stAudioSfx(stC64SfxProgram(ST_SFX_FUEL_LOW)); - } - sim->fuelCountdown = L->fuelRate; - x = sim->fuelCells; - if (sim->screen[ST_CELL_FUEL] == ST_CHAR_FUEL_HALF) { - return; - } - if (sim->screen[ST_CELL_FUEL + x] == ST_CHAR_FUEL_HALF) { - setCell(sim, (uint16_t)(ST_CELL_FUEL + x), ST_CHAR_FUEL_EMPTY); - x--; - sim->fuelCells = x; - return; - } - setCell(sim, (uint16_t)(ST_CELL_FUEL + x), ST_CHAR_FUEL_HALF); -} - - -// $43E5 -- the fare meter loses a penny every tick. -static void hudDraw(StSimT *sim) { - (void)stSimDecrementNumber(sim, ST_CELL_FARE, 6u); -} - - -// $657B -- parked on a pad: wait for UP to be released, then UP takes -// off (velocities cleared, gear retracted). -static void landedHandler(StSimT *sim) { - if (sim->activePadMirror != 0u) { - if ((sim->inputMask & 1u) == 0u) { - sim->activePadMirror = 0u; - } - return; - } - if (sim->deathInProgress != 0u) { - return; - } - if ((sim->inputMask & 1u) == 0u) { - return; - } - sim->velY = 0; - sim->velX = 0; - sim->activePad = 0u; - sim->spr[0].ptr &= 0xFEu; - if (sim->stage == ST_STAGE_WALK_TO_CAB) { - sim->stage--; - } -} - - -// $6BE7 -- the cab has flown out of the top of the screen. -static bool levelEndCheck(const StSimT *sim) { - return sim->posYrow < 0x1Bu; -} - - -// $645C -- with the gear down and a slow descent, an exact row match -// inside a pad's X bounds is a landing. -static void padDetect(StSimT *sim) { - int8_t i; - - if (sim->activePad != 0u) { - return; - } - if ((sim->spr[0].ptr & 1u) == 0u) { - return; - } - if (((uint16_t)sim->velY >> 8) != 0u) { - return; - } - for (i = (int8_t)(sim->padCount - 1u); i >= 0; i--) { - const StPadT *p = &sim->pads[i]; - int16_t taxiX = (int16_t)(((uint16_t)sim->posXmsb << 8) | sim->posXcol); - int16_t x1 = (int16_t)(((int16_t)p->x1Hi << 8) | p->x1Lo); - int16_t x2 = (int16_t)(((int16_t)p->x2Hi << 8) | p->x2Lo); - if (sim->spr[0].row != p->row) { - continue; - } - if ((int16_t)(taxiX - x1) < 0) { - continue; - } - if ((int16_t)(x2 - taxiX) < 0) { - continue; - } - sim->activePad = (uint8_t)(i + 1); - sim->activePadMirror = sim->activePad; - sim->eventDispatchType = 0u; - if (sim->activePad != sim->activeSpriteIdx) { - if (sim->stage == ST_STAGE_RIDING) { - sim->eventDispatchType = (sim->activeSpriteIdx == ST_FARE_DEST_UP) ? 3u : 2u; - } else if (sim->stage == ST_STAGE_WAIT) { - sim->eventDispatchType = 1u; - } - } - sim->dirMask = 0u; - sim->deathInProgress = 1u; - stSimPadLight(sim, true); - sim->bobTimer = 2u; - if (((uint8_t)sim->velY & 0x80u) == 0u) { - stAudioSfx(stC64SfxProgram(ST_SFX_LAND_SOFT)); - return; - } - sim->bobTimer = 8u; - stAudioSfx(stC64SfxProgram(ST_SFX_LAND_HARD)); - if (sim->stage == ST_STAGE_RIDING) { - stSimHudInit(sim); - } - return; - } -} - - -// $6DFF -- the touchdown bob: the sprite dips and rises while the -// timer runs down. -static void padLandingBob(StSimT *sim) { - if (sim->bobTimer == 0u) { - return; - } - sim->bobTimer--; - if ((sim->bobTimer & 1u) != 0u) { - sim->spr[0].row--; - return; - } - sim->spr[0].row++; - if (sim->bobTimer == 0u) { - sim->deathInProgress = 0u; - stSimPadLight(sim, false); - } -} - - -// $70B1 -- one bonus cab when the score's hundreds digit reaches 3. -static void passengerArrTick(StSimT *sim) { - uint8_t p = sim->player; - - if (sim->bonusLatch[p] != 0u) { - return; - } - if (sim->screen[ST_CELL_SCORE + 1u] < 0x6Du) { - return; - } - sim->cabs[p]++; - sim->bonusLatch[p] = sim->cabs[p]; - setCell(sim, (uint16_t)(ST_CELL_CAB_ICONS - 1u + sim->cabs[p]), ST_CHAR_CAB_ICON); - // $70D4: the bonus-cab jingle (song 4), blocking. - sim->jingleRequest = (uint8_t)(ST_JINGLE_SONG_BONUS + 1u); -} - - -// $6032 -- input, thrust, gravity, integration. -static void physicsTick(StSimT *sim) { - uint8_t in; - - sim->accelX = 0; - sim->accelY = 0; - in = readInput(sim); - in = stHookInput(sim, in); - sim->inputMask = in; - if (sim->activePad != 0u) { - landedHandler(sim); - return; - } - if (sim->screen[ST_CELL_FUEL] == ST_CHAR_FUEL_HALF) { - // The leftmost cell is down to its last half: out of fuel, so - // only FIRE survives and there is no thrust at all. - sim->inputMask &= 0x10u; - } else { - if ((sim->inputMask & 0x0Cu) != 0u) { - if ((sim->spr[0].ptr & 1u) != 0u) { - sim->inputMask &= 0x13u; - } else { - sim->accelX = (int16_t)sim->accelTemplateX; - if ((sim->inputMask & 0x08u) == 0u) { - sim->accelX = (int16_t)(-sim->accelX); - } - } - } - if ((sim->inputMask & 0x03u) != 0u) { - sim->accelY = (int16_t)sim->accelTemplateY; - if ((sim->inputMask & 0x02u) == 0u) { - sim->accelY = (int16_t)(-sim->accelY); - } - } - } - sim->dirMask = (uint8_t)(sim->inputMask & 0x0Fu); - sim->velX = (int16_t)((uint16_t)sim->accelX + sim->gravTemplateX + (uint16_t)sim->velX); - applyVelocityX(sim); - applyVelocityY(sim); -} - - -// $6040 + $48F2 -- the joystick byte, or the recorded demo mask. -static uint8_t readInput(StSimT *sim) { - uint8_t mask; - uint16_t next; - - if (sim->demoMode == 0u || sim->postMortem != 0u) { - return sim->rawInput; - } - mask = sim->demoBuf[sim->demoOff]; - sim->demoTimer--; - if (sim->demoTimer == 0u) { - next = (uint16_t)(sim->demoOff + 3u); - if (next < sizeof(sim->demoBuf)) { - sim->demoTimer = sim->demoBuf[next]; - } - next = (uint16_t)(sim->demoOff + 2u); - if (next < sizeof(sim->demoBuf)) { - sim->demoOff = next; - } - } - return mask; -} - - -static void markCell(StSimT *sim, uint16_t cell) { - if (sim->cellDirty[cell] != 0u) { - return; - } - sim->cellDirty[cell] = 1u; - if (sim->dirtyCount < (uint16_t)(sizeof(sim->dirtyList) / sizeof(sim->dirtyList[0]))) { - sim->dirtyList[sim->dirtyCount++] = cell; - } else { - sim->dirtyAll = true; - } -} - - -// ---- glyph reverse index (see StSimT in stSim.h) -------------------- - -// Take `cell` out of the list of whatever glyph it currently shows. -// Reads sim->screen[cell], so call it BEFORE the new character lands. -static void glyphIndexUnlink(StSimT *sim, uint16_t cell) { - uint16_t prev = sim->cellPrev[cell]; - uint16_t next = sim->cellNext[cell]; - - if (prev == ST_GLYPH_CELL_NONE) { - sim->glyphHead[sim->screen[cell]] = next; - } else { - sim->cellNext[prev] = next; - } - if (next != ST_GLYPH_CELL_NONE) { - sim->cellPrev[next] = prev; - } -} - - -// Push `cell` onto the front of glyph `ch`'s list. -static void glyphIndexLink(StSimT *sim, uint16_t cell, uint8_t ch) { - uint16_t head = sim->glyphHead[ch]; - - sim->cellPrev[cell] = ST_GLYPH_CELL_NONE; - sim->cellNext[cell] = head; - if (head != ST_GLYPH_CELL_NONE) { - sim->cellPrev[head] = cell; - } - sim->glyphHead[ch] = cell; -} - - -// Re-derive every list from screen[]. Bulk screen writes (level load, -// title, the blanked intro) bypass setCell, and all of them call -// stSimDirtyAll, so that is the one place this has to hang off. -static void glyphIndexRebuild(StSimT *sim) { - uint16_t cell; - uint16_t k; - - for (k = 0u; k < ST_CHARSET_CHARS; k++) { - sim->glyphHead[k] = ST_GLYPH_CELL_NONE; - } - // Built back to front, so each list comes out in ASCENDING cell - // order and the repaint walk moves forward through the stage the - // way the old span scan did. - cell = ST_SCREEN_CELLS; - while (cell != 0u) { - cell--; - glyphIndexLink(sim, cell, sim->screen[cell]); - } -} - - -// A sprite row (3 bytes, leftmost pixel = bit 7 of byte 0) shifted -// right by s (0..7) into a 4-byte window whose byte j holds the row's -// pixels 8j-s .. 8j-s+7: the row aligned to a grid of 8-pixel cells -// that its left edge sits s pixels into. Fixed shifts by 4, 2 and 1 -// on 16-bit halves -- a variable-count shift is a library bit loop on -// the 65816, and a byte carry chain gets folded back into one. -static void rowWindow(const uint8_t *row, uint8_t s, uint8_t *w) { - uint16_t hi = (uint16_t)(((uint16_t)row[0] << 8) | row[1]); - uint16_t lo = (uint16_t)((uint16_t)row[2] << 8); - - if ((s & 4u) != 0u) { - lo = (uint16_t)((lo >> 4) | (uint16_t)(hi << 12)); - hi = (uint16_t)(hi >> 4); - } - if ((s & 2u) != 0u) { - lo = (uint16_t)((lo >> 2) | (uint16_t)(hi << 14)); - hi = (uint16_t)(hi >> 2); - } - if ((s & 1u) != 0u) { - lo = (uint16_t)((lo >> 1) | (uint16_t)(hi << 15)); - hi = (uint16_t)(hi >> 1); - } - w[0] = (uint8_t)(hi >> 8); - w[1] = (uint8_t)hi; - w[2] = (uint8_t)(lo >> 8); - w[3] = (uint8_t)lo; -} - - -static void setCell(StSimT *sim, uint16_t cell, uint8_t ch) { - if (sim->screen[cell] != ch) { - glyphIndexUnlink(sim, cell); - sim->screen[cell] = ch; - glyphIndexLink(sim, cell, ch); - markCell(sim, cell); - } -} - - -static void setColorCell(StSimT *sim, uint16_t cell, uint8_t color) { - color = (uint8_t)(color & 0x0Fu); - if (sim->color[cell] != color) { - sim->color[cell] = color; - markCell(sim, cell); - } -} - - -// 21 rows of 3-byte foreground masks for a displayed sprite, kept in -// collMask[idx] and rebuilt only when its bitmap or multicolour mode -// changes. A multicolour sprite's 2-bit pairs cover two pixels each: a -// non-zero pair is two set mask bits (kPairMask maps a nibble's two -// pairs at once). The cab's cached windows die with its mask. -static void spriteMasks(StSimT *sim, uint8_t idx) { - static const uint8_t kPairMask[16] = { - 0x0u, 0x3u, 0x3u, 0x3u, 0xCu, 0xFu, 0xFu, 0xFu, - 0xCu, 0xFu, 0xFu, 0xFu, 0xCu, 0xFu, 0xFu, 0xFu - }; - const uint8_t *bm = stSimSpriteBitmap(sim, sim->frame.ptr[idx]); - uint8_t multi = (uint8_t)((sim->frame.multiMask & kStBit[idx]) != 0u ? 1u : 0u); - uint8_t *row = sim->collMask[idx][0]; - uint16_t n; - uint8_t first; - uint8_t last; - - if (bm != 0 && bm == sim->collMaskBm[idx] && multi == sim->collMaskMulti[idx]) { - return; - } - for (n = 0u; n < (uint16_t)(ST_SPRITE_H * 3u); n++) { - uint8_t b = 0u; - if (bm != 0) { - b = bm[n]; - if (multi != 0u) { - b = (uint8_t)((kPairMask[b >> 4] << 4) | kPairMask[b & 0x0Fu]); - } - } - row[n] = b; - } - first = ST_SPRITE_H; - last = 0u; - for (n = 0u; n < ST_SPRITE_H; n++) { - if ((row[n * 3u] | row[n * 3u + 1u] | row[n * 3u + 2u]) != 0u) { - if (first == ST_SPRITE_H) { - first = (uint8_t)n; - } - last = (uint8_t)n; - } - } - sim->collRowFirst[idx] = first; - sim->collRowLast[idx] = last; - sim->collMaskBm[idx] = bm; - sim->collMaskMulti[idx] = multi; - if (idx == 0u) { - sim->cabWinValid = 0u; - } -} - - -// A non-cab sprite's rows (shifted here, only rows with pixels) against -// the background rows under it. -static bool spriteRowsTouchBackground(const StSimT *sim, uint8_t idx, int16_t firstCol, int16_t py, uint8_t phase) { - uint8_t first = sim->collRowFirst[idx]; - uint8_t left = (uint8_t)(sim->collRowLast[idx] + 1u - first); - const uint8_t *row; - - if (first == ST_SPRITE_H) { - return false; - } - row = sim->collMask[idx][first]; - py = (int16_t)(py + first); - // Counted down like cabRowsTouchBackground. - do { - if ((row[0] | row[1] | row[2]) != 0u) { - uint8_t w[4]; - rowWindow(row, phase, w); - if (backgroundRowHits(sim, firstCol, py, w)) { - return true; - } - } - row += 3; - py++; - left--; - } while (left != 0u); - return false; -} - - -// One displayed sprite against the background pixels ($D01F). The -// background can only touch it where a covering character cell is -// non-blank. In open flight every covering cell is the space glyph -// (all-zero pixels), so the 21-row pixel test is skipped there; any -// non-space cell falls through to the exact test, so the result is -// unchanged (space is the only zero-pixel glyph the test can hit). -// Needs spriteMasks(sim, idx) run for this frame first. The row tests -// live in cabRowsTouchBackground / spriteRowsTouchBackground, whose -// loops count DOWN -- see the note there before "simplifying" them. -static bool spriteTouchesBackground(StSimT *sim, uint8_t idx) { - int16_t px = (int16_t)((int16_t)sim->frame.x[idx] - ST_SPRITE_X_ORIGIN); - int16_t py = (int16_t)((int16_t)sim->frame.y[idx] - ST_SPRITE_Y_ORIGIN); - int16_t firstCol = cellOfPixel(px); - int16_t topRow = (py < 0) ? 0 : cellOfPixel(py); - int16_t botRow = cellOfPixel((int16_t)(py + (int16_t)ST_SPRITE_H - 1)); - uint8_t phase = (uint8_t)(px - (int16_t)(firstCol << 3)); - - if (botRow >= (int16_t)ST_SCREEN_ROWS) { - botRow = (int16_t)(ST_SCREEN_ROWS - 1u); - } - if (!coveringCellsSolid(sim, firstCol, topRow, botRow)) { - return false; - } - if (idx == 0u) { - return cabRowsTouchBackground(sim, firstCol, py, phase); - } - return spriteRowsTouchBackground(sim, idx, firstCol, py, phase); -} - - -// $6946 -- all sprites off, latches cleared, nobody on a pad. -static void spritesOffReset(StSimT *sim) { - uint8_t i; - - for (i = 0u; i < ST_HW_SPRITES; i++) { - sim->spr[i].enable = 0u; - } - sim->spriteSpriteColl = 0u; - sim->spriteBgColl = 0u; - sim->activePad = 0u; - sim->activeSpriteIdx = 0u; -} - - -// $6888 -- place the cab at the level's spawn point with a full tank. -static void taxiSpawnInit(StSimT *sim) { - const StLevelT *L = sim->level; - uint8_t k; - - sim->spr[0].enable = 1u; - sim->multiColorMask = 0x07u; - sim->spr[0].ptr = ST_SPRITE_CAB_RIGHT; - sim->posYlo = L->spawn[3]; - sim->posYrow = L->spawn[4]; - sim->posXlo = L->spawn[0]; - sim->posXcol = L->spawn[1]; - sim->posXmsb = L->spawn[2]; - sim->spr[0].msb = L->spawn[2]; - sim->spr[0].col = L->spawn[1]; - sim->spr[0].row = L->spawn[4]; - sim->velX = 0; - sim->velY = 0; - sim->deathInProgress = 0u; - sim->dirMask = 0u; - sim->inputMask = 0u; - sim->activePad = 0u; - sim->collisionPhase = 0u; - sim->padAnimTick = 0u; - stSimPadLight(sim, false); - fuelBarHud(sim); - sim->fuelCells = 11u; - for (k = 0u; k < ST_FUEL_CELLS; k++) { - setCell(sim, (uint16_t)(ST_CELL_FUEL + k), ST_CHAR_BLANK); - } - sim->fuelCountdown = L->fuelRate; -} - - -// $619B -- LEFT/RIGHT pick the facing pair; bit 0 (the gear) survives. -static void taxiSpriteCelSelect(StSimT *sim) { - uint8_t gear = (uint8_t)(sim->spr[0].ptr & 1u); - uint8_t base; - - if ((sim->dirMask & 0x04u) != 0u) { - base = ST_SPRITE_CAB_LEFT; - } else if ((sim->dirMask & 0x08u) != 0u) { - base = ST_SPRITE_CAB_RIGHT; - } else { - return; - } - sim->spr[0].ptr = (uint8_t)(base | gear); -} - - -// $4345 -- a HUD glyph's numeric value: blanks and the zero glyph are 0. - - -static uint8_t validateDigit(uint8_t ch) { - uint8_t v; - - if (ch == ST_CHAR_BLANK) { - return 0u; - } - v = (uint8_t)(ch - ST_CHAR_DIGIT_BASE); - if (v == 10u) { - return 0u; - } - return v; -} - - -// --------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------- - -// $61FB -- next player's turn; wraps to player 0 when everybody has had -// this screen (the caller then loads the next one). -bool stSimAdvancePlayer(StSimT *sim) { - sim->player++; - if (sim->player == sim->playerCount) { - sim->player = 0u; - return true; - } - return false; -} - - -// $704E -- keep the current player's score and fare rows. -void stSimArchiveHud(StSimT *sim) { - memcpy(sim->scoreBackup[sim->player], &sim->screen[ST_CELL_SCORE], ST_NUMBER_CHARS); - memcpy(sim->fareBackup[sim->player], &sim->screen[ST_CELL_FARE], ST_NUMBER_CHARS); -} - - -// $4354 -- add a 7-glyph decimal blob onto the number at dstCell, -// right to left, skipping the decimal point, with leading blanks. -void stSimBcdAdd(StSimT *sim, uint16_t dstCell, const uint8_t *blob) { - int8_t y; - uint8_t carry = 0u; - - for (y = 6; y >= 0; y--) { - uint8_t sum; - uint8_t ch; - if (y == 4) { - continue; - } - sum = (uint8_t)(validateDigit(blob[y]) + validateDigit(sim->screen[dstCell + (uint16_t)y]) + carry); - carry = 0u; - if (sum >= 10u) { - carry = 1u; - sum = (uint8_t)(sum - 10u); - } - ch = (uint8_t)(ST_CHAR_DIGIT_BASE + sum); - if (ch == ST_CHAR_DIGIT_BASE) { - ch = ST_CHAR_ZERO; - } - setCell(sim, (uint16_t)(dstCell + (uint16_t)y), ch); - } - // $4393: leading-zero suppression across the integer part. - for (y = 0; y < 3; y++) { - if (sim->screen[dstCell + (uint16_t)y] != ST_CHAR_ZERO) { - break; - } - setCell(sim, (uint16_t)(dstCell + (uint16_t)y), ST_CHAR_BLANK); - } -} - - -// $6C38 -- blank the message row (colour 1, as the original writes it). -void stSimClearMessage(StSimT *sim) { - stSimDrawText(sim, ST_MESSAGE_COL, ST_MESSAGE_ROW, kTextBlank, 1u); -} - - -// $440B -- subtract one from the digit at `position` of the number at -// `cell`, borrowing leftward. False when there is nothing to take. -bool stSimDecrementNumber(StSimT *sim, uint16_t cell, uint8_t position) { - uint8_t y = position; - uint8_t ch; - - for (;;) { - ch = sim->screen[cell + y]; - if (ch == ST_CHAR_BLANK) { - return false; - } - if (ch != ST_CHAR_ZERO) { - break; - } - do { - y--; - } while (y == 4u); - } - y = position; - for (;;) { - ch = sim->screen[cell + y]; - if (ch != ST_CHAR_ZERO) { - break; - } - setCell(sim, (uint16_t)(cell + y), (uint8_t)(ST_CHAR_DIGIT_BASE + 9u)); - do { - y--; - } while (y == 4u); - } - ch = (uint8_t)(ch - 1u); - setCell(sim, (uint16_t)(cell + y), ch); - if (ch != ST_CHAR_DIGIT_BASE) { - return true; - } - setCell(sim, (uint16_t)(cell + y), ST_CHAR_ZERO); - if (y >= 3u) { - return true; - } - if (y == 0u) { - setCell(sim, cell, ST_CHAR_BLANK); - return true; - } - if (sim->screen[cell + y - 1u] != ST_CHAR_BLANK) { - return true; - } - setCell(sim, (uint16_t)(cell + y), ST_CHAR_BLANK); - return true; -} - - -// $41C2 -- write a screen-code string (terminated by any byte < 6) -// with one colour. -void stSimDrawText(StSimT *sim, uint8_t col, uint8_t row, const uint8_t *text, uint8_t color) { - uint16_t cell = ST_CELL(row, col); - - while (*text >= 6u) { - setCell(sim, cell, *text); - setColorCell(sim, cell, color); - cell++; - text++; - } -} - - -// A character's eight 1bpp rows: the game's edit of it, or the const ROM -// glyph when it has never been edited. -const uint8_t *stSimGlyph(const StSimT *sim, uint8_t ch) { - uint8_t slot = sim->glyphSlot[ch]; - - if (slot != 0u) { - return sim->glyphOverride[slot - 1u].rows; - } - return &stC64Charset()[(uint16_t)ch << 3]; -} - - -// The writable eight rows, copied out of ROM on the first edit. A full -// table parks the edit in the spill entry rather than stealing another -// character's slot: that glyph then simply stays at its ROM form. -uint8_t *stSimGlyphMut(StSimT *sim, uint8_t ch) { - uint8_t slot = sim->glyphSlot[ch]; - - if (slot == 0u) { - if (sim->glyphOverrideCount >= ST_GLYPH_OVERRIDES) { - return sim->glyphOverride[ST_GLYPH_OVERRIDES].rows; - } - sim->glyphOverrideCount++; - slot = sim->glyphOverrideCount; - sim->glyphSlot[ch] = slot; - memcpy(sim->glyphOverride[slot - 1u].rows, &stC64Charset()[(uint16_t)ch << 3], 8u); - } - return sim->glyphOverride[slot - 1u].rows; -} - - -// Every glyph back to the ROM set (a scene load). -void stSimGlyphReset(StSimT *sim) { - memset(sim->glyphSlot, 0, sizeof(sim->glyphSlot)); - sim->glyphOverrideCount = 0u; -} - - -// One row of a character's glyph. The collision background test reads -// four of these per sprite row, so it skips the pointer round trip. -uint8_t stSimGlyphRow(const StSimT *sim, uint8_t ch, uint8_t row) { - uint8_t slot = sim->glyphSlot[ch]; - - if (slot != 0u) { - return sim->glyphOverride[slot - 1u].rows[row]; - } - return stC64Charset()[((uint16_t)ch << 3) + row]; -} - - -// $43A5 -- blank the fare meter back to its template. -void stSimHudInit(StSimT *sim) { - uint8_t k; - - for (k = 0u; k < ST_NUMBER_CHARS; k++) { - setCell(sim, (uint16_t)(ST_CELL_FARE + k), kHudTemplate[k]); - } -} - - -// $62F0 + $5F18 -- load the screen image and run the level prelude. -void stSimEnterLevel(StSimT *sim, const StLevelT *level) { - uint8_t k; - - sim->level = level; - memcpy(sim->pads, level->pads, sizeof(sim->pads)); - sim->padCount = level->padCount; - sim->specialPad = level->specialPad; - sim->bgCollSprites = 0u; - memcpy(sim->screen, level->screen, ST_SCREEN_CELLS); - memcpy(sim->color, level->color, ST_SCREEN_CELLS); - stSimDirtyAll(sim); - stSimGlyphReset(sim); - sim->charDirtyAll = true; - sim->charDirtyCount = 0u; - // Level sprites live in the one level buffer, so the same bitmap - // address now holds other pixels: rebuild every collision mask. - memset(sim->collMaskBm, 0, sizeof(sim->collMaskBm)); - sim->borderColor = level->header[0]; - sim->bgColor = level->header[1]; - sim->spr[0].color = level->header[7]; - sim->spr[1].color = level->header[8]; - sim->spriteMc0 = level->header[5]; - sim->spriteMc1 = level->header[6]; - sim->accelTemplateY = level->accelY; - sim->accelTemplateX = level->accelX; - sim->gravTemplateY = level->gravY; - sim->gravTemplateX = level->gravX; - memset(sim->spriteSlots, 0, sizeof(sim->spriteSlots)); - sim->fareSlotCount = 0u; - // $634D: this player's HUD rows come back. - for (k = 0u; k < ST_NUMBER_CHARS; k++) { - setCell(sim, (uint16_t)(ST_CELL_SCORE + k), sim->scoreBackup[sim->player][k]); - setCell(sim, (uint16_t)(ST_CELL_FARE + k), sim->fareBackup[sim->player][k]); - } - for (k = 0u; k < 7u; k++) { - setCell(sim, (uint16_t)(ST_CELL_CAB_ICONS + k), ST_CHAR_BLANK); - } - drawCabIcons(sim); - drawScreensCount(sim); - // Prelude $5F18 (the player-turn bookkeeping ran before the load). - for (k = 0u; k < ST_HW_SPRITES; k++) { - sim->spr[k].enable = 0u; - } - sim->hookLevel = level->levelIndex; - stHookSceneLoad(sim); - stSimRespawn(sim); -} - - -// $5F27 -- the respawn half of the prelude. -void stSimRespawn(StSimT *sim) { - spritesOffReset(sim); - stHookPrelude0(sim); - // $6F18 takeoffSetup: from the third screen on the cab starts with - // a passenger aboard who wants pad 1. - sim->eventDispatchType = 0u; - sim->stage = ST_STAGE_IDLE; - if (sim->levelState >= 2u) { - static const uint8_t kPadPlease[] = "PAD 1 PLEASE"; - sim->stage = ST_STAGE_RIDING; - sim->activeSpriteIdx = 1u; - sim->activeDyingSlot = 1u; - stSimDrawText(sim, ST_MESSAGE_COL, ST_MESSAGE_ROW, kPadPlease, 1u); - sim->eventDispatchType = 1u; - } - taxiSpawnInit(sim); - stHookPrelude1(sim); - // $6906 framePresent: the start-of-screen jingle, then the SID (and - // the demo RNG) reset. $6935: level 25 gets song 4, every other - // screen rotates songs 0..3 on $5E9B. The host plays it with the sim - // frozen, which is what the C64's blocking play-and-wait amounts to. - stSimMarshal(sim); - stAudioSilence(); - sim->jingleRotate++; - if (sim->levelState == ST_JINGLE_LEVEL_25) { - sim->jingleRequest = (uint8_t)(ST_JINGLE_SONG_BONUS + 1u); - } else { - sim->jingleRequest = (uint8_t)((sim->jingleRotate & 3u) + 1u); - } - sim->spriteSpriteColl = 0u; - sim->spriteBgColl = 0u; - sim->rngT1 = 0x06u; - sim->rngT2 = 0x17u; -} - - -// $5EC4 + $4207 + $48AD -- a fresh game (or the attract demo). -void stSimNewGame(StSimT *sim, uint8_t playerCount, bool demo) { - uint8_t p; - uint8_t keepBuf[sizeof(sim->demoBuf)]; - uint8_t keepParity = sim->flameParity; - uint8_t keepWave = sim->waveIdx; - uint8_t keepWalk = sim->walkParity; - - // $5EC4 clears the game state but the playback buffer, the flame - // parity, the wave index and the walk parity all survive. So does - // the displayed picture -- screen RAM, colour RAM and the charset - // are not game state, and the attract demo's "GET READY" banner is - // drawn over the still-live title screen -- so the clear starts at - // ST_SIM_CLEAR_FROM. Every level entry rebinds the image anyway - // (stSimEnterLevel), so nothing downstream sees the difference. - memcpy(keepBuf, sim->demoBuf, sizeof(keepBuf)); - memset((uint8_t *)sim + ST_SIM_CLEAR_FROM, 0, sizeof(*sim) - ST_SIM_CLEAR_FROM); - memcpy(sim->demoBuf, keepBuf, sizeof(keepBuf)); - sim->flameParity = keepParity; - sim->waveIdx = keepWave; - sim->walkParity = keepWalk; - sim->stage0Rng = 0x64u; - sim->decayReload = 3u; - sim->playerCount = playerCount; - sim->player = 0u; - sim->playersDone = 0u; - sim->levelState = 0u; - sim->demoMode = demo ? 1u : 0u; - sim->rngHost = 0x2545F491u; - for (p = 0u; p < ST_MAX_PLAYERS; p++) { - sim->cabs[p] = ST_CABS_PER_PLAYER; - memcpy(sim->scoreBackup[p], kHudTemplate, ST_NUMBER_CHARS); - memcpy(sim->fareBackup[p], kHudTemplate, ST_NUMBER_CHARS); - } - sim->rngT1 = 0x06u; - sim->rngT2 = 0x17u; -} - - -// Everything must be repainted. -// A glyph's bits changed: the renderer has to repaint every cell showing -// that character. The list is short (one or two per frame), so a linear -// scan for duplicates is cheaper than any index; an overflow just says -// "all of them" and costs one full repaint. -void stSimMarkChar(StSimT *sim, uint8_t ch) { - uint8_t k; - - if (sim->charDirtyAll) { - return; - } - for (k = 0u; k < sim->charDirtyCount; k++) { - if (sim->charDirtyList[k] == ch) { - return; - } - } - if (sim->charDirtyCount == ST_CHAR_DIRTY_MAX) { - sim->charDirtyAll = true; - return; - } - sim->charDirtyList[sim->charDirtyCount] = ch; - sim->charDirtyCount++; -} - - -// $4253 + $4293 -- snapshot the sprite shadows into the frame the VIC -// shows next. The title and level-intro loops flush the same way. -void stSimMarshal(StSimT *sim) { - uint8_t i; - uint8_t enable = 0u; - - for (i = 0u; i < ST_HW_SPRITES; i++) { - sim->frame.x[i] = (uint16_t)(((uint16_t)(sim->spr[i].msb != 0u ? 1u : 0u) << 8) | sim->spr[i].col); - sim->frame.y[i] = sim->spr[i].row; - sim->frame.ptr[i] = sim->spr[i].ptr; - sim->frame.color[i] = sim->spr[i].color; - if (sim->spr[i].enable != 0u) { - enable |= kStBit[i]; - } - } - sim->frame.enableMask = enable; - sim->frame.multiMask = sim->multiColorMask; -} - - -// Every cell repaints AND the glyph index is rebuilt: for callers that -// replaced the screen bytes wholesale (a level load, the title scene). -void stSimDirtyAll(StSimT *sim) { - stSimDirtyCells(sim); - // Every bulk screen write calls this, and those bypass setCell, so - // this is where the glyph reverse index gets re-derived. - glyphIndexRebuild(sim); -} - - -// Every cell repaints; the screen bytes are as they were, so the glyph -// index stands. A rebuild is ~0.15 s on the IIgs (1000 far-pointer -// list links), and the level-entry path used to pay it twice. -void stSimDirtyCells(StSimT *sim) { - memset(sim->cellDirty, 1, ST_SCREEN_CELLS); - sim->dirtyCount = 0u; - sim->dirtyAll = true; -} - - -// $6866 / $6877 -- the pad indicator cells beside the HUD. -void stSimPadLight(StSimT *sim, bool on) { - if (on) { - setColorCell(sim, ST_CELL(23, 28), 0x0Bu); - setColorCell(sim, ST_CELL(23, 29), 0x0Bu); - setColorCell(sim, ST_CELL(24, 28), 0x02u); - setColorCell(sim, ST_CELL(24, 29), 0x02u); - } else { - setColorCell(sim, ST_CELL(24, 28), 0x0Bu); - setColorCell(sim, ST_CELL(24, 29), 0x0Bu); - setColorCell(sim, ST_CELL(23, 28), 0x07u); - setColorCell(sim, ST_CELL(23, 29), 0x07u); - } -} - - -// $401B -- one character into screen RAM. -void stSimPutChar(StSimT *sim, uint8_t col, uint8_t row, uint8_t ch) { - setCell(sim, ST_CELL(row, col), ch); -} - - -// $401E -- one colour into colour RAM. -void stSimPutColor(StSimT *sim, uint8_t col, uint8_t row, uint8_t color) { - setColorCell(sim, ST_CELL(row, col), color); -} - - -// $4080 -- 1..n. The demo walks a 64-byte table so its rides replay -// exactly; a real game reads the SID noise oscillator, which a host -// LCG stands in for. -uint8_t stSimRng(StSimT *sim, uint8_t n) { - uint8_t r; - uint16_t product; - - if (sim->demoMode != 0u) { - sim->rngT1 = (uint8_t)((sim->rngT1 + 1u) & 0x3Fu); - sim->rngT2++; - r = (uint8_t)(stC64RngByte(sim->rngT1) + sim->rngT2); - } else { - sim->rngHost = sim->rngHost * 1103515245u + 12345u; - r = (uint8_t)(sim->rngHost >> 16); - } - product = (uint16_t)((uint16_t)n * (uint16_t)r); - return (uint8_t)((product >> 8) + 1u); -} - - -// $63D0 -- rotate every row of a glyph one pixel right and mark every -// cell showing it for repaint: the transporter hatch and the level O -// electroids both animate this way. -void stSimRotateGlyph(StSimT *sim, uint8_t ch) { - uint8_t *rows = stSimGlyphMut(sim, ch); - uint8_t row; - - for (row = 0u; row < 8u; row++) { - uint8_t b = rows[row]; - rows[row] = (uint8_t)((b >> 1) | (b << 7)); - } - stSimMarkChar(sim, ch); -} - - -// The score glyphs as pennies (for the high-score table). -uint32_t stSimScorePennies(const StSimT *sim) { - uint32_t v = 0u; - uint8_t k; - - for (k = 0u; k < ST_NUMBER_CHARS; k++) { - if (k == 4u) { - continue; - } - v = v * 10u + validateDigit(sim->screen[ST_CELL_SCORE + k]); - } - return v; -} - - -// The boot-time content of the playback buffer. -void stSimSeedDemoBuffer(StSimT *sim, const uint8_t *image, uint16_t len) { - if (len > sizeof(sim->demoBuf)) { - len = (uint16_t)sizeof(sim->demoBuf); - } - memcpy(sim->demoBuf, image, len); -} - - -// $0902 / $4740 -- load a recording over the buffer and rewind. -void stSimSetDemoStream(StSimT *sim, const uint8_t *stream, uint16_t len) { - if (len > sizeof(sim->demoBuf)) { - len = (uint16_t)sizeof(sim->demoBuf); - } - memcpy(sim->demoBuf, stream, len); - sim->demoOff = 0u; - sim->demoTimer = sim->demoBuf[1]; -} - - -// $411B -- add a signed byte to a sprite's 17-bit X, carrying into the -// msb shadow. -void stSimSpriteAddX(StSimT *sim, uint8_t idx, uint8_t delta) { - uint16_t x = (uint16_t)(stSimSpriteX(sim, idx) + (uint16_t)(int16_t)(int8_t)delta); - - sim->spr[idx].col = (uint8_t)x; - sim->spr[idx].msb = (uint8_t)(x >> 8); -} - - -// Bitmap for a block pointer: the standard set, or the level's own. -const uint8_t *stSimSpriteBitmap(const StSimT *sim, uint8_t ptr) { - uint8_t k; - - if (ptr >= ST_SPRITE_PTR_FIRST && ptr <= ST_SPRITE_PTR_LAST) { - return stC64SpriteBitmap((uint8_t)(ptr - ST_SPRITE_PTR_FIRST)); - } - if (sim->level != 0) { - for (k = 0u; k < sim->level->spriteCount; k++) { - if (sim->level->sprites[k].ptr == ptr) { - return sim->level->sprites[k].bitmap; - } - } - } - return 0; -} - - -// The 17-bit VIC sprite X the shadow tables hold as msb:col. -uint16_t stSimSpriteX(const StSimT *sim, uint8_t idx) { - return (uint16_t)(((uint16_t)sim->spr[idx].msb << 8) | sim->spr[idx].col); -} - - -// $5F40 -- one iteration of the main loop. -StTickResultE stSimTick(StSimT *sim) { - StTickResultE result = ST_TICK_CONTINUE; - - if (sim->collisionPhase == 0u) { - physicsTick(sim); - fireButtonEdge(sim); - stSimFlameUpdate(sim); - padDetect(sim); - taxiSpriteCelSelect(sim); - if (levelEndCheck(sim)) { - stAudioNoise(false); - stAudioSilence(); - stSimArchiveHud(sim); - return ST_TICK_LEVEL_EXIT; - } - padLandingBob(sim); - fuelTick(sim); - // $61BD passengerEventDraw only re-voices the pickup/drop-off - // messages already drawn by the fare machine. - } - stFarePostTickGate(sim); - stFareStageDispatch(sim); - stHookPerTick2(sim); - stSimMarshal(sim); - stSimRotateGlyph(sim, ST_CHAR_TRANSPORTER); - stHookPerTick3(sim); - fuelBarHud(sim); - if (sim->collisionPhase == 0u) { - collisionDispatch(sim); - } else if (sim->collisionPhase == 1u) { - computeCollisions(sim); - collisionPhase1(sim); - } else if (sim->collisionPhase == 2u) { - computeCollisions(sim); - collisionPhase2(sim); - } else { - computeCollisions(sim); - result = collisionPhase3(sim); - if (result != ST_TICK_CONTINUE) { - return result; - } - } - hudDraw(sim); - // $4FCB runStopWatcher: any joystick input ends the demo. - if (sim->demoMode != 0u && (sim->rawInput & 0x1Fu) != 0u) { - return ST_TICK_DEMO_INPUT; - } - passengerArrTick(sim); - return ST_TICK_CONTINUE; -} - diff --git a/examples/spacetaxi/stSim.h b/examples/spacetaxi/stSim.h deleted file mode 100644 index 5991fe3..0000000 --- a/examples/spacetaxi/stSim.h +++ /dev/null @@ -1,677 +0,0 @@ -// Space Taxi -- the C64 game simulation, host-independent. -// -// This is a routine-for-routine re-expression of the original's game -// tick (the $5F40 main loop) in C, working in the C64's own units: -// VIC sprite coordinates (sprite-X 24 = visible column 0, sprite-Y 50 -// = visible row 0), 8-bit sub-pixel velocities, screen RAM character -// codes, the per-level pad table as shipped, and the same state -// machines with the same timers. Nothing in here touches JoeyLib, so -// the same code runs inside a plain host build (tools/stsim) where it -// is checked tick-for-tick against a VICE trace of the real game. -// -// The renderer reads this state (sprite snapshot, screen RAM, charset) -// and the audio layer receives events through the stAudio* hooks -// declared at the bottom, which each host implements. -// -// Address comments ($xxxx) name the original variable or routine a -// field or function mirrors; see stuff/spacetaxi/labels.txt. - -#ifndef ST_SIM_H -#define ST_SIM_H - -#include -#include -#include -#include - -#include "stC64Data.h" - -// Screen geometry: screen RAM is 40x25 character cells, all of which -// belong to the level image (the HUD rows included). -#define ST_SCREEN_COLS 40u -#define ST_SCREEN_ROWS 25u -#define ST_SCREEN_CELLS (ST_SCREEN_COLS * ST_SCREEN_ROWS) -// End-of-list / not-linked sentinel for the glyph reverse index below. -// ST_SCREEN_CELLS is 1000, so no real cell index can collide with it. -#define ST_GLYPH_CELL_NONE 0xFFFFu -// row * 40 + col. Written as shifts (40 == 32 + 8) because the 65816 has -// no multiply: with a variable row the * form called the compiler's -// software multiply helper, and the collision pre-check does that a -// dozen times a tick. Constant rows still fold to a constant. -#define ST_CELL(row, col) ((uint16_t)((((uint16_t)(row)) << 5) + (((uint16_t)(row)) << 3) + (uint16_t)(col))) - -// VIC sprite coordinate origins: the visible picture starts at -// sprite-X 24 and sprite-Y 50. -#define ST_SPRITE_X_ORIGIN 24 -#define ST_SPRITE_Y_ORIGIN 50 -#define ST_SPRITE_W 24 -#define ST_SPRITE_H 21 -// A sprite row shifted onto the 8-pixel character grid has one of 8 -// phases (its left edge 0..7 pixels into a cell) -- see rowWindow. -#define ST_ROW_PHASES 8 -#define ST_HW_SPRITES 8u - -// Fixed screen cells the game writes directly (C64 screen addresses). -#define ST_CELL_TRANSPORTER ST_CELL(0, 18) // $0412..$0415, 4 cells -#define ST_CELL_CAB_ICONS ST_CELL(23, 1) // $0799..$079F -#define ST_CELL_FUEL ST_CELL(23, 14) // $07A6..$07B1, 12 cells -#define ST_CELL_FUEL_LAST ST_CELL(23, 25) // $07B1 -#define ST_CELL_SCREENS ST_CELL(23, 32) // $07B8..$07BC -#define ST_CELL_SCORE ST_CELL(24, 2) // $07C2..$07C8, 7 chars -#define ST_CELL_MESSAGE ST_CELL(24, 14) // $07CE, 11 chars -#define ST_MESSAGE_COL 14u -#define ST_MESSAGE_ROW 24u -#define ST_CELL_PAD_DIGIT ST_CELL(24, 18) // $07D2 -#define ST_CELL_FARE ST_CELL(24, 32) // $07E0..$07E6, 7 chars -#define ST_NUMBER_CHARS 7u -#define ST_FUEL_CELLS 12u -#define ST_LEVEL_NAME_CHARS 22u - -// Screen-code glyphs the HUD arithmetic works on ($4345/$4354/$440B): -// inverse-video digits 1..9 are $6B..$73, zero is $74, a leading blank -// is $66 and the decimal point is $77. -#define ST_CHAR_BLANK 0x66u -#define ST_CHAR_DIGIT_BASE 0x6Au // digit d (1..9) = base + d -#define ST_CHAR_ZERO 0x74u -#define ST_CHAR_POINT 0x77u -#define ST_CHAR_SPACE 0x20u -#define ST_CHAR_FUEL_HALF 0x7Bu // half a cell of fuel left -#define ST_CHAR_FUEL_EMPTY 0x7Au -#define ST_CHAR_TRANSPORTER 0x67u // animated top-wall hatch -#define ST_CHAR_CAB_ICON 0xC8u -#define ST_CHAR_ELECTROID 0x6Fu // level O's scrolling barrier glyph -#define ST_FARE_DEST_UP 0x0Bu // $7D8E: the fare aboard wants UP - -// ---- level-intro starfield -------------------------------------------- -// -// The C64's level intro streams seven star SPRITES out of the centre of -// the screen. That is the most expensive thing this port asks a present -// for: eight sprites are save-under drawn and restored every frame, and -// because a row's dirty band is one min..max interval, two stars far -// apart on the same row widen that row to nearly full width. Radially -// spreading stars maximise exactly that, so the warp ends up presenting -// close to the whole framebuffer every frame -- which is what makes it -// the slowest thing the IIgs does, and wasted work everywhere else. -// -// So the warp is not animated in pixels at all. The starfield is painted -// ONCE into screen RAM and animated by cycling the colours its cells are -// drawn in -- the same trick the title logo already uses -// (stRenderTitleLogo). A frame costs one palette upload and NO dirty -// pixel rows. -// -// The contract between the two halves is just this colour range: -// stTitle.c colours a star's cell ST_STAR_RING_FIRST + the step of its -// ray's flight (offset by the ray's launch delay), and the renderer -// cycles that range. These are C64 colour -// indices the intro screen never uses -- it draws in 0, 3 and, in demo -// mode, 4 and 5; the cab sprite adds 2, 6 and 7. -#define ST_STAR_RING_FIRST 8u -#define ST_STAR_RING_COUNT 8u - -// The renderer wraps the sweep's phase with a mask rather than a -// modulo, so the ring count has to stay a power of two. Changing it to -// something else must fail to compile here instead of quietly making -// the warp skip rings. -typedef char stStarRingCountIsPow2[((ST_STAR_RING_COUNT & (ST_STAR_RING_COUNT - 1u)) == 0u) ? 1 : -1]; -// A C64 star flies $20 frames ($4500) and the painted field marks every -// ST_STAR_FRAMES_PER_STEP of them, so the renderer steps the lit colour on -// that many intro frames (introStepCount): the warp keeps the C64's speed at -// any frame rate. -#define ST_INTRO_STAR_FLIGHT 0x20u -#define ST_STAR_FRAMES_PER_STEP (ST_INTRO_STAR_FLIGHT / ST_STAR_RING_COUNT) -// Every star is the small glyph: the C64's star is a 5x3 speck. -#define ST_STAR_GLYPH 0x2Eu // '.' - -// VIC sprite block pointers the game selects by name. The cab's bit 0 is -// the landing gear, so each facing is a pair. -#define ST_SPRITE_CAB_RIGHT 0xC0u // +1 = gear down -#define ST_SPRITE_CAB_LEFT 0xDCu // +1 = gear down -#define ST_SPRITE_PASS_WALK_R 0xC2u // +walkParity -#define ST_SPRITE_PASS_WALK_L 0xC4u // +walkParity -#define ST_SPRITE_PASS_STAND 0xC7u -#define ST_SPRITE_PASS_BEAM 0xCBu // counts down to PASS_STAND -#define ST_SPRITE_PASS_GONE 0xCCu // one past PASS_BEAM: fully shrunk -#define ST_SPRITE_WRECK_FIRST 0xCCu // the wreck cel walk, FIRST..LAST-1 -#define ST_SPRITE_WRECK_LAST 0xD1u - -// Title logo ($4827): the logo is one character repeated over rows -// 1..11, and the flip-book animates it by copying one of four frame -// glyphs over it. The glyph stays the simulation's own record of which -// frame is showing; the renderer draws all four frames as one tile in -// reserved palette slots and animates them by rewriting those entries, -// so a flip and a colour cycle both cost no repaint at all. -#define ST_LOGO_CHAR 0x84u -#define ST_LOGO_FRAME_FIRST 0x85u // $85..$88 = frames 0..3 -#define ST_LOGO_FRAMES 4u -// Offset stSimNewGame clears from: the block above it is the displayed -// picture and survives a new game (see StSimT). -#define ST_SIM_CLEAR_FROM offsetof(StSimT, level) -// Distinct characters whose glyph can change in one frame before the -// renderer gives up and repaints everything. -#define ST_CHAR_DIRTY_MAX 8u -// Characters that may hold an edited glyph at once. The game edits six -// across the whole game (the transporter hatch $67, level O's barrier -// $6F, the title logo $84 and level W's three laser glyphs $91..$93), so -// this is generous; an overflow parks the edit in the spill entry, where -// it is simply never displayed rather than corrupting another character. -#define ST_GLYPH_OVERRIDES 16u - -typedef struct { - uint8_t rows[8]; -} StGlyphT; - -// Level image as loaded from a .dat (STL4). Kept in the C64's units. -#define ST_MAX_PADS 10u -#define ST_MAX_LEVEL_SPRITES 16u -// Per-level hook code + tables live at $7D98..$7FFF in the level's data -// blob; the port keeps the bytes (for the tables) and re-expresses the -// code in stHooks.c. -#define ST_HOOK_BASE 0x7D98u -#define ST_HOOK_BYTES (0x8000u - ST_HOOK_BASE) - -// Per-level hook scratch. The original keeps a hook's running state -// inside the level's own data block, at fixed C64 addresses interleaved -// with its tables -- and, for some levels, on top of the hook's own 6502 -// code, which this port never executes. A level reads its TABLES straight -// out of the shipped blob (the single source of truth) and keeps only the -// bytes it WRITES here. Seeded from -// the blob at scene load, so the first tick starts where the original's -// did. Every level is converted, so no C64-addressed working copy of the -// blob exists any more. -#define ST_HOOK_H_SEGS 11u // level H segment ids 0..10 -#define ST_HOOK_W_LASERS 8u - -typedef struct { - uint8_t segFlag[ST_HOOK_H_SEGS]; // $7E15..$7E1F wall segment up/down - uint8_t switchId; // $7E4C also read back as a table entry - uint8_t tick; // $7E53 - uint8_t active; // $7E54 switch being animated - uint8_t step; // $7E55 0..3 of the animation - uint8_t tone; // $7E56 rising chime -} StHookHT; - -typedef struct { - uint8_t celPhase; // $7DD2 orb cel cycle 0..5 - uint8_t tick; // $7E00 - uint8_t lastSpot; // $7DD1 landing spot used last, 1..5 - uint8_t hop; // $7DD3 ticks left in a teleport - uint8_t sweep; // $7F02 the hop's falling tone -} StHookGT; - -typedef struct { - uint8_t state[ST_HW_SPRITES]; // $7DB8 0 idle, 1 rising, 2 burst - uint8_t phase[ST_HW_SPRITES]; // $7DC0 cel phase - uint8_t dx[ST_HW_SPRITES]; // $7DC8 - uint8_t dy[ST_HW_SPRITES]; // $7DD0 -} StHookIT; - -typedef struct { - uint8_t parity; // $7DBD magnet pole flip -} StHookKT; - -typedef struct { - uint8_t tick; // $7DBD mod-8 scroll gate -} StHookOT; - -typedef struct { - uint8_t phase[ST_HW_SPRITES]; // $7E83 interference cel phase -} StHookQT; - -typedef struct { - uint8_t gateShut; // $7D9C -} StHookTT; - -typedef struct { - uint8_t colorPhase; // $7DAB - uint8_t state[ST_HOOK_W_LASERS]; // $7DAC 0 idle, 1 extending, 2 fading - uint8_t row[ST_HOOK_W_LASERS]; // $7DB5 beam head / fade phase - uint8_t started; // $7DE5 -} StHookWT; - -typedef struct { - uint8_t celPhase[ST_HW_SPRITES]; // $7F4F - uint8_t state[ST_HW_SPRITES]; // $7F57 0 idle, 1 active - uint8_t dir[ST_HW_SPRITES]; // $7F5F entry edge 0..3 - uint8_t life[ST_HW_SPRITES]; // $7F6E - uint8_t cooldown; // $7F6D ticks before the next bounce -} StHookUT; - -typedef struct { - uint8_t col; // $7D9C - uint8_t dir; // $7D9D - uint8_t tick; // $7D9E -} StHookXT; - -typedef struct { - uint8_t row; // $7D98 row the leaves grow along - uint8_t seg; // $7D99 leaves grown this cycle 0..4 - uint8_t tick; // $7D9A ticks toward the next leaf - uint8_t padSlot; // $7D9B number of the next pad to hang -} StHookET; - -typedef struct { - uint8_t state[ST_HW_SPRITES]; // $7EDB 0 idle, 1 falling, 2 bursting - uint8_t phase[ST_HW_SPRITES]; // $7ED3 burst cel phase -} StHookJT; - -typedef struct { - uint8_t state[ST_HW_SPRITES]; // $7F16 0 idle, 1 falling, 2 melting - uint8_t phase[ST_HW_SPRITES]; // $7F0E melt cel phase - uint8_t parity; // $7F1F a melt advances every other count -} StHookPT; - -typedef union { - StHookET e; - StHookGT g; - StHookHT h; - StHookIT i; - StHookJT j; - StHookKT k; - StHookOT o; - StHookPT p; - StHookQT q; - StHookTT t; - StHookUT u; - StHookWT w; - StHookXT x; -} StHookScratchT; - -typedef struct { - uint8_t x1Hi; // slot byte 0: landing X lower bound, high byte - uint8_t x1Lo; // slot byte 1 - uint8_t x2Hi; // slot byte 2: landing X upper bound - uint8_t x2Lo; // slot byte 3 - uint8_t row; // slot byte 4: sprite-Y of a landed cab - uint8_t passMsb; // slot byte 5: passenger stand X msb - uint8_t passCol; // slot byte 6: passenger stand X column - uint8_t unused; // slot byte 7 -} StPadT; - -typedef struct { - uint8_t ptr; - uint8_t bitmap[ST_SPRITE_BYTES]; -} StLevelSpriteT; - -typedef struct { - uint8_t name[ST_LEVEL_NAME_CHARS + 1u]; // $7D78, screen codes - uint8_t header[9]; // $7D00..$7D08 VIC colours - uint8_t padCount; // $7D5A - uint8_t specialPad; // $7D09 - uint8_t fuelRate; // $7D60 - uint8_t spawn[5]; // $7D5B..$7D5F - uint16_t accelY; // $7D8F/90 - uint16_t accelX; // $7D91/92 - uint16_t gravY; // $7D93/94 - uint16_t gravX; // $7D95/96 - StPadT pads[ST_MAX_PADS]; // $7D0A.. - uint8_t screen[ST_SCREEN_CELLS]; // decompressed screen RAM - uint8_t color[ST_SCREEN_CELLS]; // decompressed colour RAM - uint8_t levelIndex; // 0..23 = A..X, 24 = Y, 25 = title - uint8_t spriteCount; - StLevelSpriteT sprites[ST_MAX_LEVEL_SPRITES]; - uint8_t hookData[ST_HOOK_BYTES]; // $7D98..$7FFF as shipped -} StLevelT; - -// One VIC sprite as the game sees it through its shadow tables. Padded -// to eight bytes: the 65816 compiler indexes a 6-byte element through -// a call to its 32-bit multiply routine (sim is a far pointer), and -// every sim->spr[idx] in the game paid for one; a power-of-two stride -// is three shifts. -typedef struct { - uint8_t col; // $7175,X X low byte - uint8_t msb; // $7185,X X high bit (any non-zero value = set) - uint8_t row; // $717D,X Y - uint8_t enable; // $718E,X - uint8_t ptr; // $7197,X block pointer - uint8_t color; // $719F,X - uint8_t strideToPow2[2]; -} StSpriteT; -typedef char stSpriteStrideIsPow2[((sizeof(StSpriteT) & (sizeof(StSpriteT) - 1u)) == 0u) ? 1 : -1]; - -// The frame the VIC last displayed ($4253 marshal + $4293 flush): the -// renderer draws this and the collision test evaluates it. -typedef struct { - uint16_t x[ST_HW_SPRITES]; - uint8_t y[ST_HW_SPRITES]; - uint8_t ptr[ST_HW_SPRITES]; - uint8_t color[ST_HW_SPRITES]; - uint8_t enableMask; // $7196 -> $D015 - uint8_t multiMask; // $D01C -} StFrameT; - -// Four players ("cabbies") share one game; each keeps their own score -// row, fare row, cab count and bonus-cab latch. -#define ST_MAX_PLAYERS 4u -#define ST_CABS_PER_PLAYER 6u // $4210 - -// Passenger/fare state machine stages ($7163). -typedef enum { - ST_STAGE_IDLE = 0, // $660B RNG-timed next fare - ST_STAGE_BEAM_IN, // $665F passenger materialises - ST_STAGE_WAIT, // $66B7 waves on the pad - ST_STAGE_WALK_TO_CAB, // $6CE8 walks to the landed cab - ST_STAGE_BOARD, // $66DD shrinks into the cab - ST_STAGE_RIDING, // $6739 aboard - ST_STAGE_DROP_IN, // $6742 materialises beside the cab - ST_STAGE_WALK_TO_PAD, // $6CE8 walks to the stand - ST_STAGE_LEAVE, // $67A6 shrinks away - ST_STAGE_CLEANUP // $67C6 -} StStageE; - - -// What a tick asked the host to do next (the original abandons the -// main loop with PLA/PLA on these). -typedef enum { - ST_TICK_CONTINUE = 0, - ST_TICK_LEVEL_EXIT, // $6BFB cab flew out of the top - ST_TICK_CRASH_DONE, // $6B52 wreck sequence finished (demo) - ST_TICK_LIFE_LOST, // $6BBA wreck sequence finished (game), cabs left - ST_TICK_PLAYER_OUT, // $6BD0 wreck sequence finished, no cabs left - ST_TICK_DEMO_INPUT // $5006 real input during the demo -} StTickResultE; - -typedef struct { - // ---- the displayed picture: screen RAM, colour RAM, the charset - // and the renderer's dirty bookkeeping for them. stSimNewGame - // ($5EC4) does NOT clear this block -- the C64 resets its game - // variables with the previous picture still on screen, which is how - // the attract demo's "GET READY" banner comes up over the live - // title. Everything from `level` down IS cleared; keep new fields on - // the right side of that line ---- - uint8_t screen[ST_SCREEN_CELLS]; // $0400 screen RAM - uint8_t color[ST_SCREEN_CELLS]; // $D800 colour RAM - uint8_t cellDirty[ST_SCREEN_CELLS]; // renderer clears - uint16_t dirtyList[512]; // cells marked since the last frame - uint16_t dirtyCount; - bool dirtyAll; // list overflowed / whole screen - // Reverse index: for each glyph, the exact set of cells showing it, - // as doubly-linked lists threaded through the cell array. - // - // The renderer has to repaint every cell showing a glyph whose - // BITMAP changed -- a chunky framebuffer bakes the pixels in at - // paste time, so unlike the C64's character mode there is no free - // re-composite. It used to find those cells by scanning a per-glyph - // [first, last] span, which PC profiling showed was the single - // hottest loop in the program (22% of all samples on level H). This - // gives the exact set instead: no scan, cost proportional to the - // cells that really show the glyph. - // - // screen[] is the only truth and these mirror it: setCell keeps them - // in step one cell at a time, and a bulk screen write re-derives - // them via stSimDirtyAll, which every bulk writer already calls. - // They live in the NOT-cleared block for the same reason screen[] - // does -- stSimNewGame must leave the live picture alone. - uint16_t glyphHead[ST_CHARSET_CHARS]; // ST_GLYPH_CELL_NONE = none - uint16_t cellNext[ST_SCREEN_CELLS]; - uint16_t cellPrev[ST_SCREEN_CELLS]; - // $2800. The charset is NOT copied: the ROM set is const and only the - // handful of glyphs the game rewrites at runtime are stored here, so - // this costs ~400 bytes instead of 2 KB and a scene load clears an - // index rather than copying 2 KB. Reach glyphs through stSimGlyph / - // stSimGlyphRow / stSimGlyphMut, never by indexing directly. - uint8_t glyphSlot[ST_CHARSET_CHARS]; // 0 = ROM glyph, else slot + 1 - StGlyphT glyphOverride[ST_GLYPH_OVERRIDES + 1u]; // last = spill - uint8_t glyphOverrideCount; - // Characters whose glyph bits the game has edited since the last - // frame -- the transporter hatch, the title logo flip, the laser - // beams. A short list, not a 256-entry flag array: the renderer had - // to scan all 256 every single frame to find the one or two that - // ever change. Overflow (or a scene load) sets charDirtyAll. - uint8_t charDirtyList[ST_CHAR_DIRTY_MAX]; - uint8_t charDirtyCount; - bool charDirtyAll; - uint8_t borderColor; // $D020 - uint8_t bgColor; // $D021 - // The colour the logo band's cells would carry ($4861 writes it into - // 407 cells of colour RAM); part of the picture, so it survives the - // same way colour RAM does. - uint8_t logoColor; - - // ---- cleared by stSimNewGame from here down ---- - - // ---- level image (mutable copies: hooks edit the pad table) ---- - const StLevelT *level; - StPadT pads[ST_MAX_PADS]; // $7D0A working copy - uint8_t padCount; // $7D5A working copy (level E opens pads) - uint8_t specialPad; // $7D09 working copy - - // ---- input ---- - uint8_t inputMask; // $7169 EOR $FF of $DC00: 1 = pressed - uint8_t dirMask; // $716A low 4 bits of the above - uint8_t fireWasHeld; // $71BD - uint8_t rawInput; // the host's joystick byte for this tick - - // ---- cab motion ---- - uint8_t posXlo; // $7D61 - uint8_t posXcol; // $7D62 - uint8_t posXmsb; // $7D63 - uint8_t posYlo; // $7D64 - uint8_t posYrow; // $7D65 - int16_t velX; // $714C/4D - int16_t velY; // $714E/4F - int16_t accelX; // $7148/49 - int16_t accelY; // $714A/4B - uint16_t accelTemplateX; // $7D91/92 working copy - uint16_t accelTemplateY; // $7D8F/90 - uint16_t gravTemplateX; // $7D95/96 - uint16_t gravTemplateY; // $7D93/94 - uint8_t activePad; // $7150 0 = airborne, else pad number - uint8_t activePadMirror; // $7151 UP-release latch after landing - uint8_t bobTimer; // $716F - uint8_t flameParity; // $716C - uint8_t thrustSweep; // $721B - - // ---- collision / crash ---- - uint8_t collisionPhase; // $7164 - uint8_t phaseTimer; // $7165 - uint8_t phaseReload; // $7166 - uint8_t hitDispatchResult; // $721D - uint8_t spriteSpriteColl; // $71CA latched $D01E - uint8_t spriteBgColl; // $71CB latched $D01F - // Sprites besides the cab whose $D01F bit a hook reads (levels J - // and P watch their hazards hit the scenery); the rest are never - // tested (derived state, not C64 memory). - uint8_t bgCollSprites; - // Per-sprite row masks of the displayed frame (3 bytes per row, the - // leftmost pixel is bit 7 of byte 0), rebuilt only when the sprite's - // bitmap or multicolour mode changes (derived state, not C64 memory). - uint8_t collMask[ST_HW_SPRITES][ST_SPRITE_H][3]; - const uint8_t *collMaskBm[ST_HW_SPRITES]; - uint8_t collMaskMulti[ST_HW_SPRITES]; - // First and last rows of collMask[idx] with any pixel (first == - // ST_SPRITE_H when the mask is empty): the row tests walk only these. - uint8_t collRowFirst[ST_HW_SPRITES]; - uint8_t collRowLast[ST_HW_SPRITES]; - // The cab's rows pre-shifted to each grid phase (cabWindows), built - // per phase on first use; bit s of cabWinValid says cabWin[s] - // matches collMask[0]. - uint8_t cabWin[ST_ROW_PHASES][ST_SPRITE_H][4]; - uint8_t cabWinValid; - - // ---- fuel ---- - uint8_t fuelCountdown; // $71C7 - uint8_t fuelCells; // $71C9 rightmost non-empty cell index - uint8_t fuelBarTick; // $71BE - uint8_t padAnimTick; // $71C8 - - // ---- passenger / fare machine ---- - uint8_t stage; // $7163 - uint8_t deathInProgress; // $7167 - uint8_t decayTimer; // $715D - uint8_t decayReload; // $715E - uint8_t hoverXCol; // $715F - uint8_t hoverXFrac; // $7160 (the X msb of the target) - uint8_t walkParity; // $7161 - uint8_t waveIdx; // $716E - uint8_t stage0Rng; // $71CC - uint8_t spriteSlots[11]; // $7152..$715C: [n] = pad n reserved - uint8_t fareSlotCount; // $715C - uint8_t activeSpriteIdx; // $7D8E fare's pad (spawn, then destination) - uint8_t activeDyingSlot; // $716D - uint8_t eventDispatchType; // $71CD - - // ---- sprites ---- - StSpriteT spr[ST_HW_SPRITES]; - uint8_t multiColorMask; // $D01C - StFrameT frame; - - // ---- players and progression ---- - uint8_t playerCount; // $7213 "number of cabbies" - uint8_t player; // $7214 whose turn - uint8_t playersDone; // $71CE - uint8_t cabs[ST_MAX_PLAYERS]; // $71CF - uint8_t bonusLatch[ST_MAX_PLAYERS]; // $7223 - uint8_t scoreBackup[ST_MAX_PLAYERS][ST_NUMBER_CHARS]; // $71D3, stride 8 - uint8_t fareBackup[ST_MAX_PLAYERS][ST_NUMBER_CHARS]; // $71F3, stride 8 - uint8_t levelState; // $7215 screens completed - uint8_t demoMode; // $721C - uint8_t postMortem; // $5E9D - // Jingles. The C64 plays its tunes BLOCKING ($44CF spins until the - // song ends), so the sim only RAISES a request here and the host - // plays it while holding the sim frozen -- tick-exactness survives - // because no tick happens while the tune sounds. - uint8_t jingleRequest; // song number + 1 to play now; 0 = none - uint8_t jingleRotate; // $5E9B: start-of-screen tune = ++this & 3 - - // ---- demo playback ($48F2): the $0902 buffer a recording is - // loaded into; a shorter recording leaves the previous tail behind - // and a long ride reads on into it, as on the C64 ---- - uint8_t demoBuf[640]; - uint16_t demoOff; // $3F/$40 - $0902 - uint8_t demoTimer; // $4740 - - // ---- RNG ($4080) ---- - uint8_t rngT1; // $7171 - uint8_t rngT2; // $7172 - uint32_t rngHost; // stands in for the SID noise read in a real game - - // ---- title intro / level intro ---- - uint8_t introFrameCount; // $473F - uint8_t logoColorIdx; // $48A4 - uint8_t logoCycleAux; // $48A5 - uint8_t logoDiv; // $48A6 - uint8_t introLoopCount; // $4508 - uint8_t introStepCount; // port: intro frames run, for the painted warp - uint8_t introCabDx; // $450A - uint8_t introCabDxTimer; // $450B - uint8_t spriteMc0; // $D025 - uint8_t spriteMc1; // $D026 - - // ---- per-level hook scratch ($7D9C.. and friends) ---- - StHookScratchT hookScratch; // per-level hook state - uint8_t hookLevel; // level index the hook state belongs to - uint8_t warpCounter; // $5A66 - uint8_t warpMask; // $5CE8 - uint8_t animPhase; // $5CF1 -} StSimT; - -// ---- simulation API (stSim.c) ---- - -// Bind a level and run the scene-load + prelude ($62F0 + $5F18) for a -// fresh screen: copy the image, restore the player's HUD rows, draw -// the cab icons, spawn the cab, refill the fuel. -void stSimEnterLevel(StSimT *sim, const StLevelT *level); -// Re-entry after a crash with cabs left ($5F27): spawn + refill only. -void stSimRespawn(StSimT *sim); -// One game tick = one $5F40 iteration (two video frames on the C64). -StTickResultE stSimTick(StSimT *sim); -// Seed the whole struct for a new game ($5EC4 + $4207 + $48AD). -void stSimNewGame(StSimT *sim, uint8_t playerCount, bool demo); -// Seed the playback buffer with the boot-time RAM image, once. -void stSimSeedDemoBuffer(StSimT *sim, const uint8_t *image, uint16_t len); -// Load a recording into the buffer and rewind ($0902 + $4740 seeding). -void stSimSetDemoStream(StSimT *sim, const uint8_t *stream, uint16_t len); -// Per-player bookkeeping at a screen change ($61FB advanceFareSlot): -// returns true when the turn wrapped to player 0 (time for a new screen). -bool stSimAdvancePlayer(StSimT *sim); -// Archive the current player's HUD rows ($704E). -void stSimArchiveHud(StSimT *sim); -// Mark every cell dirty (scene change). -void stSimDirtyAll(StSimT *sim); -void stSimDirtyCells(StSimT *sim); // repaint all, glyph index kept -// $43A5 -- blank the fare meter back to its template. -void stSimHudInit(StSimT *sim); -// $4253 + $4293 -- snapshot the sprite shadows into the displayed frame. -void stSimMarshal(StSimT *sim); -// $63D0 -- rotate every row of a glyph one pixel right and mark it dirty. -void stSimRotateGlyph(StSimT *sim, uint8_t ch); -// A character's eight 1bpp rows: the ROM glyph, or the game's edit of it. -// kStBit[i] == 1 << i. A variable shift is a library bit loop on the -// 65816, so sprite-index bit tests go through this table instead. -extern const uint8_t kStBit[ST_HW_SPRITES]; - -const uint8_t *stSimGlyph(const StSimT *sim, uint8_t ch); -uint8_t stSimGlyphRow(const StSimT *sim, uint8_t ch, uint8_t row); -// The same eight rows, writable (copied from ROM on the first edit). -uint8_t *stSimGlyphMut(StSimT *sim, uint8_t ch); -// Drop every edit: the whole charset is the ROM set again (scene load). -void stSimGlyphReset(StSimT *sim); -// The 17-bit VIC sprite X (msb:col), read and signed-byte add ($411B). -uint16_t stSimSpriteX(const StSimT *sim, uint8_t idx); -void stSimSpriteAddX(StSimT *sim, uint8_t idx, uint8_t delta); -// $6C38 -- blank the 11-character message row. -void stSimClearMessage(StSimT *sim); -// A glyph's bits changed: every cell showing it has to be repainted. -void stSimMarkChar(StSimT *sim, uint8_t ch); -// Text and HUD primitives (screen RAM writes). -void stSimDrawText(StSimT *sim, uint8_t col, uint8_t row, const uint8_t *text, uint8_t color); -void stSimPutChar(StSimT *sim, uint8_t col, uint8_t row, uint8_t ch); -void stSimPutColor(StSimT *sim, uint8_t col, uint8_t row, uint8_t color); -void stSimBcdAdd(StSimT *sim, uint16_t dstCell, const uint8_t *blob); -bool stSimDecrementNumber(StSimT *sim, uint16_t cell, uint8_t position); -uint8_t stSimRng(StSimT *sim, uint8_t n); -// Sprite-mask lookup shared with the renderer: 21 rows of 24-bit masks. -const uint8_t *stSimSpriteBitmap(const StSimT *sim, uint8_t ptr); -// The score the current player has on screen, as pennies. -uint32_t stSimScorePennies(const StSimT *sim); -// $6866 / $6877 -- the pad indicator colour cells. -void stSimPadLight(StSimT *sim, bool on); -// $6D6A -- the exhaust sprite (shared with the title lift-off). -void stSimFlameUpdate(StSimT *sim); - -// ---- level file (stLevelFile.c) ---- -bool stLevelParse(StLevelT *out, FILE *fp); - -// ---- passenger machine (stFare.c) ---- -void stFareStageDispatch(StSimT *sim); // $65C1 -void stFarePostTickGate(StSimT *sim); // $67E3 -void stFarePadLightingGate(StSimT *sim); // $6EF0 -void stFareUpPlease(StSimT *sim); // $6CB8 -uint8_t stFareChooseDestination(StSimT *sim); // $6537: reserves a pad, returns its number -void stFareDrawMessage(StSimT *sim, const uint8_t *text); -void stFareSquashed(StSimT *sim); // $69B4..$69F8 -void stFareBoardingSound(StSimT *sim, const uint8_t *speech); // $6C7F -void stFareWaveStep(StSimT *sim); // $66B7 -void stFareWalkStep(StSimT *sim); // $6D0D -void stFareLeaveStep(StSimT *sim); // $67A6 - -// ---- title screen and level intro (stTitle.c) ---- -void stTitleEnter(StSimT *sim, const StLevelT *title); // $477A + $47B0..$47E1 -bool stTitleTick(StSimT *sim); // $47E4 loop body; true once the intro cab has left -void stIntroEnter(StSimT *sim, const StLevelT *level); // $4523..$4665 level-name screen -bool stIntroStep(StSimT *sim); // one $4666 frame; true when finished - -// ---- per-level hooks (stHooks.c) ---- -void stHookPrelude0(StSimT *sim); // $7D66 -void stHookPrelude1(StSimT *sim); // $7D69 -void stHookPerTick2(StSimT *sim); // $7D6C -void stHookPerTick3(StSimT *sim); // $7D6F -uint8_t stHookInput(StSimT *sim, uint8_t input); // $7D72 -uint8_t stHookHitVerdict(StSimT *sim); // $7D75: 0 = passenger contact is safe -// Seed a converted level's scratch from the shipped blob (scene load). -void stHookSceneLoad(StSimT *sim); - -// ---- audio sink (host provides; stAudio.c / stsim stub) ---- -void stAudioSfx(const uint8_t *program9); // $42E9 load -void stAudioNoise(bool on); // $D412 = $81 / $80 -void stAudioThrustSweep(uint8_t value); // $D400/$D401 = value -void stAudioSpeech(uint8_t ch); // $9802 -void stAudioSpeechPhrase(const uint8_t *speech); -void stAudioSilence(void); // voices 1+2 gate off -void stAudioJingle(uint8_t song); // $44CF: silence, then play song (caller blocks) -bool stAudioJingleActive(void); // still sounding? -void stAudioMusic(uint8_t song, bool loop); // $CB02: silence, then play song in the background -void stAudioVoice2(uint8_t freqLo, uint8_t freqHi, uint8_t ctrl); // level-hook tones -void stAudioVoice1Freq(uint8_t value); // $D400/$D401 pokes from the fuel pump - -#endif diff --git a/examples/spacetaxi/stSongData.h b/examples/spacetaxi/stSongData.h deleted file mode 100644 index f88621e..0000000 --- a/examples/spacetaxi/stSongData.h +++ /dev/null @@ -1,512 +0,0 @@ -// Space Taxi (C64) tunes as JoeyLib JYM1 streams. GENERATED by -// assets/genSongData.py -- do not hand-edit. The songs were extracted by -// running the game's own music player under a 6502 emulator -// (assets/sidCapture.py logs every SID write per PAL frame; -// assets/sidToSong.py turns that into songbake text in assets/songs/). -// -// Where the C64 plays them: -// BLOCKING via $44CF (the game spins until the tune ends; the port -// parks in ST_STATE_JINGLE with the sim frozen): -// 0..3 start of every screen, rotating on $5E9B ($6906) -// 4 level 25 start ($6935) and the bonus cab ($70B1) -// 5 game over ($5C1F), after the two-second GAME OVER! wait -// NON-BLOCKING via $CB02 (started and left to the IRQ player): -// 6 the high-score name entry screen ($4E35) -- no port screen yet -// 7 the high-score table ($4C1F, joystick UP on the title) -// 8 the level intro while the cab flies in ($459C); endless, so -// the source is trimmed to 30 s and looped -#ifndef ST_SONG_DATA_H -#define ST_SONG_DATA_H - -#include - -static const uint8_t gStSong0[109] = { - 0x4A, 0x59, 0x4D, 0x31, 0x14, 0x00, 0xFF, 0xFF, 0x81, 0x10, 0x50, 0x01, - 0x02, 0x11, 0xA8, 0x01, 0x02, 0x83, 0x20, 0x21, 0x85, 0x10, 0x7A, 0x01, - 0x02, 0x11, 0xA8, 0x01, 0x02, 0x83, 0x20, 0x21, 0x85, 0x10, 0x50, 0x01, - 0x02, 0x11, 0xA8, 0x01, 0x02, 0x83, 0x20, 0x21, 0x85, 0x10, 0x7A, 0x01, - 0x02, 0x11, 0xDC, 0x01, 0x02, 0x83, 0x20, 0x21, 0x85, 0x10, 0x3E, 0x01, - 0x02, 0x11, 0xFC, 0x00, 0x02, 0x83, 0x21, 0x85, 0x11, 0xDC, 0x01, 0x02, - 0x83, 0x20, 0x21, 0x85, 0x10, 0x1B, 0x01, 0x02, 0x11, 0xFC, 0x00, 0x02, - 0x83, 0x21, 0x85, 0x11, 0xDC, 0x01, 0x02, 0x83, 0x20, 0x21, 0x85, 0x10, - 0xFC, 0x00, 0x02, 0x11, 0xFC, 0x00, 0x02, 0x8B, 0x21, 0x88, 0x20, 0x86, - 0x00, -}; - -static const uint8_t gStSong1[119] = { - 0x4A, 0x59, 0x4D, 0x31, 0x14, 0x00, 0xFF, 0xFF, 0x81, 0x10, 0xFC, 0x00, - 0x07, 0x11, 0x9F, 0x00, 0x07, 0x87, 0x20, 0x21, 0x81, 0x10, 0x1B, 0x01, - 0x07, 0x11, 0xA8, 0x00, 0x07, 0x87, 0x20, 0x21, 0x81, 0x10, 0xFC, 0x00, - 0x07, 0x11, 0xBD, 0x00, 0x07, 0x87, 0x20, 0x21, 0x81, 0x10, 0x1B, 0x01, - 0x07, 0x11, 0xD4, 0x00, 0x07, 0x87, 0x20, 0x21, 0x81, 0x10, 0x3E, 0x01, - 0x07, 0x11, 0xBD, 0x00, 0x07, 0x87, 0x20, 0x21, 0x81, 0x10, 0x50, 0x01, - 0x07, 0x11, 0xA8, 0x00, 0x07, 0x87, 0x20, 0x21, 0x81, 0x10, 0x3E, 0x01, - 0x07, 0x11, 0x9F, 0x00, 0x07, 0x87, 0x20, 0x21, 0x81, 0x10, 0x50, 0x01, - 0x07, 0x11, 0xA8, 0x00, 0x07, 0x87, 0x20, 0x21, 0x81, 0x10, 0x7A, 0x01, - 0x07, 0x11, 0xBD, 0x00, 0x07, 0x87, 0x21, 0x90, 0x20, 0x82, 0x00, -}; - -static const uint8_t gStSong2[128] = { - 0x4A, 0x59, 0x4D, 0x31, 0x14, 0x00, 0xFF, 0xFF, 0x81, 0x10, 0x7E, 0x00, - 0x00, 0x11, 0x9F, 0x00, 0x00, 0x85, 0x21, 0x82, 0x20, 0x81, 0x10, 0x7E, - 0x00, 0x00, 0x11, 0xA8, 0x00, 0x00, 0x85, 0x21, 0x82, 0x20, 0x81, 0x10, - 0x8D, 0x00, 0x00, 0x11, 0xBD, 0x00, 0x00, 0x85, 0x21, 0x82, 0x20, 0x81, - 0x10, 0x9F, 0x00, 0x00, 0x11, 0x9F, 0x00, 0x00, 0x85, 0x21, 0x82, 0x20, - 0x81, 0x10, 0x9F, 0x00, 0x00, 0x11, 0xA8, 0x00, 0x00, 0x85, 0x21, 0x82, - 0x20, 0x81, 0x10, 0xA8, 0x00, 0x00, 0x11, 0xBD, 0x00, 0x00, 0x85, 0x21, - 0x82, 0x20, 0x81, 0x10, 0xA8, 0x00, 0x00, 0x11, 0x9F, 0x00, 0x00, 0x85, - 0x21, 0x82, 0x20, 0x81, 0x10, 0xBD, 0x00, 0x00, 0x11, 0xA8, 0x00, 0x00, - 0x85, 0x21, 0x82, 0x20, 0x81, 0x11, 0x9F, 0x00, 0x00, 0x85, 0x21, 0x83, - 0x11, 0x7E, 0x00, 0x00, 0x85, 0x21, 0x84, 0x00, -}; - -static const uint8_t gStSong3[154] = { - 0x4A, 0x59, 0x4D, 0x31, 0x14, 0x00, 0xFF, 0xFF, 0x81, 0x10, 0xFC, 0x00, - 0x00, 0x11, 0x50, 0x01, 0x02, 0x12, 0x50, 0x01, 0x02, 0x83, 0x20, 0x21, - 0x22, 0x82, 0x10, 0x1B, 0x01, 0x00, 0x83, 0x20, 0x82, 0x10, 0x3E, 0x01, - 0x00, 0x11, 0x50, 0x01, 0x02, 0x12, 0x50, 0x01, 0x02, 0x83, 0x20, 0x21, - 0x22, 0x82, 0x10, 0xFC, 0x00, 0x00, 0x83, 0x20, 0x82, 0x10, 0x1B, 0x01, - 0x00, 0x11, 0x50, 0x01, 0x02, 0x12, 0x50, 0x01, 0x02, 0x83, 0x20, 0x21, - 0x22, 0x82, 0x10, 0xFC, 0x00, 0x00, 0x83, 0x20, 0x82, 0x10, 0x1B, 0x01, - 0x00, 0x11, 0x50, 0x01, 0x02, 0x12, 0x50, 0x01, 0x02, 0x83, 0x20, 0x21, - 0x22, 0x82, 0x10, 0xDC, 0x01, 0x00, 0x83, 0x20, 0x82, 0x11, 0x3E, 0x01, - 0x02, 0x12, 0x7A, 0x01, 0x02, 0x83, 0x21, 0x22, 0x87, 0x11, 0x50, 0x01, - 0x02, 0x12, 0x7A, 0x01, 0x02, 0x83, 0x21, 0x22, 0x87, 0x11, 0x1B, 0x01, - 0x02, 0x12, 0xFC, 0x00, 0x02, 0x83, 0x21, 0x22, 0x87, 0x11, 0xFC, 0x00, - 0x02, 0x12, 0xDC, 0x01, 0x02, 0x83, 0x21, 0x22, 0x88, 0x00, -}; - -static const uint8_t gStSong4[92] = { - 0x4A, 0x59, 0x4D, 0x31, 0x14, 0x00, 0xFF, 0xFF, 0x81, 0x10, 0xF8, 0x01, - 0x05, 0x11, 0xC1, 0x01, 0x05, 0x87, 0x20, 0x21, 0x89, 0x10, 0xF8, 0x01, - 0x05, 0x11, 0xC1, 0x01, 0x05, 0x87, 0x20, 0x21, 0x81, 0x10, 0xA1, 0x02, - 0x05, 0x11, 0xF8, 0x01, 0x05, 0x97, 0x20, 0x21, 0x99, 0x10, 0xF8, 0x01, - 0x05, 0x11, 0xA8, 0x01, 0x05, 0x87, 0x20, 0x21, 0x89, 0x10, 0xA1, 0x02, - 0x05, 0x11, 0xF8, 0x01, 0x05, 0x87, 0x20, 0x21, 0x81, 0x10, 0x50, 0x03, - 0x05, 0x11, 0xA1, 0x02, 0x05, 0x88, 0x11, 0x7B, 0x02, 0x05, 0x88, 0x11, - 0xA1, 0x02, 0x05, 0x87, 0x20, 0x21, 0x82, 0x00, -}; - -static const uint8_t gStSong5[890] = { - 0x4A, 0x59, 0x4D, 0x31, 0x14, 0x00, 0xFF, 0xFF, 0x81, 0x10, 0xF8, 0x01, - 0x02, 0x11, 0x9F, 0x00, 0x02, 0x12, 0x6A, 0x00, 0x0A, 0x83, 0x21, 0x82, - 0x12, 0xD4, 0x00, 0x0A, 0x85, 0x11, 0x9F, 0x00, 0x02, 0x12, 0x6A, 0x00, - 0x0A, 0x83, 0x21, 0x82, 0x12, 0xD4, 0x00, 0x0A, 0x85, 0x11, 0x9F, 0x00, - 0x02, 0x12, 0x6A, 0x00, 0x0A, 0x83, 0x21, 0x82, 0x12, 0xD4, 0x00, 0x0A, - 0x85, 0x11, 0x9F, 0x00, 0x02, 0x12, 0x6A, 0x00, 0x0A, 0x83, 0x21, 0x81, - 0x20, 0x81, 0x12, 0xD4, 0x00, 0x0A, 0x85, 0x10, 0xF8, 0x01, 0x02, 0x11, - 0x9F, 0x00, 0x02, 0x12, 0x6A, 0x00, 0x0A, 0x83, 0x21, 0x82, 0x12, 0xD4, - 0x00, 0x0A, 0x85, 0x11, 0x9F, 0x00, 0x02, 0x12, 0x6A, 0x00, 0x0A, 0x83, - 0x21, 0x82, 0x12, 0xD4, 0x00, 0x0A, 0x85, 0x11, 0x9F, 0x00, 0x02, 0x12, - 0x6A, 0x00, 0x0A, 0x83, 0x21, 0x82, 0x12, 0xD4, 0x00, 0x0A, 0x85, 0x11, - 0x9F, 0x00, 0x02, 0x12, 0x6A, 0x00, 0x0A, 0x83, 0x21, 0x81, 0x20, 0x81, - 0x12, 0xD4, 0x00, 0x0A, 0x85, 0x10, 0xF8, 0x01, 0x02, 0x11, 0x9F, 0x00, - 0x02, 0x12, 0x6A, 0x00, 0x0A, 0x83, 0x21, 0x82, 0x12, 0xD4, 0x00, 0x0A, - 0x85, 0x11, 0x9F, 0x00, 0x02, 0x12, 0x6A, 0x00, 0x0A, 0x83, 0x21, 0x82, - 0x12, 0xD4, 0x00, 0x0A, 0x85, 0x11, 0x9F, 0x00, 0x02, 0x12, 0x6A, 0x00, - 0x0A, 0x83, 0x21, 0x81, 0x20, 0x81, 0x12, 0xD4, 0x00, 0x0A, 0x83, 0x22, - 0x82, 0x10, 0x36, 0x02, 0x02, 0x11, 0x8D, 0x00, 0x02, 0x12, 0x64, 0x00, - 0x0A, 0x83, 0x21, 0x81, 0x20, 0x81, 0x12, 0xC8, 0x00, 0x0A, 0x85, 0x10, - 0x36, 0x02, 0x02, 0x11, 0x8D, 0x00, 0x02, 0x12, 0x64, 0x00, 0x0A, 0x83, - 0x21, 0x82, 0x12, 0xC8, 0x00, 0x0A, 0x85, 0x11, 0x8D, 0x00, 0x02, 0x12, - 0x64, 0x00, 0x0A, 0x83, 0x21, 0x82, 0x12, 0xC8, 0x00, 0x0A, 0x85, 0x11, - 0x8D, 0x00, 0x02, 0x12, 0x64, 0x00, 0x0A, 0x83, 0x21, 0x82, 0x12, 0xC8, - 0x00, 0x0A, 0x83, 0x22, 0x82, 0x11, 0x8D, 0x00, 0x02, 0x12, 0x64, 0x00, - 0x0A, 0x83, 0x21, 0x81, 0x20, 0x81, 0x12, 0xC8, 0x00, 0x0A, 0x85, 0x10, - 0x7B, 0x02, 0x02, 0x11, 0xF8, 0x01, 0x02, 0x12, 0x5E, 0x00, 0x0A, 0x84, - 0x20, 0x21, 0x81, 0x12, 0xBD, 0x00, 0x0A, 0x85, 0x12, 0x5E, 0x00, 0x0A, - 0x85, 0x12, 0xBD, 0x00, 0x0A, 0x85, 0x10, 0xA1, 0x02, 0x02, 0x11, 0x36, - 0x02, 0x02, 0x12, 0x54, 0x00, 0x0A, 0x84, 0x20, 0x21, 0x81, 0x12, 0xA8, - 0x00, 0x0A, 0x85, 0x12, 0x54, 0x00, 0x0A, 0x85, 0x12, 0xA8, 0x00, 0x0A, - 0x85, 0x10, 0xF3, 0x02, 0x02, 0x11, 0x7B, 0x02, 0x02, 0x12, 0x4F, 0x00, - 0x0A, 0x84, 0x20, 0x21, 0x81, 0x12, 0x9F, 0x00, 0x0A, 0x85, 0x12, 0x4F, - 0x00, 0x0A, 0x85, 0x12, 0x9F, 0x00, 0x0A, 0x85, 0x10, 0x20, 0x03, 0x02, - 0x11, 0x7B, 0x02, 0x02, 0x12, 0x47, 0x00, 0x0A, 0x84, 0x20, 0x21, 0x81, - 0x12, 0x8D, 0x00, 0x0A, 0x85, 0x12, 0x47, 0x00, 0x0A, 0x85, 0x12, 0x8D, - 0x00, 0x0A, 0x85, 0x10, 0x50, 0x03, 0x02, 0x11, 0x7B, 0x02, 0x02, 0x12, - 0x43, 0x00, 0x0A, 0x85, 0x12, 0x85, 0x00, 0x0A, 0x85, 0x12, 0x43, 0x00, - 0x0A, 0x85, 0x12, 0x85, 0x00, 0x0A, 0x85, 0x12, 0x43, 0x00, 0x0A, 0x85, - 0x12, 0x85, 0x00, 0x0A, 0x85, 0x12, 0x43, 0x00, 0x0A, 0x85, 0x12, 0x85, - 0x00, 0x0A, 0x85, 0x10, 0xA8, 0x01, 0x02, 0x11, 0x3E, 0x01, 0x02, 0x12, - 0x43, 0x00, 0x0A, 0x84, 0x20, 0x21, 0x81, 0x12, 0x85, 0x00, 0x0A, 0x85, - 0x22, 0x9E, 0x10, 0x50, 0x03, 0x02, 0x11, 0x50, 0x01, 0x02, 0x12, 0xFC, - 0x00, 0x0A, 0xA2, 0x20, 0x21, 0x84, 0x22, 0x82, 0x10, 0x50, 0x03, 0x02, - 0x11, 0x50, 0x01, 0x02, 0x12, 0xFC, 0x00, 0x0A, 0xA2, 0x20, 0x21, 0x84, - 0x22, 0x82, 0x10, 0x50, 0x03, 0x02, 0x11, 0x50, 0x01, 0x02, 0x12, 0xFC, - 0x00, 0x0A, 0x98, 0x20, 0x21, 0x84, 0x22, 0x82, 0x10, 0xF3, 0x02, 0x02, - 0x11, 0x3E, 0x01, 0x02, 0x12, 0xE1, 0x00, 0x0A, 0x84, 0x20, 0x21, 0x84, - 0x22, 0x82, 0x10, 0xF3, 0x02, 0x02, 0x11, 0x3E, 0x01, 0x02, 0x12, 0xE1, - 0x00, 0x0A, 0xA2, 0x20, 0x21, 0x84, 0x22, 0x82, 0x10, 0xA1, 0x02, 0x02, - 0x11, 0x1B, 0x01, 0x02, 0x12, 0xD4, 0x00, 0x0A, 0xA2, 0x21, 0x84, 0x22, - 0x82, 0x10, 0x7B, 0x02, 0x02, 0x11, 0xFC, 0x00, 0x02, 0x12, 0xBD, 0x00, - 0x0A, 0x84, 0x20, 0x21, 0x84, 0x22, 0x8C, 0x10, 0x36, 0x02, 0x02, 0x11, - 0x1B, 0x01, 0x02, 0x12, 0xA8, 0x00, 0x0A, 0x84, 0x20, 0x21, 0x84, 0x22, - 0x8C, 0x10, 0xF3, 0x02, 0x02, 0x11, 0x3E, 0x01, 0x02, 0x12, 0x7E, 0x00, - 0x0A, 0x85, 0x12, 0xBD, 0x00, 0x0A, 0x85, 0x12, 0x7E, 0x00, 0x0A, 0x85, - 0x12, 0xBD, 0x00, 0x0A, 0x85, 0x12, 0x7E, 0x00, 0x0A, 0x85, 0x12, 0xBD, - 0x00, 0x0A, 0x85, 0x12, 0x7E, 0x00, 0x0A, 0x85, 0x12, 0xBD, 0x00, 0x0A, - 0x85, 0x12, 0x7E, 0x00, 0x0A, 0x84, 0x20, 0x81, 0x12, 0xBD, 0x00, 0x0A, - 0x85, 0x10, 0x7B, 0x02, 0x02, 0x12, 0x7E, 0x00, 0x0A, 0x84, 0x20, 0x81, - 0x12, 0xBD, 0x00, 0x0A, 0x85, 0x10, 0xF8, 0x01, 0x02, 0x12, 0x7E, 0x00, - 0x0A, 0x84, 0x20, 0x81, 0x12, 0xBD, 0x00, 0x0A, 0x85, 0x10, 0x7B, 0x02, - 0x02, 0x12, 0x7E, 0x00, 0x0A, 0x84, 0x20, 0x21, 0x81, 0x12, 0xBD, 0x00, - 0x0A, 0x83, 0x22, 0x82, 0x10, 0xF3, 0x02, 0x02, 0x11, 0x1B, 0x01, 0x02, - 0x12, 0x77, 0x00, 0x0A, 0x85, 0x12, 0xBD, 0x00, 0x0A, 0x85, 0x12, 0x77, - 0x00, 0x0A, 0x84, 0x20, 0x81, 0x12, 0xBD, 0x00, 0x0A, 0x85, 0x10, 0x50, - 0x03, 0x02, 0x12, 0x77, 0x00, 0x0A, 0x85, 0x12, 0xBD, 0x00, 0x0A, 0x85, - 0x12, 0x77, 0x00, 0x0A, 0x84, 0x20, 0x81, 0x12, 0xBD, 0x00, 0x0A, 0x85, - 0x10, 0xF3, 0x02, 0x02, 0x12, 0x77, 0x00, 0x0A, 0x84, 0x20, 0x81, 0x12, - 0xBD, 0x00, 0x0A, 0x85, 0x10, 0xA1, 0x02, 0x02, 0x12, 0x77, 0x00, 0x0A, - 0x84, 0x20, 0x81, 0x12, 0xBD, 0x00, 0x0A, 0x85, 0x10, 0x7B, 0x02, 0x02, - 0x12, 0x77, 0x00, 0x0A, 0x84, 0x20, 0x81, 0x12, 0xBD, 0x00, 0x0A, 0x85, - 0x10, 0x36, 0x02, 0x02, 0x12, 0x77, 0x00, 0x0A, 0x84, 0x20, 0x21, 0x81, - 0x12, 0xBD, 0x00, 0x0A, 0x83, 0x22, 0x82, 0x10, 0xF8, 0x01, 0x02, 0x11, - 0x3E, 0x01, 0x02, 0x12, 0x7E, 0x00, 0x0A, 0x84, 0x20, 0x21, 0x84, 0x22, - 0x83, 0x00, -}; - -static const uint8_t gStSong6[611] = { - 0x4A, 0x59, 0x4D, 0x31, 0x14, 0x00, 0xFF, 0xFF, 0x81, 0x11, 0x7A, 0x01, - 0x01, 0x12, 0xF8, 0x01, 0x01, 0x88, 0x21, 0x22, 0x90, 0x11, 0x7A, 0x01, - 0x01, 0x12, 0xF8, 0x01, 0x01, 0x82, 0x21, 0x22, 0x84, 0x11, 0x7A, 0x01, - 0x01, 0x12, 0xF8, 0x01, 0x01, 0x82, 0x21, 0x22, 0x84, 0x11, 0x7A, 0x01, - 0x01, 0x12, 0xF8, 0x01, 0x01, 0xA0, 0x21, 0x22, 0x84, 0x10, 0x5E, 0x00, - 0x00, 0x86, 0x20, 0x86, 0x10, 0x7E, 0x00, 0x00, 0x86, 0x20, 0x86, 0x10, - 0x9F, 0x00, 0x00, 0x86, 0x20, 0x86, 0x10, 0xBD, 0x00, 0x00, 0x11, 0x50, - 0x01, 0x01, 0x12, 0xC1, 0x01, 0x01, 0x88, 0x21, 0x22, 0x84, 0x11, 0x2C, - 0x01, 0x01, 0x12, 0x90, 0x01, 0x01, 0x88, 0x21, 0x22, 0x84, 0x11, 0x50, - 0x01, 0x01, 0x12, 0xC1, 0x01, 0x01, 0x88, 0x21, 0x22, 0x84, 0x11, 0x7A, - 0x01, 0x01, 0x12, 0xF8, 0x01, 0x01, 0x88, 0x21, 0x22, 0x90, 0x11, 0x7A, - 0x01, 0x01, 0x12, 0xF8, 0x01, 0x01, 0x82, 0x21, 0x22, 0x84, 0x20, 0x11, - 0x7A, 0x01, 0x01, 0x12, 0xF8, 0x01, 0x01, 0x82, 0x21, 0x22, 0x84, 0x11, - 0x7A, 0x01, 0x01, 0x12, 0xF8, 0x01, 0x01, 0x92, 0x10, 0xD4, 0x00, 0x00, - 0x83, 0x20, 0x8B, 0x21, 0x22, 0x84, 0x10, 0xBD, 0x00, 0x00, 0x83, 0x20, - 0x8F, 0x10, 0x9F, 0x00, 0x00, 0x83, 0x20, 0x83, 0x11, 0x7A, 0x01, 0x01, - 0x12, 0xF8, 0x01, 0x01, 0x82, 0x21, 0x22, 0x84, 0x11, 0x7A, 0x01, 0x01, - 0x12, 0xF8, 0x01, 0x01, 0x82, 0x21, 0x22, 0x84, 0x10, 0x8D, 0x00, 0x00, - 0x11, 0xA8, 0x01, 0x01, 0x12, 0x36, 0x02, 0x01, 0x88, 0x21, 0x22, 0x84, - 0x11, 0x7A, 0x01, 0x01, 0x12, 0xF8, 0x01, 0x01, 0x88, 0x21, 0x22, 0x84, - 0x11, 0xA8, 0x01, 0x01, 0x12, 0x36, 0x02, 0x01, 0x86, 0x20, 0x82, 0x21, - 0x22, 0x84, 0x10, 0x77, 0x00, 0x00, 0x11, 0xDC, 0x01, 0x01, 0x12, 0x7B, - 0x02, 0x01, 0x88, 0x21, 0x22, 0x90, 0x11, 0xDC, 0x01, 0x01, 0x12, 0x7B, - 0x02, 0x01, 0x82, 0x21, 0x22, 0x84, 0x11, 0xDC, 0x01, 0x01, 0x12, 0x7B, - 0x02, 0x01, 0x82, 0x21, 0x22, 0x84, 0x11, 0xDC, 0x01, 0x01, 0x12, 0x7B, - 0x02, 0x01, 0x92, 0x20, 0x8E, 0x21, 0x22, 0x84, 0x10, 0x3B, 0x00, 0x00, - 0x98, 0x11, 0xDC, 0x01, 0x01, 0x12, 0x7B, 0x02, 0x01, 0x82, 0x21, 0x22, - 0x84, 0x11, 0xDC, 0x01, 0x01, 0x12, 0x7B, 0x02, 0x01, 0x82, 0x21, 0x22, - 0x84, 0x11, 0xDC, 0x01, 0x01, 0x12, 0x7B, 0x02, 0x01, 0x88, 0x21, 0x22, - 0x84, 0x11, 0xA8, 0x01, 0x01, 0x12, 0x36, 0x02, 0x01, 0x86, 0x20, 0x82, - 0x21, 0x22, 0x84, 0x11, 0xDC, 0x01, 0x01, 0x12, 0x7B, 0x02, 0x01, 0x88, - 0x21, 0x22, 0x84, 0x10, 0x7E, 0x00, 0x00, 0x11, 0xF8, 0x01, 0x01, 0x12, - 0xA1, 0x02, 0x01, 0x88, 0x21, 0x22, 0x90, 0x11, 0xF8, 0x01, 0x01, 0x12, - 0xA1, 0x02, 0x01, 0x82, 0x21, 0x22, 0x84, 0x11, 0xF8, 0x01, 0x01, 0x12, - 0xA1, 0x02, 0x01, 0x82, 0x21, 0x22, 0x84, 0x11, 0xF8, 0x01, 0x01, 0x12, - 0xA1, 0x02, 0x01, 0x92, 0x20, 0x8E, 0x21, 0x22, 0x84, 0x10, 0x3F, 0x00, - 0x00, 0x98, 0x11, 0xF8, 0x01, 0x01, 0x12, 0xA1, 0x02, 0x01, 0x82, 0x21, - 0x22, 0x84, 0x20, 0x11, 0xF8, 0x01, 0x01, 0x12, 0xA1, 0x02, 0x01, 0x82, - 0x21, 0x22, 0x84, 0x10, 0x7E, 0x00, 0x00, 0x11, 0x36, 0x02, 0x01, 0x12, - 0xF3, 0x02, 0x01, 0x88, 0x21, 0x22, 0x84, 0x11, 0xF8, 0x01, 0x01, 0x12, - 0xA1, 0x02, 0x01, 0x86, 0x20, 0x82, 0x21, 0x22, 0x84, 0x11, 0x36, 0x02, - 0x01, 0x12, 0xF3, 0x02, 0x01, 0x88, 0x21, 0x22, 0x84, 0x10, 0x8D, 0x00, - 0x00, 0x11, 0x7B, 0x02, 0x01, 0x12, 0x50, 0x03, 0x01, 0xB2, 0x21, 0x84, - 0x20, 0x82, 0x22, 0x90, 0x10, 0x47, 0x00, 0x00, 0x98, 0x11, 0x7B, 0x02, - 0x01, 0x12, 0x50, 0x03, 0x01, 0x82, 0x21, 0x22, 0x84, 0x20, 0x11, 0x7B, - 0x02, 0x01, 0x12, 0x50, 0x03, 0x01, 0x82, 0x21, 0x22, 0x84, 0x10, 0x8D, - 0x00, 0x00, 0x11, 0x36, 0x02, 0x01, 0x12, 0x50, 0x03, 0x01, 0x88, 0x21, - 0x22, 0x84, 0x11, 0x36, 0x02, 0x01, 0x12, 0x50, 0x03, 0x01, 0x88, 0x21, - 0x22, 0x84, 0x11, 0x36, 0x02, 0x01, 0x12, 0x50, 0x03, 0x01, 0x86, 0x20, - 0x82, 0x21, 0x22, 0x84, 0x10, 0x9F, 0x00, 0x00, 0x11, 0x16, 0x02, 0x01, - 0x12, 0x50, 0x03, 0x01, 0xEC, 0x20, 0x22, 0x82, 0x21, 0x85, 0x00, -}; - -static const uint8_t gStSong7[777] = { - 0x4A, 0x59, 0x4D, 0x31, 0x14, 0x00, 0xFF, 0xFF, 0x81, 0x10, 0x7A, 0x01, - 0x00, 0x11, 0xF6, 0x04, 0x05, 0x12, 0x7E, 0x00, 0x07, 0x8A, 0x22, 0x84, - 0x21, 0x86, 0x11, 0x42, 0x05, 0x05, 0x85, 0x11, 0xF6, 0x04, 0x05, 0x84, - 0x21, 0x81, 0x11, 0x6C, 0x04, 0x05, 0x12, 0x77, 0x00, 0x07, 0x84, 0x21, - 0x86, 0x11, 0xF6, 0x04, 0x05, 0x22, 0x84, 0x21, 0x86, 0x11, 0x42, 0x05, - 0x05, 0x83, 0x20, 0x81, 0x21, 0x86, 0x10, 0xFC, 0x00, 0x00, 0x11, 0xF6, - 0x04, 0x05, 0x12, 0x6A, 0x00, 0x07, 0x84, 0x21, 0x86, 0x11, 0x42, 0x05, - 0x05, 0x22, 0x84, 0x21, 0x86, 0x11, 0xE6, 0x05, 0x05, 0x83, 0x20, 0x81, - 0x21, 0x86, 0x10, 0x1B, 0x01, 0x00, 0x11, 0xF0, 0x03, 0x05, 0x12, 0x5E, - 0x00, 0x07, 0x8A, 0x10, 0x3E, 0x01, 0x00, 0x22, 0x84, 0x21, 0x86, 0x10, - 0x50, 0x01, 0x00, 0x85, 0x11, 0xF0, 0x03, 0x05, 0x85, 0x10, 0x7A, 0x01, - 0x00, 0x12, 0x7E, 0x00, 0x07, 0x8A, 0x22, 0x85, 0x11, 0xB8, 0x03, 0x05, - 0x85, 0x11, 0x50, 0x03, 0x05, 0x85, 0x11, 0xF3, 0x02, 0x05, 0x85, 0x11, - 0x6C, 0x04, 0x05, 0x12, 0x77, 0x00, 0x07, 0x84, 0x21, 0x86, 0x11, 0xF6, - 0x04, 0x05, 0x22, 0x84, 0x21, 0x86, 0x11, 0x42, 0x05, 0x05, 0x84, 0x21, - 0x86, 0x11, 0xE6, 0x05, 0x05, 0x12, 0x7E, 0x00, 0x07, 0x83, 0x20, 0x81, - 0x21, 0x86, 0x11, 0xE6, 0x05, 0x05, 0x22, 0x84, 0x21, 0x86, 0x11, 0xE6, - 0x05, 0x05, 0x84, 0x21, 0x86, 0x10, 0xA8, 0x01, 0x00, 0x11, 0x6C, 0x04, - 0x05, 0x12, 0x5E, 0x00, 0x07, 0x89, 0x21, 0x81, 0x10, 0x7A, 0x01, 0x00, - 0x22, 0x8A, 0x10, 0x50, 0x01, 0x00, 0x11, 0x6C, 0x04, 0x05, 0x8A, 0x10, - 0x7A, 0x01, 0x00, 0x11, 0xF6, 0x04, 0x05, 0x12, 0x7E, 0x00, 0x07, 0x89, - 0x21, 0x81, 0x22, 0x83, 0x20, 0x91, 0x10, 0x50, 0x01, 0x00, 0x11, 0xB8, - 0x03, 0x05, 0x12, 0x77, 0x00, 0x07, 0x89, 0x21, 0x81, 0x10, 0x3E, 0x01, - 0x00, 0x22, 0x8A, 0x10, 0x1B, 0x01, 0x00, 0x11, 0xB8, 0x03, 0x05, 0x8A, - 0x10, 0x3E, 0x01, 0x00, 0x11, 0xF0, 0x03, 0x05, 0x12, 0x6A, 0x00, 0x07, - 0x89, 0x21, 0x81, 0x22, 0x83, 0x20, 0x91, 0x10, 0x1B, 0x01, 0x00, 0x11, - 0xF3, 0x02, 0x05, 0x12, 0x5E, 0x00, 0x07, 0x89, 0x21, 0x81, 0x10, 0xFC, - 0x00, 0x00, 0x22, 0x8A, 0x10, 0xEE, 0x00, 0x00, 0x11, 0xF3, 0x02, 0x05, - 0x89, 0x21, 0x81, 0x10, 0xFC, 0x00, 0x00, 0x11, 0xF0, 0x03, 0x05, 0x12, - 0x7E, 0x00, 0x07, 0x83, 0x20, 0x87, 0x11, 0xB8, 0x03, 0x05, 0x22, 0x8A, - 0x10, 0x3E, 0x01, 0x00, 0x11, 0xF0, 0x03, 0x05, 0x89, 0x21, 0x81, 0x10, - 0xBD, 0x00, 0x00, 0x11, 0x6C, 0x04, 0x05, 0x12, 0x77, 0x00, 0x07, 0x8A, - 0x11, 0xF0, 0x03, 0x05, 0x22, 0x8A, 0x11, 0x6C, 0x04, 0x05, 0x8A, 0x11, - 0xF6, 0x04, 0x05, 0x12, 0x6A, 0x00, 0x07, 0x8A, 0x11, 0x6C, 0x04, 0x05, - 0x22, 0x83, 0x20, 0x87, 0x11, 0xF6, 0x04, 0x05, 0x8A, 0x10, 0xEE, 0x00, - 0x00, 0x11, 0x42, 0x05, 0x05, 0x12, 0x5E, 0x00, 0x07, 0x8A, 0x10, 0xFC, - 0x00, 0x00, 0x11, 0xF6, 0x04, 0x05, 0x22, 0x8A, 0x10, 0x1B, 0x01, 0x00, - 0x11, 0x42, 0x05, 0x05, 0x89, 0x21, 0x81, 0x10, 0xFC, 0x00, 0x00, 0x11, - 0xF6, 0x04, 0x05, 0x12, 0x7E, 0x00, 0x07, 0x83, 0x20, 0x86, 0x21, 0x81, - 0x22, 0x8A, 0x11, 0xF6, 0x04, 0x05, 0x8A, 0x10, 0xEE, 0x00, 0x00, 0x11, - 0xE6, 0x05, 0x05, 0x12, 0x77, 0x00, 0x07, 0x8A, 0x10, 0xFC, 0x00, 0x00, - 0x22, 0x8A, 0x10, 0x1B, 0x01, 0x00, 0x8A, 0x10, 0x3E, 0x01, 0x00, 0x12, - 0x6A, 0x00, 0x07, 0x83, 0x20, 0x86, 0x21, 0x81, 0x22, 0x94, 0x10, 0x1B, - 0x01, 0x00, 0x11, 0xE6, 0x05, 0x05, 0x12, 0x5E, 0x00, 0x07, 0x83, 0x20, - 0x87, 0x22, 0x89, 0x21, 0x81, 0x10, 0x1B, 0x01, 0x00, 0x8A, 0x10, 0x7A, - 0x01, 0x00, 0x11, 0x6C, 0x04, 0x05, 0x12, 0x8D, 0x00, 0x07, 0x89, 0x21, - 0x81, 0x22, 0x8A, 0x11, 0xF0, 0x03, 0x05, 0x8A, 0x11, 0xB8, 0x03, 0x05, - 0x12, 0x77, 0x00, 0x07, 0x8A, 0x11, 0xF0, 0x03, 0x05, 0x22, 0x8A, 0x11, - 0x6C, 0x04, 0x05, 0x8A, 0x11, 0xF3, 0x02, 0x05, 0x12, 0x6A, 0x00, 0x07, - 0x83, 0x20, 0x86, 0x21, 0x81, 0x22, 0x8A, 0x10, 0x7A, 0x01, 0x00, 0x11, - 0x6C, 0x04, 0x05, 0x81, 0x20, 0x84, 0x10, 0x7A, 0x01, 0x00, 0x81, 0x20, - 0x84, 0x10, 0x7A, 0x01, 0x00, 0x11, 0xAF, 0x04, 0x05, 0x12, 0x5E, 0x00, - 0x07, 0x83, 0x20, 0x87, 0x10, 0x7A, 0x01, 0x00, 0x22, 0x83, 0x20, 0x86, - 0x21, 0x81, 0x10, 0x7A, 0x01, 0x00, 0x83, 0x20, 0x87, 0x10, 0x7A, 0x01, - 0x00, 0x11, 0xF6, 0x04, 0x05, 0x12, 0x7E, 0x00, 0x07, 0x85, 0x11, 0xE6, - 0x05, 0x05, 0x85, 0x11, 0xF6, 0x04, 0x05, 0x85, 0x11, 0xE6, 0x05, 0x05, - 0x85, 0x11, 0xF6, 0x04, 0x05, 0x84, 0x12, 0x5E, 0x00, 0x07, 0x81, 0x11, - 0xE6, 0x05, 0x05, 0x85, 0x11, 0xF6, 0x04, 0x05, 0x85, 0x11, 0xE6, 0x05, - 0x05, 0x85, 0x11, 0xF6, 0x04, 0x05, 0x85, 0x11, 0xE6, 0x05, 0x05, 0x83, - 0x12, 0x7E, 0x00, 0x07, 0x82, 0x11, 0xF6, 0x04, 0x05, 0x85, 0x11, 0xE6, - 0x05, 0x05, 0x85, 0x11, 0xF6, 0x04, 0x05, 0x85, 0x11, 0xE6, 0x05, 0x05, - 0x85, 0x11, 0xF6, 0x04, 0x05, 0x82, 0x12, 0x5E, 0x00, 0x07, 0x83, 0x11, - 0xE6, 0x05, 0x05, 0x85, 0x11, 0xF6, 0x04, 0x05, 0x85, 0x11, 0xE6, 0x05, - 0x05, 0x85, 0x11, 0xF6, 0x04, 0x05, 0x81, 0x22, 0x85, 0x12, 0x7E, 0x00, - 0x07, 0x88, 0x21, 0x89, 0x20, 0x88, 0x22, 0x86, 0x00, -}; - -static const uint8_t gStSong8[2338] = { - 0x4A, 0x59, 0x4D, 0x31, 0x14, 0x00, 0x00, 0x00, 0x81, 0x10, 0x1F, 0x00, - 0x07, 0x11, 0x7E, 0x00, 0x01, 0x13, 0x1C, 0x00, 0x0A, 0x82, 0x21, 0x84, - 0x13, 0x1D, 0x00, 0x0A, 0x84, 0x11, 0xB2, 0x00, 0x01, 0x82, 0x21, 0x13, - 0x1D, 0x00, 0x0A, 0x86, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x11, 0xD4, 0x00, - 0x01, 0x82, 0x21, 0x82, 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x11, 0xFC, 0x00, - 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1E, 0x00, 0x0A, - 0x84, 0x11, 0x50, 0x01, 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, - 0x10, 0x21, 0x00, 0x07, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0xC1, 0x01, - 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0xF8, 0x01, - 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1E, 0x00, 0x0A, - 0x84, 0x11, 0x57, 0x02, 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, - 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0xF3, 0x02, 0x01, 0x82, 0x21, 0x82, - 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0x82, 0x03, 0x01, 0x13, 0x1E, 0x00, - 0x0A, 0x82, 0x21, 0x84, 0x10, 0x23, 0x00, 0x07, 0x13, 0x1E, 0x00, 0x0A, - 0x84, 0x11, 0xF0, 0x03, 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, - 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x11, 0x42, 0x05, 0x01, 0x82, 0x21, 0x82, - 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x11, 0x31, 0x06, 0x01, 0x13, 0x1D, 0x00, - 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1D, 0x00, 0x0A, 0x84, 0x11, 0xF6, 0x04, - 0x01, 0x82, 0x21, 0x13, 0x1C, 0x00, 0x0A, 0x86, 0x13, 0x1D, 0x00, 0x0A, - 0x82, 0x11, 0xF0, 0x03, 0x01, 0x82, 0x21, 0x82, 0x10, 0x21, 0x00, 0x07, - 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x11, 0x20, 0x03, 0x01, 0x13, 0x1D, 0x00, - 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1D, 0x00, 0x0A, 0x84, 0x11, 0xA1, 0x02, - 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, - 0x82, 0x11, 0x36, 0x02, 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, - 0x86, 0x11, 0xF8, 0x01, 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, - 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0xC1, 0x01, 0x01, 0x82, 0x10, 0x1F, - 0x00, 0x07, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, - 0x82, 0x11, 0x7A, 0x01, 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, - 0x86, 0x11, 0x3E, 0x01, 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, - 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0xFC, 0x00, 0x01, 0x82, 0x21, 0x13, - 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0xD4, 0x00, - 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x10, 0x21, 0x00, - 0x07, 0x11, 0x9F, 0x00, 0x01, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x21, 0x84, - 0x13, 0x1D, 0x00, 0x0A, 0x84, 0x11, 0x7E, 0x00, 0x01, 0x82, 0x21, 0x13, - 0x1D, 0x00, 0x0A, 0x86, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x11, 0xB2, 0x00, - 0x01, 0x82, 0x21, 0x82, 0x13, 0x1C, 0x00, 0x0A, 0x86, 0x11, 0xD4, 0x00, - 0x01, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1D, 0x00, 0x0A, - 0x84, 0x11, 0xFC, 0x00, 0x01, 0x82, 0x21, 0x13, 0x1D, 0x00, 0x0A, 0x86, - 0x10, 0x23, 0x00, 0x07, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x11, 0x50, 0x01, - 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0xC1, 0x01, - 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1E, 0x00, 0x0A, - 0x84, 0x11, 0xF8, 0x01, 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, - 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0x57, 0x02, 0x01, 0x82, 0x21, 0x82, - 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0xF3, 0x02, 0x01, 0x13, 0x1E, 0x00, - 0x0A, 0x82, 0x21, 0x84, 0x10, 0x21, 0x00, 0x07, 0x13, 0x1E, 0x00, 0x0A, - 0x84, 0x11, 0x82, 0x03, 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, - 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0xF0, 0x03, 0x01, 0x82, 0x21, 0x82, - 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0x42, 0x05, 0x01, 0x13, 0x1E, 0x00, - 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0x31, 0x06, - 0x01, 0x82, 0x21, 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x13, 0x1D, 0x00, 0x0A, - 0x82, 0x11, 0xF6, 0x04, 0x01, 0x82, 0x21, 0x82, 0x10, 0x1F, 0x00, 0x07, - 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x11, 0xF0, 0x03, 0x01, 0x13, 0x1D, 0x00, - 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1C, 0x00, 0x0A, 0x84, 0x11, 0x20, 0x03, - 0x01, 0x82, 0x21, 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x13, 0x1D, 0x00, 0x0A, - 0x82, 0x11, 0xA1, 0x02, 0x01, 0x82, 0x21, 0x82, 0x13, 0x1D, 0x00, 0x0A, - 0x86, 0x11, 0x36, 0x02, 0x01, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x21, 0x84, - 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0xF8, 0x01, 0x01, 0x82, 0x10, 0x21, - 0x00, 0x07, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, - 0x82, 0x11, 0xC1, 0x01, 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, - 0x86, 0x11, 0x7A, 0x01, 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, - 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0x3E, 0x01, 0x01, 0x82, 0x21, 0x13, - 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0xFC, 0x00, - 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x10, 0x23, 0x00, - 0x07, 0x11, 0xD4, 0x00, 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, - 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0x9F, 0x00, 0x01, 0x82, 0x21, 0x13, - 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0x7E, 0x00, - 0x01, 0x82, 0x21, 0x82, 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x11, 0xB2, 0x00, - 0x01, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1D, 0x00, 0x0A, - 0x84, 0x11, 0xD4, 0x00, 0x01, 0x82, 0x21, 0x13, 0x1D, 0x00, 0x0A, 0x86, - 0x10, 0x21, 0x00, 0x07, 0x13, 0x1C, 0x00, 0x0A, 0x82, 0x11, 0xFC, 0x00, - 0x01, 0x82, 0x21, 0x82, 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x11, 0x50, 0x01, - 0x01, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1D, 0x00, 0x0A, - 0x84, 0x11, 0xC1, 0x01, 0x01, 0x82, 0x21, 0x13, 0x1D, 0x00, 0x0A, 0x86, - 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0xF8, 0x01, 0x01, 0x82, 0x21, 0x82, - 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0x57, 0x02, 0x01, 0x13, 0x1E, 0x00, - 0x0A, 0x82, 0x21, 0x84, 0x10, 0x1F, 0x00, 0x07, 0x13, 0x1E, 0x00, 0x0A, - 0x84, 0x11, 0xF3, 0x02, 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, - 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0x82, 0x03, 0x01, 0x82, 0x21, 0x82, - 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0xF0, 0x03, 0x01, 0x13, 0x1E, 0x00, - 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0x42, 0x05, - 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, - 0x82, 0x11, 0x31, 0x06, 0x01, 0x82, 0x21, 0x82, 0x10, 0x21, 0x00, 0x07, - 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0xF6, 0x04, 0x01, 0x13, 0x1E, 0x00, - 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1D, 0x00, 0x0A, 0x84, 0x11, 0xF0, 0x03, - 0x01, 0x82, 0x21, 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x13, 0x1D, 0x00, 0x0A, - 0x82, 0x11, 0x20, 0x03, 0x01, 0x82, 0x21, 0x82, 0x13, 0x1D, 0x00, 0x0A, - 0x86, 0x11, 0xA1, 0x02, 0x01, 0x13, 0x1C, 0x00, 0x0A, 0x82, 0x21, 0x84, - 0x13, 0x1D, 0x00, 0x0A, 0x84, 0x11, 0x36, 0x02, 0x01, 0x82, 0x10, 0x23, - 0x00, 0x07, 0x21, 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x13, 0x1D, 0x00, 0x0A, - 0x82, 0x11, 0xF8, 0x01, 0x01, 0x82, 0x21, 0x82, 0x13, 0x1D, 0x00, 0x0A, - 0x86, 0x11, 0xC1, 0x01, 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, - 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0x7A, 0x01, 0x01, 0x82, 0x21, 0x13, - 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0x3E, 0x01, - 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x10, 0x21, 0x00, - 0x07, 0x11, 0xFC, 0x00, 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, - 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0xD4, 0x00, 0x01, 0x82, 0x21, 0x13, - 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0x9F, 0x00, - 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0x7E, 0x00, - 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1E, 0x00, 0x0A, - 0x84, 0x11, 0xB2, 0x00, 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, - 0x10, 0x1F, 0x00, 0x07, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x11, 0xD4, 0x00, - 0x01, 0x82, 0x21, 0x82, 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x11, 0xFC, 0x00, - 0x01, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1D, 0x00, 0x0A, - 0x84, 0x11, 0x50, 0x01, 0x01, 0x82, 0x21, 0x13, 0x1C, 0x00, 0x0A, 0x86, - 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x11, 0xC1, 0x01, 0x01, 0x82, 0x21, 0x82, - 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x11, 0xF8, 0x01, 0x01, 0x13, 0x1D, 0x00, - 0x0A, 0x82, 0x21, 0x84, 0x10, 0x21, 0x00, 0x07, 0x13, 0x1D, 0x00, 0x0A, - 0x84, 0x11, 0x57, 0x02, 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, - 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0xF3, 0x02, 0x01, 0x82, 0x21, 0x82, - 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0x82, 0x03, 0x01, 0x13, 0x1E, 0x00, - 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0xF0, 0x03, - 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, - 0x82, 0x11, 0x42, 0x05, 0x01, 0x82, 0x21, 0x82, 0x10, 0x23, 0x00, 0x07, - 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0x31, 0x06, 0x01, 0x13, 0x1E, 0x00, - 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0xF6, 0x04, - 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, - 0x82, 0x11, 0xF0, 0x03, 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, - 0x86, 0x11, 0x20, 0x03, 0x01, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x21, 0x84, - 0x13, 0x1D, 0x00, 0x0A, 0x84, 0x11, 0xA1, 0x02, 0x01, 0x82, 0x10, 0x21, - 0x00, 0x07, 0x21, 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x13, 0x1D, 0x00, 0x0A, - 0x82, 0x11, 0x36, 0x02, 0x01, 0x82, 0x21, 0x82, 0x13, 0x1C, 0x00, 0x0A, - 0x86, 0x11, 0xF8, 0x01, 0x01, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x21, 0x84, - 0x13, 0x1D, 0x00, 0x0A, 0x84, 0x11, 0xC1, 0x01, 0x01, 0x82, 0x21, 0x13, - 0x1D, 0x00, 0x0A, 0x86, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x11, 0x7A, 0x01, - 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x10, 0x1F, 0x00, - 0x07, 0x11, 0x3E, 0x01, 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, - 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0xFC, 0x00, 0x01, 0x82, 0x21, 0x13, - 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0xD4, 0x00, - 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0x9F, 0x00, - 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1E, 0x00, 0x0A, - 0x84, 0x11, 0x7E, 0x00, 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, - 0x10, 0x21, 0x00, 0x07, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0xB2, 0x00, - 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0xD4, 0x00, - 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1E, 0x00, 0x0A, - 0x84, 0x11, 0xFC, 0x00, 0x01, 0x82, 0x21, 0x13, 0x1D, 0x00, 0x0A, 0x86, - 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x11, 0x50, 0x01, 0x01, 0x82, 0x21, 0x82, - 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x11, 0xC1, 0x01, 0x01, 0x13, 0x1D, 0x00, - 0x0A, 0x82, 0x21, 0x84, 0x10, 0x23, 0x00, 0x07, 0x13, 0x1C, 0x00, 0x0A, - 0x84, 0x11, 0xF8, 0x01, 0x01, 0x82, 0x21, 0x13, 0x1D, 0x00, 0x0A, 0x86, - 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x11, 0x57, 0x02, 0x01, 0x82, 0x21, 0x82, - 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x11, 0xF3, 0x02, 0x01, 0x13, 0x1D, 0x00, - 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0x82, 0x03, - 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, - 0x82, 0x11, 0xF0, 0x03, 0x01, 0x82, 0x21, 0x82, 0x10, 0x21, 0x00, 0x07, - 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0x42, 0x05, 0x01, 0x13, 0x1E, 0x00, - 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0x31, 0x06, - 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, - 0x82, 0x11, 0xF6, 0x04, 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, - 0x86, 0x11, 0xF0, 0x03, 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, - 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0x20, 0x03, 0x01, 0x82, 0x10, 0x1F, - 0x00, 0x07, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, - 0x82, 0x11, 0xA1, 0x02, 0x01, 0x82, 0x21, 0x82, 0x13, 0x1D, 0x00, 0x0A, - 0x86, 0x11, 0x36, 0x02, 0x01, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x21, 0x84, - 0x13, 0x1D, 0x00, 0x0A, 0x84, 0x11, 0xF8, 0x01, 0x01, 0x82, 0x21, 0x13, - 0x1D, 0x00, 0x0A, 0x86, 0x13, 0x1C, 0x00, 0x0A, 0x82, 0x11, 0xC1, 0x01, - 0x01, 0x82, 0x21, 0x82, 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x10, 0x21, 0x00, - 0x07, 0x11, 0x7A, 0x01, 0x01, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x21, 0x84, - 0x13, 0x1D, 0x00, 0x0A, 0x84, 0x11, 0x3E, 0x01, 0x01, 0x82, 0x21, 0x13, - 0x1D, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0xFC, 0x00, - 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0xD4, 0x00, - 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1E, 0x00, 0x0A, - 0x84, 0x11, 0x9F, 0x00, 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, - 0x10, 0x23, 0x00, 0x07, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0x7E, 0x00, - 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0xB2, 0x00, - 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1E, 0x00, 0x0A, - 0x84, 0x11, 0xD4, 0x00, 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, - 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0xFC, 0x00, 0x01, 0x82, 0x21, 0x82, - 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0x50, 0x01, 0x01, 0x13, 0x1E, 0x00, - 0x0A, 0x82, 0x21, 0x84, 0x10, 0x21, 0x00, 0x07, 0x13, 0x1D, 0x00, 0x0A, - 0x84, 0x11, 0xC1, 0x01, 0x01, 0x82, 0x21, 0x13, 0x1D, 0x00, 0x0A, 0x86, - 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x11, 0xF8, 0x01, 0x01, 0x82, 0x21, 0x82, - 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x11, 0x57, 0x02, 0x01, 0x13, 0x1C, 0x00, - 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1D, 0x00, 0x0A, 0x84, 0x11, 0xF3, 0x02, - 0x01, 0x82, 0x21, 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x13, 0x1D, 0x00, 0x0A, - 0x82, 0x11, 0x82, 0x03, 0x01, 0x82, 0x21, 0x82, 0x10, 0x1F, 0x00, 0x07, - 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x11, 0xF0, 0x03, 0x01, 0x13, 0x1E, 0x00, - 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0x42, 0x05, - 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, - 0x82, 0x11, 0x31, 0x06, 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, - 0x86, 0x11, 0xF6, 0x04, 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, - 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0xF0, 0x03, 0x01, 0x82, 0x10, 0x21, - 0x00, 0x07, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, - 0x82, 0x11, 0x20, 0x03, 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, - 0x86, 0x11, 0xA1, 0x02, 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, - 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0x36, 0x02, 0x01, 0x82, 0x21, 0x13, - 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x11, 0xF8, 0x01, - 0x01, 0x82, 0x21, 0x82, 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x10, 0x23, 0x00, - 0x07, 0x11, 0xC1, 0x01, 0x01, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x21, 0x84, - 0x13, 0x1D, 0x00, 0x0A, 0x84, 0x11, 0x7A, 0x01, 0x01, 0x82, 0x21, 0x13, - 0x1C, 0x00, 0x0A, 0x86, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x11, 0x3E, 0x01, - 0x01, 0x82, 0x21, 0x82, 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x11, 0xFC, 0x00, - 0x01, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1D, 0x00, 0x0A, - 0x84, 0x11, 0xD4, 0x00, 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, - 0x10, 0x21, 0x00, 0x07, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0x9F, 0x00, - 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, 0x85, 0x00, -}; - -typedef struct { - const uint8_t *data; - uint16_t length; -} StSongT; - -#define ST_SONG_COUNT 9u -#define ST_SONG_BONUS 4u -#define ST_SONG_GAME_OVER 5u -#define ST_SONG_NAME_ENTRY 6u -#define ST_SONG_SCORES 7u -#define ST_SONG_INTRO 8u - -static const StSongT kStSongs[ST_SONG_COUNT] = { - { gStSong0, (uint16_t)sizeof(gStSong0) }, - { gStSong1, (uint16_t)sizeof(gStSong1) }, - { gStSong2, (uint16_t)sizeof(gStSong2) }, - { gStSong3, (uint16_t)sizeof(gStSong3) }, - { gStSong4, (uint16_t)sizeof(gStSong4) }, - { gStSong5, (uint16_t)sizeof(gStSong5) }, - { gStSong6, (uint16_t)sizeof(gStSong6) }, - { gStSong7, (uint16_t)sizeof(gStSong7) }, - { gStSong8, (uint16_t)sizeof(gStSong8) }, -}; - -#endif // ST_SONG_DATA_H diff --git a/examples/spacetaxi/stSpeechData.c b/examples/spacetaxi/stSpeechData.c deleted file mode 100644 index 16b0c91..0000000 --- a/examples/spacetaxi/stSpeechData.c +++ /dev/null @@ -1,490 +0,0 @@ -// Generated by assets/extractSpeech.py. Do not hand-edit. - -#include "stSpeechData.h" - -const StSpeechEntryT kStSpeechIndex[ST_SPEECH_COUNT] = { - { 0x50u, 0u, 562u }, // 'P' - { 0x31u, 562u, 420u }, // '1' - { 0x3Fu, 982u, 266u }, // '?' - { 0x32u, 1248u, 480u }, // '2' - { 0x33u, 1728u, 463u }, // '3' - { 0x34u, 2191u, 315u }, // '4' - { 0x35u, 2506u, 550u }, // '5' - { 0x36u, 3056u, 527u }, // '6' - { 0x37u, 3583u, 758u }, // '7' - { 0x38u, 4341u, 385u }, // '8' - { 0x39u, 4726u, 491u }, // '9' - { 0x48u, 5217u, 520u }, // 'H' - { 0x54u, 5737u, 632u }, // 'T' - { 0x21u, 6369u, 708u }, // '!' - { 0x55u, 7077u, 368u }, // 'U' -}; - -const uint8_t kStSpeechRle[ST_SPEECH_RLE_BYTES] = { - 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, - 0xF2, 0x42, 0xCD, 0x27, 0x10, 0x09, 0xAB, 0x31, 0xB6, 0x12, 0x37, 0x09, 0x22, 0x04, 0x1C, 0x0D, - 0x16, 0x0E, 0x2F, 0x0B, 0x19, 0x12, 0x0E, 0x12, 0x19, 0x06, 0x1A, 0x0F, 0x09, 0x19, 0x0D, 0x16, - 0x13, 0x06, 0x05, 0x03, 0x38, 0x08, 0x0F, 0x17, 0x0E, 0x12, 0x0F, 0x12, 0x16, 0x09, 0x15, 0x15, - 0x06, 0x13, 0x08, 0x15, 0x0C, 0x13, 0x18, 0x10, 0x15, 0x1C, 0x0C, 0x0C, 0x09, 0x02, 0x0C, 0x14, - 0x15, 0x14, 0x1D, 0x11, 0x11, 0x0B, 0x0A, 0x02, 0x0A, 0x15, 0x15, 0x0B, 0x15, 0x05, 0x06, 0x07, - 0x07, 0x07, 0x15, 0x08, 0x04, 0x08, 0x0B, 0x07, 0x03, 0x08, 0x0B, 0x04, 0x05, 0x0D, 0x13, 0x08, - 0x04, 0x06, 0x24, 0x08, 0x04, 0x03, 0x02, 0x03, 0x09, 0x07, 0x05, 0x07, 0x0B, 0x05, 0x05, 0x0A, - 0x14, 0x08, 0x06, 0x06, 0x18, 0x06, 0x06, 0x09, 0x17, 0x07, 0x05, 0x08, 0x0B, 0x04, 0x05, 0x0A, - 0x14, 0x09, 0x04, 0x07, 0x09, 0x06, 0x06, 0x09, 0x07, 0x08, 0x05, 0x03, 0x0E, 0x07, 0x04, 0x09, - 0x0B, 0x04, 0x06, 0x09, 0x15, 0x08, 0x04, 0x06, 0x17, 0x09, 0x07, 0x0C, 0x15, 0x0A, 0x01, 0x09, - 0x16, 0x09, 0x15, 0x09, 0x06, 0x03, 0x0C, 0x05, 0x05, 0x0A, 0x09, 0x08, 0x07, 0x04, 0x0B, 0x07, - 0x05, 0x08, 0x15, 0x0A, 0x14, 0x08, 0x05, 0x05, 0x0B, 0x04, 0x07, 0x0F, 0x04, 0x0A, 0x05, 0x08, - 0x09, 0x07, 0x03, 0x09, 0x15, 0x0A, 0x14, 0x09, 0x04, 0x05, 0x18, 0x0A, 0x0A, 0x0A, 0x06, 0x03, - 0x0C, 0x14, 0x15, 0x0A, 0x14, 0x14, 0x0A, 0x06, 0x05, 0x0E, 0x08, 0x09, 0x16, 0x08, 0x03, 0x08, - 0x16, 0x09, 0x15, 0x13, 0x16, 0x0F, 0x08, 0x09, 0x05, 0x06, 0x0A, 0x08, 0x04, 0x07, 0x16, 0x0A, - 0x06, 0x02, 0x0B, 0x13, 0x17, 0x09, 0x0F, 0x09, 0x07, 0x02, 0x0C, 0x08, 0x03, 0x08, 0x15, 0x0B, - 0x13, 0x14, 0x0A, 0x14, 0x13, 0x0A, 0x15, 0x13, 0x12, 0x0F, 0x14, 0x0F, 0x10, 0x11, 0x10, 0x10, - 0x15, 0x0A, 0x06, 0x02, 0x0D, 0x13, 0x14, 0x0B, 0x12, 0x11, 0x0E, 0x16, 0x15, 0x0A, 0x17, 0x07, - 0x04, 0x05, 0x19, 0x06, 0x14, 0x0B, 0x14, 0x18, 0x07, 0x04, 0x0C, 0x07, 0x05, 0x05, 0x10, 0x02, - 0x05, 0x07, 0x08, 0x02, 0x0C, 0x07, 0x06, 0x07, 0x08, 0x38, 0x16, 0x09, 0x05, 0x05, 0x0C, 0x06, - 0x05, 0x07, 0x16, 0x08, 0x07, 0x03, 0x0A, 0x13, 0x0B, 0x1C, 0x08, 0x0B, 0x04, 0x07, 0x06, 0x04, - 0x0D, 0x05, 0x06, 0x06, 0x0B, 0x02, 0x08, 0x07, 0x08, 0x05, 0x08, 0x08, 0x1F, 0x0A, 0x05, 0x08, - 0x06, 0x04, 0x0C, 0x07, 0x05, 0x05, 0x18, 0x0A, 0x07, 0x02, 0x09, 0x08, 0x06, 0x07, 0x05, 0x09, - 0x17, 0x10, 0x09, 0x07, 0x06, 0x06, 0x0C, 0x05, 0x05, 0x07, 0x17, 0x09, 0x07, 0x05, 0x0C, 0x13, - 0x06, 0x06, 0x08, 0x06, 0x06, 0x0B, 0x05, 0x02, 0x0D, 0x07, 0x06, 0x03, 0x0F, 0x05, 0x05, 0x07, - 0x17, 0x17, 0x07, 0x17, 0x24, 0x11, 0x11, 0x09, 0x17, 0x08, 0x03, 0x08, 0x16, 0x0B, 0x14, 0x13, - 0x18, 0x06, 0x16, 0x0C, 0x0E, 0x0A, 0x04, 0x03, 0x10, 0x12, 0x16, 0x12, 0x0D, 0x14, 0x15, 0x0B, - 0x16, 0x0C, 0x10, 0x0F, 0x15, 0x0A, 0x02, 0x05, 0x18, 0x0C, 0x13, 0x09, 0x05, 0x07, 0x30, 0x09, - 0x11, 0x1B, 0x1A, 0x05, 0x05, 0x06, 0x1A, 0x15, 0x14, 0x0C, 0x20, 0x14, 0x18, 0x16, 0x03, 0x09, - 0x05, 0x03, 0x0F, 0x05, 0x05, 0x09, 0x15, 0x16, 0x15, 0x13, 0x17, 0x09, 0x1E, 0x14, 0x0D, 0x0B, - 0x19, 0x05, 0x03, 0x0B, 0x14, 0x16, 0x15, 0x14, 0x18, 0x08, 0x26, 0x12, 0x11, 0x0B, 0x16, 0x14, - 0x15, 0x15, 0x16, 0x16, 0x0A, 0x23, 0x42, 0x0A, 0x18, 0x05, 0x05, 0x09, 0x24, 0x08, 0x18, 0x16, - 0x13, 0x17, 0x15, 0x18, 0x1A, 0x0A, 0x23, 0x09, 0x25, 0x15, 0x17, 0x18, 0x11, 0x14, 0x3C, 0x10, - 0x57, 0x0F, 0x16, 0x15, 0x18, 0x13, 0x48, 0x1A, 0x16, 0x13, 0x4E, 0x1C, 0x6B, 0x18, 0xFE, 0xFE, - 0xFE, 0xFF, 0xFE, 0x63, 0x1D, 0xC8, 0x29, 0xBE, 0x1B, 0x59, 0x14, 0x48, 0x19, 0x1A, 0x0E, 0x1E, - 0x1F, 0x20, 0x11, 0x0E, 0x23, 0x19, 0x10, 0x19, 0x22, 0x18, 0x12, 0x15, 0x20, 0x17, 0x13, 0x18, - 0x1E, 0x17, 0x13, 0x17, 0x1D, 0x16, 0x14, 0x1A, 0x1A, 0x16, 0x12, 0x17, 0x1E, 0x16, 0x13, 0x17, - 0x1D, 0x12, 0x15, 0x17, 0x1F, 0x16, 0x12, 0x17, 0x1C, 0x14, 0x14, 0x18, 0x1E, 0x15, 0x0F, 0x1A, - 0x19, 0x13, 0x10, 0x1E, 0x1D, 0x12, 0x11, 0x1C, 0x17, 0x11, 0x1A, 0x1A, 0x1A, 0x11, 0x11, 0x1D, - 0x14, 0x10, 0x11, 0x25, 0x19, 0x10, 0x12, 0x1C, 0x13, 0x10, 0x10, 0x0B, 0x09, 0x13, 0x18, 0x12, - 0x11, 0x17, 0x16, 0x10, 0x0E, 0x0D, 0x0F, 0x0E, 0x19, 0x11, 0x10, 0x17, 0x16, 0x10, 0x0D, 0x0E, - 0x10, 0x0D, 0x18, 0x10, 0x11, 0x18, 0x12, 0x14, 0x0D, 0x0F, 0x0C, 0x0F, 0x19, 0x10, 0x10, 0x19, - 0x15, 0x11, 0x0D, 0x0C, 0x12, 0x0E, 0x17, 0x0F, 0x11, 0x1A, 0x11, 0x0C, 0x14, 0x0C, 0x17, 0x0B, - 0x13, 0x01, 0x04, 0x0D, 0x10, 0x1B, 0x10, 0x0C, 0x0C, 0x0E, 0x0A, 0x0B, 0x0B, 0x0A, 0x14, 0x13, - 0x0C, 0x1F, 0x0D, 0x0F, 0x0B, 0x0E, 0x1A, 0x0D, 0x20, 0x0C, 0x0B, 0x24, 0x09, 0x0E, 0x0C, 0x0C, - 0x0B, 0x0A, 0x05, 0x0E, 0x22, 0x06, 0x01, 0x04, 0x0A, 0x0E, 0x03, 0x10, 0x0B, 0x0F, 0x0D, 0x0C, - 0x0B, 0x06, 0x0A, 0x0B, 0x16, 0x04, 0x0E, 0x0C, 0x09, 0x0F, 0x02, 0x10, 0x0A, 0x0C, 0x0C, 0x0C, - 0x1D, 0x0B, 0x09, 0x08, 0x09, 0x04, 0x0D, 0x0C, 0x0A, 0x0E, 0x02, 0x10, 0x0C, 0x0A, 0x0B, 0x02, - 0x07, 0x0D, 0x0E, 0x18, 0x13, 0x07, 0x0D, 0x0C, 0x08, 0x09, 0x0A, 0x0D, 0x0D, 0x07, 0x08, 0x09, - 0x0B, 0x09, 0x0B, 0x0B, 0x01, 0x0F, 0x0B, 0x10, 0x0F, 0x07, 0x0D, 0x07, 0x0A, 0x0D, 0x09, 0x0E, - 0x05, 0x05, 0x0F, 0x0D, 0x10, 0x26, 0x18, 0x03, 0x02, 0x05, 0x0A, 0x0B, 0x05, 0x0D, 0x08, 0x0A, - 0x09, 0x0A, 0x1B, 0x17, 0x08, 0x0D, 0x07, 0x0B, 0x04, 0x0A, 0x08, 0x03, 0x02, 0x05, 0x08, 0x0B, - 0x07, 0x0C, 0x04, 0x09, 0x0B, 0x0D, 0x06, 0x05, 0x0D, 0x19, 0x06, 0x1A, 0x05, 0x0D, 0x04, 0x07, - 0x08, 0x04, 0x02, 0x04, 0x05, 0x09, 0x0B, 0x0F, 0x01, 0x09, 0x0A, 0x09, 0x06, 0x08, 0x0A, 0x0A, - 0x07, 0x0C, 0x04, 0x1A, 0x06, 0x0E, 0x0B, 0x05, 0x0C, 0x0C, 0x04, 0x09, 0x0A, 0x1A, 0x06, 0x0A, - 0x07, 0x08, 0x09, 0x08, 0x0A, 0x2B, 0x04, 0x0B, 0x27, 0x03, 0x03, 0x05, 0x02, 0x0B, 0x0B, 0x05, - 0x02, 0x11, 0x05, 0x02, 0x0D, 0x0A, 0x14, 0x0A, 0x07, 0x1A, 0x09, 0x06, 0x03, 0x12, 0x1F, 0x04, - 0x02, 0x04, 0x03, 0x0B, 0x0A, 0x04, 0x08, 0x0D, 0x04, 0x05, 0x19, 0x0B, 0x1B, 0x18, 0x0F, 0x1E, - 0x12, 0x04, 0x0E, 0x04, 0x04, 0x19, 0x06, 0x18, 0x0B, 0x58, 0x14, 0x14, 0x0B, 0x0D, 0x19, 0x1B, - 0x04, 0x56, 0x16, 0x40, 0x29, 0x04, 0x15, 0x3A, 0x07, 0x37, 0x14, 0x55, 0x19, 0xFE, 0x80, 0x5B, - 0xFE, 0x3C, 0x80, 0xFE, 0xFE, 0xFF, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xA2, 0x40, 0x48, 0x1D, 0x3C, - 0x0B, 0xFE, 0xFE, 0xBB, 0x51, 0xFE, 0xFE, 0xFE, 0x0D, 0x13, 0x92, 0x24, 0x36, 0x30, 0x67, 0x23, - 0x37, 0x25, 0x6D, 0x23, 0x36, 0x2B, 0x67, 0x21, 0x35, 0x46, 0x29, 0x20, 0x0A, 0x1F, 0x2F, 0x97, - 0x11, 0x1A, 0x2E, 0x1C, 0x62, 0x44, 0x2D, 0x1E, 0x23, 0x15, 0x2B, 0x0F, 0x14, 0x1E, 0x2F, 0x20, - 0x19, 0x16, 0x25, 0x1E, 0x0E, 0x1E, 0x2D, 0x20, 0x15, 0x18, 0x22, 0x4F, 0x30, 0x19, 0x21, 0x12, - 0x27, 0x4B, 0x2F, 0x21, 0x15, 0x10, 0x30, 0x47, 0x2E, 0x16, 0x6D, 0x39, 0x2E, 0x2D, 0x48, 0x48, - 0x2D, 0x24, 0x55, 0x3C, 0x30, 0x2D, 0x3C, 0x4C, 0x31, 0x29, 0x34, 0x57, 0x2E, 0x29, 0x2E, 0x34, - 0x03, 0x24, 0x32, 0x26, 0x38, 0x29, 0x04, 0x23, 0x37, 0x24, 0x38, 0x50, 0x31, 0x21, 0x3B, 0x4F, - 0x3C, 0x13, 0x43, 0x42, 0x31, 0x2A, 0x46, 0x3F, 0x2F, 0x2C, 0x3D, 0x3F, 0x33, 0x28, 0x3E, 0x48, - 0x2F, 0x31, 0x41, 0x38, 0x31, 0x2A, 0x4E, 0x30, 0x2E, 0x2A, 0x46, 0x37, 0x2F, 0x2A, 0x46, 0x33, - 0x31, 0x29, 0x49, 0x37, 0x2E, 0x2A, 0x35, 0x47, 0x2F, 0x26, 0x41, 0x42, 0x30, 0x25, 0x50, 0x2C, - 0x32, 0x24, 0x38, 0x43, 0x31, 0x29, 0x35, 0x49, 0x32, 0x1D, 0x37, 0x4C, 0x32, 0x1E, 0x48, 0x32, - 0xFE, 0xD3, 0x48, 0x74, 0x24, 0xAE, 0x2C, 0x45, 0x03, 0x50, 0x02, 0x1D, 0x26, 0x12, 0x02, 0x21, - 0x02, 0x03, 0x02, 0x48, 0x02, 0x1F, 0x1B, 0x46, 0x2E, 0x13, 0x01, 0x2D, 0x02, 0x03, 0x03, 0x01, - 0x03, 0x01, 0x03, 0xCC, 0x1B, 0x4B, 0x03, 0x03, 0x02, 0x01, 0x09, 0x74, 0x08, 0x02, 0x00, 0x02, - 0x1F, 0x79, 0x0E, 0x03, 0x01, 0x03, 0x02, 0x03, 0x02, 0x04, 0x01, 0x24, 0x01, 0x36, 0x02, 0x03, - 0x3C, 0x04, 0x01, 0x1D, 0x1F, 0xCF, 0x03, 0x01, 0x38, 0x90, 0x39, 0xFE, 0xFE, 0xFE, 0xFE, 0xFF, - 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0x5C, 0x06, 0x02, 0x03, 0x03, 0x02, 0x02, 0x03, - 0x3F, 0x0E, 0x13, 0x08, 0x9B, 0x23, 0x2B, 0x36, 0x25, 0x02, 0x25, 0x01, 0x23, 0x03, 0x02, 0x02, - 0x15, 0x02, 0x1B, 0x0A, 0x23, 0x02, 0x25, 0x02, 0x3E, 0x01, 0x05, 0x02, 0x14, 0x0F, 0x25, 0x18, - 0x3E, 0x13, 0x6B, 0x05, 0x0B, 0x0A, 0x72, 0x21, 0x09, 0x16, 0xBA, 0x24, 0x45, 0x03, 0x05, 0x1A, - 0x04, 0x09, 0x02, 0x0F, 0x12, 0x02, 0x1B, 0x04, 0x08, 0x03, 0x0F, 0x04, 0x04, 0x08, 0x04, 0x32, - 0x42, 0x03, 0x07, 0x1A, 0x03, 0x08, 0x03, 0x23, 0x48, 0x0E, 0x05, 0x04, 0x05, 0x25, 0x18, 0x05, - 0x06, 0x06, 0x23, 0x19, 0x05, 0x0B, 0x02, 0x09, 0x03, 0x0A, 0x17, 0x06, 0x07, 0x09, 0x09, 0x02, - 0x19, 0x06, 0x05, 0x07, 0x06, 0x0A, 0x03, 0x09, 0x03, 0x0A, 0x0B, 0x03, 0x07, 0x05, 0x09, 0x08, - 0x08, 0x03, 0x19, 0x07, 0x04, 0x07, 0x07, 0x16, 0x02, 0x0B, 0x0C, 0x02, 0x07, 0x07, 0x06, 0x0A, - 0x08, 0x03, 0x09, 0x03, 0x0B, 0x07, 0x05, 0x06, 0x07, 0x0B, 0x02, 0x08, 0x03, 0x0A, 0x0C, 0x03, - 0x07, 0x07, 0x07, 0x09, 0x08, 0x03, 0x18, 0x06, 0x07, 0x05, 0x0A, 0x09, 0x02, 0x08, 0x03, 0x0A, - 0x0B, 0x04, 0x07, 0x06, 0x08, 0x09, 0x07, 0x04, 0x19, 0x05, 0x07, 0x05, 0x09, 0x08, 0x05, 0x08, - 0x03, 0x0A, 0x0A, 0x05, 0x06, 0x07, 0x07, 0x09, 0x08, 0x04, 0x0B, 0x02, 0x0A, 0x07, 0x05, 0x06, - 0x0B, 0x05, 0x06, 0x09, 0x04, 0x0A, 0x0B, 0x03, 0x07, 0x06, 0x08, 0x07, 0x0A, 0x04, 0x08, 0x05, - 0x06, 0x0B, 0x07, 0x04, 0x1B, 0x09, 0x03, 0x0B, 0x0D, 0x01, 0x07, 0x05, 0x0A, 0x0A, 0x06, 0x04, - 0x09, 0x03, 0x0A, 0x08, 0x05, 0x06, 0x07, 0x05, 0x06, 0x13, 0x02, 0x0F, 0x13, 0x04, 0x0A, 0x09, - 0x08, 0x03, 0x19, 0x05, 0x07, 0x05, 0x06, 0x03, 0x17, 0x08, 0x03, 0x0D, 0x0A, 0x03, 0x07, 0x05, - 0x0A, 0x0B, 0x04, 0x06, 0x09, 0x03, 0x0E, 0x04, 0x07, 0x04, 0x1B, 0x0F, 0x03, 0x0D, 0x0A, 0x02, - 0x08, 0x04, 0x0B, 0x0B, 0x04, 0x06, 0x28, 0x03, 0x1A, 0x22, 0x16, 0x04, 0x0A, 0x0C, 0x04, 0x05, - 0x1B, 0x04, 0x08, 0x04, 0x1A, 0x06, 0x04, 0x1A, 0x17, 0x08, 0x05, 0x0B, 0x05, 0x05, 0x1B, 0x05, - 0x08, 0x09, 0x15, 0x25, 0x18, 0x08, 0x04, 0x0C, 0x26, 0x16, 0x17, 0x26, 0x0C, 0x03, 0x08, 0x19, - 0x1A, 0x06, 0x06, 0x08, 0x1B, 0x04, 0x07, 0x19, 0x05, 0x09, 0x0C, 0x04, 0x07, 0x09, 0x09, 0x06, - 0x08, 0x07, 0x09, 0x08, 0x06, 0x08, 0x19, 0x2B, 0x03, 0x0A, 0x0D, 0x02, 0x09, 0x06, 0x08, 0x0A, - 0x08, 0x05, 0x0A, 0x0B, 0x05, 0x07, 0x18, 0x09, 0x06, 0x23, 0x03, 0x0C, 0x2C, 0x04, 0x1B, 0x19, - 0x08, 0x06, 0x1A, 0x07, 0x07, 0x09, 0x04, 0x09, 0x04, 0x0B, 0x2B, 0x06, 0x1C, 0x07, 0x07, 0x0E, - 0x03, 0x0A, 0x19, 0x05, 0x0A, 0x0A, 0x05, 0x08, 0x08, 0x06, 0x0D, 0x04, 0x0A, 0x0B, 0x07, 0x05, - 0x09, 0x0E, 0x03, 0x1D, 0x2A, 0x1A, 0x0F, 0x1C, 0x28, 0x1C, 0x09, 0x04, 0x0A, 0x18, 0x2F, 0x06, - 0x19, 0x1C, 0x1F, 0x05, 0x05, 0x1B, 0x1E, 0x14, 0x23, 0x1C, 0x19, 0x1D, 0x21, 0x07, 0x04, 0x0D, - 0x36, 0x13, 0x1F, 0x17, 0x1C, 0x24, 0x36, 0x15, 0x26, 0x1C, 0x5D, 0x20, 0x63, 0x2B, 0x53, 0x2A, - 0x7C, 0x28, 0x61, 0x2D, 0xFE, 0x23, 0x22, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFF, - 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0x5C, 0xFE, 0xFE, 0xFE, 0xFE, 0x76, 0x23, 0x6F, - 0x28, 0x84, 0x32, 0x22, 0x05, 0x08, 0x04, 0x1C, 0x16, 0x1B, 0x2E, 0x52, 0x0B, 0x26, 0x2C, 0x54, - 0x08, 0x1D, 0x34, 0x54, 0x0B, 0x1B, 0x32, 0x24, 0x05, 0x2C, 0x0A, 0x1B, 0x25, 0x03, 0x08, 0x25, - 0x05, 0x2C, 0x09, 0x0C, 0x04, 0x0A, 0x2F, 0x24, 0x09, 0x2A, 0x0A, 0x1B, 0x20, 0x02, 0x09, 0x25, - 0x08, 0x4F, 0x1C, 0x04, 0x09, 0x26, 0x07, 0x4F, 0x1B, 0x05, 0x09, 0x24, 0x08, 0x20, 0x08, 0x26, - 0x1B, 0x04, 0x08, 0x0E, 0x05, 0x09, 0x10, 0x52, 0x18, 0x04, 0x08, 0x0E, 0x06, 0x06, 0x13, 0x1E, - 0x18, 0x1B, 0x17, 0x05, 0x07, 0x10, 0x02, 0x09, 0x14, 0x1D, 0x0F, 0x25, 0x16, 0x05, 0x08, 0x0B, - 0x08, 0x07, 0x12, 0x1B, 0x09, 0x05, 0x09, 0x20, 0x18, 0x06, 0x07, 0x0C, 0x08, 0x05, 0x14, 0x19, - 0x17, 0x20, 0x1A, 0x07, 0x07, 0x0B, 0x08, 0x06, 0x0F, 0x1D, 0x08, 0x06, 0x08, 0x21, 0x1B, 0x07, - 0x07, 0x0A, 0x09, 0x06, 0x0B, 0x1E, 0x08, 0x06, 0x08, 0x22, 0x1C, 0x08, 0x06, 0x0B, 0x07, 0x07, - 0x0A, 0x1E, 0x0A, 0x04, 0x09, 0x1A, 0x27, 0x07, 0x07, 0x0A, 0x07, 0x07, 0x09, 0x1E, 0x09, 0x06, - 0x08, 0x1A, 0x2B, 0x07, 0x06, 0x0A, 0x07, 0x07, 0x09, 0x1D, 0x07, 0x07, 0x08, 0x19, 0x06, 0x05, - 0x24, 0x07, 0x07, 0x09, 0x07, 0x07, 0x09, 0x1D, 0x06, 0x07, 0x08, 0x08, 0x05, 0x0A, 0x06, 0x07, - 0x09, 0x07, 0x09, 0x03, 0x0B, 0x06, 0x07, 0x0A, 0x06, 0x08, 0x09, 0x08, 0x06, 0x0C, 0x06, 0x07, - 0x09, 0x08, 0x04, 0x0B, 0x05, 0x07, 0x09, 0x06, 0x08, 0x07, 0x0A, 0x08, 0x06, 0x09, 0x06, 0x08, - 0x09, 0x07, 0x07, 0x0B, 0x06, 0x07, 0x0A, 0x06, 0x05, 0x19, 0x2D, 0x05, 0x09, 0x09, 0x05, 0x09, - 0x07, 0x09, 0x04, 0x1B, 0x07, 0x05, 0x08, 0x18, 0x18, 0x07, 0x08, 0x05, 0x09, 0x05, 0x08, 0x09, - 0x04, 0x0A, 0x07, 0x09, 0x06, 0x09, 0x06, 0x08, 0x17, 0x1F, 0x17, 0x09, 0x05, 0x02, 0x07, 0x04, - 0x0A, 0x1A, 0x04, 0x0A, 0x04, 0x0C, 0x02, 0x09, 0x08, 0x05, 0x09, 0x06, 0x05, 0x07, 0x07, 0x06, - 0x08, 0x07, 0x06, 0x0B, 0x07, 0x05, 0x08, 0x05, 0x08, 0x19, 0x06, 0x07, 0x07, 0x08, 0x03, 0x0A, - 0x07, 0x05, 0x16, 0x09, 0x04, 0x09, 0x03, 0x0A, 0x05, 0x0A, 0x07, 0x0A, 0x09, 0x04, 0x08, 0x0A, - 0x02, 0x0B, 0x06, 0x07, 0x07, 0x08, 0x05, 0x08, 0x25, 0x15, 0x04, 0x0B, 0x02, 0x1A, 0x04, 0x06, - 0x07, 0x05, 0x08, 0x18, 0x06, 0x06, 0x08, 0x06, 0x05, 0x0A, 0x04, 0x08, 0x09, 0x04, 0x08, 0x55, - 0x08, 0x04, 0x09, 0x06, 0x05, 0x0C, 0x04, 0x08, 0x07, 0x05, 0x0B, 0x15, 0x1D, 0x52, 0x0A, 0x02, - 0x0B, 0x04, 0x04, 0x1B, 0x04, 0x06, 0x06, 0x24, 0x18, 0x26, 0x1C, 0x14, 0x0A, 0x02, 0x0B, 0x24, - 0x1F, 0x15, 0x1B, 0x24, 0x1A, 0x19, 0x0E, 0x02, 0x07, 0x27, 0x06, 0x05, 0x0C, 0x1B, 0x16, 0x6C, - 0x0B, 0x03, 0x06, 0x09, 0x03, 0x25, 0x16, 0x13, 0x05, 0x8A, 0x06, 0x03, 0x0C, 0x03, 0x03, 0x6E, - 0x1D, 0x56, 0x22, 0x2C, 0x16, 0x26, 0x1B, 0x5F, 0x24, 0x8C, 0x11, 0x5A, 0x20, 0x62, 0x44, 0x5B, - 0x23, 0x80, 0x1E, 0x70, 0x20, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFF, 0xFE, - 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xBF, 0x04, 0xFE, 0xCF, 0x1A, 0x51, 0x0F, 0x23, - 0x23, 0x36, 0x08, 0x18, 0x0E, 0x1C, 0x26, 0x56, 0x12, 0x16, 0x27, 0x1C, 0x16, 0x26, 0x10, 0x16, - 0x26, 0x1C, 0x14, 0x27, 0x12, 0x15, 0x25, 0x1C, 0x17, 0x24, 0x10, 0x1A, 0x22, 0x1D, 0x1A, 0x20, - 0x11, 0x19, 0x23, 0x1C, 0x19, 0x22, 0x11, 0x1B, 0x22, 0x1A, 0x19, 0x23, 0x13, 0x18, 0x24, 0x1B, - 0x17, 0x25, 0x11, 0x19, 0x23, 0x1B, 0x17, 0x24, 0x12, 0x18, 0x24, 0x1B, 0x16, 0x25, 0x11, 0x19, - 0x24, 0x1B, 0x17, 0x25, 0x10, 0x1A, 0x24, 0x19, 0x18, 0x24, 0x11, 0x19, 0x25, 0x1B, 0x15, 0x26, - 0x11, 0x1A, 0x25, 0x19, 0x15, 0x27, 0x12, 0x19, 0x27, 0x18, 0x14, 0x28, 0x12, 0x18, 0x2A, 0x18, - 0x13, 0x2B, 0x10, 0x18, 0x2B, 0x16, 0x14, 0x2E, 0x0B, 0x19, 0x2D, 0x15, 0x14, 0x2D, 0x0D, 0x19, - 0x2F, 0x15, 0x12, 0x2E, 0x10, 0x16, 0x1A, 0x02, 0x15, 0x14, 0x11, 0x2E, 0x11, 0x16, 0x18, 0x06, - 0x14, 0x15, 0x10, 0x2D, 0x10, 0x16, 0x37, 0x15, 0x0D, 0x2B, 0x11, 0x19, 0x1B, 0x0D, 0x15, 0x2B, - 0x0A, 0x13, 0x11, 0x22, 0x14, 0x11, 0x18, 0x1A, 0x12, 0x1B, 0x1A, 0x15, 0x16, 0x0F, 0x1E, 0x47, - 0x15, 0x17, 0x18, 0x1C, 0x17, 0x22, 0x10, 0x15, 0x18, 0x14, 0x12, 0x16, 0x26, 0x15, 0x1B, 0x16, - 0x1E, 0x12, 0x12, 0x21, 0x21, 0x1B, 0x13, 0x13, 0x24, 0x0F, 0x0F, 0x1E, 0x10, 0x10, 0x0F, 0x10, - 0x0A, 0x08, 0x0C, 0x0F, 0x11, 0x07, 0x0D, 0x11, 0x0E, 0x1D, 0x0E, 0x13, 0x16, 0x21, 0x0E, 0x16, - 0x1A, 0x0F, 0x15, 0x18, 0x0F, 0x1F, 0x17, 0x1E, 0x0F, 0x14, 0x1D, 0x14, 0x12, 0x1A, 0x11, 0x16, - 0x24, 0x15, 0x16, 0x15, 0x1B, 0x16, 0x15, 0x15, 0x13, 0x15, 0x2F, 0x15, 0x16, 0x1F, 0x11, 0x1E, - 0x10, 0x11, 0x1B, 0x13, 0x21, 0x05, 0x0F, 0x22, 0x0A, 0x20, 0x0E, 0x17, 0x16, 0x1C, 0x10, 0x1B, - 0x0F, 0x0C, 0x08, 0x09, 0x0E, 0x21, 0x0C, 0x20, 0x0E, 0x1F, 0x26, 0x0C, 0x19, 0x15, 0x17, 0x12, - 0x21, 0x13, 0x0F, 0x1C, 0x0B, 0x1F, 0x0E, 0x18, 0x0D, 0x4D, 0x3A, 0x40, 0x47, 0xB0, 0x18, 0x72, - 0x30, 0xC7, 0x11, 0x1D, 0x1E, 0x78, 0x0E, 0x4C, 0x70, 0xFF, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, - 0xFE, 0xFE, 0xFE, 0xC4, 0x0E, 0x4A, 0x1A, 0x1D, 0x0F, 0x19, 0x14, 0x19, 0x11, 0x18, 0x1B, 0x11, - 0x19, 0x13, 0x1A, 0x11, 0x1B, 0x14, 0x1A, 0x0E, 0x1C, 0x10, 0x11, 0x18, 0x0D, 0x0D, 0x09, 0x0B, - 0x0A, 0x07, 0x0D, 0x0E, 0x1B, 0x0F, 0x10, 0x17, 0x0F, 0x0C, 0x11, 0x15, 0x10, 0x0D, 0x1A, 0x0F, - 0x0F, 0x17, 0x0D, 0x0E, 0x0E, 0x0D, 0x1A, 0x0E, 0x0E, 0x1A, 0x0E, 0x10, 0x15, 0x0D, 0x0F, 0x0D, - 0x19, 0x0E, 0x0E, 0x1B, 0x0D, 0x0D, 0x19, 0x0C, 0x0E, 0x0D, 0x1B, 0x0D, 0x0D, 0x1D, 0x0C, 0x0C, - 0x0D, 0x1A, 0x0D, 0x0D, 0x1D, 0x0C, 0x0E, 0x0C, 0x03, 0x0D, 0x0B, 0x0C, 0x0D, 0x1B, 0x0C, 0x0E, - 0x1E, 0x0B, 0x0E, 0x1D, 0x0C, 0x0C, 0x0B, 0x1D, 0x0D, 0x0B, 0x20, 0x0C, 0x0D, 0x0C, 0x05, 0x0C, - 0x0B, 0x0C, 0x0C, 0x0A, 0x07, 0x0A, 0x0E, 0x0D, 0x0F, 0x02, 0x0C, 0x0C, 0x0D, 0x0C, 0x05, 0x0C, - 0x0A, 0x0C, 0x0C, 0x20, 0x0B, 0x09, 0x21, 0x0C, 0x0D, 0x1D, 0x0B, 0x0B, 0x0C, 0x22, 0x09, 0x0D, - 0x04, 0x0A, 0x0D, 0x0C, 0x0E, 0x1C, 0x0B, 0x0B, 0x0D, 0x0C, 0x20, 0x15, 0x15, 0x0C, 0x0D, 0x1D, - 0x0A, 0x0C, 0x0B, 0x0D, 0x0C, 0x09, 0x0A, 0x16, 0x15, 0x0C, 0x0D, 0x0C, 0x05, 0x0B, 0x09, 0x0B, - 0x0C, 0x0C, 0x0C, 0x0C, 0x0A, 0x16, 0x13, 0x0B, 0x0D, 0x0D, 0x06, 0x0B, 0x08, 0x0C, 0x0B, 0x0C, - 0x0C, 0x0C, 0x0E, 0x11, 0x13, 0x0B, 0x0E, 0x10, 0x02, 0x0A, 0x09, 0x0C, 0x0B, 0x0C, 0x0C, 0x0C, - 0x0D, 0x12, 0x14, 0x0B, 0x0D, 0x0E, 0x05, 0x0A, 0x09, 0x0C, 0x0B, 0x0A, 0x0C, 0x0E, 0x0C, 0x15, - 0x12, 0x0C, 0x0E, 0x0E, 0x04, 0x0B, 0x09, 0x0B, 0x0C, 0x0A, 0x0B, 0x0D, 0x0F, 0x14, 0x16, 0x0B, - 0x0D, 0x0B, 0x06, 0x0B, 0x09, 0x0B, 0x0C, 0x0C, 0x09, 0x0C, 0x30, 0x05, 0x09, 0x0B, 0x0C, 0x0C, - 0x06, 0x0B, 0x09, 0x0B, 0x0B, 0x0B, 0x0A, 0x0B, 0x17, 0x0D, 0x0C, 0x07, 0x0A, 0x0A, 0x0B, 0x0B, - 0x07, 0x0A, 0x0A, 0x0B, 0x0B, 0x0B, 0x08, 0x0A, 0x0D, 0x18, 0x0D, 0x0A, 0x0A, 0x0A, 0x0B, 0x0B, - 0x06, 0x0B, 0x0A, 0x0B, 0x0C, 0x0B, 0x06, 0x09, 0x0E, 0x26, 0x02, 0x09, 0x0B, 0x0A, 0x0C, 0x09, - 0x08, 0x09, 0x0B, 0x0B, 0x0E, 0x15, 0x0A, 0x12, 0x08, 0x0C, 0x0B, 0x09, 0x08, 0x0C, 0x0B, 0x0A, - 0x09, 0x0A, 0x09, 0x0B, 0x0B, 0x09, 0x03, 0x09, 0x0C, 0x0E, 0x0F, 0x09, 0x0B, 0x0B, 0x0B, 0x09, - 0x0B, 0x0A, 0x07, 0x0B, 0x0B, 0x0B, 0x16, 0x0A, 0x0C, 0x19, 0x03, 0x0D, 0x06, 0x0E, 0x09, 0x0B, - 0x0C, 0x0A, 0x09, 0x0B, 0x0A, 0x0C, 0x16, 0x0A, 0x0B, 0x18, 0x04, 0x0B, 0x08, 0x0D, 0x0A, 0x0C, - 0x0C, 0x09, 0x05, 0x0C, 0x0C, 0x0B, 0x0F, 0x12, 0x0B, 0x18, 0x06, 0x0B, 0x09, 0x0C, 0x0D, 0x0C, - 0x0E, 0x19, 0x0A, 0x0C, 0x0A, 0x09, 0x08, 0x0A, 0x10, 0x0F, 0x04, 0x0B, 0x0A, 0x0A, 0x12, 0x0B, - 0x0C, 0x09, 0x08, 0x0C, 0x0B, 0x0D, 0x05, 0x07, 0x0B, 0x0F, 0x0A, 0x44, 0x0A, 0x0C, 0x0C, 0x09, - 0x04, 0x0B, 0x0C, 0x0A, 0x09, 0x07, 0x0B, 0x0B, 0x09, 0x25, 0x0E, 0x18, 0x0C, 0x0B, 0x0C, 0x08, - 0x04, 0x0C, 0x0C, 0x17, 0x0D, 0x0A, 0x18, 0x1D, 0x07, 0x15, 0x0A, 0x04, 0x09, 0x0B, 0x0B, 0x1B, - 0x0B, 0x17, 0x0B, 0x10, 0x13, 0x1A, 0x0A, 0x13, 0x0E, 0x08, 0x08, 0x0C, 0x08, 0x0A, 0x06, 0x0D, - 0x0A, 0x18, 0x0C, 0x13, 0x0A, 0x4E, 0x13, 0x0B, 0x1A, 0x0A, 0x0C, 0x19, 0x08, 0x17, 0x08, 0x19, - 0x09, 0x18, 0x0A, 0x14, 0x0E, 0x0F, 0x14, 0x1C, 0x09, 0x0A, 0x0B, 0x19, 0x0B, 0x0F, 0x11, 0x2E, - 0x06, 0x08, 0x0E, 0x19, 0x08, 0x19, 0x0B, 0x1A, 0x08, 0x0F, 0x14, 0x0E, 0x11, 0x19, 0x08, 0x19, - 0x0C, 0x12, 0x13, 0x81, 0x07, 0x17, 0x09, 0x2F, 0x1A, 0x83, 0x0B, 0x12, 0x2D, 0x1E, 0x09, 0x54, - 0x0B, 0x37, 0x2C, 0xFE, 0x30, 0x36, 0x59, 0x54, 0xFE, 0xFE, 0x09, 0x44, 0xC5, 0x1A, 0xFE, 0xFF, - 0xFE, 0xFE, 0xFE, 0xFE, 0x02, 0x03, 0x02, 0x4C, 0x03, 0x03, 0x02, 0x5B, 0x03, 0x03, 0x02, 0x35, - 0x02, 0x02, 0x03, 0x02, 0x03, 0x03, 0x03, 0x02, 0x03, 0x02, 0x03, 0x03, 0x02, 0x04, 0x01, 0x11, - 0x01, 0x55, 0x0F, 0x01, 0x03, 0x03, 0x02, 0x6B, 0x15, 0x03, 0x02, 0x15, 0x03, 0x02, 0x03, 0x04, - 0x01, 0x20, 0x02, 0x01, 0x04, 0x02, 0x03, 0x03, 0x02, 0x04, 0x02, 0x21, 0x03, 0x2B, 0x03, 0x02, - 0x03, 0x01, 0x04, 0x09, 0x02, 0x03, 0x02, 0x38, 0x03, 0x79, 0x19, 0x02, 0x03, 0x57, 0x03, 0x02, - 0x02, 0x2C, 0x21, 0x01, 0x03, 0x10, 0x02, 0x2C, 0x02, 0x02, 0x03, 0x02, 0x03, 0x1A, 0x02, 0x0A, - 0x07, 0x02, 0x03, 0x02, 0x03, 0x23, 0x03, 0x01, 0x03, 0x03, 0x03, 0x03, 0x03, 0x11, 0x02, 0x02, - 0x03, 0x03, 0x02, 0x3B, 0x01, 0x11, 0x02, 0x04, 0x01, 0x03, 0x02, 0x1B, 0x07, 0x1C, 0x03, 0x02, - 0x04, 0x01, 0x0A, 0x01, 0x03, 0x1E, 0x02, 0x04, 0x01, 0x19, 0x01, 0x09, 0x04, 0x01, 0x03, 0x03, - 0x02, 0x04, 0x02, 0x22, 0x0A, 0x02, 0x02, 0x11, 0x02, 0x03, 0x02, 0x03, 0x03, 0x02, 0x03, 0x03, - 0x03, 0x03, 0x02, 0x03, 0x02, 0x03, 0x0F, 0x16, 0x02, 0x02, 0x03, 0x03, 0x01, 0x10, 0x02, 0x02, - 0x03, 0x02, 0x03, 0x02, 0x02, 0x0A, 0x03, 0x02, 0x03, 0x1E, 0x03, 0x01, 0x09, 0x10, 0x03, 0x01, - 0x09, 0x02, 0x03, 0x0E, 0x03, 0x01, 0x04, 0x02, 0x02, 0x03, 0x01, 0x11, 0x02, 0x39, 0x0C, 0x02, - 0x03, 0x29, 0x01, 0x02, 0x09, 0x01, 0x03, 0x03, 0x03, 0x03, 0x02, 0x22, 0x03, 0x1C, 0x14, 0x1A, - 0x03, 0x02, 0x03, 0x03, 0x03, 0xFE, 0x5B, 0x1A, 0x1C, 0x05, 0x05, 0x0C, 0x25, 0x08, 0x23, 0x1E, - 0x09, 0x03, 0x0E, 0x07, 0x05, 0x0A, 0x1A, 0x08, 0x05, 0x06, 0x1B, 0x2B, 0x0B, 0x02, 0x0D, 0x08, - 0x06, 0x04, 0x1A, 0x17, 0x18, 0x20, 0x02, 0x0B, 0x0A, 0x03, 0x0C, 0x08, 0x06, 0x04, 0x1A, 0x0B, - 0x23, 0x22, 0x01, 0x05, 0x02, 0x03, 0x0B, 0x03, 0x0C, 0x09, 0x06, 0x03, 0x19, 0x14, 0x19, 0x18, - 0x02, 0x09, 0x03, 0x04, 0x02, 0x04, 0x0A, 0x04, 0x07, 0x0B, 0x25, 0x12, 0x1B, 0x16, 0x06, 0x08, - 0x03, 0x06, 0x01, 0x03, 0x0B, 0x04, 0x06, 0x0B, 0x1A, 0x14, 0x23, 0x16, 0x07, 0x0B, 0x03, 0x0A, - 0x0B, 0x04, 0x06, 0x0B, 0x18, 0x16, 0x19, 0x1F, 0x0D, 0x0C, 0x02, 0x05, 0x03, 0x02, 0x0B, 0x04, - 0x04, 0x0C, 0x19, 0x15, 0x1B, 0x18, 0x13, 0x0F, 0x01, 0x05, 0x04, 0x01, 0x0B, 0x05, 0x03, 0x0C, - 0x1A, 0x14, 0x18, 0x15, 0x15, 0x13, 0x04, 0x03, 0x04, 0x01, 0x0B, 0x05, 0x04, 0x0B, 0x18, 0x16, - 0x17, 0x16, 0x15, 0x17, 0x05, 0x03, 0x11, 0x06, 0x04, 0x09, 0x18, 0x16, 0x17, 0x15, 0x16, 0x1A, - 0x05, 0x03, 0x11, 0x05, 0x06, 0x07, 0x1A, 0x17, 0x1A, 0x10, 0x19, 0x1A, 0x1A, 0x05, 0x29, 0x16, - 0x17, 0x14, 0x19, 0x16, 0x04, 0x07, 0x1A, 0x05, 0x06, 0x08, 0x1A, 0x17, 0x0C, 0x16, 0x41, 0x08, - 0x0B, 0x02, 0x0C, 0x07, 0x06, 0x07, 0x19, 0x16, 0x0E, 0x1A, 0x4E, 0x0B, 0x0A, 0x03, 0x0A, 0x16, - 0x19, 0x17, 0x18, 0x17, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, - 0xFE, 0xFE, 0xFE, 0xFE, 0xEE, 0x02, 0x03, 0x03, 0xFE, 0x3D, 0x08, 0x01, 0x03, 0x02, 0x03, 0x03, - 0x02, 0x03, 0x02, 0x65, 0x1A, 0xCF, 0x02, 0x3E, 0x02, 0x03, 0x02, 0x03, 0x02, 0x03, 0x02, 0x03, - 0x03, 0x3E, 0x03, 0x02, 0x02, 0x03, 0x02, 0x04, 0x02, 0x1B, 0x02, 0x16, 0x02, 0x03, 0x02, 0x5F, - 0x08, 0x01, 0x03, 0x03, 0x02, 0x04, 0x02, 0xED, 0x02, 0x03, 0x02, 0x16, 0x02, 0xFE, 0x4A, 0x40, - 0xE7, 0x14, 0x6F, 0x02, 0x03, 0x02, 0x80, 0x02, 0x03, 0x02, 0xFE, 0xFE, 0xFE, 0xFE, 0xFF, 0xFE, - 0x7C, 0x24, 0x9B, 0x03, 0x02, 0x03, 0x03, 0x02, 0x03, 0x03, 0x0E, 0x08, 0x01, 0x03, 0x52, 0x02, - 0x03, 0x09, 0x01, 0x03, 0x03, 0x02, 0x03, 0x03, 0x03, 0x01, 0x0A, 0x01, 0x03, 0x02, 0x1F, 0x02, - 0x02, 0x03, 0x03, 0x07, 0x14, 0x02, 0x03, 0x02, 0x03, 0x03, 0x3A, 0x03, 0x02, 0x09, 0x02, 0x02, - 0x3B, 0x01, 0x16, 0x01, 0x03, 0x03, 0x01, 0x03, 0x02, 0x03, 0x23, 0x03, 0x02, 0x03, 0x02, 0x03, - 0x03, 0x04, 0x02, 0x02, 0x1E, 0x08, 0x0E, 0x02, 0x03, 0x03, 0x02, 0x02, 0x03, 0x02, 0x41, 0x01, - 0x03, 0x02, 0x11, 0x1A, 0x02, 0x17, 0x02, 0x03, 0x03, 0x02, 0x0D, 0x03, 0x01, 0x03, 0x02, 0x03, - 0x02, 0x04, 0x01, 0x03, 0x10, 0x01, 0x03, 0x01, 0x34, 0x02, 0x03, 0x02, 0x04, 0x19, 0x01, 0x03, - 0x01, 0x0F, 0x02, 0x03, 0x02, 0x03, 0x02, 0x03, 0x02, 0x02, 0x10, 0x01, 0x03, 0x02, 0x03, 0x02, - 0x16, 0x02, 0x03, 0x02, 0x03, 0x03, 0x03, 0x02, 0x0E, 0x02, 0x02, 0x02, 0x03, 0x01, 0x0B, 0x02, - 0x02, 0x02, 0x03, 0x02, 0x03, 0x01, 0x0A, 0x03, 0x01, 0x03, 0x03, 0x02, 0x18, 0x02, 0x03, 0x02, - 0x1D, 0x02, 0x04, 0x01, 0x03, 0x03, 0x03, 0x02, 0x03, 0x02, 0x03, 0x03, 0x02, 0x03, 0x02, 0x03, - 0x02, 0x03, 0x15, 0x02, 0x03, 0x02, 0x04, 0x01, 0x0C, 0x02, 0x10, 0x02, 0x03, 0x02, 0x1A, 0x02, - 0x0F, 0x02, 0x02, 0x03, 0x01, 0x04, 0x02, 0x02, 0x14, 0x03, 0x02, 0x03, 0x02, 0x03, 0x04, 0x01, - 0x0B, 0x01, 0x03, 0x02, 0x03, 0x02, 0x15, 0x02, 0x02, 0x03, 0x03, 0x02, 0x05, 0x02, 0x02, 0x03, - 0x02, 0x03, 0x02, 0x03, 0x12, 0x01, 0x03, 0x02, 0x03, 0x02, 0x03, 0x01, 0x04, 0x01, 0x03, 0x01, - 0x16, 0x02, 0x28, 0x02, 0x02, 0x0E, 0x02, 0x03, 0x19, 0x03, 0x02, 0x03, 0x02, 0x02, 0x11, 0x01, - 0x03, 0x03, 0x02, 0x02, 0x02, 0x02, 0x04, 0x02, 0x04, 0x01, 0x04, 0x01, 0x04, 0x01, 0x18, 0x02, - 0x04, 0x02, 0x03, 0x02, 0x04, 0x01, 0x0A, 0x01, 0x0C, 0x02, 0x01, 0x0B, 0x02, 0x03, 0x02, 0x04, - 0x01, 0x03, 0x02, 0x03, 0x03, 0x03, 0x02, 0x03, 0x02, 0x03, 0x02, 0x02, 0x26, 0x02, 0x10, 0x02, - 0x0F, 0x03, 0x01, 0x04, 0x02, 0x03, 0x02, 0x03, 0x02, 0x02, 0x03, 0x02, 0x0B, 0x03, 0x02, 0x03, - 0x19, 0x02, 0x03, 0x02, 0x2A, 0x03, 0x01, 0x04, 0x01, 0x03, 0x03, 0x03, 0x34, 0x02, 0x02, 0x02, - 0x03, 0x02, 0x08, 0x02, 0x03, 0x03, 0x03, 0x02, 0x0C, 0x03, 0x01, 0x03, 0x02, 0x02, 0x14, 0x02, - 0x03, 0x02, 0x03, 0x01, 0x04, 0x02, 0x03, 0x02, 0x03, 0x02, 0x03, 0x01, 0x0C, 0x02, 0x02, 0x03, - 0x03, 0x01, 0x3E, 0x03, 0x01, 0x11, 0x02, 0x03, 0x02, 0x03, 0x03, 0x03, 0x02, 0x03, 0x01, 0x03, - 0x02, 0x03, 0x03, 0x02, 0x2D, 0x02, 0x35, 0x37, 0x5D, 0x22, 0x1C, 0x15, 0x1D, 0x15, 0x1A, 0x28, - 0x13, 0x01, 0x05, 0x09, 0x0C, 0x02, 0x0C, 0x02, 0x0B, 0x0A, 0x22, 0x21, 0x03, 0x05, 0x0B, 0x0A, - 0x05, 0x08, 0x0C, 0x02, 0x0A, 0x0E, 0x02, 0x05, 0x18, 0x1A, 0x06, 0x10, 0x01, 0x05, 0x0B, 0x03, - 0x0C, 0x08, 0x0C, 0x03, 0x0A, 0x0B, 0x04, 0x05, 0x0A, 0x04, 0x0B, 0x05, 0x13, 0x03, 0x03, 0x11, - 0x02, 0x04, 0x0B, 0x0C, 0x03, 0x08, 0x19, 0x0D, 0x14, 0x15, 0x13, 0x14, 0x02, 0x02, 0x04, 0x03, - 0x0B, 0x16, 0x11, 0x02, 0x04, 0x0B, 0x17, 0x13, 0x0D, 0x1C, 0x0B, 0x03, 0x0A, 0x16, 0x11, 0x12, - 0x17, 0x09, 0x18, 0x1E, 0x0A, 0x05, 0x08, 0x0E, 0x05, 0x02, 0x11, 0x13, 0x18, 0x07, 0x17, 0x14, - 0x02, 0x09, 0x03, 0x03, 0x03, 0x04, 0x0A, 0x15, 0x10, 0x14, 0x0F, 0x01, 0x06, 0x08, 0x17, 0x14, - 0x02, 0x0B, 0x0A, 0x03, 0x09, 0x13, 0x12, 0x14, 0x0E, 0x02, 0x06, 0x08, 0x17, 0x0A, 0x06, 0x02, - 0x05, 0x0D, 0x08, 0x03, 0x09, 0x0F, 0x14, 0x15, 0x0F, 0x12, 0x16, 0x09, 0x0E, 0x0F, 0x14, 0x0F, - 0x14, 0x16, 0x0D, 0x12, 0x17, 0x0A, 0x0F, 0x0E, 0x09, 0x02, 0x09, 0x0D, 0x15, 0x0E, 0x03, 0x05, - 0x0B, 0x14, 0x0C, 0x13, 0x0A, 0x15, 0x0B, 0x03, 0x08, 0x0C, 0x16, 0x0C, 0x07, 0x03, 0x0B, 0x0A, - 0x15, 0x14, 0x0C, 0x14, 0x0C, 0x04, 0x07, 0x0B, 0x17, 0x0B, 0x08, 0x05, 0x0A, 0x08, 0x08, 0x05, - 0x08, 0x09, 0x07, 0x05, 0x09, 0x16, 0x0A, 0x06, 0x08, 0x0B, 0x18, 0x0B, 0x05, 0x06, 0x0C, 0x07, - 0x06, 0x08, 0x18, 0x06, 0x0A, 0x08, 0x03, 0x0A, 0x0B, 0x05, 0x08, 0x0C, 0x17, 0x0C, 0x04, 0x07, - 0x0A, 0x09, 0x05, 0x08, 0x09, 0x08, 0x06, 0x08, 0x08, 0x09, 0x04, 0x09, 0x0B, 0x05, 0x08, 0x0B, - 0x18, 0x0C, 0x04, 0x07, 0x0C, 0x05, 0x08, 0x09, 0x07, 0x07, 0x0A, 0x09, 0x05, 0x09, 0x06, 0x09, - 0x0A, 0x06, 0x09, 0x0B, 0x0B, 0x02, 0x0C, 0x0A, 0x03, 0x09, 0x0B, 0x04, 0x08, 0x0C, 0x04, 0x07, - 0x0B, 0x05, 0x08, 0x0B, 0x05, 0x08, 0x0B, 0x07, 0x08, 0x0B, 0x09, 0x05, 0x0E, 0x06, 0x07, 0x0D, - 0x15, 0x17, 0x17, 0x0C, 0x08, 0x08, 0x0B, 0x06, 0x0C, 0x0A, 0x0A, 0x04, 0x1B, 0x0D, 0x16, 0x19, - 0x1D, 0x05, 0x0E, 0x08, 0x0A, 0x05, 0x3F, 0x31, 0x43, 0x20, 0xB4, 0x23, 0xBE, 0x39, 0x80, 0x16, - 0x10, 0x24, 0xFE, 0xBF, 0x81, 0x75, 0x19, 0x73, 0x11, 0x5C, 0x1C, 0xE7, 0x1B, 0xA1, 0x15, 0x38, - 0x1C, 0x83, 0x2E, 0x42, 0x1A, 0x8B, 0x16, 0x5B, 0x3B, 0xEC, 0x31, 0xFE, 0x47, 0x75, 0xFE, 0xFE, - 0xFE, 0xFE, 0xFE, 0xFE, 0xFF, 0xFE, 0xFE, 0x20, 0x03, 0x04, 0x05, 0x17, 0x1A, 0x22, 0x0A, 0x1F, - 0x1B, 0x3E, 0x1C, 0x18, 0x12, 0x20, 0x10, 0x1F, 0x0A, 0x17, 0x18, 0x05, 0x02, 0x19, 0x13, 0x17, - 0x17, 0x3A, 0x1B, 0x19, 0x12, 0x16, 0x09, 0x02, 0x09, 0x1E, 0x1B, 0x03, 0x0F, 0x02, 0x06, 0x19, - 0x04, 0x22, 0x05, 0x04, 0x0D, 0x26, 0x18, 0x01, 0x06, 0x03, 0x05, 0x03, 0x02, 0x13, 0x04, 0x05, - 0x05, 0x18, 0x04, 0x03, 0x15, 0x1F, 0x23, 0x02, 0x06, 0x02, 0x03, 0x06, 0x01, 0x0A, 0x04, 0x05, - 0x05, 0x18, 0x03, 0x07, 0x09, 0x03, 0x06, 0x1E, 0x19, 0x01, 0x07, 0x03, 0x05, 0x18, 0x05, 0x04, - 0x07, 0x16, 0x02, 0x07, 0x09, 0x05, 0x04, 0x20, 0x1F, 0x03, 0x05, 0x19, 0x04, 0x05, 0x07, 0x16, - 0x02, 0x0F, 0x02, 0x05, 0x05, 0x1D, 0x1D, 0x03, 0x05, 0x04, 0x02, 0x12, 0x05, 0x03, 0x13, 0x1B, - 0x0D, 0x20, 0x1A, 0x03, 0x04, 0x05, 0x01, 0x14, 0x03, 0x04, 0x05, 0x21, 0x03, 0x03, 0x11, 0x1B, - 0x1A, 0x02, 0x05, 0x04, 0x02, 0x1B, 0x15, 0x24, 0x09, 0x1F, 0x0F, 0x02, 0x06, 0x03, 0x02, 0x1B, - 0x19, 0x24, 0x09, 0x20, 0x0A, 0x02, 0x06, 0x03, 0x03, 0x1A, 0x04, 0x05, 0x16, 0x3D, 0x04, 0x03, - 0x0B, 0x03, 0x05, 0x03, 0x04, 0x1B, 0x02, 0x09, 0x15, 0x04, 0x03, 0x23, 0x25, 0x03, 0x05, 0x03, - 0x04, 0x2F, 0x13, 0x05, 0x02, 0x22, 0x03, 0x04, 0x04, 0x04, 0x12, 0x02, 0x06, 0x03, 0x04, 0x1C, - 0x01, 0x07, 0x03, 0x07, 0x11, 0x4A, 0x08, 0x02, 0x06, 0x03, 0x04, 0x25, 0x1D, 0x24, 0x0B, 0x03, - 0x03, 0x13, 0x0B, 0x03, 0x05, 0x03, 0x05, 0x23, 0x12, 0x27, 0x06, 0x03, 0x06, 0x17, 0x03, 0x03, - 0x10, 0x03, 0x05, 0x03, 0x04, 0x24, 0x04, 0x06, 0x04, 0x13, 0x03, 0x0A, 0x02, 0x04, 0x10, 0x24, - 0x11, 0x02, 0x05, 0x04, 0x04, 0x25, 0x03, 0x22, 0x04, 0x3F, 0x16, 0x03, 0x05, 0x03, 0x04, 0x47, - 0x10, 0x06, 0x03, 0x08, 0x44, 0x03, 0x04, 0x04, 0x04, 0x47, 0x05, 0x06, 0x04, 0x04, 0x04, 0x06, - 0x05, 0x39, 0x11, 0x02, 0x06, 0x02, 0x05, 0x2E, 0x05, 0x04, 0x13, 0x10, 0x1D, 0x3D, 0x07, 0x03, - 0x05, 0x03, 0x04, 0x39, 0x38, 0x50, 0x04, 0x03, 0x05, 0x03, 0x04, 0x04, 0x02, 0x1D, 0x02, 0x05, - 0x2C, 0x11, 0x03, 0x09, 0x02, 0x06, 0x01, 0x2D, 0x1E, 0x02, 0x05, 0x03, 0x05, 0x03, 0x02, 0x04, - 0x02, 0x1E, 0x09, 0x03, 0x03, 0x04, 0x03, 0x25, 0x03, 0x0B, 0x01, 0x05, 0x10, 0x1E, 0x03, 0x06, - 0x34, 0x32, 0x1E, 0x80, 0x04, 0x03, 0x05, 0x03, 0x05, 0xD7, 0x19, 0x03, 0x18, 0xF5, 0x1B, 0xFE, - 0xFE, 0x0D, 0x8F, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, - 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFF, 0xFE, 0xFE, 0x0A, 0x1A, 0xB8, 0x1B, 0xB9, 0x0D, 0x5F, 0x2F, - 0x33, 0x0D, 0x1A, 0x08, 0x1C, 0x09, 0x18, 0x0C, 0x55, 0x09, 0x0C, 0x07, 0x0A, 0x07, 0x11, 0x03, - 0x0A, 0x0A, 0x0E, 0x15, 0x0B, 0x0A, 0x1B, 0x13, 0x0E, 0x0A, 0x0B, 0x09, 0x0A, 0x07, 0x0E, 0x06, - 0x0A, 0x09, 0x0B, 0x0B, 0x05, 0x07, 0x0A, 0x09, 0x1C, 0x14, 0x09, 0x0E, 0x0B, 0x0B, 0x09, 0x06, - 0x0E, 0x07, 0x0A, 0x09, 0x0A, 0x18, 0x09, 0x0A, 0x1D, 0x0F, 0x0B, 0x0D, 0x0C, 0x0A, 0x09, 0x08, - 0x0B, 0x0B, 0x07, 0x0A, 0x0A, 0x0C, 0x56, 0x0D, 0x0C, 0x0A, 0x09, 0x08, 0x0C, 0x0A, 0x0A, 0x07, - 0x0B, 0x0C, 0x0F, 0x07, 0x2B, 0x0B, 0x07, 0x0B, 0x09, 0x0B, 0x0A, 0x09, 0x0C, 0x08, 0x0B, 0x09, - 0x0A, 0x0B, 0x0B, 0x08, 0x3E, 0x0A, 0x0A, 0x0B, 0x0C, 0x09, 0x0C, 0x08, 0x0B, 0x0A, 0x09, 0x0A, - 0x09, 0x0A, 0x0D, 0x09, 0x23, 0x0B, 0x0A, 0x0C, 0x0B, 0x07, 0x0D, 0x08, 0x0C, 0x09, 0x09, 0x0A, - 0x0B, 0x0B, 0x3B, 0x0B, 0x08, 0x0C, 0x0A, 0x09, 0x0C, 0x0A, 0x0B, 0x08, 0x0A, 0x0A, 0x0A, 0x0B, - 0x22, 0x09, 0x0D, 0x0D, 0x06, 0x0C, 0x0B, 0x09, 0x0C, 0x09, 0x0B, 0x09, 0x0A, 0x09, 0x0B, 0x0A, - 0x24, 0x08, 0x0D, 0x09, 0x0A, 0x0C, 0x0C, 0x08, 0x0B, 0x0A, 0x0B, 0x0A, 0x09, 0x0A, 0x09, 0x0B, - 0x10, 0x06, 0x0D, 0x08, 0x0F, 0x08, 0x0B, 0x0B, 0x0D, 0x08, 0x0B, 0x0A, 0x0B, 0x09, 0x09, 0x0A, - 0x0A, 0x0B, 0x0F, 0x07, 0x0C, 0x0A, 0x0C, 0x09, 0x0B, 0x0C, 0x0D, 0x07, 0x0C, 0x0A, 0x0A, 0x0A, - 0x0A, 0x09, 0x0A, 0x0A, 0x0F, 0x09, 0x0B, 0x0A, 0x0F, 0x0C, 0x06, 0x0C, 0x0C, 0x08, 0x0C, 0x0A, - 0x0A, 0x0A, 0x09, 0x0B, 0x09, 0x0B, 0x0C, 0x09, 0x0D, 0x0A, 0x0E, 0x0F, 0x07, 0x0C, 0x0B, 0x09, - 0x0B, 0x0A, 0x0B, 0x09, 0x09, 0x0A, 0x0A, 0x0B, 0x0C, 0x0A, 0x0D, 0x09, 0x15, 0x0C, 0x05, 0x04, - 0x02, 0x05, 0x0E, 0x06, 0x0C, 0x0A, 0x0B, 0x09, 0x09, 0x0B, 0x0A, 0x0A, 0x0C, 0x08, 0x0E, 0x08, - 0x0F, 0x05, 0x04, 0x0C, 0x0A, 0x09, 0x0B, 0x08, 0x0B, 0x0A, 0x0B, 0x09, 0x09, 0x0A, 0x0A, 0x0B, - 0x44, 0x0D, 0x06, 0x0C, 0x0D, 0x06, 0x0C, 0x0A, 0x0B, 0x09, 0x09, 0x0B, 0x0B, 0x0B, 0x45, 0x0C, - 0x0B, 0x0A, 0x0B, 0x07, 0x0C, 0x09, 0x0B, 0x08, 0x0A, 0x0A, 0x0C, 0x0B, 0x22, 0x09, 0x0D, 0x19, - 0x08, 0x0B, 0x11, 0x02, 0x0C, 0x0A, 0x0B, 0x08, 0x08, 0x0B, 0x0C, 0x0B, 0x3B, 0x1A, 0x06, 0x0C, - 0x21, 0x0A, 0x0B, 0x1C, 0x0D, 0x0C, 0x3A, 0x1B, 0x07, 0x0B, 0x0D, 0x07, 0x0C, 0x09, 0x0E, 0x09, - 0x4D, 0x09, 0x0D, 0x0F, 0x04, 0x0B, 0x08, 0x0A, 0x0D, 0x07, 0x0C, 0x0A, 0x0B, 0x0A, 0x0A, 0x0C, - 0x64, 0x0B, 0x09, 0x0A, 0x0B, 0x09, 0x0B, 0x09, 0x0C, 0x09, 0x0A, 0x08, 0x0B, 0x0B, 0x57, 0x0C, - 0x06, 0x0B, 0x0B, 0x09, 0x0C, 0x06, 0x0C, 0x09, 0x0A, 0x0A, 0x0A, 0x08, 0x0A, 0x09, 0x45, 0x0D, - 0x0B, 0x0B, 0x0A, 0x09, 0x0A, 0x09, 0x0A, 0x0A, 0x09, 0x09, 0x0C, 0x09, 0x65, 0x08, 0x0A, 0x0B, - 0x0B, 0x05, 0x0D, 0x08, 0xA4, 0x08, 0x0B, 0x0A, 0x0B, 0x05, 0x0E, 0x05, 0x0D, 0x05, 0x98, 0x08, - 0x0B, 0x0B, 0x0A, 0x04, 0x99, 0x1A, 0x08, 0x0E, 0x0B, 0x06, 0x10, 0x03, 0x1B, 0x0A, 0x2C, 0x09, - 0x5E, 0x12, 0x0C, 0x06, 0xD9, 0x09, 0x1A, 0x08, 0x3C, 0x07, 0x87, 0x07, 0x0E, 0x02, 0x09, 0x09, - 0x59, 0x07, 0x6A, 0x07, 0x1A, 0x16, 0xC4, 0x07, 0xFA, 0x07, 0xFE, 0x03, 0x17, 0xFE, 0xFE, 0xFE, - 0xFF, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0x9C, 0x6F, 0xFE, 0x97, 0x39, 0x70, - 0x18, 0x16, 0x0D, 0x17, 0x18, 0x1B, 0x12, 0x17, 0x15, 0x17, 0x0E, 0x18, 0x12, 0x20, 0x16, 0x0A, - 0x0B, 0x02, 0x06, 0x10, 0x04, 0x03, 0x0D, 0x15, 0x14, 0x14, 0x15, 0x0A, 0x05, 0x05, 0x0D, 0x04, - 0x03, 0x10, 0x06, 0x03, 0x0B, 0x16, 0x13, 0x14, 0x0C, 0x16, 0x18, 0x04, 0x03, 0x0F, 0x07, 0x04, - 0x0A, 0x15, 0x14, 0x14, 0x13, 0x0F, 0x1A, 0x16, 0x09, 0x03, 0x09, 0x15, 0x14, 0x15, 0x0A, 0x16, - 0x1A, 0x16, 0x08, 0x04, 0x09, 0x15, 0x14, 0x16, 0x08, 0x16, 0x17, 0x05, 0x02, 0x05, 0x01, 0x08, - 0x08, 0x03, 0x09, 0x15, 0x15, 0x15, 0x0A, 0x15, 0x17, 0x06, 0x02, 0x0D, 0x09, 0x03, 0x09, 0x15, - 0x0E, 0x1C, 0x09, 0x15, 0x07, 0x03, 0x0A, 0x04, 0x03, 0x03, 0x02, 0x08, 0x08, 0x03, 0x0A, 0x14, - 0x15, 0x0E, 0x0F, 0x15, 0x06, 0x04, 0x09, 0x06, 0x07, 0x09, 0x08, 0x03, 0x09, 0x15, 0x13, 0x15, - 0x09, 0x07, 0x02, 0x0C, 0x04, 0x04, 0x0A, 0x05, 0x07, 0x0A, 0x06, 0x04, 0x0A, 0x13, 0x14, 0x15, - 0x09, 0x07, 0x04, 0x09, 0x12, 0x06, 0x01, 0x04, 0x01, 0x09, 0x07, 0x04, 0x09, 0x14, 0x14, 0x15, - 0x0A, 0x1D, 0x08, 0x04, 0x03, 0x03, 0x02, 0x09, 0x06, 0x04, 0x0A, 0x13, 0x15, 0x15, 0x14, 0x12, - 0x08, 0x04, 0x03, 0x03, 0x02, 0x09, 0x07, 0x03, 0x0A, 0x14, 0x15, 0x15, 0x0B, 0x04, 0x06, 0x0F, - 0x08, 0x04, 0x04, 0x0E, 0x06, 0x04, 0x09, 0x14, 0x15, 0x16, 0x14, 0x0F, 0x08, 0x04, 0x09, 0x08, - 0x07, 0x04, 0x0A, 0x13, 0x15, 0x15, 0x07, 0x1F, 0x07, 0x03, 0x05, 0x03, 0x01, 0x09, 0x06, 0x04, - 0x09, 0x14, 0x14, 0x17, 0x06, 0x06, 0x06, 0x12, 0x08, 0x03, 0x09, 0x09, 0x06, 0x04, 0x0A, 0x13, - 0x15, 0x16, 0x07, 0x06, 0x05, 0x13, 0x07, 0x04, 0x09, 0x09, 0x05, 0x05, 0x0A, 0x13, 0x15, 0x16, - 0x06, 0x08, 0x03, 0x14, 0x08, 0x03, 0x09, 0x09, 0x06, 0x04, 0x0A, 0x14, 0x14, 0x15, 0x15, 0x13, - 0x09, 0x03, 0x05, 0x0E, 0x04, 0x05, 0x09, 0x14, 0x14, 0x16, 0x14, 0x15, 0x09, 0x03, 0x05, 0x0F, - 0x04, 0x05, 0x0B, 0x12, 0x14, 0x17, 0x13, 0x15, 0x0B, 0x03, 0x05, 0x0E, 0x04, 0x05, 0x0A, 0x14, - 0x14, 0x16, 0x13, 0x18, 0x0B, 0x03, 0x04, 0x10, 0x05, 0x03, 0x0B, 0x14, 0x13, 0x18, 0x0E, 0x16, - 0x11, 0x04, 0x04, 0x0F, 0x05, 0x03, 0x0A, 0x14, 0x15, 0x16, 0x11, 0x18, 0x10, 0x03, 0x04, 0x0F, - 0x05, 0x04, 0x07, 0x17, 0x05, 0x02, 0x0F, 0x16, 0x14, 0x17, 0x06, 0x01, 0x08, 0x03, 0x04, 0x10, - 0x04, 0x04, 0x0C, 0x13, 0x03, 0x04, 0x12, 0x13, 0x10, 0x17, 0x16, 0x04, 0x03, 0x10, 0x04, 0x05, - 0x0B, 0x13, 0x03, 0x04, 0x10, 0x14, 0x03, 0x04, 0x0C, 0x1F, 0x11, 0x03, 0x04, 0x10, 0x04, 0x04, - 0x0C, 0x13, 0x04, 0x02, 0x11, 0x18, 0x16, 0x16, 0x19, 0x02, 0x04, 0x1A, 0x06, 0x20, 0x17, 0x16, - 0x16, 0x16, 0x19, 0x04, 0x03, 0x11, 0x03, 0x05, 0x06, 0x18, 0x03, 0x0A, 0x12, 0x17, 0x19, 0x12, - 0x21, 0x04, 0x04, 0x1A, 0x04, 0x28, 0x11, 0x18, 0x16, 0x16, 0x14, 0x0C, 0x07, 0x02, 0x05, 0x19, - 0x05, 0x22, 0x17, 0x05, 0x02, 0x0D, 0x1A, 0x1E, 0x0B, 0x15, 0x05, 0x03, 0x05, 0x1A, 0x03, 0x05, - 0x0B, 0x09, 0x02, 0x0B, 0x1F, 0x40, 0x11, 0x13, 0x07, 0x02, 0x05, 0x04, 0x02, 0x12, 0x15, 0x20, - 0x11, 0x19, 0x10, 0x22, 0x2D, 0x03, 0x05, 0x19, 0x05, 0x03, 0x0C, 0x1F, 0x17, 0x4A, 0x2E, 0x03, - 0x05, 0x19, 0x03, 0x3E, 0x11, 0x22, 0x0B, 0x44, 0x18, 0x1B, 0x17, 0x1B, 0x19, 0x19, 0x12, 0x4B, - 0x1D, 0xE1, 0x27, 0xF4, 0x1E, 0x3C, 0x49, 0xFE, 0xFF, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, - 0xAA, 0x1C, 0xD0, 0x2F, 0x09, 0x03, 0x0F, 0x03, 0x04, 0x01, 0x0A, 0x03, 0x42, 0x17, 0x23, 0x16, - 0x1C, 0x0E, 0x10, 0x10, 0x32, 0x09, 0x12, 0x0F, 0x03, 0x02, 0x26, 0x02, 0x22, 0x13, 0x81, 0x08, - 0x20, 0x2B, 0x11, 0x0F, 0x16, 0x03, 0x2C, 0x04, 0x21, 0x03, 0x09, 0x08, 0x08, 0x04, 0x10, 0x0D, - 0x06, 0x06, 0x04, 0x03, 0x3A, 0x03, 0x09, 0x06, 0x06, 0x09, 0x02, 0x15, 0x11, 0x18, 0x4F, 0x02, - 0x1A, 0x1A, 0x09, 0x14, 0x18, 0x06, 0x04, 0x05, 0x1A, 0x05, 0x05, 0x3E, 0x0B, 0x10, 0x19, 0x07, - 0x24, 0x06, 0x0A, 0x04, 0x04, 0x0B, 0x02, 0x1B, 0x10, 0x11, 0x18, 0x08, 0x18, 0x04, 0x08, 0x05, - 0x0A, 0x18, 0x01, 0x1A, 0x09, 0x09, 0x03, 0x06, 0x17, 0x07, 0x19, 0x05, 0x08, 0x05, 0x11, 0x1E, - 0x02, 0x0C, 0x08, 0x09, 0x02, 0x08, 0x0D, 0x03, 0x05, 0x07, 0x18, 0x04, 0x0A, 0x04, 0x17, 0x1C, - 0x01, 0x0C, 0x08, 0x0A, 0x01, 0x09, 0x0C, 0x04, 0x05, 0x06, 0x17, 0x05, 0x0B, 0x03, 0x0B, 0x04, - 0x0A, 0x06, 0x05, 0x12, 0x01, 0x06, 0x03, 0x01, 0x07, 0x15, 0x0E, 0x03, 0x03, 0x08, 0x16, 0x08, - 0x16, 0x06, 0x07, 0x06, 0x0A, 0x12, 0x02, 0x05, 0x03, 0x02, 0x05, 0x15, 0x0E, 0x10, 0x17, 0x06, - 0x17, 0x08, 0x07, 0x03, 0x0B, 0x13, 0x03, 0x04, 0x03, 0x03, 0x03, 0x15, 0x0D, 0x12, 0x18, 0x04, - 0x17, 0x08, 0x16, 0x15, 0x02, 0x06, 0x02, 0x04, 0x03, 0x0E, 0x01, 0x04, 0x0E, 0x0B, 0x02, 0x03, - 0x1A, 0x04, 0x17, 0x08, 0x15, 0x14, 0x03, 0x04, 0x03, 0x04, 0x02, 0x0F, 0x01, 0x03, 0x0E, 0x10, - 0x17, 0x07, 0x17, 0x09, 0x0F, 0x1A, 0x02, 0x06, 0x03, 0x03, 0x03, 0x0D, 0x04, 0x01, 0x11, 0x07, - 0x23, 0x03, 0x16, 0x09, 0x07, 0x04, 0x05, 0x19, 0x02, 0x06, 0x02, 0x03, 0x04, 0x0C, 0x04, 0x02, - 0x0F, 0x09, 0x05, 0x03, 0x14, 0x09, 0x14, 0x08, 0x07, 0x07, 0x06, 0x15, 0x03, 0x04, 0x03, 0x02, - 0x05, 0x0C, 0x05, 0x02, 0x0F, 0x08, 0x04, 0x04, 0x14, 0x09, 0x15, 0x08, 0x06, 0x07, 0x09, 0x13, - 0x05, 0x03, 0x0C, 0x0A, 0x05, 0x04, 0x0D, 0x09, 0x02, 0x05, 0x13, 0x0B, 0x14, 0x0B, 0x02, 0x07, - 0x09, 0x16, 0x06, 0x02, 0x0B, 0x0B, 0x06, 0x03, 0x0C, 0x08, 0x05, 0x06, 0x09, 0x03, 0x07, 0x07, - 0x15, 0x09, 0x09, 0x03, 0x0A, 0x0A, 0x02, 0x0A, 0x06, 0x06, 0x07, 0x0A, 0x06, 0x04, 0x0A, 0x06, - 0x07, 0x06, 0x08, 0x06, 0x08, 0x06, 0x09, 0x05, 0x06, 0x0E, 0x13, 0x05, 0x09, 0x0A, 0x04, 0x08, - 0x06, 0x05, 0x09, 0x05, 0x18, 0x07, 0x06, 0x07, 0x08, 0x08, 0x06, 0x09, 0x05, 0x18, 0x11, 0x08, - 0x05, 0x07, 0x05, 0x07, 0x07, 0x08, 0x05, 0x08, 0x08, 0x04, 0x08, 0x07, 0x07, 0x05, 0x08, 0x08, - 0x15, 0x17, 0x14, 0x0B, 0x11, 0x07, 0x07, 0x05, 0x15, 0x07, 0x08, 0x02, 0x0C, 0x04, 0x07, 0x07, - 0x09, 0x05, 0x05, 0x1B, 0x08, 0x12, 0x3E, 0x09, 0x58, 0x09, 0x15, 0x0C, 0x13, 0x04, 0x3C, 0x09, - 0x20, 0x07, 0x7B, 0x16, 0x05, 0x26, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0x40, 0x04, 0x3B, 0x07, 0xCB, - 0x03, 0x07, 0x08, 0x72, 0x08, 0x19, 0x07, 0x04, 0x08, 0x03, 0x03, 0x0F, 0x03, 0x01, 0x03, 0x03, - 0x02, 0x02, 0x03, 0x02, 0x02, 0x2A, 0x02, 0x18, 0x02, 0x03, 0x02, 0x5D, 0x09, 0x01, 0x03, 0x03, - 0x02, 0x11, 0x1A, 0x20, 0x01, 0x1D, 0x03, 0x03, 0x01, 0x32, 0x38, 0x03, 0x02, 0x21, 0x02, 0x51, - 0x01, 0x0A, 0x02, 0x02, 0x02, 0x03, 0x02, 0x04, 0x02, 0x02, 0x08, 0x03, 0x02, 0x0E, 0x03, 0x01, - 0x02, 0x03, 0x02, 0x44, 0x03, 0x02, 0x03, 0x03, 0x01, 0x2F, 0x02, 0x29, 0x08, 0x2A, 0x07, 0x02, - 0x04, 0x01, 0x0A, 0x02, 0x03, 0x02, 0x03, 0x03, 0x02, 0x6C, 0x15, 0x02, 0x02, 0x02, 0x03, 0x10, - 0x01, 0x17, 0x02, 0x03, 0x01, 0x11, 0x02, 0x02, 0x03, 0x50, 0x02, 0x02, 0x02, 0x38, 0x03, 0x1A, - 0x29, 0x02, 0x03, 0x03, 0x02, 0x04, 0x01, 0x67, 0x01, 0x03, 0x02, 0x03, 0x01, 0x1A, 0x02, 0x04, - 0x01, 0x0A, 0x01, 0x04, 0x01, 0x04, 0x02, 0x1A, 0x01, 0x02, 0x03, 0x03, 0x02, 0x03, 0x02, 0x03, - 0x02, 0x27, 0x01, 0x02, 0x03, 0x03, 0x01, 0x22, 0x02, 0x03, 0x03, 0x03, 0x02, 0x39, 0x0F, 0x02, - 0x03, 0x0E, 0x03, 0x2A, 0x02, 0x03, 0x02, 0xFE, 0xFE, 0x34, 0x1B, 0xED, 0x20, 0xE7, 0x21, 0x40, - 0x59, 0x57, 0x2E, 0xE9, 0x23, 0xFE, 0x01, 0x27, 0x40, 0x5B, 0xFE, 0xFE, 0x16, 0x71, 0xFE, 0xFE, - 0xFF, 0x0F, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, - 0x4D, 0x20, 0x22, 0x19, 0x1A, 0x16, 0x1B, 0x19, 0x1B, 0x19, 0x1C, 0x13, 0x1C, 0x1D, 0x19, 0x10, - 0x2E, 0x08, 0x14, 0x2B, 0x17, 0x0E, 0x4B, 0x12, 0x09, 0x13, 0x13, 0x10, 0x0E, 0x0C, 0x14, 0x09, - 0x12, 0x10, 0x0B, 0x12, 0x13, 0x10, 0x0F, 0x0A, 0x16, 0x04, 0x16, 0x0E, 0x0D, 0x12, 0x13, 0x11, - 0x0E, 0x0E, 0x12, 0x04, 0x17, 0x0E, 0x0D, 0x11, 0x13, 0x12, 0x0F, 0x08, 0x15, 0x08, 0x15, 0x0D, - 0x0B, 0x13, 0x13, 0x12, 0x0C, 0x07, 0x17, 0x04, 0x17, 0x2C, 0x22, 0x16, 0x18, 0x05, 0x17, 0x08, - 0x04, 0x07, 0x04, 0x14, 0x31, 0x06, 0x17, 0x06, 0x17, 0x28, 0x10, 0x0B, 0x13, 0x0B, 0x18, 0x05, - 0x17, 0x26, 0x12, 0x0B, 0x16, 0x06, 0x37, 0x05, 0x06, 0x18, 0x12, 0x0C, 0x12, 0x07, 0x18, 0x04, - 0x19, 0x07, 0x07, 0x04, 0x03, 0x0F, 0x12, 0x09, 0x0A, 0x03, 0x06, 0x07, 0x15, 0x0A, 0x16, 0x13, - 0x05, 0x0E, 0x12, 0x09, 0x0A, 0x03, 0x06, 0x07, 0x16, 0x09, 0x17, 0x10, 0x06, 0x0E, 0x12, 0x09, - 0x13, 0x09, 0x15, 0x0A, 0x18, 0x10, 0x04, 0x0D, 0x12, 0x0A, 0x0A, 0x03, 0x04, 0x09, 0x15, 0x0A, - 0x15, 0x08, 0x08, 0x01, 0x05, 0x0E, 0x12, 0x0A, 0x0A, 0x02, 0x05, 0x09, 0x15, 0x07, 0x17, 0x09, - 0x10, 0x0E, 0x11, 0x0A, 0x13, 0x09, 0x14, 0x09, 0x15, 0x13, 0x07, 0x0F, 0x10, 0x0A, 0x13, 0x09, - 0x14, 0x08, 0x17, 0x12, 0x08, 0x13, 0x0C, 0x0A, 0x13, 0x09, 0x15, 0x08, 0x16, 0x14, 0x06, 0x0E, - 0x12, 0x09, 0x13, 0x09, 0x15, 0x13, 0x0B, 0x13, 0x08, 0x0D, 0x13, 0x08, 0x16, 0x07, 0x16, 0x08, - 0x16, 0x12, 0x08, 0x15, 0x0B, 0x09, 0x15, 0x08, 0x15, 0x08, 0x06, 0x03, 0x0C, 0x13, 0x09, 0x15, - 0x0C, 0x08, 0x03, 0x06, 0x0A, 0x09, 0x05, 0x04, 0x0A, 0x13, 0x15, 0x09, 0x0B, 0x14, 0x17, 0x09, - 0x18, 0x04, 0x0C, 0x03, 0x06, 0x06, 0x15, 0x08, 0x10, 0x12, 0x13, 0x0E, 0x17, 0x05, 0x12, 0x08, - 0x14, 0x08, 0x04, 0x04, 0x09, 0x12, 0x0F, 0x12, 0x0F, 0x0D, 0x13, 0x07, 0x14, 0x11, 0x0A, 0x13, - 0x10, 0x10, 0x15, 0x09, 0x2F, 0x0D, 0x10, 0x13, 0x0F, 0x10, 0x16, 0x09, 0x2D, 0x0F, 0x10, 0x14, - 0x0F, 0x0F, 0x16, 0x08, 0x17, 0x04, 0x14, 0x0D, 0x11, 0x15, 0x11, 0x0E, 0x12, 0x0C, 0x26, 0x14, - 0x0F, 0x1E, 0x19, 0x06, 0x18, 0x06, 0x07, 0x02, 0x18, 0x06, 0x08, 0x05, 0x09, 0x13, 0x04, 0x08, - 0x03, 0x06, 0x17, 0x08, 0x05, 0x03, 0x0F, 0x03, 0x31, 0x05, 0x07, 0x14, 0x0B, 0x13, 0x16, 0x11, - 0x14, 0x0B, 0x2E, 0x08, 0x15, 0x12, 0x02, 0x05, 0x17, 0x0A, 0x05, 0x01, 0x19, 0x07, 0x3C, 0x13, - 0x04, 0x16, 0x16, 0x08, 0x1B, 0x12, 0x21, 0x11, 0x14, 0x16, 0x02, 0x0E, 0x2F, 0x04, 0x06, 0x0A, - 0x10, 0x14, 0x18, 0x07, 0x09, 0x14, 0x0B, 0x05, 0x02, 0x06, 0x26, 0x02, 0x2E, 0x12, 0x2B, 0x11, - 0x14, 0x07, 0x03, 0x05, 0x30, 0x02, 0x06, 0x04, 0x18, 0x1F, 0x58, 0x06, 0x02, 0x05, 0x2F, 0x04, - 0x04, 0x06, 0x09, 0x02, 0x04, 0x31, 0x22, 0x3A, 0xC5, 0x16, 0x3F, 0x03, 0x0A, 0x02, 0x51, 0x12, - 0x12, 0x10, 0xFE, 0xD0, 0x35, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, - 0xFE, 0xFE, 0x63, 0x02, 0x04, 0x02, 0x03, 0x03, 0x81, 0x02, 0x03, 0x02, 0x47, 0x09, 0x0A, 0x02, - 0x02, 0x03, 0x03, 0x02, 0x2B, 0x01, 0x04, 0x02, 0x04, 0x02, 0x04, 0x01, 0x38, 0x02, 0x80, 0x03, - 0x03, 0x02, 0x63, 0x01, 0x03, 0x02, 0x03, 0x02, 0x56, 0x02, 0x03, 0x02, 0x03, 0x03, 0x19, 0x02, - 0x02, 0x03, 0x02, 0x03, 0x02, 0x03, 0x02, 0x03, 0x03, 0x01, 0x60, 0x2C, 0x01, 0x03, 0x3E, 0x01, - 0x04, 0x01, 0x32, 0x02, 0x02, 0x03, 0x02, 0x17, 0x01, 0x03, 0x3D, 0x02, 0x03, 0x03, 0x03, 0x02, - 0x0A, 0x02, 0x5B, 0x02, 0x1F, 0x03, 0x02, 0x03, 0x03, 0x02, 0x03, 0x02, 0x03, 0x02, 0x04, 0x01, - 0x09, 0x02, 0x03, 0x02, 0x02, 0x03, 0x02, 0x02, 0x04, 0x01, 0x03, 0x02, 0x03, 0x02, 0x04, 0x01, - 0x03, 0x02, 0x03, 0x02, 0x03, 0x02, 0x03, 0x02, 0x2C, 0x02, 0x02, 0x03, 0x03, 0x02, 0x03, 0x03, - 0x04, 0x01, 0x23, 0x02, 0x03, 0x02, 0x25, 0x03, 0x02, 0x02, 0x03, 0x02, 0x1B, 0x02, 0x03, 0x02, - 0x04, 0x01, 0x37, 0x02, 0x04, 0x02, 0x12, 0x02, 0x02, 0x03, 0x03, 0x02, 0x03, 0x02, 0x03, 0x02, - 0x10, 0x02, 0x04, 0x01, 0x0D, 0x01, 0x04, 0x01, 0x04, 0x01, 0x25, 0x02, 0x03, 0x02, 0x03, 0x02, - 0x04, 0x02, 0x0A, 0x02, 0x0B, 0x02, 0x04, 0x02, 0x04, 0x03, 0x02, 0x03, 0x03, 0x0E, 0x3E, 0x02, - 0x04, 0x18, 0x01, 0x03, 0x03, 0x02, 0x04, 0x0A, 0x87, 0x11, 0x03, 0x02, 0x03, 0x02, 0x52, 0x02, - 0x04, 0x02, 0x12, 0x02, 0x30, 0x02, 0x03, 0x02, 0x04, 0x02, 0x04, 0x01, 0x04, 0x02, 0x02, 0x03, - 0x02, 0x02, 0x02, 0x03, 0x03, 0x01, 0x27, 0x02, 0xA4, 0x02, 0x03, 0x02, 0x18, 0x02, 0x03, 0x02, - 0x24, 0x16, 0x01, 0x1B, 0xFF, 0x01, 0x0F, 0x1A, 0x0D, 0x0B, 0x20, 0x0E, 0x07, 0x0C, 0x0A, 0x0B, - 0x22, 0x0A, 0x0D, 0x09, 0x0A, 0x0B, 0x1E, 0x10, 0x04, 0x0D, 0x10, 0x0C, 0x1E, 0x0A, 0x0B, 0x08, - 0x0C, 0x0A, 0x10, 0x07, 0x09, 0x0C, 0x04, 0x0E, 0x0C, 0x0C, 0x1F, 0x0E, 0x1D, 0x08, 0x11, 0x1A, - 0x03, 0x0D, 0x0C, 0x0B, 0x21, 0x0C, 0x10, 0x13, 0x14, 0x12, 0x0A, 0x0D, 0x0B, 0x0B, 0x0E, 0x03, - 0x10, 0x0C, 0x1E, 0x09, 0x11, 0x13, 0x08, 0x0B, 0x0C, 0x0C, 0x0C, 0x04, 0x10, 0x0E, 0x3E, 0x1F, - 0x0A, 0x0D, 0x0F, 0x02, 0x0B, 0x0F, 0x0E, 0x0C, 0x20, 0x04, 0x03, 0x1E, 0x0B, 0x0D, 0x1C, 0x0E, - 0x0E, 0x0C, 0x1E, 0x17, 0x02, 0x0C, 0x0B, 0x0D, 0x1B, 0x11, 0x1D, 0x06, 0x20, 0x0F, 0x02, 0x0B, - 0x0B, 0x0E, 0x0C, 0x04, 0x0F, 0x0B, 0x10, 0x12, 0x1E, 0x20, 0x0B, 0x0B, 0x0E, 0x04, 0x09, 0x03, - 0x02, 0x0A, 0x0B, 0x0D, 0x04, 0x08, 0x1D, 0x10, 0x05, 0x0D, 0x0B, 0x0C, 0x0D, 0x04, 0x0F, 0x0B, - 0x0D, 0x0A, 0x0B, 0x08, 0x0D, 0x0E, 0x11, 0x12, 0x0B, 0x0B, 0x0E, 0x03, 0x0A, 0x0E, 0x0C, 0x0C, - 0x0B, 0x0A, 0x09, 0x0B, 0x0D, 0x1D, 0x0B, 0x0C, 0x0E, 0x04, 0x0A, 0x0D, 0x0C, 0x0D, 0x0B, 0x09, - 0x0B, 0x07, 0x0E, 0x0E, 0x03, 0x0B, 0x03, 0x02, 0x06, 0x01, 0x02, 0x0B, 0x0F, 0x03, 0x0B, 0x0B, - 0x0D, 0x0E, 0x0C, 0x07, 0x09, 0x0A, 0x0C, 0x0A, 0x0B, 0x0C, 0x04, 0x01, 0x05, 0x10, 0x10, 0x02, - 0x0B, 0x03, 0x02, 0x04, 0x0F, 0x09, 0x11, 0x09, 0x03, 0x0D, 0x0B, 0x0C, 0x0A, 0x12, 0x01, 0x03, - 0x0B, 0x0B, 0x1D, 0x02, 0x02, 0x09, 0x0F, 0x0C, 0x13, 0x12, 0x0F, 0x0F, 0x0E, 0x0D, 0x0B, 0x03, - 0x02, 0x05, 0x22, 0x09, 0x10, 0x0A, 0x02, 0x04, 0x10, 0x0E, 0x0D, 0x1B, 0x0A, 0x0C, 0x0C, 0x0B, - 0x0E, 0x03, 0x0F, 0x0C, 0x0D, 0x0B, 0x05, 0x0B, 0x0A, 0x0C, 0x0D, 0x0F, 0x09, 0x03, 0x0B, 0x0C, - 0x0C, 0x0C, 0x0C, 0x04, 0x0F, 0x0C, 0x0C, 0x0D, 0x06, 0x0C, 0x0D, 0x0A, 0x0B, 0x08, 0x0E, 0x09, - 0x0D, 0x0D, 0x0B, 0x0E, 0x1E, 0x0C, 0x0E, 0x1B, 0x0D, 0x0C, 0x0C, 0x0E, 0x1D, 0x09, 0x0B, 0x0A, - 0x0C, 0x0C, 0x0A, 0x0A, 0x0E, 0x0B, 0x0B, 0x09, 0x0C, 0x0A, 0x0E, 0x0F, 0x07, 0x0C, 0x2F, 0x0A, - 0x10, 0x09, 0x15, 0x0B, 0x19, 0x0E, 0x13, 0x10, 0x11, 0x11, 0x11, 0x14, 0x31, 0x1F, 0x54, 0x11, - 0x5B, 0x0F, 0x2F, 0x0A, 0x12, 0x34, 0x9E, 0x0C, 0x12, 0x18, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, - 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, - 0xFE, 0xFE, 0xFE, 0xFE, 0xFF, -}; diff --git a/examples/spacetaxi/stSpeechData.h b/examples/spacetaxi/stSpeechData.h deleted file mode 100644 index b34e046..0000000 --- a/examples/spacetaxi/stSpeechData.h +++ /dev/null @@ -1,26 +0,0 @@ -// Generated by assets/extractSpeech.py. Do not hand-edit. -// -// Space Taxi's digitised speech as the C64 stores it: 1-bit -// run-length streams. Each byte is one half-cycle length; the -// player toggles its output after each. $FE is a raster sync -// and $FF ends an utterance. stAudio.c expands these to PCM -// through the jlAudioPlaySfxStream fill callback. - -#ifndef ST_SPEECH_DATA_H -#define ST_SPEECH_DATA_H - -#include - -typedef struct { - uint8_t code; // PETSCII: 'H' 'T' '!' 'P' '1'..'9' '?' 'U' - uint16_t offset; // into kStSpeechRle - uint16_t length; -} StSpeechEntryT; - -#define ST_SPEECH_COUNT 15u -#define ST_SPEECH_RLE_BYTES 7445u - -extern const StSpeechEntryT kStSpeechIndex[ST_SPEECH_COUNT]; -extern const uint8_t kStSpeechRle[ST_SPEECH_RLE_BYTES]; - -#endif diff --git a/examples/spacetaxi/stTitle.c b/examples/spacetaxi/stTitle.c deleted file mode 100644 index 20beee3..0000000 --- a/examples/spacetaxi/stTitle.c +++ /dev/null @@ -1,354 +0,0 @@ -// Space Taxi -- the title screen intro and the level-name intro -// screen, run on the same simulation state as the game so everything -// that carries over (flame parity, wave index, walk parity, the -// sprite shadows) behaves as on the C64. -// -// Title ($477A + $47B0..$4A6C): the logo flip-book and colour cycle, -// a passenger who beams in, walks to the parked cab and boards, then -// the cab lifts off under thrust. When it has left, the attract demo -// starts. -// -// Level intro ($4523..$4727): a black screen with the level name, the -// cab rising from the bottom and seven star sprites streaming out of -// the centre, until the cab reaches mid-screen. -// -// The seven star sprites are replaced by a static starfield whose -// colours are cycled -- see ST_STAR_RING_FIRST in stSim.h for why and -// how. The stars sit on the seven rays the C64's stars fly, where those -// stars are every few frames of a flight, so the field plays the -// original's paths. The cab still flies, and it is still what ends the -// intro, so the sequence keeps its length and its cue. - -#include - -#include "stSim.h" - -// Title sprite init tables ($4994 / $499C / $49A4 / $49AC / $49B4). -static const uint8_t kTitleCol[8] = { 0x28, 0x32, 0x00, 0x86, 0x9C, 0xA2, 0xBA, 0xD4 }; -static const uint8_t kTitleMsb[8] = { 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; -static const uint8_t kTitleColor[8] = { 0x06, 0x06, 0x06, 0x07, 0x07, 0x07, 0x07, 0x07 }; -static const uint8_t kTitleRow[8] = { 0x84, 0x84, 0x00, 0xE2, 0xE2, 0xE2, 0xE2, 0xE2 }; -static const uint8_t kTitlePtr[8] = { 0xC1, 0xC7, 0x00, 0xDB, 0xDE, 0xDF, 0xE0, 0xE1 }; -// Logo flip-book frame order ($48A7) and colour cycle ($489C). -static const uint8_t kLogoFlip[6] = { 1u, 2u, 3u, 4u, 3u, 2u }; -static const uint8_t kLogoColor[8] = { 0x02, 0x08, 0x07, 0x05, 0x06, 0x0E, 0x03, 0x04 }; -// Intro cab drift per loop count ($470D). -static const int8_t kIntroCabDy[4] = { -2, 1, -1, 1 }; -// Intro texts ($4624 / $463F). -static const uint8_t kTextDemoExit[] = "DEMO, USE JOYSTICK TO EXIT"; -static const uint8_t kTextScreens[] = "THIS IS 1 OF 25 DIFFERENT SCREENS!"; - -#define ST_TITLE_CAB_LIFT_ROW 0x14u -#define ST_TITLE_SPARKLE_TICKS 0x5Au -#define ST_TITLE_HOVER_COL 0x28u -#define ST_INTRO_STAR_PTR 0xDAu -#define ST_INTRO_STAR_COL 0xAAu // where a star launches ($4523) -#define ST_INTRO_STAR_ROW 0x8Cu -#define ST_INTRO_STAR_DELAYS 0x20u // launch delays drawn from 0..$1F ($44F8) -#define ST_INTRO_CAB_END_ROW 0x94u -#define ST_INTRO_CAB_DX_RELOAD 0x28u - -// The palette-cycled starfield. The C64 flies one star sprite along each -// of seven rays at these velocities ($450C / $4513, pixels a frame) for -// $20 frames ($4500), then launches it again from the start. The painted -// field puts a star where each flight is every ST_STAR_FRAMES_PER_STEP -// frames; the renderer lights one step of every ray at a time, so a star -// hops out along its ray. A star is drawn where the sprite's image ($DA: -// a 5x3 speck centred on pixel 11, 9 of the sprite) would be. -#define ST_INTRO_STARS 7u -#define ST_INTRO_STAR_IMAGE_X 11 // the speck's centre in its sprite -#define ST_INTRO_STAR_IMAGE_Y 9 -#define ST_SCREEN_WIDTH_PX ((int16_t)(ST_SCREEN_COLS * 8u)) -#define ST_SCREEN_HEIGHT_PX ((int16_t)(ST_SCREEN_ROWS * 8u)) -#define ST_CELL_SHIFT 3 // 8 pixels a cell - -static const int8_t kIntroStarDx[ST_INTRO_STARS] = { 0, 6, 6, 6, -6, -6, -6 }; -static const int8_t kIntroStarDy[ST_INTRO_STARS] = { -4, -4, 0, 4, 4, 0, -4 }; - - -static void addToRow(StSimT *sim, uint8_t idx, uint8_t delta); -static void logoColorCycle(StSimT *sim); -static void logoFlip(StSimT *sim); -static void paintStarfield(StSimT *sim, const uint8_t *delay); -static bool rowHasText(const StSimT *sim, uint8_t row); - - -// $4113 -static void addToRow(StSimT *sim, uint8_t idx, uint8_t delta) { - sim->spr[idx].row = (uint8_t)(sim->spr[idx].row + delta); -} - - -// $4861 -- the next logo colour, onto sprites 3..7 and onto the logo -// itself. The C64 writes the colour into all 407 cells of rows 1..11, -// columns 1..37; only the cells showing the logo character take any -// colour from it (the rest are blank), so the colour is published in -// logoColor instead and the renderer applies it to the logo's palette -// slot. Nothing reads the band's colour RAM. -static void logoColorCycle(StSimT *sim) { - uint8_t color; - uint8_t k; - - sim->logoColorIdx = (uint8_t)((sim->logoColorIdx + 1u) & 7u); - color = kLogoColor[sim->logoColorIdx]; - sim->logoColor = color; - for (k = 3u; k < ST_HW_SPRITES; k++) { - sim->spr[k].color = color; - } -} - - -// $4827 -- every fourth tick copy the next flip frame ($85..$88) over -// the logo glyph; the colour advances when the ping-pong hits 4. -static void logoFlip(StSimT *sim) { - sim->logoDiv = (uint8_t)((sim->logoDiv + 1u) & 3u); - if (sim->logoDiv != 0u) { - return; - } - memcpy(stSimGlyphMut(sim, ST_LOGO_CHAR), - stSimGlyph(sim, (uint8_t)(ST_LOGO_CHAR + kLogoFlip[sim->logoCycleAux])), 8u); - stSimMarkChar(sim, ST_LOGO_CHAR); - sim->logoCycleAux++; - if (sim->logoCycleAux == 6u) { - sim->logoCycleAux = 0u; - return; - } - if (sim->logoCycleAux == 4u) { - logoColorCycle(sim); - } -} - - -// Paint the level-intro starfield into screen RAM, once. A star's cell is -// coloured ST_STAR_RING_FIRST + the step of its flight it marks, offset by -// its ray's launch delay so the rays fire out of step as on the C64, and is -// never touched again -- the renderer animates the warp by cycling those -// colours, which costs no dirty rows at all (ST_STAR_RING_FIRST, stSim.h). -// Step 0 is the launch point, which every ray shares, and steps off the -// screen are left out, as are rows that carry the intro's text: on the C64 -// the stars fly over the text, but a cell star there reads as part of it -- -// a gap in the level name, or a full stop after the demo hint. -static void paintStarfield(StSimT *sim, const uint8_t *delay) { - static const uint8_t one[2] = { ST_STAR_GLYPH, 0u }; // stSimDrawText stops below screen code 6 - bool textRow[ST_SCREEN_ROWS]; - int16_t originX; - int16_t originY; - int16_t x; - int16_t y; - uint8_t row; - uint8_t ray; - uint8_t step; - uint8_t slot; - - // Before any star goes down, or the first star on a row would hide the - // rest of that row's. - for (row = 0u; row < ST_SCREEN_ROWS; row++) { - textRow[row] = rowHasText(sim, row); - } - originX = (int16_t)(ST_INTRO_STAR_COL - ST_SPRITE_X_ORIGIN + ST_INTRO_STAR_IMAGE_X); - originY = (int16_t)(ST_INTRO_STAR_ROW - ST_SPRITE_Y_ORIGIN + ST_INTRO_STAR_IMAGE_Y); - for (ray = 0u; ray < ST_INTRO_STARS; ray++) { - for (step = 1u; step < ST_STAR_RING_COUNT; step++) { - x = (int16_t)(originX + kIntroStarDx[ray] * (int16_t)(step * ST_STAR_FRAMES_PER_STEP)); - y = (int16_t)(originY + kIntroStarDy[ray] * (int16_t)(step * ST_STAR_FRAMES_PER_STEP)); - if (x < 0 || x >= ST_SCREEN_WIDTH_PX || y < 0 || y >= ST_SCREEN_HEIGHT_PX || textRow[y >> ST_CELL_SHIFT]) { - continue; - } - slot = (uint8_t)((step + delay[ray] / ST_STAR_FRAMES_PER_STEP) & (ST_STAR_RING_COUNT - 1u)); - stSimDrawText(sim, (uint8_t)(x >> ST_CELL_SHIFT), (uint8_t)(y >> ST_CELL_SHIFT), one, (uint8_t)(ST_STAR_RING_FIRST + slot)); - } - } -} - - -// True when screen row `row` holds anything but blanks. -static bool rowHasText(const StSimT *sim, uint8_t row) { - const uint8_t *cell; - uint8_t col; - - cell = &sim->screen[(uint16_t)row * ST_SCREEN_COLS]; - for (col = 0u; col < ST_SCREEN_COLS; col++) { - if (cell[col] != ST_CHAR_SPACE) { - return true; - } - } - return false; -} - - -// --------------------------------------------------------------------------- -// Public -// --------------------------------------------------------------------------- - -// $4523..$4665 -- set up the level-name screen. -void stIntroEnter(StSimT *sim, const StLevelT *level) { - uint8_t delay[ST_INTRO_STARS]; - uint8_t k; - - sim->level = level; - sim->spr[7].ptr = 0xC0u; - sim->spriteMc1 = 0x07u; - sim->spriteMc0 = 0x02u; - sim->spr[7].enable = 1u; - sim->spr[7].col = 0xAAu; - sim->spr[7].msb = 0u; - sim->spr[7].color = 0x06u; - sim->spr[7].row = 0xE4u; - sim->introCabDxTimer = 0x14u; - sim->introCabDx = 0x01u; - for (k = 0u; k < ST_INTRO_STARS; k++) { - uint8_t idx = (uint8_t)(ST_INTRO_STARS - 1u - k); - sim->spr[idx].row = ST_INTRO_STAR_ROW; - sim->spr[idx].msb = 0u; - sim->spr[idx].col = ST_INTRO_STAR_COL; - sim->spr[idx].color = 0x07u; - sim->spr[idx].ptr = ST_INTRO_STAR_PTR; - sim->spr[idx].enable = 0u; - // $44F8: each star's launch delay. The painted field uses it to - // put the rays out of step, and the seven RNG draws MUST stay in - // any case: they advance the shared RNG and the level that - // follows draws from it. - delay[idx] = stSimRng(sim, ST_INTRO_STAR_DELAYS); - } - sim->borderColor = 0u; - sim->bgColor = 0u; - sim->multiColorMask = 0x80u; - stSimMarshal(sim); - // $40CA: blank screen. - memset(sim->screen, ST_CHAR_SPACE, ST_SCREEN_CELLS); - memset(sim->color, 0, ST_SCREEN_CELLS); - stSimDirtyAll(sim); - stSimGlyphReset(sim); - sim->charDirtyAll = true; - sim->charDirtyCount = 0u; - stSimDrawText(sim, 10u, 12u, level->name, 3u); - if (sim->demoMode != 0u && sim->postMortem == 0u) { - stSimDrawText(sim, 7u, 3u, kTextDemoExit, 5u); - stSimDrawText(sim, 3u, 24u, kTextScreens, 4u); - } - paintStarfield(sim, delay); - sim->introLoopCount = 0u; - sim->introStepCount = 0u; -} - - -// $4666 -- one frame of the star field; true when the cab has risen -// to mid-screen and everything is hidden again. -bool stIntroStep(StSimT *sim) { - uint8_t k; - - stSimMarshal(sim); - sim->introStepCount++; - sim->introLoopCount = (uint8_t)((sim->introLoopCount + 1u) & 3u); - addToRow(sim, 7u, (uint8_t)kIntroCabDy[sim->introLoopCount]); - if ((sim->introLoopCount & 1u) == 0u) { - stSimSpriteAddX(sim, 7u, sim->introCabDx); - sim->introCabDxTimer--; - if (sim->introCabDxTimer == 0u) { - sim->introCabDxTimer = ST_INTRO_CAB_DX_RELOAD; - sim->spr[7].ptr ^= 0x1Cu; - sim->introCabDx ^= 0xFEu; - } - } - if (sim->spr[7].row != ST_INTRO_CAB_END_ROW) { - return false; - } - // $4728: everything off; $44E1 then resets the SID and the RNG. - for (k = 0u; k < ST_HW_SPRITES; k++) { - sim->spr[k].enable = 0u; - } - stSimMarshal(sim); - sim->rngT1 = 0x06u; - sim->rngT2 = 0x17u; - return true; -} - - -// $477A + $47B0..$47E1 -- show the title and start the intro. -void stTitleEnter(StSimT *sim, const StLevelT *title) { - uint8_t k; - - sim->level = title; - memcpy(sim->screen, title->screen, ST_SCREEN_CELLS); - memcpy(sim->color, title->color, ST_SCREEN_CELLS); - stSimDirtyAll(sim); - stSimGlyphReset(sim); - sim->charDirtyAll = true; - sim->charDirtyCount = 0u; - logoColorCycle(sim); - for (k = 0u; k < ST_HW_SPRITES; k++) { - sim->spr[k].enable = 0u; - } - sim->logoCycleAux = 0u; - sim->borderColor = 0u; - sim->bgColor = 0u; - // $49BC: the cab parked on the pad, the passenger far right, the - // decoration sprites along the bottom. - for (k = 0u; k < ST_HW_SPRITES; k++) { - sim->spr[k].enable = 1u; - sim->spr[k].col = kTitleCol[k]; - sim->spr[k].msb = kTitleMsb[k]; - sim->spr[k].row = kTitleRow[k]; - sim->spr[k].ptr = kTitlePtr[k]; - sim->spr[k].color = kTitleColor[k]; - } - sim->multiColorMask = 0x07u; - sim->spriteMc1 = 0x07u; - sim->spriteMc0 = 0x02u; - stSimMarshal(sim); - sim->stage = ST_STAGE_WAIT; - sim->introFrameCount = ST_TITLE_SPARKLE_TICKS; - sim->hoverXFrac = 0u; - sim->hoverXCol = ST_TITLE_HOVER_COL; - sim->decayTimer = sim->decayReload; - sim->collisionPhase = 0u; - sim->activePad = 0u; - sim->dirMask = 0u; -} - - -// $47E4 loop body: the intro stage machine ($4A03), the sprite flush -// and the logo animation. True once the cab has lifted off. -bool stTitleTick(StSimT *sim) { - switch (sim->stage) { - case ST_STAGE_WAIT: - // $4A17: sparkle while the countdown runs. - stFareWaveStep(sim); - sim->introFrameCount--; - if (sim->introFrameCount == 0u) { - sim->stage++; - } - break; - case ST_STAGE_WALK_TO_CAB: - // $4A24: walk in until parked at column $28. - sim->decayTimer--; - if (sim->decayTimer == 0u) { - sim->decayTimer = sim->decayReload; - stFareWalkStep(sim); - if (sim->spr[1].col == ST_TITLE_HOVER_COL && sim->spr[1].msb == 0u) { - sim->stage++; - } - } - break; - case ST_STAGE_BOARD: - // $4A45 -> $67A6: shrink into the cab. - stFareLeaveStep(sim); - break; - case ST_STAGE_RIDING: - // $4A48: lift off under UP thrust until above row $14. - addToRow(sim, 0u, 0xFEu); - sim->dirMask = 0x01u; - sim->spr[0].ptr = 0xC0u; - stSimFlameUpdate(sim); - if (sim->spr[0].row < ST_TITLE_CAB_LIFT_ROW) { - sim->stage++; - stAudioNoise(false); - } - break; - default: - break; - } - stSimMarshal(sim); - logoFlip(sim); - return sim->stage == ST_STAGE_DROP_IN; -} diff --git a/install.sh b/install.sh index 03c1e65..75bffe0 100755 --- a/install.sh +++ b/install.sh @@ -24,8 +24,8 @@ # Targets are component names (see `list`) or groups: # toolchain:PORT emulator:PORT PORT toolchains emulators all # where PORT is iigs, amiga, atarist, dos, x68000 or dvx. `all`, `toolchains` -# and `emulators` leave out optional components (staxiData, and dvx, which -# unpacks a DVX tree's dist/ zips you have); name those explicitly. +# and `emulators` leave out optional components (dvx, which unpacks a DVX +# tree's dist/ zips you have); name those explicitly. # # Options: # -y, --yes install missing host packages without asking diff --git a/installer/components/staxiData.sh b/installer/components/staxiData.sh deleted file mode 100644 index a80ee0c..0000000 --- a/installer/components/staxiData.sh +++ /dev/null @@ -1,30 +0,0 @@ -# Space Taxi's level source: a memory image of the original Commodore 64 -# game. Optional -- without it every port still builds, skipping only the -# Space Taxi data -- so it is not part of `all`; install it by name. - -compDescription="Space Taxi C64 memory image (private, optional: bundle or import)" -compGroups="staxi optional" -compRequires="" -compHostDeps="" -compPrivateFile="stuff/spacetaxi/raw.bin" -compPin="$(privateSha1For "$compPrivateFile")" - - -compInstall() { - privateEnsure "$compPrivateFile" -} - - -compInstalled() { - privatePresent "$compPrivateFile" -} - - -compVerify() { - privatePresent "$compPrivateFile" -} - - -compEnv() { - : -} diff --git a/installer/files.sha1 b/installer/files.sha1 index 1dab3fc..5a18873 100644 --- a/installer/files.sha1 +++ b/installer/files.sha1 @@ -7,4 +7,3 @@ 3b7f1493b27e212830f989f26ca76c02049f09ca toolchains/firmware/amiga/kickstart.rom Amiga Kickstart 3.1 r40.63 (A500/A600/A2000) d814d6ce8efb4a87966dae581eddf4dca528094a toolchains/firmware/amiga/Devs/serial.device Amiga serial.device 37.4 (Workbench 3.1 Devs/) 8d72c5b4d63bb14c5dbdac495244d659aa1498b6 toolchains/firmware/x68000/roms/x68000/cgrom.dat X68000 character-generator (font) ROM -5bfa4ba70431135575dab610a73dde02b8095953 stuff/spacetaxi/raw.bin Space Taxi C64 memory image (level source) diff --git a/joey-run b/joey-run index 9ed37b0..0e7c832 100755 --- a/joey-run +++ b/joey-run @@ -9,24 +9,29 @@ # a program file build/dos/bin/DRAW.EXE, build/atarist/bin/DRAW.PRG, # build/amiga/bin/Draw, build/x68000/bin/DRAW.X, # build/iigs/bin/DRAW, build/dvx/bin/apps/joeylib/draw/draw.app -# an app folder build/amiga/bin/Taxi (the program plus its DATA), -# build/dvx/bin/apps/joeylib/staxi -# an IIgs disk image build/iigs/bin/STAXI.2mg (make iigs-game-disk), or +# an app folder a program in a folder of its own with its DATA: +# build/dvx/bin/apps/joeylib/audio +# an IIgs disk image build/iigs/bin/DRAW.2mg (make iigs-game-disk), or # a data disk such as joey.2mg (boots GS/OS with it) -# an example name draw, staxi, uber, ... together with --port -# The machine is recognised from the file itself. +# an example name draw, audio, uber, ... together with --port +# The machine is recognised from the file itself. A program built outside +# JoeyLib (make/project.mk) runs the same way: name its file, folder or disk +# image in the project's build/, or give its name with --project and --port. # # Options: # --port PORT iigs, amiga, atarist, dos, x68000 or dvx +# --project DIR look a program name up in the project at DIR (its +# build/PORT/bin) instead of JoeyLib's examples; DVX runs +# keep their DVX in DIR's build/dvx/run # --data DIR copy DIR's files into the program's DATA folder for -# this run (for example an AGI game's files) +# this run (for example the game an interpreter plays) # --headless no window and no sound, for scripts and CI; stops # after 60 seconds unless --seconds says otherwise # --seconds N stop after N seconds # --snapshot FILE save a PNG of the screen when the run stops # (needs --seconds or --headless) # --machine rom01 IIgs: the ROM 01 machine instead of ROM 03 -# --ram SIZE IIgs: memory size for MAME (default 4M; AGI wants 8M) +# --ram SIZE IIgs: memory size for MAME (default 4M) # --debug IIgs and X68000: start MAME's debugger # --serial HOST:PORT connect the machine's serial port to the TCP server # at HOST:PORT: a BBS, a program of your own, another @@ -53,7 +58,8 @@ # 1024x768 desktop (640x480 with --headless). Game > Full Screen, or Alt # with the keypad's Enter (DOSBox keeps Alt+Enter for its own full screen), # puts it on the whole screen. Interactive runs keep that DVX in -# build/dvx/run, so what the program saves (in DVX's CONFIG folder), and +# build/dvx/run (the project's with --project), so what the program saves +# (in DVX's CONFIG folder), and # whether it was left on the whole screen, is still there next time. The link is raw TCP, without telnet negotiation. # socat relays it where the emulator cannot make it itself: the ST, the # Amiga with --serial, and the IIgs and X68000 with --serial-listen. When @@ -75,6 +81,7 @@ repo="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$repo/scripts/lib/joeyEnv.sh" port="" +projectRoot="$repo" dataDir="" headless=0 seconds="" @@ -391,7 +398,7 @@ runIigs() { local media=() joeyNeed MAME mame joeyNeed IIGS_ROMPATH iigsFirmware - [ -z "$dataDir" ] || fail "--data is not supported on the IIgs; build a game disk (make -f make/iigs.mk iigs-game-disk-agi for AGI)" + [ -z "$dataDir" ] || fail "--data is not supported on the IIgs; build a disk with the data on it (a game disk)" case "$program" in *.2mg|*.2MG|*.po|*.PO|*.hdv|*.HDV) joeyNeed CADIUS cadius @@ -576,10 +583,14 @@ runDvx() { joeyNeed DOSBOX dosbox joeyNeed DVX_ROOT dvx app="$(dirname "$program")" + # Built as apps///: install it under the same vendor. + if [ "$(basename "$(dirname "$(dirname "$app")")")" = apps ]; then + DVX_VENDOR="$(basename "$(dirname "$app")")" + fi if [ "$headless" = "1" ] || [ -n "$dataDir" ]; then dir="$work/dvx" else - dir="$repo/build/dvx/run" + dir="$projectRoot/build/dvx/run" # DVX's own desktop size, where a program's window is the stage at 3x. DVX_VIDEO_W=1024 DVX_VIDEO_H=768 @@ -648,6 +659,11 @@ runX68000() { while [ $# -gt 0 ]; do case "$1" in --port) port="${2:?--port needs iigs, amiga, atarist, dos, x68000 or dvx}"; shift ;; + --project) + [ -d "${2:-}" ] || fail "--project needs a project folder (one built on make/project.mk)" + projectRoot="$(cd "$2" && pwd)" + shift + ;; --data) dataDir="${2:?--data needs a folder}"; shift ;; --headless) headless=1 ;; --seconds) seconds="${2:?--seconds needs a number}"; shift ;; @@ -713,9 +729,9 @@ fi if [ -d "$program" ]; then program="$(folderProgram "$program")" elif [ ! -e "$program" ]; then - [ -n "$port" ] || fail "$program is not a file; for an example name add --port" - program="$(joeyExampleProgram "$program" "$port")" || exit 1 - [ -e "$program" ] || fail "$program is not built (make $port)" + [ -n "$port" ] || fail "$program is not a file; for a program name add --port" + program="$(joeyExampleProgram "$program" "$port" "$projectRoot")" || exit 1 + [ -e "$program" ] || fail "$program is not built (make $port in $projectRoot)" fi program="$(cd "$(dirname "$program")" && pwd)/$(basename "$program")" detected="$(magicPort "$program")" diff --git a/make/amiga.mk b/make/amiga.mk index 082230e..bdcd639 100644 --- a/make/amiga.mk +++ b/make/amiga.mk @@ -1,16 +1,15 @@ -# Amiga (Bebbo m68k-amigaos-gcc) build rules. +# Amiga (Bebbo m68k-amigaos-gcc) build rules: JoeyLib's library for the +# Amiga, and programRules, the Amiga's rules for one program, which +# make/programs.mk instantiates for every program (a project reaches both +# through make/project.mk). JoeyLib's own examples are at the end. +PLATFORM := amiga include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk $(call requireTool,$(AMIGA_CC),toolchain:amiga) $(call requireTool,$(AMIGA_AS),toolchain:amiga) $(call requireTool,$(AMIGA_PTPLAYER)/ptplayer.asm,toolchain:amiga) -PLATFORM := amiga -BUILD := $(REPO_DIR)/build/$(PLATFORM) -LIBDIR := $(BUILD)/lib -BINDIR := $(BUILD)/bin - # PTPlayer is installed by ./install.sh (AMIGA_PTPLAYER); we # reference its ptplayer.asm + header from there rather than copying # them into the source tree, so the installer can refresh / version it @@ -64,103 +63,6 @@ LIB_OBJS := \ LIB := $(LIBDIR)/libjoey.a -HELLO_SRC := $(EXAMPLES)/hello/hello.c -HELLO_BIN := $(BINDIR)/Hello -PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c -PATTERN_BIN := $(BINDIR)/Pattern -DRAW_SRC := $(EXAMPLES)/draw/draw.c -DRAW_BIN := $(BINDIR)/Draw -KEYS_SRC := $(EXAMPLES)/keys/keys.c -KEYS_BIN := $(BINDIR)/Keys -SERIAL_SRC := $(EXAMPLES)/serial/serial.c -SERIAL_BIN := $(BINDIR)/Serial -SERTEST_SRC := $(EXAMPLES)/sertest/sertest.c -SERTEST_BIN := $(BINDIR)/SerTest -SAVE_SRC := $(EXAMPLES)/save/save.c -SAVE_BIN := $(BINDIR)/Save -TILEBANK_SRC := $(EXAMPLES)/tilebank/tilebank.c -TILEBANK_BIN := $(BINDIR)/TileBank -JOY_SRC := $(EXAMPLES)/joy/joy.c -JOY_BIN := $(BINDIR)/Joy -SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c -SPRITE_BIN := $(BINDIR)/Sprite -AUDIO_SRC := $(EXAMPLES)/audio/audio.c -AUDIO_BIN := $(BINDIR)/Audio -UBER_SRC := $(EXAMPLES)/uber/uber.c -UBER_BIN := $(BINDIR)/Uber -ADV_SRC := $(EXAMPLES)/adventure/adventure.c -ADV_BIN := $(BINDIR)/Adv -ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c -ADV2_BIN := $(BINDIR)/Adv2 -AGI_SRCS := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c -AGI_BIN := $(BINDIR)/Agi -STAXI_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stLevelFile.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stSim.c $(EXAMPLES)/spacetaxi/stFare.c $(EXAMPLES)/spacetaxi/stHooks.c $(EXAMPLES)/spacetaxi/stTitle.c $(EXAMPLES)/spacetaxi/stC64Data.c $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stAudio.c $(EXAMPLES)/spacetaxi/stLevelCels.c -STAXI_INSTALL_DIR := $(BINDIR)/Taxi -STAXI_BIN := $(STAXI_INSTALL_DIR)/Taxi - -# Space Taxi asset staging: same pattern as the DOS port -- generated -# artifacts under examples/spacetaxi/generated (nukeable), the runtime -# tree in DATA/ beside the binary. Mirrors the dos.mk wiring exactly. -STAXI_GEN_DIR := $(EXAMPLES)/spacetaxi/generated/amiga -STAXI_RUN_DIR := $(STAXI_INSTALL_DIR)/DATA -# Level data is extracted from the C64 ROM (see dos.mk for the long -# explanation): 24 lettered levels A..X from scenes 0..23 plus the -# title from scene 24, via examples/spacetaxi/assets/romToLevel.py. -STAXI_ROM_LEVEL_GEN := $(foreach n,$(STAXI_LEVEL_NUMS),$(STAXI_GEN_DIR)/levels/level$(n).dat) \ - $(STAXI_GEN_DIR)/levels/title.dat -STAXI_LEVEL_GEN := $(STAXI_ROM_LEVEL_GEN) -STAXI_LEVEL_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_LEVEL_GEN)) -STAXI_ASSET_DSTS := $(STAXI_LEVEL_RUN) -# Pre-compiled sprite banks (.spc): baked from the .spr by the host spritebake -# tool for THIS target and staged in DATA/sprites, so the runtime loads -# compiled routines with no startup JIT (jlSpriteBankLoadPrecompiled). -SPRITEBAKE_BIN := $(REPO_DIR)/build/tools/spritebake-amiga -# The C64-exact cel bank, generated by staxibake (from the shared cel table -# stCels.c + C64 bitmaps) and compiled to a .spc by the host spritebake, so -# STAXI loads its cels already compiled instead of JIT-compiling them at boot. -STAXIBAKE_BIN := $(REPO_DIR)/build/tools/staxibake - -# Per-level hook-sprite banks: the cels stLevelCelList names for each -# level that has hook sprites, baked from its level file and pre-compiled -# for this target, so no cel is JIT-compiled mid-ride. Levels without -# hook sprites have no bank; the game JIT-compiles at level entry when a -# bank is missing. -STAXI_CEL_LEVELS := 07 08 09 10 12 16 17 21 -STAXILEVEL_SPR_GEN := $(foreach n,$(STAXI_CEL_LEVELS),$(STAXI_GEN_DIR)/sprites/level$(n).spr) -STAXILEVEL_SPC_GEN := $(STAXILEVEL_SPR_GEN:.spr=.spc) -STAXILEVEL_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXILEVEL_SPC_GEN)) -STAXI_ASSET_DSTS += $(STAXILEVEL_SPC_RUN) - -# Speech baked to sample bytes per passenger pitch (assets/genSpeechPcm.py): -# expanding bits at play time is what keeps "HEY TAXI" from stalling the -# ride; the C64 run-length source stays in stSpeechData.c as the input. -STAXI_SPEECH_GEN := $(STAXI_GEN_DIR)/speech.bin -STAXI_SPEECH_RUN := $(STAXI_RUN_DIR)/speech.bin -STAXI_ASSET_DSTS += $(STAXI_SPEECH_RUN) -STAXICELS_SPR_GEN := $(STAXI_GEN_DIR)/sprites/staxicels.spr -STAXICELS_SPC_GEN := $(STAXICELS_SPR_GEN:.spr=.spc) -STAXICELS_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXICELS_SPC_GEN)) -# Added HERE, not up with the other asset destinations: this variable is -# defined below that point, and the := there expanded it to nothing, so -# the compiled cel bank was never a prerequisite of the port and never -# got copied into the build tree. Every port shipped whatever .spc was -# staged the day the file was first created, and edits to stCels.c did -# not reach the game (found 2026-09-23 when a new cel made the runtime -# reject the bank as too short). -STAXI_ASSET_DSTS += $(STAXICELS_SPC_RUN) - -# Game data lives under bin/DATA/, ready to be copied into the -# scratch JOEYLIB hard-drive dir staged by joey-run. -# audio.c fopens "DATA/test.mod" etc. relative to the boot volume. -DATA_DIR := $(BINDIR)/DATA -DATA_FILES := $(DATA_DIR)/test.mod $(DATA_DIR)/test.sfx -# TileBank's DATA/tilebank: this port's bake of the tile-bank fixture, the -# other targets' bakes it must refuse, and the expected pixels (common.mk). -TILEBANK_DATA := $(DATA_DIR)/tilebank/own.tbk - -.PHONY: all amiga clean-amiga clean -all amiga: $(LIB) $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(SERIAL_BIN) $(SERTEST_BIN) $(SAVE_BIN) $(TILEBANK_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) $(DATA_FILES) $(TILEBANK_DATA) $(call staxiData,$(STAXI_ASSET_DSTS)) - $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c @mkdir -p $(dir $@) $(AMIGA_CC) $(CFLAGS) -c $< -o $@ @@ -199,115 +101,45 @@ $(BUILD)/obj/68k/%.o: $(SRC_68K)/%.c @mkdir -p $(dir $@) $(AMIGA_CC) $(CFLAGS) -I$(CODEGEN_DIR) -c $< -o $@ +# Archived from scratch, so the members -- and every program's layout -- +# follow LIB_OBJS rather than the order earlier builds added them in. $(LIB): $(LIB_OBJS) @mkdir -p $(dir $@) + rm -f $@ $(AMIGA_AR) rcs $@ $^ -$(HELLO_BIN): $(HELLO_SRC) $(LIB) - @mkdir -p $(dir $@) - $(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) +# Pull in per-object header-dependency files generated by gcc -MMD/-MP. +# Without this, editing a header (e.g. surfaceInternal.h) doesn't rebuild +# the .c files that include it, leaving a frankenstein binary where +# different TUs see different struct layouts. +-include $(LIB_OBJS:.o=.d) -$(PATTERN_BIN): $(PATTERN_SRC) $(LIB) - @mkdir -p $(dir $@) - $(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) +# ---- programs ----------------------------------------------------------------- +# A program is a mixed-case Name with no extension (an AmigaDOS name), in bin/ +# or in its own bin/Name/ with its DATA beside it; joey-run boots the folder +# as the JOEYLIB hard drive. +PROGRAM_EXT := +PROGRAM_FOLDERS := yes +programNameRule = $(call capFirst,$(call lc,$(1))) -$(DRAW_BIN): $(DRAW_SRC) $(LIB) - @mkdir -p $(dir $@) - $(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) +# One program, $(1) its id: compiled and linked in one step with the library. +# gcc's -MMD leaves Name.d beside it, for header edits. +define programRules +$(call programBin,$(1)): $(call programVar,$(1),SRCS) $(LIB) + @mkdir -p $$(dir $$@) + $(AMIGA_CC) $(CFLAGS) $(call programVar,$(1),CFLAGS) $(call programVar,$(1),SRCS) $(LIB) -o $$@ $(LDFLAGS) $(call programVar,$(1),LDFLAGS) +-include $(call programBin,$(1)).d +endef -$(KEYS_BIN): $(KEYS_SRC) $(LIB) - @mkdir -p $(dir $@) - $(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) +# ---- JoeyLib's own build: its examples ------------------------------------------ +ifndef JOEY_PROJECT -$(SERIAL_BIN): $(SERIAL_SRC) $(LIB) - @mkdir -p $(dir $@) - $(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) +include $(REPO_DIR)/make/examples.mk +include $(REPO_DIR)/make/programs.mk -$(SERTEST_BIN): $(SERTEST_SRC) $(LIB) - @mkdir -p $(dir $@) - $(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) - -$(SAVE_BIN): $(SAVE_SRC) $(LIB) - @mkdir -p $(dir $@) - $(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) - -$(TILEBANK_BIN): $(TILEBANK_SRC) $(LIB) - @mkdir -p $(dir $@) - $(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) - -$(JOY_BIN): $(JOY_SRC) $(LIB) - @mkdir -p $(dir $@) - $(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) - -$(SPRITE_BIN): $(SPRITE_SRC) $(LIB) - @mkdir -p $(dir $@) - $(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) - -$(AUDIO_BIN): $(AUDIO_SRC) $(LIB) - @mkdir -p $(dir $@) - $(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) - -$(UBER_BIN): $(UBER_SRC) $(LIB) - @mkdir -p $(dir $@) - $(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) - -$(ADV_BIN): $(ADV_SRC) $(LIB) - @mkdir -p $(dir $@) - $(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) - -$(ADV2_BIN): $(ADV2_SRC) $(LIB) - @mkdir -p $(dir $@) - $(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) - -$(AGI_BIN): $(AGI_SRCS) $(LIB) - @mkdir -p $(dir $@) - $(AMIGA_CC) $(CFLAGS) $(AGI_SRCS) $(LIB) -o $@ $(LDFLAGS) - -$(STAXI_BIN): $(STAXI_SRCS) $(LIB) - @mkdir -p $(dir $@) - $(AMIGA_CC) $(CFLAGS) $(STAXI_SRCS) $(LIB) -o $@ $(LDFLAGS) - -# Space Taxi asset pipeline. .SECONDARY keeps the generated/ artifacts -# from being auto-deleted after staging. -.SECONDARY: $(STAXI_LEVEL_GEN) $(STAXICELS_SPC_GEN) $(STAXILEVEL_SPR_GEN) $(STAXILEVEL_SPC_GEN) - -$(STAXI_ROM_LEVEL_GEN) &: $(STAXI_RAW_DUMP) $(ROMTOLEVEL_BIN) - @mkdir -p $(STAXI_GEN_DIR)/levels - python3 $(ROMTOLEVEL_BIN) $(STAXI_RAW_DUMP) $(STAXI_GEN_DIR)/levels - -$(STAXI_GEN_DIR)/%.spc: $(STAXI_GEN_DIR)/%.spr $(SPRITEBAKE_BIN) - @mkdir -p $(dir $@) - $(SPRITEBAKE_BIN) $< $@ - -$(SPRITEBAKE_BIN): FORCE - $(MAKE) -f $(REPO_DIR)/make/tools.mk spritebake-amiga - -$(STAXIBAKE_BIN): FORCE - $(MAKE) -f $(REPO_DIR)/make/tools.mk staxibake - -# The cel bank is generated from the game's cel table (stCels.c). -$(STAXICELS_SPR_GEN): $(STAXIBAKE_BIN) $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stC64Data.c - @mkdir -p $(dir $@) - $(STAXIBAKE_BIN) $@ - -$(STAXI_RUN_DIR)/levels/%.dat: $(STAXI_GEN_DIR)/levels/%.dat - @mkdir -p $(dir $@) - cp $< $@ - -$(STAXI_RUN_DIR)/%.spc: $(STAXI_GEN_DIR)/%.spc - @mkdir -p $(dir $@) - cp $< $@ - -$(DATA_DIR)/test.mod: $(REPO_DIR)/assets/test.mod - @mkdir -p $(DATA_DIR) - cp $< $@ - -$(DATA_DIR)/test.sfx: $(REPO_DIR)/assets/test.sfx - @mkdir -p $(DATA_DIR) - cp $< $@ - -$(TILEBANK_DATA): $(TILEBANK_BAKE_DEPS) - python3 $(TILEBANK_STAGE) $(PLATFORM) $(TILEBANK_PNG) $(dir $@) +.DEFAULT_GOAL := all +.PHONY: all amiga clean-amiga clean +all amiga: $(LIB) programs clean-amiga: rm -rf $(BUILD) @@ -317,21 +149,4 @@ clean-amiga: # looks like success while leaving stale objects the next build links. clean: clean-amiga -# Pull in per-object header-dependency files generated by gcc -MMD/-MP. -# Without this, editing a header (e.g. surfaceInternal.h) doesn't rebuild -# the .c files that include it, leaving a frankenstein binary where -# different TUs see different struct layouts. --include $(LIB_OBJS:.o=.d) - -# The per-level cel banks, baked from each level's file. -$(STAXI_GEN_DIR)/sprites/level%.spr: $(STAXI_GEN_DIR)/levels/level%.dat $(STAXIBAKE_BIN) $(EXAMPLES)/spacetaxi/stLevelCels.c - @mkdir -p $(dir $@) - $(STAXIBAKE_BIN) --level $< $@ - -# Speech bake rules live down here so they never become the default goal. -$(STAXI_SPEECH_GEN): $(EXAMPLES)/spacetaxi/stSpeechData.c $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py - @mkdir -p $(dir $@) - python3 $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py $< $@ -$(STAXI_SPEECH_RUN): $(STAXI_SPEECH_GEN) - @mkdir -p $(dir $@) - cp $< $@ +endif diff --git a/make/atarist.mk b/make/atarist.mk index 5da213e..34ad7cb 100644 --- a/make/atarist.mk +++ b/make/atarist.mk @@ -1,15 +1,15 @@ -# Atari ST (m68k-atari-mint-gcc) build rules. +# Atari ST (m68k-atari-mint-gcc) build rules: JoeyLib's library for the ST, +# and programRules, the ST's rules for one program, which make/programs.mk +# instantiates for every program (a project reaches both through +# make/project.mk). JoeyLib's own examples and gates are at the end. +PLATFORM := atarist include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk $(call requireTool,$(ST_CC),toolchain:atarist) $(call requireTool,$(LIBXMP_DIR)/include/libxmp-lite/xmp.h,toolchain:atarist) -PLATFORM := atarist ST_SHIM := $(REPO_DIR)/make/support/atarist/include-shim -BUILD := $(REPO_DIR)/build/$(PLATFORM) -LIBDIR := $(BUILD)/lib -BINDIR := $(BUILD)/bin # so flag experiments never edit a tracked file (perf-hunt-2 rank 17). CFLAGS := $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer -I$(LIBXMP_DIR)/include -I$(ST_SHIM) -I$(SRC_DIR)/atarist -I$(SRC_68K) -MMD -MP @@ -50,113 +50,6 @@ LIB_OBJS := \ LIB := $(LIBDIR)/libjoey.a -HELLO_SRC := $(EXAMPLES)/hello/hello.c -HELLO_BIN := $(BINDIR)/HELLO.PRG -PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c -PATTERN_BIN := $(BINDIR)/PATTERN.PRG -DRAW_SRC := $(EXAMPLES)/draw/draw.c -DRAW_BIN := $(BINDIR)/DRAW.PRG -KEYS_SRC := $(EXAMPLES)/keys/keys.c -KEYS_BIN := $(BINDIR)/KEYS.PRG -SERIAL_SRC := $(EXAMPLES)/serial/serial.c -SERIAL_BIN := $(BINDIR)/SERIAL.PRG -SERTEST_SRC := $(EXAMPLES)/sertest/sertest.c -SERTEST_BIN := $(BINDIR)/SERTEST.PRG -SAVE_SRC := $(EXAMPLES)/save/save.c -SAVE_BIN := $(BINDIR)/SAVE.PRG -TILEBANK_SRC := $(EXAMPLES)/tilebank/tilebank.c -TILEBANK_BIN := $(BINDIR)/TILEBANK.PRG -JOY_SRC := $(EXAMPLES)/joy/joy.c -JOY_BIN := $(BINDIR)/JOY.PRG -SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c -SPRITE_BIN := $(BINDIR)/SPRITE.PRG -AUDIO_SRC := $(EXAMPLES)/audio/audio.c -AUDIO_BIN := $(BINDIR)/AUDIO.PRG -UBER_SRC := $(EXAMPLES)/uber/uber.c -UBER_BIN := $(BINDIR)/UBER.PRG -ADV_SRC := $(EXAMPLES)/adventure/adventure.c -ADV_BIN := $(BINDIR)/ADV.PRG -ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c -ADV2_BIN := $(BINDIR)/ADV2.PRG -AGI_SRCS := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c -AGI_BIN := $(BINDIR)/AGI.PRG -STAXI_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stLevelFile.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stSim.c $(EXAMPLES)/spacetaxi/stFare.c $(EXAMPLES)/spacetaxi/stHooks.c $(EXAMPLES)/spacetaxi/stTitle.c $(EXAMPLES)/spacetaxi/stC64Data.c $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stAudio.c $(EXAMPLES)/spacetaxi/stLevelCels.c -STAXI_INSTALL_DIR := $(BINDIR)/STAXI -STAXI_BIN := $(STAXI_INSTALL_DIR)/STAXI.PRG - -# Space Taxi asset pipeline (mirrors dos.mk / amiga.mk). -STAXI_GEN_DIR := $(EXAMPLES)/spacetaxi/generated/atarist -STAXI_RUN_DIR := $(STAXI_INSTALL_DIR)/DATA -# Level data is extracted from the C64 ROM (see dos.mk for the long -# explanation): 24 lettered levels A..X from scenes 0..23 plus the -# title from scene 24, via examples/spacetaxi/assets/romToLevel.py. -STAXI_ROM_LEVEL_GEN := $(foreach n,$(STAXI_LEVEL_NUMS),$(STAXI_GEN_DIR)/levels/level$(n).dat) \ - $(STAXI_GEN_DIR)/levels/title.dat -STAXI_LEVEL_GEN := $(STAXI_ROM_LEVEL_GEN) -STAXI_LEVEL_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_LEVEL_GEN)) -STAXI_ASSET_DSTS := $(STAXI_LEVEL_RUN) -# Pre-compiled sprite banks (.spc): baked from the .spr by the host spritebake -# tool for THIS target and staged in DATA/sprites, so the runtime loads -# compiled routines with no startup JIT (jlSpriteBankLoadPrecompiled). ST is -# the one port whose .spc carries a reloc list (the shifted-DRAW thunk's jmp to -# stSpriteDrawShiftWalker), re-stamped by the loader. -SPRITEBAKE_BIN := $(REPO_DIR)/build/tools/spritebake-atarist -# The C64-exact cel bank, generated by staxibake (from the shared cel table -# stCels.c + C64 bitmaps) and compiled to a .spc by the host spritebake, so -# STAXI loads its cels already compiled instead of JIT-compiling them at boot. -STAXIBAKE_BIN := $(REPO_DIR)/build/tools/staxibake - -# Per-level hook-sprite banks: the cels stLevelCelList names for each -# level that has hook sprites, baked from its level file and pre-compiled -# for this target, so no cel is JIT-compiled mid-ride. Levels without -# hook sprites have no bank; the game JIT-compiles at level entry when a -# bank is missing. -STAXI_CEL_LEVELS := 07 08 09 10 12 16 17 21 -STAXILEVEL_SPR_GEN := $(foreach n,$(STAXI_CEL_LEVELS),$(STAXI_GEN_DIR)/sprites/level$(n).spr) -STAXILEVEL_SPC_GEN := $(STAXILEVEL_SPR_GEN:.spr=.spc) -STAXILEVEL_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXILEVEL_SPC_GEN)) -STAXI_ASSET_DSTS += $(STAXILEVEL_SPC_RUN) - -# Speech baked to sample bytes per passenger pitch (assets/genSpeechPcm.py): -# expanding bits at play time is what keeps "HEY TAXI" from stalling the -# ride; the C64 run-length source stays in stSpeechData.c as the input. -STAXI_SPEECH_GEN := $(STAXI_GEN_DIR)/speech.bin -STAXI_SPEECH_RUN := $(STAXI_RUN_DIR)/speech.bin -STAXI_ASSET_DSTS += $(STAXI_SPEECH_RUN) -STAXICELS_SPR_GEN := $(STAXI_GEN_DIR)/sprites/staxicels.spr -STAXICELS_SPC_GEN := $(STAXICELS_SPR_GEN:.spr=.spc) -STAXICELS_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXICELS_SPC_GEN)) -# Added HERE, not up with the other asset destinations: this variable is -# defined below that point, and the := there expanded it to nothing, so -# the compiled cel bank was never a prerequisite of the port and never -# got copied into the build tree. Every port shipped whatever .spc was -# staged the day the file was first created, and edits to stCels.c did -# not reach the game (found 2026-09-23 when a new cel made the runtime -# reject the bank as too short). -STAXI_ASSET_DSTS += $(STAXICELS_SPC_RUN) - -# Game data lives under bin/DATA/, alongside the binaries Hatari picks -# up when bin/ is mounted as the GEMDOS C: drive. audio.c fopens -# "DATA/test.mod" etc. -DATA_DIR := $(BINDIR)/DATA -DATA_FILES := $(DATA_DIR)/test.mod $(DATA_DIR)/test.sfx -# TILEBANK's DATA/tilebank: this port's bake of the tile-bank fixture, the -# other targets' bakes it must refuse, and the expected pixels (common.mk). -TILEBANK_DATA := $(DATA_DIR)/tilebank/own.tbk - -# The dirty-band marker's on-target check: tests/host/dirtyMarkHost.c against -# src/m68k/stageBands.s, the asm the Amiga and X68000 share. Not a shipped -# program, so it builds beside bin/; scripts/verify-atarist-dirtymark.sh runs -# it under Hatari. The host check runs 200,000 scenarios; this many takes -# under a minute of emulated time. -DIRTYMARK_SRC := $(REPO_DIR)/tests/host/dirtyMarkHost.c -DIRTYMARK_BIN := $(BUILD)/test/DIRTYMRK.PRG -DIRTYMARK_SCENARIOS := 4000ul -DIRTYMARK_LOG := DIRTYMRK.TXT - -.PHONY: all atarist clean-atarist clean atarist-dirtymark atarist-verify-dirtymark -all atarist: $(LIB) $(LIBXMP_AR) $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(SERIAL_BIN) $(SERTEST_BIN) $(SAVE_BIN) $(TILEBANK_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) $(DATA_FILES) $(TILEBANK_DATA) $(call staxiData,$(STAXI_ASSET_DSTS)) - $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c @mkdir -p $(dir $@) $(ST_CC) $(CFLAGS) -c $< -o $@ @@ -190,8 +83,11 @@ $(BUILD)/obj/codegen/%.o: $(CODEGEN_DIR)/%.c @mkdir -p $(dir $@) $(ST_CC) $(CFLAGS) -I$(CODEGEN_DIR) -c $< -o $@ +# Archived from scratch, so the members -- and every program's layout -- +# follow LIB_OBJS rather than the order earlier builds added them in. $(LIB): $(LIB_OBJS) @mkdir -p $(dir $@) + rm -f $@ $(ST_AR) rcs $@ $^ $(LIBXMP_OBJDIR)/%.o: $(LIBXMP_DIR)/src/%.c @@ -204,122 +100,45 @@ $(LIBXMP_OBJDIR)/math_shim.o: $(ST_SHIM)/math_shim.c $(LIBXMP_AR): $(LIBXMP_OBJS) @mkdir -p $(dir $@) + rm -f $@ $(ST_AR) rcs $@ $^ -$(HELLO_BIN): $(HELLO_SRC) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) +# Pull in per-object header-dependency files generated by gcc -MMD/-MP. +# Without this, editing a header (e.g. surfaceInternal.h) doesn't rebuild +# the .c files that include it, leaving a frankenstein binary where +# different TUs see different struct layouts. +-include $(LIB_OBJS:.o=.d) -$(PATTERN_BIN): $(PATTERN_SRC) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) +# ---- programs ----------------------------------------------------------------- +# A program is NAME.PRG, in bin/ or in its own bin/NAME/ with its DATA beside +# it, where Hatari's GEMDOS drive (bin/ or the folder as C:) finds both. +PROGRAM_EXT := .PRG +PROGRAM_FOLDERS := yes -$(DRAW_BIN): $(DRAW_SRC) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) +# One program, $(1) its id: compiled and linked in one step with the library +# and libxmp-lite. gcc's -MMD leaves NAME.d beside it, for header edits. +define programRules +$(call programBin,$(1)): $(call programVar,$(1),SRCS) $(LIB) $(LIBXMP_AR) + @mkdir -p $$(dir $$@) + $(ST_CC) $(CFLAGS) $(call programVar,$(1),CFLAGS) $(call programVar,$(1),SRCS) $(LIB) $(LIBXMP_AR) -o $$@ $(LDFLAGS) $(call programVar,$(1),LDFLAGS) +-include $(basename $(call programBin,$(1))).d +endef -$(KEYS_BIN): $(KEYS_SRC) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) +# ---- JoeyLib's own build: its examples and the ST gates ------------------------- +ifndef JOEY_PROJECT -$(SERIAL_BIN): $(SERIAL_SRC) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) +include $(REPO_DIR)/make/examples.mk +include $(REPO_DIR)/make/programs.mk -$(SERTEST_BIN): $(SERTEST_SRC) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) +.DEFAULT_GOAL := all +.PHONY: all atarist clean-atarist clean atarist-dirtymark atarist-verify-dirtymark +all atarist: $(LIB) $(LIBXMP_AR) programs -$(SAVE_BIN): $(SAVE_SRC) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) +atarist-dirtymark: $(call programBin,DIRTYMRK) -$(DIRTYMARK_BIN): $(DIRTYMARK_SRC) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(ST_CC) $(CFLAGS) -DDIRTY_MARK_SCENARIOS=$(DIRTYMARK_SCENARIOS) -DDIRTY_MARK_LOG='"$(DIRTYMARK_LOG)"' $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) - -atarist-dirtymark: $(DIRTYMARK_BIN) - -atarist-verify-dirtymark: $(DIRTYMARK_BIN) +atarist-verify-dirtymark: $(call programBin,DIRTYMRK) $(REPO_DIR)/scripts/verify-atarist-dirtymark.sh -$(TILEBANK_BIN): $(TILEBANK_SRC) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) - -$(JOY_BIN): $(JOY_SRC) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) - -$(SPRITE_BIN): $(SPRITE_SRC) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) - -$(AUDIO_BIN): $(AUDIO_SRC) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) - -$(UBER_BIN): $(UBER_SRC) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) - -$(ADV_BIN): $(ADV_SRC) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) - -$(ADV2_BIN): $(ADV2_SRC) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) - -$(AGI_BIN): $(AGI_SRCS) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(ST_CC) $(CFLAGS) $(AGI_SRCS) $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) - -$(STAXI_BIN): $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(ST_CC) $(CFLAGS) $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) - -# Space Taxi asset pipeline (mirrors dos.mk + amiga.mk). -.SECONDARY: $(STAXI_LEVEL_GEN) $(STAXICELS_SPC_GEN) $(STAXILEVEL_SPR_GEN) $(STAXILEVEL_SPC_GEN) - -$(STAXI_ROM_LEVEL_GEN) &: $(STAXI_RAW_DUMP) $(ROMTOLEVEL_BIN) - @mkdir -p $(STAXI_GEN_DIR)/levels - python3 $(ROMTOLEVEL_BIN) $(STAXI_RAW_DUMP) $(STAXI_GEN_DIR)/levels - -$(STAXI_GEN_DIR)/%.spc: $(STAXI_GEN_DIR)/%.spr $(SPRITEBAKE_BIN) - @mkdir -p $(dir $@) - $(SPRITEBAKE_BIN) $< $@ - -$(SPRITEBAKE_BIN): FORCE - $(MAKE) -f $(REPO_DIR)/make/tools.mk spritebake-atarist - -$(STAXIBAKE_BIN): FORCE - $(MAKE) -f $(REPO_DIR)/make/tools.mk staxibake - -# The cel bank is generated from the game's cel table (stCels.c). -$(STAXICELS_SPR_GEN): $(STAXIBAKE_BIN) $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stC64Data.c - @mkdir -p $(dir $@) - $(STAXIBAKE_BIN) $@ - -$(STAXI_RUN_DIR)/levels/%.dat: $(STAXI_GEN_DIR)/levels/%.dat - @mkdir -p $(dir $@) - cp $< $@ - -$(STAXI_RUN_DIR)/%.spc: $(STAXI_GEN_DIR)/%.spc - @mkdir -p $(dir $@) - cp $< $@ - -$(DATA_DIR)/test.mod: $(REPO_DIR)/assets/test.mod - @mkdir -p $(DATA_DIR) - cp $< $@ - -$(DATA_DIR)/test.sfx: $(REPO_DIR)/assets/test.sfx - @mkdir -p $(DATA_DIR) - cp $< $@ - -$(TILEBANK_DATA): $(TILEBANK_BAKE_DEPS) - python3 $(TILEBANK_STAGE) $(PLATFORM) $(TILEBANK_PNG) $(dir $@) - clean-atarist: rm -rf $(BUILD) @@ -328,27 +147,4 @@ clean-atarist: # looks like success while leaving stale objects the next build links. clean: clean-atarist -# Pull in per-object header-dependency files generated by gcc -MMD/-MP. -# Without this, editing a header (e.g. surfaceInternal.h) doesn't rebuild -# the .c files that include it, leaving a frankenstein binary where -# different TUs see different struct layouts. --include $(LIB_OBJS:.o=.d) - -# The per-level cel banks, baked from each level's file. -$(STAXI_GEN_DIR)/sprites/level%.spr: $(STAXI_GEN_DIR)/levels/level%.dat $(STAXIBAKE_BIN) $(EXAMPLES)/spacetaxi/stLevelCels.c - @mkdir -p $(dir $@) - $(STAXIBAKE_BIN) --level $< $@ - -# Speech bake rules live down here so they never become the default goal. -$(STAXI_SPEECH_GEN): $(EXAMPLES)/spacetaxi/stSpeechData.c $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py - @mkdir -p $(dir $@) - # --single: one polarity, 60 KB instead of 120 KB. Measured on a - # stock 1 MB machine, the two-polarity file cannot be allocated at - # all -- "speech alloc FAILED (120828 bytes)", so that ST plays the - # whole game mute -- while the half-size one loads there. 2 MB and up - # take either. The samples are identical: the player negates them - # when the running polarity is odd, one subtraction per byte. - python3 $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py $< $@ --single -$(STAXI_SPEECH_RUN): $(STAXI_SPEECH_GEN) - @mkdir -p $(dir $@) - cp $< $@ +endif diff --git a/make/common.mk b/make/common.mk index b4ee01c..143f8ce 100644 --- a/make/common.mk +++ b/make/common.mk @@ -1,6 +1,7 @@ # Shared variables used by per-target Makefile fragments. # -# Source this only via the per-target make file (make/iigs.mk etc). +# Source this only via the per-target make file (make/iigs.mk etc), which +# sets PLATFORM first; make/tools.mk and the top Makefile include it without. REPO_DIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))/..) @@ -15,18 +16,36 @@ REPO_DIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))/..) # `./install.sh doctor` names exactly what is wrong. requireTool = $(if $(filter-out clean%,$(or $(MAKECMDGOALS),all)),$(if $(wildcard $(1)),,$(error $(2) is not installed; run: ./install.sh install $(2)))) -# Space Taxi's levels are extracted from the original C64 game image, which -# is not part of the repository (the owner's private bundle provides it; see -# ./install.sh import). Without it every port still builds the -# STAXI binary and skips only the data derived from the image. -STAXI_RAW_DUMP := $(REPO_DIR)/stuff/spacetaxi/raw.bin -ROMTOLEVEL_BIN := $(REPO_DIR)/examples/spacetaxi/assets/romToLevel.py -STAXI_LEVEL_NUMS := 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 -HAVE_STAXI_DATA := $(if $(wildcard $(STAXI_RAW_DUMP)),1) -# $(call staxiData,TARGETS): TARGETS, or nothing (with a note, once per make -# process) without the image. -staxiData = $(if $(HAVE_STAXI_DATA),$(1),$(staxiNote)) -staxiNote = $(if $(staxiNoted),,$(eval staxiNoted := 1)$(info note: Space Taxi data skipped: stuff/spacetaxi/raw.bin is not present)) +# Letter case, for the port tags (IIGS) and the program names each port +# derives from a program's id (Hello on the Amiga, hello on DVX). +lc = $(subst A,a,$(subst B,b,$(subst C,c,$(subst D,d,$(subst E,e,$(subst F,f,$(subst G,g,$(subst H,h,$(subst I,i,$(subst J,j,$(subst K,k,$(subst L,l,$(subst M,m,$(subst N,n,$(subst O,o,$(subst P,p,$(subst Q,q,$(subst R,r,$(subst S,s,$(subst T,t,$(subst U,u,$(subst V,v,$(subst W,w,$(subst X,x,$(subst Y,y,$(subst Z,z,$(1))))))))))))))))))))))))))) +uc = $(subst a,A,$(subst b,B,$(subst c,C,$(subst d,D,$(subst e,E,$(subst f,F,$(subst g,G,$(subst h,H,$(subst i,I,$(subst j,J,$(subst k,K,$(subst l,L,$(subst m,M,$(subst n,N,$(subst o,O,$(subst p,P,$(subst q,Q,$(subst r,R,$(subst s,S,$(subst t,T,$(subst u,U,$(subst v,V,$(subst w,W,$(subst x,X,$(subst y,Y,$(subst z,Z,$(1))))))))))))))))))))))))))) +capFirst = $(patsubst a%,A%,$(patsubst b%,B%,$(patsubst c%,C%,$(patsubst d%,D%,$(patsubst e%,E%,$(patsubst f%,F%,$(patsubst g%,G%,$(patsubst h%,H%,$(patsubst i%,I%,$(patsubst j%,J%,$(patsubst k%,K%,$(patsubst l%,L%,$(patsubst m%,M%,$(patsubst n%,N%,$(patsubst o%,O%,$(patsubst p%,P%,$(patsubst q%,Q%,$(patsubst r%,R%,$(patsubst s%,S%,$(patsubst t%,T%,$(patsubst u%,U%,$(patsubst v%,V%,$(patsubst w%,W%,$(patsubst x%,X%,$(patsubst y%,Y%,$(patsubst z%,Z%,$(1))))))))))))))))))))))))))) + +# The ports, and which of them can build here: a port is available when its +# real compiler binary is installed (DVX also needs the DVX SDK). The top +# Makefile's `make` and a project's `make` build exactly these. +JOEY_PORTS := iigs amiga atarist dos x68000 dvx +HAVE_IIGS := $(if $(wildcard $(LLVM816_ROOT)/tools/llvm-mos-build/bin/clang),1) +HAVE_AMIGA := $(if $(wildcard $(AMIGA_CC)),1) +HAVE_ATARIST := $(if $(wildcard $(ST_CC)),1) +HAVE_DOS := $(if $(wildcard $(DOS_CC)),1) +HAVE_X68000 := $(if $(wildcard $(X68K_ROOT)/bin/m68k-xelf-gcc),1) +HAVE_DVX := $(if $(and $(wildcard $(DOS_CC)),$(wildcard $(DVX_SDK)/sdk.mk)),1) +JOEY_PORTS_INSTALLED := $(foreach p,$(JOEY_PORTS),$(if $(HAVE_$(call uc,$(p))),$(p))) + +# Where things are built. JOEY_BUILD holds what is JoeyLib's -- each port's +# library and the host tools -- and PROJECT_BUILD the programs: for JoeyLib's +# own examples both are build/. A project (make/project.mk) sets JOEY_PROJECT +# to its directory, so its programs land in its own build/ while JoeyLib's +# library is built once, in JoeyLib's. Point JOEY_BUILD elsewhere to keep a +# project's build out of the JoeyLib checkout altogether. +JOEY_BUILD ?= $(REPO_DIR)/build +PROJECT_DIR := $(or $(JOEY_PROJECT),$(REPO_DIR)) +PROJECT_BUILD ?= $(PROJECT_DIR)/build +# make/tools.mk runs as a separate make; it builds into the same place. +export JOEY_BUILD + INCLUDE_DIR := $(REPO_DIR)/include # Machine-specific code lives in src// (iigs, amiga, atarist, dos, # x68000, dvx); code shared across machines of one CPU lives in a processor @@ -38,13 +57,10 @@ SRC_68K := $(REPO_DIR)/src/m68k SRC_X86 := $(REPO_DIR)/src/x86 EXAMPLES := $(REPO_DIR)/examples -# The tile-bank example (examples/tilebank) loads the host gate's fixture, so -# the host (scripts/check-tilebank.sh) and every port share one expected hash. -# Each port stages its DATA/tilebank with TILEBANK_STAGE: its own bake, every -# other target's bake (to be refused) and the expected pixels. -TILEBANK_PNG := $(REPO_DIR)/tests/host/tileBank.png -TILEBANK_STAGE := $(REPO_DIR)/tests/host/tileBankStage.py -TILEBANK_BAKE_DEPS := $(TILEBANK_PNG) $(TILEBANK_STAGE) $(REPO_DIR)/tools/assetbake/assetbake.py +# Host tools (make/tools.mk, and a program's own bake tools) build with the +# host's cc. +HOST_CC ?= cc +HOST_CFLAGS := -std=c99 -Wall -Wextra -Werror -O2 # Portable C sources for libjoey -- present on every target. CORE_C_SRCS := $(wildcard $(SRC_CORE)/*.c) @@ -61,14 +77,75 @@ GENERIC_C_SRCS := $(filter-out %/spriteEmitStub.c, $(wildcard $(SRC_DIR)/generic # make/iigs.mk, which does not use COMMON_CFLAGS, honours it too. COMMON_CFLAGS := -I$(INCLUDE_DIR) -I$(SRC_CORE) -Wall -Wextra -Werror -O2 $(EXTRA_CFLAGS) +ifdef PLATFORM +# One port's directories: its library (BUILD, LIBDIR) is JoeyLib's, its +# programs (PROGRAM_BUILD, BINDIR) the project's. +BUILD := $(JOEY_BUILD)/$(PLATFORM) +LIBDIR := $(BUILD)/lib +PROGRAM_BUILD := $(PROJECT_BUILD)/$(PLATFORM) +BINDIR := $(PROGRAM_BUILD)/bin +PORT_TAG := $(call uc,$(PLATFORM)) +# Baked assets (sprite and tile banks) carry the target they were baked for. +# DVX shows the DOS port's stage and runs its x86 code, so it takes the DOS +# bakes; every other port its own. +ASSET_PLATFORM := $(if $(filter dvx,$(PLATFORM)),dos,$(PLATFORM)) +# The host sprite pre-compiler for this port's bakes (.spr -> .spc); its rule +# is make/programs.mk's. +SPRITEBAKE := $(JOEY_BUILD)/tools/spritebake-$(ASSET_PLATFORM) +endif + +# ---- programs ----------------------------------------------------------------- +# What a program's variables mean is listed in make/programs.mk. These are the +# lookups that file and the program descriptions use; each port sets +# PROGRAM_EXT, PROGRAM_FOLDERS (programs may ship in a folder of their own), +# PROGRAM_DATA_APART (DATA is staged apart from the program, for the +# packager) and PROGRAM_OPTIONAL_APART (optional DATA apart again), and may +# redefine programNameRule, programDir and programTarget. + +# $(call programVar,ID,VAR): the program's VAR on this port -- ID__VAR +# when that is defined (even empty), else ID_VAR. +programVar = $(if $(filter undefined,$(origin $(1)_$(PORT_TAG)_$(2))),$($(1)_$(2)),$($(1)_$(PORT_TAG)_$(2))) + +# $(call programOnPort,ID): ID when it is built for this port (ID_PORTS, +# every port by default), else nothing. +programOnPort = $(if $(filter $(PLATFORM),$(or $($(1)_PORTS),$(JOEY_PORTS))),$(1)) + +# $(call programName,ID): its file name without the extension: ID_NAME, else +# the port's naming rule -- the id itself, unless the port redefines it. +programName = $(or $(call programVar,$(1),NAME),$(call programNameRule,$(1))) +programNameRule = $(1) + +# $(call programDir,ID): the folder it is built into: ID_BINDIR or bin/, and +# in there a folder of its own when it ships in one (ID_FOLDER) on a port +# whose programs can. +programDir = $(or $(call programVar,$(1),BINDIR),$(BINDIR))$(if $(and $(PROGRAM_FOLDERS),$(call programVar,$(1),FOLDER)),/$(call programName,$(1))) + +# $(call programBin,ID): the program file. +programBin = $(call programDir,$(1))/$(call programName,$(1))$(PROGRAM_EXT) + +# $(call programTarget,ID): what the programs goal builds for it. +programTarget = $(call programBin,$(1)) + +# $(call programDataDir,ID): where its DATA files are staged -- the DATA +# folder beside it, or on a port that packages programs (the IIgs disks, the +# DVX SDK) build//data//DATA. +programDataDir = $(if $(PROGRAM_DATA_APART),$(PROGRAM_BUILD)/data/$(call programName,$(1))/DATA,$(call programDir,$(1))/DATA) + +# $(call programOptionalDir,ID): where its optional DATA files are staged: +# ones it runs without (a game that JIT-compiles a level's sprites when the +# level's pre-compiled bank is missing). Only the IIgs disk packer treats +# them apart -- they go on last, while there is room -- so elsewhere this is +# the DATA folder itself. +programOptionalDir = $(if $(PROGRAM_OPTIONAL_APART),$(PROGRAM_BUILD)/data/$(call programName,$(1))/OPTIONAL,$(call programDataDir,$(1))) + # Rules that hand a host tool's build to make/tools.mk depend on FORCE, so # tools.mk -- which owns each tool's source list -- is always asked; it does # nothing when the tool is current. Without it an existing tool binary looked -# up to date forever, and edits to its sources never reached it (staxibake -# kept running a stale binary after stCels.c changed). Defining it must not -# change any port's default goal (most include this file before their `all`), -# so the goal in force is saved and put back around it. +# up to date forever, and edits to its sources never reached it (a bake tool +# kept running a stale binary after a source it links changed). Defining it +# must not change any port's default goal (most include this file before +# their `all`), so the goal in force is saved and put back around it. COMMON_DEFAULT_GOAL := $(.DEFAULT_GOAL) .PHONY: FORCE FORCE: diff --git a/make/dos.mk b/make/dos.mk index df66b28..1f4d552 100644 --- a/make/dos.mk +++ b/make/dos.mk @@ -1,5 +1,9 @@ -# DOS (DJGPP) build rules. +# DOS (DJGPP) build rules: JoeyLib's library for DOS, and programRules, the +# DOS rules for one program, which make/programs.mk instantiates for every +# program (a project reaches both through make/project.mk). JoeyLib's own +# examples are at the end. +PLATFORM := dos include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk $(call requireTool,$(DOS_CC),toolchain:dos) @@ -7,18 +11,12 @@ $(call requireTool,$(DOS_AS),toolchain:dos) $(call requireTool,$(DOS_CWSDSTUB),toolchain:dos) $(call requireTool,$(LIBXMP_DIR)/include/libxmp-lite/xmp.h,toolchain:dos) -PLATFORM := dos -BUILD := $(REPO_DIR)/build/$(PLATFORM) -LIBDIR := $(BUILD)/lib -BINDIR := $(BUILD)/bin - # -fomit-frame-pointer: flag parity with the ST/Amiga CFLAGS (both had # it; DOS never did). Frees EBP for allocation in the hot loops under # DOSBox's per-instruction-charged core (perf-hunt-2 rank 2). # so flag experiments never edit a tracked file (perf-hunt-2 rank 17). CFLAGS := $(COMMON_CFLAGS) -march=i386 -m32 -fomit-frame-pointer -I$(SRC_DIR)/dos -I$(SRC_X86) -I$(LIBXMP_DIR)/include -MMD -MP ASFLAGS := -f coff -LDFLAGS := # libxmp-lite (MIT, libxmp.org) -- 4-channel-Protracker-and-friends # replayer + SMIX SFX overlay. Installed by ./install.sh @@ -49,123 +47,6 @@ LIB_OBJS := \ LIB := $(LIBDIR)/libjoey.a -HELLO_SRC := $(EXAMPLES)/hello/hello.c -HELLO_BIN := $(BINDIR)/HELLO.EXE -PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c -PATTERN_BIN := $(BINDIR)/PATTERN.EXE -DRAW_SRC := $(EXAMPLES)/draw/draw.c -DRAW_BIN := $(BINDIR)/DRAW.EXE -KEYS_SRC := $(EXAMPLES)/keys/keys.c -KEYS_BIN := $(BINDIR)/KEYS.EXE -SERIAL_SRC := $(EXAMPLES)/serial/serial.c -SERIAL_BIN := $(BINDIR)/SERIAL.EXE -SERTEST_SRC := $(EXAMPLES)/sertest/sertest.c -SERTEST_BIN := $(BINDIR)/SERTEST.EXE -SAVE_SRC := $(EXAMPLES)/save/save.c -SAVE_BIN := $(BINDIR)/SAVE.EXE -TILEBANK_SRC := $(EXAMPLES)/tilebank/tilebank.c -TILEBANK_BIN := $(BINDIR)/TILEBANK.EXE -JOY_SRC := $(EXAMPLES)/joy/joy.c -JOY_BIN := $(BINDIR)/JOY.EXE -SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c -SPRITE_BIN := $(BINDIR)/SPRITE.EXE -AUDIO_SRC := $(EXAMPLES)/audio/audio.c -AUDIO_BIN := $(BINDIR)/AUDIO.EXE -UBER_SRC := $(EXAMPLES)/uber/uber.c -UBER_BIN := $(BINDIR)/UBER.EXE -ADV_SRC := $(EXAMPLES)/adventure/adventure.c -ADV_BIN := $(BINDIR)/ADV.EXE -ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c -ADV2_BIN := $(BINDIR)/ADV2.EXE -AGI_SRCS := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c -AGI_BIN := $(BINDIR)/AGI.EXE -STAXI_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stLevelFile.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stSim.c $(EXAMPLES)/spacetaxi/stFare.c $(EXAMPLES)/spacetaxi/stHooks.c $(EXAMPLES)/spacetaxi/stTitle.c $(EXAMPLES)/spacetaxi/stC64Data.c $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stAudio.c $(EXAMPLES)/spacetaxi/stLevelCels.c -# JoeyLib install convention: each app lives in its own bin subdir -# with a DATA/ folder alongside the binary for runtime assets. -STAXI_INSTALL_DIR := $(BINDIR)/STAXI -STAXI_BIN := $(STAXI_INSTALL_DIR)/STAXI.EXE - -# Game data lives under bin/DATA/, alongside the binaries DOSBox picks -# up when bin/ is mounted as C:. audio.c fopens "DATA/test.mod" etc. -DATA_DIR := $(BINDIR)/DATA -DATA_FILES := $(DATA_DIR)/test.mod $(DATA_DIR)/test.sfx -# TILEBANK's DATA/tilebank: this port's bake of the tile-bank fixture, the -# other targets' bakes it must refuse, and the expected pixels (common.mk). -TILEBANK_DATA := $(DATA_DIR)/tilebank/own.tbk - -# Space Taxi asset pipeline. -# -# Build artifacts go to examples/spacetaxi/generated/ -- safe to -# nuke; recreated by this makefile. What the game loads is then copied -# into the DATA tree beside its binary: the levels, the cel bank, the -# per-level cel banks and the speech. Tiles and the font come from the -# C64 data compiled into the binary (stC64Data.c). -STAXI_GEN_DIR := $(EXAMPLES)/spacetaxi/generated/dos -STAXI_RUN_DIR := $(STAXI_INSTALL_DIR)/DATA - -# Level data is extracted from the C64 ROM by -# examples/spacetaxi/assets/romToLevel.py: 24 lettered levels (A..X) from scenes -# 0..23 plus the title from scene 24. The script runs the captured -# decompressor through a Python 6502 emulator and writes STL2 .dat -# files directly. Input is the raw RAM dump. mkstlevel + title.txt -# is no longer used: the C64 title encodes its screen with the game's -# custom charset (C64 data in stC64Data.c), so a hand-authored tile -# layout with a different glyph mapping renders as garbage. -STAXI_ROM_LEVEL_GEN := $(foreach n,$(STAXI_LEVEL_NUMS),$(STAXI_GEN_DIR)/levels/level$(n).dat) \ - $(STAXI_GEN_DIR)/levels/title.dat -STAXI_LEVEL_GEN := $(STAXI_ROM_LEVEL_GEN) - - -# The runtime tree beside the binary. -STAXI_LEVEL_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_LEVEL_GEN)) -STAXI_ASSET_DSTS := $(STAXI_LEVEL_RUN) -# Pre-compiled sprite banks (.spc): baked from the .spr by the host spritebake -# tool for THIS target and staged in DATA/sprites, so the runtime loads -# compiled routines with no startup JIT (jlSpriteBankLoadPrecompiled). -SPRITEBAKE_BIN := $(REPO_DIR)/build/tools/spritebake-dos -# The C64-exact cel bank, generated by staxibake (from the shared cel table -# stCels.c + C64 bitmaps) and compiled to a .spc by the host spritebake, so -# STAXI loads its cels already compiled instead of JIT-compiling them at boot. -STAXIBAKE_BIN := $(REPO_DIR)/build/tools/staxibake - -# Per-level hook-sprite banks: the cels stLevelCelList names for each -# level that has hook sprites, baked from its level file and pre-compiled -# for this target, so no cel is JIT-compiled mid-ride. Levels without -# hook sprites have no bank; the game JIT-compiles at level entry when a -# bank is missing. -STAXI_CEL_LEVELS := 07 08 09 10 12 16 17 21 -STAXILEVEL_SPR_GEN := $(foreach n,$(STAXI_CEL_LEVELS),$(STAXI_GEN_DIR)/sprites/level$(n).spr) -STAXILEVEL_SPC_GEN := $(STAXILEVEL_SPR_GEN:.spr=.spc) -STAXILEVEL_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXILEVEL_SPC_GEN)) -STAXI_ASSET_DSTS += $(STAXILEVEL_SPC_RUN) - -# Speech baked to sample bytes per passenger pitch (assets/genSpeechPcm.py): -# expanding bits at play time is what keeps "HEY TAXI" from stalling the -# ride; the C64 run-length source stays in stSpeechData.c as the input. -STAXI_SPEECH_GEN := $(STAXI_GEN_DIR)/speech.bin -STAXI_SPEECH_RUN := $(STAXI_RUN_DIR)/speech.bin -STAXI_ASSET_DSTS += $(STAXI_SPEECH_RUN) -STAXICELS_SPR_GEN := $(STAXI_GEN_DIR)/sprites/staxicels.spr -STAXICELS_SPC_GEN := $(STAXICELS_SPR_GEN:.spr=.spc) -STAXICELS_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXICELS_SPC_GEN)) -# Added HERE, not up with the other asset destinations: this variable is -# defined below that point, and the := there expanded it to nothing, so -# the compiled cel bank was never a prerequisite of the port and never -# got copied into the build tree. Every port shipped whatever .spc was -# staged the day the file was first created, and edits to stCels.c did -# not reach the game (found 2026-09-23 when a new cel made the runtime -# reject the bank as too short). -STAXI_ASSET_DSTS += $(STAXICELS_SPC_RUN) - -MKSTLEVEL_BIN := $(REPO_DIR)/build/tools/mkstlevel - -.PHONY: all dos dos-staxi-data clean-dos clean -all dos: $(LIB) $(LIBXMP_AR) $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(SERIAL_BIN) $(SERTEST_BIN) $(SAVE_BIN) $(TILEBANK_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) $(DATA_FILES) $(TILEBANK_DATA) $(call staxiData,$(STAXI_ASSET_DSTS)) - -# Space Taxi's DATA tree alone: make/dvx.mk stages this same bake, since DVX -# runs the DOS stage and the same x86 code. -dos-staxi-data: $(call staxiData,$(STAXI_ASSET_DSTS)) - $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c @mkdir -p $(dir $@) $(DOS_CC) $(CFLAGS) -c $< -o $@ @@ -190,8 +71,11 @@ $(BUILD)/obj/codegen/%.o: $(CODEGEN_DIR)/%.c @mkdir -p $(dir $@) $(DOS_CC) $(CFLAGS) -I$(CODEGEN_DIR) -c $< -o $@ +# Archived from scratch, so the members -- and every program's layout -- +# follow LIB_OBJS rather than the order earlier builds added them in. $(LIB): $(LIB_OBJS) @mkdir -p $(dir $@) + rm -f $@ $(DOS_AR) rcs $@ $^ $(LIBXMP_OBJDIR)/%.o: $(LIBXMP_DIR)/src/%.c @@ -200,139 +84,42 @@ $(LIBXMP_OBJDIR)/%.o: $(LIBXMP_DIR)/src/%.c $(LIBXMP_AR): $(LIBXMP_OBJS) @mkdir -p $(dir $@) + rm -f $@ $(DOS_AR) rcs $@ $^ -$(HELLO_BIN): $(HELLO_SRC) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ - $(DOS_EMBED_DPMI) $@ +# Pull in per-object header-dependency files generated by gcc -MMD/-MP. +# Without this, editing a header (e.g. surfaceInternal.h) doesn't rebuild +# the .c files that include it, leaving a frankenstein binary where +# different TUs see different struct layouts. +-include $(LIB_OBJS:.o=.d) -$(PATTERN_BIN): $(PATTERN_SRC) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ - $(DOS_EMBED_DPMI) $@ +# ---- programs ----------------------------------------------------------------- +# A program is NAME.EXE, in bin/ or in its own bin/NAME/ with its DATA beside +# it (JoeyLib's install convention), where DOSBox's C: (bin/ or the folder) +# finds both. +PROGRAM_EXT := .EXE +PROGRAM_FOLDERS := yes -$(DRAW_BIN): $(DRAW_SRC) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ - $(DOS_EMBED_DPMI) $@ +# One program, $(1) its id: compiled and linked in one step with the library +# and libxmp-lite, then given the CWSDPMI DPMI server, so the .EXE needs no +# CWSDPMI.EXE beside it. gcc's -MMD leaves NAME.d beside it, for header edits. +define programRules +$(call programBin,$(1)): $(call programVar,$(1),SRCS) $(LIB) $(LIBXMP_AR) + @mkdir -p $$(dir $$@) + $(DOS_CC) $(CFLAGS) $(call programVar,$(1),CFLAGS) $(call programVar,$(1),SRCS) $(LIB) $(LIBXMP_AR) -o $$@ $(call programVar,$(1),LDFLAGS) + $(DOS_EMBED_DPMI) $$@ +-include $(basename $(call programBin,$(1))).d +endef -$(KEYS_BIN): $(KEYS_SRC) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ - $(DOS_EMBED_DPMI) $@ +# ---- JoeyLib's own build: its examples ------------------------------------------ +ifndef JOEY_PROJECT -$(SERIAL_BIN): $(SERIAL_SRC) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ - $(DOS_EMBED_DPMI) $@ +include $(REPO_DIR)/make/examples.mk +include $(REPO_DIR)/make/programs.mk -$(SERTEST_BIN): $(SERTEST_SRC) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ - $(DOS_EMBED_DPMI) $@ - -$(SAVE_BIN): $(SAVE_SRC) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ - $(DOS_EMBED_DPMI) $@ - -$(TILEBANK_BIN): $(TILEBANK_SRC) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ - $(DOS_EMBED_DPMI) $@ - -$(JOY_BIN): $(JOY_SRC) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ - $(DOS_EMBED_DPMI) $@ - -$(SPRITE_BIN): $(SPRITE_SRC) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ - $(DOS_EMBED_DPMI) $@ - -$(AUDIO_BIN): $(AUDIO_SRC) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ - $(DOS_EMBED_DPMI) $@ - -$(UBER_BIN): $(UBER_SRC) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ - $(DOS_EMBED_DPMI) $@ - -$(ADV_BIN): $(ADV_SRC) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ - $(DOS_EMBED_DPMI) $@ - -$(ADV2_BIN): $(ADV2_SRC) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ - $(DOS_EMBED_DPMI) $@ - -$(AGI_BIN): $(AGI_SRCS) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(DOS_CC) $(CFLAGS) $(AGI_SRCS) $(LIB) $(LIBXMP_AR) -o $@ - $(DOS_EMBED_DPMI) $@ - -$(STAXI_BIN): $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(DOS_CC) $(CFLAGS) $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) -o $@ - $(DOS_EMBED_DPMI) $@ - -# Stage A: generate artifacts into examples/spacetaxi/generated/. -# Mark these .SECONDARY so make doesn't auto-delete them after the -# stage-B copy into bin/ -- they're the canonical distribution -# artifacts. -.SECONDARY: $(STAXI_LEVEL_GEN) $(STAXICELS_SPC_GEN) $(STAXILEVEL_SPR_GEN) $(STAXILEVEL_SPC_GEN) - -# Bulk-extract all 24 canonical levels plus the title from the C64 -# ROM in one invocation. Grouped target (&:) means make treats the -# recipe as producing the whole list together -- it runs once even -# though many targets depend on it. Requires GNU Make 4.3+. -$(STAXI_ROM_LEVEL_GEN) &: $(STAXI_RAW_DUMP) $(ROMTOLEVEL_BIN) - @mkdir -p $(STAXI_GEN_DIR)/levels - python3 $(ROMTOLEVEL_BIN) $(STAXI_RAW_DUMP) $(STAXI_GEN_DIR)/levels - -$(STAXI_GEN_DIR)/%.spc: $(STAXI_GEN_DIR)/%.spr $(SPRITEBAKE_BIN) - @mkdir -p $(dir $@) - $(SPRITEBAKE_BIN) $< $@ - -$(SPRITEBAKE_BIN): FORCE - $(MAKE) -f $(REPO_DIR)/make/tools.mk spritebake-dos - -$(STAXIBAKE_BIN): FORCE - $(MAKE) -f $(REPO_DIR)/make/tools.mk staxibake - -# The cel bank is generated from the game's cel table (stCels.c). -$(STAXICELS_SPR_GEN): $(STAXIBAKE_BIN) $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stC64Data.c - @mkdir -p $(dir $@) - $(STAXIBAKE_BIN) $@ - -# Stage B: copy generated/ into the runtime tree beside the binary -$(STAXI_RUN_DIR)/levels/%.dat: $(STAXI_GEN_DIR)/levels/%.dat - @mkdir -p $(dir $@) - cp $< $@ - -$(STAXI_RUN_DIR)/%.spc: $(STAXI_GEN_DIR)/%.spc - @mkdir -p $(dir $@) - cp $< $@ - -$(MKSTLEVEL_BIN): FORCE - $(MAKE) -f $(REPO_DIR)/make/tools.mk mkstlevel - -$(DATA_DIR)/test.mod: $(REPO_DIR)/assets/test.mod - @mkdir -p $(DATA_DIR) - cp $< $@ - -$(DATA_DIR)/test.sfx: $(REPO_DIR)/assets/test.sfx - @mkdir -p $(DATA_DIR) - cp $< $@ - -$(TILEBANK_DATA): $(TILEBANK_BAKE_DEPS) - python3 $(TILEBANK_STAGE) $(PLATFORM) $(TILEBANK_PNG) $(dir $@) +.DEFAULT_GOAL := all +.PHONY: all dos clean-dos clean +all dos: $(LIB) $(LIBXMP_AR) programs clean-dos: rm -rf $(BUILD) @@ -342,28 +129,4 @@ clean-dos: # looks like success while leaving stale objects the next build links. clean: clean-dos -# Nuke regenerable Space Taxi artifacts. Sources under -# examples/spacetaxi/assets/ are untouched; everything under -# generated/ is recreated by the next build. -.PHONY: clean-spacetaxi -clean-spacetaxi: - find $(STAXI_GEN_DIR) -mindepth 1 ! -name '.gitignore' -delete 2>/dev/null || true - -# Pull in per-object header-dependency files generated by gcc -MMD/-MP. -# Without this, editing a header (e.g. surfaceInternal.h) doesn't rebuild -# the .c files that include it, leaving a frankenstein binary where -# different TUs see different struct layouts. --include $(LIB_OBJS:.o=.d) - -# The per-level cel banks, baked from each level's file. -$(STAXI_GEN_DIR)/sprites/level%.spr: $(STAXI_GEN_DIR)/levels/level%.dat $(STAXIBAKE_BIN) $(EXAMPLES)/spacetaxi/stLevelCels.c - @mkdir -p $(dir $@) - $(STAXIBAKE_BIN) --level $< $@ - -# Speech bake rules live down here so they never become the default goal. -$(STAXI_SPEECH_GEN): $(EXAMPLES)/spacetaxi/stSpeechData.c $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py - @mkdir -p $(dir $@) - python3 $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py $< $@ -$(STAXI_SPEECH_RUN): $(STAXI_SPEECH_GEN) - @mkdir -p $(dir $@) - cp $< $@ +endif diff --git a/make/dvx.mk b/make/dvx.mk index 330b4a8..5a2ee30 100644 --- a/make/dvx.mk +++ b/make/dvx.mk @@ -12,25 +12,19 @@ # LIBS = libjoey.a, DATA = its data files) and copied into build/dvx/bin/ # laid out as it goes into DVX: apps/joeylib//.app, .hlp # and DATA/. The gates and joey-run copy that tree into a DVX installation. +# +# This file holds the library and programRules, the DVX rules for one +# program, which make/programs.mk instantiates for every program (a project +# reaches both through make/project.mk). JoeyLib's own examples are at the +# end. +PLATFORM := dvx include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk -# The asset rules below define real targets before `all`, which would -# otherwise become make's default goal. -.DEFAULT_GOAL := all - $(call requireTool,$(DOS_CC),toolchain:dos) $(call requireTool,$(DVX_SDK)/sdk.mk,toolchain:dvx) -PLATFORM := dvx -BUILD := $(REPO_DIR)/build/$(PLATFORM) -LIBDIR := $(BUILD)/lib -BINDIR := $(BUILD)/bin -APPSDIR := $(BUILD)/apps -DATADIR := $(BUILD)/data - -# Where a program installs in DVX: apps///. -VENDOR := joeylib +APPSDIR := $(PROGRAM_BUILD)/apps PORT_DIR := $(SRC_DIR)/dvx @@ -52,7 +46,7 @@ LIB_CFLAGS := $(CFLAGS) $(PROGRAM_RENAMES) PORT_CFLAGS := $(CFLAGS) -I$(PORT_DIR) $(DVX_SDK_INCS) # The flags every program adds to sdk.mk's (which bring the SDK headers and -# -Werror), with the include folders every port gives its examples: the +# -Werror), with the include folders every port gives its programs: the # program's main becomes jlGameMain, which the port calls. APP_CFLAGS := -DJOEYLIB_PLATFORM_DVX -Dmain=jlGameMain $(PROGRAM_RENAMES) -fomit-frame-pointer -I$(INCLUDE_DIR) -I$(SRC_CORE) -I$(PORT_DIR) @@ -71,101 +65,6 @@ LIB_OBJS := \ LIB := $(LIBDIR)/libjoey.a -# The examples: for each, its sources, its window title and a line of help -# (no apostrophes: the help lines are written through the shell in quotes). -# Worth knowing which ones the gates use: -# UBER the golden-hash vehicle: a hash per public op, which must equal -# the DOS port's (tests/goldens/uber/dos.txt): the same stage. -# TILEBANK the tile-bank gate's program: the DOS bake loaded both ways and -# pixel-checked, every other target's bake refused. -# KEYS the input gate's program: typed characters to jlInputGetChar. -hello_SRCS := $(EXAMPLES)/hello/hello.c -hello_TITLE := Hello -hello_HELP := The smallest JoeyLib program: it clears the screen and waits for a key. -pattern_SRCS := $(EXAMPLES)/pattern/pattern.c -pattern_TITLE := Pattern -pattern_HELP := Eight bands of gradient palettes, one per band of scan lines. -draw_SRCS := $(EXAMPLES)/draw/draw.c -draw_TITLE := Draw -draw_HELP := The drawing primitives: pixels, lines, circles, rectangles and flood fills. -keys_SRCS := $(EXAMPLES)/keys/keys.c -keys_TITLE := Keys -keys_HELP := Keyboard, typed characters and mouse: held keys light up and typed text appears. Escape ends it. -serial_SRCS := $(EXAMPLES)/serial/serial.c -serial_TITLE := Serial -serial_HELP := A serial terminal on COM1. -sertest_SRCS := $(EXAMPLES)/sertest/sertest.c -sertest_TITLE := SerTest -sertest_HELP := The serial port test program. -save_SRCS := $(EXAMPLES)/save/save.c -save_TITLE := Save -save_HELP := Saved files: counts its runs in a file in its settings folder. -tilebank_SRCS := $(EXAMPLES)/tilebank/tilebank.c -tilebank_TITLE := TileBank -tilebank_HELP := The tile bank check: loads a baked tile bank two ways and checks every pixel. -joy_SRCS := $(EXAMPLES)/joy/joy.c -joy_TITLE := Joy -joy_HELP := Joysticks: shows the position and buttons of each joystick. -sprite_SRCS := $(EXAMPLES)/sprite/sprite.c -sprite_TITLE := Sprite -sprite_HELP := Compiled sprites moving over a background. -audio_SRCS := $(EXAMPLES)/audio/audio.c -audio_TITLE := Audio -audio_HELP := Sound: music, sound effects and tones. -uber_SRCS := $(EXAMPLES)/uber/uber.c -uber_TITLE := Uber -uber_HELP := The JoeyLib conformance and speed test: every drawing call, timed and checked. Its results go to joeylog.txt in its settings folder. -adv_SRCS := $(EXAMPLES)/adventure/adventure.c -adv_TITLE := Adventure -adv_HELP := A small adventure game. -adv2_SRCS := $(EXAMPLES)/adventure2/adventure2.c -adv2_TITLE := Adventure 2 -adv2_HELP := A second small adventure game. -agi_SRCS := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c -agi_TITLE := AGI -agi_HELP := A Sierra AGI interpreter. The files of the game it plays go in its DATA folder. -staxi_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stLevelFile.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stSim.c $(EXAMPLES)/spacetaxi/stFare.c $(EXAMPLES)/spacetaxi/stHooks.c $(EXAMPLES)/spacetaxi/stTitle.c $(EXAMPLES)/spacetaxi/stC64Data.c $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stAudio.c $(EXAMPLES)/spacetaxi/stLevelCels.c -staxi_TITLE := Space Taxi -staxi_HELP := Space Taxi: fly the taxi, pick up the fares, take them where they ask to go. - -APPS := hello pattern draw keys serial sertest save tilebank joy sprite audio uber adv adv2 agi staxi - -# UBER's probe build (part of all; scripts/verify-dvx-display.sh): UBER -# with its UBER_PROBE rows, the display parity row among them, as a program -# of its own packaged apart in build/dvx/probe, so neither bin/ (joey-run's -# tree) nor the goldened UBER changes. -DUBER_FRAMES=1 as the IIgs probe -# build: the run is wanted for its probe rows, not its timings. -# (Its name, like every program's, is a DOS 8.3 name: DVX runs on DOS.) -uberprob_SRCS := $(uber_SRCS) -uberprob_TITLE := Uber Probe -uberprob_HELP := UBER with its probe rows: the present checked against the stage on screen. Its results go to joeylog.txt in its settings folder. -uberprob_CFLAGS := -DUBER_PROBE -DUBER_FRAMES=1 -uberprob_BIN := $(BUILD)/probe -PROBE_APPS := uberprob - -# Data the examples read from DATA/ beside the program: AUDIO its music and -# sound effect; TILEBANK the DOS bake of the tile-bank fixture (the DOS -# target byte: DVX shows the DOS stage), every other target's bake it must -# refuse, and the expected pixels (common.mk); Space Taxi the DOS bake of -# its levels, compiled cel banks (x86 code, the DOS target byte) and speech, -# made by make/dos.mk -- only when the C64 image it comes from is present. -audio_DATA := $(DATADIR)/audio/DATA -tilebank_DATA := $(DATADIR)/tilebank/DATA -staxi_DATA := $(if $(HAVE_STAXI_DATA),$(DATADIR)/staxi/DATA) -AUDIO_DATA_FILES := $(audio_DATA)/test.mod $(audio_DATA)/test.sfx -TILEBANK_OWN := $(tilebank_DATA)/tilebank/own.tbk -DOS_STAXI_DATA := $(REPO_DIR)/build/dos/bin/STAXI/DATA - -.PHONY: all dvx dvx-lib dvx-apps dvx-probe clean-dvx clean $(addprefix app-,$(APPS) $(PROBE_APPS)) - -all dvx: dvx-lib dvx-apps dvx-probe - -dvx-lib: $(LIB) - -dvx-apps: $(addprefix app-,$(APPS)) - -dvx-probe: $(addprefix app-,$(PROBE_APPS)) - $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c @mkdir -p $(dir $@) $(DOS_CC) $(LIB_CFLAGS) -c $< -o $@ @@ -191,70 +90,94 @@ $(LIB): $(LIB_OBJS) rm -f $@ $(DOS_AR) rcs $@ $^ -# One program: (re)write its SDK project -- the Makefile, symlinks to its -# sources and the port's entry, the help source and the resource list -- -# then let sdk.mk build and package it, and copy the package into bin/. -# sdk.mk rebuilds only what changed; the project files are rewritten only -# when their contents change, so an unchanged program is not relinked. -# $(1) = the program's name; $(1)_CFLAGS, if any, are added to its flags, -# and $(1)_BIN, if set, is where it goes instead of bin/. -define appRule -app-$(1): $(LIB) $$($(1)_SRCS) $(PORT_ENTRY) $$(if $$($(1)_DATA),$(1)-data) - @mkdir -p $(APPSDIR)/$(1) - @for src in $$($(1)_SRCS) $(PORT_ENTRY); do ln -sfn $$$$src $(APPSDIR)/$(1)/$$$$(basename $$$$src); done +# Pull in per-object header-dependency files generated by gcc -MMD/-MP. +-include $(LIB_OBJS:.o=.d) + +# ---- programs ----------------------------------------------------------------- +# A program is a folder laid out as it installs in DVX, apps/// +# under bin/: .app, .hlp and its DATA/. The name is the id in lower +# case (DVX runs on DOS: an 8.3 name). DATA is staged apart, in +# build/dvx/data//DATA, for the SDK to package. +# DVX_VENDOR the vendor folder (default joeylib) +# DVX_KIND what the help and resource texts call a program: " is a +# <kind> program" (default JoeyLib) +# DVX_AUTHOR the resource list's author (default JoeyLib) +DVX_VENDOR := joeylib +DVX_KIND := JoeyLib +DVX_AUTHOR := JoeyLib +PROGRAM_EXT := .app +PROGRAM_DATA_APART := yes +programNameRule = $(call lc,$(1)) +programDir = $(or $(call programVar,$(1),BINDIR),$(BINDIR))/apps/$(DVX_VENDOR)/$(call programName,$(1)) +programTarget = app-$(call programName,$(1)) +programTitle = $(or $(call programVar,$(1),TITLE),$(call capFirst,$(call lc,$(1)))) + +# One program, $(1) its id, as the phony app-<name>: (re)write its SDK project +# in build/dvx/apps/<name>/ -- the Makefile, symlinks to its sources and the +# port's entry, the help source and the resource list -- then let sdk.mk build +# and package it, and copy the package into bin/ (or ID_BINDIR). sdk.mk +# rebuilds only what changed; the project files are rewritten only when their +# contents change, so an unchanged program is not relinked. Its CFLAGS are +# added to the program's flags. +programRules = $(call programDvxRules,$(1),$(call programName,$(1))) + +# $(1) the id, $(2) the name. Everything that may hold a comma (the title, +# the help line) is looked up here rather than passed in. +define programDvxRules +.PHONY: app-$(2) +app-$(2): $(LIB) $(call programVar,$(1),SRCS) $(PORT_ENTRY) $(call programVar,$(1),DATA) + @mkdir -p $(APPSDIR)/$(2) + @for src in $(call programVar,$(1),SRCS) $(PORT_ENTRY); do ln -sfn $$$$src $(APPSDIR)/$(2)/$$$$(basename $$$$src); done @printf '%s\n' \ - '# Generated by make/dvx.mk: the DVX SDK project of the JoeyLib example $(1).' \ + '# Generated by make/dvx.mk: the DVX SDK project of the $(DVX_KIND) $(2).' \ 'SDK_DIR = $(DVX_SDK)' \ 'KIND = app' \ - 'NAME = $(1)' \ - 'VENDOR = $(VENDOR)' \ + 'NAME = $(2)' \ + 'VENDOR = $(DVX_VENDOR)' \ 'LIBS = $(LIB)' \ - 'DATA = $$($(1)_DATA)' \ - "CFLAGS = $(APP_CFLAGS) $$($(1)_CFLAGS) -I$$(dir $$(firstword $$($(1)_SRCS))) -DDVX_PORT_APP_NAME='\"$$($(1)_TITLE)\"'" \ - 'include $$$$(SDK_DIR)/sdk.mk' | $(REPO_DIR)/make/support/dvx/writeIfChanged.sh $(APPSDIR)/$(1)/Makefile + 'DATA = $(if $(call programVar,$(1),DATA),$(call programDataDir,$(1)))' \ + "CFLAGS = $(APP_CFLAGS) $(call programVar,$(1),CFLAGS) -I$(dir $(firstword $(call programVar,$(1),SRCS))) -DDVX_PORT_APP_NAME='\"$(call programTitle,$(1))\"'" \ + 'include $$$$(SDK_DIR)/sdk.mk' | $(REPO_DIR)/make/support/dvx/writeIfChanged.sh $(APPSDIR)/$(2)/Makefile @printf '%s\n' \ - '.topic $(1).overview' \ - '.title $$($(1)_TITLE)' \ - '.toc 0 $$($(1)_TITLE)' \ + '.topic $(2).overview' \ + '.title $(call programTitle,$(1))' \ + '.toc 0 $(call programTitle,$(1))' \ '.default' \ - '.index $$($(1)_TITLE)' \ + '.index $(call programTitle,$(1))' \ '' \ - '.h1 $$($(1)_TITLE)' \ + '.h1 $(call programTitle,$(1))' \ '' \ - '$$($(1)_HELP)' \ + '$(call programVar,$(1),HELP)' \ '' \ - '$$($(1)_TITLE) is a JoeyLib example program. It runs in its own window at a virtual 70 frames a second; while the window is active every key goes to it except Alt+Tab (switch programs), Ctrl+Esc (Task Manager) and Alt+F4 (close it). Game > Exit also closes it.' \ + '$(call programTitle,$(1)) is a $(DVX_KIND) program. It runs in its own window at a virtual 70 frames a second; while the window is active every key goes to it except Alt+Tab (switch programs), Ctrl+Esc (Task Manager) and Alt+F4 (close it). Game > Exit also closes it.' \ '' \ 'Game > Full Screen, or Alt+Enter, puts it on the whole screen at 320x200 in 256 colours, and back in its window. It starts the way it was left. Alt+Tab or Ctrl+Esc brings the desktop back while it runs on; coming back to it gives it the whole screen again.' \ - | $(REPO_DIR)/make/support/dvx/writeIfChanged.sh $(APPSDIR)/$(1)/$(1).dhs + | $(REPO_DIR)/make/support/dvx/writeIfChanged.sh $(APPSDIR)/$(2)/$(2).dhs @printf '%s\n' \ - 'name text "$$($(1)_TITLE)"' \ - 'author text "JoeyLib"' \ - 'description text "JoeyLib example: $$($(1)_TITLE)"' \ - | $(REPO_DIR)/make/support/dvx/writeIfChanged.sh $(APPSDIR)/$(1)/$(1).res - $$(MAKE) -C $(APPSDIR)/$(1) DJGPP_PREFIX=$(DOS_PREFIX) package - @mkdir -p $$(or $$($(1)_BIN),$(BINDIR)) - rm -rf $$(or $$($(1)_BIN),$(BINDIR))/apps/$(VENDOR)/$(1) - cp -R $(APPSDIR)/$(1)/pkg/apps $$(or $$($(1)_BIN),$(BINDIR))/ + 'name text "$(call programTitle,$(1))"' \ + 'author text "$(DVX_AUTHOR)"' \ + 'description text "$(DVX_KIND): $(call programTitle,$(1))"' \ + | $(REPO_DIR)/make/support/dvx/writeIfChanged.sh $(APPSDIR)/$(2)/$(2).res + $$(MAKE) -C $(APPSDIR)/$(2) DJGPP_PREFIX=$(DOS_PREFIX) package + @mkdir -p $(or $(call programVar,$(1),BINDIR),$(BINDIR)) + rm -rf $(call programDir,$(1)) + cp -R $(APPSDIR)/$(2)/pkg/apps $(or $(call programVar,$(1),BINDIR),$(BINDIR))/ endef -$(foreach a,$(APPS) $(PROBE_APPS),$(eval $(call appRule,$(a)))) -.PHONY: audio-data tilebank-data staxi-data -audio-data: $(AUDIO_DATA_FILES) -tilebank-data: $(TILEBANK_OWN) +# ---- JoeyLib's own build: its examples ------------------------------------------ +ifndef JOEY_PROJECT -staxi-data: - $(MAKE) -f $(REPO_DIR)/make/dos.mk dos-staxi-data - rm -rf $(staxi_DATA) - @mkdir -p $(dir $(staxi_DATA)) - cp -R $(DOS_STAXI_DATA) $(staxi_DATA) +include $(REPO_DIR)/make/examples.mk +include $(REPO_DIR)/make/programs.mk -$(audio_DATA)/%: $(REPO_DIR)/assets/% - @mkdir -p $(dir $@) - cp $< $@ +# UBER's probe build is part of `all` (scripts/verify-dvx-display.sh). +.DEFAULT_GOAL := all +.PHONY: all dvx dvx-lib dvx-probe clean-dvx clean +all dvx: dvx-lib programs dvx-probe -$(TILEBANK_OWN): $(TILEBANK_BAKE_DEPS) - python3 $(TILEBANK_STAGE) dos $(TILEBANK_PNG) $(dir $@) +dvx-lib: $(LIB) + +dvx-probe: $(call programTarget,UBERPROB) clean-dvx: rm -rf $(BUILD) @@ -264,5 +187,4 @@ clean-dvx: # looks like success while leaving stale objects the next build links. clean: clean-dvx -# Pull in per-object header-dependency files generated by gcc -MMD/-MP. --include $(LIB_OBJS:.o=.d) +endif diff --git a/make/examples.mk b/make/examples.mk new file mode 100644 index 0000000..7fb02d2 --- /dev/null +++ b/make/examples.mk @@ -0,0 +1,164 @@ +# JoeyLib's own example programs, described for make/programs.mk (which +# lists what each variable means). Every port's makefile includes this file, +# then make/programs.mk, which makes each port's rules for them. +# +# Worth knowing which ones the gates use: +# UBER the golden-hash vehicle: every public op, a hash per op, which +# tools/diff-uber-hashes compares against tests/goldens/uber (the +# X68000 against the IIgs's, DVX against the DOS port's: the same +# stage). UBERPROB is UBER with its UBER_PROBE rows. +# PATTERN the SCB acceptance vehicle: 8 bands of 25 lines with 8 gradient +# palettes, so a misplaced band boundary is immediately visible. +# AUDIO every audio path: chip music with noise drums, MOD and SFX +# (ADPCM on the X68000), a raw-format stream and a noise sweep. +# AUDIOPRB is AUDIO pressing its own keys, for the IIgs and +# X68000 gates. +# KEYS the input and mouse vehicle: key predicates, the typed line +# (jlInputGetChar) and its verification row. +# SERIAL the serial gates' echo program. +# TILEBANK the tile-bank gates' program: this port's .tbk bake loaded both +# ways and pixel-checked, every other target's bake refused. +# SERMOUSE X68000: the serial + mouse gate's program: streams both ways on +# the RS-232C port (SCC channel A) while the mouse (channel B) +# moves. +# SCB X68000: the SCB gate's program: all 16 palettes, 147 palette +# changes a frame (every line for 128 of them), for the raster +# interrupt that reloads the text plane's one palette as the beam +# scans. + +PROGRAMS := HELLO PATTERN DRAW KEYS SERIAL SERTEST SERMOUSE SAVE TILEBANK SCB JOY SPRITE AUDIO UBER ADV ADV2 +TEST_PROGRAMS := SCCPROBE SERLOSS SERLOSSS DIRTYMRK UBERPROB AUDIOPRB + +# DVX's help and resource texts call these JoeyLib examples. +DVX_KIND := JoeyLib example + +# The IIgs build has never included HELLO. +HELLO_SRCS := $(EXAMPLES)/hello/hello.c +HELLO_PORTS := amiga atarist dos x68000 dvx +HELLO_TITLE := Hello +HELLO_HELP := The smallest JoeyLib program: it clears the screen and waits for a key. +PATTERN_SRCS := $(EXAMPLES)/pattern/pattern.c +PATTERN_TITLE := Pattern +PATTERN_HELP := Eight bands of gradient palettes, one per band of scan lines. +DRAW_SRCS := $(EXAMPLES)/draw/draw.c +DRAW_TITLE := Draw +DRAW_HELP := The drawing primitives: pixels, lines, circles, rectangles and flood fills. +KEYS_SRCS := $(EXAMPLES)/keys/keys.c +KEYS_TITLE := Keys +KEYS_HELP := Keyboard, typed characters and mouse: held keys light up and typed text appears. Escape ends it. +SERIAL_SRCS := $(EXAMPLES)/serial/serial.c +SERIAL_TITLE := Serial +SERIAL_HELP := A serial terminal on COM1. +SERTEST_SRCS := $(EXAMPLES)/sertest/sertest.c +SERTEST_TITLE := SerTest +SERTEST_HELP := The serial port test program. +SERTEST_AMIGA_NAME := SerTest +SERMOUSE_SRCS := $(EXAMPLES)/sermouse/sermouse.c +SERMOUSE_PORTS := x68000 +SAVE_SRCS := $(EXAMPLES)/save/save.c +SAVE_TITLE := Save +SAVE_HELP := Saved files: counts its runs in a file in its settings folder. +TILEBANK_SRCS := $(EXAMPLES)/tilebank/tilebank.c +TILEBANK_TITLE := TileBank +TILEBANK_HELP := The tile bank check: loads a baked tile bank two ways and checks every pixel. +TILEBANK_AMIGA_NAME := TileBank +SCB_SRCS := $(EXAMPLES)/scb/scb.c +SCB_PORTS := x68000 +JOY_SRCS := $(EXAMPLES)/joy/joy.c +JOY_TITLE := Joy +JOY_HELP := Joysticks: shows the position and buttons of each joystick. +SPRITE_SRCS := $(EXAMPLES)/sprite/sprite.c +SPRITE_TITLE := Sprite +SPRITE_HELP := Compiled sprites moving over a background. +AUDIO_SRCS := $(EXAMPLES)/audio/audio.c +AUDIO_TITLE := Audio +AUDIO_HELP := Sound: tracker music, a MOD, sound effects, a streamed tone and a noise sweep. +UBER_SRCS := $(EXAMPLES)/uber/uber.c +UBER_TITLE := Uber +UBER_HELP := The JoeyLib conformance and speed test: every drawing call, timed and checked. Its results go to joeylog.txt in its settings folder. +ADV_SRCS := $(EXAMPLES)/adventure/adventure.c +ADV_TITLE := Adventure +ADV_HELP := A small adventure game. +ADV2_SRCS := $(EXAMPLES)/adventure2/adventure2.c +ADV2_TITLE := Adventure 2 +ADV2_HELP := A second small adventure game. + +# Every example that ships as its own bootable IIgs volume (make +# iigs-game-disks). +$(foreach p,DRAW PATTERN KEYS JOY SPRITE SAVE AUDIO ADV ADV2 UBER SERIAL SERTEST,$(eval $(p)_IIGS_GAME_DISK := yes)) + +# IIgs serial probes for the gates: SCCPROBE (verify-iigs-serial.sh: does +# opening the modem port leave the printer port, AppleTalk's channel, alive?) +# and the receive-loss probes for the modem port and a Super Serial Card in +# slot 2 (verify-iigs-serial-loss.sh). The card runs at SERLOSS_SSC_BAUD: +# its 6551 holds ONE received byte, so it is lossless through full-screen +# presents only up to 2400 (docs/ApiSerial.adoc); at 9600 that half of the +# gate could never pass. +SERLOSS_SSC_BAUD := 2400 +SCCPROBE_SRCS := $(EXAMPLES)/sccprobe/sccprobe.c +SCCPROBE_PORTS := iigs +SERLOSS_SRCS := $(EXAMPLES)/serloss/serloss.c +SERLOSS_PORTS := iigs +SERLOSSS_SRCS := $(SERLOSS_SRCS) +SERLOSSS_PORTS := iigs +SERLOSSS_CFLAGS := -DSERLOSS_SLOT=2 -DSERLOSS_BAUD=$(SERLOSS_SSC_BAUD)u + +# The dirty-band marker's on-target check: tests/host/dirtyMarkHost.c against +# src/m68k/stageBands.s, the asm the Amiga and X68000 share. Not a shipped +# program, so it builds beside bin/; scripts/verify-atarist-dirtymark.sh runs +# it under Hatari. The host check runs 200,000 scenarios; this many takes +# under a minute of emulated time. +DIRTYMARK_SCENARIOS := 4000ul +DIRTYMARK_LOG := DIRTYMRK.TXT +DIRTYMRK_SRCS := $(REPO_DIR)/tests/host/dirtyMarkHost.c +DIRTYMRK_PORTS := atarist +DIRTYMRK_BINDIR := $(PROGRAM_BUILD)/test +DIRTYMRK_CFLAGS := -DDIRTY_MARK_SCENARIOS=$(DIRTYMARK_SCENARIOS) -DDIRTY_MARK_LOG='"$(DIRTYMARK_LOG)"' + +# UBER with its UBER_PROBE rows (the present checked against the stage), +# built apart so neither bin/ (joey-run's tree) nor the goldened UBER +# changes. -DUBER_FRAMES=1: the run is wanted for its probe rows, not its +# timings, and a probe run at the golden 16 frames does not fit +# bench-iigs.sh's MAME cap. The IIgs packs it as UBER, the name its disks +# and bench-iigs.sh look for (iigs-verify-shrtail); DVX builds it as part of +# `all` (scripts/verify-dvx-display.sh), packaged in build/dvx/probe. +UBERPROB_SRCS := $(UBER_SRCS) +UBERPROB_PORTS := iigs dvx +UBERPROB_CFLAGS := -DUBER_PROBE -DUBER_FRAMES=1 +UBERPROB_TITLE := Uber Probe +UBERPROB_HELP := UBER with its probe rows: the present checked against the stage on screen. Its results go to joeylog.txt in its settings folder. +UBERPROB_IIGS_NAME := UBER +UBERPROB_IIGS_BINDIR := $(PROGRAM_BUILD)/probe/bin +UBERPROB_DVX_BINDIR := $(PROGRAM_BUILD)/probe + +# AUDIO with its keys pressed on a timetable (-DAUDIO_PROBE): N once, S every +# 1.5 seconds, under the tracker song. The IIgs and X68000 audio gates read +# their sound chips under MAME and cannot type, so this is their subject +# (iigs-verify-audio, x68000-verify-audio). Built apart, so neither bin/ nor +# the shipped AUDIO changes; it needs no DATA. +AUDIOPRB_SRCS := $(AUDIO_SRCS) +AUDIOPRB_PORTS := iigs x68000 +AUDIOPRB_CFLAGS := -DAUDIO_PROBE +AUDIOPRB_BINDIR := $(PROGRAM_BUILD)/probe +AUDIOPRB_IIGS_BINDIR := $(PROGRAM_BUILD)/probe/bin + +# AUDIO loads DATA/test.mod and DATA/test.sfx from beside the program. The +# IIgs plays its chip music through NTP and stages no MOD. +AUDIO_DATA := $(addprefix $(call programDataDir,AUDIO)/,test.mod test.sfx) +AUDIO_IIGS_DATA := + +$(call programDataDir,AUDIO)/%: $(REPO_DIR)/assets/% + @mkdir -p $(dir $@) + cp $< $@ + +# TILEBANK loads the host gate's fixture, so the host +# (scripts/check-tilebank.sh) and every port share one expected hash. Its +# DATA/tilebank holds this port's bake of it, every other target's bake (each +# must be refused) and the expected pixels. +TILEBANK_PNG := $(REPO_DIR)/tests/host/tileBank.png +TILEBANK_STAGE := $(REPO_DIR)/tests/host/tileBankStage.py +TILEBANK_BAKE_DEPS := $(TILEBANK_PNG) $(TILEBANK_STAGE) $(REPO_DIR)/tools/assetbake/assetbake.py +TILEBANK_DATA := $(call programDataDir,TILEBANK)/tilebank/own.tbk + +$(TILEBANK_DATA): $(TILEBANK_BAKE_DEPS) + python3 $(TILEBANK_STAGE) $(ASSET_PLATFORM) $(TILEBANK_PNG) $(dir $@) diff --git a/make/iigs.mk b/make/iigs.mk index 13d2481..c7684e7 100644 --- a/make/iigs.mk +++ b/make/iigs.mk @@ -13,12 +13,16 @@ # validates the full clang -> llvm-mc -> link816 -> MAME path end to end. Per-example binary targets build via # clang-build.sh; large examples need the multi-segment OMF wiring still # in progress. - -include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk +# +# This file holds JoeyLib's library for the IIgs, programRules, the IIgs +# rules for one program, which make/programs.mk instantiates for every +# program, and the disk images made of programs (a project reaches all three +# through make/project.mk). JoeyLib's own examples and the IIgs gates are at +# the end. PLATFORM := iigs -BUILD := $(REPO_DIR)/build/$(PLATFORM) -BINDIR := $(BUILD)/bin +include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk + OBJDIR := $(BUILD)/obj CLANG := $(LLVM816_ROOT)/tools/llvm-mos-build/bin/clang @@ -31,7 +35,6 @@ CCWRAP := $(LLVM816_ROOT)/scripts/ccRegallocFallback.sh RT_INC := $(LLVM816_ROOT)/runtime/include CFLAGS := --target=w65816 -O2 -ffreestanding -ffunction-sections $(EXTRA_CFLAGS) INCLUDES := -I$(RT_INC) -I$(INCLUDE_DIR) -I$(INCLUDE_DIR)/joey -I$(SRC_CORE) -I$(REPO_DIR)/src/codegen -LIBDIR := $(BUILD)/lib LIB := $(LIBDIR)/libjoey.a PORT_C_SRCS := $(wildcard $(SRC_DIR)/iigs/*.c) @@ -55,239 +58,16 @@ LIB_SRCS := $(LIB_C_SRCS) $(PORT_ASM_SRCS) # missed one is not). IIGS_LIB_HDRS := $(wildcard $(INCLUDE_DIR)/joey/*.h) $(wildcard $(SRC_CORE)/*.h) $(wildcard $(REPO_DIR)/src/codegen/*.h) $(wildcard $(SRC_DIR)/iigs/*.h) $(wildcard $(REPO_DIR)/src/m68k/*.h) -# Example sources (single-TU unless noted). -PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c -DRAW_SRC := $(EXAMPLES)/draw/draw.c -KEYS_SRC := $(EXAMPLES)/keys/keys.c -SERIAL_SRC := $(EXAMPLES)/serial/serial.c -SERTEST_SRC := $(EXAMPLES)/sertest/sertest.c -SCCPROBE_SRC := $(EXAMPLES)/sccprobe/sccprobe.c -SERLOSS_SRC := $(EXAMPLES)/serloss/serloss.c -SAVE_SRC := $(EXAMPLES)/save/save.c -TILEBANK_SRC := $(EXAMPLES)/tilebank/tilebank.c -JOY_SRC := $(EXAMPLES)/joy/joy.c -SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c -UBER_SRC := $(EXAMPLES)/uber/uber.c -ADV_SRC := $(EXAMPLES)/adventure/adventure.c -ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c -AGI_SRCS := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c -STAXI_SRCS := $(EXAMPLES)/spacetaxi/stC64Data.c $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stSim.c $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stLevelFile.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stFare.c $(EXAMPLES)/spacetaxi/stHooks.c $(EXAMPLES)/spacetaxi/stTitle.c $(EXAMPLES)/spacetaxi/stAudio.c $(EXAMPLES)/spacetaxi/stLevelCels.c -AUDIO_SRC := $(EXAMPLES)/audio/audio.c - -# TILEBANK's DATA/tilebank: this port's bake of the tile-bank fixture, the -# other targets' bakes it must refuse, and the expected pixels (common.mk). -# make-iigs-disk.sh packs it as /JOEYLIB/DATA/TILEBANK beside the program. -TILEBANK_DATA := $(BINDIR)/DATA/tilebank/own.tbk - # NinjaTrackerPlus replayer: Merlin32-assemble ninjatrackerplus.s to a 34KB # binary that ships next to the program; jlpAudioInit loads "ntpplayer" from -# disk at run time (see the $(BINDIR)/AUDIO rule). The source is assembled +# disk at run time (src/iigs/audio_full.c NTP_PATH), so every disk a program +# goes on carries it (make-iigs-disk.sh). The source is assembled # with JoeyLib's interrupt window added to NTPstreamsound (NTP_PATCH). NTP_SRC := $(IIGS_NTP_DIR)/ninjatrackerplus.s NTP_PATCH := $(REPO_DIR)/make/support/iigs/ntpIrqWindow.py NTP_BIN := $(BUILD)/audio/ntpplayer.bin IIGS_MERLIN := $(IIGS_AS) -.PHONY: all iigs iigs-lib iigs-clang-smoke iigs-examples iigs-disk iigs-disk-all iigs-game-disk iigs-game-disks iigs-game-disk-agi iigs-verify-game-disk iigs-verify iigs-verify-all iigs-verify-save iigs-verify-serial iigs-verify-serial-loss iigs-verify-shrtail iigs-verify-tilebank clean-iigs clean - -# Default: build (compile-check) the library. The MAME smoke test is -# `iigs-clang-smoke`; disks and examples are `iigs-disk`. -all iigs: iigs-lib - -# Per-example launchable binaries. STAXI and AUDIO are INCLUDED: the -# old exclusion comment ("blocked on deeper toolchain features") went -# stale -- STAXI has linked and run since 2026-07-08 (toolchain #77 -# fix), and AUDIO's 34KB-rodata blocker was resolved by loading the -# NTP replayer from disk (see the $(BINDIR)/AUDIO rule). Leaving them -# out made `make iigs` silently test 8-day-old STAXI binaries against -# new assets (the 2026-07-20 gear-out/no-flame reports). TILEBANK's staged -# DATA rides along, so every disk that packs TILEBANK finds it. -iigs-examples: $(BINDIR)/PATTERN $(BINDIR)/DRAW $(BINDIR)/KEYS $(BINDIR)/SERIAL $(BINDIR)/SERTEST $(BINDIR)/SAVE $(BINDIR)/JOY \ - $(BINDIR)/SPRITE $(BINDIR)/UBER $(BINDIR)/ADV $(BINDIR)/ADV2 \ - $(BINDIR)/AGI $(BINDIR)/STAXI $(BINDIR)/AUDIO $(BINDIR)/TILEBANK $(TILEBANK_DATA) - -# Build the JOEYLIB data disk (build/iigs/bin/joey.2mg) that the gates, and -# `joey-run build/iigs/bin/joey.2mg`, mount beside GS/OS. Packs the launchable examples as ProDOS S16 apps -# (an 800KB floppy can't hold every example; see make-iigs-disk.sh). -# NTP_BIN rides along so jlpAudioInit's runtime load of "ntpplayer" finds it -# on the volume -- without it every example logs "audioInit failed" (Phase 1 -# IIgs stabilization item c: the audio code is wired; the file was missing). -# Pre-compiled sprite banks for Space Taxi. Unlike the other ports, the IIgs -# asset tree (examples/spacetaxi/generated/iigs) is produced out-of-band, so -# we bake the .spc straight from the committed .spr with the host spritebake -# tool; make-iigs-disk.sh then find(1)s the whole generated/iigs tree, so the -# .spc rides onto the disk automatically once it exists. -# The cel bank is GENERATED by staxibake from the game's shared cel table -# (stCels.c) + C64 bitmaps, then compiled to a .spc by the host -# spritebake; make-iigs-disk.sh find(1)s the whole generated/iigs tree so -# the .spc rides onto the disk. The game loads it at boot -# (jlSpriteBankLoadPrecompiled) instead of JIT-compiling every cel. -STAXICELS_SPR := $(EXAMPLES)/spacetaxi/generated/iigs/sprites/staxicels.spr -STAXI_IIGS_SPC := $(STAXICELS_SPR:.spr=.spc) -# Per-level hook-sprite banks (see make/dos.mk): baked from the level file -# by staxibake --level, pre-compiled for the 65816. make-iigs-disk.sh copies -# them onto the floppy last and only while there is room; a missing bank -# makes the game JIT-compile that level's cels at entry instead. -STAXI_CEL_LEVELS := 07 08 09 10 12 16 17 21 -STAXILEVEL_IIGS_SPC := $(foreach n,$(STAXI_CEL_LEVELS),$(EXAMPLES)/spacetaxi/generated/iigs/sprites/level$(n).spc) -# Speech baked to 1-bit PCM per passenger pitch (assets/genSpeechPcm.py); -# make-iigs-disk.sh picks it up from the generated tree. -STAXI_IIGS_SPEECH := $(EXAMPLES)/spacetaxi/generated/iigs/speech.bin -$(STAXI_IIGS_SPEECH): $(EXAMPLES)/spacetaxi/stSpeechData.c $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py - @mkdir -p $(dir $@) - python3 $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py $< $@ --biased -# Level files: extracted from the C64 game image exactly as on the other -# ports (see make/dos.mk), then found by make-iigs-disk.sh in the tree. -STAXI_IIGS_LEVELS := $(foreach n,$(STAXI_LEVEL_NUMS),$(EXAMPLES)/spacetaxi/generated/iigs/levels/level$(n).dat) \ - $(EXAMPLES)/spacetaxi/generated/iigs/levels/title.dat -$(STAXI_IIGS_LEVELS) &: $(STAXI_RAW_DUMP) $(ROMTOLEVEL_BIN) - @mkdir -p $(EXAMPLES)/spacetaxi/generated/iigs/levels - python3 $(ROMTOLEVEL_BIN) $(STAXI_RAW_DUMP) $(EXAMPLES)/spacetaxi/generated/iigs/levels -SPRITEBAKE_IIGS := $(REPO_DIR)/build/tools/spritebake-iigs -STAXIBAKE := $(REPO_DIR)/build/tools/staxibake - -$(SPRITEBAKE_IIGS): FORCE - $(MAKE) -f $(REPO_DIR)/make/tools.mk spritebake-iigs - -$(STAXIBAKE): FORCE - $(MAKE) -f $(REPO_DIR)/make/tools.mk staxibake - -$(STAXICELS_SPR): $(STAXIBAKE) $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stC64Data.c - @mkdir -p $(dir $@) - $(STAXIBAKE) $@ - -$(EXAMPLES)/spacetaxi/generated/iigs/sprites/level%.spr: $(EXAMPLES)/spacetaxi/generated/iigs/levels/level%.dat $(STAXIBAKE) $(EXAMPLES)/spacetaxi/stLevelCels.c - @mkdir -p $(dir $@) - $(STAXIBAKE) --level $< $@ - -$(EXAMPLES)/spacetaxi/generated/iigs/sprites/%.spc: $(EXAMPLES)/spacetaxi/generated/iigs/sprites/%.spr $(SPRITEBAKE_IIGS) - $(SPRITEBAKE_IIGS) $< $@ - -iigs-disk: iigs-examples $(NTP_BIN) $(STAXI_IIGS_SPC) $(call staxiData,$(STAXI_IIGS_LEVELS) $(STAXILEVEL_IIGS_SPC)) $(STAXI_IIGS_SPEECH) - BINDIR="$(BINDIR)" NTP_BIN="$(NTP_BIN)" $(REPO_DIR)/scripts/make-iigs-disk.sh $(BINDIR)/joey.2mg - -# ONE self-booting volume per game: a GS/OS system stripped to what JoeyLib -# actually uses, a launcher in place of the Finder, and the program in its own -# folder so that folder can be copied to a hard drive and run from there. -# Boot to the program is 21.8 s against 43.4 s for the two-floppy boot disk. -# The size is chosen per example: a program that fits beside a stripped -# system gets a bootable 800KB 3.5" floppy, and one that does not (STAXI, -# AGI) gets a 2048KB volume, which MAME can only boot off a CFFA2 card -# because its 3.5 drive is 800KB GCR: -# mame apple2gs -sl7 cffa2 -hard1 build/iigs/bin/STAXI.2mg -# Pass GAME=name for a different example, GAME_DISK_SIZE to force a size. -# It builds its OWN one-example data image rather than depending on -# iigs-disk: the default joey.2mg carries the small examples and NOT STAXI -# (see make-iigs-disk.sh's JOEY_DISK_EXAMPLES), and the gates depend on -# joey.2mg's exact contents, so this must not rewrite it. -GAME ?= STAXI -iigs-game-disk: iigs-examples $(NTP_BIN) $(STAXI_IIGS_SPC) $(call staxiData,$(STAXI_IIGS_LEVELS) $(STAXILEVEL_IIGS_SPC)) $(STAXI_IIGS_SPEECH) - BINDIR="$(BINDIR)" NTP_BIN="$(NTP_BIN)" JOEY_DISK_EXAMPLES="$(GAME)" \ - $(REPO_DIR)/scripts/make-iigs-disk.sh $(BINDIR)/$(GAME)-data.2mg - JOEY_GAME_DISK_SIZE="$(or $(GAME_DISK_SIZE),auto)" \ - $(REPO_DIR)/scripts/make-iigs-game-disk.sh $(BINDIR)/$(GAME).2mg $(GAME) $(GAME) $(BINDIR)/$(GAME)-data.2mg - -# Every example that ships as its own bootable volume. AGI is excluded -# because its resources come from a different builder (see below). -IIGS_GAME_DISKS := DRAW PATTERN KEYS JOY SPRITE SAVE AUDIO ADV ADV2 UBER SERIAL SERTEST STAXI - -iigs-game-disks: - @for g in $(IIGS_GAME_DISKS); do \ - $(MAKE) --no-print-directory -f make/iigs.mk iigs-game-disk GAME=$$g || exit 1; \ - done - -# AGI needs King's Quest III's resources, which make-agi-iigs-disk.sh lays -# out, so it cannot use the generic one-example data image. It also wants a -# bigger Memory Manager pool: run it with -ramsize 8M. -AGI_GAME ?= kq3 -iigs-game-disk-agi: $(BINDIR)/AGI - $(REPO_DIR)/scripts/make-agi-iigs-disk.sh $(BINDIR)/AGI-data.2mg $(AGI_GAME) - $(REPO_DIR)/scripts/make-iigs-game-disk.sh $(BINDIR)/AGI.2mg AGI AGI $(BINDIR)/AGI-data.2mg - -# Boot one game disk headlessly and assert it reaches the program and draws. -# An 800KB image goes in the 3.5 drive, a larger one on a CFFA2 card. -iigs-verify-game-disk: iigs-game-disk - $(REPO_DIR)/scripts/verify-iigs-game-disk.sh $(BINDIR)/$(GAME).2mg - -# Every launchable example on one image. The full set (~1.7 MB of binaries -# plus STAXI's data) will not fit an 800KB 3.5" floppy, so this is a 16 MB -# ProDOS hard-disk image (joey-all.2mg): boot it in MAME with -# `-sl7 cffa2 -hard1 <GS/OS> -hard2 build/iigs/bin/joey-all.2mg` and launch -# any example from the JOEYLIB volume in the Finder. -iigs-disk-all: iigs-examples $(NTP_BIN) $(STAXI_IIGS_SPC) $(call staxiData,$(STAXI_IIGS_LEVELS) $(STAXILEVEL_IIGS_SPC)) $(STAXI_IIGS_SPEECH) - BINDIR="$(BINDIR)" NTP_BIN="$(NTP_BIN)" \ - JOEY_DISK_SIZE="16MB" \ - JOEY_DISK_EXAMPLES="DRAW PATTERN KEYS JOY SPRITE SERIAL SERTEST SAVE TILEBANK AUDIO UBER ADV2 ADV AGI STAXI" \ - $(REPO_DIR)/scripts/make-iigs-disk.sh $(BINDIR)/joey-all.2mg - -# Headless visual-verification gate: boot GS/OS under MAME, launch the DRAW -# example off joey.2mg, and assert the SHR framebuffer was actually rendered -# (many distinct colors -> pixels/lines/circles/tiles/flood all drew). Pass -# VERIFY_EXAMPLE=name to check a different example. -iigs-verify: iigs-disk - $(REPO_DIR)/scripts/verify-iigs.sh $(or $(VERIFY_EXAMPLE),draw) - -# Cross-channel SCC gate: opening the modem port must leave the printer channel -# (AppleTalk's) transmitting. Builds its own one-example disk; ~2 min. -iigs-verify-serial: $(BINDIR)/SCCPROBE - $(REPO_DIR)/scripts/verify-iigs-serial.sh - -# Receive-loss gate: 4096 bytes streamed at 9600 baud while the program does -# nothing but full-screen presents must all arrive. Runs the modem port (SCC) -# and a Super Serial Card in slot 2 (6551) on the installed MAME, with the SCC -# receive FIFO cut to the real chip's 3 bytes (JOEY_Z80SCC_RX_FIFO=3) and the -# card's a2ssc firmware from iigsFirmware; see the script header. ~3 min per mode. -# The card runs at SERLOSS_SSC_BAUD: its 6551 holds ONE received byte, so it is -# lossless through full-screen presents only up to 2400 (docs/ApiSerial.adoc); -# at 9600 this half could never pass. -SERLOSS_SSC_BAUD := 2400 -iigs-verify-serial-loss: $(BINDIR)/SERLOSS $(BINDIR)/SERLOSSS - $(REPO_DIR)/scripts/verify-iigs-serial-loss.sh scc - SERLOSS_BAUD=$(SERLOSS_SSC_BAUD) $(REPO_DIR)/scripts/verify-iigs-serial-loss.sh ssc - -# Boot-verify every launchable example on joey.2mg (per-example thresholds). -iigs-verify-all: iigs-disk - $(REPO_DIR)/scripts/verify-iigs-all.sh - -# Gate the 712 display-state bytes ABOVE the pixel block: the SCB array -# ($E1:9D00-$9DC7) and all 16 palettes ($E1:9E00-$9FFF). Nothing else in the -# tree can see them -- the golden hash covers the STAGE, not $E1, and P9's -# pixel compare stops at $9CFF -- so a wrong palette is invisible to every -# other gate and shows only as wrong colors on a real screen. That blind spot -# is why a phantom "PEI-slam corrupts palettes 1-15" note survived five weeks. -# -# UBER's shrTailUpload / shrTailSlam rows live behind #ifdef UBER_PROBE, which -# no normal build defines, so this target builds its own UBER (never the -# goldened one) into a scratch dir, packs a single-app disk, and benches that. -# -DUBER_FRAMES=1 shrinks the timing window: a -DUBER_PROBE run at the golden -# 16 frames does not fit bench-iigs.sh's MAME cap. The goldened UBER binary, -# joey.2mg and joeylog.txt are all left untouched. -IIGS_PROBE_DIR := $(BUILD)/probe - -iigs-verify-shrtail: $(LIB) $(NTP_BIN) - @mkdir -p $(IIGS_PROBE_DIR)/bin - $(IIGS_CLANG_BUILD) -DUBER_PROBE -DUBER_FRAMES=1 $(INCLUDES) -o $(IIGS_PROBE_DIR)/bin/UBER $(UBER_SRC) $(LIB) - BINDIR="$(IIGS_PROBE_DIR)/bin" NTP_BIN="$(NTP_BIN)" JOEY_DISK_EXAMPLES="UBER" \ - $(REPO_DIR)/scripts/make-iigs-disk.sh $(IIGS_PROBE_DIR)/uberprobe.2mg - BENCH_DATA_DISK="$(IIGS_PROBE_DIR)/uberprobe.2mg" BENCH_OUT_LOG="$(IIGS_PROBE_DIR)/probelog.txt" \ - BENCH_MAX_FRAMES="$(or $(PROBE_MAX_FRAMES),45000)" $(REPO_DIR)/scripts/bench-iigs.sh - @grep -E 'shrParity|shrTailUpload|shrTailSlam' $(IIGS_PROBE_DIR)/probelog.txt || \ - { echo "iigs-verify-shrtail: FAIL - no probe rows captured" >&2; exit 1; } - @grep -qE 'shrTail(Upload|Slam) FAIL' $(IIGS_PROBE_DIR)/probelog.txt && \ - { echo "iigs-verify-shrtail: FAIL - \$$E1 SCB/palette block mismatched the stage" >&2; exit 1; } || true - @echo "iigs-verify-shrtail: PASS" - -# Tile-bank gate: TILEBANK loads this port's own .tbk bake both ways off its -# own one-example disk; both must hash like the host gate's, and every other -# target's bake must be refused. NTP_BIN keeps the disk's audio init quiet. -iigs-verify-tilebank: $(BINDIR)/TILEBANK $(TILEBANK_DATA) $(NTP_BIN) - $(REPO_DIR)/scripts/verify-iigs-tilebank.sh - -# Runtime gate for the save HAL's GS/OS calls (Create + GetDevNumber/DInfo/ -# Volume): boots SAVE under MAME and checks the save-OK lamp + disk-free bar. -iigs-verify-save: $(BINDIR)/SAVE $(BINDIR)/DRAW - $(REPO_DIR)/scripts/verify-iigs-save.sh - $(NTP_BIN): $(NTP_SRC) $(NTP_PATCH) $(IIGS_MERLIN) @mkdir -p $(BUILD)/audio @python3 $(NTP_PATCH) $(NTP_SRC) $(BUILD)/audio/ninjatrackerplus.s @@ -323,116 +103,175 @@ $(LIB): $(LIB_SRCS) $(IIGS_LIB_HDRS) iigs-lib: $(LIB) +# ---- programs ----------------------------------------------------------------- +# A program is an S16 OMF named NAME, in bin/. It runs off a ProDOS volume: +# make-iigs-disk.sh packs programs, NTP and their DATA onto a data disk, and +# make-iigs-game-disk.sh one program into a bootable volume (below). DATA is +# staged apart for that, in build/iigs/data/NAME/DATA, and the optional files +# in build/iigs/data/NAME/OPTIONAL (make/common.mk's programOptionalDir). +PROGRAM_EXT := +PROGRAM_DATA_APART := yes +PROGRAM_OPTIONAL_APART := yes +DEP_DIR := $(PROGRAM_BUILD)/dep + +# One program, $(1) its id: clang-build.sh compiles, links and wraps it, and +# writes one header-dependency file for all its sources. ID_IIGS_OVERFLOW_CAP +# opts into large overflow segments (see clang-build.sh). NTP rides on every +# disk a program goes on, so it is built first. +define programRules +$(if $(call programVar,$(1),OVERFLOW_CAP),$(call programBin,$(1)): export JOEY_OVERFLOW_CAP := $(call programVar,$(1),OVERFLOW_CAP)) +$(call programBin,$(1)): $(call programVar,$(1),SRCS) $(LIB) $(IIGS_CLANG_BUILD) | $(NTP_BIN) + @mkdir -p $$(dir $$@) $(DEP_DIR) + $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/$(1).d $(call programVar,$(1),CFLAGS) $(INCLUDES) -o $$@ $(call programVar,$(1),SRCS) $(LIB) +endef + +-include $(wildcard $(DEP_DIR)/*.d) + +# ONE self-booting volume per program (ID_IIGS_GAME_DISK; iigs-game-disk +# GAME=NAME for any): a GS/OS system stripped to what JoeyLib actually uses, +# a launcher in place of the Finder, and the program in its own folder so +# that folder can be copied to a hard drive and run from there. Boot to the +# program is 21.8 s against 43.4 s for the two-floppy boot disk. The size is +# chosen per program: one that fits beside a stripped system gets a bootable +# 800KB 3.5" floppy, and a bigger one a 2048KB volume, which MAME can only +# boot off a CFFA2 card because its 3.5 drive is 800KB GCR: +# mame apple2gs -sl7 cffa2 -hard1 build/iigs/bin/NAME.2mg +# GAME_DISK_SIZE forces a size. Each builds its OWN one-program data disk +# (NAME-data.2mg) first rather than using JoeyLib's joey.2mg, which carries +# the small examples and whose exact contents the gates depend on. +IIGS_GAME_DISKS = $(foreach p,$(PORT_PROGRAMS),$(if $(call programVar,$(p),GAME_DISK),$(call programName,$(p)))) +IIGS_GAME = $(or $(GAME),$(firstword $(IIGS_GAME_DISKS))) +PORT_PACKAGES = $(if $(IIGS_GAME_DISKS),iigs-game-disks) + +# The commands that make program $$g's (a shell variable's) game disk. +IIGS_GAME_DISK_CMDS = BINDIR="$(BINDIR)" NTP_BIN="$(NTP_BIN)" JOEY_DISK_EXAMPLES="$$g" \ + $(REPO_DIR)/scripts/make-iigs-disk.sh $(BINDIR)/$$g-data.2mg && \ + JOEY_GAME_DISK_SIZE="$(or $(GAME_DISK_SIZE),auto)" \ + $(REPO_DIR)/scripts/make-iigs-game-disk.sh $(BINDIR)/$$g.2mg $$g $$g $(BINDIR)/$$g-data.2mg + +.PHONY: iigs-game-disk iigs-game-disks iigs-verify-game-disk +iigs-game-disk: programs $(NTP_BIN) + g=$(IIGS_GAME); $(IIGS_GAME_DISK_CMDS) + +iigs-game-disks: programs $(NTP_BIN) + @set -e; for g in $(IIGS_GAME_DISKS); do $(IIGS_GAME_DISK_CMDS); done + +# Boot one game disk headlessly and assert it reaches the program and draws. +# An 800KB image goes in the 3.5 drive, a larger one on a CFFA2 card. +iigs-verify-game-disk: iigs-game-disk + $(REPO_DIR)/scripts/verify-iigs-game-disk.sh $(BINDIR)/$(IIGS_GAME).2mg + +# ---- JoeyLib's own build: its examples and the IIgs gates ----------------------- +ifndef JOEY_PROJECT + +include $(REPO_DIR)/make/examples.mk +include $(REPO_DIR)/make/programs.mk + +# Default: build (compile-check) the library. The MAME smoke test is +# `iigs-clang-smoke`; disks and examples are `iigs-disk`. +.DEFAULT_GOAL := all +.PHONY: all iigs iigs-lib iigs-clang-smoke iigs-examples iigs-disk iigs-disk-all iigs-verify iigs-verify-all iigs-verify-audio iigs-verify-save iigs-verify-serial iigs-verify-serial-loss iigs-verify-shrtail iigs-verify-tilebank clean-iigs clean +all iigs: iigs-lib + +# Per-example launchable binaries with their DATA -- all of them: leaving +# some out once made `make iigs` silently test 8-day-old binaries against new +# assets. +iigs-examples: programs + +# Build the JOEYLIB data disk (build/iigs/bin/joey.2mg) that the gates, and +# `joey-run build/iigs/bin/joey.2mg`, mount beside GS/OS. Packs the launchable +# examples as ProDOS S16 apps (an 800KB floppy can't hold every example; see +# make-iigs-disk.sh) with their DATA. NTP_BIN rides along so jlpAudioInit's +# runtime load of "ntpplayer" finds it on the volume -- without it every +# example logs "audioInit failed". +iigs-disk: programs $(NTP_BIN) + BINDIR="$(BINDIR)" NTP_BIN="$(NTP_BIN)" $(REPO_DIR)/scripts/make-iigs-disk.sh $(BINDIR)/joey.2mg + +# Every launchable example on one image. The full set will not fit an 800KB +# 3.5" floppy, so this is a 16 MB ProDOS hard-disk image (joey-all.2mg): boot +# it in MAME with +# `-sl7 cffa2 -hard1 <GS/OS> -hard2 build/iigs/bin/joey-all.2mg` and launch +# any example from the JOEYLIB volume in the Finder. +iigs-disk-all: programs $(NTP_BIN) + BINDIR="$(BINDIR)" NTP_BIN="$(NTP_BIN)" \ + JOEY_DISK_SIZE="16MB" \ + JOEY_DISK_EXAMPLES="DRAW PATTERN KEYS JOY SPRITE SERIAL SERTEST SAVE TILEBANK AUDIO UBER ADV2 ADV" \ + $(REPO_DIR)/scripts/make-iigs-disk.sh $(BINDIR)/joey-all.2mg + +# Headless visual-verification gate: boot GS/OS under MAME, launch the DRAW +# example off joey.2mg, and assert the SHR framebuffer was actually rendered +# (many distinct colors -> pixels/lines/circles/tiles/flood all drew). Pass +# VERIFY_EXAMPLE=name to check a different example. +iigs-verify: iigs-disk + $(REPO_DIR)/scripts/verify-iigs.sh $(or $(VERIFY_EXAMPLE),draw) + +# Audio gate: AUDIOPRB (AUDIO pressing its own keys, make/examples.mk) off a +# one-program disk of its own: NTP's sound interrupt installed, the DOC's +# master volume up, and the tracker's, the noise's and the stream's slots +# carrying waveforms (~10 s). +iigs-verify-audio: $(call programBin,AUDIOPRB) $(NTP_BIN) + $(REPO_DIR)/scripts/verify-iigs-audio.sh $(call programBin,AUDIOPRB) + +# Cross-channel SCC gate: opening the modem port must leave the printer channel +# (AppleTalk's) transmitting. Builds its own one-example disk; ~2 min. +iigs-verify-serial: $(call programBin,SCCPROBE) + $(REPO_DIR)/scripts/verify-iigs-serial.sh + +# Receive-loss gate: 4096 bytes streamed at 9600 baud while the program does +# nothing but full-screen presents must all arrive. Runs the modem port (SCC) +# and a Super Serial Card in slot 2 (6551) on the installed MAME, with the SCC +# receive FIFO cut to the real chip's 3 bytes (JOEY_Z80SCC_RX_FIFO=3) and the +# card's a2ssc firmware from iigsFirmware; see the script header. ~3 min per mode. +# The card runs at SERLOSS_SSC_BAUD (make/examples.mk says why). +iigs-verify-serial-loss: $(call programBin,SERLOSS) $(call programBin,SERLOSSS) + $(REPO_DIR)/scripts/verify-iigs-serial-loss.sh scc + SERLOSS_BAUD=$(SERLOSS_SSC_BAUD) $(REPO_DIR)/scripts/verify-iigs-serial-loss.sh ssc + +# Boot-verify every launchable example on joey.2mg (per-example thresholds). +iigs-verify-all: iigs-disk + $(REPO_DIR)/scripts/verify-iigs-all.sh + +# Gate the 712 display-state bytes ABOVE the pixel block: the SCB array +# ($E1:9D00-$9DC7) and all 16 palettes ($E1:9E00-$9FFF). Nothing else in the +# tree can see them -- the golden hash covers the STAGE, not $E1, and P9's +# pixel compare stops at $9CFF -- so a wrong palette is invisible to every +# other gate and shows only as wrong colors on a real screen. That blind spot +# is why a phantom "PEI-slam corrupts palettes 1-15" note survived five weeks. +# +# UBER's shrTailUpload / shrTailSlam rows live behind #ifdef UBER_PROBE, which +# no normal build defines, so this target benches UBERPROB (make/examples.mk: +# UBER built with them, into build/iigs/probe/bin) off a single-app disk of its +# own. The goldened UBER binary, joey.2mg and joeylog.txt are all left +# untouched. +IIGS_PROBE_DIR := $(PROGRAM_BUILD)/probe + +iigs-verify-shrtail: $(call programBin,UBERPROB) $(NTP_BIN) + BINDIR="$(call programDir,UBERPROB)" NTP_BIN="$(NTP_BIN)" JOEY_DISK_EXAMPLES="UBER" \ + $(REPO_DIR)/scripts/make-iigs-disk.sh $(IIGS_PROBE_DIR)/uberprobe.2mg + BENCH_DATA_DISK="$(IIGS_PROBE_DIR)/uberprobe.2mg" BENCH_OUT_LOG="$(IIGS_PROBE_DIR)/probelog.txt" \ + BENCH_MAX_FRAMES="$(or $(PROBE_MAX_FRAMES),45000)" $(REPO_DIR)/scripts/bench-iigs.sh + @grep -E 'shrParity|shrTailUpload|shrTailSlam' $(IIGS_PROBE_DIR)/probelog.txt || \ + { echo "iigs-verify-shrtail: FAIL - no probe rows captured" >&2; exit 1; } + @grep -qE 'shrTail(Upload|Slam) FAIL' $(IIGS_PROBE_DIR)/probelog.txt && \ + { echo "iigs-verify-shrtail: FAIL - \$$E1 SCB/palette block mismatched the stage" >&2; exit 1; } || true + @echo "iigs-verify-shrtail: PASS" + +# Tile-bank gate: TILEBANK loads this port's own .tbk bake both ways off its +# own one-example disk; both must hash like the host gate's, and every other +# target's bake must be refused. NTP_BIN keeps the disk's audio init quiet. +iigs-verify-tilebank: $(call programBin,TILEBANK) $(TILEBANK_DATA) $(NTP_BIN) + $(REPO_DIR)/scripts/verify-iigs-tilebank.sh + +# Runtime gate for the save HAL's GS/OS calls (Create + GetDevNumber/DInfo/ +# Volume): boots SAVE under MAME and checks the save-OK lamp + disk-free bar. +iigs-verify-save: $(call programBin,SAVE) $(call programBin,DRAW) + $(REPO_DIR)/scripts/verify-iigs-save.sh + # End-to-end smoke test: clang -> llvm-mc -> link816 -> MAME, validating # the ported asm computes correctly on emulated hardware. iigs-clang-smoke: $(REPO_DIR)/make/support/iigs/clang-smoke.sh -# Per-example binaries via clang-build.sh. clang-build.sh mirrors the -# old driver's CLI (-I / -o / -M; -s and -b are accepted no-ops). These -# link single-segment today; examples larger than one bank need the -# multi-segment OMF wiring still in progress. -DEP_DIR := $(BUILD)/dep - -$(BINDIR)/PATTERN: $(PATTERN_SRC) $(LIB) $(IIGS_CLANG_BUILD) - @mkdir -p $(dir $@) $(DEP_DIR) - $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/PATTERN.d $(INCLUDES) -o $@ $(PATTERN_SRC) $(LIB) - -$(BINDIR)/DRAW: $(DRAW_SRC) $(LIB) $(IIGS_CLANG_BUILD) - @mkdir -p $(dir $@) $(DEP_DIR) - $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/DRAW.d $(INCLUDES) -o $@ $(DRAW_SRC) $(LIB) - -$(BINDIR)/KEYS: $(KEYS_SRC) $(LIB) $(IIGS_CLANG_BUILD) - @mkdir -p $(dir $@) $(DEP_DIR) - $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/KEYS.d $(INCLUDES) -o $@ $(KEYS_SRC) $(LIB) - -$(BINDIR)/SERIAL: $(SERIAL_SRC) $(LIB) $(IIGS_CLANG_BUILD) - @mkdir -p $(dir $@) $(DEP_DIR) - $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/SERIAL.d $(INCLUDES) -o $@ $(SERIAL_SRC) $(LIB) - -$(BINDIR)/SERTEST: $(SERTEST_SRC) $(LIB) $(IIGS_CLANG_BUILD) - @mkdir -p $(dir $@) $(DEP_DIR) - $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/SERTEST.d $(INCLUDES) -o $@ $(SERTEST_SRC) $(LIB) - -# Cross-channel SCC check: does opening the modem port leave the printer port -# (AppleTalk's channel) alive? Driven by scripts/verify-iigs-serial.sh. -$(BINDIR)/SCCPROBE: $(SCCPROBE_SRC) $(LIB) $(IIGS_CLANG_BUILD) - @mkdir -p $(dir $@) $(DEP_DIR) - $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/SCCPROBE.d $(INCLUDES) -o $@ $(SCCPROBE_SRC) $(LIB) - -# Receive-loss probes for iigs-verify-serial-loss: modem port and slot-2 SSC. -$(BINDIR)/SERLOSS: $(SERLOSS_SRC) $(LIB) $(IIGS_CLANG_BUILD) - @mkdir -p $(dir $@) $(DEP_DIR) - $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/SERLOSS.d $(INCLUDES) -o $@ $(SERLOSS_SRC) $(LIB) - -$(BINDIR)/SERLOSSS: $(SERLOSS_SRC) $(LIB) $(IIGS_CLANG_BUILD) - @mkdir -p $(dir $@) $(DEP_DIR) - $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/SERLOSSS.d -DSERLOSS_SLOT=2 -DSERLOSS_BAUD=$(SERLOSS_SSC_BAUD)u $(INCLUDES) -o $@ $(SERLOSS_SRC) $(LIB) - -$(BINDIR)/SAVE: $(SAVE_SRC) $(LIB) $(IIGS_CLANG_BUILD) - @mkdir -p $(dir $@) $(DEP_DIR) - $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/SAVE.d $(INCLUDES) -o $@ $(SAVE_SRC) $(LIB) - -$(BINDIR)/TILEBANK: $(TILEBANK_SRC) $(LIB) $(IIGS_CLANG_BUILD) - @mkdir -p $(dir $@) $(DEP_DIR) - $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/TILEBANK.d $(INCLUDES) -o $@ $(TILEBANK_SRC) $(LIB) - -$(TILEBANK_DATA): $(TILEBANK_BAKE_DEPS) - python3 $(TILEBANK_STAGE) $(PLATFORM) $(TILEBANK_PNG) $(dir $@) - -$(BINDIR)/JOY: $(JOY_SRC) $(LIB) $(IIGS_CLANG_BUILD) - @mkdir -p $(dir $@) $(DEP_DIR) - $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/JOY.d $(INCLUDES) -o $@ $(JOY_SRC) $(LIB) - -$(BINDIR)/SPRITE: $(SPRITE_SRC) $(LIB) $(IIGS_CLANG_BUILD) - @mkdir -p $(dir $@) $(DEP_DIR) - $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/SPRITE.d $(INCLUDES) -o $@ $(SPRITE_SRC) $(LIB) - -$(BINDIR)/UBER: $(UBER_SRC) $(LIB) $(IIGS_CLANG_BUILD) - @mkdir -p $(dir $@) $(DEP_DIR) - $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/UBER.d $(INCLUDES) -o $@ $(UBER_SRC) $(LIB) - -$(BINDIR)/ADV: $(ADV_SRC) $(LIB) $(IIGS_CLANG_BUILD) - @mkdir -p $(dir $@) $(DEP_DIR) - $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/ADV.d $(INCLUDES) -o $@ $(ADV_SRC) $(LIB) - -$(BINDIR)/ADV2: $(ADV2_SRC) $(LIB) $(IIGS_CLANG_BUILD) - @mkdir -p $(dir $@) $(DEP_DIR) - $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/ADV2.d $(INCLUDES) -o $@ $(ADV2_SRC) $(LIB) - -# AGI opts into large overflow segments. --segment-cap bounds the ENTRY -# bank (which also holds rodata + bss + heap under the $$C000 IO window); -# overflow banks carry text only, so capping them the same way wasted -# ~50K per bank. With this AGI links 5 bank-aligned segments instead of -# 19, which matters because it already needs -ramsize 4M. Verified by -# running KQ3 headless and diffing AGI's own joeylog.txt against the -# 19-segment build: identical apart from the embedded build timestamp. -# -# Both AGI and STAXI opt in; see clang-build.sh for the measurements. -$(BINDIR)/AGI: export JOEY_OVERFLOW_CAP := 0xF000 -$(BINDIR)/AGI: $(AGI_SRCS) $(LIB) $(IIGS_CLANG_BUILD) - @mkdir -p $(dir $@) $(DEP_DIR) - $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/AGI.d $(INCLUDES) -I$(EXAMPLES)/agi -o $@ $(AGI_SRCS) $(LIB) - -# STAXI now also opts into large overflow segments: 26 bank-aligned -# segments starved the Memory Manager, so NTP's 34K NewHandle failed and -# the IIgs build ran SILENT (gNTPReady stayed 0). Freeing that memory lets -# audio initialise; the latent crash that then surfaced is fixed in -# src/iigs/audio_full.c (sfxTrigger now requires a running replayer). -$(BINDIR)/STAXI: export JOEY_OVERFLOW_CAP := 0xF000 -$(BINDIR)/STAXI: $(STAXI_SRCS) $(LIB) $(IIGS_CLANG_BUILD) - @mkdir -p $(dir $@) $(DEP_DIR) - $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/STAXI.d $(INCLUDES) -I$(EXAMPLES)/spacetaxi -o $@ $(STAXI_SRCS) $(LIB) - -# AUDIO loads the NTP replayer from disk at runtime: audio_full.c reads -# ntpplayer.bin via NTP_PATH instead of baking the 34KB replayer as rodata -# (which overflowed bank 0, and clang's IMM16+$BE near form can't reach a far -# rodata segment anyway). $(NTP_BIN) is shipped on the application disk next -# to the binary. -$(BINDIR)/AUDIO: $(AUDIO_SRC) $(LIB) $(NTP_BIN) $(IIGS_CLANG_BUILD) - @mkdir -p $(dir $@) $(DEP_DIR) - $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/AUDIO.d $(INCLUDES) -o $@ $(AUDIO_SRC) $(LIB) - clean-iigs: rm -rf $(BUILD) @@ -441,4 +280,4 @@ clean-iigs: # looks like success while leaving stale objects the next build links. clean: clean-iigs --include $(wildcard $(DEP_DIR)/*.d) +endif diff --git a/make/programs.mk b/make/programs.mk new file mode 100644 index 0000000..2ef8370 --- /dev/null +++ b/make/programs.mk @@ -0,0 +1,67 @@ +# Every program's rules, for JoeyLib's own examples and for a project's +# programs alike: one description per program, one set of per-port rules. +# +# Included LAST: by make/<port>.mk after JoeyLib's example descriptions +# (make/examples.mk), and by a project's Makefile after its own (see +# make/project.mk). By then the port is set up, so a description may use +# PLATFORM, ASSET_PLATFORM, BINDIR, PROGRAM_BUILD, PROJECT_DIR, +# PROJECT_BUILD, SPRITEBAKE, HOST_CC/HOST_CFLAGS and the program lookups in +# make/common.mk ($(call programDataDir,ID) and the rest) in its own rules. +# +# A program is the variables named after its ID: an upper-case name of up to +# eight letters and digits, which is also its file name unless NAME says +# otherwise. Any of them can be given for one port only as ID_<PORT>_<VAR> +# (MYGAME_AMIGA_NAME, MYGAME_IIGS_SRCS), which wins over ID_<VAR> there, even +# when empty. +# +# PROGRAMS the ids the `programs` goal (and a project's `all`) builds +# TEST_PROGRAMS ids with rules but built only when asked for (gate probes) +# ID_SRCS its C sources, in link order +# ID_CFLAGS extra compiler flags (-I, -D) +# ID_LDFLAGS extra link flags (-s) +# ID_PORTS the ports it builds for; every port when unset +# ID_NAME its file name without the extension; by default the ID, +# on the Amiga Capitalised (Hello), on DVX lower case +# ID_FOLDER non-empty: it ships in a folder of its own, bin/<name>/, +# with its DATA beside it (the Amiga, ST, DOS and X68000; +# on the IIgs and DVX every program is packaged apart anyway) +# ID_BINDIR build it here instead of bin/ (probe builds) +# ID_DATA its DATA files, as the files the description's own rules +# make under $(call programDataDir,ID) (or +# programOptionalDir, for files it can run without) +# ID_TITLE DVX: its window title (default: its Amiga-style name) +# ID_HELP DVX: the line of its help file (no apostrophes: the help is +# written through the shell in quotes) +# ID_IIGS_OVERFLOW_CAP IIgs: link large overflow segments (clang-build.sh's +# JOEY_OVERFLOW_CAP) +# ID_IIGS_GAME_DISK IIgs: non-empty: it ships as its own bootable volume +# (iigs-game-disks, and a project's `all`) +# +# A port's rules for one program are its programRules template, written in +# make/<port>.mk next to that port's library rules. + +ifndef JOEY_DISPATCH + +PORT_PROGRAMS := $(foreach p,$(PROGRAMS),$(call programOnPort,$(p))) +PORT_TEST_PROGRAMS := $(foreach p,$(TEST_PROGRAMS),$(call programOnPort,$(p))) + +$(foreach p,$(PORT_PROGRAMS) $(PORT_TEST_PROGRAMS),$(eval $(call programRules,$(p)))) + +# Every program of this port with its DATA. +.PHONY: programs +programs: $(foreach p,$(PORT_PROGRAMS),$(call programTarget,$(p)) $(call programVar,$(p),DATA)) + +# The host sprite pre-compiler a description's bakes use; make/tools.mk owns +# its sources. +$(SPRITEBAKE): FORCE + $(MAKE) -f $(REPO_DIR)/make/tools.mk $(notdir $@) + +# A project's goal for one port: its programs, their DATA and the packages +# the port makes of them (PORT_PACKAGES: the IIgs game disks). JoeyLib's own +# make/<port>.mk files say what their `all` is themselves. +ifdef JOEY_PROJECT +.PHONY: all +all: programs $(PORT_PACKAGES) +endif + +endif diff --git a/make/project.mk b/make/project.mk new file mode 100644 index 0000000..b1b4977 --- /dev/null +++ b/make/project.mk @@ -0,0 +1,97 @@ +# Build programs that live outside JoeyLib, with JoeyLib's toolchains, +# library, asset tools and packaging -- the same rules JoeyLib's own examples +# use. A project's Makefile: +# +# JOEYLIB ?= ../joeylib +# include $(JOEYLIB)/make/project.mk +# +# PROGRAMS := MYGAME +# MYGAME_SRCS := mygame.c world.c +# ...the variables make/programs.mk lists, and the project's own rules for +# its DATA files (bakes), which may use what make/programs.mk names... +# +# include $(JOEYLIB)/make/programs.mk +# +# Paths in it are relative to the project's directory: run make there. +# +# make every port the programs build for whose toolchain +# is installed (ports without one are noted, skipped) +# make <port> one port: iigs amiga atarist dos x68000 dvx +# make <port>-<goal> one of that port's goals: iigs-game-disk GAME=NAME, +# iigs-verify-game-disk, iigs-game-disks +# make JOEY_PORT=<port> <target> +# any target of that port, a file among them +# make clean remove the project's build/ (clean-<port>: one port's) +# +# Outputs land in the project's build/<port>/, laid out like JoeyLib's own: +# programs in bin/ (joey-run --project DIR finds them by name), DATA staged +# beside them, the IIgs data and game disks in bin/ too. JoeyLib's library +# and host tools are built on demand where JoeyLib keeps them (JOEY_BUILD, +# make/common.mk), shared with JoeyLib's own build and every other project; +# make JOEY_BUILD=DIR keeps them out of the JoeyLib checkout. PROJECT_BUILD=DIR +# moves the project's own. +# +# How it works: each port has its own compiler and its own make variables, +# so one make process cannot hold two ports. This level only reads the +# descriptions (for the ports they build for) and runs make on the same +# Makefile again with JOEY_PORT set, once per port; in that run this file +# includes make/<port>.mk -- the port's library rules and its programRules +# template -- and make/programs.mk at the bottom instantiates the template +# for every program. + +JOEY_PROJECT := $(abspath $(dir $(firstword $(MAKEFILE_LIST)))) +JOEY_MAKE_DIR := $(dir $(lastword $(MAKEFILE_LIST))) + +ifeq ($(JOEY_PORT),) + +include $(JOEY_MAKE_DIR)common.mk + +# Read by make/programs.mk, which then makes no rules: there is no port here. +JOEY_DISPATCH := 1 +PROJECT_MAKEFILE := $(abspath $(firstword $(MAKEFILE_LIST))) +# This Makefile again, quietly: $(MAKE) $(PORT_MAKE) JOEY_PORT=<port>. +# ($(MAKE) itself stays in each recipe: that is how make knows the line runs +# make, to share its job slots with it and to run it under -n.) +PORT_MAKE := --no-print-directory -f $(PROJECT_MAKEFILE) + +# The ports the programs build for (each one's ID_PORTS, by default every +# port), in JOEY_PORTS order. Expanded in recipes: the descriptions come after. +PROJECT_PORTS = $(filter $(sort $(foreach p,$(PROGRAMS),$(or $($(p)_PORTS),$(JOEY_PORTS)))),$(JOEY_PORTS)) +PROJECT_BUILDING = $(filter $(JOEY_PORTS_INSTALLED),$(PROJECT_PORTS)) + +.DEFAULT_GOAL := all +.PHONY: all clean help $(JOEY_PORTS) $(addprefix clean-,$(JOEY_PORTS)) + +# One port after another: each is a make of its own, already parallel inside +# under -j, and DVX shares the DOS port's asset bakes. +all: + @$(foreach p,$(filter-out $(PROJECT_BUILDING),$(PROJECT_PORTS)),echo "note: $(p) skipped: its toolchain is not installed (./install.sh doctor)";) true + @$(if $(PROJECT_BUILDING),,echo "No toolchain for any of: $(PROJECT_PORTS). Run: ./install.sh install toolchains" >&2; exit 1) + @set -e; for p in $(PROJECT_BUILDING); do $(MAKE) $(PORT_MAKE) JOEY_PORT=$$p; done + +$(JOEY_PORTS): + @$(MAKE) $(PORT_MAKE) JOEY_PORT=$@ + +# make iigs-game-disk and the like: the goal, run in its port's make. +$(foreach p,$(JOEY_PORTS),$(eval $(p)-%: ; @$$(MAKE) $$(PORT_MAKE) JOEY_PORT=$(p) $$@)) + +clean: + rm -rf $(PROJECT_BUILD) + +$(addprefix clean-,$(JOEY_PORTS)): + rm -rf $(PROJECT_BUILD)/$(patsubst clean-%,%,$@) + +help: + @echo "Ports this project builds for: $(PROJECT_PORTS) (installed: $(PROJECT_BUILDING))" + @echo "Targets: all $(JOEY_PORTS) clean clean-<port> help" + @echo " <port>-<goal> (iigs-game-disk GAME=NAME, iigs-verify-game-disk, iigs-game-disks)" + @echo " JOEY_PORT=<port> <target> (any target of one port)" + +else + +# One port: its library and program rules. The project's descriptions follow, +# then make/programs.mk. +include $(JOEY_MAKE_DIR)$(JOEY_PORT).mk +.DEFAULT_GOAL := all + +endif diff --git a/make/support/iigs/clang-build.sh b/make/support/iigs/clang-build.sh index f8b14b3..a50801b 100755 --- a/make/support/iigs/clang-build.sh +++ b/make/support/iigs/clang-build.sh @@ -232,8 +232,9 @@ JOEY_MIN_HEAP="${JOEY_MIN_HEAP:-0}" # so this stays below 0x10000. # # DEFAULT 0 = overflow segments use --segment-cap (the historical -# behaviour); each target opts in from make/iigs.mk. AGI links 5 segments -# instead of 19, STAXI 4 instead of 26. +# behaviour); a program opts in with ID_IIGS_OVERFLOW_CAP in its description +# (make/programs.mk). AGI links 5 segments instead of 19, STAXI 4 instead of +# 26 (both now separate projects built on JoeyLib). # # Raising this on STAXI used to produce a dead image, which looked like a # segment-size limit. It was not -- AGI ships 28798-byte segments and runs. diff --git a/make/tools.mk b/make/tools.mk index d5ef5ec..12bacf8 100644 --- a/make/tools.mk +++ b/make/tools.mk @@ -1,11 +1,11 @@ # Host-side tools for the JoeyLib asset pipeline. # # These build with the host's default `cc` (no cross-toolchain) into -# build/tools/. Currently: +# build/tools/ (JOEY_BUILD's, make/common.mk). Currently: # modloopend -- inject an E8FF stop-marker pattern into a .MOD so # the Amiga audio HAL can honor PlayMod(loop=false). # joeymod invokes this transparently for Amiga output. -# mkstlevel -- host helper for Space Taxi level packaging. +# spritebake -- the sprite pre-compiler, one per target (below). # # Asset baking (PNG -> .tbk / .spr) is handled by tools/assetbake/ # assetbake.py (Python). It is invoked directly from each platform's @@ -13,24 +13,11 @@ include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk -BUILD_DIR := $(REPO_DIR)/build/tools +BUILD_DIR := $(JOEY_BUILD)/tools TOOLS_DIR := $(REPO_DIR)/tools -HOST_CC ?= cc -HOST_CFLAGS := -std=c99 -Wall -Wextra -Werror -O2 - MODLOOPEND_SRC := $(TOOLS_DIR)/modloopend/modloopend.c MODLOOPEND_BIN := $(BUILD_DIR)/modloopend -MKSTLEVEL_SRC := $(REPO_DIR)/examples/spacetaxi/mkstlevel/mkstlevel.c -MKSTLEVEL_BIN := $(BUILD_DIR)/mkstlevel - -# staxibake -- generates the Space Taxi cel bank (.spr) from the game's -# shared cel table + C64 bitmaps, for offline sprite pre-compilation. A -# plain host tool (the .spr is cross-platform); spritebake then compiles -# it per target. -STAXI_DIR := $(REPO_DIR)/examples/spacetaxi -STAXIBAKE_SRC := $(TOOLS_DIR)/staxibake/staxibake.c $(STAXI_DIR)/stCels.c $(STAXI_DIR)/stC64Data.c $(STAXI_DIR)/stLevelCels.c $(STAXI_DIR)/stLevelFile.c -STAXIBAKE_BIN := $(BUILD_DIR)/staxibake # spritebake -- offline sprite pre-compiler. Built ONCE PER TARGET on the # host: it links the shared staging (spriteStage.c) + that target's emitter, @@ -41,12 +28,10 @@ STAXIBAKE_BIN := $(BUILD_DIR)/staxibake SPRITEBAKE_SRC := $(TOOLS_DIR)/spritebake/spritebake.c $(SRC_CG)/spriteStage.c SPRITEBAKE_INC := -I$(INCLUDE_DIR) -I$(SRC_CORE) -I$(SRC_CG) -.PHONY: all tools clean-tools modloopend mkstlevel staxibake \ +.PHONY: all tools clean-tools modloopend \ spritebake-dos spritebake-amiga spritebake-atarist spritebake-iigs spritebake-x68000 -all tools: $(MODLOOPEND_BIN) $(MKSTLEVEL_BIN) +all tools: $(MODLOOPEND_BIN) modloopend: $(MODLOOPEND_BIN) -mkstlevel: $(MKSTLEVEL_BIN) -staxibake: $(STAXIBAKE_BIN) spritebake-dos: $(BUILD_DIR)/spritebake-dos spritebake-amiga: $(BUILD_DIR)/spritebake-amiga spritebake-atarist: $(BUILD_DIR)/spritebake-atarist @@ -57,14 +42,6 @@ $(MODLOOPEND_BIN): $(MODLOOPEND_SRC) @mkdir -p $(dir $@) $(HOST_CC) $(HOST_CFLAGS) $< -o $@ -$(STAXIBAKE_BIN): $(STAXIBAKE_SRC) - @mkdir -p $(dir $@) - $(HOST_CC) $(HOST_CFLAGS) -I$(STAXI_DIR) $^ -o $@ - -$(MKSTLEVEL_BIN): $(MKSTLEVEL_SRC) - @mkdir -p $(dir $@) - $(HOST_CC) $(HOST_CFLAGS) $< -o $@ - $(BUILD_DIR)/spritebake-dos: $(SPRITEBAKE_SRC) $(SRC_X86)/spriteEmitX86.c @mkdir -p $(dir $@) $(HOST_CC) $(HOST_CFLAGS) -DJOEYLIB_PLATFORM_DOS $(SPRITEBAKE_INC) -I$(SRC_X86) $^ -o $@ diff --git a/make/x68000.mk b/make/x68000.mk index 72f94ba..e548278 100644 --- a/make/x68000.mk +++ b/make/x68000.mk @@ -7,22 +7,18 @@ # The toolchain is elf2x68k (BSD-3, gcc 13.4.0 / binutils / newlib), installed # by ./install.sh (X68K_ROOT). m68k-xelf-gcc emits ELF; the driver's # -Wl,-elf2x68k pass converts it to a Human68k .X. +# +# This file holds JoeyLib's library for the X68000 and programRules, the +# X68000's rules for one program, which make/programs.mk instantiates for +# every program (a project reaches both through make/project.mk). JoeyLib's +# own examples and the X68000 gates are at the end. +PLATFORM := x68000 include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk -# The libxmp-lite rules below define a real target before `all`, which would -# otherwise silently become make's default goal (symptom: a bare `make -f -# make/x68000.mk` reports only "libxmplite.a is up to date" and builds nothing). -.DEFAULT_GOAL := all - $(call requireTool,$(X68K_ROOT)/bin/m68k-xelf-gcc,toolchain:x68000) $(call requireTool,$(LIBXMP_DIR)/include/libxmp-lite/xmp.h,toolchain:x68000) -PLATFORM := x68000 -BUILD := $(REPO_DIR)/build/$(PLATFORM) -LIBDIR := $(BUILD)/lib -BINDIR := $(BUILD)/bin - X68K_CC := $(X68K_ROOT)/bin/m68k-xelf-gcc X68K_AR := $(X68K_ROOT)/bin/m68k-xelf-ar @@ -81,68 +77,11 @@ $(LIBXMP_OBJDIR)/%.o: $(LIBXMP_DIR)/src/%.c $(LIBXMP_AR): $(LIBXMP_OBJS) @mkdir -p $(dir $@) + rm -f $@ $(X68K_AR) rcs $@ $(LIBXMP_OBJS) LIB := $(LIBDIR)/libjoey.a -# Every example but Space Taxi is its sources plus the library, linked by the -# one rule below. Worth knowing which ones the gates use: -# PATTERN the SCB acceptance vehicle: 8 bands of 25 lines with 8 gradient -# palettes, so a misplaced band boundary is immediately visible. -# UBER the golden-hash vehicle: every public op, a hash per op, which -# tools/diff-uber-hashes compares against the IIgs reference. -# AUDIO both audio paths: JYM1 chip music on the OPM, MOD/SFX on ADPCM. -# KEYS the input and mouse vehicle: key predicates, the typed line -# (jlInputGetChar) and its verification row. -# SERIAL the RS-232C gate's echo program. -# SERMOUSE the serial + mouse gate's program: streams both ways on the -# RS-232C port (SCC channel A) while the mouse (channel B) moves. -# TILEBANK the tile-bank gate's program: this port's .tbk bake loaded both -# ways and pixel-checked, every other target's bake refused. -# SCB the SCB gate's program: all 16 palettes, 147 palette changes a -# frame (every line for 128 of them), for the raster interrupt -# that reloads the text plane's one palette as the beam scans. -HELLO_SRC := $(EXAMPLES)/hello/hello.c -PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c -DRAW_SRC := $(EXAMPLES)/draw/draw.c -KEYS_SRC := $(EXAMPLES)/keys/keys.c -SERIAL_SRC := $(EXAMPLES)/serial/serial.c -SERTEST_SRC := $(EXAMPLES)/sertest/sertest.c -SERMOUSE_SRC := $(EXAMPLES)/sermouse/sermouse.c -SAVE_SRC := $(EXAMPLES)/save/save.c -TILEBANK_SRC := $(EXAMPLES)/tilebank/tilebank.c -SCB_SRC := $(EXAMPLES)/scb/scb.c -JOY_SRC := $(EXAMPLES)/joy/joy.c -SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c -AUDIO_SRC := $(EXAMPLES)/audio/audio.c -UBER_SRC := $(EXAMPLES)/uber/uber.c -ADV_SRC := $(EXAMPLES)/adventure/adventure.c -ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c -AGI_SRC := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c -EXAMPLES_X := HELLO PATTERN DRAW KEYS SERIAL SERTEST SERMOUSE SAVE TILEBANK SCB JOY SPRITE AUDIO UBER ADV ADV2 AGI -EXAMPLE_BINS := $(foreach e,$(EXAMPLES_X),$(BINDIR)/$(e).X) - -# AUDIO loads DATA/test.mod and DATA/test.sfx from beside the program. -DATA_DIR := $(BINDIR)/DATA -DATA_FILES := $(DATA_DIR)/test.mod $(DATA_DIR)/test.sfx -# TILEBANK's DATA/tilebank: this port's bake of the tile-bank fixture, the -# other targets' bakes it must refuse, and the expected pixels (common.mk). -TILEBANK_DATA := $(DATA_DIR)/tilebank/own.tbk - -# Space Taxi ships in its own folder with its DATA, as on the other ports, -# so its megabyte of data never rides onto every other program's floppy. -STAXI_DIR := $(BINDIR)/STAXI -STAXI_BIN := $(STAXI_DIR)/STAXI.X -STAXI_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stLevelFile.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stSim.c $(EXAMPLES)/spacetaxi/stFare.c $(EXAMPLES)/spacetaxi/stHooks.c $(EXAMPLES)/spacetaxi/stTitle.c $(EXAMPLES)/spacetaxi/stC64Data.c $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stAudio.c $(EXAMPLES)/spacetaxi/stLevelCels.c - -.PHONY: all x68000 x68000-lib x68000-examples x68000-verify-serial x68000-verify-mouse x68000-verify-serial-mouse x68000-verify-serial-scb x68000-verify-joy x68000-verify-tilebank x68000-verify-scb x68000-verify-golden clean-x68000 clean - -all x68000: x68000-lib x68000-examples - -x68000-lib: $(LIB) $(LIBXMP_AR) - -x68000-examples: $(EXAMPLE_BINS) $(STAXI_BIN) $(DATA_FILES) $(TILEBANK_DATA) - $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c @mkdir -p $(dir $@) $(X68K_CC) $(CFLAGS) -c $< -o $@ @@ -173,127 +112,68 @@ $(BUILD)/obj/port/%.o: $(SRC_DIR)/x68000/%.c @mkdir -p $(dir $@) $(X68K_CC) $(CFLAGS) -c $< -o $@ +# Archived from scratch, so the members -- and every program's layout -- +# follow LIB_OBJS rather than the order earlier builds added them in. $(LIB): $(LIB_OBJS) @mkdir -p $(dir $@) + rm -f $@ $(X68K_AR) rcs $@ $(LIB_OBJS) -.SECONDEXPANSION: -$(EXAMPLE_BINS): $(BINDIR)/%.X: $$($$*_SRC) $(LIB) $(LIBXMP_AR) - @mkdir -p $(dir $@) - $(X68K_CC) $(CFLAGS) $($*_SRC) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@ +# Pull in per-object header-dependency files generated by gcc -MMD/-MP, so a +# header edit rebuilds every object that includes it. +-include $(LIB_OBJS:.o=.d) -$(DATA_DIR)/%: $(REPO_DIR)/assets/% - @mkdir -p $(dir $@) - cp $< $@ +# ---- programs ----------------------------------------------------------------- +# A program is NAME.X, in bin/ or in its own bin/NAME/ with its DATA beside it +# (ID_FOLDER: a game's megabyte of data then never rides onto every other +# program's floppy); joey-run copies the program and its DATA onto a Human68k +# data floppy. +PROGRAM_EXT := .X +PROGRAM_FOLDERS := yes -$(TILEBANK_DATA): $(TILEBANK_BAKE_DEPS) - python3 $(TILEBANK_STAGE) $(PLATFORM) $(TILEBANK_PNG) $(dir $@) +# One program, $(1) its id: compiled and linked in one step with the library +# and libxmp-lite (the link also leaves NAME.X.elf). gcc's -MMD leaves NAME.d +# beside it, for header edits. +define programRules +$(call programBin,$(1)): $(call programVar,$(1),SRCS) $(LIB) $(LIBXMP_AR) + @mkdir -p $$(dir $$@) + $(X68K_CC) $(CFLAGS) $(call programVar,$(1),CFLAGS) $(call programVar,$(1),SRCS) $(LIB) $(LIBXMP_AR) $(LDFLAGS) $(call programVar,$(1),LDFLAGS) -o $$@ +-include $(basename $(call programBin,$(1))).d +endef -# Space Taxi asset variables (the rules follow the binary below); make -# expands prerequisites when it reads the rule, so these come first. -STAXI_GEN_DIR := $(EXAMPLES)/spacetaxi/generated/x68000 -STAXI_RUN_DIR := $(STAXI_DIR)/DATA -STAXI_LEVEL_GEN := $(foreach n,$(STAXI_LEVEL_NUMS),$(STAXI_GEN_DIR)/levels/level$(n).dat) \ - $(STAXI_GEN_DIR)/levels/title.dat -STAXI_LEVEL_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_LEVEL_GEN)) -SPRITEBAKE_BIN := $(REPO_DIR)/build/tools/spritebake-x68000 -STAXIBAKE_BIN := $(REPO_DIR)/build/tools/staxibake -STAXICELS_SPR_GEN := $(STAXI_GEN_DIR)/sprites/staxicels.spr -STAXICELS_SPC_GEN := $(STAXICELS_SPR_GEN:.spr=.spc) -STAXICELS_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXICELS_SPC_GEN)) -STAXI_ASSET_DSTS := $(STAXI_LEVEL_RUN) $(STAXICELS_SPC_RUN) +# ---- JoeyLib's own build: its examples and the X68000 gates ---------------------- +ifndef JOEY_PROJECT -# Per-level hook-sprite banks: the cels stLevelCelList names for each -# level that has hook sprites, baked from its level file and pre-compiled -# for this target, so no cel is JIT-compiled mid-ride. Levels without -# hook sprites have no bank; the game JIT-compiles at level entry when a -# bank is missing. -# ...except on the X68000, where they do not fit. The Human68k floppy is -# 1232 KB and these eight banks are 787 KB of it (level09 alone is 236 KB, -# level10 175, level17 165), which with the 296 KB binary, the levels and -# the speech put the disk about 300 KB over however small speech gets. -# stRenderLevelCels already handles their absence the right way: it builds -# and compiles the level's cels at ENTRY, behind the LOADING bar, so the -# ride itself still never compiles. That is also what spacetaxi.c's -# codegen-arena comment says the 68k ports are meant to do -- their cels -# are big and their JIT is fast. Set STAXI_CEL_LEVELS to the list above to -# ship them again if the payload ever moves to a hard disk image. -STAXI_CEL_LEVELS := -STAXILEVEL_SPR_GEN := $(foreach n,$(STAXI_CEL_LEVELS),$(STAXI_GEN_DIR)/sprites/level$(n).spr) -STAXILEVEL_SPC_GEN := $(STAXILEVEL_SPR_GEN:.spr=.spc) -STAXILEVEL_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXILEVEL_SPC_GEN)) -STAXI_ASSET_DSTS += $(STAXILEVEL_SPC_RUN) +include $(REPO_DIR)/make/examples.mk +include $(REPO_DIR)/make/programs.mk -# Speech baked to sample bytes per passenger pitch (assets/genSpeechPcm.py): -# expanding bits at play time is what keeps "HEY TAXI" from stalling the -# ride; the C64 run-length source stays in stSpeechData.c as the input. -STAXI_SPEECH_GEN := $(STAXI_GEN_DIR)/speech.bin -STAXI_SPEECH_RUN := $(STAXI_RUN_DIR)/speech.bin -STAXI_ASSET_DSTS += $(STAXI_SPEECH_RUN) -$(STAXI_SPEECH_GEN): $(EXAMPLES)/spacetaxi/stSpeechData.c $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py - @mkdir -p $(dir $@) - # --single: one polarity, 60 KB instead of 120 KB. The 1232 KB - # Human68k floppy cannot hold the two-polarity file alongside the - # binary and the levels -- it came up 18 KB short -- and the player - # negates the samples itself when the running polarity is odd. - python3 $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py $< $@ --single -$(STAXI_SPEECH_RUN): $(STAXI_SPEECH_GEN) - @mkdir -p $(dir $@) - cp $< $@ +.DEFAULT_GOAL := all +.PHONY: all x68000 x68000-lib x68000-examples x68000-verify-audio x68000-verify-serial x68000-verify-mouse x68000-verify-serial-mouse x68000-verify-serial-scb x68000-verify-joy x68000-verify-tilebank x68000-verify-scb x68000-verify-golden clean-x68000 clean -# Space Taxi. -s (strip) because the image it ships on is a 1232 KB -# Human68k floppy: the unstripped binary is most of the volume on its -# own. The DATA tree next to it (levels + the precompiled cel bank) is -# what joey-run copies onto the data floppy. -$(STAXI_BIN): $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) $(call staxiData,$(STAXI_ASSET_DSTS)) - @mkdir -p $(dir $@) - $(X68K_CC) $(CFLAGS) -s -I$(EXAMPLES)/spacetaxi $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@ +all x68000: x68000-lib x68000-examples -# Space Taxi asset staging, the amiga.mk pattern cut down to what this -# port loads at run time: the 24 levels + title extracted from the C64 -# dump, and the cel bank baked by staxibake and pre-compiled for THIS -# target by spritebake-x68000 (the planar 68k emitter under target byte -# 5, so an Amiga or DOS bank is refused by the loader). Tiles and the -# font come from the C64 data in stC64Data.c, so no .tbk is staged. -.SECONDARY: $(STAXI_LEVEL_GEN) $(STAXICELS_SPR_GEN) $(STAXICELS_SPC_GEN) $(STAXILEVEL_SPR_GEN) $(STAXILEVEL_SPC_GEN) +x68000-lib: $(LIB) $(LIBXMP_AR) -$(STAXI_LEVEL_GEN) &: $(STAXI_RAW_DUMP) $(ROMTOLEVEL_BIN) - @mkdir -p $(STAXI_GEN_DIR)/levels - python3 $(ROMTOLEVEL_BIN) $(STAXI_RAW_DUMP) $(STAXI_GEN_DIR)/levels +x68000-examples: programs -$(SPRITEBAKE_BIN): FORCE - $(MAKE) -f $(REPO_DIR)/make/tools.mk spritebake-x68000 - -$(STAXIBAKE_BIN): FORCE - $(MAKE) -f $(REPO_DIR)/make/tools.mk staxibake - -$(STAXICELS_SPR_GEN): $(STAXIBAKE_BIN) $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stC64Data.c - @mkdir -p $(dir $@) - $(STAXIBAKE_BIN) $@ - -$(STAXI_GEN_DIR)/sprites/%.spc: $(STAXI_GEN_DIR)/sprites/%.spr $(SPRITEBAKE_BIN) - $(SPRITEBAKE_BIN) $< $@ - -$(STAXI_RUN_DIR)/levels/%.dat: $(STAXI_GEN_DIR)/levels/%.dat - @mkdir -p $(dir $@) - cp $< $@ - -$(STAXI_RUN_DIR)/sprites/%.spc: $(STAXI_GEN_DIR)/sprites/%.spc - @mkdir -p $(dir $@) - cp $< $@ +# Audio gate: AUDIOPRB (AUDIO pressing its own keys, make/examples.mk) under +# MAME with write taps on the OPM, the PPI and the ADPCM: every noise code in +# the port's table, the ADPCM fed at 7812 Hz to both speakers (~1 min). +x68000-verify-audio: $(call programBin,AUDIOPRB) + $(REPO_DIR)/scripts/verify-x68000-audio.sh $(call programBin,AUDIOPRB) # Both-directions RS-232C gate (~1 min). -x68000-verify-serial: $(BINDIR)/SERIAL.X +x68000-verify-serial: $(call programBin,SERIAL) $(REPO_DIR)/scripts/verify-x68000-serial.sh # Mouse gate: injected MAME mouse packets through the IOCS ROM into # jlMouse* state, read back from KEYS.X's verification scanline. -x68000-verify-mouse: $(BINDIR)/KEYS.X +x68000-verify-mouse: $(call programBin,KEYS) $(REPO_DIR)/scripts/verify-x68000-mouse.sh # Serial + mouse together: both share the SCC, so stream both ways on the # RS-232C port while the mouse moves, and check neither disturbs the other. -x68000-verify-serial-mouse: $(BINDIR)/SERMOUSE.X +x68000-verify-serial-mouse: $(call programBin,SERMOUSE) $(REPO_DIR)/scripts/verify-x68000-serial-mouse.sh # Serial + mouse under per-line palette changes: the serial + mouse gate with @@ -303,18 +183,18 @@ x68000-verify-serial-scb: $(LIB) $(LIBXMP_AR) # Joystick gate: every direction, both triggers and every release on both # ports, held through MAME's pads and read back from JOY.X's display (~10 s). -x68000-verify-joy: $(BINDIR)/JOY.X +x68000-verify-joy: $(call programBin,JOY) $(REPO_DIR)/scripts/verify-x68000-joy.sh # Tile-bank gate: this port's own .tbk bake loaded both ways must hash like # the host gate's, and every other target's bake must be refused (~1 min). -x68000-verify-tilebank: $(BINDIR)/TILEBANK.X $(TILEBANK_DATA) +x68000-verify-tilebank: $(call programBin,TILEBANK) $(TILEBANK_DATA) $(REPO_DIR)/scripts/verify-x68000-tilebank.sh # SCB gate: every text-palette reload of a 147-change screen, timed on the # beam, lands between the last stage pixel of the line above its band and the # band's first (~10 s). -x68000-verify-scb: $(BINDIR)/SCB.X +x68000-verify-scb: $(call programBin,SCB) $(REPO_DIR)/scripts/verify-x68000-scb.sh # Golden-hash gate against the Apple IIgs reference (~1 min: UBER with @@ -330,9 +210,4 @@ clean-x68000: # looks like success while leaving stale objects the next build links. clean: clean-x68000 --include $(shell find $(BUILD) -name '*.d' 2>/dev/null) - -# Shorter stem than the %.spr-from-%.png pattern, so it wins for level banks. -$(STAXI_GEN_DIR)/sprites/level%.spr: $(STAXI_GEN_DIR)/levels/level%.dat $(STAXIBAKE_BIN) $(EXAMPLES)/spacetaxi/stLevelCels.c - @mkdir -p $(dir $@) - $(STAXIBAKE_BIN) --level $< $@ +endif diff --git a/scripts/bench-staxi-title.sh b/scripts/bench-staxi-title.sh deleted file mode 100755 index d1ba47f..0000000 --- a/scripts/bench-staxi-title.sh +++ /dev/null @@ -1,164 +0,0 @@ -#!/usr/bin/env bash -# bench-staxi-title.sh - Space Taxi title-screen logo cadence measurement -# (the PERF-AUDIT #3 acceptance test: the logo repaints via 103 -# jlTilePasteMono calls, intended every 4th frame -- stRender.c -# titleAnimateLogo's `& 3u` divider mirroring the C64 original). -# -# Boots GS/OS under headless MAME with a WORK COPY of joey.2mg (must -# contain STAXI + its DATA tree; build with -# JOEY_DISK_EXAMPLES="STAXI" scripts/make-iigs-disk.sh), -# launches STAXI via the standard Finder keystroke timeline, then -# samples 8 known logo tile cells from the SHR framebuffer EVERY frame. -# Each frame the sampled 256 bytes fold into a rolling hash; a hash -# change means the logo visibly changed on screen (the flip-book tile -# differs on every 4-frame step, so a healthy title changes every 4 -# frames; a slow repaint stretches the observed period). -# -# Emits "STAXI-CHG frame=N sum=XXXXXX" per change, heartbeats, and a -# final verify-style summary. Post-process the change frames into -# periods with the analysis snippet in the acceptance report. -# -# Usage: scripts/bench-staxi-title.sh [label] -# MAME_ROMPATH override ROM dir (default ~/.mame/roms) -# STAXI_MAX_FRAMES frame cap (default 15000) -# STAXI_MAX_CHANGES stop after this many logo changes (default 80) -set -euo pipefail - -repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) -source "$repo/scripts/lib/joeyEnv.sh" -joeyNeed LLVM816_ROOT toolchain:iigs -joeyNeed CADIUS cadius -joeyNeed MAME mame -joeyNeed IIGS_ROMPATH iigsFirmware -joeyNeed GSOS_SYSTEM_DISK iigsFirmware - -sys_disk=$GSOS_SYSTEM_DISK -data_disk=$repo/build/iigs/bin/joey.2mg -rompath="$IIGS_ROMPATH" -maxFrames="${STAXI_MAX_FRAMES:-15000}" -maxChanges="${STAXI_MAX_CHANGES:-80}" -label="${1:-run}" - -for f in "$sys_disk" "$data_disk"; do - [ -f "$f" ] || { echo "bench-staxi-title: missing $f" >&2; exit 2; } -done -if ! "$CADIUS" CATALOG "$data_disk" 2>/dev/null | grep -qE '^ STAXI +S16'; then - echo "bench-staxi-title: STAXI not on joey.2mg (JOEY_DISK_EXAMPLES=\"STAXI\" scripts/make-iigs-disk.sh)" >&2 - exit 2 -fi - -work=$(mktemp -d -t joeylib-staxi.XXXXXX) -trap 'rm -rf "$work"' EXIT -cp "$sys_disk" "$work/boot.po" -cp "$data_disk" "$work/joey.2mg" - -cat > "$work/staxi.lua" <<LUA -local cpu = manager.machine.devices[":maincpu"] -local mem = cpu.spaces["program"] -local nat = manager.machine.natkeyboard -local frame = 0 -local idx = 1 - -local function field(port, name) - local p = manager.machine.ioport.ports[port] - if p == nil then return nil end - return p.fields[name] -end -local key_cmd = field(":macadb:KEY3", "Command / Open Apple") -local function press(f) if f then f:set_value(1) end end -local function release(f) if f then f:set_value(0) end end - --- 8 logo tile cells (bx,by) chosen from title.dat's 0x84 map: byte --- offset in SHR = 0xE12000 + (by*8 + r)*160 + bx*4, 4 bytes per row. --- Every repaint pastes a different flip tile into these cells, so --- their content changes on every animation step. -local cells = { {6,1},{11,1},{24,1},{29,1},{12,7},{16,8},{12,9},{15,10} } -local function logoSum() - local sum = 0 - for _, c in ipairs(cells) do - local base = 0xE12000 + (c[2] * 8) * 160 + c[1] * 4 - for r = 0, 7 do - local off = base + r * 160 - for b = 0, 3 do - sum = (sum * 33 + mem:read_u8(off + b)) % 16777213 - end - end - end - return sum -end - -local steps = { - {3000, function() nat:post("J") end}, - {3120, function() press(key_cmd) end}, - {3126, function() nat:post("o") end}, - {3180, function() release(key_cmd) end}, - {3540, function() nat:post("STAXI") end}, - {3660, function() press(key_cmd) end}, - {3666, function() nat:post("o") end}, - {3720, function() release(key_cmd) end}, -} - -local prevSum = -1 -local nChanges = 0 - -local function summary(tag) - local distinct = {} - local nonzero = 0 - local checksum = 0 - for i = 0, 31999 do - local b = mem:read_u8(0xE12000 + i) - if b ~= 0 then nonzero = nonzero + 1 end - distinct[b & 0x0F] = true - distinct[(b >> 4) & 0x0F] = true - checksum = (checksum + b) & 0xFFFFFF - end - local n = 0 - for _ in pairs(distinct) do n = n + 1 end - io.write(string.format("STAXI-%s label=$label frame=%d changes=%d distinctNibbles=%d nonZeroBytes=%d checksum=%06X\n", - tag, frame, nChanges, n, nonzero, checksum)) - io.flush() -end - -emu.register_frame_done(function() - frame = frame + 1 - while idx <= #steps and frame >= steps[idx][1] do - steps[idx][2]() - idx = idx + 1 - end - if frame >= 3800 then - local s = logoSum() - if s ~= prevSum then - io.write(string.format("STAXI-CHG frame=%d sum=%06X\n", frame, s)) - io.flush() - prevSum = s - nChanges = nChanges + 1 - if nChanges >= $maxChanges then - summary("DONE") - manager.machine:exit() - end - end - end - if frame % 1500 == 0 then - io.write(string.format("STAXI-HEART frame=%d changes=%d\n", frame, nChanges)) - io.flush() - end - if frame >= $maxFrames then - summary("TIMEOUT") - manager.machine:exit() - end -end) -LUA - -echo "bench-staxi-title[$label]: running STAXI headless under MAME (cap $maxFrames frames, $maxChanges changes)..." >&2 -cd "$work" -out=$(QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \ - timeout "${STAXI_WALL_TIMEOUT:-900}" "$MAME" apple2gs \ - -rompath "$rompath" \ - -flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \ - -video none -sound none -nothrottle \ - -autoboot_script "$work/staxi.lua" </dev/null 2>&1) || true - -echo "$out" | grep -E '^STAXI-(CHG|HEART|DONE|TIMEOUT)' > "$repo/build/iigs/bin/staxi-title-$label.log" || true -tail -3 "$repo/build/iigs/bin/staxi-title-$label.log" >&2 -lines=$(grep -c '^STAXI-CHG' "$repo/build/iigs/bin/staxi-title-$label.log" || true) -echo "bench-staxi-title[$label]: $lines change events -> build/iigs/bin/staxi-title-$label.log" >&2 diff --git a/scripts/diag-staxi-screen.sh b/scripts/diag-staxi-screen.sh deleted file mode 100755 index 2d2f16b..0000000 --- a/scripts/diag-staxi-screen.sh +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/env bash -# diag-staxi-screen.sh - one-off diagnostic for the STAXI title test: -# boot + launch STAXI, dump the full SHR framebuffer + palettes as hex -# at a target frame, then extract JOEYLOG.TXT from the work disk so the -# game's own init/asset-load log is visible. Outputs land in -# build/iigs/bin/ (staxi-shr.hex, staxi-joeylog.txt). -set -euo pipefail - -repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) -source "$repo/scripts/lib/joeyEnv.sh" -joeyNeed LLVM816_ROOT toolchain:iigs -joeyNeed CADIUS cadius -joeyNeed MAME mame -joeyNeed IIGS_ROMPATH iigsFirmware -joeyNeed GSOS_SYSTEM_DISK iigsFirmware - -sys_disk=$GSOS_SYSTEM_DISK -data_disk=$repo/build/iigs/bin/joey.2mg -rompath="$IIGS_ROMPATH" -dumpFrame="${STAXI_DUMP_FRAME:-9000}" - -work=$(mktemp -d -t joeylib-staxidiag.XXXXXX) -trap 'rm -rf "$work"' EXIT -cp "$sys_disk" "$work/boot.po" -cp "$data_disk" "$work/joey.2mg" - -cat > "$work/diag.lua" <<LUA -local cpu = manager.machine.devices[":maincpu"] -local mem = cpu.spaces["program"] -local nat = manager.machine.natkeyboard -local frame = 0 -local idx = 1 - -local function field(port, name) - local p = manager.machine.ioport.ports[port] - if p == nil then return nil end - return p.fields[name] -end -local key_cmd = field(":macadb:KEY3", "Command / Open Apple") -local function press(f) if f then f:set_value(1) end end -local function release(f) if f then f:set_value(0) end end - -local steps = { - {3000, function() nat:post("J") end}, - {3120, function() press(key_cmd) end}, - {3126, function() nat:post("o") end}, - {3180, function() release(key_cmd) end}, - {3540, function() nat:post("STAXI") end}, - {3660, function() press(key_cmd) end}, - {3666, function() nat:post("o") end}, - {3720, function() release(key_cmd) end}, -} -local pcs = {} - -emu.register_frame_done(function() - frame = frame + 1 - while idx <= #steps and frame >= steps[idx][1] do - steps[idx][2]() - idx = idx + 1 - end - -- Hang localization: sample the PC over 60 frames before the dump, - -- and probe NEWVIDEO (0xE0C029: bit7 = SHR on -> jlpInit ran). - if frame == $dumpFrame - 120 then - io.write(string.format("SHRPROBE newvideo=%02X shadow=%02X\n", - mem:read_u8(0xE0C029), mem:read_u8(0xE0C035))) - io.flush() - end - if frame >= $dumpFrame - 100 and frame < $dumpFrame - 40 then - local pc = cpu.state["PC"].value - local k = string.format("%06X", pc) - pcs[k] = (pcs[k] or 0) + 1 - end - if frame == $dumpFrame - 40 then - for k, v in pairs(pcs) do - io.write(string.format("SHRPC %s count=%d\n", k, v)) - end - io.flush() - end - if frame == $dumpFrame - 20 then - -- Locate the jlLog RAM ring without UBER's mailbox: bulk-read every - -- RAM bank and emit it as hex; the host greps for the build-stamp - -- text. read_range failures are reported, not swallowed. - local banks = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,0xE0,0xE1} - for _, bank in ipairs(banks) do - local base = bank * 65536 - local ok, buf = pcall(function() return mem:read_range(base, base + 65535, 8) end) - if ok and buf ~= nil then - for off = 0, 65535, 256 do - local chunk = string.sub(buf, off + 1, off + 256) - local hex = {} - for i = 1, #chunk do - hex[#hex + 1] = string.format("%02X", string.byte(chunk, i)) - end - io.write(string.format("SHRMEM %02X%04X %s\n", bank, off, table.concat(hex))) - end - else - io.write(string.format("SHRMEM-FAIL bank=%02X err=%s\n", bank, tostring(buf))) - end - end - io.flush() - end - if frame == $dumpFrame then - -- SHR pixel dump: 200 rows x 160 bytes as hex lines. - for row = 0, 199 do - local parts = {} - for b = 0, 159 do - parts[#parts + 1] = string.format("%02X", mem:read_u8(0xE12000 + row * 160 + b)) - end - io.write("SHRROW " .. table.concat(parts) .. "\n") - end - -- SCBs + palettes. - local scb = {} - for i = 0, 15 do scb[#scb + 1] = string.format("%02X", mem:read_u8(0xE19D00 + i)) end - io.write("SHRSCB " .. table.concat(scb, " ") .. "\n") - local pal = {} - for i = 0, 31 do pal[#pal + 1] = string.format("%02X", mem:read_u8(0xE19E00 + i)) end - io.write("SHRPAL " .. table.concat(pal, " ") .. "\n") - io.flush() - manager.machine:exit() - end -end) -LUA - -echo "diag-staxi: dumping SHR at frame $dumpFrame..." >&2 -cd "$work" -out=$(QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \ - timeout 900 "$MAME" apple2gs \ - -rompath "$rompath" \ - -flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \ - -video none -sound none -nothrottle \ - -autoboot_script "$work/diag.lua" </dev/null 2>&1) || true - -echo "$out" | grep -E '^SHR(PROBE|PC)|^SHRMEM-FAIL' | sed 's/^/diag-staxi: /' >&2 -echo "$out" | grep -E '^SHRMEM ' > "$repo/build/iigs/bin/staxi-mem.hex" || true -echo "$out" | grep -E '^SHR(ROW|SCB|PAL)' > "$repo/build/iigs/bin/staxi-shr.hex" -wc -l "$repo/build/iigs/bin/staxi-shr.hex" >&2 - -# The game flushed its log to the volume root; pull it off the WORK copy. -rm -f "$repo/build/iigs/bin/staxi-joeylog.txt" "$work/JOEYLOG.TXT"* 2>/dev/null || true -"$CADIUS" EXTRACTFILE "$work/joey.2mg" "/JOEYLIB/JOEYLOG.TXT" "$work/" >/dev/null 2>&1 || true -found=$(ls "$work"/JOEYLOG.TXT* 2>/dev/null | head -1) -if [ -n "$found" ]; then - tr -d '\r' < "$found" > "$repo/build/iigs/bin/staxi-joeylog.txt" - echo "diag-staxi: extracted JOEYLOG.TXT:" >&2 -else - echo "diag-staxi: no JOEYLOG.TXT on work disk (flush never ran?)" >&2 -fi diff --git a/scripts/lib/dvxGate.sh b/scripts/lib/dvxGate.sh index dc66e31..414fb96 100644 --- a/scripts/lib/dvxGate.sh +++ b/scripts/lib/dvxGate.sh @@ -4,14 +4,17 @@ # without its SDK) plus JoeyLib programs from build/dvx/bin -- whose DVX.INI makes # one of them the desktop, so DVX boots straight into it; and the DOSBox # Staging run of that folder as drive C:. Nothing here writes to the DVX -# project or to toolchains/opt/dvx. Source after joeyEnv.sh. +# project or to toolchains/opt/dvx. Source after joeyEnv.sh. A project built +# on make/project.mk sets DVX_BIN (and DVX_VENDOR, if it changed it) to its +# own before sourcing. DVX_GATE="$(basename "$0" .sh)" -DVX_BIN="$JOEY_REPO/build/dvx/bin" +DVX_BIN="${DVX_BIN:-$JOEY_REPO/build/dvx/bin}" # UBER's probe build (make dvx-probe), packaged apart from bin/. -DVX_PROBE_DIR="$JOEY_REPO/build/dvx/probe" -# The folder a program installs in under apps/, and the name of its log. -DVX_VENDOR=joeylib +DVX_PROBE_DIR="${DVX_PROBE_DIR:-$JOEY_REPO/build/dvx/probe}" +# The folder a program installs in under apps/ (make/dvx.mk's DVX_VENDOR), and +# the name of its log. +DVX_VENDOR="${DVX_VENDOR:-joeylib}" DVX_LOG_NAME=joeylog.txt # The port's own settings file in a program's settings folder, and its # full-screen switch (src/dvx/hal.c). diff --git a/scripts/lib/joeyEnv.sh b/scripts/lib/joeyEnv.sh index b7e8e33..eac1313 100644 --- a/scripts/lib/joeyEnv.sh +++ b/scripts/lib/joeyEnv.sh @@ -38,37 +38,37 @@ joeyNeedFile() { JOEY_DOSBOX_ISOLATE=(--noprimaryconf --nolocalconf) -# joeyExampleProgram NAME PORT -- the built program for an example name. A -# program that ships in its own folder with its DATA (Space Taxi on every -# port but the IIgs) is found inside that folder. When nothing is built yet -# it prints the flat path, so the caller's "not built" message names it. +# joeyExampleProgram NAME PORT [ROOT] -- the built program for a program +# name, in ROOT's build/PORT/bin (ROOT: JoeyLib by default, or a project +# built on make/project.mk, which lays its build/ out the same way). A +# program that ships in its own folder with its DATA (ID_FOLDER) is found +# inside that folder. When nothing is built yet it prints the flat path, so +# the caller's "not built" message names it. joeyExampleProgram() { local name="$1" - local bin="$JOEY_REPO/build/$2/bin" + local bin="${3:-$JOEY_REPO}/build/$2/bin" local up="${name^^}" local ext="" local candidate - [ "$up" = "SPACETAXI" ] && up=STAXI case "$2" in iigs) printf '%s\n' "$bin/$up" return 0 ;; amiga) - # Mixed-case names, and Space Taxi is called Taxi there. + # Mixed-case names. up="${name,,}" - case "$up" in - staxi|spacetaxi) up=taxi ;; - esac up="${up^}" ;; atarist) ext=".PRG" ;; dos) ext=".EXE" ;; x68000) ext=".X" ;; dvx) - # Every program is a folder of its own, as it installs in DVX. + # Every program is a folder of its own, as it installs in DVX: + # apps/<vendor>/<name>/, whatever the vendor. up="${up,,}" - printf '%s\n' "$bin/apps/joeylib/$up/$up.app" + candidate="$(find "$bin/apps" -mindepth 3 -maxdepth 3 -type f -path "*/$up/$up.app" -print -quit 2>/dev/null)" + printf '%s\n' "${candidate:-$bin/apps/joeylib/$up/$up.app}" return 0 ;; *) @@ -93,6 +93,19 @@ joeyExampleProgram() { } +# joeyProgramPath PROGRAM PORT [ROOT] -- the built program a gate is given: +# PROGRAM itself when it is a path (it holds a /), else the program of that +# name (joeyExampleProgram). So a gate runs on a program built anywhere -- +# a project's, or a test build outside bin/ -- as well as on an example. +joeyProgramPath() { + if [[ $1 == */* ]]; then + printf '%s\n' "$1" + return 0 + fi + joeyExampleProgram "$@" +} + + # joeyFindWindow DISPLAY PID -- print the X window owned by process PID. # More robust than matching titles, which differ between emulator builds. joeyFindWindow() { diff --git a/scripts/make-agi-iigs-disk.sh b/scripts/make-agi-iigs-disk.sh deleted file mode 100755 index a24b672..0000000 --- a/scripts/make-agi-iigs-disk.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env bash -# make-agi-iigs-disk.sh - Build the 2MB ProDOS data volume that holds the -# IIgs AGI interpreter plus one AGI v2 game's resources. The -# iigs-game-disk-agi make target merges it into the bootable AGI.2mg that -# joey-run starts (--ram 8M). -# -# A single 800KB 3.5" floppy can't hold the ~240KB AGI binary plus KQ3's -# ~644KB of required resources (878KB total), and MAME's IIgs 3.5 drive is -# 800K-GCR-only (it rejects a 1.44MB image). A CFFA2 hard-disk partition -# has no such cap, so the whole payload lands on one 2MB ProDOS volume. -# -# Usage: scripts/make-agi-iigs-disk.sh <output.2mg> [game-name] -# game-name : subdirectory under examples/agi/gamedata/ (default kq3) -# Requires: ./install.sh install toolchain:iigs; built build/iigs/bin/AGI. -set -euo pipefail - -repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) -source "$repo/scripts/lib/joeyEnv.sh" -joeyNeed LLVM816_ROOT toolchain:iigs -joeyNeed CADIUS cadius -BINDIR="${BINDIR:-$repo/build/iigs/bin}" - -out=${1:?usage: make-agi-iigs-disk.sh <output.2mg> [game]} -game=${2:-kq3} -gamedata=$repo/examples/agi/gamedata/$game -agiBin=$BINDIR/AGI -VOL=AGI - -# The AGI volume name and the game files use uppercase ProDOS names. AGI -# fopen()s LOGDIR / VOL.x etc. from its launch directory; GS/OS sets the -# prefix to the volume it launched from, and ProDOS lookups are -# case-insensitive, so the uppercase names match the lowercase fopen paths. -[ -x "$CADIUS" ] || { echo "make-agi-iigs-disk.sh: cadius not found at $CADIUS" >&2; exit 2; } -[ -f "$agiBin" ] || { echo "make-agi-iigs-disk.sh: $agiBin not built (make -f make/iigs.mk $agiBin)" >&2; exit 1; } -[ -d "$gamedata" ] || { echo "make-agi-iigs-disk.sh: $gamedata is not an AGI v2 game directory" >&2; exit 1; } -[ -f "$gamedata/LOGDIR" ] || { echo "make-agi-iigs-disk.sh: $gamedata has no LOGDIR -- not AGI v2" >&2; exit 1; } - -work=$(mktemp -d -t agi-iigs-disk.XXXXXX) -trap 'rm -rf "$work"' EXIT - -rm -f "$out" -"$CADIUS" CREATEVOLUME "$out" "$VOL" 2048KB >/dev/null - -# AGI interpreter as a ProDOS type $B3 (S16 / GS-OS application). It stays -# at the volume root so Finder lists and launches it. -cp "$agiBin" "$work/AGI#B30000" -"$CADIUS" ADDFILE "$out" "/$VOL" "$work/AGI#B30000" >/dev/null - -# Game resources live under DATA/ -- JoeyLib forces every data-file open -# through that prefix (jlDataOpen), so they must sit in /VOL/DATA. GS/OS -# sets prefix 0 to the launch volume, so AGI's "DATA/LOGDIR" resolves here. -"$CADIUS" CREATEFOLDER "$out" "/$VOL/DATA" >/dev/null - -# Game resources as plain ProDOS BIN ($06). Only the subset AGI v2 reads -# (the four DIR indexes, the VOL.x archives, OBJECT, WORDS.TOK) is copied; -# Sierra's original binary / OVL drivers are left behind. -added=0 -for f in LOGDIR PICDIR VIEWDIR SNDDIR OBJECT WORDS.TOK; do - if [ -f "$gamedata/$f" ]; then - cp "$gamedata/$f" "$work/${f}#060000" - "$CADIUS" ADDFILE "$out" "/$VOL/DATA" "$work/${f}#060000" >/dev/null - added=$((added + 1)) - fi -done -for f in "$gamedata"/VOL.*; do - [ -f "$f" ] || continue - b=$(basename "$f") - cp "$f" "$work/${b}#060000" - "$CADIUS" ADDFILE "$out" "/$VOL/DATA" "$work/${b}#060000" >/dev/null - added=$((added + 1)) -done - -free=$("$CADIUS" CATALOG "$out" 2>/dev/null | grep -oE 'Free : [0-9]+' | grep -oE '[0-9]+' | head -1) -echo "agi-iigs-disk: $out (volume /$VOL, game $game)" -echo " AGI + $added resource files, $free blocks free" diff --git a/scripts/make-iigs-disk.sh b/scripts/make-iigs-disk.sh index 096032a..37672ed 100755 --- a/scripts/make-iigs-disk.sh +++ b/scripts/make-iigs-disk.sh @@ -9,7 +9,16 @@ # would overflow the volume are skipped with a warning rather than corrupting # the image. Override the set with JOEY_DISK_EXAMPLES="DRAW SPRITE ...". # +# A program's data goes on with it, into the volume's DATA folder (programs +# share it): make stages it per program in $JOEY_DISK_DATA/<NAME>/DATA, and the +# files the program runs without (say a game's per-level sprite banks, which it +# JIT-compiles at level entry when one is missing) in <NAME>/OPTIONAL, laid out +# the same way. Those go on last, and only while there is room. +# # Usage: scripts/make-iigs-disk.sh [output.2mg] +# Env: BINDIR the programs (default build/iigs/bin) +# JOEY_DISK_DATA their staged data (default BINDIR/../data) +# NTP_BIN the NTP replayer (default build/iigs/audio/ntpplayer.bin) # Requires: ./install.sh install toolchain:iigs; built IIgs examples. set -euo pipefail @@ -18,11 +27,12 @@ source "$repo/scripts/lib/joeyEnv.sh" joeyNeed LLVM816_ROOT toolchain:iigs joeyNeed CADIUS cadius BINDIR="${BINDIR:-$repo/build/iigs/bin}" +DATA_ROOT="${JOEY_DISK_DATA:-$BINDIR/../data}" # The NTP replayer. DEFAULTED, not optional: without it on the volume every # IIgs example is silent except the ROM boot beep, because jlpAudioInit # fopen()s "ntpplayer" and returns before ntpArm() when that read fails -- # with no diagnostic. Callers used to have to pass NTP_BIN and one of them -# passed the wrong path, which is exactly how Space Taxi ended up mute. +# passed the wrong path, which is exactly how a game ended up mute. NTP_BIN="${NTP_BIN:-$repo/build/iigs/audio/ntpplayer.bin}" OUT="${1:-$BINDIR/joey.2mg}" VOL=JOEYLIB @@ -35,8 +45,8 @@ VOLSIZE="${JOEY_DISK_SIZE:-800KB}" [ -x "$CADIUS" ] || { echo "make-iigs-disk.sh: cadius not found at $CADIUS" >&2; exit 2; } # Priority order: small primitive demos first so DRAW/PATTERN/etc. always fit; -# the large AGI build lands last and is dropped if the floppy is full. -order=(${JOEY_DISK_EXAMPLES:-DRAW PATTERN KEYS JOY SPRITE UBER ADV2 ADV AGI}) +# the larger adventures land last and are dropped if the floppy is full. +order=(${JOEY_DISK_EXAMPLES:-DRAW PATTERN KEYS JOY SPRITE UBER ADV2 ADV}) work=$(mktemp -d -t joeylib-disk.XXXXXX) trap 'rm -rf "$work"' EXIT @@ -58,6 +68,30 @@ freeBlocks() { | grep -oE 'Free : [0-9]+' | grep -oE '[0-9]+' | head -1 } +# fileBlocks FILE -- the blocks FILE takes on the volume: its data blocks, one +# index block per 256 of them, and a little directory slack. +fileBlocks() { + local sz + sz=$(stat -c%s "$1") + printf '%s\n' $(( (sz + 511) / 512 + (sz + 131071) / 131072 + 2 )) +} + +# treeFiles DIR -- every file under DIR, sorted (nothing when DIR is absent). +treeFiles() { + [ -d "$1" ] || return 0 + find "$1" -type f | sort +} + +# treeBlocks DIR -- the blocks every file under DIR takes. +treeBlocks() { + local f + local total=0 + while IFS= read -r f; do + total=$(( total + $(fileBlocks "$f") )) + done < <(treeFiles "$1") + printf '%s\n' "$total" +} + # addDataFile FILE FOLDER -- add FILE to FOLDER on the volume as a plain ProDOS # BIN ($06) file under its upper-cased name. GS/OS ProDOS lookups are # case-insensitive, so a program's lowercase fopen paths still match. @@ -69,15 +103,32 @@ addDataFile() { rm -f "$work/${base^^}#060000" } +# addToData FILE ROOT -- add FILE, which lies under ROOT, to the volume's DATA +# folder at the same place (subfolders upper-cased). CREATEFOLDER makes the +# missing parents and leaves a folder another program's data made alone. +addToData() { + local rel + local dst="/$VOL/DATA" + rel=$(dirname "${1#"$2/"}") + if [ "$rel" != "." ]; then + dst="$dst/${rel^^}" + fi + "$CADIUS" CREATEFOLDER "$OUT" "$dst" >/dev/null + addDataFile "$1" "$dst" +} + added=() skipped=() +programs=() +# Blocks promised to the data of the programs already on: it goes on after +# them and NTP, so every later fit check leaves it room. +reserved=0 for name in "${order[@]}"; do bin="$BINDIR/$name" [ -f "$bin" ] || continue - sz=$(stat -c%s "$bin") - # data blocks + one index block per 256 data blocks + a little dir slack - need=$(( (sz + 511) / 512 + (sz + 131071) / 131072 + 2 )) - if [ "$need" -gt "$(freeBlocks)" ]; then + dataNeed=$(treeBlocks "$DATA_ROOT/$name/DATA") + need=$(( $(fileBlocks "$bin") + dataNeed )) + if [ "$need" -gt $(( $(freeBlocks) - reserved )) ]; then skipped+=("$name") continue fi @@ -87,6 +138,8 @@ for name in "${order[@]}"; do rm -f "$work/$name#B30000" if "$CADIUS" CATALOG "$OUT" 2>/dev/null | grep -qE "^ $name +S16"; then added+=("$name") + programs+=("$name") + reserved=$((reserved + dataNeed)) else skipped+=("$name") fi @@ -99,7 +152,7 @@ ntpOk=0 if [ -f "$NTP_BIN" ]; then ntpSz=$(stat -c%s "$NTP_BIN") ntpNeed=$(( (ntpSz + 511) / 512 + 2 )) - if [ "$ntpNeed" -le "$(freeBlocks)" ]; then + if [ "$ntpNeed" -le $(( $(freeBlocks) - reserved )) ]; then cp "$NTP_BIN" "$work/NTPPLAYER#060000" "$CADIUS" ADDFILE "$OUT" "/$VOL" "$work/NTPPLAYER#060000" >/dev/null rm -f "$work/NTPPLAYER#060000" @@ -123,73 +176,35 @@ make-iigs-disk.sh: WARNING - NTPPLAYER is NOT on $OUT. EOF fi -# Space Taxi runtime data: the game fopen()s a DATA/ tree -- levels/*.dat, -# sprites/staxicels.spc, the optional sprites/levelNN.spc and speech.bin -# (stLevel.c, stRender.c, stAudio.c); tiles and the font are C64 data -# compiled into the binary. Packed only when STAXI made it onto the disk, -# from the IIgs bake at examples/spacetaxi/generated/iigs, and ONLY those -# files: the bake directory also holds intermediates (the .spr the .spc -# compile from) and, in an old tree, art the game stopped loading. -staxiData="$repo/examples/spacetaxi/generated/iigs" -if [[ " ${added[*]:-} " == *" STAXI "* ]]; then - if [ -d "$staxiData" ]; then - "$CADIUS" CREATEFOLDER "$OUT" "/$VOL/DATA" >/dev/null - "$CADIUS" CREATEFOLDER "$OUT" "/$VOL/DATA/LEVELS" >/dev/null - "$CADIUS" CREATEFOLDER "$OUT" "/$VOL/DATA/SPRITES" >/dev/null - dataAdded=0 - while IFS= read -r f; do - rel=${f#"$staxiData/"} - dir=$(dirname "$rel") - dst="/$VOL/DATA" - if [ "$dir" != "." ]; then - dst="/$VOL/DATA/${dir^^}" - fi - addDataFile "$f" "$dst" - dataAdded=$((dataAdded + 1)) - done < <(find "$staxiData/levels" -type f -name '*.dat' | sort - printf '%s\n' "$staxiData/sprites/staxicels.spc" "$staxiData/speech.bin") - # The per-level hook-sprite banks are optional (the game JIT-compiles - # a level's cels at entry when its bank is missing) and large, so - # they go on last and only while the floppy has room for them. - levelSkipped=0 - while IFS= read -r f; do - sz=$(stat -c%s "$f") - need=$(( (sz + 511) / 512 + (sz + 131071) / 131072 + 2 )) - if [ "$need" -gt "$(freeBlocks)" ]; then - levelSkipped=$((levelSkipped + 1)) - continue - fi - addDataFile "$f" "/$VOL/DATA/SPRITES" - dataAdded=$((dataAdded + 1)) - done < <(find "$staxiData" -type f -path '*/sprites/level*.spc' | sort) - added+=("DATA[$dataAdded files]") - if [ "$levelSkipped" -gt 0 ]; then - echo " note: $levelSkipped level sprite bank(s) left off (floppy full; those levels JIT at entry)" >&2 +# Each packed program's DATA, then whatever of its optional data fits. +for name in "${programs[@]}"; do + count=0 + while IFS= read -r f; do + addToData "$f" "$DATA_ROOT/$name/DATA" + count=$((count + 1)) + done < <(treeFiles "$DATA_ROOT/$name/DATA") + if [ "$count" -gt 0 ]; then + added+=("$name/DATA[$count files]") + fi +done +for name in "${programs[@]}"; do + count=0 + left=0 + while IFS= read -r f; do + if [ "$(fileBlocks "$f")" -gt "$(freeBlocks)" ]; then + left=$((left + 1)) + continue fi - else - echo " WARNING: STAXI packed without its DATA tree ($staxiData missing)" >&2 + addToData "$f" "$DATA_ROOT/$name/OPTIONAL" + count=$((count + 1)) + done < <(treeFiles "$DATA_ROOT/$name/OPTIONAL") + if [ "$count" -gt 0 ]; then + added+=("$name/OPTIONAL[$count files]") fi -fi - -# The tile-bank example's DATA/tilebank, staged by make/iigs.mk from the host -# gate's fixture (tests/host/tileBankStage.py): its own bake, every other -# target's bake (each must be refused) and the expected pixels. Packed only -# when TILEBANK made it onto the disk. CREATEFOLDER makes the missing parent -# and leaves a DATA folder Space Taxi's data already made alone. -tileBankData="$BINDIR/DATA/tilebank" -if [[ " ${added[*]:-} " == *" TILEBANK "* ]]; then - if [ -d "$tileBankData" ]; then - "$CADIUS" CREATEFOLDER "$OUT" "/$VOL/DATA/TILEBANK" >/dev/null - tileBankAdded=0 - for f in "$tileBankData"/*; do - addDataFile "$f" "/$VOL/DATA/TILEBANK" - tileBankAdded=$((tileBankAdded + 1)) - done - added+=("DATA/TILEBANK[$tileBankAdded files]") - else - echo " WARNING: TILEBANK packed without its DATA/tilebank ($tileBankData missing)" >&2 + if [ "$left" -gt 0 ]; then + echo " note: $left optional $name data file(s) left off (volume full; $name runs without them)" >&2 fi -fi +done echo "iigs-disk: $OUT (volume /$VOL)" echo " added: ${added[*]:-none}" diff --git a/scripts/make-iigs-game-disk.sh b/scripts/make-iigs-game-disk.sh index 94d24b9..a9450d5 100755 --- a/scripts/make-iigs-game-disk.sh +++ b/scripts/make-iigs-game-disk.sh @@ -27,7 +27,7 @@ # # Usage: scripts/make-iigs-game-disk.sh <out> <PROG> [volume] [data.2mg] # out .po for an 800KB floppy, .2mg for anything larger -# PROG uppercase binary name, e.g. STAXI +# PROG uppercase binary name, e.g. DRAW # volume boot volume name, default = PROG # data.2mg disk holding PROG + ntpplayer + DATA, # default build/iigs/bin/joey.2mg diff --git a/scripts/profile-x68000.sh b/scripts/profile-x68000.sh index 97666e7..1600b4f 100755 --- a/scripts/profile-x68000.sh +++ b/scripts/profile-x68000.sh @@ -19,18 +19,18 @@ # 150 ms or more, so each frame still gets several samples and the shape of the # profile is clear. Read it as "which functions dominate", never as a timing. # -# Usage: scripts/profile-x68000.sh [example] [seconds] [skipSeconds] -# example .X base name in build/x68000/bin, default staxi +# Usage: scripts/profile-x68000.sh program [seconds] [skipSeconds] +# program the built .X: its path (a project's, say), or an +# example's name # seconds emulated seconds to run, default 240 # skipSeconds ignore samples before this, default 130 (boot + menu) # Env: X68K_DIR dir holding HUMAN302.XDF + roms/ (default: the installer's toolchains/firmware/x68000) -# Requires: the example built WITH symbols (build/x68000/bin/<NAME>.X.elf). +# Requires: the program built WITH symbols (<NAME>.X.elf beside it). set -uo pipefail -prog=${1:-staxi} +prog=${1:?usage: profile-x68000.sh program [seconds] [skipSeconds]} seconds=${2:-240} skip=${3:-130} -PROG=$(echo "$prog" | tr '[:lower:]' '[:upper:]') repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) source "$repo/scripts/lib/joeyEnv.sh" @@ -38,7 +38,8 @@ joeyNeed MAME mame joeyNeed X68K_DIR x68kFirmware joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom source "$repo/scripts/lib/x68kDisk.sh" -bin=$(joeyExampleProgram "$prog" x68000) || exit 2 +bin=$(joeyProgramPath "$prog" x68000) || exit 2 +PROG=$(basename "$bin" .X) elf=$bin.elf joeyNeed X68K_ROOT toolchain:x68000 export PATH="$X68K_ROOT/bin:$PATH" @@ -71,8 +72,8 @@ trap 'rm -rf "$work"' EXIT x68kBootDisk "$work/boot.xdf" 'B:' "B:\\$PROG.X" || exit 1 x68kDataDisk "$work/data.xdf" || exit 1 x68kXdf add "$work/data.xdf" "$bin" "$PROG.X" >/dev/null || exit 1 -# The program's DATA tree sits next to it: its own folder's for Space Taxi, -# the shared bin/DATA for the rest. +# The program's DATA tree sits next to it: its own folder's for a program +# that ships in one (ID_FOLDER), the shared bin/DATA for the rest. if [[ -d $(dirname "$bin")/DATA ]]; then x68kAddTree "$work/data.xdf" "$(dirname "$bin")/DATA" DATA || exit 1 fi diff --git a/scripts/test-agi.sh b/scripts/test-agi.sh deleted file mode 100755 index b92fc04..0000000 --- a/scripts/test-agi.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env bash -# Host-side regression tests for the AGI port. -# -# Compiles tests/agi/testAgiRes.c + examples/agi/agiRes.c with the -# host gcc and runs it against every game directory in -# examples/agi/gamedata/. Each game is exercised in isolation; the -# script aggregates pass/fail counts and exits non-zero if any test -# fails. -# -# Why a host build instead of running the emulator binaries: the AGI -# resource loader is plain stdio C with no platform dependencies, so -# the parsing logic is identical on every JoeyLib target. Validating -# it on the build host means each iteration takes a second instead -# of the 15-30s of an emulator boot, and reproducible failures don't -# depend on capturing video / log output through DOSBox or MAME. -# -# Usage: -# scripts/test-agi.sh # test every subdir of gamedata/ -# scripts/test-agi.sh kq3 agidemo # test only the named subdirs - -set -euo pipefail - -repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) -gamedata=$repo/examples/agi/gamedata -build=$repo/build/tests -testRes=$build/testAgiRes -testPic=$build/testAgiPic -testView=$build/testAgiView -testVm=$build/testAgiVm -testPipeline=$build/testAgiPipeline - -mkdir -p "$build" - -# Shared host compile flags. agiPic.c calls jlDrawPixel in the blit -# step, which the host test never invokes; we stub it as an empty -# function via the link-line shim below so the host build can resolve -# the symbol without dragging in the JoeyLib runtime. -# Pick a JoeyLib platform for the host build. None of the platform- -# specific code paths are exercised by the host test (it only touches -# the AGI parsers, never the JoeyLib HAL), but joey/platform.h #errors -# unless exactly one is defined. DOS is the closest fit for a Linux -# host (chunky, little-endian, x86 lineage). -hostCflags=(-std=c99 -Wall -Wextra -Werror -O2 -DJOEYLIB_PLATFORM_DOS -I "$repo/include" -I "$repo/examples/agi" -I "$repo/src/core") -stubSrc=$build/hostStubs.c -cat > "$stubSrc" <<'EOF' -#include "joey/draw.h" -#include "joey/core.h" -#include "joey/file.h" -#include "surfaceInternal.h" -#include <stdlib.h> -#include <stdio.h> -#include <string.h> -// Host jlDataOpen: the real one (assetLoad.c) forces a DATA/ prefix, but -// the test gamedata dirs hold their files flat. Open <AGI_DATA_DIR>/<name> -// so agiRes.c's bare-name jlDataOpen calls resolve against the game dir the -// harness points us at. -FILE *jlDataOpen(const char *name, const char *mode) { - const char *base = getenv("AGI_DATA_DIR"); - char path[1024]; - if (base == NULL) { base = "."; } - if ((size_t)snprintf(path, sizeof(path), "%s/%s", base, name) >= sizeof(path)) { return NULL; } - return fopen(path, mode); -} -void jlDrawPixel(jlSurfaceT *s, int16_t x, int16_t y, uint8_t c) { (void)s; (void)x; (void)y; (void)c; } -void jlFillRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t c) { (void)s; (void)x; (void)y; (void)w; (void)h; (void)c; } -uint32_t jlRandom(void) { return (uint32_t)rand(); } -uint16_t jlRandomRange(uint16_t bound) { return (bound == 0u) ? 0u : (uint16_t)((unsigned)rand() % bound); } -void jlRandomSeed(uint32_t seed) { srand((unsigned)seed); } -void *jlAlloc(uint32_t bytes) { return malloc((size_t)bytes); } -void jlFree(void *p) { free(p); } -void jlLogF(const char *fmt, ...) { (void)fmt; } -void jlLogFlush(void) { } -void jlLog(const char *msg) { (void)msg; } -/* surfaceMarkDirtyRect is a MACRO since the Phase-6 mark rework -- the - * host TUs (agiPic/agiView) expand it inline, so the stubs provide the - * band state it references instead of a function. gStage stays NULL, - * so every expansion takes the s != gStage no-op branch; the symbols - * only need to exist to link. */ -jlSurfaceT *gStage = 0; -uint8_t gStageMinWord[SURFACE_HEIGHT]; -uint8_t gStageMaxWord[SURFACE_HEIGHT]; -void surfaceMarkDirtyRows(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord) { (void)y; (void)yEnd; (void)minWord; (void)maxWord; } -void surfaceMarkDirtyAll(const jlSurfaceT *s) { (void)s; } -EOF - -echo "Building host tests..." -gcc "${hostCflags[@]}" \ - "$repo/tests/agi/testAgiRes.c" \ - "$repo/examples/agi/agiRes.c" \ - "$stubSrc" \ - -o "$testRes" - -gcc "${hostCflags[@]}" \ - "$repo/tests/agi/testAgiPic.c" \ - "$repo/examples/agi/agiRes.c" \ - "$repo/examples/agi/agiPic.c" \ - "$stubSrc" \ - -o "$testPic" - -gcc "${hostCflags[@]}" \ - "$repo/tests/agi/testAgiView.c" \ - "$repo/examples/agi/agiRes.c" \ - "$repo/examples/agi/agiPic.c" \ - "$repo/examples/agi/agiView.c" \ - "$stubSrc" \ - -o "$testView" - -gcc "${hostCflags[@]}" \ - "$repo/tests/agi/testAgiVm.c" \ - "$repo/examples/agi/agiRes.c" \ - "$repo/examples/agi/agiVm.c" \ - "$repo/examples/agi/agiObj.c" \ - "$stubSrc" \ - -o "$testVm" - -gcc "${hostCflags[@]}" \ - "$repo/tests/agi/testAgiPipeline.c" \ - "$repo/examples/agi/agiRes.c" \ - "$repo/examples/agi/agiVm.c" \ - "$repo/examples/agi/agiObj.c" \ - "$stubSrc" \ - -o "$testPipeline" - -# Pick the game directories to exercise. With no args, every -# subdirectory of examples/agi/gamedata that contains LOGDIR is a -# candidate (skips stray README files etc). -if [[ $# -gt 0 ]]; then - games=("$@") -else - games=() - if [[ -d $gamedata ]]; then - for dir in "$gamedata"/*/; do - [[ -d $dir ]] || continue - name=$(basename "$dir") - if [[ -f $dir/LOGDIR ]]; then - games+=("$name") - fi - done - fi -fi - -if [[ ${#games[@]} -eq 0 ]]; then - echo - echo "No AGI game directories found under $gamedata" - echo "Drop an AGI v2 game in examples/agi/gamedata/<name>/ and rerun." - echo "See examples/agi/gamedata/README.md for the required files." - exit 0 -fi - -passed=0 -failed=0 -echo -for game in "${games[@]}"; do - dir=$gamedata/$game - echo "===== $game =====" - if [[ ! -d $dir ]]; then - echo " FAIL: $dir does not exist" - failed=$((failed + 1)) - continue - fi - - # agiRes.c opens data by bare name via jlDataOpen; the host stub - # resolves those against AGI_DATA_DIR (see stubSrc above). - export AGI_DATA_DIR="$dir" - gameOk=1 - echo "--- resource loader ---" - if ! "$testRes" "$dir"; then - gameOk=0 - fi - echo "--- PIC decoder ---" - if ! "$testPic" "$dir"; then - gameOk=0 - fi - echo "--- VIEW decoder ---" - if ! "$testView" "$dir"; then - gameOk=0 - fi - echo "--- LOGIC VM (logic.0) ---" - if ! "$testVm" "$dir"; then - gameOk=0 - fi - echo "--- VM pipeline (callbacks + room transitions) ---" - if ! "$testPipeline" "$dir"; then - gameOk=0 - fi - - if [[ $gameOk -eq 1 ]]; then - passed=$((passed + 1)) - else - failed=$((failed + 1)) - fi - echo -done - -total=$((passed + failed)) -echo "===== summary: $passed/$total passed =====" -if [[ $failed -gt 0 ]]; then - exit 1 -fi -exit 0 diff --git a/scripts/verify-all.sh b/scripts/verify-all.sh index ec6dd97..a6639cf 100755 --- a/scripts/verify-all.sh +++ b/scripts/verify-all.sh @@ -30,7 +30,8 @@ # at 8/15/32 bpp and 2x/3x; Ctrl+Esc must not end the program) # x68000 and dvx: also both joysticks -- every direction, both # buttons and every release, read back from examples/joy's display -# audio six gates, two kinds: +# audio six gates, two kinds (the IIgs and X68000 on AUDIOPRB, AUDIO +# pressing its own keys; the rest drive AUDIO's keys): # iigs DOC wavetable + sound IRQ vector (register tap) # x68000 YM2151 noise + PPI port C + ADPCM rate (register tap) # atarist YM2149 write trace, with the pc (register tap) @@ -53,8 +54,9 @@ # modem port at 9600 and a Super Serial Card at 2400 # x68000: both serial streams byte-exact and the mouse right while # the SCB changes palette on every line of a run (9600, 19200, 38400) -# disk iigs: the one-disk STAXI (ROM 3 and ROM 01) and AGI images boot -# and draw +# disk iigs: a one-program boot volume (DRAW's) boots, launches the +# program and draws: as a 2048KB volume off a CFFA2 card, then as +# an 800KB floppy on ROM 3 and on ROM 01 # # `all` first BUILDS every selected port (make <port>, and iigs-disk plus the # loss probes and game disks for the IIgs), stopping on a build failure, then @@ -134,8 +136,8 @@ input dos input scripts/verify-dos-input.sh input dvx input scripts/verify-dvx-input.sh input dvx depths scripts/verify-dvx-input.sh 640x480x8:1 800x600x15:2 1024x768x32:3 input dvx joy scripts/verify-dvx-joy.sh -audio iigs audio scripts/verify-iigs-audio.sh staxi -audio x68000 audio scripts/verify-x68000-audio.sh +audio iigs audio make -s -f make/iigs.mk iigs-verify-audio || exit 1 +audio x68000 audio make -s -f make/x68000.mk x68000-verify-audio || exit 1 audio atarist audio scripts/verify-atarist-audio.sh audio amiga audio scripts/verify-amiga-audio.sh audio dos audio scripts/verify-dos-audio.sh @@ -152,9 +154,9 @@ mouse x68000 sermouse scripts/verify-x68000-serial-mouse.sh save iigs save make -s -f make/iigs.mk iigs-verify-save || exit 1 loss iigs loss make -s -f make/iigs.mk iigs-verify-serial-loss || exit 1 loss x68000 scb scripts/verify-x68000-serial-scb.sh -disk iigs staxi make -s -f make/iigs.mk iigs-verify-game-disk GAME=STAXI || exit 1 -disk iigs staxi01 GAME_VERIFY_MACHINE=apple2gsr1 scripts/verify-iigs-game-disk.sh build/iigs/bin/STAXI.2mg -disk iigs agi make -s -f make/iigs.mk iigs-game-disk-agi || exit 1; scripts/verify-iigs-game-disk.sh build/iigs/bin/AGI.2mg 2 8M +disk iigs cffa2 make -s -f make/iigs.mk iigs-verify-game-disk GAME=DRAW GAME_DISK_SIZE=2048KB || exit 1 +disk iigs floppy make -s -f make/iigs.mk iigs-verify-game-disk GAME=DRAW || exit 1 +disk iigs rom01 GAME_VERIFY_MACHINE=apple2gsr1 scripts/verify-iigs-game-disk.sh build/iigs/bin/DRAW.2mg EOF ) diff --git a/scripts/verify-atarist-audio.sh b/scripts/verify-atarist-audio.sh index 64dcc85..1785f03 100755 --- a/scripts/verify-atarist-audio.sh +++ b/scripts/verify-atarist-audio.sh @@ -39,8 +39,9 @@ # delivered between two polls is never seen. A bare `keypress` produces a # run with the tracker alone and no pump at all. # -# STAXI is a poor subject here despite exercising both layers: it STOPS -# the music while speech plays, so check 4 can never pass on it. +# A program that stops its music while it plays a sample (as a game may +# for speech) is a poor subject here despite exercising both layers: +# check 4 can never pass on it. # # Usage: scripts/verify-atarist-audio.sh [example] # example PRG base name in build/atarist/bin, default AUDIO @@ -59,7 +60,8 @@ taps=${ST_AUDIO_TAPS:-6} tos=$EMUTOS_IMG binDir=$repo/build/atarist/bin -# Space Taxi ships as its own folder; everything else is a bare PRG. +# A program that ships in a folder of its own (ID_FOLDER) is found there; +# everything else is a bare PRG. if [ -d "$binDir/$prog" ]; then driveDir=$binDir/$prog else @@ -88,8 +90,8 @@ cleanup() { trap cleanup EXIT INT TERM [ -n "${ST_AUDIO_KEEP:-}" ] && echo "verify-atarist-audio: work dir $work" >&2 -# 4 MB, not the stock 1 MB: STAXI's loader calls 1 MB a fatal asset -# failure and this gate is about sound, not about memory. +# 4 MB, not the stock 1 MB: a big program's loader may call 1 MB a fatal +# asset failure, and this gate is about sound, not about memory. # # The psg_write trace runs ~800 KB/s, so it is filtered as it is produced # -- keeping the raw stream would put hundreds of MB in the work dir. diff --git a/scripts/verify-dos-audio.sh b/scripts/verify-dos-audio.sh index 5e815fe..92fa9b6 100755 --- a/scripts/verify-dos-audio.sh +++ b/scripts/verify-dos-audio.sh @@ -63,7 +63,8 @@ for tool in Xvfb xdotool parec pactl python3; do done pulseEnsureServer || { echo "$label: no PulseAudio/PipeWire server to record from (tried XDG_RUNTIME_DIR and /run/user/$(id -u)/pulse/native)" >&2; exit 2; } -# Space Taxi ships as its own folder; everything else is a bare EXE. +# A program that ships in a folder of its own (ID_FOLDER) is found there; +# everything else is a bare EXE. if [ -f "$binDir/$prog/$prog.EXE" ]; then mountDir=$binDir/$prog elif [ -f "$binDir/$prog.EXE" ]; then diff --git a/scripts/verify-iigs-audio.sh b/scripts/verify-iigs-audio.sh index 05aee3e..b4508f6 100755 --- a/scripts/verify-iigs-audio.sh +++ b/scripts/verify-iigs-audio.sh @@ -31,17 +31,22 @@ # in src/iigs/audio_full.c), and bytes only land there when # NTPstreamsound runs. Every slot is hashed twice, frames # apart. Which slots an application uses is its own business -# -- AUDIO's tracker takes 0-2, Space Taxi only ever reaches -# the noise slot 3 -- so the gate asks that SOME slot carry a +# -- AUDIO's tracker takes 0-2 and its noise 3, its streamed +# phrase 4 -- so the gate asks that SOME slot carry a # waveform and that SOME slot's hash move between the probes. # A moving hash is the live-interrupt evidence: NTP streams # the sample through the page in 256-byte halves, so a dead # interrupt leaves whatever the one initial copy wrote. # -# Usage: scripts/verify-iigs-audio.sh [example] -# example lowercase name, default "audio". A single-example -# disk is built for it (joey.2mg cannot hold them all). -# Requires: ./install.sh install iigs; the example built under build/iigs/bin. +# Usage: scripts/verify-iigs-audio.sh [program] +# program the built program: its path (a project's, say), or an +# example's name. Default build/iigs/probe/bin/AUDIOPRB, +# the AUDIO example pressing its own keys: tracker music +# with noise drums, a noise sweep and a streamed phrase +# (make -f make/iigs.mk iigs-verify-audio builds it and +# runs this). A one-program disk is built for it, with +# its DATA from the data/ beside its bin/ folder. +# Requires: ./install.sh install iigs; the program built. set -euo pipefail repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) @@ -49,8 +54,8 @@ source "$repo/scripts/lib/joeyEnv.sh" joeyNeed MAME mame joeyNeed IIGS_ROMPATH iigsFirmware joeyNeed GSOS_SYSTEM_DISK iigsFirmware -example=${1:-audio} -NAME=${example^^} +prog=$(joeyProgramPath "${1:-$repo/build/iigs/probe/bin/AUDIOPRB}" iigs) || exit 2 +NAME=$(basename "$prog") sys_disk=$GSOS_SYSTEM_DISK rompath="$IIGS_ROMPATH" @@ -64,15 +69,14 @@ basePage=0xC0 pageStep=2 [ -f "$sys_disk" ] || { echo "verify-iigs-audio: missing $sys_disk" >&2; exit 2; } -[ -f "$repo/build/iigs/bin/$NAME" ] || { echo "verify-iigs-audio: missing build/iigs/bin/$NAME (run 'make iigs-disk')" >&2; exit 2; } +[ -f "$prog" ] || { echo "verify-iigs-audio: missing $prog (not built)" >&2; exit 2; } work=$(mktemp -d -t joeylib-audio.XXXXXX) trap 'rm -rf "$work"' EXIT -# One-example volume: the 800KB floppy cannot hold every example, and the -# shared joey.2mg drops AUDIO and STAXI first (see make-iigs-disk.sh). -JOEY_DISK_EXAMPLES="$NAME" BINDIR="$repo/build/iigs/bin" \ - NTP_BIN="$repo/build/iigs/audio/ntpplayer.bin" \ +# A one-program volume: the 800KB floppy cannot hold every example, and the +# shared joey.2mg leaves AUDIO off (see make-iigs-disk.sh). +JOEY_DISK_EXAMPLES="$NAME" BINDIR="$(dirname "$prog")" \ "$repo/scripts/make-iigs-disk.sh" "$work/joey.2mg" >/dev/null cp "$sys_disk" "$work/boot.po" diff --git a/scripts/verify-iigs-tilebank.sh b/scripts/verify-iigs-tilebank.sh index 3d9d866..001a0a2 100755 --- a/scripts/verify-iigs-tilebank.sh +++ b/scripts/verify-iigs-tilebank.sh @@ -28,7 +28,7 @@ # Env: TILEBANK_DATA_DIR stage this DATA/tilebank instead (negative controls) # Requires: ./install.sh install iigs; MAME with the apple2gs ROM and the GS/OS # system disk; a host cc; build/iigs/bin/TILEBANK and its -# DATA/tilebank. +# DATA/tilebank (staged in build/iigs/data/TILEBANK/DATA). set -uo pipefail repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) @@ -41,7 +41,7 @@ joeyNeed IIGS_ROMPATH iigsFirmware joeyNeed GSOS_SYSTEM_DISK iigsFirmware binDir=$repo/build/iigs/bin -dataDir=$(tileBankData "$binDir") +dataDir=$(tileBankData "$repo/build/iigs/data/TILEBANK") wall=900 # hard cap on MAME, whatever happens to this script frames=24000 # emulated frames before the Lua gives up on the lamp @@ -51,11 +51,12 @@ tileBankReference work=$(mktemp -d -t joeylib-iigs-tilebank.XXXXXX) trap 'rm -rf "$work"' EXIT -# make-iigs-disk.sh packs BINDIR's TILEBANK and BINDIR/DATA/tilebank, so a -# scratch BINDIR carries whichever DATA/tilebank this run stages. -mkdir -p "$work/bin/DATA" "$work/log" +# make-iigs-disk.sh packs BINDIR's TILEBANK and its DATA, staged in +# BINDIR/../data/TILEBANK/DATA, so a scratch BINDIR carries whichever +# DATA/tilebank this run stages. +mkdir -p "$work/bin" "$work/data/TILEBANK/DATA" "$work/log" cp "$binDir/TILEBANK" "$work/bin/" -cp -r "$dataDir" "$work/bin/DATA/tilebank" +cp -r "$dataDir" "$work/data/TILEBANK/DATA/tilebank" BINDIR="$work/bin" JOEY_DISK_EXAMPLES="TILEBANK" "$repo/scripts/make-iigs-disk.sh" "$work/joey.2mg" > /dev/null || exit 1 if ! "$CADIUS" CATALOG "$work/joey.2mg" 2> /dev/null | grep -qE '^ +OWN\.TBK +BIN'; then echo "verify-iigs-tilebank: FAIL - the disk has no DATA/TILEBANK/OWN.TBK" >&2 diff --git a/scripts/verify-x68000-audio.sh b/scripts/verify-x68000-audio.sh index 106537d..48994be 100755 --- a/scripts/verify-x68000-audio.sh +++ b/scripts/verify-x68000-audio.sh @@ -24,25 +24,30 @@ # average: the port stops the chip between sounds, so an average # over a mostly silent run would say nothing. # -# Space Taxi is the default because its attract demo exercises the whole -# noise curve (all seventeen distinct table values) and speaks. +# The default subject is JoeyLib's audio probe, examples/audio built to +# press its own keys: it sweeps the noise generator through all 32 pitches, +# which reaches every distinct kOpmNoiseFreq value, plays its tracker song's +# noise drums, and keeps a phrase streaming through the ADPCM. # -# Usage: scripts/verify-x68000-audio.sh [example] [seconds] +# Usage: scripts/verify-x68000-audio.sh [program] [seconds] +# program the built program: its path (a project's, say), or an +# example's name. Default build/x68000/probe/AUDIOPRB.X +# (make -f make/x68000.mk x68000-verify-audio builds it +# and runs this). Its DATA folder, if it has one beside +# it, goes on the floppy with it. # Env: X68K_DIR dir holding HUMAN302.XDF + roms/ (default: the installer's toolchains/firmware/x68000) -# Requires: the example built (make -f make/x68000.mk), MAME with x68000. +# Requires: the program built, MAME with x68000. set -uo pipefail -prog=${1:-staxi} -seconds=${2:-160} -PROG=$(echo "$prog" | tr '[:lower:]' '[:upper:]') - repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) source "$repo/scripts/lib/joeyEnv.sh" joeyNeed MAME mame joeyNeed X68K_DIR x68kFirmware joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom source "$repo/scripts/lib/x68kDisk.sh" -bin=$(joeyExampleProgram "$prog" x68000) || exit 2 +bin=$(joeyProgramPath "${1:-$repo/build/x68000/probe/AUDIOPRB.X}" x68000) || exit 2 +seconds=${2:-160} +PROG=$(basename "$bin" .X) source=$repo/src/x68000/audio.c [[ -f $bin ]] || { echo "verify-x68000-audio: $bin not built" >&2; exit 2; } @@ -65,8 +70,8 @@ trap 'rm -rf "$work"' EXIT x68kBootDisk "$work/boot.xdf" 'B:' "B:\\$PROG.X" || exit 1 x68kDataDisk "$work/data.xdf" || exit 1 x68kXdf add "$work/data.xdf" "$bin" "$PROG.X" >/dev/null || exit 1 -# The program's DATA tree sits next to it: its own folder's for Space Taxi, -# the shared bin/DATA for the rest. +# The program's DATA tree sits next to it: its own folder's for a program +# that ships in one (ID_FOLDER), the shared bin/DATA for the rest. if [[ -d $(dirname "$bin")/DATA ]]; then x68kAddTree "$work/data.xdf" "$(dirname "$bin")/DATA" DATA || exit 1 fi diff --git a/src/core/debug.c b/src/core/debug.c index e7be433..463bfa5 100644 --- a/src/core/debug.c +++ b/src/core/debug.c @@ -143,11 +143,11 @@ void jlLogFlush(void) { // fclose) was the first floppy WRITE any app performed and wedged // forever in SmartPort firmware retries under headless MAME -- the // same #82 failure class the RAM ring exists to avoid. UBER never -// calls jlLogReset, so the write path lay unexercised until the -// spacetaxi acceptance test hit it (hang before the first log line, -// PC spinning in the ROM WAIT loop). jlLogFlush opens with "w" now, -// so the on-disk file still ends up truncated-to-current on the next -// flush. +// calls jlLogReset, so the write path lay unexercised until Space +// Taxi's acceptance test (a separate project) hit it (hang before the +// first log line, PC spinning in the ROM WAIT loop). jlLogFlush opens +// with "w" now, so the on-disk file still ends up truncated-to-current +// on the next flush. void jlLogReset(void) { gJoeyLogRingHead = 0; } diff --git a/src/generic/genericSurface.c b/src/generic/genericSurface.c index 110cd66..d06248e 100644 --- a/src/generic/genericSurface.c +++ b/src/generic/genericSurface.c @@ -163,8 +163,8 @@ void jlpGenericStageFreePixels(uint8_t *pixels) { // item 4), so the old calloc here zero-filled 32000 bytes through // pointer 1 and shredded bank 0 on the first OFFSCREEN // jlSurfaceCreate -- the stage never hit it (pinned $01:2000 path), -// which is why UBER could pass while spacetaxi died in -// loadFontSheet. Other ports map jlpBigAlloc to malloc, so behavior +// which is why UBER could pass while Space Taxi (a separate project) died +// in its loadFontSheet. Other ports map jlpBigAlloc to malloc, so behavior // is unchanged there (finding #31 pattern). uint8_t *jlpGenericSurfaceAllocPixels(void) { uint8_t *pixels; diff --git a/tests/agi/disasmAgi.c b/tests/agi/disasmAgi.c deleted file mode 100644 index 2963877..0000000 --- a/tests/agi/disasmAgi.c +++ /dev/null @@ -1,413 +0,0 @@ -// AGI v2 LOGIC disassembler. -// -// Walks one LOGIC resource and prints the bytecode in a human-readable -// form, with names for action opcodes, test commands, common variables -// and flags. Intended as a debugging aid while filling out the VM: -// dump KQ3's logic.0 + logic.45 to identify which stub opcodes are -// gating title-screen progression. -// -// Usage: -// disasmAgi <game-directory> <logic-id> [end-pc] -// -// `end-pc` caps the walk at a given offset; useful for spelunking -// through huge logic.0 without dumping the whole thing. - -#include "agi.h" - -#include <stdio.h> -#include <stdlib.h> -#include <string.h> - - -#define AGI_LOGIC_HDR_BYTES 2u -#define ACTION_UNKNOWN 0xFFu -#define MAX_TEST 0x12u - - -// ----- Prototypes ----- - -static const char *actionName(uint8_t op); -static uint16_t disasmAction(const uint8_t *code, uint16_t pc, uint16_t length, uint8_t op); -static uint16_t disasmIf(const uint8_t *code, uint16_t pc, uint16_t length); -static uint16_t disasmTest(const uint8_t *code, uint16_t pc, uint16_t length); -static const char *testName(uint8_t op); -static int walk(const uint8_t *code, uint16_t length, uint16_t endPc); - - -// AGI v2 action-opcode names. Indexed by opcode id; NULL means "no -// such opcode" (matches the VM's ACTION_UNKNOWN sentinel). -static const char *kActionNames[256] = { - /* 0x00 */ "return", "increment", "decrement", "assignn", - /* 0x04 */ "assignv", "addn", "addv", "subn", - /* 0x08 */ "subv", "lindirectv", "rindirect", "lindirectn", - /* 0x0C */ "set", "reset", "toggle", "setv", - /* 0x10 */ "resetv", "togglev", "new.room", "new.room.v", - /* 0x14 */ "load.logic", "load.logic.v", "call", "call.v", - /* 0x18 */ "load.pic", "draw.pic", "show.pic", "discard.pic", - /* 0x1C */ "overlay.pic", "show.pri.screen", "load.view", "load.view.v", - /* 0x20 */ "discard.view", "animate.obj", "unanimate.all", "draw", - /* 0x24 */ "erase", "position", "position.v", "get.posn", - /* 0x28 */ "reposition", "set.view", "set.view.v", "set.loop", - /* 0x2C */ "set.loop.v", "fix.loop", "release.loop", "set.cel", - /* 0x30 */ "set.cel.v", "last.cel", "current.cel", "current.loop", - /* 0x34 */ "current.view", "number.of.loops", "set.priority", "set.priority.v", - /* 0x38 */ "release.priority", "get.priority", "stop.update", "start.update", - /* 0x3C */ "force.update", "ignore.horizon", "observe.horizon", "set.horizon", - /* 0x40 */ "object.on.water", "object.on.land", "object.on.anything", "ignore.objs", - /* 0x44 */ "observe.objs", "distance", "stop.cycling", "start.cycling", - /* 0x48 */ "normal.cycle", "end.of.loop", "reverse.cycle", "reverse.loop", - /* 0x4C */ "cycle.time", "stop.motion", "start.motion", "step.size", - /* 0x50 */ "step.time", "move.obj", "move.obj.v", "follow.ego", - /* 0x54 */ "wander", "normal.motion", "set.dir", "get.dir", - /* 0x58 */ "ignore.blocks", "observe.blocks", "block", "unblock", - /* 0x5C */ "get", "get.v", "drop", "put", - /* 0x60 */ "put.v", "get.room.v", "load.sound", "sound", - /* 0x64 */ "stop.sound", "print", "print.v", "display", - /* 0x68 */ "display.v", "clear.lines", "text.screen", "graphics", - /* 0x6C */ "set.cursor.char", "set.text.attribute", "shake.screen", "configure.screen", - /* 0x70 */ "status.line.on", "status.line.off", "set.string", "get.string", - /* 0x74 */ "word.to.string", "parse", "get.num", "prevent.input", - /* 0x78 */ "accept.input", "set.key", "add.to.pic", "add.to.pic.v", - /* 0x7C */ "status", "save.game", "restore.game", "init.disk", - /* 0x80 */ "restart.game", "show.obj", "random", "program.control", - /* 0x84 */ "player.control", "obj.status.v", "quit", "show.mem", - /* 0x88 */ "pause", "echo.line", "cancel.line", "init.joy", - /* 0x8C */ "toggle.monitor", "version", "script.size", "set.game.id", - /* 0x90 */ "log", "set.scan.start", "reset.scan.start", "reposition.to", - /* 0x94 */ "reposition.to.v", "trace.on", "trace.info", "print.at", - /* 0x98 */ "print.at.v", "discard.view.v", "clear.text.rect", "set.upper.left", - /* 0x9C */ "set.menu", "set.menu.item", "submit.menu", "enable.item", - /* 0xA0 */ "disable.item", "menu.input", "show.obj.v", "open.dialogue", - /* 0xA4 */ "close.dialogue", "mul.n", "mul.v", "div.n", - /* 0xA8 */ "div.v", "close.window", "set.simple", "push.script", - /* 0xAC */ "pop.script", "hold.key", "set.pri.base", "discard.sound", - /* 0xB0 */ "hide.mouse", "allow.menu", "show.mouse", "fence.mouse", - /* 0xB4 */ "mouse.posn", "release.key", "adj.ego.move.to.x.y", - /* 0xB7..0xFD */ NULL - // 0xFE = goto, 0xFF = if -- handled separately -}; - - -// AGI v2 action arg byte counts (matches kActionArgBytes in agiVm.c). -static const uint8_t kActionArgBytes[256] = { - /* 0x00 */ 0u, 1u, 1u, 2u, 2u, 2u, 2u, 2u, 2u, 2u, 2u, 2u, 1u, 1u, 1u, 1u, - /* 0x10 */ 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 0u, 1u, 1u, 0u, 1u, 1u, - /* 0x20 */ 1u, 1u, 0u, 1u, 1u, 3u, 3u, 3u, 3u, 2u, 2u, 2u, 2u, 1u, 1u, 2u, - /* 0x30 */ 2u, 2u, 2u, 2u, 2u, 2u, 2u, 2u, 1u, 2u, 1u, 1u, 1u, 1u, 1u, 1u, - /* 0x40 */ 1u, 1u, 1u, 1u, 1u, 3u, 1u, 1u, 1u, 2u, 1u, 2u, 2u, 1u, 1u, 2u, - /* 0x50 */ 2u, 5u, 5u, 3u, 1u, 1u, 2u, 2u, 1u, 1u, 4u, 0u, 1u, 1u, 1u, 2u, - /* 0x60 */ 2u, 2u, 1u, 2u, 0u, 1u, 1u, 3u, 3u, 3u, 0u, 0u, 1u, 2u, 1u, 3u, - /* 0x70 */ 0u, 0u, 2u, 5u, 2u, 1u, 2u, 0u, 0u, 3u, 7u, 7u, 0u, 0u, 0u, 0u, - /* 0x80 */ 0u, 1u, 3u, 0u, 0u, 1u, 1u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 1u, 1u, - /* 0x90 */ 1u, 0u, 0u, 3u, 3u, 0u, 3u, 4u, 4u, 1u, 4u, 2u, 1u, 2u, 0u, 1u, - /* 0xA0 */ 1u, 0u, 1u, 0u, 0u, 2u, 2u, 2u, 2u, 0u, 1u, 0u, 0u, 0u, 1u, 1u, - /* 0xB0 */ 0u, 1u, 0u, 4u, 2u, 0u, 0u, - ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, - /* 0xBA */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, - /* 0xC0 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, - /* 0xC8 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, - /* 0xD0 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, - /* 0xD8 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, - /* 0xE0 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, - /* 0xE8 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, - /* 0xF0 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, - /* 0xF8 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN -}; - - -static const char *kTestNames[MAX_TEST + 1u] = { - /* 0x00 */ NULL, - /* 0x01 */ "equaln", - /* 0x02 */ "equalv", - /* 0x03 */ "lessn", - /* 0x04 */ "lessv", - /* 0x05 */ "greatern", - /* 0x06 */ "greaterv", - /* 0x07 */ "isset", - /* 0x08 */ "issetv", - /* 0x09 */ "has", - /* 0x0A */ "obj.in.room", - /* 0x0B */ "posn", - /* 0x0C */ "controller", - /* 0x0D */ "have.key", - /* 0x0E */ "said", - /* 0x0F */ "compare.strings", - /* 0x10 */ "obj.in.box", - /* 0x11 */ "center.posn", - /* 0x12 */ "right.posn" -}; - - -static const uint8_t kTestArgBytes[MAX_TEST + 1u] = { - 0u, 2u, 2u, 2u, 2u, 2u, 2u, 1u, 1u, 1u, 2u, 5u, 1u, 0u, 0u /*said*/, 2u, 5u, 5u, 5u -}; - - -// ----- Helpers (alphabetical) ----- - -static const char *actionName(uint8_t op) { - if (op == 0xFFu) { - return "if"; - } - if (op == 0xFEu) { - return "goto"; - } - if (kActionNames[op] != NULL) { - return kActionNames[op]; - } - return "???"; -} - - -static uint16_t disasmAction(const uint8_t *code, uint16_t pc, uint16_t length, uint8_t op) { - uint8_t args; - uint8_t i; - uint16_t here; - - here = (uint16_t)(pc - 1u); - args = kActionArgBytes[op]; - if (args == ACTION_UNKNOWN) { - printf(" %04X: %02X ???\n", (unsigned)here, (unsigned)op); - return length; - } - if ((uint32_t)pc + (uint32_t)args > (uint32_t)length) { - printf(" %04X: %02X truncated args\n", (unsigned)here, (unsigned)op); - return length; - } - printf(" %04X: %s", (unsigned)here, actionName(op)); - if (args == 0u) { - printf("()\n"); - } else { - printf("("); - for (i = 0u; i < args; i++) { - if (i > 0u) { - printf(", "); - } - printf("%u", (unsigned)code[pc + i]); - } - printf(")\n"); - } - return (uint16_t)(pc + args); -} - - -static uint16_t disasmIf(const uint8_t *code, uint16_t pc, uint16_t length) { - uint16_t here; - uint8_t testOp; - bool first; - bool notNext; - bool inOr; - uint16_t skip; - uint16_t target; - - here = (uint16_t)(pc - 1u); - printf(" %04X: if (", (unsigned)here); - first = true; - notNext = false; - inOr = false; - for (;;) { - if (pc >= length) { - printf(" <truncated>)\n"); - return length; - } - testOp = code[pc]; - pc = (uint16_t)(pc + 1u); - if (testOp == 0xFFu) { - // End of test list. - break; - } - if (testOp == 0xFCu) { - if (inOr) { - printf(")"); - inOr = false; - } else { - if (!first) { - printf(" && "); - } - printf("("); - inOr = true; - first = true; - } - continue; - } - if (testOp == 0xFDu) { - notNext = true; - continue; - } - if (!first) { - printf(inOr ? " || " : " && "); - } - first = false; - if (notNext) { - printf("!"); - notNext = false; - } - pc = disasmTest(code, pc, length); - } - if (inOr) { - printf(")"); - } - if ((uint32_t)pc + 2u > (uint32_t)length) { - printf(") <truncated skip>\n"); - return length; - } - skip = (uint16_t)(code[pc] | ((uint16_t)code[pc + 1u] << 8)); - target = (uint16_t)(pc + 2u + skip); - printf(") -> %04X\n", (unsigned)target); - return (uint16_t)(pc + 2u); -} - - -static uint16_t disasmTest(const uint8_t *code, uint16_t pc, uint16_t length) { - uint8_t args; - uint8_t i; - uint8_t testOp; - uint8_t wordCount; - uint16_t word; - - testOp = code[pc - 1u]; - if (testOp == 0u || testOp > MAX_TEST) { - printf("???_test_0x%02X(", (unsigned)testOp); - return pc; - } - if (testOp == 0x0Eu) { - // said(word, word, ...) - if (pc >= length) { - printf("said(<truncated>"); - return length; - } - wordCount = code[pc]; - pc = (uint16_t)(pc + 1u); - printf("said("); - for (i = 0u; i < wordCount; i++) { - if (i > 0u) { - printf(", "); - } - if ((uint32_t)pc + 2u > (uint32_t)length) { - printf("<truncated>"); - return length; - } - word = (uint16_t)(code[pc] | ((uint16_t)code[pc + 1u] << 8)); - printf("w%u", (unsigned)word); - pc = (uint16_t)(pc + 2u); - } - printf(")"); - return pc; - } - args = kTestArgBytes[testOp]; - printf("%s(", testName(testOp)); - if ((uint32_t)pc + (uint32_t)args > (uint32_t)length) { - printf("<truncated>)"); - return length; - } - for (i = 0u; i < args; i++) { - if (i > 0u) { - printf(", "); - } - printf("%u", (unsigned)code[pc + i]); - } - printf(")"); - return (uint16_t)(pc + args); -} - - -static const char *testName(uint8_t op) { - if (op > MAX_TEST || kTestNames[op] == NULL) { - return "???"; - } - return kTestNames[op]; -} - - -static int walk(const uint8_t *code, uint16_t length, uint16_t endPc) { - uint16_t pc; - uint8_t op; - int16_t gotoOff; - uint16_t target; - - pc = 0u; - while (pc < length && pc < endPc) { - op = code[pc]; - pc = (uint16_t)(pc + 1u); - if (op == 0xFFu) { - pc = disasmIf(code, pc, length); - continue; - } - if (op == 0xFEu) { - if ((uint32_t)pc + 2u > (uint32_t)length) { - printf(" %04X: goto <truncated>\n", (unsigned)(pc - 1u)); - return 1; - } - gotoOff = (int16_t)(code[pc] | ((uint16_t)code[pc + 1u] << 8)); - target = (uint16_t)((int32_t)pc + 2 + (int32_t)gotoOff); - printf(" %04X: goto %04X\n", (unsigned)(pc - 1u), (unsigned)target); - pc = (uint16_t)(pc + 2u); - continue; - } - pc = disasmAction(code, pc, length, op); - if (op == 0x00u) { - // return -- end of logic. Keep walking past it; some - // logics have unreachable handlers below the top-level - // return that callers jump into via goto. - } - } - return 0; -} - - -// ----- main ----- - -int main(int argc, char **argv) { - AgiGameT game; - uint16_t logicId; - uint8_t *bytes; - uint16_t length; - uint16_t bytecodeLen; - uint16_t endPc; - int ret; - - if (argc < 3 || argc > 4) { - fprintf(stderr, "usage: %s <game-directory> <logic-id> [end-pc]\n", argv[0]); - return 2; - } - if (!agiResOpen(&game)) { - fprintf(stderr, "FAIL: agiResOpen rejected '%s'\n", argv[1]); - return 1; - } - logicId = (uint16_t)atoi(argv[2]); - if (logicId >= game.resCount[AGI_RES_LOGIC]) { - fprintf(stderr, "FAIL: no LOGIC at id %u\n", (unsigned)logicId); - agiResClose(&game); - return 1; - } - bytes = agiResLoad(&game, AGI_RES_LOGIC, logicId, &length); - if (bytes == NULL) { - fprintf(stderr, "FAIL: LOGIC %u failed to load\n", (unsigned)logicId); - agiResClose(&game); - return 1; - } - if (length < AGI_LOGIC_HDR_BYTES) { - fprintf(stderr, "FAIL: LOGIC %u truncated header\n", (unsigned)logicId); - free(bytes); - agiResClose(&game); - return 1; - } - bytecodeLen = (uint16_t)(bytes[0] | ((uint16_t)bytes[1] << 8)); - if ((uint32_t)bytecodeLen + AGI_LOGIC_HDR_BYTES > (uint32_t)length) { - fprintf(stderr, "FAIL: LOGIC %u bytecode length exceeds resource\n", (unsigned)logicId); - free(bytes); - agiResClose(&game); - return 1; - } - endPc = (argc == 4) ? (uint16_t)atoi(argv[3]) : bytecodeLen; - if (endPc > bytecodeLen) { - endPc = bytecodeLen; - } - - printf("LOGIC %u: %u bytes resource, %u bytes bytecode\n", - (unsigned)logicId, (unsigned)length, (unsigned)bytecodeLen); - ret = walk(bytes + AGI_LOGIC_HDR_BYTES, bytecodeLen, endPc); - free(bytes); - agiResClose(&game); - return ret; -} diff --git a/tests/agi/testAgiPic.c b/tests/agi/testAgiPic.c deleted file mode 100644 index c18eb52..0000000 --- a/tests/agi/testAgiPic.c +++ /dev/null @@ -1,191 +0,0 @@ -// Host-side test for examples/agi/agiPic.c. -// -// Loads a single PIC resource from a game directory, decodes it, and -// writes two image files: a color PPM for the visual plane (palette -// expanded to 8-bit RGB) and a grayscale PGM for the priority plane. -// Also reports the byte count and pixel coverage so a regression in -// the decoder shows up as a numeric diff. -// -// Usage: -// testAgiPic <game-directory> [pic-id] -// -// The default pic-id is the first non-empty entry in PICDIR; KQ3 has -// id 0 unused so the loader hops forward until it finds one. -// -// Exit codes: -// 0 decoded; outputs written -// 1 game directory missing, PIC slot empty, or decode failed -// 2 invalid command line - -#include "agi.h" - -#include <stdio.h> -#include <stdlib.h> -#include <string.h> - - -// 4-bit channel -> 8-bit channel scaling: x * 17 maps 0..15 to 0..255 -// without bias (15 * 17 = 255 exactly). -#define CHANNEL_SCALE 17 - - -// ----- Prototypes ----- - -static int16_t firstNonEmptyPic(const AgiGameT *game); -static bool writePgm(const char *path, const uint8_t *plane); -static bool writePpm(const char *path, const uint8_t *plane); - - -// ----- Helpers (alphabetical) ----- - -static int16_t firstNonEmptyPic(const AgiGameT *game) { - uint16_t count; - uint16_t i; - - count = game->resCount[AGI_RES_PIC]; - for (i = 0u; i < count; i++) { - if (game->resDir[AGI_RES_PIC][i].volume < AGI_MAX_VOLUMES) { - return (int16_t)i; - } - } - return -1; -} - - -static bool writePgm(const char *path, const uint8_t *plane) { - FILE *fp; - uint32_t i; - uint8_t gray; - - fp = fopen(path, "wb"); - if (fp == NULL) { - return false; - } - fprintf(fp, "P5\n%d %d\n255\n", AGI_PIC_WIDTH, AGI_PIC_HEIGHT); - for (i = 0u; i < (uint32_t)AGI_PIC_PIXELS; i++) { - gray = (uint8_t)((plane[i] & 0x0Fu) * CHANNEL_SCALE); - fputc(gray, fp); - } - fclose(fp); - return true; -} - - -static bool writePpm(const char *path, const uint8_t *plane) { - FILE *fp; - uint32_t i; - uint16_t rgb; - uint8_t r; - uint8_t g; - uint8_t b; - - fp = fopen(path, "wb"); - if (fp == NULL) { - return false; - } - fprintf(fp, "P6\n%d %d\n255\n", AGI_PIC_WIDTH, AGI_PIC_HEIGHT); - for (i = 0u; i < (uint32_t)AGI_PIC_PIXELS; i++) { - rgb = kAgiPalette[plane[i] & 0x0Fu]; - r = (uint8_t)(((rgb >> 8) & 0x0Fu) * CHANNEL_SCALE); - g = (uint8_t)(((rgb >> 4) & 0x0Fu) * CHANNEL_SCALE); - b = (uint8_t)((rgb & 0x0Fu) * CHANNEL_SCALE); - fputc(r, fp); - fputc(g, fp); - fputc(b, fp); - } - fclose(fp); - return true; -} - - -// ----- main ----- - -int main(int argc, char **argv) { - AgiGameT game; - AgiPicT pic; - int16_t picId; - uint8_t *bytes; - uint16_t length; - uint32_t nonBgVisual; - uint32_t nonBgPriority; - uint32_t i; - char visPath[256]; - char priPath[256]; - - if (argc < 2 || argc > 3) { - fprintf(stderr, "usage: %s <game-directory> [pic-id]\n", argv[0]); - return 2; - } - - if (!agiResOpen(&game)) { - fprintf(stderr, "FAIL: agiResOpen rejected '%s'\n", argv[1]); - return 1; - } - - if (argc == 3) { - picId = (int16_t)atoi(argv[2]); - } else { - picId = firstNonEmptyPic(&game); - } - if (picId < 0 || (uint16_t)picId >= game.resCount[AGI_RES_PIC]) { - fprintf(stderr, "FAIL: no PIC at id %d\n", (int)picId); - agiResClose(&game); - return 1; - } - - bytes = agiResLoad(&game, AGI_RES_PIC, (uint16_t)picId, &length); - if (bytes == NULL) { - fprintf(stderr, "FAIL: PIC %d failed to load\n", (int)picId); - agiResClose(&game); - return 1; - } - - if (!agiPicAlloc(&pic)) { - fprintf(stderr, "FAIL: agiPicAlloc out of memory\n"); - free(bytes); - agiResClose(&game); - return 1; - } - agiPicClear(&pic); - - if (!agiPicDecode(&pic, bytes, length)) { - fprintf(stderr, "FAIL: PIC %d decode rejected (malformed stream?)\n", (int)picId); - agiPicFree(&pic); - free(bytes); - agiResClose(&game); - return 1; - } - - nonBgVisual = 0u; - nonBgPriority = 0u; - for (i = 0u; i < (uint32_t)AGI_PIC_PIXELS; i++) { - if (pic.visual[i] != AGI_VISUAL_BG) { - nonBgVisual++; - } - if (pic.priority[i] != AGI_PRIORITY_BG) { - nonBgPriority++; - } - } - - snprintf(visPath, sizeof(visPath), "build/tests/pic%d_visual.ppm", (int)picId); - snprintf(priPath, sizeof(priPath), "build/tests/pic%d_priority.pgm", (int)picId); - if (!writePpm(visPath, pic.visual) || !writePgm(priPath, pic.priority)) { - fprintf(stderr, "FAIL: could not write output images\n"); - agiPicFree(&pic); - free(bytes); - agiResClose(&game); - return 1; - } - - printf("PIC %d: %u bytes, visual %u/%u px painted, priority %u/%u px painted\n", - (int)picId, (unsigned)length, - (unsigned)nonBgVisual, (unsigned)AGI_PIC_PIXELS, - (unsigned)nonBgPriority, (unsigned)AGI_PIC_PIXELS); - printf(" wrote %s\n", visPath); - printf(" wrote %s\n", priPath); - - agiPicFree(&pic); - free(bytes); - agiResClose(&game); - return 0; -} diff --git a/tests/agi/testAgiPipeline.c b/tests/agi/testAgiPipeline.c deleted file mode 100644 index b329dda..0000000 --- a/tests/agi/testAgiPipeline.c +++ /dev/null @@ -1,284 +0,0 @@ -// Host-side AGI pipeline test (Phase 1 sub-D). -// -// Wires up a real logic-resource cache + trace callbacks and runs -// the VM in a simulated game-cycle loop: -// * agiVmRun() -// * NEW_ROOM -> update var[0]/var[1], reset flags, restart logic.0 -// * RETURN -> count one cycle, restart logic.0 -// * UNKNOWN_OP -> print and exit -// Capped at MAX_CYCLES so a logic that always returns without -// painting can't loop forever. -// -// What this lets us verify before touching any emulator: -// - logic.0's prologue + IF branching against fresh game state -// - room transition handling (new.room(N) loads logic N) -// - room logic execution (load.pic/draw.pic/show.pic callbacks) -// - call.logic / RETURN frame management -// -// Usage: -// testAgiPipeline <game-directory> [max-cycles] - -#include "agi.h" - -#include <stdio.h> -#include <stdlib.h> -#include <string.h> - - -#define MAX_CYCLES_DEFAULT 20u -#define AGI_LOGIC_HDR_BYTES 2u - - -typedef struct { - uint8_t *raw; - uint16_t rawLength; - const uint8_t *bytecode; - uint16_t bytecodeLength; -} CachedLogicT; - - -static AgiGameT gGame; -static CachedLogicT gLogicCache[AGI_MAX_RESOURCES]; -static uint32_t gTraceLoadPic; -static uint32_t gTraceDrawPic; -static uint32_t gTraceShowPic; -static uint32_t gTraceDiscardPic; -static uint32_t gTraceOverlayPic; -static uint32_t gTraceFetchLogic; - - -static void cbDiscardPic(void *ctx, uint8_t picId); -static void cbDrawPic(void *ctx, uint8_t picId); -static const uint8_t *cbFetchLogic(void *ctx, uint8_t logicId, uint16_t *outLength); -static void cbLoadPic(void *ctx, uint8_t picId); -static void cbOverlayPic(void *ctx, uint8_t picId); -static void cbShowPic(void *ctx); -static const char *haltName(AgiVmHaltE r); -static void releaseCache(void); - - -static void cbDiscardPic(void *ctx, uint8_t picId) { - (void)ctx; - gTraceDiscardPic++; - printf(" [host] discardPic(%u)\n", (unsigned)picId); -} - - -static void cbDrawPic(void *ctx, uint8_t picId) { - (void)ctx; - gTraceDrawPic++; - printf(" [host] drawPic(%u)\n", (unsigned)picId); -} - - -static const uint8_t *cbFetchLogic(void *ctx, uint8_t logicId, uint16_t *outLength) { - CachedLogicT *slot; - uint8_t *raw; - uint16_t rawLength; - uint16_t bcLen; - - (void)ctx; - slot = &gLogicCache[logicId]; - if (slot->bytecode != NULL) { - *outLength = slot->bytecodeLength; - return slot->bytecode; - } - - if (logicId >= gGame.resCount[AGI_RES_LOGIC]) { - return NULL; - } - raw = agiResLoad(&gGame, AGI_RES_LOGIC, (uint16_t)logicId, &rawLength); - if (raw == NULL || rawLength < AGI_LOGIC_HDR_BYTES) { - if (raw != NULL) { - free(raw); - } - return NULL; - } - bcLen = (uint16_t)(raw[0] | ((uint16_t)raw[1] << 8)); - if ((uint16_t)(AGI_LOGIC_HDR_BYTES + bcLen) > rawLength) { - free(raw); - return NULL; - } - slot->raw = raw; - slot->rawLength = rawLength; - slot->bytecode = raw + AGI_LOGIC_HDR_BYTES; - slot->bytecodeLength = bcLen; - gTraceFetchLogic++; - printf(" [host] fetchLogic(%u) -> %u bytes (first load)\n", - (unsigned)logicId, (unsigned)bcLen); - - *outLength = bcLen; - return slot->bytecode; -} - - -static void cbLoadPic(void *ctx, uint8_t picId) { - (void)ctx; - gTraceLoadPic++; - printf(" [host] loadPic(%u)\n", (unsigned)picId); -} - - -static void cbOverlayPic(void *ctx, uint8_t picId) { - (void)ctx; - gTraceOverlayPic++; - printf(" [host] overlayPic(%u)\n", (unsigned)picId); -} - - -static void cbShowPic(void *ctx) { - (void)ctx; - gTraceShowPic++; - printf(" [host] showPic\n"); -} - - -static const char *haltName(AgiVmHaltE r) { - switch (r) { - case AGI_VM_HALT_NONE: return "NONE"; - case AGI_VM_HALT_RETURN: return "RETURN"; - case AGI_VM_HALT_UNKNOWN_OP: return "UNKNOWN_OP"; - case AGI_VM_HALT_TRUNCATED: return "TRUNCATED"; - case AGI_VM_HALT_NEW_ROOM: return "NEW_ROOM"; - case AGI_VM_HALT_NO_LOGIC: return "NO_LOGIC"; - case AGI_VM_HALT_STACK_OVER: return "STACK_OVER"; - default: return "?"; - } -} - - -static void releaseCache(void) { - uint16_t i; - - for (i = 0u; i < AGI_MAX_RESOURCES; i++) { - if (gLogicCache[i].raw != NULL) { - free(gLogicCache[i].raw); - gLogicCache[i].raw = NULL; - } - } -} - - -int main(int argc, char **argv) { - AgiVmT vm; - AgiVmCallbacksT cb; - uint16_t length; - const uint8_t *logic0; - uint32_t maxCycles; - uint32_t cycles; - AgiVmHaltE reason; - uint8_t prevRoom; - uint8_t i; - - if (argc < 2 || argc > 3) { - fprintf(stderr, "usage: %s <game-directory> [max-cycles]\n", argv[0]); - return 2; - } - maxCycles = (argc == 3) ? (uint32_t)atoi(argv[2]) : (uint32_t)MAX_CYCLES_DEFAULT; - if (maxCycles == 0u) { - maxCycles = (uint32_t)MAX_CYCLES_DEFAULT; - } - - if (!agiResOpen(&gGame)) { - fprintf(stderr, "FAIL: agiResOpen rejected '%s'\n", argv[1]); - return 1; - } - memset(gLogicCache, 0, sizeof(gLogicCache)); - - cb.fetchLogic = cbFetchLogic; - cb.loadPic = cbLoadPic; - cb.drawPic = cbDrawPic; - cb.showPic = cbShowPic; - cb.discardPic = cbDiscardPic; - cb.overlayPic = cbOverlayPic; - cb.haveKey = NULL; - cb.fetchMessage = NULL; - cb.addToPic = NULL; - cb.soundDuration = NULL; - cb.playSound = NULL; - cb.stopSound = NULL; - cb.ctx = NULL; - - agiVmInit(&vm); - agiVmSetCallbacks(&vm, &cb); - - length = 0u; - logic0 = cbFetchLogic(NULL, 0u, &length); - if (logic0 == NULL) { - fprintf(stderr, "FAIL: could not load logic.0\n"); - releaseCache(); - agiResClose(&gGame); - return 1; - } - agiVmResetToLogic(&vm, logic0, length, 0u); - - cycles = 0u; - for (;;) { - if (cycles >= maxCycles) { - printf(" (capped at %u cycles)\n", (unsigned)maxCycles); - break; - } - cycles++; - // Simulate one wall-clock second per cycle so per-second - // game logic (countdowns, state machines, animation cadence) - // advances under the host test without waiting in real time. - // KQ3's title state machine in particular is gated entirely - // on var 11 changing; without this tick logic.45's countdowns - // never fire and the title appears to stall at 20+ cycles. - agiVmTickSeconds(&vm, 1u); - printf(" cycle %u: pc=%u/%u logic=%u callDepth=%u\n", - (unsigned)cycles, (unsigned)vm.pc, (unsigned)vm.codeLength, - (unsigned)vm.currentLogicId, (unsigned)vm.callDepth); - - reason = agiVmRun(&vm); - - printf(" halt=%s at pc=%u logic=%u", - haltName(reason), (unsigned)vm.pc, (unsigned)vm.currentLogicId); - if (reason == AGI_VM_HALT_UNKNOWN_OP) { - printf(" opcode=0x%02X", (unsigned)vm.lastUnknownOp); - } - if (reason == AGI_VM_HALT_NEW_ROOM) { - printf(" newRoom=%u", (unsigned)vm.newRoomId); - } - printf("\n"); - - if (reason == AGI_VM_HALT_NEW_ROOM) { - prevRoom = vm.vars[0]; - vm.vars[0] = vm.newRoomId; - vm.vars[1] = prevRoom; - // Reset most flags, then set flag 5 ("new room") to - // signal the room logic that its init branch should - // fire. In the real interpreter the engine also resets - // a handful of cycle vars (input, signal, etc.) but for - // the sub-D pipeline test the new-room flag is the only - // one room logic actually checks. - for (i = 0u; i < 32u; i++) { - vm.flags[i] = 0u; - } - vm.flags[5] = 1u; - agiVmResetToLogic(&vm, logic0, length, 0u); - continue; - } - if (reason == AGI_VM_HALT_RETURN) { - // End-of-cycle housekeeping the real interpreter would - // perform: clear flag 5 (new.room) so subsequent cycles - // don't re-run the room-init branch. - vm.flags[5] = 0u; - agiVmResetToLogic(&vm, logic0, length, 0u); - continue; - } - break; - } - - printf("===== trace =====\n"); - printf(" fetchLogic: %u (first-loads only)\n", (unsigned)gTraceFetchLogic); - printf(" loadPic: %u\n", (unsigned)gTraceLoadPic); - printf(" drawPic: %u\n", (unsigned)gTraceDrawPic); - printf(" showPic: %u\n", (unsigned)gTraceShowPic); - printf(" discardPic: %u\n", (unsigned)gTraceDiscardPic); - printf(" overlayPic: %u\n", (unsigned)gTraceOverlayPic); - - releaseCache(); - agiResClose(&gGame); - return 0; -} diff --git a/tests/agi/testAgiRes.c b/tests/agi/testAgiRes.c deleted file mode 100644 index b56cc58..0000000 --- a/tests/agi/testAgiRes.c +++ /dev/null @@ -1,101 +0,0 @@ -// Host-side regression test for examples/agi/agiRes.c. -// -// The AGI resource loader is plain stdio C with no JoeyLib runtime -// dependencies, so we can compile and exercise it on the build host -// without touching any emulator. This keeps the Phase 0 iteration -// loop fast: edit agiRes.c, rerun scripts/test-agi.sh, get a pass -// or fail against KQ3 (or any AGI v2 directory) in under a second. -// -// Usage: testAgiRes <game-directory> -// -// Exit codes: -// 0 resource directories parsed and at least one resource per -// type loaded successfully (or the type was empty) -// 1 game directory missing or malformed -// 2 invalid command line - -#include "agi.h" - -#include <stdio.h> -#include <stdlib.h> -#include <string.h> - - -// ----- Prototypes ----- - -static const char *resTypeName(AgiResTypeE type); -static int testFirstResource(AgiGameT *game, AgiResTypeE type); - - -// ----- Helpers (alphabetical) ----- - -static const char *resTypeName(AgiResTypeE type) { - switch (type) { - case AGI_RES_LOGIC: return "LOGIC"; - case AGI_RES_PIC: return "PIC "; - case AGI_RES_VIEW: return "VIEW "; - case AGI_RES_SOUND: return "SOUND"; - default: return "?????"; - } -} - - -// Locate the first non-empty resource of `type`, load it, print its -// length. Returns 0 on success or empty type, 1 on load failure. -static int testFirstResource(AgiGameT *game, AgiResTypeE type) { - uint16_t i; - uint16_t length; - uint8_t *payload; - uint16_t count; - - count = game->resCount[type]; - for (i = 0; i < count; i++) { - if (game->resDir[type][i].volume >= AGI_MAX_VOLUMES) { - continue; - } - payload = agiResLoad(game, type, i, &length); - if (payload == NULL) { - fprintf(stderr, "FAIL: %s id %u failed to load\n", resTypeName(type), (unsigned)i); - return 1; - } - printf(" %s: %u resources, first non-empty id %u is %u bytes\n", - resTypeName(type), (unsigned)count, (unsigned)i, (unsigned)length); - free(payload); - return 0; - } - printf(" %s: %u directory slots, all empty\n", resTypeName(type), (unsigned)count); - return 0; -} - - -// ----- main ----- - -int main(int argc, char **argv) { - AgiGameT game; - uint8_t t; - int rc; - - if (argc != 2) { - fprintf(stderr, "usage: %s <game-directory>\n", argv[0]); - return 2; - } - - printf("Opening AGI game directory: %s\n", argv[1]); - if (!agiResOpen(&game)) { - fprintf(stderr, "FAIL: agiResOpen rejected '%s' (missing files? wrong format?)\n", argv[1]); - return 1; - } - - rc = 0; - for (t = 0; t < AGI_RES_COUNT; t++) { - if (testFirstResource(&game, (AgiResTypeE)t) != 0) { - rc = 1; - } - } - - agiResClose(&game); - if (rc == 0) { - printf("OK\n"); - } - return rc; -} diff --git a/tests/agi/testAgiView.c b/tests/agi/testAgiView.c deleted file mode 100644 index 9c332e0..0000000 --- a/tests/agi/testAgiView.c +++ /dev/null @@ -1,198 +0,0 @@ -// Host-side test for examples/agi/agiView.c. -// -// Loads a single VIEW resource, parses it, dumps the loop/cel -// structure, and writes cel 0 of every loop as a PPM image. The -// transparent color is rendered as bright magenta so the cel's -// silhouette stands out from the actual painted pixels. -// -// Usage: -// testAgiView <game-directory> [view-id] -// -// Exit codes: -// 0 parsed; outputs written -// 1 game directory missing, view slot empty, or parse failed -// 2 invalid command line - -#include "agi.h" - -#include <stdio.h> -#include <stdlib.h> -#include <string.h> - - -#define CHANNEL_SCALE 17u - -// Pseudo-color used for "transparent" pixels in the output image. -#define TRANS_R 0xFFu -#define TRANS_G 0x00u -#define TRANS_B 0xFFu - - -// ----- Prototypes ----- - -static int16_t firstNonEmptyView(const AgiGameT *game); -static bool writeCelPpm(const char *path, const AgiCelInfoT *cel); - - -// ----- Helpers (alphabetical) ----- - -static int16_t firstNonEmptyView(const AgiGameT *game) { - uint16_t count; - uint16_t i; - - count = game->resCount[AGI_RES_VIEW]; - for (i = 0u; i < count; i++) { - if (game->resDir[AGI_RES_VIEW][i].volume < AGI_MAX_VOLUMES) { - return (int16_t)i; - } - } - return -1; -} - - -// Walk the cel's RLE data, paint into a chunky 8bpp buffer, then -// emit a PPM with transparent pixels mapped to magenta. Same RLE -// rules as agiViewDraw: each byte = (color << 4) | runLen, runLen == 0 -// ends the row. -static bool writeCelPpm(const char *path, const AgiCelInfoT *cel) { - FILE *fp; - uint8_t *buf; - uint16_t w; - uint16_t h; - uint16_t cx; - uint16_t cy; - uint16_t i; - const uint8_t *rle; - uint8_t rleByte; - uint8_t color; - uint8_t runLen; - uint8_t r; - uint8_t transparent; - uint16_t rgb; - - w = (uint16_t)cel->width; - h = (uint16_t)cel->height; - if (w == 0u || h == 0u) { - return false; - } - - buf = (uint8_t *)malloc((size_t)w * (size_t)h); - if (buf == NULL) { - return false; - } - transparent = cel->transparentColor; - memset(buf, transparent, (size_t)w * (size_t)h); - - rle = cel->rleData; - for (cy = 0u; cy < h; cy++) { - cx = 0u; - for (;;) { - rleByte = *rle++; - if (rleByte == 0u) { - break; - } - color = (uint8_t)(rleByte >> 4); - runLen = (uint8_t)(rleByte & 0x0Fu); - for (r = 0u; r < runLen; r++) { - if (cx >= w) { - break; - } - buf[cy * w + cx] = color; - cx++; - } - } - } - - fp = fopen(path, "wb"); - if (fp == NULL) { - free(buf); - return false; - } - fprintf(fp, "P6\n%u %u\n255\n", (unsigned)w, (unsigned)h); - for (i = 0u; i < (uint16_t)(w * h); i++) { - if (buf[i] == transparent) { - fputc(TRANS_R, fp); - fputc(TRANS_G, fp); - fputc(TRANS_B, fp); - } else { - rgb = kAgiPalette[buf[i] & 0x0Fu]; - fputc((int)(((rgb >> 8) & 0x0Fu) * CHANNEL_SCALE), fp); - fputc((int)(((rgb >> 4) & 0x0Fu) * CHANNEL_SCALE), fp); - fputc((int)((rgb & 0x0Fu) * CHANNEL_SCALE), fp); - } - } - fclose(fp); - free(buf); - return true; -} - - -// ----- main ----- - -int main(int argc, char **argv) { - AgiGameT game; - AgiViewT view; - int16_t viewId; - uint8_t *bytes; - uint16_t length; - uint8_t li; - uint8_t ci; - const AgiCelInfoT *cel; - char path[256]; - - if (argc < 2 || argc > 3) { - fprintf(stderr, "usage: %s <game-directory> [view-id]\n", argv[0]); - return 2; - } - - if (!agiResOpen(&game)) { - fprintf(stderr, "FAIL: agiResOpen rejected '%s'\n", argv[1]); - return 1; - } - - if (argc == 3) { - viewId = (int16_t)atoi(argv[2]); - } else { - viewId = firstNonEmptyView(&game); - } - if (viewId < 0 || (uint16_t)viewId >= game.resCount[AGI_RES_VIEW]) { - fprintf(stderr, "FAIL: no VIEW at id %d\n", (int)viewId); - agiResClose(&game); - return 1; - } - - bytes = agiResLoad(&game, AGI_RES_VIEW, (uint16_t)viewId, &length); - if (bytes == NULL) { - fprintf(stderr, "FAIL: VIEW %d failed to load\n", (int)viewId); - agiResClose(&game); - return 1; - } - - if (!agiViewParse(&view, bytes, length)) { - fprintf(stderr, "FAIL: VIEW %d parse rejected\n", (int)viewId); - agiResClose(&game); - return 1; - } - - printf("VIEW %d: %u bytes, %u loops\n", (int)viewId, (unsigned)length, (unsigned)view.loopCount); - for (li = 0u; li < view.loopCount; li++) { - printf(" loop %u: %u cels\n", (unsigned)li, (unsigned)view.loops[li].celCount); - for (ci = 0u; ci < view.loops[li].celCount; ci++) { - cel = &view.loops[li].cels[ci]; - printf(" cel %u: %ux%u trans=%u mirror=%u (src loop %u)\n", - (unsigned)ci, (unsigned)cel->width, (unsigned)cel->height, - (unsigned)cel->transparentColor, - (unsigned)cel->mirrored, (unsigned)cel->mirrorSourceLoop); - } - if (view.loops[li].celCount > 0u) { - snprintf(path, sizeof(path), "build/tests/view%d_loop%u_cel0.ppm", (int)viewId, (unsigned)li); - if (!writeCelPpm(path, &view.loops[li].cels[0])) { - fprintf(stderr, " warn: could not write %s\n", path); - } - } - } - - agiViewFree(&view); - agiResClose(&game); - return 0; -} diff --git a/tests/agi/testAgiVm.c b/tests/agi/testAgiVm.c deleted file mode 100644 index b0948f2..0000000 --- a/tests/agi/testAgiVm.c +++ /dev/null @@ -1,135 +0,0 @@ -// Host-side test for examples/agi/agiVm.c (Phase 1 sub-A). -// -// Loads a LOGIC resource (default 0), runs the VM until it halts, -// and prints the halt reason plus any non-zero vars/flags. Since -// only the arithmetic and flag opcodes (0x00-0x11) are implemented -// this pass, expectation is one of: -// - HALT_RETURN at offset N (logic was entirely sub-A opcodes) -// - HALT_UNKNOWN_OP 0xXX at offset N (hit IF / new.room / etc.) -// - HALT_TRUNCATED at offset N (malformed resource) -// -// Usage: -// testAgiVm <game-directory> [logic-id] - -#include "agi.h" - -#include <stdio.h> -#include <stdlib.h> - - -// ----- Prototypes ----- - -static const char *haltName(AgiVmHaltE r); -static void printNonZeroState(const AgiVmT *vm); - - -// ----- Helpers (alphabetical) ----- - -static const char *haltName(AgiVmHaltE r) { - switch (r) { - case AGI_VM_HALT_NONE: return "NONE"; - case AGI_VM_HALT_RETURN: return "RETURN"; - case AGI_VM_HALT_UNKNOWN_OP: return "UNKNOWN_OP"; - case AGI_VM_HALT_TRUNCATED: return "TRUNCATED"; - case AGI_VM_HALT_NEW_ROOM: return "NEW_ROOM"; - default: return "?"; - } -} - - -static void printNonZeroState(const AgiVmT *vm) { - uint16_t i; - uint16_t nonZeroVars; - uint16_t nonZeroFlags; - - nonZeroVars = 0u; - nonZeroFlags = 0u; - for (i = 0u; i < AGI_VM_NUM_VARS; i++) { - if (vm->vars[i] != 0u) { - if (nonZeroVars == 0u) { - printf(" vars:"); - } - printf(" v%u=%u", (unsigned)i, (unsigned)vm->vars[i]); - nonZeroVars++; - } - } - if (nonZeroVars > 0u) { - printf("\n"); - } - for (i = 0u; i < AGI_VM_NUM_FLAGS; i++) { - if (vm->flags[i] != 0u) { - if (nonZeroFlags == 0u) { - printf(" flags:"); - } - printf(" f%u", (unsigned)i); - nonZeroFlags++; - } - } - if (nonZeroFlags > 0u) { - printf("\n"); - } -} - - -// ----- main ----- - -int main(int argc, char **argv) { - AgiGameT game; - AgiVmT vm; - uint16_t logicId; - uint8_t *bytes; - uint16_t length; - AgiVmHaltE reason; - - if (argc < 2 || argc > 3) { - fprintf(stderr, "usage: %s <game-directory> [logic-id]\n", argv[0]); - return 2; - } - - if (!agiResOpen(&game)) { - fprintf(stderr, "FAIL: agiResOpen rejected '%s'\n", argv[1]); - return 1; - } - - logicId = (argc == 3) ? (uint16_t)atoi(argv[2]) : 0u; - if (logicId >= game.resCount[AGI_RES_LOGIC]) { - fprintf(stderr, "FAIL: no LOGIC at id %u\n", (unsigned)logicId); - agiResClose(&game); - return 1; - } - - bytes = agiResLoad(&game, AGI_RES_LOGIC, logicId, &length); - if (bytes == NULL) { - fprintf(stderr, "FAIL: LOGIC %u failed to load\n", (unsigned)logicId); - agiResClose(&game); - return 1; - } - - agiVmInit(&vm); - if (!agiVmLoadLogic(&vm, bytes, length)) { - fprintf(stderr, "FAIL: agiVmLoadLogic rejected LOGIC %u (truncated header?)\n", (unsigned)logicId); - free(bytes); - agiResClose(&game); - return 1; - } - - printf("LOGIC %u: %u bytes resource, %u bytes bytecode\n", - (unsigned)logicId, (unsigned)length, (unsigned)vm.codeLength); - - reason = agiVmRun(&vm); - - printf(" halt=%s at pc=%u/%u", - haltName(reason), (unsigned)vm.pc, (unsigned)vm.codeLength); - if (reason == AGI_VM_HALT_UNKNOWN_OP) { - printf(" opcode=0x%02X", (unsigned)vm.lastUnknownOp); - } - if (reason == AGI_VM_HALT_NEW_ROOM) { - printf(" newRoom=%u", (unsigned)vm.newRoomId); - } - printf("\n"); - printNonZeroState(&vm); - - free(bytes); - agiResClose(&game); - return 0; -} diff --git a/tools/iigsStart/iigsStart.c b/tools/iigsStart/iigsStart.c index 2d8d8e9..c8d02f1 100644 --- a/tools/iigsStart/iigsStart.c +++ b/tools/iigsStart/iigsStart.c @@ -20,14 +20,14 @@ // Finder sets prefix 0 itself, which is why this only bites here. // QuitGS with pCount = 2 and a pathname -- "quit, and launch this". // -// Build: clang-build.sh -DJOEY_START_PATH='"/JOEYLIB/STAXI"' \ +// Build: clang-build.sh -DJOEY_START_PATH='"/JOEYLIB/DRAW"' \ // -o START iigsStart.c iigsStartQuit.s #include <stdint.h> // The program to launch. Overridden per-disk from the build. #ifndef JOEY_START_PATH -#define JOEY_START_PATH "/JOEYLIB/STAXI" +#define JOEY_START_PATH "/JOEYLIB/DRAW" #endif // The harness's marker, in the $E1:9DC8-$9DFD window that survives a @@ -95,7 +95,7 @@ int main(void) { len++; } // Directory part, including the trailing '/': everything up to the last - // separator. "/JOEYLIB/STAXI" -> "/JOEYLIB/". + // separator. "/JOEYLIB/DRAW" -> "/JOEYLIB/". for (i = 0; i < len; i++) { if (path[i] == '/') { dir = (uint16_t)(i + 1u); diff --git a/tools/staxibake/staxibake.c b/tools/staxibake/staxibake.c deleted file mode 100644 index d3a6621..0000000 --- a/tools/staxibake/staxibake.c +++ /dev/null @@ -1,124 +0,0 @@ -// staxibake -- generate the Space Taxi cel bank (.spr) for offline -// sprite pre-compilation. Runs on the host: it links the game's shared -// cel table (stCels.c) and the C64 sprite bitmaps (stC64Data.c) and -// writes the cross-platform JSP1 sprite bank in kStCels order. The -// per-target spritebake then compiles it to a .spc the game loads at -// boot with jlSpriteBankLoadPrecompiled, so no cel is JIT-compiled on -// the 65816 (which costs ~0.4 s each). -// -// Usage: staxibake OUTPUT.spr the cab/passenger/exhaust bank -// staxibake --level LEVEL.dat OUTPUT.spr one level's hook-sprite cels -// -// The per-level bank holds the cels stLevelCelList names for that level -// (its own VIC bitmaps in the colours its hook program uses), in list -// order -- the game rebuilds the same list at level entry to key them. - -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <stdint.h> - -#include "stCels.h" -#include "stC64Data.h" - -// JSP1 header (mirrors tools/spritebake + jlSpriteBankLoad): magic, -// target byte (0 = cross-platform chunky), has-palette, w/h tiles, -// cell count LE16, then a 32-byte palette block, all in 44 bytes. -#define SPR_HEADER_BYTES 44u -#define SPR_OFF_TARGET 4u -#define SPR_OFF_HAS_PAL 5u -#define SPR_OFF_WIDTH 6u -#define SPR_OFF_HEIGHT 7u -#define SPR_OFF_CELLS 8u - -static const uint8_t *levelBitmap(const StLevelT *level, uint8_t ptr); -static bool writeHeader(FILE *out, uint16_t count); - - -// A level's own sprite block, or NULL when the level does not ship it. -static const uint8_t *levelBitmap(const StLevelT *level, uint8_t ptr) { - uint8_t k; - - for (k = 0u; k < level->spriteCount; k++) { - if (level->sprites[k].ptr == ptr) { - return level->sprites[k].bitmap; - } - } - return NULL; -} - - -static bool writeHeader(FILE *out, uint16_t count) { - uint8_t header[SPR_HEADER_BYTES]; - - memset(header, 0, sizeof(header)); - memcpy(header, "JSP1", 4); - header[SPR_OFF_TARGET] = 0u; // cross-platform chunky - header[SPR_OFF_HAS_PAL] = 0u; // the game owns its palette - header[SPR_OFF_WIDTH] = (uint8_t)ST_CEL_TILES; - header[SPR_OFF_HEIGHT] = (uint8_t)ST_CEL_TILES; - header[SPR_OFF_CELLS] = (uint8_t)(count & 0xFFu); - header[SPR_OFF_CELLS + 1u] = (uint8_t)(count >> 8); - return fwrite(header, 1, SPR_HEADER_BYTES, out) == SPR_HEADER_BYTES; -} - - -int main(int argc, char **argv) { - static StLevelT level; - static StCelDefT levelCels[ST_LEVEL_CELS_MAX]; - const StCelDefT *cels = kStCels; - uint16_t count = kStCelCount; - const char *outPath; - FILE *out; - uint16_t i; - - if (argc == 4 && strcmp(argv[1], "--level") == 0) { - FILE *fp = fopen(argv[2], "rb"); - if (fp == NULL || !stLevelParse(&level, fp)) { - fprintf(stderr, "staxibake: cannot load level %s\n", argv[2]); - return 1; - } - fclose(fp); - cels = levelCels; - count = stLevelCelList(&level, levelCels, ST_LEVEL_CELS_MAX); - outPath = argv[3]; - } else if (argc == 2) { - outPath = argv[1]; - } else { - fprintf(stderr, "usage: staxibake OUTPUT.spr\n staxibake --level LEVEL.dat OUTPUT.spr\n"); - return 2; - } - out = fopen(outPath, "wb"); - if (out == NULL) { - fprintf(stderr, "staxibake: cannot write %s\n", outPath); - return 1; - } - if (!writeHeader(out, count)) { - fprintf(stderr, "staxibake: header write failed\n"); - fclose(out); - return 1; - } - for (i = 0u; i < count; i++) { - uint8_t blob[ST_CEL_BYTES]; - const uint8_t *bm; - if (cels == levelCels) { - bm = levelBitmap(&level, cels[i].ptr); - if (bm == NULL) { - fprintf(stderr, "staxibake: level %s has no sprite block $%02X\n", argv[2], (unsigned)cels[i].ptr); - fclose(out); - return 1; - } - } else { - bm = stC64SpriteBitmap((uint8_t)(cels[i].ptr - ST_SPRITE_PTR_FIRST)); - } - stCelBlob(bm, cels[i].multi, cels[i].color, cels[i].mc0, cels[i].mc1, blob); - if (fwrite(blob, 1, ST_CEL_BYTES, out) != ST_CEL_BYTES) { - fprintf(stderr, "staxibake: cel %u write failed\n", (unsigned)i); - fclose(out); - return 1; - } - } - fclose(out); - fprintf(stderr, "staxibake: wrote %u cels (%ux%u tiles) -> %s\n", (unsigned)count, (unsigned)ST_CEL_TILES, (unsigned)ST_CEL_TILES, outPath); - return 0; -} diff --git a/tools/staxiharness/Makefile b/tools/staxiharness/Makefile deleted file mode 100644 index 09c44a7..0000000 --- a/tools/staxiharness/Makefile +++ /dev/null @@ -1,36 +0,0 @@ -# Host-side checks of Space Taxi's front-end screens (the parts tools/stsim -# cannot reach: they live in spacetaxi.c, above the simulation). Builds the -# game on the BLANK port with spacetaxi.c INCLUDED into the harness so its -# statics are reachable, and wraps the keyboard queue and the save file -# with scripted stand-ins. -# -# make -C tools/staxiharness build + run - -REPO := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))/../..) -ST := $(REPO)/examples/spacetaxi -OUT := $(REPO)/build/tools/staxiharness -SRCS := $(filter-out $(ST)/spacetaxi.c,$(wildcard $(ST)/*.c)) \ - $(wildcard $(REPO)/src/core/*.c) $(wildcard $(REPO)/src/generic/*.c) \ - $(REPO)/src/blank/blank.c -CFLAGS := -O0 -g -w -DJOEYLIB_PLATFORM_BLANK -I$(REPO)/include -I$(REPO)/src/core \ - -I$(REPO)/src/codegen -I$(ST) -WRAPS := -Wl,--wrap=jlInputGetChar -Wl,--wrap=jlSaveWrite -Wl,--wrap=jlSaveRead - -.PHONY: all run clean - -all: run - -$(OUT)/harness: harness.c $(ST)/spacetaxi.c $(SRCS) - @mkdir -p $(OUT) - $(CC) $(CFLAGS) harness.c $(SRCS) $(WRAPS) -o $@ - -# The title screen's level file is read through DATA/levels (the DOS bake -# is the cross-platform STL4 form), so run from a directory that has one. -run: $(OUT)/harness - @mkdir -p $(OUT)/DATA - @ln -sfn $(ST)/generated/dos/levels $(OUT)/DATA/levels - @ln -sfn $(ST)/generated/dos/speech.bin $(OUT)/DATA/speech.bin - cd $(OUT) && ./harness - -clean: - rm -rf $(OUT) diff --git a/tools/staxiharness/harness.c b/tools/staxiharness/harness.c deleted file mode 100644 index c368693..0000000 --- a/tools/staxiharness/harness.c +++ /dev/null @@ -1,182 +0,0 @@ -// Space Taxi front-end harness: the immortal-cabbies table and the name -// entry screen, driven with scripted keys on the host (BLANK port). -// -// spacetaxi.c is INCLUDED so its static state and routines are visible; -// its main() is renamed away. jlInputGetChar, jlSaveWrite and jlSaveRead -// are replaced through the linker's --wrap with a key script and an -// in-memory save image (see the Makefile). -#define main staxiMain -#include "spacetaxi.c" -#undef main - -#include <stdio.h> - -#define SAVE_IMAGE_MAX 512u - -bool __wrap_jlSaveWrite(const char *name, const void *buf, uint32_t len); -uint32_t __wrap_jlSaveRead(const char *name, void *buf, uint32_t max); -int __wrap_jlInputGetChar(void); - -static void check(bool ok, const char *what); -static void rowText(uint8_t row, uint8_t col, uint8_t n, char *out); -static void setScore(uint8_t player, const uint8_t *digits); - -static const char *gKeys = ""; -static uint8_t gSaveImage[SAVE_IMAGE_MAX]; -static uint32_t gSaveLen = 0u; -static int gFails = 0; - - -int __wrap_jlInputGetChar(void) { - if (*gKeys == 0) { - return -1; - } - return (unsigned char)*gKeys++; -} - - -uint32_t __wrap_jlSaveRead(const char *name, void *buf, uint32_t max) { - (void)name; - if (gSaveLen == 0u || gSaveLen > max) { - return 0u; - } - memcpy(buf, gSaveImage, gSaveLen); - return gSaveLen; -} - - -bool __wrap_jlSaveWrite(const char *name, const void *buf, uint32_t len) { - (void)name; - if (len > sizeof(gSaveImage)) { - return false; - } - memcpy(gSaveImage, buf, len); - gSaveLen = len; - return true; -} - - -static void check(bool ok, const char *what) { - printf("%s %s\n", ok ? "ok " : "FAIL", what); - if (!ok) { - gFails++; - } -} - - -// `n` screen cells of one row as a C string. -static void rowText(uint8_t row, uint8_t col, uint8_t n, char *out) { - uint8_t k; - - for (k = 0u; k < n; k++) { - out[k] = (char)gSim.screen[ST_CELL(row, col + k)]; - } - out[n] = 0; -} - - -// A player's score as the sim keeps it: seven screen-code digits with -// the point at ST_HS_POINT_AT. -static void setScore(uint8_t player, const uint8_t *digits) { - uint8_t k; - - for (k = 0u; k < ST_NUMBER_CHARS; k++) { - gSim.scoreBackup[player][k] = (uint8_t)(ST_CHAR_DIGIT_BASE + digits[k]); - } - gSim.scoreBackup[player][ST_HS_POINT_AT] = ST_CHAR_POINT; -} - - -int main(void) { - static const uint8_t kDigits[ST_NUMBER_CHARS] = { 1u, 2u, 3u, 4u, 0u, 5u, 6u }; - jlConfigT config; - jlSurfaceT *stage; - char buf[48]; - uint8_t k; - - config.codegenBytes = 160UL * 1024; - if (!jlInit(&config)) { - printf("jlInit failed\n"); - return 1; - } - stage = jlStageGet(); - memset(&gGame, 0, sizeof(gGame)); - memset(&gSim, 0, sizeof(gSim)); - stRenderInit(stage); - (void)stAudioInit(); - stSimNewGame(&gSim, 2u, false); - - // ---- ranking ($4D92) ---- - check(highScoreInsert((const uint8_t *)" 49.99") == ST_HS_ENTRIES, "49.99 is below the $50 floor"); - check(highScoreInsert((const uint8_t *)" 50.00") == ST_HS_ENTRIES, "50.00 passes the floor but beats nobody"); - check(highScoreInsert((const uint8_t *)" 500.00") == 8u, "500.00 lands in slot 8 (under 504.97, above 498.77)"); - check(memcmp(gHighScores[9].score, " 498.77", 7) == 0 && memcmp(gHighScores[7].score, " 504.97", 7) == 0, "older entries shifted down, the last one dropped"); - check(highScoreInsert((const uint8_t *)"3877.56") == 0u, "a tie goes above the older entry"); - check(memcmp(gHighScores[1].score, "3877.56", 7) == 0 && memcmp(gHighScores[1].name, "MICHAEL PLATE ", 15) == 0, "the tied old entry is now second"); - - // ---- the entry screen for cabbie 2 with $1234.56 on shift 3 ($4D52 / $4E35) ---- - gSim.player = 1u; - gGame.variation = 3u; - setScore(1u, kDigits); - gKeys = "zz"; // typed before the screen: flushed like $0277 - check(nameEntryStart(), "1234.56 ranks"); - check(gGame.state == ST_STATE_NAME_ENTRY && gGame.hsSlot == 3u, "slot 3 (under the two 3877.56 and 1809.51)"); - check(gSim.borderColor == 2u && gSim.bgColor == 15u, "red border, grey field"); - rowText(3u, 7u, 25u, buf); - check(strcmp(buf, "CONGRATULATIONS CABBIE 2,") == 0, buf); - rowText(0u, 15u, 8u, buf); - check(strcmp(buf, "$1234.56") == 0, buf); - rowText(9u, 9u, 21u, buf); - check(strcmp(buf, "ENTER YOUR NAME BELOW") == 0, buf); - check(*gKeys == 0, "earlier keystrokes discarded"); - - // ---- typing ($4ED8): s c o 9 t t <backspace> T ---- - gKeys = "sco9tt\bT"; - runNameEntry(1u); - rowText(ST_HS_NAME_ROW, ST_HS_NAME_COL, 5u, buf); - check(strcmp(buf, "SCOTT") == 0, buf); - check(gGame.hsLen == 5u, "five characters kept ('9' ignored, one deleted, one retyped)"); - check(gGame.state == ST_STATE_NAME_ENTRY, "still entering"); - for (k = 0u; k < 2u; k++) { - runNameEntry(1u); - } - check(gSim.screen[ST_CELL(ST_HS_NAME_ROW, ST_HS_NAME_COL + 5u)] == ' ', "first flip blanks the cursor ($4F4D)"); - for (k = 0u; k < 3u; k++) { - runNameEntry(1u); - } - check(gSim.screen[ST_CELL(ST_HS_NAME_ROW, ST_HS_NAME_COL + 5u)] == '%', "second flip shows it"); - gKeys = "\r"; - runNameEntry(1u); - check(gGame.state != ST_STATE_NAME_ENTRY, "RETURN leaves the screen"); - check(memcmp(gHighScores[3].name, "SCOTT ", 15) == 0 && gHighScores[3].shift == 3u, "name and shift stored"); - check(gSaveLen == 240u && gSaveImage[3 * 24 + 7] == 0 && memcmp(&gSaveImage[3 * 24 + 8], "SCOTT ", 6) == 0 && gSaveImage[3 * 24 + 23] == 3u, "saved in the C64 image layout"); - check(gSim.playersDone == 1u, "game-over bookkeeping continued"); - - // ---- reload from the image ($23E5) ---- - memset(gHighScores, 0, sizeof(gHighScores)); - highScoreLoad(); - check(memcmp(gHighScores[3].name, "SCOTT ", 15) == 0 && memcmp(gHighScores[0].score, "3877.56", 7) == 0, "table restored from the save"); - - // ---- an empty name becomes "*" ---- - gSim.player = 0u; - setScore(0u, kDigits); - check(nameEntryStart() && gGame.hsSlot == 3u, "second entry, a tie goes above"); - gKeys = "\r"; - runNameEntry(1u); - check(gHighScores[3].name[0] == '*' && memcmp(gHighScores[4].name, "SCOTT", 5) == 0, "an empty name is stored as *"); - - // ---- the table screen ($4C1F) ---- - highScoreDraw(); - rowText(1u, 9u, 20u, buf); - check(strcmp(buf, "THE IMMORTAL CABBIES") == 0, buf); - rowText(12u, 4u, 26u, buf); - check(strcmp(buf, "$1234.56 SCOTT ") == 0, buf); - check(gSim.screen[ST_CELL(12, 34)] == '3' && gSim.screen[ST_CELL(10, 15)] == '*', "shift digit in the SHIFT column, * name above"); - rowText(24u, 3u, 34u, buf); - check(strcmp(buf, "PRESS FIRE TO RETURN TO TITLE PAGE") == 0, buf); - check(jlMusicIsPlaying(), "song 7 under the table"); - - printf("%s (%d failures)\n", gFails ? "staxiharness: FAIL" : "staxiharness: PASS", gFails); - jlShutdown(); - return gFails ? 1 : 0; -} diff --git a/tools/stsim/Makefile b/tools/stsim/Makefile deleted file mode 100644 index 0101e3f..0000000 --- a/tools/stsim/Makefile +++ /dev/null @@ -1,17 +0,0 @@ -# Host build of the Space Taxi simulation core for trace comparison. -ST := ../../examples/spacetaxi -CC ?= gcc -CFLAGS := -O1 -g -Wall -Wextra -std=c99 -I$(ST) -SRCS := stsim.c $(ST)/stSim.c $(ST)/stFare.c $(ST)/stHooks.c $(ST)/stLevelFile.c $(ST)/stTitle.c $(ST)/stC64Data.c $(ST)/stLevelCels.c - -stsim: $(SRCS) $(ST)/stSim.h $(ST)/stC64Data.h $(ST)/stDemoStreams.h gateStreams.h - $(CC) $(CFLAGS) $(SRCS) -o $@ - -# The fixture generator for gateStreams.h. Not built by default: it runs -# a long search and is only needed when a level's stream is (re)made. -recorder: recorder.c $(SRCS:stsim.c=) $(ST)/stSim.h - $(CC) $(CFLAGS) recorder.c $(ST)/stSim.c $(ST)/stFare.c $(ST)/stHooks.c \ - $(ST)/stLevelFile.c $(ST)/stTitle.c $(ST)/stC64Data.c -o $@ - -clean: - rm -f stsim recorder diff --git a/tools/stsim/celGate.sh b/tools/stsim/celGate.sh deleted file mode 100755 index 68f5e72..0000000 --- a/tools/stsim/celGate.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash -# Cel gate for Space Taxi: every level cel a recorded ride DISPLAYS must be -# in the level's baked manifest (stLevelCelList in stLevelCels.c), or the -# real ports build it mid-ride and draw it interpreted -- the "compiling -# sprites on the fly" hitch. Runs the same rides gate.sh does with the -# stsim census (STSIM_CELS=1) and fails on any MISSING line. -# -# celGate.sh check every hook level -set -u - -here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) -repo=$(cd "$here/../.." && pwd) -levels=$repo/examples/spacetaxi/generated/iigs/levels -ticks=4000 -pairs="H:08 T:20 W:23 X:24 G:07 I:09 K:11 O:15 Q:17 R:18 U:21 V:22 E:05 J:10 L:12 P:16 S:19" - -make -C "$here" >/dev/null || { echo "celGate: build failed" >&2; exit 2; } - -fail=0 -for p in $pairs; do - l=${p%%:*}; n=${p##*:} - out=$(STSIM_CELS=1 "$here/stsim" "$levels/level$n.dat" "$l" "$ticks" 2>&1 >/dev/null | grep -E '^(CEL|CENSUS)') - missing=$(echo "$out" | grep -c '^CEL MISSING') - census=$(echo "$out" | grep '^CENSUS' | sed 's/^CENSUS //') - if [ "$missing" -ne 0 ]; then - printf ' %s: *** %s cel(s) shown but not baked *** (%s)\n' "$l" "$missing" "$census" - echo "$out" | grep '^CEL MISSING' | sed 's/^/ /' - fail=1 - else - printf ' %s: ok (%s)\n' "$l" "$census" - fi -done -[ $fail -eq 0 ] && echo "celGate: PASS" || echo "celGate: FAIL" -exit $fail diff --git a/tools/stsim/compare.py b/tools/stsim/compare.py deleted file mode 100644 index 9174b31..0000000 --- a/tools/stsim/compare.py +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env python3 -# Compare stsim output against a VICE trace segment. -# compare.py <trace.jsonl> <startTick> <stsim-output.jsonl> [maxReports] -import json, sys - -trace = [json.loads(l) for l in open(sys.argv[1])] -start = int(sys.argv[2]) -sim = [json.loads(l) for l in open(sys.argv[3])] -maxRep = int(sys.argv[4]) if len(sys.argv) > 4 else 12 - -def fromTrace(r): - g = bytes.fromhex(r['g7140']) - pos = bytes.fromhex(r['pos']) - hud = bytes.fromhex(r['hud']) # 0x0799 .. 0x07E6 - def G(a): return g[a - 0x7140] - def H(a): return hud[a - 0x0799] - d = {} - d['x'] = '%02X%02X.%02X' % (pos[9], pos[8], pos[7]) # 7D63 7D62 7D61 - d['y'] = '%02X.%02X' % (pos[11], pos[10]) - d['vx'] = '%02X%02X' % (G(0x714D), G(0x714C)) - d['vy'] = '%02X%02X' % (G(0x714F), G(0x714E)) - d['pad'] = G(0x7150) - d['st'] = G(0x7163) - d['ph'] = G(0x7164) - d['in'] = '%02X' % G(0x7169) - d['dir'] = '%02X' % G(0x716A) - d['ptr0'] = '%02X' % G(0x7197) - d['ptr1'] = '%02X' % G(0x7198) - d['s1'] = '%02X%02X.%02X' % (G(0x7186), G(0x7176), G(0x717E)) - d['en'] = '%02X' % G(0x7196) - d['t4740']= r['t4740'].upper() - d['rng'] = '%02X%02X' % (G(0x7171), G(0x7172)) - d['fuel'] = G(0x71C9) - d['slots']= G(0x715C) - d['asi'] = pos[0x7D8E - 0x7D5A] if len(pos) > 0x7D8E - 0x7D5A else -1 - d['score']= ''.join('%02X' % H(0x07C2 + k) for k in range(7)) - d['fare'] = ''.join('%02X' % H(0x07E0 + k) for k in range(7)) - d['fuelc']= ''.join('%02X' % H(0x07A6 + k) for k in range(12)) - # sprite shadow positions $71A7.. interleaved X,Y for 8 sprites + msb $718D - msb = G(0x718D) - sx = '' - for k in range(8): - x = G(0x71A7 + 2*k) | (0x100 if (msb >> k) & 1 else 0) - sx += '%03X%02X' % (x, G(0x71A8 + 2*k)) - d['sprx'] = sx - return d - -fields = ['x','y','vx','vy','pad','st','ph','in','dir','ptr0','ptr1','s1','en','t4740','rng','fuel','slots','score','fare','fuelc','sprx'] -reports = 0 -firstBad = None -for s in sim: - if 'end' in s: - print('sim ended at tick', s['tick'], 'result', s['end']) - break - t = s['tick'] - if start + t >= len(trace): - print('trace exhausted at sim tick', t); break - r = fromTrace(trace[start + t]) - # Disabled sprites keep stale shadow values on the C64; only compare - # positions/pointers of enabled ones. - en = int(r['en'], 16) - def spr(sx): - return ''.join(sx[k*5:k*5+5] if (en >> k) & 1 else '-----' for k in range(8)) - s2 = dict(s); r2 = dict(r) - s2['sprx'] = spr(s['sprx']); r2['sprx'] = spr(r['sprx']) - if not (en & 2): - s2['ptr1'] = r2['ptr1'] = '--'; s2['s1'] = r2['s1'] = '--' - bad = [f for f in fields if f in s2 and str(s2[f]) != str(r2[f])] - s = s2; r = r2 - if bad: - if firstBad is None: - firstBad = t - if reports < maxRep: - print(f'tick {t}: ' + ' '.join(f'{f}: sim={s[f]} c64={r[f]}' for f in bad)) - reports += 1 -if firstBad is None: - print('ALL', len(sim), 'ticks match') -else: - print('first mismatch at tick', firstBad) diff --git a/tools/stsim/gate.sh b/tools/stsim/gate.sh deleted file mode 100755 index 8320eb1..0000000 --- a/tools/stsim/gate.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env bash -# Behaviour gate for the Space Taxi simulation. -# -# Runs every hook level through stsim and compares the per-tick trace -# against a golden capture. A refactor that changes nothing observable -# leaves all seventeen traces byte-identical; anything else is a regression. -# -# H, W, T and X ride the C64's own attract recordings (the ones checked -# against VICE traces). The other thirteen ride synthesised fixtures from -# gateStreams.h -- a regression gate only, not evidence of C64 fidelity. -# -# gate.sh capture [dir] record goldens from the CURRENT tree -# gate.sh check [dir] compare the current tree against them -# gate.sh hashes print one md5 per level (compact reference) -# -# Capture from a tree you trust, refactor, then check. -set -u - -here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) -repo=$(cd "$here/../.." && pwd) -levels=$repo/examples/spacetaxi/generated/iigs/levels -mode=${1:-check} -dir=${2:-$here/golden} -ticks=4000 - -# level letter -> level file number -pairs="H:08 T:20 W:23 X:24 G:07 I:09 K:11 O:15 Q:17 R:18 U:21 V:22 E:05 J:10 L:12 P:16 S:19" - -make -C "$here" >/dev/null || { echo "gate: build failed" >&2; exit 2; } - -case "$mode" in -capture) - mkdir -p "$dir" - for p in $pairs; do - l=${p%%:*}; n=${p##*:} - "$here/stsim" "$levels/level$n.dat" "$l" "$ticks" > "$dir/demo$l.jsonl" 2>/dev/null - printf ' %s: %s ticks\n' "$l" "$(wc -l < "$dir/demo$l.jsonl")" - done - echo "gate: captured to $dir" - ;; -check) - fail=0 - for p in $pairs; do - l=${p%%:*}; n=${p##*:} - got=$(mktemp) - "$here/stsim" "$levels/level$n.dat" "$l" "$ticks" > "$got" 2>/dev/null - if [ ! -f "$dir/demo$l.jsonl" ]; then - echo " $l: no golden (run: gate.sh capture)" - elif cmp -s "$dir/demo$l.jsonl" "$got"; then - printf ' %s: IDENTICAL (%s ticks)\n' "$l" "$(wc -l < "$got")" - else - echo " $l: *** DIFFERS ***" - diff "$dir/demo$l.jsonl" "$got" | head -3 - fail=1 - fi - rm -f "$got" - done - [ $fail -eq 0 ] && echo "gate: PASS" || echo "gate: FAIL" - # The cel census rides the same recordings: a cel a ride displays that - # its manifest does not bake is a mid-ride interpreted sprite. - "$here/celGate.sh" || fail=1 - exit $fail - ;; -hashes) - for p in $pairs; do - l=${p%%:*}; n=${p##*:} - h=$("$here/stsim" "$levels/level$n.dat" "$l" "$ticks" 2>/dev/null | md5sum | cut -d' ' -f1) - printf '%s %s\n' "$l" "$h" - done - ;; -*) - sed -n '2,20p' "$0" - exit 2 - ;; -esac diff --git a/tools/stsim/gateHashes.txt b/tools/stsim/gateHashes.txt deleted file mode 100644 index 9c1b5c8..0000000 --- a/tools/stsim/gateHashes.txt +++ /dev/null @@ -1,21 +0,0 @@ -# Space Taxi behaviour gate -- md5 of each level's 4000-tick trace. -# Regenerate with: tools/stsim/gate.sh hashes -# H/W/T/X ride the C64's attract recordings; the rest ride -# synthesised fixtures from gateStreams.h (regression gate only). -H 760501b0e48f3922bfb0f23fd2831935 -T c0b3f9e9a74e0d526703cf9b1875557b -W 92226455989e8326577a6ea20df8c8ea -X 87251d616a1be6e0c9c9317987b0fe20 -G 73530651372c60042f1f46b5c0cbada0 -I 7fe4415429d2b72a50884424269cb685 -K 567be2ed37219356a055866459971345 -O 02455280933e915e57803a4059aaefd9 -Q a7732866d54ed06d52e5dc7406b5b229 -R 066a9be80107446893b4571d247ee5cc -U 02dd2f26245442ab016ef02e10fde611 -V cc2cb13c5e393d07fbece9c0c465e387 -E 21ff29d69d8318bcb2e870b9602e910e -J 4c22a7515949b1701ed4fedabb7823dd -L a0383cbde2800bb09353df15214934ef -P f345d0adc9e011662d582c4277ee663e -S daa3cd12320e32d3e9b6b97b19b0a6d0 diff --git a/tools/stsim/gateStreams.h b/tools/stsim/gateStreams.h deleted file mode 100644 index 8d179b5..0000000 --- a/tools/stsim/gateStreams.h +++ /dev/null @@ -1,460 +0,0 @@ -// Synthesised demo input streams -- a TEST FIXTURE, not game content. -// -// The C64 ships attract recordings for four screens only (H, W, T and X); -// those live in examples/spacetaxi/stDemoStreams.h and are the streams -// checked against VICE traces. The thirteen streams below were searched for -// by tools/stsim's recorder against this port's own simulation, scored on -// how long the cab survives and how much of the level's hook state, fare -// machine and screen animation the ride reaches. -// -// So they prove one thing and not another: they are a REGRESSION gate -- -// refactor the hooks and the tick-by-tick trace must not move -- but they -// are NOT evidence that a hook matches the original, because no C64 trace -// exists for these screens. Treat a change to these bytes as invalidating -// the goldens, and never put them in the attract rotation. - -#ifndef ST_GATE_STREAMS_H -#define ST_GATE_STREAMS_H - -#include <stdint.h> - -// level G -- 1400 ticks, 5 hook bytes, 10 fare stages, 256 pictures, 13 landings -// regenerate: tools/stsim/recorder levels/level??.dat G 20260921 -static const uint8_t kGateStreamG[320] = { - 0x88, 0x05, 0x81, 0x09, 0x81, 0x05, 0x81, 0x04, 0x81, 0x06, 0x84, 0x0C, - 0x81, 0x01, 0x89, 0x02, 0x81, 0x09, 0x82, 0x0D, 0x88, 0x05, 0x81, 0x03, - 0x80, 0x09, 0x81, 0x01, 0x90, 0x02, 0x81, 0x04, 0x90, 0x08, 0x81, 0x03, - 0x90, 0x0A, 0x88, 0x0E, 0x88, 0x0C, 0x90, 0x0C, 0x81, 0x04, 0x81, 0x0D, - 0x90, 0x0A, 0x81, 0x0D, 0x84, 0x0E, 0x81, 0x09, 0x81, 0x08, 0x81, 0x0C, - 0x82, 0x04, 0x81, 0x02, 0x81, 0x0E, 0x81, 0x02, 0x84, 0x01, 0x88, 0x0A, - 0x88, 0x05, 0x90, 0x09, 0x81, 0x02, 0x84, 0x09, 0x84, 0x05, 0x81, 0x0D, - 0x84, 0x09, 0x88, 0x03, 0x84, 0x06, 0x84, 0x0D, 0x81, 0x05, 0x84, 0x0C, - 0x88, 0x0C, 0x88, 0x0C, 0x88, 0x03, 0x88, 0x09, 0x81, 0x0A, 0x81, 0x09, - 0x81, 0x06, 0x81, 0x07, 0x81, 0x05, 0x88, 0x09, 0x88, 0x02, 0x81, 0x0B, - 0x84, 0x0A, 0x84, 0x05, 0x88, 0x03, 0x81, 0x05, 0x81, 0x0B, 0x81, 0x05, - 0x90, 0x05, 0x84, 0x09, 0x81, 0x06, 0x81, 0x07, 0x90, 0x0D, 0x81, 0x04, - 0x81, 0x0E, 0x88, 0x0D, 0x88, 0x0D, 0x88, 0x06, 0x82, 0x02, 0x88, 0x0D, - 0x90, 0x0E, 0x81, 0x04, 0x81, 0x0B, 0x84, 0x0B, 0x81, 0x05, 0x84, 0x07, - 0x88, 0x0A, 0x80, 0x04, 0x84, 0x07, 0x84, 0x0B, 0x88, 0x0D, 0x90, 0x0E, - 0x90, 0x06, 0x88, 0x08, 0x82, 0x0C, 0x81, 0x02, 0x81, 0x0E, 0x88, 0x0A, - 0x90, 0x0B, 0x81, 0x04, 0x88, 0x08, 0x90, 0x07, 0x84, 0x03, 0x82, 0x08, - 0x81, 0x05, 0x81, 0x03, 0x81, 0x03, 0x81, 0x06, 0x90, 0x0B, 0x84, 0x0E, - 0x82, 0x0E, 0x88, 0x08, 0x88, 0x04, 0x81, 0x0D, 0x88, 0x04, 0x81, 0x08, - 0x88, 0x0C, 0x81, 0x06, 0x81, 0x04, 0x90, 0x0B, 0x81, 0x08, 0x84, 0x04, - 0x90, 0x08, 0x84, 0x09, 0x88, 0x09, 0x81, 0x0B, 0x81, 0x07, 0x84, 0x0D, - 0x88, 0x0A, 0x88, 0x09, 0x90, 0x0B, 0x90, 0x02, 0x84, 0x0D, 0x81, 0x0E, - 0x90, 0x0D, 0x84, 0x07, 0x82, 0x0C, 0x81, 0x0E, 0x81, 0x06, 0x88, 0x09, - 0x90, 0x02, 0x90, 0x04, 0x81, 0x09, 0x84, 0x0A, 0x90, 0x06, 0x81, 0x04, - 0x90, 0x03, 0x90, 0x08, 0x84, 0x0B, 0x81, 0x02, 0x90, 0x04, 0x84, 0x03, - 0x81, 0x03, 0x84, 0x05, 0x90, 0x0C, 0x81, 0x01, 0x90, 0x0D, 0x81, 0x02, - 0x90, 0x05, 0x90, 0x0E, 0x81, 0x03, 0x90, 0x0A, -}; - -// level I -- 1400 ticks, 20 hook bytes, 10 fare stages, 256 pictures, 22 landings -// regenerate: tools/stsim/recorder levels/level??.dat I 20260918 -static const uint8_t kGateStreamI[320] = { - 0x84, 0x0E, 0x81, 0x06, 0x84, 0x0E, 0x88, 0x0B, 0x88, 0x05, 0x81, 0x03, - 0x82, 0x07, 0x90, 0x0A, 0x81, 0x08, 0x88, 0x0E, 0x88, 0x0E, 0x90, 0x0B, - 0x82, 0x0D, 0x81, 0x01, 0x81, 0x0A, 0x90, 0x0A, 0x82, 0x03, 0x81, 0x07, - 0x82, 0x05, 0x81, 0x0B, 0x81, 0x05, 0x82, 0x05, 0x82, 0x0C, 0x81, 0x08, - 0x88, 0x0C, 0x90, 0x04, 0x81, 0x02, 0x88, 0x08, 0x81, 0x03, 0x81, 0x05, - 0x81, 0x0A, 0x88, 0x0E, 0x81, 0x06, 0x84, 0x0E, 0x82, 0x07, 0x82, 0x01, - 0x81, 0x04, 0x82, 0x08, 0x90, 0x0C, 0x90, 0x05, 0x81, 0x08, 0x82, 0x0D, - 0x90, 0x06, 0x81, 0x0A, 0x84, 0x0B, 0x84, 0x0C, 0x84, 0x0C, 0x88, 0x09, - 0x81, 0x06, 0x81, 0x0C, 0x81, 0x04, 0x82, 0x09, 0x88, 0x05, 0x82, 0x0E, - 0x81, 0x0C, 0x81, 0x07, 0x88, 0x01, 0x90, 0x0E, 0x81, 0x02, 0x90, 0x0E, - 0x81, 0x02, 0x90, 0x07, 0x84, 0x05, 0x84, 0x06, 0x88, 0x06, 0x88, 0x0C, - 0x81, 0x0C, 0x84, 0x0B, 0x82, 0x09, 0x81, 0x05, 0x81, 0x0B, 0x82, 0x09, - 0x90, 0x0E, 0x90, 0x06, 0x88, 0x0D, 0x84, 0x03, 0x82, 0x0B, 0x84, 0x0D, - 0x90, 0x0B, 0x90, 0x04, 0x81, 0x0B, 0x81, 0x08, 0x88, 0x08, 0x90, 0x0A, - 0x81, 0x03, 0x81, 0x01, 0x81, 0x0E, 0x81, 0x04, 0x88, 0x02, 0x90, 0x05, - 0x81, 0x07, 0x81, 0x01, 0x82, 0x09, 0x90, 0x06, 0x81, 0x09, 0x90, 0x0E, - 0x88, 0x05, 0x82, 0x0D, 0x88, 0x03, 0x81, 0x06, 0x84, 0x0C, 0x82, 0x01, - 0x81, 0x09, 0x82, 0x0E, 0x90, 0x01, 0x81, 0x01, 0x84, 0x05, 0x82, 0x06, - 0x88, 0x0A, 0x81, 0x05, 0x82, 0x03, 0x90, 0x07, 0x90, 0x08, 0x81, 0x02, - 0x90, 0x0D, 0x88, 0x0D, 0x81, 0x08, 0x90, 0x0E, 0x82, 0x0D, 0x88, 0x04, - 0x81, 0x0C, 0x82, 0x09, 0x90, 0x04, 0x81, 0x0C, 0x82, 0x06, 0x90, 0x0E, - 0x90, 0x0D, 0x90, 0x07, 0x90, 0x03, 0x82, 0x03, 0x88, 0x06, 0x81, 0x07, - 0x90, 0x08, 0x81, 0x02, 0x90, 0x09, 0x81, 0x04, 0x90, 0x06, 0x82, 0x0A, - 0x90, 0x05, 0x88, 0x05, 0x88, 0x06, 0x90, 0x0B, 0x81, 0x03, 0x82, 0x02, - 0x90, 0x0B, 0x81, 0x02, 0x90, 0x0C, 0x90, 0x07, 0x81, 0x01, 0x84, 0x08, - 0x90, 0x0C, 0x81, 0x0E, 0x88, 0x08, 0x88, 0x0C, 0x81, 0x0C, 0x90, 0x04, - 0x84, 0x0D, 0x81, 0x01, 0x88, 0x08, 0x88, 0x09, -}; - -// level K -- 1400 ticks, 1 hook bytes, 10 fare stages, 256 pictures, 16 landings -// regenerate: tools/stsim/recorder levels/level??.dat K 11 -static const uint8_t kGateStreamK[320] = { - 0x84, 0x06, 0x82, 0x0B, 0x90, 0x0C, 0x82, 0x0E, 0x90, 0x05, 0x88, 0x03, - 0x88, 0x02, 0x82, 0x0E, 0x88, 0x07, 0x84, 0x02, 0x84, 0x0B, 0x84, 0x08, - 0x90, 0x0A, 0x88, 0x07, 0x84, 0x07, 0x81, 0x03, 0x84, 0x08, 0x82, 0x0E, - 0x82, 0x0D, 0x84, 0x01, 0x81, 0x09, 0x81, 0x0E, 0x81, 0x0C, 0x81, 0x0B, - 0x82, 0x0C, 0x82, 0x06, 0x81, 0x01, 0x84, 0x01, 0x90, 0x06, 0x82, 0x0D, - 0x84, 0x0B, 0x82, 0x09, 0x88, 0x09, 0x88, 0x05, 0x82, 0x02, 0x84, 0x04, - 0x84, 0x09, 0x88, 0x08, 0x90, 0x09, 0x82, 0x08, 0x81, 0x01, 0x88, 0x01, - 0x81, 0x06, 0x90, 0x04, 0x82, 0x0A, 0x82, 0x09, 0x82, 0x09, 0x84, 0x0C, - 0x82, 0x07, 0x90, 0x0E, 0x90, 0x0E, 0x90, 0x07, 0x82, 0x0D, 0x90, 0x02, - 0x84, 0x04, 0x84, 0x09, 0x88, 0x05, 0x88, 0x03, 0x81, 0x05, 0x82, 0x0E, - 0x90, 0x04, 0x90, 0x08, 0x88, 0x0C, 0x90, 0x08, 0x81, 0x05, 0x82, 0x0B, - 0x90, 0x0B, 0x84, 0x05, 0x82, 0x0D, 0x82, 0x08, 0x84, 0x0D, 0x81, 0x01, - 0x90, 0x08, 0x82, 0x0C, 0x81, 0x09, 0x82, 0x05, 0x84, 0x01, 0x82, 0x0A, - 0x82, 0x0E, 0x88, 0x0A, 0x84, 0x04, 0x90, 0x01, 0x90, 0x0D, 0x82, 0x08, - 0x90, 0x0B, 0x84, 0x03, 0x81, 0x01, 0x90, 0x0D, 0x82, 0x0C, 0x82, 0x05, - 0x81, 0x02, 0x81, 0x06, 0x84, 0x08, 0x81, 0x01, 0x82, 0x06, 0x88, 0x0E, - 0x88, 0x02, 0x82, 0x0A, 0x90, 0x0E, 0x82, 0x0B, 0x81, 0x0E, 0x81, 0x0A, - 0x82, 0x08, 0x90, 0x01, 0x81, 0x04, 0x81, 0x0D, 0x90, 0x0C, 0x88, 0x0D, - 0x88, 0x07, 0x81, 0x05, 0x90, 0x02, 0x82, 0x07, 0x88, 0x02, 0x84, 0x0E, - 0x82, 0x0D, 0x90, 0x02, 0x88, 0x04, 0x90, 0x0D, 0x82, 0x0A, 0x82, 0x0D, - 0x88, 0x09, 0x81, 0x04, 0x84, 0x08, 0x90, 0x0B, 0x81, 0x01, 0x90, 0x0C, - 0x82, 0x0E, 0x84, 0x04, 0x82, 0x06, 0x90, 0x09, 0x84, 0x03, 0x90, 0x01, - 0x82, 0x0C, 0x81, 0x03, 0x82, 0x05, 0x90, 0x0E, 0x82, 0x0E, 0x84, 0x07, - 0x90, 0x06, 0x81, 0x02, 0x90, 0x03, 0x82, 0x09, 0x82, 0x0A, 0x81, 0x05, - 0x82, 0x0D, 0x84, 0x01, 0x90, 0x0A, 0x82, 0x09, 0x81, 0x04, 0x82, 0x02, - 0x82, 0x08, 0x90, 0x06, 0x90, 0x03, 0x84, 0x09, 0x82, 0x01, 0x82, 0x08, - 0x82, 0x09, 0x90, 0x01, 0x88, 0x07, 0x88, 0x08, -}; - -// level O -- 1400 ticks, 1 hook bytes, 3 fare stages, 256 pictures, 0 landings -// regenerate: tools/stsim/recorder levels/level??.dat O 20260918 -static const uint8_t kGateStreamO[320] = { - 0x84, 0x0E, 0x90, 0x08, 0x82, 0x07, 0x81, 0x0B, 0x84, 0x02, 0x81, 0x09, - 0x82, 0x06, 0x90, 0x0C, 0x81, 0x08, 0x88, 0x01, 0x88, 0x02, 0x90, 0x04, - 0x88, 0x06, 0x81, 0x04, 0x82, 0x0A, 0x81, 0x06, 0x81, 0x0E, 0x82, 0x08, - 0x88, 0x06, 0x82, 0x03, 0x81, 0x06, 0x81, 0x02, 0x88, 0x0D, 0x82, 0x04, - 0x84, 0x0B, 0x84, 0x03, 0x81, 0x04, 0x84, 0x06, 0x81, 0x04, 0x81, 0x02, - 0x84, 0x07, 0x82, 0x06, 0x81, 0x06, 0x90, 0x05, 0x88, 0x09, 0x81, 0x0A, - 0x81, 0x0C, 0x81, 0x02, 0x82, 0x09, 0x82, 0x0D, 0x81, 0x04, 0x81, 0x0B, - 0x84, 0x03, 0x88, 0x0A, 0x90, 0x0A, 0x81, 0x08, 0x82, 0x08, 0x81, 0x06, - 0x81, 0x03, 0x82, 0x0A, 0x81, 0x0C, 0x82, 0x08, 0x81, 0x0C, 0x82, 0x0E, - 0x81, 0x06, 0x81, 0x04, 0x81, 0x0A, 0x84, 0x05, 0x90, 0x0E, 0x84, 0x06, - 0x82, 0x0B, 0x81, 0x0E, 0x81, 0x03, 0x81, 0x05, 0x90, 0x08, 0x81, 0x01, - 0x90, 0x0E, 0x84, 0x0D, 0x88, 0x01, 0x90, 0x05, 0x84, 0x0B, 0x81, 0x04, - 0x81, 0x04, 0x82, 0x01, 0x90, 0x04, 0x82, 0x0D, 0x88, 0x07, 0x81, 0x0B, - 0x81, 0x07, 0x82, 0x01, 0x88, 0x0A, 0x81, 0x06, 0x90, 0x02, 0x82, 0x08, - 0x81, 0x01, 0x81, 0x0A, 0x88, 0x0E, 0x88, 0x0D, 0x81, 0x03, 0x88, 0x0D, - 0x81, 0x02, 0x88, 0x04, 0x84, 0x0E, 0x90, 0x01, 0x82, 0x0C, 0x81, 0x0B, - 0x81, 0x04, 0x88, 0x03, 0x81, 0x03, 0x88, 0x09, 0x84, 0x0E, 0x81, 0x02, - 0x81, 0x02, 0x81, 0x08, 0x84, 0x07, 0x84, 0x02, 0x88, 0x0C, 0x84, 0x08, - 0x90, 0x09, 0x90, 0x0D, 0x81, 0x02, 0x88, 0x0D, 0x84, 0x0C, 0x84, 0x0D, - 0x84, 0x01, 0x81, 0x07, 0x81, 0x0A, 0x81, 0x01, 0x84, 0x01, 0x84, 0x0D, - 0x82, 0x0E, 0x81, 0x02, 0x81, 0x06, 0x81, 0x09, 0x88, 0x0D, 0x88, 0x09, - 0x82, 0x0B, 0x81, 0x0B, 0x88, 0x02, 0x88, 0x0C, 0x81, 0x04, 0x84, 0x0B, - 0x81, 0x06, 0x88, 0x09, 0x84, 0x0C, 0x81, 0x03, 0x84, 0x0D, 0x84, 0x0D, - 0x88, 0x0E, 0x84, 0x04, 0x90, 0x0E, 0x81, 0x02, 0x88, 0x01, 0x88, 0x06, - 0x81, 0x0E, 0x82, 0x08, 0x81, 0x0D, 0x90, 0x0D, 0x88, 0x0E, 0x81, 0x03, - 0x8A, 0x09, 0x90, 0x0B, 0x84, 0x07, 0x88, 0x03, 0x81, 0x0C, 0x82, 0x07, - 0x81, 0x0E, 0x81, 0x08, 0x90, 0x0D, 0x84, 0x0D, -}; - -// level Q -- 1400 ticks, 5 hook bytes, 10 fare stages, 256 pictures, 10 landings -// regenerate: tools/stsim/recorder levels/level??.dat Q 20260918 -static const uint8_t kGateStreamQ[320] = { - 0x84, 0x0E, 0x81, 0x04, 0x81, 0x03, 0x84, 0x09, 0x88, 0x0A, 0x81, 0x09, - 0x82, 0x06, 0x88, 0x0C, 0x81, 0x08, 0x82, 0x06, 0x88, 0x0E, 0x84, 0x0B, - 0x88, 0x08, 0x81, 0x09, 0x81, 0x0A, 0x84, 0x05, 0x84, 0x01, 0x88, 0x01, - 0x81, 0x08, 0x84, 0x0B, 0x81, 0x05, 0x82, 0x05, 0x80, 0x0D, 0x81, 0x08, - 0x88, 0x0A, 0x88, 0x04, 0x84, 0x08, 0x81, 0x06, 0x81, 0x0C, 0x81, 0x01, - 0x90, 0x0A, 0x84, 0x08, 0x81, 0x0A, 0x81, 0x0A, 0x82, 0x08, 0x81, 0x06, - 0x81, 0x09, 0x90, 0x06, 0x81, 0x09, 0x90, 0x0D, 0x88, 0x04, 0x82, 0x05, - 0x81, 0x0B, 0x84, 0x08, 0x81, 0x08, 0x81, 0x03, 0x82, 0x0D, 0x82, 0x0A, - 0x84, 0x0E, 0x81, 0x05, 0x81, 0x0E, 0x81, 0x08, 0x84, 0x0A, 0x82, 0x0C, - 0x84, 0x09, 0x84, 0x01, 0x82, 0x06, 0x88, 0x0E, 0x81, 0x09, 0x90, 0x07, - 0x81, 0x09, 0x81, 0x07, 0x84, 0x03, 0x81, 0x0B, 0x84, 0x01, 0x90, 0x08, - 0x90, 0x06, 0x82, 0x03, 0x88, 0x06, 0x88, 0x0C, 0x84, 0x0A, 0x81, 0x03, - 0x82, 0x0E, 0x81, 0x07, 0x81, 0x04, 0x81, 0x0D, 0x84, 0x02, 0x81, 0x09, - 0x82, 0x08, 0x88, 0x04, 0x81, 0x09, 0x84, 0x0C, 0x81, 0x0B, 0x84, 0x0D, - 0x81, 0x02, 0x82, 0x03, 0x81, 0x05, 0x88, 0x0E, 0x84, 0x0D, 0x81, 0x0B, - 0x88, 0x08, 0x84, 0x0C, 0x90, 0x07, 0x82, 0x0B, 0x81, 0x03, 0x90, 0x05, - 0x84, 0x05, 0x84, 0x09, 0x81, 0x04, 0x81, 0x0B, 0x81, 0x08, 0x82, 0x0E, - 0x81, 0x0A, 0x81, 0x03, 0x81, 0x06, 0x88, 0x09, 0x84, 0x0A, 0x88, 0x0B, - 0x81, 0x05, 0x81, 0x0D, 0x88, 0x03, 0x88, 0x0D, 0x81, 0x01, 0x90, 0x06, - 0x88, 0x07, 0x82, 0x02, 0x81, 0x04, 0x84, 0x09, 0x81, 0x07, 0x90, 0x06, - 0x82, 0x03, 0x84, 0x07, 0x81, 0x05, 0x90, 0x0B, 0x81, 0x0B, 0x88, 0x05, - 0x81, 0x0D, 0x81, 0x0B, 0x92, 0x0E, 0x90, 0x05, 0x82, 0x02, 0x88, 0x0C, - 0x84, 0x08, 0x81, 0x0A, 0x81, 0x0D, 0x81, 0x05, 0x81, 0x07, 0x81, 0x01, - 0x81, 0x04, 0x88, 0x01, 0x81, 0x0E, 0x84, 0x04, 0x90, 0x03, 0x81, 0x0D, - 0x90, 0x09, 0x81, 0x02, 0x81, 0x08, 0x90, 0x07, 0x82, 0x0B, 0x90, 0x08, - 0x81, 0x03, 0x90, 0x0C, 0x81, 0x0B, 0x88, 0x08, 0x90, 0x04, 0x81, 0x02, - 0x90, 0x0C, 0x88, 0x05, 0x81, 0x01, 0x90, 0x03, -}; - -// level R -- 1400 ticks, 0 hook bytes, 3 fare stages, 256 pictures, 0 landings -// regenerate: tools/stsim/recorder levels/level??.dat R 20260918 -static const uint8_t kGateStreamR[320] = { - 0x90, 0x0E, 0x81, 0x02, 0x82, 0x07, 0x81, 0x05, 0x88, 0x0B, 0x88, 0x09, - 0x90, 0x0D, 0x90, 0x06, 0x81, 0x08, 0x81, 0x01, 0x90, 0x02, 0x88, 0x04, - 0x82, 0x07, 0x90, 0x06, 0x88, 0x07, 0x81, 0x06, 0x81, 0x06, 0x82, 0x05, - 0x84, 0x07, 0x82, 0x05, 0x81, 0x03, 0x84, 0x0E, 0x88, 0x05, 0x88, 0x09, - 0x88, 0x04, 0x81, 0x09, 0x81, 0x02, 0x82, 0x04, 0x84, 0x0B, 0x81, 0x04, - 0x82, 0x0C, 0x81, 0x03, 0x88, 0x06, 0x84, 0x08, 0x81, 0x0D, 0x88, 0x09, - 0x90, 0x0E, 0x82, 0x07, 0x82, 0x02, 0x84, 0x0D, 0x88, 0x0B, 0x81, 0x0B, - 0x81, 0x03, 0x82, 0x02, 0x90, 0x0B, 0x84, 0x03, 0x88, 0x02, 0x88, 0x09, - 0x81, 0x02, 0x82, 0x0A, 0x81, 0x06, 0x81, 0x02, 0x90, 0x0A, 0x82, 0x0E, - 0x81, 0x09, 0x88, 0x03, 0x81, 0x07, 0x84, 0x0D, 0x84, 0x0A, 0x81, 0x03, - 0x82, 0x07, 0x84, 0x0D, 0x81, 0x03, 0x81, 0x08, 0x84, 0x0E, 0x81, 0x02, - 0x82, 0x0E, 0x88, 0x03, 0x81, 0x01, 0x81, 0x05, 0x81, 0x06, 0x90, 0x0A, - 0x84, 0x0A, 0x81, 0x02, 0x82, 0x05, 0x81, 0x03, 0x88, 0x07, 0x84, 0x0C, - 0x81, 0x04, 0x88, 0x09, 0x90, 0x0E, 0x82, 0x0A, 0x88, 0x09, 0x82, 0x01, - 0x81, 0x06, 0x81, 0x0D, 0x84, 0x0A, 0x82, 0x0D, 0x88, 0x0A, 0x84, 0x0A, - 0x90, 0x02, 0x84, 0x02, 0x81, 0x09, 0x82, 0x0A, 0x81, 0x0B, 0x81, 0x0D, - 0x82, 0x05, 0x84, 0x09, 0x84, 0x04, 0x88, 0x06, 0x84, 0x04, 0x88, 0x0D, - 0x81, 0x02, 0x82, 0x0B, 0x81, 0x0E, 0x82, 0x0D, 0x82, 0x0A, 0x81, 0x02, - 0x81, 0x09, 0x81, 0x0D, 0x84, 0x04, 0x82, 0x09, 0x88, 0x04, 0x81, 0x07, - 0x90, 0x0A, 0x90, 0x0B, 0x81, 0x07, 0x84, 0x0D, 0x82, 0x0A, 0x81, 0x0C, - 0x88, 0x07, 0x84, 0x09, 0x82, 0x08, 0x90, 0x0A, 0x84, 0x06, 0x82, 0x0D, - 0x88, 0x05, 0x81, 0x06, 0x81, 0x05, 0x90, 0x0D, 0x90, 0x0B, 0x81, 0x0C, - 0x81, 0x04, 0x84, 0x06, 0x90, 0x08, 0x90, 0x08, 0x90, 0x08, 0x84, 0x0E, - 0x88, 0x0E, 0x81, 0x0E, 0x82, 0x0A, 0x90, 0x04, 0x81, 0x06, 0x82, 0x09, - 0x84, 0x08, 0x90, 0x07, 0x84, 0x0C, 0x90, 0x05, 0x88, 0x0C, 0x84, 0x06, - 0x84, 0x0A, 0x81, 0x08, 0x81, 0x01, 0x81, 0x01, 0x82, 0x08, 0x88, 0x0A, - 0x90, 0x0A, 0x90, 0x01, 0x84, 0x0A, 0x81, 0x0B, -}; - -// level U -- 896 ticks, 21 hook bytes, 3 fare stages, 256 pictures, 0 landings -// regenerate: tools/stsim/recorder levels/level??.dat U 44 -static const uint8_t kGateStreamU[320] = { - 0x90, 0x01, 0x90, 0x0B, 0x88, 0x08, 0x90, 0x07, 0x88, 0x0D, 0x82, 0x0A, - 0x82, 0x06, 0x88, 0x0C, 0x90, 0x03, 0x81, 0x04, 0x84, 0x0B, 0x81, 0x0C, - 0x88, 0x02, 0x84, 0x08, 0x84, 0x01, 0x81, 0x04, 0x90, 0x05, 0x84, 0x06, - 0x84, 0x08, 0x84, 0x05, 0x81, 0x03, 0x81, 0x09, 0x81, 0x08, 0x88, 0x07, - 0x81, 0x08, 0x84, 0x06, 0x81, 0x0B, 0x88, 0x0D, 0x90, 0x08, 0x81, 0x01, - 0x82, 0x01, 0x88, 0x03, 0x81, 0x0E, 0x82, 0x01, 0x81, 0x0D, 0x81, 0x07, - 0x88, 0x05, 0x88, 0x07, 0x88, 0x0E, 0x84, 0x07, 0x81, 0x03, 0x81, 0x01, - 0x84, 0x03, 0x81, 0x04, 0x81, 0x08, 0x90, 0x06, 0x81, 0x0C, 0x82, 0x05, - 0x81, 0x08, 0x82, 0x09, 0x84, 0x06, 0x81, 0x06, 0x90, 0x0C, 0x84, 0x0E, - 0x82, 0x0C, 0x81, 0x03, 0x81, 0x06, 0x84, 0x0D, 0x88, 0x03, 0x84, 0x0C, - 0x81, 0x02, 0x81, 0x09, 0x81, 0x0E, 0x88, 0x01, 0x90, 0x06, 0x88, 0x08, - 0x81, 0x08, 0x81, 0x0C, 0x81, 0x0C, 0x82, 0x06, 0x82, 0x08, 0x88, 0x01, - 0x81, 0x07, 0x88, 0x0D, 0x84, 0x03, 0x81, 0x02, 0x82, 0x06, 0x81, 0x05, - 0x81, 0x0E, 0x81, 0x04, 0x90, 0x01, 0x81, 0x02, 0x90, 0x03, 0x81, 0x0A, - 0x81, 0x04, 0x84, 0x02, 0x81, 0x06, 0x81, 0x0E, 0x81, 0x06, 0x84, 0x07, - 0x84, 0x04, 0x81, 0x07, 0x81, 0x0E, 0x81, 0x0D, 0x90, 0x0D, 0x90, 0x05, - 0x88, 0x0C, 0x88, 0x02, 0x81, 0x0B, 0x90, 0x07, 0x88, 0x06, 0x82, 0x0D, - 0x84, 0x08, 0x84, 0x08, 0x81, 0x01, 0x81, 0x06, 0x82, 0x05, 0x84, 0x06, - 0x81, 0x06, 0x84, 0x0D, 0x81, 0x04, 0x90, 0x07, 0x88, 0x0D, 0x81, 0x09, - 0x88, 0x0D, 0x81, 0x0E, 0x81, 0x03, 0x84, 0x0E, 0x81, 0x03, 0x81, 0x0C, - 0x84, 0x0A, 0x81, 0x06, 0x90, 0x02, 0x81, 0x01, 0x88, 0x02, 0x90, 0x02, - 0x81, 0x08, 0x84, 0x01, 0x88, 0x0B, 0x88, 0x0B, 0x81, 0x04, 0x81, 0x0C, - 0x81, 0x07, 0x82, 0x06, 0x88, 0x0C, 0x81, 0x03, 0x90, 0x02, 0x88, 0x0A, - 0x84, 0x08, 0x81, 0x0E, 0x81, 0x05, 0x88, 0x0C, 0x81, 0x0D, 0x81, 0x05, - 0x81, 0x04, 0x90, 0x0D, 0x88, 0x03, 0x88, 0x06, 0x81, 0x0B, 0x81, 0x0D, - 0x84, 0x0D, 0x81, 0x01, 0x81, 0x0D, 0x81, 0x01, 0x82, 0x07, 0x88, 0x02, - 0x81, 0x09, 0x90, 0x0B, 0x88, 0x0D, 0x82, 0x02, -}; - -// level V -- 1400 ticks, 0 hook bytes, 6 fare stages, 256 pictures, 23 landings -// regenerate: tools/stsim/recorder levels/level??.dat V 20260918 -static const uint8_t kGateStreamV[320] = { - 0x84, 0x0E, 0x81, 0x04, 0x81, 0x05, 0x88, 0x0B, 0x84, 0x01, 0x81, 0x03, - 0x82, 0x06, 0x84, 0x0C, 0x81, 0x08, 0x84, 0x07, 0x82, 0x03, 0x88, 0x0A, - 0x90, 0x03, 0x81, 0x0D, 0x81, 0x0D, 0x82, 0x05, 0x81, 0x02, 0x81, 0x0E, - 0x90, 0x06, 0x81, 0x05, 0x90, 0x05, 0x81, 0x0E, 0x82, 0x0D, 0x90, 0x0E, - 0x88, 0x0E, 0x81, 0x03, 0x90, 0x07, 0x84, 0x0A, 0x81, 0x02, 0x90, 0x07, - 0x82, 0x07, 0x81, 0x02, 0x81, 0x06, 0x82, 0x08, 0x81, 0x04, 0x88, 0x0C, - 0x81, 0x06, 0x88, 0x0D, 0x90, 0x0D, 0x81, 0x05, 0x90, 0x03, 0x84, 0x0D, - 0x84, 0x0A, 0x88, 0x0A, 0x88, 0x0D, 0x81, 0x08, 0x90, 0x08, 0x90, 0x01, - 0x84, 0x06, 0x82, 0x0B, 0x81, 0x0D, 0x82, 0x07, 0x82, 0x0C, 0x81, 0x01, - 0x90, 0x08, 0x81, 0x02, 0x81, 0x04, 0x90, 0x0C, 0x84, 0x04, 0x88, 0x04, - 0x82, 0x03, 0x88, 0x07, 0x81, 0x03, 0x90, 0x05, 0x81, 0x02, 0x81, 0x03, - 0x88, 0x01, 0x90, 0x05, 0x81, 0x08, 0x81, 0x06, 0x81, 0x07, 0x81, 0x08, - 0x81, 0x06, 0x81, 0x02, 0x84, 0x0B, 0x88, 0x0D, 0x81, 0x06, 0x90, 0x02, - 0x81, 0x07, 0x82, 0x0B, 0x88, 0x04, 0x84, 0x04, 0x84, 0x05, 0x81, 0x05, - 0x81, 0x02, 0x82, 0x09, 0x81, 0x03, 0x90, 0x0C, 0x88, 0x0B, 0x82, 0x01, - 0x88, 0x0D, 0x88, 0x05, 0x81, 0x02, 0x90, 0x0B, 0x81, 0x02, 0x90, 0x0A, - 0x88, 0x0C, 0x81, 0x01, 0x90, 0x0A, 0x88, 0x0E, 0x90, 0x0A, 0x84, 0x0B, - 0x81, 0x03, 0x90, 0x08, 0x88, 0x03, 0x82, 0x0E, 0x84, 0x06, 0x90, 0x0E, - 0x81, 0x03, 0x88, 0x0A, 0x90, 0x0A, 0x82, 0x09, 0x81, 0x04, 0x88, 0x0C, - 0x84, 0x04, 0x81, 0x07, 0x90, 0x03, 0x81, 0x01, 0x81, 0x01, 0x81, 0x01, - 0x82, 0x0E, 0x81, 0x05, 0x81, 0x06, 0x81, 0x08, 0x84, 0x08, 0x88, 0x0C, - 0x82, 0x09, 0x88, 0x05, 0x81, 0x02, 0x88, 0x04, 0x81, 0x04, 0x88, 0x0C, - 0x90, 0x07, 0x88, 0x0E, 0x81, 0x03, 0x81, 0x0A, 0x82, 0x0B, 0x81, 0x02, - 0x88, 0x02, 0x90, 0x0E, 0x88, 0x0D, 0x81, 0x03, 0x88, 0x09, 0x90, 0x04, - 0x84, 0x01, 0x88, 0x04, 0x90, 0x04, 0x84, 0x04, 0x81, 0x03, 0x90, 0x0C, - 0x88, 0x07, 0x84, 0x05, 0x81, 0x02, 0x90, 0x0B, 0x90, 0x04, 0x90, 0x09, - 0x81, 0x02, 0x81, 0x01, 0x90, 0x0B, 0x88, 0x04, -}; - -// level E -- 1400 ticks, 4 hook bytes, 8 fare stages, 256 pictures, 11 landings -// regenerate: tools/stsim/recorder levels/level??.dat E 20260921 -static const uint8_t kGateStreamE[320] = { - 0x84, 0x05, 0x82, 0x03, 0x82, 0x05, 0x81, 0x0B, 0x81, 0x07, 0x81, 0x02, - 0x81, 0x01, 0x89, 0x02, 0x90, 0x03, 0x81, 0x0A, 0x81, 0x05, 0x84, 0x0C, - 0x81, 0x02, 0x88, 0x07, 0x81, 0x0B, 0x90, 0x08, 0x90, 0x0A, 0x88, 0x02, - 0x81, 0x05, 0x84, 0x08, 0x81, 0x05, 0x90, 0x03, 0x84, 0x01, 0x84, 0x08, - 0x88, 0x0A, 0x84, 0x07, 0x81, 0x04, 0x84, 0x07, 0x90, 0x0C, 0x80, 0x08, - 0x90, 0x07, 0x88, 0x03, 0x90, 0x0B, 0x88, 0x01, 0x90, 0x07, 0x88, 0x04, - 0x81, 0x02, 0x88, 0x08, 0x84, 0x05, 0x81, 0x05, 0x81, 0x02, 0x81, 0x09, - 0x88, 0x05, 0x90, 0x0E, 0x81, 0x05, 0x81, 0x01, 0x90, 0x04, 0x90, 0x08, - 0x81, 0x06, 0x84, 0x0E, 0x81, 0x09, 0x81, 0x01, 0x84, 0x05, 0x81, 0x01, - 0x81, 0x02, 0x88, 0x0B, 0x90, 0x08, 0x88, 0x0C, 0x88, 0x05, 0x88, 0x08, - 0x84, 0x09, 0x88, 0x05, 0x90, 0x05, 0x82, 0x0A, 0x81, 0x07, 0x90, 0x0A, - 0x81, 0x07, 0x81, 0x03, 0x84, 0x0B, 0x84, 0x0E, 0x88, 0x09, 0x90, 0x02, - 0x81, 0x0E, 0x81, 0x06, 0x90, 0x0E, 0x81, 0x02, 0x90, 0x08, 0x84, 0x08, - 0x81, 0x0B, 0x88, 0x0D, 0x84, 0x0C, 0x81, 0x09, 0x90, 0x08, 0x82, 0x0B, - 0x88, 0x08, 0x81, 0x07, 0x82, 0x02, 0x84, 0x0B, 0x88, 0x0B, 0x81, 0x0D, - 0x84, 0x0E, 0x82, 0x01, 0x81, 0x03, 0x90, 0x0C, 0x81, 0x05, 0x90, 0x04, - 0x82, 0x0C, 0x81, 0x06, 0x90, 0x0E, 0x88, 0x0B, 0x88, 0x09, 0x81, 0x0E, - 0x82, 0x0E, 0x90, 0x0C, 0x81, 0x06, 0x81, 0x05, 0x84, 0x0E, 0x81, 0x0C, - 0x88, 0x0B, 0x90, 0x06, 0x82, 0x05, 0x81, 0x06, 0x88, 0x0D, 0x88, 0x07, - 0x81, 0x07, 0x84, 0x07, 0x88, 0x08, 0x81, 0x07, 0x90, 0x0C, 0x84, 0x04, - 0x84, 0x0A, 0x88, 0x0B, 0x80, 0x0A, 0x81, 0x0C, 0x81, 0x07, 0x88, 0x06, - 0x88, 0x09, 0x88, 0x09, 0x81, 0x0C, 0x80, 0x06, 0x81, 0x06, 0x81, 0x07, - 0x84, 0x08, 0x82, 0x09, 0x84, 0x0C, 0x90, 0x09, 0x90, 0x03, 0x90, 0x0D, - 0x81, 0x08, 0x84, 0x05, 0x84, 0x09, 0x88, 0x0B, 0x81, 0x01, 0x81, 0x0A, - 0x90, 0x0D, 0x88, 0x0E, 0x88, 0x0D, 0x88, 0x05, 0x84, 0x06, 0x81, 0x0E, - 0x81, 0x0A, 0x81, 0x08, 0x84, 0x0C, 0x88, 0x05, 0x81, 0x02, 0x90, 0x0E, - 0x84, 0x0D, 0x84, 0x0A, 0x81, 0x05, 0x90, 0x09, -}; - -// level J -- 1400 ticks, 10 hook bytes, 10 fare stages, 256 pictures, 8 landings -// regenerate: tools/stsim/recorder levels/level??.dat J 20260921 -static const uint8_t kGateStreamJ[320] = { - 0x88, 0x05, 0x82, 0x09, 0x81, 0x01, 0x81, 0x04, 0x90, 0x06, 0x81, 0x05, - 0x81, 0x01, 0x90, 0x09, 0x88, 0x05, 0x82, 0x08, 0x81, 0x0A, 0x81, 0x03, - 0x84, 0x07, 0x88, 0x0D, 0x81, 0x06, 0x81, 0x0A, 0x84, 0x05, 0x90, 0x08, - 0x81, 0x0B, 0x90, 0x0E, 0x84, 0x0E, 0x81, 0x03, 0x90, 0x09, 0x81, 0x02, - 0x88, 0x04, 0x88, 0x08, 0x84, 0x0E, 0x81, 0x04, 0x88, 0x0D, 0x90, 0x0E, - 0x84, 0x09, 0x90, 0x05, 0x81, 0x01, 0x81, 0x03, 0x88, 0x03, 0x88, 0x07, - 0x81, 0x0B, 0x90, 0x0B, 0x88, 0x08, 0x82, 0x03, 0x90, 0x09, 0x88, 0x03, - 0x84, 0x06, 0x88, 0x0A, 0x88, 0x0B, 0x84, 0x04, 0x81, 0x05, 0x88, 0x0E, - 0x81, 0x0A, 0x90, 0x0C, 0x85, 0x04, 0x81, 0x0E, 0x90, 0x02, 0x82, 0x0D, - 0x84, 0x01, 0x81, 0x02, 0x88, 0x09, 0x81, 0x0B, 0x81, 0x02, 0x82, 0x0C, - 0x84, 0x04, 0x81, 0x05, 0x81, 0x04, 0x81, 0x04, 0x81, 0x05, 0x90, 0x05, - 0x88, 0x09, 0x81, 0x02, 0x88, 0x01, 0x81, 0x05, 0x90, 0x04, 0x81, 0x0B, - 0x81, 0x05, 0x82, 0x0D, 0x88, 0x06, 0x88, 0x07, 0x88, 0x02, 0x84, 0x02, - 0x90, 0x07, 0x88, 0x09, 0x84, 0x0C, 0x81, 0x01, 0x88, 0x0B, 0x81, 0x0C, - 0x82, 0x0A, 0x81, 0x08, 0x82, 0x01, 0x88, 0x09, 0x84, 0x05, 0x81, 0x08, - 0x84, 0x0A, 0x84, 0x09, 0x88, 0x0E, 0x81, 0x01, 0x84, 0x03, 0x82, 0x0C, - 0x81, 0x0E, 0x84, 0x0A, 0x88, 0x02, 0x84, 0x0D, 0x81, 0x0A, 0x90, 0x0E, - 0x88, 0x09, 0x90, 0x09, 0x81, 0x05, 0x90, 0x03, 0x88, 0x09, 0x84, 0x06, - 0x81, 0x01, 0x88, 0x06, 0x81, 0x05, 0x81, 0x09, 0x81, 0x06, 0x90, 0x04, - 0x82, 0x0A, 0x90, 0x07, 0x81, 0x07, 0x81, 0x0A, 0x84, 0x0E, 0x84, 0x07, - 0x81, 0x0A, 0x90, 0x0C, 0x81, 0x01, 0x90, 0x01, 0x88, 0x0D, 0x81, 0x03, - 0x88, 0x05, 0x81, 0x03, 0x88, 0x01, 0x81, 0x01, 0x82, 0x02, 0x88, 0x07, - 0x83, 0x02, 0x90, 0x07, 0x81, 0x05, 0x81, 0x04, 0x81, 0x0E, 0x88, 0x09, - 0x81, 0x07, 0x90, 0x03, 0x82, 0x0B, 0x82, 0x0B, 0x90, 0x04, 0x81, 0x05, - 0x81, 0x03, 0x90, 0x0C, 0x82, 0x07, 0x88, 0x0E, 0x90, 0x04, 0x81, 0x0A, - 0x88, 0x02, 0x88, 0x0D, 0x82, 0x07, 0x90, 0x01, 0x81, 0x02, 0x81, 0x01, - 0x82, 0x07, 0x81, 0x08, 0x84, 0x0E, 0x81, 0x03, -}; - -// level L -- 1400 ticks, 0 hook bytes, 10 fare stages, 256 pictures, 8 landings -// regenerate: tools/stsim/recorder levels/level??.dat L 20260921 -static const uint8_t kGateStreamL[320] = { - 0x84, 0x05, 0x84, 0x09, 0x81, 0x06, 0x81, 0x04, 0x81, 0x08, 0x81, 0x05, - 0x81, 0x01, 0x84, 0x09, 0x90, 0x09, 0x82, 0x0D, 0x88, 0x07, 0x81, 0x06, - 0x81, 0x09, 0x82, 0x01, 0x81, 0x08, 0x88, 0x0B, 0x84, 0x0E, 0x81, 0x03, - 0x82, 0x02, 0x82, 0x06, 0x82, 0x03, 0x88, 0x0E, 0x84, 0x0C, 0x81, 0x06, - 0x88, 0x02, 0x81, 0x04, 0x88, 0x0E, 0x81, 0x04, 0x90, 0x0B, 0x81, 0x08, - 0x84, 0x09, 0x82, 0x09, 0x81, 0x01, 0x90, 0x03, 0x81, 0x05, 0x84, 0x07, - 0x81, 0x07, 0x90, 0x0B, 0x81, 0x07, 0x88, 0x09, 0x81, 0x0E, 0x88, 0x03, - 0x81, 0x01, 0x88, 0x09, 0x81, 0x07, 0x81, 0x0C, 0x88, 0x0C, 0x90, 0x0E, - 0x81, 0x0E, 0x90, 0x05, 0x84, 0x0E, 0x82, 0x0E, 0x82, 0x01, 0x84, 0x0A, - 0x88, 0x03, 0x90, 0x09, 0x82, 0x05, 0x81, 0x0E, 0x81, 0x05, 0x81, 0x07, - 0x81, 0x0D, 0x81, 0x0E, 0x90, 0x04, 0x90, 0x04, 0x81, 0x06, 0x81, 0x01, - 0x88, 0x0B, 0x81, 0x01, 0x90, 0x0B, 0x81, 0x05, 0x84, 0x0D, 0x81, 0x01, - 0x81, 0x0A, 0x84, 0x0D, 0x82, 0x09, 0x88, 0x06, 0x88, 0x04, 0x84, 0x0C, - 0x88, 0x0C, 0x84, 0x02, 0x81, 0x07, 0x82, 0x07, 0x84, 0x08, 0x88, 0x09, - 0x81, 0x01, 0x88, 0x01, 0x81, 0x0C, 0x81, 0x04, 0x84, 0x06, 0x82, 0x06, - 0x90, 0x0D, 0x81, 0x06, 0x81, 0x0D, 0x82, 0x0D, 0x82, 0x08, 0x81, 0x0C, - 0x84, 0x05, 0x82, 0x09, 0x81, 0x05, 0x84, 0x07, 0x82, 0x02, 0x84, 0x01, - 0x81, 0x02, 0x90, 0x02, 0x82, 0x06, 0x88, 0x03, 0x84, 0x0B, 0x81, 0x0E, - 0x88, 0x0E, 0x90, 0x07, 0x82, 0x08, 0x82, 0x02, 0x90, 0x02, 0x82, 0x03, - 0x84, 0x07, 0x82, 0x05, 0x84, 0x07, 0x81, 0x0B, 0x82, 0x0E, 0x90, 0x05, - 0x88, 0x04, 0x90, 0x05, 0x84, 0x0A, 0x82, 0x0A, 0x88, 0x0C, 0x81, 0x08, - 0x82, 0x0C, 0x81, 0x06, 0x84, 0x09, 0x81, 0x04, 0x84, 0x0B, 0x88, 0x0B, - 0x81, 0x0C, 0x81, 0x0C, 0x82, 0x0E, 0x81, 0x03, 0x82, 0x09, 0x88, 0x04, - 0x81, 0x05, 0x88, 0x04, 0x80, 0x0B, 0x84, 0x06, 0x81, 0x0E, 0x84, 0x0B, - 0x90, 0x0B, 0x90, 0x07, 0x82, 0x0D, 0x84, 0x0D, 0x81, 0x08, 0x81, 0x09, - 0x84, 0x0E, 0x81, 0x0B, 0x90, 0x04, 0x84, 0x0B, 0x84, 0x03, 0x81, 0x03, - 0x88, 0x0A, 0x81, 0x07, 0x84, 0x0B, 0x90, 0x0E, -}; - -// level P -- 1400 ticks, 11 hook bytes, 10 fare stages, 256 pictures, 4 landings -// regenerate: tools/stsim/recorder levels/level??.dat P 20260921 -static const uint8_t kGateStreamP[320] = { - 0x88, 0x05, 0x82, 0x09, 0x81, 0x0B, 0x81, 0x04, 0x81, 0x04, 0x84, 0x05, - 0x82, 0x01, 0x90, 0x0C, 0x81, 0x03, 0x81, 0x0D, 0x81, 0x01, 0x84, 0x0C, - 0x84, 0x0E, 0x88, 0x07, 0x81, 0x0B, 0x88, 0x0D, 0x90, 0x0A, 0x88, 0x02, - 0x81, 0x05, 0x84, 0x08, 0x81, 0x05, 0x90, 0x0C, 0x84, 0x01, 0x88, 0x08, - 0x88, 0x0A, 0x84, 0x0C, 0x81, 0x04, 0x84, 0x07, 0x90, 0x0C, 0x80, 0x09, - 0x90, 0x06, 0x81, 0x03, 0x81, 0x0C, 0x88, 0x01, 0x82, 0x03, 0x81, 0x04, - 0x84, 0x0D, 0x88, 0x09, 0x84, 0x05, 0x81, 0x05, 0x81, 0x02, 0x81, 0x09, - 0x81, 0x02, 0x90, 0x0E, 0x81, 0x05, 0x81, 0x01, 0x81, 0x03, 0x88, 0x0A, - 0x81, 0x06, 0x88, 0x05, 0x90, 0x0C, 0x90, 0x01, 0x84, 0x0E, 0x88, 0x01, - 0x81, 0x02, 0x82, 0x05, 0x90, 0x08, 0x88, 0x0A, 0x84, 0x0C, 0x90, 0x08, - 0x84, 0x0C, 0x81, 0x06, 0x81, 0x03, 0x88, 0x0A, 0x84, 0x08, 0x90, 0x0E, - 0x81, 0x02, 0x88, 0x0C, 0x81, 0x06, 0x84, 0x0C, 0x81, 0x09, 0x80, 0x07, - 0x88, 0x04, 0x81, 0x07, 0x81, 0x03, 0x81, 0x09, 0x81, 0x05, 0x86, 0x05, - 0x90, 0x09, 0x84, 0x06, 0x81, 0x03, 0x84, 0x09, 0x81, 0x0E, 0x88, 0x0B, - 0x90, 0x08, 0x88, 0x0A, 0x81, 0x04, 0x90, 0x0D, 0x88, 0x0E, 0x88, 0x09, - 0x81, 0x0B, 0x90, 0x0C, 0x84, 0x05, 0x81, 0x08, 0x84, 0x01, 0x82, 0x02, - 0x81, 0x03, 0x82, 0x0E, 0x88, 0x02, 0x88, 0x05, 0x81, 0x01, 0x81, 0x08, - 0x82, 0x03, 0x84, 0x0E, 0x81, 0x01, 0x81, 0x08, 0x84, 0x03, 0x82, 0x03, - 0x82, 0x05, 0x81, 0x0A, 0x81, 0x02, 0x90, 0x01, 0x81, 0x0E, 0x81, 0x0C, - 0x90, 0x0C, 0x88, 0x0A, 0x84, 0x03, 0x81, 0x03, 0x84, 0x0A, 0x84, 0x05, - 0x82, 0x09, 0x88, 0x0A, 0x81, 0x06, 0x84, 0x07, 0x84, 0x0C, 0x81, 0x0C, - 0x82, 0x07, 0x81, 0x06, 0x81, 0x03, 0x88, 0x09, 0x90, 0x01, 0x84, 0x0E, - 0x81, 0x01, 0x88, 0x06, 0x81, 0x0E, 0x81, 0x0A, 0x88, 0x0A, 0x81, 0x08, - 0x84, 0x08, 0x88, 0x0E, 0x81, 0x02, 0x84, 0x0E, 0x82, 0x05, 0x81, 0x06, - 0x88, 0x0C, 0x81, 0x0B, 0x81, 0x0D, 0x84, 0x02, 0x90, 0x0E, 0x84, 0x0E, - 0x81, 0x02, 0x90, 0x07, 0x88, 0x0C, 0x90, 0x0A, 0x81, 0x0E, 0x81, 0x0A, - 0x81, 0x0D, 0x81, 0x0B, 0x81, 0x08, 0x81, 0x03, -}; - -// level S -- 1400 ticks, 0 hook bytes, 10 fare stages, 256 pictures, 23 landings -// regenerate: tools/stsim/recorder levels/level??.dat S 20260921 -static const uint8_t kGateStreamS[320] = { - 0x90, 0x04, 0x90, 0x0E, 0x88, 0x08, 0x82, 0x0D, 0x88, 0x0C, 0x81, 0x03, - 0x81, 0x0A, 0x81, 0x0D, 0x88, 0x09, 0x88, 0x0C, 0x82, 0x0E, 0x81, 0x07, - 0x81, 0x0D, 0x81, 0x0A, 0x90, 0x0D, 0x84, 0x01, 0x90, 0x05, 0x88, 0x0E, - 0x81, 0x0A, 0x81, 0x0A, 0x90, 0x06, 0x88, 0x03, 0x82, 0x0B, 0x82, 0x0B, - 0x88, 0x03, 0x88, 0x0A, 0x90, 0x0C, 0x81, 0x0E, 0x81, 0x0E, 0x90, 0x06, - 0x88, 0x02, 0x82, 0x08, 0x81, 0x0A, 0x90, 0x0D, 0x90, 0x05, 0x81, 0x05, - 0x88, 0x0B, 0x81, 0x05, 0x90, 0x0B, 0x88, 0x0E, 0x82, 0x09, 0x90, 0x07, - 0x81, 0x03, 0x81, 0x0D, 0x88, 0x09, 0x88, 0x0E, 0x84, 0x0A, 0x88, 0x0A, - 0x82, 0x01, 0x90, 0x0C, 0x82, 0x01, 0x90, 0x0A, 0x82, 0x0A, 0x84, 0x03, - 0x81, 0x02, 0x90, 0x04, 0x84, 0x0E, 0x90, 0x09, 0x81, 0x0D, 0x90, 0x06, - 0x81, 0x02, 0x90, 0x0E, 0x90, 0x07, 0x82, 0x05, 0x81, 0x06, 0x90, 0x07, - 0x82, 0x03, 0x90, 0x07, 0x90, 0x0B, 0x82, 0x06, 0x88, 0x0E, 0x90, 0x08, - 0x84, 0x0A, 0x82, 0x07, 0x81, 0x04, 0x81, 0x03, 0x84, 0x05, 0x90, 0x08, - 0x81, 0x0C, 0x82, 0x01, 0x90, 0x0B, 0x81, 0x0A, 0x84, 0x05, 0x81, 0x02, - 0x82, 0x09, 0x90, 0x0E, 0x81, 0x02, 0x84, 0x03, 0x81, 0x0C, 0x81, 0x0C, - 0x81, 0x0D, 0x82, 0x08, 0x81, 0x03, 0x81, 0x0D, 0x81, 0x0A, 0x81, 0x0A, - 0x81, 0x0C, 0x82, 0x06, 0x90, 0x0D, 0x88, 0x0C, 0x84, 0x04, 0x81, 0x01, - 0x81, 0x0A, 0x81, 0x03, 0x90, 0x06, 0x88, 0x07, 0x88, 0x0E, 0x84, 0x0E, - 0x81, 0x0E, 0x82, 0x03, 0x90, 0x0E, 0x82, 0x05, 0x90, 0x02, 0x88, 0x03, - 0x81, 0x07, 0x81, 0x0A, 0x81, 0x0B, 0x81, 0x05, 0x84, 0x0D, 0x81, 0x01, - 0x82, 0x02, 0x90, 0x0C, 0x82, 0x03, 0x90, 0x08, 0x81, 0x05, 0x84, 0x08, - 0x84, 0x06, 0x88, 0x08, 0x81, 0x0E, 0x82, 0x01, 0x81, 0x01, 0x90, 0x08, - 0x82, 0x03, 0x90, 0x0C, 0x84, 0x02, 0x81, 0x09, 0x82, 0x04, 0x88, 0x0D, - 0x90, 0x07, 0x88, 0x0E, 0x82, 0x05, 0x81, 0x0C, 0x90, 0x0A, 0x81, 0x04, - 0x84, 0x06, 0x82, 0x02, 0x81, 0x09, 0x90, 0x0D, 0x82, 0x06, 0x82, 0x01, - 0x90, 0x09, 0x81, 0x0B, 0x84, 0x04, 0x88, 0x07, 0x81, 0x0B, 0x81, 0x05, - 0x82, 0x03, 0x81, 0x07, 0x90, 0x06, 0x82, 0x07, -}; - -typedef struct { - uint8_t level; // level letter the ride was searched for - const uint8_t *stream; - uint16_t length; -} StGateStreamT; - -static const StGateStreamT kGateStreams[] = { - { 'G', kGateStreamG, (uint16_t)sizeof(kGateStreamG) }, - { 'I', kGateStreamI, (uint16_t)sizeof(kGateStreamI) }, - { 'K', kGateStreamK, (uint16_t)sizeof(kGateStreamK) }, - { 'O', kGateStreamO, (uint16_t)sizeof(kGateStreamO) }, - { 'Q', kGateStreamQ, (uint16_t)sizeof(kGateStreamQ) }, - { 'R', kGateStreamR, (uint16_t)sizeof(kGateStreamR) }, - { 'U', kGateStreamU, (uint16_t)sizeof(kGateStreamU) }, - { 'V', kGateStreamV, (uint16_t)sizeof(kGateStreamV) }, - { 'E', kGateStreamE, (uint16_t)sizeof(kGateStreamE) }, - { 'J', kGateStreamJ, (uint16_t)sizeof(kGateStreamJ) }, - { 'L', kGateStreamL, (uint16_t)sizeof(kGateStreamL) }, - { 'P', kGateStreamP, (uint16_t)sizeof(kGateStreamP) }, - { 'S', kGateStreamS, (uint16_t)sizeof(kGateStreamS) }, -}; -#define ST_GATE_STREAMS (sizeof(kGateStreams) / sizeof(kGateStreams[0])) - -#endif diff --git a/tools/stsim/recorder.c b/tools/stsim/recorder.c deleted file mode 100644 index f5be285..0000000 --- a/tools/stsim/recorder.c +++ /dev/null @@ -1,195 +0,0 @@ -// recorder -- search for a demo input stream that exercises a level. -// -// The C64 ships attract recordings for four screens (H, W, T, X). The -// other levels have none, so a refactor of their hooks had nothing to be -// checked against. This searches for a synthetic ride instead: random -// restarts plus hill climbing over (joystick mask, duration) pairs, -// scored on how long the cab survives and how much of the level it -// reaches -- hook state bytes written, fare-machine stages entered, -// distinct pictures drawn, pads landed on. -// -// The result is a REGRESSION fixture, not a recording: it pins current -// behaviour so a refactor that changes nothing observable leaves the -// trace identical. It says nothing about matching the real C64. -// -// Usage: recorder <level.dat> <letter> <seed> > stream.h -// Then assemble the per-level outputs into gateStreams.h. -// -// Determinism: a candidate is only useful if it replays identically from -// a cold start, so every evaluation clears the whole StSimT (see the note -// in evaluate) -- stSimNewGame deliberately preserves some cross-screen -// state, which would otherwise leak between candidates. - -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include "stSim.h" -#include "stDemoStreams.h" - -static StLevelT gLevel; -static StSimT gSim; - -void stAudioSfx(const uint8_t *p){(void)p;} -void stAudioNoise(bool o){(void)o;} -void stAudioThrustSweep(uint8_t v){(void)v;} -void stAudioSpeech(uint8_t c){(void)c;} -void stAudioSpeechPhrase(const uint8_t *s){(void)s;} -void stAudioSilence(void){} -void stAudioVoice2(uint8_t a,uint8_t b,uint8_t c){(void)a;(void)b;(void)c;} -void stAudioVoice1Freq(uint8_t v){(void)v;} - -#define PAIRS 160 -#define STREAMLEN (PAIRS * 2) -#define MAXTICKS 1400 - -typedef struct { uint32_t score; uint32_t ticks; uint32_t cover; uint32_t lands; uint32_t stages; uint32_t scr; uint32_t near; } ResT; - -static uint32_t rs = 1; -static uint32_t rnd(void){ rs = rs*1664525u + 1013904223u; return rs>>8; } - -static ResT evaluate(const uint8_t *stream, uint8_t *touchOut) -{ - uint8_t touched[sizeof(StHookScratchT)]; - uint8_t base[sizeof(StHookScratchT)]; - uint8_t stageSeen[16]; - uint32_t t, cover = 0, lands = 0, stages = 0, scrSeen = 0; - uint32_t nearest = 255u; - uint32_t scrHash[256]; - uint8_t lastPad = 0; - ResT r; - - memset(stageSeen, 0, sizeof(stageSeen)); - memset(touched, 0, sizeof(touched)); - // stSimNewGame deliberately PRESERVES flameParity, waveIdx and - // walkParity (the C64 carries them from screen to screen), so a - // search that reuses one StSimT would score each candidate against - // whatever the previous ride left behind -- and the winning stream - // would then not reproduce from a cold start. Clear the whole struct - // so every evaluation begins exactly where the gate will. - memset(&gSim, 0, sizeof(gSim)); - stSimNewGame(&gSim, 1u, true); - stSimSeedDemoBuffer(&gSim, kDemoBufferInit, ST_DEMO_BUFFER_BYTES); - stSimEnterLevel(&gSim, &gLevel); - stSimSetDemoStream(&gSim, stream, STREAMLEN); - memcpy(base, &gSim.hookScratch, sizeof(base)); - for (t = 0; t < MAXTICKS; t++) { - uint16_t k; - if (stSimTick(&gSim) != ST_TICK_CONTINUE) { break; } - { const uint8_t *cur = (const uint8_t *)&gSim.hookScratch; - for (k = 0; k < (uint16_t)sizeof(base); k++) { - if (cur[k] != base[k]) { touched[k] = 1; } } } - if (gSim.activePad != 0u && lastPad == 0u) { lands++; } - lastPad = gSim.activePad; - if (gSim.stage < 16u) { stageSeen[gSim.stage] = 1u; } - /* Shaping: how close the ride came to an actual touchdown. Some - levels (R) only run their hook once a passenger has boarded, - which needs a landing, and a landing is far too precise for a - blind search to stumble on -- so reward getting near one. */ - if (gSim.activePad == 0u && (gSim.spr[0].ptr & 1u) != 0u) { - int16_t tx = (int16_t)(((uint16_t)gSim.posXmsb << 8) | gSim.posXcol); - uint8_t pi; - for (pi = 0u; pi < gLevel.padCount && pi < ST_MAX_PADS; pi++) { - int16_t x1 = (int16_t)(((uint16_t)gSim.pads[pi].x1Hi << 8) | gSim.pads[pi].x1Lo); - int16_t x2 = (int16_t)(((uint16_t)gSim.pads[pi].x2Hi << 8) | gSim.pads[pi].x2Lo); - int16_t dr = (int16_t)((int16_t)gSim.spr[0].row - (int16_t)gSim.pads[pi].row); - uint32_t ar = (uint32_t)(dr < 0 ? -dr : dr); - uint32_t ax = 0u; - uint32_t d; - if (tx < x1) { ax = (uint32_t)(x1 - tx); } - else if (tx > x2) { ax = (uint32_t)(tx - x2); } - /* Both axes matter: the row alone is easy to hit while - the cab sits at the far side of the screen. */ - d = ar * 4u + ax; - if (d < nearest) { nearest = d; } - } - } - /* distinct pictures the hook has produced (screen + colour) */ - { uint32_t h = 2166136261u; uint16_t c; uint32_t i2; int dup = 0; - for (c = 0; c < ST_SCREEN_CELLS; c++) { h = (h ^ gSim.screen[c]) * 16777619u; } - for (c = 0; c < ST_SCREEN_CELLS; c++) { h = (h ^ gSim.color[c]) * 16777619u; } - for (i2 = 0; i2 < scrSeen; i2++) { if (scrHash[i2] == h) { dup = 1; break; } } - if (!dup && scrSeen < 256u) { scrHash[scrSeen++] = h; } } - } - { uint16_t k; for (k = 0; k < (uint16_t)sizeof(touched); k++) { if (touched[k]) { cover++; } } } - { int k; for (k = 0; k < 16; k++) { if (stageSeen[k]) { stages++; } } } - if (touchOut) { memcpy(touchOut, touched, sizeof(touched)); } - r.ticks = t; r.cover = cover; r.lands = lands; r.stages = stages; r.scr = scrSeen; - r.near = nearest; - r.score = cover * 30000u + stages * 30000u + lands * 4000u + t * 100u + scrSeen * 200u - + (255u - nearest) * 60u; - return r; -} - -static void randomise(uint8_t *s, int from) -{ - int i; - for (i = from; i < PAIRS; i++) { - uint32_t d = rnd() % 100u; - uint8_t m = 0x80u; - if (d < 45u) { m |= 0x01u; } /* thrust up a lot */ - else if (d < 60u) { m |= 0x04u; } - else if (d < 75u) { m |= 0x08u; } - else if (d < 82u) { m |= 0x02u; } - else if (d < 88u) { m |= 0x10u; } - if (rnd() % 5u == 0u) { m |= (uint8_t)(1u << (rnd() % 4u)); } - s[i*2] = m; - s[i*2 + 1] = (uint8_t)(1u + rnd() % 14u); - } -} - -int main(int argc, char **argv) -{ - uint8_t best[STREAMLEN], cur[STREAMLEN], touched[ST_HOOK_BYTES]; - ResT bestR, curR; - FILE *fp; - int restart, iter; - - if (argc < 4) { fprintf(stderr, "usage: recorder <level.dat> <name> <seed>\n"); return 2; } - fp = fopen(argv[1], "rb"); - if (!fp || !stLevelParse(&gLevel, fp)) { fprintf(stderr, "cannot load %s\n", argv[1]); return 1; } - fclose(fp); - rs = (uint32_t)strtoul(argv[3], NULL, 0); - - randomise(best, 0); - bestR = evaluate(best, NULL); - for (restart = 0; restart < 120; restart++) { - memcpy(cur, best, STREAMLEN); - if (restart > 0) { randomise(cur, (int)(rnd() % PAIRS)); } - curR = evaluate(cur, NULL); - for (iter = 0; iter < 3000; iter++) { - uint8_t trial[STREAMLEN]; - ResT tr; - int j, nmut = 1 + (int)(rnd() % 3u); - memcpy(trial, cur, STREAMLEN); - for (j = 0; j < nmut; j++) { - int i = (int)(rnd() % PAIRS); - if (rnd() % 2u) { - uint8_t m = 0x80u; - uint32_t d = rnd() % 100u; - if (d < 45u) { m |= 0x01u; } - else if (d < 62u) { m |= 0x04u; } - else if (d < 79u) { m |= 0x08u; } - else if (d < 88u) { m |= 0x02u; } - else { m |= 0x10u; } - trial[i*2] = m; - } else { - trial[i*2 + 1] = (uint8_t)(1u + rnd() % 14u); - } - } - tr = evaluate(trial, NULL); - if (tr.score >= curR.score) { memcpy(cur, trial, STREAMLEN); curR = tr; } - } - if (curR.score > bestR.score) { memcpy(best, cur, STREAMLEN); bestR = curR; } - } - bestR = evaluate(best, touched); - fprintf(stderr, "%s: ticks=%u hookBytes=%u stages=%u pictures=%u lands=%u nearPad=%u\n", - argv[2], bestR.ticks, bestR.cover, bestR.stages, bestR.scr, bestR.lands, bestR.near); - printf("// %s: %u ticks, %u hook bytes, %u fare stages, %u pictures, %u landings\n", - argv[2], bestR.ticks, bestR.cover, bestR.stages, bestR.scr, bestR.lands); - printf("static const uint8_t kGateStream%s[%d] = {", argv[2], STREAMLEN); - { int i; for (i = 0; i < STREAMLEN; i++) { - if (i % 12 == 0) { printf("\n "); } - printf("0x%02X,%s", best[i], (i % 12 == 11) ? "" : " "); } } - printf("\n};\n"); - return 0; -} diff --git a/tools/stsim/stsim.c b/tools/stsim/stsim.c deleted file mode 100644 index 7791b46..0000000 --- a/tools/stsim/stsim.c +++ /dev/null @@ -1,238 +0,0 @@ -// stsim -- host-side driver for the Space Taxi simulation core. -// -// Loads an STL4 level, attaches a recorded demo input stream and runs -// the game tick, printing one line of state per tick in the same shape -// the VICE tracer records (see stuff/spacetaxi and the compare script), -// so the port's engine can be diffed against the real machine. -// -// stsim <level.dat> <level letter> <ticks> -// -// H, W, T and X use the C64's own attract recordings; every other -// letter uses a synthesised gate fixture from gateStreams.h. - -#include <stdio.h> -#include <stdlib.h> -#include <string.h> - -#include "../../examples/spacetaxi/stSim.h" -#include "../../examples/spacetaxi/stCels.h" -#include "../../examples/spacetaxi/stDemoStreams.h" -#include "gateStreams.h" - - -static StLevelT gLevel; -static StSimT gSim; - - -void stAudioSfx(const uint8_t *program9) { (void)program9; } -void stAudioNoise(bool on) { (void)on; } -void stAudioThrustSweep(uint8_t value) { (void)value; } -void stAudioSpeech(uint8_t ch) { (void)ch; } -void stAudioSpeechPhrase(const uint8_t *speech) { (void)speech; } -void stAudioSilence(void) { } -void stAudioVoice2(uint8_t freqLo, uint8_t freqHi, uint8_t ctrl) { (void)freqLo; (void)freqHi; (void)ctrl; } -void stAudioVoice1Freq(uint8_t value) { (void)value; } - - -// FNV-1a over a byte span: the picture the player actually sees. The -// hooks that animate the screen (O, R, V, W, X) change screen, colour or -// charset bytes and nothing else this line used to capture. -static uint32_t hashBytes(const uint8_t *p, size_t n) { - uint32_t h = 2166136261u; - size_t i; - - for (i = 0u; i < n; i++) { - h = (h ^ p[i]) * 16777619u; - } - return h; -} - - -// The charset the game would be showing. The sim no longer holds a -// contiguous 2 KB copy -- edited glyphs live in an override table -- so -// rebuild the effective image here. Hashing this keeps the trace value -// identical to the pre-override captures, which is what lets the goldens -// prove the change is behaviour-preserving. -static uint32_t charsetHash(const StSimT *sim) { - uint8_t image[ST_CHARSET_CHARS * 8u]; - uint16_t ch; - - for (ch = 0u; ch < ST_CHARSET_CHARS; ch++) { - memcpy(&image[ch * 8u], stSimGlyph(sim, (uint8_t)ch), 8u); - } - return hashBytes(image, sizeof(image)); -} - - -// STSIM_CELS=1: every level cel (ptr below the baked set) a ride -// displays, as (ptr, colour, multi, mc0, mc1), against what -// stLevelCelList bakes for the level -- a cel shown but not listed is -// built and drawn interpreted mid-ride on the real ports. -#define CENSUS_MAX 64u -static StCelDefT gCensus[CENSUS_MAX]; -static uint8_t gCensusCount; - - -static void celCensusTick(const StSimT *sim) { - uint8_t i; - - for (i = 1u; i < ST_HW_SPRITES; i++) { - StCelDefT d; - uint8_t k; - - if ((sim->frame.enableMask & kStBit[i]) == 0u || sim->frame.ptr[i] >= ST_SPRITE_PTR_FIRST) { - continue; - } - // A pointer with no bitmap (a hook enables a sprite a tick before - // aiming it) draws nothing on every port -- not a cel. - if (stSimSpriteBitmap(sim, sim->frame.ptr[i]) == 0) { - continue; - } - d.ptr = sim->frame.ptr[i]; - d.color = sim->frame.color[i]; - d.multi = (sim->frame.multiMask & kStBit[i]) != 0u ? 1u : 0u; - d.mc0 = d.multi ? sim->spriteMc0 : 0u; - d.mc1 = d.multi ? sim->spriteMc1 : 0u; - for (k = 0u; k < gCensusCount; k++) { - if (memcmp(&gCensus[k], &d, sizeof(d)) == 0) { - break; - } - } - if (k == gCensusCount && gCensusCount < CENSUS_MAX) { - gCensus[gCensusCount++] = d; - } - } -} - - -static void celCensusReport(const StLevelT *level) { - StCelDefT listed[ST_LEVEL_CELS_MAX]; - uint8_t n = stLevelCelList(level, listed, ST_LEVEL_CELS_MAX); - uint8_t k; - uint8_t j; - - for (k = 0u; k < gCensusCount; k++) { - for (j = 0u; j < n; j++) { - if (memcmp(&listed[j], &gCensus[k], sizeof(StCelDefT)) == 0) { - break; - } - } - fprintf(stderr, "CEL %s ptr=%02X color=%u multi=%u mc=%u/%u\n", (j < n) ? "listed " : "MISSING", gCensus[k].ptr, gCensus[k].color, gCensus[k].multi, gCensus[k].mc0, gCensus[k].mc1); - } - fprintf(stderr, "CENSUS shown=%u listed=%u\n", gCensusCount, n); -} - - -static void celCensusReport(const StLevelT *level); -static void celCensusTick(const StSimT *sim); - - -static void printState(const StSimT *sim, uint32_t tick) { - uint8_t k; - - printf("{\"tick\":%u,\"x\":\"%02X%02X.%02X\",\"y\":\"%02X.%02X\",\"vx\":\"%04X\",\"vy\":\"%04X\"," - "\"pad\":%u,\"st\":%u,\"ph\":%u,\"in\":\"%02X\",\"dir\":\"%02X\",\"ptr0\":\"%02X\",\"ptr1\":\"%02X\"," - "\"s1\":\"%02X%02X.%02X\",\"en\":\"%02X\",\"t4740\":\"%02X\",\"off\":%u,\"rng\":\"%02X%02X\"," - "\"fuel\":%u,\"slots\":%u,\"asi\":%u,\"score\":\"", - tick, sim->posXmsb, sim->posXcol, sim->posXlo, sim->posYrow, sim->posYlo, - (uint16_t)sim->velX, (uint16_t)sim->velY, sim->activePad, sim->stage, sim->collisionPhase, - sim->inputMask, sim->dirMask, sim->spr[0].ptr, sim->spr[1].ptr, - sim->spr[1].msb, sim->spr[1].col, sim->spr[1].row, sim->frame.enableMask, - sim->demoTimer, sim->demoOff, sim->rngT1, sim->rngT2, sim->fuelCells, sim->fareSlotCount, sim->activeSpriteIdx); - for (k = 0u; k < ST_NUMBER_CHARS; k++) { - printf("%02X", sim->screen[ST_CELL_SCORE + k]); - } - printf("\",\"fare\":\""); - for (k = 0u; k < ST_NUMBER_CHARS; k++) { - printf("%02X", sim->screen[ST_CELL_FARE + k]); - } - printf("\",\"fuelc\":\""); - for (k = 0u; k < ST_FUEL_CELLS; k++) { - printf("%02X", sim->screen[ST_CELL_FUEL + k]); - } - printf("\",\"sprx\":\""); - for (k = 0u; k < ST_HW_SPRITES; k++) { - printf("%03X%02X", sim->frame.x[k], sim->frame.y[k]); - } - printf("\",\"scr\":\"%08X\",\"col\":\"%08X\",\"chr\":\"%08X\",\"bd\":\"%02X\",\"bg\":\"%02X\"}\n", - hashBytes(sim->screen, ST_SCREEN_CELLS), - hashBytes(sim->color, ST_SCREEN_CELLS), - charsetHash(sim), - sim->borderColor, sim->bgColor); -} - - -int main(int argc, char **argv) { - FILE *fp; - const uint8_t *stream = kDemoStreamH; - uint16_t len = (uint16_t)sizeof(kDemoStreamH); - uint8_t want; - uint32_t ticks; - uint32_t t; - - if (argc < 4) { - fprintf(stderr, "usage: stsim <level.dat> <level letter> <ticks>\n"); - return 2; - } - fp = fopen(argv[1], "rb"); - if (fp == NULL || !stLevelParse(&gLevel, fp)) { - fprintf(stderr, "stsim: cannot load %s\n", argv[1]); - return 1; - } - fclose(fp); - want = (uint8_t)argv[2][0]; - switch (want) { - case 'W': stream = kDemoStreamW; len = (uint16_t)sizeof(kDemoStreamW); break; - case 'T': stream = kDemoStreamT; len = (uint16_t)sizeof(kDemoStreamT); break; - case 'X': stream = kDemoStreamX; len = (uint16_t)sizeof(kDemoStreamX); break; - case 'H': break; - default: { - // A synthesised gate fixture for a level the C64 has no - // attract recording of (see gateStreams.h). - uint8_t g; - for (g = 0u; g < ST_GATE_STREAMS; g++) { - if (kGateStreams[g].level == want) { - stream = kGateStreams[g].stream; - len = kGateStreams[g].length; - break; - } - } - if (g == ST_GATE_STREAMS) { - fprintf(stderr, "stsim: no stream for '%c'\n", (char)want); - return 2; - } - break; - } - } - ticks = (uint32_t)strtoul(argv[3], NULL, 0); - stSimNewGame(&gSim, 1u, true); - stSimSeedDemoBuffer(&gSim, kDemoBufferInit, ST_DEMO_BUFFER_BYTES); - if (argc > 4) { - // carried-over state: flame parity, wave index, walk parity - gSim.flameParity = (uint8_t)strtoul(argv[4], NULL, 16); - } - if (argc > 5) { - gSim.waveIdx = (uint8_t)strtoul(argv[5], NULL, 16); - } - if (argc > 6) { - gSim.walkParity = (uint8_t)strtoul(argv[6], NULL, 16); - } - stSimEnterLevel(&gSim, &gLevel); - stSimSetDemoStream(&gSim, stream, len); - for (t = 0u; t < ticks; t++) { - StTickResultE r; - printState(&gSim, t); - r = stSimTick(&gSim); - if (getenv("STSIM_CELS") != NULL) { - celCensusTick(&gSim); - } - if (r != ST_TICK_CONTINUE) { - printf("{\"tick\":%u,\"end\":%d}\n", t, (int)r); - break; - } - } - if (getenv("STSIM_CELS") != NULL) { - celCensusReport(&gLevel); - } - return 0; -}