From 477d39f7e9806bb62ab2d605afb87558b0a7e92d Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Tue, 22 Sep 2026 18:12:37 -0500 Subject: [PATCH] Space Taxi is finally starting to look viable on the GS! --- .gitignore | 6 + examples/spacetaxi/21_!.wav | Bin 0 -> 33460 bytes examples/spacetaxi/31_1.wav | Bin 0 -> 12986 bytes examples/spacetaxi/32_2.wav | Bin 0 -> 21446 bytes examples/spacetaxi/33_3.wav | Bin 0 -> 25554 bytes examples/spacetaxi/34_4.wav | Bin 0 -> 17962 bytes examples/spacetaxi/35_5.wav | Bin 0 -> 20456 bytes examples/spacetaxi/36_6.wav | Bin 0 -> 28952 bytes examples/spacetaxi/37_7.wav | Bin 0 -> 18852 bytes examples/spacetaxi/38_8.wav | Bin 0 -> 23360 bytes examples/spacetaxi/39_9.wav | Bin 0 -> 14764 bytes examples/spacetaxi/3F_x.wav | Bin 0 -> 24876 bytes examples/spacetaxi/48_H.wav | Bin 0 -> 18072 bytes examples/spacetaxi/50_P.wav | Bin 0 -> 24446 bytes examples/spacetaxi/54_T.wav | Bin 0 -> 25694 bytes examples/spacetaxi/55_U.wav | Bin 0 -> 25264 bytes examples/spacetaxi/assets/RawTitle.png | 3 + examples/spacetaxi/assets/TitleShr.png | 3 + examples/spacetaxi/assets/depackSpeechRam.py | 81 + examples/spacetaxi/assets/extractSpeech.py | 225 +++ examples/spacetaxi/assets/genC64Data.py | 45 +- examples/spacetaxi/assets/genSongData.py | 76 + examples/spacetaxi/assets/genSpeechPcm.py | 106 ++ examples/spacetaxi/assets/sidCapture.py | 57 + examples/spacetaxi/assets/sidToSong.py | 111 ++ examples/spacetaxi/assets/songs/song0.song | 94 ++ examples/spacetaxi/assets/songs/song1.song | 94 ++ examples/spacetaxi/assets/songs/song2.song | 86 + examples/spacetaxi/assets/songs/song3.song | 86 + examples/spacetaxi/assets/songs/song4.song | 126 ++ examples/spacetaxi/assets/songs/song5.song | 736 +++++++++ examples/spacetaxi/assets/songs/song6.song | 840 ++++++++++ examples/spacetaxi/assets/songs/song7.song | 852 ++++++++++ examples/spacetaxi/assets/songs/song8.song | 1508 ++++++++++++++++++ examples/spacetaxi/spacetaxi.c | 479 +++++- examples/spacetaxi/spacetaxi.h | 21 + examples/spacetaxi/stAudio.c | 421 ++++- examples/spacetaxi/stC64Data.c | 14 - examples/spacetaxi/stC64Data.h | 6 - examples/spacetaxi/stCels.h | 7 + examples/spacetaxi/stFare.c | 95 +- examples/spacetaxi/stHookTables.h | 85 + examples/spacetaxi/stHooks.c | 1354 ++++++++++++---- examples/spacetaxi/stLevelCels.c | 114 ++ examples/spacetaxi/stMarshalIigs.s | 354 ++++ examples/spacetaxi/stRender.c | 386 ++++- examples/spacetaxi/stSim.c | 729 ++++++--- examples/spacetaxi/stSim.h | 270 +++- examples/spacetaxi/stSongData.h | 512 ++++++ examples/spacetaxi/stSpeechData.c | 490 ++++++ examples/spacetaxi/stSpeechData.h | 26 + examples/spacetaxi/stTitle.c | 133 +- examples/uber/uber.c | 27 +- include/joey/audio.h | 57 +- include/joey/core.h | 5 + include/joey/platform.h | 5 +- make/amiga.mk | 35 +- make/atarist.mk | 35 +- make/dos.mk | 35 +- make/iigs.mk | 37 +- make/tools.mk | 11 +- make/x68000.mk | 88 +- scripts/make-iigs-boot-disk.sh | 69 + scripts/make-iigs-disk.sh | 51 +- scripts/run-iigs.sh | 67 +- scripts/verify-iigs-audio.sh | 237 +++ src/amiga/audio.c | 151 +- src/codegen/spriteCompile.c | 2 +- src/core/assetLoad.c | 26 +- src/core/audio.c | 39 +- src/core/codegenArena.c | 380 ++--- src/core/codegenArenaInternal.h | 71 +- src/core/init.c | 5 + src/core/music.c | 20 +- src/core/palette.c | 3 +- src/core/port.h | 43 +- src/core/spriteDispatch.h | 46 +- src/core/spriteInternal.h | 2 +- src/core/surface.c | 10 +- src/core/surfaceInternal.h | 5 + src/generic/genericPort.c | 7 + src/iigs/audio_full.c | 889 +++++++++-- src/iigs/hal.c | 146 +- src/iigs/joeyDraw.s | 32 + src/iigs/peislam.s | 163 +- tools/iigsStart/iigsStart.c | 121 ++ tools/iigsStart/iigsStartQuit.s | 36 + tools/spritebake/spritebake.c | 2 + tools/staxibake/staxibake.c | 103 +- tools/staxiharness/Makefile | 36 + tools/staxiharness/harness.c | 183 +++ tools/stsim/Makefile | 12 +- tools/stsim/celGate.sh | 34 + tools/stsim/gate.sh | 75 + tools/stsim/gateHashes.txt | 21 + tools/stsim/gateStreams.h | 460 ++++++ tools/stsim/recorder.c | 195 +++ tools/stsim/stsim.c | 138 +- 98 files changed, 13506 insertions(+), 1340 deletions(-) create mode 100644 examples/spacetaxi/21_!.wav create mode 100644 examples/spacetaxi/31_1.wav create mode 100644 examples/spacetaxi/32_2.wav create mode 100644 examples/spacetaxi/33_3.wav create mode 100644 examples/spacetaxi/34_4.wav create mode 100644 examples/spacetaxi/35_5.wav create mode 100644 examples/spacetaxi/36_6.wav create mode 100644 examples/spacetaxi/37_7.wav create mode 100644 examples/spacetaxi/38_8.wav create mode 100644 examples/spacetaxi/39_9.wav create mode 100644 examples/spacetaxi/3F_x.wav create mode 100644 examples/spacetaxi/48_H.wav create mode 100644 examples/spacetaxi/50_P.wav create mode 100644 examples/spacetaxi/54_T.wav create mode 100644 examples/spacetaxi/55_U.wav create mode 100644 examples/spacetaxi/assets/RawTitle.png create mode 100644 examples/spacetaxi/assets/TitleShr.png create mode 100755 examples/spacetaxi/assets/depackSpeechRam.py create mode 100755 examples/spacetaxi/assets/extractSpeech.py create mode 100644 examples/spacetaxi/assets/genSongData.py create mode 100755 examples/spacetaxi/assets/genSpeechPcm.py create mode 100644 examples/spacetaxi/assets/sidCapture.py create mode 100644 examples/spacetaxi/assets/sidToSong.py create mode 100644 examples/spacetaxi/assets/songs/song0.song create mode 100644 examples/spacetaxi/assets/songs/song1.song create mode 100644 examples/spacetaxi/assets/songs/song2.song create mode 100644 examples/spacetaxi/assets/songs/song3.song create mode 100644 examples/spacetaxi/assets/songs/song4.song create mode 100644 examples/spacetaxi/assets/songs/song5.song create mode 100644 examples/spacetaxi/assets/songs/song6.song create mode 100644 examples/spacetaxi/assets/songs/song7.song create mode 100644 examples/spacetaxi/assets/songs/song8.song create mode 100644 examples/spacetaxi/stHookTables.h create mode 100644 examples/spacetaxi/stLevelCels.c create mode 100644 examples/spacetaxi/stMarshalIigs.s create mode 100644 examples/spacetaxi/stSongData.h create mode 100644 examples/spacetaxi/stSpeechData.c create mode 100644 examples/spacetaxi/stSpeechData.h create mode 100755 scripts/make-iigs-boot-disk.sh create mode 100755 scripts/verify-iigs-audio.sh create mode 100644 tools/iigsStart/iigsStart.c create mode 100644 tools/iigsStart/iigsStartQuit.s create mode 100644 tools/staxiharness/Makefile create mode 100644 tools/staxiharness/harness.c create mode 100755 tools/stsim/celGate.sh create mode 100755 tools/stsim/gate.sh create mode 100644 tools/stsim/gateHashes.txt create mode 100644 tools/stsim/gateStreams.h create mode 100644 tools/stsim/recorder.c diff --git a/.gitignore b/.gitignore index 5c78102..62cf4ed 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,9 @@ __pycache__/ # AGI game data (Sierra/fan-made AGI games used for testing; never committed) examples/agi/gamedata/ + +# Space Taxi behaviour-gate captures (tools/stsim/gate.sh capture). Large +# and regenerable from the tree; gateHashes.txt is the committed reference. +tools/stsim/golden/ +tools/stsim/stsim +tools/stsim/recorder diff --git a/examples/spacetaxi/21_!.wav b/examples/spacetaxi/21_!.wav new file mode 100644 index 0000000000000000000000000000000000000000..3772b5ef2adbfaee060b9cdb69ff22f936954551 GIT binary patch literal 33460 zcmeI2&8<~O5QMKC0U;m$Y}bIzQ5-@|}f zNbbL{tLN0kJO23n&6`hu-)?{Y`u4ZKK74%n;&!|J^ySYlZ*RBPpKiCGZ!d1|-+g@d z&ll$9zxB`OSN*3QXb0MXcAy<-2ik#lpdDxj+JSbU9cTyIfp(xBXb0MXcAy<-2ik#l zpdDxj+JSbU9cTyIfp(xBXb0MXcAy<-2ik%A9jN~{aaHeLSnX&3Oyy|%k=@XJZ2xTg z^Q+a{@849#V-+QlA8)Lt*p9fvD1Vf%w4y)m9np`>#!Jyr#W~^38L#uqU3HwJqa1o3 zl|X(@e69E6xAYfjg*P+mG;&Tfs}yQwm}APw88Xf^%c*v1j(x^+BG^i0O%=~Zn)7jn z4w==xAeD+7+ZpQ=wNtDx_sDrON~y z?2Q>Is5_(c%zchZIrM6LD#l%!t7Mb3@t8mGh}iG%6Y6fEdg`8?uHBc*)IVQ6RdN!-S=pHlP#0`%#H78JwBqod_6P>fU5@hSIpcCe^ zi<~l>aoHnNFRpj$L^U$6MARb{on3OqWlwe2)X_v0kk5F`YTRRIRQ6PFO~Jtxp(>fr z$dN*4M&l-!hgDvlR1%}@n9cOBV9qngt?0{uVW>u>~&Fc<+ zwaR6G?Cy8+5_w+sj6`NWMO7)S!D!UE=SrRZO1{ZTHQ240Qp|(Os`6fQgRf-fEGq7x z^Jt`^W~r*3njhUIdql;Iq-JMZnClL6Olu@DX2H(8Rb{76C6KC%o<}7#cdAw(wU6Ey zmBP1152Vk}s@OVXR&!kRxT25xu8`H52c5(jz1HDAm>n%#KaNm*?ub{!ot@XO-=o`5 z^}idaIK7^&@B8Gs@@&I3f7ELB<>)=#H;1CDjUAsqMTPJ1I9GNit2x=-cAy>jJ{_3- zUw))Lv)rWSu0N~S@4jSw!TBa$i3qRGH;)KaTYWtkME+jrAl6IlGb4k0oS{SJD8n8i z-=$e9kbxpQV-_9$Mp@y%*nd3YM!pvMHO3t!aaTF?zRjpFWnRDQ+HZf)m#T}aYh>=b pkXuyI%x9R?7{B(_+t)X)UU+^U-vB?>7xzik2;J46p#a4^11*3 literal 0 HcmV?d00001 diff --git a/examples/spacetaxi/31_1.wav b/examples/spacetaxi/31_1.wav new file mode 100644 index 0000000000000000000000000000000000000000..d17740d077bd72bea121b05c125a419e64c54aea GIT binary patch literal 12986 zcmeH`&21Gy5QG;Za0nIw_&^3oV2LXSbGC%IadR9*!ih}Cz@gyLs8m(`v%634J;}0G zK1_G@SJks)``g$1`@bKT<@=|HFF$`h-QFzA^6us5!^5(C{bN3CTqmD=%qMe@BbieCF&_8w0sR@&#t?AW6ch==BL`bbkZj%#k%Lmgj`p3 z{!{sx)45ddHdeRo*?s$JzupFQO-2=Te@4gbYrbYxt$kBfj`f;j{qJ*Rth+mZ<>HO{ zs`3r3a@1@^c3PbPwRU0ZXU(eWJUS&3hR=JiPqgy{cFOGew&8S7?Q(|uQ29J};BaQ5Qb6N$2I`(uotf8Z{{FS;oNXm~ zf_Ly@Z#^Y7=IHcX_YKmCn9@yE*L^uJld82+t&ZH8vI6)N&??L%V#`SnJzjx=NKC76?RGp|xb<%_ACu61=sMj2m zabk4v#(n*Sj+_~1NQJ((zi0F=pw`a3h065t%Q>Nj>^VcnlOAfLqV6|Vy{fYjaCn2!&m2su)R+PWUQft$ z&W6&no;@~X7WdE-$*Rp76VIttxSbv9IuBpms5%p32pmx9jO098$FA zlu143^yuiC6};7qY)z?J)%5$fuydF4eLX$Q801&PotnF+p1XVUGIvA% z`DsXs=lRKd65>|x_aq$5@dC#ST=E6{fAE^!C2xIX9WU@UU*MYmf#+nu&DTD@>Ue?U L1&$YZ>o4#hN7}rM literal 0 HcmV?d00001 diff --git a/examples/spacetaxi/32_2.wav b/examples/spacetaxi/32_2.wav new file mode 100644 index 0000000000000000000000000000000000000000..b3b74acfbf8b47517d80638655d7630149fd10fb GIT binary patch literal 21446 zcmeHH+ig@)47^Z5N{A?c9*{yLenCpUBcc59mujei8t8!zD1}(oX#6_op1au zI)P506X*mwfliI)P506X*mwfm0Ir{a!z!l}`h?ao(SDeeSrwJ#tgrj_~DQ(K;XQsI_vwRgG}?seLMR#oOwR z@~t-a%xdP#?}aWq%@C-nI@2B7Rqlcvb2&E~ukyY?|Ni^%F8iWc55Ck@p4I&O{p?%& z-TA+(`m3+Mm8Z-SWW>vFUHRqM_t3ra5~kwz1sSp1l|J8k3Bp&3%ijISiSu0ll6Q`^ z`KEFehc2_m2sCx|zRavY%q3N@75y64e`a&OMaa#5un+BhYAt zlUBiqfJwx<5v`oK)ec_pkF#~=4xMVGODfC>+A+SyfEWkoTUQ`ew>xZIlf&tbOVz}cDENTFZajXp~oBWR?a{?(@(+% zQokK2S=w2hAiRq`5U*4e7&ua+j+kfr$+PGOmQ+Q@2vwfxC#(0p)$<-u&(%G#>IRfj zXH36WXK0~|b4&yKSiKv!@by*q6&5#f_8OgV4ZC{AwyIDdKhD}&O+?}>T(4I(u;^uH z%OqZ1;FcXOuv))}az_We+#-dLH{W1kss`I&m)MCuN_(YIF>S>Ra&SbbrH!eO*04kqsq zL8oj?QmL;y<@#NvCuX@oEhhb*t9@7I!j+Yt#Hud7V}Gt(ES4SYcGPFp92xl#rydv) zE>geLqPgtgqPd zc*gb{L6J`%@9uto-L^09K7aWB2~+}=!2gxN`d<#upUe8KrhNX3#}^#^7S)fZ`VKr~S95_gdpRvfnP-K|r(KTq zHyoSmy{px+9jB=ElDS{i$MJmr`3is4Y@Syit7fycZ*yhm-ezbV~)i$K4@9jOl z-gCB|SND~<$!619S9z*EQq2f^M?ZPw0w3LEq6Un;Q8~)S`?89&kW6GUpXb!mV=B=h zV+cGh7<~=<{RGsU6cIH0Z}fSFZt~4u(6 zLtsz4=iCX{j+;=gywL+Dze>NAU#lLEaRXT}?TsuMZX&nu#Y9!jcC2s7ly({Rt=&mg zLu+2Bvae}9oeAwe2bFh|Rrl;l?l-^p#)?~xAe*BOoeVzXcwhD<>LA8Ecv*v?R*~u= zrjk8yWUwJJrZpbE_EMR#s~o2gsdtR2k4RnCZ47nRXWfu`BIXqKGS0S86SrmzIQn=S zxX8d2Z-;j5g(7<4!3!nod}~ut(GYy#Sx2N!>-LP+n4NLWt-S6so{Z{jW@P-l%nOcQ zFe@)zJuNVm2fTH}k1n_u8{m^AlM0Z;x?H%IvdWydpmmDb2)?ADHVjw*F{JaT~d z>ir6y(RS3Y&Ex7fcGGI~p6wVq*4iD=|C<*55|czlsv zgbHtCU&fcJZpLRXv%AHMfy$fn`B~S@e0+m1|Pzh85l|Us>2~+}=KqXKK ZR05SiB~S@e0+m1|Pzh85mB8f^_y-I*bcg@| literal 0 HcmV?d00001 diff --git a/examples/spacetaxi/34_4.wav b/examples/spacetaxi/34_4.wav new file mode 100644 index 0000000000000000000000000000000000000000..ae9d522ab381a775e1fed59cb30fe3cf07b1f569 GIT binary patch literal 17962 zcmeH|O==uL423H>fsbH(fZQPv8?w#9SsP4t+4)$xOm2{qgrq2h{@zpfv^yRGV!)D~ z-lwY8{P@e~hljg|F}{9$y#Mz7<@RQb@$T>I?s1GyKgM`JZpQP|%hT`sF>ZhMt2fXa z=neD+dIPQFt^agqZy@B39Z=g4D?G60;Fkjcc zMW5dr=nb5?f%Uy+f6qJ#%|hoQAJ!0!HO%i6xn|_0|4H$je4Z%S&s3p!WaQL26+383^u!u5Sc_z?F{f%K zkfMH@A*X8AS?9{ItmgN>A9xNsRdh@tRR|UlqZ*^Ku8hyJQx)&5E2UUg@r*$_nuB~+ zLk!j|g>puuF@x2568^=PNn5P#3uB}0quhGy9MN8@c<-z$G^Zx&9maX} ztmyW2&DyT#q_U2ji)&BJbX541=&WlrrzXyPKlrSzJH$SEj;;SbnBi(X)7Hl*J3(?h zNxC>F|^ zyeM1b(I=I;a!!oW_c6Ly?I~U{UUWuEKSt)ydL(DP&lSl`;rHZ-&z|LZ+mYNmRwcds zblS05o?*KzkJMhD+ik#BUB^>fbO(**- z`c~~dxw@3XeyrxxM8W>4iX%tPNGCI*h$BaFC>W;xHy%64O(-Ks=yuGOnqc6mjensJBisDEX)SLMgJ zNY#7P^D|c|%FV2D&YpMMsv@mV^NO%NC46Q*EBAd=c?LPM=9yV#k5!`I_Rh`YNyq9l y?{w@2HFf6wu0QL4c-H!kynFrazSRxz4z^FnoL9G}yrsk6ZC3y8^nBLaD}Mk6uJ_Xb literal 0 HcmV?d00001 diff --git a/examples/spacetaxi/35_5.wav b/examples/spacetaxi/35_5.wav new file mode 100644 index 0000000000000000000000000000000000000000..ef53f4f90e7e9aada6a93fd4ee421d63544f4bb8 GIT binary patch literal 20456 zcmeH|OKy}w41|9;L5>h{fZPEIBC$eZ5!R6qJ9ar1N8l29v=dY*Ro8vg%z5ac8IF3)hkK_GubNqOGdi?Xs+&kJ`25JMff!aWApf*q&s14KxY6G=_+CXifHc%U=4b%o|1GRzLKyBbUHt=%i z|1L9^f5qQ-UiF@ryOk?CdS~v-T{BPa;BMw-J*zX%?Q=$Q@0_ld#o1Zy-^&>7dB^yb zj6_dUg@(CtM>RvS;uEupU)izyr;9xN5*%M*Q=GT{F+`9ni#)_mF{0?_qoTb z@lDNK^O1<1Q!>>PKbh*QH}c*vSGwQ4$1|#XHQtqPH)W!pb=0%^Rhd3_ z%~hQfGpP&xUa8t{p6I#0t3I#lEXY(B`prMnGTrn2u9GmM%J{~fR(sOx@jiE`8r`D~ z`#jS%?K0nLRpaN1gm-w4>TiF~JF+S9Mn?A@m|l6(>)D+>US}@mF=7-lu?zFS5v?_a*g`U zIn$8rNmYD`X4?FDH}i73x>%XbG;^tD)A?kd?m3^r_nyU_R^x@SvU*jwWr><+T|4J$ ze9?IG_tU!hX{@T&ZKo={jXa;dnSYkItMMD`;l9Rnl#(`$Oy zocv~vTy0$=`q??|sgo6*RXMxu^J`KG-0ZE<335C!GrdL{H1v8kQmpTJ-F8&DV;TKq zh3h?v9jDg%5^kD@eqJ%{3D#l_I%?c`J2(DS^jsv)NTzztQ^h3aBk4P1nw8mnEpg2% z(Qkg1-MeLhw+>Edd9_P|YgL|wF4O2*#VXgP<75>z^sL(DcD}8ak36ojm#?9R8E4ag zT7Q?L9L~tZ5I_q(if?8Z~4<=bLqlh*e+J3i0c?|v5@VCF1%vfiF2;+Bl=SJYt}5vb3( z2ihL{{LXo9#BZAYv}>&QTvuTi;;DOQ&r$LG$<13Hvpf3bJM-*X^sE<7!A6?WIGqYBane)nwb!O%&@^YSexpJmsSrt3h zuQia7s=wE9>sZm}N6l(m=Jg8utb;s9Xn>-g@jH(SW?+pYdCbO4qGXOUGDjISquv^W zvd5LKvw^HstIv+>FlF!E;Uw%kT}cHqOhaU+W?&o>IX%xaJR^K^c4q3ik_u#}a7R~F z{bOp}r9{-Y` u(Seztk#m{cF7wUIE_2CzD=tHvGu4p;azGBq0XZNCJJT0pS>8hq z8J|^n5;c9M&~!8w>mGCUsv{@QsPE;k-TN(=)5S9{oa?P~?l*FUba8XM^ezx$WZ#za zeqwd=4d>s+pLgp`y8XerSoL{3b|&WT^;Mm`evz}I#&UhQ2r<6_UEPj1D3VnszK-KZ z)Za|jc|WqczSJ|O;kFr3itHCFULw!ft+`la<19Lw4l;79zt@qU5%J*&sZ3s08LHkRsF7v<}&crim`F@KCjH!5=O zn#i4Lz@sAS;*cw%!d)&A$?Va~xxN|SaW}^*;;ydsx%pS3lc%q$akDtHD)DDK z;&=m?au~Vl)l6z-m9wf*F_T(*uGH%uUNxc`aS#~#<*dUmsBX=~fy=M9>Rj(qng6Bs zP3(L8n4kCc@)2=e$2h+a)6OfU7~gRYk<(*j*Y9?Do4MCRYUfk?&TJR{H~33(b!Hou z5&b<1k8_1y%A#tOn-Bf$3F^^%5gXYfQu)-GSu0qx2*2yEUW1M(JqXb{8xDg);;ClWvGZ9r#iwV&NTFEG`?Z-%?_0x>&&bUk%@n&Ff4PaMmC*! z%o^j3NUzR3sxw^IL~#B^ zyvSYKl1zExFS>!^smoJW@D)(5@Bl_D}x*BCjP zv1U0>%1dl9>jZV&L$zhmH+NzcSuu~C>K1wVS7WNwE3R1MgxGVBh%m=G5g(axK1?ub zi(q4nEb$_mHL+9ko(Vc<(}9^^O;-_~5X(SOs|JBjhdOG#r_Pw6COU&E&k`zJSoH4D zHHM>DAo$XuuJ07yWvGeHp*lB9{=HtPe6PjwUYfLit+_|8<>SN=LE{~&td+O)Vro*v z#O%~{SFzE`|C_G(wf9tS z+H>~>_62UeK>ct2QR-XYbwB<3BKMnfi&MYjEskfk{RR;|>-c}?$J=!IbAEsK9>?zS zs<}IzdrWy44>mj(@4L&T+4r6HFmCfVJj}e_b-u-!PVfD)(KGkU+?Rc~{rY`@^B2hf tK~Gto-+e3g1@;B@1@;B@1@;B@1@;B@1@;B@1@;B@1@;B@1zySp{spRnkvISV literal 0 HcmV?d00001 diff --git a/examples/spacetaxi/38_8.wav b/examples/spacetaxi/38_8.wav new file mode 100644 index 0000000000000000000000000000000000000000..22c374505e9c7243faed0ae57272f47fdcc98e8b GIT binary patch literal 23360 zcmeHHJ#Lgy4E*2(DL6pH0aDWC984=NX7d=C z`^d<$9L$)bmt)IBol&_0rivMPURCMl-ds0hQO!8OSTtN)&$ulkC{_fJ!{l4K4#3J+P=<_K8-Bk`)DY3 z!9kZbG2@KrD&sq_(NSYPn?T7?xvH6l;RFnJy+g!2i~LdU3VXT{MZMV#@ioL5H||A6 zM3r&lD2tlRE3N#ynd{Yj>PPkr%UNVpdDLc3e$GsRC{`I&Rm6_3sCejAtk^T0*UqpU z+;NNwIUjS{N#wP&x{eIb4Tr9Ctg6}Uf>B4})>%|oqYV)meknSfu9TeND6p%z(%GGl z_n87VGhgc(C!(OyM@O~z$nEo9W!#}YE^=(Iy4M+P;+{PnLzxzNIq&!IH{r0g^V4@kCF-J!Sm|Omjuq>i9yd7K z>pnk6n;dRAH>1|y$VRVj`|I7t0P38#f}3MhwMndFo>{AE=RR!1tDNJ?+{|pgGj6Pu z=j=R%TUjW-+VAdr!}2dsefZZ7Yq-%;_NBV0Xm)GIw2mD?F&9-&fDPd-)f< z?h9t#+3a;+uj}N19FPNYV9$ZofBV-m-+PTvazGBq0XZNCR^{41^z?AV-KeK=v$=AQD>^$vP5Z$1VrpFf6$YH^@U1-ygT_@zcYXA3vXOZ?4lxvT#!`*r#D^{Zd*a^3x-tlw51EYm|7^zB%Ep*r`lCMw#}vO#5Po(){AAQwiUAX53Ax_FeRv0ZaGu8Lo(h(=jFua zF^%)Vl%Q@ASXQs6?4t13Xl|NrXb#Y+9}|9+YLvjMn$xp5c{b1I1uB*alqQfix z&53nn6m^T-3tw>5)UVe;qmEgfx9i<>nr@-WdB&TJCW7zhsVcbT_fXRGnNDB5OUdtJ zN3QRhrxOo-oo=CS)g)yz@8+I;r?O6W%?Q_dgI)XQI-AQ>_u!1XFuPURg*7d9r4p~g z0h}4Cx+!+hDW+wO#qf)yW}y9yI0Z9uqSINCYm~5N-c39A$#`yy-6ayQ``5YW{`I z=DeSGwQt-DR0--9%`#O*ow}SZ(=B)o6Rzv;RSC14#J6ISYEZXG+3B)uYF4*RnCWI9 zSv|fCik!@PY2SVs?_;sLD9srY)^I()W3I?G>U_#9+m7qm-Qr%XiR{vHyhS%h-J&tm zD*lyc>#^_c<+wuJ4J6*Dk=TC;xxT5-@P>+6mvbl(HHLf~xx-xd$b%h5{FF7GjaVmU zb?v(T40nkC&&0RScSdEF!!hf4D|O$r-^U17y))Li=lHQtJ-+k*IQkZPk=_A)ebjef zW5wTnjc+^toBUe4epknzyd2jB&U*p;)<@BKcUjZc1=a=D1=a=D1=a=D1?mg@0|Wrm A?EnA( literal 0 HcmV?d00001 diff --git a/examples/spacetaxi/3F_x.wav b/examples/spacetaxi/3F_x.wav new file mode 100644 index 0000000000000000000000000000000000000000..181300750175b4d64dd66f959e9a10715c0253a8 GIT binary patch literal 24876 zcmeHJOKub~5H*}2M;Ia2xdReJV#^{~kA&E<%dxmjZjhspmSuI@U9bGA>>g%9X%=l) zyfoLEaIIaQo|Icxb^Oey%?B}E2dqs9K>g?yU319F19+}BIEyz)~ zQ9hsca!zZeQBOaA&7ht9`kAR|`*!w3oKLOm;nWJ+jZ?oKl>9Eed@`#y?Wz8o-zfc5 zHbk!ldCz#rOnHj*WVAk&v=BqWwll=-&08IG*L4uf`t8Fy>t>&1-Qm-o#Z0i?c7v6+ zsQnr9>dl@xb5eU*Zh8ta$EA}@c( z+1QOYGpk?Yylxt=c9?b39P!TLj#cJuEO|7nH<6}^cV4%ahaE;YtrLH@I`hgB((2~L zOWysN-L0=FA?!X|MpoQT^s|(Jo$1Q$mQ}^LR_z44C1~gB9_{>(%nGgMHI#Tkcjj zi?)unIx~3P5pNl7gu$xz4f3$e?XHDkamm=iJmP6jQy9K9h0Q;GK01qcM@U^CZ;>9J zV=eCndodfY>*DYR9|_E*z^69t?u~wGCBM% z4Aa*Fql_@TBT}%;c{c`Wajvn9dX1_1_FJts>t@ffGWr=-toPLS@FKj479Qg{qK~#Cqo3@kHmj9N}MxQTU3nF{wXSwUl_qWx0FFhUc)}{Np{gUPg zM~`O7Q@$^Y#yZ0%kJpi$Hfi=MKR@epJz3HemW5MemwY8to7Hn)wQ3*f#_3DU_dUzK naNl>m!`taee-CbZJ8v6Fqk(838i)p>foLEahz6p8+iu`56anwq literal 0 HcmV?d00001 diff --git a/examples/spacetaxi/48_H.wav b/examples/spacetaxi/48_H.wav new file mode 100644 index 0000000000000000000000000000000000000000..173de91f23867961f1f83bfba4553326517a3a54 GIT binary patch literal 18072 zcmeH{&8?nQ41`~(ASFZ;Ko3X|i7kt;j)YjjE`?AC&Cmg*5X&0PjO}y2AHE0_9bGB5 z$Isa3b#C(0kMG|7@$R~Q{_gz`zyAL5?VIbmzWVg@&HL;6{?F_B`g(Ky^5Nr$-##&K z|2jYC0_Otf0_Otf0_Otf0_Otf0_Otf0_Otf0_Otf0_Otf0_Otf0_Otf0_Otf0_Otf z0-xsse}DVm|9qY=J5$dE&IO*iz$^YocxLabJ-hGN?W(>abC}~Er6X6$b0@v0m2+N!n>Hz#Tvy=NsMIu9+2`i2 zT)8>AzE@v_F14K2|0Gefd)UrE!L+PVMFhd$Au7~B&Q5hiu9W7LV?9x!k;p7L&Ec$? zJ6BZS03RGRqTi#E?54_kbsZ9T6JmBaz1Gfg&T3OkQp$7g*-`Y6Pc7@n>mHEGxyGpz z1Gdg}mr|ZPhl-5ujJ)n~l2zp}88e(jrIhC&GG-5&litJMm->|V8K@#E<+)OxJExU% ztdv1syR7|H(9?%1BWBg;lvb5w4T{Px1D$V06^_W&sXhO9sg&k$^r%$LK|`-KbVT(1 z3ql1E5maiZGAgIJQ(7tIIVtE+P}k^vR1uYg4h?nX(2uB$=Sq1_j{0tgZb=6x=oe7k zQQX-nPSCmcI@X>#7xij#e_16_V^WVGdh9He&O!kZF*MZZGiC=hI9Jil=nN~Tydve9 zQ(8@2YD#79x;i@xMJdhEpQ3WiCz2Yu$Wl*@3^VOaPLjx+Vuk)3$swD$P(gT1KDF)z z+x?x@N$_c0Ir{9?YjlE6My`{T&0Hy7qjRe8C2%t;S94OHRZeS?eBHnKO@mi=M4o$6 zyUHM#JH>Z|NjR}Z%_>pt!IgwCcyhx4db zzR=I$eCkB@uy4t9Ody;b^? z=BznW%ID8~Ro&xNWnWeMmV2w*4toEVp!UQgUF`ID?}X=E_fF@(9p97H?J1KB+ATM@ zRowe$*WUAdci!ge^V7d9r(fk(^<{ngtH0*f^`if(o%if|oO#vj&YW|B|KA1v0SwI- ATmS$7 literal 0 HcmV?d00001 diff --git a/examples/spacetaxi/50_P.wav b/examples/spacetaxi/50_P.wav new file mode 100644 index 0000000000000000000000000000000000000000..e9ea5a4a352b030283761af9e43a7fe3cda80b06 GIT binary patch literal 24446 zcmeH`L5dw`41_y5fsbHvfZQRF7_#-kSsFvgE<2M` zRr;jApRnKm{_fpBACKdYUq1Zy*WaJtzB!KLhp&G>eK?L^|2>W$k2lAkpFV$j|CM?B zW&id9dx5>cUSKb<7uXBz1@;1afxW<9U@x#2*bD3h_5yo>y}({zFR&Nb3+x5<0(*hI zz+PZ4uou`1>;?7$dx5>cUSKb<7uXBz1@;1afxW<9U@!2RF7V&a-}3J@{qk?|txvt( z^d@&q-Pf7%H$7Fo&foj!tLG~`ntI&Vy^rZ6a^^9uyF*`1RQ8&_zv+s6`=!4+f9GA! zU&EBA-GO6OhUuh~Og&MVb=D`)4;uOO(tCS2=nzu1K6KpMaZq^s+h;nI_JvQ_8hczHR1^ zat;Y^$fPPx;VmZ4dQOKq(AjZE8hNdifUn$@#|*wGeN2SX$x5DG3LIwP>pt|el0uFI zey){*k2+6eN)A*{ly$bWQIR8qXGMo> z^q6*4XRGfMcXxA>t7#)LC&QsLzY3mID5RLK-syB?LV-{{ah$@$R7r6&xw+e4{+7Rs zucy*)Q0<&{R69j?htXf9{B)_<>`Xeo=&G~cPuHnP?vfKX?FBdOp1)xJhS=jgHhXmj z&Y8XoT-44YQR<4*FU{GU#GbQ?{g3*OwqegyxqIHinIz#kM-@G%;E|F=Quo2@B(vwM z`ZB%lM4A~KD#+^fThT+sOjOU{qAI0YnRAL0$}9cCaifkOp%<}>Hs@R-3{WTwPt z@y=AamwT9DA`>{&yBzqeoZ^f5`;;Ta6d4lusK}USOt$mj#%0hE{S77+{o$24psjlwNE7|ujA(vV8Bvp|L zr5)8y(J9k4xZXhm0u?zcI#Dytgannc)a-XfN>$~wr@C_VO5wZNRpsQ&R%WJhtF!#~ zs&95(r#cadQXrTid1~c`v)`q}q#wPAndIQkUc)wWRe|W$$=;t6pQ4*-q~xkfLQ_pJ z&k9nh%*a%QnWf@6GFrsjucbjpSH;z+PZ4uou`1>;?7$&%40?&koL2 literal 0 HcmV?d00001 diff --git a/examples/spacetaxi/54_T.wav b/examples/spacetaxi/54_T.wav new file mode 100644 index 0000000000000000000000000000000000000000..2614012419a8ab6266c73d84ded321c3b22a56d6 GIT binary patch literal 25694 zcmeHLOHLe74D93t90BD3EOLWL2~xIMkTnv@E<28u%WwmZ5-h7!{=NQ8_YfvnBVoH- zF8fu85d8RldwcgVO+UZfef#zM@%n0-rVlTlPj}Pw_0KeYoUW#a`^WpwFP!VA{0(M9Gv;L@J`NSl8DXFor}Wv{gHxaS{6Hs zQZKT6tvrim#CA2O`Y4MpvcB3G(Tg?rX$LiUsH<3WypGOgEY70#QKo?)$0fjX1cOmI zP+&!d&fUx7t5BfK9yM48fD=Ot$bmXF+o%=fiIuZrMTJz(HS1g5vLlFagK&YtqA zV_81;$|LQ_^0g{@%vap$abJ-YGvNVa&I~x?Sq|JF0Gtao1cwvTUvS_^armi(GdB zMI@@3{R)mm;5G-wSWU3289D4^j7ZF63K;UJtbOQ9g`5!@?D;7&H6x!Bk;BW#;hbd? z^%*#VB1g1LkD)@0Ow{Q6h^|K8vsLC18S}{Div+V1);HhtmCjHR8bDA>^pjx3y>SA( zh}+s zci`yOEx2B>{*HAo^zYqT`D?H-M*gevy}AiZzgJQAn0elw3Yrr275**Tv`?AU&Dg$e zpE`xOh-pu7kzHV;Mk-Z~t z9&4R?@mTSFcGmm&KK)veSHv;*xxJMcCf*z;52sM)vS QL|@Skv;*xxJFv}xzXGWd>i_@% literal 0 HcmV?d00001 diff --git a/examples/spacetaxi/55_U.wav b/examples/spacetaxi/55_U.wav new file mode 100644 index 0000000000000000000000000000000000000000..427a765959e600d204e1c887805250b0b6b84cbf GIT binary patch literal 25264 zcmeHFOKufG3>-K?j_||*xC2s%#Fj-^Cqmh=%MmyX3oerza1>%$maA+}zZqqPD7{77 z_3NI}`}*bX?$_hCefxC(`RT{=_uF>+d)wY`H{17z=ZBxKn#;?dkM(zX zS^w7h%I@v`j!;%6a6g%4Zhu?5kmU?yrmk3R8#{b+Rb{%jyC+S>IG5Sv80VWRXZ!3- z)F*dVv^G`w>AsFtS555>eD$pEnT*WeQ*W9)RnzzDE>`5$vm8kUPpD_6x57L+^Nx;k z)0Q!5Du#De&UWN+MlIs?R^}=j`Ir`Sb#`wk&!0WJBb-`!W_{}~tgfzz22wdCcg*#M zr*k!NLRB(5ZPju8OQw{L_1r{EotsVUXRhsm{rjxyQr_f%?guZEHN-)ff2W zMsDphWmV*xFlSOS-*dW$9sTY#k1XDXc+ZT6S;ZTsbv?7TxtnoL?C)6f$nNDbp6{q$ z+~XZ_x~qHlvKgO}Q^Bll$@!EDWD;FD)?Bc8E2hSbx5k_fL_E)Zf#(@{-5Z*!z`eW9 zv93PyFO1jhpYaxa!K6k$y6!qw#nyH4_E6Z_RmgUQv*yaFXG{p@9^v=%F=OJ9^BQ*_l%ObhWX0&?_j-e=wg1K;WkZ&PV_O;Z>&6eG1KN1 z$E+*Uk4~GDIX7X> z)BR`ssKqSAaYoJg8I%5{xqj*V_NwB2_6Su2YCsLB0X3io)PNdL18P7Gr~x&g2GoEW zPy=c}4X6P%pa#@{8c+jjKn`qlKE=1 literal 0 HcmV?d00001 diff --git a/examples/spacetaxi/assets/RawTitle.png b/examples/spacetaxi/assets/RawTitle.png new file mode 100644 index 0000000..2710f32 --- /dev/null +++ b/examples/spacetaxi/assets/RawTitle.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:037ff3028d1e5e1e4022109199d53664812f21c258f2b95f1945792b99cbccdc +size 109832 diff --git a/examples/spacetaxi/assets/TitleShr.png b/examples/spacetaxi/assets/TitleShr.png new file mode 100644 index 0000000..627e4a0 --- /dev/null +++ b/examples/spacetaxi/assets/TitleShr.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6846cf0cc6fc16da5d290a2f395ed44cab1f98f38e652877744ebbd44c3429b8 +size 17887 diff --git a/examples/spacetaxi/assets/depackSpeechRam.py b/examples/spacetaxi/assets/depackSpeechRam.py new file mode 100755 index 0000000..765fac1 --- /dev/null +++ b/examples/spacetaxi/assets/depackSpeechRam.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""Run Space Taxi's depacker to produce a FLAT 64K RAM image. + +Why this exists: the disk's "SPACE TAXI+" is a crunched single file (it +matches the running game's memory in 0.6% of bytes), so the speech +samples cannot be read out of it statically. And a VICE memory dump is +no good either -- it captures the banked-IN view, so $A000-$BFFF comes +back as BASIC ROM and 13 of the 15 utterances are missing. Every +mem0000*.bin in this tree has that problem. + +The way out needs no emulator install: stuff/spacetaxi/cpu6502.py is a +plain 6502 core over a flat 64K bytearray with no banking and no I/O, +which is all a depacker needs. Load the .prg, run from the BASIC SYS +entry, and the depacked image IS the RAM -- $A000-$BFFF included. + +Feed the result to extractSpeech.py. + +Usage: depackSpeechRam.py [spaceTaxiPlus.prg] [out.bin] + Defaults resolve inside stuff/spacetaxi/ (gitignored research + area, same as genC64Data.py's inputs). +""" + +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +STUFF = os.path.join(HERE, "..", "..", "..", "stuff", "spacetaxi") + +# The BASIC stub is `0B08 D307 9E "2059" 00` -- SYS 2059. +ENTRY = 0x080B +MAX_STEPS = 60000000 +CHECK_MASK = 0xFFFF # how often to test for completion + +# Completion test: the $9B00 index table is populated and the first +# utterance's $FE lead-in is in place. +TABLE_ADDR = 0x9B00 +FIRST_UTT = 0x9B40 + + +def main(): + prgPath = sys.argv[1] if len(sys.argv) > 1 else os.path.join(STUFF, "extract", "SPACE TAXI+.prg") + outPath = sys.argv[2] if len(sys.argv) > 2 else os.path.join(STUFF, "staxi-ram.bin") + + sys.path.insert(0, STUFF) + try: + from cpu6502 import Cpu6502 + except ImportError: + sys.exit("depackSpeechRam: need cpu6502.py in %s" % STUFF) + + prg = open(prgPath, "rb").read() + load = prg[0] | (prg[1] << 8) + ram = bytearray(65536) + ram[load:load + len(prg) - 2] = prg[2:] + print("loaded %s at $%04X..$%04X" % (os.path.basename(prgPath), load, load + len(prg) - 3)) + + cpu = Cpu6502(ram) + cpu.pc = ENTRY + steps = 0 + done = False + try: + while steps < MAX_STEPS: + cpu.step() + steps += 1 + if (steps & CHECK_MASK) == 0: + if ram[TABLE_ADDR] != 0 and ram[FIRST_UTT] == 0xFE: + done = True + break + except Exception as exc: + # A depacker that runs off into I/O or an undocumented opcode + # still usually gets the data down first, so report and carry on + # to the write rather than throwing the work away. + print("6502 stopped after %d steps at PC=$%04X: %s" % (steps, cpu.pc, exc)) + + open(outPath, "wb").write(bytes(ram)) + print("%d steps, depack %s -> %s" % (steps, "detected" if done else "NOT detected", outPath)) + if not done: + print("WARNING: completion check never fired; the image may be partial.") + + +if __name__ == "__main__": + main() diff --git a/examples/spacetaxi/assets/extractSpeech.py b/examples/spacetaxi/assets/extractSpeech.py new file mode 100755 index 0000000..e666e7f --- /dev/null +++ b/examples/spacetaxi/assets/extractSpeech.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +"""Extract Space Taxi's digitised speech from a C64 memory dump. + +FORMAT (reverse-engineered from bankedSfxLoad at $9802, see +stuff/spacetaxi/live-level1.lst). It is NOT 4-bit $D418 digi and it is +NOT the SID speech synth. It is 1-BIT RUN-LENGTH audio: + + * An index table at $9B00 holds [code, ptrLo, ptrHi] triples, + terminated by a zero code. `code` is the PETSCII character the game + asks for -- 'H' 'T' '!' spell "Hey Taxi!", 'P' + digit + '?' spell + "Pad two, please", 'U' is "up". + * Each utterance is a byte stream. For every byte the player busy-waits + that many passes of a fixed delay loop and then TOGGLES bit 0 of + $D417, which flips voice 1 in and out of the filter -- a 1-bit + speaker. So each byte is the length of one half-cycle. + * $FE is a raster sync (the player waits on $D011), $FF ends the + utterance. + + The delay loop at $98A1..$98AA costs 27 cycles per pass and the + toggle/advance/fetch tail ($98AC..$987F) another 33, hence CYCLES(). + +LIMITATION: a plain VICE memory dump captures the BANKED-IN view, so +$A000-$BFFF comes back as BASIC ROM ("CBMBASIC" at $A004) and most of the +speech is missing -- the player banks BASIC out ($01 = $06) precisely +because the samples live in the RAM underneath. Dump it with the ROMs +banked out, e.g. in the VICE monitor: + + bank ram + save "staxi-ram.bin" 0 0000 ffff + +Usage: extractSpeech.py [outDir] + The dump may have a 2-byte load address or not; both are handled. +""" + +import os +import struct +import sys +import wave + +NTSC_HZ = 1022727.0 # the port already uses this (ST_SID_HZ_NUM) +OUT_RATE = 22050 +TABLE_ADDR = 0x9B00 +END_MARK = 0xFF +SYNC_MARK = 0xFE + + +def cyclesFor(count): + """Half-cycle length in CPU cycles for one stream byte.""" + return 27 * count + 33 + + +def loadDump(path): + """Return a 64K-addressable bytes object from a dump with or without + a load-address header.""" + raw = open(path, "rb").read() + if len(raw) in (65538, 65536 + 2): + return raw[2:] + if len(raw) == 65536: + return raw + # Headerless dump starting at $0801 (what mem0801.prg turned out to be). + pad = bytearray(65536) + pad[0x0801:0x0801 + len(raw)] = raw[:65536 - 0x0801] + return bytes(pad) + + +def readTable(mem): + """[(code, pointer)] from the $9B00 index.""" + out = [] + y = 0 + while y < 0x300: + code = mem[TABLE_ADDR + y] + if code == 0 or code == 0xFF: + break + out.append((code, mem[TABLE_ADDR + y + 1] | (mem[TABLE_ADDR + y + 2] << 8))) + y += 3 + return out + + +def decode(mem, ptr, limit=0x4000): + """Return (runs, byteCount, sawEnd). runs is [(level, cycles)].""" + runs = [] + level = 0 + addr = ptr + while addr - ptr < limit: + b = mem[addr] + addr += 1 + if b == END_MARK: + return runs, addr - ptr, True + if b == SYNC_MARK: + runs.append((level, int(NTSC_HZ / 60.0))) + continue + runs.append((level, cyclesFor(b))) + level ^= 1 + return runs, addr - ptr, False + + +def writeWav(runs, path): + samples = bytearray() + carry = 0.0 + perOut = NTSC_HZ / OUT_RATE + for level, cyc in runs: + carry += cyc + n = int(carry / perOut) + carry -= n * perOut + samples += struct.pack("= romLo: + continue + runs, nbytes, sawEnd = decode(mem, ptr) + nxt = next((q for _, q in sorted(table, key=lambda e: e[1]) if q > ptr), None) + if not sawEnd or (nxt is not None and nbytes > nxt - ptr): + continue + index.append((code, sum(len(b) for b in blobs), nbytes)) + blobs.append(bytes(mem[ptr:ptr + nbytes])) + data = b"".join(blobs) + + h = os.path.join(outDir, "stSpeechData.h") + c = os.path.join(outDir, "stSpeechData.c") + with open(h, "w") as fp: + fp.write("// Generated by assets/extractSpeech.py. Do not hand-edit.\n" + "//\n" + "// Space Taxi's digitised speech as the C64 stores it: 1-bit\n" + "// run-length streams. Each byte is one half-cycle length; the\n" + "// player toggles its output after each. $FE is a raster sync\n" + "// and $FF ends an utterance. stAudio.c expands these to PCM\n" + "// through the jlAudioPlaySfxStream fill callback.\n\n" + "#ifndef ST_SPEECH_DATA_H\n#define ST_SPEECH_DATA_H\n\n" + "#include \n\n" + "typedef struct {\n" + " uint8_t code; // PETSCII: 'H' 'T' '!' 'P' '1'..'9' '?' 'U'\n" + " uint16_t offset; // into kStSpeechRle\n" + " uint16_t length;\n" + "} StSpeechEntryT;\n\n" + "#define ST_SPEECH_COUNT %du\n" + "#define ST_SPEECH_RLE_BYTES %du\n\n" + "extern const StSpeechEntryT kStSpeechIndex[ST_SPEECH_COUNT];\n" + "extern const uint8_t kStSpeechRle[ST_SPEECH_RLE_BYTES];\n\n" + "#endif\n" % (len(index), len(data))) + with open(c, "w") as fp: + fp.write('// Generated by assets/extractSpeech.py. Do not hand-edit.\n\n' + '#include "stSpeechData.h"\n\n' + 'const StSpeechEntryT kStSpeechIndex[ST_SPEECH_COUNT] = {\n') + for code, off, ln in index: + fp.write(" { 0x%02Xu, %5du, %5du }, // '%s'\n" + % (code, off, ln, chr(code) if 33 <= code < 127 else "?")) + fp.write("};\n\nconst uint8_t kStSpeechRle[ST_SPEECH_RLE_BYTES] = {\n") + for i in range(0, len(data), 16): + fp.write(" " + " ".join("0x%02X," % b for b in data[i:i + 16]) + "\n") + fp.write("};\n") + print("\nemitted %s (%d entries) and %s (%d bytes)" % (h, len(index), c, len(data))) + + +def main(): + if len(sys.argv) < 2: + sys.exit(__doc__) + emit = "--c" in sys.argv + args = [a for a in sys.argv[1:] if a != "--c"] + mem = loadDump(args[0]) + outDir = args[1] if len(args) > 1 else "speech" + os.makedirs(outDir, exist_ok=True) + + if mem[0xA004:0xA00C] == b"CBMBASIC": + sys.stderr.write("WARNING: $A000 holds BASIC ROM, not the RAM under it.\n" + " Utterances at $A000 and above will not decode.\n" + " Re-dump with `bank ram` (see the module docstring).\n\n") + + table = readTable(mem) + romLo = 0xA000 if mem[0xA004:0xA00C] == b"CBMBASIC" else 0x10000 + ordered = sorted(p for _, p in table) + good = 0 + + print("%-4s %-6s %7s %7s %s" % ("code", "ptr", "bytes", "dur(s)", "file")) + for code, ptr in table: + label = chr(code) if 33 <= code < 127 else "?" + # Utterances are stored back to back, so the next pointer is the + # expected end. Both checks matter: BASIC ROM is full of $FF, so + # "found a terminator" alone happily reports success on garbage -- + # it claimed all 15 decoded from a ROM-shadowed dump. + nxt = next((q for q in ordered if q > ptr), None) + expected = (nxt - ptr) if nxt else None + if ptr >= romLo: + print("%-4s $%04X %7s %7s -- SKIPPED (shadowed by BASIC ROM in this dump)" + % (label, ptr, "-", "-")) + continue + runs, nbytes, sawEnd = decode(mem, ptr) + if not sawEnd: + print("%-4s $%04X %7d %7s -- SKIPPED (no $FF terminator)" % (label, ptr, nbytes, "-")) + continue + # The terminator must land BEFORE the next utterance starts. + # (Entries are back to back but may carry a pad byte, so this is + # <= rather than ==.) Overrunning means the $FF we found was not + # ours -- which is exactly what happens against a ROM shadow. + if expected is not None and nbytes > expected: + print("%-4s $%04X %7d %7s -- SKIPPED (ran %d bytes, next entry is %d away)" + % (label, ptr, nbytes, "-", nbytes, expected)) + continue + path = os.path.join(outDir, "%02X_%s.wav" % (code, label if label != "?" else "x")) + dur = writeWav(runs, path) + good += 1 + print("%-4s $%04X %7d %7.2f %s" % (label, ptr, nbytes, dur, os.path.basename(path))) + print("\n%d of %d utterances decoded into %s" % (good, len(table), outDir)) + if emit: + emitC(mem, table, romLo, outDir) + + +if __name__ == "__main__": + main() diff --git a/examples/spacetaxi/assets/genC64Data.py b/examples/spacetaxi/assets/genC64Data.py index c0feaa5..6c6f130 100644 --- a/examples/spacetaxi/assets/genC64Data.py +++ b/examples/spacetaxi/assets/genC64Data.py @@ -55,6 +55,21 @@ def c_bytes(data, per_line=16, indent=" "): return "\n".join(lines) +# The cross-segment accessors the IIgs needs (see the block this emits +# into stC64Data.c). These used to be hand-added to the GENERATED file, +# which meant re-running this script silently deleted them and broke the +# IIgs build -- the header kept declaring them and nothing defined them. +# Prototype and return expression per accessor; the header prints the +# prototypes aligned, so keep the spacing. +ACCESSORS = [ + ("const uint8_t *stC64Charset(void);", "&kStCharset[0][0]"), + ("const uint8_t *stC64SpriteBitmap(uint8_t idx);", "kStSpriteBitmaps[idx]"), + ("uint8_t stC64RngByte(uint8_t idx);", "kStRngTable[idx]"), + ("const uint8_t *stC64SfxProgram(uint8_t idx);", "kStSfxPrograms[idx]"), + ("uint8_t stC64FlameCel(uint8_t dirMask);", "kStFlameCelByDirMask[dirMask]"), +] + + def main(): raw = load_raw() sprite_count = SPRITE_PTR_LAST - SPRITE_PTR_FIRST + 1 @@ -100,9 +115,11 @@ def main(): 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("// Cross-segment-safe accessors (see stC64Data.c): reach the tables above") + h.append("// by an inter-segment call on the IIgs, where a direct 2-byte data") + h.append("// reference from another bank is not relocatable.") + for decl in ACCESSORS: + h.append(decl[0]) h.append("") h.append("#endif") @@ -143,11 +160,25 @@ def main(): 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("") + c.append("// ---------------------------------------------------------------------------") + c.append("// Accessors. On the IIgs the program is split across banks and a 2-byte") + c.append("// data reference cannot cross a segment (only a 3-byte JSL/JML can). These") + c.append("// tiny functions live in the same segment as the tables, so their reads") + c.append("// are intra-segment; callers in other segments reach them by a supported") + c.append("// inter-segment call. On the other ports they inline away to nothing.") + c.append("// ---------------------------------------------------------------------------") + first = True + for decl, body in ACCESSORS: + c.append("") + if not first: + c.append("") + first = False + # The header pads return types to align its prototypes; the + # definitions here must not inherit that padding. + c.append(" ".join(decl[:-1].split()) + " {") + c.append(" return " + body + ";") + c.append("}") with open(OUT_H, "w", encoding="ascii") as fp: fp.write("\n".join(h) + "\n") diff --git a/examples/spacetaxi/assets/genSongData.py b/examples/spacetaxi/assets/genSongData.py new file mode 100644 index 0000000..8610eab --- /dev/null +++ b/examples/spacetaxi/assets/genSongData.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# Bake examples/spacetaxi/assets/songs/song*.song into stSongData.h. +# +# python3 examples/spacetaxi/assets/genSongData.py +# +# The .song sources come from the game's own player run under a 6502 +# emulator: assets/sidCapture.py logs every SID write per PAL frame and +# assets/sidToSong.py turns that into songbake text. +import os +import subprocess +import sys +import tempfile + +here = os.path.dirname(os.path.abspath(__file__)) +repo = os.path.abspath(os.path.join(here, "..", "..", "..")) +songbake = os.path.join(repo, "tools", "songbake", "songbake.py") +out = os.path.join(here, "..", "stSongData.h") +count = 9 + +header = """// Space Taxi (C64) tunes as JoeyLib JYM1 streams. GENERATED by +// assets/genSongData.py -- do not hand-edit. The songs were extracted by +// running the game's own music player under a 6502 emulator +// (assets/sidCapture.py logs every SID write per PAL frame; +// assets/sidToSong.py turns that into songbake text in assets/songs/). +// +// Where the C64 plays them: +// BLOCKING via $44CF (the game spins until the tune ends; the port +// parks in ST_STATE_JINGLE with the sim frozen): +// 0..3 start of every screen, rotating on $5E9B ($6906) +// 4 level 25 start ($6935) and the bonus cab ($70B1) +// 5 game over ($5C1F), after the two-second GAME OVER! wait +// NON-BLOCKING via $CB02 (started and left to the IRQ player): +// 6 the high-score name entry screen ($4E35) -- no port screen yet +// 7 the high-score table ($4C1F, joystick UP on the title) +// 8 the level intro while the cab flies in ($459C); endless, so +// the source is trimmed to 30 s and looped +#ifndef ST_SONG_DATA_H +#define ST_SONG_DATA_H + +#include + +""" + +footer = """typedef struct { + const uint8_t *data; + uint16_t length; +} StSongT; + +#define ST_SONG_COUNT %du +#define ST_SONG_BONUS 4u +#define ST_SONG_GAME_OVER 5u +#define ST_SONG_NAME_ENTRY 6u +#define ST_SONG_SCORES 7u +#define ST_SONG_INTRO 8u + +static const StSongT kStSongs[ST_SONG_COUNT] = { +%s}; + +#endif // ST_SONG_DATA_H +""" + +parts = [] +for n in range(count): + src = os.path.join(here, "songs", "song%d.song" % n) + with tempfile.NamedTemporaryFile(suffix=".h", delete=False) as tmp: + path = tmp.name + subprocess.run([sys.executable, songbake, "-c", "gStSong%d" % n, src, path], check=True, stderr=subprocess.DEVNULL) + body = open(path).read() + os.unlink(path) + body = "\n".join(l for l in body.split("\n") if l.startswith("static") or l.startswith(" ") or l.startswith("}")) + parts.append(body.rstrip() + "\n\n") + +table = "".join(" { gStSong%d, (uint16_t)sizeof(gStSong%d) },\n" % (n, n) for n in range(count)) +with open(out, "w") as fp: + fp.write(header + "".join(parts) + footer % (count, table)) +print("wrote", os.path.normpath(out)) diff --git a/examples/spacetaxi/assets/genSpeechPcm.py b/examples/spacetaxi/assets/genSpeechPcm.py new file mode 100755 index 0000000..e707924 --- /dev/null +++ b/examples/spacetaxi/assets/genSpeechPcm.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +"""genSpeechPcm.py - bake Space Taxi's speech to 8-bit PCM. +Reads the C64 run-length speech streams from stSpeechData.c (the output +of extractSpeech.py) and writes speech.bin: every utterance decoded to +5 kHz sample bytes for each passenger pitch the game can select, in BOTH +polarities. The decode is the exact algorithm stAudio.c used to run per +sample at play time; on the 65816 that took 2.4 s of CPU per second of +speech (three run bytes per sample, each a multiply) and stalled the ride +for half a second on every "HEY TAXI". A 1-bit intermediate format came +next, but expanding bits still needed a per-byte loop (and an asm copy of +it on the IIgs); with sample bytes the player is a block copy. +Polarity: an utterance is decoded from a +100 start. The C64 keeps the +output level running from one utterance into the next, so a phrase whose +first word ends on the low level plays its second word inverted. Both +versions are stored and the player picks by the running parity. +speech.bin (little-endian): + "STSP" u8 version(2) u8 pitchBase u8 pitchCount u8 count + u8 format (0 = signed +/-100, 1 = 0x80-biased for the IIgs raw stream) + u8 pad[3] u32 dataBytes + entries[pitchCount * count * 2]: u8 code, u8 parity, u16 samples, + u32 offset -- polarity is the innermost index (0 = as decoded, + 1 = inverted). parity = 1 when the utterance ends on the + opposite level from the one it started on. + data: sample bytes +usage: genSpeechPcm.py stSpeechData.c speech.bin [--biased] +""" +import re +import struct +import sys + +SID_HZ = 1022727 # ST_SID_HZ_NUM +RATE = 5000 # ST_SPEECH_RATE +CYC_PER_SAMPLE = SID_HZ // RATE +SYNC_CYCLES = SID_HZ // 60 // 8 +CYCLE_TAIL = 33 +SYNC = 0xFE +END = 0xFF +PITCH_BASE = 2 # stFare.c: stAudioSpeech(rng(3) + 2) +PITCH_COUNT = 3 + + +def parseSource(path): + text = open(path).read() + idx = re.search(r"kStSpeechIndex\[[^\]]*\]\s*=\s*\{(.*?)\};", text, re.S).group(1) + entries = [(int(c, 16), int(o), int(n)) for c, o, n in + re.findall(r"\{\s*0x([0-9A-Fa-f]+)u,\s*(\d+)u,\s*(\d+)u\s*\}", idx)] + rle = re.search(r"kStSpeechRle\[[^\]]*\]\s*=\s*\{(.*?)\};", text, re.S).group(1) + data = bytes(int(x, 16) for x in re.findall(r"0x([0-9A-Fa-f]{2})", rle)) + return entries, data + + +def decode(rle, perOn): + """speechDecode() for one utterance from a +100 start, remain 0.""" + remain = 0 + level = 100 + out = [] + i = 0 + n = len(rle) + while True: + while remain <= 0: + if i >= n: + return out, level + b = rle[i] + i += 1 + if b == END: + i = n + continue + if b == SYNC: + remain += SYNC_CYCLES + continue + remain += perOn * b + CYCLE_TAIL + level = -100 if level > 0 else 100 + out.append(level) + remain -= CYC_PER_SAMPLE + + +def main(): + args = [a for a in sys.argv[1:] if not a.startswith("--")] + biased = "--biased" in sys.argv[1:] + src, dst = args[0], args[1] + entries, data = parseSource(src) + records = [] + blob = bytearray() + for p in range(PITCH_COUNT): + perOn = 5 * (PITCH_BASE + p) + 12 + for code, off, length in entries: + samples, level = decode(data[off:off + length], perOn) + parity = 1 if level < 0 else 0 + for pol in range(2): + pcm = bytearray() + for s in samples: + v = -s if pol else s + pcm.append((v + 0x80) & 0xFF if biased else v & 0xFF) + records.append((code, parity, len(samples), len(blob))) + blob += pcm + with open(dst, "wb") as f: + f.write(b"STSP" + struct.pack(" RTS + cpu = SidTrap(ram) + cpu.call(0xCC00) # full reset first + ram[0xCB01] = song + cpu.call(0xCB00) # select + arm + for frame in range(MAX_FRAMES): + cpu.frame = frame + ram[0xA2] = (ram[0xA2] + 1) & 0xFF + cpu.call(0xCF52) + if ram[0xCB81] == 0 and ram[0xCB88] == 0 and ram[0xCB8F] == 0: + break + songs[song] = {"frames": frame + 1, "writes": cpu.log, + "ptr": ram[0xCB40 + song * 2] | (ram[0xCB41 + song * 2] << 8)} + print(f"song {song}: ptr=${songs[song]['ptr']:04X} frames={frame+1} " + f"({(frame+1)/50:.1f}s PAL) sidWrites={len(cpu.log)}") + +json.dump(songs, open(OUT, "w")) diff --git a/examples/spacetaxi/assets/sidToSong.py b/examples/spacetaxi/assets/sidToSong.py new file mode 100644 index 0000000..43ede3f --- /dev/null +++ b/examples/spacetaxi/assets/sidToSong.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +"""SID register log (sidCapture.py) -> JoeyLib .song text for songbake. + +One row per PAL frame (tickms 20). Per voice, a note-on is a gate rising +edge or a pitch change while gated; gate falling is 'off'. SID frequency -> +Hz exactly (@Hz cells), so no equal-temperament rounding. Attenuation comes +from the sustain nibble. A voice whose waveform is NOISE goes to the noise +column instead of a tone column, with a pitch scaled from its frequency. +Loop detection: the player is deterministic, so once the (frame-relative) +event rows repeat with a fixed period, that period is the loop. +""" +import json, sys + +PAL_CLOCK = 985248.0 +songs = json.load(open(sys.argv[1])) +outDir = sys.argv[2] + + +def sidHz(lo, hi): + return ((hi << 8) | lo) * PAL_CLOCK / 16777216.0 + + +def rowsFor(writes, nframes): + # replay register writes frame by frame, producing per-frame voice state + regs = [0] * 0x19 + state = [] + byFrame = {} + for f, r, v in writes: + byFrame.setdefault(f, []).append((r, v)) + for f in range(nframes): + for r, v in byFrame.get(f, []): + regs[r] = v + vs = [] + for vc in range(3): + b = vc * 7 + vs.append(dict(hz=sidHz(regs[b], regs[b + 1]), gate=regs[b + 4] & 1, + wave=regs[b + 4] & 0xF0, sus=regs[b + 6] >> 4)) + state.append(vs) + return state + + +def atten(sus): + # SID sustain 0..15 -> AGI attenuation 14..0 (0 loudest) + return max(0, min(14, round(14 - sus * 14 / 15))) + + +def noisePitch(hz): + # brighter hiss for higher SID "frequency"; 0..31, 0 = brightest + p = int(31 - min(31, hz / 400.0)) + return max(0, min(31, p)) + + +def toRows(state): + rows = [] + prev = [dict(hz=None, gate=0, wave=0, sus=0) for _ in range(3)] + for vs in state: + tone = ["---", "---", "---"] + noise = "---" + for vc in range(3): + cur, was = vs[vc], prev[vc] + isNoise = (cur["wave"] & 0x80) != 0 + on = cur["gate"] and (not was["gate"] or abs(cur["hz"] - was["hz"]) > 0.5 + or cur["wave"] != was["wave"]) + off = was["gate"] and not cur["gate"] + if on: + if isNoise: + noise = f"N{noisePitch(cur['hz'])}:{atten(cur['sus'])}" + else: + hz = max(1, min(65535, int(round(cur["hz"])))) + tone[vc] = f"@{hz}:{atten(cur['sus'])}" + elif off: + if (was["wave"] & 0x80): + noise = "off" + else: + tone[vc] = "off" + prev[vc] = dict(cur) + rows.append((tone, noise)) + return rows + + +def findLoop(rows): + # smallest (offset, period) with rows[o+k] == rows[o+p+k] for the tail + n = len(rows) + for p in range(8, n // 2): + for o in range(0, n - 2 * p): + if rows[o:o + p] == rows[o + p:o + 2 * p] and rows[o:n - p] == rows[o + p:n]: + return o, p + return None + + +for num, s in sorted(songs.items(), key=lambda kv: int(kv[0])): + num = int(num) + st = rowsFor(s["writes"], s["frames"]) + rows = toRows(st) + loop = findLoop(rows) if s["frames"] >= 4000 else None + if loop: + o, p = loop + rows = rows[:o + p] + lines = ["; Space Taxi (C64) song %d, extracted by running the game's own" % num, + "; player ($CB00/$CF52) under cpu6502.py and logging SID writes.", + "; One row = one PAL frame.", "tickms 20"] + for i, (tone, noise) in enumerate(rows): + if loop and i == loop[0]: + lines.append("loop") + cells = tone + ([noise] if noise != "---" or True else []) + lines.append(" ".join(cells)) + open(f"{outDir}/song{num}.song", "w").write("\n".join(lines) + "\n") + events = sum(1 for t, n in rows for c in t + [n] if c != "---") + waves = sorted({hex(v["wave"]) for vs in st for v in vs if v["gate"]}) + print(f"song {num}: {len(rows)} rows, {events} events, waveforms {waves}" + + (f", LOOP at row {loop[0]} period {loop[1]}" if loop else "")) diff --git a/examples/spacetaxi/assets/songs/song0.song b/examples/spacetaxi/assets/songs/song0.song new file mode 100644 index 0000000..7e6036a --- /dev/null +++ b/examples/spacetaxi/assets/songs/song0.song @@ -0,0 +1,94 @@ +; Space Taxi (C64) song 0, extracted by running the game's own +; player ($CB00/$CF52) under cpu6502.py and logging SID writes. +; One row = one PAL frame. +tickms 20 +--- --- --- --- +@336:2 @424:2 --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@378:2 @424:2 --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@336:2 @424:2 --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@378:2 @476:2 --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@318:2 @252:2 --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @476:2 --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@283:2 @252:2 --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @476:2 --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@252:2 @252:2 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- diff --git a/examples/spacetaxi/assets/songs/song1.song b/examples/spacetaxi/assets/songs/song1.song new file mode 100644 index 0000000..e067e38 --- /dev/null +++ b/examples/spacetaxi/assets/songs/song1.song @@ -0,0 +1,94 @@ +; Space Taxi (C64) song 1, extracted by running the game's own +; player ($CB00/$CF52) under cpu6502.py and logging SID writes. +; One row = one PAL frame. +tickms 20 +--- --- --- --- +@252:7 @159:7 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +@283:7 @168:7 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +@252:7 @189:7 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +@283:7 @212:7 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +@318:7 @189:7 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +@336:7 @168:7 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +@318:7 @159:7 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +@336:7 @168:7 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +@378:7 @189:7 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- --- --- diff --git a/examples/spacetaxi/assets/songs/song2.song b/examples/spacetaxi/assets/songs/song2.song new file mode 100644 index 0000000..fe58aa5 --- /dev/null +++ b/examples/spacetaxi/assets/songs/song2.song @@ -0,0 +1,86 @@ +; Space Taxi (C64) song 2, extracted by running the game's own +; player ($CB00/$CF52) under cpu6502.py and logging SID writes. +; One row = one PAL frame. +tickms 20 +--- --- --- --- +@126:0 @159:0 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +off --- --- --- +@126:0 @168:0 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +off --- --- --- +@141:0 @189:0 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +off --- --- --- +@159:0 @159:0 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +off --- --- --- +@159:0 @168:0 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +off --- --- --- +@168:0 @189:0 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +off --- --- --- +@168:0 @159:0 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +off --- --- --- +@189:0 @168:0 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +off --- --- --- +--- @159:0 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- @126:0 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- diff --git a/examples/spacetaxi/assets/songs/song3.song b/examples/spacetaxi/assets/songs/song3.song new file mode 100644 index 0000000..20a3f8d --- /dev/null +++ b/examples/spacetaxi/assets/songs/song3.song @@ -0,0 +1,86 @@ +; Space Taxi (C64) song 3, extracted by running the game's own +; player ($CB00/$CF52) under cpu6502.py and logging SID writes. +; One row = one PAL frame. +tickms 20 +--- --- --- --- +@252:0 @336:2 @336:2 --- +--- --- --- --- +--- --- --- --- +off off off --- +--- --- --- --- +@283:0 --- --- --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- --- --- +@318:0 @336:2 @336:2 --- +--- --- --- --- +--- --- --- --- +off off off --- +--- --- --- --- +@252:0 --- --- --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- --- --- +@283:0 @336:2 @336:2 --- +--- --- --- --- +--- --- --- --- +off off off --- +--- --- --- --- +@252:0 --- --- --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- --- --- +@283:0 @336:2 @336:2 --- +--- --- --- --- +--- --- --- --- +off off off --- +--- --- --- --- +@476:0 --- --- --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- --- --- +--- @318:2 @378:2 --- +--- --- --- --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @336:2 @378:2 --- +--- --- --- --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @283:2 @252:2 --- +--- --- --- --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @252:2 @476:2 --- +--- --- --- --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- diff --git a/examples/spacetaxi/assets/songs/song4.song b/examples/spacetaxi/assets/songs/song4.song new file mode 100644 index 0000000..e32f538 --- /dev/null +++ b/examples/spacetaxi/assets/songs/song4.song @@ -0,0 +1,126 @@ +; Space Taxi (C64) song 4, extracted by running the game's own +; player ($CB00/$CF52) under cpu6502.py and logging SID writes. +; One row = one PAL frame. +tickms 20 +--- --- --- --- +@504:5 @449:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@504:5 @449:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +@673:5 @504:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@504:5 @424:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@673:5 @504:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +@848:5 @673:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @635:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @673:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +--- --- --- --- diff --git a/examples/spacetaxi/assets/songs/song5.song b/examples/spacetaxi/assets/songs/song5.song new file mode 100644 index 0000000..ddf5ada --- /dev/null +++ b/examples/spacetaxi/assets/songs/song5.song @@ -0,0 +1,736 @@ +; Space Taxi (C64) song 5, extracted by running the game's own +; player ($CB00/$CF52) under cpu6502.py and logging SID writes. +; One row = one PAL frame. +tickms 20 +--- --- --- --- +@504:2 @159:2 @106:10 --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- @212:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @159:2 @106:10 --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- @212:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @159:2 @106:10 --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- @212:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @159:2 @106:10 --- +--- --- --- --- +--- --- --- --- +--- off --- --- +off --- --- --- +--- --- @212:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@504:2 @159:2 @106:10 --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- @212:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @159:2 @106:10 --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- @212:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @159:2 @106:10 --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- @212:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @159:2 @106:10 --- +--- --- --- --- +--- --- --- --- +--- off --- --- +off --- --- --- +--- --- @212:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@504:2 @159:2 @106:10 --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- @212:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @159:2 @106:10 --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- @212:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @159:2 @106:10 --- +--- --- --- --- +--- --- --- --- +--- off --- --- +off --- --- --- +--- --- @212:10 --- +--- --- --- --- +--- --- --- --- +--- --- off --- +--- --- --- --- +@566:2 @141:2 @100:10 --- +--- --- --- --- +--- --- --- --- +--- off --- --- +off --- --- --- +--- --- @200:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@566:2 @141:2 @100:10 --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- @200:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @141:2 @100:10 --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- @200:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @141:2 @100:10 --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- @200:10 --- +--- --- --- --- +--- --- --- --- +--- --- off --- +--- --- --- --- +--- @141:2 @100:10 --- +--- --- --- --- +--- --- --- --- +--- off --- --- +off --- --- --- +--- --- @200:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@635:2 @504:2 @94:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +--- --- @189:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- @94:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- @189:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@673:2 @566:2 @84:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +--- --- @168:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- @84:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- @168:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@755:2 @635:2 @79:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +--- --- @159:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- @79:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- @159:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@800:2 @635:2 @71:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +--- --- @141:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- @71:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- @141:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@848:2 @635:2 @67:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- @133:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- @67:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- @133:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- @67:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- @133:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- @67:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- @133:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@424:2 @318:2 @67:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +--- --- @133:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@848:2 @336:2 @252:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- off --- +--- --- --- --- +@848:2 @336:2 @252:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- off --- +--- --- --- --- +@848:2 @336:2 @252:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- off --- +--- --- --- --- +@755:2 @318:2 @225:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- off --- +--- --- --- --- +@755:2 @318:2 @225:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- off --- +--- --- --- --- +@673:2 @283:2 @212:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- off --- +--- --- --- --- +@635:2 @252:2 @189:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@566:2 @283:2 @168:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@755:2 @318:2 @126:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- @189:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- @126:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- @189:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- @126:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- @189:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- @126:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- @189:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- @126:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- @189:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@635:2 --- @126:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- @189:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@504:2 --- @126:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- @189:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@635:2 --- @126:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +--- --- @189:10 --- +--- --- --- --- +--- --- --- --- +--- --- off --- +--- --- --- --- +@755:2 @283:2 @119:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- @189:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- @119:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- @189:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@848:2 --- @119:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- @189:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- @119:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- @189:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@755:2 --- @119:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- @189:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@673:2 --- @119:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- @189:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@635:2 --- @119:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- @189:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@566:2 --- @119:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +--- --- @189:10 --- +--- --- --- --- +--- --- --- --- +--- --- off --- +--- --- --- --- +@504:2 @318:2 @126:10 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- off --- +--- --- --- --- +--- --- --- --- diff --git a/examples/spacetaxi/assets/songs/song6.song b/examples/spacetaxi/assets/songs/song6.song new file mode 100644 index 0000000..f26cc4d --- /dev/null +++ b/examples/spacetaxi/assets/songs/song6.song @@ -0,0 +1,840 @@ +; Space Taxi (C64) song 6, extracted by running the game's own +; player ($CB00/$CF52) under cpu6502.py and logging SID writes. +; One row = one PAL frame. +tickms 20 +--- --- --- --- +--- @378:1 @504:1 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @378:1 @504:1 --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @378:1 @504:1 --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @378:1 @504:1 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@94:0 --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@126:0 --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@159:0 --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@189:0 @336:1 @449:1 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @300:1 @400:1 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @336:1 @449:1 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @378:1 @504:1 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @378:1 @504:1 --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off @378:1 @504:1 --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @378:1 @504:1 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@212:0 --- --- --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@189:0 --- --- --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@159:0 --- --- --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- --- --- +--- --- --- --- +--- @378:1 @504:1 --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @378:1 @504:1 --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@141:0 @424:1 @566:1 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @378:1 @504:1 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @424:1 @566:1 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@119:0 @476:1 @635:1 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @476:1 @635:1 --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @476:1 @635:1 --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @476:1 @635:1 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@59:0 --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @476:1 @635:1 --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @476:1 @635:1 --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @476:1 @635:1 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @424:1 @566:1 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @476:1 @635:1 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@126:0 @504:1 @673:1 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @504:1 @673:1 --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @504:1 @673:1 --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @504:1 @673:1 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@63:0 --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @504:1 @673:1 --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off @504:1 @673:1 --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@126:0 @566:1 @755:1 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @504:1 @673:1 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @566:1 @755:1 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@141:0 @635:1 @848:1 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- --- --- +--- --- off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@71:0 --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @635:1 @848:1 --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off @635:1 @848:1 --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@141:0 @566:1 @848:1 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @566:1 @848:1 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @566:1 @848:1 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- --- --- +--- off off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@159:0 @534:1 @848:1 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off --- off --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- diff --git a/examples/spacetaxi/assets/songs/song7.song b/examples/spacetaxi/assets/songs/song7.song new file mode 100644 index 0000000..fea5ac3 --- /dev/null +++ b/examples/spacetaxi/assets/songs/song7.song @@ -0,0 +1,852 @@ +; Space Taxi (C64) song 7, extracted by running the game's own +; player ($CB00/$CF52) under cpu6502.py and logging SID writes. +; One row = one PAL frame. +tickms 20 +--- --- --- --- +@378:0 @1270:5 @126:7 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1346:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1270:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- @1132:5 @119:7 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1270:5 off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1346:5 --- --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@252:0 @1270:5 @106:7 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1346:5 off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1510:5 --- --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@283:0 @1008:5 @94:7 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@318:0 --- off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@336:0 --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1008:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@378:0 --- @126:7 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @952:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @848:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @755:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1132:5 @119:7 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1270:5 off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1346:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1510:5 @126:7 --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1510:5 off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1510:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@424:0 @1132:5 @94:7 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +@378:0 --- off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@336:0 @1132:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@378:0 @1270:5 @126:7 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- off --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@336:0 @952:5 @119:7 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +@318:0 --- off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@283:0 @952:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@318:0 @1008:5 @106:7 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- off --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@283:0 @755:5 @94:7 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +@252:0 --- off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@238:0 @755:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +@252:0 @1008:5 @126:7 --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @952:5 off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@318:0 @1008:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +@189:0 @1132:5 @119:7 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1008:5 off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1132:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1270:5 @106:7 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1132:5 off --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1270:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@238:0 @1346:5 @94:7 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@252:0 @1270:5 off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@283:0 @1346:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +@252:0 @1270:5 @126:7 --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1270:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@238:0 @1510:5 @119:7 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@252:0 --- off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@283:0 --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@318:0 --- @106:7 --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@283:0 @1510:5 @94:7 --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +@283:0 --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@378:0 @1132:5 @141:7 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1008:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @952:5 @119:7 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1008:5 off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1132:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @755:5 @106:7 --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@378:0 @1132:5 --- --- +off --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@378:0 --- --- --- +off --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@378:0 @1199:5 @94:7 --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@378:0 --- off --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +@378:0 --- --- --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@378:0 @1270:5 @126:7 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1510:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1270:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1510:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1270:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- @94:7 --- +--- @1510:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1270:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1510:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1270:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1510:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- @126:7 --- +--- --- --- --- +--- @1270:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1510:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1270:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1510:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1270:5 --- --- +--- --- --- --- +--- --- @94:7 --- +--- --- --- --- +--- --- --- --- +--- @1510:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1270:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1510:5 --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1270:5 --- --- +--- --- off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- @126:7 --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +off --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- off --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- diff --git a/examples/spacetaxi/assets/songs/song8.song b/examples/spacetaxi/assets/songs/song8.song new file mode 100644 index 0000000..93a08c5 --- /dev/null +++ b/examples/spacetaxi/assets/songs/song8.song @@ -0,0 +1,1508 @@ +; Space Taxi (C64) song 8, extracted by running the game's own +; player ($CB00/$CF52) under cpu6502.py and logging SID writes. +; One row = one PAL frame. +; The C64 tune never ends (no state repeat in 12000 frames); the level +; intro it underscores lasts a few seconds, so the first 1500 rows (30 s) +; are kept and looped back to the start. +tickms 20 +loop +--- --- --- --- +@31:7 @126:1 --- N28:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @178:1 --- --- +--- --- --- --- +--- off --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- @212:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @252:1 --- N30:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @336:1 --- --- +--- --- --- --- +--- off --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@33:7 --- --- N30:10 +--- --- --- --- +--- @449:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @504:1 --- N30:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @599:1 --- --- +--- --- --- --- +--- off --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- @755:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @898:1 --- N30:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@35:7 --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1008:1 --- --- +--- --- --- --- +--- off --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- @1346:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1585:1 --- N29:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1270:1 --- --- +--- --- --- --- +--- off --- N28:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- @1008:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +@33:7 --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @800:1 --- N29:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @673:1 --- --- +--- --- --- --- +--- off --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- @566:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @504:1 --- N30:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @449:1 --- --- +--- --- --- --- +@31:7 off --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- @378:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @318:1 --- N30:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @252:1 --- --- +--- --- --- --- +--- off --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- @212:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@33:7 @159:1 --- N29:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @126:1 --- --- +--- --- --- --- +--- off --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- @178:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N28:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @212:1 --- N29:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @252:1 --- --- +--- --- --- --- +--- off --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@35:7 --- --- N29:10 +--- --- --- --- +--- @336:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @449:1 --- N30:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @504:1 --- --- +--- --- --- --- +--- off --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- @599:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @755:1 --- N30:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@33:7 --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @898:1 --- --- +--- --- --- --- +--- off --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- @1008:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1346:1 --- N30:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1585:1 --- --- +--- --- --- --- +--- off --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- @1270:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +@31:7 --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1008:1 --- N29:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N28:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @800:1 --- --- +--- --- --- --- +--- off --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- @673:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @566:1 --- N29:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @504:1 --- --- +--- --- --- --- +@33:7 off --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- @449:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @378:1 --- N30:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @318:1 --- --- +--- --- --- --- +--- off --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- @252:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@35:7 @212:1 --- N30:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @159:1 --- --- +--- --- --- --- +--- off --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- @126:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @178:1 --- N29:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @212:1 --- --- +--- --- --- --- +--- off --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@33:7 --- --- N28:10 +--- --- --- --- +--- @252:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @336:1 --- N29:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @449:1 --- --- +--- --- --- --- +--- off --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- @504:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @599:1 --- N30:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@31:7 --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @755:1 --- --- +--- --- --- --- +--- off --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- @898:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1008:1 --- N30:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1346:1 --- --- +--- --- --- --- +--- off --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- @1585:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +@33:7 --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1270:1 --- N30:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1008:1 --- --- +--- --- --- --- +--- off --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- @800:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @673:1 --- N28:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @566:1 --- --- +--- --- --- --- +@35:7 off --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- @504:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @449:1 --- N30:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @378:1 --- --- +--- --- --- --- +--- off --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- @318:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@33:7 @252:1 --- N30:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @212:1 --- --- +--- --- --- --- +--- off --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- @159:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @126:1 --- N30:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @178:1 --- --- +--- --- --- --- +--- off --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@31:7 --- --- N29:10 +--- --- --- --- +--- @212:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @252:1 --- N29:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @336:1 --- --- +--- --- --- --- +--- off --- N28:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- @449:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @504:1 --- N29:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@33:7 --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @599:1 --- --- +--- --- --- --- +--- off --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- @755:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @898:1 --- N30:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1008:1 --- --- +--- --- --- --- +--- off --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- @1346:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +@35:7 --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1585:1 --- N30:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1270:1 --- --- +--- --- --- --- +--- off --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- @1008:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @800:1 --- N29:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @673:1 --- --- +--- --- --- --- +@33:7 off --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- @566:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N28:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @504:1 --- N29:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @449:1 --- --- +--- --- --- --- +--- off --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- @378:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@31:7 @318:1 --- N30:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @252:1 --- --- +--- --- --- --- +--- off --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- @212:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @159:1 --- N30:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @126:1 --- --- +--- --- --- --- +--- off --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@33:7 --- --- N30:10 +--- --- --- --- +--- @178:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @212:1 --- N30:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @252:1 --- --- +--- --- --- --- +--- off --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- @336:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @449:1 --- N29:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@35:7 --- --- N28:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @504:1 --- --- +--- --- --- --- +--- off --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- @599:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @755:1 --- N29:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @898:1 --- --- +--- --- --- --- +--- off --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- @1008:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +@33:7 --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1346:1 --- N30:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1585:1 --- --- +--- --- --- --- +--- off --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- @1270:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1008:1 --- N30:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @800:1 --- --- +--- --- --- --- +@31:7 off --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- @673:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @566:1 --- N29:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @504:1 --- --- +--- --- --- --- +--- off --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N28:10 +--- --- --- --- +--- @449:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@33:7 @378:1 --- N29:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @318:1 --- --- +--- --- --- --- +--- off --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- @252:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @212:1 --- N30:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @159:1 --- --- +--- --- --- --- +--- off --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@35:7 --- --- N30:10 +--- --- --- --- +--- @126:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @178:1 --- N30:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @212:1 --- --- +--- --- --- --- +--- off --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- @252:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @336:1 --- N30:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@33:7 --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @449:1 --- --- +--- --- --- --- +--- off --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- @504:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @599:1 --- N28:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @755:1 --- --- +--- --- --- --- +--- off --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- @898:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +@31:7 --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1008:1 --- N30:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1346:1 --- --- +--- --- --- --- +--- off --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- @1585:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1270:1 --- N30:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @1008:1 --- --- +--- --- --- --- +@33:7 off --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- @800:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @673:1 --- N30:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @566:1 --- --- +--- --- --- --- +--- off --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- @504:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@35:7 @449:1 --- N29:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @378:1 --- --- +--- --- --- --- +--- off --- N28:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- @318:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @252:1 --- N29:10 +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- N29:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- @212:1 --- --- +--- --- --- --- +--- off --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- +@33:7 --- --- N30:10 +--- --- --- --- +--- @159:1 --- --- +--- --- --- --- +--- off --- --- +--- --- --- --- +--- --- --- N30:10 +--- --- --- --- +--- --- --- --- +--- --- --- --- +--- --- --- --- diff --git a/examples/spacetaxi/spacetaxi.c b/examples/spacetaxi/spacetaxi.c index c326d5c..f9f063b 100644 --- a/examples/spacetaxi/spacetaxi.c +++ b/examples/spacetaxi/spacetaxi.c @@ -19,27 +19,20 @@ #include -#include "spacetaxi.h" -#include "stDemoStreams.h" +#include -// Diagnostics. The logger drags in vsnprintf (with newlib that pulls the -// whole formatted-output and float-conversion family, ~45 KB) plus a -// multi-KB ring buffer, and the game has no need of either. That is -// unaffordable on the IIgs, whose bank-0 BSS budget cannot spare the -// ring, and on the X68000, which ships on a Human68k floppy with about -// 330 KB of usable space for the binary AND its levels. Both compile it -// out; the ports with room keep it. -#if defined(__W65816__) || defined(JOEYLIB_PLATFORM_X68000) - #define ST_LOG_RESET() ((void)0) - #define ST_LOG(...) ((void)0) -#else - #define ST_LOG_RESET() jlLogReset() - #define ST_LOG(...) jlLogF(__VA_ARGS__) -#endif +#include "spacetaxi.h" +#include "stSongData.h" +#include "stDemoStreams.h" #define ST_TICK_HZ 30u #define ST_MAX_CATCHUP_TICKS 4u -#define ST_LEVEL_COUNT 24u +// Screens A..X, then screen Y -- the 25th, " MYSTERY SCREEN !!! " -- which +// $5167 loads once the 24-hour shifts have cleared all of A..X. Files are +// levels/level01.dat .. level25.dat, so Y is index 24. +#define ST_LEVEL_COUNT 24u // A..X, the screens a shift sweeps +#define ST_SCREEN_Y 24u // the 25th screen ($5171 LDA #$59) +#define ST_LEVEL_FILES 25u // level01.dat .. level25.dat // "GET READY" shows over the old screen while the C64 cycles the // leaving-passenger sprites (three busy waits, about half a second). #define ST_GET_READY_TICKS 15u @@ -48,6 +41,18 @@ // Menu joystick repeat delay ($4101 with X = $82). #define ST_MENU_REPEAT_TICKS 8u +// The immortal cabbies ($4A89 image, $4E35 screen). +#define ST_HS_ENTRIES 10u +#define ST_HS_NAME_CHARS 15u +#define ST_HS_ENTRY_BYTES 24u // score[7] 0 name[15] shift +#define ST_HS_POINT_AT 4u // "dddd.dd" +#define ST_HS_GREET_DIGIT_AT 23u // "CONGRATULATIONS CABBIE n," +#define ST_HS_NAME_COL 12u // $0664: row 15, column 12 +#define ST_HS_NAME_ROW 15u +#define ST_HS_CURSOR '%' // $4F4D flips it against blank +#define ST_HS_BLINK_TICKS 3u // $4101 x $1E between flips, ~55 ms +#define ST_HS_SAVE_NAME "IMMORTAL" + typedef enum { ST_STATE_TITLE = 0, ST_STATE_HIGHSCORES, @@ -57,7 +62,9 @@ typedef enum { ST_STATE_GET_READY, ST_STATE_INTRO, ST_STATE_PLAYING, - ST_STATE_GAME_OVER + ST_STATE_GAME_OVER, + ST_STATE_JINGLE, // a $44CF tune sounding; sim frozen until it ends + ST_STATE_NAME_ENTRY // $4E35: a top-ten cabbie types a name } StStateE; typedef struct { @@ -73,8 +80,39 @@ typedef struct { bool fireArmed; // $445D: FIRE must be released first uint8_t lastFrame; uint16_t paceAcc; + StStateE afterJingle; // where ST_STATE_JINGLE returns to + bool gameOverTune; // $5C24: song 5 started after the wait + uint8_t hsSlot; // $4D51 table slot being named + uint8_t hsLen; // $4D2C characters typed + uint8_t hsCursor; // $4F4D the blinking cursor's current glyph + uint8_t hsBlinkTicks; // ticks toward the next cursor flip + bool paused; // $4FCB: SHIFT is holding the game } StGameT; +// The immortal cabbies ($4A89): ten entries of a right-aligned "dddd.dd" +// score, a name of up to fifteen letters and the shift it was earned on. +// The C64 keeps this table on its own disk (the $9948 IEC routines load +// it at boot and save it after every entry); the port keeps the same +// 240-byte image in a save file. Shipped contents as the dump has them. +typedef struct { + uint8_t score[ST_NUMBER_CHARS + 1u]; // ASCII, NUL-terminated + uint8_t name[ST_HS_NAME_CHARS + 1u]; // ASCII, space-padded, NUL-terminated + uint8_t shift; // 1..5 +} StHighScoreT; + +static StHighScoreT gHighScores[ST_HS_ENTRIES] = { + { "3877.56", "MICHAEL PLATE ", 4u }, + { "1809.51", "MICHAEL PLATE ", 2u }, + { "1179.87", "ANDREAS PLATE ", 4u }, + { " 892.65", "MICHAEL PLATE ", 1u }, + { " 685.37", "ANDREAS PLATE ", 1u }, + { " 629.45", "MICHAEL PLATE ", 4u }, + { " 569.50", "MICHAEL PLATE ", 1u }, + { " 504.97", "ANDREAS PLATE ", 2u }, + { " 498.77", "ANDREAS PLATE ", 5u }, + { " 490.85", "ANDREAS PLATE ", 2u }, +}; + // Menu texts ($5347..$545D) in screen codes. static const uint8_t kTextGameVariations[] = "GAME VARIATIONS"; static const uint8_t kTextCabbies[] = "SELECT NUMBER OF CABBIES:"; @@ -94,18 +132,14 @@ static const uint8_t kTextGetReady[] = " GET READY FOR A RIDE "; static const uint8_t kTextOnTheTaxi[] = " ON THE SPACE TAXI! "; static const uint8_t kTextGameOver[] = " GAME OVER! "; static const uint8_t kTextBlank12[] = " "; -// $4C1F / $556A screens. +// $4C1F (the table) and $4E35 (the name entry) screens. static const uint8_t kTextImmortal[] = "THE IMMORTAL CABBIES"; -static const uint8_t *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 kTextShift[] = "SHIFT"; +static const uint8_t kTextReturnTitle[] = "PRESS FIRE TO RETURN TO TITLE PAGE"; +static const uint8_t kTextCongrats[] = "CONGRATULATIONS CABBIE ,"; +static const uint8_t kTextRanks[] = "YOUR SCORE RANKS YOU AS ONE OF"; +static const uint8_t kTextTopTen[] = "THE TOP TEN CABBIES OF ALL TIME"; +static const uint8_t kTextEnterName[] = "ENTER YOUR NAME BELOW"; static const uint8_t *kAboutLines[7] = { (const uint8_t *)"THIS PROGRAM DEVELOPED AND WRITTEN BY", (const uint8_t *)"JOHN F. KUTCHER", @@ -117,8 +151,9 @@ static const uint8_t *kAboutLines[7] = { }; static const uint8_t kTextReturn[] = "PRESS FIRE TO RETURN"; -static StGameT gGame; -static StSimT gSim; +static StGameT gGame; +static StSimT gSim; +static jlSurfaceT *gStage; // 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 @@ -126,17 +161,25 @@ static StSimT gSim; static StLevelT gLevel; -static void blankScreen(StSimT *sim); +static void blankScreen(StSimT *sim, uint8_t border, uint8_t bg); static void enterTitle(void); static void gameOverEnter(void); +static void gameOverFinish(void); +static void highScoreDraw(void); +static uint8_t highScoreInsert(const uint8_t *score); +static void highScoreLoad(void); +static void highScoreSave(void); static bool joyFire(void); static uint8_t joyMask(void); static bool loadLevel(uint8_t index); static uint8_t nextLevelForShift(void); static uint8_t paceTicks(void); +static bool serviceJingle(void); static void runCabbiesMenu(uint8_t ticks); static void runDemoEnd(void); static void runIntro(uint8_t ticks); +static void runNameEntry(uint8_t ticks); +static bool nameEntryStart(void); static void runPlaying(uint8_t ticks); static void runShiftMenu(uint8_t ticks); static void runTitle(uint8_t ticks); @@ -149,12 +192,12 @@ static void startNewScreen(void); // $40CA -- spaces everywhere, black. -static void blankScreen(StSimT *sim) { +static void blankScreen(StSimT *sim, uint8_t border, uint8_t bg) { memset(sim->screen, ST_CHAR_SPACE, ST_SCREEN_CELLS); memset(sim->color, 0, ST_SCREEN_CELLS); stSimDirtyAll(sim); - sim->bgColor = 0u; - sim->borderColor = 0u; + sim->bgColor = bg; + sim->borderColor = border; sim->frame.enableMask = 0u; stRenderSceneChanged(sim); } @@ -166,16 +209,239 @@ static void enterTitle(void) { if (!stLevelLoad(&gLevel, "levels/title.dat")) { ST_LOG("spacetaxi: ! title load FAILED"); } + stRenderLevelCels(gStage, 0); // the last level's cels go stTitleEnter(&gSim, &gLevel); stRenderSceneChanged(&gSim); stRenderTitleLogo(&gSim); stAudioNoise(false); stAudioSilence(); + jlMusicStop(); gGame.state = ST_STATE_TITLE; gGame.fireArmed = false; } +// $6BD0 -- one more player done; everybody done -> title. +static void gameOverFinish(void) { + gSim.playersDone++; + if (gSim.playersDone == gSim.playerCount) { + enterTitle(); + } else { + startNewScreen(); + } +} + + +// $4C1F -- the immortal cabbies: ten rows of shift, "$", score and name, +// each entry drawn only while its last score digit is a digit (a blank +// row is an unused slot). Song 7 plays under it until FIRE ($4D04). +static void highScoreDraw(void) { + uint8_t i; + + blankScreen(&gSim, 3u, 14u); + stSimDrawText(&gSim, 9u, 1u, kTextImmortal, 6u); + stSimDrawText(&gSim, 32u, 3u, kTextShift, 15u); + for (i = 0u; i < ST_HS_ENTRIES; i++) { + const StHighScoreT *e = &gHighScores[i]; + uint8_t row = (uint8_t)(4u + i * 2u); + if (e->score[ST_NUMBER_CHARS - 1u] < '0') { + continue; + } + stSimPutChar(&gSim, 34u, row, (uint8_t)('0' + e->shift)); + stSimPutColor(&gSim, 34u, row, 15u); + stSimPutChar(&gSim, 4u, row, '$'); + stSimPutColor(&gSim, 4u, row, 1u); + stSimDrawText(&gSim, 5u, row, e->score, 1u); + stSimDrawText(&gSim, 15u, row, e->name, 1u); + } + stSimDrawText(&gSim, 3u, 24u, kTextReturnTitle, 0u); + stAudioMusic(ST_SONG_SCORES, false); // $4C29 +} + + +// $4D92 -- where a score belongs in the table, or ST_HS_ENTRIES when it +// does not make it. The table is only opened to scores of $50.00 and up +// (second character a digit, or third character 5 or more). The scores +// are right-aligned text, so a character compare orders them; a tie goes +// above the older entry. The new entry is opened with a blank name. +static uint8_t highScoreInsert(const uint8_t *score) { + uint8_t slot; + uint8_t k; + + if (score[1] < '0' && score[2] < '5') { + return ST_HS_ENTRIES; + } + for (slot = 0u; slot < ST_HS_ENTRIES; slot++) { + const uint8_t *old = gHighScores[slot].score; + for (k = 0u; k < ST_NUMBER_CHARS; k++) { + if (score[k] != old[k]) { + break; + } + } + if (k == ST_NUMBER_CHARS || score[k] > old[k]) { + break; + } + } + if (slot == ST_HS_ENTRIES) { + return ST_HS_ENTRIES; + } + for (k = (uint8_t)(ST_HS_ENTRIES - 1u); k > slot; k--) { + gHighScores[k] = gHighScores[k - 1u]; + } + memcpy(gHighScores[slot].score, score, ST_NUMBER_CHARS + 1u); + memset(gHighScores[slot].name, ' ', ST_HS_NAME_CHARS); + gHighScores[slot].name[ST_HS_NAME_CHARS] = 0u; + gHighScores[slot].shift = 0u; + return slot; +} + + +// $23E5 -> $9948 at boot: the table from disk, when there is one. The +// image is the C64's own: per entry score[7], 0, name[15], shift. +static void highScoreLoad(void) { + uint8_t image[ST_HS_ENTRIES * ST_HS_ENTRY_BYTES]; + uint8_t i; + + if (jlSaveRead(ST_HS_SAVE_NAME, image, sizeof(image)) != sizeof(image)) { + return; + } + for (i = 0u; i < ST_HS_ENTRIES; i++) { + const uint8_t *e = &image[i * ST_HS_ENTRY_BYTES]; + if (e[ST_NUMBER_CHARS] != 0u) { + ST_LOG("spacetaxi: ! %s is not a score table", ST_HS_SAVE_NAME); + return; + } + } + for (i = 0u; i < ST_HS_ENTRIES; i++) { + const uint8_t *e = &image[i * ST_HS_ENTRY_BYTES]; + memcpy(gHighScores[i].score, e, ST_NUMBER_CHARS); + gHighScores[i].score[ST_NUMBER_CHARS] = 0u; + memcpy(gHighScores[i].name, &e[ST_NUMBER_CHARS + 1u], ST_HS_NAME_CHARS); + gHighScores[i].name[ST_HS_NAME_CHARS] = 0u; + gHighScores[i].shift = e[ST_HS_ENTRY_BYTES - 1u]; + } +} + + +// $4E11 -> $9948 after an entry: the table back to disk. +static void highScoreSave(void) { + uint8_t image[ST_HS_ENTRIES * ST_HS_ENTRY_BYTES]; + uint8_t i; + + for (i = 0u; i < ST_HS_ENTRIES; i++) { + uint8_t *e = &image[i * ST_HS_ENTRY_BYTES]; + memcpy(e, gHighScores[i].score, ST_NUMBER_CHARS); + e[ST_NUMBER_CHARS] = 0u; + memcpy(&e[ST_NUMBER_CHARS + 1u], gHighScores[i].name, ST_HS_NAME_CHARS); + e[ST_HS_ENTRY_BYTES - 1u] = gHighScores[i].shift; + } + if (!jlSaveWrite(ST_HS_SAVE_NAME, image, sizeof(image))) { + ST_LOG("spacetaxi: ! could not save %s", ST_HS_SAVE_NAME); + } +} + + +// $4D52 -- after a player's GAME OVER: their score as "dddd.dd" text, and +// if it ranks, the $4E35 screen: red border, grey field, the cabbie's +// number in the greeting, the score at the top, song 6, and a blinking +// cursor where the name goes. Returns false when the score did not rank +// and the game moves straight on. +static bool nameEntryStart(void) { + uint8_t score[ST_NUMBER_CHARS + 1u]; + uint8_t greet[sizeof(kTextCongrats)]; + uint8_t k; + + for (k = 0u; k < ST_NUMBER_CHARS; k++) { + uint8_t ch = gSim.scoreBackup[gSim.player][k]; + if (ch == ST_CHAR_BLANK) { + score[k] = ' '; + } else { + uint8_t d = (uint8_t)(ch - ST_CHAR_DIGIT_BASE); // $4345 + score[k] = (uint8_t)('0' + ((d == 10u) ? 0u : d)); + } + } + score[ST_HS_POINT_AT] = '.'; + score[ST_NUMBER_CHARS] = 0u; + gGame.hsSlot = highScoreInsert(score); + if (gGame.hsSlot == ST_HS_ENTRIES) { + return false; + } + blankScreen(&gSim, 2u, 15u); + while (jlInputGetChar() >= 0) { // $0277 = 0: nothing typed earlier counts + } + memcpy(greet, kTextCongrats, sizeof(greet)); + greet[ST_HS_GREET_DIGIT_AT] = (uint8_t)('1' + gSim.player); + stSimDrawText(&gSim, 7u, 3u, greet, 0u); + stSimDrawText(&gSim, 5u, 5u, kTextRanks, 0u); + stSimDrawText(&gSim, 4u, 7u, kTextTopTen, 0u); + stSimDrawText(&gSim, 9u, 9u, kTextEnterName, 0u); + stSimDrawText(&gSim, 16u, 0u, score, 6u); + stSimPutChar(&gSim, 15u, 0u, '$'); + stSimPutColor(&gSim, 15u, 0u, 6u); + stAudioMusic(ST_SONG_NAME_ENTRY, false); // $4EB6 + gGame.hsLen = 0u; + gGame.hsCursor = ST_HS_CURSOR; + gGame.hsBlinkTicks = 0u; + gGame.state = ST_STATE_NAME_ENTRY; + return true; +} + + +// $4ED8 -- the entry loop: the cursor flips between "%" and blank, then +// the keyboard: letters, space, ".", "!" and "," go in (fifteen at most), +// DEL takes one back, RETURN keeps the name (a "*" when none was typed), +// stamps the shift, stops the tune and saves the table. +static void runNameEntry(uint8_t ticks) { + StHighScoreT *e = &gHighScores[gGame.hsSlot]; + uint8_t col = (uint8_t)(ST_HS_NAME_COL + gGame.hsLen); + int c; + + gGame.hsBlinkTicks = (uint8_t)(gGame.hsBlinkTicks + ticks); + if (gGame.hsBlinkTicks >= ST_HS_BLINK_TICKS) { + gGame.hsBlinkTicks = 0u; + gGame.hsCursor ^= ST_HS_CURSOR ^ ' '; + stSimPutChar(&gSim, col, ST_HS_NAME_ROW, gGame.hsCursor); + stSimPutColor(&gSim, col, ST_HS_NAME_ROW, 6u); + } + while ((c = jlInputGetChar()) >= 0) { + if (c >= 'a' && c <= 'z') { + c = c - 'a' + 'A'; + } + if (c == JL_CHAR_RETURN) { + stSimPutChar(&gSim, col, ST_HS_NAME_ROW, ' '); + if (gGame.hsLen == 0u) { + e->name[0] = '*'; + } + e->shift = gGame.variation; + jlMusicStop(); // $44E1 + highScoreSave(); // $4E11 + gameOverFinish(); + return; + } + if (c == JL_CHAR_BACKSPACE || c == JL_CHAR_DELETE) { + if (gGame.hsLen != 0u) { + stSimPutChar(&gSim, col, ST_HS_NAME_ROW, ' '); + gGame.hsLen--; + col--; + e->name[gGame.hsLen] = ' '; + } + continue; + } + if (c != ' ' && c != '.' && c != '!' && c != ',' && (c < 'A' || c > 'Z')) { + continue; + } + if (gGame.hsLen == ST_HS_NAME_CHARS) { + continue; + } + e->name[gGame.hsLen] = (uint8_t)c; + stSimPutChar(&gSim, col, ST_HS_NAME_ROW, (uint8_t)c); + stSimPutColor(&gSim, col, ST_HS_NAME_ROW, 6u); + gGame.hsLen++; + col++; + } +} + + // $5FBB -- "GAME OVER!" over the play field, sprites hidden. static void gameOverEnter(void) { stSimDrawText(&gSim, 14u, 10u, kTextBlank12, 1u); @@ -184,8 +450,9 @@ static void gameOverEnter(void) { gSim.frame.enableMask = 0u; stAudioNoise(false); stAudioSilence(); - gGame.waitTicks = ST_GAME_OVER_TICKS; - gGame.state = ST_STATE_GAME_OVER; + gGame.waitTicks = ST_GAME_OVER_TICKS; + gGame.gameOverTune = false; + gGame.state = ST_STATE_GAME_OVER; } @@ -222,7 +489,7 @@ static uint8_t joyMask(void) { static bool loadLevel(uint8_t index) { char path[32]; - if (index >= ST_LEVEL_COUNT) { + if (index >= ST_LEVEL_FILES) { return false; } // Hand-rolled "levels/levelNN.dat" (NN = index+1, 01..24) so the @@ -238,6 +505,7 @@ static bool loadLevel(uint8_t index) { ST_LOG("spacetaxi: ! cannot load %s", path); return false; } + stRenderLevelCels(gStage, &gLevel); return true; } @@ -254,7 +522,13 @@ static uint8_t nextLevelForShift(void) { } return (uint8_t)((gGame.variation - 1u) * 8u + sim->levelState); } - if (sim->levelState >= ST_LEVEL_COUNT) { + // $5022/$5167: the 24-hour shifts play screen Y after A..X, and only + // then is the shift over -- which is also what lights the "finished" + // marker in the screens-completed field (levelState reaches 25). + if (sim->levelState == ST_LEVEL_COUNT) { + return ST_SCREEN_Y; + } + if (sim->levelState > ST_LEVEL_COUNT) { return 0xFFu; } if (gGame.variation == 4u) { @@ -355,6 +629,7 @@ static void runIntro(uint8_t ticks) { } for (k = 0u; k < (uint8_t)(ticks * 2u); k++) { if (stIntroStep(&gSim)) { + jlMusicStop(); // $4709 stSimEnterLevel(&gSim, &gLevel); stRenderSceneChanged(&gSim); if (gGame.demo) { @@ -362,6 +637,7 @@ static void runIntro(uint8_t ticks) { stSimSetDemoStream(&gSim, e->stream, e->length); } gGame.state = ST_STATE_PLAYING; + serviceJingle(); return; } } @@ -377,6 +653,9 @@ static void runPlaying(uint8_t ticks) { StTickResultE r = stSimTick(&gSim); stAudioTick(); if (r == ST_TICK_CONTINUE) { + if (serviceJingle()) { + return; // $70D4 bonus cab: the tune blocks + } continue; } if (gGame.demo) { @@ -390,6 +669,9 @@ static void runPlaying(uint8_t ticks) { case ST_TICK_LIFE_LOST: stSimRespawn(&gSim); stRenderSceneChanged(&gSim); + if (serviceJingle()) { + return; // $6906 on the new cab + } break; case ST_TICK_PLAYER_OUT: gameOverEnter(); @@ -401,6 +683,27 @@ static void runPlaying(uint8_t ticks) { } +// Start a tune the sim asked for and park the loop in ST_STATE_JINGLE +// until it ends. Returns true when a tune was started, so callers can +// stop ticking the sim for this frame -- the C64's $44CF blocks there. +static bool serviceJingle(void) { + uint8_t song; + + if (gSim.jingleRequest == 0u) { + return false; + } + song = (uint8_t)(gSim.jingleRequest - 1u); + gSim.jingleRequest = 0u; + stAudioJingle(song); + if (!stAudioJingleActive()) { + return false; // no audio: nothing to wait for + } + gGame.afterJingle = gGame.state; + gGame.state = ST_STATE_JINGLE; + return true; +} + + // $54F8 -- UP/DOWN pick the shift, FIRE confirms and starts the game. static void runShiftMenu(uint8_t ticks) { uint8_t mask = joyMask(); @@ -462,13 +765,7 @@ static void runTitle(uint8_t ticks) { 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); + highScoreDraw(); gGame.state = ST_STATE_HIGHSCORES; gGame.fireArmed = false; return; @@ -477,7 +774,7 @@ static void runTitle(uint8_t ticks) { static const uint8_t kAboutCols[7] = { 1u, 12u, 15u, 4u, 5u, 6u, 1u }; static const uint8_t kAboutRows[7] = { 3u, 5u, 7u, 10u, 12u, 14u, 18u }; uint8_t i; - blankScreen(&gSim); + blankScreen(&gSim, 12u, 15u); // $558E: grey border, light-grey field for (i = 0u; i < 7u; i++) { stSimDrawText(&gSim, kAboutCols[i], kAboutRows[i], kAboutLines[i], 1u); } @@ -500,7 +797,7 @@ static void shiftMenuColor(uint8_t sel, uint8_t color) { // $5295 -- GAME VARIATIONS page 1. static void showCabbiesMenu(void) { - blankScreen(&gSim); + blankScreen(&gSim, 0u, 0u); stSimDrawText(&gSim, 13u, 0u, kTextGameVariations, 5u); stSimDrawText(&gSim, 1u, 2u, kTextCabbies, 7u); stSimDrawText(&gSim, 5u, 4u, kTextLeftRight, 11u); @@ -518,7 +815,7 @@ static void showCabbiesMenu(void) { static void showShiftMenu(void) { uint8_t i; - blankScreen(&gSim); + blankScreen(&gSim, 0u, 0u); stSimDrawText(&gSim, 1u, 8u, kTextSelectShift, 7u); for (i = 0u; i < 5u; i++) { stSimDrawText(&gSim, 2u, (uint8_t)(10u + i * 2u), kTextShifts[i], 6u); @@ -560,6 +857,11 @@ static void startDemoScreen(void) { static void startIntro(void) { stIntroEnter(&gSim, &gLevel); stRenderSceneChanged(&gSim); + // After the scene change, which turns it back off. + stRenderIntroStars(); + // $459C: the intro's tune plays under the fly-in, not blocking; the + // C64 leaves it to the IRQ player and stops it at $4709. + stAudioMusic(ST_SONG_INTRO, true); gGame.state = ST_STATE_INTRO; } @@ -601,9 +903,15 @@ int main(void) { jlConfigT config; jlSurfaceT *stage; -#if defined(JOEYLIB_PLATFORM_IIGS) - // One 64 KB bank: enough for the cab, exhaust and passenger cels. - config.codegenBytes = 60UL * 1024; + // The codegen arena holds every compiled cel, the baked ones too: the + // 37 cab/exhaust/passenger cels plus a level's hook cels (level I has + // 30). On the IIgs that is ~230 KB of 65816 code in 64 KB banks, and + // the arena grants as many banks as the machine has (a 1 MB machine + // gets one; whatever does not fit draws interpreted). The 68k ports' + // cels are far larger (9-13 KB each) and their JIT is fast, so they + // keep a modest arena and compile at level entry. +#if defined(JOEYLIB_PLATFORM_IIGS) || defined(JOEYLIB_PLATFORM_DOS) + config.codegenBytes = 256UL * 1024; #else config.codegenBytes = 160UL * 1024; #endif @@ -618,6 +926,7 @@ int main(void) { jlShutdown(); return 1; } + gStage = stage; memset(&gGame, 0, sizeof(gGame)); memset(&gSim, 0, sizeof(gSim)); stRenderInit(stage); @@ -630,6 +939,7 @@ int main(void) { stSimSeedDemoBuffer(&gSim, kDemoBufferInit, ST_DEMO_BUFFER_BYTES); gSim.waveIdx = 3u; stRenderPrewarm(stage, &gSim); + highScoreLoad(); // $23E5 enterTitle(); gGame.lastFrame = (uint8_t)jlFrameCount(); @@ -637,8 +947,39 @@ int main(void) { uint8_t ticks; jlInputPoll(); - if (jlKeyPressed(KEY_ESCAPE)) { - break; + // $4FCB runStopWatcher, on the game tick, the intro and the menus: + // SHIFT held freezes everything with the sound gated off and any + // tune stopped (the intro's restarts on release); RUN/STOP -- ESC + // here -- abandons the game for the title, and on the title it + // leaves the program (the C64 had nowhere else to go). + if (gGame.state != ST_STATE_JINGLE && gGame.state != ST_STATE_GAME_OVER && gGame.state != ST_STATE_NAME_ENTRY) { + if (jlKeyDown(KEY_LSHIFT) || jlKeyDown(KEY_RSHIFT)) { + if (!gGame.paused) { + gGame.paused = true; + stAudioSilence(); + stAudioNoise(false); + jlMusicStop(); + } + stRenderFrame(stage, &gSim); + jlAudioFrameTick(); + continue; + } + if (gGame.paused) { + gGame.paused = false; + gGame.lastFrame = (uint8_t)jlFrameCount(); // the held time is not owed + gGame.paceAcc = 0u; + if (gGame.state == ST_STATE_INTRO) { + stAudioMusic(ST_SONG_INTRO, true); // $46B5 + } + } + if (jlKeyPressed(KEY_ESCAPE)) { + if (gGame.state == ST_STATE_TITLE) { + break; + } + jlMusicStop(); // $5000 + gGame.demo = false; + enterTitle(); + } } ticks = paceTicks(); switch (gGame.state) { @@ -677,19 +1018,33 @@ int main(void) { case ST_STATE_PLAYING: runPlaying(ticks); break; + case ST_STATE_JINGLE: + // The C64 spins in $44D5 until the tune ends. A joystick + // press ends the attract demo here too ($4FE2 stops the + // music on input). + if (gGame.demo && (joyMask() & 0x1Fu) != 0u) { + jlMusicStop(); + runDemoEnd(); + } else if (!stAudioJingleActive()) { + gGame.state = gGame.afterJingle; + } + break; case ST_STATE_GAME_OVER: 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(); - } + } else if (!gGame.gameOverTune) { + // $5C24: after the GAME OVER! wait, song 5 plays out. + gGame.gameOverTune = true; + stAudioJingle(ST_SONG_GAME_OVER); + } else if (stAudioJingleActive()) { + // still sounding + } else if (!nameEntryStart()) { + gameOverFinish(); } break; + case ST_STATE_NAME_ENTRY: + runNameEntry(ticks); + break; default: enterTitle(); break; diff --git a/examples/spacetaxi/spacetaxi.h b/examples/spacetaxi/spacetaxi.h index 4acad39..bf9730d 100644 --- a/examples/spacetaxi/spacetaxi.h +++ b/examples/spacetaxi/spacetaxi.h @@ -15,6 +15,21 @@ #include "stSim.h" +// Diagnostics. The logger drags in vsnprintf (with newlib that pulls the +// whole formatted-output and float-conversion family, ~45 KB) plus a +// multi-KB ring buffer, and the game has no need of either. That is +// unaffordable on the IIgs, whose bank-0 BSS budget cannot spare the +// ring, and on the X68000, which ships on a Human68k floppy with about +// 330 KB of usable space for the binary AND its levels. Both compile it +// out; the ports with room keep it. +#if defined(__W65816__) || defined(JOEYLIB_PLATFORM_X68000) + #define ST_LOG_RESET() ((void)0) + #define ST_LOG(...) ((void)0) +#else + #define ST_LOG_RESET() jlLogReset() + #define ST_LOG(...) jlLogF(__VA_ARGS__) +#endif + // Level loading through the JoeyLib data path (DATA/levels/...). bool stLevelLoad(StLevelT *out, const char *path); @@ -35,7 +50,13 @@ void stRenderSceneChanged(StSimT *sim); // cycle are palette writes instead of repainting 103 (and 407) cells. // Call after stRenderSceneChanged, which turns it back off. void stRenderTitleLogo(StSimT *sim); +// Level intro only: cycle the starfield's ring palette slots so the warp +// animates without a single pixel moving. Call after +// stRenderSceneChanged, which turns it back off. +void stRenderIntroStars(void); void stRenderPrewarm(jlSurfaceT *stage, StSimT *sim); +// Ready a level's hook cels before its ride (evicts the previous level's). +void stRenderLevelCels(jlSurfaceT *stage, const StLevelT *level); // Audio sink setup and the per-tick release timers ($4320). void stAudioInit(void); diff --git a/examples/spacetaxi/stAudio.c b/examples/spacetaxi/stAudio.c index 5ac786a..8d3eefe 100644 --- a/examples/spacetaxi/stAudio.c +++ b/examples/spacetaxi/stAudio.c @@ -8,8 +8,18 @@ // time; envelopes and waveforms are approximated with an attenuation // ramp. The speech samples of the original are not extracted; each // spoken character becomes a short chirp so the cues still land. +// +// UPDATE: the speech IS extracted now (assets/extractSpeech.py), so the +// chirp is gone. stSpeechData.c carries the C64's own 1-bit run-length +// streams; assets/genSpeechPcm.py bakes them to sample bytes +// (DATA/speech.bin) and speechFill below streams those through +// jlAudioPlaySfxStream. + +#include #include "spacetaxi.h" +#include +#include "stSongData.h" // SID voice index in a program's byte 8 -> JoeyLib voice slot. @@ -25,10 +35,44 @@ #define ST_SID_HZ_NUM 1022727UL #define ST_SID_HZ_DEN 16777216UL -// 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 +// ---- speech ---- +// +// The C64 player ($9802) busy-waits a delay loop per stream byte and then +// toggles its 1-bit output, so a byte is one half-cycle. The loop costs +// (5k + 12) cycles per pass plus a 33-cycle tail, where k is the inner +// count at $98A2 -- normally 3, giving 27. A code below $09 does not +// speak: it REWRITES k, which is how the original gives each fare a +// slightly different voice pitch. stAudioSpeech honours that. +#define ST_SPEECH_SLOT 0u // slots 1..3 are the SID voices +#define ST_SPEECH_RATE 5000u // enough for a 1-bit source +#define ST_SPEECH_PITCH_BASE 3u // $98A2's power-on value +#define ST_SPEECH_PITCH_MAX 15u // the player's AND #$0F +// Longest phrase is "PX?" -- pad, digit, please. +#define ST_SPEECH_QUEUE 4u +// The speech is baked to sample bytes per pitch, in both polarities, by +// assets/genSpeechPcm.py (DATA/speech.bin, format in that script), so +// playing it is a block copy. Decoding the C64's run-length stream at +// play time cost the 65816 2.4 s of CPU per second of speech and stalled +// the ride half a second on every "HEY TAXI"; the 1-bit intermediate +// format that followed still needed a per-byte expansion loop. +#define ST_SPEECH_FILE "speech.bin" +#define ST_SPEECH_VERSION 2u +#define ST_SPEECH_HEADER_BYTES 16u +#define ST_SPEECH_ENTRY_BYTES 8u +#define ST_SPEECH_POLARITIES 2u +#define ST_SPEECH_FORMAT_SIGNED 0u +#define ST_SPEECH_FORMAT_BIASED 1u +#define ST_SPEECH_LOAD_CHUNK 16384u +#define ST_SPEECH_STAGE_MASK (ST_SPEECH_STAGE_BYTES - 1u) +// Decode-ahead staging: a whole engine chunk (7168 at 5 kHz, 1.4 s) plus +// slack. The per-tick slice covers playback (5000/s is ~330 a tick) +// with room to spare and is cheap enough to never need a burst; a +// starved pull still copies ST_SPEECH_STARVED_SLICE on the spot rather +// than ending the stream. +#define ST_SPEECH_STAGE_BYTES 8192u +#define ST_SPEECH_TICK_SLICE 384u +#define ST_SPEECH_STARVED_SLICE 1024u +#define ST_SPEECH_ARM_SLICE 1024u // Thrust sweep register value -> Hz: the C64 writes the same byte to // both frequency bytes, so the word is value * 257. @@ -44,10 +88,46 @@ typedef struct { static StSfxSlotT gSfx; // SID voice 1 (programs + sweep) static StSfxSlotT gTone; // SID voice 2 static bool gNoiseOn; -static uint16_t gSpeechTicks; + +// Speech playback state. The fill callback walks a queue of utterances so +// a phrase like "HT" ("Hey" + "Taxi") runs as one continuous stream +// instead of each character cutting off the last. +static uint8_t gSpeechQueue[ST_SPEECH_QUEUE]; +static uint8_t gSpeechHead; +static uint8_t gSpeechTail; +static bool gSpeechActive; +static uint8_t gSpeechPitch = ST_SPEECH_PITCH_BASE; +// DATA/speech.bin, whole: header, entry table, sample bytes. The bytes +// are already in the port's raw stream format (jlAudioSfxRawFormat): +// signed +/-100, or 0x80-biased on the IIgs so a chunk needs no +// conversion; speechLoad refuses a file baked for the other one. +static uint8_t *gSpeechBlob; +static uint8_t gSpeechPitchBase; +static uint8_t gSpeechPitchCount; +static uint8_t gSpeechCount; // utterances per pitch +static const uint8_t *gSpeechPcm; // current utterance's samples +static uint16_t gSpeechSample; // next sample index in it +static uint16_t gSpeechSamples; // its length +static uint8_t gSpeechParity; // it ends on the opposite level +static uint8_t gSpeechPol; // 0 / 1: which baked polarity the next utterance plays +// Copied-ahead PCM ring for the stream (see speechFill). Allocated by +// stAudioInit rather than static: 8 KB of BSS is a fifth of the IIgs +// entry bank, which text, rodata, BSS and the C heap all share. +static uint8_t *gSpeechStage; +static uint16_t gSpeechStageRead; // byte index of the next unread sample +static uint16_t gSpeechStageWrite; // byte index of the next free byte +static uint16_t gSpeechStageCount; // bytes staged and unread +static bool gSpeechDecoding; // the decoder still has stream left static uint16_t sidHz(uint16_t word); +static void speechLoad(void); +static bool speechNext(void); +static void speechArm(void); +static uint16_t speechDecode(uint8_t *dst, uint16_t count); +static uint32_t speechFill(void *ctx, int8_t *dst, uint32_t count); +static void speechPumpSlice(uint16_t samples); +static bool speechQueue(uint8_t ch); static void voiceOff(StSfxSlotT *slot); @@ -61,6 +141,208 @@ static uint16_t sidHz(uint16_t word) { } +// Point the decoder at the next queued utterance. False when the queue +// is empty, which ends the stream. +// DATA/speech.bin into one jlAlloc block; without it the game is mute +// but otherwise fine. +static void speechLoad(void) { + FILE *fp = jlDataOpen(ST_SPEECH_FILE, "rb"); + uint8_t head[ST_SPEECH_HEADER_BYTES]; + uint8_t format; + uint32_t dataBytes; + uint32_t total; + uint32_t done; + + gSpeechBlob = 0; + if (fp == 0) { + return; + } + format = (jlAudioSfxRawFormat() == JL_SFX_RAW_UNSIGNED8_NOZERO) ? ST_SPEECH_FORMAT_BIASED : ST_SPEECH_FORMAT_SIGNED; + if (fread(head, 1u, sizeof(head), fp) != sizeof(head) || head[0] != 'S' || head[1] != 'T' || head[2] != 'S' || head[3] != 'P' || head[4] != ST_SPEECH_VERSION || head[8] != format) { + fclose(fp); + return; + } + gSpeechPitchBase = head[5]; + gSpeechPitchCount = head[6]; + gSpeechCount = head[7]; + dataBytes = (uint32_t)head[12] | ((uint32_t)head[13] << 8) | ((uint32_t)head[14] << 16) | ((uint32_t)head[15] << 24); + total = ST_SPEECH_HEADER_BYTES + (uint32_t)gSpeechPitchCount * gSpeechCount * ST_SPEECH_POLARITIES * ST_SPEECH_ENTRY_BYTES + dataBytes; + gSpeechBlob = (uint8_t *)jlAlloc(total); + if (gSpeechBlob == 0) { + fclose(fp); + return; + } + memcpy(gSpeechBlob, head, sizeof(head)); + // The block spans banks on the IIgs; read it in bank-safe pieces. + done = ST_SPEECH_HEADER_BYTES; + while (done < total) { + uint32_t want = total - done; + + if (want > ST_SPEECH_LOAD_CHUNK) { + want = ST_SPEECH_LOAD_CHUNK; + } + if (fread(gSpeechBlob + done, 1u, (size_t)want, fp) != (size_t)want) { + jlFree(gSpeechBlob); + gSpeechBlob = 0; + break; + } + done += want; + } + fclose(fp); +} + + +static bool speechNext(void) { + uint8_t row = 0u; + + if (gSpeechBlob == 0) { + return false; + } + if (gSpeechPitch > gSpeechPitchBase) { + row = (uint8_t)(gSpeechPitch - gSpeechPitchBase); + if (row >= gSpeechPitchCount) { + row = (uint8_t)(gSpeechPitchCount - 1u); + } + } + while (gSpeechHead != gSpeechTail) { + uint8_t code = gSpeechQueue[gSpeechHead]; + const uint8_t *e = gSpeechBlob + ST_SPEECH_HEADER_BYTES + (uint16_t)row * gSpeechCount * ST_SPEECH_POLARITIES * ST_SPEECH_ENTRY_BYTES; + uint8_t k; + + gSpeechHead = (uint8_t)((gSpeechHead + 1u) % ST_SPEECH_QUEUE); + for (k = 0u; k < gSpeechCount; k++, e += ST_SPEECH_POLARITIES * ST_SPEECH_ENTRY_BYTES) { + if (e[0] == code) { + const uint8_t *v = e + (uint16_t)gSpeechPol * ST_SPEECH_ENTRY_BYTES; + uint32_t offset = (uint32_t)v[4] | ((uint32_t)v[5] << 8) | ((uint32_t)v[6] << 16) | ((uint32_t)v[7] << 24); + + gSpeechParity = v[1]; + gSpeechSamples = (uint16_t)(v[2] | ((uint16_t)v[3] << 8)); + gSpeechSample = 0u; + gSpeechPcm = gSpeechBlob + ST_SPEECH_HEADER_BYTES + (uint32_t)gSpeechPitchCount * gSpeechCount * ST_SPEECH_POLARITIES * ST_SPEECH_ENTRY_BYTES + offset; + return true; + } + } + // Unknown code: skip it rather than dropping the whole phrase. + } + return false; +} + + +// Copy up to `count` samples of the queued utterances into `dst`, fewer +// when they run out (0 = none left). jlMemCopy is an MVN on the IIgs; +// the utterance bytes are already in the stream's format, so this is +// the whole of the play-time speech work. +static uint16_t speechDecode(uint8_t *dst, uint16_t count) { + uint16_t n = 0u; + + while (n < count) { + uint16_t left = (uint16_t)(gSpeechSamples - gSpeechSample); + uint16_t take = (uint16_t)(count - n); + + if (left == 0u) { + // The next utterance carries on from the level this one left. + if (gSpeechParity != 0u) { + gSpeechPol ^= 1u; + } + if (!speechNext()) { + return n; + } + continue; + } + if (take > left) { + take = left; + } + jlMemCopy(dst + n, gSpeechPcm + gSpeechSample, take); + gSpeechSample = (uint16_t)(gSpeechSample + take); + n = (uint16_t)(n + take); + } + return n; +} + + +// The audio engine's chunk pull. It used to decode the whole chunk right +// here -- 7168 samples, ~1.5 s of speech, in ONE frame: every "HEY +// TAXI" froze the ride for over a second on the IIgs. Now the chunk is +// copied out of the staging buffer that speechPumpSlice fills a slice at +// a time across the ticks before it is due, and only a stream that has +// outrun the pump decodes here, a bounded slice, so it never starves. +static uint32_t speechFill(void *ctx, int8_t *dst, uint32_t count) { + uint16_t want = (count > ST_SPEECH_STAGE_BYTES) ? (uint16_t)ST_SPEECH_STAGE_BYTES : (uint16_t)count; + uint16_t n; + + (void)ctx; + if (gSpeechStageCount == 0u && gSpeechDecoding) { + speechPumpSlice(ST_SPEECH_STARVED_SLICE); + } + n = (gSpeechStageCount < want) ? gSpeechStageCount : want; + if (n != 0u) { + // jlMemCopy (MVN on the IIgs; libc memcpy is a byte loop there), + // in two runs when the unread bytes wrap the ring's end. + uint8_t *to = (uint8_t *)dst; + uint16_t left = n; + + while (left != 0u) { + uint16_t run = (uint16_t)(ST_SPEECH_STAGE_BYTES - gSpeechStageRead); + + if (run > left) { + run = left; + } + jlMemCopy(to, gSpeechStage + gSpeechStageRead, run); + to += run; + gSpeechStageRead = (uint16_t)((gSpeechStageRead + run) & ST_SPEECH_STAGE_MASK); + left = (uint16_t)(left - run); + } + gSpeechStageCount = (uint16_t)(gSpeechStageCount - n); + } + if (n == 0u) { + // Out of queued utterances. Only END the stream if this chunk + // produced nothing at all -- a phrase is fed one character at a + // time ("HT" is "Hey" then "Taxi"), and the first arm decodes + // all of 'H' before 'T' has even been queued. Ending here would + // let the 'T' arm a NEW stream that cut 'H' off, so the player + // only ever heard the last character. Returning a short chunk + // keeps the slot armed and the next pull picks up whatever + // arrived since. + gSpeechActive = false; + } + return n; +} + + +// Decode up to `samples` more into the staging buffer. Once the decoder +// reports nothing left it stays quiet until the next speechArm. Reads +// and writes stay even so the chunk copy can move words. +static void speechPumpSlice(uint16_t samples) { + uint16_t room = (uint16_t)(ST_SPEECH_STAGE_BYTES - gSpeechStageCount); + + if (!gSpeechDecoding || room == 0u) { + return; + } + if (samples > room) { + samples = room; + } + // Fill towards the ring's end, then from its start. + while (samples != 0u) { + uint16_t run = (uint16_t)(ST_SPEECH_STAGE_BYTES - gSpeechStageWrite); + uint16_t got; + + if (run > samples) { + run = samples; + } + got = speechDecode(gSpeechStage + gSpeechStageWrite, run); + if (got < run) { + gSpeechDecoding = false; + } + gSpeechStageWrite = (uint16_t)((gSpeechStageWrite + got) & ST_SPEECH_STAGE_MASK); + gSpeechStageCount = (uint16_t)(gSpeechStageCount + got); + samples = (uint16_t)(samples - got); + if (!gSpeechDecoding) { + return; + } + } +} + + static void voiceOff(StSfxSlotT *slot) { if (slot->ticksLeft != 0u) { jlAudioVoice(slot->voice, 0u, ST_ATTEN_OFF); @@ -71,6 +353,9 @@ static void voiceOff(StSfxSlotT *slot) { // Once per game tick: run the release timers ($4320 sfxEnvelopeTick). void stAudioTick(void) { + if (gSpeechActive) { + speechPumpSlice(ST_SPEECH_TICK_SLICE); + } if (gSfx.ticksLeft != 0u) { gSfx.ticksLeft--; if (gSfx.ticksLeft == 0u) { @@ -83,23 +368,23 @@ void stAudioTick(void) { 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(); + gSpeechStage = (uint8_t *)jlAlloc(ST_SPEECH_STAGE_BYTES); + speechLoad(); gSfx.voice = ST_VOICE_SFX; gSfx.ticksLeft = 0u; gTone.voice = ST_VOICE_TONE; gTone.ticksLeft = 0u; gNoiseOn = false; - gSpeechTicks = 0u; + gSpeechHead = 0u; + gSpeechTail = 0u; + gSpeechActive = false; + gSpeechPitch = ST_SPEECH_PITCH_BASE; + gSpeechPol = 0u; } @@ -108,6 +393,10 @@ void stAudioShutdown(void) { voiceOff(&gTone); jlAudioNoise(0u, ST_ATTEN_OFF); jlAudioShutdown(); + jlFree(gSpeechStage); + gSpeechStage = 0; + jlFree(gSpeechBlob); + gSpeechBlob = 0; } @@ -151,10 +440,112 @@ void stAudioSilence(void) { } -// $9802 -- one spoken character (a chirp here). +// $44CF -- play one of the game's tunes and block. The C64 silences the +// SID first ($5C1A / $6906) and then spins until the song ends; the host +// does the spinning (ST_STATE_JINGLE), so here it is just the start. +void stAudioJingle(uint8_t song) { + stAudioMusic(song, false); +} + + +// $CB02 -- start a tune and leave it to the player: the level intro's +// song and the high-score table's. Speech is cancelled too: it streams +// on the slot the tracker's voice 0 rides. +void stAudioMusic(uint8_t song, bool loop) { + if (song >= ST_SONG_COUNT) { + return; + } + stAudioSilence(); + stAudioNoise(false); + gSpeechActive = false; + gSpeechDecoding = false; + gSpeechStageRead = 0u; + gSpeechStageWrite = 0u; + gSpeechStageCount = 0u; + gSpeechHead = gSpeechTail; + jlAudioStopSfx(ST_SPEECH_SLOT); + jlMusicPlay(kStSongs[song].data, kStSongs[song].length, loop); +} + + +bool stAudioJingleActive(void) { + return jlMusicIsPlaying(); +} + + +// Queue one utterance code. Returns false when the code is not speech (a +// pitch change) or the queue is full. +static bool speechQueue(uint8_t ch) { + uint8_t next; + + if (ch < 9u) { + // Below $09 the original does not speak: it rewrites the delay + // loop's inner count, re-pitching every later utterance. That is + // where the passenger's voice variation comes from, so keep it. + gSpeechPitch = (uint8_t)(ch & ST_SPEECH_PITCH_MAX); + if (gSpeechPitch == 0u) { + gSpeechPitch = ST_SPEECH_PITCH_BASE; + } + return false; + } + next = (uint8_t)((gSpeechTail + 1u) % ST_SPEECH_QUEUE); + if (next == gSpeechHead) { + return false; // queue full; drop rather than stall + } + gSpeechQueue[gSpeechTail] = ch; + gSpeechTail = next; + return true; +} + + +// Start the stream if it is idle. Call this ONCE, after the whole phrase is +// queued: arming decodes everything queued so far into a single chunk, and +// each extra chunk costs a refill deadline of dead air, because the refill +// only happens when the game next calls jlAudioFrameTick -- once per rendered +// frame. Arming per character is what broke phrases into words with silence +// between them. +static void speechArm(void) { + if (gSpeechActive || gSpeechStage == 0 || gSpeechBlob == 0) { + return; + } + if (!speechNext()) { + return; + } + gSpeechPol = 0u; // a phrase starts on +100 + gSpeechActive = true; + gSpeechDecoding = true; + gSpeechStageRead = 0u; + gSpeechStageWrite = 0u; + gSpeechStageCount = 0u; + // A first slice now, so the opening chunk sounds this tick. + speechPumpSlice(ST_SPEECH_ARM_SLICE); + jlAudioPlaySfxStreamRaw(ST_SPEECH_SLOT, speechFill, (void *)0, ST_SPEECH_RATE); +} + + +// $9802 -- one spoken character. Speaks immediately; prefer +// stAudioSpeechPhrase for anything longer than a single code. void stAudioSpeech(uint8_t ch) { - 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; + if (speechQueue(ch)) { + speechArm(); + } +} + + +// A whole phrase: queue every code FIRST, then arm once, so the phrase +// decodes into as few chunks as it fits into. +void stAudioSpeechPhrase(const uint8_t *speech) { + bool any = false; + + while (*speech != 0u) { + if (speechQueue(*speech)) { + any = true; + } + speech++; + } + if (any) { + speechArm(); + } } diff --git a/examples/spacetaxi/stC64Data.c b/examples/spacetaxi/stC64Data.c index 53b7f05..b8fa792 100644 --- a/examples/spacetaxi/stC64Data.c +++ b/examples/spacetaxi/stC64Data.c @@ -1322,10 +1322,6 @@ const uint8_t kStFlameCelByDirMask[16] = { 0x00, 0xD8, 0xD4, 0x00, 0xD5, 0xD2, 0xD1, 0x00, 0xD7, 0xD3, 0xD6, 0x00, 0x00, 0x00, 0x00, 0x00, }; -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 @@ -1358,13 +1354,3 @@ const uint8_t *stC64SfxProgram(uint8_t idx) { uint8_t stC64FlameCel(uint8_t dirMask) { return kStFlameCelByDirMask[dirMask]; } - - -int8_t stC64IntroStarDx(uint8_t idx) { - return kStIntroStarDx[idx]; -} - - -int8_t stC64IntroStarDy(uint8_t idx) { - return kStIntroStarDy[idx]; -} diff --git a/examples/spacetaxi/stC64Data.h b/examples/spacetaxi/stC64Data.h index 69751ab..c30c083 100644 --- a/examples/spacetaxi/stC64Data.h +++ b/examples/spacetaxi/stC64Data.h @@ -44,10 +44,6 @@ extern const uint8_t kStSfxPrograms[ST_SFX_COUNT][ST_SFX_PROGRAM_BYTES]; // 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. @@ -56,7 +52,5 @@ const uint8_t *stC64SpriteBitmap(uint8_t idx); uint8_t stC64RngByte(uint8_t idx); const uint8_t *stC64SfxProgram(uint8_t idx); uint8_t stC64FlameCel(uint8_t dirMask); -int8_t stC64IntroStarDx(uint8_t idx); -int8_t stC64IntroStarDy(uint8_t idx); #endif diff --git a/examples/spacetaxi/stCels.h b/examples/spacetaxi/stCels.h index e78e1e6..2ab7dc8 100644 --- a/examples/spacetaxi/stCels.h +++ b/examples/spacetaxi/stCels.h @@ -12,6 +12,8 @@ #include +#include "stSim.h" + // 3x3 tiles = 24x24 px; 32 bytes per 8x8 tile, tile-major (matches // jlSpriteCreateFromSurface / the .spr cel layout). #define ST_CEL_TILES 3u @@ -29,6 +31,11 @@ typedef struct { extern const StCelDefT kStCels[]; extern const uint16_t kStCelCount; +// The cels a level's hook program can show (stLevelCels.c): at most `max` +// into `out`, no duplicates. 0 for a level without hook sprites. +#define ST_LEVEL_CELS_MAX 40u +uint8_t stLevelCelList(const StLevelT *level, StCelDefT *out, uint8_t max); + // Expand a VIC bitmap (63 bytes, 21 rows x 3, or NULL) into the 288-byte // tile-major 4bpp-packed cel blob. Colours as in the C64 multicolour // rules; hires uses `color` for a set bit. diff --git a/examples/spacetaxi/stFare.c b/examples/spacetaxi/stFare.c index 99e1d76..508f2c1 100644 --- a/examples/spacetaxi/stFare.c +++ b/examples/spacetaxi/stFare.c @@ -11,17 +11,16 @@ #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 " +static const uint8_t kTextPadPlease[] = "PAD PLEASE"; // $6C1E +static const uint8_t kTextUpPlease[] = " UP PLEASE! "; // $6C2B +static const uint8_t kTextHeyTaxi[] = " HEY, TAXI! "; // $6C45 +static const uint8_t kTextThanks[] = " THANKS "; // $6C5F // Speech cue strings fed to the sample player one character at a time. -static const uint8_t kSpeechHeyTaxi[] = { 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" +static const uint8_t kSpeechHeyTaxi[] = "HT"; // $6C6C +static const uint8_t kSpeechUp[] = "U?"; // $6C6F +static const uint8_t kSpeechPad[] = "PX?"; // $6C72 (X = digit) +static const uint8_t kSpeechThanks[] = "!"; // $6C76 +static const uint8_t kSpeechHey[] = "H"; // $6C78 // Waiting wave cycle ($66D6) and the base-fare blob ($43B9 = +5.00). static const uint8_t kWaveCels[4] = { 0xC6, 0xC7, 0xD9, 0xC7 }; static const uint8_t kFareBlob[7] = { 0x66, 0x66, 0x66, 0x6F, 0x77, 0x74, 0x74 }; @@ -45,12 +44,12 @@ static void beamIn(StSimT *sim) { return; } sim->spr[1].ptr--; - if (sim->spr[1].ptr != 0xC7u) { + if (sim->spr[1].ptr != ST_SPRITE_PASS_STAND) { return; } if (sim->collisionPhase != 0u) { sim->stage = ST_STAGE_LEAVE; - sim->spr[1].ptr = 0xC7u; + sim->spr[1].ptr = ST_SPRITE_PASS_STAND; sim->spriteSlots[sim->activeSpriteIdx] = 0u; sim->activeSpriteIdx = 0u; sim->fareSlotCount--; @@ -60,15 +59,9 @@ static void beamIn(StSimT *sim) { 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)); - } + stSimHudInit(sim); + stSimPutChar(sim, 35u, 24u, (uint8_t)(ST_CHAR_DIGIT_BASE + stSimRng(sim, 10u))); + stSimPutChar(sim, 34u, 24u, (uint8_t)(ST_CHAR_DIGIT_BASE + 1u)); } @@ -125,7 +118,7 @@ static void newFare(StSimT *sim) { sim->spr[1].enable = 1u; sim->stage = ST_STAGE_BEAM_IN; sim->decayTimer = sim->decayReload; - sim->spr[1].ptr = 0xCBu; + sim->spr[1].ptr = ST_SPRITE_PASS_BEAM; stAudioSpeech((uint8_t)(stSimRng(sim, 3u) + 2u)); } @@ -136,14 +129,14 @@ static void stageBoard(StSimT *sim) { return; } sim->spr[1].ptr++; - if (sim->spr[1].ptr != 0xCCu) { + if (sim->spr[1].ptr != ST_SPRITE_PASS_GONE) { return; } sim->spr[1].enable = 0u; sim->stage++; sim->activeDyingSlot = sim->activePad; (void)stFareChooseDestination(sim); - if (sim->activeSpriteIdx == 0x0Bu) { + if (sim->activeSpriteIdx == ST_FARE_DEST_UP) { return; } stSimDrawText(sim, 14u, 24u, kTextPadPlease, 1u); @@ -164,7 +157,7 @@ static void stageCleanup(StSimT *sim) { sim->deathInProgress = 0u; sim->stage = ST_STAGE_IDLE; stSimPadLight(sim, false); - stSimDrawText(sim, 14u, 24u, kTextBlank, 1u); + stSimClearMessage(sim); } @@ -176,7 +169,7 @@ static void stageDropIn(StSimT *sim) { return; } sim->spr[1].ptr--; - if (sim->spr[1].ptr != 0xC7u) { + if (sim->spr[1].ptr != ST_SPRITE_PASS_STAND) { return; } sim->stage++; @@ -184,13 +177,7 @@ static void stageDropIn(StSimT *sim) { 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]); - } - } + stSimHudInit(sim); if (sim->fareSlotCount != 0u) { return; } @@ -204,7 +191,7 @@ void stFareLeaveStep(StSimT *sim) { return; } sim->spr[1].ptr++; - if (sim->spr[1].ptr != 0xCCu) { + if (sim->spr[1].ptr != ST_SPRITE_PASS_GONE) { return; } sim->spr[1].enable = 0u; @@ -227,24 +214,19 @@ void stFareWaveStep(StSimT *sim) { 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); + passX = (int16_t)stSimSpriteX(sim, 1u); targetX = (int16_t)(((int16_t)sim->hoverXFrac << 8) | sim->hoverXCol); if ((int16_t)(passX - targetX) < 0) { // $6D30: walk right, cels $C2/$C3. - sim->spr[1].ptr = (uint8_t)(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)); + sim->spr[1].ptr = (uint8_t)(ST_SPRITE_PASS_WALK_R + sim->walkParity); + stSimSpriteAddX(sim, 1u, 2u); return; } // $6D4B: walk left, cels $C4/$C5. - sim->spr[1].ptr = (uint8_t)(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)); + sim->spr[1].ptr = (uint8_t)(ST_SPRITE_PASS_WALK_L + sim->walkParity); + stSimSpriteAddX(sim, 1u, 0xFEu); } @@ -255,7 +237,7 @@ static void walkToTarget(StSimT *sim) { return; } if ((sim->spr[1].col & 0xFEu) == (sim->hoverXCol & 0xFEu)) { - sim->spr[1].ptr = 0xC7u; + sim->spr[1].ptr = ST_SPRITE_PASS_STAND; sim->stage++; return; } @@ -272,17 +254,14 @@ void stFareBoardingSound(StSimT *sim, const uint8_t *speech) { (void)sim; stAudioSilence(); stAudioNoise(false); - while (*speech != 0u) { - stAudioSpeech(*speech); - speech++; - } + stAudioSpeechPhrase(speech); } // $6537 -- reserve a free fare pad by RNG (linear probe on collision), // or "UP PLEASE" once every fare pad is taken. Returns the pad number. uint8_t stFareChooseDestination(StSimT *sim) { - uint8_t special = sim->level->specialPad; + uint8_t special = sim->specialPad; uint8_t x; if (sim->fareSlotCount == special) { @@ -349,7 +328,7 @@ void stFarePostTickGate(StSimT *sim) { 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->spr[1].ptr = ST_SPRITE_PASS_BEAM; sim->decayTimer = sim->decayReload; pad = &sim->pads[sim->activeSpriteIdx - 1u]; sim->hoverXFrac = pad->passMsb; @@ -374,7 +353,7 @@ void stFareSquashed(StSimT *sim) { sim->spriteSlots[sim->activeSpriteIdx] = 0u; sim->activeSpriteIdx = 0u; sim->fareSlotCount--; - stSimDrawText(sim, 14u, 24u, kTextBlank, 1u); + stSimClearMessage(sim); } sim->stage = ST_STAGE_LEAVE; stFareBoardingSound(sim, kSpeechHey); @@ -387,14 +366,8 @@ void stFareSquashed(StSimT *sim) { // $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]); - } - } + sim->spr[1].ptr = ST_SPRITE_PASS_STAND; + stSimHudInit(sim); } @@ -442,7 +415,7 @@ void stFareStageDispatch(StSimT *sim) { void stFareUpPlease(StSimT *sim) { uint8_t k; - sim->activeSpriteIdx = 0x0Bu; + sim->activeSpriteIdx = ST_FARE_DEST_UP; stSimDrawText(sim, 14u, 24u, kTextUpPlease, 1u); for (k = 0u; k < 4u; k++) { stSimPutChar(sim, (uint8_t)(18u + k), 0u, ST_CHAR_SPACE); diff --git a/examples/spacetaxi/stHookTables.h b/examples/spacetaxi/stHookTables.h new file mode 100644 index 0000000..3b203cd --- /dev/null +++ b/examples/spacetaxi/stHookTables.h @@ -0,0 +1,85 @@ +// Space Taxi -- where each level's hook program keeps its tables. +// +// Every address is a byte in the level's shipped hook blob ($7D98..$7FFF, +// StLevelT.hookData), so the same numbers serve the hook programs +// (stHooks.c, reading through the live simulation) and the cel manifest +// (stLevelCels.c, reading the level file alone -- the offline cel baker +// has no simulation). One list, two readers. +#ifndef ST_HOOK_TABLES_H +#define ST_HOOK_TABLES_H + +// Level indices (0 = A). +#define ST_LEVEL_E 4u +#define ST_LEVEL_G 6u +#define ST_LEVEL_H 7u +#define ST_LEVEL_I 8u +#define ST_LEVEL_J 9u +#define ST_LEVEL_K 10u +#define ST_LEVEL_L 11u +#define ST_LEVEL_O 14u +#define ST_LEVEL_P 15u +#define ST_LEVEL_Q 16u +#define ST_LEVEL_R 17u +#define ST_LEVEL_S 18u +#define ST_LEVEL_T 19u +#define ST_LEVEL_U 20u +#define ST_LEVEL_V 21u +#define ST_LEVEL_W 22u +#define ST_LEVEL_X 23u + +// Hardware sprites 3..7 are the hook sprites on every level that has any. +#define ST_HOOK_FIRST_SPRITE 3u + +// ---- level G "TELEPORTS" ---- +#define ST_HK_G_CEL 0x7DD4u // 6 cels the orbs cycle through +#define ST_HK_G_CEL_PHASES 6u +#define ST_HK_G_ORB_COL 0x7DDAu // per sprite: start column, msb, row +#define ST_HK_G_ORB_MSB 0x7DE2u +#define ST_HK_G_ORB_ROW 0x7DEAu +#define ST_HK_G_SPOT_COL 0x7EF3u // 5 landing spots: column, msb, row +#define ST_HK_G_SPOT_MSB 0x7EF8u +#define ST_HK_G_SPOT_ROW 0x7EFDu +#define ST_HK_G_SFX_HOP 0x7F03u +#define ST_HK_G_ORB_COLOR 4u + +// ---- level H "PUZZLER" ---- +#define ST_HK_H_CEL 0x80u // the one sprite cel +#define ST_HK_H_COLOR_IDLE 0x01u +#define ST_HK_H_COLOR_LIT 0x0Au +#define ST_HK_H_COLOR_ACTIVE 0x05u + +// ---- level I "CROSSFIRE" ---- +#define ST_HK_I_CEL_RISE 0x7DD8u // 5 cels +#define ST_HK_I_RISE_PHASES 5u +#define ST_HK_I_CEL_BURST 0x7DE5u // 8 cels +#define ST_HK_I_BURST_PHASES 8u +#define ST_HK_I_COLOR 0x7E7Fu // 3 colours +#define ST_HK_I_COLORS 3u +#define ST_HK_I_COLOR_IDLE 0x02u + +// ---- levels J "SHOOTING STARS" and P "BLIZZARD" (the falling hazards) ---- +#define ST_HK_FALL_CEL 0x80u // falling: $80/$81 +#define ST_HK_FALL_CEL_BURST 0x82u // then the burst run from the table +#define ST_HK_J_CEL_BURST 0x7EC9u // phases 1..9 +#define ST_HK_J_BURST_PHASES 10u +#define ST_HK_J_COLOR 7u +#define ST_HK_P_CEL_BURST 0x7F09u // phases 1..4 +#define ST_HK_P_BURST_PHASES 5u +#define ST_HK_P_COLOR 3u + +// ---- level L "BLACK HOLE" ---- +#define ST_HK_L_CEL_FIRST 0x80u // $80..$82 spin +#define ST_HK_L_CEL_END 0x83u +#define ST_HK_L_COLOR 0u + +// ---- level Q "INTERFERENCE" ---- +#define ST_HK_Q_CEL 0x7DF2u // 12 cels, multicolour +#define ST_HK_Q_CEL_PHASES 12u +#define ST_HK_Q_COLOR 6u + +// ---- level U "REBOUND" ---- +#define ST_HK_U_CEL 0x7F4Bu // 4-entry walk over $80..$82 +#define ST_HK_U_CEL_PHASES 4u +#define ST_HK_U_COLOR 7u + +#endif // ST_HOOK_TABLES_H diff --git a/examples/spacetaxi/stHooks.c b/examples/spacetaxi/stHooks.c index a6bb4f8..ce3768d 100644 --- a/examples/spacetaxi/stHooks.c +++ b/examples/spacetaxi/stHooks.c @@ -6,38 +6,212 @@ // 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. +// original bytes in the level blob and re-expresses each program here, +// naming the tables it reads at the addresses the original used, so +// nothing has to be transcribed by hand. Only the bytes a hook WRITES +// are held in the port's own typed scratch (StHookScratchT). + +#include #include "stSim.h" +#include "stHookTables.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]) +// Keeping the two per-tick dispatch halves out of line is what lets the +// 65816 build use the default register allocator. Inlined back together +// they are one twelve-way switch whose live values exceed what greedy +// allocation can place on a single-accumulator machine, and the build +// then falls back to the basic allocator for this entire file. The call +// costs one JSL per game tick. +#if defined(__GNUC__) || defined(__clang__) + #define ST_NOINLINE __attribute__((noinline)) +#else + #define ST_NOINLINE +#endif -// 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 +// A level's read-only tables, straight out of the shipped level blob. +// Nothing writes these, so there is no working copy and no second source +// of truth: the .dat is the only place the values live. Every byte a +// hook WRITES lives in the typed per-level scratch (StHookScratchT), so +// the 616-byte C64-addressed working copy is gone. +#define HKT(sim, addr) ((sim)->level->hookData[(addr) - ST_HOOK_BASE]) +#define HKTP(sim, addr) (&(sim)->level->hookData[(addr) - ST_HOOK_BASE]) + +// ---- level H "PUZZLER" ($7E15/$7E4C/$7E53..$7E56 scratch) ---- +// Five parallel 11-entry segment tables sit back to back in the blob, +// then the pad and switch segment lists (four ids each, $80 = end). +#define H_SPR_COL(sim, k) HKT(sim, 0x7DCF + (k)) +#define H_SPR_MSB(sim, k) HKT(sim, 0x7DD7 + (k)) +#define H_SPR_ROW(sim, k) HKT(sim, 0x7DDF + (k)) +#define H_SEG_COL(sim, id) HKT(sim, 0x7DE7 + (id)) +#define H_SEG_ROW(sim, id) HKT(sim, 0x7DF2 + (id)) +#define H_SEG_SENSE(sim, id) HKT(sim, 0x7DFD + (id)) +#define H_SEG_GLYPH(sim, id) HKT(sim, 0x7E08 + (id)) +#define H_PAD_SEGS(sim, i) HKT(sim, 0x7E24 + (i)) +#define H_SFX_CHIME(sim) HKTP(sim, 0x7ED0) +// These two index PAST the table they name and must see live scratch, so +// they go through the byte-addressed view below -- see hookHByte. +#define H_SEG_FLAG(sim, id) hookHByte(sim, (uint16_t)(0x7E15u + (id))) +#define H_SWITCH_SEGS(sim, i) hookHByte(sim, (uint16_t)(0x7E38u + (i))) + +// ---- level G "TELEPORTS" ($7DD1..$7DD3/$7E00/$7F02 scratch) ---- +#define G_CEL(sim, ph) HKT(sim, ST_HK_G_CEL + (ph)) +#define G_ORB_COL(sim, x) HKT(sim, ST_HK_G_ORB_COL + (x)) +#define G_ORB_MSB(sim, x) HKT(sim, ST_HK_G_ORB_MSB + (x)) +#define G_ORB_ROW(sim, x) HKT(sim, ST_HK_G_ORB_ROW + (x)) +#define G_SPOT_COL(sim, i) HKT(sim, ST_HK_G_SPOT_COL + (i)) +#define G_SPOT_MSB(sim, i) HKT(sim, ST_HK_G_SPOT_MSB + (i)) +#define G_SPOT_ROW(sim, i) HKT(sim, ST_HK_G_SPOT_ROW + (i)) +#define G_SFX_HOP(sim) HKTP(sim, ST_HK_G_SFX_HOP) + +// ---- level I "CROSSFIRE" ($7DB8/$7DC0/$7DC8/$7DD0 scratch) ---- +#define I_CEL_RISE(sim, ph) HKT(sim, ST_HK_I_CEL_RISE + (ph)) +#define I_START_COL(sim, d) HKT(sim, 0x7DDD + (d)) +#define I_DX(sim, d) HKT(sim, 0x7DE1 + (d)) +#define I_CEL_BURST(sim, ph) HKT(sim, ST_HK_I_CEL_BURST + (ph)) +#define I_COLOR(sim, i) HKT(sim, ST_HK_I_COLOR + (i)) +#define I_SFX_FIRE(sim) HKTP(sim, 0x7E82) + +// ---- level Q "INTERFERENCE" ($7E83 scratch) ---- +#define Q_SPR_COL(sim, x) HKT(sim, 0x7DD2 + (x)) +#define Q_SPR_MSB(sim, x) HKT(sim, 0x7DDA + (x)) +#define Q_SPR_ROW(sim, x) HKT(sim, 0x7DE2 + (x)) +#define Q_INIT_PHASE(sim, x) HKT(sim, 0x7DEA + (x)) +#define Q_CEL(sim, ph) HKT(sim, ST_HK_Q_CEL + (ph)) +#define Q_SCRAMBLE(sim, i) HKT(sim, 0x7E7F + (i)) + +// ---- level T "FAST BREAK" ($7D9C scratch) ---- +#define T_SFX_BOUNCE(sim) HKTP(sim, 0x7E56) +#define T_SFX_OPEN(sim) HKTP(sim, 0x7E5F) +#define T_SFX_SHUT(sim) HKTP(sim, 0x7E68) + +// ---- level U "REBOUND" ($7F4F/$7F57/$7F5F/$7F6D/$7F6E scratch) ---- +#define U_ENTRY(sim, d) HKT(sim, 0x7F3B + (d)) +#define U_ENTRY_MSB(sim, d) HKT(sim, 0x7F3F + (d)) +#define U_DX(sim, d) HKT(sim, 0x7F43 + (d)) +#define U_DY(sim, d) HKT(sim, 0x7F47 + (d)) +#define U_CEL(sim, ph) HKT(sim, ST_HK_U_CEL + (ph)) +#define U_LIFE(sim, d) HKT(sim, 0x7F69 + (d)) +#define U_SFX_BOUNCE(sim) HKTP(sim, 0x7F76) + +// ---- level W "LASERS" ($7DAB/$7DAC/$7DB5/$7DE5 scratch) ---- +#define W_BEAM_GLYPH(sim, i) HKT(sim, 0x7DBD + (i)) +#define W_TOP_ROW(sim, i) HKT(sim, 0x7DC5 + (i)) +#define W_COL(sim, i) HKT(sim, 0x7DCD + (i)) +#define W_START_ROW(sim, i) HKT(sim, 0x7DD5 + (i)) +#define W_ROW_STEP(sim, i) HKT(sim, 0x7DDD + (i)) +#define W_FADE_COLOR(sim, ph) HKT(sim, 0x7DE6 + (ph)) +#define W_END_ROW(sim, i) HKT(sim, 0x7DE9 + (i)) +#define W_SFX_HUM(sim) HKTP(sim, 0x7DA2) + +// ---- level X "MOVING PADS" ($7D9C..$7D9E scratch) ---- +#define X_GLYPH(sim, col) HKT(sim, 0x7D9F + (col)) +#define X_PAD_SLOT(sim, k) HKTP(sim, 0x7FB8 + (k) * 8u) + +// ---- level E "BEANSTALK" ($7D98..$7D9B scratch) ---- +#define E_SFX_GROW(sim) HKTP(sim, 0x7EA0) + +// ---- level J "SHOOTING STARS" ($7ED3/$7EDB scratch) ---- +#define J_SFX_BURST(sim) HKTP(sim, 0x7DF8) + +// ---- level P "BLIZZARD" ($7F0E/$7F16/$7F1F scratch) ---- +#define P_SFX_WIND(sim) HKTP(sim, 0x7E3C) + +// ---- level S "THE SWITCH" ---- +#define S_DIR_MAP(sim, d) HKT(sim, 0x7DAC + (d)) + +// Levels J and P run the same falling-hazard program over hardware +// sprites 3..7 with their own tables: the per-sprite X drift, the +// per-sprite spawn column base and the burst (J) or melt (P) cel run. +typedef struct { + uint16_t dxTable; // blob address, indexed by sprite + uint16_t startTable; // blob address, indexed by sprite + uint16_t celTable; // blob address, indexed by phase 1..phaseEnd-1 + uint8_t phaseEnd; // the phase that ends the burst + bool avoidMiddle; // J rerolls a spawn over the middle band +} StHookFallT; + +static const StHookFallT kFallJ = { 0x7EB9u, 0x7EC1u, ST_HK_J_CEL_BURST, ST_HK_J_BURST_PHASES, true }; +static const StHookFallT kFallP = { 0x7EF9u, 0x7F01u, ST_HK_P_CEL_BURST, ST_HK_P_BURST_PHASES, false }; + +// Each level H switch drives four wall segments ($7E24 stride, $7E38 +// list), animated over four steps. +#define ST_HOOK_H_SEGMENTS 4u +#define ST_HOOK_H_STEPS 4u + +// Level U: hardware sprites 3..7 are the drifting orbs, $80 is the first +// of their three cels, and a bounce holds off for twelve ticks ($7E0E). +#define ST_HOOK_U_COOLDOWN 0x0Cu + +// Level E: pad 1 alone at first; while the fare wants UP a leaf cell +// sprouts every $57 ticks (the count starts at $1E, or $46 on the +// second screen), four to a side from columns 20/19, and the fifth +// step hangs the next pair of pads at the leaf ends, opens them and +// moves the growth four rows up, until pad 9 is out. +#define ST_HOOK_E_GROW_TICKS 0x57u +#define ST_HOOK_E_TICK_START 0x1Eu +#define ST_HOOK_E_TICK_SECOND 0x46u +#define ST_HOOK_E_LEAVES 5u +#define ST_HOOK_E_LAST_PAD 9u +#define ST_HOOK_E_FIRST_ROW 0x13u +#define ST_HOOK_E_ROW_STEP 4u +#define ST_HOOK_E_STALK_COL 20u +#define ST_HOOK_E_PAD_COL_RIGHT 25u +#define ST_HOOK_E_PAD_COL_LEFT 15u +#define ST_HOOK_E_CHAR_LEAF 0xD2u +#define ST_HOOK_E_CHAR_TIP_R 0xD7u +#define ST_HOOK_E_CHAR_TIP_L 0xD5u +#define ST_HOOK_E_COLOR 5u + +// Levels J and P: hardware sprites 3..7 are the falling hazards, cels +// $80/$81 falling and $82 the burst. An idle sprite spawns on rng($3C) +// < 3, on row $17; a scenery hit counts from row $38 down and drops +// the sprite four rows onto it. J rerolls a spawn column in $91..$CC. +#define ST_HOOK_FALL_SPRITES 0xF8u // $D01F bits 3..7 +#define ST_HOOK_FALL_SPAWN_ODDS 0x3Cu +#define ST_HOOK_FALL_SPAWN_HIT 3u +#define ST_HOOK_FALL_TOP_ROW 0x17u +#define ST_HOOK_FALL_HIT_ROW 0x38u +#define ST_HOOK_FALL_HIT_DROP 4u +#define ST_HOOK_FALL_BAND_LO 0x91u +#define ST_HOOK_FALL_BAND_HI 0xCDu + +// Level G: the five orbs (sprites 3..7) cycle a cel every fourth tick; +// the cab touching one hides it for $14 ticks under a falling tone that +// starts at $5A and drops 4 a tick, then lands it on one of five spots: +// the fare's destination pad half the time, else a random spot other +// than the last one used. +#define ST_HOOK_G_HOP_TICKS 0x14u +#define ST_HOOK_G_SWEEP_START 0x5Au +#define ST_HOOK_G_SWEEP_STEP 4u +#define ST_HOOK_G_SPOTS 5u +#define ST_HOOK_G_PAD_SPOTS 6u // a destination of 6+ (UP) lands on spot 5 + +// Level L: hardware sprite 3 is the hole at ($AC, $82), cels $80..$82. +// Gravity pulls the cab toward it: $10 (Y) and $0C (X) less an eighth +// of the distance, X measured in halved sprite units so it fits a +// byte. Touching it returns verdict $FF: a kill with no falling wreck. +#define ST_HOOK_L_HOLE 3u +#define ST_HOOK_L_HOLE_COL 0xACu +#define ST_HOOK_L_HOLE_ROW 0x82u +#define ST_HOOK_L_HOLE_X_HALF 0x5Bu +#define ST_HOOK_L_PULL_Y 0x10u +#define ST_HOOK_L_PULL_X 0x0Cu +#define ST_HOOK_L_VERDICT 0xFFu + +// Level P: the wind turns on rng($78) < 3 per tick, flipping the three +// windsock cells (glyph ^ 6) and the sign of the X gravity. +#define ST_HOOK_P_WIND_ODDS 0x78u +#define ST_HOOK_P_WIND_HIT 3u +#define ST_HOOK_P_SOCK_FLIP 0x06u + +// Level S: FIRE passes, the four direction bits go through a swap table. +#define ST_HOOK_S_FIRE 0x10u +#define ST_HOOK_S_DIRS 0x0Fu -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 uint8_t hookHByte(const StSimT *sim, uint16_t addr); static void hookHPerTick(StSimT *sim); static void hookHPrelude0(StSimT *sim); static void hookHSegment(StSimT *sim, uint8_t id); @@ -51,10 +225,10 @@ 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); +ST_NOINLINE static void hookPerTickHigh(StSimT *sim); +ST_NOINLINE static void hookPerTickLow(StSimT *sim); static void hookRMaze(StSimT *sim, uint8_t ch); static void hookRPerTick(StSimT *sim); static void hookVPerTick(StSimT *sim); @@ -65,19 +239,28 @@ 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 hookGHop(StSimT *sim); static void hookGPerTick(StSimT *sim); +static void hookGPrelude0(StSimT *sim); +static void hookUBounce(StSimT *sim); +static void hookUOrbs(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); -} +static void hookUPrelude0(StSimT *sim); +static void hookEPerTick(StSimT *sim); +static void hookEPrelude0(StSimT *sim); +static void hookEPut(StSimT *sim, uint8_t col, uint8_t row, uint8_t ch); +static void hookETips(StSimT *sim); +static void hookFallBgHits(StSimT *sim, uint8_t *state, uint8_t *phase, const uint8_t *sfx); +static void hookFallTick(StSimT *sim, uint8_t *state, uint8_t *phase, const StHookFallT *d, uint8_t *parity); +static void hookJPerTick(StSimT *sim); +static void hookJPrelude0(StSimT *sim); +static void hookLPerTick(StSimT *sim); +static void hookLPrelude0(StSimT *sim); +static uint16_t hookLPull(uint8_t base, uint8_t dist); +static void hookPFlipSock(StSimT *sim, uint16_t cell); +static void hookPPerTick(StSimT *sim); +static void hookPPrelude0(StSimT *sim); +static uint8_t hookSInput(StSimT *sim, uint8_t input); // $41AD after $401B -- the same character into the `rows` cells below. @@ -104,12 +287,54 @@ static void fillDownColor(StSimT *sim, uint8_t col, uint8_t row, uint8_t color, } +// Level H's byte-addressed view of its own data block. +// +// Two of its reads index past the end of the table they name and land on +// bytes the level itself writes, so H cannot simply split into "const +// tables" plus "named scratch" the way the other levels do: +// +// * the pad lists ($7E24) end with $FF, and the landing check indexes +// the segment flags with that terminator unguarded -- $7E15 + $FF is +// $7F14, 380 bytes into the blob, holding $30. Non-zero, so the +// "no segment here" case reports a match, with switch id $FF. +// * `active` is therefore 6 on every landing, so the switch-segment +// read is always $7E38 + 20 = $7E4C -- which is where the id was just +// stored. The level writes a value and reads it back as table data. +// ($7E4D..$7E4F ship as $FF, so the $80 guard skips them and only the +// stored id is ever animated. That also bounds every write to ids +// 0..10, so the flags array is never indexed out of range.) +// +// Scratch where the level has written, the shipped blob everywhere else. +static uint8_t hookHByte(const StSimT *sim, uint16_t addr) { + const StHookHT *h = &sim->hookScratch.h; + + if (addr >= 0x7E15u && addr < 0x7E15u + ST_HOOK_H_SEGS) { + return h->segFlag[addr - 0x7E15u]; + } + switch (addr) { + case 0x7E4Cu: + return h->switchId; + case 0x7E53u: + return h->tick; + case 0x7E54u: + return h->active; + case 0x7E55u: + return h->step; + case 0x7E56u: + return h->tone; + default: + break; + } + return HKT(sim, addr); +} + + // 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->hookScratch.h.active == 0u) { if (sim->collisionPhase != 0u) { return; } @@ -118,10 +343,10 @@ static void hookHPerTick(StSimT *sim) { 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; + uint8_t y = H_PAD_SEGS(sim, x + n); + if (H_SEG_FLAG(sim, y) != 0u) { + sim->hookScratch.h.switchId = y; + sim->hookScratch.h.active = 6u; found = true; break; } @@ -143,47 +368,45 @@ static void hookHPerTick(StSimT *sim) { if (y == 0u) { return; } - HK(sim, 0x7E54) = y; + sim->hookScratch.h.active = 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)); + sim->hookScratch.h.tick = 0u; + sim->hookScratch.h.step = 0xFFu; + sim->hookScratch.h.tone = 0x12u; + stAudioSfx(H_SFX_CHIME(sim)); } // $7ED9 - HK(sim, 0x7E56)++; - stAudioVoice2(HK(sim, 0x7E56), HK(sim, 0x7E56), 0xFFu); - HK(sim, 0x7E53)++; - if ((HK(sim, 0x7E53) & 7u) != 0u) { + sim->hookScratch.h.tone++; + stAudioVoice2(sim->hookScratch.h.tone, sim->hookScratch.h.tone, 0xFFu); + sim->hookScratch.h.tick++; + if ((sim->hookScratch.h.tick & 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; + sim->hookScratch.h.step++; + { + uint8_t idx = (uint8_t)((sim->hookScratch.h.active - 1u) * ST_HOOK_H_SEGMENTS); + bool last = (sim->hookScratch.h.step == ST_HOOK_H_STEPS); + uint8_t n; + + if (last) { + // $7F7C: finished -- flip the segment flags. + sim->hookScratch.h.active = 0u; + setSpriteColors(sim, 0x01u); + } + for (n = 0u; n < ST_HOOK_H_SEGMENTS; n++) { + uint8_t id = H_SWITCH_SEGS(sim, idx + n); + if ((id & 0x80u) != 0u) { + continue; + } + if (last) { + sim->hookScratch.h.segFlag[id] ^= 1u; + } else { + hookHSegment(sim, id); } - 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)--; } } @@ -193,17 +416,17 @@ static void hookHPerTick(StSimT *sim) { static void hookHPrelude0(StSimT *sim) { uint8_t k; - for (k = 0u; k <= 10u; k++) { - HK(sim, 0x7E15 + k) = 0u; + for (k = 0u; k < ST_HOOK_H_SEGS; k++) { + sim->hookScratch.h.segFlag[k] = 0u; } - HK(sim, 0x7E54) = 0u; + sim->hookScratch.h.active = 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); + sim->spr[k].col = H_SPR_COL(sim, k); + sim->spr[k].msb = H_SPR_MSB(sim, k); + sim->spr[k].row = H_SPR_ROW(sim, k); } } @@ -216,22 +439,19 @@ static void hookHSegment(StSimT *sim, uint8_t id) { uint8_t row; uint8_t ch; - HK(sim, 0x7E50) = id; - if ((HK(sim, 0x7E15 + id) ^ HK(sim, 0x7DFD + id)) == 0u) { - step = HK(sim, 0x7E55); + if ((H_SEG_FLAG(sim, id) ^ H_SEG_SENSE(sim, id)) == 0u) { + step = sim->hookScratch.h.step; } else { - step = (uint8_t)(3u - HK(sim, 0x7E55)); + step = (uint8_t)(3u - sim->hookScratch.h.step); } - if (HK(sim, 0x7E08 + id) == 0x9Bu) { - row = (uint8_t)(step + HK(sim, 0x7DF2 + id)); - col = HK(sim, 0x7DE7 + id); + if (H_SEG_GLYPH(sim, id) == 0x9Bu) { + row = (uint8_t)(step + H_SEG_ROW(sim, id)); + col = H_SEG_COL(sim, id); } else { - col = (uint8_t)(step + HK(sim, 0x7DE7 + id)); - row = HK(sim, 0x7DF2 + id); + col = (uint8_t)(step + H_SEG_COL(sim, id)); + row = H_SEG_ROW(sim, id); } - HK(sim, 0x7E13) = col; - HK(sim, 0x7E14) = row; - ch = (HK(sim, 0x7E15 + id) != 0u) ? HK(sim, 0x7E08 + id) : ST_CHAR_SPACE; + ch = (H_SEG_FLAG(sim, id) != 0u) ? H_SEG_GLYPH(sim, id) : ST_CHAR_SPACE; stSimPutChar(sim, col, row, ch); } @@ -257,7 +477,7 @@ static void hookTGate(StSimT *sim, uint8_t ch) { // 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->hookScratch.t.gateShut != 0u) { if (sim->spr[0].row >= 0x5Fu) { hookTPrelude1(sim); } @@ -274,7 +494,7 @@ static void hookTPerTick(StSimT *sim) { return; } sim->velY = (int16_t)(-sim->velY); - stAudioSfx(HKP(sim, 0x7E56)); + stAudioSfx(T_SFX_BOUNCE(sim)); return; } if (sim->spr[0].row >= 0x3Fu) { @@ -283,16 +503,16 @@ static void hookTPerTick(StSimT *sim) { sim->velY = 0; sim->velX = 0; hookTGate(sim, 0x75u); - HK(sim, 0x7D9C)++; - stAudioSfx(HKP(sim, 0x7E68)); + sim->hookScratch.t.gateShut++; + stAudioSfx(T_SFX_SHUT(sim)); } // 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)); + sim->hookScratch.t.gateShut = 0u; + stAudioSfx(T_SFX_OPEN(sim)); } @@ -305,53 +525,52 @@ static void hookWLaserUpdate(StSimT *sim) { uint8_t k; for (k = 0u; k < 16u; k++) { - sim->charset[0x92u + (k >> 3)][k & 7u] = 0u; + stSimGlyphMut(sim, (uint8_t)(0x92u + (k >> 3)))[k & 7u] = 0u; } stSimMarkChar(sim, 0x92u); stSimMarkChar(sim, 0x93u); for (i = 0u; i < 8u; i++) { - uint8_t state = HK(sim, 0x7DAC + i); - uint8_t col = HK(sim, 0x7DCD + i); - HK(sim, 0x7DB4) = i; + uint8_t state = sim->hookScratch.w.state[i]; + uint8_t col = W_COL(sim, 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); + sim->hookScratch.w.state[i] = 1u; + sim->hookScratch.w.row[i] = W_START_ROW(sim, i); + stSimPutChar(sim, col, sim->hookScratch.w.row[i], W_BEAM_GLYPH(sim, i)); + stSimPutColor(sim, col, sim->hookScratch.w.row[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; + stSimPutChar(sim, col, sim->hookScratch.w.row[i], 0x91u); + if (sim->hookScratch.w.row[i] == W_END_ROW(sim, i)) { + sim->hookScratch.w.state[i] = 2u; + sim->hookScratch.w.row[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); + sim->hookScratch.w.row[i] = (uint8_t)(sim->hookScratch.w.row[i] + W_ROW_STEP(sim, i)); + stSimPutChar(sim, col, sim->hookScratch.w.row[i], W_BEAM_GLYPH(sim, i)); + stSimPutColor(sim, col, sim->hookScratch.w.row[i], 2u); continue; } } if (state == 2u) { uint8_t phase; - HK(sim, 0x7DB5 + i)++; - phase = HK(sim, 0x7DB5 + i); + sim->hookScratch.w.row[i]++; + phase = sim->hookScratch.w.row[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); + sim->hookScratch.w.state[i] = 0u; + stSimPutChar(sim, col, W_TOP_ROW(sim, i), ST_CHAR_SPACE); + fillDownChar(sim, col, W_TOP_ROW(sim, 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); + uint8_t color = W_FADE_COLOR(sim, phase); + stSimPutColor(sim, col, W_TOP_ROW(sim, i), color); + fillDownColor(sim, col, W_TOP_ROW(sim, i), color, 7u); } continue; } - HK(sim, 0x7DAC + i) = 0u; + sim->hookScratch.w.state[i] = 0u; } } @@ -361,22 +580,22 @@ static void hookWLaserUpdate(StSimT *sim) { static void hookWPerTick(StSimT *sim) { for (;;) { uint8_t c; - if (HK(sim, 0x7DE5) != 0u) { - stAudioSfx(HKP(sim, 0x7DA2)); - HK(sim, 0x7DE5) = 0u; + if (sim->hookScratch.w.started != 0u) { + stAudioSfx(W_SFX_HUM(sim)); + sim->hookScratch.w.started = 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); + sim->hookScratch.w.colorPhase = (uint8_t)((sim->hookScratch.w.colorPhase + 1u) & 7u); + c = sim->hookScratch.w.colorPhase; 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; + stSimGlyphMut(sim, (uint8_t)(0x91u + ((7u + c) >> 3)))[(7u + c) & 7u] = 0x3Cu; + stSimGlyphMut(sim, 0x93u)[8u - c] = 0x3Cu; stSimMarkChar(sim, 0x92u); stSimMarkChar(sim, 0x93u); if ((c & 1u) == 0u) { @@ -389,11 +608,11 @@ static void hookWPerTick(StSimT *sim) { // 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 dir = sim->hookScratch.x.dir; uint8_t off = 0u; uint8_t phase = 0u; - addToSpriteCol(sim, 1u, dir); + stSimSpriteAddX(sim, 1u, dir); while (off < 0x48u) { uint8_t pad = (uint8_t)(off >> 3); uint8_t field = (uint8_t)(off & 7u); @@ -425,7 +644,7 @@ static void hookXMovePads(StSimT *sim) { if (sim->activePad == 0u) { return; } - addToSpriteCol(sim, 0u, dir); + stSimSpriteAddX(sim, 0u, dir); sim->posXmsb = sim->spr[0].msb; sim->hoverXFrac = sim->spr[0].msb; sim->posXcol = sim->spr[0].col; @@ -438,22 +657,22 @@ static void hookXMovePads(StSimT *sim) { static void hookXPerTick(StSimT *sim) { uint8_t col; - HK(sim, 0x7D9E)++; - if (HK(sim, 0x7D9E) != 0x10u) { + sim->hookScratch.x.tick++; + if (sim->hookScratch.x.tick != 0x10u) { return; } - HK(sim, 0x7D9E) = 0u; - col = HK(sim, 0x7D9C); - if ((HK(sim, 0x7D9D) & 0x80u) == 0u) { + sim->hookScratch.x.tick = 0u; + col = sim->hookScratch.x.col; + if ((sim->hookScratch.x.dir & 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)); + stSimPutChar(sim, col, 5u, X_GLYPH(sim, col)); + stSimPutChar(sim, col, 10u, X_GLYPH(sim, col)); + stSimPutChar(sim, col, 15u, X_GLYPH(sim, col)); + stSimPutChar(sim, col, 20u, X_GLYPH(sim, col)); stSimPutColor(sim, col, 5u, 0x0Cu); stSimPutColor(sim, col, 10u, 0x0Cu); stSimPutColor(sim, col, 15u, 0x0Cu); @@ -462,18 +681,18 @@ static void hookXPerTick(StSimT *sim) { 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)); + stSimPutChar(sim, (uint8_t)(col + 11u), 5u, X_GLYPH(sim, col + 11u)); + stSimPutChar(sim, (uint8_t)(col + 11u), 10u, X_GLYPH(sim, col + 11u)); + stSimPutChar(sim, (uint8_t)(col + 11u), 15u, X_GLYPH(sim, col + 11u)); + stSimPutChar(sim, (uint8_t)(col + 11u), 20u, X_GLYPH(sim, 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; + sim->hookScratch.x.col++; + if (sim->hookScratch.x.col == 0x15u) { + sim->hookScratch.x.dir = 0xF8u; } return; } @@ -482,10 +701,10 @@ static void hookXPerTick(StSimT *sim) { 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)); + stSimPutChar(sim, (uint8_t)(col + 6u), 5u, X_GLYPH(sim, col + 6u)); + stSimPutChar(sim, (uint8_t)(col + 6u), 10u, X_GLYPH(sim, col + 6u)); + stSimPutChar(sim, (uint8_t)(col + 6u), 15u, X_GLYPH(sim, col + 6u)); + stSimPutChar(sim, (uint8_t)(col + 6u), 20u, X_GLYPH(sim, 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); @@ -493,18 +712,18 @@ static void hookXPerTick(StSimT *sim) { 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)); + stSimPutChar(sim, (uint8_t)(col + 17u), 5u, X_GLYPH(sim, col + 17u)); + stSimPutChar(sim, (uint8_t)(col + 17u), 10u, X_GLYPH(sim, col + 17u)); + stSimPutChar(sim, (uint8_t)(col + 17u), 15u, X_GLYPH(sim, col + 17u)); + stSimPutChar(sim, (uint8_t)(col + 17u), 20u, X_GLYPH(sim, 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; + sim->hookScratch.x.col--; + if (sim->hookScratch.x.col == 1u) { + sim->hookScratch.x.dir = 0x08u; } } @@ -514,11 +733,11 @@ static void hookXPerTick(StSimT *sim) { static void hookXPrelude0(StSimT *sim) { uint8_t k; - HK(sim, 0x7D9D) = 0xF8u; - HK(sim, 0x7D9C) = 0x0Bu; - HK(sim, 0x7D9E) = 0u; + sim->hookScratch.x.dir = 0xF8u; + sim->hookScratch.x.col = 0x0Bu; + sim->hookScratch.x.tick = 0u; for (k = 0u; k < 8u; k++) { - const uint8_t *src = HKP(sim, 0x7FB8 + k * 8u); + const uint8_t *src = X_PAD_SLOT(sim, k); sim->pads[k].x1Hi = src[0]; sim->pads[k].x1Lo = src[1]; sim->pads[k].x2Hi = src[2]; @@ -565,7 +784,6 @@ static void setSpriteColors(StSimT *sim, uint8_t color) { // 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. @@ -573,8 +791,8 @@ 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) { + sim->hookScratch.k.parity ^= 1u; + if ((sim->hookScratch.k.parity & 1u) != 0u) { return; } for (k = 0u; k < 6u; k++) { @@ -606,14 +824,14 @@ static void hookOScrollRow(StSimT *sim, uint16_t base) { 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) { + stSimRotateGlyph(sim, ST_CHAR_ELECTROID); // $7E6E bitScroll of the barrier glyph + sim->hookScratch.o.tick = (uint8_t)((sim->hookScratch.o.tick + 1u) & 7u); + if ((sim->hookScratch.o.tick & 3u) != 0u) { return; } hookOScrollRow(sim, 0xC9u); // $04C9 row 5 col 1 hookOScrollRow(sim, 0x209u); // $0609 row 12 col 1 - if (HK(sim, 0x7DBD) != 4u) { + if (sim->hookScratch.o.tick != 4u) { return; } hookOScrollRow(sim, 0x169u); // $0569 row 9 col 1 @@ -621,18 +839,6 @@ static void hookOPerTick(StSimT *sim) { } -// $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)); - } - stSimMarkChar(sim, 0x6Fu); -} - - // 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 @@ -665,10 +871,10 @@ static void hookRPerTick(StSimT *sim) { } -// 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. +// Level V "SHIFT-O-RAMA" per-tick ($7D9D): every screen tick, the 12 +// rows 6..17 each scroll one column, alternating direction in bands of +// two (bit 1 of the row index): bands with the bit set go left, the +// others 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]; @@ -721,12 +927,12 @@ static void hookQPrelude(StSimT *sim) { 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->hookScratch.q.phase[x] = Q_INIT_PHASE(sim, x); + sim->spr[x].ptr = Q_CEL(sim, sim->hookScratch.q.phase[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].col = Q_SPR_COL(sim, x); + sim->spr[x].msb = Q_SPR_MSB(sim, x); + sim->spr[x].row = Q_SPR_ROW(sim, x); sim->spr[x].enable = 1u; } } @@ -736,11 +942,11 @@ 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->hookScratch.q.phase[x]++; + if (sim->hookScratch.q.phase[x] == 0x0Cu) { + sim->hookScratch.q.phase[x] = 0u; } - sim->spr[x].ptr = HK(sim, 0x7DF2 + HK(sim, 0x7E83 + (uint8_t)x)); + sim->spr[x].ptr = Q_CEL(sim, sim->hookScratch.q.phase[x]); } } @@ -755,7 +961,7 @@ static uint8_t hookQInput(StSimT *sim, uint8_t input) { if (stSimRng(sim, 10u) < 8u) { return input; } - return HK(sim, 0x7E7F + (uint8_t)(stSimRng(sim, 4u) - 1u)); + return Q_SCRAMBLE(sim, (uint8_t)(stSimRng(sim, 4u) - 1u)); } @@ -765,7 +971,7 @@ static void hookIPrelude(StSimT *sim) { int8_t x; for (x = 7; x >= 3; x--) { - HK(sim, 0x7DB8 + (uint8_t)x) = 0u; // state + sim->hookScratch.i.state[x] = 0u; // state sim->spr[x].enable = 0u; sim->spr[x].color = 0x02u; } @@ -777,105 +983,632 @@ static void hookIPerTick(StSimT *sim) { for (s = 3; s <= 7; s++) { uint8_t x = (uint8_t)s; - uint8_t state = HK(sim, 0x7DB8 + x); + uint8_t state = sim->hookScratch.i.state[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->hookScratch.i.dx[x] = I_DX(sim, y); // dx + sim->spr[x].col = I_START_COL(sim, 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->hookScratch.i.dy[x] = (uint8_t)(stSimRng(sim, 2u) + 0xFDu); // dy = rng(2)-3 + sim->hookScratch.i.phase[x] = 0u; // cel phase + sim->hookScratch.i.state[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)); + stAudioSfx(I_SFX_FIRE(sim)); + sim->spr[x].color = I_COLOR(sim, (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; + uint8_t ph = (uint8_t)(sim->hookScratch.i.phase[x] + 1u); + sim->hookScratch.i.phase[x] = ph; if (ph == 5u) { - HK(sim, 0x7DC0 + x) = 0u; + sim->hookScratch.i.phase[x] = 0u; sim->spr[x].ptr = 0x85u; - HK(sim, 0x7DB8 + x) = 2u; + sim->hookScratch.i.state[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)); + stSimSpriteAddX(sim, x, sim->hookScratch.i.dx[x]); + sim->spr[x].row = (uint8_t)(sim->spr[x].row + sim->hookScratch.i.dy[x]); } - sim->spr[x].ptr = HK(sim, 0x7DD8 + ph); + sim->spr[x].ptr = I_CEL_RISE(sim, 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)); + uint8_t ph = (uint8_t)((sim->hookScratch.i.phase[x] + 1u) & 7u); + sim->hookScratch.i.phase[x] = ph; + sim->spr[x].ptr = I_CEL_BURST(sim, ph); + stSimSpriteAddX(sim, x, sim->hookScratch.i.dx[x]); + sim->spr[x].row = (uint8_t)(sim->spr[x].row + sim->hookScratch.i.dy[x]); if (sim->spr[x].row < 0x25u) { sim->spr[x].enable = 0u; - HK(sim, 0x7DB8 + x) = 0u; + sim->hookScratch.i.state[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; +// Level G "TELEPORTS" ($7DA0 prelude0, $7D9A prelude1, $7E01 per-tick2, +// $7DF2 verdict): five orbs on sprites 3..7 sit where the blob puts them +// and cycle their cel every fourth tick. The cab touching one starts a +// hop: it vanishes for $14 ticks with the stick dead and a falling tone, +// then reappears on a landing spot (hookGHop). Touching an orb is never +// a kill -- the verdict is 0 unless the scenery was hit, and a scenery +// hit also puts the cab sprite back. +static void hookGPrelude0(StSimT *sim) { + uint8_t x; + + for (x = ST_HOOK_FIRST_SPRITE; x < ST_HW_SPRITES; x++) { + sim->spr[x].color = ST_HK_G_ORB_COLOR; + sim->spr[x].ptr = HKT(sim, ST_HK_G_CEL); + sim->spr[x].col = G_ORB_COL(sim, x); + sim->spr[x].msb = G_ORB_MSB(sim, x); + sim->spr[x].row = G_ORB_ROW(sim, x); + sim->spr[x].enable = 1u; } - 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; + sim->hookScratch.g.celPhase = 0u; + sim->hookScratch.g.hop = 0u; +} + + +// $7E4A -- land: rng(2) odd with a fare aboard sends the cab to its +// destination pad's spot (UP and beyond = spot 5); otherwise a random +// spot, stepped down if it repeats the last one (wrapping 1 -> 5). +static void hookGHop(StSimT *sim) { + StHookGT *g = &sim->hookScratch.g; + uint8_t spot; + uint8_t i; + + if ((stSimRng(sim, 2u) & 1u) != 0u && sim->activeSpriteIdx != 0u) { + spot = sim->activeSpriteIdx; + if (spot >= ST_HOOK_G_PAD_SPOTS) { + spot = ST_HOOK_G_SPOTS; + } + } else { + spot = stSimRng(sim, ST_HOOK_G_SPOTS); + if (spot == g->lastSpot) { + spot--; + if (spot == 0u) { + spot = ST_HOOK_G_SPOTS; + } } } + g->lastSpot = spot; + i = (uint8_t)(spot - 1u); + sim->spr[0].col = G_SPOT_COL(sim, i); + sim->posXcol = sim->spr[0].col; + sim->spr[0].msb = G_SPOT_MSB(sim, i); + sim->posXmsb = sim->spr[0].msb; + sim->spr[0].row = G_SPOT_ROW(sim, i); + sim->posYrow = sim->spr[0].row; + sim->velX = 0; + sim->velY = 0; } -// 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) { +static void hookGPerTick(StSimT *sim) { + StHookGT *g = &sim->hookScratch.g; + uint8_t x; + + g->tick++; + if ((g->tick & 3u) == 0u) { + uint8_t cel; + g->celPhase++; + if (g->celPhase == ST_HK_G_CEL_PHASES) { + g->celPhase = 0u; + } + cel = G_CEL(sim, g->celPhase); + for (x = ST_HOOK_FIRST_SPRITE; x < ST_HW_SPRITES; x++) { + sim->spr[x].ptr = cel; + } + } + if (g->hop != 0u) { + // $7ECB: the tone falls, the stick is dead, and at zero the cab + // is back and lands. + g->sweep = (uint8_t)(g->sweep - ST_HOOK_G_SWEEP_STEP); + stAudioVoice1Freq(g->sweep); + sim->dirMask = 0u; + g->hop--; + if (g->hop == 0u) { + sim->spr[0].enable = 1u; + hookGHop(sim); + } 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); + if ((sim->spriteSpriteColl & ST_HOOK_FALL_SPRITES) == 0u) { + return; + } + // $7E3D: an orb took the cab. + sim->spr[0].enable = 0u; + g->hop = ST_HOOK_G_HOP_TICKS; + stAudioSfx(G_SFX_HOP(sim)); + g->sweep = ST_HOOK_G_SWEEP_START; + sim->velX = 0; + sim->velY = 0; +} + + +// Level U "REBOUND" ($7D9B prelude0, $7DAD per-tick2). Five orbs drift +// in from the four edges on their own cel walk; touching one bounces the +// cab by negating its velocity. The orb cels are the level's own sprite +// blocks $80..$82, and every table below is read from the shipped hook +// blob at the address the original indexes. +// +// $7D9B -- clear the orb states and colour the five hazard sprites. +static void hookUPrelude0(StSimT *sim) { + uint8_t x; + + for (x = (uint8_t)(ST_HW_SPRITES - 1u); x >= ST_HOOK_FIRST_SPRITE; x--) { + sim->hookScratch.u.state[x] = 0u; + sim->spr[x].color = 0x07u; } } -#endif /* !IIGS */ +// $7DDD -- a cab/orb contact bounces the cab, then holds off for twelve +// ticks. Which axes flip is a 1-in-3 roll: 1 negates both, 2 negates +// neither and 3 negates Y alone -- the original has no X-only case. +static void hookUBounce(StSimT *sim) { + uint8_t coll; + uint8_t hits = 0u; + uint8_t b; + uint8_t pick; + + if (sim->hookScratch.u.cooldown != 0u) { + sim->hookScratch.u.cooldown--; + return; + } + if ((sim->spriteSpriteColl & 1u) == 0u) { + return; + } + // $7DF8: count the orb sprites in the latch (bits 7..3). + coll = sim->spriteSpriteColl; + for (b = (uint8_t)(ST_HW_SPRITES - 1u); b >= ST_HOOK_FIRST_SPRITE; b--) { + if ((coll & 0x80u) != 0u) { + hits++; + } + coll = (uint8_t)(coll << 1); + } + if (hits == 0u) { + return; + } + sim->hookScratch.u.cooldown = ST_HOOK_U_COOLDOWN; + stAudioSfx(U_SFX_BOUNCE(sim)); + pick = stSimRng(sim, 3u); + if (pick == 1u) { + sim->velX = (int16_t)(-sim->velX); + } + if (pick == 2u) { + return; + } + sim->velY = (int16_t)(-sim->velY); +} + + +// $7E60 -- the orbs themselves: an idle one appears on a 2-in-30 roll +// from one of the four edges (0 up from the floor, 1 down from the +// ceiling, 2 in from the left, 3 in from the right), an active one +// drifts by its direction's step, walks its cel and expires. +static void hookUOrbs(StSimT *sim) { + uint8_t x; + + for (x = (uint8_t)(ST_HW_SPRITES - 1u); x >= ST_HOOK_FIRST_SPRITE; x--) { + uint8_t state = sim->hookScratch.u.state[x]; + uint8_t dir; + + if (state == 0u) { + // $7E84: nothing here yet -- maybe start one. + if (stSimRng(sim, 0x1Eu) >= 3u) { + continue; + } + dir = (uint8_t)(stSimRng(sim, 4u) - 1u); + sim->hookScratch.u.dir[x] = dir; + sim->hookScratch.u.life[x] = U_LIFE(sim, dir); + if ((dir & 2u) == 0u) { + // $7EA8: fixed row, random column. + uint16_t col = (uint16_t)(((uint16_t)stSimRng(sim, 0x90u) + 0x11u) << 1); + sim->spr[x].row = U_ENTRY(sim, dir); + sim->spr[x].col = (uint8_t)col; + sim->spr[x].msb = (uint8_t)(col >> 8); + } else { + // $7ECF: fixed column, random row. + sim->spr[x].col = U_ENTRY(sim, dir); + sim->spr[x].msb = U_ENTRY_MSB(sim, dir); + sim->spr[x].row = (uint8_t)((uint8_t)(stSimRng(sim, 0x4Fu) + 0x1Du) << 1); + } + sim->hookScratch.u.state[x] = 1u; + sim->spr[x].enable = 1u; + sim->spr[x].ptr = ST_HK_FALL_CEL; + sim->hookScratch.u.celPhase[x] = 0u; + continue; + } + if (state != 1u) { + sim->hookScratch.u.state[x] = 0u; + continue; + } + // $7F02: drift until the lifetime runs out. + sim->hookScratch.u.life[x]--; + if (sim->hookScratch.u.life[x] == 0u) { + sim->spr[x].enable = 0u; + sim->hookScratch.u.state[x] = 0u; + continue; + } + dir = sim->hookScratch.u.dir[x]; + stSimSpriteAddX(sim, x, U_DX(sim, dir)); + sim->spr[x].row = (uint8_t)(sim->spr[x].row + U_DY(sim, dir)); + sim->hookScratch.u.celPhase[x] = (uint8_t)((sim->hookScratch.u.celPhase[x] + 1u) & 3u); + sim->spr[x].ptr = U_CEL(sim, sim->hookScratch.u.celPhase[x]); + } +} + + +static void hookUPerTick(StSimT *sim) { + // $7DAD: the original opens with a ceiling reflect that only runs + // while a crash is in progress and only above row $11. The wreck + // sequence moves sprite 0 downward to $DA and never back up, so this + // cannot fire on the real machine either; it is kept as shipped. + if (sim->collisionPhase != 0u && sim->spr[0].row < 0x11u) { + sim->velY = (int16_t)(-sim->velY); + sim->posYrow = (uint8_t)(sim->posYrow + 2u); + } + hookUBounce(sim); + hookUOrbs(sim); +} + + // --------------------------------------------------------------------------- // Dispatch // --------------------------------------------------------------------------- +// $62F0 -- a converted level's scratch starts from the bytes the shipped +// blob holds at the addresses the original used, so the first tick sees +// exactly what the C64's did. +// Level E "BEANSTALK" ($7DA0 prelude0, $7DCA per-tick2): the screen +// opens with pad 1 only. While the fare aboard wants UP the stalk grows: +// every $57 ticks a leaf cell sprouts right along row `row` and left +// along row `row`-2, the green tips one cell beyond; the fifth step +// hangs pads N and N+1 at the leaf ends instead, opens them (pad count +// +2) and moves the growth four rows up, until pad 9 is out. $7DA0 runs +// from the respawn prelude, so a lost cab restarts the count from pad 1 +// while the leaves already drawn stay. +static void hookEPrelude0(StSimT *sim) { + StHookET *e = &sim->hookScratch.e; + + sim->specialPad = 1u; + sim->padCount = 1u; + e->seg = 0u; + e->padSlot = 2u; + e->row = ST_HOOK_E_FIRST_ROW; + e->tick = ST_HOOK_E_TICK_START; + if (sim->levelState == 1u) { + e->tick = ST_HOOK_E_TICK_SECOND; + } +} + + +// $7E89 -- one green cell. +static void hookEPut(StSimT *sim, uint8_t col, uint8_t row, uint8_t ch) { + stSimPutChar(sim, col, row, ch); + stSimPutColor(sim, col, row, ST_HOOK_E_COLOR); +} + + +// $7E39 -- the tips, one cell beyond this step's leaves. +static void hookETips(StSimT *sim) { + const StHookET *e = &sim->hookScratch.e; + + hookEPut(sim, (uint8_t)(ST_HOOK_E_STALK_COL + 1u + e->seg), e->row, ST_HOOK_E_CHAR_TIP_R); + hookEPut(sim, (uint8_t)(ST_HOOK_E_STALK_COL - 1u - e->seg), (uint8_t)(e->row - 2u), ST_HOOK_E_CHAR_TIP_L); +} + + +static void hookEPerTick(StSimT *sim) { + StHookET *e = &sim->hookScratch.e; + + if (sim->specialPad == ST_HOOK_E_LAST_PAD || sim->activeSpriteIdx != ST_FARE_DEST_UP) { + return; + } + e->tick++; + if (e->tick != ST_HOOK_E_GROW_TICKS) { + return; + } + e->tick = 0u; + e->seg++; + if (e->seg != ST_HOOK_E_LEAVES) { + hookEPut(sim, (uint8_t)(ST_HOOK_E_STALK_COL + e->seg), e->row, ST_HOOK_E_CHAR_LEAF); + hookEPut(sim, (uint8_t)(ST_HOOK_E_STALK_COL - e->seg), (uint8_t)(e->row - 2u), ST_HOOK_E_CHAR_LEAF); + hookETips(sim); + return; + } + // $7E15: the pads hang from the leaf ends, then the stalk climbs. + hookEPut(sim, ST_HOOK_E_PAD_COL_RIGHT, e->row, (uint8_t)(ST_CHAR_DIGIT_BASE + e->padSlot)); + hookEPut(sim, ST_HOOK_E_PAD_COL_LEFT, (uint8_t)(e->row - 2u), (uint8_t)(ST_CHAR_DIGIT_BASE + e->padSlot + 1u)); + hookETips(sim); + sim->specialPad = (uint8_t)(sim->specialPad + 2u); + sim->padCount = (uint8_t)(sim->padCount + 2u); + e->seg = 0u; + e->row = (uint8_t)(e->row - ST_HOOK_E_ROW_STEP); + e->padSlot = (uint8_t)(e->padSlot + 2u); + stAudioSfx(E_SFX_GROW(sim)); +} + + +// $7DAF (J) / $7DFE (P) -- a hazard that touched the scenery below row +// $38 bursts: cel $82, phase 0, dropped four rows onto what it hit. +// Sprites 7 down to 3, every one that hit. +static void hookFallBgHits(StSimT *sim, uint8_t *state, uint8_t *phase, const uint8_t *sfx) { + uint8_t x; + + for (x = (uint8_t)(ST_HW_SPRITES - 1u); x >= ST_HOOK_FIRST_SPRITE; x--) { + if ((sim->spriteBgColl & kStBit[x]) == 0u || sim->spr[x].row < ST_HOOK_FALL_HIT_ROW || state[x] == 2u) { + continue; + } + state[x] = 2u; + sim->spr[x].ptr = ST_HK_FALL_CEL_BURST; + phase[x] = 0u; + sim->spr[x].row = (uint8_t)(sim->spr[x].row + ST_HOOK_FALL_HIT_DROP); + if (sfx != 0) { + stAudioSfx(sfx); + } + } +} + + +// $7E01 (J) / $7E45 (P) -- the per-sprite machine, sprites 3..7. Idle: +// spawn on rng($3C) < 3 at rng($FF) + the sprite's column base (J +// rerolls one over the middle band), cel $80 on row $17. Falling: one +// row per tick, cel flipping $80/$81 and drifting sideways on the odd +// cel. Bursting: walk the cel run (P only every other count of a +// shared parity) and go idle at its end. +static void hookFallTick(StSimT *sim, uint8_t *state, uint8_t *phase, const StHookFallT *d, uint8_t *parity) { + uint8_t x; + + for (x = ST_HOOK_FIRST_SPRITE; x < ST_HW_SPRITES; x++) { + if (state[x] == 0u) { + uint16_t col; + if (stSimRng(sim, ST_HOOK_FALL_SPAWN_ODDS) >= ST_HOOK_FALL_SPAWN_HIT) { + continue; + } + do { + col = (uint16_t)(stSimRng(sim, 0xFFu) + HKT(sim, d->startTable + x)); + } while (d->avoidMiddle && col >= ST_HOOK_FALL_BAND_LO && col < ST_HOOK_FALL_BAND_HI); + sim->spr[x].col = (uint8_t)col; + sim->spr[x].msb = (uint8_t)(col >> 8); + sim->spr[x].ptr = ST_HK_FALL_CEL; + sim->spr[x].row = ST_HOOK_FALL_TOP_ROW; + sim->spr[x].enable = 1u; + state[x] = 1u; + } else if (state[x] == 1u) { + sim->spr[x].ptr ^= 1u; + if ((sim->spr[x].ptr & 1u) != 0u) { + stSimSpriteAddX(sim, x, HKT(sim, d->dxTable + x)); + } + sim->spr[x].row++; + } else if (state[x] == 2u) { + if (parity != 0) { + (*parity)++; + if ((*parity & 1u) != 0u) { + continue; + } + } + phase[x]++; + if (phase[x] == d->phaseEnd) { + state[x] = 0u; + sim->spr[x].enable = 0u; + } else { + sim->spr[x].ptr = HKT(sim, d->celTable + phase[x]); + } + } else { + state[x] = 0u; // $7E1C: anything else resets + } + } +} + + +// Level J "SHOOTING STARS" ($7D9D prelude0, $7DAF per-tick2): stars fall +// from the top and burst on the scenery (hookFall*). The hook reads the +// stars' own $D01F bits, so the prelude opts them into the collision +// test. +static void hookJPrelude0(StSimT *sim) { + uint8_t x; + + for (x = ST_HOOK_FIRST_SPRITE; x < ST_HW_SPRITES; x++) { + sim->hookScratch.j.state[x] = 0u; + sim->spr[x].color = ST_HK_J_COLOR; + } + sim->bgCollSprites = ST_HOOK_FALL_SPRITES; +} + + +static void hookJPerTick(StSimT *sim) { + StHookJT *j = &sim->hookScratch.j; + + hookFallBgHits(sim, j->state, j->phase, J_SFX_BURST(sim)); + hookFallTick(sim, j->state, j->phase, &kFallJ, 0); +} + + +// Level L "BLACK HOLE" ($7D9F prelude0, $7DBC per-tick2, $7D9B verdict): +// hardware sprite 3 spins through cels $80..$82 at ($AC, $82) and pulls +// the cab in -- each tick the gravity templates become the pull toward +// it, weaker with distance. +static void hookLPrelude0(StSimT *sim) { + StSpriteT *hole = &sim->spr[ST_HOOK_L_HOLE]; + + hole->color = ST_HK_L_COLOR; + hole->msb = 0u; + hole->col = ST_HOOK_L_HOLE_COL; + hole->row = ST_HOOK_L_HOLE_ROW; + hole->ptr = ST_HK_L_CEL_FIRST; + hole->enable = 1u; +} + + +// $7DCB / $7E01 -- base less |dist|/8 ($7E42), signed by the direction. +static uint16_t hookLPull(uint8_t base, uint8_t dist) { + uint8_t mag = dist; + uint16_t pull; + + if ((mag & 0x80u) != 0u) { + mag = (uint8_t)(0u - mag); + } + pull = (uint8_t)(base - (uint8_t)(mag >> 3)); + if ((dist & 0x80u) != 0u) { + pull = (uint16_t)(0u - pull); + } + return pull; +} + + +static void hookLPerTick(StSimT *sim) { + StSpriteT *hole = &sim->spr[ST_HOOK_L_HOLE]; + uint8_t dy; + uint8_t dx; + + hole->ptr++; + if (hole->ptr == ST_HK_L_CEL_END) { + hole->ptr = ST_HK_L_CEL_FIRST; + } + dy = (uint8_t)(ST_HOOK_L_HOLE_ROW - sim->spr[0].row); + sim->gravTemplateY = hookLPull(ST_HOOK_L_PULL_Y, dy); + dx = (uint8_t)(ST_HOOK_L_HOLE_X_HALF - (uint8_t)(stSimSpriteX(sim, 0u) >> 1)); + sim->gravTemplateX = hookLPull(ST_HOOK_L_PULL_X, dx); +} + + +// Level P "BLIZZARD" ($7DA8 prelude0, $7D9C prelude1, $7DBA per-tick2): +// snow falls and melts on the scenery (hookFall*), and now and then the +// wind turns: the windsocks flip and the X gravity changes sign. +static void hookPPrelude0(StSimT *sim) { + uint8_t x; + + for (x = ST_HOOK_FIRST_SPRITE; x < ST_HW_SPRITES; x++) { + sim->hookScratch.p.state[x] = 0u; + sim->spr[x].color = ST_HK_P_COLOR; + } + sim->bgCollSprites = ST_HOOK_FALL_SPRITES; +} + + +static void hookPFlipSock(StSimT *sim, uint16_t cell) { + stSimPutChar(sim, (uint8_t)(cell % ST_SCREEN_COLS), (uint8_t)(cell / ST_SCREEN_COLS), (uint8_t)(sim->screen[cell] ^ ST_HOOK_P_SOCK_FLIP)); +} + + +static void hookPPerTick(StSimT *sim) { + StHookPT *p = &sim->hookScratch.p; + + if (stSimRng(sim, ST_HOOK_P_WIND_ODDS) < ST_HOOK_P_WIND_HIT) { + stAudioSfx(P_SFX_WIND(sim)); + sim->gravTemplateX = (uint16_t)(0u - sim->gravTemplateX); + hookPFlipSock(sim, ST_CELL(7, 6)); // $051E + hookPFlipSock(sim, ST_CELL(9, 21)); // $057D + hookPFlipSock(sim, ST_CELL(15, 31)); // $0677 + } + hookFallBgHits(sim, p->state, p->phase, 0); + hookFallTick(sim, p->state, p->phase, &kFallP, &p->parity); +} + + +// Level S "THE SWITCH" ($7D9D input filter): the stick's direction bits +// come back swapped through the level's table; FIRE passes. +static uint8_t hookSInput(StSimT *sim, uint8_t input) { + return (uint8_t)((input & ST_HOOK_S_FIRE) | S_DIR_MAP(sim, input & ST_HOOK_S_DIRS)); +} + + +void stHookSceneLoad(StSimT *sim) { + uint8_t k; + + switch (sim->hookLevel) { + case ST_LEVEL_G: + sim->hookScratch.g.lastSpot = HKT(sim, 0x7DD1); + sim->hookScratch.g.celPhase = HKT(sim, 0x7DD2); + sim->hookScratch.g.hop = HKT(sim, 0x7DD3); + sim->hookScratch.g.tick = HKT(sim, 0x7E00); + sim->hookScratch.g.sweep = HKT(sim, 0x7F02); + break; + case ST_LEVEL_H: + sim->hookScratch.h.switchId = HKT(sim, 0x7E4C); + sim->hookScratch.h.tick = HKT(sim, 0x7E53); + sim->hookScratch.h.active = HKT(sim, 0x7E54); + sim->hookScratch.h.step = HKT(sim, 0x7E55); + sim->hookScratch.h.tone = HKT(sim, 0x7E56); + for (k = 0u; k < ST_HOOK_H_SEGS; k++) { + sim->hookScratch.h.segFlag[k] = HKT(sim, 0x7E15 + k); + } + break; + case ST_LEVEL_I: + for (k = 0u; k < ST_HW_SPRITES; k++) { + sim->hookScratch.i.state[k] = HKT(sim, 0x7DB8 + k); + sim->hookScratch.i.phase[k] = HKT(sim, 0x7DC0 + k); + sim->hookScratch.i.dx[k] = HKT(sim, 0x7DC8 + k); + sim->hookScratch.i.dy[k] = HKT(sim, 0x7DD0 + k); + } + break; + case ST_LEVEL_K: + sim->hookScratch.k.parity = HKT(sim, 0x7DBD); + break; + case ST_LEVEL_O: + sim->hookScratch.o.tick = HKT(sim, 0x7DBD); + break; + case ST_LEVEL_P: + sim->hookScratch.p.parity = HKT(sim, 0x7F1F); + break; + case ST_LEVEL_Q: + for (k = 0u; k < ST_HW_SPRITES; k++) { + sim->hookScratch.q.phase[k] = HKT(sim, 0x7E83 + k); + } + break; + case ST_LEVEL_T: + sim->hookScratch.t.gateShut = HKT(sim, 0x7D9C); + break; + case ST_LEVEL_U: + sim->hookScratch.u.cooldown = HKT(sim, 0x7F6D); + for (k = 0u; k < ST_HW_SPRITES; k++) { + sim->hookScratch.u.celPhase[k] = HKT(sim, 0x7F4F + k); + sim->hookScratch.u.state[k] = HKT(sim, 0x7F57 + k); + sim->hookScratch.u.dir[k] = HKT(sim, 0x7F5F + k); + sim->hookScratch.u.life[k] = HKT(sim, 0x7F6E + k); + } + break; + case ST_LEVEL_W: + sim->hookScratch.w.colorPhase = HKT(sim, 0x7DAB); + sim->hookScratch.w.started = HKT(sim, 0x7DE5); + for (k = 0u; k < ST_HOOK_W_LASERS; k++) { + sim->hookScratch.w.state[k] = HKT(sim, 0x7DAC + k); + sim->hookScratch.w.row[k] = HKT(sim, 0x7DB5 + k); + } + break; + case ST_LEVEL_X: + sim->hookScratch.x.col = HKT(sim, 0x7D9C); + sim->hookScratch.x.dir = HKT(sim, 0x7D9D); + sim->hookScratch.x.tick = HKT(sim, 0x7D9E); + break; + default: + break; + } +} + + // $7D75 -- 0 means sprite contact (passenger, level sprites) is safe. uint8_t stHookHitVerdict(StSimT *sim) { switch (sim->hookLevel) { + case ST_LEVEL_G: + // $7DF2: only the scenery kills; that also unhides the cab. + if ((sim->spriteBgColl & 1u) != 0u) { + sim->spr[0].enable = 1u; + return 1u; + } + return 0u; case ST_LEVEL_H: return 0u; + case ST_LEVEL_L: + return ST_HOOK_L_VERDICT; default: return 1u; } @@ -884,67 +1617,95 @@ uint8_t stHookHitVerdict(StSimT *sim) { // $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); + case ST_LEVEL_S: + return hookSInput(sim, input); default: break; } -#else - (void)sim; -#endif return input; } -// $7D6C -- before the sprite flush. -void stHookPerTick2(StSimT *sim) { +// $7D6C -- before the sprite flush. Split in two: one switch over all +// sixteen levels left the 65816's single accumulator with more live +// values than greedy register allocation can place, and the build then +// falls back to the basic allocator for this whole file. +ST_NOINLINE static void hookPerTickLow(StSimT *sim) { switch (sim->hookLevel) { + case ST_LEVEL_E: + hookEPerTick(sim); + break; + case ST_LEVEL_G: + hookGPerTick(sim); + break; case ST_LEVEL_H: hookHPerTick(sim); break; + case ST_LEVEL_I: + hookIPerTick(sim); + break; + case ST_LEVEL_J: + hookJPerTick(sim); + break; + case ST_LEVEL_K: + hookKPerTick(sim); + break; + case ST_LEVEL_L: + hookLPerTick(sim); + break; + default: + break; + } +} + + +ST_NOINLINE static void hookPerTickHigh(StSimT *sim) { + switch (sim->hookLevel) { + case ST_LEVEL_O: + hookOPerTick(sim); + break; + case ST_LEVEL_P: + hookPPerTick(sim); + break; + case ST_LEVEL_Q: + hookQPerTick(sim); + break; + case ST_LEVEL_R: + hookRPerTick(sim); + break; case ST_LEVEL_T: hookTPerTick(sim); break; + case ST_LEVEL_U: + hookUPerTick(sim); + break; + case ST_LEVEL_V: + hookVPerTick(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; } } +void stHookPerTick2(StSimT *sim) { + if (sim->hookLevel <= ST_LEVEL_L) { + hookPerTickLow(sim); + return; + } + hookPerTickHigh(sim); +} + + // $7D6F -- after the sprite flush. void stHookPerTick3(StSimT *sim) { (void)sim; @@ -960,21 +1721,32 @@ void stHookPrelude0(StSimT *sim) { case ST_LEVEL_X: hookXPrelude0(sim); break; -#if !defined(__W65816__) + case ST_LEVEL_U: + hookUPrelude0(sim); + break; 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; - } - } + memset(sim->hookScratch.w.state, 0, sizeof(sim->hookScratch.w.state)); + break; + case ST_LEVEL_E: + hookEPrelude0(sim); + break; + case ST_LEVEL_G: + hookGPrelude0(sim); + break; + case ST_LEVEL_J: + hookJPrelude0(sim); + break; + case ST_LEVEL_L: + hookLPrelude0(sim); + break; + case ST_LEVEL_P: + hookPPrelude0(sim); break; default: break; @@ -989,13 +1761,17 @@ void stHookPrelude1(StSimT *sim) { hookTPrelude1(sim); break; case ST_LEVEL_W: - HK(sim, 0x7DE5) = 1u; + sim->hookScratch.w.started = 1u; break; -#if !defined(__W65816__) case ST_LEVEL_R: hookRMaze(sim, ST_CHAR_BLANK); break; -#endif + case ST_LEVEL_P: + stAudioSfx(P_SFX_WIND(sim)); + break; + case ST_LEVEL_G: + sim->hookScratch.g.lastSpot = ST_HOOK_G_SPOTS; // $7D9A + break; default: break; } diff --git a/examples/spacetaxi/stLevelCels.c b/examples/spacetaxi/stLevelCels.c new file mode 100644 index 0000000..bb916e7 --- /dev/null +++ b/examples/spacetaxi/stLevelCels.c @@ -0,0 +1,114 @@ +// Space Taxi -- the cels a level's hook program can put on screen. +// +// Hook sprites are built from the level's own VIC bitmaps in whatever +// colour the hook gives them, and compiling one on the 65816 costs about +// half a second -- far too long to do the first time a cel appears mid- +// ride. So every level lists its cels up front, from the same hook +// tables the hook program reads (stHookTables.h): the offline baker turns +// the list into a pre-compiled bank (sprites/levelNN.spc) and the game +// compiles whatever the bank does not supply BEFORE the ride starts. +// +// Only the level file is needed, no simulation, so the baker can link +// this on the host without the rest of the game. + +#include "stCels.h" +#include "stHookTables.h" + +#define HKL(level, addr) ((level)->hookData[(addr) - ST_HOOK_BASE]) + +static void celAdd(StCelDefT *out, uint8_t *count, uint8_t max, uint8_t ptr, uint8_t color, uint8_t multi, uint8_t mc0, uint8_t mc1); + + +// Append one cel unless the list already has it. +static void celAdd(StCelDefT *out, uint8_t *count, uint8_t max, uint8_t ptr, uint8_t color, uint8_t multi, uint8_t mc0, uint8_t mc1) { + uint8_t k; + + for (k = 0u; k < *count; k++) { + if (out[k].ptr == ptr && out[k].color == color && out[k].multi == multi && out[k].mc0 == mc0 && out[k].mc1 == mc1) { + return; + } + } + if (*count == max) { + return; + } + out[*count].ptr = ptr; + out[*count].color = color; + out[*count].multi = multi; + out[*count].mc0 = mc0; + out[*count].mc1 = mc1; + (*count)++; +} + + +uint8_t stLevelCelList(const StLevelT *level, StCelDefT *out, uint8_t max) { + uint8_t n = 0u; + uint8_t k; + uint8_t c; + + switch (level->levelIndex) { + case ST_LEVEL_G: + for (k = 0u; k < ST_HK_G_CEL_PHASES; k++) { + celAdd(out, &n, max, HKL(level, ST_HK_G_CEL + k), ST_HK_G_ORB_COLOR, 0u, 0u, 0u); + } + break; + case ST_LEVEL_H: + celAdd(out, &n, max, ST_HK_H_CEL, ST_HK_H_COLOR_IDLE, 0u, 0u, 0u); + celAdd(out, &n, max, ST_HK_H_CEL, ST_HK_H_COLOR_LIT, 0u, 0u, 0u); + celAdd(out, &n, max, ST_HK_H_CEL, ST_HK_H_COLOR_ACTIVE, 0u, 0u, 0u); + break; + case ST_LEVEL_I: + // Every cel in the idle colour and in each of the three the + // launcher picks at random. + for (c = 0u; c <= ST_HK_I_COLORS; c++) { + uint8_t color = (c == 0u) ? ST_HK_I_COLOR_IDLE : HKL(level, ST_HK_I_COLOR + c - 1u); + for (k = 0u; k < ST_HK_I_RISE_PHASES; k++) { + celAdd(out, &n, max, HKL(level, ST_HK_I_CEL_RISE + k), color, 0u, 0u, 0u); + } + for (k = 0u; k < ST_HK_I_BURST_PHASES; k++) { + celAdd(out, &n, max, HKL(level, ST_HK_I_CEL_BURST + k), color, 0u, 0u, 0u); + } + } + break; + case ST_LEVEL_J: + celAdd(out, &n, max, ST_HK_FALL_CEL, ST_HK_J_COLOR, 0u, 0u, 0u); + celAdd(out, &n, max, (uint8_t)(ST_HK_FALL_CEL + 1u), ST_HK_J_COLOR, 0u, 0u, 0u); + celAdd(out, &n, max, ST_HK_FALL_CEL_BURST, ST_HK_J_COLOR, 0u, 0u, 0u); + for (k = 1u; k < ST_HK_J_BURST_PHASES; k++) { + celAdd(out, &n, max, HKL(level, ST_HK_J_CEL_BURST + k), ST_HK_J_COLOR, 0u, 0u, 0u); + } + break; + case ST_LEVEL_L: + for (k = ST_HK_L_CEL_FIRST; k < ST_HK_L_CEL_END; k++) { + celAdd(out, &n, max, k, ST_HK_L_COLOR, 0u, 0u, 0u); + } + break; + case ST_LEVEL_P: + celAdd(out, &n, max, ST_HK_FALL_CEL, ST_HK_P_COLOR, 0u, 0u, 0u); + celAdd(out, &n, max, (uint8_t)(ST_HK_FALL_CEL + 1u), ST_HK_P_COLOR, 0u, 0u, 0u); + celAdd(out, &n, max, ST_HK_FALL_CEL_BURST, ST_HK_P_COLOR, 0u, 0u, 0u); + for (k = 1u; k < ST_HK_P_BURST_PHASES; k++) { + celAdd(out, &n, max, HKL(level, ST_HK_P_CEL_BURST + k), ST_HK_P_COLOR, 0u, 0u, 0u); + } + break; + case ST_LEVEL_Q: + // The hook's cels show HIRES in colour 6 for the whole recorded + // ride (tools/stsim STSIM_CELS census, 2026-09-22); the + // multicolour variant (the level's own $D025/$D026) stays + // listed for the phases the census did not reach. + for (k = 0u; k < ST_HK_Q_CEL_PHASES; k++) { + celAdd(out, &n, max, HKL(level, ST_HK_Q_CEL + k), ST_HK_Q_COLOR, 0u, 0u, 0u); + } + for (k = 0u; k < ST_HK_Q_CEL_PHASES; k++) { + celAdd(out, &n, max, HKL(level, ST_HK_Q_CEL + k), ST_HK_Q_COLOR, 1u, level->header[5], level->header[6]); + } + break; + case ST_LEVEL_U: + for (k = 0u; k < ST_HK_U_CEL_PHASES; k++) { + celAdd(out, &n, max, HKL(level, ST_HK_U_CEL + k), ST_HK_U_COLOR, 0u, 0u, 0u); + } + break; + default: + break; + } + return n; +} diff --git a/examples/spacetaxi/stMarshalIigs.s b/examples/spacetaxi/stMarshalIigs.s new file mode 100644 index 0000000..ea291c5 --- /dev/null +++ b/examples/spacetaxi/stMarshalIigs.s @@ -0,0 +1,354 @@ +; stMarshalIigs.s - Space Taxi's stSimMarshal for the Apple IIgs. +; +; GAS syntax for the llvm-mos w65816 toolchain (see src/iigs/peislam.s +; for the conventions). The C version copies eight sprites' shadow +; registers into the displayed frame through far pointers, which the +; w65816 backend turns into ~5000 cycles of pointer re-materialisation +; per tick (5% of a ride frame). This is the same copy as straight-line +; [dp],y moves: ~700 cycles. +; +; void stSimMarshalIigs(const StMarshalArgsT *args) +; args (A = low word, X = bank) points at { spr, frame, multi }: +; +0 const StSpriteT *spr (32-bit: low word, bank word) +; +4 StFrameT *frame +; +8 uint8_t multi +; StSpriteT (6 bytes): col, msb, row, enable, ptr, color. +; StFrameT: x[8] (u16, +0), y[8] (+16), ptr[8] (+24), color[8] (+32), +; enableMask (+40), multiMask (+41). +; stSim.c checks these layouts at compile time. +; +; Every store runs with M=16 and writes two bytes: the byte after each +; single-byte field gets a junk high byte that the NEXT store repairs, +; so the blocks run in order x, y, ptr, color and finish with the +; enable/multi word. The last colour store's junk lands on enableMask, +; which the final word overwrites. +; +; Direct-page $E0-$EF are the compiler's own per-function scratch +; (re-set before every use, never live across a call); $C0 is left alone. + .section .text.stSimMarshalIigs,"ax" + .globl stSimMarshalIigs +stSimMarshalIigs: + php + rep #0x30 + .a16 + .i16 + sta 0xe0 ; args low + txa + sta 0xe2 ; args bank + ldy #0 + lda [0xe0],y + sta 0xe4 ; spr low + ldy #2 + lda [0xe0],y + sta 0xe6 ; spr bank + ldy #4 + lda [0xe0],y + sta 0xe8 ; frame low + ldy #6 + lda [0xe0],y + sta 0xea ; frame bank + ldy #8 + lda [0xe0],y + and #0xff + sta 0xec ; multi + stz 0xee ; enable mask accumulator +; sprite 0: x = (msb ? 0x100 : 0) | col; enable bit + ldy #0 + lda [0xe4],y ; col | msb<<8 + sta 0xf0 + and #0xff00 + beq mX0clr + lda 0xf0 + and #0x00ff + ora #0x0100 + bra mX0st +mX0clr: + lda 0xf0 + and #0x00ff +mX0st: + ldy #0 + sta [0xe8],y + ldy #3 + lda [0xe4],y ; enable | ptr<<8 + and #0xff + beq mE0off + lda 0xee + ora #1 + sta 0xee +mE0off: +; sprite 1: x = (msb ? 0x100 : 0) | col; enable bit + ldy #6 + lda [0xe4],y ; col | msb<<8 + sta 0xf0 + and #0xff00 + beq mX1clr + lda 0xf0 + and #0x00ff + ora #0x0100 + bra mX1st +mX1clr: + lda 0xf0 + and #0x00ff +mX1st: + ldy #2 + sta [0xe8],y + ldy #9 + lda [0xe4],y ; enable | ptr<<8 + and #0xff + beq mE1off + lda 0xee + ora #2 + sta 0xee +mE1off: +; sprite 2: x = (msb ? 0x100 : 0) | col; enable bit + ldy #12 + lda [0xe4],y ; col | msb<<8 + sta 0xf0 + and #0xff00 + beq mX2clr + lda 0xf0 + and #0x00ff + ora #0x0100 + bra mX2st +mX2clr: + lda 0xf0 + and #0x00ff +mX2st: + ldy #4 + sta [0xe8],y + ldy #15 + lda [0xe4],y ; enable | ptr<<8 + and #0xff + beq mE2off + lda 0xee + ora #4 + sta 0xee +mE2off: +; sprite 3: x = (msb ? 0x100 : 0) | col; enable bit + ldy #18 + lda [0xe4],y ; col | msb<<8 + sta 0xf0 + and #0xff00 + beq mX3clr + lda 0xf0 + and #0x00ff + ora #0x0100 + bra mX3st +mX3clr: + lda 0xf0 + and #0x00ff +mX3st: + ldy #6 + sta [0xe8],y + ldy #21 + lda [0xe4],y ; enable | ptr<<8 + and #0xff + beq mE3off + lda 0xee + ora #8 + sta 0xee +mE3off: +; sprite 4: x = (msb ? 0x100 : 0) | col; enable bit + ldy #24 + lda [0xe4],y ; col | msb<<8 + sta 0xf0 + and #0xff00 + beq mX4clr + lda 0xf0 + and #0x00ff + ora #0x0100 + bra mX4st +mX4clr: + lda 0xf0 + and #0x00ff +mX4st: + ldy #8 + sta [0xe8],y + ldy #27 + lda [0xe4],y ; enable | ptr<<8 + and #0xff + beq mE4off + lda 0xee + ora #16 + sta 0xee +mE4off: +; sprite 5: x = (msb ? 0x100 : 0) | col; enable bit + ldy #30 + lda [0xe4],y ; col | msb<<8 + sta 0xf0 + and #0xff00 + beq mX5clr + lda 0xf0 + and #0x00ff + ora #0x0100 + bra mX5st +mX5clr: + lda 0xf0 + and #0x00ff +mX5st: + ldy #10 + sta [0xe8],y + ldy #33 + lda [0xe4],y ; enable | ptr<<8 + and #0xff + beq mE5off + lda 0xee + ora #32 + sta 0xee +mE5off: +; sprite 6: x = (msb ? 0x100 : 0) | col; enable bit + ldy #36 + lda [0xe4],y ; col | msb<<8 + sta 0xf0 + and #0xff00 + beq mX6clr + lda 0xf0 + and #0x00ff + ora #0x0100 + bra mX6st +mX6clr: + lda 0xf0 + and #0x00ff +mX6st: + ldy #12 + sta [0xe8],y + ldy #39 + lda [0xe4],y ; enable | ptr<<8 + and #0xff + beq mE6off + lda 0xee + ora #64 + sta 0xee +mE6off: +; sprite 7: x = (msb ? 0x100 : 0) | col; enable bit + ldy #42 + lda [0xe4],y ; col | msb<<8 + sta 0xf0 + and #0xff00 + beq mX7clr + lda 0xf0 + and #0x00ff + ora #0x0100 + bra mX7st +mX7clr: + lda 0xf0 + and #0x00ff +mX7st: + ldy #14 + sta [0xe8],y + ldy #45 + lda [0xe4],y ; enable | ptr<<8 + and #0xff + beq mE7off + lda 0xee + ora #128 + sta 0xee +mE7off: +; y[] block (16-bit stores; the junk high byte is repaired by the next store) + ldy #2 + lda [0xe4],y + ldy #16 + sta [0xe8],y + ldy #8 + lda [0xe4],y + ldy #17 + sta [0xe8],y + ldy #14 + lda [0xe4],y + ldy #18 + sta [0xe8],y + ldy #20 + lda [0xe4],y + ldy #19 + sta [0xe8],y + ldy #26 + lda [0xe4],y + ldy #20 + sta [0xe8],y + ldy #32 + lda [0xe4],y + ldy #21 + sta [0xe8],y + ldy #38 + lda [0xe4],y + ldy #22 + sta [0xe8],y + ldy #44 + lda [0xe4],y + ldy #23 + sta [0xe8],y +; ptr[] block (16-bit stores; the junk high byte is repaired by the next store) + ldy #4 + lda [0xe4],y + ldy #24 + sta [0xe8],y + ldy #10 + lda [0xe4],y + ldy #25 + sta [0xe8],y + ldy #16 + lda [0xe4],y + ldy #26 + sta [0xe8],y + ldy #22 + lda [0xe4],y + ldy #27 + sta [0xe8],y + ldy #28 + lda [0xe4],y + ldy #28 + sta [0xe8],y + ldy #34 + lda [0xe4],y + ldy #29 + sta [0xe8],y + ldy #40 + lda [0xe4],y + ldy #30 + sta [0xe8],y + ldy #46 + lda [0xe4],y + ldy #31 + sta [0xe8],y +; color[] block (16-bit stores; the junk high byte is repaired by the next store) + ldy #5 + lda [0xe4],y + ldy #32 + sta [0xe8],y + ldy #11 + lda [0xe4],y + ldy #33 + sta [0xe8],y + ldy #17 + lda [0xe4],y + ldy #34 + sta [0xe8],y + ldy #23 + lda [0xe4],y + ldy #35 + sta [0xe8],y + ldy #29 + lda [0xe4],y + ldy #36 + sta [0xe8],y + ldy #35 + lda [0xe4],y + ldy #37 + sta [0xe8],y + ldy #41 + lda [0xe4],y + ldy #38 + sta [0xe8],y + ldy #47 + lda [0xe4],y + ldy #39 + sta [0xe8],y +; enableMask | multiMask<<8 + lda 0xec + xba + and #0xff00 + ora 0xee + ldy #40 + sta [0xe8],y + plp + rtl diff --git a/examples/spacetaxi/stRender.c b/examples/spacetaxi/stRender.c index 65fce0e..9391631 100644 --- a/examples/spacetaxi/stRender.c +++ b/examples/spacetaxi/stRender.c @@ -40,11 +40,9 @@ // cab, exhaust and passenger sets plus the intro star) or a cel change // mid-play rebuilds and recompiles a sprite. Glyphs are direct-mapped // by character, so the count is a power of two. -#if defined(__W65816__) - #define ST_SPRITE_CACHE 48u -#else - #define ST_SPRITE_CACHE 72u -#endif +// Room for the baked cab/passenger/exhaust set (37) plus the largest +// level's hook cels (I: 30) at once. +#define ST_SPRITE_CACHE 80u #define ST_SPRITE_BACKUP_BYTES JOEY_SPRITE_BACKUP_BYTES(ST_SPRITE_TILES, ST_SPRITE_TILES) #define ST_SPRITE_PX (8 * ST_SPRITE_TILES) // Palette slots the title logo animation owns. The title screen draws @@ -71,7 +69,9 @@ typedef struct { bool used; bool pinned; // prewarmed moving cel: never evicted (its // compiled code is costly to re-emit on the 65816) + bool compiled; // jlSpriteCompile succeeded for this cel uint32_t lastUse; // for least-recently-used eviction + uint32_t retryAt; // cacheStamp at which a failed compile may retry } StSpriteCacheT; @@ -91,6 +91,9 @@ typedef struct { // Title logo (stRenderTitleLogo): all four flip-book frames in one // tile, animated by rewriting logoSlotMask[]'s palette slots. bool logoMode; + bool starMode; // level intro: cycling the starfield + uint8_t starPhase; // which ring the bright band is on + uint8_t starTick; // frames until the band steps outward bool logoTileOk; // the live glyph is one of the frames uint8_t logoSlotCount; uint8_t logoSlotMask[ST_LOGO_PAL_SLOTS]; // frames each slot is lit in @@ -100,8 +103,6 @@ typedef struct { jlTileT logoTile; // Cell range (inclusive) each character has been painted in since // the last full repaint: where a changed glyph's cells can be. - uint16_t glyphFirst[ST_CHARSET_CHARS]; - uint16_t glyphLast[ST_CHARSET_CHARS]; // Column band of the cells about to be repainted, per character row // (min > max = the row is clean), the same shape as the library's // own per-row dirty bands. @@ -111,7 +112,10 @@ typedef struct { StSpriteCacheT cache[ST_SPRITE_CACHE]; StSpriteCacheT *lastHit[ST_HW_SPRITES]; // the entry each hardware sprite used last jlSpriteBackupT backup[ST_HW_SPRITES]; - uint8_t backupMem[ST_HW_SPRITES][ST_SPRITE_BACKUP_BYTES] __attribute__((aligned(2))); + // ST_HW_SPRITES backups of ST_SPRITE_BACKUP_BYTES each, from jlAlloc: + // 4.5 KB that as BSS would not fit the IIgs entry bank (text, rodata, + // BSS and the C heap share its 64 KB). + uint8_t *backupMem; StDrawnT slot[ST_HW_SPRITES]; uint8_t lastPresentFrame; } StRenderStateT; @@ -132,9 +136,26 @@ static const uint16_t kC64Palette[16] = { // four of these are used; the rest are headroom if the frames change. static const uint8_t kLogoPalSlot[ST_LOGO_PAL_SLOTS] = { 4u, 8u, 9u, 10u, 13u, 14u, 15u }; +// Level-intro warp: how a star ring looks at each distance BEHIND the +// bright band, as C64 colour indices (white, light grey, grey, dark +// grey, then black). A ring this far behind the band or further is +// unlit, so a lit head with a short fading tail sweeps outward and the +// rest of the field stays dark. +static const uint8_t kStarRamp[] = { 1u, 15u, 12u, 11u }; +#define ST_STAR_RAMP_LEN (sizeof(kStarRamp) / sizeof(kStarRamp[0])) +// Frames per ring step. The intro runs at the game's 30 Hz tick, so a +// step every other frame sweeps the eight rings in about half a second. +#define ST_STAR_STEP_FRAMES 2u + static void buildSpriteCel(const uint8_t *bm, uint8_t multi, uint8_t color, uint8_t mc0, uint8_t mc1); static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx); +static void cacheCompile(StSpriteCacheT *e); +static StSpriteCacheT *cacheFreeSlot(void); +static void levelCelsEvict(void); +static const uint8_t *levelSpriteBitmap(const StLevelT *level, uint8_t ptr); +static void loadingBar(jlSurfaceT *stage, uint8_t done, uint8_t total); +static uint8_t cellCol(uint16_t cell, uint8_t row); static uint8_t cellRow(uint16_t cell); static void collectGlyphs(StSimT *sim); static void dirtyBands(const StSimT *sim); @@ -145,6 +166,7 @@ static bool loadPrecompiledCels(void); static bool logoBuildTile(const StSimT *sim); static uint8_t logoFrameIndex(const StSimT *sim); static void logoPaletteApply(const StSimT *sim, uint8_t frame); +static void starPaletteApply(void); static void paintCells(jlSurfaceT *stage, StSimT *sim); static void pasteCell(jlSurfaceT *stage, const StSimT *sim, uint16_t cell, uint8_t bx, uint8_t by); static bool spriteOnScreen(int16_t px, int16_t py); @@ -171,12 +193,27 @@ static void buildSpriteCel(const uint8_t *bm, uint8_t multi, uint8_t color, uint } +// Compile a cached cel if it is not already, backing off after a failure +// (arena full) so a cel that cannot fit is not re-staged every frame -- +// jlSpriteCompile's failure path runs the whole emitter twice. +#define ST_COMPILE_RETRY_STAMPS 4096u // ~8 frames' worth of cachedSprite calls per frame -> ~25 s +static void cacheCompile(StSpriteCacheT *e) { + if (e->compiled || gRender.cacheStamp < e->retryAt) { + return; + } + e->compiled = jlSpriteCompile(e->sprite); + if (!e->compiled) { + e->retryAt = gRender.cacheStamp + ST_COMPILE_RETRY_STAMPS; + } +} + + // The JoeyLib sprite for hardware sprite `idx` of the current frame, // 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 = drawColor(sim, idx); - uint8_t multi = (uint8_t)((sim->frame.multiMask >> idx) & 1u); + uint8_t multi = (uint8_t)((sim->frame.multiMask & kStBit[idx]) != 0u ? 1u : 0u); uint8_t mc0 = multi ? sim->spriteMc0 : 0u; uint8_t mc1 = multi ? sim->spriteMc1 : 0u; uint8_t level = (ptr < ST_SPRITE_PTR_FIRST && sim->level != 0) ? sim->level->levelIndex : 0xFFu; @@ -213,20 +250,36 @@ static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx) { // so fail into the interpreter path. return 0; } + // No bitmap for this pointer (hooks enable a sprite a tick before + // they aim it, leaving it at block 0): nothing to draw, and no cel + // to build and evict for. + if (stSimSpriteBitmap(sim, ptr) == 0) { + return 0; + } if (slot->used) { jlSpriteDestroy(slot->sprite); - slot->sprite = 0; - slot->used = false; + slot->sprite = 0; + slot->used = false; + slot->compiled = false; } buildSpriteCel(stSimSpriteBitmap(sim, ptr), multi, color, mc0, mc1); slot->sprite = jlSpriteCreateFromSurface(gRender.scratch, 0, 0, ST_SPRITE_TILES, ST_SPRITE_TILES); if (slot->sprite == 0) { return 0; } - // 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); + // Nothing compiles mid-ride: a compile is ~0.5 s on the 65816, a + // visible hitch. The cab, exhaust and passenger cels come baked + // (stRenderPrewarm), a level's hook cels come baked or are compiled + // at level entry (stRenderLevelCels), and a cel that still gets + // built here draws interpreted -- and is logged, because it means + // the level's cel list (stLevelCels.c) missed one. + slot->compiled = false; + slot->retryAt = 0u; + if (idx <= 2u && gPrewarming) { + cacheCompile(slot); + } + if (!gPrewarming) { + ST_LOG("spacetaxi: ! cel $%02X colour %u (level %u) built mid-ride -- add it to stLevelCelList", ptr, color, level); } slot->lastUse = gRender.cacheStamp; slot->ptr = ptr; @@ -257,6 +310,19 @@ static const uint8_t kCellRow[125] = { }; +// Column of a cell whose row is known: cell - row * 40 through a table, +// because the two-shift form of the multiply came back as a __mulhi3 +// library call on the 65816. +static uint8_t cellCol(uint16_t cell, uint8_t row) { + static const uint16_t kRowBase[ST_SCREEN_ROWS] = { + 0, 40, 80, 120, 160, 200, 240, 280, 320, 360, 400, 440, 480, + 520, 560, 600, 640, 680, 720, 760, 800, 840, 880, 920, 960 + }; + return (uint8_t)(cell - kRowBase[row]); +} + + + static uint8_t cellRow(uint16_t cell) { return kCellRow[cell >> 3]; } @@ -271,7 +337,7 @@ static uint8_t cellRow(uint16_t cell) { static void collectGlyphs(StSimT *sim) { uint8_t k; uint16_t cell; - uint16_t last; + uint16_t next; if (sim->charDirtyAll) { sim->charDirtyAll = false; @@ -283,18 +349,33 @@ static void collectGlyphs(StSimT *sim) { if (sim->dirtyAll) { continue; } - last = gRender.glyphLast[ch]; - for (cell = gRender.glyphFirst[ch]; cell <= last; cell++) { - if (sim->screen[cell] != ch || sim->cellDirty[cell] != 0u) { - continue; - } - sim->cellDirty[cell] = 1u; - if (sim->dirtyCount < (uint16_t)(sizeof(sim->dirtyList) / sizeof(sim->dirtyList[0]))) { - sim->dirtyList[sim->dirtyCount++] = cell; - } else { - sim->dirtyAll = true; - break; + // Exactly the cells showing `ch`, straight off the reverse index + // the sim maintains (StSimT in stSim.h). No scan, no screen[] + // test: a cell is in this list if and only if it shows `ch`. + // + // This replaced a walk of a per-glyph [first, last] span. That + // span only ever widened -- pasteCell pushed it out and nothing + // pulled it back -- so a glyph that once appeared in transient + // text kept a screen-wide span for the rest of the level and was + // rescanned on every animation frame, at 24-bit far-pointer cost + // per cell. It measured 22% of the whole program on level H. + // + // `next` is read before the body because the overflow arm below + // stops the walk, and because keeping the read up here makes the + // loop independent of anything the body does to the cell. + cell = sim->glyphHead[ch]; + while (cell != ST_GLYPH_CELL_NONE) { + next = sim->cellNext[cell]; + if (sim->cellDirty[cell] == 0u) { + sim->cellDirty[cell] = 1u; + if (sim->dirtyCount < (uint16_t)(sizeof(sim->dirtyList) / sizeof(sim->dirtyList[0]))) { + sim->dirtyList[sim->dirtyCount++] = cell; + } else { + sim->dirtyAll = true; + break; + } } + cell = next; } } sim->charDirtyCount = 0u; @@ -331,7 +412,7 @@ static void dirtyBands(const StSimT *sim) { for (k = 0u; k < sim->dirtyCount; k++) { uint16_t cell = sim->dirtyList[k]; uint8_t r = cellRow(cell); - uint8_t col = (uint8_t)(cell - ((uint16_t)r << 5) - ((uint16_t)r << 3)); + uint8_t col = cellCol(cell, r); if (col < gRender.dirtyColMin[r]) { gRender.dirtyColMin[r] = col; } @@ -422,7 +503,7 @@ static bool logoBuildTile(const StSimT *sim) { uint8_t f; uint8_t slot; for (f = 0u; f < ST_LOGO_FRAMES; f++) { - if (((sim->charset[ST_LOGO_FRAME_FIRST + f][row] >> (7u - col)) & 1u) != 0u) { + if (((stSimGlyphRow(sim, (uint8_t)(ST_LOGO_FRAME_FIRST + f), row) >> (7u - col)) & 1u) != 0u) { mask |= (uint8_t)(1u << f); } } @@ -454,7 +535,8 @@ static uint8_t logoFrameIndex(const StSimT *sim) { uint8_t f; for (f = 0u; f < ST_LOGO_FRAMES; f++) { - if (memcmp(sim->charset[ST_LOGO_CHAR], sim->charset[ST_LOGO_FRAME_FIRST + f], 8u) == 0) { + if (memcmp(stSimGlyph(sim, ST_LOGO_CHAR), + stSimGlyph(sim, (uint8_t)(ST_LOGO_FRAME_FIRST + f)), 8u) == 0) { return f; } } @@ -494,8 +576,6 @@ static void paintCells(jlSurfaceT *stage, StSimT *sim) { uint8_t by; if (sim->dirtyAll) { - memset(gRender.glyphFirst, 0xFF, sizeof(gRender.glyphFirst)); - memset(gRender.glyphLast, 0, sizeof(gRender.glyphLast)); cell = 0u; for (by = 0u; by < ST_SCREEN_ROWS; by++) { for (bx = 0u; bx < ST_SCREEN_COLS; bx++) { @@ -511,7 +591,7 @@ static void paintCells(jlSurfaceT *stage, StSimT *sim) { for (k = 0u; k < sim->dirtyCount; k++) { cell = sim->dirtyList[k]; by = cellRow(cell); - bx = (uint8_t)(cell - ((uint16_t)by << 5) - ((uint16_t)by << 3)); + bx = cellCol(cell, by); pasteCell(stage, sim, cell, bx, by); sim->cellDirty[cell] = 0u; } @@ -525,12 +605,6 @@ static void paintCells(jlSurfaceT *stage, StSimT *sim) { static void pasteCell(jlSurfaceT *stage, const StSimT *sim, uint16_t cell, uint8_t bx, uint8_t by) { uint8_t chr = sim->screen[cell]; - if (cell < gRender.glyphFirst[chr]) { - gRender.glyphFirst[chr] = cell; - } - if (cell > gRender.glyphLast[chr]) { - gRender.glyphLast[chr] = cell; - } // On the title the logo's cells carry all four flip-book frames at // once; the palette decides which one shows, so they are pasted as // they are and never touched again. @@ -538,7 +612,30 @@ static void pasteCell(jlSurfaceT *stage, const StSimT *sim, uint16_t cell, uint8 jlTilePaste(stage, bx, by, &gRender.logoTile); return; } - jlTilePasteGlyph(stage, bx, by, sim->charset[chr], sim->color[cell], sim->bgColor); + jlTilePasteGlyph(stage, bx, by, stSimGlyph(sim, chr), sim->color[cell], sim->bgColor); +} + + +// Light the starfield's ring colours for the current phase: the ring +// the band is on takes kStarRamp[0], the ones just inside it the rest +// of the ramp, and everything else the background. One palette write +// per step and not a single pixel moves -- that is the whole point of +// ST_STAR_RING_FIRST (stSim.h). +static void starPaletteApply(void) { + uint16_t pal[SURFACE_COLORS_PER_PALETTE]; + uint16_t color; + uint8_t behind; + uint8_t ring; + + jlPaletteGet(gRender.stage, 0u, pal); + for (ring = 0u; ring < ST_STAR_RING_COUNT; ring++) { + // How far this ring sits behind the band, wrapping so the band + // re-enters at the centre as it leaves the edge. + behind = (uint8_t)((gRender.starPhase - ring) & (ST_STAR_RING_COUNT - 1u)); + color = (behind < ST_STAR_RAMP_LEN) ? kC64Palette[kStarRamp[behind]] : kC64Palette[0]; + pal[ST_STAR_RING_FIRST + ring] = color; + } + jlPaletteSet(gRender.stage, 0u, pal); } @@ -598,15 +695,26 @@ void stRenderFrame(jlSurfaceT *stage, StSimT *sim) { } } } + if (gRender.starMode) { + // The warp is the only thing animating here, and it animates in + // the palette: no cells change, no sprite moves for it, so the + // present that ends this frame copies no pixels at all. + gRender.starTick++; + if (gRender.starTick >= ST_STAR_STEP_FRAMES) { + gRender.starTick = 0u; + gRender.starPhase = (uint8_t)((gRender.starPhase + 1u) & (ST_STAR_RING_COUNT - 1u)); + starPaletteApply(); + } + } collectGlyphs(sim); dirtyBands(sim); 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); + uint8_t multi = (uint8_t)((sim->frame.multiMask & kStBit[idx]) != 0u ? 1u : 0u); int16_t px = (int16_t)((int16_t)sim->frame.x[idx] - ST_SPRITE_X_ORIGIN); int16_t py = (int16_t)((int16_t)sim->frame.y[idx] - ST_SPRITE_Y_ORIGIN); - bool want = (sim->frame.enableMask & (uint8_t)(1u << idx)) != 0u && spriteOnScreen(px, py); + bool want = (sim->frame.enableMask & kStBit[idx]) != 0u && spriteOnScreen(px, py); bool same = (want == d->drawn); if (same && want) { same = (d->x == sim->frame.x[idx] && d->y == sim->frame.y[idx] && d->ptr == sim->frame.ptr[idx] && d->color == drawColor(sim, idx) && d->multi == multi); @@ -633,7 +741,7 @@ void stRenderFrame(jlSurfaceT *stage, StSimT *sim) { jlSpriteT *sp; int16_t px; int16_t py; - if ((sim->frame.enableMask & (uint8_t)(1u << idx)) == 0u) { + if ((sim->frame.enableMask & kStBit[idx]) == 0u) { continue; } px = (int16_t)((int16_t)sim->frame.x[idx] - ST_SPRITE_X_ORIGIN); @@ -651,7 +759,7 @@ void stRenderFrame(jlSurfaceT *stage, StSimT *sim) { d->y = sim->frame.y[idx]; d->ptr = sim->frame.ptr[idx]; d->color = drawColor(sim, idx); - d->multi = (uint8_t)((sim->frame.multiMask >> idx) & 1u); + d->multi = (uint8_t)((sim->frame.multiMask & kStBit[idx]) != 0u ? 1u : 0u); } { // Sync to the retrace unless the frame already spans more than @@ -667,13 +775,20 @@ void stRenderFrame(jlSurfaceT *stage, StSimT *sim) { void stRenderInit(jlSurfaceT *stage) { - uint8_t k; + uint8_t *backupMem = gRender.backupMem; + uint8_t k; memset(&gRender, 0, sizeof(gRender)); - memset(gRender.glyphFirst, 0xFF, sizeof(gRender.glyphFirst)); - gRender.stage = stage; + if (backupMem == 0) { + backupMem = (uint8_t *)jlAlloc((uint32_t)ST_HW_SPRITES * ST_SPRITE_BACKUP_BYTES); + if (backupMem == 0) { + ST_LOG("stRenderInit: no memory for sprite backups"); + } + } + gRender.backupMem = backupMem; + gRender.stage = stage; for (k = 0u; k < ST_HW_SPRITES; k++) { - gRender.backup[k].bytes = gRender.backupMem[k]; + gRender.backup[k].bytes = (backupMem == 0) ? 0 : backupMem + (uint16_t)k * ST_SPRITE_BACKUP_BYTES; } jlPaletteSet(stage, 0u, kC64Palette); jlScbSetRange(stage, 0u, (uint16_t)(SURFACE_HEIGHT - 1u), 0u); @@ -695,10 +810,145 @@ void stRenderInit(jlSurfaceT *stage) { // cache so no cel is JIT-compiled at boot (each 65816 compile costs // ~0.4 s). Every kStCels entry becomes a pinned cache slot keyed exactly // as the play lookups: system ptr (level 0xFF), the level-wide colours. +// The first unused cache slot, or NULL when the cache is full. +static StSpriteCacheT *cacheFreeSlot(void) { + uint8_t k; + + for (k = 0u; k < ST_SPRITE_CACHE; k++) { + if (!gRender.cache[k].used) { + return &gRender.cache[k]; + } + } + return 0; +} + + +// Drop every cel that belongs to a level (the baked set is level 0xFF and +// stays), freeing its arena code, and forget any hardware sprite's last +// hit on it. +static void levelCelsEvict(void) { + uint8_t k; + + for (k = 0u; k < ST_SPRITE_CACHE; k++) { + StSpriteCacheT *e = &gRender.cache[k]; + uint8_t h; + if (!e->used || e->level == 0xFFu) { + continue; + } + for (h = 0u; h < ST_HW_SPRITES; h++) { + if (gRender.lastHit[h] == e) { + gRender.lastHit[h] = 0; + } + } + jlSpriteDestroy(e->sprite); + e->sprite = 0; + e->used = false; + e->pinned = false; + e->compiled = false; + } +} + + +// A level's own sprite block, or NULL when the level does not ship it. +static const uint8_t *levelSpriteBitmap(const StLevelT *level, uint8_t ptr) { + uint8_t k; + + for (k = 0u; k < level->spriteCount; k++) { + if (level->sprites[k].ptr == ptr) { + return level->sprites[k].bitmap; + } + } + return 0; +} + + +static void loadingBar(jlSurfaceT *stage, uint8_t done, uint8_t total) { + jlFillRect(stage, 60, 110, (uint16_t)((uint16_t)done * 200u / total), 6u, 1u); + jlStagePresent(); +} + + +// Ready a level's hook cels BEFORE the ride: the previous level's cels +// go, then the level's list (stLevelCelList) is filled from its baked +// bank (sprites/levelNN.spc) or, when there is none, built and compiled +// here behind the LOADING bar. Either way nothing compiles once the +// level is running. Keyed exactly as cachedSprite keys them, so every +// lookup during the ride is a hit. +void stRenderLevelCels(jlSurfaceT *stage, const StLevelT *level) { + static StCelDefT list[ST_LEVEL_CELS_MAX]; + jlSpriteT *cels[ST_LEVEL_CELS_MAX]; + char path[24]; + uint8_t n; + uint8_t got; + uint8_t k; + + levelCelsEvict(); + if (level == 0) { + return; + } + n = stLevelCelList(level, list, ST_LEVEL_CELS_MAX); + if (n == 0u) { + return; + } + memcpy(path, "sprites/level", 13u); + path[13] = (char)('0' + (level->levelIndex + 1u) / 10u); + path[14] = (char)('0' + (level->levelIndex + 1u) % 10u); + memcpy(path + 15, ".spc", 5u); + got = (uint8_t)jlSpriteBankLoadPrecompiled(path, cels, n, 0); + if (got == n) { + ST_LOG("spacetaxi: level %u cels from %s (%u)", level->levelIndex, path, n); + } else { + for (k = 0u; k < got; k++) { + jlSpriteDestroy(cels[k]); + } + ST_LOG("spacetaxi: level %u: no usable %s -- compiling %u cels at entry", level->levelIndex, path, n); + stRenderLoading(stage); + } + gPrewarming = true; + for (k = 0u; k < n; k++) { + StSpriteCacheT *slot = cacheFreeSlot(); + if (slot == 0) { + ST_LOG("spacetaxi: ! sprite cache full at level cel %u", k); + break; + } + if (got == n) { + slot->sprite = cels[k]; + slot->compiled = true; + } else { + const uint8_t *bm = levelSpriteBitmap(level, list[k].ptr); + if (bm == 0) { + continue; + } + buildSpriteCel(bm, list[k].multi, list[k].color, list[k].mc0, list[k].mc1); + slot->sprite = jlSpriteCreateFromSurface(gRender.scratch, 0, 0, ST_SPRITE_TILES, ST_SPRITE_TILES); + if (slot->sprite == 0) { + continue; + } + slot->compiled = false; + slot->retryAt = 0u; + cacheCompile(slot); + loadingBar(stage, (uint8_t)(k + 1u), n); + } + slot->ptr = list[k].ptr; + slot->color = list[k].color; + slot->multi = list[k].multi; + slot->mc0 = list[k].mc0; + slot->mc1 = list[k].mc1; + slot->level = level->levelIndex; + slot->used = true; + slot->pinned = true; + slot->lastUse = gRender.cacheStamp; + } + gPrewarming = false; + if (got != n) { + jlFillRect(stage, 60, 110, 200u, 6u, 0u); + } +} + + // Returns false if the .spc is missing or built for another target/shift -// count (other ports, or a stale bake), so the caller falls back to the -// runtime JIT prewarm below. Its cross-platform contract makes this a -// no-op everywhere but the IIgs, where the bake is wired. +// count (a stale bake), so the caller falls back to the runtime JIT +// prewarm below. Every port stages its own bake (make/.mk). static bool loadPrecompiledCels(void) { jlSpriteT *cels[64]; uint16_t n; @@ -761,8 +1011,10 @@ void stRenderPrewarm(jlSurfaceT *stage, StSimT *sim) { // Precompiled bank: instant, no per-cel JIT. Falls through to the // runtime build below on any port without a matching .spc. if (loadPrecompiledCels()) { + ST_LOG("spacetaxi: cels from sprites/staxicels.spc"); return; } + ST_LOG("spacetaxi: ! no usable sprites/staxicels.spc -- JIT prewarm"); saved = sim->frame; gPrewarming = true; sim->frame.multiMask = 0x07u; @@ -773,24 +1025,21 @@ void stRenderPrewarm(jlSurfaceT *stage, StSimT *sim) { sim->frame.color[0] = 0x06u; (void)cachedSprite(sim, 0u); done++; - jlFillRect(stage, 60, 110, (uint16_t)((uint16_t)done * 200u / total), 6u, 1u); - jlStagePresent(); + loadingBar(stage, done, total); } 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(); + loadingBar(stage, done, total); } 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(); + loadingBar(stage, done, total); } // The intro star is a hires sprite in colour 7. Build it as a moving // (idx <= 2) cel so it compiles: the level-name intro draws seven of @@ -809,16 +1058,17 @@ void stRenderPrewarm(jlSurfaceT *stage, StSimT *sim) { void stRenderSceneChanged(StSimT *sim) { uint8_t k; - if (gRender.logoMode) { - // The logo's reserved slots go back to being C64 colours. + if (gRender.logoMode || gRender.starMode) { + // The borrowed slots go back to being C64 colours. gRender.logoMode = false; gRender.logoTileOk = false; + gRender.starMode = false; jlPaletteSet(gRender.stage, 0u, kC64Palette); } for (k = 0u; k < ST_HW_SPRITES; k++) { gRender.slot[k].drawn = false; } - stSimDirtyAll(sim); + stSimDirtyCells(sim); sim->charDirtyAll = true; sim->charDirtyCount = 0u; } @@ -838,7 +1088,19 @@ void stRenderTitleLogo(StSimT *sim) { gRender.logoMode = true; gRender.logoTileOk = (frame < ST_LOGO_FRAMES); logoPaletteApply(sim, gRender.logoTileOk ? frame : 0u); - stSimDirtyAll(sim); + stSimDirtyCells(sim); +} + + +// Turn on the level intro's palette-cycled starfield. Call it after +// stRenderSceneChanged (which turns it back off) on entry to the level +// intro. The cells were painted by stIntroEnter; from here the warp is +// one palette write every ST_STAR_STEP_FRAMES frames. +void stRenderIntroStars(void) { + gRender.starMode = true; + gRender.starPhase = 0u; + gRender.starTick = 0u; + starPaletteApply(); } @@ -848,4 +1110,6 @@ void stRenderShutdown(void) { jlSurfaceDestroy(gRender.scratch); gRender.scratch = 0; } + jlFree(gRender.backupMem); + gRender.backupMem = 0; } diff --git a/examples/spacetaxi/stSim.c b/examples/spacetaxi/stSim.c index af93ba1..3a6069a 100644 --- a/examples/spacetaxi/stSim.c +++ b/examples/spacetaxi/stSim.c @@ -11,24 +11,34 @@ #include "stSim.h" +// Jingle selection ($6935 / $70D4). Song numbers index kStSongs. +#define ST_JINGLE_SONG_BONUS 4u // level-25 start and the bonus cab +#define ST_JINGLE_LEVEL_25 0x19u // $7215 compared against #$19 + // 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 +// $6C38 -- the 11-character blank that clears the message row. +static const uint8_t kTextBlank[] = " "; +// $43B1 -- the fare meter's blank template: three leading blanks, then +// "0.00" in the inverse-video digit glyphs. +static const uint8_t kHudTemplate[ST_NUMBER_CHARS] = { 0x66, 0x66, 0x66, 0x74, 0x77, 0x74, 0x74 }; // 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 }; +const uint8_t kStBit[ST_HW_SPRITES] = { 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80 }; static void applyVelocityX(StSimT *sim); static void applyVelocityY(StSimT *sim); -static uint32_t backgroundRowMask(const StSimT *sim, int16_t px, int16_t py); +static bool backgroundRowHits(const StSimT *sim, int16_t firstCol, int16_t py, const uint8_t *w); +static bool cabRowsTouchBackground(StSimT *sim, int16_t firstCol, int16_t py, uint8_t phase); +static const uint8_t *cabWindows(StSimT *sim, uint8_t s); static int16_t cellOfPixel(int16_t px); -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 bool coveringCellsSolid(const StSimT *sim, int16_t firstCol, int16_t topRow, int16_t botRow); static void crashStart(StSimT *sim); static void drawCabIcons(StSimT *sim); static void drawScreensCount(StSimT *sim); @@ -37,19 +47,20 @@ 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 markCell(StSimT *sim, uint16_t cell); 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 rowWindow(const uint8_t *row, uint8_t s, uint8_t *w); 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(StSimT *sim, uint8_t idx); +static bool spriteRowsTouchBackground(const StSimT *sim, uint8_t idx, int16_t firstCol, int16_t py, uint8_t phase); +static bool spriteTouchesBackground(StSimT *sim, uint8_t idx); static void spritesOffReset(StSimT *sim); static void taxiSpawnInit(StSimT *sim); static void taxiSpriteCelSelect(StSimT *sim); @@ -87,30 +98,73 @@ static void applyVelocityY(StSimT *sim) { } -// 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; +// Whether a 4-byte row window (rowWindow: byte j covers character +// column firstCol + j) meets character graphics on screen pixel row py. +static bool backgroundRowHits(const StSimT *sim, int16_t firstCol, int16_t py, const uint8_t *w) { + uint8_t glyphRow; + uint8_t j; if (py < 0 || py >= (int16_t)(ST_SCREEN_ROWS * 8u)) { - return 0u; + return false; } - // Floor division so a negative px lands on the column to its left. - firstCol = cellOfPixel(px); - shift = (uint8_t)(px - (int16_t)(firstCol << 3)); - 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]; + glyphRow = (uint8_t)(py & 7); + for (j = 0u; j < 4u; j++) { + int16_t col = (int16_t)(firstCol + (int16_t)j); + if (w[j] != 0u && col >= 0 && col < (int16_t)ST_SCREEN_COLS) { + if ((stSimGlyphRow(sim, sim->screen[ST_CELL((uint16_t)py >> 3, (uint16_t)col)], glyphRow) & w[j]) != 0u) { + return true; + } } - window = (window << 8) | byte; } - return (window >> (8u - shift)) & 0xFFFFFFu; + return false; +} + + +// The cab's 21 cached row windows at grid phase `phase` against the +// background rows under it. +static bool cabRowsTouchBackground(StSimT *sim, int16_t firstCol, int16_t py, uint8_t phase) { + uint8_t first = sim->collRowFirst[0]; + uint8_t left = (uint8_t)(sim->collRowLast[0] + 1u - first); + const uint8_t *win; + + if (first == ST_SPRITE_H) { + return false; + } + win = cabWindows(sim, phase) + (uint16_t)first * 4u; + py = (int16_t)(py + first); + // Counted down to zero: `dec a / bne` needs no compare. (It also + // dodged the llvm816 bug, fixed 2026-09-22, where the up-counting form + // compiled to `cmp #end / inc a / bne` with the inc trampling the + // compare's Z, so the loop ran past row 20 into the next phase's + // windows -- the false landing crash in the IIgs "Puzzles" demo.) + do { + if (backgroundRowHits(sim, firstCol, py, win)) { + return true; + } + win += 4; + py++; + left--; + } while (left != 0u); + return false; +} + + +// The cab's 21 row windows at grid phase s, 4 bytes per row, built on +// first use after the cab's mask changed and cached until it changes +// again. The cab is one side of every collision test, so no test +// shifts a row (a variable 32-bit shift is a library bit loop on the +// 65816 and this was the collision test's whole cost). +static const uint8_t *cabWindows(StSimT *sim, uint8_t s) { + uint8_t (*win)[4] = sim->cabWin[s]; + uint8_t r; + + if ((sim->cabWinValid & (uint8_t)(1u << s)) == 0u) { + for (r = 0u; r < ST_SPRITE_H; r++) { + rowWindow(sim->collMask[0][r], s, win[r]); + } + sim->cabWinValid |= (uint8_t)(1u << s); + } + return &win[0][0]; } @@ -125,19 +179,6 @@ static int16_t cellOfPixel(int16_t px) { } -// $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)); - } - stSimMarkChar(sim, ST_CHAR_TRANSPORTER); -} - - // $6966 -- latch the frame's collisions and run the crash phases. static void collisionDispatch(StSimT *sim) { computeCollisions(sim); @@ -181,7 +222,6 @@ static void collisionDispatch(StSimT *sim) { static void collisionPhase1(StSimT *sim) { uint8_t jitter; uint16_t sum; - uint16_t colFrac; if ((sim->hitDispatchResult & 0x80u) == 0u) { sim->phaseTimer--; @@ -193,11 +233,8 @@ static void collisionPhase1(StSimT *sim) { 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); + jitter = (uint8_t)(stSimRng(sim, 5u) - 3u); + stSimSpriteAddX(sim, 0u, jitter); edgeReflect(sim); applyVelocityX(sim); // vy += 40 (low byte with carry into the high byte), row += vy hi. @@ -226,7 +263,7 @@ static void collisionPhase2(StSimT *sim) { sim->phaseReload++; sim->phaseTimer = sim->phaseReload; sim->spr[0].ptr++; - if (sim->spr[0].ptr != 0xD1u) { + if (sim->spr[0].ptr != ST_SPRITE_WRECK_LAST) { return; } sim->spr[0].enable = 0u; @@ -247,11 +284,11 @@ static StTickResultE collisionPhase3(StSimT *sim) { return ST_TICK_CRASH_DONE; } if (sim->stage == ST_STAGE_RIDING) { - hudInit(sim); + stSimHudInit(sim); if (sim->fareSlotCount != 0u) { sim->spriteSlots[sim->activeDyingSlot] = 0u; sim->fareSlotCount--; - if (sim->activeSpriteIdx == 0x0Bu) { + if (sim->activeSpriteIdx == ST_FARE_DEST_UP) { uint8_t k; for (k = 0u; k < 4u; k++) { setCell(sim, (uint16_t)(ST_CELL_TRANSPORTER + k), ST_CHAR_TRANSPORTER); @@ -263,7 +300,7 @@ static StTickResultE collisionPhase3(StSimT *sim) { } sim->activeSpriteIdx = 0u; sim->stage = ST_STAGE_IDLE; - stSimDrawText(sim, 14u, 24u, kTextBlank, 1u); + stSimClearMessage(sim); } // $6BBA p = sim->player; @@ -280,69 +317,56 @@ static StTickResultE collisionPhase3(StSimT *sim) { // The C64 reads two VIC collision registers each frame: $D01F (a sprite // touched the background) and $D01E (two sprites touched). Only the cab // (sprite 0) can drive a gameplay outcome from them: the crash dispatch -// returns unless the cab or the background is touched, and every hook -// reads a bit set by cab contact. So the full all-pairs register is not -// reproduced -- only sprite 0 versus the background and versus the other -// sprites, and the exhaust flame (sprite 2, the cab's own, always -// overlapping it) is skipped. Non-cab pairs (hazard vs hazard, passenger -// vs hazard with the cab clear) never change the dispatch outcome, so -// their bits are left unset. Proven against all four demo traces: the -// observable state (positions, crash phase, score) is byte-identical. +// returns unless the cab or the background is touched, and the hooks +// read a bit set by cab contact -- except levels J and P, which watch +// their own hazards hit the scenery and opt those sprites in through +// bgCollSprites. So the full all-pairs register is not reproduced -- +// only sprite 0 versus the background and versus the other sprites, +// plus the opted-in sprites versus the background, and the exhaust +// flame (sprite 2, the cab's own, always overlapping it) is skipped. +// Non-cab pairs (hazard vs hazard, passenger vs hazard with the cab +// clear) never change the dispatch outcome, so their bits are left +// unset. Proven against all four demo traces: the observable state +// (positions, crash phase, score) is byte-identical. static void computeCollisions(StSimT *sim) { - uint32_t (*mask)[ST_SPRITE_H] = sim->collMask; - uint8_t ss = 0u; - uint8_t bg = 0u; - int16_t cabX; - int16_t cabY; - uint8_t b; - uint8_t r; + uint8_t ss = 0u; + uint8_t bg = 0u; + uint8_t b; + uint8_t r; + for (b = 1u; b < ST_HW_SPRITES; b++) { + if ((sim->bgCollSprites & kStBit[b]) == 0u || (sim->frame.enableMask & kStBit[b]) == 0u) { + continue; + } + spriteMasks(sim, b); + if (spriteTouchesBackground(sim, b)) { + bg |= kStBit[b]; + } + } if ((sim->frame.enableMask & 1u) == 0u) { sim->spriteSpriteColl = 0u; - sim->spriteBgColl = 0u; + sim->spriteBgColl = bg; return; } spriteMasks(sim, 0u); - cabX = (int16_t)((int16_t)sim->frame.x[0] - ST_SPRITE_X_ORIGIN); - cabY = (int16_t)((int16_t)sim->frame.y[0] - ST_SPRITE_Y_ORIGIN); - // The background can only touch the cab where a covering character - // cell is non-blank. In open flight every covering cell is the space - // glyph (all-zero pixels), so the 21-row pixel test is skipped there; - // any non-space cell falls through to the exact test, so the result - // is unchanged (space is the only zero-pixel glyph the test can hit). - { - int16_t firstCol = cellOfPixel(cabX); - int16_t topRow = (cabY < 0) ? 0 : cellOfPixel(cabY); - int16_t botRow = cellOfPixel((int16_t)(cabY + (int16_t)ST_SPRITE_H - 1)); - bool solid = false; - int16_t rr; - int16_t cc; - - if (botRow >= (int16_t)ST_SCREEN_ROWS) { - botRow = (int16_t)(ST_SCREEN_ROWS - 1u); - } - for (rr = topRow; rr <= botRow && !solid; rr++) { - for (cc = firstCol; cc <= (int16_t)(firstCol + 3); cc++) { - if (cc >= 0 && cc < (int16_t)ST_SCREEN_COLS && sim->screen[ST_CELL((uint16_t)rr, (uint16_t)cc)] != ST_CHAR_SPACE) { - solid = true; - break; - } - } - } - if (solid) { - for (r = 0u; r < ST_SPRITE_H; r++) { - if (mask[0][r] != 0u && (mask[0][r] & backgroundRowMask(sim, cabX, (int16_t)(cabY + r))) != 0u) { - bg = 1u; - break; - } - } - } + if (spriteTouchesBackground(sim, 0u)) { + bg = 1u; } for (b = 1u; b < ST_HW_SPRITES; b++) { - int16_t dx; - int16_t dy; + int16_t dx; + int16_t dy; + uint8_t s; + int16_t q; + uint8_t kLo; + uint8_t kHi; + uint8_t rLo; + uint8_t rHi; + int16_t lo; + int16_t hi; + const uint8_t *win; + const uint8_t *bRow; // Skip the exhaust flame (sprite 2) and any disabled sprite. - if (b == 2u || (sim->frame.enableMask & (uint8_t)(1u << b)) == 0u) { + if (b == 2u || (sim->frame.enableMask & kStBit[b]) == 0u) { continue; } dx = (int16_t)((int16_t)sim->frame.x[b] - (int16_t)sim->frame.x[0]); @@ -351,20 +375,44 @@ static void computeCollisions(StSimT *sim) { continue; } spriteMasks(sim, b); - for (r = 0u; r < ST_SPRITE_H; r++) { - int16_t rb = (int16_t)((int16_t)r - dy); - uint32_t mb; - if (rb < 0 || rb >= (int16_t)ST_SPRITE_H) { - continue; + // Sprite b sits dx pixels right of the cab; with dx = 8q - s + // (s in 0..7) its row byte k lines up with byte k + q of the + // cab's phase-s windows, and only 0 <= k + q <= 3 can overlap. + s = (uint8_t)((0 - dx) & (ST_ROW_PHASES - 1)); + q = (int16_t)(((dx + (int16_t)s + 32) >> 3) - 4); + kLo = (q < 0) ? (uint8_t)(0 - q) : 0u; + kHi = (q > 1) ? (uint8_t)(3 - q) : 2u; + // Rows r of the cab meet rows r - dy of sprite b; walk only the + // rows where both have pixels, both pointers advancing in step. + if (sim->collRowFirst[b] == ST_SPRITE_H || sim->collRowFirst[0] == ST_SPRITE_H) { + continue; + } + lo = (int16_t)sim->collRowFirst[0]; + hi = (int16_t)(sim->collRowLast[0] + 1); + if ((int16_t)sim->collRowFirst[b] + dy > lo) { + lo = (int16_t)((int16_t)sim->collRowFirst[b] + dy); + } + if ((int16_t)sim->collRowLast[b] + 1 + dy < hi) { + hi = (int16_t)((int16_t)sim->collRowLast[b] + 1 + dy); + } + if (lo >= hi) { + continue; + } + rLo = (uint8_t)lo; + rHi = (uint8_t)hi; + win = cabWindows(sim, s) + (uint16_t)rLo * 4u; + bRow = sim->collMask[b][(uint8_t)((int16_t)rLo - dy)]; + for (r = rLo; r < rHi; r++, win += 4, bRow += 3) { + bool hit = false; + uint8_t k; + for (k = kLo; k <= kHi; k++) { + if ((bRow[k] & win[(uint8_t)((int16_t)k + q)]) != 0u) { + hit = true; + break; + } } - mb = mask[b][rb]; - if (dx >= 0) { - mb >>= dx; - } else { - mb = (mb << (uint8_t)(-dx)) & 0xFFFFFFu; - } - if ((mask[0][r] & mb) != 0u) { - ss |= (uint8_t)(1u | (1u << b)); + if (hit) { + ss |= (uint8_t)(1u | kStBit[b]); break; } } @@ -374,9 +422,26 @@ static void computeCollisions(StSimT *sim) { } +// Whether any character cell in rows topRow..botRow, columns firstCol.. +// firstCol+3 (the cells a sprite at that spot covers) is non-blank. +static bool coveringCellsSolid(const StSimT *sim, int16_t firstCol, int16_t topRow, int16_t botRow) { + int16_t rr; + int16_t cc; + + for (rr = topRow; rr <= botRow; rr++) { + for (cc = firstCol; cc <= (int16_t)(firstCol + 3); cc++) { + if (cc >= 0 && cc < (int16_t)ST_SCREEN_COLS && sim->screen[ST_CELL((uint16_t)rr, (uint16_t)cc)] != ST_CHAR_SPACE) { + return true; + } + } + } + return false; +} + + // $6A2B -- start the wreck sequence. static void crashStart(StSimT *sim) { - sim->spr[0].ptr = 0xCCu; + sim->spr[0].ptr = ST_SPRITE_WRECK_FIRST; sim->collisionPhase++; sim->phaseTimer = 2u; sim->phaseReload = 2u; @@ -471,7 +536,7 @@ static void fireButtonEdge(StSimT *sim) { // $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; + uint16_t x; if (sim->dirMask == 0u) { stAudioNoise(false); @@ -479,9 +544,9 @@ void stSimFlameUpdate(StSimT *sim) { 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)); + x = (uint16_t)(stSimSpriteX(sim, 0u) - 2u); + sim->spr[2].col = (uint8_t)x; + sim->spr[2].msb = (uint8_t)(x >> 8); sim->spr[2].row = sim->spr[0].row; sim->flameParity ^= 1u; if (sim->flameParity == 0u) { @@ -529,7 +594,7 @@ static void fuelTick(StSimT *sim) { const StLevelT *L = sim->level; uint8_t x; - if (sim->activePad == L->padCount && sim->activePad != L->specialPad) { + if (sim->activePad == sim->padCount && sim->activePad != sim->specialPad) { if (sim->screen[ST_CELL_FUEL_LAST] == ST_CHAR_BLANK) { return; } @@ -590,16 +655,6 @@ static void hudDraw(StSimT *sim) { } -// $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) { @@ -631,26 +686,6 @@ static bool levelEndCheck(const StSimT *sim) { } -// $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) { @@ -665,9 +700,9 @@ static void padDetect(StSimT *sim) { if (((uint16_t)sim->velY >> 8) != 0u) { return; } - for (i = (int8_t)(sim->level->padCount - 1u); i >= 0; i--) { + for (i = (int8_t)(sim->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 taxiX = (int16_t)(((uint16_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) { @@ -684,7 +719,7 @@ static void padDetect(StSimT *sim) { sim->eventDispatchType = 0u; if (sim->activePad != sim->activeSpriteIdx) { if (sim->stage == ST_STAGE_RIDING) { - sim->eventDispatchType = (sim->activeSpriteIdx == 0x0Bu) ? 3u : 2u; + sim->eventDispatchType = (sim->activeSpriteIdx == ST_FARE_DEST_UP) ? 3u : 2u; } else if (sim->stage == ST_STAGE_WAIT) { sim->eventDispatchType = 1u; } @@ -700,7 +735,7 @@ static void padDetect(StSimT *sim) { sim->bobTimer = 8u; stAudioSfx(stC64SfxProgram(ST_SFX_LAND_HARD)); if (sim->stage == ST_STAGE_RIDING) { - hudInit(sim); + stSimHudInit(sim); } return; } @@ -739,6 +774,8 @@ static void passengerArrTick(StSimT *sim) { 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); + // $70D4: the bonus-cab jingle (song 4), blocking. + sim->jingleRequest = (uint8_t)(ST_JINGLE_SONG_BONUS + 1u); } @@ -755,8 +792,9 @@ static void physicsTick(StSimT *sim) { landedHandler(sim); return; } - if (sim->screen[ST_CELL_FUEL] == ST_CHAR_MENU_SENTINEL) { - // Out of fuel: only FIRE survives, no thrust at all. + if (sim->screen[ST_CELL_FUEL] == ST_CHAR_FUEL_HALF) { + // The leftmost cell is down to its last half: out of fuel, so + // only FIRE survives and there is no thrust at all. sim->inputMask &= 0x10u; } else { if ((sim->inputMask & 0x0Cu) != 0u) { @@ -820,9 +858,93 @@ static void markCell(StSimT *sim, uint16_t cell) { } +// ---- glyph reverse index (see StSimT in stSim.h) -------------------- + +// Take `cell` out of the list of whatever glyph it currently shows. +// Reads sim->screen[cell], so call it BEFORE the new character lands. +static void glyphIndexUnlink(StSimT *sim, uint16_t cell) { + uint16_t prev = sim->cellPrev[cell]; + uint16_t next = sim->cellNext[cell]; + + if (prev == ST_GLYPH_CELL_NONE) { + sim->glyphHead[sim->screen[cell]] = next; + } else { + sim->cellNext[prev] = next; + } + if (next != ST_GLYPH_CELL_NONE) { + sim->cellPrev[next] = prev; + } +} + + +// Push `cell` onto the front of glyph `ch`'s list. +static void glyphIndexLink(StSimT *sim, uint16_t cell, uint8_t ch) { + uint16_t head = sim->glyphHead[ch]; + + sim->cellPrev[cell] = ST_GLYPH_CELL_NONE; + sim->cellNext[cell] = head; + if (head != ST_GLYPH_CELL_NONE) { + sim->cellPrev[head] = cell; + } + sim->glyphHead[ch] = cell; +} + + +// Re-derive every list from screen[]. Bulk screen writes (level load, +// title, the blanked intro) bypass setCell, and all of them call +// stSimDirtyAll, so that is the one place this has to hang off. +static void glyphIndexRebuild(StSimT *sim) { + uint16_t cell; + uint16_t k; + + for (k = 0u; k < ST_CHARSET_CHARS; k++) { + sim->glyphHead[k] = ST_GLYPH_CELL_NONE; + } + // Built back to front, so each list comes out in ASCENDING cell + // order and the repaint walk moves forward through the stage the + // way the old span scan did. + cell = ST_SCREEN_CELLS; + while (cell != 0u) { + cell--; + glyphIndexLink(sim, cell, sim->screen[cell]); + } +} + + +// A sprite row (3 bytes, leftmost pixel = bit 7 of byte 0) shifted +// right by s (0..7) into a 4-byte window whose byte j holds the row's +// pixels 8j-s .. 8j-s+7: the row aligned to a grid of 8-pixel cells +// that its left edge sits s pixels into. Fixed shifts by 4, 2 and 1 +// on 16-bit halves -- a variable-count shift is a library bit loop on +// the 65816, and a byte carry chain gets folded back into one. +static void rowWindow(const uint8_t *row, uint8_t s, uint8_t *w) { + uint16_t hi = (uint16_t)(((uint16_t)row[0] << 8) | row[1]); + uint16_t lo = (uint16_t)((uint16_t)row[2] << 8); + + if ((s & 4u) != 0u) { + lo = (uint16_t)((lo >> 4) | (uint16_t)(hi << 12)); + hi = (uint16_t)(hi >> 4); + } + if ((s & 2u) != 0u) { + lo = (uint16_t)((lo >> 2) | (uint16_t)(hi << 14)); + hi = (uint16_t)(hi >> 2); + } + if ((s & 1u) != 0u) { + lo = (uint16_t)((lo >> 1) | (uint16_t)(hi << 15)); + hi = (uint16_t)(hi >> 1); + } + w[0] = (uint8_t)(hi >> 8); + w[1] = (uint8_t)hi; + w[2] = (uint8_t)(lo >> 8); + w[3] = (uint8_t)lo; +} + + static void setCell(StSimT *sim, uint16_t cell, uint8_t ch) { if (sim->screen[cell] != ch) { + glyphIndexUnlink(sim, cell); sim->screen[cell] = ch; + glyphIndexLink(sim, cell, ch); markCell(sim, cell); } } @@ -837,42 +959,112 @@ static void setColorCell(StSimT *sim, uint16_t cell, uint8_t color) { } -// 21 rows of 24-bit foreground masks for a displayed sprite, kept in +// 21 rows of 3-byte foreground masks for a displayed sprite, kept in // collMask[idx] and rebuilt only when its bitmap or multicolour mode // changes. A multicolour sprite's 2-bit pairs cover two pixels each: a // non-zero pair is two set mask bits (kPairMask maps a nibble's two -// pairs at once). +// pairs at once). The cab's cached windows die with its mask. static void spriteMasks(StSimT *sim, uint8_t idx) { static const uint8_t kPairMask[16] = { 0x0u, 0x3u, 0x3u, 0x3u, 0xCu, 0xFu, 0xFu, 0xFu, 0xCu, 0xFu, 0xFu, 0xFu, 0xCu, 0xFu, 0xFu, 0xFu }; const uint8_t *bm = stSimSpriteBitmap(sim, sim->frame.ptr[idx]); - uint8_t multi = (uint8_t)((sim->frame.multiMask >> idx) & 1u); - uint32_t *rows = sim->collMask[idx]; - uint8_t r; + uint8_t multi = (uint8_t)((sim->frame.multiMask & kStBit[idx]) != 0u ? 1u : 0u); + uint8_t *row = sim->collMask[idx][0]; + uint16_t n; + uint8_t first; + uint8_t last; if (bm != 0 && bm == sim->collMaskBm[idx] && multi == sim->collMaskMulti[idx]) { return; } - for (r = 0u; r < ST_SPRITE_H; r++) { - uint8_t b0 = 0u; - uint8_t b1 = 0u; - uint8_t b2 = 0u; + for (n = 0u; n < (uint16_t)(ST_SPRITE_H * 3u); n++) { + uint8_t b = 0u; if (bm != 0) { - b0 = bm[r * 3u]; - b1 = bm[r * 3u + 1u]; - b2 = bm[r * 3u + 2u]; + b = bm[n]; if (multi != 0u) { - b0 = (uint8_t)((kPairMask[b0 >> 4] << 4) | kPairMask[b0 & 0x0Fu]); - b1 = (uint8_t)((kPairMask[b1 >> 4] << 4) | kPairMask[b1 & 0x0Fu]); - b2 = (uint8_t)((kPairMask[b2 >> 4] << 4) | kPairMask[b2 & 0x0Fu]); + b = (uint8_t)((kPairMask[b >> 4] << 4) | kPairMask[b & 0x0Fu]); } } - rows[r] = ((uint32_t)b0 << 16) | ((uint32_t)b1 << 8) | b2; + row[n] = b; } + first = ST_SPRITE_H; + last = 0u; + for (n = 0u; n < ST_SPRITE_H; n++) { + if ((row[n * 3u] | row[n * 3u + 1u] | row[n * 3u + 2u]) != 0u) { + if (first == ST_SPRITE_H) { + first = (uint8_t)n; + } + last = (uint8_t)n; + } + } + sim->collRowFirst[idx] = first; + sim->collRowLast[idx] = last; sim->collMaskBm[idx] = bm; sim->collMaskMulti[idx] = multi; + if (idx == 0u) { + sim->cabWinValid = 0u; + } +} + + +// A non-cab sprite's rows (shifted here, only rows with pixels) against +// the background rows under it. +static bool spriteRowsTouchBackground(const StSimT *sim, uint8_t idx, int16_t firstCol, int16_t py, uint8_t phase) { + uint8_t first = sim->collRowFirst[idx]; + uint8_t left = (uint8_t)(sim->collRowLast[idx] + 1u - first); + const uint8_t *row; + + if (first == ST_SPRITE_H) { + return false; + } + row = sim->collMask[idx][first]; + py = (int16_t)(py + first); + // Counted down like cabRowsTouchBackground. + do { + if ((row[0] | row[1] | row[2]) != 0u) { + uint8_t w[4]; + rowWindow(row, phase, w); + if (backgroundRowHits(sim, firstCol, py, w)) { + return true; + } + } + row += 3; + py++; + left--; + } while (left != 0u); + return false; +} + + +// One displayed sprite against the background pixels ($D01F). The +// background can only touch it where a covering character cell is +// non-blank. In open flight every covering cell is the space glyph +// (all-zero pixels), so the 21-row pixel test is skipped there; any +// non-space cell falls through to the exact test, so the result is +// unchanged (space is the only zero-pixel glyph the test can hit). +// Needs spriteMasks(sim, idx) run for this frame first. The row tests +// live in cabRowsTouchBackground / spriteRowsTouchBackground, whose +// loops count DOWN -- see the note there before "simplifying" them. +static bool spriteTouchesBackground(StSimT *sim, uint8_t idx) { + int16_t px = (int16_t)((int16_t)sim->frame.x[idx] - ST_SPRITE_X_ORIGIN); + int16_t py = (int16_t)((int16_t)sim->frame.y[idx] - ST_SPRITE_Y_ORIGIN); + int16_t firstCol = cellOfPixel(px); + int16_t topRow = (py < 0) ? 0 : cellOfPixel(py); + int16_t botRow = cellOfPixel((int16_t)(py + (int16_t)ST_SPRITE_H - 1)); + uint8_t phase = (uint8_t)(px - (int16_t)(firstCol << 3)); + + if (botRow >= (int16_t)ST_SCREEN_ROWS) { + botRow = (int16_t)(ST_SCREEN_ROWS - 1u); + } + if (!coveringCellsSolid(sim, firstCol, topRow, botRow)) { + return false; + } + if (idx == 0u) { + return cabRowsTouchBackground(sim, firstCol, py, phase); + } + return spriteRowsTouchBackground(sim, idx, firstCol, py, phase); } @@ -897,7 +1089,7 @@ static void taxiSpawnInit(StSimT *sim) { sim->spr[0].enable = 1u; sim->multiColorMask = 0x07u; - sim->spr[0].ptr = 0xC0u; + sim->spr[0].ptr = ST_SPRITE_CAB_RIGHT; sim->posYlo = L->spawn[3]; sim->posYrow = L->spawn[4]; sim->posXlo = L->spawn[0]; @@ -930,9 +1122,9 @@ static void taxiSpriteCelSelect(StSimT *sim) { uint8_t base; if ((sim->dirMask & 0x04u) != 0u) { - base = 0xDCu; + base = ST_SPRITE_CAB_LEFT; } else if ((sim->dirMask & 0x08u) != 0u) { - base = 0xC0u; + base = ST_SPRITE_CAB_RIGHT; } else { return; } @@ -1014,6 +1206,12 @@ void stSimBcdAdd(StSimT *sim, uint16_t dstCell, const uint8_t *blob) { } +// $6C38 -- blank the message row (colour 1, as the original writes it). +void stSimClearMessage(StSimT *sim) { + stSimDrawText(sim, ST_MESSAGE_COL, ST_MESSAGE_ROW, kTextBlank, 1u); +} + + // $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) { @@ -1078,16 +1276,79 @@ void stSimDrawText(StSimT *sim, uint8_t col, uint8_t row, const uint8_t *text, u } +// A character's eight 1bpp rows: the game's edit of it, or the const ROM +// glyph when it has never been edited. +const uint8_t *stSimGlyph(const StSimT *sim, uint8_t ch) { + uint8_t slot = sim->glyphSlot[ch]; + + if (slot != 0u) { + return sim->glyphOverride[slot - 1u].rows; + } + return &stC64Charset()[(uint16_t)ch << 3]; +} + + +// The writable eight rows, copied out of ROM on the first edit. A full +// table parks the edit in the spill entry rather than stealing another +// character's slot: that glyph then simply stays at its ROM form. +uint8_t *stSimGlyphMut(StSimT *sim, uint8_t ch) { + uint8_t slot = sim->glyphSlot[ch]; + + if (slot == 0u) { + if (sim->glyphOverrideCount >= ST_GLYPH_OVERRIDES) { + return sim->glyphOverride[ST_GLYPH_OVERRIDES].rows; + } + sim->glyphOverrideCount++; + slot = sim->glyphOverrideCount; + sim->glyphSlot[ch] = slot; + memcpy(sim->glyphOverride[slot - 1u].rows, &stC64Charset()[(uint16_t)ch << 3], 8u); + } + return sim->glyphOverride[slot - 1u].rows; +} + + +// Every glyph back to the ROM set (a scene load). +void stSimGlyphReset(StSimT *sim) { + memset(sim->glyphSlot, 0, sizeof(sim->glyphSlot)); + sim->glyphOverrideCount = 0u; +} + + +// One row of a character's glyph. The collision background test reads +// four of these per sprite row, so it skips the pointer round trip. +uint8_t stSimGlyphRow(const StSimT *sim, uint8_t ch, uint8_t row) { + uint8_t slot = sim->glyphSlot[ch]; + + if (slot != 0u) { + return sim->glyphOverride[slot - 1u].rows[row]; + } + return stC64Charset()[((uint16_t)ch << 3) + row]; +} + + +// $43A5 -- blank the fare meter back to its template. +void stSimHudInit(StSimT *sim) { + uint8_t k; + + for (k = 0u; k < ST_NUMBER_CHARS; k++) { + setCell(sim, (uint16_t)(ST_CELL_FARE + k), kHudTemplate[k]); + } +} + + // $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)); + sim->padCount = level->padCount; + sim->specialPad = level->specialPad; + sim->bgCollSprites = 0u; 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)); + stSimGlyphReset(sim); sim->charDirtyAll = true; sim->charDirtyCount = 0u; // Level sprites live in the one level buffer, so the same bitmap @@ -1120,7 +1381,7 @@ void stSimEnterLevel(StSimT *sim, const StLevelT *level) { sim->spr[k].enable = 0u; } sim->hookLevel = level->levelIndex; - memcpy(sim->hookRam, level->hookData, ST_HOOK_BYTES); + stHookSceneLoad(sim); stSimRespawn(sim); } @@ -1134,19 +1395,27 @@ void stSimRespawn(StSimT *sim) { 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 }; + static const uint8_t kPadPlease[] = "PAD 1 PLEASE"; sim->stage = ST_STAGE_RIDING; sim->activeSpriteIdx = 1u; sim->activeDyingSlot = 1u; - stSimDrawText(sim, 14u, 24u, kPadPlease, 1u); + stSimDrawText(sim, ST_MESSAGE_COL, ST_MESSAGE_ROW, 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); + // $6906 framePresent: the start-of-screen jingle, then the SID (and + // the demo RNG) reset. $6935: level 25 gets song 4, every other + // screen rotates songs 0..3 on $5E9B. The host plays it with the sim + // frozen, which is what the C64's blocking play-and-wait amounts to. + stSimMarshal(sim); stAudioSilence(); + sim->jingleRotate++; + if (sim->levelState == ST_JINGLE_LEVEL_25) { + sim->jingleRequest = (uint8_t)(ST_JINGLE_SONG_BONUS + 1u); + } else { + sim->jingleRequest = (uint8_t)((sim->jingleRotate & 3u) + 1u); + } sim->spriteSpriteColl = 0u; sim->spriteBgColl = 0u; sim->rngT1 = 0x06u; @@ -1218,7 +1487,65 @@ void stSimMarkChar(StSimT *sim, uint8_t ch) { } +// $4253 + $4293 -- snapshot the sprite shadows into the frame the VIC +// shows next. The title and level-intro loops flush the same way. +#if defined(__W65816__) +// The IIgs copies the frame in asm (stMarshalIigs.s); it takes one far +// pointer to this block so no stack-argument convention is involved. +typedef struct { + const StSpriteT *spr; + StFrameT *frame; + uint8_t multi; +} StMarshalArgsT; +extern void stSimMarshalIigs(const StMarshalArgsT *args); +// The asm hardcodes these layouts; a mismatch fails the build here. +typedef char stMarshalCheckSprite[(sizeof(StSpriteT) == 6u && offsetof(StSpriteT, col) == 0u && offsetof(StSpriteT, msb) == 1u && offsetof(StSpriteT, row) == 2u && offsetof(StSpriteT, enable) == 3u && offsetof(StSpriteT, ptr) == 4u && offsetof(StSpriteT, color) == 5u) ? 1 : -1]; +typedef char stMarshalCheckFrame[(offsetof(StFrameT, x) == 0u && offsetof(StFrameT, y) == 16u && offsetof(StFrameT, ptr) == 24u && offsetof(StFrameT, color) == 32u && offsetof(StFrameT, enableMask) == 40u && offsetof(StFrameT, multiMask) == 41u) ? 1 : -1]; +typedef char stMarshalCheckArgs[(sizeof(void *) == 4u && offsetof(StMarshalArgsT, frame) == 4u && offsetof(StMarshalArgsT, multi) == 8u) ? 1 : -1]; +#endif + + +void stSimMarshal(StSimT *sim) { +#if defined(__W65816__) + static StMarshalArgsT args; + + args.spr = sim->spr; + args.frame = &sim->frame; + args.multi = sim->multiColorMask; + stSimMarshalIigs(&args); +#else + 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 |= kStBit[i]; + } + } + sim->frame.enableMask = enable; + sim->frame.multiMask = sim->multiColorMask; +#endif +} + + +// Every cell repaints AND the glyph index is rebuilt: for callers that +// replaced the screen bytes wholesale (a level load, the title scene). void stSimDirtyAll(StSimT *sim) { + stSimDirtyCells(sim); + // Every bulk screen write calls this, and those bypass setCell, so + // this is where the glyph reverse index gets re-derived. + glyphIndexRebuild(sim); +} + + +// Every cell repaints; the screen bytes are as they were, so the glyph +// index stands. A rebuild is ~0.15 s on the IIgs (1000 far-pointer +// list links), and the level-entry path used to pay it twice. +void stSimDirtyCells(StSimT *sim) { memset(sim->cellDirty, 1, ST_SCREEN_CELLS); sim->dirtyCount = 0u; sim->dirtyAll = true; @@ -1273,6 +1600,21 @@ uint8_t stSimRng(StSimT *sim, uint8_t n) { } +// $63D0 -- rotate every row of a glyph one pixel right and mark every +// cell showing it for repaint: the transporter hatch and the level O +// electroids both animate this way. +void stSimRotateGlyph(StSimT *sim, uint8_t ch) { + uint8_t *rows = stSimGlyphMut(sim, ch); + uint8_t row; + + for (row = 0u; row < 8u; row++) { + uint8_t b = rows[row]; + rows[row] = (uint8_t)((b >> 1) | (b << 7)); + } + stSimMarkChar(sim, ch); +} + + // The score glyphs as pennies (for the high-score table). uint32_t stSimScorePennies(const StSimT *sim) { uint32_t v = 0u; @@ -1308,6 +1650,16 @@ void stSimSetDemoStream(StSimT *sim, const uint8_t *stream, uint16_t len) { } +// $411B -- add a signed byte to a sprite's 17-bit X, carrying into the +// msb shadow. +void stSimSpriteAddX(StSimT *sim, uint8_t idx, uint8_t delta) { + uint16_t x = (uint16_t)(stSimSpriteX(sim, idx) + (uint16_t)(int16_t)(int8_t)delta); + + sim->spr[idx].col = (uint8_t)x; + sim->spr[idx].msb = (uint8_t)(x >> 8); +} + + // 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; @@ -1326,6 +1678,12 @@ const uint8_t *stSimSpriteBitmap(const StSimT *sim, uint8_t ptr) { } +// The 17-bit VIC sprite X the shadow tables hold as msb:col. +uint16_t stSimSpriteX(const StSimT *sim, uint8_t idx) { + return (uint16_t)(((uint16_t)sim->spr[idx].msb << 8) | sim->spr[idx].col); +} + + // $5F40 -- one iteration of the main loop. StTickResultE stSimTick(StSimT *sim) { StTickResultE result = ST_TICK_CONTINUE; @@ -1350,8 +1708,8 @@ StTickResultE stSimTick(StSimT *sim) { stFarePostTickGate(sim); stFareStageDispatch(sim); stHookPerTick2(sim); - marshal(sim); - bitScrollHatch(sim); + stSimMarshal(sim); + stSimRotateGlyph(sim, ST_CHAR_TRANSPORTER); stHookPerTick3(sim); fuelBarHud(sim); if (sim->collisionPhase == 0u) { @@ -1377,3 +1735,4 @@ StTickResultE stSimTick(StSimT *sim) { passengerArrTick(sim); return ST_TICK_CONTINUE; } + diff --git a/examples/spacetaxi/stSim.h b/examples/spacetaxi/stSim.h index 718a7df..9f3f541 100644 --- a/examples/spacetaxi/stSim.h +++ b/examples/spacetaxi/stSim.h @@ -31,6 +31,9 @@ #define ST_SCREEN_COLS 40u #define ST_SCREEN_ROWS 25u #define ST_SCREEN_CELLS (ST_SCREEN_COLS * ST_SCREEN_ROWS) +// End-of-list / not-linked sentinel for the glyph reverse index below. +// ST_SCREEN_CELLS is 1000, so no real cell index can collide with it. +#define ST_GLYPH_CELL_NONE 0xFFFFu // row * 40 + col. Written as shifts (40 == 32 + 8) because the 65816 has // no multiply: with a variable row the * form called the compiler's // software multiply helper, and the collision pre-check does that a @@ -43,6 +46,9 @@ #define ST_SPRITE_Y_ORIGIN 50 #define ST_SPRITE_W 24 #define ST_SPRITE_H 21 +// A sprite row shifted onto the 8-pixel character grid has one of 8 +// phases (its left edge 0..7 pixels into a cell) -- see rowWindow. +#define ST_ROW_PHASES 8 #define ST_HW_SPRITES 8u // Fixed screen cells the game writes directly (C64 screen addresses). @@ -53,6 +59,8 @@ #define ST_CELL_SCREENS ST_CELL(23, 32) // $07B8..$07BC #define ST_CELL_SCORE ST_CELL(24, 2) // $07C2..$07C8, 7 chars #define ST_CELL_MESSAGE ST_CELL(24, 14) // $07CE, 11 chars +#define ST_MESSAGE_COL 14u +#define ST_MESSAGE_ROW 24u #define ST_CELL_PAD_DIGIT ST_CELL(24, 18) // $07D2 #define ST_CELL_FARE ST_CELL(24, 32) // $07E0..$07E6, 7 chars #define ST_NUMBER_CHARS 7u @@ -71,7 +79,57 @@ #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 +#define ST_CHAR_ELECTROID 0x6Fu // level O's scrolling barrier glyph +#define ST_FARE_DEST_UP 0x0Bu // $7D8E: the fare aboard wants UP + +// ---- level-intro starfield -------------------------------------------- +// +// The C64's level intro streams seven star SPRITES out of the centre of +// the screen. That is the most expensive thing this port asks a present +// for: eight sprites are save-under drawn and restored every frame, and +// because a row's dirty band is one min..max interval, two stars far +// apart on the same row widen that row to nearly full width. Radially +// spreading stars maximise exactly that, so the warp ends up presenting +// close to the whole framebuffer every frame -- which is what makes it +// the slowest thing the IIgs does, and wasted work everywhere else. +// +// So the warp is not animated in pixels at all. The starfield is painted +// ONCE into screen RAM and animated by cycling the colours its cells are +// drawn in -- the same trick the title logo already uses +// (stRenderTitleLogo). A frame costs one palette upload and NO dirty +// pixel rows. +// +// The contract between the two halves is just this colour range: +// stTitle.c colours a star's cell ST_STAR_RING_FIRST + ring (ring 0 is +// the centre), and the renderer cycles that range. These are C64 colour +// indices the intro screen never uses -- it draws in 0, 3 and, in demo +// mode, 4 and 5; the cab sprite adds 2, 6 and 7. +#define ST_STAR_RING_FIRST 8u +#define ST_STAR_RING_COUNT 8u + +// The renderer wraps the sweep's phase with a mask rather than a +// modulo, so the ring count has to stay a power of two. Changing it to +// something else must fail to compile here instead of quietly making +// the warp skip rings. +typedef char stStarRingCountIsPow2[((ST_STAR_RING_COUNT & (ST_STAR_RING_COUNT - 1u)) == 0u) ? 1 : -1]; +// Ring 0 is the centre, which in a warp is the VANISHING POINT: those +// stars are the far ones and get the small glyph. The big glyph belongs +// to the outer rings, the stars about to rush past the viewer. +#define ST_STAR_GLYPH_NEAR 0x2Au // '*', the outer rings +#define ST_STAR_GLYPH_FAR 0x2Eu // '.', the inner rings +#define ST_STAR_FAR_RINGS 5u // rings 0..4 are still distant + +// VIC sprite block pointers the game selects by name. The cab's bit 0 is +// the landing gear, so each facing is a pair. +#define ST_SPRITE_CAB_RIGHT 0xC0u // +1 = gear down +#define ST_SPRITE_CAB_LEFT 0xDCu // +1 = gear down +#define ST_SPRITE_PASS_WALK_R 0xC2u // +walkParity +#define ST_SPRITE_PASS_WALK_L 0xC4u // +walkParity +#define ST_SPRITE_PASS_STAND 0xC7u +#define ST_SPRITE_PASS_BEAM 0xCBu // counts down to PASS_STAND +#define ST_SPRITE_PASS_GONE 0xCCu // one past PASS_BEAM: fully shrunk +#define ST_SPRITE_WRECK_FIRST 0xCCu // the wreck cel walk, FIRST..LAST-1 +#define ST_SPRITE_WRECK_LAST 0xD1u // Title logo ($4827): the logo is one character repeated over rows // 1..11, and the flip-book animates it by copying one of four frame @@ -88,6 +146,16 @@ // Distinct characters whose glyph can change in one frame before the // renderer gives up and repaints everything. #define ST_CHAR_DIRTY_MAX 8u +// Characters that may hold an edited glyph at once. The game edits six +// across the whole game (the transporter hatch $67, level O's barrier +// $6F, the title logo $84 and level W's three laser glyphs $91..$93), so +// this is generous; an overflow parks the edit in the spill entry, where +// it is simply never displayed rather than corrupting another character. +#define ST_GLYPH_OVERRIDES 16u + +typedef struct { + uint8_t rows[8]; +} StGlyphT; // Level image as loaded from a .dat (STL4). Kept in the C64's units. #define ST_MAX_PADS 10u @@ -98,6 +166,113 @@ #define ST_HOOK_BASE 0x7D98u #define ST_HOOK_BYTES (0x8000u - ST_HOOK_BASE) +// Per-level hook scratch. The original keeps a hook's running state +// inside the level's own data block, at fixed C64 addresses interleaved +// with its tables -- and, for some levels, on top of the hook's own 6502 +// code, which this port never executes. A level reads its TABLES straight +// out of the shipped blob (the single source of truth) and keeps only the +// bytes it WRITES here. Seeded from +// the blob at scene load, so the first tick starts where the original's +// did. Every level is converted, so no C64-addressed working copy of the +// blob exists any more. +#define ST_HOOK_H_SEGS 11u // level H segment ids 0..10 +#define ST_HOOK_W_LASERS 8u + +typedef struct { + uint8_t segFlag[ST_HOOK_H_SEGS]; // $7E15..$7E1F wall segment up/down + uint8_t switchId; // $7E4C also read back as a table entry + uint8_t tick; // $7E53 + uint8_t active; // $7E54 switch being animated + uint8_t step; // $7E55 0..3 of the animation + uint8_t tone; // $7E56 rising chime +} StHookHT; + +typedef struct { + uint8_t celPhase; // $7DD2 orb cel cycle 0..5 + uint8_t tick; // $7E00 + uint8_t lastSpot; // $7DD1 landing spot used last, 1..5 + uint8_t hop; // $7DD3 ticks left in a teleport + uint8_t sweep; // $7F02 the hop's falling tone +} StHookGT; + +typedef struct { + uint8_t state[ST_HW_SPRITES]; // $7DB8 0 idle, 1 rising, 2 burst + uint8_t phase[ST_HW_SPRITES]; // $7DC0 cel phase + uint8_t dx[ST_HW_SPRITES]; // $7DC8 + uint8_t dy[ST_HW_SPRITES]; // $7DD0 +} StHookIT; + +typedef struct { + uint8_t parity; // $7DBD magnet pole flip +} StHookKT; + +typedef struct { + uint8_t tick; // $7DBD mod-8 scroll gate +} StHookOT; + +typedef struct { + uint8_t phase[ST_HW_SPRITES]; // $7E83 interference cel phase +} StHookQT; + +typedef struct { + uint8_t gateShut; // $7D9C +} StHookTT; + +typedef struct { + uint8_t colorPhase; // $7DAB + uint8_t state[ST_HOOK_W_LASERS]; // $7DAC 0 idle, 1 extending, 2 fading + uint8_t row[ST_HOOK_W_LASERS]; // $7DB5 beam head / fade phase + uint8_t started; // $7DE5 +} StHookWT; + +typedef struct { + uint8_t celPhase[ST_HW_SPRITES]; // $7F4F + uint8_t state[ST_HW_SPRITES]; // $7F57 0 idle, 1 active + uint8_t dir[ST_HW_SPRITES]; // $7F5F entry edge 0..3 + uint8_t life[ST_HW_SPRITES]; // $7F6E + uint8_t cooldown; // $7F6D ticks before the next bounce +} StHookUT; + +typedef struct { + uint8_t col; // $7D9C + uint8_t dir; // $7D9D + uint8_t tick; // $7D9E +} StHookXT; + +typedef struct { + uint8_t row; // $7D98 row the leaves grow along + uint8_t seg; // $7D99 leaves grown this cycle 0..4 + uint8_t tick; // $7D9A ticks toward the next leaf + uint8_t padSlot; // $7D9B number of the next pad to hang +} StHookET; + +typedef struct { + uint8_t state[ST_HW_SPRITES]; // $7EDB 0 idle, 1 falling, 2 bursting + uint8_t phase[ST_HW_SPRITES]; // $7ED3 burst cel phase +} StHookJT; + +typedef struct { + uint8_t state[ST_HW_SPRITES]; // $7F16 0 idle, 1 falling, 2 melting + uint8_t phase[ST_HW_SPRITES]; // $7F0E melt cel phase + uint8_t parity; // $7F1F a melt advances every other count +} StHookPT; + +typedef union { + StHookET e; + StHookGT g; + StHookHT h; + StHookIT i; + StHookJT j; + StHookKT k; + StHookOT o; + StHookPT p; + StHookQT q; + StHookTT t; + StHookUT u; + StHookWT w; + StHookXT x; +} StHookScratchT; + typedef struct { uint8_t x1Hi; // slot byte 0: landing X lower bound, high byte uint8_t x1Lo; // slot byte 1 @@ -128,7 +303,7 @@ typedef struct { 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 levelIndex; // 0..23 = A..X, 24 = Y, 25 = title uint8_t spriteCount; StLevelSpriteT sprites[ST_MAX_LEVEL_SPRITES]; uint8_t hookData[ST_HOOK_BYTES]; // $7D98..$7FFF as shipped @@ -200,7 +375,34 @@ typedef struct { uint16_t dirtyList[512]; // cells marked since the last frame uint16_t dirtyCount; bool dirtyAll; // list overflowed / whole screen - uint8_t charset[ST_CHARSET_CHARS][8]; // $2800 working copy + // Reverse index: for each glyph, the exact set of cells showing it, + // as doubly-linked lists threaded through the cell array. + // + // The renderer has to repaint every cell showing a glyph whose + // BITMAP changed -- a chunky framebuffer bakes the pixels in at + // paste time, so unlike the C64's character mode there is no free + // re-composite. It used to find those cells by scanning a per-glyph + // [first, last] span, which PC profiling showed was the single + // hottest loop in the program (22% of all samples on level H). This + // gives the exact set instead: no scan, cost proportional to the + // cells that really show the glyph. + // + // screen[] is the only truth and these mirror it: setCell keeps them + // in step one cell at a time, and a bulk screen write re-derives + // them via stSimDirtyAll, which every bulk writer already calls. + // They live in the NOT-cleared block for the same reason screen[] + // does -- stSimNewGame must leave the live picture alone. + uint16_t glyphHead[ST_CHARSET_CHARS]; // ST_GLYPH_CELL_NONE = none + uint16_t cellNext[ST_SCREEN_CELLS]; + uint16_t cellPrev[ST_SCREEN_CELLS]; + // $2800. The charset is NOT copied: the ROM set is const and only the + // handful of glyphs the game rewrites at runtime are stored here, so + // this costs ~400 bytes instead of 2 KB and a scene load clears an + // index rather than copying 2 KB. Reach glyphs through stSimGlyph / + // stSimGlyphRow / stSimGlyphMut, never by indexing directly. + uint8_t glyphSlot[ST_CHARSET_CHARS]; // 0 = ROM glyph, else slot + 1 + StGlyphT glyphOverride[ST_GLYPH_OVERRIDES + 1u]; // last = spill + uint8_t glyphOverrideCount; // Characters whose glyph bits the game has edited since the last // frame -- the transporter hatch, the title logo flip, the laser // beams. A short list, not a 256-entry flag array: the renderer had @@ -221,6 +423,8 @@ typedef struct { // ---- level image (mutable copies: hooks edit the pad table) ---- const StLevelT *level; StPadT pads[ST_MAX_PADS]; // $7D0A working copy + uint8_t padCount; // $7D5A working copy (level E opens pads) + uint8_t specialPad; // $7D09 working copy // ---- input ---- uint8_t inputMask; // $7169 EOR $FF of $DC00: 1 = pressed @@ -255,12 +459,25 @@ typedef struct { uint8_t hitDispatchResult; // $721D uint8_t spriteSpriteColl; // $71CA latched $D01E uint8_t spriteBgColl; // $71CB latched $D01F - // Per-sprite 24-bit row masks of the displayed frame, rebuilt only - // when the sprite's bitmap or multicolour mode changes (derived - // state, not C64 memory). - uint32_t collMask[ST_HW_SPRITES][ST_SPRITE_H]; + // Sprites besides the cab whose $D01F bit a hook reads (levels J + // and P watch their hazards hit the scenery); the rest are never + // tested (derived state, not C64 memory). + uint8_t bgCollSprites; + // Per-sprite row masks of the displayed frame (3 bytes per row, the + // leftmost pixel is bit 7 of byte 0), rebuilt only when the sprite's + // bitmap or multicolour mode changes (derived state, not C64 memory). + uint8_t collMask[ST_HW_SPRITES][ST_SPRITE_H][3]; const uint8_t *collMaskBm[ST_HW_SPRITES]; uint8_t collMaskMulti[ST_HW_SPRITES]; + // First and last rows of collMask[idx] with any pixel (first == + // ST_SPRITE_H when the mask is empty): the row tests walk only these. + uint8_t collRowFirst[ST_HW_SPRITES]; + uint8_t collRowLast[ST_HW_SPRITES]; + // The cab's rows pre-shifted to each grid phase (cabWindows), built + // per phase on first use; bit s of cabWinValid says cabWin[s] + // matches collMask[0]. + uint8_t cabWin[ST_ROW_PHASES][ST_SPRITE_H][4]; + uint8_t cabWinValid; // ---- fuel ---- uint8_t fuelCountdown; // $71C7 @@ -300,6 +517,12 @@ typedef struct { uint8_t levelState; // $7215 screens completed uint8_t demoMode; // $721C uint8_t postMortem; // $5E9D + // Jingles. The C64 plays its tunes BLOCKING ($44CF spins until the + // song ends), so the sim only RAISES a request here and the host + // plays it while holding the sim frozen -- tick-exactness survives + // because no tick happens while the tune sounds. + uint8_t jingleRequest; // song number + 1 to play now; 0 = none + uint8_t jingleRotate; // $5E9B: start-of-screen tune = ++this & 3 // ---- demo playback ($48F2): the $0902 buffer a recording is // loaded into; a shorter recording leaves the previous tail behind @@ -318,8 +541,6 @@ typedef struct { 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 @@ -327,7 +548,7 @@ typedef struct { uint8_t spriteMc1; // $D026 // ---- per-level hook scratch ($7D9C.. and friends) ---- - uint8_t hookRam[ST_HOOK_BYTES]; // $7D98..$7FFF working copy + StHookScratchT hookScratch; // per-level hook state uint8_t hookLevel; // level index the hook state belongs to uint8_t warpCounter; // $5A66 uint8_t warpMask; // $5CE8 @@ -357,6 +578,29 @@ bool stSimAdvancePlayer(StSimT *sim); void stSimArchiveHud(StSimT *sim); // Mark every cell dirty (scene change). void stSimDirtyAll(StSimT *sim); +void stSimDirtyCells(StSimT *sim); // repaint all, glyph index kept +// $43A5 -- blank the fare meter back to its template. +void stSimHudInit(StSimT *sim); +// $4253 + $4293 -- snapshot the sprite shadows into the displayed frame. +void stSimMarshal(StSimT *sim); +// $63D0 -- rotate every row of a glyph one pixel right and mark it dirty. +void stSimRotateGlyph(StSimT *sim, uint8_t ch); +// A character's eight 1bpp rows: the ROM glyph, or the game's edit of it. +// kStBit[i] == 1 << i. A variable shift is a library bit loop on the +// 65816, so sprite-index bit tests go through this table instead. +extern const uint8_t kStBit[ST_HW_SPRITES]; + +const uint8_t *stSimGlyph(const StSimT *sim, uint8_t ch); +uint8_t stSimGlyphRow(const StSimT *sim, uint8_t ch, uint8_t row); +// The same eight rows, writable (copied from ROM on the first edit). +uint8_t *stSimGlyphMut(StSimT *sim, uint8_t ch); +// Drop every edit: the whole charset is the ROM set again (scene load). +void stSimGlyphReset(StSimT *sim); +// The 17-bit VIC sprite X (msb:col), read and signed-byte add ($411B). +uint16_t stSimSpriteX(const StSimT *sim, uint8_t idx); +void stSimSpriteAddX(StSimT *sim, uint8_t idx, uint8_t delta); +// $6C38 -- blank the 11-character message row. +void stSimClearMessage(StSimT *sim); // A glyph's bits changed: every cell showing it has to be repainted. void stSimMarkChar(StSimT *sim, uint8_t ch); // Text and HUD primitives (screen RAM writes). @@ -404,13 +648,19 @@ void stHookPerTick2(StSimT *sim); // $7D6C void stHookPerTick3(StSimT *sim); // $7D6F uint8_t stHookInput(StSimT *sim, uint8_t input); // $7D72 uint8_t stHookHitVerdict(StSimT *sim); // $7D75: 0 = passenger contact is safe +// Seed a converted level's scratch from the shipped blob (scene load). +void stHookSceneLoad(StSimT *sim); // ---- audio sink (host provides; stAudio.c / stsim stub) ---- void stAudioSfx(const uint8_t *program9); // $42E9 load void stAudioNoise(bool on); // $D412 = $81 / $80 void stAudioThrustSweep(uint8_t value); // $D400/$D401 = value void stAudioSpeech(uint8_t ch); // $9802 +void stAudioSpeechPhrase(const uint8_t *speech); void stAudioSilence(void); // voices 1+2 gate off +void stAudioJingle(uint8_t song); // $44CF: silence, then play song (caller blocks) +bool stAudioJingleActive(void); // still sounding? +void stAudioMusic(uint8_t song, bool loop); // $CB02: silence, then play song in the background void stAudioVoice2(uint8_t freqLo, uint8_t freqHi, uint8_t ctrl); // level-hook tones void stAudioVoice1Freq(uint8_t value); // $D400/$D401 pokes from the fuel pump diff --git a/examples/spacetaxi/stSongData.h b/examples/spacetaxi/stSongData.h new file mode 100644 index 0000000..f88621e --- /dev/null +++ b/examples/spacetaxi/stSongData.h @@ -0,0 +1,512 @@ +// Space Taxi (C64) tunes as JoeyLib JYM1 streams. GENERATED by +// assets/genSongData.py -- do not hand-edit. The songs were extracted by +// running the game's own music player under a 6502 emulator +// (assets/sidCapture.py logs every SID write per PAL frame; +// assets/sidToSong.py turns that into songbake text in assets/songs/). +// +// Where the C64 plays them: +// BLOCKING via $44CF (the game spins until the tune ends; the port +// parks in ST_STATE_JINGLE with the sim frozen): +// 0..3 start of every screen, rotating on $5E9B ($6906) +// 4 level 25 start ($6935) and the bonus cab ($70B1) +// 5 game over ($5C1F), after the two-second GAME OVER! wait +// NON-BLOCKING via $CB02 (started and left to the IRQ player): +// 6 the high-score name entry screen ($4E35) -- no port screen yet +// 7 the high-score table ($4C1F, joystick UP on the title) +// 8 the level intro while the cab flies in ($459C); endless, so +// the source is trimmed to 30 s and looped +#ifndef ST_SONG_DATA_H +#define ST_SONG_DATA_H + +#include + +static const uint8_t gStSong0[109] = { + 0x4A, 0x59, 0x4D, 0x31, 0x14, 0x00, 0xFF, 0xFF, 0x81, 0x10, 0x50, 0x01, + 0x02, 0x11, 0xA8, 0x01, 0x02, 0x83, 0x20, 0x21, 0x85, 0x10, 0x7A, 0x01, + 0x02, 0x11, 0xA8, 0x01, 0x02, 0x83, 0x20, 0x21, 0x85, 0x10, 0x50, 0x01, + 0x02, 0x11, 0xA8, 0x01, 0x02, 0x83, 0x20, 0x21, 0x85, 0x10, 0x7A, 0x01, + 0x02, 0x11, 0xDC, 0x01, 0x02, 0x83, 0x20, 0x21, 0x85, 0x10, 0x3E, 0x01, + 0x02, 0x11, 0xFC, 0x00, 0x02, 0x83, 0x21, 0x85, 0x11, 0xDC, 0x01, 0x02, + 0x83, 0x20, 0x21, 0x85, 0x10, 0x1B, 0x01, 0x02, 0x11, 0xFC, 0x00, 0x02, + 0x83, 0x21, 0x85, 0x11, 0xDC, 0x01, 0x02, 0x83, 0x20, 0x21, 0x85, 0x10, + 0xFC, 0x00, 0x02, 0x11, 0xFC, 0x00, 0x02, 0x8B, 0x21, 0x88, 0x20, 0x86, + 0x00, +}; + +static const uint8_t gStSong1[119] = { + 0x4A, 0x59, 0x4D, 0x31, 0x14, 0x00, 0xFF, 0xFF, 0x81, 0x10, 0xFC, 0x00, + 0x07, 0x11, 0x9F, 0x00, 0x07, 0x87, 0x20, 0x21, 0x81, 0x10, 0x1B, 0x01, + 0x07, 0x11, 0xA8, 0x00, 0x07, 0x87, 0x20, 0x21, 0x81, 0x10, 0xFC, 0x00, + 0x07, 0x11, 0xBD, 0x00, 0x07, 0x87, 0x20, 0x21, 0x81, 0x10, 0x1B, 0x01, + 0x07, 0x11, 0xD4, 0x00, 0x07, 0x87, 0x20, 0x21, 0x81, 0x10, 0x3E, 0x01, + 0x07, 0x11, 0xBD, 0x00, 0x07, 0x87, 0x20, 0x21, 0x81, 0x10, 0x50, 0x01, + 0x07, 0x11, 0xA8, 0x00, 0x07, 0x87, 0x20, 0x21, 0x81, 0x10, 0x3E, 0x01, + 0x07, 0x11, 0x9F, 0x00, 0x07, 0x87, 0x20, 0x21, 0x81, 0x10, 0x50, 0x01, + 0x07, 0x11, 0xA8, 0x00, 0x07, 0x87, 0x20, 0x21, 0x81, 0x10, 0x7A, 0x01, + 0x07, 0x11, 0xBD, 0x00, 0x07, 0x87, 0x21, 0x90, 0x20, 0x82, 0x00, +}; + +static const uint8_t gStSong2[128] = { + 0x4A, 0x59, 0x4D, 0x31, 0x14, 0x00, 0xFF, 0xFF, 0x81, 0x10, 0x7E, 0x00, + 0x00, 0x11, 0x9F, 0x00, 0x00, 0x85, 0x21, 0x82, 0x20, 0x81, 0x10, 0x7E, + 0x00, 0x00, 0x11, 0xA8, 0x00, 0x00, 0x85, 0x21, 0x82, 0x20, 0x81, 0x10, + 0x8D, 0x00, 0x00, 0x11, 0xBD, 0x00, 0x00, 0x85, 0x21, 0x82, 0x20, 0x81, + 0x10, 0x9F, 0x00, 0x00, 0x11, 0x9F, 0x00, 0x00, 0x85, 0x21, 0x82, 0x20, + 0x81, 0x10, 0x9F, 0x00, 0x00, 0x11, 0xA8, 0x00, 0x00, 0x85, 0x21, 0x82, + 0x20, 0x81, 0x10, 0xA8, 0x00, 0x00, 0x11, 0xBD, 0x00, 0x00, 0x85, 0x21, + 0x82, 0x20, 0x81, 0x10, 0xA8, 0x00, 0x00, 0x11, 0x9F, 0x00, 0x00, 0x85, + 0x21, 0x82, 0x20, 0x81, 0x10, 0xBD, 0x00, 0x00, 0x11, 0xA8, 0x00, 0x00, + 0x85, 0x21, 0x82, 0x20, 0x81, 0x11, 0x9F, 0x00, 0x00, 0x85, 0x21, 0x83, + 0x11, 0x7E, 0x00, 0x00, 0x85, 0x21, 0x84, 0x00, +}; + +static const uint8_t gStSong3[154] = { + 0x4A, 0x59, 0x4D, 0x31, 0x14, 0x00, 0xFF, 0xFF, 0x81, 0x10, 0xFC, 0x00, + 0x00, 0x11, 0x50, 0x01, 0x02, 0x12, 0x50, 0x01, 0x02, 0x83, 0x20, 0x21, + 0x22, 0x82, 0x10, 0x1B, 0x01, 0x00, 0x83, 0x20, 0x82, 0x10, 0x3E, 0x01, + 0x00, 0x11, 0x50, 0x01, 0x02, 0x12, 0x50, 0x01, 0x02, 0x83, 0x20, 0x21, + 0x22, 0x82, 0x10, 0xFC, 0x00, 0x00, 0x83, 0x20, 0x82, 0x10, 0x1B, 0x01, + 0x00, 0x11, 0x50, 0x01, 0x02, 0x12, 0x50, 0x01, 0x02, 0x83, 0x20, 0x21, + 0x22, 0x82, 0x10, 0xFC, 0x00, 0x00, 0x83, 0x20, 0x82, 0x10, 0x1B, 0x01, + 0x00, 0x11, 0x50, 0x01, 0x02, 0x12, 0x50, 0x01, 0x02, 0x83, 0x20, 0x21, + 0x22, 0x82, 0x10, 0xDC, 0x01, 0x00, 0x83, 0x20, 0x82, 0x11, 0x3E, 0x01, + 0x02, 0x12, 0x7A, 0x01, 0x02, 0x83, 0x21, 0x22, 0x87, 0x11, 0x50, 0x01, + 0x02, 0x12, 0x7A, 0x01, 0x02, 0x83, 0x21, 0x22, 0x87, 0x11, 0x1B, 0x01, + 0x02, 0x12, 0xFC, 0x00, 0x02, 0x83, 0x21, 0x22, 0x87, 0x11, 0xFC, 0x00, + 0x02, 0x12, 0xDC, 0x01, 0x02, 0x83, 0x21, 0x22, 0x88, 0x00, +}; + +static const uint8_t gStSong4[92] = { + 0x4A, 0x59, 0x4D, 0x31, 0x14, 0x00, 0xFF, 0xFF, 0x81, 0x10, 0xF8, 0x01, + 0x05, 0x11, 0xC1, 0x01, 0x05, 0x87, 0x20, 0x21, 0x89, 0x10, 0xF8, 0x01, + 0x05, 0x11, 0xC1, 0x01, 0x05, 0x87, 0x20, 0x21, 0x81, 0x10, 0xA1, 0x02, + 0x05, 0x11, 0xF8, 0x01, 0x05, 0x97, 0x20, 0x21, 0x99, 0x10, 0xF8, 0x01, + 0x05, 0x11, 0xA8, 0x01, 0x05, 0x87, 0x20, 0x21, 0x89, 0x10, 0xA1, 0x02, + 0x05, 0x11, 0xF8, 0x01, 0x05, 0x87, 0x20, 0x21, 0x81, 0x10, 0x50, 0x03, + 0x05, 0x11, 0xA1, 0x02, 0x05, 0x88, 0x11, 0x7B, 0x02, 0x05, 0x88, 0x11, + 0xA1, 0x02, 0x05, 0x87, 0x20, 0x21, 0x82, 0x00, +}; + +static const uint8_t gStSong5[890] = { + 0x4A, 0x59, 0x4D, 0x31, 0x14, 0x00, 0xFF, 0xFF, 0x81, 0x10, 0xF8, 0x01, + 0x02, 0x11, 0x9F, 0x00, 0x02, 0x12, 0x6A, 0x00, 0x0A, 0x83, 0x21, 0x82, + 0x12, 0xD4, 0x00, 0x0A, 0x85, 0x11, 0x9F, 0x00, 0x02, 0x12, 0x6A, 0x00, + 0x0A, 0x83, 0x21, 0x82, 0x12, 0xD4, 0x00, 0x0A, 0x85, 0x11, 0x9F, 0x00, + 0x02, 0x12, 0x6A, 0x00, 0x0A, 0x83, 0x21, 0x82, 0x12, 0xD4, 0x00, 0x0A, + 0x85, 0x11, 0x9F, 0x00, 0x02, 0x12, 0x6A, 0x00, 0x0A, 0x83, 0x21, 0x81, + 0x20, 0x81, 0x12, 0xD4, 0x00, 0x0A, 0x85, 0x10, 0xF8, 0x01, 0x02, 0x11, + 0x9F, 0x00, 0x02, 0x12, 0x6A, 0x00, 0x0A, 0x83, 0x21, 0x82, 0x12, 0xD4, + 0x00, 0x0A, 0x85, 0x11, 0x9F, 0x00, 0x02, 0x12, 0x6A, 0x00, 0x0A, 0x83, + 0x21, 0x82, 0x12, 0xD4, 0x00, 0x0A, 0x85, 0x11, 0x9F, 0x00, 0x02, 0x12, + 0x6A, 0x00, 0x0A, 0x83, 0x21, 0x82, 0x12, 0xD4, 0x00, 0x0A, 0x85, 0x11, + 0x9F, 0x00, 0x02, 0x12, 0x6A, 0x00, 0x0A, 0x83, 0x21, 0x81, 0x20, 0x81, + 0x12, 0xD4, 0x00, 0x0A, 0x85, 0x10, 0xF8, 0x01, 0x02, 0x11, 0x9F, 0x00, + 0x02, 0x12, 0x6A, 0x00, 0x0A, 0x83, 0x21, 0x82, 0x12, 0xD4, 0x00, 0x0A, + 0x85, 0x11, 0x9F, 0x00, 0x02, 0x12, 0x6A, 0x00, 0x0A, 0x83, 0x21, 0x82, + 0x12, 0xD4, 0x00, 0x0A, 0x85, 0x11, 0x9F, 0x00, 0x02, 0x12, 0x6A, 0x00, + 0x0A, 0x83, 0x21, 0x81, 0x20, 0x81, 0x12, 0xD4, 0x00, 0x0A, 0x83, 0x22, + 0x82, 0x10, 0x36, 0x02, 0x02, 0x11, 0x8D, 0x00, 0x02, 0x12, 0x64, 0x00, + 0x0A, 0x83, 0x21, 0x81, 0x20, 0x81, 0x12, 0xC8, 0x00, 0x0A, 0x85, 0x10, + 0x36, 0x02, 0x02, 0x11, 0x8D, 0x00, 0x02, 0x12, 0x64, 0x00, 0x0A, 0x83, + 0x21, 0x82, 0x12, 0xC8, 0x00, 0x0A, 0x85, 0x11, 0x8D, 0x00, 0x02, 0x12, + 0x64, 0x00, 0x0A, 0x83, 0x21, 0x82, 0x12, 0xC8, 0x00, 0x0A, 0x85, 0x11, + 0x8D, 0x00, 0x02, 0x12, 0x64, 0x00, 0x0A, 0x83, 0x21, 0x82, 0x12, 0xC8, + 0x00, 0x0A, 0x83, 0x22, 0x82, 0x11, 0x8D, 0x00, 0x02, 0x12, 0x64, 0x00, + 0x0A, 0x83, 0x21, 0x81, 0x20, 0x81, 0x12, 0xC8, 0x00, 0x0A, 0x85, 0x10, + 0x7B, 0x02, 0x02, 0x11, 0xF8, 0x01, 0x02, 0x12, 0x5E, 0x00, 0x0A, 0x84, + 0x20, 0x21, 0x81, 0x12, 0xBD, 0x00, 0x0A, 0x85, 0x12, 0x5E, 0x00, 0x0A, + 0x85, 0x12, 0xBD, 0x00, 0x0A, 0x85, 0x10, 0xA1, 0x02, 0x02, 0x11, 0x36, + 0x02, 0x02, 0x12, 0x54, 0x00, 0x0A, 0x84, 0x20, 0x21, 0x81, 0x12, 0xA8, + 0x00, 0x0A, 0x85, 0x12, 0x54, 0x00, 0x0A, 0x85, 0x12, 0xA8, 0x00, 0x0A, + 0x85, 0x10, 0xF3, 0x02, 0x02, 0x11, 0x7B, 0x02, 0x02, 0x12, 0x4F, 0x00, + 0x0A, 0x84, 0x20, 0x21, 0x81, 0x12, 0x9F, 0x00, 0x0A, 0x85, 0x12, 0x4F, + 0x00, 0x0A, 0x85, 0x12, 0x9F, 0x00, 0x0A, 0x85, 0x10, 0x20, 0x03, 0x02, + 0x11, 0x7B, 0x02, 0x02, 0x12, 0x47, 0x00, 0x0A, 0x84, 0x20, 0x21, 0x81, + 0x12, 0x8D, 0x00, 0x0A, 0x85, 0x12, 0x47, 0x00, 0x0A, 0x85, 0x12, 0x8D, + 0x00, 0x0A, 0x85, 0x10, 0x50, 0x03, 0x02, 0x11, 0x7B, 0x02, 0x02, 0x12, + 0x43, 0x00, 0x0A, 0x85, 0x12, 0x85, 0x00, 0x0A, 0x85, 0x12, 0x43, 0x00, + 0x0A, 0x85, 0x12, 0x85, 0x00, 0x0A, 0x85, 0x12, 0x43, 0x00, 0x0A, 0x85, + 0x12, 0x85, 0x00, 0x0A, 0x85, 0x12, 0x43, 0x00, 0x0A, 0x85, 0x12, 0x85, + 0x00, 0x0A, 0x85, 0x10, 0xA8, 0x01, 0x02, 0x11, 0x3E, 0x01, 0x02, 0x12, + 0x43, 0x00, 0x0A, 0x84, 0x20, 0x21, 0x81, 0x12, 0x85, 0x00, 0x0A, 0x85, + 0x22, 0x9E, 0x10, 0x50, 0x03, 0x02, 0x11, 0x50, 0x01, 0x02, 0x12, 0xFC, + 0x00, 0x0A, 0xA2, 0x20, 0x21, 0x84, 0x22, 0x82, 0x10, 0x50, 0x03, 0x02, + 0x11, 0x50, 0x01, 0x02, 0x12, 0xFC, 0x00, 0x0A, 0xA2, 0x20, 0x21, 0x84, + 0x22, 0x82, 0x10, 0x50, 0x03, 0x02, 0x11, 0x50, 0x01, 0x02, 0x12, 0xFC, + 0x00, 0x0A, 0x98, 0x20, 0x21, 0x84, 0x22, 0x82, 0x10, 0xF3, 0x02, 0x02, + 0x11, 0x3E, 0x01, 0x02, 0x12, 0xE1, 0x00, 0x0A, 0x84, 0x20, 0x21, 0x84, + 0x22, 0x82, 0x10, 0xF3, 0x02, 0x02, 0x11, 0x3E, 0x01, 0x02, 0x12, 0xE1, + 0x00, 0x0A, 0xA2, 0x20, 0x21, 0x84, 0x22, 0x82, 0x10, 0xA1, 0x02, 0x02, + 0x11, 0x1B, 0x01, 0x02, 0x12, 0xD4, 0x00, 0x0A, 0xA2, 0x21, 0x84, 0x22, + 0x82, 0x10, 0x7B, 0x02, 0x02, 0x11, 0xFC, 0x00, 0x02, 0x12, 0xBD, 0x00, + 0x0A, 0x84, 0x20, 0x21, 0x84, 0x22, 0x8C, 0x10, 0x36, 0x02, 0x02, 0x11, + 0x1B, 0x01, 0x02, 0x12, 0xA8, 0x00, 0x0A, 0x84, 0x20, 0x21, 0x84, 0x22, + 0x8C, 0x10, 0xF3, 0x02, 0x02, 0x11, 0x3E, 0x01, 0x02, 0x12, 0x7E, 0x00, + 0x0A, 0x85, 0x12, 0xBD, 0x00, 0x0A, 0x85, 0x12, 0x7E, 0x00, 0x0A, 0x85, + 0x12, 0xBD, 0x00, 0x0A, 0x85, 0x12, 0x7E, 0x00, 0x0A, 0x85, 0x12, 0xBD, + 0x00, 0x0A, 0x85, 0x12, 0x7E, 0x00, 0x0A, 0x85, 0x12, 0xBD, 0x00, 0x0A, + 0x85, 0x12, 0x7E, 0x00, 0x0A, 0x84, 0x20, 0x81, 0x12, 0xBD, 0x00, 0x0A, + 0x85, 0x10, 0x7B, 0x02, 0x02, 0x12, 0x7E, 0x00, 0x0A, 0x84, 0x20, 0x81, + 0x12, 0xBD, 0x00, 0x0A, 0x85, 0x10, 0xF8, 0x01, 0x02, 0x12, 0x7E, 0x00, + 0x0A, 0x84, 0x20, 0x81, 0x12, 0xBD, 0x00, 0x0A, 0x85, 0x10, 0x7B, 0x02, + 0x02, 0x12, 0x7E, 0x00, 0x0A, 0x84, 0x20, 0x21, 0x81, 0x12, 0xBD, 0x00, + 0x0A, 0x83, 0x22, 0x82, 0x10, 0xF3, 0x02, 0x02, 0x11, 0x1B, 0x01, 0x02, + 0x12, 0x77, 0x00, 0x0A, 0x85, 0x12, 0xBD, 0x00, 0x0A, 0x85, 0x12, 0x77, + 0x00, 0x0A, 0x84, 0x20, 0x81, 0x12, 0xBD, 0x00, 0x0A, 0x85, 0x10, 0x50, + 0x03, 0x02, 0x12, 0x77, 0x00, 0x0A, 0x85, 0x12, 0xBD, 0x00, 0x0A, 0x85, + 0x12, 0x77, 0x00, 0x0A, 0x84, 0x20, 0x81, 0x12, 0xBD, 0x00, 0x0A, 0x85, + 0x10, 0xF3, 0x02, 0x02, 0x12, 0x77, 0x00, 0x0A, 0x84, 0x20, 0x81, 0x12, + 0xBD, 0x00, 0x0A, 0x85, 0x10, 0xA1, 0x02, 0x02, 0x12, 0x77, 0x00, 0x0A, + 0x84, 0x20, 0x81, 0x12, 0xBD, 0x00, 0x0A, 0x85, 0x10, 0x7B, 0x02, 0x02, + 0x12, 0x77, 0x00, 0x0A, 0x84, 0x20, 0x81, 0x12, 0xBD, 0x00, 0x0A, 0x85, + 0x10, 0x36, 0x02, 0x02, 0x12, 0x77, 0x00, 0x0A, 0x84, 0x20, 0x21, 0x81, + 0x12, 0xBD, 0x00, 0x0A, 0x83, 0x22, 0x82, 0x10, 0xF8, 0x01, 0x02, 0x11, + 0x3E, 0x01, 0x02, 0x12, 0x7E, 0x00, 0x0A, 0x84, 0x20, 0x21, 0x84, 0x22, + 0x83, 0x00, +}; + +static const uint8_t gStSong6[611] = { + 0x4A, 0x59, 0x4D, 0x31, 0x14, 0x00, 0xFF, 0xFF, 0x81, 0x11, 0x7A, 0x01, + 0x01, 0x12, 0xF8, 0x01, 0x01, 0x88, 0x21, 0x22, 0x90, 0x11, 0x7A, 0x01, + 0x01, 0x12, 0xF8, 0x01, 0x01, 0x82, 0x21, 0x22, 0x84, 0x11, 0x7A, 0x01, + 0x01, 0x12, 0xF8, 0x01, 0x01, 0x82, 0x21, 0x22, 0x84, 0x11, 0x7A, 0x01, + 0x01, 0x12, 0xF8, 0x01, 0x01, 0xA0, 0x21, 0x22, 0x84, 0x10, 0x5E, 0x00, + 0x00, 0x86, 0x20, 0x86, 0x10, 0x7E, 0x00, 0x00, 0x86, 0x20, 0x86, 0x10, + 0x9F, 0x00, 0x00, 0x86, 0x20, 0x86, 0x10, 0xBD, 0x00, 0x00, 0x11, 0x50, + 0x01, 0x01, 0x12, 0xC1, 0x01, 0x01, 0x88, 0x21, 0x22, 0x84, 0x11, 0x2C, + 0x01, 0x01, 0x12, 0x90, 0x01, 0x01, 0x88, 0x21, 0x22, 0x84, 0x11, 0x50, + 0x01, 0x01, 0x12, 0xC1, 0x01, 0x01, 0x88, 0x21, 0x22, 0x84, 0x11, 0x7A, + 0x01, 0x01, 0x12, 0xF8, 0x01, 0x01, 0x88, 0x21, 0x22, 0x90, 0x11, 0x7A, + 0x01, 0x01, 0x12, 0xF8, 0x01, 0x01, 0x82, 0x21, 0x22, 0x84, 0x20, 0x11, + 0x7A, 0x01, 0x01, 0x12, 0xF8, 0x01, 0x01, 0x82, 0x21, 0x22, 0x84, 0x11, + 0x7A, 0x01, 0x01, 0x12, 0xF8, 0x01, 0x01, 0x92, 0x10, 0xD4, 0x00, 0x00, + 0x83, 0x20, 0x8B, 0x21, 0x22, 0x84, 0x10, 0xBD, 0x00, 0x00, 0x83, 0x20, + 0x8F, 0x10, 0x9F, 0x00, 0x00, 0x83, 0x20, 0x83, 0x11, 0x7A, 0x01, 0x01, + 0x12, 0xF8, 0x01, 0x01, 0x82, 0x21, 0x22, 0x84, 0x11, 0x7A, 0x01, 0x01, + 0x12, 0xF8, 0x01, 0x01, 0x82, 0x21, 0x22, 0x84, 0x10, 0x8D, 0x00, 0x00, + 0x11, 0xA8, 0x01, 0x01, 0x12, 0x36, 0x02, 0x01, 0x88, 0x21, 0x22, 0x84, + 0x11, 0x7A, 0x01, 0x01, 0x12, 0xF8, 0x01, 0x01, 0x88, 0x21, 0x22, 0x84, + 0x11, 0xA8, 0x01, 0x01, 0x12, 0x36, 0x02, 0x01, 0x86, 0x20, 0x82, 0x21, + 0x22, 0x84, 0x10, 0x77, 0x00, 0x00, 0x11, 0xDC, 0x01, 0x01, 0x12, 0x7B, + 0x02, 0x01, 0x88, 0x21, 0x22, 0x90, 0x11, 0xDC, 0x01, 0x01, 0x12, 0x7B, + 0x02, 0x01, 0x82, 0x21, 0x22, 0x84, 0x11, 0xDC, 0x01, 0x01, 0x12, 0x7B, + 0x02, 0x01, 0x82, 0x21, 0x22, 0x84, 0x11, 0xDC, 0x01, 0x01, 0x12, 0x7B, + 0x02, 0x01, 0x92, 0x20, 0x8E, 0x21, 0x22, 0x84, 0x10, 0x3B, 0x00, 0x00, + 0x98, 0x11, 0xDC, 0x01, 0x01, 0x12, 0x7B, 0x02, 0x01, 0x82, 0x21, 0x22, + 0x84, 0x11, 0xDC, 0x01, 0x01, 0x12, 0x7B, 0x02, 0x01, 0x82, 0x21, 0x22, + 0x84, 0x11, 0xDC, 0x01, 0x01, 0x12, 0x7B, 0x02, 0x01, 0x88, 0x21, 0x22, + 0x84, 0x11, 0xA8, 0x01, 0x01, 0x12, 0x36, 0x02, 0x01, 0x86, 0x20, 0x82, + 0x21, 0x22, 0x84, 0x11, 0xDC, 0x01, 0x01, 0x12, 0x7B, 0x02, 0x01, 0x88, + 0x21, 0x22, 0x84, 0x10, 0x7E, 0x00, 0x00, 0x11, 0xF8, 0x01, 0x01, 0x12, + 0xA1, 0x02, 0x01, 0x88, 0x21, 0x22, 0x90, 0x11, 0xF8, 0x01, 0x01, 0x12, + 0xA1, 0x02, 0x01, 0x82, 0x21, 0x22, 0x84, 0x11, 0xF8, 0x01, 0x01, 0x12, + 0xA1, 0x02, 0x01, 0x82, 0x21, 0x22, 0x84, 0x11, 0xF8, 0x01, 0x01, 0x12, + 0xA1, 0x02, 0x01, 0x92, 0x20, 0x8E, 0x21, 0x22, 0x84, 0x10, 0x3F, 0x00, + 0x00, 0x98, 0x11, 0xF8, 0x01, 0x01, 0x12, 0xA1, 0x02, 0x01, 0x82, 0x21, + 0x22, 0x84, 0x20, 0x11, 0xF8, 0x01, 0x01, 0x12, 0xA1, 0x02, 0x01, 0x82, + 0x21, 0x22, 0x84, 0x10, 0x7E, 0x00, 0x00, 0x11, 0x36, 0x02, 0x01, 0x12, + 0xF3, 0x02, 0x01, 0x88, 0x21, 0x22, 0x84, 0x11, 0xF8, 0x01, 0x01, 0x12, + 0xA1, 0x02, 0x01, 0x86, 0x20, 0x82, 0x21, 0x22, 0x84, 0x11, 0x36, 0x02, + 0x01, 0x12, 0xF3, 0x02, 0x01, 0x88, 0x21, 0x22, 0x84, 0x10, 0x8D, 0x00, + 0x00, 0x11, 0x7B, 0x02, 0x01, 0x12, 0x50, 0x03, 0x01, 0xB2, 0x21, 0x84, + 0x20, 0x82, 0x22, 0x90, 0x10, 0x47, 0x00, 0x00, 0x98, 0x11, 0x7B, 0x02, + 0x01, 0x12, 0x50, 0x03, 0x01, 0x82, 0x21, 0x22, 0x84, 0x20, 0x11, 0x7B, + 0x02, 0x01, 0x12, 0x50, 0x03, 0x01, 0x82, 0x21, 0x22, 0x84, 0x10, 0x8D, + 0x00, 0x00, 0x11, 0x36, 0x02, 0x01, 0x12, 0x50, 0x03, 0x01, 0x88, 0x21, + 0x22, 0x84, 0x11, 0x36, 0x02, 0x01, 0x12, 0x50, 0x03, 0x01, 0x88, 0x21, + 0x22, 0x84, 0x11, 0x36, 0x02, 0x01, 0x12, 0x50, 0x03, 0x01, 0x86, 0x20, + 0x82, 0x21, 0x22, 0x84, 0x10, 0x9F, 0x00, 0x00, 0x11, 0x16, 0x02, 0x01, + 0x12, 0x50, 0x03, 0x01, 0xEC, 0x20, 0x22, 0x82, 0x21, 0x85, 0x00, +}; + +static const uint8_t gStSong7[777] = { + 0x4A, 0x59, 0x4D, 0x31, 0x14, 0x00, 0xFF, 0xFF, 0x81, 0x10, 0x7A, 0x01, + 0x00, 0x11, 0xF6, 0x04, 0x05, 0x12, 0x7E, 0x00, 0x07, 0x8A, 0x22, 0x84, + 0x21, 0x86, 0x11, 0x42, 0x05, 0x05, 0x85, 0x11, 0xF6, 0x04, 0x05, 0x84, + 0x21, 0x81, 0x11, 0x6C, 0x04, 0x05, 0x12, 0x77, 0x00, 0x07, 0x84, 0x21, + 0x86, 0x11, 0xF6, 0x04, 0x05, 0x22, 0x84, 0x21, 0x86, 0x11, 0x42, 0x05, + 0x05, 0x83, 0x20, 0x81, 0x21, 0x86, 0x10, 0xFC, 0x00, 0x00, 0x11, 0xF6, + 0x04, 0x05, 0x12, 0x6A, 0x00, 0x07, 0x84, 0x21, 0x86, 0x11, 0x42, 0x05, + 0x05, 0x22, 0x84, 0x21, 0x86, 0x11, 0xE6, 0x05, 0x05, 0x83, 0x20, 0x81, + 0x21, 0x86, 0x10, 0x1B, 0x01, 0x00, 0x11, 0xF0, 0x03, 0x05, 0x12, 0x5E, + 0x00, 0x07, 0x8A, 0x10, 0x3E, 0x01, 0x00, 0x22, 0x84, 0x21, 0x86, 0x10, + 0x50, 0x01, 0x00, 0x85, 0x11, 0xF0, 0x03, 0x05, 0x85, 0x10, 0x7A, 0x01, + 0x00, 0x12, 0x7E, 0x00, 0x07, 0x8A, 0x22, 0x85, 0x11, 0xB8, 0x03, 0x05, + 0x85, 0x11, 0x50, 0x03, 0x05, 0x85, 0x11, 0xF3, 0x02, 0x05, 0x85, 0x11, + 0x6C, 0x04, 0x05, 0x12, 0x77, 0x00, 0x07, 0x84, 0x21, 0x86, 0x11, 0xF6, + 0x04, 0x05, 0x22, 0x84, 0x21, 0x86, 0x11, 0x42, 0x05, 0x05, 0x84, 0x21, + 0x86, 0x11, 0xE6, 0x05, 0x05, 0x12, 0x7E, 0x00, 0x07, 0x83, 0x20, 0x81, + 0x21, 0x86, 0x11, 0xE6, 0x05, 0x05, 0x22, 0x84, 0x21, 0x86, 0x11, 0xE6, + 0x05, 0x05, 0x84, 0x21, 0x86, 0x10, 0xA8, 0x01, 0x00, 0x11, 0x6C, 0x04, + 0x05, 0x12, 0x5E, 0x00, 0x07, 0x89, 0x21, 0x81, 0x10, 0x7A, 0x01, 0x00, + 0x22, 0x8A, 0x10, 0x50, 0x01, 0x00, 0x11, 0x6C, 0x04, 0x05, 0x8A, 0x10, + 0x7A, 0x01, 0x00, 0x11, 0xF6, 0x04, 0x05, 0x12, 0x7E, 0x00, 0x07, 0x89, + 0x21, 0x81, 0x22, 0x83, 0x20, 0x91, 0x10, 0x50, 0x01, 0x00, 0x11, 0xB8, + 0x03, 0x05, 0x12, 0x77, 0x00, 0x07, 0x89, 0x21, 0x81, 0x10, 0x3E, 0x01, + 0x00, 0x22, 0x8A, 0x10, 0x1B, 0x01, 0x00, 0x11, 0xB8, 0x03, 0x05, 0x8A, + 0x10, 0x3E, 0x01, 0x00, 0x11, 0xF0, 0x03, 0x05, 0x12, 0x6A, 0x00, 0x07, + 0x89, 0x21, 0x81, 0x22, 0x83, 0x20, 0x91, 0x10, 0x1B, 0x01, 0x00, 0x11, + 0xF3, 0x02, 0x05, 0x12, 0x5E, 0x00, 0x07, 0x89, 0x21, 0x81, 0x10, 0xFC, + 0x00, 0x00, 0x22, 0x8A, 0x10, 0xEE, 0x00, 0x00, 0x11, 0xF3, 0x02, 0x05, + 0x89, 0x21, 0x81, 0x10, 0xFC, 0x00, 0x00, 0x11, 0xF0, 0x03, 0x05, 0x12, + 0x7E, 0x00, 0x07, 0x83, 0x20, 0x87, 0x11, 0xB8, 0x03, 0x05, 0x22, 0x8A, + 0x10, 0x3E, 0x01, 0x00, 0x11, 0xF0, 0x03, 0x05, 0x89, 0x21, 0x81, 0x10, + 0xBD, 0x00, 0x00, 0x11, 0x6C, 0x04, 0x05, 0x12, 0x77, 0x00, 0x07, 0x8A, + 0x11, 0xF0, 0x03, 0x05, 0x22, 0x8A, 0x11, 0x6C, 0x04, 0x05, 0x8A, 0x11, + 0xF6, 0x04, 0x05, 0x12, 0x6A, 0x00, 0x07, 0x8A, 0x11, 0x6C, 0x04, 0x05, + 0x22, 0x83, 0x20, 0x87, 0x11, 0xF6, 0x04, 0x05, 0x8A, 0x10, 0xEE, 0x00, + 0x00, 0x11, 0x42, 0x05, 0x05, 0x12, 0x5E, 0x00, 0x07, 0x8A, 0x10, 0xFC, + 0x00, 0x00, 0x11, 0xF6, 0x04, 0x05, 0x22, 0x8A, 0x10, 0x1B, 0x01, 0x00, + 0x11, 0x42, 0x05, 0x05, 0x89, 0x21, 0x81, 0x10, 0xFC, 0x00, 0x00, 0x11, + 0xF6, 0x04, 0x05, 0x12, 0x7E, 0x00, 0x07, 0x83, 0x20, 0x86, 0x21, 0x81, + 0x22, 0x8A, 0x11, 0xF6, 0x04, 0x05, 0x8A, 0x10, 0xEE, 0x00, 0x00, 0x11, + 0xE6, 0x05, 0x05, 0x12, 0x77, 0x00, 0x07, 0x8A, 0x10, 0xFC, 0x00, 0x00, + 0x22, 0x8A, 0x10, 0x1B, 0x01, 0x00, 0x8A, 0x10, 0x3E, 0x01, 0x00, 0x12, + 0x6A, 0x00, 0x07, 0x83, 0x20, 0x86, 0x21, 0x81, 0x22, 0x94, 0x10, 0x1B, + 0x01, 0x00, 0x11, 0xE6, 0x05, 0x05, 0x12, 0x5E, 0x00, 0x07, 0x83, 0x20, + 0x87, 0x22, 0x89, 0x21, 0x81, 0x10, 0x1B, 0x01, 0x00, 0x8A, 0x10, 0x7A, + 0x01, 0x00, 0x11, 0x6C, 0x04, 0x05, 0x12, 0x8D, 0x00, 0x07, 0x89, 0x21, + 0x81, 0x22, 0x8A, 0x11, 0xF0, 0x03, 0x05, 0x8A, 0x11, 0xB8, 0x03, 0x05, + 0x12, 0x77, 0x00, 0x07, 0x8A, 0x11, 0xF0, 0x03, 0x05, 0x22, 0x8A, 0x11, + 0x6C, 0x04, 0x05, 0x8A, 0x11, 0xF3, 0x02, 0x05, 0x12, 0x6A, 0x00, 0x07, + 0x83, 0x20, 0x86, 0x21, 0x81, 0x22, 0x8A, 0x10, 0x7A, 0x01, 0x00, 0x11, + 0x6C, 0x04, 0x05, 0x81, 0x20, 0x84, 0x10, 0x7A, 0x01, 0x00, 0x81, 0x20, + 0x84, 0x10, 0x7A, 0x01, 0x00, 0x11, 0xAF, 0x04, 0x05, 0x12, 0x5E, 0x00, + 0x07, 0x83, 0x20, 0x87, 0x10, 0x7A, 0x01, 0x00, 0x22, 0x83, 0x20, 0x86, + 0x21, 0x81, 0x10, 0x7A, 0x01, 0x00, 0x83, 0x20, 0x87, 0x10, 0x7A, 0x01, + 0x00, 0x11, 0xF6, 0x04, 0x05, 0x12, 0x7E, 0x00, 0x07, 0x85, 0x11, 0xE6, + 0x05, 0x05, 0x85, 0x11, 0xF6, 0x04, 0x05, 0x85, 0x11, 0xE6, 0x05, 0x05, + 0x85, 0x11, 0xF6, 0x04, 0x05, 0x84, 0x12, 0x5E, 0x00, 0x07, 0x81, 0x11, + 0xE6, 0x05, 0x05, 0x85, 0x11, 0xF6, 0x04, 0x05, 0x85, 0x11, 0xE6, 0x05, + 0x05, 0x85, 0x11, 0xF6, 0x04, 0x05, 0x85, 0x11, 0xE6, 0x05, 0x05, 0x83, + 0x12, 0x7E, 0x00, 0x07, 0x82, 0x11, 0xF6, 0x04, 0x05, 0x85, 0x11, 0xE6, + 0x05, 0x05, 0x85, 0x11, 0xF6, 0x04, 0x05, 0x85, 0x11, 0xE6, 0x05, 0x05, + 0x85, 0x11, 0xF6, 0x04, 0x05, 0x82, 0x12, 0x5E, 0x00, 0x07, 0x83, 0x11, + 0xE6, 0x05, 0x05, 0x85, 0x11, 0xF6, 0x04, 0x05, 0x85, 0x11, 0xE6, 0x05, + 0x05, 0x85, 0x11, 0xF6, 0x04, 0x05, 0x81, 0x22, 0x85, 0x12, 0x7E, 0x00, + 0x07, 0x88, 0x21, 0x89, 0x20, 0x88, 0x22, 0x86, 0x00, +}; + +static const uint8_t gStSong8[2338] = { + 0x4A, 0x59, 0x4D, 0x31, 0x14, 0x00, 0x00, 0x00, 0x81, 0x10, 0x1F, 0x00, + 0x07, 0x11, 0x7E, 0x00, 0x01, 0x13, 0x1C, 0x00, 0x0A, 0x82, 0x21, 0x84, + 0x13, 0x1D, 0x00, 0x0A, 0x84, 0x11, 0xB2, 0x00, 0x01, 0x82, 0x21, 0x13, + 0x1D, 0x00, 0x0A, 0x86, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x11, 0xD4, 0x00, + 0x01, 0x82, 0x21, 0x82, 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x11, 0xFC, 0x00, + 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1E, 0x00, 0x0A, + 0x84, 0x11, 0x50, 0x01, 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, + 0x10, 0x21, 0x00, 0x07, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0xC1, 0x01, + 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0xF8, 0x01, + 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1E, 0x00, 0x0A, + 0x84, 0x11, 0x57, 0x02, 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, + 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0xF3, 0x02, 0x01, 0x82, 0x21, 0x82, + 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0x82, 0x03, 0x01, 0x13, 0x1E, 0x00, + 0x0A, 0x82, 0x21, 0x84, 0x10, 0x23, 0x00, 0x07, 0x13, 0x1E, 0x00, 0x0A, + 0x84, 0x11, 0xF0, 0x03, 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, + 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x11, 0x42, 0x05, 0x01, 0x82, 0x21, 0x82, + 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x11, 0x31, 0x06, 0x01, 0x13, 0x1D, 0x00, + 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1D, 0x00, 0x0A, 0x84, 0x11, 0xF6, 0x04, + 0x01, 0x82, 0x21, 0x13, 0x1C, 0x00, 0x0A, 0x86, 0x13, 0x1D, 0x00, 0x0A, + 0x82, 0x11, 0xF0, 0x03, 0x01, 0x82, 0x21, 0x82, 0x10, 0x21, 0x00, 0x07, + 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x11, 0x20, 0x03, 0x01, 0x13, 0x1D, 0x00, + 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1D, 0x00, 0x0A, 0x84, 0x11, 0xA1, 0x02, + 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, + 0x82, 0x11, 0x36, 0x02, 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, + 0x86, 0x11, 0xF8, 0x01, 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, + 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0xC1, 0x01, 0x01, 0x82, 0x10, 0x1F, + 0x00, 0x07, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, + 0x82, 0x11, 0x7A, 0x01, 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, + 0x86, 0x11, 0x3E, 0x01, 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, + 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0xFC, 0x00, 0x01, 0x82, 0x21, 0x13, + 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0xD4, 0x00, + 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x10, 0x21, 0x00, + 0x07, 0x11, 0x9F, 0x00, 0x01, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x21, 0x84, + 0x13, 0x1D, 0x00, 0x0A, 0x84, 0x11, 0x7E, 0x00, 0x01, 0x82, 0x21, 0x13, + 0x1D, 0x00, 0x0A, 0x86, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x11, 0xB2, 0x00, + 0x01, 0x82, 0x21, 0x82, 0x13, 0x1C, 0x00, 0x0A, 0x86, 0x11, 0xD4, 0x00, + 0x01, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1D, 0x00, 0x0A, + 0x84, 0x11, 0xFC, 0x00, 0x01, 0x82, 0x21, 0x13, 0x1D, 0x00, 0x0A, 0x86, + 0x10, 0x23, 0x00, 0x07, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x11, 0x50, 0x01, + 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0xC1, 0x01, + 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1E, 0x00, 0x0A, + 0x84, 0x11, 0xF8, 0x01, 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, + 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0x57, 0x02, 0x01, 0x82, 0x21, 0x82, + 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0xF3, 0x02, 0x01, 0x13, 0x1E, 0x00, + 0x0A, 0x82, 0x21, 0x84, 0x10, 0x21, 0x00, 0x07, 0x13, 0x1E, 0x00, 0x0A, + 0x84, 0x11, 0x82, 0x03, 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, + 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0xF0, 0x03, 0x01, 0x82, 0x21, 0x82, + 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0x42, 0x05, 0x01, 0x13, 0x1E, 0x00, + 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0x31, 0x06, + 0x01, 0x82, 0x21, 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x13, 0x1D, 0x00, 0x0A, + 0x82, 0x11, 0xF6, 0x04, 0x01, 0x82, 0x21, 0x82, 0x10, 0x1F, 0x00, 0x07, + 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x11, 0xF0, 0x03, 0x01, 0x13, 0x1D, 0x00, + 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1C, 0x00, 0x0A, 0x84, 0x11, 0x20, 0x03, + 0x01, 0x82, 0x21, 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x13, 0x1D, 0x00, 0x0A, + 0x82, 0x11, 0xA1, 0x02, 0x01, 0x82, 0x21, 0x82, 0x13, 0x1D, 0x00, 0x0A, + 0x86, 0x11, 0x36, 0x02, 0x01, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x21, 0x84, + 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0xF8, 0x01, 0x01, 0x82, 0x10, 0x21, + 0x00, 0x07, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, + 0x82, 0x11, 0xC1, 0x01, 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, + 0x86, 0x11, 0x7A, 0x01, 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, + 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0x3E, 0x01, 0x01, 0x82, 0x21, 0x13, + 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0xFC, 0x00, + 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x10, 0x23, 0x00, + 0x07, 0x11, 0xD4, 0x00, 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, + 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0x9F, 0x00, 0x01, 0x82, 0x21, 0x13, + 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0x7E, 0x00, + 0x01, 0x82, 0x21, 0x82, 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x11, 0xB2, 0x00, + 0x01, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1D, 0x00, 0x0A, + 0x84, 0x11, 0xD4, 0x00, 0x01, 0x82, 0x21, 0x13, 0x1D, 0x00, 0x0A, 0x86, + 0x10, 0x21, 0x00, 0x07, 0x13, 0x1C, 0x00, 0x0A, 0x82, 0x11, 0xFC, 0x00, + 0x01, 0x82, 0x21, 0x82, 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x11, 0x50, 0x01, + 0x01, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1D, 0x00, 0x0A, + 0x84, 0x11, 0xC1, 0x01, 0x01, 0x82, 0x21, 0x13, 0x1D, 0x00, 0x0A, 0x86, + 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0xF8, 0x01, 0x01, 0x82, 0x21, 0x82, + 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0x57, 0x02, 0x01, 0x13, 0x1E, 0x00, + 0x0A, 0x82, 0x21, 0x84, 0x10, 0x1F, 0x00, 0x07, 0x13, 0x1E, 0x00, 0x0A, + 0x84, 0x11, 0xF3, 0x02, 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, + 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0x82, 0x03, 0x01, 0x82, 0x21, 0x82, + 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0xF0, 0x03, 0x01, 0x13, 0x1E, 0x00, + 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0x42, 0x05, + 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, + 0x82, 0x11, 0x31, 0x06, 0x01, 0x82, 0x21, 0x82, 0x10, 0x21, 0x00, 0x07, + 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0xF6, 0x04, 0x01, 0x13, 0x1E, 0x00, + 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1D, 0x00, 0x0A, 0x84, 0x11, 0xF0, 0x03, + 0x01, 0x82, 0x21, 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x13, 0x1D, 0x00, 0x0A, + 0x82, 0x11, 0x20, 0x03, 0x01, 0x82, 0x21, 0x82, 0x13, 0x1D, 0x00, 0x0A, + 0x86, 0x11, 0xA1, 0x02, 0x01, 0x13, 0x1C, 0x00, 0x0A, 0x82, 0x21, 0x84, + 0x13, 0x1D, 0x00, 0x0A, 0x84, 0x11, 0x36, 0x02, 0x01, 0x82, 0x10, 0x23, + 0x00, 0x07, 0x21, 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x13, 0x1D, 0x00, 0x0A, + 0x82, 0x11, 0xF8, 0x01, 0x01, 0x82, 0x21, 0x82, 0x13, 0x1D, 0x00, 0x0A, + 0x86, 0x11, 0xC1, 0x01, 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, + 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0x7A, 0x01, 0x01, 0x82, 0x21, 0x13, + 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0x3E, 0x01, + 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x10, 0x21, 0x00, + 0x07, 0x11, 0xFC, 0x00, 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, + 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0xD4, 0x00, 0x01, 0x82, 0x21, 0x13, + 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0x9F, 0x00, + 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0x7E, 0x00, + 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1E, 0x00, 0x0A, + 0x84, 0x11, 0xB2, 0x00, 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, + 0x10, 0x1F, 0x00, 0x07, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x11, 0xD4, 0x00, + 0x01, 0x82, 0x21, 0x82, 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x11, 0xFC, 0x00, + 0x01, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1D, 0x00, 0x0A, + 0x84, 0x11, 0x50, 0x01, 0x01, 0x82, 0x21, 0x13, 0x1C, 0x00, 0x0A, 0x86, + 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x11, 0xC1, 0x01, 0x01, 0x82, 0x21, 0x82, + 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x11, 0xF8, 0x01, 0x01, 0x13, 0x1D, 0x00, + 0x0A, 0x82, 0x21, 0x84, 0x10, 0x21, 0x00, 0x07, 0x13, 0x1D, 0x00, 0x0A, + 0x84, 0x11, 0x57, 0x02, 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, + 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0xF3, 0x02, 0x01, 0x82, 0x21, 0x82, + 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0x82, 0x03, 0x01, 0x13, 0x1E, 0x00, + 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0xF0, 0x03, + 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, + 0x82, 0x11, 0x42, 0x05, 0x01, 0x82, 0x21, 0x82, 0x10, 0x23, 0x00, 0x07, + 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0x31, 0x06, 0x01, 0x13, 0x1E, 0x00, + 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0xF6, 0x04, + 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, + 0x82, 0x11, 0xF0, 0x03, 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, + 0x86, 0x11, 0x20, 0x03, 0x01, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x21, 0x84, + 0x13, 0x1D, 0x00, 0x0A, 0x84, 0x11, 0xA1, 0x02, 0x01, 0x82, 0x10, 0x21, + 0x00, 0x07, 0x21, 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x13, 0x1D, 0x00, 0x0A, + 0x82, 0x11, 0x36, 0x02, 0x01, 0x82, 0x21, 0x82, 0x13, 0x1C, 0x00, 0x0A, + 0x86, 0x11, 0xF8, 0x01, 0x01, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x21, 0x84, + 0x13, 0x1D, 0x00, 0x0A, 0x84, 0x11, 0xC1, 0x01, 0x01, 0x82, 0x21, 0x13, + 0x1D, 0x00, 0x0A, 0x86, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x11, 0x7A, 0x01, + 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x10, 0x1F, 0x00, + 0x07, 0x11, 0x3E, 0x01, 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, + 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0xFC, 0x00, 0x01, 0x82, 0x21, 0x13, + 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0xD4, 0x00, + 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0x9F, 0x00, + 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1E, 0x00, 0x0A, + 0x84, 0x11, 0x7E, 0x00, 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, + 0x10, 0x21, 0x00, 0x07, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0xB2, 0x00, + 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0xD4, 0x00, + 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1E, 0x00, 0x0A, + 0x84, 0x11, 0xFC, 0x00, 0x01, 0x82, 0x21, 0x13, 0x1D, 0x00, 0x0A, 0x86, + 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x11, 0x50, 0x01, 0x01, 0x82, 0x21, 0x82, + 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x11, 0xC1, 0x01, 0x01, 0x13, 0x1D, 0x00, + 0x0A, 0x82, 0x21, 0x84, 0x10, 0x23, 0x00, 0x07, 0x13, 0x1C, 0x00, 0x0A, + 0x84, 0x11, 0xF8, 0x01, 0x01, 0x82, 0x21, 0x13, 0x1D, 0x00, 0x0A, 0x86, + 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x11, 0x57, 0x02, 0x01, 0x82, 0x21, 0x82, + 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x11, 0xF3, 0x02, 0x01, 0x13, 0x1D, 0x00, + 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0x82, 0x03, + 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, + 0x82, 0x11, 0xF0, 0x03, 0x01, 0x82, 0x21, 0x82, 0x10, 0x21, 0x00, 0x07, + 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0x42, 0x05, 0x01, 0x13, 0x1E, 0x00, + 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0x31, 0x06, + 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, + 0x82, 0x11, 0xF6, 0x04, 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, + 0x86, 0x11, 0xF0, 0x03, 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, + 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0x20, 0x03, 0x01, 0x82, 0x10, 0x1F, + 0x00, 0x07, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, + 0x82, 0x11, 0xA1, 0x02, 0x01, 0x82, 0x21, 0x82, 0x13, 0x1D, 0x00, 0x0A, + 0x86, 0x11, 0x36, 0x02, 0x01, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x21, 0x84, + 0x13, 0x1D, 0x00, 0x0A, 0x84, 0x11, 0xF8, 0x01, 0x01, 0x82, 0x21, 0x13, + 0x1D, 0x00, 0x0A, 0x86, 0x13, 0x1C, 0x00, 0x0A, 0x82, 0x11, 0xC1, 0x01, + 0x01, 0x82, 0x21, 0x82, 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x10, 0x21, 0x00, + 0x07, 0x11, 0x7A, 0x01, 0x01, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x21, 0x84, + 0x13, 0x1D, 0x00, 0x0A, 0x84, 0x11, 0x3E, 0x01, 0x01, 0x82, 0x21, 0x13, + 0x1D, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0xFC, 0x00, + 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0xD4, 0x00, + 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1E, 0x00, 0x0A, + 0x84, 0x11, 0x9F, 0x00, 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, + 0x10, 0x23, 0x00, 0x07, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0x7E, 0x00, + 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0xB2, 0x00, + 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1E, 0x00, 0x0A, + 0x84, 0x11, 0xD4, 0x00, 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, + 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0xFC, 0x00, 0x01, 0x82, 0x21, 0x82, + 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x11, 0x50, 0x01, 0x01, 0x13, 0x1E, 0x00, + 0x0A, 0x82, 0x21, 0x84, 0x10, 0x21, 0x00, 0x07, 0x13, 0x1D, 0x00, 0x0A, + 0x84, 0x11, 0xC1, 0x01, 0x01, 0x82, 0x21, 0x13, 0x1D, 0x00, 0x0A, 0x86, + 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x11, 0xF8, 0x01, 0x01, 0x82, 0x21, 0x82, + 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x11, 0x57, 0x02, 0x01, 0x13, 0x1C, 0x00, + 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1D, 0x00, 0x0A, 0x84, 0x11, 0xF3, 0x02, + 0x01, 0x82, 0x21, 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x13, 0x1D, 0x00, 0x0A, + 0x82, 0x11, 0x82, 0x03, 0x01, 0x82, 0x21, 0x82, 0x10, 0x1F, 0x00, 0x07, + 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x11, 0xF0, 0x03, 0x01, 0x13, 0x1E, 0x00, + 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0x42, 0x05, + 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, + 0x82, 0x11, 0x31, 0x06, 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, + 0x86, 0x11, 0xF6, 0x04, 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, + 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0xF0, 0x03, 0x01, 0x82, 0x10, 0x21, + 0x00, 0x07, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1E, 0x00, 0x0A, + 0x82, 0x11, 0x20, 0x03, 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, + 0x86, 0x11, 0xA1, 0x02, 0x01, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x21, 0x84, + 0x13, 0x1E, 0x00, 0x0A, 0x84, 0x11, 0x36, 0x02, 0x01, 0x82, 0x21, 0x13, + 0x1E, 0x00, 0x0A, 0x86, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x11, 0xF8, 0x01, + 0x01, 0x82, 0x21, 0x82, 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x10, 0x23, 0x00, + 0x07, 0x11, 0xC1, 0x01, 0x01, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x21, 0x84, + 0x13, 0x1D, 0x00, 0x0A, 0x84, 0x11, 0x7A, 0x01, 0x01, 0x82, 0x21, 0x13, + 0x1C, 0x00, 0x0A, 0x86, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x11, 0x3E, 0x01, + 0x01, 0x82, 0x21, 0x82, 0x13, 0x1D, 0x00, 0x0A, 0x86, 0x11, 0xFC, 0x00, + 0x01, 0x13, 0x1D, 0x00, 0x0A, 0x82, 0x21, 0x84, 0x13, 0x1D, 0x00, 0x0A, + 0x84, 0x11, 0xD4, 0x00, 0x01, 0x82, 0x21, 0x13, 0x1E, 0x00, 0x0A, 0x86, + 0x10, 0x21, 0x00, 0x07, 0x13, 0x1E, 0x00, 0x0A, 0x82, 0x11, 0x9F, 0x00, + 0x01, 0x82, 0x21, 0x82, 0x13, 0x1E, 0x00, 0x0A, 0x85, 0x00, +}; + +typedef struct { + const uint8_t *data; + uint16_t length; +} StSongT; + +#define ST_SONG_COUNT 9u +#define ST_SONG_BONUS 4u +#define ST_SONG_GAME_OVER 5u +#define ST_SONG_NAME_ENTRY 6u +#define ST_SONG_SCORES 7u +#define ST_SONG_INTRO 8u + +static const StSongT kStSongs[ST_SONG_COUNT] = { + { gStSong0, (uint16_t)sizeof(gStSong0) }, + { gStSong1, (uint16_t)sizeof(gStSong1) }, + { gStSong2, (uint16_t)sizeof(gStSong2) }, + { gStSong3, (uint16_t)sizeof(gStSong3) }, + { gStSong4, (uint16_t)sizeof(gStSong4) }, + { gStSong5, (uint16_t)sizeof(gStSong5) }, + { gStSong6, (uint16_t)sizeof(gStSong6) }, + { gStSong7, (uint16_t)sizeof(gStSong7) }, + { gStSong8, (uint16_t)sizeof(gStSong8) }, +}; + +#endif // ST_SONG_DATA_H diff --git a/examples/spacetaxi/stSpeechData.c b/examples/spacetaxi/stSpeechData.c new file mode 100644 index 0000000..16b0c91 --- /dev/null +++ b/examples/spacetaxi/stSpeechData.c @@ -0,0 +1,490 @@ +// Generated by assets/extractSpeech.py. Do not hand-edit. + +#include "stSpeechData.h" + +const StSpeechEntryT kStSpeechIndex[ST_SPEECH_COUNT] = { + { 0x50u, 0u, 562u }, // 'P' + { 0x31u, 562u, 420u }, // '1' + { 0x3Fu, 982u, 266u }, // '?' + { 0x32u, 1248u, 480u }, // '2' + { 0x33u, 1728u, 463u }, // '3' + { 0x34u, 2191u, 315u }, // '4' + { 0x35u, 2506u, 550u }, // '5' + { 0x36u, 3056u, 527u }, // '6' + { 0x37u, 3583u, 758u }, // '7' + { 0x38u, 4341u, 385u }, // '8' + { 0x39u, 4726u, 491u }, // '9' + { 0x48u, 5217u, 520u }, // 'H' + { 0x54u, 5737u, 632u }, // 'T' + { 0x21u, 6369u, 708u }, // '!' + { 0x55u, 7077u, 368u }, // 'U' +}; + +const uint8_t kStSpeechRle[ST_SPEECH_RLE_BYTES] = { + 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, + 0xF2, 0x42, 0xCD, 0x27, 0x10, 0x09, 0xAB, 0x31, 0xB6, 0x12, 0x37, 0x09, 0x22, 0x04, 0x1C, 0x0D, + 0x16, 0x0E, 0x2F, 0x0B, 0x19, 0x12, 0x0E, 0x12, 0x19, 0x06, 0x1A, 0x0F, 0x09, 0x19, 0x0D, 0x16, + 0x13, 0x06, 0x05, 0x03, 0x38, 0x08, 0x0F, 0x17, 0x0E, 0x12, 0x0F, 0x12, 0x16, 0x09, 0x15, 0x15, + 0x06, 0x13, 0x08, 0x15, 0x0C, 0x13, 0x18, 0x10, 0x15, 0x1C, 0x0C, 0x0C, 0x09, 0x02, 0x0C, 0x14, + 0x15, 0x14, 0x1D, 0x11, 0x11, 0x0B, 0x0A, 0x02, 0x0A, 0x15, 0x15, 0x0B, 0x15, 0x05, 0x06, 0x07, + 0x07, 0x07, 0x15, 0x08, 0x04, 0x08, 0x0B, 0x07, 0x03, 0x08, 0x0B, 0x04, 0x05, 0x0D, 0x13, 0x08, + 0x04, 0x06, 0x24, 0x08, 0x04, 0x03, 0x02, 0x03, 0x09, 0x07, 0x05, 0x07, 0x0B, 0x05, 0x05, 0x0A, + 0x14, 0x08, 0x06, 0x06, 0x18, 0x06, 0x06, 0x09, 0x17, 0x07, 0x05, 0x08, 0x0B, 0x04, 0x05, 0x0A, + 0x14, 0x09, 0x04, 0x07, 0x09, 0x06, 0x06, 0x09, 0x07, 0x08, 0x05, 0x03, 0x0E, 0x07, 0x04, 0x09, + 0x0B, 0x04, 0x06, 0x09, 0x15, 0x08, 0x04, 0x06, 0x17, 0x09, 0x07, 0x0C, 0x15, 0x0A, 0x01, 0x09, + 0x16, 0x09, 0x15, 0x09, 0x06, 0x03, 0x0C, 0x05, 0x05, 0x0A, 0x09, 0x08, 0x07, 0x04, 0x0B, 0x07, + 0x05, 0x08, 0x15, 0x0A, 0x14, 0x08, 0x05, 0x05, 0x0B, 0x04, 0x07, 0x0F, 0x04, 0x0A, 0x05, 0x08, + 0x09, 0x07, 0x03, 0x09, 0x15, 0x0A, 0x14, 0x09, 0x04, 0x05, 0x18, 0x0A, 0x0A, 0x0A, 0x06, 0x03, + 0x0C, 0x14, 0x15, 0x0A, 0x14, 0x14, 0x0A, 0x06, 0x05, 0x0E, 0x08, 0x09, 0x16, 0x08, 0x03, 0x08, + 0x16, 0x09, 0x15, 0x13, 0x16, 0x0F, 0x08, 0x09, 0x05, 0x06, 0x0A, 0x08, 0x04, 0x07, 0x16, 0x0A, + 0x06, 0x02, 0x0B, 0x13, 0x17, 0x09, 0x0F, 0x09, 0x07, 0x02, 0x0C, 0x08, 0x03, 0x08, 0x15, 0x0B, + 0x13, 0x14, 0x0A, 0x14, 0x13, 0x0A, 0x15, 0x13, 0x12, 0x0F, 0x14, 0x0F, 0x10, 0x11, 0x10, 0x10, + 0x15, 0x0A, 0x06, 0x02, 0x0D, 0x13, 0x14, 0x0B, 0x12, 0x11, 0x0E, 0x16, 0x15, 0x0A, 0x17, 0x07, + 0x04, 0x05, 0x19, 0x06, 0x14, 0x0B, 0x14, 0x18, 0x07, 0x04, 0x0C, 0x07, 0x05, 0x05, 0x10, 0x02, + 0x05, 0x07, 0x08, 0x02, 0x0C, 0x07, 0x06, 0x07, 0x08, 0x38, 0x16, 0x09, 0x05, 0x05, 0x0C, 0x06, + 0x05, 0x07, 0x16, 0x08, 0x07, 0x03, 0x0A, 0x13, 0x0B, 0x1C, 0x08, 0x0B, 0x04, 0x07, 0x06, 0x04, + 0x0D, 0x05, 0x06, 0x06, 0x0B, 0x02, 0x08, 0x07, 0x08, 0x05, 0x08, 0x08, 0x1F, 0x0A, 0x05, 0x08, + 0x06, 0x04, 0x0C, 0x07, 0x05, 0x05, 0x18, 0x0A, 0x07, 0x02, 0x09, 0x08, 0x06, 0x07, 0x05, 0x09, + 0x17, 0x10, 0x09, 0x07, 0x06, 0x06, 0x0C, 0x05, 0x05, 0x07, 0x17, 0x09, 0x07, 0x05, 0x0C, 0x13, + 0x06, 0x06, 0x08, 0x06, 0x06, 0x0B, 0x05, 0x02, 0x0D, 0x07, 0x06, 0x03, 0x0F, 0x05, 0x05, 0x07, + 0x17, 0x17, 0x07, 0x17, 0x24, 0x11, 0x11, 0x09, 0x17, 0x08, 0x03, 0x08, 0x16, 0x0B, 0x14, 0x13, + 0x18, 0x06, 0x16, 0x0C, 0x0E, 0x0A, 0x04, 0x03, 0x10, 0x12, 0x16, 0x12, 0x0D, 0x14, 0x15, 0x0B, + 0x16, 0x0C, 0x10, 0x0F, 0x15, 0x0A, 0x02, 0x05, 0x18, 0x0C, 0x13, 0x09, 0x05, 0x07, 0x30, 0x09, + 0x11, 0x1B, 0x1A, 0x05, 0x05, 0x06, 0x1A, 0x15, 0x14, 0x0C, 0x20, 0x14, 0x18, 0x16, 0x03, 0x09, + 0x05, 0x03, 0x0F, 0x05, 0x05, 0x09, 0x15, 0x16, 0x15, 0x13, 0x17, 0x09, 0x1E, 0x14, 0x0D, 0x0B, + 0x19, 0x05, 0x03, 0x0B, 0x14, 0x16, 0x15, 0x14, 0x18, 0x08, 0x26, 0x12, 0x11, 0x0B, 0x16, 0x14, + 0x15, 0x15, 0x16, 0x16, 0x0A, 0x23, 0x42, 0x0A, 0x18, 0x05, 0x05, 0x09, 0x24, 0x08, 0x18, 0x16, + 0x13, 0x17, 0x15, 0x18, 0x1A, 0x0A, 0x23, 0x09, 0x25, 0x15, 0x17, 0x18, 0x11, 0x14, 0x3C, 0x10, + 0x57, 0x0F, 0x16, 0x15, 0x18, 0x13, 0x48, 0x1A, 0x16, 0x13, 0x4E, 0x1C, 0x6B, 0x18, 0xFE, 0xFE, + 0xFE, 0xFF, 0xFE, 0x63, 0x1D, 0xC8, 0x29, 0xBE, 0x1B, 0x59, 0x14, 0x48, 0x19, 0x1A, 0x0E, 0x1E, + 0x1F, 0x20, 0x11, 0x0E, 0x23, 0x19, 0x10, 0x19, 0x22, 0x18, 0x12, 0x15, 0x20, 0x17, 0x13, 0x18, + 0x1E, 0x17, 0x13, 0x17, 0x1D, 0x16, 0x14, 0x1A, 0x1A, 0x16, 0x12, 0x17, 0x1E, 0x16, 0x13, 0x17, + 0x1D, 0x12, 0x15, 0x17, 0x1F, 0x16, 0x12, 0x17, 0x1C, 0x14, 0x14, 0x18, 0x1E, 0x15, 0x0F, 0x1A, + 0x19, 0x13, 0x10, 0x1E, 0x1D, 0x12, 0x11, 0x1C, 0x17, 0x11, 0x1A, 0x1A, 0x1A, 0x11, 0x11, 0x1D, + 0x14, 0x10, 0x11, 0x25, 0x19, 0x10, 0x12, 0x1C, 0x13, 0x10, 0x10, 0x0B, 0x09, 0x13, 0x18, 0x12, + 0x11, 0x17, 0x16, 0x10, 0x0E, 0x0D, 0x0F, 0x0E, 0x19, 0x11, 0x10, 0x17, 0x16, 0x10, 0x0D, 0x0E, + 0x10, 0x0D, 0x18, 0x10, 0x11, 0x18, 0x12, 0x14, 0x0D, 0x0F, 0x0C, 0x0F, 0x19, 0x10, 0x10, 0x19, + 0x15, 0x11, 0x0D, 0x0C, 0x12, 0x0E, 0x17, 0x0F, 0x11, 0x1A, 0x11, 0x0C, 0x14, 0x0C, 0x17, 0x0B, + 0x13, 0x01, 0x04, 0x0D, 0x10, 0x1B, 0x10, 0x0C, 0x0C, 0x0E, 0x0A, 0x0B, 0x0B, 0x0A, 0x14, 0x13, + 0x0C, 0x1F, 0x0D, 0x0F, 0x0B, 0x0E, 0x1A, 0x0D, 0x20, 0x0C, 0x0B, 0x24, 0x09, 0x0E, 0x0C, 0x0C, + 0x0B, 0x0A, 0x05, 0x0E, 0x22, 0x06, 0x01, 0x04, 0x0A, 0x0E, 0x03, 0x10, 0x0B, 0x0F, 0x0D, 0x0C, + 0x0B, 0x06, 0x0A, 0x0B, 0x16, 0x04, 0x0E, 0x0C, 0x09, 0x0F, 0x02, 0x10, 0x0A, 0x0C, 0x0C, 0x0C, + 0x1D, 0x0B, 0x09, 0x08, 0x09, 0x04, 0x0D, 0x0C, 0x0A, 0x0E, 0x02, 0x10, 0x0C, 0x0A, 0x0B, 0x02, + 0x07, 0x0D, 0x0E, 0x18, 0x13, 0x07, 0x0D, 0x0C, 0x08, 0x09, 0x0A, 0x0D, 0x0D, 0x07, 0x08, 0x09, + 0x0B, 0x09, 0x0B, 0x0B, 0x01, 0x0F, 0x0B, 0x10, 0x0F, 0x07, 0x0D, 0x07, 0x0A, 0x0D, 0x09, 0x0E, + 0x05, 0x05, 0x0F, 0x0D, 0x10, 0x26, 0x18, 0x03, 0x02, 0x05, 0x0A, 0x0B, 0x05, 0x0D, 0x08, 0x0A, + 0x09, 0x0A, 0x1B, 0x17, 0x08, 0x0D, 0x07, 0x0B, 0x04, 0x0A, 0x08, 0x03, 0x02, 0x05, 0x08, 0x0B, + 0x07, 0x0C, 0x04, 0x09, 0x0B, 0x0D, 0x06, 0x05, 0x0D, 0x19, 0x06, 0x1A, 0x05, 0x0D, 0x04, 0x07, + 0x08, 0x04, 0x02, 0x04, 0x05, 0x09, 0x0B, 0x0F, 0x01, 0x09, 0x0A, 0x09, 0x06, 0x08, 0x0A, 0x0A, + 0x07, 0x0C, 0x04, 0x1A, 0x06, 0x0E, 0x0B, 0x05, 0x0C, 0x0C, 0x04, 0x09, 0x0A, 0x1A, 0x06, 0x0A, + 0x07, 0x08, 0x09, 0x08, 0x0A, 0x2B, 0x04, 0x0B, 0x27, 0x03, 0x03, 0x05, 0x02, 0x0B, 0x0B, 0x05, + 0x02, 0x11, 0x05, 0x02, 0x0D, 0x0A, 0x14, 0x0A, 0x07, 0x1A, 0x09, 0x06, 0x03, 0x12, 0x1F, 0x04, + 0x02, 0x04, 0x03, 0x0B, 0x0A, 0x04, 0x08, 0x0D, 0x04, 0x05, 0x19, 0x0B, 0x1B, 0x18, 0x0F, 0x1E, + 0x12, 0x04, 0x0E, 0x04, 0x04, 0x19, 0x06, 0x18, 0x0B, 0x58, 0x14, 0x14, 0x0B, 0x0D, 0x19, 0x1B, + 0x04, 0x56, 0x16, 0x40, 0x29, 0x04, 0x15, 0x3A, 0x07, 0x37, 0x14, 0x55, 0x19, 0xFE, 0x80, 0x5B, + 0xFE, 0x3C, 0x80, 0xFE, 0xFE, 0xFF, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xA2, 0x40, 0x48, 0x1D, 0x3C, + 0x0B, 0xFE, 0xFE, 0xBB, 0x51, 0xFE, 0xFE, 0xFE, 0x0D, 0x13, 0x92, 0x24, 0x36, 0x30, 0x67, 0x23, + 0x37, 0x25, 0x6D, 0x23, 0x36, 0x2B, 0x67, 0x21, 0x35, 0x46, 0x29, 0x20, 0x0A, 0x1F, 0x2F, 0x97, + 0x11, 0x1A, 0x2E, 0x1C, 0x62, 0x44, 0x2D, 0x1E, 0x23, 0x15, 0x2B, 0x0F, 0x14, 0x1E, 0x2F, 0x20, + 0x19, 0x16, 0x25, 0x1E, 0x0E, 0x1E, 0x2D, 0x20, 0x15, 0x18, 0x22, 0x4F, 0x30, 0x19, 0x21, 0x12, + 0x27, 0x4B, 0x2F, 0x21, 0x15, 0x10, 0x30, 0x47, 0x2E, 0x16, 0x6D, 0x39, 0x2E, 0x2D, 0x48, 0x48, + 0x2D, 0x24, 0x55, 0x3C, 0x30, 0x2D, 0x3C, 0x4C, 0x31, 0x29, 0x34, 0x57, 0x2E, 0x29, 0x2E, 0x34, + 0x03, 0x24, 0x32, 0x26, 0x38, 0x29, 0x04, 0x23, 0x37, 0x24, 0x38, 0x50, 0x31, 0x21, 0x3B, 0x4F, + 0x3C, 0x13, 0x43, 0x42, 0x31, 0x2A, 0x46, 0x3F, 0x2F, 0x2C, 0x3D, 0x3F, 0x33, 0x28, 0x3E, 0x48, + 0x2F, 0x31, 0x41, 0x38, 0x31, 0x2A, 0x4E, 0x30, 0x2E, 0x2A, 0x46, 0x37, 0x2F, 0x2A, 0x46, 0x33, + 0x31, 0x29, 0x49, 0x37, 0x2E, 0x2A, 0x35, 0x47, 0x2F, 0x26, 0x41, 0x42, 0x30, 0x25, 0x50, 0x2C, + 0x32, 0x24, 0x38, 0x43, 0x31, 0x29, 0x35, 0x49, 0x32, 0x1D, 0x37, 0x4C, 0x32, 0x1E, 0x48, 0x32, + 0xFE, 0xD3, 0x48, 0x74, 0x24, 0xAE, 0x2C, 0x45, 0x03, 0x50, 0x02, 0x1D, 0x26, 0x12, 0x02, 0x21, + 0x02, 0x03, 0x02, 0x48, 0x02, 0x1F, 0x1B, 0x46, 0x2E, 0x13, 0x01, 0x2D, 0x02, 0x03, 0x03, 0x01, + 0x03, 0x01, 0x03, 0xCC, 0x1B, 0x4B, 0x03, 0x03, 0x02, 0x01, 0x09, 0x74, 0x08, 0x02, 0x00, 0x02, + 0x1F, 0x79, 0x0E, 0x03, 0x01, 0x03, 0x02, 0x03, 0x02, 0x04, 0x01, 0x24, 0x01, 0x36, 0x02, 0x03, + 0x3C, 0x04, 0x01, 0x1D, 0x1F, 0xCF, 0x03, 0x01, 0x38, 0x90, 0x39, 0xFE, 0xFE, 0xFE, 0xFE, 0xFF, + 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0x5C, 0x06, 0x02, 0x03, 0x03, 0x02, 0x02, 0x03, + 0x3F, 0x0E, 0x13, 0x08, 0x9B, 0x23, 0x2B, 0x36, 0x25, 0x02, 0x25, 0x01, 0x23, 0x03, 0x02, 0x02, + 0x15, 0x02, 0x1B, 0x0A, 0x23, 0x02, 0x25, 0x02, 0x3E, 0x01, 0x05, 0x02, 0x14, 0x0F, 0x25, 0x18, + 0x3E, 0x13, 0x6B, 0x05, 0x0B, 0x0A, 0x72, 0x21, 0x09, 0x16, 0xBA, 0x24, 0x45, 0x03, 0x05, 0x1A, + 0x04, 0x09, 0x02, 0x0F, 0x12, 0x02, 0x1B, 0x04, 0x08, 0x03, 0x0F, 0x04, 0x04, 0x08, 0x04, 0x32, + 0x42, 0x03, 0x07, 0x1A, 0x03, 0x08, 0x03, 0x23, 0x48, 0x0E, 0x05, 0x04, 0x05, 0x25, 0x18, 0x05, + 0x06, 0x06, 0x23, 0x19, 0x05, 0x0B, 0x02, 0x09, 0x03, 0x0A, 0x17, 0x06, 0x07, 0x09, 0x09, 0x02, + 0x19, 0x06, 0x05, 0x07, 0x06, 0x0A, 0x03, 0x09, 0x03, 0x0A, 0x0B, 0x03, 0x07, 0x05, 0x09, 0x08, + 0x08, 0x03, 0x19, 0x07, 0x04, 0x07, 0x07, 0x16, 0x02, 0x0B, 0x0C, 0x02, 0x07, 0x07, 0x06, 0x0A, + 0x08, 0x03, 0x09, 0x03, 0x0B, 0x07, 0x05, 0x06, 0x07, 0x0B, 0x02, 0x08, 0x03, 0x0A, 0x0C, 0x03, + 0x07, 0x07, 0x07, 0x09, 0x08, 0x03, 0x18, 0x06, 0x07, 0x05, 0x0A, 0x09, 0x02, 0x08, 0x03, 0x0A, + 0x0B, 0x04, 0x07, 0x06, 0x08, 0x09, 0x07, 0x04, 0x19, 0x05, 0x07, 0x05, 0x09, 0x08, 0x05, 0x08, + 0x03, 0x0A, 0x0A, 0x05, 0x06, 0x07, 0x07, 0x09, 0x08, 0x04, 0x0B, 0x02, 0x0A, 0x07, 0x05, 0x06, + 0x0B, 0x05, 0x06, 0x09, 0x04, 0x0A, 0x0B, 0x03, 0x07, 0x06, 0x08, 0x07, 0x0A, 0x04, 0x08, 0x05, + 0x06, 0x0B, 0x07, 0x04, 0x1B, 0x09, 0x03, 0x0B, 0x0D, 0x01, 0x07, 0x05, 0x0A, 0x0A, 0x06, 0x04, + 0x09, 0x03, 0x0A, 0x08, 0x05, 0x06, 0x07, 0x05, 0x06, 0x13, 0x02, 0x0F, 0x13, 0x04, 0x0A, 0x09, + 0x08, 0x03, 0x19, 0x05, 0x07, 0x05, 0x06, 0x03, 0x17, 0x08, 0x03, 0x0D, 0x0A, 0x03, 0x07, 0x05, + 0x0A, 0x0B, 0x04, 0x06, 0x09, 0x03, 0x0E, 0x04, 0x07, 0x04, 0x1B, 0x0F, 0x03, 0x0D, 0x0A, 0x02, + 0x08, 0x04, 0x0B, 0x0B, 0x04, 0x06, 0x28, 0x03, 0x1A, 0x22, 0x16, 0x04, 0x0A, 0x0C, 0x04, 0x05, + 0x1B, 0x04, 0x08, 0x04, 0x1A, 0x06, 0x04, 0x1A, 0x17, 0x08, 0x05, 0x0B, 0x05, 0x05, 0x1B, 0x05, + 0x08, 0x09, 0x15, 0x25, 0x18, 0x08, 0x04, 0x0C, 0x26, 0x16, 0x17, 0x26, 0x0C, 0x03, 0x08, 0x19, + 0x1A, 0x06, 0x06, 0x08, 0x1B, 0x04, 0x07, 0x19, 0x05, 0x09, 0x0C, 0x04, 0x07, 0x09, 0x09, 0x06, + 0x08, 0x07, 0x09, 0x08, 0x06, 0x08, 0x19, 0x2B, 0x03, 0x0A, 0x0D, 0x02, 0x09, 0x06, 0x08, 0x0A, + 0x08, 0x05, 0x0A, 0x0B, 0x05, 0x07, 0x18, 0x09, 0x06, 0x23, 0x03, 0x0C, 0x2C, 0x04, 0x1B, 0x19, + 0x08, 0x06, 0x1A, 0x07, 0x07, 0x09, 0x04, 0x09, 0x04, 0x0B, 0x2B, 0x06, 0x1C, 0x07, 0x07, 0x0E, + 0x03, 0x0A, 0x19, 0x05, 0x0A, 0x0A, 0x05, 0x08, 0x08, 0x06, 0x0D, 0x04, 0x0A, 0x0B, 0x07, 0x05, + 0x09, 0x0E, 0x03, 0x1D, 0x2A, 0x1A, 0x0F, 0x1C, 0x28, 0x1C, 0x09, 0x04, 0x0A, 0x18, 0x2F, 0x06, + 0x19, 0x1C, 0x1F, 0x05, 0x05, 0x1B, 0x1E, 0x14, 0x23, 0x1C, 0x19, 0x1D, 0x21, 0x07, 0x04, 0x0D, + 0x36, 0x13, 0x1F, 0x17, 0x1C, 0x24, 0x36, 0x15, 0x26, 0x1C, 0x5D, 0x20, 0x63, 0x2B, 0x53, 0x2A, + 0x7C, 0x28, 0x61, 0x2D, 0xFE, 0x23, 0x22, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFF, + 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0x5C, 0xFE, 0xFE, 0xFE, 0xFE, 0x76, 0x23, 0x6F, + 0x28, 0x84, 0x32, 0x22, 0x05, 0x08, 0x04, 0x1C, 0x16, 0x1B, 0x2E, 0x52, 0x0B, 0x26, 0x2C, 0x54, + 0x08, 0x1D, 0x34, 0x54, 0x0B, 0x1B, 0x32, 0x24, 0x05, 0x2C, 0x0A, 0x1B, 0x25, 0x03, 0x08, 0x25, + 0x05, 0x2C, 0x09, 0x0C, 0x04, 0x0A, 0x2F, 0x24, 0x09, 0x2A, 0x0A, 0x1B, 0x20, 0x02, 0x09, 0x25, + 0x08, 0x4F, 0x1C, 0x04, 0x09, 0x26, 0x07, 0x4F, 0x1B, 0x05, 0x09, 0x24, 0x08, 0x20, 0x08, 0x26, + 0x1B, 0x04, 0x08, 0x0E, 0x05, 0x09, 0x10, 0x52, 0x18, 0x04, 0x08, 0x0E, 0x06, 0x06, 0x13, 0x1E, + 0x18, 0x1B, 0x17, 0x05, 0x07, 0x10, 0x02, 0x09, 0x14, 0x1D, 0x0F, 0x25, 0x16, 0x05, 0x08, 0x0B, + 0x08, 0x07, 0x12, 0x1B, 0x09, 0x05, 0x09, 0x20, 0x18, 0x06, 0x07, 0x0C, 0x08, 0x05, 0x14, 0x19, + 0x17, 0x20, 0x1A, 0x07, 0x07, 0x0B, 0x08, 0x06, 0x0F, 0x1D, 0x08, 0x06, 0x08, 0x21, 0x1B, 0x07, + 0x07, 0x0A, 0x09, 0x06, 0x0B, 0x1E, 0x08, 0x06, 0x08, 0x22, 0x1C, 0x08, 0x06, 0x0B, 0x07, 0x07, + 0x0A, 0x1E, 0x0A, 0x04, 0x09, 0x1A, 0x27, 0x07, 0x07, 0x0A, 0x07, 0x07, 0x09, 0x1E, 0x09, 0x06, + 0x08, 0x1A, 0x2B, 0x07, 0x06, 0x0A, 0x07, 0x07, 0x09, 0x1D, 0x07, 0x07, 0x08, 0x19, 0x06, 0x05, + 0x24, 0x07, 0x07, 0x09, 0x07, 0x07, 0x09, 0x1D, 0x06, 0x07, 0x08, 0x08, 0x05, 0x0A, 0x06, 0x07, + 0x09, 0x07, 0x09, 0x03, 0x0B, 0x06, 0x07, 0x0A, 0x06, 0x08, 0x09, 0x08, 0x06, 0x0C, 0x06, 0x07, + 0x09, 0x08, 0x04, 0x0B, 0x05, 0x07, 0x09, 0x06, 0x08, 0x07, 0x0A, 0x08, 0x06, 0x09, 0x06, 0x08, + 0x09, 0x07, 0x07, 0x0B, 0x06, 0x07, 0x0A, 0x06, 0x05, 0x19, 0x2D, 0x05, 0x09, 0x09, 0x05, 0x09, + 0x07, 0x09, 0x04, 0x1B, 0x07, 0x05, 0x08, 0x18, 0x18, 0x07, 0x08, 0x05, 0x09, 0x05, 0x08, 0x09, + 0x04, 0x0A, 0x07, 0x09, 0x06, 0x09, 0x06, 0x08, 0x17, 0x1F, 0x17, 0x09, 0x05, 0x02, 0x07, 0x04, + 0x0A, 0x1A, 0x04, 0x0A, 0x04, 0x0C, 0x02, 0x09, 0x08, 0x05, 0x09, 0x06, 0x05, 0x07, 0x07, 0x06, + 0x08, 0x07, 0x06, 0x0B, 0x07, 0x05, 0x08, 0x05, 0x08, 0x19, 0x06, 0x07, 0x07, 0x08, 0x03, 0x0A, + 0x07, 0x05, 0x16, 0x09, 0x04, 0x09, 0x03, 0x0A, 0x05, 0x0A, 0x07, 0x0A, 0x09, 0x04, 0x08, 0x0A, + 0x02, 0x0B, 0x06, 0x07, 0x07, 0x08, 0x05, 0x08, 0x25, 0x15, 0x04, 0x0B, 0x02, 0x1A, 0x04, 0x06, + 0x07, 0x05, 0x08, 0x18, 0x06, 0x06, 0x08, 0x06, 0x05, 0x0A, 0x04, 0x08, 0x09, 0x04, 0x08, 0x55, + 0x08, 0x04, 0x09, 0x06, 0x05, 0x0C, 0x04, 0x08, 0x07, 0x05, 0x0B, 0x15, 0x1D, 0x52, 0x0A, 0x02, + 0x0B, 0x04, 0x04, 0x1B, 0x04, 0x06, 0x06, 0x24, 0x18, 0x26, 0x1C, 0x14, 0x0A, 0x02, 0x0B, 0x24, + 0x1F, 0x15, 0x1B, 0x24, 0x1A, 0x19, 0x0E, 0x02, 0x07, 0x27, 0x06, 0x05, 0x0C, 0x1B, 0x16, 0x6C, + 0x0B, 0x03, 0x06, 0x09, 0x03, 0x25, 0x16, 0x13, 0x05, 0x8A, 0x06, 0x03, 0x0C, 0x03, 0x03, 0x6E, + 0x1D, 0x56, 0x22, 0x2C, 0x16, 0x26, 0x1B, 0x5F, 0x24, 0x8C, 0x11, 0x5A, 0x20, 0x62, 0x44, 0x5B, + 0x23, 0x80, 0x1E, 0x70, 0x20, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFF, 0xFE, + 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xBF, 0x04, 0xFE, 0xCF, 0x1A, 0x51, 0x0F, 0x23, + 0x23, 0x36, 0x08, 0x18, 0x0E, 0x1C, 0x26, 0x56, 0x12, 0x16, 0x27, 0x1C, 0x16, 0x26, 0x10, 0x16, + 0x26, 0x1C, 0x14, 0x27, 0x12, 0x15, 0x25, 0x1C, 0x17, 0x24, 0x10, 0x1A, 0x22, 0x1D, 0x1A, 0x20, + 0x11, 0x19, 0x23, 0x1C, 0x19, 0x22, 0x11, 0x1B, 0x22, 0x1A, 0x19, 0x23, 0x13, 0x18, 0x24, 0x1B, + 0x17, 0x25, 0x11, 0x19, 0x23, 0x1B, 0x17, 0x24, 0x12, 0x18, 0x24, 0x1B, 0x16, 0x25, 0x11, 0x19, + 0x24, 0x1B, 0x17, 0x25, 0x10, 0x1A, 0x24, 0x19, 0x18, 0x24, 0x11, 0x19, 0x25, 0x1B, 0x15, 0x26, + 0x11, 0x1A, 0x25, 0x19, 0x15, 0x27, 0x12, 0x19, 0x27, 0x18, 0x14, 0x28, 0x12, 0x18, 0x2A, 0x18, + 0x13, 0x2B, 0x10, 0x18, 0x2B, 0x16, 0x14, 0x2E, 0x0B, 0x19, 0x2D, 0x15, 0x14, 0x2D, 0x0D, 0x19, + 0x2F, 0x15, 0x12, 0x2E, 0x10, 0x16, 0x1A, 0x02, 0x15, 0x14, 0x11, 0x2E, 0x11, 0x16, 0x18, 0x06, + 0x14, 0x15, 0x10, 0x2D, 0x10, 0x16, 0x37, 0x15, 0x0D, 0x2B, 0x11, 0x19, 0x1B, 0x0D, 0x15, 0x2B, + 0x0A, 0x13, 0x11, 0x22, 0x14, 0x11, 0x18, 0x1A, 0x12, 0x1B, 0x1A, 0x15, 0x16, 0x0F, 0x1E, 0x47, + 0x15, 0x17, 0x18, 0x1C, 0x17, 0x22, 0x10, 0x15, 0x18, 0x14, 0x12, 0x16, 0x26, 0x15, 0x1B, 0x16, + 0x1E, 0x12, 0x12, 0x21, 0x21, 0x1B, 0x13, 0x13, 0x24, 0x0F, 0x0F, 0x1E, 0x10, 0x10, 0x0F, 0x10, + 0x0A, 0x08, 0x0C, 0x0F, 0x11, 0x07, 0x0D, 0x11, 0x0E, 0x1D, 0x0E, 0x13, 0x16, 0x21, 0x0E, 0x16, + 0x1A, 0x0F, 0x15, 0x18, 0x0F, 0x1F, 0x17, 0x1E, 0x0F, 0x14, 0x1D, 0x14, 0x12, 0x1A, 0x11, 0x16, + 0x24, 0x15, 0x16, 0x15, 0x1B, 0x16, 0x15, 0x15, 0x13, 0x15, 0x2F, 0x15, 0x16, 0x1F, 0x11, 0x1E, + 0x10, 0x11, 0x1B, 0x13, 0x21, 0x05, 0x0F, 0x22, 0x0A, 0x20, 0x0E, 0x17, 0x16, 0x1C, 0x10, 0x1B, + 0x0F, 0x0C, 0x08, 0x09, 0x0E, 0x21, 0x0C, 0x20, 0x0E, 0x1F, 0x26, 0x0C, 0x19, 0x15, 0x17, 0x12, + 0x21, 0x13, 0x0F, 0x1C, 0x0B, 0x1F, 0x0E, 0x18, 0x0D, 0x4D, 0x3A, 0x40, 0x47, 0xB0, 0x18, 0x72, + 0x30, 0xC7, 0x11, 0x1D, 0x1E, 0x78, 0x0E, 0x4C, 0x70, 0xFF, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, + 0xFE, 0xFE, 0xFE, 0xC4, 0x0E, 0x4A, 0x1A, 0x1D, 0x0F, 0x19, 0x14, 0x19, 0x11, 0x18, 0x1B, 0x11, + 0x19, 0x13, 0x1A, 0x11, 0x1B, 0x14, 0x1A, 0x0E, 0x1C, 0x10, 0x11, 0x18, 0x0D, 0x0D, 0x09, 0x0B, + 0x0A, 0x07, 0x0D, 0x0E, 0x1B, 0x0F, 0x10, 0x17, 0x0F, 0x0C, 0x11, 0x15, 0x10, 0x0D, 0x1A, 0x0F, + 0x0F, 0x17, 0x0D, 0x0E, 0x0E, 0x0D, 0x1A, 0x0E, 0x0E, 0x1A, 0x0E, 0x10, 0x15, 0x0D, 0x0F, 0x0D, + 0x19, 0x0E, 0x0E, 0x1B, 0x0D, 0x0D, 0x19, 0x0C, 0x0E, 0x0D, 0x1B, 0x0D, 0x0D, 0x1D, 0x0C, 0x0C, + 0x0D, 0x1A, 0x0D, 0x0D, 0x1D, 0x0C, 0x0E, 0x0C, 0x03, 0x0D, 0x0B, 0x0C, 0x0D, 0x1B, 0x0C, 0x0E, + 0x1E, 0x0B, 0x0E, 0x1D, 0x0C, 0x0C, 0x0B, 0x1D, 0x0D, 0x0B, 0x20, 0x0C, 0x0D, 0x0C, 0x05, 0x0C, + 0x0B, 0x0C, 0x0C, 0x0A, 0x07, 0x0A, 0x0E, 0x0D, 0x0F, 0x02, 0x0C, 0x0C, 0x0D, 0x0C, 0x05, 0x0C, + 0x0A, 0x0C, 0x0C, 0x20, 0x0B, 0x09, 0x21, 0x0C, 0x0D, 0x1D, 0x0B, 0x0B, 0x0C, 0x22, 0x09, 0x0D, + 0x04, 0x0A, 0x0D, 0x0C, 0x0E, 0x1C, 0x0B, 0x0B, 0x0D, 0x0C, 0x20, 0x15, 0x15, 0x0C, 0x0D, 0x1D, + 0x0A, 0x0C, 0x0B, 0x0D, 0x0C, 0x09, 0x0A, 0x16, 0x15, 0x0C, 0x0D, 0x0C, 0x05, 0x0B, 0x09, 0x0B, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0A, 0x16, 0x13, 0x0B, 0x0D, 0x0D, 0x06, 0x0B, 0x08, 0x0C, 0x0B, 0x0C, + 0x0C, 0x0C, 0x0E, 0x11, 0x13, 0x0B, 0x0E, 0x10, 0x02, 0x0A, 0x09, 0x0C, 0x0B, 0x0C, 0x0C, 0x0C, + 0x0D, 0x12, 0x14, 0x0B, 0x0D, 0x0E, 0x05, 0x0A, 0x09, 0x0C, 0x0B, 0x0A, 0x0C, 0x0E, 0x0C, 0x15, + 0x12, 0x0C, 0x0E, 0x0E, 0x04, 0x0B, 0x09, 0x0B, 0x0C, 0x0A, 0x0B, 0x0D, 0x0F, 0x14, 0x16, 0x0B, + 0x0D, 0x0B, 0x06, 0x0B, 0x09, 0x0B, 0x0C, 0x0C, 0x09, 0x0C, 0x30, 0x05, 0x09, 0x0B, 0x0C, 0x0C, + 0x06, 0x0B, 0x09, 0x0B, 0x0B, 0x0B, 0x0A, 0x0B, 0x17, 0x0D, 0x0C, 0x07, 0x0A, 0x0A, 0x0B, 0x0B, + 0x07, 0x0A, 0x0A, 0x0B, 0x0B, 0x0B, 0x08, 0x0A, 0x0D, 0x18, 0x0D, 0x0A, 0x0A, 0x0A, 0x0B, 0x0B, + 0x06, 0x0B, 0x0A, 0x0B, 0x0C, 0x0B, 0x06, 0x09, 0x0E, 0x26, 0x02, 0x09, 0x0B, 0x0A, 0x0C, 0x09, + 0x08, 0x09, 0x0B, 0x0B, 0x0E, 0x15, 0x0A, 0x12, 0x08, 0x0C, 0x0B, 0x09, 0x08, 0x0C, 0x0B, 0x0A, + 0x09, 0x0A, 0x09, 0x0B, 0x0B, 0x09, 0x03, 0x09, 0x0C, 0x0E, 0x0F, 0x09, 0x0B, 0x0B, 0x0B, 0x09, + 0x0B, 0x0A, 0x07, 0x0B, 0x0B, 0x0B, 0x16, 0x0A, 0x0C, 0x19, 0x03, 0x0D, 0x06, 0x0E, 0x09, 0x0B, + 0x0C, 0x0A, 0x09, 0x0B, 0x0A, 0x0C, 0x16, 0x0A, 0x0B, 0x18, 0x04, 0x0B, 0x08, 0x0D, 0x0A, 0x0C, + 0x0C, 0x09, 0x05, 0x0C, 0x0C, 0x0B, 0x0F, 0x12, 0x0B, 0x18, 0x06, 0x0B, 0x09, 0x0C, 0x0D, 0x0C, + 0x0E, 0x19, 0x0A, 0x0C, 0x0A, 0x09, 0x08, 0x0A, 0x10, 0x0F, 0x04, 0x0B, 0x0A, 0x0A, 0x12, 0x0B, + 0x0C, 0x09, 0x08, 0x0C, 0x0B, 0x0D, 0x05, 0x07, 0x0B, 0x0F, 0x0A, 0x44, 0x0A, 0x0C, 0x0C, 0x09, + 0x04, 0x0B, 0x0C, 0x0A, 0x09, 0x07, 0x0B, 0x0B, 0x09, 0x25, 0x0E, 0x18, 0x0C, 0x0B, 0x0C, 0x08, + 0x04, 0x0C, 0x0C, 0x17, 0x0D, 0x0A, 0x18, 0x1D, 0x07, 0x15, 0x0A, 0x04, 0x09, 0x0B, 0x0B, 0x1B, + 0x0B, 0x17, 0x0B, 0x10, 0x13, 0x1A, 0x0A, 0x13, 0x0E, 0x08, 0x08, 0x0C, 0x08, 0x0A, 0x06, 0x0D, + 0x0A, 0x18, 0x0C, 0x13, 0x0A, 0x4E, 0x13, 0x0B, 0x1A, 0x0A, 0x0C, 0x19, 0x08, 0x17, 0x08, 0x19, + 0x09, 0x18, 0x0A, 0x14, 0x0E, 0x0F, 0x14, 0x1C, 0x09, 0x0A, 0x0B, 0x19, 0x0B, 0x0F, 0x11, 0x2E, + 0x06, 0x08, 0x0E, 0x19, 0x08, 0x19, 0x0B, 0x1A, 0x08, 0x0F, 0x14, 0x0E, 0x11, 0x19, 0x08, 0x19, + 0x0C, 0x12, 0x13, 0x81, 0x07, 0x17, 0x09, 0x2F, 0x1A, 0x83, 0x0B, 0x12, 0x2D, 0x1E, 0x09, 0x54, + 0x0B, 0x37, 0x2C, 0xFE, 0x30, 0x36, 0x59, 0x54, 0xFE, 0xFE, 0x09, 0x44, 0xC5, 0x1A, 0xFE, 0xFF, + 0xFE, 0xFE, 0xFE, 0xFE, 0x02, 0x03, 0x02, 0x4C, 0x03, 0x03, 0x02, 0x5B, 0x03, 0x03, 0x02, 0x35, + 0x02, 0x02, 0x03, 0x02, 0x03, 0x03, 0x03, 0x02, 0x03, 0x02, 0x03, 0x03, 0x02, 0x04, 0x01, 0x11, + 0x01, 0x55, 0x0F, 0x01, 0x03, 0x03, 0x02, 0x6B, 0x15, 0x03, 0x02, 0x15, 0x03, 0x02, 0x03, 0x04, + 0x01, 0x20, 0x02, 0x01, 0x04, 0x02, 0x03, 0x03, 0x02, 0x04, 0x02, 0x21, 0x03, 0x2B, 0x03, 0x02, + 0x03, 0x01, 0x04, 0x09, 0x02, 0x03, 0x02, 0x38, 0x03, 0x79, 0x19, 0x02, 0x03, 0x57, 0x03, 0x02, + 0x02, 0x2C, 0x21, 0x01, 0x03, 0x10, 0x02, 0x2C, 0x02, 0x02, 0x03, 0x02, 0x03, 0x1A, 0x02, 0x0A, + 0x07, 0x02, 0x03, 0x02, 0x03, 0x23, 0x03, 0x01, 0x03, 0x03, 0x03, 0x03, 0x03, 0x11, 0x02, 0x02, + 0x03, 0x03, 0x02, 0x3B, 0x01, 0x11, 0x02, 0x04, 0x01, 0x03, 0x02, 0x1B, 0x07, 0x1C, 0x03, 0x02, + 0x04, 0x01, 0x0A, 0x01, 0x03, 0x1E, 0x02, 0x04, 0x01, 0x19, 0x01, 0x09, 0x04, 0x01, 0x03, 0x03, + 0x02, 0x04, 0x02, 0x22, 0x0A, 0x02, 0x02, 0x11, 0x02, 0x03, 0x02, 0x03, 0x03, 0x02, 0x03, 0x03, + 0x03, 0x03, 0x02, 0x03, 0x02, 0x03, 0x0F, 0x16, 0x02, 0x02, 0x03, 0x03, 0x01, 0x10, 0x02, 0x02, + 0x03, 0x02, 0x03, 0x02, 0x02, 0x0A, 0x03, 0x02, 0x03, 0x1E, 0x03, 0x01, 0x09, 0x10, 0x03, 0x01, + 0x09, 0x02, 0x03, 0x0E, 0x03, 0x01, 0x04, 0x02, 0x02, 0x03, 0x01, 0x11, 0x02, 0x39, 0x0C, 0x02, + 0x03, 0x29, 0x01, 0x02, 0x09, 0x01, 0x03, 0x03, 0x03, 0x03, 0x02, 0x22, 0x03, 0x1C, 0x14, 0x1A, + 0x03, 0x02, 0x03, 0x03, 0x03, 0xFE, 0x5B, 0x1A, 0x1C, 0x05, 0x05, 0x0C, 0x25, 0x08, 0x23, 0x1E, + 0x09, 0x03, 0x0E, 0x07, 0x05, 0x0A, 0x1A, 0x08, 0x05, 0x06, 0x1B, 0x2B, 0x0B, 0x02, 0x0D, 0x08, + 0x06, 0x04, 0x1A, 0x17, 0x18, 0x20, 0x02, 0x0B, 0x0A, 0x03, 0x0C, 0x08, 0x06, 0x04, 0x1A, 0x0B, + 0x23, 0x22, 0x01, 0x05, 0x02, 0x03, 0x0B, 0x03, 0x0C, 0x09, 0x06, 0x03, 0x19, 0x14, 0x19, 0x18, + 0x02, 0x09, 0x03, 0x04, 0x02, 0x04, 0x0A, 0x04, 0x07, 0x0B, 0x25, 0x12, 0x1B, 0x16, 0x06, 0x08, + 0x03, 0x06, 0x01, 0x03, 0x0B, 0x04, 0x06, 0x0B, 0x1A, 0x14, 0x23, 0x16, 0x07, 0x0B, 0x03, 0x0A, + 0x0B, 0x04, 0x06, 0x0B, 0x18, 0x16, 0x19, 0x1F, 0x0D, 0x0C, 0x02, 0x05, 0x03, 0x02, 0x0B, 0x04, + 0x04, 0x0C, 0x19, 0x15, 0x1B, 0x18, 0x13, 0x0F, 0x01, 0x05, 0x04, 0x01, 0x0B, 0x05, 0x03, 0x0C, + 0x1A, 0x14, 0x18, 0x15, 0x15, 0x13, 0x04, 0x03, 0x04, 0x01, 0x0B, 0x05, 0x04, 0x0B, 0x18, 0x16, + 0x17, 0x16, 0x15, 0x17, 0x05, 0x03, 0x11, 0x06, 0x04, 0x09, 0x18, 0x16, 0x17, 0x15, 0x16, 0x1A, + 0x05, 0x03, 0x11, 0x05, 0x06, 0x07, 0x1A, 0x17, 0x1A, 0x10, 0x19, 0x1A, 0x1A, 0x05, 0x29, 0x16, + 0x17, 0x14, 0x19, 0x16, 0x04, 0x07, 0x1A, 0x05, 0x06, 0x08, 0x1A, 0x17, 0x0C, 0x16, 0x41, 0x08, + 0x0B, 0x02, 0x0C, 0x07, 0x06, 0x07, 0x19, 0x16, 0x0E, 0x1A, 0x4E, 0x0B, 0x0A, 0x03, 0x0A, 0x16, + 0x19, 0x17, 0x18, 0x17, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, + 0xFE, 0xFE, 0xFE, 0xFE, 0xEE, 0x02, 0x03, 0x03, 0xFE, 0x3D, 0x08, 0x01, 0x03, 0x02, 0x03, 0x03, + 0x02, 0x03, 0x02, 0x65, 0x1A, 0xCF, 0x02, 0x3E, 0x02, 0x03, 0x02, 0x03, 0x02, 0x03, 0x02, 0x03, + 0x03, 0x3E, 0x03, 0x02, 0x02, 0x03, 0x02, 0x04, 0x02, 0x1B, 0x02, 0x16, 0x02, 0x03, 0x02, 0x5F, + 0x08, 0x01, 0x03, 0x03, 0x02, 0x04, 0x02, 0xED, 0x02, 0x03, 0x02, 0x16, 0x02, 0xFE, 0x4A, 0x40, + 0xE7, 0x14, 0x6F, 0x02, 0x03, 0x02, 0x80, 0x02, 0x03, 0x02, 0xFE, 0xFE, 0xFE, 0xFE, 0xFF, 0xFE, + 0x7C, 0x24, 0x9B, 0x03, 0x02, 0x03, 0x03, 0x02, 0x03, 0x03, 0x0E, 0x08, 0x01, 0x03, 0x52, 0x02, + 0x03, 0x09, 0x01, 0x03, 0x03, 0x02, 0x03, 0x03, 0x03, 0x01, 0x0A, 0x01, 0x03, 0x02, 0x1F, 0x02, + 0x02, 0x03, 0x03, 0x07, 0x14, 0x02, 0x03, 0x02, 0x03, 0x03, 0x3A, 0x03, 0x02, 0x09, 0x02, 0x02, + 0x3B, 0x01, 0x16, 0x01, 0x03, 0x03, 0x01, 0x03, 0x02, 0x03, 0x23, 0x03, 0x02, 0x03, 0x02, 0x03, + 0x03, 0x04, 0x02, 0x02, 0x1E, 0x08, 0x0E, 0x02, 0x03, 0x03, 0x02, 0x02, 0x03, 0x02, 0x41, 0x01, + 0x03, 0x02, 0x11, 0x1A, 0x02, 0x17, 0x02, 0x03, 0x03, 0x02, 0x0D, 0x03, 0x01, 0x03, 0x02, 0x03, + 0x02, 0x04, 0x01, 0x03, 0x10, 0x01, 0x03, 0x01, 0x34, 0x02, 0x03, 0x02, 0x04, 0x19, 0x01, 0x03, + 0x01, 0x0F, 0x02, 0x03, 0x02, 0x03, 0x02, 0x03, 0x02, 0x02, 0x10, 0x01, 0x03, 0x02, 0x03, 0x02, + 0x16, 0x02, 0x03, 0x02, 0x03, 0x03, 0x03, 0x02, 0x0E, 0x02, 0x02, 0x02, 0x03, 0x01, 0x0B, 0x02, + 0x02, 0x02, 0x03, 0x02, 0x03, 0x01, 0x0A, 0x03, 0x01, 0x03, 0x03, 0x02, 0x18, 0x02, 0x03, 0x02, + 0x1D, 0x02, 0x04, 0x01, 0x03, 0x03, 0x03, 0x02, 0x03, 0x02, 0x03, 0x03, 0x02, 0x03, 0x02, 0x03, + 0x02, 0x03, 0x15, 0x02, 0x03, 0x02, 0x04, 0x01, 0x0C, 0x02, 0x10, 0x02, 0x03, 0x02, 0x1A, 0x02, + 0x0F, 0x02, 0x02, 0x03, 0x01, 0x04, 0x02, 0x02, 0x14, 0x03, 0x02, 0x03, 0x02, 0x03, 0x04, 0x01, + 0x0B, 0x01, 0x03, 0x02, 0x03, 0x02, 0x15, 0x02, 0x02, 0x03, 0x03, 0x02, 0x05, 0x02, 0x02, 0x03, + 0x02, 0x03, 0x02, 0x03, 0x12, 0x01, 0x03, 0x02, 0x03, 0x02, 0x03, 0x01, 0x04, 0x01, 0x03, 0x01, + 0x16, 0x02, 0x28, 0x02, 0x02, 0x0E, 0x02, 0x03, 0x19, 0x03, 0x02, 0x03, 0x02, 0x02, 0x11, 0x01, + 0x03, 0x03, 0x02, 0x02, 0x02, 0x02, 0x04, 0x02, 0x04, 0x01, 0x04, 0x01, 0x04, 0x01, 0x18, 0x02, + 0x04, 0x02, 0x03, 0x02, 0x04, 0x01, 0x0A, 0x01, 0x0C, 0x02, 0x01, 0x0B, 0x02, 0x03, 0x02, 0x04, + 0x01, 0x03, 0x02, 0x03, 0x03, 0x03, 0x02, 0x03, 0x02, 0x03, 0x02, 0x02, 0x26, 0x02, 0x10, 0x02, + 0x0F, 0x03, 0x01, 0x04, 0x02, 0x03, 0x02, 0x03, 0x02, 0x02, 0x03, 0x02, 0x0B, 0x03, 0x02, 0x03, + 0x19, 0x02, 0x03, 0x02, 0x2A, 0x03, 0x01, 0x04, 0x01, 0x03, 0x03, 0x03, 0x34, 0x02, 0x02, 0x02, + 0x03, 0x02, 0x08, 0x02, 0x03, 0x03, 0x03, 0x02, 0x0C, 0x03, 0x01, 0x03, 0x02, 0x02, 0x14, 0x02, + 0x03, 0x02, 0x03, 0x01, 0x04, 0x02, 0x03, 0x02, 0x03, 0x02, 0x03, 0x01, 0x0C, 0x02, 0x02, 0x03, + 0x03, 0x01, 0x3E, 0x03, 0x01, 0x11, 0x02, 0x03, 0x02, 0x03, 0x03, 0x03, 0x02, 0x03, 0x01, 0x03, + 0x02, 0x03, 0x03, 0x02, 0x2D, 0x02, 0x35, 0x37, 0x5D, 0x22, 0x1C, 0x15, 0x1D, 0x15, 0x1A, 0x28, + 0x13, 0x01, 0x05, 0x09, 0x0C, 0x02, 0x0C, 0x02, 0x0B, 0x0A, 0x22, 0x21, 0x03, 0x05, 0x0B, 0x0A, + 0x05, 0x08, 0x0C, 0x02, 0x0A, 0x0E, 0x02, 0x05, 0x18, 0x1A, 0x06, 0x10, 0x01, 0x05, 0x0B, 0x03, + 0x0C, 0x08, 0x0C, 0x03, 0x0A, 0x0B, 0x04, 0x05, 0x0A, 0x04, 0x0B, 0x05, 0x13, 0x03, 0x03, 0x11, + 0x02, 0x04, 0x0B, 0x0C, 0x03, 0x08, 0x19, 0x0D, 0x14, 0x15, 0x13, 0x14, 0x02, 0x02, 0x04, 0x03, + 0x0B, 0x16, 0x11, 0x02, 0x04, 0x0B, 0x17, 0x13, 0x0D, 0x1C, 0x0B, 0x03, 0x0A, 0x16, 0x11, 0x12, + 0x17, 0x09, 0x18, 0x1E, 0x0A, 0x05, 0x08, 0x0E, 0x05, 0x02, 0x11, 0x13, 0x18, 0x07, 0x17, 0x14, + 0x02, 0x09, 0x03, 0x03, 0x03, 0x04, 0x0A, 0x15, 0x10, 0x14, 0x0F, 0x01, 0x06, 0x08, 0x17, 0x14, + 0x02, 0x0B, 0x0A, 0x03, 0x09, 0x13, 0x12, 0x14, 0x0E, 0x02, 0x06, 0x08, 0x17, 0x0A, 0x06, 0x02, + 0x05, 0x0D, 0x08, 0x03, 0x09, 0x0F, 0x14, 0x15, 0x0F, 0x12, 0x16, 0x09, 0x0E, 0x0F, 0x14, 0x0F, + 0x14, 0x16, 0x0D, 0x12, 0x17, 0x0A, 0x0F, 0x0E, 0x09, 0x02, 0x09, 0x0D, 0x15, 0x0E, 0x03, 0x05, + 0x0B, 0x14, 0x0C, 0x13, 0x0A, 0x15, 0x0B, 0x03, 0x08, 0x0C, 0x16, 0x0C, 0x07, 0x03, 0x0B, 0x0A, + 0x15, 0x14, 0x0C, 0x14, 0x0C, 0x04, 0x07, 0x0B, 0x17, 0x0B, 0x08, 0x05, 0x0A, 0x08, 0x08, 0x05, + 0x08, 0x09, 0x07, 0x05, 0x09, 0x16, 0x0A, 0x06, 0x08, 0x0B, 0x18, 0x0B, 0x05, 0x06, 0x0C, 0x07, + 0x06, 0x08, 0x18, 0x06, 0x0A, 0x08, 0x03, 0x0A, 0x0B, 0x05, 0x08, 0x0C, 0x17, 0x0C, 0x04, 0x07, + 0x0A, 0x09, 0x05, 0x08, 0x09, 0x08, 0x06, 0x08, 0x08, 0x09, 0x04, 0x09, 0x0B, 0x05, 0x08, 0x0B, + 0x18, 0x0C, 0x04, 0x07, 0x0C, 0x05, 0x08, 0x09, 0x07, 0x07, 0x0A, 0x09, 0x05, 0x09, 0x06, 0x09, + 0x0A, 0x06, 0x09, 0x0B, 0x0B, 0x02, 0x0C, 0x0A, 0x03, 0x09, 0x0B, 0x04, 0x08, 0x0C, 0x04, 0x07, + 0x0B, 0x05, 0x08, 0x0B, 0x05, 0x08, 0x0B, 0x07, 0x08, 0x0B, 0x09, 0x05, 0x0E, 0x06, 0x07, 0x0D, + 0x15, 0x17, 0x17, 0x0C, 0x08, 0x08, 0x0B, 0x06, 0x0C, 0x0A, 0x0A, 0x04, 0x1B, 0x0D, 0x16, 0x19, + 0x1D, 0x05, 0x0E, 0x08, 0x0A, 0x05, 0x3F, 0x31, 0x43, 0x20, 0xB4, 0x23, 0xBE, 0x39, 0x80, 0x16, + 0x10, 0x24, 0xFE, 0xBF, 0x81, 0x75, 0x19, 0x73, 0x11, 0x5C, 0x1C, 0xE7, 0x1B, 0xA1, 0x15, 0x38, + 0x1C, 0x83, 0x2E, 0x42, 0x1A, 0x8B, 0x16, 0x5B, 0x3B, 0xEC, 0x31, 0xFE, 0x47, 0x75, 0xFE, 0xFE, + 0xFE, 0xFE, 0xFE, 0xFE, 0xFF, 0xFE, 0xFE, 0x20, 0x03, 0x04, 0x05, 0x17, 0x1A, 0x22, 0x0A, 0x1F, + 0x1B, 0x3E, 0x1C, 0x18, 0x12, 0x20, 0x10, 0x1F, 0x0A, 0x17, 0x18, 0x05, 0x02, 0x19, 0x13, 0x17, + 0x17, 0x3A, 0x1B, 0x19, 0x12, 0x16, 0x09, 0x02, 0x09, 0x1E, 0x1B, 0x03, 0x0F, 0x02, 0x06, 0x19, + 0x04, 0x22, 0x05, 0x04, 0x0D, 0x26, 0x18, 0x01, 0x06, 0x03, 0x05, 0x03, 0x02, 0x13, 0x04, 0x05, + 0x05, 0x18, 0x04, 0x03, 0x15, 0x1F, 0x23, 0x02, 0x06, 0x02, 0x03, 0x06, 0x01, 0x0A, 0x04, 0x05, + 0x05, 0x18, 0x03, 0x07, 0x09, 0x03, 0x06, 0x1E, 0x19, 0x01, 0x07, 0x03, 0x05, 0x18, 0x05, 0x04, + 0x07, 0x16, 0x02, 0x07, 0x09, 0x05, 0x04, 0x20, 0x1F, 0x03, 0x05, 0x19, 0x04, 0x05, 0x07, 0x16, + 0x02, 0x0F, 0x02, 0x05, 0x05, 0x1D, 0x1D, 0x03, 0x05, 0x04, 0x02, 0x12, 0x05, 0x03, 0x13, 0x1B, + 0x0D, 0x20, 0x1A, 0x03, 0x04, 0x05, 0x01, 0x14, 0x03, 0x04, 0x05, 0x21, 0x03, 0x03, 0x11, 0x1B, + 0x1A, 0x02, 0x05, 0x04, 0x02, 0x1B, 0x15, 0x24, 0x09, 0x1F, 0x0F, 0x02, 0x06, 0x03, 0x02, 0x1B, + 0x19, 0x24, 0x09, 0x20, 0x0A, 0x02, 0x06, 0x03, 0x03, 0x1A, 0x04, 0x05, 0x16, 0x3D, 0x04, 0x03, + 0x0B, 0x03, 0x05, 0x03, 0x04, 0x1B, 0x02, 0x09, 0x15, 0x04, 0x03, 0x23, 0x25, 0x03, 0x05, 0x03, + 0x04, 0x2F, 0x13, 0x05, 0x02, 0x22, 0x03, 0x04, 0x04, 0x04, 0x12, 0x02, 0x06, 0x03, 0x04, 0x1C, + 0x01, 0x07, 0x03, 0x07, 0x11, 0x4A, 0x08, 0x02, 0x06, 0x03, 0x04, 0x25, 0x1D, 0x24, 0x0B, 0x03, + 0x03, 0x13, 0x0B, 0x03, 0x05, 0x03, 0x05, 0x23, 0x12, 0x27, 0x06, 0x03, 0x06, 0x17, 0x03, 0x03, + 0x10, 0x03, 0x05, 0x03, 0x04, 0x24, 0x04, 0x06, 0x04, 0x13, 0x03, 0x0A, 0x02, 0x04, 0x10, 0x24, + 0x11, 0x02, 0x05, 0x04, 0x04, 0x25, 0x03, 0x22, 0x04, 0x3F, 0x16, 0x03, 0x05, 0x03, 0x04, 0x47, + 0x10, 0x06, 0x03, 0x08, 0x44, 0x03, 0x04, 0x04, 0x04, 0x47, 0x05, 0x06, 0x04, 0x04, 0x04, 0x06, + 0x05, 0x39, 0x11, 0x02, 0x06, 0x02, 0x05, 0x2E, 0x05, 0x04, 0x13, 0x10, 0x1D, 0x3D, 0x07, 0x03, + 0x05, 0x03, 0x04, 0x39, 0x38, 0x50, 0x04, 0x03, 0x05, 0x03, 0x04, 0x04, 0x02, 0x1D, 0x02, 0x05, + 0x2C, 0x11, 0x03, 0x09, 0x02, 0x06, 0x01, 0x2D, 0x1E, 0x02, 0x05, 0x03, 0x05, 0x03, 0x02, 0x04, + 0x02, 0x1E, 0x09, 0x03, 0x03, 0x04, 0x03, 0x25, 0x03, 0x0B, 0x01, 0x05, 0x10, 0x1E, 0x03, 0x06, + 0x34, 0x32, 0x1E, 0x80, 0x04, 0x03, 0x05, 0x03, 0x05, 0xD7, 0x19, 0x03, 0x18, 0xF5, 0x1B, 0xFE, + 0xFE, 0x0D, 0x8F, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, + 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFF, 0xFE, 0xFE, 0x0A, 0x1A, 0xB8, 0x1B, 0xB9, 0x0D, 0x5F, 0x2F, + 0x33, 0x0D, 0x1A, 0x08, 0x1C, 0x09, 0x18, 0x0C, 0x55, 0x09, 0x0C, 0x07, 0x0A, 0x07, 0x11, 0x03, + 0x0A, 0x0A, 0x0E, 0x15, 0x0B, 0x0A, 0x1B, 0x13, 0x0E, 0x0A, 0x0B, 0x09, 0x0A, 0x07, 0x0E, 0x06, + 0x0A, 0x09, 0x0B, 0x0B, 0x05, 0x07, 0x0A, 0x09, 0x1C, 0x14, 0x09, 0x0E, 0x0B, 0x0B, 0x09, 0x06, + 0x0E, 0x07, 0x0A, 0x09, 0x0A, 0x18, 0x09, 0x0A, 0x1D, 0x0F, 0x0B, 0x0D, 0x0C, 0x0A, 0x09, 0x08, + 0x0B, 0x0B, 0x07, 0x0A, 0x0A, 0x0C, 0x56, 0x0D, 0x0C, 0x0A, 0x09, 0x08, 0x0C, 0x0A, 0x0A, 0x07, + 0x0B, 0x0C, 0x0F, 0x07, 0x2B, 0x0B, 0x07, 0x0B, 0x09, 0x0B, 0x0A, 0x09, 0x0C, 0x08, 0x0B, 0x09, + 0x0A, 0x0B, 0x0B, 0x08, 0x3E, 0x0A, 0x0A, 0x0B, 0x0C, 0x09, 0x0C, 0x08, 0x0B, 0x0A, 0x09, 0x0A, + 0x09, 0x0A, 0x0D, 0x09, 0x23, 0x0B, 0x0A, 0x0C, 0x0B, 0x07, 0x0D, 0x08, 0x0C, 0x09, 0x09, 0x0A, + 0x0B, 0x0B, 0x3B, 0x0B, 0x08, 0x0C, 0x0A, 0x09, 0x0C, 0x0A, 0x0B, 0x08, 0x0A, 0x0A, 0x0A, 0x0B, + 0x22, 0x09, 0x0D, 0x0D, 0x06, 0x0C, 0x0B, 0x09, 0x0C, 0x09, 0x0B, 0x09, 0x0A, 0x09, 0x0B, 0x0A, + 0x24, 0x08, 0x0D, 0x09, 0x0A, 0x0C, 0x0C, 0x08, 0x0B, 0x0A, 0x0B, 0x0A, 0x09, 0x0A, 0x09, 0x0B, + 0x10, 0x06, 0x0D, 0x08, 0x0F, 0x08, 0x0B, 0x0B, 0x0D, 0x08, 0x0B, 0x0A, 0x0B, 0x09, 0x09, 0x0A, + 0x0A, 0x0B, 0x0F, 0x07, 0x0C, 0x0A, 0x0C, 0x09, 0x0B, 0x0C, 0x0D, 0x07, 0x0C, 0x0A, 0x0A, 0x0A, + 0x0A, 0x09, 0x0A, 0x0A, 0x0F, 0x09, 0x0B, 0x0A, 0x0F, 0x0C, 0x06, 0x0C, 0x0C, 0x08, 0x0C, 0x0A, + 0x0A, 0x0A, 0x09, 0x0B, 0x09, 0x0B, 0x0C, 0x09, 0x0D, 0x0A, 0x0E, 0x0F, 0x07, 0x0C, 0x0B, 0x09, + 0x0B, 0x0A, 0x0B, 0x09, 0x09, 0x0A, 0x0A, 0x0B, 0x0C, 0x0A, 0x0D, 0x09, 0x15, 0x0C, 0x05, 0x04, + 0x02, 0x05, 0x0E, 0x06, 0x0C, 0x0A, 0x0B, 0x09, 0x09, 0x0B, 0x0A, 0x0A, 0x0C, 0x08, 0x0E, 0x08, + 0x0F, 0x05, 0x04, 0x0C, 0x0A, 0x09, 0x0B, 0x08, 0x0B, 0x0A, 0x0B, 0x09, 0x09, 0x0A, 0x0A, 0x0B, + 0x44, 0x0D, 0x06, 0x0C, 0x0D, 0x06, 0x0C, 0x0A, 0x0B, 0x09, 0x09, 0x0B, 0x0B, 0x0B, 0x45, 0x0C, + 0x0B, 0x0A, 0x0B, 0x07, 0x0C, 0x09, 0x0B, 0x08, 0x0A, 0x0A, 0x0C, 0x0B, 0x22, 0x09, 0x0D, 0x19, + 0x08, 0x0B, 0x11, 0x02, 0x0C, 0x0A, 0x0B, 0x08, 0x08, 0x0B, 0x0C, 0x0B, 0x3B, 0x1A, 0x06, 0x0C, + 0x21, 0x0A, 0x0B, 0x1C, 0x0D, 0x0C, 0x3A, 0x1B, 0x07, 0x0B, 0x0D, 0x07, 0x0C, 0x09, 0x0E, 0x09, + 0x4D, 0x09, 0x0D, 0x0F, 0x04, 0x0B, 0x08, 0x0A, 0x0D, 0x07, 0x0C, 0x0A, 0x0B, 0x0A, 0x0A, 0x0C, + 0x64, 0x0B, 0x09, 0x0A, 0x0B, 0x09, 0x0B, 0x09, 0x0C, 0x09, 0x0A, 0x08, 0x0B, 0x0B, 0x57, 0x0C, + 0x06, 0x0B, 0x0B, 0x09, 0x0C, 0x06, 0x0C, 0x09, 0x0A, 0x0A, 0x0A, 0x08, 0x0A, 0x09, 0x45, 0x0D, + 0x0B, 0x0B, 0x0A, 0x09, 0x0A, 0x09, 0x0A, 0x0A, 0x09, 0x09, 0x0C, 0x09, 0x65, 0x08, 0x0A, 0x0B, + 0x0B, 0x05, 0x0D, 0x08, 0xA4, 0x08, 0x0B, 0x0A, 0x0B, 0x05, 0x0E, 0x05, 0x0D, 0x05, 0x98, 0x08, + 0x0B, 0x0B, 0x0A, 0x04, 0x99, 0x1A, 0x08, 0x0E, 0x0B, 0x06, 0x10, 0x03, 0x1B, 0x0A, 0x2C, 0x09, + 0x5E, 0x12, 0x0C, 0x06, 0xD9, 0x09, 0x1A, 0x08, 0x3C, 0x07, 0x87, 0x07, 0x0E, 0x02, 0x09, 0x09, + 0x59, 0x07, 0x6A, 0x07, 0x1A, 0x16, 0xC4, 0x07, 0xFA, 0x07, 0xFE, 0x03, 0x17, 0xFE, 0xFE, 0xFE, + 0xFF, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0x9C, 0x6F, 0xFE, 0x97, 0x39, 0x70, + 0x18, 0x16, 0x0D, 0x17, 0x18, 0x1B, 0x12, 0x17, 0x15, 0x17, 0x0E, 0x18, 0x12, 0x20, 0x16, 0x0A, + 0x0B, 0x02, 0x06, 0x10, 0x04, 0x03, 0x0D, 0x15, 0x14, 0x14, 0x15, 0x0A, 0x05, 0x05, 0x0D, 0x04, + 0x03, 0x10, 0x06, 0x03, 0x0B, 0x16, 0x13, 0x14, 0x0C, 0x16, 0x18, 0x04, 0x03, 0x0F, 0x07, 0x04, + 0x0A, 0x15, 0x14, 0x14, 0x13, 0x0F, 0x1A, 0x16, 0x09, 0x03, 0x09, 0x15, 0x14, 0x15, 0x0A, 0x16, + 0x1A, 0x16, 0x08, 0x04, 0x09, 0x15, 0x14, 0x16, 0x08, 0x16, 0x17, 0x05, 0x02, 0x05, 0x01, 0x08, + 0x08, 0x03, 0x09, 0x15, 0x15, 0x15, 0x0A, 0x15, 0x17, 0x06, 0x02, 0x0D, 0x09, 0x03, 0x09, 0x15, + 0x0E, 0x1C, 0x09, 0x15, 0x07, 0x03, 0x0A, 0x04, 0x03, 0x03, 0x02, 0x08, 0x08, 0x03, 0x0A, 0x14, + 0x15, 0x0E, 0x0F, 0x15, 0x06, 0x04, 0x09, 0x06, 0x07, 0x09, 0x08, 0x03, 0x09, 0x15, 0x13, 0x15, + 0x09, 0x07, 0x02, 0x0C, 0x04, 0x04, 0x0A, 0x05, 0x07, 0x0A, 0x06, 0x04, 0x0A, 0x13, 0x14, 0x15, + 0x09, 0x07, 0x04, 0x09, 0x12, 0x06, 0x01, 0x04, 0x01, 0x09, 0x07, 0x04, 0x09, 0x14, 0x14, 0x15, + 0x0A, 0x1D, 0x08, 0x04, 0x03, 0x03, 0x02, 0x09, 0x06, 0x04, 0x0A, 0x13, 0x15, 0x15, 0x14, 0x12, + 0x08, 0x04, 0x03, 0x03, 0x02, 0x09, 0x07, 0x03, 0x0A, 0x14, 0x15, 0x15, 0x0B, 0x04, 0x06, 0x0F, + 0x08, 0x04, 0x04, 0x0E, 0x06, 0x04, 0x09, 0x14, 0x15, 0x16, 0x14, 0x0F, 0x08, 0x04, 0x09, 0x08, + 0x07, 0x04, 0x0A, 0x13, 0x15, 0x15, 0x07, 0x1F, 0x07, 0x03, 0x05, 0x03, 0x01, 0x09, 0x06, 0x04, + 0x09, 0x14, 0x14, 0x17, 0x06, 0x06, 0x06, 0x12, 0x08, 0x03, 0x09, 0x09, 0x06, 0x04, 0x0A, 0x13, + 0x15, 0x16, 0x07, 0x06, 0x05, 0x13, 0x07, 0x04, 0x09, 0x09, 0x05, 0x05, 0x0A, 0x13, 0x15, 0x16, + 0x06, 0x08, 0x03, 0x14, 0x08, 0x03, 0x09, 0x09, 0x06, 0x04, 0x0A, 0x14, 0x14, 0x15, 0x15, 0x13, + 0x09, 0x03, 0x05, 0x0E, 0x04, 0x05, 0x09, 0x14, 0x14, 0x16, 0x14, 0x15, 0x09, 0x03, 0x05, 0x0F, + 0x04, 0x05, 0x0B, 0x12, 0x14, 0x17, 0x13, 0x15, 0x0B, 0x03, 0x05, 0x0E, 0x04, 0x05, 0x0A, 0x14, + 0x14, 0x16, 0x13, 0x18, 0x0B, 0x03, 0x04, 0x10, 0x05, 0x03, 0x0B, 0x14, 0x13, 0x18, 0x0E, 0x16, + 0x11, 0x04, 0x04, 0x0F, 0x05, 0x03, 0x0A, 0x14, 0x15, 0x16, 0x11, 0x18, 0x10, 0x03, 0x04, 0x0F, + 0x05, 0x04, 0x07, 0x17, 0x05, 0x02, 0x0F, 0x16, 0x14, 0x17, 0x06, 0x01, 0x08, 0x03, 0x04, 0x10, + 0x04, 0x04, 0x0C, 0x13, 0x03, 0x04, 0x12, 0x13, 0x10, 0x17, 0x16, 0x04, 0x03, 0x10, 0x04, 0x05, + 0x0B, 0x13, 0x03, 0x04, 0x10, 0x14, 0x03, 0x04, 0x0C, 0x1F, 0x11, 0x03, 0x04, 0x10, 0x04, 0x04, + 0x0C, 0x13, 0x04, 0x02, 0x11, 0x18, 0x16, 0x16, 0x19, 0x02, 0x04, 0x1A, 0x06, 0x20, 0x17, 0x16, + 0x16, 0x16, 0x19, 0x04, 0x03, 0x11, 0x03, 0x05, 0x06, 0x18, 0x03, 0x0A, 0x12, 0x17, 0x19, 0x12, + 0x21, 0x04, 0x04, 0x1A, 0x04, 0x28, 0x11, 0x18, 0x16, 0x16, 0x14, 0x0C, 0x07, 0x02, 0x05, 0x19, + 0x05, 0x22, 0x17, 0x05, 0x02, 0x0D, 0x1A, 0x1E, 0x0B, 0x15, 0x05, 0x03, 0x05, 0x1A, 0x03, 0x05, + 0x0B, 0x09, 0x02, 0x0B, 0x1F, 0x40, 0x11, 0x13, 0x07, 0x02, 0x05, 0x04, 0x02, 0x12, 0x15, 0x20, + 0x11, 0x19, 0x10, 0x22, 0x2D, 0x03, 0x05, 0x19, 0x05, 0x03, 0x0C, 0x1F, 0x17, 0x4A, 0x2E, 0x03, + 0x05, 0x19, 0x03, 0x3E, 0x11, 0x22, 0x0B, 0x44, 0x18, 0x1B, 0x17, 0x1B, 0x19, 0x19, 0x12, 0x4B, + 0x1D, 0xE1, 0x27, 0xF4, 0x1E, 0x3C, 0x49, 0xFE, 0xFF, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, + 0xAA, 0x1C, 0xD0, 0x2F, 0x09, 0x03, 0x0F, 0x03, 0x04, 0x01, 0x0A, 0x03, 0x42, 0x17, 0x23, 0x16, + 0x1C, 0x0E, 0x10, 0x10, 0x32, 0x09, 0x12, 0x0F, 0x03, 0x02, 0x26, 0x02, 0x22, 0x13, 0x81, 0x08, + 0x20, 0x2B, 0x11, 0x0F, 0x16, 0x03, 0x2C, 0x04, 0x21, 0x03, 0x09, 0x08, 0x08, 0x04, 0x10, 0x0D, + 0x06, 0x06, 0x04, 0x03, 0x3A, 0x03, 0x09, 0x06, 0x06, 0x09, 0x02, 0x15, 0x11, 0x18, 0x4F, 0x02, + 0x1A, 0x1A, 0x09, 0x14, 0x18, 0x06, 0x04, 0x05, 0x1A, 0x05, 0x05, 0x3E, 0x0B, 0x10, 0x19, 0x07, + 0x24, 0x06, 0x0A, 0x04, 0x04, 0x0B, 0x02, 0x1B, 0x10, 0x11, 0x18, 0x08, 0x18, 0x04, 0x08, 0x05, + 0x0A, 0x18, 0x01, 0x1A, 0x09, 0x09, 0x03, 0x06, 0x17, 0x07, 0x19, 0x05, 0x08, 0x05, 0x11, 0x1E, + 0x02, 0x0C, 0x08, 0x09, 0x02, 0x08, 0x0D, 0x03, 0x05, 0x07, 0x18, 0x04, 0x0A, 0x04, 0x17, 0x1C, + 0x01, 0x0C, 0x08, 0x0A, 0x01, 0x09, 0x0C, 0x04, 0x05, 0x06, 0x17, 0x05, 0x0B, 0x03, 0x0B, 0x04, + 0x0A, 0x06, 0x05, 0x12, 0x01, 0x06, 0x03, 0x01, 0x07, 0x15, 0x0E, 0x03, 0x03, 0x08, 0x16, 0x08, + 0x16, 0x06, 0x07, 0x06, 0x0A, 0x12, 0x02, 0x05, 0x03, 0x02, 0x05, 0x15, 0x0E, 0x10, 0x17, 0x06, + 0x17, 0x08, 0x07, 0x03, 0x0B, 0x13, 0x03, 0x04, 0x03, 0x03, 0x03, 0x15, 0x0D, 0x12, 0x18, 0x04, + 0x17, 0x08, 0x16, 0x15, 0x02, 0x06, 0x02, 0x04, 0x03, 0x0E, 0x01, 0x04, 0x0E, 0x0B, 0x02, 0x03, + 0x1A, 0x04, 0x17, 0x08, 0x15, 0x14, 0x03, 0x04, 0x03, 0x04, 0x02, 0x0F, 0x01, 0x03, 0x0E, 0x10, + 0x17, 0x07, 0x17, 0x09, 0x0F, 0x1A, 0x02, 0x06, 0x03, 0x03, 0x03, 0x0D, 0x04, 0x01, 0x11, 0x07, + 0x23, 0x03, 0x16, 0x09, 0x07, 0x04, 0x05, 0x19, 0x02, 0x06, 0x02, 0x03, 0x04, 0x0C, 0x04, 0x02, + 0x0F, 0x09, 0x05, 0x03, 0x14, 0x09, 0x14, 0x08, 0x07, 0x07, 0x06, 0x15, 0x03, 0x04, 0x03, 0x02, + 0x05, 0x0C, 0x05, 0x02, 0x0F, 0x08, 0x04, 0x04, 0x14, 0x09, 0x15, 0x08, 0x06, 0x07, 0x09, 0x13, + 0x05, 0x03, 0x0C, 0x0A, 0x05, 0x04, 0x0D, 0x09, 0x02, 0x05, 0x13, 0x0B, 0x14, 0x0B, 0x02, 0x07, + 0x09, 0x16, 0x06, 0x02, 0x0B, 0x0B, 0x06, 0x03, 0x0C, 0x08, 0x05, 0x06, 0x09, 0x03, 0x07, 0x07, + 0x15, 0x09, 0x09, 0x03, 0x0A, 0x0A, 0x02, 0x0A, 0x06, 0x06, 0x07, 0x0A, 0x06, 0x04, 0x0A, 0x06, + 0x07, 0x06, 0x08, 0x06, 0x08, 0x06, 0x09, 0x05, 0x06, 0x0E, 0x13, 0x05, 0x09, 0x0A, 0x04, 0x08, + 0x06, 0x05, 0x09, 0x05, 0x18, 0x07, 0x06, 0x07, 0x08, 0x08, 0x06, 0x09, 0x05, 0x18, 0x11, 0x08, + 0x05, 0x07, 0x05, 0x07, 0x07, 0x08, 0x05, 0x08, 0x08, 0x04, 0x08, 0x07, 0x07, 0x05, 0x08, 0x08, + 0x15, 0x17, 0x14, 0x0B, 0x11, 0x07, 0x07, 0x05, 0x15, 0x07, 0x08, 0x02, 0x0C, 0x04, 0x07, 0x07, + 0x09, 0x05, 0x05, 0x1B, 0x08, 0x12, 0x3E, 0x09, 0x58, 0x09, 0x15, 0x0C, 0x13, 0x04, 0x3C, 0x09, + 0x20, 0x07, 0x7B, 0x16, 0x05, 0x26, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0x40, 0x04, 0x3B, 0x07, 0xCB, + 0x03, 0x07, 0x08, 0x72, 0x08, 0x19, 0x07, 0x04, 0x08, 0x03, 0x03, 0x0F, 0x03, 0x01, 0x03, 0x03, + 0x02, 0x02, 0x03, 0x02, 0x02, 0x2A, 0x02, 0x18, 0x02, 0x03, 0x02, 0x5D, 0x09, 0x01, 0x03, 0x03, + 0x02, 0x11, 0x1A, 0x20, 0x01, 0x1D, 0x03, 0x03, 0x01, 0x32, 0x38, 0x03, 0x02, 0x21, 0x02, 0x51, + 0x01, 0x0A, 0x02, 0x02, 0x02, 0x03, 0x02, 0x04, 0x02, 0x02, 0x08, 0x03, 0x02, 0x0E, 0x03, 0x01, + 0x02, 0x03, 0x02, 0x44, 0x03, 0x02, 0x03, 0x03, 0x01, 0x2F, 0x02, 0x29, 0x08, 0x2A, 0x07, 0x02, + 0x04, 0x01, 0x0A, 0x02, 0x03, 0x02, 0x03, 0x03, 0x02, 0x6C, 0x15, 0x02, 0x02, 0x02, 0x03, 0x10, + 0x01, 0x17, 0x02, 0x03, 0x01, 0x11, 0x02, 0x02, 0x03, 0x50, 0x02, 0x02, 0x02, 0x38, 0x03, 0x1A, + 0x29, 0x02, 0x03, 0x03, 0x02, 0x04, 0x01, 0x67, 0x01, 0x03, 0x02, 0x03, 0x01, 0x1A, 0x02, 0x04, + 0x01, 0x0A, 0x01, 0x04, 0x01, 0x04, 0x02, 0x1A, 0x01, 0x02, 0x03, 0x03, 0x02, 0x03, 0x02, 0x03, + 0x02, 0x27, 0x01, 0x02, 0x03, 0x03, 0x01, 0x22, 0x02, 0x03, 0x03, 0x03, 0x02, 0x39, 0x0F, 0x02, + 0x03, 0x0E, 0x03, 0x2A, 0x02, 0x03, 0x02, 0xFE, 0xFE, 0x34, 0x1B, 0xED, 0x20, 0xE7, 0x21, 0x40, + 0x59, 0x57, 0x2E, 0xE9, 0x23, 0xFE, 0x01, 0x27, 0x40, 0x5B, 0xFE, 0xFE, 0x16, 0x71, 0xFE, 0xFE, + 0xFF, 0x0F, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, + 0x4D, 0x20, 0x22, 0x19, 0x1A, 0x16, 0x1B, 0x19, 0x1B, 0x19, 0x1C, 0x13, 0x1C, 0x1D, 0x19, 0x10, + 0x2E, 0x08, 0x14, 0x2B, 0x17, 0x0E, 0x4B, 0x12, 0x09, 0x13, 0x13, 0x10, 0x0E, 0x0C, 0x14, 0x09, + 0x12, 0x10, 0x0B, 0x12, 0x13, 0x10, 0x0F, 0x0A, 0x16, 0x04, 0x16, 0x0E, 0x0D, 0x12, 0x13, 0x11, + 0x0E, 0x0E, 0x12, 0x04, 0x17, 0x0E, 0x0D, 0x11, 0x13, 0x12, 0x0F, 0x08, 0x15, 0x08, 0x15, 0x0D, + 0x0B, 0x13, 0x13, 0x12, 0x0C, 0x07, 0x17, 0x04, 0x17, 0x2C, 0x22, 0x16, 0x18, 0x05, 0x17, 0x08, + 0x04, 0x07, 0x04, 0x14, 0x31, 0x06, 0x17, 0x06, 0x17, 0x28, 0x10, 0x0B, 0x13, 0x0B, 0x18, 0x05, + 0x17, 0x26, 0x12, 0x0B, 0x16, 0x06, 0x37, 0x05, 0x06, 0x18, 0x12, 0x0C, 0x12, 0x07, 0x18, 0x04, + 0x19, 0x07, 0x07, 0x04, 0x03, 0x0F, 0x12, 0x09, 0x0A, 0x03, 0x06, 0x07, 0x15, 0x0A, 0x16, 0x13, + 0x05, 0x0E, 0x12, 0x09, 0x0A, 0x03, 0x06, 0x07, 0x16, 0x09, 0x17, 0x10, 0x06, 0x0E, 0x12, 0x09, + 0x13, 0x09, 0x15, 0x0A, 0x18, 0x10, 0x04, 0x0D, 0x12, 0x0A, 0x0A, 0x03, 0x04, 0x09, 0x15, 0x0A, + 0x15, 0x08, 0x08, 0x01, 0x05, 0x0E, 0x12, 0x0A, 0x0A, 0x02, 0x05, 0x09, 0x15, 0x07, 0x17, 0x09, + 0x10, 0x0E, 0x11, 0x0A, 0x13, 0x09, 0x14, 0x09, 0x15, 0x13, 0x07, 0x0F, 0x10, 0x0A, 0x13, 0x09, + 0x14, 0x08, 0x17, 0x12, 0x08, 0x13, 0x0C, 0x0A, 0x13, 0x09, 0x15, 0x08, 0x16, 0x14, 0x06, 0x0E, + 0x12, 0x09, 0x13, 0x09, 0x15, 0x13, 0x0B, 0x13, 0x08, 0x0D, 0x13, 0x08, 0x16, 0x07, 0x16, 0x08, + 0x16, 0x12, 0x08, 0x15, 0x0B, 0x09, 0x15, 0x08, 0x15, 0x08, 0x06, 0x03, 0x0C, 0x13, 0x09, 0x15, + 0x0C, 0x08, 0x03, 0x06, 0x0A, 0x09, 0x05, 0x04, 0x0A, 0x13, 0x15, 0x09, 0x0B, 0x14, 0x17, 0x09, + 0x18, 0x04, 0x0C, 0x03, 0x06, 0x06, 0x15, 0x08, 0x10, 0x12, 0x13, 0x0E, 0x17, 0x05, 0x12, 0x08, + 0x14, 0x08, 0x04, 0x04, 0x09, 0x12, 0x0F, 0x12, 0x0F, 0x0D, 0x13, 0x07, 0x14, 0x11, 0x0A, 0x13, + 0x10, 0x10, 0x15, 0x09, 0x2F, 0x0D, 0x10, 0x13, 0x0F, 0x10, 0x16, 0x09, 0x2D, 0x0F, 0x10, 0x14, + 0x0F, 0x0F, 0x16, 0x08, 0x17, 0x04, 0x14, 0x0D, 0x11, 0x15, 0x11, 0x0E, 0x12, 0x0C, 0x26, 0x14, + 0x0F, 0x1E, 0x19, 0x06, 0x18, 0x06, 0x07, 0x02, 0x18, 0x06, 0x08, 0x05, 0x09, 0x13, 0x04, 0x08, + 0x03, 0x06, 0x17, 0x08, 0x05, 0x03, 0x0F, 0x03, 0x31, 0x05, 0x07, 0x14, 0x0B, 0x13, 0x16, 0x11, + 0x14, 0x0B, 0x2E, 0x08, 0x15, 0x12, 0x02, 0x05, 0x17, 0x0A, 0x05, 0x01, 0x19, 0x07, 0x3C, 0x13, + 0x04, 0x16, 0x16, 0x08, 0x1B, 0x12, 0x21, 0x11, 0x14, 0x16, 0x02, 0x0E, 0x2F, 0x04, 0x06, 0x0A, + 0x10, 0x14, 0x18, 0x07, 0x09, 0x14, 0x0B, 0x05, 0x02, 0x06, 0x26, 0x02, 0x2E, 0x12, 0x2B, 0x11, + 0x14, 0x07, 0x03, 0x05, 0x30, 0x02, 0x06, 0x04, 0x18, 0x1F, 0x58, 0x06, 0x02, 0x05, 0x2F, 0x04, + 0x04, 0x06, 0x09, 0x02, 0x04, 0x31, 0x22, 0x3A, 0xC5, 0x16, 0x3F, 0x03, 0x0A, 0x02, 0x51, 0x12, + 0x12, 0x10, 0xFE, 0xD0, 0x35, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, + 0xFE, 0xFE, 0x63, 0x02, 0x04, 0x02, 0x03, 0x03, 0x81, 0x02, 0x03, 0x02, 0x47, 0x09, 0x0A, 0x02, + 0x02, 0x03, 0x03, 0x02, 0x2B, 0x01, 0x04, 0x02, 0x04, 0x02, 0x04, 0x01, 0x38, 0x02, 0x80, 0x03, + 0x03, 0x02, 0x63, 0x01, 0x03, 0x02, 0x03, 0x02, 0x56, 0x02, 0x03, 0x02, 0x03, 0x03, 0x19, 0x02, + 0x02, 0x03, 0x02, 0x03, 0x02, 0x03, 0x02, 0x03, 0x03, 0x01, 0x60, 0x2C, 0x01, 0x03, 0x3E, 0x01, + 0x04, 0x01, 0x32, 0x02, 0x02, 0x03, 0x02, 0x17, 0x01, 0x03, 0x3D, 0x02, 0x03, 0x03, 0x03, 0x02, + 0x0A, 0x02, 0x5B, 0x02, 0x1F, 0x03, 0x02, 0x03, 0x03, 0x02, 0x03, 0x02, 0x03, 0x02, 0x04, 0x01, + 0x09, 0x02, 0x03, 0x02, 0x02, 0x03, 0x02, 0x02, 0x04, 0x01, 0x03, 0x02, 0x03, 0x02, 0x04, 0x01, + 0x03, 0x02, 0x03, 0x02, 0x03, 0x02, 0x03, 0x02, 0x2C, 0x02, 0x02, 0x03, 0x03, 0x02, 0x03, 0x03, + 0x04, 0x01, 0x23, 0x02, 0x03, 0x02, 0x25, 0x03, 0x02, 0x02, 0x03, 0x02, 0x1B, 0x02, 0x03, 0x02, + 0x04, 0x01, 0x37, 0x02, 0x04, 0x02, 0x12, 0x02, 0x02, 0x03, 0x03, 0x02, 0x03, 0x02, 0x03, 0x02, + 0x10, 0x02, 0x04, 0x01, 0x0D, 0x01, 0x04, 0x01, 0x04, 0x01, 0x25, 0x02, 0x03, 0x02, 0x03, 0x02, + 0x04, 0x02, 0x0A, 0x02, 0x0B, 0x02, 0x04, 0x02, 0x04, 0x03, 0x02, 0x03, 0x03, 0x0E, 0x3E, 0x02, + 0x04, 0x18, 0x01, 0x03, 0x03, 0x02, 0x04, 0x0A, 0x87, 0x11, 0x03, 0x02, 0x03, 0x02, 0x52, 0x02, + 0x04, 0x02, 0x12, 0x02, 0x30, 0x02, 0x03, 0x02, 0x04, 0x02, 0x04, 0x01, 0x04, 0x02, 0x02, 0x03, + 0x02, 0x02, 0x02, 0x03, 0x03, 0x01, 0x27, 0x02, 0xA4, 0x02, 0x03, 0x02, 0x18, 0x02, 0x03, 0x02, + 0x24, 0x16, 0x01, 0x1B, 0xFF, 0x01, 0x0F, 0x1A, 0x0D, 0x0B, 0x20, 0x0E, 0x07, 0x0C, 0x0A, 0x0B, + 0x22, 0x0A, 0x0D, 0x09, 0x0A, 0x0B, 0x1E, 0x10, 0x04, 0x0D, 0x10, 0x0C, 0x1E, 0x0A, 0x0B, 0x08, + 0x0C, 0x0A, 0x10, 0x07, 0x09, 0x0C, 0x04, 0x0E, 0x0C, 0x0C, 0x1F, 0x0E, 0x1D, 0x08, 0x11, 0x1A, + 0x03, 0x0D, 0x0C, 0x0B, 0x21, 0x0C, 0x10, 0x13, 0x14, 0x12, 0x0A, 0x0D, 0x0B, 0x0B, 0x0E, 0x03, + 0x10, 0x0C, 0x1E, 0x09, 0x11, 0x13, 0x08, 0x0B, 0x0C, 0x0C, 0x0C, 0x04, 0x10, 0x0E, 0x3E, 0x1F, + 0x0A, 0x0D, 0x0F, 0x02, 0x0B, 0x0F, 0x0E, 0x0C, 0x20, 0x04, 0x03, 0x1E, 0x0B, 0x0D, 0x1C, 0x0E, + 0x0E, 0x0C, 0x1E, 0x17, 0x02, 0x0C, 0x0B, 0x0D, 0x1B, 0x11, 0x1D, 0x06, 0x20, 0x0F, 0x02, 0x0B, + 0x0B, 0x0E, 0x0C, 0x04, 0x0F, 0x0B, 0x10, 0x12, 0x1E, 0x20, 0x0B, 0x0B, 0x0E, 0x04, 0x09, 0x03, + 0x02, 0x0A, 0x0B, 0x0D, 0x04, 0x08, 0x1D, 0x10, 0x05, 0x0D, 0x0B, 0x0C, 0x0D, 0x04, 0x0F, 0x0B, + 0x0D, 0x0A, 0x0B, 0x08, 0x0D, 0x0E, 0x11, 0x12, 0x0B, 0x0B, 0x0E, 0x03, 0x0A, 0x0E, 0x0C, 0x0C, + 0x0B, 0x0A, 0x09, 0x0B, 0x0D, 0x1D, 0x0B, 0x0C, 0x0E, 0x04, 0x0A, 0x0D, 0x0C, 0x0D, 0x0B, 0x09, + 0x0B, 0x07, 0x0E, 0x0E, 0x03, 0x0B, 0x03, 0x02, 0x06, 0x01, 0x02, 0x0B, 0x0F, 0x03, 0x0B, 0x0B, + 0x0D, 0x0E, 0x0C, 0x07, 0x09, 0x0A, 0x0C, 0x0A, 0x0B, 0x0C, 0x04, 0x01, 0x05, 0x10, 0x10, 0x02, + 0x0B, 0x03, 0x02, 0x04, 0x0F, 0x09, 0x11, 0x09, 0x03, 0x0D, 0x0B, 0x0C, 0x0A, 0x12, 0x01, 0x03, + 0x0B, 0x0B, 0x1D, 0x02, 0x02, 0x09, 0x0F, 0x0C, 0x13, 0x12, 0x0F, 0x0F, 0x0E, 0x0D, 0x0B, 0x03, + 0x02, 0x05, 0x22, 0x09, 0x10, 0x0A, 0x02, 0x04, 0x10, 0x0E, 0x0D, 0x1B, 0x0A, 0x0C, 0x0C, 0x0B, + 0x0E, 0x03, 0x0F, 0x0C, 0x0D, 0x0B, 0x05, 0x0B, 0x0A, 0x0C, 0x0D, 0x0F, 0x09, 0x03, 0x0B, 0x0C, + 0x0C, 0x0C, 0x0C, 0x04, 0x0F, 0x0C, 0x0C, 0x0D, 0x06, 0x0C, 0x0D, 0x0A, 0x0B, 0x08, 0x0E, 0x09, + 0x0D, 0x0D, 0x0B, 0x0E, 0x1E, 0x0C, 0x0E, 0x1B, 0x0D, 0x0C, 0x0C, 0x0E, 0x1D, 0x09, 0x0B, 0x0A, + 0x0C, 0x0C, 0x0A, 0x0A, 0x0E, 0x0B, 0x0B, 0x09, 0x0C, 0x0A, 0x0E, 0x0F, 0x07, 0x0C, 0x2F, 0x0A, + 0x10, 0x09, 0x15, 0x0B, 0x19, 0x0E, 0x13, 0x10, 0x11, 0x11, 0x11, 0x14, 0x31, 0x1F, 0x54, 0x11, + 0x5B, 0x0F, 0x2F, 0x0A, 0x12, 0x34, 0x9E, 0x0C, 0x12, 0x18, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, + 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, + 0xFE, 0xFE, 0xFE, 0xFE, 0xFF, +}; diff --git a/examples/spacetaxi/stSpeechData.h b/examples/spacetaxi/stSpeechData.h new file mode 100644 index 0000000..b34e046 --- /dev/null +++ b/examples/spacetaxi/stSpeechData.h @@ -0,0 +1,26 @@ +// Generated by assets/extractSpeech.py. Do not hand-edit. +// +// Space Taxi's digitised speech as the C64 stores it: 1-bit +// run-length streams. Each byte is one half-cycle length; the +// player toggles its output after each. $FE is a raster sync +// and $FF ends an utterance. stAudio.c expands these to PCM +// through the jlAudioPlaySfxStream fill callback. + +#ifndef ST_SPEECH_DATA_H +#define ST_SPEECH_DATA_H + +#include + +typedef struct { + uint8_t code; // PETSCII: 'H' 'T' '!' 'P' '1'..'9' '?' 'U' + uint16_t offset; // into kStSpeechRle + uint16_t length; +} StSpeechEntryT; + +#define ST_SPEECH_COUNT 15u +#define ST_SPEECH_RLE_BYTES 7445u + +extern const StSpeechEntryT kStSpeechIndex[ST_SPEECH_COUNT]; +extern const uint8_t kStSpeechRle[ST_SPEECH_RLE_BYTES]; + +#endif diff --git a/examples/spacetaxi/stTitle.c b/examples/spacetaxi/stTitle.c index 8536963..ecc37d3 100644 --- a/examples/spacetaxi/stTitle.c +++ b/examples/spacetaxi/stTitle.c @@ -11,6 +11,11 @@ // Level intro ($4523..$4727): a black screen with the level name, the // cab rising from the bottom and seven star sprites streaming out of // the centre, until the cab reaches mid-screen. +// +// The seven star sprites are replaced by a static starfield whose +// colours are cycled -- see ST_STAR_RING_FIRST in stSim.h for why and +// how. The cab still flies, and it is still what ends the intro, so the +// sequence keeps its length and its cue; only the warp changes. #include @@ -28,24 +33,49 @@ static const uint8_t kLogoColor[8] = { 0x02, 0x08, 0x07, 0x05, 0x06, 0x0E, 0x03 // 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 }; +static const uint8_t kTextDemoExit[] = "DEMO, USE JOYSTICK TO EXIT"; +static const uint8_t kTextScreens[] = "THIS IS 1 OF 25 DIFFERENT SCREENS!"; #define ST_TITLE_CAB_LIFT_ROW 0x14u #define ST_TITLE_SPARKLE_TICKS 0x5Au #define ST_TITLE_HOVER_COL 0x28u #define ST_INTRO_STAR_PTR 0xDAu -#define ST_INTRO_STAR_RELOAD 0x20u #define ST_INTRO_CAB_END_ROW 0x94u #define ST_INTRO_CAB_DX_RELOAD 0x28u +// The palette-cycled starfield. Seven stars per ring so the bright band +// stays the same weight all the way out, placed at fixed pseudo-random +// angles and clear of the three lines the intro screen writes (the level +// name on row 12, and the demo hints on rows 3 and 24). +typedef struct { + uint8_t col; + uint8_t row; + uint8_t ring; +} StStarT; + +static const StStarT kIntroStar[] = { + { 18u, 10u, 0u }, { 19u, 10u, 0u }, { 17u, 11u, 0u }, { 18u, 13u, 0u }, + { 21u, 13u, 0u }, { 22u, 14u, 0u }, { 18u, 15u, 0u }, { 18u, 8u, 1u }, + { 19u, 8u, 1u }, { 14u, 10u, 1u }, { 25u, 14u, 1u }, { 21u, 15u, 1u }, + { 25u, 15u, 1u }, { 20u, 17u, 1u }, { 26u, 7u, 2u }, { 12u, 10u, 2u }, + { 29u, 11u, 2u }, { 11u, 13u, 2u }, { 29u, 14u, 2u }, { 21u, 17u, 2u }, + { 23u, 18u, 2u }, { 21u, 4u, 3u }, { 24u, 4u, 3u }, { 11u, 8u, 3u }, + { 12u, 8u, 3u }, { 10u, 10u, 3u }, { 30u, 16u, 3u }, { 14u, 17u, 3u }, + { 15u, 2u, 4u }, { 8u, 8u, 4u }, { 32u, 9u, 4u }, { 9u, 17u, 4u }, + { 32u, 17u, 4u }, { 32u, 18u, 4u }, { 24u, 22u, 4u }, { 16u, 1u, 5u }, + { 28u, 1u, 5u }, { 31u, 2u, 5u }, { 36u, 13u, 5u }, { 6u, 15u, 5u }, + { 4u, 16u, 5u }, { 5u, 19u, 5u }, { 8u, 0u, 6u }, { 30u, 0u, 6u }, + { 1u, 8u, 6u }, { 2u, 12u, 6u }, { 37u, 16u, 6u }, { 7u, 21u, 6u }, + { 32u, 22u, 6u }, { 0u, 4u, 7u }, { 39u, 7u, 7u }, { 0u, 10u, 7u }, + { 38u, 20u, 7u }, { 38u, 21u, 7u }, { 4u, 22u, 7u }, { 35u, 22u, 7u }, +}; +#define ST_INTRO_STAR_COUNT (sizeof(kIntroStar) / sizeof(kIntroStar[0])) + static void addToRow(StSimT *sim, uint8_t idx, uint8_t delta); -static void 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); +static void paintStarfield(StSimT *sim); // $4113 @@ -54,16 +84,6 @@ static void addToRow(StSimT *sim, uint8_t idx, uint8_t delta) { } -// $411B -static void addToSpriteX(StSimT *sim, uint8_t idx, uint8_t delta) { - uint16_t x = (uint16_t)(((uint16_t)sim->spr[idx].msb << 8) | sim->spr[idx].col); - - x = (uint16_t)(x + (uint16_t)(int16_t)(int8_t)delta); - sim->spr[idx].col = (uint8_t)x; - sim->spr[idx].msb = (uint8_t)(x >> 8); -} - - // $4861 -- the next logo colour, onto sprites 3..7 and onto the logo // itself. The C64 writes the colour into all 407 cells of rows 1..11, // columns 1..37; only the cells showing the logo character take any @@ -90,7 +110,8 @@ static void logoFlip(StSimT *sim) { if (sim->logoDiv != 0u) { return; } - memcpy(sim->charset[ST_LOGO_CHAR], sim->charset[ST_LOGO_CHAR + kLogoFlip[sim->logoCycleAux]], 8u); + memcpy(stSimGlyphMut(sim, ST_LOGO_CHAR), + stSimGlyph(sim, (uint8_t)(ST_LOGO_CHAR + kLogoFlip[sim->logoCycleAux])), 8u); stSimMarkChar(sim, ST_LOGO_CHAR); sim->logoCycleAux++; if (sim->logoCycleAux == 6u) { @@ -103,32 +124,21 @@ static void logoFlip(StSimT *sim) { } -// $4253 + $4293 as the title and intro loops call them. -static void marshalFrame(StSimT *sim) { - uint8_t i; - uint8_t enable = 0u; +// Paint the level-intro starfield into screen RAM, once. A star's cell +// is coloured ST_STAR_RING_FIRST + its ring and never touched again -- +// the renderer animates the warp by cycling those colours, which costs +// no dirty rows at all. See ST_STAR_RING_FIRST in stSim.h. +static void paintStarfield(StSimT *sim) { + const StStarT *star; + uint8_t one[2]; + uint8_t k; - for (i = 0u; i < ST_HW_SPRITES; i++) { - 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); - } + one[1] = 0u; // stSimDrawText stops below screen code 6 + for (k = 0u; k < (uint8_t)ST_INTRO_STAR_COUNT; k++) { + star = &kIntroStar[k]; + one[0] = (star->ring < ST_STAR_FAR_RINGS) ? ST_STAR_GLYPH_FAR : ST_STAR_GLYPH_NEAR; + stSimDrawText(sim, star->col, star->row, one, (uint8_t)(ST_STAR_RING_FIRST + star->ring)); } - sim->frame.enableMask = enable; - sim->frame.multiMask = sim->multiColorMask; -} - - -// $4675 -- a star reappears near the centre, seeded by star 0's row. -static void starRespawn(StSimT *sim, uint8_t idx) { - sim->spr[idx].enable = 1u; - sim->spr[idx].msb = 0u; - sim->spr[idx].col = (uint8_t)((sim->spr[0].row & 7u) + 0xA6u); - sim->spr[idx].row = (uint8_t)(((sim->spr[0].row & 0x30u) >> 4) + 0x89u); - sim->starReload[idx] = ST_INTRO_STAR_RELOAD; } @@ -159,21 +169,26 @@ void stIntroEnter(StSimT *sim, const StLevelT *level) { sim->spr[idx].color = 0x07u; sim->spr[idx].ptr = ST_INTRO_STAR_PTR; sim->spr[idx].enable = 0u; - sim->starTimer[idx] = stSimRng(sim, 0x20u); - sim->starReload[idx] = 0u; + // $44F8/$4500 held each star's spawn timer and reload. Nothing + // reads them now that the warp is painted rather than flown, but + // the seven RNG draws MUST stay: they advance the shared RNG and + // the level that follows draws from it, so dropping them would + // change the game that comes after the intro. + (void)stSimRng(sim, 0x20u); } sim->borderColor = 0u; sim->bgColor = 0u; sim->multiColorMask = 0x80u; - marshalFrame(sim); + stSimMarshal(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)); + stSimGlyphReset(sim); sim->charDirtyAll = true; sim->charDirtyCount = 0u; stSimDrawText(sim, 10u, 12u, level->name, 3u); + paintStarfield(sim); if (sim->demoMode != 0u && sim->postMortem == 0u) { stSimDrawText(sim, 7u, 3u, kTextDemoExit, 5u); stSimDrawText(sim, 3u, 24u, kTextScreens, 4u); @@ -187,27 +202,11 @@ void stIntroEnter(StSimT *sim, const StLevelT *level) { 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); + stSimMarshal(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); + stSimSpriteAddX(sim, 7u, sim->introCabDx); sim->introCabDxTimer--; if (sim->introCabDxTimer == 0u) { sim->introCabDxTimer = ST_INTRO_CAB_DX_RELOAD; @@ -222,7 +221,7 @@ bool stIntroStep(StSimT *sim) { for (k = 0u; k < ST_HW_SPRITES; k++) { sim->spr[k].enable = 0u; } - marshalFrame(sim); + stSimMarshal(sim); sim->rngT1 = 0x06u; sim->rngT2 = 0x17u; return true; @@ -237,7 +236,7 @@ void stTitleEnter(StSimT *sim, const StLevelT *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)); + stSimGlyphReset(sim); sim->charDirtyAll = true; sim->charDirtyCount = 0u; logoColorCycle(sim); @@ -260,7 +259,7 @@ void stTitleEnter(StSimT *sim, const StLevelT *title) { sim->multiColorMask = 0x07u; sim->spriteMc1 = 0x07u; sim->spriteMc0 = 0x02u; - marshalFrame(sim); + stSimMarshal(sim); sim->stage = ST_STAGE_WAIT; sim->introFrameCount = ST_TITLE_SPARKLE_TICKS; sim->hoverXFrac = 0u; @@ -313,7 +312,7 @@ bool stTitleTick(StSimT *sim) { default: break; } - marshalFrame(sim); + stSimMarshal(sim); logoFlip(sim); return sim->stage == ST_STAGE_DROP_IN; } diff --git a/examples/uber/uber.c b/examples/uber/uber.c index cef65fd..7d46138 100644 --- a/examples/uber/uber.c +++ b/examples/uber/uber.c @@ -1062,11 +1062,13 @@ static void PROBE_ATTR op_probeCleanErases(void) { // Wrap a probe timeOp with counter-delta logging. The IIgs skips the // wrapper entirely: its present is asm and never touches the -// counters (always zero there), and the IIgs -DUBER_PROBE build dies -// before main when this block is compiled in (bisected 2026-07-07: -// trivial-op probe launches, counter/fn-ptr wrapper variants do not, -// optnone does not help -- llvm-mos issue, flagged to Scott). Direct -// timeOp calls carry the IIgs probe rows instead. +// counters (always zero there). The IIgs -DUBER_PROBE build also used +// to die mid-run (bisected 2026-07-07, blamed on this wrapper; the real +// cause, found 2026-09-22, was hal.c asking MMStartUp for a NEW Memory +// Manager ID on every allocation until the ID pool ran dry and the +// gameFrameClean row's jlSurfaceCreate BRK'd -- fixed in hal.c). The +// direct-timeOp IIgs arm below is a near duplicate of this path that +// could now be folded back in. #ifndef JOEYLIB_PLATFORM_IIGS static void PROBE_ATTR probeOp(const char *name, void (*fn)(void)) { uint32_t bytes0; @@ -1791,10 +1793,9 @@ static void __attribute__((noinline)) runAllTests(void) { // for mark-path changes. IIgs: no counter wrapper (see probeOp), // no P5 (mark inertness there is proven by object-code compare). #ifdef JOEYLIB_PLATFORM_IIGS - // Direct delta logging: the probeOp fn-pointer wrapper dies before - // main on llvm-mos (W0 bisect); direct extern reads are the - // bisected-safe shape. Same rows + steady-delta protocol as the - // probeOp arm below. + // Direct delta logging (kept from the days the probeOp wrapper was + // blamed for the IIgs probe death; see probeOp). Same rows + + // steady-delta protocol as the probeOp arm below. { uint32_t probeBytes0; uint32_t probeRows0; @@ -1821,8 +1822,8 @@ static void __attribute__((noinline)) runAllTests(void) { // SaveAndDraw rows, so each diff vs its golden row = the // validation the trusted entry skips. Placed BEFORE the present // pair below so the stage writes these do are re-synced to SHR - // before op_probeShrParity. timeOp (not probeOp) because the - // probeOp fn-pointer wrapper dies pre-main on llvm-mos. + // before op_probeShrParity. timeOp (not probeOp) to match the + // rest of this arm (see probeOp). gProbeRestoreBackup.sprite = gSprite; gProbeRestoreBackup.bytes = gProbeRestoreBackupBytes; jlSpriteSaveUnder(gStage, gSprite, 40, 30, &gProbeRestoreBackup); @@ -1844,8 +1845,8 @@ static void __attribute__((noinline)) runAllTests(void) { // 16-21 ms unattributed residual). Same recipe as the golden // row (gCleanSurface was destroyed after it): re-create with // clean == stage everywhere, run the two split rows via direct - // timeOp (the probeOp wrapper dies pre-main on llvm-mos), - // destroy again. + // timeOp (this arm does not use probeOp; see probeOp), destroy + // again. gCleanSurface = jlSurfaceCreate(); if (gCleanSurface != NULL) { paintFrameStatic(); diff --git a/include/joey/audio.h b/include/joey/audio.h index 33dac1e..f51033a 100644 --- a/include/joey/audio.h +++ b/include/joey/audio.h @@ -68,11 +68,17 @@ bool jlAudioIsPlayingMod(void); // the sample was recorded at; the engine pitches as needed for its // own output rate. If the slot is currently playing, the new sample // replaces it. +// +// The full -128..127 range is accepted everywhere, but the IIgs plays +// -128 as -127: its DOC reads 0x00 (which is what -128 becomes in the +// chip's unsigned format) as a wave-end marker and would halt the voice +// on that sample. One level out of 256, inaudible in practice. void jlAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz); // Stream-fill callback signature. Called by the mixer to refill a // streaming slot's prefetch buffer; should write up to `count` signed -// 8-bit samples to `dst` and return the number written. Returning 0 +// 8-bit samples to `dst` and return the number written (see +// jlAudioPlaySfx for the one clamp the IIgs applies). Returning 0 // signals end-of-stream and the slot auto-deactivates. Called from // the same context as the audio engine's refill (main thread on // DOS/ST, audio ISR on Amiga/IIgs); must not block or allocate. @@ -84,10 +90,55 @@ typedef uint32_t (*jlAudioStreamFillT)(void *ctx, int8_t *dst, uint32_t count); // allocation. `rateHz` is the sample rate `fill` produces; the // engine resamples (with linear interpolation) to its own output // rate. If the slot is currently playing, the new stream replaces -// it. No-op on ports whose audio engine doesn't expose the shared -// SFX overlay mixer. +// it. Implemented on every port: DOS/ST/X68000 pull through the shared +// SFX overlay mixer, while the IIgs (NTPstreamsound) and Amiga +// (PTPlayer mt_playfx) play one chunk at a time and fire the next from +// jlAudioFrameTick as the current one runs down -- so on those two the +// host must be calling jlAudioFrameTick for a stream to continue past +// its first chunk, and a seam can carry up to one frame of jitter. void jlAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz); +// Raw-format streaming: jlAudioPlaySfxStream, except `fill` delivers +// bytes already in the port's raw stream format (jlAudioSfxRawFormat), +// so the port skips its per-byte conversion of every chunk. On the IIgs +// that conversion is a 65816 byte loop over each 3.5 KB chunk -- it +// stalled Space Taxi's ride for a third of a second per chunk of speech. +// Every other port streams signed PCM natively, so there Raw is the +// plain call. A producer that cannot make the port's format uses +// jlAudioPlaySfxStream. +#define JL_SFX_RAW_SIGNED8 0u // int8_t PCM, the jlAudioPlaySfxStream contract +#define JL_SFX_RAW_UNSIGNED8_NOZERO 1u // uint8_t PCM biased 0x80; 0x00 is forbidden (DOC stop byte) +uint8_t jlAudioSfxRawFormat(void); +void jlAudioPlaySfxStreamRaw(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz); + +// Stereo placement for a SFX slot. +typedef enum { + AUDIO_PAN_LEFT = 0, + AUDIO_PAN_CENTER, + AUDIO_PAN_RIGHT, + + AUDIO_PAN_COUNT +} jlAudioPanE; + +// Place a SFX slot in the stereo field. Slots start at AUDIO_PAN_CENTER, and +// the setting sticks to the SLOT, so it survives retriggers -- set it once +// when a sound is meant to come from one side. +// +// What each port can actually do, because the hardware differs: +// IIgs all three. The DOC pans per OSCILLATOR (control bits 7:4 are the +// output channel, and per Apple TN #19 odd = left, even = right), +// so a centred sound is played on two oscillators, one per side. +// A stock IIgs sums to mono anyway; this only shows up on a stereo +// card or an emulator. +// Amiga left and right, by picking the Paula channel side (0 and 3 are +// left, 1 and 2 right). Paula cannot place one voice in both ears, +// so AUDIO_PAN_CENTER keeps PTPlayer's own channel choice. +// X68000 ignored. Everything is mixed into ONE MSM6258 ADPCM stream, so +// its pan bits would move all audio at once, not one slot. +// DOS, ST ignored -- their output is mono. +// Passing an out-of-range pan is ignored. +void jlAudioSetPan(uint8_t slot, jlAudioPanE pan); + // Stop a SFX slot early. No-op if the slot is already idle. void jlAudioStopSfx(uint8_t slot); diff --git a/include/joey/core.h b/include/joey/core.h index c1d33d9..bc6b431 100644 --- a/include/joey/core.h +++ b/include/joey/core.h @@ -47,6 +47,11 @@ void *jlAlloc(uint32_t bytes); // Release a buffer returned by jlAlloc. NULL is accepted and ignored. void jlFree(void *p); +// memcpy for big buffers. Same contract as memcpy (no overlap), but the +// IIgs runs it as MVN block moves (7 cycles a byte) instead of the libc +// byte loop, split at bank boundaries by the port. +void jlMemCopy(void *dst, const void *src, uint32_t bytes); + // Block the calling thread until the next display vertical blank. // Used to pace game loops to the display's native refresh rate // (~70 Hz on VGA mode 13h, ~50 or ~60 Hz on Amiga/ST PAL/NTSC, ~60 Hz diff --git a/include/joey/platform.h b/include/joey/platform.h index f4b6de5..adb30c5 100644 --- a/include/joey/platform.h +++ b/include/joey/platform.h @@ -134,7 +134,7 @@ #define JL_HAS_STAGE_FREE_PIXELS // no-op (pinned), function #define JL_HAS_FRAME_HZ // runtime 50/60 from battery RAM, function // (frameCount: port.h #defines jlpFrameCount straight to iigsGetTickWord, - // the GetTick asm wrapper -- no function override to register here.) + // the VBL heartbeat tick read -- no function override to register here.) #define JL_HAS_WAIT_VBL // $C019 VBL poll, function // audio: every real port implements the full engine #define JL_HAS_AUDIO_INIT @@ -144,6 +144,8 @@ #define JL_HAS_AUDIO_IS_PLAYING_MOD #define JL_HAS_AUDIO_PLAY_SFX #define JL_HAS_AUDIO_PLAY_SFX_STREAM + #define JL_HAS_AUDIO_PLAY_SFX_STREAM_RAW + #define JL_HAS_AUDIO_SET_PAN #define JL_HAS_AUDIO_STOP_SFX #define JL_HAS_AUDIO_TONE #define JL_HAS_AUDIO_VOICE @@ -216,6 +218,7 @@ #define JL_HAS_AUDIO_IS_PLAYING_MOD #define JL_HAS_AUDIO_PLAY_SFX #define JL_HAS_AUDIO_PLAY_SFX_STREAM + #define JL_HAS_AUDIO_SET_PAN #define JL_HAS_AUDIO_STOP_SFX #define JL_HAS_AUDIO_TONE #define JL_HAS_AUDIO_VOICE diff --git a/make/amiga.mk b/make/amiga.mk index 1406f54..aa8f4ae 100644 --- a/make/amiga.mk +++ b/make/amiga.mk @@ -88,7 +88,7 @@ ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c ADV2_BIN := $(BINDIR)/Adv2 AGI_SRCS := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c AGI_BIN := $(BINDIR)/Agi -STAXI_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stLevelFile.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stSim.c $(EXAMPLES)/spacetaxi/stFare.c $(EXAMPLES)/spacetaxi/stHooks.c $(EXAMPLES)/spacetaxi/stTitle.c $(EXAMPLES)/spacetaxi/stC64Data.c $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stAudio.c +STAXI_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stLevelFile.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stSim.c $(EXAMPLES)/spacetaxi/stFare.c $(EXAMPLES)/spacetaxi/stHooks.c $(EXAMPLES)/spacetaxi/stTitle.c $(EXAMPLES)/spacetaxi/stC64Data.c $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stAudio.c $(EXAMPLES)/spacetaxi/stLevelCels.c STAXI_INSTALL_DIR := $(BINDIR)/Taxi STAXI_BIN := $(STAXI_INSTALL_DIR)/Taxi @@ -130,6 +130,24 @@ SPRITEBAKE_BIN := $(REPO_DIR)/build/tools/spritebake-amiga # stCels.c + C64 bitmaps) and compiled to a .spc by the host spritebake, so # STAXI loads its cels already compiled instead of JIT-compiling them at boot. STAXIBAKE_BIN := $(REPO_DIR)/build/tools/staxibake + +# Per-level hook-sprite banks: the cels stLevelCelList names for each +# level that has hook sprites, baked from its level file and pre-compiled +# for this target, so no cel is JIT-compiled mid-ride. Levels without +# hook sprites have no bank; the game JIT-compiles at level entry when a +# bank is missing. +STAXI_CEL_LEVELS := 07 08 09 10 12 16 17 21 +STAXILEVEL_SPR_GEN := $(foreach n,$(STAXI_CEL_LEVELS),$(STAXI_GEN_DIR)/sprites/level$(n).spr) +STAXILEVEL_SPC_GEN := $(STAXILEVEL_SPR_GEN:.spr=.spc) +STAXILEVEL_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXILEVEL_SPC_GEN)) +STAXI_ASSET_DSTS += $(STAXILEVEL_SPC_RUN) + +# Speech baked to sample bytes per passenger pitch (assets/genSpeechPcm.py): +# expanding bits at play time is what keeps "HEY TAXI" from stalling the +# ride; the C64 run-length source stays in stSpeechData.c as the input. +STAXI_SPEECH_GEN := $(STAXI_GEN_DIR)/speech.bin +STAXI_SPEECH_RUN := $(STAXI_RUN_DIR)/speech.bin +STAXI_ASSET_DSTS += $(STAXI_SPEECH_RUN) STAXICELS_SPR_GEN := $(STAXI_GEN_DIR)/sprites/staxicels.spr STAXICELS_SPC_GEN := $(STAXICELS_SPR_GEN:.spr=.spc) STAXICELS_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXICELS_SPC_GEN)) @@ -251,7 +269,7 @@ $(STAXI_BIN): $(STAXI_SRCS) $(LIB) # Space Taxi asset pipeline. .SECONDARY keeps the generated/ artifacts # from being auto-deleted after staging. -.SECONDARY: $(STAXI_LEVEL_GEN) $(STAXI_TBK_GEN) $(STAXI_SPR_GEN) $(STAXI_SPC_GEN) $(STAXICELS_SPC_GEN) +.SECONDARY: $(STAXI_LEVEL_GEN) $(STAXI_TBK_GEN) $(STAXI_SPR_GEN) $(STAXI_SPC_GEN) $(STAXICELS_SPC_GEN) $(STAXILEVEL_SPR_GEN) $(STAXILEVEL_SPC_GEN) $(STAXI_ROM_LEVEL_GEN) &: $(STAXI_RAW_DUMP) $(ROMTOLEVEL_BIN) @mkdir -p $(STAXI_GEN_DIR)/levels @@ -318,3 +336,16 @@ clean: clean-amiga # the .c files that include it, leaving a frankenstein binary where # different TUs see different struct layouts. -include $(LIB_OBJS:.o=.d) + +# Shorter stem than the %.spr-from-%.png pattern, so it wins for level banks. +$(STAXI_GEN_DIR)/sprites/level%.spr: $(STAXI_GEN_DIR)/levels/level%.dat $(STAXIBAKE_BIN) $(EXAMPLES)/spacetaxi/stLevelCels.c + @mkdir -p $(dir $@) + $(STAXIBAKE_BIN) --level $< $@ + +# Speech bake rules live down here so they never become the default goal. +$(STAXI_SPEECH_GEN): $(EXAMPLES)/spacetaxi/stSpeechData.c $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py + @mkdir -p $(dir $@) + python3 $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py $< $@ +$(STAXI_SPEECH_RUN): $(STAXI_SPEECH_GEN) + @mkdir -p $(dir $@) + cp $< $@ diff --git a/make/atarist.mk b/make/atarist.mk index 26a7a56..e9cb472 100644 --- a/make/atarist.mk +++ b/make/atarist.mk @@ -75,7 +75,7 @@ ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c ADV2_BIN := $(BINDIR)/ADV2.PRG AGI_SRCS := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c AGI_BIN := $(BINDIR)/AGI.PRG -STAXI_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stLevelFile.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stSim.c $(EXAMPLES)/spacetaxi/stFare.c $(EXAMPLES)/spacetaxi/stHooks.c $(EXAMPLES)/spacetaxi/stTitle.c $(EXAMPLES)/spacetaxi/stC64Data.c $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stAudio.c +STAXI_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stLevelFile.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stSim.c $(EXAMPLES)/spacetaxi/stFare.c $(EXAMPLES)/spacetaxi/stHooks.c $(EXAMPLES)/spacetaxi/stTitle.c $(EXAMPLES)/spacetaxi/stC64Data.c $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stAudio.c $(EXAMPLES)/spacetaxi/stLevelCels.c STAXI_INSTALL_DIR := $(BINDIR)/STAXI STAXI_BIN := $(STAXI_INSTALL_DIR)/STAXI.PRG @@ -110,6 +110,24 @@ SPRITEBAKE_BIN := $(REPO_DIR)/build/tools/spritebake-atarist # stCels.c + C64 bitmaps) and compiled to a .spc by the host spritebake, so # STAXI loads its cels already compiled instead of JIT-compiling them at boot. STAXIBAKE_BIN := $(REPO_DIR)/build/tools/staxibake + +# Per-level hook-sprite banks: the cels stLevelCelList names for each +# level that has hook sprites, baked from its level file and pre-compiled +# for this target, so no cel is JIT-compiled mid-ride. Levels without +# hook sprites have no bank; the game JIT-compiles at level entry when a +# bank is missing. +STAXI_CEL_LEVELS := 07 08 09 10 12 16 17 21 +STAXILEVEL_SPR_GEN := $(foreach n,$(STAXI_CEL_LEVELS),$(STAXI_GEN_DIR)/sprites/level$(n).spr) +STAXILEVEL_SPC_GEN := $(STAXILEVEL_SPR_GEN:.spr=.spc) +STAXILEVEL_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXILEVEL_SPC_GEN)) +STAXI_ASSET_DSTS += $(STAXILEVEL_SPC_RUN) + +# Speech baked to sample bytes per passenger pitch (assets/genSpeechPcm.py): +# expanding bits at play time is what keeps "HEY TAXI" from stalling the +# ride; the C64 run-length source stays in stSpeechData.c as the input. +STAXI_SPEECH_GEN := $(STAXI_GEN_DIR)/speech.bin +STAXI_SPEECH_RUN := $(STAXI_RUN_DIR)/speech.bin +STAXI_ASSET_DSTS += $(STAXI_SPEECH_RUN) STAXICELS_SPR_GEN := $(STAXI_GEN_DIR)/sprites/staxicels.spr STAXICELS_SPC_GEN := $(STAXICELS_SPR_GEN:.spr=.spc) STAXICELS_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXICELS_SPC_GEN)) @@ -237,7 +255,7 @@ $(STAXI_BIN): $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) $(ST_CC) $(CFLAGS) $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) # Space Taxi asset pipeline (mirrors dos.mk + amiga.mk). -.SECONDARY: $(STAXI_LEVEL_GEN) $(STAXI_TBK_GEN) $(STAXI_SPR_GEN) $(STAXI_SPC_GEN) $(STAXICELS_SPC_GEN) +.SECONDARY: $(STAXI_LEVEL_GEN) $(STAXI_TBK_GEN) $(STAXI_SPR_GEN) $(STAXI_SPC_GEN) $(STAXICELS_SPC_GEN) $(STAXILEVEL_SPR_GEN) $(STAXILEVEL_SPC_GEN) $(STAXI_ROM_LEVEL_GEN) &: $(STAXI_RAW_DUMP) $(ROMTOLEVEL_BIN) @mkdir -p $(STAXI_GEN_DIR)/levels @@ -304,3 +322,16 @@ clean: clean-atarist # the .c files that include it, leaving a frankenstein binary where # different TUs see different struct layouts. -include $(LIB_OBJS:.o=.d) + +# Shorter stem than the %.spr-from-%.png pattern, so it wins for level banks. +$(STAXI_GEN_DIR)/sprites/level%.spr: $(STAXI_GEN_DIR)/levels/level%.dat $(STAXIBAKE_BIN) $(EXAMPLES)/spacetaxi/stLevelCels.c + @mkdir -p $(dir $@) + $(STAXIBAKE_BIN) --level $< $@ + +# Speech bake rules live down here so they never become the default goal. +$(STAXI_SPEECH_GEN): $(EXAMPLES)/spacetaxi/stSpeechData.c $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py + @mkdir -p $(dir $@) + python3 $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py $< $@ +$(STAXI_SPEECH_RUN): $(STAXI_SPEECH_GEN) + @mkdir -p $(dir $@) + cp $< $@ diff --git a/make/dos.mk b/make/dos.mk index de03dbf..f65ac39 100644 --- a/make/dos.mk +++ b/make/dos.mk @@ -71,7 +71,7 @@ ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c ADV2_BIN := $(BINDIR)/ADV2.EXE AGI_SRCS := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c AGI_BIN := $(BINDIR)/AGI.EXE -STAXI_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stLevelFile.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stSim.c $(EXAMPLES)/spacetaxi/stFare.c $(EXAMPLES)/spacetaxi/stHooks.c $(EXAMPLES)/spacetaxi/stTitle.c $(EXAMPLES)/spacetaxi/stC64Data.c $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stAudio.c +STAXI_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stLevelFile.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stSim.c $(EXAMPLES)/spacetaxi/stFare.c $(EXAMPLES)/spacetaxi/stHooks.c $(EXAMPLES)/spacetaxi/stTitle.c $(EXAMPLES)/spacetaxi/stC64Data.c $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stAudio.c $(EXAMPLES)/spacetaxi/stLevelCels.c # JoeyLib install convention: each app lives in its own bin subdir # with a DATA/ folder alongside the binary for runtime assets. STAXI_INSTALL_DIR := $(BINDIR)/STAXI @@ -131,6 +131,24 @@ SPRITEBAKE_BIN := $(REPO_DIR)/build/tools/spritebake-dos # stCels.c + C64 bitmaps) and compiled to a .spc by the host spritebake, so # STAXI loads its cels already compiled instead of JIT-compiling them at boot. STAXIBAKE_BIN := $(REPO_DIR)/build/tools/staxibake + +# Per-level hook-sprite banks: the cels stLevelCelList names for each +# level that has hook sprites, baked from its level file and pre-compiled +# for this target, so no cel is JIT-compiled mid-ride. Levels without +# hook sprites have no bank; the game JIT-compiles at level entry when a +# bank is missing. +STAXI_CEL_LEVELS := 07 08 09 10 12 16 17 21 +STAXILEVEL_SPR_GEN := $(foreach n,$(STAXI_CEL_LEVELS),$(STAXI_GEN_DIR)/sprites/level$(n).spr) +STAXILEVEL_SPC_GEN := $(STAXILEVEL_SPR_GEN:.spr=.spc) +STAXILEVEL_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXILEVEL_SPC_GEN)) +STAXI_ASSET_DSTS += $(STAXILEVEL_SPC_RUN) + +# Speech baked to sample bytes per passenger pitch (assets/genSpeechPcm.py): +# expanding bits at play time is what keeps "HEY TAXI" from stalling the +# ride; the C64 run-length source stays in stSpeechData.c as the input. +STAXI_SPEECH_GEN := $(STAXI_GEN_DIR)/speech.bin +STAXI_SPEECH_RUN := $(STAXI_RUN_DIR)/speech.bin +STAXI_ASSET_DSTS += $(STAXI_SPEECH_RUN) STAXICELS_SPR_GEN := $(STAXI_GEN_DIR)/sprites/staxicels.spr STAXICELS_SPC_GEN := $(STAXICELS_SPR_GEN:.spr=.spc) STAXICELS_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXICELS_SPC_GEN)) @@ -253,7 +271,7 @@ $(STAXI_BIN): $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) # Mark these .SECONDARY so make doesn't auto-delete them after the # stage-B copy into bin/ -- they're the canonical distribution # artifacts. -.SECONDARY: $(STAXI_LEVEL_GEN) $(STAXI_TBK_GEN) $(STAXI_SPR_GEN) $(STAXI_SPC_GEN) $(STAXICELS_SPC_GEN) +.SECONDARY: $(STAXI_LEVEL_GEN) $(STAXI_TBK_GEN) $(STAXI_SPR_GEN) $(STAXI_SPC_GEN) $(STAXICELS_SPC_GEN) $(STAXILEVEL_SPR_GEN) $(STAXILEVEL_SPC_GEN) # Bulk-extract all 24 canonical levels plus the title from the C64 # ROM in one invocation. Grouped target (&:) means make treats the @@ -335,3 +353,16 @@ clean-spacetaxi: # the .c files that include it, leaving a frankenstein binary where # different TUs see different struct layouts. -include $(LIB_OBJS:.o=.d) + +# Shorter stem than the %.spr-from-%.png pattern, so it wins for level banks. +$(STAXI_GEN_DIR)/sprites/level%.spr: $(STAXI_GEN_DIR)/levels/level%.dat $(STAXIBAKE_BIN) $(EXAMPLES)/spacetaxi/stLevelCels.c + @mkdir -p $(dir $@) + $(STAXIBAKE_BIN) --level $< $@ + +# Speech bake rules live down here so they never become the default goal. +$(STAXI_SPEECH_GEN): $(EXAMPLES)/spacetaxi/stSpeechData.c $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py + @mkdir -p $(dir $@) + python3 $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py $< $@ +$(STAXI_SPEECH_RUN): $(STAXI_SPEECH_GEN) + @mkdir -p $(dir $@) + cp $< $@ diff --git a/make/iigs.mk b/make/iigs.mk index e77808e..c69864b 100644 --- a/make/iigs.mk +++ b/make/iigs.mk @@ -66,7 +66,7 @@ UBER_SRC := $(EXAMPLES)/uber/uber.c ADV_SRC := $(EXAMPLES)/adventure/adventure.c ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c AGI_SRCS := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c -STAXI_SRCS := $(EXAMPLES)/spacetaxi/stC64Data.c $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stSim.c $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stLevelFile.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stFare.c $(EXAMPLES)/spacetaxi/stHooks.c $(EXAMPLES)/spacetaxi/stTitle.c $(EXAMPLES)/spacetaxi/stAudio.c +STAXI_SRCS := $(EXAMPLES)/spacetaxi/stC64Data.c $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stSim.c $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stLevelFile.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stFare.c $(EXAMPLES)/spacetaxi/stHooks.c $(EXAMPLES)/spacetaxi/stTitle.c $(EXAMPLES)/spacetaxi/stAudio.c $(EXAMPLES)/spacetaxi/stLevelCels.c $(EXAMPLES)/spacetaxi/stMarshalIigs.s AUDIO_SRC := $(EXAMPLES)/audio/audio.c # NinjaTrackerPlus replayer: Merlin32-assemble ninjatrackerplus.s to a 34KB @@ -113,6 +113,17 @@ iigs-examples: $(BINDIR)/PATTERN $(BINDIR)/DRAW $(BINDIR)/KEYS $(BINDIR)/SERIAL # (jlSpriteBankLoadPrecompiled) instead of JIT-compiling every cel. STAXICELS_SPR := $(EXAMPLES)/spacetaxi/generated/iigs/sprites/staxicels.spr STAXI_IIGS_SPC := $(STAXICELS_SPR:.spr=.spc) +# Per-level hook-sprite banks (see make/dos.mk): baked from the level file +# by staxibake --level, pre-compiled for the 65816. make-iigs-disk.sh copies +# them onto the floppy last and only while there is room; a missing bank +# makes the game JIT-compile that level's cels at entry instead. +STAXI_CEL_LEVELS := 07 08 09 10 12 16 17 21 +STAXILEVEL_IIGS_SPC := $(foreach n,$(STAXI_CEL_LEVELS),$(EXAMPLES)/spacetaxi/generated/iigs/sprites/level$(n).spc) +# Speech baked to 1-bit PCM per passenger pitch (assets/genSpeechPcm.py); +# make-iigs-disk.sh picks it up from the generated tree like font.tbk. +STAXI_IIGS_SPEECH := $(EXAMPLES)/spacetaxi/generated/iigs/speech.bin +$(STAXI_IIGS_SPEECH): $(EXAMPLES)/spacetaxi/stSpeechData.c $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py + python3 $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py $< $@ --biased SPRITEBAKE_IIGS := $(REPO_DIR)/build/tools/spritebake-iigs STAXIBAKE := $(REPO_DIR)/build/tools/staxibake @@ -126,10 +137,14 @@ $(STAXICELS_SPR): $(STAXIBAKE) $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacet @mkdir -p $(dir $@) $(STAXIBAKE) $@ +$(EXAMPLES)/spacetaxi/generated/iigs/sprites/level%.spr: $(EXAMPLES)/spacetaxi/generated/iigs/levels/level%.dat $(STAXIBAKE) $(EXAMPLES)/spacetaxi/stLevelCels.c + @mkdir -p $(dir $@) + $(STAXIBAKE) --level $< $@ + $(EXAMPLES)/spacetaxi/generated/iigs/sprites/%.spc: $(EXAMPLES)/spacetaxi/generated/iigs/sprites/%.spr $(SPRITEBAKE_IIGS) $(SPRITEBAKE_IIGS) $< $@ -iigs-disk: iigs-examples $(NTP_BIN) $(STAXI_IIGS_SPC) +iigs-disk: iigs-examples $(NTP_BIN) $(STAXI_IIGS_SPC) $(STAXILEVEL_IIGS_SPC) $(STAXI_IIGS_SPEECH) BINDIR="$(BINDIR)" NTP_BIN="$(NTP_BIN)" $(REPO_DIR)/scripts/make-iigs-disk.sh $(BINDIR)/joey.2mg # Every launchable example on one image. The full set (~1.7 MB of binaries @@ -137,7 +152,7 @@ iigs-disk: iigs-examples $(NTP_BIN) $(STAXI_IIGS_SPC) # ProDOS hard-disk image (joey-all.2mg): boot it in MAME with # `-sl7 cffa2 -hard1 -hard2 build/iigs/bin/joey-all.2mg` and launch # any example from the JOEYLIB volume in the Finder. -iigs-disk-all: iigs-examples $(NTP_BIN) $(STAXI_IIGS_SPC) +iigs-disk-all: iigs-examples $(NTP_BIN) $(STAXI_IIGS_SPC) $(STAXILEVEL_IIGS_SPC) $(STAXI_IIGS_SPEECH) BINDIR="$(BINDIR)" NTP_BIN="$(NTP_BIN)" \ JOEY_DISK_SIZE="16MB" \ JOEY_DISK_EXAMPLES="DRAW PATTERN KEYS JOY SPRITE SERIAL SERTEST SAVE AUDIO UBER ADV2 ADV AGI STAXI" \ @@ -291,10 +306,26 @@ $(BINDIR)/ADV2: $(ADV2_SRC) $(LIB) $(IIGS_CLANG_BUILD) @mkdir -p $(dir $@) $(DEP_DIR) $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/ADV2.d $(INCLUDES) -o $@ $(ADV2_SRC) $(LIB) +# AGI opts into large overflow segments. --segment-cap bounds the ENTRY +# bank (which also holds rodata + bss + heap under the $$C000 IO window); +# overflow banks carry text only, so capping them the same way wasted +# ~50K per bank. With this AGI links 5 bank-aligned segments instead of +# 19, which matters because it already needs -ramsize 4M. Verified by +# running KQ3 headless and diffing AGI's own joeylog.txt against the +# 19-segment build: identical apart from the embedded build timestamp. +# +# Both AGI and STAXI opt in; see clang-build.sh for the measurements. +$(BINDIR)/AGI: export JOEY_OVERFLOW_CAP := 0xF000 $(BINDIR)/AGI: $(AGI_SRCS) $(LIB) $(IIGS_CLANG_BUILD) @mkdir -p $(dir $@) $(DEP_DIR) $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/AGI.d $(INCLUDES) -I$(EXAMPLES)/agi -o $@ $(AGI_SRCS) $(LIB) +# STAXI now also opts into large overflow segments: 26 bank-aligned +# segments starved the Memory Manager, so NTP's 34K NewHandle failed and +# the IIgs build ran SILENT (gNTPReady stayed 0). Freeing that memory lets +# audio initialise; the latent crash that then surfaced is fixed in +# src/iigs/audio_full.c (sfxTrigger now requires a running replayer). +$(BINDIR)/STAXI: export JOEY_OVERFLOW_CAP := 0xF000 $(BINDIR)/STAXI: $(STAXI_SRCS) $(LIB) $(IIGS_CLANG_BUILD) @mkdir -p $(dir $@) $(DEP_DIR) $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/STAXI.d $(INCLUDES) -I$(EXAMPLES)/spacetaxi -o $@ $(STAXI_SRCS) $(LIB) diff --git a/make/tools.mk b/make/tools.mk index d38629b..1834d3a 100644 --- a/make/tools.mk +++ b/make/tools.mk @@ -29,7 +29,7 @@ MKSTLEVEL_BIN := $(BUILD_DIR)/mkstlevel # plain host tool (the .spr is cross-platform); spritebake then compiles # it per target. STAXI_DIR := $(REPO_DIR)/examples/spacetaxi -STAXIBAKE_SRC := $(TOOLS_DIR)/staxibake/staxibake.c $(STAXI_DIR)/stCels.c $(STAXI_DIR)/stC64Data.c +STAXIBAKE_SRC := $(TOOLS_DIR)/staxibake/staxibake.c $(STAXI_DIR)/stCels.c $(STAXI_DIR)/stC64Data.c $(STAXI_DIR)/stLevelCels.c $(STAXI_DIR)/stLevelFile.c STAXIBAKE_BIN := $(BUILD_DIR)/staxibake # spritebake -- offline sprite pre-compiler. Built ONCE PER TARGET on the @@ -42,7 +42,7 @@ SPRITEBAKE_SRC := $(TOOLS_DIR)/spritebake/spritebake.c $(SRC_CG)/spriteStage.c SPRITEBAKE_INC := -I$(INCLUDE_DIR) -I$(SRC_CORE) -I$(SRC_CG) .PHONY: all tools clean-tools modloopend mkstlevel staxibake \ - spritebake-dos spritebake-amiga spritebake-atarist spritebake-iigs + spritebake-dos spritebake-amiga spritebake-atarist spritebake-iigs spritebake-x68000 all tools: $(MODLOOPEND_BIN) $(MKSTLEVEL_BIN) modloopend: $(MODLOOPEND_BIN) mkstlevel: $(MKSTLEVEL_BIN) @@ -51,6 +51,7 @@ spritebake-dos: $(BUILD_DIR)/spritebake-dos spritebake-amiga: $(BUILD_DIR)/spritebake-amiga spritebake-atarist: $(BUILD_DIR)/spritebake-atarist spritebake-iigs: $(BUILD_DIR)/spritebake-iigs +spritebake-x68000: $(BUILD_DIR)/spritebake-x68000 $(MODLOOPEND_BIN): $(MODLOOPEND_SRC) @mkdir -p $(dir $@) @@ -72,6 +73,12 @@ $(BUILD_DIR)/spritebake-amiga: $(SPRITEBAKE_SRC) $(SRC_68K)/spriteEmitPlanar68k. @mkdir -p $(dir $@) $(HOST_CC) $(HOST_CFLAGS) -DJOEYLIB_PLATFORM_AMIGA $(SPRITEBAKE_INC) -I$(SRC_DIR)/amiga -I$(SRC_68K) $^ -o $@ +# The X68000 runs the Amiga's planar 68k emitter unchanged (see +# make/x68000.mk), so its bake is the same source under its own target byte. +$(BUILD_DIR)/spritebake-x68000: $(SPRITEBAKE_SRC) $(SRC_68K)/spriteEmitPlanar68k.c + @mkdir -p $(dir $@) + $(HOST_CC) $(HOST_CFLAGS) -DJOEYLIB_PLATFORM_X68000 $(SPRITEBAKE_INC) -I$(SRC_DIR)/x68000 -I$(SRC_68K) $^ -o $@ + $(BUILD_DIR)/spritebake-atarist: $(SPRITEBAKE_SRC) $(SRC_68K)/spriteEmitInterleaved68k.c @mkdir -p $(dir $@) $(HOST_CC) $(HOST_CFLAGS) -DJOEYLIB_PLATFORM_ATARIST $(SPRITEBAKE_INC) -I$(SRC_DIR)/atarist -I$(SRC_68K) $^ -o $@ diff --git a/make/x68000.mk b/make/x68000.mk index 3f9cbb8..568cbb3 100644 --- a/make/x68000.mk +++ b/make/x68000.mk @@ -95,7 +95,7 @@ SERIAL_SRC := $(EXAMPLES)/serial/serial.c UBER_SRC := $(EXAMPLES)/uber/uber.c AUDIO_SRC := $(EXAMPLES)/audio/audio.c KEYS_SRC := $(EXAMPLES)/keys/keys.c -STAXI_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stLevelFile.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stSim.c $(EXAMPLES)/spacetaxi/stFare.c $(EXAMPLES)/spacetaxi/stHooks.c $(EXAMPLES)/spacetaxi/stTitle.c $(EXAMPLES)/spacetaxi/stC64Data.c $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stAudio.c +STAXI_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stLevelFile.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stSim.c $(EXAMPLES)/spacetaxi/stFare.c $(EXAMPLES)/spacetaxi/stHooks.c $(EXAMPLES)/spacetaxi/stTitle.c $(EXAMPLES)/spacetaxi/stC64Data.c $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stAudio.c $(EXAMPLES)/spacetaxi/stLevelCels.c .PHONY: all x68000 x68000-lib x68000-examples x68000-verify-serial x68000-verify-mouse x68000-verify-golden clean-x68000 clean @@ -167,15 +167,88 @@ $(BINDIR)/KEYS.X: $(KEYS_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(X68K_CC) $(CFLAGS) $(KEYS_SRC) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@ +# Space Taxi asset variables (the rules follow the binary below); make +# expands prerequisites when it reads the rule, so these come first. +STAXI_GEN_DIR := $(EXAMPLES)/spacetaxi/generated/x68000 +STAXI_RUN_DIR := $(BINDIR)/DATA +STAXI_RAW_DUMP := $(REPO_DIR)/stuff/spacetaxi/raw.bin +ROMTOLEVEL_BIN := $(REPO_DIR)/stuff/spacetaxi/romToLevel.py +STAXI_LEVEL_NUMS := 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 +STAXI_LEVEL_GEN := $(foreach n,$(STAXI_LEVEL_NUMS),$(STAXI_GEN_DIR)/levels/level$(n).dat) \ + $(STAXI_GEN_DIR)/levels/title.dat +STAXI_LEVEL_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_LEVEL_GEN)) +SPRITEBAKE_BIN := $(REPO_DIR)/build/tools/spritebake-x68000 +STAXIBAKE_BIN := $(REPO_DIR)/build/tools/staxibake +STAXICELS_SPR_GEN := $(STAXI_GEN_DIR)/sprites/staxicels.spr +STAXICELS_SPC_GEN := $(STAXICELS_SPR_GEN:.spr=.spc) +STAXICELS_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXICELS_SPC_GEN)) +STAXI_ASSET_DSTS := $(STAXI_LEVEL_RUN) $(STAXICELS_SPC_RUN) + +# Per-level hook-sprite banks: the cels stLevelCelList names for each +# level that has hook sprites, baked from its level file and pre-compiled +# for this target, so no cel is JIT-compiled mid-ride. Levels without +# hook sprites have no bank; the game JIT-compiles at level entry when a +# bank is missing. +STAXI_CEL_LEVELS := 07 08 09 10 12 16 17 21 +STAXILEVEL_SPR_GEN := $(foreach n,$(STAXI_CEL_LEVELS),$(STAXI_GEN_DIR)/sprites/level$(n).spr) +STAXILEVEL_SPC_GEN := $(STAXILEVEL_SPR_GEN:.spr=.spc) +STAXILEVEL_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXILEVEL_SPC_GEN)) +STAXI_ASSET_DSTS += $(STAXILEVEL_SPC_RUN) + +# Speech baked to sample bytes per passenger pitch (assets/genSpeechPcm.py): +# expanding bits at play time is what keeps "HEY TAXI" from stalling the +# ride; the C64 run-length source stays in stSpeechData.c as the input. +STAXI_SPEECH_GEN := $(STAXI_GEN_DIR)/speech.bin +STAXI_SPEECH_RUN := $(STAXI_RUN_DIR)/speech.bin +STAXI_ASSET_DSTS += $(STAXI_SPEECH_RUN) +$(STAXI_SPEECH_GEN): $(EXAMPLES)/spacetaxi/stSpeechData.c $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py + @mkdir -p $(dir $@) + python3 $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py $< $@ +$(STAXI_SPEECH_RUN): $(STAXI_SPEECH_GEN) + @mkdir -p $(dir $@) + cp $< $@ + # Space Taxi. -s (strip) because the image it ships on is a 1232 KB # Human68k floppy: the unstripped binary is most of the volume on its -# own. No precompiled sprite bank is staged -- this port has a real 68k -# planar emitter, so the boot prewarm JITs its cels in well under a -# second (the reason the IIgs needs a baked .spc is its ~0.4 s per cel). -$(BINDIR)/STAXI.X: $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) +# own. The DATA tree next to it (levels + the precompiled cel bank) is +# what scripts/run-x68000.sh copies onto the data floppy. +$(BINDIR)/STAXI.X: $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) $(STAXI_ASSET_DSTS) @mkdir -p $(dir $@) $(X68K_CC) $(CFLAGS) -s -I$(EXAMPLES)/spacetaxi $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@ +# Space Taxi asset staging, the amiga.mk pattern cut down to what this +# port loads at run time: the 24 levels + title extracted from the C64 +# dump, and the cel bank baked by staxibake and pre-compiled for THIS +# target by spritebake-x68000 (the planar 68k emitter under target byte +# 5, so an Amiga or DOS bank is refused by the loader). Tiles and the +# font come from the C64 data in stC64Data.c, so no .tbk is staged. +.SECONDARY: $(STAXI_LEVEL_GEN) $(STAXICELS_SPR_GEN) $(STAXICELS_SPC_GEN) $(STAXILEVEL_SPR_GEN) $(STAXILEVEL_SPC_GEN) + +$(STAXI_LEVEL_GEN) &: $(STAXI_RAW_DUMP) $(ROMTOLEVEL_BIN) + @mkdir -p $(STAXI_GEN_DIR)/levels + python3 $(ROMTOLEVEL_BIN) $(STAXI_RAW_DUMP) $(STAXI_GEN_DIR)/levels + +$(SPRITEBAKE_BIN): + $(MAKE) -f $(REPO_DIR)/make/tools.mk spritebake-x68000 + +$(STAXIBAKE_BIN): + $(MAKE) -f $(REPO_DIR)/make/tools.mk staxibake + +$(STAXICELS_SPR_GEN): $(STAXIBAKE_BIN) $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stC64Data.c + @mkdir -p $(dir $@) + $(STAXIBAKE_BIN) $@ + +$(STAXI_GEN_DIR)/sprites/%.spc: $(STAXI_GEN_DIR)/sprites/%.spr $(SPRITEBAKE_BIN) + $(SPRITEBAKE_BIN) $< $@ + +$(STAXI_RUN_DIR)/levels/%.dat: $(STAXI_GEN_DIR)/levels/%.dat + @mkdir -p $(dir $@) + cp $< $@ + +$(STAXI_RUN_DIR)/sprites/%.spc: $(STAXI_GEN_DIR)/sprites/%.spc + @mkdir -p $(dir $@) + cp $< $@ + # Both-directions RS-232C gate (~1 min). x68000-verify-serial: $(BINDIR)/SERIAL.X $(REPO_DIR)/scripts/verify-x68000-serial.sh @@ -199,3 +272,8 @@ clean-x68000: clean: clean-x68000 -include $(shell find $(BUILD) -name '*.d' 2>/dev/null) + +# Shorter stem than the %.spr-from-%.png pattern, so it wins for level banks. +$(STAXI_GEN_DIR)/sprites/level%.spr: $(STAXI_GEN_DIR)/levels/level%.dat $(STAXIBAKE_BIN) $(EXAMPLES)/spacetaxi/stLevelCels.c + @mkdir -p $(dir $@) + $(STAXIBAKE_BIN) --level $< $@ diff --git a/scripts/make-iigs-boot-disk.sh b/scripts/make-iigs-boot-disk.sh new file mode 100755 index 0000000..e80a7b7 --- /dev/null +++ b/scripts/make-iigs-boot-disk.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# make-iigs-boot-disk.sh - Build a GS/OS boot floppy that launches one +# JoeyLib example DIRECTLY, with no Finder in the way. +# +# WHY: the GS/OS Finder cannot be driven from a headless harness. Under +# gssquared, injected keystrokes reach the $C000 latch but never reach the +# Finder -- Open-Apple-A, Tab, arrow keys and plain letters all leave the +# desktop untouched (the single redraw seen after the first key of a run is +# the Finder's own settling: it was byte-identical for three different +# keys). Reaching the desktop also costs ~61s of boot per run. MAME's Lua +# harness drives the Finder by a different route, but that is one more +# moving part per probe. +# +# This replaces /System/Start (normally the Finder) with tools/iigsStart, +# whose whole job is a GS/OS QuitGS to the pathname baked in at build time. +# Boot the result as the FIRST drive and the example's own disk as the +# second; the example volume is mounted before Start runs. +# +# Usage: scripts/make-iigs-boot-disk.sh [volume] +# EXAMPLE uppercase binary name as it appears on the data disk +# volume data volume name, default JOEYLIB +# Requires: toolchains/env.sh sourced. +set -euo pipefail + +out=${1:?usage: make-iigs-boot-disk.sh [volume]} +prog=${2:?usage: make-iigs-boot-disk.sh [volume]} +vol=${3:-JOEYLIB} + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +: "${LLVM816_ROOT:?make-iigs-boot-disk.sh: source toolchains/env.sh first}" +CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}" +sysDisk="$repo/toolchains/emulators/support/gsos-system.po" + +[ -x "$CADIUS" ] || { echo "make-iigs-boot-disk.sh: cadius not found at $CADIUS" >&2; exit 2; } +[ -f "$sysDisk" ] || { echo "make-iigs-boot-disk.sh: missing $sysDisk" >&2; exit 2; } + +work=$(mktemp -d -t joeylib-boot.XXXXXX) +trap 'rm -rf "$work"' EXIT + +# The GS/OS system disk's own volume name, which cadius paths are relative to. +# Match the volume line, which is a WHOLE line of the form "/NAME/". An +# unanchored match also hits the absolute image path in cadius's header +# (it yielded "home" from /home/scott/...). +sysVol=$("$CADIUS" CATALOG "$sysDisk" 2>/dev/null \ + | grep -oE '^/[A-Za-z0-9._]+/$' | head -1 | tr -d '/') +[ -n "$sysVol" ] || { echo "make-iigs-boot-disk.sh: cannot read system volume name" >&2; exit 1; } + +cp "$sysDisk" "$out" + +# Out with the Finder (~200KB), in with the launcher. Deleting first is what +# makes room: the stock disk has under 50KB free. +"$CADIUS" DELETEFILE "$out" "/$sysVol/System/Start" >/dev/null + +"$repo/toolchains/iigs/clang-build.sh" \ + -DJOEY_START_PATH="\"/$vol/$prog\"" \ + -o "$work/Start#B30000" \ + "$repo/tools/iigsStart/iigsStart.c" \ + "$repo/tools/iigsStart/iigsStartQuit.s" >/dev/null + +"$CADIUS" ADDFILE "$out" "/$sysVol/System" "$work/Start#B30000" >/dev/null + +# Never trust the exit code of a disk build here -- cadius reports success +# for a file it silently dropped when the volume was full. +if ! "$CADIUS" CATALOG "$out" 2>/dev/null | grep -qE '^ +Start +S16'; then + echo "make-iigs-boot-disk.sh: Start was not written (volume full?)" >&2 + exit 1 +fi + +echo "iigs-boot-disk: $out (boots straight into /$vol/$prog)" diff --git a/scripts/make-iigs-disk.sh b/scripts/make-iigs-disk.sh index e20dc7a..5b14bcd 100755 --- a/scripts/make-iigs-disk.sh +++ b/scripts/make-iigs-disk.sh @@ -17,6 +17,12 @@ repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) : "${LLVM816_ROOT:?make-iigs-disk.sh: source toolchains/env.sh first}" CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}" BINDIR="${BINDIR:-$repo/build/iigs/bin}" +# The NTP replayer. DEFAULTED, not optional: without it on the volume every +# IIgs example is silent except the ROM boot beep, because jlpAudioInit +# fopen()s "ntpplayer" and returns before ntpArm() when that read fails -- +# with no diagnostic. Callers used to have to pass NTP_BIN and one of them +# passed the wrong path, which is exactly how Space Taxi ended up mute. +NTP_BIN="${NTP_BIN:-$repo/build/iigs/audio/ntpplayer.bin}" OUT="${1:-$BINDIR/joey.2mg}" VOL=JOEYLIB # Volume size passed to cadius CREATEVOLUME. Defaults to an 800KB 3.5" @@ -77,18 +83,33 @@ done # The NTP replayer binary (audio): jlpAudioInit fopen()s "ntpplayer" from # the volume at runtime (see src/iigs/audio_full.c NTP_PATH). Plain ProDOS # BIN ($06) file. Skipped with a warning if it doesn't fit or wasn't built. -if [ -n "${NTP_BIN:-}" ] && [ -f "$NTP_BIN" ]; then +ntpOk=0 +if [ -f "$NTP_BIN" ]; then ntpSz=$(stat -c%s "$NTP_BIN") ntpNeed=$(( (ntpSz + 511) / 512 + 2 )) if [ "$ntpNeed" -le "$(freeBlocks)" ]; then cp "$NTP_BIN" "$work/NTPPLAYER#060000" "$CADIUS" ADDFILE "$OUT" "/$VOL" "$work/NTPPLAYER#060000" >/dev/null rm -f "$work/NTPPLAYER#060000" - added+=(NTPPLAYER) - else - skipped+=(NTPPLAYER) + # Never trust the exit code here (see the launch check below). + if "$CADIUS" CATALOG "$OUT" 2>/dev/null | grep -qE '^ +NTPPLAYER +BIN'; then + added+=(NTPPLAYER) + ntpOk=1 + fi fi fi +if [ "$ntpOk" -eq 0 ]; then + skipped+=(NTPPLAYER) + cat >&2 </dev/null rm -f "$work/${base^^}#060000" dataAdded=$((dataAdded + 1)) - done < <(find "$staxiData" -type f | sort) + done < <(find "$staxiData" -type f -not -path '*/sprites/level*.spc' | sort) + # The per-level hook-sprite banks are optional (the game JIT-compiles + # a level's cels at entry when its bank is missing) and large, so + # they go on last and only while the floppy has room for them. + levelSkipped=0 + while IFS= read -r f; do + base=$(basename "$f") + sz=$(stat -c%s "$f") + need=$(( (sz + 511) / 512 + (sz + 131071) / 131072 + 2 )) + if [ "$need" -gt "$(freeBlocks)" ]; then + levelSkipped=$((levelSkipped + 1)) + continue + fi + cp "$f" "$work/${base^^}#060000" + "$CADIUS" ADDFILE "$OUT" "/$VOL/DATA/SPRITES" "$work/${base^^}#060000" >/dev/null + rm -f "$work/${base^^}#060000" + dataAdded=$((dataAdded + 1)) + done < <(find "$staxiData" -type f -path '*/sprites/level*.spc' | sort) added+=("DATA[$dataAdded files]") + if [ "$levelSkipped" -gt 0 ]; then + echo " note: $levelSkipped level sprite bank(s) left off (floppy full; those levels JIT at entry)" >&2 + fi else echo " WARNING: STAXI packed without its DATA tree ($staxiData missing)" >&2 fi diff --git a/scripts/run-iigs.sh b/scripts/run-iigs.sh index ea27d0e..16e428a 100755 --- a/scripts/run-iigs.sh +++ b/scripts/run-iigs.sh @@ -7,12 +7,12 @@ # scripts/run-iigs.sh staxi MAME auto-launch: builds a # single-example run disk # (joey-run.2mg -- joey.2mg is left -# alone so benches stay honest), -# boots GS/OS at real speed, and -# drives Finder via Lua to launch -# the example. No clicking needed. -# GS/OS itself takes ~50 s to boot; -# that part is authentic. +# alone so benches stay honest) plus a +# Finder-less boot disk whose +# /System/Start IS the launcher, so +# the example is on screen in ~50 s +# with no Finder and no keystroke +# driving. No clicking needed. # scripts/run-iigs.sh --all MAME + every example on one 16 MB # CFFA2 hard disk (the full set does # not fit an 800KB floppy): boots @@ -97,7 +97,7 @@ if [[ $all_mode -eq 1 ]]; then fi if [[ $need_disk -eq 1 ]]; then echo "run-iigs: building all-examples disk ($all_disk)..." - BINDIR="$bin_dir" NTP_BIN="$bin_dir/ntpplayer.bin" \ + BINDIR="$bin_dir" \ JOEY_DISK_SIZE="16MB" JOEY_DISK_EXAMPLES="$all_examples" \ "$repo/scripts/make-iigs-disk.sh" "$all_disk" else @@ -433,42 +433,27 @@ EOF } trap cleanup EXIT INT TERM - cp "$sys_disk" "$work/boot.po" + # Finder-less boot disk: /System/Start is tools/iigsStart, which + # SetPrefixGS's to the data volume and QuitGS-launches the example. + # This replaces the old Lua keyboard driver, which typed J / Cmd-O / + # name / Cmd-O at calibrated frame counts -- workable but fragile, + # and it made every run sit through the Finder's ~60 s desktop paint + # before anything happened. Straight to the example in ~50 s now. + if [[ -z "${LLVM816_ROOT:-}" ]]; then + # shellcheck disable=SC1091 + source "$repo/toolchains/env.sh" >/dev/null 2>&1 || true + fi + echo "run-iigs: building Finder-less boot disk for $target..." + "$repo/scripts/make-iigs-boot-disk.sh" "$work/boot.po" "$target" >/dev/null cp "$run_disk" "$work/joey-run.2mg" - - # Slim Finder driver -- the keyboard-injection steps proven in - # run-iigs-mame.sh's harness, without the debugger/crash plumbing: - # at calibrated frame counts, type J (select the JOEYLIB volume), - # Cmd-O (open it), the example's full name (select; full-name - # typing disambiguates e.g. DRAW vs DATA), Cmd-O (launch). Frame - # counts are emulated frames, so throttling doesn't shift them. headless=${MAME_HEADLESS:-0} cat > "$work/launch.lua" <&2; exit 2; } +[ -f "$repo/build/iigs/bin/$NAME" ] || { echo "verify-iigs-audio: missing build/iigs/bin/$NAME (run 'make iigs-disk')" >&2; exit 2; } + +work=$(mktemp -d -t joeylib-audio.XXXXXX) +trap 'rm -rf "$work"' EXIT + +# One-example volume: the 800KB floppy cannot hold every example, and the +# shared joey.2mg drops AUDIO and STAXI first (see make-iigs-disk.sh). +JOEY_DISK_EXAMPLES="$NAME" BINDIR="$repo/build/iigs/bin" \ + NTP_BIN="$repo/build/iigs/audio/ntpplayer.bin" \ + "$repo/scripts/make-iigs-disk.sh" "$work/joey.2mg" >/dev/null +cp "$sys_disk" "$work/boot.po" + +cat > "$work/audio.lua" <= steps[idx][1] do + steps[idx][2]() + idx = idx + 1 + end +end) +LUA + +cd "$work" +out=$(QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \ + timeout 400 mame apple2gs \ + -rompath "$rompath" \ + -flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \ + -video none -sound none -nothrottle \ + -autoboot_script "$work/audio.lua" &1) || true + +lineA=$(echo "$out" | grep -E '^VERIFY-AUDIO .* tag=A ' | tail -1) +lineB=$(echo "$out" | grep -E '^VERIFY-AUDIO .* tag=B ' | tail -1) +echo "$lineA" +echo "$lineB" +if [ -z "$lineA" ] || [ -z "$lineB" ]; then + echo "verify-iigs-audio: FAIL - no probe report (boot/launch failed)" >&2 + echo "$out" | tail -15 >&2 + exit 1 +fi + +field() { echo "$2" | sed -E "s/.*$1=([0-9A-Fa-f]+).*/\1/"; } + +# Launch gate first, exactly as verify-iigs.sh: the Finder desktop alone +# would otherwise be graded as if it were the example. +scb0=$(field scb0 "$lineB") +if [ $((16#$scb0)) -ge 16 ]; then + echo "verify-iigs-audio: FAIL ($NAME never launched - SHR SCB[0]=0x$scb0 is the Finder's 640-mode fill)" >&2 + exit 1 +fi + +vec=$(field irqVec "$lineB") +if [ "${vec:6:2}" != "5C" ]; then + echo "verify-iigs-audio: FAIL (sound interrupt vector is $vec, not a JML - NTPprepare never ran, so there is no interrupt to refill a streamed sample)" >&2 + exit 1 +fi +# The vector the ROM leaves in place is a JML too ($5C 62 CD FC on this +# machine), so "is it a JML" is not enough -- the control run with the +# pre-ntpArm audio_full.c passed that test. NTP lives in a Memory +# Manager handle, always in RAM, so a ROM bank ($F0 and up) means the +# system vector is still installed and NTPprepare never ran. +bank=${vec:0:2} +if [ $((16#$bank)) -ge 240 ]; then + echo "verify-iigs-audio: FAIL (sound interrupt vector $vec still points into ROM bank \$$bank - NTPprepare never ran)" >&2 + exit 1 +fi + +# Audible-output precondition: the DOC master volume must not be zero. +docVol=$(field docVol "$lineB") +if [ $(( 16#$docVol & 0x0F )) -eq 0 ]; then + echo "verify-iigs-audio: FAIL (DOC master volume is 0 -- \$E100CA=0x$docVol. The Sound Tool Set was never started, so NTP copies a zero volume into \$C03C: the DOC streams but nothing is audible)" >&2 + exit 1 +fi + +if [ "$(field slot0NZ "$lineA")" = "-1" ]; then + echo "verify-iigs-audio: FAIL (MAME exposed no :doc/rom space - the DOC RAM probe read nothing)" >&2 + exit 1 +fi +loud="" +moved="" +slot=0 +while [ "$slot" -lt "$slots" ]; do + nz=$(field "slot${slot}NZ" "$lineB") + if [ "$nz" -gt 0 ]; then + loud="$loud $slot" + fi + if [ "$(field "slot$slot" "$lineA")" != "$(field "slot$slot" "$lineB")" ]; then + moved="$moved $slot" + fi + slot=$((slot + 1)) +done +if [ -z "$loud" ]; then + echo "verify-iigs-audio: FAIL (no SFX slot's DOC page holds a waveform - NTPstreamsound never reached the DOC)" >&2 + exit 1 +fi + +# A live NTP interrupt keeps rewriting the DOC page it streams through. +# A slot parked on the resident tone square (toneSquareEnsure) hashes +# the same every time -- every refill writes the same square -- so this +# only warns when NO slot moved at all. +if [ -z "$moved" ]; then + echo "verify-iigs-audio: WARN (no DOC slot changed between frames $probeA and $probeB - samples are present but nothing is refilling them)" +fi +echo "verify-iigs-audio: PASS ($NAME: NTP interrupt installed at bank $bank; DOC slots with a waveform:$loud; refilled between probes:$moved)" diff --git a/src/amiga/audio.c b/src/amiga/audio.c index db2fb14..68ccace 100644 --- a/src/amiga/audio.c +++ b/src/amiga/audio.c @@ -79,9 +79,29 @@ extern UBYTE mt_E8Trigger; // ----- Module state ----- +// Streaming SFX chunk size, in bytes. PTPlayer's mt_playfx plays a +// one-shot sample and does not queue, so a stream is played one chunk at +// a time with the next one fired from jlpAudioFrameTick when the current +// is due to end. Bigger chunks mean fewer seams; 4 KB is ~0.8 s at 5 kHz, +// which covers a whole speech sample with no seam at all. +#define SFX_STREAM_CHUNK 4096u + +// The host calls jlpAudioFrameTick once per display frame, and this port +// is PAL throughout (see PAULA_CLOCK_PAL in periodForRate), so chunk +// lifetimes are counted in 50 Hz frames. +#define AMIGA_FRAME_HZ 50u + typedef struct { UBYTE *samples; // Chip-RAM copy of one SFX sample UWORD lengthWords; // sample length in 16-bit words + // ---- streaming (jlpAudioPlaySfxStream) ---- + UBYTE *chunk; // Chip-RAM chunk buffer, SFX_STREAM_CHUNK + BYTE cha; // Paula channel, -1 = let PTPlayer pick + jlAudioStreamFillT fill; + void *ctx; + uint16_t rateHz; + uint16_t framesLeft; // until the playing chunk runs out + bool active; } SfxSlotT; static UBYTE *gModuleChip = NULL; // Chip-RAM copy of the playing module @@ -157,6 +177,9 @@ bool jlpAudioInit(void) { for (i = 0; i < JOEY_AUDIO_SFX_SLOTS; i++) { gSfxSlots[i].samples = NULL; gSfxSlots[i].lengthWords = 0; + // -1, not 0: zero is Paula channel 0, which would silently pin every + // slot hard left instead of letting PTPlayer choose. + gSfxSlots[i].cha = -1; } return true; } @@ -202,6 +225,11 @@ void jlpAudioShutdown(void) { gSfxSlots[i].samples = NULL; gSfxSlots[i].lengthWords = 0; } + gSfxSlots[i].active = false; + if (gSfxSlots[i].chunk != NULL) { + FreeMem(gSfxSlots[i].chunk, (ULONG)SFX_STREAM_CHUNK); + gSfxSlots[i].chunk = NULL; + } } } @@ -247,6 +275,42 @@ void jlpAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) { } +// Pull one chunk from a streaming slot and hand it to PTPlayer. +// Returns false when the stream has ended (fill returned 0). +static bool streamChunk(uint8_t slot) { + SfxSlotT *s = &gSfxSlots[slot]; + SfxStructure sfx; + uint32_t got; + + // The fill contract is SIGNED 8-bit, which is exactly what Paula + // wants, so the callback writes straight into Chip RAM. + got = s->fill(s->ctx, (int8_t *)s->chunk, (uint32_t)SFX_STREAM_CHUNK); + if (got < 2u || got > (uint32_t)SFX_STREAM_CHUNK) { + return false; + } + got &= ~1u; // Paula counts 16-bit words + + sfx.sfx_ptr = s->chunk; + sfx.sfx_len = (UWORD)(got >> 1); + sfx.sfx_per = periodForRate(s->rateHz); + sfx.sfx_vol = SFX_VOLUME_MAX; + sfx.sfx_cha = s->cha; + sfx.sfx_pri = (BYTE)(slot + 1); + mt_playfx((void *)&custom, &sfx); + + // Fire the next chunk a frame early rather than leaving a gap. + { + uint32_t frames = (got * (uint32_t)AMIGA_FRAME_HZ) / (uint32_t)s->rateHz; + + if (frames > 1u) { + frames--; + } + s->framesLeft = (uint16_t)frames; + } + return true; +} + + void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz) { SfxStructure sfx; SfxSlotT *s; @@ -281,22 +345,42 @@ void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1 sfx.sfx_len = words; sfx.sfx_per = periodForRate(rateHz); sfx.sfx_vol = SFX_VOLUME_MAX; - sfx.sfx_cha = -1; // best free channel + sfx.sfx_cha = s->cha; // -1 = best free channel sfx.sfx_pri = (BYTE)(slot + 1); // priority 1..N from slot index mt_playfx((void *)&custom, &sfx); } -// Streaming SFX is not wired on Amiga: PTPlayer's mt_playfx wants a -// complete sample at fire time. Implementing streaming here would -// need a per-slot Paula buffer with mid-playback refill from the -// audio IRQ. No-op until then; callers that depend on streaming -// will hear silence on Amiga. +// Streaming SFX. PTPlayer's mt_playfx wants a complete sample at fire +// time, so the stream is chunked rather than refilled mid-playback: +// arm the slot, push the first chunk now, and let jlpAudioFrameTick +// push each following one as the previous is due to run out. The seam +// therefore quantises to a 50 Hz frame, which is the jitter the public +// contract documents. Each slot gets one SFX_STREAM_CHUNK Chip-RAM +// buffer, allocated on first use and freed in jlpAudioShutdown. void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz) { - (void)slot; - (void)fill; - (void)ctx; - (void)rateHz; + SfxSlotT *s; + + if (!gInstalled || slot >= JOEY_AUDIO_SFX_SLOTS) { + return; + } + if (fill == (jlAudioStreamFillT)0 || rateHz == 0) { + return; + } + s = &gSfxSlots[slot]; + if (s->chunk == NULL) { + s->chunk = (UBYTE *)AllocMem((ULONG)SFX_STREAM_CHUNK, MEMF_CHIP); + if (s->chunk == NULL) { + return; + } + } + s->fill = fill; + s->ctx = ctx; + s->rateHz = rateHz; + s->active = true; + if (!streamChunk(slot)) { + s->active = false; + } } @@ -429,12 +513,36 @@ void jlpAudioStopMod(void) { } +// Paula pans per CHANNEL: 0 and 3 feed the left output, 1 and 2 the right. +// One voice cannot reach both ears, so AUDIO_PAN_CENTER keeps PTPlayer's own +// "best free channel" pick (sfx_cha = -1) -- which is what every slot did +// before this call existed, so the default behaviour is unchanged. A panned +// slot pins its SFX to a channel on that side instead. +void jlpAudioSetPan(uint8_t slot, uint8_t pan) { + if (slot >= JOEY_AUDIO_SFX_SLOTS) { + return; + } + switch (pan) { + case AUDIO_PAN_LEFT: + gSfxSlots[slot].cha = 3; + break; + case AUDIO_PAN_RIGHT: + gSfxSlots[slot].cha = 2; + break; + default: + gSfxSlots[slot].cha = -1; // PTPlayer picks + break; + } +} + + void jlpAudioStopSfx(uint8_t slot) { UBYTE i; if (!gInstalled || slot >= JOEY_AUDIO_SFX_SLOTS) { return; } + gSfxSlots[slot].active = false; // PTPlayer addresses SFX by Paula channel (0..3), but mt_playfx // picks a channel dynamically and does not return which one. We // can't reliably map slot -> channel after the fact, so stop all @@ -447,8 +555,27 @@ void jlpAudioStopSfx(uint8_t slot) { void jlpAudioFrameTick(void) { - // PTPlayer drives itself off CIA-B; the only host-loop work is the - // play-once watchdog. When the song author placed an `E8FF` at + // Streaming slots: mt_playfx plays one chunk and stops, so the next + // chunk goes out here as the previous one runs down. + { + uint8_t slot; + + for (slot = 0u; slot < JOEY_AUDIO_SFX_SLOTS; slot++) { + if (!gSfxSlots[slot].active) { + continue; + } + if (gSfxSlots[slot].framesLeft != 0u) { + gSfxSlots[slot].framesLeft--; + continue; + } + if (!streamChunk(slot)) { + gSfxSlots[slot].active = false; + } + } + } + + // PTPlayer drives itself off CIA-B; the only other host-loop work is + // the play-once watchdog. When the song author placed an `E8FF` at // song end, mt_E8Trigger latches to 0xFF -- if the caller passed // loop=false to PlayMod, that's our cue to stop the song. if (gWantLoopOnce && gModPlaying && mt_E8Trigger == PTPLAYER_LOOP_END_MARKER) { diff --git a/src/codegen/spriteCompile.c b/src/codegen/spriteCompile.c index 33de74a..0eb8f43 100644 --- a/src/codegen/spriteCompile.c +++ b/src/codegen/spriteCompile.c @@ -124,7 +124,7 @@ bool jlSpriteCompile(jlSpriteT *sp) { // Copy the staged image into the slot in one shot. routineOffsets are // already slot-relative (offset within `staging` == offset within the // slot after this copy), so nothing else needs fixing up. - memcpy(codegenArenaBase() + slot->offset, staging, (size_t)total); + memcpy(codegenArenaSlotBase(slot), staging, (size_t)total); sp->slot = slot; // Registry (#34): every creation path (including jlSpriteBankLoad, // #32) already registered sp via spriteInitFields; this idempotent diff --git a/src/core/assetLoad.c b/src/core/assetLoad.c index dab092f..af5bc37 100644 --- a/src/core/assetLoad.c +++ b/src/core/assetLoad.c @@ -109,6 +109,22 @@ typedef char AssetTileSizeCheckT[(sizeof(jlTileT) == TILE_BYTES) ? 1 : -1]; #error "Unknown platform for asset loader" #endif +// Per-platform target byte for .spc files (pre-compiled sprite code, so +// it is per MACHINE CODE, not per pixel format: the X68000 shares DOS's +// chunky tile byte above but runs 68k planar sprite routines, and must +// never accept an x86 bank). Mirrors SPC_TARGET in tools/spritebake. +#if defined(JOEYLIB_PLATFORM_AMIGA) +#define EXPECTED_SPC_TARGET 1u +#elif defined(JOEYLIB_PLATFORM_ATARIST) +#define EXPECTED_SPC_TARGET 2u +#elif defined(JOEYLIB_PLATFORM_DOS) || defined(JOEYLIB_PLATFORM_BLANK) +#define EXPECTED_SPC_TARGET 3u +#elif defined(JOEYLIB_PLATFORM_IIGS) +#define EXPECTED_SPC_TARGET 4u +#elif defined(JOEYLIB_PLATFORM_X68000) +#define EXPECTED_SPC_TARGET 5u +#endif + // ----- Prototypes ----- static bool openReadValidateHeader(const char *path, const char *magic, uint8_t expectedTarget, uint8_t *header, uint16_t *outPalette, FILE **outFp); @@ -290,8 +306,8 @@ uint16_t jlSpriteBankLoadPrecompiled(const char *path, jlSpriteT **outCels, uint return 0u; } // "JSC1" + the per-machine target byte (pre-compiled code is machine- - // specific, validated like a .tbk via the shared EXPECTED target). - if (!openReadValidateHeader(path, SPC_MAGIC, EXPECTED_TILE_TARGET, header, outPalette, &fp)) { + // specific, validated like a .tbk but against the .spc target table). + if (!openReadValidateHeader(path, SPC_MAGIC, EXPECTED_SPC_TARGET, header, outPalette, &fp)) { return 0u; } widthTiles = header[SPC_OFF_WIDTH_TILES]; @@ -400,7 +416,7 @@ uint16_t jlSpriteBankLoadPrecompiled(const char *path, jlSpriteT **outCels, uint } } if (slot != NULL) { - uint8_t *slotBase = codegenArenaBase() + slot->offset; + uint8_t *slotBase = codegenArenaSlotBase(slot); memcpy(slotBase, imageBuf, (size_t)imageSize); for (shift = 0; shift < shiftCount; shift++) { @@ -470,7 +486,7 @@ bool jlSpriteBankSavePrecompiled(const char *path, jlSpriteT **cels, uint16_t co // Shared 44-byte header (mirrors tools/spritebake + jlSpriteBankLoadPrecompiled). fwrite(SPC_MAGIC, 1, 4, fp); - fputc((int)EXPECTED_TILE_TARGET, fp); + fputc((int)EXPECTED_SPC_TARGET, fp); fputc(palette != NULL ? 1 : 0, fp); fputc((int)widthTiles, fp); fputc((int)heightTiles, fp); @@ -534,7 +550,7 @@ bool jlSpriteBankSavePrecompiled(const char *path, jlSpriteT **cels, uint16_t co } // image: the compiled routines, straight from the arena slot. if (imageSize > 0u) { - fwrite(codegenArenaBase() + sp->slot->offset, 1, (size_t)imageSize, fp); + fwrite(codegenArenaSlotBase(sp->slot), 1, (size_t)imageSize, fp); } for (r = 0; r < relocCount; r++) { fputc((int)(relocOffsets[r] & 0xFFu), fp); diff --git a/src/core/audio.c b/src/core/audio.c index b370b27..c319d46 100644 --- a/src/core/audio.c +++ b/src/core/audio.c @@ -77,6 +77,22 @@ void jlAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint } +void jlAudioPlaySfxStreamRaw(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz) { + if (!gAudioReady || fill == NULL || rateHz == 0) { + return; + } + if (slot >= JOEY_AUDIO_SFX_SLOTS) { + return; + } + jlpAudioPlaySfxStreamRaw(slot, fill, ctx, rateHz); +} + + +uint8_t jlAudioSfxRawFormat(void) { + return jlpAudioSfxRawFormat(); +} + + void jlAudioTone(uint16_t freqHz) { // Deliberately NOT gated on gAudioReady -- jlAudioInit is for // PCM/SFX paths that need DMA + libxmp wiring. The PC speaker @@ -124,6 +140,17 @@ void jlAudioCriticalExit(void) { } +void jlAudioSetPan(uint8_t slot, jlAudioPanE pan) { + if (!gAudioReady || slot >= JOEY_AUDIO_SFX_SLOTS) { + return; + } + if (pan >= AUDIO_PAN_COUNT) { + return; + } + jlpAudioSetPan(slot, (uint8_t)pan); +} + + void jlAudioStopSfx(uint8_t slot) { if (!gAudioReady || slot >= JOEY_AUDIO_SFX_SLOTS) { return; @@ -140,10 +167,12 @@ void jlAudioFrameTick(void) { if (!gAudioReady) { return; } -#ifndef JOEYLIB_PLATFORM_IIGS - // IIgs: NTPstreamsound is fully DOC-IRQ-driven, jlpAudioFrameTick - // is an empty no-op there. Skip the wrapper JSL entirely on IIgs - // so per-frame audio cost stays at the gAudioReady branch above. + // This used to be #ifndef'd out on the IIgs, on the grounds that + // NTPstreamsound is DOC-IRQ driven so jlpAudioFrameTick "is an empty + // no-op there". It is not: it pumps a registered jlAudioTickRegister + // callback, and since streaming SFX landed it also fires the next + // chunk of every active stream. Skipping it meant a stream played + // its first chunk and then stopped forever -- Space Taxi spoke its + // first word and never another. jlpAudioFrameTick(); -#endif } diff --git a/src/core/codegenArena.c b/src/core/codegenArena.c index 36b3129..89cccac 100644 --- a/src/core/codegenArena.c +++ b/src/core/codegenArena.c @@ -1,5 +1,6 @@ // Codegen arena: free-list allocator with adjacent-hole coalescing -// and manual compaction. See codegenArenaInternal.h for the contract. +// and manual compaction, over one bank (most ports) or several 64 KB +// banks (IIgs). See codegenArenaInternal.h for the contract. #include "joey/platform.h" @@ -22,6 +23,7 @@ #define attrLocked 0x8000 #endif +#include #include #include @@ -30,36 +32,154 @@ // ----- Module state ----- - -// gCodegenArenaBase / gCodegenArenaBaseAddr are non-static so jlSpriteCompile.c can read them -// directly via extern instead of paying a JSL/RTL per access through -// the codegenArenaBase() / codegenArenaBaseAddr() wrappers. Both are -// set once at codegenArenaInit and never moved (the underlying -// Memory Manager handle is locked-in-place on IIgs). Callers MUST +// +// gCodegenBankBase / gCodegenBankAddr are non-static so the sprite +// dispatch reads them directly via extern instead of paying a JSL/RTL +// per draw through a wrapper. They are set once at codegenArenaInit and +// never moved (a bank is locked in place on the IIgs). Callers MUST // treat them as read-only. -uint8_t *gCodegenArenaBase = NULL; -// gCodegenArenaBaseAddr mirrors gCodegenArenaBase as a 24-bit absolute -// address that JSL targets read directly (captured by copying the -// handle pointer's raw bytes -- see codegenArenaInit). -uint32_t gCodegenArenaBaseAddr = 0; + +uint8_t *gCodegenBankBase[CODEGEN_ARENA_BANKS_MAX]; +uint32_t gCodegenBankAddr[CODEGEN_ARENA_BANKS_MAX]; +static uint32_t gBankSize[CODEGEN_ARENA_BANKS_MAX]; +static ArenaSlotT *gBankFirst[CODEGEN_ARENA_BANKS_MAX]; +#if defined(JOEYLIB_PLATFORM_IIGS) +static void *gBankHandle[CODEGEN_ARENA_BANKS_MAX]; +#endif +static uint8_t gBankCount = 0u; static uint32_t gTotalBytes = 0; static uint32_t gUsedBytes = 0; -static ArenaSlotT *gFirstSlot = NULL; -#if defined(JOEYLIB_PLATFORM_IIGS) -static void *gCodegenArenaBaseHandle = NULL; -#endif // ----- Prototypes ----- -static ArenaSlotT *newSlot(uint32_t offset, uint32_t size, bool used); +static bool bankAlloc(uint8_t bank, uint32_t bytes); +static void bankCompact(uint8_t bank); +static void bankFree(uint8_t bank); static void coalesceWithNext(ArenaSlotT *slot); +static ArenaSlotT *newSlot(uint32_t offset, uint32_t size, bool used); -// ----- Internal helpers (alphabetical) ----- +// ----- Helpers ----- + +// One bank's memory. On the IIgs a Memory Manager block that may not +// cross a bank boundary (a normal C heap allocation returns bank-0 +// system RAM, and JSL'ing into that lands on whatever lives there); +// attrFixed | attrLocked | attrPage | attrNoCross gives a fixed +// page-aligned region in one bank. Elsewhere, malloc. +static bool bankAlloc(uint8_t bank, uint32_t bytes) { +#if defined(JOEYLIB_PLATFORM_IIGS) + void *h; + void *p; + uint8_t raw[4]; + + h = NewHandle((unsigned long)bytes, MMStartUp(), attrFixed | attrLocked | attrPage | attrNoCross, (void *)0); + if (h == NULL) { + return false; + } + HLock(h); + p = *(void **)h; + if (p == NULL) { + DisposeHandle(h); + return false; + } + // The 24-bit absolute address is the master pointer's raw bytes; + // byte 3 (bank-high/flags) is masked off. + memcpy(raw, &p, 4); + gBankHandle[bank] = h; + gCodegenBankBase[bank] = (uint8_t *)p; + gCodegenBankAddr[bank] = (uint32_t)raw[0] | ((uint32_t)raw[1] << 8) | ((uint32_t)raw[2] << 16); +#else + uint8_t *p = (uint8_t *)malloc(bytes); + + if (p == NULL) { + return false; + } + gCodegenBankBase[bank] = p; + gCodegenBankAddr[bank] = (uint32_t)(uintptr_t)p; +#endif + gBankSize[bank] = bytes; + gBankFirst[bank] = newSlot((uint32_t)bank << CODEGEN_ARENA_BANK_SHIFT, bytes, false); + if (gBankFirst[bank] == NULL) { + bankFree(bank); + return false; + } + return true; +} + + +static void bankCompact(uint8_t bank) { + uint8_t *base = gCodegenBankBase[bank]; + ArenaSlotT *slot; + ArenaSlotT *next; + ArenaSlotT *trailing; + uint32_t cursor = 0; + + slot = gBankFirst[bank]; + while (slot != NULL) { + next = slot->next; + if (slot->used) { + uint32_t at = slot->offset & CODEGEN_ARENA_BANK_MASK; + if (at != cursor) { + memmove(base + cursor, base + at, slot->size); + slot->offset = ((uint32_t)bank << CODEGEN_ARENA_BANK_SHIFT) | cursor; + } + cursor += slot->size; + slot = next; + continue; + } + if (slot->prev != NULL) { + slot->prev->next = next; + } else { + gBankFirst[bank] = next; + } + if (next != NULL) { + next->prev = slot->prev; + } + free(slot); + slot = next; + } + if (cursor < gBankSize[bank]) { + trailing = newSlot(((uint32_t)bank << CODEGEN_ARENA_BANK_SHIFT) | cursor, gBankSize[bank] - cursor, false); + if (trailing == NULL) { + return; // Compaction succeeded; just skip the free-slot record. + } + trailing->prev = NULL; + for (slot = gBankFirst[bank]; slot != NULL && slot->next != NULL; slot = slot->next) { + } + if (slot == NULL) { + gBankFirst[bank] = trailing; + } else { + slot->next = trailing; + trailing->prev = slot; + } + } +} + + +static void bankFree(uint8_t bank) { + ArenaSlotT *slot; + ArenaSlotT *next; + + for (slot = gBankFirst[bank]; slot != NULL; slot = next) { + next = slot->next; + free(slot); + } +#if defined(JOEYLIB_PLATFORM_IIGS) + if (gBankHandle[bank] != NULL) { + DisposeHandle(gBankHandle[bank]); + gBankHandle[bank] = NULL; + } +#else + free(gCodegenBankBase[bank]); +#endif + gCodegenBankBase[bank] = NULL; + gCodegenBankAddr[bank] = 0; + gBankSize[bank] = 0; + gBankFirst[bank] = NULL; +} + -// If `slot` is free and slot->next is also free, merge them into a -// single free slot. Caller must already have ensured slot is free. static void coalesceWithNext(ArenaSlotT *slot) { ArenaSlotT *victim; @@ -95,49 +215,43 @@ static ArenaSlotT *newSlot(uint32_t offset, uint32_t size, bool used) { } -// ----- Public-internal API (alphabetical) ----- +// ----- Public API ----- ArenaSlotT *codegenArenaAlloc(uint32_t bytes) { ArenaSlotT *slot; ArenaSlotT *remainder; + uint8_t bank; - if (gCodegenArenaBase == NULL || bytes == 0) { + if (gBankCount == 0u || bytes == 0) { return NULL; } - for (slot = gFirstSlot; slot != NULL; slot = slot->next) { - if (slot->used || slot->size < bytes) { - continue; - } - // First fit. If there's slack, split: shrink this slot to - // exactly `bytes` and insert a new free slot for the rest. - if (slot->size > bytes) { - remainder = newSlot(slot->offset + bytes, slot->size - bytes, false); - if (remainder == NULL) { - return NULL; + for (bank = 0u; bank < gBankCount; bank++) { + for (slot = gBankFirst[bank]; slot != NULL; slot = slot->next) { + if (slot->used || slot->size < bytes) { + continue; } - remainder->prev = slot; - remainder->next = slot->next; - if (slot->next != NULL) { - slot->next->prev = remainder; + if (slot->size > bytes) { + remainder = newSlot(slot->offset + bytes, slot->size - bytes, false); + if (remainder == NULL) { + return NULL; + } + remainder->prev = slot; + remainder->next = slot->next; + if (slot->next != NULL) { + slot->next->prev = remainder; + } + slot->next = remainder; + slot->size = bytes; } - slot->next = remainder; - slot->size = bytes; + slot->used = true; + gUsedBytes += bytes; + return slot; } - slot->used = true; - gUsedBytes += bytes; - return slot; } return NULL; } -// codegenArenaBase() / codegenArenaBaseAddr() are now header-only -// macros that read gCodegenArenaBase / gCodegenArenaBaseAddr -// directly, so the C function bodies that used to live here are -// gone. The wrappers cost ~30 cyc per call on IIgs and were hit -// 3x per sprite frame. - - uint32_t codegenArenaBytesTotal(void) { return gTotalBytes; } @@ -149,63 +263,16 @@ uint32_t codegenArenaBytesUsed(void) { void codegenArenaCompact(void) { - ArenaSlotT *slot; - ArenaSlotT *next; - ArenaSlotT *trailing; - uint32_t cursor; + uint8_t bank; - if (gCodegenArenaBase == NULL) { - return; - } - cursor = 0; - slot = gFirstSlot; - while (slot != NULL) { - next = slot->next; - if (slot->used) { - if (slot->offset != cursor) { - memmove(gCodegenArenaBase + cursor, gCodegenArenaBase + slot->offset, slot->size); - slot->offset = cursor; - } - cursor += slot->size; - slot = next; - continue; - } - // Free slot: drop from the list. The caller-side ArenaSlotT* - // for any free slot was already invalidated when it was - // freed (coalesce released the struct); nothing live points - // at it. - if (slot->prev != NULL) { - slot->prev->next = next; - } else { - gFirstSlot = next; - } - if (next != NULL) { - next->prev = slot->prev; - } - free(slot); - slot = next; - } - if (cursor < gTotalBytes) { - trailing = newSlot(cursor, gTotalBytes - cursor, false); - if (trailing == NULL) { - return; // Compaction succeeded; just skip the free-slot record. - } - trailing->prev = NULL; - for (slot = gFirstSlot; slot != NULL && slot->next != NULL; slot = slot->next) { - // walk to last - } - if (slot == NULL) { - gFirstSlot = trailing; - } else { - slot->next = trailing; - trailing->prev = slot; - } + for (bank = 0u; bank < gBankCount; bank++) { + bankCompact(bank); } } void codegenArenaFree(ArenaSlotT *slot) { - if (slot == NULL || gCodegenArenaBase == NULL) { + if (slot == NULL || gBankCount == 0u) { return; } if (!slot->used) { @@ -220,100 +287,51 @@ void codegenArenaFree(ArenaSlotT *slot) { bool codegenArenaInit(uint32_t totalBytes) { - if (gCodegenArenaBase != NULL) { + uint32_t want; + + if (gBankCount != 0u) { return true; } if (totalBytes == 0) { return false; } -#if defined(JOEYLIB_PLATFORM_IIGS) - // attrNoCross forbids the block from crossing a 64KB bank boundary, - // so any request larger than one bank (65536 bytes) is unsatisfiable - // and NewHandle would always return NULL. Clamp to a single bank. - // The clamped request can still fail below -- a 64KB attrNoCross - // block needs a completely free bank -- and the caller's error - // message covers that. - if (totalBytes > 65536UL) { - totalBytes = 65536UL; - } - // MMStartUp() returns this application's master user ID (idempotent; - // the system starts the Memory Manager before the app runs). A NULL - // handle is the failure signal. - { - uint16_t ownerId; - - ownerId = MMStartUp(); - gCodegenArenaBaseHandle = NewHandle((unsigned long)totalBytes, ownerId, - attrFixed | attrLocked | attrPage | attrNoCross, - (void *)0); - } - if (gCodegenArenaBaseHandle == NULL) { - return false; - } - HLock(gCodegenArenaBaseHandle); - // Capture the 24-bit absolute address by copying the master - // pointer's raw bytes; bytes[3] (bank-high/flags) is masked off. - { - void *p; - uint8_t bytes[4]; - - p = *(void **)gCodegenArenaBaseHandle; - gCodegenArenaBase = (uint8_t *)p; - memcpy(bytes, &p, 4); - gCodegenArenaBaseAddr = (uint32_t)bytes[0] - | ((uint32_t)bytes[1] << 8) - | ((uint32_t)bytes[2] << 16); - } - if (gCodegenArenaBase == NULL) { - DisposeHandle(gCodegenArenaBaseHandle); - gCodegenArenaBaseHandle = NULL; - return false; - } -#else - gCodegenArenaBase = (uint8_t *)malloc(totalBytes); - if (gCodegenArenaBase == NULL) { - return false; - } - gCodegenArenaBaseAddr = (uint32_t)gCodegenArenaBase; -#endif - gFirstSlot = newSlot(0, totalBytes, false); - if (gFirstSlot == NULL) { -#if defined(JOEYLIB_PLATFORM_IIGS) - DisposeHandle(gCodegenArenaBaseHandle); - gCodegenArenaBaseHandle = NULL; -#else - free(gCodegenArenaBase); -#endif - gCodegenArenaBase = NULL; - gCodegenArenaBaseAddr = 0; - return false; - } - gTotalBytes = totalBytes; + gTotalBytes = 0; gUsedBytes = 0; - return true; +#if defined(JOEYLIB_PLATFORM_IIGS) + // 64 KB banks until the request is met or the machine runs out: a + // whole bank needs a completely free one, so a bank that is refused + // is retried at 60 KB (any page-aligned run inside a bank) before + // giving up. Whatever was granted is the arena. + want = totalBytes; + while (want > 0u && gBankCount < CODEGEN_ARENA_BANKS_MAX) { + uint32_t bytes = (want > CODEGEN_ARENA_IIGS_BANK) ? CODEGEN_ARENA_IIGS_BANK : want; + if (!bankAlloc(gBankCount, bytes)) { + if (bytes <= 60ul * 1024ul || !bankAlloc(gBankCount, 60ul * 1024ul)) { + break; + } + } + gTotalBytes += gBankSize[gBankCount]; + gBankCount++; + want = (want > gBankSize[gBankCount - 1u]) ? want - gBankSize[gBankCount - 1u] : 0u; + } +#else + want = totalBytes; + if (bankAlloc(0u, want)) { + gTotalBytes = want; + gBankCount = 1u; + } +#endif + return gBankCount != 0u; } void codegenArenaShutdown(void) { - ArenaSlotT *slot; - ArenaSlotT *next; + uint8_t bank; - if (gCodegenArenaBase == NULL) { - return; + for (bank = 0u; bank < gBankCount; bank++) { + bankFree(bank); } - for (slot = gFirstSlot; slot != NULL; slot = next) { - next = slot->next; - free(slot); - } -#if defined(JOEYLIB_PLATFORM_IIGS) - DisposeHandle(gCodegenArenaBaseHandle); - gCodegenArenaBaseHandle = NULL; -#else - free(gCodegenArenaBase); -#endif - gCodegenArenaBase = NULL; - gCodegenArenaBaseAddr = 0; - gFirstSlot = NULL; + gBankCount = 0u; gTotalBytes = 0; gUsedBytes = 0; } diff --git a/src/core/codegenArenaInternal.h b/src/core/codegenArenaInternal.h index c5592a0..6f33a39 100644 --- a/src/core/codegenArenaInternal.h +++ b/src/core/codegenArenaInternal.h @@ -3,13 +3,21 @@ // coalescing on free; manual compaction (codegenArenaCompact) // memmoves live slots down to consolidate free space. // +// The arena is one or more BANKS. Every port but the IIgs has a single +// bank of jlConfigT.codegenBytes from the C heap. On the IIgs a bank is +// a Memory Manager block that may not cross a 64 KB boundary (compiled +// routines are called by 24-bit JSL and run with PC inside one bank), so +// a request larger than 64 KB becomes several banks and a compiled cel +// lives wholly inside one of them. A slot's `offset` carries its bank in +// the top byte and its byte offset within the bank below. +// // jlSpriteT references slots via ArenaSlotT*, never raw function // pointers. jlSpriteDraw computes the call address each invocation as -// `gArenaBase + slot->offset + routineOffsets[shift][op]` so a +// `codegenArenaSlotAddr(slot) + routineOffsets[shift][op]` so a // compaction that moves a slot's bytes is transparent to callers. // -// Allocations go through plain malloc; on every supported port -// (IIgs, Amiga libnix, ST mintlib, DJGPP+CWSDPMI) the heap is +// Allocations go through plain malloc off the IIgs; on every supported +// port (Amiga libnix, ST mintlib, DJGPP+CWSDPMI, X68000) the heap is // readable, writable, AND executable. We are not running in a W^X // environment. // @@ -24,23 +32,30 @@ #include "joey/types.h" typedef struct ArenaSlotT { - uint32_t offset; // byte offset within the arena base + uint32_t offset; // bank << CODEGEN_ARENA_BANK_SHIFT | byte offset within the bank uint32_t size; // bytes occupied by this slot bool used; // true = held by a sprite; false = free - struct ArenaSlotT *next; // linked-list, sorted by offset + struct ArenaSlotT *next; // linked-list, sorted by offset, one list per bank struct ArenaSlotT *prev; } ArenaSlotT; +// Banks: at most this many, and the IIgs's bank is one 64 KB block (the +// Memory Manager refuses a no-cross block any larger). +#define CODEGEN_ARENA_BANKS_MAX 4u +#define CODEGEN_ARENA_BANK_SHIFT 24u +#define CODEGEN_ARENA_BANK_MASK 0x00FFFFFFul +#define CODEGEN_ARENA_IIGS_BANK 65536ul -// Lifecycle. Returns false if totalBytes is 0 or the underlying -// allocation fails. Idempotent: calling Init twice without an -// intervening Shutdown is a no-op. +// Lifecycle. Returns false if totalBytes is 0 or no bank at all could be +// allocated; a request the machine cannot fully satisfy yields as many +// banks as it can (codegenArenaBytesTotal tells). Idempotent: calling +// Init twice without an intervening Shutdown is a no-op. bool codegenArenaInit(uint32_t totalBytes); void codegenArenaShutdown(void); -// First-fit allocate `bytes` of executable memory. Returns NULL if -// no free slot is large enough -- the caller should run -// codegenArenaCompact and retry, or surface the failure. +// First-fit allocate `bytes` of executable memory, in the first bank with +// room. Returns NULL if no free slot is large enough -- the caller should +// run codegenArenaCompact and retry, or surface the failure. ArenaSlotT *codegenArenaAlloc(uint32_t bytes); // Mark the slot free and merge with adjacent free neighbors. The @@ -48,29 +63,23 @@ ArenaSlotT *codegenArenaAlloc(uint32_t bytes); // freed (if it coalesced into a neighbor). void codegenArenaFree(ArenaSlotT *slot); -// Walk live slots in offset order, memmove each down to fill any -// preceding hole, drop all free slots from the list, and finish with -// one trailing free slot covering the remaining space. Per-slot -// `offset` fields are updated atomically with the memmove so any -// callers indexing through `gArenaBase + slot->offset` see the new -// value on their next read. +// In every bank: walk live slots in offset order, memmove each down to +// fill any preceding hole, drop all free slots from the list, and finish +// with one trailing free slot covering the remaining space. Per-slot +// `offset` fields are updated atomically with the memmove so any callers +// resolving the slot see the new value on their next read. void codegenArenaCompact(void); -// Used for jlSpriteDraw's address computation. The base pointer is -// stable for the lifetime of the arena; only slot->offset moves. -// -// Direct extern access (instead of a getter function) so per-frame -// hot paths in jlSpriteCompile.c skip the JSL/PHB/RTL/PLB the wrapper -// would impose. Both globals are read-only after codegenArenaInit; -// the function-form getters below are kept as a back-compat shim. -extern uint8_t *gCodegenArenaBase; -extern uint32_t gCodegenArenaBaseAddr; -#define codegenArenaBase() ((uint8_t *)gCodegenArenaBase) -#define codegenArenaBaseAddr() ((uint32_t)gCodegenArenaBaseAddr) +// Where a slot's bytes are: as a pointer for the C side, and as the +// 24-bit absolute address the IIgs JSLs to. The bank tables are read-only +// after codegenArenaInit and exposed directly so the per-draw hot paths +// pay an indexed load, not a call. +extern uint8_t *gCodegenBankBase[CODEGEN_ARENA_BANKS_MAX]; +extern uint32_t gCodegenBankAddr[CODEGEN_ARENA_BANKS_MAX]; +#define codegenArenaSlotBase(slot) (gCodegenBankBase[(slot)->offset >> CODEGEN_ARENA_BANK_SHIFT] + ((slot)->offset & CODEGEN_ARENA_BANK_MASK)) +#define codegenArenaSlotAddr(slot) (gCodegenBankAddr[(slot)->offset >> CODEGEN_ARENA_BANK_SHIFT] + ((slot)->offset & CODEGEN_ARENA_BANK_MASK)) -// Public-API support: sum of live slot sizes, total arena size. -// Difference is free space (which may be fragmented across holes -// until codegenArenaCompact runs). +// Occupancy across all banks. uint32_t codegenArenaBytesUsed(void); uint32_t codegenArenaBytesTotal(void); diff --git a/src/core/init.c b/src/core/init.c index 358ed6b..4cae051 100644 --- a/src/core/init.c +++ b/src/core/init.c @@ -85,6 +85,11 @@ void *jlAlloc(uint32_t bytes) { } +void jlMemCopy(void *dst, const void *src, uint32_t bytes) { + jlpMemCopy(dst, src, bytes); +} + + void jlFree(void *p) { jlpBigFree(p); } diff --git a/src/core/music.c b/src/core/music.c index 530071f..c5f6411 100644 --- a/src/core/music.c +++ b/src/core/music.c @@ -238,10 +238,11 @@ void musicPump(void) { now = jlMillisElapsed(); if ((int32_t)(now - gMusicNextMs) < 0) { #if defined(JOEYLIB_PLATFORM_IIGS) - // The IIgs backends are ~80 ms one-shots retriggered per call - // (DOC oscillators have no gate to hold); re-assert the active - // voices every frame so held notes sustain. The other ports' - // tone/noise hardware holds a programmed voice on its own. + // The IIgs backends are finite one-shots (DOC oscillators have + // no gate to hold); re-assert the active voices every frame so + // held notes sustain -- the HAL restarts a voice only when its + // one-shot is about to end. The other ports' tone/noise + // hardware holds a programmed voice on its own. for (voice = 0; voice < MUSIC_VOICE_SLOTS; voice++) { if (gMusicOn[voice]) { musicApplyVoice(voice); @@ -254,9 +255,12 @@ void musicPump(void) { gMusicNextMs = now; } + // The event budget counts DOWN at the end of the body: as a `> 0u` + // guard llvm816 compiled it to `cmp #0 / inc a / bne`, and the inc + // clobbers the compare's flags (the same backend hole that broke + // Space Taxi's collision rows on the IIgs, 2026-09-21). eventsLeft = MUSIC_MAX_EVENTS_PER_PUMP; - while (gMusicPlaying && eventsLeft > 0u && (int32_t)(now - gMusicNextMs) >= 0) { - eventsLeft--; + while (gMusicPlaying && (int32_t)(now - gMusicNextMs) >= 0) { if (gMusicPos >= gMusicLength) { musicHalt(); return; @@ -304,5 +308,9 @@ void musicPump(void) { musicHalt(); // malformed opcode: hard stop return; } + eventsLeft--; + if (eventsLeft == 0u) { + break; + } } } diff --git a/src/core/palette.c b/src/core/palette.c index b78cbb0..651cc2f 100644 --- a/src/core/palette.c +++ b/src/core/palette.c @@ -121,6 +121,7 @@ void jlPaletteSet(jlSurfaceT *s, uint8_t paletteIndex, const uint16_t *colors16) row[14] = colors16[14]; row[15] = colors16[15]; if (s == gStage) { - gStagePaletteDirty = true; + gStagePaletteDirty = true; + gStagePaletteDirtyMask |= (uint16_t)(1u << paletteIndex); } } diff --git a/src/core/port.h b/src/core/port.h index 535a712..056358c 100644 --- a/src/core/port.h +++ b/src/core/port.h @@ -282,13 +282,24 @@ extern jlSurfaceT *gStage; #define jlpSpriteSavePlanes(_s, _x, _y, _w, _h, _dst) ((void)0) #define jlpSpriteRestorePlanes(_s, _x, _y, _w, _h, _src) ((void)0) -// frameCount: straight to the GetTick asm wrapper (peislam.s). The hal.c -// C wrapper this replaces was one of two pure forwarding layers between -// jlFrameCount and the Misc Toolset GetTick call -- ~60-80 cycles each on -// a clock that game loops (and the UBER bench loop) poll constantly. +// frameCount: straight to the heartbeat tick read in peislam.s (a VBL +// interrupt task keeps the count; the read is a same-bank word load). +// The hal.c C wrapper this replaces was a pure forwarding layer on a +// clock that game loops (and the UBER bench loop) poll constantly. extern uint16_t iigsGetTickWord(void); #define jlpFrameCount() iigsGetTickWord() +// jlMemCopy: MVN block moves, split at bank boundaries in hal.c. +void iigsMemCopy(void *dst, const void *src, uint32_t bytes); +#define jlpMemCopy(_d, _s, _n) iigsMemCopy((_d), (_s), (_n)) +// The one Memory Manager owner ID every IIgs allocation uses (hal.c). +uint16_t iigsMemUserId(void); + +// SFX streams go to NTPstreamsound as unsigned bytes with 0x00 forbidden +// (joey/audio.h JL_SFX_RAW_UNSIGNED8_NOZERO): a raw producer hands +// audio_full.c chunks it can stream without the per-byte conversion. +#define jlpAudioSfxRawFormat() 1u + // ===================================================================== // JL_HAS registry consistency checks (PERF-AUDIT #26). // @@ -767,6 +778,9 @@ uint16_t jlpGenericFrameHz(void); #endif uint16_t jlpGenericFrameCount(void); +#if !defined(jlpMemCopy) + #define jlpMemCopy(_d, _s, _n) memcpy((_d), (_s), (size_t)(_n)) +#endif #if !defined(jlpFrameCount) #if defined(JL_HAS_FRAME_COUNT) uint16_t jlpFrameCount(void); @@ -851,6 +865,27 @@ void jlpGenericAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *c #define jlpAudioPlaySfxStream(_a, _b, _c, _d) jlpGenericAudioPlaySfxStream((_a), (_b), (_c), (_d)) #endif #endif +// Raw-format stream (joey/audio.h jlAudioPlaySfxStreamRaw): a port whose +// stream bytes are not signed PCM overrides both; everyone else streams +// signed PCM, so raw IS the plain stream and the format is 0 (signed). +#if !defined(jlpAudioPlaySfxStreamRaw) + #if defined(JL_HAS_AUDIO_PLAY_SFX_STREAM_RAW) + void jlpAudioPlaySfxStreamRaw(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz); + #else + #define jlpAudioPlaySfxStreamRaw(_a, _b, _c, _d) jlpAudioPlaySfxStream((_a), (_b), (_c), (_d)) + #endif +#endif +#if !defined(jlpAudioSfxRawFormat) + #define jlpAudioSfxRawFormat() 0u +#endif +void jlpGenericAudioSetPan(uint8_t slot, uint8_t pan); +#if !defined(jlpAudioSetPan) + #if defined(JL_HAS_AUDIO_SET_PAN) + void jlpAudioSetPan(uint8_t slot, uint8_t pan); + #else + #define jlpAudioSetPan(_s, _p) jlpGenericAudioSetPan((_s), (_p)) + #endif +#endif void jlpGenericAudioStopSfx(uint8_t slot); #if !defined(jlpAudioStopSfx) #if defined(JL_HAS_AUDIO_STOP_SFX) diff --git a/src/core/spriteDispatch.h b/src/core/spriteDispatch.h index fb7fd4a..bffe0d0 100644 --- a/src/core/spriteDispatch.h +++ b/src/core/spriteDispatch.h @@ -55,8 +55,7 @@ static inline __attribute__((always_inline)) void spriteCompiledDraw(jlSurfaceT // sets up its own DBR/Y and restores DBR on exit. routeOffset is the // gate-derived DRAW offset for this shift (#29). dstRow0 = &dst->pixels[SURFACE_ROW_OFFSET(y) + ((uint16_t)x >> 1)]; - fnAddr = codegenArenaBaseAddr() - + sp->slot->offset + fnAddr = codegenArenaSlotAddr(sp->slot) + (uint32_t)routeOffset; drawFn = (void (*)(uint8_t *))fnAddr; drawFn(dstRow0); @@ -131,9 +130,8 @@ extern void iigsSpriteSaveStage(uint32_t fnAddr, uint16_t backupLo, uint16_t x, static inline __attribute__((always_inline)) void spriteCompiledDrawMark(const jlSpriteT *sp, int16_t x, int16_t y, uint16_t routeOffset) { uint32_t fnAddr; - fnAddr = codegenArenaBaseAddr() - + sp->slot->offset - + (uint32_t)routeOffset; + fnAddr = codegenArenaSlotAddr(sp->slot) + + (uint32_t)routeOffset; iigsSpriteDrawMark(fnAddr, (uint16_t)x, (uint16_t)y, sp->widthPx, sp->heightPx); } @@ -188,9 +186,8 @@ static inline __attribute__((always_inline)) void spriteCompiledSaveUnder(const cachedDst = (uint8_t *)sp->cachedDstBank + cacheIdx; cachedSrc = (uint8_t *)sp->cachedSrcBank + cacheIdx; - fnAddr = codegenArenaBaseAddr() - + sp->slot->offset - + (uint32_t)routeOffset; + fnAddr = codegenArenaSlotAddr(sp->slot) + + (uint32_t)routeOffset; // Patch the MVN bank operands only if the dst/src bank pair changed since // last call (the routine's only dynamic bytes). SAVE copies screen -> @@ -259,9 +256,8 @@ static inline __attribute__((always_inline)) void spriteCompiledSaveUnderStage(j cachedDst = (uint8_t *)sp->cachedDstBank + cacheIdx; cachedSrc = (uint8_t *)sp->cachedSrcBank + cacheIdx; - fnAddr = codegenArenaBaseAddr() - + sp->slot->offset - + (uint32_t)routeOffset; + fnAddr = codegenArenaSlotAddr(sp->slot) + + (uint32_t)routeOffset; if (*cachedDst != backupBank || *cachedSrc != screenBank) { patchMvnBanks((uint8_t *)fnAddr, heightPx, /*dst*/backupBank, /*src*/screenBank); @@ -301,9 +297,8 @@ static inline __attribute__((always_inline)) void spriteCompiledRestoreUnder(jlS cachedDst = (uint8_t *)sp->cachedDstBank + cacheIdx; cachedSrc = (uint8_t *)sp->cachedSrcBank + cacheIdx; - fnAddr = codegenArenaBaseAddr() - + sp->slot->offset - + (uint32_t)routeOffset; + fnAddr = codegenArenaSlotAddr(sp->slot) + + (uint32_t)routeOffset; // Patch the MVN bank operands only if they changed (the routine's only // dynamic bytes). RESTORE copies backup -> screen: dst = screen bank, @@ -344,9 +339,8 @@ static inline __attribute__((always_inline)) void spriteCompiledRestoreMark(jlSp SPLIT_POINTER(backup->bytes, &backupLo, &backupBank); screenBank = ((const uint8_t *)&gStage->pixels)[2]; - fnAddr = codegenArenaBaseAddr() - + sp->slot->offset - + (uint32_t)routeOffset; + fnAddr = codegenArenaSlotAddr(sp->slot) + + (uint32_t)routeOffset; cacheIdx = SPRITE_ROUTINE_INDEX(shift, SPRITE_OP_RESTORE); cachedDst = (uint8_t *)sp->cachedDstBank + cacheIdx; @@ -413,7 +407,7 @@ static inline __attribute__((always_inline)) void spriteCompiledDraw(jlSurfaceT return; } byteOff = (uint16_t)((uint16_t)y * JL_PLANAR_BYTES_PER_ROW + ((uint16_t)x >> 3)); - fn = (DrawFn)(codegenArenaBase() + sp->slot->offset + routeOffset); + fn = (DrawFn)(codegenArenaSlotBase(sp->slot) + routeOffset); fn(pd->planes[0] + byteOff, pd->planes[1] + byteOff, pd->planes[2] + byteOff, pd->planes[3] + byteOff); } @@ -448,7 +442,7 @@ static inline __attribute__((always_inline)) void spriteCompiledSaveUnder(const backup->height = sp->heightPx; backup->sizeBytes = (uint16_t)((uint16_t)(nGroups << 3) * sp->heightPx); - fn = (CopyFn)(codegenArenaBase() + sp->slot->offset + routeOffset); + fn = (CopyFn)(codegenArenaSlotBase(sp->slot) + routeOffset); fn(pd->planes[0] + byteOff, pd->planes[1] + byteOff, pd->planes[2] + byteOff, pd->planes[3] + byteOff, backup->bytes); } @@ -465,7 +459,7 @@ static inline __attribute__((always_inline)) void spriteCompiledRestoreUnder(jlS return; } byteOff = (uint16_t)((uint16_t)backup->y * JL_PLANAR_BYTES_PER_ROW + ((uint16_t)backup->x >> 3)); - fn = (CopyFn)(codegenArenaBase() + backup->sprite->slot->offset + routeOffset); + fn = (CopyFn)(codegenArenaSlotBase(backup->sprite->slot) + routeOffset); fn(pd->planes[0] + byteOff, pd->planes[1] + byteOff, pd->planes[2] + byteOff, pd->planes[3] + byteOff, backup->bytes); } @@ -516,7 +510,7 @@ static inline __attribute__((always_inline)) void spriteCompiledDraw(jlSurfaceT groupBase = pd->base + (uint16_t)y * ST_GROUP_BYTES_PER_ROW + (uint16_t)((uint16_t)x >> 4) * ST_BYTES_PER_PLANE_GROUP; - fn = (DrawFn)(codegenArenaBase() + sp->slot->offset + routeOffset); + fn = (DrawFn)(codegenArenaSlotBase(sp->slot) + routeOffset); fn(groupBase); } @@ -553,7 +547,7 @@ static inline __attribute__((always_inline)) void spriteCompiledSaveUnder(const backup->height = sp->heightPx; backup->sizeBytes = (uint16_t)((uint16_t)(nGroups << 3) * sp->heightPx); - fn = (CopyFn)(codegenArenaBase() + sp->slot->offset + routeOffset); + fn = (CopyFn)(codegenArenaSlotBase(sp->slot) + routeOffset); fn(rowStart, backup->bytes); } @@ -572,7 +566,7 @@ static inline __attribute__((always_inline)) void spriteCompiledRestoreUnder(jlS rowStart = pd->base + (uint16_t)backup->y * ST_GROUP_BYTES_PER_ROW + (uint16_t)((uint16_t)backup->x >> 4) * ST_BYTES_PER_PLANE_GROUP; - fn = (CopyFn)(codegenArenaBase() + backup->sprite->slot->offset + routeOffset); + fn = (CopyFn)(codegenArenaSlotBase(backup->sprite->slot) + routeOffset); fn(backup->bytes, rowStart); } @@ -584,7 +578,7 @@ static inline __attribute__((always_inline)) void spriteCompiledDraw(jlSurfaceT DrawFn fn; destRow = &dst->pixels[(uint16_t)y * SURFACE_BYTES_PER_ROW + ((uint16_t)x >> 1)]; - fn = (DrawFn)(codegenArenaBase() + sp->slot->offset + routeOffset); + fn = (DrawFn)(codegenArenaSlotBase(sp->slot) + routeOffset); fn(destRow); } @@ -615,7 +609,7 @@ static inline __attribute__((always_inline)) void spriteCompiledSaveUnder(const backup->height = heightPx; backup->sizeBytes = (uint16_t)(copyBytes * heightPx); - fn = (CopyFn)(codegenArenaBase() + sp->slot->offset + routeOffset); + fn = (CopyFn)(codegenArenaSlotBase(sp->slot) + routeOffset); fn(screenPtr, backup->bytes); } @@ -635,7 +629,7 @@ static inline __attribute__((always_inline)) void spriteCompiledRestoreUnder(jlS (void)shift; sp = backup->sprite; screenPtr = (uint8_t *)&dst->pixels[(uint16_t)backup->y * SURFACE_BYTES_PER_ROW + ((uint16_t)backup->x >> 1)]; - fn = (CopyFn)(codegenArenaBase() + sp->slot->offset + routeOffset); + fn = (CopyFn)(codegenArenaSlotBase(sp->slot) + routeOffset); fn(backup->bytes, screenPtr); } diff --git a/src/core/spriteInternal.h b/src/core/spriteInternal.h index 9deda1e..6936932 100644 --- a/src/core/spriteInternal.h +++ b/src/core/spriteInternal.h @@ -153,7 +153,7 @@ struct jlSpriteT { // Compiled-path state. slot==NULL means not yet compiled (or // compile failed); jlSpriteDraw falls back to the interpreter. // The fn-call address for (shift, op) is computed at draw time: - // (codegenArenaBase() + slot->offset + routineOffsets[shift][op]) + // (codegenArenaSlotAddr(slot) + routineOffsets[shift][op]) // so a codegenArenaCompact that moves the slot's bytes is // transparent to the caller. ArenaSlotT *slot; diff --git a/src/core/surface.c b/src/core/surface.c index 552ef3f..e2b3188 100644 --- a/src/core/surface.c +++ b/src/core/surface.c @@ -56,6 +56,7 @@ uint32_t gPresentScannedRows = 0; // code reads and clears the flags after uploading. bool gStageScbDirty = true; bool gStagePaletteDirty = true; +uint16_t gStagePaletteDirtyMask = 0xFFFFu; // ----- Internal helpers (alphabetical) ----- @@ -85,7 +86,8 @@ void jlSurfaceCopy(jlSurfaceT *dst, const jlSurfaceT *src) { // widens the pixel bands. Matches the jlScbSet / jlPaletteSet path. if (dst == gStage) { gStageScbDirty = true; - gStagePaletteDirty = true; + gStagePaletteDirty = true; + gStagePaletteDirtyMask = 0xFFFFu; } } @@ -242,7 +244,8 @@ bool jlSurfaceLoadFile(jlSurfaceT *dst, const char *path) { * them dirty so the next present re-uploads them. */ if (dst == gStage) { gStageScbDirty = true; - gStagePaletteDirty = true; + gStagePaletteDirty = true; + gStagePaletteDirtyMask = 0xFFFFu; } return true; } @@ -428,7 +431,8 @@ bool stageAlloc(void) { // (NATIVE-PERF Phase 2 R9 -- ports now trust these flags instead // of memcmp'ing cached SCB/palette mirrors every present). gStageScbDirty = true; - gStagePaletteDirty = true; + gStagePaletteDirty = true; + gStagePaletteDirtyMask = 0xFFFFu; return true; } diff --git a/src/core/surfaceInternal.h b/src/core/surfaceInternal.h index d954b56..6e98ad2 100644 --- a/src/core/surfaceInternal.h +++ b/src/core/surfaceInternal.h @@ -118,6 +118,11 @@ extern jlSurfaceT *gStage; // 712-byte memcmp pair the IIgs port used to run unconditionally. extern bool gStageScbDirty; extern bool gStagePaletteDirty; +// Companion to gStagePaletteDirty: bit N set = palette N changed since the +// last upload. A port that can upload per palette (IIgs: 32 bytes each, +// not 512) consults this; the bool stays the coarse "anything" flag the +// other ports read. Both are set together and cleared together. +extern uint16_t gStagePaletteDirtyMask; // Coarse dirty map: one byte per GROUP of 8 stage rows, non-zero when // any row in the group may be dirty. It is a conservative hint, never diff --git a/src/generic/genericPort.c b/src/generic/genericPort.c index e1a6cf5..e5000dc 100644 --- a/src/generic/genericPort.c +++ b/src/generic/genericPort.c @@ -134,6 +134,13 @@ void jlpGenericAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *c (void)rateHz; } +// Mono ports (DOS, ST) have nowhere to put a pan; the header says so. +void jlpGenericAudioSetPan(uint8_t slot, uint8_t pan) { + (void)slot; + (void)pan; +} + + void jlpGenericAudioStopSfx(uint8_t slot) { (void)slot; } diff --git a/src/iigs/audio_full.c b/src/iigs/audio_full.c index 5c5328f..e4554b0 100644 --- a/src/iigs/audio_full.c +++ b/src/iigs/audio_full.c @@ -14,14 +14,14 @@ #include // Memory Manager via . Handles/pointers are void* and the -// owner ID comes from MMStartUp(); _ownerid maps to MMStartUp() and -// _toolErr collapses to 0 so each `|| _toolErr != 0` guard reduces to the -// NULL-handle failure check. Memory Manager attribute bits are defined -// here (values from Apple's Memory Manager reference). +// owner ID is the one iigsMemUserId() (hal.c) fetched from MMStartUp once +// for the whole program; _toolErr collapses to 0 so each `|| _toolErr != 0` +// guard reduces to the NULL-handle failure check. Memory Manager attribute +// bits are defined here (values from Apple's Memory Manager reference). #include typedef void *Handle; typedef void *Pointer; -#define _ownerid (MMStartUp()) +#define _ownerid (iigsMemUserId()) #define _toolErr 0 #define attrPage 0x0004 #define attrNoCross 0x0010 @@ -44,9 +44,37 @@ typedef void *Pointer; #define NTP_BUFFER_BYTES (64L * 1024L) -// Ensoniq 5503 (DOC) I/O at $E1C03C..$E1C03E. NTP saves its preferred -// sound_control byte at $E100CA so callers that want DOC-register -// access don't have to know NTP's mode bits. +// Offsets into NTP's jump table at the start of the replayer image. +#define NTP_ENTRY_PREPARE 0 +#define NTP_ENTRY_PLAY 3 +#define NTP_ENTRY_STOP 6 +#define NTP_ENTRY_STREAMSOUND 24 + +// The silent module handed to NTPprepare when no song is playing (see +// ntpArm). One track, one never-triggered streamed instrument, one +// empty pattern -- the smallest file check_header/extract_header_info +// accept. Every count is 1 because NTP's parse loops are do-while: a +// count of 0 still reads one record. +#define NTP_SILENT_VERSION 2 +#define NTP_SILENT_CHANNEL 0 // DOC output channel 0 +#define NTP_SILENT_STREAM_PAGE 0xE0u // clear of the SFX pages at 0xC0 +#define NTP_SILENT_OSC 0x00u // %x00yyyyy: 1 oscillator, osc 0 +#define NTP_SILENT_INSTR_TYPE 0x08u // TYPE_STREAM_SINGLE: no DOC copy +#define NTP_SILENT_SAMPLE 256u +#define NTP_SILENT_PATTERN (4u * 64u) // 4 bytes * 1 track * 64 lines + +// Ensoniq 5503 (DOC) I/O at $E1C03C..$E1C03E. +// +// $E100CA is the SOUND TOOL SET's copy of the sound control register +// ($C03C), and its low nibble is the DOC's master volume. NTP only ever +// READS it -- `access_doc_registers` copies it into sound_control before +// touching the DOC -- so if nothing has started the Sound Tool Set the +// byte is whatever GS/OS left there. A zero low nibble means the DOC +// streams perfectly and outputs silence, which is exactly the bug this +// file shipped with: jlpAudioInit allocated, loaded and armed NTP but +// never called SoundStartUp, so Space Taxi filled DOC RAM at volume 0. +// (The old comment here claimed NTP "saves its preferred byte" at +// $E100CA. It does not write it at all.) #define DOC_SOUND_CONTROL ((volatile uint8_t *)0xE1C03CL) #define DOC_SOUND_DATA ((volatile uint8_t *)0xE1C03DL) #define DOC_SOUND_ADDRESS ((volatile uint8_t *)0xE1C03EL) @@ -56,12 +84,21 @@ typedef void *Pointer; // itself uses 0x03 (halt + IE) when it wants an osc stopped, so we // mirror that. #define DOC_OSC_CTRL_BASE 0xA0 +#define DOC_OSC_FREQ_LO 0x00 // DOC register bases, per oscillator +#define DOC_OSC_FREQ_HI 0x20 #define DOC_OSC_HALT_BITS 0x03 // SFX: one fixed-bank handle holding all slots' stream structures + // sample buffers. Per slot: 16-byte struct followed by 4080 bytes of // sample, total 4 KB. JOEY_AUDIO_SFX_SLOTS slots fit in 20 KB. -#define SFX_SLOT_BYTES (4L * 1024L) +// 8 KB per slot, not 4 KB, so a whole spoken phrase fits in ONE chunk. +// NTPstreamsound restarts from the buffer head on every trigger, so chunks +// cannot overlap: the next one can only go out after the current has played, +// and it is pumped from jlAudioFrameTick -- once per RENDERED FRAME. At 4 fps +// that seam is ~250 ms of silence. 4 KB slots capped a chunk at 614 ms while +// Space Taxi phrases run ~820 ms, so every phrase seamed. 8 KB gives 1.43 s +// at the 5 kHz speech rate, which covers them with room to spare. +#define SFX_SLOT_BYTES (8L * 1024L) #define SFX_BUFFER_BYTES (SFX_SLOT_BYTES * (long)JOEY_AUDIO_SFX_SLOTS) // NTP uses oscillators 0..N for music. Each SFX slot reserves 2 oscs @@ -71,19 +108,85 @@ typedef void *Pointer; // also gets its own 512-byte-aligned DOC RAM page so they don't share // the streaming buffer. #define SFX_BASE_OSC 14 -#define SFX_OSCS_PER_SLOT 2 +// 1 streaming/interrupt oscillator + 2 playing oscillators per slot, so a +// sound can be CENTRED: the DOC pans per oscillator, with no way to put one +// oscillator in both ears. 5 slots * 3 = oscs 14..28, clear of NTP's music +// oscillators (0..13) and its interrupt timer (31). +#define SFX_OSCS_PER_SLOT 3 +#define SFX_PLAYING_OSCS 2 #define SFX_BASE_DOC_PAGE 0xC0 // 512-aligned (bit 0 must be 0) #define SFX_DOC_PAGE_STEP 2 // 512 bytes per slot #define SFX_VOLUME 255 -#define SFX_CHANNEL_LEFT 0x00 // Upper 4 bits = output channel -// DOC freq formula: rate_Hz = freq * input_clk / (8 * num_oscs * 65536). -// NTP's max_tracks_available = 31 + 1 interrupt osc => 32 active oscs. -// IIgs DOC input clock = 7158000 Hz; (input_clk / 8 / 32) = 27961. -// Inverted: freq_word = rate * 65536 / 27961. Caps usable rate at -// ~27 kHz (above that freq_word overflows uint16). -#define IIGS_DOC_DIVISOR 27961UL -#define IIGS_MAX_SFX_RATE 27000U +// Oscillator control byte: bit 0 halt, bits 2:1 mode, bit 3 IE, bits 7:4 +// output channel. On the IIgs channel 0 is the RIGHT output and channel 1 is +// the LEFT one -- MAME wires the DOC as add_route(0,"rspeaker"), +// add_route(1,"lspeaker"), matching the hardware. This file used to define a +// single `SFX_CHANNEL_LEFT 0x00`, which is neither left nor centred: every +// sound came out of the right ear only. It goes unnoticed on a stock IIgs +// because its built-in output is mono and sums both channels. +// Mirrors jlAudioPanE (joey/audio.h); the HAL boundary passes a plain byte. +#define SFX_PAN_LEFT 0u +#define SFX_PAN_CENTER 1u +#define SFX_PAN_RIGHT 2u + +#define SFX_CTRL_RIGHT 0x00 +#define SFX_CTRL_LEFT 0x10 + +// The DOC's per-oscillator sample rate (see docFreqWord below for how a +// playback rate becomes a frequency word -- the 65536 in the textbook +// formula is NOT the right scale for how NTP programs our oscillators). NTP enables 32 oscillators +// (setup_interrupt writes $E1 = $3E, i.e. ($3E/2)+1 = 32). +// +// The ES5503 divides by (oscsEnabled + 2), NOT by oscsEnabled -- the +// chip interleaves two extra refresh slots. This constant used to be +// 7158000/8/32 = 27961, which dropped the +2, so every IIgs sound played +// 5.9% flat (very nearly a semitone). Correct value: +// 7159090 / 8 / (32 + 2) = 26320 +// Confirmed against GSSquared's ES5503, whose calculate_output_rate() is +// `(m_clock_rate / 8) / (m_oscsenabled + 2)`, and whose debug protocol +// reports rate=26320 with oscsenabled=32 on a live NTP-driven IIgs. +// +#define IIGS_DOC_DIVISOR 26320UL +#define IIGS_MAX_SFX_RATE 26000U // 1 sample byte per DOC tick, the ceiling + +// Accumulator units per SAMPLE BYTE. This is NOT 65536. +// +// The ES5503 forms the sample index as +// acc >> (resshifts[resolution] - wavetblsize), resshifts = {9,10,...,16} +// and NTPstreamsound programs the playing oscillator's size register to +// %001001 -- resolution 1, table size 1 (512 bytes) -- so the shift is +// 10 - 1 = 9 and the accumulator advances 512 units per byte. Its silent +// interrupt oscillator gets size register 0 (resolution 0, size 0), which +// is 9 - 0 = 9 as well, so one constant covers both. +// +// This file used to invert the textbook "rate = freq * clk / (8*oscs*65536)" +// formula, i.e. assume 65536 units per byte. That is the value for +// resolution 7 / size 0, not for what NTP actually programs, so every SFX +// ran 65536/512 = 128x too fast: a 34 ms speech chunk played as a 0.3 ms +// click. Measured on a live IIgs -- osc 15 with freq=12449, resolution=1, +// wavetblsize=1 advanced 12449/512 = 24 bytes per DOC tick instead of the +// intended 0.19, and Space Taxi's speech came out as "horrible clicking". +// Verified against both emulators' ES5503 (gssquared ensoniq.cpp and MAME +// es5503.cpp carry the identical resshifts/accmasks tables). +#define DOC_ACC_PER_BYTE 512UL + + +// DOC frequency word for a playback rate in Hz, for an oscillator +// programmed the way NTPstreamsound programs ours. Rounds to nearest and +// never returns 0, which would freeze the oscillator on one sample. +static uint16_t docFreqWord(uint16_t rateHz) { + uint32_t w = (((uint32_t)rateHz * DOC_ACC_PER_BYTE) + (IIGS_DOC_DIVISOR / 2u)) + / IIGS_DOC_DIVISOR; + + if (w == 0u) { + w = 1u; + } + if (w > 0xFFFFu) { + w = 0xFFFFu; + } + return (uint16_t)w; +} // PSG-style tone voices: each voice's SFX slot holds a RESIDENT // full-amplitude square wave (whole periods so retriggers seam), and @@ -93,11 +196,23 @@ typedef void *Pointer; // stored in DOC-unsigned form; the low level is 0x01 because a 0x00 // sample byte is the DOC's wave-end marker and would halt the osc. #define TONE_PERIOD_BYTES 8u // samples per square period (4 hi + 4 lo) -#define TONE_SQUARE_BYTES 2040u // 255 whole periods, fits the 4080-byte slot +#define TONE_SQUARE_BYTES 4080u // 510 whole periods: the whole sample area #define TONE_LEVEL_HI 0xFFu #define TONE_LEVEL_LO 0x01u -#define TONE_MAX_HZ 3495u // freq word ceiling: 3495 * 8 * 65536 / 27961 = 65533 -#define TONE_SUSTAIN_MUL 41u // len = freqHz * 41 / 64 bytes -> ~80 ms sustain +// Pitch ceiling: playback rate is toneHz * TONE_PERIOD_BYTES and must stay +// at or below one sample byte per DOC tick, so toneHz <= 26320/8 = 3290. +#define TONE_MAX_HZ 3290u +// Sustain: one trigger plays len = freqHz * 256 / 64 = freqHz * 4 bytes, +// which at the playback rate of freqHz * 8 is 500 ms of tone (capped by +// TONE_SQUARE_BYTES above ~1020 Hz: 4080 / (freqHz * 8) s). The tracker +// re-asserts a held voice every pump, and jlpAudioVoice only restarts +// the sample when the running one is within TONE_HOLD_MARGIN_TICKS of +// its end -- an identical call inside that window is a no-op and a +// pitch change is a live retune. The margin covers the longest gap +// between pumps (one rendered frame, p90 ~6 ticks); a restart inside +// the margin still seams because len is whole periods. +#define TONE_SUSTAIN_MUL 256u +#define TONE_HOLD_MARGIN_TICKS 8u #define AGI_ATTEN_OFF 15u // AGI attenuation: 0 = loudest .. 15 = silent #define IIGS_FRAME_HZ 60U @@ -107,7 +222,7 @@ typedef void *Pointer; // 26000..2750 Hz; the rate ceiling must stay below IIGS_DOC_DIVISOR // (27961) or the freq word wraps uint16 into a near-zero growl. #define NOISE_SLOT 3u -#define NOISE_BYTES 4080u // whole sample area: SFX_SLOT_BYTES - SFX_SAMPLE_OFFSET +#define NOISE_BYTES 4080u // LFSR noise run; well inside the sample area #define NOISE_LEVEL_HI 0xFFu #define NOISE_LEVEL_LO 0x01u #define NOISE_RATE_MAX 26000u @@ -115,6 +230,54 @@ typedef void *Pointer; #define NOISE_SUSTAIN_MUL 41u // len = rate * 41 / 512 -> ~80 ms sustain #define NOISE_LFSR_SEED 0xACE1u #define NOISE_LFSR_TAPS 0xB400u // 16-bit maximal Galois LFSR, period 65535 +// How long one noise burst sounds, in 60 Hz GetTick units. +// jlpAudioNoise plays NOISE_SUSTAIN_MUL/512 of a second of sample +// (41/512 s = 80 ms = 4.8 ticks). 4 retriggers a shade early, which +// keeps the sound gapless. +#define NOISE_HOLD_TICKS 4u + +// ----- Streaming SFX (jlpAudioPlaySfxStream) ----- +// +// The shared overlay mixer in src/core/audioSfxMix.c is DOS/ST/X68000 +// only -- it needs 32-bit ints and says so -- so streaming here is built +// on NTPstreamsound instead, which is what the IIgs SFX path already +// uses. One chunk at a time: pull up to SFX_SAMPLE_BYTES from the +// caller's fill(), hand it to NTP, and schedule the next pull for when +// that chunk finishes. +// +// NTPstreamsound restarts from the buffer head on every trigger and has +// no queue, so chunks cannot overlap; the next one goes out when the +// previous has played. The seam therefore carries up to one frame of +// jitter. Chunks are as large as the slot allows to make seams as rare +// as possible -- at 5 kHz SFX_STREAM_BYTES is 0.7 s, and anything +// shorter than that (every Space Taxi speech sample) plays as a single +// chunk with no seam at all. +#define SFX_SAMPLE_BYTES (SFX_SLOT_BYTES - SFX_SAMPLE_OFFSET) +// NTP streams through DOC RAM one 512-byte page at a time +// (streamsound's 'we're filling both stream pages' copy). +#define SFX_STREAM_PAGE 512u +// The most a stream chunk may be. NTP's stream_copy is an unrolled +// 256-byte block move that runs to completion whatever the sample +// length says, so a chunk that is not a whole number of pages makes the +// last copy read past the slot -- straight into the NEXT slot's stream +// struct, whose bytes are addresses and lengths that can easily be 0x00 +// and would halt the voice early. SFX_SAMPLE_BYTES (4080) is not a +// multiple of 512; this rounds it down to the 3584 that is. +// NTPstreamsound eats 512 bytes before it plays anything: streamsound does +// two stream_copy calls up front (stream_instr_ptr = start + 512), and every +// refill interrupt then stops the voice as soon as that pointer reaches +// start + length. So a buffer declared as L bytes goes SILENT at sample +// L - 512 -- the last 512 samples never sound. Measured: a 512-byte chunk +// played 50 ms, i.e. it stopped at the first refill interrupt (256 samples), +// not the 102 ms its length implied. +// +// So every chunk carries a 512-byte tail of DOC silence and declares +// got + SFX_STREAM_TAIL to NTP. The cut then lands inside the tail, all +// `got` real samples sound, and the refill deadline computed from `got` is +// finally honest -- that mismatch is what put dead air between spoken words. +#define SFX_STREAM_TAIL SFX_STREAM_PAGE +// Largest whole number of pages leaving room for the tail inside the slot. +#define SFX_STREAM_BYTES (((SFX_SAMPLE_BYTES - SFX_STREAM_TAIL) / SFX_STREAM_PAGE) * SFX_STREAM_PAGE) // ----- Module state ----- @@ -124,8 +287,9 @@ static Handle gModuleHandle = NULL; static uint32_t gModuleBase = 0; static Handle gSfxHandle = NULL; static uint32_t gSfxBase = 0; -static bool gNTPReady = false; -static bool gNTPPlaying = false; +static bool gNTPReady = false; // binary loaded, buffers allocated +static bool gNTPArmed = false; // NTPprepare done: DOC IRQ installed +static bool gNTPPlaying = false; // NTPplay done: music is ticking // Per-slot config cache. jlpAudioPlaySfx's biggest cost is the // per-byte XOR-with-$80 loop over the entire sample (signed -> DOC's @@ -156,7 +320,37 @@ static const uint8_t gNoiseMarker = 0; // noise; rotating the start offset (stride co-prime with the buffer) // decorrelates the retriggers. Max start 1023 + max len 2082 = 3105, // inside the 4080-byte area. +// Per-slot stereo placement (jlAudioSetPan). Index is AUDIO_PAN_*; the DOC +// pans per oscillator, so a centred slot sounds both of its playing +// oscillators and a panned one mutes the wrong side. +static uint8_t gSlotPan[JOEY_AUDIO_SFX_SLOTS] = { + SFX_PAN_CENTER, SFX_PAN_CENTER, SFX_PAN_CENTER, SFX_PAN_CENTER, SFX_PAN_CENTER +}; + static uint16_t gNoisePhase = 0; +// GetTick deadline for the burst currently sounding on the noise slot. +static uint16_t gNoiseHoldUntil = 0; +// Volume the sounding burst was started at; a change forces a restart +// rather than a frequency-only update. +static uint8_t gNoiseVol = 0; +// The same pair per tone voice: the GetTick by which the running +// square must be restarted, and the volume it was started at. +static uint16_t gToneHoldUntil[JOEY_AUDIO_VOICES] = { 0 }; +static uint8_t gToneVol[JOEY_AUDIO_VOICES] = { 0 }; + +// Per-slot streaming state. `holdUntil` is a GetTick deadline: the tick +// by which the chunk now playing will have finished. +typedef struct { + jlAudioStreamFillT fill; + void *ctx; + uint16_t rateHz; + uint16_t holdUntil; + bool active; + bool raw; // fill delivers DOC-ready unsigned bytes + uint16_t paddedGot; // chunk length whose silence tail is already in place +} StreamSlotT; + +static StreamSlotT gStream[JOEY_AUDIO_SFX_SLOTS]; // SFX handle layout: stream structure first, sample bytes after. // Both end up at known 24-bit addresses, side-stepping the small @@ -171,22 +365,25 @@ static uint16_t gNoisePhase = 0; // 12 : playing-osc count (1..30; total oscs used = 1 + count) // 13 : volume of playing osc 0 (0..255) // 14 : channel of playing osc 0 (upper 4 bits) -#define SFX_STRUCT_BYTES 16 +// NTPstreamsound reads a volume+control PAIR per playing oscillator, at +// struct[13],[14] then struct[15],[16] (its streamsound_copy_indiv steps x by +// 2 per oscillator). Two playing oscillators therefore need 17 bytes; round +// to 20 to keep the sample area even. +#define SFX_STRUCT_BYTES 20 #define SFX_SAMPLE_OFFSET SFX_STRUCT_BYTES -// Self-modifying call stub. Bakes the X/Y/A register loads AND the -// JSL target into the buffer, so the C-side inline asm only needs -// `jsl gCallStub` followed by `rep #0x30` -- no global-operand -// references in the asm block. +// Self-modifying call stub, executed by ntpCall. Bakes the X/Y/A +// register loads AND the JSL target into the buffer, which keeps the +// inline asm block free of absolute-global operands entirely -- all it +// contains is `jsl gCallStub`. // -// The trailing REP restores M=16/X=16 for the function epilogue: the -// byte writes the call sites do to patch this stub leave M=8, and -// NTP's RTL leaves M/X in whatever state NTP chose. Without REP, the -// C epilogue's ADC/LDX widths decode wrong and the function returns -// to garbage. See project_iigs_inline_asm_mode.md. -// -// Folding the X/Y/A loads and the JSL target into the stub keeps the -// inline asm block free of absolute-global operands entirely. +// The asm block declares "a", "x", "y" clobbers: the stub loads all +// three and NTP is free to trash them, so a caller value the compiler +// kept live across the block would come back corrupted. This went +// unnoticed for a long time because NTP fails to allocate on a +// many-segment build (gNTPReady stays 0), so jlpAudioNoise +// early-returns and the stub is NEVER executed -- shipped IIgs builds +// simply had no audio. It only bites once the allocation succeeds. // // Layout (15 bytes): // 00: C2 30 REP #$30 ; 16-bit M/X @@ -195,14 +392,36 @@ static uint16_t gNoisePhase = 0; // 08: A9 lo hi LDA #A // 0B: 22 lo hi bk JSL target // 0F: 6B RTL -// GLOBAL (not static): referenced by name from the inline `jsl gCallStub` -// blocks below; link816 resolves .text relocations only against global -// symbols, so a file-local buffer links as "unresolved gCallStub". +// GLOBAL (not static): referenced by name from ntpCall's inline +// `jsl gCallStub`; link816 resolves .text relocations only against +// global symbols, so a file-local buffer links as "unresolved +// gCallStub". unsigned char gCallStub[16]; +// The VBL heartbeat tick (peislam.s), 60 Hz NTSC / 50 Hz PAL. Declared +// here rather than shared because only the holds below need it. +extern uint16_t iigsGetTickWord(void); + // ----- Internal helpers ----- +static void buildCallStub(uint32_t target, uint16_t x, uint16_t y, uint16_t a); +static uint16_t docFreqWord(uint16_t rateHz); +static bool loadNTP(void); +static void noiseEnsure(uint8_t slot, uint32_t slotBase); +static void ntpArm(void); +static void ntpCall(uint32_t entry, uint16_t x, uint16_t y, uint16_t a); +static void ntpDisarm(void); +static void ntpSilentModule(uint32_t base); +static void sfxApplyPan(unsigned char *sfx, uint8_t slot, uint8_t vol); +static void sfxCopyUnsigned(unsigned char *dst, const uint8_t *src, uint32_t length); +static void sfxFillStruct(uint8_t slot, uint32_t slotBase, uint32_t length, uint16_t rateHz); +static void sfxTrigger(uint32_t structAddr); +static bool streamChunk(uint8_t slot); +static void streamStart(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz, bool raw); +static void toneSquareEnsure(uint8_t slot, uint32_t slotBase); + + static void buildCallStub(uint32_t target, uint16_t x, uint16_t y, uint16_t a) { gCallStub[0] = 0xC2; gCallStub[1] = 0x30; @@ -222,6 +441,35 @@ static void buildCallStub(uint32_t target, uint16_t x, uint16_t y, uint16_t a) { gCallStub[15] = 0x6B; } +// Retune the noise slot's oscillator pair without restarting the sample. +// +// This is what the C64 does: level H's door sweep rewrites SID voice 2's +// frequency every tick with the gate already on, so the voice runs +// continuously and only the pitch moves. Going through NTPstreamsound +// instead would re-copy 512 bytes into DOC RAM and re-prime the +// oscillators every tick -- the most expensive thing on that screen. +// +// The registers and the oscillator pair mirror what NTP's own +// streamsound sets them from: struct bytes 8/9 into $00+osc and $20+osc, +// written for osc and osc+1 (its streamsound_copy runs +// streamsound_osc_count + 1 = 2 iterations). SEI/CLI for the same reason +// jlpAudioStopSfx uses it -- NTP's DOC IRQ must not be midway through +// its own sound_address write. +static void docRetuneOscPair(uint8_t firstOsc, uint16_t freqWord) { + uint8_t i; + + __asm__ volatile ("sei" ::: "memory"); + *DOC_SOUND_CONTROL = *DOC_IRQ_VOLUME; + for (i = 0; i < SFX_OSCS_PER_SLOT; i++) { + *DOC_SOUND_ADDRESS = (uint8_t)(DOC_OSC_FREQ_LO + firstOsc + i); + *DOC_SOUND_DATA = (uint8_t)(freqWord & 0xFFu); + *DOC_SOUND_ADDRESS = (uint8_t)(DOC_OSC_FREQ_HI + firstOsc + i); + *DOC_SOUND_DATA = (uint8_t)((freqWord >> 8) & 0xFFu); + } + __asm__ volatile ("cli" ::: "memory"); +} + + static bool loadNTP(void) { Handle h; Pointer p; @@ -297,8 +545,9 @@ static void noiseEnsure(uint8_t slot, uint32_t slotBase) { sfx[3] = 0; sfx[10] = (unsigned char)(SFX_BASE_DOC_PAGE + slot * SFX_DOC_PAGE_STEP); sfx[11] = (unsigned char)(SFX_BASE_OSC + slot * SFX_OSCS_PER_SLOT); - sfx[12] = 1; // one playing osc - sfx[14] = SFX_CHANNEL_LEFT; + sfx[12] = SFX_PLAYING_OSCS; + sfx[14] = SFX_CTRL_RIGHT; + sfx[16] = SFX_CTRL_LEFT; gSfxSlotSample[slot] = &gNoiseMarker; gSfxSlotLength[slot] = 0; @@ -306,19 +555,228 @@ static void noiseEnsure(uint8_t slot, uint32_t slotBase) { } +// Put NTP's DOC interrupt in place without starting any music. +// +// SFX streaming is not self-contained: NTPstreamsound only primes the +// oscillators, and NTP's own DOC IRQ (interrupt_handler, job 10 in +// oscillator_job_tbl) does the refilling. That IRQ exists only after +// NTPprepare, which is what installs the vector at $E1002C and starts +// the free-running interrupt oscillator 31. +// +// NTPplay is NOT called: it only sets NTP's `playing` byte, and +// tracker_interrupt returns immediately while that byte is 0. So a +// prepared-but-not-played replayer is a running interrupt with no +// music -- exactly what an SFX-only application wants. jlpAudioInit +// used to stop after loading the binary, leaving no IRQ at all, so +// every SFX reached the DOC once and was never refilled. +static void ntpArm(void) { + if (!gNTPReady || gNTPArmed) { + return; + } + ntpSilentModule(gModuleBase); + ntpCall(gNTPBase + NTP_ENTRY_PREPARE, + (uint16_t)(gModuleBase & 0xFFFFu), + (uint16_t)((gModuleBase >> 16) & 0xFFu), + 0); + gNTPArmed = true; +} + + +// Call an NTP entry point through gCallStub. +// +// Every site brackets the JSL with PHP/SEI ... PLP and saves DBR and +// DP. NTP is an interrupt-driven third-party replayer: it may return +// with M/X, DBR or D changed, and its own DOC IRQ must not fire while +// we are inside its non-reentrant entry points (the same reason +// jlpAudioStopSfx wraps its DOC pokes in SEI/CLI). PLP restores the +// saved P, which puts M=16/X=16 back for the C epilogue and restores +// the caller's interrupt state rather than unconditionally enabling +// interrupts. See project_iigs_inline_asm_mode.md. +static void ntpCall(uint32_t entry, uint16_t x, uint16_t y, uint16_t a) { + buildCallStub(entry, x, y, a); + __asm__ volatile ( + "php\n\t" + "sei\n\t" + "phb\n\t" + "phd\n\t" + "jsl gCallStub\n\t" + "pld\n\t" + "plb\n\t" + "plp" + ::: "a", "x", "y", "memory" + ); +} + + +// Tear the replayer down: NTPstop halts the interrupt oscillator AND +// restores the pre-NTP sound interrupt vector it saved in +// backup_interrupt_ptr. That is why a stop always has to be paired +// with a fresh ntpArm if SFX are still wanted -- see jlpAudioStopMod. +// Pairing them also keeps backup_interrupt_ptr honest: NTPprepare +// overwrites it with whatever is installed at the time, so preparing +// twice in a row would make NTP "restore" its own handler and lose the +// system vector for good. +static void ntpDisarm(void) { + if (!gNTPArmed) { + return; + } + ntpCall(gNTPBase + NTP_ENTRY_STOP, 0, 0, 0); + gNTPArmed = false; + gNTPPlaying = false; +} + + +// Build the silent module described at NTP_SILENT_VERSION into the +// module handle. Layout is the v2 writer's (ntpconverter_lib.php +// save_ntp) as read back by extract_header_info. +static void ntpSilentModule(uint32_t base) { + unsigned char *m = (unsigned char *)base; + uint16_t i = 0; + uint16_t n; + + m[i++] = 'n'; + m[i++] = 'f'; + m[i++] = 'c'; + m[i++] = '!'; + m[i++] = NTP_SILENT_VERSION; + m[i++] = 1; // tracks + m[i++] = 1; // instruments + m[i++] = 1; // patterns + m[i++] = 1; // pattern-order length + m[i++] = 0; // song-name length + + // Track 0. + m[i++] = NTP_SILENT_CHANNEL; + m[i++] = NTP_SILENT_STREAM_PAGE; + m[i++] = NTP_SILENT_OSC; + + // Instrument 0. The streamed type makes copy_instruments_to_doc + // skip the DOC copy, so no DOC page is needed and nothing of ours + // (the SFX slots live at DOC page 0xC0 and up) gets overwritten. + m[i++] = NTP_SILENT_INSTR_TYPE; + m[i++] = (unsigned char)(NTP_SILENT_SAMPLE & 0xFFu); + m[i++] = (unsigned char)((NTP_SILENT_SAMPLE >> 8) & 0xFFu); + m[i++] = 0; // length, byte 2 (24-bit) + m[i++] = 0; // repeat pointer + m[i++] = 0; + m[i++] = 0; + m[i++] = 0; // repeat length + m[i++] = 0; + m[i++] = 0; + m[i++] = 0; // volume + m[i++] = 0; // finetune + m[i++] = 0; // DOC page (unused: streamed) + m[i++] = 0; // DOC size (unused: streamed) + m[i++] = 0; // instrument-name length + + m[i++] = 0; // pattern order: pattern 0 + + for (n = 0; n < NTP_SILENT_PATTERN; n++) { + m[i++] = 0; // an empty pattern line + } + for (n = 0; n < NTP_SILENT_SAMPLE; n++) { + m[i++] = 0x80; // DOC silence, never played + } +} + + +// Write the volume pair for a slot's two playing oscillators, honouring its +// pan. Oscillator 0 is on DOC channel 0 (right), oscillator 1 on channel 1 +// (left); silencing one is how a single-oscillator-per-side chip pans. +static void sfxApplyPan(unsigned char *sfx, uint8_t slot, uint8_t vol) { + uint8_t pan = (slot < JOEY_AUDIO_SFX_SLOTS) ? gSlotPan[slot] : 0u; + + sfx[13] = (pan == SFX_PAN_LEFT) ? 0u : vol; // right oscillator + sfx[15] = (pan == SFX_PAN_RIGHT) ? 0u : vol; // left oscillator +} + + +// Stage a caller's sample into a slot for NTPstreamsound: convert signed +// 8-bit PCM (the public jlAudioPlaySfx/Stream contract) to the DOC's +// unsigned format, then pad the tail out to NTP's 512-byte page. +// +// Two separate 0x00 hazards, both of which halt the oscillator dead -- +// 0x00 in DOC RAM is the ES5503's wave-end byte, and the chip honours it +// even in free-run mode: +// - the sign flip alone turns a perfectly legal -128 sample into 0x00, +// so it clamps to -127. One level out of 256, and every caller's +// worst-case waveform stays audible. +// - NTP copies whole 512-byte pages into DOC RAM whatever length we +// declare, so the bytes past a short sample get played too. They are +// whatever the slot held before, very often zeros. 0x80 is unsigned +// silence and plays harmlessly if it is ever reached. +// Safe in place (dst == src), which is how streamChunk uses it. +static void sfxCopyUnsigned(unsigned char *dst, const uint8_t *src, uint32_t length) { + uint32_t padTo; + uint32_t i; + + for (i = 0; i < length; i++) { + unsigned char v = (unsigned char)(src[i] ^ 0x80u); + + if (v == 0u) { + v = 1u; + } + dst[i] = v; + } + + padTo = (length + SFX_STREAM_PAGE - 1u) & ~(SFX_STREAM_PAGE - 1u); + if (padTo > (uint32_t)SFX_SAMPLE_BYTES) { + padTo = (uint32_t)SFX_SAMPLE_BYTES; + } + for (i = length; i < padTo; i++) { + dst[i] = 0x80u; + } +} + + +// Build the 16-byte NTPstreamsound stream structure at the head of a +// slot. Shared by the fixed-buffer path (jlpAudioPlaySfx) and the +// streaming one (streamChunk), which differ only in where the sample +// bytes came from. +static void sfxFillStruct(uint8_t slot, uint32_t slotBase, uint32_t length, uint16_t rateHz) { + unsigned char *sfx = (unsigned char *)slotBase; + uint32_t sampleAddr = slotBase + SFX_SAMPLE_OFFSET; + uint16_t rate = (rateHz > IIGS_MAX_SFX_RATE) ? IIGS_MAX_SFX_RATE : rateHz; + uint16_t freqWord = docFreqWord(rate); + + sfx[0] = (unsigned char)(sampleAddr & 0xFFu); + sfx[1] = (unsigned char)((sampleAddr >> 8) & 0xFFu); + sfx[2] = (unsigned char)((sampleAddr >> 16) & 0xFFu); + sfx[3] = 0; + sfx[4] = (unsigned char)(length & 0xFFu); + sfx[5] = (unsigned char)((length >> 8) & 0xFFu); + sfx[6] = (unsigned char)((length >> 16) & 0xFFu); + sfx[7] = (unsigned char)((length >> 24) & 0xFFu); + sfx[8] = (unsigned char)(freqWord & 0xFFu); + sfx[9] = (unsigned char)((freqWord >> 8) & 0xFFu); + sfx[10] = (unsigned char)(SFX_BASE_DOC_PAGE + slot * SFX_DOC_PAGE_STEP); + sfx[11] = (unsigned char)(SFX_BASE_OSC + slot * SFX_OSCS_PER_SLOT); + sfx[12] = SFX_PLAYING_OSCS; + sfx[14] = SFX_CTRL_RIGHT; // playing osc 0: DOC channel 0 + sfx[16] = SFX_CTRL_LEFT; // playing osc 1: DOC channel 1, same sample + sfxApplyPan(sfx, slot, SFX_VOLUME); +} + + // Fire NTPstreamsound on a slot's stream structure. structAddr is the // 24-bit address of the 16-byte struct; NTP takes it low 16 in X, bank // in Y (same packing as NTPprepare). +// NTPstreamsound streams a sample through DOC RAM and relies on NTP's +// own DOC IRQ to keep refilling it -- the NTP source describes it as +// playing SFX "alongside music". That IRQ exists only once the replayer +// has been prepared, so the guard here is gNTPArmed. Calling in before +// then executes NTP entry code against uninitialised state and takes +// the machine down; guarding on gNTPPlaying instead would be safe but +// would silence every SFX in an application that plays no music, which +// is all of them today. static void sfxTrigger(uint32_t structAddr) { - buildCallStub(gNTPBase + 24, - (uint16_t)(structAddr & 0xFFFFu), - (uint16_t)((structAddr >> 16) & 0xFFu), - 0); - __asm__ volatile ( - "jsl gCallStub\n\t" - "rep #0x30" - ::: "memory" - ); + if (!gNTPArmed) { + return; + } + ntpCall(gNTPBase + NTP_ENTRY_STREAMSOUND, + (uint16_t)(structAddr & 0xFFFFu), + (uint16_t)((structAddr >> 16) & 0xFFu), + 0); } @@ -364,12 +822,14 @@ static void toneSquareEnsure(uint8_t slot, uint32_t slotBase) { sfx[3] = 0; sfx[10] = (unsigned char)(SFX_BASE_DOC_PAGE + slot * SFX_DOC_PAGE_STEP); sfx[11] = (unsigned char)(SFX_BASE_OSC + slot * SFX_OSCS_PER_SLOT); - sfx[12] = 1; // one playing osc - sfx[14] = SFX_CHANNEL_LEFT; + sfx[12] = SFX_PLAYING_OSCS; + sfx[14] = SFX_CTRL_RIGHT; + sfx[16] = SFX_CTRL_LEFT; gSfxSlotSample[slot] = &gToneMarker; gSfxSlotLength[slot] = 0; gSfxSlotRateHz[slot] = 0; + gToneHoldUntil[slot] = iigsGetTickWord(); // nothing of ours is sounding } @@ -382,7 +842,14 @@ bool jlpAudioInit(void) { if (gNTPReady) { return true; } + // Bring up the Sound Tool Set FIRST. It owns the DOC master volume + // in $E100CA (see above) and the sound interrupt vector at $E1002C + // that NTPprepare installs into, so NTP is written expecting it to + // be running. The tool set reference-counts startups, so this is + // cheap if the Finder already brought it up. + SoundStartUp(_ownerid); if (!loadNTP()) { + SoundShutDown(); return false; } // A second fixed-bank handle holds the per-call .NTP module bytes. @@ -394,6 +861,7 @@ bool jlpAudioInit(void) { if (modHandle == NULL || _toolErr != 0) { DisposeHandle(gNTPHandle); gNTPHandle = NULL; + SoundShutDown(); return false; } HLock(modHandle); @@ -411,6 +879,7 @@ bool jlpAudioInit(void) { DisposeHandle(gNTPHandle); gModuleHandle = NULL; gNTPHandle = NULL; + SoundShutDown(); return false; } HLock(sfxHandle); @@ -418,6 +887,22 @@ bool jlpAudioInit(void) { gSfxBase = (uint32_t)*(void **)sfxHandle; gNTPReady = true; + + // Lay the tone voices' square waves into their slots now: the 4 KB + // fill is ~30 ms of far-pointer stores, which used to land on the + // first note of a ride (Space Taxi's first thrust) as a visible hitch. + { + uint8_t voice; + + for (voice = 0u; voice < JOEY_AUDIO_VOICES; voice++) { + toneSquareEnsure(voice, gSfxBase + (uint32_t)voice * (uint32_t)SFX_SLOT_BYTES); + } + } + + // Install NTP's DOC interrupt now. SFX-only applications never call + // jlpAudioPlayMod, and without this there is no interrupt to refill + // a streamed sample. + ntpArm(); return true; } @@ -426,9 +911,11 @@ void jlpAudioShutdown(void) { if (!gNTPReady) { return; } - if (gNTPPlaying) { - jlpAudioStopMod(); - } + // Stop the music and hand the sound interrupt vector back to the + // system. Unlike jlpAudioStopMod this does not re-arm: the handles + // NTP's interrupt reads from are about to be disposed. + ntpDisarm(); + // Silence every SFX slot before disposing the handles. NTP's DOC // IRQ vector points into the buffer we are about to free; if any // oscillator finishes its sample after the dispose, the wave-done @@ -459,6 +946,7 @@ void jlpAudioShutdown(void) { gNTPHandle = NULL; gNTPBase = 0; } + SoundShutDown(); gNTPReady = false; } @@ -486,15 +974,17 @@ void jlpAudioNoise(uint8_t pitch, uint8_t atten) { } if (atten >= AGI_ATTEN_OFF) { jlpAudioStopSfx((uint8_t)NOISE_SLOT); + gNoiseHoldUntil = iigsGetTickWord(); // next call may retrigger return; } + slotBase = gSfxBase + (uint32_t)NOISE_SLOT * (uint32_t)SFX_SLOT_BYTES; noiseEnsure((uint8_t)NOISE_SLOT, slotBase); // Core clamps pitch <= 31, so the subtrahend is at most 23250. rate = (uint16_t)(NOISE_RATE_MAX - (uint16_t)pitch * NOISE_RATE_STEP); - freqWord = (uint16_t)(((uint32_t)rate * 65536UL) / IIGS_DOC_DIVISOR); + freqWord = docFreqWord(rate); len = (uint16_t)(((uint32_t)rate * NOISE_SUSTAIN_MUL) >> 9); // Rotate the start offset so per-frame retriggers do not replay @@ -505,6 +995,20 @@ void jlpAudioNoise(uint8_t pitch, uint8_t atten) { vol = (uint8_t)(254u - (uint16_t)atten * 17u); + // Still sounding at the same volume? Then this is a pitch change on + // a running voice, which is exactly what the caller means: retune + // the oscillators and leave the sample alone. Only a finished (or + // re-levelled) burst needs the full NTPstreamsound restart below. + // + // Wrapping compare: GetTick is a 16-bit counter, so test the signed + // difference rather than `now < deadline`. + if (vol == gNoiseVol + && (int16_t)(iigsGetTickWord() - gNoiseHoldUntil) < 0) { + docRetuneOscPair((uint8_t)(SFX_BASE_OSC + NOISE_SLOT * SFX_OSCS_PER_SLOT), + freqWord); + return; + } + sfx = (unsigned char *)slotBase; sfx[0] = (unsigned char)(startAddr & 0xFFu); sfx[1] = (unsigned char)((startAddr >> 8) & 0xFFu); @@ -515,8 +1019,10 @@ void jlpAudioNoise(uint8_t pitch, uint8_t atten) { sfx[7] = 0; sfx[8] = (unsigned char)(freqWord & 0xFFu); sfx[9] = (unsigned char)((freqWord >> 8) & 0xFFu); - sfx[13] = vol; + sfxApplyPan(sfx, (uint8_t)NOISE_SLOT, vol); sfxTrigger(slotBase); + gNoiseVol = vol; + gNoiseHoldUntil = (uint16_t)(iigsGetTickWord() + NOISE_HOLD_TICKS); } @@ -527,43 +1033,31 @@ void jlpAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) { if (length == 0 || length > NTP_BUFFER_BYTES) { return; } - // Drop any previous song before loading a new one. - if (gNTPPlaying) { - jlpAudioStopMod(); - } + // Drop any previous song -- and the silent module ntpArm prepared -- + // before loading a new one. NTPprepare must see the system sound + // vector installed, not its own; see ntpDisarm. + ntpDisarm(); BlockMove((Pointer)data, (Pointer)gModuleBase, length); // NTPprepare(modPtr in X/Y, doubling in A). modPtr is a 24-bit // address: low 16 bits in X, bank byte in Y (high byte is don't // care, low byte is the bank). - buildCallStub(gNTPBase + 0, - (uint16_t)(gModuleBase & 0xFFFFu), - (uint16_t)((gModuleBase >> 16) & 0xFFu), - 0); - __asm__ volatile ( - "jsl gCallStub\n\t" - "rep #0x30" - ::: "memory" - ); + ntpCall(gNTPBase + NTP_ENTRY_PREPARE, + (uint16_t)(gModuleBase & 0xFFFFu), + (uint16_t)((gModuleBase >> 16) & 0xFFu), + 0); + gNTPArmed = true; // NTPplay(loopFlag in A). 0 = loop forever, 1 = play once. - buildCallStub(gNTPBase + 3, 0, 0, loop ? 0 : 1); - __asm__ volatile ( - "jsl gCallStub\n\t" - "rep #0x30" - ::: "memory" - ); + ntpCall(gNTPBase + NTP_ENTRY_PLAY, 0, 0, loop ? 0u : 1u); gNTPPlaying = true; } void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz) { - unsigned char *sfx; - uint32_t slotBase; - uint32_t sampleAddr; - uint32_t structAddr; - uint16_t freqWord; - uint16_t clampedRate; + uint32_t slotBase; + uint32_t sampleAddr; + uint32_t structAddr; if (!gNTPReady || gSfxHandle == NULL || slot >= JOEY_AUDIO_SFX_SLOTS) { return; @@ -572,9 +1066,6 @@ void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1 return; } - clampedRate = (rateHz > IIGS_MAX_SFX_RATE) ? IIGS_MAX_SFX_RATE : rateHz; - freqWord = (uint16_t)(((uint32_t)clampedRate * 65536UL) / IIGS_DOC_DIVISOR); - slotBase = gSfxBase + (uint32_t)slot * (uint32_t)SFX_SLOT_BYTES; structAddr = slotBase; sampleAddr = slotBase + SFX_SAMPLE_OFFSET; @@ -592,33 +1083,9 @@ void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1 // Cache miss: copy the sample into this slot's fixed-bank // region, converting signed 8-bit (public API contract) to // unsigned 8-bit (DOC RAM format) by flipping the sign bit. - { - unsigned char *dst; - uint32_t i; + sfxCopyUnsigned((unsigned char *)sampleAddr, sample, length); - dst = (unsigned char *)sampleAddr; - for (i = 0; i < length; i++) { - dst[i] = (unsigned char)(sample[i] ^ 0x80); - } - } - - // Build the stream structure in this slot's first 16 bytes. - sfx = (unsigned char *)slotBase; - sfx[0] = (unsigned char)(sampleAddr & 0xFFu); - sfx[1] = (unsigned char)((sampleAddr >> 8) & 0xFFu); - sfx[2] = (unsigned char)((sampleAddr >> 16) & 0xFFu); - sfx[3] = 0; - sfx[4] = (unsigned char)(length & 0xFFu); - sfx[5] = (unsigned char)((length >> 8) & 0xFFu); - sfx[6] = (unsigned char)((length >> 16) & 0xFFu); - sfx[7] = (unsigned char)((length >> 24) & 0xFFu); - sfx[8] = (unsigned char)(freqWord & 0xFFu); - sfx[9] = (unsigned char)((freqWord >> 8) & 0xFFu); - sfx[10] = (unsigned char)(SFX_BASE_DOC_PAGE + slot * SFX_DOC_PAGE_STEP); - sfx[11] = (unsigned char)(SFX_BASE_OSC + slot * SFX_OSCS_PER_SLOT); - sfx[12] = 1; // one playing osc - sfx[13] = SFX_VOLUME; - sfx[14] = SFX_CHANNEL_LEFT; + sfxFillStruct(slot, slotBase, length, rateHz); gSfxSlotSample[slot] = sample; gSfxSlotLength[slot] = length; @@ -629,15 +1096,20 @@ void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1 } -// Streaming SFX not wired on IIgs: the DOC-RAM streamer is tuned for -// fully-resident samples handed to NTPstreamsound. Live refill from -// a callback would need a DOC-IRQ ring buffer, which is a separate -// project. No-op until then. +// Streaming SFX. NTPstreamsound plays one resident buffer and stops, +// so the stream is chunked: arm the slot, push the first chunk now, +// and let jlpAudioFrameTick push each following one as the previous +// is due to run out. The seam therefore quantises to a frame, which +// is the jitter the public contract documents. void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz) { - (void)slot; - (void)fill; - (void)ctx; - (void)rateHz; + streamStart(slot, fill, ctx, rateHz, false); +} + + +// jlAudioPlaySfxStreamRaw: the fill hands over unsigned, zero-free bytes +// (JL_SFX_RAW_UNSIGNED8_NOZERO), so streamChunk streams them as they are. +void jlpAudioPlaySfxStreamRaw(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz) { + streamStart(slot, fill, ctx, rateHz, true); } @@ -661,7 +1133,9 @@ void jlpAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) { uint32_t lenWide; uint16_t freqWord; uint16_t len; + uint16_t holdTicks; uint8_t vol; + uint8_t firstOsc; // Tone voices own SFX slots 0-2 ONLY: slot 3 is the noise sample, // slot 4 the app SFX slot -- a wider bound would let a stray call @@ -682,16 +1156,17 @@ void jlpAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) { slotBase = gSfxBase + (uint32_t)voice * (uint32_t)SFX_SLOT_BYTES; toneSquareEnsure(voice, slotBase); + firstOsc = (uint8_t)(SFX_BASE_OSC + voice * SFX_OSCS_PER_SLOT); // Pitch: play the 8-sample square period at toneHz, so the DOC - // playback rate is freqHz * TONE_PERIOD_BYTES samples/sec and - // freq_word = rate * 65536 / 27961. Tone resolution ~0.05 Hz. - // Overflow check: TONE_MAX_HZ * 524288 = 1.83e9 < UINT32_MAX. - freqWord = (uint16_t)(((uint32_t)freqHz * (65536UL * (uint32_t)TONE_PERIOD_BYTES)) / IIGS_DOC_DIVISOR); + // playback rate is freqHz * TONE_PERIOD_BYTES samples/sec. The word + // comes from docFreqWord, which uses the 512-units-per-byte scale the + // oscillator is actually programmed with -- this used to inline the + // 65536 scale and ran every tone 128x sharp, far above hearing. + freqWord = docFreqWord((uint16_t)(freqHz * TONE_PERIOD_BYTES)); - // Sustain: scale the one-shot length to ~80 ms of audio at this - // rate (len = rate * 0.08 = freqHz * 8 * 0.08 ~= freqHz * 41/64), - // rounded down to whole periods so retriggers seam. + // Sustain (see TONE_SUSTAIN_MUL), rounded down to whole periods so + // restarts seam. lenWide = ((uint32_t)freqHz * TONE_SUSTAIN_MUL) >> 6; len = (uint16_t)(lenWide & ~(uint32_t)(TONE_PERIOD_BYTES - 1u)); if (len < TONE_PERIOD_BYTES) { @@ -705,6 +1180,22 @@ void jlpAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) { // same slope as the old baked-amplitude map). vol = (uint8_t)(254u - (uint16_t)atten * 17u); + // Still sounding at this volume? Retune in place (the same pitch + // is then a no-op) instead of restarting the square every pump. + // Wrapping compare: GetTick is a 16-bit counter. + if (vol == gToneVol[voice] && (int16_t)(iigsGetTickWord() - gToneHoldUntil[voice]) < 0) { + docRetuneOscPair(firstOsc, freqWord); + return; + } + + // Ticks this trigger sounds for: len bytes at freqHz * 8 bytes/s. + holdTicks = (uint16_t)(((uint32_t)len * IIGS_FRAME_HZ) / ((uint32_t)freqHz * TONE_PERIOD_BYTES)); + if (holdTicks > TONE_HOLD_MARGIN_TICKS) { + holdTicks = (uint16_t)(holdTicks - TONE_HOLD_MARGIN_TICKS); + } else { + holdTicks = 0u; // too short to hold: restart per call + } + sfx = (unsigned char *)slotBase; sfx[4] = (unsigned char)(len & 0xFFu); sfx[5] = (unsigned char)((len >> 8) & 0xFFu); @@ -712,8 +1203,10 @@ void jlpAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) { sfx[7] = 0; sfx[8] = (unsigned char)(freqWord & 0xFFu); sfx[9] = (unsigned char)((freqWord >> 8) & 0xFFu); - sfx[13] = vol; + sfxApplyPan(sfx, voice, vol); sfxTrigger(slotBase); + gToneVol[voice] = vol; + gToneHoldUntil[voice] = (uint16_t)(iigsGetTickWord() + holdTicks); } @@ -750,13 +1243,108 @@ void jlpAudioStopMod(void) { if (!gNTPReady || !gNTPPlaying) { return; } - buildCallStub(gNTPBase + 6, 0, 0, 0); - __asm__ volatile ( - "jsl gCallStub\n\t" - "rep #0x30" - ::: "memory" - ); - gNTPPlaying = false; + ntpDisarm(); + + // NTPstop took the DOC interrupt away with the music. SFX ride on + // that same interrupt, so put it straight back. + ntpArm(); +} + + +// Pull one chunk from a streaming slot's callback and play it. +// Returns false when the stream has ended (fill returned 0). +static bool streamChunk(uint8_t slot) { + StreamSlotT *st = &gStream[slot]; + unsigned char *dst; + uint32_t slotBase; + uint32_t got; + uint32_t declared; + + slotBase = gSfxBase + (uint32_t)slot * (uint32_t)SFX_SLOT_BYTES; + dst = (unsigned char *)(slotBase + SFX_SAMPLE_OFFSET); + + // The callback writes straight into the slot, which avoids a second + // buffer: SIGNED 8-bit (the public contract), which sfxCopyUnsigned + // then converts in place, or DOC-ready bytes from a raw stream. + got = st->fill(st->ctx, (int8_t *)dst, (uint32_t)SFX_STREAM_BYTES); + if (got == 0u || got > (uint32_t)SFX_STREAM_BYTES) { + return false; + } + if (!st->raw) { + sfxCopyUnsigned(dst, (const uint8_t *)dst, got); + } + // Silence tail: NTP stops the voice 512 bytes before the declared end + // (see SFX_STREAM_TAIL), so give it 512 bytes it may discard. The + // fill only ever writes [0, got), so a tail laid down for the same + // length last chunk is still there: full chunks pad once per stream. + { + uint32_t padTo = (got + (SFX_STREAM_PAGE - 1u)) & ~(SFX_STREAM_PAGE - 1u); + + declared = padTo + SFX_STREAM_TAIL; + if (st->paddedGot != (uint16_t)got) { + unsigned char *p = dst + got; + uint16_t left = (uint16_t)(declared - got); + + while (left != 0u) { + *p++ = 0x80u; + left--; + } + st->paddedGot = (uint16_t)got; + } + } + + // Hand the chunk to the same stream-struct path jlpAudioPlaySfx uses, + // then bypass its per-slot cache: the buffer address never changes + // between chunks, so the cache would take every chunk after the first + // for a repeat of the first and replay it. + sfxFillStruct(slot, slotBase, declared, st->rateHz); + gSfxSlotSample[slot] = (const uint8_t *)0; + gSfxSlotLength[slot] = 0; + gSfxSlotRateHz[slot] = 0; + sfxTrigger(slotBase); + + // Schedule the next pull for when this chunk runs out. One tick short + // so the seam lands early rather than leaving a gap. + { + uint32_t ticks = ((uint32_t)got * (uint32_t)IIGS_FRAME_HZ) / (uint32_t)st->rateHz; + + if (ticks > 1u) { + ticks--; + } + st->holdUntil = (uint16_t)(iigsGetTickWord() + (uint16_t)ticks); + } + return true; +} + + +// Arm a slot's stream and push its first chunk (jlpAudioPlaySfxStream +// and the raw variant differ only in how the chunk bytes arrive). +static void streamStart(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz, bool raw) { + if (!gNTPReady || gSfxHandle == NULL || slot >= JOEY_AUDIO_SFX_SLOTS) { + return; + } + if (fill == (jlAudioStreamFillT)0 || rateHz == 0u) { + return; + } + gStream[slot].fill = fill; + gStream[slot].ctx = ctx; + gStream[slot].rateHz = (rateHz > IIGS_MAX_SFX_RATE) ? IIGS_MAX_SFX_RATE : rateHz; + gStream[slot].raw = raw; + gStream[slot].paddedGot = 0u; + gStream[slot].active = true; + if (!streamChunk(slot)) { + gStream[slot].active = false; + } +} + + +void jlpAudioSetPan(uint8_t slot, uint8_t pan) { + if (slot >= JOEY_AUDIO_SFX_SLOTS) { + return; + } + gSlotPan[slot] = pan; + // Takes effect on the slot's next trigger: the volume pair lives in the + // stream struct, which every trigger rewrites through sfxApplyPan. } @@ -767,6 +1355,10 @@ void jlpAudioStopSfx(uint8_t slot) { if (!gNTPReady || slot >= JOEY_AUDIO_SFX_SLOTS) { return; } + gStream[slot].active = false; + if (slot < JOEY_AUDIO_VOICES) { + gToneHoldUntil[slot] = iigsGetTickWord(); // next jlpAudioVoice restarts + } firstOsc = (uint8_t)(SFX_BASE_OSC + slot * SFX_OSCS_PER_SLOT); @@ -784,9 +1376,30 @@ void jlpAudioStopSfx(uint8_t slot) { void jlpAudioFrameTick(void) { - // NTP is DOC-IRQ driven; nothing to pump there. But a registered audio - // tick (jlpAudioTickRegister) rides this per-frame hook to re-assert PSG - // voices and advance AGI-style sound at up to IIGS_FRAME_HZ. + // Streaming slots: NTPstreamsound plays one chunk and stops, so the + // next chunk goes out here when the previous one is due to finish. + { + uint8_t slot; + uint16_t now = iigsGetTickWord(); + + for (slot = 0u; slot < JOEY_AUDIO_SFX_SLOTS; slot++) { + if (!gStream[slot].active) { + continue; + } + // Wrapping compare: GetTick is a 16-bit counter. + if ((int16_t)(now - gStream[slot].holdUntil) < 0) { + continue; + } + if (!streamChunk(slot)) { + gStream[slot].active = false; + } + } + } + + // NTP is DOC-IRQ driven; nothing else to pump there. But a registered + // audio tick (jlpAudioTickRegister) rides this per-frame hook to + // re-assert PSG voices and advance AGI-style sound at up to + // IIGS_FRAME_HZ. if (gTickFn != (jlAudioTickFnT)0) { gTickCount--; if (gTickCount == 0u) { diff --git a/src/iigs/hal.c b/src/iigs/hal.c index 3a155ab..455231e 100644 --- a/src/iigs/hal.c +++ b/src/iigs/hal.c @@ -34,11 +34,17 @@ // Memory Manager surface for jlpBigAlloc / jlpBigFree (jlAlloc backing). // Same idiom as src/iigs/audio_full.c: Handles/pointers are void*, -// the owner ID comes from MMStartUp(), and the attribute bit values are -// Apple's Memory Manager constants. Only the bits we use are defined. +// the owner ID comes from iigsMemUserId(), and the attribute bit values +// are Apple's Memory Manager constants. Only the bits we use are defined. +// +// _ownerid used to expand to MMStartUp() at EVERY call site. MMStartUp +// hands out a NEW memory ID each time it is called, so every allocation +// burned one of the Memory Manager's application IDs; once the pool ran +// dry the next NewHandle went wild (the UBER probe build BRK'd into the +// ROM monitor inside jlSurfaceCreate, 2026-09-22). One ID, fetched once. #include typedef void *Handle; -#define _ownerid (MMStartUp()) +#define _ownerid (iigsMemUserId()) #define attrAddr 0x0002 #define attrNoSpec 0x0020 #define attrFixed 0x4000 @@ -48,14 +54,25 @@ typedef void *Handle; // stash it just before the pointer we return. #define BIG_ALLOC_HDR ((uint32_t)sizeof(Handle)) -/* GetTick wrapper in peislam.s: invokes the Misc Toolset GetTick - * ($2503) and returns the low 16 bits of the system's tick counter - * (firmware VBL ISR-driven). Polling $C019 from C user code missed - * transitions for any op over ~1 ms; the system's tick counter is - * updated by the actual interrupt handler so it stays accurate - * regardless of caller polling rate. Tick rate matches the video - * field rate -- 60 Hz on NTSC, 50 Hz on PAL. */ +// VBL tick counter in peislam.s: a Misc Toolset heartbeat task that the +// firmware VBL interrupt handler bumps every field, read by +// iigsGetTickWord as a same-bank word load. It replaced a _GetTick +// toolbox call per read (hundreds of cycles, ten-odd reads per Space +// Taxi frame). Polling $C019 from C missed transitions for any op over +// ~1 ms; the interrupt-driven count stays accurate regardless of the +// caller's polling rate. Tick rate matches the video field rate -- 60 Hz +// on NTSC, 50 Hz on PAL. iigsVblInstall/iigsVblRemove wrap SetHeartBeat/ +// DelHeartBeat for that task and return the Misc Tool Set error (0 = ok). extern uint16_t iigsGetTickWord(void); +extern uint16_t iigsVblInstall(void); +extern uint16_t iigsVblRemove(void); +// MVN block copy (peislam.s): one run that crosses no bank on either side. +typedef struct { + uint32_t dst; + uint32_t src; + uint16_t len; +} IigsMvnArgsT; +extern void iigsMvnCopy(const IigsMvnArgsT *args); /* Reads battery RAM hrtz50or60: 0 = NTSC, 1 = PAL. */ extern uint16_t iigsReadHzParam(void); static uint16_t gFrameHz = 60u; @@ -217,14 +234,40 @@ volatile uint16_t gPeiSkipTmp; // scan: rows skipped by a clean group // start true (forces the very first present to upload), get set true // again whenever jlScbSet* / jlPaletteSet mutate the stage's data, and // get cleared here after upload. +// Word stores, NOT memcpy: the clang IIgs libc memcpy is a ~30 cyc/byte +// byte loop (finding #79 in palette.c), so the 712 bytes here cost ~21k +// cycles -- 7.6 ms, a third of a frame -- every time both flags fire. +// 16-bit stores land at ~8 cyc/word, ~2.8k cycles for the pair. +static void copyWords(volatile uint16_t *dst, const uint16_t *src, uint16_t words) { + uint16_t i; + + for (i = 0; i < words; i++) { + dst[i] = src[i]; + } +} + + static void uploadScbAndPaletteIfNeeded(const jlSurfaceT *src) { if (gStageScbDirty) { - memcpy(IIGS_SHR_SCB, src->scb, SURFACE_HEIGHT); + copyWords((volatile uint16_t *)IIGS_SHR_SCB, (const uint16_t *)src->scb, SURFACE_HEIGHT / 2u); gStageScbDirty = false; } if (gStagePaletteDirty) { - memcpy(IIGS_SHR_PALETTE, src->palette, sizeof(src->palette)); - gStagePaletteDirty = false; + // Only the palettes that changed: a colour-cycling effect touches + // one palette a frame, and 32 bytes beats 512 sixteen times over. + uint16_t mask = gStagePaletteDirtyMask; + uint8_t pal; + + for (pal = 0; mask != 0u && pal < SURFACE_PALETTE_COUNT; pal++) { + if ((mask & 1u) != 0u) { + copyWords((volatile uint16_t *)IIGS_SHR_PALETTE + (uint16_t)pal * SURFACE_COLORS_PER_PALETTE, + (const uint16_t *)src->palette + (uint16_t)pal * SURFACE_COLORS_PER_PALETTE, + SURFACE_COLORS_PER_PALETTE); + } + mask >>= 1; + } + gStagePaletteDirty = false; + gStagePaletteDirtyMask = 0u; } } @@ -274,7 +317,26 @@ void jlpBigFree(void *p) { static void claimStagePixels(void); bool jlpInit(const jlConfigT *config) { + uint16_t tick; + uint16_t spin = 0u; + (void)config; + // The heartbeat task is the frame clock; nothing else can wait for a + // VBL, so prove it advances before touching anything else (a task + // that never runs would spin jlpWaitVBL forever). + if (iigsVblInstall() != 0u) { + jlLog("iigs: SetHeartBeat failed"); + return false; + } + tick = iigsGetTickWord(); + while (iigsGetTickWord() == tick) { + spin++; + if (spin == 0u) { + iigsVblRemove(); + jlLog("iigs: VBL heartbeat task never ran"); + return false; + } + } claimStagePixels(); gPreviousNewVideo = *IIGS_NEWVIDEO_REG; gPreviousBorder = *IIGS_BORDER_REG; @@ -355,7 +417,59 @@ void jlpPresent(const jlSurfaceT *src) { #define IIGS_QUIT_MAILBOX ((volatile uint8_t *)0xE19DC8L) +// jlMemCopy: MVN moves of up to a bank at a time. MVN's X and Y wrap +// inside their banks, so a run ends where either side would cross one; +// Memory Manager blocks are free to straddle banks. +void iigsMemCopy(void *dst, const void *src, uint32_t bytes) { + static IigsMvnArgsT args; + uint32_t d = (uint32_t)dst; + uint32_t s = (uint32_t)src; + + while (bytes != 0u) { + uint32_t run = bytes; + uint32_t toD = 0x10000UL - (d & 0xFFFFu); + uint32_t toS = 0x10000UL - (s & 0xFFFFu); + + if (run > toD) { + run = toD; + } + if (run > toS) { + run = toS; + } + if (run > 0xFFFFu) { + run = 0xFFFFu; + } + args.dst = d; + args.src = s; + args.len = (uint16_t)run; + iigsMvnCopy(&args); + d += run; + s += run; + bytes -= run; + } +} + + +// The Memory Manager owner ID for every JoeyLib allocation on the IIgs +// (jlpBigAlloc, the stage pixels, the audio buffers). MMStartUp creates +// a fresh application ID per call, so it is called exactly once here +// and the result cached for the life of the program. +uint16_t iigsMemUserId(void) { + static uint16_t gMemUserId = 0u; + static bool gMemUserIdReady = false; + + if (!gMemUserIdReady) { + gMemUserId = (uint16_t)MMStartUp(); + gMemUserIdReady = true; + } + return gMemUserId; +} + + void jlpShutdown(void) { + // The task lives in this program's memory: pull it from the heartbeat + // queue before GS/OS frees the segments. + iigsVblRemove(); if (gModeSet) { *IIGS_NEWVIDEO_REG = gPreviousNewVideo; *IIGS_BORDER_REG = gPreviousBorder; @@ -514,8 +628,8 @@ void jlpSurfaceCopyRect(jlSurfaceT *dst, const jlSurfaceT *src, uint16_t x, uint * the legacy paths did. Same logic as the DOS port. */ -// Block until the next VBL boundary by waiting for the firmware tick -// (GetTick, incremented by the VBL interrupt handler) to advance. The +// Block until the next VBL boundary by waiting for the heartbeat tick +// (incremented by the VBL interrupt handler) to advance. The // old form polled $C019's VBL status bit, which never reads high under // MAME's apple2gs (PERF-AUDIT.md finding #76): the wait-for-scan loop // spun forever, hanging every program that paces with jlWaitVBL. UBER @@ -537,7 +651,7 @@ void jlpWaitVBL(void) { // (jlpFrameCount is a port.h macro straight to iigsGetTickWord -- the -// GetTick asm wrapper in peislam.s -- so no C wrapper lives here.) +// heartbeat counter read in peislam.s -- so no C wrapper lives here.) uint16_t jlpFrameHz(void) { diff --git a/src/iigs/joeyDraw.s b/src/iigs/joeyDraw.s index ae2349f..b017968 100644 --- a/src/iigs/joeyDraw.s +++ b/src/iigs/joeyDraw.s @@ -1321,6 +1321,23 @@ iigsTilePasteGlyphMark: and #0x00FF sta tpgBitsW+14 +; The 16-word colour table depends only on (fg, bg), and consecutive +; cells almost always share them (a level is painted in colour runs), +; so it is kept across calls: rebuild only when the pair changes. The +; key is fg << 4 | bg; a zeroed bss key with zeroed tables IS the +; fg = bg = 0 table, so no initialisation is needed. + lda 16,s ; fg + asl a + asl a + asl a + asl a + ora 18,s ; | bg + cmp tpgLastKey + bne tpgTableBuild + brl tpgTableReady ; the build is past an 8-bit branch's reach +tpgTableBuild: + sta tpgLastKey + ; pb[0..3]: the output byte for each 2-bit pixel pair, ; and the same four values pre-shifted into a high byte. lda 16,s ; fg @@ -1405,6 +1422,7 @@ iigsTilePasteGlyphMark: lda tpgPbHi+6 ora tpgPb+6 sta tpgWord+30 +tpgTableReady: ; X = stage byte offset of the tile's row 0 lda 14,s ; by @@ -1579,6 +1597,11 @@ tpgPb: tpgPbHi: .zero 8 +.section .bss.tpgLastKey,"aw" +.globl tpgLastKey +tpgLastKey: +.zero 2 + .section .bss.tpgFgHi,"aw" .globl tpgFgHi tpgFgHi: @@ -4738,6 +4761,14 @@ peiRowClean: ; The index maths runs in M=16 throughout: TAX/TAY there move a clean ; 16-bit value, whereas in M=8 they would carry whatever the hidden ; high byte B happens to hold. +; The flag is consulted only on a group's FIRST row: a clean row inside +; a group that has already been entered just steps (the flag could only +; have said "may be dirty", which the band tests are settling row by +; row). Measured 2026-09-22: ~90 of the ~160 clean rows per present sat +; inside dirty groups and paid the full index dance for nothing. + txa ; M=8: low byte of the row + and #7 + bne peiRowStep ; mid-group: step phx ; park the row rep #0x20 .a16 @@ -4752,6 +4783,7 @@ peiRowClean: plx ; row back in X (PLX sets flags on X) cmp #0 beq peiRowSkipGroup +peiRowStep: inx iny bra peiRowBounds diff --git a/src/iigs/peislam.s b/src/iigs/peislam.s index 2dcfbf7..488ba05 100644 --- a/src/iigs/peislam.s +++ b/src/iigs/peislam.s @@ -1,8 +1,8 @@ -; peislam.s - GetTick and ReadBParam trampolines for the IIgs. +; peislam.s - VBL tick counter and ReadBParam trampoline for the IIgs. ; ; GAS syntax for the llvm-mos w65816 toolchain. The old PEI-slam present -; logic was rolled into iigsBlitStageToShr in joeyDraw; only the two -; toolbox trampolines remain. +; logic was rolled into iigsBlitStageToShr in joeyDraw; what remains is +; the heartbeat-driven tick counter and one toolbox trampoline. ; ; C ABI (llvm-mos w65816): caller has M=I=16. A uint16_t return value ; is left in A, which matches the toolbox dispatcher's word result, so @@ -11,27 +11,94 @@ .text ; ---------------------------------------------------------------- -; uint16_t iigsGetTickWord(void) +; VBL tick counter. ; -; Calls Misc Toolset GetTick ($2503) and returns the low 16 bits of -; the 32-bit tick counter. The system increments this counter from the -; actual VBL hardware interrupt, so it stays accurate regardless of -; caller polling rate -- C-side polling of $C019 missed transitions -; for any op over ~1 ms. +; A Misc Toolset heartbeat task (SetHeartBeat, $1203) that the firmware +; VBL interrupt handler runs once per video field. It bumps a 16-bit +; counter that iigsGetTickWord returns. The old wrapper went through +; the toolbox dispatcher to _GetTick ($2503) on every read -- hundreds +; of cycles, ten-odd times per Space Taxi frame (~5% of a frame). The +; rate is unchanged: both GetTick and the heartbeat queue are driven +; by the same VBL interrupt (60 Hz NTSC, 50 Hz PAL), so jlpFrameHz and +; every tick-based hold in audio_full.c keep their meaning. As before, +; a caller holding SEI freezes the count. ; -; GetTick output convention: caller pushes 4 bytes of output space, the -; tool dispatcher writes the LongWord into them. We pull the low 16 -; bits into A (the return value) and discard the high 16 into X. +; The header, the task code, the counter and the accessors share ONE +; section, so every reference between them is a same-bank 16-bit +; absolute (K is the bank; B is set from K around each access -- the +; heartbeat handler's B and D are undefined, and C globals live in a +; different bank from this code). +; +; Heartbeat task header (Toolbox Reference, Misc Tool Set): +; +0 long link owned by the heartbeat handler +; +4 word count decremented every tick; the task runs when it +; reaches 0 and must re-arm it +; +6 word signature $A55A +; +8 code, ended by rtl ; ---------------------------------------------------------------- - .section .text.iigsGetTickWord,"ax" + .section .text.iigsVblTask,"ax" .globl iigsGetTickWord + .globl iigsVblInstall + .globl iigsVblRemove +iigsVblTask: + .long 0 + .word 1 + .word 0xA55A + php + rep #0x30 + pha + phb + phk + plb + inc vblTick + lda #1 + sta iigsVblTask+4 ; re-arm for the next tick + plb + pla + plp + rtl +vblTick: + .word 0 + +; uint16_t iigsGetTickWord(void) -- the current VBL count. iigsGetTickWord: - pha ; output space high word - pha ; output space low word - ldx #0x2503 ; _GetTick - jsl 0xe10000 - pla ; A = low 16 bits (return value) - plx ; discard high 16 bits + phb + phk + plb + lda vblTick + plb + rtl + +; uint16_t iigsVblInstall(void) -- SetHeartBeat(iigsVblTask). +; uint16_t iigsVblRemove(void) -- DelHeartBeat(iigsVblTask). +; Both return the Misc Tool Set error code, 0 = success. The long +; pointer is pushed high word first ($00, K) then the low word (a +; same-section 16-bit relocation). The high word is built without +; leaving M=16: a 16-bit zero push plus phk leaves ONE stray zero byte +; under the parameter block, dropped by bumping S after the call +; (tsc/inc/tcs preserve the carry that flags a tool error). An 8-bit +; push after sep #0x20 is what the first cut did, and llvm-mc kept +; assembling `lda #0` as a 16-bit immediate (no .a8 directive) -- the +; CPU ran the extra byte as BRK inside the call. +iigsVblInstall: + ldx #0x1203 ; _SetHeartBeat + bra vblTool +iigsVblRemove: + ldx #0x1303 ; _DelHeartBeat +vblTool: + lda #0 + pha ; 00 00 + phk ; K -> stack holds K 00 00 (one zero too many) + pea iigsVblTask ; low word + jsl 0xe10000 ; consumes lo hi K 00 + tay ; A = error code (carry set on error) + tsc + inc + tcs ; drop the stray zero byte + tya + bcs vblToolDone + lda #0 +vblToolDone: rtl ; ---------------------------------------------------------------- @@ -40,8 +107,8 @@ iigsGetTickWord: ; Reads battery RAM parameter hrtz50or60 ($1D) via _ReadBParam ($0C03) ; and returns the raw value: 0 = NTSC (60 Hz), 1 = PAL (50 Hz). ; -; GetTick fires from the hardware VBL ISR, so its rate matches the -; video field rate -- 60 Hz on NTSC, 50 Hz on PAL. jlpFrameHz must +; The heartbeat tick fires from the hardware VBL ISR, so its rate +; matches the video field rate -- 60 Hz on NTSC, 50 Hz on PAL. jlpFrameHz must ; report whichever this machine actually runs so wall-clock math ; (frames * 1000 / jlpFrameHz) is correct on both. ; ---------------------------------------------------------------- @@ -54,3 +121,57 @@ iigsReadHzParam: jsl 0xe10000 pla ; A = result (return value) rtl + +; ---------------------------------------------------------------- +; void iigsMvnCopy(const IigsMvnArgsT *args) A = args low, X = bank +; +; +0 uint32_t dst 24-bit destination +; +4 uint32_t src 24-bit source +; +8 uint16_t len bytes, 1..65535, and hal.c guarantees neither +; [dst, dst+len) nor [src, src+len) crosses a bank +; (MVN's X/Y wrap inside one bank). +; +; One MVN: 7 cycles a byte against libc memcpy's byte loop of ~40. The +; two bank operands are patched into the instruction itself (this +; segment is RAM; the write goes through B = K, same section), then +; B is restored -- MVN leaves B pointing at the destination bank. +; ---------------------------------------------------------------- + .section .text.iigsMvnCopy,"ax" + .globl iigsMvnCopy +iigsMvnCopy: + php + rep #0x30 + .a16 + .i16 + phb + sta 0xe0 ; args low + txa + sta 0xe2 ; args bank + phk + plb ; B = K for the operand patch + ldy #6 + lda [0xe0],y ; src bank (byte at +6, junk high) + sep #0x20 + .a8 + sta mvnOp+2 ; MVN encodes source bank second + ldy #2 + lda [0xe0],y ; dst bank + sta mvnOp+1 ; destination bank first + rep #0x20 + .a16 + ldy #8 + lda [0xe0],y + dec a ; A = len - 1 + pha + ldy #4 + lda [0xe0],y ; src low + tax + ldy #0 + lda [0xe0],y ; dst low + tay + pla +mvnOp: + mvn 0, 0 ; patched: dst bank, src bank (54 dd ss) + plb + plp + rtl diff --git a/tools/iigsStart/iigsStart.c b/tools/iigsStart/iigsStart.c new file mode 100644 index 0000000..a0dd86e --- /dev/null +++ b/tools/iigsStart/iigsStart.c @@ -0,0 +1,121 @@ +// iigsStart -- a GS/OS boot launcher for the headless IIgs harnesses. +// +// Installed as /System/Start on a copy of the GS/OS system disk, this runs +// at boot INSTEAD of the Finder and hands off to one JoeyLib example. The +// point is that the Finder cannot be driven from a script: measured under +// gssquared, injected keystrokes reach the $C000 latch but never reach the +// Finder -- Open-Apple-A, Tab, arrows and bare letters all left the desktop +// untouched. (The one redraw seen after the first key of a run is the +// Finder's own settling: byte-identical for three different keys.) Reaching +// the desktop also costs ~61s of boot that a probe must sit through; this +// route has the example on screen in ~52s. +// +// Two GS/OS calls, in this order: +// SetPrefixGS -- point prefix 0 at the example's DIRECTORY. GS/OS does +// NOT do this for a QuitGS launch; prefix 0 was measured still pointing +// at the boot volume. That is not cosmetic: JoeyLib opens its NTP +// replayer as bare "ntpplayer", and when that read fails jlpAudioInit +// returns before ntpArm(), so the sound interrupt vector stays at its +// ROM default ($E1002C = 5C xxxx FF) and the example is SILENT. The +// Finder sets prefix 0 itself, which is why this only bites here. +// QuitGS with pCount = 2 and a pathname -- "quit, and launch this". +// +// Build: clang-build.sh -DJOEY_START_PATH='"/JOEYLIB/STAXI"' \ +// -o START iigsStart.c iigsStartQuit.s + +#include + +// The program to launch. Overridden per-disk from the build. +#ifndef JOEY_START_PATH +#define JOEY_START_PATH "/JOEYLIB/STAXI" +#endif + +// Fixed scratch in bank $E1: the $E1:9DC8-$9DFD window that survives a +// present (SCB proper ends at $9DC7). The asm stubs hard-code the two +// parameter-block addresses, so these must agree with iigsStartQuit.s. +// 9DC8 mailbox 9DCC QuitGS parm (8) +// 9DD4 pathname string 9DE4 SetPrefix parm (8) +// 9DEC prefix string +#define QUIT_MAILBOX ((volatile uint8_t *)0xE19DC8L) +#define QUIT_PARM ((volatile uint8_t *)0xE19DCCL) +#define QUIT_PATH ((volatile uint8_t *)0xE19DD4L) +#define PREFIX_PARM ((volatile uint8_t *)0xE19DE4L) +#define PREFIX_PATH ((volatile uint8_t *)0xE19DECL) +#define QUIT_PATH_ADDR 0x00E19DD4UL +#define PREFIX_PATH_ADDR 0x00E19DECUL + +// A GS/OS class-1 input string is a length WORD followed by the characters. +#define GSOS_STRING_HEADER 2u + +// Markers for the harness. +#define QUIT_REACHED 0xAAu +#define QUIT_RETURNED 0xEEu + + +void iigsStartQuit(void); +void iigsStartSetPrefix(void); + + +// Write a GS/OS input string (length word + characters) and return its length. +static uint16_t gsosString(volatile uint8_t *dst, const char *src, uint16_t len) { + uint16_t i; + + dst[0] = (uint8_t)(len & 0xFFu); + dst[1] = (uint8_t)((len >> 8) & 0xFFu); + for (i = 0; i < len; i++) { + dst[GSOS_STRING_HEADER + i] = (uint8_t)src[i]; + } + return len; +} + + +static void putLong(volatile uint8_t *dst, uint32_t v) { + dst[0] = (uint8_t)(v & 0xFFu); + dst[1] = (uint8_t)((v >> 8) & 0xFFu); + dst[2] = (uint8_t)((v >> 16) & 0xFFu); + dst[3] = (uint8_t)((v >> 24) & 0xFFu); +} + + +int main(void) { + const char *path = JOEY_START_PATH; + uint16_t len = 0; + uint16_t dir = 0; + uint16_t i; + + while (path[len] != '\0') { + len++; + } + // Directory part, including the trailing '/': everything up to the last + // separator. "/JOEYLIB/STAXI" -> "/JOEYLIB/". + for (i = 0; i < len; i++) { + if (path[i] == '/') { + dir = (uint16_t)(i + 1u); + } + } + + gsosString(PREFIX_PATH, path, dir); + PREFIX_PARM[0] = 2u; // pCount + PREFIX_PARM[1] = 0u; + PREFIX_PARM[2] = 0u; // prefixNum 0 = current directory + PREFIX_PARM[3] = 0u; + putLong(PREFIX_PARM + 4, PREFIX_PATH_ADDR); + iigsStartSetPrefix(); + + gsosString(QUIT_PATH, path, len); + QUIT_PARM[0] = 2u; // pCount + QUIT_PARM[1] = 0u; + putLong(QUIT_PARM + 2, QUIT_PATH_ADDR); // pathname + QUIT_PARM[6] = 0u; // flags + QUIT_PARM[7] = 0u; + + *QUIT_MAILBOX = QUIT_REACHED; + iigsStartQuit(); + *QUIT_MAILBOX = QUIT_RETURNED; + + // QuitGS only returns on failure. Nothing useful is left to do and the + // llvm-mos post-main exit path is broken on GS/OS, so spin rather than + // crash: the harness reads the mailbox to see what happened. + for (;;) { + } +} diff --git a/tools/iigsStart/iigsStartQuit.s b/tools/iigsStart/iigsStartQuit.s new file mode 100644 index 0000000..771d278 --- /dev/null +++ b/tools/iigsStart/iigsStartQuit.s @@ -0,0 +1,36 @@ +; GS/OS inline-call stubs for iigsStart. +; +; Same shape as iigsQuitGS in src/iigs/joeyDraw.s and for the same reason: +; pure constants, no relocations, so the cross-bank DBR trap that bit the +; first attempt at that routine cannot apply here either. Each parameter +; block lives at a FIXED address in the $E1:9DC8-$9DFD scratch window and +; the C caller fills it in before calling. + +; iigsStartSetPrefix -- SetPrefixGS ($2009), parm block at $E1:9DE4. +; +; GS/OS does NOT point prefix 0 at the program named in a QuitGS launch: it +; was measured still pointing at the BOOT volume, so the launched example's +; own relative fopen()s missed. That is not cosmetic -- JoeyLib loads the +; NTP replayer as bare "ntpplayer", and when that read fails jlpAudioInit +; bails before ntpArm(), leaving the sound vector at its ROM default and +; the application completely silent. + .section .text.iigsStartSetPrefix,"ax" + .globl iigsStartSetPrefix +iigsStartSetPrefix: + jsl 0xE100A8 + .word 0x2009 + .long 0x00E19DE4 + rtl + +; iigsStartQuit -- QuitGS ($2029), parm block at $E1:9DCC. +; +; Unlike jlpShutdown's pCount=0 "quit to my parent", this is called with +; pCount=2 and a pathname, which tells GS/OS to LAUNCH that program in our +; place. On success it never returns. + .section .text.iigsStartQuit,"ax" + .globl iigsStartQuit +iigsStartQuit: + jsl 0xE100A8 + .word 0x2029 + .long 0x00E19DCC + rtl diff --git a/tools/spritebake/spritebake.c b/tools/spritebake/spritebake.c index cff07b7..cb382cc 100644 --- a/tools/spritebake/spritebake.c +++ b/tools/spritebake/spritebake.c @@ -40,6 +40,8 @@ #define SPC_TARGET 3 #elif defined(JOEYLIB_PLATFORM_IIGS) #define SPC_TARGET 4 +#elif defined(JOEYLIB_PLATFORM_X68000) +#define SPC_TARGET 5 #else #error "spritebake: build with -DJOEYLIB_PLATFORM_" #endif diff --git a/tools/staxibake/staxibake.c b/tools/staxibake/staxibake.c index 65c5500..d3a6621 100644 --- a/tools/staxibake/staxibake.c +++ b/tools/staxibake/staxibake.c @@ -6,7 +6,12 @@ // boot with jlSpriteBankLoadPrecompiled, so no cel is JIT-compiled on // the 65816 (which costs ~0.4 s each). // -// Usage: staxibake OUTPUT.spr +// Usage: staxibake OUTPUT.spr the cab/passenger/exhaust bank +// staxibake --level LEVEL.dat OUTPUT.spr one level's hook-sprite cels +// +// The per-level bank holds the cels stLevelCelList names for that level +// (its own VIC bitmaps in the colours its hook program uses), in list +// order -- the game rebuilds the same list at level entry to key them. #include #include @@ -26,39 +31,87 @@ #define SPR_OFF_HEIGHT 7u #define SPR_OFF_CELLS 8u +static const uint8_t *levelBitmap(const StLevelT *level, uint8_t ptr); +static bool writeHeader(FILE *out, uint16_t count); -int main(int argc, char **argv) { - uint8_t header[SPR_HEADER_BYTES]; - FILE *out; - uint16_t i; - if (argc < 2) { - fprintf(stderr, "usage: staxibake OUTPUT.spr\n"); - return 2; - } - out = fopen(argv[1], "wb"); - if (out == NULL) { - fprintf(stderr, "staxibake: cannot write %s\n", argv[1]); - return 1; +// A level's own sprite block, or NULL when the level does not ship it. +static const uint8_t *levelBitmap(const StLevelT *level, uint8_t ptr) { + uint8_t k; + + for (k = 0u; k < level->spriteCount; k++) { + if (level->sprites[k].ptr == ptr) { + return level->sprites[k].bitmap; + } } + return NULL; +} + + +static bool writeHeader(FILE *out, uint16_t count) { + uint8_t header[SPR_HEADER_BYTES]; + memset(header, 0, sizeof(header)); memcpy(header, "JSP1", 4); - header[SPR_OFF_TARGET] = 0u; // cross-platform chunky - header[SPR_OFF_HAS_PAL] = 0u; // the game owns its palette - header[SPR_OFF_WIDTH] = (uint8_t)ST_CEL_TILES; - header[SPR_OFF_HEIGHT] = (uint8_t)ST_CEL_TILES; - header[SPR_OFF_CELLS] = (uint8_t)(kStCelCount & 0xFFu); - header[SPR_OFF_CELLS + 1u] = (uint8_t)(kStCelCount >> 8); - if (fwrite(header, 1, SPR_HEADER_BYTES, out) != SPR_HEADER_BYTES) { + header[SPR_OFF_TARGET] = 0u; // cross-platform chunky + header[SPR_OFF_HAS_PAL] = 0u; // the game owns its palette + header[SPR_OFF_WIDTH] = (uint8_t)ST_CEL_TILES; + header[SPR_OFF_HEIGHT] = (uint8_t)ST_CEL_TILES; + header[SPR_OFF_CELLS] = (uint8_t)(count & 0xFFu); + header[SPR_OFF_CELLS + 1u] = (uint8_t)(count >> 8); + return fwrite(header, 1, SPR_HEADER_BYTES, out) == SPR_HEADER_BYTES; +} + + +int main(int argc, char **argv) { + static StLevelT level; + static StCelDefT levelCels[ST_LEVEL_CELS_MAX]; + const StCelDefT *cels = kStCels; + uint16_t count = kStCelCount; + const char *outPath; + FILE *out; + uint16_t i; + + if (argc == 4 && strcmp(argv[1], "--level") == 0) { + FILE *fp = fopen(argv[2], "rb"); + if (fp == NULL || !stLevelParse(&level, fp)) { + fprintf(stderr, "staxibake: cannot load level %s\n", argv[2]); + return 1; + } + fclose(fp); + cels = levelCels; + count = stLevelCelList(&level, levelCels, ST_LEVEL_CELS_MAX); + outPath = argv[3]; + } else if (argc == 2) { + outPath = argv[1]; + } else { + fprintf(stderr, "usage: staxibake OUTPUT.spr\n staxibake --level LEVEL.dat OUTPUT.spr\n"); + return 2; + } + out = fopen(outPath, "wb"); + if (out == NULL) { + fprintf(stderr, "staxibake: cannot write %s\n", outPath); + return 1; + } + if (!writeHeader(out, count)) { fprintf(stderr, "staxibake: header write failed\n"); fclose(out); return 1; } - for (i = 0u; i < kStCelCount; i++) { + for (i = 0u; i < count; i++) { uint8_t blob[ST_CEL_BYTES]; - const uint8_t *bm = stC64SpriteBitmap((uint8_t)(kStCels[i].ptr - ST_SPRITE_PTR_FIRST)); - - stCelBlob(bm, kStCels[i].multi, kStCels[i].color, kStCels[i].mc0, kStCels[i].mc1, blob); + const uint8_t *bm; + if (cels == levelCels) { + bm = levelBitmap(&level, cels[i].ptr); + if (bm == NULL) { + fprintf(stderr, "staxibake: level %s has no sprite block $%02X\n", argv[2], (unsigned)cels[i].ptr); + fclose(out); + return 1; + } + } else { + bm = stC64SpriteBitmap((uint8_t)(cels[i].ptr - ST_SPRITE_PTR_FIRST)); + } + stCelBlob(bm, cels[i].multi, cels[i].color, cels[i].mc0, cels[i].mc1, blob); if (fwrite(blob, 1, ST_CEL_BYTES, out) != ST_CEL_BYTES) { fprintf(stderr, "staxibake: cel %u write failed\n", (unsigned)i); fclose(out); @@ -66,6 +119,6 @@ int main(int argc, char **argv) { } } fclose(out); - fprintf(stderr, "staxibake: wrote %u cels (%ux%u tiles) -> %s\n", (unsigned)kStCelCount, (unsigned)ST_CEL_TILES, (unsigned)ST_CEL_TILES, argv[1]); + fprintf(stderr, "staxibake: wrote %u cels (%ux%u tiles) -> %s\n", (unsigned)count, (unsigned)ST_CEL_TILES, (unsigned)ST_CEL_TILES, outPath); return 0; } diff --git a/tools/staxiharness/Makefile b/tools/staxiharness/Makefile new file mode 100644 index 0000000..09c44a7 --- /dev/null +++ b/tools/staxiharness/Makefile @@ -0,0 +1,36 @@ +# Host-side checks of Space Taxi's front-end screens (the parts tools/stsim +# cannot reach: they live in spacetaxi.c, above the simulation). Builds the +# game on the BLANK port with spacetaxi.c INCLUDED into the harness so its +# statics are reachable, and wraps the keyboard queue and the save file +# with scripted stand-ins. +# +# make -C tools/staxiharness build + run + +REPO := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))/../..) +ST := $(REPO)/examples/spacetaxi +OUT := $(REPO)/build/tools/staxiharness +SRCS := $(filter-out $(ST)/spacetaxi.c,$(wildcard $(ST)/*.c)) \ + $(wildcard $(REPO)/src/core/*.c) $(wildcard $(REPO)/src/generic/*.c) \ + $(REPO)/src/blank/blank.c +CFLAGS := -O0 -g -w -DJOEYLIB_PLATFORM_BLANK -I$(REPO)/include -I$(REPO)/src/core \ + -I$(REPO)/src/codegen -I$(ST) +WRAPS := -Wl,--wrap=jlInputGetChar -Wl,--wrap=jlSaveWrite -Wl,--wrap=jlSaveRead + +.PHONY: all run clean + +all: run + +$(OUT)/harness: harness.c $(ST)/spacetaxi.c $(SRCS) + @mkdir -p $(OUT) + $(CC) $(CFLAGS) harness.c $(SRCS) $(WRAPS) -o $@ + +# The title screen's level file is read through DATA/levels (the DOS bake +# is the cross-platform STL4 form), so run from a directory that has one. +run: $(OUT)/harness + @mkdir -p $(OUT)/DATA + @ln -sfn $(ST)/generated/dos/levels $(OUT)/DATA/levels + @ln -sfn $(ST)/generated/dos/speech.bin $(OUT)/DATA/speech.bin + cd $(OUT) && ./harness + +clean: + rm -rf $(OUT) diff --git a/tools/staxiharness/harness.c b/tools/staxiharness/harness.c new file mode 100644 index 0000000..11f5c30 --- /dev/null +++ b/tools/staxiharness/harness.c @@ -0,0 +1,183 @@ +// Space Taxi front-end harness: the immortal-cabbies table and the name +// entry screen, driven with scripted keys on the host (BLANK port). +// +// spacetaxi.c is INCLUDED so its static state and routines are visible; +// its main() is renamed away. jlInputGetChar, jlSaveWrite and jlSaveRead +// are replaced through the linker's --wrap with a key script and an +// in-memory save image (see the Makefile). +#define main staxiMain +#include "spacetaxi.c" +#undef main + +#include + +#define SAVE_IMAGE_MAX 512u + +bool __wrap_jlSaveWrite(const char *name, const void *buf, uint32_t len); +uint32_t __wrap_jlSaveRead(const char *name, void *buf, uint32_t max); +int __wrap_jlInputGetChar(void); + +static void check(bool ok, const char *what); +static void rowText(uint8_t row, uint8_t col, uint8_t n, char *out); +static void setScore(uint8_t player, const uint8_t *digits); + +static const char *gKeys = ""; +static uint8_t gSaveImage[SAVE_IMAGE_MAX]; +static uint32_t gSaveLen = 0u; +static int gFails = 0; + + +int __wrap_jlInputGetChar(void) { + if (*gKeys == 0) { + return -1; + } + return (unsigned char)*gKeys++; +} + + +uint32_t __wrap_jlSaveRead(const char *name, void *buf, uint32_t max) { + (void)name; + if (gSaveLen == 0u || gSaveLen > max) { + return 0u; + } + memcpy(buf, gSaveImage, gSaveLen); + return gSaveLen; +} + + +bool __wrap_jlSaveWrite(const char *name, const void *buf, uint32_t len) { + (void)name; + if (len > sizeof(gSaveImage)) { + return false; + } + memcpy(gSaveImage, buf, len); + gSaveLen = len; + return true; +} + + +static void check(bool ok, const char *what) { + printf("%s %s\n", ok ? "ok " : "FAIL", what); + if (!ok) { + gFails++; + } +} + + +// `n` screen cells of one row as a C string. +static void rowText(uint8_t row, uint8_t col, uint8_t n, char *out) { + uint8_t k; + + for (k = 0u; k < n; k++) { + out[k] = (char)gSim.screen[ST_CELL(row, col + k)]; + } + out[n] = 0; +} + + +// A player's score as the sim keeps it: seven screen-code digits with +// the point at ST_HS_POINT_AT. +static void setScore(uint8_t player, const uint8_t *digits) { + uint8_t k; + + for (k = 0u; k < ST_NUMBER_CHARS; k++) { + gSim.scoreBackup[player][k] = (uint8_t)(ST_CHAR_DIGIT_BASE + digits[k]); + } + gSim.scoreBackup[player][ST_HS_POINT_AT] = ST_CHAR_POINT; +} + + +int main(void) { + static const uint8_t kDigits[ST_NUMBER_CHARS] = { 1u, 2u, 3u, 4u, 0u, 5u, 6u }; + jlConfigT config; + jlSurfaceT *stage; + char buf[48]; + uint8_t k; + + config.codegenBytes = 160UL * 1024; + config.audioBytes = 32UL * 1024; + if (!jlInit(&config)) { + printf("jlInit failed\n"); + return 1; + } + stage = jlStageGet(); + memset(&gGame, 0, sizeof(gGame)); + memset(&gSim, 0, sizeof(gSim)); + stRenderInit(stage); + stAudioInit(); + stSimNewGame(&gSim, 2u, false); + + // ---- ranking ($4D92) ---- + check(highScoreInsert((const uint8_t *)" 49.99") == ST_HS_ENTRIES, "49.99 is below the $50 floor"); + check(highScoreInsert((const uint8_t *)" 50.00") == ST_HS_ENTRIES, "50.00 passes the floor but beats nobody"); + check(highScoreInsert((const uint8_t *)" 500.00") == 8u, "500.00 lands in slot 8 (under 504.97, above 498.77)"); + check(memcmp(gHighScores[9].score, " 498.77", 7) == 0 && memcmp(gHighScores[7].score, " 504.97", 7) == 0, "older entries shifted down, the last one dropped"); + check(highScoreInsert((const uint8_t *)"3877.56") == 0u, "a tie goes above the older entry"); + check(memcmp(gHighScores[1].score, "3877.56", 7) == 0 && memcmp(gHighScores[1].name, "MICHAEL PLATE ", 15) == 0, "the tied old entry is now second"); + + // ---- the entry screen for cabbie 2 with $1234.56 on shift 3 ($4D52 / $4E35) ---- + gSim.player = 1u; + gGame.variation = 3u; + setScore(1u, kDigits); + gKeys = "zz"; // typed before the screen: flushed like $0277 + check(nameEntryStart(), "1234.56 ranks"); + check(gGame.state == ST_STATE_NAME_ENTRY && gGame.hsSlot == 3u, "slot 3 (under the two 3877.56 and 1809.51)"); + check(gSim.borderColor == 2u && gSim.bgColor == 15u, "red border, grey field"); + rowText(3u, 7u, 25u, buf); + check(strcmp(buf, "CONGRATULATIONS CABBIE 2,") == 0, buf); + rowText(0u, 15u, 8u, buf); + check(strcmp(buf, "$1234.56") == 0, buf); + rowText(9u, 9u, 21u, buf); + check(strcmp(buf, "ENTER YOUR NAME BELOW") == 0, buf); + check(*gKeys == 0, "earlier keystrokes discarded"); + + // ---- typing ($4ED8): s c o 9 t t T ---- + gKeys = "sco9tt\bT"; + runNameEntry(1u); + rowText(ST_HS_NAME_ROW, ST_HS_NAME_COL, 5u, buf); + check(strcmp(buf, "SCOTT") == 0, buf); + check(gGame.hsLen == 5u, "five characters kept ('9' ignored, one deleted, one retyped)"); + check(gGame.state == ST_STATE_NAME_ENTRY, "still entering"); + for (k = 0u; k < 2u; k++) { + runNameEntry(1u); + } + check(gSim.screen[ST_CELL(ST_HS_NAME_ROW, ST_HS_NAME_COL + 5u)] == ' ', "first flip blanks the cursor ($4F4D)"); + for (k = 0u; k < 3u; k++) { + runNameEntry(1u); + } + check(gSim.screen[ST_CELL(ST_HS_NAME_ROW, ST_HS_NAME_COL + 5u)] == '%', "second flip shows it"); + gKeys = "\r"; + runNameEntry(1u); + check(gGame.state != ST_STATE_NAME_ENTRY, "RETURN leaves the screen"); + check(memcmp(gHighScores[3].name, "SCOTT ", 15) == 0 && gHighScores[3].shift == 3u, "name and shift stored"); + check(gSaveLen == 240u && gSaveImage[3 * 24 + 7] == 0 && memcmp(&gSaveImage[3 * 24 + 8], "SCOTT ", 6) == 0 && gSaveImage[3 * 24 + 23] == 3u, "saved in the C64 image layout"); + check(gSim.playersDone == 1u, "game-over bookkeeping continued"); + + // ---- reload from the image ($23E5) ---- + memset(gHighScores, 0, sizeof(gHighScores)); + highScoreLoad(); + check(memcmp(gHighScores[3].name, "SCOTT ", 15) == 0 && memcmp(gHighScores[0].score, "3877.56", 7) == 0, "table restored from the save"); + + // ---- an empty name becomes "*" ---- + gSim.player = 0u; + setScore(0u, kDigits); + check(nameEntryStart() && gGame.hsSlot == 3u, "second entry, a tie goes above"); + gKeys = "\r"; + runNameEntry(1u); + check(gHighScores[3].name[0] == '*' && memcmp(gHighScores[4].name, "SCOTT", 5) == 0, "an empty name is stored as *"); + + // ---- the table screen ($4C1F) ---- + highScoreDraw(); + rowText(1u, 9u, 20u, buf); + check(strcmp(buf, "THE IMMORTAL CABBIES") == 0, buf); + rowText(12u, 4u, 26u, buf); + check(strcmp(buf, "$1234.56 SCOTT ") == 0, buf); + check(gSim.screen[ST_CELL(12, 34)] == '3' && gSim.screen[ST_CELL(10, 15)] == '*', "shift digit in the SHIFT column, * name above"); + rowText(24u, 3u, 34u, buf); + check(strcmp(buf, "PRESS FIRE TO RETURN TO TITLE PAGE") == 0, buf); + check(jlMusicIsPlaying(), "song 7 under the table"); + + printf("%s (%d failures)\n", gFails ? "staxiharness: FAIL" : "staxiharness: PASS", gFails); + jlShutdown(); + return gFails ? 1 : 0; +} diff --git a/tools/stsim/Makefile b/tools/stsim/Makefile index ebf1bea..0101e3f 100644 --- a/tools/stsim/Makefile +++ b/tools/stsim/Makefile @@ -2,10 +2,16 @@ 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 +SRCS := stsim.c $(ST)/stSim.c $(ST)/stFare.c $(ST)/stHooks.c $(ST)/stLevelFile.c $(ST)/stTitle.c $(ST)/stC64Data.c $(ST)/stLevelCels.c -stsim: $(SRCS) $(ST)/stSim.h $(ST)/stC64Data.h $(ST)/stDemoStreams.h +stsim: $(SRCS) $(ST)/stSim.h $(ST)/stC64Data.h $(ST)/stDemoStreams.h gateStreams.h $(CC) $(CFLAGS) $(SRCS) -o $@ +# The fixture generator for gateStreams.h. Not built by default: it runs +# a long search and is only needed when a level's stream is (re)made. +recorder: recorder.c $(SRCS:stsim.c=) $(ST)/stSim.h + $(CC) $(CFLAGS) recorder.c $(ST)/stSim.c $(ST)/stFare.c $(ST)/stHooks.c \ + $(ST)/stLevelFile.c $(ST)/stTitle.c $(ST)/stC64Data.c -o $@ + clean: - rm -f stsim + rm -f stsim recorder diff --git a/tools/stsim/celGate.sh b/tools/stsim/celGate.sh new file mode 100755 index 0000000..68f5e72 --- /dev/null +++ b/tools/stsim/celGate.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Cel gate for Space Taxi: every level cel a recorded ride DISPLAYS must be +# in the level's baked manifest (stLevelCelList in stLevelCels.c), or the +# real ports build it mid-ride and draw it interpreted -- the "compiling +# sprites on the fly" hitch. Runs the same rides gate.sh does with the +# stsim census (STSIM_CELS=1) and fails on any MISSING line. +# +# celGate.sh check every hook level +set -u + +here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +repo=$(cd "$here/../.." && pwd) +levels=$repo/examples/spacetaxi/generated/iigs/levels +ticks=4000 +pairs="H:08 T:20 W:23 X:24 G:07 I:09 K:11 O:15 Q:17 R:18 U:21 V:22 E:05 J:10 L:12 P:16 S:19" + +make -C "$here" >/dev/null || { echo "celGate: build failed" >&2; exit 2; } + +fail=0 +for p in $pairs; do + l=${p%%:*}; n=${p##*:} + out=$(STSIM_CELS=1 "$here/stsim" "$levels/level$n.dat" "$l" "$ticks" 2>&1 >/dev/null | grep -E '^(CEL|CENSUS)') + missing=$(echo "$out" | grep -c '^CEL MISSING') + census=$(echo "$out" | grep '^CENSUS' | sed 's/^CENSUS //') + if [ "$missing" -ne 0 ]; then + printf ' %s: *** %s cel(s) shown but not baked *** (%s)\n' "$l" "$missing" "$census" + echo "$out" | grep '^CEL MISSING' | sed 's/^/ /' + fail=1 + else + printf ' %s: ok (%s)\n' "$l" "$census" + fi +done +[ $fail -eq 0 ] && echo "celGate: PASS" || echo "celGate: FAIL" +exit $fail diff --git a/tools/stsim/gate.sh b/tools/stsim/gate.sh new file mode 100755 index 0000000..8320eb1 --- /dev/null +++ b/tools/stsim/gate.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# Behaviour gate for the Space Taxi simulation. +# +# Runs every hook level through stsim and compares the per-tick trace +# against a golden capture. A refactor that changes nothing observable +# leaves all seventeen traces byte-identical; anything else is a regression. +# +# H, W, T and X ride the C64's own attract recordings (the ones checked +# against VICE traces). The other thirteen ride synthesised fixtures from +# gateStreams.h -- a regression gate only, not evidence of C64 fidelity. +# +# gate.sh capture [dir] record goldens from the CURRENT tree +# gate.sh check [dir] compare the current tree against them +# gate.sh hashes print one md5 per level (compact reference) +# +# Capture from a tree you trust, refactor, then check. +set -u + +here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +repo=$(cd "$here/../.." && pwd) +levels=$repo/examples/spacetaxi/generated/iigs/levels +mode=${1:-check} +dir=${2:-$here/golden} +ticks=4000 + +# level letter -> level file number +pairs="H:08 T:20 W:23 X:24 G:07 I:09 K:11 O:15 Q:17 R:18 U:21 V:22 E:05 J:10 L:12 P:16 S:19" + +make -C "$here" >/dev/null || { echo "gate: build failed" >&2; exit 2; } + +case "$mode" in +capture) + mkdir -p "$dir" + for p in $pairs; do + l=${p%%:*}; n=${p##*:} + "$here/stsim" "$levels/level$n.dat" "$l" "$ticks" > "$dir/demo$l.jsonl" 2>/dev/null + printf ' %s: %s ticks\n' "$l" "$(wc -l < "$dir/demo$l.jsonl")" + done + echo "gate: captured to $dir" + ;; +check) + fail=0 + for p in $pairs; do + l=${p%%:*}; n=${p##*:} + got=$(mktemp) + "$here/stsim" "$levels/level$n.dat" "$l" "$ticks" > "$got" 2>/dev/null + if [ ! -f "$dir/demo$l.jsonl" ]; then + echo " $l: no golden (run: gate.sh capture)" + elif cmp -s "$dir/demo$l.jsonl" "$got"; then + printf ' %s: IDENTICAL (%s ticks)\n' "$l" "$(wc -l < "$got")" + else + echo " $l: *** DIFFERS ***" + diff "$dir/demo$l.jsonl" "$got" | head -3 + fail=1 + fi + rm -f "$got" + done + [ $fail -eq 0 ] && echo "gate: PASS" || echo "gate: FAIL" + # The cel census rides the same recordings: a cel a ride displays that + # its manifest does not bake is a mid-ride interpreted sprite. + "$here/celGate.sh" || fail=1 + exit $fail + ;; +hashes) + for p in $pairs; do + l=${p%%:*}; n=${p##*:} + h=$("$here/stsim" "$levels/level$n.dat" "$l" "$ticks" 2>/dev/null | md5sum | cut -d' ' -f1) + printf '%s %s\n' "$l" "$h" + done + ;; +*) + sed -n '2,20p' "$0" + exit 2 + ;; +esac diff --git a/tools/stsim/gateHashes.txt b/tools/stsim/gateHashes.txt new file mode 100644 index 0000000..9c1b5c8 --- /dev/null +++ b/tools/stsim/gateHashes.txt @@ -0,0 +1,21 @@ +# Space Taxi behaviour gate -- md5 of each level's 4000-tick trace. +# Regenerate with: tools/stsim/gate.sh hashes +# H/W/T/X ride the C64's attract recordings; the rest ride +# synthesised fixtures from gateStreams.h (regression gate only). +H 760501b0e48f3922bfb0f23fd2831935 +T c0b3f9e9a74e0d526703cf9b1875557b +W 92226455989e8326577a6ea20df8c8ea +X 87251d616a1be6e0c9c9317987b0fe20 +G 73530651372c60042f1f46b5c0cbada0 +I 7fe4415429d2b72a50884424269cb685 +K 567be2ed37219356a055866459971345 +O 02455280933e915e57803a4059aaefd9 +Q a7732866d54ed06d52e5dc7406b5b229 +R 066a9be80107446893b4571d247ee5cc +U 02dd2f26245442ab016ef02e10fde611 +V cc2cb13c5e393d07fbece9c0c465e387 +E 21ff29d69d8318bcb2e870b9602e910e +J 4c22a7515949b1701ed4fedabb7823dd +L a0383cbde2800bb09353df15214934ef +P f345d0adc9e011662d582c4277ee663e +S daa3cd12320e32d3e9b6b97b19b0a6d0 diff --git a/tools/stsim/gateStreams.h b/tools/stsim/gateStreams.h new file mode 100644 index 0000000..8d179b5 --- /dev/null +++ b/tools/stsim/gateStreams.h @@ -0,0 +1,460 @@ +// Synthesised demo input streams -- a TEST FIXTURE, not game content. +// +// The C64 ships attract recordings for four screens only (H, W, T and X); +// those live in examples/spacetaxi/stDemoStreams.h and are the streams +// checked against VICE traces. The thirteen streams below were searched for +// by tools/stsim's recorder against this port's own simulation, scored on +// how long the cab survives and how much of the level's hook state, fare +// machine and screen animation the ride reaches. +// +// So they prove one thing and not another: they are a REGRESSION gate -- +// refactor the hooks and the tick-by-tick trace must not move -- but they +// are NOT evidence that a hook matches the original, because no C64 trace +// exists for these screens. Treat a change to these bytes as invalidating +// the goldens, and never put them in the attract rotation. + +#ifndef ST_GATE_STREAMS_H +#define ST_GATE_STREAMS_H + +#include + +// level G -- 1400 ticks, 5 hook bytes, 10 fare stages, 256 pictures, 13 landings +// regenerate: tools/stsim/recorder levels/level??.dat G 20260921 +static const uint8_t kGateStreamG[320] = { + 0x88, 0x05, 0x81, 0x09, 0x81, 0x05, 0x81, 0x04, 0x81, 0x06, 0x84, 0x0C, + 0x81, 0x01, 0x89, 0x02, 0x81, 0x09, 0x82, 0x0D, 0x88, 0x05, 0x81, 0x03, + 0x80, 0x09, 0x81, 0x01, 0x90, 0x02, 0x81, 0x04, 0x90, 0x08, 0x81, 0x03, + 0x90, 0x0A, 0x88, 0x0E, 0x88, 0x0C, 0x90, 0x0C, 0x81, 0x04, 0x81, 0x0D, + 0x90, 0x0A, 0x81, 0x0D, 0x84, 0x0E, 0x81, 0x09, 0x81, 0x08, 0x81, 0x0C, + 0x82, 0x04, 0x81, 0x02, 0x81, 0x0E, 0x81, 0x02, 0x84, 0x01, 0x88, 0x0A, + 0x88, 0x05, 0x90, 0x09, 0x81, 0x02, 0x84, 0x09, 0x84, 0x05, 0x81, 0x0D, + 0x84, 0x09, 0x88, 0x03, 0x84, 0x06, 0x84, 0x0D, 0x81, 0x05, 0x84, 0x0C, + 0x88, 0x0C, 0x88, 0x0C, 0x88, 0x03, 0x88, 0x09, 0x81, 0x0A, 0x81, 0x09, + 0x81, 0x06, 0x81, 0x07, 0x81, 0x05, 0x88, 0x09, 0x88, 0x02, 0x81, 0x0B, + 0x84, 0x0A, 0x84, 0x05, 0x88, 0x03, 0x81, 0x05, 0x81, 0x0B, 0x81, 0x05, + 0x90, 0x05, 0x84, 0x09, 0x81, 0x06, 0x81, 0x07, 0x90, 0x0D, 0x81, 0x04, + 0x81, 0x0E, 0x88, 0x0D, 0x88, 0x0D, 0x88, 0x06, 0x82, 0x02, 0x88, 0x0D, + 0x90, 0x0E, 0x81, 0x04, 0x81, 0x0B, 0x84, 0x0B, 0x81, 0x05, 0x84, 0x07, + 0x88, 0x0A, 0x80, 0x04, 0x84, 0x07, 0x84, 0x0B, 0x88, 0x0D, 0x90, 0x0E, + 0x90, 0x06, 0x88, 0x08, 0x82, 0x0C, 0x81, 0x02, 0x81, 0x0E, 0x88, 0x0A, + 0x90, 0x0B, 0x81, 0x04, 0x88, 0x08, 0x90, 0x07, 0x84, 0x03, 0x82, 0x08, + 0x81, 0x05, 0x81, 0x03, 0x81, 0x03, 0x81, 0x06, 0x90, 0x0B, 0x84, 0x0E, + 0x82, 0x0E, 0x88, 0x08, 0x88, 0x04, 0x81, 0x0D, 0x88, 0x04, 0x81, 0x08, + 0x88, 0x0C, 0x81, 0x06, 0x81, 0x04, 0x90, 0x0B, 0x81, 0x08, 0x84, 0x04, + 0x90, 0x08, 0x84, 0x09, 0x88, 0x09, 0x81, 0x0B, 0x81, 0x07, 0x84, 0x0D, + 0x88, 0x0A, 0x88, 0x09, 0x90, 0x0B, 0x90, 0x02, 0x84, 0x0D, 0x81, 0x0E, + 0x90, 0x0D, 0x84, 0x07, 0x82, 0x0C, 0x81, 0x0E, 0x81, 0x06, 0x88, 0x09, + 0x90, 0x02, 0x90, 0x04, 0x81, 0x09, 0x84, 0x0A, 0x90, 0x06, 0x81, 0x04, + 0x90, 0x03, 0x90, 0x08, 0x84, 0x0B, 0x81, 0x02, 0x90, 0x04, 0x84, 0x03, + 0x81, 0x03, 0x84, 0x05, 0x90, 0x0C, 0x81, 0x01, 0x90, 0x0D, 0x81, 0x02, + 0x90, 0x05, 0x90, 0x0E, 0x81, 0x03, 0x90, 0x0A, +}; + +// level I -- 1400 ticks, 20 hook bytes, 10 fare stages, 256 pictures, 22 landings +// regenerate: tools/stsim/recorder levels/level??.dat I 20260918 +static const uint8_t kGateStreamI[320] = { + 0x84, 0x0E, 0x81, 0x06, 0x84, 0x0E, 0x88, 0x0B, 0x88, 0x05, 0x81, 0x03, + 0x82, 0x07, 0x90, 0x0A, 0x81, 0x08, 0x88, 0x0E, 0x88, 0x0E, 0x90, 0x0B, + 0x82, 0x0D, 0x81, 0x01, 0x81, 0x0A, 0x90, 0x0A, 0x82, 0x03, 0x81, 0x07, + 0x82, 0x05, 0x81, 0x0B, 0x81, 0x05, 0x82, 0x05, 0x82, 0x0C, 0x81, 0x08, + 0x88, 0x0C, 0x90, 0x04, 0x81, 0x02, 0x88, 0x08, 0x81, 0x03, 0x81, 0x05, + 0x81, 0x0A, 0x88, 0x0E, 0x81, 0x06, 0x84, 0x0E, 0x82, 0x07, 0x82, 0x01, + 0x81, 0x04, 0x82, 0x08, 0x90, 0x0C, 0x90, 0x05, 0x81, 0x08, 0x82, 0x0D, + 0x90, 0x06, 0x81, 0x0A, 0x84, 0x0B, 0x84, 0x0C, 0x84, 0x0C, 0x88, 0x09, + 0x81, 0x06, 0x81, 0x0C, 0x81, 0x04, 0x82, 0x09, 0x88, 0x05, 0x82, 0x0E, + 0x81, 0x0C, 0x81, 0x07, 0x88, 0x01, 0x90, 0x0E, 0x81, 0x02, 0x90, 0x0E, + 0x81, 0x02, 0x90, 0x07, 0x84, 0x05, 0x84, 0x06, 0x88, 0x06, 0x88, 0x0C, + 0x81, 0x0C, 0x84, 0x0B, 0x82, 0x09, 0x81, 0x05, 0x81, 0x0B, 0x82, 0x09, + 0x90, 0x0E, 0x90, 0x06, 0x88, 0x0D, 0x84, 0x03, 0x82, 0x0B, 0x84, 0x0D, + 0x90, 0x0B, 0x90, 0x04, 0x81, 0x0B, 0x81, 0x08, 0x88, 0x08, 0x90, 0x0A, + 0x81, 0x03, 0x81, 0x01, 0x81, 0x0E, 0x81, 0x04, 0x88, 0x02, 0x90, 0x05, + 0x81, 0x07, 0x81, 0x01, 0x82, 0x09, 0x90, 0x06, 0x81, 0x09, 0x90, 0x0E, + 0x88, 0x05, 0x82, 0x0D, 0x88, 0x03, 0x81, 0x06, 0x84, 0x0C, 0x82, 0x01, + 0x81, 0x09, 0x82, 0x0E, 0x90, 0x01, 0x81, 0x01, 0x84, 0x05, 0x82, 0x06, + 0x88, 0x0A, 0x81, 0x05, 0x82, 0x03, 0x90, 0x07, 0x90, 0x08, 0x81, 0x02, + 0x90, 0x0D, 0x88, 0x0D, 0x81, 0x08, 0x90, 0x0E, 0x82, 0x0D, 0x88, 0x04, + 0x81, 0x0C, 0x82, 0x09, 0x90, 0x04, 0x81, 0x0C, 0x82, 0x06, 0x90, 0x0E, + 0x90, 0x0D, 0x90, 0x07, 0x90, 0x03, 0x82, 0x03, 0x88, 0x06, 0x81, 0x07, + 0x90, 0x08, 0x81, 0x02, 0x90, 0x09, 0x81, 0x04, 0x90, 0x06, 0x82, 0x0A, + 0x90, 0x05, 0x88, 0x05, 0x88, 0x06, 0x90, 0x0B, 0x81, 0x03, 0x82, 0x02, + 0x90, 0x0B, 0x81, 0x02, 0x90, 0x0C, 0x90, 0x07, 0x81, 0x01, 0x84, 0x08, + 0x90, 0x0C, 0x81, 0x0E, 0x88, 0x08, 0x88, 0x0C, 0x81, 0x0C, 0x90, 0x04, + 0x84, 0x0D, 0x81, 0x01, 0x88, 0x08, 0x88, 0x09, +}; + +// level K -- 1400 ticks, 1 hook bytes, 10 fare stages, 256 pictures, 16 landings +// regenerate: tools/stsim/recorder levels/level??.dat K 11 +static const uint8_t kGateStreamK[320] = { + 0x84, 0x06, 0x82, 0x0B, 0x90, 0x0C, 0x82, 0x0E, 0x90, 0x05, 0x88, 0x03, + 0x88, 0x02, 0x82, 0x0E, 0x88, 0x07, 0x84, 0x02, 0x84, 0x0B, 0x84, 0x08, + 0x90, 0x0A, 0x88, 0x07, 0x84, 0x07, 0x81, 0x03, 0x84, 0x08, 0x82, 0x0E, + 0x82, 0x0D, 0x84, 0x01, 0x81, 0x09, 0x81, 0x0E, 0x81, 0x0C, 0x81, 0x0B, + 0x82, 0x0C, 0x82, 0x06, 0x81, 0x01, 0x84, 0x01, 0x90, 0x06, 0x82, 0x0D, + 0x84, 0x0B, 0x82, 0x09, 0x88, 0x09, 0x88, 0x05, 0x82, 0x02, 0x84, 0x04, + 0x84, 0x09, 0x88, 0x08, 0x90, 0x09, 0x82, 0x08, 0x81, 0x01, 0x88, 0x01, + 0x81, 0x06, 0x90, 0x04, 0x82, 0x0A, 0x82, 0x09, 0x82, 0x09, 0x84, 0x0C, + 0x82, 0x07, 0x90, 0x0E, 0x90, 0x0E, 0x90, 0x07, 0x82, 0x0D, 0x90, 0x02, + 0x84, 0x04, 0x84, 0x09, 0x88, 0x05, 0x88, 0x03, 0x81, 0x05, 0x82, 0x0E, + 0x90, 0x04, 0x90, 0x08, 0x88, 0x0C, 0x90, 0x08, 0x81, 0x05, 0x82, 0x0B, + 0x90, 0x0B, 0x84, 0x05, 0x82, 0x0D, 0x82, 0x08, 0x84, 0x0D, 0x81, 0x01, + 0x90, 0x08, 0x82, 0x0C, 0x81, 0x09, 0x82, 0x05, 0x84, 0x01, 0x82, 0x0A, + 0x82, 0x0E, 0x88, 0x0A, 0x84, 0x04, 0x90, 0x01, 0x90, 0x0D, 0x82, 0x08, + 0x90, 0x0B, 0x84, 0x03, 0x81, 0x01, 0x90, 0x0D, 0x82, 0x0C, 0x82, 0x05, + 0x81, 0x02, 0x81, 0x06, 0x84, 0x08, 0x81, 0x01, 0x82, 0x06, 0x88, 0x0E, + 0x88, 0x02, 0x82, 0x0A, 0x90, 0x0E, 0x82, 0x0B, 0x81, 0x0E, 0x81, 0x0A, + 0x82, 0x08, 0x90, 0x01, 0x81, 0x04, 0x81, 0x0D, 0x90, 0x0C, 0x88, 0x0D, + 0x88, 0x07, 0x81, 0x05, 0x90, 0x02, 0x82, 0x07, 0x88, 0x02, 0x84, 0x0E, + 0x82, 0x0D, 0x90, 0x02, 0x88, 0x04, 0x90, 0x0D, 0x82, 0x0A, 0x82, 0x0D, + 0x88, 0x09, 0x81, 0x04, 0x84, 0x08, 0x90, 0x0B, 0x81, 0x01, 0x90, 0x0C, + 0x82, 0x0E, 0x84, 0x04, 0x82, 0x06, 0x90, 0x09, 0x84, 0x03, 0x90, 0x01, + 0x82, 0x0C, 0x81, 0x03, 0x82, 0x05, 0x90, 0x0E, 0x82, 0x0E, 0x84, 0x07, + 0x90, 0x06, 0x81, 0x02, 0x90, 0x03, 0x82, 0x09, 0x82, 0x0A, 0x81, 0x05, + 0x82, 0x0D, 0x84, 0x01, 0x90, 0x0A, 0x82, 0x09, 0x81, 0x04, 0x82, 0x02, + 0x82, 0x08, 0x90, 0x06, 0x90, 0x03, 0x84, 0x09, 0x82, 0x01, 0x82, 0x08, + 0x82, 0x09, 0x90, 0x01, 0x88, 0x07, 0x88, 0x08, +}; + +// level O -- 1400 ticks, 1 hook bytes, 3 fare stages, 256 pictures, 0 landings +// regenerate: tools/stsim/recorder levels/level??.dat O 20260918 +static const uint8_t kGateStreamO[320] = { + 0x84, 0x0E, 0x90, 0x08, 0x82, 0x07, 0x81, 0x0B, 0x84, 0x02, 0x81, 0x09, + 0x82, 0x06, 0x90, 0x0C, 0x81, 0x08, 0x88, 0x01, 0x88, 0x02, 0x90, 0x04, + 0x88, 0x06, 0x81, 0x04, 0x82, 0x0A, 0x81, 0x06, 0x81, 0x0E, 0x82, 0x08, + 0x88, 0x06, 0x82, 0x03, 0x81, 0x06, 0x81, 0x02, 0x88, 0x0D, 0x82, 0x04, + 0x84, 0x0B, 0x84, 0x03, 0x81, 0x04, 0x84, 0x06, 0x81, 0x04, 0x81, 0x02, + 0x84, 0x07, 0x82, 0x06, 0x81, 0x06, 0x90, 0x05, 0x88, 0x09, 0x81, 0x0A, + 0x81, 0x0C, 0x81, 0x02, 0x82, 0x09, 0x82, 0x0D, 0x81, 0x04, 0x81, 0x0B, + 0x84, 0x03, 0x88, 0x0A, 0x90, 0x0A, 0x81, 0x08, 0x82, 0x08, 0x81, 0x06, + 0x81, 0x03, 0x82, 0x0A, 0x81, 0x0C, 0x82, 0x08, 0x81, 0x0C, 0x82, 0x0E, + 0x81, 0x06, 0x81, 0x04, 0x81, 0x0A, 0x84, 0x05, 0x90, 0x0E, 0x84, 0x06, + 0x82, 0x0B, 0x81, 0x0E, 0x81, 0x03, 0x81, 0x05, 0x90, 0x08, 0x81, 0x01, + 0x90, 0x0E, 0x84, 0x0D, 0x88, 0x01, 0x90, 0x05, 0x84, 0x0B, 0x81, 0x04, + 0x81, 0x04, 0x82, 0x01, 0x90, 0x04, 0x82, 0x0D, 0x88, 0x07, 0x81, 0x0B, + 0x81, 0x07, 0x82, 0x01, 0x88, 0x0A, 0x81, 0x06, 0x90, 0x02, 0x82, 0x08, + 0x81, 0x01, 0x81, 0x0A, 0x88, 0x0E, 0x88, 0x0D, 0x81, 0x03, 0x88, 0x0D, + 0x81, 0x02, 0x88, 0x04, 0x84, 0x0E, 0x90, 0x01, 0x82, 0x0C, 0x81, 0x0B, + 0x81, 0x04, 0x88, 0x03, 0x81, 0x03, 0x88, 0x09, 0x84, 0x0E, 0x81, 0x02, + 0x81, 0x02, 0x81, 0x08, 0x84, 0x07, 0x84, 0x02, 0x88, 0x0C, 0x84, 0x08, + 0x90, 0x09, 0x90, 0x0D, 0x81, 0x02, 0x88, 0x0D, 0x84, 0x0C, 0x84, 0x0D, + 0x84, 0x01, 0x81, 0x07, 0x81, 0x0A, 0x81, 0x01, 0x84, 0x01, 0x84, 0x0D, + 0x82, 0x0E, 0x81, 0x02, 0x81, 0x06, 0x81, 0x09, 0x88, 0x0D, 0x88, 0x09, + 0x82, 0x0B, 0x81, 0x0B, 0x88, 0x02, 0x88, 0x0C, 0x81, 0x04, 0x84, 0x0B, + 0x81, 0x06, 0x88, 0x09, 0x84, 0x0C, 0x81, 0x03, 0x84, 0x0D, 0x84, 0x0D, + 0x88, 0x0E, 0x84, 0x04, 0x90, 0x0E, 0x81, 0x02, 0x88, 0x01, 0x88, 0x06, + 0x81, 0x0E, 0x82, 0x08, 0x81, 0x0D, 0x90, 0x0D, 0x88, 0x0E, 0x81, 0x03, + 0x8A, 0x09, 0x90, 0x0B, 0x84, 0x07, 0x88, 0x03, 0x81, 0x0C, 0x82, 0x07, + 0x81, 0x0E, 0x81, 0x08, 0x90, 0x0D, 0x84, 0x0D, +}; + +// level Q -- 1400 ticks, 5 hook bytes, 10 fare stages, 256 pictures, 10 landings +// regenerate: tools/stsim/recorder levels/level??.dat Q 20260918 +static const uint8_t kGateStreamQ[320] = { + 0x84, 0x0E, 0x81, 0x04, 0x81, 0x03, 0x84, 0x09, 0x88, 0x0A, 0x81, 0x09, + 0x82, 0x06, 0x88, 0x0C, 0x81, 0x08, 0x82, 0x06, 0x88, 0x0E, 0x84, 0x0B, + 0x88, 0x08, 0x81, 0x09, 0x81, 0x0A, 0x84, 0x05, 0x84, 0x01, 0x88, 0x01, + 0x81, 0x08, 0x84, 0x0B, 0x81, 0x05, 0x82, 0x05, 0x80, 0x0D, 0x81, 0x08, + 0x88, 0x0A, 0x88, 0x04, 0x84, 0x08, 0x81, 0x06, 0x81, 0x0C, 0x81, 0x01, + 0x90, 0x0A, 0x84, 0x08, 0x81, 0x0A, 0x81, 0x0A, 0x82, 0x08, 0x81, 0x06, + 0x81, 0x09, 0x90, 0x06, 0x81, 0x09, 0x90, 0x0D, 0x88, 0x04, 0x82, 0x05, + 0x81, 0x0B, 0x84, 0x08, 0x81, 0x08, 0x81, 0x03, 0x82, 0x0D, 0x82, 0x0A, + 0x84, 0x0E, 0x81, 0x05, 0x81, 0x0E, 0x81, 0x08, 0x84, 0x0A, 0x82, 0x0C, + 0x84, 0x09, 0x84, 0x01, 0x82, 0x06, 0x88, 0x0E, 0x81, 0x09, 0x90, 0x07, + 0x81, 0x09, 0x81, 0x07, 0x84, 0x03, 0x81, 0x0B, 0x84, 0x01, 0x90, 0x08, + 0x90, 0x06, 0x82, 0x03, 0x88, 0x06, 0x88, 0x0C, 0x84, 0x0A, 0x81, 0x03, + 0x82, 0x0E, 0x81, 0x07, 0x81, 0x04, 0x81, 0x0D, 0x84, 0x02, 0x81, 0x09, + 0x82, 0x08, 0x88, 0x04, 0x81, 0x09, 0x84, 0x0C, 0x81, 0x0B, 0x84, 0x0D, + 0x81, 0x02, 0x82, 0x03, 0x81, 0x05, 0x88, 0x0E, 0x84, 0x0D, 0x81, 0x0B, + 0x88, 0x08, 0x84, 0x0C, 0x90, 0x07, 0x82, 0x0B, 0x81, 0x03, 0x90, 0x05, + 0x84, 0x05, 0x84, 0x09, 0x81, 0x04, 0x81, 0x0B, 0x81, 0x08, 0x82, 0x0E, + 0x81, 0x0A, 0x81, 0x03, 0x81, 0x06, 0x88, 0x09, 0x84, 0x0A, 0x88, 0x0B, + 0x81, 0x05, 0x81, 0x0D, 0x88, 0x03, 0x88, 0x0D, 0x81, 0x01, 0x90, 0x06, + 0x88, 0x07, 0x82, 0x02, 0x81, 0x04, 0x84, 0x09, 0x81, 0x07, 0x90, 0x06, + 0x82, 0x03, 0x84, 0x07, 0x81, 0x05, 0x90, 0x0B, 0x81, 0x0B, 0x88, 0x05, + 0x81, 0x0D, 0x81, 0x0B, 0x92, 0x0E, 0x90, 0x05, 0x82, 0x02, 0x88, 0x0C, + 0x84, 0x08, 0x81, 0x0A, 0x81, 0x0D, 0x81, 0x05, 0x81, 0x07, 0x81, 0x01, + 0x81, 0x04, 0x88, 0x01, 0x81, 0x0E, 0x84, 0x04, 0x90, 0x03, 0x81, 0x0D, + 0x90, 0x09, 0x81, 0x02, 0x81, 0x08, 0x90, 0x07, 0x82, 0x0B, 0x90, 0x08, + 0x81, 0x03, 0x90, 0x0C, 0x81, 0x0B, 0x88, 0x08, 0x90, 0x04, 0x81, 0x02, + 0x90, 0x0C, 0x88, 0x05, 0x81, 0x01, 0x90, 0x03, +}; + +// level R -- 1400 ticks, 0 hook bytes, 3 fare stages, 256 pictures, 0 landings +// regenerate: tools/stsim/recorder levels/level??.dat R 20260918 +static const uint8_t kGateStreamR[320] = { + 0x90, 0x0E, 0x81, 0x02, 0x82, 0x07, 0x81, 0x05, 0x88, 0x0B, 0x88, 0x09, + 0x90, 0x0D, 0x90, 0x06, 0x81, 0x08, 0x81, 0x01, 0x90, 0x02, 0x88, 0x04, + 0x82, 0x07, 0x90, 0x06, 0x88, 0x07, 0x81, 0x06, 0x81, 0x06, 0x82, 0x05, + 0x84, 0x07, 0x82, 0x05, 0x81, 0x03, 0x84, 0x0E, 0x88, 0x05, 0x88, 0x09, + 0x88, 0x04, 0x81, 0x09, 0x81, 0x02, 0x82, 0x04, 0x84, 0x0B, 0x81, 0x04, + 0x82, 0x0C, 0x81, 0x03, 0x88, 0x06, 0x84, 0x08, 0x81, 0x0D, 0x88, 0x09, + 0x90, 0x0E, 0x82, 0x07, 0x82, 0x02, 0x84, 0x0D, 0x88, 0x0B, 0x81, 0x0B, + 0x81, 0x03, 0x82, 0x02, 0x90, 0x0B, 0x84, 0x03, 0x88, 0x02, 0x88, 0x09, + 0x81, 0x02, 0x82, 0x0A, 0x81, 0x06, 0x81, 0x02, 0x90, 0x0A, 0x82, 0x0E, + 0x81, 0x09, 0x88, 0x03, 0x81, 0x07, 0x84, 0x0D, 0x84, 0x0A, 0x81, 0x03, + 0x82, 0x07, 0x84, 0x0D, 0x81, 0x03, 0x81, 0x08, 0x84, 0x0E, 0x81, 0x02, + 0x82, 0x0E, 0x88, 0x03, 0x81, 0x01, 0x81, 0x05, 0x81, 0x06, 0x90, 0x0A, + 0x84, 0x0A, 0x81, 0x02, 0x82, 0x05, 0x81, 0x03, 0x88, 0x07, 0x84, 0x0C, + 0x81, 0x04, 0x88, 0x09, 0x90, 0x0E, 0x82, 0x0A, 0x88, 0x09, 0x82, 0x01, + 0x81, 0x06, 0x81, 0x0D, 0x84, 0x0A, 0x82, 0x0D, 0x88, 0x0A, 0x84, 0x0A, + 0x90, 0x02, 0x84, 0x02, 0x81, 0x09, 0x82, 0x0A, 0x81, 0x0B, 0x81, 0x0D, + 0x82, 0x05, 0x84, 0x09, 0x84, 0x04, 0x88, 0x06, 0x84, 0x04, 0x88, 0x0D, + 0x81, 0x02, 0x82, 0x0B, 0x81, 0x0E, 0x82, 0x0D, 0x82, 0x0A, 0x81, 0x02, + 0x81, 0x09, 0x81, 0x0D, 0x84, 0x04, 0x82, 0x09, 0x88, 0x04, 0x81, 0x07, + 0x90, 0x0A, 0x90, 0x0B, 0x81, 0x07, 0x84, 0x0D, 0x82, 0x0A, 0x81, 0x0C, + 0x88, 0x07, 0x84, 0x09, 0x82, 0x08, 0x90, 0x0A, 0x84, 0x06, 0x82, 0x0D, + 0x88, 0x05, 0x81, 0x06, 0x81, 0x05, 0x90, 0x0D, 0x90, 0x0B, 0x81, 0x0C, + 0x81, 0x04, 0x84, 0x06, 0x90, 0x08, 0x90, 0x08, 0x90, 0x08, 0x84, 0x0E, + 0x88, 0x0E, 0x81, 0x0E, 0x82, 0x0A, 0x90, 0x04, 0x81, 0x06, 0x82, 0x09, + 0x84, 0x08, 0x90, 0x07, 0x84, 0x0C, 0x90, 0x05, 0x88, 0x0C, 0x84, 0x06, + 0x84, 0x0A, 0x81, 0x08, 0x81, 0x01, 0x81, 0x01, 0x82, 0x08, 0x88, 0x0A, + 0x90, 0x0A, 0x90, 0x01, 0x84, 0x0A, 0x81, 0x0B, +}; + +// level U -- 896 ticks, 21 hook bytes, 3 fare stages, 256 pictures, 0 landings +// regenerate: tools/stsim/recorder levels/level??.dat U 44 +static const uint8_t kGateStreamU[320] = { + 0x90, 0x01, 0x90, 0x0B, 0x88, 0x08, 0x90, 0x07, 0x88, 0x0D, 0x82, 0x0A, + 0x82, 0x06, 0x88, 0x0C, 0x90, 0x03, 0x81, 0x04, 0x84, 0x0B, 0x81, 0x0C, + 0x88, 0x02, 0x84, 0x08, 0x84, 0x01, 0x81, 0x04, 0x90, 0x05, 0x84, 0x06, + 0x84, 0x08, 0x84, 0x05, 0x81, 0x03, 0x81, 0x09, 0x81, 0x08, 0x88, 0x07, + 0x81, 0x08, 0x84, 0x06, 0x81, 0x0B, 0x88, 0x0D, 0x90, 0x08, 0x81, 0x01, + 0x82, 0x01, 0x88, 0x03, 0x81, 0x0E, 0x82, 0x01, 0x81, 0x0D, 0x81, 0x07, + 0x88, 0x05, 0x88, 0x07, 0x88, 0x0E, 0x84, 0x07, 0x81, 0x03, 0x81, 0x01, + 0x84, 0x03, 0x81, 0x04, 0x81, 0x08, 0x90, 0x06, 0x81, 0x0C, 0x82, 0x05, + 0x81, 0x08, 0x82, 0x09, 0x84, 0x06, 0x81, 0x06, 0x90, 0x0C, 0x84, 0x0E, + 0x82, 0x0C, 0x81, 0x03, 0x81, 0x06, 0x84, 0x0D, 0x88, 0x03, 0x84, 0x0C, + 0x81, 0x02, 0x81, 0x09, 0x81, 0x0E, 0x88, 0x01, 0x90, 0x06, 0x88, 0x08, + 0x81, 0x08, 0x81, 0x0C, 0x81, 0x0C, 0x82, 0x06, 0x82, 0x08, 0x88, 0x01, + 0x81, 0x07, 0x88, 0x0D, 0x84, 0x03, 0x81, 0x02, 0x82, 0x06, 0x81, 0x05, + 0x81, 0x0E, 0x81, 0x04, 0x90, 0x01, 0x81, 0x02, 0x90, 0x03, 0x81, 0x0A, + 0x81, 0x04, 0x84, 0x02, 0x81, 0x06, 0x81, 0x0E, 0x81, 0x06, 0x84, 0x07, + 0x84, 0x04, 0x81, 0x07, 0x81, 0x0E, 0x81, 0x0D, 0x90, 0x0D, 0x90, 0x05, + 0x88, 0x0C, 0x88, 0x02, 0x81, 0x0B, 0x90, 0x07, 0x88, 0x06, 0x82, 0x0D, + 0x84, 0x08, 0x84, 0x08, 0x81, 0x01, 0x81, 0x06, 0x82, 0x05, 0x84, 0x06, + 0x81, 0x06, 0x84, 0x0D, 0x81, 0x04, 0x90, 0x07, 0x88, 0x0D, 0x81, 0x09, + 0x88, 0x0D, 0x81, 0x0E, 0x81, 0x03, 0x84, 0x0E, 0x81, 0x03, 0x81, 0x0C, + 0x84, 0x0A, 0x81, 0x06, 0x90, 0x02, 0x81, 0x01, 0x88, 0x02, 0x90, 0x02, + 0x81, 0x08, 0x84, 0x01, 0x88, 0x0B, 0x88, 0x0B, 0x81, 0x04, 0x81, 0x0C, + 0x81, 0x07, 0x82, 0x06, 0x88, 0x0C, 0x81, 0x03, 0x90, 0x02, 0x88, 0x0A, + 0x84, 0x08, 0x81, 0x0E, 0x81, 0x05, 0x88, 0x0C, 0x81, 0x0D, 0x81, 0x05, + 0x81, 0x04, 0x90, 0x0D, 0x88, 0x03, 0x88, 0x06, 0x81, 0x0B, 0x81, 0x0D, + 0x84, 0x0D, 0x81, 0x01, 0x81, 0x0D, 0x81, 0x01, 0x82, 0x07, 0x88, 0x02, + 0x81, 0x09, 0x90, 0x0B, 0x88, 0x0D, 0x82, 0x02, +}; + +// level V -- 1400 ticks, 0 hook bytes, 6 fare stages, 256 pictures, 23 landings +// regenerate: tools/stsim/recorder levels/level??.dat V 20260918 +static const uint8_t kGateStreamV[320] = { + 0x84, 0x0E, 0x81, 0x04, 0x81, 0x05, 0x88, 0x0B, 0x84, 0x01, 0x81, 0x03, + 0x82, 0x06, 0x84, 0x0C, 0x81, 0x08, 0x84, 0x07, 0x82, 0x03, 0x88, 0x0A, + 0x90, 0x03, 0x81, 0x0D, 0x81, 0x0D, 0x82, 0x05, 0x81, 0x02, 0x81, 0x0E, + 0x90, 0x06, 0x81, 0x05, 0x90, 0x05, 0x81, 0x0E, 0x82, 0x0D, 0x90, 0x0E, + 0x88, 0x0E, 0x81, 0x03, 0x90, 0x07, 0x84, 0x0A, 0x81, 0x02, 0x90, 0x07, + 0x82, 0x07, 0x81, 0x02, 0x81, 0x06, 0x82, 0x08, 0x81, 0x04, 0x88, 0x0C, + 0x81, 0x06, 0x88, 0x0D, 0x90, 0x0D, 0x81, 0x05, 0x90, 0x03, 0x84, 0x0D, + 0x84, 0x0A, 0x88, 0x0A, 0x88, 0x0D, 0x81, 0x08, 0x90, 0x08, 0x90, 0x01, + 0x84, 0x06, 0x82, 0x0B, 0x81, 0x0D, 0x82, 0x07, 0x82, 0x0C, 0x81, 0x01, + 0x90, 0x08, 0x81, 0x02, 0x81, 0x04, 0x90, 0x0C, 0x84, 0x04, 0x88, 0x04, + 0x82, 0x03, 0x88, 0x07, 0x81, 0x03, 0x90, 0x05, 0x81, 0x02, 0x81, 0x03, + 0x88, 0x01, 0x90, 0x05, 0x81, 0x08, 0x81, 0x06, 0x81, 0x07, 0x81, 0x08, + 0x81, 0x06, 0x81, 0x02, 0x84, 0x0B, 0x88, 0x0D, 0x81, 0x06, 0x90, 0x02, + 0x81, 0x07, 0x82, 0x0B, 0x88, 0x04, 0x84, 0x04, 0x84, 0x05, 0x81, 0x05, + 0x81, 0x02, 0x82, 0x09, 0x81, 0x03, 0x90, 0x0C, 0x88, 0x0B, 0x82, 0x01, + 0x88, 0x0D, 0x88, 0x05, 0x81, 0x02, 0x90, 0x0B, 0x81, 0x02, 0x90, 0x0A, + 0x88, 0x0C, 0x81, 0x01, 0x90, 0x0A, 0x88, 0x0E, 0x90, 0x0A, 0x84, 0x0B, + 0x81, 0x03, 0x90, 0x08, 0x88, 0x03, 0x82, 0x0E, 0x84, 0x06, 0x90, 0x0E, + 0x81, 0x03, 0x88, 0x0A, 0x90, 0x0A, 0x82, 0x09, 0x81, 0x04, 0x88, 0x0C, + 0x84, 0x04, 0x81, 0x07, 0x90, 0x03, 0x81, 0x01, 0x81, 0x01, 0x81, 0x01, + 0x82, 0x0E, 0x81, 0x05, 0x81, 0x06, 0x81, 0x08, 0x84, 0x08, 0x88, 0x0C, + 0x82, 0x09, 0x88, 0x05, 0x81, 0x02, 0x88, 0x04, 0x81, 0x04, 0x88, 0x0C, + 0x90, 0x07, 0x88, 0x0E, 0x81, 0x03, 0x81, 0x0A, 0x82, 0x0B, 0x81, 0x02, + 0x88, 0x02, 0x90, 0x0E, 0x88, 0x0D, 0x81, 0x03, 0x88, 0x09, 0x90, 0x04, + 0x84, 0x01, 0x88, 0x04, 0x90, 0x04, 0x84, 0x04, 0x81, 0x03, 0x90, 0x0C, + 0x88, 0x07, 0x84, 0x05, 0x81, 0x02, 0x90, 0x0B, 0x90, 0x04, 0x90, 0x09, + 0x81, 0x02, 0x81, 0x01, 0x90, 0x0B, 0x88, 0x04, +}; + +// level E -- 1400 ticks, 4 hook bytes, 8 fare stages, 256 pictures, 11 landings +// regenerate: tools/stsim/recorder levels/level??.dat E 20260921 +static const uint8_t kGateStreamE[320] = { + 0x84, 0x05, 0x82, 0x03, 0x82, 0x05, 0x81, 0x0B, 0x81, 0x07, 0x81, 0x02, + 0x81, 0x01, 0x89, 0x02, 0x90, 0x03, 0x81, 0x0A, 0x81, 0x05, 0x84, 0x0C, + 0x81, 0x02, 0x88, 0x07, 0x81, 0x0B, 0x90, 0x08, 0x90, 0x0A, 0x88, 0x02, + 0x81, 0x05, 0x84, 0x08, 0x81, 0x05, 0x90, 0x03, 0x84, 0x01, 0x84, 0x08, + 0x88, 0x0A, 0x84, 0x07, 0x81, 0x04, 0x84, 0x07, 0x90, 0x0C, 0x80, 0x08, + 0x90, 0x07, 0x88, 0x03, 0x90, 0x0B, 0x88, 0x01, 0x90, 0x07, 0x88, 0x04, + 0x81, 0x02, 0x88, 0x08, 0x84, 0x05, 0x81, 0x05, 0x81, 0x02, 0x81, 0x09, + 0x88, 0x05, 0x90, 0x0E, 0x81, 0x05, 0x81, 0x01, 0x90, 0x04, 0x90, 0x08, + 0x81, 0x06, 0x84, 0x0E, 0x81, 0x09, 0x81, 0x01, 0x84, 0x05, 0x81, 0x01, + 0x81, 0x02, 0x88, 0x0B, 0x90, 0x08, 0x88, 0x0C, 0x88, 0x05, 0x88, 0x08, + 0x84, 0x09, 0x88, 0x05, 0x90, 0x05, 0x82, 0x0A, 0x81, 0x07, 0x90, 0x0A, + 0x81, 0x07, 0x81, 0x03, 0x84, 0x0B, 0x84, 0x0E, 0x88, 0x09, 0x90, 0x02, + 0x81, 0x0E, 0x81, 0x06, 0x90, 0x0E, 0x81, 0x02, 0x90, 0x08, 0x84, 0x08, + 0x81, 0x0B, 0x88, 0x0D, 0x84, 0x0C, 0x81, 0x09, 0x90, 0x08, 0x82, 0x0B, + 0x88, 0x08, 0x81, 0x07, 0x82, 0x02, 0x84, 0x0B, 0x88, 0x0B, 0x81, 0x0D, + 0x84, 0x0E, 0x82, 0x01, 0x81, 0x03, 0x90, 0x0C, 0x81, 0x05, 0x90, 0x04, + 0x82, 0x0C, 0x81, 0x06, 0x90, 0x0E, 0x88, 0x0B, 0x88, 0x09, 0x81, 0x0E, + 0x82, 0x0E, 0x90, 0x0C, 0x81, 0x06, 0x81, 0x05, 0x84, 0x0E, 0x81, 0x0C, + 0x88, 0x0B, 0x90, 0x06, 0x82, 0x05, 0x81, 0x06, 0x88, 0x0D, 0x88, 0x07, + 0x81, 0x07, 0x84, 0x07, 0x88, 0x08, 0x81, 0x07, 0x90, 0x0C, 0x84, 0x04, + 0x84, 0x0A, 0x88, 0x0B, 0x80, 0x0A, 0x81, 0x0C, 0x81, 0x07, 0x88, 0x06, + 0x88, 0x09, 0x88, 0x09, 0x81, 0x0C, 0x80, 0x06, 0x81, 0x06, 0x81, 0x07, + 0x84, 0x08, 0x82, 0x09, 0x84, 0x0C, 0x90, 0x09, 0x90, 0x03, 0x90, 0x0D, + 0x81, 0x08, 0x84, 0x05, 0x84, 0x09, 0x88, 0x0B, 0x81, 0x01, 0x81, 0x0A, + 0x90, 0x0D, 0x88, 0x0E, 0x88, 0x0D, 0x88, 0x05, 0x84, 0x06, 0x81, 0x0E, + 0x81, 0x0A, 0x81, 0x08, 0x84, 0x0C, 0x88, 0x05, 0x81, 0x02, 0x90, 0x0E, + 0x84, 0x0D, 0x84, 0x0A, 0x81, 0x05, 0x90, 0x09, +}; + +// level J -- 1400 ticks, 10 hook bytes, 10 fare stages, 256 pictures, 8 landings +// regenerate: tools/stsim/recorder levels/level??.dat J 20260921 +static const uint8_t kGateStreamJ[320] = { + 0x88, 0x05, 0x82, 0x09, 0x81, 0x01, 0x81, 0x04, 0x90, 0x06, 0x81, 0x05, + 0x81, 0x01, 0x90, 0x09, 0x88, 0x05, 0x82, 0x08, 0x81, 0x0A, 0x81, 0x03, + 0x84, 0x07, 0x88, 0x0D, 0x81, 0x06, 0x81, 0x0A, 0x84, 0x05, 0x90, 0x08, + 0x81, 0x0B, 0x90, 0x0E, 0x84, 0x0E, 0x81, 0x03, 0x90, 0x09, 0x81, 0x02, + 0x88, 0x04, 0x88, 0x08, 0x84, 0x0E, 0x81, 0x04, 0x88, 0x0D, 0x90, 0x0E, + 0x84, 0x09, 0x90, 0x05, 0x81, 0x01, 0x81, 0x03, 0x88, 0x03, 0x88, 0x07, + 0x81, 0x0B, 0x90, 0x0B, 0x88, 0x08, 0x82, 0x03, 0x90, 0x09, 0x88, 0x03, + 0x84, 0x06, 0x88, 0x0A, 0x88, 0x0B, 0x84, 0x04, 0x81, 0x05, 0x88, 0x0E, + 0x81, 0x0A, 0x90, 0x0C, 0x85, 0x04, 0x81, 0x0E, 0x90, 0x02, 0x82, 0x0D, + 0x84, 0x01, 0x81, 0x02, 0x88, 0x09, 0x81, 0x0B, 0x81, 0x02, 0x82, 0x0C, + 0x84, 0x04, 0x81, 0x05, 0x81, 0x04, 0x81, 0x04, 0x81, 0x05, 0x90, 0x05, + 0x88, 0x09, 0x81, 0x02, 0x88, 0x01, 0x81, 0x05, 0x90, 0x04, 0x81, 0x0B, + 0x81, 0x05, 0x82, 0x0D, 0x88, 0x06, 0x88, 0x07, 0x88, 0x02, 0x84, 0x02, + 0x90, 0x07, 0x88, 0x09, 0x84, 0x0C, 0x81, 0x01, 0x88, 0x0B, 0x81, 0x0C, + 0x82, 0x0A, 0x81, 0x08, 0x82, 0x01, 0x88, 0x09, 0x84, 0x05, 0x81, 0x08, + 0x84, 0x0A, 0x84, 0x09, 0x88, 0x0E, 0x81, 0x01, 0x84, 0x03, 0x82, 0x0C, + 0x81, 0x0E, 0x84, 0x0A, 0x88, 0x02, 0x84, 0x0D, 0x81, 0x0A, 0x90, 0x0E, + 0x88, 0x09, 0x90, 0x09, 0x81, 0x05, 0x90, 0x03, 0x88, 0x09, 0x84, 0x06, + 0x81, 0x01, 0x88, 0x06, 0x81, 0x05, 0x81, 0x09, 0x81, 0x06, 0x90, 0x04, + 0x82, 0x0A, 0x90, 0x07, 0x81, 0x07, 0x81, 0x0A, 0x84, 0x0E, 0x84, 0x07, + 0x81, 0x0A, 0x90, 0x0C, 0x81, 0x01, 0x90, 0x01, 0x88, 0x0D, 0x81, 0x03, + 0x88, 0x05, 0x81, 0x03, 0x88, 0x01, 0x81, 0x01, 0x82, 0x02, 0x88, 0x07, + 0x83, 0x02, 0x90, 0x07, 0x81, 0x05, 0x81, 0x04, 0x81, 0x0E, 0x88, 0x09, + 0x81, 0x07, 0x90, 0x03, 0x82, 0x0B, 0x82, 0x0B, 0x90, 0x04, 0x81, 0x05, + 0x81, 0x03, 0x90, 0x0C, 0x82, 0x07, 0x88, 0x0E, 0x90, 0x04, 0x81, 0x0A, + 0x88, 0x02, 0x88, 0x0D, 0x82, 0x07, 0x90, 0x01, 0x81, 0x02, 0x81, 0x01, + 0x82, 0x07, 0x81, 0x08, 0x84, 0x0E, 0x81, 0x03, +}; + +// level L -- 1400 ticks, 0 hook bytes, 10 fare stages, 256 pictures, 8 landings +// regenerate: tools/stsim/recorder levels/level??.dat L 20260921 +static const uint8_t kGateStreamL[320] = { + 0x84, 0x05, 0x84, 0x09, 0x81, 0x06, 0x81, 0x04, 0x81, 0x08, 0x81, 0x05, + 0x81, 0x01, 0x84, 0x09, 0x90, 0x09, 0x82, 0x0D, 0x88, 0x07, 0x81, 0x06, + 0x81, 0x09, 0x82, 0x01, 0x81, 0x08, 0x88, 0x0B, 0x84, 0x0E, 0x81, 0x03, + 0x82, 0x02, 0x82, 0x06, 0x82, 0x03, 0x88, 0x0E, 0x84, 0x0C, 0x81, 0x06, + 0x88, 0x02, 0x81, 0x04, 0x88, 0x0E, 0x81, 0x04, 0x90, 0x0B, 0x81, 0x08, + 0x84, 0x09, 0x82, 0x09, 0x81, 0x01, 0x90, 0x03, 0x81, 0x05, 0x84, 0x07, + 0x81, 0x07, 0x90, 0x0B, 0x81, 0x07, 0x88, 0x09, 0x81, 0x0E, 0x88, 0x03, + 0x81, 0x01, 0x88, 0x09, 0x81, 0x07, 0x81, 0x0C, 0x88, 0x0C, 0x90, 0x0E, + 0x81, 0x0E, 0x90, 0x05, 0x84, 0x0E, 0x82, 0x0E, 0x82, 0x01, 0x84, 0x0A, + 0x88, 0x03, 0x90, 0x09, 0x82, 0x05, 0x81, 0x0E, 0x81, 0x05, 0x81, 0x07, + 0x81, 0x0D, 0x81, 0x0E, 0x90, 0x04, 0x90, 0x04, 0x81, 0x06, 0x81, 0x01, + 0x88, 0x0B, 0x81, 0x01, 0x90, 0x0B, 0x81, 0x05, 0x84, 0x0D, 0x81, 0x01, + 0x81, 0x0A, 0x84, 0x0D, 0x82, 0x09, 0x88, 0x06, 0x88, 0x04, 0x84, 0x0C, + 0x88, 0x0C, 0x84, 0x02, 0x81, 0x07, 0x82, 0x07, 0x84, 0x08, 0x88, 0x09, + 0x81, 0x01, 0x88, 0x01, 0x81, 0x0C, 0x81, 0x04, 0x84, 0x06, 0x82, 0x06, + 0x90, 0x0D, 0x81, 0x06, 0x81, 0x0D, 0x82, 0x0D, 0x82, 0x08, 0x81, 0x0C, + 0x84, 0x05, 0x82, 0x09, 0x81, 0x05, 0x84, 0x07, 0x82, 0x02, 0x84, 0x01, + 0x81, 0x02, 0x90, 0x02, 0x82, 0x06, 0x88, 0x03, 0x84, 0x0B, 0x81, 0x0E, + 0x88, 0x0E, 0x90, 0x07, 0x82, 0x08, 0x82, 0x02, 0x90, 0x02, 0x82, 0x03, + 0x84, 0x07, 0x82, 0x05, 0x84, 0x07, 0x81, 0x0B, 0x82, 0x0E, 0x90, 0x05, + 0x88, 0x04, 0x90, 0x05, 0x84, 0x0A, 0x82, 0x0A, 0x88, 0x0C, 0x81, 0x08, + 0x82, 0x0C, 0x81, 0x06, 0x84, 0x09, 0x81, 0x04, 0x84, 0x0B, 0x88, 0x0B, + 0x81, 0x0C, 0x81, 0x0C, 0x82, 0x0E, 0x81, 0x03, 0x82, 0x09, 0x88, 0x04, + 0x81, 0x05, 0x88, 0x04, 0x80, 0x0B, 0x84, 0x06, 0x81, 0x0E, 0x84, 0x0B, + 0x90, 0x0B, 0x90, 0x07, 0x82, 0x0D, 0x84, 0x0D, 0x81, 0x08, 0x81, 0x09, + 0x84, 0x0E, 0x81, 0x0B, 0x90, 0x04, 0x84, 0x0B, 0x84, 0x03, 0x81, 0x03, + 0x88, 0x0A, 0x81, 0x07, 0x84, 0x0B, 0x90, 0x0E, +}; + +// level P -- 1400 ticks, 11 hook bytes, 10 fare stages, 256 pictures, 4 landings +// regenerate: tools/stsim/recorder levels/level??.dat P 20260921 +static const uint8_t kGateStreamP[320] = { + 0x88, 0x05, 0x82, 0x09, 0x81, 0x0B, 0x81, 0x04, 0x81, 0x04, 0x84, 0x05, + 0x82, 0x01, 0x90, 0x0C, 0x81, 0x03, 0x81, 0x0D, 0x81, 0x01, 0x84, 0x0C, + 0x84, 0x0E, 0x88, 0x07, 0x81, 0x0B, 0x88, 0x0D, 0x90, 0x0A, 0x88, 0x02, + 0x81, 0x05, 0x84, 0x08, 0x81, 0x05, 0x90, 0x0C, 0x84, 0x01, 0x88, 0x08, + 0x88, 0x0A, 0x84, 0x0C, 0x81, 0x04, 0x84, 0x07, 0x90, 0x0C, 0x80, 0x09, + 0x90, 0x06, 0x81, 0x03, 0x81, 0x0C, 0x88, 0x01, 0x82, 0x03, 0x81, 0x04, + 0x84, 0x0D, 0x88, 0x09, 0x84, 0x05, 0x81, 0x05, 0x81, 0x02, 0x81, 0x09, + 0x81, 0x02, 0x90, 0x0E, 0x81, 0x05, 0x81, 0x01, 0x81, 0x03, 0x88, 0x0A, + 0x81, 0x06, 0x88, 0x05, 0x90, 0x0C, 0x90, 0x01, 0x84, 0x0E, 0x88, 0x01, + 0x81, 0x02, 0x82, 0x05, 0x90, 0x08, 0x88, 0x0A, 0x84, 0x0C, 0x90, 0x08, + 0x84, 0x0C, 0x81, 0x06, 0x81, 0x03, 0x88, 0x0A, 0x84, 0x08, 0x90, 0x0E, + 0x81, 0x02, 0x88, 0x0C, 0x81, 0x06, 0x84, 0x0C, 0x81, 0x09, 0x80, 0x07, + 0x88, 0x04, 0x81, 0x07, 0x81, 0x03, 0x81, 0x09, 0x81, 0x05, 0x86, 0x05, + 0x90, 0x09, 0x84, 0x06, 0x81, 0x03, 0x84, 0x09, 0x81, 0x0E, 0x88, 0x0B, + 0x90, 0x08, 0x88, 0x0A, 0x81, 0x04, 0x90, 0x0D, 0x88, 0x0E, 0x88, 0x09, + 0x81, 0x0B, 0x90, 0x0C, 0x84, 0x05, 0x81, 0x08, 0x84, 0x01, 0x82, 0x02, + 0x81, 0x03, 0x82, 0x0E, 0x88, 0x02, 0x88, 0x05, 0x81, 0x01, 0x81, 0x08, + 0x82, 0x03, 0x84, 0x0E, 0x81, 0x01, 0x81, 0x08, 0x84, 0x03, 0x82, 0x03, + 0x82, 0x05, 0x81, 0x0A, 0x81, 0x02, 0x90, 0x01, 0x81, 0x0E, 0x81, 0x0C, + 0x90, 0x0C, 0x88, 0x0A, 0x84, 0x03, 0x81, 0x03, 0x84, 0x0A, 0x84, 0x05, + 0x82, 0x09, 0x88, 0x0A, 0x81, 0x06, 0x84, 0x07, 0x84, 0x0C, 0x81, 0x0C, + 0x82, 0x07, 0x81, 0x06, 0x81, 0x03, 0x88, 0x09, 0x90, 0x01, 0x84, 0x0E, + 0x81, 0x01, 0x88, 0x06, 0x81, 0x0E, 0x81, 0x0A, 0x88, 0x0A, 0x81, 0x08, + 0x84, 0x08, 0x88, 0x0E, 0x81, 0x02, 0x84, 0x0E, 0x82, 0x05, 0x81, 0x06, + 0x88, 0x0C, 0x81, 0x0B, 0x81, 0x0D, 0x84, 0x02, 0x90, 0x0E, 0x84, 0x0E, + 0x81, 0x02, 0x90, 0x07, 0x88, 0x0C, 0x90, 0x0A, 0x81, 0x0E, 0x81, 0x0A, + 0x81, 0x0D, 0x81, 0x0B, 0x81, 0x08, 0x81, 0x03, +}; + +// level S -- 1400 ticks, 0 hook bytes, 10 fare stages, 256 pictures, 23 landings +// regenerate: tools/stsim/recorder levels/level??.dat S 20260921 +static const uint8_t kGateStreamS[320] = { + 0x90, 0x04, 0x90, 0x0E, 0x88, 0x08, 0x82, 0x0D, 0x88, 0x0C, 0x81, 0x03, + 0x81, 0x0A, 0x81, 0x0D, 0x88, 0x09, 0x88, 0x0C, 0x82, 0x0E, 0x81, 0x07, + 0x81, 0x0D, 0x81, 0x0A, 0x90, 0x0D, 0x84, 0x01, 0x90, 0x05, 0x88, 0x0E, + 0x81, 0x0A, 0x81, 0x0A, 0x90, 0x06, 0x88, 0x03, 0x82, 0x0B, 0x82, 0x0B, + 0x88, 0x03, 0x88, 0x0A, 0x90, 0x0C, 0x81, 0x0E, 0x81, 0x0E, 0x90, 0x06, + 0x88, 0x02, 0x82, 0x08, 0x81, 0x0A, 0x90, 0x0D, 0x90, 0x05, 0x81, 0x05, + 0x88, 0x0B, 0x81, 0x05, 0x90, 0x0B, 0x88, 0x0E, 0x82, 0x09, 0x90, 0x07, + 0x81, 0x03, 0x81, 0x0D, 0x88, 0x09, 0x88, 0x0E, 0x84, 0x0A, 0x88, 0x0A, + 0x82, 0x01, 0x90, 0x0C, 0x82, 0x01, 0x90, 0x0A, 0x82, 0x0A, 0x84, 0x03, + 0x81, 0x02, 0x90, 0x04, 0x84, 0x0E, 0x90, 0x09, 0x81, 0x0D, 0x90, 0x06, + 0x81, 0x02, 0x90, 0x0E, 0x90, 0x07, 0x82, 0x05, 0x81, 0x06, 0x90, 0x07, + 0x82, 0x03, 0x90, 0x07, 0x90, 0x0B, 0x82, 0x06, 0x88, 0x0E, 0x90, 0x08, + 0x84, 0x0A, 0x82, 0x07, 0x81, 0x04, 0x81, 0x03, 0x84, 0x05, 0x90, 0x08, + 0x81, 0x0C, 0x82, 0x01, 0x90, 0x0B, 0x81, 0x0A, 0x84, 0x05, 0x81, 0x02, + 0x82, 0x09, 0x90, 0x0E, 0x81, 0x02, 0x84, 0x03, 0x81, 0x0C, 0x81, 0x0C, + 0x81, 0x0D, 0x82, 0x08, 0x81, 0x03, 0x81, 0x0D, 0x81, 0x0A, 0x81, 0x0A, + 0x81, 0x0C, 0x82, 0x06, 0x90, 0x0D, 0x88, 0x0C, 0x84, 0x04, 0x81, 0x01, + 0x81, 0x0A, 0x81, 0x03, 0x90, 0x06, 0x88, 0x07, 0x88, 0x0E, 0x84, 0x0E, + 0x81, 0x0E, 0x82, 0x03, 0x90, 0x0E, 0x82, 0x05, 0x90, 0x02, 0x88, 0x03, + 0x81, 0x07, 0x81, 0x0A, 0x81, 0x0B, 0x81, 0x05, 0x84, 0x0D, 0x81, 0x01, + 0x82, 0x02, 0x90, 0x0C, 0x82, 0x03, 0x90, 0x08, 0x81, 0x05, 0x84, 0x08, + 0x84, 0x06, 0x88, 0x08, 0x81, 0x0E, 0x82, 0x01, 0x81, 0x01, 0x90, 0x08, + 0x82, 0x03, 0x90, 0x0C, 0x84, 0x02, 0x81, 0x09, 0x82, 0x04, 0x88, 0x0D, + 0x90, 0x07, 0x88, 0x0E, 0x82, 0x05, 0x81, 0x0C, 0x90, 0x0A, 0x81, 0x04, + 0x84, 0x06, 0x82, 0x02, 0x81, 0x09, 0x90, 0x0D, 0x82, 0x06, 0x82, 0x01, + 0x90, 0x09, 0x81, 0x0B, 0x84, 0x04, 0x88, 0x07, 0x81, 0x0B, 0x81, 0x05, + 0x82, 0x03, 0x81, 0x07, 0x90, 0x06, 0x82, 0x07, +}; + +typedef struct { + uint8_t level; // level letter the ride was searched for + const uint8_t *stream; + uint16_t length; +} StGateStreamT; + +static const StGateStreamT kGateStreams[] = { + { 'G', kGateStreamG, (uint16_t)sizeof(kGateStreamG) }, + { 'I', kGateStreamI, (uint16_t)sizeof(kGateStreamI) }, + { 'K', kGateStreamK, (uint16_t)sizeof(kGateStreamK) }, + { 'O', kGateStreamO, (uint16_t)sizeof(kGateStreamO) }, + { 'Q', kGateStreamQ, (uint16_t)sizeof(kGateStreamQ) }, + { 'R', kGateStreamR, (uint16_t)sizeof(kGateStreamR) }, + { 'U', kGateStreamU, (uint16_t)sizeof(kGateStreamU) }, + { 'V', kGateStreamV, (uint16_t)sizeof(kGateStreamV) }, + { 'E', kGateStreamE, (uint16_t)sizeof(kGateStreamE) }, + { 'J', kGateStreamJ, (uint16_t)sizeof(kGateStreamJ) }, + { 'L', kGateStreamL, (uint16_t)sizeof(kGateStreamL) }, + { 'P', kGateStreamP, (uint16_t)sizeof(kGateStreamP) }, + { 'S', kGateStreamS, (uint16_t)sizeof(kGateStreamS) }, +}; +#define ST_GATE_STREAMS (sizeof(kGateStreams) / sizeof(kGateStreams[0])) + +#endif diff --git a/tools/stsim/recorder.c b/tools/stsim/recorder.c new file mode 100644 index 0000000..f5be285 --- /dev/null +++ b/tools/stsim/recorder.c @@ -0,0 +1,195 @@ +// recorder -- search for a demo input stream that exercises a level. +// +// The C64 ships attract recordings for four screens (H, W, T, X). The +// other levels have none, so a refactor of their hooks had nothing to be +// checked against. This searches for a synthetic ride instead: random +// restarts plus hill climbing over (joystick mask, duration) pairs, +// scored on how long the cab survives and how much of the level it +// reaches -- hook state bytes written, fare-machine stages entered, +// distinct pictures drawn, pads landed on. +// +// The result is a REGRESSION fixture, not a recording: it pins current +// behaviour so a refactor that changes nothing observable leaves the +// trace identical. It says nothing about matching the real C64. +// +// Usage: recorder > stream.h +// Then assemble the per-level outputs into gateStreams.h. +// +// Determinism: a candidate is only useful if it replays identically from +// a cold start, so every evaluation clears the whole StSimT (see the note +// in evaluate) -- stSimNewGame deliberately preserves some cross-screen +// state, which would otherwise leak between candidates. + +#include +#include +#include +#include "stSim.h" +#include "stDemoStreams.h" + +static StLevelT gLevel; +static StSimT gSim; + +void stAudioSfx(const uint8_t *p){(void)p;} +void stAudioNoise(bool o){(void)o;} +void stAudioThrustSweep(uint8_t v){(void)v;} +void stAudioSpeech(uint8_t c){(void)c;} +void stAudioSpeechPhrase(const uint8_t *s){(void)s;} +void stAudioSilence(void){} +void stAudioVoice2(uint8_t a,uint8_t b,uint8_t c){(void)a;(void)b;(void)c;} +void stAudioVoice1Freq(uint8_t v){(void)v;} + +#define PAIRS 160 +#define STREAMLEN (PAIRS * 2) +#define MAXTICKS 1400 + +typedef struct { uint32_t score; uint32_t ticks; uint32_t cover; uint32_t lands; uint32_t stages; uint32_t scr; uint32_t near; } ResT; + +static uint32_t rs = 1; +static uint32_t rnd(void){ rs = rs*1664525u + 1013904223u; return rs>>8; } + +static ResT evaluate(const uint8_t *stream, uint8_t *touchOut) +{ + uint8_t touched[sizeof(StHookScratchT)]; + uint8_t base[sizeof(StHookScratchT)]; + uint8_t stageSeen[16]; + uint32_t t, cover = 0, lands = 0, stages = 0, scrSeen = 0; + uint32_t nearest = 255u; + uint32_t scrHash[256]; + uint8_t lastPad = 0; + ResT r; + + memset(stageSeen, 0, sizeof(stageSeen)); + memset(touched, 0, sizeof(touched)); + // stSimNewGame deliberately PRESERVES flameParity, waveIdx and + // walkParity (the C64 carries them from screen to screen), so a + // search that reuses one StSimT would score each candidate against + // whatever the previous ride left behind -- and the winning stream + // would then not reproduce from a cold start. Clear the whole struct + // so every evaluation begins exactly where the gate will. + memset(&gSim, 0, sizeof(gSim)); + stSimNewGame(&gSim, 1u, true); + stSimSeedDemoBuffer(&gSim, kDemoBufferInit, ST_DEMO_BUFFER_BYTES); + stSimEnterLevel(&gSim, &gLevel); + stSimSetDemoStream(&gSim, stream, STREAMLEN); + memcpy(base, &gSim.hookScratch, sizeof(base)); + for (t = 0; t < MAXTICKS; t++) { + uint16_t k; + if (stSimTick(&gSim) != ST_TICK_CONTINUE) { break; } + { const uint8_t *cur = (const uint8_t *)&gSim.hookScratch; + for (k = 0; k < (uint16_t)sizeof(base); k++) { + if (cur[k] != base[k]) { touched[k] = 1; } } } + if (gSim.activePad != 0u && lastPad == 0u) { lands++; } + lastPad = gSim.activePad; + if (gSim.stage < 16u) { stageSeen[gSim.stage] = 1u; } + /* Shaping: how close the ride came to an actual touchdown. Some + levels (R) only run their hook once a passenger has boarded, + which needs a landing, and a landing is far too precise for a + blind search to stumble on -- so reward getting near one. */ + if (gSim.activePad == 0u && (gSim.spr[0].ptr & 1u) != 0u) { + int16_t tx = (int16_t)(((uint16_t)gSim.posXmsb << 8) | gSim.posXcol); + uint8_t pi; + for (pi = 0u; pi < gLevel.padCount && pi < ST_MAX_PADS; pi++) { + int16_t x1 = (int16_t)(((uint16_t)gSim.pads[pi].x1Hi << 8) | gSim.pads[pi].x1Lo); + int16_t x2 = (int16_t)(((uint16_t)gSim.pads[pi].x2Hi << 8) | gSim.pads[pi].x2Lo); + int16_t dr = (int16_t)((int16_t)gSim.spr[0].row - (int16_t)gSim.pads[pi].row); + uint32_t ar = (uint32_t)(dr < 0 ? -dr : dr); + uint32_t ax = 0u; + uint32_t d; + if (tx < x1) { ax = (uint32_t)(x1 - tx); } + else if (tx > x2) { ax = (uint32_t)(tx - x2); } + /* Both axes matter: the row alone is easy to hit while + the cab sits at the far side of the screen. */ + d = ar * 4u + ax; + if (d < nearest) { nearest = d; } + } + } + /* distinct pictures the hook has produced (screen + colour) */ + { uint32_t h = 2166136261u; uint16_t c; uint32_t i2; int dup = 0; + for (c = 0; c < ST_SCREEN_CELLS; c++) { h = (h ^ gSim.screen[c]) * 16777619u; } + for (c = 0; c < ST_SCREEN_CELLS; c++) { h = (h ^ gSim.color[c]) * 16777619u; } + for (i2 = 0; i2 < scrSeen; i2++) { if (scrHash[i2] == h) { dup = 1; break; } } + if (!dup && scrSeen < 256u) { scrHash[scrSeen++] = h; } } + } + { uint16_t k; for (k = 0; k < (uint16_t)sizeof(touched); k++) { if (touched[k]) { cover++; } } } + { int k; for (k = 0; k < 16; k++) { if (stageSeen[k]) { stages++; } } } + if (touchOut) { memcpy(touchOut, touched, sizeof(touched)); } + r.ticks = t; r.cover = cover; r.lands = lands; r.stages = stages; r.scr = scrSeen; + r.near = nearest; + r.score = cover * 30000u + stages * 30000u + lands * 4000u + t * 100u + scrSeen * 200u + + (255u - nearest) * 60u; + return r; +} + +static void randomise(uint8_t *s, int from) +{ + int i; + for (i = from; i < PAIRS; i++) { + uint32_t d = rnd() % 100u; + uint8_t m = 0x80u; + if (d < 45u) { m |= 0x01u; } /* thrust up a lot */ + else if (d < 60u) { m |= 0x04u; } + else if (d < 75u) { m |= 0x08u; } + else if (d < 82u) { m |= 0x02u; } + else if (d < 88u) { m |= 0x10u; } + if (rnd() % 5u == 0u) { m |= (uint8_t)(1u << (rnd() % 4u)); } + s[i*2] = m; + s[i*2 + 1] = (uint8_t)(1u + rnd() % 14u); + } +} + +int main(int argc, char **argv) +{ + uint8_t best[STREAMLEN], cur[STREAMLEN], touched[ST_HOOK_BYTES]; + ResT bestR, curR; + FILE *fp; + int restart, iter; + + if (argc < 4) { fprintf(stderr, "usage: recorder \n"); return 2; } + fp = fopen(argv[1], "rb"); + if (!fp || !stLevelParse(&gLevel, fp)) { fprintf(stderr, "cannot load %s\n", argv[1]); return 1; } + fclose(fp); + rs = (uint32_t)strtoul(argv[3], NULL, 0); + + randomise(best, 0); + bestR = evaluate(best, NULL); + for (restart = 0; restart < 120; restart++) { + memcpy(cur, best, STREAMLEN); + if (restart > 0) { randomise(cur, (int)(rnd() % PAIRS)); } + curR = evaluate(cur, NULL); + for (iter = 0; iter < 3000; iter++) { + uint8_t trial[STREAMLEN]; + ResT tr; + int j, nmut = 1 + (int)(rnd() % 3u); + memcpy(trial, cur, STREAMLEN); + for (j = 0; j < nmut; j++) { + int i = (int)(rnd() % PAIRS); + if (rnd() % 2u) { + uint8_t m = 0x80u; + uint32_t d = rnd() % 100u; + if (d < 45u) { m |= 0x01u; } + else if (d < 62u) { m |= 0x04u; } + else if (d < 79u) { m |= 0x08u; } + else if (d < 88u) { m |= 0x02u; } + else { m |= 0x10u; } + trial[i*2] = m; + } else { + trial[i*2 + 1] = (uint8_t)(1u + rnd() % 14u); + } + } + tr = evaluate(trial, NULL); + if (tr.score >= curR.score) { memcpy(cur, trial, STREAMLEN); curR = tr; } + } + if (curR.score > bestR.score) { memcpy(best, cur, STREAMLEN); bestR = curR; } + } + bestR = evaluate(best, touched); + fprintf(stderr, "%s: ticks=%u hookBytes=%u stages=%u pictures=%u lands=%u nearPad=%u\n", + argv[2], bestR.ticks, bestR.cover, bestR.stages, bestR.scr, bestR.lands, bestR.near); + printf("// %s: %u ticks, %u hook bytes, %u fare stages, %u pictures, %u landings\n", + argv[2], bestR.ticks, bestR.cover, bestR.stages, bestR.scr, bestR.lands); + printf("static const uint8_t kGateStream%s[%d] = {", argv[2], STREAMLEN); + { int i; for (i = 0; i < STREAMLEN; i++) { + if (i % 12 == 0) { printf("\n "); } + printf("0x%02X,%s", best[i], (i % 12 == 11) ? "" : " "); } } + printf("\n};\n"); + return 0; +} diff --git a/tools/stsim/stsim.c b/tools/stsim/stsim.c index a6b8e88..7791b46 100644 --- a/tools/stsim/stsim.c +++ b/tools/stsim/stsim.c @@ -5,14 +5,19 @@ // the VICE tracer records (see stuff/spacetaxi and the compare script), // so the port's engine can be diffed against the real machine. // -// stsim +// stsim +// +// H, W, T and X use the C64's own attract recordings; every other +// letter uses a synthesised gate fixture from gateStreams.h. #include #include #include #include "../../examples/spacetaxi/stSim.h" +#include "../../examples/spacetaxi/stCels.h" #include "../../examples/spacetaxi/stDemoStreams.h" +#include "gateStreams.h" static StLevelT gLevel; @@ -23,11 +28,105 @@ 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 stAudioSpeechPhrase(const uint8_t *speech) { (void)speech; } 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; } +// FNV-1a over a byte span: the picture the player actually sees. The +// hooks that animate the screen (O, R, V, W, X) change screen, colour or +// charset bytes and nothing else this line used to capture. +static uint32_t hashBytes(const uint8_t *p, size_t n) { + uint32_t h = 2166136261u; + size_t i; + + for (i = 0u; i < n; i++) { + h = (h ^ p[i]) * 16777619u; + } + return h; +} + + +// The charset the game would be showing. The sim no longer holds a +// contiguous 2 KB copy -- edited glyphs live in an override table -- so +// rebuild the effective image here. Hashing this keeps the trace value +// identical to the pre-override captures, which is what lets the goldens +// prove the change is behaviour-preserving. +static uint32_t charsetHash(const StSimT *sim) { + uint8_t image[ST_CHARSET_CHARS * 8u]; + uint16_t ch; + + for (ch = 0u; ch < ST_CHARSET_CHARS; ch++) { + memcpy(&image[ch * 8u], stSimGlyph(sim, (uint8_t)ch), 8u); + } + return hashBytes(image, sizeof(image)); +} + + +// STSIM_CELS=1: every level cel (ptr below the baked set) a ride +// displays, as (ptr, colour, multi, mc0, mc1), against what +// stLevelCelList bakes for the level -- a cel shown but not listed is +// built and drawn interpreted mid-ride on the real ports. +#define CENSUS_MAX 64u +static StCelDefT gCensus[CENSUS_MAX]; +static uint8_t gCensusCount; + + +static void celCensusTick(const StSimT *sim) { + uint8_t i; + + for (i = 1u; i < ST_HW_SPRITES; i++) { + StCelDefT d; + uint8_t k; + + if ((sim->frame.enableMask & kStBit[i]) == 0u || sim->frame.ptr[i] >= ST_SPRITE_PTR_FIRST) { + continue; + } + // A pointer with no bitmap (a hook enables a sprite a tick before + // aiming it) draws nothing on every port -- not a cel. + if (stSimSpriteBitmap(sim, sim->frame.ptr[i]) == 0) { + continue; + } + d.ptr = sim->frame.ptr[i]; + d.color = sim->frame.color[i]; + d.multi = (sim->frame.multiMask & kStBit[i]) != 0u ? 1u : 0u; + d.mc0 = d.multi ? sim->spriteMc0 : 0u; + d.mc1 = d.multi ? sim->spriteMc1 : 0u; + for (k = 0u; k < gCensusCount; k++) { + if (memcmp(&gCensus[k], &d, sizeof(d)) == 0) { + break; + } + } + if (k == gCensusCount && gCensusCount < CENSUS_MAX) { + gCensus[gCensusCount++] = d; + } + } +} + + +static void celCensusReport(const StLevelT *level) { + StCelDefT listed[ST_LEVEL_CELS_MAX]; + uint8_t n = stLevelCelList(level, listed, ST_LEVEL_CELS_MAX); + uint8_t k; + uint8_t j; + + for (k = 0u; k < gCensusCount; k++) { + for (j = 0u; j < n; j++) { + if (memcmp(&listed[j], &gCensus[k], sizeof(StCelDefT)) == 0) { + break; + } + } + fprintf(stderr, "CEL %s ptr=%02X color=%u multi=%u mc=%u/%u\n", (j < n) ? "listed " : "MISSING", gCensus[k].ptr, gCensus[k].color, gCensus[k].multi, gCensus[k].mc0, gCensus[k].mc1); + } + fprintf(stderr, "CENSUS shown=%u listed=%u\n", gCensusCount, n); +} + + +static void celCensusReport(const StLevelT *level); +static void celCensusTick(const StSimT *sim); + + static void printState(const StSimT *sim, uint32_t tick) { uint8_t k; @@ -55,7 +154,11 @@ static void printState(const StSimT *sim, uint32_t tick) { for (k = 0u; k < ST_HW_SPRITES; k++) { printf("%03X%02X", sim->frame.x[k], sim->frame.y[k]); } - printf("\"}\n"); + printf("\",\"scr\":\"%08X\",\"col\":\"%08X\",\"chr\":\"%08X\",\"bd\":\"%02X\",\"bg\":\"%02X\"}\n", + hashBytes(sim->screen, ST_SCREEN_CELLS), + hashBytes(sim->color, ST_SCREEN_CELLS), + charsetHash(sim), + sim->borderColor, sim->bgColor); } @@ -63,11 +166,12 @@ int main(int argc, char **argv) { FILE *fp; const uint8_t *stream = kDemoStreamH; uint16_t len = (uint16_t)sizeof(kDemoStreamH); + uint8_t want; uint32_t ticks; uint32_t t; if (argc < 4) { - fprintf(stderr, "usage: stsim \n"); + fprintf(stderr, "usage: stsim \n"); return 2; } fp = fopen(argv[1], "rb"); @@ -76,11 +180,29 @@ int main(int argc, char **argv) { return 1; } fclose(fp); - switch (argv[2][0]) { + want = (uint8_t)argv[2][0]; + switch (want) { case 'W': stream = kDemoStreamW; len = (uint16_t)sizeof(kDemoStreamW); break; case 'T': stream = kDemoStreamT; len = (uint16_t)sizeof(kDemoStreamT); break; case 'X': stream = kDemoStreamX; len = (uint16_t)sizeof(kDemoStreamX); break; - default: break; + case 'H': break; + default: { + // A synthesised gate fixture for a level the C64 has no + // attract recording of (see gateStreams.h). + uint8_t g; + for (g = 0u; g < ST_GATE_STREAMS; g++) { + if (kGateStreams[g].level == want) { + stream = kGateStreams[g].stream; + len = kGateStreams[g].length; + break; + } + } + if (g == ST_GATE_STREAMS) { + fprintf(stderr, "stsim: no stream for '%c'\n", (char)want); + return 2; + } + break; + } } ticks = (uint32_t)strtoul(argv[3], NULL, 0); stSimNewGame(&gSim, 1u, true); @@ -101,10 +223,16 @@ int main(int argc, char **argv) { StTickResultE r; printState(&gSim, t); r = stSimTick(&gSim); + if (getenv("STSIM_CELS") != NULL) { + celCensusTick(&gSim); + } if (r != ST_TICK_CONTINUE) { printf("{\"tick\":%u,\"end\":%d}\n", t, (int)r); break; } } + if (getenv("STSIM_CELS") != NULL) { + celCensusReport(&gLevel); + } return 0; }