From a9ccb6e5c244922ea02e0c4b8ba38d1776b0d197 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. --- assets/genC64Data.py | 160 +++ 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 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 generated/iigs/levels/title.dat | Bin 2028 -> 2749 bytes spacetaxi.c | 1083 ++++++++------- spacetaxi.h | 348 +---- stAudio.c | 331 ++--- stC64Data.c | 1370 +++++++++++++++++++ stC64Data.h | 62 + stDemoStreams.h | 89 +- stEngine.c | 456 ------- stFare.c | 451 +++++++ stHooks.c | 1001 ++++++++++++++ stHud.c | 82 -- stLevel.c | 122 +- stLevelFile.c | 90 ++ stPassenger.c | 318 ----- stRender.c | 1810 +++++++------------------- stSim.c | 1291 ++++++++++++++++++ stSim.h | 366 ++++++ stTitle.c | 320 +++++ tools/stsim/Makefile | 11 + tools/stsim/compare.py | 79 ++ tools/stsim/stsim | Bin 0 -> 148328 bytes tools/stsim/stsim.c | 110 ++ 123 files changed, 6632 insertions(+), 3347 deletions(-) create mode 100644 assets/genC64Data.py create mode 100644 stC64Data.c create mode 100644 stC64Data.h delete mode 100644 stEngine.c create mode 100644 stFare.c create mode 100644 stHooks.c delete mode 100644 stHud.c create mode 100644 stLevelFile.c delete mode 100644 stPassenger.c create mode 100644 stSim.c create mode 100644 stSim.h create mode 100644 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/assets/genC64Data.py b/assets/genC64Data.py new file mode 100644 index 0000000..c0feaa5 --- /dev/null +++ b/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/assets/genDemoStreams.py b/assets/genDemoStreams.py index 9517557..5095393 100644 --- a/assets/genDemoStreams.py +++ b/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/generated/amiga/levels/level01.dat b/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/generated/amiga/levels/level02.dat b/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/generated/amiga/levels/level03.dat b/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/generated/amiga/levels/level04.dat b/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/generated/amiga/levels/level05.dat b/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/generated/amiga/levels/level07.dat b/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/generated/amiga/levels/level08.dat b/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/generated/amiga/levels/level09.dat b/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/generated/amiga/levels/level11.dat b/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/generated/amiga/levels/level12.dat b/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/generated/amiga/levels/level13.dat b/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/generated/amiga/levels/level15.dat b/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/generated/amiga/levels/level16.dat b/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/generated/amiga/levels/level17.dat b/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/generated/amiga/levels/level18.dat b/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/generated/amiga/levels/level19.dat b/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/generated/amiga/levels/level20.dat b/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/generated/amiga/levels/level23.dat b/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/generated/amiga/levels/level24.dat b/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/generated/amiga/levels/title.dat b/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/generated/atarist/levels/level02.dat b/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/generated/atarist/levels/level03.dat b/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/generated/atarist/levels/level04.dat b/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/generated/atarist/levels/level05.dat b/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/generated/atarist/levels/level07.dat b/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/generated/atarist/levels/level08.dat b/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/generated/atarist/levels/level09.dat b/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/generated/atarist/levels/level11.dat b/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/generated/atarist/levels/level12.dat b/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/generated/atarist/levels/level13.dat b/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/generated/atarist/levels/level15.dat b/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/generated/atarist/levels/level16.dat b/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/generated/atarist/levels/level17.dat b/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/generated/atarist/levels/level18.dat b/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/generated/atarist/levels/level19.dat b/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/generated/atarist/levels/level20.dat b/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/generated/atarist/levels/level23.dat b/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/generated/atarist/levels/level24.dat b/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/generated/atarist/levels/title.dat b/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/generated/dos/levels/level02.dat b/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/generated/dos/levels/level03.dat b/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/generated/dos/levels/level04.dat b/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/generated/dos/levels/level05.dat b/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/generated/dos/levels/level07.dat b/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/generated/dos/levels/level08.dat b/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/generated/dos/levels/level09.dat b/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/generated/dos/levels/level11.dat b/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/generated/dos/levels/level12.dat b/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/generated/dos/levels/level13.dat b/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/generated/dos/levels/level15.dat b/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/generated/dos/levels/level16.dat b/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/generated/dos/levels/level17.dat b/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/generated/dos/levels/level18.dat b/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/generated/dos/levels/level19.dat b/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/generated/dos/levels/level20.dat b/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/generated/dos/levels/level23.dat b/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/generated/dos/levels/level24.dat b/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/generated/dos/levels/title.dat b/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/generated/iigs/levels/level02.dat b/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/generated/iigs/levels/level03.dat b/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/generated/iigs/levels/level04.dat b/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/generated/iigs/levels/level05.dat b/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/generated/iigs/levels/level07.dat b/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/generated/iigs/levels/level08.dat b/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/generated/iigs/levels/level09.dat b/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/generated/iigs/levels/level11.dat b/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/generated/iigs/levels/level12.dat b/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/generated/iigs/levels/level13.dat b/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/generated/iigs/levels/level15.dat b/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/generated/iigs/levels/level16.dat b/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/generated/iigs/levels/level17.dat b/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/generated/iigs/levels/level18.dat b/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/generated/iigs/levels/level19.dat b/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/generated/iigs/levels/level20.dat b/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/generated/iigs/levels/level23.dat b/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/generated/iigs/levels/level24.dat b/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/generated/iigs/levels/title.dat b/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/spacetaxi.h b/spacetaxi.h index 791f05d..07fa63f 100644 --- a/spacetaxi.h +++ b/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/stAudio.c b/stAudio.c index 4f06c01..5ac786a 100644 --- a/stAudio.c +++ b/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/stC64Data.c b/stC64Data.c new file mode 100644 index 0000000..53b7f05 --- /dev/null +++ b/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/stC64Data.h b/stC64Data.h new file mode 100644 index 0000000..69751ab --- /dev/null +++ b/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/stDemoStreams.h b/stDemoStreams.h index dc71e8f..a4baf41 100644 --- a/stDemoStreams.h +++ b/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/stEngine.c b/stEngine.c deleted file mode 100644 index 2c766c2..0000000 --- a/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/stFare.c b/stFare.c new file mode 100644 index 0000000..99e1d76 --- /dev/null +++ b/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/stHooks.c b/stHooks.c new file mode 100644 index 0000000..89db67b --- /dev/null +++ b/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/stHud.c b/stHud.c deleted file mode 100644 index 459a3b8..0000000 --- a/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/stLevel.c b/stLevel.c index 1603ddf..fb23c27 100644 --- a/stLevel.c +++ b/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/stLevelFile.c b/stLevelFile.c new file mode 100644 index 0000000..dc59814 --- /dev/null +++ b/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/stPassenger.c b/stPassenger.c deleted file mode 100644 index 7aab2e7..0000000 --- a/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/stRender.c b/stRender.c index c15eea6..276183b 100644 --- a/stRender.c +++ b/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/stSim.c b/stSim.c new file mode 100644 index 0000000..daf844d --- /dev/null +++ b/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/stSim.h b/stSim.h new file mode 100644 index 0000000..759ea94 --- /dev/null +++ b/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/stTitle.c b/stTitle.c new file mode 100644 index 0000000..02890e7 --- /dev/null +++ b/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/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; +}