diff --git a/examples/spacetaxi/assets/genC64Data.py b/examples/spacetaxi/assets/genC64Data.py new file mode 100644 index 0000000..c0feaa5 --- /dev/null +++ b/examples/spacetaxi/assets/genC64Data.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +# genC64Data.py -- emit the C64 game data the JoeyLib Space Taxi port +# needs at runtime as C source: the custom charset (rendering AND the +# exact sprite-to-background collision bits), the standard sprite +# bitmaps (cab, passenger, flame, warp, death, intro star, leaving +# cels), the demo-mode RNG table ($446A), the SID SFX programs (the +# 9-byte blocks fed to $42E9), the engine-flame pointer table ($6DB0) +# and the level-intro star velocity tables ($450C/$4513). +# +# Everything comes straight out of stuff/spacetaxi/raw.bin (a VICE +# "bank ram" dump, 2-byte load-address header) so the port has one +# source of truth: the original bytes. Output: +# examples/spacetaxi/stC64Data.c / stC64Data.h (committed, do not edit) + +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +RAW = os.path.join(HERE, "..", "..", "..", "stuff", "spacetaxi", "raw.bin") +OUT_C = os.path.join(HERE, "..", "stC64Data.c") +OUT_H = os.path.join(HERE, "..", "stC64Data.h") + +SPRITE_PTR_FIRST = 0xC0 +SPRITE_PTR_LAST = 0xEC # $E9-$EC = passengers-leaving cels +SPRITE_BYTES = 63 + +# (enum name, address of the 9-byte program, comment) +SFX = [ + ("ST_SFX_GEAR", 0x6410, "landing gear toggle ($63DD)"), + ("ST_SFX_CASH", 0x6DBB, "refuel ka-ching ($6E5B)"), + ("ST_SFX_FUEL_LOW", 0x6DC4, "fuel below 3 cells ($6EBD)"), + ("ST_SFX_FUEL_FULL", 0x6DCD, "tank full ($6E9E)"), + ("ST_SFX_LAND_HARD", 0x6DD6, "hard landing ($6520)"), + ("ST_SFX_IMPACT", 0x6DDF, "wreck hits the floor ($6AE1)"), + ("ST_SFX_LAND_SOFT", 0x6DE8, "soft landing ($650F)"), + ("ST_SFX_CRASH", 0x6DF1, "crash start ($6A56)"), +] + + +def load_raw(): + with open(RAW, "rb") as fp: + raw = fp.read() + if len(raw) == 0x10002: + raw = raw[2:] + if len(raw) != 0x10000: + sys.exit(f"{RAW}: unexpected size {len(raw)}") + return raw + + +def c_bytes(data, per_line=16, indent=" "): + lines = [] + for i in range(0, len(data), per_line): + chunk = data[i:i + per_line] + lines.append(indent + ", ".join(f"0x{b:02X}" for b in chunk) + ",") + return "\n".join(lines) + + +def main(): + raw = load_raw() + sprite_count = SPRITE_PTR_LAST - SPRITE_PTR_FIRST + 1 + + h = [] + h.append("// Generated by assets/genC64Data.py from stuff/spacetaxi/raw.bin.") + h.append("// Do not hand-edit; re-run the generator.") + h.append("") + h.append("#ifndef ST_C64_DATA_H") + h.append("#define ST_C64_DATA_H") + h.append("") + h.append("#include ") + h.append("") + h.append("// The game's custom character set ($2800-$2FFF): 256 glyphs x 8 rows,") + h.append("// bit 7 = leftmost pixel. Used to paint the screen AND for the") + h.append("// sprite-to-background collision test (a set bit is background data).") + h.append("#define ST_CHARSET_CHARS 256u") + h.append("extern const uint8_t kStCharset[ST_CHARSET_CHARS][8];") + h.append("") + h.append("// Standard sprite bitmaps (VIC bank 0, block ptr * 64). 21 rows x 3") + h.append("// bytes. Index = ptr - ST_SPRITE_PTR_FIRST.") + h.append(f"#define ST_SPRITE_PTR_FIRST 0x{SPRITE_PTR_FIRST:02X}u") + h.append(f"#define ST_SPRITE_PTR_LAST 0x{SPRITE_PTR_LAST:02X}u") + h.append(f"#define ST_SPRITE_COUNT {sprite_count}u") + h.append(f"#define ST_SPRITE_BYTES {SPRITE_BYTES}u") + h.append("extern const uint8_t kStSpriteBitmaps[ST_SPRITE_COUNT][ST_SPRITE_BYTES];") + h.append("") + h.append("// Demo-mode RNG lookup ($446A): rnd = table[t1] + t2 (see stSim.c).") + h.append("#define ST_RNG_TABLE_SIZE 64u") + h.append("extern const uint8_t kStRngTable[ST_RNG_TABLE_SIZE];") + h.append("") + h.append("// SID SFX programs, 9 bytes each, in $42E9 order: freq lo, freq hi,") + h.append("// pulse lo, pulse hi, control, AD, SR, release ticks, voice index.") + h.append("typedef enum {") + for name, addr, comment in SFX: + h.append(f" {name}, // ${addr:04X} {comment}") + h.append(" ST_SFX_COUNT") + h.append("} StSfxE;") + h.append("#define ST_SFX_PROGRAM_BYTES 9u") + h.append("extern const uint8_t kStSfxPrograms[ST_SFX_COUNT][ST_SFX_PROGRAM_BYTES];") + h.append("") + h.append("// Engine-flame cel pointer by direction mask ($6DB0, 1=UP 2=DOWN 4=LEFT") + h.append("// 8=RIGHT); 0 = no cel for that combination.") + h.append("extern const uint8_t kStFlameCelByDirMask[16];") + h.append("") + h.append("// Level-intro star velocities per sprite 0..6 ($450C / $4513).") + h.append("extern const int8_t kStIntroStarDx[7];") + h.append("extern const int8_t kStIntroStarDy[7];") + h.append("") + h.append("#endif") + + c = [] + c.append("// Generated by assets/genC64Data.py from stuff/spacetaxi/raw.bin.") + c.append("// Do not hand-edit; re-run the generator.") + c.append("") + c.append('#include "stC64Data.h"') + c.append("") + c.append("const uint8_t kStCharset[ST_CHARSET_CHARS][8] = {") + for ch in range(256): + rows = raw[0x2800 + ch * 8:0x2800 + ch * 8 + 8] + c.append(" { " + ", ".join(f"0x{b:02X}" for b in rows) + " }, // $" + f"{ch:02X}") + c.append("};") + c.append("") + c.append("const uint8_t kStSpriteBitmaps[ST_SPRITE_COUNT][ST_SPRITE_BYTES] = {") + for ptr in range(SPRITE_PTR_FIRST, SPRITE_PTR_LAST + 1): + base = ptr * 64 + c.append(f" {{ // ptr ${ptr:02X}") + for row in range(21): + b = raw[base + row * 3:base + row * 3 + 3] + c.append(" " + ", ".join(f"0x{x:02X}" for x in b) + ",") + c.append(" },") + c.append("};") + c.append("") + c.append("const uint8_t kStRngTable[ST_RNG_TABLE_SIZE] = {") + c.append(c_bytes(raw[0x446A:0x446A + 64])) + c.append("};") + c.append("") + c.append("const uint8_t kStSfxPrograms[ST_SFX_COUNT][ST_SFX_PROGRAM_BYTES] = {") + for name, addr, comment in SFX: + b = raw[addr:addr + 9] + c.append(" { " + ", ".join(f"0x{x:02X}" for x in b) + " }, // " + name) + c.append("};") + c.append("") + flame = list(raw[0x6DB0:0x6DB0 + 11]) + [0] * 5 + c.append("const uint8_t kStFlameCelByDirMask[16] = {") + c.append(c_bytes(flame)) + c.append("};") + c.append("") + 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("") + + with open(OUT_H, "w", encoding="ascii") as fp: + fp.write("\n".join(h) + "\n") + with open(OUT_C, "w", encoding="ascii") as fp: + fp.write("\n".join(c) + "\n") + print(f"wrote {OUT_H} and {OUT_C}") + + +if __name__ == "__main__": + main() diff --git a/examples/spacetaxi/assets/genDemoStreams.py b/examples/spacetaxi/assets/genDemoStreams.py index 9517557..5095393 100644 --- a/examples/spacetaxi/assets/genDemoStreams.py +++ b/examples/spacetaxi/assets/genDemoStreams.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 # Embed the C64 attract-demo recorded input streams (the four # "SPACETAXI 0x" disk files -- RLE [joystick mask][tick count] pairs; -# mask bit0=UP bit1=DOWN bit2=LEFT bit3=RIGHT, bit7 always set, no -# fire bit -- which is why every recorded ride ends in a crash: the -# demo cab can never lower its gear) as C arrays for stDemoStreams.h. +# mask bit0=UP bit1=DOWN bit2=LEFT bit3=RIGHT bit4=FIRE, bit7 always +# set) as C arrays for stDemoStreams.h. # -# Every stream file is odd-length: the $9936 saver's end=$3F+3 -# off-by-one appends one dangling byte past the last complete pair, -# trimmed here on all four. +# Every stream file is odd-length: the $9936 saver appends one +# dangling byte past the last complete pair. It is kept: the C64 reads +# it (and whatever follows in the buffer) when a ride outlasts its +# recording. # # Rotation order is H -> W -> T -> X ($6FE2/$50C1 trace) with file # mapping H=01, T=02, W=03, X=04 and demo levels H=8, W=23, T=20, @@ -38,15 +38,26 @@ def main(): entries = [] for fname, sym, level, letter in STREAMS: data = open(os.path.join(EXTRACT, fname), "rb").read() - if len(data) % 2 == 1: - data = data[:-1] # trim the saver's dangling byte - f.write(f"// {letter}: {fname} ({len(data)} bytes after trim)\n") + # The whole file, dangling byte included: the C64 plays past + # the last pair into whatever the buffer held before. + f.write(f"// {letter}: {fname} ({len(data)} bytes)\n") f.write(f"static const uint8_t {sym}[{len(data)}] = {{\n") for i in range(0, len(data), 12): row = ", ".join(f"0x{b:02X}" for b in data[i:i + 12]) f.write(f" {row},\n") f.write("};\n\n") entries.append((sym, len(data), level)) + # The playback buffer at $0902 as the boot leaves it (demo H + # loaded over older RAM): recordings shorter than the previous one + # leave its tail in place, and the game reads on into it. + raw = open(os.path.join(HERE, "..", "..", "..", "stuff", "spacetaxi", "raw.bin"), "rb").read()[2:] + buf = raw[0x0902:0x0902 + 640] + f.write("#define ST_DEMO_BUFFER_BYTES 640u\n") + f.write("static const uint8_t kDemoBufferInit[ST_DEMO_BUFFER_BYTES] = {\n") + for i in range(0, len(buf), 12): + row = ", ".join(f"0x{b:02X}" for b in buf[i:i + 12]) + f.write(f" {row},\n") + f.write("};\n\n") f.write("typedef struct {\n") f.write(" const uint8_t *stream;\n") f.write(" uint16_t length;\n") diff --git a/examples/spacetaxi/generated/amiga/levels/level01.dat b/examples/spacetaxi/generated/amiga/levels/level01.dat index f8a6f9a..0c762d8 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level01.dat and b/examples/spacetaxi/generated/amiga/levels/level01.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level02.dat b/examples/spacetaxi/generated/amiga/levels/level02.dat index dea5903..dbebde5 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level02.dat and b/examples/spacetaxi/generated/amiga/levels/level02.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level03.dat b/examples/spacetaxi/generated/amiga/levels/level03.dat index 9b5a703..41ae91b 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level03.dat and b/examples/spacetaxi/generated/amiga/levels/level03.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level04.dat b/examples/spacetaxi/generated/amiga/levels/level04.dat index 74993ec..55b6b37 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level04.dat and b/examples/spacetaxi/generated/amiga/levels/level04.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level05.dat b/examples/spacetaxi/generated/amiga/levels/level05.dat index 8467bf0..02e66be 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level05.dat and b/examples/spacetaxi/generated/amiga/levels/level05.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level06.dat b/examples/spacetaxi/generated/amiga/levels/level06.dat index d232194..f744d27 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level06.dat and b/examples/spacetaxi/generated/amiga/levels/level06.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level07.dat b/examples/spacetaxi/generated/amiga/levels/level07.dat index 805246c..0eb4025 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level07.dat and b/examples/spacetaxi/generated/amiga/levels/level07.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level08.dat b/examples/spacetaxi/generated/amiga/levels/level08.dat index 3d6d273..08cdd01 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level08.dat and b/examples/spacetaxi/generated/amiga/levels/level08.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level09.dat b/examples/spacetaxi/generated/amiga/levels/level09.dat index fc9246e..6451796 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level09.dat and b/examples/spacetaxi/generated/amiga/levels/level09.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level10.dat b/examples/spacetaxi/generated/amiga/levels/level10.dat index c4717cc..afc00aa 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level10.dat and b/examples/spacetaxi/generated/amiga/levels/level10.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level11.dat b/examples/spacetaxi/generated/amiga/levels/level11.dat index 4c3ce87..8099ce8 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level11.dat and b/examples/spacetaxi/generated/amiga/levels/level11.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level12.dat b/examples/spacetaxi/generated/amiga/levels/level12.dat index 6a5ba48..50b1b33 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level12.dat and b/examples/spacetaxi/generated/amiga/levels/level12.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level13.dat b/examples/spacetaxi/generated/amiga/levels/level13.dat index 459177b..3eb8f9d 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level13.dat and b/examples/spacetaxi/generated/amiga/levels/level13.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level14.dat b/examples/spacetaxi/generated/amiga/levels/level14.dat index 4a4374a..3e5b7c3 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level14.dat and b/examples/spacetaxi/generated/amiga/levels/level14.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level15.dat b/examples/spacetaxi/generated/amiga/levels/level15.dat index f091239..7607e7d 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level15.dat and b/examples/spacetaxi/generated/amiga/levels/level15.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level16.dat b/examples/spacetaxi/generated/amiga/levels/level16.dat index e6471a5..11f416d 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level16.dat and b/examples/spacetaxi/generated/amiga/levels/level16.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level17.dat b/examples/spacetaxi/generated/amiga/levels/level17.dat index 4aa266c..457defe 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level17.dat and b/examples/spacetaxi/generated/amiga/levels/level17.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level18.dat b/examples/spacetaxi/generated/amiga/levels/level18.dat index 2303e0d..b682e46 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level18.dat and b/examples/spacetaxi/generated/amiga/levels/level18.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level19.dat b/examples/spacetaxi/generated/amiga/levels/level19.dat index 76c0dd3..7a90761 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level19.dat and b/examples/spacetaxi/generated/amiga/levels/level19.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level20.dat b/examples/spacetaxi/generated/amiga/levels/level20.dat index 0c70215..396008f 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level20.dat and b/examples/spacetaxi/generated/amiga/levels/level20.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level21.dat b/examples/spacetaxi/generated/amiga/levels/level21.dat index cd178e5..7d6c4c9 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level21.dat and b/examples/spacetaxi/generated/amiga/levels/level21.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level22.dat b/examples/spacetaxi/generated/amiga/levels/level22.dat index 89df634..75c4d17 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level22.dat and b/examples/spacetaxi/generated/amiga/levels/level22.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level23.dat b/examples/spacetaxi/generated/amiga/levels/level23.dat index ccf8623..23b1bec 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level23.dat and b/examples/spacetaxi/generated/amiga/levels/level23.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level24.dat b/examples/spacetaxi/generated/amiga/levels/level24.dat index a499a94..13dca31 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level24.dat and b/examples/spacetaxi/generated/amiga/levels/level24.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/title.dat b/examples/spacetaxi/generated/amiga/levels/title.dat index 9887b56..83ca545 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/title.dat and b/examples/spacetaxi/generated/amiga/levels/title.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level01.dat b/examples/spacetaxi/generated/atarist/levels/level01.dat index f8a6f9a..0c762d8 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level01.dat and b/examples/spacetaxi/generated/atarist/levels/level01.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level02.dat b/examples/spacetaxi/generated/atarist/levels/level02.dat index dea5903..dbebde5 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level02.dat and b/examples/spacetaxi/generated/atarist/levels/level02.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level03.dat b/examples/spacetaxi/generated/atarist/levels/level03.dat index 9b5a703..41ae91b 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level03.dat and b/examples/spacetaxi/generated/atarist/levels/level03.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level04.dat b/examples/spacetaxi/generated/atarist/levels/level04.dat index 74993ec..55b6b37 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level04.dat and b/examples/spacetaxi/generated/atarist/levels/level04.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level05.dat b/examples/spacetaxi/generated/atarist/levels/level05.dat index 8467bf0..02e66be 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level05.dat and b/examples/spacetaxi/generated/atarist/levels/level05.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level06.dat b/examples/spacetaxi/generated/atarist/levels/level06.dat index d232194..f744d27 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level06.dat and b/examples/spacetaxi/generated/atarist/levels/level06.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level07.dat b/examples/spacetaxi/generated/atarist/levels/level07.dat index 805246c..0eb4025 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level07.dat and b/examples/spacetaxi/generated/atarist/levels/level07.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level08.dat b/examples/spacetaxi/generated/atarist/levels/level08.dat index 3d6d273..08cdd01 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level08.dat and b/examples/spacetaxi/generated/atarist/levels/level08.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level09.dat b/examples/spacetaxi/generated/atarist/levels/level09.dat index fc9246e..6451796 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level09.dat and b/examples/spacetaxi/generated/atarist/levels/level09.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level10.dat b/examples/spacetaxi/generated/atarist/levels/level10.dat index c4717cc..afc00aa 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level10.dat and b/examples/spacetaxi/generated/atarist/levels/level10.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level11.dat b/examples/spacetaxi/generated/atarist/levels/level11.dat index 4c3ce87..8099ce8 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level11.dat and b/examples/spacetaxi/generated/atarist/levels/level11.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level12.dat b/examples/spacetaxi/generated/atarist/levels/level12.dat index 6a5ba48..50b1b33 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level12.dat and b/examples/spacetaxi/generated/atarist/levels/level12.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level13.dat b/examples/spacetaxi/generated/atarist/levels/level13.dat index 459177b..3eb8f9d 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level13.dat and b/examples/spacetaxi/generated/atarist/levels/level13.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level14.dat b/examples/spacetaxi/generated/atarist/levels/level14.dat index 4a4374a..3e5b7c3 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level14.dat and b/examples/spacetaxi/generated/atarist/levels/level14.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level15.dat b/examples/spacetaxi/generated/atarist/levels/level15.dat index f091239..7607e7d 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level15.dat and b/examples/spacetaxi/generated/atarist/levels/level15.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level16.dat b/examples/spacetaxi/generated/atarist/levels/level16.dat index e6471a5..11f416d 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level16.dat and b/examples/spacetaxi/generated/atarist/levels/level16.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level17.dat b/examples/spacetaxi/generated/atarist/levels/level17.dat index 4aa266c..457defe 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level17.dat and b/examples/spacetaxi/generated/atarist/levels/level17.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level18.dat b/examples/spacetaxi/generated/atarist/levels/level18.dat index 2303e0d..b682e46 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level18.dat and b/examples/spacetaxi/generated/atarist/levels/level18.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level19.dat b/examples/spacetaxi/generated/atarist/levels/level19.dat index 76c0dd3..7a90761 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level19.dat and b/examples/spacetaxi/generated/atarist/levels/level19.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level20.dat b/examples/spacetaxi/generated/atarist/levels/level20.dat index 0c70215..396008f 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level20.dat and b/examples/spacetaxi/generated/atarist/levels/level20.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level21.dat b/examples/spacetaxi/generated/atarist/levels/level21.dat index cd178e5..7d6c4c9 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level21.dat and b/examples/spacetaxi/generated/atarist/levels/level21.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level22.dat b/examples/spacetaxi/generated/atarist/levels/level22.dat index 89df634..75c4d17 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level22.dat and b/examples/spacetaxi/generated/atarist/levels/level22.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level23.dat b/examples/spacetaxi/generated/atarist/levels/level23.dat index ccf8623..23b1bec 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level23.dat and b/examples/spacetaxi/generated/atarist/levels/level23.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level24.dat b/examples/spacetaxi/generated/atarist/levels/level24.dat index a499a94..13dca31 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level24.dat and b/examples/spacetaxi/generated/atarist/levels/level24.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/title.dat b/examples/spacetaxi/generated/atarist/levels/title.dat index 9887b56..83ca545 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/title.dat and b/examples/spacetaxi/generated/atarist/levels/title.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level01.dat b/examples/spacetaxi/generated/dos/levels/level01.dat index f8a6f9a..0c762d8 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level01.dat and b/examples/spacetaxi/generated/dos/levels/level01.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level02.dat b/examples/spacetaxi/generated/dos/levels/level02.dat index dea5903..dbebde5 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level02.dat and b/examples/spacetaxi/generated/dos/levels/level02.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level03.dat b/examples/spacetaxi/generated/dos/levels/level03.dat index 9b5a703..41ae91b 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level03.dat and b/examples/spacetaxi/generated/dos/levels/level03.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level04.dat b/examples/spacetaxi/generated/dos/levels/level04.dat index 74993ec..55b6b37 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level04.dat and b/examples/spacetaxi/generated/dos/levels/level04.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level05.dat b/examples/spacetaxi/generated/dos/levels/level05.dat index 8467bf0..02e66be 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level05.dat and b/examples/spacetaxi/generated/dos/levels/level05.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level06.dat b/examples/spacetaxi/generated/dos/levels/level06.dat index d232194..f744d27 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level06.dat and b/examples/spacetaxi/generated/dos/levels/level06.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level07.dat b/examples/spacetaxi/generated/dos/levels/level07.dat index 805246c..0eb4025 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level07.dat and b/examples/spacetaxi/generated/dos/levels/level07.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level08.dat b/examples/spacetaxi/generated/dos/levels/level08.dat index 3d6d273..08cdd01 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level08.dat and b/examples/spacetaxi/generated/dos/levels/level08.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level09.dat b/examples/spacetaxi/generated/dos/levels/level09.dat index fc9246e..6451796 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level09.dat and b/examples/spacetaxi/generated/dos/levels/level09.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level10.dat b/examples/spacetaxi/generated/dos/levels/level10.dat index c4717cc..afc00aa 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level10.dat and b/examples/spacetaxi/generated/dos/levels/level10.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level11.dat b/examples/spacetaxi/generated/dos/levels/level11.dat index 4c3ce87..8099ce8 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level11.dat and b/examples/spacetaxi/generated/dos/levels/level11.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level12.dat b/examples/spacetaxi/generated/dos/levels/level12.dat index 6a5ba48..50b1b33 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level12.dat and b/examples/spacetaxi/generated/dos/levels/level12.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level13.dat b/examples/spacetaxi/generated/dos/levels/level13.dat index 459177b..3eb8f9d 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level13.dat and b/examples/spacetaxi/generated/dos/levels/level13.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level14.dat b/examples/spacetaxi/generated/dos/levels/level14.dat index 4a4374a..3e5b7c3 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level14.dat and b/examples/spacetaxi/generated/dos/levels/level14.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level15.dat b/examples/spacetaxi/generated/dos/levels/level15.dat index f091239..7607e7d 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level15.dat and b/examples/spacetaxi/generated/dos/levels/level15.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level16.dat b/examples/spacetaxi/generated/dos/levels/level16.dat index e6471a5..11f416d 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level16.dat and b/examples/spacetaxi/generated/dos/levels/level16.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level17.dat b/examples/spacetaxi/generated/dos/levels/level17.dat index 4aa266c..457defe 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level17.dat and b/examples/spacetaxi/generated/dos/levels/level17.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level18.dat b/examples/spacetaxi/generated/dos/levels/level18.dat index 2303e0d..b682e46 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level18.dat and b/examples/spacetaxi/generated/dos/levels/level18.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level19.dat b/examples/spacetaxi/generated/dos/levels/level19.dat index 76c0dd3..7a90761 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level19.dat and b/examples/spacetaxi/generated/dos/levels/level19.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level20.dat b/examples/spacetaxi/generated/dos/levels/level20.dat index 0c70215..396008f 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level20.dat and b/examples/spacetaxi/generated/dos/levels/level20.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level21.dat b/examples/spacetaxi/generated/dos/levels/level21.dat index cd178e5..7d6c4c9 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level21.dat and b/examples/spacetaxi/generated/dos/levels/level21.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level22.dat b/examples/spacetaxi/generated/dos/levels/level22.dat index 89df634..75c4d17 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level22.dat and b/examples/spacetaxi/generated/dos/levels/level22.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level23.dat b/examples/spacetaxi/generated/dos/levels/level23.dat index ccf8623..23b1bec 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level23.dat and b/examples/spacetaxi/generated/dos/levels/level23.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level24.dat b/examples/spacetaxi/generated/dos/levels/level24.dat index a499a94..13dca31 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level24.dat and b/examples/spacetaxi/generated/dos/levels/level24.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/title.dat b/examples/spacetaxi/generated/dos/levels/title.dat index 9887b56..83ca545 100644 Binary files a/examples/spacetaxi/generated/dos/levels/title.dat and b/examples/spacetaxi/generated/dos/levels/title.dat differ diff --git a/examples/spacetaxi/generated/iigs/levels/level01.dat b/examples/spacetaxi/generated/iigs/levels/level01.dat index f8a6f9a..0c762d8 100644 Binary files a/examples/spacetaxi/generated/iigs/levels/level01.dat and b/examples/spacetaxi/generated/iigs/levels/level01.dat differ diff --git a/examples/spacetaxi/generated/iigs/levels/level02.dat b/examples/spacetaxi/generated/iigs/levels/level02.dat index dea5903..dbebde5 100644 Binary files a/examples/spacetaxi/generated/iigs/levels/level02.dat and b/examples/spacetaxi/generated/iigs/levels/level02.dat differ diff --git a/examples/spacetaxi/generated/iigs/levels/level03.dat b/examples/spacetaxi/generated/iigs/levels/level03.dat index 9b5a703..41ae91b 100644 Binary files a/examples/spacetaxi/generated/iigs/levels/level03.dat and b/examples/spacetaxi/generated/iigs/levels/level03.dat differ diff --git a/examples/spacetaxi/generated/iigs/levels/level04.dat b/examples/spacetaxi/generated/iigs/levels/level04.dat index 74993ec..55b6b37 100644 Binary files a/examples/spacetaxi/generated/iigs/levels/level04.dat and b/examples/spacetaxi/generated/iigs/levels/level04.dat differ diff --git a/examples/spacetaxi/generated/iigs/levels/level05.dat b/examples/spacetaxi/generated/iigs/levels/level05.dat index 8467bf0..02e66be 100644 Binary files a/examples/spacetaxi/generated/iigs/levels/level05.dat and b/examples/spacetaxi/generated/iigs/levels/level05.dat differ diff --git a/examples/spacetaxi/generated/iigs/levels/level06.dat b/examples/spacetaxi/generated/iigs/levels/level06.dat index d232194..f744d27 100644 Binary files a/examples/spacetaxi/generated/iigs/levels/level06.dat and b/examples/spacetaxi/generated/iigs/levels/level06.dat differ diff --git a/examples/spacetaxi/generated/iigs/levels/level07.dat b/examples/spacetaxi/generated/iigs/levels/level07.dat index 805246c..0eb4025 100644 Binary files a/examples/spacetaxi/generated/iigs/levels/level07.dat and b/examples/spacetaxi/generated/iigs/levels/level07.dat differ diff --git a/examples/spacetaxi/generated/iigs/levels/level08.dat b/examples/spacetaxi/generated/iigs/levels/level08.dat index 3d6d273..08cdd01 100644 Binary files a/examples/spacetaxi/generated/iigs/levels/level08.dat and b/examples/spacetaxi/generated/iigs/levels/level08.dat differ diff --git a/examples/spacetaxi/generated/iigs/levels/level09.dat b/examples/spacetaxi/generated/iigs/levels/level09.dat index fc9246e..6451796 100644 Binary files a/examples/spacetaxi/generated/iigs/levels/level09.dat and b/examples/spacetaxi/generated/iigs/levels/level09.dat differ diff --git a/examples/spacetaxi/generated/iigs/levels/level10.dat b/examples/spacetaxi/generated/iigs/levels/level10.dat index c4717cc..afc00aa 100644 Binary files a/examples/spacetaxi/generated/iigs/levels/level10.dat and b/examples/spacetaxi/generated/iigs/levels/level10.dat differ diff --git a/examples/spacetaxi/generated/iigs/levels/level11.dat b/examples/spacetaxi/generated/iigs/levels/level11.dat index 4c3ce87..8099ce8 100644 Binary files a/examples/spacetaxi/generated/iigs/levels/level11.dat and b/examples/spacetaxi/generated/iigs/levels/level11.dat differ diff --git a/examples/spacetaxi/generated/iigs/levels/level12.dat b/examples/spacetaxi/generated/iigs/levels/level12.dat index 6a5ba48..50b1b33 100644 Binary files a/examples/spacetaxi/generated/iigs/levels/level12.dat and b/examples/spacetaxi/generated/iigs/levels/level12.dat differ diff --git a/examples/spacetaxi/generated/iigs/levels/level13.dat b/examples/spacetaxi/generated/iigs/levels/level13.dat index 459177b..3eb8f9d 100644 Binary files a/examples/spacetaxi/generated/iigs/levels/level13.dat and b/examples/spacetaxi/generated/iigs/levels/level13.dat differ diff --git a/examples/spacetaxi/generated/iigs/levels/level14.dat b/examples/spacetaxi/generated/iigs/levels/level14.dat index 4a4374a..3e5b7c3 100644 Binary files a/examples/spacetaxi/generated/iigs/levels/level14.dat and b/examples/spacetaxi/generated/iigs/levels/level14.dat differ diff --git a/examples/spacetaxi/generated/iigs/levels/level15.dat b/examples/spacetaxi/generated/iigs/levels/level15.dat index f091239..7607e7d 100644 Binary files a/examples/spacetaxi/generated/iigs/levels/level15.dat and b/examples/spacetaxi/generated/iigs/levels/level15.dat differ diff --git a/examples/spacetaxi/generated/iigs/levels/level16.dat b/examples/spacetaxi/generated/iigs/levels/level16.dat index e6471a5..11f416d 100644 Binary files a/examples/spacetaxi/generated/iigs/levels/level16.dat and b/examples/spacetaxi/generated/iigs/levels/level16.dat differ diff --git a/examples/spacetaxi/generated/iigs/levels/level17.dat b/examples/spacetaxi/generated/iigs/levels/level17.dat index 4aa266c..457defe 100644 Binary files a/examples/spacetaxi/generated/iigs/levels/level17.dat and b/examples/spacetaxi/generated/iigs/levels/level17.dat differ diff --git a/examples/spacetaxi/generated/iigs/levels/level18.dat b/examples/spacetaxi/generated/iigs/levels/level18.dat index 2303e0d..b682e46 100644 Binary files a/examples/spacetaxi/generated/iigs/levels/level18.dat and b/examples/spacetaxi/generated/iigs/levels/level18.dat differ diff --git a/examples/spacetaxi/generated/iigs/levels/level19.dat b/examples/spacetaxi/generated/iigs/levels/level19.dat index 76c0dd3..7a90761 100644 Binary files a/examples/spacetaxi/generated/iigs/levels/level19.dat and b/examples/spacetaxi/generated/iigs/levels/level19.dat differ diff --git a/examples/spacetaxi/generated/iigs/levels/level20.dat b/examples/spacetaxi/generated/iigs/levels/level20.dat index 0c70215..396008f 100644 Binary files a/examples/spacetaxi/generated/iigs/levels/level20.dat and b/examples/spacetaxi/generated/iigs/levels/level20.dat differ diff --git a/examples/spacetaxi/generated/iigs/levels/level21.dat b/examples/spacetaxi/generated/iigs/levels/level21.dat index cd178e5..7d6c4c9 100644 Binary files a/examples/spacetaxi/generated/iigs/levels/level21.dat and b/examples/spacetaxi/generated/iigs/levels/level21.dat differ diff --git a/examples/spacetaxi/generated/iigs/levels/level22.dat b/examples/spacetaxi/generated/iigs/levels/level22.dat index 89df634..75c4d17 100644 Binary files a/examples/spacetaxi/generated/iigs/levels/level22.dat and b/examples/spacetaxi/generated/iigs/levels/level22.dat differ diff --git a/examples/spacetaxi/generated/iigs/levels/level23.dat b/examples/spacetaxi/generated/iigs/levels/level23.dat index ccf8623..23b1bec 100644 Binary files a/examples/spacetaxi/generated/iigs/levels/level23.dat and b/examples/spacetaxi/generated/iigs/levels/level23.dat differ diff --git a/examples/spacetaxi/generated/iigs/levels/level24.dat b/examples/spacetaxi/generated/iigs/levels/level24.dat index a499a94..13dca31 100644 Binary files a/examples/spacetaxi/generated/iigs/levels/level24.dat and b/examples/spacetaxi/generated/iigs/levels/level24.dat differ diff --git a/examples/spacetaxi/generated/iigs/levels/title.dat b/examples/spacetaxi/generated/iigs/levels/title.dat index 9887b56..83ca545 100644 Binary files a/examples/spacetaxi/generated/iigs/levels/title.dat and b/examples/spacetaxi/generated/iigs/levels/title.dat differ diff --git a/examples/spacetaxi/spacetaxi.c b/examples/spacetaxi/spacetaxi.c index 2f5e80b..4c16ef3 100644 --- a/examples/spacetaxi/spacetaxi.c +++ b/examples/spacetaxi/spacetaxi.c @@ -1,569 +1,698 @@ -// Space Taxi (JoeyLib port) -- main loop and game state machine. +// Space Taxi (JoeyLib port) -- front-end flow and frame pacing. // // Build: see make/{dos,amiga,atarist,iigs}.mk -- target is // `EXAMPLE=spacetaxi`. // -// Runtime layout: -// 1. jlInit + jlStageGet -// 2. stRenderInit loads tile and sprite asset banks from disk -// 3. State machine: title -> level-intro -> playing -> done -// 4. Each frame: -// - jlInputPoll (read joystick + keyboard) -// - stEngineTick (taxi physics) -// - stPassengerTick (passenger AI) -// - stRenderFrame (sprite save/restore + draw + HUD) -// - jlAudioFrameTick -// - jlWaitVBL +// The game runs in the host-independent simulation (stSim.c and +// friends); this file is the C64's outer flow around it: the title +// with its intro, the attract-demo rotation, the two GAME VARIATIONS +// menus, the level-name intro screen, the players' turns, game over +// and the return to the title ($5EC4 / $48AD / $4741 / $5010 / $61FB). +// +// Pacing: the C64 runs one game tick per two video frames (its main +// loop waits on the raster twice), 30 Hz on NTSC. Every port steps the +// simulation from its own vertical blank count: exactly one tick per +// two frames on 60 Hz and 50 Hz displays (the latter is the PAL C64's +// own cadence), and three ticks per seven frames on the 70 Hz VGA +// mode, so motion is a steady multiple of the display and never +// keyed to a wall clock. -#include #include #include "spacetaxi.h" #include "stDemoStreams.h" -// The C64 runs its physics + game logic once per GAME TICK, and a game -// tick is 2 video frames (the main loop waits on the raster twice per -// iteration, $5F94 + $5FA3) = ~30 Hz NTSC. The port renders at each -// platform's own frame rate (70 Hz DOS, 50 Hz ST/Amiga, ~15 fps IIgs), -// so gameplay is stepped on a fixed 33 ms accumulator rather than once -// per rendered frame. Without this the cab accelerates ~2.3x too fast -// on DOS and half-speed on the IIgs -- every per-tick constant (accel -// 14, gravity 1, the $7D8F templates, the demo RLE counts) is authored -// for this 30 Hz cadence, so only stepping at 30 Hz reproduces the -// original feel, identically on every port. -#define ST_GAME_TICK_MS 33u -// Cap catch-up ticks per rendered frame so a hitch (disk stall, cold -// cache) drops backlog instead of spiraling; ~165 ms simulated max. -#define ST_MAX_CATCHUP_TICKS 5u +// Diagnostics. On the IIgs the logger drags in vsnprintf and an 8 KB +// ring buffer that the bank-0 BSS budget cannot spare, and the game has +// no need of them there, so logging compiles to nothing on the 65816 +// and stays on the other ports. +#if defined(__W65816__) + #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_MAX_CATCHUP_TICKS 4u +#define ST_LEVEL_COUNT 24u +// "GET READY" shows over the old screen while the C64 cycles the +// leaving-passenger sprites (three busy waits, about half a second). +#define ST_GET_READY_TICKS 15u +// $5FBB: the GAME OVER pause is six full busy waits, about two seconds. +#define ST_GAME_OVER_TICKS 60u +// Menu joystick repeat delay ($4101 with X = $82). +#define ST_MENU_REPEAT_TICKS 8u -static void applyInput(StGameT *game); -static void demoEnter(StGameT *game); -static void demoExitToTitle(StGameT *game); -static void demoTick(StGameT *game); -static void demoTickInput(StGameT *game); -static bool loadLevelByIndex(StGameT *game, uint8_t idx); -static void playingTick(StGameT *game); +typedef enum { + ST_STATE_TITLE = 0, + ST_STATE_HIGHSCORES, + ST_STATE_ABOUT, + ST_STATE_CABBIES, + ST_STATE_SHIFT, + ST_STATE_GET_READY, + ST_STATE_INTRO, + ST_STATE_PLAYING, + ST_STATE_GAME_OVER +} StStateE; +typedef struct { + StStateE state; + bool demo; // the attract demo owns PLAYING + uint8_t demoRotation; // $5143 + uint8_t variation; // $7221: 1..5 + uint8_t usedLevels[ST_LEVEL_COUNT]; // $5254 (random shift) + uint8_t pendingLevel; // $716B as an index + uint16_t waitTicks; + uint8_t menuSel; // cabbies 0..3 / shift 0..4 + uint8_t menuRepeat; + bool fireArmed; // $445D: FIRE must be released first + uint8_t lastFrame; + uint16_t paceAcc; +} StGameT; -// All 24 canonical Space Taxi levels (A..X), extracted from the C64 -// ROM via stuff/spacetaxi/romToLevel.py. level01 = scene 0 = "A", -// level24 = scene 23 = "X". -static const char *gLevelPaths[] = { - "levels/level01.dat", "levels/level02.dat", - "levels/level03.dat", "levels/level04.dat", - "levels/level05.dat", "levels/level06.dat", - "levels/level07.dat", "levels/level08.dat", - "levels/level09.dat", "levels/level10.dat", - "levels/level11.dat", "levels/level12.dat", - "levels/level13.dat", "levels/level14.dat", - "levels/level15.dat", "levels/level16.dat", - "levels/level17.dat", "levels/level18.dat", - "levels/level19.dat", "levels/level20.dat", - "levels/level21.dat", "levels/level22.dat", - "levels/level23.dat", "levels/level24.dat", +// Menu texts ($5347..$545D) in screen codes. +static const uint8_t kTextGameVariations[] = "GAME VARIATIONS"; +static const uint8_t kTextCabbies[] = "SELECT NUMBER OF CABBIES:"; +static const uint8_t kTextDigits[] = "1 2 3 4"; +static const uint8_t kTextLeftRight[] = "USE JOYSTICK: LEFT OR RIGHT,"; +static const uint8_t kTextFire[] = "FIRE TO SELECT"; +static const uint8_t kTextSelectShift[] = "SELECT SHIFT:"; +static const uint8_t *kTextShifts[5] = { + (const uint8_t *)"1. MORNING SHIFT - BEGINNER", + (const uint8_t *)"2. DAY SHIFT - INTERMEDIATE", + (const uint8_t *)"3. NIGHT SHIFT - EXPERT", + (const uint8_t *)"4. STANDARD 24 HOUR SHIFT", + (const uint8_t *)"5. RANDOM 24 HOUR SHIFT", }; +static const uint8_t kTextUpDown[] = "USE JOYSTICK: UP OR DOWN,"; +static const uint8_t kTextGetReady[] = " GET READY FOR A RIDE "; +static const uint8_t kTextOnTheTaxi[] = " ON THE SPACE TAXI! "; +static const uint8_t kTextGameOver[] = " GAME OVER! "; +static const uint8_t kTextBlank12[] = " "; +// $4C1F / $556A screens. +static const uint8_t kTextImmortal[] = "THE IMMORTAL CABBIES"; +static const uint8_t *kHighScores[8] = { + (const uint8_t *)"3877.56 MICHAEL PLATE", + (const uint8_t *)"1809.51 MICHAEL PLATE", + (const uint8_t *)"1179.87 ANDREAS PLATE", + (const uint8_t *)" 892.65 MICHAEL PLATE", + (const uint8_t *)" 685.37 ANDREAS PLATE", + (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] = { + (const uint8_t *)"THIS PROGRAM DEVELOPED AND WRITTEN BY", + (const uint8_t *)"JOHN F. KUTCHER", + (const uint8_t *)"11/21/65", + (const uint8_t *)"CURRENTLY, AS OF JANUARY 1984,", + (const uint8_t *)"HE IS ATTENDING JOHNS HOPKINS", + (const uint8_t *)"UNIVERSITY IN BALTIMORE, MD", + (const uint8_t *)"ALSO TRY RESCUE SQUAD BY JOHN KUTCHER", +}; +static const uint8_t kTextReturn[] = "PRESS FIRE TO RETURN"; -#define ST_LEVEL_COUNT (sizeof(gLevelPaths) / sizeof(gLevelPaths[0])) +static StGameT gGame; +static StSimT gSim; +// One level buffer serves both gameplay and the title screen: the title +// is another scene, and gameplay never needs it at the same time, so it +// is loaded into gLevel on each return to the title. (Keeping a second +// StLevelT cost ~3.7 KB of static state the IIgs bank-0 BSS can't spare.) +static StLevelT gLevel; -// Attract-demo rotation cursor (mirrors $5143). Playback is stepped by -// the main loop's fixed game-tick accumulator, so no wall-clock here. -static uint8_t gDemoRotation; +static void blankScreen(StSimT *sim); +static void enterTitle(void); +static void gameOverEnter(void); +static bool joyFire(void); +static uint8_t joyMask(void); +static bool loadLevel(uint8_t index); +static uint8_t nextLevelForShift(void); +static uint8_t paceTicks(void); +static void runCabbiesMenu(uint8_t ticks); +static void runDemoEnd(void); +static void runIntro(uint8_t ticks); +static void runPlaying(uint8_t ticks); +static void runShiftMenu(uint8_t ticks); +static void runTitle(uint8_t ticks); +static void shiftMenuColor(uint8_t row, uint8_t color); +static void showCabbiesMenu(void); +static void showShiftMenu(void); +static void startDemoScreen(void); +static void startIntro(void); +static void startNewScreen(void); -// Enter (or advance to the next screen of) the rolling attract demo: -// pick the rotation slot, load its level, seed the recorded stream -// ($6262 seeds mask/timer from the head), zero the demo score. -static void demoEnter(StGameT *game) { - const StDemoEntryT *e = &kDemoRotation[gDemoRotation & 3u]; - - gDemoRotation++; - if (!loadLevelByIndex(game, e->levelIndex)) { - demoExitToTitle(game); - return; - } - stEngineReset(game); - game->score = 0u; - game->demoStream = e->stream; - game->demoLen = e->length; - game->demoMask = e->stream[0]; - game->demoTimer = e->stream[1]; - game->demoOff = 2u; - game->demoEnded = false; - game->state = ST_STATE_DEMO; +// $40CA -- spaces everywhere, black. +static void blankScreen(StSimT *sim) { + memset(sim->screen, ST_CHAR_SPACE, ST_SCREEN_CELLS); + memset(sim->color, 0, ST_SCREEN_CELLS); + stSimDirtyAll(sim); + sim->bgColor = 0u; + sim->borderColor = 0u; + sim->frame.enableMask = 0u; + stRenderSceneChanged(sim); } -static void demoExitToTitle(StGameT *game) { - if (stLevelLoad(&game->level, "levels/title.dat")) { - stRenderLevelChanged(); +// $477A -- back to the title, intro from the top. +static void enterTitle(void) { + gSim.demoMode = 0u; + if (!stLevelLoad(&gLevel, "levels/title.dat")) { + ST_LOG("spacetaxi: ! title load FAILED"); } - stRenderTitleReset(); // replay the intro -> attract cycle - stAudioSfxThrust(false); - game->state = ST_STATE_TITLE; + stTitleEnter(&gSim, &gLevel); + stRenderSceneChanged(&gSim); + stAudioNoise(false); + stAudioSilence(); + gGame.state = ST_STATE_TITLE; + gGame.fireArmed = false; } -// Recorded-input playback, substituting for applyInput ($48F2): DEC -// the pair timer each game tick; at zero take the next [mask][count] -// pair. Mask bits 0-3 = UP/DOWN/LEFT/RIGHT (bit 7 is always set in -// the recordings, no fire bit -- the demo cab never lowers its gear, -// which is why every recorded ride ends in a crash). -static void demoTickInput(StGameT *game) { - StTaxiT *t = &game->taxi; - uint8_t mask; - bool up; - bool down; - bool left; - bool right; - - // Advance the RLE stream by exactly one game tick (the caller runs - // this once per fixed 33 ms step, in lockstep with stEngineTick). - if (game->demoTimer > 0u) { - game->demoTimer--; - } - if (game->demoTimer == 0u) { - if ((uint16_t)(game->demoOff + 1u) < game->demoLen) { - game->demoMask = game->demoStream[game->demoOff]; - game->demoTimer = game->demoStream[game->demoOff + 1u]; - game->demoOff = (uint16_t)(game->demoOff + 2u); - } else { - // The C64 has no end-of-stream check (every recorded ride - // crashes first); hold idle until then. - game->demoMask = 0x80u; - game->demoTimer = 0xFFu; - } - } - mask = game->demoMask; - up = (mask & 0x01u) != 0u; - down = (mask & 0x02u) != 0u; - left = (mask & 0x04u) != 0u; - right = (mask & 0x08u) != 0u; - t->thrustDx = (int8_t)((right ? 1 : 0) - (left ? 1 : 0)); - t->thrustDy = (int8_t)((down ? 1 : 0) - (up ? 1 : 0)); - t->thrusting = (up || down || left || right); - t->dirMask = (uint8_t)((up ? 1u : 0u) | (down ? 2u : 0u) | - (left ? 4u : 0u) | (right ? 8u : 0u)); - if (left) { - t->facing = ST_DIR_LEFT; - } else if (right) { - t->facing = ST_DIR_RIGHT; - } +// $5FBB -- "GAME OVER!" over the play field, sprites hidden. +static void gameOverEnter(void) { + stSimDrawText(&gSim, 14u, 10u, kTextBlank12, 1u); + stSimDrawText(&gSim, 14u, 11u, kTextGameOver, 1u); + stSimDrawText(&gSim, 14u, 12u, kTextBlank12, 1u); + gSim.frame.enableMask = 0u; + stAudioNoise(false); + stAudioSilence(); + gGame.waitTicks = ST_GAME_OVER_TICKS; + gGame.state = ST_STATE_GAME_OVER; } -// One fixed game-tick of the attract demo: recorded input -> physics -> -// passenger. The main loop runs this in lockstep with the 30 Hz clock. -static void demoTick(StGameT *game) { - demoTickInput(game); - stEngineTick(game); - stPassengerTick(game); +static bool joyFire(void) { + return jlKeyDown(KEY_SPACE) || jlJoyDown(JOYSTICK_0, JOY_BUTTON_0); } -// One fixed game-tick of PLAYING: warp recede, or input -> physics -> -// passenger, with the land-SFX edge and the transporter-exit trigger. -// The thrust SFX is left to the caller (once per rendered frame). -static void playingTick(StGameT *game) { - bool wasLanded; +// The C64's EOR'd $DC00: bit0 UP, 1 DOWN, 2 LEFT, 3 RIGHT, 4 FIRE, bit 7 set. +static uint8_t joyMask(void) { + int8_t jx = jlJoystickX(JOYSTICK_0); + int8_t jy = jlJoystickY(JOYSTICK_0); + uint8_t mask = 0x80u; - if (game->taxi.warpFrame > 0u) { - if (stEngineWarpTick(game)) { - stPassengerTransporterExit(game); - game->state = ST_STATE_LEVEL_DONE; - } - return; + if (jlKeyDown(KEY_UP) || jy < -32) { + mask |= 0x01u; } - wasLanded = game->taxi.landed; - applyInput(game); - stEngineTick(game); - stPassengerTick(game); - if (!wasLanded && game->taxi.landed) { - stAudioSfxLand(); + if (jlKeyDown(KEY_DOWN) || jy > 32) { + mask |= 0x02u; } - // Flying up through the top-wall transporter exits the screen: - // begin the shrink-warp (delivers a carried "UP PLEASE" fare and - // advances the screen when it ends). - if (game->taxi.crashTicks == 0u && stEngineInTransporter(game)) { - stEngineStartWarp(game); + if (jlKeyDown(KEY_LEFT) || jx < -32) { + mask |= 0x04u; } + if (jlKeyDown(KEY_RIGHT) || jx > 32) { + mask |= 0x08u; + } + if (joyFire()) { + mask |= 0x10u; + } + return mask; } -static void applyInput(StGameT *game) { - StTaxiT *t = &game->taxi; - int8_t jx; - int8_t jy; - bool up; - bool down; - bool left; - bool right; +static bool loadLevel(uint8_t index) { + char path[32]; - // Joystick port 2 is the canonical Space Taxi control. Keyboard - // is the fallback so the same binary is testable on hosts without - // a stick. jlJoystickX/Y return -127..127; treat anything past - // 1/4 deflection as "held in that direction". - jx = jlJoystickX(JOYSTICK_0); - jy = jlJoystickY(JOYSTICK_0); - up = jlKeyDown(KEY_UP) || jy < -32; - down = jlKeyDown(KEY_DOWN) || jy > 32; - left = jlKeyDown(KEY_LEFT) || jx < -32; - right = jlKeyDown(KEY_RIGHT) || jx > 32; - - // FIRE is the LANDING GEAR ($63DD): a press EDGE while airborne - // toggles the gear with an SFX; pressing UP while parked on a pad - // is the takeoff, which retracts the gear ($65AD AND #$FE). (The - // old comment here claiming "fire is never tested in this path" - // was wrong -- $63F1-$6401 tests it every tick.) + if (index >= ST_LEVEL_COUNT) { + return false; + } + // Hand-rolled "levels/levelNN.dat" (NN = index+1, 01..24) so the + // binary does not drag in the whole formatted-output machinery. { - bool fire = jlKeyDown(KEY_SPACE) || jlJoyDown(JOYSTICK_0, JOY_BUTTON_0); - if (fire && !t->fireHeld && !t->landed) { - t->gearDown = !t->gearDown; - stAudioSfxGear(); - } - t->fireHeld = fire; + uint8_t n = (uint8_t)(index + 1u); + memcpy(path, "levels/level", 12u); + path[12] = (char)('0' + n / 10u); + path[13] = (char)('0' + n % 10u); + memcpy(path + 14, ".dat", 5u); } - if (up && t->landed) { - t->gearDown = false; // takeoff retracts the gear - } - // Gear down strips lateral thrust BEFORE the direction mask is - // built ($6190 AND #$13 runs upstream of the $60D6 mask store), - // so the flame table never sees L/R with the gear down either. - if (t->gearDown) { - left = false; - right = false; - } - t->thrustDx = (int8_t)((right ? 1 : 0) - (left ? 1 : 0)); - t->thrustDy = (int8_t)((down ? 1 : 0) - (up ? 1 : 0)); - t->thrusting = (up || down || left || right); - // $716A-style mask for the flame cel table (1=UP 2=DOWN 4=LEFT - // 8=RIGHT, same encoding kFlameCelByDirMask indexes). - t->dirMask = (uint8_t)((up ? 1u : 0u) | (down ? 2u : 0u) | - (left ? 4u : 0u) | (right ? 8u : 0u)); - - // Facing is purely cosmetic (sprite cel selection). Update when - // horizontal thrust is commanded; keep last facing otherwise so - // a stopped cab stays facing where it was. - if (left) { - t->facing = ST_DIR_LEFT; - } else if (right) { - t->facing = ST_DIR_RIGHT; - } -} - - -static bool loadLevelByIndex(StGameT *game, uint8_t idx) { - if (idx >= ST_LEVEL_COUNT) { + if (!stLevelLoad(&gLevel, path)) { + ST_LOG("spacetaxi: ! cannot load %s", path); return false; } - if (!stLevelLoad(&game->level, gLevelPaths[idx])) { - return false; - } - game->levelIndex = idx; - stRenderLevelChanged(); return true; } +// $5010 -- which screen comes next for the chosen shift. Returns 0xFF +// when the shift is over. +static uint8_t nextLevelForShift(void) { + StSimT *sim = &gSim; + + if (gGame.variation <= 3u) { + // $517D: eight screens per shift, A..H / I..P / Q..X. + if (sim->levelState == 8u) { + return 0xFFu; + } + return (uint8_t)((gGame.variation - 1u) * 8u + sim->levelState); + } + if (sim->levelState >= ST_LEVEL_COUNT) { + return 0xFFu; + } + if (gGame.variation == 4u) { + return sim->levelState; + } + // $521A: random order without repeats. + { + uint8_t x = (uint8_t)(stSimRng(sim, ST_LEVEL_COUNT) - 1u); + while (gGame.usedLevels[x] != 0u) { + x = (x == 0u) ? (uint8_t)(ST_LEVEL_COUNT - 1u) : (uint8_t)(x - 1u); + } + gGame.usedLevels[x] = 1u; + return x; + } +} + + +// Game ticks due this rendered frame, from the vertical blank count: +// one tick per two frames on 50/60 Hz displays, three per seven on +// the 70 Hz VGA mode. jlFrameCount is 8-bit-wrapped so a stall of any +// length just clamps to the catch-up cap. +static uint8_t paceTicks(void) { + uint16_t hz = jlFrameHz(); + uint8_t now = (uint8_t)jlFrameCount(); + uint8_t frames = (uint8_t)(now - gGame.lastFrame); + uint8_t ticks = 0u; + + gGame.lastFrame = now; + if (frames > 16u) { + frames = 16u; + } + if (hz == 50u || hz == 60u) { + gGame.paceAcc += (uint16_t)(frames * 35u); + hz = 70u; + } else { + gGame.paceAcc += (uint16_t)(frames * ST_TICK_HZ); + } + // Ticks not run this frame stay owed (a slow frame catches up over + // the next ones); a stall longer than the cap's worth is forgiven. + while (gGame.paceAcc >= hz && ticks < ST_MAX_CATCHUP_TICKS) { + gGame.paceAcc = (uint16_t)(gGame.paceAcc - hz); + ticks++; + } + if (gGame.paceAcc > (uint16_t)(hz * ST_MAX_CATCHUP_TICKS)) { + gGame.paceAcc = (uint16_t)(hz * ST_MAX_CATCHUP_TICKS); + } + return ticks; +} + + +// $52F7 -- LEFT/RIGHT cycle 1..4, FIRE confirms. +static void runCabbiesMenu(uint8_t ticks) { + uint8_t mask = joyMask(); + + if (!gGame.fireArmed) { + if ((mask & 0x10u) == 0u) { + gGame.fireArmed = true; + } + return; + } + if ((mask & 0x10u) != 0u) { + stSimPutColor(&gSim, (uint8_t)(28u + gGame.menuSel * 2u), 2u, 1u); + gSim.playerCount = (uint8_t)(gGame.menuSel + 1u); + showShiftMenu(); + return; + } + if (gGame.menuRepeat > 0u) { + gGame.menuRepeat = (uint8_t)(gGame.menuRepeat - ((ticks < gGame.menuRepeat) ? ticks : gGame.menuRepeat)); + return; + } + if ((mask & 0x0Cu) != 0u) { + stSimPutColor(&gSim, (uint8_t)(28u + gGame.menuSel * 2u), 2u, 6u); + if ((mask & 0x08u) != 0u) { + gGame.menuSel = (uint8_t)((gGame.menuSel + 1u) & 3u); + } else { + gGame.menuSel = (uint8_t)((gGame.menuSel - 1u) & 3u); + } + stSimPutColor(&gSim, (uint8_t)(28u + gGame.menuSel * 2u), 2u, 3u); + gGame.menuRepeat = ST_MENU_REPEAT_TICKS; + } +} + + +// The demo crashed, flew out or was interrupted: $5EC4 -> title. +static void runDemoEnd(void) { + gGame.demo = false; + enterTitle(); +} + + +// $4666 loop: two frames per game tick; joystick ends a demo. +static void runIntro(uint8_t ticks) { + uint8_t k; + + if (gGame.demo && (joyMask() & 0x1Fu) != 0u) { + runDemoEnd(); + return; + } + for (k = 0u; k < (uint8_t)(ticks * 2u); k++) { + if (stIntroStep(&gSim)) { + stSimEnterLevel(&gSim, &gLevel); + stRenderSceneChanged(&gSim); + if (gGame.demo) { + const StDemoEntryT *e = &kDemoRotation[(uint8_t)(gGame.demoRotation - 1u) & 3u]; + stSimSetDemoStream(&gSim, e->stream, e->length); + } + gGame.state = ST_STATE_PLAYING; + return; + } + } +} + + +// The main loop proper, one $5F40 iteration per tick. +static void runPlaying(uint8_t ticks) { + uint8_t k; + + gSim.rawInput = joyMask(); + for (k = 0u; k < ticks; k++) { + StTickResultE r = stSimTick(&gSim); + stAudioTick(); + if (r == ST_TICK_CONTINUE) { + continue; + } + if (gGame.demo) { + runDemoEnd(); + return; + } + switch (r) { + case ST_TICK_LEVEL_EXIT: + startNewScreen(); + return; + case ST_TICK_LIFE_LOST: + stSimRespawn(&gSim); + stRenderSceneChanged(&gSim); + break; + case ST_TICK_PLAYER_OUT: + gameOverEnter(); + return; + default: + break; + } + } +} + + +// $54F8 -- UP/DOWN pick the shift, FIRE confirms and starts the game. +static void runShiftMenu(uint8_t ticks) { + uint8_t mask = joyMask(); + + if (!gGame.fireArmed) { + if ((mask & 0x10u) == 0u) { + gGame.fireArmed = true; + } + return; + } + if ((mask & 0x10u) != 0u) { + shiftMenuColor(gGame.menuSel, 1u); + gGame.variation = (uint8_t)(gGame.menuSel + 1u); + // $5EC4 + $5333: the game starts; the first advance wraps to + // player 0 and loads the first screen. + stSimNewGame(&gSim, gSim.playerCount, false); + gSim.player = (uint8_t)(gSim.playerCount - 1u); + memset(gGame.usedLevels, 0, sizeof(gGame.usedLevels)); + gGame.demo = false; + startNewScreen(); + return; + } + if (gGame.menuRepeat > 0u) { + gGame.menuRepeat = (uint8_t)(gGame.menuRepeat - ((ticks < gGame.menuRepeat) ? ticks : gGame.menuRepeat)); + return; + } + if ((mask & 0x03u) != 0u) { + shiftMenuColor(gGame.menuSel, 6u); + if ((mask & 0x02u) != 0u) { + gGame.menuSel = (uint8_t)((gGame.menuSel + 1u) % 5u); + } else { + gGame.menuSel = (uint8_t)((gGame.menuSel + 4u) % 5u); + } + shiftMenuColor(gGame.menuSel, 3u); + gGame.menuRepeat = ST_MENU_REPEAT_TICKS; + } +} + + +// $47E4 -- the title loop: intro, then FIRE / UP / DOWN, else the demo. +static void runTitle(uint8_t ticks) { + uint8_t mask = joyMask(); + uint8_t k; + + if (!gGame.fireArmed) { + if ((mask & 0x10u) == 0u) { + gGame.fireArmed = true; + } + } + for (k = 0u; k < ticks; k++) { + if (stTitleTick(&gSim)) { + // $4818: intro over -> the attract demo (demo mode on). + startDemoScreen(); + return; + } + } + if ((mask & 0x10u) != 0u && gGame.fireArmed) { + showCabbiesMenu(); + return; + } + if ((mask & 0x01u) != 0u) { + uint8_t i; + 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.fireArmed = false; + return; + } + if ((mask & 0x02u) != 0u) { + static const uint8_t kAboutCols[7] = { 1u, 12u, 15u, 4u, 5u, 6u, 1u }; + static const uint8_t kAboutRows[7] = { 3u, 5u, 7u, 10u, 12u, 14u, 18u }; + uint8_t i; + blankScreen(&gSim); + for (i = 0u; i < 7u; i++) { + stSimDrawText(&gSim, kAboutCols[i], kAboutRows[i], kAboutLines[i], 1u); + } + stSimDrawText(&gSim, 10u, 22u, kTextReturn, 1u); + gGame.state = ST_STATE_ABOUT; + gGame.fireArmed = false; + } +} + + +// $554F -- colour one shift line (row 10 + 2*sel, 35 cells from col 2). +static void shiftMenuColor(uint8_t sel, uint8_t color) { + uint8_t col; + + for (col = 2u; col < 37u; col++) { + stSimPutColor(&gSim, col, (uint8_t)(10u + sel * 2u), color); + } +} + + +// $5295 -- GAME VARIATIONS page 1. +static void showCabbiesMenu(void) { + blankScreen(&gSim); + stSimDrawText(&gSim, 13u, 0u, kTextGameVariations, 5u); + stSimDrawText(&gSim, 1u, 2u, kTextCabbies, 7u); + stSimDrawText(&gSim, 5u, 4u, kTextLeftRight, 11u); + stSimDrawText(&gSim, 19u, 5u, kTextFire, 11u); + stSimDrawText(&gSim, 28u, 2u, kTextDigits, 6u); + gGame.menuSel = 0u; + stSimPutColor(&gSim, 28u, 2u, 3u); + gGame.state = ST_STATE_CABBIES; + gGame.fireArmed = false; + gGame.menuRepeat = 0u; +} + + +// $545E -- GAME VARIATIONS page 2. +static void showShiftMenu(void) { + uint8_t i; + + blankScreen(&gSim); + stSimDrawText(&gSim, 1u, 8u, kTextSelectShift, 7u); + for (i = 0u; i < 5u; i++) { + stSimDrawText(&gSim, 2u, (uint8_t)(10u + i * 2u), kTextShifts[i], 6u); + } + stSimDrawText(&gSim, 5u, 21u, kTextUpDown, 11u); + stSimDrawText(&gSim, 19u, 22u, kTextFire, 11u); + gGame.menuSel = 0u; + shiftMenuColor(0u, 3u); + gGame.state = ST_STATE_SHIFT; + gGame.fireArmed = false; + gGame.menuRepeat = 0u; +} + + +// $48AD + $61FB + $50C6 -- the next demo screen: "GET READY" over the +// title, then the level intro. +static void startDemoScreen(void) { + const StDemoEntryT *e = &kDemoRotation[gGame.demoRotation & 3u]; + + gGame.demoRotation++; + gGame.demo = true; + stSimNewGame(&gSim, 1u, true); + gSim.player = 0u; + (void)stSimAdvancePlayer(&gSim); + gSim.levelState++; + if (!loadLevel(e->levelIndex)) { + enterTitle(); + return; + } + stSimDrawText(&gSim, 10u, 8u, kTextGetReady, 5u); + stSimDrawText(&gSim, 10u, 10u, kTextOnTheTaxi, 5u); + gSim.frame.enableMask &= 0x07u; + gGame.waitTicks = ST_GET_READY_TICKS; + gGame.state = ST_STATE_GET_READY; +} + + +// $62F0 -> $4523 -- the level-name screen for gLevel. +static void startIntro(void) { + stIntroEnter(&gSim, &gLevel); + stRenderSceneChanged(&gSim); + gGame.state = ST_STATE_INTRO; +} + + +// $61FB -- the next player's turn on this screen, or the next screen +// once everybody has had it; players out of cabs are skipped. +static void startNewScreen(void) { + uint8_t guard = 0u; + + for (;;) { + if (stSimAdvancePlayer(&gSim)) { + uint8_t next = nextLevelForShift(); + if (next == 0xFFu) { + // $602F: the shift is over. + enterTitle(); + return; + } + gGame.pendingLevel = next; + if (!loadLevel(next)) { + enterTitle(); + return; + } + gSim.levelState++; + } + if (gSim.cabs[gSim.player] != 0u) { + break; + } + guard++; + if (guard > (uint8_t)(ST_MAX_PLAYERS * 2u)) { + enterTitle(); + return; + } + } + startIntro(); +} + + int main(void) { - jlConfigT config; - jlSurfaceT *stage; - // game embeds a full StLevelT (tilemap + colormap = ~2 KB). As a - // stack local it held ~2 KB across the whole program, overflowing the - // IIgs (llvm-mos) soft stack once the deep asset-load call chain ran - // (stRenderInit -> loadSpriteSheet -> jlSpriteBankLoad -> fopen) and - // silently crashing back to GS/OS. It is a single program-lifetime - // instance, so static (BSS) is both correct and stack-friendly. - static StGameT game; + jlConfigT config; + jlSurfaceT *stage; - // Sprite codegen arena: after Phase 11 (shared-walker rewrite of - // the planar sprite path), sprites no longer consume arena bytes - // -- the per-cel work happens inside halSpriteDrawPlanes / - // halSpriteSavePlanes / halSpriteRestorePlanes as static lib code. - // 32 KB is plenty for whatever other codegen needs are around. - // Sprite codegen: the 26 drawn cels (taxi/warp/passenger/flame, - // 24x24 = 3x3-tile) each compile to per-shift save/draw/restore asm. - // Compiling them is what makes sprite blitting fast (the interpreter - // costs ~200 ms/frame on the IIgs 65816). The arena is capped at one - // 64 KB bank (codegenArena attrNoCross); 60 KB fits all 26 cels with - // headroom. The old 32 KB was sized for procedural sprites only. +#if defined(JOEYLIB_PLATFORM_IIGS) + // One 64 KB bank: enough for the cab, exhaust and passenger cels. config.codegenBytes = 60UL * 1024; - config.audioBytes = 32UL * 1024; // music + SFX - +#else + config.codegenBytes = 160UL * 1024; +#endif + config.audioBytes = 32UL * 1024; if (!jlInit(&config)) { - fprintf(stderr, "jlInit: %s\n", jlLastError()); return 1; } - jlLogReset(); - jlLogF("spacetaxi: build=%s %s", __DATE__, __TIME__); - + ST_LOG_RESET(); + ST_LOG("spacetaxi: build=%s %s", __DATE__, __TIME__); stage = jlStageGet(); if (stage == NULL) { - jlLogF("spacetaxi: ! jlStageGet returned NULL"); jlShutdown(); return 1; } - - memset(&game, 0, sizeof(game)); - game.state = ST_STATE_TITLE; - game.lives = 5; - game.levelIndex = 0; - game.fareTarget = 1; // C64 default: 1 fare per game (see $48AF). - - jlLogF("spacetaxi: stRenderInit ..."); + memset(&gGame, 0, sizeof(gGame)); + memset(&gSim, 0, sizeof(gSim)); stRenderInit(stage); - jlLogF("spacetaxi: stAudioInit ..."); stAudioInit(); - - // Load the title-screen tilemap. raw.bin captured the C64 game - // at the title screen, so its screen RAM at $0400 IS the title - // (the big SPACE TAXI letters, JOHN F. BUTCHER credit, joystick - // instructions, etc.). romToLevel.py emits title.dat from that - // capture. Falls through to a black field if the asset is - // missing. - jlLogF("spacetaxi: stLevelLoad title.dat ..."); - if (stLevelLoad(&game.level, "levels/title.dat")) { - jlLogF("spacetaxi: title loaded, tilebank=%u", - (unsigned)game.level.tileBankId); - stRenderLevelChanged(); - } else { - jlLogF("spacetaxi: title load FAILED"); - } - jlLogFlush(); - - // Fixed-timestep gameplay clock. gTickAccumMs banks real elapsed - // time; each frame consumes it in whole 30 Hz game ticks so physics - // advance at the C64's rate regardless of the render frame rate. - uint32_t gPrevTickMs = jlMillisElapsed(); - uint32_t gTickAccumMs = 0u; + // Boot: the playback buffer as the C64 leaves it, and the state a + // fresh machine has when the first title intro starts ($4092 + the + // title's own sparkle/walk history from the dump). + stSimNewGame(&gSim, 1u, false); + stSimSeedDemoBuffer(&gSim, kDemoBufferInit, ST_DEMO_BUFFER_BYTES); + gSim.waveIdx = 3u; + stRenderPrewarm(stage, &gSim); + enterTitle(); + gGame.lastFrame = (uint8_t)jlFrameCount(); for (;;) { - uint8_t pendingTicks; - uint8_t tick; + uint8_t ticks; jlInputPoll(); if (jlKeyPressed(KEY_ESCAPE)) { break; } - - { - uint32_t nowMs = jlMillisElapsed(); - uint32_t want; - gTickAccumMs += (uint32_t)(nowMs - gPrevTickMs); - gPrevTickMs = nowMs; - want = gTickAccumMs / ST_GAME_TICK_MS; // wide: no cast wrap - if (want > ST_MAX_CATCHUP_TICKS) { - // A long stall (level load, disk seek): resync to now - // and skip the gap instead of spiraling. Zero the whole - // accumulator so no backlog survives the truncated cast. - want = ST_MAX_CATCHUP_TICKS; - gTickAccumMs = 0u; - } else { - gTickAccumMs -= want * ST_GAME_TICK_MS; // keep sub-tick remainder - } - pendingTicks = (uint8_t)want; // guaranteed <= 5 - } - - switch (game.state) { + ticks = paceTicks(); + switch (gGame.state) { case ST_STATE_TITLE: - { - // C64 title listens for UP (high scores), DOWN - // (instructions), and FIRE. FIRE on the title does NOT - // start gameplay -- it advances to the "GAME VARIATIONS" - // options menu ($5295, reached via $4750 -> JSR $5295), - // where the player selects the cabbie count before the - // game begins. (UP/DOWN high-scores/instructions screens - // are still TODO.) - { - int8_t jy = jlJoystickY(JOYSTICK_0); - if (jlKeyDown(KEY_UP) || jy < -32) { - game.state = ST_STATE_HIGHSCORES; - break; - } - if (jlKeyDown(KEY_DOWN) || jy > 32) { - game.state = ST_STATE_INSTRUCTIONS; - break; - } - } - if (jlKeyPressed(KEY_SPACE) || - jlJoyPressed(JOYSTICK_0, JOY_BUTTON_0)) { - game.fareTarget = 1u; // C64 default ($48AF sets 1) - game.state = ST_STATE_OPTIONS; - break; - } - // Advance the sparkle -> walk -> takeoff intro (and the - // logo flip) on the fixed 30 Hz clock, so pre-game - // timing is identical on every port. - for (tick = 0u; - tick < pendingTicks && game.state == ST_STATE_TITLE; - tick++) { - stRenderTitleAdvance(); - } - // Intro finished (cab took off): roll the attract - // demo, exactly like the C64's post-takeoff flow. - if (stRenderTitleIntroDone()) { - demoEnter(&game); - } + runTitle(ticks); break; - } - case ST_STATE_DEMO: - { - // Any real input exits the attract loop ($5006 tests - // $DC00 != $7F). - int8_t jx = jlJoystickX(JOYSTICK_0); - int8_t jy = jlJoystickY(JOYSTICK_0); - if (jlKeyPressed(KEY_SPACE) || - jlJoyPressed(JOYSTICK_0, JOY_BUTTON_0) || - jlKeyDown(KEY_UP) || jlKeyDown(KEY_DOWN) || - jlKeyDown(KEY_LEFT) || jlKeyDown(KEY_RIGHT) || - jx < -32 || jx > 32 || jy < -32 || jy > 32) { - demoExitToTitle(&game); - break; - } - if (game.demoEnded) { - // The recorded ride crashed out; $6B57 skips the - // life bookkeeping in demo mode -- back to title. - demoExitToTitle(&game); - break; - } - if (game.score != 0u) { - // Fare delivered: advance the rotation ($501D -> - // $50C6) without visiting the title. - demoEnter(&game); - break; - } - for (tick = 0u; - tick < pendingTicks && game.state == ST_STATE_DEMO && - !game.demoEnded && game.score == 0u; - tick++) { - demoTick(&game); - } - stAudioSfxThrust(game.taxi.thrusting); - break; - } case ST_STATE_HIGHSCORES: - case ST_STATE_INSTRUCTIONS: - // Both are title sub-screens; FIRE returns to the title. - // The screens cleared the stage, so force a full title - // tilemap repaint on the way back. - if (jlKeyPressed(KEY_SPACE) || - jlJoyPressed(JOYSTICK_0, JOY_BUTTON_0)) { - stRenderLevelChanged(); - stRenderTitleReset(); - game.state = ST_STATE_TITLE; + case ST_STATE_ABOUT: + if (!gGame.fireArmed) { + gGame.fireArmed = !joyFire(); + } else if (joyFire()) { + enterTitle(); } break; - case ST_STATE_OPTIONS: - { - // "GAME VARIATIONS" menu ($5295). LEFT/RIGHT cycle the - // cabbie count 1..4 (C64 stores 0..3 in $7213 and wraps - // with AND #$03 -- so it rolls 4->1 and 1->4); FIRE - // confirms and starts the game. LEFT/RIGHT are edge- - // triggered so one press = one step. - static bool prevLeft = false; - static bool prevRight = false; - int8_t jx = jlJoystickX(JOYSTICK_0); - bool left = jlKeyDown(KEY_LEFT) || jx < -32; - bool right = jlKeyDown(KEY_RIGHT) || jx > 32; - uint8_t sel = (uint8_t)((game.fareTarget - 1u) & 3u); - - if (right && !prevRight) { - sel = (uint8_t)((sel + 1u) & 3u); - } - if (left && !prevLeft) { - sel = (uint8_t)((sel - 1u) & 3u); - } - game.fareTarget = (uint8_t)(sel + 1u); - prevLeft = left; - prevRight = right; - - if (jlKeyPressed(KEY_SPACE) || - jlJoyPressed(JOYSTICK_0, JOY_BUTTON_0)) { - game.score = 0; - // "SELECT NUMBER OF CABBIES" ($7213) sets the number of - // taxi LIVES, not a fare quota: gFareCounter increments - // only on the crash path ($6BD6), and the game ends when - // crashes reach the cabbie count. Deliveries never end - // the game; you advance screens by flying up. - game.lives = game.fareTarget; - game.levelIndex = 0; - if (!loadLevelByIndex(&game, 0)) { - jlLogF("spacetaxi: ! cannot load level 0 (DATA/levels/level01.dat)"); - jlLogFlush(); - goto shutdown; - } - stEngineReset(&game); - game.state = ST_STATE_PLAYING; - } + case ST_STATE_CABBIES: + runCabbiesMenu(ticks); break; - } - case ST_STATE_PLAYING: - // Advance physics at the fixed 30 Hz cadence -- each - // playingTick handles the warp recede, physics, passenger, - // land SFX, and transporter-exit trigger. Stop early if a - // tick changed state (crash game-over, level done). - for (tick = 0u; - tick < pendingTicks && game.state == ST_STATE_PLAYING; - tick++) { - playingTick(&game); - } - // Continuous thrust SFX reflects the final tick's state. - stAudioSfxThrust(game.taxi.thrusting); + case ST_STATE_SHIFT: + runShiftMenu(ticks); break; - case ST_STATE_LEVEL_DONE: - // No music to stop -- gameplay is silent. The C64 plays - // a score-screen jingle (song 7 / song 6) between levels; - // not yet wired up here. - if (loadLevelByIndex(&game, (uint8_t)(game.levelIndex + 1))) { - stEngineReset(&game); - game.state = ST_STATE_PLAYING; + case ST_STATE_GET_READY: + if (gGame.demo && (joyMask() & 0x1Fu) != 0u) { + runDemoEnd(); + break; + } + if (gGame.waitTicks > ticks) { + gGame.waitTicks = (uint16_t)(gGame.waitTicks - ticks); } else { - // ran out of levels -- back to title (player wins) - if (stLevelLoad(&game.level, "levels/title.dat")) { - stRenderLevelChanged(); - } - stRenderTitleReset(); - game.state = ST_STATE_TITLE; + gGame.waitTicks = 0u; + startIntro(); } break; + case ST_STATE_INTRO: + runIntro(ticks); + break; + case ST_STATE_PLAYING: + runPlaying(ticks); + break; case ST_STATE_GAME_OVER: - if (jlKeyPressed(KEY_SPACE)) { - // Restore the title tilemap so the press-start - // overlay isn't sitting on top of the last-played - // level's art. - if (stLevelLoad(&game.level, "levels/title.dat")) { - stRenderLevelChanged(); + if (gGame.waitTicks > ticks) { + gGame.waitTicks = (uint16_t)(gGame.waitTicks - ticks); + } else { + // $6BD0: one more player done; everybody done -> title. + gSim.playersDone++; + if (gSim.playersDone == gSim.playerCount) { + enterTitle(); + } else { + startNewScreen(); } - stRenderTitleReset(); - game.state = ST_STATE_TITLE; } break; default: - game.state = ST_STATE_TITLE; + enterTitle(); break; } - - // Silence continuous thrust SFX whenever no cab is flying - // under thrust (title, menus, game-over, level-done). The - // attract DEMO is a recorded gameplay session and thrusts - // audibly, so it keeps its own per-tick thrust SFX -- without - // this exclusion the blanket silence overrode it and the demo - // cab flamed in total silence. - if (game.state != ST_STATE_PLAYING && game.state != ST_STATE_DEMO) { - stAudioSfxThrust(false); - } - - stRenderFrame(stage, &game); - stAudioFrameTick(); + stRenderFrame(stage, &gSim); jlAudioFrameTick(); - // stRenderFrame already does jlWaitVBL right before its - // jlStagePresent (sync-on-present), so an extra wait here - // would just slow the loop to half framerate. } -shutdown: - stAudioShutdown(); stRenderShutdown(); jlShutdown(); diff --git a/examples/spacetaxi/spacetaxi.h b/examples/spacetaxi/spacetaxi.h index 791f05d..07fa63f 100644 --- a/examples/spacetaxi/spacetaxi.h +++ b/examples/spacetaxi/spacetaxi.h @@ -1,337 +1,35 @@ -// Space Taxi (JoeyLib port) -- shared types and decls. +// Space Taxi (JoeyLib port) -- host-side declarations. // -// Architecture (all four JoeyLib targets): -// -// tilemap 40x25 cells, each cell = (tileIndex, paletteSlot). -// jlTilePaste blits the tile bank into the stage one cell -// at a time at level boot, then it's static until the -// scene changes. No per-frame redraw of the tilemap. -// -// taxi Single jlSpriteT with multiple cels (thrust frames, -// facing variants). Save-under + restore-under each frame -// so we don't repaint the tilemap behind it. -// -// passenger Up to 2 simultaneous sprites: one waiting on a pad, -// one already in the cab (or none). Same save/restore -// discipline as the taxi. -// -// audio One ~3-voice event stream rendered per platform: -// SB+PSG-synth (DOS), PT 4-voice (Amiga), YM2149 (ST), -// Ensoniq DOC channels (IIgs). The dispatch lives in -// stAudio.c with one entry point per voice command. -// -// input Joystick port 2 conventionally on the C64: direction = -// the four cardinals (thrust), FIRE = the LANDING GEAR -// toggle while airborne ($63DD: press edge EORs $7197 -// bit 0 with an SFX; $6462 requires gear down to land; -// $6190 strips left/right thrust while gear is down). -// Keyboard fallback: arrows + space for hosts without -// joysticks. -// -// Level data lives in a small custom .dat per level (see -// assets/levels/format.md). Tile bitmaps and sprite cels are PNG -// authored externally and baked to native .tbk / .spr blobs at -// build time via tools/assetbake/assetbake.py. +// The game itself lives in the host-independent simulation (stSim.h): +// physics, collision, the fare machine, the level hooks, the title +// intro and the level intro all run there in the C64's own units and +// are verified tick-for-tick against a VICE trace of the original. +// This header is the JoeyLib side: the renderer that paints the +// simulation's screen RAM and sprite frame, the audio sink, the +// level loader and the front-end flow in spacetaxi.c. #ifndef SPACETAXI_H #define SPACETAXI_H #include -#define ST_TILEMAP_W 40u -#define ST_TILEMAP_H 25u -#define ST_TILE_PIXELS 8u +#include "stSim.h" -// The tilemap stores raw C64 screen-RAM character codes. The empty -// (non-colliding) playfield cell is the space character, $20. Every -// other code is non-blank background -- on the C64 it latches a sprite- -// background collision ($D01F). romToLevel.py uses BG=0x20 as the empty -// marker when extracting screen RAM. -#define ST_TILE_EMPTY 0x20u +// Level loading through the JoeyLib data path (DATA/levels/...). +bool stLevelLoad(StLevelT *out, const char *path); -// Display field is 320x200 (JoeyLib's SURFACE_WIDTH x SURFACE_HEIGHT). -// 40 tiles x 8 px = 320, 25 tiles x 8 px = 200. The bottom 3 rows are -// the HUD band (score / lives / level / current-fare strip). The top -// 22 rows are the playfield where the taxi moves. -// Full C64 screen height. The original title uses all 25 rows -// (frame at 0, 12, 24 + content). For gameplay the bottom 3 rows -// are HUD territory -- HUD draws over the tilemap there. -#define ST_PLAYFIELD_ROWS 25u -#define ST_HUD_ROW 22u -#define ST_HUD_ROW_COUNT 3u +// Renderer: paints dirty screen cells with the live charset and draws +// the eight sprites of the last marshaled frame with save-under. +void stRenderInit(jlSurfaceT *stage); +void stRenderShutdown(void); +void stRenderFrame(jlSurfaceT *stage, StSimT *sim); +// Drop the sprite cache and force a full repaint (new scene). +void stRenderSceneChanged(StSimT *sim); +void stRenderPrewarm(jlSurfaceT *stage, StSimT *sim); -// Maximum stuff. Tuned for fitting the smallest target (IIgs): -// 10 pads is the highest count in any canonical Space Taxi level -// (D and M each have 10 pads; everything else <= 9) -// Exactly ONE fare exists at a time. Verified: the C64 spawns a new -// fare only while its state machine (gDeathStage) is idle, so -// waiting-vs-carrying is the single passenger's `onboard` flag -- NOT -// two passengers. (A prior "waiting + carrying = 2 sprites" note was -// a misconception / false second source of truth.) -#define ST_MAX_PADS 10u -#define ST_MAX_PASSENGERS 1u - -// Passenger destination sentinel: "UP PLEASE!" -- the fare wants to be -// flown UP through the top-wall transporter opening rather than to a -// landing pad. This is the C64's $0B destination ($6CB8), used on -// single-pad screens (and when all pads are consumed). Any real pad -// index is 0..ST_MAX_PADS-1, so 0xFE can't collide. -#define ST_DEST_TRANSPORTER 0xFEu - -// The transporter opening is a fixed 4-column gap ($67 tiles) in the top -// wall, at columns 18..21 in every canonical Space Taxi screen (verified -// across the extracted tilemaps). Flying the taxi up through it exits the -// screen. -#define ST_TRANSPORTER_COL_LO 18u -#define ST_TRANSPORTER_COL_HI 22u // exclusive -#define ST_TILE_TRANSPORTER 0x67u - -// Fixed-point taxi physics: position and velocity are int16_t in -// units of 1/16 px, so the taxi can drift fractionally and the -// thrust/gravity terms are integers without losing precision over -// the whole field. (22 rows x 8 px x 16 = 2816 < 32767 so int16 -// is fine.) -// Match the C64 fixed-point scale: 8-bit sub-pixel (256 sub-units per -// pixel) so the C64's accel=14 and gravity=1 are usable directly -// (14/256 px/frame initial accel; constant 1/256 px/frame gravity). -// Position needs int32_t since a 320-wide playfield * 256 sub-units -// overflows int16_t. -#define ST_SUBPIXEL_SHIFT 8 -#define ST_SUBPIXEL (1 << ST_SUBPIXEL_SHIFT) - -// Taxi cels are the real C64 art (extractSprites.py): the $C0/$C1 -// right-facing and $DC/$DD left-facing pairs with the $6A95 per-frame -// low-bit flicker; the engine flame is the separate direction-indexed -// sprite-2 cel set. Selection lives in stRender.c. - - -typedef enum { - ST_STATE_TITLE = 0, - ST_STATE_HIGHSCORES, // UP on title -> "THE IMMORTAL CABBIES" ($4C1F) - ST_STATE_INSTRUCTIONS, // DOWN on title -> author/about screen ($556A) - ST_STATE_OPTIONS, // "GAME VARIATIONS" menu ($5295): pick cabbie count - ST_STATE_DEMO, // rolling attract demo (recorded input playback) - ST_STATE_PLAYING, - ST_STATE_LEVEL_DONE, - ST_STATE_GAME_OVER -} StGameStateE; - - -typedef enum { - ST_DIR_RIGHT = 0, - ST_DIR_LEFT = 1 -} StFacingE; - - -typedef struct { - uint8_t tileX; // landing surface left edge (tile coord) - uint8_t tileY; // landing surface row (tile coord) - uint8_t tileW; // pad width in tiles - uint8_t standX; // passenger stand X (screen px; slot byte6-24) -} StPadT; - - -typedef struct { - char name[24]; // level display name ("UP & DOWN", etc.) - uint8_t tileBankId; // which tile asset (0 = default bank) - uint8_t musicId; // UNUSED in C64 Space Taxi -- the - // game has no per-level background - // music; gameplay is silent except - // for SFX. Title plays song 8, score - // screen plays song 6 or 7 (see - // MECHANICS.md "Sound (SID)"). Kept - // here as scaffolding for a possible - // future "level-entry jingle" event. - uint8_t bgColor; // background palette slot - uint8_t borderColor; // border palette slot (for HUD if used) - uint8_t taxiSpawnTileX; - uint8_t taxiSpawnTileY; - // Per-level physics templates. Mirror the C64 templates at - // $7D8F/$7D91 (Y/X accel) and $7D93/$7D95 (Y/X gravity). Accels - // are unsigned magnitudes; gravities are int8 so a level can pull - // upward (e.g. canonical level K = -7). Hand-authored levels can - // leave them zero; loader substitutes per-level defaults below. - uint8_t xAccel; - uint8_t yAccel; - int8_t xGrav; - int8_t yGrav; - // VIC color block from C64 $7D00-$7D08, mapped to $D020-$D028 by - // $62F0 (scene-load). borderColor/bgColor above are $7D00/$7D01. - // bgColor1/2/3 and spriteMc0/1 only matter in VIC multicolor mode - // which the JoeyLib port doesn't reproduce -- they're stored so - // the .dat format stays a faithful capture of $7D00-$7D08 but the - // runtime ignores them. sprite0Color/sprite1Color drive the cab - // and flame placeholder colors when no sprite asset is authored. - uint8_t bgColor1; // $D022 (multicolor only, unused) - uint8_t bgColor2; // $D023 (multicolor only, unused) - uint8_t bgColor3; // $D024 (multicolor only, unused) - uint8_t spriteMc0; // $D025 sprite multicolor 0 (unused) - uint8_t spriteMc1; // $D026 sprite multicolor 1 (unused) - uint8_t sprite0Color; // $D027 sprite 0 (taxi) - uint8_t sprite1Color; // $D028 sprite 1 (flame) - uint8_t padCount; - StPadT pads[ST_MAX_PADS]; - // Tilemap: tile-index per cell (row-major). - uint8_t tilemap[ST_TILEMAP_W * ST_PLAYFIELD_ROWS]; - // Palette slot per cell -- which surface palette index a cell uses. - uint8_t colormap[ST_TILEMAP_W * ST_PLAYFIELD_ROWS]; -} StLevelT; - - -typedef struct { - // 16-bit-fixed-point position (8 bits sub-pixel + 8 bits pixel), - // but stored in int32_t so a 320-wide playfield fits without - // wrap. `x >> ST_SUBPIXEL_SHIFT` is the pixel column. - int32_t x; - int32_t y; - int16_t vx; // velocity accumulator (sub-pixel/frame) - int16_t vy; - StFacingE facing; - bool thrusting; // any directional input held this frame - int8_t thrustDx; // -1 left, 0 neutral, +1 right - int8_t thrustDy; // -1 up, 0 neutral, +1 down - // Held-direction bit mask, mirroring $716A's encoding (the index - // into the $6DB0 flame cel table): 1=UP 2=DOWN 4=LEFT 8=RIGHT. - // With gear down the left/right bits are stripped before this is - // built, exactly like $6190 AND #$13 feeding $60D6. - uint8_t dirMask; - // Landing gear ($7197 bit 0). Toggled by a FIRE press edge while - // airborne, retracted on takeoff ($65B0), REQUIRED down to land - // ($6462) -- gear-up contact with a pad is a crash. - bool gearDown; - bool fireHeld; // previous-frame fire state (edge detect) - bool landed; // sitting on a pad - uint8_t onPad; // pad index if landed (0xFF if none) - // Death-animation countdown. >0 means crashed -- engine keeps the - // cab integrating under gravity (no explosion visual; the C64 - // just lets the cab fall, see VERIFIED.md "Stage 1 $665F death - // branch"). Reaches 0 -> respawn (or game-over). Mirrors the - // C64's $6B24/$6B4C phase-2/3 timing. - uint8_t crashTicks; - // Crash phase split ($6A72 fall vs $6B2A impact walk): false while - // the debris tumbles, true once it hits the floor. - bool crashImpacted; - // Transporter shrink-warp progress (0 = not warping; 1..64 = the - // takeoff/shrink animation $5C91/$5CB9). During the warp the taxi - // recedes up through the opening; the screen advances when it ends. - uint8_t warpFrame; -} StTaxiT; - - -// Passenger lifecycle phases (see the field comments in StPassengerT -// for the C64 sequences each one mirrors). -typedef enum { - ST_PASS_BEAM_IN = 0, // materialize at standX ($CB..$C7) - ST_PASS_WAIT, // stand + wave ($66D6 cycle) - ST_PASS_WALK_TO_CAB, // walk to the captured taxi X - ST_PASS_BOARD_OUT, // shrink into the cab ($C7..$CB) - ST_PASS_RIDING, // aboard, invisible - ST_PASS_DROP_IN, // delivery: materialize beside the cab - ST_PASS_WALK_TO_PAD, // walk to the destination standX - ST_PASS_DROP_OUT // shrink out -- fare complete -} StPassPhaseE; - - -typedef struct { - bool active; - bool onboard; // in the cab (true) or waiting at pad (false) - uint8_t currentPad; // where they are if waiting - uint8_t destPad; - int16_t x; // pixel position - int16_t y; - // Passenger lifecycle, per the traced sequences: materialize - // ($CB..$C7 grow-in), wave in place ($66D6 cycle -- he NEVER - // walks while waiting), walk to the landed cab ($6811 captures - // the taxi X once; $C4/$C5 leftward, $C2/$C3 rightward, +/-2 px - // per 3-tick step), shrink into the cab ($C7..$CB), ride, and - // the delivery mirror (materialize at the cab, walk to standX, - // shrink out). - StPassPhaseE phase; - int16_t targetX; // walk destination (captured once) - // Step counter within the current phase; in ST_PASS_WAIT it is - // the 0..3 wave-cycle index, in the walk phases its low bit is - // the stride cel alternation. - uint8_t waitPhase; -} StPassengerT; - - -typedef struct { - StGameStateE state; - StLevelT level; - StTaxiT taxi; - StPassengerT passengers[ST_MAX_PASSENGERS]; - uint32_t score; - uint8_t lives; - uint8_t levelIndex; - // Attract-demo playback state (ST_STATE_DEMO): the recorded RLE - // input stream and its cursor, plus the crash-finished signal the - // engine raises instead of decrementing lives ($6B57). - const uint8_t *demoStream; - uint16_t demoLen; - uint16_t demoOff; - uint8_t demoTimer; - uint8_t demoMask; - bool demoEnded; - // Player-selectable fare quota (1..4). Mirrors C64 $7213 (the - // GAME-VARIATIONS "cabbies" count), set on the title screen via - // LEFT/RIGHT joystick ($5310-$5328). This is the GLOBAL number of - // fares to deliver before the game completes -- NOT per-level. - uint8_t fareTarget; -} StGameT; - - -// Public entry points (one per source file). -bool stLevelLoad(StLevelT *out, const char *path); - -void stRenderInit(jlSurfaceT *stage); -void stRenderShutdown(void); -// Blit the level's static tile art into the stage. Called once per -// scene change; falls back to colored solid tiles per index-range -// when no tile bank asset is loaded. -void stRenderLevel(jlSurfaceT *stage, const StLevelT *level); -void stRenderFrame(jlSurfaceT *stage, const StGameT *game); -// Tell the renderer the current game.level contents changed. Required -// after stLevelLoad even when the StLevelT pointer is unchanged -- -// stRenderLevel's dirty-cache compares pointers, not contents. -void stRenderLevelChanged(void); -void stRenderTitleAdvance(void); -bool stRenderTitleIntroDone(void); -void stRenderTitleReset(void); - -// Draw an ASCII string into the stage at tile coords (bx, by) using -// the loaded font asset. No-op if the font asset failed to load. -void stRenderDrawText(jlSurfaceT *stage, uint8_t bx, uint8_t by, const char *s); - -void stEngineReset(StGameT *game); -void stEngineTick(StGameT *game); -// True when the taxi has flown up into the top-wall transporter opening -// (used to trigger a screen exit). -bool stEngineInTransporter(const StGameT *game); -// Begin the transporter shrink-warp (taxi.warpFrame 0 -> 1). -void stEngineStartWarp(StGameT *game); -// Advance the shrink-warp one frame; returns true when it completes. -bool stEngineWarpTick(StGameT *game); -// Current shrink step 0..7 (0 = full size, 7 = smallest) for rendering. -uint8_t stEngineWarpStep(const StGameT *game); - -void stPassengerReset(StGameT *game); -void stPassengerTick(StGameT *game); -// Called when the taxi exits up through the transporter: delivers a -// carried "UP PLEASE" fare (if any) before the screen advances. -void stPassengerTransporterExit(StGameT *game); - -void stAudioInit(void); -void stAudioShutdown(void); -void stAudioFrameTick(void); // call once per host frame; counts down SFX -void stAudioPlayMusic(uint8_t musicId); -void stAudioStopMusic(void); -void stAudioSfxThrust(bool on); -void stAudioSfxGear(void); -void stAudioSfxLand(void); -void stAudioSfxPickup(void); -void stAudioSfxDropoff(void); -void stAudioSfxCrash(void); - -void stHudDraw(jlSurfaceT *stage, const StGameT *game); +// Audio sink setup and the per-tick release timers ($4320). +void stAudioInit(void); +void stAudioShutdown(void); +void stAudioTick(void); #endif diff --git a/examples/spacetaxi/stAudio.c b/examples/spacetaxi/stAudio.c index 4f06c01..5ac786a 100644 --- a/examples/spacetaxi/stAudio.c +++ b/examples/spacetaxi/stAudio.c @@ -1,245 +1,186 @@ -// Space Taxi -- audio dispatch. +// Space Taxi -- audio sink for the simulation's SID events. // -// Simple 1-voice SFX engine on top of jlAudioVoice. Each SFX call -// programs voice slot 2 (conventionally SFX) with a tone + attenuation -// and arms a frame-tick countdown. When the countdown reaches 0 the -// voice is silenced. New SFX preempt any in-progress one. -// -// Continuous thrust SFX is edge-triggered: on -> arm a looping tone, -// off -> silence. The thrust voice (slot 1) is independent of the -// transient SFX voice so a pickup chirp on top of thrust doesn't -// cut the thrust note. -// -// MUSIC MODEL (important): Space Taxi gameplay has NO background -// music. The C64 only loads songs via $CB02 at non-gameplay sites: -// - title-screen setup ($459C / $46BC) -> song 8 -// - title-init via $4741 ($477C) -> song 25 -// - score-screen draw ($4C29) -> song 7 -// - score-screen variant ($4EB6) -> song 6 -// During gameplay the IRQ music engine ticks ($CC6C) but all 3 -// voices are silent because no track pointers are loaded. -// stAudioPlayMusic / stAudioStopMusic stay as stubs for the eventual -// title-screen and score-screen jingle dispatch; do not call them -// from gameplay state transitions. +// The C64 drives three SID voices: voice 1 carries the SFX programs +// (landing, gear, crash, fuel) and the thrust/crash frequency sweep, +// voice 2 the level gimmick tones (laser hum, puzzle chime), voice 3 +// the jet noise. JoeyLib's audio HAL is a PSG-style tone/noise +// interface, so each SID program is mapped to its pitch and release +// time; envelopes and waveforms are approximated with an attenuation +// ramp. The speech samples of the original are not extracted; each +// spoken character becomes a short chirp so the cues still land. #include "spacetaxi.h" -#define ST_SFX_VOICE 2u -#define ST_THRUST_VOICE 1u +// SID voice index in a program's byte 8 -> JoeyLib voice slot. +#define ST_VOICE_SFX 1u // SID voice 1 +#define ST_VOICE_TONE 2u // SID voice 2 +#define ST_VOICE_NOISE 3u // SID voice 3 (noise) -#define ST_SFX_PICKUP_HZ 720u -#define ST_SFX_PICKUP_TICKS 20u -#define ST_SFX_DROPOFF_HZ 1100u -#define ST_SFX_DROPOFF_TICKS 25u -#define ST_SFX_LAND_HZ 90u -#define ST_SFX_LAND_TICKS 12u -#define ST_SFX_GEAR_HZ 140u // approximation -- $6410 table untraced -#define ST_SFX_GEAR_TICKS 5u +#define ST_ATTEN_LOUD 4u +#define ST_ATTEN_SOFT 8u +#define ST_ATTEN_OFF 15u -// C64-matched SFX behavior (see MECHANICS.md "Sound (SID)"): -// -// Thrust: voice 1 freq sweep. $6A63 writes $A0 = 160 to $721B at -// thrust-engage; the phase handler decrements $721B once per tick, -// LSRs it, and writes the result to both bytes of $D400/$D401 -- -// giving a SID freq word that drops over the burst. PAL C64 maps -// freq_word=$5050 -> ~1207 Hz at the start; the sweep ends near -// silence. Reproduce with per-frame freq updates on ST_THRUST_VOICE. -#define ST_THRUST_SWEEP_INIT 160u -// Atten scale on the JoeyLib audio HAL is SN76489-style: 0 = loud, -// 15 = silent. Anything >= 15 keys-off the voice (treated as silent -// by halAudioVoice on DOS). Keep all SFX in 0..14 to actually be -// audible. 6 = moderately loud; 10 = quieter. -#define ST_THRUST_ATTEN 8u -// Audible-Hz scale factor for the sweep counter -- chosen so initial -// (counter=160) lands ~1200 Hz to match the C64 PAL freq mapping. -#define ST_THRUST_HZ_PER_TICK 8u +// NTSC SID: Hz = word * 1022727 / 16777216 = word * 0.06096. +#define ST_SID_HZ_NUM 1022727UL +#define ST_SID_HZ_DEN 16777216UL -// Crash audio is TWO simultaneous events on the C64: -// 1. impact "bang": voice 3 noise burst ($D412 = $81 then $80 a -// short time later). Approximated here with rapid pseudo-random -// freq jumps on ST_SFX_VOICE. -// 2. descending "scream": $721B initialised to $A0 at $6A61, then -// the phase-1 handler ($6A72) LSRs it each tick into $D400/$D401 -// -- voice 1 freq drops from ~1.2 kHz toward silence over the -// death anim. Reproduced here on ST_THRUST_VOICE (same voice the -// C64 uses; the cab can't be thrusting during a crash anyway). -#define ST_SFX_CRASH_TICKS 30u -#define ST_CRASH_SCREAM_INIT 120u // matches stEngine ST_CRASH_ANIM_FRAMES +// Speech chirp: one short tone per character, pitched by the character. +#define ST_SPEECH_TICKS 3u +#define ST_SPEECH_BASE_HZ 300u +#define ST_SPEECH_STEP_HZ 9u -#define ST_SFX_DEFAULT_ATTEN 6u +// Thrust sweep register value -> Hz: the C64 writes the same byte to +// both frequency bytes, so the word is value * 257. +#define ST_SWEEP_WORD_SCALE 257u -static uint16_t gSfxTicksLeft; -static bool gThrustOn; -static uint16_t gThrustSweep; // counts down to zero; per-frame freq -static uint16_t gCrashTicks; // noise burst countdown (impact "bang") -static uint16_t gCrashScreamSweep; // voice-1 descending sweep during crash anim -static uint16_t gNoiseRng; // xorshift state for crash noise +typedef struct { + uint16_t ticksLeft; // release countdown (game ticks); 0 = idle + uint8_t voice; +} StSfxSlotT; -static void armSfx(uint16_t freq, uint8_t atten, uint16_t ticks); -static void silenceSfx(void); +static StSfxSlotT gSfx; // SID voice 1 (programs + sweep) +static StSfxSlotT gTone; // SID voice 2 +static bool gNoiseOn; +static uint16_t gSpeechTicks; + + +static uint16_t sidHz(uint16_t word); +static void voiceOff(StSfxSlotT *slot); + + +static uint16_t sidHz(uint16_t word) { + uint32_t hz = ((uint32_t)word * ST_SID_HZ_NUM) / ST_SID_HZ_DEN; + + if (hz > 20000u) { + hz = 20000u; + } + return (uint16_t)hz; +} + + +static void voiceOff(StSfxSlotT *slot) { + if (slot->ticksLeft != 0u) { + jlAudioVoice(slot->voice, 0u, ST_ATTEN_OFF); + slot->ticksLeft = 0u; + } +} + + +// Once per game tick: run the release timers ($4320 sfxEnvelopeTick). +void stAudioTick(void) { + if (gSfx.ticksLeft != 0u) { + gSfx.ticksLeft--; + if (gSfx.ticksLeft == 0u) { + jlAudioVoice(gSfx.voice, 0u, ST_ATTEN_OFF); + } + } + if (gTone.ticksLeft != 0u) { + gTone.ticksLeft--; + if (gTone.ticksLeft == 0u) { + jlAudioVoice(gTone.voice, 0u, ST_ATTEN_OFF); + } + } + if (gSpeechTicks != 0u) { + gSpeechTicks--; + if (gSpeechTicks == 0u) { + jlAudioVoice(ST_VOICE_TONE, 0u, ST_ATTEN_OFF); + } + } +} void stAudioInit(void) { (void)jlAudioInit(); - gThrustOn = false; - gSfxTicksLeft = 0u; - gThrustSweep = 0u; - gCrashTicks = 0u; - gCrashScreamSweep = 0u; - gNoiseRng = 0xACE1u; // arbitrary nonzero xorshift seed - jlAudioVoice(ST_SFX_VOICE, 0u, 0u); - jlAudioVoice(ST_THRUST_VOICE, 0u, 0u); + gSfx.voice = ST_VOICE_SFX; + gSfx.ticksLeft = 0u; + gTone.voice = ST_VOICE_TONE; + gTone.ticksLeft = 0u; + gNoiseOn = false; + gSpeechTicks = 0u; } void stAudioShutdown(void) { - silenceSfx(); - if (gThrustOn) { - jlAudioVoice(ST_THRUST_VOICE, 0u, 0u); - gThrustOn = false; - } - jlAudioStopMod(); + voiceOff(&gSfx); + voiceOff(&gTone); + jlAudioNoise(0u, ST_ATTEN_OFF); jlAudioShutdown(); } -// Called every host frame from spacetaxi.c's main loop (alongside -// jlAudioFrameTick which advances the mixer). Drives: -// - thrust sweep: per-frame freq decrement on ST_THRUST_VOICE -// mirroring the C64's $721B sweep -// - crash noise: per-frame xorshift -> freq jump on ST_SFX_VOICE -// approximating voice-3 noise -// - transient SFX countdown (pickup/dropoff/land tones) -void stAudioFrameTick(void) { - // Thrust voice has two modes: normal thrust (gThrustOn) sweeps - // freq down while held, OR crash scream sweep takes over when - // active (mutually exclusive -- the cab can't crash and thrust - // simultaneously). Crash scream wins if both are armed. - if (gCrashScreamSweep > 0u) { - gCrashScreamSweep--; - if (gCrashScreamSweep == 0u) { - jlAudioVoice(ST_THRUST_VOICE, 0u, 0u); - } else { - jlAudioVoice(ST_THRUST_VOICE, - (uint16_t)(gCrashScreamSweep * ST_THRUST_HZ_PER_TICK), - ST_THRUST_ATTEN); - } - } else if (gThrustOn && gThrustSweep > 0u) { - gThrustSweep--; - if (gThrustSweep == 0u) { - jlAudioVoice(ST_THRUST_VOICE, 0u, 0u); - } else { - jlAudioVoice(ST_THRUST_VOICE, - (uint16_t)(gThrustSweep * ST_THRUST_HZ_PER_TICK), - ST_THRUST_ATTEN); - } - } - // Noise-burst "bang" on the SFX voice, separate from the scream. - if (gCrashTicks > 0u) { - uint16_t pitchHz; - // xorshift LFSR -- 16-bit, period 65535. - gNoiseRng ^= (uint16_t)(gNoiseRng << 7); - gNoiseRng ^= (uint16_t)(gNoiseRng >> 9); - gNoiseRng ^= (uint16_t)(gNoiseRng << 8); - // Map random word to a noisy-feeling pitch range 100..700 Hz. - pitchHz = (uint16_t)(100u + (gNoiseRng % 600u)); - jlAudioVoice(ST_SFX_VOICE, pitchHz, ST_SFX_DEFAULT_ATTEN); - gCrashTicks--; - if (gCrashTicks == 0u) { - jlAudioVoice(ST_SFX_VOICE, 0u, 0u); - } - } else if (gSfxTicksLeft > 0u) { - gSfxTicksLeft--; - if (gSfxTicksLeft == 0u) { - silenceSfx(); - } - } -} - - -void stAudioPlayMusic(uint8_t musicId) { - // TODO: per-target music dispatch. Silent for now so the - // engine bring-up isn't blocked on writing per-platform music - // assets. - (void)musicId; -} - - -void stAudioStopMusic(void) { - jlAudioStopMod(); -} - - -// Engine thrust: starts a new freq-sweep burst on each rising edge. -// Mirrors the C64's $6A63 reset of $721B to $A0 -> phase handler -// decrements and writes to $D400/$D401 each tick. -void stAudioSfxThrust(bool on) { - if (on == gThrustOn) { +// $6D6A / $6A3B -- voice 3 jet noise gate. +void stAudioNoise(bool on) { + if (on == gNoiseOn) { return; } - gThrustOn = on; + gNoiseOn = on; if (on) { - gThrustSweep = ST_THRUST_SWEEP_INIT; - jlAudioVoice(ST_THRUST_VOICE, - (uint16_t)(gThrustSweep * ST_THRUST_HZ_PER_TICK), - ST_THRUST_ATTEN); + jlAudioNoise(31u, ST_ATTEN_SOFT); } else { - gThrustSweep = 0u; - jlAudioVoice(ST_THRUST_VOICE, 0u, 0u); + jlAudioNoise(0u, ST_ATTEN_OFF); } } -void stAudioSfxGear(void) { - // $63FC-$640C: the gear toggle fires SFX ptr $6410 through $42E9. - // The exact $6410 waveform is untraced; a short low click stands - // in until that SFX table is decoded -- marked approximation, not - // ROM-derived. - armSfx(ST_SFX_GEAR_HZ, ST_SFX_DEFAULT_ATTEN, ST_SFX_GEAR_TICKS); +// $42E9 -- a 9-byte SID program: freq lo/hi, pulse lo/hi, control, +// AD, SR, release ticks, voice. +void stAudioSfx(const uint8_t *program9) { + uint16_t word = (uint16_t)(program9[0] | ((uint16_t)program9[1] << 8)); + uint8_t ctrl = program9[4]; + uint8_t ticks = program9[7]; + StSfxSlotT *slot = (program9[8] == 1u) ? &gTone : &gSfx; + + if ((ctrl & 0x80u) != 0u) { + // Noise waveform: a burst on the noise generator. + jlAudioNoise(31u, ST_ATTEN_LOUD); + gNoiseOn = true; + return; + } + jlAudioVoice(slot->voice, sidHz(word), ST_ATTEN_LOUD); + slot->ticksLeft = (uint16_t)(ticks + 1u); } -void stAudioSfxLand(void) { - armSfx(ST_SFX_LAND_HZ, ST_SFX_DEFAULT_ATTEN, ST_SFX_LAND_TICKS); +// $6C7F / $6CAA -- voices 1 and 2 gated off. +void stAudioSilence(void) { + voiceOff(&gSfx); + voiceOff(&gTone); } -void stAudioSfxPickup(void) { - armSfx(ST_SFX_PICKUP_HZ, ST_SFX_DEFAULT_ATTEN, ST_SFX_PICKUP_TICKS); +// $9802 -- one spoken character (a chirp here). +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); + gSpeechTicks = ST_SPEECH_TICKS; } -void stAudioSfxDropoff(void) { - armSfx(ST_SFX_DROPOFF_HZ, ST_SFX_DEFAULT_ATTEN, ST_SFX_DROPOFF_TICKS); +// $6A8A -- the crash scream: voice 1 frequency written from the sweep. +void stAudioThrustSweep(uint8_t value) { + jlAudioVoice(ST_VOICE_SFX, sidHz((uint16_t)(value * ST_SWEEP_WORD_SCALE)), ST_ATTEN_LOUD); + gSfx.ticksLeft = 3u; } -// Crash: kicks off TWO simultaneous events on the C64 (see header -// comment near ST_SFX_CRASH_TICKS): -// 1. impact "bang" -- noise burst on ST_SFX_VOICE -// 2. descending "scream" -- voice-1 freq sweep on ST_THRUST_VOICE -// Force gThrustOn false so the scream takes over the thrust voice -// cleanly even if the player was thrust-holding into the wall. -void stAudioSfxCrash(void) { - gThrustOn = false; - gCrashTicks = ST_SFX_CRASH_TICKS; - gCrashScreamSweep = ST_CRASH_SCREAM_INIT; - gSfxTicksLeft = 0u; +// $6E71 -- the fuel pump's rising note on voice 1. +void stAudioVoice1Freq(uint8_t value) { + jlAudioVoice(ST_VOICE_SFX, sidHz((uint16_t)(value * ST_SWEEP_WORD_SCALE)), ST_ATTEN_LOUD); + gSfx.ticksLeft = 2u; } -// ----- internal ----- +// Level hooks poke voice 2 directly (laser hum, puzzle chime). +void stAudioVoice2(uint8_t freqLo, uint8_t freqHi, uint8_t ctrl) { + uint16_t word = (uint16_t)(freqLo | ((uint16_t)freqHi << 8)); -static void armSfx(uint16_t freq, uint8_t atten, uint16_t ticks) { - jlAudioVoice(ST_SFX_VOICE, freq, atten); - gSfxTicksLeft = ticks; -} - - -static void silenceSfx(void) { - jlAudioVoice(ST_SFX_VOICE, 0u, 0u); - gSfxTicksLeft = 0u; + if ((ctrl & 0x80u) != 0u) { + jlAudioNoise((uint8_t)(freqHi & 31u), ST_ATTEN_SOFT); + gNoiseOn = true; + return; + } + jlAudioVoice(ST_VOICE_TONE, sidHz(word), ST_ATTEN_SOFT); + gTone.ticksLeft = 2u; } diff --git a/examples/spacetaxi/stC64Data.c b/examples/spacetaxi/stC64Data.c new file mode 100644 index 0000000..53b7f05 --- /dev/null +++ b/examples/spacetaxi/stC64Data.c @@ -0,0 +1,1370 @@ +// Generated by assets/genC64Data.py from stuff/spacetaxi/raw.bin. +// Do not hand-edit; re-run the generator. + +#include "stC64Data.h" + +const uint8_t kStCharset[ST_CHARSET_CHARS][8] = { + { 0x3C, 0x66, 0x6E, 0x6E, 0x60, 0x62, 0x3C, 0x00 }, // $00 + { 0x00, 0x00, 0x3C, 0x06, 0x3E, 0x66, 0x3E, 0x00 }, // $01 + { 0x00, 0x60, 0x60, 0x7C, 0x66, 0x66, 0x7C, 0x00 }, // $02 + { 0x00, 0x00, 0x3C, 0x60, 0x60, 0x60, 0x3C, 0x00 }, // $03 + { 0x00, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x3E, 0x00 }, // $04 + { 0x00, 0x00, 0x3C, 0x66, 0x7E, 0x60, 0x3C, 0x00 }, // $05 + { 0x00, 0x0E, 0x18, 0x3E, 0x18, 0x18, 0x18, 0x00 }, // $06 + { 0x00, 0x00, 0x3E, 0x66, 0x66, 0x3E, 0x06, 0x7C }, // $07 + { 0x00, 0x60, 0x60, 0x7C, 0x66, 0x66, 0x66, 0x00 }, // $08 + { 0x00, 0x18, 0x00, 0x38, 0x18, 0x18, 0x3C, 0x00 }, // $09 + { 0x00, 0x06, 0x00, 0x06, 0x06, 0x06, 0x06, 0x3C }, // $0A + { 0x00, 0x60, 0x60, 0x6C, 0x78, 0x6C, 0x66, 0x00 }, // $0B + { 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00 }, // $0C + { 0x00, 0x00, 0x66, 0x7F, 0x7F, 0x6B, 0x63, 0x00 }, // $0D + { 0x00, 0x00, 0x7C, 0x66, 0x66, 0x66, 0x66, 0x00 }, // $0E + { 0x00, 0x00, 0x3C, 0x66, 0x66, 0x66, 0x3C, 0x00 }, // $0F + { 0x00, 0x00, 0x7C, 0x66, 0x66, 0x7C, 0x60, 0x60 }, // $10 + { 0x00, 0x00, 0x3E, 0x66, 0x66, 0x3E, 0x06, 0x06 }, // $11 + { 0x00, 0x00, 0x7C, 0x66, 0x60, 0x60, 0x60, 0x00 }, // $12 + { 0x00, 0x00, 0x3E, 0x60, 0x3C, 0x06, 0x7C, 0x00 }, // $13 + { 0x00, 0x18, 0x7E, 0x18, 0x18, 0x18, 0x0E, 0x00 }, // $14 + { 0x00, 0x00, 0x66, 0x66, 0x66, 0x66, 0x3E, 0x00 }, // $15 + { 0x00, 0x00, 0x66, 0x66, 0x66, 0x3C, 0x18, 0x00 }, // $16 + { 0x00, 0x00, 0x63, 0x6B, 0x7F, 0x3E, 0x36, 0x00 }, // $17 + { 0x00, 0x00, 0x66, 0x3C, 0x18, 0x3C, 0x66, 0x00 }, // $18 + { 0x00, 0x00, 0x66, 0x66, 0x66, 0x3E, 0x0C, 0x78 }, // $19 + { 0x00, 0x00, 0x7E, 0x0C, 0x18, 0x30, 0x7E, 0x00 }, // $1A + { 0x3C, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3C, 0x00 }, // $1B + { 0x0C, 0x12, 0x30, 0x7C, 0x30, 0x62, 0xFC, 0x00 }, // $1C + { 0x3C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x3C, 0x00 }, // $1D + { 0x00, 0x18, 0x3C, 0x7E, 0x18, 0x18, 0x18, 0x18 }, // $1E + { 0x00, 0x10, 0x30, 0x7F, 0x7F, 0x30, 0x10, 0x00 }, // $1F + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // $20 + { 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x18, 0x00 }, // $21 + { 0x66, 0x66, 0x22, 0x44, 0x00, 0x00, 0x00, 0x00 }, // $22 + { 0xF8, 0x30, 0x30, 0xE9, 0x1B, 0x1E, 0x1B, 0x1B }, // $23 + { 0x18, 0x3E, 0x60, 0x3C, 0x06, 0x7C, 0x18, 0x00 }, // $24 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF }, // $25 + { 0x3C, 0x42, 0x9D, 0xB1, 0xB1, 0x9D, 0x42, 0x3C }, // $26 + { 0x06, 0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00 }, // $27 + { 0x0C, 0x18, 0x30, 0x30, 0x30, 0x18, 0x0C, 0x00 }, // $28 + { 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x18, 0x30, 0x00 }, // $29 + { 0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00 }, // $2A + { 0x00, 0x18, 0x18, 0x7E, 0x18, 0x18, 0x00, 0x00 }, // $2B + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x30 }, // $2C + { 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x00 }, // $2D + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00 }, // $2E + { 0x00, 0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x00 }, // $2F + { 0x3C, 0x66, 0x6E, 0x76, 0x66, 0x66, 0x3C, 0x00 }, // $30 + { 0x18, 0x18, 0x38, 0x18, 0x18, 0x18, 0x7E, 0x00 }, // $31 + { 0x3C, 0x66, 0x06, 0x0C, 0x30, 0x60, 0x7E, 0x00 }, // $32 + { 0x3C, 0x66, 0x06, 0x1C, 0x06, 0x66, 0x3C, 0x00 }, // $33 + { 0x06, 0x0E, 0x1E, 0x66, 0x7F, 0x06, 0x06, 0x00 }, // $34 + { 0x7E, 0x60, 0x7C, 0x06, 0x06, 0x66, 0x3C, 0x00 }, // $35 + { 0x3C, 0x66, 0x60, 0x7C, 0x66, 0x66, 0x3C, 0x00 }, // $36 + { 0x7E, 0x66, 0x0C, 0x18, 0x18, 0x18, 0x18, 0x00 }, // $37 + { 0x3C, 0x66, 0x66, 0x3C, 0x66, 0x66, 0x3C, 0x00 }, // $38 + { 0x3C, 0x66, 0x66, 0x3E, 0x06, 0x66, 0x3C, 0x00 }, // $39 + { 0x00, 0x00, 0x18, 0x00, 0x00, 0x18, 0x00, 0x00 }, // $3A + { 0x00, 0x00, 0x18, 0x00, 0x00, 0x18, 0x18, 0x30 }, // $3B + { 0x0E, 0x18, 0x30, 0x60, 0x30, 0x18, 0x0E, 0x00 }, // $3C + { 0x00, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0x00, 0x00 }, // $3D + { 0x70, 0x18, 0x0C, 0x06, 0x0C, 0x18, 0x70, 0x00 }, // $3E + { 0x3C, 0x66, 0x06, 0x0C, 0x18, 0x00, 0x18, 0x00 }, // $3F + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF }, // $40 + { 0x18, 0x3C, 0x66, 0x7E, 0x66, 0x66, 0x66, 0x00 }, // $41 + { 0x7C, 0x66, 0x66, 0x7C, 0x66, 0x66, 0x7C, 0x00 }, // $42 + { 0x3C, 0x66, 0x60, 0x60, 0x60, 0x66, 0x3C, 0x00 }, // $43 + { 0x78, 0x6C, 0x66, 0x66, 0x66, 0x6C, 0x78, 0x00 }, // $44 + { 0x7E, 0x60, 0x60, 0x78, 0x60, 0x60, 0x7E, 0x00 }, // $45 + { 0x7E, 0x60, 0x60, 0x78, 0x60, 0x60, 0x60, 0x00 }, // $46 + { 0x3C, 0x66, 0x60, 0x6E, 0x66, 0x66, 0x3C, 0x00 }, // $47 + { 0x66, 0x66, 0x66, 0x7E, 0x66, 0x66, 0x66, 0x00 }, // $48 + { 0x3C, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00 }, // $49 + { 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x6C, 0x38, 0x00 }, // $4A + { 0x66, 0x6C, 0x78, 0x70, 0x78, 0x6C, 0x66, 0x00 }, // $4B + { 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x7E, 0x00 }, // $4C + { 0x63, 0x77, 0x7F, 0x6B, 0x63, 0x63, 0x63, 0x00 }, // $4D + { 0x66, 0x76, 0x7E, 0x7E, 0x6E, 0x66, 0x66, 0x00 }, // $4E + { 0x3C, 0x66, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x00 }, // $4F + { 0x7C, 0x66, 0x66, 0x7C, 0x60, 0x60, 0x60, 0x00 }, // $50 + { 0x3C, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x0E, 0x00 }, // $51 + { 0x7C, 0x66, 0x66, 0x7C, 0x78, 0x6C, 0x66, 0x00 }, // $52 + { 0x3C, 0x66, 0x60, 0x3C, 0x06, 0x66, 0x3C, 0x00 }, // $53 + { 0x7E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00 }, // $54 + { 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x00 }, // $55 + { 0x66, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x18, 0x00 }, // $56 + { 0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00 }, // $57 + { 0x66, 0x66, 0x3C, 0x18, 0x3C, 0x66, 0x66, 0x00 }, // $58 + { 0x66, 0x66, 0x66, 0x3C, 0x18, 0x18, 0x18, 0x00 }, // $59 + { 0x7E, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x7E, 0x00 }, // $5A + { 0x18, 0x18, 0x18, 0xFF, 0xFF, 0x18, 0x18, 0x18 }, // $5B + { 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFF, 0xFF }, // $5C + { 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0 }, // $5D + { 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03 }, // $5E + { 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0xFF, 0xFF }, // $5F + { 0xFF, 0xFF, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0 }, // $60 + { 0xFF, 0xFF, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03 }, // $61 + { 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // $62 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x79, 0xFF }, // $63 + { 0xFE, 0xFC, 0xF8, 0xF0, 0xE0, 0xC0, 0x80, 0x00 }, // $64 + { 0x7F, 0x3F, 0x1F, 0x0F, 0x07, 0x03, 0x01, 0x00 }, // $65 + { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, // $66 + { 0x88, 0x44, 0x22, 0x11, 0x22, 0x44, 0x88, 0x44 }, // $67 + { 0xFF, 0x33, 0xCC, 0x33, 0xCC, 0x33, 0xCC, 0xFF }, // $68 + { 0x00, 0x80, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC, 0xFE }, // $69 + { 0x00, 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F }, // $6A + { 0xFF, 0xE7, 0xC7, 0xE7, 0xE7, 0xE7, 0x81, 0xFF }, // $6B + { 0xFF, 0xE7, 0x99, 0xF9, 0xE7, 0x9F, 0x81, 0xFF }, // $6C + { 0xFF, 0x83, 0xF9, 0xE3, 0xF9, 0xF9, 0x83, 0xFF }, // $6D + { 0xFF, 0x99, 0x99, 0x81, 0xF9, 0xF9, 0xF9, 0xFF }, // $6E + { 0xFF, 0x81, 0x9F, 0x87, 0xF9, 0xF9, 0x87, 0xFF }, // $6F + { 0xFF, 0xC1, 0x9F, 0x83, 0x99, 0x99, 0xC3, 0xFF }, // $70 + { 0xFF, 0x81, 0xF9, 0xF3, 0xF3, 0xF3, 0xF3, 0xFF }, // $71 + { 0xFF, 0xC3, 0x99, 0xC3, 0x99, 0x99, 0xC3, 0xFF }, // $72 + { 0xFF, 0xC3, 0x99, 0x99, 0xC1, 0xF9, 0x83, 0xFF }, // $73 + { 0xFF, 0xC3, 0x99, 0x99, 0x99, 0x99, 0xC3, 0xFF }, // $74 + { 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00 }, // $75 + { 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18 }, // $76 + { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC7, 0xC7, 0xFF }, // $77 + { 0xF7, 0xC1, 0x97, 0x97, 0xE1, 0xE9, 0x83, 0xEF }, // $78 + { 0xFF, 0x81, 0x9F, 0x87, 0x9F, 0x9F, 0x9F, 0xFF }, // $79 + { 0xFF, 0x00, 0x00, 0x30, 0x30, 0x30, 0x30, 0xFF }, // $7A + { 0xFF, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xFF }, // $7B + { 0x20, 0x50, 0x50, 0x58, 0x89, 0x0A, 0x0A, 0x04 }, // $7C + { 0x03, 0x0F, 0x3F, 0xFF, 0xFF, 0x3F, 0x0F, 0x03 }, // $7D + { 0x0E, 0x1B, 0x18, 0x7E, 0xC3, 0xCF, 0xC3, 0xCF }, // $7E + { 0x00, 0xC0, 0xE0, 0xF0, 0xF0, 0xE0, 0xC0, 0x00 }, // $7F + { 0xFF, 0xD3, 0xD3, 0xFF, 0xD3, 0xD3, 0xD3, 0xFF }, // $80 + { 0x00, 0x03, 0x07, 0x0F, 0x0F, 0x07, 0x03, 0x00 }, // $81 + { 0xC0, 0xF0, 0xFC, 0xFF, 0xFF, 0xFC, 0xF0, 0xC0 }, // $82 + { 0x30, 0x79, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, // $83 + { 0x00, 0x00, 0x7E, 0xFF, 0x7E, 0x00, 0x00, 0x00 }, // $84 + { 0x7E, 0x7E, 0x7E, 0xFF, 0x7E, 0x7E, 0x7E, 0x00 }, // $85 + { 0x00, 0x7E, 0x7E, 0xFF, 0x7E, 0x7E, 0x00, 0x00 }, // $86 + { 0x00, 0x00, 0x7E, 0xFF, 0x7E, 0x00, 0x00, 0x00 }, // $87 + { 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00 }, // $88 + { 0x00, 0x03, 0x03, 0x03, 0x0F, 0x0F, 0x3F, 0xFF }, // $89 + { 0x00, 0xC0, 0xC0, 0xC0, 0xF0, 0xF0, 0xFC, 0xFF }, // $8A + { 0xFF, 0xFC, 0xF0, 0xF0, 0xC0, 0xC0, 0xC0, 0x00 }, // $8B + { 0xFF, 0x3F, 0x0F, 0x0F, 0x03, 0x03, 0x03, 0x00 }, // $8C + { 0x9F, 0x9F, 0x00, 0xF3, 0xF3, 0xF3, 0x00, 0x9F }, // $8D + { 0xFE, 0xFD, 0xFB, 0xF7, 0xEF, 0xDF, 0xBF, 0x7F }, // $8E + { 0x7F, 0xBF, 0xDF, 0xEF, 0xF7, 0xFB, 0xFD, 0xFE }, // $8F + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF }, // $90 + { 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C }, // $91 + { 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C }, // $92 + { 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C }, // $93 + { 0x22, 0x22, 0x46, 0x65, 0x95, 0x91, 0x90, 0x08 }, // $94 + { 0x22, 0x41, 0x61, 0x92, 0x4A, 0x4A, 0x55, 0x05 }, // $95 + { 0xFF, 0x3C, 0x5A, 0x5A, 0x42, 0x42, 0xE7, 0x42 }, // $96 + { 0x00, 0x3C, 0x7E, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C }, // $97 + { 0x10, 0x10, 0x20, 0x20, 0x40, 0x40, 0x80, 0x80 }, // $98 + { 0x4C, 0x61, 0x88, 0x4C, 0xC7, 0x19, 0x21, 0x91 }, // $99 + { 0x80, 0x80, 0x40, 0xC0, 0x60, 0xB0, 0x6C, 0xBB }, // $9A + { 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C }, // $9B + { 0x01, 0x02, 0x03, 0x06, 0x05, 0x0E, 0x32, 0xD5 }, // $9C + { 0x3C, 0x5A, 0x99, 0x3C, 0x5A, 0x99, 0x3C, 0x5A }, // $9D + { 0x00, 0x00, 0x77, 0x99, 0x99, 0x77, 0x00, 0x00 }, // $9E + { 0x00, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x7E, 0xFF }, // $9F + { 0x0F, 0x1F, 0x3F, 0x7F, 0x7F, 0xFF, 0xFF, 0xFF }, // $A0 + { 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x07, 0x0F }, // $A1 + { 0x04, 0x08, 0x30, 0x60, 0x38, 0x0C, 0x78, 0xF0 }, // $A2 + { 0xC0, 0xC0, 0x80, 0x00, 0x00, 0x80, 0xE0, 0xF0 }, // $A3 + { 0xE0, 0xC0, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00 }, // $A4 + { 0x00, 0x01, 0x07, 0x0E, 0x3E, 0x7C, 0x7C, 0xFE }, // $A5 + { 0x00, 0x03, 0x0F, 0x3C, 0x70, 0x38, 0x1F, 0x07 }, // $A6 + { 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0 }, // $A7 + { 0xFC, 0xFC, 0xFC, 0xFC, 0xFC, 0xFC, 0xFC, 0xFC }, // $A8 + { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xE0 }, // $A9 + { 0xFE, 0xFE, 0xFC, 0xFC, 0xF8, 0xF0, 0xE0, 0xC0 }, // $AA + { 0xFF, 0xFE, 0xFC, 0xF8, 0xE0, 0x80, 0x00, 0x00 }, // $AB + { 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03 }, // $AC + { 0x03, 0x03, 0x03, 0xFF, 0xFF, 0x03, 0x03, 0x03 }, // $AD + { 0x03, 0x03, 0x03, 0xFF, 0xFF, 0x00, 0x00, 0x00 }, // $AE + { 0x00, 0x00, 0x33, 0x66, 0xFF, 0x66, 0xFF, 0xFF }, // $AF + { 0x54, 0x54, 0x04, 0x04, 0x00, 0x00, 0x00, 0xFF }, // $B0 + { 0xFF, 0x00, 0xFF, 0x55, 0x41, 0x41, 0x55, 0x55 }, // $B1 + { 0x00, 0x00, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55 }, // $B2 + { 0x00, 0x00, 0x54, 0x54, 0x54, 0x54, 0x54, 0x15 }, // $B3 + { 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x01 }, // $B4 + { 0x00, 0x00, 0x01, 0x05, 0x05, 0x05, 0x05, 0x05 }, // $B5 + { 0xFF, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55 }, // $B6 + { 0x55, 0x55, 0x55, 0xD5, 0xD5, 0xD5, 0xD5, 0xD5 }, // $B7 + { 0xD5, 0xD5, 0xD5, 0xD5, 0xD5, 0xD5, 0xD5, 0xD5 }, // $B8 + { 0x99, 0x65, 0x49, 0x00, 0x00, 0x00, 0x00, 0x00 }, // $B9 + { 0x66, 0x59, 0x55, 0x95, 0x84, 0x00, 0x00, 0x00 }, // $BA + { 0x65, 0x55, 0x96, 0x59, 0x56, 0x95, 0x11, 0x00 }, // $BB + { 0x3C, 0x42, 0xB9, 0xA5, 0xB9, 0x95, 0x42, 0x3C }, // $BC + { 0x3C, 0x42, 0x9D, 0xA1, 0xA1, 0x9D, 0x42, 0x3C }, // $BD + { 0x03, 0x0C, 0x18, 0x28, 0x68, 0x68, 0xC8, 0xC8 }, // $BE + { 0xC8, 0xC8, 0x68, 0x68, 0x38, 0x18, 0x0C, 0x03 }, // $BF + { 0x00, 0x00, 0x00, 0x00, 0x03, 0x0F, 0x1B, 0xE3 }, // $C0 + { 0xF3, 0x1E, 0x0E, 0x06, 0x06, 0x08, 0x00, 0x00 }, // $C1 + { 0xC0, 0x60, 0x30, 0x30, 0x30, 0x3F, 0x32, 0x32 }, // $C2 + { 0x32, 0x32, 0x32, 0x32, 0x1A, 0x0E, 0x06, 0x03 }, // $C3 + { 0x30, 0x30, 0x30, 0x30, 0x30, 0x78, 0xCC, 0x83 }, // $C4 + { 0x00, 0x00, 0x00, 0x60, 0x30, 0xFF, 0x30, 0x30 }, // $C5 + { 0x60, 0x30, 0x18, 0x18, 0x18, 0xF8, 0x18, 0x18 }, // $C6 + { 0x18, 0x18, 0x18, 0x18, 0x30, 0x60, 0xC0, 0x80 }, // $C7 + { 0xFF, 0xFF, 0xF1, 0xC0, 0x80, 0xDD, 0xFF, 0xFF }, // $C8 + { 0xF8, 0x30, 0x30, 0xE9, 0x1B, 0x1E, 0x1B, 0x1B }, // $C9 + { 0xFF, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, 0xE7, 0xFF }, // $CA + { 0x03, 0x98, 0x9C, 0x83, 0x9C, 0x9C, 0x01, 0xFF }, // $CB + { 0xC3, 0x99, 0x99, 0x99, 0x99, 0x99, 0xC3, 0xFF }, // $CC + { 0x18, 0x89, 0x81, 0x81, 0x91, 0x99, 0x18, 0xFF }, // $CD + { 0x18, 0x99, 0x99, 0x99, 0x99, 0x81, 0xC3, 0xFF }, // $CE + { 0xC2, 0x99, 0x9F, 0xC3, 0xF9, 0x99, 0x43, 0xFF }, // $CF + { 0xFB, 0xFD, 0xFD, 0xFB, 0xFD, 0xB9, 0xCB, 0xFF }, // $D0 + { 0xDD, 0xB9, 0xDB, 0xA7, 0xDB, 0xB9, 0xDE, 0xFF }, // $D1 + { 0xBD, 0xBF, 0xB7, 0xBB, 0xDD, 0xED, 0xFD, 0xBD }, // $D2 + { 0x00, 0x00, 0x00, 0x00, 0x38, 0x78, 0xC0, 0x00 }, // $D3 + { 0x00, 0x0E, 0x0F, 0x01, 0x00, 0x00, 0x00, 0x00 }, // $D4 + { 0x00, 0xC7, 0x3F, 0xDF, 0x3F, 0x07, 0x01, 0x00 }, // $D5 + { 0x56, 0x55, 0x6B, 0x3E, 0xBF, 0x7C, 0xFE, 0xFF }, // $D6 + { 0x00, 0x85, 0xF5, 0xFF, 0xFE, 0xF5, 0x86, 0x00 }, // $D7 + { 0x99, 0x99, 0xC3, 0xE7, 0xC3, 0x99, 0x99, 0xFF }, // $D8 + { 0x99, 0x99, 0x99, 0xC3, 0xE7, 0xE7, 0xE7, 0xFF }, // $D9 + { 0x81, 0xF9, 0xF3, 0xE7, 0xCF, 0x9F, 0x81, 0xFF }, // $DA + { 0xE7, 0xE7, 0xE7, 0x00, 0x00, 0xE7, 0xE7, 0xE7 }, // $DB + { 0x3F, 0x3F, 0xCF, 0xCF, 0x3F, 0x3F, 0xCF, 0xCF }, // $DC + { 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7 }, // $DD + { 0xCC, 0xCC, 0x33, 0x33, 0xCC, 0xCC, 0x33, 0x33 }, // $DE + { 0xCC, 0x66, 0x33, 0x99, 0xCC, 0x66, 0x33, 0x99 }, // $DF + { 0x00, 0x3C, 0x7E, 0x5A, 0x66, 0x3C, 0x18, 0x3C }, // $E0 + { 0x00, 0x3E, 0x7F, 0x6F, 0x3F, 0x7E, 0x18, 0x3C }, // $E1 + { 0x00, 0x3E, 0x7F, 0x7B, 0x7E, 0x3F, 0x18, 0x3C }, // $E2 + { 0x00, 0x3C, 0x7E, 0x7E, 0x7E, 0x3C, 0x18, 0x3C }, // $E3 + { 0x3C, 0x42, 0x42, 0xFF, 0x5A, 0x66, 0x18, 0x3C }, // $E4 + { 0x3C, 0x42, 0xF2, 0x5A, 0x3F, 0x7C, 0x18, 0x3C }, // $E5 + { 0x3C, 0x42, 0x4F, 0x5A, 0xFC, 0x3E, 0x18, 0x3C }, // $E6 + { 0x3C, 0x42, 0x42, 0x42, 0xFF, 0x3C, 0x18, 0x3C }, // $E7 + { 0x66, 0x66, 0x66, 0x66, 0x66, 0x24, 0x66, 0xE7 }, // $E8 + { 0x6C, 0x6C, 0x6C, 0x6C, 0x68, 0x2C, 0x6E, 0xE0 }, // $E9 + { 0x36, 0x36, 0x36, 0x36, 0x16, 0x34, 0x76, 0x07 }, // $EA + { 0xF8, 0xD8, 0xD8, 0xCC, 0xCF, 0x8C, 0xC0, 0xE0 }, // $EB + { 0xFF, 0xDB, 0xDB, 0xDB, 0xDB, 0xDB, 0xDB, 0xFF }, // $EC + { 0xFF, 0x8D, 0xED, 0x81, 0xB7, 0xB1, 0xFF, 0xFF }, // $ED + { 0xFF, 0xC9, 0x93, 0xC9, 0x93, 0xFF, 0xFF, 0xFF }, // $EE + { 0x1F, 0x1B, 0x1B, 0x33, 0xF3, 0x31, 0x03, 0x07 }, // $EF + { 0x6C, 0x6C, 0x6C, 0x58, 0x58, 0x70, 0x78, 0x7C }, // $F0 + { 0x6C, 0x6C, 0x38, 0x30, 0xF0, 0xD0, 0x98, 0x1C }, // $F1 + { 0x36, 0x36, 0x1C, 0x0C, 0x0F, 0x0B, 0x19, 0x38 }, // $F2 + { 0x36, 0x36, 0x36, 0x1A, 0x1A, 0x0E, 0x1E, 0x3E }, // $F3 + { 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00 }, // $F4 + { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, // $F5 + { 0x03, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06 }, // $F6 + { 0xC0, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60 }, // $F7 + { 0xC0, 0x60, 0x60, 0x30, 0xF0, 0x70, 0x30, 0x30 }, // $F8 + { 0xC0, 0x60, 0x30, 0x30, 0x18, 0x3C, 0x66, 0x03 }, // $F9 + { 0xC0, 0x60, 0x60, 0x60, 0x3F, 0x1C, 0x18, 0x00 }, // $FA + { 0xC3, 0x66, 0x6C, 0x3C, 0x0E, 0x02, 0x00, 0x00 }, // $FB + { 0xCC, 0xEC, 0x6E, 0x6F, 0x3C, 0x18, 0x00, 0x00 }, // $FC + { 0xC3, 0x66, 0x36, 0x3C, 0x70, 0x40, 0x00, 0x00 }, // $FD + { 0x03, 0x06, 0x06, 0x0E, 0xFC, 0x38, 0x18, 0x00 }, // $FE + { 0x03, 0x06, 0x0C, 0x0C, 0x18, 0x3C, 0x66, 0x57 }, // $FF +}; + +const uint8_t kStSpriteBitmaps[ST_SPRITE_COUNT][ST_SPRITE_BYTES] = { + { // ptr $C0 + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x07, 0xE0, 0x00, + 0x1E, 0xB3, 0x80, + 0x7E, 0xB2, 0x00, + 0xBF, 0xDF, 0xC0, + 0xA1, 0xDC, 0xF0, + 0x6D, 0xF3, 0x70, + 0x0C, 0x03, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $C1 + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x07, 0xE0, 0x00, + 0x1E, 0xB3, 0x80, + 0x7E, 0xB2, 0x00, + 0xBF, 0xDF, 0xC0, + 0xA1, 0xDC, 0xF0, + 0x6D, 0xF3, 0x70, + 0x0C, 0x03, 0x00, + 0x1C, 0x03, 0x80, + 0x38, 0x01, 0xC0, + 0x30, 0x00, 0xC0, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $C2 + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x28, 0x00, + 0x00, 0xB8, 0x00, + 0x00, 0x20, 0x00, + 0x00, 0xA8, 0x00, + 0x00, 0xA8, 0x00, + 0x00, 0xF8, 0x00, + 0x00, 0x38, 0x00, + 0x00, 0x3C, 0x00, + 0x00, 0xCC, 0x00, + 0x00, 0xCE, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $C3 + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x28, 0x00, + 0x00, 0xB8, 0x00, + 0x00, 0x20, 0x00, + 0x00, 0xA8, 0x00, + 0x00, 0xAA, 0x00, + 0x00, 0xAB, 0x00, + 0x00, 0x70, 0x00, + 0x00, 0x3C, 0x00, + 0x00, 0x30, 0x00, + 0x00, 0xF0, 0x00, + 0x00, 0x38, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $C4 + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0xA0, 0x00, + 0x00, 0xE8, 0x00, + 0x00, 0x20, 0x00, + 0x00, 0xA8, 0x00, + 0x00, 0xA8, 0x00, + 0x00, 0x3C, 0x00, + 0x00, 0xB0, 0x00, + 0x00, 0xF0, 0x00, + 0x00, 0xCC, 0x00, + 0x02, 0xCC, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $C5 + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0xA0, 0x00, + 0x00, 0xE8, 0x00, + 0x00, 0x20, 0x00, + 0x00, 0xA8, 0x00, + 0x02, 0xA8, 0x00, + 0x03, 0x28, 0x00, + 0x00, 0x34, 0x00, + 0x00, 0xF0, 0x00, + 0x00, 0x30, 0x00, + 0x00, 0x3C, 0x00, + 0x00, 0xB0, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $C6 + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x20, 0x00, + 0x00, 0xB8, 0x00, + 0x00, 0x20, 0x00, + 0x00, 0x9A, 0x00, + 0x02, 0x23, 0x00, + 0x03, 0x20, 0x00, + 0x00, 0x74, 0x00, + 0x00, 0xCC, 0x00, + 0x00, 0xCC, 0x00, + 0x01, 0xCE, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $C7 + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x20, 0x00, + 0x00, 0xB8, 0x00, + 0x00, 0x20, 0x00, + 0x00, 0x9B, 0x00, + 0x02, 0x20, 0x00, + 0x03, 0x20, 0x00, + 0x00, 0x74, 0x00, + 0x00, 0xCC, 0x00, + 0x00, 0xCE, 0x00, + 0x01, 0xCC, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $C8 + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x20, 0x00, + 0x02, 0x00, 0x00, + 0x00, 0x04, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x98, 0x00, + 0x02, 0x22, 0x00, + 0x00, 0x20, 0x00, + 0x00, 0xFC, 0x00, + 0x00, 0xCC, 0x00, + 0x00, 0xCC, 0x00, + 0x01, 0xCE, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $C9 + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x08, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x20, 0x00, + 0x00, 0x00, 0x00, + 0x02, 0x22, 0x00, + 0x00, 0x20, 0x00, + 0x00, 0x3C, 0x00, + 0x00, 0xCC, 0x00, + 0x00, 0xCC, 0x00, + 0x01, 0xCE, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $CA + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x20, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x81, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x20, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0xC0, 0x00, + 0x00, 0xCC, 0x00, + 0x01, 0xCE, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $CB + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, + 0x00, 0x20, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x40, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x0C, 0x00, + 0x00, 0x00, 0x00, + 0x01, 0xCE, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $CC + 0x00, 0x00, 0x00, + 0x00, 0x11, 0xC0, + 0x0C, 0x03, 0x00, + 0x42, 0x02, 0x00, + 0x02, 0x03, 0x00, + 0x02, 0x01, 0x00, + 0x01, 0x01, 0x30, + 0x7D, 0x1D, 0xF0, + 0x7D, 0xDE, 0x98, + 0x1F, 0xCA, 0x70, + 0x02, 0x6A, 0x40, + 0x0A, 0x2A, 0xC0, + 0x1C, 0x2F, 0x80, + 0x02, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $CD + 0x00, 0x10, 0x40, + 0x00, 0x00, 0x04, + 0x08, 0x03, 0x00, + 0x40, 0x01, 0xC0, + 0x03, 0x00, 0x80, + 0x00, 0x80, 0x80, + 0x01, 0x00, 0xB0, + 0x1D, 0x1D, 0xF0, + 0x35, 0xDA, 0x98, + 0x77, 0xCA, 0x50, + 0x40, 0x7A, 0x70, + 0x00, 0x18, 0xC1, + 0x04, 0x1C, 0x80, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $CE + 0x00, 0x00, 0x00, + 0x10, 0x03, 0x00, + 0x1C, 0x03, 0x00, + 0x00, 0x00, 0x08, + 0x02, 0x00, 0x10, + 0x00, 0x00, 0x00, + 0x23, 0xC0, 0x40, + 0x90, 0x2C, 0x70, + 0x84, 0x06, 0x20, + 0x2F, 0xC0, 0xE0, + 0x04, 0x14, 0x80, + 0x00, 0x06, 0x00, + 0x60, 0x1C, 0x60, + 0x00, 0x04, 0x00, + 0x00, 0x00, 0x02, + 0x20, 0x08, 0x08, + 0x10, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $CF + 0x00, 0x18, 0x00, + 0x10, 0x01, 0xC0, + 0x10, 0x00, 0x40, + 0x04, 0x00, 0x48, + 0x0C, 0x00, 0x00, + 0x20, 0x00, 0x00, + 0x00, 0xE1, 0xC0, + 0x18, 0xE1, 0x80, + 0xB0, 0x04, 0x02, + 0x00, 0x0C, 0x00, + 0x01, 0x00, 0x00, + 0x00, 0x00, 0xC0, + 0x08, 0x02, 0x40, + 0x00, 0x07, 0x10, + 0x00, 0x00, 0x00, + 0xC0, 0x08, 0x00, + 0x80, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $D0 + 0x00, 0x00, 0x00, + 0x10, 0x01, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x08, + 0x00, 0x10, 0x00, + 0x00, 0x04, 0x00, + 0x30, 0x05, 0x80, + 0x60, 0x00, 0x00, + 0x00, 0x00, 0x20, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, + 0x00, 0x00, 0xD0, + 0x00, 0x00, 0x00, + 0x00, 0x08, 0x00, + 0x10, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $D1 + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x10, 0x00, + 0x00, 0x14, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, + 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $D2 + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, + 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, + 0x00, 0x14, 0x00, + 0x00, 0x1C, 0x00, + 0x00, 0x04, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $D3 + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x40, 0x00, 0x00, + 0x40, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, + 0x00, 0x14, 0x00, + 0x00, 0x1C, 0x00, + 0x00, 0x04, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $D4 + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x10, 0x00, + 0x00, 0x14, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $D5 + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, + 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $D6 + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x10, 0x00, + 0x00, 0x14, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x40, 0x00, 0x00, + 0x40, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $D7 + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x40, 0x00, 0x00, + 0x40, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $D8 + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, + 0x00, 0x14, 0x00, + 0x00, 0x1C, 0x00, + 0x00, 0x04, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $D9 + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x20, 0x00, + 0x00, 0xBB, 0x00, + 0x00, 0x22, 0x00, + 0x00, 0x98, 0x00, + 0x02, 0x20, 0x00, + 0x03, 0x20, 0x00, + 0x00, 0x74, 0x00, + 0x00, 0xCC, 0x00, + 0x00, 0xCC, 0x00, + 0x01, 0xCE, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $DA + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x18, 0x00, + 0x00, 0x7C, 0x00, + 0x00, 0x30, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $DB + 0xFE, 0x00, 0x07, + 0x3E, 0x00, 0x07, + 0x3F, 0x00, 0x0D, + 0x3F, 0x00, 0x0D, + 0x37, 0x80, 0x19, + 0x37, 0x80, 0x19, + 0x33, 0xC0, 0x31, + 0x33, 0xC0, 0x31, + 0x31, 0xE0, 0x61, + 0x31, 0xE0, 0x61, + 0x30, 0xF0, 0xC1, + 0x30, 0xF0, 0xC1, + 0x30, 0x79, 0x81, + 0x30, 0x79, 0x81, + 0x30, 0x3F, 0x01, + 0x30, 0x3F, 0x01, + 0x30, 0x1E, 0x01, + 0x30, 0x1E, 0x01, + 0x30, 0x0C, 0x01, + 0x30, 0x0C, 0x01, + 0xFC, 0x00, 0x07, + }, + { // ptr $DC + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0xBD, 0x00, + 0x2C, 0xEB, 0x40, + 0x08, 0xEB, 0xD0, + 0x3F, 0x7F, 0xE0, + 0xF3, 0x74, 0xA0, + 0xDF, 0xF7, 0x90, + 0x0C, 0x03, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $DD + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0xBD, 0x00, + 0x2C, 0xEB, 0x40, + 0x08, 0xEB, 0xD0, + 0x3F, 0x7F, 0xE0, + 0xF3, 0x74, 0xA0, + 0xDF, 0xF7, 0x90, + 0x0C, 0x03, 0x00, + 0x1C, 0x03, 0x80, + 0x38, 0x01, 0xC0, + 0x30, 0x00, 0xC0, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $DE + 0xF8, 0x00, 0x00, + 0xE0, 0x00, 0x00, + 0xE0, 0x00, 0x00, + 0xE0, 0x00, 0x00, + 0xE0, 0x00, 0x00, + 0xE0, 0x00, 0x00, + 0xE0, 0x00, 0x00, + 0xE0, 0x00, 0x00, + 0xE0, 0x00, 0x00, + 0xE0, 0x00, 0x00, + 0xE0, 0x00, 0x00, + 0xE0, 0x00, 0x00, + 0xE0, 0x00, 0x00, + 0xE0, 0x00, 0x00, + 0xE0, 0x00, 0x00, + 0xE0, 0x00, 0x00, + 0xE0, 0x00, 0x00, + 0xE0, 0x00, 0x00, + 0xE0, 0x00, 0x00, + 0xE0, 0x00, 0x00, + 0xF8, 0x00, 0x00, + }, + { // ptr $DF + 0xFF, 0x00, 0x3F, + 0x3C, 0x00, 0x0C, + 0x3C, 0x00, 0x0C, + 0x3C, 0x00, 0x0C, + 0x3C, 0x00, 0x0C, + 0x3C, 0x00, 0x0C, + 0x3C, 0x00, 0x0C, + 0x3C, 0x00, 0x0C, + 0x3C, 0x00, 0x0C, + 0x3C, 0x00, 0x0C, + 0x3C, 0x00, 0x0C, + 0x3C, 0x00, 0x0C, + 0x3C, 0x00, 0x0C, + 0x3C, 0x00, 0x0C, + 0x3C, 0x00, 0x0C, + 0x3E, 0x00, 0x18, + 0x1E, 0x00, 0x18, + 0x1F, 0x80, 0x70, + 0x07, 0xF3, 0xF0, + 0x01, 0xFF, 0xC0, + 0x00, 0x3E, 0x00, + }, + { // ptr $E0 + 0x00, 0xFF, 0x03, + 0x0F, 0xFF, 0xFF, + 0x3F, 0x00, 0xFF, + 0x7C, 0x00, 0x0F, + 0xF0, 0x00, 0x03, + 0xF0, 0x00, 0x00, + 0xF0, 0x00, 0x00, + 0x7C, 0x00, 0x00, + 0x3F, 0x00, 0x00, + 0x0F, 0xF0, 0x00, + 0x00, 0xFF, 0x00, + 0x00, 0x0F, 0xF0, + 0x00, 0x00, 0xFC, + 0x00, 0x00, 0x3E, + 0x00, 0x00, 0x0F, + 0x00, 0x00, 0x0F, + 0xC0, 0x00, 0x0F, + 0xF0, 0x00, 0x3E, + 0xFF, 0x00, 0xFC, + 0xFF, 0xFF, 0xF0, + 0xC0, 0xFF, 0x00, + }, + { // ptr $E1 + 0xFF, 0xFF, 0xFF, + 0x3C, 0x00, 0x07, + 0x3C, 0x00, 0x03, + 0x3C, 0x00, 0x00, + 0x3C, 0x00, 0x00, + 0x3C, 0x00, 0x00, + 0x3C, 0x00, 0x00, + 0x3C, 0x00, 0x00, + 0x3C, 0x00, 0x60, + 0x3C, 0x00, 0xE0, + 0x3F, 0xFF, 0xE0, + 0x3C, 0x00, 0xE0, + 0x3C, 0x00, 0x60, + 0x3C, 0x00, 0x00, + 0x3C, 0x00, 0x00, + 0x3C, 0x00, 0x00, + 0x3C, 0x00, 0x00, + 0x3C, 0x00, 0x00, + 0x3C, 0x00, 0x03, + 0x3C, 0x00, 0x07, + 0xFF, 0xFF, 0xFF, + }, + { // ptr $E2 + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x07, 0xE0, 0x00, + 0x1E, 0xB3, 0x80, + 0x7E, 0xB2, 0x00, + 0xBF, 0xDF, 0xC0, + 0xA1, 0xDC, 0xC0, + 0x6D, 0xF3, 0xC0, + 0x0C, 0x03, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $E3 + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x07, 0xE0, 0x00, + 0x06, 0xBE, 0x00, + 0x1E, 0xB8, 0x00, + 0x7F, 0xDF, 0x00, + 0x61, 0xDF, 0x00, + 0x6D, 0xF7, 0x00, + 0x0C, 0x0C, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $E4 + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x07, 0xE0, 0x00, + 0x06, 0xB8, 0x00, + 0x1E, 0xB8, 0x00, + 0x7F, 0xFC, 0x00, + 0xD7, 0xD7, 0x00, + 0x7D, 0xBC, 0x00, + 0x3C, 0x3C, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $E5 + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x03, 0xE0, 0x00, + 0x0E, 0xB0, 0x00, + 0x0F, 0xF0, 0x00, + 0x37, 0xDC, 0x00, + 0x3D, 0xBC, 0x00, + 0x0C, 0x30, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $E6 + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x03, 0xC0, 0x00, + 0x06, 0xB0, 0x00, + 0x0D, 0xD0, 0x00, + 0x0F, 0xF0, 0x00, + 0x06, 0xC0, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $E7 + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, + 0x0E, 0xC0, 0x00, + 0x0F, 0xC0, 0x00, + 0x06, 0xC0, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $E8 + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x01, 0x80, 0x00, + 0x03, 0xC0, 0x00, + 0x01, 0x80, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $E9 + 0x00, 0x00, 0x00, + 0x0C, 0x00, 0x00, + 0x3E, 0x38, 0x80, + 0x3A, 0x28, 0x00, + 0x0E, 0xEE, 0x00, + 0x00, 0x82, 0x00, + 0x01, 0xE3, 0xC0, + 0x67, 0x60, 0x80, + 0x06, 0x79, 0x80, + 0x03, 0x91, 0x00, + 0x00, 0xBB, 0x20, + 0x00, 0xEE, 0x00, + 0x0C, 0x60, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x18, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $EA + 0x00, 0x00, 0x00, + 0x0C, 0x00, 0x00, + 0x26, 0x38, 0x00, + 0x38, 0x00, 0x00, + 0x08, 0xB8, 0x00, + 0x00, 0x92, 0x00, + 0x00, 0x1B, 0x40, + 0x07, 0x20, 0x00, + 0x18, 0x09, 0x80, + 0x03, 0x45, 0x00, + 0x00, 0x21, 0x00, + 0x01, 0xAC, 0x00, + 0x00, 0x20, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $EB + 0x00, 0x00, 0x00, + 0x0C, 0x00, 0x00, + 0x20, 0x28, 0x00, + 0x28, 0x00, 0x00, + 0x08, 0x08, 0x00, + 0x00, 0x80, 0x00, + 0x00, 0x11, 0x40, + 0x05, 0x00, 0x00, + 0x18, 0x49, 0x80, + 0x00, 0x00, 0x00, + 0x00, 0x21, 0x00, + 0x01, 0x84, 0x00, + 0x00, 0x20, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, + { // ptr $EC + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + }, +}; + +const uint8_t kStRngTable[ST_RNG_TABLE_SIZE] = { + 0x23, 0x25, 0x76, 0xC1, 0x75, 0xE7, 0x7B, 0x2E, 0x13, 0x6A, 0xAC, 0xD4, 0xA6, 0xAD, 0xE9, 0x7A, + 0x74, 0xE8, 0xEE, 0x76, 0xAC, 0x37, 0x27, 0xDF, 0x81, 0xE7, 0x1C, 0xC0, 0x91, 0xE3, 0x96, 0x4A, + 0x75, 0x59, 0xE0, 0x7A, 0x11, 0xE9, 0x5E, 0xF9, 0x76, 0x29, 0x93, 0x6C, 0xEE, 0x6C, 0xA8, 0xC3, + 0xE0, 0xA5, 0x81, 0xE3, 0x3D, 0xDB, 0xA3, 0x73, 0xF7, 0x16, 0x5F, 0x9B, 0xD8, 0xDC, 0xC4, 0x69, +}; + +const uint8_t kStSfxPrograms[ST_SFX_COUNT][ST_SFX_PROGRAM_BYTES] = { + { 0x20, 0x20, 0xFF, 0x03, 0x61, 0x00, 0xF6, 0x07, 0x00 }, // ST_SFX_GEAR + { 0x04, 0x04, 0xFF, 0x07, 0x41, 0x46, 0x58, 0x05, 0x00 }, // ST_SFX_CASH + { 0x78, 0x78, 0x00, 0x00, 0x21, 0x00, 0xF9, 0x06, 0x00 }, // ST_SFX_FUEL_LOW + { 0x5A, 0x5A, 0x00, 0x00, 0x21, 0x10, 0xF9, 0x0A, 0x00 }, // ST_SFX_FUEL_FULL + { 0x80, 0x01, 0x96, 0x02, 0x41, 0x20, 0xF8, 0x0A, 0x00 }, // ST_SFX_LAND_HARD + { 0x22, 0x22, 0x00, 0x00, 0x81, 0x30, 0xFB, 0x03, 0x00 }, // ST_SFX_IMPACT + { 0x02, 0x02, 0xFF, 0x03, 0x41, 0x00, 0xF7, 0x03, 0x00 }, // ST_SFX_LAND_SOFT + { 0x50, 0x50, 0xFF, 0x06, 0x41, 0x50, 0xFB, 0x06, 0x00 }, // ST_SFX_CRASH +}; + +const uint8_t kStFlameCelByDirMask[16] = { + 0x00, 0xD8, 0xD4, 0x00, 0xD5, 0xD2, 0xD1, 0x00, 0xD7, 0xD3, 0xD6, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +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 +// data reference cannot cross a segment (only a 3-byte JSL/JML can). These +// tiny functions live in the same segment as the tables, so their reads +// are intra-segment; callers in other segments reach them by a supported +// inter-segment call. On the other ports they inline away to nothing. +// --------------------------------------------------------------------------- + +const uint8_t *stC64Charset(void) { + return &kStCharset[0][0]; +} + + +const uint8_t *stC64SpriteBitmap(uint8_t idx) { + return kStSpriteBitmaps[idx]; +} + + +uint8_t stC64RngByte(uint8_t idx) { + return kStRngTable[idx]; +} + + +const uint8_t *stC64SfxProgram(uint8_t idx) { + return kStSfxPrograms[idx]; +} + + +uint8_t stC64FlameCel(uint8_t dirMask) { + return kStFlameCelByDirMask[dirMask]; +} + + +int8_t stC64IntroStarDx(uint8_t idx) { + return kStIntroStarDx[idx]; +} + + +int8_t stC64IntroStarDy(uint8_t idx) { + return kStIntroStarDy[idx]; +} diff --git a/examples/spacetaxi/stC64Data.h b/examples/spacetaxi/stC64Data.h new file mode 100644 index 0000000..69751ab --- /dev/null +++ b/examples/spacetaxi/stC64Data.h @@ -0,0 +1,62 @@ +// Generated by assets/genC64Data.py from stuff/spacetaxi/raw.bin. +// Do not hand-edit; re-run the generator. + +#ifndef ST_C64_DATA_H +#define ST_C64_DATA_H + +#include + +// The game's custom character set ($2800-$2FFF): 256 glyphs x 8 rows, +// bit 7 = leftmost pixel. Used to paint the screen AND for the +// sprite-to-background collision test (a set bit is background data). +#define ST_CHARSET_CHARS 256u +extern const uint8_t kStCharset[ST_CHARSET_CHARS][8]; + +// Standard sprite bitmaps (VIC bank 0, block ptr * 64). 21 rows x 3 +// bytes. Index = ptr - ST_SPRITE_PTR_FIRST. +#define ST_SPRITE_PTR_FIRST 0xC0u +#define ST_SPRITE_PTR_LAST 0xECu +#define ST_SPRITE_COUNT 45u +#define ST_SPRITE_BYTES 63u +extern const uint8_t kStSpriteBitmaps[ST_SPRITE_COUNT][ST_SPRITE_BYTES]; + +// Demo-mode RNG lookup ($446A): rnd = table[t1] + t2 (see stSim.c). +#define ST_RNG_TABLE_SIZE 64u +extern const uint8_t kStRngTable[ST_RNG_TABLE_SIZE]; + +// SID SFX programs, 9 bytes each, in $42E9 order: freq lo, freq hi, +// pulse lo, pulse hi, control, AD, SR, release ticks, voice index. +typedef enum { + ST_SFX_GEAR, // $6410 landing gear toggle ($63DD) + ST_SFX_CASH, // $6DBB refuel ka-ching ($6E5B) + ST_SFX_FUEL_LOW, // $6DC4 fuel below 3 cells ($6EBD) + ST_SFX_FUEL_FULL, // $6DCD tank full ($6E9E) + ST_SFX_LAND_HARD, // $6DD6 hard landing ($6520) + ST_SFX_IMPACT, // $6DDF wreck hits the floor ($6AE1) + ST_SFX_LAND_SOFT, // $6DE8 soft landing ($650F) + ST_SFX_CRASH, // $6DF1 crash start ($6A56) + ST_SFX_COUNT +} StSfxE; +#define ST_SFX_PROGRAM_BYTES 9u +extern const uint8_t kStSfxPrograms[ST_SFX_COUNT][ST_SFX_PROGRAM_BYTES]; + +// Engine-flame cel pointer by direction mask ($6DB0, 1=UP 2=DOWN 4=LEFT +// 8=RIGHT); 0 = no cel for that combination. +extern const uint8_t kStFlameCelByDirMask[16]; + +// 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 +// by an inter-segment call on the IIgs, where a direct 2-byte data +// reference from another bank is not relocatable. +const uint8_t *stC64Charset(void); +const uint8_t *stC64SpriteBitmap(uint8_t idx); +uint8_t stC64RngByte(uint8_t idx); +const uint8_t *stC64SfxProgram(uint8_t idx); +uint8_t stC64FlameCel(uint8_t dirMask); +int8_t stC64IntroStarDx(uint8_t idx); +int8_t stC64IntroStarDy(uint8_t idx); + +#endif diff --git a/examples/spacetaxi/stDemoStreams.h b/examples/spacetaxi/stDemoStreams.h index dc71e8f..a4baf41 100644 --- a/examples/spacetaxi/stDemoStreams.h +++ b/examples/spacetaxi/stDemoStreams.h @@ -7,8 +7,8 @@ #include -// H: SPACETAXI 01_N0S.prg (296 bytes after trim) -static const uint8_t kDemoStreamH[296] = { +// H: SPACETAXI 01_N0S.prg (297 bytes) +static const uint8_t kDemoStreamH[297] = { 0x80, 0x17, 0x88, 0x0A, 0x80, 0x0A, 0x84, 0x04, 0x85, 0x03, 0x81, 0x03, 0x80, 0x0A, 0x88, 0x04, 0x80, 0x01, 0x81, 0x0E, 0x80, 0x0D, 0x82, 0x02, 0x80, 0x0A, 0x81, 0x09, 0x85, 0x07, 0x84, 0x05, 0x80, 0x02, 0x88, 0x05, @@ -33,11 +33,11 @@ static const uint8_t kDemoStreamH[296] = { 0x89, 0x12, 0x81, 0x08, 0x89, 0x01, 0x88, 0x07, 0x8A, 0x04, 0x82, 0x0F, 0x80, 0x07, 0x81, 0x04, 0x80, 0x03, 0x82, 0x03, 0x86, 0x09, 0x84, 0x08, 0x81, 0x03, 0x89, 0x06, 0x80, 0x01, 0x86, 0x08, 0x82, 0x01, 0x80, 0x03, - 0x88, 0x03, 0x80, 0x04, 0x95, 0x07, 0x85, 0x0A, + 0x88, 0x03, 0x80, 0x04, 0x95, 0x07, 0x85, 0x0A, 0x80, }; -// W: SPACETAXI 03_N0S.prg (334 bytes after trim) -static const uint8_t kDemoStreamW[334] = { +// W: SPACETAXI 03_N0S.prg (335 bytes) +static const uint8_t kDemoStreamW[335] = { 0x80, 0x0E, 0x82, 0x05, 0x8A, 0x01, 0x88, 0x0F, 0x89, 0x06, 0x81, 0x06, 0x85, 0x0D, 0x84, 0x07, 0x86, 0x0C, 0x82, 0x01, 0x88, 0x09, 0x89, 0x02, 0x81, 0x02, 0x80, 0x05, 0x88, 0x04, 0x80, 0x06, 0x84, 0x04, 0x80, 0x02, @@ -65,11 +65,11 @@ static const uint8_t kDemoStreamW[334] = { 0x94, 0x05, 0x84, 0x01, 0x80, 0x08, 0x84, 0x05, 0x80, 0x78, 0x81, 0x0D, 0x80, 0x07, 0x82, 0x06, 0x80, 0x2D, 0x88, 0x08, 0x8A, 0x02, 0x88, 0x0C, 0x80, 0x02, 0x84, 0x06, 0x85, 0x0F, 0x81, 0x02, 0x89, 0x04, 0x88, 0x07, - 0x8A, 0x0B, 0x82, 0x05, 0x86, 0x03, 0x84, 0x07, 0x85, 0x03, + 0x8A, 0x0B, 0x82, 0x05, 0x86, 0x03, 0x84, 0x07, 0x85, 0x03, 0x04, }; -// T: SPACETAXI 02_N0S.prg (374 bytes after trim) -static const uint8_t kDemoStreamT[374] = { +// T: SPACETAXI 02_N0S.prg (375 bytes) +static const uint8_t kDemoStreamT[375] = { 0x89, 0x0B, 0x88, 0x03, 0x80, 0x04, 0x86, 0x05, 0x84, 0x07, 0x80, 0x01, 0x81, 0x0A, 0x80, 0x06, 0x86, 0x02, 0x84, 0x02, 0x80, 0x09, 0x84, 0x04, 0x80, 0x04, 0x81, 0x01, 0x89, 0x04, 0x81, 0x01, 0x80, 0x0B, 0x84, 0x02, @@ -101,11 +101,11 @@ static const uint8_t kDemoStreamT[374] = { 0x82, 0x03, 0x80, 0x5D, 0x81, 0x08, 0x80, 0x05, 0x82, 0x05, 0x80, 0x04, 0x81, 0x01, 0x85, 0x02, 0x84, 0x07, 0x81, 0x04, 0x80, 0x06, 0x88, 0x06, 0x80, 0x0E, 0x81, 0x06, 0x80, 0x0D, 0x81, 0x0D, 0x84, 0x05, 0x85, 0x0F, - 0x81, 0x06, + 0x81, 0x06, 0x44, }; -// X: SPACETAXI 04_N0S.prg (564 bytes after trim) -static const uint8_t kDemoStreamX[564] = { +// X: SPACETAXI 04_N0S.prg (565 bytes) +static const uint8_t kDemoStreamX[565] = { 0x84, 0x07, 0x85, 0x06, 0x84, 0x06, 0x80, 0x0A, 0x81, 0x05, 0x80, 0x0C, 0x88, 0x07, 0x81, 0x05, 0x88, 0x08, 0x81, 0x07, 0x80, 0x07, 0x88, 0x04, 0x8A, 0x02, 0x88, 0x01, 0x80, 0x0A, 0x81, 0x07, 0x80, 0x08, 0x81, 0x07, @@ -153,6 +153,65 @@ static const uint8_t kDemoStreamX[564] = { 0x84, 0x04, 0x80, 0x0C, 0x84, 0x03, 0x80, 0x02, 0x81, 0x06, 0x80, 0x07, 0x81, 0x08, 0x89, 0x02, 0x88, 0x05, 0x80, 0x07, 0x84, 0x02, 0x86, 0x05, 0x80, 0x0D, 0x81, 0x0B, 0x80, 0x08, 0x88, 0x01, 0x89, 0x05, 0x80, 0x04, + 0xFF, +}; + +#define ST_DEMO_BUFFER_BYTES 640u +static const uint8_t kDemoBufferInit[ST_DEMO_BUFFER_BYTES] = { + 0x80, 0x17, 0x88, 0x0A, 0x80, 0x0A, 0x84, 0x04, 0x85, 0x03, 0x81, 0x03, + 0x80, 0x0A, 0x88, 0x04, 0x80, 0x01, 0x81, 0x0E, 0x80, 0x0D, 0x82, 0x02, + 0x80, 0x0A, 0x81, 0x09, 0x85, 0x07, 0x84, 0x05, 0x80, 0x02, 0x88, 0x05, + 0x8A, 0x04, 0x82, 0x03, 0x80, 0x05, 0x85, 0x10, 0x81, 0x08, 0x89, 0x02, + 0x88, 0x0E, 0x8A, 0x03, 0x88, 0x06, 0x81, 0x07, 0x80, 0x12, 0x82, 0x05, + 0x80, 0x04, 0x81, 0x11, 0x80, 0x04, 0x82, 0x04, 0x86, 0x09, 0x84, 0x02, + 0x95, 0x07, 0x85, 0x04, 0x81, 0x01, 0x80, 0x43, 0x81, 0x0A, 0x84, 0x05, + 0x86, 0x03, 0x82, 0x06, 0x80, 0x07, 0x81, 0x09, 0x80, 0x04, 0x88, 0x0B, + 0x80, 0x09, 0x84, 0x04, 0x80, 0x09, 0x81, 0x01, 0x89, 0x04, 0x80, 0x08, + 0x85, 0x04, 0x81, 0x01, 0x80, 0x05, 0x81, 0x15, 0x80, 0x0C, 0x82, 0x02, + 0x86, 0x07, 0x80, 0x05, 0x81, 0x09, 0x80, 0x13, 0x84, 0x01, 0x85, 0x03, + 0x80, 0x06, 0x88, 0x0B, 0x80, 0x16, 0x81, 0x0A, 0x80, 0x01, 0x88, 0x04, + 0x80, 0x0C, 0x81, 0x03, 0x85, 0x02, 0x84, 0x03, 0x85, 0x0C, 0x81, 0x01, + 0x80, 0x0D, 0x82, 0x07, 0x80, 0x06, 0x81, 0x02, 0x89, 0x0C, 0x88, 0x02, + 0x80, 0x05, 0x82, 0x04, 0x80, 0x0A, 0x81, 0x04, 0x85, 0x01, 0x84, 0x04, + 0x80, 0x0D, 0x81, 0x05, 0x89, 0x0E, 0x81, 0x01, 0x80, 0x11, 0x84, 0x17, + 0x80, 0x05, 0x81, 0x04, 0x80, 0x19, 0x81, 0x03, 0x89, 0x01, 0x88, 0x0E, + 0x80, 0x07, 0x84, 0x05, 0x80, 0x0C, 0x81, 0x0D, 0x80, 0x19, 0x88, 0x01, + 0x80, 0x02, 0x88, 0x04, 0x80, 0x1B, 0x88, 0x0A, 0x89, 0x0B, 0x81, 0x14, + 0x80, 0x02, 0x82, 0x03, 0x80, 0x05, 0x84, 0x03, 0x85, 0x09, 0x84, 0x05, + 0x85, 0x05, 0x84, 0x02, 0x86, 0x08, 0x84, 0x01, 0x80, 0x01, 0x81, 0x03, + 0x89, 0x12, 0x81, 0x08, 0x89, 0x01, 0x88, 0x07, 0x8A, 0x04, 0x82, 0x0F, + 0x80, 0x07, 0x81, 0x04, 0x80, 0x03, 0x82, 0x03, 0x86, 0x09, 0x84, 0x08, + 0x81, 0x03, 0x89, 0x06, 0x80, 0x01, 0x86, 0x08, 0x82, 0x01, 0x80, 0x03, + 0x88, 0x03, 0x80, 0x04, 0x95, 0x07, 0x85, 0x0A, 0x80, 0xA9, 0x01, 0x85, + 0xF8, 0x60, 0xA5, 0xF9, 0x38, 0xE5, 0xF8, 0x85, 0xF9, 0x30, 0x01, 0x60, + 0xE9, 0xF7, 0x85, 0xF9, 0xA2, 0x00, 0xBD, 0x81, 0x06, 0x9D, 0x80, 0x06, + 0xE8, 0xE0, 0x26, 0xD0, 0xF5, 0xAD, 0x00, 0x0B, 0xD0, 0x06, 0x20, 0x21, + 0x0A, 0x4C, 0x4B, 0x0A, 0xC9, 0x80, 0x90, 0x06, 0x29, 0x07, 0x85, 0xF8, + 0xA9, 0x20, 0x29, 0x7F, 0x8D, 0xA6, 0x06, 0xEE, 0x4C, 0x0A, 0xD0, 0x03, + 0xEE, 0x4D, 0x0A, 0x60, 0x00, 0x80, 0x6F, 0xCF, 0x00, 0x04, 0x20, 0x21, + 0x22, 0x0F, 0x6F, 0x7F, 0x9F, 0x7F, 0xCF, 0x7F, 0x18, 0xE0, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B, 0x10, 0x00, 0x00, 0x0F, + 0xC8, 0x07, 0x16, 0x0F, 0xF0, 0x00, 0x07, 0x07, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0D, 0x05, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, + 0x00, 0xFF, 0x00, 0x88, 0x88, 0x41, 0xF1, 0xA9, 0xFF, 0x02, 0x81, 0x87, + 0x41, 0xF0, 0xAA, 0x00, 0x03, 0x00, 0x00, 0x15, 0xF1, 0x69, 0x00, 0x11, + 0xF3, 0x1F, 0x01, 0xC6, 0x40, 0x01, 0x29, 0x40, 0x01, 0xCF, 0x40, 0x01, + 0x09, 0x40, 0x01, 0x09, 0x70, 0x01, 0x2E, 0x73, 0x01, 0xA4, 0x84, 0x01, + 0xE4, 0x64, 0x01, 0x64, 0x14, 0x01, 0x24, 0xE3, 0x09, 0x09, 0x02, 0x08, + 0x07, 0x01, 0x01, 0x07, 0x08, 0x02, 0x09, 0x09, 0x00, 0x00, 0x0B, 0x0B, + 0x05, 0x0F, 0x0D, 0x01, 0x01, 0x0D, 0x0F, 0x05, 0x0B, 0x0B, 0x00, 0x80, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x82, 0x23, 0x31, 0x37, 0x36, + 0x20, 0x2D, 0x20, 0x20, 0x53, 0x50, 0x41, 0x43, 0x45, 0x20, 0x54, 0x41, + 0x58, 0x49, 0x20, 0x2B, 0x39, 0x44, 0x48, 0x20, 0x28, 0x43, 0x29, 0x20, + 0x31, 0x39, 0x38, 0x34, 0x20, 0x4D, 0x55, 0x53, 0x45, 0x20, 0x53, 0x4F, + 0x46, 0x54, 0x57, 0x41, 0x52, 0x45, 0x2E, 0x20, 0x20, 0x43, 0x12, 0x01, + 0x03, 0x0B, 0x05, 0x04, 0x2C, 0x20, 0x4F, 0x0E, 0x05, 0x06, 0x09, 0x0C, + 0x05, 0x04, 0x20, 0x26, 0x20, 0x54, 0x12, 0x01, 0x09, 0x0E, 0x05, 0x04, + 0x20, 0x02, 0x19, 0x20, 0x46, 0x15, 0x0E, 0x07, 0x15, 0x13, 0x20, 0x26, + 0x20, 0x36, 0x52, 0x36, 0x2E, 0x20, 0x20, 0x20, 0x20, 0x44, 0x09, 0x13, + 0x0B, 0x20, 0x0F, 0x12, 0x09, 0x07, 0x09, 0x0E, 0x01, 0x0C, 0x20, 0x01, + 0x0E, 0x04, 0x20, 0x04, 0x0F, 0x03, 0x15, 0x0D, 0x05, 0x0E, 0x14, 0x13, + 0x20, 0x13, 0x15, 0x10, }; typedef struct { @@ -162,10 +221,10 @@ typedef struct { } StDemoEntryT; static const StDemoEntryT kDemoRotation[4] = { - { kDemoStreamH, 296u, 7u }, - { kDemoStreamW, 334u, 22u }, - { kDemoStreamT, 374u, 19u }, - { kDemoStreamX, 564u, 23u }, + { kDemoStreamH, 297u, 7u }, + { kDemoStreamW, 335u, 22u }, + { kDemoStreamT, 375u, 19u }, + { kDemoStreamX, 565u, 23u }, }; #endif diff --git a/examples/spacetaxi/stEngine.c b/examples/spacetaxi/stEngine.c deleted file mode 100644 index 2c766c2..0000000 --- a/examples/spacetaxi/stEngine.c +++ /dev/null @@ -1,456 +0,0 @@ -// Space Taxi -- taxi physics. -// -// Faithful translation of the C64's $6032 per-frame physics routine -// (see MECHANICS.md "Per-frame algorithm" and "Velocity model -// (CORRECTED)"). Acceleration-based: stick deflection sets an INSTANT -// per-frame acceleration; gravity is a constant acceleration always -// added (Y, signed -- level K is anti-gravity); both feed a persistent -// velocity accumulator that integrates into position each frame. -// Release the stick and velocity persists -- the cab drifts. -// -// Edge handling MATCHES the C64's $6AED behaviour: vx reflects at -// column 23 going left or column 65 going right. The cab BOUNCES off -// the screen edges rather than stopping. -// -// Crash detection: any contact with a solid non-pad tile crashes the -// cab. Any contact with a pad-surface tile is a successful landing. -// The C64 game does the same via sprite-bg hardware collision plus -// the $7D75 trampoline predicate; we substitute explicit pad-vs-wall -// tile classification. -// -// Per-level physics templates ($7D8F-$7D96 in the C64; xAccel/yAccel/ -// xGrav/yGrav fields on StLevelT in the port) make every level feel -// different -- accel ranges $0E..$40, Y-gravity ranges $F9 (-7, -// anti-gravity on level K) to $06 (heavy on levels D/E/H/M/etc.). - -#include - -#include "spacetaxi.h" - - -// Velocity safety clamp. With ST_SUBPIXEL = 256, ST_MAX_VX = 512 caps -// the cab at ~2 px/frame which is in line with the C64 max effective -// velocity (the C64's int16 accumulator integrates accel=14..64 over -// many frames; left uncapped it'd wrap, but real-game effective speeds -// stay in the hundreds-of-sub-units range). Earlier 127 capped at -// 0.5 px/frame, making the cab feel glued to molasses. -#define ST_MAX_VX 512 -#define ST_MAX_VY 512 - -// Initial downward velocity injected at crash. The C64 sets $714F = -// $03 and $714E = $03 at $6A4E-$6A53 -- a 16-bit vy of $0303 = 771 -// sub-units (~3 px/frame). The port's ST_MAX_VY caps the effective -// max, so we just start at that ceiling for a strong opening fall; -// gravity (the level's own yGrav) then integrates normally during -// the death anim. On anti-gravity levels (K = -7) the initial high -// downward velocity dominates for many frames before being slowed -// or reversed -- matches the C64's behavior on those levels. -#define ST_CRASH_FALL_VY ST_MAX_VY - -#define ST_TAXI_W_PX 24 -#define ST_TAXI_H_PX 24 - -// Death-animation duration, in GAME TICKS (the death countdown runs -// inside stEngineTick, now stepped at the fixed 30 Hz cadence). Matches -// the C64 sequence at $6A72/$6B24/$6B4C: phase-1 integrates Y velocity -// until the cab falls to the floor (row >= $DA), phase-2 walks cels -// $CC..$D1 at a slowing rate, phase-3 holds ~70 video frames (~35 game -// ticks). 60 game ticks (~2 s) comfortably covers fall + impact walk + -// hold. The cab keeps falling under gravity during the countdown so the -// visual matches the original "watch it drop". -#define ST_CRASH_ANIM_FRAMES 60u - -// Edge reflection at the visible playfield edges. The C64's $6AED -// thresholds (col 23 left / col 65 in X-MSB high half) are in VIC -// sprite-X coordinates where X=24 is the left visible column -- so -// "col 23" means "cab leftedge at visible left edge". Our port has -// no VIC-X offset (X=0 IS the visible left edge), so the equivalent -// is "bounce when the cab tries to leave the visible playfield". - - -static bool isSolidAt(const StLevelT *level, int16_t px, int16_t py); -static bool onLandingPad(const StLevelT *level, int16_t px, int16_t py, uint8_t *outPad); -static void reflectAtEdges(StTaxiT *t); -static void respawnTaxi(StGameT *game); - - -static bool isSolidAt(const StLevelT *level, int16_t px, int16_t py) { - int16_t tx = (int16_t)(px / ST_TILE_PIXELS); - int16_t ty = (int16_t)(py / ST_TILE_PIXELS); - uint8_t tile; - - if (tx < 0 || tx >= (int16_t)ST_TILEMAP_W) { - return true; // off-screen sides treated as walls - } - if (ty < 0 || ty >= (int16_t)ST_PLAYFIELD_ROWS) { - return true; // top/bottom out-of-field treated as walls - } - tile = level->tilemap[ty * ST_TILEMAP_W + tx]; - // The tilemap holds raw C64 screen-RAM character codes (romToLevel.py - // copies screen RAM directly; the empty cell is the space character - // $20, BG=0x20 in the extractor -- confirmed by the .dat histogram - // where char 32 dominates the tilemap). On the C64 a sprite-vs- - // background collision ($D01F) latches on ANY non-blank background - // cell the cab overlaps, so a cell is solid iff its char code is not - // the space character. (The earlier "1..63 solid / 64..127 pad / - // 128+ decorative" convention was invented and is wrong against the - // real data -- it treated char 32, the empty playfield, as solid, - // which crashes the cab the instant it descends into open air.) - // - // The transporter opening ($67, the 4-tile top-wall gap) is the one - // non-space tile the cab may pass through: it's the screen exit. It - // only ever occurs at row 0 cols 18..21, so treating it as passable - // everywhere is safe. - return tile != ST_TILE_EMPTY && tile != ST_TILE_TRANSPORTER; -} - - -static bool onLandingPad(const StLevelT *level, int16_t px, int16_t py, uint8_t *outPad) { - uint8_t i; - uint8_t tx; - uint8_t ty; - - tx = (uint8_t)(px / ST_TILE_PIXELS); - ty = (uint8_t)(py / ST_TILE_PIXELS); - for (i = 0u; i < level->padCount; i++) { - const StPadT *p = &level->pads[i]; - if (ty == p->tileY && tx >= p->tileX && tx < (uint8_t)(p->tileX + p->tileW)) { - if (outPad != NULL) { - *outPad = i; - } - return true; - } - } - return false; -} - - -static void reflectAtEdges(StTaxiT *t) { - // C64 $6AED behavior translated to the port's 0..319 visible - // playfield: when vx is negative and the cab leftedge reaches 0, - // negate vx. Same on the right when leftedge reaches (320 - W). - // The cab visually touches the screen edge and bounces back; - // matches the C64's "screen-edge bumper" behavior. Y has no - // bounce -- the cab just stops at top/bottom (no real level - // throws the cab against those edges). - int32_t maxX = ((int32_t)ST_TILEMAP_W * ST_TILE_PIXELS - ST_TAXI_W_PX) * ST_SUBPIXEL; - int32_t maxY = ((int32_t)ST_PLAYFIELD_ROWS * ST_TILE_PIXELS - ST_TAXI_H_PX) * ST_SUBPIXEL; - - if (t->x < 0 && t->vx < 0) { - t->vx = (int16_t)(-t->vx); - t->x = 0; - } - if (t->x > maxX && t->vx > 0) { - t->vx = (int16_t)(-t->vx); - t->x = maxX; - } - if (t->y < 0) { t->y = 0; t->vy = 0; } - if (t->y > maxY) { t->y = maxY; t->vy = 0; } -} - - -static void respawnTaxi(StGameT *game) { - StTaxiT *t = &game->taxi; - int32_t spawnX; - int32_t spawnY; - uint8_t i; - - spawnX = (int32_t)game->level.taxiSpawnTileX * ST_TILE_PIXELS * ST_SUBPIXEL; - spawnY = (int32_t)game->level.taxiSpawnTileY * ST_TILE_PIXELS * ST_SUBPIXEL; - t->x = spawnX; - t->y = spawnY; - t->vx = 0; - t->vy = 0; - t->landed = false; - t->onPad = 0xFFu; - t->thrusting = false; - // $6892 forces #$C0 on every respawn: gear up, facing right. - t->gearDown = false; - t->fireHeld = false; - t->facing = ST_DIR_RIGHT; - t->crashImpacted = false; - // Boot any in-flight passenger out of the cab; a respawn doesn't - // keep the fare. Waiting passengers (still on a pad) survive. - for (i = 0u; i < ST_MAX_PASSENGERS; i++) { - if (game->passengers[i].active && game->passengers[i].onboard) { - game->passengers[i].active = false; - game->passengers[i].onboard = false; - } - } -} - - -void stEngineReset(StGameT *game) { - StTaxiT *t = &game->taxi; - const StLevelT *L = &game->level; - - memset(t, 0, sizeof(*t)); - t->x = (int32_t)L->taxiSpawnTileX * ST_TILE_PIXELS * ST_SUBPIXEL; - t->y = (int32_t)L->taxiSpawnTileY * ST_TILE_PIXELS * ST_SUBPIXEL; - t->facing = ST_DIR_RIGHT; - t->onPad = 0xFFu; - - // Delegated to stPassenger.c so the fare-cursor (gNextFareIdx) and - // the seed-spawn share one code path. Previously stEngineReset - // spawned fares[0] inline without advancing the cursor, which then - // caused spawnNextFare to re-spawn fare 0 instead of fare 1. - stPassengerReset(game); -} - - -void stEngineTick(StGameT *game) { - StTaxiT *t = &game->taxi; - const StLevelT *L = &game->level; - int32_t nx; - int32_t ny; - int16_t ax; - int16_t ay; - int16_t centerX; - int16_t feetY; - uint8_t landingPad; - - // Death animation: mirror the C64 sequence -- the cab keeps - // FALLING through phase 1 ($6A72) until it hits the floor, then - // sits there through phases 2/3 before respawn. No input accepted, - // no further crash checks (otherwise hitting the floor on the way - // down would re-trigger). Mute thrust input so applyInput's - // per-frame joystick read can't keep the SFX going. - // - // Uses the LEVEL's normal gravity during integration -- the C64 - // doesn't override gravity at crash, it just injects a high - // downward vy at crash entry and then lets normal physics run. - // On level K (anti-gravity = -7) the high initial vy dominates - // for many frames before being slowed/reversed; matches C64. - if (t->crashTicks > 0u) { - int32_t maxY = ((int32_t)ST_PLAYFIELD_ROWS * ST_TILE_PIXELS - - ST_TAXI_H_PX) * ST_SUBPIXEL; - - t->thrusting = false; - t->vy = (int16_t)(t->vy + (int16_t)L->yGrav); - if (t->vy > ST_MAX_VY) { t->vy = ST_MAX_VY; } - if (t->vy < -ST_MAX_VY) { t->vy = -ST_MAX_VY; } - t->y += t->vy; - if (t->y > maxY) { - // Floor hit: the C64's phase-1 -> phase-2 transition (row - // $DA, $6ACC) -- the debris stops falling and the impact - // cel walk plays out where it landed. - t->y = maxY; - t->vy = 0; - t->crashImpacted = true; - } - t->crashTicks--; - if (t->crashTicks == 0u) { - if (game->state == ST_STATE_DEMO) { - // $6B57-$6B5E: a demo crash skips the life bookkeeping - // entirely; the main loop returns to the title. - game->demoEnded = true; - return; - } - if (game->lives > 0u) { - game->lives--; - } - if (game->lives == 0u) { - game->state = ST_STATE_GAME_OVER; - } else { - respawnTaxi(game); - } - } - return; - } - - // Step 1: instant acceleration from stick. - ax = (int16_t)((int16_t)t->thrustDx * (int16_t)L->xAccel); - ay = (int16_t)((int16_t)t->thrustDy * (int16_t)L->yAccel); - if (!t->thrusting) { - ax = 0; - ay = 0; - } - - // Step 2: integrate accel + per-level gravity into velocity. - // Gravity is int8 signed so a level can pull upward (level K). - t->vx = (int16_t)(t->vx + ax + (int16_t)L->xGrav); - t->vy = (int16_t)(t->vy + ay + (int16_t)L->yGrav); - - // Safety clamp on the velocity accumulator -- without this a long - // fall under gravity (or sustained one-way thrust against no - // collision) lets vx/vy wrap int16_t and reverse sign. - if (t->vx > ST_MAX_VX) { t->vx = ST_MAX_VX; } - if (t->vx < -ST_MAX_VX) { t->vx = -ST_MAX_VX; } - if (t->vy > ST_MAX_VY) { t->vy = ST_MAX_VY; } - if (t->vy < -ST_MAX_VY) { t->vy = -ST_MAX_VY; } - - nx = t->x + t->vx; - ny = t->y + t->vy; - - // Wall collision (X-axis): sample the cab's leading edge at three - // Y rows (top, middle, bottom). If any sample hits a solid cell, - // undo X movement and zero vx so the cab stops against the wall. - { - int16_t leadX = (t->vx > 0) - ? (int16_t)((nx >> ST_SUBPIXEL_SHIFT) + ST_TAXI_W_PX - 1) - : (int16_t)(nx >> ST_SUBPIXEL_SHIFT); - int16_t topY = (int16_t)(t->y >> ST_SUBPIXEL_SHIFT); - int16_t midY = (int16_t)(topY + ST_TAXI_H_PX / 2); - int16_t botY = (int16_t)(topY + ST_TAXI_H_PX - 1); - if (isSolidAt(L, leadX, topY) || - isSolidAt(L, leadX, midY) || - isSolidAt(L, leadX, botY)) { - nx = t->x; - t->vx = 0; - } - } - - // Y-axis collision. The C64 game has no velocity threshold: any - // sprite-vs-background contact crashes the cab unless the cab is - // touching a pad surface (in which case it's a successful landing - // regardless of descent speed). We do the same via tile lookup. - feetY = (int16_t)((ny >> ST_SUBPIXEL_SHIFT) + ST_TAXI_H_PX - 1); - centerX = (int16_t)((nx >> ST_SUBPIXEL_SHIFT) + ST_TAXI_W_PX / 2); - - if (t->vy > 0) { - if (isSolidAt(L, centerX, feetY)) { - // The C64 pad detector ($645C) only registers a landing on a - // SLOW descent: it gates on the Y-velocity high byte being - // zero (vy < 1 px/frame). A fast descent fails the gate, so - // the pad surface acts as solid background and the cab - // crashes into it -- this is the "land gently or die" - // mechanic. In the port's 8-bit sub-pixel scale that high- - // byte-zero test is vy < ST_SUBPIXEL (256). - // $6462 additionally requires the GEAR DOWN: with the gear - // up the pad detector aborts and the pad surface is just - // lethal background -- fast descent OR gear-up = crash. - bool slowDescent = (t->vy < ST_SUBPIXEL); - if (slowDescent && t->gearDown && - onLandingPad(L, centerX, feetY, &landingPad)) { - ny = (int32_t)(feetY / ST_TILE_PIXELS) * ST_TILE_PIXELS * ST_SUBPIXEL - - (int32_t)ST_TAXI_H_PX * ST_SUBPIXEL; - t->vx = 0; - t->vy = 0; - t->landed = true; - t->onPad = landingPad; - } else { - stAudioSfxCrash(); - // Enter the falling-death state. Zero vx so the cab - // doesn't drift sideways during the fall, and inject - // a high downward vy matching the C64's $6A4E-$6A53 - // setup (which writes $0303 = 771 sub-units into - // $714E/$714F regardless of pre-impact state). The - // crashTicks gate at the top of stEngineTick handles - // gravity integration + respawn from here. - t->vx = 0; - t->vy = ST_CRASH_FALL_VY; - t->thrusting = false; - t->crashTicks = ST_CRASH_ANIM_FRAMES; - t->crashImpacted = false; - } - } else { - t->landed = false; - t->onPad = 0xFFu; - } - } else if (t->vy < 0) { - // Ascending: head hits ceiling? Stop vertical motion, leave - // horizontal alone so the cab can drift along the underside. - int16_t headY = (int16_t)(ny >> ST_SUBPIXEL_SHIFT); - if (isSolidAt(L, centerX, headY)) { - ny = t->y; - t->vy = 0; - } - t->landed = false; - } - - t->x = nx; - t->y = ny; - reflectAtEdges(t); -} - - -bool stEngineInTransporter(const StGameT *game) { - const StTaxiT *t = &game->taxi; - int16_t topY; - int16_t centerCol; - - // The cab has exited when its top has risen into row 0 while its - // center sits within the transporter columns (18..21). The opening - // tile ($67) is passable (see isSolidAt), so the cab can ascend the - // empty channel and reach row 0 only through the gap -- the walls - // stop it everywhere else. - topY = (int16_t)(t->y >> ST_SUBPIXEL_SHIFT); - centerCol = (int16_t)(((t->x >> ST_SUBPIXEL_SHIFT) + ST_TAXI_W_PX / 2) - / ST_TILE_PIXELS); - // Fire once the cab has risen into the opening (top within rows 0-1). - // The walls block every column but the 18-21 gap, so reaching here at - // all means the cab is exiting through the transporter. - return topY < (int16_t)(2 * ST_TILE_PIXELS) && - centerCol >= (int16_t)ST_TRANSPORTER_COL_LO && - centerCol < (int16_t)ST_TRANSPORTER_COL_HI; -} - - -// Transporter shrink-warp ($5C91 takeoff + $5CB9 pad2pad). The C64 walks -// the cab sprite through shrink cels $E2..$E9 (8 cels / 7 steps) while -// nudging it +1px right / -1px up per step, with a cadence that slows as -// it recedes. $5CB9 INCs $5A66 and gates the step on (counter & mask): -// mask $03 (every 4) for the first steps, then $0F (every 16) from $E5. -// -// UNIT PROOF (from raw.bin, so this is not re-litigated): the animation -// runs in the loop at $5B85 -- JSR $5BBB (the animDispatcher, sole caller, -// which advances $5A66 via $5CB9) is called ONCE per iteration, while the -// single-frame raster wait $404B (LDA $D019 / AND #$01 / BEQ on line 252) -// runs TWICE per iteration ($5BAC + $5BB2), exactly like the gameplay -// loop's $5F94/$5FA3. So $5A66 advances once per 2 video frames = one -// GAME TICK. The boundaries 4,8,12,16,32,48,64 and the total 64 are -// therefore GAME TICKS: 64 ticks = 128 video frames = ~2.13 s NTSC. The -// values stay in their native C64 units (stEngineWarpTick runs once per -// game tick), unlike the port-invented crashTicks. The port has no shrink -// cels, so the renderer scales a block by stEngineWarpStep instead. -#define ST_WARP_TOTAL_FRAMES 64u - -static const uint8_t kWarpStepFrame[8] = { 0u, 4u, 8u, 12u, 16u, 32u, 48u, 64u }; - - -uint8_t stEngineWarpStep(const StGameT *game) { - uint8_t frame = game->taxi.warpFrame; - uint8_t step = 0u; - uint8_t k; - - for (k = 0u; k < 8u; k++) { - if (frame >= kWarpStepFrame[k]) { - step = k; - } - } - return step; -} - - -void stEngineStartWarp(StGameT *game) { - game->taxi.warpFrame = 1u; - game->taxi.vx = 0; - game->taxi.vy = 0; - game->taxi.thrusting = false; - // $5CB0: the C64 gates SID voice-3 noise on ($D412 = $81) for the - // warp. Voice 3's freq word is $0617 (set at the $4092 RNG init, - // ~92 Hz shift rate) -- the darkest rumble, portable pitch 31. - jlAudioNoise(31u, 4u); -} - - -bool stEngineWarpTick(StGameT *game) { - StTaxiT *t = &game->taxi; - uint8_t before; - uint8_t after; - - before = stEngineWarpStep(game); - t->warpFrame++; - after = stEngineWarpStep(game); - // Nudge +1px right / -1px up on each step boundary (net +7/-7). - if (after != before) { - t->x += (int32_t)ST_SUBPIXEL; - t->y -= (int32_t)ST_SUBPIXEL; - } - if (t->warpFrame >= (uint8_t)ST_WARP_TOTAL_FRAMES) { - t->warpFrame = 0u; - jlAudioNoise(0u, 15u); // warp done: rumble off - return true; - } - return false; -} diff --git a/examples/spacetaxi/stFare.c b/examples/spacetaxi/stFare.c new file mode 100644 index 0000000..99e1d76 --- /dev/null +++ b/examples/spacetaxi/stFare.c @@ -0,0 +1,451 @@ +// Space Taxi -- the passenger / fare state machine ($65C1 dispatch on +// $7163, the $67E3 post-tick gate and the pad reservation table). +// +// One passenger at a time: they beam in on a free pad, wave, walk to a +// cab parked on their pad, shrink into it, ride, and reverse the whole +// sequence at the destination. Every step runs on the shared 3-tick +// decay timer. Pads get reserved as fares use them; once every fare +// pad has been used the destination becomes "UP PLEASE" and the top +// hatch opens. + +#include "stSim.h" + +// Screen-code strings ($6C1E..$6C7D). All ASCII-compatible glyphs. +static const uint8_t kTextPadPlease[] = { 0x50, 0x41, 0x44, 0x20, 0x20, 0x20, 0x50, 0x4C, 0x45, 0x41, 0x53, 0x45, 0 }; // $6C1E "PAD PLEASE" +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 kTextBlank[] = { 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0 }; // $6C38 +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[] = { 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. +static const uint8_t kSpeechHeyTaxi[] = { 0x48, 0x54, 0 }; // $6C6C "HT" +static const uint8_t kSpeechUp[] = { 0x55, 0x3F, 0 }; // $6C6F "U?" +static const uint8_t kSpeechPad[] = { 0x50, 0x58, 0x3F, 0 }; // $6C72 "PX?" (X = digit) +static const uint8_t kSpeechThanks[] = { 0x21, 0 }; // $6C76 "!" +static const uint8_t kSpeechHey[] = { 0x48, 0 }; // $6C78 "H" +// Waiting wave cycle ($66D6) and the base-fare blob ($43B9 = +5.00). +static const uint8_t kWaveCels[4] = { 0xC6, 0xC7, 0xD9, 0xC7 }; +static const uint8_t kFareBlob[7] = { 0x66, 0x66, 0x66, 0x6F, 0x77, 0x74, 0x74 }; + + +static void beamIn(StSimT *sim); +static bool decayStep(StSimT *sim); +static void idleTicker(StSimT *sim); +static void lightOnAndLatch(StSimT *sim); +static void newFare(StSimT *sim); +static void stageBoard(StSimT *sim); +static void stageCleanup(StSimT *sim); +static void stageDropIn(StSimT *sim); +static void walkToTarget(StSimT *sim); + + +// $665F -- the passenger grows in ($CB down to $C7); a crash during +// it cancels the fare. +static void beamIn(StSimT *sim) { + if (!decayStep(sim)) { + return; + } + sim->spr[1].ptr--; + if (sim->spr[1].ptr != 0xC7u) { + return; + } + if (sim->collisionPhase != 0u) { + sim->stage = ST_STAGE_LEAVE; + sim->spr[1].ptr = 0xC7u; + sim->spriteSlots[sim->activeSpriteIdx] = 0u; + sim->activeSpriteIdx = 0u; + sim->fareSlotCount--; + return; + } + sim->stage++; + stSimDrawText(sim, 14u, 24u, kTextHeyTaxi, 1u); + stFareBoardingSound(sim, kSpeechHeyTaxi); + // $43D1: the meter starts at $10.00..$19.00. + { + 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, 34u, 24u, (uint8_t)(ST_CHAR_DIGIT_BASE + 1u)); + } +} + + +// The shared 3-tick cadence: true on the tick a step is due. +static bool decayStep(StSimT *sim) { + sim->decayTimer--; + if (sim->decayTimer != 0u) { + return false; + } + sim->decayTimer = sim->decayReload; + return true; +} + + +// $660B -- a new fare turns up on a 3% roll per tick or when the +// 100-tick timer runs out, never mid-crash. +static void idleTicker(StSimT *sim) { + if (stSimRng(sim, 100u) >= 3u) { + sim->stage0Rng--; + if (sim->stage0Rng != 0u) { + return; + } + } + if (sim->collisionPhase != 0u) { + return; + } + newFare(sim); +} + + +// $6808 -- the pad lights up and stays "busy". +static void lightOnAndLatch(StSimT *sim) { + sim->deathInProgress = 1u; + stSimPadLight(sim, true); +} + + +// $6620 -- pick a free pad, stand the passenger there, start beaming. +static void newFare(StSimT *sim) { + uint8_t x; + const StPadT *pad; + + sim->stage0Rng = 0x64u; + x = stFareChooseDestination(sim); + // $6628: X-1 selects the slot; a garbage X (all pads used) reads + // past the table on the C64 -- clamp to the last pad here. + if (x == 0u || x > ST_MAX_PADS) { + x = ST_MAX_PADS; + } + pad = &sim->pads[x - 1u]; + sim->spr[1].msb = pad->passMsb; + sim->spr[1].col = pad->passCol; + sim->spr[1].row = pad->row; + sim->spr[1].enable = 1u; + sim->stage = ST_STAGE_BEAM_IN; + sim->decayTimer = sim->decayReload; + sim->spr[1].ptr = 0xCBu; + stAudioSpeech((uint8_t)(stSimRng(sim, 3u) + 2u)); +} + + +// $66DD -- the passenger shrinks into the cab, then picks where to go. +static void stageBoard(StSimT *sim) { + if (!decayStep(sim)) { + return; + } + sim->spr[1].ptr++; + if (sim->spr[1].ptr != 0xCCu) { + return; + } + sim->spr[1].enable = 0u; + sim->stage++; + sim->activeDyingSlot = sim->activePad; + (void)stFareChooseDestination(sim); + if (sim->activeSpriteIdx == 0x0Bu) { + return; + } + stSimDrawText(sim, 14u, 24u, kTextPadPlease, 1u); + stSimPutChar(sim, 18u, 24u, (uint8_t)(sim->activeSpriteIdx + 0x30u)); + { + uint8_t speech[4]; + speech[0] = kSpeechPad[0]; + speech[1] = (uint8_t)(sim->activeSpriteIdx + 0x30u); + speech[2] = kSpeechPad[2]; + speech[3] = 0u; + stFareBoardingSound(sim, speech); + } +} + + +// $67C6 -- back to idle. +static void stageCleanup(StSimT *sim) { + sim->deathInProgress = 0u; + sim->stage = ST_STAGE_IDLE; + stSimPadLight(sim, false); + stSimDrawText(sim, 14u, 24u, kTextBlank, 1u); +} + + +// $6742 -- the passenger materialises beside the cab; the fare pays +// out: $5.00 plus whatever is left on the meter (twice the base when +// no pads are reserved). +static void stageDropIn(StSimT *sim) { + if (!decayStep(sim)) { + return; + } + sim->spr[1].ptr--; + if (sim->spr[1].ptr != 0xC7u) { + return; + } + sim->stage++; + stSimDrawText(sim, 14u, 24u, kTextThanks, 1u); + stFareBoardingSound(sim, kSpeechThanks); + stSimBcdAdd(sim, ST_CELL_SCORE, kFareBlob); + stSimBcdAdd(sim, ST_CELL_SCORE, &sim->screen[ST_CELL_FARE]); + { + 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) { + return; + } + stSimBcdAdd(sim, ST_CELL_SCORE, kFareBlob); +} + + +// $67A6 -- the passenger shrinks away at the stand (also intro stage 4). +void stFareLeaveStep(StSimT *sim) { + if (!decayStep(sim)) { + return; + } + sim->spr[1].ptr++; + if (sim->spr[1].ptr != 0xCCu) { + return; + } + sim->spr[1].enable = 0u; + sim->stage++; +} + + +// $66B7 -- waving on the pad (also the title's sparkle cycle). +void stFareWaveStep(StSimT *sim) { + if (!decayStep(sim)) { + return; + } + sim->waveIdx = (uint8_t)((sim->waveIdx + 1u) & 3u); + sim->spr[1].ptr = kWaveCels[sim->waveIdx]; +} + + +// $6D0D -- one two-pixel stride toward the hover target, cel +// alternating each step (also the title's walk-in). +void stFareWalkStep(StSimT *sim) { + int16_t passX; + int16_t targetX; + uint16_t sum; + + sim->walkParity ^= 1u; + passX = (int16_t)(((int16_t)sim->spr[1].msb << 8) | sim->spr[1].col); + targetX = (int16_t)(((int16_t)sim->hoverXFrac << 8) | sim->hoverXCol); + if ((int16_t)(passX - targetX) < 0) { + // $6D30: walk right, cels $C2/$C3. + sim->spr[1].ptr = (uint8_t)(0xC2u + sim->walkParity); + sum = (uint16_t)((uint16_t)sim->spr[1].col + 2u); + sim->spr[1].col = (uint8_t)sum; + sim->spr[1].msb = (uint8_t)(sim->spr[1].msb + (uint8_t)(sum >> 8)); + return; + } + // $6D4B: walk left, cels $C4/$C5. + sim->spr[1].ptr = (uint8_t)(0xC4u + sim->walkParity); + sum = (uint16_t)((uint16_t)sim->spr[1].col + 0xFEu); + sim->spr[1].col = (uint8_t)sum; + sim->spr[1].msb = (uint8_t)(sim->spr[1].msb + 0xFFu + (uint8_t)(sum >> 8)); +} + + +// $6CE8 -- stages 3 and 7: walk until the column matches (even +// pixels), then stand and advance. +static void walkToTarget(StSimT *sim) { + if (!decayStep(sim)) { + return; + } + if ((sim->spr[1].col & 0xFEu) == (sim->hoverXCol & 0xFEu)) { + sim->spr[1].ptr = 0xC7u; + sim->stage++; + return; + } + stFareWalkStep(sim); +} + + +// --------------------------------------------------------------------------- +// Public +// --------------------------------------------------------------------------- + +// $6C7F -- the passenger speaks: one sample per character. +void stFareBoardingSound(StSimT *sim, const uint8_t *speech) { + (void)sim; + stAudioSilence(); + stAudioNoise(false); + while (*speech != 0u) { + stAudioSpeech(*speech); + speech++; + } +} + + +// $6537 -- reserve a free fare pad by RNG (linear probe on collision), +// or "UP PLEASE" once every fare pad is taken. Returns the pad number. +uint8_t stFareChooseDestination(StSimT *sim) { + uint8_t special = sim->level->specialPad; + uint8_t x; + + if (sim->fareSlotCount == special) { + stFareUpPlease(sim); + return special; + } + x = stSimRng(sim, special); + while (sim->spriteSlots[x] != 0u) { + if (x == special) { + x = 1u; + } else { + x++; + } + } + sim->fareSlotCount++; + sim->activeSpriteIdx = x; + sim->spriteSlots[x] = 1u; + return x; +} + + +// $6EF0 -- while the passenger walks (to or from the cab) the pad stays +// lit only while the two sprites touch. +void stFarePadLightingGate(StSimT *sim) { + if (sim->stage != ST_STAGE_WALK_TO_CAB && sim->stage != ST_STAGE_WALK_TO_PAD && sim->stage != ST_STAGE_LEAVE) { + return; + } + if (sim->activePad == 0u) { + return; + } + sim->deathInProgress = (uint8_t)(sim->spriteSpriteColl & 2u); + stSimPadLight(sim, sim->deathInProgress != 0u); +} + + +// $67E3 -- landed on the fare's pad: hand the passenger over. +void stFarePostTickGate(StSimT *sim) { + if (sim->activePad == 0u) { + return; + } + if (sim->activePad != sim->activeSpriteIdx) { + return; + } + switch (sim->stage) { + case ST_STAGE_IDLE: + case ST_STAGE_BEAM_IN: + case ST_STAGE_WALK_TO_CAB: + case ST_STAGE_WALK_TO_PAD: + return; + case ST_STAGE_WAIT: + // $6811: remember where the cab parked. + sim->stage++; + sim->hoverXFrac = sim->spr[0].msb; + sim->hoverXCol = sim->spr[0].col; + lightOnAndLatch(sim); + return; + case ST_STAGE_RIDING: { + const StPadT *pad; + if (sim->bobTimer != 0u) { + return; + } + sim->stage++; + sim->spr[1].enable = 1u; + sim->spr[1].msb = sim->spr[0].msb; + sim->spr[1].col = sim->spr[0].col; + sim->spr[1].row = sim->spr[0].row; + sim->spr[1].ptr = 0xCBu; + sim->decayTimer = sim->decayReload; + pad = &sim->pads[sim->activeSpriteIdx - 1u]; + sim->hoverXFrac = pad->passMsb; + sim->hoverXCol = pad->passCol; + lightOnAndLatch(sim); + return; + } + default: + lightOnAndLatch(sim); + return; + } +} + + +// $69B4 -- the cab touched the passenger: he vanishes, the fare is +// lost and the meter is docked ten dollars. +void stFareSquashed(StSimT *sim) { + if (sim->stage >= ST_STAGE_LEAVE) { + return; + } + if (sim->stage < ST_STAGE_DROP_IN) { + sim->spriteSlots[sim->activeSpriteIdx] = 0u; + sim->activeSpriteIdx = 0u; + sim->fareSlotCount--; + stSimDrawText(sim, 14u, 24u, kTextBlank, 1u); + } + sim->stage = ST_STAGE_LEAVE; + stFareBoardingSound(sim, kSpeechHey); + // $43F3: a hundred dime decrements. + { + uint8_t k; + for (k = 0u; k < 100u; k++) { + (void)stSimDecrementNumber(sim, ST_CELL_SCORE, 5u); + } + // $43F3 counts down in the input-mask byte and leaves it zero. + sim->inputMask = 0u; + } + sim->spr[1].ptr = 0xC7u; + { + 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]); + } + } +} + + +// $65C1 -- run the current stage. +void stFareStageDispatch(StSimT *sim) { + switch (sim->stage) { + case ST_STAGE_IDLE: + idleTicker(sim); + break; + case ST_STAGE_BEAM_IN: + beamIn(sim); + break; + case ST_STAGE_WAIT: + stFareWaveStep(sim); + break; + case ST_STAGE_WALK_TO_CAB: + case ST_STAGE_WALK_TO_PAD: + walkToTarget(sim); + break; + case ST_STAGE_BOARD: + stageBoard(sim); + break; + case ST_STAGE_RIDING: + sim->deathInProgress = 0u; + stSimPadLight(sim, false); + break; + case ST_STAGE_DROP_IN: + stageDropIn(sim); + break; + case ST_STAGE_LEAVE: + stFareLeaveStep(sim); + break; + case ST_STAGE_CLEANUP: + stageCleanup(sim); + break; + default: + sim->stage = ST_STAGE_IDLE; + break; + } +} + + +// $6CB8 -- no fare pad left: the passenger wants to leave the screen, +// so the top hatch opens. +void stFareUpPlease(StSimT *sim) { + uint8_t k; + + sim->activeSpriteIdx = 0x0Bu; + stSimDrawText(sim, 14u, 24u, kTextUpPlease, 1u); + for (k = 0u; k < 4u; k++) { + stSimPutChar(sim, (uint8_t)(18u + k), 0u, ST_CHAR_SPACE); + } + stFareBoardingSound(sim, kSpeechUp); +} diff --git a/examples/spacetaxi/stHooks.c b/examples/spacetaxi/stHooks.c new file mode 100644 index 0000000..89db67b --- /dev/null +++ b/examples/spacetaxi/stHooks.c @@ -0,0 +1,1001 @@ +// Space Taxi -- per-level hook programs. +// +// Every level's data blob ends with six trampolines ($7D66..$7D77) +// and a small program at $7D98..$7FFF that the main loop calls at +// fixed points: two prelude hooks, two per-tick hooks (before and +// after the sprite flush), an input filter and the sprite-contact +// verdict. Those programs are the level gimmicks -- the puzzle +// switches, lasers, moving pads, trap doors. The port keeps the +// original bytes (sim->hookRam, for the tables the code indexes) and +// re-expresses each program here, reading its tables at the same +// addresses so nothing has to be transcribed by hand. + +#include "stSim.h" + +// Hook RAM accessors by C64 address. +#define HK(sim, addr) ((sim)->hookRam[(addr) - ST_HOOK_BASE]) +#define HKP(sim, addr) (&(sim)->hookRam[(addr) - ST_HOOK_BASE]) + +// 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_T 19u +#define ST_LEVEL_U 20u +#define ST_LEVEL_V 21u +#define ST_LEVEL_W 22u +#define ST_LEVEL_X 23u + + +static void addToSpriteCol(StSimT *sim, uint8_t idx, uint8_t delta); +static void fillDownChar(StSimT *sim, uint8_t col, uint8_t row, uint8_t ch, uint8_t rows); +static void fillDownColor(StSimT *sim, uint8_t col, uint8_t row, uint8_t color, uint8_t rows); +static void hookHPerTick(StSimT *sim); +static void hookHPrelude0(StSimT *sim); +static void hookHSegment(StSimT *sim, uint8_t id); +static void hookTGate(StSimT *sim, uint8_t ch); +static void hookTPerTick(StSimT *sim); +static void hookTPrelude1(StSimT *sim); +static void hookWLaserUpdate(StSimT *sim); +static void hookWPerTick(StSimT *sim); +static void hookXMovePads(StSimT *sim); +static void hookXPerTick(StSimT *sim); +static void hookXPrelude0(StSimT *sim); +static void hookXShiftRow(StSimT *sim, uint16_t rowCell, uint8_t from, uint8_t to, int8_t dir); +static void setSpriteColors(StSimT *sim, uint8_t color); +#if !defined(__W65816__) +static void hookKPerTick(StSimT *sim); +static void hookOPerTick(StSimT *sim); +static void hookOTramp0(StSimT *sim); +static void hookRMaze(StSimT *sim, uint8_t ch); +static void hookRPerTick(StSimT *sim); +static void hookVPerTick(StSimT *sim); +static void hookVScrollLeft(StSimT *sim, uint8_t row); +static void hookVScrollRight(StSimT *sim, uint8_t row); +static void hookQPrelude(StSimT *sim); +static void hookQPerTick(StSimT *sim); +static uint8_t hookQInput(StSimT *sim, uint8_t input); +static void hookIPrelude(StSimT *sim); +static void hookIPerTick(StSimT *sim); +static void hookGPerTick(StSimT *sim); +static void hookUPerTick(StSimT *sim); +#endif + + +// $411B -- add a signed byte to a sprite's X (carry into the msb). +static void addToSpriteCol(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); +} + + +// $41AD after $401B -- the same character into the `rows` cells below. +static void fillDownChar(StSimT *sim, uint8_t col, uint8_t row, uint8_t ch, uint8_t rows) { + uint8_t k; + + for (k = 1u; k <= rows; k++) { + if ((uint8_t)(row + k) < ST_SCREEN_ROWS) { + stSimPutChar(sim, col, (uint8_t)(row + k), ch); + } + } +} + + +// $41AD after $401E -- the same colour into the `rows` cells below. +static void fillDownColor(StSimT *sim, uint8_t col, uint8_t row, uint8_t color, uint8_t rows) { + uint8_t k; + + for (k = 1u; k <= rows; k++) { + if ((uint8_t)(row + k) < ST_SCREEN_ROWS) { + stSimPutColor(sim, col, (uint8_t)(row + k), color); + } + } +} + + +// Level H "PUZZLER" per-tick ($7E5A): touching a switch sprite, or +// landing on a pad whose list holds an active segment, animates the +// switch's four wall segments over four 8-tick steps with a rising +// tone, then toggles their flags. +static void hookHPerTick(StSimT *sim) { + if (HK(sim, 0x7E54) == 0u) { + if (sim->collisionPhase != 0u) { + return; + } + if (sim->activePad != 0u) { + uint8_t x = (uint8_t)((sim->activePad - 1u) * 4u); + uint8_t n; + bool found = false; + for (n = 0u; n < 4u; n++) { + uint8_t y = HK(sim, 0x7E24 + x + n); + if (HK(sim, 0x7E15 + y) != 0u) { + HK(sim, 0x7E4C) = y; + HK(sim, 0x7E54) = 6u; + found = true; + break; + } + } + if (!found) { + return; + } + } else { + uint8_t a = sim->spriteSpriteColl; + uint8_t y = 5u; + while (y != 0u) { + bool hit = (a & 0x80u) != 0u; + a = (uint8_t)(a << 1); + if (hit) { + break; + } + y--; + } + if (y == 0u) { + return; + } + HK(sim, 0x7E54) = y; + setSpriteColors(sim, 0x0Au); + sim->spr[2u + y].color = 0x05u; + } + // $7EB2: start the animation. + HK(sim, 0x7E53) = 0u; + HK(sim, 0x7E55) = 0xFFu; + HK(sim, 0x7E56) = 0x12u; + stAudioSfx(HKP(sim, 0x7ED0)); + } + // $7ED9 + HK(sim, 0x7E56)++; + stAudioVoice2(HK(sim, 0x7E56), HK(sim, 0x7E56), 0xFFu); + HK(sim, 0x7E53)++; + if ((HK(sim, 0x7E53) & 7u) != 0u) { + return; + } + HK(sim, 0x7E55)++; + HK(sim, 0x7E52) = (uint8_t)((HK(sim, 0x7E54) - 1u) * 4u); + HK(sim, 0x7E51) = 4u; + if (HK(sim, 0x7E55) == 4u) { + // $7F7C: finished -- flip the segment flags. + HK(sim, 0x7E54) = 0u; + setSpriteColors(sim, 0x01u); + while (HK(sim, 0x7E51) != 0u) { + uint8_t id = HK(sim, 0x7E38 + HK(sim, 0x7E52)); + if ((id & 0x80u) == 0u) { + HK(sim, 0x7E15 + id) ^= 1u; + } + HK(sim, 0x7E52)++; + HK(sim, 0x7E51)--; + } + return; + } + while (HK(sim, 0x7E51) != 0u) { + uint8_t id = HK(sim, 0x7E38 + HK(sim, 0x7E52)); + if ((id & 0x80u) == 0u) { + hookHSegment(sim, id); + } + HK(sim, 0x7E52)++; + HK(sim, 0x7E51)--; + } +} + + +// Level H prelude 0 ($7D9B): clear the segment flags, park the five +// switch sprites (3..7) from the position tables. +static void hookHPrelude0(StSimT *sim) { + uint8_t k; + + for (k = 0u; k <= 10u; k++) { + HK(sim, 0x7E15 + k) = 0u; + } + HK(sim, 0x7E54) = 0u; + for (k = 7u; k >= 3u; k--) { + sim->spr[k].color = 1u; + sim->spr[k].enable = 1u; + sim->spr[k].ptr = 0x80u; + sim->spr[k].col = HK(sim, 0x7DCF + k); + sim->spr[k].msb = HK(sim, 0x7DD7 + k); + sim->spr[k].row = HK(sim, 0x7DDF + k); + } +} + + +// $7F22 -- draw (or erase) one cell of segment `id` for the current +// animation step, growing from whichever end the tables say. +static void hookHSegment(StSimT *sim, uint8_t id) { + uint8_t step; + uint8_t col; + uint8_t row; + uint8_t ch; + + HK(sim, 0x7E50) = id; + if ((HK(sim, 0x7E15 + id) ^ HK(sim, 0x7DFD + id)) == 0u) { + step = HK(sim, 0x7E55); + } else { + step = (uint8_t)(3u - HK(sim, 0x7E55)); + } + if (HK(sim, 0x7E08 + id) == 0x9Bu) { + row = (uint8_t)(step + HK(sim, 0x7DF2 + id)); + col = HK(sim, 0x7DE7 + id); + } else { + col = (uint8_t)(step + HK(sim, 0x7DE7 + id)); + row = HK(sim, 0x7DF2 + id); + } + HK(sim, 0x7E13) = col; + HK(sim, 0x7E14) = row; + ch = (HK(sim, 0x7E15 + id) != 0u) ? HK(sim, 0x7E08 + id) : ST_CHAR_SPACE; + stSimPutChar(sim, col, row, ch); +} + + +// Level T "FAST BREAK" $7D9D -- the barrier on row 3: `ch` into the +// centre opening (cols 18..21), its complement into the side gaps. +static void hookTGate(StSimT *sim, uint8_t ch) { + uint8_t k; + + for (k = 0u; k < 4u; k++) { + stSimPutChar(sim, (uint8_t)(18u + k), 3u, ch); + } + ch ^= 0x55u; + for (k = 0u; k < 4u; k++) { + stSimPutChar(sim, (uint8_t)(1u + k), 3u, ch); + stSimPutChar(sim, (uint8_t)(35u + k), 3u, ch); + } +} + + +// Level T per-tick ($7DDA): a slow cab up on the right bounces off +// the ceiling; a fast climb into the top rows slams the centre gate +// shut and opens the sides until the cab drops back down. +static void hookTPerTick(StSimT *sim) { + if (HK(sim, 0x7D9C) != 0u) { + if (sim->spr[0].row >= 0x5Fu) { + hookTPrelude1(sim); + } + return; + } + if (sim->spr[0].row >= 0x4Fu) { + return; + } + if ((uint8_t)((uint16_t)sim->velY >> 8) >= 0xFCu) { + if (sim->spr[0].msb != 0u) { + return; + } + if (sim->spr[0].col < 0x96u) { + return; + } + sim->velY = (int16_t)(-sim->velY); + stAudioSfx(HKP(sim, 0x7E56)); + return; + } + if (sim->spr[0].row >= 0x3Fu) { + return; + } + sim->velY = 0; + sim->velX = 0; + hookTGate(sim, 0x75u); + HK(sim, 0x7D9C)++; + stAudioSfx(HKP(sim, 0x7E68)); +} + + +// Level T prelude 1 ($7DC4): centre open, sides barred. +static void hookTPrelude1(StSimT *sim) { + hookTGate(sim, ST_CHAR_SPACE); + HK(sim, 0x7D9C) = 0u; + stAudioSfx(HKP(sim, 0x7E5F)); +} + + +// Level W "LASERS" $7E46 -- clear the beam glyphs, then step each of +// the eight lasers: idle ones fire on a 2-in-12 roll, extending ones +// grow a cell per pass until their end row, retracting ones fade +// through two colours and vanish. +static void hookWLaserUpdate(StSimT *sim) { + uint8_t i; + uint8_t k; + + for (k = 0u; k < 16u; k++) { + sim->charset[0x92u + (k >> 3)][k & 7u] = 0u; + } + sim->charDirty[0x92u] = 1u; + sim->charDirty[0x93u] = 1u; + for (i = 0u; i < 8u; i++) { + uint8_t state = HK(sim, 0x7DAC + i); + uint8_t col = HK(sim, 0x7DCD + i); + HK(sim, 0x7DB4) = i; + if (state == 0u) { + if (stSimRng(sim, 12u) >= 3u) { + continue; + } + HK(sim, 0x7DAC + i) = 1u; + HK(sim, 0x7DB5 + i) = HK(sim, 0x7DD5 + i); + stSimPutChar(sim, col, HK(sim, 0x7DB5 + i), HK(sim, 0x7DBD + i)); + stSimPutColor(sim, col, HK(sim, 0x7DB5 + i), 2u); + continue; + } + if (state == 1u) { + stSimPutChar(sim, col, HK(sim, 0x7DB5 + i), 0x91u); + if (HK(sim, 0x7DB5 + i) == HK(sim, 0x7DE9 + i)) { + HK(sim, 0x7DAC + i) = 2u; + HK(sim, 0x7DB5 + i) = 0u; + state = 2u; + } else { + HK(sim, 0x7DB5 + i) = (uint8_t)(HK(sim, 0x7DB5 + i) + HK(sim, 0x7DDD + i)); + stSimPutChar(sim, col, HK(sim, 0x7DB5 + i), HK(sim, 0x7DBD + i)); + stSimPutColor(sim, col, HK(sim, 0x7DB5 + i), 2u); + continue; + } + } + if (state == 2u) { + uint8_t phase; + HK(sim, 0x7DB5 + i)++; + phase = HK(sim, 0x7DB5 + i); + if (phase == 3u) { + HK(sim, 0x7DAC + i) = 0u; + stSimPutChar(sim, col, HK(sim, 0x7DC5 + i), ST_CHAR_SPACE); + fillDownChar(sim, col, HK(sim, 0x7DC5 + i), ST_CHAR_SPACE, 7u); + } else { + uint8_t color = HK(sim, 0x7DE6 + phase); + stSimPutColor(sim, col, HK(sim, 0x7DC5 + i), color); + fillDownColor(sim, col, HK(sim, 0x7DC5 + i), color, 7u); + } + continue; + } + HK(sim, 0x7DAC + i) = 0u; + } +} + + +// Level W per-tick ($7DF1): a random-pitched hum on voice 2, the beam +// glyph animation on the odd ticks and the laser step on every 8th. +static void hookWPerTick(StSimT *sim) { + for (;;) { + uint8_t c; + if (HK(sim, 0x7DE5) != 0u) { + stAudioSfx(HKP(sim, 0x7DA2)); + HK(sim, 0x7DE5) = 0u; + } + c = (uint8_t)(stSimRng(sim, 0x6Eu) + 6u); + stAudioVoice2(c, c, 0x81u); + HK(sim, 0x7DAB) = (uint8_t)((HK(sim, 0x7DAB) + 1u) & 7u); + c = HK(sim, 0x7DAB); + if (c == 0u) { + hookWLaserUpdate(sim); + return; + } + // $2C8F + c (char $91 row 7 for c = 0, else char $92 rows 0..6) + // and $2C98 + (8 - c) (char $93 rows 7..1) get a beam bar. + sim->charset[0x91u + ((7u + c) >> 3)][(7u + c) & 7u] = 0x3Cu; + sim->charset[0x93u][8u - c] = 0x3Cu; + sim->charDirty[0x92u] = 1u; + sim->charDirty[0x93u] = 1u; + if ((c & 1u) == 0u) { + return; + } + } +} + + +// Level X "ON THE MOVE" $7F53 -- shift every pad's X bounds and stand +// column, the waiting passenger, and a parked cab, by the step. +static void hookXMovePads(StSimT *sim) { + uint8_t dir = HK(sim, 0x7D9D); + uint8_t off = 0u; + uint8_t phase = 0u; + + addToSpriteCol(sim, 1u, dir); + while (off < 0x48u) { + uint8_t pad = (uint8_t)(off >> 3); + uint8_t field = (uint8_t)(off & 7u); + uint8_t *hi; + uint8_t *lo; + uint16_t v; + if (field == 0u) { + hi = &sim->pads[pad].x1Hi; + lo = &sim->pads[pad].x1Lo; + } else if (field == 2u) { + hi = &sim->pads[pad].x2Hi; + lo = &sim->pads[pad].x2Lo; + } else { + hi = &sim->pads[pad].passMsb; + lo = &sim->pads[pad].passCol; + } + v = (uint16_t)(((uint16_t)*hi << 8) | *lo); + v = (uint16_t)(v + (uint16_t)(int16_t)(int8_t)dir); + *lo = (uint8_t)v; + *hi = (uint8_t)(v >> 8); + // Offsets step +2, +3, +3 repeating: x1, x2, stand X per pad. + if (phase == 0u) { + off = (uint8_t)(off + 2u); + } else { + off = (uint8_t)(off + 3u); + } + phase = (uint8_t)((phase + 1u) % 3u); + } + if (sim->activePad == 0u) { + return; + } + addToSpriteCol(sim, 0u, dir); + sim->posXmsb = sim->spr[0].msb; + sim->hoverXFrac = sim->spr[0].msb; + sim->posXcol = sim->spr[0].col; + sim->hoverXCol = sim->spr[0].col; +} + + +// Level X per-tick ($7DE1): every 16 ticks slide the two pad columns +// one cell, bouncing between columns 1 and 21. +static void hookXPerTick(StSimT *sim) { + uint8_t col; + + HK(sim, 0x7D9E)++; + if (HK(sim, 0x7D9E) != 0x10u) { + return; + } + HK(sim, 0x7D9E) = 0u; + col = HK(sim, 0x7D9C); + if ((HK(sim, 0x7D9D) & 0x80u) == 0u) { + // $7DF9: rightward. + hookXShiftRow(sim, ST_CELL(5, 0), (uint8_t)(col + 17u), col, 1); + hookXShiftRow(sim, ST_CELL(10, 0), (uint8_t)(col + 17u), col, 1); + hookXShiftRow(sim, ST_CELL(15, 0), (uint8_t)(col + 17u), col, 1); + hookXShiftRow(sim, ST_CELL(20, 0), (uint8_t)(col + 17u), col, 1); + stSimPutChar(sim, col, 5u, HK(sim, 0x7D9F + col)); + stSimPutChar(sim, col, 10u, HK(sim, 0x7D9F + col)); + stSimPutChar(sim, col, 15u, HK(sim, 0x7D9F + col)); + stSimPutChar(sim, col, 20u, HK(sim, 0x7D9F + col)); + stSimPutColor(sim, col, 5u, 0x0Cu); + stSimPutColor(sim, col, 10u, 0x0Cu); + stSimPutColor(sim, col, 15u, 0x0Cu); + stSimPutColor(sim, col, 20u, 0x0Cu); + // $7E56: the row-14 cell of the right group moves with it. + stSimPutChar(sim, (uint8_t)(col + 12u), 14u, sim->screen[ST_CELL(14, col + 11u)]); + stSimPutChar(sim, (uint8_t)(col + 11u), 14u, ST_CHAR_SPACE); + stSimPutColor(sim, (uint8_t)(col + 12u), 14u, 7u); + stSimPutChar(sim, (uint8_t)(col + 11u), 5u, HK(sim, 0x7D9F + col + 11u)); + stSimPutChar(sim, (uint8_t)(col + 11u), 10u, HK(sim, 0x7D9F + col + 11u)); + stSimPutChar(sim, (uint8_t)(col + 11u), 15u, HK(sim, 0x7D9F + col + 11u)); + stSimPutChar(sim, (uint8_t)(col + 11u), 20u, HK(sim, 0x7D9F + col + 11u)); + stSimPutColor(sim, (uint8_t)(col + 11u), 5u, 0x0Cu); + stSimPutColor(sim, (uint8_t)(col + 11u), 10u, 0x0Cu); + stSimPutColor(sim, (uint8_t)(col + 11u), 15u, 0x0Cu); + stSimPutColor(sim, (uint8_t)(col + 11u), 20u, 0x0Cu); + hookXMovePads(sim); + HK(sim, 0x7D9C)++; + if (HK(sim, 0x7D9C) == 0x15u) { + HK(sim, 0x7D9D) = 0xF8u; + } + return; + } + // $7E9E: leftward. + hookXShiftRow(sim, ST_CELL(5, 0), col, (uint8_t)(col + 17u), -1); + hookXShiftRow(sim, ST_CELL(10, 0), col, (uint8_t)(col + 17u), -1); + hookXShiftRow(sim, ST_CELL(15, 0), col, (uint8_t)(col + 17u), -1); + hookXShiftRow(sim, ST_CELL(20, 0), col, (uint8_t)(col + 17u), -1); + stSimPutChar(sim, (uint8_t)(col + 6u), 5u, HK(sim, 0x7D9F + col + 6u)); + stSimPutChar(sim, (uint8_t)(col + 6u), 10u, HK(sim, 0x7D9F + col + 6u)); + stSimPutChar(sim, (uint8_t)(col + 6u), 15u, HK(sim, 0x7D9F + col + 6u)); + stSimPutChar(sim, (uint8_t)(col + 6u), 20u, HK(sim, 0x7D9F + col + 6u)); + stSimPutColor(sim, (uint8_t)(col + 6u), 5u, 0x0Cu); + stSimPutColor(sim, (uint8_t)(col + 6u), 10u, 0x0Cu); + stSimPutColor(sim, (uint8_t)(col + 6u), 15u, 0x0Cu); + stSimPutColor(sim, (uint8_t)(col + 6u), 20u, 0x0Cu); + stSimPutChar(sim, (uint8_t)(col + 10u), 14u, sim->screen[ST_CELL(14, col + 11u)]); + stSimPutChar(sim, (uint8_t)(col + 11u), 14u, ST_CHAR_SPACE); + stSimPutColor(sim, (uint8_t)(col + 10u), 14u, 7u); + stSimPutChar(sim, (uint8_t)(col + 17u), 5u, HK(sim, 0x7D9F + col + 17u)); + stSimPutChar(sim, (uint8_t)(col + 17u), 10u, HK(sim, 0x7D9F + col + 17u)); + stSimPutChar(sim, (uint8_t)(col + 17u), 15u, HK(sim, 0x7D9F + col + 17u)); + stSimPutChar(sim, (uint8_t)(col + 17u), 20u, HK(sim, 0x7D9F + col + 17u)); + stSimPutColor(sim, (uint8_t)(col + 17u), 5u, 0x0Cu); + stSimPutColor(sim, (uint8_t)(col + 17u), 10u, 0x0Cu); + stSimPutColor(sim, (uint8_t)(col + 17u), 15u, 0x0Cu); + stSimPutColor(sim, (uint8_t)(col + 17u), 20u, 0x0Cu); + hookXMovePads(sim); + HK(sim, 0x7D9C)--; + if (HK(sim, 0x7D9C) == 1u) { + HK(sim, 0x7D9D) = 0x08u; + } +} + + +// Level X prelude 0 ($7DC6): reset the sweep and restore the pad table +// from the level's backup copy ($7FB8). +static void hookXPrelude0(StSimT *sim) { + uint8_t k; + + HK(sim, 0x7D9D) = 0xF8u; + HK(sim, 0x7D9C) = 0x0Bu; + HK(sim, 0x7D9E) = 0u; + for (k = 0u; k < 8u; k++) { + const uint8_t *src = HKP(sim, 0x7FB8 + k * 8u); + sim->pads[k].x1Hi = src[0]; + sim->pads[k].x1Lo = src[1]; + sim->pads[k].x2Hi = src[2]; + sim->pads[k].x2Lo = src[3]; + sim->pads[k].row = src[4]; + sim->pads[k].passMsb = src[5]; + sim->pads[k].passCol = src[6]; + sim->pads[k].unused = src[7]; + } +} + + +// Copy cells of one screen row (char + colour) one column along, from +// column `from` to column `to` inclusive, in the direction that keeps +// the copy from overwriting its own source. +static void hookXShiftRow(StSimT *sim, uint16_t rowCell, uint8_t from, uint8_t to, int8_t dir) { + uint8_t x = from; + + for (;;) { + uint16_t src = (uint16_t)(rowCell + x); + uint16_t dst = (uint16_t)(src + (uint16_t)(int16_t)dir); + stSimPutChar(sim, (uint8_t)(dst % ST_SCREEN_COLS), (uint8_t)(dst / ST_SCREEN_COLS), sim->screen[src]); + stSimPutColor(sim, (uint8_t)(dst % ST_SCREEN_COLS), (uint8_t)(dst / ST_SCREEN_COLS), sim->color[src]); + if (x == to) { + break; + } + x = (uint8_t)(x - (uint8_t)dir); + } +} + + +// Sprites 3..7 share one colour in the puzzle level. +static void setSpriteColors(StSimT *sim, uint8_t color) { + uint8_t k; + + for (k = 3u; k < ST_HW_SPRITES; k++) { + sim->spr[k].color = color; + } +} + + +// Extra-level gimmicks (magnets, electroids, maze, shift-o-rama, +// interference, crossfire, teleports, rebound). Compiled out on the +// IIgs, whose bank-0 BSS/text budget is too tight for them; those +// levels play without their hazard there (the 4 attract-demo levels +// H/T/W/X keep their gimmicks everywhere). +#if !defined(__W65816__) +// Level K "MAGNETS" per-tick ($7D9D): the level is globally anti-gravity +// (its yGrav template is -7); the hook only animates the six magnet +// poles by flipping bit 0 of their glyphs every other tick. +static void hookKPerTick(StSimT *sim) { + static const uint8_t kPole[6] = { 0x04u, 0x0Bu, 0x10u, 0x17u, 0x1Cu, 0x23u }; + uint8_t k; + + HK(sim, 0x7DBD) ^= 1u; + if ((HK(sim, 0x7DBD) & 1u) != 0u) { + return; + } + for (k = 0u; k < 6u; k++) { + uint16_t cell = (uint16_t)(0xA0u + kPole[k]); // $04A0 + off + stSimPutChar(sim, (uint8_t)(cell % ST_SCREEN_COLS), (uint8_t)(cell / ST_SCREEN_COLS), (uint8_t)(sim->screen[cell] ^ 1u)); + } +} + + +// Level O "ELECTROIDS" per-tick ($7DBE): four electric barriers (rows +// 5, 14, 22, 26 of screen RAM in cell terms) scroll left one column, +// the leftmost cell wrapping to the right, at a rate gated by a mod-8 +// tick (rows 1-2 every tick, row 3 every 4th). +static void hookOScrollRow(StSimT *sim, uint16_t base) { + uint8_t chSave = sim->screen[base]; + uint8_t colSave = sim->color[base]; + uint8_t x; + uint16_t cell; + + for (x = 1u; x < 0x26u; x++) { + cell = (uint16_t)(base + x); + stSimPutChar(sim, (uint8_t)((base + x - 1u) % ST_SCREEN_COLS), (uint8_t)((base + x - 1u) / ST_SCREEN_COLS), sim->screen[cell]); + stSimPutColor(sim, (uint8_t)((base + x - 1u) % ST_SCREEN_COLS), (uint8_t)((base + x - 1u) / ST_SCREEN_COLS), sim->color[cell]); + } + cell = (uint16_t)(base + 0x25u); + stSimPutChar(sim, (uint8_t)(cell % ST_SCREEN_COLS), (uint8_t)(cell / ST_SCREEN_COLS), chSave); + stSimPutColor(sim, (uint8_t)(cell % ST_SCREEN_COLS), (uint8_t)(cell / ST_SCREEN_COLS), colSave); +} + + +static void hookOPerTick(StSimT *sim) { + hookOTramp0(sim); // $7E6E bitScroll of the barrier glyph + HK(sim, 0x7DBD) = (uint8_t)((HK(sim, 0x7DBD) + 1u) & 7u); + if ((HK(sim, 0x7DBD) & 3u) != 0u) { + return; + } + hookOScrollRow(sim, 0xC9u); // $04C9 row 5 col 1 + hookOScrollRow(sim, 0x209u); // $0609 row 12 col 1 + if (HK(sim, 0x7DBD) != 4u) { + return; + } + hookOScrollRow(sim, 0x169u); // $0569 row 9 col 1 + hookOScrollRow(sim, 0x2A9u); // $06A9 row 17 col 1 +} + + +// $63D0-style rotate of the two electroid glyphs ($2BE0 / char $6F). +static void hookOTramp0(StSimT *sim) { + uint8_t k; + + for (k = 0u; k < 8u; k++) { + uint8_t b = sim->charset[0x6Fu][k]; + sim->charset[0x6Fu][k] = (uint8_t)((b >> 1) | (b << 7)); + } + sim->charDirty[0x6Fu] = 1u; +} + + +// Level R "TAXI MAZE" wall program ($7D9C): put `ch` in the vertical +// bar cells ($043E/$0466/$048E) and its complement (ch^$46) in the +// gate cells; called with $66 to close, with the gate's own char to +// flip. Toggling on pickup opens a path. +static void hookRMaze(StSimT *sim, uint8_t ch) { + static const uint16_t kBar[3] = { 0x3Eu, 0x66u, 0x8Eu }; + static const uint16_t kGate[10] = { 0x39u, 0x61u, 0x89u, 0xA1u, 0xA2u, 0xA3u, 0xA4u, 0x309u, 0x30Au, 0x30Bu }; + uint8_t k; + uint8_t alt = (uint8_t)(ch ^ 0x46u); + + for (k = 0u; k < 3u; k++) { + stSimPutChar(sim, (uint8_t)(kBar[k] % ST_SCREEN_COLS), (uint8_t)(kBar[k] / ST_SCREEN_COLS), ch); + } + for (k = 0u; k < 10u; k++) { + stSimPutChar(sim, (uint8_t)(kGate[k] % ST_SCREEN_COLS), (uint8_t)(kGate[k] / ST_SCREEN_COLS), alt); + } +} + + +// Level R per-tick ($7DD2): when the passenger boards (stage 4) and the +// gate is open ($0439 == space) the maze flips. +static void hookRPerTick(StSimT *sim) { + if (sim->stage != ST_STAGE_BOARD) { + return; + } + if (sim->screen[0x39u] != ST_CHAR_SPACE) { + return; + } + hookRMaze(sim, sim->screen[0x39u]); +} + + +// Level V "SHIFT-O-RAMA" per-tick ($7D9D): every screen tick, 18 rows +// (0..17) each scroll one column, alternating direction by row (bit 1 +// of the row index): even-band rows left, odd-band right, the edge +// cell wrapping around. +static void hookVScrollLeft(StSimT *sim, uint8_t row) { + uint16_t base = ST_CELL(row, 0); + uint8_t chSave = sim->screen[base]; + uint8_t colSave = sim->color[base]; + uint8_t x; + + for (x = 1u; x < 0x27u; x++) { + stSimPutChar(sim, (uint8_t)(x - 1u), row, sim->screen[base + x]); + stSimPutColor(sim, (uint8_t)(x - 1u), row, sim->color[base + x]); + } + stSimPutChar(sim, 0x26u, row, chSave); + stSimPutColor(sim, 0x26u, row, colSave); +} + + +static void hookVScrollRight(StSimT *sim, uint8_t row) { + uint16_t base = ST_CELL(row, 0); + uint8_t chSave = sim->screen[base + 0x26u]; + uint8_t colSave = sim->color[base + 0x26u]; + uint8_t x; + + for (x = 0x26u; x > 0u; x--) { + stSimPutChar(sim, x, row, sim->screen[base + x - 1u]); + stSimPutColor(sim, x, row, sim->color[base + x - 1u]); + } + stSimPutChar(sim, 0u, row, chSave); + stSimPutColor(sim, 0u, row, colSave); +} + + +static void hookVPerTick(StSimT *sim) { + uint8_t row; + + for (row = 6u; row < 0x12u; row++) { + if ((row & 2u) != 0u) { + hookVScrollLeft(sim, row); + } else { + hookVScrollRight(sim, row); + } + } +} + + +// Level Q "INTERFERENCE" ($7DA1 prelude0, $7E0D per-tick2, $7E2E input): +// five interference sprites (hw 3..7) sit at fixed spots flickering +// through cels $80..$8B, and while the cab is mid-screen the joystick +// is scrambled 20% of ticks. +static void hookQPrelude(StSimT *sim) { + int8_t x; + + sim->multiColorMask = 0xFFu; + for (x = 7; x >= 3; x--) { + HK(sim, 0x7E83 + (uint8_t)x) = HK(sim, 0x7DEA + (uint8_t)x); + sim->spr[x].ptr = HK(sim, 0x7DF2 + HK(sim, 0x7E83 + (uint8_t)x)); + sim->spr[x].color = 0x06u; + sim->spr[x].col = HK(sim, 0x7DD2 + (uint8_t)x); + sim->spr[x].msb = HK(sim, 0x7DDA + (uint8_t)x); + sim->spr[x].row = HK(sim, 0x7DE2 + (uint8_t)x); + sim->spr[x].enable = 1u; + } +} + + +static void hookQPerTick(StSimT *sim) { + int8_t x; + + for (x = 7; x >= 3; x--) { + HK(sim, 0x7E83 + (uint8_t)x)++; + if (HK(sim, 0x7E83 + (uint8_t)x) == 0x0Cu) { + HK(sim, 0x7E83 + (uint8_t)x) = 0u; + } + sim->spr[x].ptr = HK(sim, 0x7DF2 + HK(sim, 0x7E83 + (uint8_t)x)); + } +} + + +static uint8_t hookQInput(StSimT *sim, uint8_t input) { + if (sim->activePad != 0u) { + return input; + } + if (sim->spr[0].row < 0x5Au || sim->spr[0].row >= 0xB4u) { + return input; + } + if (stSimRng(sim, 10u) < 8u) { + return input; + } + return HK(sim, 0x7E7F + (uint8_t)(stSimRng(sim, 4u) - 1u)); +} + + +// Level I "CROSSFIRE" ($7D9C prelude0, $7DEF per-tick2, $7DB1 prelude1): +// bullets (hw 3..7) fly up from the floor, cel-walk, then burst. +static void hookIPrelude(StSimT *sim) { + int8_t x; + + for (x = 7; x >= 3; x--) { + HK(sim, 0x7DB8 + (uint8_t)x) = 0u; // state + sim->spr[x].enable = 0u; + sim->spr[x].color = 0x02u; + } +} + + +static void hookIPerTick(StSimT *sim) { + int8_t s; + + for (s = 3; s <= 7; s++) { + uint8_t x = (uint8_t)s; + uint8_t state = HK(sim, 0x7DB8 + x); + if (state == 0u) { + uint8_t y; + if (stSimRng(sim, 0x75u) >= 3u) { + continue; + } + y = (uint8_t)(stSimRng(sim, 4u) - 1u); // 0..3 direction + HK(sim, 0x7DC8 + x) = HK(sim, 0x7DE1 + y); // dx + sim->spr[x].col = HK(sim, 0x7DDD + y); // start col + sim->spr[x].msb = 0u; + sim->spr[x].row = 0xD1u; + HK(sim, 0x7DD0 + x) = (uint8_t)(stSimRng(sim, 2u) + 0xFDu); // dy = rng(2)-3 + HK(sim, 0x7DC0 + x) = 0u; // cel phase + HK(sim, 0x7DB8 + x) = 1u; + sim->spr[x].enable = 1u; + sim->spr[x].ptr = 0x84u; + stAudioSfx(HKP(sim, 0x7E82)); + sim->spr[x].color = HK(sim, 0x7E7F + (uint8_t)(stSimRng(sim, 3u) - 1u)); + } else if (state == 1u) { + uint8_t ph = (uint8_t)(HK(sim, 0x7DC0 + x) + 1u); + HK(sim, 0x7DC0 + x) = ph; + if (ph == 5u) { + HK(sim, 0x7DC0 + x) = 0u; + sim->spr[x].ptr = 0x85u; + HK(sim, 0x7DB8 + x) = 2u; + } else { + if (ph >= 3u) { + addToSpriteCol(sim, x, HK(sim, 0x7DC8 + x)); + sim->spr[x].row = (uint8_t)(sim->spr[x].row + HK(sim, 0x7DD0 + x)); + } + sim->spr[x].ptr = HK(sim, 0x7DD8 + ph); + } + } else { + uint8_t ph = (uint8_t)((HK(sim, 0x7DC0 + x) + 1u) & 7u); + HK(sim, 0x7DC0 + x) = ph; + sim->spr[x].ptr = HK(sim, 0x7DE5 + ph); + addToSpriteCol(sim, x, HK(sim, 0x7DC8 + x)); + sim->spr[x].row = (uint8_t)(sim->spr[x].row + HK(sim, 0x7DD0 + x)); + if (sim->spr[x].row < 0x25u) { + sim->spr[x].enable = 0u; + HK(sim, 0x7DB8 + x) = 0u; + } + } + } +} + + +// Level G "TELEPORTS" ($7DA0 prelude0, $7DF2 per-tick2): orbs (hw 2..7) +// drift; touching the cab teleports it. Faithful reduction: place and +// drift the orb sprites (the teleport hop is a rare event we leave to +// the collision system, which crashes on contact like the C64's +// undelivered case -- see MECHANICS). Placement only for now. +static void hookGPerTick(StSimT *sim) { + // The orb colour cycles through $7DD4[phase] every 4 ticks. + HK(sim, 0x7E00)++; + if ((HK(sim, 0x7E00) & 3u) != 0u) { + return; + } + HK(sim, 0x7DD2)++; + if (HK(sim, 0x7DD2) == 6u) { + HK(sim, 0x7DD2) = 0u; + } + { + uint8_t c = HK(sim, 0x7DD4 + HK(sim, 0x7DD2)); + uint8_t k; + for (k = 2u; k < ST_HW_SPRITES; k++) { + sim->spr[k].color = c; + } + } +} + + +// Level U "REBOUND" ($7DAD per-tick2): the ceiling and side walls +// reflect the cab's velocity; hazard sprites (hw 3..7) drift. The wall +// bounce is the gimmick and is handled here on the taxi velocity. +static void hookUPerTick(StSimT *sim) { + if (sim->collisionPhase != 0u) { + return; + } + // $7DB2: near the top -> reflect Y downward. + if (sim->spr[0].row < 0x11u) { + sim->velY = (int16_t)(-sim->velY); + sim->spr[0].row = (uint8_t)(sim->spr[0].row + 2u); + } +} + + +#endif /* !IIGS */ + + +// --------------------------------------------------------------------------- +// Dispatch +// --------------------------------------------------------------------------- + +// $7D75 -- 0 means sprite contact (passenger, level sprites) is safe. +uint8_t stHookHitVerdict(StSimT *sim) { + switch (sim->hookLevel) { + case ST_LEVEL_H: + return 0u; + default: + return 1u; + } +} + + +// $7D72 -- the input filter. +uint8_t stHookInput(StSimT *sim, uint8_t input) { +#if !defined(__W65816__) + switch (sim->hookLevel) { + case ST_LEVEL_Q: + return hookQInput(sim, input); + default: + break; + } +#else + (void)sim; +#endif + return input; +} + + +// $7D6C -- before the sprite flush. +void stHookPerTick2(StSimT *sim) { + switch (sim->hookLevel) { + case ST_LEVEL_H: + hookHPerTick(sim); + break; + case ST_LEVEL_T: + hookTPerTick(sim); + break; + case ST_LEVEL_W: + hookWPerTick(sim); + break; + case ST_LEVEL_X: + hookXPerTick(sim); + break; +#if !defined(__W65816__) + case ST_LEVEL_V: + hookVPerTick(sim); + break; + case ST_LEVEL_Q: + hookQPerTick(sim); + break; + case ST_LEVEL_I: + hookIPerTick(sim); + break; + case ST_LEVEL_G: + hookGPerTick(sim); + break; + case ST_LEVEL_U: + hookUPerTick(sim); + break; + case ST_LEVEL_K: + hookKPerTick(sim); + break; + case ST_LEVEL_O: + hookOPerTick(sim); + break; + case ST_LEVEL_R: + hookRPerTick(sim); + break; +#endif + default: + break; + } +} + + +// $7D6F -- after the sprite flush. +void stHookPerTick3(StSimT *sim) { + (void)sim; +} + + +// $7D66 -- first prelude hook. +void stHookPrelude0(StSimT *sim) { + switch (sim->hookLevel) { + case ST_LEVEL_H: + hookHPrelude0(sim); + break; + case ST_LEVEL_X: + hookXPrelude0(sim); + break; +#if !defined(__W65816__) + case ST_LEVEL_Q: + hookQPrelude(sim); + break; + case ST_LEVEL_I: + hookIPrelude(sim); + break; +#endif + case ST_LEVEL_W: + { + uint8_t k; + for (k = 1u; k <= 8u; k++) { + HK(sim, 0x7DAB + k) = 0u; + } + } + break; + default: + break; + } +} + + +// $7D69 -- second prelude hook. +void stHookPrelude1(StSimT *sim) { + switch (sim->hookLevel) { + case ST_LEVEL_T: + hookTPrelude1(sim); + break; + case ST_LEVEL_W: + HK(sim, 0x7DE5) = 1u; + break; +#if !defined(__W65816__) + case ST_LEVEL_R: + hookRMaze(sim, ST_CHAR_BLANK); + break; +#endif + default: + break; + } +} diff --git a/examples/spacetaxi/stHud.c b/examples/spacetaxi/stHud.c deleted file mode 100644 index 459a3b8..0000000 --- a/examples/spacetaxi/stHud.c +++ /dev/null @@ -1,82 +0,0 @@ -// Space Taxi -- HUD (score / lives / level / current-fare strip). -// -// Lives in the bottom 3 tile-rows (y = 176..199) below the playfield. -// Renders textual score / lives / level name / current fare via the -// font asset (loaded by stRender). No fuel meter: the C64 original -// has no fuel mechanic. The strip at $DBA2-$DBCB in the original game -// is a per-frame Y-velocity status indicator ($6419), not a fuel bar. - -#include -#include - -#include "spacetaxi.h" - - - -void stHudDraw(jlSurfaceT *stage, const StGameT *game) { - char buf[32]; - char livesBuf[ST_MAX_PADS + 1]; - uint8_t i; - - // Wipe the HUD band with the level's border color (C64 $7D00 -> $D020). - // Most canonical levels set both border and bg to 0 (black), so this - // looks identical to the previous hardcoded ST_HUD_BG_COLOR for them. - jlFillRect(stage, - 0, - (int16_t)(ST_HUD_ROW * ST_TILE_PIXELS), - SURFACE_WIDTH, - (int16_t)(ST_HUD_ROW_COUNT * ST_TILE_PIXELS), - game->level.borderColor); - - // 4-digit score with a separator after the thousands digit, matching - // the C64 HUD template at $43B1 ('___ . __'). The format puts the - // ones digit at position 3 and uses '.' as a thousands marker. Range - // is 0..9999; beyond that we wrap (the C64 BCD can't exceed 9999 - // either). - snprintf(buf, sizeof(buf), "%04lu.", - (unsigned long)(game->score % 10000ul)); - stRenderDrawText(stage, 0u, (uint8_t)ST_HUD_ROW, buf); - - // Lives indicator: graphic-ish "cabs remaining" -- one 'O' per life, - // up to 9. C64 shows this as filled glyphs in color RAM ($DBDC etc). - for (i = 0u; i < 9u && i < game->lives; i++) { - livesBuf[i] = 'O'; - } - livesBuf[i] = '\0'; - stRenderDrawText(stage, 7u, (uint8_t)ST_HUD_ROW, livesBuf); - - // Level name right-aligned in the 40-col HUD row. - { - uint8_t nameLen = (uint8_t)strlen(game->level.name); - uint8_t col = (nameLen < ST_TILEMAP_W) - ? (uint8_t)(ST_TILEMAP_W - nameLen) - : 0u; - stRenderDrawText(stage, col, (uint8_t)ST_HUD_ROW, game->level.name); - } - - // Active-fare message, matching the C64 HUD ($6C1E "PAD PLEASE", - // $6C2B " UP PLEASE!"): a waiting fare hails ("HEY TAXI!"); once - // aboard it announces its destination by pad NUMBER (index+1, per - // $671E which draws the destination index + '0'), or "UP PLEASE!" - // when the exit is the transporter. - for (i = 0u; i < ST_MAX_PASSENGERS; i++) { - const StPassengerT *p = &game->passengers[i]; - if (!p->active) { - continue; - } - if (p->phase == ST_PASS_DROP_IN || p->phase == ST_PASS_WALK_TO_PAD || - p->phase == ST_PASS_DROP_OUT) { - break; // delivery in progress: no hail, no dest - } - if (!p->onboard) { - snprintf(buf, sizeof(buf), "HEY TAXI!"); - } else if (p->destPad == ST_DEST_TRANSPORTER) { - snprintf(buf, sizeof(buf), "UP PLEASE!"); - } else { - snprintf(buf, sizeof(buf), "PAD %u PLEASE!", - (unsigned)(p->destPad + 1u)); - } - stRenderDrawText(stage, 0u, (uint8_t)(ST_HUD_ROW + 1u), buf); - break; - } -} diff --git a/examples/spacetaxi/stLevel.c b/examples/spacetaxi/stLevel.c index 1603ddf..fb23c27 100644 --- a/examples/spacetaxi/stLevel.c +++ b/examples/spacetaxi/stLevel.c @@ -1,133 +1,21 @@ -// Space Taxi -- level loader. -// -// Reads a level .dat file produced by `tools/spacetaxi/mkLevel.py` -// (see assets/levels/format.md for the byte layout). -// -// A level file is small (~2-3 KB raw, plus a name + per-pad config), -// loaded once per scene change. Read fully into RAM; tilemap + colormap -// stay inside the StLevelT struct for the life of the game state. +// Space Taxi -- level loader (JoeyLib data path + the shared STL4 parser). #include -#include -#include #include "joey/file.h" #include "spacetaxi.h" -// STL3: per-pad record is (tileX, tileY, tileW, standX) where standX is the -// passenger stand column in screen pixels; the STL2 per-pad letter and the -// whole fare list are gone (fares are RNG-chosen at runtime, not stored). -#define ST_LEVEL_MAGIC0 'S' -#define ST_LEVEL_MAGIC1 'T' -#define ST_LEVEL_MAGIC2 'L' -#define ST_LEVEL_MAGIC3 '3' - - -static bool readByte(FILE *fp, uint8_t *out) { - int c = fgetc(fp); - if (c == EOF) { - return false; - } - *out = (uint8_t)c; - return true; -} - - -static bool readBytes(FILE *fp, void *dst, size_t n) { - return fread(dst, 1, n, fp) == n; -} - - bool stLevelLoad(StLevelT *out, const char *path) { - FILE *fp; - uint8_t hdr[4]; - uint8_t nameLen; - uint8_t i; - size_t cells; - - memset(out, 0, sizeof(*out)); + FILE *fp; + bool ok; // path is a name relative to DATA/ (e.g. "levels/title.dat"). fp = jlDataOpen(path, "rb"); if (fp == NULL) { return false; } - - if (!readBytes(fp, hdr, 4) || - hdr[0] != ST_LEVEL_MAGIC0 || hdr[1] != ST_LEVEL_MAGIC1 || - hdr[2] != ST_LEVEL_MAGIC2 || hdr[3] != ST_LEVEL_MAGIC3) { - fclose(fp); - return false; - } - - if (!readByte(fp, &nameLen) || nameLen >= sizeof(out->name)) { - fclose(fp); - return false; - } - if (!readBytes(fp, out->name, nameLen)) { - fclose(fp); - return false; - } - out->name[nameLen] = '\0'; - - if (!readByte(fp, &out->tileBankId) || - !readByte(fp, &out->musicId) || - !readByte(fp, &out->bgColor) || - !readByte(fp, &out->borderColor) || - !readByte(fp, &out->taxiSpawnTileX) || - !readByte(fp, &out->taxiSpawnTileY)) { - fclose(fp); - return false; - } - - { - uint8_t xGravByte; - uint8_t yGravByte; - if (!readByte(fp, &out->xAccel) || - !readByte(fp, &out->yAccel) || - !readByte(fp, &xGravByte) || - !readByte(fp, &yGravByte)) { - fclose(fp); - return false; - } - out->xGrav = (int8_t)xGravByte; - out->yGrav = (int8_t)yGravByte; - } - if (!readByte(fp, &out->bgColor1) || - !readByte(fp, &out->bgColor2) || - !readByte(fp, &out->bgColor3) || - !readByte(fp, &out->spriteMc0) || - !readByte(fp, &out->spriteMc1) || - !readByte(fp, &out->sprite0Color) || - !readByte(fp, &out->sprite1Color)) { - fclose(fp); - return false; - } - - if (!readByte(fp, &out->padCount) || out->padCount > ST_MAX_PADS) { - fclose(fp); - return false; - } - for (i = 0u; i < out->padCount; i++) { - if (!readByte(fp, &out->pads[i].tileX) || - !readByte(fp, &out->pads[i].tileY) || - !readByte(fp, &out->pads[i].tileW) || - !readByte(fp, &out->pads[i].standX)) { - fclose(fp); - return false; - } - } - - cells = (size_t)ST_TILEMAP_W * (size_t)ST_PLAYFIELD_ROWS; - if (!readBytes(fp, out->tilemap, cells) || - !readBytes(fp, out->colormap, cells)) { - fclose(fp); - return false; - } - + ok = stLevelParse(out, fp); fclose(fp); - return true; + return ok; } - - diff --git a/examples/spacetaxi/stLevelFile.c b/examples/spacetaxi/stLevelFile.c new file mode 100644 index 0000000..dc59814 --- /dev/null +++ b/examples/spacetaxi/stLevelFile.c @@ -0,0 +1,90 @@ +// Space Taxi -- STL4 level file parser (plain stdio, no JoeyLib), shared +// by the game and the host-side simulation harness. +// +// Layout (all bytes, written by stuff/spacetaxi/romToLevel.py): +// "STL4" +// name[22] $7D78 level name in screen codes +// header[9] $7D00 VIC colour block +// padCount, specialPad, fuelRate +// spawn[5] $7D5B +// templates[8] $7D8F Y accel, X accel, Y gravity, X gravity (LE words) +// pads[10][8] $7D0A raw slots +// screen[1000], color[1000] +// levelIndex +// spriteCount, then spriteCount x (ptr, 63 bytes) +// hookData[616] $7D98..$7FFF + +#include +#include + +#include "stSim.h" + + +static bool readBlock(FILE *fp, void *dst, size_t n); + + +static bool readBlock(FILE *fp, void *dst, size_t n) { + return fread(dst, 1, n, fp) == n; +} + + +bool stLevelParse(StLevelT *out, FILE *fp) { + uint8_t hdr[4]; + uint8_t counts[3]; + uint8_t templates[8]; + uint8_t slots[ST_MAX_PADS][8]; + uint8_t k; + + memset(out, 0, sizeof(*out)); + if (!readBlock(fp, hdr, 4u) || hdr[0] != 'S' || hdr[1] != 'T' || hdr[2] != 'L' || hdr[3] != '4') { + return false; + } + if (!readBlock(fp, out->name, ST_LEVEL_NAME_CHARS)) { + return false; + } + out->name[ST_LEVEL_NAME_CHARS] = 0u; + if (!readBlock(fp, out->header, 9u) || !readBlock(fp, counts, 3u) || !readBlock(fp, out->spawn, 5u)) { + return false; + } + out->padCount = counts[0]; + out->specialPad = counts[1]; + out->fuelRate = counts[2]; + if (!readBlock(fp, templates, 8u)) { + return false; + } + out->accelY = (uint16_t)(templates[0] | ((uint16_t)templates[1] << 8)); + out->accelX = (uint16_t)(templates[2] | ((uint16_t)templates[3] << 8)); + out->gravY = (uint16_t)(templates[4] | ((uint16_t)templates[5] << 8)); + out->gravX = (uint16_t)(templates[6] | ((uint16_t)templates[7] << 8)); + if (!readBlock(fp, slots, sizeof(slots))) { + return false; + } + for (k = 0u; k < ST_MAX_PADS; k++) { + out->pads[k].x1Hi = slots[k][0]; + out->pads[k].x1Lo = slots[k][1]; + out->pads[k].x2Hi = slots[k][2]; + out->pads[k].x2Lo = slots[k][3]; + out->pads[k].row = slots[k][4]; + out->pads[k].passMsb = slots[k][5]; + out->pads[k].passCol = slots[k][6]; + out->pads[k].unused = slots[k][7]; + } + if (!readBlock(fp, out->screen, ST_SCREEN_CELLS) || !readBlock(fp, out->color, ST_SCREEN_CELLS)) { + return false; + } + if (!readBlock(fp, &out->levelIndex, 1u) || !readBlock(fp, &out->spriteCount, 1u)) { + return false; + } + if (out->spriteCount > ST_MAX_LEVEL_SPRITES) { + return false; + } + for (k = 0u; k < out->spriteCount; k++) { + if (!readBlock(fp, &out->sprites[k].ptr, 1u) || !readBlock(fp, out->sprites[k].bitmap, ST_SPRITE_BYTES)) { + return false; + } + } + if (!readBlock(fp, out->hookData, ST_HOOK_BYTES)) { + return false; + } + return true; +} diff --git a/examples/spacetaxi/stPassenger.c b/examples/spacetaxi/stPassenger.c deleted file mode 100644 index 7aab2e7..0000000 --- a/examples/spacetaxi/stPassenger.c +++ /dev/null @@ -1,318 +0,0 @@ -// Space Taxi -- passenger AI (dynamic fare model). -// -// The C64 game has NO static per-level fare list (verified: the prior -// port's "circular A->B->..->A" fares were fabricated). Both the SPAWN -// pad and the DELIVERY destination are chosen at RUNTIME by RNG: -// -// * padHoverSetup ($6537) picks a random free pad in [1..padCount] for -// the passenger to appear on, and again for where they want to go. -// * When all pads are consumed (single-pad screens, notably level A), -// the destination becomes the "UP PLEASE!" sentinel ($0B, $6CB8): -// the fare is delivered by flying UP through the top-wall -// transporter opening. -// * "PAD n PLEASE" is drawn from the runtime destination index+'0' -// ($671E: gActiveSpriteIdx + $30); pads are numeric, not lettered. -// -// This port keeps ONE active fare at a time (spawn -> board -> deliver -> -// spawn next), a faithful reduction of the C64's multi-slot queue. The -// RNG here is a small port-side LCG (the exact C64 RNG sequence isn't -// observable and depends on live seed state); it only needs to pick a -// plausible free pad each fare. -// -// Scoring is flat-rate per the C64 original ($43B9 BCD blob added at -// success-stage 6 / $6742): a delivered fare adds ST_FARE_SCORE. - -#include - -#include "spacetaxi.h" - - -#define ST_PASSENGER_H_PX 16 -#define ST_PASSENGER_H_TILES (ST_PASSENGER_H_PX / ST_TILE_PIXELS) -// The waiting passenger's in-place wave (and every beam/walk step) -// advances every 3 game ticks -- the C64's $715E == 3 reload. Since -// stPassengerTick is now called once per fixed game tick (~30 Hz), -// this is a plain tick counter, in lockstep with the physics. -#define ST_WAVE_STEP_TICKS 3u - -// Verified via emulator trace of $4354 (BCD-add) with the $43B9 blob: -// a basic delivered fare adds '5' at the ones digit of the DDDD.DD HUD -// score. See stuff/spacetaxi/trace.py. -#define ST_FARE_SCORE 5u - -// Spawn cadence (C64 idle ticker $660B): while no fare is active, each -// frame rolls rand%100 and spawns on roll < 3, else counts a 100-frame -// cap down to a forced spawn -- a fresh fare appears within ~1-2 seconds. -#define ST_SPAWN_ROLL_THRESH 3u -#define ST_SPAWN_CAP_FRAMES 100 - - -static uint32_t gRng; -static uint8_t gWaveTicks; // game ticks since the last wave/step advance -// Frames until the next fare is forced to spawn (mirrors gDeathStage0Rng). -static int16_t gSpawnCountdown; -// Pad deliveries completed on the current screen. Destination pads are -// "used up" as fares are delivered (C64 gSpriteSlots/gFareSlotCount); once -// only the last pad would remain free, the next fare wants "UP PLEASE!" -// (fly up to the next screen), which is what makes each screen finite. -static uint8_t gDeliveredThisScreen; - - -static bool arrivedAtTarget(const StPassengerT *p); -static void deliverFare(StGameT *game, StPassengerT *p); -static void pickDestination(StGameT *game, StPassengerT *p); -static void placeOnPad(StPassengerT *p, const StLevelT *level, uint8_t padIdx); -static uint8_t rngPad(uint8_t n); -static void spawnPassenger(StGameT *game); -static void walkStride(StPassengerT *p); - - -// Walk arrival test. The C64 aligns the stride with AND #$FE and -// compares ((x ^ target) & ~1) == 0; the port's +/-2 window is the -// same idea, robust to an odd captured taxi X. -static bool arrivedAtTarget(const StPassengerT *p) { - int16_t d = (int16_t)(p->x - p->targetX); - return d >= -2 && d <= 2; -} - - -static void deliverFare(StGameT *game, StPassengerT *p) { - game->score += ST_FARE_SCORE; - stAudioSfxDropoff(); - p->active = false; - gDeliveredThisScreen++; - // The next fare appears after the spawn cadence (see stPassengerTick), - // not instantly. - gSpawnCountdown = ST_SPAWN_CAP_FRAMES; -} - - -// On boarding, choose where the fare wants to go. Destination pads get -// used up as the screen's fares are delivered: once delivering would leave -// no fresh pad (single-pad screens, or after padCount-1 deliveries), the -// fare wants "UP PLEASE!" -- the player flies up to the next screen. -static void pickDestination(StGameT *game, StPassengerT *p) { - uint8_t pc = game->level.padCount; - uint8_t d; - - if (pc <= 1u || gDeliveredThisScreen >= (uint8_t)(pc - 1u)) { - p->destPad = ST_DEST_TRANSPORTER; - return; - } - d = rngPad(pc); - if (d == p->currentPad) { - d = (uint8_t)((d + 1u) % pc); - } - p->destPad = d; -} - - -// Stand the passenger at the pad's real stand column (pad->standX, the -// C64 slot byte6 = gPassenger1Col). pad->tileY is the contact-surface -// row; the fare's feet sit on it, so the sprite top-left is two tiles up. -static void placeOnPad(StPassengerT *p, const StLevelT *level, uint8_t padIdx) { - const StPadT *pad = &level->pads[padIdx]; - - p->currentPad = padIdx; - p->x = (int16_t)pad->standX; - p->y = (int16_t)((pad->tileY - ST_PASSENGER_H_TILES) * ST_TILE_PIXELS); -} - - -// Small LCG -> uniform pad index in [0, n-1]. n==0 guarded to 0. -static uint8_t rngPad(uint8_t n) { - if (n == 0u) { - return 0u; - } - gRng = gRng * 1103515245u + 12345u; - return (uint8_t)(((gRng >> 16) & 0x7FFFu) % n); -} - - -// One +/-2 px walk stride ($6D66 rightward, $6D68 leftward), with -// the stride cel alternation on waitPhase's low bit. -static void walkStride(StPassengerT *p) { - if (p->targetX > p->x) { - p->x = (int16_t)(p->x + 2); - } else if (p->targetX < p->x) { - p->x = (int16_t)(p->x - 2); - } - p->waitPhase ^= 1u; -} - - -static void spawnPassenger(StGameT *game) { - StPassengerT *p = &game->passengers[0]; - uint8_t padIdx; - - if (game->level.padCount == 0u) { - return; - } - padIdx = rngPad(game->level.padCount); - - p->active = true; - p->onboard = false; - p->phase = ST_PASS_BEAM_IN; - p->waitPhase = 0u; - p->destPad = 0u; - placeOnPad(p, &game->level, padIdx); -} - - -void stPassengerReset(StGameT *game) { - uint8_t i; - - gWaveTicks = 0u; - gDeliveredThisScreen = 0u; - gSpawnCountdown = ST_SPAWN_CAP_FRAMES; - // Deterministic per-screen seed: varies the pad sequence level to - // level without needing a wall-clock source. - gRng = 0x2545F491u ^ ((uint32_t)game->levelIndex * 2654435761u); - - for (i = 0u; i < ST_MAX_PASSENGERS; i++) { - game->passengers[i].active = false; - } - // The screen starts empty; the first fare appears after the spawn - // cadence in stPassengerTick. -} - - -void stPassengerTick(StGameT *game) { - StPassengerT *p = &game->passengers[0]; - StTaxiT *t = &game->taxi; - - if (!p->active) { - // Idle: a fresh fare appears after the C64 spawn cadence. Never - // spawn mid-crash (the C64 gates the idle ticker on - // gCollisionPhase == 0). - if (t->crashTicks == 0u) { - if (gSpawnCountdown > 0) { - gSpawnCountdown--; - } - if (rngPad(100u) < ST_SPAWN_ROLL_THRESH || gSpawnCountdown <= 0) { - spawnPassenger(game); - } - } - return; - } - - // One shared step clock (the C64's 3-game-tick cadence, $715E == 3) - // drives every phase: beams, wave, and walk strides. Called once - // per fixed game tick, so this is a plain tick count. - { - bool stepDue = (++gWaveTicks >= ST_WAVE_STEP_TICKS); - if (stepDue) { - gWaveTicks = 0u; - } - - switch (p->phase) { - case ST_PASS_BEAM_IN: - // Materialize at standX: cels $CB..$C7 (5 steps). - if (stepDue && ++p->waitPhase >= 5u) { - p->phase = ST_PASS_WAIT; - p->waitPhase = 0u; - } - break; - - case ST_PASS_WAIT: - // Stand still and wave through the $66D6 cycle. - if (stepDue) { - p->waitPhase = (uint8_t)((p->waitPhase + 1u) & 3u); - } - if (t->landed && t->onPad == p->currentPad) { - // $6811 captures the taxi X ONCE at landing; the - // walk aims there even if the cab lifts off. - p->targetX = (int16_t)(t->x >> ST_SUBPIXEL_SHIFT); - p->phase = ST_PASS_WALK_TO_CAB; - p->waitPhase = 0u; - } - break; - - case ST_PASS_WALK_TO_CAB: - if (stepDue) { - walkStride(p); - if (arrivedAtTarget(p)) { - if (t->landed && t->onPad == p->currentPad) { - p->phase = ST_PASS_BOARD_OUT; - p->waitPhase = 0u; - } else { - // Cab left early: walk home and wait again. - // (Not byte-traced -- the C64 cab cannot - // normally leave mid-walk; this just keeps - // the state machine sane if ours does.) - p->targetX = (int16_t)game->level.pads[p->currentPad].standX; - p->phase = ST_PASS_WALK_TO_PAD; - p->waitPhase = 0u; - } - } - } - break; - - case ST_PASS_BOARD_OUT: - // Shrink into the cab: cels $C7..$CB (5 steps). - if (stepDue && ++p->waitPhase >= 5u) { - p->onboard = true; - p->phase = ST_PASS_RIDING; - p->waitPhase = 0u; - pickDestination(game, p); - stAudioSfxPickup(); - } - break; - - case ST_PASS_RIDING: - // Deliver by landing on the destination pad. (UP-PLEASE - // fares deliver on the transporter fly-up instead; see - // stPassengerTransporterExit.) - if (p->destPad != ST_DEST_TRANSPORTER && - t->landed && - t->onPad < game->level.padCount && - t->onPad == p->destPad) { - p->onboard = false; - p->phase = ST_PASS_DROP_IN; - p->waitPhase = 0u; - p->x = (int16_t)(t->x >> ST_SUBPIXEL_SHIFT); - p->targetX = (int16_t)game->level.pads[p->destPad].standX; - p->currentPad = p->destPad; - p->y = (int16_t)((game->level.pads[p->destPad].tileY - - ST_PASSENGER_H_TILES) * ST_TILE_PIXELS); - } - break; - - case ST_PASS_DROP_IN: - // Materialize beside the cab: cels $CB..$C7. - if (stepDue && ++p->waitPhase >= 5u) { - p->phase = ST_PASS_WALK_TO_PAD; - p->waitPhase = 0u; - } - break; - - case ST_PASS_WALK_TO_PAD: - if (stepDue) { - walkStride(p); - if (arrivedAtTarget(p)) { - p->phase = ST_PASS_DROP_OUT; - p->waitPhase = 0u; - } - } - break; - - case ST_PASS_DROP_OUT: - // Shrink out at standX -- fare complete. - if (stepDue && ++p->waitPhase >= 5u) { - deliverFare(game, p); - } - break; - } - } -} - - -void stPassengerTransporterExit(StGameT *game) { - StPassengerT *p = &game->passengers[0]; - - if (p->active && p->onboard && p->destPad == ST_DEST_TRANSPORTER) { - game->score += ST_FARE_SCORE; - stAudioSfxDropoff(); - p->active = false; - } -} diff --git a/examples/spacetaxi/stRender.c b/examples/spacetaxi/stRender.c index c15eea6..276183b 100644 --- a/examples/spacetaxi/stRender.c +++ b/examples/spacetaxi/stRender.c @@ -1,1395 +1,499 @@ -// Space Taxi -- tile bank + sprite rendering. +// Space Taxi -- renderer: the simulation's screen RAM, colour RAM, +// charset and sprite frame onto the JoeyLib stage. // -// Loads native (.tbk / .spr) assets at startup via jlTileBankLoad -// and jlSpriteBankLoad. Tile bytes are per-target planar; sprite -// data is cross-target chunky 4bpp (the Phase 11 walker reads -// chunky and c2p's inline at draw time). Per-frame work: save- -// under for moving sprites, draw, restore-under next frame. The -// static tilemap is committed once per scene change. -// -// Source PNGs live in assets/ and are baked at build time by -// tools/assetbake/assetbake.py: -// font.png -> font.tbk (1000-tile 40x25 glyph sheet) -// tiles/tbankN.png -> tiles/tbankN.tbk (256-tile playfield bank) -// sprites/sprites.png -> sprites/sprites.spr (9x3 grid of 3x3 -// 24x24 cels: row 0 taxi, row 1 passenger, row 2 flame) +// The screen is 40x25 character cells painted from the live charset +// (the game edits glyphs at runtime: the transporter hatch, the title +// logo flip-book, the laser beams), one JoeyLib tile per glyph built +// on a scratch surface and cached by bitmap so an animated glyph is +// built once per shape. Only the cells the simulation listed as dirty +// are repainted. The eight VIC sprites are drawn from their bitmaps +// through a small cache of JoeyLib sprites keyed on (pointer, colour, +// multicolour mode), with save-under and LIFO restore. Sprite 0 has +// VIC priority, so the draw order is sprite 7 first, sprite 0 last, +// and a sprite that has not moved (and has nothing repainted under +// it) simply stays on the stage: only it and everything drawn after +// it are undrawn and redrawn when it changes. -#include -#include #include #include "spacetaxi.h" -// All STAXI example sources share the STAXI load segment so the -// IIgs binary's _ROOT bank stays under 64 KB. No-op on other ports. - -// Tile bank files are per-level: each level's .dat references a -// numeric bank id, and the host loads tiles/tbankN.tbk on demand -// (cached so re-entry on the same id is free). -// -// Per JoeyLib convention: each app installs into its own subdir -// of bin/, with runtime assets under DATA/. The DOS binary cwd's -// to the app dir when launched, so these paths are relative. -// DOS 8.3 filename limit: "tilebank0.tbk" is 9.3, fopen fails under -// DOSBox strict 8.3. Shortened to "tbank%u.tbk" (6.3) so all four -// targets can use the same filenames without per-platform aliasing. -#define ST_TILE_BANK_PATH_FMT "tiles/tbank%u.tbk" -#define ST_SPRITE_SHEET_PATH "sprites/sprites.spr" -#define ST_SPRITE_SHEET_SPC "sprites/sprites.spc" -#define ST_FONT_PATH "font.tbk" - -#define ST_TILE_BANK_MAX 256u - -// Font sheet layout: 320x200 indexed PNG = 40x25 grid of 8x8 glyphs, -// 1000 tiles total. asciiMap[c] packs the (col, row) location for -// ASCII c into a uint16_t; jlDrawText looks the tile up and pastes -// it as a transparent-on-color-0 glyph. -#define ST_FONT_COLS 40 -#define ST_FONT_ROWS 25 -#define ST_FONT_TILES_MAX (ST_FONT_COLS * ST_FONT_ROWS) - -// Sprite sheet is 9 cols x 3 rows of 24x24 (= 3x3 tile) cels. Row 0 -// is the taxi (4 cels used, rest blank), row 1 is the passenger (9 -// cels), row 2 is the flame (8 cels). Cells lay out left-to-right -// top-to-bottom in the .spr blob's cellCount = 27. -#define ST_SPRITE_SHEET_COLS 9 -#define ST_SPRITE_SHEET_CELS 55 /* 11 cols x 5 rows: taxi, passenger, flame, warp, death */ -#define ST_SPRITE_TAXI_FIRST 0 -#define ST_SPRITE_PASS_FIRST 11 -#define ST_SPRITE_FLAME_FIRST 22 -// Taxi sprite: 24x24 px = 3 tiles wide x 3 tiles tall. The port's -// sprite asset (extractSprites.py, real C64 art from raw.bin) lays -// out sheet row 0 in the live sprite-pointer order: -// cel 0 = $C1 (right, gear DOWN) cel 1 = $C0 (right, gear up) -// cel 2 = $DC (left, gear up) cel 3 = $DD (left, gear DOWN) -// $619B selects the base pair by held direction (LEFT -> $DC pair, -// RIGHT -> $C0 pair, otherwise keep the last facing) and preserves -// bit 0 = the LANDING GEAR state (fire-toggled, $63DD). The in- -// flight body NEVER animates -- the cel is a static function of -// (facing, gearDown): -// cel = facingLeft ? (2 + gearDown) : (1 - gearDown) -// (The $6A95 EOR that an earlier port version modeled as a flicker -// is the crash-debris animation, $CC<->$CD during the fall.) -#define ST_TAXI_W_PX 24 -#define ST_TAXI_H_PX 24 -#define ST_TAXI_CEL_COUNT 4 -// Transporter shrink-warp cels (sheet row 3): the SEVEN drawn cels -// $E2..$E8 of the $5CB9 chain, identity-mapped to warp steps 0..6. -// $5CDA ends the warp when the pointer REACHES $E9 and hides the -// sprite ($718E = 0) -- $E9 is never displayed, so step 7 draws -// nothing at all. -#define ST_SPRITE_WARP_FIRST 33 -#define ST_WARP_CEL_COUNT 7 - -// Flame placeholder fallback (only used if real sprite-2 cel is -// missing): a small bright rectangle below the cab while thrusting. -// $6D6A in the asm positions sprite 2 at (taxi_col - 2, taxi_row); -// the real cels are extracted at runtime as flameCels[0..7]. -#define ST_FLAME_W_PX 8 -#define ST_FLAME_H_PX 8 -#define ST_FLAME_OFFSET_X_PX ((ST_TAXI_W_PX / 2) - (ST_FLAME_W_PX / 2)) -#define ST_FLAME_OFFSET_Y_PX (ST_TAXI_H_PX - 2) - -// Real flame sprite cels (24x24, from raw.bin sprite ptrs in $6DB0 -// table). Indexed by direction-mask -> cel via kFlameCelByDirMask -// below. $6D6A's parity bit ($716C) flickers the flame off every -// other GAME TICK (2 video frames, so a 12.5Hz PAL strobe that fuses -// on a CRT). The port reproduces the LOOK with a wall-clock virtual -// strobe (flameStrobeOn): at high frame rates it yields the authentic -// shimmer; a rendered frame long enough to span a whole strobe phase -// (low-fps IIgs) shows the flame continuously instead of a 7.5Hz -// blink that reads as "no flame". -#define ST_FLAME_CEL_W_PX 24 -#define ST_FLAME_CEL_H_PX 24 -#define ST_FLAME_CEL_COUNT 8 -#define ST_FLAME_STROBE_MS 40u /* one $716C phase = 1 PAL game tick */ - -// Passenger: 24x24 (C64 hardware sprite size is 24x21; we pad 3 rows -// transparent at the bottom in the asset extraction to keep a square -// cell). Cels: -// 0/1 = walk-LEFT alternation ($C4 / $C5) for the title walk -// 2-7 = boarding sequence ($C6 / $C7 / $C8 / $C9 / $CA / $CB) per -// $67A6 incrementing gSpr1PtrShadow from $C6 toward $CC. -#define ST_PASSENGER_W_PX 24 -#define ST_PASSENGER_H_PX 24 -#define ST_PASSENGER_CEL_COUNT 11 -#define ST_PASSENGER_CEL_WALK0 0 /* $C4 walk LEFT a */ -#define ST_PASSENGER_CEL_WALK1 1 /* $C5 walk LEFT b */ -#define ST_PASSENGER_CEL_BOARD0 2 /* maps to C64 ptr $C6 */ -#define ST_PASSENGER_CEL_SPARKLE 8 /* C64 ptr $D9 -- 3rd sparkle cel */ -#define ST_PASSENGER_CEL_WALKR0 9 /* $C2 walk RIGHT a */ -#define ST_PASSENGER_CEL_WALKR1 10 /* $C3 walk RIGHT b */ - -// Crash-death cels (sheet row 4): $CC/$CD debris alternation during -// the fall, then the $CE..$D1 ground-impact walk, then hidden. -#define ST_DEATH_CEL_COUNT 6 -#define ST_SPRITE_DEATH_FIRST 44 - -// Startup progress bar: 2 load steps (font, sprite bank) + the 36 -// per-cel compiles (4 taxi + 11 passenger + 8 flame + 7 warp + -// 6 death). -#define ST_LOAD_STEPS_TOTAL 38 -#define ST_LOAD_BAR_X 60 -#define ST_LOAD_BAR_Y 110 -#define ST_LOAD_BAR_H 6 -#define ST_LOAD_BAR_COLOR 1u /* white fill */ -#define ST_LOAD_BAR_FRAME_COLOR 6u /* blue frame, cab color */ - -// Library-wide worst case (16-px-group window on planar ports). -#define ST_SPRITE_BACKUP_BYTES JOEY_SPRITE_BACKUP_BYTES(ST_TAXI_W_PX / 8, ST_TAXI_H_PX / 8) - +#define ST_SPRITE_TILES 3u +// The IIgs reaches its globals DBR-relative, so all of BSS must fit the +// entry bank below the I/O window; the caches are sized down there. +#if defined(__W65816__) + #define ST_SPRITE_CACHE 24u + #define ST_CELL_CACHE 24u +#else + #define ST_SPRITE_CACHE 72u + // Pre-coloured cells keyed direct-mapped by character + colours, so a + // full 256 covers every (char, fg, bg=black) with no collisions. + #define ST_CELL_CACHE 256u +#endif +#define ST_SPRITE_BACKUP_BYTES JOEY_SPRITE_BACKUP_BYTES(ST_SPRITE_TILES, ST_SPRITE_TILES) +#define ST_SPRITE_PX (8 * ST_SPRITE_TILES) typedef struct { - jlTileT tiles[ST_TILE_BANK_MAX]; - bool tileValid[ST_TILE_BANK_MAX]; - uint8_t currentBankId; // which tbank%u.tbk is loaded (0xFF = none) - bool bankLoaded; - jlSpriteT *taxiCels[ST_TAXI_CEL_COUNT]; - jlSpriteT *warpCels[ST_WARP_CEL_COUNT]; - jlSpriteT *deathCels[ST_DEATH_CEL_COUNT]; - jlSpriteT *flameCels[ST_FLAME_CEL_COUNT]; - jlSpriteBackupT flameBackup; - uint8_t flameBackupMem[ST_SPRITE_BACKUP_BYTES] __attribute__((aligned(2))); - bool flameHasBackup; - jlSpriteT *passengerCels[ST_PASSENGER_CEL_COUNT]; - jlSurfaceT *fontSurface; // glyph surface, built from font.tbk at load - uint16_t asciiMap[128]; - bool fontReady; - jlSpriteBackupT taxiBackup; - uint8_t taxiBackupMem[ST_SPRITE_BACKUP_BYTES] __attribute__((aligned(2))); - jlSpriteBackupT passengerBackup[ST_MAX_PASSENGERS]; - uint8_t passengerBackupMem[ST_MAX_PASSENGERS][ST_SPRITE_BACKUP_BYTES] __attribute__((aligned(2))); - bool taxiHasBackup; - bool passengerHasBackup[ST_MAX_PASSENGERS]; - uint32_t flamePrevMs; // flameStrobeOn frame-span tracker - uint32_t deathMs; // crash-anim step anchor - uint8_t deathStep; // impact-walk position (0..4) - bool deathActive; // crash anim state latched - // Tilemap repaint gating: the static playfield art only needs to - // be blitted to the stage once per scene change, not every frame. - // Per-frame full repaint blows the per-frame budget on emulated - // 386 in DOSBox and exposes tearing as the paint races the raster. - bool tilemapDirty; - const StLevelT *lastLevel; + jlSpriteT *sprite; + uint8_t ptr; + uint8_t color; + uint8_t multi; + uint8_t mc0; + uint8_t mc1; + uint8_t level; // level index the bitmap came from (level sprites) + bool used; + uint32_t lastUse; // for least-recently-used eviction +} StSpriteCacheT; + +typedef struct { + uint8_t bits[8]; + uint8_t fg; + uint8_t bg; + jlTileT tile; + bool used; +} StCellCacheT; + +// What is drawn in a draw slot (slot 0 = sprite 7 ... slot 7 = sprite 0). +typedef struct { + bool drawn; + uint16_t x; + uint8_t y; + uint8_t ptr; + uint8_t color; + uint8_t multi; +} StDrawnT; + +typedef struct { + jlSurfaceT *scratch; + StCellCacheT cells[ST_CELL_CACHE]; + uint32_t cacheStamp; + StSpriteCacheT cache[ST_SPRITE_CACHE]; + jlSpriteBackupT backup[ST_HW_SPRITES]; + uint8_t backupMem[ST_HW_SPRITES][ST_SPRITE_BACKUP_BYTES] __attribute__((aligned(2))); + StDrawnT slot[ST_HW_SPRITES]; + uint8_t lastPresentFrame; } StRenderStateT; static StRenderStateT gRender; - -static bool loadTileBank(uint8_t bankId); -static bool loadSpriteSheet(jlSurfaceT *stage); -static bool loadFontSheet(jlSurfaceT *stage); -static void loadProgress(jlSurfaceT *stage, uint8_t step); -static bool flameStrobeOn(void); -static int16_t passCelForPtr(uint8_t ptr); -static void buildAsciiMap(void); -static void destroySprites(void); - -// ---- Title intro + demo -------------------------------------------------- -// -// Faithful to the C64 sequence (see disassembly $4A03 et al): -// -// Stage 2 ($4A17): wait $5A=90 frames running $66B7 (sparkle/effect) -// before the passenger appears. -// Stage 3 ($4A24): passenger sprite walks horizontally toward the cab, -// one tick per $715D countdown. $6D0D moves $7176 -// (passenger col) +/- 2 each tick based on relative -// pad-hover X, toggling sprite cel via $7161 parity. -// Advances when passenger col == $28 (= 40, at cab). -// Stage 4 ($4A45): JMP $67A6 (transition). -// Stage 5 ($4A48): forces gInputDirMask = $01 (UP). Calls flameSpriteUpdate. -// Cab climbs until gTaxiRow < $14 = 20, then silences -// voice 3 and advances. This is the takeoff. -// Stage 6+: physicsTick now sources its input from the script at -// $0902 via $48F2. The recorded demo plays. -// -// Single cab, single passenger. The 7-sprite init at $4555-$456F just -// parks all hardware sprite slots at (col $AA, row $8C) so they don't -// flash garbage when the title is first shown. - -// (The fabricated title "demo physics" stage and its truncated copy -// of demo stream 01 were deleted 2026-07-21: the real C64 never flies -// the cab around the title after takeoff -- it enters the ATTRACT -// DEMO, now a proper game state (ST_STATE_DEMO) driven by the full -// recorded input streams in stDemoStreams.h.) - -// C64 reference values straight from the asm at $4A03 dispatch + -// titleSpriteSetup ($4525) + sprite-init tables ($4994/$49AC). -#define ST_TITLE_STAGE_SPARKLE 2u // gDeathStage value, $4A17 -#define ST_TITLE_STAGE_WALK 3u // $4A24 -#define ST_TITLE_STAGE_HANDOFF 4u // $4A45 -> $67A6 -#define ST_TITLE_STAGE_LIFTOFF 5u // $4A48 -#define ST_TITLE_STAGE_DONE 6u // intro over; attract demo takes over -#define ST_TITLE_SPARKLE_FRAMES 0x5Au // $47CC LDA #$5A STA $473F -#define ST_TITLE_WALK_TICK_RELOAD 3u // $715E observed in raw.bin -// Walk target = gPadHoverXCol ($715F) = $28 set at $47D8. -// In C64 sprite-X coords. Visible col = sprite-X - 24. -#define ST_TITLE_PAD_HOVER_SX 0x28u -// Cab init from table $4994/$49AC: sprite-X=$28, sprite-Y=$84, ptr=$C1. -#define ST_TITLE_CAB_SX_INIT 0x28 -#define ST_TITLE_CAB_SY_INIT 0x84 -// Passenger init from table: sprite-X=$32 + frac=$01 -> X=$32+$100=306, -// sprite-Y=$84, ptr=$C7. -#define ST_TITLE_PASS_SX_INIT ((int16_t)0x132) // $32 + $100 -#define ST_TITLE_PASS_SY_INIT 0x84 -#define ST_TITLE_PASS_PTR_INIT 0xC7 // $49B4[1]: initial passenger sprite ptr -#define ST_TITLE_PASS_PTR_WALK_LO 0xC4 // $6D68: walk-LEFT cel A -#define ST_TITLE_PASS_PTR_WALK_HI 0xC5 // $6D69: walk-LEFT cel B -#define ST_TITLE_PASS_PTR_BOARD0 0xC6 // boarding sequence start -#define ST_TITLE_PASS_PTR_BOARDED 0xCC // $67B8: advance when ptr == $CC - -// $66D6 sparkle cel table -- cycled by $66B7 during stage 2. -// Each entry shown for $715E (= 3) frames; one full cycle = 12 frames. -// The $66D6 wave/sparkle cycle {$C6,$C7,$D9,$C7}: the title sparkle -// AND the waiting passenger's in-place wave both step through it (the -// gameplay passenger never moves -- he stands at standX and waves). -static const uint8_t kPassengerWaveCels[4] = { 0xC6, 0xC7, 0xD9, 0xC7 }; - -// Crash impact walk cadence ($6B2A: reload starts at 2 ticks and -// INCs each step -- 2,3,4,5 ticks for cels $CE..$D1 at the NTSC -// 33 ms game tick), then the sprite hides. -static const uint16_t kDeathStepMs[4] = { 66u, 100u, 133u, 166u }; -#define ST_DEATH_TUMBLE_MS 66u /* $CC/$CD swap every 2 game ticks */ - -// Map direction-mask (CIA1 PortA bits after EOR #$FF: bit0=UP bit1=DOWN -// bit2=LEFT bit3=RIGHT) to a flameCels[] index, mirroring the C64 -// table at $6DB0. Entries with no flame (no input, or invalid combos -// like UP+DOWN / LEFT+RIGHT / four-way) return -1. -static const int8_t kFlameCelByDirMask[16] = { - -1, /* 0 no input */ - 0, /* 1 UP -> $D8 */ - 1, /* 2 DOWN -> $D4 */ - -1, /* 3 UP+DOWN invalid */ - 2, /* 4 LEFT -> $D5 */ - 3, /* 5 UP+LEFT -> $D2 */ - 4, /* 6 DOWN+LEFT -> $D1 */ - -1, /* 7 */ - 5, /* 8 RIGHT -> $D7 */ - 6, /* 9 UP+RIGHT -> $D3 */ - 7, /* 10 DOWN+RIGHT -> $D6 */ - -1, -1, -1, -1, -1 +// The VIC-II palette in register order, $0RGB. +static const uint16_t kC64Palette[16] = { + 0x0000, 0x0FFF, 0x0833, 0x06BB, 0x0839, 0x05A4, 0x0438, 0x0BC7, + 0x0852, 0x0540, 0x0B66, 0x0555, 0x0777, 0x09E8, 0x076C, 0x09AA }; -// Stage-5 advance gate: $4A5F CMP #$14 BCC $4A64. Cab row in sprite-Y -// coords; visible row = sprite-Y - 50, so $14 = 20 = visible row -30. -#define ST_TITLE_CAB_TAKEOFF_SY 0x14 -// C64-to-visible conversion offsets (sprite hardware borders). -#define ST_SPRITE_X_OFFSET 24 -#define ST_SPRITE_Y_OFFSET 50 - -typedef struct { - uint8_t stage; // mirrors gDeathStage during intro - uint8_t introFrameCount; // mirrors $473F (stage 2 countdown) - uint8_t decayReload; // mirrors $715E - uint8_t decayTimer; // mirrors $715D (countdown per tick) - uint8_t passengerCelParity;// mirrors $7161 (toggled each walk step) - uint8_t sparkleIdx; // mirrors $716E (sparkle cel index 0..3) - uint8_t passengerPtr; // mirrors gSpr1PtrShadow $7198 - uint8_t cabPtr; // mirrors gSpr0PtrShadow $7197 - // Sprite positions in C64 sprite-X / sprite-Y space (16-bit because - // sprite-X is 9 bits when MSB latched). Convert with the OFFSET - // constants above when rendering. - int16_t cabSx; - int16_t cabSy; - int16_t passengerSx; - int16_t passengerSy; - bool passengerVisible; - bool cabVisible; - bool flameVisible; // sprite 2 from $6D6A flameSpriteUpdate - uint8_t flameDirMask; // mirrors $716A passed into $6DB0,X - // Demo (post-intro stage 6) playback state -- script from $0902. - // Logo flip-book + color cycle. The "SPACE TAXI" logo is 103 cells - // of char $84; the C64 ($4827) copies one of the flip frames - // $85..$88 over char $84's bitmap every 4th frame, walking the - // ping-pong table $48A7 = {1,2,3,4,3,2} so each dot rotates end over - // end, and advances the 8-color cycle $489C when the index hits 4. - uint8_t logoFlipDiv; // mirrors $48A6 (mod-4 tick divider) - uint8_t logoFlipIdx; // mirrors $48A5 (0..5 ping-pong index) - uint8_t logoColorIdx; // mirrors $48A4 (0..7 color cycle index) - bool logoNeedsPaint; // flip/color changed (or tilemap repainted) -} StTitleStateT; - -// The single animated logo character. Its bitmap is flip-booked through -// the four frames that follow it in the charset ($85..$88). -#define ST_LOGO_CHAR 0x84u - -static StTitleStateT gTitle; - -static void titleReset(void); -static void titleTick(void); -// Public so stLevel.c can ask "give me the tile object for index N". -const jlTileT *stRenderTileForIndex(uint8_t tileIdx) { - if (!gRender.tileValid[tileIdx]) { - return NULL; +static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx); +static void collectGlyphs(StSimT *sim); +static void dirtyRect(const StSimT *sim, int16_t *x0, int16_t *y0, int16_t *x1, int16_t *y1); +static void dropCache(void); +static void paintCells(jlSurfaceT *stage, StSimT *sim); +static void pasteCell(jlSurfaceT *stage, const StSimT *sim, uint16_t cell); +static bool spriteOnScreen(int16_t px, int16_t py); + + +// The JoeyLib sprite for hardware sprite `idx` of the current frame, +// built on first use from its VIC bitmap and colours. +static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx) { + uint8_t ptr = sim->frame.ptr[idx]; + uint8_t color = sim->frame.color[idx]; + uint8_t multi = (uint8_t)((sim->frame.multiMask >> idx) & 1u); + uint8_t mc0 = multi ? sim->spriteMc0 : 0u; + uint8_t mc1 = multi ? sim->spriteMc1 : 0u; + uint8_t level = (ptr < ST_SPRITE_PTR_FIRST && sim->level != 0) ? sim->level->levelIndex : 0xFFu; + const uint8_t *bm; + StSpriteCacheT *slot = 0; + uint8_t k; + uint8_t row; + + gRender.cacheStamp++; + for (k = 0u; k < ST_SPRITE_CACHE; k++) { + StSpriteCacheT *e = &gRender.cache[k]; + if (e->used && e->ptr == ptr && e->color == color && e->multi == multi && e->mc0 == mc0 && e->mc1 == mc1 && e->level == level) { + e->lastUse = gRender.cacheStamp; + return e->sprite; + } + if (slot == 0 || !e->used || (slot->used && e->used && e->lastUse < slot->lastUse)) { + if (slot == 0 || !slot->used || !e->used || e->lastUse < slot->lastUse) { + slot = e; + } + } } - return &gRender.tiles[tileIdx]; + if (slot->used) { + jlSpriteDestroy(slot->sprite); + slot->sprite = 0; + slot->used = false; + } + bm = stSimSpriteBitmap(sim, ptr); + jlFillRect(gRender.scratch, 0, 0, ST_SPRITE_PX, ST_SPRITE_PX, 0u); + if (bm != 0) { + for (row = 0u; row < ST_SPRITE_H; row++) { + uint32_t bits = ((uint32_t)bm[row * 3u] << 16) | ((uint32_t)bm[row * 3u + 1u] << 8) | bm[row * 3u + 2u]; + uint8_t x; + if (multi != 0u) { + for (x = 0u; x < 12u; x++) { + uint8_t pair = (uint8_t)((bits >> (22u - x * 2u)) & 3u); + uint8_t c = 0u; + if (pair == 1u) { + c = mc0; + } else if (pair == 2u) { + c = color; + } else if (pair == 3u) { + c = mc1; + } + if (c != 0u) { + jlDrawPixel(gRender.scratch, (int16_t)(x * 2u), (int16_t)row, c); + jlDrawPixel(gRender.scratch, (int16_t)(x * 2u + 1u), (int16_t)row, c); + } + } + } else { + for (x = 0u; x < 24u; x++) { + if ((bits & (1uL << (23u - x))) != 0u) { + jlDrawPixel(gRender.scratch, (int16_t)x, (int16_t)row, color); + } + } + } + } + } + slot->sprite = jlSpriteCreateFromSurface(gRender.scratch, 0, 0, ST_SPRITE_TILES, ST_SPRITE_TILES); + if (slot->sprite == 0) { + return 0; + } + // Only the cab, passenger and flame move every tick; the codegen + // arena is theirs. Everything else stays interpreted (drawn once). + // Only the cab, exhaust and passenger move every tick; the codegen + // arena is theirs. Everything else stays interpreted (drawn seldom). + if (idx <= 2u) { + (void)jlSpriteCompile(slot->sprite); + } + slot->lastUse = gRender.cacheStamp; + slot->ptr = ptr; + slot->color = color; + slot->multi = multi; + slot->mc0 = mc0; + slot->mc1 = mc1; + slot->level = level; + slot->used = true; + return slot->sprite; } -// Default 16-entry palette: matches the C64 VIC-II color register -// order so that an asset extracted with the C64 palette (via the -// extractFromDump.py tool) renders with the right colors here. The -// JoeyLib value format is $0RGB (4 bits per channel, top nibble -// unused). Index 0 black, 1 white, then C64 standard order. -static const uint16_t kDefaultPalette[16] = { - 0x0000, // 0 black - 0x0FFF, // 1 white - 0x0833, // 2 red - 0x06BB, // 3 cyan - 0x0839, // 4 purple - 0x05A4, // 5 green - 0x0438, // 6 blue - 0x0BC7, // 7 yellow - 0x0852, // 8 orange - 0x0540, // 9 brown - 0x0B66, // 10 light red - 0x0555, // 11 dark gray - 0x0777, // 12 mid gray - 0x09E8, // 13 light green - 0x076C, // 14 light blue - 0x09AA // 15 light gray -}; +// A changed glyph (the hatch, the logo flip, the laser beams) makes +// every cell that shows it dirty; the cell cache self-invalidates on the +// bitmap compare, so no per-char tile needs rebuilding here. +static void collectGlyphs(StSimT *sim) { + uint16_t ch; + uint16_t cell; + uint8_t dirty[ST_CHARSET_CHARS]; + bool any = false; -// Tile-index range -> placeholder fill color (used when no tile bank -// is loaded). Matches the engine's index-range convention. -static uint8_t placeholderColorFor(uint8_t tileIdx, uint8_t bgColor) { - // Used only when no tile bank is loaded -- debug-rendering. Slot - // indices reference kDefaultPalette above. - if (tileIdx == 0u) { return bgColor; } - if (tileIdx < 64u) { return 6u; } // walls -> blue - if (tileIdx < 128u) { return 5u; } // pads -> green - return 14u; // decor -> light blue + for (ch = 0u; ch < ST_CHARSET_CHARS; ch++) { + dirty[ch] = sim->charDirty[ch]; + if (dirty[ch] != 0u) { + sim->charDirty[ch] = 0u; + any = true; + } + } + if (!any || sim->dirtyAll) { + return; + } + for (cell = 0u; cell < ST_SCREEN_CELLS; cell++) { + if (dirty[sim->screen[cell]] != 0u && sim->cellDirty[cell] == 0u) { + sim->cellDirty[cell] = 1u; + if (sim->dirtyCount < (uint8_t)(sizeof(sim->dirtyList) / sizeof(sim->dirtyList[0]))) { + sim->dirtyList[sim->dirtyCount++] = cell; + } else { + sim->dirtyAll = true; + return; + } + } + } +} + + +// Bounding box (pixels, x1/y1 exclusive) of the cells about to be +// repainted. Empty when x1 <= x0. +static void dirtyRect(const StSimT *sim, int16_t *x0, int16_t *y0, int16_t *x1, int16_t *y1) { + uint8_t k; + + if (sim->dirtyAll) { + *x0 = 0; + *y0 = 0; + *x1 = SURFACE_WIDTH; + *y1 = SURFACE_HEIGHT; + return; + } + *x0 = SURFACE_WIDTH; + *y0 = SURFACE_HEIGHT; + *x1 = 0; + *y1 = 0; + for (k = 0u; k < sim->dirtyCount; k++) { + uint16_t cell = sim->dirtyList[k]; + int16_t cx = (int16_t)((cell % ST_SCREEN_COLS) * 8u); + int16_t cy = (int16_t)((cell / ST_SCREEN_COLS) * 8u); + if (cx < *x0) { + *x0 = cx; + } + if (cy < *y0) { + *y0 = cy; + } + if ((int16_t)(cx + 8) > *x1) { + *x1 = (int16_t)(cx + 8); + } + if ((int16_t)(cy + 8) > *y1) { + *y1 = (int16_t)(cy + 8); + } + } +} + + +static void dropCache(void) { + uint8_t k; + + for (k = 0u; k < ST_SPRITE_CACHE; k++) { + if (gRender.cache[k].used) { + jlSpriteDestroy(gRender.cache[k].sprite); + gRender.cache[k].sprite = 0; + gRender.cache[k].used = false; + } + } +} + + +// Repaint the dirty cells: the list, or everything after an overflow +// or a scene change. +static void paintCells(jlSurfaceT *stage, StSimT *sim) { + uint16_t cell; + uint8_t k; + + if (sim->dirtyAll) { + for (cell = 0u; cell < ST_SCREEN_CELLS; cell++) { + pasteCell(stage, sim, cell); + } + memset(sim->cellDirty, 0, ST_SCREEN_CELLS); + sim->dirtyCount = 0u; + sim->dirtyAll = false; + return; + } + for (k = 0u; k < sim->dirtyCount; k++) { + cell = sim->dirtyList[k]; + pasteCell(stage, sim, cell); + sim->cellDirty[cell] = 0u; + } + sim->dirtyCount = 0u; +} + + +// Paste one cell through the coloured-tile cache (direct-mapped on +// character and colours; an entry is valid while its glyph bits match). +static void pasteCell(jlSurfaceT *stage, const StSimT *sim, uint16_t cell) { + uint8_t chr = sim->screen[cell]; + uint8_t fg = sim->color[cell]; + uint8_t bg = sim->bgColor; + const uint8_t *bits = sim->charset[chr]; + uint8_t bx = (uint8_t)(cell % ST_SCREEN_COLS); + uint8_t by = (uint8_t)(cell / ST_SCREEN_COLS); + StCellCacheT *c = &gRender.cells[(uint16_t)(chr + fg * 61u + bg * 7u) % ST_CELL_CACHE]; + + uint8_t row; + uint8_t bit; + + if (c->used && c->fg == fg && c->bg == bg && memcmp(c->bits, bits, 8u) == 0) { + jlTilePaste(stage, bx, by, &c->tile); + return; + } + for (row = 0u; row < 8u; row++) { + uint8_t b = bits[row]; + for (bit = 0u; bit < 8u; bit++) { + jlDrawPixel(gRender.scratch, (int16_t)bit, (int16_t)row, + ((b & (uint8_t)(0x80u >> bit)) != 0u) ? fg : bg); + } + } + jlTileSnap(gRender.scratch, 0u, 0u, &c->tile); + memcpy(c->bits, bits, 8u); + c->fg = fg; + c->bg = bg; + c->used = true; + jlTilePaste(stage, bx, by, &c->tile); +} + + +static bool spriteOnScreen(int16_t px, int16_t py) { + return px < SURFACE_WIDTH && py < SURFACE_HEIGHT && px > -ST_SPRITE_W && py > -(int16_t)ST_SPRITE_PX; +} + + +// --------------------------------------------------------------------------- +// Public +// --------------------------------------------------------------------------- + +void stRenderFrame(jlSurfaceT *stage, StSimT *sim) { + int16_t dx0; + int16_t dy0; + int16_t dx1; + int16_t dy1; + uint8_t first = ST_HW_SPRITES; + uint8_t k; + + // Find the first draw slot whose sprite changed, vanished, or sits + // over cells about to be repainted; it and every later slot are + // undrawn (last first) and redrawn below, the rest stay put. + collectGlyphs(sim); + dirtyRect(sim, &dx0, &dy0, &dx1, &dy1); + for (k = 0u; k < ST_HW_SPRITES; k++) { + uint8_t idx = (uint8_t)(ST_HW_SPRITES - 1u - k); + StDrawnT *d = &gRender.slot[k]; + uint8_t multi = (uint8_t)((sim->frame.multiMask >> idx) & 1u); + int16_t px = (int16_t)((int16_t)sim->frame.x[idx] - ST_SPRITE_X_ORIGIN); + int16_t py = (int16_t)((int16_t)sim->frame.y[idx] - ST_SPRITE_Y_ORIGIN); + bool want = (sim->frame.enableMask & (uint8_t)(1u << idx)) != 0u && spriteOnScreen(px, py); + bool same = (want == d->drawn); + if (same && want) { + same = (d->x == sim->frame.x[idx] && d->y == sim->frame.y[idx] && d->ptr == sim->frame.ptr[idx] && d->color == sim->frame.color[idx] && d->multi == multi); + } + if (same && want && dx1 > dx0) { + if (px < dx1 && (int16_t)(px + ST_SPRITE_W) > dx0 && py < dy1 && (int16_t)(py + ST_SPRITE_PX) > dy0) { + same = false; + } + } + if (!same) { + first = k; + break; + } + } + for (k = ST_HW_SPRITES; k > first; k--) { + StDrawnT *d = &gRender.slot[k - 1u]; + if (d->drawn) { + jlSpriteRestoreUnder(stage, &gRender.backup[k - 1u]); + d->drawn = false; + } + } + paintCells(stage, sim); + for (k = first; k < ST_HW_SPRITES; k++) { + uint8_t idx = (uint8_t)(ST_HW_SPRITES - 1u - k); + StDrawnT *d = &gRender.slot[k]; + jlSpriteT *sp; + int16_t px; + int16_t py; + if ((sim->frame.enableMask & (uint8_t)(1u << idx)) == 0u) { + continue; + } + px = (int16_t)((int16_t)sim->frame.x[idx] - ST_SPRITE_X_ORIGIN); + py = (int16_t)((int16_t)sim->frame.y[idx] - ST_SPRITE_Y_ORIGIN); + if (!spriteOnScreen(px, py)) { + continue; + } + sp = cachedSprite(sim, idx); + if (sp == 0) { + continue; + } + jlSpriteSaveAndDraw(stage, sp, px, py, &gRender.backup[k]); + d->drawn = true; + d->x = sim->frame.x[idx]; + d->y = sim->frame.y[idx]; + d->ptr = sim->frame.ptr[idx]; + d->color = sim->frame.color[idx]; + d->multi = (uint8_t)((sim->frame.multiMask >> idx) & 1u); + } + { + // Sync to the retrace unless the frame already spans more than + // one: a late frame goes out at once rather than waiting again. + uint8_t now = (uint8_t)jlFrameCount(); + if ((uint8_t)(now - gRender.lastPresentFrame) < 2u) { + jlWaitVBL(); + } + jlStagePresent(); + gRender.lastPresentFrame = (uint8_t)jlFrameCount(); + } } void stRenderInit(jlSurfaceT *stage) { + uint8_t k; + memset(&gRender, 0, sizeof(gRender)); - gRender.taxiBackup.bytes = gRender.taxiBackupMem; - gRender.flameBackup.bytes = gRender.flameBackupMem; - gRender.currentBankId = 0xFFu; // no bank loaded yet - for (uint8_t i = 0u; i < ST_MAX_PASSENGERS; i++) { - gRender.passengerBackup[i].bytes = gRender.passengerBackupMem[i]; + for (k = 0u; k < ST_HW_SPRITES; k++) { + gRender.backup[k].bytes = gRender.backupMem[k]; } - - // Install the default 16-color palette on slot 0 and route the - // whole screen through it. Subsequent asset palettes can override - // by writing into slot 1+ via jlPaletteSet / jlScbSetRange. - jlPaletteSet(stage, 0u, kDefaultPalette); + jlPaletteSet(stage, 0u, kC64Palette); jlScbSetRange(stage, 0u, (uint16_t)(SURFACE_HEIGHT - 1u), 0u); - - // Startup feedback: the IIgs spends several seconds streaming the - // banks and compiling sprite cels to their asm routines, all of - // which used to happen against a black screen. Show a growing bar - // immediately; once the font is in, add the LOADING caption. The - // font loads FIRST because text needs its glyph surface. jlSurfaceClear(stage, 0u); - loadProgress(stage, 0u); - if (!loadFontSheet(stage)) { - jlLogF("stRender: ! font load failed (%s)", ST_FONT_PATH); + gRender.scratch = jlSurfaceCreate(); + if (gRender.scratch != 0) { + jlPaletteSet(gRender.scratch, 0u, kC64Palette); + jlScbSetRange(gRender.scratch, 0u, (uint16_t)(SURFACE_HEIGHT - 1u), 0u); + jlSurfaceClear(gRender.scratch, 0u); } - buildAsciiMap(); - loadProgress(stage, 1u); +} - // Tile bank is loaded on demand in stRenderLevel (per-level). - if (!loadSpriteSheet(stage)) { - jlLogF("stRender: ! sprite sheet load failed (%s)", ST_SPRITE_SHEET_PATH); + +// Build (and compile) the cels the game draws every screen -- cab, +// exhaust, passenger, wreck, warp, intro cab and star -- up front, so +// no frame pays for a sprite build mid-play. A bar on the stage shows +// progress on the slower ports. +void stRenderPrewarm(jlSurfaceT *stage, StSimT *sim) { + static const uint8_t kCabPtrs[] = { 0xC0, 0xC1, 0xDC, 0xDD, 0xCC, 0xCD, 0xCE, 0xCF, 0xD0, 0xD1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8 }; + static const uint8_t kPassPtrs[] = { 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xD9 }; + static const uint8_t kFlamePtrs[] = { 0xD8, 0xD4, 0xD5, 0xD2, 0xD1, 0xD7, 0xD3, 0xD6 }; + uint8_t total = (uint8_t)(sizeof(kCabPtrs) + sizeof(kPassPtrs) + sizeof(kFlamePtrs) + 1u); + uint8_t done = 0u; + uint8_t k; + StFrameT saved = sim->frame; + + sim->frame.multiMask = 0x07u; + sim->spriteMc0 = 0x02u; + sim->spriteMc1 = 0x07u; + for (k = 0u; k < sizeof(kCabPtrs); k++) { + sim->frame.ptr[0] = kCabPtrs[k]; + sim->frame.color[0] = 0x06u; + (void)cachedSprite(sim, 0u); + done++; + jlFillRect(stage, 60, 110, (uint16_t)((uint16_t)done * 200u / total), 6u, 1u); + jlStagePresent(); } - titleReset(); + for (k = 0u; k < sizeof(kPassPtrs); k++) { + sim->frame.ptr[1] = kPassPtrs[k]; + sim->frame.color[1] = 0x06u; + (void)cachedSprite(sim, 1u); + done++; + jlFillRect(stage, 60, 110, (uint16_t)((uint16_t)done * 200u / total), 6u, 1u); + jlStagePresent(); + } + for (k = 0u; k < sizeof(kFlamePtrs); k++) { + sim->frame.ptr[2] = kFlamePtrs[k]; + sim->frame.color[2] = 0x07u; + (void)cachedSprite(sim, 2u); + done++; + jlFillRect(stage, 60, 110, (uint16_t)((uint16_t)done * 200u / total), 6u, 1u); + jlStagePresent(); + } + // The intro star is a hires sprite in colour 7 (built, not compiled). + sim->frame.multiMask = 0u; + sim->frame.ptr[6] = 0xDAu; + sim->frame.color[6] = 0x07u; + (void)cachedSprite(sim, 6u); + sim->frame = saved; + jlFillRect(stage, 60, 110, 200u, 6u, 0u); } -bool stRenderTitleIntroDone(void) { - return gTitle.stage == ST_TITLE_STAGE_DONE; -} +// A new scene: nothing drawn is valid any more. +void stRenderSceneChanged(StSimT *sim) { + uint8_t k; - -// Advance the title intro by ONE game tick. The main loop calls this -// on the fixed 30 Hz accumulator (not once per rendered frame) so the -// sparkle / walk / takeoff sequence and the logo flip run at the C64's -// rate on every port -- identical pre-game timing everywhere. -void stRenderTitleAdvance(void) { - titleTick(); -} - - -// Restart the title intro (sparkle -> walk -> takeoff). Called on -// every return to the title so the attract cycle replays -- the C64 -// re-enters the intro the same way after a demo exits. -void stRenderTitleReset(void) { - titleReset(); + for (k = 0u; k < ST_HW_SPRITES; k++) { + gRender.slot[k].drawn = false; + } + stSimDirtyAll(sim); + memset(sim->charDirty, 1, ST_CHARSET_CHARS); } void stRenderShutdown(void) { - destroySprites(); - if (gRender.fontSurface != NULL) { - jlSurfaceDestroy(gRender.fontSurface); - gRender.fontSurface = NULL; - } -} - - -void stRenderDrawText(jlSurfaceT *stage, uint8_t bx, uint8_t by, const char *s) { - if (!gRender.fontReady || gRender.fontSurface == NULL || s == NULL) { - return; - } - jlDrawText(stage, bx, by, gRender.fontSurface, gRender.asciiMap, s); -} - - -void stRenderLevel(jlSurfaceT *stage, const StLevelT *level) { - const jlTileT *tile; - uint8_t bx; - uint8_t by; - uint8_t tileIdx; - size_t i; - - // Swap to the level's tile bank if it's not already loaded. - // First call lands here and pulls tbank.tbk off disk. - if (!gRender.bankLoaded || gRender.currentBankId != level->tileBankId) { - (void)loadTileBank(level->tileBankId); - gRender.tilemapDirty = true; - } - // Scene change detected -> force a repaint. Otherwise this is a - // no-op (the level tilemap data is static between calls). - if (gRender.lastLevel != level) { - gRender.tilemapDirty = true; - gRender.lastLevel = level; - } - if (!gRender.tilemapDirty) { - return; - } - gRender.tilemapDirty = false; - - jlSurfaceClear(stage, level->bgColor); - - for (by = 0u; by < ST_PLAYFIELD_ROWS; by++) { - for (bx = 0u; bx < ST_TILEMAP_W; bx++) { - i = (size_t)by * ST_TILEMAP_W + bx; - tileIdx = level->tilemap[i]; - // tileIdx is uint8_t, naturally bounded to ST_TILE_BANK_MAX=256. - tile = gRender.tileValid[tileIdx] ? &gRender.tiles[tileIdx] : NULL; - if (tile != NULL) { - // C64-style per-cell coloring: tile bitmap is a fixed - // glyph from the charset, colormap[i] is the foreground - // color from color RAM ($D800). Empty tile (idx 0) - // still goes through here as bgColor on both fg/bg -- - // jlTileFill is left for the no-bank-loaded fallback. - uint8_t fg = (uint8_t)(level->colormap[i] & 0x0Fu); - jlTilePasteMono(stage, bx, by, tile, fg, level->bgColor); - } else { - jlTileFill(stage, bx, by, placeholderColorFor(tileIdx, level->bgColor)); - } - } - } -} - - -void stRenderLevelChanged(void) { - // Force the next stRenderLevel call to repaint. Needed because the - // dirty-cache compares lastLevel by pointer, but stLevelLoad - // overwrites *game.level in place -- the pointer stays equal and - // the cache would otherwise skip the new tilemap. - gRender.tilemapDirty = true; - gRender.lastLevel = NULL; -} - - - -// $4A03 dispatcher entry state, from $47C9 postInitPressFire: -// $47C7: A9 02 LDA #$02 -// $47C9: 8D 63 71 STA $7163 ; gDeathStage = 2 -// $47CC: A9 5A LDA #$5A -// $47CE: 8D 3F 47 STA $473F ; gIntroFrameCount -// $47D1: A9 00 LDA #$00 -// $47D3: 8D 60 71 STA $7160 ; gPadHoverXFrac -// $47D6: A9 28 LDA #$28 -// $47D8: 8D 5F 71 STA $715F ; gPadHoverXCol -// $47DB: AD 5E 71 LDA $715E ; decayReload (snapshot $03) -// $47DE: 8D 5D 71 STA $715D ; decayTimer -// Sprite positions from the $49BC table-loop, X=0..7: -// $4994 col[] = 28 32 00 86 9C A2 BA D4 -// $499C frac[] = 00 01 00 00 00 00 00 00 -// $49AC row[] = 84 84 00 E2 E2 E2 E2 E2 -// $49B4 ptr[] = C1 C7 00 DB DE DF E0 E1 -// Sprite 0 (cab) sprite-X = $28 = 40, sprite-Y = $84, ptr $C1. -// Sprite 1 (passenger) sprite-X = $32 + $100 = 306, sprite-Y = $84, -// ptr $C7. (frac=1 latches the X-MSB.) -static void titleReset(void) { - memset(&gTitle, 0, sizeof(gTitle)); - gTitle.stage = ST_TITLE_STAGE_SPARKLE; - gTitle.introFrameCount = ST_TITLE_SPARKLE_FRAMES; - gTitle.decayReload = ST_TITLE_WALK_TICK_RELOAD; - gTitle.decayTimer = ST_TITLE_WALK_TICK_RELOAD; - gTitle.passengerCelParity = 0u; - gTitle.passengerPtr = ST_TITLE_PASS_PTR_INIT; - gTitle.cabPtr = 0xC1; // sprite 0 init ptr - gTitle.cabSx = ST_TITLE_CAB_SX_INIT; - gTitle.cabSy = ST_TITLE_CAB_SY_INIT; - gTitle.passengerSx = ST_TITLE_PASS_SX_INIT; - gTitle.passengerSy = ST_TITLE_PASS_SY_INIT; - gTitle.cabVisible = true; - gTitle.passengerVisible = true; - gTitle.flameVisible = false; - // Force the first tick to be an animation step (3 -> 0 on first ++) - // AND force a logo paint on the first render frame so the flip - // frame + color appear immediately (the tilemap was just repainted - // by stRenderLevelChanged, erasing any prior logo cells). - gTitle.logoFlipDiv = 3u; - gTitle.logoNeedsPaint = true; -} - - -// $4827 logo flip-book + $4861 color cycle. The title's "SPACE TAXI" -// logo is 103 cells of char $84 (verified in the title dump). Every 4th -// frame ($4827: INC $48A6 / AND #$03 / BEQ) the C64 copies one flip -// frame ($85..$88) over char $84's bitmap, walking the ping-pong table -// $48A7 = {1,2,3,4,3,2} -> frames $85,$86,$87,$88,$87,$86 so each dot -// rotates end over end; when the index reaches 4 it advances the 8-entry -// color cycle $489C = {red,orange,yellow,green,blue,lt-blue,cyan,purple} -// (C64 codes, which are this port's palette indices). We reproduce it by -// re-pasting every $84 cell with the current flip-frame tile + color. -// Flip/color advance ($4827 divider + $484D/$4853 index+color). Runs -// once per GAME TICK (called from titleTick), so the flip-book steps -// every 4 game ticks -- the C64's $48A6 mod-4 gate at the 30 Hz main- -// loop rate, frame-rate-independent. Sets logoNeedsPaint when it steps; -// the actual re-paste is deferred to titlePaintLogo in the render pass. -static void titleAdvanceLogo(void) { - gTitle.logoFlipDiv = (uint8_t)((gTitle.logoFlipDiv + 1u) & 3u); - if (gTitle.logoFlipDiv != 0u) { - return; - } - gTitle.logoFlipIdx++; - if (gTitle.logoFlipIdx == 6u) { - gTitle.logoFlipIdx = 0u; - } - if (gTitle.logoFlipIdx == 4u) { - gTitle.logoColorIdx = (uint8_t)((gTitle.logoColorIdx + 1u) & 7u); - } - gTitle.logoNeedsPaint = true; -} - - -// Re-paste every $84 logo cell with the current flip-frame tile + color -// ($489C palette codes = the port's indices). Render-pass only, and only -// when the flip advanced or the tilemap was just repainted -- between -// steps the logo cells sit static on the tilemap. -static void titlePaintLogo(jlSurfaceT *stage, const StLevelT *level) { - static const uint8_t kFlip[6] = { 1u, 2u, 3u, 4u, 3u, 2u }; // $48A7 - static const uint8_t kColor[8] = { 0x02u, 0x08u, 0x07u, 0x05u, - 0x06u, 0x0Eu, 0x03u, 0x04u }; // $489C - const jlTileT *tile; - uint8_t ch; - uint8_t fg; - uint8_t bx; - uint8_t by; - size_t i; - - if (!gTitle.logoNeedsPaint) { - return; - } - gTitle.logoNeedsPaint = false; - - ch = (uint8_t)(ST_LOGO_CHAR + kFlip[gTitle.logoFlipIdx]); - fg = kColor[gTitle.logoColorIdx]; - tile = gRender.tileValid[ch] ? &gRender.tiles[ch] : NULL; - if (tile != NULL) { - for (by = 0u; by < ST_PLAYFIELD_ROWS; by++) { - for (bx = 0u; bx < ST_TILEMAP_W; bx++) { - i = (size_t)by * ST_TILEMAP_W + bx; - if (level->tilemap[i] == ST_LOGO_CHAR) { - jlTilePasteMono(stage, bx, by, tile, fg, level->bgColor); - } - } - } - } -} - - -// $6D0D passenger-walk: toggle $7161 cel parity, compute sign of -// (passenger_X - pad_hover_X), branch to walk-LEFT or walk-RIGHT -// helper. Both load gSpr1PtrShadow from a 2-entry ptr table indexed -// by the cel-parity bit, then advance the column by +/- 2. -// $6D30 LDX $7161 / LDA $6D66,X / STA $7198 (walk RIGHT) -// $6D33: $6D66 = $C2, $6D67 = $C3 -// $6D4B LDX $7161 / LDA $6D68,X / STA $7198 (walk LEFT) -// $6D4E: $6D68 = $C4, $6D69 = $C5 -// In the title intro the passenger starts at sprite-X 306 and walks -// LEFT toward $28, so the LEFT cel table ($C4 / $C5) is the one in -// use. We track the live ptr in gTitle.passengerPtr so the boarding -// stage can keep incrementing from wherever the walk left off. -static void titleStepPassenger(void) { - gTitle.passengerCelParity ^= 1u; - if (gTitle.passengerSx > (int16_t)ST_TITLE_PAD_HOVER_SX) { - gTitle.passengerSx -= 2; - gTitle.passengerPtr = (gTitle.passengerCelParity & 1u) - ? ST_TITLE_PASS_PTR_WALK_HI - : ST_TITLE_PASS_PTR_WALK_LO; - } else if (gTitle.passengerSx < (int16_t)ST_TITLE_PAD_HOVER_SX) { - gTitle.passengerSx += 2; - // Walk-RIGHT path uses $C2/$C3; not used by the title, but - // include it for completeness so any side-entry passenger - // would render correctly. - gTitle.passengerPtr = (gTitle.passengerCelParity & 1u) - ? 0xC3 - : 0xC2; - } -} - - -// $4A03 dispatcher. One iteration per host frame, exactly as the C64 -// runs $4A03 from $47E4 each iteration of the title intro loop. -static void titleTick(void) { - // Logo flip-book + color cycle advance once per game tick (its own - // mod-4 divider gates the actual step), independent of the intro - // stage machine below. - titleAdvanceLogo(); - - switch (gTitle.stage) { - case ST_TITLE_STAGE_SPARKLE: - // $4A17: JSR $66B7 / DEC $473F / BEQ -> INC $7163. - // $66B7 logic (per the asm): - // DEC $715D ; gIntroDecayTimer - // BEQ $66BD ; hit-zero -> cycle a cel - // RTS ; else keep current cel - // $66BD: LDA $715E STA $715D ; reload from gIntroDecayReload - // INC $716E ; sparkleIdx - // LDA $716E AND #$03 STA $716E ; mask 0..3 - // TAX - // LDA $66D6,X STA $7198 ; passenger ptr = sparkle table - // So the passenger ptr cycles $C6 / $C7 / $D9 / $C7 every reload - // frames during the 90-frame sparkle wait, AND the decay timer - // is left in the middle of its countdown when stage 3 takes - // over -- which is why the walk starts after 0-2 frames, not - // the full 3-frame reload. - if (gTitle.decayTimer > 0u) { - gTitle.decayTimer--; - } - if (gTitle.decayTimer == 0u) { - gTitle.decayTimer = gTitle.decayReload; - gTitle.sparkleIdx = (uint8_t)((gTitle.sparkleIdx + 1u) & 0x03u); - gTitle.passengerPtr = kPassengerWaveCels[gTitle.sparkleIdx]; - } - if (gTitle.introFrameCount > 0u) { - gTitle.introFrameCount--; - } - if (gTitle.introFrameCount == 0u) { - gTitle.stage = ST_TITLE_STAGE_WALK; - } - break; - - case ST_TITLE_STAGE_WALK: - // $4A24: DEC $715D / BEQ -> reload from $715E + JSR $6D0D / - // then check (passenger col == $28) && ($7186 == 0) -> advance. - if (gTitle.decayTimer > 0u) { - gTitle.decayTimer--; - } - if (gTitle.decayTimer == 0u) { - gTitle.decayTimer = gTitle.decayReload; - titleStepPassenger(); - if (gTitle.passengerSx == (int16_t)ST_TITLE_PAD_HOVER_SX) { - gTitle.stage = ST_TITLE_STAGE_HANDOFF; - } - } - break; - - case ST_TITLE_STAGE_HANDOFF: - // $4A45: JMP $67A6. - // $67A6: DEC $715D / BEQ -> reload + INC $7198 (passenger - // sprite cel ptr). Advance when ptr == $CC. - if (gTitle.decayTimer > 0u) { - gTitle.decayTimer--; - } - if (gTitle.decayTimer == 0u) { - gTitle.decayTimer = gTitle.decayReload; - gTitle.passengerPtr++; - if (gTitle.passengerPtr >= ST_TITLE_PASS_PTR_BOARDED) { - // $67BD: STA $718F = 0 (pad hover off); INC $7163 to 5. - gTitle.passengerVisible = false; - gTitle.stage = ST_TITLE_STAGE_LIFTOFF; - } - } - break; - - case ST_TITLE_STAGE_LIFTOFF: - // $4A48: LDX #0; LDA #$FE; JSR $4113 (gTaxiRow[0] += -2). - // LDA #$01; STA gInputDirMask. LDA #$C0; STA gSpr0Ptr. - // JSR $6D6A (flame sprite update -- sprite 2 visible). - // LDA gTaxiRow[0]; CMP #$14; BCC $4A64. - gTitle.cabSy -= 2; - gTitle.cabPtr = 0xC0; - gTitle.flameDirMask = 0x01; // UP only - gTitle.flameVisible = true; - if (gTitle.cabSy < (int16_t)ST_TITLE_CAB_TAKEOFF_SY) { - // $4A64-$4A6C: INC gDeathStage; silence voice 3. - gTitle.cabVisible = false; - gTitle.flameVisible = false; - // $4A64 INCs gDeathStage out of the intro: on the C64 the - // machine now enters the rolling attract demo. The port - // signals spacetaxi.c via stRenderTitleIntroDone(). - gTitle.stage = ST_TITLE_STAGE_DONE; - } - break; - - case ST_TITLE_STAGE_DONE: - default: - // Intro finished -- the title art sits static (logo cycle - // continues); spacetaxi.c switches to ST_STATE_DEMO. - break; - } -} - - -void stRenderFrame(jlSurfaceT *stage, const StGameT *game) { - int16_t px; - int16_t py; - uint8_t taxiCel; - uint8_t i; - char buf[32]; - - // Title screen: render the title tilemap (extracted from the C64 - // game's screen RAM at $0400). The "SPACE TAXI" logo is animated by - // titleAnimateLogo below -- the C64 flip-books char $84's bitmap - // ($4827) so each dot rotates end over end while an 8-color cycle - // ($4861) runs; both were missing before. - if (game->state == ST_STATE_TITLE) { - // Restore the area saved under the prev-frame sprites BEFORE - // anything else paints. Without this, jlSpriteDraw leaves the - // old sprite pixels on the stage and the cab/passenger smear - // across the screen as they move ("5 cabs then erase" was the - // titleCycleTick periodic tilemap re-blit fighting the - // accumulated trails). - // C64 sprite priority: sprite 0 (cab) > 1 (passenger) > 2 (flame). - // Draw order each frame: flame, passenger, cab (lowest priority - // first). Restore is the reverse: cab, passenger, flame -- so - // overlapping saved underlays peel off in the right sequence. - if (gRender.taxiHasBackup) { - jlSpriteRestoreUnder(stage, &gRender.taxiBackup); - gRender.taxiHasBackup = false; - } - if (gRender.passengerHasBackup[0]) { - jlSpriteRestoreUnder(stage, &gRender.passengerBackup[0]); - gRender.passengerHasBackup[0] = false; - } - if (gRender.flameHasBackup) { - jlSpriteRestoreUnder(stage, &gRender.flameBackup); - gRender.flameHasBackup = false; - } - // stRenderLevel only re-paints the tilemap when lastLevel changed - // (e.g., transitioned from gameplay back to title) -- otherwise - // it's a fast no-op. - stRenderLevel(stage, &game->level); - // Paint the "SPACE TAXI" logo (char $84 cells) at its current - // flip frame + color over the freshly-painted tilemap, BEFORE - // the sprites save-under and draw so they overlay it. The flip - // ADVANCE happens on the game-tick clock (titleAdvanceLogo); - // this only re-pastes when it changed or the tilemap repainted. - titlePaintLogo(stage, &game->level); - // No "fares per game" overlay on the title: the C64 has a - // separate options/menu scene driven by $5295 (header text) - // and $52E1 / $533C (the "1 2 3 4" digit row at row 2 col 28 - // with the selected digit highlighted in color RAM at $D86C). - // The main title's screen RAM at $0400 contains no game-option - // text -- only "BY JOHN F. BUTCHER" credits and the - // UP=hiscore / DOWN=instructions / FIRE=begin joystick line. - // Options-menu scene is a separate state to be built later. - // The intro state machine (sparkle -> passenger walk -> taxi - // takeoff) is advanced on the fixed game-tick clock in the main - // loop (stRenderTitleAdvance), not here -- this pass only DRAWS - // whichever sprites the current intro state has visible. - // sprite-X -> visible col = sprite-X - 24 (C64 border offset). - // sprite-Y -> visible row = sprite-Y - 50 (top border). - // Draw lowest priority first: flame (sprite 2), then passenger - // (sprite 1), then cab (sprite 0) on top. - if (gTitle.flameVisible && flameStrobeOn() && - gTitle.flameDirMask < 16u) { - int8_t cel = kFlameCelByDirMask[gTitle.flameDirMask]; - if (cel >= 0 && gRender.flameCels[cel] != NULL) { - // $6D74-$6D82: flame sprite-X = cab sprite-X - 2. - // $6D85-$6D88: flame sprite-Y = cab sprite-Y. - int16_t fx = (int16_t)(gTitle.cabSx - 2 - ST_SPRITE_X_OFFSET); - int16_t fy = (int16_t)(gTitle.cabSy - ST_SPRITE_Y_OFFSET); - jlSpriteSaveAndDraw(stage, gRender.flameCels[cel], - fx, fy, &gRender.flameBackup); - gRender.flameHasBackup = true; - } - } - if (gTitle.passengerVisible) { - int16_t cel = passCelForPtr(gTitle.passengerPtr); - if (cel < 0) { - cel = 0; - } - if (cel >= ST_PASSENGER_CEL_COUNT) { - cel = ST_PASSENGER_CEL_COUNT - 1; - } - if (gRender.passengerCels[cel] != NULL) { - jlSpriteSaveAndDraw(stage, gRender.passengerCels[cel], - (int16_t)(gTitle.passengerSx - ST_SPRITE_X_OFFSET), - (int16_t)(gTitle.passengerSy - ST_SPRITE_Y_OFFSET), - &gRender.passengerBackup[0]); - gRender.passengerHasBackup[0] = true; - } - } - if (gTitle.cabVisible) { - // $4A54 sets gSpr0PtrShadow = $C0 (cab with landing gear - // RETRACTED) during takeoff. Stages 2-4 use the init ptr - // $C1 (gear extended). We track the live ptr in - // gTitle.cabPtr and map: $C1 -> cel 0, $C0 -> cel 1. - int16_t cel = (gTitle.cabPtr == 0xC0) ? 1 : 0; - if (gRender.taxiCels[cel] == NULL) { - cel = 0; - } - if (gRender.taxiCels[cel] != NULL) { - jlSpriteSaveAndDraw(stage, gRender.taxiCels[cel], - (int16_t)(gTitle.cabSx - ST_SPRITE_X_OFFSET), - (int16_t)(gTitle.cabSy - ST_SPRITE_Y_OFFSET), - &gRender.taxiBackup); - gRender.taxiHasBackup = true; - } - } - jlWaitVBL(); - jlStagePresent(); - return; - } - if (game->state == ST_STATE_HIGHSCORES) { - // "THE IMMORTAL CABBIES" high-score screen ($4C1F). Header + - // the game's default 8-entry table, transcribed from $4A89 - // (score string + 15-char name). Persistent scoring isn't wired - // up, so we show the original default table. - static const char *kHighScores[8] = { - "3877.56 MICHAEL PLATE", - "1809.51 MICHAEL PLATE", - "1179.87 ANDREAS PLATE", - " 892.65 MICHAEL PLATE", - " 685.37 ANDREAS PLATE", - " 629.45 MICHAEL PLATE", - " 569.50 MICHAEL PLATE", - " 504.97 ANDREAS PLATE", - }; - uint8_t i; - jlSurfaceClear(stage, 0u); - stRenderDrawText(stage, 10u, 1u, "THE IMMORTAL CABBIES"); - for (i = 0u; i < 8u; i++) { - stRenderDrawText(stage, 9u, (uint8_t)(4u + i * 2u), kHighScores[i]); - } - stRenderDrawText(stage, 10u, 22u, "PRESS FIRE TO RETURN"); - jlWaitVBL(); - jlStagePresent(); - return; - } - if (game->state == ST_STATE_INSTRUCTIONS) { - // Author/about screen ($556A, reached via DOWN). Text transcribed - // verbatim from $56D4-$57A6. - jlSurfaceClear(stage, 0u); - stRenderDrawText(stage, 1u, 3u, "THIS PROGRAM DEVELOPED AND WRITTEN BY"); - stRenderDrawText(stage, 12u, 5u, "JOHN F. KUTCHER"); - stRenderDrawText(stage, 15u, 7u, "11/21/65"); - stRenderDrawText(stage, 4u, 10u, "CURRENTLY, AS OF JANUARY 1984,"); - stRenderDrawText(stage, 5u, 12u, "HE IS ATTENDING JOHNS HOPKINS"); - stRenderDrawText(stage, 6u, 14u, "UNIVERSITY IN BALTIMORE, MD"); - stRenderDrawText(stage, 1u, 18u, "ALSO TRY RESCUE SQUAD BY JOHN KUTCHER"); - stRenderDrawText(stage, 10u, 22u, "PRESS FIRE TO RETURN"); - jlWaitVBL(); - jlStagePresent(); - return; - } - if (game->state == ST_STATE_OPTIONS) { - // "GAME VARIATIONS" menu ($5295). Text + positions transcribed - // from the C64: header (13,0), prompt (1,2), digits (28,2), - // instructions (5,4) and (19,5). The C64 recolors the selected - // digit via color RAM ($D86C + sel*2); we mark it with a - // highlight box behind the digit cell (row 2, col 28 + sel*2). - uint8_t sel = (uint8_t)((game->fareTarget - 1u) & 3u); // 0..3 - int16_t hx = (int16_t)((28 + (int)sel * 2) * (int)ST_TILE_PIXELS); - jlSurfaceClear(stage, 0u); - jlFillRect(stage, hx, (int16_t)(2 * ST_TILE_PIXELS), - (int16_t)ST_TILE_PIXELS, (int16_t)ST_TILE_PIXELS, 6u); - stRenderDrawText(stage, 13u, 0u, "GAME VARIATIONS"); - stRenderDrawText(stage, 1u, 2u, "SELECT NUMBER OF CABBIES:"); - stRenderDrawText(stage, 28u, 2u, "1 2 3 4"); - stRenderDrawText(stage, 5u, 4u, "USE JOYSTICK: LEFT OR RIGHT,"); - stRenderDrawText(stage, 19u, 5u, "FIRE TO SELECT"); - jlWaitVBL(); - jlStagePresent(); - return; - } - if (game->state == ST_STATE_GAME_OVER) { - jlSurfaceClear(stage, 0u); - stRenderDrawText(stage, 15u, 8u, "GAME OVER"); - snprintf(buf, sizeof(buf), "FINAL SCORE %06lu", - (unsigned long)game->score); - stRenderDrawText(stage, 12u, 11u, buf); - stRenderDrawText(stage, 9u, 16u, "PRESS SPACE TO RESTART"); - jlWaitVBL(); - jlStagePresent(); - return; - } - - // Restore prev-frame sprite backups FIRST so the static tilemap - // shows through where the taxi/passenger/flame used to be. Then - // we'll save+draw at the new positions below. LIFO order: the cab - // was saved AFTER the flame (its backup holds flame pixels), so - // the cab restores first and the flame restore then cleans the - // re-deposited flame pixels. - if (gRender.taxiHasBackup) { - jlSpriteRestoreUnder(stage, &gRender.taxiBackup); - gRender.taxiHasBackup = false; - } - if (gRender.flameHasBackup) { - jlSpriteRestoreUnder(stage, &gRender.flameBackup); - gRender.flameHasBackup = false; - } - for (i = 0u; i < ST_MAX_PASSENGERS; i++) { - if (gRender.passengerHasBackup[i]) { - jlSpriteRestoreUnder(stage, &gRender.passengerBackup[i]); - gRender.passengerHasBackup[i] = false; - } - } - // Static tilemap commit (no-op after first frame in a scene). - stRenderLevel(stage, &game->level); - - // Re-draw HUD strip (cheap; just a few characters per cycle). - stHudDraw(stage, game); - - px = (int16_t)(game->taxi.x >> ST_SUBPIXEL_SHIFT); - py = (int16_t)(game->taxi.y >> ST_SUBPIXEL_SHIFT); - - // $619B cel select: facing picks the $C0/$C1 (right) or $DC/$DD - // (left) pair; bit 0 is the LANDING GEAR state. The body is a - // STATIC cel -- no flicker of any kind. Sheet order - // [$C1,$C0,$DC,$DD]: - // right: ptr = $C0|gear -> cel 1 - gear - // left: ptr = $DC|gear -> cel 2 + gear - // (The crash anim uses the dedicated death cels below.) - if (game->taxi.facing == ST_DIR_LEFT) { - taxiCel = (uint8_t)(game->taxi.gearDown ? 3u : 2u); - } else { - taxiCel = (uint8_t)(game->taxi.gearDown ? 0u : 1u); - } - if (gRender.taxiCels[taxiCel] == NULL) { - taxiCel = 0u; - } - // Engine flame: C64 sprite 2 ($6D6A) at (cab_x - 2, cab_y), - // cel indexed by the $716A direction mask through the $6DB0 - // table, drawn on alternate frames only (the parity flicker), - // hidden when no input is held. Drawn BEFORE the cab so the cab - // (sprite 0) keeps VIC priority on top. - if (game->taxi.warpFrame == 0u && game->taxi.thrusting && - !game->taxi.landed && flameStrobeOn() && - game->taxi.dirMask < 16u) { - int8_t fcel = kFlameCelByDirMask[game->taxi.dirMask]; - if (fcel >= 0 && gRender.flameCels[fcel] != NULL) { - jlSpriteSaveAndDraw(stage, gRender.flameCels[fcel], - (int16_t)(px - 2), py, - &gRender.flameBackup); - gRender.flameHasBackup = true; - } - } - if (game->taxi.crashTicks > 0u) { - // Crash death: phase 1 tumbles the $CC/$CD debris pair while - // falling; the floor hit ($6ACC row $DA) starts the $CE..$D1 - // impact walk at the slowing $6B2A cadence, then the wreck - // hides until respawn. - uint32_t nowMs = jlMillisElapsed(); - int16_t dcel = -1; - if (!gRender.deathActive) { - gRender.deathActive = true; - gRender.deathMs = nowMs; - gRender.deathStep = 0u; - } - if (!game->taxi.crashImpacted) { - dcel = (int16_t)((nowMs / ST_DEATH_TUMBLE_MS) & 1u); - gRender.deathMs = nowMs; // re-anchor for the walk - gRender.deathStep = 0u; - } else { - if (gRender.deathStep < 4u && - nowMs - gRender.deathMs >= kDeathStepMs[gRender.deathStep]) { - gRender.deathMs = nowMs; - gRender.deathStep++; - } - if (gRender.deathStep < 4u) { - dcel = (int16_t)(2u + gRender.deathStep); - } - } - if (dcel >= 0 && gRender.deathCels[dcel] != NULL) { - jlSpriteSaveAndDraw(stage, gRender.deathCels[dcel], px, py, - &gRender.taxiBackup); - gRender.taxiHasBackup = true; - } - } else if (game->taxi.warpFrame > 0u) { - // Transporter shrink-warp: identity map onto the SEVEN drawn - // chain cels $E2..$E8; step 7 (pointer reaching $E9) hides - // the sprite entirely, exactly like $5CDA/$718E. - uint8_t step = stEngineWarpStep(game); /* 0..7 */ - if (step < ST_WARP_CEL_COUNT && gRender.warpCels[step] != NULL) { - jlSpriteSaveAndDraw(stage, gRender.warpCels[step], px, py, - &gRender.taxiBackup); - gRender.taxiHasBackup = true; - } - } else if (gRender.taxiCels[taxiCel] != NULL) { - // Save-under + draw. Backup is replayed at the start of next - // frame above to undraw cleanly. - gRender.deathActive = false; - jlSpriteSaveAndDraw(stage, gRender.taxiCels[taxiCel], px, py, - &gRender.taxiBackup); - gRender.taxiHasBackup = true; - } else { - // No sprite asset: fall back to a placeholder rect AND mark - // the tilemap dirty so the level repaints over us next frame. - // Use the level's per-level sprite0Color ($D027) so the cab - // tint matches the level palette. - jlFillRect(stage, px, py, ST_TAXI_W_PX, ST_TAXI_H_PX, - game->level.sprite0Color); - gRender.tilemapDirty = true; - if (game->taxi.thrusting) { - jlFillRect(stage, - (int16_t)(px + ST_FLAME_OFFSET_X_PX), - (int16_t)(py + ST_FLAME_OFFSET_Y_PX), - ST_FLAME_W_PX, - ST_FLAME_H_PX, - game->level.sprite1Color); - } - } - - // Passengers (waiting or being carried) - for (i = 0u; i < ST_MAX_PASSENGERS; i++) { - const StPassengerT *p = &game->passengers[i]; - jlSpriteT *cel; - int16_t pcel; - if (!p->active || p->phase == ST_PASS_RIDING) { - continue; // riding passengers are invisible in the cab - } - // Cel by lifecycle phase (see StPassPhaseE): beams shrink or - // grow through $C7..$CB, the wait wave cycles $66D6, walks - // alternate the direction's stride pair. - switch (p->phase) { - case ST_PASS_BEAM_IN: - case ST_PASS_DROP_IN: - pcel = (int16_t)(7 - (p->waitPhase > 4u ? 4u : p->waitPhase)); - break; - case ST_PASS_BOARD_OUT: - case ST_PASS_DROP_OUT: - pcel = (int16_t)(3 + (p->waitPhase > 4u ? 4u : p->waitPhase)); - break; - case ST_PASS_WALK_TO_CAB: - case ST_PASS_WALK_TO_PAD: - if (p->targetX < p->x) { - pcel = (int16_t)((p->waitPhase & 1u) ? ST_PASSENGER_CEL_WALK1 - : ST_PASSENGER_CEL_WALK0); - } else { - pcel = (int16_t)((p->waitPhase & 1u) ? ST_PASSENGER_CEL_WALKR1 - : ST_PASSENGER_CEL_WALKR0); - } - break; - case ST_PASS_WAIT: - default: - pcel = passCelForPtr(kPassengerWaveCels[p->waitPhase & 3u]); - break; - } - cel = gRender.passengerCels[pcel]; - if (cel == NULL) { - jlFillRect(stage, p->x, p->y, - ST_PASSENGER_W_PX, ST_PASSENGER_H_PX, 8u); - gRender.tilemapDirty = true; - continue; - } - jlSpriteSaveAndDraw(stage, cel, p->x, p->y, - &gRender.passengerBackup[i]); - gRender.passengerHasBackup[i] = true; - } - - if (game->state == ST_STATE_DEMO) { - // Attract banners ($45A7-$4614 text): drawn every frame over - // the live demo, before the present. - stRenderDrawText(stage, 7u, 3u, "DEMO, USE JOYSTICK TO EXIT"); - stRenderDrawText(stage, 3u, 24u, "THIS IS 1 OF 25 DIFFERENT SCREENS!"); - } - - if (game->state == ST_STATE_LEVEL_DONE) { - int16_t midY = (int16_t)(10 * ST_TILE_PIXELS); - jlFillRect(stage, 0, midY, SURFACE_WIDTH, - (int16_t)(2 * ST_TILE_PIXELS), 0u); - stRenderDrawText(stage, 13u, 10u, "LEVEL COMPLETE"); - } - - jlWaitVBL(); - jlStagePresent(); -} - - -// ----- internal helpers ----- - -static bool loadTileBank(uint8_t bankId) { - uint16_t idx; - uint16_t loaded; - char path[64]; - - snprintf(path, sizeof(path), ST_TILE_BANK_PATH_FMT, (unsigned)bankId); - jlLogF("stRender: loadTileBank(%u) -> %s", (unsigned)bankId, path); - - // Reset the previous bank's validity bits so a smaller new bank - // doesn't inherit stale tiles past its end. - for (idx = 0u; idx < ST_TILE_BANK_MAX; idx++) { - gRender.tileValid[idx] = false; - } - gRender.bankLoaded = false; - gRender.currentBankId = bankId; - - // Native bake: jlTileBankLoad fread's per-target planar tile - // bytes straight into gRender.tiles[].pixels with no chunky <-> - // planar conversion. Replaces the JAS-load + jlSurfaceCreate + - // jlSurfaceBlit + per-tile jlTileSnap path, which was the - // dominant startup cost. - loaded = jlTileBankLoad(path, gRender.tiles, ST_TILE_BANK_MAX, - gRender.tileValid, NULL); - if (loaded == 0u) { - jlLogF("stRender: ! tile bank load failed (%s)", path); - return false; - } - jlLogF("stRender: tile bank loaded (%u tiles)", (unsigned)loaded); - gRender.bankLoaded = true; - return true; -} - - -static bool loadSpriteSheet(jlSurfaceT *stage) { - jlSpriteT *cels[ST_SPRITE_SHEET_CELS]; - uint16_t count; - uint16_t i; - uint8_t step; - bool precompiled; - - // .spr blob carries all 36 cels (9 cols x 4 rows of 3x3-tile - // chunky 4bpp blobs) in PNG reading order: row 0 taxi, row 1 - // passenger, row 2 flame, row 3 warp. The padding slots between - // named sprites are loaded but never referenced; they leak a few - // KB at startup which is well below the cost of writing a - // free-loop here (extra code in _ROOT eats the IIgs cluster - // budget more than the heap fragments hurt). - memset(cels, 0, sizeof(cels)); - // Prefer the pre-compiled bank: its cels arrive already compiled (native - // routines dropped straight into the codegen arena), so the jlSpriteCompile - // loop below is a no-op fast path. Fall back to the portable .spr + runtime - // JIT if no .spc is staged for this build. - count = jlSpriteBankLoadPrecompiled(ST_SPRITE_SHEET_SPC, cels, ST_SPRITE_SHEET_CELS, NULL); - precompiled = (count != 0u); - if (count == 0u) { - count = jlSpriteBankLoad(ST_SPRITE_SHEET_PATH, cels, ST_SPRITE_SHEET_CELS, NULL); - } - if (count == 0u) { - return false; - } - jlLogF("stRender: sprite sheet loaded (%u cels, %s)", (unsigned)count, - precompiled ? "PRECOMPILED" : "jit"); - loadProgress(stage, 2u); - - // Compile every drawn cel to its per-shift asm routine. Without - // this, jlSprite{Draw,SaveUnder,SaveAndDraw} fall to the per-pixel - // interpreter (sp->slot stays NULL) -- catastrophic on the IIgs - // 65816: the 3 title sprites alone cost ~200 ms/frame interpreted - // (~3.7 fps). Compiling routes them to the fast path (UBER's - // benchmarks prove codegen works on every port). jlSpriteCompile is - // a no-op fallback if the arena is exhausted (cel stays interpreted). - { - uint16_t compiled = 0u; - - // Assign all cels first, then compile title-critical ones - // (taxi, passenger, flame) before the gameplay-only warp cels so - // a tight arena still fast-paths the title. - for (i = 0u; i < ST_TAXI_CEL_COUNT; i++) { - gRender.taxiCels[i] = cels[ST_SPRITE_TAXI_FIRST + i]; - } - for (i = 0u; i < ST_WARP_CEL_COUNT; i++) { - gRender.warpCels[i] = cels[ST_SPRITE_WARP_FIRST + i]; - } - for (i = 0u; i < ST_DEATH_CEL_COUNT; i++) { - gRender.deathCels[i] = cels[ST_SPRITE_DEATH_FIRST + i]; - } - for (i = 0u; i < ST_PASSENGER_CEL_COUNT; i++) { - gRender.passengerCels[i] = cels[ST_SPRITE_PASS_FIRST + i]; - } - for (i = 0u; i < ST_FLAME_CEL_COUNT; i++) { - gRender.flameCels[i] = cels[ST_SPRITE_FLAME_FIRST + i]; - } - step = 3u; - for (i = 0u; i < ST_TAXI_CEL_COUNT; i++) { - compiled = (uint16_t)(compiled + (jlSpriteCompile(gRender.taxiCels[i]) ? 1u : 0u)); - loadProgress(stage, step++); - } - for (i = 0u; i < ST_PASSENGER_CEL_COUNT; i++) { - compiled = (uint16_t)(compiled + (jlSpriteCompile(gRender.passengerCels[i]) ? 1u : 0u)); - loadProgress(stage, step++); - } - for (i = 0u; i < ST_FLAME_CEL_COUNT; i++) { - compiled = (uint16_t)(compiled + (jlSpriteCompile(gRender.flameCels[i]) ? 1u : 0u)); - loadProgress(stage, step++); - } - for (i = 0u; i < ST_WARP_CEL_COUNT; i++) { - compiled = (uint16_t)(compiled + (jlSpriteCompile(gRender.warpCels[i]) ? 1u : 0u)); - loadProgress(stage, step++); - } - for (i = 0u; i < ST_DEATH_CEL_COUNT; i++) { - compiled = (uint16_t)(compiled + (jlSpriteCompile(gRender.deathCels[i]) ? 1u : 0u)); - loadProgress(stage, step++); - } - jlLogF("stRender: sprite cels compiled: %u/%u", (unsigned)compiled, - (unsigned)(ST_TAXI_CEL_COUNT + ST_PASSENGER_CEL_COUNT + ST_FLAME_CEL_COUNT + ST_WARP_CEL_COUNT + ST_DEATH_CEL_COUNT)); - } - - // First-run cache: having just JIT-compiled the bank, write it out as a - // .spc so the next launch loads the compiled routines and skips the JIT - // (ship the small .spr, cache the big per-platform compiled bank). Best- - // effort -- a read-only/write-protected disk just means we JIT again. - if (!precompiled) { - if (jlSpriteBankSavePrecompiled(ST_SPRITE_SHEET_SPC, cels, count, NULL)) { - jlLogF("stRender: cached compiled bank -> %s", ST_SPRITE_SHEET_SPC); - } - } - return true; -} - - -static void destroySprites(void) { - uint8_t i; - for (i = 0u; i < ST_TAXI_CEL_COUNT; i++) { - if (gRender.taxiCels[i] != NULL) { - jlSpriteDestroy(gRender.taxiCels[i]); - gRender.taxiCels[i] = NULL; - } - } - for (i = 0u; i < ST_DEATH_CEL_COUNT; i++) { - if (gRender.deathCels[i] != NULL) { - jlSpriteDestroy(gRender.deathCels[i]); - gRender.deathCels[i] = NULL; - } - } - for (i = 0u; i < ST_WARP_CEL_COUNT; i++) { - if (gRender.warpCels[i] != NULL) { - jlSpriteDestroy(gRender.warpCels[i]); - gRender.warpCels[i] = NULL; - } - } - for (i = 0u; i < ST_PASSENGER_CEL_COUNT; i++) { - if (gRender.passengerCels[i] != NULL) { - jlSpriteDestroy(gRender.passengerCels[i]); - gRender.passengerCels[i] = NULL; - } - } -} - - -// The 1000-glyph font streams straight onto the font surface via -// jlTileBankLoadToSurface -- no 32KB staging buffer. The old -// malloc(32000) was lethal on IIgs (the clang libc heap is ~665 -// bytes and malloc returns 1, not NULL, at exhaustion -- LLVM816-ASKS -// item 4 -- so jlTileBankLoad fread the payload through pointer 1 and -// shredded bank 0 before the first present), and a 32KB static blows -// the bank-0 BSS ceiling. The streaming loader exists for exactly -// this case; its block grid matches ST_FONT_COLS row-major layout. -static bool loadFontSheet(jlSurfaceT *stage) { - uint16_t count; - uint16_t palette[16]; - - (void)stage; - gRender.fontSurface = jlSurfaceCreate(); - if (gRender.fontSurface == NULL) { - return false; - } - count = jlTileBankLoadToSurface(ST_FONT_PATH, gRender.fontSurface, palette); - if (count == 0u) { - jlSurfaceDestroy(gRender.fontSurface); - gRender.fontSurface = NULL; - return false; - } - // Font palette is authoritative for the font surface so jlDrawText - // reads back the authored colors. Stage palette is untouched. - jlPaletteSet(gRender.fontSurface, 0u, palette); - jlScbSetRange(gRender.fontSurface, 0, SURFACE_HEIGHT - 1, 0); - gRender.fontReady = true; - return true; -} - - -// The C64 flame strobe ($716C: one game tick on, one off) mapped to -// wall-clock so every port shows the intended LOOK: at >=50fps this -// is the authentic 12.5Hz shimmer; when a rendered frame spans a -// whole strobe phase (low fps) the flame draws every frame -- the -// CRT-fusion appearance instead of an unreadable slow blink. -static bool flameStrobeOn(void) { - uint32_t nowMs = jlMillisElapsed(); - bool phase = ((nowMs / ST_FLAME_STROBE_MS) & 1u) != 0u; - bool spans = (nowMs - gRender.flamePrevMs) >= ST_FLAME_STROBE_MS; - - gRender.flamePrevMs = nowMs; - return phase || spans; -} - - -// Map a live C64 sprite-1 pointer value onto the sheet's passenger -// row (extractSprites.py pass_ptrs order): -// $C4..$CB -> cels 0..7 (walk cels, boarding shrink) -// $D9 -> cel 8 (the wave/sparkle cel) -// Single source of truth for the title path and the gameplay wave. -static int16_t passCelForPtr(uint8_t ptr) { - if (ptr == 0xD9u) { - return ST_PASSENGER_CEL_SPARKLE; - } - if (ptr == 0xC2u) { - return ST_PASSENGER_CEL_WALKR0; - } - if (ptr == 0xC3u) { - return ST_PASSENGER_CEL_WALKR1; - } - return (int16_t)ptr - 0xC4; -} - - -// Startup progress: outline drawn on step 0 so the screen shows life -// the moment stRenderInit runs, bar grows per completed step, LOADING -// caption appears once the font glyphs exist. Cheap enough to call -// per sprite compile even on the IIgs (present base ~3 ms). -static void loadProgress(jlSurfaceT *stage, uint8_t step) { - int16_t w; - - if (step > (uint8_t)ST_LOAD_STEPS_TOTAL) { - step = (uint8_t)ST_LOAD_STEPS_TOTAL; - } - if (step == 0u) { - jlFillRect(stage, ST_LOAD_BAR_X - 2, ST_LOAD_BAR_Y - 2, - (uint16_t)(SURFACE_WIDTH - 2 * ST_LOAD_BAR_X + 4), - ST_LOAD_BAR_H + 4, ST_LOAD_BAR_FRAME_COLOR); - jlFillRect(stage, ST_LOAD_BAR_X - 1, ST_LOAD_BAR_Y - 1, - (uint16_t)(SURFACE_WIDTH - 2 * ST_LOAD_BAR_X + 2), - ST_LOAD_BAR_H + 2, 0u); - } - w = (int16_t)(((int32_t)step * (SURFACE_WIDTH - 2 * ST_LOAD_BAR_X)) / ST_LOAD_STEPS_TOTAL); - if (w > 0) { - jlFillRect(stage, ST_LOAD_BAR_X, ST_LOAD_BAR_Y, (uint16_t)w, - ST_LOAD_BAR_H, ST_LOAD_BAR_COLOR); - } - if (gRender.fontReady) { - stRenderDrawText(stage, 16u, 11u, "LOADING"); - } - jlStagePresent(); -} - - -// Build the ASCII -> (blockX | blockY << 8) lookup table used by -// jlDrawText. The font sheet was authored with each ASCII glyph at -// cell (ascii % 40, ascii / 40) (see assets/genPlaceholderArt.py), -// so the map is a direct computation. Control characters and DEL -// (0..31, 127) are marked TILE_NO_GLYPH so jlDrawText skips them. -static void buildAsciiMap(void) { - uint16_t i; - for (i = 0u; i < 128u; i++) { - if (i < 32u || i == 127u) { - gRender.asciiMap[i] = TILE_NO_GLYPH; - } else { - uint16_t col = (uint16_t)(i % ST_FONT_COLS); - uint16_t row = (uint16_t)(i / ST_FONT_COLS); - gRender.asciiMap[i] = (uint16_t)(col | (row << 8)); - } + dropCache(); + if (gRender.scratch != 0) { + jlSurfaceDestroy(gRender.scratch); + gRender.scratch = 0; } } diff --git a/examples/spacetaxi/stSim.c b/examples/spacetaxi/stSim.c new file mode 100644 index 0000000..daf844d --- /dev/null +++ b/examples/spacetaxi/stSim.c @@ -0,0 +1,1291 @@ +// Space Taxi -- the game tick ($5F40 main loop), physics, landing, +// collision, crash sequence, fuel and HUD arithmetic. +// +// Every function names the original routine it mirrors. The order of +// calls inside stSimTick is the order of the JSRs in the C64 main +// loop; the two raster waits that split it are where the VIC latched +// the frame, so the sprite snapshot (marshal) and the collision test +// sit at the same places. + +#include + +#include "stSim.h" + +// Screen-code strings the engine writes (all ASCII-compatible glyphs). +static const uint8_t kTextBlank[] = { 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0 }; // $6C38 +static const uint8_t kHudTemplate[7] = { 0x66, 0x66, 0x66, 0x74, 0x77, 0x74, 0x74 }; // $43B1 +// Fuel-pump chirp frequencies while refuelling ($6DFA, indexed by the +// mod-8 pump tick 1..4). +static const uint8_t kPumpFreq[5] = { 0x04, 0x05, 0x07, 0x0A, 0x10 }; + + +static void applyVelocityX(StSimT *sim); +static void applyVelocityY(StSimT *sim); +static uint32_t backgroundRowMask(const StSimT *sim, int16_t px, int16_t py); +static void bitScrollHatch(StSimT *sim); +static void collisionDispatch(StSimT *sim); +static void collisionPhase1(StSimT *sim); +static void collisionPhase2(StSimT *sim); +static StTickResultE collisionPhase3(StSimT *sim); +static void computeCollisions(StSimT *sim); +static void crashStart(StSimT *sim); +static void drawCabIcons(StSimT *sim); +static void drawScreensCount(StSimT *sim); +static void edgeReflect(StSimT *sim); +static void fireButtonEdge(StSimT *sim); +static void fuelBarHud(StSimT *sim); +static void fuelTick(StSimT *sim); +static void hudDraw(StSimT *sim); +static void hudInit(StSimT *sim); +static void landedHandler(StSimT *sim); +static bool levelEndCheck(const StSimT *sim); +static void marshal(StSimT *sim); +static void padDetect(StSimT *sim); +static void padLandingBob(StSimT *sim); +static void passengerArrTick(StSimT *sim); +static void physicsTick(StSimT *sim); +static uint8_t readInput(StSimT *sim); +static void markCell(StSimT *sim, uint16_t cell); +static void setCell(StSimT *sim, uint16_t cell, uint8_t ch); +static void setColorCell(StSimT *sim, uint16_t cell, uint8_t color); +static void spriteMasks(const StSimT *sim, uint8_t idx, uint32_t *rows); +static void spritesOffReset(StSimT *sim); +static void taxiSpawnInit(StSimT *sim); +static void taxiSpriteCelSelect(StSimT *sim); +static uint8_t validateDigit(uint8_t ch); + + +// $6112 -- add the X velocity to the 17-bit X position (msb:col:frac) +// and refresh the sprite-0 column shadow. +static void applyVelocityX(StSimT *sim) { + uint16_t pos = (uint16_t)(((uint16_t)sim->posXcol << 8) | sim->posXlo); + uint32_t sum = (uint32_t)pos + (uint16_t)sim->velX; + uint8_t msb = (uint8_t)(sim->posXmsb + (uint8_t)(sum >> 16)); + + if (sim->velX < 0) { + msb++; + } + sim->posXmsb = (uint8_t)(msb & 1u); + sim->posXlo = (uint8_t)sum; + sim->posXcol = (uint8_t)(sum >> 8); + sim->spr[0].msb = sim->posXmsb; + sim->spr[0].col = sim->posXcol; +} + + +// $6145 -- Y: velocity += accel + gravity, position += velocity. +static void applyVelocityY(StSimT *sim) { + uint16_t pos; + + sim->velY = (int16_t)((uint16_t)sim->accelY + sim->gravTemplateY + (uint16_t)sim->velY); + pos = (uint16_t)(((uint16_t)sim->posYrow << 8) | sim->posYlo); + pos = (uint16_t)(pos + (uint16_t)sim->velY); + sim->posYlo = (uint8_t)pos; + sim->posYrow = (uint8_t)(pos >> 8); + sim->spr[0].row = sim->posYrow; +} + + +// The 24 background bits under a sprite row whose leftmost pixel sits +// at visible pixel (px, py); a set bit is character graphics data. +static uint32_t backgroundRowMask(const StSimT *sim, int16_t px, int16_t py) { + uint32_t window = 0u; + int16_t col; + int16_t firstCol; + uint8_t k; + uint8_t shift; + + if (py < 0 || py >= (int16_t)(ST_SCREEN_ROWS * 8u)) { + return 0u; + } + // Floor division so a negative px lands on the column to its left. + firstCol = (int16_t)((px - (px < 0 ? 7 : 0)) / 8); + shift = (uint8_t)(px - firstCol * 8); + for (k = 0u; k < 4u; k++) { + uint8_t byte = 0u; + col = (int16_t)(firstCol + (int16_t)k); + if (col >= 0 && col < (int16_t)ST_SCREEN_COLS) { + byte = sim->charset[sim->screen[ST_CELL((uint16_t)py >> 3, (uint16_t)col)]][py & 7]; + } + window = (window << 8) | byte; + } + return (window >> (8u - shift)) & 0xFFFFFFu; +} + + +// $63D0 -- rotate every row of the transporter hatch glyph one pixel +// right: the animated energy field in the top-wall opening. +static void bitScrollHatch(StSimT *sim) { + uint8_t row; + + for (row = 0u; row < 8u; row++) { + uint8_t b = sim->charset[ST_CHAR_TRANSPORTER][row]; + sim->charset[ST_CHAR_TRANSPORTER][row] = (uint8_t)((b >> 1) | (b << 7)); + } + sim->charDirty[ST_CHAR_TRANSPORTER] = 1u; +} + + +// $6966 -- latch the frame's collisions and run the crash phases. +static void collisionDispatch(StSimT *sim) { + computeCollisions(sim); + if (sim->collisionPhase != 0u) { + // Phases 1..3 dispatch elsewhere (stSimTick handles 3's exit). + return; + } + stFarePadLightingGate(sim); + if ((sim->spriteBgColl & 1u) == 0u) { + if ((sim->spriteSpriteColl & 1u) == 0u) { + return; + } + if (sim->deathInProgress != 0u) { + return; + } + if ((sim->spriteSpriteColl & 2u) != 0u) { + if ((sim->spriteSpriteColl & 0xF8u) != 0u) { + return; + } + stFareSquashed(sim); + } + if ((sim->spriteSpriteColl & 0xF8u) == 0u) { + return; + } + } + // $6A01: the level's verdict on sprite contact; background contact + // always kills. + sim->hitDispatchResult = stHookHitVerdict(sim); + if ((sim->spriteBgColl & 1u) == 0u) { + if (sim->hitDispatchResult == 0u) { + return; + } + } + crashStart(sim); +} + + +// $6A72 -- the wreck falls: every second tick sweep the scream, flip +// the debris cel, jitter, reflect off the side walls, drift with the +// old X velocity and accelerate down until row $DA. +static void collisionPhase1(StSimT *sim) { + uint8_t jitter; + uint16_t sum; + uint16_t colFrac; + + if ((sim->hitDispatchResult & 0x80u) == 0u) { + sim->phaseTimer--; + if (sim->phaseTimer != 0u) { + return; + } + sim->phaseTimer = sim->phaseReload; + sim->thrustSweep--; + stAudioThrustSweep((uint8_t)(sim->thrustSweep >> 1)); + sim->spr[0].ptr ^= 1u; + // $6A98: rng(5) - 3 added to the sprite column (msb follows). + jitter = (uint8_t)(stSimRng(sim, 5u) - 3u); + colFrac = (uint16_t)(((uint16_t)sim->spr[0].msb << 8) | sim->spr[0].col); + colFrac = (uint16_t)(colFrac + (uint16_t)(int16_t)(int8_t)jitter); + sim->spr[0].col = (uint8_t)colFrac; + sim->spr[0].msb = (uint8_t)(colFrac >> 8); + edgeReflect(sim); + applyVelocityX(sim); + // vy += 40 (low byte with carry into the high byte), row += vy hi. + sum = (uint16_t)((uint16_t)(sim->velY & 0xFFu) + 0x28u); + sim->velY = (int16_t)((uint16_t)(((uint16_t)sim->velY & 0xFF00u) + (sum & 0x100u)) | (sum & 0xFFu)); + sim->spr[0].row = (uint8_t)(sim->spr[0].row + (uint8_t)((uint16_t)sim->velY >> 8)); + if (sim->spr[0].row < 0xDAu) { + return; + } + sim->spr[0].row = 0xDAu; + } + // $6AD6: hit the floor -> phase 2 with a fresh 2-tick cadence. + sim->collisionPhase++; + sim->phaseReload = 2u; + sim->phaseTimer = 2u; + stAudioSfx(stC64SfxProgram(ST_SFX_IMPACT)); +} + + +// $6B24 -- walk the wreck cels $CC..$D1 at a slowing rate, then hide. +static void collisionPhase2(StSimT *sim) { + sim->phaseTimer--; + if (sim->phaseTimer != 0u) { + return; + } + sim->phaseReload++; + sim->phaseTimer = sim->phaseReload; + sim->spr[0].ptr++; + if (sim->spr[0].ptr != 0xD1u) { + return; + } + sim->spr[0].enable = 0u; + sim->collisionPhase++; + sim->phaseTimer = 0x46u; +} + + +// $6B4C -- 70-tick pause, then the life bookkeeping. +static StTickResultE collisionPhase3(StSimT *sim) { + uint8_t p; + + sim->phaseTimer--; + if (sim->phaseTimer != 0u) { + return ST_TICK_CONTINUE; + } + if (sim->demoMode != 0u) { + return ST_TICK_CRASH_DONE; + } + if (sim->stage == ST_STAGE_RIDING) { + hudInit(sim); + if (sim->fareSlotCount != 0u) { + sim->spriteSlots[sim->activeDyingSlot] = 0u; + sim->fareSlotCount--; + if (sim->activeSpriteIdx == 0x0Bu) { + uint8_t k; + for (k = 0u; k < 4u; k++) { + setCell(sim, (uint16_t)(ST_CELL_TRANSPORTER + k), ST_CHAR_TRANSPORTER); + } + } else { + sim->spriteSlots[sim->activeSpriteIdx] = 0u; + sim->fareSlotCount--; + } + } + sim->activeSpriteIdx = 0u; + sim->stage = ST_STAGE_IDLE; + stSimDrawText(sim, 14u, 24u, kTextBlank, 1u); + } + // $6BBA + p = sim->player; + sim->cabs[p]--; + if (sim->cabs[p] == 0u) { + return ST_TICK_PLAYER_OUT; + } + setCell(sim, (uint16_t)(ST_CELL_CAB_ICONS + sim->cabs[p]), ST_CHAR_BLANK); + return ST_TICK_LIFE_LOST; +} + + +// The VIC's $D01E / $D01F for the displayed frame: pixel overlap of +// enabled sprites with each other and of sprite 0 with the background. +static void computeCollisions(StSimT *sim) { + uint32_t mask[ST_HW_SPRITES][ST_SPRITE_H]; + uint8_t live = 0u; + uint8_t ss = 0u; + uint8_t bg = 0u; + uint8_t a; + uint8_t b; + uint8_t r; + + for (a = 0u; a < ST_HW_SPRITES; a++) { + if ((sim->frame.enableMask & (uint8_t)(1u << a)) != 0u) { + spriteMasks(sim, a, mask[a]); + live |= (uint8_t)(1u << a); + } + } + if ((live & 1u) != 0u) { + int16_t px = (int16_t)((int16_t)sim->frame.x[0] - ST_SPRITE_X_ORIGIN); + int16_t py = (int16_t)((int16_t)sim->frame.y[0] - ST_SPRITE_Y_ORIGIN); + for (r = 0u; r < ST_SPRITE_H; r++) { + if (mask[0][r] != 0u && (mask[0][r] & backgroundRowMask(sim, px, (int16_t)(py + r))) != 0u) { + bg |= 1u; + break; + } + } + } + for (a = 0u; a < ST_HW_SPRITES; a++) { + if ((live & (uint8_t)(1u << a)) == 0u) { + continue; + } + for (b = (uint8_t)(a + 1u); b < ST_HW_SPRITES; b++) { + int16_t dx; + int16_t dy; + bool hit = false; + if ((live & (uint8_t)(1u << b)) == 0u) { + continue; + } + dx = (int16_t)((int16_t)sim->frame.x[b] - (int16_t)sim->frame.x[a]); + dy = (int16_t)((int16_t)sim->frame.y[b] - (int16_t)sim->frame.y[a]); + if (dx <= -ST_SPRITE_W || dx >= ST_SPRITE_W || dy <= -ST_SPRITE_H || dy >= ST_SPRITE_H) { + continue; + } + for (r = 0u; r < ST_SPRITE_H && !hit; r++) { + int16_t rb = (int16_t)((int16_t)r - dy); + uint32_t mb; + if (rb < 0 || rb >= (int16_t)ST_SPRITE_H) { + continue; + } + mb = mask[b][rb]; + if (dx >= 0) { + mb >>= dx; + } else { + mb = (mb << (uint8_t)(-dx)) & 0xFFFFFFu; + } + if ((mask[a][r] & mb) != 0u) { + hit = true; + } + } + if (hit) { + ss |= (uint8_t)((1u << a) | (1u << b)); + } + } + } + sim->spriteSpriteColl = ss; + sim->spriteBgColl = bg; +} + + +// $6A2B -- start the wreck sequence. +static void crashStart(StSimT *sim) { + sim->spr[0].ptr = 0xCCu; + sim->collisionPhase++; + sim->phaseTimer = 2u; + sim->phaseReload = 2u; + stAudioNoise(false); + sim->eventDispatchType = 0u; + sim->activePad = 0u; + sim->spr[2].enable = 0u; + sim->spr[2].row = 0u; + sim->velY = 0x0303; + stAudioSfx(stC64SfxProgram(ST_SFX_CRASH)); + sim->thrustSweep = 0xA0u; + if (sim->stage == ST_STAGE_WALK_TO_CAB) { + sim->stage--; + } +} + + +// $6372 -- one cab icon per spare cab (cabs - 1 of them). +static void drawCabIcons(StSimT *sim) { + uint8_t k = sim->cabs[sim->player]; + + while (k > 1u) { + k--; + setCell(sim, (uint16_t)(ST_CELL_CAB_ICONS + k), ST_CHAR_CAB_ICON); + } +} + + +// $6384 -- screens completed, or the finished marker past 25. +static void drawScreensCount(StSimT *sim) { + uint8_t v = sim->levelState; + uint8_t tens = 0u; + + if (v >= 0x19u) { + setCell(sim, (uint16_t)(ST_CELL_SCREENS + 0u), 0xCBu); + setCell(sim, (uint16_t)(ST_CELL_SCREENS + 1u), 0xCCu); + setCell(sim, (uint16_t)(ST_CELL_SCREENS + 2u), 0xCDu); + setCell(sim, (uint16_t)(ST_CELL_SCREENS + 3u), 0xCEu); + setCell(sim, (uint16_t)(ST_CELL_SCREENS + 4u), 0xCFu); + return; + } + setCell(sim, (uint16_t)(ST_CELL_SCREENS + 2u), 0xCAu); + setCell(sim, (uint16_t)(ST_CELL_SCREENS + 3u), ST_CHAR_ZERO); + setCell(sim, (uint16_t)(ST_CELL_SCREENS + 4u), ST_CHAR_ZERO); + while (v >= 10u) { + v = (uint8_t)(v - 10u); + tens++; + } + setCell(sim, (uint16_t)(ST_CELL_SCREENS + 1u), (v == 0u) ? ST_CHAR_ZERO : (uint8_t)(ST_CHAR_DIGIT_BASE + v)); + setCell(sim, (uint16_t)(ST_CELL_SCREENS + 0u), (tens == 0u) ? ST_CHAR_BLANK : (uint8_t)(ST_CHAR_DIGIT_BASE + tens)); +} + + +// $6AED -- negate the X velocity when the wreck crosses column 23 +// leftward or column 65 (in the high half) rightward. +static void edgeReflect(StSimT *sim) { + bool reflect = false; + + if (sim->spr[0].msb == 0u) { + if (sim->velX < 0 && sim->spr[0].col < 0x17u) { + reflect = true; + } + } else { + if (sim->velX >= 0 && sim->spr[0].col >= 0x41u) { + reflect = true; + } + } + if (reflect) { + sim->velX = (int16_t)(-sim->velX); + } +} + + +// $63DD -- FIRE press edge while airborne toggles the landing gear. +static void fireButtonEdge(StSimT *sim) { + if (sim->activePad != 0u) { + return; + } + if (sim->fireWasHeld != 0u) { + sim->fireWasHeld = (uint8_t)(sim->inputMask & 0x10u); + return; + } + if ((sim->inputMask & 0x10u) == 0u) { + return; + } + sim->fireWasHeld = 0x10u; + sim->spr[0].ptr ^= 1u; + stAudioSfx(stC64SfxProgram(ST_SFX_GEAR)); +} + + +// $6D6A -- sprite 2 is the exhaust: two columns left of the cab, +// visible every other tick while a direction is held. +void stSimFlameUpdate(StSimT *sim) { + uint16_t sum; + + if (sim->dirMask == 0u) { + stAudioNoise(false); + sim->spr[2].enable = 0u; + return; + } + stAudioNoise(true); + sum = (uint16_t)((uint16_t)sim->spr[0].col + 0xFEu); + sim->spr[2].col = (uint8_t)sum; + sim->spr[2].msb = (uint8_t)(sim->spr[0].msb + 0xFFu + (uint8_t)(sum >> 8)); + sim->spr[2].row = sim->spr[0].row; + sim->flameParity ^= 1u; + if (sim->flameParity == 0u) { + sim->spr[2].enable = 0u; + return; + } + sim->spr[2].ptr = stC64FlameCel(sim->dirMask & 0x0Fu); + sim->spr[2].enable = 1u; +} + + +// $6419 -- the climb/descend indicator: colour RAM cells beside the +// fuel gauge flash red (rising), cyan (level) or yellow (falling). +static void fuelBarHud(StSimT *sim) { + uint16_t cell; + uint8_t color; + + setColorCell(sim, ST_CELL(23, 10), 0x0Bu); + setColorCell(sim, ST_CELL(23, 11), 0x0Bu); + setColorCell(sim, ST_CELL(24, 10), 0x0Bu); + setColorCell(sim, ST_CELL(24, 11), 0x0Bu); + sim->fuelBarTick = (uint8_t)((sim->fuelBarTick + 1u) & 7u); + if (sim->fuelBarTick >= 5u) { + return; + } + if (sim->velY < 0) { + color = 0x02u; + cell = ST_CELL(24, 10); + } else if (sim->velY == 0) { + color = 0x03u; + cell = ST_CELL(23, 10); + } else { + color = 0x07u; + cell = ST_CELL(23, 10); + } + setColorCell(sim, cell, color); + setColorCell(sim, (uint16_t)(cell + 1u), color); +} + + +// $6E23 -- the fuel gauge: half a cell burns every fuelRate ticks in +// the air; on the fuel pad (the extra last pad) it refills at ten +// cents per half cell. +static void fuelTick(StSimT *sim) { + const StLevelT *L = sim->level; + uint8_t x; + + if (sim->activePad == L->padCount && sim->activePad != L->specialPad) { + if (sim->screen[ST_CELL_FUEL_LAST] == ST_CHAR_BLANK) { + return; + } + if (!stSimDecrementNumber(sim, ST_CELL_SCORE, 5u)) { + return; + } + sim->padAnimTick = (uint8_t)((sim->padAnimTick + 1u) & 7u); + if (sim->padAnimTick == 0u) { + stAudioSfx(stC64SfxProgram(ST_SFX_CASH)); + } else if (sim->padAnimTick < 5u) { + stAudioVoice1Freq(kPumpFreq[sim->padAnimTick]); + } + if (sim->padAnimTick != 0u) { + return; + } + x = sim->fuelCells; + if (sim->screen[ST_CELL_FUEL + x] == ST_CHAR_BLANK) { + x++; + sim->fuelCells = x; + setCell(sim, (uint16_t)(ST_CELL_FUEL + x), ST_CHAR_FUEL_HALF); + return; + } + setCell(sim, (uint16_t)(ST_CELL_FUEL + x), ST_CHAR_BLANK); + if (sim->screen[ST_CELL_FUEL_LAST] == ST_CHAR_BLANK) { + stAudioSfx(stC64SfxProgram(ST_SFX_FUEL_FULL)); + } + return; + } + // $6EAA + if (sim->activePad != 0u) { + return; + } + sim->fuelCountdown--; + if (sim->fuelCountdown != 0u) { + return; + } + if (sim->fuelCells < 3u) { + stAudioSfx(stC64SfxProgram(ST_SFX_FUEL_LOW)); + } + sim->fuelCountdown = L->fuelRate; + x = sim->fuelCells; + if (sim->screen[ST_CELL_FUEL] == ST_CHAR_FUEL_HALF) { + return; + } + if (sim->screen[ST_CELL_FUEL + x] == ST_CHAR_FUEL_HALF) { + setCell(sim, (uint16_t)(ST_CELL_FUEL + x), ST_CHAR_FUEL_EMPTY); + x--; + sim->fuelCells = x; + return; + } + setCell(sim, (uint16_t)(ST_CELL_FUEL + x), ST_CHAR_FUEL_HALF); +} + + +// $43E5 -- the fare meter loses a penny every tick. +static void hudDraw(StSimT *sim) { + (void)stSimDecrementNumber(sim, ST_CELL_FARE, 6u); +} + + +// $43A5 -- blank the fare meter. +static void hudInit(StSimT *sim) { + uint8_t k; + + for (k = 0u; k < ST_NUMBER_CHARS; k++) { + setCell(sim, (uint16_t)(ST_CELL_FARE + k), kHudTemplate[k]); + } +} + + +// $657B -- parked on a pad: wait for UP to be released, then UP takes +// off (velocities cleared, gear retracted). +static void landedHandler(StSimT *sim) { + if (sim->activePadMirror != 0u) { + if ((sim->inputMask & 1u) == 0u) { + sim->activePadMirror = 0u; + } + return; + } + if (sim->deathInProgress != 0u) { + return; + } + if ((sim->inputMask & 1u) == 0u) { + return; + } + sim->velY = 0; + sim->velX = 0; + sim->activePad = 0u; + sim->spr[0].ptr &= 0xFEu; + if (sim->stage == ST_STAGE_WALK_TO_CAB) { + sim->stage--; + } +} + + +// $6BE7 -- the cab has flown out of the top of the screen. +static bool levelEndCheck(const StSimT *sim) { + return sim->posYrow < 0x1Bu; +} + + +// $4253 + $4293 -- snapshot the shadow tables into the frame the VIC +// shows next. +static void marshal(StSimT *sim) { + uint8_t i; + uint8_t enable = 0u; + + for (i = 0u; i < ST_HW_SPRITES; i++) { + sim->frame.x[i] = (uint16_t)(((uint16_t)(sim->spr[i].msb != 0u ? 1u : 0u) << 8) | sim->spr[i].col); + sim->frame.y[i] = sim->spr[i].row; + sim->frame.ptr[i] = sim->spr[i].ptr; + sim->frame.color[i] = sim->spr[i].color; + if (sim->spr[i].enable != 0u) { + enable |= (uint8_t)(1u << i); + } + } + sim->frame.enableMask = enable; + sim->frame.multiMask = sim->multiColorMask; +} + + +// $645C -- with the gear down and a slow descent, an exact row match +// inside a pad's X bounds is a landing. +static void padDetect(StSimT *sim) { + int8_t i; + + if (sim->activePad != 0u) { + return; + } + if ((sim->spr[0].ptr & 1u) == 0u) { + return; + } + if (((uint16_t)sim->velY >> 8) != 0u) { + return; + } + for (i = (int8_t)(sim->level->padCount - 1u); i >= 0; i--) { + const StPadT *p = &sim->pads[i]; + int16_t taxiX = (int16_t)(((int16_t)sim->posXmsb << 8) | sim->posXcol); + int16_t x1 = (int16_t)(((int16_t)p->x1Hi << 8) | p->x1Lo); + int16_t x2 = (int16_t)(((int16_t)p->x2Hi << 8) | p->x2Lo); + if (sim->spr[0].row != p->row) { + continue; + } + if ((int16_t)(taxiX - x1) < 0) { + continue; + } + if ((int16_t)(x2 - taxiX) < 0) { + continue; + } + sim->activePad = (uint8_t)(i + 1); + sim->activePadMirror = sim->activePad; + sim->eventDispatchType = 0u; + if (sim->activePad != sim->activeSpriteIdx) { + if (sim->stage == ST_STAGE_RIDING) { + sim->eventDispatchType = (sim->activeSpriteIdx == 0x0Bu) ? 3u : 2u; + } else if (sim->stage == ST_STAGE_WAIT) { + sim->eventDispatchType = 1u; + } + } + sim->dirMask = 0u; + sim->deathInProgress = 1u; + stSimPadLight(sim, true); + sim->bobTimer = 2u; + if (((uint8_t)sim->velY & 0x80u) == 0u) { + stAudioSfx(stC64SfxProgram(ST_SFX_LAND_SOFT)); + return; + } + sim->bobTimer = 8u; + stAudioSfx(stC64SfxProgram(ST_SFX_LAND_HARD)); + if (sim->stage == ST_STAGE_RIDING) { + hudInit(sim); + } + return; + } +} + + +// $6DFF -- the touchdown bob: the sprite dips and rises while the +// timer runs down. +static void padLandingBob(StSimT *sim) { + if (sim->bobTimer == 0u) { + return; + } + sim->bobTimer--; + if ((sim->bobTimer & 1u) != 0u) { + sim->spr[0].row--; + return; + } + sim->spr[0].row++; + if (sim->bobTimer == 0u) { + sim->deathInProgress = 0u; + stSimPadLight(sim, false); + } +} + + +// $70B1 -- one bonus cab when the score's hundreds digit reaches 3. +static void passengerArrTick(StSimT *sim) { + uint8_t p = sim->player; + + if (sim->bonusLatch[p] != 0u) { + return; + } + if (sim->screen[ST_CELL_SCORE + 1u] < 0x6Du) { + return; + } + sim->cabs[p]++; + sim->bonusLatch[p] = sim->cabs[p]; + setCell(sim, (uint16_t)(ST_CELL_CAB_ICONS - 1u + sim->cabs[p]), ST_CHAR_CAB_ICON); +} + + +// $6032 -- input, thrust, gravity, integration. +static void physicsTick(StSimT *sim) { + uint8_t in; + + sim->accelX = 0; + sim->accelY = 0; + in = readInput(sim); + in = stHookInput(sim, in); + sim->inputMask = in; + if (sim->activePad != 0u) { + landedHandler(sim); + return; + } + if (sim->screen[ST_CELL_FUEL] == ST_CHAR_MENU_SENTINEL) { + // Out of fuel: only FIRE survives, no thrust at all. + sim->inputMask &= 0x10u; + } else { + if ((sim->inputMask & 0x0Cu) != 0u) { + if ((sim->spr[0].ptr & 1u) != 0u) { + sim->inputMask &= 0x13u; + } else { + sim->accelX = (int16_t)sim->accelTemplateX; + if ((sim->inputMask & 0x08u) == 0u) { + sim->accelX = (int16_t)(-sim->accelX); + } + } + } + if ((sim->inputMask & 0x03u) != 0u) { + sim->accelY = (int16_t)sim->accelTemplateY; + if ((sim->inputMask & 0x02u) == 0u) { + sim->accelY = (int16_t)(-sim->accelY); + } + } + } + sim->dirMask = (uint8_t)(sim->inputMask & 0x0Fu); + sim->velX = (int16_t)((uint16_t)sim->accelX + sim->gravTemplateX + (uint16_t)sim->velX); + applyVelocityX(sim); + applyVelocityY(sim); +} + + +// $6040 + $48F2 -- the joystick byte, or the recorded demo mask. +static uint8_t readInput(StSimT *sim) { + uint8_t mask; + uint16_t next; + + if (sim->demoMode == 0u || sim->postMortem != 0u) { + return sim->rawInput; + } + mask = sim->demoBuf[sim->demoOff]; + sim->demoTimer--; + if (sim->demoTimer == 0u) { + next = (uint16_t)(sim->demoOff + 3u); + if (next < sizeof(sim->demoBuf)) { + sim->demoTimer = sim->demoBuf[next]; + } + next = (uint16_t)(sim->demoOff + 2u); + if (next < sizeof(sim->demoBuf)) { + sim->demoOff = next; + } + } + return mask; +} + + +static void markCell(StSimT *sim, uint16_t cell) { + if (sim->cellDirty[cell] != 0u) { + return; + } + sim->cellDirty[cell] = 1u; + if (sim->dirtyCount < (uint8_t)(sizeof(sim->dirtyList) / sizeof(sim->dirtyList[0]))) { + sim->dirtyList[sim->dirtyCount++] = cell; + } else { + sim->dirtyAll = true; + } +} + + +static void setCell(StSimT *sim, uint16_t cell, uint8_t ch) { + if (sim->screen[cell] != ch) { + sim->screen[cell] = ch; + markCell(sim, cell); + } +} + + +static void setColorCell(StSimT *sim, uint16_t cell, uint8_t color) { + color = (uint8_t)(color & 0x0Fu); + if (sim->color[cell] != color) { + sim->color[cell] = color; + markCell(sim, cell); + } +} + + +// 21 rows of 24-bit foreground masks for a displayed sprite. A +// multicolour sprite's 2-bit pairs cover two pixels each. +static void spriteMasks(const StSimT *sim, uint8_t idx, uint32_t *rows) { + const uint8_t *bm = stSimSpriteBitmap(sim, sim->frame.ptr[idx]); + bool multi = (sim->frame.multiMask & (uint8_t)(1u << idx)) != 0u; + uint8_t r; + + for (r = 0u; r < ST_SPRITE_H; r++) { + uint32_t bits = 0u; + if (bm != 0) { + bits = ((uint32_t)bm[r * 3u] << 16) | ((uint32_t)bm[r * 3u + 1u] << 8) | bm[r * 3u + 2u]; + } + if (multi) { + uint32_t m = 0u; + uint8_t i; + for (i = 0u; i < 12u; i++) { + if ((bits & (3u << (i * 2u))) != 0u) { + m |= (3u << (i * 2u)); + } + } + bits = m; + } + rows[r] = bits; + } +} + + +// $6946 -- all sprites off, latches cleared, nobody on a pad. +static void spritesOffReset(StSimT *sim) { + uint8_t i; + + for (i = 0u; i < ST_HW_SPRITES; i++) { + sim->spr[i].enable = 0u; + } + sim->spriteSpriteColl = 0u; + sim->spriteBgColl = 0u; + sim->activePad = 0u; + sim->activeSpriteIdx = 0u; +} + + +// $6888 -- place the cab at the level's spawn point with a full tank. +static void taxiSpawnInit(StSimT *sim) { + const StLevelT *L = sim->level; + uint8_t k; + + sim->spr[0].enable = 1u; + sim->multiColorMask = 0x07u; + sim->spr[0].ptr = 0xC0u; + sim->posYlo = L->spawn[3]; + sim->posYrow = L->spawn[4]; + sim->posXlo = L->spawn[0]; + sim->posXcol = L->spawn[1]; + sim->posXmsb = L->spawn[2]; + sim->spr[0].msb = L->spawn[2]; + sim->spr[0].col = L->spawn[1]; + sim->spr[0].row = L->spawn[4]; + sim->velX = 0; + sim->velY = 0; + sim->deathInProgress = 0u; + sim->dirMask = 0u; + sim->inputMask = 0u; + sim->activePad = 0u; + sim->collisionPhase = 0u; + sim->padAnimTick = 0u; + stSimPadLight(sim, false); + fuelBarHud(sim); + sim->fuelCells = 11u; + for (k = 0u; k < ST_FUEL_CELLS; k++) { + setCell(sim, (uint16_t)(ST_CELL_FUEL + k), ST_CHAR_BLANK); + } + sim->fuelCountdown = L->fuelRate; +} + + +// $619B -- LEFT/RIGHT pick the facing pair; bit 0 (the gear) survives. +static void taxiSpriteCelSelect(StSimT *sim) { + uint8_t gear = (uint8_t)(sim->spr[0].ptr & 1u); + uint8_t base; + + if ((sim->dirMask & 0x04u) != 0u) { + base = 0xDCu; + } else if ((sim->dirMask & 0x08u) != 0u) { + base = 0xC0u; + } else { + return; + } + sim->spr[0].ptr = (uint8_t)(base | gear); +} + + +// $4345 -- a HUD glyph's numeric value: blanks and the zero glyph are 0. +static uint8_t validateDigit(uint8_t ch) { + uint8_t v; + + if (ch == ST_CHAR_BLANK) { + return 0u; + } + v = (uint8_t)(ch - ST_CHAR_DIGIT_BASE); + if (v == 10u) { + return 0u; + } + return v; +} + + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +// $61FB -- next player's turn; wraps to player 0 when everybody has had +// this screen (the caller then loads the next one). +bool stSimAdvancePlayer(StSimT *sim) { + sim->player++; + if (sim->player == sim->playerCount) { + sim->player = 0u; + return true; + } + return false; +} + + +// $704E -- keep the current player's score and fare rows. +void stSimArchiveHud(StSimT *sim) { + memcpy(sim->scoreBackup[sim->player], &sim->screen[ST_CELL_SCORE], ST_NUMBER_CHARS); + memcpy(sim->fareBackup[sim->player], &sim->screen[ST_CELL_FARE], ST_NUMBER_CHARS); +} + + +// $4354 -- add a 7-glyph decimal blob onto the number at dstCell, +// right to left, skipping the decimal point, with leading blanks. +void stSimBcdAdd(StSimT *sim, uint16_t dstCell, const uint8_t *blob) { + int8_t y; + uint8_t carry = 0u; + + for (y = 6; y >= 0; y--) { + uint8_t sum; + uint8_t ch; + if (y == 4) { + continue; + } + sum = (uint8_t)(validateDigit(blob[y]) + validateDigit(sim->screen[dstCell + (uint16_t)y]) + carry); + carry = 0u; + if (sum >= 10u) { + carry = 1u; + sum = (uint8_t)(sum - 10u); + } + ch = (uint8_t)(ST_CHAR_DIGIT_BASE + sum); + if (ch == ST_CHAR_DIGIT_BASE) { + ch = ST_CHAR_ZERO; + } + setCell(sim, (uint16_t)(dstCell + (uint16_t)y), ch); + } + // $4393: leading-zero suppression across the integer part. + for (y = 0; y < 3; y++) { + if (sim->screen[dstCell + (uint16_t)y] != ST_CHAR_ZERO) { + break; + } + setCell(sim, (uint16_t)(dstCell + (uint16_t)y), ST_CHAR_BLANK); + } +} + + +// $440B -- subtract one from the digit at `position` of the number at +// `cell`, borrowing leftward. False when there is nothing to take. +bool stSimDecrementNumber(StSimT *sim, uint16_t cell, uint8_t position) { + uint8_t y = position; + uint8_t ch; + + for (;;) { + ch = sim->screen[cell + y]; + if (ch == ST_CHAR_BLANK) { + return false; + } + if (ch != ST_CHAR_ZERO) { + break; + } + do { + y--; + } while (y == 4u); + } + y = position; + for (;;) { + ch = sim->screen[cell + y]; + if (ch != ST_CHAR_ZERO) { + break; + } + setCell(sim, (uint16_t)(cell + y), (uint8_t)(ST_CHAR_DIGIT_BASE + 9u)); + do { + y--; + } while (y == 4u); + } + ch = (uint8_t)(ch - 1u); + setCell(sim, (uint16_t)(cell + y), ch); + if (ch != ST_CHAR_DIGIT_BASE) { + return true; + } + setCell(sim, (uint16_t)(cell + y), ST_CHAR_ZERO); + if (y >= 3u) { + return true; + } + if (y == 0u) { + setCell(sim, cell, ST_CHAR_BLANK); + return true; + } + if (sim->screen[cell + y - 1u] != ST_CHAR_BLANK) { + return true; + } + setCell(sim, (uint16_t)(cell + y), ST_CHAR_BLANK); + return true; +} + + +// $41C2 -- write a screen-code string (terminated by any byte < 6) +// with one colour. +void stSimDrawText(StSimT *sim, uint8_t col, uint8_t row, const uint8_t *text, uint8_t color) { + uint16_t cell = ST_CELL(row, col); + + while (*text >= 6u) { + setCell(sim, cell, *text); + setColorCell(sim, cell, color); + cell++; + text++; + } +} + + +// $62F0 + $5F18 -- load the screen image and run the level prelude. +void stSimEnterLevel(StSimT *sim, const StLevelT *level) { + uint8_t k; + + sim->level = level; + memcpy(sim->pads, level->pads, sizeof(sim->pads)); + memcpy(sim->screen, level->screen, ST_SCREEN_CELLS); + memcpy(sim->color, level->color, ST_SCREEN_CELLS); + stSimDirtyAll(sim); + memcpy(sim->charset, stC64Charset(), sizeof(sim->charset)); + memset(sim->charDirty, 1, ST_CHARSET_CHARS); + sim->borderColor = level->header[0]; + sim->bgColor = level->header[1]; + sim->spr[0].color = level->header[7]; + sim->spr[1].color = level->header[8]; + sim->spriteMc0 = level->header[5]; + sim->spriteMc1 = level->header[6]; + sim->accelTemplateY = level->accelY; + sim->accelTemplateX = level->accelX; + sim->gravTemplateY = level->gravY; + sim->gravTemplateX = level->gravX; + memset(sim->spriteSlots, 0, sizeof(sim->spriteSlots)); + sim->fareSlotCount = 0u; + // $634D: this player's HUD rows come back. + for (k = 0u; k < ST_NUMBER_CHARS; k++) { + setCell(sim, (uint16_t)(ST_CELL_SCORE + k), sim->scoreBackup[sim->player][k]); + setCell(sim, (uint16_t)(ST_CELL_FARE + k), sim->fareBackup[sim->player][k]); + } + for (k = 0u; k < 7u; k++) { + setCell(sim, (uint16_t)(ST_CELL_CAB_ICONS + k), ST_CHAR_BLANK); + } + drawCabIcons(sim); + drawScreensCount(sim); + // Prelude $5F18 (the player-turn bookkeeping ran before the load). + for (k = 0u; k < ST_HW_SPRITES; k++) { + sim->spr[k].enable = 0u; + } + sim->hookLevel = level->levelIndex; + memcpy(sim->hookRam, level->hookData, ST_HOOK_BYTES); + stSimRespawn(sim); +} + + +// $5F27 -- the respawn half of the prelude. +void stSimRespawn(StSimT *sim) { + spritesOffReset(sim); + stHookPrelude0(sim); + // $6F18 takeoffSetup: from the third screen on the cab starts with + // a passenger aboard who wants pad 1. + sim->eventDispatchType = 0u; + sim->stage = ST_STAGE_IDLE; + if (sim->levelState >= 2u) { + static const uint8_t kPadPlease[] = { 0x50, 0x41, 0x44, 0x20, 0x31, 0x20, 0x20, 0x50, 0x4C, 0x45, 0x41, 0x53, 0x45, 0 }; + sim->stage = ST_STAGE_RIDING; + sim->activeSpriteIdx = 1u; + sim->activeDyingSlot = 1u; + stSimDrawText(sim, 14u, 24u, kPadPlease, 1u); + sim->eventDispatchType = 1u; + } + taxiSpawnInit(sim); + stHookPrelude1(sim); + // $6906 framePresent: the start-of-screen jingle ends with the SID + // (and the demo RNG) reset. + marshal(sim); + stAudioSilence(); + sim->spriteSpriteColl = 0u; + sim->spriteBgColl = 0u; + sim->rngT1 = 0x06u; + sim->rngT2 = 0x17u; +} + + +// $5EC4 + $4207 + $48AD -- a fresh game (or the attract demo). +void stSimNewGame(StSimT *sim, uint8_t playerCount, bool demo) { + uint8_t p; + uint8_t keepBuf[sizeof(sim->demoBuf)]; + uint8_t keepParity = sim->flameParity; + uint8_t keepWave = sim->waveIdx; + uint8_t keepWalk = sim->walkParity; + + // $5EC4 clears the game state but the playback buffer, the flame + // parity, the wave index and the walk parity all survive. + memcpy(keepBuf, sim->demoBuf, sizeof(keepBuf)); + memset(sim, 0, sizeof(*sim)); + memcpy(sim->demoBuf, keepBuf, sizeof(keepBuf)); + sim->flameParity = keepParity; + sim->waveIdx = keepWave; + sim->walkParity = keepWalk; + sim->stage0Rng = 0x64u; + sim->decayReload = 3u; + sim->playerCount = playerCount; + sim->player = 0u; + sim->playersDone = 0u; + sim->levelState = 0u; + sim->demoMode = demo ? 1u : 0u; + sim->rngHost = 0x2545F491u; + for (p = 0u; p < ST_MAX_PLAYERS; p++) { + sim->cabs[p] = ST_CABS_PER_PLAYER; + memcpy(sim->scoreBackup[p], kHudTemplate, ST_NUMBER_CHARS); + memcpy(sim->fareBackup[p], kHudTemplate, ST_NUMBER_CHARS); + } + sim->rngT1 = 0x06u; + sim->rngT2 = 0x17u; +} + + +// Everything must be repainted. +void stSimDirtyAll(StSimT *sim) { + memset(sim->cellDirty, 1, ST_SCREEN_CELLS); + sim->dirtyCount = 0u; + sim->dirtyAll = true; +} + + +// $6866 / $6877 -- the pad indicator cells beside the HUD. +void stSimPadLight(StSimT *sim, bool on) { + if (on) { + setColorCell(sim, ST_CELL(23, 28), 0x0Bu); + setColorCell(sim, ST_CELL(23, 29), 0x0Bu); + setColorCell(sim, ST_CELL(24, 28), 0x02u); + setColorCell(sim, ST_CELL(24, 29), 0x02u); + } else { + setColorCell(sim, ST_CELL(24, 28), 0x0Bu); + setColorCell(sim, ST_CELL(24, 29), 0x0Bu); + setColorCell(sim, ST_CELL(23, 28), 0x07u); + setColorCell(sim, ST_CELL(23, 29), 0x07u); + } +} + + +// $401B -- one character into screen RAM. +void stSimPutChar(StSimT *sim, uint8_t col, uint8_t row, uint8_t ch) { + setCell(sim, ST_CELL(row, col), ch); +} + + +// $401E -- one colour into colour RAM. +void stSimPutColor(StSimT *sim, uint8_t col, uint8_t row, uint8_t color) { + setColorCell(sim, ST_CELL(row, col), color); +} + + +// $4080 -- 1..n. The demo walks a 64-byte table so its rides replay +// exactly; a real game reads the SID noise oscillator, which a host +// LCG stands in for. +uint8_t stSimRng(StSimT *sim, uint8_t n) { + uint8_t r; + uint16_t product; + + if (sim->demoMode != 0u) { + sim->rngT1 = (uint8_t)((sim->rngT1 + 1u) & 0x3Fu); + sim->rngT2++; + r = (uint8_t)(stC64RngByte(sim->rngT1) + sim->rngT2); + } else { + sim->rngHost = sim->rngHost * 1103515245u + 12345u; + r = (uint8_t)(sim->rngHost >> 16); + } + product = (uint16_t)((uint16_t)n * (uint16_t)r); + return (uint8_t)((product >> 8) + 1u); +} + + +// The score glyphs as pennies (for the high-score table). +uint32_t stSimScorePennies(const StSimT *sim) { + uint32_t v = 0u; + uint8_t k; + + for (k = 0u; k < ST_NUMBER_CHARS; k++) { + if (k == 4u) { + continue; + } + v = v * 10u + validateDigit(sim->screen[ST_CELL_SCORE + k]); + } + return v; +} + + +// The boot-time content of the playback buffer. +void stSimSeedDemoBuffer(StSimT *sim, const uint8_t *image, uint16_t len) { + if (len > sizeof(sim->demoBuf)) { + len = (uint16_t)sizeof(sim->demoBuf); + } + memcpy(sim->demoBuf, image, len); +} + + +// $0902 / $4740 -- load a recording over the buffer and rewind. +void stSimSetDemoStream(StSimT *sim, const uint8_t *stream, uint16_t len) { + if (len > sizeof(sim->demoBuf)) { + len = (uint16_t)sizeof(sim->demoBuf); + } + memcpy(sim->demoBuf, stream, len); + sim->demoOff = 0u; + sim->demoTimer = sim->demoBuf[1]; +} + + +// Bitmap for a block pointer: the standard set, or the level's own. +const uint8_t *stSimSpriteBitmap(const StSimT *sim, uint8_t ptr) { + uint8_t k; + + if (ptr >= ST_SPRITE_PTR_FIRST && ptr <= ST_SPRITE_PTR_LAST) { + return stC64SpriteBitmap((uint8_t)(ptr - ST_SPRITE_PTR_FIRST)); + } + if (sim->level != 0) { + for (k = 0u; k < sim->level->spriteCount; k++) { + if (sim->level->sprites[k].ptr == ptr) { + return sim->level->sprites[k].bitmap; + } + } + } + return 0; +} + + +// $5F40 -- one iteration of the main loop. +StTickResultE stSimTick(StSimT *sim) { + StTickResultE result = ST_TICK_CONTINUE; + + if (sim->collisionPhase == 0u) { + physicsTick(sim); + fireButtonEdge(sim); + stSimFlameUpdate(sim); + padDetect(sim); + taxiSpriteCelSelect(sim); + if (levelEndCheck(sim)) { + stAudioNoise(false); + stAudioSilence(); + stSimArchiveHud(sim); + return ST_TICK_LEVEL_EXIT; + } + padLandingBob(sim); + fuelTick(sim); + // $61BD passengerEventDraw only re-voices the pickup/drop-off + // messages already drawn by the fare machine. + } + stFarePostTickGate(sim); + stFareStageDispatch(sim); + stHookPerTick2(sim); + marshal(sim); + bitScrollHatch(sim); + stHookPerTick3(sim); + fuelBarHud(sim); + if (sim->collisionPhase == 0u) { + collisionDispatch(sim); + } else if (sim->collisionPhase == 1u) { + computeCollisions(sim); + collisionPhase1(sim); + } else if (sim->collisionPhase == 2u) { + computeCollisions(sim); + collisionPhase2(sim); + } else { + computeCollisions(sim); + result = collisionPhase3(sim); + if (result != ST_TICK_CONTINUE) { + return result; + } + } + hudDraw(sim); + // $4FCB runStopWatcher: any joystick input ends the demo. + if (sim->demoMode != 0u && (sim->rawInput & 0x1Fu) != 0u) { + return ST_TICK_DEMO_INPUT; + } + passengerArrTick(sim); + return ST_TICK_CONTINUE; +} diff --git a/examples/spacetaxi/stSim.h b/examples/spacetaxi/stSim.h new file mode 100644 index 0000000..759ea94 --- /dev/null +++ b/examples/spacetaxi/stSim.h @@ -0,0 +1,366 @@ +// Space Taxi -- the C64 game simulation, host-independent. +// +// This is a routine-for-routine re-expression of the original's game +// tick (the $5F40 main loop) in C, working in the C64's own units: +// VIC sprite coordinates (sprite-X 24 = visible column 0, sprite-Y 50 +// = visible row 0), 8-bit sub-pixel velocities, screen RAM character +// codes, the per-level pad table as shipped, and the same state +// machines with the same timers. Nothing in here touches JoeyLib, so +// the same code runs inside a plain host build (tools/stsim) where it +// is checked tick-for-tick against a VICE trace of the real game. +// +// The renderer reads this state (sprite snapshot, screen RAM, charset) +// and the audio layer receives events through the stAudio* hooks +// declared at the bottom, which each host implements. +// +// Address comments ($xxxx) name the original variable or routine a +// field or function mirrors; see stuff/spacetaxi/labels.txt. + +#ifndef ST_SIM_H +#define ST_SIM_H + +#include +#include +#include + +#include "stC64Data.h" + +// Screen geometry: screen RAM is 40x25 character cells, all of which +// belong to the level image (the HUD rows included). +#define ST_SCREEN_COLS 40u +#define ST_SCREEN_ROWS 25u +#define ST_SCREEN_CELLS (ST_SCREEN_COLS * ST_SCREEN_ROWS) +#define ST_CELL(row, col) ((uint16_t)((row) * ST_SCREEN_COLS + (col))) + +// VIC sprite coordinate origins: the visible picture starts at +// sprite-X 24 and sprite-Y 50. +#define ST_SPRITE_X_ORIGIN 24 +#define ST_SPRITE_Y_ORIGIN 50 +#define ST_SPRITE_W 24 +#define ST_SPRITE_H 21 +#define ST_HW_SPRITES 8u + +// Fixed screen cells the game writes directly (C64 screen addresses). +#define ST_CELL_TRANSPORTER ST_CELL(0, 18) // $0412..$0415, 4 cells +#define ST_CELL_CAB_ICONS ST_CELL(23, 1) // $0799..$079F +#define ST_CELL_FUEL ST_CELL(23, 14) // $07A6..$07B1, 12 cells +#define ST_CELL_FUEL_LAST ST_CELL(23, 25) // $07B1 +#define ST_CELL_SCREENS ST_CELL(23, 32) // $07B8..$07BC +#define ST_CELL_SCORE ST_CELL(24, 2) // $07C2..$07C8, 7 chars +#define ST_CELL_MESSAGE ST_CELL(24, 14) // $07CE, 11 chars +#define ST_CELL_PAD_DIGIT ST_CELL(24, 18) // $07D2 +#define ST_CELL_FARE ST_CELL(24, 32) // $07E0..$07E6, 7 chars +#define ST_NUMBER_CHARS 7u +#define ST_FUEL_CELLS 12u +#define ST_LEVEL_NAME_CHARS 22u + +// Screen-code glyphs the HUD arithmetic works on ($4345/$4354/$440B): +// inverse-video digits 1..9 are $6B..$73, zero is $74, a leading blank +// is $66 and the decimal point is $77. +#define ST_CHAR_BLANK 0x66u +#define ST_CHAR_DIGIT_BASE 0x6Au // digit d (1..9) = base + d +#define ST_CHAR_ZERO 0x74u +#define ST_CHAR_POINT 0x77u +#define ST_CHAR_SPACE 0x20u +#define ST_CHAR_FUEL_HALF 0x7Bu // half a cell of fuel left +#define ST_CHAR_FUEL_EMPTY 0x7Au +#define ST_CHAR_TRANSPORTER 0x67u // animated top-wall hatch +#define ST_CHAR_CAB_ICON 0xC8u +#define ST_CHAR_MENU_SENTINEL 0x7Bu // $07A6 == $7B: first fuel cell half = out of fuel + +// Level image as loaded from a .dat (STL4). Kept in the C64's units. +#define ST_MAX_PADS 10u +#define ST_MAX_LEVEL_SPRITES 16u +// Per-level hook code + tables live at $7D98..$7FFF in the level's data +// blob; the port keeps the bytes (for the tables) and re-expresses the +// code in stHooks.c. +#define ST_HOOK_BASE 0x7D98u +#define ST_HOOK_BYTES (0x8000u - ST_HOOK_BASE) + +typedef struct { + uint8_t x1Hi; // slot byte 0: landing X lower bound, high byte + uint8_t x1Lo; // slot byte 1 + uint8_t x2Hi; // slot byte 2: landing X upper bound + uint8_t x2Lo; // slot byte 3 + uint8_t row; // slot byte 4: sprite-Y of a landed cab + uint8_t passMsb; // slot byte 5: passenger stand X msb + uint8_t passCol; // slot byte 6: passenger stand X column + uint8_t unused; // slot byte 7 +} StPadT; + +typedef struct { + uint8_t ptr; + uint8_t bitmap[ST_SPRITE_BYTES]; +} StLevelSpriteT; + +typedef struct { + uint8_t name[ST_LEVEL_NAME_CHARS + 1u]; // $7D78, screen codes + uint8_t header[9]; // $7D00..$7D08 VIC colours + uint8_t padCount; // $7D5A + uint8_t specialPad; // $7D09 + uint8_t fuelRate; // $7D60 + uint8_t spawn[5]; // $7D5B..$7D5F + uint16_t accelY; // $7D8F/90 + uint16_t accelX; // $7D91/92 + uint16_t gravY; // $7D93/94 + uint16_t gravX; // $7D95/96 + StPadT pads[ST_MAX_PADS]; // $7D0A.. + uint8_t screen[ST_SCREEN_CELLS]; // decompressed screen RAM + uint8_t color[ST_SCREEN_CELLS]; // decompressed colour RAM + uint8_t levelIndex; // 0..23 = A..X, 24 = title + uint8_t spriteCount; + StLevelSpriteT sprites[ST_MAX_LEVEL_SPRITES]; + uint8_t hookData[ST_HOOK_BYTES]; // $7D98..$7FFF as shipped +} StLevelT; + +// One VIC sprite as the game sees it through its shadow tables. +typedef struct { + uint8_t col; // $7175,X X low byte + uint8_t msb; // $7185,X X high bit (any non-zero value = set) + uint8_t row; // $717D,X Y + uint8_t enable; // $718E,X + uint8_t ptr; // $7197,X block pointer + uint8_t color; // $719F,X +} StSpriteT; + +// The frame the VIC last displayed ($4253 marshal + $4293 flush): the +// renderer draws this and the collision test evaluates it. +typedef struct { + uint16_t x[ST_HW_SPRITES]; + uint8_t y[ST_HW_SPRITES]; + uint8_t ptr[ST_HW_SPRITES]; + uint8_t color[ST_HW_SPRITES]; + uint8_t enableMask; // $7196 -> $D015 + uint8_t multiMask; // $D01C +} StFrameT; + +// Four players ("cabbies") share one game; each keeps their own score +// row, fare row, cab count and bonus-cab latch. +#define ST_MAX_PLAYERS 4u +#define ST_CABS_PER_PLAYER 6u // $4210 + +// Passenger/fare state machine stages ($7163). +typedef enum { + ST_STAGE_IDLE = 0, // $660B RNG-timed next fare + ST_STAGE_BEAM_IN, // $665F passenger materialises + ST_STAGE_WAIT, // $66B7 waves on the pad + ST_STAGE_WALK_TO_CAB, // $6CE8 walks to the landed cab + ST_STAGE_BOARD, // $66DD shrinks into the cab + ST_STAGE_RIDING, // $6739 aboard + ST_STAGE_DROP_IN, // $6742 materialises beside the cab + ST_STAGE_WALK_TO_PAD, // $6CE8 walks to the stand + ST_STAGE_LEAVE, // $67A6 shrinks away + ST_STAGE_CLEANUP // $67C6 +} StStageE; + +// What a tick asked the host to do next (the original abandons the +// main loop with PLA/PLA on these). +typedef enum { + ST_TICK_CONTINUE = 0, + ST_TICK_LEVEL_EXIT, // $6BFB cab flew out of the top + ST_TICK_CRASH_DONE, // $6B52 wreck sequence finished (demo) + ST_TICK_LIFE_LOST, // $6BBA wreck sequence finished (game), cabs left + ST_TICK_PLAYER_OUT, // $6BD0 wreck sequence finished, no cabs left + ST_TICK_DEMO_INPUT // $5006 real input during the demo +} StTickResultE; + +typedef struct { + // ---- level image (mutable copies: hooks edit the pad table) ---- + const StLevelT *level; + StPadT pads[ST_MAX_PADS]; // $7D0A working copy + uint8_t screen[ST_SCREEN_CELLS]; // $0400 screen RAM + uint8_t color[ST_SCREEN_CELLS]; // $D800 colour RAM + uint8_t cellDirty[ST_SCREEN_CELLS]; // renderer clears + uint16_t dirtyList[128]; // cells marked since the last frame + uint8_t dirtyCount; + bool dirtyAll; // list overflowed / whole screen + uint8_t charset[ST_CHARSET_CHARS][8]; // $2800 working copy + uint8_t charDirty[ST_CHARSET_CHARS]; // renderer clears + uint8_t borderColor; // $D020 + uint8_t bgColor; // $D021 + + // ---- input ---- + uint8_t inputMask; // $7169 EOR $FF of $DC00: 1 = pressed + uint8_t dirMask; // $716A low 4 bits of the above + uint8_t fireWasHeld; // $71BD + uint8_t rawInput; // the host's joystick byte for this tick + + // ---- cab motion ---- + uint8_t posXlo; // $7D61 + uint8_t posXcol; // $7D62 + uint8_t posXmsb; // $7D63 + uint8_t posYlo; // $7D64 + uint8_t posYrow; // $7D65 + int16_t velX; // $714C/4D + int16_t velY; // $714E/4F + int16_t accelX; // $7148/49 + int16_t accelY; // $714A/4B + uint16_t accelTemplateX; // $7D91/92 working copy + uint16_t accelTemplateY; // $7D8F/90 + uint16_t gravTemplateX; // $7D95/96 + uint16_t gravTemplateY; // $7D93/94 + uint8_t activePad; // $7150 0 = airborne, else pad number + uint8_t activePadMirror; // $7151 UP-release latch after landing + uint8_t bobTimer; // $716F + uint8_t flameParity; // $716C + uint8_t thrustSweep; // $721B + + // ---- collision / crash ---- + uint8_t collisionPhase; // $7164 + uint8_t phaseTimer; // $7165 + uint8_t phaseReload; // $7166 + uint8_t hitDispatchResult; // $721D + uint8_t spriteSpriteColl; // $71CA latched $D01E + uint8_t spriteBgColl; // $71CB latched $D01F + + // ---- fuel ---- + uint8_t fuelCountdown; // $71C7 + uint8_t fuelCells; // $71C9 rightmost non-empty cell index + uint8_t fuelBarTick; // $71BE + uint8_t padAnimTick; // $71C8 + + // ---- passenger / fare machine ---- + uint8_t stage; // $7163 + uint8_t deathInProgress; // $7167 + uint8_t decayTimer; // $715D + uint8_t decayReload; // $715E + uint8_t hoverXCol; // $715F + uint8_t hoverXFrac; // $7160 (the X msb of the target) + uint8_t walkParity; // $7161 + uint8_t waveIdx; // $716E + uint8_t stage0Rng; // $71CC + uint8_t spriteSlots[11]; // $7152..$715C: [n] = pad n reserved + uint8_t fareSlotCount; // $715C + uint8_t activeSpriteIdx; // $7D8E fare's pad (spawn, then destination) + uint8_t activeDyingSlot; // $716D + uint8_t eventDispatchType; // $71CD + + // ---- sprites ---- + StSpriteT spr[ST_HW_SPRITES]; + uint8_t multiColorMask; // $D01C + StFrameT frame; + + // ---- players and progression ---- + uint8_t playerCount; // $7213 "number of cabbies" + uint8_t player; // $7214 whose turn + uint8_t playersDone; // $71CE + uint8_t cabs[ST_MAX_PLAYERS]; // $71CF + uint8_t bonusLatch[ST_MAX_PLAYERS]; // $7223 + uint8_t scoreBackup[ST_MAX_PLAYERS][ST_NUMBER_CHARS]; // $71D3, stride 8 + uint8_t fareBackup[ST_MAX_PLAYERS][ST_NUMBER_CHARS]; // $71F3, stride 8 + uint8_t levelState; // $7215 screens completed + uint8_t demoMode; // $721C + uint8_t postMortem; // $5E9D + + // ---- demo playback ($48F2): the $0902 buffer a recording is + // loaded into; a shorter recording leaves the previous tail behind + // and a long ride reads on into it, as on the C64 ---- + uint8_t demoBuf[640]; + uint16_t demoOff; // $3F/$40 - $0902 + uint8_t demoTimer; // $4740 + + // ---- RNG ($4080) ---- + uint8_t rngT1; // $7171 + uint8_t rngT2; // $7172 + uint32_t rngHost; // stands in for the SID noise read in a real game + + // ---- title intro / level intro ---- + uint8_t introFrameCount; // $473F + uint8_t logoColorIdx; // $48A4 + uint8_t logoCycleAux; // $48A5 + uint8_t logoDiv; // $48A6 + uint8_t starTimer[7]; // $44F8 + uint8_t starReload[7]; // $4500 + uint8_t introLoopCount; // $4508 + uint8_t introCabDx; // $450A + uint8_t introCabDxTimer; // $450B + uint8_t spriteMc0; // $D025 + uint8_t spriteMc1; // $D026 + + // ---- per-level hook scratch ($7D9C.. and friends) ---- + uint8_t hookRam[ST_HOOK_BYTES]; // $7D98..$7FFF working copy + uint8_t hookLevel; // level index the hook state belongs to + uint8_t warpCounter; // $5A66 + uint8_t warpMask; // $5CE8 + uint8_t animPhase; // $5CF1 +} StSimT; + +// ---- simulation API (stSim.c) ---- + +// Bind a level and run the scene-load + prelude ($62F0 + $5F18) for a +// fresh screen: copy the image, restore the player's HUD rows, draw +// the cab icons, spawn the cab, refill the fuel. +void stSimEnterLevel(StSimT *sim, const StLevelT *level); +// Re-entry after a crash with cabs left ($5F27): spawn + refill only. +void stSimRespawn(StSimT *sim); +// One game tick = one $5F40 iteration (two video frames on the C64). +StTickResultE stSimTick(StSimT *sim); +// Seed the whole struct for a new game ($5EC4 + $4207 + $48AD). +void stSimNewGame(StSimT *sim, uint8_t playerCount, bool demo); +// Seed the playback buffer with the boot-time RAM image, once. +void stSimSeedDemoBuffer(StSimT *sim, const uint8_t *image, uint16_t len); +// Load a recording into the buffer and rewind ($0902 + $4740 seeding). +void stSimSetDemoStream(StSimT *sim, const uint8_t *stream, uint16_t len); +// Per-player bookkeeping at a screen change ($61FB advanceFareSlot): +// returns true when the turn wrapped to player 0 (time for a new screen). +bool stSimAdvancePlayer(StSimT *sim); +// Archive the current player's HUD rows ($704E). +void stSimArchiveHud(StSimT *sim); +// Mark every cell dirty (scene change). +void stSimDirtyAll(StSimT *sim); +// Text and HUD primitives (screen RAM writes). +void stSimDrawText(StSimT *sim, uint8_t col, uint8_t row, const uint8_t *text, uint8_t color); +void stSimPutChar(StSimT *sim, uint8_t col, uint8_t row, uint8_t ch); +void stSimPutColor(StSimT *sim, uint8_t col, uint8_t row, uint8_t color); +void stSimBcdAdd(StSimT *sim, uint16_t dstCell, const uint8_t *blob); +bool stSimDecrementNumber(StSimT *sim, uint16_t cell, uint8_t position); +uint8_t stSimRng(StSimT *sim, uint8_t n); +// Sprite-mask lookup shared with the renderer: 21 rows of 24-bit masks. +const uint8_t *stSimSpriteBitmap(const StSimT *sim, uint8_t ptr); +// The score the current player has on screen, as pennies. +uint32_t stSimScorePennies(const StSimT *sim); +// $6866 / $6877 -- the pad indicator colour cells. +void stSimPadLight(StSimT *sim, bool on); +// $6D6A -- the exhaust sprite (shared with the title lift-off). +void stSimFlameUpdate(StSimT *sim); + +// ---- level file (stLevelFile.c) ---- +bool stLevelParse(StLevelT *out, FILE *fp); + +// ---- passenger machine (stFare.c) ---- +void stFareStageDispatch(StSimT *sim); // $65C1 +void stFarePostTickGate(StSimT *sim); // $67E3 +void stFarePadLightingGate(StSimT *sim); // $6EF0 +void stFareUpPlease(StSimT *sim); // $6CB8 +uint8_t stFareChooseDestination(StSimT *sim); // $6537: reserves a pad, returns its number +void stFareDrawMessage(StSimT *sim, const uint8_t *text); +void stFareSquashed(StSimT *sim); // $69B4..$69F8 +void stFareBoardingSound(StSimT *sim, const uint8_t *speech); // $6C7F +void stFareWaveStep(StSimT *sim); // $66B7 +void stFareWalkStep(StSimT *sim); // $6D0D +void stFareLeaveStep(StSimT *sim); // $67A6 + +// ---- title screen and level intro (stTitle.c) ---- +void stTitleEnter(StSimT *sim, const StLevelT *title); // $477A + $47B0..$47E1 +bool stTitleTick(StSimT *sim); // $47E4 loop body; true once the intro cab has left +void stIntroEnter(StSimT *sim, const StLevelT *level); // $4523..$4665 level-name screen +bool stIntroStep(StSimT *sim); // one $4666 frame; true when finished + +// ---- per-level hooks (stHooks.c) ---- +void stHookPrelude0(StSimT *sim); // $7D66 +void stHookPrelude1(StSimT *sim); // $7D69 +void stHookPerTick2(StSimT *sim); // $7D6C +void stHookPerTick3(StSimT *sim); // $7D6F +uint8_t stHookInput(StSimT *sim, uint8_t input); // $7D72 +uint8_t stHookHitVerdict(StSimT *sim); // $7D75: 0 = passenger contact is safe + +// ---- audio sink (host provides; stAudio.c / stsim stub) ---- +void stAudioSfx(const uint8_t *program9); // $42E9 load +void stAudioNoise(bool on); // $D412 = $81 / $80 +void stAudioThrustSweep(uint8_t value); // $D400/$D401 = value +void stAudioSpeech(uint8_t ch); // $9802 +void stAudioSilence(void); // voices 1+2 gate off +void stAudioVoice2(uint8_t freqLo, uint8_t freqHi, uint8_t ctrl); // level-hook tones +void stAudioVoice1Freq(uint8_t value); // $D400/$D401 pokes from the fuel pump + +#endif diff --git a/examples/spacetaxi/stTitle.c b/examples/spacetaxi/stTitle.c new file mode 100644 index 0000000..02890e7 --- /dev/null +++ b/examples/spacetaxi/stTitle.c @@ -0,0 +1,320 @@ +// Space Taxi -- the title screen intro and the level-name intro +// screen, run on the same simulation state as the game so everything +// that carries over (flame parity, wave index, walk parity, the +// sprite shadows) behaves as on the C64. +// +// Title ($477A + $47B0..$4A6C): the logo flip-book and colour cycle, +// a passenger who beams in, walks to the parked cab and boards, then +// the cab lifts off under thrust. When it has left, the attract demo +// starts. +// +// Level intro ($4523..$4727): a black screen with the level name, the +// cab rising from the bottom and seven star sprites streaming out of +// the centre, until the cab reaches mid-screen. + +#include + +#include "stSim.h" + +// Title sprite init tables ($4994 / $499C / $49A4 / $49AC / $49B4). +static const uint8_t kTitleCol[8] = { 0x28, 0x32, 0x00, 0x86, 0x9C, 0xA2, 0xBA, 0xD4 }; +static const uint8_t kTitleMsb[8] = { 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; +static const uint8_t kTitleColor[8] = { 0x06, 0x06, 0x06, 0x07, 0x07, 0x07, 0x07, 0x07 }; +static const uint8_t kTitleRow[8] = { 0x84, 0x84, 0x00, 0xE2, 0xE2, 0xE2, 0xE2, 0xE2 }; +static const uint8_t kTitlePtr[8] = { 0xC1, 0xC7, 0x00, 0xDB, 0xDE, 0xDF, 0xE0, 0xE1 }; +// Logo flip-book frame order ($48A7) and colour cycle ($489C). +static const uint8_t kLogoFlip[6] = { 1u, 2u, 3u, 4u, 3u, 2u }; +static const uint8_t kLogoColor[8] = { 0x02, 0x08, 0x07, 0x05, 0x06, 0x0E, 0x03, 0x04 }; +// Intro cab drift per loop count ($470D). +static const int8_t kIntroCabDy[4] = { -2, 1, -1, 1 }; +// Intro texts ($4624 / $463F). +static const uint8_t kTextDemoExit[] = { 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 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 }; + +#define ST_LOGO_CHAR 0x84u +#define ST_TITLE_CAB_LIFT_ROW 0x14u +#define ST_TITLE_SPARKLE_TICKS 0x5Au +#define ST_TITLE_HOVER_COL 0x28u +#define ST_INTRO_STAR_PTR 0xDAu +#define ST_INTRO_STAR_RELOAD 0x20u +#define ST_INTRO_CAB_END_ROW 0x94u +#define ST_INTRO_CAB_DX_RELOAD 0x28u + + +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 logoFlip(StSimT *sim); +static void marshalFrame(StSimT *sim); +static void starRespawn(StSimT *sim, uint8_t idx); + + +// $4113 +static void addToRow(StSimT *sim, uint8_t idx, uint8_t delta) { + sim->spr[idx].row = (uint8_t)(sim->spr[idx].row + 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 -- next logo colour over rows 1..11, columns 1..37, and onto +// sprites 3..7. +static void logoColorCycle(StSimT *sim) { + uint8_t color; + uint8_t row; + uint8_t col; + uint8_t k; + + sim->logoColorIdx = (uint8_t)((sim->logoColorIdx + 1u) & 7u); + color = kLogoColor[sim->logoColorIdx]; + for (row = 1u; row <= 11u; row++) { + for (col = 1u; col <= 37u; col++) { + stSimPutColor(sim, col, row, color); + } + } + for (k = 3u; k < ST_HW_SPRITES; k++) { + sim->spr[k].color = color; + } +} + + +// $4827 -- every fourth tick copy the next flip frame ($85..$88) over +// the logo glyph; the colour advances when the ping-pong hits 4. +static void logoFlip(StSimT *sim) { + sim->logoDiv = (uint8_t)((sim->logoDiv + 1u) & 3u); + if (sim->logoDiv != 0u) { + return; + } + memcpy(sim->charset[ST_LOGO_CHAR], sim->charset[ST_LOGO_CHAR + kLogoFlip[sim->logoCycleAux]], 8u); + sim->charDirty[ST_LOGO_CHAR] = 1u; + sim->logoCycleAux++; + if (sim->logoCycleAux == 6u) { + sim->logoCycleAux = 0u; + return; + } + if (sim->logoCycleAux == 4u) { + logoColorCycle(sim); + } +} + + +// $4253 + $4293 as the title and intro loops call them. +static void marshalFrame(StSimT *sim) { + uint8_t i; + uint8_t enable = 0u; + + for (i = 0u; i < ST_HW_SPRITES; i++) { + sim->frame.x[i] = (uint16_t)(((uint16_t)(sim->spr[i].msb != 0u ? 1u : 0u) << 8) | sim->spr[i].col); + sim->frame.y[i] = sim->spr[i].row; + sim->frame.ptr[i] = sim->spr[i].ptr; + sim->frame.color[i] = sim->spr[i].color; + if (sim->spr[i].enable != 0u) { + enable |= (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; +} + + +// --------------------------------------------------------------------------- +// Public +// --------------------------------------------------------------------------- + +// $4523..$4665 -- set up the level-name screen. +void stIntroEnter(StSimT *sim, const StLevelT *level) { + uint8_t k; + + sim->level = level; + sim->spr[7].ptr = 0xC0u; + sim->spriteMc1 = 0x07u; + sim->spriteMc0 = 0x02u; + sim->spr[7].enable = 1u; + sim->spr[7].col = 0xAAu; + sim->spr[7].msb = 0u; + sim->spr[7].color = 0x06u; + sim->spr[7].row = 0xE4u; + sim->introCabDxTimer = 0x14u; + sim->introCabDx = 0x01u; + for (k = 0u; k < 7u; k++) { + uint8_t idx = (uint8_t)(6u - k); + sim->spr[idx].row = 0x8Cu; + sim->spr[idx].msb = 0u; + sim->spr[idx].col = 0xAAu; + sim->spr[idx].color = 0x07u; + sim->spr[idx].ptr = ST_INTRO_STAR_PTR; + sim->spr[idx].enable = 0u; + sim->starTimer[idx] = stSimRng(sim, 0x20u); + sim->starReload[idx] = 0u; + } + sim->borderColor = 0u; + sim->bgColor = 0u; + sim->multiColorMask = 0x80u; + marshalFrame(sim); + // $40CA: blank screen. + memset(sim->screen, ST_CHAR_SPACE, ST_SCREEN_CELLS); + memset(sim->color, 0, ST_SCREEN_CELLS); + stSimDirtyAll(sim); + memcpy(sim->charset, stC64Charset(), sizeof(sim->charset)); + memset(sim->charDirty, 1, ST_CHARSET_CHARS); + stSimDrawText(sim, 10u, 12u, level->name, 3u); + if (sim->demoMode != 0u && sim->postMortem == 0u) { + stSimDrawText(sim, 7u, 3u, kTextDemoExit, 5u); + stSimDrawText(sim, 3u, 24u, kTextScreens, 4u); + } + sim->introLoopCount = 0u; +} + + +// $4666 -- one frame of the star field; true when the cab has risen +// to mid-screen and everything is hidden again. +bool stIntroStep(StSimT *sim) { + uint8_t k; + + for (k = 0u; k < 7u; k++) { + 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); + addToRow(sim, 7u, (uint8_t)kIntroCabDy[sim->introLoopCount]); + if ((sim->introLoopCount & 1u) == 0u) { + addToSpriteX(sim, 7u, sim->introCabDx); + sim->introCabDxTimer--; + if (sim->introCabDxTimer == 0u) { + sim->introCabDxTimer = ST_INTRO_CAB_DX_RELOAD; + sim->spr[7].ptr ^= 0x1Cu; + sim->introCabDx ^= 0xFEu; + } + } + if (sim->spr[7].row != ST_INTRO_CAB_END_ROW) { + return false; + } + // $4728: everything off; $44E1 then resets the SID and the RNG. + for (k = 0u; k < ST_HW_SPRITES; k++) { + sim->spr[k].enable = 0u; + } + marshalFrame(sim); + sim->rngT1 = 0x06u; + sim->rngT2 = 0x17u; + return true; +} + + +// $477A + $47B0..$47E1 -- show the title and start the intro. +void stTitleEnter(StSimT *sim, const StLevelT *title) { + uint8_t k; + + sim->level = title; + memcpy(sim->screen, title->screen, ST_SCREEN_CELLS); + memcpy(sim->color, title->color, ST_SCREEN_CELLS); + stSimDirtyAll(sim); + memcpy(sim->charset, stC64Charset(), sizeof(sim->charset)); + memset(sim->charDirty, 1, ST_CHARSET_CHARS); + logoColorCycle(sim); + for (k = 0u; k < ST_HW_SPRITES; k++) { + sim->spr[k].enable = 0u; + } + sim->logoCycleAux = 0u; + sim->borderColor = 0u; + sim->bgColor = 0u; + // $49BC: the cab parked on the pad, the passenger far right, the + // decoration sprites along the bottom. + for (k = 0u; k < ST_HW_SPRITES; k++) { + sim->spr[k].enable = 1u; + sim->spr[k].col = kTitleCol[k]; + sim->spr[k].msb = kTitleMsb[k]; + sim->spr[k].row = kTitleRow[k]; + sim->spr[k].ptr = kTitlePtr[k]; + sim->spr[k].color = kTitleColor[k]; + } + sim->multiColorMask = 0x07u; + sim->spriteMc1 = 0x07u; + sim->spriteMc0 = 0x02u; + marshalFrame(sim); + sim->stage = ST_STAGE_WAIT; + sim->introFrameCount = ST_TITLE_SPARKLE_TICKS; + sim->hoverXFrac = 0u; + sim->hoverXCol = ST_TITLE_HOVER_COL; + sim->decayTimer = sim->decayReload; + sim->collisionPhase = 0u; + sim->activePad = 0u; + sim->dirMask = 0u; +} + + +// $47E4 loop body: the intro stage machine ($4A03), the sprite flush +// and the logo animation. True once the cab has lifted off. +bool stTitleTick(StSimT *sim) { + switch (sim->stage) { + case ST_STAGE_WAIT: + // $4A17: sparkle while the countdown runs. + stFareWaveStep(sim); + sim->introFrameCount--; + if (sim->introFrameCount == 0u) { + sim->stage++; + } + break; + case ST_STAGE_WALK_TO_CAB: + // $4A24: walk in until parked at column $28. + sim->decayTimer--; + if (sim->decayTimer == 0u) { + sim->decayTimer = sim->decayReload; + stFareWalkStep(sim); + if (sim->spr[1].col == ST_TITLE_HOVER_COL && sim->spr[1].msb == 0u) { + sim->stage++; + } + } + break; + case ST_STAGE_BOARD: + // $4A45 -> $67A6: shrink into the cab. + stFareLeaveStep(sim); + break; + case ST_STAGE_RIDING: + // $4A48: lift off under UP thrust until above row $14. + addToRow(sim, 0u, 0xFEu); + sim->dirMask = 0x01u; + sim->spr[0].ptr = 0xC0u; + stSimFlameUpdate(sim); + if (sim->spr[0].row < ST_TITLE_CAB_LIFT_ROW) { + sim->stage++; + stAudioNoise(false); + } + break; + default: + break; + } + marshalFrame(sim); + logoFlip(sim); + return sim->stage == ST_STAGE_DROP_IN; +} diff --git a/make/amiga.mk b/make/amiga.mk index a6c59ad..d327bfe 100644 --- a/make/amiga.mk +++ b/make/amiga.mk @@ -88,7 +88,7 @@ ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c ADV2_BIN := $(BINDIR)/Adv2 AGI_SRCS := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c AGI_BIN := $(BINDIR)/Agi -STAXI_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stEngine.c $(EXAMPLES)/spacetaxi/stPassenger.c $(EXAMPLES)/spacetaxi/stHud.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/stAudio.c STAXI_INSTALL_DIR := $(BINDIR)/Taxi STAXI_BIN := $(STAXI_INSTALL_DIR)/Taxi @@ -121,14 +121,13 @@ STAXI_SPR_GEN := $(patsubst $(STAXI_SRC_DIR)/%.png,$(STAXI_GEN_DIR)/%.spr,$( STAXI_LEVEL_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_LEVEL_GEN)) STAXI_TBK_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_TBK_GEN)) STAXI_SPR_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_SPR_GEN)) -STAXI_ASSET_DSTS := $(STAXI_LEVEL_RUN) $(STAXI_TBK_RUN) $(STAXI_SPR_RUN) +STAXI_ASSET_DSTS := $(STAXI_LEVEL_RUN) # Pre-compiled sprite banks (.spc): baked from the .spr by the host spritebake # tool for THIS target and staged next to the .spr, so the runtime loads # compiled routines with no startup JIT (jlSpriteBankLoadPrecompiled). SPRITEBAKE_BIN := $(REPO_DIR)/build/tools/spritebake-amiga STAXI_SPC_GEN := $(STAXI_SPR_GEN:.spr=.spc) STAXI_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_SPC_GEN)) -STAXI_ASSET_DSTS += $(STAXI_SPC_RUN) ASSETBAKE_BIN := $(REPO_DIR)/tools/assetbake/assetbake.py ASSETBAKE_TARGET := amiga diff --git a/make/atarist.mk b/make/atarist.mk index 9773f31..2a1341b 100644 --- a/make/atarist.mk +++ b/make/atarist.mk @@ -75,7 +75,7 @@ ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c ADV2_BIN := $(BINDIR)/ADV2.PRG AGI_SRCS := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c AGI_BIN := $(BINDIR)/AGI.PRG -STAXI_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stEngine.c $(EXAMPLES)/spacetaxi/stPassenger.c $(EXAMPLES)/spacetaxi/stHud.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/stAudio.c STAXI_INSTALL_DIR := $(BINDIR)/STAXI STAXI_BIN := $(STAXI_INSTALL_DIR)/STAXI.PRG @@ -99,7 +99,7 @@ STAXI_SPR_GEN := $(patsubst $(STAXI_SRC_DIR)/%.png,$(STAXI_GEN_DIR)/%.spr,$( STAXI_LEVEL_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_LEVEL_GEN)) STAXI_TBK_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_TBK_GEN)) STAXI_SPR_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_SPR_GEN)) -STAXI_ASSET_DSTS := $(STAXI_LEVEL_RUN) $(STAXI_TBK_RUN) $(STAXI_SPR_RUN) +STAXI_ASSET_DSTS := $(STAXI_LEVEL_RUN) # Pre-compiled sprite banks (.spc): baked from the .spr by the host spritebake # tool for THIS target and staged next to the .spr, so the runtime loads # compiled routines with no startup JIT (jlSpriteBankLoadPrecompiled). ST is @@ -108,7 +108,6 @@ STAXI_ASSET_DSTS := $(STAXI_LEVEL_RUN) $(STAXI_TBK_RUN) $(STAXI_SPR_RUN) SPRITEBAKE_BIN := $(REPO_DIR)/build/tools/spritebake-atarist STAXI_SPC_GEN := $(STAXI_SPR_GEN:.spr=.spc) STAXI_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_SPC_GEN)) -STAXI_ASSET_DSTS += $(STAXI_SPC_RUN) ASSETBAKE_BIN := $(REPO_DIR)/tools/assetbake/assetbake.py ASSETBAKE_TARGET := atarist diff --git a/make/dos.mk b/make/dos.mk index 439545c..528dad1 100644 --- a/make/dos.mk +++ b/make/dos.mk @@ -71,7 +71,7 @@ ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c ADV2_BIN := $(BINDIR)/ADV2.EXE AGI_SRCS := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c AGI_BIN := $(BINDIR)/AGI.EXE -STAXI_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stEngine.c $(EXAMPLES)/spacetaxi/stPassenger.c $(EXAMPLES)/spacetaxi/stHud.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/stAudio.c # JoeyLib install convention: each app lives in its own bin subdir # with a DATA/ folder alongside the binary for runtime assets. STAXI_INSTALL_DIR := $(BINDIR)/STAXI @@ -122,14 +122,13 @@ ASSETBAKE_TARGET := dos STAXI_LEVEL_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_LEVEL_GEN)) STAXI_TBK_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_TBK_GEN)) STAXI_SPR_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_SPR_GEN)) -STAXI_ASSET_DSTS := $(STAXI_LEVEL_RUN) $(STAXI_TBK_RUN) $(STAXI_SPR_RUN) +STAXI_ASSET_DSTS := $(STAXI_LEVEL_RUN) # Pre-compiled sprite banks (.spc): baked from the .spr by the host spritebake # tool for THIS target and staged next to the .spr, so the runtime loads # compiled routines with no startup JIT (jlSpriteBankLoadPrecompiled). SPRITEBAKE_BIN := $(REPO_DIR)/build/tools/spritebake-dos STAXI_SPC_GEN := $(STAXI_SPR_GEN:.spr=.spc) STAXI_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_SPC_GEN)) -STAXI_ASSET_DSTS += $(STAXI_SPC_RUN) MKSTLEVEL_BIN := $(REPO_DIR)/build/tools/mkstlevel diff --git a/make/iigs.mk b/make/iigs.mk index a078f76..faacded 100644 --- a/make/iigs.mk +++ b/make/iigs.mk @@ -66,7 +66,7 @@ UBER_SRC := $(EXAMPLES)/uber/uber.c ADV_SRC := $(EXAMPLES)/adventure/adventure.c ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c AGI_SRCS := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c -STAXI_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stEngine.c $(EXAMPLES)/spacetaxi/stPassenger.c $(EXAMPLES)/spacetaxi/stHud.c $(EXAMPLES)/spacetaxi/stAudio.c +STAXI_SRCS := $(EXAMPLES)/spacetaxi/stC64Data.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 AUDIO_SRC := $(EXAMPLES)/audio/audio.c # NinjaTrackerPlus replayer: Merlin32-assemble ninjatrackerplus.s to a 34KB @@ -79,7 +79,7 @@ NTP_BIN := $(BUILD)/audio/ntpplayer.bin NTP_ASM := $(BUILD)/audio/ntpdata.s IIGS_MERLIN := $(REPO_DIR)/toolchains/iigs/merlin32/bin/merlin32 -.PHONY: all iigs iigs-lib iigs-clang-smoke iigs-examples iigs-disk iigs-verify iigs-verify-all iigs-verify-save iigs-verify-serial iigs-verify-shrtail clean-iigs clean +.PHONY: all iigs iigs-lib iigs-clang-smoke iigs-examples iigs-disk iigs-disk-all iigs-verify iigs-verify-all iigs-verify-save iigs-verify-serial iigs-verify-shrtail clean-iigs clean # Default: compile-check the library + run the end-to-end smoke test. all iigs: iigs-lib iigs-clang-smoke @@ -119,6 +119,17 @@ $(EXAMPLES)/spacetaxi/generated/iigs/sprites/%.spc: $(EXAMPLES)/spacetaxi/genera iigs-disk: iigs-examples $(NTP_BIN) $(STAXI_IIGS_SPC) 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 +# plus STAXI's data) will not fit an 800KB 3.5" floppy, so this is a 16 MB +# ProDOS hard-disk image (joey-all.2mg): boot it in MAME with +# `-sl7 cffa2 -hard1 -hard2 build/iigs/bin/joey-all.2mg` and launch +# any example from the JOEYLIB volume in the Finder. +iigs-disk-all: iigs-examples $(NTP_BIN) $(STAXI_IIGS_SPC) + BINDIR="$(BINDIR)" NTP_BIN="$(NTP_BIN)" \ + JOEY_DISK_SIZE="16MB" \ + JOEY_DISK_EXAMPLES="DRAW PATTERN KEYS JOY SPRITE SERIAL SERTEST SAVE AUDIO UBER ADV2 ADV AGI STAXI" \ + $(REPO_DIR)/scripts/make-iigs-disk.sh $(BINDIR)/joey-all.2mg + # Headless visual-verification gate: boot GS/OS under MAME, launch the DRAW # example off joey.2mg, and assert the SHR framebuffer was actually rendered # (many distinct colors -> pixels/lines/circles/tiles/flood all drew). Pass diff --git a/scripts/make-iigs-disk.sh b/scripts/make-iigs-disk.sh index 5a7bc86..e20dc7a 100755 --- a/scripts/make-iigs-disk.sh +++ b/scripts/make-iigs-disk.sh @@ -19,6 +19,11 @@ CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}" BINDIR="${BINDIR:-$repo/build/iigs/bin}" OUT="${1:-$BINDIR/joey.2mg}" VOL=JOEYLIB +# Volume size passed to cadius CREATEVOLUME. Defaults to an 800KB 3.5" +# floppy (the classic bootable image); set JOEY_DISK_SIZE to a larger +# ProDOS size (e.g. "5MB", "32MB") to build a hard-disk image that holds +# the full example set, which does not fit an 800KB floppy. +VOLSIZE="${JOEY_DISK_SIZE:-800KB}" [ -x "$CADIUS" ] || { echo "make-iigs-disk.sh: cadius not found at $CADIUS" >&2; exit 2; } @@ -30,7 +35,7 @@ work=$(mktemp -d -t joeylib-disk.XXXXXX) trap 'rm -rf "$work"' EXIT rm -f "$OUT" -"$CADIUS" CREATEVOLUME "$OUT" "$VOL" 800KB >/dev/null +"$CADIUS" CREATEVOLUME "$OUT" "$VOL" "$VOLSIZE" >/dev/null # Writable save area for the save-file API (joey/file.h -> jlSave*). GS/OS's # libc has no mkdir, but the IIgs save HAL creates SAVES/ at runtime via GS/OS @@ -122,6 +127,6 @@ fi echo "iigs-disk: $OUT (volume /$VOL)" echo " added: ${added[*]:-none}" if [ ${#skipped[@]} -gt 0 ]; then - echo " SKIPPED (800KB floppy full): ${skipped[*]}" + echo " SKIPPED ($VOLSIZE volume full): ${skipped[*]}" echo " -> mount a single-example disk via JOEY_DISK_EXAMPLES=\"NAME\" $0" fi diff --git a/scripts/run-iigs.sh b/scripts/run-iigs.sh index 241a83c..6c06392 100755 --- a/scripts/run-iigs.sh +++ b/scripts/run-iigs.sh @@ -13,10 +13,18 @@ # the example. No clicking needed. # GS/OS itself takes ~50 s to boot; # that part is authentic. +# scripts/run-iigs.sh --all MAME + every example on one 16 MB +# CFFA2 hard disk (the full set does +# not fit an 800KB floppy): boots +# GS/OS with the JOEYLIB volume so you +# pick a demo in Finder. +# scripts/run-iigs.sh --all staxi same disk, auto-launches the named +# example via the Lua Finder driver. # # The MAME path needs the apple2gs ROM set (default ~/.mame/roms, # override with MAME_ROMPATH). MAME_HEADLESS=1 runs it windowless with -# periodic PNG snapshots (batch verification). +# periodic PNG snapshots (batch verification). --all also needs the +# CFFA2 firmware (a2cffa2/cffa20eec02.bin), like the AGI path. set -euo pipefail @@ -24,11 +32,188 @@ repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) bin_dir=$repo/build/iigs/bin sys_disk=$repo/toolchains/emulators/support/gsos-system.po +# --all boots the full example set off a CFFA2 hard-disk image; an +# optional example name after it auto-launches that one. +all_mode=0 +if [[ "${1:-}" == "--all" ]]; then + all_mode=1 + shift +fi + if [[ $# -gt 1 ]]; then - echo "usage: $0 [example-name]" >&2 + echo "usage: $0 [--all] [example-name]" >&2 exit 2 fi +# ------------------------------------------------------------ --all (CFFA2) +if [[ $all_mode -eq 1 ]]; then + all_disk=$bin_dir/joey-all.2mg + target="" + if [[ $# -eq 1 ]]; then + target=${1^^} + if [[ ! -f "$bin_dir/$target" ]]; then + echo "$bin_dir/$target not built. Run 'make iigs' first." >&2 + exit 1 + fi + fi + if [[ ! -f $sys_disk ]]; then + echo "missing: $sys_disk (run ./toolchains/install.sh)" >&2 + exit 1 + fi + + # make-iigs-disk.sh needs the toolchain env (cadius path). + if [[ -z "${LLVM816_ROOT:-}" ]]; then + # shellcheck disable=SC1091 + source "$repo/toolchains/env.sh" >/dev/null 2>&1 || true + fi + + # (Re)build the all-examples image when it is missing or older than any + # example binary. The full launchable set, sized to a 16 MB ProDOS + # volume (an 800KB floppy can't hold it). + all_examples="DRAW PATTERN KEYS JOY SPRITE SERIAL SERTEST SAVE AUDIO UBER ADV2 ADV AGI STAXI" + need_disk=0 + if [[ ! -f $all_disk ]]; then + need_disk=1 + else + for name in $all_examples; do + if [[ -f "$bin_dir/$name" && "$bin_dir/$name" -nt $all_disk ]]; then + need_disk=1 + break + fi + done + fi + if [[ $need_disk -eq 1 ]]; then + echo "run-iigs: building all-examples disk ($all_disk)..." + BINDIR="$bin_dir" NTP_BIN="$bin_dir/ntpplayer.bin" \ + JOEY_DISK_SIZE="16MB" JOEY_DISK_EXAMPLES="$all_examples" \ + "$repo/scripts/make-iigs-disk.sh" "$all_disk" + else + echo "run-iigs: all-examples disk is current ($all_disk)" + fi + + # CFFA2 firmware (same resolution as the AGI path): prefer the repo- + # staged copy, fall back to the user's rompath. + staged_roms=$repo/toolchains/emulators/support/mame-roms + user_roms="${MAME_ROMPATH:-$HOME/.mame/roms}" + rompath="$user_roms" + if [[ -f "$staged_roms/a2cffa2/cffa20eec02.bin" ]]; then + rompath="$user_roms;$staged_roms" + elif [[ ! -f "$user_roms/a2cffa2/cffa20eec02.bin" ]]; then + cat >&2 </dev/null; then + kill "$mame_pid" 2>/dev/null || true + sleep 1 + kill -9 "$mame_pid" 2>/dev/null || true + fi + rm -rf "$work" + } + trap cleanup EXIT INT TERM + + # hard1: writable bootable GS/OS. hard2: the all-examples volume. + cp "$sys_disk" "$work/boot.po" + cp "$all_disk" "$work/joey-all.2mg" + + # Finder driver: only when a target is named (else the user picks a + # demo). Type J (select the JOEYLIB volume), Cmd-O (open), the full + # example name, Cmd-O (launch) -- the same steps the floppy path uses. + cat > "$work/launch.lua" <= steps[step_idx][1] do + steps[step_idx][2]() + step_idx = step_idx + 1 + end +end) +LUA + + video_arg="-window" + sound_arg="" + throttle_arg="" + if [[ "$headless" = "1" ]]; then + video_arg="-video soft" + sound_arg="-sound none" + throttle_arg="-nothrottle" + export QT_QPA_PLATFORM=offscreen + export SDL_VIDEODRIVER=dummy + export SDL_AUDIODRIVER=dummy + else + export SDL_MOUSE_RELATIVE_MODE_WARP=0 + fi + + if [[ -n $target ]]; then + echo "MAME apple2gs: all-examples CFFA2 disk, auto-launching $target." + else + echo "MAME apple2gs: all-examples CFFA2 disk. Open JOEYLIB and pick a demo." + fi + echo "GS/OS takes ~50 s to reach Finder. Quit MAME to end the session." + + cd "$work" + # -ramsize 4M: AGI (on this disk) needs the larger Memory Manager pool; + # harmless for the smaller demos. + mame apple2gs \ + -ramsize 4M \ + -rompath "$rompath" \ + -sl7 cffa2 -hard1 "$work/boot.po" -hard2 "$work/joey-all.2mg" \ + $video_arg $sound_arg $throttle_arg \ + -skip_gameinfo \ + -snapshot_directory "$work/snap" \ + -autoboot_script "$work/launch.lua" & + mame_pid=$! + wait "$mame_pid" || true + mame_pid="" + if [[ -d $work/snap ]]; then + rm -rf /tmp/run-iigs-snap + mv "$work/snap" /tmp/run-iigs-snap + echo "run-iigs: snapshots -> /tmp/run-iigs-snap" + fi + exit 0 +fi + # ---------------------------------------------------------------- MAME # Example given: auto-launching MAME path. if [[ $# -eq 1 ]]; then diff --git a/src/core/assetLoad.c b/src/core/assetLoad.c index b4d4ed3..dab092f 100644 --- a/src/core/assetLoad.c +++ b/src/core/assetLoad.c @@ -170,14 +170,23 @@ static void readPaletteFromHeader(const uint8_t *header, uint16_t *outPalette) { // file cannot be opened. FILE *jlDataOpen(const char *name, const char *mode) { char path[DATA_PATH_MAX]; - int written; + size_t prefixLen = sizeof(DATA_DIR_PREFIX) - 1u; // literal length, no NUL + size_t nameLen; - // snprintf's %s reads `name` through varargs, which keeps the whole - // path build in one place and out of any manual pointer arithmetic. - written = snprintf(path, sizeof(path), DATA_DIR_PREFIX "%s", name); - if (written < 0 || (size_t)written >= sizeof(path)) { + // A manual prefix concat instead of snprintf: the format is fixed + // ("DATA/" + name), and snprintf drags the whole C formatted-output + // engine (~20 KB of code plus a soft-float dependency) into every + // binary that opens a data file, which the IIgs bank-0 budget cannot + // spare. strlen/memcpy do the same join with none of that. + if (name == NULL) { return NULL; } + nameLen = strlen(name); + if (prefixLen + nameLen + 1u > sizeof(path)) { + return NULL; + } + memcpy(path, DATA_DIR_PREFIX, prefixLen); + memcpy(path + prefixLen, name, nameLen + 1u); // copy the NUL too return fopen(path, mode); } diff --git a/tools/stsim/Makefile b/tools/stsim/Makefile new file mode 100644 index 0000000..ebf1bea --- /dev/null +++ b/tools/stsim/Makefile @@ -0,0 +1,11 @@ +# Host build of the Space Taxi simulation core for trace comparison. +ST := ../../examples/spacetaxi +CC ?= gcc +CFLAGS := -O1 -g -Wall -Wextra -std=c99 -I$(ST) +SRCS := stsim.c $(ST)/stSim.c $(ST)/stFare.c $(ST)/stHooks.c $(ST)/stLevelFile.c $(ST)/stTitle.c $(ST)/stC64Data.c + +stsim: $(SRCS) $(ST)/stSim.h $(ST)/stC64Data.h $(ST)/stDemoStreams.h + $(CC) $(CFLAGS) $(SRCS) -o $@ + +clean: + rm -f stsim diff --git a/tools/stsim/compare.py b/tools/stsim/compare.py new file mode 100644 index 0000000..9174b31 --- /dev/null +++ b/tools/stsim/compare.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +# Compare stsim output against a VICE trace segment. +# compare.py [maxReports] +import json, sys + +trace = [json.loads(l) for l in open(sys.argv[1])] +start = int(sys.argv[2]) +sim = [json.loads(l) for l in open(sys.argv[3])] +maxRep = int(sys.argv[4]) if len(sys.argv) > 4 else 12 + +def fromTrace(r): + g = bytes.fromhex(r['g7140']) + pos = bytes.fromhex(r['pos']) + hud = bytes.fromhex(r['hud']) # 0x0799 .. 0x07E6 + def G(a): return g[a - 0x7140] + def H(a): return hud[a - 0x0799] + d = {} + d['x'] = '%02X%02X.%02X' % (pos[9], pos[8], pos[7]) # 7D63 7D62 7D61 + d['y'] = '%02X.%02X' % (pos[11], pos[10]) + d['vx'] = '%02X%02X' % (G(0x714D), G(0x714C)) + d['vy'] = '%02X%02X' % (G(0x714F), G(0x714E)) + d['pad'] = G(0x7150) + d['st'] = G(0x7163) + d['ph'] = G(0x7164) + d['in'] = '%02X' % G(0x7169) + d['dir'] = '%02X' % G(0x716A) + d['ptr0'] = '%02X' % G(0x7197) + d['ptr1'] = '%02X' % G(0x7198) + d['s1'] = '%02X%02X.%02X' % (G(0x7186), G(0x7176), G(0x717E)) + d['en'] = '%02X' % G(0x7196) + d['t4740']= r['t4740'].upper() + d['rng'] = '%02X%02X' % (G(0x7171), G(0x7172)) + d['fuel'] = G(0x71C9) + d['slots']= G(0x715C) + d['asi'] = pos[0x7D8E - 0x7D5A] if len(pos) > 0x7D8E - 0x7D5A else -1 + d['score']= ''.join('%02X' % H(0x07C2 + k) for k in range(7)) + d['fare'] = ''.join('%02X' % H(0x07E0 + k) for k in range(7)) + d['fuelc']= ''.join('%02X' % H(0x07A6 + k) for k in range(12)) + # sprite shadow positions $71A7.. interleaved X,Y for 8 sprites + msb $718D + msb = G(0x718D) + sx = '' + for k in range(8): + x = G(0x71A7 + 2*k) | (0x100 if (msb >> k) & 1 else 0) + sx += '%03X%02X' % (x, G(0x71A8 + 2*k)) + d['sprx'] = sx + return d + +fields = ['x','y','vx','vy','pad','st','ph','in','dir','ptr0','ptr1','s1','en','t4740','rng','fuel','slots','score','fare','fuelc','sprx'] +reports = 0 +firstBad = None +for s in sim: + if 'end' in s: + print('sim ended at tick', s['tick'], 'result', s['end']) + break + t = s['tick'] + if start + t >= len(trace): + print('trace exhausted at sim tick', t); break + r = fromTrace(trace[start + t]) + # Disabled sprites keep stale shadow values on the C64; only compare + # positions/pointers of enabled ones. + en = int(r['en'], 16) + def spr(sx): + return ''.join(sx[k*5:k*5+5] if (en >> k) & 1 else '-----' for k in range(8)) + s2 = dict(s); r2 = dict(r) + s2['sprx'] = spr(s['sprx']); r2['sprx'] = spr(r['sprx']) + if not (en & 2): + s2['ptr1'] = r2['ptr1'] = '--'; s2['s1'] = r2['s1'] = '--' + bad = [f for f in fields if f in s2 and str(s2[f]) != str(r2[f])] + s = s2; r = r2 + if bad: + if firstBad is None: + firstBad = t + if reports < maxRep: + print(f'tick {t}: ' + ' '.join(f'{f}: sim={s[f]} c64={r[f]}' for f in bad)) + reports += 1 +if firstBad is None: + print('ALL', len(sim), 'ticks match') +else: + print('first mismatch at tick', firstBad) diff --git a/tools/stsim/stsim b/tools/stsim/stsim new file mode 100755 index 0000000..d5fb5c4 Binary files /dev/null and b/tools/stsim/stsim differ diff --git a/tools/stsim/stsim.c b/tools/stsim/stsim.c new file mode 100644 index 0000000..a6b8e88 --- /dev/null +++ b/tools/stsim/stsim.c @@ -0,0 +1,110 @@ +// stsim -- host-side driver for the Space Taxi simulation core. +// +// Loads an STL4 level, attaches a recorded demo input stream and runs +// the game tick, printing one line of state per tick in the same shape +// the VICE tracer records (see stuff/spacetaxi and the compare script), +// so the port's engine can be diffed against the real machine. +// +// stsim + +#include +#include +#include + +#include "../../examples/spacetaxi/stSim.h" +#include "../../examples/spacetaxi/stDemoStreams.h" + + +static StLevelT gLevel; +static StSimT gSim; + + +void stAudioSfx(const uint8_t *program9) { (void)program9; } +void stAudioNoise(bool on) { (void)on; } +void stAudioThrustSweep(uint8_t value) { (void)value; } +void stAudioSpeech(uint8_t ch) { (void)ch; } +void stAudioSilence(void) { } +void stAudioVoice2(uint8_t freqLo, uint8_t freqHi, uint8_t ctrl) { (void)freqLo; (void)freqHi; (void)ctrl; } +void stAudioVoice1Freq(uint8_t value) { (void)value; } + + +static void printState(const StSimT *sim, uint32_t tick) { + uint8_t k; + + printf("{\"tick\":%u,\"x\":\"%02X%02X.%02X\",\"y\":\"%02X.%02X\",\"vx\":\"%04X\",\"vy\":\"%04X\"," + "\"pad\":%u,\"st\":%u,\"ph\":%u,\"in\":\"%02X\",\"dir\":\"%02X\",\"ptr0\":\"%02X\",\"ptr1\":\"%02X\"," + "\"s1\":\"%02X%02X.%02X\",\"en\":\"%02X\",\"t4740\":\"%02X\",\"off\":%u,\"rng\":\"%02X%02X\"," + "\"fuel\":%u,\"slots\":%u,\"asi\":%u,\"score\":\"", + tick, sim->posXmsb, sim->posXcol, sim->posXlo, sim->posYrow, sim->posYlo, + (uint16_t)sim->velX, (uint16_t)sim->velY, sim->activePad, sim->stage, sim->collisionPhase, + sim->inputMask, sim->dirMask, sim->spr[0].ptr, sim->spr[1].ptr, + sim->spr[1].msb, sim->spr[1].col, sim->spr[1].row, sim->frame.enableMask, + sim->demoTimer, sim->demoOff, sim->rngT1, sim->rngT2, sim->fuelCells, sim->fareSlotCount, sim->activeSpriteIdx); + for (k = 0u; k < ST_NUMBER_CHARS; k++) { + printf("%02X", sim->screen[ST_CELL_SCORE + k]); + } + printf("\",\"fare\":\""); + for (k = 0u; k < ST_NUMBER_CHARS; k++) { + printf("%02X", sim->screen[ST_CELL_FARE + k]); + } + printf("\",\"fuelc\":\""); + for (k = 0u; k < ST_FUEL_CELLS; k++) { + printf("%02X", sim->screen[ST_CELL_FUEL + k]); + } + printf("\",\"sprx\":\""); + for (k = 0u; k < ST_HW_SPRITES; k++) { + printf("%03X%02X", sim->frame.x[k], sim->frame.y[k]); + } + printf("\"}\n"); +} + + +int main(int argc, char **argv) { + FILE *fp; + const uint8_t *stream = kDemoStreamH; + uint16_t len = (uint16_t)sizeof(kDemoStreamH); + uint32_t ticks; + uint32_t t; + + if (argc < 4) { + fprintf(stderr, "usage: stsim \n"); + return 2; + } + fp = fopen(argv[1], "rb"); + if (fp == NULL || !stLevelParse(&gLevel, fp)) { + fprintf(stderr, "stsim: cannot load %s\n", argv[1]); + return 1; + } + fclose(fp); + switch (argv[2][0]) { + case 'W': stream = kDemoStreamW; len = (uint16_t)sizeof(kDemoStreamW); break; + case 'T': stream = kDemoStreamT; len = (uint16_t)sizeof(kDemoStreamT); break; + case 'X': stream = kDemoStreamX; len = (uint16_t)sizeof(kDemoStreamX); break; + default: break; + } + ticks = (uint32_t)strtoul(argv[3], NULL, 0); + stSimNewGame(&gSim, 1u, true); + stSimSeedDemoBuffer(&gSim, kDemoBufferInit, ST_DEMO_BUFFER_BYTES); + if (argc > 4) { + // carried-over state: flame parity, wave index, walk parity + gSim.flameParity = (uint8_t)strtoul(argv[4], NULL, 16); + } + if (argc > 5) { + gSim.waveIdx = (uint8_t)strtoul(argv[5], NULL, 16); + } + if (argc > 6) { + gSim.walkParity = (uint8_t)strtoul(argv[6], NULL, 16); + } + stSimEnterLevel(&gSim, &gLevel); + stSimSetDemoStream(&gSim, stream, len); + for (t = 0u; t < ticks; t++) { + StTickResultE r; + printState(&gSim, t); + r = stSimTick(&gSim); + if (r != ST_TICK_CONTINUE) { + printf("{\"tick\":%u,\"end\":%d}\n", t, (int)r); + break; + } + } + return 0; +}