Split Space Taxi and AGI into their own repositories.

This commit is contained in:
Scott Duensing 2026-10-05 18:49:14 -05:00
parent e0898cc576
commit a334d2dd13
133 changed files with 1647 additions and 33336 deletions

19
.gitignore vendored
View file

@ -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

View file

@ -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`.
---

View file

@ -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_<PORT>,
# 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

View file

@ -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/<name>/`,
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/<port>.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/<plat>/ 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 <target>` produces baked blobs under
`examples/<game>/generated/<target>/` and stages them into the
runtime tree at `build/<target>/.../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 <target>`
produces the baked blobs and stages them into the
runtime tree at `build/<target>/.../DATA/` -- on the IIgs and DVX into
`build/<target>/data/<name>/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

View file

@ -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-<port>-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

View file

@ -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/
games/
joeylib/ a JoeyLib checkout, set up with ./install.sh
mygame/
Makefile
mygame.c
mygame.h
world.c
assets/
tiles.png
sprites.png
music.song
----
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/<target>.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_<TARGET>_<VAR>`, 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
<<Running What You Built>>), on every machine:
<<Running What You Built>>) 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-<port>-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

View file

@ -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>>).

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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 <stdio.h>
// AGI resource directories are sized by the on-disk file length. The
// maximum index any single game references is well under 256 for the
// supported game set; oversize directories are rejected at load.
#define AGI_MAX_RESOURCES 256
// AGI VOL.x files are numbered 0..15 (4-bit field in the directory
// entry). One open FILE * per volume kept resident for the life of
// the game session.
#define AGI_MAX_VOLUMES 16
// AGI's native rendering surface. Visual and priority screens are
// each 160 wide; horizontal pixel doubling produces the 320-wide
// display. The vertical 168 leaves room above and below for the
// status line and command prompt in a 200-line display.
#define AGI_PIC_WIDTH 160
#define AGI_PIC_HEIGHT 168
#define AGI_PIC_PIXELS (AGI_PIC_WIDTH * AGI_PIC_HEIGHT)
// Flood-fill spread tests: a fill spreads through pixels matching the
// background color and stops at anything else. Visual default bg is
// 15 (white), priority default bg is 4 (red).
#define AGI_VISUAL_BG 15u
#define AGI_PRIORITY_BG 4u
// AGI 16-color CGA-derived palette in JoeyLib's $0RGB format (4-bit
// per channel). Entry 6 keeps the classic CGA "brown" tweak (R=A,
// G=5, B=0) so the muddy-yellow-on-CGA-monitors look matches the
// original render rather than reading as pure yellow-green.
extern const uint16_t kAgiPalette[16];
// Resource directory entry: which VOL.x file holds the resource and
// where in that file it starts. A 0xFF marker in volume means the
// directory slot is empty (game does not define this resource ID).
typedef struct {
uint8_t volume;
uint32_t offset;
} AgiResEntryT;
typedef enum {
AGI_RES_LOGIC = 0,
AGI_RES_PIC = 1,
AGI_RES_VIEW = 2,
AGI_RES_SOUND = 3,
AGI_RES_COUNT = 4
} AgiResTypeE;
// Open-game state: directory metadata + handles to every VOL.x file.
// Loading and freeing individual resource blobs is per-call so the
// working-set footprint stays small enough for stock IIgs / Amiga.
typedef struct {
FILE *volFiles[AGI_MAX_VOLUMES];
uint16_t resCount[AGI_RES_COUNT];
AgiResEntryT resDir[AGI_RES_COUNT][AGI_MAX_RESOURCES];
// 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

View file

@ -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 <stddef.h>
#include <string.h>
// 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;
}
}

View file

@ -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 <stddef.h>
#include <stdlib.h>
#include <string.h>
// PIC opcodes. Anything < 0xF0 is an argument byte; >= 0xF0 ends the
// current opcode's argument list.
#define OP_FIRST 0xF0u
#define OP_SET_PIC_COLOR 0xF0u
#define OP_DISABLE_PIC 0xF1u
#define OP_SET_PRI_COLOR 0xF2u
#define OP_DISABLE_PRI 0xF3u
#define OP_Y_CORNER 0xF4u
#define OP_X_CORNER 0xF5u
#define OP_ABS_LINE 0xF6u
#define OP_REL_LINE 0xF7u
#define OP_FILL 0xF8u
#define OP_SET_PEN 0xF9u
#define OP_PLOT_PEN 0xFAu
#define OP_END 0xFFu
// Pen control byte bits (set via OP_SET_PEN, consumed by OP_PLOT_PEN).
#define PEN_PATTERN_BIT 0x20u
// Scanline-fill seed stack. Each fill is reentrant-free, so a single
// static stack serves both the visual and priority passes. The bound
// is a safe over-estimate of typical PIC fill complexity; KQ3 PICs
// don't approach it.
#define FILL_STACK_MAX 512
typedef struct {
uint8_t visualEnabled;
uint8_t priorityEnabled;
uint8_t visualColor;
uint8_t priorityColor;
uint8_t penPattern;
} PicStateT;
// AGI 16-color CGA-derived palette. Entry 6's (R=A, G=5, B=0) keeps the
// classic CGA "brown fix" -- pure (R=A, G=A, B=0) would read as a vivid
// yellow-green on the host, which is wrong for AGI's intended look.
const uint16_t kAgiPalette[16] = {
0x000u, 0x00Au, 0x0A0u, 0x0AAu,
0xA00u, 0xA0Au, 0xA50u, 0xAAAu,
0x555u, 0x55Fu, 0x5F5u, 0x5FFu,
0xF55u, 0xF5Fu, 0xFF5u, 0xFFFu
};
// ----- Prototypes -----
static void drawCorner(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos, bool yFirst);
static void drawLine(AgiPicT *pic, const PicStateT *state, int16_t x0, int16_t y0, int16_t x1, int16_t y1);
static void fillPlane(uint8_t *plane, int16_t x, int16_t y, uint8_t bgValue, uint8_t newValue);
static bool isOpcode(uint8_t byte);
static void opAbsLine(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos);
static void opFill(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos);
static void opPlotPen(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos);
static void opRelLine(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos);
static void plot(AgiPicT *pic, const PicStateT *state, int16_t x, int16_t y);
// ----- 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;
}
}

View file

@ -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 <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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.<n>". 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;
}

View file

@ -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 <stddef.h>
#include <string.h>
#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++;
}
}
}

View file

@ -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 <stddef.h>
#include <stdlib.h>
#include <string.h>
// VIEW header byte offsets (publicly documented v2 layout).
#define AGI_VIEW_HDR_LOOP_COUNT 2u
#define AGI_VIEW_HDR_LOOP_TABLE 5u
// Cel header bits (publicly documented v2 layout).
#define AGI_CEL_TRANSPARENT_MASK 0x0Fu
#define AGI_CEL_MIRROR_FLAG 0x80u
#define AGI_CEL_MIRROR_SRC_SHIFT 4u
#define AGI_CEL_MIRROR_SRC_MASK 0x07u
// Priority bands. Y 0..47 is sky (priority 4); Y 48..167 splits into
// 10 ground bands of 12 pixels each at priorities 5..14. Priority 15
// is "always on top" and reserved for HUD/overlay; an actor's natural
// Y never reaches it.
#define AGI_PRI_SKY_TOP 48
#define AGI_PRI_GROUND_FIRST 5u
#define AGI_PRI_GROUND_BAND_PX 12
// ----- 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;
}

File diff suppressed because it is too large Load diff

View file

@ -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/<plat>.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,29 +200,102 @@ 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);
}
@ -123,6 +304,10 @@ int main(void) {
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;
@ -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);
@ -162,27 +348,42 @@ int main(void) {
initialPaint(screen, audioOk);
musicPaused = false;
noiseOn = false;
noiseResumes = false;
noiseFrame = 0;
flashFrames = 0;
for (;;) {
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();

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load diff

View file

@ -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
<dump> <out> <start> <end>`, study the annotated listing.
3. **Document** -- append behavior summary to `MECHANICS.md` in
plain English. Add new labels/vars to `stuff/spacetaxi/labels.txt`.
4. **Reannotate** -- `python3 disLive.py mem0000-level1.bin
live-level1.lst` to regenerate the (annotated) listing from
the dump + labels.txt in one step. The .lst file is the only
readable form -- labels are baked in directly.
5. **Port** -- update the matching JoeyLib C file
(`stEngine.c`, `stPassenger.c`, `stRender.c`, `stAudio.c`,
`stLevel.c`, `spacetaxi.c`).
6. **Build + ship** -- `make -f make/dos.mk EXAMPLE=spacetaxi`
first, then once stable also amiga / atarist / iigs.
7. **User test** -- run `./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.

View file

@ -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.

BIN
examples/spacetaxi/assets/RawTitle.png (Stored with Git LFS)

Binary file not shown.

BIN
examples/spacetaxi/assets/TitleShr.png (Stored with Git LFS)

Binary file not shown.

View file

@ -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,
}

View file

@ -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()

View file

@ -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 <dump-path> [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()

View file

@ -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 <dump.bin> [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("<h", 12000 if level else -12000) * n
w = wave.open(path, "wb")
w.setnchannels(1)
w.setsampwidth(2)
w.setframerate(OUT_RATE)
w.writeframes(bytes(samples))
w.close()
return len(samples) // 2 / float(OUT_RATE)
def emitC(mem, table, romLo, outDir):
"""Write stSpeechData.c/.h: the raw run-length streams plus an index.
The streams ship as-is (about 7.4 KB for all 15) and the game expands
them to PCM on the fly -- rendering them at bake time would be ~38 KB
at 5 kHz, which will not fit an IIgs bank alongside everything else.
"""
blobs = []
index = []
for code, ptr in table:
if ptr >= 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 <stdint.h>\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()

View file

@ -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 <stdint.h>")
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()

View file

@ -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 <stdint.h>\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()

View file

@ -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 <stdint.h>
"""
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))

View file

@ -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("<BBBBBBBBI", 3, PITCH_BASE, PITCH_COUNT, len(entries), 1 if biased else 0, polarities, 0, 0, len(blob)))
for code, parity, n, off in records:
f.write(struct.pack("<BBHI", code, parity, n, off))
f.write(blob)
total = sum(n for _, _, n, _ in records)
print(f"{dst}: {len(entries)} utterances x {PITCH_COUNT} pitches x {polarities} "
f"polarit{'y' if polarities == 1 else 'ies'}, {total} samples, {len(blob)} data bytes, "
f"{'biased' if biased else 'signed'}")
if __name__ == "__main__":
main()

View file

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

View file

@ -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

View file

@ -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 <raw-dump> <out-dir> # all 24 levels
# romToLevel.py <raw-dump> <out-dir> <scene_id> # one specific
#
# Writes:
# <out-dir>/level01.dat ... level24.dat (scene 0 = "A" = level01)
# <out-dir>/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<NN>.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 <raw-dump> <out-dir> [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()

View file

@ -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"))

View file

@ -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 ""))

View file

@ -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 --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---

View file

@ -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 --- --- ---
--- --- --- ---

View file

@ -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 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---

View file

@ -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 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---

View file

@ -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 --- ---
--- --- --- ---

View file

@ -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 ---
--- --- --- ---
--- --- --- ---

View file

@ -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 --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---

View file

@ -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 ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---
--- --- --- ---

File diff suppressed because it is too large Load diff

View file

@ -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 <up-to-23-char level name>
// @tilebank <0..255>
// @music <0..255>
// @bgColor <palette slot 0..15>
// @borderColor <palette slot 0..15>
// @taxiSpawn <tileX> <tileY>
// @accel <xAccel> <yAccel> [optional, default 14 14]
// @gravity <xGrav> <yGrav> [optional, default 0 1; yGrav signed]
// @vicColor <bg1> <bg2> <bg3> <mc0> <mc1> <sc0> <sc1> [optional, default all 0]
// @pad <letter> <tileX> <tileY> <tileW> [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 <ctype.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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 <joey/joey.h>
#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

View file

@ -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 <string.h>
#include "spacetaxi.h"
#include <joey/file.h>
#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;
}

File diff suppressed because it is too large Load diff

View file

@ -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 <stdint.h>
// 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

View file

@ -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));
}
}
}
}
}
}

View file

@ -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 <stdint.h>
#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

View file

@ -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 <stdint.h>
// 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

View file

@ -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);
}

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -1,21 +0,0 @@
// Space Taxi -- level loader (JoeyLib data path + the shared STL4 parser).
#include <stdio.h>
#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;
}

View file

@ -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;
}

View file

@ -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 <stdio.h>
#include <string.h>
#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;
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -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 <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#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

View file

@ -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 <stdint.h>
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

View file

@ -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,
};

View file

@ -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 <stdint.h>
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

View file

@ -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 <string.h>
#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;
}

View file

@ -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

View file

@ -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() {
:
}

View file

@ -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)

View file

@ -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/<vendor>/<name>/: 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")"

View file

@ -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

View file

@ -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

View file

@ -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/<machine>/ (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_<PORT>_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/<port>/data/<name>/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:

View file

@ -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/<port> -- 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

View file

@ -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/<name>/<name>.app, <name>.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>/<name>/.
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/<vendor>/<name>/
# under bin/: <name>.app, <name>.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/<name>/DATA, for the SDK to package.
# DVX_VENDOR the vendor folder (default joeylib)
# DVX_KIND what the help and resource texts call a program: "<title> 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

164
make/examples.mk Normal file
View file

@ -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 $@)

View file

@ -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

67
make/programs.mk Normal file
View file

@ -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

97
make/project.mk Normal file
View file

@ -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

View file

@ -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.

View file

@ -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 $@

View file

@ -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

View file

@ -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

View file

@ -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

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