From cd2c5e6b095b41540ad48b573b6e28ec50a8781b Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Sun, 6 Sep 2026 18:22:45 -0500 Subject: [PATCH] More work on STAXI. --- examples/spacetaxi/assets/genC64Data.py | 160 ++ examples/spacetaxi/assets/genDemoStreams.py | 29 +- .../generated/amiga/levels/level01.dat | Bin 2034 -> 2749 bytes .../generated/amiga/levels/level02.dat | Bin 2042 -> 2749 bytes .../generated/amiga/levels/level03.dat | Bin 2050 -> 2749 bytes .../generated/amiga/levels/level04.dat | Bin 2070 -> 2749 bytes .../generated/amiga/levels/level05.dat | Bin 2066 -> 2749 bytes .../generated/amiga/levels/level06.dat | Bin 2042 -> 2813 bytes .../generated/amiga/levels/level07.dat | Bin 2050 -> 3005 bytes .../generated/amiga/levels/level08.dat | Bin 2050 -> 2813 bytes .../generated/amiga/levels/level09.dat | Bin 2058 -> 3389 bytes .../generated/amiga/levels/level10.dat | Bin 2054 -> 3581 bytes .../generated/amiga/levels/level11.dat | Bin 2050 -> 2749 bytes .../generated/amiga/levels/level12.dat | Bin 2054 -> 3005 bytes .../generated/amiga/levels/level13.dat | Bin 2070 -> 2749 bytes .../generated/amiga/levels/level14.dat | Bin 2062 -> 2749 bytes .../generated/amiga/levels/level15.dat | Bin 2034 -> 2749 bytes .../generated/amiga/levels/level16.dat | Bin 2042 -> 3197 bytes .../generated/amiga/levels/level17.dat | Bin 2062 -> 3517 bytes .../generated/amiga/levels/level18.dat | Bin 2034 -> 2749 bytes .../generated/amiga/levels/level19.dat | Bin 2050 -> 2749 bytes .../generated/amiga/levels/level20.dat | Bin 2062 -> 2749 bytes .../generated/amiga/levels/level21.dat | Bin 2050 -> 2941 bytes .../generated/amiga/levels/level22.dat | Bin 2058 -> 2749 bytes .../generated/amiga/levels/level23.dat | Bin 2042 -> 2749 bytes .../generated/amiga/levels/level24.dat | Bin 2062 -> 2749 bytes .../generated/amiga/levels/title.dat | Bin 2028 -> 2749 bytes .../generated/atarist/levels/level01.dat | Bin 2034 -> 2749 bytes .../generated/atarist/levels/level02.dat | Bin 2042 -> 2749 bytes .../generated/atarist/levels/level03.dat | Bin 2050 -> 2749 bytes .../generated/atarist/levels/level04.dat | Bin 2070 -> 2749 bytes .../generated/atarist/levels/level05.dat | Bin 2066 -> 2749 bytes .../generated/atarist/levels/level06.dat | Bin 2042 -> 2813 bytes .../generated/atarist/levels/level07.dat | Bin 2050 -> 3005 bytes .../generated/atarist/levels/level08.dat | Bin 2050 -> 2813 bytes .../generated/atarist/levels/level09.dat | Bin 2058 -> 3389 bytes .../generated/atarist/levels/level10.dat | Bin 2054 -> 3581 bytes .../generated/atarist/levels/level11.dat | Bin 2050 -> 2749 bytes .../generated/atarist/levels/level12.dat | Bin 2054 -> 3005 bytes .../generated/atarist/levels/level13.dat | Bin 2070 -> 2749 bytes .../generated/atarist/levels/level14.dat | Bin 2062 -> 2749 bytes .../generated/atarist/levels/level15.dat | Bin 2034 -> 2749 bytes .../generated/atarist/levels/level16.dat | Bin 2042 -> 3197 bytes .../generated/atarist/levels/level17.dat | Bin 2062 -> 3517 bytes .../generated/atarist/levels/level18.dat | Bin 2034 -> 2749 bytes .../generated/atarist/levels/level19.dat | Bin 2050 -> 2749 bytes .../generated/atarist/levels/level20.dat | Bin 2062 -> 2749 bytes .../generated/atarist/levels/level21.dat | Bin 2050 -> 2941 bytes .../generated/atarist/levels/level22.dat | Bin 2058 -> 2749 bytes .../generated/atarist/levels/level23.dat | Bin 2042 -> 2749 bytes .../generated/atarist/levels/level24.dat | Bin 2062 -> 2749 bytes .../generated/atarist/levels/title.dat | Bin 2028 -> 2749 bytes .../generated/dos/levels/level01.dat | Bin 2034 -> 2749 bytes .../generated/dos/levels/level02.dat | Bin 2042 -> 2749 bytes .../generated/dos/levels/level03.dat | Bin 2050 -> 2749 bytes .../generated/dos/levels/level04.dat | Bin 2070 -> 2749 bytes .../generated/dos/levels/level05.dat | Bin 2066 -> 2749 bytes .../generated/dos/levels/level06.dat | Bin 2042 -> 2813 bytes .../generated/dos/levels/level07.dat | Bin 2050 -> 3005 bytes .../generated/dos/levels/level08.dat | Bin 2050 -> 2813 bytes .../generated/dos/levels/level09.dat | Bin 2058 -> 3389 bytes .../generated/dos/levels/level10.dat | Bin 2054 -> 3581 bytes .../generated/dos/levels/level11.dat | Bin 2050 -> 2749 bytes .../generated/dos/levels/level12.dat | Bin 2054 -> 3005 bytes .../generated/dos/levels/level13.dat | Bin 2070 -> 2749 bytes .../generated/dos/levels/level14.dat | Bin 2062 -> 2749 bytes .../generated/dos/levels/level15.dat | Bin 2034 -> 2749 bytes .../generated/dos/levels/level16.dat | Bin 2042 -> 3197 bytes .../generated/dos/levels/level17.dat | Bin 2062 -> 3517 bytes .../generated/dos/levels/level18.dat | Bin 2034 -> 2749 bytes .../generated/dos/levels/level19.dat | Bin 2050 -> 2749 bytes .../generated/dos/levels/level20.dat | Bin 2062 -> 2749 bytes .../generated/dos/levels/level21.dat | Bin 2050 -> 2941 bytes .../generated/dos/levels/level22.dat | Bin 2058 -> 2749 bytes .../generated/dos/levels/level23.dat | Bin 2042 -> 2749 bytes .../generated/dos/levels/level24.dat | Bin 2062 -> 2749 bytes .../spacetaxi/generated/dos/levels/title.dat | Bin 2028 -> 2749 bytes .../generated/iigs/levels/level01.dat | Bin 2034 -> 2749 bytes .../generated/iigs/levels/level02.dat | Bin 2042 -> 2749 bytes .../generated/iigs/levels/level03.dat | Bin 2050 -> 2749 bytes .../generated/iigs/levels/level04.dat | Bin 2070 -> 2749 bytes .../generated/iigs/levels/level05.dat | Bin 2066 -> 2749 bytes .../generated/iigs/levels/level06.dat | Bin 2042 -> 2813 bytes .../generated/iigs/levels/level07.dat | Bin 2050 -> 3005 bytes .../generated/iigs/levels/level08.dat | Bin 2050 -> 2813 bytes .../generated/iigs/levels/level09.dat | Bin 2058 -> 3389 bytes .../generated/iigs/levels/level10.dat | Bin 2054 -> 3581 bytes .../generated/iigs/levels/level11.dat | Bin 2050 -> 2749 bytes .../generated/iigs/levels/level12.dat | Bin 2054 -> 3005 bytes .../generated/iigs/levels/level13.dat | Bin 2070 -> 2749 bytes .../generated/iigs/levels/level14.dat | Bin 2062 -> 2749 bytes .../generated/iigs/levels/level15.dat | Bin 2034 -> 2749 bytes .../generated/iigs/levels/level16.dat | Bin 2042 -> 3197 bytes .../generated/iigs/levels/level17.dat | Bin 2062 -> 3517 bytes .../generated/iigs/levels/level18.dat | Bin 2034 -> 2749 bytes .../generated/iigs/levels/level19.dat | Bin 2050 -> 2749 bytes .../generated/iigs/levels/level20.dat | Bin 2062 -> 2749 bytes .../generated/iigs/levels/level21.dat | Bin 2050 -> 2941 bytes .../generated/iigs/levels/level22.dat | Bin 2058 -> 2749 bytes .../generated/iigs/levels/level23.dat | Bin 2042 -> 2749 bytes .../generated/iigs/levels/level24.dat | Bin 2062 -> 2749 bytes .../spacetaxi/generated/iigs/levels/title.dat | Bin 2028 -> 2749 bytes examples/spacetaxi/spacetaxi.c | 1083 +++++----- examples/spacetaxi/spacetaxi.h | 348 +--- examples/spacetaxi/stAudio.c | 331 ++- examples/spacetaxi/stC64Data.c | 1370 +++++++++++++ examples/spacetaxi/stC64Data.h | 62 + examples/spacetaxi/stDemoStreams.h | 89 +- examples/spacetaxi/stEngine.c | 456 ----- examples/spacetaxi/stFare.c | 451 ++++ examples/spacetaxi/stHooks.c | 1001 +++++++++ examples/spacetaxi/stHud.c | 82 - examples/spacetaxi/stLevel.c | 122 +- examples/spacetaxi/stLevelFile.c | 90 + examples/spacetaxi/stPassenger.c | 318 --- examples/spacetaxi/stRender.c | 1810 +++++------------ examples/spacetaxi/stSim.c | 1291 ++++++++++++ examples/spacetaxi/stSim.h | 366 ++++ examples/spacetaxi/stTitle.c | 320 +++ make/amiga.mk | 5 +- make/atarist.mk | 5 +- make/dos.mk | 5 +- make/iigs.mk | 15 +- scripts/make-iigs-disk.sh | 9 +- scripts/run-iigs.sh | 189 +- src/core/assetLoad.c | 19 +- tools/stsim/Makefile | 11 + tools/stsim/compare.py | 79 + tools/stsim/stsim | Bin 0 -> 148328 bytes tools/stsim/stsim.c | 110 + 130 files changed, 6859 insertions(+), 3367 deletions(-) create mode 100644 examples/spacetaxi/assets/genC64Data.py create mode 100644 examples/spacetaxi/stC64Data.c create mode 100644 examples/spacetaxi/stC64Data.h delete mode 100644 examples/spacetaxi/stEngine.c create mode 100644 examples/spacetaxi/stFare.c create mode 100644 examples/spacetaxi/stHooks.c delete mode 100644 examples/spacetaxi/stHud.c create mode 100644 examples/spacetaxi/stLevelFile.c delete mode 100644 examples/spacetaxi/stPassenger.c create mode 100644 examples/spacetaxi/stSim.c create mode 100644 examples/spacetaxi/stSim.h create mode 100644 examples/spacetaxi/stTitle.c create mode 100644 tools/stsim/Makefile create mode 100644 tools/stsim/compare.py create mode 100755 tools/stsim/stsim create mode 100644 tools/stsim/stsim.c 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 f8a6f9a577c6c7e3d73bde9cbd621efb842d4093..0c762d8e3431392fad52c3f34c8606532ccbde06 100644 GIT binary patch literal 2749 zcmeH{OHKko5QeMUX~vd>GxP=6c?FCyMiarfbpazn;)m@WqcrS9QO!PN*xpeXzTU*E9)^vJJ& zs+K*WwSrZJZRNI@)0rHzc;Hsma~5s8fx|-WWEKhOMaA{9OmsiU3!Hn6@wS8HLEK;J z`9SkE%uVa!Ni+UY{)E>xq-*5%;@~q{j@fvizSo=cDWi^S+cDuc(Fp}&Y-Obx(!_j=4 ziXt(q^kIM6)=euqrz7ME#orTUJpRHP^R_k^_XiVL!ht_`m<$y4*|gqY3hUYxSu{wSw$;BJ!zZz>o!X(;r?W6 z+3I3dbRg)V%PZ>Pu)n%qE|Pp21J8Kg{B8*mJC$jrzXCb@Crdiyfy_!_(&M}w8XS*+ zdC>##YNY6mFAz*zS`OxL;zm)d#m}5vsBxU2RxgSeUnxz;`-}Iw!D%1iczph>J*k`Y yLsjW|b=cdcv?;t9rA(={J`Gly;V@+}Ek;p%=aLztb;<%fvZegdJ8s}VHt++Ni0Fd= delta 43 vcmdlh`iWmDIK;=8-N!Y|)kndR0SM%n`S=(Zfea>gHa12sQSRW4%@^1K!$$|$ diff --git a/examples/spacetaxi/generated/amiga/levels/level02.dat b/examples/spacetaxi/generated/amiga/levels/level02.dat index dea590378b780e5ff61a0e7c7938644c1f6d5b50..dbebde54cfb5666c5d7580175e47678faa75eb5d 100644 GIT binary patch literal 2749 zcmeH}O>PrG5QWPV_Y9*pi`-x&q?~{Y@M9DSlnq%DDxQ`cK@a7=z}Y9XF18v;rvj^lJFNqsetnbx>I81n)nZgrcSH+LLz z(OvIFZylVmpfhP{@Zw-orQVlyIQTD1}tEo~F)Cl3+oZNuNv9M0TpDJ(C^k z3fU#Hz;L3hKO=~yDkJ~Nai0G&{*C7`YmC&Ab=orQ`kOw*gz_GjN1s61ajS z6FEobmr8@HJxI=QIOJqKRZbj(&J{(09cmP@ICzxiy28}}g;Q9+Kp{PP>^K2aUo^{% znX8$Dq`&AWiTm=@2e{afliw+Wef2m@oGT%YWbDVj%aw{TXeo;`uEg6lo6TzeB&E3W xSd4P|?M$cq0=SC1yq8ot?`^evKakB|KK>~cg{|`c2*;xC2JeEq8@P=d_yZFt4gUZD delta 51 zcmdlh`ioyHIK;=8-N!Y|)kndJ0SNfG1O*uwfea>gHa2EP7FKRHVP0cVc9shpn=h~f E0QX=B$p8QV diff --git a/examples/spacetaxi/generated/amiga/levels/level03.dat b/examples/spacetaxi/generated/amiga/levels/level03.dat index 9b5a7033826b7eb8a3f31643ba59989ee02327b3..41ae91bbb408052b501566bdb8013a845b3f593c 100644 GIT binary patch delta 764 zcmcgoKWGzC9R9s$lDmqP=zl%QLO;O{QDI`)J2et*7??|ZzJe0bWf+$>*K z5>#&7QYy0xSLf#zk`cz>QZARxzJdVYS{_aTPu6Fkro(g(X5da2uJqtN{OH17A6DUY z9~wOvvFkch`(uay-Wb~5lxPJUQlX*_aP*B(E4`fToCgzXU)S#yL#rg>7> z5CxOjZbL*T;;awUw03@@yOEmjYo+$Q6;HL%iWXl?M%}j)-f?9@jpRHxG%rJKFfc+} zY@NMNb|p`zjj13}ssNxwEXiv_<*0 z*jy@@U!0}Q<@Kr-?qVyE`0J$EdWSTBKfvK6)dK!RcNfcb;d6Hz>B%&F6$xW9>Z4=B zbFH0U6!CHFVJPQSPf{+bltJok{73i-nWv0RlqVC}!9jfJOYIJ7$@d)Z9Py00crzLIMm-Kn4>#8yhPF2M;rg5Ua9?5X%W(5teKj L4o;1Y%@^1K8ixre diff --git a/examples/spacetaxi/generated/amiga/levels/level04.dat b/examples/spacetaxi/generated/amiga/levels/level04.dat index 74993ec09de70db8622005e7f4914b7e75932f3b..55b6b37d2503e565229c90151c0e83f1978d2bb1 100644 GIT binary patch literal 2749 zcmeHHON$dh5U!rslXMmY!Ee0#W;Myfk7_8si(u1A#t?i8+eg}u7 zou*xN9cBQtvv3Adc3+0=5nMFzNA!LOZ2;l@6_^?L1mC{E2Lr!EdKG>eabEO?;+%mS zaQGQ^nSbFU{5H^o{&(0kq9^)a#irT2*TDV*0(o;B29f>ST-ULfgx)*<&dU+;>Ch*M z=c`3@hR+bob2Z&;93u0nwXX5PK*L@kkNSI#7@vz*rv9vM{@6F>quwfcx;_w3D6DLp zy|ubacv??fzyHbOO}?ZLj{{N<29M1qtVEEt3i-~?5U!14s8NCyWMUrq+&?IfRy1|&5rR%h~TL)o>B>b zr>P`7SJO@E2+pV)FAOy774lA7&|KV~cS9(6g;a5U0QNu-XqW|$64HSMfRw=dSjGdD z+I|=FsQe{7*C~CIY^i%(FK!U_n|a`L7dBJyxFZLr)$E=wF851FW6g9NWKnFCr|7OF z?!H*XSU$yh5&fl7636REh#}ZB5Ex WvLM-0MO(0EVM5A@eKLWSPv9@&{us;v delta 79 zcmdlhI!(YWIK;=8-N!Y|)kndF0SNfG1VtFwfD9&fHa0G1E@o+IQC1F4W|jheL6&MM g7Us)}94y@I{LI$UeC)Rwgjo#~gjfwWHeX-|0Nc?E_5c6? diff --git a/examples/spacetaxi/generated/amiga/levels/level05.dat b/examples/spacetaxi/generated/amiga/levels/level05.dat index 8467bf0bb7a8d5024ef51fcdfb7feb0145747eed..02e66bed022342e2e1bbdd8a862150d75e1ec75d 100644 GIT binary patch literal 2749 zcmeHH&ubGw6rNo=i9-$|Z6G3)?H}OHgF+9f&|VB)S`b3jRUszUlLVCD?5$9-n{(4h zD?Nx468{0kL-Ejnh{U%36^5QXc=EkBvzys&e|QkR^kdoC_r33Z@6GHc^9!Xb9^(4! z%+2|QnbHl4JjMhAxY=!q~vSe+c&;0@CblJ=tmZJO4npyZ3s ze~S-Nfca9~-AasVwK5wAjYeax0?Y`{;IaO+UMJT6T~v^Xvpj_?AVqR6jsvq3oJW5k zu-XYB5~1a16_AADsMOK{g(<;cI82|SW2hqy7DY( zJk5YQdNAeqZ1xce$r*H7X5FoQAWH{HeXUu**6cU7J4pxX6Lr*`<{Rmv`Sd17<#>M9 zv^1hVKdjIh>Zf&3Xgo1(&V_W806q0Z|FkaA2mNRaqr+!&F4UQF;F9#y7WT-fLpC`d zZCCqgqo~&*KRqS&H9BntZV{EIA>z<2{<+&V>vyj$6Ht`-fcimBT zjE3+5$_a%G%TlqhC6ybtr5X>{q?!!ZWiS7v?;JS3nuEaa3E3B49lzI<{T!@A5Uk7o zEfg>i3|q3-l6`(KRj5sLy5KSJUIehNtf+~rKU%Uwgm*3Qf!w;-Ys*6IQ6$+qPw^7O zpeU8G5bO%Id`EqcL~307>;d5qg~Y3j7ft_VO~%)-E#XqHm#ZpRu6dV>esd#O-3+`- g#djM)cQa7jn=JZ_O;0m6<^9Y!=CzzHfA<7_11l4gYybcN delta 75 zcmdlhI!VAOIK;=8-N!Y|)kndVfq{WjMo3VUfepxFVrOIHy4+N`%MLhVCq;ZUB3kwOB;FzSa;AIY8 zJxY7n!u}KDB{vgI^YKT796edzo0*-RB-^G!@zft-=FNM*`Mrn zz0YN3^VS!q$ZOV{^*MU4sK6$rr}{UA)obetA1`}tur<^(yFMLC7E=AfZ?TEH+&9*KD1j7kG*@`QJhCpAP=YDiu*K5-v-Myf1I4;ceN1RLLnZC5mC8m(2*s!2w6}QUZ7jc?mD& zlq=4QnN_xUlO9o#dQU}pF+o|1hbLK|@E@r+%`>MaZ(>SWk1F_>UZT)~f^Av019QNy zLBO$dIa@~|EP-D0nkb{Ci4{Cxal-1fCFSf|}m6dwy(v{_4-!BIr z{WtFA)!=Xq3LP-mq1quE4cL$L2$Z{QY`3 delta 51 zcmew>`ioyHIK;=8-N!Y|)kndN0SJWHM1>icfea>gHa2D!0cK@c0cH+a7G{o(%@^1K D0^kT$ diff --git a/examples/spacetaxi/generated/amiga/levels/level07.dat b/examples/spacetaxi/generated/amiga/levels/level07.dat index 805246c776f4e9d3139e81976dc2a8730425a10d..0eb4025aa5dd7be0ba68be67b8dbbf7b0dd6fd6d 100644 GIT binary patch literal 3005 zcmd^9O=whC6h80Gym_Xv31SctDT9SV7TvUxRgi3wg^oYe&_xz9q-~N~f4CA+0{3MS z$<7E;ILKcM=I=GWAIuL>jjjc>#=)#qB=|Tc7nvFl_ocr^BGRE0^k~#1EeCIpg zy>FPaqm`p&veDtn@R`$Jjh^MK46ql_{IFCCf@Qc1@by9HgAx@7;M@iHauq&~C-Xs@LNUOncp{@Grdn$%Fd@?ZhTKHLcA0$w`jP!&b}Xd_lw7)NRAixYPxj zGsBloPN0-p#vG1NJguLGY$so80WqP` zQZyInv~8t5Zs%>k3ukw^6?zMv>H=N4vz=Pn>C?k*2pKM#`_Tt`kDqWhH8nBpl9`#A zkqN9Mb{)g%Ytz$Qb?Y#gpo;5;omy97(A5=&oYDic#KJKBMby@qv0{lWxW2Tu}l(X%nTSZE+%JToF3QHI(DO$ z$Ka@rd{&6_I4rQ=6XG!}SX5cDcQ~D+&rQVbwf`NeA2tyK(05Ro;4$YM)lCaiG>^bt#OC_1{EXtN$#TPi6h8X#OYb z3!?d4)}@HO`l48S=C_{4pf;p>BC7yzYIE~9=I4<2$oZpRtT4al(PQw|?ZFd`N21?r zdDYlci=qlIAZkF=UuYe9u^}59@?ohJFxhf*Q>tHtx-HZlq3S~YDjHAZR-y7JYOIO= zZK43Ai2HDD<@adGFLw>~dt1GYMY;9S!_tp_94uF^L`wnIm;YIptzfO?F9nP1a;^C? zQdflSeS}u7rrllJ`|5Bf3_{?u|Mnn y8~&^cH`Sg^M4lIqQMWm8<#;r5ePin;-^$2?2ZP@}9~%SMSKIF6E8_YGZ9P!z`(#G#3CZWzy@S7v9qzUGIO#@Gm5Zs%Zsuy$Z@eU Ti1V>BOq?h^aRJZ9RVNv{fd diff --git a/examples/spacetaxi/generated/amiga/levels/level08.dat b/examples/spacetaxi/generated/amiga/levels/level08.dat index 3d6d2733ec28d49c9cd5ec24da6b852a2913424b..08cdd0163b1bf720dc360fd2b38a6ed3d46c0418 100644 GIT binary patch delta 829 zcmb7BPiWIn7=N$%*JK@=TPvGAEcj>WK@^oy1Z9_rLXfUa3Jwn$>`;#pdJvW`Ln!mm z>>{Y61a+sqv6Lyh^pHy#L(7~w3`7w1fu(Dc8c>-v&JkgbA~f@c50 zl0dNY+;A2QGUq%sy!WP4GQ1C_Q#QOWrehk-uQC6VPB~^OpDlL1Lx#i2oS!$>6C9W5 z9XOF58A-D}iN3xtdZ=JbpJ=F-O4Vu@rE2GFcf37uO+h%ua508uB!nei5F|m2Gnj8n ztk{;sSR2K~HYW;vn<6}B(U&0IRUu%MkKoQ20oL8D5uC~FVM$scY!HD4gj;-R(6;2k)?ix$GGG^g_~jyj z@;&+dIAu#FDgUvxK*p^C&~OW2$9HUDr+9Y5U9O7fl?&BKf>Z9Pn00jJul9CK;Kn4>#8yl-U8;gRhD2o8UC|fnND2p^B L8w>Zw<_qiqD8mTS diff --git a/examples/spacetaxi/generated/amiga/levels/level09.dat b/examples/spacetaxi/generated/amiga/levels/level09.dat index fc9246e304891ed5249d67941bb6d946a311beb5..64517963b02417574ab3b8291467b407ae1c9ec9 100644 GIT binary patch literal 3389 zcmd^B&1(};5TAX?W|Q@UqE$Q;*^8iv(u03MKr9Fa#nz(`TU{H!11(DNrJLIH1E~mt zo`R55H>h}PduS-)!FY-{K?FSr9z;BN@t}3)ee6e*Xay0$NxC!d_kQ!6nYWv=1B3mC z9J0eFjtva-9X)YGtQ?TWtW?UfYET2Xyd8Exg5ZPDHwgQmlE0z+L-O~a{tWIuHFg>Y z9~!;S$lp+Y5WSLYaW2}#P{dB3L(4^gp%QUi4?pgN?62*$Bq^?YrtNkFJSzm<2+|>x zbvtZEU~0|XpdK+VbesfdEf9n3-=D+1*y)ax+^cekKR(As&)M6^{rJuM=3eSN@0GtY zFUKmE@ozHn^f?Zd*7KeZw!#D8c}b_|)N_Z0DK|WP-pxw{uX<0M3#p=wjJQW+gxB%$ z@jgOAyN==Lwb4<5M!OQ3!LChaM$JNtu1!I)E&SMarqw_}wxZa!{$~>BYF;KI1+|uB zhg`9iOCmDjX+;P{B&K+bn2Ln#2tTGyEN!GJv!hqND?C;c1>5jMjv^BY%e2*_DgZ=e z(f*<(1w~$vgr?@`KRVBo+Dwb*ilSl_mD+P_|(!x`g z=U%XymosRDr13C7B3e+9?1Ia8gNVzGP&BK3kfRw)5+aw~LZ7#MIBxAn*{z4*`%T+BODoea_yWq3RgTjlqtobSDug&)~bAfs` zh~mMNSDL&z^kwLavBtiar)Os8W@o1T#QY+EyvSFU_~+dXqnYTxRctVH^`Z~+OZ?L! zf3(OO>E`QPb<)kzgW&Y2eQ>fkUUYuf%>-;S<195fKu1jq_MKfcjXPtZZ9Pv00ctpqQVT!Kn4>#8yhrbcT9QjF0Z zCzoxrNm|wiOnUr?Wwrx0^RyhgY%3!<6s&vU;t&>+;+jI9yNYAiUBKJj`p% zDI!pp*OM=YTWXL-MFPgOSb2`byIXm8<2CAdjgQ=wFX!FE`+hL6A00Jb6~NTgRF8p3 z-6nB6@!`Y-N4*g_wIEM;m5^y@Ay83b2O0Ps3V#;44;H^87Hi-Mr$ZJ;{)7|UU|ysi zis!`eJ>G;W7*D{V%^`Vk2p%sZhVyG|v>~a}o6@{?e=mFn?0)#CA%Z#S*to^4n(~;N zF?>7`Bd|d==lPx_UYBs3>+y(M4ke`4=m~7e= z<01pZLGCT6eJj!!m7Yu=i>Ep|-;8zk-$OrB*N1m;fKK)yzroR4mB?kAJPj}&$Uhra zxd^s{;v{JCMC)d^_BkGaL@QB!9BzU7gPokURj}t8_3Z!&fYbaF)ip`eS|Z1xZIY-j z6+n_J5Fdw4NX*)wMC=6EIY0p~654PCI-IxxCjmiB-zI|!8ihzjZyN_SjSy5=1NbF& zRj;I>a*^qAa*yB(9N$ozA?#K_3j<^dWCfz_Ag39O){kE**#)3FtXy)~ECed>+Qd+g zhPC;OU>F?;%)1!&e6XdlkC1i24aD*ccLsJq2ozW0XhR4TPm&5=7hL^QJPJEB8Y&AA z3+sCcMg1b9_i#<7QR$q2#&=*8+%q+s{U1ym|&*3Y@+n0Rg&qW z`jvgFbSF1j%I40|=k#y)tU?JxlC?^X*aFhFje)n%uegtQ2DchR{s5ZB=v%|HdMS| zu4Rfjvt+Z99xrCi5-DCWvB=a3Yh&5$BZxmss_MqNMIY8n1nXsT_(6xHy-0m)>tr)Oq%E|*`pxOi#lg1P?kr-6mib9O7fl?&BKf>Z9Prz`(%Cz$hunzzSqBv9qzUG4Qf5a|yBri;A&a;1Od@ RmJwoRU=(9g*w}o59RN|43Dp1q diff --git a/examples/spacetaxi/generated/amiga/levels/level11.dat b/examples/spacetaxi/generated/amiga/levels/level11.dat index 4c3ce870f5b7044a002cf19e29e2ad0a95de9b80..8099ce8ce9459e8ce08a46bf9bc6765d1bc06dd2 100644 GIT binary patch literal 2749 zcmeH{ziSjh6vy8#J2}^boivg{2?tj8)2JeJRkx{m`Lx1hqgLTR3#3;&&ZpN}lV#)et- z;b1VhVlY$Kb9DR9`hCuZT^Y*wszQsc9ponj|@ z6qn$`AXjv4Jv?S;c#(KsJ!P!n^rkA)fpC100F{zlr|Tz8GgNdtlYLI!xw( za|ux;MO-fd$!ML4vAiu{nL!F|nf7HV8@ml%J+n(~e}t&LL?oE}c97=^*zyC(g+Y9Onl90fnI- Am;e9( delta 59 zcmdlh+9Y5U9O7fl?&BKf>Z9Pz00hFU;-U;cfea>gHa1oU0TyNsK^A8LVU}hIK^6uD L0Tzvo%@^1KIVlOV diff --git a/examples/spacetaxi/generated/amiga/levels/level12.dat b/examples/spacetaxi/generated/amiga/levels/level12.dat index 6a5ba48d7fdcee5d1e495fd03ded99b50e7a2167..50b1b33da5a0db562634a96ef2aae124c770bcdf 100644 GIT binary patch delta 814 zcmaKqO-K}B7{{NP{g_=$8kPJc83O4Pc!<~vbTTrlkq&hWYDO2?-9t1%*5#cUKPJ3P zxUGr^LM$ve(MozN5f<~1C9zP1mq6%)AX)~3hz>qI@2*~Z=Y4oS|G(!w%tLv-W%$~4 z?WDp_pLXv26=mS6cA=UA)YSoiC`F^uND^)X^tVGhgs281@Ms%`H{c!o-iAM$LZ{HL z2^|~UPm+gb=$Qm{lY5PPE-NW3r^=FD7k0Q@0g(b-7JD7VhA>Wta6`c0B92l?RdDtJq@N&zUH~i! zrQN2zI}O;KK<6};=(b^S6nEPZ2V`TYLj`vG*_M>6p8rZ;8!&%6hwKkhlE z?|2KQFL+r~uPm6w1#@en9H|w@6F-^h8g54EvwS)6)$nqR05hzRkz0Kn%rRcT!5K!i zN{*F7l^IsNN0OpzULG<08>Dd!y9PMK0ka%1cL11WynusqjB1rx4wz$l!H}bu&^5jp z{nbymt>u=xfuIlMlS*7oC%sY2yKniUZpydZv~ogC>B%I(82l0iK@7)Uv4QpX>}g8r zP<_$O*vh=6-LeWpt?3W$P$82w^z}8i#7F!r%`CrUlz*zQpq_jy`^*;Cm^Nnl;^Hbj zmnR=B-bo7-Z-s)kr58VF#oHPqrF5!!N$FN;2TiZCgNCmVoH|FUt$d7{E9_1AuP+r> Sm`;CZSMG__{9h|YBL4tZZ#uRB delta 63 zcmdlh-X>rc9O7fl?&BKf>Z9Ppz`)JT#Uv%gz|6qFz{JkR#>Oei>MbV9`apq|m05v@ Sm6?HuRfB<*RbylG1$F>P#|e7? diff --git a/examples/spacetaxi/generated/amiga/levels/level13.dat b/examples/spacetaxi/generated/amiga/levels/level13.dat index 459177ba1eaea6f82b75b1c0592d26b34e9a1ee9..3eb8f9da136d0e2b9cbdba81a9d5991030aa7569 100644 GIT binary patch literal 2749 zcmeHH&1w`u5Uv@W9p?w~074k>?9KC%RTC8!gk%LzA_U50{ntU`9yGaln>Sd*H^>7B z9+rIuUqJ9N#H#A*>7GB>gXkq6%v4u>Usu=Hv+TiadZU;<*uQ!I`mMW@{X4t2i`nGi zy#inagfJeB(qsbX0OLK_F~#rjV+r3@@C$yg;pYk-!Min_tl=|!S;E(KupNw-!H&dI zo+tp5w${J420XFTCs zdORQVs`7;F;}xqqd#lr`>J)o)MURZm?QAWo({4?5AAP33AL57F$GpN9SP}3ksu}k2 zvgL!E3Pjp)-JVZ{68upHb?v)p`K3mrFzD|`v*B|c%;@kLrS{9%ek7|K*@jY5A>tu} zo%ZXhW1_De^35oYj&Rqmaj{tJAyphsXj#55OR4&^BUfrR?I0#}ifNh*0em7&`439r z6G_t2)ua*EX(G8zo97Z`3F^nNX_{U#Fa_bfSCS~*>}Pj(O!}(x$e=KxU$uwTBensT#a&!GE2l{SSB9ZE(FyI2MOjGG+ znJv38QBxVl)76cfnoG7{#9(Sbmk^ytX>?f;1wjz0|YR!v$1io2QaK>V6YQo5Mp2h3EMG* zxiCZ=VNheRt6@++#GuFEwueFc5W_r%ZATb(9AVhTaIS^n!VyL}Mzaz|iz5vC7_PZ6 s>^=nIA7PX}1T`8=v4F^pb{E(=co`DFVDdp$(a9HB*+%iehXEr402dank^lez delta 71 zcmdlh+9zNa9O7fl?&BKf>Z9Pt00cseLShVTKn4>#8yg1;3$p_wH#0YrFtf0L2=fAA Y9_AeiT+9k0EX<1)M3|X3HeX-|0E5#Cp#T5? diff --git a/examples/spacetaxi/generated/amiga/levels/level15.dat b/examples/spacetaxi/generated/amiga/levels/level15.dat index f0912390c12055cd84f9e54c7237f4abdf5e3598..7607e7d5f1a4666d13c740c55331cc4e762fd932 100644 GIT binary patch delta 403 zcmeywzgN^cIK;<90R&uqT%ALL{5@TQ!CVFiU}9%uV`NNW&|_pU5oQo&UtuTu!y;DM(w&8wR^MbR_slyTeVU_LE2#vyMm&_ zxf!(r&k_*?EFj}DSv}Tf z-UO-5WSyJAx;OLY+>D!0wS6};diz+{t^^vl5^UT`wz(_V_O84&cLmJ2b3o(Hu_ef{ fitw{?@(M0u-}^v2tL}mJDS>YZXkLc}0mU=`v~HIG delta 43 vcmdlh`iWmDIK;=8-N!Y|)kndf0SJ^BM1>icfea>gHa13PQD*6l%@^1K#AgSt diff --git a/examples/spacetaxi/generated/amiga/levels/level16.dat b/examples/spacetaxi/generated/amiga/levels/level16.dat index e6471a50d2b3723e2d5ebd91922044cb2528bef6..11f416df153d6ea08842786a48d1896c41c4dc87 100644 GIT binary patch literal 3197 zcmds3PiP!v6o23B&g{$v>O@Tygvk(kh=?~21&fT54Mf(9=2U6iB+WKOJq$!r0<$xj z?GzPKyd))(#jsFt3M;*OSdrpoYYsVi@t|jsbPHlFJ*@A0-_Ex)lS;Mr)W`C^_kQp9 z{(avj?0fUo*KCrvs&i-0zWMHq@BqdDMJZ`op>Q5f0lYl{6QDs6U;!Ej@cu3M3I05Q zzkXmAd*gt;bc^#JM`}wsS-gO996cu{dz4;yx_Ep4Zr{D#FCRe^_mbxwFaJxFrxlzG%dAZTTxrCMw6Q6EA#iNI)la@-lS>E!p z4A{$@Tqi9y6fe3^Z#J8Cl+Q)6&*}n@t~5AlC?6(h#@lOKjfU2hc*`x!8k>kmrNRX~Ue&S~PVkik< zJuZox<%t*(gi2^ejwkWFG32STL7pId5InrDhsQZe6L10`J6kZ5Mav)$`J9)MtVgbWb^wUzLd&H3`TK)ylZ4|hP)>vYH=ty zfm%`&MNg!pj*X3?!vT?mXzp91D5K~zr9f;hv=Roml5!(}8GswW#-ViQd(>znf;f+c znUu{@eh4aa6bRIcqlY%DK=HIQd8dyWZA1{~QRYCHP9hr%mtnsMPC48kXQpUkn&t5KL{(JP*@4n5Y$S zE`JKP$$u%~M=1kN=9JBV(@At_^L!K&bg;RB!lrbGPjw245vB#i;62PK>O`bO**Hf^ z99>~aAqSKz0pOQqIAMw=P2G{EG)xG@N{G~IwfIc58aJYg_Nz{CwsyRuZSSyfwBvOwbxzx|)Q-nbPwlL0c&=-m zAKhrBUD8%=>b&T^YNgTj+}`Og?siZ5J*EF!tnj8`>za%0Pxx`so^Y;O{x_CiH5!8f zz-H~c=UBTSFih%OJ$6{A-fZ*~JZ?8=>LbhVSbk4G{JGXXBc}enJwPK`j3HWz&SSkU z^P^>3bFMG8K5?%vwIVkTc-t3Ro*T2)RTl@Tt~9#W+&PB$%dD?;?>FpcotWWF7&)5s zJCi=*1x`ERzj%@1#67iN{`y@rz$8IK;=8-N!Y|)kh(Kfq{XWky%ufg$>AJVrOGx<`89Z78hlCpdiY^ys`NL FI{*&@33LDe diff --git a/examples/spacetaxi/generated/amiga/levels/level17.dat b/examples/spacetaxi/generated/amiga/levels/level17.dat index 4aa266c2696025cf131931f6561b0fa4fe95cce8..457defe79466772cf3d5653e69b056e82e7aa772 100644 GIT binary patch literal 3517 zcmeHHL1-Lh6n-<=$?lphNkB9~DRUK3JV+@t*cIInAylb771U`6YNKEzcPm-qF zG}#@om$smzu(Xg_Oh`*FHLNT#2pcbJyLeC(sUQ-e2EA3;^}T=QpP9^VRu6iSK7Ri9 z-uK@3y|;fh`{I$&CsnlP4jsuH9?l%j9C{{0IY1Af$?@*)MEn9=2N)iNK~M;sf(xI( zr49H9KC8nA>+m=XUxr85;S2cbL-=_^+AHn5EbU*H9+39crTrW5Jv8d@?K>ofF5C@kFD$w;!h4Ul5Hw=+uA80kK*RNKdZ}>P ztez1u`C%QAz}~@d^3>!cM?<@-Iw5{lX|Xt33#jnlnD7ro`}khpfA}sknkV1!ojdmh zRX?J@mlhu&fObY=g?4>m%0>8mv{z(-@mV;_vY9_k%L4d7FS80{b3cpLMhydcL(7}}3Y?O~p2S|d>ul&X!LdG7! zKi!^xAPs-?L9<3T#;Ay#@DcpE7Jdwdo`s>O;m>|(enmINh@@g|`Da@A07HDeCP1?S zc;hHU!ox%R=i2a*fQQKZxaltZvr#^0b~U%dhlCIhOJK@vKFW7@<0}7T{<${%UVv$k z4pyXQ1#gT2k??SL24^lr_^}kkdZF)m= z7k<7ye~W+f8shu6`}YSl&1qT>*R*21yv6J}bLqx!%)V$i-S)>u`6jd9Hp<^HJ7<({ zGn*NXWWQ%@{vg+GYB+e{XzA`#-U1T(`HRyh@wM>hDb}5snLRysCUJywbf$7y$IMNrkEOHP_&C5U{4Pt997}x8M#{HX zDXTt|cI4b~^^!UIig{&hV4|KIyK?;eF|EADmZ-xyk1Z>oGR(ib!?yRJA7DQ+dyS3G znvQH=W4JLUUz=3HkFmz8UE708H-5nhU%DDub#J;DSD#4Z4D7zfM8Q!}TvUn)tX*ZB QwYyGy`6|=s?f#$k7aEx~yZ`_I delta 71 zcmdlh-6vod9O7fl?&BKf>Z1_Iz`(#Q%p@tvzzSqBv9qypa0{|D2#B#XGYK#Yi}JHv ZP!M2NkQZZ7kPu@z%qhm4v9b9AI{>AF3*rC( diff --git a/examples/spacetaxi/generated/amiga/levels/level18.dat b/examples/spacetaxi/generated/amiga/levels/level18.dat index 2303e0d174f825305fce854af5d2c2c6fc04d53f..b682e46e550fc6503224c59e6f721d3bafc90ac9 100644 GIT binary patch literal 2749 zcmeHHJ8l#~5bfFN*=AT+nH$U@2ylXA39n=cA{3$#+x~YoDq+&v)4xLY2e@9_!+6~7b_+{P!h70o-)*Z_N Ffg6$c9^L=| delta 43 vcmdlh`iWmDIK;=8-N!Y|)kh(S0SF`+1Oylufea>gHa1397M6gG%@^1K!`lbN diff --git a/examples/spacetaxi/generated/amiga/levels/level19.dat b/examples/spacetaxi/generated/amiga/levels/level19.dat index 76c0dd3b2aa448ae774063f34f1f05a3fa888a12..7a9076126fb30a6749800a89cb49b90ef359bf92 100644 GIT binary patch delta 764 zcmcgoPiWIn9R9tmZ8C9KhSSPGa_vD7@g|IbZbaxoSV7pubOcFuP~xGUelP24HviDE zo#rJVfk_kWu%nkb;-U7EdI>5X1ZB*lFpA!GDE?lDXK(x9@%w%6`}n@cTgk^$?tHm) zT^*sU&#LoxZ3-~v=lnC`+1-0#4RF1&{y9XRO0 zD!lGNy$eG&XELoPtp7ts7~0oT*_{MKKriOKid8%p#cwcMF+gD;k!9qg`3-%~i4W07 zCuIJ*(?5{XPX-Bvh3I&OrU0X}w&gx_1(7m2Wx2Lef8wpKJ$?54#q7}#TSF|e#t;Y0 z2;{iV3pR88I#14~*$`-H?)^r8YZQKH=&d_WdcK8Dvam6cbl#2!XVh^mQSy>y1Q}YR zz7@NC=i(!Z(*u)kY_n12jmJE!dRE9iTfL%LhG_!S;E%+Zlu zE-&5V+lvMJi?_JFv{^CYeQXL#xJjO!cS!vE5e_D37Vszf`&g>+Q1W+?o=oH`7mCej zPsRq9ntQ*f;^Rq%rC!zoS-q-J2f26gpAf1PK2ltg0)@mj_S54~Zne=6-*;Mga9Twt GJ%0h@*~1_J delta 59 zcmdlh+9Y5U9O7fl?&BKf>Z1_Mz`(&S#3UrZz|6qFz{JkR#>&9K!^|SYsw^VJa)MWc OC0mArQ)6TE1$F=*sR>8` diff --git a/examples/spacetaxi/generated/amiga/levels/level20.dat b/examples/spacetaxi/generated/amiga/levels/level20.dat index 0c7021577c93f23a6cde8696bc6d8b5e34f753d5..396008f50e492bb856f9d155333d16cc0d656fc6 100644 GIT binary patch delta 438 zcmb`9ze@sP9LArwUQdUHBAtS`AVPwmp{>I*NQl&{G^faeAZ&aIg?^kax>Vl?1edh^ z2FZ3?jw5pOFKCpe!^s+gdaobUAJ79|-sgGY^SKmm?X?St^($sHb4M^d62i($ z*N!Vyxj(9k=u0jyIyLFC-V+i?3i(P(7j9B?hh1G*nO`Cp1jgnEJ{ArGQ+V;QSnRu0 NH~Dnc;GuswZ1_Cz`(#JBqXiIzy@S7v9qypurRYIOA50va5J++bMi2I Zi!!rZkli_ko%ZIKS{gMts~Necx* zQu?MZ!4X87K}8<}t4P^{-NVQ~Ac%sXNa-F#5Otm;G7&^7cyg2T>-)ZQ!@c#%Z#(eP zW%Y;>*~QZ*^($I7P85hukR@qsY!Q|K&ZQs)0zGg8s#&mB;2>y^V73QMc)Sevdhngi zEHh;V-Z3c)Z+kx_{^}B8_u1h<#-wN+2e=8aWWn=jQ9vmbN3>eZC-O$LC>WiOG^9=f z41>9yodQWalAYeJup*M&}wI&UQ@RpL)8_+sJ9$);M{c+d3a;JP-%u zubW7(`pWdc=u~hhtyNGps-0DyW`P(-b_o~3mA93!#a@QmpI$)J^S0U*^lmpw9yZF# zfs89EJfVPM5t$SkA*&uPLq3)+AsQ9O7fl?&BKf>Z1_K00ctJ5)w>IKn4>#8yhPp3v&jO0E;l6FmthtFbjty L3(M(^%@^1KFvV5h(!2N9HG zFmqv$J_O{NodMB#3~eq9*@qbBG3-0TF#8b0HHLRC43`fvsxjJ~VRSeGqFsRWZ1_Gz`(#J#3-l0zy@S7v9qzUGq5l-Gl()XbFnZ73y88b Vi?cBARupB{P-J1&*w}o59ROcq3IzZF diff --git a/examples/spacetaxi/generated/amiga/levels/level23.dat b/examples/spacetaxi/generated/amiga/levels/level23.dat index ccf8623dfdd5af9218af3fcd4073bb39333ae942..23b1bec9e7a2f3fa145528b102e258ca124dcc81 100644 GIT binary patch delta 764 zcmbtSF^dyH6rS0H?B-%fkf;$QOshl?v9J(X3+0LwVl3>O`i zhq`r^5YD-z;t;rMt`d_{5*5#*;!RXw@ir>nMMbOKYU65Yh$KQ!WtrMqT#ILtnIt`? z$G9lb3>{kFM}IA2$0C^k9-3&@fWyHwGG}Q3Dk{ zIO>Dnhk+69bN3Yb4I=4ZD3=tu;0rig&tYE6ulvvHq733ueT4@JuuH)L*kwuV3hL~o Yw6gHa2DkK^6^uPPS@(5w_}$%@^1K D{q_k1 diff --git a/examples/spacetaxi/generated/amiga/levels/level24.dat b/examples/spacetaxi/generated/amiga/levels/level24.dat index a499a94d1b4acd5d005ff8ff570cc2856e66aa1e..13dca31b4b3937c07b17df8bd08a25a41909d135 100644 GIT binary patch delta 764 zcmb`Fzf0sm6vy9s>qLx2PLQ>8{0m&cT5S}I^#>@YR>sxlEQEl9&AU`7b1H&byJnhmQL_ifmb%+mSS*|@c3L_!Nny3seT(_dB=0-#Gs%3n#E`qZQkvjWiwk^Z zZide*kHJ`c_hEYIj16LrkofoUPx* zgx050_$>}_`0MAYxXV+L7e~Ex+$+`?tuw(5mqrb7cTdT^IC`Mt2eH8T~(+wqDgNx>7gcF)ubb>_S4SGpJJkSVAZazKj4l@TsP$%Yo^`1 zXC?OE6g#J=`xJF0CqFYEoUQ;raocS@WaHyp<6pewG;($Y(xTXN{4MltC*F7bGWr|N mt}RUTHbp;rZ1_Bz`(#Q#3U}xzy@S7v9qypaIvyBaB%@KKM;$sHi)va Tt`_A2Vi6$bXI;Iq`2srtvla~d diff --git a/examples/spacetaxi/generated/amiga/levels/title.dat b/examples/spacetaxi/generated/amiga/levels/title.dat index 9887b563cea400c708312b014cc0a86ce2c0f27a..83ca545328d4c48b3b960ce6b48517eb6f470f02 100644 GIT binary patch literal 2749 zcmb`JPiP!f9LL|x?99$gw%NLWO4c0u@J~?Cq9|o8D5;uY>yT{sPl~1LM2f`Y2ssGu zTMrHOknKfK%d)6FbxJoB=_TM^LMa+z-9teHQBPJ64T}enf~>#adv7*xveo9`FPk^- z{od#I{e9n?B&1UHo~1lWwNt0OnX*_=B0^7EmT7)XJ|^Ua0x1v+(%J#-a8)Z_*DP&n zOMC1(Y~I)+A8nAsWMPXuvq7fFY?VB`Atmnn@ZZ`~EWXaIorR!$faO_C4KsX2-{72> z;|SkSAX!m?Rg%MMDZzy}id8(XaF~)>Q${kImXc?I-Et^?0bHGdtg;j2t zyvGE&+=NMy0Xstx8bcWg>O0z~fV`iU_HF3R{=TFXY(izX@GI=BCq{jss=Vp(U zXXuNyYIS~&=F(I*lg{`dt_WNyucw@>t(e~NHyy>B(LKW*kHl9>CgPOTJ*TH5Wqghh zmHFA~$)jMJYI_X_{$yA-1koYz?}v^lzQ93uFf@%omhU*2(FuHD!V7RE133rwP>C1w zQRf6t3puU_9M|O41V`kc&xoF|fdr_cIVq0$9e=3D0}mmmQ6^dne+Wop;xw=g#{ttZ zjxUhJZ3*HP!1pKz-ta!8Bbe^yhMqXbCAlha!!5X%NLlLbw2u2ri5UCWkbt7+$dvLR z(YD$bTJj50hsaf&OKFN0Orb)83b%{l6%iJlS8Wxa5B1`mMpm*jyk9~_Hsc9wjz0(S z?8u5M0;g<1D(+7CC6gOK!z{Wk{v`N2A!B&qQ9^mfq!nf5E(L9nXqS));`{zZHzds} zYvVg~ z43}uGl}x%x$2K%;;3k}b>*xbwI|I{6S_24K781WxWqrf@fpyLVea-Kbg8rk$-!<30 z941y4yN|F->v`5secJR`Xu&bPFWGN57JI+?oz38L)4iJbuy@yAtui>c3kS#J{S}%Q zz6jt_Dy&&!H)|}+G#6OdTUiR($~*MQY3P>A5%R#*KGn7t7<@VltZCnBCU0$5Sf|1s zPlA>08Ut_KOS8*+-l#Pe&bczFPdcMM+W;3?(=kA~x%R{UUrWx0Z?EJs#& z*Q^GmDx(OFG+WKCv^rPHY+rUF!O+f}{J4TS1N^uM&K7fDGl%se1HrkL3_9F)`O3M8 z%v5G!FyK(eYyR@Tas(hM=X+}z{QL3{ogrN1X&)vdxE@q-niQ<#>dn`vVd8T;aF%Zk z3*AdA*gHBrSn-k@XBjMrDPfV6NwJ-5G@#@faZN(pRX!{$9d91P-3@f}0#Y=EGR}*n z+_g2n-}!twGoeZGxP=6c?FCyMiarfbpazn;)m@WqcrS9QO!PN*xpeXzTU*E9)^vJJ& zs+K*WwSrZJZRNI@)0rHzc;Hsma~5s8fx|-WWEKhOMaA{9OmsiU3!Hn6@wS8HLEK;J z`9SkE%uVa!Ni+UY{)E>xq-*5%;@~q{j@fvizSo=cDWi^S+cDuc(Fp}&Y-Obx(!_j=4 ziXt(q^kIM6)=euqrz7ME#orTUJpRHP^R_k^_XiVL!ht_`m<$y4*|gqY3hUYxSu{wSw$;BJ!zZz>o!X(;r?W6 z+3I3dbRg)V%PZ>Pu)n%qE|Pp21J8Kg{B8*mJC$jrzXCb@Crdiyfy_!_(&M}w8XS*+ zdC>##YNY6mFAz*zS`OxL;zm)d#m}5vsBxU2RxgSeUnxz;`-}Iw!D%1iczph>J*k`Y yLsjW|b=cdcv?;t9rA(={J`Gly;V@+}Ek;p%=aLztb;<%fvZegdJ8s}VHt++Ni0Fd= delta 43 vcmdlh`iWmDIK;=8-N!Y|)kndR0SM%n`S=(Zfea>gHa12sQSRW4%@^1K!$$|$ diff --git a/examples/spacetaxi/generated/atarist/levels/level02.dat b/examples/spacetaxi/generated/atarist/levels/level02.dat index dea590378b780e5ff61a0e7c7938644c1f6d5b50..dbebde54cfb5666c5d7580175e47678faa75eb5d 100644 GIT binary patch literal 2749 zcmeH}O>PrG5QWPV_Y9*pi`-x&q?~{Y@M9DSlnq%DDxQ`cK@a7=z}Y9XF18v;rvj^lJFNqsetnbx>I81n)nZgrcSH+LLz z(OvIFZylVmpfhP{@Zw-orQVlyIQTD1}tEo~F)Cl3+oZNuNv9M0TpDJ(C^k z3fU#Hz;L3hKO=~yDkJ~Nai0G&{*C7`YmC&Ab=orQ`kOw*gz_GjN1s61ajS z6FEobmr8@HJxI=QIOJqKRZbj(&J{(09cmP@ICzxiy28}}g;Q9+Kp{PP>^K2aUo^{% znX8$Dq`&AWiTm=@2e{afliw+Wef2m@oGT%YWbDVj%aw{TXeo;`uEg6lo6TzeB&E3W xSd4P|?M$cq0=SC1yq8ot?`^evKakB|KK>~cg{|`c2*;xC2JeEq8@P=d_yZFt4gUZD delta 51 zcmdlh`ioyHIK;=8-N!Y|)kndJ0SNfG1O*uwfea>gHa2EP7FKRHVP0cVc9shpn=h~f E0QX=B$p8QV diff --git a/examples/spacetaxi/generated/atarist/levels/level03.dat b/examples/spacetaxi/generated/atarist/levels/level03.dat index 9b5a7033826b7eb8a3f31643ba59989ee02327b3..41ae91bbb408052b501566bdb8013a845b3f593c 100644 GIT binary patch delta 764 zcmcgoKWGzC9R9s$lDmqP=zl%QLO;O{QDI`)J2et*7??|ZzJe0bWf+$>*K z5>#&7QYy0xSLf#zk`cz>QZARxzJdVYS{_aTPu6Fkro(g(X5da2uJqtN{OH17A6DUY z9~wOvvFkch`(uay-Wb~5lxPJUQlX*_aP*B(E4`fToCgzXU)S#yL#rg>7> z5CxOjZbL*T;;awUw03@@yOEmjYo+$Q6;HL%iWXl?M%}j)-f?9@jpRHxG%rJKFfc+} zY@NMNb|p`zjj13}ssNxwEXiv_<*0 z*jy@@U!0}Q<@Kr-?qVyE`0J$EdWSTBKfvK6)dK!RcNfcb;d6Hz>B%&F6$xW9>Z4=B zbFH0U6!CHFVJPQSPf{+bltJok{73i-nWv0RlqVC}!9jfJOYIJ7$@d)Z9Py00crzLIMm-Kn4>#8yhPF2M;rg5Ua9?5X%W(5teKj L4o;1Y%@^1K8ixre diff --git a/examples/spacetaxi/generated/atarist/levels/level04.dat b/examples/spacetaxi/generated/atarist/levels/level04.dat index 74993ec09de70db8622005e7f4914b7e75932f3b..55b6b37d2503e565229c90151c0e83f1978d2bb1 100644 GIT binary patch literal 2749 zcmeHHON$dh5U!rslXMmY!Ee0#W;Myfk7_8si(u1A#t?i8+eg}u7 zou*xN9cBQtvv3Adc3+0=5nMFzNA!LOZ2;l@6_^?L1mC{E2Lr!EdKG>eabEO?;+%mS zaQGQ^nSbFU{5H^o{&(0kq9^)a#irT2*TDV*0(o;B29f>ST-ULfgx)*<&dU+;>Ch*M z=c`3@hR+bob2Z&;93u0nwXX5PK*L@kkNSI#7@vz*rv9vM{@6F>quwfcx;_w3D6DLp zy|ubacv??fzyHbOO}?ZLj{{N<29M1qtVEEt3i-~?5U!14s8NCyWMUrq+&?IfRy1|&5rR%h~TL)o>B>b zr>P`7SJO@E2+pV)FAOy774lA7&|KV~cS9(6g;a5U0QNu-XqW|$64HSMfRw=dSjGdD z+I|=FsQe{7*C~CIY^i%(FK!U_n|a`L7dBJyxFZLr)$E=wF851FW6g9NWKnFCr|7OF z?!H*XSU$yh5&fl7636REh#}ZB5Ex WvLM-0MO(0EVM5A@eKLWSPv9@&{us;v delta 79 zcmdlhI!(YWIK;=8-N!Y|)kndF0SNfG1VtFwfD9&fHa0G1E@o+IQC1F4W|jheL6&MM g7Us)}94y@I{LI$UeC)Rwgjo#~gjfwWHeX-|0Nc?E_5c6? diff --git a/examples/spacetaxi/generated/atarist/levels/level05.dat b/examples/spacetaxi/generated/atarist/levels/level05.dat index 8467bf0bb7a8d5024ef51fcdfb7feb0145747eed..02e66bed022342e2e1bbdd8a862150d75e1ec75d 100644 GIT binary patch literal 2749 zcmeHH&ubGw6rNo=i9-$|Z6G3)?H}OHgF+9f&|VB)S`b3jRUszUlLVCD?5$9-n{(4h zD?Nx468{0kL-Ejnh{U%36^5QXc=EkBvzys&e|QkR^kdoC_r33Z@6GHc^9!Xb9^(4! z%+2|QnbHl4JjMhAxY=!q~vSe+c&;0@CblJ=tmZJO4npyZ3s ze~S-Nfca9~-AasVwK5wAjYeax0?Y`{;IaO+UMJT6T~v^Xvpj_?AVqR6jsvq3oJW5k zu-XYB5~1a16_AADsMOK{g(<;cI82|SW2hqy7DY( zJk5YQdNAeqZ1xce$r*H7X5FoQAWH{HeXUu**6cU7J4pxX6Lr*`<{Rmv`Sd17<#>M9 zv^1hVKdjIh>Zf&3Xgo1(&V_W806q0Z|FkaA2mNRaqr+!&F4UQF;F9#y7WT-fLpC`d zZCCqgqo~&*KRqS&H9BntZV{EIA>z<2{<+&V>vyj$6Ht`-fcimBT zjE3+5$_a%G%TlqhC6ybtr5X>{q?!!ZWiS7v?;JS3nuEaa3E3B49lzI<{T!@A5Uk7o zEfg>i3|q3-l6`(KRj5sLy5KSJUIehNtf+~rKU%Uwgm*3Qf!w;-Ys*6IQ6$+qPw^7O zpeU8G5bO%Id`EqcL~307>;d5qg~Y3j7ft_VO~%)-E#XqHm#ZpRu6dV>esd#O-3+`- g#djM)cQa7jn=JZ_O;0m6<^9Y!=CzzHfA<7_11l4gYybcN delta 75 zcmdlhI!VAOIK;=8-N!Y|)kndVfq{WjMo3VUfepxFVrOIHy4+N`%MLhVCq;ZUB3kwOB;FzSa;AIY8 zJxY7n!u}KDB{vgI^YKT796edzo0*-RB-^G!@zft-=FNM*`Mrn zz0YN3^VS!q$ZOV{^*MU4sK6$rr}{UA)obetA1`}tur<^(yFMLC7E=AfZ?TEH+&9*KD1j7kG*@`QJhCpAP=YDiu*K5-v-Myf1I4;ceN1RLLnZC5mC8m(2*s!2w6}QUZ7jc?mD& zlq=4QnN_xUlO9o#dQU}pF+o|1hbLK|@E@r+%`>MaZ(>SWk1F_>UZT)~f^Av019QNy zLBO$dIa@~|EP-D0nkb{Ci4{Cxal-1fCFSf|}m6dwy(v{_4-!BIr z{WtFA)!=Xq3LP-mq1quE4cL$L2$Z{QY`3 delta 51 zcmew>`ioyHIK;=8-N!Y|)kndN0SJWHM1>icfea>gHa2D!0cK@c0cH+a7G{o(%@^1K D0^kT$ diff --git a/examples/spacetaxi/generated/atarist/levels/level07.dat b/examples/spacetaxi/generated/atarist/levels/level07.dat index 805246c776f4e9d3139e81976dc2a8730425a10d..0eb4025aa5dd7be0ba68be67b8dbbf7b0dd6fd6d 100644 GIT binary patch literal 3005 zcmd^9O=whC6h80Gym_Xv31SctDT9SV7TvUxRgi3wg^oYe&_xz9q-~N~f4CA+0{3MS z$<7E;ILKcM=I=GWAIuL>jjjc>#=)#qB=|Tc7nvFl_ocr^BGRE0^k~#1EeCIpg zy>FPaqm`p&veDtn@R`$Jjh^MK46ql_{IFCCf@Qc1@by9HgAx@7;M@iHauq&~C-Xs@LNUOncp{@Grdn$%Fd@?ZhTKHLcA0$w`jP!&b}Xd_lw7)NRAixYPxj zGsBloPN0-p#vG1NJguLGY$so80WqP` zQZyInv~8t5Zs%>k3ukw^6?zMv>H=N4vz=Pn>C?k*2pKM#`_Tt`kDqWhH8nBpl9`#A zkqN9Mb{)g%Ytz$Qb?Y#gpo;5;omy97(A5=&oYDic#KJKBMby@qv0{lWxW2Tu}l(X%nTSZE+%JToF3QHI(DO$ z$Ka@rd{&6_I4rQ=6XG!}SX5cDcQ~D+&rQVbwf`NeA2tyK(05Ro;4$YM)lCaiG>^bt#OC_1{EXtN$#TPi6h8X#OYb z3!?d4)}@HO`l48S=C_{4pf;p>BC7yzYIE~9=I4<2$oZpRtT4al(PQw|?ZFd`N21?r zdDYlci=qlIAZkF=UuYe9u^}59@?ohJFxhf*Q>tHtx-HZlq3S~YDjHAZR-y7JYOIO= zZK43Ai2HDD<@adGFLw>~dt1GYMY;9S!_tp_94uF^L`wnIm;YIptzfO?F9nP1a;^C? zQdflSeS}u7rrllJ`|5Bf3_{?u|Mnn y8~&^cH`Sg^M4lIqQMWm8<#;r5ePin;-^$2?2ZP@}9~%SMSKIF6E8_YGZ9P!z`(#G#3CZWzy@S7v9qzUGIO#@Gm5Zs%Zsuy$Z@eU Ti1V>BOq?h^aRJZ9RVNv{fd diff --git a/examples/spacetaxi/generated/atarist/levels/level08.dat b/examples/spacetaxi/generated/atarist/levels/level08.dat index 3d6d2733ec28d49c9cd5ec24da6b852a2913424b..08cdd0163b1bf720dc360fd2b38a6ed3d46c0418 100644 GIT binary patch delta 829 zcmb7BPiWIn7=N$%*JK@=TPvGAEcj>WK@^oy1Z9_rLXfUa3Jwn$>`;#pdJvW`Ln!mm z>>{Y61a+sqv6Lyh^pHy#L(7~w3`7w1fu(Dc8c>-v&JkgbA~f@c50 zl0dNY+;A2QGUq%sy!WP4GQ1C_Q#QOWrehk-uQC6VPB~^OpDlL1Lx#i2oS!$>6C9W5 z9XOF58A-D}iN3xtdZ=JbpJ=F-O4Vu@rE2GFcf37uO+h%ua508uB!nei5F|m2Gnj8n ztk{;sSR2K~HYW;vn<6}B(U&0IRUu%MkKoQ20oL8D5uC~FVM$scY!HD4gj;-R(6;2k)?ix$GGG^g_~jyj z@;&+dIAu#FDgUvxK*p^C&~OW2$9HUDr+9Y5U9O7fl?&BKf>Z9Pn00jJul9CK;Kn4>#8yl-U8;gRhD2o8UC|fnND2p^B L8w>Zw<_qiqD8mTS diff --git a/examples/spacetaxi/generated/atarist/levels/level09.dat b/examples/spacetaxi/generated/atarist/levels/level09.dat index fc9246e304891ed5249d67941bb6d946a311beb5..64517963b02417574ab3b8291467b407ae1c9ec9 100644 GIT binary patch literal 3389 zcmd^B&1(};5TAX?W|Q@UqE$Q;*^8iv(u03MKr9Fa#nz(`TU{H!11(DNrJLIH1E~mt zo`R55H>h}PduS-)!FY-{K?FSr9z;BN@t}3)ee6e*Xay0$NxC!d_kQ!6nYWv=1B3mC z9J0eFjtva-9X)YGtQ?TWtW?UfYET2Xyd8Exg5ZPDHwgQmlE0z+L-O~a{tWIuHFg>Y z9~!;S$lp+Y5WSLYaW2}#P{dB3L(4^gp%QUi4?pgN?62*$Bq^?YrtNkFJSzm<2+|>x zbvtZEU~0|XpdK+VbesfdEf9n3-=D+1*y)ax+^cekKR(As&)M6^{rJuM=3eSN@0GtY zFUKmE@ozHn^f?Zd*7KeZw!#D8c}b_|)N_Z0DK|WP-pxw{uX<0M3#p=wjJQW+gxB%$ z@jgOAyN==Lwb4<5M!OQ3!LChaM$JNtu1!I)E&SMarqw_}wxZa!{$~>BYF;KI1+|uB zhg`9iOCmDjX+;P{B&K+bn2Ln#2tTGyEN!GJv!hqND?C;c1>5jMjv^BY%e2*_DgZ=e z(f*<(1w~$vgr?@`KRVBo+Dwb*ilSl_mD+P_|(!x`g z=U%XymosRDr13C7B3e+9?1Ia8gNVzGP&BK3kfRw)5+aw~LZ7#MIBxAn*{z4*`%T+BODoea_yWq3RgTjlqtobSDug&)~bAfs` zh~mMNSDL&z^kwLavBtiar)Os8W@o1T#QY+EyvSFU_~+dXqnYTxRctVH^`Z~+OZ?L! zf3(OO>E`QPb<)kzgW&Y2eQ>fkUUYuf%>-;S<195fKu1jq_MKfcjXPtZZ9Pv00ctpqQVT!Kn4>#8yhrbcT9QjF0Z zCzoxrNm|wiOnUr?Wwrx0^RyhgY%3!<6s&vU;t&>+;+jI9yNYAiUBKJj`p% zDI!pp*OM=YTWXL-MFPgOSb2`byIXm8<2CAdjgQ=wFX!FE`+hL6A00Jb6~NTgRF8p3 z-6nB6@!`Y-N4*g_wIEM;m5^y@Ay83b2O0Ps3V#;44;H^87Hi-Mr$ZJ;{)7|UU|ysi zis!`eJ>G;W7*D{V%^`Vk2p%sZhVyG|v>~a}o6@{?e=mFn?0)#CA%Z#S*to^4n(~;N zF?>7`Bd|d==lPx_UYBs3>+y(M4ke`4=m~7e= z<01pZLGCT6eJj!!m7Yu=i>Ep|-;8zk-$OrB*N1m;fKK)yzroR4mB?kAJPj}&$Uhra zxd^s{;v{JCMC)d^_BkGaL@QB!9BzU7gPokURj}t8_3Z!&fYbaF)ip`eS|Z1xZIY-j z6+n_J5Fdw4NX*)wMC=6EIY0p~654PCI-IxxCjmiB-zI|!8ihzjZyN_SjSy5=1NbF& zRj;I>a*^qAa*yB(9N$ozA?#K_3j<^dWCfz_Ag39O){kE**#)3FtXy)~ECed>+Qd+g zhPC;OU>F?;%)1!&e6XdlkC1i24aD*ccLsJq2ozW0XhR4TPm&5=7hL^QJPJEB8Y&AA z3+sCcMg1b9_i#<7QR$q2#&=*8+%q+s{U1ym|&*3Y@+n0Rg&qW z`jvgFbSF1j%I40|=k#y)tU?JxlC?^X*aFhFje)n%uegtQ2DchR{s5ZB=v%|HdMS| zu4Rfjvt+Z99xrCi5-DCWvB=a3Yh&5$BZxmss_MqNMIY8n1nXsT_(6xHy-0m)>tr)Oq%E|*`pxOi#lg1P?kr-6mib9O7fl?&BKf>Z9Prz`(%Cz$hunzzSqBv9qzUG4Qf5a|yBri;A&a;1Od@ RmJwoRU=(9g*w}o59RN|43Dp1q diff --git a/examples/spacetaxi/generated/atarist/levels/level11.dat b/examples/spacetaxi/generated/atarist/levels/level11.dat index 4c3ce870f5b7044a002cf19e29e2ad0a95de9b80..8099ce8ce9459e8ce08a46bf9bc6765d1bc06dd2 100644 GIT binary patch literal 2749 zcmeH{ziSjh6vy8#J2}^boivg{2?tj8)2JeJRkx{m`Lx1hqgLTR3#3;&&ZpN}lV#)et- z;b1VhVlY$Kb9DR9`hCuZT^Y*wszQsc9ponj|@ z6qn$`AXjv4Jv?S;c#(KsJ!P!n^rkA)fpC100F{zlr|Tz8GgNdtlYLI!xw( za|ux;MO-fd$!ML4vAiu{nL!F|nf7HV8@ml%J+n(~e}t&LL?oE}c97=^*zyC(g+Y9Onl90fnI- Am;e9( delta 59 zcmdlh+9Y5U9O7fl?&BKf>Z9Pz00hFU;-U;cfea>gHa1oU0TyNsK^A8LVU}hIK^6uD L0Tzvo%@^1KIVlOV diff --git a/examples/spacetaxi/generated/atarist/levels/level12.dat b/examples/spacetaxi/generated/atarist/levels/level12.dat index 6a5ba48d7fdcee5d1e495fd03ded99b50e7a2167..50b1b33da5a0db562634a96ef2aae124c770bcdf 100644 GIT binary patch delta 814 zcmaKqO-K}B7{{NP{g_=$8kPJc83O4Pc!<~vbTTrlkq&hWYDO2?-9t1%*5#cUKPJ3P zxUGr^LM$ve(MozN5f<~1C9zP1mq6%)AX)~3hz>qI@2*~Z=Y4oS|G(!w%tLv-W%$~4 z?WDp_pLXv26=mS6cA=UA)YSoiC`F^uND^)X^tVGhgs281@Ms%`H{c!o-iAM$LZ{HL z2^|~UPm+gb=$Qm{lY5PPE-NW3r^=FD7k0Q@0g(b-7JD7VhA>Wta6`c0B92l?RdDtJq@N&zUH~i! zrQN2zI}O;KK<6};=(b^S6nEPZ2V`TYLj`vG*_M>6p8rZ;8!&%6hwKkhlE z?|2KQFL+r~uPm6w1#@en9H|w@6F-^h8g54EvwS)6)$nqR05hzRkz0Kn%rRcT!5K!i zN{*F7l^IsNN0OpzULG<08>Dd!y9PMK0ka%1cL11WynusqjB1rx4wz$l!H}bu&^5jp z{nbymt>u=xfuIlMlS*7oC%sY2yKniUZpydZv~ogC>B%I(82l0iK@7)Uv4QpX>}g8r zP<_$O*vh=6-LeWpt?3W$P$82w^z}8i#7F!r%`CrUlz*zQpq_jy`^*;Cm^Nnl;^Hbj zmnR=B-bo7-Z-s)kr58VF#oHPqrF5!!N$FN;2TiZCgNCmVoH|FUt$d7{E9_1AuP+r> Sm`;CZSMG__{9h|YBL4tZZ#uRB delta 63 zcmdlh-X>rc9O7fl?&BKf>Z9Ppz`)JT#Uv%gz|6qFz{JkR#>Oei>MbV9`apq|m05v@ Sm6?HuRfB<*RbylG1$F>P#|e7? diff --git a/examples/spacetaxi/generated/atarist/levels/level13.dat b/examples/spacetaxi/generated/atarist/levels/level13.dat index 459177ba1eaea6f82b75b1c0592d26b34e9a1ee9..3eb8f9da136d0e2b9cbdba81a9d5991030aa7569 100644 GIT binary patch literal 2749 zcmeHH&1w`u5Uv@W9p?w~074k>?9KC%RTC8!gk%LzA_U50{ntU`9yGaln>Sd*H^>7B z9+rIuUqJ9N#H#A*>7GB>gXkq6%v4u>Usu=Hv+TiadZU;<*uQ!I`mMW@{X4t2i`nGi zy#inagfJeB(qsbX0OLK_F~#rjV+r3@@C$yg;pYk-!Min_tl=|!S;E(KupNw-!H&dI zo+tp5w${J420XFTCs zdORQVs`7;F;}xqqd#lr`>J)o)MURZm?QAWo({4?5AAP33AL57F$GpN9SP}3ksu}k2 zvgL!E3Pjp)-JVZ{68upHb?v)p`K3mrFzD|`v*B|c%;@kLrS{9%ek7|K*@jY5A>tu} zo%ZXhW1_De^35oYj&Rqmaj{tJAyphsXj#55OR4&^BUfrR?I0#}ifNh*0em7&`439r z6G_t2)ua*EX(G8zo97Z`3F^nNX_{U#Fa_bfSCS~*>}Pj(O!}(x$e=KxU$uwTBensT#a&!GE2l{SSB9ZE(FyI2MOjGG+ znJv38QBxVl)76cfnoG7{#9(Sbmk^ytX>?f;1wjz0|YR!v$1io2QaK>V6YQo5Mp2h3EMG* zxiCZ=VNheRt6@++#GuFEwueFc5W_r%ZATb(9AVhTaIS^n!VyL}Mzaz|iz5vC7_PZ6 s>^=nIA7PX}1T`8=v4F^pb{E(=co`DFVDdp$(a9HB*+%iehXEr402dank^lez delta 71 zcmdlh+9zNa9O7fl?&BKf>Z9Pt00cseLShVTKn4>#8yg1;3$p_wH#0YrFtf0L2=fAA Y9_AeiT+9k0EX<1)M3|X3HeX-|0E5#Cp#T5? diff --git a/examples/spacetaxi/generated/atarist/levels/level15.dat b/examples/spacetaxi/generated/atarist/levels/level15.dat index f0912390c12055cd84f9e54c7237f4abdf5e3598..7607e7d5f1a4666d13c740c55331cc4e762fd932 100644 GIT binary patch delta 403 zcmeywzgN^cIK;<90R&uqT%ALL{5@TQ!CVFiU}9%uV`NNW&|_pU5oQo&UtuTu!y;DM(w&8wR^MbR_slyTeVU_LE2#vyMm&_ zxf!(r&k_*?EFj}DSv}Tf z-UO-5WSyJAx;OLY+>D!0wS6};diz+{t^^vl5^UT`wz(_V_O84&cLmJ2b3o(Hu_ef{ fitw{?@(M0u-}^v2tL}mJDS>YZXkLc}0mU=`v~HIG delta 43 vcmdlh`iWmDIK;=8-N!Y|)kndf0SJ^BM1>icfea>gHa13PQD*6l%@^1K#AgSt diff --git a/examples/spacetaxi/generated/atarist/levels/level16.dat b/examples/spacetaxi/generated/atarist/levels/level16.dat index e6471a50d2b3723e2d5ebd91922044cb2528bef6..11f416df153d6ea08842786a48d1896c41c4dc87 100644 GIT binary patch literal 3197 zcmds3PiP!v6o23B&g{$v>O@Tygvk(kh=?~21&fT54Mf(9=2U6iB+WKOJq$!r0<$xj z?GzPKyd))(#jsFt3M;*OSdrpoYYsVi@t|jsbPHlFJ*@A0-_Ex)lS;Mr)W`C^_kQp9 z{(avj?0fUo*KCrvs&i-0zWMHq@BqdDMJZ`op>Q5f0lYl{6QDs6U;!Ej@cu3M3I05Q zzkXmAd*gt;bc^#JM`}wsS-gO996cu{dz4;yx_Ep4Zr{D#FCRe^_mbxwFaJxFrxlzG%dAZTTxrCMw6Q6EA#iNI)la@-lS>E!p z4A{$@Tqi9y6fe3^Z#J8Cl+Q)6&*}n@t~5AlC?6(h#@lOKjfU2hc*`x!8k>kmrNRX~Ue&S~PVkik< zJuZox<%t*(gi2^ejwkWFG32STL7pId5InrDhsQZe6L10`J6kZ5Mav)$`J9)MtVgbWb^wUzLd&H3`TK)ylZ4|hP)>vYH=ty zfm%`&MNg!pj*X3?!vT?mXzp91D5K~zr9f;hv=Roml5!(}8GswW#-ViQd(>znf;f+c znUu{@eh4aa6bRIcqlY%DK=HIQd8dyWZA1{~QRYCHP9hr%mtnsMPC48kXQpUkn&t5KL{(JP*@4n5Y$S zE`JKP$$u%~M=1kN=9JBV(@At_^L!K&bg;RB!lrbGPjw245vB#i;62PK>O`bO**Hf^ z99>~aAqSKz0pOQqIAMw=P2G{EG)xG@N{G~IwfIc58aJYg_Nz{CwsyRuZSSyfwBvOwbxzx|)Q-nbPwlL0c&=-m zAKhrBUD8%=>b&T^YNgTj+}`Og?siZ5J*EF!tnj8`>za%0Pxx`so^Y;O{x_CiH5!8f zz-H~c=UBTSFih%OJ$6{A-fZ*~JZ?8=>LbhVSbk4G{JGXXBc}enJwPK`j3HWz&SSkU z^P^>3bFMG8K5?%vwIVkTc-t3Ro*T2)RTl@Tt~9#W+&PB$%dD?;?>FpcotWWF7&)5s zJCi=*1x`ERzj%@1#67iN{`y@rz$8IK;=8-N!Y|)kh(Kfq{XWky%ufg$>AJVrOGx<`89Z78hlCpdiY^ys`NL FI{*&@33LDe diff --git a/examples/spacetaxi/generated/atarist/levels/level17.dat b/examples/spacetaxi/generated/atarist/levels/level17.dat index 4aa266c2696025cf131931f6561b0fa4fe95cce8..457defe79466772cf3d5653e69b056e82e7aa772 100644 GIT binary patch literal 3517 zcmeHHL1-Lh6n-<=$?lphNkB9~DRUK3JV+@t*cIInAylb771U`6YNKEzcPm-qF zG}#@om$smzu(Xg_Oh`*FHLNT#2pcbJyLeC(sUQ-e2EA3;^}T=QpP9^VRu6iSK7Ri9 z-uK@3y|;fh`{I$&CsnlP4jsuH9?l%j9C{{0IY1Af$?@*)MEn9=2N)iNK~M;sf(xI( zr49H9KC8nA>+m=XUxr85;S2cbL-=_^+AHn5EbU*H9+39crTrW5Jv8d@?K>ofF5C@kFD$w;!h4Ul5Hw=+uA80kK*RNKdZ}>P ztez1u`C%QAz}~@d^3>!cM?<@-Iw5{lX|Xt33#jnlnD7ro`}khpfA}sknkV1!ojdmh zRX?J@mlhu&fObY=g?4>m%0>8mv{z(-@mV;_vY9_k%L4d7FS80{b3cpLMhydcL(7}}3Y?O~p2S|d>ul&X!LdG7! zKi!^xAPs-?L9<3T#;Ay#@DcpE7Jdwdo`s>O;m>|(enmINh@@g|`Da@A07HDeCP1?S zc;hHU!ox%R=i2a*fQQKZxaltZvr#^0b~U%dhlCIhOJK@vKFW7@<0}7T{<${%UVv$k z4pyXQ1#gT2k??SL24^lr_^}kkdZF)m= z7k<7ye~W+f8shu6`}YSl&1qT>*R*21yv6J}bLqx!%)V$i-S)>u`6jd9Hp<^HJ7<({ zGn*NXWWQ%@{vg+GYB+e{XzA`#-U1T(`HRyh@wM>hDb}5snLRysCUJywbf$7y$IMNrkEOHP_&C5U{4Pt997}x8M#{HX zDXTt|cI4b~^^!UIig{&hV4|KIyK?;eF|EADmZ-xyk1Z>oGR(ib!?yRJA7DQ+dyS3G znvQH=W4JLUUz=3HkFmz8UE708H-5nhU%DDub#J;DSD#4Z4D7zfM8Q!}TvUn)tX*ZB QwYyGy`6|=s?f#$k7aEx~yZ`_I delta 71 zcmdlh-6vod9O7fl?&BKf>Z1_Iz`(#Q%p@tvzzSqBv9qypa0{|D2#B#XGYK#Yi}JHv ZP!M2NkQZZ7kPu@z%qhm4v9b9AI{>AF3*rC( diff --git a/examples/spacetaxi/generated/atarist/levels/level18.dat b/examples/spacetaxi/generated/atarist/levels/level18.dat index 2303e0d174f825305fce854af5d2c2c6fc04d53f..b682e46e550fc6503224c59e6f721d3bafc90ac9 100644 GIT binary patch literal 2749 zcmeHHJ8l#~5bfFN*=AT+nH$U@2ylXA39n=cA{3$#+x~YoDq+&v)4xLY2e@9_!+6~7b_+{P!h70o-)*Z_N Ffg6$c9^L=| delta 43 vcmdlh`iWmDIK;=8-N!Y|)kh(S0SF`+1Oylufea>gHa1397M6gG%@^1K!`lbN diff --git a/examples/spacetaxi/generated/atarist/levels/level19.dat b/examples/spacetaxi/generated/atarist/levels/level19.dat index 76c0dd3b2aa448ae774063f34f1f05a3fa888a12..7a9076126fb30a6749800a89cb49b90ef359bf92 100644 GIT binary patch delta 764 zcmcgoPiWIn9R9tmZ8C9KhSSPGa_vD7@g|IbZbaxoSV7pubOcFuP~xGUelP24HviDE zo#rJVfk_kWu%nkb;-U7EdI>5X1ZB*lFpA!GDE?lDXK(x9@%w%6`}n@cTgk^$?tHm) zT^*sU&#LoxZ3-~v=lnC`+1-0#4RF1&{y9XRO0 zD!lGNy$eG&XELoPtp7ts7~0oT*_{MKKriOKid8%p#cwcMF+gD;k!9qg`3-%~i4W07 zCuIJ*(?5{XPX-Bvh3I&OrU0X}w&gx_1(7m2Wx2Lef8wpKJ$?54#q7}#TSF|e#t;Y0 z2;{iV3pR88I#14~*$`-H?)^r8YZQKH=&d_WdcK8Dvam6cbl#2!XVh^mQSy>y1Q}YR zz7@NC=i(!Z(*u)kY_n12jmJE!dRE9iTfL%LhG_!S;E%+Zlu zE-&5V+lvMJi?_JFv{^CYeQXL#xJjO!cS!vE5e_D37Vszf`&g>+Q1W+?o=oH`7mCej zPsRq9ntQ*f;^Rq%rC!zoS-q-J2f26gpAf1PK2ltg0)@mj_S54~Zne=6-*;Mga9Twt GJ%0h@*~1_J delta 59 zcmdlh+9Y5U9O7fl?&BKf>Z1_Mz`(&S#3UrZz|6qFz{JkR#>&9K!^|SYsw^VJa)MWc OC0mArQ)6TE1$F=*sR>8` diff --git a/examples/spacetaxi/generated/atarist/levels/level20.dat b/examples/spacetaxi/generated/atarist/levels/level20.dat index 0c7021577c93f23a6cde8696bc6d8b5e34f753d5..396008f50e492bb856f9d155333d16cc0d656fc6 100644 GIT binary patch delta 438 zcmb`9ze@sP9LArwUQdUHBAtS`AVPwmp{>I*NQl&{G^faeAZ&aIg?^kax>Vl?1edh^ z2FZ3?jw5pOFKCpe!^s+gdaobUAJ79|-sgGY^SKmm?X?St^($sHb4M^d62i($ z*N!Vyxj(9k=u0jyIyLFC-V+i?3i(P(7j9B?hh1G*nO`Cp1jgnEJ{ArGQ+V;QSnRu0 NH~Dnc;GuswZ1_Cz`(#JBqXiIzy@S7v9qypurRYIOA50va5J++bMi2I Zi!!rZkli_ko%ZIKS{gMts~Necx* zQu?MZ!4X87K}8<}t4P^{-NVQ~Ac%sXNa-F#5Otm;G7&^7cyg2T>-)ZQ!@c#%Z#(eP zW%Y;>*~QZ*^($I7P85hukR@qsY!Q|K&ZQs)0zGg8s#&mB;2>y^V73QMc)Sevdhngi zEHh;V-Z3c)Z+kx_{^}B8_u1h<#-wN+2e=8aWWn=jQ9vmbN3>eZC-O$LC>WiOG^9=f z41>9yodQWalAYeJup*M&}wI&UQ@RpL)8_+sJ9$);M{c+d3a;JP-%u zubW7(`pWdc=u~hhtyNGps-0DyW`P(-b_o~3mA93!#a@QmpI$)J^S0U*^lmpw9yZF# zfs89EJfVPM5t$SkA*&uPLq3)+AsQ9O7fl?&BKf>Z1_K00ctJ5)w>IKn4>#8yhPp3v&jO0E;l6FmthtFbjty L3(M(^%@^1KFvV5h(!2N9HG zFmqv$J_O{NodMB#3~eq9*@qbBG3-0TF#8b0HHLRC43`fvsxjJ~VRSeGqFsRWZ1_Gz`(#J#3-l0zy@S7v9qzUGq5l-Gl()XbFnZ73y88b Vi?cBARupB{P-J1&*w}o59ROcq3IzZF diff --git a/examples/spacetaxi/generated/atarist/levels/level23.dat b/examples/spacetaxi/generated/atarist/levels/level23.dat index ccf8623dfdd5af9218af3fcd4073bb39333ae942..23b1bec9e7a2f3fa145528b102e258ca124dcc81 100644 GIT binary patch delta 764 zcmbtSF^dyH6rS0H?B-%fkf;$QOshl?v9J(X3+0LwVl3>O`i zhq`r^5YD-z;t;rMt`d_{5*5#*;!RXw@ir>nMMbOKYU65Yh$KQ!WtrMqT#ILtnIt`? z$G9lb3>{kFM}IA2$0C^k9-3&@fWyHwGG}Q3Dk{ zIO>Dnhk+69bN3Yb4I=4ZD3=tu;0rig&tYE6ulvvHq733ueT4@JuuH)L*kwuV3hL~o Yw6gHa2DkK^6^uPPS@(5w_}$%@^1K D{q_k1 diff --git a/examples/spacetaxi/generated/atarist/levels/level24.dat b/examples/spacetaxi/generated/atarist/levels/level24.dat index a499a94d1b4acd5d005ff8ff570cc2856e66aa1e..13dca31b4b3937c07b17df8bd08a25a41909d135 100644 GIT binary patch delta 764 zcmb`Fzf0sm6vy9s>qLx2PLQ>8{0m&cT5S}I^#>@YR>sxlEQEl9&AU`7b1H&byJnhmQL_ifmb%+mSS*|@c3L_!Nny3seT(_dB=0-#Gs%3n#E`qZQkvjWiwk^Z zZide*kHJ`c_hEYIj16LrkofoUPx* zgx050_$>}_`0MAYxXV+L7e~Ex+$+`?tuw(5mqrb7cTdT^IC`Mt2eH8T~(+wqDgNx>7gcF)ubb>_S4SGpJJkSVAZazKj4l@TsP$%Yo^`1 zXC?OE6g#J=`xJF0CqFYEoUQ;raocS@WaHyp<6pewG;($Y(xTXN{4MltC*F7bGWr|N mt}RUTHbp;rZ1_Bz`(#Q#3U}xzy@S7v9qypaIvyBaB%@KKM;$sHi)va Tt`_A2Vi6$bXI;Iq`2srtvla~d diff --git a/examples/spacetaxi/generated/atarist/levels/title.dat b/examples/spacetaxi/generated/atarist/levels/title.dat index 9887b563cea400c708312b014cc0a86ce2c0f27a..83ca545328d4c48b3b960ce6b48517eb6f470f02 100644 GIT binary patch literal 2749 zcmb`JPiP!f9LL|x?99$gw%NLWO4c0u@J~?Cq9|o8D5;uY>yT{sPl~1LM2f`Y2ssGu zTMrHOknKfK%d)6FbxJoB=_TM^LMa+z-9teHQBPJ64T}enf~>#adv7*xveo9`FPk^- z{od#I{e9n?B&1UHo~1lWwNt0OnX*_=B0^7EmT7)XJ|^Ua0x1v+(%J#-a8)Z_*DP&n zOMC1(Y~I)+A8nAsWMPXuvq7fFY?VB`Atmnn@ZZ`~EWXaIorR!$faO_C4KsX2-{72> z;|SkSAX!m?Rg%MMDZzy}id8(XaF~)>Q${kImXc?I-Et^?0bHGdtg;j2t zyvGE&+=NMy0Xstx8bcWg>O0z~fV`iU_HF3R{=TFXY(izX@GI=BCq{jss=Vp(U zXXuNyYIS~&=F(I*lg{`dt_WNyucw@>t(e~NHyy>B(LKW*kHl9>CgPOTJ*TH5Wqghh zmHFA~$)jMJYI_X_{$yA-1koYz?}v^lzQ93uFf@%omhU*2(FuHD!V7RE133rwP>C1w zQRf6t3puU_9M|O41V`kc&xoF|fdr_cIVq0$9e=3D0}mmmQ6^dne+Wop;xw=g#{ttZ zjxUhJZ3*HP!1pKz-ta!8Bbe^yhMqXbCAlha!!5X%NLlLbw2u2ri5UCWkbt7+$dvLR z(YD$bTJj50hsaf&OKFN0Orb)83b%{l6%iJlS8Wxa5B1`mMpm*jyk9~_Hsc9wjz0(S z?8u5M0;g<1D(+7CC6gOK!z{Wk{v`N2A!B&qQ9^mfq!nf5E(L9nXqS));`{zZHzds} zYvVg~ z43}uGl}x%x$2K%;;3k}b>*xbwI|I{6S_24K781WxWqrf@fpyLVea-Kbg8rk$-!<30 z941y4yN|F->v`5secJR`Xu&bPFWGN57JI+?oz38L)4iJbuy@yAtui>c3kS#J{S}%Q zz6jt_Dy&&!H)|}+G#6OdTUiR($~*MQY3P>A5%R#*KGn7t7<@VltZCnBCU0$5Sf|1s zPlA>08Ut_KOS8*+-l#Pe&bczFPdcMM+W;3?(=kA~x%R{UUrWx0Z?EJs#& z*Q^GmDx(OFG+WKCv^rPHY+rUF!O+f}{J4TS1N^uM&K7fDGl%se1HrkL3_9F)`O3M8 z%v5G!FyK(eYyR@Tas(hM=X+}z{QL3{ogrN1X&)vdxE@q-niQ<#>dn`vVd8T;aF%Zk z3*AdA*gHBrSn-k@XBjMrDPfV6NwJ-5G@#@faZN(pRX!{$9d91P-3@f}0#Y=EGR}*n z+_g2n-}!twGoeZGxP=6c?FCyMiarfbpazn;)m@WqcrS9QO!PN*xpeXzTU*E9)^vJJ& zs+K*WwSrZJZRNI@)0rHzc;Hsma~5s8fx|-WWEKhOMaA{9OmsiU3!Hn6@wS8HLEK;J z`9SkE%uVa!Ni+UY{)E>xq-*5%;@~q{j@fvizSo=cDWi^S+cDuc(Fp}&Y-Obx(!_j=4 ziXt(q^kIM6)=euqrz7ME#orTUJpRHP^R_k^_XiVL!ht_`m<$y4*|gqY3hUYxSu{wSw$;BJ!zZz>o!X(;r?W6 z+3I3dbRg)V%PZ>Pu)n%qE|Pp21J8Kg{B8*mJC$jrzXCb@Crdiyfy_!_(&M}w8XS*+ zdC>##YNY6mFAz*zS`OxL;zm)d#m}5vsBxU2RxgSeUnxz;`-}Iw!D%1iczph>J*k`Y yLsjW|b=cdcv?;t9rA(={J`Gly;V@+}Ek;p%=aLztb;<%fvZegdJ8s}VHt++Ni0Fd= delta 43 vcmdlh`iWmDIK;=8-N!Y|)kndR0SM%n`S=(Zfea>gHa12sQSRW4%@^1K!$$|$ diff --git a/examples/spacetaxi/generated/dos/levels/level02.dat b/examples/spacetaxi/generated/dos/levels/level02.dat index dea590378b780e5ff61a0e7c7938644c1f6d5b50..dbebde54cfb5666c5d7580175e47678faa75eb5d 100644 GIT binary patch literal 2749 zcmeH}O>PrG5QWPV_Y9*pi`-x&q?~{Y@M9DSlnq%DDxQ`cK@a7=z}Y9XF18v;rvj^lJFNqsetnbx>I81n)nZgrcSH+LLz z(OvIFZylVmpfhP{@Zw-orQVlyIQTD1}tEo~F)Cl3+oZNuNv9M0TpDJ(C^k z3fU#Hz;L3hKO=~yDkJ~Nai0G&{*C7`YmC&Ab=orQ`kOw*gz_GjN1s61ajS z6FEobmr8@HJxI=QIOJqKRZbj(&J{(09cmP@ICzxiy28}}g;Q9+Kp{PP>^K2aUo^{% znX8$Dq`&AWiTm=@2e{afliw+Wef2m@oGT%YWbDVj%aw{TXeo;`uEg6lo6TzeB&E3W xSd4P|?M$cq0=SC1yq8ot?`^evKakB|KK>~cg{|`c2*;xC2JeEq8@P=d_yZFt4gUZD delta 51 zcmdlh`ioyHIK;=8-N!Y|)kndJ0SNfG1O*uwfea>gHa2EP7FKRHVP0cVc9shpn=h~f E0QX=B$p8QV diff --git a/examples/spacetaxi/generated/dos/levels/level03.dat b/examples/spacetaxi/generated/dos/levels/level03.dat index 9b5a7033826b7eb8a3f31643ba59989ee02327b3..41ae91bbb408052b501566bdb8013a845b3f593c 100644 GIT binary patch delta 764 zcmcgoKWGzC9R9s$lDmqP=zl%QLO;O{QDI`)J2et*7??|ZzJe0bWf+$>*K z5>#&7QYy0xSLf#zk`cz>QZARxzJdVYS{_aTPu6Fkro(g(X5da2uJqtN{OH17A6DUY z9~wOvvFkch`(uay-Wb~5lxPJUQlX*_aP*B(E4`fToCgzXU)S#yL#rg>7> z5CxOjZbL*T;;awUw03@@yOEmjYo+$Q6;HL%iWXl?M%}j)-f?9@jpRHxG%rJKFfc+} zY@NMNb|p`zjj13}ssNxwEXiv_<*0 z*jy@@U!0}Q<@Kr-?qVyE`0J$EdWSTBKfvK6)dK!RcNfcb;d6Hz>B%&F6$xW9>Z4=B zbFH0U6!CHFVJPQSPf{+bltJok{73i-nWv0RlqVC}!9jfJOYIJ7$@d)Z9Py00crzLIMm-Kn4>#8yhPF2M;rg5Ua9?5X%W(5teKj L4o;1Y%@^1K8ixre diff --git a/examples/spacetaxi/generated/dos/levels/level04.dat b/examples/spacetaxi/generated/dos/levels/level04.dat index 74993ec09de70db8622005e7f4914b7e75932f3b..55b6b37d2503e565229c90151c0e83f1978d2bb1 100644 GIT binary patch literal 2749 zcmeHHON$dh5U!rslXMmY!Ee0#W;Myfk7_8si(u1A#t?i8+eg}u7 zou*xN9cBQtvv3Adc3+0=5nMFzNA!LOZ2;l@6_^?L1mC{E2Lr!EdKG>eabEO?;+%mS zaQGQ^nSbFU{5H^o{&(0kq9^)a#irT2*TDV*0(o;B29f>ST-ULfgx)*<&dU+;>Ch*M z=c`3@hR+bob2Z&;93u0nwXX5PK*L@kkNSI#7@vz*rv9vM{@6F>quwfcx;_w3D6DLp zy|ubacv??fzyHbOO}?ZLj{{N<29M1qtVEEt3i-~?5U!14s8NCyWMUrq+&?IfRy1|&5rR%h~TL)o>B>b zr>P`7SJO@E2+pV)FAOy774lA7&|KV~cS9(6g;a5U0QNu-XqW|$64HSMfRw=dSjGdD z+I|=FsQe{7*C~CIY^i%(FK!U_n|a`L7dBJyxFZLr)$E=wF851FW6g9NWKnFCr|7OF z?!H*XSU$yh5&fl7636REh#}ZB5Ex WvLM-0MO(0EVM5A@eKLWSPv9@&{us;v delta 79 zcmdlhI!(YWIK;=8-N!Y|)kndF0SNfG1VtFwfD9&fHa0G1E@o+IQC1F4W|jheL6&MM g7Us)}94y@I{LI$UeC)Rwgjo#~gjfwWHeX-|0Nc?E_5c6? diff --git a/examples/spacetaxi/generated/dos/levels/level05.dat b/examples/spacetaxi/generated/dos/levels/level05.dat index 8467bf0bb7a8d5024ef51fcdfb7feb0145747eed..02e66bed022342e2e1bbdd8a862150d75e1ec75d 100644 GIT binary patch literal 2749 zcmeHH&ubGw6rNo=i9-$|Z6G3)?H}OHgF+9f&|VB)S`b3jRUszUlLVCD?5$9-n{(4h zD?Nx468{0kL-Ejnh{U%36^5QXc=EkBvzys&e|QkR^kdoC_r33Z@6GHc^9!Xb9^(4! z%+2|QnbHl4JjMhAxY=!q~vSe+c&;0@CblJ=tmZJO4npyZ3s ze~S-Nfca9~-AasVwK5wAjYeax0?Y`{;IaO+UMJT6T~v^Xvpj_?AVqR6jsvq3oJW5k zu-XYB5~1a16_AADsMOK{g(<;cI82|SW2hqy7DY( zJk5YQdNAeqZ1xce$r*H7X5FoQAWH{HeXUu**6cU7J4pxX6Lr*`<{Rmv`Sd17<#>M9 zv^1hVKdjIh>Zf&3Xgo1(&V_W806q0Z|FkaA2mNRaqr+!&F4UQF;F9#y7WT-fLpC`d zZCCqgqo~&*KRqS&H9BntZV{EIA>z<2{<+&V>vyj$6Ht`-fcimBT zjE3+5$_a%G%TlqhC6ybtr5X>{q?!!ZWiS7v?;JS3nuEaa3E3B49lzI<{T!@A5Uk7o zEfg>i3|q3-l6`(KRj5sLy5KSJUIehNtf+~rKU%Uwgm*3Qf!w;-Ys*6IQ6$+qPw^7O zpeU8G5bO%Id`EqcL~307>;d5qg~Y3j7ft_VO~%)-E#XqHm#ZpRu6dV>esd#O-3+`- g#djM)cQa7jn=JZ_O;0m6<^9Y!=CzzHfA<7_11l4gYybcN delta 75 zcmdlhI!VAOIK;=8-N!Y|)kndVfq{WjMo3VUfepxFVrOIHy4+N`%MLhVCq;ZUB3kwOB;FzSa;AIY8 zJxY7n!u}KDB{vgI^YKT796edzo0*-RB-^G!@zft-=FNM*`Mrn zz0YN3^VS!q$ZOV{^*MU4sK6$rr}{UA)obetA1`}tur<^(yFMLC7E=AfZ?TEH+&9*KD1j7kG*@`QJhCpAP=YDiu*K5-v-Myf1I4;ceN1RLLnZC5mC8m(2*s!2w6}QUZ7jc?mD& zlq=4QnN_xUlO9o#dQU}pF+o|1hbLK|@E@r+%`>MaZ(>SWk1F_>UZT)~f^Av019QNy zLBO$dIa@~|EP-D0nkb{Ci4{Cxal-1fCFSf|}m6dwy(v{_4-!BIr z{WtFA)!=Xq3LP-mq1quE4cL$L2$Z{QY`3 delta 51 zcmew>`ioyHIK;=8-N!Y|)kndN0SJWHM1>icfea>gHa2D!0cK@c0cH+a7G{o(%@^1K D0^kT$ diff --git a/examples/spacetaxi/generated/dos/levels/level07.dat b/examples/spacetaxi/generated/dos/levels/level07.dat index 805246c776f4e9d3139e81976dc2a8730425a10d..0eb4025aa5dd7be0ba68be67b8dbbf7b0dd6fd6d 100644 GIT binary patch literal 3005 zcmd^9O=whC6h80Gym_Xv31SctDT9SV7TvUxRgi3wg^oYe&_xz9q-~N~f4CA+0{3MS z$<7E;ILKcM=I=GWAIuL>jjjc>#=)#qB=|Tc7nvFl_ocr^BGRE0^k~#1EeCIpg zy>FPaqm`p&veDtn@R`$Jjh^MK46ql_{IFCCf@Qc1@by9HgAx@7;M@iHauq&~C-Xs@LNUOncp{@Grdn$%Fd@?ZhTKHLcA0$w`jP!&b}Xd_lw7)NRAixYPxj zGsBloPN0-p#vG1NJguLGY$so80WqP` zQZyInv~8t5Zs%>k3ukw^6?zMv>H=N4vz=Pn>C?k*2pKM#`_Tt`kDqWhH8nBpl9`#A zkqN9Mb{)g%Ytz$Qb?Y#gpo;5;omy97(A5=&oYDic#KJKBMby@qv0{lWxW2Tu}l(X%nTSZE+%JToF3QHI(DO$ z$Ka@rd{&6_I4rQ=6XG!}SX5cDcQ~D+&rQVbwf`NeA2tyK(05Ro;4$YM)lCaiG>^bt#OC_1{EXtN$#TPi6h8X#OYb z3!?d4)}@HO`l48S=C_{4pf;p>BC7yzYIE~9=I4<2$oZpRtT4al(PQw|?ZFd`N21?r zdDYlci=qlIAZkF=UuYe9u^}59@?ohJFxhf*Q>tHtx-HZlq3S~YDjHAZR-y7JYOIO= zZK43Ai2HDD<@adGFLw>~dt1GYMY;9S!_tp_94uF^L`wnIm;YIptzfO?F9nP1a;^C? zQdflSeS}u7rrllJ`|5Bf3_{?u|Mnn y8~&^cH`Sg^M4lIqQMWm8<#;r5ePin;-^$2?2ZP@}9~%SMSKIF6E8_YGZ9P!z`(#G#3CZWzy@S7v9qzUGIO#@Gm5Zs%Zsuy$Z@eU Ti1V>BOq?h^aRJZ9RVNv{fd diff --git a/examples/spacetaxi/generated/dos/levels/level08.dat b/examples/spacetaxi/generated/dos/levels/level08.dat index 3d6d2733ec28d49c9cd5ec24da6b852a2913424b..08cdd0163b1bf720dc360fd2b38a6ed3d46c0418 100644 GIT binary patch delta 829 zcmb7BPiWIn7=N$%*JK@=TPvGAEcj>WK@^oy1Z9_rLXfUa3Jwn$>`;#pdJvW`Ln!mm z>>{Y61a+sqv6Lyh^pHy#L(7~w3`7w1fu(Dc8c>-v&JkgbA~f@c50 zl0dNY+;A2QGUq%sy!WP4GQ1C_Q#QOWrehk-uQC6VPB~^OpDlL1Lx#i2oS!$>6C9W5 z9XOF58A-D}iN3xtdZ=JbpJ=F-O4Vu@rE2GFcf37uO+h%ua508uB!nei5F|m2Gnj8n ztk{;sSR2K~HYW;vn<6}B(U&0IRUu%MkKoQ20oL8D5uC~FVM$scY!HD4gj;-R(6;2k)?ix$GGG^g_~jyj z@;&+dIAu#FDgUvxK*p^C&~OW2$9HUDr+9Y5U9O7fl?&BKf>Z9Pn00jJul9CK;Kn4>#8yl-U8;gRhD2o8UC|fnND2p^B L8w>Zw<_qiqD8mTS diff --git a/examples/spacetaxi/generated/dos/levels/level09.dat b/examples/spacetaxi/generated/dos/levels/level09.dat index fc9246e304891ed5249d67941bb6d946a311beb5..64517963b02417574ab3b8291467b407ae1c9ec9 100644 GIT binary patch literal 3389 zcmd^B&1(};5TAX?W|Q@UqE$Q;*^8iv(u03MKr9Fa#nz(`TU{H!11(DNrJLIH1E~mt zo`R55H>h}PduS-)!FY-{K?FSr9z;BN@t}3)ee6e*Xay0$NxC!d_kQ!6nYWv=1B3mC z9J0eFjtva-9X)YGtQ?TWtW?UfYET2Xyd8Exg5ZPDHwgQmlE0z+L-O~a{tWIuHFg>Y z9~!;S$lp+Y5WSLYaW2}#P{dB3L(4^gp%QUi4?pgN?62*$Bq^?YrtNkFJSzm<2+|>x zbvtZEU~0|XpdK+VbesfdEf9n3-=D+1*y)ax+^cekKR(As&)M6^{rJuM=3eSN@0GtY zFUKmE@ozHn^f?Zd*7KeZw!#D8c}b_|)N_Z0DK|WP-pxw{uX<0M3#p=wjJQW+gxB%$ z@jgOAyN==Lwb4<5M!OQ3!LChaM$JNtu1!I)E&SMarqw_}wxZa!{$~>BYF;KI1+|uB zhg`9iOCmDjX+;P{B&K+bn2Ln#2tTGyEN!GJv!hqND?C;c1>5jMjv^BY%e2*_DgZ=e z(f*<(1w~$vgr?@`KRVBo+Dwb*ilSl_mD+P_|(!x`g z=U%XymosRDr13C7B3e+9?1Ia8gNVzGP&BK3kfRw)5+aw~LZ7#MIBxAn*{z4*`%T+BODoea_yWq3RgTjlqtobSDug&)~bAfs` zh~mMNSDL&z^kwLavBtiar)Os8W@o1T#QY+EyvSFU_~+dXqnYTxRctVH^`Z~+OZ?L! zf3(OO>E`QPb<)kzgW&Y2eQ>fkUUYuf%>-;S<195fKu1jq_MKfcjXPtZZ9Pv00ctpqQVT!Kn4>#8yhrbcT9QjF0Z zCzoxrNm|wiOnUr?Wwrx0^RyhgY%3!<6s&vU;t&>+;+jI9yNYAiUBKJj`p% zDI!pp*OM=YTWXL-MFPgOSb2`byIXm8<2CAdjgQ=wFX!FE`+hL6A00Jb6~NTgRF8p3 z-6nB6@!`Y-N4*g_wIEM;m5^y@Ay83b2O0Ps3V#;44;H^87Hi-Mr$ZJ;{)7|UU|ysi zis!`eJ>G;W7*D{V%^`Vk2p%sZhVyG|v>~a}o6@{?e=mFn?0)#CA%Z#S*to^4n(~;N zF?>7`Bd|d==lPx_UYBs3>+y(M4ke`4=m~7e= z<01pZLGCT6eJj!!m7Yu=i>Ep|-;8zk-$OrB*N1m;fKK)yzroR4mB?kAJPj}&$Uhra zxd^s{;v{JCMC)d^_BkGaL@QB!9BzU7gPokURj}t8_3Z!&fYbaF)ip`eS|Z1xZIY-j z6+n_J5Fdw4NX*)wMC=6EIY0p~654PCI-IxxCjmiB-zI|!8ihzjZyN_SjSy5=1NbF& zRj;I>a*^qAa*yB(9N$ozA?#K_3j<^dWCfz_Ag39O){kE**#)3FtXy)~ECed>+Qd+g zhPC;OU>F?;%)1!&e6XdlkC1i24aD*ccLsJq2ozW0XhR4TPm&5=7hL^QJPJEB8Y&AA z3+sCcMg1b9_i#<7QR$q2#&=*8+%q+s{U1ym|&*3Y@+n0Rg&qW z`jvgFbSF1j%I40|=k#y)tU?JxlC?^X*aFhFje)n%uegtQ2DchR{s5ZB=v%|HdMS| zu4Rfjvt+Z99xrCi5-DCWvB=a3Yh&5$BZxmss_MqNMIY8n1nXsT_(6xHy-0m)>tr)Oq%E|*`pxOi#lg1P?kr-6mib9O7fl?&BKf>Z9Prz`(%Cz$hunzzSqBv9qzUG4Qf5a|yBri;A&a;1Od@ RmJwoRU=(9g*w}o59RN|43Dp1q diff --git a/examples/spacetaxi/generated/dos/levels/level11.dat b/examples/spacetaxi/generated/dos/levels/level11.dat index 4c3ce870f5b7044a002cf19e29e2ad0a95de9b80..8099ce8ce9459e8ce08a46bf9bc6765d1bc06dd2 100644 GIT binary patch literal 2749 zcmeH{ziSjh6vy8#J2}^boivg{2?tj8)2JeJRkx{m`Lx1hqgLTR3#3;&&ZpN}lV#)et- z;b1VhVlY$Kb9DR9`hCuZT^Y*wszQsc9ponj|@ z6qn$`AXjv4Jv?S;c#(KsJ!P!n^rkA)fpC100F{zlr|Tz8GgNdtlYLI!xw( za|ux;MO-fd$!ML4vAiu{nL!F|nf7HV8@ml%J+n(~e}t&LL?oE}c97=^*zyC(g+Y9Onl90fnI- Am;e9( delta 59 zcmdlh+9Y5U9O7fl?&BKf>Z9Pz00hFU;-U;cfea>gHa1oU0TyNsK^A8LVU}hIK^6uD L0Tzvo%@^1KIVlOV diff --git a/examples/spacetaxi/generated/dos/levels/level12.dat b/examples/spacetaxi/generated/dos/levels/level12.dat index 6a5ba48d7fdcee5d1e495fd03ded99b50e7a2167..50b1b33da5a0db562634a96ef2aae124c770bcdf 100644 GIT binary patch delta 814 zcmaKqO-K}B7{{NP{g_=$8kPJc83O4Pc!<~vbTTrlkq&hWYDO2?-9t1%*5#cUKPJ3P zxUGr^LM$ve(MozN5f<~1C9zP1mq6%)AX)~3hz>qI@2*~Z=Y4oS|G(!w%tLv-W%$~4 z?WDp_pLXv26=mS6cA=UA)YSoiC`F^uND^)X^tVGhgs281@Ms%`H{c!o-iAM$LZ{HL z2^|~UPm+gb=$Qm{lY5PPE-NW3r^=FD7k0Q@0g(b-7JD7VhA>Wta6`c0B92l?RdDtJq@N&zUH~i! zrQN2zI}O;KK<6};=(b^S6nEPZ2V`TYLj`vG*_M>6p8rZ;8!&%6hwKkhlE z?|2KQFL+r~uPm6w1#@en9H|w@6F-^h8g54EvwS)6)$nqR05hzRkz0Kn%rRcT!5K!i zN{*F7l^IsNN0OpzULG<08>Dd!y9PMK0ka%1cL11WynusqjB1rx4wz$l!H}bu&^5jp z{nbymt>u=xfuIlMlS*7oC%sY2yKniUZpydZv~ogC>B%I(82l0iK@7)Uv4QpX>}g8r zP<_$O*vh=6-LeWpt?3W$P$82w^z}8i#7F!r%`CrUlz*zQpq_jy`^*;Cm^Nnl;^Hbj zmnR=B-bo7-Z-s)kr58VF#oHPqrF5!!N$FN;2TiZCgNCmVoH|FUt$d7{E9_1AuP+r> Sm`;CZSMG__{9h|YBL4tZZ#uRB delta 63 zcmdlh-X>rc9O7fl?&BKf>Z9Ppz`)JT#Uv%gz|6qFz{JkR#>Oei>MbV9`apq|m05v@ Sm6?HuRfB<*RbylG1$F>P#|e7? diff --git a/examples/spacetaxi/generated/dos/levels/level13.dat b/examples/spacetaxi/generated/dos/levels/level13.dat index 459177ba1eaea6f82b75b1c0592d26b34e9a1ee9..3eb8f9da136d0e2b9cbdba81a9d5991030aa7569 100644 GIT binary patch literal 2749 zcmeHH&1w`u5Uv@W9p?w~074k>?9KC%RTC8!gk%LzA_U50{ntU`9yGaln>Sd*H^>7B z9+rIuUqJ9N#H#A*>7GB>gXkq6%v4u>Usu=Hv+TiadZU;<*uQ!I`mMW@{X4t2i`nGi zy#inagfJeB(qsbX0OLK_F~#rjV+r3@@C$yg;pYk-!Min_tl=|!S;E(KupNw-!H&dI zo+tp5w${J420XFTCs zdORQVs`7;F;}xqqd#lr`>J)o)MURZm?QAWo({4?5AAP33AL57F$GpN9SP}3ksu}k2 zvgL!E3Pjp)-JVZ{68upHb?v)p`K3mrFzD|`v*B|c%;@kLrS{9%ek7|K*@jY5A>tu} zo%ZXhW1_De^35oYj&Rqmaj{tJAyphsXj#55OR4&^BUfrR?I0#}ifNh*0em7&`439r z6G_t2)ua*EX(G8zo97Z`3F^nNX_{U#Fa_bfSCS~*>}Pj(O!}(x$e=KxU$uwTBensT#a&!GE2l{SSB9ZE(FyI2MOjGG+ znJv38QBxVl)76cfnoG7{#9(Sbmk^ytX>?f;1wjz0|YR!v$1io2QaK>V6YQo5Mp2h3EMG* zxiCZ=VNheRt6@++#GuFEwueFc5W_r%ZATb(9AVhTaIS^n!VyL}Mzaz|iz5vC7_PZ6 s>^=nIA7PX}1T`8=v4F^pb{E(=co`DFVDdp$(a9HB*+%iehXEr402dank^lez delta 71 zcmdlh+9zNa9O7fl?&BKf>Z9Pt00cseLShVTKn4>#8yg1;3$p_wH#0YrFtf0L2=fAA Y9_AeiT+9k0EX<1)M3|X3HeX-|0E5#Cp#T5? diff --git a/examples/spacetaxi/generated/dos/levels/level15.dat b/examples/spacetaxi/generated/dos/levels/level15.dat index f0912390c12055cd84f9e54c7237f4abdf5e3598..7607e7d5f1a4666d13c740c55331cc4e762fd932 100644 GIT binary patch delta 403 zcmeywzgN^cIK;<90R&uqT%ALL{5@TQ!CVFiU}9%uV`NNW&|_pU5oQo&UtuTu!y;DM(w&8wR^MbR_slyTeVU_LE2#vyMm&_ zxf!(r&k_*?EFj}DSv}Tf z-UO-5WSyJAx;OLY+>D!0wS6};diz+{t^^vl5^UT`wz(_V_O84&cLmJ2b3o(Hu_ef{ fitw{?@(M0u-}^v2tL}mJDS>YZXkLc}0mU=`v~HIG delta 43 vcmdlh`iWmDIK;=8-N!Y|)kndf0SJ^BM1>icfea>gHa13PQD*6l%@^1K#AgSt diff --git a/examples/spacetaxi/generated/dos/levels/level16.dat b/examples/spacetaxi/generated/dos/levels/level16.dat index e6471a50d2b3723e2d5ebd91922044cb2528bef6..11f416df153d6ea08842786a48d1896c41c4dc87 100644 GIT binary patch literal 3197 zcmds3PiP!v6o23B&g{$v>O@Tygvk(kh=?~21&fT54Mf(9=2U6iB+WKOJq$!r0<$xj z?GzPKyd))(#jsFt3M;*OSdrpoYYsVi@t|jsbPHlFJ*@A0-_Ex)lS;Mr)W`C^_kQp9 z{(avj?0fUo*KCrvs&i-0zWMHq@BqdDMJZ`op>Q5f0lYl{6QDs6U;!Ej@cu3M3I05Q zzkXmAd*gt;bc^#JM`}wsS-gO996cu{dz4;yx_Ep4Zr{D#FCRe^_mbxwFaJxFrxlzG%dAZTTxrCMw6Q6EA#iNI)la@-lS>E!p z4A{$@Tqi9y6fe3^Z#J8Cl+Q)6&*}n@t~5AlC?6(h#@lOKjfU2hc*`x!8k>kmrNRX~Ue&S~PVkik< zJuZox<%t*(gi2^ejwkWFG32STL7pId5InrDhsQZe6L10`J6kZ5Mav)$`J9)MtVgbWb^wUzLd&H3`TK)ylZ4|hP)>vYH=ty zfm%`&MNg!pj*X3?!vT?mXzp91D5K~zr9f;hv=Roml5!(}8GswW#-ViQd(>znf;f+c znUu{@eh4aa6bRIcqlY%DK=HIQd8dyWZA1{~QRYCHP9hr%mtnsMPC48kXQpUkn&t5KL{(JP*@4n5Y$S zE`JKP$$u%~M=1kN=9JBV(@At_^L!K&bg;RB!lrbGPjw245vB#i;62PK>O`bO**Hf^ z99>~aAqSKz0pOQqIAMw=P2G{EG)xG@N{G~IwfIc58aJYg_Nz{CwsyRuZSSyfwBvOwbxzx|)Q-nbPwlL0c&=-m zAKhrBUD8%=>b&T^YNgTj+}`Og?siZ5J*EF!tnj8`>za%0Pxx`so^Y;O{x_CiH5!8f zz-H~c=UBTSFih%OJ$6{A-fZ*~JZ?8=>LbhVSbk4G{JGXXBc}enJwPK`j3HWz&SSkU z^P^>3bFMG8K5?%vwIVkTc-t3Ro*T2)RTl@Tt~9#W+&PB$%dD?;?>FpcotWWF7&)5s zJCi=*1x`ERzj%@1#67iN{`y@rz$8IK;=8-N!Y|)kh(Kfq{XWky%ufg$>AJVrOGx<`89Z78hlCpdiY^ys`NL FI{*&@33LDe diff --git a/examples/spacetaxi/generated/dos/levels/level17.dat b/examples/spacetaxi/generated/dos/levels/level17.dat index 4aa266c2696025cf131931f6561b0fa4fe95cce8..457defe79466772cf3d5653e69b056e82e7aa772 100644 GIT binary patch literal 3517 zcmeHHL1-Lh6n-<=$?lphNkB9~DRUK3JV+@t*cIInAylb771U`6YNKEzcPm-qF zG}#@om$smzu(Xg_Oh`*FHLNT#2pcbJyLeC(sUQ-e2EA3;^}T=QpP9^VRu6iSK7Ri9 z-uK@3y|;fh`{I$&CsnlP4jsuH9?l%j9C{{0IY1Af$?@*)MEn9=2N)iNK~M;sf(xI( zr49H9KC8nA>+m=XUxr85;S2cbL-=_^+AHn5EbU*H9+39crTrW5Jv8d@?K>ofF5C@kFD$w;!h4Ul5Hw=+uA80kK*RNKdZ}>P ztez1u`C%QAz}~@d^3>!cM?<@-Iw5{lX|Xt33#jnlnD7ro`}khpfA}sknkV1!ojdmh zRX?J@mlhu&fObY=g?4>m%0>8mv{z(-@mV;_vY9_k%L4d7FS80{b3cpLMhydcL(7}}3Y?O~p2S|d>ul&X!LdG7! zKi!^xAPs-?L9<3T#;Ay#@DcpE7Jdwdo`s>O;m>|(enmINh@@g|`Da@A07HDeCP1?S zc;hHU!ox%R=i2a*fQQKZxaltZvr#^0b~U%dhlCIhOJK@vKFW7@<0}7T{<${%UVv$k z4pyXQ1#gT2k??SL24^lr_^}kkdZF)m= z7k<7ye~W+f8shu6`}YSl&1qT>*R*21yv6J}bLqx!%)V$i-S)>u`6jd9Hp<^HJ7<({ zGn*NXWWQ%@{vg+GYB+e{XzA`#-U1T(`HRyh@wM>hDb}5snLRysCUJywbf$7y$IMNrkEOHP_&C5U{4Pt997}x8M#{HX zDXTt|cI4b~^^!UIig{&hV4|KIyK?;eF|EADmZ-xyk1Z>oGR(ib!?yRJA7DQ+dyS3G znvQH=W4JLUUz=3HkFmz8UE708H-5nhU%DDub#J;DSD#4Z4D7zfM8Q!}TvUn)tX*ZB QwYyGy`6|=s?f#$k7aEx~yZ`_I delta 71 zcmdlh-6vod9O7fl?&BKf>Z1_Iz`(#Q%p@tvzzSqBv9qypa0{|D2#B#XGYK#Yi}JHv ZP!M2NkQZZ7kPu@z%qhm4v9b9AI{>AF3*rC( diff --git a/examples/spacetaxi/generated/dos/levels/level18.dat b/examples/spacetaxi/generated/dos/levels/level18.dat index 2303e0d174f825305fce854af5d2c2c6fc04d53f..b682e46e550fc6503224c59e6f721d3bafc90ac9 100644 GIT binary patch literal 2749 zcmeHHJ8l#~5bfFN*=AT+nH$U@2ylXA39n=cA{3$#+x~YoDq+&v)4xLY2e@9_!+6~7b_+{P!h70o-)*Z_N Ffg6$c9^L=| delta 43 vcmdlh`iWmDIK;=8-N!Y|)kh(S0SF`+1Oylufea>gHa1397M6gG%@^1K!`lbN diff --git a/examples/spacetaxi/generated/dos/levels/level19.dat b/examples/spacetaxi/generated/dos/levels/level19.dat index 76c0dd3b2aa448ae774063f34f1f05a3fa888a12..7a9076126fb30a6749800a89cb49b90ef359bf92 100644 GIT binary patch delta 764 zcmcgoPiWIn9R9tmZ8C9KhSSPGa_vD7@g|IbZbaxoSV7pubOcFuP~xGUelP24HviDE zo#rJVfk_kWu%nkb;-U7EdI>5X1ZB*lFpA!GDE?lDXK(x9@%w%6`}n@cTgk^$?tHm) zT^*sU&#LoxZ3-~v=lnC`+1-0#4RF1&{y9XRO0 zD!lGNy$eG&XELoPtp7ts7~0oT*_{MKKriOKid8%p#cwcMF+gD;k!9qg`3-%~i4W07 zCuIJ*(?5{XPX-Bvh3I&OrU0X}w&gx_1(7m2Wx2Lef8wpKJ$?54#q7}#TSF|e#t;Y0 z2;{iV3pR88I#14~*$`-H?)^r8YZQKH=&d_WdcK8Dvam6cbl#2!XVh^mQSy>y1Q}YR zz7@NC=i(!Z(*u)kY_n12jmJE!dRE9iTfL%LhG_!S;E%+Zlu zE-&5V+lvMJi?_JFv{^CYeQXL#xJjO!cS!vE5e_D37Vszf`&g>+Q1W+?o=oH`7mCej zPsRq9ntQ*f;^Rq%rC!zoS-q-J2f26gpAf1PK2ltg0)@mj_S54~Zne=6-*;Mga9Twt GJ%0h@*~1_J delta 59 zcmdlh+9Y5U9O7fl?&BKf>Z1_Mz`(&S#3UrZz|6qFz{JkR#>&9K!^|SYsw^VJa)MWc OC0mArQ)6TE1$F=*sR>8` diff --git a/examples/spacetaxi/generated/dos/levels/level20.dat b/examples/spacetaxi/generated/dos/levels/level20.dat index 0c7021577c93f23a6cde8696bc6d8b5e34f753d5..396008f50e492bb856f9d155333d16cc0d656fc6 100644 GIT binary patch delta 438 zcmb`9ze@sP9LArwUQdUHBAtS`AVPwmp{>I*NQl&{G^faeAZ&aIg?^kax>Vl?1edh^ z2FZ3?jw5pOFKCpe!^s+gdaobUAJ79|-sgGY^SKmm?X?St^($sHb4M^d62i($ z*N!Vyxj(9k=u0jyIyLFC-V+i?3i(P(7j9B?hh1G*nO`Cp1jgnEJ{ArGQ+V;QSnRu0 NH~Dnc;GuswZ1_Cz`(#JBqXiIzy@S7v9qypurRYIOA50va5J++bMi2I Zi!!rZkli_ko%ZIKS{gMts~Necx* zQu?MZ!4X87K}8<}t4P^{-NVQ~Ac%sXNa-F#5Otm;G7&^7cyg2T>-)ZQ!@c#%Z#(eP zW%Y;>*~QZ*^($I7P85hukR@qsY!Q|K&ZQs)0zGg8s#&mB;2>y^V73QMc)Sevdhngi zEHh;V-Z3c)Z+kx_{^}B8_u1h<#-wN+2e=8aWWn=jQ9vmbN3>eZC-O$LC>WiOG^9=f z41>9yodQWalAYeJup*M&}wI&UQ@RpL)8_+sJ9$);M{c+d3a;JP-%u zubW7(`pWdc=u~hhtyNGps-0DyW`P(-b_o~3mA93!#a@QmpI$)J^S0U*^lmpw9yZF# zfs89EJfVPM5t$SkA*&uPLq3)+AsQ9O7fl?&BKf>Z1_K00ctJ5)w>IKn4>#8yhPp3v&jO0E;l6FmthtFbjty L3(M(^%@^1KFvV5h(!2N9HG zFmqv$J_O{NodMB#3~eq9*@qbBG3-0TF#8b0HHLRC43`fvsxjJ~VRSeGqFsRWZ1_Gz`(#J#3-l0zy@S7v9qzUGq5l-Gl()XbFnZ73y88b Vi?cBARupB{P-J1&*w}o59ROcq3IzZF diff --git a/examples/spacetaxi/generated/dos/levels/level23.dat b/examples/spacetaxi/generated/dos/levels/level23.dat index ccf8623dfdd5af9218af3fcd4073bb39333ae942..23b1bec9e7a2f3fa145528b102e258ca124dcc81 100644 GIT binary patch delta 764 zcmbtSF^dyH6rS0H?B-%fkf;$QOshl?v9J(X3+0LwVl3>O`i zhq`r^5YD-z;t;rMt`d_{5*5#*;!RXw@ir>nMMbOKYU65Yh$KQ!WtrMqT#ILtnIt`? z$G9lb3>{kFM}IA2$0C^k9-3&@fWyHwGG}Q3Dk{ zIO>Dnhk+69bN3Yb4I=4ZD3=tu;0rig&tYE6ulvvHq733ueT4@JuuH)L*kwuV3hL~o Yw6gHa2DkK^6^uPPS@(5w_}$%@^1K D{q_k1 diff --git a/examples/spacetaxi/generated/dos/levels/level24.dat b/examples/spacetaxi/generated/dos/levels/level24.dat index a499a94d1b4acd5d005ff8ff570cc2856e66aa1e..13dca31b4b3937c07b17df8bd08a25a41909d135 100644 GIT binary patch delta 764 zcmb`Fzf0sm6vy9s>qLx2PLQ>8{0m&cT5S}I^#>@YR>sxlEQEl9&AU`7b1H&byJnhmQL_ifmb%+mSS*|@c3L_!Nny3seT(_dB=0-#Gs%3n#E`qZQkvjWiwk^Z zZide*kHJ`c_hEYIj16LrkofoUPx* zgx050_$>}_`0MAYxXV+L7e~Ex+$+`?tuw(5mqrb7cTdT^IC`Mt2eH8T~(+wqDgNx>7gcF)ubb>_S4SGpJJkSVAZazKj4l@TsP$%Yo^`1 zXC?OE6g#J=`xJF0CqFYEoUQ;raocS@WaHyp<6pewG;($Y(xTXN{4MltC*F7bGWr|N mt}RUTHbp;rZ1_Bz`(#Q#3U}xzy@S7v9qypaIvyBaB%@KKM;$sHi)va Tt`_A2Vi6$bXI;Iq`2srtvla~d diff --git a/examples/spacetaxi/generated/dos/levels/title.dat b/examples/spacetaxi/generated/dos/levels/title.dat index 9887b563cea400c708312b014cc0a86ce2c0f27a..83ca545328d4c48b3b960ce6b48517eb6f470f02 100644 GIT binary patch literal 2749 zcmb`JPiP!f9LL|x?99$gw%NLWO4c0u@J~?Cq9|o8D5;uY>yT{sPl~1LM2f`Y2ssGu zTMrHOknKfK%d)6FbxJoB=_TM^LMa+z-9teHQBPJ64T}enf~>#adv7*xveo9`FPk^- z{od#I{e9n?B&1UHo~1lWwNt0OnX*_=B0^7EmT7)XJ|^Ua0x1v+(%J#-a8)Z_*DP&n zOMC1(Y~I)+A8nAsWMPXuvq7fFY?VB`Atmnn@ZZ`~EWXaIorR!$faO_C4KsX2-{72> z;|SkSAX!m?Rg%MMDZzy}id8(XaF~)>Q${kImXc?I-Et^?0bHGdtg;j2t zyvGE&+=NMy0Xstx8bcWg>O0z~fV`iU_HF3R{=TFXY(izX@GI=BCq{jss=Vp(U zXXuNyYIS~&=F(I*lg{`dt_WNyucw@>t(e~NHyy>B(LKW*kHl9>CgPOTJ*TH5Wqghh zmHFA~$)jMJYI_X_{$yA-1koYz?}v^lzQ93uFf@%omhU*2(FuHD!V7RE133rwP>C1w zQRf6t3puU_9M|O41V`kc&xoF|fdr_cIVq0$9e=3D0}mmmQ6^dne+Wop;xw=g#{ttZ zjxUhJZ3*HP!1pKz-ta!8Bbe^yhMqXbCAlha!!5X%NLlLbw2u2ri5UCWkbt7+$dvLR z(YD$bTJj50hsaf&OKFN0Orb)83b%{l6%iJlS8Wxa5B1`mMpm*jyk9~_Hsc9wjz0(S z?8u5M0;g<1D(+7CC6gOK!z{Wk{v`N2A!B&qQ9^mfq!nf5E(L9nXqS));`{zZHzds} zYvVg~ z43}uGl}x%x$2K%;;3k}b>*xbwI|I{6S_24K781WxWqrf@fpyLVea-Kbg8rk$-!<30 z941y4yN|F->v`5secJR`Xu&bPFWGN57JI+?oz38L)4iJbuy@yAtui>c3kS#J{S}%Q zz6jt_Dy&&!H)|}+G#6OdTUiR($~*MQY3P>A5%R#*KGn7t7<@VltZCnBCU0$5Sf|1s zPlA>08Ut_KOS8*+-l#Pe&bczFPdcMM+W;3?(=kA~x%R{UUrWx0Z?EJs#& z*Q^GmDx(OFG+WKCv^rPHY+rUF!O+f}{J4TS1N^uM&K7fDGl%se1HrkL3_9F)`O3M8 z%v5G!FyK(eYyR@Tas(hM=X+}z{QL3{ogrN1X&)vdxE@q-niQ<#>dn`vVd8T;aF%Zk z3*AdA*gHBrSn-k@XBjMrDPfV6NwJ-5G@#@faZN(pRX!{$9d91P-3@f}0#Y=EGR}*n z+_g2n-}!twGoeZGxP=6c?FCyMiarfbpazn;)m@WqcrS9QO!PN*xpeXzTU*E9)^vJJ& zs+K*WwSrZJZRNI@)0rHzc;Hsma~5s8fx|-WWEKhOMaA{9OmsiU3!Hn6@wS8HLEK;J z`9SkE%uVa!Ni+UY{)E>xq-*5%;@~q{j@fvizSo=cDWi^S+cDuc(Fp}&Y-Obx(!_j=4 ziXt(q^kIM6)=euqrz7ME#orTUJpRHP^R_k^_XiVL!ht_`m<$y4*|gqY3hUYxSu{wSw$;BJ!zZz>o!X(;r?W6 z+3I3dbRg)V%PZ>Pu)n%qE|Pp21J8Kg{B8*mJC$jrzXCb@Crdiyfy_!_(&M}w8XS*+ zdC>##YNY6mFAz*zS`OxL;zm)d#m}5vsBxU2RxgSeUnxz;`-}Iw!D%1iczph>J*k`Y yLsjW|b=cdcv?;t9rA(={J`Gly;V@+}Ek;p%=aLztb;<%fvZegdJ8s}VHt++Ni0Fd= delta 43 vcmdlh`iWmDIK;=8-N!Y|)kndR0SM%n`S=(Zfea>gHa12sQSRW4%@^1K!$$|$ diff --git a/examples/spacetaxi/generated/iigs/levels/level02.dat b/examples/spacetaxi/generated/iigs/levels/level02.dat index dea590378b780e5ff61a0e7c7938644c1f6d5b50..dbebde54cfb5666c5d7580175e47678faa75eb5d 100644 GIT binary patch literal 2749 zcmeH}O>PrG5QWPV_Y9*pi`-x&q?~{Y@M9DSlnq%DDxQ`cK@a7=z}Y9XF18v;rvj^lJFNqsetnbx>I81n)nZgrcSH+LLz z(OvIFZylVmpfhP{@Zw-orQVlyIQTD1}tEo~F)Cl3+oZNuNv9M0TpDJ(C^k z3fU#Hz;L3hKO=~yDkJ~Nai0G&{*C7`YmC&Ab=orQ`kOw*gz_GjN1s61ajS z6FEobmr8@HJxI=QIOJqKRZbj(&J{(09cmP@ICzxiy28}}g;Q9+Kp{PP>^K2aUo^{% znX8$Dq`&AWiTm=@2e{afliw+Wef2m@oGT%YWbDVj%aw{TXeo;`uEg6lo6TzeB&E3W xSd4P|?M$cq0=SC1yq8ot?`^evKakB|KK>~cg{|`c2*;xC2JeEq8@P=d_yZFt4gUZD delta 51 zcmdlh`ioyHIK;=8-N!Y|)kndJ0SNfG1O*uwfea>gHa2EP7FKRHVP0cVc9shpn=h~f E0QX=B$p8QV diff --git a/examples/spacetaxi/generated/iigs/levels/level03.dat b/examples/spacetaxi/generated/iigs/levels/level03.dat index 9b5a7033826b7eb8a3f31643ba59989ee02327b3..41ae91bbb408052b501566bdb8013a845b3f593c 100644 GIT binary patch delta 764 zcmcgoKWGzC9R9s$lDmqP=zl%QLO;O{QDI`)J2et*7??|ZzJe0bWf+$>*K z5>#&7QYy0xSLf#zk`cz>QZARxzJdVYS{_aTPu6Fkro(g(X5da2uJqtN{OH17A6DUY z9~wOvvFkch`(uay-Wb~5lxPJUQlX*_aP*B(E4`fToCgzXU)S#yL#rg>7> z5CxOjZbL*T;;awUw03@@yOEmjYo+$Q6;HL%iWXl?M%}j)-f?9@jpRHxG%rJKFfc+} zY@NMNb|p`zjj13}ssNxwEXiv_<*0 z*jy@@U!0}Q<@Kr-?qVyE`0J$EdWSTBKfvK6)dK!RcNfcb;d6Hz>B%&F6$xW9>Z4=B zbFH0U6!CHFVJPQSPf{+bltJok{73i-nWv0RlqVC}!9jfJOYIJ7$@d)Z9Py00crzLIMm-Kn4>#8yhPF2M;rg5Ua9?5X%W(5teKj L4o;1Y%@^1K8ixre diff --git a/examples/spacetaxi/generated/iigs/levels/level04.dat b/examples/spacetaxi/generated/iigs/levels/level04.dat index 74993ec09de70db8622005e7f4914b7e75932f3b..55b6b37d2503e565229c90151c0e83f1978d2bb1 100644 GIT binary patch literal 2749 zcmeHHON$dh5U!rslXMmY!Ee0#W;Myfk7_8si(u1A#t?i8+eg}u7 zou*xN9cBQtvv3Adc3+0=5nMFzNA!LOZ2;l@6_^?L1mC{E2Lr!EdKG>eabEO?;+%mS zaQGQ^nSbFU{5H^o{&(0kq9^)a#irT2*TDV*0(o;B29f>ST-ULfgx)*<&dU+;>Ch*M z=c`3@hR+bob2Z&;93u0nwXX5PK*L@kkNSI#7@vz*rv9vM{@6F>quwfcx;_w3D6DLp zy|ubacv??fzyHbOO}?ZLj{{N<29M1qtVEEt3i-~?5U!14s8NCyWMUrq+&?IfRy1|&5rR%h~TL)o>B>b zr>P`7SJO@E2+pV)FAOy774lA7&|KV~cS9(6g;a5U0QNu-XqW|$64HSMfRw=dSjGdD z+I|=FsQe{7*C~CIY^i%(FK!U_n|a`L7dBJyxFZLr)$E=wF851FW6g9NWKnFCr|7OF z?!H*XSU$yh5&fl7636REh#}ZB5Ex WvLM-0MO(0EVM5A@eKLWSPv9@&{us;v delta 79 zcmdlhI!(YWIK;=8-N!Y|)kndF0SNfG1VtFwfD9&fHa0G1E@o+IQC1F4W|jheL6&MM g7Us)}94y@I{LI$UeC)Rwgjo#~gjfwWHeX-|0Nc?E_5c6? diff --git a/examples/spacetaxi/generated/iigs/levels/level05.dat b/examples/spacetaxi/generated/iigs/levels/level05.dat index 8467bf0bb7a8d5024ef51fcdfb7feb0145747eed..02e66bed022342e2e1bbdd8a862150d75e1ec75d 100644 GIT binary patch literal 2749 zcmeHH&ubGw6rNo=i9-$|Z6G3)?H}OHgF+9f&|VB)S`b3jRUszUlLVCD?5$9-n{(4h zD?Nx468{0kL-Ejnh{U%36^5QXc=EkBvzys&e|QkR^kdoC_r33Z@6GHc^9!Xb9^(4! z%+2|QnbHl4JjMhAxY=!q~vSe+c&;0@CblJ=tmZJO4npyZ3s ze~S-Nfca9~-AasVwK5wAjYeax0?Y`{;IaO+UMJT6T~v^Xvpj_?AVqR6jsvq3oJW5k zu-XYB5~1a16_AADsMOK{g(<;cI82|SW2hqy7DY( zJk5YQdNAeqZ1xce$r*H7X5FoQAWH{HeXUu**6cU7J4pxX6Lr*`<{Rmv`Sd17<#>M9 zv^1hVKdjIh>Zf&3Xgo1(&V_W806q0Z|FkaA2mNRaqr+!&F4UQF;F9#y7WT-fLpC`d zZCCqgqo~&*KRqS&H9BntZV{EIA>z<2{<+&V>vyj$6Ht`-fcimBT zjE3+5$_a%G%TlqhC6ybtr5X>{q?!!ZWiS7v?;JS3nuEaa3E3B49lzI<{T!@A5Uk7o zEfg>i3|q3-l6`(KRj5sLy5KSJUIehNtf+~rKU%Uwgm*3Qf!w;-Ys*6IQ6$+qPw^7O zpeU8G5bO%Id`EqcL~307>;d5qg~Y3j7ft_VO~%)-E#XqHm#ZpRu6dV>esd#O-3+`- g#djM)cQa7jn=JZ_O;0m6<^9Y!=CzzHfA<7_11l4gYybcN delta 75 zcmdlhI!VAOIK;=8-N!Y|)kndVfq{WjMo3VUfepxFVrOIHy4+N`%MLhVCq;ZUB3kwOB;FzSa;AIY8 zJxY7n!u}KDB{vgI^YKT796edzo0*-RB-^G!@zft-=FNM*`Mrn zz0YN3^VS!q$ZOV{^*MU4sK6$rr}{UA)obetA1`}tur<^(yFMLC7E=AfZ?TEH+&9*KD1j7kG*@`QJhCpAP=YDiu*K5-v-Myf1I4;ceN1RLLnZC5mC8m(2*s!2w6}QUZ7jc?mD& zlq=4QnN_xUlO9o#dQU}pF+o|1hbLK|@E@r+%`>MaZ(>SWk1F_>UZT)~f^Av019QNy zLBO$dIa@~|EP-D0nkb{Ci4{Cxal-1fCFSf|}m6dwy(v{_4-!BIr z{WtFA)!=Xq3LP-mq1quE4cL$L2$Z{QY`3 delta 51 zcmew>`ioyHIK;=8-N!Y|)kndN0SJWHM1>icfea>gHa2D!0cK@c0cH+a7G{o(%@^1K D0^kT$ diff --git a/examples/spacetaxi/generated/iigs/levels/level07.dat b/examples/spacetaxi/generated/iigs/levels/level07.dat index 805246c776f4e9d3139e81976dc2a8730425a10d..0eb4025aa5dd7be0ba68be67b8dbbf7b0dd6fd6d 100644 GIT binary patch literal 3005 zcmd^9O=whC6h80Gym_Xv31SctDT9SV7TvUxRgi3wg^oYe&_xz9q-~N~f4CA+0{3MS z$<7E;ILKcM=I=GWAIuL>jjjc>#=)#qB=|Tc7nvFl_ocr^BGRE0^k~#1EeCIpg zy>FPaqm`p&veDtn@R`$Jjh^MK46ql_{IFCCf@Qc1@by9HgAx@7;M@iHauq&~C-Xs@LNUOncp{@Grdn$%Fd@?ZhTKHLcA0$w`jP!&b}Xd_lw7)NRAixYPxj zGsBloPN0-p#vG1NJguLGY$so80WqP` zQZyInv~8t5Zs%>k3ukw^6?zMv>H=N4vz=Pn>C?k*2pKM#`_Tt`kDqWhH8nBpl9`#A zkqN9Mb{)g%Ytz$Qb?Y#gpo;5;omy97(A5=&oYDic#KJKBMby@qv0{lWxW2Tu}l(X%nTSZE+%JToF3QHI(DO$ z$Ka@rd{&6_I4rQ=6XG!}SX5cDcQ~D+&rQVbwf`NeA2tyK(05Ro;4$YM)lCaiG>^bt#OC_1{EXtN$#TPi6h8X#OYb z3!?d4)}@HO`l48S=C_{4pf;p>BC7yzYIE~9=I4<2$oZpRtT4al(PQw|?ZFd`N21?r zdDYlci=qlIAZkF=UuYe9u^}59@?ohJFxhf*Q>tHtx-HZlq3S~YDjHAZR-y7JYOIO= zZK43Ai2HDD<@adGFLw>~dt1GYMY;9S!_tp_94uF^L`wnIm;YIptzfO?F9nP1a;^C? zQdflSeS}u7rrllJ`|5Bf3_{?u|Mnn y8~&^cH`Sg^M4lIqQMWm8<#;r5ePin;-^$2?2ZP@}9~%SMSKIF6E8_YGZ9P!z`(#G#3CZWzy@S7v9qzUGIO#@Gm5Zs%Zsuy$Z@eU Ti1V>BOq?h^aRJZ9RVNv{fd diff --git a/examples/spacetaxi/generated/iigs/levels/level08.dat b/examples/spacetaxi/generated/iigs/levels/level08.dat index 3d6d2733ec28d49c9cd5ec24da6b852a2913424b..08cdd0163b1bf720dc360fd2b38a6ed3d46c0418 100644 GIT binary patch delta 829 zcmb7BPiWIn7=N$%*JK@=TPvGAEcj>WK@^oy1Z9_rLXfUa3Jwn$>`;#pdJvW`Ln!mm z>>{Y61a+sqv6Lyh^pHy#L(7~w3`7w1fu(Dc8c>-v&JkgbA~f@c50 zl0dNY+;A2QGUq%sy!WP4GQ1C_Q#QOWrehk-uQC6VPB~^OpDlL1Lx#i2oS!$>6C9W5 z9XOF58A-D}iN3xtdZ=JbpJ=F-O4Vu@rE2GFcf37uO+h%ua508uB!nei5F|m2Gnj8n ztk{;sSR2K~HYW;vn<6}B(U&0IRUu%MkKoQ20oL8D5uC~FVM$scY!HD4gj;-R(6;2k)?ix$GGG^g_~jyj z@;&+dIAu#FDgUvxK*p^C&~OW2$9HUDr+9Y5U9O7fl?&BKf>Z9Pn00jJul9CK;Kn4>#8yl-U8;gRhD2o8UC|fnND2p^B L8w>Zw<_qiqD8mTS diff --git a/examples/spacetaxi/generated/iigs/levels/level09.dat b/examples/spacetaxi/generated/iigs/levels/level09.dat index fc9246e304891ed5249d67941bb6d946a311beb5..64517963b02417574ab3b8291467b407ae1c9ec9 100644 GIT binary patch literal 3389 zcmd^B&1(};5TAX?W|Q@UqE$Q;*^8iv(u03MKr9Fa#nz(`TU{H!11(DNrJLIH1E~mt zo`R55H>h}PduS-)!FY-{K?FSr9z;BN@t}3)ee6e*Xay0$NxC!d_kQ!6nYWv=1B3mC z9J0eFjtva-9X)YGtQ?TWtW?UfYET2Xyd8Exg5ZPDHwgQmlE0z+L-O~a{tWIuHFg>Y z9~!;S$lp+Y5WSLYaW2}#P{dB3L(4^gp%QUi4?pgN?62*$Bq^?YrtNkFJSzm<2+|>x zbvtZEU~0|XpdK+VbesfdEf9n3-=D+1*y)ax+^cekKR(As&)M6^{rJuM=3eSN@0GtY zFUKmE@ozHn^f?Zd*7KeZw!#D8c}b_|)N_Z0DK|WP-pxw{uX<0M3#p=wjJQW+gxB%$ z@jgOAyN==Lwb4<5M!OQ3!LChaM$JNtu1!I)E&SMarqw_}wxZa!{$~>BYF;KI1+|uB zhg`9iOCmDjX+;P{B&K+bn2Ln#2tTGyEN!GJv!hqND?C;c1>5jMjv^BY%e2*_DgZ=e z(f*<(1w~$vgr?@`KRVBo+Dwb*ilSl_mD+P_|(!x`g z=U%XymosRDr13C7B3e+9?1Ia8gNVzGP&BK3kfRw)5+aw~LZ7#MIBxAn*{z4*`%T+BODoea_yWq3RgTjlqtobSDug&)~bAfs` zh~mMNSDL&z^kwLavBtiar)Os8W@o1T#QY+EyvSFU_~+dXqnYTxRctVH^`Z~+OZ?L! zf3(OO>E`QPb<)kzgW&Y2eQ>fkUUYuf%>-;S<195fKu1jq_MKfcjXPtZZ9Pv00ctpqQVT!Kn4>#8yhrbcT9QjF0Z zCzoxrNm|wiOnUr?Wwrx0^RyhgY%3!<6s&vU;t&>+;+jI9yNYAiUBKJj`p% zDI!pp*OM=YTWXL-MFPgOSb2`byIXm8<2CAdjgQ=wFX!FE`+hL6A00Jb6~NTgRF8p3 z-6nB6@!`Y-N4*g_wIEM;m5^y@Ay83b2O0Ps3V#;44;H^87Hi-Mr$ZJ;{)7|UU|ysi zis!`eJ>G;W7*D{V%^`Vk2p%sZhVyG|v>~a}o6@{?e=mFn?0)#CA%Z#S*to^4n(~;N zF?>7`Bd|d==lPx_UYBs3>+y(M4ke`4=m~7e= z<01pZLGCT6eJj!!m7Yu=i>Ep|-;8zk-$OrB*N1m;fKK)yzroR4mB?kAJPj}&$Uhra zxd^s{;v{JCMC)d^_BkGaL@QB!9BzU7gPokURj}t8_3Z!&fYbaF)ip`eS|Z1xZIY-j z6+n_J5Fdw4NX*)wMC=6EIY0p~654PCI-IxxCjmiB-zI|!8ihzjZyN_SjSy5=1NbF& zRj;I>a*^qAa*yB(9N$ozA?#K_3j<^dWCfz_Ag39O){kE**#)3FtXy)~ECed>+Qd+g zhPC;OU>F?;%)1!&e6XdlkC1i24aD*ccLsJq2ozW0XhR4TPm&5=7hL^QJPJEB8Y&AA z3+sCcMg1b9_i#<7QR$q2#&=*8+%q+s{U1ym|&*3Y@+n0Rg&qW z`jvgFbSF1j%I40|=k#y)tU?JxlC?^X*aFhFje)n%uegtQ2DchR{s5ZB=v%|HdMS| zu4Rfjvt+Z99xrCi5-DCWvB=a3Yh&5$BZxmss_MqNMIY8n1nXsT_(6xHy-0m)>tr)Oq%E|*`pxOi#lg1P?kr-6mib9O7fl?&BKf>Z9Prz`(%Cz$hunzzSqBv9qzUG4Qf5a|yBri;A&a;1Od@ RmJwoRU=(9g*w}o59RN|43Dp1q diff --git a/examples/spacetaxi/generated/iigs/levels/level11.dat b/examples/spacetaxi/generated/iigs/levels/level11.dat index 4c3ce870f5b7044a002cf19e29e2ad0a95de9b80..8099ce8ce9459e8ce08a46bf9bc6765d1bc06dd2 100644 GIT binary patch literal 2749 zcmeH{ziSjh6vy8#J2}^boivg{2?tj8)2JeJRkx{m`Lx1hqgLTR3#3;&&ZpN}lV#)et- z;b1VhVlY$Kb9DR9`hCuZT^Y*wszQsc9ponj|@ z6qn$`AXjv4Jv?S;c#(KsJ!P!n^rkA)fpC100F{zlr|Tz8GgNdtlYLI!xw( za|ux;MO-fd$!ML4vAiu{nL!F|nf7HV8@ml%J+n(~e}t&LL?oE}c97=^*zyC(g+Y9Onl90fnI- Am;e9( delta 59 zcmdlh+9Y5U9O7fl?&BKf>Z9Pz00hFU;-U;cfea>gHa1oU0TyNsK^A8LVU}hIK^6uD L0Tzvo%@^1KIVlOV diff --git a/examples/spacetaxi/generated/iigs/levels/level12.dat b/examples/spacetaxi/generated/iigs/levels/level12.dat index 6a5ba48d7fdcee5d1e495fd03ded99b50e7a2167..50b1b33da5a0db562634a96ef2aae124c770bcdf 100644 GIT binary patch delta 814 zcmaKqO-K}B7{{NP{g_=$8kPJc83O4Pc!<~vbTTrlkq&hWYDO2?-9t1%*5#cUKPJ3P zxUGr^LM$ve(MozN5f<~1C9zP1mq6%)AX)~3hz>qI@2*~Z=Y4oS|G(!w%tLv-W%$~4 z?WDp_pLXv26=mS6cA=UA)YSoiC`F^uND^)X^tVGhgs281@Ms%`H{c!o-iAM$LZ{HL z2^|~UPm+gb=$Qm{lY5PPE-NW3r^=FD7k0Q@0g(b-7JD7VhA>Wta6`c0B92l?RdDtJq@N&zUH~i! zrQN2zI}O;KK<6};=(b^S6nEPZ2V`TYLj`vG*_M>6p8rZ;8!&%6hwKkhlE z?|2KQFL+r~uPm6w1#@en9H|w@6F-^h8g54EvwS)6)$nqR05hzRkz0Kn%rRcT!5K!i zN{*F7l^IsNN0OpzULG<08>Dd!y9PMK0ka%1cL11WynusqjB1rx4wz$l!H}bu&^5jp z{nbymt>u=xfuIlMlS*7oC%sY2yKniUZpydZv~ogC>B%I(82l0iK@7)Uv4QpX>}g8r zP<_$O*vh=6-LeWpt?3W$P$82w^z}8i#7F!r%`CrUlz*zQpq_jy`^*;Cm^Nnl;^Hbj zmnR=B-bo7-Z-s)kr58VF#oHPqrF5!!N$FN;2TiZCgNCmVoH|FUt$d7{E9_1AuP+r> Sm`;CZSMG__{9h|YBL4tZZ#uRB delta 63 zcmdlh-X>rc9O7fl?&BKf>Z9Ppz`)JT#Uv%gz|6qFz{JkR#>Oei>MbV9`apq|m05v@ Sm6?HuRfB<*RbylG1$F>P#|e7? diff --git a/examples/spacetaxi/generated/iigs/levels/level13.dat b/examples/spacetaxi/generated/iigs/levels/level13.dat index 459177ba1eaea6f82b75b1c0592d26b34e9a1ee9..3eb8f9da136d0e2b9cbdba81a9d5991030aa7569 100644 GIT binary patch literal 2749 zcmeHH&1w`u5Uv@W9p?w~074k>?9KC%RTC8!gk%LzA_U50{ntU`9yGaln>Sd*H^>7B z9+rIuUqJ9N#H#A*>7GB>gXkq6%v4u>Usu=Hv+TiadZU;<*uQ!I`mMW@{X4t2i`nGi zy#inagfJeB(qsbX0OLK_F~#rjV+r3@@C$yg;pYk-!Min_tl=|!S;E(KupNw-!H&dI zo+tp5w${J420XFTCs zdORQVs`7;F;}xqqd#lr`>J)o)MURZm?QAWo({4?5AAP33AL57F$GpN9SP}3ksu}k2 zvgL!E3Pjp)-JVZ{68upHb?v)p`K3mrFzD|`v*B|c%;@kLrS{9%ek7|K*@jY5A>tu} zo%ZXhW1_De^35oYj&Rqmaj{tJAyphsXj#55OR4&^BUfrR?I0#}ifNh*0em7&`439r z6G_t2)ua*EX(G8zo97Z`3F^nNX_{U#Fa_bfSCS~*>}Pj(O!}(x$e=KxU$uwTBensT#a&!GE2l{SSB9ZE(FyI2MOjGG+ znJv38QBxVl)76cfnoG7{#9(Sbmk^ytX>?f;1wjz0|YR!v$1io2QaK>V6YQo5Mp2h3EMG* zxiCZ=VNheRt6@++#GuFEwueFc5W_r%ZATb(9AVhTaIS^n!VyL}Mzaz|iz5vC7_PZ6 s>^=nIA7PX}1T`8=v4F^pb{E(=co`DFVDdp$(a9HB*+%iehXEr402dank^lez delta 71 zcmdlh+9zNa9O7fl?&BKf>Z9Pt00cseLShVTKn4>#8yg1;3$p_wH#0YrFtf0L2=fAA Y9_AeiT+9k0EX<1)M3|X3HeX-|0E5#Cp#T5? diff --git a/examples/spacetaxi/generated/iigs/levels/level15.dat b/examples/spacetaxi/generated/iigs/levels/level15.dat index f0912390c12055cd84f9e54c7237f4abdf5e3598..7607e7d5f1a4666d13c740c55331cc4e762fd932 100644 GIT binary patch delta 403 zcmeywzgN^cIK;<90R&uqT%ALL{5@TQ!CVFiU}9%uV`NNW&|_pU5oQo&UtuTu!y;DM(w&8wR^MbR_slyTeVU_LE2#vyMm&_ zxf!(r&k_*?EFj}DSv}Tf z-UO-5WSyJAx;OLY+>D!0wS6};diz+{t^^vl5^UT`wz(_V_O84&cLmJ2b3o(Hu_ef{ fitw{?@(M0u-}^v2tL}mJDS>YZXkLc}0mU=`v~HIG delta 43 vcmdlh`iWmDIK;=8-N!Y|)kndf0SJ^BM1>icfea>gHa13PQD*6l%@^1K#AgSt diff --git a/examples/spacetaxi/generated/iigs/levels/level16.dat b/examples/spacetaxi/generated/iigs/levels/level16.dat index e6471a50d2b3723e2d5ebd91922044cb2528bef6..11f416df153d6ea08842786a48d1896c41c4dc87 100644 GIT binary patch literal 3197 zcmds3PiP!v6o23B&g{$v>O@Tygvk(kh=?~21&fT54Mf(9=2U6iB+WKOJq$!r0<$xj z?GzPKyd))(#jsFt3M;*OSdrpoYYsVi@t|jsbPHlFJ*@A0-_Ex)lS;Mr)W`C^_kQp9 z{(avj?0fUo*KCrvs&i-0zWMHq@BqdDMJZ`op>Q5f0lYl{6QDs6U;!Ej@cu3M3I05Q zzkXmAd*gt;bc^#JM`}wsS-gO996cu{dz4;yx_Ep4Zr{D#FCRe^_mbxwFaJxFrxlzG%dAZTTxrCMw6Q6EA#iNI)la@-lS>E!p z4A{$@Tqi9y6fe3^Z#J8Cl+Q)6&*}n@t~5AlC?6(h#@lOKjfU2hc*`x!8k>kmrNRX~Ue&S~PVkik< zJuZox<%t*(gi2^ejwkWFG32STL7pId5InrDhsQZe6L10`J6kZ5Mav)$`J9)MtVgbWb^wUzLd&H3`TK)ylZ4|hP)>vYH=ty zfm%`&MNg!pj*X3?!vT?mXzp91D5K~zr9f;hv=Roml5!(}8GswW#-ViQd(>znf;f+c znUu{@eh4aa6bRIcqlY%DK=HIQd8dyWZA1{~QRYCHP9hr%mtnsMPC48kXQpUkn&t5KL{(JP*@4n5Y$S zE`JKP$$u%~M=1kN=9JBV(@At_^L!K&bg;RB!lrbGPjw245vB#i;62PK>O`bO**Hf^ z99>~aAqSKz0pOQqIAMw=P2G{EG)xG@N{G~IwfIc58aJYg_Nz{CwsyRuZSSyfwBvOwbxzx|)Q-nbPwlL0c&=-m zAKhrBUD8%=>b&T^YNgTj+}`Og?siZ5J*EF!tnj8`>za%0Pxx`so^Y;O{x_CiH5!8f zz-H~c=UBTSFih%OJ$6{A-fZ*~JZ?8=>LbhVSbk4G{JGXXBc}enJwPK`j3HWz&SSkU z^P^>3bFMG8K5?%vwIVkTc-t3Ro*T2)RTl@Tt~9#W+&PB$%dD?;?>FpcotWWF7&)5s zJCi=*1x`ERzj%@1#67iN{`y@rz$8IK;=8-N!Y|)kh(Kfq{XWky%ufg$>AJVrOGx<`89Z78hlCpdiY^ys`NL FI{*&@33LDe diff --git a/examples/spacetaxi/generated/iigs/levels/level17.dat b/examples/spacetaxi/generated/iigs/levels/level17.dat index 4aa266c2696025cf131931f6561b0fa4fe95cce8..457defe79466772cf3d5653e69b056e82e7aa772 100644 GIT binary patch literal 3517 zcmeHHL1-Lh6n-<=$?lphNkB9~DRUK3JV+@t*cIInAylb771U`6YNKEzcPm-qF zG}#@om$smzu(Xg_Oh`*FHLNT#2pcbJyLeC(sUQ-e2EA3;^}T=QpP9^VRu6iSK7Ri9 z-uK@3y|;fh`{I$&CsnlP4jsuH9?l%j9C{{0IY1Af$?@*)MEn9=2N)iNK~M;sf(xI( zr49H9KC8nA>+m=XUxr85;S2cbL-=_^+AHn5EbU*H9+39crTrW5Jv8d@?K>ofF5C@kFD$w;!h4Ul5Hw=+uA80kK*RNKdZ}>P ztez1u`C%QAz}~@d^3>!cM?<@-Iw5{lX|Xt33#jnlnD7ro`}khpfA}sknkV1!ojdmh zRX?J@mlhu&fObY=g?4>m%0>8mv{z(-@mV;_vY9_k%L4d7FS80{b3cpLMhydcL(7}}3Y?O~p2S|d>ul&X!LdG7! zKi!^xAPs-?L9<3T#;Ay#@DcpE7Jdwdo`s>O;m>|(enmINh@@g|`Da@A07HDeCP1?S zc;hHU!ox%R=i2a*fQQKZxaltZvr#^0b~U%dhlCIhOJK@vKFW7@<0}7T{<${%UVv$k z4pyXQ1#gT2k??SL24^lr_^}kkdZF)m= z7k<7ye~W+f8shu6`}YSl&1qT>*R*21yv6J}bLqx!%)V$i-S)>u`6jd9Hp<^HJ7<({ zGn*NXWWQ%@{vg+GYB+e{XzA`#-U1T(`HRyh@wM>hDb}5snLRysCUJywbf$7y$IMNrkEOHP_&C5U{4Pt997}x8M#{HX zDXTt|cI4b~^^!UIig{&hV4|KIyK?;eF|EADmZ-xyk1Z>oGR(ib!?yRJA7DQ+dyS3G znvQH=W4JLUUz=3HkFmz8UE708H-5nhU%DDub#J;DSD#4Z4D7zfM8Q!}TvUn)tX*ZB QwYyGy`6|=s?f#$k7aEx~yZ`_I delta 71 zcmdlh-6vod9O7fl?&BKf>Z1_Iz`(#Q%p@tvzzSqBv9qypa0{|D2#B#XGYK#Yi}JHv ZP!M2NkQZZ7kPu@z%qhm4v9b9AI{>AF3*rC( diff --git a/examples/spacetaxi/generated/iigs/levels/level18.dat b/examples/spacetaxi/generated/iigs/levels/level18.dat index 2303e0d174f825305fce854af5d2c2c6fc04d53f..b682e46e550fc6503224c59e6f721d3bafc90ac9 100644 GIT binary patch literal 2749 zcmeHHJ8l#~5bfFN*=AT+nH$U@2ylXA39n=cA{3$#+x~YoDq+&v)4xLY2e@9_!+6~7b_+{P!h70o-)*Z_N Ffg6$c9^L=| delta 43 vcmdlh`iWmDIK;=8-N!Y|)kh(S0SF`+1Oylufea>gHa1397M6gG%@^1K!`lbN diff --git a/examples/spacetaxi/generated/iigs/levels/level19.dat b/examples/spacetaxi/generated/iigs/levels/level19.dat index 76c0dd3b2aa448ae774063f34f1f05a3fa888a12..7a9076126fb30a6749800a89cb49b90ef359bf92 100644 GIT binary patch delta 764 zcmcgoPiWIn9R9tmZ8C9KhSSPGa_vD7@g|IbZbaxoSV7pubOcFuP~xGUelP24HviDE zo#rJVfk_kWu%nkb;-U7EdI>5X1ZB*lFpA!GDE?lDXK(x9@%w%6`}n@cTgk^$?tHm) zT^*sU&#LoxZ3-~v=lnC`+1-0#4RF1&{y9XRO0 zD!lGNy$eG&XELoPtp7ts7~0oT*_{MKKriOKid8%p#cwcMF+gD;k!9qg`3-%~i4W07 zCuIJ*(?5{XPX-Bvh3I&OrU0X}w&gx_1(7m2Wx2Lef8wpKJ$?54#q7}#TSF|e#t;Y0 z2;{iV3pR88I#14~*$`-H?)^r8YZQKH=&d_WdcK8Dvam6cbl#2!XVh^mQSy>y1Q}YR zz7@NC=i(!Z(*u)kY_n12jmJE!dRE9iTfL%LhG_!S;E%+Zlu zE-&5V+lvMJi?_JFv{^CYeQXL#xJjO!cS!vE5e_D37Vszf`&g>+Q1W+?o=oH`7mCej zPsRq9ntQ*f;^Rq%rC!zoS-q-J2f26gpAf1PK2ltg0)@mj_S54~Zne=6-*;Mga9Twt GJ%0h@*~1_J delta 59 zcmdlh+9Y5U9O7fl?&BKf>Z1_Mz`(&S#3UrZz|6qFz{JkR#>&9K!^|SYsw^VJa)MWc OC0mArQ)6TE1$F=*sR>8` diff --git a/examples/spacetaxi/generated/iigs/levels/level20.dat b/examples/spacetaxi/generated/iigs/levels/level20.dat index 0c7021577c93f23a6cde8696bc6d8b5e34f753d5..396008f50e492bb856f9d155333d16cc0d656fc6 100644 GIT binary patch delta 438 zcmb`9ze@sP9LArwUQdUHBAtS`AVPwmp{>I*NQl&{G^faeAZ&aIg?^kax>Vl?1edh^ z2FZ3?jw5pOFKCpe!^s+gdaobUAJ79|-sgGY^SKmm?X?St^($sHb4M^d62i($ z*N!Vyxj(9k=u0jyIyLFC-V+i?3i(P(7j9B?hh1G*nO`Cp1jgnEJ{ArGQ+V;QSnRu0 NH~Dnc;GuswZ1_Cz`(#JBqXiIzy@S7v9qypurRYIOA50va5J++bMi2I Zi!!rZkli_ko%ZIKS{gMts~Necx* zQu?MZ!4X87K}8<}t4P^{-NVQ~Ac%sXNa-F#5Otm;G7&^7cyg2T>-)ZQ!@c#%Z#(eP zW%Y;>*~QZ*^($I7P85hukR@qsY!Q|K&ZQs)0zGg8s#&mB;2>y^V73QMc)Sevdhngi zEHh;V-Z3c)Z+kx_{^}B8_u1h<#-wN+2e=8aWWn=jQ9vmbN3>eZC-O$LC>WiOG^9=f z41>9yodQWalAYeJup*M&}wI&UQ@RpL)8_+sJ9$);M{c+d3a;JP-%u zubW7(`pWdc=u~hhtyNGps-0DyW`P(-b_o~3mA93!#a@QmpI$)J^S0U*^lmpw9yZF# zfs89EJfVPM5t$SkA*&uPLq3)+AsQ9O7fl?&BKf>Z1_K00ctJ5)w>IKn4>#8yhPp3v&jO0E;l6FmthtFbjty L3(M(^%@^1KFvV5h(!2N9HG zFmqv$J_O{NodMB#3~eq9*@qbBG3-0TF#8b0HHLRC43`fvsxjJ~VRSeGqFsRWZ1_Gz`(#J#3-l0zy@S7v9qzUGq5l-Gl()XbFnZ73y88b Vi?cBARupB{P-J1&*w}o59ROcq3IzZF diff --git a/examples/spacetaxi/generated/iigs/levels/level23.dat b/examples/spacetaxi/generated/iigs/levels/level23.dat index ccf8623dfdd5af9218af3fcd4073bb39333ae942..23b1bec9e7a2f3fa145528b102e258ca124dcc81 100644 GIT binary patch delta 764 zcmbtSF^dyH6rS0H?B-%fkf;$QOshl?v9J(X3+0LwVl3>O`i zhq`r^5YD-z;t;rMt`d_{5*5#*;!RXw@ir>nMMbOKYU65Yh$KQ!WtrMqT#ILtnIt`? z$G9lb3>{kFM}IA2$0C^k9-3&@fWyHwGG}Q3Dk{ zIO>Dnhk+69bN3Yb4I=4ZD3=tu;0rig&tYE6ulvvHq733ueT4@JuuH)L*kwuV3hL~o Yw6gHa2DkK^6^uPPS@(5w_}$%@^1K D{q_k1 diff --git a/examples/spacetaxi/generated/iigs/levels/level24.dat b/examples/spacetaxi/generated/iigs/levels/level24.dat index a499a94d1b4acd5d005ff8ff570cc2856e66aa1e..13dca31b4b3937c07b17df8bd08a25a41909d135 100644 GIT binary patch delta 764 zcmb`Fzf0sm6vy9s>qLx2PLQ>8{0m&cT5S}I^#>@YR>sxlEQEl9&AU`7b1H&byJnhmQL_ifmb%+mSS*|@c3L_!Nny3seT(_dB=0-#Gs%3n#E`qZQkvjWiwk^Z zZide*kHJ`c_hEYIj16LrkofoUPx* zgx050_$>}_`0MAYxXV+L7e~Ex+$+`?tuw(5mqrb7cTdT^IC`Mt2eH8T~(+wqDgNx>7gcF)ubb>_S4SGpJJkSVAZazKj4l@TsP$%Yo^`1 zXC?OE6g#J=`xJF0CqFYEoUQ;raocS@WaHyp<6pewG;($Y(xTXN{4MltC*F7bGWr|N mt}RUTHbp;rZ1_Bz`(#Q#3U}xzy@S7v9qypaIvyBaB%@KKM;$sHi)va Tt`_A2Vi6$bXI;Iq`2srtvla~d diff --git a/examples/spacetaxi/generated/iigs/levels/title.dat b/examples/spacetaxi/generated/iigs/levels/title.dat index 9887b563cea400c708312b014cc0a86ce2c0f27a..83ca545328d4c48b3b960ce6b48517eb6f470f02 100644 GIT binary patch literal 2749 zcmb`JPiP!f9LL|x?99$gw%NLWO4c0u@J~?Cq9|o8D5;uY>yT{sPl~1LM2f`Y2ssGu zTMrHOknKfK%d)6FbxJoB=_TM^LMa+z-9teHQBPJ64T}enf~>#adv7*xveo9`FPk^- z{od#I{e9n?B&1UHo~1lWwNt0OnX*_=B0^7EmT7)XJ|^Ua0x1v+(%J#-a8)Z_*DP&n zOMC1(Y~I)+A8nAsWMPXuvq7fFY?VB`Atmnn@ZZ`~EWXaIorR!$faO_C4KsX2-{72> z;|SkSAX!m?Rg%MMDZzy}id8(XaF~)>Q${kImXc?I-Et^?0bHGdtg;j2t zyvGE&+=NMy0Xstx8bcWg>O0z~fV`iU_HF3R{=TFXY(izX@GI=BCq{jss=Vp(U zXXuNyYIS~&=F(I*lg{`dt_WNyucw@>t(e~NHyy>B(LKW*kHl9>CgPOTJ*TH5Wqghh zmHFA~$)jMJYI_X_{$yA-1koYz?}v^lzQ93uFf@%omhU*2(FuHD!V7RE133rwP>C1w zQRf6t3puU_9M|O41V`kc&xoF|fdr_cIVq0$9e=3D0}mmmQ6^dne+Wop;xw=g#{ttZ zjxUhJZ3*HP!1pKz-ta!8Bbe^yhMqXbCAlha!!5X%NLlLbw2u2ri5UCWkbt7+$dvLR z(YD$bTJj50hsaf&OKFN0Orb)83b%{l6%iJlS8Wxa5B1`mMpm*jyk9~_Hsc9wjz0(S z?8u5M0;g<1D(+7CC6gOK!z{Wk{v`N2A!B&qQ9^mfq!nf5E(L9nXqS));`{zZHzds} zYvVg~ z43}uGl}x%x$2K%;;3k}b>*xbwI|I{6S_24K781WxWqrf@fpyLVea-Kbg8rk$-!<30 z941y4yN|F->v`5secJR`Xu&bPFWGN57JI+?oz38L)4iJbuy@yAtui>c3kS#J{S}%Q zz6jt_Dy&&!H)|}+G#6OdTUiR($~*MQY3P>A5%R#*KGn7t7<@VltZCnBCU0$5Sf|1s zPlA>08Ut_KOS8*+-l#Pe&bczFPdcMM+W;3?(=kA~x%R{UUrWx0Z?EJs#& z*Q^GmDx(OFG+WKCv^rPHY+rUF!O+f}{J4TS1N^uM&K7fDGl%se1HrkL3_9F)`O3M8 z%v5G!FyK(eYyR@Tas(hM=X+}z{QL3{ogrN1X&)vdxE@q-niQ<#>dn`vVd8T;aF%Zk z3*AdA*gHBrSn-k@XBjMrDPfV6NwJ-5G@#@faZN(pRX!{$9d91P-3@f}0#Y=EGR}*n z+_g2n-}!twGoeZ 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 0000000000000000000000000000000000000000..d5fb5c44cc6de72528729819c53ccb52ac3cfa69 GIT binary patch literal 148328 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 z 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 + +#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; +}