Space Taxi is finally starting to look viable on the GS!

This commit is contained in:
Scott Duensing 2026-09-22 18:12:37 -05:00
parent 2acfce011d
commit 477d39f7e9
98 changed files with 13506 additions and 1340 deletions

6
.gitignore vendored
View file

@ -64,3 +64,9 @@ __pycache__/
# AGI game data (Sierra/fan-made AGI games used for testing; never committed) # AGI game data (Sierra/fan-made AGI games used for testing; never committed)
examples/agi/gamedata/ 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

BIN
examples/spacetaxi/21_!.wav Normal file

Binary file not shown.

BIN
examples/spacetaxi/31_1.wav Normal file

Binary file not shown.

BIN
examples/spacetaxi/32_2.wav Normal file

Binary file not shown.

BIN
examples/spacetaxi/33_3.wav Normal file

Binary file not shown.

BIN
examples/spacetaxi/34_4.wav Normal file

Binary file not shown.

BIN
examples/spacetaxi/35_5.wav Normal file

Binary file not shown.

BIN
examples/spacetaxi/36_6.wav Normal file

Binary file not shown.

BIN
examples/spacetaxi/37_7.wav Normal file

Binary file not shown.

BIN
examples/spacetaxi/38_8.wav Normal file

Binary file not shown.

BIN
examples/spacetaxi/39_9.wav Normal file

Binary file not shown.

BIN
examples/spacetaxi/3F_x.wav Normal file

Binary file not shown.

BIN
examples/spacetaxi/48_H.wav Normal file

Binary file not shown.

BIN
examples/spacetaxi/50_P.wav Normal file

Binary file not shown.

BIN
examples/spacetaxi/54_T.wav Normal file

Binary file not shown.

BIN
examples/spacetaxi/55_U.wav Normal file

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

View file

@ -0,0 +1,81 @@
#!/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: stuff/spacetaxi/cpu6502.py 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, STUFF)
try:
from cpu6502 import Cpu6502
except ImportError:
sys.exit("depackSpeechRam: need cpu6502.py in %s" % STUFF)
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

@ -0,0 +1,225 @@
#!/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

@ -55,6 +55,21 @@ def c_bytes(data, per_line=16, indent=" "):
return "\n".join(lines) 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(): def main():
raw = load_raw() raw = load_raw()
sprite_count = SPRITE_PTR_LAST - SPRITE_PTR_FIRST + 1 sprite_count = SPRITE_PTR_LAST - SPRITE_PTR_FIRST + 1
@ -100,9 +115,11 @@ def main():
h.append("// 8=RIGHT); 0 = no cel for that combination.") h.append("// 8=RIGHT); 0 = no cel for that combination.")
h.append("extern const uint8_t kStFlameCelByDirMask[16];") h.append("extern const uint8_t kStFlameCelByDirMask[16];")
h.append("") h.append("")
h.append("// Level-intro star velocities per sprite 0..6 ($450C / $4513).") h.append("// Cross-segment-safe accessors (see stC64Data.c): reach the tables above")
h.append("extern const int8_t kStIntroStarDx[7];") h.append("// by an inter-segment call on the IIgs, where a direct 2-byte data")
h.append("extern const int8_t kStIntroStarDy[7];") h.append("// reference from another bank is not relocatable.")
for decl in ACCESSORS:
h.append(decl[0])
h.append("") h.append("")
h.append("#endif") h.append("#endif")
@ -143,11 +160,25 @@ def main():
c.append(c_bytes(flame)) c.append(c_bytes(flame))
c.append("};") c.append("};")
c.append("") c.append("")
dx = [b - 256 if b >= 128 else b for b in raw[0x450C:0x450C + 7]]
dy = [b - 256 if b >= 128 else b for b in raw[0x4513:0x4513 + 7]]
c.append("const int8_t kStIntroStarDx[7] = { " + ", ".join(str(v) for v in dx) + " };")
c.append("const int8_t kStIntroStarDy[7] = { " + ", ".join(str(v) for v in dy) + " };")
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: with open(OUT_H, "w", encoding="ascii") as fp:
fp.write("\n".join(h) + "\n") fp.write("\n".join(h) + "\n")

View file

@ -0,0 +1,76 @@
#!/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

@ -0,0 +1,106 @@
#!/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(2) u8 pitchBase u8 pitchCount u8 count
u8 format (0 = signed +/-100, 1 = 0x80-biased for the IIgs raw stream)
u8 pad[3] u32 dataBytes
entries[pitchCount * count * 2]: 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
usage: genSpeechPcm.py stSpeechData.c speech.bin [--biased]
"""
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:]
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(2):
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", 2, PITCH_BASE, PITCH_COUNT, len(entries), 1 if biased else 0, 0, 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 2 polarities, {total} samples, {len(blob)} data bytes, {'biased' if biased else 'signed'}")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,57 @@
#!/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, sys
sys.path.insert(0, "/home/scott/claude/joeylib/stuff/spacetaxi")
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

@ -0,0 +1,111 @@
#!/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

@ -0,0 +1,94 @@
; 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

@ -0,0 +1,94 @@
; 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

@ -0,0 +1,86 @@
; 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

@ -0,0 +1,86 @@
; 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

@ -0,0 +1,126 @@
; 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

@ -0,0 +1,736 @@
; 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

@ -0,0 +1,840 @@
; 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

@ -0,0 +1,852 @@
; 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

@ -19,27 +19,20 @@
#include <string.h> #include <string.h>
#include "spacetaxi.h" #include <joey/file.h>
#include "stDemoStreams.h"
// Diagnostics. The logger drags in vsnprintf (with newlib that pulls the #include "spacetaxi.h"
// whole formatted-output and float-conversion family, ~45 KB) plus a #include "stSongData.h"
// multi-KB ring buffer, and the game has no need of either. That is #include "stDemoStreams.h"
// 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)
#else
#define ST_LOG_RESET() jlLogReset()
#define ST_LOG(...) jlLogF(__VA_ARGS__)
#endif
#define ST_TICK_HZ 30u #define ST_TICK_HZ 30u
#define ST_MAX_CATCHUP_TICKS 4u #define ST_MAX_CATCHUP_TICKS 4u
#define ST_LEVEL_COUNT 24u // Screens A..X, then screen Y -- the 25th, " MYSTERY SCREEN !!! " -- which
// $5167 loads once the 24-hour shifts have cleared all of A..X. Files are
// levels/level01.dat .. level25.dat, so Y is index 24.
#define ST_LEVEL_COUNT 24u // A..X, the screens a shift sweeps
#define ST_SCREEN_Y 24u // the 25th screen ($5171 LDA #$59)
#define ST_LEVEL_FILES 25u // level01.dat .. level25.dat
// "GET READY" shows over the old screen while the C64 cycles the // "GET READY" shows over the old screen while the C64 cycles the
// leaving-passenger sprites (three busy waits, about half a second). // leaving-passenger sprites (three busy waits, about half a second).
#define ST_GET_READY_TICKS 15u #define ST_GET_READY_TICKS 15u
@ -48,6 +41,18 @@
// Menu joystick repeat delay ($4101 with X = $82). // Menu joystick repeat delay ($4101 with X = $82).
#define ST_MENU_REPEAT_TICKS 8u #define ST_MENU_REPEAT_TICKS 8u
// The immortal cabbies ($4A89 image, $4E35 screen).
#define ST_HS_ENTRIES 10u
#define ST_HS_NAME_CHARS 15u
#define ST_HS_ENTRY_BYTES 24u // score[7] 0 name[15] shift
#define ST_HS_POINT_AT 4u // "dddd.dd"
#define ST_HS_GREET_DIGIT_AT 23u // "CONGRATULATIONS CABBIE n,"
#define ST_HS_NAME_COL 12u // $0664: row 15, column 12
#define ST_HS_NAME_ROW 15u
#define ST_HS_CURSOR '%' // $4F4D flips it against blank
#define ST_HS_BLINK_TICKS 3u // $4101 x $1E between flips, ~55 ms
#define ST_HS_SAVE_NAME "IMMORTAL"
typedef enum { typedef enum {
ST_STATE_TITLE = 0, ST_STATE_TITLE = 0,
ST_STATE_HIGHSCORES, ST_STATE_HIGHSCORES,
@ -57,7 +62,9 @@ typedef enum {
ST_STATE_GET_READY, ST_STATE_GET_READY,
ST_STATE_INTRO, ST_STATE_INTRO,
ST_STATE_PLAYING, ST_STATE_PLAYING,
ST_STATE_GAME_OVER ST_STATE_GAME_OVER,
ST_STATE_JINGLE, // a $44CF tune sounding; sim frozen until it ends
ST_STATE_NAME_ENTRY // $4E35: a top-ten cabbie types a name
} StStateE; } StStateE;
typedef struct { typedef struct {
@ -73,8 +80,39 @@ typedef struct {
bool fireArmed; // $445D: FIRE must be released first bool fireArmed; // $445D: FIRE must be released first
uint8_t lastFrame; uint8_t lastFrame;
uint16_t paceAcc; uint16_t paceAcc;
StStateE afterJingle; // where ST_STATE_JINGLE returns to
bool gameOverTune; // $5C24: song 5 started after the wait
uint8_t hsSlot; // $4D51 table slot being named
uint8_t hsLen; // $4D2C characters typed
uint8_t hsCursor; // $4F4D the blinking cursor's current glyph
uint8_t hsBlinkTicks; // ticks toward the next cursor flip
bool paused; // $4FCB: SHIFT is holding the game
} StGameT; } StGameT;
// The immortal cabbies ($4A89): ten entries of a right-aligned "dddd.dd"
// score, a name of up to fifteen letters and the shift it was earned on.
// The C64 keeps this table on its own disk (the $9948 IEC routines load
// it at boot and save it after every entry); the port keeps the same
// 240-byte image in a save file. Shipped contents as the dump has them.
typedef struct {
uint8_t score[ST_NUMBER_CHARS + 1u]; // ASCII, NUL-terminated
uint8_t name[ST_HS_NAME_CHARS + 1u]; // ASCII, space-padded, NUL-terminated
uint8_t shift; // 1..5
} StHighScoreT;
static StHighScoreT gHighScores[ST_HS_ENTRIES] = {
{ "3877.56", "MICHAEL PLATE ", 4u },
{ "1809.51", "MICHAEL PLATE ", 2u },
{ "1179.87", "ANDREAS PLATE ", 4u },
{ " 892.65", "MICHAEL PLATE ", 1u },
{ " 685.37", "ANDREAS PLATE ", 1u },
{ " 629.45", "MICHAEL PLATE ", 4u },
{ " 569.50", "MICHAEL PLATE ", 1u },
{ " 504.97", "ANDREAS PLATE ", 2u },
{ " 498.77", "ANDREAS PLATE ", 5u },
{ " 490.85", "ANDREAS PLATE ", 2u },
};
// Menu texts ($5347..$545D) in screen codes. // Menu texts ($5347..$545D) in screen codes.
static const uint8_t kTextGameVariations[] = "GAME VARIATIONS"; static const uint8_t kTextGameVariations[] = "GAME VARIATIONS";
static const uint8_t kTextCabbies[] = "SELECT NUMBER OF CABBIES:"; static const uint8_t kTextCabbies[] = "SELECT NUMBER OF CABBIES:";
@ -94,18 +132,14 @@ static const uint8_t kTextGetReady[] = " GET READY FOR A RIDE ";
static const uint8_t kTextOnTheTaxi[] = " ON THE SPACE TAXI! "; static const uint8_t kTextOnTheTaxi[] = " ON THE SPACE TAXI! ";
static const uint8_t kTextGameOver[] = " GAME OVER! "; static const uint8_t kTextGameOver[] = " GAME OVER! ";
static const uint8_t kTextBlank12[] = " "; static const uint8_t kTextBlank12[] = " ";
// $4C1F / $556A screens. // $4C1F (the table) and $4E35 (the name entry) screens.
static const uint8_t kTextImmortal[] = "THE IMMORTAL CABBIES"; static const uint8_t kTextImmortal[] = "THE IMMORTAL CABBIES";
static const uint8_t *kHighScores[8] = { static const uint8_t kTextShift[] = "SHIFT";
(const uint8_t *)"3877.56 MICHAEL PLATE", static const uint8_t kTextReturnTitle[] = "PRESS FIRE TO RETURN TO TITLE PAGE";
(const uint8_t *)"1809.51 MICHAEL PLATE", static const uint8_t kTextCongrats[] = "CONGRATULATIONS CABBIE ,";
(const uint8_t *)"1179.87 ANDREAS PLATE", static const uint8_t kTextRanks[] = "YOUR SCORE RANKS YOU AS ONE OF";
(const uint8_t *)" 892.65 MICHAEL PLATE", static const uint8_t kTextTopTen[] = "THE TOP TEN CABBIES OF ALL TIME";
(const uint8_t *)" 685.37 ANDREAS PLATE", static const uint8_t kTextEnterName[] = "ENTER YOUR NAME BELOW";
(const uint8_t *)" 629.45 MICHAEL PLATE",
(const uint8_t *)" 569.50 MICHAEL PLATE",
(const uint8_t *)" 504.97 ANDREAS PLATE",
};
static const uint8_t *kAboutLines[7] = { static const uint8_t *kAboutLines[7] = {
(const uint8_t *)"THIS PROGRAM DEVELOPED AND WRITTEN BY", (const uint8_t *)"THIS PROGRAM DEVELOPED AND WRITTEN BY",
(const uint8_t *)"JOHN F. KUTCHER", (const uint8_t *)"JOHN F. KUTCHER",
@ -119,6 +153,7 @@ static const uint8_t kTextReturn[] = "PRESS FIRE TO RETURN";
static StGameT gGame; static StGameT gGame;
static StSimT gSim; static StSimT gSim;
static jlSurfaceT *gStage;
// One level buffer serves both gameplay and the title screen: the title // One level buffer serves both gameplay and the title screen: the title
// is another scene, and gameplay never needs it at the same time, so it // is another scene, and gameplay never needs it at the same time, so it
// is loaded into gLevel on each return to the title. (Keeping a second // is loaded into gLevel on each return to the title. (Keeping a second
@ -126,17 +161,25 @@ static StSimT gSim;
static StLevelT gLevel; static StLevelT gLevel;
static void blankScreen(StSimT *sim); static void blankScreen(StSimT *sim, uint8_t border, uint8_t bg);
static void enterTitle(void); static void enterTitle(void);
static void gameOverEnter(void); static void gameOverEnter(void);
static void gameOverFinish(void);
static void highScoreDraw(void);
static uint8_t highScoreInsert(const uint8_t *score);
static void highScoreLoad(void);
static void highScoreSave(void);
static bool joyFire(void); static bool joyFire(void);
static uint8_t joyMask(void); static uint8_t joyMask(void);
static bool loadLevel(uint8_t index); static bool loadLevel(uint8_t index);
static uint8_t nextLevelForShift(void); static uint8_t nextLevelForShift(void);
static uint8_t paceTicks(void); static uint8_t paceTicks(void);
static bool serviceJingle(void);
static void runCabbiesMenu(uint8_t ticks); static void runCabbiesMenu(uint8_t ticks);
static void runDemoEnd(void); static void runDemoEnd(void);
static void runIntro(uint8_t ticks); static void runIntro(uint8_t ticks);
static void runNameEntry(uint8_t ticks);
static bool nameEntryStart(void);
static void runPlaying(uint8_t ticks); static void runPlaying(uint8_t ticks);
static void runShiftMenu(uint8_t ticks); static void runShiftMenu(uint8_t ticks);
static void runTitle(uint8_t ticks); static void runTitle(uint8_t ticks);
@ -149,12 +192,12 @@ static void startNewScreen(void);
// $40CA -- spaces everywhere, black. // $40CA -- spaces everywhere, black.
static void blankScreen(StSimT *sim) { static void blankScreen(StSimT *sim, uint8_t border, uint8_t bg) {
memset(sim->screen, ST_CHAR_SPACE, ST_SCREEN_CELLS); memset(sim->screen, ST_CHAR_SPACE, ST_SCREEN_CELLS);
memset(sim->color, 0, ST_SCREEN_CELLS); memset(sim->color, 0, ST_SCREEN_CELLS);
stSimDirtyAll(sim); stSimDirtyAll(sim);
sim->bgColor = 0u; sim->bgColor = bg;
sim->borderColor = 0u; sim->borderColor = border;
sim->frame.enableMask = 0u; sim->frame.enableMask = 0u;
stRenderSceneChanged(sim); stRenderSceneChanged(sim);
} }
@ -166,16 +209,239 @@ static void enterTitle(void) {
if (!stLevelLoad(&gLevel, "levels/title.dat")) { if (!stLevelLoad(&gLevel, "levels/title.dat")) {
ST_LOG("spacetaxi: ! title load FAILED"); ST_LOG("spacetaxi: ! title load FAILED");
} }
stRenderLevelCels(gStage, 0); // the last level's cels go
stTitleEnter(&gSim, &gLevel); stTitleEnter(&gSim, &gLevel);
stRenderSceneChanged(&gSim); stRenderSceneChanged(&gSim);
stRenderTitleLogo(&gSim); stRenderTitleLogo(&gSim);
stAudioNoise(false); stAudioNoise(false);
stAudioSilence(); stAudioSilence();
jlMusicStop();
gGame.state = ST_STATE_TITLE; gGame.state = ST_STATE_TITLE;
gGame.fireArmed = false; gGame.fireArmed = false;
} }
// $6BD0 -- one more player done; everybody done -> title.
static void gameOverFinish(void) {
gSim.playersDone++;
if (gSim.playersDone == gSim.playerCount) {
enterTitle();
} else {
startNewScreen();
}
}
// $4C1F -- the immortal cabbies: ten rows of shift, "$", score and name,
// each entry drawn only while its last score digit is a digit (a blank
// row is an unused slot). Song 7 plays under it until FIRE ($4D04).
static void highScoreDraw(void) {
uint8_t i;
blankScreen(&gSim, 3u, 14u);
stSimDrawText(&gSim, 9u, 1u, kTextImmortal, 6u);
stSimDrawText(&gSim, 32u, 3u, kTextShift, 15u);
for (i = 0u; i < ST_HS_ENTRIES; i++) {
const StHighScoreT *e = &gHighScores[i];
uint8_t row = (uint8_t)(4u + i * 2u);
if (e->score[ST_NUMBER_CHARS - 1u] < '0') {
continue;
}
stSimPutChar(&gSim, 34u, row, (uint8_t)('0' + e->shift));
stSimPutColor(&gSim, 34u, row, 15u);
stSimPutChar(&gSim, 4u, row, '$');
stSimPutColor(&gSim, 4u, row, 1u);
stSimDrawText(&gSim, 5u, row, e->score, 1u);
stSimDrawText(&gSim, 15u, row, e->name, 1u);
}
stSimDrawText(&gSim, 3u, 24u, kTextReturnTitle, 0u);
stAudioMusic(ST_SONG_SCORES, false); // $4C29
}
// $4D92 -- where a score belongs in the table, or ST_HS_ENTRIES when it
// does not make it. The table is only opened to scores of $50.00 and up
// (second character a digit, or third character 5 or more). The scores
// are right-aligned text, so a character compare orders them; a tie goes
// above the older entry. The new entry is opened with a blank name.
static uint8_t highScoreInsert(const uint8_t *score) {
uint8_t slot;
uint8_t k;
if (score[1] < '0' && score[2] < '5') {
return ST_HS_ENTRIES;
}
for (slot = 0u; slot < ST_HS_ENTRIES; slot++) {
const uint8_t *old = gHighScores[slot].score;
for (k = 0u; k < ST_NUMBER_CHARS; k++) {
if (score[k] != old[k]) {
break;
}
}
if (k == ST_NUMBER_CHARS || score[k] > old[k]) {
break;
}
}
if (slot == ST_HS_ENTRIES) {
return ST_HS_ENTRIES;
}
for (k = (uint8_t)(ST_HS_ENTRIES - 1u); k > slot; k--) {
gHighScores[k] = gHighScores[k - 1u];
}
memcpy(gHighScores[slot].score, score, ST_NUMBER_CHARS + 1u);
memset(gHighScores[slot].name, ' ', ST_HS_NAME_CHARS);
gHighScores[slot].name[ST_HS_NAME_CHARS] = 0u;
gHighScores[slot].shift = 0u;
return slot;
}
// $23E5 -> $9948 at boot: the table from disk, when there is one. The
// image is the C64's own: per entry score[7], 0, name[15], shift.
static void highScoreLoad(void) {
uint8_t image[ST_HS_ENTRIES * ST_HS_ENTRY_BYTES];
uint8_t i;
if (jlSaveRead(ST_HS_SAVE_NAME, image, sizeof(image)) != sizeof(image)) {
return;
}
for (i = 0u; i < ST_HS_ENTRIES; i++) {
const uint8_t *e = &image[i * ST_HS_ENTRY_BYTES];
if (e[ST_NUMBER_CHARS] != 0u) {
ST_LOG("spacetaxi: ! %s is not a score table", ST_HS_SAVE_NAME);
return;
}
}
for (i = 0u; i < ST_HS_ENTRIES; i++) {
const uint8_t *e = &image[i * ST_HS_ENTRY_BYTES];
memcpy(gHighScores[i].score, e, ST_NUMBER_CHARS);
gHighScores[i].score[ST_NUMBER_CHARS] = 0u;
memcpy(gHighScores[i].name, &e[ST_NUMBER_CHARS + 1u], ST_HS_NAME_CHARS);
gHighScores[i].name[ST_HS_NAME_CHARS] = 0u;
gHighScores[i].shift = e[ST_HS_ENTRY_BYTES - 1u];
}
}
// $4E11 -> $9948 after an entry: the table back to disk.
static void highScoreSave(void) {
uint8_t image[ST_HS_ENTRIES * ST_HS_ENTRY_BYTES];
uint8_t i;
for (i = 0u; i < ST_HS_ENTRIES; i++) {
uint8_t *e = &image[i * ST_HS_ENTRY_BYTES];
memcpy(e, gHighScores[i].score, ST_NUMBER_CHARS);
e[ST_NUMBER_CHARS] = 0u;
memcpy(&e[ST_NUMBER_CHARS + 1u], gHighScores[i].name, ST_HS_NAME_CHARS);
e[ST_HS_ENTRY_BYTES - 1u] = gHighScores[i].shift;
}
if (!jlSaveWrite(ST_HS_SAVE_NAME, image, sizeof(image))) {
ST_LOG("spacetaxi: ! could not save %s", ST_HS_SAVE_NAME);
}
}
// $4D52 -- after a player's GAME OVER: their score as "dddd.dd" text, and
// if it ranks, the $4E35 screen: red border, grey field, the cabbie's
// number in the greeting, the score at the top, song 6, and a blinking
// cursor where the name goes. Returns false when the score did not rank
// and the game moves straight on.
static bool nameEntryStart(void) {
uint8_t score[ST_NUMBER_CHARS + 1u];
uint8_t greet[sizeof(kTextCongrats)];
uint8_t k;
for (k = 0u; k < ST_NUMBER_CHARS; k++) {
uint8_t ch = gSim.scoreBackup[gSim.player][k];
if (ch == ST_CHAR_BLANK) {
score[k] = ' ';
} else {
uint8_t d = (uint8_t)(ch - ST_CHAR_DIGIT_BASE); // $4345
score[k] = (uint8_t)('0' + ((d == 10u) ? 0u : d));
}
}
score[ST_HS_POINT_AT] = '.';
score[ST_NUMBER_CHARS] = 0u;
gGame.hsSlot = highScoreInsert(score);
if (gGame.hsSlot == ST_HS_ENTRIES) {
return false;
}
blankScreen(&gSim, 2u, 15u);
while (jlInputGetChar() >= 0) { // $0277 = 0: nothing typed earlier counts
}
memcpy(greet, kTextCongrats, sizeof(greet));
greet[ST_HS_GREET_DIGIT_AT] = (uint8_t)('1' + gSim.player);
stSimDrawText(&gSim, 7u, 3u, greet, 0u);
stSimDrawText(&gSim, 5u, 5u, kTextRanks, 0u);
stSimDrawText(&gSim, 4u, 7u, kTextTopTen, 0u);
stSimDrawText(&gSim, 9u, 9u, kTextEnterName, 0u);
stSimDrawText(&gSim, 16u, 0u, score, 6u);
stSimPutChar(&gSim, 15u, 0u, '$');
stSimPutColor(&gSim, 15u, 0u, 6u);
stAudioMusic(ST_SONG_NAME_ENTRY, false); // $4EB6
gGame.hsLen = 0u;
gGame.hsCursor = ST_HS_CURSOR;
gGame.hsBlinkTicks = 0u;
gGame.state = ST_STATE_NAME_ENTRY;
return true;
}
// $4ED8 -- the entry loop: the cursor flips between "%" and blank, then
// the keyboard: letters, space, ".", "!" and "," go in (fifteen at most),
// DEL takes one back, RETURN keeps the name (a "*" when none was typed),
// stamps the shift, stops the tune and saves the table.
static void runNameEntry(uint8_t ticks) {
StHighScoreT *e = &gHighScores[gGame.hsSlot];
uint8_t col = (uint8_t)(ST_HS_NAME_COL + gGame.hsLen);
int c;
gGame.hsBlinkTicks = (uint8_t)(gGame.hsBlinkTicks + ticks);
if (gGame.hsBlinkTicks >= ST_HS_BLINK_TICKS) {
gGame.hsBlinkTicks = 0u;
gGame.hsCursor ^= ST_HS_CURSOR ^ ' ';
stSimPutChar(&gSim, col, ST_HS_NAME_ROW, gGame.hsCursor);
stSimPutColor(&gSim, col, ST_HS_NAME_ROW, 6u);
}
while ((c = jlInputGetChar()) >= 0) {
if (c >= 'a' && c <= 'z') {
c = c - 'a' + 'A';
}
if (c == JL_CHAR_RETURN) {
stSimPutChar(&gSim, col, ST_HS_NAME_ROW, ' ');
if (gGame.hsLen == 0u) {
e->name[0] = '*';
}
e->shift = gGame.variation;
jlMusicStop(); // $44E1
highScoreSave(); // $4E11
gameOverFinish();
return;
}
if (c == JL_CHAR_BACKSPACE || c == JL_CHAR_DELETE) {
if (gGame.hsLen != 0u) {
stSimPutChar(&gSim, col, ST_HS_NAME_ROW, ' ');
gGame.hsLen--;
col--;
e->name[gGame.hsLen] = ' ';
}
continue;
}
if (c != ' ' && c != '.' && c != '!' && c != ',' && (c < 'A' || c > 'Z')) {
continue;
}
if (gGame.hsLen == ST_HS_NAME_CHARS) {
continue;
}
e->name[gGame.hsLen] = (uint8_t)c;
stSimPutChar(&gSim, col, ST_HS_NAME_ROW, (uint8_t)c);
stSimPutColor(&gSim, col, ST_HS_NAME_ROW, 6u);
gGame.hsLen++;
col++;
}
}
// $5FBB -- "GAME OVER!" over the play field, sprites hidden. // $5FBB -- "GAME OVER!" over the play field, sprites hidden.
static void gameOverEnter(void) { static void gameOverEnter(void) {
stSimDrawText(&gSim, 14u, 10u, kTextBlank12, 1u); stSimDrawText(&gSim, 14u, 10u, kTextBlank12, 1u);
@ -185,6 +451,7 @@ static void gameOverEnter(void) {
stAudioNoise(false); stAudioNoise(false);
stAudioSilence(); stAudioSilence();
gGame.waitTicks = ST_GAME_OVER_TICKS; gGame.waitTicks = ST_GAME_OVER_TICKS;
gGame.gameOverTune = false;
gGame.state = ST_STATE_GAME_OVER; gGame.state = ST_STATE_GAME_OVER;
} }
@ -222,7 +489,7 @@ static uint8_t joyMask(void) {
static bool loadLevel(uint8_t index) { static bool loadLevel(uint8_t index) {
char path[32]; char path[32];
if (index >= ST_LEVEL_COUNT) { if (index >= ST_LEVEL_FILES) {
return false; return false;
} }
// Hand-rolled "levels/levelNN.dat" (NN = index+1, 01..24) so the // Hand-rolled "levels/levelNN.dat" (NN = index+1, 01..24) so the
@ -238,6 +505,7 @@ static bool loadLevel(uint8_t index) {
ST_LOG("spacetaxi: ! cannot load %s", path); ST_LOG("spacetaxi: ! cannot load %s", path);
return false; return false;
} }
stRenderLevelCels(gStage, &gLevel);
return true; return true;
} }
@ -254,7 +522,13 @@ static uint8_t nextLevelForShift(void) {
} }
return (uint8_t)((gGame.variation - 1u) * 8u + sim->levelState); return (uint8_t)((gGame.variation - 1u) * 8u + sim->levelState);
} }
if (sim->levelState >= ST_LEVEL_COUNT) { // $5022/$5167: the 24-hour shifts play screen Y after A..X, and only
// then is the shift over -- which is also what lights the "finished"
// marker in the screens-completed field (levelState reaches 25).
if (sim->levelState == ST_LEVEL_COUNT) {
return ST_SCREEN_Y;
}
if (sim->levelState > ST_LEVEL_COUNT) {
return 0xFFu; return 0xFFu;
} }
if (gGame.variation == 4u) { if (gGame.variation == 4u) {
@ -355,6 +629,7 @@ static void runIntro(uint8_t ticks) {
} }
for (k = 0u; k < (uint8_t)(ticks * 2u); k++) { for (k = 0u; k < (uint8_t)(ticks * 2u); k++) {
if (stIntroStep(&gSim)) { if (stIntroStep(&gSim)) {
jlMusicStop(); // $4709
stSimEnterLevel(&gSim, &gLevel); stSimEnterLevel(&gSim, &gLevel);
stRenderSceneChanged(&gSim); stRenderSceneChanged(&gSim);
if (gGame.demo) { if (gGame.demo) {
@ -362,6 +637,7 @@ static void runIntro(uint8_t ticks) {
stSimSetDemoStream(&gSim, e->stream, e->length); stSimSetDemoStream(&gSim, e->stream, e->length);
} }
gGame.state = ST_STATE_PLAYING; gGame.state = ST_STATE_PLAYING;
serviceJingle();
return; return;
} }
} }
@ -377,6 +653,9 @@ static void runPlaying(uint8_t ticks) {
StTickResultE r = stSimTick(&gSim); StTickResultE r = stSimTick(&gSim);
stAudioTick(); stAudioTick();
if (r == ST_TICK_CONTINUE) { if (r == ST_TICK_CONTINUE) {
if (serviceJingle()) {
return; // $70D4 bonus cab: the tune blocks
}
continue; continue;
} }
if (gGame.demo) { if (gGame.demo) {
@ -390,6 +669,9 @@ static void runPlaying(uint8_t ticks) {
case ST_TICK_LIFE_LOST: case ST_TICK_LIFE_LOST:
stSimRespawn(&gSim); stSimRespawn(&gSim);
stRenderSceneChanged(&gSim); stRenderSceneChanged(&gSim);
if (serviceJingle()) {
return; // $6906 on the new cab
}
break; break;
case ST_TICK_PLAYER_OUT: case ST_TICK_PLAYER_OUT:
gameOverEnter(); gameOverEnter();
@ -401,6 +683,27 @@ static void runPlaying(uint8_t ticks) {
} }
// Start a tune the sim asked for and park the loop in ST_STATE_JINGLE
// until it ends. Returns true when a tune was started, so callers can
// stop ticking the sim for this frame -- the C64's $44CF blocks there.
static bool serviceJingle(void) {
uint8_t song;
if (gSim.jingleRequest == 0u) {
return false;
}
song = (uint8_t)(gSim.jingleRequest - 1u);
gSim.jingleRequest = 0u;
stAudioJingle(song);
if (!stAudioJingleActive()) {
return false; // no audio: nothing to wait for
}
gGame.afterJingle = gGame.state;
gGame.state = ST_STATE_JINGLE;
return true;
}
// $54F8 -- UP/DOWN pick the shift, FIRE confirms and starts the game. // $54F8 -- UP/DOWN pick the shift, FIRE confirms and starts the game.
static void runShiftMenu(uint8_t ticks) { static void runShiftMenu(uint8_t ticks) {
uint8_t mask = joyMask(); uint8_t mask = joyMask();
@ -462,13 +765,7 @@ static void runTitle(uint8_t ticks) {
return; return;
} }
if ((mask & 0x01u) != 0u) { if ((mask & 0x01u) != 0u) {
uint8_t i; highScoreDraw();
blankScreen(&gSim);
stSimDrawText(&gSim, 10u, 1u, kTextImmortal, 1u);
for (i = 0u; i < 8u; i++) {
stSimDrawText(&gSim, 9u, (uint8_t)(4u + i * 2u), kHighScores[i], 1u);
}
stSimDrawText(&gSim, 10u, 22u, kTextReturn, 1u);
gGame.state = ST_STATE_HIGHSCORES; gGame.state = ST_STATE_HIGHSCORES;
gGame.fireArmed = false; gGame.fireArmed = false;
return; return;
@ -477,7 +774,7 @@ static void runTitle(uint8_t ticks) {
static const uint8_t kAboutCols[7] = { 1u, 12u, 15u, 4u, 5u, 6u, 1u }; static const uint8_t kAboutCols[7] = { 1u, 12u, 15u, 4u, 5u, 6u, 1u };
static const uint8_t kAboutRows[7] = { 3u, 5u, 7u, 10u, 12u, 14u, 18u }; static const uint8_t kAboutRows[7] = { 3u, 5u, 7u, 10u, 12u, 14u, 18u };
uint8_t i; uint8_t i;
blankScreen(&gSim); blankScreen(&gSim, 12u, 15u); // $558E: grey border, light-grey field
for (i = 0u; i < 7u; i++) { for (i = 0u; i < 7u; i++) {
stSimDrawText(&gSim, kAboutCols[i], kAboutRows[i], kAboutLines[i], 1u); stSimDrawText(&gSim, kAboutCols[i], kAboutRows[i], kAboutLines[i], 1u);
} }
@ -500,7 +797,7 @@ static void shiftMenuColor(uint8_t sel, uint8_t color) {
// $5295 -- GAME VARIATIONS page 1. // $5295 -- GAME VARIATIONS page 1.
static void showCabbiesMenu(void) { static void showCabbiesMenu(void) {
blankScreen(&gSim); blankScreen(&gSim, 0u, 0u);
stSimDrawText(&gSim, 13u, 0u, kTextGameVariations, 5u); stSimDrawText(&gSim, 13u, 0u, kTextGameVariations, 5u);
stSimDrawText(&gSim, 1u, 2u, kTextCabbies, 7u); stSimDrawText(&gSim, 1u, 2u, kTextCabbies, 7u);
stSimDrawText(&gSim, 5u, 4u, kTextLeftRight, 11u); stSimDrawText(&gSim, 5u, 4u, kTextLeftRight, 11u);
@ -518,7 +815,7 @@ static void showCabbiesMenu(void) {
static void showShiftMenu(void) { static void showShiftMenu(void) {
uint8_t i; uint8_t i;
blankScreen(&gSim); blankScreen(&gSim, 0u, 0u);
stSimDrawText(&gSim, 1u, 8u, kTextSelectShift, 7u); stSimDrawText(&gSim, 1u, 8u, kTextSelectShift, 7u);
for (i = 0u; i < 5u; i++) { for (i = 0u; i < 5u; i++) {
stSimDrawText(&gSim, 2u, (uint8_t)(10u + i * 2u), kTextShifts[i], 6u); stSimDrawText(&gSim, 2u, (uint8_t)(10u + i * 2u), kTextShifts[i], 6u);
@ -560,6 +857,11 @@ static void startDemoScreen(void) {
static void startIntro(void) { static void startIntro(void) {
stIntroEnter(&gSim, &gLevel); stIntroEnter(&gSim, &gLevel);
stRenderSceneChanged(&gSim); stRenderSceneChanged(&gSim);
// After the scene change, which turns it back off.
stRenderIntroStars();
// $459C: the intro's tune plays under the fly-in, not blocking; the
// C64 leaves it to the IRQ player and stops it at $4709.
stAudioMusic(ST_SONG_INTRO, true);
gGame.state = ST_STATE_INTRO; gGame.state = ST_STATE_INTRO;
} }
@ -601,9 +903,15 @@ int main(void) {
jlConfigT config; jlConfigT config;
jlSurfaceT *stage; jlSurfaceT *stage;
#if defined(JOEYLIB_PLATFORM_IIGS) // The codegen arena holds every compiled cel, the baked ones too: the
// One 64 KB bank: enough for the cab, exhaust and passenger cels. // 37 cab/exhaust/passenger cels plus a level's hook cels (level I has
config.codegenBytes = 60UL * 1024; // 30). On the IIgs that is ~230 KB of 65816 code in 64 KB banks, and
// the arena grants as many banks as the machine has (a 1 MB machine
// gets one; whatever does not fit draws interpreted). The 68k ports'
// cels are far larger (9-13 KB each) and their JIT is fast, so they
// keep a modest arena and compile at level entry.
#if defined(JOEYLIB_PLATFORM_IIGS) || defined(JOEYLIB_PLATFORM_DOS)
config.codegenBytes = 256UL * 1024;
#else #else
config.codegenBytes = 160UL * 1024; config.codegenBytes = 160UL * 1024;
#endif #endif
@ -618,6 +926,7 @@ int main(void) {
jlShutdown(); jlShutdown();
return 1; return 1;
} }
gStage = stage;
memset(&gGame, 0, sizeof(gGame)); memset(&gGame, 0, sizeof(gGame));
memset(&gSim, 0, sizeof(gSim)); memset(&gSim, 0, sizeof(gSim));
stRenderInit(stage); stRenderInit(stage);
@ -630,6 +939,7 @@ int main(void) {
stSimSeedDemoBuffer(&gSim, kDemoBufferInit, ST_DEMO_BUFFER_BYTES); stSimSeedDemoBuffer(&gSim, kDemoBufferInit, ST_DEMO_BUFFER_BYTES);
gSim.waveIdx = 3u; gSim.waveIdx = 3u;
stRenderPrewarm(stage, &gSim); stRenderPrewarm(stage, &gSim);
highScoreLoad(); // $23E5
enterTitle(); enterTitle();
gGame.lastFrame = (uint8_t)jlFrameCount(); gGame.lastFrame = (uint8_t)jlFrameCount();
@ -637,9 +947,40 @@ int main(void) {
uint8_t ticks; uint8_t ticks;
jlInputPoll(); jlInputPoll();
// $4FCB runStopWatcher, on the game tick, the intro and the menus:
// SHIFT held freezes everything with the sound gated off and any
// tune stopped (the intro's restarts on release); RUN/STOP -- ESC
// here -- abandons the game for the title, and on the title it
// leaves the program (the C64 had nowhere else to go).
if (gGame.state != ST_STATE_JINGLE && gGame.state != ST_STATE_GAME_OVER && gGame.state != ST_STATE_NAME_ENTRY) {
if (jlKeyDown(KEY_LSHIFT) || jlKeyDown(KEY_RSHIFT)) {
if (!gGame.paused) {
gGame.paused = true;
stAudioSilence();
stAudioNoise(false);
jlMusicStop();
}
stRenderFrame(stage, &gSim);
jlAudioFrameTick();
continue;
}
if (gGame.paused) {
gGame.paused = false;
gGame.lastFrame = (uint8_t)jlFrameCount(); // the held time is not owed
gGame.paceAcc = 0u;
if (gGame.state == ST_STATE_INTRO) {
stAudioMusic(ST_SONG_INTRO, true); // $46B5
}
}
if (jlKeyPressed(KEY_ESCAPE)) { if (jlKeyPressed(KEY_ESCAPE)) {
if (gGame.state == ST_STATE_TITLE) {
break; break;
} }
jlMusicStop(); // $5000
gGame.demo = false;
enterTitle();
}
}
ticks = paceTicks(); ticks = paceTicks();
switch (gGame.state) { switch (gGame.state) {
case ST_STATE_TITLE: case ST_STATE_TITLE:
@ -677,19 +1018,33 @@ int main(void) {
case ST_STATE_PLAYING: case ST_STATE_PLAYING:
runPlaying(ticks); runPlaying(ticks);
break; break;
case ST_STATE_JINGLE:
// The C64 spins in $44D5 until the tune ends. A joystick
// press ends the attract demo here too ($4FE2 stops the
// music on input).
if (gGame.demo && (joyMask() & 0x1Fu) != 0u) {
jlMusicStop();
runDemoEnd();
} else if (!stAudioJingleActive()) {
gGame.state = gGame.afterJingle;
}
break;
case ST_STATE_GAME_OVER: case ST_STATE_GAME_OVER:
if (gGame.waitTicks > ticks) { if (gGame.waitTicks > ticks) {
gGame.waitTicks = (uint16_t)(gGame.waitTicks - ticks); gGame.waitTicks = (uint16_t)(gGame.waitTicks - ticks);
} else { } else if (!gGame.gameOverTune) {
// $6BD0: one more player done; everybody done -> title. // $5C24: after the GAME OVER! wait, song 5 plays out.
gSim.playersDone++; gGame.gameOverTune = true;
if (gSim.playersDone == gSim.playerCount) { stAudioJingle(ST_SONG_GAME_OVER);
enterTitle(); } else if (stAudioJingleActive()) {
} else { // still sounding
startNewScreen(); } else if (!nameEntryStart()) {
} gameOverFinish();
} }
break; break;
case ST_STATE_NAME_ENTRY:
runNameEntry(ticks);
break;
default: default:
enterTitle(); enterTitle();
break; break;

View file

@ -15,6 +15,21 @@
#include "stSim.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)
#else
#define ST_LOG_RESET() jlLogReset()
#define ST_LOG(...) jlLogF(__VA_ARGS__)
#endif
// Level loading through the JoeyLib data path (DATA/levels/...). // Level loading through the JoeyLib data path (DATA/levels/...).
bool stLevelLoad(StLevelT *out, const char *path); bool stLevelLoad(StLevelT *out, const char *path);
@ -35,7 +50,13 @@ void stRenderSceneChanged(StSimT *sim);
// cycle are palette writes instead of repainting 103 (and 407) cells. // cycle are palette writes instead of repainting 103 (and 407) cells.
// Call after stRenderSceneChanged, which turns it back off. // Call after stRenderSceneChanged, which turns it back off.
void stRenderTitleLogo(StSimT *sim); 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);
void stRenderPrewarm(jlSurfaceT *stage, StSimT *sim); void stRenderPrewarm(jlSurfaceT *stage, StSimT *sim);
// Ready a level's hook cels before its ride (evicts the previous level's).
void stRenderLevelCels(jlSurfaceT *stage, const StLevelT *level);
// Audio sink setup and the per-tick release timers ($4320). // Audio sink setup and the per-tick release timers ($4320).
void stAudioInit(void); void stAudioInit(void);

View file

@ -8,8 +8,18 @@
// time; envelopes and waveforms are approximated with an attenuation // time; envelopes and waveforms are approximated with an attenuation
// ramp. The speech samples of the original are not extracted; each // ramp. The speech samples of the original are not extracted; each
// spoken character becomes a short chirp so the cues still land. // 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 "spacetaxi.h"
#include <joey/file.h>
#include "stSongData.h"
// SID voice index in a program's byte 8 -> JoeyLib voice slot. // SID voice index in a program's byte 8 -> JoeyLib voice slot.
@ -25,10 +35,44 @@
#define ST_SID_HZ_NUM 1022727UL #define ST_SID_HZ_NUM 1022727UL
#define ST_SID_HZ_DEN 16777216UL #define ST_SID_HZ_DEN 16777216UL
// Speech chirp: one short tone per character, pitched by the character. // ---- speech ----
#define ST_SPEECH_TICKS 3u //
#define ST_SPEECH_BASE_HZ 300u // The C64 player ($9802) busy-waits a delay loop per stream byte and then
#define ST_SPEECH_STEP_HZ 9u // 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 2u
#define ST_SPEECH_HEADER_BYTES 16u
#define ST_SPEECH_ENTRY_BYTES 8u
#define ST_SPEECH_POLARITIES 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 // Thrust sweep register value -> Hz: the C64 writes the same byte to
// both frequency bytes, so the word is value * 257. // both frequency bytes, so the word is value * 257.
@ -44,10 +88,46 @@ typedef struct {
static StSfxSlotT gSfx; // SID voice 1 (programs + sweep) static StSfxSlotT gSfx; // SID voice 1 (programs + sweep)
static StSfxSlotT gTone; // SID voice 2 static StSfxSlotT gTone; // SID voice 2
static bool gNoiseOn; static bool gNoiseOn;
static uint16_t gSpeechTicks;
// 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: which baked polarity the next utterance plays
// 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 uint16_t sidHz(uint16_t word);
static void 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 void voiceOff(StSfxSlotT *slot);
@ -61,6 +141,208 @@ static uint16_t sidHz(uint16_t word) {
} }
// 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 void 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) {
return;
}
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) {
fclose(fp);
return;
}
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 * ST_SPEECH_POLARITIES * ST_SPEECH_ENTRY_BYTES + dataBytes;
gSpeechBlob = (uint8_t *)jlAlloc(total);
if (gSpeechBlob == 0) {
fclose(fp);
return;
}
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) {
jlFree(gSpeechBlob);
gSpeechBlob = 0;
break;
}
done += want;
}
fclose(fp);
}
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 * ST_SPEECH_POLARITIES * ST_SPEECH_ENTRY_BYTES;
uint8_t k;
gSpeechHead = (uint8_t)((gSpeechHead + 1u) % ST_SPEECH_QUEUE);
for (k = 0u; k < gSpeechCount; k++, e += ST_SPEECH_POLARITIES * ST_SPEECH_ENTRY_BYTES) {
if (e[0] == code) {
const uint8_t *v = e + (uint16_t)gSpeechPol * 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);
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 * ST_SPEECH_POLARITIES * 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;
}
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; libc memcpy is a byte loop there),
// 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) { static void voiceOff(StSfxSlotT *slot) {
if (slot->ticksLeft != 0u) { if (slot->ticksLeft != 0u) {
jlAudioVoice(slot->voice, 0u, ST_ATTEN_OFF); jlAudioVoice(slot->voice, 0u, ST_ATTEN_OFF);
@ -71,6 +353,9 @@ static void voiceOff(StSfxSlotT *slot) {
// Once per game tick: run the release timers ($4320 sfxEnvelopeTick). // Once per game tick: run the release timers ($4320 sfxEnvelopeTick).
void stAudioTick(void) { void stAudioTick(void) {
if (gSpeechActive) {
speechPumpSlice(ST_SPEECH_TICK_SLICE);
}
if (gSfx.ticksLeft != 0u) { if (gSfx.ticksLeft != 0u) {
gSfx.ticksLeft--; gSfx.ticksLeft--;
if (gSfx.ticksLeft == 0u) { if (gSfx.ticksLeft == 0u) {
@ -83,23 +368,23 @@ void stAudioTick(void) {
jlAudioVoice(gTone.voice, 0u, ST_ATTEN_OFF); jlAudioVoice(gTone.voice, 0u, ST_ATTEN_OFF);
} }
} }
if (gSpeechTicks != 0u) {
gSpeechTicks--;
if (gSpeechTicks == 0u) {
jlAudioVoice(ST_VOICE_TONE, 0u, ST_ATTEN_OFF);
}
}
} }
void stAudioInit(void) { void stAudioInit(void) {
(void)jlAudioInit(); (void)jlAudioInit();
gSpeechStage = (uint8_t *)jlAlloc(ST_SPEECH_STAGE_BYTES);
speechLoad();
gSfx.voice = ST_VOICE_SFX; gSfx.voice = ST_VOICE_SFX;
gSfx.ticksLeft = 0u; gSfx.ticksLeft = 0u;
gTone.voice = ST_VOICE_TONE; gTone.voice = ST_VOICE_TONE;
gTone.ticksLeft = 0u; gTone.ticksLeft = 0u;
gNoiseOn = false; gNoiseOn = false;
gSpeechTicks = 0u; gSpeechHead = 0u;
gSpeechTail = 0u;
gSpeechActive = false;
gSpeechPitch = ST_SPEECH_PITCH_BASE;
gSpeechPol = 0u;
} }
@ -108,6 +393,10 @@ void stAudioShutdown(void) {
voiceOff(&gTone); voiceOff(&gTone);
jlAudioNoise(0u, ST_ATTEN_OFF); jlAudioNoise(0u, ST_ATTEN_OFF);
jlAudioShutdown(); jlAudioShutdown();
jlFree(gSpeechStage);
gSpeechStage = 0;
jlFree(gSpeechBlob);
gSpeechBlob = 0;
} }
@ -151,10 +440,112 @@ void stAudioSilence(void) {
} }
// $9802 -- one spoken character (a chirp here). // $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) { void stAudioSpeech(uint8_t ch) {
jlAudioVoice(ST_VOICE_TONE, (uint16_t)(ST_SPEECH_BASE_HZ + (uint16_t)(ch & 0x1Fu) * ST_SPEECH_STEP_HZ), ST_ATTEN_LOUD); if (speechQueue(ch)) {
gSpeechTicks = ST_SPEECH_TICKS; 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();
}
} }

View file

@ -1322,10 +1322,6 @@ const uint8_t kStFlameCelByDirMask[16] = {
0x00, 0xD8, 0xD4, 0x00, 0xD5, 0xD2, 0xD1, 0x00, 0xD7, 0xD3, 0xD6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD8, 0xD4, 0x00, 0xD5, 0xD2, 0xD1, 0x00, 0xD7, 0xD3, 0xD6, 0x00, 0x00, 0x00, 0x00, 0x00,
}; };
const int8_t kStIntroStarDx[7] = { 0, 6, 6, 6, -6, -6, -6 };
const int8_t kStIntroStarDy[7] = { -4, -4, 0, 4, 4, 0, -4 };
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Accessors. On the IIgs the program is split across banks and a 2-byte // Accessors. On the IIgs the program is split across banks and a 2-byte
@ -1358,13 +1354,3 @@ const uint8_t *stC64SfxProgram(uint8_t idx) {
uint8_t stC64FlameCel(uint8_t dirMask) { uint8_t stC64FlameCel(uint8_t dirMask) {
return kStFlameCelByDirMask[dirMask]; return kStFlameCelByDirMask[dirMask];
} }
int8_t stC64IntroStarDx(uint8_t idx) {
return kStIntroStarDx[idx];
}
int8_t stC64IntroStarDy(uint8_t idx) {
return kStIntroStarDy[idx];
}

View file

@ -44,10 +44,6 @@ extern const uint8_t kStSfxPrograms[ST_SFX_COUNT][ST_SFX_PROGRAM_BYTES];
// 8=RIGHT); 0 = no cel for that combination. // 8=RIGHT); 0 = no cel for that combination.
extern const uint8_t kStFlameCelByDirMask[16]; extern const uint8_t kStFlameCelByDirMask[16];
// Level-intro star velocities per sprite 0..6 ($450C / $4513).
extern const int8_t kStIntroStarDx[7];
extern const int8_t kStIntroStarDy[7];
// Cross-segment-safe accessors (see stC64Data.c): reach the tables above // 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 // by an inter-segment call on the IIgs, where a direct 2-byte data
// reference from another bank is not relocatable. // reference from another bank is not relocatable.
@ -56,7 +52,5 @@ const uint8_t *stC64SpriteBitmap(uint8_t idx);
uint8_t stC64RngByte(uint8_t idx); uint8_t stC64RngByte(uint8_t idx);
const uint8_t *stC64SfxProgram(uint8_t idx); const uint8_t *stC64SfxProgram(uint8_t idx);
uint8_t stC64FlameCel(uint8_t dirMask); uint8_t stC64FlameCel(uint8_t dirMask);
int8_t stC64IntroStarDx(uint8_t idx);
int8_t stC64IntroStarDy(uint8_t idx);
#endif #endif

View file

@ -12,6 +12,8 @@
#include <stdint.h> #include <stdint.h>
#include "stSim.h"
// 3x3 tiles = 24x24 px; 32 bytes per 8x8 tile, tile-major (matches // 3x3 tiles = 24x24 px; 32 bytes per 8x8 tile, tile-major (matches
// jlSpriteCreateFromSurface / the .spr cel layout). // jlSpriteCreateFromSurface / the .spr cel layout).
#define ST_CEL_TILES 3u #define ST_CEL_TILES 3u
@ -29,6 +31,11 @@ typedef struct {
extern const StCelDefT kStCels[]; extern const StCelDefT kStCels[];
extern const uint16_t kStCelCount; extern const uint16_t kStCelCount;
// 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 // 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 // tile-major 4bpp-packed cel blob. Colours as in the C64 multicolour
// rules; hires uses `color` for a set bit. // rules; hires uses `color` for a set bit.

View file

@ -11,17 +11,16 @@
#include "stSim.h" #include "stSim.h"
// Screen-code strings ($6C1E..$6C7D). All ASCII-compatible glyphs. // Screen-code strings ($6C1E..$6C7D). All ASCII-compatible glyphs.
static const uint8_t kTextPadPlease[] = { 0x50, 0x41, 0x44, 0x20, 0x20, 0x20, 0x50, 0x4C, 0x45, 0x41, 0x53, 0x45, 0 }; // $6C1E "PAD PLEASE" static const uint8_t kTextPadPlease[] = "PAD PLEASE"; // $6C1E
static const uint8_t kTextUpPlease[] = { 0x20, 0x55, 0x50, 0x20, 0x50, 0x4C, 0x45, 0x41, 0x53, 0x45, 0x21, 0x20, 0 }; // $6C2B " UP PLEASE! " static const uint8_t kTextUpPlease[] = " UP PLEASE! "; // $6C2B
static const uint8_t kTextBlank[] = { 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0 }; // $6C38 static const uint8_t kTextHeyTaxi[] = " HEY, TAXI! "; // $6C45
static const uint8_t kTextHeyTaxi[] = { 0x20, 0x48, 0x45, 0x59, 0x2C, 0x20, 0x54, 0x41, 0x58, 0x49, 0x21, 0x20, 0 }; // $6C45 " HEY, TAXI! " static const uint8_t kTextThanks[] = " THANKS "; // $6C5F
static const uint8_t kTextThanks[] = { 0x20, 0x20, 0x20, 0x54, 0x48, 0x41, 0x4E, 0x4B, 0x53, 0x20, 0x20, 0x20, 0 }; // $6C5F " THANKS "
// Speech cue strings fed to the sample player one character at a time. // Speech cue strings fed to the sample player one character at a time.
static const uint8_t kSpeechHeyTaxi[] = { 0x48, 0x54, 0 }; // $6C6C "HT" static const uint8_t kSpeechHeyTaxi[] = "HT"; // $6C6C
static const uint8_t kSpeechUp[] = { 0x55, 0x3F, 0 }; // $6C6F "U?" static const uint8_t kSpeechUp[] = "U?"; // $6C6F
static const uint8_t kSpeechPad[] = { 0x50, 0x58, 0x3F, 0 }; // $6C72 "PX?" (X = digit) static const uint8_t kSpeechPad[] = "PX?"; // $6C72 (X = digit)
static const uint8_t kSpeechThanks[] = { 0x21, 0 }; // $6C76 "!" static const uint8_t kSpeechThanks[] = "!"; // $6C76
static const uint8_t kSpeechHey[] = { 0x48, 0 }; // $6C78 "H" static const uint8_t kSpeechHey[] = "H"; // $6C78
// Waiting wave cycle ($66D6) and the base-fare blob ($43B9 = +5.00). // 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 kWaveCels[4] = { 0xC6, 0xC7, 0xD9, 0xC7 };
static const uint8_t kFareBlob[7] = { 0x66, 0x66, 0x66, 0x6F, 0x77, 0x74, 0x74 }; static const uint8_t kFareBlob[7] = { 0x66, 0x66, 0x66, 0x6F, 0x77, 0x74, 0x74 };
@ -45,12 +44,12 @@ static void beamIn(StSimT *sim) {
return; return;
} }
sim->spr[1].ptr--; sim->spr[1].ptr--;
if (sim->spr[1].ptr != 0xC7u) { if (sim->spr[1].ptr != ST_SPRITE_PASS_STAND) {
return; return;
} }
if (sim->collisionPhase != 0u) { if (sim->collisionPhase != 0u) {
sim->stage = ST_STAGE_LEAVE; sim->stage = ST_STAGE_LEAVE;
sim->spr[1].ptr = 0xC7u; sim->spr[1].ptr = ST_SPRITE_PASS_STAND;
sim->spriteSlots[sim->activeSpriteIdx] = 0u; sim->spriteSlots[sim->activeSpriteIdx] = 0u;
sim->activeSpriteIdx = 0u; sim->activeSpriteIdx = 0u;
sim->fareSlotCount--; sim->fareSlotCount--;
@ -60,15 +59,9 @@ static void beamIn(StSimT *sim) {
stSimDrawText(sim, 14u, 24u, kTextHeyTaxi, 1u); stSimDrawText(sim, 14u, 24u, kTextHeyTaxi, 1u);
stFareBoardingSound(sim, kSpeechHeyTaxi); stFareBoardingSound(sim, kSpeechHeyTaxi);
// $43D1: the meter starts at $10.00..$19.00. // $43D1: the meter starts at $10.00..$19.00.
{ stSimHudInit(sim);
uint8_t k;
static const uint8_t kTemplate[7] = { 0x66, 0x66, 0x66, 0x74, 0x77, 0x74, 0x74 };
for (k = 0u; k < ST_NUMBER_CHARS; k++) {
stSimPutChar(sim, (uint8_t)(32u + k), 24u, kTemplate[k]);
}
stSimPutChar(sim, 35u, 24u, (uint8_t)(ST_CHAR_DIGIT_BASE + stSimRng(sim, 10u))); stSimPutChar(sim, 35u, 24u, (uint8_t)(ST_CHAR_DIGIT_BASE + stSimRng(sim, 10u)));
stSimPutChar(sim, 34u, 24u, (uint8_t)(ST_CHAR_DIGIT_BASE + 1u)); stSimPutChar(sim, 34u, 24u, (uint8_t)(ST_CHAR_DIGIT_BASE + 1u));
}
} }
@ -125,7 +118,7 @@ static void newFare(StSimT *sim) {
sim->spr[1].enable = 1u; sim->spr[1].enable = 1u;
sim->stage = ST_STAGE_BEAM_IN; sim->stage = ST_STAGE_BEAM_IN;
sim->decayTimer = sim->decayReload; sim->decayTimer = sim->decayReload;
sim->spr[1].ptr = 0xCBu; sim->spr[1].ptr = ST_SPRITE_PASS_BEAM;
stAudioSpeech((uint8_t)(stSimRng(sim, 3u) + 2u)); stAudioSpeech((uint8_t)(stSimRng(sim, 3u) + 2u));
} }
@ -136,14 +129,14 @@ static void stageBoard(StSimT *sim) {
return; return;
} }
sim->spr[1].ptr++; sim->spr[1].ptr++;
if (sim->spr[1].ptr != 0xCCu) { if (sim->spr[1].ptr != ST_SPRITE_PASS_GONE) {
return; return;
} }
sim->spr[1].enable = 0u; sim->spr[1].enable = 0u;
sim->stage++; sim->stage++;
sim->activeDyingSlot = sim->activePad; sim->activeDyingSlot = sim->activePad;
(void)stFareChooseDestination(sim); (void)stFareChooseDestination(sim);
if (sim->activeSpriteIdx == 0x0Bu) { if (sim->activeSpriteIdx == ST_FARE_DEST_UP) {
return; return;
} }
stSimDrawText(sim, 14u, 24u, kTextPadPlease, 1u); stSimDrawText(sim, 14u, 24u, kTextPadPlease, 1u);
@ -164,7 +157,7 @@ static void stageCleanup(StSimT *sim) {
sim->deathInProgress = 0u; sim->deathInProgress = 0u;
sim->stage = ST_STAGE_IDLE; sim->stage = ST_STAGE_IDLE;
stSimPadLight(sim, false); stSimPadLight(sim, false);
stSimDrawText(sim, 14u, 24u, kTextBlank, 1u); stSimClearMessage(sim);
} }
@ -176,7 +169,7 @@ static void stageDropIn(StSimT *sim) {
return; return;
} }
sim->spr[1].ptr--; sim->spr[1].ptr--;
if (sim->spr[1].ptr != 0xC7u) { if (sim->spr[1].ptr != ST_SPRITE_PASS_STAND) {
return; return;
} }
sim->stage++; sim->stage++;
@ -184,13 +177,7 @@ static void stageDropIn(StSimT *sim) {
stFareBoardingSound(sim, kSpeechThanks); stFareBoardingSound(sim, kSpeechThanks);
stSimBcdAdd(sim, ST_CELL_SCORE, kFareBlob); stSimBcdAdd(sim, ST_CELL_SCORE, kFareBlob);
stSimBcdAdd(sim, ST_CELL_SCORE, &sim->screen[ST_CELL_FARE]); stSimBcdAdd(sim, ST_CELL_SCORE, &sim->screen[ST_CELL_FARE]);
{ stSimHudInit(sim);
static const uint8_t kTemplate[7] = { 0x66, 0x66, 0x66, 0x74, 0x77, 0x74, 0x74 };
uint8_t k;
for (k = 0u; k < ST_NUMBER_CHARS; k++) {
stSimPutChar(sim, (uint8_t)(32u + k), 24u, kTemplate[k]);
}
}
if (sim->fareSlotCount != 0u) { if (sim->fareSlotCount != 0u) {
return; return;
} }
@ -204,7 +191,7 @@ void stFareLeaveStep(StSimT *sim) {
return; return;
} }
sim->spr[1].ptr++; sim->spr[1].ptr++;
if (sim->spr[1].ptr != 0xCCu) { if (sim->spr[1].ptr != ST_SPRITE_PASS_GONE) {
return; return;
} }
sim->spr[1].enable = 0u; sim->spr[1].enable = 0u;
@ -227,24 +214,19 @@ void stFareWaveStep(StSimT *sim) {
void stFareWalkStep(StSimT *sim) { void stFareWalkStep(StSimT *sim) {
int16_t passX; int16_t passX;
int16_t targetX; int16_t targetX;
uint16_t sum;
sim->walkParity ^= 1u; sim->walkParity ^= 1u;
passX = (int16_t)(((int16_t)sim->spr[1].msb << 8) | sim->spr[1].col); passX = (int16_t)stSimSpriteX(sim, 1u);
targetX = (int16_t)(((int16_t)sim->hoverXFrac << 8) | sim->hoverXCol); targetX = (int16_t)(((int16_t)sim->hoverXFrac << 8) | sim->hoverXCol);
if ((int16_t)(passX - targetX) < 0) { if ((int16_t)(passX - targetX) < 0) {
// $6D30: walk right, cels $C2/$C3. // $6D30: walk right, cels $C2/$C3.
sim->spr[1].ptr = (uint8_t)(0xC2u + sim->walkParity); sim->spr[1].ptr = (uint8_t)(ST_SPRITE_PASS_WALK_R + sim->walkParity);
sum = (uint16_t)((uint16_t)sim->spr[1].col + 2u); stSimSpriteAddX(sim, 1u, 2u);
sim->spr[1].col = (uint8_t)sum;
sim->spr[1].msb = (uint8_t)(sim->spr[1].msb + (uint8_t)(sum >> 8));
return; return;
} }
// $6D4B: walk left, cels $C4/$C5. // $6D4B: walk left, cels $C4/$C5.
sim->spr[1].ptr = (uint8_t)(0xC4u + sim->walkParity); sim->spr[1].ptr = (uint8_t)(ST_SPRITE_PASS_WALK_L + sim->walkParity);
sum = (uint16_t)((uint16_t)sim->spr[1].col + 0xFEu); stSimSpriteAddX(sim, 1u, 0xFEu);
sim->spr[1].col = (uint8_t)sum;
sim->spr[1].msb = (uint8_t)(sim->spr[1].msb + 0xFFu + (uint8_t)(sum >> 8));
} }
@ -255,7 +237,7 @@ static void walkToTarget(StSimT *sim) {
return; return;
} }
if ((sim->spr[1].col & 0xFEu) == (sim->hoverXCol & 0xFEu)) { if ((sim->spr[1].col & 0xFEu) == (sim->hoverXCol & 0xFEu)) {
sim->spr[1].ptr = 0xC7u; sim->spr[1].ptr = ST_SPRITE_PASS_STAND;
sim->stage++; sim->stage++;
return; return;
} }
@ -272,17 +254,14 @@ void stFareBoardingSound(StSimT *sim, const uint8_t *speech) {
(void)sim; (void)sim;
stAudioSilence(); stAudioSilence();
stAudioNoise(false); stAudioNoise(false);
while (*speech != 0u) { stAudioSpeechPhrase(speech);
stAudioSpeech(*speech);
speech++;
}
} }
// $6537 -- reserve a free fare pad by RNG (linear probe on collision), // $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. // or "UP PLEASE" once every fare pad is taken. Returns the pad number.
uint8_t stFareChooseDestination(StSimT *sim) { uint8_t stFareChooseDestination(StSimT *sim) {
uint8_t special = sim->level->specialPad; uint8_t special = sim->specialPad;
uint8_t x; uint8_t x;
if (sim->fareSlotCount == special) { if (sim->fareSlotCount == special) {
@ -349,7 +328,7 @@ void stFarePostTickGate(StSimT *sim) {
sim->spr[1].msb = sim->spr[0].msb; sim->spr[1].msb = sim->spr[0].msb;
sim->spr[1].col = sim->spr[0].col; sim->spr[1].col = sim->spr[0].col;
sim->spr[1].row = sim->spr[0].row; sim->spr[1].row = sim->spr[0].row;
sim->spr[1].ptr = 0xCBu; sim->spr[1].ptr = ST_SPRITE_PASS_BEAM;
sim->decayTimer = sim->decayReload; sim->decayTimer = sim->decayReload;
pad = &sim->pads[sim->activeSpriteIdx - 1u]; pad = &sim->pads[sim->activeSpriteIdx - 1u];
sim->hoverXFrac = pad->passMsb; sim->hoverXFrac = pad->passMsb;
@ -374,7 +353,7 @@ void stFareSquashed(StSimT *sim) {
sim->spriteSlots[sim->activeSpriteIdx] = 0u; sim->spriteSlots[sim->activeSpriteIdx] = 0u;
sim->activeSpriteIdx = 0u; sim->activeSpriteIdx = 0u;
sim->fareSlotCount--; sim->fareSlotCount--;
stSimDrawText(sim, 14u, 24u, kTextBlank, 1u); stSimClearMessage(sim);
} }
sim->stage = ST_STAGE_LEAVE; sim->stage = ST_STAGE_LEAVE;
stFareBoardingSound(sim, kSpeechHey); stFareBoardingSound(sim, kSpeechHey);
@ -387,14 +366,8 @@ void stFareSquashed(StSimT *sim) {
// $43F3 counts down in the input-mask byte and leaves it zero. // $43F3 counts down in the input-mask byte and leaves it zero.
sim->inputMask = 0u; sim->inputMask = 0u;
} }
sim->spr[1].ptr = 0xC7u; sim->spr[1].ptr = ST_SPRITE_PASS_STAND;
{ stSimHudInit(sim);
static const uint8_t kTemplate[7] = { 0x66, 0x66, 0x66, 0x74, 0x77, 0x74, 0x74 };
uint8_t k;
for (k = 0u; k < ST_NUMBER_CHARS; k++) {
stSimPutChar(sim, (uint8_t)(32u + k), 24u, kTemplate[k]);
}
}
} }
@ -442,7 +415,7 @@ void stFareStageDispatch(StSimT *sim) {
void stFareUpPlease(StSimT *sim) { void stFareUpPlease(StSimT *sim) {
uint8_t k; uint8_t k;
sim->activeSpriteIdx = 0x0Bu; sim->activeSpriteIdx = ST_FARE_DEST_UP;
stSimDrawText(sim, 14u, 24u, kTextUpPlease, 1u); stSimDrawText(sim, 14u, 24u, kTextUpPlease, 1u);
for (k = 0u; k < 4u; k++) { for (k = 0u; k < 4u; k++) {
stSimPutChar(sim, (uint8_t)(18u + k), 0u, ST_CHAR_SPACE); stSimPutChar(sim, (uint8_t)(18u + k), 0u, ST_CHAR_SPACE);

View file

@ -0,0 +1,85 @@
// 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

@ -0,0 +1,114 @@
// 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

@ -0,0 +1,354 @@
; stMarshalIigs.s - Space Taxi's stSimMarshal for the Apple IIgs.
;
; GAS syntax for the llvm-mos w65816 toolchain (see src/iigs/peislam.s
; for the conventions). The C version copies eight sprites' shadow
; registers into the displayed frame through far pointers, which the
; w65816 backend turns into ~5000 cycles of pointer re-materialisation
; per tick (5% of a ride frame). This is the same copy as straight-line
; [dp],y moves: ~700 cycles.
;
; void stSimMarshalIigs(const StMarshalArgsT *args)
; args (A = low word, X = bank) points at { spr, frame, multi }:
; +0 const StSpriteT *spr (32-bit: low word, bank word)
; +4 StFrameT *frame
; +8 uint8_t multi
; StSpriteT (6 bytes): col, msb, row, enable, ptr, color.
; StFrameT: x[8] (u16, +0), y[8] (+16), ptr[8] (+24), color[8] (+32),
; enableMask (+40), multiMask (+41).
; stSim.c checks these layouts at compile time.
;
; Every store runs with M=16 and writes two bytes: the byte after each
; single-byte field gets a junk high byte that the NEXT store repairs,
; so the blocks run in order x, y, ptr, color and finish with the
; enable/multi word. The last colour store's junk lands on enableMask,
; which the final word overwrites.
;
; Direct-page $E0-$EF are the compiler's own per-function scratch
; (re-set before every use, never live across a call); $C0 is left alone.
.section .text.stSimMarshalIigs,"ax"
.globl stSimMarshalIigs
stSimMarshalIigs:
php
rep #0x30
.a16
.i16
sta 0xe0 ; args low
txa
sta 0xe2 ; args bank
ldy #0
lda [0xe0],y
sta 0xe4 ; spr low
ldy #2
lda [0xe0],y
sta 0xe6 ; spr bank
ldy #4
lda [0xe0],y
sta 0xe8 ; frame low
ldy #6
lda [0xe0],y
sta 0xea ; frame bank
ldy #8
lda [0xe0],y
and #0xff
sta 0xec ; multi
stz 0xee ; enable mask accumulator
; sprite 0: x = (msb ? 0x100 : 0) | col; enable bit
ldy #0
lda [0xe4],y ; col | msb<<8
sta 0xf0
and #0xff00
beq mX0clr
lda 0xf0
and #0x00ff
ora #0x0100
bra mX0st
mX0clr:
lda 0xf0
and #0x00ff
mX0st:
ldy #0
sta [0xe8],y
ldy #3
lda [0xe4],y ; enable | ptr<<8
and #0xff
beq mE0off
lda 0xee
ora #1
sta 0xee
mE0off:
; sprite 1: x = (msb ? 0x100 : 0) | col; enable bit
ldy #6
lda [0xe4],y ; col | msb<<8
sta 0xf0
and #0xff00
beq mX1clr
lda 0xf0
and #0x00ff
ora #0x0100
bra mX1st
mX1clr:
lda 0xf0
and #0x00ff
mX1st:
ldy #2
sta [0xe8],y
ldy #9
lda [0xe4],y ; enable | ptr<<8
and #0xff
beq mE1off
lda 0xee
ora #2
sta 0xee
mE1off:
; sprite 2: x = (msb ? 0x100 : 0) | col; enable bit
ldy #12
lda [0xe4],y ; col | msb<<8
sta 0xf0
and #0xff00
beq mX2clr
lda 0xf0
and #0x00ff
ora #0x0100
bra mX2st
mX2clr:
lda 0xf0
and #0x00ff
mX2st:
ldy #4
sta [0xe8],y
ldy #15
lda [0xe4],y ; enable | ptr<<8
and #0xff
beq mE2off
lda 0xee
ora #4
sta 0xee
mE2off:
; sprite 3: x = (msb ? 0x100 : 0) | col; enable bit
ldy #18
lda [0xe4],y ; col | msb<<8
sta 0xf0
and #0xff00
beq mX3clr
lda 0xf0
and #0x00ff
ora #0x0100
bra mX3st
mX3clr:
lda 0xf0
and #0x00ff
mX3st:
ldy #6
sta [0xe8],y
ldy #21
lda [0xe4],y ; enable | ptr<<8
and #0xff
beq mE3off
lda 0xee
ora #8
sta 0xee
mE3off:
; sprite 4: x = (msb ? 0x100 : 0) | col; enable bit
ldy #24
lda [0xe4],y ; col | msb<<8
sta 0xf0
and #0xff00
beq mX4clr
lda 0xf0
and #0x00ff
ora #0x0100
bra mX4st
mX4clr:
lda 0xf0
and #0x00ff
mX4st:
ldy #8
sta [0xe8],y
ldy #27
lda [0xe4],y ; enable | ptr<<8
and #0xff
beq mE4off
lda 0xee
ora #16
sta 0xee
mE4off:
; sprite 5: x = (msb ? 0x100 : 0) | col; enable bit
ldy #30
lda [0xe4],y ; col | msb<<8
sta 0xf0
and #0xff00
beq mX5clr
lda 0xf0
and #0x00ff
ora #0x0100
bra mX5st
mX5clr:
lda 0xf0
and #0x00ff
mX5st:
ldy #10
sta [0xe8],y
ldy #33
lda [0xe4],y ; enable | ptr<<8
and #0xff
beq mE5off
lda 0xee
ora #32
sta 0xee
mE5off:
; sprite 6: x = (msb ? 0x100 : 0) | col; enable bit
ldy #36
lda [0xe4],y ; col | msb<<8
sta 0xf0
and #0xff00
beq mX6clr
lda 0xf0
and #0x00ff
ora #0x0100
bra mX6st
mX6clr:
lda 0xf0
and #0x00ff
mX6st:
ldy #12
sta [0xe8],y
ldy #39
lda [0xe4],y ; enable | ptr<<8
and #0xff
beq mE6off
lda 0xee
ora #64
sta 0xee
mE6off:
; sprite 7: x = (msb ? 0x100 : 0) | col; enable bit
ldy #42
lda [0xe4],y ; col | msb<<8
sta 0xf0
and #0xff00
beq mX7clr
lda 0xf0
and #0x00ff
ora #0x0100
bra mX7st
mX7clr:
lda 0xf0
and #0x00ff
mX7st:
ldy #14
sta [0xe8],y
ldy #45
lda [0xe4],y ; enable | ptr<<8
and #0xff
beq mE7off
lda 0xee
ora #128
sta 0xee
mE7off:
; y[] block (16-bit stores; the junk high byte is repaired by the next store)
ldy #2
lda [0xe4],y
ldy #16
sta [0xe8],y
ldy #8
lda [0xe4],y
ldy #17
sta [0xe8],y
ldy #14
lda [0xe4],y
ldy #18
sta [0xe8],y
ldy #20
lda [0xe4],y
ldy #19
sta [0xe8],y
ldy #26
lda [0xe4],y
ldy #20
sta [0xe8],y
ldy #32
lda [0xe4],y
ldy #21
sta [0xe8],y
ldy #38
lda [0xe4],y
ldy #22
sta [0xe8],y
ldy #44
lda [0xe4],y
ldy #23
sta [0xe8],y
; ptr[] block (16-bit stores; the junk high byte is repaired by the next store)
ldy #4
lda [0xe4],y
ldy #24
sta [0xe8],y
ldy #10
lda [0xe4],y
ldy #25
sta [0xe8],y
ldy #16
lda [0xe4],y
ldy #26
sta [0xe8],y
ldy #22
lda [0xe4],y
ldy #27
sta [0xe8],y
ldy #28
lda [0xe4],y
ldy #28
sta [0xe8],y
ldy #34
lda [0xe4],y
ldy #29
sta [0xe8],y
ldy #40
lda [0xe4],y
ldy #30
sta [0xe8],y
ldy #46
lda [0xe4],y
ldy #31
sta [0xe8],y
; color[] block (16-bit stores; the junk high byte is repaired by the next store)
ldy #5
lda [0xe4],y
ldy #32
sta [0xe8],y
ldy #11
lda [0xe4],y
ldy #33
sta [0xe8],y
ldy #17
lda [0xe4],y
ldy #34
sta [0xe8],y
ldy #23
lda [0xe4],y
ldy #35
sta [0xe8],y
ldy #29
lda [0xe4],y
ldy #36
sta [0xe8],y
ldy #35
lda [0xe4],y
ldy #37
sta [0xe8],y
ldy #41
lda [0xe4],y
ldy #38
sta [0xe8],y
ldy #47
lda [0xe4],y
ldy #39
sta [0xe8],y
; enableMask | multiMask<<8
lda 0xec
xba
and #0xff00
ora 0xee
ldy #40
sta [0xe8],y
plp
rtl

View file

@ -40,11 +40,9 @@
// cab, exhaust and passenger sets plus the intro star) or a cel change // cab, exhaust and passenger sets plus the intro star) or a cel change
// mid-play rebuilds and recompiles a sprite. Glyphs are direct-mapped // mid-play rebuilds and recompiles a sprite. Glyphs are direct-mapped
// by character, so the count is a power of two. // by character, so the count is a power of two.
#if defined(__W65816__) // Room for the baked cab/passenger/exhaust set (37) plus the largest
#define ST_SPRITE_CACHE 48u // level's hook cels (I: 30) at once.
#else #define ST_SPRITE_CACHE 80u
#define ST_SPRITE_CACHE 72u
#endif
#define ST_SPRITE_BACKUP_BYTES JOEY_SPRITE_BACKUP_BYTES(ST_SPRITE_TILES, ST_SPRITE_TILES) #define ST_SPRITE_BACKUP_BYTES JOEY_SPRITE_BACKUP_BYTES(ST_SPRITE_TILES, ST_SPRITE_TILES)
#define ST_SPRITE_PX (8 * ST_SPRITE_TILES) #define ST_SPRITE_PX (8 * ST_SPRITE_TILES)
// Palette slots the title logo animation owns. The title screen draws // Palette slots the title logo animation owns. The title screen draws
@ -71,7 +69,9 @@ typedef struct {
bool used; bool used;
bool pinned; // prewarmed moving cel: never evicted (its bool pinned; // prewarmed moving cel: never evicted (its
// compiled code is costly to re-emit on the 65816) // compiled code is costly to re-emit on the 65816)
bool compiled; // jlSpriteCompile succeeded for this cel
uint32_t lastUse; // for least-recently-used eviction uint32_t lastUse; // for least-recently-used eviction
uint32_t retryAt; // cacheStamp at which a failed compile may retry
} StSpriteCacheT; } StSpriteCacheT;
@ -91,6 +91,9 @@ typedef struct {
// Title logo (stRenderTitleLogo): all four flip-book frames in one // Title logo (stRenderTitleLogo): all four flip-book frames in one
// tile, animated by rewriting logoSlotMask[]'s palette slots. // tile, animated by rewriting logoSlotMask[]'s palette slots.
bool logoMode; bool logoMode;
bool starMode; // level intro: cycling the starfield
uint8_t starPhase; // which ring the bright band is on
uint8_t starTick; // frames until the band steps outward
bool logoTileOk; // the live glyph is one of the frames bool logoTileOk; // the live glyph is one of the frames
uint8_t logoSlotCount; uint8_t logoSlotCount;
uint8_t logoSlotMask[ST_LOGO_PAL_SLOTS]; // frames each slot is lit in uint8_t logoSlotMask[ST_LOGO_PAL_SLOTS]; // frames each slot is lit in
@ -100,8 +103,6 @@ typedef struct {
jlTileT logoTile; jlTileT logoTile;
// Cell range (inclusive) each character has been painted in since // Cell range (inclusive) each character has been painted in since
// the last full repaint: where a changed glyph's cells can be. // the last full repaint: where a changed glyph's cells can be.
uint16_t glyphFirst[ST_CHARSET_CHARS];
uint16_t glyphLast[ST_CHARSET_CHARS];
// Column band of the cells about to be repainted, per character row // Column band of the cells about to be repainted, per character row
// (min > max = the row is clean), the same shape as the library's // (min > max = the row is clean), the same shape as the library's
// own per-row dirty bands. // own per-row dirty bands.
@ -111,7 +112,10 @@ typedef struct {
StSpriteCacheT cache[ST_SPRITE_CACHE]; StSpriteCacheT cache[ST_SPRITE_CACHE];
StSpriteCacheT *lastHit[ST_HW_SPRITES]; // the entry each hardware sprite used last StSpriteCacheT *lastHit[ST_HW_SPRITES]; // the entry each hardware sprite used last
jlSpriteBackupT backup[ST_HW_SPRITES]; jlSpriteBackupT backup[ST_HW_SPRITES];
uint8_t backupMem[ST_HW_SPRITES][ST_SPRITE_BACKUP_BYTES] __attribute__((aligned(2))); // ST_HW_SPRITES backups of ST_SPRITE_BACKUP_BYTES each, from jlAlloc:
// 4.5 KB that as BSS would not fit the IIgs entry bank (text, rodata,
// BSS and the C heap share its 64 KB).
uint8_t *backupMem;
StDrawnT slot[ST_HW_SPRITES]; StDrawnT slot[ST_HW_SPRITES];
uint8_t lastPresentFrame; uint8_t lastPresentFrame;
} StRenderStateT; } StRenderStateT;
@ -132,9 +136,26 @@ static const uint16_t kC64Palette[16] = {
// four of these are used; the rest are headroom if the frames change. // four of these are used; the rest are headroom if the frames change.
static const uint8_t kLogoPalSlot[ST_LOGO_PAL_SLOTS] = { 4u, 8u, 9u, 10u, 13u, 14u, 15u }; static const uint8_t kLogoPalSlot[ST_LOGO_PAL_SLOTS] = { 4u, 8u, 9u, 10u, 13u, 14u, 15u };
// Level-intro warp: how a star ring looks at each distance BEHIND the
// bright band, as C64 colour indices (white, light grey, grey, dark
// grey, then black). A ring this far behind the band or further is
// unlit, so a lit head with a short fading tail sweeps outward and the
// rest of the field stays dark.
static const uint8_t kStarRamp[] = { 1u, 15u, 12u, 11u };
#define ST_STAR_RAMP_LEN (sizeof(kStarRamp) / sizeof(kStarRamp[0]))
// Frames per ring step. The intro runs at the game's 30 Hz tick, so a
// step every other frame sweeps the eight rings in about half a second.
#define ST_STAR_STEP_FRAMES 2u
static void buildSpriteCel(const uint8_t *bm, uint8_t multi, uint8_t color, uint8_t mc0, uint8_t mc1); static void buildSpriteCel(const uint8_t *bm, uint8_t multi, uint8_t color, uint8_t mc0, uint8_t mc1);
static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx); static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx);
static void cacheCompile(StSpriteCacheT *e);
static StSpriteCacheT *cacheFreeSlot(void);
static void levelCelsEvict(void);
static const uint8_t *levelSpriteBitmap(const StLevelT *level, uint8_t ptr);
static void loadingBar(jlSurfaceT *stage, uint8_t done, uint8_t total);
static uint8_t cellCol(uint16_t cell, uint8_t row);
static uint8_t cellRow(uint16_t cell); static uint8_t cellRow(uint16_t cell);
static void collectGlyphs(StSimT *sim); static void collectGlyphs(StSimT *sim);
static void dirtyBands(const StSimT *sim); static void dirtyBands(const StSimT *sim);
@ -145,6 +166,7 @@ static bool loadPrecompiledCels(void);
static bool logoBuildTile(const StSimT *sim); static bool logoBuildTile(const StSimT *sim);
static uint8_t logoFrameIndex(const StSimT *sim); static uint8_t logoFrameIndex(const StSimT *sim);
static void logoPaletteApply(const StSimT *sim, uint8_t frame); static void logoPaletteApply(const StSimT *sim, uint8_t frame);
static void starPaletteApply(void);
static void paintCells(jlSurfaceT *stage, StSimT *sim); static void paintCells(jlSurfaceT *stage, StSimT *sim);
static void pasteCell(jlSurfaceT *stage, const StSimT *sim, uint16_t cell, uint8_t bx, uint8_t by); static void pasteCell(jlSurfaceT *stage, const StSimT *sim, uint16_t cell, uint8_t bx, uint8_t by);
static bool spriteOnScreen(int16_t px, int16_t py); static bool spriteOnScreen(int16_t px, int16_t py);
@ -171,12 +193,27 @@ static void buildSpriteCel(const uint8_t *bm, uint8_t multi, uint8_t color, uint
} }
// Compile a cached cel if it is not already, backing off after a failure
// (arena full) so a cel that cannot fit is not re-staged every frame --
// jlSpriteCompile's failure path runs the whole emitter twice.
#define ST_COMPILE_RETRY_STAMPS 4096u // ~8 frames' worth of cachedSprite calls per frame -> ~25 s
static void cacheCompile(StSpriteCacheT *e) {
if (e->compiled || gRender.cacheStamp < e->retryAt) {
return;
}
e->compiled = jlSpriteCompile(e->sprite);
if (!e->compiled) {
e->retryAt = gRender.cacheStamp + ST_COMPILE_RETRY_STAMPS;
}
}
// The JoeyLib sprite for hardware sprite `idx` of the current frame, // The JoeyLib sprite for hardware sprite `idx` of the current frame,
// built on first use from its VIC bitmap and colours. // built on first use from its VIC bitmap and colours.
static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx) { static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx) {
uint8_t ptr = sim->frame.ptr[idx]; uint8_t ptr = sim->frame.ptr[idx];
uint8_t color = drawColor(sim, idx); uint8_t color = drawColor(sim, idx);
uint8_t multi = (uint8_t)((sim->frame.multiMask >> idx) & 1u); uint8_t multi = (uint8_t)((sim->frame.multiMask & kStBit[idx]) != 0u ? 1u : 0u);
uint8_t mc0 = multi ? sim->spriteMc0 : 0u; uint8_t mc0 = multi ? sim->spriteMc0 : 0u;
uint8_t mc1 = multi ? sim->spriteMc1 : 0u; uint8_t mc1 = multi ? sim->spriteMc1 : 0u;
uint8_t level = (ptr < ST_SPRITE_PTR_FIRST && sim->level != 0) ? sim->level->levelIndex : 0xFFu; uint8_t level = (ptr < ST_SPRITE_PTR_FIRST && sim->level != 0) ? sim->level->levelIndex : 0xFFu;
@ -213,20 +250,36 @@ static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx) {
// so fail into the interpreter path. // so fail into the interpreter path.
return 0; return 0;
} }
// No bitmap for this pointer (hooks enable a sprite a tick before
// they aim it, leaving it at block 0): nothing to draw, and no cel
// to build and evict for.
if (stSimSpriteBitmap(sim, ptr) == 0) {
return 0;
}
if (slot->used) { if (slot->used) {
jlSpriteDestroy(slot->sprite); jlSpriteDestroy(slot->sprite);
slot->sprite = 0; slot->sprite = 0;
slot->used = false; slot->used = false;
slot->compiled = false;
} }
buildSpriteCel(stSimSpriteBitmap(sim, ptr), multi, color, mc0, mc1); buildSpriteCel(stSimSpriteBitmap(sim, ptr), multi, color, mc0, mc1);
slot->sprite = jlSpriteCreateFromSurface(gRender.scratch, 0, 0, ST_SPRITE_TILES, ST_SPRITE_TILES); slot->sprite = jlSpriteCreateFromSurface(gRender.scratch, 0, 0, ST_SPRITE_TILES, ST_SPRITE_TILES);
if (slot->sprite == 0) { if (slot->sprite == 0) {
return 0; return 0;
} }
// Only the cab, exhaust and passenger move every tick; the codegen // Nothing compiles mid-ride: a compile is ~0.5 s on the 65816, a
// arena is theirs. Everything else stays interpreted (drawn seldom). // visible hitch. The cab, exhaust and passenger cels come baked
if (idx <= 2u) { // (stRenderPrewarm), a level's hook cels come baked or are compiled
(void)jlSpriteCompile(slot->sprite); // at level entry (stRenderLevelCels), and a cel that still gets
// built here draws interpreted -- and is logged, because it means
// the level's cel list (stLevelCels.c) missed one.
slot->compiled = false;
slot->retryAt = 0u;
if (idx <= 2u && gPrewarming) {
cacheCompile(slot);
}
if (!gPrewarming) {
ST_LOG("spacetaxi: ! cel $%02X colour %u (level %u) built mid-ride -- add it to stLevelCelList", ptr, color, level);
} }
slot->lastUse = gRender.cacheStamp; slot->lastUse = gRender.cacheStamp;
slot->ptr = ptr; slot->ptr = ptr;
@ -257,6 +310,19 @@ static const uint8_t kCellRow[125] = {
}; };
// Column of a cell whose row is known: cell - row * 40 through a table,
// because the two-shift form of the multiply came back as a __mulhi3
// library call on the 65816.
static uint8_t cellCol(uint16_t cell, uint8_t row) {
static const uint16_t kRowBase[ST_SCREEN_ROWS] = {
0, 40, 80, 120, 160, 200, 240, 280, 320, 360, 400, 440, 480,
520, 560, 600, 640, 680, 720, 760, 800, 840, 880, 920, 960
};
return (uint8_t)(cell - kRowBase[row]);
}
static uint8_t cellRow(uint16_t cell) { static uint8_t cellRow(uint16_t cell) {
return kCellRow[cell >> 3]; return kCellRow[cell >> 3];
} }
@ -271,7 +337,7 @@ static uint8_t cellRow(uint16_t cell) {
static void collectGlyphs(StSimT *sim) { static void collectGlyphs(StSimT *sim) {
uint8_t k; uint8_t k;
uint16_t cell; uint16_t cell;
uint16_t last; uint16_t next;
if (sim->charDirtyAll) { if (sim->charDirtyAll) {
sim->charDirtyAll = false; sim->charDirtyAll = false;
@ -283,11 +349,24 @@ static void collectGlyphs(StSimT *sim) {
if (sim->dirtyAll) { if (sim->dirtyAll) {
continue; continue;
} }
last = gRender.glyphLast[ch]; // Exactly the cells showing `ch`, straight off the reverse index
for (cell = gRender.glyphFirst[ch]; cell <= last; cell++) { // the sim maintains (StSimT in stSim.h). No scan, no screen[]
if (sim->screen[cell] != ch || sim->cellDirty[cell] != 0u) { // test: a cell is in this list if and only if it shows `ch`.
continue; //
} // This replaced a walk of a per-glyph [first, last] span. That
// span only ever widened -- pasteCell pushed it out and nothing
// pulled it back -- so a glyph that once appeared in transient
// text kept a screen-wide span for the rest of the level and was
// rescanned on every animation frame, at 24-bit far-pointer cost
// per cell. It measured 22% of the whole program on level H.
//
// `next` is read before the body because the overflow arm below
// stops the walk, and because keeping the read up here makes the
// loop independent of anything the body does to the cell.
cell = sim->glyphHead[ch];
while (cell != ST_GLYPH_CELL_NONE) {
next = sim->cellNext[cell];
if (sim->cellDirty[cell] == 0u) {
sim->cellDirty[cell] = 1u; sim->cellDirty[cell] = 1u;
if (sim->dirtyCount < (uint16_t)(sizeof(sim->dirtyList) / sizeof(sim->dirtyList[0]))) { if (sim->dirtyCount < (uint16_t)(sizeof(sim->dirtyList) / sizeof(sim->dirtyList[0]))) {
sim->dirtyList[sim->dirtyCount++] = cell; sim->dirtyList[sim->dirtyCount++] = cell;
@ -296,6 +375,8 @@ static void collectGlyphs(StSimT *sim) {
break; break;
} }
} }
cell = next;
}
} }
sim->charDirtyCount = 0u; sim->charDirtyCount = 0u;
} }
@ -331,7 +412,7 @@ static void dirtyBands(const StSimT *sim) {
for (k = 0u; k < sim->dirtyCount; k++) { for (k = 0u; k < sim->dirtyCount; k++) {
uint16_t cell = sim->dirtyList[k]; uint16_t cell = sim->dirtyList[k];
uint8_t r = cellRow(cell); uint8_t r = cellRow(cell);
uint8_t col = (uint8_t)(cell - ((uint16_t)r << 5) - ((uint16_t)r << 3)); uint8_t col = cellCol(cell, r);
if (col < gRender.dirtyColMin[r]) { if (col < gRender.dirtyColMin[r]) {
gRender.dirtyColMin[r] = col; gRender.dirtyColMin[r] = col;
} }
@ -422,7 +503,7 @@ static bool logoBuildTile(const StSimT *sim) {
uint8_t f; uint8_t f;
uint8_t slot; uint8_t slot;
for (f = 0u; f < ST_LOGO_FRAMES; f++) { for (f = 0u; f < ST_LOGO_FRAMES; f++) {
if (((sim->charset[ST_LOGO_FRAME_FIRST + f][row] >> (7u - col)) & 1u) != 0u) { if (((stSimGlyphRow(sim, (uint8_t)(ST_LOGO_FRAME_FIRST + f), row) >> (7u - col)) & 1u) != 0u) {
mask |= (uint8_t)(1u << f); mask |= (uint8_t)(1u << f);
} }
} }
@ -454,7 +535,8 @@ static uint8_t logoFrameIndex(const StSimT *sim) {
uint8_t f; uint8_t f;
for (f = 0u; f < ST_LOGO_FRAMES; f++) { for (f = 0u; f < ST_LOGO_FRAMES; f++) {
if (memcmp(sim->charset[ST_LOGO_CHAR], sim->charset[ST_LOGO_FRAME_FIRST + f], 8u) == 0) { if (memcmp(stSimGlyph(sim, ST_LOGO_CHAR),
stSimGlyph(sim, (uint8_t)(ST_LOGO_FRAME_FIRST + f)), 8u) == 0) {
return f; return f;
} }
} }
@ -494,8 +576,6 @@ static void paintCells(jlSurfaceT *stage, StSimT *sim) {
uint8_t by; uint8_t by;
if (sim->dirtyAll) { if (sim->dirtyAll) {
memset(gRender.glyphFirst, 0xFF, sizeof(gRender.glyphFirst));
memset(gRender.glyphLast, 0, sizeof(gRender.glyphLast));
cell = 0u; cell = 0u;
for (by = 0u; by < ST_SCREEN_ROWS; by++) { for (by = 0u; by < ST_SCREEN_ROWS; by++) {
for (bx = 0u; bx < ST_SCREEN_COLS; bx++) { for (bx = 0u; bx < ST_SCREEN_COLS; bx++) {
@ -511,7 +591,7 @@ static void paintCells(jlSurfaceT *stage, StSimT *sim) {
for (k = 0u; k < sim->dirtyCount; k++) { for (k = 0u; k < sim->dirtyCount; k++) {
cell = sim->dirtyList[k]; cell = sim->dirtyList[k];
by = cellRow(cell); by = cellRow(cell);
bx = (uint8_t)(cell - ((uint16_t)by << 5) - ((uint16_t)by << 3)); bx = cellCol(cell, by);
pasteCell(stage, sim, cell, bx, by); pasteCell(stage, sim, cell, bx, by);
sim->cellDirty[cell] = 0u; sim->cellDirty[cell] = 0u;
} }
@ -525,12 +605,6 @@ static void paintCells(jlSurfaceT *stage, StSimT *sim) {
static void pasteCell(jlSurfaceT *stage, const StSimT *sim, uint16_t cell, uint8_t bx, uint8_t by) { static void pasteCell(jlSurfaceT *stage, const StSimT *sim, uint16_t cell, uint8_t bx, uint8_t by) {
uint8_t chr = sim->screen[cell]; uint8_t chr = sim->screen[cell];
if (cell < gRender.glyphFirst[chr]) {
gRender.glyphFirst[chr] = cell;
}
if (cell > gRender.glyphLast[chr]) {
gRender.glyphLast[chr] = cell;
}
// On the title the logo's cells carry all four flip-book frames at // On the title the logo's cells carry all four flip-book frames at
// once; the palette decides which one shows, so they are pasted as // once; the palette decides which one shows, so they are pasted as
// they are and never touched again. // they are and never touched again.
@ -538,7 +612,30 @@ static void pasteCell(jlSurfaceT *stage, const StSimT *sim, uint16_t cell, uint8
jlTilePaste(stage, bx, by, &gRender.logoTile); jlTilePaste(stage, bx, by, &gRender.logoTile);
return; return;
} }
jlTilePasteGlyph(stage, bx, by, sim->charset[chr], sim->color[cell], sim->bgColor); jlTilePasteGlyph(stage, bx, by, stSimGlyph(sim, chr), sim->color[cell], sim->bgColor);
}
// Light the starfield's ring colours for the current phase: the ring
// the band is on takes kStarRamp[0], the ones just inside it the rest
// of the ramp, and everything else the background. One palette write
// per step and not a single pixel moves -- that is the whole point of
// ST_STAR_RING_FIRST (stSim.h).
static void starPaletteApply(void) {
uint16_t pal[SURFACE_COLORS_PER_PALETTE];
uint16_t color;
uint8_t behind;
uint8_t ring;
jlPaletteGet(gRender.stage, 0u, pal);
for (ring = 0u; ring < ST_STAR_RING_COUNT; ring++) {
// How far this ring sits behind the band, wrapping so the band
// re-enters at the centre as it leaves the edge.
behind = (uint8_t)((gRender.starPhase - ring) & (ST_STAR_RING_COUNT - 1u));
color = (behind < ST_STAR_RAMP_LEN) ? kC64Palette[kStarRamp[behind]] : kC64Palette[0];
pal[ST_STAR_RING_FIRST + ring] = color;
}
jlPaletteSet(gRender.stage, 0u, pal);
} }
@ -598,15 +695,26 @@ void stRenderFrame(jlSurfaceT *stage, StSimT *sim) {
} }
} }
} }
if (gRender.starMode) {
// The warp is the only thing animating here, and it animates in
// the palette: no cells change, no sprite moves for it, so the
// present that ends this frame copies no pixels at all.
gRender.starTick++;
if (gRender.starTick >= ST_STAR_STEP_FRAMES) {
gRender.starTick = 0u;
gRender.starPhase = (uint8_t)((gRender.starPhase + 1u) & (ST_STAR_RING_COUNT - 1u));
starPaletteApply();
}
}
collectGlyphs(sim); collectGlyphs(sim);
dirtyBands(sim); dirtyBands(sim);
for (k = 0u; k < ST_HW_SPRITES; k++) { for (k = 0u; k < ST_HW_SPRITES; k++) {
uint8_t idx = (uint8_t)(ST_HW_SPRITES - 1u - k); uint8_t idx = (uint8_t)(ST_HW_SPRITES - 1u - k);
StDrawnT *d = &gRender.slot[k]; StDrawnT *d = &gRender.slot[k];
uint8_t multi = (uint8_t)((sim->frame.multiMask >> idx) & 1u); uint8_t multi = (uint8_t)((sim->frame.multiMask & kStBit[idx]) != 0u ? 1u : 0u);
int16_t px = (int16_t)((int16_t)sim->frame.x[idx] - ST_SPRITE_X_ORIGIN); int16_t px = (int16_t)((int16_t)sim->frame.x[idx] - ST_SPRITE_X_ORIGIN);
int16_t py = (int16_t)((int16_t)sim->frame.y[idx] - ST_SPRITE_Y_ORIGIN); int16_t py = (int16_t)((int16_t)sim->frame.y[idx] - ST_SPRITE_Y_ORIGIN);
bool want = (sim->frame.enableMask & (uint8_t)(1u << idx)) != 0u && spriteOnScreen(px, py); bool want = (sim->frame.enableMask & kStBit[idx]) != 0u && spriteOnScreen(px, py);
bool same = (want == d->drawn); bool same = (want == d->drawn);
if (same && want) { if (same && want) {
same = (d->x == sim->frame.x[idx] && d->y == sim->frame.y[idx] && d->ptr == sim->frame.ptr[idx] && d->color == drawColor(sim, idx) && d->multi == multi); same = (d->x == sim->frame.x[idx] && d->y == sim->frame.y[idx] && d->ptr == sim->frame.ptr[idx] && d->color == drawColor(sim, idx) && d->multi == multi);
@ -633,7 +741,7 @@ void stRenderFrame(jlSurfaceT *stage, StSimT *sim) {
jlSpriteT *sp; jlSpriteT *sp;
int16_t px; int16_t px;
int16_t py; int16_t py;
if ((sim->frame.enableMask & (uint8_t)(1u << idx)) == 0u) { if ((sim->frame.enableMask & kStBit[idx]) == 0u) {
continue; continue;
} }
px = (int16_t)((int16_t)sim->frame.x[idx] - ST_SPRITE_X_ORIGIN); px = (int16_t)((int16_t)sim->frame.x[idx] - ST_SPRITE_X_ORIGIN);
@ -651,7 +759,7 @@ void stRenderFrame(jlSurfaceT *stage, StSimT *sim) {
d->y = sim->frame.y[idx]; d->y = sim->frame.y[idx];
d->ptr = sim->frame.ptr[idx]; d->ptr = sim->frame.ptr[idx];
d->color = drawColor(sim, idx); d->color = drawColor(sim, idx);
d->multi = (uint8_t)((sim->frame.multiMask >> idx) & 1u); d->multi = (uint8_t)((sim->frame.multiMask & kStBit[idx]) != 0u ? 1u : 0u);
} }
{ {
// Sync to the retrace unless the frame already spans more than // Sync to the retrace unless the frame already spans more than
@ -667,13 +775,20 @@ void stRenderFrame(jlSurfaceT *stage, StSimT *sim) {
void stRenderInit(jlSurfaceT *stage) { void stRenderInit(jlSurfaceT *stage) {
uint8_t *backupMem = gRender.backupMem;
uint8_t k; uint8_t k;
memset(&gRender, 0, sizeof(gRender)); memset(&gRender, 0, sizeof(gRender));
memset(gRender.glyphFirst, 0xFF, sizeof(gRender.glyphFirst)); if (backupMem == 0) {
backupMem = (uint8_t *)jlAlloc((uint32_t)ST_HW_SPRITES * ST_SPRITE_BACKUP_BYTES);
if (backupMem == 0) {
ST_LOG("stRenderInit: no memory for sprite backups");
}
}
gRender.backupMem = backupMem;
gRender.stage = stage; gRender.stage = stage;
for (k = 0u; k < ST_HW_SPRITES; k++) { for (k = 0u; k < ST_HW_SPRITES; k++) {
gRender.backup[k].bytes = gRender.backupMem[k]; gRender.backup[k].bytes = (backupMem == 0) ? 0 : backupMem + (uint16_t)k * ST_SPRITE_BACKUP_BYTES;
} }
jlPaletteSet(stage, 0u, kC64Palette); jlPaletteSet(stage, 0u, kC64Palette);
jlScbSetRange(stage, 0u, (uint16_t)(SURFACE_HEIGHT - 1u), 0u); jlScbSetRange(stage, 0u, (uint16_t)(SURFACE_HEIGHT - 1u), 0u);
@ -695,10 +810,145 @@ void stRenderInit(jlSurfaceT *stage) {
// cache so no cel is JIT-compiled at boot (each 65816 compile costs // cache so no cel is JIT-compiled at boot (each 65816 compile costs
// ~0.4 s). Every kStCels entry becomes a pinned cache slot keyed exactly // ~0.4 s). Every kStCels entry becomes a pinned cache slot keyed exactly
// as the play lookups: system ptr (level 0xFF), the level-wide colours. // as the play lookups: system ptr (level 0xFF), the level-wide colours.
// The first unused cache slot, or NULL when the cache is full.
static StSpriteCacheT *cacheFreeSlot(void) {
uint8_t k;
for (k = 0u; k < ST_SPRITE_CACHE; k++) {
if (!gRender.cache[k].used) {
return &gRender.cache[k];
}
}
return 0;
}
// Drop every cel that belongs to a level (the baked set is level 0xFF and
// stays), freeing its arena code, and forget any hardware sprite's last
// hit on it.
static void levelCelsEvict(void) {
uint8_t k;
for (k = 0u; k < ST_SPRITE_CACHE; k++) {
StSpriteCacheT *e = &gRender.cache[k];
uint8_t h;
if (!e->used || e->level == 0xFFu) {
continue;
}
for (h = 0u; h < ST_HW_SPRITES; h++) {
if (gRender.lastHit[h] == e) {
gRender.lastHit[h] = 0;
}
}
jlSpriteDestroy(e->sprite);
e->sprite = 0;
e->used = false;
e->pinned = false;
e->compiled = false;
}
}
// A level's own sprite block, or NULL when the level does not ship it.
static const uint8_t *levelSpriteBitmap(const StLevelT *level, uint8_t ptr) {
uint8_t k;
for (k = 0u; k < level->spriteCount; k++) {
if (level->sprites[k].ptr == ptr) {
return level->sprites[k].bitmap;
}
}
return 0;
}
static void loadingBar(jlSurfaceT *stage, uint8_t done, uint8_t total) {
jlFillRect(stage, 60, 110, (uint16_t)((uint16_t)done * 200u / total), 6u, 1u);
jlStagePresent();
}
// Ready a level's hook cels BEFORE the ride: the previous level's cels
// go, then the level's list (stLevelCelList) is filled from its baked
// bank (sprites/levelNN.spc) or, when there is none, built and compiled
// here behind the LOADING bar. Either way nothing compiles once the
// level is running. Keyed exactly as cachedSprite keys them, so every
// lookup during the ride is a hit.
void stRenderLevelCels(jlSurfaceT *stage, const StLevelT *level) {
static StCelDefT list[ST_LEVEL_CELS_MAX];
jlSpriteT *cels[ST_LEVEL_CELS_MAX];
char path[24];
uint8_t n;
uint8_t got;
uint8_t k;
levelCelsEvict();
if (level == 0) {
return;
}
n = stLevelCelList(level, list, ST_LEVEL_CELS_MAX);
if (n == 0u) {
return;
}
memcpy(path, "sprites/level", 13u);
path[13] = (char)('0' + (level->levelIndex + 1u) / 10u);
path[14] = (char)('0' + (level->levelIndex + 1u) % 10u);
memcpy(path + 15, ".spc", 5u);
got = (uint8_t)jlSpriteBankLoadPrecompiled(path, cels, n, 0);
if (got == n) {
ST_LOG("spacetaxi: level %u cels from %s (%u)", level->levelIndex, path, n);
} else {
for (k = 0u; k < got; k++) {
jlSpriteDestroy(cels[k]);
}
ST_LOG("spacetaxi: level %u: no usable %s -- compiling %u cels at entry", level->levelIndex, path, n);
stRenderLoading(stage);
}
gPrewarming = true;
for (k = 0u; k < n; k++) {
StSpriteCacheT *slot = cacheFreeSlot();
if (slot == 0) {
ST_LOG("spacetaxi: ! sprite cache full at level cel %u", k);
break;
}
if (got == n) {
slot->sprite = cels[k];
slot->compiled = true;
} else {
const uint8_t *bm = levelSpriteBitmap(level, list[k].ptr);
if (bm == 0) {
continue;
}
buildSpriteCel(bm, list[k].multi, list[k].color, list[k].mc0, list[k].mc1);
slot->sprite = jlSpriteCreateFromSurface(gRender.scratch, 0, 0, ST_SPRITE_TILES, ST_SPRITE_TILES);
if (slot->sprite == 0) {
continue;
}
slot->compiled = false;
slot->retryAt = 0u;
cacheCompile(slot);
loadingBar(stage, (uint8_t)(k + 1u), n);
}
slot->ptr = list[k].ptr;
slot->color = list[k].color;
slot->multi = list[k].multi;
slot->mc0 = list[k].mc0;
slot->mc1 = list[k].mc1;
slot->level = level->levelIndex;
slot->used = true;
slot->pinned = true;
slot->lastUse = gRender.cacheStamp;
}
gPrewarming = false;
if (got != n) {
jlFillRect(stage, 60, 110, 200u, 6u, 0u);
}
}
// Returns false if the .spc is missing or built for another target/shift // Returns false if the .spc is missing or built for another target/shift
// count (other ports, or a stale bake), so the caller falls back to the // count (a stale bake), so the caller falls back to the runtime JIT
// runtime JIT prewarm below. Its cross-platform contract makes this a // prewarm below. Every port stages its own bake (make/<port>.mk).
// no-op everywhere but the IIgs, where the bake is wired.
static bool loadPrecompiledCels(void) { static bool loadPrecompiledCels(void) {
jlSpriteT *cels[64]; jlSpriteT *cels[64];
uint16_t n; uint16_t n;
@ -761,8 +1011,10 @@ void stRenderPrewarm(jlSurfaceT *stage, StSimT *sim) {
// Precompiled bank: instant, no per-cel JIT. Falls through to the // Precompiled bank: instant, no per-cel JIT. Falls through to the
// runtime build below on any port without a matching .spc. // runtime build below on any port without a matching .spc.
if (loadPrecompiledCels()) { if (loadPrecompiledCels()) {
ST_LOG("spacetaxi: cels from sprites/staxicels.spc");
return; return;
} }
ST_LOG("spacetaxi: ! no usable sprites/staxicels.spc -- JIT prewarm");
saved = sim->frame; saved = sim->frame;
gPrewarming = true; gPrewarming = true;
sim->frame.multiMask = 0x07u; sim->frame.multiMask = 0x07u;
@ -773,24 +1025,21 @@ void stRenderPrewarm(jlSurfaceT *stage, StSimT *sim) {
sim->frame.color[0] = 0x06u; sim->frame.color[0] = 0x06u;
(void)cachedSprite(sim, 0u); (void)cachedSprite(sim, 0u);
done++; done++;
jlFillRect(stage, 60, 110, (uint16_t)((uint16_t)done * 200u / total), 6u, 1u); loadingBar(stage, done, total);
jlStagePresent();
} }
for (k = 0u; k < sizeof(kPassPtrs); k++) { for (k = 0u; k < sizeof(kPassPtrs); k++) {
sim->frame.ptr[1] = kPassPtrs[k]; sim->frame.ptr[1] = kPassPtrs[k];
sim->frame.color[1] = 0x06u; sim->frame.color[1] = 0x06u;
(void)cachedSprite(sim, 1u); (void)cachedSprite(sim, 1u);
done++; done++;
jlFillRect(stage, 60, 110, (uint16_t)((uint16_t)done * 200u / total), 6u, 1u); loadingBar(stage, done, total);
jlStagePresent();
} }
for (k = 0u; k < sizeof(kFlamePtrs); k++) { for (k = 0u; k < sizeof(kFlamePtrs); k++) {
sim->frame.ptr[2] = kFlamePtrs[k]; sim->frame.ptr[2] = kFlamePtrs[k];
sim->frame.color[2] = 0x07u; sim->frame.color[2] = 0x07u;
(void)cachedSprite(sim, 2u); (void)cachedSprite(sim, 2u);
done++; done++;
jlFillRect(stage, 60, 110, (uint16_t)((uint16_t)done * 200u / total), 6u, 1u); loadingBar(stage, done, total);
jlStagePresent();
} }
// The intro star is a hires sprite in colour 7. Build it as a moving // The intro star is a hires sprite in colour 7. Build it as a moving
// (idx <= 2) cel so it compiles: the level-name intro draws seven of // (idx <= 2) cel so it compiles: the level-name intro draws seven of
@ -809,16 +1058,17 @@ void stRenderPrewarm(jlSurfaceT *stage, StSimT *sim) {
void stRenderSceneChanged(StSimT *sim) { void stRenderSceneChanged(StSimT *sim) {
uint8_t k; uint8_t k;
if (gRender.logoMode) { if (gRender.logoMode || gRender.starMode) {
// The logo's reserved slots go back to being C64 colours. // The borrowed slots go back to being C64 colours.
gRender.logoMode = false; gRender.logoMode = false;
gRender.logoTileOk = false; gRender.logoTileOk = false;
gRender.starMode = false;
jlPaletteSet(gRender.stage, 0u, kC64Palette); jlPaletteSet(gRender.stage, 0u, kC64Palette);
} }
for (k = 0u; k < ST_HW_SPRITES; k++) { for (k = 0u; k < ST_HW_SPRITES; k++) {
gRender.slot[k].drawn = false; gRender.slot[k].drawn = false;
} }
stSimDirtyAll(sim); stSimDirtyCells(sim);
sim->charDirtyAll = true; sim->charDirtyAll = true;
sim->charDirtyCount = 0u; sim->charDirtyCount = 0u;
} }
@ -838,7 +1088,19 @@ void stRenderTitleLogo(StSimT *sim) {
gRender.logoMode = true; gRender.logoMode = true;
gRender.logoTileOk = (frame < ST_LOGO_FRAMES); gRender.logoTileOk = (frame < ST_LOGO_FRAMES);
logoPaletteApply(sim, gRender.logoTileOk ? frame : 0u); logoPaletteApply(sim, gRender.logoTileOk ? frame : 0u);
stSimDirtyAll(sim); stSimDirtyCells(sim);
}
// Turn on the level intro's palette-cycled starfield. Call it after
// stRenderSceneChanged (which turns it back off) on entry to the level
// intro. The cells were painted by stIntroEnter; from here the warp is
// one palette write every ST_STAR_STEP_FRAMES frames.
void stRenderIntroStars(void) {
gRender.starMode = true;
gRender.starPhase = 0u;
gRender.starTick = 0u;
starPaletteApply();
} }
@ -848,4 +1110,6 @@ void stRenderShutdown(void) {
jlSurfaceDestroy(gRender.scratch); jlSurfaceDestroy(gRender.scratch);
gRender.scratch = 0; gRender.scratch = 0;
} }
jlFree(gRender.backupMem);
gRender.backupMem = 0;
} }

File diff suppressed because it is too large Load diff

View file

@ -31,6 +31,9 @@
#define ST_SCREEN_COLS 40u #define ST_SCREEN_COLS 40u
#define ST_SCREEN_ROWS 25u #define ST_SCREEN_ROWS 25u
#define ST_SCREEN_CELLS (ST_SCREEN_COLS * ST_SCREEN_ROWS) #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 // 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 // no multiply: with a variable row the * form called the compiler's
// software multiply helper, and the collision pre-check does that a // software multiply helper, and the collision pre-check does that a
@ -43,6 +46,9 @@
#define ST_SPRITE_Y_ORIGIN 50 #define ST_SPRITE_Y_ORIGIN 50
#define ST_SPRITE_W 24 #define ST_SPRITE_W 24
#define ST_SPRITE_H 21 #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 #define ST_HW_SPRITES 8u
// Fixed screen cells the game writes directly (C64 screen addresses). // Fixed screen cells the game writes directly (C64 screen addresses).
@ -53,6 +59,8 @@
#define ST_CELL_SCREENS ST_CELL(23, 32) // $07B8..$07BC #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_SCORE ST_CELL(24, 2) // $07C2..$07C8, 7 chars
#define ST_CELL_MESSAGE ST_CELL(24, 14) // $07CE, 11 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_PAD_DIGIT ST_CELL(24, 18) // $07D2
#define ST_CELL_FARE ST_CELL(24, 32) // $07E0..$07E6, 7 chars #define ST_CELL_FARE ST_CELL(24, 32) // $07E0..$07E6, 7 chars
#define ST_NUMBER_CHARS 7u #define ST_NUMBER_CHARS 7u
@ -71,7 +79,57 @@
#define ST_CHAR_FUEL_EMPTY 0x7Au #define ST_CHAR_FUEL_EMPTY 0x7Au
#define ST_CHAR_TRANSPORTER 0x67u // animated top-wall hatch #define ST_CHAR_TRANSPORTER 0x67u // animated top-wall hatch
#define ST_CHAR_CAB_ICON 0xC8u #define ST_CHAR_CAB_ICON 0xC8u
#define ST_CHAR_MENU_SENTINEL 0x7Bu // $07A6 == $7B: first fuel cell half = out of fuel #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 + ring (ring 0 is
// the centre), 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];
// Ring 0 is the centre, which in a warp is the VANISHING POINT: those
// stars are the far ones and get the small glyph. The big glyph belongs
// to the outer rings, the stars about to rush past the viewer.
#define ST_STAR_GLYPH_NEAR 0x2Au // '*', the outer rings
#define ST_STAR_GLYPH_FAR 0x2Eu // '.', the inner rings
#define ST_STAR_FAR_RINGS 5u // rings 0..4 are still distant
// 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 // 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 // 1..11, and the flip-book animates it by copying one of four frame
@ -88,6 +146,16 @@
// Distinct characters whose glyph can change in one frame before the // Distinct characters whose glyph can change in one frame before the
// renderer gives up and repaints everything. // renderer gives up and repaints everything.
#define ST_CHAR_DIRTY_MAX 8u #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. // Level image as loaded from a .dat (STL4). Kept in the C64's units.
#define ST_MAX_PADS 10u #define ST_MAX_PADS 10u
@ -98,6 +166,113 @@
#define ST_HOOK_BASE 0x7D98u #define ST_HOOK_BASE 0x7D98u
#define ST_HOOK_BYTES (0x8000u - ST_HOOK_BASE) #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 { typedef struct {
uint8_t x1Hi; // slot byte 0: landing X lower bound, high byte uint8_t x1Hi; // slot byte 0: landing X lower bound, high byte
uint8_t x1Lo; // slot byte 1 uint8_t x1Lo; // slot byte 1
@ -128,7 +303,7 @@ typedef struct {
StPadT pads[ST_MAX_PADS]; // $7D0A.. StPadT pads[ST_MAX_PADS]; // $7D0A..
uint8_t screen[ST_SCREEN_CELLS]; // decompressed screen RAM uint8_t screen[ST_SCREEN_CELLS]; // decompressed screen RAM
uint8_t color[ST_SCREEN_CELLS]; // decompressed colour RAM uint8_t color[ST_SCREEN_CELLS]; // decompressed colour RAM
uint8_t levelIndex; // 0..23 = A..X, 24 = title uint8_t levelIndex; // 0..23 = A..X, 24 = Y, 25 = title
uint8_t spriteCount; uint8_t spriteCount;
StLevelSpriteT sprites[ST_MAX_LEVEL_SPRITES]; StLevelSpriteT sprites[ST_MAX_LEVEL_SPRITES];
uint8_t hookData[ST_HOOK_BYTES]; // $7D98..$7FFF as shipped uint8_t hookData[ST_HOOK_BYTES]; // $7D98..$7FFF as shipped
@ -200,7 +375,34 @@ typedef struct {
uint16_t dirtyList[512]; // cells marked since the last frame uint16_t dirtyList[512]; // cells marked since the last frame
uint16_t dirtyCount; uint16_t dirtyCount;
bool dirtyAll; // list overflowed / whole screen bool dirtyAll; // list overflowed / whole screen
uint8_t charset[ST_CHARSET_CHARS][8]; // $2800 working copy // 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 // Characters whose glyph bits the game has edited since the last
// frame -- the transporter hatch, the title logo flip, the laser // frame -- the transporter hatch, the title logo flip, the laser
// beams. A short list, not a 256-entry flag array: the renderer had // beams. A short list, not a 256-entry flag array: the renderer had
@ -221,6 +423,8 @@ typedef struct {
// ---- level image (mutable copies: hooks edit the pad table) ---- // ---- level image (mutable copies: hooks edit the pad table) ----
const StLevelT *level; const StLevelT *level;
StPadT pads[ST_MAX_PADS]; // $7D0A working copy 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 ---- // ---- input ----
uint8_t inputMask; // $7169 EOR $FF of $DC00: 1 = pressed uint8_t inputMask; // $7169 EOR $FF of $DC00: 1 = pressed
@ -255,12 +459,25 @@ typedef struct {
uint8_t hitDispatchResult; // $721D uint8_t hitDispatchResult; // $721D
uint8_t spriteSpriteColl; // $71CA latched $D01E uint8_t spriteSpriteColl; // $71CA latched $D01E
uint8_t spriteBgColl; // $71CB latched $D01F uint8_t spriteBgColl; // $71CB latched $D01F
// Per-sprite 24-bit row masks of the displayed frame, rebuilt only // Sprites besides the cab whose $D01F bit a hook reads (levels J
// when the sprite's bitmap or multicolour mode changes (derived // and P watch their hazards hit the scenery); the rest are never
// state, not C64 memory). // tested (derived state, not C64 memory).
uint32_t collMask[ST_HW_SPRITES][ST_SPRITE_H]; 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]; const uint8_t *collMaskBm[ST_HW_SPRITES];
uint8_t collMaskMulti[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 ---- // ---- fuel ----
uint8_t fuelCountdown; // $71C7 uint8_t fuelCountdown; // $71C7
@ -300,6 +517,12 @@ typedef struct {
uint8_t levelState; // $7215 screens completed uint8_t levelState; // $7215 screens completed
uint8_t demoMode; // $721C uint8_t demoMode; // $721C
uint8_t postMortem; // $5E9D 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 // ---- demo playback ($48F2): the $0902 buffer a recording is
// loaded into; a shorter recording leaves the previous tail behind // loaded into; a shorter recording leaves the previous tail behind
@ -318,8 +541,6 @@ typedef struct {
uint8_t logoColorIdx; // $48A4 uint8_t logoColorIdx; // $48A4
uint8_t logoCycleAux; // $48A5 uint8_t logoCycleAux; // $48A5
uint8_t logoDiv; // $48A6 uint8_t logoDiv; // $48A6
uint8_t starTimer[7]; // $44F8
uint8_t starReload[7]; // $4500
uint8_t introLoopCount; // $4508 uint8_t introLoopCount; // $4508
uint8_t introCabDx; // $450A uint8_t introCabDx; // $450A
uint8_t introCabDxTimer; // $450B uint8_t introCabDxTimer; // $450B
@ -327,7 +548,7 @@ typedef struct {
uint8_t spriteMc1; // $D026 uint8_t spriteMc1; // $D026
// ---- per-level hook scratch ($7D9C.. and friends) ---- // ---- per-level hook scratch ($7D9C.. and friends) ----
uint8_t hookRam[ST_HOOK_BYTES]; // $7D98..$7FFF working copy StHookScratchT hookScratch; // per-level hook state
uint8_t hookLevel; // level index the hook state belongs to uint8_t hookLevel; // level index the hook state belongs to
uint8_t warpCounter; // $5A66 uint8_t warpCounter; // $5A66
uint8_t warpMask; // $5CE8 uint8_t warpMask; // $5CE8
@ -357,6 +578,29 @@ bool stSimAdvancePlayer(StSimT *sim);
void stSimArchiveHud(StSimT *sim); void stSimArchiveHud(StSimT *sim);
// Mark every cell dirty (scene change). // Mark every cell dirty (scene change).
void stSimDirtyAll(StSimT *sim); 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. // A glyph's bits changed: every cell showing it has to be repainted.
void stSimMarkChar(StSimT *sim, uint8_t ch); void stSimMarkChar(StSimT *sim, uint8_t ch);
// Text and HUD primitives (screen RAM writes). // Text and HUD primitives (screen RAM writes).
@ -404,13 +648,19 @@ void stHookPerTick2(StSimT *sim); // $7D6C
void stHookPerTick3(StSimT *sim); // $7D6F void stHookPerTick3(StSimT *sim); // $7D6F
uint8_t stHookInput(StSimT *sim, uint8_t input); // $7D72 uint8_t stHookInput(StSimT *sim, uint8_t input); // $7D72
uint8_t stHookHitVerdict(StSimT *sim); // $7D75: 0 = passenger contact is safe 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) ---- // ---- audio sink (host provides; stAudio.c / stsim stub) ----
void stAudioSfx(const uint8_t *program9); // $42E9 load void stAudioSfx(const uint8_t *program9); // $42E9 load
void stAudioNoise(bool on); // $D412 = $81 / $80 void stAudioNoise(bool on); // $D412 = $81 / $80
void stAudioThrustSweep(uint8_t value); // $D400/$D401 = value void stAudioThrustSweep(uint8_t value); // $D400/$D401 = value
void stAudioSpeech(uint8_t ch); // $9802 void stAudioSpeech(uint8_t ch); // $9802
void stAudioSpeechPhrase(const uint8_t *speech);
void stAudioSilence(void); // voices 1+2 gate off 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 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 void stAudioVoice1Freq(uint8_t value); // $D400/$D401 pokes from the fuel pump

View file

@ -0,0 +1,512 @@
// 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

@ -0,0 +1,490 @@
// 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

@ -0,0 +1,26 @@
// 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

@ -11,6 +11,11 @@
// Level intro ($4523..$4727): a black screen with the level name, the // Level intro ($4523..$4727): a black screen with the level name, the
// cab rising from the bottom and seven star sprites streaming out of // cab rising from the bottom and seven star sprites streaming out of
// the centre, until the cab reaches mid-screen. // 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 cab still flies, and it is still what ends the intro, so the
// sequence keeps its length and its cue; only the warp changes.
#include <string.h> #include <string.h>
@ -28,24 +33,49 @@ static const uint8_t kLogoColor[8] = { 0x02, 0x08, 0x07, 0x05, 0x06, 0x0E, 0x03
// Intro cab drift per loop count ($470D). // Intro cab drift per loop count ($470D).
static const int8_t kIntroCabDy[4] = { -2, 1, -1, 1 }; static const int8_t kIntroCabDy[4] = { -2, 1, -1, 1 };
// Intro texts ($4624 / $463F). // Intro texts ($4624 / $463F).
static const uint8_t kTextDemoExit[] = { 0x44, 0x45, 0x4D, 0x4F, 0x2C, 0x20, 0x55, 0x53, 0x45, 0x20, 0x4A, 0x4F, 0x59, 0x53, 0x54, 0x49, 0x43, 0x4B, 0x20, 0x54, 0x4F, 0x20, 0x45, 0x58, 0x49, 0x54, 0 }; static const uint8_t kTextDemoExit[] = "DEMO, USE JOYSTICK TO EXIT";
static const uint8_t kTextScreens[] = { 0x54, 0x48, 0x49, 0x53, 0x20, 0x49, 0x53, 0x20, 0x31, 0x20, 0x4F, 0x46, 0x20, 0x32, 0x35, 0x20, 0x44, 0x49, 0x46, 0x46, 0x45, 0x52, 0x45, 0x4E, 0x54, 0x20, 0x53, 0x43, 0x52, 0x45, 0x45, 0x4E, 0x53, 0x21, 0 }; static const uint8_t kTextScreens[] = "THIS IS 1 OF 25 DIFFERENT SCREENS!";
#define ST_TITLE_CAB_LIFT_ROW 0x14u #define ST_TITLE_CAB_LIFT_ROW 0x14u
#define ST_TITLE_SPARKLE_TICKS 0x5Au #define ST_TITLE_SPARKLE_TICKS 0x5Au
#define ST_TITLE_HOVER_COL 0x28u #define ST_TITLE_HOVER_COL 0x28u
#define ST_INTRO_STAR_PTR 0xDAu #define ST_INTRO_STAR_PTR 0xDAu
#define ST_INTRO_STAR_RELOAD 0x20u
#define ST_INTRO_CAB_END_ROW 0x94u #define ST_INTRO_CAB_END_ROW 0x94u
#define ST_INTRO_CAB_DX_RELOAD 0x28u #define ST_INTRO_CAB_DX_RELOAD 0x28u
// The palette-cycled starfield. Seven stars per ring so the bright band
// stays the same weight all the way out, placed at fixed pseudo-random
// angles and clear of the three lines the intro screen writes (the level
// name on row 12, and the demo hints on rows 3 and 24).
typedef struct {
uint8_t col;
uint8_t row;
uint8_t ring;
} StStarT;
static const StStarT kIntroStar[] = {
{ 18u, 10u, 0u }, { 19u, 10u, 0u }, { 17u, 11u, 0u }, { 18u, 13u, 0u },
{ 21u, 13u, 0u }, { 22u, 14u, 0u }, { 18u, 15u, 0u }, { 18u, 8u, 1u },
{ 19u, 8u, 1u }, { 14u, 10u, 1u }, { 25u, 14u, 1u }, { 21u, 15u, 1u },
{ 25u, 15u, 1u }, { 20u, 17u, 1u }, { 26u, 7u, 2u }, { 12u, 10u, 2u },
{ 29u, 11u, 2u }, { 11u, 13u, 2u }, { 29u, 14u, 2u }, { 21u, 17u, 2u },
{ 23u, 18u, 2u }, { 21u, 4u, 3u }, { 24u, 4u, 3u }, { 11u, 8u, 3u },
{ 12u, 8u, 3u }, { 10u, 10u, 3u }, { 30u, 16u, 3u }, { 14u, 17u, 3u },
{ 15u, 2u, 4u }, { 8u, 8u, 4u }, { 32u, 9u, 4u }, { 9u, 17u, 4u },
{ 32u, 17u, 4u }, { 32u, 18u, 4u }, { 24u, 22u, 4u }, { 16u, 1u, 5u },
{ 28u, 1u, 5u }, { 31u, 2u, 5u }, { 36u, 13u, 5u }, { 6u, 15u, 5u },
{ 4u, 16u, 5u }, { 5u, 19u, 5u }, { 8u, 0u, 6u }, { 30u, 0u, 6u },
{ 1u, 8u, 6u }, { 2u, 12u, 6u }, { 37u, 16u, 6u }, { 7u, 21u, 6u },
{ 32u, 22u, 6u }, { 0u, 4u, 7u }, { 39u, 7u, 7u }, { 0u, 10u, 7u },
{ 38u, 20u, 7u }, { 38u, 21u, 7u }, { 4u, 22u, 7u }, { 35u, 22u, 7u },
};
#define ST_INTRO_STAR_COUNT (sizeof(kIntroStar) / sizeof(kIntroStar[0]))
static void addToRow(StSimT *sim, uint8_t idx, uint8_t delta); static void addToRow(StSimT *sim, uint8_t idx, uint8_t delta);
static void addToSpriteX(StSimT *sim, uint8_t idx, uint8_t delta);
static void logoColorCycle(StSimT *sim); static void logoColorCycle(StSimT *sim);
static void logoFlip(StSimT *sim); static void logoFlip(StSimT *sim);
static void marshalFrame(StSimT *sim); static void paintStarfield(StSimT *sim);
static void starRespawn(StSimT *sim, uint8_t idx);
// $4113 // $4113
@ -54,16 +84,6 @@ static void addToRow(StSimT *sim, uint8_t idx, uint8_t delta) {
} }
// $411B
static void addToSpriteX(StSimT *sim, uint8_t idx, uint8_t delta) {
uint16_t x = (uint16_t)(((uint16_t)sim->spr[idx].msb << 8) | sim->spr[idx].col);
x = (uint16_t)(x + (uint16_t)(int16_t)(int8_t)delta);
sim->spr[idx].col = (uint8_t)x;
sim->spr[idx].msb = (uint8_t)(x >> 8);
}
// $4861 -- the next logo colour, onto sprites 3..7 and onto the logo // $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, // 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 // columns 1..37; only the cells showing the logo character take any
@ -90,7 +110,8 @@ static void logoFlip(StSimT *sim) {
if (sim->logoDiv != 0u) { if (sim->logoDiv != 0u) {
return; return;
} }
memcpy(sim->charset[ST_LOGO_CHAR], sim->charset[ST_LOGO_CHAR + kLogoFlip[sim->logoCycleAux]], 8u); memcpy(stSimGlyphMut(sim, ST_LOGO_CHAR),
stSimGlyph(sim, (uint8_t)(ST_LOGO_CHAR + kLogoFlip[sim->logoCycleAux])), 8u);
stSimMarkChar(sim, ST_LOGO_CHAR); stSimMarkChar(sim, ST_LOGO_CHAR);
sim->logoCycleAux++; sim->logoCycleAux++;
if (sim->logoCycleAux == 6u) { if (sim->logoCycleAux == 6u) {
@ -103,32 +124,21 @@ static void logoFlip(StSimT *sim) {
} }
// $4253 + $4293 as the title and intro loops call them. // Paint the level-intro starfield into screen RAM, once. A star's cell
static void marshalFrame(StSimT *sim) { // is coloured ST_STAR_RING_FIRST + its ring and never touched again --
uint8_t i; // the renderer animates the warp by cycling those colours, which costs
uint8_t enable = 0u; // no dirty rows at all. See ST_STAR_RING_FIRST in stSim.h.
static void paintStarfield(StSimT *sim) {
const StStarT *star;
uint8_t one[2];
uint8_t k;
for (i = 0u; i < ST_HW_SPRITES; i++) { one[1] = 0u; // stSimDrawText stops below screen code 6
sim->frame.x[i] = (uint16_t)(((uint16_t)(sim->spr[i].msb != 0u ? 1u : 0u) << 8) | sim->spr[i].col); for (k = 0u; k < (uint8_t)ST_INTRO_STAR_COUNT; k++) {
sim->frame.y[i] = sim->spr[i].row; star = &kIntroStar[k];
sim->frame.ptr[i] = sim->spr[i].ptr; one[0] = (star->ring < ST_STAR_FAR_RINGS) ? ST_STAR_GLYPH_FAR : ST_STAR_GLYPH_NEAR;
sim->frame.color[i] = sim->spr[i].color; stSimDrawText(sim, star->col, star->row, one, (uint8_t)(ST_STAR_RING_FIRST + star->ring));
if (sim->spr[i].enable != 0u) {
enable |= (uint8_t)(1u << i);
} }
}
sim->frame.enableMask = enable;
sim->frame.multiMask = sim->multiColorMask;
}
// $4675 -- a star reappears near the centre, seeded by star 0's row.
static void starRespawn(StSimT *sim, uint8_t idx) {
sim->spr[idx].enable = 1u;
sim->spr[idx].msb = 0u;
sim->spr[idx].col = (uint8_t)((sim->spr[0].row & 7u) + 0xA6u);
sim->spr[idx].row = (uint8_t)(((sim->spr[0].row & 0x30u) >> 4) + 0x89u);
sim->starReload[idx] = ST_INTRO_STAR_RELOAD;
} }
@ -159,21 +169,26 @@ void stIntroEnter(StSimT *sim, const StLevelT *level) {
sim->spr[idx].color = 0x07u; sim->spr[idx].color = 0x07u;
sim->spr[idx].ptr = ST_INTRO_STAR_PTR; sim->spr[idx].ptr = ST_INTRO_STAR_PTR;
sim->spr[idx].enable = 0u; sim->spr[idx].enable = 0u;
sim->starTimer[idx] = stSimRng(sim, 0x20u); // $44F8/$4500 held each star's spawn timer and reload. Nothing
sim->starReload[idx] = 0u; // reads them now that the warp is painted rather than flown, but
// the seven RNG draws MUST stay: they advance the shared RNG and
// the level that follows draws from it, so dropping them would
// change the game that comes after the intro.
(void)stSimRng(sim, 0x20u);
} }
sim->borderColor = 0u; sim->borderColor = 0u;
sim->bgColor = 0u; sim->bgColor = 0u;
sim->multiColorMask = 0x80u; sim->multiColorMask = 0x80u;
marshalFrame(sim); stSimMarshal(sim);
// $40CA: blank screen. // $40CA: blank screen.
memset(sim->screen, ST_CHAR_SPACE, ST_SCREEN_CELLS); memset(sim->screen, ST_CHAR_SPACE, ST_SCREEN_CELLS);
memset(sim->color, 0, ST_SCREEN_CELLS); memset(sim->color, 0, ST_SCREEN_CELLS);
stSimDirtyAll(sim); stSimDirtyAll(sim);
memcpy(sim->charset, stC64Charset(), sizeof(sim->charset)); stSimGlyphReset(sim);
sim->charDirtyAll = true; sim->charDirtyAll = true;
sim->charDirtyCount = 0u; sim->charDirtyCount = 0u;
stSimDrawText(sim, 10u, 12u, level->name, 3u); stSimDrawText(sim, 10u, 12u, level->name, 3u);
paintStarfield(sim);
if (sim->demoMode != 0u && sim->postMortem == 0u) { if (sim->demoMode != 0u && sim->postMortem == 0u) {
stSimDrawText(sim, 7u, 3u, kTextDemoExit, 5u); stSimDrawText(sim, 7u, 3u, kTextDemoExit, 5u);
stSimDrawText(sim, 3u, 24u, kTextScreens, 4u); stSimDrawText(sim, 3u, 24u, kTextScreens, 4u);
@ -187,27 +202,11 @@ void stIntroEnter(StSimT *sim, const StLevelT *level) {
bool stIntroStep(StSimT *sim) { bool stIntroStep(StSimT *sim) {
uint8_t k; uint8_t k;
for (k = 0u; k < 7u; k++) { stSimMarshal(sim);
uint8_t idx = (uint8_t)(6u - k);
if (sim->starTimer[idx] == 0u) {
addToSpriteX(sim, idx, (uint8_t)stC64IntroStarDx(idx));
addToRow(sim, idx, (uint8_t)stC64IntroStarDy(idx));
sim->starReload[idx]--;
if (sim->starReload[idx] == 0u) {
starRespawn(sim, idx);
}
continue;
}
sim->starTimer[idx]--;
if (sim->starTimer[idx] == 0u) {
starRespawn(sim, idx);
}
}
marshalFrame(sim);
sim->introLoopCount = (uint8_t)((sim->introLoopCount + 1u) & 3u); sim->introLoopCount = (uint8_t)((sim->introLoopCount + 1u) & 3u);
addToRow(sim, 7u, (uint8_t)kIntroCabDy[sim->introLoopCount]); addToRow(sim, 7u, (uint8_t)kIntroCabDy[sim->introLoopCount]);
if ((sim->introLoopCount & 1u) == 0u) { if ((sim->introLoopCount & 1u) == 0u) {
addToSpriteX(sim, 7u, sim->introCabDx); stSimSpriteAddX(sim, 7u, sim->introCabDx);
sim->introCabDxTimer--; sim->introCabDxTimer--;
if (sim->introCabDxTimer == 0u) { if (sim->introCabDxTimer == 0u) {
sim->introCabDxTimer = ST_INTRO_CAB_DX_RELOAD; sim->introCabDxTimer = ST_INTRO_CAB_DX_RELOAD;
@ -222,7 +221,7 @@ bool stIntroStep(StSimT *sim) {
for (k = 0u; k < ST_HW_SPRITES; k++) { for (k = 0u; k < ST_HW_SPRITES; k++) {
sim->spr[k].enable = 0u; sim->spr[k].enable = 0u;
} }
marshalFrame(sim); stSimMarshal(sim);
sim->rngT1 = 0x06u; sim->rngT1 = 0x06u;
sim->rngT2 = 0x17u; sim->rngT2 = 0x17u;
return true; return true;
@ -237,7 +236,7 @@ void stTitleEnter(StSimT *sim, const StLevelT *title) {
memcpy(sim->screen, title->screen, ST_SCREEN_CELLS); memcpy(sim->screen, title->screen, ST_SCREEN_CELLS);
memcpy(sim->color, title->color, ST_SCREEN_CELLS); memcpy(sim->color, title->color, ST_SCREEN_CELLS);
stSimDirtyAll(sim); stSimDirtyAll(sim);
memcpy(sim->charset, stC64Charset(), sizeof(sim->charset)); stSimGlyphReset(sim);
sim->charDirtyAll = true; sim->charDirtyAll = true;
sim->charDirtyCount = 0u; sim->charDirtyCount = 0u;
logoColorCycle(sim); logoColorCycle(sim);
@ -260,7 +259,7 @@ void stTitleEnter(StSimT *sim, const StLevelT *title) {
sim->multiColorMask = 0x07u; sim->multiColorMask = 0x07u;
sim->spriteMc1 = 0x07u; sim->spriteMc1 = 0x07u;
sim->spriteMc0 = 0x02u; sim->spriteMc0 = 0x02u;
marshalFrame(sim); stSimMarshal(sim);
sim->stage = ST_STAGE_WAIT; sim->stage = ST_STAGE_WAIT;
sim->introFrameCount = ST_TITLE_SPARKLE_TICKS; sim->introFrameCount = ST_TITLE_SPARKLE_TICKS;
sim->hoverXFrac = 0u; sim->hoverXFrac = 0u;
@ -313,7 +312,7 @@ bool stTitleTick(StSimT *sim) {
default: default:
break; break;
} }
marshalFrame(sim); stSimMarshal(sim);
logoFlip(sim); logoFlip(sim);
return sim->stage == ST_STAGE_DROP_IN; return sim->stage == ST_STAGE_DROP_IN;
} }

View file

@ -1062,11 +1062,13 @@ static void PROBE_ATTR op_probeCleanErases(void) {
// Wrap a probe timeOp with counter-delta logging. The IIgs skips the // Wrap a probe timeOp with counter-delta logging. The IIgs skips the
// wrapper entirely: its present is asm and never touches the // wrapper entirely: its present is asm and never touches the
// counters (always zero there), and the IIgs -DUBER_PROBE build dies // counters (always zero there). The IIgs -DUBER_PROBE build also used
// before main when this block is compiled in (bisected 2026-07-07: // to die mid-run (bisected 2026-07-07, blamed on this wrapper; the real
// trivial-op probe launches, counter/fn-ptr wrapper variants do not, // cause, found 2026-09-22, was hal.c asking MMStartUp for a NEW Memory
// optnone does not help -- llvm-mos issue, flagged to Scott). Direct // Manager ID on every allocation until the ID pool ran dry and the
// timeOp calls carry the IIgs probe rows instead. // gameFrameClean row's jlSurfaceCreate BRK'd -- fixed in hal.c). The
// direct-timeOp IIgs arm below is a near duplicate of this path that
// could now be folded back in.
#ifndef JOEYLIB_PLATFORM_IIGS #ifndef JOEYLIB_PLATFORM_IIGS
static void PROBE_ATTR probeOp(const char *name, void (*fn)(void)) { static void PROBE_ATTR probeOp(const char *name, void (*fn)(void)) {
uint32_t bytes0; uint32_t bytes0;
@ -1791,10 +1793,9 @@ static void __attribute__((noinline)) runAllTests(void) {
// for mark-path changes. IIgs: no counter wrapper (see probeOp), // for mark-path changes. IIgs: no counter wrapper (see probeOp),
// no P5 (mark inertness there is proven by object-code compare). // no P5 (mark inertness there is proven by object-code compare).
#ifdef JOEYLIB_PLATFORM_IIGS #ifdef JOEYLIB_PLATFORM_IIGS
// Direct delta logging: the probeOp fn-pointer wrapper dies before // Direct delta logging (kept from the days the probeOp wrapper was
// main on llvm-mos (W0 bisect); direct extern reads are the // blamed for the IIgs probe death; see probeOp). Same rows +
// bisected-safe shape. Same rows + steady-delta protocol as the // steady-delta protocol as the probeOp arm below.
// probeOp arm below.
{ {
uint32_t probeBytes0; uint32_t probeBytes0;
uint32_t probeRows0; uint32_t probeRows0;
@ -1821,8 +1822,8 @@ static void __attribute__((noinline)) runAllTests(void) {
// SaveAndDraw rows, so each diff vs its golden row = the // SaveAndDraw rows, so each diff vs its golden row = the
// validation the trusted entry skips. Placed BEFORE the present // validation the trusted entry skips. Placed BEFORE the present
// pair below so the stage writes these do are re-synced to SHR // pair below so the stage writes these do are re-synced to SHR
// before op_probeShrParity. timeOp (not probeOp) because the // before op_probeShrParity. timeOp (not probeOp) to match the
// probeOp fn-pointer wrapper dies pre-main on llvm-mos. // rest of this arm (see probeOp).
gProbeRestoreBackup.sprite = gSprite; gProbeRestoreBackup.sprite = gSprite;
gProbeRestoreBackup.bytes = gProbeRestoreBackupBytes; gProbeRestoreBackup.bytes = gProbeRestoreBackupBytes;
jlSpriteSaveUnder(gStage, gSprite, 40, 30, &gProbeRestoreBackup); jlSpriteSaveUnder(gStage, gSprite, 40, 30, &gProbeRestoreBackup);
@ -1844,8 +1845,8 @@ static void __attribute__((noinline)) runAllTests(void) {
// 16-21 ms unattributed residual). Same recipe as the golden // 16-21 ms unattributed residual). Same recipe as the golden
// row (gCleanSurface was destroyed after it): re-create with // row (gCleanSurface was destroyed after it): re-create with
// clean == stage everywhere, run the two split rows via direct // clean == stage everywhere, run the two split rows via direct
// timeOp (the probeOp wrapper dies pre-main on llvm-mos), // timeOp (this arm does not use probeOp; see probeOp), destroy
// destroy again. // again.
gCleanSurface = jlSurfaceCreate(); gCleanSurface = jlSurfaceCreate();
if (gCleanSurface != NULL) { if (gCleanSurface != NULL) {
paintFrameStatic(); paintFrameStatic();

View file

@ -68,11 +68,17 @@ bool jlAudioIsPlayingMod(void);
// the sample was recorded at; the engine pitches as needed for its // the sample was recorded at; the engine pitches as needed for its
// own output rate. If the slot is currently playing, the new sample // own output rate. If the slot is currently playing, the new sample
// replaces it. // replaces it.
//
// The full -128..127 range is accepted everywhere, but the IIgs plays
// -128 as -127: its DOC reads 0x00 (which is what -128 becomes in the
// chip's unsigned format) as a wave-end marker and would halt the voice
// on that sample. One level out of 256, inaudible in practice.
void jlAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz); void jlAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz);
// Stream-fill callback signature. Called by the mixer to refill a // Stream-fill callback signature. Called by the mixer to refill a
// streaming slot's prefetch buffer; should write up to `count` signed // streaming slot's prefetch buffer; should write up to `count` signed
// 8-bit samples to `dst` and return the number written. Returning 0 // 8-bit samples to `dst` and return the number written (see
// jlAudioPlaySfx for the one clamp the IIgs applies). Returning 0
// signals end-of-stream and the slot auto-deactivates. Called from // signals end-of-stream and the slot auto-deactivates. Called from
// the same context as the audio engine's refill (main thread on // the same context as the audio engine's refill (main thread on
// DOS/ST, audio ISR on Amiga/IIgs); must not block or allocate. // DOS/ST, audio ISR on Amiga/IIgs); must not block or allocate.
@ -84,10 +90,55 @@ typedef uint32_t (*jlAudioStreamFillT)(void *ctx, int8_t *dst, uint32_t count);
// allocation. `rateHz` is the sample rate `fill` produces; the // allocation. `rateHz` is the sample rate `fill` produces; the
// engine resamples (with linear interpolation) to its own output // engine resamples (with linear interpolation) to its own output
// rate. If the slot is currently playing, the new stream replaces // rate. If the slot is currently playing, the new stream replaces
// it. No-op on ports whose audio engine doesn't expose the shared // it. Implemented on every port: DOS/ST/X68000 pull through the shared
// SFX overlay mixer. // SFX overlay mixer, while the IIgs (NTPstreamsound) and Amiga
// (PTPlayer mt_playfx) play one chunk at a time and fire the next from
// jlAudioFrameTick as the current one runs down -- so on those two the
// host must be calling jlAudioFrameTick for a stream to continue past
// its first chunk, and a seam can carry up to one frame of jitter.
void jlAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz); void jlAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz);
// Raw-format streaming: jlAudioPlaySfxStream, except `fill` delivers
// bytes already in the port's raw stream format (jlAudioSfxRawFormat),
// so the port skips its per-byte conversion of every chunk. On the IIgs
// that conversion is a 65816 byte loop over each 3.5 KB chunk -- it
// stalled Space Taxi's ride for a third of a second per chunk of speech.
// Every other port streams signed PCM natively, so there Raw is the
// plain call. A producer that cannot make the port's format uses
// jlAudioPlaySfxStream.
#define JL_SFX_RAW_SIGNED8 0u // int8_t PCM, the jlAudioPlaySfxStream contract
#define JL_SFX_RAW_UNSIGNED8_NOZERO 1u // uint8_t PCM biased 0x80; 0x00 is forbidden (DOC stop byte)
uint8_t jlAudioSfxRawFormat(void);
void jlAudioPlaySfxStreamRaw(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz);
// Stereo placement for a SFX slot.
typedef enum {
AUDIO_PAN_LEFT = 0,
AUDIO_PAN_CENTER,
AUDIO_PAN_RIGHT,
AUDIO_PAN_COUNT
} jlAudioPanE;
// Place a SFX slot in the stereo field. Slots start at AUDIO_PAN_CENTER, and
// the setting sticks to the SLOT, so it survives retriggers -- set it once
// when a sound is meant to come from one side.
//
// What each port can actually do, because the hardware differs:
// IIgs all three. The DOC pans per OSCILLATOR (control bits 7:4 are the
// output channel, and per Apple TN #19 odd = left, even = right),
// so a centred sound is played on two oscillators, one per side.
// A stock IIgs sums to mono anyway; this only shows up on a stereo
// card or an emulator.
// Amiga left and right, by picking the Paula channel side (0 and 3 are
// left, 1 and 2 right). Paula cannot place one voice in both ears,
// so AUDIO_PAN_CENTER keeps PTPlayer's own channel choice.
// X68000 ignored. Everything is mixed into ONE MSM6258 ADPCM stream, so
// its pan bits would move all audio at once, not one slot.
// DOS, ST ignored -- their output is mono.
// Passing an out-of-range pan is ignored.
void jlAudioSetPan(uint8_t slot, jlAudioPanE pan);
// Stop a SFX slot early. No-op if the slot is already idle. // Stop a SFX slot early. No-op if the slot is already idle.
void jlAudioStopSfx(uint8_t slot); void jlAudioStopSfx(uint8_t slot);

View file

@ -47,6 +47,11 @@ void *jlAlloc(uint32_t bytes);
// Release a buffer returned by jlAlloc. NULL is accepted and ignored. // Release a buffer returned by jlAlloc. NULL is accepted and ignored.
void jlFree(void *p); void jlFree(void *p);
// memcpy for big buffers. Same contract as memcpy (no overlap), but the
// IIgs runs it as MVN block moves (7 cycles a byte) instead of the libc
// byte loop, split at bank boundaries by the port.
void jlMemCopy(void *dst, const void *src, uint32_t bytes);
// Block the calling thread until the next display vertical blank. // Block the calling thread until the next display vertical blank.
// Used to pace game loops to the display's native refresh rate // Used to pace game loops to the display's native refresh rate
// (~70 Hz on VGA mode 13h, ~50 or ~60 Hz on Amiga/ST PAL/NTSC, ~60 Hz // (~70 Hz on VGA mode 13h, ~50 or ~60 Hz on Amiga/ST PAL/NTSC, ~60 Hz

View file

@ -134,7 +134,7 @@
#define JL_HAS_STAGE_FREE_PIXELS // no-op (pinned), function #define JL_HAS_STAGE_FREE_PIXELS // no-op (pinned), function
#define JL_HAS_FRAME_HZ // runtime 50/60 from battery RAM, function #define JL_HAS_FRAME_HZ // runtime 50/60 from battery RAM, function
// (frameCount: port.h #defines jlpFrameCount straight to iigsGetTickWord, // (frameCount: port.h #defines jlpFrameCount straight to iigsGetTickWord,
// the GetTick asm wrapper -- no function override to register here.) // the VBL heartbeat tick read -- no function override to register here.)
#define JL_HAS_WAIT_VBL // $C019 VBL poll, function #define JL_HAS_WAIT_VBL // $C019 VBL poll, function
// audio: every real port implements the full engine // audio: every real port implements the full engine
#define JL_HAS_AUDIO_INIT #define JL_HAS_AUDIO_INIT
@ -144,6 +144,8 @@
#define JL_HAS_AUDIO_IS_PLAYING_MOD #define JL_HAS_AUDIO_IS_PLAYING_MOD
#define JL_HAS_AUDIO_PLAY_SFX #define JL_HAS_AUDIO_PLAY_SFX
#define JL_HAS_AUDIO_PLAY_SFX_STREAM #define JL_HAS_AUDIO_PLAY_SFX_STREAM
#define JL_HAS_AUDIO_PLAY_SFX_STREAM_RAW
#define JL_HAS_AUDIO_SET_PAN
#define JL_HAS_AUDIO_STOP_SFX #define JL_HAS_AUDIO_STOP_SFX
#define JL_HAS_AUDIO_TONE #define JL_HAS_AUDIO_TONE
#define JL_HAS_AUDIO_VOICE #define JL_HAS_AUDIO_VOICE
@ -216,6 +218,7 @@
#define JL_HAS_AUDIO_IS_PLAYING_MOD #define JL_HAS_AUDIO_IS_PLAYING_MOD
#define JL_HAS_AUDIO_PLAY_SFX #define JL_HAS_AUDIO_PLAY_SFX
#define JL_HAS_AUDIO_PLAY_SFX_STREAM #define JL_HAS_AUDIO_PLAY_SFX_STREAM
#define JL_HAS_AUDIO_SET_PAN
#define JL_HAS_AUDIO_STOP_SFX #define JL_HAS_AUDIO_STOP_SFX
#define JL_HAS_AUDIO_TONE #define JL_HAS_AUDIO_TONE
#define JL_HAS_AUDIO_VOICE #define JL_HAS_AUDIO_VOICE

View file

@ -88,7 +88,7 @@ ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c
ADV2_BIN := $(BINDIR)/Adv2 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_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 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 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_INSTALL_DIR := $(BINDIR)/Taxi
STAXI_BIN := $(STAXI_INSTALL_DIR)/Taxi STAXI_BIN := $(STAXI_INSTALL_DIR)/Taxi
@ -130,6 +130,24 @@ SPRITEBAKE_BIN := $(REPO_DIR)/build/tools/spritebake-amiga
# stCels.c + C64 bitmaps) and compiled to a .spc by the host spritebake, so # 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. # STAXI loads its cels already compiled instead of JIT-compiling them at boot.
STAXIBAKE_BIN := $(REPO_DIR)/build/tools/staxibake 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_SPR_GEN := $(STAXI_GEN_DIR)/sprites/staxicels.spr
STAXICELS_SPC_GEN := $(STAXICELS_SPR_GEN:.spr=.spc) STAXICELS_SPC_GEN := $(STAXICELS_SPR_GEN:.spr=.spc)
STAXICELS_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXICELS_SPC_GEN)) STAXICELS_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXICELS_SPC_GEN))
@ -251,7 +269,7 @@ $(STAXI_BIN): $(STAXI_SRCS) $(LIB)
# Space Taxi asset pipeline. .SECONDARY keeps the generated/ artifacts # Space Taxi asset pipeline. .SECONDARY keeps the generated/ artifacts
# from being auto-deleted after staging. # from being auto-deleted after staging.
.SECONDARY: $(STAXI_LEVEL_GEN) $(STAXI_TBK_GEN) $(STAXI_SPR_GEN) $(STAXI_SPC_GEN) $(STAXICELS_SPC_GEN) .SECONDARY: $(STAXI_LEVEL_GEN) $(STAXI_TBK_GEN) $(STAXI_SPR_GEN) $(STAXI_SPC_GEN) $(STAXICELS_SPC_GEN) $(STAXILEVEL_SPR_GEN) $(STAXILEVEL_SPC_GEN)
$(STAXI_ROM_LEVEL_GEN) &: $(STAXI_RAW_DUMP) $(ROMTOLEVEL_BIN) $(STAXI_ROM_LEVEL_GEN) &: $(STAXI_RAW_DUMP) $(ROMTOLEVEL_BIN)
@mkdir -p $(STAXI_GEN_DIR)/levels @mkdir -p $(STAXI_GEN_DIR)/levels
@ -318,3 +336,16 @@ clean: clean-amiga
# the .c files that include it, leaving a frankenstein binary where # the .c files that include it, leaving a frankenstein binary where
# different TUs see different struct layouts. # different TUs see different struct layouts.
-include $(LIB_OBJS:.o=.d) -include $(LIB_OBJS:.o=.d)
# 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 $< $@
# 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 $< $@

View file

@ -75,7 +75,7 @@ ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c
ADV2_BIN := $(BINDIR)/ADV2.PRG 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_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 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 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_INSTALL_DIR := $(BINDIR)/STAXI
STAXI_BIN := $(STAXI_INSTALL_DIR)/STAXI.PRG STAXI_BIN := $(STAXI_INSTALL_DIR)/STAXI.PRG
@ -110,6 +110,24 @@ SPRITEBAKE_BIN := $(REPO_DIR)/build/tools/spritebake-atarist
# stCels.c + C64 bitmaps) and compiled to a .spc by the host spritebake, so # 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. # STAXI loads its cels already compiled instead of JIT-compiling them at boot.
STAXIBAKE_BIN := $(REPO_DIR)/build/tools/staxibake 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_SPR_GEN := $(STAXI_GEN_DIR)/sprites/staxicels.spr
STAXICELS_SPC_GEN := $(STAXICELS_SPR_GEN:.spr=.spc) STAXICELS_SPC_GEN := $(STAXICELS_SPR_GEN:.spr=.spc)
STAXICELS_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXICELS_SPC_GEN)) STAXICELS_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXICELS_SPC_GEN))
@ -237,7 +255,7 @@ $(STAXI_BIN): $(STAXI_SRCS) $(LIB) $(LIBXMP_AR)
$(ST_CC) $(CFLAGS) $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) $(ST_CC) $(CFLAGS) $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS)
# Space Taxi asset pipeline (mirrors dos.mk + amiga.mk). # Space Taxi asset pipeline (mirrors dos.mk + amiga.mk).
.SECONDARY: $(STAXI_LEVEL_GEN) $(STAXI_TBK_GEN) $(STAXI_SPR_GEN) $(STAXI_SPC_GEN) $(STAXICELS_SPC_GEN) .SECONDARY: $(STAXI_LEVEL_GEN) $(STAXI_TBK_GEN) $(STAXI_SPR_GEN) $(STAXI_SPC_GEN) $(STAXICELS_SPC_GEN) $(STAXILEVEL_SPR_GEN) $(STAXILEVEL_SPC_GEN)
$(STAXI_ROM_LEVEL_GEN) &: $(STAXI_RAW_DUMP) $(ROMTOLEVEL_BIN) $(STAXI_ROM_LEVEL_GEN) &: $(STAXI_RAW_DUMP) $(ROMTOLEVEL_BIN)
@mkdir -p $(STAXI_GEN_DIR)/levels @mkdir -p $(STAXI_GEN_DIR)/levels
@ -304,3 +322,16 @@ clean: clean-atarist
# the .c files that include it, leaving a frankenstein binary where # the .c files that include it, leaving a frankenstein binary where
# different TUs see different struct layouts. # different TUs see different struct layouts.
-include $(LIB_OBJS:.o=.d) -include $(LIB_OBJS:.o=.d)
# 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 $< $@
# 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 $< $@

View file

@ -71,7 +71,7 @@ ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c
ADV2_BIN := $(BINDIR)/ADV2.EXE 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_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 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 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 # JoeyLib install convention: each app lives in its own bin subdir
# with a DATA/ folder alongside the binary for runtime assets. # with a DATA/ folder alongside the binary for runtime assets.
STAXI_INSTALL_DIR := $(BINDIR)/STAXI STAXI_INSTALL_DIR := $(BINDIR)/STAXI
@ -131,6 +131,24 @@ SPRITEBAKE_BIN := $(REPO_DIR)/build/tools/spritebake-dos
# stCels.c + C64 bitmaps) and compiled to a .spc by the host spritebake, so # 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. # STAXI loads its cels already compiled instead of JIT-compiling them at boot.
STAXIBAKE_BIN := $(REPO_DIR)/build/tools/staxibake 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_SPR_GEN := $(STAXI_GEN_DIR)/sprites/staxicels.spr
STAXICELS_SPC_GEN := $(STAXICELS_SPR_GEN:.spr=.spc) STAXICELS_SPC_GEN := $(STAXICELS_SPR_GEN:.spr=.spc)
STAXICELS_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXICELS_SPC_GEN)) STAXICELS_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXICELS_SPC_GEN))
@ -253,7 +271,7 @@ $(STAXI_BIN): $(STAXI_SRCS) $(LIB) $(LIBXMP_AR)
# Mark these .SECONDARY so make doesn't auto-delete them after the # Mark these .SECONDARY so make doesn't auto-delete them after the
# stage-B copy into bin/ -- they're the canonical distribution # stage-B copy into bin/ -- they're the canonical distribution
# artifacts. # artifacts.
.SECONDARY: $(STAXI_LEVEL_GEN) $(STAXI_TBK_GEN) $(STAXI_SPR_GEN) $(STAXI_SPC_GEN) $(STAXICELS_SPC_GEN) .SECONDARY: $(STAXI_LEVEL_GEN) $(STAXI_TBK_GEN) $(STAXI_SPR_GEN) $(STAXI_SPC_GEN) $(STAXICELS_SPC_GEN) $(STAXILEVEL_SPR_GEN) $(STAXILEVEL_SPC_GEN)
# Bulk-extract all 24 canonical levels plus the title from the C64 # Bulk-extract all 24 canonical levels plus the title from the C64
# ROM in one invocation. Grouped target (&:) means make treats the # ROM in one invocation. Grouped target (&:) means make treats the
@ -335,3 +353,16 @@ clean-spacetaxi:
# the .c files that include it, leaving a frankenstein binary where # the .c files that include it, leaving a frankenstein binary where
# different TUs see different struct layouts. # different TUs see different struct layouts.
-include $(LIB_OBJS:.o=.d) -include $(LIB_OBJS:.o=.d)
# 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 $< $@
# 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 $< $@

View file

@ -66,7 +66,7 @@ UBER_SRC := $(EXAMPLES)/uber/uber.c
ADV_SRC := $(EXAMPLES)/adventure/adventure.c ADV_SRC := $(EXAMPLES)/adventure/adventure.c
ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.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 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 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 $(EXAMPLES)/spacetaxi/stMarshalIigs.s
AUDIO_SRC := $(EXAMPLES)/audio/audio.c AUDIO_SRC := $(EXAMPLES)/audio/audio.c
# NinjaTrackerPlus replayer: Merlin32-assemble ninjatrackerplus.s to a 34KB # NinjaTrackerPlus replayer: Merlin32-assemble ninjatrackerplus.s to a 34KB
@ -113,6 +113,17 @@ iigs-examples: $(BINDIR)/PATTERN $(BINDIR)/DRAW $(BINDIR)/KEYS $(BINDIR)/SERIAL
# (jlSpriteBankLoadPrecompiled) instead of JIT-compiling every cel. # (jlSpriteBankLoadPrecompiled) instead of JIT-compiling every cel.
STAXICELS_SPR := $(EXAMPLES)/spacetaxi/generated/iigs/sprites/staxicels.spr STAXICELS_SPR := $(EXAMPLES)/spacetaxi/generated/iigs/sprites/staxicels.spr
STAXI_IIGS_SPC := $(STAXICELS_SPR:.spr=.spc) 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 like font.tbk.
STAXI_IIGS_SPEECH := $(EXAMPLES)/spacetaxi/generated/iigs/speech.bin
$(STAXI_IIGS_SPEECH): $(EXAMPLES)/spacetaxi/stSpeechData.c $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py
python3 $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py $< $@ --biased
SPRITEBAKE_IIGS := $(REPO_DIR)/build/tools/spritebake-iigs SPRITEBAKE_IIGS := $(REPO_DIR)/build/tools/spritebake-iigs
STAXIBAKE := $(REPO_DIR)/build/tools/staxibake STAXIBAKE := $(REPO_DIR)/build/tools/staxibake
@ -126,10 +137,14 @@ $(STAXICELS_SPR): $(STAXIBAKE) $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacet
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(STAXIBAKE) $@ $(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) $(EXAMPLES)/spacetaxi/generated/iigs/sprites/%.spc: $(EXAMPLES)/spacetaxi/generated/iigs/sprites/%.spr $(SPRITEBAKE_IIGS)
$(SPRITEBAKE_IIGS) $< $@ $(SPRITEBAKE_IIGS) $< $@
iigs-disk: iigs-examples $(NTP_BIN) $(STAXI_IIGS_SPC) iigs-disk: iigs-examples $(NTP_BIN) $(STAXI_IIGS_SPC) $(STAXILEVEL_IIGS_SPC) $(STAXI_IIGS_SPEECH)
BINDIR="$(BINDIR)" NTP_BIN="$(NTP_BIN)" $(REPO_DIR)/scripts/make-iigs-disk.sh $(BINDIR)/joey.2mg 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 (~1.7 MB of binaries # Every launchable example on one image. The full set (~1.7 MB of binaries
@ -137,7 +152,7 @@ iigs-disk: iigs-examples $(NTP_BIN) $(STAXI_IIGS_SPC)
# ProDOS hard-disk image (joey-all.2mg): boot it in MAME with # 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 # `-sl7 cffa2 -hard1 <GS/OS> -hard2 build/iigs/bin/joey-all.2mg` and launch
# any example from the JOEYLIB volume in the Finder. # any example from the JOEYLIB volume in the Finder.
iigs-disk-all: iigs-examples $(NTP_BIN) $(STAXI_IIGS_SPC) iigs-disk-all: iigs-examples $(NTP_BIN) $(STAXI_IIGS_SPC) $(STAXILEVEL_IIGS_SPC) $(STAXI_IIGS_SPEECH)
BINDIR="$(BINDIR)" NTP_BIN="$(NTP_BIN)" \ BINDIR="$(BINDIR)" NTP_BIN="$(NTP_BIN)" \
JOEY_DISK_SIZE="16MB" \ JOEY_DISK_SIZE="16MB" \
JOEY_DISK_EXAMPLES="DRAW PATTERN KEYS JOY SPRITE SERIAL SERTEST SAVE AUDIO UBER ADV2 ADV AGI STAXI" \ JOEY_DISK_EXAMPLES="DRAW PATTERN KEYS JOY SPRITE SERIAL SERTEST SAVE AUDIO UBER ADV2 ADV AGI STAXI" \
@ -291,10 +306,26 @@ $(BINDIR)/ADV2: $(ADV2_SRC) $(LIB) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR) @mkdir -p $(dir $@) $(DEP_DIR)
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/ADV2.d $(INCLUDES) -o $@ $(ADV2_SRC) $(LIB) $(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) $(BINDIR)/AGI: $(AGI_SRCS) $(LIB) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR) @mkdir -p $(dir $@) $(DEP_DIR)
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/AGI.d $(INCLUDES) -I$(EXAMPLES)/agi -o $@ $(AGI_SRCS) $(LIB) $(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) $(BINDIR)/STAXI: $(STAXI_SRCS) $(LIB) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR) @mkdir -p $(dir $@) $(DEP_DIR)
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/STAXI.d $(INCLUDES) -I$(EXAMPLES)/spacetaxi -o $@ $(STAXI_SRCS) $(LIB) $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/STAXI.d $(INCLUDES) -I$(EXAMPLES)/spacetaxi -o $@ $(STAXI_SRCS) $(LIB)

View file

@ -29,7 +29,7 @@ MKSTLEVEL_BIN := $(BUILD_DIR)/mkstlevel
# plain host tool (the .spr is cross-platform); spritebake then compiles # plain host tool (the .spr is cross-platform); spritebake then compiles
# it per target. # it per target.
STAXI_DIR := $(REPO_DIR)/examples/spacetaxi STAXI_DIR := $(REPO_DIR)/examples/spacetaxi
STAXIBAKE_SRC := $(TOOLS_DIR)/staxibake/staxibake.c $(STAXI_DIR)/stCels.c $(STAXI_DIR)/stC64Data.c 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 STAXIBAKE_BIN := $(BUILD_DIR)/staxibake
# spritebake -- offline sprite pre-compiler. Built ONCE PER TARGET on the # spritebake -- offline sprite pre-compiler. Built ONCE PER TARGET on the
@ -42,7 +42,7 @@ SPRITEBAKE_SRC := $(TOOLS_DIR)/spritebake/spritebake.c $(SRC_CG)/spriteStage.c
SPRITEBAKE_INC := -I$(INCLUDE_DIR) -I$(SRC_CORE) -I$(SRC_CG) 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 mkstlevel staxibake \
spritebake-dos spritebake-amiga spritebake-atarist spritebake-iigs spritebake-dos spritebake-amiga spritebake-atarist spritebake-iigs spritebake-x68000
all tools: $(MODLOOPEND_BIN) $(MKSTLEVEL_BIN) all tools: $(MODLOOPEND_BIN) $(MKSTLEVEL_BIN)
modloopend: $(MODLOOPEND_BIN) modloopend: $(MODLOOPEND_BIN)
mkstlevel: $(MKSTLEVEL_BIN) mkstlevel: $(MKSTLEVEL_BIN)
@ -51,6 +51,7 @@ spritebake-dos: $(BUILD_DIR)/spritebake-dos
spritebake-amiga: $(BUILD_DIR)/spritebake-amiga spritebake-amiga: $(BUILD_DIR)/spritebake-amiga
spritebake-atarist: $(BUILD_DIR)/spritebake-atarist spritebake-atarist: $(BUILD_DIR)/spritebake-atarist
spritebake-iigs: $(BUILD_DIR)/spritebake-iigs spritebake-iigs: $(BUILD_DIR)/spritebake-iigs
spritebake-x68000: $(BUILD_DIR)/spritebake-x68000
$(MODLOOPEND_BIN): $(MODLOOPEND_SRC) $(MODLOOPEND_BIN): $(MODLOOPEND_SRC)
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
@ -72,6 +73,12 @@ $(BUILD_DIR)/spritebake-amiga: $(SPRITEBAKE_SRC) $(SRC_68K)/spriteEmitPlanar68k.
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(HOST_CC) $(HOST_CFLAGS) -DJOEYLIB_PLATFORM_AMIGA $(SPRITEBAKE_INC) -I$(SRC_DIR)/amiga -I$(SRC_68K) $^ -o $@ $(HOST_CC) $(HOST_CFLAGS) -DJOEYLIB_PLATFORM_AMIGA $(SPRITEBAKE_INC) -I$(SRC_DIR)/amiga -I$(SRC_68K) $^ -o $@
# The X68000 runs the Amiga's planar 68k emitter unchanged (see
# make/x68000.mk), so its bake is the same source under its own target byte.
$(BUILD_DIR)/spritebake-x68000: $(SPRITEBAKE_SRC) $(SRC_68K)/spriteEmitPlanar68k.c
@mkdir -p $(dir $@)
$(HOST_CC) $(HOST_CFLAGS) -DJOEYLIB_PLATFORM_X68000 $(SPRITEBAKE_INC) -I$(SRC_DIR)/x68000 -I$(SRC_68K) $^ -o $@
$(BUILD_DIR)/spritebake-atarist: $(SPRITEBAKE_SRC) $(SRC_68K)/spriteEmitInterleaved68k.c $(BUILD_DIR)/spritebake-atarist: $(SPRITEBAKE_SRC) $(SRC_68K)/spriteEmitInterleaved68k.c
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(HOST_CC) $(HOST_CFLAGS) -DJOEYLIB_PLATFORM_ATARIST $(SPRITEBAKE_INC) -I$(SRC_DIR)/atarist -I$(SRC_68K) $^ -o $@ $(HOST_CC) $(HOST_CFLAGS) -DJOEYLIB_PLATFORM_ATARIST $(SPRITEBAKE_INC) -I$(SRC_DIR)/atarist -I$(SRC_68K) $^ -o $@

View file

@ -95,7 +95,7 @@ SERIAL_SRC := $(EXAMPLES)/serial/serial.c
UBER_SRC := $(EXAMPLES)/uber/uber.c UBER_SRC := $(EXAMPLES)/uber/uber.c
AUDIO_SRC := $(EXAMPLES)/audio/audio.c AUDIO_SRC := $(EXAMPLES)/audio/audio.c
KEYS_SRC := $(EXAMPLES)/keys/keys.c KEYS_SRC := $(EXAMPLES)/keys/keys.c
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 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-golden clean-x68000 clean .PHONY: all x68000 x68000-lib x68000-examples x68000-verify-serial x68000-verify-mouse x68000-verify-golden clean-x68000 clean
@ -167,15 +167,88 @@ $(BINDIR)/KEYS.X: $(KEYS_SRC) $(LIB) $(LIBXMP_AR)
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(X68K_CC) $(CFLAGS) $(KEYS_SRC) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@ $(X68K_CC) $(CFLAGS) $(KEYS_SRC) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@
# 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 := $(BINDIR)/DATA
STAXI_RAW_DUMP := $(REPO_DIR)/stuff/spacetaxi/raw.bin
ROMTOLEVEL_BIN := $(REPO_DIR)/stuff/spacetaxi/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
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)
# 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)
$(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 $< $@
# Space Taxi. -s (strip) because the image it ships on is a 1232 KB # 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 # Human68k floppy: the unstripped binary is most of the volume on its
# own. No precompiled sprite bank is staged -- this port has a real 68k # own. The DATA tree next to it (levels + the precompiled cel bank) is
# planar emitter, so the boot prewarm JITs its cels in well under a # what scripts/run-x68000.sh copies onto the data floppy.
# second (the reason the IIgs needs a baked .spc is its ~0.4 s per cel). $(BINDIR)/STAXI.X: $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) $(STAXI_ASSET_DSTS)
$(BINDIR)/STAXI.X: $(STAXI_SRCS) $(LIB) $(LIBXMP_AR)
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(X68K_CC) $(CFLAGS) -s -I$(EXAMPLES)/spacetaxi $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@ $(X68K_CC) $(CFLAGS) -s -I$(EXAMPLES)/spacetaxi $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@
# 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)
$(STAXI_LEVEL_GEN) &: $(STAXI_RAW_DUMP) $(ROMTOLEVEL_BIN)
@mkdir -p $(STAXI_GEN_DIR)/levels
python3 $(ROMTOLEVEL_BIN) $(STAXI_RAW_DUMP) $(STAXI_GEN_DIR)/levels
$(SPRITEBAKE_BIN):
$(MAKE) -f $(REPO_DIR)/make/tools.mk spritebake-x68000
$(STAXIBAKE_BIN):
$(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 $< $@
# Both-directions RS-232C gate (~1 min). # Both-directions RS-232C gate (~1 min).
x68000-verify-serial: $(BINDIR)/SERIAL.X x68000-verify-serial: $(BINDIR)/SERIAL.X
$(REPO_DIR)/scripts/verify-x68000-serial.sh $(REPO_DIR)/scripts/verify-x68000-serial.sh
@ -199,3 +272,8 @@ clean-x68000:
clean: clean-x68000 clean: clean-x68000
-include $(shell find $(BUILD) -name '*.d' 2>/dev/null) -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 $< $@

69
scripts/make-iigs-boot-disk.sh Executable file
View file

@ -0,0 +1,69 @@
#!/usr/bin/env bash
# make-iigs-boot-disk.sh - Build a GS/OS boot floppy that launches one
# JoeyLib example DIRECTLY, with no Finder in the way.
#
# WHY: the GS/OS Finder cannot be driven from a headless harness. Under
# gssquared, injected keystrokes reach the $C000 latch but never reach the
# Finder -- Open-Apple-A, Tab, arrow keys and plain letters all leave the
# desktop untouched (the single redraw seen after the first key of a run is
# the Finder's own settling: it was byte-identical for three different
# keys). Reaching the desktop also costs ~61s of boot per run. MAME's Lua
# harness drives the Finder by a different route, but that is one more
# moving part per probe.
#
# This replaces /System/Start (normally the Finder) with tools/iigsStart,
# whose whole job is a GS/OS QuitGS to the pathname baked in at build time.
# Boot the result as the FIRST drive and the example's own disk as the
# second; the example volume is mounted before Start runs.
#
# Usage: scripts/make-iigs-boot-disk.sh <out.po> <EXAMPLE> [volume]
# EXAMPLE uppercase binary name as it appears on the data disk
# volume data volume name, default JOEYLIB
# Requires: toolchains/env.sh sourced.
set -euo pipefail
out=${1:?usage: make-iigs-boot-disk.sh <out.po> <EXAMPLE> [volume]}
prog=${2:?usage: make-iigs-boot-disk.sh <out.po> <EXAMPLE> [volume]}
vol=${3:-JOEYLIB}
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
: "${LLVM816_ROOT:?make-iigs-boot-disk.sh: source toolchains/env.sh first}"
CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}"
sysDisk="$repo/toolchains/emulators/support/gsos-system.po"
[ -x "$CADIUS" ] || { echo "make-iigs-boot-disk.sh: cadius not found at $CADIUS" >&2; exit 2; }
[ -f "$sysDisk" ] || { echo "make-iigs-boot-disk.sh: missing $sysDisk" >&2; exit 2; }
work=$(mktemp -d -t joeylib-boot.XXXXXX)
trap 'rm -rf "$work"' EXIT
# The GS/OS system disk's own volume name, which cadius paths are relative to.
# Match the volume line, which is a WHOLE line of the form "/NAME/". An
# unanchored match also hits the absolute image path in cadius's header
# (it yielded "home" from /home/scott/...).
sysVol=$("$CADIUS" CATALOG "$sysDisk" 2>/dev/null \
| grep -oE '^/[A-Za-z0-9._]+/$' | head -1 | tr -d '/')
[ -n "$sysVol" ] || { echo "make-iigs-boot-disk.sh: cannot read system volume name" >&2; exit 1; }
cp "$sysDisk" "$out"
# Out with the Finder (~200KB), in with the launcher. Deleting first is what
# makes room: the stock disk has under 50KB free.
"$CADIUS" DELETEFILE "$out" "/$sysVol/System/Start" >/dev/null
"$repo/toolchains/iigs/clang-build.sh" \
-DJOEY_START_PATH="\"/$vol/$prog\"" \
-o "$work/Start#B30000" \
"$repo/tools/iigsStart/iigsStart.c" \
"$repo/tools/iigsStart/iigsStartQuit.s" >/dev/null
"$CADIUS" ADDFILE "$out" "/$sysVol/System" "$work/Start#B30000" >/dev/null
# Never trust the exit code of a disk build here -- cadius reports success
# for a file it silently dropped when the volume was full.
if ! "$CADIUS" CATALOG "$out" 2>/dev/null | grep -qE '^ +Start +S16'; then
echo "make-iigs-boot-disk.sh: Start was not written (volume full?)" >&2
exit 1
fi
echo "iigs-boot-disk: $out (boots straight into /$vol/$prog)"

View file

@ -17,6 +17,12 @@ repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
: "${LLVM816_ROOT:?make-iigs-disk.sh: source toolchains/env.sh first}" : "${LLVM816_ROOT:?make-iigs-disk.sh: source toolchains/env.sh first}"
CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}" CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}"
BINDIR="${BINDIR:-$repo/build/iigs/bin}" BINDIR="${BINDIR:-$repo/build/iigs/bin}"
# The NTP replayer. DEFAULTED, not optional: without it on the volume every
# IIgs example is silent except the ROM boot beep, because jlpAudioInit
# fopen()s "ntpplayer" and returns before ntpArm() when that read fails --
# with no diagnostic. Callers used to have to pass NTP_BIN and one of them
# passed the wrong path, which is exactly how Space Taxi ended up mute.
NTP_BIN="${NTP_BIN:-$repo/build/iigs/audio/ntpplayer.bin}"
OUT="${1:-$BINDIR/joey.2mg}" OUT="${1:-$BINDIR/joey.2mg}"
VOL=JOEYLIB VOL=JOEYLIB
# Volume size passed to cadius CREATEVOLUME. Defaults to an 800KB 3.5" # Volume size passed to cadius CREATEVOLUME. Defaults to an 800KB 3.5"
@ -77,17 +83,32 @@ done
# The NTP replayer binary (audio): jlpAudioInit fopen()s "ntpplayer" from # The NTP replayer binary (audio): jlpAudioInit fopen()s "ntpplayer" from
# the volume at runtime (see src/iigs/audio_full.c NTP_PATH). Plain ProDOS # the volume at runtime (see src/iigs/audio_full.c NTP_PATH). Plain ProDOS
# BIN ($06) file. Skipped with a warning if it doesn't fit or wasn't built. # BIN ($06) file. Skipped with a warning if it doesn't fit or wasn't built.
if [ -n "${NTP_BIN:-}" ] && [ -f "$NTP_BIN" ]; then ntpOk=0
if [ -f "$NTP_BIN" ]; then
ntpSz=$(stat -c%s "$NTP_BIN") ntpSz=$(stat -c%s "$NTP_BIN")
ntpNeed=$(( (ntpSz + 511) / 512 + 2 )) ntpNeed=$(( (ntpSz + 511) / 512 + 2 ))
if [ "$ntpNeed" -le "$(freeBlocks)" ]; then if [ "$ntpNeed" -le "$(freeBlocks)" ]; then
cp "$NTP_BIN" "$work/NTPPLAYER#060000" cp "$NTP_BIN" "$work/NTPPLAYER#060000"
"$CADIUS" ADDFILE "$OUT" "/$VOL" "$work/NTPPLAYER#060000" >/dev/null "$CADIUS" ADDFILE "$OUT" "/$VOL" "$work/NTPPLAYER#060000" >/dev/null
rm -f "$work/NTPPLAYER#060000" rm -f "$work/NTPPLAYER#060000"
# Never trust the exit code here (see the launch check below).
if "$CADIUS" CATALOG "$OUT" 2>/dev/null | grep -qE '^ +NTPPLAYER +BIN'; then
added+=(NTPPLAYER) added+=(NTPPLAYER)
else ntpOk=1
skipped+=(NTPPLAYER)
fi fi
fi
fi
if [ "$ntpOk" -eq 0 ]; then
skipped+=(NTPPLAYER)
cat >&2 <<EOF
make-iigs-disk.sh: WARNING - NTPPLAYER is NOT on $OUT.
Every example built against this volume will be SILENT (you will hear the
ROM boot beep and nothing else). jlpAudioInit fopen()s "ntpplayer" and
gives up quietly when it is missing; \$E1002C keeps its ROM-bank default.
Looked for: $NTP_BIN
Build it with 'make -f make/iigs.mk \$(BUILD)/audio/ntpplayer.bin', or set
NTP_BIN to its location.
EOF
fi fi
# Space Taxi runtime data: the game fopen()s a DATA/ tree (levels/*.dat, # Space Taxi runtime data: the game fopen()s a DATA/ tree (levels/*.dat,
@ -117,8 +138,28 @@ if [[ " ${added[*]:-} " == *" STAXI "* ]]; then
"$CADIUS" ADDFILE "$OUT" "$dst" "$work/${base^^}#060000" >/dev/null "$CADIUS" ADDFILE "$OUT" "$dst" "$work/${base^^}#060000" >/dev/null
rm -f "$work/${base^^}#060000" rm -f "$work/${base^^}#060000"
dataAdded=$((dataAdded + 1)) dataAdded=$((dataAdded + 1))
done < <(find "$staxiData" -type f | sort) done < <(find "$staxiData" -type f -not -path '*/sprites/level*.spc' | sort)
# The per-level hook-sprite banks are optional (the game JIT-compiles
# a level's cels at entry when its bank is missing) and large, so
# they go on last and only while the floppy has room for them.
levelSkipped=0
while IFS= read -r f; do
base=$(basename "$f")
sz=$(stat -c%s "$f")
need=$(( (sz + 511) / 512 + (sz + 131071) / 131072 + 2 ))
if [ "$need" -gt "$(freeBlocks)" ]; then
levelSkipped=$((levelSkipped + 1))
continue
fi
cp "$f" "$work/${base^^}#060000"
"$CADIUS" ADDFILE "$OUT" "/$VOL/DATA/SPRITES" "$work/${base^^}#060000" >/dev/null
rm -f "$work/${base^^}#060000"
dataAdded=$((dataAdded + 1))
done < <(find "$staxiData" -type f -path '*/sprites/level*.spc' | sort)
added+=("DATA[$dataAdded files]") added+=("DATA[$dataAdded files]")
if [ "$levelSkipped" -gt 0 ]; then
echo " note: $levelSkipped level sprite bank(s) left off (floppy full; those levels JIT at entry)" >&2
fi
else else
echo " WARNING: STAXI packed without its DATA tree ($staxiData missing)" >&2 echo " WARNING: STAXI packed without its DATA tree ($staxiData missing)" >&2
fi fi

View file

@ -7,12 +7,12 @@
# scripts/run-iigs.sh staxi MAME auto-launch: builds a # scripts/run-iigs.sh staxi MAME auto-launch: builds a
# single-example run disk # single-example run disk
# (joey-run.2mg -- joey.2mg is left # (joey-run.2mg -- joey.2mg is left
# alone so benches stay honest), # alone so benches stay honest) plus a
# boots GS/OS at real speed, and # Finder-less boot disk whose
# drives Finder via Lua to launch # /System/Start IS the launcher, so
# the example. No clicking needed. # the example is on screen in ~50 s
# GS/OS itself takes ~50 s to boot; # with no Finder and no keystroke
# that part is authentic. # driving. No clicking needed.
# scripts/run-iigs.sh --all MAME + every example on one 16 MB # scripts/run-iigs.sh --all MAME + every example on one 16 MB
# CFFA2 hard disk (the full set does # CFFA2 hard disk (the full set does
# not fit an 800KB floppy): boots # not fit an 800KB floppy): boots
@ -97,7 +97,7 @@ if [[ $all_mode -eq 1 ]]; then
fi fi
if [[ $need_disk -eq 1 ]]; then if [[ $need_disk -eq 1 ]]; then
echo "run-iigs: building all-examples disk ($all_disk)..." echo "run-iigs: building all-examples disk ($all_disk)..."
BINDIR="$bin_dir" NTP_BIN="$bin_dir/ntpplayer.bin" \ BINDIR="$bin_dir" \
JOEY_DISK_SIZE="16MB" JOEY_DISK_EXAMPLES="$all_examples" \ JOEY_DISK_SIZE="16MB" JOEY_DISK_EXAMPLES="$all_examples" \
"$repo/scripts/make-iigs-disk.sh" "$all_disk" "$repo/scripts/make-iigs-disk.sh" "$all_disk"
else else
@ -433,42 +433,27 @@ EOF
} }
trap cleanup EXIT INT TERM trap cleanup EXIT INT TERM
cp "$sys_disk" "$work/boot.po" # Finder-less boot disk: /System/Start is tools/iigsStart, which
# SetPrefixGS's to the data volume and QuitGS-launches the example.
# This replaces the old Lua keyboard driver, which typed J / Cmd-O /
# name / Cmd-O at calibrated frame counts -- workable but fragile,
# and it made every run sit through the Finder's ~60 s desktop paint
# before anything happened. Straight to the example in ~50 s now.
if [[ -z "${LLVM816_ROOT:-}" ]]; then
# shellcheck disable=SC1091
source "$repo/toolchains/env.sh" >/dev/null 2>&1 || true
fi
echo "run-iigs: building Finder-less boot disk for $target..."
"$repo/scripts/make-iigs-boot-disk.sh" "$work/boot.po" "$target" >/dev/null
cp "$run_disk" "$work/joey-run.2mg" cp "$run_disk" "$work/joey-run.2mg"
# Slim Finder driver -- the keyboard-injection steps proven in
# run-iigs-mame.sh's harness, without the debugger/crash plumbing:
# at calibrated frame counts, type J (select the JOEYLIB volume),
# Cmd-O (open it), the example's full name (select; full-name
# typing disambiguates e.g. DRAW vs DATA), Cmd-O (launch). Frame
# counts are emulated frames, so throttling doesn't shift them.
headless=${MAME_HEADLESS:-0} headless=${MAME_HEADLESS:-0}
cat > "$work/launch.lua" <<LUA cat > "$work/launch.lua" <<LUA
local nat = manager.machine.natkeyboard -- The boot disk launches the example by itself (tools/iigsStart), so
-- nothing here drives the Finder any more. This script only exists for
local function get_field(port, field_name) -- the headless snapshot/exit schedule.
local p = manager.machine.ioport.ports[port]
if p == nil then return nil end
return p.fields[field_name]
end
local key_cmd = get_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 headless = ($headless == 1) local headless = ($headless == 1)
local frames = 0 local frames = 0
local steps = { 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("$target") end },
{ 3660, function() press(key_cmd) end },
{ 3666, function() nat:post("o") end },
{ 3720, function() release(key_cmd) end },
}
if headless then if headless then
-- Batch verification: snapshot right after the launch lands, mid -- Batch verification: snapshot right after the launch lands, mid
-- asset load, and after the example settles on its first screen, -- asset load, and after the example settles on its first screen,
@ -515,9 +500,9 @@ LUA
cat <<EOF cat <<EOF
MAME apple2gs: auto-launching $target off $run_disk. MAME apple2gs: auto-launching $target off $run_disk.
GS/OS takes ~50 s to reach Finder; the Lua driver then opens JOEYLIB Booting a Finder-less GS/OS whose startup application IS $target, so it
and launches $target for you. Quit MAME (or close its window) to end comes up in ~50 s with no Finder in the way. Quit MAME (or close its
the session. window) to end the session.
EOF EOF
cd "$work" cd "$work"

237
scripts/verify-iigs-audio.sh Executable file
View file

@ -0,0 +1,237 @@
#!/usr/bin/env bash
# verify-iigs-audio.sh - Headless gate proving NTP is actually running on
# the IIgs, modelled on verify-iigs.sh (same Finder launch timeline, same
# "read emulated memory, not a snapshot" approach).
#
# Sound cannot be graded from a -sound none run, so this grades the two
# pieces of state that make sound possible:
#
# irqVec $E1002C is the GS sound-interrupt vector. NTPprepare writes
# $5C (JML) + the address of its interrupt_handler + its own
# bank there (setup_interrupt), and NTPstop puts the previous
# vector back. $5C plus a RAM bank therefore means "NTP's DOC
# interrupt is installed". Before the ntpArm change nothing
# ever called NTPprepare unless the app played a .NTP module,
# so an SFX-only app left this at the system value and every
# streamed sample reached the DOC exactly once, unrefilled.
#
# docVol $E100CA is the Sound Tool Set's copy of the sound control
# register, and its LOW NIBBLE is the DOC master volume. NTP
# copies this byte into $C03C before every DOC access and
# never writes it, so if nothing started the Sound Tool Set it
# is whatever GS/OS left behind -- and a zero nibble means the
# DOC streams perfectly while outputting SILENCE. Everything
# below passed in exactly that state ("I don't hear any sound
# other than the IIgs startup beep"), which is why this check
# exists: streaming is necessary, not sufficient.
#
# slotN MAME exposes the Ensoniq 5503's 64KB wavetable RAM as the
# ":doc" device's "rom" space. JoeyLib's SFX slots live at DOC
# pages 0xC0, 0xC2, ... (SFX_BASE_DOC_PAGE / SFX_DOC_PAGE_STEP
# in src/iigs/audio_full.c), and bytes only land there when
# NTPstreamsound runs. Every slot is hashed twice, frames
# apart. Which slots an application uses is its own business
# -- AUDIO's tracker takes 0-2, Space Taxi only ever reaches
# the noise slot 3 -- so the gate asks that SOME slot carry a
# waveform and that SOME slot's hash move between the probes.
# A moving hash is the live-interrupt evidence: NTP streams
# the sample through the page in 256-byte halves, so a dead
# interrupt leaves whatever the one initial copy wrote.
#
# Usage: scripts/verify-iigs-audio.sh [example]
# example lowercase name, default "audio". A single-example
# disk is built for it (joey.2mg cannot hold them all).
# Requires: toolchains/env.sh sourced; the example built under build/iigs/bin.
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
example=${1:-audio}
NAME=${example^^}
sys_disk=$repo/toolchains/emulators/support/gsos-system.po
rompath="${MAME_ROMPATH:-$HOME/.mame/roms}"
probeA="${MAME_PROBE_A:-5400}"
probeB="${MAME_PROBE_B:-9000}"
# Mirrors JOEY_AUDIO_SFX_SLOTS / SFX_BASE_DOC_PAGE / SFX_DOC_PAGE_STEP
# in src/iigs/audio_full.c.
slots=5
basePage=0xC0
pageStep=2
[ -f "$sys_disk" ] || { echo "verify-iigs-audio: missing $sys_disk" >&2; exit 2; }
[ -f "$repo/build/iigs/bin/$NAME" ] || { echo "verify-iigs-audio: missing build/iigs/bin/$NAME (run 'make iigs-disk')" >&2; exit 2; }
work=$(mktemp -d -t joeylib-audio.XXXXXX)
trap 'rm -rf "$work"' EXIT
# One-example volume: the 800KB floppy cannot hold every example, and the
# shared joey.2mg drops AUDIO and STAXI first (see make-iigs-disk.sh).
JOEY_DISK_EXAMPLES="$NAME" BINDIR="$repo/build/iigs/bin" \
NTP_BIN="$repo/build/iigs/audio/ntpplayer.bin" \
"$repo/scripts/make-iigs-disk.sh" "$work/joey.2mg" >/dev/null
cp "$sys_disk" "$work/boot.po"
cat > "$work/audio.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
-- The DOC's wavetable RAM. Guarded: if a MAME build ever renames the
-- device or its space, say so instead of silently reporting zeroes.
local docSpace = nil
do
local ok = pcall(function()
docSpace = manager.machine.devices[":doc"].spaces["rom"]
end)
if not ok then docSpace = nil end
end
-- FNV-1a over a DOC page pair (one JoeyLib SFX slot = 512 bytes), plus a
-- count of bytes that are neither 0x00 (never written) nor 0x80 (DOC
-- silence), i.e. bytes that carry an actual waveform.
local function docSlot(page)
if docSpace == nil then return 0, -1 end
local h = 2166136261
local nz = 0
for i = 0, 511 do
local b = docSpace:read_u8((page * 256) + i)
if b ~= 0 and b ~= 0x80 then nz = nz + 1 end
h = (h ~ b) & 0xFFFFFFFF
h = (h * 16777619) & 0xFFFFFFFF
end
return h, nz
end
local function report(tag)
local v0 = mem:read_u8(0xE1002C)
local v1 = mem:read_u8(0xE1002D)
local v2 = mem:read_u8(0xE1002E)
local v3 = mem:read_u8(0xE1002F)
local scb0 = mem:read_u8(0xE19D00)
local docVol = mem:read_u8(0xE100CA)
local line = string.format(
"VERIFY-AUDIO name=$NAME tag=%s frame=%d scb0=%02X irqVec=%02X%02X%02X%02X docVol=%02X",
tag, frame, scb0, v3, v2, v1, v0, docVol)
for slot = 0, $slots - 1 do
local h, n = docSlot($basePage + slot * $pageStep)
line = line .. string.format(" slot%d=%08X slot%dNZ=%d", slot, h, slot, n)
end
io.write(line .. "\n")
io.flush()
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("$NAME") end},
{3660, function() press(key_cmd) end},
{3666, function() nat:post("o") end},
{3720, function() release(key_cmd) end},
{$probeA, function() report("A") end},
{$probeB, function() report("B"); manager.machine:exit() 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
end)
LUA
cd "$work"
out=$(QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
timeout 400 mame apple2gs \
-rompath "$rompath" \
-flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \
-video none -sound none -nothrottle \
-autoboot_script "$work/audio.lua" </dev/null 2>&1) || true
lineA=$(echo "$out" | grep -E '^VERIFY-AUDIO .* tag=A ' | tail -1)
lineB=$(echo "$out" | grep -E '^VERIFY-AUDIO .* tag=B ' | tail -1)
echo "$lineA"
echo "$lineB"
if [ -z "$lineA" ] || [ -z "$lineB" ]; then
echo "verify-iigs-audio: FAIL - no probe report (boot/launch failed)" >&2
echo "$out" | tail -15 >&2
exit 1
fi
field() { echo "$2" | sed -E "s/.*$1=([0-9A-Fa-f]+).*/\1/"; }
# Launch gate first, exactly as verify-iigs.sh: the Finder desktop alone
# would otherwise be graded as if it were the example.
scb0=$(field scb0 "$lineB")
if [ $((16#$scb0)) -ge 16 ]; then
echo "verify-iigs-audio: FAIL ($NAME never launched - SHR SCB[0]=0x$scb0 is the Finder's 640-mode fill)" >&2
exit 1
fi
vec=$(field irqVec "$lineB")
if [ "${vec:6:2}" != "5C" ]; then
echo "verify-iigs-audio: FAIL (sound interrupt vector is $vec, not a JML - NTPprepare never ran, so there is no interrupt to refill a streamed sample)" >&2
exit 1
fi
# The vector the ROM leaves in place is a JML too ($5C 62 CD FC on this
# machine), so "is it a JML" is not enough -- the control run with the
# pre-ntpArm audio_full.c passed that test. NTP lives in a Memory
# Manager handle, always in RAM, so a ROM bank ($F0 and up) means the
# system vector is still installed and NTPprepare never ran.
bank=${vec:0:2}
if [ $((16#$bank)) -ge 240 ]; then
echo "verify-iigs-audio: FAIL (sound interrupt vector $vec still points into ROM bank \$$bank - NTPprepare never ran)" >&2
exit 1
fi
# Audible-output precondition: the DOC master volume must not be zero.
docVol=$(field docVol "$lineB")
if [ $(( 16#$docVol & 0x0F )) -eq 0 ]; then
echo "verify-iigs-audio: FAIL (DOC master volume is 0 -- \$E100CA=0x$docVol. The Sound Tool Set was never started, so NTP copies a zero volume into \$C03C: the DOC streams but nothing is audible)" >&2
exit 1
fi
if [ "$(field slot0NZ "$lineA")" = "-1" ]; then
echo "verify-iigs-audio: FAIL (MAME exposed no :doc/rom space - the DOC RAM probe read nothing)" >&2
exit 1
fi
loud=""
moved=""
slot=0
while [ "$slot" -lt "$slots" ]; do
nz=$(field "slot${slot}NZ" "$lineB")
if [ "$nz" -gt 0 ]; then
loud="$loud $slot"
fi
if [ "$(field "slot$slot" "$lineA")" != "$(field "slot$slot" "$lineB")" ]; then
moved="$moved $slot"
fi
slot=$((slot + 1))
done
if [ -z "$loud" ]; then
echo "verify-iigs-audio: FAIL (no SFX slot's DOC page holds a waveform - NTPstreamsound never reached the DOC)" >&2
exit 1
fi
# A live NTP interrupt keeps rewriting the DOC page it streams through.
# A slot parked on the resident tone square (toneSquareEnsure) hashes
# the same every time -- every refill writes the same square -- so this
# only warns when NO slot moved at all.
if [ -z "$moved" ]; then
echo "verify-iigs-audio: WARN (no DOC slot changed between frames $probeA and $probeB - samples are present but nothing is refilling them)"
fi
echo "verify-iigs-audio: PASS ($NAME: NTP interrupt installed at bank $bank; DOC slots with a waveform:$loud; refilled between probes:$moved)"

View file

@ -79,9 +79,29 @@ extern UBYTE mt_E8Trigger;
// ----- Module state ----- // ----- Module state -----
// Streaming SFX chunk size, in bytes. PTPlayer's mt_playfx plays a
// one-shot sample and does not queue, so a stream is played one chunk at
// a time with the next one fired from jlpAudioFrameTick when the current
// is due to end. Bigger chunks mean fewer seams; 4 KB is ~0.8 s at 5 kHz,
// which covers a whole speech sample with no seam at all.
#define SFX_STREAM_CHUNK 4096u
// The host calls jlpAudioFrameTick once per display frame, and this port
// is PAL throughout (see PAULA_CLOCK_PAL in periodForRate), so chunk
// lifetimes are counted in 50 Hz frames.
#define AMIGA_FRAME_HZ 50u
typedef struct { typedef struct {
UBYTE *samples; // Chip-RAM copy of one SFX sample UBYTE *samples; // Chip-RAM copy of one SFX sample
UWORD lengthWords; // sample length in 16-bit words UWORD lengthWords; // sample length in 16-bit words
// ---- streaming (jlpAudioPlaySfxStream) ----
UBYTE *chunk; // Chip-RAM chunk buffer, SFX_STREAM_CHUNK
BYTE cha; // Paula channel, -1 = let PTPlayer pick
jlAudioStreamFillT fill;
void *ctx;
uint16_t rateHz;
uint16_t framesLeft; // until the playing chunk runs out
bool active;
} SfxSlotT; } SfxSlotT;
static UBYTE *gModuleChip = NULL; // Chip-RAM copy of the playing module static UBYTE *gModuleChip = NULL; // Chip-RAM copy of the playing module
@ -157,6 +177,9 @@ bool jlpAudioInit(void) {
for (i = 0; i < JOEY_AUDIO_SFX_SLOTS; i++) { for (i = 0; i < JOEY_AUDIO_SFX_SLOTS; i++) {
gSfxSlots[i].samples = NULL; gSfxSlots[i].samples = NULL;
gSfxSlots[i].lengthWords = 0; gSfxSlots[i].lengthWords = 0;
// -1, not 0: zero is Paula channel 0, which would silently pin every
// slot hard left instead of letting PTPlayer choose.
gSfxSlots[i].cha = -1;
} }
return true; return true;
} }
@ -202,6 +225,11 @@ void jlpAudioShutdown(void) {
gSfxSlots[i].samples = NULL; gSfxSlots[i].samples = NULL;
gSfxSlots[i].lengthWords = 0; gSfxSlots[i].lengthWords = 0;
} }
gSfxSlots[i].active = false;
if (gSfxSlots[i].chunk != NULL) {
FreeMem(gSfxSlots[i].chunk, (ULONG)SFX_STREAM_CHUNK);
gSfxSlots[i].chunk = NULL;
}
} }
} }
@ -247,6 +275,42 @@ void jlpAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
} }
// Pull one chunk from a streaming slot and hand it to PTPlayer.
// Returns false when the stream has ended (fill returned 0).
static bool streamChunk(uint8_t slot) {
SfxSlotT *s = &gSfxSlots[slot];
SfxStructure sfx;
uint32_t got;
// The fill contract is SIGNED 8-bit, which is exactly what Paula
// wants, so the callback writes straight into Chip RAM.
got = s->fill(s->ctx, (int8_t *)s->chunk, (uint32_t)SFX_STREAM_CHUNK);
if (got < 2u || got > (uint32_t)SFX_STREAM_CHUNK) {
return false;
}
got &= ~1u; // Paula counts 16-bit words
sfx.sfx_ptr = s->chunk;
sfx.sfx_len = (UWORD)(got >> 1);
sfx.sfx_per = periodForRate(s->rateHz);
sfx.sfx_vol = SFX_VOLUME_MAX;
sfx.sfx_cha = s->cha;
sfx.sfx_pri = (BYTE)(slot + 1);
mt_playfx((void *)&custom, &sfx);
// Fire the next chunk a frame early rather than leaving a gap.
{
uint32_t frames = (got * (uint32_t)AMIGA_FRAME_HZ) / (uint32_t)s->rateHz;
if (frames > 1u) {
frames--;
}
s->framesLeft = (uint16_t)frames;
}
return true;
}
void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz) { void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz) {
SfxStructure sfx; SfxStructure sfx;
SfxSlotT *s; SfxSlotT *s;
@ -281,22 +345,42 @@ void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1
sfx.sfx_len = words; sfx.sfx_len = words;
sfx.sfx_per = periodForRate(rateHz); sfx.sfx_per = periodForRate(rateHz);
sfx.sfx_vol = SFX_VOLUME_MAX; sfx.sfx_vol = SFX_VOLUME_MAX;
sfx.sfx_cha = -1; // best free channel sfx.sfx_cha = s->cha; // -1 = best free channel
sfx.sfx_pri = (BYTE)(slot + 1); // priority 1..N from slot index sfx.sfx_pri = (BYTE)(slot + 1); // priority 1..N from slot index
mt_playfx((void *)&custom, &sfx); mt_playfx((void *)&custom, &sfx);
} }
// Streaming SFX is not wired on Amiga: PTPlayer's mt_playfx wants a // Streaming SFX. PTPlayer's mt_playfx wants a complete sample at fire
// complete sample at fire time. Implementing streaming here would // time, so the stream is chunked rather than refilled mid-playback:
// need a per-slot Paula buffer with mid-playback refill from the // arm the slot, push the first chunk now, and let jlpAudioFrameTick
// audio IRQ. No-op until then; callers that depend on streaming // push each following one as the previous is due to run out. The seam
// will hear silence on Amiga. // therefore quantises to a 50 Hz frame, which is the jitter the public
// contract documents. Each slot gets one SFX_STREAM_CHUNK Chip-RAM
// buffer, allocated on first use and freed in jlpAudioShutdown.
void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz) { void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz) {
(void)slot; SfxSlotT *s;
(void)fill;
(void)ctx; if (!gInstalled || slot >= JOEY_AUDIO_SFX_SLOTS) {
(void)rateHz; return;
}
if (fill == (jlAudioStreamFillT)0 || rateHz == 0) {
return;
}
s = &gSfxSlots[slot];
if (s->chunk == NULL) {
s->chunk = (UBYTE *)AllocMem((ULONG)SFX_STREAM_CHUNK, MEMF_CHIP);
if (s->chunk == NULL) {
return;
}
}
s->fill = fill;
s->ctx = ctx;
s->rateHz = rateHz;
s->active = true;
if (!streamChunk(slot)) {
s->active = false;
}
} }
@ -429,12 +513,36 @@ void jlpAudioStopMod(void) {
} }
// Paula pans per CHANNEL: 0 and 3 feed the left output, 1 and 2 the right.
// One voice cannot reach both ears, so AUDIO_PAN_CENTER keeps PTPlayer's own
// "best free channel" pick (sfx_cha = -1) -- which is what every slot did
// before this call existed, so the default behaviour is unchanged. A panned
// slot pins its SFX to a channel on that side instead.
void jlpAudioSetPan(uint8_t slot, uint8_t pan) {
if (slot >= JOEY_AUDIO_SFX_SLOTS) {
return;
}
switch (pan) {
case AUDIO_PAN_LEFT:
gSfxSlots[slot].cha = 3;
break;
case AUDIO_PAN_RIGHT:
gSfxSlots[slot].cha = 2;
break;
default:
gSfxSlots[slot].cha = -1; // PTPlayer picks
break;
}
}
void jlpAudioStopSfx(uint8_t slot) { void jlpAudioStopSfx(uint8_t slot) {
UBYTE i; UBYTE i;
if (!gInstalled || slot >= JOEY_AUDIO_SFX_SLOTS) { if (!gInstalled || slot >= JOEY_AUDIO_SFX_SLOTS) {
return; return;
} }
gSfxSlots[slot].active = false;
// PTPlayer addresses SFX by Paula channel (0..3), but mt_playfx // PTPlayer addresses SFX by Paula channel (0..3), but mt_playfx
// picks a channel dynamically and does not return which one. We // picks a channel dynamically and does not return which one. We
// can't reliably map slot -> channel after the fact, so stop all // can't reliably map slot -> channel after the fact, so stop all
@ -447,8 +555,27 @@ void jlpAudioStopSfx(uint8_t slot) {
void jlpAudioFrameTick(void) { void jlpAudioFrameTick(void) {
// PTPlayer drives itself off CIA-B; the only host-loop work is the // Streaming slots: mt_playfx plays one chunk and stops, so the next
// play-once watchdog. When the song author placed an `E8FF` at // chunk goes out here as the previous one runs down.
{
uint8_t slot;
for (slot = 0u; slot < JOEY_AUDIO_SFX_SLOTS; slot++) {
if (!gSfxSlots[slot].active) {
continue;
}
if (gSfxSlots[slot].framesLeft != 0u) {
gSfxSlots[slot].framesLeft--;
continue;
}
if (!streamChunk(slot)) {
gSfxSlots[slot].active = false;
}
}
}
// PTPlayer drives itself off CIA-B; the only other host-loop work is
// the play-once watchdog. When the song author placed an `E8FF` at
// song end, mt_E8Trigger latches to 0xFF -- if the caller passed // song end, mt_E8Trigger latches to 0xFF -- if the caller passed
// loop=false to PlayMod, that's our cue to stop the song. // loop=false to PlayMod, that's our cue to stop the song.
if (gWantLoopOnce && gModPlaying && mt_E8Trigger == PTPLAYER_LOOP_END_MARKER) { if (gWantLoopOnce && gModPlaying && mt_E8Trigger == PTPLAYER_LOOP_END_MARKER) {

View file

@ -124,7 +124,7 @@ bool jlSpriteCompile(jlSpriteT *sp) {
// Copy the staged image into the slot in one shot. routineOffsets are // Copy the staged image into the slot in one shot. routineOffsets are
// already slot-relative (offset within `staging` == offset within the // already slot-relative (offset within `staging` == offset within the
// slot after this copy), so nothing else needs fixing up. // slot after this copy), so nothing else needs fixing up.
memcpy(codegenArenaBase() + slot->offset, staging, (size_t)total); memcpy(codegenArenaSlotBase(slot), staging, (size_t)total);
sp->slot = slot; sp->slot = slot;
// Registry (#34): every creation path (including jlSpriteBankLoad, // Registry (#34): every creation path (including jlSpriteBankLoad,
// #32) already registered sp via spriteInitFields; this idempotent // #32) already registered sp via spriteInitFields; this idempotent

View file

@ -109,6 +109,22 @@ typedef char AssetTileSizeCheckT[(sizeof(jlTileT) == TILE_BYTES) ? 1 : -1];
#error "Unknown platform for asset loader" #error "Unknown platform for asset loader"
#endif #endif
// Per-platform target byte for .spc files (pre-compiled sprite code, so
// it is per MACHINE CODE, not per pixel format: the X68000 shares DOS's
// chunky tile byte above but runs 68k planar sprite routines, and must
// never accept an x86 bank). Mirrors SPC_TARGET in tools/spritebake.
#if defined(JOEYLIB_PLATFORM_AMIGA)
#define EXPECTED_SPC_TARGET 1u
#elif defined(JOEYLIB_PLATFORM_ATARIST)
#define EXPECTED_SPC_TARGET 2u
#elif defined(JOEYLIB_PLATFORM_DOS) || defined(JOEYLIB_PLATFORM_BLANK)
#define EXPECTED_SPC_TARGET 3u
#elif defined(JOEYLIB_PLATFORM_IIGS)
#define EXPECTED_SPC_TARGET 4u
#elif defined(JOEYLIB_PLATFORM_X68000)
#define EXPECTED_SPC_TARGET 5u
#endif
// ----- Prototypes ----- // ----- Prototypes -----
static bool openReadValidateHeader(const char *path, const char *magic, uint8_t expectedTarget, uint8_t *header, uint16_t *outPalette, FILE **outFp); static bool openReadValidateHeader(const char *path, const char *magic, uint8_t expectedTarget, uint8_t *header, uint16_t *outPalette, FILE **outFp);
@ -290,8 +306,8 @@ uint16_t jlSpriteBankLoadPrecompiled(const char *path, jlSpriteT **outCels, uint
return 0u; return 0u;
} }
// "JSC1" + the per-machine target byte (pre-compiled code is machine- // "JSC1" + the per-machine target byte (pre-compiled code is machine-
// specific, validated like a .tbk via the shared EXPECTED target). // specific, validated like a .tbk but against the .spc target table).
if (!openReadValidateHeader(path, SPC_MAGIC, EXPECTED_TILE_TARGET, header, outPalette, &fp)) { if (!openReadValidateHeader(path, SPC_MAGIC, EXPECTED_SPC_TARGET, header, outPalette, &fp)) {
return 0u; return 0u;
} }
widthTiles = header[SPC_OFF_WIDTH_TILES]; widthTiles = header[SPC_OFF_WIDTH_TILES];
@ -400,7 +416,7 @@ uint16_t jlSpriteBankLoadPrecompiled(const char *path, jlSpriteT **outCels, uint
} }
} }
if (slot != NULL) { if (slot != NULL) {
uint8_t *slotBase = codegenArenaBase() + slot->offset; uint8_t *slotBase = codegenArenaSlotBase(slot);
memcpy(slotBase, imageBuf, (size_t)imageSize); memcpy(slotBase, imageBuf, (size_t)imageSize);
for (shift = 0; shift < shiftCount; shift++) { for (shift = 0; shift < shiftCount; shift++) {
@ -470,7 +486,7 @@ bool jlSpriteBankSavePrecompiled(const char *path, jlSpriteT **cels, uint16_t co
// Shared 44-byte header (mirrors tools/spritebake + jlSpriteBankLoadPrecompiled). // Shared 44-byte header (mirrors tools/spritebake + jlSpriteBankLoadPrecompiled).
fwrite(SPC_MAGIC, 1, 4, fp); fwrite(SPC_MAGIC, 1, 4, fp);
fputc((int)EXPECTED_TILE_TARGET, fp); fputc((int)EXPECTED_SPC_TARGET, fp);
fputc(palette != NULL ? 1 : 0, fp); fputc(palette != NULL ? 1 : 0, fp);
fputc((int)widthTiles, fp); fputc((int)widthTiles, fp);
fputc((int)heightTiles, fp); fputc((int)heightTiles, fp);
@ -534,7 +550,7 @@ bool jlSpriteBankSavePrecompiled(const char *path, jlSpriteT **cels, uint16_t co
} }
// image: the compiled routines, straight from the arena slot. // image: the compiled routines, straight from the arena slot.
if (imageSize > 0u) { if (imageSize > 0u) {
fwrite(codegenArenaBase() + sp->slot->offset, 1, (size_t)imageSize, fp); fwrite(codegenArenaSlotBase(sp->slot), 1, (size_t)imageSize, fp);
} }
for (r = 0; r < relocCount; r++) { for (r = 0; r < relocCount; r++) {
fputc((int)(relocOffsets[r] & 0xFFu), fp); fputc((int)(relocOffsets[r] & 0xFFu), fp);

View file

@ -77,6 +77,22 @@ void jlAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint
} }
void jlAudioPlaySfxStreamRaw(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz) {
if (!gAudioReady || fill == NULL || rateHz == 0) {
return;
}
if (slot >= JOEY_AUDIO_SFX_SLOTS) {
return;
}
jlpAudioPlaySfxStreamRaw(slot, fill, ctx, rateHz);
}
uint8_t jlAudioSfxRawFormat(void) {
return jlpAudioSfxRawFormat();
}
void jlAudioTone(uint16_t freqHz) { void jlAudioTone(uint16_t freqHz) {
// Deliberately NOT gated on gAudioReady -- jlAudioInit is for // Deliberately NOT gated on gAudioReady -- jlAudioInit is for
// PCM/SFX paths that need DMA + libxmp wiring. The PC speaker // PCM/SFX paths that need DMA + libxmp wiring. The PC speaker
@ -124,6 +140,17 @@ void jlAudioCriticalExit(void) {
} }
void jlAudioSetPan(uint8_t slot, jlAudioPanE pan) {
if (!gAudioReady || slot >= JOEY_AUDIO_SFX_SLOTS) {
return;
}
if (pan >= AUDIO_PAN_COUNT) {
return;
}
jlpAudioSetPan(slot, (uint8_t)pan);
}
void jlAudioStopSfx(uint8_t slot) { void jlAudioStopSfx(uint8_t slot) {
if (!gAudioReady || slot >= JOEY_AUDIO_SFX_SLOTS) { if (!gAudioReady || slot >= JOEY_AUDIO_SFX_SLOTS) {
return; return;
@ -140,10 +167,12 @@ void jlAudioFrameTick(void) {
if (!gAudioReady) { if (!gAudioReady) {
return; return;
} }
#ifndef JOEYLIB_PLATFORM_IIGS // This used to be #ifndef'd out on the IIgs, on the grounds that
// IIgs: NTPstreamsound is fully DOC-IRQ-driven, jlpAudioFrameTick // NTPstreamsound is DOC-IRQ driven so jlpAudioFrameTick "is an empty
// is an empty no-op there. Skip the wrapper JSL entirely on IIgs // no-op there". It is not: it pumps a registered jlAudioTickRegister
// so per-frame audio cost stays at the gAudioReady branch above. // callback, and since streaming SFX landed it also fires the next
// chunk of every active stream. Skipping it meant a stream played
// its first chunk and then stopped forever -- Space Taxi spoke its
// first word and never another.
jlpAudioFrameTick(); jlpAudioFrameTick();
#endif
} }

View file

@ -1,5 +1,6 @@
// Codegen arena: free-list allocator with adjacent-hole coalescing // Codegen arena: free-list allocator with adjacent-hole coalescing
// and manual compaction. See codegenArenaInternal.h for the contract. // and manual compaction, over one bank (most ports) or several 64 KB
// banks (IIgs). See codegenArenaInternal.h for the contract.
#include "joey/platform.h" #include "joey/platform.h"
@ -22,6 +23,7 @@
#define attrLocked 0x8000 #define attrLocked 0x8000
#endif #endif
#include <stdint.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@ -30,36 +32,154 @@
// ----- Module state ----- // ----- Module state -----
//
// gCodegenArenaBase / gCodegenArenaBaseAddr are non-static so jlSpriteCompile.c can read them // gCodegenBankBase / gCodegenBankAddr are non-static so the sprite
// directly via extern instead of paying a JSL/RTL per access through // dispatch reads them directly via extern instead of paying a JSL/RTL
// the codegenArenaBase() / codegenArenaBaseAddr() wrappers. Both are // per draw through a wrapper. They are set once at codegenArenaInit and
// set once at codegenArenaInit and never moved (the underlying // never moved (a bank is locked in place on the IIgs). Callers MUST
// Memory Manager handle is locked-in-place on IIgs). Callers MUST
// treat them as read-only. // treat them as read-only.
uint8_t *gCodegenArenaBase = NULL;
// gCodegenArenaBaseAddr mirrors gCodegenArenaBase as a 24-bit absolute uint8_t *gCodegenBankBase[CODEGEN_ARENA_BANKS_MAX];
// address that JSL targets read directly (captured by copying the uint32_t gCodegenBankAddr[CODEGEN_ARENA_BANKS_MAX];
// handle pointer's raw bytes -- see codegenArenaInit). static uint32_t gBankSize[CODEGEN_ARENA_BANKS_MAX];
uint32_t gCodegenArenaBaseAddr = 0; static ArenaSlotT *gBankFirst[CODEGEN_ARENA_BANKS_MAX];
#if defined(JOEYLIB_PLATFORM_IIGS)
static void *gBankHandle[CODEGEN_ARENA_BANKS_MAX];
#endif
static uint8_t gBankCount = 0u;
static uint32_t gTotalBytes = 0; static uint32_t gTotalBytes = 0;
static uint32_t gUsedBytes = 0; static uint32_t gUsedBytes = 0;
static ArenaSlotT *gFirstSlot = NULL;
#if defined(JOEYLIB_PLATFORM_IIGS)
static void *gCodegenArenaBaseHandle = NULL;
#endif
// ----- Prototypes ----- // ----- Prototypes -----
static ArenaSlotT *newSlot(uint32_t offset, uint32_t size, bool used); static bool bankAlloc(uint8_t bank, uint32_t bytes);
static void bankCompact(uint8_t bank);
static void bankFree(uint8_t bank);
static void coalesceWithNext(ArenaSlotT *slot); static void coalesceWithNext(ArenaSlotT *slot);
static ArenaSlotT *newSlot(uint32_t offset, uint32_t size, bool used);
// ----- Internal helpers (alphabetical) ----- // ----- Helpers -----
// One bank's memory. On the IIgs a Memory Manager block that may not
// cross a bank boundary (a normal C heap allocation returns bank-0
// system RAM, and JSL'ing into that lands on whatever lives there);
// attrFixed | attrLocked | attrPage | attrNoCross gives a fixed
// page-aligned region in one bank. Elsewhere, malloc.
static bool bankAlloc(uint8_t bank, uint32_t bytes) {
#if defined(JOEYLIB_PLATFORM_IIGS)
void *h;
void *p;
uint8_t raw[4];
h = NewHandle((unsigned long)bytes, MMStartUp(), attrFixed | attrLocked | attrPage | attrNoCross, (void *)0);
if (h == NULL) {
return false;
}
HLock(h);
p = *(void **)h;
if (p == NULL) {
DisposeHandle(h);
return false;
}
// The 24-bit absolute address is the master pointer's raw bytes;
// byte 3 (bank-high/flags) is masked off.
memcpy(raw, &p, 4);
gBankHandle[bank] = h;
gCodegenBankBase[bank] = (uint8_t *)p;
gCodegenBankAddr[bank] = (uint32_t)raw[0] | ((uint32_t)raw[1] << 8) | ((uint32_t)raw[2] << 16);
#else
uint8_t *p = (uint8_t *)malloc(bytes);
if (p == NULL) {
return false;
}
gCodegenBankBase[bank] = p;
gCodegenBankAddr[bank] = (uint32_t)(uintptr_t)p;
#endif
gBankSize[bank] = bytes;
gBankFirst[bank] = newSlot((uint32_t)bank << CODEGEN_ARENA_BANK_SHIFT, bytes, false);
if (gBankFirst[bank] == NULL) {
bankFree(bank);
return false;
}
return true;
}
static void bankCompact(uint8_t bank) {
uint8_t *base = gCodegenBankBase[bank];
ArenaSlotT *slot;
ArenaSlotT *next;
ArenaSlotT *trailing;
uint32_t cursor = 0;
slot = gBankFirst[bank];
while (slot != NULL) {
next = slot->next;
if (slot->used) {
uint32_t at = slot->offset & CODEGEN_ARENA_BANK_MASK;
if (at != cursor) {
memmove(base + cursor, base + at, slot->size);
slot->offset = ((uint32_t)bank << CODEGEN_ARENA_BANK_SHIFT) | cursor;
}
cursor += slot->size;
slot = next;
continue;
}
if (slot->prev != NULL) {
slot->prev->next = next;
} else {
gBankFirst[bank] = next;
}
if (next != NULL) {
next->prev = slot->prev;
}
free(slot);
slot = next;
}
if (cursor < gBankSize[bank]) {
trailing = newSlot(((uint32_t)bank << CODEGEN_ARENA_BANK_SHIFT) | cursor, gBankSize[bank] - cursor, false);
if (trailing == NULL) {
return; // Compaction succeeded; just skip the free-slot record.
}
trailing->prev = NULL;
for (slot = gBankFirst[bank]; slot != NULL && slot->next != NULL; slot = slot->next) {
}
if (slot == NULL) {
gBankFirst[bank] = trailing;
} else {
slot->next = trailing;
trailing->prev = slot;
}
}
}
static void bankFree(uint8_t bank) {
ArenaSlotT *slot;
ArenaSlotT *next;
for (slot = gBankFirst[bank]; slot != NULL; slot = next) {
next = slot->next;
free(slot);
}
#if defined(JOEYLIB_PLATFORM_IIGS)
if (gBankHandle[bank] != NULL) {
DisposeHandle(gBankHandle[bank]);
gBankHandle[bank] = NULL;
}
#else
free(gCodegenBankBase[bank]);
#endif
gCodegenBankBase[bank] = NULL;
gCodegenBankAddr[bank] = 0;
gBankSize[bank] = 0;
gBankFirst[bank] = NULL;
}
// If `slot` is free and slot->next is also free, merge them into a
// single free slot. Caller must already have ensured slot is free.
static void coalesceWithNext(ArenaSlotT *slot) { static void coalesceWithNext(ArenaSlotT *slot) {
ArenaSlotT *victim; ArenaSlotT *victim;
@ -95,21 +215,21 @@ static ArenaSlotT *newSlot(uint32_t offset, uint32_t size, bool used) {
} }
// ----- Public-internal API (alphabetical) ----- // ----- Public API -----
ArenaSlotT *codegenArenaAlloc(uint32_t bytes) { ArenaSlotT *codegenArenaAlloc(uint32_t bytes) {
ArenaSlotT *slot; ArenaSlotT *slot;
ArenaSlotT *remainder; ArenaSlotT *remainder;
uint8_t bank;
if (gCodegenArenaBase == NULL || bytes == 0) { if (gBankCount == 0u || bytes == 0) {
return NULL; return NULL;
} }
for (slot = gFirstSlot; slot != NULL; slot = slot->next) { for (bank = 0u; bank < gBankCount; bank++) {
for (slot = gBankFirst[bank]; slot != NULL; slot = slot->next) {
if (slot->used || slot->size < bytes) { if (slot->used || slot->size < bytes) {
continue; continue;
} }
// First fit. If there's slack, split: shrink this slot to
// exactly `bytes` and insert a new free slot for the rest.
if (slot->size > bytes) { if (slot->size > bytes) {
remainder = newSlot(slot->offset + bytes, slot->size - bytes, false); remainder = newSlot(slot->offset + bytes, slot->size - bytes, false);
if (remainder == NULL) { if (remainder == NULL) {
@ -127,17 +247,11 @@ ArenaSlotT *codegenArenaAlloc(uint32_t bytes) {
gUsedBytes += bytes; gUsedBytes += bytes;
return slot; return slot;
} }
}
return NULL; return NULL;
} }
// codegenArenaBase() / codegenArenaBaseAddr() are now header-only
// macros that read gCodegenArenaBase / gCodegenArenaBaseAddr
// directly, so the C function bodies that used to live here are
// gone. The wrappers cost ~30 cyc per call on IIgs and were hit
// 3x per sprite frame.
uint32_t codegenArenaBytesTotal(void) { uint32_t codegenArenaBytesTotal(void) {
return gTotalBytes; return gTotalBytes;
} }
@ -149,63 +263,16 @@ uint32_t codegenArenaBytesUsed(void) {
void codegenArenaCompact(void) { void codegenArenaCompact(void) {
ArenaSlotT *slot; uint8_t bank;
ArenaSlotT *next;
ArenaSlotT *trailing;
uint32_t cursor;
if (gCodegenArenaBase == NULL) { for (bank = 0u; bank < gBankCount; bank++) {
return; bankCompact(bank);
}
cursor = 0;
slot = gFirstSlot;
while (slot != NULL) {
next = slot->next;
if (slot->used) {
if (slot->offset != cursor) {
memmove(gCodegenArenaBase + cursor, gCodegenArenaBase + slot->offset, slot->size);
slot->offset = cursor;
}
cursor += slot->size;
slot = next;
continue;
}
// Free slot: drop from the list. The caller-side ArenaSlotT*
// for any free slot was already invalidated when it was
// freed (coalesce released the struct); nothing live points
// at it.
if (slot->prev != NULL) {
slot->prev->next = next;
} else {
gFirstSlot = next;
}
if (next != NULL) {
next->prev = slot->prev;
}
free(slot);
slot = next;
}
if (cursor < gTotalBytes) {
trailing = newSlot(cursor, gTotalBytes - cursor, false);
if (trailing == NULL) {
return; // Compaction succeeded; just skip the free-slot record.
}
trailing->prev = NULL;
for (slot = gFirstSlot; slot != NULL && slot->next != NULL; slot = slot->next) {
// walk to last
}
if (slot == NULL) {
gFirstSlot = trailing;
} else {
slot->next = trailing;
trailing->prev = slot;
}
} }
} }
void codegenArenaFree(ArenaSlotT *slot) { void codegenArenaFree(ArenaSlotT *slot) {
if (slot == NULL || gCodegenArenaBase == NULL) { if (slot == NULL || gBankCount == 0u) {
return; return;
} }
if (!slot->used) { if (!slot->used) {
@ -220,100 +287,51 @@ void codegenArenaFree(ArenaSlotT *slot) {
bool codegenArenaInit(uint32_t totalBytes) { bool codegenArenaInit(uint32_t totalBytes) {
if (gCodegenArenaBase != NULL) { uint32_t want;
if (gBankCount != 0u) {
return true; return true;
} }
if (totalBytes == 0) { if (totalBytes == 0) {
return false; return false;
} }
#if defined(JOEYLIB_PLATFORM_IIGS) gTotalBytes = 0;
// attrNoCross forbids the block from crossing a 64KB bank boundary,
// so any request larger than one bank (65536 bytes) is unsatisfiable
// and NewHandle would always return NULL. Clamp to a single bank.
// The clamped request can still fail below -- a 64KB attrNoCross
// block needs a completely free bank -- and the caller's error
// message covers that.
if (totalBytes > 65536UL) {
totalBytes = 65536UL;
}
// MMStartUp() returns this application's master user ID (idempotent;
// the system starts the Memory Manager before the app runs). A NULL
// handle is the failure signal.
{
uint16_t ownerId;
ownerId = MMStartUp();
gCodegenArenaBaseHandle = NewHandle((unsigned long)totalBytes, ownerId,
attrFixed | attrLocked | attrPage | attrNoCross,
(void *)0);
}
if (gCodegenArenaBaseHandle == NULL) {
return false;
}
HLock(gCodegenArenaBaseHandle);
// Capture the 24-bit absolute address by copying the master
// pointer's raw bytes; bytes[3] (bank-high/flags) is masked off.
{
void *p;
uint8_t bytes[4];
p = *(void **)gCodegenArenaBaseHandle;
gCodegenArenaBase = (uint8_t *)p;
memcpy(bytes, &p, 4);
gCodegenArenaBaseAddr = (uint32_t)bytes[0]
| ((uint32_t)bytes[1] << 8)
| ((uint32_t)bytes[2] << 16);
}
if (gCodegenArenaBase == NULL) {
DisposeHandle(gCodegenArenaBaseHandle);
gCodegenArenaBaseHandle = NULL;
return false;
}
#else
gCodegenArenaBase = (uint8_t *)malloc(totalBytes);
if (gCodegenArenaBase == NULL) {
return false;
}
gCodegenArenaBaseAddr = (uint32_t)gCodegenArenaBase;
#endif
gFirstSlot = newSlot(0, totalBytes, false);
if (gFirstSlot == NULL) {
#if defined(JOEYLIB_PLATFORM_IIGS)
DisposeHandle(gCodegenArenaBaseHandle);
gCodegenArenaBaseHandle = NULL;
#else
free(gCodegenArenaBase);
#endif
gCodegenArenaBase = NULL;
gCodegenArenaBaseAddr = 0;
return false;
}
gTotalBytes = totalBytes;
gUsedBytes = 0; gUsedBytes = 0;
return true; #if defined(JOEYLIB_PLATFORM_IIGS)
// 64 KB banks until the request is met or the machine runs out: a
// whole bank needs a completely free one, so a bank that is refused
// is retried at 60 KB (any page-aligned run inside a bank) before
// giving up. Whatever was granted is the arena.
want = totalBytes;
while (want > 0u && gBankCount < CODEGEN_ARENA_BANKS_MAX) {
uint32_t bytes = (want > CODEGEN_ARENA_IIGS_BANK) ? CODEGEN_ARENA_IIGS_BANK : want;
if (!bankAlloc(gBankCount, bytes)) {
if (bytes <= 60ul * 1024ul || !bankAlloc(gBankCount, 60ul * 1024ul)) {
break;
}
}
gTotalBytes += gBankSize[gBankCount];
gBankCount++;
want = (want > gBankSize[gBankCount - 1u]) ? want - gBankSize[gBankCount - 1u] : 0u;
}
#else
want = totalBytes;
if (bankAlloc(0u, want)) {
gTotalBytes = want;
gBankCount = 1u;
}
#endif
return gBankCount != 0u;
} }
void codegenArenaShutdown(void) { void codegenArenaShutdown(void) {
ArenaSlotT *slot; uint8_t bank;
ArenaSlotT *next;
if (gCodegenArenaBase == NULL) { for (bank = 0u; bank < gBankCount; bank++) {
return; bankFree(bank);
} }
for (slot = gFirstSlot; slot != NULL; slot = next) { gBankCount = 0u;
next = slot->next;
free(slot);
}
#if defined(JOEYLIB_PLATFORM_IIGS)
DisposeHandle(gCodegenArenaBaseHandle);
gCodegenArenaBaseHandle = NULL;
#else
free(gCodegenArenaBase);
#endif
gCodegenArenaBase = NULL;
gCodegenArenaBaseAddr = 0;
gFirstSlot = NULL;
gTotalBytes = 0; gTotalBytes = 0;
gUsedBytes = 0; gUsedBytes = 0;
} }

View file

@ -3,13 +3,21 @@
// coalescing on free; manual compaction (codegenArenaCompact) // coalescing on free; manual compaction (codegenArenaCompact)
// memmoves live slots down to consolidate free space. // memmoves live slots down to consolidate free space.
// //
// The arena is one or more BANKS. Every port but the IIgs has a single
// bank of jlConfigT.codegenBytes from the C heap. On the IIgs a bank is
// a Memory Manager block that may not cross a 64 KB boundary (compiled
// routines are called by 24-bit JSL and run with PC inside one bank), so
// a request larger than 64 KB becomes several banks and a compiled cel
// lives wholly inside one of them. A slot's `offset` carries its bank in
// the top byte and its byte offset within the bank below.
//
// jlSpriteT references slots via ArenaSlotT*, never raw function // jlSpriteT references slots via ArenaSlotT*, never raw function
// pointers. jlSpriteDraw computes the call address each invocation as // pointers. jlSpriteDraw computes the call address each invocation as
// `gArenaBase + slot->offset + routineOffsets[shift][op]` so a // `codegenArenaSlotAddr(slot) + routineOffsets[shift][op]` so a
// compaction that moves a slot's bytes is transparent to callers. // compaction that moves a slot's bytes is transparent to callers.
// //
// Allocations go through plain malloc; on every supported port // Allocations go through plain malloc off the IIgs; on every supported
// (IIgs, Amiga libnix, ST mintlib, DJGPP+CWSDPMI) the heap is // port (Amiga libnix, ST mintlib, DJGPP+CWSDPMI, X68000) the heap is
// readable, writable, AND executable. We are not running in a W^X // readable, writable, AND executable. We are not running in a W^X
// environment. // environment.
// //
@ -24,23 +32,30 @@
#include "joey/types.h" #include "joey/types.h"
typedef struct ArenaSlotT { typedef struct ArenaSlotT {
uint32_t offset; // byte offset within the arena base uint32_t offset; // bank << CODEGEN_ARENA_BANK_SHIFT | byte offset within the bank
uint32_t size; // bytes occupied by this slot uint32_t size; // bytes occupied by this slot
bool used; // true = held by a sprite; false = free bool used; // true = held by a sprite; false = free
struct ArenaSlotT *next; // linked-list, sorted by offset struct ArenaSlotT *next; // linked-list, sorted by offset, one list per bank
struct ArenaSlotT *prev; struct ArenaSlotT *prev;
} ArenaSlotT; } ArenaSlotT;
// Banks: at most this many, and the IIgs's bank is one 64 KB block (the
// Memory Manager refuses a no-cross block any larger).
#define CODEGEN_ARENA_BANKS_MAX 4u
#define CODEGEN_ARENA_BANK_SHIFT 24u
#define CODEGEN_ARENA_BANK_MASK 0x00FFFFFFul
#define CODEGEN_ARENA_IIGS_BANK 65536ul
// Lifecycle. Returns false if totalBytes is 0 or the underlying // Lifecycle. Returns false if totalBytes is 0 or no bank at all could be
// allocation fails. Idempotent: calling Init twice without an // allocated; a request the machine cannot fully satisfy yields as many
// intervening Shutdown is a no-op. // banks as it can (codegenArenaBytesTotal tells). Idempotent: calling
// Init twice without an intervening Shutdown is a no-op.
bool codegenArenaInit(uint32_t totalBytes); bool codegenArenaInit(uint32_t totalBytes);
void codegenArenaShutdown(void); void codegenArenaShutdown(void);
// First-fit allocate `bytes` of executable memory. Returns NULL if // First-fit allocate `bytes` of executable memory, in the first bank with
// no free slot is large enough -- the caller should run // room. Returns NULL if no free slot is large enough -- the caller should
// codegenArenaCompact and retry, or surface the failure. // run codegenArenaCompact and retry, or surface the failure.
ArenaSlotT *codegenArenaAlloc(uint32_t bytes); ArenaSlotT *codegenArenaAlloc(uint32_t bytes);
// Mark the slot free and merge with adjacent free neighbors. The // Mark the slot free and merge with adjacent free neighbors. The
@ -48,29 +63,23 @@ ArenaSlotT *codegenArenaAlloc(uint32_t bytes);
// freed (if it coalesced into a neighbor). // freed (if it coalesced into a neighbor).
void codegenArenaFree(ArenaSlotT *slot); void codegenArenaFree(ArenaSlotT *slot);
// Walk live slots in offset order, memmove each down to fill any // In every bank: walk live slots in offset order, memmove each down to
// preceding hole, drop all free slots from the list, and finish with // fill any preceding hole, drop all free slots from the list, and finish
// one trailing free slot covering the remaining space. Per-slot // with one trailing free slot covering the remaining space. Per-slot
// `offset` fields are updated atomically with the memmove so any // `offset` fields are updated atomically with the memmove so any callers
// callers indexing through `gArenaBase + slot->offset` see the new // resolving the slot see the new value on their next read.
// value on their next read.
void codegenArenaCompact(void); void codegenArenaCompact(void);
// Used for jlSpriteDraw's address computation. The base pointer is // Where a slot's bytes are: as a pointer for the C side, and as the
// stable for the lifetime of the arena; only slot->offset moves. // 24-bit absolute address the IIgs JSLs to. The bank tables are read-only
// // after codegenArenaInit and exposed directly so the per-draw hot paths
// Direct extern access (instead of a getter function) so per-frame // pay an indexed load, not a call.
// hot paths in jlSpriteCompile.c skip the JSL/PHB/RTL/PLB the wrapper extern uint8_t *gCodegenBankBase[CODEGEN_ARENA_BANKS_MAX];
// would impose. Both globals are read-only after codegenArenaInit; extern uint32_t gCodegenBankAddr[CODEGEN_ARENA_BANKS_MAX];
// the function-form getters below are kept as a back-compat shim. #define codegenArenaSlotBase(slot) (gCodegenBankBase[(slot)->offset >> CODEGEN_ARENA_BANK_SHIFT] + ((slot)->offset & CODEGEN_ARENA_BANK_MASK))
extern uint8_t *gCodegenArenaBase; #define codegenArenaSlotAddr(slot) (gCodegenBankAddr[(slot)->offset >> CODEGEN_ARENA_BANK_SHIFT] + ((slot)->offset & CODEGEN_ARENA_BANK_MASK))
extern uint32_t gCodegenArenaBaseAddr;
#define codegenArenaBase() ((uint8_t *)gCodegenArenaBase)
#define codegenArenaBaseAddr() ((uint32_t)gCodegenArenaBaseAddr)
// Public-API support: sum of live slot sizes, total arena size. // Occupancy across all banks.
// Difference is free space (which may be fragmented across holes
// until codegenArenaCompact runs).
uint32_t codegenArenaBytesUsed(void); uint32_t codegenArenaBytesUsed(void);
uint32_t codegenArenaBytesTotal(void); uint32_t codegenArenaBytesTotal(void);

View file

@ -85,6 +85,11 @@ void *jlAlloc(uint32_t bytes) {
} }
void jlMemCopy(void *dst, const void *src, uint32_t bytes) {
jlpMemCopy(dst, src, bytes);
}
void jlFree(void *p) { void jlFree(void *p) {
jlpBigFree(p); jlpBigFree(p);
} }

View file

@ -238,10 +238,11 @@ void musicPump(void) {
now = jlMillisElapsed(); now = jlMillisElapsed();
if ((int32_t)(now - gMusicNextMs) < 0) { if ((int32_t)(now - gMusicNextMs) < 0) {
#if defined(JOEYLIB_PLATFORM_IIGS) #if defined(JOEYLIB_PLATFORM_IIGS)
// The IIgs backends are ~80 ms one-shots retriggered per call // The IIgs backends are finite one-shots (DOC oscillators have
// (DOC oscillators have no gate to hold); re-assert the active // no gate to hold); re-assert the active voices every frame so
// voices every frame so held notes sustain. The other ports' // held notes sustain -- the HAL restarts a voice only when its
// tone/noise hardware holds a programmed voice on its own. // one-shot is about to end. The other ports' tone/noise
// hardware holds a programmed voice on its own.
for (voice = 0; voice < MUSIC_VOICE_SLOTS; voice++) { for (voice = 0; voice < MUSIC_VOICE_SLOTS; voice++) {
if (gMusicOn[voice]) { if (gMusicOn[voice]) {
musicApplyVoice(voice); musicApplyVoice(voice);
@ -254,9 +255,12 @@ void musicPump(void) {
gMusicNextMs = now; gMusicNextMs = now;
} }
// The event budget counts DOWN at the end of the body: as a `> 0u`
// guard llvm816 compiled it to `cmp #0 / inc a / bne`, and the inc
// clobbers the compare's flags (the same backend hole that broke
// Space Taxi's collision rows on the IIgs, 2026-09-21).
eventsLeft = MUSIC_MAX_EVENTS_PER_PUMP; eventsLeft = MUSIC_MAX_EVENTS_PER_PUMP;
while (gMusicPlaying && eventsLeft > 0u && (int32_t)(now - gMusicNextMs) >= 0) { while (gMusicPlaying && (int32_t)(now - gMusicNextMs) >= 0) {
eventsLeft--;
if (gMusicPos >= gMusicLength) { if (gMusicPos >= gMusicLength) {
musicHalt(); musicHalt();
return; return;
@ -304,5 +308,9 @@ void musicPump(void) {
musicHalt(); // malformed opcode: hard stop musicHalt(); // malformed opcode: hard stop
return; return;
} }
eventsLeft--;
if (eventsLeft == 0u) {
break;
}
} }
} }

View file

@ -122,5 +122,6 @@ void jlPaletteSet(jlSurfaceT *s, uint8_t paletteIndex, const uint16_t *colors16)
row[15] = colors16[15]; row[15] = colors16[15];
if (s == gStage) { if (s == gStage) {
gStagePaletteDirty = true; gStagePaletteDirty = true;
gStagePaletteDirtyMask |= (uint16_t)(1u << paletteIndex);
} }
} }

View file

@ -282,13 +282,24 @@ extern jlSurfaceT *gStage;
#define jlpSpriteSavePlanes(_s, _x, _y, _w, _h, _dst) ((void)0) #define jlpSpriteSavePlanes(_s, _x, _y, _w, _h, _dst) ((void)0)
#define jlpSpriteRestorePlanes(_s, _x, _y, _w, _h, _src) ((void)0) #define jlpSpriteRestorePlanes(_s, _x, _y, _w, _h, _src) ((void)0)
// frameCount: straight to the GetTick asm wrapper (peislam.s). The hal.c // frameCount: straight to the heartbeat tick read in peislam.s (a VBL
// C wrapper this replaces was one of two pure forwarding layers between // interrupt task keeps the count; the read is a same-bank word load).
// jlFrameCount and the Misc Toolset GetTick call -- ~60-80 cycles each on // The hal.c C wrapper this replaces was a pure forwarding layer on a
// a clock that game loops (and the UBER bench loop) poll constantly. // clock that game loops (and the UBER bench loop) poll constantly.
extern uint16_t iigsGetTickWord(void); extern uint16_t iigsGetTickWord(void);
#define jlpFrameCount() iigsGetTickWord() #define jlpFrameCount() iigsGetTickWord()
// jlMemCopy: MVN block moves, split at bank boundaries in hal.c.
void iigsMemCopy(void *dst, const void *src, uint32_t bytes);
#define jlpMemCopy(_d, _s, _n) iigsMemCopy((_d), (_s), (_n))
// The one Memory Manager owner ID every IIgs allocation uses (hal.c).
uint16_t iigsMemUserId(void);
// SFX streams go to NTPstreamsound as unsigned bytes with 0x00 forbidden
// (joey/audio.h JL_SFX_RAW_UNSIGNED8_NOZERO): a raw producer hands
// audio_full.c chunks it can stream without the per-byte conversion.
#define jlpAudioSfxRawFormat() 1u
// ===================================================================== // =====================================================================
// JL_HAS registry consistency checks (PERF-AUDIT #26). // JL_HAS registry consistency checks (PERF-AUDIT #26).
// //
@ -767,6 +778,9 @@ uint16_t jlpGenericFrameHz(void);
#endif #endif
uint16_t jlpGenericFrameCount(void); uint16_t jlpGenericFrameCount(void);
#if !defined(jlpMemCopy)
#define jlpMemCopy(_d, _s, _n) memcpy((_d), (_s), (size_t)(_n))
#endif
#if !defined(jlpFrameCount) #if !defined(jlpFrameCount)
#if defined(JL_HAS_FRAME_COUNT) #if defined(JL_HAS_FRAME_COUNT)
uint16_t jlpFrameCount(void); uint16_t jlpFrameCount(void);
@ -851,6 +865,27 @@ void jlpGenericAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *c
#define jlpAudioPlaySfxStream(_a, _b, _c, _d) jlpGenericAudioPlaySfxStream((_a), (_b), (_c), (_d)) #define jlpAudioPlaySfxStream(_a, _b, _c, _d) jlpGenericAudioPlaySfxStream((_a), (_b), (_c), (_d))
#endif #endif
#endif #endif
// Raw-format stream (joey/audio.h jlAudioPlaySfxStreamRaw): a port whose
// stream bytes are not signed PCM overrides both; everyone else streams
// signed PCM, so raw IS the plain stream and the format is 0 (signed).
#if !defined(jlpAudioPlaySfxStreamRaw)
#if defined(JL_HAS_AUDIO_PLAY_SFX_STREAM_RAW)
void jlpAudioPlaySfxStreamRaw(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz);
#else
#define jlpAudioPlaySfxStreamRaw(_a, _b, _c, _d) jlpAudioPlaySfxStream((_a), (_b), (_c), (_d))
#endif
#endif
#if !defined(jlpAudioSfxRawFormat)
#define jlpAudioSfxRawFormat() 0u
#endif
void jlpGenericAudioSetPan(uint8_t slot, uint8_t pan);
#if !defined(jlpAudioSetPan)
#if defined(JL_HAS_AUDIO_SET_PAN)
void jlpAudioSetPan(uint8_t slot, uint8_t pan);
#else
#define jlpAudioSetPan(_s, _p) jlpGenericAudioSetPan((_s), (_p))
#endif
#endif
void jlpGenericAudioStopSfx(uint8_t slot); void jlpGenericAudioStopSfx(uint8_t slot);
#if !defined(jlpAudioStopSfx) #if !defined(jlpAudioStopSfx)
#if defined(JL_HAS_AUDIO_STOP_SFX) #if defined(JL_HAS_AUDIO_STOP_SFX)

View file

@ -55,8 +55,7 @@ static inline __attribute__((always_inline)) void spriteCompiledDraw(jlSurfaceT
// sets up its own DBR/Y and restores DBR on exit. routeOffset is the // sets up its own DBR/Y and restores DBR on exit. routeOffset is the
// gate-derived DRAW offset for this shift (#29). // gate-derived DRAW offset for this shift (#29).
dstRow0 = &dst->pixels[SURFACE_ROW_OFFSET(y) + ((uint16_t)x >> 1)]; dstRow0 = &dst->pixels[SURFACE_ROW_OFFSET(y) + ((uint16_t)x >> 1)];
fnAddr = codegenArenaBaseAddr() fnAddr = codegenArenaSlotAddr(sp->slot)
+ sp->slot->offset
+ (uint32_t)routeOffset; + (uint32_t)routeOffset;
drawFn = (void (*)(uint8_t *))fnAddr; drawFn = (void (*)(uint8_t *))fnAddr;
drawFn(dstRow0); drawFn(dstRow0);
@ -131,8 +130,7 @@ extern void iigsSpriteSaveStage(uint32_t fnAddr, uint16_t backupLo, uint16_t x,
static inline __attribute__((always_inline)) void spriteCompiledDrawMark(const jlSpriteT *sp, int16_t x, int16_t y, uint16_t routeOffset) { static inline __attribute__((always_inline)) void spriteCompiledDrawMark(const jlSpriteT *sp, int16_t x, int16_t y, uint16_t routeOffset) {
uint32_t fnAddr; uint32_t fnAddr;
fnAddr = codegenArenaBaseAddr() fnAddr = codegenArenaSlotAddr(sp->slot)
+ sp->slot->offset
+ (uint32_t)routeOffset; + (uint32_t)routeOffset;
iigsSpriteDrawMark(fnAddr, (uint16_t)x, (uint16_t)y, sp->widthPx, sp->heightPx); iigsSpriteDrawMark(fnAddr, (uint16_t)x, (uint16_t)y, sp->widthPx, sp->heightPx);
} }
@ -188,8 +186,7 @@ static inline __attribute__((always_inline)) void spriteCompiledSaveUnder(const
cachedDst = (uint8_t *)sp->cachedDstBank + cacheIdx; cachedDst = (uint8_t *)sp->cachedDstBank + cacheIdx;
cachedSrc = (uint8_t *)sp->cachedSrcBank + cacheIdx; cachedSrc = (uint8_t *)sp->cachedSrcBank + cacheIdx;
fnAddr = codegenArenaBaseAddr() fnAddr = codegenArenaSlotAddr(sp->slot)
+ sp->slot->offset
+ (uint32_t)routeOffset; + (uint32_t)routeOffset;
// Patch the MVN bank operands only if the dst/src bank pair changed since // Patch the MVN bank operands only if the dst/src bank pair changed since
@ -259,8 +256,7 @@ static inline __attribute__((always_inline)) void spriteCompiledSaveUnderStage(j
cachedDst = (uint8_t *)sp->cachedDstBank + cacheIdx; cachedDst = (uint8_t *)sp->cachedDstBank + cacheIdx;
cachedSrc = (uint8_t *)sp->cachedSrcBank + cacheIdx; cachedSrc = (uint8_t *)sp->cachedSrcBank + cacheIdx;
fnAddr = codegenArenaBaseAddr() fnAddr = codegenArenaSlotAddr(sp->slot)
+ sp->slot->offset
+ (uint32_t)routeOffset; + (uint32_t)routeOffset;
if (*cachedDst != backupBank || *cachedSrc != screenBank) { if (*cachedDst != backupBank || *cachedSrc != screenBank) {
@ -301,8 +297,7 @@ static inline __attribute__((always_inline)) void spriteCompiledRestoreUnder(jlS
cachedDst = (uint8_t *)sp->cachedDstBank + cacheIdx; cachedDst = (uint8_t *)sp->cachedDstBank + cacheIdx;
cachedSrc = (uint8_t *)sp->cachedSrcBank + cacheIdx; cachedSrc = (uint8_t *)sp->cachedSrcBank + cacheIdx;
fnAddr = codegenArenaBaseAddr() fnAddr = codegenArenaSlotAddr(sp->slot)
+ sp->slot->offset
+ (uint32_t)routeOffset; + (uint32_t)routeOffset;
// Patch the MVN bank operands only if they changed (the routine's only // Patch the MVN bank operands only if they changed (the routine's only
@ -344,8 +339,7 @@ static inline __attribute__((always_inline)) void spriteCompiledRestoreMark(jlSp
SPLIT_POINTER(backup->bytes, &backupLo, &backupBank); SPLIT_POINTER(backup->bytes, &backupLo, &backupBank);
screenBank = ((const uint8_t *)&gStage->pixels)[2]; screenBank = ((const uint8_t *)&gStage->pixels)[2];
fnAddr = codegenArenaBaseAddr() fnAddr = codegenArenaSlotAddr(sp->slot)
+ sp->slot->offset
+ (uint32_t)routeOffset; + (uint32_t)routeOffset;
cacheIdx = SPRITE_ROUTINE_INDEX(shift, SPRITE_OP_RESTORE); cacheIdx = SPRITE_ROUTINE_INDEX(shift, SPRITE_OP_RESTORE);
@ -413,7 +407,7 @@ static inline __attribute__((always_inline)) void spriteCompiledDraw(jlSurfaceT
return; return;
} }
byteOff = (uint16_t)((uint16_t)y * JL_PLANAR_BYTES_PER_ROW + ((uint16_t)x >> 3)); byteOff = (uint16_t)((uint16_t)y * JL_PLANAR_BYTES_PER_ROW + ((uint16_t)x >> 3));
fn = (DrawFn)(codegenArenaBase() + sp->slot->offset + routeOffset); fn = (DrawFn)(codegenArenaSlotBase(sp->slot) + routeOffset);
fn(pd->planes[0] + byteOff, pd->planes[1] + byteOff, pd->planes[2] + byteOff, pd->planes[3] + byteOff); fn(pd->planes[0] + byteOff, pd->planes[1] + byteOff, pd->planes[2] + byteOff, pd->planes[3] + byteOff);
} }
@ -448,7 +442,7 @@ static inline __attribute__((always_inline)) void spriteCompiledSaveUnder(const
backup->height = sp->heightPx; backup->height = sp->heightPx;
backup->sizeBytes = (uint16_t)((uint16_t)(nGroups << 3) * sp->heightPx); backup->sizeBytes = (uint16_t)((uint16_t)(nGroups << 3) * sp->heightPx);
fn = (CopyFn)(codegenArenaBase() + sp->slot->offset + routeOffset); fn = (CopyFn)(codegenArenaSlotBase(sp->slot) + routeOffset);
fn(pd->planes[0] + byteOff, pd->planes[1] + byteOff, pd->planes[2] + byteOff, pd->planes[3] + byteOff, backup->bytes); fn(pd->planes[0] + byteOff, pd->planes[1] + byteOff, pd->planes[2] + byteOff, pd->planes[3] + byteOff, backup->bytes);
} }
@ -465,7 +459,7 @@ static inline __attribute__((always_inline)) void spriteCompiledRestoreUnder(jlS
return; return;
} }
byteOff = (uint16_t)((uint16_t)backup->y * JL_PLANAR_BYTES_PER_ROW + ((uint16_t)backup->x >> 3)); byteOff = (uint16_t)((uint16_t)backup->y * JL_PLANAR_BYTES_PER_ROW + ((uint16_t)backup->x >> 3));
fn = (CopyFn)(codegenArenaBase() + backup->sprite->slot->offset + routeOffset); fn = (CopyFn)(codegenArenaSlotBase(backup->sprite->slot) + routeOffset);
fn(pd->planes[0] + byteOff, pd->planes[1] + byteOff, pd->planes[2] + byteOff, pd->planes[3] + byteOff, backup->bytes); fn(pd->planes[0] + byteOff, pd->planes[1] + byteOff, pd->planes[2] + byteOff, pd->planes[3] + byteOff, backup->bytes);
} }
@ -516,7 +510,7 @@ static inline __attribute__((always_inline)) void spriteCompiledDraw(jlSurfaceT
groupBase = pd->base groupBase = pd->base
+ (uint16_t)y * ST_GROUP_BYTES_PER_ROW + (uint16_t)y * ST_GROUP_BYTES_PER_ROW
+ (uint16_t)((uint16_t)x >> 4) * ST_BYTES_PER_PLANE_GROUP; + (uint16_t)((uint16_t)x >> 4) * ST_BYTES_PER_PLANE_GROUP;
fn = (DrawFn)(codegenArenaBase() + sp->slot->offset + routeOffset); fn = (DrawFn)(codegenArenaSlotBase(sp->slot) + routeOffset);
fn(groupBase); fn(groupBase);
} }
@ -553,7 +547,7 @@ static inline __attribute__((always_inline)) void spriteCompiledSaveUnder(const
backup->height = sp->heightPx; backup->height = sp->heightPx;
backup->sizeBytes = (uint16_t)((uint16_t)(nGroups << 3) * sp->heightPx); backup->sizeBytes = (uint16_t)((uint16_t)(nGroups << 3) * sp->heightPx);
fn = (CopyFn)(codegenArenaBase() + sp->slot->offset + routeOffset); fn = (CopyFn)(codegenArenaSlotBase(sp->slot) + routeOffset);
fn(rowStart, backup->bytes); fn(rowStart, backup->bytes);
} }
@ -572,7 +566,7 @@ static inline __attribute__((always_inline)) void spriteCompiledRestoreUnder(jlS
rowStart = pd->base rowStart = pd->base
+ (uint16_t)backup->y * ST_GROUP_BYTES_PER_ROW + (uint16_t)backup->y * ST_GROUP_BYTES_PER_ROW
+ (uint16_t)((uint16_t)backup->x >> 4) * ST_BYTES_PER_PLANE_GROUP; + (uint16_t)((uint16_t)backup->x >> 4) * ST_BYTES_PER_PLANE_GROUP;
fn = (CopyFn)(codegenArenaBase() + backup->sprite->slot->offset + routeOffset); fn = (CopyFn)(codegenArenaSlotBase(backup->sprite->slot) + routeOffset);
fn(backup->bytes, rowStart); fn(backup->bytes, rowStart);
} }
@ -584,7 +578,7 @@ static inline __attribute__((always_inline)) void spriteCompiledDraw(jlSurfaceT
DrawFn fn; DrawFn fn;
destRow = &dst->pixels[(uint16_t)y * SURFACE_BYTES_PER_ROW + ((uint16_t)x >> 1)]; destRow = &dst->pixels[(uint16_t)y * SURFACE_BYTES_PER_ROW + ((uint16_t)x >> 1)];
fn = (DrawFn)(codegenArenaBase() + sp->slot->offset + routeOffset); fn = (DrawFn)(codegenArenaSlotBase(sp->slot) + routeOffset);
fn(destRow); fn(destRow);
} }
@ -615,7 +609,7 @@ static inline __attribute__((always_inline)) void spriteCompiledSaveUnder(const
backup->height = heightPx; backup->height = heightPx;
backup->sizeBytes = (uint16_t)(copyBytes * heightPx); backup->sizeBytes = (uint16_t)(copyBytes * heightPx);
fn = (CopyFn)(codegenArenaBase() + sp->slot->offset + routeOffset); fn = (CopyFn)(codegenArenaSlotBase(sp->slot) + routeOffset);
fn(screenPtr, backup->bytes); fn(screenPtr, backup->bytes);
} }
@ -635,7 +629,7 @@ static inline __attribute__((always_inline)) void spriteCompiledRestoreUnder(jlS
(void)shift; (void)shift;
sp = backup->sprite; sp = backup->sprite;
screenPtr = (uint8_t *)&dst->pixels[(uint16_t)backup->y * SURFACE_BYTES_PER_ROW + ((uint16_t)backup->x >> 1)]; screenPtr = (uint8_t *)&dst->pixels[(uint16_t)backup->y * SURFACE_BYTES_PER_ROW + ((uint16_t)backup->x >> 1)];
fn = (CopyFn)(codegenArenaBase() + sp->slot->offset + routeOffset); fn = (CopyFn)(codegenArenaSlotBase(sp->slot) + routeOffset);
fn(backup->bytes, screenPtr); fn(backup->bytes, screenPtr);
} }

View file

@ -153,7 +153,7 @@ struct jlSpriteT {
// Compiled-path state. slot==NULL means not yet compiled (or // Compiled-path state. slot==NULL means not yet compiled (or
// compile failed); jlSpriteDraw falls back to the interpreter. // compile failed); jlSpriteDraw falls back to the interpreter.
// The fn-call address for (shift, op) is computed at draw time: // The fn-call address for (shift, op) is computed at draw time:
// (codegenArenaBase() + slot->offset + routineOffsets[shift][op]) // (codegenArenaSlotAddr(slot) + routineOffsets[shift][op])
// so a codegenArenaCompact that moves the slot's bytes is // so a codegenArenaCompact that moves the slot's bytes is
// transparent to the caller. // transparent to the caller.
ArenaSlotT *slot; ArenaSlotT *slot;

View file

@ -56,6 +56,7 @@ uint32_t gPresentScannedRows = 0;
// code reads and clears the flags after uploading. // code reads and clears the flags after uploading.
bool gStageScbDirty = true; bool gStageScbDirty = true;
bool gStagePaletteDirty = true; bool gStagePaletteDirty = true;
uint16_t gStagePaletteDirtyMask = 0xFFFFu;
// ----- Internal helpers (alphabetical) ----- // ----- Internal helpers (alphabetical) -----
@ -86,6 +87,7 @@ void jlSurfaceCopy(jlSurfaceT *dst, const jlSurfaceT *src) {
if (dst == gStage) { if (dst == gStage) {
gStageScbDirty = true; gStageScbDirty = true;
gStagePaletteDirty = true; gStagePaletteDirty = true;
gStagePaletteDirtyMask = 0xFFFFu;
} }
} }
@ -243,6 +245,7 @@ bool jlSurfaceLoadFile(jlSurfaceT *dst, const char *path) {
if (dst == gStage) { if (dst == gStage) {
gStageScbDirty = true; gStageScbDirty = true;
gStagePaletteDirty = true; gStagePaletteDirty = true;
gStagePaletteDirtyMask = 0xFFFFu;
} }
return true; return true;
} }
@ -429,6 +432,7 @@ bool stageAlloc(void) {
// of memcmp'ing cached SCB/palette mirrors every present). // of memcmp'ing cached SCB/palette mirrors every present).
gStageScbDirty = true; gStageScbDirty = true;
gStagePaletteDirty = true; gStagePaletteDirty = true;
gStagePaletteDirtyMask = 0xFFFFu;
return true; return true;
} }

View file

@ -118,6 +118,11 @@ extern jlSurfaceT *gStage;
// 712-byte memcmp pair the IIgs port used to run unconditionally. // 712-byte memcmp pair the IIgs port used to run unconditionally.
extern bool gStageScbDirty; extern bool gStageScbDirty;
extern bool gStagePaletteDirty; extern bool gStagePaletteDirty;
// Companion to gStagePaletteDirty: bit N set = palette N changed since the
// last upload. A port that can upload per palette (IIgs: 32 bytes each,
// not 512) consults this; the bool stays the coarse "anything" flag the
// other ports read. Both are set together and cleared together.
extern uint16_t gStagePaletteDirtyMask;
// Coarse dirty map: one byte per GROUP of 8 stage rows, non-zero when // Coarse dirty map: one byte per GROUP of 8 stage rows, non-zero when
// any row in the group may be dirty. It is a conservative hint, never // any row in the group may be dirty. It is a conservative hint, never

View file

@ -134,6 +134,13 @@ void jlpGenericAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *c
(void)rateHz; (void)rateHz;
} }
// Mono ports (DOS, ST) have nowhere to put a pan; the header says so.
void jlpGenericAudioSetPan(uint8_t slot, uint8_t pan) {
(void)slot;
(void)pan;
}
void jlpGenericAudioStopSfx(uint8_t slot) { void jlpGenericAudioStopSfx(uint8_t slot) {
(void)slot; (void)slot;
} }

File diff suppressed because it is too large Load diff

View file

@ -34,11 +34,17 @@
// Memory Manager surface for jlpBigAlloc / jlpBigFree (jlAlloc backing). // Memory Manager surface for jlpBigAlloc / jlpBigFree (jlAlloc backing).
// Same idiom as src/iigs/audio_full.c: Handles/pointers are void*, // Same idiom as src/iigs/audio_full.c: Handles/pointers are void*,
// the owner ID comes from MMStartUp(), and the attribute bit values are // the owner ID comes from iigsMemUserId(), and the attribute bit values
// Apple's Memory Manager constants. Only the bits we use are defined. // are Apple's Memory Manager constants. Only the bits we use are defined.
//
// _ownerid used to expand to MMStartUp() at EVERY call site. MMStartUp
// hands out a NEW memory ID each time it is called, so every allocation
// burned one of the Memory Manager's application IDs; once the pool ran
// dry the next NewHandle went wild (the UBER probe build BRK'd into the
// ROM monitor inside jlSurfaceCreate, 2026-09-22). One ID, fetched once.
#include <iigs/toolbox.h> #include <iigs/toolbox.h>
typedef void *Handle; typedef void *Handle;
#define _ownerid (MMStartUp()) #define _ownerid (iigsMemUserId())
#define attrAddr 0x0002 #define attrAddr 0x0002
#define attrNoSpec 0x0020 #define attrNoSpec 0x0020
#define attrFixed 0x4000 #define attrFixed 0x4000
@ -48,14 +54,25 @@ typedef void *Handle;
// stash it just before the pointer we return. // stash it just before the pointer we return.
#define BIG_ALLOC_HDR ((uint32_t)sizeof(Handle)) #define BIG_ALLOC_HDR ((uint32_t)sizeof(Handle))
/* GetTick wrapper in peislam.s: invokes the Misc Toolset GetTick // VBL tick counter in peislam.s: a Misc Toolset heartbeat task that the
* ($2503) and returns the low 16 bits of the system's tick counter // firmware VBL interrupt handler bumps every field, read by
* (firmware VBL ISR-driven). Polling $C019 from C user code missed // iigsGetTickWord as a same-bank word load. It replaced a _GetTick
* transitions for any op over ~1 ms; the system's tick counter is // toolbox call per read (hundreds of cycles, ten-odd reads per Space
* updated by the actual interrupt handler so it stays accurate // Taxi frame). Polling $C019 from C missed transitions for any op over
* regardless of caller polling rate. Tick rate matches the video // ~1 ms; the interrupt-driven count stays accurate regardless of the
* field rate -- 60 Hz on NTSC, 50 Hz on PAL. */ // caller's polling rate. Tick rate matches the video field rate -- 60 Hz
// on NTSC, 50 Hz on PAL. iigsVblInstall/iigsVblRemove wrap SetHeartBeat/
// DelHeartBeat for that task and return the Misc Tool Set error (0 = ok).
extern uint16_t iigsGetTickWord(void); extern uint16_t iigsGetTickWord(void);
extern uint16_t iigsVblInstall(void);
extern uint16_t iigsVblRemove(void);
// MVN block copy (peislam.s): one run that crosses no bank on either side.
typedef struct {
uint32_t dst;
uint32_t src;
uint16_t len;
} IigsMvnArgsT;
extern void iigsMvnCopy(const IigsMvnArgsT *args);
/* Reads battery RAM hrtz50or60: 0 = NTSC, 1 = PAL. */ /* Reads battery RAM hrtz50or60: 0 = NTSC, 1 = PAL. */
extern uint16_t iigsReadHzParam(void); extern uint16_t iigsReadHzParam(void);
static uint16_t gFrameHz = 60u; static uint16_t gFrameHz = 60u;
@ -217,14 +234,40 @@ volatile uint16_t gPeiSkipTmp; // scan: rows skipped by a clean group
// start true (forces the very first present to upload), get set true // start true (forces the very first present to upload), get set true
// again whenever jlScbSet* / jlPaletteSet mutate the stage's data, and // again whenever jlScbSet* / jlPaletteSet mutate the stage's data, and
// get cleared here after upload. // get cleared here after upload.
// Word stores, NOT memcpy: the clang IIgs libc memcpy is a ~30 cyc/byte
// byte loop (finding #79 in palette.c), so the 712 bytes here cost ~21k
// cycles -- 7.6 ms, a third of a frame -- every time both flags fire.
// 16-bit stores land at ~8 cyc/word, ~2.8k cycles for the pair.
static void copyWords(volatile uint16_t *dst, const uint16_t *src, uint16_t words) {
uint16_t i;
for (i = 0; i < words; i++) {
dst[i] = src[i];
}
}
static void uploadScbAndPaletteIfNeeded(const jlSurfaceT *src) { static void uploadScbAndPaletteIfNeeded(const jlSurfaceT *src) {
if (gStageScbDirty) { if (gStageScbDirty) {
memcpy(IIGS_SHR_SCB, src->scb, SURFACE_HEIGHT); copyWords((volatile uint16_t *)IIGS_SHR_SCB, (const uint16_t *)src->scb, SURFACE_HEIGHT / 2u);
gStageScbDirty = false; gStageScbDirty = false;
} }
if (gStagePaletteDirty) { if (gStagePaletteDirty) {
memcpy(IIGS_SHR_PALETTE, src->palette, sizeof(src->palette)); // Only the palettes that changed: a colour-cycling effect touches
// one palette a frame, and 32 bytes beats 512 sixteen times over.
uint16_t mask = gStagePaletteDirtyMask;
uint8_t pal;
for (pal = 0; mask != 0u && pal < SURFACE_PALETTE_COUNT; pal++) {
if ((mask & 1u) != 0u) {
copyWords((volatile uint16_t *)IIGS_SHR_PALETTE + (uint16_t)pal * SURFACE_COLORS_PER_PALETTE,
(const uint16_t *)src->palette + (uint16_t)pal * SURFACE_COLORS_PER_PALETTE,
SURFACE_COLORS_PER_PALETTE);
}
mask >>= 1;
}
gStagePaletteDirty = false; gStagePaletteDirty = false;
gStagePaletteDirtyMask = 0u;
} }
} }
@ -274,7 +317,26 @@ void jlpBigFree(void *p) {
static void claimStagePixels(void); static void claimStagePixels(void);
bool jlpInit(const jlConfigT *config) { bool jlpInit(const jlConfigT *config) {
uint16_t tick;
uint16_t spin = 0u;
(void)config; (void)config;
// The heartbeat task is the frame clock; nothing else can wait for a
// VBL, so prove it advances before touching anything else (a task
// that never runs would spin jlpWaitVBL forever).
if (iigsVblInstall() != 0u) {
jlLog("iigs: SetHeartBeat failed");
return false;
}
tick = iigsGetTickWord();
while (iigsGetTickWord() == tick) {
spin++;
if (spin == 0u) {
iigsVblRemove();
jlLog("iigs: VBL heartbeat task never ran");
return false;
}
}
claimStagePixels(); claimStagePixels();
gPreviousNewVideo = *IIGS_NEWVIDEO_REG; gPreviousNewVideo = *IIGS_NEWVIDEO_REG;
gPreviousBorder = *IIGS_BORDER_REG; gPreviousBorder = *IIGS_BORDER_REG;
@ -355,7 +417,59 @@ void jlpPresent(const jlSurfaceT *src) {
#define IIGS_QUIT_MAILBOX ((volatile uint8_t *)0xE19DC8L) #define IIGS_QUIT_MAILBOX ((volatile uint8_t *)0xE19DC8L)
// jlMemCopy: MVN moves of up to a bank at a time. MVN's X and Y wrap
// inside their banks, so a run ends where either side would cross one;
// Memory Manager blocks are free to straddle banks.
void iigsMemCopy(void *dst, const void *src, uint32_t bytes) {
static IigsMvnArgsT args;
uint32_t d = (uint32_t)dst;
uint32_t s = (uint32_t)src;
while (bytes != 0u) {
uint32_t run = bytes;
uint32_t toD = 0x10000UL - (d & 0xFFFFu);
uint32_t toS = 0x10000UL - (s & 0xFFFFu);
if (run > toD) {
run = toD;
}
if (run > toS) {
run = toS;
}
if (run > 0xFFFFu) {
run = 0xFFFFu;
}
args.dst = d;
args.src = s;
args.len = (uint16_t)run;
iigsMvnCopy(&args);
d += run;
s += run;
bytes -= run;
}
}
// The Memory Manager owner ID for every JoeyLib allocation on the IIgs
// (jlpBigAlloc, the stage pixels, the audio buffers). MMStartUp creates
// a fresh application ID per call, so it is called exactly once here
// and the result cached for the life of the program.
uint16_t iigsMemUserId(void) {
static uint16_t gMemUserId = 0u;
static bool gMemUserIdReady = false;
if (!gMemUserIdReady) {
gMemUserId = (uint16_t)MMStartUp();
gMemUserIdReady = true;
}
return gMemUserId;
}
void jlpShutdown(void) { void jlpShutdown(void) {
// The task lives in this program's memory: pull it from the heartbeat
// queue before GS/OS frees the segments.
iigsVblRemove();
if (gModeSet) { if (gModeSet) {
*IIGS_NEWVIDEO_REG = gPreviousNewVideo; *IIGS_NEWVIDEO_REG = gPreviousNewVideo;
*IIGS_BORDER_REG = gPreviousBorder; *IIGS_BORDER_REG = gPreviousBorder;
@ -514,8 +628,8 @@ void jlpSurfaceCopyRect(jlSurfaceT *dst, const jlSurfaceT *src, uint16_t x, uint
* the legacy paths did. Same logic as the DOS port. */ * the legacy paths did. Same logic as the DOS port. */
// Block until the next VBL boundary by waiting for the firmware tick // Block until the next VBL boundary by waiting for the heartbeat tick
// (GetTick, incremented by the VBL interrupt handler) to advance. The // (incremented by the VBL interrupt handler) to advance. The
// old form polled $C019's VBL status bit, which never reads high under // old form polled $C019's VBL status bit, which never reads high under
// MAME's apple2gs (PERF-AUDIT.md finding #76): the wait-for-scan loop // MAME's apple2gs (PERF-AUDIT.md finding #76): the wait-for-scan loop
// spun forever, hanging every program that paces with jlWaitVBL. UBER // spun forever, hanging every program that paces with jlWaitVBL. UBER
@ -537,7 +651,7 @@ void jlpWaitVBL(void) {
// (jlpFrameCount is a port.h macro straight to iigsGetTickWord -- the // (jlpFrameCount is a port.h macro straight to iigsGetTickWord -- the
// GetTick asm wrapper in peislam.s -- so no C wrapper lives here.) // heartbeat counter read in peislam.s -- so no C wrapper lives here.)
uint16_t jlpFrameHz(void) { uint16_t jlpFrameHz(void) {

View file

@ -1321,6 +1321,23 @@ iigsTilePasteGlyphMark:
and #0x00FF and #0x00FF
sta tpgBitsW+14 sta tpgBitsW+14
; The 16-word colour table depends only on (fg, bg), and consecutive
; cells almost always share them (a level is painted in colour runs),
; so it is kept across calls: rebuild only when the pair changes. The
; key is fg << 4 | bg; a zeroed bss key with zeroed tables IS the
; fg = bg = 0 table, so no initialisation is needed.
lda 16,s ; fg
asl a
asl a
asl a
asl a
ora 18,s ; | bg
cmp tpgLastKey
bne tpgTableBuild
brl tpgTableReady ; the build is past an 8-bit branch's reach
tpgTableBuild:
sta tpgLastKey
; pb[0..3]: the output byte for each 2-bit pixel pair, ; pb[0..3]: the output byte for each 2-bit pixel pair,
; and the same four values pre-shifted into a high byte. ; and the same four values pre-shifted into a high byte.
lda 16,s ; fg lda 16,s ; fg
@ -1405,6 +1422,7 @@ iigsTilePasteGlyphMark:
lda tpgPbHi+6 lda tpgPbHi+6
ora tpgPb+6 ora tpgPb+6
sta tpgWord+30 sta tpgWord+30
tpgTableReady:
; X = stage byte offset of the tile's row 0 ; X = stage byte offset of the tile's row 0
lda 14,s ; by lda 14,s ; by
@ -1579,6 +1597,11 @@ tpgPb:
tpgPbHi: tpgPbHi:
.zero 8 .zero 8
.section .bss.tpgLastKey,"aw"
.globl tpgLastKey
tpgLastKey:
.zero 2
.section .bss.tpgFgHi,"aw" .section .bss.tpgFgHi,"aw"
.globl tpgFgHi .globl tpgFgHi
tpgFgHi: tpgFgHi:
@ -4738,6 +4761,14 @@ peiRowClean:
; The index maths runs in M=16 throughout: TAX/TAY there move a clean ; The index maths runs in M=16 throughout: TAX/TAY there move a clean
; 16-bit value, whereas in M=8 they would carry whatever the hidden ; 16-bit value, whereas in M=8 they would carry whatever the hidden
; high byte B happens to hold. ; high byte B happens to hold.
; The flag is consulted only on a group's FIRST row: a clean row inside
; a group that has already been entered just steps (the flag could only
; have said "may be dirty", which the band tests are settling row by
; row). Measured 2026-09-22: ~90 of the ~160 clean rows per present sat
; inside dirty groups and paid the full index dance for nothing.
txa ; M=8: low byte of the row
and #7
bne peiRowStep ; mid-group: step
phx ; park the row phx ; park the row
rep #0x20 rep #0x20
.a16 .a16
@ -4752,6 +4783,7 @@ peiRowClean:
plx ; row back in X (PLX sets flags on X) plx ; row back in X (PLX sets flags on X)
cmp #0 cmp #0
beq peiRowSkipGroup beq peiRowSkipGroup
peiRowStep:
inx inx
iny iny
bra peiRowBounds bra peiRowBounds

View file

@ -1,8 +1,8 @@
; peislam.s - GetTick and ReadBParam trampolines for the IIgs. ; peislam.s - VBL tick counter and ReadBParam trampoline for the IIgs.
; ;
; GAS syntax for the llvm-mos w65816 toolchain. The old PEI-slam present ; GAS syntax for the llvm-mos w65816 toolchain. The old PEI-slam present
; logic was rolled into iigsBlitStageToShr in joeyDraw; only the two ; logic was rolled into iigsBlitStageToShr in joeyDraw; what remains is
; toolbox trampolines remain. ; the heartbeat-driven tick counter and one toolbox trampoline.
; ;
; C ABI (llvm-mos w65816): caller has M=I=16. A uint16_t return value ; C ABI (llvm-mos w65816): caller has M=I=16. A uint16_t return value
; is left in A, which matches the toolbox dispatcher's word result, so ; is left in A, which matches the toolbox dispatcher's word result, so
@ -11,27 +11,94 @@
.text .text
; ---------------------------------------------------------------- ; ----------------------------------------------------------------
; uint16_t iigsGetTickWord(void) ; VBL tick counter.
; ;
; Calls Misc Toolset GetTick ($2503) and returns the low 16 bits of ; A Misc Toolset heartbeat task (SetHeartBeat, $1203) that the firmware
; the 32-bit tick counter. The system increments this counter from the ; VBL interrupt handler runs once per video field. It bumps a 16-bit
; actual VBL hardware interrupt, so it stays accurate regardless of ; counter that iigsGetTickWord returns. The old wrapper went through
; caller polling rate -- C-side polling of $C019 missed transitions ; the toolbox dispatcher to _GetTick ($2503) on every read -- hundreds
; for any op over ~1 ms. ; of cycles, ten-odd times per Space Taxi frame (~5% of a frame). The
; rate is unchanged: both GetTick and the heartbeat queue are driven
; by the same VBL interrupt (60 Hz NTSC, 50 Hz PAL), so jlpFrameHz and
; every tick-based hold in audio_full.c keep their meaning. As before,
; a caller holding SEI freezes the count.
; ;
; GetTick output convention: caller pushes 4 bytes of output space, the ; The header, the task code, the counter and the accessors share ONE
; tool dispatcher writes the LongWord into them. We pull the low 16 ; section, so every reference between them is a same-bank 16-bit
; bits into A (the return value) and discard the high 16 into X. ; absolute (K is the bank; B is set from K around each access -- the
; heartbeat handler's B and D are undefined, and C globals live in a
; different bank from this code).
;
; Heartbeat task header (Toolbox Reference, Misc Tool Set):
; +0 long link owned by the heartbeat handler
; +4 word count decremented every tick; the task runs when it
; reaches 0 and must re-arm it
; +6 word signature $A55A
; +8 code, ended by rtl
; ---------------------------------------------------------------- ; ----------------------------------------------------------------
.section .text.iigsGetTickWord,"ax" .section .text.iigsVblTask,"ax"
.globl iigsGetTickWord .globl iigsGetTickWord
.globl iigsVblInstall
.globl iigsVblRemove
iigsVblTask:
.long 0
.word 1
.word 0xA55A
php
rep #0x30
pha
phb
phk
plb
inc vblTick
lda #1
sta iigsVblTask+4 ; re-arm for the next tick
plb
pla
plp
rtl
vblTick:
.word 0
; uint16_t iigsGetTickWord(void) -- the current VBL count.
iigsGetTickWord: iigsGetTickWord:
pha ; output space high word phb
pha ; output space low word phk
ldx #0x2503 ; _GetTick plb
jsl 0xe10000 lda vblTick
pla ; A = low 16 bits (return value) plb
plx ; discard high 16 bits rtl
; uint16_t iigsVblInstall(void) -- SetHeartBeat(iigsVblTask).
; uint16_t iigsVblRemove(void) -- DelHeartBeat(iigsVblTask).
; Both return the Misc Tool Set error code, 0 = success. The long
; pointer is pushed high word first ($00, K) then the low word (a
; same-section 16-bit relocation). The high word is built without
; leaving M=16: a 16-bit zero push plus phk leaves ONE stray zero byte
; under the parameter block, dropped by bumping S after the call
; (tsc/inc/tcs preserve the carry that flags a tool error). An 8-bit
; push after sep #0x20 is what the first cut did, and llvm-mc kept
; assembling `lda #0` as a 16-bit immediate (no .a8 directive) -- the
; CPU ran the extra byte as BRK inside the call.
iigsVblInstall:
ldx #0x1203 ; _SetHeartBeat
bra vblTool
iigsVblRemove:
ldx #0x1303 ; _DelHeartBeat
vblTool:
lda #0
pha ; 00 00
phk ; K -> stack holds K 00 00 (one zero too many)
pea iigsVblTask ; low word
jsl 0xe10000 ; consumes lo hi K 00
tay ; A = error code (carry set on error)
tsc
inc
tcs ; drop the stray zero byte
tya
bcs vblToolDone
lda #0
vblToolDone:
rtl rtl
; ---------------------------------------------------------------- ; ----------------------------------------------------------------
@ -40,8 +107,8 @@ iigsGetTickWord:
; Reads battery RAM parameter hrtz50or60 ($1D) via _ReadBParam ($0C03) ; Reads battery RAM parameter hrtz50or60 ($1D) via _ReadBParam ($0C03)
; and returns the raw value: 0 = NTSC (60 Hz), 1 = PAL (50 Hz). ; and returns the raw value: 0 = NTSC (60 Hz), 1 = PAL (50 Hz).
; ;
; GetTick fires from the hardware VBL ISR, so its rate matches the ; The heartbeat tick fires from the hardware VBL ISR, so its rate
; video field rate -- 60 Hz on NTSC, 50 Hz on PAL. jlpFrameHz must ; matches the video field rate -- 60 Hz on NTSC, 50 Hz on PAL. jlpFrameHz must
; report whichever this machine actually runs so wall-clock math ; report whichever this machine actually runs so wall-clock math
; (frames * 1000 / jlpFrameHz) is correct on both. ; (frames * 1000 / jlpFrameHz) is correct on both.
; ---------------------------------------------------------------- ; ----------------------------------------------------------------
@ -54,3 +121,57 @@ iigsReadHzParam:
jsl 0xe10000 jsl 0xe10000
pla ; A = result (return value) pla ; A = result (return value)
rtl rtl
; ----------------------------------------------------------------
; void iigsMvnCopy(const IigsMvnArgsT *args) A = args low, X = bank
;
; +0 uint32_t dst 24-bit destination
; +4 uint32_t src 24-bit source
; +8 uint16_t len bytes, 1..65535, and hal.c guarantees neither
; [dst, dst+len) nor [src, src+len) crosses a bank
; (MVN's X/Y wrap inside one bank).
;
; One MVN: 7 cycles a byte against libc memcpy's byte loop of ~40. The
; two bank operands are patched into the instruction itself (this
; segment is RAM; the write goes through B = K, same section), then
; B is restored -- MVN leaves B pointing at the destination bank.
; ----------------------------------------------------------------
.section .text.iigsMvnCopy,"ax"
.globl iigsMvnCopy
iigsMvnCopy:
php
rep #0x30
.a16
.i16
phb
sta 0xe0 ; args low
txa
sta 0xe2 ; args bank
phk
plb ; B = K for the operand patch
ldy #6
lda [0xe0],y ; src bank (byte at +6, junk high)
sep #0x20
.a8
sta mvnOp+2 ; MVN encodes source bank second
ldy #2
lda [0xe0],y ; dst bank
sta mvnOp+1 ; destination bank first
rep #0x20
.a16
ldy #8
lda [0xe0],y
dec a ; A = len - 1
pha
ldy #4
lda [0xe0],y ; src low
tax
ldy #0
lda [0xe0],y ; dst low
tay
pla
mvnOp:
mvn 0, 0 ; patched: dst bank, src bank (54 dd ss)
plb
plp
rtl

121
tools/iigsStart/iigsStart.c Normal file
View file

@ -0,0 +1,121 @@
// iigsStart -- a GS/OS boot launcher for the headless IIgs harnesses.
//
// Installed as /System/Start on a copy of the GS/OS system disk, this runs
// at boot INSTEAD of the Finder and hands off to one JoeyLib example. The
// point is that the Finder cannot be driven from a script: measured under
// gssquared, injected keystrokes reach the $C000 latch but never reach the
// Finder -- Open-Apple-A, Tab, arrows and bare letters all left the desktop
// untouched. (The one redraw seen after the first key of a run is the
// Finder's own settling: byte-identical for three different keys.) Reaching
// the desktop also costs ~61s of boot that a probe must sit through; this
// route has the example on screen in ~52s.
//
// Two GS/OS calls, in this order:
// SetPrefixGS -- point prefix 0 at the example's DIRECTORY. GS/OS does
// NOT do this for a QuitGS launch; prefix 0 was measured still pointing
// at the boot volume. That is not cosmetic: JoeyLib opens its NTP
// replayer as bare "ntpplayer", and when that read fails jlpAudioInit
// returns before ntpArm(), so the sound interrupt vector stays at its
// ROM default ($E1002C = 5C xxxx FF) and the example is SILENT. The
// Finder sets prefix 0 itself, which is why this only bites here.
// QuitGS with pCount = 2 and a pathname -- "quit, and launch this".
//
// Build: clang-build.sh -DJOEY_START_PATH='"/JOEYLIB/STAXI"' \
// -o START iigsStart.c iigsStartQuit.s
#include <stdint.h>
// The program to launch. Overridden per-disk from the build.
#ifndef JOEY_START_PATH
#define JOEY_START_PATH "/JOEYLIB/STAXI"
#endif
// Fixed scratch in bank $E1: the $E1:9DC8-$9DFD window that survives a
// present (SCB proper ends at $9DC7). The asm stubs hard-code the two
// parameter-block addresses, so these must agree with iigsStartQuit.s.
// 9DC8 mailbox 9DCC QuitGS parm (8)
// 9DD4 pathname string 9DE4 SetPrefix parm (8)
// 9DEC prefix string
#define QUIT_MAILBOX ((volatile uint8_t *)0xE19DC8L)
#define QUIT_PARM ((volatile uint8_t *)0xE19DCCL)
#define QUIT_PATH ((volatile uint8_t *)0xE19DD4L)
#define PREFIX_PARM ((volatile uint8_t *)0xE19DE4L)
#define PREFIX_PATH ((volatile uint8_t *)0xE19DECL)
#define QUIT_PATH_ADDR 0x00E19DD4UL
#define PREFIX_PATH_ADDR 0x00E19DECUL
// A GS/OS class-1 input string is a length WORD followed by the characters.
#define GSOS_STRING_HEADER 2u
// Markers for the harness.
#define QUIT_REACHED 0xAAu
#define QUIT_RETURNED 0xEEu
void iigsStartQuit(void);
void iigsStartSetPrefix(void);
// Write a GS/OS input string (length word + characters) and return its length.
static uint16_t gsosString(volatile uint8_t *dst, const char *src, uint16_t len) {
uint16_t i;
dst[0] = (uint8_t)(len & 0xFFu);
dst[1] = (uint8_t)((len >> 8) & 0xFFu);
for (i = 0; i < len; i++) {
dst[GSOS_STRING_HEADER + i] = (uint8_t)src[i];
}
return len;
}
static void putLong(volatile uint8_t *dst, uint32_t v) {
dst[0] = (uint8_t)(v & 0xFFu);
dst[1] = (uint8_t)((v >> 8) & 0xFFu);
dst[2] = (uint8_t)((v >> 16) & 0xFFu);
dst[3] = (uint8_t)((v >> 24) & 0xFFu);
}
int main(void) {
const char *path = JOEY_START_PATH;
uint16_t len = 0;
uint16_t dir = 0;
uint16_t i;
while (path[len] != '\0') {
len++;
}
// Directory part, including the trailing '/': everything up to the last
// separator. "/JOEYLIB/STAXI" -> "/JOEYLIB/".
for (i = 0; i < len; i++) {
if (path[i] == '/') {
dir = (uint16_t)(i + 1u);
}
}
gsosString(PREFIX_PATH, path, dir);
PREFIX_PARM[0] = 2u; // pCount
PREFIX_PARM[1] = 0u;
PREFIX_PARM[2] = 0u; // prefixNum 0 = current directory
PREFIX_PARM[3] = 0u;
putLong(PREFIX_PARM + 4, PREFIX_PATH_ADDR);
iigsStartSetPrefix();
gsosString(QUIT_PATH, path, len);
QUIT_PARM[0] = 2u; // pCount
QUIT_PARM[1] = 0u;
putLong(QUIT_PARM + 2, QUIT_PATH_ADDR); // pathname
QUIT_PARM[6] = 0u; // flags
QUIT_PARM[7] = 0u;
*QUIT_MAILBOX = QUIT_REACHED;
iigsStartQuit();
*QUIT_MAILBOX = QUIT_RETURNED;
// QuitGS only returns on failure. Nothing useful is left to do and the
// llvm-mos post-main exit path is broken on GS/OS, so spin rather than
// crash: the harness reads the mailbox to see what happened.
for (;;) {
}
}

View file

@ -0,0 +1,36 @@
; GS/OS inline-call stubs for iigsStart.
;
; Same shape as iigsQuitGS in src/iigs/joeyDraw.s and for the same reason:
; pure constants, no relocations, so the cross-bank DBR trap that bit the
; first attempt at that routine cannot apply here either. Each parameter
; block lives at a FIXED address in the $E1:9DC8-$9DFD scratch window and
; the C caller fills it in before calling.
; iigsStartSetPrefix -- SetPrefixGS ($2009), parm block at $E1:9DE4.
;
; GS/OS does NOT point prefix 0 at the program named in a QuitGS launch: it
; was measured still pointing at the BOOT volume, so the launched example's
; own relative fopen()s missed. That is not cosmetic -- JoeyLib loads the
; NTP replayer as bare "ntpplayer", and when that read fails jlpAudioInit
; bails before ntpArm(), leaving the sound vector at its ROM default and
; the application completely silent.
.section .text.iigsStartSetPrefix,"ax"
.globl iigsStartSetPrefix
iigsStartSetPrefix:
jsl 0xE100A8
.word 0x2009
.long 0x00E19DE4
rtl
; iigsStartQuit -- QuitGS ($2029), parm block at $E1:9DCC.
;
; Unlike jlpShutdown's pCount=0 "quit to my parent", this is called with
; pCount=2 and a pathname, which tells GS/OS to LAUNCH that program in our
; place. On success it never returns.
.section .text.iigsStartQuit,"ax"
.globl iigsStartQuit
iigsStartQuit:
jsl 0xE100A8
.word 0x2029
.long 0x00E19DCC
rtl

View file

@ -40,6 +40,8 @@
#define SPC_TARGET 3 #define SPC_TARGET 3
#elif defined(JOEYLIB_PLATFORM_IIGS) #elif defined(JOEYLIB_PLATFORM_IIGS)
#define SPC_TARGET 4 #define SPC_TARGET 4
#elif defined(JOEYLIB_PLATFORM_X68000)
#define SPC_TARGET 5
#else #else
#error "spritebake: build with -DJOEYLIB_PLATFORM_<port>" #error "spritebake: build with -DJOEYLIB_PLATFORM_<port>"
#endif #endif

View file

@ -6,7 +6,12 @@
// boot with jlSpriteBankLoadPrecompiled, so no cel is JIT-compiled on // boot with jlSpriteBankLoadPrecompiled, so no cel is JIT-compiled on
// the 65816 (which costs ~0.4 s each). // the 65816 (which costs ~0.4 s each).
// //
// Usage: staxibake OUTPUT.spr // Usage: staxibake OUTPUT.spr the cab/passenger/exhaust bank
// staxibake --level LEVEL.dat OUTPUT.spr one level's hook-sprite cels
//
// The per-level bank holds the cels stLevelCelList names for that level
// (its own VIC bitmaps in the colours its hook program uses), in list
// order -- the game rebuilds the same list at level entry to key them.
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
@ -26,39 +31,87 @@
#define SPR_OFF_HEIGHT 7u #define SPR_OFF_HEIGHT 7u
#define SPR_OFF_CELLS 8u #define SPR_OFF_CELLS 8u
static const uint8_t *levelBitmap(const StLevelT *level, uint8_t ptr);
static bool writeHeader(FILE *out, uint16_t count);
int main(int argc, char **argv) {
// A level's own sprite block, or NULL when the level does not ship it.
static const uint8_t *levelBitmap(const StLevelT *level, uint8_t ptr) {
uint8_t k;
for (k = 0u; k < level->spriteCount; k++) {
if (level->sprites[k].ptr == ptr) {
return level->sprites[k].bitmap;
}
}
return NULL;
}
static bool writeHeader(FILE *out, uint16_t count) {
uint8_t header[SPR_HEADER_BYTES]; uint8_t header[SPR_HEADER_BYTES];
FILE *out;
uint16_t i;
if (argc < 2) {
fprintf(stderr, "usage: staxibake OUTPUT.spr\n");
return 2;
}
out = fopen(argv[1], "wb");
if (out == NULL) {
fprintf(stderr, "staxibake: cannot write %s\n", argv[1]);
return 1;
}
memset(header, 0, sizeof(header)); memset(header, 0, sizeof(header));
memcpy(header, "JSP1", 4); memcpy(header, "JSP1", 4);
header[SPR_OFF_TARGET] = 0u; // cross-platform chunky header[SPR_OFF_TARGET] = 0u; // cross-platform chunky
header[SPR_OFF_HAS_PAL] = 0u; // the game owns its palette header[SPR_OFF_HAS_PAL] = 0u; // the game owns its palette
header[SPR_OFF_WIDTH] = (uint8_t)ST_CEL_TILES; header[SPR_OFF_WIDTH] = (uint8_t)ST_CEL_TILES;
header[SPR_OFF_HEIGHT] = (uint8_t)ST_CEL_TILES; header[SPR_OFF_HEIGHT] = (uint8_t)ST_CEL_TILES;
header[SPR_OFF_CELLS] = (uint8_t)(kStCelCount & 0xFFu); header[SPR_OFF_CELLS] = (uint8_t)(count & 0xFFu);
header[SPR_OFF_CELLS + 1u] = (uint8_t)(kStCelCount >> 8); header[SPR_OFF_CELLS + 1u] = (uint8_t)(count >> 8);
if (fwrite(header, 1, SPR_HEADER_BYTES, out) != SPR_HEADER_BYTES) { return fwrite(header, 1, SPR_HEADER_BYTES, out) == SPR_HEADER_BYTES;
}
int main(int argc, char **argv) {
static StLevelT level;
static StCelDefT levelCels[ST_LEVEL_CELS_MAX];
const StCelDefT *cels = kStCels;
uint16_t count = kStCelCount;
const char *outPath;
FILE *out;
uint16_t i;
if (argc == 4 && strcmp(argv[1], "--level") == 0) {
FILE *fp = fopen(argv[2], "rb");
if (fp == NULL || !stLevelParse(&level, fp)) {
fprintf(stderr, "staxibake: cannot load level %s\n", argv[2]);
return 1;
}
fclose(fp);
cels = levelCels;
count = stLevelCelList(&level, levelCels, ST_LEVEL_CELS_MAX);
outPath = argv[3];
} else if (argc == 2) {
outPath = argv[1];
} else {
fprintf(stderr, "usage: staxibake OUTPUT.spr\n staxibake --level LEVEL.dat OUTPUT.spr\n");
return 2;
}
out = fopen(outPath, "wb");
if (out == NULL) {
fprintf(stderr, "staxibake: cannot write %s\n", outPath);
return 1;
}
if (!writeHeader(out, count)) {
fprintf(stderr, "staxibake: header write failed\n"); fprintf(stderr, "staxibake: header write failed\n");
fclose(out); fclose(out);
return 1; return 1;
} }
for (i = 0u; i < kStCelCount; i++) { for (i = 0u; i < count; i++) {
uint8_t blob[ST_CEL_BYTES]; uint8_t blob[ST_CEL_BYTES];
const uint8_t *bm = stC64SpriteBitmap((uint8_t)(kStCels[i].ptr - ST_SPRITE_PTR_FIRST)); const uint8_t *bm;
if (cels == levelCels) {
stCelBlob(bm, kStCels[i].multi, kStCels[i].color, kStCels[i].mc0, kStCels[i].mc1, blob); bm = levelBitmap(&level, cels[i].ptr);
if (bm == NULL) {
fprintf(stderr, "staxibake: level %s has no sprite block $%02X\n", argv[2], (unsigned)cels[i].ptr);
fclose(out);
return 1;
}
} else {
bm = stC64SpriteBitmap((uint8_t)(cels[i].ptr - ST_SPRITE_PTR_FIRST));
}
stCelBlob(bm, cels[i].multi, cels[i].color, cels[i].mc0, cels[i].mc1, blob);
if (fwrite(blob, 1, ST_CEL_BYTES, out) != ST_CEL_BYTES) { if (fwrite(blob, 1, ST_CEL_BYTES, out) != ST_CEL_BYTES) {
fprintf(stderr, "staxibake: cel %u write failed\n", (unsigned)i); fprintf(stderr, "staxibake: cel %u write failed\n", (unsigned)i);
fclose(out); fclose(out);
@ -66,6 +119,6 @@ int main(int argc, char **argv) {
} }
} }
fclose(out); fclose(out);
fprintf(stderr, "staxibake: wrote %u cels (%ux%u tiles) -> %s\n", (unsigned)kStCelCount, (unsigned)ST_CEL_TILES, (unsigned)ST_CEL_TILES, argv[1]); fprintf(stderr, "staxibake: wrote %u cels (%ux%u tiles) -> %s\n", (unsigned)count, (unsigned)ST_CEL_TILES, (unsigned)ST_CEL_TILES, outPath);
return 0; return 0;
} }

View file

@ -0,0 +1,36 @@
# Host-side checks of Space Taxi's front-end screens (the parts tools/stsim
# cannot reach: they live in spacetaxi.c, above the simulation). Builds the
# game on the BLANK port with spacetaxi.c INCLUDED into the harness so its
# statics are reachable, and wraps the keyboard queue and the save file
# with scripted stand-ins.
#
# make -C tools/staxiharness build + run
REPO := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))/../..)
ST := $(REPO)/examples/spacetaxi
OUT := $(REPO)/build/tools/staxiharness
SRCS := $(filter-out $(ST)/spacetaxi.c,$(wildcard $(ST)/*.c)) \
$(wildcard $(REPO)/src/core/*.c) $(wildcard $(REPO)/src/generic/*.c) \
$(REPO)/src/blank/blank.c
CFLAGS := -O0 -g -w -DJOEYLIB_PLATFORM_BLANK -I$(REPO)/include -I$(REPO)/src/core \
-I$(REPO)/src/codegen -I$(ST)
WRAPS := -Wl,--wrap=jlInputGetChar -Wl,--wrap=jlSaveWrite -Wl,--wrap=jlSaveRead
.PHONY: all run clean
all: run
$(OUT)/harness: harness.c $(ST)/spacetaxi.c $(SRCS)
@mkdir -p $(OUT)
$(CC) $(CFLAGS) harness.c $(SRCS) $(WRAPS) -o $@
# The title screen's level file is read through DATA/levels (the DOS bake
# is the cross-platform STL4 form), so run from a directory that has one.
run: $(OUT)/harness
@mkdir -p $(OUT)/DATA
@ln -sfn $(ST)/generated/dos/levels $(OUT)/DATA/levels
@ln -sfn $(ST)/generated/dos/speech.bin $(OUT)/DATA/speech.bin
cd $(OUT) && ./harness
clean:
rm -rf $(OUT)

View file

@ -0,0 +1,183 @@
// Space Taxi front-end harness: the immortal-cabbies table and the name
// entry screen, driven with scripted keys on the host (BLANK port).
//
// spacetaxi.c is INCLUDED so its static state and routines are visible;
// its main() is renamed away. jlInputGetChar, jlSaveWrite and jlSaveRead
// are replaced through the linker's --wrap with a key script and an
// in-memory save image (see the Makefile).
#define main staxiMain
#include "spacetaxi.c"
#undef main
#include <stdio.h>
#define SAVE_IMAGE_MAX 512u
bool __wrap_jlSaveWrite(const char *name, const void *buf, uint32_t len);
uint32_t __wrap_jlSaveRead(const char *name, void *buf, uint32_t max);
int __wrap_jlInputGetChar(void);
static void check(bool ok, const char *what);
static void rowText(uint8_t row, uint8_t col, uint8_t n, char *out);
static void setScore(uint8_t player, const uint8_t *digits);
static const char *gKeys = "";
static uint8_t gSaveImage[SAVE_IMAGE_MAX];
static uint32_t gSaveLen = 0u;
static int gFails = 0;
int __wrap_jlInputGetChar(void) {
if (*gKeys == 0) {
return -1;
}
return (unsigned char)*gKeys++;
}
uint32_t __wrap_jlSaveRead(const char *name, void *buf, uint32_t max) {
(void)name;
if (gSaveLen == 0u || gSaveLen > max) {
return 0u;
}
memcpy(buf, gSaveImage, gSaveLen);
return gSaveLen;
}
bool __wrap_jlSaveWrite(const char *name, const void *buf, uint32_t len) {
(void)name;
if (len > sizeof(gSaveImage)) {
return false;
}
memcpy(gSaveImage, buf, len);
gSaveLen = len;
return true;
}
static void check(bool ok, const char *what) {
printf("%s %s\n", ok ? "ok " : "FAIL", what);
if (!ok) {
gFails++;
}
}
// `n` screen cells of one row as a C string.
static void rowText(uint8_t row, uint8_t col, uint8_t n, char *out) {
uint8_t k;
for (k = 0u; k < n; k++) {
out[k] = (char)gSim.screen[ST_CELL(row, col + k)];
}
out[n] = 0;
}
// A player's score as the sim keeps it: seven screen-code digits with
// the point at ST_HS_POINT_AT.
static void setScore(uint8_t player, const uint8_t *digits) {
uint8_t k;
for (k = 0u; k < ST_NUMBER_CHARS; k++) {
gSim.scoreBackup[player][k] = (uint8_t)(ST_CHAR_DIGIT_BASE + digits[k]);
}
gSim.scoreBackup[player][ST_HS_POINT_AT] = ST_CHAR_POINT;
}
int main(void) {
static const uint8_t kDigits[ST_NUMBER_CHARS] = { 1u, 2u, 3u, 4u, 0u, 5u, 6u };
jlConfigT config;
jlSurfaceT *stage;
char buf[48];
uint8_t k;
config.codegenBytes = 160UL * 1024;
config.audioBytes = 32UL * 1024;
if (!jlInit(&config)) {
printf("jlInit failed\n");
return 1;
}
stage = jlStageGet();
memset(&gGame, 0, sizeof(gGame));
memset(&gSim, 0, sizeof(gSim));
stRenderInit(stage);
stAudioInit();
stSimNewGame(&gSim, 2u, false);
// ---- ranking ($4D92) ----
check(highScoreInsert((const uint8_t *)" 49.99") == ST_HS_ENTRIES, "49.99 is below the $50 floor");
check(highScoreInsert((const uint8_t *)" 50.00") == ST_HS_ENTRIES, "50.00 passes the floor but beats nobody");
check(highScoreInsert((const uint8_t *)" 500.00") == 8u, "500.00 lands in slot 8 (under 504.97, above 498.77)");
check(memcmp(gHighScores[9].score, " 498.77", 7) == 0 && memcmp(gHighScores[7].score, " 504.97", 7) == 0, "older entries shifted down, the last one dropped");
check(highScoreInsert((const uint8_t *)"3877.56") == 0u, "a tie goes above the older entry");
check(memcmp(gHighScores[1].score, "3877.56", 7) == 0 && memcmp(gHighScores[1].name, "MICHAEL PLATE ", 15) == 0, "the tied old entry is now second");
// ---- the entry screen for cabbie 2 with $1234.56 on shift 3 ($4D52 / $4E35) ----
gSim.player = 1u;
gGame.variation = 3u;
setScore(1u, kDigits);
gKeys = "zz"; // typed before the screen: flushed like $0277
check(nameEntryStart(), "1234.56 ranks");
check(gGame.state == ST_STATE_NAME_ENTRY && gGame.hsSlot == 3u, "slot 3 (under the two 3877.56 and 1809.51)");
check(gSim.borderColor == 2u && gSim.bgColor == 15u, "red border, grey field");
rowText(3u, 7u, 25u, buf);
check(strcmp(buf, "CONGRATULATIONS CABBIE 2,") == 0, buf);
rowText(0u, 15u, 8u, buf);
check(strcmp(buf, "$1234.56") == 0, buf);
rowText(9u, 9u, 21u, buf);
check(strcmp(buf, "ENTER YOUR NAME BELOW") == 0, buf);
check(*gKeys == 0, "earlier keystrokes discarded");
// ---- typing ($4ED8): s c o 9 t t <backspace> T ----
gKeys = "sco9tt\bT";
runNameEntry(1u);
rowText(ST_HS_NAME_ROW, ST_HS_NAME_COL, 5u, buf);
check(strcmp(buf, "SCOTT") == 0, buf);
check(gGame.hsLen == 5u, "five characters kept ('9' ignored, one deleted, one retyped)");
check(gGame.state == ST_STATE_NAME_ENTRY, "still entering");
for (k = 0u; k < 2u; k++) {
runNameEntry(1u);
}
check(gSim.screen[ST_CELL(ST_HS_NAME_ROW, ST_HS_NAME_COL + 5u)] == ' ', "first flip blanks the cursor ($4F4D)");
for (k = 0u; k < 3u; k++) {
runNameEntry(1u);
}
check(gSim.screen[ST_CELL(ST_HS_NAME_ROW, ST_HS_NAME_COL + 5u)] == '%', "second flip shows it");
gKeys = "\r";
runNameEntry(1u);
check(gGame.state != ST_STATE_NAME_ENTRY, "RETURN leaves the screen");
check(memcmp(gHighScores[3].name, "SCOTT ", 15) == 0 && gHighScores[3].shift == 3u, "name and shift stored");
check(gSaveLen == 240u && gSaveImage[3 * 24 + 7] == 0 && memcmp(&gSaveImage[3 * 24 + 8], "SCOTT ", 6) == 0 && gSaveImage[3 * 24 + 23] == 3u, "saved in the C64 image layout");
check(gSim.playersDone == 1u, "game-over bookkeeping continued");
// ---- reload from the image ($23E5) ----
memset(gHighScores, 0, sizeof(gHighScores));
highScoreLoad();
check(memcmp(gHighScores[3].name, "SCOTT ", 15) == 0 && memcmp(gHighScores[0].score, "3877.56", 7) == 0, "table restored from the save");
// ---- an empty name becomes "*" ----
gSim.player = 0u;
setScore(0u, kDigits);
check(nameEntryStart() && gGame.hsSlot == 3u, "second entry, a tie goes above");
gKeys = "\r";
runNameEntry(1u);
check(gHighScores[3].name[0] == '*' && memcmp(gHighScores[4].name, "SCOTT", 5) == 0, "an empty name is stored as *");
// ---- the table screen ($4C1F) ----
highScoreDraw();
rowText(1u, 9u, 20u, buf);
check(strcmp(buf, "THE IMMORTAL CABBIES") == 0, buf);
rowText(12u, 4u, 26u, buf);
check(strcmp(buf, "$1234.56 SCOTT ") == 0, buf);
check(gSim.screen[ST_CELL(12, 34)] == '3' && gSim.screen[ST_CELL(10, 15)] == '*', "shift digit in the SHIFT column, * name above");
rowText(24u, 3u, 34u, buf);
check(strcmp(buf, "PRESS FIRE TO RETURN TO TITLE PAGE") == 0, buf);
check(jlMusicIsPlaying(), "song 7 under the table");
printf("%s (%d failures)\n", gFails ? "staxiharness: FAIL" : "staxiharness: PASS", gFails);
jlShutdown();
return gFails ? 1 : 0;
}

View file

@ -2,10 +2,16 @@
ST := ../../examples/spacetaxi ST := ../../examples/spacetaxi
CC ?= gcc CC ?= gcc
CFLAGS := -O1 -g -Wall -Wextra -std=c99 -I$(ST) CFLAGS := -O1 -g -Wall -Wextra -std=c99 -I$(ST)
SRCS := stsim.c $(ST)/stSim.c $(ST)/stFare.c $(ST)/stHooks.c $(ST)/stLevelFile.c $(ST)/stTitle.c $(ST)/stC64Data.c SRCS := stsim.c $(ST)/stSim.c $(ST)/stFare.c $(ST)/stHooks.c $(ST)/stLevelFile.c $(ST)/stTitle.c $(ST)/stC64Data.c $(ST)/stLevelCels.c
stsim: $(SRCS) $(ST)/stSim.h $(ST)/stC64Data.h $(ST)/stDemoStreams.h stsim: $(SRCS) $(ST)/stSim.h $(ST)/stC64Data.h $(ST)/stDemoStreams.h gateStreams.h
$(CC) $(CFLAGS) $(SRCS) -o $@ $(CC) $(CFLAGS) $(SRCS) -o $@
# The fixture generator for gateStreams.h. Not built by default: it runs
# a long search and is only needed when a level's stream is (re)made.
recorder: recorder.c $(SRCS:stsim.c=) $(ST)/stSim.h
$(CC) $(CFLAGS) recorder.c $(ST)/stSim.c $(ST)/stFare.c $(ST)/stHooks.c \
$(ST)/stLevelFile.c $(ST)/stTitle.c $(ST)/stC64Data.c -o $@
clean: clean:
rm -f stsim rm -f stsim recorder

34
tools/stsim/celGate.sh Executable file
View file

@ -0,0 +1,34 @@
#!/usr/bin/env bash
# Cel gate for Space Taxi: every level cel a recorded ride DISPLAYS must be
# in the level's baked manifest (stLevelCelList in stLevelCels.c), or the
# real ports build it mid-ride and draw it interpreted -- the "compiling
# sprites on the fly" hitch. Runs the same rides gate.sh does with the
# stsim census (STSIM_CELS=1) and fails on any MISSING line.
#
# celGate.sh check every hook level
set -u
here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
repo=$(cd "$here/../.." && pwd)
levels=$repo/examples/spacetaxi/generated/iigs/levels
ticks=4000
pairs="H:08 T:20 W:23 X:24 G:07 I:09 K:11 O:15 Q:17 R:18 U:21 V:22 E:05 J:10 L:12 P:16 S:19"
make -C "$here" >/dev/null || { echo "celGate: build failed" >&2; exit 2; }
fail=0
for p in $pairs; do
l=${p%%:*}; n=${p##*:}
out=$(STSIM_CELS=1 "$here/stsim" "$levels/level$n.dat" "$l" "$ticks" 2>&1 >/dev/null | grep -E '^(CEL|CENSUS)')
missing=$(echo "$out" | grep -c '^CEL MISSING')
census=$(echo "$out" | grep '^CENSUS' | sed 's/^CENSUS //')
if [ "$missing" -ne 0 ]; then
printf ' %s: *** %s cel(s) shown but not baked *** (%s)\n' "$l" "$missing" "$census"
echo "$out" | grep '^CEL MISSING' | sed 's/^/ /'
fail=1
else
printf ' %s: ok (%s)\n' "$l" "$census"
fi
done
[ $fail -eq 0 ] && echo "celGate: PASS" || echo "celGate: FAIL"
exit $fail

75
tools/stsim/gate.sh Executable file
View file

@ -0,0 +1,75 @@
#!/usr/bin/env bash
# Behaviour gate for the Space Taxi simulation.
#
# Runs every hook level through stsim and compares the per-tick trace
# against a golden capture. A refactor that changes nothing observable
# leaves all seventeen traces byte-identical; anything else is a regression.
#
# H, W, T and X ride the C64's own attract recordings (the ones checked
# against VICE traces). The other thirteen ride synthesised fixtures from
# gateStreams.h -- a regression gate only, not evidence of C64 fidelity.
#
# gate.sh capture [dir] record goldens from the CURRENT tree
# gate.sh check [dir] compare the current tree against them
# gate.sh hashes print one md5 per level (compact reference)
#
# Capture from a tree you trust, refactor, then check.
set -u
here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
repo=$(cd "$here/../.." && pwd)
levels=$repo/examples/spacetaxi/generated/iigs/levels
mode=${1:-check}
dir=${2:-$here/golden}
ticks=4000
# level letter -> level file number
pairs="H:08 T:20 W:23 X:24 G:07 I:09 K:11 O:15 Q:17 R:18 U:21 V:22 E:05 J:10 L:12 P:16 S:19"
make -C "$here" >/dev/null || { echo "gate: build failed" >&2; exit 2; }
case "$mode" in
capture)
mkdir -p "$dir"
for p in $pairs; do
l=${p%%:*}; n=${p##*:}
"$here/stsim" "$levels/level$n.dat" "$l" "$ticks" > "$dir/demo$l.jsonl" 2>/dev/null
printf ' %s: %s ticks\n' "$l" "$(wc -l < "$dir/demo$l.jsonl")"
done
echo "gate: captured to $dir"
;;
check)
fail=0
for p in $pairs; do
l=${p%%:*}; n=${p##*:}
got=$(mktemp)
"$here/stsim" "$levels/level$n.dat" "$l" "$ticks" > "$got" 2>/dev/null
if [ ! -f "$dir/demo$l.jsonl" ]; then
echo " $l: no golden (run: gate.sh capture)"
elif cmp -s "$dir/demo$l.jsonl" "$got"; then
printf ' %s: IDENTICAL (%s ticks)\n' "$l" "$(wc -l < "$got")"
else
echo " $l: *** DIFFERS ***"
diff "$dir/demo$l.jsonl" "$got" | head -3
fail=1
fi
rm -f "$got"
done
[ $fail -eq 0 ] && echo "gate: PASS" || echo "gate: FAIL"
# The cel census rides the same recordings: a cel a ride displays that
# its manifest does not bake is a mid-ride interpreted sprite.
"$here/celGate.sh" || fail=1
exit $fail
;;
hashes)
for p in $pairs; do
l=${p%%:*}; n=${p##*:}
h=$("$here/stsim" "$levels/level$n.dat" "$l" "$ticks" 2>/dev/null | md5sum | cut -d' ' -f1)
printf '%s %s\n' "$l" "$h"
done
;;
*)
sed -n '2,20p' "$0"
exit 2
;;
esac

View file

@ -0,0 +1,21 @@
# Space Taxi behaviour gate -- md5 of each level's 4000-tick trace.
# Regenerate with: tools/stsim/gate.sh hashes
# H/W/T/X ride the C64's attract recordings; the rest ride
# synthesised fixtures from gateStreams.h (regression gate only).
H 760501b0e48f3922bfb0f23fd2831935
T c0b3f9e9a74e0d526703cf9b1875557b
W 92226455989e8326577a6ea20df8c8ea
X 87251d616a1be6e0c9c9317987b0fe20
G 73530651372c60042f1f46b5c0cbada0
I 7fe4415429d2b72a50884424269cb685
K 567be2ed37219356a055866459971345
O 02455280933e915e57803a4059aaefd9
Q a7732866d54ed06d52e5dc7406b5b229
R 066a9be80107446893b4571d247ee5cc
U 02dd2f26245442ab016ef02e10fde611
V cc2cb13c5e393d07fbece9c0c465e387
E 21ff29d69d8318bcb2e870b9602e910e
J 4c22a7515949b1701ed4fedabb7823dd
L a0383cbde2800bb09353df15214934ef
P f345d0adc9e011662d582c4277ee663e
S daa3cd12320e32d3e9b6b97b19b0a6d0

460
tools/stsim/gateStreams.h Normal file
View file

@ -0,0 +1,460 @@
// Synthesised demo input streams -- a TEST FIXTURE, not game content.
//
// The C64 ships attract recordings for four screens only (H, W, T and X);
// those live in examples/spacetaxi/stDemoStreams.h and are the streams
// checked against VICE traces. The thirteen streams below were searched for
// by tools/stsim's recorder against this port's own simulation, scored on
// how long the cab survives and how much of the level's hook state, fare
// machine and screen animation the ride reaches.
//
// So they prove one thing and not another: they are a REGRESSION gate --
// refactor the hooks and the tick-by-tick trace must not move -- but they
// are NOT evidence that a hook matches the original, because no C64 trace
// exists for these screens. Treat a change to these bytes as invalidating
// the goldens, and never put them in the attract rotation.
#ifndef ST_GATE_STREAMS_H
#define ST_GATE_STREAMS_H
#include <stdint.h>
// level G -- 1400 ticks, 5 hook bytes, 10 fare stages, 256 pictures, 13 landings
// regenerate: tools/stsim/recorder levels/level??.dat G 20260921
static const uint8_t kGateStreamG[320] = {
0x88, 0x05, 0x81, 0x09, 0x81, 0x05, 0x81, 0x04, 0x81, 0x06, 0x84, 0x0C,
0x81, 0x01, 0x89, 0x02, 0x81, 0x09, 0x82, 0x0D, 0x88, 0x05, 0x81, 0x03,
0x80, 0x09, 0x81, 0x01, 0x90, 0x02, 0x81, 0x04, 0x90, 0x08, 0x81, 0x03,
0x90, 0x0A, 0x88, 0x0E, 0x88, 0x0C, 0x90, 0x0C, 0x81, 0x04, 0x81, 0x0D,
0x90, 0x0A, 0x81, 0x0D, 0x84, 0x0E, 0x81, 0x09, 0x81, 0x08, 0x81, 0x0C,
0x82, 0x04, 0x81, 0x02, 0x81, 0x0E, 0x81, 0x02, 0x84, 0x01, 0x88, 0x0A,
0x88, 0x05, 0x90, 0x09, 0x81, 0x02, 0x84, 0x09, 0x84, 0x05, 0x81, 0x0D,
0x84, 0x09, 0x88, 0x03, 0x84, 0x06, 0x84, 0x0D, 0x81, 0x05, 0x84, 0x0C,
0x88, 0x0C, 0x88, 0x0C, 0x88, 0x03, 0x88, 0x09, 0x81, 0x0A, 0x81, 0x09,
0x81, 0x06, 0x81, 0x07, 0x81, 0x05, 0x88, 0x09, 0x88, 0x02, 0x81, 0x0B,
0x84, 0x0A, 0x84, 0x05, 0x88, 0x03, 0x81, 0x05, 0x81, 0x0B, 0x81, 0x05,
0x90, 0x05, 0x84, 0x09, 0x81, 0x06, 0x81, 0x07, 0x90, 0x0D, 0x81, 0x04,
0x81, 0x0E, 0x88, 0x0D, 0x88, 0x0D, 0x88, 0x06, 0x82, 0x02, 0x88, 0x0D,
0x90, 0x0E, 0x81, 0x04, 0x81, 0x0B, 0x84, 0x0B, 0x81, 0x05, 0x84, 0x07,
0x88, 0x0A, 0x80, 0x04, 0x84, 0x07, 0x84, 0x0B, 0x88, 0x0D, 0x90, 0x0E,
0x90, 0x06, 0x88, 0x08, 0x82, 0x0C, 0x81, 0x02, 0x81, 0x0E, 0x88, 0x0A,
0x90, 0x0B, 0x81, 0x04, 0x88, 0x08, 0x90, 0x07, 0x84, 0x03, 0x82, 0x08,
0x81, 0x05, 0x81, 0x03, 0x81, 0x03, 0x81, 0x06, 0x90, 0x0B, 0x84, 0x0E,
0x82, 0x0E, 0x88, 0x08, 0x88, 0x04, 0x81, 0x0D, 0x88, 0x04, 0x81, 0x08,
0x88, 0x0C, 0x81, 0x06, 0x81, 0x04, 0x90, 0x0B, 0x81, 0x08, 0x84, 0x04,
0x90, 0x08, 0x84, 0x09, 0x88, 0x09, 0x81, 0x0B, 0x81, 0x07, 0x84, 0x0D,
0x88, 0x0A, 0x88, 0x09, 0x90, 0x0B, 0x90, 0x02, 0x84, 0x0D, 0x81, 0x0E,
0x90, 0x0D, 0x84, 0x07, 0x82, 0x0C, 0x81, 0x0E, 0x81, 0x06, 0x88, 0x09,
0x90, 0x02, 0x90, 0x04, 0x81, 0x09, 0x84, 0x0A, 0x90, 0x06, 0x81, 0x04,
0x90, 0x03, 0x90, 0x08, 0x84, 0x0B, 0x81, 0x02, 0x90, 0x04, 0x84, 0x03,
0x81, 0x03, 0x84, 0x05, 0x90, 0x0C, 0x81, 0x01, 0x90, 0x0D, 0x81, 0x02,
0x90, 0x05, 0x90, 0x0E, 0x81, 0x03, 0x90, 0x0A,
};
// level I -- 1400 ticks, 20 hook bytes, 10 fare stages, 256 pictures, 22 landings
// regenerate: tools/stsim/recorder levels/level??.dat I 20260918
static const uint8_t kGateStreamI[320] = {
0x84, 0x0E, 0x81, 0x06, 0x84, 0x0E, 0x88, 0x0B, 0x88, 0x05, 0x81, 0x03,
0x82, 0x07, 0x90, 0x0A, 0x81, 0x08, 0x88, 0x0E, 0x88, 0x0E, 0x90, 0x0B,
0x82, 0x0D, 0x81, 0x01, 0x81, 0x0A, 0x90, 0x0A, 0x82, 0x03, 0x81, 0x07,
0x82, 0x05, 0x81, 0x0B, 0x81, 0x05, 0x82, 0x05, 0x82, 0x0C, 0x81, 0x08,
0x88, 0x0C, 0x90, 0x04, 0x81, 0x02, 0x88, 0x08, 0x81, 0x03, 0x81, 0x05,
0x81, 0x0A, 0x88, 0x0E, 0x81, 0x06, 0x84, 0x0E, 0x82, 0x07, 0x82, 0x01,
0x81, 0x04, 0x82, 0x08, 0x90, 0x0C, 0x90, 0x05, 0x81, 0x08, 0x82, 0x0D,
0x90, 0x06, 0x81, 0x0A, 0x84, 0x0B, 0x84, 0x0C, 0x84, 0x0C, 0x88, 0x09,
0x81, 0x06, 0x81, 0x0C, 0x81, 0x04, 0x82, 0x09, 0x88, 0x05, 0x82, 0x0E,
0x81, 0x0C, 0x81, 0x07, 0x88, 0x01, 0x90, 0x0E, 0x81, 0x02, 0x90, 0x0E,
0x81, 0x02, 0x90, 0x07, 0x84, 0x05, 0x84, 0x06, 0x88, 0x06, 0x88, 0x0C,
0x81, 0x0C, 0x84, 0x0B, 0x82, 0x09, 0x81, 0x05, 0x81, 0x0B, 0x82, 0x09,
0x90, 0x0E, 0x90, 0x06, 0x88, 0x0D, 0x84, 0x03, 0x82, 0x0B, 0x84, 0x0D,
0x90, 0x0B, 0x90, 0x04, 0x81, 0x0B, 0x81, 0x08, 0x88, 0x08, 0x90, 0x0A,
0x81, 0x03, 0x81, 0x01, 0x81, 0x0E, 0x81, 0x04, 0x88, 0x02, 0x90, 0x05,
0x81, 0x07, 0x81, 0x01, 0x82, 0x09, 0x90, 0x06, 0x81, 0x09, 0x90, 0x0E,
0x88, 0x05, 0x82, 0x0D, 0x88, 0x03, 0x81, 0x06, 0x84, 0x0C, 0x82, 0x01,
0x81, 0x09, 0x82, 0x0E, 0x90, 0x01, 0x81, 0x01, 0x84, 0x05, 0x82, 0x06,
0x88, 0x0A, 0x81, 0x05, 0x82, 0x03, 0x90, 0x07, 0x90, 0x08, 0x81, 0x02,
0x90, 0x0D, 0x88, 0x0D, 0x81, 0x08, 0x90, 0x0E, 0x82, 0x0D, 0x88, 0x04,
0x81, 0x0C, 0x82, 0x09, 0x90, 0x04, 0x81, 0x0C, 0x82, 0x06, 0x90, 0x0E,
0x90, 0x0D, 0x90, 0x07, 0x90, 0x03, 0x82, 0x03, 0x88, 0x06, 0x81, 0x07,
0x90, 0x08, 0x81, 0x02, 0x90, 0x09, 0x81, 0x04, 0x90, 0x06, 0x82, 0x0A,
0x90, 0x05, 0x88, 0x05, 0x88, 0x06, 0x90, 0x0B, 0x81, 0x03, 0x82, 0x02,
0x90, 0x0B, 0x81, 0x02, 0x90, 0x0C, 0x90, 0x07, 0x81, 0x01, 0x84, 0x08,
0x90, 0x0C, 0x81, 0x0E, 0x88, 0x08, 0x88, 0x0C, 0x81, 0x0C, 0x90, 0x04,
0x84, 0x0D, 0x81, 0x01, 0x88, 0x08, 0x88, 0x09,
};
// level K -- 1400 ticks, 1 hook bytes, 10 fare stages, 256 pictures, 16 landings
// regenerate: tools/stsim/recorder levels/level??.dat K 11
static const uint8_t kGateStreamK[320] = {
0x84, 0x06, 0x82, 0x0B, 0x90, 0x0C, 0x82, 0x0E, 0x90, 0x05, 0x88, 0x03,
0x88, 0x02, 0x82, 0x0E, 0x88, 0x07, 0x84, 0x02, 0x84, 0x0B, 0x84, 0x08,
0x90, 0x0A, 0x88, 0x07, 0x84, 0x07, 0x81, 0x03, 0x84, 0x08, 0x82, 0x0E,
0x82, 0x0D, 0x84, 0x01, 0x81, 0x09, 0x81, 0x0E, 0x81, 0x0C, 0x81, 0x0B,
0x82, 0x0C, 0x82, 0x06, 0x81, 0x01, 0x84, 0x01, 0x90, 0x06, 0x82, 0x0D,
0x84, 0x0B, 0x82, 0x09, 0x88, 0x09, 0x88, 0x05, 0x82, 0x02, 0x84, 0x04,
0x84, 0x09, 0x88, 0x08, 0x90, 0x09, 0x82, 0x08, 0x81, 0x01, 0x88, 0x01,
0x81, 0x06, 0x90, 0x04, 0x82, 0x0A, 0x82, 0x09, 0x82, 0x09, 0x84, 0x0C,
0x82, 0x07, 0x90, 0x0E, 0x90, 0x0E, 0x90, 0x07, 0x82, 0x0D, 0x90, 0x02,
0x84, 0x04, 0x84, 0x09, 0x88, 0x05, 0x88, 0x03, 0x81, 0x05, 0x82, 0x0E,
0x90, 0x04, 0x90, 0x08, 0x88, 0x0C, 0x90, 0x08, 0x81, 0x05, 0x82, 0x0B,
0x90, 0x0B, 0x84, 0x05, 0x82, 0x0D, 0x82, 0x08, 0x84, 0x0D, 0x81, 0x01,
0x90, 0x08, 0x82, 0x0C, 0x81, 0x09, 0x82, 0x05, 0x84, 0x01, 0x82, 0x0A,
0x82, 0x0E, 0x88, 0x0A, 0x84, 0x04, 0x90, 0x01, 0x90, 0x0D, 0x82, 0x08,
0x90, 0x0B, 0x84, 0x03, 0x81, 0x01, 0x90, 0x0D, 0x82, 0x0C, 0x82, 0x05,
0x81, 0x02, 0x81, 0x06, 0x84, 0x08, 0x81, 0x01, 0x82, 0x06, 0x88, 0x0E,
0x88, 0x02, 0x82, 0x0A, 0x90, 0x0E, 0x82, 0x0B, 0x81, 0x0E, 0x81, 0x0A,
0x82, 0x08, 0x90, 0x01, 0x81, 0x04, 0x81, 0x0D, 0x90, 0x0C, 0x88, 0x0D,
0x88, 0x07, 0x81, 0x05, 0x90, 0x02, 0x82, 0x07, 0x88, 0x02, 0x84, 0x0E,
0x82, 0x0D, 0x90, 0x02, 0x88, 0x04, 0x90, 0x0D, 0x82, 0x0A, 0x82, 0x0D,
0x88, 0x09, 0x81, 0x04, 0x84, 0x08, 0x90, 0x0B, 0x81, 0x01, 0x90, 0x0C,
0x82, 0x0E, 0x84, 0x04, 0x82, 0x06, 0x90, 0x09, 0x84, 0x03, 0x90, 0x01,
0x82, 0x0C, 0x81, 0x03, 0x82, 0x05, 0x90, 0x0E, 0x82, 0x0E, 0x84, 0x07,
0x90, 0x06, 0x81, 0x02, 0x90, 0x03, 0x82, 0x09, 0x82, 0x0A, 0x81, 0x05,
0x82, 0x0D, 0x84, 0x01, 0x90, 0x0A, 0x82, 0x09, 0x81, 0x04, 0x82, 0x02,
0x82, 0x08, 0x90, 0x06, 0x90, 0x03, 0x84, 0x09, 0x82, 0x01, 0x82, 0x08,
0x82, 0x09, 0x90, 0x01, 0x88, 0x07, 0x88, 0x08,
};
// level O -- 1400 ticks, 1 hook bytes, 3 fare stages, 256 pictures, 0 landings
// regenerate: tools/stsim/recorder levels/level??.dat O 20260918
static const uint8_t kGateStreamO[320] = {
0x84, 0x0E, 0x90, 0x08, 0x82, 0x07, 0x81, 0x0B, 0x84, 0x02, 0x81, 0x09,
0x82, 0x06, 0x90, 0x0C, 0x81, 0x08, 0x88, 0x01, 0x88, 0x02, 0x90, 0x04,
0x88, 0x06, 0x81, 0x04, 0x82, 0x0A, 0x81, 0x06, 0x81, 0x0E, 0x82, 0x08,
0x88, 0x06, 0x82, 0x03, 0x81, 0x06, 0x81, 0x02, 0x88, 0x0D, 0x82, 0x04,
0x84, 0x0B, 0x84, 0x03, 0x81, 0x04, 0x84, 0x06, 0x81, 0x04, 0x81, 0x02,
0x84, 0x07, 0x82, 0x06, 0x81, 0x06, 0x90, 0x05, 0x88, 0x09, 0x81, 0x0A,
0x81, 0x0C, 0x81, 0x02, 0x82, 0x09, 0x82, 0x0D, 0x81, 0x04, 0x81, 0x0B,
0x84, 0x03, 0x88, 0x0A, 0x90, 0x0A, 0x81, 0x08, 0x82, 0x08, 0x81, 0x06,
0x81, 0x03, 0x82, 0x0A, 0x81, 0x0C, 0x82, 0x08, 0x81, 0x0C, 0x82, 0x0E,
0x81, 0x06, 0x81, 0x04, 0x81, 0x0A, 0x84, 0x05, 0x90, 0x0E, 0x84, 0x06,
0x82, 0x0B, 0x81, 0x0E, 0x81, 0x03, 0x81, 0x05, 0x90, 0x08, 0x81, 0x01,
0x90, 0x0E, 0x84, 0x0D, 0x88, 0x01, 0x90, 0x05, 0x84, 0x0B, 0x81, 0x04,
0x81, 0x04, 0x82, 0x01, 0x90, 0x04, 0x82, 0x0D, 0x88, 0x07, 0x81, 0x0B,
0x81, 0x07, 0x82, 0x01, 0x88, 0x0A, 0x81, 0x06, 0x90, 0x02, 0x82, 0x08,
0x81, 0x01, 0x81, 0x0A, 0x88, 0x0E, 0x88, 0x0D, 0x81, 0x03, 0x88, 0x0D,
0x81, 0x02, 0x88, 0x04, 0x84, 0x0E, 0x90, 0x01, 0x82, 0x0C, 0x81, 0x0B,
0x81, 0x04, 0x88, 0x03, 0x81, 0x03, 0x88, 0x09, 0x84, 0x0E, 0x81, 0x02,
0x81, 0x02, 0x81, 0x08, 0x84, 0x07, 0x84, 0x02, 0x88, 0x0C, 0x84, 0x08,
0x90, 0x09, 0x90, 0x0D, 0x81, 0x02, 0x88, 0x0D, 0x84, 0x0C, 0x84, 0x0D,
0x84, 0x01, 0x81, 0x07, 0x81, 0x0A, 0x81, 0x01, 0x84, 0x01, 0x84, 0x0D,
0x82, 0x0E, 0x81, 0x02, 0x81, 0x06, 0x81, 0x09, 0x88, 0x0D, 0x88, 0x09,
0x82, 0x0B, 0x81, 0x0B, 0x88, 0x02, 0x88, 0x0C, 0x81, 0x04, 0x84, 0x0B,
0x81, 0x06, 0x88, 0x09, 0x84, 0x0C, 0x81, 0x03, 0x84, 0x0D, 0x84, 0x0D,
0x88, 0x0E, 0x84, 0x04, 0x90, 0x0E, 0x81, 0x02, 0x88, 0x01, 0x88, 0x06,
0x81, 0x0E, 0x82, 0x08, 0x81, 0x0D, 0x90, 0x0D, 0x88, 0x0E, 0x81, 0x03,
0x8A, 0x09, 0x90, 0x0B, 0x84, 0x07, 0x88, 0x03, 0x81, 0x0C, 0x82, 0x07,
0x81, 0x0E, 0x81, 0x08, 0x90, 0x0D, 0x84, 0x0D,
};
// level Q -- 1400 ticks, 5 hook bytes, 10 fare stages, 256 pictures, 10 landings
// regenerate: tools/stsim/recorder levels/level??.dat Q 20260918
static const uint8_t kGateStreamQ[320] = {
0x84, 0x0E, 0x81, 0x04, 0x81, 0x03, 0x84, 0x09, 0x88, 0x0A, 0x81, 0x09,
0x82, 0x06, 0x88, 0x0C, 0x81, 0x08, 0x82, 0x06, 0x88, 0x0E, 0x84, 0x0B,
0x88, 0x08, 0x81, 0x09, 0x81, 0x0A, 0x84, 0x05, 0x84, 0x01, 0x88, 0x01,
0x81, 0x08, 0x84, 0x0B, 0x81, 0x05, 0x82, 0x05, 0x80, 0x0D, 0x81, 0x08,
0x88, 0x0A, 0x88, 0x04, 0x84, 0x08, 0x81, 0x06, 0x81, 0x0C, 0x81, 0x01,
0x90, 0x0A, 0x84, 0x08, 0x81, 0x0A, 0x81, 0x0A, 0x82, 0x08, 0x81, 0x06,
0x81, 0x09, 0x90, 0x06, 0x81, 0x09, 0x90, 0x0D, 0x88, 0x04, 0x82, 0x05,
0x81, 0x0B, 0x84, 0x08, 0x81, 0x08, 0x81, 0x03, 0x82, 0x0D, 0x82, 0x0A,
0x84, 0x0E, 0x81, 0x05, 0x81, 0x0E, 0x81, 0x08, 0x84, 0x0A, 0x82, 0x0C,
0x84, 0x09, 0x84, 0x01, 0x82, 0x06, 0x88, 0x0E, 0x81, 0x09, 0x90, 0x07,
0x81, 0x09, 0x81, 0x07, 0x84, 0x03, 0x81, 0x0B, 0x84, 0x01, 0x90, 0x08,
0x90, 0x06, 0x82, 0x03, 0x88, 0x06, 0x88, 0x0C, 0x84, 0x0A, 0x81, 0x03,
0x82, 0x0E, 0x81, 0x07, 0x81, 0x04, 0x81, 0x0D, 0x84, 0x02, 0x81, 0x09,
0x82, 0x08, 0x88, 0x04, 0x81, 0x09, 0x84, 0x0C, 0x81, 0x0B, 0x84, 0x0D,
0x81, 0x02, 0x82, 0x03, 0x81, 0x05, 0x88, 0x0E, 0x84, 0x0D, 0x81, 0x0B,
0x88, 0x08, 0x84, 0x0C, 0x90, 0x07, 0x82, 0x0B, 0x81, 0x03, 0x90, 0x05,
0x84, 0x05, 0x84, 0x09, 0x81, 0x04, 0x81, 0x0B, 0x81, 0x08, 0x82, 0x0E,
0x81, 0x0A, 0x81, 0x03, 0x81, 0x06, 0x88, 0x09, 0x84, 0x0A, 0x88, 0x0B,
0x81, 0x05, 0x81, 0x0D, 0x88, 0x03, 0x88, 0x0D, 0x81, 0x01, 0x90, 0x06,
0x88, 0x07, 0x82, 0x02, 0x81, 0x04, 0x84, 0x09, 0x81, 0x07, 0x90, 0x06,
0x82, 0x03, 0x84, 0x07, 0x81, 0x05, 0x90, 0x0B, 0x81, 0x0B, 0x88, 0x05,
0x81, 0x0D, 0x81, 0x0B, 0x92, 0x0E, 0x90, 0x05, 0x82, 0x02, 0x88, 0x0C,
0x84, 0x08, 0x81, 0x0A, 0x81, 0x0D, 0x81, 0x05, 0x81, 0x07, 0x81, 0x01,
0x81, 0x04, 0x88, 0x01, 0x81, 0x0E, 0x84, 0x04, 0x90, 0x03, 0x81, 0x0D,
0x90, 0x09, 0x81, 0x02, 0x81, 0x08, 0x90, 0x07, 0x82, 0x0B, 0x90, 0x08,
0x81, 0x03, 0x90, 0x0C, 0x81, 0x0B, 0x88, 0x08, 0x90, 0x04, 0x81, 0x02,
0x90, 0x0C, 0x88, 0x05, 0x81, 0x01, 0x90, 0x03,
};
// level R -- 1400 ticks, 0 hook bytes, 3 fare stages, 256 pictures, 0 landings
// regenerate: tools/stsim/recorder levels/level??.dat R 20260918
static const uint8_t kGateStreamR[320] = {
0x90, 0x0E, 0x81, 0x02, 0x82, 0x07, 0x81, 0x05, 0x88, 0x0B, 0x88, 0x09,
0x90, 0x0D, 0x90, 0x06, 0x81, 0x08, 0x81, 0x01, 0x90, 0x02, 0x88, 0x04,
0x82, 0x07, 0x90, 0x06, 0x88, 0x07, 0x81, 0x06, 0x81, 0x06, 0x82, 0x05,
0x84, 0x07, 0x82, 0x05, 0x81, 0x03, 0x84, 0x0E, 0x88, 0x05, 0x88, 0x09,
0x88, 0x04, 0x81, 0x09, 0x81, 0x02, 0x82, 0x04, 0x84, 0x0B, 0x81, 0x04,
0x82, 0x0C, 0x81, 0x03, 0x88, 0x06, 0x84, 0x08, 0x81, 0x0D, 0x88, 0x09,
0x90, 0x0E, 0x82, 0x07, 0x82, 0x02, 0x84, 0x0D, 0x88, 0x0B, 0x81, 0x0B,
0x81, 0x03, 0x82, 0x02, 0x90, 0x0B, 0x84, 0x03, 0x88, 0x02, 0x88, 0x09,
0x81, 0x02, 0x82, 0x0A, 0x81, 0x06, 0x81, 0x02, 0x90, 0x0A, 0x82, 0x0E,
0x81, 0x09, 0x88, 0x03, 0x81, 0x07, 0x84, 0x0D, 0x84, 0x0A, 0x81, 0x03,
0x82, 0x07, 0x84, 0x0D, 0x81, 0x03, 0x81, 0x08, 0x84, 0x0E, 0x81, 0x02,
0x82, 0x0E, 0x88, 0x03, 0x81, 0x01, 0x81, 0x05, 0x81, 0x06, 0x90, 0x0A,
0x84, 0x0A, 0x81, 0x02, 0x82, 0x05, 0x81, 0x03, 0x88, 0x07, 0x84, 0x0C,
0x81, 0x04, 0x88, 0x09, 0x90, 0x0E, 0x82, 0x0A, 0x88, 0x09, 0x82, 0x01,
0x81, 0x06, 0x81, 0x0D, 0x84, 0x0A, 0x82, 0x0D, 0x88, 0x0A, 0x84, 0x0A,
0x90, 0x02, 0x84, 0x02, 0x81, 0x09, 0x82, 0x0A, 0x81, 0x0B, 0x81, 0x0D,
0x82, 0x05, 0x84, 0x09, 0x84, 0x04, 0x88, 0x06, 0x84, 0x04, 0x88, 0x0D,
0x81, 0x02, 0x82, 0x0B, 0x81, 0x0E, 0x82, 0x0D, 0x82, 0x0A, 0x81, 0x02,
0x81, 0x09, 0x81, 0x0D, 0x84, 0x04, 0x82, 0x09, 0x88, 0x04, 0x81, 0x07,
0x90, 0x0A, 0x90, 0x0B, 0x81, 0x07, 0x84, 0x0D, 0x82, 0x0A, 0x81, 0x0C,
0x88, 0x07, 0x84, 0x09, 0x82, 0x08, 0x90, 0x0A, 0x84, 0x06, 0x82, 0x0D,
0x88, 0x05, 0x81, 0x06, 0x81, 0x05, 0x90, 0x0D, 0x90, 0x0B, 0x81, 0x0C,
0x81, 0x04, 0x84, 0x06, 0x90, 0x08, 0x90, 0x08, 0x90, 0x08, 0x84, 0x0E,
0x88, 0x0E, 0x81, 0x0E, 0x82, 0x0A, 0x90, 0x04, 0x81, 0x06, 0x82, 0x09,
0x84, 0x08, 0x90, 0x07, 0x84, 0x0C, 0x90, 0x05, 0x88, 0x0C, 0x84, 0x06,
0x84, 0x0A, 0x81, 0x08, 0x81, 0x01, 0x81, 0x01, 0x82, 0x08, 0x88, 0x0A,
0x90, 0x0A, 0x90, 0x01, 0x84, 0x0A, 0x81, 0x0B,
};
// level U -- 896 ticks, 21 hook bytes, 3 fare stages, 256 pictures, 0 landings
// regenerate: tools/stsim/recorder levels/level??.dat U 44
static const uint8_t kGateStreamU[320] = {
0x90, 0x01, 0x90, 0x0B, 0x88, 0x08, 0x90, 0x07, 0x88, 0x0D, 0x82, 0x0A,
0x82, 0x06, 0x88, 0x0C, 0x90, 0x03, 0x81, 0x04, 0x84, 0x0B, 0x81, 0x0C,
0x88, 0x02, 0x84, 0x08, 0x84, 0x01, 0x81, 0x04, 0x90, 0x05, 0x84, 0x06,
0x84, 0x08, 0x84, 0x05, 0x81, 0x03, 0x81, 0x09, 0x81, 0x08, 0x88, 0x07,
0x81, 0x08, 0x84, 0x06, 0x81, 0x0B, 0x88, 0x0D, 0x90, 0x08, 0x81, 0x01,
0x82, 0x01, 0x88, 0x03, 0x81, 0x0E, 0x82, 0x01, 0x81, 0x0D, 0x81, 0x07,
0x88, 0x05, 0x88, 0x07, 0x88, 0x0E, 0x84, 0x07, 0x81, 0x03, 0x81, 0x01,
0x84, 0x03, 0x81, 0x04, 0x81, 0x08, 0x90, 0x06, 0x81, 0x0C, 0x82, 0x05,
0x81, 0x08, 0x82, 0x09, 0x84, 0x06, 0x81, 0x06, 0x90, 0x0C, 0x84, 0x0E,
0x82, 0x0C, 0x81, 0x03, 0x81, 0x06, 0x84, 0x0D, 0x88, 0x03, 0x84, 0x0C,
0x81, 0x02, 0x81, 0x09, 0x81, 0x0E, 0x88, 0x01, 0x90, 0x06, 0x88, 0x08,
0x81, 0x08, 0x81, 0x0C, 0x81, 0x0C, 0x82, 0x06, 0x82, 0x08, 0x88, 0x01,
0x81, 0x07, 0x88, 0x0D, 0x84, 0x03, 0x81, 0x02, 0x82, 0x06, 0x81, 0x05,
0x81, 0x0E, 0x81, 0x04, 0x90, 0x01, 0x81, 0x02, 0x90, 0x03, 0x81, 0x0A,
0x81, 0x04, 0x84, 0x02, 0x81, 0x06, 0x81, 0x0E, 0x81, 0x06, 0x84, 0x07,
0x84, 0x04, 0x81, 0x07, 0x81, 0x0E, 0x81, 0x0D, 0x90, 0x0D, 0x90, 0x05,
0x88, 0x0C, 0x88, 0x02, 0x81, 0x0B, 0x90, 0x07, 0x88, 0x06, 0x82, 0x0D,
0x84, 0x08, 0x84, 0x08, 0x81, 0x01, 0x81, 0x06, 0x82, 0x05, 0x84, 0x06,
0x81, 0x06, 0x84, 0x0D, 0x81, 0x04, 0x90, 0x07, 0x88, 0x0D, 0x81, 0x09,
0x88, 0x0D, 0x81, 0x0E, 0x81, 0x03, 0x84, 0x0E, 0x81, 0x03, 0x81, 0x0C,
0x84, 0x0A, 0x81, 0x06, 0x90, 0x02, 0x81, 0x01, 0x88, 0x02, 0x90, 0x02,
0x81, 0x08, 0x84, 0x01, 0x88, 0x0B, 0x88, 0x0B, 0x81, 0x04, 0x81, 0x0C,
0x81, 0x07, 0x82, 0x06, 0x88, 0x0C, 0x81, 0x03, 0x90, 0x02, 0x88, 0x0A,
0x84, 0x08, 0x81, 0x0E, 0x81, 0x05, 0x88, 0x0C, 0x81, 0x0D, 0x81, 0x05,
0x81, 0x04, 0x90, 0x0D, 0x88, 0x03, 0x88, 0x06, 0x81, 0x0B, 0x81, 0x0D,
0x84, 0x0D, 0x81, 0x01, 0x81, 0x0D, 0x81, 0x01, 0x82, 0x07, 0x88, 0x02,
0x81, 0x09, 0x90, 0x0B, 0x88, 0x0D, 0x82, 0x02,
};
// level V -- 1400 ticks, 0 hook bytes, 6 fare stages, 256 pictures, 23 landings
// regenerate: tools/stsim/recorder levels/level??.dat V 20260918
static const uint8_t kGateStreamV[320] = {
0x84, 0x0E, 0x81, 0x04, 0x81, 0x05, 0x88, 0x0B, 0x84, 0x01, 0x81, 0x03,
0x82, 0x06, 0x84, 0x0C, 0x81, 0x08, 0x84, 0x07, 0x82, 0x03, 0x88, 0x0A,
0x90, 0x03, 0x81, 0x0D, 0x81, 0x0D, 0x82, 0x05, 0x81, 0x02, 0x81, 0x0E,
0x90, 0x06, 0x81, 0x05, 0x90, 0x05, 0x81, 0x0E, 0x82, 0x0D, 0x90, 0x0E,
0x88, 0x0E, 0x81, 0x03, 0x90, 0x07, 0x84, 0x0A, 0x81, 0x02, 0x90, 0x07,
0x82, 0x07, 0x81, 0x02, 0x81, 0x06, 0x82, 0x08, 0x81, 0x04, 0x88, 0x0C,
0x81, 0x06, 0x88, 0x0D, 0x90, 0x0D, 0x81, 0x05, 0x90, 0x03, 0x84, 0x0D,
0x84, 0x0A, 0x88, 0x0A, 0x88, 0x0D, 0x81, 0x08, 0x90, 0x08, 0x90, 0x01,
0x84, 0x06, 0x82, 0x0B, 0x81, 0x0D, 0x82, 0x07, 0x82, 0x0C, 0x81, 0x01,
0x90, 0x08, 0x81, 0x02, 0x81, 0x04, 0x90, 0x0C, 0x84, 0x04, 0x88, 0x04,
0x82, 0x03, 0x88, 0x07, 0x81, 0x03, 0x90, 0x05, 0x81, 0x02, 0x81, 0x03,
0x88, 0x01, 0x90, 0x05, 0x81, 0x08, 0x81, 0x06, 0x81, 0x07, 0x81, 0x08,
0x81, 0x06, 0x81, 0x02, 0x84, 0x0B, 0x88, 0x0D, 0x81, 0x06, 0x90, 0x02,
0x81, 0x07, 0x82, 0x0B, 0x88, 0x04, 0x84, 0x04, 0x84, 0x05, 0x81, 0x05,
0x81, 0x02, 0x82, 0x09, 0x81, 0x03, 0x90, 0x0C, 0x88, 0x0B, 0x82, 0x01,
0x88, 0x0D, 0x88, 0x05, 0x81, 0x02, 0x90, 0x0B, 0x81, 0x02, 0x90, 0x0A,
0x88, 0x0C, 0x81, 0x01, 0x90, 0x0A, 0x88, 0x0E, 0x90, 0x0A, 0x84, 0x0B,
0x81, 0x03, 0x90, 0x08, 0x88, 0x03, 0x82, 0x0E, 0x84, 0x06, 0x90, 0x0E,
0x81, 0x03, 0x88, 0x0A, 0x90, 0x0A, 0x82, 0x09, 0x81, 0x04, 0x88, 0x0C,
0x84, 0x04, 0x81, 0x07, 0x90, 0x03, 0x81, 0x01, 0x81, 0x01, 0x81, 0x01,
0x82, 0x0E, 0x81, 0x05, 0x81, 0x06, 0x81, 0x08, 0x84, 0x08, 0x88, 0x0C,
0x82, 0x09, 0x88, 0x05, 0x81, 0x02, 0x88, 0x04, 0x81, 0x04, 0x88, 0x0C,
0x90, 0x07, 0x88, 0x0E, 0x81, 0x03, 0x81, 0x0A, 0x82, 0x0B, 0x81, 0x02,
0x88, 0x02, 0x90, 0x0E, 0x88, 0x0D, 0x81, 0x03, 0x88, 0x09, 0x90, 0x04,
0x84, 0x01, 0x88, 0x04, 0x90, 0x04, 0x84, 0x04, 0x81, 0x03, 0x90, 0x0C,
0x88, 0x07, 0x84, 0x05, 0x81, 0x02, 0x90, 0x0B, 0x90, 0x04, 0x90, 0x09,
0x81, 0x02, 0x81, 0x01, 0x90, 0x0B, 0x88, 0x04,
};
// level E -- 1400 ticks, 4 hook bytes, 8 fare stages, 256 pictures, 11 landings
// regenerate: tools/stsim/recorder levels/level??.dat E 20260921
static const uint8_t kGateStreamE[320] = {
0x84, 0x05, 0x82, 0x03, 0x82, 0x05, 0x81, 0x0B, 0x81, 0x07, 0x81, 0x02,
0x81, 0x01, 0x89, 0x02, 0x90, 0x03, 0x81, 0x0A, 0x81, 0x05, 0x84, 0x0C,
0x81, 0x02, 0x88, 0x07, 0x81, 0x0B, 0x90, 0x08, 0x90, 0x0A, 0x88, 0x02,
0x81, 0x05, 0x84, 0x08, 0x81, 0x05, 0x90, 0x03, 0x84, 0x01, 0x84, 0x08,
0x88, 0x0A, 0x84, 0x07, 0x81, 0x04, 0x84, 0x07, 0x90, 0x0C, 0x80, 0x08,
0x90, 0x07, 0x88, 0x03, 0x90, 0x0B, 0x88, 0x01, 0x90, 0x07, 0x88, 0x04,
0x81, 0x02, 0x88, 0x08, 0x84, 0x05, 0x81, 0x05, 0x81, 0x02, 0x81, 0x09,
0x88, 0x05, 0x90, 0x0E, 0x81, 0x05, 0x81, 0x01, 0x90, 0x04, 0x90, 0x08,
0x81, 0x06, 0x84, 0x0E, 0x81, 0x09, 0x81, 0x01, 0x84, 0x05, 0x81, 0x01,
0x81, 0x02, 0x88, 0x0B, 0x90, 0x08, 0x88, 0x0C, 0x88, 0x05, 0x88, 0x08,
0x84, 0x09, 0x88, 0x05, 0x90, 0x05, 0x82, 0x0A, 0x81, 0x07, 0x90, 0x0A,
0x81, 0x07, 0x81, 0x03, 0x84, 0x0B, 0x84, 0x0E, 0x88, 0x09, 0x90, 0x02,
0x81, 0x0E, 0x81, 0x06, 0x90, 0x0E, 0x81, 0x02, 0x90, 0x08, 0x84, 0x08,
0x81, 0x0B, 0x88, 0x0D, 0x84, 0x0C, 0x81, 0x09, 0x90, 0x08, 0x82, 0x0B,
0x88, 0x08, 0x81, 0x07, 0x82, 0x02, 0x84, 0x0B, 0x88, 0x0B, 0x81, 0x0D,
0x84, 0x0E, 0x82, 0x01, 0x81, 0x03, 0x90, 0x0C, 0x81, 0x05, 0x90, 0x04,
0x82, 0x0C, 0x81, 0x06, 0x90, 0x0E, 0x88, 0x0B, 0x88, 0x09, 0x81, 0x0E,
0x82, 0x0E, 0x90, 0x0C, 0x81, 0x06, 0x81, 0x05, 0x84, 0x0E, 0x81, 0x0C,
0x88, 0x0B, 0x90, 0x06, 0x82, 0x05, 0x81, 0x06, 0x88, 0x0D, 0x88, 0x07,
0x81, 0x07, 0x84, 0x07, 0x88, 0x08, 0x81, 0x07, 0x90, 0x0C, 0x84, 0x04,
0x84, 0x0A, 0x88, 0x0B, 0x80, 0x0A, 0x81, 0x0C, 0x81, 0x07, 0x88, 0x06,
0x88, 0x09, 0x88, 0x09, 0x81, 0x0C, 0x80, 0x06, 0x81, 0x06, 0x81, 0x07,
0x84, 0x08, 0x82, 0x09, 0x84, 0x0C, 0x90, 0x09, 0x90, 0x03, 0x90, 0x0D,
0x81, 0x08, 0x84, 0x05, 0x84, 0x09, 0x88, 0x0B, 0x81, 0x01, 0x81, 0x0A,
0x90, 0x0D, 0x88, 0x0E, 0x88, 0x0D, 0x88, 0x05, 0x84, 0x06, 0x81, 0x0E,
0x81, 0x0A, 0x81, 0x08, 0x84, 0x0C, 0x88, 0x05, 0x81, 0x02, 0x90, 0x0E,
0x84, 0x0D, 0x84, 0x0A, 0x81, 0x05, 0x90, 0x09,
};
// level J -- 1400 ticks, 10 hook bytes, 10 fare stages, 256 pictures, 8 landings
// regenerate: tools/stsim/recorder levels/level??.dat J 20260921
static const uint8_t kGateStreamJ[320] = {
0x88, 0x05, 0x82, 0x09, 0x81, 0x01, 0x81, 0x04, 0x90, 0x06, 0x81, 0x05,
0x81, 0x01, 0x90, 0x09, 0x88, 0x05, 0x82, 0x08, 0x81, 0x0A, 0x81, 0x03,
0x84, 0x07, 0x88, 0x0D, 0x81, 0x06, 0x81, 0x0A, 0x84, 0x05, 0x90, 0x08,
0x81, 0x0B, 0x90, 0x0E, 0x84, 0x0E, 0x81, 0x03, 0x90, 0x09, 0x81, 0x02,
0x88, 0x04, 0x88, 0x08, 0x84, 0x0E, 0x81, 0x04, 0x88, 0x0D, 0x90, 0x0E,
0x84, 0x09, 0x90, 0x05, 0x81, 0x01, 0x81, 0x03, 0x88, 0x03, 0x88, 0x07,
0x81, 0x0B, 0x90, 0x0B, 0x88, 0x08, 0x82, 0x03, 0x90, 0x09, 0x88, 0x03,
0x84, 0x06, 0x88, 0x0A, 0x88, 0x0B, 0x84, 0x04, 0x81, 0x05, 0x88, 0x0E,
0x81, 0x0A, 0x90, 0x0C, 0x85, 0x04, 0x81, 0x0E, 0x90, 0x02, 0x82, 0x0D,
0x84, 0x01, 0x81, 0x02, 0x88, 0x09, 0x81, 0x0B, 0x81, 0x02, 0x82, 0x0C,
0x84, 0x04, 0x81, 0x05, 0x81, 0x04, 0x81, 0x04, 0x81, 0x05, 0x90, 0x05,
0x88, 0x09, 0x81, 0x02, 0x88, 0x01, 0x81, 0x05, 0x90, 0x04, 0x81, 0x0B,
0x81, 0x05, 0x82, 0x0D, 0x88, 0x06, 0x88, 0x07, 0x88, 0x02, 0x84, 0x02,
0x90, 0x07, 0x88, 0x09, 0x84, 0x0C, 0x81, 0x01, 0x88, 0x0B, 0x81, 0x0C,
0x82, 0x0A, 0x81, 0x08, 0x82, 0x01, 0x88, 0x09, 0x84, 0x05, 0x81, 0x08,
0x84, 0x0A, 0x84, 0x09, 0x88, 0x0E, 0x81, 0x01, 0x84, 0x03, 0x82, 0x0C,
0x81, 0x0E, 0x84, 0x0A, 0x88, 0x02, 0x84, 0x0D, 0x81, 0x0A, 0x90, 0x0E,
0x88, 0x09, 0x90, 0x09, 0x81, 0x05, 0x90, 0x03, 0x88, 0x09, 0x84, 0x06,
0x81, 0x01, 0x88, 0x06, 0x81, 0x05, 0x81, 0x09, 0x81, 0x06, 0x90, 0x04,
0x82, 0x0A, 0x90, 0x07, 0x81, 0x07, 0x81, 0x0A, 0x84, 0x0E, 0x84, 0x07,
0x81, 0x0A, 0x90, 0x0C, 0x81, 0x01, 0x90, 0x01, 0x88, 0x0D, 0x81, 0x03,
0x88, 0x05, 0x81, 0x03, 0x88, 0x01, 0x81, 0x01, 0x82, 0x02, 0x88, 0x07,
0x83, 0x02, 0x90, 0x07, 0x81, 0x05, 0x81, 0x04, 0x81, 0x0E, 0x88, 0x09,
0x81, 0x07, 0x90, 0x03, 0x82, 0x0B, 0x82, 0x0B, 0x90, 0x04, 0x81, 0x05,
0x81, 0x03, 0x90, 0x0C, 0x82, 0x07, 0x88, 0x0E, 0x90, 0x04, 0x81, 0x0A,
0x88, 0x02, 0x88, 0x0D, 0x82, 0x07, 0x90, 0x01, 0x81, 0x02, 0x81, 0x01,
0x82, 0x07, 0x81, 0x08, 0x84, 0x0E, 0x81, 0x03,
};
// level L -- 1400 ticks, 0 hook bytes, 10 fare stages, 256 pictures, 8 landings
// regenerate: tools/stsim/recorder levels/level??.dat L 20260921
static const uint8_t kGateStreamL[320] = {
0x84, 0x05, 0x84, 0x09, 0x81, 0x06, 0x81, 0x04, 0x81, 0x08, 0x81, 0x05,
0x81, 0x01, 0x84, 0x09, 0x90, 0x09, 0x82, 0x0D, 0x88, 0x07, 0x81, 0x06,
0x81, 0x09, 0x82, 0x01, 0x81, 0x08, 0x88, 0x0B, 0x84, 0x0E, 0x81, 0x03,
0x82, 0x02, 0x82, 0x06, 0x82, 0x03, 0x88, 0x0E, 0x84, 0x0C, 0x81, 0x06,
0x88, 0x02, 0x81, 0x04, 0x88, 0x0E, 0x81, 0x04, 0x90, 0x0B, 0x81, 0x08,
0x84, 0x09, 0x82, 0x09, 0x81, 0x01, 0x90, 0x03, 0x81, 0x05, 0x84, 0x07,
0x81, 0x07, 0x90, 0x0B, 0x81, 0x07, 0x88, 0x09, 0x81, 0x0E, 0x88, 0x03,
0x81, 0x01, 0x88, 0x09, 0x81, 0x07, 0x81, 0x0C, 0x88, 0x0C, 0x90, 0x0E,
0x81, 0x0E, 0x90, 0x05, 0x84, 0x0E, 0x82, 0x0E, 0x82, 0x01, 0x84, 0x0A,
0x88, 0x03, 0x90, 0x09, 0x82, 0x05, 0x81, 0x0E, 0x81, 0x05, 0x81, 0x07,
0x81, 0x0D, 0x81, 0x0E, 0x90, 0x04, 0x90, 0x04, 0x81, 0x06, 0x81, 0x01,
0x88, 0x0B, 0x81, 0x01, 0x90, 0x0B, 0x81, 0x05, 0x84, 0x0D, 0x81, 0x01,
0x81, 0x0A, 0x84, 0x0D, 0x82, 0x09, 0x88, 0x06, 0x88, 0x04, 0x84, 0x0C,
0x88, 0x0C, 0x84, 0x02, 0x81, 0x07, 0x82, 0x07, 0x84, 0x08, 0x88, 0x09,
0x81, 0x01, 0x88, 0x01, 0x81, 0x0C, 0x81, 0x04, 0x84, 0x06, 0x82, 0x06,
0x90, 0x0D, 0x81, 0x06, 0x81, 0x0D, 0x82, 0x0D, 0x82, 0x08, 0x81, 0x0C,
0x84, 0x05, 0x82, 0x09, 0x81, 0x05, 0x84, 0x07, 0x82, 0x02, 0x84, 0x01,
0x81, 0x02, 0x90, 0x02, 0x82, 0x06, 0x88, 0x03, 0x84, 0x0B, 0x81, 0x0E,
0x88, 0x0E, 0x90, 0x07, 0x82, 0x08, 0x82, 0x02, 0x90, 0x02, 0x82, 0x03,
0x84, 0x07, 0x82, 0x05, 0x84, 0x07, 0x81, 0x0B, 0x82, 0x0E, 0x90, 0x05,
0x88, 0x04, 0x90, 0x05, 0x84, 0x0A, 0x82, 0x0A, 0x88, 0x0C, 0x81, 0x08,
0x82, 0x0C, 0x81, 0x06, 0x84, 0x09, 0x81, 0x04, 0x84, 0x0B, 0x88, 0x0B,
0x81, 0x0C, 0x81, 0x0C, 0x82, 0x0E, 0x81, 0x03, 0x82, 0x09, 0x88, 0x04,
0x81, 0x05, 0x88, 0x04, 0x80, 0x0B, 0x84, 0x06, 0x81, 0x0E, 0x84, 0x0B,
0x90, 0x0B, 0x90, 0x07, 0x82, 0x0D, 0x84, 0x0D, 0x81, 0x08, 0x81, 0x09,
0x84, 0x0E, 0x81, 0x0B, 0x90, 0x04, 0x84, 0x0B, 0x84, 0x03, 0x81, 0x03,
0x88, 0x0A, 0x81, 0x07, 0x84, 0x0B, 0x90, 0x0E,
};
// level P -- 1400 ticks, 11 hook bytes, 10 fare stages, 256 pictures, 4 landings
// regenerate: tools/stsim/recorder levels/level??.dat P 20260921
static const uint8_t kGateStreamP[320] = {
0x88, 0x05, 0x82, 0x09, 0x81, 0x0B, 0x81, 0x04, 0x81, 0x04, 0x84, 0x05,
0x82, 0x01, 0x90, 0x0C, 0x81, 0x03, 0x81, 0x0D, 0x81, 0x01, 0x84, 0x0C,
0x84, 0x0E, 0x88, 0x07, 0x81, 0x0B, 0x88, 0x0D, 0x90, 0x0A, 0x88, 0x02,
0x81, 0x05, 0x84, 0x08, 0x81, 0x05, 0x90, 0x0C, 0x84, 0x01, 0x88, 0x08,
0x88, 0x0A, 0x84, 0x0C, 0x81, 0x04, 0x84, 0x07, 0x90, 0x0C, 0x80, 0x09,
0x90, 0x06, 0x81, 0x03, 0x81, 0x0C, 0x88, 0x01, 0x82, 0x03, 0x81, 0x04,
0x84, 0x0D, 0x88, 0x09, 0x84, 0x05, 0x81, 0x05, 0x81, 0x02, 0x81, 0x09,
0x81, 0x02, 0x90, 0x0E, 0x81, 0x05, 0x81, 0x01, 0x81, 0x03, 0x88, 0x0A,
0x81, 0x06, 0x88, 0x05, 0x90, 0x0C, 0x90, 0x01, 0x84, 0x0E, 0x88, 0x01,
0x81, 0x02, 0x82, 0x05, 0x90, 0x08, 0x88, 0x0A, 0x84, 0x0C, 0x90, 0x08,
0x84, 0x0C, 0x81, 0x06, 0x81, 0x03, 0x88, 0x0A, 0x84, 0x08, 0x90, 0x0E,
0x81, 0x02, 0x88, 0x0C, 0x81, 0x06, 0x84, 0x0C, 0x81, 0x09, 0x80, 0x07,
0x88, 0x04, 0x81, 0x07, 0x81, 0x03, 0x81, 0x09, 0x81, 0x05, 0x86, 0x05,
0x90, 0x09, 0x84, 0x06, 0x81, 0x03, 0x84, 0x09, 0x81, 0x0E, 0x88, 0x0B,
0x90, 0x08, 0x88, 0x0A, 0x81, 0x04, 0x90, 0x0D, 0x88, 0x0E, 0x88, 0x09,
0x81, 0x0B, 0x90, 0x0C, 0x84, 0x05, 0x81, 0x08, 0x84, 0x01, 0x82, 0x02,
0x81, 0x03, 0x82, 0x0E, 0x88, 0x02, 0x88, 0x05, 0x81, 0x01, 0x81, 0x08,
0x82, 0x03, 0x84, 0x0E, 0x81, 0x01, 0x81, 0x08, 0x84, 0x03, 0x82, 0x03,
0x82, 0x05, 0x81, 0x0A, 0x81, 0x02, 0x90, 0x01, 0x81, 0x0E, 0x81, 0x0C,
0x90, 0x0C, 0x88, 0x0A, 0x84, 0x03, 0x81, 0x03, 0x84, 0x0A, 0x84, 0x05,
0x82, 0x09, 0x88, 0x0A, 0x81, 0x06, 0x84, 0x07, 0x84, 0x0C, 0x81, 0x0C,
0x82, 0x07, 0x81, 0x06, 0x81, 0x03, 0x88, 0x09, 0x90, 0x01, 0x84, 0x0E,
0x81, 0x01, 0x88, 0x06, 0x81, 0x0E, 0x81, 0x0A, 0x88, 0x0A, 0x81, 0x08,
0x84, 0x08, 0x88, 0x0E, 0x81, 0x02, 0x84, 0x0E, 0x82, 0x05, 0x81, 0x06,
0x88, 0x0C, 0x81, 0x0B, 0x81, 0x0D, 0x84, 0x02, 0x90, 0x0E, 0x84, 0x0E,
0x81, 0x02, 0x90, 0x07, 0x88, 0x0C, 0x90, 0x0A, 0x81, 0x0E, 0x81, 0x0A,
0x81, 0x0D, 0x81, 0x0B, 0x81, 0x08, 0x81, 0x03,
};
// level S -- 1400 ticks, 0 hook bytes, 10 fare stages, 256 pictures, 23 landings
// regenerate: tools/stsim/recorder levels/level??.dat S 20260921
static const uint8_t kGateStreamS[320] = {
0x90, 0x04, 0x90, 0x0E, 0x88, 0x08, 0x82, 0x0D, 0x88, 0x0C, 0x81, 0x03,
0x81, 0x0A, 0x81, 0x0D, 0x88, 0x09, 0x88, 0x0C, 0x82, 0x0E, 0x81, 0x07,
0x81, 0x0D, 0x81, 0x0A, 0x90, 0x0D, 0x84, 0x01, 0x90, 0x05, 0x88, 0x0E,
0x81, 0x0A, 0x81, 0x0A, 0x90, 0x06, 0x88, 0x03, 0x82, 0x0B, 0x82, 0x0B,
0x88, 0x03, 0x88, 0x0A, 0x90, 0x0C, 0x81, 0x0E, 0x81, 0x0E, 0x90, 0x06,
0x88, 0x02, 0x82, 0x08, 0x81, 0x0A, 0x90, 0x0D, 0x90, 0x05, 0x81, 0x05,
0x88, 0x0B, 0x81, 0x05, 0x90, 0x0B, 0x88, 0x0E, 0x82, 0x09, 0x90, 0x07,
0x81, 0x03, 0x81, 0x0D, 0x88, 0x09, 0x88, 0x0E, 0x84, 0x0A, 0x88, 0x0A,
0x82, 0x01, 0x90, 0x0C, 0x82, 0x01, 0x90, 0x0A, 0x82, 0x0A, 0x84, 0x03,
0x81, 0x02, 0x90, 0x04, 0x84, 0x0E, 0x90, 0x09, 0x81, 0x0D, 0x90, 0x06,
0x81, 0x02, 0x90, 0x0E, 0x90, 0x07, 0x82, 0x05, 0x81, 0x06, 0x90, 0x07,
0x82, 0x03, 0x90, 0x07, 0x90, 0x0B, 0x82, 0x06, 0x88, 0x0E, 0x90, 0x08,
0x84, 0x0A, 0x82, 0x07, 0x81, 0x04, 0x81, 0x03, 0x84, 0x05, 0x90, 0x08,
0x81, 0x0C, 0x82, 0x01, 0x90, 0x0B, 0x81, 0x0A, 0x84, 0x05, 0x81, 0x02,
0x82, 0x09, 0x90, 0x0E, 0x81, 0x02, 0x84, 0x03, 0x81, 0x0C, 0x81, 0x0C,
0x81, 0x0D, 0x82, 0x08, 0x81, 0x03, 0x81, 0x0D, 0x81, 0x0A, 0x81, 0x0A,
0x81, 0x0C, 0x82, 0x06, 0x90, 0x0D, 0x88, 0x0C, 0x84, 0x04, 0x81, 0x01,
0x81, 0x0A, 0x81, 0x03, 0x90, 0x06, 0x88, 0x07, 0x88, 0x0E, 0x84, 0x0E,
0x81, 0x0E, 0x82, 0x03, 0x90, 0x0E, 0x82, 0x05, 0x90, 0x02, 0x88, 0x03,
0x81, 0x07, 0x81, 0x0A, 0x81, 0x0B, 0x81, 0x05, 0x84, 0x0D, 0x81, 0x01,
0x82, 0x02, 0x90, 0x0C, 0x82, 0x03, 0x90, 0x08, 0x81, 0x05, 0x84, 0x08,
0x84, 0x06, 0x88, 0x08, 0x81, 0x0E, 0x82, 0x01, 0x81, 0x01, 0x90, 0x08,
0x82, 0x03, 0x90, 0x0C, 0x84, 0x02, 0x81, 0x09, 0x82, 0x04, 0x88, 0x0D,
0x90, 0x07, 0x88, 0x0E, 0x82, 0x05, 0x81, 0x0C, 0x90, 0x0A, 0x81, 0x04,
0x84, 0x06, 0x82, 0x02, 0x81, 0x09, 0x90, 0x0D, 0x82, 0x06, 0x82, 0x01,
0x90, 0x09, 0x81, 0x0B, 0x84, 0x04, 0x88, 0x07, 0x81, 0x0B, 0x81, 0x05,
0x82, 0x03, 0x81, 0x07, 0x90, 0x06, 0x82, 0x07,
};
typedef struct {
uint8_t level; // level letter the ride was searched for
const uint8_t *stream;
uint16_t length;
} StGateStreamT;
static const StGateStreamT kGateStreams[] = {
{ 'G', kGateStreamG, (uint16_t)sizeof(kGateStreamG) },
{ 'I', kGateStreamI, (uint16_t)sizeof(kGateStreamI) },
{ 'K', kGateStreamK, (uint16_t)sizeof(kGateStreamK) },
{ 'O', kGateStreamO, (uint16_t)sizeof(kGateStreamO) },
{ 'Q', kGateStreamQ, (uint16_t)sizeof(kGateStreamQ) },
{ 'R', kGateStreamR, (uint16_t)sizeof(kGateStreamR) },
{ 'U', kGateStreamU, (uint16_t)sizeof(kGateStreamU) },
{ 'V', kGateStreamV, (uint16_t)sizeof(kGateStreamV) },
{ 'E', kGateStreamE, (uint16_t)sizeof(kGateStreamE) },
{ 'J', kGateStreamJ, (uint16_t)sizeof(kGateStreamJ) },
{ 'L', kGateStreamL, (uint16_t)sizeof(kGateStreamL) },
{ 'P', kGateStreamP, (uint16_t)sizeof(kGateStreamP) },
{ 'S', kGateStreamS, (uint16_t)sizeof(kGateStreamS) },
};
#define ST_GATE_STREAMS (sizeof(kGateStreams) / sizeof(kGateStreams[0]))
#endif

195
tools/stsim/recorder.c Normal file
View file

@ -0,0 +1,195 @@
// recorder -- search for a demo input stream that exercises a level.
//
// The C64 ships attract recordings for four screens (H, W, T, X). The
// other levels have none, so a refactor of their hooks had nothing to be
// checked against. This searches for a synthetic ride instead: random
// restarts plus hill climbing over (joystick mask, duration) pairs,
// scored on how long the cab survives and how much of the level it
// reaches -- hook state bytes written, fare-machine stages entered,
// distinct pictures drawn, pads landed on.
//
// The result is a REGRESSION fixture, not a recording: it pins current
// behaviour so a refactor that changes nothing observable leaves the
// trace identical. It says nothing about matching the real C64.
//
// Usage: recorder <level.dat> <letter> <seed> > stream.h
// Then assemble the per-level outputs into gateStreams.h.
//
// Determinism: a candidate is only useful if it replays identically from
// a cold start, so every evaluation clears the whole StSimT (see the note
// in evaluate) -- stSimNewGame deliberately preserves some cross-screen
// state, which would otherwise leak between candidates.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "stSim.h"
#include "stDemoStreams.h"
static StLevelT gLevel;
static StSimT gSim;
void stAudioSfx(const uint8_t *p){(void)p;}
void stAudioNoise(bool o){(void)o;}
void stAudioThrustSweep(uint8_t v){(void)v;}
void stAudioSpeech(uint8_t c){(void)c;}
void stAudioSpeechPhrase(const uint8_t *s){(void)s;}
void stAudioSilence(void){}
void stAudioVoice2(uint8_t a,uint8_t b,uint8_t c){(void)a;(void)b;(void)c;}
void stAudioVoice1Freq(uint8_t v){(void)v;}
#define PAIRS 160
#define STREAMLEN (PAIRS * 2)
#define MAXTICKS 1400
typedef struct { uint32_t score; uint32_t ticks; uint32_t cover; uint32_t lands; uint32_t stages; uint32_t scr; uint32_t near; } ResT;
static uint32_t rs = 1;
static uint32_t rnd(void){ rs = rs*1664525u + 1013904223u; return rs>>8; }
static ResT evaluate(const uint8_t *stream, uint8_t *touchOut)
{
uint8_t touched[sizeof(StHookScratchT)];
uint8_t base[sizeof(StHookScratchT)];
uint8_t stageSeen[16];
uint32_t t, cover = 0, lands = 0, stages = 0, scrSeen = 0;
uint32_t nearest = 255u;
uint32_t scrHash[256];
uint8_t lastPad = 0;
ResT r;
memset(stageSeen, 0, sizeof(stageSeen));
memset(touched, 0, sizeof(touched));
// stSimNewGame deliberately PRESERVES flameParity, waveIdx and
// walkParity (the C64 carries them from screen to screen), so a
// search that reuses one StSimT would score each candidate against
// whatever the previous ride left behind -- and the winning stream
// would then not reproduce from a cold start. Clear the whole struct
// so every evaluation begins exactly where the gate will.
memset(&gSim, 0, sizeof(gSim));
stSimNewGame(&gSim, 1u, true);
stSimSeedDemoBuffer(&gSim, kDemoBufferInit, ST_DEMO_BUFFER_BYTES);
stSimEnterLevel(&gSim, &gLevel);
stSimSetDemoStream(&gSim, stream, STREAMLEN);
memcpy(base, &gSim.hookScratch, sizeof(base));
for (t = 0; t < MAXTICKS; t++) {
uint16_t k;
if (stSimTick(&gSim) != ST_TICK_CONTINUE) { break; }
{ const uint8_t *cur = (const uint8_t *)&gSim.hookScratch;
for (k = 0; k < (uint16_t)sizeof(base); k++) {
if (cur[k] != base[k]) { touched[k] = 1; } } }
if (gSim.activePad != 0u && lastPad == 0u) { lands++; }
lastPad = gSim.activePad;
if (gSim.stage < 16u) { stageSeen[gSim.stage] = 1u; }
/* Shaping: how close the ride came to an actual touchdown. Some
levels (R) only run their hook once a passenger has boarded,
which needs a landing, and a landing is far too precise for a
blind search to stumble on -- so reward getting near one. */
if (gSim.activePad == 0u && (gSim.spr[0].ptr & 1u) != 0u) {
int16_t tx = (int16_t)(((uint16_t)gSim.posXmsb << 8) | gSim.posXcol);
uint8_t pi;
for (pi = 0u; pi < gLevel.padCount && pi < ST_MAX_PADS; pi++) {
int16_t x1 = (int16_t)(((uint16_t)gSim.pads[pi].x1Hi << 8) | gSim.pads[pi].x1Lo);
int16_t x2 = (int16_t)(((uint16_t)gSim.pads[pi].x2Hi << 8) | gSim.pads[pi].x2Lo);
int16_t dr = (int16_t)((int16_t)gSim.spr[0].row - (int16_t)gSim.pads[pi].row);
uint32_t ar = (uint32_t)(dr < 0 ? -dr : dr);
uint32_t ax = 0u;
uint32_t d;
if (tx < x1) { ax = (uint32_t)(x1 - tx); }
else if (tx > x2) { ax = (uint32_t)(tx - x2); }
/* Both axes matter: the row alone is easy to hit while
the cab sits at the far side of the screen. */
d = ar * 4u + ax;
if (d < nearest) { nearest = d; }
}
}
/* distinct pictures the hook has produced (screen + colour) */
{ uint32_t h = 2166136261u; uint16_t c; uint32_t i2; int dup = 0;
for (c = 0; c < ST_SCREEN_CELLS; c++) { h = (h ^ gSim.screen[c]) * 16777619u; }
for (c = 0; c < ST_SCREEN_CELLS; c++) { h = (h ^ gSim.color[c]) * 16777619u; }
for (i2 = 0; i2 < scrSeen; i2++) { if (scrHash[i2] == h) { dup = 1; break; } }
if (!dup && scrSeen < 256u) { scrHash[scrSeen++] = h; } }
}
{ uint16_t k; for (k = 0; k < (uint16_t)sizeof(touched); k++) { if (touched[k]) { cover++; } } }
{ int k; for (k = 0; k < 16; k++) { if (stageSeen[k]) { stages++; } } }
if (touchOut) { memcpy(touchOut, touched, sizeof(touched)); }
r.ticks = t; r.cover = cover; r.lands = lands; r.stages = stages; r.scr = scrSeen;
r.near = nearest;
r.score = cover * 30000u + stages * 30000u + lands * 4000u + t * 100u + scrSeen * 200u
+ (255u - nearest) * 60u;
return r;
}
static void randomise(uint8_t *s, int from)
{
int i;
for (i = from; i < PAIRS; i++) {
uint32_t d = rnd() % 100u;
uint8_t m = 0x80u;
if (d < 45u) { m |= 0x01u; } /* thrust up a lot */
else if (d < 60u) { m |= 0x04u; }
else if (d < 75u) { m |= 0x08u; }
else if (d < 82u) { m |= 0x02u; }
else if (d < 88u) { m |= 0x10u; }
if (rnd() % 5u == 0u) { m |= (uint8_t)(1u << (rnd() % 4u)); }
s[i*2] = m;
s[i*2 + 1] = (uint8_t)(1u + rnd() % 14u);
}
}
int main(int argc, char **argv)
{
uint8_t best[STREAMLEN], cur[STREAMLEN], touched[ST_HOOK_BYTES];
ResT bestR, curR;
FILE *fp;
int restart, iter;
if (argc < 4) { fprintf(stderr, "usage: recorder <level.dat> <name> <seed>\n"); return 2; }
fp = fopen(argv[1], "rb");
if (!fp || !stLevelParse(&gLevel, fp)) { fprintf(stderr, "cannot load %s\n", argv[1]); return 1; }
fclose(fp);
rs = (uint32_t)strtoul(argv[3], NULL, 0);
randomise(best, 0);
bestR = evaluate(best, NULL);
for (restart = 0; restart < 120; restart++) {
memcpy(cur, best, STREAMLEN);
if (restart > 0) { randomise(cur, (int)(rnd() % PAIRS)); }
curR = evaluate(cur, NULL);
for (iter = 0; iter < 3000; iter++) {
uint8_t trial[STREAMLEN];
ResT tr;
int j, nmut = 1 + (int)(rnd() % 3u);
memcpy(trial, cur, STREAMLEN);
for (j = 0; j < nmut; j++) {
int i = (int)(rnd() % PAIRS);
if (rnd() % 2u) {
uint8_t m = 0x80u;
uint32_t d = rnd() % 100u;
if (d < 45u) { m |= 0x01u; }
else if (d < 62u) { m |= 0x04u; }
else if (d < 79u) { m |= 0x08u; }
else if (d < 88u) { m |= 0x02u; }
else { m |= 0x10u; }
trial[i*2] = m;
} else {
trial[i*2 + 1] = (uint8_t)(1u + rnd() % 14u);
}
}
tr = evaluate(trial, NULL);
if (tr.score >= curR.score) { memcpy(cur, trial, STREAMLEN); curR = tr; }
}
if (curR.score > bestR.score) { memcpy(best, cur, STREAMLEN); bestR = curR; }
}
bestR = evaluate(best, touched);
fprintf(stderr, "%s: ticks=%u hookBytes=%u stages=%u pictures=%u lands=%u nearPad=%u\n",
argv[2], bestR.ticks, bestR.cover, bestR.stages, bestR.scr, bestR.lands, bestR.near);
printf("// %s: %u ticks, %u hook bytes, %u fare stages, %u pictures, %u landings\n",
argv[2], bestR.ticks, bestR.cover, bestR.stages, bestR.scr, bestR.lands);
printf("static const uint8_t kGateStream%s[%d] = {", argv[2], STREAMLEN);
{ int i; for (i = 0; i < STREAMLEN; i++) {
if (i % 12 == 0) { printf("\n "); }
printf("0x%02X,%s", best[i], (i % 12 == 11) ? "" : " "); } }
printf("\n};\n");
return 0;
}

View file

@ -5,14 +5,19 @@
// the VICE tracer records (see stuff/spacetaxi and the compare script), // the VICE tracer records (see stuff/spacetaxi and the compare script),
// so the port's engine can be diffed against the real machine. // so the port's engine can be diffed against the real machine.
// //
// stsim <level.dat> <H|W|T|X> <ticks> // stsim <level.dat> <level letter> <ticks>
//
// H, W, T and X use the C64's own attract recordings; every other
// letter uses a synthesised gate fixture from gateStreams.h.
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include "../../examples/spacetaxi/stSim.h" #include "../../examples/spacetaxi/stSim.h"
#include "../../examples/spacetaxi/stCels.h"
#include "../../examples/spacetaxi/stDemoStreams.h" #include "../../examples/spacetaxi/stDemoStreams.h"
#include "gateStreams.h"
static StLevelT gLevel; static StLevelT gLevel;
@ -23,11 +28,105 @@ void stAudioSfx(const uint8_t *program9) { (void)program9; }
void stAudioNoise(bool on) { (void)on; } void stAudioNoise(bool on) { (void)on; }
void stAudioThrustSweep(uint8_t value) { (void)value; } void stAudioThrustSweep(uint8_t value) { (void)value; }
void stAudioSpeech(uint8_t ch) { (void)ch; } void stAudioSpeech(uint8_t ch) { (void)ch; }
void stAudioSpeechPhrase(const uint8_t *speech) { (void)speech; }
void stAudioSilence(void) { } void stAudioSilence(void) { }
void stAudioVoice2(uint8_t freqLo, uint8_t freqHi, uint8_t ctrl) { (void)freqLo; (void)freqHi; (void)ctrl; } void stAudioVoice2(uint8_t freqLo, uint8_t freqHi, uint8_t ctrl) { (void)freqLo; (void)freqHi; (void)ctrl; }
void stAudioVoice1Freq(uint8_t value) { (void)value; } void stAudioVoice1Freq(uint8_t value) { (void)value; }
// FNV-1a over a byte span: the picture the player actually sees. The
// hooks that animate the screen (O, R, V, W, X) change screen, colour or
// charset bytes and nothing else this line used to capture.
static uint32_t hashBytes(const uint8_t *p, size_t n) {
uint32_t h = 2166136261u;
size_t i;
for (i = 0u; i < n; i++) {
h = (h ^ p[i]) * 16777619u;
}
return h;
}
// The charset the game would be showing. The sim no longer holds a
// contiguous 2 KB copy -- edited glyphs live in an override table -- so
// rebuild the effective image here. Hashing this keeps the trace value
// identical to the pre-override captures, which is what lets the goldens
// prove the change is behaviour-preserving.
static uint32_t charsetHash(const StSimT *sim) {
uint8_t image[ST_CHARSET_CHARS * 8u];
uint16_t ch;
for (ch = 0u; ch < ST_CHARSET_CHARS; ch++) {
memcpy(&image[ch * 8u], stSimGlyph(sim, (uint8_t)ch), 8u);
}
return hashBytes(image, sizeof(image));
}
// STSIM_CELS=1: every level cel (ptr below the baked set) a ride
// displays, as (ptr, colour, multi, mc0, mc1), against what
// stLevelCelList bakes for the level -- a cel shown but not listed is
// built and drawn interpreted mid-ride on the real ports.
#define CENSUS_MAX 64u
static StCelDefT gCensus[CENSUS_MAX];
static uint8_t gCensusCount;
static void celCensusTick(const StSimT *sim) {
uint8_t i;
for (i = 1u; i < ST_HW_SPRITES; i++) {
StCelDefT d;
uint8_t k;
if ((sim->frame.enableMask & kStBit[i]) == 0u || sim->frame.ptr[i] >= ST_SPRITE_PTR_FIRST) {
continue;
}
// A pointer with no bitmap (a hook enables a sprite a tick before
// aiming it) draws nothing on every port -- not a cel.
if (stSimSpriteBitmap(sim, sim->frame.ptr[i]) == 0) {
continue;
}
d.ptr = sim->frame.ptr[i];
d.color = sim->frame.color[i];
d.multi = (sim->frame.multiMask & kStBit[i]) != 0u ? 1u : 0u;
d.mc0 = d.multi ? sim->spriteMc0 : 0u;
d.mc1 = d.multi ? sim->spriteMc1 : 0u;
for (k = 0u; k < gCensusCount; k++) {
if (memcmp(&gCensus[k], &d, sizeof(d)) == 0) {
break;
}
}
if (k == gCensusCount && gCensusCount < CENSUS_MAX) {
gCensus[gCensusCount++] = d;
}
}
}
static void celCensusReport(const StLevelT *level) {
StCelDefT listed[ST_LEVEL_CELS_MAX];
uint8_t n = stLevelCelList(level, listed, ST_LEVEL_CELS_MAX);
uint8_t k;
uint8_t j;
for (k = 0u; k < gCensusCount; k++) {
for (j = 0u; j < n; j++) {
if (memcmp(&listed[j], &gCensus[k], sizeof(StCelDefT)) == 0) {
break;
}
}
fprintf(stderr, "CEL %s ptr=%02X color=%u multi=%u mc=%u/%u\n", (j < n) ? "listed " : "MISSING", gCensus[k].ptr, gCensus[k].color, gCensus[k].multi, gCensus[k].mc0, gCensus[k].mc1);
}
fprintf(stderr, "CENSUS shown=%u listed=%u\n", gCensusCount, n);
}
static void celCensusReport(const StLevelT *level);
static void celCensusTick(const StSimT *sim);
static void printState(const StSimT *sim, uint32_t tick) { static void printState(const StSimT *sim, uint32_t tick) {
uint8_t k; uint8_t k;
@ -55,7 +154,11 @@ static void printState(const StSimT *sim, uint32_t tick) {
for (k = 0u; k < ST_HW_SPRITES; k++) { for (k = 0u; k < ST_HW_SPRITES; k++) {
printf("%03X%02X", sim->frame.x[k], sim->frame.y[k]); printf("%03X%02X", sim->frame.x[k], sim->frame.y[k]);
} }
printf("\"}\n"); printf("\",\"scr\":\"%08X\",\"col\":\"%08X\",\"chr\":\"%08X\",\"bd\":\"%02X\",\"bg\":\"%02X\"}\n",
hashBytes(sim->screen, ST_SCREEN_CELLS),
hashBytes(sim->color, ST_SCREEN_CELLS),
charsetHash(sim),
sim->borderColor, sim->bgColor);
} }
@ -63,11 +166,12 @@ int main(int argc, char **argv) {
FILE *fp; FILE *fp;
const uint8_t *stream = kDemoStreamH; const uint8_t *stream = kDemoStreamH;
uint16_t len = (uint16_t)sizeof(kDemoStreamH); uint16_t len = (uint16_t)sizeof(kDemoStreamH);
uint8_t want;
uint32_t ticks; uint32_t ticks;
uint32_t t; uint32_t t;
if (argc < 4) { if (argc < 4) {
fprintf(stderr, "usage: stsim <level.dat> <H|W|T|X> <ticks>\n"); fprintf(stderr, "usage: stsim <level.dat> <level letter> <ticks>\n");
return 2; return 2;
} }
fp = fopen(argv[1], "rb"); fp = fopen(argv[1], "rb");
@ -76,11 +180,29 @@ int main(int argc, char **argv) {
return 1; return 1;
} }
fclose(fp); fclose(fp);
switch (argv[2][0]) { want = (uint8_t)argv[2][0];
switch (want) {
case 'W': stream = kDemoStreamW; len = (uint16_t)sizeof(kDemoStreamW); break; case 'W': stream = kDemoStreamW; len = (uint16_t)sizeof(kDemoStreamW); break;
case 'T': stream = kDemoStreamT; len = (uint16_t)sizeof(kDemoStreamT); break; case 'T': stream = kDemoStreamT; len = (uint16_t)sizeof(kDemoStreamT); break;
case 'X': stream = kDemoStreamX; len = (uint16_t)sizeof(kDemoStreamX); break; case 'X': stream = kDemoStreamX; len = (uint16_t)sizeof(kDemoStreamX); break;
default: break; case 'H': break;
default: {
// A synthesised gate fixture for a level the C64 has no
// attract recording of (see gateStreams.h).
uint8_t g;
for (g = 0u; g < ST_GATE_STREAMS; g++) {
if (kGateStreams[g].level == want) {
stream = kGateStreams[g].stream;
len = kGateStreams[g].length;
break;
}
}
if (g == ST_GATE_STREAMS) {
fprintf(stderr, "stsim: no stream for '%c'\n", (char)want);
return 2;
}
break;
}
} }
ticks = (uint32_t)strtoul(argv[3], NULL, 0); ticks = (uint32_t)strtoul(argv[3], NULL, 0);
stSimNewGame(&gSim, 1u, true); stSimNewGame(&gSim, 1u, true);
@ -101,10 +223,16 @@ int main(int argc, char **argv) {
StTickResultE r; StTickResultE r;
printState(&gSim, t); printState(&gSim, t);
r = stSimTick(&gSim); r = stSimTick(&gSim);
if (getenv("STSIM_CELS") != NULL) {
celCensusTick(&gSim);
}
if (r != ST_TICK_CONTINUE) { if (r != ST_TICK_CONTINUE) {
printf("{\"tick\":%u,\"end\":%d}\n", t, (int)r); printf("{\"tick\":%u,\"end\":%d}\n", t, (int)r);
break; break;
} }
} }
if (getenv("STSIM_CELS") != NULL) {
celCensusReport(&gLevel);
}
return 0; return 0;
} }