From 73931629ae9ae8883ab95453d45c56f7f418a9f1 Mon Sep 17 00:00:00 2001 From: Maddo Date: Sun, 5 Apr 2026 18:27:09 +0200 Subject: [PATCH] deepened understanding --- .../idata/01/~00000015.db/change.data.gbf | Bin 98304 -> 98304 bytes .../idata/01/~00000015.db/change.map.gbf | Bin 32768 -> 32768 bytes .../01/~00000015.db/{db.41.gbf => db.42.gbf} | Bin 24461312 -> 24461312 bytes .../01/~00000015.db/{db.40.gbf => db.43.gbf} | Bin 24461312 -> 24461312 bytes Crusader.rep/projectState | 343 ++++- .../00/~00000008.db/{db.33.gbf => db.35.gbf} | Bin 81920 -> 81920 bytes _tmp_parse_combat_dat.py | 166 +++ _tmp_scummvm_attack_process.cpp | 1203 +++++++++++++++++ _tmp_scummvm_combat_dat.h | 75 + crusader_decompilation_notes.md | 22 +- crusader_segment_coverage_ledger.csv | 6 +- docs/combat-dat.md | 484 +++++++ docs/entity-class-family-split.md | 170 +++ docs/entity-dispatch-entry-class-layout.md | 244 ++++ ...entity-vm-runtime-owner-resource-layout.md | 169 +++ .../ghidra-mcp-class-lifting-endpoint-spec.md | 439 ++++++ docs/map_renderer/editor-object-survey.md | 35 +- docs/map_renderer/trigger-usecode-links.md | 2 + docs/ne-hole-filling-priorities.md | 9 +- docs/presentation-callback-broker-layout.md | 207 +++ docs/raw-0008-000c.md | 12 + docs/remorse-class-candidate-inventory.md | 115 ++ docs/remorse-class-lift-index.md | 94 ++ .../remorse-cpp-compatibility-header-draft.md | 159 +++ docs/remorse-cpp-decompilation-plan.md | 256 ++++ ...remorse-first-class-authoring-checklist.md | 139 ++ docs/remorse-rebuild-abi-notes.md | 212 +++ .../remorse-toolchain-fingerprint-evidence.md | 175 +++ docs/removed_items.md | 2 +- docs/sprite-node-class-layout.md | 165 +++ ghidra_mcp_wishlist.md | 25 +- plan-mid.md | 334 ++--- 32 files changed, 5007 insertions(+), 255 deletions(-) rename Crusader.rep/idata/01/~00000015.db/{db.41.gbf => db.42.gbf} (99%) rename Crusader.rep/idata/01/~00000015.db/{db.40.gbf => db.43.gbf} (99%) rename Crusader.rep/user/00/~00000008.db/{db.33.gbf => db.35.gbf} (99%) create mode 100644 _tmp_parse_combat_dat.py create mode 100644 _tmp_scummvm_attack_process.cpp create mode 100644 _tmp_scummvm_combat_dat.h create mode 100644 docs/combat-dat.md create mode 100644 docs/entity-class-family-split.md create mode 100644 docs/entity-dispatch-entry-class-layout.md create mode 100644 docs/entity-vm-runtime-owner-resource-layout.md create mode 100644 docs/ghidra-mcp-class-lifting-endpoint-spec.md create mode 100644 docs/presentation-callback-broker-layout.md create mode 100644 docs/remorse-class-candidate-inventory.md create mode 100644 docs/remorse-class-lift-index.md create mode 100644 docs/remorse-cpp-compatibility-header-draft.md create mode 100644 docs/remorse-cpp-decompilation-plan.md create mode 100644 docs/remorse-first-class-authoring-checklist.md create mode 100644 docs/remorse-rebuild-abi-notes.md create mode 100644 docs/remorse-toolchain-fingerprint-evidence.md create mode 100644 docs/sprite-node-class-layout.md diff --git a/Crusader.rep/idata/01/~00000015.db/change.data.gbf b/Crusader.rep/idata/01/~00000015.db/change.data.gbf index e25785239b7cd7cea3048413abbbefaa9e7d293e..c362e08ab40d36923c2909dc1a5e1137102d6e99 100644 GIT binary patch literal 98304 zcmeI)J7}GC7{~GVT$0mVPV&CVX-vF6)>~7fO`4#o8Dgx6H!5Bh5s^rPwx$U*R;(a8 zh&YL3x=6sK;NYS-=<3wP!9s_E0Tlu+hq|~({5|jce^7$Bh^52#f%E*%?|-iSa5}y@ zJUuftH#v3a$$=jx7A{^M$g-@KWe;b&8r^3QKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z;93dv>4Vz3>_4l`etz-HJ0HJy^jdu~pMwAb2q1s}0tg_000IagfB*srAbWOsQ*ZdrpISM$u&igzl>5knUuK`4HmCI4(DUa`zq-D< z=S|9&*Ze*D{XO1VUOl_=_g9bV5x?c>)%q$Y-#od#7GI+7Fa4L-`&Ivb%j&wKKdbGz z{=a-A%Z@MkGZ+LAKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#~EzXck8{r~7cU;p1amOkB| zqZ$0H%FO?`fTsOum07dO#;MeMQhy`$-YV;>skhSO(f3vT>Er3~zV!H7>iy~QXHp-i zvi3#lgH_hMskc+Vn)*=czokB0W&7*Y^D3J!re0LpdMWjhDjQc)_rJWLX)RXCMgRc> z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0;J*qq{o4Myz1RPnzis>1pCY|#>ZX-jeJN7ar(s$; ze<4!Ur)gR`|1?t7r^mE({(YpXPp@g^^TkZ0s!z+bbpA)As?R>t${+84EK=2{&$M)Y zB~sO=-?Z{i>i0#e`V5#>zBRrSsp>OmTKU$*dZemP+qCivjSnMLeTGaczcBVtq^i%b zY319+caf?-x%t{2sh+R&Lrq~?`S$)VBUODyOe^0WI~=L%GiqA-PVKo!RiBP&8?WukjNWNt zH&WHdOskk5+KE*4x!tsi`Oyy|RecVaR`Gt{vyrMkcbHbOS^qgw)#pyrDmEKOBUOFw zGOc2>@8w8UpSw+KWL{q^rNZYP(;B&4FMq$DeyBNUS{)nwF;dlM(zJB`OQfpLA=A=% z>EP*yntM&FW9=s*Reh#ROXsgds``}we{^2{o;|DM?0(aojt!N+XU}wA{$4xQIbS{A zIiJp_%RXKGdppWBGv)bwt^8edlZ|v0tg_0 z00IagfB*srAbwd$-pur@wc;*~-lCv`LE5qKKJ??I5KfiMxr2R}nl4dD@G1=n@nX zty`UnP7NVD1a|S_x*i8u;=r8p26NfEQ+#Glr{IN_op6COidnt zZG1}~U4H*)8vd83o!w8rE<5GZ`kQKQ-;1{EnTq2$mA#$A>5Pw)zyI@azTx_B^X_=F zaDauR6M4&t9WSxJVtXd>!ZX{m_WQc+{AIAP*eUPSvtA14%Da5Sebt)FKM^zoL&O&Z z`iUBq^a#SdPTf_blAIvS=fTHnRFW5j>AFodD!C#E)1CN7w|Ia_uOLkK!KNCO6a-<; z(~s1sq)!k*$=On)l72x1rObvJl^8)_KUSl*@Nt58xYK{8<_L;{2x%@te|M6u>LYE)7ZMDcejezi15a9t3g z^!`z!k{g1+{#T7kh6I8AO!)jfK;)(%LMhCvQOU3%u&=36$t^)(KT@NT5kZ8~7yl<{ zjz9zvO88zKzkh7yc&H{%9Kx2-c(1w19m94Xi}&Z=OSRo^C*A*`wtFS%qd(MkKe(86 K{h3a6-Te7z5{t)?1BDaKLRpmmb-&rz|IpsofdK{t z1VAiCAcO!11_qD_2Ll5Okl^3~iZg>{9DrPqG8PaID9y;g#=+4bz`(??d9Q=BJpl1a B6wm+w diff --git a/Crusader.rep/idata/01/~00000015.db/db.41.gbf b/Crusader.rep/idata/01/~00000015.db/db.42.gbf similarity index 99% rename from Crusader.rep/idata/01/~00000015.db/db.41.gbf rename to Crusader.rep/idata/01/~00000015.db/db.42.gbf index 02cb60afdc76575fb5af825897a847c385ce6660..13bbb5f9aeef562e12f493eafa9921d176029000 100644 GIT binary patch delta 18886 zcma)^2Y3|K-v6g;LI^FPLr5|S=_DZ`p$bw|L~ICxfIE&>fADaYnOG;n4y+7OWPw2FO-Z@P%3gz z8cIjKQ3lFHStuLjpj?!P`k;JNfC^DxRD_CAKU9KBQGYZ54Mc;`U^E0BgNCAE=vXuy zjX)#OD0Cb;9-V+jqZ82>bP^hiPDZDoQ_*SYbTkf~fySc==u9*bO+sg($!H3iiq1yo zplN72Dnm0+Ihu(oP$deYDl`ktM%CzCGzZn7xu_P^p?PRNsz(hdgc{KTv=A*qi_sEv z9y%XgfG$K~)PycV7o(-<60{60M=Q{!=rVLUx&mE^u0mI%m1q^Z23?C*qwCQ1=mxX~ ztwrn5jp!zHGg^-}pk}lY-GXjK|3J5)O=vT^9o>QMM0cUP(LLy1bRW7OJ%AoW521(A zBj{1|7-~U}qb=wO^dx!;ZADL`2-=3Wqi4{w=sEN}dI7zNUTR5bdHL5d<`a*$dFAVv z@a1&z+%mBev0h^Bu$(`{z7g9e_J-IqVh@YmCU(78SZuD?*lgvt>x?D@)_UPD7}!zI;}&Y+ij`MX;fvtUg#(R#V*&Dys}OgzD=SlN7<0 zBvb$6xfS)n@=(w{)3DF|ucwyJn-{FD^iQ3$;)&c>(5o#^nhewth-nb zGxF)eo@VwK^Q=hxww~sfojNRhdrZQSSQ!vf?Apk8Z}l{r(pFylLr)V+G9wrN(92vA zZ$7*=KH0o45at!5&kDogk_9Om!*S*YKM*21{=J7xeV37ygBqnW_57yn3KkWgZTI~bo`Oq#K80$k~91yQS z-8+8fwsoYk9KY_j^@sL?ALtq+krU$zuCZ*Yf@GgVdI%SSW?@3hhV?LN^ z!1X>@^pfO|4>3;y$9S+$uIaVf2XhrrSlJ=-ZvHH;n~`q2GeV1x(sZNuokR)H?ueWb!9puG>lacI@#P2-@Ny$R5Q1E??<)f!8Ju`v)V-N>XvLyiwT>vj%%#1 z57vg91;P62s_I~+!vnLju_9RSRF%)Iu379<)zv$p*+HkKuA;ogDO%L8WPmfld$=B7 z9x5N_Jzt&qjlssCQ{CXqty>VBONxCf>S}o4R@c=Q%np_>SUkk3tga7MgdCEo3D!3_ z)wLCqYZ@zq&Pk!*+_F=Gv&w5u<=Qjr#?@8VhVuXY$7fZCW*?^spHy2_m+zcdU%sF^ zwAjx`+4#oVd}qxcAH0)g=Q|^tKRI^1*{ONokPc?R%x_Musxo^F$al(XE8Rj>l{bV6 z%6ZB=v%FNCike_~ZR0#=cCcn1l~5jXii=7H4Jj_JTC+W4R+mUza3A_-w=|z^gZHcX$?+8FcjkE3#!YVHS@cV=sG=LidE|@ zEU%f<;FhqVd~VQjbLeHoDW6qdUF(GE912Pnqy`+f254vh-N>mA&gCTT<5bkuhG@)& zzV7|j%rBnRIdXS?vRT&2ERD1ok!=1Rzb5XAt{v9wIQVd8Wa!Ds<|T=dBj+TWhkIE| zu5RA7IN6+TExD@MS(0p~SWB*4S;PUBB9ebGBMteb%7H8%r;G=}%!VLy8iHmnU$ zi$*mA%)xH3HUd0#J-Ed%hVqE>X~jd}q{Qh3oYO5RoA0bH_H;bmaD!YjbB3a@cRsg$WyZ z_j7|)6R{>_nfp7gjH${pv23v%v0Sk{u|8t?Vg+J_VtvJm#EQlGnUR>P67#nnRJ{?{ zNnwrI-eV_bjCUtH#53h-@7S;#f%6p>07DfP@=Vd*YcB%+p)rer8462)5eoFrzaBj{ z=?;5bqiD`y(VFI9*g%aTr$gx%Zm^pG=4THs0?4fg7XzsZOL=hYQ=o$%vqoW=|9p}1 z8}@P@7IaiM*eigi6fOlQl^1gvaJ&M2e~9*Sn@&CWeT}&i;O*88_Eo@Gg{yg1{Hd@K zc-Fl?pV+G)(szS>4WLuKO;Uq&Fxabkay+A_T?fz@-Zj?)(aO4k$HTughC&b6s<0M_ zUb9XfJ^eN6MjjT?8omj*OJi;Z&QVwobXM5Fv*LM$WNeJgOk z4`G}BQc|T+w*lQ1Ht|GwQDHL>t<>9r<22?Drg{dy8|*uQ`xMB3@eGB#fh>i4nCZV1 zkovdph4{n`Hdhx_E0E2iRD}na>c3EU5YR%{4*~NuhJM*MN8u5s`mYrp1)_EO7%)d4 zJ$4J!N6%w46zo;l0?>kPu%7_VPJB!1~MxZJ^;G5OnLtB^Qn*EKS>%;QLxS<7x!}SS+I#Ojr+bYne$L#f?QweadLMd>i z2P5RhLq1oqfz1l7fD099=pkc#u&B==$pT>`VT`0OxY3ImV-x_VDcl11nHytlXIlE% zkBQF#_9={JirKDkCUB?1YNn1o3Ji_@Pbtt={hN(2c_0h%A2N6ND?4M_&@`ok{PWt5 z0+JLcM5(E;j;Z5-2X5csb<|C5GD#Z!Zcr!&Dip>8{zBYjYdPgF_E*a$Thx8AzY;gu zV$>8b@-MQpfD;sEGeP(znj8}g-0#Q43`orUx@5bA?ZMFx#CC z3Oo7~pl26+zxE*z1Ne7c6gUa+ zmKMVaO_D<1``&q%y3o%Dc^%D5@F6gOaeL5%RdQOczizckx=d~ljbRSY@jJ#UgJ~n@ z9zVuB1qdoIC*_$rI!vaa*bvpWLAeF)HPvr-i3C7Hkb5R(k}i)yQ4 z&IX>;7$&UD8-0kK!Mdm416Re;#u?$IepD-(GGmeg^Hc8`KD16_+0svYtrR@1)Qedg zpf{y;QDFXa4*3uh2i)RA?7hJG9&D5ZQr}jH9}QigMAfDGcim_*ucr3&W6a}O-F!uX zL6D-!$#?&hiAKxk$Me7v@kH~6Kb*RCq@YmEPPjMHTX0eV_TUju#rkrJ=sTsqH z@>y1yJ6Fz>g*9&tr?0G=Mb~`X#hjqOm{T7NHP+WQIAnnJxVl=by2`y&Z(d$Q1}hh{ zv{qkV%>q+ZP5CTmW_5_Aq@tptLC(w|D^HSo&}nRFWEn3Mte;z5%ip$~isn7j%S>nG zf?)$qY>?Psu_0o|hz%7RCU&gYaIq0$BgICE9Vd3Y*a>2z#ZD9(BX*M5Sh17EP7ym* z>@=~{&B%gbQaV6{cSp1y_pIiA@q?lo4=ec$^sdkBdpy%MXi9 z7aJj#iuE}v#xqF9H%5mr`qYTgJB>aJCZkUxmiL30oR_yvY?ILmUnYl)7b9<^SXT1| zOUIeX;YJ|$Ofh-><;wLrKZ;3ya<+-lS&f{_#U%4Nr;0I*jqG2%}U>28ngW zvc4C4N9-Z7>%^GPjI7her1X5dqL`V7In3N9woz=M7&C~G$y8`$cE&P(7JEzVez9xC z8pNiGNm}k7rh9+OQE%QfjNbQ&T_Y9}J4IC zN|N3VOJn{q(moPi@hez zoGKNZdXbp4PwH^7?pVq}vFF5Y7L%G`3c%8_WU1ifH^goes~3}-B=Ci+ z#H59K4Hc6tB+-6pMv`=tq==YQYf?~5&Pj65Nr<#88)r`MB4z6pdFqx5(>61EO=x5- zdBVJUCW*2M3xmOVXYxnrk(CR|Yb)e>g8gCSr<=x^T}-okv%?~FQsm6_e-24&NsvGJ3=4V3hE4YvcBTR?F|4Bkw;%eF!dJkh3jYKq zDA2+~J189DMQ)qIVc>juC6i!(1LbSf5#HA*h#PFW^N{Nmz5}EiOR#DCA#J>4!#>Kp zTQomE0MhRz*gpc(yko;l5Z+dYo!l)5HdRr2yK^0+g)pA@}~A8gQVUngZ$e)xHoQV|}9y)*Fb{PX;hh zuc3z&(goZQ$O4{Mpu-hju8;%h@Cf8m{=yQCViqX)Q=t#=ssec~xK5z}2q`d`78EP= zc|b@0JrZGy{Ht}qUZGo<|_;U&QPGxeWG_A1SD$A zV4kbrD)6w$i>7uAuufx!0*eKt{sY6HDH?SwFj!$Y&_iJa59ePLMgn^jMgdzC7y!BJ z6pjZLE1UpKQ5X#j_U9iXa3aLGazkJYPxk`~Cjl=gj0H9;oD3{iI0dLuI29PJ!1E}F z;pc|H>AV_zr7({2XTPFE?Xo|^3-Yp^eYpbtCHq{3Gl8)R69Gn?8v@KDS>GwpNweNm zm<&9mKn-L?Q%ju+MU!Jhx?An!)WA7Fd}PDu@#g&0=Bo~zZdOEUtHztB^yvRzpM4J3 z{BZ0`96H6*|n+lTyAm&+PSskM6+W|SS!}1r)E!=JP_;} zfIi^{n@eQhhzGU6CWSg6s4x%6RhZ8k)b|SYz!rrDV39&2T`^PZ#a;m9Y1BgAt-ey= zf{bev=vo=HuN&+oK%v5UykZ?tI3I|X?gD^9xxu~=$n=iGhD{dIKi8-x;1-38fSC#x z`)_5!_EO$pUelOMfMp6Cq%pMIAZx(RCkiWoyA&=3ssI^^hJ6{-Popm9EllSISp-gf zNn@@AniQ@Am<8NmU(LJOA%&H|g9@tvs?rUz2Asl_;K8-LvHjrQAC^Vnc!kl`IE*fttZM-*uaLC zhZULueV*7Gfi#Vwt$KW{a4SF}Zjd$L9t>L#ZUbT!HnG*^Q6IwgW~fP{ZU>4L?qI9S zmkM_RH!IML-J(5^8tB$pWA0%?%exBq0uZl%{jEQXd1DZrmViz@2+2JkExeoeEokWjYnhB5-**l%f2 zs>%(q-vK)AW4{NOD7~1YK()dTz;Ox;gLa*bmZrFpb%7fO4iAK77oPd2ANU1N>oa7( zR)aIIoK0Dk{P*0hhOwQin0FsXr1>_ zV6OL3o9WKs^WBg3b!=`T^`rowOkA1&`Xm!O%Z%i|KH2=Z$UM5W%@lK!86IKqy{?Z~ zTg?3%WPb*l(Vxl9=zp2mJh8LHhKe~@=^tV;XiJ$Uj8f)yqx4d-xneS{mJSh15sSgx zKS7o-x{VU%P@{xrkWsQ!=G79J^h#txD(NpK({R6^#Xb~!Ozax5g<{jhP7o^+i^GcP zy+*N2*u`?P`<1L%GF>d0E*>V9Di)6w(QS>QePYjJ;i8RlC`lJdu8X8c7fF2-wZr-z z7JEmG8QI&1(sz*@$;o|>5lhAj{}lT~jH%WrykG1Ju{mN=IQOemVKc zWs6BeTTWOl(aB&!*c)r^f|O=8o3Bf**vY;zBRdf*BLx=w*~GYM7*Nb`;ji!9_z<4Lf>G)MlE9vId_ z;1UG}LH=n9bhG?!3QOo{(QKawM6*ptiGF9dE&!wzC0G{%GCC#5vQyq3_mCs236eID zU|j@IUN>0uhCG>MBv?xU>E047a+$l!J2tFkz;y~VWb|9MMIXpb(U?o=x?dp8c7^)xZM^4A*Q5?*?lXFh+riEnCKv1nXLQ{D
y;=#)=Y)#fjosZ41^yQ7;Kqa71jY>2}t`!-~^4iiIEY#=4RkAjp2qFG`btC z4P2ekU#Xb^a#Ud>@VEla(>q$cTY+f1{sV~C&uxsM-}IVIKtzFQB%MCw2J3d9#5-nl zEYnzeCylz3FO54D?gFA6`fgyP#?ax?qJ80BzB7KVG4}x^;s)87>M*Q4psk!@g$Mc0 zDA%~PCp%M9DZCr3hk@A&j{wnzc$6=VUup~$n!>4Wuv!4diwD%2`|~s*YYX2QqgBFl zG5JaFEW_d;d69d6IqxZGutw3q$+iMBTCZ0XsL)=~YqkN$Xv}uLGk&k|3_wk|!Fm>G zPheL1z><6Gw*iIKXrIO-a`=zU;{Ui1Nwsz9N;e5&vfKzDb8^)V3bGSp|6e2v-1 zx6Z=~p8#|D9}ke zZd0JDJ6@vjPhhyh*Ob4brF4idq|Ymm=fvd--+@yV z{s6iu{KR@;T< z2pd$#N4~{VdPb&KO*5a4JMRDUox&m6zc#3P-~5jHn^q%5b<@oESDD9eZ8P26k`y_b zG~H}SYt1YrA@=*`8N;TVSv~dZXY6+lAOH0W%h+#MHgQ6vX;_(gnPpzVPT4YZq#c&8 znMFGd?_{~S$lvR8B$nDL^7mjIiQT}PvA-qA{rO_wD(@QFjrWzO6(Yc;3d|6NWj@#p z=K}xtPd~@dPzAjuCTt{10PXd8iyi^?aPm7rev?x>qOBCh< zUKtXNn8iSx#$3od#V#M>YJj`ET~dig+*OdK5XZR7o8X@pe-9ArL)${$FkV-<9=J(i zBj9Z}@y1KeZZ9UzBB#7kyQPb>m|JtE_z-BK{5e@Z$&RONZ$D0ATnx349ncu+IO|y- zS}l_I2p{5Z1KfIKl(yc%E5ZAIOxrN*LH@AGUI2$P z5)%SUtm!j+h&c`LHpwL>#4xM*n}re+S}{qbZT3%#<5K^3k%Rj!oC<`Kt#x64lecD93mV=%q38)cwf^ zi%F`-MuigrzhmUcp9gxJ5phl|GY52x9GY5Y_Uc+wGrB(LFSxirwsU`~C+jo?^W+C} zEPk;2r+l$&Ig*7Ieg!6LGc|P!|Jn}3G6}nl+-*fp!(4t!=KpMuALvzbecjA+Z#|lnuWpI&fpwc}}EGE@dI(>>3m6R837aq~jI8v4KNw@7?sjXSCt(JELZnhuGg$-&k8qRk-U&Za(MA z-Tkm~G8KZ&Fe8)amzxh>ZJx9~ex{k1BEL?PzfVc9TQT!b@Q*|8->inrG&Ag#3+$Qf zcd{$QD#e0gRbsQmW{XveohvqneNlFexq7I1AG3XpS=}MB^z<6@{-l-Zb81X%u2`*D zo!C6F`C|2A4Q3>LPRKmi**wa&s*w45>ovPukLVrw{j!j`JS9?gN61{&y7{*ULuT7G zHxHi{*PQfF$lMrt5g&D`xp`}RquD=hgjb9uyeV`)-#a#zFxhseySrh@)x4?gRagn= z6Me}lAU_h0n`J)KzMaD<@|Qk~e@8U8V{eOdvb56wu`}lH>va1ENh0!ml|%-Po_JbW zaZ!0uS@EFK;^w+LQ_UXXfA=l_?@4^R+)aWfpd?Z3B~evWRy1H{f60x%0Q%ql(}=$% z=Nyjrl8dn-5$50ypU?%rtTXa7G}Uw~x* delta 13295 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 - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Crusader.rep/user/00/~00000008.db/db.33.gbf b/Crusader.rep/user/00/~00000008.db/db.35.gbf similarity index 99% rename from Crusader.rep/user/00/~00000008.db/db.33.gbf rename to Crusader.rep/user/00/~00000008.db/db.35.gbf index d6661a544587aa9d267009ae32244ae6da79ab86..c050da9d37c0bd4d6c070ce713e9ef991dda0b73 100644 GIT binary patch delta 97 zcmZo@U~On%70@>@)G^W2(K0kws}=c{8QUl@Re+I~(QTumug&yq8^*`e{cRZ~8D~t- zv}OFkXtF)gj!~A8$=F~zkRdUBhCO4TxV4gkz8x=@jlNG}aY?*SesW?-W`3R>04zfr Aga7~l delta 95 zcmZo@U~On%70@>@)G^W2(fZCZwIh#(qp49~ssJM|qx(ihUz_RKHjIy_``R)}F;1VJ yVaxbqd!ikqEF+VV#dIJ;V)_hw#z66EB?WyuUM?GbpTy#lc%S^_#FEVXJUaj&DI3iI diff --git a/_tmp_parse_combat_dat.py b/_tmp_parse_combat_dat.py new file mode 100644 index 0000000..ddcfb3f --- /dev/null +++ b/_tmp_parse_combat_dat.py @@ -0,0 +1,166 @@ +from __future__ import annotations + +import struct +from pathlib import Path + + +MAGIC_DATA_OFF = 33000 + + +OPCODE_NAMES = { + 0x81: "set_unused_field53", + 0x82: "clear_unused_field53", + 0x84: "set_target_objid", + 0x85: "anim_walk", + 0x86: "anim_run", + 0x87: "anim_retreat", + 0x88: "turn_left_90", + 0x89: "turn_right_90", + 0x8A: "fire_small_if_clear", + 0x8B: "fire_large_if_clear", + 0x8C: "anim_stand", + 0x8D: "pathfind_home", + 0x8E: "pathfind_target", + 0x8F: "pathfind_midpoint", + 0x92: "anim_kneel_and_fire", + 0x93: "sleep_scaled", + 0x94: "loiter", + 0x95: "face_target", + 0x96: "set_activity", + 0x97: "switch_tactic", + 0x98: "teleport_home", + 0x99: "terminate", + 0x9A: "jump_if_dist_lt_481", + 0x9B: "jump_if_dist_gt_160", + 0x9C: "jump_if_shot_blocked", + 0x9D: "jump_if_shot_clear", + 0x9E: "random_jump_nonzero", + 0x9F: "loop_begin", + 0xA6: "pathfind_marker_frame", + 0xA7: "face_north", + 0xA8: "face_south", + 0xA9: "face_east", + 0xAA: "face_west", + 0xAB: "face_northeast", + 0xAC: "face_southwest", + 0xAD: "face_southeast", + 0xAE: "face_northwest", + 0xAF: "var_set", + 0xB0: "var_add", + 0xB1: "var_sub", + 0xB2: "var_mul", + 0xB3: "var_div", + 0xB4: "var_store_curdir", + 0xB5: "set_dir_raw", + 0xB6: "var_store_curdir_again", + 0xB7: "anim_kneeling_retreat", + 0xB8: "anim_kneeling_advance", + 0xB9: "anim_kneeling_slow_retreat", + 0xC0: "jump", + 0xC1: "loop_end", + 0xFF: "flip_to_block1_restart", +} + + +def format_word(value: int) -> str: + if value >= MAGIC_DATA_OFF: + return f"var[{value - MAGIC_DATA_OFF}]" + return f"0x{value:04x} ({value})" + + +def decode_block(block: bytes, start_offset: int) -> list[str]: + pc = 0 + lines: list[str] = [] + + def need_word(use_data: bool) -> int: + nonlocal pc + value = struct.unpack_from(" int: + return struct.unpack_from(" int: + return struct.unpack_from(" None: + path = Path("STATIC/COMBAT.DAT") + data = path.read_bytes() + + print(f"file={path} size={len(data)}") + print(f"header_magic_fill={data[:0x56].hex()[:32]}...") + + entries = [] + for index in range(64): + off = 0x80 + index * 8 + rec_off = read_u32(data, off) + rec_len = read_u32(data, off + 4) + if rec_off == 0 and rec_len == 0: + continue + entries.append((index, rec_off, rec_len)) + + print(f"entry_count={len(entries)}") + for index, rec_off, rec_len in entries: + rec = data[rec_off:rec_off + rec_len] + name = rec[:16].split(b"\0", 1)[0].decode("ascii") + block_offsets = [read_u16(rec, 16 + i * 2) for i in range(4)] + valid_blocks = [value for value in block_offsets if value and value < rec_len] + print( + f"[{index:02d}] off=0x{rec_off:04x} len=0x{rec_len:04x}" + f" name={name:<16} block_offsets={block_offsets}" + f" body_len={rec_len - min(valid_blocks) if valid_blocks else 0}" + ) + + for block_no, block_off in enumerate(block_offsets[:2]): + if not block_off or block_off >= rec_len: + continue + next_offsets = sorted(value for value in block_offsets[:2] if value > block_off and value < rec_len) + block_end = next_offsets[0] if next_offsets else rec_len + block = rec[block_off:block_end] + print( + f" block{block_no}: start=0x{block_off:04x} end=0x{block_end:04x}" + f" size={block_end - block_off:02d} bytes={block.hex(' ')}" + ) + for line in decode_block(block, block_off): + print(f" {line}") + + trailing_offsets = block_offsets[2:] + print(f" extra_offsets_unused={trailing_offsets}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/_tmp_scummvm_attack_process.cpp b/_tmp_scummvm_attack_process.cpp new file mode 100644 index 0000000..89a9f49 --- /dev/null +++ b/_tmp_scummvm_attack_process.cpp @@ -0,0 +1,1203 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + + +#include "ultima/ultima8/world/actors/attack_process.h" +#include "ultima/ultima8/ultima8.h" +#include "ultima/ultima8/audio/audio_process.h" +#include "ultima/ultima8/games/game_data.h" +#include "ultima/ultima8/kernel/kernel.h" +#include "ultima/ultima8/kernel/delay_process.h" +#include "ultima/ultima8/usecode/uc_list.h" +#include "ultima/ultima8/world/actors/actor.h" +#include "ultima/ultima8/world/current_map.h" +#include "ultima/ultima8/world/get_object.h" +#include "ultima/ultima8/world/world.h" +#include "ultima/ultima8/world/loop_script.h" +#include "ultima/ultima8/world/actors/combat_dat.h" +#include "ultima/ultima8/world/actors/loiter_process.h" +#include "ultima/ultima8/world/actors/cru_pathfinder_process.h" +#include "ultima/ultima8/misc/direction_util.h" + +namespace Ultima { +namespace Ultima8 { + +//#define WATCHACTOR 3 + +DEFINE_RUNTIME_CLASSTYPE_CODE(AttackProcess) + +// These sound number arrays are in the order they appear in the original exes + +static const int16 REM_SFX_1[] = {0x15, 0x78, 0x80, 0x83, 0xDC, 0xDD}; +static const int16 REM_SFX_2[] = {0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xE7}; +static const int16 REM_SFX_3[] = {0xFC, 0xFD, 0xFE, 0xC8}; +static const int16 REM_SFX_4[] = {0xCC, 0xCD, 0xCE, 0xCF}; +static const int16 REM_SFX_5[] = {0xC7, 0xCA, 0xC9}; +static const int16 REM_SFX_6[] = {0x82, 0x84, 0x85}; +static const int16 REM_SFX_7[] = {0x9B, 0x9C, 0x9D, 0x9E, 0x9F}; + +static const int16 REG_SFX_1[] = { 0xD2, 0xD3, 0xD4, 0xD5, 0xE5, 0x100 }; +static const int16 REG_SFX_2[] = { 0x9, 0x79, 0x7A, 0x7B, 0x7C, 0x7D }; +static const int16 REG_SFX_3[] = { 0x7E, 0x7F, 0x90, 0xB6, 0xC2, 0xD0 }; +static const int16 REG_SFX_4[] = { 0x101, 0x102, 0x103, 0x104, 0x105, 0x106 }; +static const int16 REG_SFX_5[] = { 0x108, 0x109, 0x1AB, 0x1AC, 0x1AD, 0x1AF, 0x1AE }; +static const int16 REG_SFX_6[] = { 0x1B0, 0x1B1, 0x1B2, 0x1B3, 0x1B4 }; +static const int16 REG_SFX_7[] = { 0x1B5, 0x1B6, 0x1B7, 0x1B8, 0x1B9, 0x1BA, 0x1BB }; +static const int16 REG_SFX_8[] = { 0x1C1, 0x1C0, 0x1BF, 0x1BE, 0x1BD, 0x1BC }; +static const int16 REG_SFX_9[] = { 0x1C2, 0x1C3, 0x1C4, 0x1C5, 0x1C6, 0x1C7 }; +static const int16 REG_SFX_10[] = { 0x1C8, 0x1C9, 0x1CA, 0x1CB, 0x1CC, 0x1CD }; +static const int16 REG_SFX_11[] = { 0x1D0, 0x1D1, 0x1D2, 0x1D3, 0x1D4, 0x1D5 }; +static const int16 REG_SFX_12[] = { 0x1D7, 0x1D8, 0x1D9, 0x1DA, 0x1DB, 0x1DC }; +static const int16 REG_SFX_13[] = { 0x1DD, 0x1DE, 0x1DF, 0x1E0, 0x1E1, 0x1E2, 0x1E3 }; +static const int16 REG_SFX_14[] = { 0x9B, 0x9C, 0x9D, 0x9E, 0x9F }; +static const int16 REG_SFX_15[] = { 0x1E7, 0x1E8, 0x1E9, 0x1EA, 0x1ED }; + +#define RANDOM_ELEM(array) (array[rs.getRandomNumber(ARRAYSIZE(array) - 1)]) + +// If data is referenced in the metalang with an offset of this or greater, +// read from the data array. +static const int MAGIC_DATA_OFF = 33000; + +int16 AttackProcess::_lastAttackSound = -1; +int16 AttackProcess::_lastLastAttackSound = -1; + +static uint16 someSleepGlobal = 0; + +AttackProcess::AttackProcess() : Process(), _block(0), _target(1), _tactic(0), _tacticDat(nullptr), +_tacticDatReadStream(nullptr), _tacticDatStartOffset(0), _soundNo(-1), _playedStartSound(false), +_npcInitialDir(dir_invalid), _field57(0), _field59(0), _field7f(false), _field96(false), _field97(false), +_isActivity9orB(false), _isActivityAorB(false), _timer3set(false), _timer2set(false), +_doubleDelay(false), _wpnField8(1), _wpnBasedTimeout(0), _difficultyBasedTimeout(0), _timer2(0), +_timer3(0), _timer4(0), _timer5(0), _soundTimestamp(0), _soundDelayTicks(480), _fireTimestamp(0) { + for (int i = 0; i < ARRAYSIZE(_dataArray); i++) { + _dataArray[i] = 0; + } + if (GAME_IS_REGRET) { + Common::RandomSource &rs = Ultima8Engine::get_instance()->getRandomSource(); + _soundDelayTicks = rs.getRandomNumberRng(10, 24) * 60; + if (rs.getRandomNumber(2) == 0) + _soundTimestamp = Kernel::get_instance()->getTickNum(); + } +} + +AttackProcess::AttackProcess(Actor *actor) : _block(0), _target(1), _tactic(0), _tacticDat(nullptr), +_tacticDatReadStream(nullptr), _tacticDatStartOffset(0), _soundNo(-1), _playedStartSound(false), +_field57(0), _field59(0), _field7f(false), _field96(false), _field97(false), _isActivity9orB(false), +_isActivityAorB(false), _timer3set(false), _timer2set(false), _doubleDelay(false), _wpnField8(1), +_wpnBasedTimeout(0), _difficultyBasedTimeout(0), _timer2(0), _timer3(0), _timer4(0), _timer5(0), +_soundTimestamp(0), _soundDelayTicks(480), _fireTimestamp(0) { + assert(actor); + _itemNum = actor->getObjId(); + _npcInitialDir = actor->getDir(); + + // Note: this isn't actually initialized in the original which + // suggests it can't ever get used before setting, but to make + // coverity etc happy clear it anyway. + for (int i = 0; i < ARRAYSIZE(_dataArray); i++) { + _dataArray[i] = 0; + } + + if (GAME_IS_REGRET) { + Common::RandomSource &rs = Ultima8Engine::get_instance()->getRandomSource(); + _soundDelayTicks = rs.getRandomNumberRng(10, 24) * 60; + if (rs.getRandomNumber(2) == 0) + _soundTimestamp = Kernel::get_instance()->getTickNum(); + } + + actor->setAttackAimFlag(false); + + const Item *wpn = getItem(actor->getActiveWeapon()); + if (wpn) { + const uint32 wpnshape = wpn->getShape(); + const uint32 npcshape = actor->getShape(); + const uint8 difficulty = World::get_instance()->getGameDifficulty(); + if (wpnshape == 0x386 || wpnshape == 0x388 || wpnshape == 0x38e) { + _wpnBasedTimeout = 0x3c; + switch (difficulty) { + case 1: + _difficultyBasedTimeout = 0x78; + break; + case 2: + _difficultyBasedTimeout = 0x5a; + break; + case 3: + case 4: + default: + if (npcshape == 0x3ac) + _difficultyBasedTimeout = 0xf; + else + _difficultyBasedTimeout = 0x3c; + break; + } + } else { + _wpnBasedTimeout = 0x1e; + switch (difficulty) { + case 1: + _difficultyBasedTimeout = _wpnBasedTimeout; + break; + case 2: + _difficultyBasedTimeout = 0x14; + break; + case 3: + _difficultyBasedTimeout = 0xf; + break; + case 4: + default: + _difficultyBasedTimeout = 0; + } + } + } + + _type = ATTACK_PROC_TYPE; + + setTacticNo(actor->getCombatTactic()); + actor->setToStartOfAnim(Animation::stand); +} + +AttackProcess::~AttackProcess() { + delete _tacticDatReadStream; +} + +void AttackProcess::terminate() { + Actor *a = getActor(_itemNum); + if (a) + a->clearActorFlag(Actor::ACT_INCOMBAT); + + Process::terminate(); +} + +void AttackProcess::run() { + Actor *a = getActor(_itemNum); + Actor *target = getActor(_target); + + if (!a || a->isDead() || !_tacticDatReadStream) { + terminate(); + return; + } + + if (!a->hasFlags(Item::FLG_FASTAREA)) + return; + + if (_tactic == 0) { + genericAttack(); + return; + } + + if (!target || target->isDead()) { + warning("got into attack process with invalid target"); + terminate(); + return; + } + + Common::RandomSource &rs = Ultima8Engine::get_instance()->getRandomSource(); + const Direction curdir = a->getDir(); + + const uint8 opcode = _tacticDatReadStream->readByte(); + switch (opcode) { + case 0x81: + // Seems like field 0x53 is never used anywhere? + /*_field53 = */readNextWordWithData(); + return; + case 0x82: + /*_field53 = 0*/; + return; + case 0x84: + _target = readNextWordWithData(); + // This is called in the original, but basically redundant. + // a->setActivity(5); + return; + case 0x85: + a->doAnim(Animation::walk, dir_current); + return; + case 0x86: + a->doAnim(Animation::run, dir_current); + return; + case 0x87: + a->doAnim(Animation::retreat, dir_current); + return; + case 0x88: + { + // Turn 90 degrees left + Direction newdir = Direction_TurnByDelta(curdir, -2, dirmode_8dirs); + a->turnTowardDir(newdir); + return; + } + case 0x89: + { + // Turn 90 degrees right + Direction newdir = Direction_TurnByDelta(curdir, 2, dirmode_8dirs); + a->turnTowardDir(newdir); + return; + } + case 0x8a: + { + bool result = a->fireDistance(target, curdir, 0, 0, 0); + // Fire small weapon + if (result) + a->doAnim(Animation::attack, dir_current); + return; + } + case 0x8b: + { + bool result = a->fireDistance(target, curdir, 0, 0, 0); + // Fire large weapon + if (result) + a->doAnim(Animation::attack, dir_current); + return; + } + case 0x8c: + a->doAnim(Animation::stand, dir_current); + return; + case 0x8d: + { + // Pathfind to home + int32 x, y, z; + a->getHomePosition(x, y, z); + ProcId pid = Kernel::get_instance()->addProcess( + new CruPathfinderProcess(a, Point3(x, y, z), 100, 0x80, true)); + waitFor(pid); + return; + } + case 0x8e: + { + // Pathfind to target + Point3 pt = target->getLocation(); + ProcId pid = Kernel::get_instance()->addProcess( + new CruPathfinderProcess(a, pt, 12, 0x80, true)); + waitFor(pid); + return; + } + case 0x8f: + { + // Pathfind to a point between npc and the target + Point3 apt = a->getLocation(); + Point3 tpt = target->getLocation(); + int32 x = (tpt.x + apt.x) / 2; + int32 y = (tpt.y + apt.y) / 2; + int32 z = (tpt.z + apt.z) / 2; + ProcId pid = Kernel::get_instance()->addProcess( + new CruPathfinderProcess(a, Point3(x, y, z), 12, 0x80, true)); + waitFor(pid); + return; + } + case 0x92: + a->doAnim(Animation::kneelAndFire, dir_current); + return; + case 0x93: + { + // Sleep for a random value scaled by difficult level + int ticks = readNextWordWithData(); + if (ticks == someSleepGlobal) { + ticks = rs.getRandomNumberRng(0x31, 0x45); + } + ticks /= World::get_instance()->getGameDifficulty(); + sleep(ticks); + return; + } + case 0x94: + { + // Loiter a bit.. + uint16 data = readNextWordWithData(); + ProcId pid = Kernel::get_instance()->addProcess(new LoiterProcess(a, data)); + waitFor(pid); + return; + } + case 0x95: + { + Direction dir = a->getDirToItemCentre(*target); + a->turnTowardDir(dir); + return; + } + case 0x96: + // do activity specified by next word + a->setActivity(readNextWordWithData()); + return; + case 0x97: + // switch to tactic no specified by next word + setTacticNo(readNextWordWithData()); + return; + case 0x98: + { + a->setDir(_npcInitialDir); + a->moveToEtherealVoid(); + int32 hx, hy, hz; + a->getHomePosition(hx, hy, hz); + a->move(hx, hy, hz); + return; + } + case 0x99: + terminate(); + return; + case 0x9a: + { + // get next word and jump to that offset if distance < 481 + Point3 apt = a->getLocation(); + Point3 tpt = target->getLocation(); + int maxdiff = apt.maxDistXYZ(tpt); + int16 data = readNextWordWithData(); + if (maxdiff < 481) { + _tacticDatReadStream->seek(data, SEEK_SET); + } + return; + } + case 0x9b: + { + // get next word and jump to that offset if distance > 160 + Point3 apt = a->getLocation(); + Point3 tpt = target->getLocation(); + int maxdiff = apt.maxDistXYZ(tpt); + int16 data = readNextWordWithData(); + if (maxdiff > 160) { + _tacticDatReadStream->seek(data, SEEK_SET); + } + return; + } + case 0x9c: + { + bool result = a->fireDistance(target, curdir, 0, 0, 0); + uint16 data = readNextWordWithData(); + if (!result) { + _tacticDatReadStream->seek(data, SEEK_SET); + } + return; + } + case 0x9d: + { + bool result = a->fireDistance(target, curdir, 0, 0, 0); + uint16 data = readNextWordWithData(); + if (result) { + _tacticDatReadStream->seek(data, SEEK_SET); + } + return; + } + case 0x9e: + { + uint16 maxval = readNextWordWithData(); + uint16 offset = readNextWordWithData(); + if (maxval != 0) { + uint16 randval = rs.getRandomNumber(maxval - 1); + if (randval != 0) { + _tacticDatReadStream->seek(offset); + } + } + return; + } + case 0x9f: + _field57 = readNextWordWithData(); + _field59 = _tacticDatReadStream->pos(); + return; + case 0xa6: + { + const uint16 targetFrame = readNextWordWithData(); + const uint16 targetQ = a->getUnkByte(); + + UCList uclist(2); + // loopscript to find shape = 0x33A (826), the numbers that NPCs wander between + LOOPSCRIPT(script, LS_SHAPE_EQUAL(0x33a)); + CurrentMap *currentmap = World::get_instance()->getCurrentMap(); + currentmap->areaSearch(&uclist, script, sizeof(script), nullptr, + 0x200 * 16, true); + for (unsigned int i = 0; i < uclist.getSize(); ++i) { + Item *founditem = getItem(uclist.getuint16(i)); + uint16 itemQlo = founditem->getQuality() & 0xff; + uint32 itemFrame = founditem->getFrame(); + + if (itemFrame == targetFrame && (targetQ == 0 || itemQlo == targetQ)) { + ProcId pid = Kernel::get_instance()->addProcess( + new CruPathfinderProcess(a, founditem, 100, 0x80, true)); + waitFor(pid); + break; + } + } + return; + } + case 0xa7: + a->turnTowardDir(dir_north); + return; + case 0xa8: + a->turnTowardDir(dir_south); + return; + case 0xa9: + a->turnTowardDir(dir_east); + return; + case 0xaa: + a->turnTowardDir(dir_west); + return; + case 0xab: + a->turnTowardDir(dir_northeast); + return; + case 0xac: + a->turnTowardDir(dir_southwest); + return; + case 0xad: + a->turnTowardDir(dir_southeast); + return; + case 0xae: + a->turnTowardDir(dir_northwest); + return; + case 0xaf: + { + uint16 next = readNextWordWithData(); + uint16 offset = readNextWordRaw(); + setAttackData(offset, next); + return; + } + case 0xb0: + { + uint16 offset = readNextWordRaw(); + uint16 val = getAttackData(offset); + setAttackData(opcode, val + readNextWordWithData()); + return; + } + case 0xb1: + { + uint16 offset = readNextWordRaw(); + uint16 val = getAttackData(offset); + setAttackData(offset, val - readNextWordWithData()); + return; + } + case 0xb2: + { + uint16 offset = readNextWordRaw(); + uint16 val = getAttackData(offset); + setAttackData(offset, val * readNextWordWithData()); + return; + } + case 0xb3: + { + uint16 offset = readNextWordRaw(); + uint16 val = getAttackData(offset); + uint16 divisor = readNextWordWithData(); + if (!divisor) + divisor = 1; // shouldn't happen in real data, but just to be sure.. + setAttackData(offset, val / divisor); + return; + } + case 0xb4: + { + uint16 dir = Direction_ToUsecodeDir(curdir); + uint16 offset = readNextWordRaw(); + setAttackData(offset, dir); + return; + } + case 0xb5: + { + uint16 dir = readNextWordWithData(); + a->setDir(Direction_FromUsecodeDir(dir)); + return; + } + case 0xb6: + { + uint16 offset = readNextWordRaw(); + uint16 dir = Direction_ToUsecodeDir(curdir); + setAttackData(offset, dir); + return; + } + case 0xb7: + a->doAnim(Animation::kneelingRetreat, dir_current); + return; + case 0xb8: + a->doAnim(Animation::kneelingAdvance, dir_current); + return; + case 0xb9: + a->doAnim(Animation::kneelingSlowRetreat, dir_current); + return; + case 0xc0: + _tacticDatReadStream->seek(readNextWordWithData(), SEEK_SET); + return; + case 0xc1: + _field57--; + if (_field57 > 0) { + _tacticDatReadStream->seek(_field59, SEEK_SET); + } + return; + case 0xff: + // flip to block 1 and restart + if (_block == 0) { + setBlockNo(1); + } + _tacticDatReadStream->seek(_tacticDatStartOffset, SEEK_SET); + return; + } +} + +void AttackProcess::genericAttack() { + Actor *a = getActor(_itemNum); + assert(a); + + if (a->isBusy() || a->hasActorFlags(Actor::ACT_PATHFINDING)) { + return; + } + +#ifdef WATCHACTOR + if (_itemNum == WATCHACTOR) + debug("Attack: actor %d genericAttack (not busy or pathfinding)", _itemNum); +#endif + + // This should never be running on the controlled npc. + if (_itemNum == World::get_instance()->getControlledNPCNum()) { + terminate(); + return; + } + + const Item *wpn = getItem(a->getActiveWeapon()); + /*if (!wpn) { + warning("started attack for NPC %d with no weapon", _itemNum); + }*/ + + Common::RandomSource &rs = Ultima8Engine::get_instance()->getRandomSource(); + AudioProcess *audio = AudioProcess::get_instance(); + const Direction curdir = a->getDir(); + const int32 ticknow = Kernel::get_instance()->getTickNum(); + int wpnField8 = wpn ? wpn->getShapeInfo()->_weaponInfo->_field8 : 1; + const uint16 controlledNPC = World::get_instance()->getControlledNPCNum(); + Direction targetdir = dir_invalid; + + if (_target != controlledNPC) { + if (controlledNPC <= 1) + _target = 1; + else + _target = controlledNPC; + } + +#ifdef WATCHACTOR + if (_itemNum == WATCHACTOR) + debug("Attack: genericAttack chose target %d", _target); +#endif + + Actor *target = getActor(_target); + if (!target || !target->isOnScreen() || target->isDead()) { + // Walk around randomly in hope of finding target + _target = 0; + if (!_isActivity9orB) { +#ifdef WATCHACTOR + if (_itemNum == WATCHACTOR) + debug("Attack: genericAttack walking around looking for target %d", _target); +#endif + Point3 pt = a->getLocation(); + pt.x += rs.getRandomNumberRngSigned(-0x1ff, 0x1ff); + pt.y += rs.getRandomNumberRngSigned(-0x1ff, 0x1ff); + _field96 = true; + const ProcId pid = Kernel::get_instance()->addProcess( + new CruPathfinderProcess(a, pt, 12, 0x80, true)); + // add a tiny delay to avoid tight loops + Process *delayproc = new DelayProcess(2); + Kernel::get_instance()->addProcess(delayproc); + delayproc->waitFor(pid); + waitFor(delayproc); + return; + } + } else { + Animation::Sequence anim; + if (a->isInCombat()) { + anim = Animation::combatStand; + } else { + anim = Animation::stand; + } + DirectionMode standDirMode = a->animDirMode(anim); + if (_timer3set) { +#ifdef WATCHACTOR + if (_itemNum == WATCHACTOR) + debug("Attack: _timer3set"); +#endif + if (_timer3 >= ticknow) { + if (a->isInCombat()) { + if (rs.getRandomNumber(2) != 0) { +#ifdef WATCHACTOR + if (_itemNum == WATCHACTOR) + debug("Attack: toggle weapon state"); +#endif + const Animation::Sequence lastanim = a->getLastAnim(); + if ((lastanim != Animation::unreadyWeapon) && (lastanim != Animation::unreadyLargeWeapon)) + a->doAnim(Animation::unreadyWeapon, dir_current); + else + a->doAnim(Animation::readyWeapon, dir_current); + return; + } + + if (rs.getRandomNumber(2) == 0) { + a->turnTowardDir(Direction_TurnByDelta(curdir, rs.getRandomNumber(7), dirmode_8dirs)); + return; + } + + if (!a->hasActorFlags(Actor::ACT_WEAPONREADY)) + return; + + if (curdir != a->getDirToItemCentre(*target)) + return; + + if (_soundNo != -1) { + if (audio->isSFXPlayingForObject(_soundNo, _itemNum)) + return; + _soundNo = -1; + } + + _wpnField8 = wpnField8; + if (_wpnField8 < 3) { + _wpnField8 = 1; + } else if ((_doubleDelay && rs.getRandomNumber(1) == 0) || (rs.getRandomNumber(4) == 0)) { + a->setAttackAimFlag(true); + _wpnField8 *= 4; + } + _fireTimestamp = ticknow; + if (_timer4 == 0) + _timer4 = ticknow; + +#ifdef WATCHACTOR + if (_itemNum == WATCHACTOR) + debug("Attack: firing weapon at tick %d!", ticknow); +#endif + + const ProcId animpid = a->doAnim(Animation::attack, dir_current); // fire small weapon. + if (animpid == 0) { + return; + } + waitFor(animpid); + _wpnField8--; + if (_wpnField8 == 0) + return; + + // TODO: this is not correct - should be Process_11e0_15ab(animpid); (not waitFor).. + waitFor(animpid); + return; + } + } else { + _timer3set = false; + a->setActivity(5); + } + } + if (targetdir == dir_invalid) { + targetdir = a->getDirToItemCentre(*target); + } + + Point3 apt = a->getLocation(); + Point3 tpt = target->getLocation(); + const int32 dist = apt.maxDistXYZ(tpt); + const int32 zdiff = abs(a->getZ() - target->getZ()); + const bool onscreen = a->isPartlyOnScreen(); // note: original uses "isMajorityOnScreen", this is close enough. + if ((!_isActivity9orB && !onscreen) || (dist <= zdiff)) { +#ifdef WATCHACTOR + if (_itemNum == WATCHACTOR) + debug("Attack: Not 9/B and actor not onscreen or dist %d < zdiff %d, pathfinding", dist, zdiff); +#endif + pathfindToItemInNPCData(); + return; + } + if (targetdir == curdir) { +#ifdef WATCHACTOR + if (_itemNum == WATCHACTOR) + debug("Attack: targetdir == currentdir"); +#endif + const uint16 rnd = rs.getRandomNumber(9); + const uint32 frameno = Kernel::get_instance()->getFrameNum(); + const uint32 timeoutfinish = target->getAttackMoveTimeoutFinishFrame(); + + if (!onscreen || + (!_field96 && !timer4and5Update(ticknow) && frameno < timeoutfinish + && rnd > 2 && (!_isActivityAorB || rnd > 3))) { + sleep(0x14); + return; + } + + _field96 = false; + bool ready; + if (ticknow - a->getLastTickWasHit() <= 120) + ready = true; + else + ready = checkReady(ticknow, targetdir); + + if (_timer2set && (rs.getRandomNumber(4) == 0 || checkTimer2PlusDelayElapsed(ticknow))) { + _timer2set = false; + } + + if (!ready) { + if (!_isActivity9orB) + pathfindToItemInNPCData(); + else + sleep(0xf); + return; + } + + checkRandomAttackSound(ticknow, a->getShape()); + + if (!a->hasActorFlags(Actor::ACT_WEAPONREADY)) { + _timer4 = ticknow; + a->doAnim(Animation::readyWeapon, dir_current); // ready small wpn + return; + } + + // Wait until sound is finished playing. + if (_soundNo != -1) { + if (audio->isSFXPlayingForObject(_soundNo, _itemNum)) + return; + _soundNo = -1; + } + + const int32 t5elapsed = ticknow - _timer5; + if (t5elapsed > _wpnBasedTimeout) { + const int32 fireelapsed = ticknow - _fireTimestamp; + if (fireelapsed <= _difficultyBasedTimeout) { + sleep(_difficultyBasedTimeout - fireelapsed); + return; + } + + if (!wpn) { + _wpnField8 = 1; + } else { + _wpnField8 = wpnField8; + if (_wpnField8 > 2 && ((_doubleDelay && rs.getRandomNumber(1) == 0) || rs.getRandomNumber(4) == 0)) { + a->setAttackAimFlag(true); + _wpnField8 *= 4; + } + } + + _fireTimestamp = ticknow; + if (_timer4 == 0) { + _timer4 = ticknow; + } + + const ProcId firepid = a->doAnim(Animation::attack, dir_current); // Fire SmallWpn + if (firepid != 0) { + waitFor(firepid); + _wpnField8--; + if (_wpnField8 != 0) { + // TODO: this is not correct - should be Process_11e0_15ab(firepid); (not waitFor).. + waitFor(firepid); + return; + } + } + } else if (t5elapsed != 0) { + sleep(_wpnBasedTimeout - t5elapsed); + return; + } + } else { +#ifdef WATCHACTOR + if (_itemNum == WATCHACTOR) + debug("Attack: targetdir != currentdir"); +#endif + bool ready; + if (!timer4and5Update(ticknow) && !_field7f) { + if (standDirMode != dirmode_16dirs) { + targetdir = a->getDirToItemCentre(*target); + } + ready = a->fireDistance(target, targetdir, 0, 0, 0); + if (ready) + timeNowToTimerVal2(ticknow); + } else { + timeNowToTimerVal2(ticknow); + ready = true; + _field7f = false; + } + + // 5a flag 1 set? + if (!a->hasActorFlags(Actor::ACT_WEAPONREADY) && ready) { + _timer4 = ticknow; + a->doAnim(Animation::readyWeapon, dir_current); // ready SmallWpn + return; + } + if (ready || _isActivity9orB) { + a->turnTowardDir(targetdir); + return; + } + pathfindToItemInNPCData(); + } + } +} + +void AttackProcess::checkRandomAttackSoundRegret(const Actor *actor) { + if (!readyForNextSound(Kernel::get_instance()->getTickNum())) + return; + + AudioProcess *audio = AudioProcess::get_instance(); + if (audio->isSFXPlayingForObject(-1, actor->getObjId())) + return; + + int16 sndno = getRandomAttackSoundRegret(actor); + + if (sndno != -1 && _lastAttackSound != sndno && _lastLastAttackSound != sndno) { + _lastLastAttackSound = _lastAttackSound; + _lastAttackSound = sndno; + _soundNo = sndno; + audio->playSFX(sndno, 0x80, actor->getObjId(), 1); + } +} + +/* static */ +int16 AttackProcess::getRandomAttackSoundRegret(const Actor *actor) { + if (World::get_instance()->getControlledNPCNum() != kMainActorId) + return -1; + + if (actor->isDead()) + return -1; + + Common::RandomSource &rs = Ultima8Engine::get_instance()->getRandomSource(); + uint32 shapeno = actor->getShape(); + + int16 sndno = -1; + // The order here is pretty random, how it comes out of the disasm. + switch (shapeno) { + case 0x4e0: + sndno = RANDOM_ELEM(REG_SFX_8); + break; + case 899: + sndno = RANDOM_ELEM(REG_SFX_14); + break; + case 900: + sndno = RANDOM_ELEM(REG_SFX_7); + break; + case 0x4d1: + case 0x528: + sndno = RANDOM_ELEM(REG_SFX_3); + break; + case 0x344: + sndno = RANDOM_ELEM(REG_SFX_13); + break; + case 0x371: + case 0x62f: + case 0x630: + sndno = RANDOM_ELEM(REG_SFX_2); + break; + case 0x2f5: + sndno = RANDOM_ELEM(REG_SFX_9); + break; + case 0x2f6: + sndno = RANDOM_ELEM(REG_SFX_12); + break; + case 0x2f7: + case 0x595: + sndno = RANDOM_ELEM(REG_SFX_11); + break; + case 0x2df: + sndno = RANDOM_ELEM(REG_SFX_6); + break; + case 0x597: + sndno = RANDOM_ELEM(REG_SFX_10); + break; + case 0x5b1: + sndno = RANDOM_ELEM(REG_SFX_15); + break; + case 0x5ff: + case 0x5d7: + sndno = RANDOM_ELEM(REG_SFX_5); + break; + case 0x1b4: + case 0x625: + sndno = RANDOM_ELEM(REG_SFX_4); + break; + case 0x5f0: + case 0x308: + sndno = RANDOM_ELEM(REG_SFX_1); + break; + default: + break; + } + + return sndno; +} + +void AttackProcess::checkRandomAttackSound(int now, uint32 shapeno) { + if (GAME_IS_REGRET) { + checkRandomAttackSoundRegret(getActor(_itemNum)); + return; + } + + Common::RandomSource &rs = Ultima8Engine::get_instance()->getRandomSource(); + AudioProcess *audio = AudioProcess::get_instance(); + int16 attacksound = -1; + if (!_playedStartSound) { + _playedStartSound = true; + if (rs.getRandomNumber(2) == 0) { + switch(shapeno) { + case 0x371: + attacksound = RANDOM_ELEM(REM_SFX_3); + break; + case 0x1b4: + attacksound = RANDOM_ELEM(REM_SFX_5); + break; + case 0x2fd: + case 0x319: + attacksound = RANDOM_ELEM(REM_SFX_1); + break; + case 900: + attacksound = RANDOM_ELEM(REM_SFX_2); + break; + case 0x4d1: + case 0x528: + attacksound = RANDOM_ELEM(REM_SFX_4); + break; + default: + break; + } + } + } else { + if (readyForNextSound(now)) { + if (shapeno == 0x2df) + attacksound = RANDOM_ELEM(REM_SFX_6); + else if (shapeno == 899) + attacksound = RANDOM_ELEM(REM_SFX_7); + } + } + + if (attacksound != -1) { + _soundNo = attacksound; + audio->playSFX(attacksound, 0x80, _itemNum, 1); + } +} + +bool AttackProcess::readyForNextSound(uint32 now) { + if (_soundTimestamp == 0 || now - _soundTimestamp >= _soundDelayTicks) { + _soundTimestamp = now; + return true; + } + return false; +} + +bool AttackProcess::checkTimer2PlusDelayElapsed(int now) { + int delay = 60; + if (_doubleDelay) + delay *= 2; + return (now >= _timer2 + delay); +} + +void AttackProcess::setAttackData(uint16 off, uint16 val) { + if (off >= MAGIC_DATA_OFF && off < MAGIC_DATA_OFF + ARRAYSIZE(_dataArray) - 1) + _dataArray[off - MAGIC_DATA_OFF] = val; + + warning("Invalid offset to setAttackDataArray %d %d", off, val); +} + +uint16 AttackProcess::getAttackData(uint16 off) const { + if (off >= MAGIC_DATA_OFF && off < MAGIC_DATA_OFF + ARRAYSIZE(_dataArray) - 1) + return _dataArray[off - MAGIC_DATA_OFF]; + + warning("Invalid offset to getAttackDataArray: %d", off); + return 0; +} + +void AttackProcess::pathfindToItemInNPCData() { +#ifdef WATCHACTOR + if (_itemNum == WATCHACTOR) + debug("Attack: pathfindToItemInNPCData"); +#endif + _doubleDelay = false; + _timer2set = false; + _field96 = true; + + Actor *a = getActor(_itemNum); + Actor *target = getActor(_target); + + Process *pathproc = new CruPathfinderProcess(a, target, 12, 0x80, false); + // In case pathfinding fails delay for a bit to ensure we don't get + // stuck in a tight loop using all the cpu + Process *delayproc = new DelayProcess(10); + Kernel::get_instance()->addProcess(pathproc); + Kernel::get_instance()->addProcess(delayproc); + delayproc->waitFor(pathproc); + waitFor(delayproc); +} + +bool AttackProcess::timer4and5Update(int now) { + int32 delay = 120; + if (_doubleDelay) { + delay = 240; + } + +#ifdef WATCHACTOR + if (_itemNum == WATCHACTOR) + debug("Attack: timer4and5Update (doubledelay=%d, timer4=%d, timer5=%d)", + _doubleDelay, _timer4, _timer5); +#endif + + if (_timer4) { + _timer5 = _timer4; + if (_timer4 + delay >= now) { + return true; + } + } + + _timer4 = 0; + _doubleDelay = false; + return false; +} + +bool AttackProcess::checkReady(int now, Direction targetdir) { + if (timer4and5Update(now) || _timer2set) { + return true; + } + + Actor *a = getActor(_itemNum); + Actor *target = getActor(_target); + if (!a || !target) + return false; + return a->fireDistance(target, targetdir, 0, 0, 0) > 0; +} + +void AttackProcess::timeNowToTimerVal2(int now) { + _timer2 = now; + _timer2set = true; +} + +void AttackProcess::setTimer3() { + Common::RandomSource &rs = Ultima8Engine::get_instance()->getRandomSource(); + const int32 now = Kernel::get_instance()->getTickNum(); + _timer3set = true; + _timer3 = rs.getRandomNumber(9) * 60 + now; + return; +} + +void AttackProcess::sleep(int ticks) { + // waiting less than 2 ticks can cause a tight loop +#ifdef WATCHACTOR + if (_itemNum == WATCHACTOR) + debug("Attack: sleeping for %d", ticks); +#endif + ticks = MAX(ticks, 2); + Process *delayProc = new DelayProcess(ticks); + ProcId pid = Kernel::get_instance()->addProcess(delayProc); + waitFor(pid); +} + +void AttackProcess::setTacticNo(int tactic) { + assert(tactic < 32); + _tactic = tactic; + _tacticDat = GameData::get_instance()->getCombatDat(tactic); + delete _tacticDatReadStream; + _tacticDatReadStream = new Common::MemoryReadStream(_tacticDat->getData(), _tacticDat->getDataLen()); + setBlockNo(0); +} + +void AttackProcess::setBlockNo(int block) { + _block = block; + + if (!_tacticDat) + return; + + _tacticDatStartOffset = _tacticDat->getOffset(block); + _tacticDatReadStream->seek(_tacticDatStartOffset, SEEK_SET); +} + +uint16 AttackProcess::readNextWordWithData() { + uint16 data = _tacticDatReadStream->readUint16LE(); + if (data >= MAGIC_DATA_OFF) { + data = getAttackData(data); + } + + return data; +} + +uint16 AttackProcess::readNextWordRaw() { + assert(_tacticDatReadStream); + return _tacticDatReadStream->readUint16LE(); +} + +Common::String AttackProcess::dumpInfo() const { + return Process::dumpInfo(); +} + +void AttackProcess::saveData(Common::WriteStream *ws) { + Process::saveData(ws); + + ws->writeUint16LE(_target); + ws->writeUint16LE(_tactic); + ws->writeUint16LE(_block); + ws->writeUint16LE(_tacticDatStartOffset); + + ws->writeUint16LE(_soundNo); + ws->writeByte(_playedStartSound ? 1 : 0); + ws->writeByte(Direction_ToUsecodeDir(_npcInitialDir)); + + ws->writeSint16LE(_field57); + ws->writeUint16LE(_field59); + ws->writeByte(_field7f ? 1 : 0); + ws->writeByte(_field96 ? 1 : 0); + ws->writeByte(_field97 ? 1 : 0); + + ws->writeByte(_isActivity9orB ? 1 : 0); + ws->writeByte(_isActivityAorB ? 1 : 0); + ws->writeByte(_timer2set ? 1 : 0); + ws->writeByte(_timer3set ? 1 : 0); + ws->writeByte(_doubleDelay ? 1 : 0); + + ws->writeUint16LE(_wpnField8); + + for (int i = 0; i < ARRAYSIZE(_dataArray); i++) { + ws->writeUint16LE(_dataArray[i]); + } + + ws->writeSint32LE(_wpnBasedTimeout); + ws->writeSint32LE(_difficultyBasedTimeout); + + ws->writeSint32LE(_timer2); + ws->writeSint32LE(_timer3); + ws->writeSint32LE(_timer4); + ws->writeSint32LE(_timer5); + ws->writeSint32LE(_soundTimestamp); // bug: this should ideally be unsigned, probably won't make any difference. + // Don't write the sound delay because it only affects + // No Regret, adding it now would need a version bump, and + // and it doesn't make much difference to re-randomize it. + ws->writeSint32LE(_fireTimestamp); +} + +bool AttackProcess::loadData(Common::ReadStream *rs, uint32 version) { + if (!Process::loadData(rs, version)) return false; + + _target = rs->readUint16LE(); + setTacticNo(rs->readUint16LE()); + setBlockNo(rs->readUint16LE()); + _tacticDatStartOffset = rs->readUint16LE(); + + _soundNo = rs->readUint16LE(); + _playedStartSound = rs->readByte(); + _npcInitialDir = Direction_FromUsecodeDir(rs->readByte()); + + _field57 = rs->readSint16LE(); + _field59 = rs->readUint16LE(); + _field7f = rs->readByte(); + _field96 = rs->readByte(); + _field97 = rs->readByte(); + + _isActivity9orB= rs->readByte(); + _isActivityAorB = rs->readByte(); + _timer2set = rs->readByte(); + _timer3set = rs->readByte(); + _doubleDelay = rs->readByte(); + + _wpnField8 = rs->readUint16LE(); + + for (int i = 0; i < ARRAYSIZE(_dataArray); i++) { + _dataArray[i] = rs->readUint16LE(); + } + + _wpnBasedTimeout = rs->readSint32LE(); + _difficultyBasedTimeout = rs->readSint32LE(); + + _timer2 = rs->readSint32LE(); + _timer3 = rs->readSint32LE(); + _timer4 = rs->readSint32LE(); + _timer5 = rs->readSint32LE(); + _soundTimestamp = rs->readSint32LE(); + _fireTimestamp = rs->readSint32LE(); + + return true; +} + +} // End of namespace Ultima8 +} // End of namespace Ultima diff --git a/_tmp_scummvm_combat_dat.h b/_tmp_scummvm_combat_dat.h new file mode 100644 index 0000000..0720bd5 --- /dev/null +++ b/_tmp_scummvm_combat_dat.h @@ -0,0 +1,75 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef WORLD_ACTORS_COMBAT_DAT_H +#define WORLD_ACTORS_COMBAT_DAT_H + +#include "common/stream.h" + +#include "common/str.h" + +namespace Ultima { +namespace Ultima8 { + +/** + * A single entry in the Crusader combat.dat flex. The files consist of 3 parts: + * 1. human-readable name (zero-padded 16 bytes) + * 2. offset table (10x2-byte offsets, in practice only the first 2 offsets are ever used) + * 3. tactic blocks starting at the offsets given in the offset (in practice only 2 blocks are used) + * + * The tactic blocks are a sequence of opcodes of things the NPC should + * do - eg, turn towards direction X. + */ +class CombatDat { +public: + CombatDat(Common::SeekableReadStream &rs); + + ~CombatDat(); + + const Common::String &getName() const { + return _name; + }; + + const uint8 *getData() const { + return _data; + } + + uint16 getOffset(int block) const { + assert(block < ARRAYSIZE(_offsets)); + return _offsets[block]; + } + + uint16 getDataLen() const { + return _dataLen; + } + +private: + Common::String _name; + + uint16 _offsets[4]; + uint8 *_data; + uint16 _dataLen; +}; + +} // End of namespace Ultima8 +} // End of namespace Ultima + +#endif diff --git a/crusader_decompilation_notes.md b/crusader_decompilation_notes.md index 12ae566..cb874b3 100644 --- a/crusader_decompilation_notes.md +++ b/crusader_decompilation_notes.md @@ -4,11 +4,17 @@ This file is an index. Detailed notes have been split into the `docs/` folder by Active live analysis target is now `CRUSADER.EXE`. Existing `CRUSADER-RAW.EXE` notes remain in scope as cross-reference evidence and should be cited alongside live NE addresses when they support a rename, variable role, or behavior claim. +Recent verified combat-data batch: [docs/combat-dat.md](docs/combat-dat.md) now documents the shipped `COMBAT.DAT` archive end to end. Current best read is that all local Remorse/Regret variants ship the same `14`-record combat-tactic archive, each record contains a `16`-byte name plus four block offsets and bytecode, and the tactic VM is now grounded both in the live `CRUSADER.EXE` helpers (`Attack_SetupForTacticNo`, `Attack_SetupForBlockNo`, `NPC_Get/SetNPCTacticNo`) and in ScummVM's readable Crusader attack-process implementation. The new note also promotes the per-tactic human-readable catalog, including the midpoint-pressure, marker-shuttle, step-out-shoot, and stationary-chaos families. + +Recent verified NE movement/collision batch: [docs/raw-0008-000c.md](docs/raw-0008-000c.md) now extends the live `AreaSearch_CollideMove` lane one helper layer deeper. Current best read is that the collision-storage queue is no longer only anchored at `StorageDataProcess_Create` / `Run` and the legal-move wrappers: the live database now also carries the step-aware seg029 sweep helpers `AreaSearch_SweepShapeBetweenPoints`, `AreaSearch_SweepItemToPointWithStepUp`, and `AreaSearch_SweepShapeBetweenPointsWithStepUp`, the seg031 release-side queue cleanup pair `StorageDataProcess_Release` and `storage_process_ref_list_terminate_item_matches`, and adjacent seg090 helper `ItemCache_PushAndPopToDirectionalOffset`. The practical remaining gap in this lane is now earlier caller policy rather than local helper identity. + Recent map-renderer egg-link follow-up: [docs/map_renderer/egg-identification.md](docs/map_renderer/egg-identification.md) now closes the old No Regret map-`3` destination-egg `102` gap. Current best read is that Regret uses a second elevator family at `shape:400` (`0x0190`) in addition to the earlier Remorse-focused `shape:542` rule: recovered Regret `ELEVATOR::gotHit` accepts `QLo >= 100`, treats `QLo < 0x00c8` as the generic same-map lane, and map `3` contains a concrete source object `item:664:fixed:400:0:44030:9662:0` with `quality 614` (`QLo 102`) that resolves the previously unexplained destination egg `102`. Recent map-renderer editor-object follow-up: [docs/map_renderer/trigger-usecode-links.md](docs/map_renderer/trigger-usecode-links.md) and [docs/map_renderer/editor-object-survey.md](docs/map_renderer/editor-object-survey.md) now promote a Regret-only controller cluster that had still been sitting in the unresolved editor bucket. Current best read is that `0x04c6` / `0x04de` are `WATCHNS` / `WATCHEW` secret-door watcher controllers, `0x0510` is their nearby `SECRET_DOOR_POST` target keyed by shared `QLo`, `0x05e1` is `CRYOBOX`, and `0x05df` / `0x05e0` are the paired pressure-barrier faces it drives by shared `QLo`. The same batch also closes `0x0451` / `0x05ae` as `CRAZYEW` / `CRAZYNS` hit-driven NPC wake-up relays and `0x056d` as `VIDEOBOX`, then promotes cautious local viewer arrows for `WATCH* -> 0x0510` and `CRYOBOX -> 0x05DF/0x05E0` in the map renderer. -Recent map-renderer control-pad follow-up: [docs/map_renderer/egg-identification.md](docs/map_renderer/egg-identification.md), [docs/map_renderer/trigger-usecode-links.md](docs/map_renderer/trigger-usecode-links.md), and [docs/map_renderer/editor-object-survey.md](docs/map_renderer/editor-object-survey.md) now tighten the Regret read on `0x0318` / `0x0366` using the decompressed `.cache` scenes rather than the packed site export. Current best read is that `0x0318` is `CRUMORPH`, a control-transfer pad whose `equip` body scans nearby NPCs for a local-`QLo` key before bracketing `TRIGGER.slot_20`, while `0x0366` remains `NPC_ONLY`, a hit-driven NPC-only trigger pad keyed by an internal actor field. The viewer now promotes cautious same-`QLo` local `CRUMORPH -> 0x04B1` and `NPC_ONLY -> 0x04B1` arrows where authored matches exist, but still keeps `NPC_ONLY -> actor` out of the overlay. +Recent map-renderer control-pad follow-up: [docs/map_renderer/egg-identification.md](docs/map_renderer/egg-identification.md), [docs/map_renderer/trigger-usecode-links.md](docs/map_renderer/trigger-usecode-links.md), and [docs/map_renderer/editor-object-survey.md](docs/map_renderer/editor-object-survey.md) now tighten the `0x0318` / `0x0366` read using the decompressed `.cache` scenes rather than the packed site export. Current best read is that `0x0318` is `CRUMORPH`, not a generic placeholder cube: both extracted usecode corpora expose class `0x0318` as `CRUMORPH`, and the recovered `equip` body is a control-transfer pad that scans nearby NPCs for a local-`QLo` actor-key match before bracketing `TRIGGER.slot_20`. The strongest current viewer promotion is still grounded in Regret scene evidence, where authored same-`QLo` local `CRUMORPH -> 0x04B1` matches are strong enough to expose, while `0x0366` remains `NPC_ONLY`, a hit-driven NPC-only trigger pad keyed by an internal actor field. + +Recent map-renderer controller follow-up: [docs/map_renderer/trigger-usecode-links.md](docs/map_renderer/trigger-usecode-links.md) and [docs/map_renderer/editor-object-survey.md](docs/map_renderer/editor-object-survey.md) now tighten three more shared controller shapes. Current best read is that `0x00A2` is `PANELEW`, the east-west panel-switch counterpart to `PANELNS`; `0x03C1` is `GENERATR`, a destroyable generator/controller whose `gotHit` body immediately forwards into `TRIGGER.slot_20` lane `0`; and `0x04E7` is the same `DEATHBOX` class in both Remorse and Regret rather than a Remorse-only crosswalk. The map viewer now has enough evidence to label those shapes directly, open `PANELEW::use` / `GENERATR::gotHit`, and expose cautious same-`QLo` cmd-link arrows for `PANELEW` and `GENERATR`. Recent actor-key follow-up: the same map-renderer notes now make the current blocker explicit instead of leaving it as an implied missing export. Current best read is that the hidden actor-side value behind `CRUMORPH` / `NPC_ONLY` is mutable actor field `0x63`, not a stable DTABLE row: sampled Regret DTABLE rows still read as zero at record byte `0x63`, while recovered `TRIGGER.slot_29` / `slot_2B` lanes can rewrite actor field `0x63` on nearby matched NPCs after load. The same pass also widens the sibling-family set that uses this mechanism: `WATCHNS` / `WATCHEW`, `THRMBCKN` / `THRMBCKE`, and `SURCAMNS` / `SURCAMEW` all compare controller-local bytes against actor field `0x63` in recovered lanes, so the viewer now documents a broader actor-key controller family while still withholding speculative actor-target arrows. @@ -67,6 +73,7 @@ Latest F7 overlay follow-up: new note [docs/f7-overlays.md](docs/f7-overlays.md) | File | Contents | |------|----------| | [docs/overview.md](docs/overview.md) | Binary overview, installed copy findings, address space layout, NE fixup placeholder, segment map, NE import details, next steps | +| [docs/combat-dat.md](docs/combat-dat.md) | `COMBAT.DAT` archive layout, live `CRUSADER.EXE` tactic-field integration, shipped opcode subset, and a human-readable catalog of all `14` tactic records | | [docs/phar-lap-extender.md](docs/phar-lap-extender.md) | DOS extender architecture, named functions (entry, loading, memory, I/O, interrupts), key string references | | [docs/ne-segment1.md](docs/ne-segment1.md) | NE Segment 1 full analysis: cursor, input, entity system, shot lifecycle, combat, weapons, AI, player/HUD, destruction, entity constants, vtable index, cheat system | | [docs/f7-overlays.md](docs/f7-overlays.md) | Focused note on the three cheat-gated F7 debug overlays: toggle sites, live consumers, recovered geometry math, what each overlay represents, and the current viewer-safe reproduction rules | @@ -83,17 +90,30 @@ Latest F7 overlay follow-up: new note [docs/f7-overlays.md](docs/f7-overlays.md) | [docs/ne-hole-filling-priorities.md](docs/ne-hole-filling-priorities.md) | Ranked `CRUSADER.EXE` hole-filling tracker: NE-side unclear lanes, the verified raw-side knowledge that can close them, and the recommended order for old-to-new porting passes | | [docs/retail-debugger-patch-attempts.md](docs/retail-debugger-patch-attempts.md) | Chronological log of retail `CRUSADER.EXE` debugger-unlock patch attempts, byte-level designs, runtime failures, root-cause findings, and the current live candidate | | [docs/retail-debug-arg.md](docs/retail-debug-arg.md) | Focused note on the retail `-debug` command-line switch: live parser evidence, exact startup message, surviving globals, segment `1468` instrumentation path, and why it is currently separate from the hidden usecode debugger bootstrap | +| [docs/remorse-class-candidate-inventory.md](docs/remorse-class-candidate-inventory.md) | Evidence-backed inventory of the strongest current Remorse class families, with confidence, ctor/dtor/vtable/layout anchors, and recommended modeling order for later Ghidra class work | +| [docs/remorse-class-lift-index.md](docs/remorse-class-lift-index.md) | Central navigation note for the Remorse class-lift and C++-reconstruction prep lane, grouping the plan, candidate inventory, ABI notes, endpoint spec, and family-specific layout notes into one work order | +| [docs/remorse-first-class-authoring-checklist.md](docs/remorse-first-class-authoring-checklist.md) | Operational checklist for the first real Ghidra/MCP class-authoring batch, including pilot-family order, authoring rules, and source-emission readiness gates | +| [docs/remorse-cpp-decompilation-plan.md](docs/remorse-cpp-decompilation-plan.md) | Plan for shifting the current Remorse decompilation from flat C-like recovery toward evidence-backed C++ classes, typed object models, and an eventual recompilable source tree | +| [docs/remorse-cpp-compatibility-header-draft.md](docs/remorse-cpp-compatibility-header-draft.md) | Draft contract for the future compatibility/support header that early Remorse C++ skeletons should target: exact-width aliases, packing markers, calling-convention placeholders, segmented-pointer helpers, and slot-order guardrails | +| [docs/remorse-toolchain-fingerprint-evidence.md](docs/remorse-toolchain-fingerprint-evidence.md) | Focused evidence note for the current toolchain story behind Remorse reconstruction: bound `MZ -> NE` structure, Phar Lap runtime, loader-patched far calls, and the current High-C-related runtime fingerprints | +| [docs/ghidra-mcp-class-lifting-endpoint-spec.md](docs/ghidra-mcp-class-lifting-endpoint-spec.md) | Draft endpoint surface for future GhidraMCP class-lifting work: namespace/class creation, symbol moves, struct and vtable authoring, `this` typing, and transactional class-layout application | | [docs/scummvm-crusader-reference.md](docs/scummvm-crusader-reference.md) | ScummVM Ultima8/Pentagram Crusader integration survey: USECODE/event tables, FLEX/resource formats, world/map loaders, HUD/media, and RE follow-up priorities | | [docs/pentagram-crusader-reference.md](docs/pentagram-crusader-reference.md) | Pentagram-source Crusader/U8 reference: direct Crusader USECODE parser and VM evidence, U8 usecode docs, runtime-confidence limits, and cross-checks against the ScummVM note | | [docs/map-rendering.md](docs/map-rendering.md) | Offline map-rendering lane: `FIXED.DAT`/`GLOB.FLX`/`SHAPES.FLX`/`GAMEPAL.PAL` format notes, current Python renderer, supported inputs, and fidelity gaps | | [docs/editor-object-visibility.md](docs/editor-object-visibility.md) | Focused note on retail editor-only map object hiding: the live `1198:02e4` `SI_EDITOR` early-out in the normal item paint path, the lack of a recovered retail visibility toggle, and the ScummVM/Pentagram cross-check that treats `show editor items` as an engine-side debug feature | +| [docs/entity-class-family-split.md](docs/entity-class-family-split.md) | Focused working note on the large seg001 `Entity` lane: shared base-layout evidence, conservative split into projectile, debris, corpse/remnant, and adjacent non-entity families, and the recommended promotion order for later class lifting | +| [docs/entity-dispatch-entry-class-layout.md](docs/entity-dispatch-entry-class-layout.md) | Focused working note for the `EntityDispatchEntry` family: base versus derived split, stable field groups, constructor and release surfaces, candidate method map, and conservative future Ghidra modeling order | +| [docs/entity-vm-runtime-owner-resource-layout.md](docs/entity-vm-runtime-owner-resource-layout.md) | Focused working note for the VM runtime lane: `EntityVmRuntime`, `EntityVmOwnerResource`, and `EntityVmContext` ownership, stable layout claims, masked-create helpers, and the safest current class-lift order | +| [docs/presentation-callback-broker-layout.md](docs/presentation-callback-broker-layout.md) | Focused working note for the `0x4588` callback-object lane: install/teardown lifecycle, global state cluster, provisional vtable slots, payload-pair evidence, and conservative class-lift guidance | | [docs/map_renderer/trigger-usecode-links.md](docs/map_renderer/trigger-usecode-links.md) | Evidence-backed map-viewer note for editor/controller shapes that now expose direct USECODE navigation, including the stable class/event targets and the special `TRIGGER.slot_20` handling for `0x04B1` cmd helpers | | [docs/map_1_spawners_targeted_investigation.md](docs/map_1_spawners_targeted_investigation.md) | Focused map-1 note on suspicious `0x04D0` frame-paired spawners: decompressed-cache examples, the recovered `MONSTER -> ITEM.slot_2D -> create NPC` chain, QLo-based pairing, and the corrected `mapNum bit 0x08` enter-area interpretation | | [docs/first-mission-map-selection.md](docs/first-mission-map-selection.md) | Focused note on fresh-game startup map selection: No Remorse `Game_Start`, No Regret's early and later mission-start selectors, the separate embedded `-warp mission` table, and the split between code-selected startup and external `FIXED.DAT` map content | | [docs/regret-game-start.md](docs/regret-game-start.md) | Detailed `REGRET.EXE` startup-flow note: `Game_Start`, `Game_RunNewGameFlow`, newly named helpers, startup override globals, and the current best explanation for the duplicated map-1 selector | +| [docs/remorse-rebuild-abi-notes.md](docs/remorse-rebuild-abi-notes.md) | Working note for rebuild constraints: segmented-memory model, far-call provenance, runtime/toolchain evidence, ABI guardrails, and the split between original-style executable reconstruction and a behaviorally equivalent port | | [docs/command-line-parameters.md](docs/command-line-parameters.md) | Consolidated startup/debug argument reference for the retail Crusader executables: live retail `-u` usecode override, the current `-setver` caution, `-debug`, `-asylum`, `-warp`, `-skill`, `-mapoff`, `-egg`, `-demo`, the `-laurie` cross-reference, and the evidence-backed direct-coordinate warp syntax/limits | | [docs/psx/psx.md](docs/psx/psx.md) | PlayStation `SLUS_002.68` and disc-resource note: boot/load layout, `LSET`/menu WDL structure, executable-backed map inventory, passcode alphabet/display path, recovered PSX ammo/item/weapon tables, and current unresolved enemy/password-compare gaps | | [docs/psx/prealpha.md](docs/psx/prealpha.md) | PlayStation pre-pre alpha `/psx/prealpha/SLUS_002.68` comparison note: reduced disc inventory, retained retail-style `LSET` loader, surviving No Remorse branding, stale `TALK1.XA` and `LoadExec` leftovers, and the current read that this build is closer to an unfinished No Remorse PSX branch than to a visibly rebranded sequel executable | +| [docs/sprite-node-class-layout.md](docs/sprite-node-class-layout.md) | Focused working note for the `SpriteNode` family: current core layout, destructor and event-dispatch evidence, candidate virtual slots, and a conservative Ghidra modeling plan | | [docs/usecode-startup-override.md](docs/usecode-startup-override.md) | Focused retail `-u` deep dive: startup call order, why the override looks like full live-root replacement rather than addition, which event/process/interpreter consumers use that root, and what that implies for future custom usecode experiments | | [docs/usecode-roundtrip-ir.md](docs/usecode-roundtrip-ir.md) | ScummVM-to-binary USECODE cross-walk, owner-loaded class-layout and header/event-count reconciliation, conservative IR v0 plan, and the generated class-event/body-window outputs that now ground reversible `_BOOT`, `SURCAM*`, and environmental family decompile artifacts plus repeated-family regression checks | | [docs/usecode-pentagram-ghidra-path.md](docs/usecode-pentagram-ghidra-path.md) | Pentagram-derived Crusader USECODE parser plan, proof-of-concept workflow, canonical IR v1 goals, and the Ghidra-side annotation import path | diff --git a/crusader_segment_coverage_ledger.csv b/crusader_segment_coverage_ledger.csv index 9c8d5d2..67db405 100644 --- a/crusader_segment_coverage_ledger.csv +++ b/crusader_segment_coverage_ledger.csv @@ -27,9 +27,9 @@ "26","code","0x5AE00","0x4DE","None","","","","crusader_ne_segments.csv" "27","code","0x5B400","0x57B","None","","","","crusader_ne_segments.csv" "28","code","0x5BA00","0x788","None","","","","crusader_ne_segments.csv" -"29","code","0x5C400","0x190A","None","","","","crusader_ne_segments.csv" +"29","code","0x5C400","0x190A","Partial","Area-search collision move and step-aware sweep helper lane","AreaSearch_CollideMove; AreaSearch_SweepShapeBetweenPoints; AreaSearch_SweepItemToPointWithStepUp; AreaSearch_SweepShapeBetweenPointsWithStepUp","Current direct callers are still all movement/collision-side and the remaining gap is the earlier policy layer deciding when those paths instantiate the 0x236 queue or whether any non-collision lane feeds it","plan-mid.md; docs/ne-hole-filling-priorities.md; docs/raw-0008-000c.md" "30","code","0x5E000","0x5071","None","","","","crusader_ne_segments.csv" -"31","code","0x64000","0x6EE","None","","","","crusader_ne_segments.csv" +"31","code","0x64000","0x6EE","Partial","StorageDataProcess queue create/run/release helpers","StorageDataProcess_Create; StorageDataProcess_Run; StorageDataProcess_Release; StorageDataProcess_RunAndTerminateProcs; storage_process_ref_list_create; storage_process_ref_list_append; storage_process_ref_list_terminate_item_matches; storage_process_ref_list_destroy","Still needs caller-side recovery beyond the verified collision producer and any stronger subsystem naming than the local hit/got-hit storage queue","plan-mid.md; docs/ne-hole-filling-priorities.md; docs/raw-0008-000c.md" "32","code","0x64800","0x56A","None","","","","crusader_ne_segments.csv" "33","code","0x65000","0x10D7","None","","","","crusader_ne_segments.csv" "34","code","0x66600","0x253A","None","","","","crusader_ne_segments.csv" @@ -88,7 +88,7 @@ "87","code","0xA2800","0x50C","None","","","","crusader_ne_segments.csv" "88","code","0xA2E00","0x523","None","","","","crusader_ne_segments.csv" "89","code","0xA3400","0x373","None","","","","crusader_ne_segments.csv" -"90","code","0xA3800","0x9C6","None","","","","crusader_ne_segments.csv" +"90","code","0xA3800","0x9C6","Partial","Item movement legality, collision-info, and directional cache-offset helpers","Item_LegalMoveToPoint; Item_LegalMoveToPointWithCollisionInfo; ItemCache_PushAndPopToDirectionalOffset","Still needs the surrounding item-move dispatcher/flags policy and stronger linkage to the upstream queue-instantiation layer","plan-mid.md; docs/ne-hole-filling-priorities.md; docs/raw-0008-000c.md" "91","code","0xA4400","0x6FA","Partial","RNG, fatal-report helpers, and runtime callback/video-state lifecycle","fatal_error_report_fmt_a_and_exit; fatal_error_report_buffered_fmt_and_exit; fatal_error_report_fmt_c_and_exit; rng_set_seed; rng_next_modulo; runtime_callback_object_init_once; runtime_callback_object_teardown_once; video_bios_state_snapshot; video_mode_set_and_record_state","Exact fatal-report template text and callback object subsystem identity still need tighter recovery","crusader_decompilation_notes.md; plan-mid.md" "92","code","0xA4E00","0x59E","None","","","","crusader_ne_segments.csv" "93","code","0xA5600","0x4F1","None","","","","crusader_ne_segments.csv" diff --git a/docs/combat-dat.md b/docs/combat-dat.md new file mode 100644 index 0000000..362b1a8 --- /dev/null +++ b/docs/combat-dat.md @@ -0,0 +1,484 @@ +# COMBAT.DAT + +## Scope + +This note documents the shipped `COMBAT.DAT` used by the Crusader builds in this workspace. + +Verified corpus facts: + +- `STATIC/COMBAT.DAT`, `STATIC_1.01/COMBAT.DAT`, `STATIC_DEMO/COMBAT.DAT`, `STATIC_JP/COMBAT.DAT`, `STATIC_REGRET/COMBAT.DAT`, and `STATIC_REGRET_DEMO/COMBAT.DAT` are byte-identical. +- All six files are `1734` bytes and share SHA-256 `c6097a721141a2e66ec6d0cd578427305f28ae9efe8a206bd5cf946ce2075faf`. +- ScummVM's Crusader support code and the live `CRUSADER.EXE` database agree that this file drives NPC ranged-combat tactics through a small bytecode interpreter. + +This note uses three evidence sources together: + +- direct binary parsing of the shipped `COMBAT.DAT` +- live `CRUSADER.EXE` function/type names already present in the workspace export map +- ScummVM Crusader-source behavior as a readable reference model for the tactic interpreter + +## High-Level Findings + +- The file is a small FLEX-style archive with `14` populated tactic records. +- Each tactic record has a zero-padded `16`-byte name, followed by four `uint16` block offsets, followed by bytecode. +- In the shipped data, only block `0` and block `1` matter. Block `0` acts like an entry/setup phase. Block `1` acts like the steady-state loop. The third and fourth offset slots are present but only contain near-EOF placeholder values in this file. +- The tactic bytecode is not just a name table. It is real AI scripting: face target, move, pathfind, test line-of-fire, loop, jump, sleep, and switch tactic/block. +- Several tactics are waypoint-driven. ScummVM identifies opcode `0xA6` as a search for nearby shape `0x33A` marker objects by frame number; that matches the naming of `OneToTwo`, `ThreeToFour`, `EggHopper1`, and `123_Shoot`. +- There is one notable model discrepancy to keep in mind: the shipped archive contains a valid entry `0` named `Dumb`, but ScummVM still special-cases tactic number `0` as a generic built-in attack path rather than always interpreting `COMBAT.DAT` entry `0`. Treat that as an open compatibility detail rather than silently preferring one model. + +## Archive Layout + +### Outer Container + +Observed on-disk structure: + +- file size: `0x06c6` +- first populated record starts at `0x0280` +- archive directory begins at `0x0080` +- directory entries are `8` bytes each: `` +- the shipped file leaves most directory slots empty and populates only `14` entries + +Populated record table: + +| Index | File offset | Length | Name | +|------:|------------:|-------:|------| +| 0 | `0x0280` | `0x004f` | `Dumb` | +| 1 | `0x02cf` | `0x003f` | `Pivot` | +| 2 | `0x030e` | `0x004f` | `Advance` | +| 3 | `0x035d` | `0x0047` | `Mental` | +| 4 | `0x03a4` | `0x0052` | `Careful` | +| 5 | `0x03f6` | `0x004c` | `OneToTwo` | +| 6 | `0x0442` | `0x004c` | `ThreeToFour` | +| 7 | `0x048e` | `0x0074` | `EggHopper1` | +| 8 | `0x0502` | `0x0056` | `StepOutShootNE` | +| 9 | `0x0558` | `0x005c` | `StepOutShootNW` | +| 10 | `0x05b4` | `0x0046` | `Random_CHAOS` | +| 11 | `0x05fa` | `0x003b` | `Static_Chaos` | +| 12 | `0x0635` | `0x0043` | `Stand_Choas` | +| 13 | `0x0678` | `0x004e` | `123_Shoot` | + +Two names preserve the shipped typos/spelling: + +- `Stand_Choas` +- `Random_CHAOS` / `Static_Chaos` mixed casing + +### Inner Record Format + +Per record: + +| Offset | Size | Meaning | +|-------:|-----:|---------| +| `0x00` | `16` | ASCII tactic name, NUL-padded | +| `0x10` | `2` | block 0 start offset | +| `0x12` | `2` | block 1 start offset | +| `0x14` | `2` | block 2 start offset | +| `0x16` | `2` | block 3 start offset | +| `0x18+` | variable | bytecode stream | + +Observed block-offset pattern: + +- Most tactics use block 0 at `0x002c` and block 1 at `0x0031`. +- The waypoint tactics `OneToTwo`, `ThreeToFour`, and `123_Shoot` have a longer setup block and therefore move block 1 farther forward. +- The later two offset slots are usually `0x004d` and `0x004e` even when they point at the final sentinel bytes rather than real independent blocks. + +ScummVM's comment in `combat_dat.h` says the format has `10x2-byte offsets`, but both its constructor and the live executable logic only use four offsets. The shipped file matches the implementation, not the stale comment. + +## Live Executable Integration + +The live `CRUSADER.EXE` export map already carries the important attack-process fields: + +- `combatDatTacticPtr` at attack-process offset `0x45` +- `combatDatTacticPtr2` at `0x49` +- `combatDatTacticCurOffset` at `0x4d` +- `combatDatBlockNo` at `0x4f` +- `tacticNo` at `0x51` + +Relevant live helpers: + +- `1108:0586` `Attack_SetupForTacticNo` +- `1108:0506` `Attack_SetupForBlockNo` +- `10e8:3572` `NPC_GetNPCTacticNo` +- `10e8:358c` `NPC_SetNPCTacticNo` + +Current best read of the runtime flow: + +1. NPC state stores a tactic number in `ItemNPCData.field21_0x5c`. +2. `Attack_SetupForTacticNo` validates that the tactic slot has loaded COMBAT.DAT data, stores the selected tactic number in the attack process, and copies the far pointer to that archive entry into the process. +3. `Attack_SetupForBlockNo` selects one of the record's four offset words and seeds `combatDatTacticCurOffset` from it. +4. The attack-process main loop reads one opcode byte at a time from the selected block and executes it as AI logic. + +ScummVM's Crusader attack-process code adds one more important detail: + +- `readNextWordWithData()` treats any immediate operand `>= 33000` as an index into a `10`-entry tactic-local variable array instead of as a literal value. + +The shipped `COMBAT.DAT` in this workspace does not appear to use that variable indirection. All operands decoded here are direct literals. + +## Used Opcode Set + +Only this subset is actually used by the shipped tactics: + +| Opcode | Mnemonic | Meaning | +|------:|----------|---------| +| `0x84` | `set_target_objid` | Set attack target object id | +| `0x85` | `anim_walk` | Play walk/advance animation | +| `0x88` | `turn_left_90` | Turn `90` degrees left | +| `0x89` | `turn_right_90` | Turn `90` degrees right | +| `0x8a` | `fire_small_if_clear` | Fire if line-of-fire is valid | +| `0x8d` | `pathfind_home` | Pathfind to actor home position | +| `0x8f` | `pathfind_midpoint` | Pathfind to midpoint between actor and target | +| `0x93` | `sleep_scaled` | Sleep for N ticks, scaled by difficulty | +| `0x94` | `loiter` | Run a loiter sub-process for N ticks | +| `0x95` | `face_target` | Turn toward target center | +| `0x9a` | `jump_if_dist_lt_481` | Jump if target distance is under `481` | +| `0x9c` | `jump_if_shot_blocked` | Jump if `fireDistance()` fails | +| `0x9d` | `jump_if_shot_clear` | Jump if `fireDistance()` succeeds | +| `0x9f` | `loop_begin` | Set loop counter and remember current stream position | +| `0xa6` | `pathfind_marker_frame` | Find nearby shape `0x33a` marker by frame and pathfind to it | +| `0xa9` | `face_east` | Turn east | +| `0xaa` | `face_west` | Turn west | +| `0xc0` | `jump` | Unconditional jump to stream offset | +| `0xc1` | `loop_end` | Decrement loop counter and jump back while nonzero | +| `0xff` | `flip_to_block1_restart` | If still in block 0, switch to block 1; then restart current block | + +Practical interpretation of `0xff` in shipped data: + +- In block `0`, it is a one-way handoff into block `1`. +- In block `1`, it behaves like `restart block 1 forever`. + +That is why most tactics have a very short block `0`: setup once, then live in block `1`. + +## Tactic Catalog + +### 0. `Dumb` + +Role: + +- simplest mobile shooter with midpoint fallback + +Decoded behavior: + +- block 0: target object `1`, face target, then hand off to block 1 +- block 1: + - if a shot is already clear, jump straight into a `3`-iteration fire loop + - otherwise pathfind to the midpoint between NPC and target, face target, and re-test line-of-fire + - if still not clear after midpoint attempts, loiter briefly + - once clear, loop `3` times: face target, fire, sleep `30` + - restart block 1 + +Human read: + +- close some distance toward a firing lane, then burst-fire three times + +### 1. `Pivot` + +Role: + +- stationary pivot-and-shoot burst + +Decoded behavior: + +- block 0: target object `1`, face target, hand off +- block 1: + - if line-of-fire is clear, enter a `3`-shot loop immediately + - loop body: face target, fire, sleep `30` + - restart + +Human read: + +- no movement logic beyond facing the target; just turn and shoot in bursts + +### 2. `Advance` + +Role: + +- move forward between bursts if a clear shot is available; midpoint fallback otherwise + +Decoded behavior: + +- block 0: target object `1`, face target, hand off +- block 1: + - face target + - if shot is blocked, jump to midpoint-reposition logic + - otherwise run a `3`-iteration loop: face target, fire, loop + - sleep `30` + - play walk animation + - restart + - midpoint branch: pathfind midpoint, face target, re-test for clear shot, loiter briefly if still blocked, restart + +Human read: + +- pressure forward when able to shoot, otherwise drift toward a midpoint lane until a shot opens + +### 3. `Mental` + +Role: + +- shoot if clear; otherwise midpoint hop and shoot + +Decoded behavior: + +- block 0: target object `1`, face target, hand off +- block 1: + - if shot is blocked, pathfind to midpoint first + - run a `3`-iteration fire loop + - restart + +Human read: + +- simpler than `Advance`: it does not include the extra walk/sleep pressure cycle, only burst-fire with midpoint correction + +### 4. `Careful` + +Role: + +- range-gated cautious shooter + +Decoded behavior: + +- block 0: target object `1`, face target, hand off +- block 1: + - if target distance is not under `481`, loiter and restart + - if within `481` and shot is clear, fire once and restart + - if blocked, face target and test again + - if still blocked, pathfind midpoint and test again + - if still blocked, face target and test again + - if still blocked after all of that, loiter briefly and restart + +Human read: + +- only engages at medium/close range and spends more effort finding a clean shot before moving or firing + +### 5. `OneToTwo` + +Role: + +- shuttle between marker frame `1` and marker frame `2` + +Decoded behavior: + +- block 0: + - target object `1` + - pathfind to marker frame `1` + - face target + - hand off +- block 1: + - pathfind to marker frame `2` + - face target + - run a `3`-iteration fire loop while line-of-fire is clear + - return to marker frame `1` + - face target + - sleep `120` + - restart block 1 + +Human read: + +- waypoint-based peek-and-return behavior across two authored combat markers + +### 6. `ThreeToFour` + +Role: + +- same pattern as `OneToTwo`, but between marker frames `3` and `4` + +Decoded behavior: + +- block 0: target object `1`, face target, pathfind marker `3`, hand off +- block 1: + - pathfind marker `4` + - face target + - if shot is clear, run a `3`-shot burst loop + - return to marker `3` + - face target + - sleep `120` + - restart + +Human read: + +- authored two-point lateral or cover movement using a second pair of marker frames + +### 7. `EggHopper1` + +Role: + +- multi-marker patrol shooter + +Decoded behavior: + +- block 0: target object `1`, face target, hand off +- block 1: + - pathfind marker `0`, attempt a `3`-shot loop + - pathfind marker `1`, attempt a `3`-shot loop + - pathfind marker `2`, attempt a `3`-shot loop + - pathfind marker `3`, attempt a `3`-shot loop + - pathfind home + - face target + - restart + +Human read: + +- sweep across four authored combat markers in order, trying a short firing burst at each stop, then return home + +### 8. `StepOutShootNE` + +Role: + +- east-west step-out gunner, nominally biased to an east-facing start + +Decoded behavior: + +- block 0: target object `1`, face east, hand off +- block 1: + - walk outward + - face target + - up to `3` short fire attempts with `5`-tick delays + - face west + - walk back + - face target + - another `3` short fire attempts with `5`-tick delays + - if blocked, wait `25` ticks instead of firing through + - face east + - restart + +Human read: + +- pop out from one side, take a short burst, step back across the lane, burst again, and reset orientation + +### 9. `StepOutShootNW` + +Role: + +- mirror-image of `StepOutShootNE`, nominally biased to a west-facing start + +Decoded behavior: + +- block 0: target object `1`, face west, hand off +- block 1: + - walk outward from the west-facing side + - face target and attempt short burst fire + - if blocked, delay `25` + - face east and walk across + - repeat the short-burst pattern + - face west and restart + +Human read: + +- same authored cover-pop logic as the northeast variant, but with opposite home orientation + +### 10. `Random_CHAOS` + +Role: + +- immediate short burst, then aggressive midpoint pressure + +Decoded behavior: + +- block 0: + - target object `1` + - face target + - run a `2`-shot loop with `30`-tick sleeps + - hand off +- block 1: + - face target + - pathfind midpoint + - face target + - run a `3`-shot loop with `30`-tick sleeps + - restart block 1 forever + +Human read: + +- start with a short static burst, then keep pressing toward midpoint and firing + +### 11. `Static_Chaos` + +Role: + +- pure stationary burst shooter + +Decoded behavior: + +- block 0: target object `1`, face target, hand off +- block 1: + - face target + - loop `3` times: fire, loop + - sleep `30` + - restart + +Human read: + +- no pathfinding at all; just keep facing and firing from the current spot + +### 12. `Stand_Choas` + +Role: + +- stationary turret-like spread pattern + +Decoded behavior: + +- block 0: target object `1`, hand off +- block 1: + - face target + - if shot is clear, fire once and skip to the post-shot delay + - otherwise rotate and fire in a fixed sweep: left, original/right-adjusted, then right again + - sleep `30` + - face target + - restart + +Human read: + +- a stand-and-sweep pattern for targets that are partially obstructed or moving across the arc + +### 13. `123_Shoot` + +Role: + +- two marker hops followed by midpoint pressure + +Decoded behavior: + +- block 0: + - target object `1` + - face target + - pathfind marker `0` + - face target + - pathfind marker `1` + - face target + - run a `2`-shot loop + - hand off +- block 1: + - face target + - pathfind midpoint + - run a `3`-shot loop + - restart + +Human read: + +- staged opening movement across authored markers, then transition into a more ordinary midpoint-pressure gunner + +## Pattern Summary + +Across the whole file, the tactics cluster into a few clear families: + +| Family | Tactics | Shared idea | +|--------|---------|-------------| +| stationary shooters | `Pivot`, `Static_Chaos`, `Stand_Choas` | little or no movement; rely on facing and burst loops | +| midpoint pressers | `Dumb`, `Advance`, `Mental`, `Random_CHAOS`, `123_Shoot` block 1 | move toward a midpoint lane when a shot is blocked | +| cautious/range-gated | `Careful` | only engage inside a distance window and avoid overcommitting | +| authored marker shuttles | `OneToTwo`, `ThreeToFour`, `EggHopper1`, `123_Shoot` block 0 | follow placed map markers keyed by frame number | +| step-out cover shooters | `StepOutShootNE`, `StepOutShootNW` | walk out, burst, walk back, repeat | + +This is enough to treat `COMBAT.DAT` as a compact authored AI-script table rather than a loose name list. + +## Open Questions + +1. Tactic `0` is the main remaining semantic mismatch. The archive contains `Dumb` at index `0`, while ScummVM still routes `_tactic == 0` through `genericAttack()`. The live executable-side helper accepts tactic `0` as a normal COMBAT.DAT slot, so this needs a later direct compiled-side pass if exact retail precedence matters. +2. The later two per-record offset slots are structurally present but operationally unimportant in this file. A future pass could still check whether any retail code path ever selects block `2` or `3`. +3. The marker-search opcode `0xA6` is strongly understood from ScummVM and the tactic names, but the live-game name of shape `0x33A` and the exact authored map placement conventions remain better documented on the map-data side than in the live NE database. + +## Practical RE Use + +For future compiled-side work, the main safe takeaways are: + +- `tacticNo` in NPC data is not cosmetic; it selects a real bytecode program. +- block `0` is usually an initialization or reposition phase; block `1` is the stable loop. +- the named tactics are portable data labels because the shipped file is identical across the local Remorse/Regret variants. +- waypoint-driven tactics should be interpreted together with local marker placements, not only from executable code. \ No newline at end of file diff --git a/docs/entity-class-family-split.md b/docs/entity-class-family-split.md new file mode 100644 index 0000000..c30bf88 --- /dev/null +++ b/docs/entity-class-family-split.md @@ -0,0 +1,170 @@ +# Entity Class Family Split + +## Purpose + +This note breaks the large seg001 `Entity` lane into a conservative class-family model that can later be promoted in Ghidra or emitted as C++ without pretending that every vtable and helper belongs to one monolithic base class. + +Current goal is not full inheritance recovery. Current goal is to identify the safest boundaries between: + +- one shared gameplay-entity core layout +- projectile-specific allocation and movement behavior +- debris/corpse variants +- registry/helper surfaces that look adjacent but should not be merged automatically + +## Core Shared Entity Object + +Strongest current common object is the gameplay entity body documented in [docs/ne-segment1.md](docs/ne-segment1.md). + +Stable shared anchors: + +- `0007:3f2f entity_spawn` +- `0007:40d4 entity_remove` +- `0007:4552 entity_set_position` +- `0007:4591 entity_try_place` +- `0007:5092 entity_deactivate` + +Stable shared fields from the current note set include: + +- `+0x00` vtable pointer +- `+0x02` slot index +- `+0x04` entity type +- `+0x19/+0x1a` flags +- `+0x3c` sprite handle +- `+0x45/+0x47/+0x49` world position +- `+0x4f/+0x51/+0x53` base position +- `+0x54/+0x56/+0x58` previous position + +This is the safest current candidate for a future `Entity` or `ActorBase` style root. + +## Candidate Split + +### 1. `Entity` base gameplay family + +Best current scope: + +- allocation/spawn and placement +- common position, flags, facing, and sprite ownership +- generic remove/deactivate behavior +- registry-facing slot identity + +Best current vtable anchor: + +- generic/AI entity vtable `0x29aa` + +Current caution: + +- this family likely includes several behaviorally different actors, but the verified note set still supports one shared base before the split gets more specific. + +### 2. `ShotEntity` or projectile-derived family + +Strong anchors: + +- `0007:28ce shot_entity_alloc` +- `0007:44a9 shot_entity_free` +- `0007:4659 projectile_init_vector` +- `0007:4b78 projectile_check_hit` +- `0007:4c2e projectile_step_update` +- `0007:4d28 projectile_trace_ray` +- `0007:51ad projectile_update_tick` +- `0007:5a99 projectile_apply_hit` + +Best current distinct evidence: + +- dedicated vtable `0x297e` +- extra projectile ownership/target fields through `+0x6a..+0xbd` +- separate shot sprite handle at `+0x3f` +- dedicated cleanup path in `shot_entity_free` + +Current safest interpretation: + +- projectile objects are not just one mode of the generic entity vtable; they deserve at least one derived-family model with their own ctor/free/update surface. + +### 3. `DebrisEntity` family + +Strong anchors: + +- `0007:7490 debris_spawn` +- `0007:75ff entity_die` + +Best current distinct evidence: + +- dedicated debris/fragment vtable `0x2a57` +- corpse/debris-adjacent vtable pair `0x2a1a` and `0x2a33` +- death-spawn path uses separate velocity/facing behavior rather than only the generic entity update lane + +Current safest interpretation: + +- debris should stay separate from `ShotEntity`; both share movement-style fields but not the same lifecycle intent. + +### 4. `CorpseEntity` or actor-remnant family + +Strong current evidence: + +- vtable pair `0x2a1a` and `0x2a33` +- adjacency to `entity_die` and debris-spawn behavior + +Current caution: + +- the notes support a corpse/remnant family, but not yet a crisp split between static remains, actor corpse, and debris fragments. +- keep this as a provisional derived branch until a dedicated caller pass closes object lifetime more tightly. + +### 5. Adjacent but probably separate: dialog/menu object lane + +Anchor: + +- `0007:2c92 dialog_spawn` + +Why it should stay separate: + +- vtable `0x28b5` +- callback registration at `0x39ca` +- behavior is UI/dialog-style rather than ordinary gameplay entity movement or projectile logic + +Current safest interpretation: + +- this object is near the entity notes because it lives in the same broad segment lane, but it should not be promoted under the core gameplay `Entity` family automatically. + +## Registry And Helper Surfaces That Should Not Be Mis-modeled + +### Entity registry vtable `0x2969` + +Current evidence in [docs/ne-segment1.md](docs/ne-segment1.md) shows `0x2969` stored at `0x39ca + slot*4` as a registry vtable rather than as the entity instance's primary vtable. + +That means: + +- do not treat `0x2969` as a normal `Entity` virtual table +- keep registry or handle-table behavior separate from per-instance inheritance + +### Pure helpers that should remain free functions for now + +Examples: + +- `snap_entity_to_ground` +- `spawn_entity_checked` +- `map_find_spawn_point` +- `actor_find_in_view` + +These may operate on entities or produce entities, but current evidence still reads better as subsystem helpers than as obvious instance methods. + +## Recommended Promotion Order + +1. model the shared `Entity` base layout first +2. split `ShotEntity` next because its ctor/free/update lane is strongest +3. split debris/corpse branches only after one more caller-side lifetime pass +4. leave dialog/menu object modeling separate from the entity inheritance tree + +## Source-Emission Guidance + +If this family is emitted to provisional C++ later, safest first skeleton is: + +- one `Entity` base struct/class with the stable common layout +- one `ShotEntity` derived placeholder +- one `DebrisEntity` derived placeholder +- one unresolved `CorpseLikeEntity` placeholder if needed +- separate `DialogMenuObject` class rather than folding it into the gameplay entity tree + +## Bottom Line + +The current evidence strongly supports a shared gameplay entity core, but it does not support flattening generic actor, projectile, debris, corpse, and dialog/menu behavior into one class. + +The right near-term move is `base first, derived families second, adjacent objects separate`. \ No newline at end of file diff --git a/docs/entity-dispatch-entry-class-layout.md b/docs/entity-dispatch-entry-class-layout.md new file mode 100644 index 0000000..9494292 --- /dev/null +++ b/docs/entity-dispatch-entry-class-layout.md @@ -0,0 +1,244 @@ +# EntityDispatchEntry Class Layout + +## Purpose + +This note is the first focused class-layout working paper for the Remorse C++ lift. + +It takes the broad `EntityDispatchEntry*` inventory entry and narrows it into a base/derived object model that can later be pushed into Ghidra as class namespaces, instance structs, vtable structs, and method ownership. + +The goal is not to claim a final C++ API. The goal is to lock down the pieces that are already stable enough to support later implementation work. + +## Why This Family Goes First + +`EntityDispatchEntry` is the strongest current pilot family because it already has: + +- a clear constructor-style base init path +- multiple derived constructor variants +- explicit owned state and word-list teardown +- stable field groups with known offsets +- repeated virtual-slot dispatch through known offsets +- strong caller evidence across scheduler, runtime-state, palette, and startup/display lanes + +That makes it the best place to prototype the full later workflow: + +- class namespace creation +- method ownership +- instance-struct typing +- vtable typing +- base/derived split +- later C++ skeleton emission + +## Candidate High-Level Model + +Current best working split: + +- `EntityDispatchEntryBase` +- `EntityDispatchEntryTimed` or `EntityDispatchEntryPeriodic` for the `0x3aa6` timing/period variant +- `EntityDispatchEntryRuntimeState` for the later `000d:7e00/8078` runtime-state owned-buffer family + +This should stay a working model, not a hard rename, until the class work lands in Ghidra. + +## Base Constructor Surface + +### `0008:ba00` `entity_dispatch_entry_init` + +Current best read: + +- optional allocate/init path for a `0x32`-byte base object +- stamps base vtable/list-link state using `0x3b06`, `0x2d10`, and `0x3afe` +- zeroes core state fields +- seeds the group/layer byte through `entity_set_group_id` + +This is the strongest current candidate for the base constructor-style init method. + +### Derived constructor variants + +#### `0008:cefb` `entity_dispatch_entry_ctor_vtbl_3ad2` + +- allocates if null +- reinitializes through `entity_dispatch_entry_init` +- sets vtable `0x3ad2` +- sets flag `0x100` at `+0x16` +- zeroes extension words `+0x32/+0x34` + +#### `0008:d214` `entity_dispatch_entry_ctor_vtbl_3aa6` + +- allocates `0x40` bytes if null +- reuses `0008:cefb` +- sets vtable `0x3aa6` +- sets flag `0x200` at `+0x16` +- zeroes fields `+0x38..+0x3e` + +#### Related alloc/init helpers + +- `0004:ea00 entity_dispatch_entry_alloc_type_0f5e` +- `0004:eb1f entity_dispatch_entry_ctor_0f3a_with_cache_reset` + +These look more like subtype-specific factory/create helpers than pure base constructors, but they still belong in the family map. + +## Destroy / Release Surface + +### Base-owned word-list destruction + +#### `0008:dbec` `entity_word_list_destroy` + +- resets vtable to `0x2d10` +- frees list storage if present +- optionally frees object when destroy flag bit `1` is set + +This is the clearest current destructor-style path on the base object. + +### Runtime-state release + +#### `000d:8078` `entity_dispatch_entry_release_runtime_state` + +- frees paired owned buffers +- updates shared hold/owner propagation through `g_active_dispatch_entry_farptr` +- destroys embedded word-list members + +This reads as the release/destructor path for the runtime-state derived family rather than for the whole base type. + +## Current Base Layout + +This table is a working layout, not a finished header. + +| Offset | Current name | Confidence | Current meaning | +|---|---|---|---| +| `+0x00` | `type_or_kind` | Medium | Constructor/factory helpers stamp type words such as `0x0f3a`, `0x0f5e`, or `0x051e` here in some subfamilies. Base-vtable interpretation remains separate. | +| `+0x02` | `slot_index_or_count` | Medium | Used as entry slot/index in several wrappers; also used as count in the base word-list family, so exact role may vary by subtype or overlay. | +| `+0x04` | `source_type` | High | Written by `entity_set_source_type`. | +| `+0x06` | `event_type_or_list_ptr_lo` | Medium | Written by `entity_set_event_type_checked`, but also participates in word-list storage in the list-owning variant. This is likely one of the current overlay collisions to resolve later. | +| `+0x08` | `group_id_byte` | High | Low 5-bit group/layer value managed by `entity_set_group_id`. | +| `+0x0a/+0x0c/+0x0e/+0x10` | `link_or_state_words` | High | Cleared by `entity_dispatch_entry_unlink`; belong to link/extent/target/reset state. | +| `+0x12/+0x14` | `target_farptr` | High | Managed by `entity_flag20_*_target` helpers. | +| `+0x16` | `flags1` | High | Holds bits `0x10`, `0x20`, `0x100`, `0x200`, `0x4000`, and other subtype/state gates. | +| `+0x18` | `flags2` | High | Holds bits `0x40`, `0x80`, `0x100`, `0x400`, `0x1000`; used by unlink, periodic, and refresh paths. | +| `+0x1e/+0x28` | `embedded_dispatch_or_word_list_members` | Medium | Many callsites treat these as subobject or vtable-dispatch bases. Exact split still needs a dedicated subobject note. | +| `+0x24/+0x26`, `+0x2e/+0x30` | `optional_member_ptrs` | Medium | Checked before freeing both embedded word-list members. | +| `+0x32/+0x34` | `extension_words_a` | High | Zeroed by the `0x3ad2` constructor variant; also used by later runtime/VM helper flows. | +| `+0x36/+0x38/+0x3a` | `period_or_schedule_words` | Medium | Written by `entity_set_update_period_and_reschedule`; clearly timing-related in the periodic variant. | +| `+0x3c/+0x3e` | `accumulator_words` | High | Used by `entity_periodic_accumulate_and_dispatch`. | +| `+0x40` | `hold_token` | High | Shared/borrowed hold byte in startup/display and runtime-state families. | +| `+0x41/+0x42/+0x44` | `runtime_state_flags` | High | Initialized by `entity_dispatch_entry_init_runtime_state`. | +| `+0x46/+0x48` | `owned_buffer_a` | High | Runtime-state owned work/palette-like buffer. | +| `+0x4a/+0x4c` | `owned_buffer_b` | High | Second runtime-state owned buffer. | +| `+0x49` | `file_family_selector` | High for the seg126 subtype | Local selector state in startup/display transition family. Likely subtype-specific, not general base meaning. | +| `+0x5b` | `state_flags` | High for the seg126 subtype | State-machine bits in the `000c` startup/display lane. Likely subtype-specific overlay. | +| `+0x520` | `selected_resource` | Medium | Loaded file/resource object in the transition-file-family subtype. | + +## Important Layout Caveat + +This family is almost certainly not one flat struct with universally stable semantics at every offset. Current evidence already shows subtype overlays: + +- base scheduler/dispatch-entry state +- word-list-owning variants +- periodic/timer variants +- startup/display transition variants +- runtime-state/palette-backed variants + +So the safest future Ghidra modeling strategy is: + +1. create a minimal `EntityDispatchEntryBase` +2. create derived or overlay structs for subtype-specific tails +3. avoid prematurely forcing every offset into one monolithic universal class layout + +## Candidate Method Map + +### Strong base methods + +| Address | Current function | Candidate method role | +|---|---|---| +| `0008:ba00` | `entity_dispatch_entry_init` | `InitBase()` | +| `0008:bbb6` | `entity_set_source_type` | `SetSourceType()` | +| `0008:bc27` | `entity_set_event_type_checked` | `SetEventTypeChecked()` | +| `0008:bca8` | `entity_set_group_id` | `SetGroupId()` | +| `0008:bd53` | `entity_dispatch_entry_unlink` | `Unlink()` | +| `0008:be05` | `entity_increment_group_id` | `IncrementGroupId()` | +| `0008:c01d` | `entity_refresh_dispatch_state` | `RefreshDispatchState()` | +| `0008:bfb2` | `entity_clear_status_bits_from_flags` | `ClearStatusBitsFromFlags()` | +| `0008:bf8e` | `entity_call_update_vfunc14` | `CallUpdateSlot14()` | +| `0008:beee` | `entity_run_flagged_handlers` | `RunFlaggedHandlers()` | + +### Pair/link/target helpers + +| Address | Current function | Candidate method role | +|---|---|---| +| `0008:c7f1` | `entity_pair_update_link_slot_a` | `UpdateLinkSlotA()` | +| `0008:c890` | `entity_pair_update_link_slot_b` | `UpdateLinkSlotB()` | +| `0008:c92f` | `entity_pair_sync_a` | `PairSyncA()` | +| `0008:ca18` | `entity_pair_sync_b` | `PairSyncB()` | +| `0008:c9ee` | `entity_pair_mark_and_sync_a` | `MarkAndPairSyncA()` | +| `0008:cad7` | `entity_pair_mark_and_sync_b` | `MarkAndPairSyncB()` | +| `0008:cb2c` | `entity_flag20_clear_and_update_target` | `ClearFlag20AndUpdateTarget()` | +| `0008:cb5c` | `entity_flag20_set_and_init_target` | `SetFlag20AndInitTarget()` | + +### Periodic/timed subtype methods + +| Address | Current function | Candidate method role | +|---|---|---| +| `0008:cefb` | `entity_dispatch_entry_ctor_vtbl_3ad2` | `ConstructVtable3AD2()` | +| `0008:d214` | `entity_dispatch_entry_ctor_vtbl_3aa6` | `ConstructVtable3AA6()` | +| `0008:d313` | `entity_periodic_accumulate_and_dispatch` | `TickPeriodic()` | +| `0008:d3e6` | `entity_set_flag2000_and_update_active_counters` | `EnableActiveCounters()` | +| `0008:d433` | `entity_clear_flag2000_and_update_active_counters` | `DisableActiveCounters()` | +| `0008:d27e` | `entity_set_update_period_and_reschedule` | `SetUpdatePeriodAndReschedule()` | + +### Word-list-owning subtype methods + +| Address | Current function | Candidate method role | +|---|---|---| +| `0008:da00` | `entity_word_list_set_0408_terminated` | `SetWordList0408Terminated()` | +| `0008:dba3` | `entity_word_list_free_existing` | `FreeWordList()` | +| `0008:dbec` | `entity_word_list_destroy` | `Destroy()` | +| `0008:dc38` | `entity_word_list_ensure_contains` | `EnsureWordListContains()` | +| `0008:dcab` | `entity_word_list_append_unique` | `AppendUniqueWord()` | +| `0008:ddaf` | `entity_word_list_remove_value` | `RemoveWordValue()` | +| `0008:deea` | `entity_word_list_get_at` | `GetWordAt()` | +| `0008:df1b` | `entity_word_list_set_at` | `SetWordAt()` | +| `0008:dfa1` | `entity_word_list_find_unflagged_by_id10` | `FindUnflaggedWordById10()` | + +### Runtime-state subtype methods + +| Address | Current function | Candidate method role | +|---|---|---| +| `000d:7e00` | `entity_dispatch_entry_init_runtime_state` | `InitRuntimeState()` | +| `000d:8078` | `entity_dispatch_entry_release_runtime_state` | `ReleaseRuntimeState()` | + +## Candidate Virtual Surface + +The current evidence does not justify a fully named vtable yet, but some slot use is already real: + +- `+0x14` = update callback slot used by `entity_call_update_vfunc14` +- `+0x28` = callback slot used by the periodic and proximity-style dispatch helpers +- embedded subobject/member surfaces at `+0x1e` and `+0x28` are also dispatched through helper wrappers in `far-call-targets.md` + +Recommended future vtable note shape: + +| Slot offset | Current best role | Evidence quality | +|---|---|---| +| `+0x14` | update/refresh callback | High | +| `+0x28` | periodic/dispatch callback | High | +| others | unknown/default stubs | Low | + +## Safe Future Ghidra Modeling Steps + +When manual class work starts, the safest order for this family is: + +1. create class namespace `EntityDispatchEntry` +2. move only the strong base methods first +3. create minimal `EntityDispatchEntryBase` struct with the stable fields through `+0x18` +4. create subtype overlay structs for word-list, timed, and runtime-state tails +5. create a small provisional vtable for only the verified slots + +Do not start by forcing one complete 0x520-byte monolithic class. + +## Questions To Close Later + +- whether `+0x00` should be modeled as a literal `kind` field in all variants or only in some factory-built subtypes +- exact ownership split between the base object and the embedded surfaces at `+0x1e` and `+0x28` +- whether the seg126 startup/display subtype is truly part of the same inheritance family or only shares a lower-level dispatch-entry substrate +- final base-size versus subtype-size boundaries once class namespaces exist in Ghidra + +## Immediate Next Documentation Value + +The next best companion note after this one is a slot-focused `SpriteNode` virtual table note, because that gives a second family with a cleaner explicit virtual surface and helps calibrate how aggressive the first Ghidra class conversion should be. \ No newline at end of file diff --git a/docs/entity-vm-runtime-owner-resource-layout.md b/docs/entity-vm-runtime-owner-resource-layout.md new file mode 100644 index 0000000..5c1678a --- /dev/null +++ b/docs/entity-vm-runtime-owner-resource-layout.md @@ -0,0 +1,169 @@ +# Entity VM Runtime And Owner-Resource Layout + +## Purpose + +This note gathers the current class-lift-relevant structure for the VM runtime lane into one place. + +It focuses on four connected objects: + +- `EntityVmRuntime` +- `EntityVmOwnerResource` +- `EntityVmContext` +- the slot/value helpers that connect gameplay entities to owner-loaded VM source data + +The goal is not full opcode recovery. The goal is to make later class authoring and C++ skeleton emission faster by freezing the current ownership model. + +## High-Level Ownership Model + +Current best model from [docs/raw-0008-000c.md](docs/raw-0008-000c.md) and [docs/raw-000a-000d.md](docs/raw-000a-000d.md): + +1. startup path resolves a configured EUSECODE root/path +2. `entity_vm_runtime_create` allocates the main runtime body +3. runtime constructor attaches one file-backed helper created by `entity_vm_runtime_owner_resource_create` +4. gameplay entities map to slot indices through `entity_vm_slot_index_from_entity` +5. masked-create helpers test owner-side capability bits and then build per-entity or per-slot `EntityVmContext` objects +6. contexts seed their local stream/value state from owner-loaded source rows and runtime slot caches + +## `EntityVmRuntime` + +Strong anchors: + +- `000d:44df entity_vm_runtime_init_from_path_if_configured` +- `000d:4c99 entity_vm_runtime_create` +- `000d:4d36 entity_vm_runtime_init_slots` +- `000d:4d75 entity_vm_runtime_release_slots` +- `000d:4e01 entity_vm_runtime_destroy` + +Current strongest structural claims: + +- runtime body is the global owner behind `0x6611/0x6613` +- front region behaves like a `0x80` entry slot table with stride `0x26` +- tail region around `+0x1300..+0x1318` holds runtime budget/default metadata plus the owner-resource helper pointer +- helper attachment lives at `+0x1315/+0x1317` + +Current safe class role: + +- long-lived VM root object that owns slot state, owner resource, category-base words, and runtime-wide value budgets + +## `EntityVmOwnerResource` + +Strong anchors: + +- `000d:7000 entity_vm_runtime_owner_resource_create` +- `000d:70fd entity_vm_runtime_owner_resource_destroy` + +Best current helper shape: + +- compact file-backed helper object +- helper-owned count at `+0x14` +- far-pointer table at `+0x10` +- paired 16-bit table at `+0x18` +- helper vtable `+0x04` acts as size query +- helper vtable `+0x0c` materializes the `0x0d`-stride owner rows later consumed by contexts + +Current safest interpretation: + +- this is the most bounded class-lift target in the VM lane +- it looks like a real helper object with a compact stable layout and a clear owner relationship to `EntityVmRuntime` + +### seg070 loader contract + +The paired loops rooted at raw windows `0009:67b6` and `0009:6916` are current best evidence that the helper is file-backed rather than a pure in-memory descriptor copier. + +Verified behavior already captured in the main notes: + +- iterate helper-owned count at `+0x14` +- index path/id tables at `+0x10` and `+0x18` +- build formatted paths with two distinct format strings +- open, seek/read, close, and free loop-local buffers through the DOS/file helper lane + +Current caution: + +- exact per-family record schema is still open, so the helper should be modeled as a loader/index object first, not as a final descriptor-schema class. + +## `EntityVmContext` + +Strong anchors: + +- `000d:463a entity_vm_context_try_create_masked_for_entity` +- `000d:46ec entity_vm_context_create_from_slot_index` +- `000d:48b6 entity_vm_context_free_buffer` +- `000d:48da entity_vm_context_sync_global_value_and_dispatch` +- `000d:4962 entity_vm_context_destroy` +- `000d:498f entity_vm_context_save` +- `000d:4a78 entity_vm_context_load` + +Current safe role: + +- per-entity or per-slot execution/context object built from runtime slot state plus owner-loaded source data + +Current layout claims that matter for class lifting: + +- `+0x32` stores slot index +- `+0x34` stores the additive offset word used by the `slot_load_value_plus_offset` lane +- `+0x36` embeds the mini-VM/state object +- `+0xd6/+0xd8` hold the seeded source/control stream lane +- `+0x102` is the backward-growing local payload/buffer lane +- `+0x10c/+0x10e` store a derived low/high pair reused by save/load +- `+0x117/+0x119` cache the owner-linked source pair +- `+0x123` behaves as a busy or active flag in the sync/dispatch path + +Current caution: + +- context dispatch semantics are still active work, so this object should be modeled around lifecycle and data ownership first, not around final method names for every opcode-facing helper. + +## Gameplay Entity To VM Bridge + +### Slot selection + +`entity_vm_slot_index_from_entity` (`000d:45c5`) is the key bridge from gameplay entity identity into the VM lane. + +Current safest summary: + +- it does not choose `NPCTRIG` versus `EVENT` directly +- it maps gameplay entities into category spans using runtime base words such as `0x8c7c/0x8c7e/0x8c80` +- owner-row capability bits and later slot-value materialization do the next stage of filtering + +### Masked-create helpers + +The masked-create family is already class-lift relevant even before final event labels are known. + +What is safe now: + +- the hub at `000d:463a` checks runtime-disable state and owner-side mask bits +- low-slot and high-slot wrappers differ by slot id, mask, and whether they pass an extra signed/additive word +- wrappers like slot `0x0a` / `0x0b` are offset-specialized context creators, not separate selector universes + +This means future Ghidra or C++ modeling should treat them as helper factories around `EntityVmContext`, not as methods on unrelated gameplay classes. + +## Best Class-Lift Targets In This Lane + +1. `EntityVmOwnerResource` +2. `EntityVmRuntime` +3. `EntityVmContext` + +Why this order: + +- owner-resource helper is compact and structurally bounded +- runtime has clear ownership over the helper and slot table +- context has the richest semantics but also the most unresolved dispatcher behavior + +## Source-Emission Guidance + +If emitted as provisional C++ later, safest early skeleton is: + +- `EntityVmOwnerResource` with explicit loader/index fields and placeholder virtual/helper methods +- `EntityVmRuntime` with fixed-size slot table, owner pointer, category-base fields, and create/destroy methods +- `EntityVmContext` with exact saved-field placeholders and a distinct embedded mini-VM state member + +Avoid in the first skeleton: + +- speculative opcode enums presented as final +- collapsing the owner-resource helper into plain runtime fields +- flattening the source/control stream pair into one host-only pointer abstraction if Track A remains active + +## Bottom Line + +The VM lane now supports a real class model, but it should start with ownership and layout rather than with overconfident script-semantic names. + +The most defensible current model is `runtime owns helper and slot state; contexts are short-lived objects built from slot selection plus owner-loaded source rows`. \ No newline at end of file diff --git a/docs/ghidra-mcp-class-lifting-endpoint-spec.md b/docs/ghidra-mcp-class-lifting-endpoint-spec.md new file mode 100644 index 0000000..f5825ec --- /dev/null +++ b/docs/ghidra-mcp-class-lifting-endpoint-spec.md @@ -0,0 +1,439 @@ +# GhidraMCP Class-Lifting Endpoint Spec + +## Purpose + +This note drafts the endpoint surface needed to support the Remorse class-lifting workflow described in `docs/remorse-cpp-decompilation-plan.md` and grounded by `docs/remorse-class-candidate-inventory.md`. + +This is not an implementation batch. It is a local design spec so that when MCP work resumes later, the endpoint set can be built in a way that matches the actual reverse-engineering workflow instead of a generic symbol-edit API. + +## Design Goals + +The new endpoints should make these workflows cheap and repeatable: + +1. create class and namespace containers in Ghidra without touching the GUI +2. move already-renamed flat functions under explicit class ownership +3. build typed instance structs and typed vtables from verified evidence +4. attach `this`-pointer semantics and method signatures to recovered methods +5. preserve ambiguity when evidence is partial instead of forcing speculative class conversions +6. support dry-run review before any bulk symbol or datatype mutation + +## Non-Goals + +- automatic recovery of class hierarchies from raw heuristics alone +- one-shot `convert whole binary to C++ classes` +- speculative inheritance inference without vtable or field evidence +- silent symbol moves that hide rename collisions or ownership conflicts + +## Existing MCP Behavior To Reuse + +The local fork already has patterns worth reusing: + +- explicit target selectors: `project_dir`, `project_name`, `folder_path`, `program_name` +- dry-run oriented edit-plan behavior +- machine-friendly outputs rather than prose-heavy summaries +- backward-compatible aliases when route names change + +Every new class-lifting endpoint should follow the same conventions. + +## Core Object Model Assumptions + +The class-lifting workflow needs to manipulate four kinds of things explicitly: + +1. namespace/class containers in the symbol tree +2. function ownership and method naming +3. datatypes for instance structs and vtables +4. binding metadata between methods, vtable slots, and instance layouts + +That means symbol-only endpoints are not enough. Datatype endpoints and method-binding endpoints are part of the minimum viable feature set. + +## Proposed Endpoints + +### 1. `create_namespace` + +Create a namespace or class container. + +Parameters: + +- `name`: string +- `parent_path`: string, optional +- `kind`: enum `namespace|class`, default `namespace` +- explicit target selectors, optional + +Response: + +- `status` +- `created`: bool +- `kind` +- `path` +- `symbol_id` or equivalent stable identifier if available +- `collision`: existing path info when create is skipped or merged + +Why it matters: + +- lets the workflow create `Entity`, `SpriteNode`, `EntityVmRuntime`, or similar owners before moving methods + +### 2. `list_namespace_members` + +Return members of a namespace or class container in a machine-friendly form. + +Parameters: + +- `path`: string +- `include_child_namespaces`: bool, default `false` +- `include_functions`: bool, default `true` +- `include_data`: bool, default `true` +- explicit target selectors, optional + +Response: + +- `status` +- `path` +- `members`: array of `{ kind, name, address?, datatype?, child_count? }` + +Why it matters: + +- needed for inventory verification and idempotent batch moves + +### 3. `move_symbol_to_namespace` + +Move a function or data symbol under a namespace/class. + +Parameters: + +- `symbol_address`: string, optional +- `symbol_name`: string, optional +- one of the above required +- `namespace_path`: string +- `new_name`: string, optional +- `conflict_policy`: enum `fail|keep_existing|rename_incoming`, default `fail` +- `dry_run`: bool, default `false` +- explicit target selectors, optional + +Response: + +- `status` +- `moved`: bool +- `old_path` +- `new_path` +- `collision`: optional structured collision detail + +Why it matters: + +- this is the basic operation needed to turn flat functions into methods after evidence is verified + +### 4. `set_function_class` + +High-level helper to move a function into a class and apply method-oriented naming/signature metadata in one call. + +Parameters: + +- `function_address`: string +- `class_path`: string +- `method_name`: string +- `this_param_name`: string, optional, default `this` +- `calling_convention`: string, optional +- `dry_run`: bool, default `false` +- explicit target selectors, optional + +Response: + +- `status` +- `function_address` +- `old_path` +- `new_path` +- `signature_before` +- `signature_after` + +Why it matters: + +- reduces the number of separate write operations for the common `move + rename + set this semantics` workflow + +### 5. `create_or_update_struct` + +Create or update a structure datatype. + +Parameters: + +- `name`: string +- `category_path`: string, optional +- `size`: integer, optional +- `packing`: integer, optional +- `fields`: array of field specs + +Each field spec: + +- `offset`: integer +- `name`: string +- `datatype`: string +- `comment`: string, optional +- `confidence`: enum `high|medium|low`, optional + +- `dry_run`: bool, default `false` +- explicit target selectors, optional + +Response: + +- `status` +- `datatype_path` +- `created_or_updated` +- `size` +- `field_count` +- `conflicts`: array, optional + +Why it matters: + +- class lifting without struct authoring is not enough for readable or recompilable source + +### 6. `create_or_update_vtable` + +Create a vtable datatype as a structure of function pointers. + +Parameters: + +- `name`: string +- `category_path`: string, optional +- `slots`: array of slot specs +- `dry_run`: bool, default `false` +- explicit target selectors, optional + +Each slot spec: + +- `offset`: integer +- `name`: string +- `function_address`: string, optional +- `prototype`: string, optional +- `comment`: string, optional + +Response: + +- `status` +- `datatype_path` +- `slot_count` +- `bound_functions`: array of `{ offset, function_address, name }` + +Why it matters: + +- this is the missing datatype-side half of stable virtual dispatch recovery + +### 7. `set_function_this_type` + +Apply or update `this`-pointer typing on a function. + +Parameters: + +- `function_address`: string +- `this_type`: string +- `this_param_name`: string, optional, default `this` +- `this_storage`: enum `stack|register|farptr`, optional +- `calling_convention`: string, optional +- `dry_run`: bool, default `false` +- explicit target selectors, optional + +Response: + +- `status` +- `function_address` +- `signature_before` +- `signature_after` + +Why it matters: + +- many decompiler improvements only show up after the instance type is attached to the first argument correctly + +### 8. `analyze_vtable` + +Read-side helper that inspects a suspected vtable region and emits slot candidates. + +Parameters: + +- `address`: string +- `slot_count`: integer, optional +- `stop_on_invalid_pointer`: bool, default `true` +- explicit target selectors, optional + +Response: + +- `status` +- `address` +- `slots`: array of `{ offset, target_address, target_name, is_function, current_owner?, comment? }` +- `warnings`: array, optional + +Why it matters: + +- this is the minimum analysis helper needed before class authorship is applied at scale + +### 9. `apply_class_layout` + +Bind a class namespace, instance struct, optional vtable struct, and a set of methods in one dry-runnable transaction. + +Parameters: + +- `class_path`: string +- `instance_struct`: string +- `vtable_struct`: string, optional +- `vtable_address`: string, optional +- `methods`: array of method specs +- `dry_run`: bool, default `false` +- explicit target selectors, optional + +Each method spec: + +- `function_address`: string +- `method_name`: string +- `slot_offset`: integer, optional +- `is_virtual`: bool, default `false` +- `this_type`: string, optional +- `comment`: string, optional + +Response: + +- `status` +- `class_path` +- `applied_methods` +- `applied_structs` +- `warnings` + +Why it matters: + +- supports one-shot promotion of a verified family from notes into Ghidra with explicit review first + +### 10. `export_class_candidate` + +Read-side export helper for documentation and source-generation prep. + +Parameters: + +- `class_path`: string +- `include_struct_fields`: bool, default `true` +- `include_vtable`: bool, default `true` +- `include_method_signatures`: bool, default `true` +- explicit target selectors, optional + +Response: + +- machine-friendly JSON-like object containing class metadata, methods, field layouts, and slot maps + +Why it matters: + +- the local docs and future C++ skeleton emission need a clean export surface, not just screen scraping + +## Field Schemas + +### Struct field schema + +Recommended stable shape: + +```json +{ + "offset": 0, + "name": "vtable", + "datatype": "EntityVTable *", + "comment": "Primary vtable pointer", + "confidence": "high" +} +``` + +### Method schema + +```json +{ + "function_address": "0008:ba00", + "method_name": "Init", + "slot_offset": null, + "is_virtual": false, + "this_type": "EntityDispatchEntry *", + "comment": "Base constructor-style init" +} +``` + +### Vtable slot schema + +```json +{ + "offset": 20, + "name": "OnEventType2", + "function_address": "000b:3ab2", + "prototype": "void (__far *OnEventType2)(SpriteNode *, Event *)" +} +``` + +## Transaction And Safety Rules + +All write-capable class-lifting endpoints should support: + +- `dry_run` +- explicit target selectors +- structured conflict reporting +- idempotent repeat calls where practical +- no silent overwrite of unrelated symbols or datatype fields + +Recommended conflict output shape: + +- `type`: `symbol_collision|datatype_collision|slot_conflict|owner_conflict|signature_conflict` +- `path` or `address` +- `existing` +- `requested` +- `resolution_options` + +## Backward Compatibility And Aliases + +Where practical, add aliases instead of replacing older names. + +Recommended aliases: + +- `create_class` -> `create_namespace(kind=class)` +- `move_function_to_class` -> `set_function_class` +- `set_this_type` -> `set_function_this_type` +- `build_vtable` -> `create_or_update_vtable` + +This follows the local fork’s existing pattern of keeping compatibility wrappers when route names evolve. + +## Suggested Implementation Order + +If implementation resumes later, the smallest useful sequence is: + +1. `create_namespace` +2. `move_symbol_to_namespace` +3. `set_function_this_type` +4. `create_or_update_struct` +5. `analyze_vtable` +6. `create_or_update_vtable` +7. `apply_class_layout` +8. `export_class_candidate` + +That order enables immediate manual class work after only the first three or four endpoints, while leaving the richer transactional workflows for later. + +## First Real Workflow To Target + +The first workflow this API should make easy is the pilot family from the current inventory: + +### `EntityDispatchEntryBase` promotion workflow + +1. create class namespace `Remorse::EntityDispatchEntry` +2. create instance struct `EntityDispatchEntry` +3. move `0008:ba00`, `0008:bca8`, `0008:bd53`, `0008:bf8e`, `0008:c01d`, `0008:dbec`, and constructor variants under that class as methods +4. attach `this` typing +5. analyze or define vtables `0x3b06`, `0x2d10`, `0x3afe`, `0x3ad2`, `0x3aa6` +6. export the class candidate for repo-side documentation and C++ skeleton generation + +If the endpoint surface handles that family cleanly, it is probably sufficient for the rest of the early C++ lifting work. + +## Open Questions To Resolve Later + +- whether Ghidra class namespaces or plain namespaces produce better decompiler output in this 16-bit NE environment +- how best to encode far-pointer aware `this` conventions in method signatures +- whether vtable datatypes should be attached to concrete memory addresses automatically or only on explicit request +- whether confidence annotations should live in datatype comments, decompiler comments, or external export metadata + +## Summary + +The endpoint surface needed here is not large, but it does need to span both symbol ownership and datatype authorship. If later MCP work only adds `move function into class`, it will still leave the hardest part of the C++ lift undone. + +The minimum viable class-lifting feature set is therefore: + +- namespace/class creation +- symbol-to-class moves +- `this` typing +- struct authoring +- vtable analysis/authoring +- one transactional `apply_class_layout` path \ No newline at end of file diff --git a/docs/map_renderer/editor-object-survey.md b/docs/map_renderer/editor-object-survey.md index a052649..8bdf507 100644 --- a/docs/map_renderer/editor-object-survey.md +++ b/docs/map_renderer/editor-object-survey.md @@ -69,9 +69,17 @@ The tooltip now exposes generalized metadata for editor/helper objects instead o - Decode more shape-specific field semantics for the still-unresolved editor objects, especially the remaining non-promoted invisible-wall, camera/helper, music-controller, and secret-door-switch families, and keep folding any new results back into the dedicated USECODE-link note. - Find the No Regret replacement for the Remorse `0x024F` monster-egg workflow instead of assuming the same shape is reused. +## `0x0318` Frame `0`: `CRUMORPH` + +- The older `placeholder cube` label is no longer the best behavioral read for `0x0318`. Both extracted corpora now name class `0x0318` as `CRUMORPH`: Remorse `EUSECODE_extracted/class_event_index.tsv` entry `173` and Regret `REGRET_USECODE_extracted/class_event_index.tsv` entry `174` both expose a live `equip` body at slot `0x0A`. +- The two recovered `equip` bodies differ slightly in helper naming, but they agree on the same high-level lane. Both scan nearby family-`6` actors, compare the pad `QLo` against mutable actor field `0x63`, reject dead actors, transfer control to the first live match, wait until control sticks, and then dispatch `TRIGGER.slot_20` lane `0` or `1` depending on whether that controlled actor is still alive. +- Current best read is therefore `control-transfer morph pad`, not decorative cube and not DTABLE-backed NPC spawner. The object's authored low quality byte is the local control key; `npcNum` does not carry the actor target directly, and the actor-side match is not a stable exported scene field. +- Static scene evidence is strongest in Regret, which is why the viewer promotion was first justified there. The decompressed `.cache` scenes repeatedly show nearby same-`QLo` `0x04B1` helpers close enough to expose a cautious local `CRUMORPH -> CMD_LINK` overlay rule. +- The deeper actor-target side remains intentionally unexported. The same actor-key follow-up that covered `NPC_ONLY` still applies here: the compared actor byte is mutable field `0x63`, and recovered `TRIGGER.slot_29` / `slot_2B` lanes can rewrite it after load. That keeps `CRUMORPH -> actor` arrows out of the viewer for now. +- Practical viewer implication: `0x0318` should be labeled `CRUMORPH`, should expose its `QLo` / `QHi` / `mapNum` / `npcNum` / `nextItem` bytes in tooltip metadata, should open `CRUMORPH::equip` from the USECODE action, and should keep only the already-evidenced nearby same-`QLo` `0x04B1` arrows. + ## Newly Promoted Regret-Only Controllers -- `0x0318` is now promoted as `CRUMORPH`, not a blank placeholder cube. The recovered `equip` body scans nearby NPCs for a shared internal control key derived from the item's `QLo`, temporarily transfers player control to the first live match, and then brackets `TRIGGER.slot_20` with success or failure lanes. - `0x0366` remains `NPC_ONLY`, but the latest decompressed `.cache` sweep tightens its practical viewer behavior: actor-target arrows are still not justified, while cautious local `NPC_ONLY -> 0x04B1` same-`QLo` arrows are now strong enough to expose. - `0x04c6` / `0x04de` are now promoted as `WATCHNS` / `WATCHEW`, not generic editor leftovers. Their recovered `slot_20` bodies scan nearby `0x0510` posts by shared `QLo` and then bracket `TRIGGER.slot_20` around a watcher-specific follow-up lane. - `0x0510` is now better treated as a `SECRET_DOOR_POST` helper target rather than an unresolved standalone controller. The strongest current viewer behavior is a cautious local arrow from `WATCHNS` / `WATCHEW` plus tooltip decoding of its `QLo`/`QHi` bytes. @@ -79,6 +87,31 @@ The tooltip now exposes generalized metadata for editor/helper objects instead o - `0x0451` / `0x05ae` are now closed as `CRAZYEW` / `CRAZYNS`, small Regret-only hit-driven NPC wake-up relays rather than vague contextual map labels. - `0x056d` is now closed as `VIDEOBOX`, a gated controller with a direct `equip` body, even though its higher-level gameplay meaning is still less explicit than the watcher and cryobox lanes. +## Shared Trigger Follow-Up: `0x00A2`, `0x03C1`, And `0x04E7` + +### `0x04E7` Frame `0`: `DEATHBOX` In Both Games + +- The `npc death` icon label now has a clean cross-game closure, not just a Remorse-side guess. Both extracted corpora expose class `0x04E7` as `DEATHBOX`, and both corpora keep the active exported body at slot `0x0A` (`equip` / `func0A`). +- That means the Remorse equivalent is exact rather than approximate: same shape id, same class label, same nearby-`DEATHBOX` scan from `NPCDEATH.slot_20`, and the same practical viewer interpretation as an NPC-death helper/controller keyed by local `QLo`. +- Practical viewer implication: Regret should no longer leave `0x04E7` as an anonymous editor object when the underlying usecode/export evidence already matches Remorse exactly. + +### `0x00A2`: `PANELEW` + +- Both extracted corpora now close `0x00A2` directly as `PANELEW`, the east-west counterpart to `PANELNS`, not as a generic unnamed wall button. +- Recovered body `PANELEW::use` is small but consistent across both games: + - if `frame == 0`, it returns immediately + - otherwise, if the panel's map byte is clear, it dispatches `TRIGGER.slot_20` lane `0` from the panel item itself +- The handler does not need to read a second bespoke target field because the downstream trigger family already uses the panel's local `QLo` as the practical authored link id. +- Practical viewer implication: `0x00A2` should be labeled `PANELEW`, should open `PANELEW::use` from the USECODE action, and should participate in the same cautious nearby same-`QLo` `0x04B1` helper-arrow rule already used for `PANELNS` and other local switch/controller shapes. + +### `0x03C1`: `GENERATR` + +- The old `generator` hunch is directionally right, but the extracted name is now explicit in both games: class `0x03C1` is `GENERATR`. +- The direct active lane is very small and decisive. `GENERATR::gotHit` does not contain a long custom destruction script; it simply excludes the source item and immediately spawns `TRIGGER.slot_20` lane `0` from that same item. +- Current safest read is therefore `destroyable generator/controller` rather than `free-standing scripted puzzle object`: destroying it is useful because it forwards the object's local trigger key into the standard trigger network. +- There is also a second, narrower set-piece lane in Remorse. Recovered `SATARG::use` explicitly scans nearby `shape=0x03C1` items during its countdown/shutdown sequence and drives them through `ITEM.slot_28` beside the related `0x03BF` bank, which fits authored generator-bank or power-node shutdown scenes rather than a different standalone class meaning. +- Practical viewer implication: `0x03C1` should be labeled `GENERATR`, should open `GENERATR::gotHit` from the USECODE action, and should expose the same cautious nearby same-`QLo` `0x04B1` helper arrows as other trigger-source objects because its recovered destruction lane feeds directly into `TRIGGER`. + ## Actor-Key Family Follow-Up - The latest actor-link follow-up did not justify exporting a stable `NPC_ONLY -> actor` or `CRUMORPH -> actor` overlay from static map/cache data alone. diff --git a/docs/map_renderer/trigger-usecode-links.md b/docs/map_renderer/trigger-usecode-links.md index c6727fe..c423f53 100644 --- a/docs/map_renderer/trigger-usecode-links.md +++ b/docs/map_renderer/trigger-usecode-links.md @@ -13,12 +13,14 @@ The implementation uses extracted `class_event_index.tsv` results plus existing | `MONITNS` (`0x0102`) | `MONITNS::use` (`slot 0x01`) | Existing gameplay notes tie shape `258` / `0x0102` to a live monitor/computer-adjacent use handler, making it a strong non-editor first-view script target. | | `MONITEW` (`0x0165`) | `MONITEW::use` (`slot 0x01`) | Disasm crosswalks shape `0x0165` to the east-west monitor variant, which keeps the same live computer-adjacent use handler family. | | `PANELNS` (`0x00A1`) | `PANELNS::use` (`slot 0x01`) | Verified panel-switch wrapper for the same nearby trigger-helper chain. | +| `PANELEW` (`0x00A2`) | `PANELEW::use` (`slot 0x01`) | East-west panel-switch counterpart to `PANELNS`; nonzero frames with clear map state forward the panel's local `QLo` into `TRIGGER.slot_20` lane `0`. | | `CRUMORPH` (`0x0318`) | `CRUMORPH::equip` (`slot 0x0A`) | Recovered control-transfer pad body scans nearby NPCs for a local-`QLo` control key match, temporarily hands control to the first live hit, and then dispatches `TRIGGER.slot_20` lane `0` or `1`. | | `NPCTRIG` (`0x0363`) | `NPCTRIG::equip` (`slot 0x0A`) | Crosswalked shape/class match; the compact slot-`0x0A` body is still the strongest active-event frontier for this trigger family. | | `CRUZTRIG` (`0x0365`) | `CRUZTRIG::gotHit` (`slot 0x06`) | Disasm crosswalks shape `0x0365` to CRUZTRIG, and `gotHit` is the recovered live body for this trigger/helper family. | | `VMAIL` (`0x0367`) | `VMAIL::slot_0a` (`slot 0x0A`) | Disasm crosswalks shape `0x0367` to VMAIL; slot `0x0A` is the active helper body even though its final semantic label is still weaker than the slot number. | | `CARD_NS` (`0x031D`) | `CARD_NS::use` (`slot 0x01`) | Thin wrapper into the downstream `SWITCH` / `TRIGGER` path. Regret also exposes `cast`, but `use` remains the stable first inspection point. | | `SPANEL` (`0x03AA`) | `SPANEL::use` (`slot 0x01`) | Same local `QLo`-keyed switch/controller family as `PANELNS` and `CARD_NS`. | +| `GENERATR` (`0x03C1`) | `GENERATR::gotHit` (`slot 0x06`) | Destroyable generator/controller lane; the recovered body immediately excludes the source item and dispatches `TRIGGER.slot_20` lane `0`, making it the right first inspection point for power-node objects. | | `FASTSKIL` (`0x0120`) | `FASTSKIL::enterFastArea` (`slot 0x0F`) | Difficulty-gated trigger router, including the verified `QLo`, `QLo + 1`, and `QLo + 2` remap lane. | | `SKILLBOX` (`0x04E3`) | `SKILLBOX::equip` (`slot 0x0A`) | Corpus-backed skill-gated controller body; this is the active recovered lane, not `enterFastArea`. | | `CHEST_NS` (`0x054F`) | `CHEST_NS::use` (`slot 0x01`) | The live chest-open handler runs the animation/audio path and the same general FREE-backed content-spawn flow as the east-west chest family. | diff --git a/docs/ne-hole-filling-priorities.md b/docs/ne-hole-filling-priorities.md index 5201b80..441029d 100644 --- a/docs/ne-hole-filling-priorities.md +++ b/docs/ne-hole-filling-priorities.md @@ -61,9 +61,14 @@ Evidence used here: - The `0006:43c3` lane now shows where the owner-row bit-`0x0040` probe is consumed locally: inside a subtype-`0x20c` dispatch-entry object rather than at a generic descriptor-choice site. That improves caller provenance for `0005:295f`, but it still does not prove which owner-loaded class family seeded the later VM data. - The second direct caller family is now closed too. Old `0006:c5f0` lands at live call site `1128:0ff0` inside `Item_ReceiveHit`, where the non-NPC item path calls `Item_GetDamaged` with hitter sentinel `0x4000`, packed damage `(damagetype << 8) | damage_lo`, and a local flag-out byte that records the returned owner-row bit-`0x0040` capability before the local destruction / impact follow-up. - The third direct caller family is now closed too. Old `0007:3584` lands at live call site `1138:1384` inside `SuperSprite_HitAndFinish`, where the non-NPC collision lane probes `Item_GetDamaged` with hitter sentinel `0x4000` and packed damage `(firetype << 8) | damage`; only when that flag-out byte stays clear and the target is not fixed does the lane fall through into the local `Item_ReceiveHit` knockback path. +- The next earlier compiled-side producer is now closed for one real gameplay family. `AreaSearch_CollideMove` at `10e0:123a` queues paired `0x236` storage processes in both its first-collision and linked-list collision lanes: `0x20b` is always created as the local `hit` notifier from moving item to collided item, and the reciprocal `0x20c` process is always created as the `got-hit` notifier from collided item back to the moving item. +- The local queue-helper cluster around that producer is now named in the live NE database too: `10f0:046d` = `storage_process_ref_list_create`, `10f0:0502` = `storage_process_ref_list_append`, and `10f0:06b5` = `storage_process_ref_list_destroy`. Their recovered contract is a counted far-pointer array drained later by `StorageDataProcess_RunAndTerminateProcs`, not a darker allocator or owner-resource helper. +- The producer surface above `AreaSearch_CollideMove` is now wider but still collision-local. Direct callers currently verified in the live session are `Item_LegalMoveToPoint`, `Item_LegalMoveToPointWithCollisionInfo` (`10a0:1841`), `GravityProcess_Run`, `AnimPrimitive_CheckToStartNewAnimation`, `AnimPrimitiveProcess_Run`, `SuperSprite_AdvanceFrame`, and `GravityProcess_FastAreaCleanup` (`1038:11fd`). No non-collision caller currently reaches `StorageDataProcess_Create` or `StorageDataProcess_RunAndTerminateProcs` directly. +- Two more local movement helpers are now named structurally in the live NE database: `10a0:1841` = `Item_LegalMoveToPointWithCollisionInfo`, a legal-move wrapper that preserves blocked/collision outputs around the same `AreaSearch_CollideMove` commit path, and `1138:0ee8` = `SuperSprite_SweepTestAdvance`, the supersprite-side sweep probe that stores the first collision before `SuperSprite_AdvanceFrame` commits the move. +- The surrounding movement and cleanup helper layer is now less anonymous too. `10e0:11c5` = `AreaSearch_SweepShapeBetweenPoints`, `10e0:15b4` = `AreaSearch_SweepItemToPointWithStepUp`, and `10e0:162f` = `AreaSearch_SweepShapeBetweenPointsWithStepUp` now cover the step-aware sweep path beneath the legal-move wrappers, while `10f0:03ff` = `StorageDataProcess_Release` and `10f0:0542` = `storage_process_ref_list_terminate_item_matches` close the local queue-release side. Adjacent seg090 helper `10a0:196f` is now `ItemCache_PushAndPopToDirectionalOffset`. - `0005:2c35` remains outward-dark in the current NE session: instruction search still shows no recovered code or data xrefs, and its proven local role is still only `sign-extended additive word -> slot 0x0a / mask 0x0400 -> generic masked hub`. -- The live `CRUSADER.EXE` integration batch is now extended for this lane. Comment-backed anchors were already present at `1420:0dc5` (`Item_GetUsecodeClassId`), `1420:0e3a` (`Usecode_ItemCallEvent`), `10a0:2718` (`Item_Hit`), `10a0:275f` (`Item_GetDamaged`), `10f0:02d9` (`StorageDataProcess_Create`), and `10f0:0379` (`StorageDataProcess_Run`), with branch comments at `10f0:03c3` and `10f0:03e5` preserving the verified `0x20c` / `0x20b` split; new live comments now also anchor the remaining direct caller sites at `1128:0ff0` and `1138:1384`. -- Result of this pass: all currently recovered direct `0005:295f` caller families are now closed, but the compiled-side selector evidence still bottoms out at subtype-gated dispatch or generic gameplay damage consumers plus owner-row capability bits, not a concrete `NPCTRIG` / `EVENT` class-family choice. The next defensible NE step is therefore an earlier producer that assigns subtype `0x20b/0x20c` into field `+0x3c` or otherwise chooses the owner-loaded class family before these generic damage consumers run. +- The live `CRUSADER.EXE` integration batch is now extended for this lane. Comment-backed anchors were already present at `1420:0dc5` (`Item_GetUsecodeClassId`), `1420:0e3a` (`Usecode_ItemCallEvent`), `10a0:2718` (`Item_Hit`), `10a0:275f` (`Item_GetDamaged`), `10f0:02d9` (`StorageDataProcess_Create`), and `10f0:0379` (`StorageDataProcess_Run`), with branch comments at `10f0:03c3` and `10f0:03e5` preserving the verified `0x20c` / `0x20b` split; this pass adds live comments at `10e0:123a`, `10f0:046d`, `10f0:0502`, and `10f0:06b5` and promotes the queue helpers to stable names. +- Result of this pass: all currently recovered direct `0005:295f` caller families are now closed, and the current `0x236` storage-process producer surface is now mapped far enough to say something negative too: the queue remains collision-local in the current database and is reached through movement, gravity, animation, and supersprite area-search paths rather than through a broader owner-family selector. The next defensible NE step is therefore either an earlier policy/dispatch layer deciding when those movement lanes call `AreaSearch_CollideMove`, or the first real non-collision producer if one exists elsewhere. ## Priority 2: Rendering / Camera / Tile-Visibility / Watch-Controller Lane diff --git a/docs/presentation-callback-broker-layout.md b/docs/presentation-callback-broker-layout.md new file mode 100644 index 0000000..623328f --- /dev/null +++ b/docs/presentation-callback-broker-layout.md @@ -0,0 +1,207 @@ +# Presentation Callback Broker Layout + +## Purpose + +This note isolates the current class-lift-relevant evidence for the callback/broker object rooted at `0x4588`. + +The subsystem name is still intentionally conservative. The goal here is to freeze the object model, lifecycle, and vtable-slot surface that are already well evidenced, so later Ghidra class work does not have to rediscover them. + +Current working family name: + +- `PresentationCallbackBroker` + +That name is still provisional, but it fits the current evidence better than a generic allocator or generic render-object label. + +## Why This Looks Like A Real Object Family + +Current evidence does not support treating `0x4588` as a stray callback function pointer. + +What is already stable: + +- one installed nullable FAR object pointer at `0x4588` +- explicit once-only install and teardown helpers +- live vtable slots `+0x04`, `+0x08`, and `+0x0c` +- state snapshot globals at `0x458c` and `0x4590` +- once guards at `0x4594` and `0x4595` +- a fallback/auxiliary buffer pointer at `0x45a6` + +That is strong enough to treat this as a typed broker/helper object with global lifetime rather than as a raw callback cell. + +## Core Lifecycle + +### Install + +Strong anchors: + +- `000a:4913 runtime_callback_object_init_once` +- `000a:4a1f video_bios_state_snapshot` + +Current verified behavior from the raw notes: + +- checks one-time guard `0x4594` +- snapshots state through `video_bios_state_snapshot` +- stores previous/current state words in `0x458c` and `0x4590` +- installs the incoming FAR object pointer at `0x4588` +- ensures fallback buffer allocation at `0x45a6` + +Current safest role: + +- object installation is tied to video or presentation state capture, not to generic allocation alone + +### Teardown + +Strong anchor: + +- `000a:4a56 runtime_callback_object_teardown_once` + +Current verified behavior: + +- checks once-only teardown guard `0x4595` +- clears `0x4588` when an object is present +- emits vtable `+0x0c` callback only when `0x4590 != 0x458c` +- then calls vtable slot `+0x04` +- follows with cleanup through `FUN_0009_0d30()` + +Current safest role: + +- the broker is not only a notifier; it also owns a release/finalize path and a final conditional dispatch when presentation state changed + +### Finalize sweep phase + +Strong anchor: + +- `0009:b1c3 allocator_phase_finalize_pass` + +Verified behavior: + +- accepts finalize phase `0` or `1` +- forwards that phase twice through broker vtable slot `+0x08` +- then sweeps allocator heads through `allocator_head_finalize_sweep` + +Current safest role: + +- slot `+0x08` looks like a finalize or phase-advance callback surface shared with allocator/presentation cleanup, not a normal per-entity method + +## Vtable Surface + +### Slot `+0x04` + +Evidence: + +- teardown path calls it as the broker release path + +Current working meaning: + +- `release` or `shutdown` + +### Slot `+0x08` + +Evidence: + +- `allocator_phase_finalize_pass` forwards phase `0` or `1` twice through this slot + +Current working meaning: + +- `phase_finalize` or `advance_finalize_phase` + +### Slot `+0x0c` + +Evidence: + +- teardown emits it conditionally when recorded state changed +- `entity_cleanup_resources_and_dispatch` uses it at `000d:9d5e` and `000d:a3b7` +- raw notes also document callers `000a:b9e5` and `000a:ba66` +- one caller uses literal mode-like pair `0x0101` + +Current working meaning: + +- `emit_state_pair` or `dispatch_state_change` + +Current caution: + +- payload semantics are still not closed well enough to call this a specific display-mode or palette-event method with confidence + +## Global State Cluster + +| Address | Current role | +|---------|--------------| +| `0x4588` | nullable FAR broker object pointer | +| `0x458c` | recorded state word A | +| `0x4590` | recorded state word B | +| `0x4594` | install-once guard | +| `0x4595` | teardown-once guard | +| `0x45a6` | fallback or auxiliary FAR buffer | + +Current safest class-lift read: + +- this global cluster behaves like one installed process-wide broker instance plus its remembered state and support buffer + +## Caller-Side Payload Evidence + +The payload side is important even though exact semantics are still open. + +Verified pairs already documented: + +- `entity_cleanup_resources_and_dispatch` call at `000d:9d5e` uses object fields `+0x12d/+0x12f` +- matching call at `000d:a3b7` uses object fields `+0x74f/+0x751` +- one live caller uses literal `0x0101` + +What this supports now: + +- slot `+0x0c` is consuming compact two-word state/payload pairs +- those pairs are presentation-adjacent enough to align with the earlier video-state snapshot evidence + +What it does not yet support: + +- one final semantic label for those two words across every callsite + +## Relationship To Other Families + +### Presentation and startup/display lane + +The broker note belongs next to the startup/display and runtime-state family notes because its callers overlap with that cleanup/palette/presentation lane. + +Strong surrounding anchors: + +- `entity_cleanup_resources_and_dispatch` +- palette/state handoff work in `FUN_000d_938c` +- active dispatch-entry hold token at `g_active_dispatch_entry_farptr[+0x40]` + +Current safest read: + +- the broker is presentation-side infrastructure shared by cleanup/finalize paths, not a child class of `EntityDispatchEntry` + +### Allocator lane + +The allocator interacts with this broker through finalize phases, but current evidence still reads as `allocator client callback` rather than `allocator-owned object`. + +That means: + +- keep allocator classes and this broker separate in future class modeling + +## Class-Lift Guidance + +If promoted in Ghidra later, safest current move is: + +1. create a conservative owner like `PresentationCallbackBroker` +2. model the global state cluster as supporting globals, not as instance fields until constructor ownership is tighter +3. create a provisional vtable with slots `+0x04`, `+0x08`, and `+0x0c` +4. leave slot names conservative and role-based +5. do not promote this family as the first MCP/class-lift pilot + +Why not first: + +- lifecycle is strong, but subsystem semantics are still weaker than `EntityDispatchEntry`, `SpriteNode`, or `EntityVmOwnerResource` + +## Best Next Evidence To Collect Later + +1. close more callers of vtable slot `+0x0c` +2. classify the exact payload meaning of the two-word pairs +3. decide whether `0x45a6` is an owned buffer, fallback object, or adapter scratch lane +4. tighten the constructor/installation provenance in the live NE program, not only the raw notes + +## Bottom Line + +The `0x4588` family is now documented well enough to be treated as a real object candidate. + +The safest current model is: one global presentation-state callback broker with a small live vtable surface, explicit install/teardown, conditional state-pair emission, and allocator-linked finalize phases. \ No newline at end of file diff --git a/docs/raw-0008-000c.md b/docs/raw-0008-000c.md index 8843afc..8e90563 100644 --- a/docs/raw-0008-000c.md +++ b/docs/raw-0008-000c.md @@ -518,6 +518,18 @@ The next gameplay-side wrapper pass now extends well past the three earlier seed - Taken together, the new seg004 and seg006 callers strengthen the existing read of the still-dark wrappers `0005:2c35` (`0x0400:0x000a`) and `0005:2c68` (`0x0800:0x000b`). Those wrappers still have no direct caller evidence, but they now sit inside a larger verified subfamily of `extra-word masked materializers` whose known members feed state selectors, class-linked values, or other gameplay-side payload resolution instead of acting as the real upstream selector into `entity_vm_opcode_sequence_run`. - MCP-native function xrefs now reinforce that stopping point rather than changing it: `entity_vm_context_try_create_masked_for_entity` reports the expected direct callers through `0004:f047`, `0004:f076`, the named `0005` wrapper island, and the two seg006 callsites `0006:0bbc` / `0006:10e7`, while `entity_vm_opcode_sequence_run` plus the dark `0x0400/0x000a` and `0x0800/0x000b` wrappers still surface no direct function-xref callers in the current database. The best next path therefore remains caller-frame recovery or nearby unnamed-function repair, not another generic masked-hub sweep. +#### Latest verified NE pass: collision producer and local storage-process queue + +- The next earlier compiled-side producer for the already-named `StorageDataProcess_Create` / `StorageDataProcess_Run` pair is now closed in the live `CRUSADER.EXE` session. `AreaSearch_CollideMove` at `10e0:123a` allocates a local queue, then emits paired `0x236` processes in both the first-collision lane and the linked-list collision lane. +- The subtype assignment is now explicit at the caller, not just inferred from `StorageDataProcess_Run`: `0x20b` is the local `hit` notifier from the moving item to the collided item, and the reciprocal `0x20c` process is the `got-hit` notifier from the collided item back to the moving item. The first-collision lane uses the precomputed collision magnitude `local_4` as the damage word; the later linked-list lane uses `0`. +- The same pass also closes the local queue-helper trio in seg031. `10f0:046d` is now `storage_process_ref_list_create`, allocating the small queue header plus a counted far-pointer array; `10f0:0502` is now `storage_process_ref_list_append`, storing one `StorageDataProcess` far pointer and recording the assigned slot index in process field `+0x3a`; and `10f0:06b5` is now `storage_process_ref_list_destroy`, freeing the array and optionally the header object. +- The same live pass also widens the producer surface around that queue without breaking the earlier read. Direct callers into `AreaSearch_CollideMove` are now confirmed as movement/collision heavy: `Item_LegalMoveToPoint`, `Item_LegalMoveToPointWithCollisionInfo`, `GravityProcess_Run`, `AnimPrimitive_CheckToStartNewAnimation`, `AnimPrimitiveProcess_Run`, `SuperSprite_AdvanceFrame`, and `GravityProcess_FastAreaCleanup` (`1038:11fd`). +- Two more structural names now anchor that caller set in the live NE database. `10a0:1841` is `Item_LegalMoveToPointWithCollisionInfo`, the legal-move wrapper variant that preserves blocked/collision outputs around the same area-search commit path, and `1138:0ee8` is `SuperSprite_SweepTestAdvance`, the supersprite-side sweep probe that stores the first collision before `SuperSprite_AdvanceFrame` commits movement. +- The same movement lane is now tighter at the helper level too. `10e0:11c5` is now `AreaSearch_SweepShapeBetweenPoints`, the thin wrapper that seeds the search struct and forwards one shape/path sweep into `AreaSearch_SweepTestPt`; `10e0:15b4` is `AreaSearch_SweepItemToPointWithStepUp`, the item-based bridge from current item position and shape into that sweep path; and `10e0:162f` is `AreaSearch_SweepShapeBetweenPointsWithStepUp`, the step-aware wrapper that retries same-z sweeps with vertical offsets and optional `+8` / `+9` step-up probes before returning the resolved point in `srch->pt`. +- The seg031 queue now has its release-side cleanup pair named as well. `10f0:03ff` is `StorageDataProcess_Release`, a release path that terminates queued peer processes referencing the same item before unlinking both MList hooks, and `10f0:0542` is `storage_process_ref_list_terminate_item_matches`, the counted-array helper that clears matching queue slots and forces termination for processes whose `itemno` or `otheritem` matches the requested item. +- One adjacent seg090 helper is now anchored structurally too: `10a0:196f` is `ItemCache_PushAndPopToDirectionalOffset`, which pushes the current item into the cache and repositions the cache pop target to the current point plus one direction-offset lookup from the local `0x0ffe` / `0x100e` tables. +- This moves the VM/caller frontier one step earlier without overclaiming the selector. The closed producer family is still a gameplay collision queue, not an owner-loaded class-family chooser, and no direct non-collision caller currently reaches `StorageDataProcess_Create` or `StorageDataProcess_RunAndTerminateProcs`. The remaining gap is therefore the earlier policy layer that decides when those movement lanes call `AreaSearch_CollideMove`, or the first non-collision producer if one exists elsewhere. + | `000c:f844` | `entity_vm_context_setup` | Calls `entity_vm_stack_init_with_data`, then sets `+0xd6..+0xe3` with position/dimension/state params | | `000c:f600` | `entity_vm_pair_stack_push` | Push (word_a, word_b) onto 31-entry array at `[ptr+0x80]` (count); error if full | | `000c:f63c` | `entity_vm_pair_stack_pop` | Pop and return word from pair stack; error if empty | diff --git a/docs/remorse-class-candidate-inventory.md b/docs/remorse-class-candidate-inventory.md new file mode 100644 index 0000000..e3170a1 --- /dev/null +++ b/docs/remorse-class-candidate-inventory.md @@ -0,0 +1,115 @@ +# Remorse Class Candidate Inventory + +## Purpose + +This note is the working inventory for the first C++-oriented class lift in Remorse. + +It is intentionally narrower than a full source plan. The goal here is to identify the object families that already have enough verified evidence to justify explicit class modeling in Ghidra later, even before the MCP layer grows class-authoring endpoints. + +Until MCP can create namespaces, move methods, and build typed vtable/struct layouts directly, this note should act as the canonical queue for manual class-upgrade work. + +## Selection Rules + +A family belongs here only if at least some of the following are already true in the current notes: + +- a constructor-style allocator/init path exists +- a destructor, teardown, or release path exists +- one or more stable vtable roots or slot dispatches are known +- instance fields have repeatable meanings across multiple methods +- the family has enough caller context to separate instance methods from generic helpers + +Families that are only `callback-shaped` or `object-like` but still lack a safe subsystem label can stay here at lower confidence. They are still useful because they tell us where later class tooling must preserve ambiguity. + +## Confidence Scale + +- High: enough evidence to start class namespaces, instance structs, and method ownership now +- Medium: class-like enough to inventory and maybe type, but higher-level naming or ownership is still partially open +- Low: strong object mechanics, but subsystem naming or field ownership is still too ambiguous for broad lifting + +## Inventory + +| Candidate family | Confidence | Current best class-level read | Core evidence | Ctor / create evidence | Dtor / release evidence | Vtable evidence | Size / layout evidence | Immediate lift value | +|---|---|---|---|---|---|---|---|---| +| `EntityDispatchEntryBase` | High | Base dispatch-entry object used by scheduler/event and transition/runtime-state families | `entity_dispatch_entry_init`, repeated field writes, list ownership, flag/state helpers | `0008:ba00 entity_dispatch_entry_init` alloc/init path for `0x32` bytes; variant allocators in `0004:ea00`, `0008:cefb`, `0008:d214` | `0008:dbec entity_word_list_destroy`; `000d:8078 entity_dispatch_entry_release_runtime_state` for runtime-state flavor | base/root vtables `0x3b06`, `0x2d10`, `0x3afe`; derived vtables `0x3ad2`, `0x3aa6`; slot dispatch at `+0x14`, `+0x28` | stable fields at `+0x02`, `+0x04`, `+0x06`, `+0x08`, `+0x0a..+0x18`, `+0x32..+0x3e`, plus runtime-state tail | Strongest pilot family for the first full class-model pass | +| `EntityDispatchEntryRuntimeState` | High | Runtime-state / palette-backed derived dispatch entry with owned buffers and hold-state propagation | runtime-state init/release pair, palette-entry emission families, owner-byte propagation through `g_active_dispatch_entry_farptr` | `000d:7e00 entity_dispatch_entry_init_runtime_state`; several `dispatch_entry_create_*_palette_state*` helpers build this family | `000d:8078 entity_dispatch_entry_release_runtime_state` frees owned buffers and updates hold-state propagation | inherits dispatch-entry behavior; runtime-state users dispatch through vtable slot `+0x08` after waits | owned fields `+0x41/+0x42/+0x44`, paired buffers at `+0x46/+0x48` and `+0x4a/+0x4c` | Excellent second step after the base dispatch-entry type | +| `SpriteNode` | High | Tree-based UI/render node with child links, dirty-state propagation, event dispatch, and destructor ownership | recursive tree helpers, event switch dispatch, dirty/update family, destructor | constructor not yet explicitly named in current notes, but object-init helper and repeated sprite-node ownership patterns are strong | `000b:326e sprite_node_destroy` releases child nodes and frees self | event dispatch through vtable slots `+0x14/+0x18/+0x20/+0x24`; many default vtable stubs identified in seg091 | repeated fields at `+0x19/+0x1b`, `+0x21`, `+0x23`, `+0x29`; global focus pointer interaction at `0x4fd0:0x4fd2` | Strong candidate because virtual surface is already visible and bounded | +| `Entity` | High | Base gameplay entity / actor-like object with multiple derived vtables for generic, shot, corpse, and debris behaviors | stable NE entity layout, repeated lifecycle helpers, projectile/debris subfamilies | `0007:3f2f entity_spawn`; `0007:435e shot_entity_alloc`; `0007:7490 debris_spawn`; `0007:2c92 dialog_spawn` is adjacent but likely separate family | `0007:40d4 entity_remove`; `0007:5092 entity_deactivate`; `0007:44a9 shot_entity_free` for projectile flavor | vtables `0x29aa`, `0x297e`, `0x2a1a`, `0x2a33`, `0x2a57`, plus registry vtable `0x2969`; multiple virtual-slot helper wrappers in raw notes | strong instance layout from `+0x00` through `+0xbd` in `ne-segment1.md` | Central long-term class family, but should probably be split into base + derived subfamilies | +| `DialogMenuObject` | Medium | Small UI/dialog object family sharing one vtable and event-notify wrappers | `dialog_spawn`, menu/cursor event notify wrappers, UI update callback | `0007:2c92 dialog_spawn` allocates object and stamps vtable `0x28b5` | no standalone destructor named yet in current notes | vtable `0x28b5`; `cursor_event_notify_*`, `menu_event_notify_*`, and `ui_update_callback` all behave like method wrappers | enough to establish family ownership, but layout is still thin | Good compact pilot if a smaller UI-oriented family is preferred | +| `WatchEntityController` | Medium | Global controller/watch/camera object with explicit virtual dispatch and startup/display involvement | global object at `0x2bd8`, dispatch wrapper, create-global path, startup/display callsites | `0007:ba00 watch_entity_controller_create_global` delegates to `0007:ba45 watch_entity_controller_create`, stamps type `0x2c2b`, stores global object | no direct destructor identified in current notes | repeated dispatch through vtable slots `+0x24`, `+0x2c`, and `+0x30` | global-object ownership clearer than field layout; seed row at `0x2be4` into callback table | Worth inventorying now because it will benefit immediately from namespace/method grouping | +| `EntityVmRuntime` | High | Main VM runtime object that owns owner-resource helper, cached slot/value state, and category-base setup | creation/load path is structurally stable and repeatedly cross-checked against extracted usecode evidence | `000d:44df entity_vm_runtime_init_from_path_if_configured`; `000d:4c99 entity_vm_runtime_create` | destroy path not fully named in the snippets here, but owner-resource destroy is known and runtime state/save-load consumers are well constrained | not a classic gameplay vtable family in the current notes, but method-style ownership and object fields are stable | object size and field zones strongly implied by `+0x10c/+0x10e`, `+0x117/+0x119`, `+0x1315/+0x1317` and related runtime state | Major lift target because VM readability is a blocker for recompilable source | +| `EntityVmOwnerResource` | High | File-backed helper owned by VM runtime that indexes source tables and materializes owner rows | helper object shape and per-entry loader contract are already tight | `000d:7000 entity_vm_runtime_owner_resource_create` allocates helper/object tables | paired destroy helper `000d:70fd entity_vm_runtime_owner_resource_destroy` is documented in related notes | helper method-table uses slots `+0x04` size-query and `+0x0c` materialization callback | helper-owned count `+0x14`, far-pointer table `+0x10`, paired word table `+0x18`, owner rows stride `0x0d` | One of the cleanest non-gameplay object families for typed struct work | +| `EntityVmContext` | Medium | Per-slot/per-entity VM context object built from runtime and owner-resource data | create/setup/load helpers already have clear ownership, but broader dispatch semantics are still active work | `000d:46ec entity_vm_context_create_from_slot_index` and related masked-create wrappers | no single destroy method is highlighted in the current note set used here | context-side dispatch and busy-state updates through virtual or callback-like method surface at least on the context object | stable fields include `+0x32/+0x34`, `+0xd6/+0xd8`, `+0x102`, `+0x10c/+0x10e`, `+0x11b/+0x11d`, `+0x123` | Important for VM readability, but should follow runtime and owner-resource typing | +| `CacheBackendObject` | Medium | Small backend/cache loader object with DOS file-handle state and method table | constructor and callback roles are already explicit | `0009:5600 cache_backend_object_init` allocates `0x20` bytes and seeds method-table state | no explicit destructor named in current note slice | backend callback roles at `+0x34` and `+0x0c` are verified in cache lookup/load path | concrete `0x20`-byte size; fields at `+0x08`, `+0x0c`, `+0x10`, `+0x14`, `+0x16`, `+0x18`, `+0x1c` | Good contained family for early datatype work | +| `PresentationCallbackBroker` | Low | Video/presentation-state callback broker rooted at `0x4588` | init/teardown/callback slot evidence is real, but subsystem naming remains intentionally conservative | `runtime_callback_object_init_once` family is documented, but not all constructor details are fully promoted here | `runtime_callback_object_teardown_once` and finalize path are explicit | vtable slots `+0x04`, `+0x08`, `+0x0c` all have live evidence | global state at `0x4588/0x458c/0x4590/0x4594/0x4595/0x45a6`; payload fields from caller objects at `+0x12d/+0x12f`, `+0x74f/+0x751` | Useful as a typed broker object later, but not a good first namespace/class pilot | +| `UsecodeDebuggerBreakState` | Medium | Dormant debugger-state object retained in retail binary | clear constructor and method expectations, but retail instantiation path is missing | `1408:0000 usecode_debugger_break_state_create` allocates and initializes the object | no direct destroy path highlighted in current notes | breakpoint gate callbacks through object vtable during `1408:0053` flow | internal tables and state exist, but field map is not yet summarized into one layout note | Good archival class candidate even if not a current gameplay priority | + +## Recommended Modeling Order + +If the goal is to make later class-authoring work fast and low-risk, the best order is: + +1. `EntityDispatchEntryBase` +2. `EntityDispatchEntryRuntimeState` +3. `SpriteNode` +4. `EntityVmOwnerResource` +5. `CacheBackendObject` +6. `WatchEntityController` +7. `Entity` +8. `EntityVmRuntime` +9. `EntityVmContext` +10. `PresentationCallbackBroker` + +This order prioritizes bounded families with visible constructors, derived variants, or explicit method tables before the larger gameplay and VM surfaces. + +## First Pilot Candidates + +### Best pilot: `EntityDispatchEntryBase` + +Why it is the safest first full class lift: + +- constructor variants are already named +- derived vtable variants are known +- multiple field groups have stable semantics +- destroy/release behavior is present +- it directly exercises the exact MCP features later needed: namespace creation, struct typing, method ownership, and vtable slot labeling + +### Smallest bounded pilot: `CacheBackendObject` + +Why it is attractive: + +- explicit `0x20` size +- explicit create path +- explicit method-table callback roles +- smaller ambiguity surface than gameplay entities or the VM runtime + +### Best UI-oriented pilot: `SpriteNode` + +Why it is valuable: + +- clear virtual event surface +- tree ownership and destructor logic already exist +- likely to benefit quickly from readable class/derived-method naming + +## Per-Family Documentation Tasks + +For each inventory entry, later class-upgrade work should produce the same minimum artifacts: + +1. candidate namespace/class name +2. constructor and destructor list +3. instance-size estimate or known size +4. vtable root(s) and known slots +5. field map grouped by confidence +6. caller ownership notes +7. explicit `keep as free function` list for helpers that should not become methods + +## Immediate Follow-Up Notes Worth Writing Later + +- dedicated `EntityDispatchEntry` class-layout note with base/derived split +- dedicated `SpriteNode` virtual-slot table note +- dedicated `EntityVmRuntime` / `EntityVmOwnerResource` layout note +- dedicated `Entity` family split note covering base entity, projectile, debris, and corpse variants + +## Current Rule Until MCP Catches Up + +Do not rely on automatic class listings from the live MCP tools as the class-recovery source of truth. The current output is still noisy and does not reflect the actual game object families well enough for disciplined C++ lifting. + +Use this inventory plus the linked evidence notes as the authoritative queue for future class-authoring work. \ No newline at end of file diff --git a/docs/remorse-class-lift-index.md b/docs/remorse-class-lift-index.md new file mode 100644 index 0000000..4a80aba --- /dev/null +++ b/docs/remorse-class-lift-index.md @@ -0,0 +1,94 @@ +# Remorse Class-Lift Work Index + +## Purpose + +This note is the easy-to-find landing page for the current Remorse C++ and class-lifting preparation work. + +Use it as the starting point when the project returns to: + +- class and namespace authoring inside Ghidra +- vtable and instance-layout promotion +- hand-maintained C++ skeleton emission +- ABI-safe source reconstruction planning + +This index does not replace the detailed notes. It groups them into one work order so later implementation can resume quickly. + +## Read This First + +1. [docs/remorse-cpp-decompilation-plan.md](docs/remorse-cpp-decompilation-plan.md) +2. [docs/remorse-class-candidate-inventory.md](docs/remorse-class-candidate-inventory.md) +3. [docs/remorse-rebuild-abi-notes.md](docs/remorse-rebuild-abi-notes.md) +4. [docs/ghidra-mcp-class-lifting-endpoint-spec.md](docs/ghidra-mcp-class-lifting-endpoint-spec.md) + +That set gives the high-level target, the current candidate families, the rebuild constraints, and the future MCP authoring surface. + +## Current Note Groups + +### 1. Overall Direction + +- [docs/remorse-cpp-decompilation-plan.md](docs/remorse-cpp-decompilation-plan.md): staged route from decompiler-style C toward evidence-backed C++. +- [docs/remorse-rebuild-abi-notes.md](docs/remorse-rebuild-abi-notes.md): Track A versus Track B guardrails, segmented-pointer concerns, packing, and calling-convention constraints. +- [docs/remorse-toolchain-fingerprint-evidence.md](docs/remorse-toolchain-fingerprint-evidence.md): focused evidence note for the bound NE/Phar Lap/High-C-related toolchain story that underlies the ABI constraints. +- [docs/remorse-cpp-compatibility-header-draft.md](docs/remorse-cpp-compatibility-header-draft.md): first draft of the compatibility/support layer that future C++ skeletons should target. + +### 2. Candidate Inventory And Tooling Surface + +- [docs/remorse-class-candidate-inventory.md](docs/remorse-class-candidate-inventory.md): strongest current class candidates and modeling order. +- [docs/ghidra-mcp-class-lifting-endpoint-spec.md](docs/ghidra-mcp-class-lifting-endpoint-spec.md): missing class/vtable/datatype authoring operations for the local MCP fork. + +### 3. Family-Specific Layout Notes + +- [docs/entity-dispatch-entry-class-layout.md](docs/entity-dispatch-entry-class-layout.md): current `EntityDispatchEntry` base-versus-derived model, release surface, and subtype overlays. +- [docs/sprite-node-class-layout.md](docs/sprite-node-class-layout.md): `SpriteNode` destructor/event surface and candidate virtual-slot map. +- [docs/entity-class-family-split.md](docs/entity-class-family-split.md): conservative split of the large `Entity` lane into base, projectile, debris, corpse/actor, and adjacent non-entity families. +- [docs/entity-vm-runtime-owner-resource-layout.md](docs/entity-vm-runtime-owner-resource-layout.md): current runtime/helper/context ownership model for the VM lane. +- [docs/presentation-callback-broker-layout.md](docs/presentation-callback-broker-layout.md): current object/lifecycle/vtable evidence for the `0x4588` presentation-state callback broker family. + +### 4. Execution Checklists + +- [docs/remorse-first-class-authoring-checklist.md](docs/remorse-first-class-authoring-checklist.md): concrete first-batch checklist for the initial Ghidra/MCP class-authoring pass, with pilot-family guidance and source-emission gates. + +## Recommended Work Order + +### Stage 1: Keep The Evidence Model Honest + +1. Re-read the plan, ABI note, and candidate inventory. +2. Pick one family with bounded ambiguity. +3. Confirm ctor, dtor, vtable root, and stable field groups before any class ownership changes in Ghidra. + +Best current pilot families: + +1. `EntityDispatchEntry` +2. `SpriteNode` +3. `EntityVmOwnerResource` + +`Entity` remains a top-priority family, but it should be split deliberately rather than promoted as one giant class too early. + +### Stage 2: Ghidra Authoring Pass + +1. Create class or namespace owners. +2. Move only strongly owned methods first. +3. Create provisional instance structs and vtable structs. +4. Preserve slot order and unresolved fields instead of trying to beautify them. + +The future MCP endpoint sequence should follow the spec note rather than ad hoc scripting. + +### Stage 3: First C++ Skeleton Slice + +1. Emit one header/source pair for the pilot family. +2. Build it against the compatibility layer rather than raw host C++ alone. +3. Keep unresolved offsets as named placeholders instead of collapsing them into speculative semantics. +4. Record which parts are Track A safe versus Track B convenience-only. + +## Immediate Follow-Ups Still Open + +1. Convert the first family note into a hand-maintained C++ skeleton once the compatibility header is accepted. +2. Implement the MCP class/vtable authoring endpoints only after the workflow and note set above are stable enough to drive them. +3. Add one more dedicated note for the callback/object lane around `0x4588` only if later caller evidence supports a stronger subsystem name than `PresentationCallbackBroker`. +4. Turn the first-class-authoring checklist into a completed execution log once the first real MCP batch lands. + +## Bottom Line + +The current prep work is now large enough that it should be treated as one coordinated lane rather than scattered notes. + +Use this file as the resume point for future class-lift and C++ reconstruction work. \ No newline at end of file diff --git a/docs/remorse-cpp-compatibility-header-draft.md b/docs/remorse-cpp-compatibility-header-draft.md new file mode 100644 index 0000000..78db210 --- /dev/null +++ b/docs/remorse-cpp-compatibility-header-draft.md @@ -0,0 +1,159 @@ +# Remorse C++ Compatibility Header Draft + +## Purpose + +This note defines the minimum compatibility/support layer that future hand-maintained Remorse C++ skeletons should target. + +It is not a final header and it is not a compiler lock-in decision. It is a draft contract so early class-emission work does not silently drift into host-only modern C++ assumptions. + +This note should stay paired with [docs/remorse-rebuild-abi-notes.md](docs/remorse-rebuild-abi-notes.md). + +## Why This Layer Is Needed + +Current rebuild notes already make three constraints clear: + +- Track A original-style executable reconstruction remains in scope +- the executable is shaped by segmented pointers and far-call conventions +- host compilation success is not enough if layout, slot order, or call shape drift + +That means the first C++ skeletons should compile against a compatibility layer that makes these constraints explicit even before the exact compiler/toolchain decision is closed. + +## Minimum Header Categories + +### 1. Exact-width integer aliases + +Needed because: + +- field maps are being recovered by offset +- slot/value and save/load structures use exact-width arithmetic +- sign extension versus zero extension matters repeatedly in the current notes + +Current draft surface: + +```cpp +using u8 = std::uint8_t; +using s8 = std::int8_t; +using u16 = std::uint16_t; +using s16 = std::int16_t; +using u32 = std::uint32_t; +using s32 = std::int32_t; +``` + +### 2. Packing controls + +Needed because: + +- object and save-state layouts are currently tracked by exact offsets +- future vtable-bearing structs must not silently pick host-default padding + +Current draft surface: + +```cpp +#define CR_PACK_PUSH_1 +#define CR_PACK_POP +#define CR_PACKED +``` + +The final spelling can change per compiler. The important part is that early source slices already depend on named packing controls rather than ad hoc pragmas in every file. + +### 3. Calling-convention markers + +Needed because: + +- later method and helper promotion will need to distinguish ordinary helpers from call-shape-sensitive surfaces +- Track A may need explicit far/near or compiler-family-specific conventions + +Current draft surface: + +```cpp +#define CR_CDECL +#define CR_STDCALL +#define CR_THISCALL +#define CR_FARCALL +``` + +`CR_FARCALL` is intentionally placeholder-level today. The current value is in making far-call-sensitive surfaces visible in source, not in pretending the final compiler spelling is already known. + +### 4. Segmented pointer helper types + +Needed because: + +- several notes still preserve segment:offset provenance directly +- owner-loaded VM source pairs and some callback/resource lanes should not be flattened too early + +Current draft surface: + +```cpp +struct FarPtr16 { + u16 offset; + u16 segment; +}; + +template +struct FarPtr { + u16 offset; + u16 segment; +}; +``` + +Current rule: + +- use these first as evidence-preserving placeholders, not as proof that final emitted code must literally use this exact host representation everywhere. + +### 5. Vtable and slot-order helpers + +Needed because: + +- current family notes still treat slot order as evidence, not cosmetics +- later MCP or hand-authored promotion should preserve raw order even when names are provisional + +Current draft surface: + +```cpp +#define CR_VSLOT(index) +``` + +This can stay documentation-only at first if needed. The point is to keep raw slot numbering visible in provisional class headers. + +## First Skeleton Rules + +When the first class family is emitted to source, the compatibility header should enforce these rules: + +1. every field uses exact-width aliases +2. every packed layout is explicit +3. every unresolved far/segment-sensitive pointer uses a named placeholder type +4. every provisional virtual surface keeps raw slot order visible +5. Track A-only assumptions are marked instead of being silently baked into Track B-style cleanup + +## Families Most Likely To Need This Immediately + +1. `EntityDispatchEntry` +2. `SpriteNode` +3. `EntityVmRuntime` +4. `EntityVmOwnerResource` +5. `EntityVmContext` + +`Entity` will also need it, but its family split is large enough that it should probably not be the first source-emission pilot. + +## What This Draft Deliberately Avoids + +- picking one final compiler family now +- pretending near/far semantics are already solved +- turning unresolved imported-runtime calls into polished modern interfaces +- flattening segment:offset provenance into generic host pointers too early + +## Proposed Next Step + +Once the first pilot family is chosen, convert this note into a real minimal header with: + +- exact-width aliases +- packing markers +- calling-convention placeholders +- one segmented-pointer placeholder type +- one comment block explaining Track A versus Track B expectations + +## Bottom Line + +The compatibility header should exist before the first C++ skeleton is emitted, not after. + +That keeps early source work honest and preserves the option of an original-style rebuild instead of quietly drifting into a pure host-port codebase. \ No newline at end of file diff --git a/docs/remorse-cpp-decompilation-plan.md b/docs/remorse-cpp-decompilation-plan.md new file mode 100644 index 0000000..a8fda8e --- /dev/null +++ b/docs/remorse-cpp-decompilation-plan.md @@ -0,0 +1,256 @@ +# Remorse C++ Decompilation Plan + +## Goal + +Turn the current evidence-backed Remorse decompilation into understandable, maintainable C++ source that can eventually be rebuilt into a working executable. + +The important constraint is that this should be treated as a staged lift, not a direct dump of Ghidra pseudocode into a compiler. The shortest path to a recompilable result is to recover the original object model deliberately: class ownership, instance layouts, vtables, calling conventions, segmented-pointer rules, resource formats, and subsystem boundaries. + +## Short Answer: Can Ghidra Be Made More Class-Aware? + +Yes, but only partially and mostly through explicit modeling. + +Ghidra can already represent a lot of what we need: + +- class and namespace symbols in the Symbol Tree +- structs and unions in the Data Type Manager +- vtable data and typed function pointers +- method ownership through namespaces/classes +- `this`-pointer style signatures when the calling convention and object layout are known + +What it does not do well here is infer all of that automatically from a 16-bit DOS binary with mixed C/C++ patterns, custom memory conventions, and incomplete original type information. For this project, class recovery has to be evidence-driven. + +## Why The Shift Is Justified Now + +The current notes already contain repeated object-oriented evidence, not just loose procedural code: + +- constructor-style helpers that allocate, stamp a vtable, and zero instance state +- destructor or teardown paths that restore a base vtable and free owned buffers +- stable indirect dispatch through known vtable slots +- controller, entity, sprite-node, VM-context, and resource-helper families with repeatable instance fields +- several class-like clusters that already have better behavioral names than generic `FUN_...` placeholders + +That is enough to start building a real C++ object model rather than treating the entire program as flat C with random function pointers. + +Useful evidence anchors already in the repo include: + +- `docs/ne-segment1.md` for entity, projectile, dialog, and sprite-adjacent object lanes +- `docs/raw-0008-000c.md` for constructor families, vtable-backed dispatch entries, VM/runtime helpers, and stateful controller objects +- `docs/raw-000a-000d.md` for loader/resource families, callback brokers, and teardown-heavy object lanes +- `docs/raw-porting-progress.md` for callback-object evidence and cross-segment vtable dispatch patterns +- `docs/far-call-targets.md` for high-frequency ctor/dtor/vtable-slot helpers + +## End State + +The real target should be defined more tightly than `nice C++`: + +1. major gameplay, rendering, UI, VM, and resource subsystems are expressed as named classes with understandable responsibilities +2. instance layouts and ownership rules are explicit enough that decompiled code stops depending on anonymous offset math for routine work +3. virtual dispatch is expressed through named methods or typed vtable tables rather than raw slot offsets +4. the source can be rebuilt with a documented toolchain into a working executable or an equivalent working runtime target +5. the rebuilt result is validated by behavior, not by cosmetic similarity to decompiler output + +## Working Assumption About The Rebuild Target + +There are two plausible endgames, and the plan should keep them separate from the start: + +### Track A: Original-style executable rebuild + +Rebuild a DOS executable that preserves the segmented-memory model, calling conventions, packed layouts, and resource/file expectations closely enough to run the original game data. + +This is the harder but most direct historical target. It likely depends on recovering or emulating: + +- the original or closest-possible compiler model +- near/far pointer conventions +- packed struct layout and enum sizes +- startup/runtime integration with the Phar Lap environment or an equivalent replacement layer + +### Track B: Behaviorally equivalent source port + +Rebuild the game logic in modern C++ while preserving data formats and behavior, but not necessarily the original binary ABI. + +This is often the faster path to a working recompiled game, but it is a different goal. If the project wants a true executable reconstruction rather than an engine rewrite, Track A has to remain the primary constraint. + +For now, the safest planning stance is: recover source in a way that keeps both tracks open for as long as possible. + +## Recommended Strategy + +### Phase 0: Treat Ghidra As The Truth Database + +Use Ghidra as the canonical place where recovered class ownership, vtable slots, field layouts, and method names live. + +That means pushing beyond flat rename work into: + +- class namespaces for object families +- typed instance structs +- typed vtable structs where the slots are stable enough +- method names that distinguish static helpers from instance methods +- explicit comments recording why a family is believed to be one class and not just one subsystem + +### Phase 1: Recover The Object Model Before Chasing Pretty Output + +Prioritize families that already have strong OO evidence. + +Best early targets: + +1. entity families in `seg001` and the raw/live `0007` lanes +2. dispatch-entry / controller objects in `0008` and `000c` +3. sprite-node and UI/menu object families +4. VM runtime, context, owner-resource, and loader helpers +5. callback/resource broker objects around `0x4588` + +For each candidate class family, the minimum closure should be: + +- candidate class name +- constructor and destructor candidates +- instance size estimate +- confirmed or suspected vtable base +- known slot-to-method map +- field map with confidence levels +- inbound callers that prove object lifetime or ownership + +### Phase 2: Separate Methods From Free Functions + +Not every helper touching an object should become a class method. + +The conversion rule should be conservative: + +- make it a method when the object pointer is clearly the owner, the function acts on instance state, and the function participates in the class lifecycle or virtual surface +- keep it free or subsystem-local when it behaves like a pure helper, allocator utility, serializer, or cross-object coordinator + +This matters because over-classing weak evidence will make the source look cleaner while actually reducing correctness. + +### Phase 3: Build Stable Type Layers + +Before broad C++ emission, define a small number of disciplined type layers: + +- ABI layer: exact-width integers, near/far pointer wrappers, packed structs, fixed calling-convention macros +- runtime layer: allocators, file/resource handles, callback tables, event records, dispatch entries +- gameplay layer: entities, actors, projectiles, triggers, controller objects, UI nodes +- VM layer: runtime/context/owner-resource classes, opcode streams, slot/value helpers + +The source should compile against these types first, even if some methods still contain low-level or ugly code. + +### Phase 4: Land Recompilable C++ In Vertical Slices + +Do not wait for the whole game to be class-clean before testing compilation. + +Instead, move in subsystem slices: + +1. one object family +2. its structs and vtable +3. its constructors/destructors +4. a handful of live methods +5. a compile test for that slice + +This is the only realistic way to find layout or calling-convention mistakes early. + +### Phase 5: Add Runtime Validation Harnesses + +A source-level recompile effort will fail if verification is only manual. + +Needed validation layers: + +- map/resource load smoke tests +- deterministic startup path checks +- function-level trace comparisons for selected hot methods +- data-layout assertions on recovered structs +- script/VM behavior checks where extracted USECODE already gives a second evidence source + +### Phase 6: Choose The First Real Rebuild Milestone + +The first meaningful source milestone should not be `whole game builds`. + +A better first milestone is one of these: + +1. compile a library that matches one major subsystem ABI and can run against fixture data +2. rebuild the startup/resource path far enough to load into a title/menu state +3. rebuild one contained gameplay loop such as entity allocation/update/teardown with equivalent traces + +## Ghidra/MCP Gaps That Matter For This Plan + +The local MCP fork already gives enough read/query power to continue class recovery, but it is still missing key authoring operations for a serious C++ lift: + +- create class or namespace symbols through MCP +- move existing functions under class ownership cleanly +- create or update struct and vtable datatypes through MCP +- set `this`-pointer types and method signatures systematically +- analyze a candidate vtable and bind slots to named methods in one operation + +Those gaps have been added to `ghidra_mcp_wishlist.md` in this batch. + +## First Concrete Work Batches + +The most defensible first batches are small and structural. + +### Batch 1: Class Inventory Pass + +Build a repo-side inventory of the strongest current class candidates: + +- class family name +- addresses for ctor/dtor/vtable roots +- known methods +- instance-size estimate +- notes/doc references + +### Batch 2: One Fully Modeled Family + +Pick one family with low ambiguity and carry it through end to end inside Ghidra and the notes: + +- class namespace +- method ownership +- instance struct +- vtable struct +- method-slot table +- short rationale note + +Good initial candidates are the `entity_dispatch_entry_*` family, the sprite-node family, or one compact controller object family. + +### Batch 3: C++ Skeleton Output + +Emit one hand-maintained C++ header/source pair for that family with: + +- exact-width field placeholders +- named methods +- comments for unresolved fields or slot semantics +- enough type discipline that the code could later be compiled under a chosen toolchain + +### Batch 4: Toolchain Recon + +Establish the most credible compile target and constraints early: + +- likely original compiler family or nearest substitute +- calling convention spelling +- memory-model requirements +- struct packing behavior +- import/library expectations + +Without this, the source can drift into modernized C++ that reads well but cannot realistically rebuild the game. + +## What To Avoid + +- Do not mass-convert procedural helpers into methods just to make the output look object-oriented. +- Do not let Ghidra pseudocode naming outrun field-layout evidence. +- Do not assume modern C++ ABI rules match the original compiler. +- Do not mix `behaviorally equivalent port` goals with `original-style executable rebuild` claims in the same milestone. +- Do not wait for perfect global understanding before compiling anything. + +## Immediate Next Steps + +1. add the missing class/namespace and vtable-authoring MCP endpoints to the local fork when ready +2. make a `class candidate inventory` note from the strongest existing families in the current docs +3. choose one family and model it all the way through as a pilot C++ class +4. decide whether the primary rebuild constraint is original-style DOS/NE compatibility or a behaviorally equivalent C++ port +5. define the first compile/test harness before broad source emission starts + +## Success Criteria For This Plan + +This plan is working if, after a few batches, the project has all of the following: + +- at least one real class family fully modeled in Ghidra and mirrored in source +- repeatable rules for when a function becomes a method +- repeatable rules for vtable and field-layout evidence +- a documented compile target with ABI constraints +- a narrow but real compilation/validation loop + +If those do not exist, the project is still doing useful reverse engineering, but it has not yet truly shifted into a recompilable C++ decompilation lane. \ No newline at end of file diff --git a/docs/remorse-first-class-authoring-checklist.md b/docs/remorse-first-class-authoring-checklist.md new file mode 100644 index 0000000..194ba4a --- /dev/null +++ b/docs/remorse-first-class-authoring-checklist.md @@ -0,0 +1,139 @@ +# Remorse First Class-Authoring Checklist + +## Purpose + +This note turns the current class-lift preparation into a concrete execution checklist for the first real Ghidra/MCP authoring batch. + +It is intentionally operational. The goal is to remove startup cost once the necessary MCP class/vtable/datatype tools exist. + +## Recommended First Pilot Order + +Current best pilot ranking: + +1. `EntityDispatchEntry` +2. `SpriteNode` +3. `EntityVmOwnerResource` + +Why this order: + +- `EntityDispatchEntry` has strong ctor/release and field-group evidence +- `SpriteNode` has a compact, visible virtual surface +- `EntityVmOwnerResource` is structurally bounded but still slightly more loader-schema-sensitive + +## Batch 0: Preconditions + +Before touching class ownership in Ghidra: + +1. re-read [docs/remorse-class-lift-index.md](docs/remorse-class-lift-index.md) +2. re-read the target family note +3. confirm the ABI note and compatibility-header draft are still aligned with the intended source track +4. confirm the MCP tool surface actually supports namespace/class creation, datatype authoring, and method ownership changes + +Do not start with authoring if any of those are still missing. + +## Batch 1: Authoring Rules + +These rules should hold for the first pilot family regardless of which family is chosen: + +1. move only strongly owned methods first +2. preserve raw slot order in provisional vtables +3. keep uncertain fields explicit as padding or unknown words/bytes +4. prefer conservative owner names over polished but speculative subsystem names +5. add provenance comments when a layout or slot label is imported from an earlier note rather than re-derived in-session + +## Pilot A: `EntityDispatchEntry` + +Use [docs/entity-dispatch-entry-class-layout.md](docs/entity-dispatch-entry-class-layout.md) as the source note. + +### Minimum Ghidra/MCP deliverables + +1. create owner namespace/class for the base family +2. create provisional instance struct for the base layout +3. create provisional vtable datatype with stable known slots only +4. move base ctor/release methods under that owner +5. keep subtype/overlay-specific methods outside the base owner until the split is tighter + +### First methods to move + +- base ctor at `0008:ba00` +- release/destroy surface at `0008:dbec` +- runtime-state init/release pair `000d:7e00` / `000d:8078` only if the owner relationship is still judged direct in-session + +### Fields that should stay visible immediately + +- type and subtype-related words near `+0x04/+0x06/+0x08` +- active/hold/runtime flags around `+0x16/+0x18` +- runtime-state and paired resource lanes around `+0x3c/+0x40` + +### Things to avoid in the first pass + +- flattening every derived constructor into one base class +- forcing final names for every slot in the vtable +- pretending all runtime-state fields belong to the same subtype + +## Pilot B: `SpriteNode` + +Use [docs/sprite-node-class-layout.md](docs/sprite-node-class-layout.md) as the source note. + +### Minimum Ghidra/MCP deliverables + +1. create owner namespace/class for `SpriteNode` +2. create provisional vtable with only the slot order and best current role labels +3. move destructor and event-dispatch surface under that owner +4. create instance struct with the known layout anchors and visible unknown regions + +### First methods to move + +- `000b:326e sprite_node_destroy` +- `000b:3ab2 sprite_node_dispatch_event` +- `000b:3380 sprite_node_is_dirty` +- `000b:33a6 sprite_node_mark_dirty` + +### Things to avoid in the first pass + +- pushing traversal helpers into the class just because they walk the tree +- overcommitting to child/sibling field semantics beyond the already-noted anchors + +## Pilot C: `EntityVmOwnerResource` + +Use [docs/entity-vm-runtime-owner-resource-layout.md](docs/entity-vm-runtime-owner-resource-layout.md) as the source note. + +### Minimum Ghidra/MCP deliverables + +1. create helper class owner +2. create compact helper instance struct +3. create provisional method table for the `+0x04` and `+0x0c` helper callbacks +4. move create/destroy pair under that owner + +### Things to avoid in the first pass + +- baking in final file-family schema labels +- collapsing the helper into the runtime object as anonymous fields + +## Source-Emission Readiness Gate + +Do not emit the first C++ skeleton immediately after Ghidra authoring. + +Only emit when all of these are true: + +1. owner namespace/class exists +2. provisional instance struct exists +3. provisional vtable exists when relevant +4. top 3-5 strongly owned methods are moved +5. unresolved fields remain explicit instead of silently renamed away +6. Track A versus Track B assumptions are recorded for that family + +## Success Criteria For The First Real Batch + +The first class-authoring batch is successful if: + +1. one family becomes visibly easier to navigate in Ghidra +2. method ownership is improved without speculative over-grouping +3. instance layout is more explicit than raw offset math +4. the result can drive one hand-maintained C++ header/source skeleton with the compatibility layer + +## Bottom Line + +The first class-authoring batch should be judged by structural clarity, not by how polished or object-oriented the output looks. + +`EntityDispatchEntry` remains the best first pilot because it offers the highest ratio of stable object evidence to remaining subtype ambiguity. \ No newline at end of file diff --git a/docs/remorse-rebuild-abi-notes.md b/docs/remorse-rebuild-abi-notes.md new file mode 100644 index 0000000..b7f4023 --- /dev/null +++ b/docs/remorse-rebuild-abi-notes.md @@ -0,0 +1,212 @@ +# Remorse Rebuild ABI Notes + +## Purpose + +This note records the current ABI, memory-model, and toolchain constraints that should shape any future Remorse source reconstruction. + +The class-lifting notes answer `what the objects probably are`. +This note answers `what the rebuilt source must still respect if it aims to become a working executable rather than only readable C++`. + +## Current Baseline + +The live target is not a flat modern Win32 program. + +Current verified binary facts: + +- DOS target +- 16-bit protected-mode environment +- Phar Lap 286 DOS-Extender (`RUN286`) +- bound `MZ -> NE` executable +- heavy use of inter-segment and external `CALLF` fixups + +That means the default safe assumption is: + +- segmented code/data model matters +- near/far calls matter +- pointer width and calling convention details matter +- loader/runtime expectations matter + +## Hard Constraints Already Visible In The Binary + +### 1. Segmented addressing is real, not presentation noise + +Evidence: + +- executable format is `MZ -> NE` +- raw import behavior collapses unresolved calls to `0000:ffff` until NE fixups are applied +- repaired raw import had thousands of internal literal `CALLF` sites patched to real segment:offset targets +- the notes repeatedly distinguish far pointers, segment:offset storage, and per-segment relocation behavior + +Practical implication: + +- a rebuild target that ignores far calls and far data pointers too early will drift away from the original executable model + +### 2. Function boundaries and external calls are loader-sensitive + +Evidence: + +- `CALLF 0000:ffff` is a placeholder used by the NE loader for real inter-segment/external targets +- unresolved far thunk behavior in raw import is explicitly not a real dispatcher + +Practical implication: + +- source emission must preserve which calls are logically intra-object methods and which ones are ABI-significant far calls or imported runtime/library calls + +### 3. Runtime/library layer is not trivial glue + +Evidence: + +- large Phar Lap runtime/extender segments remain part of startup and low-level system behavior +- CRT wrappers and formatter/runtime helpers are explicitly identified +- MetaWare High C formatting/runtime wrappers are present in the notes + +Practical implication: + +- the original or near-original compiler/runtime environment matters enough that `just compile with a modern compiler` is not a safe early assumption for an original-style rebuild + +### 4. Object layout is tightly coupled to exact field offsets + +Evidence: + +- major gameplay and UI families are still being recovered by exact offsets +- VM/runtime helpers, dispatch entries, and entity families all depend on stable field positions + +Practical implication: + +- class lifting must preserve packed layout discipline and exact-width integer choices from the start + +## Current Best Toolchain Read + +This is still a working model, not a closed historical claim. + +### High-confidence environment facts + +- DOS protected mode under Phar Lap 286 extender +- NE executable image +- runtime/CRT evidence compatible with MetaWare High C presence in at least part of the binary toolchain story + +### What remains open + +- exact original compiler version +- exact memory-model flags used for all modules +- exact calling-convention mapping for each object family +- exact linker/build recipe needed to reproduce compatible NE output + +## Recommended Rebuild Tracks + +### Track A: Original-style executable reconstruction + +If the goal is to rebuild something close to the shipped executable model, the source must preserve: + +- segmented pointer distinctions +- explicit near/far calling boundaries where needed +- exact struct packing +- compatible CRT/runtime assumptions +- executable/resource layout expectations + +This is the stricter track. + +### Track B: Behaviorally equivalent source port + +If the goal is instead a working engine/game rebuild using the original data with equivalent behavior, then the source can relax some ABI constraints later. + +But even on this track, the early reverse-engineering output should still preserve ABI facts long enough that the project can make an informed choice instead of accidentally forcing itself into a port. + +## Source-Level Rules To Adopt Early + +Any future generated or handwritten code should default to these constraints: + +### Integer widths + +- use explicit fixed-width integer types everywhere possible +- do not use plain `int`, `long`, or compiler-default enum width as semantic types in the first pass + +### Layout control + +- keep a visible packing strategy for recovered structs +- record uncertain padding explicitly rather than letting the compiler invent it silently + +### Pointer model + +- keep far-pointer distinctions visible in the type system or wrapper layer +- do not immediately collapse all pointers to one flat host pointer type if Track A remains in scope + +### Calling conventions + +- keep calling convention annotations explicit in working notes and emitted skeletons +- do not assume one modern host calling convention is an adequate stand-in for every recovered method or helper + +### Virtual dispatch + +- preserve raw slot order in provisional vtable types +- do not rename or reorder slots to look cleaner before the mapping is stable + +## Candidate ABI Support Layer + +The first C++ source slices should probably compile against a small compatibility layer rather than raw host C++ alone. + +Current likely categories: + +- exact-width integer typedefs +- far/near pointer wrappers or placeholder abstractions +- packing macros or pragmas +- calling-convention macros +- segmented address helper types for debugging and trace comparison +- imported runtime service shims for file, memory, and platform calls + +## Immediate Compiler/Runtime Questions To Close Later + +These are the most useful next ABI questions for the repo: + +1. Which compiler/runtime signatures in the binary most strongly identify the original toolchain family and version? +2. Which current methods clearly require far-call semantics even after class lifting? +3. Which object families can safely be emitted as host-side plain structs first, and which still need explicit segmented-pointer wrappers? +4. What is the narrowest executable milestone that can validate calling conventions and struct layout before whole-program reconstruction is attempted? + +## Practical Risk List + +### Risk: pretty C++ that cannot rebuild the game + +Cause: + +- class lifting done without ABI discipline + +Mitigation: + +- keep this note paired with the class-layout notes and require exact-width/packing/calling-convention placeholders in early skeletons + +### Risk: false confidence from host compilation success + +Cause: + +- code compiles under a modern compiler but no longer matches segmented runtime behavior + +Mitigation: + +- define compile success and behavioral/ABI success as separate milestones + +### Risk: loss of far-call/import provenance + +Cause: + +- unresolved thunk placeholders or loader-patched calls get flattened into generic helper names + +Mitigation: + +- preserve call provenance in notes and later exports, especially for methods that only look local after fixup repair + +## Recommended Near-Term Documentation Follow-Ups + +1. collect all current compiler/runtime fingerprints into one evidence note +2. add an `ABI concerns` section to future class-layout notes when a family uses far pointers or segmented ownership directly +3. draft the first minimal compatibility header for future C++ skeletons once the first class family is selected for source emission + +## Current Bottom Line + +The project is now documented well enough to start class lifting, but not well enough to safely emit `clean modern C++` without guardrails. + +The safest present rule is: + +- keep object recovery aggressive +- keep ABI assumptions conservative +- keep Track A and Track B separate in every future source milestone \ No newline at end of file diff --git a/docs/remorse-toolchain-fingerprint-evidence.md b/docs/remorse-toolchain-fingerprint-evidence.md new file mode 100644 index 0000000..1f530c4 --- /dev/null +++ b/docs/remorse-toolchain-fingerprint-evidence.md @@ -0,0 +1,175 @@ +# Remorse Toolchain Fingerprint Evidence + +## Purpose + +This note gathers the strongest current compiler, runtime, loader, and ABI fingerprints that matter for eventual source reconstruction. + +It exists to answer one narrow question better than the broader ABI note: + +- what concrete binary evidence currently supports the working toolchain and executable-model assumptions? + +This note should stay paired with [docs/remorse-rebuild-abi-notes.md](docs/remorse-rebuild-abi-notes.md). + +## High-Confidence Executable Model Evidence + +### 1. Bound `MZ -> NE` executable + +Strong anchors from [docs/overview.md](docs/overview.md): + +- outer DOS header is `MZ` +- `e_lfanew = 0x36F70` +- internal header at `0x36F70` is `NE` +- internal NE image describes `145` segments + +Why it matters: + +- the game is not a flat DOS EXE with incidental overlays +- the executable model already assumes segmented protected-mode program structure + +### 2. Phar Lap 286 DOS extender + +Strong anchors from [docs/overview.md](docs/overview.md) and [docs/phar-lap-extender.md](docs/phar-lap-extender.md): + +- executable is documented as using Phar Lap 286 DOS-Extender (`RUN286`) +- major code regions are extender/runtime segments rather than game logic segments +- named loader path includes `init_dos_extender`, `load_executable_image`, `apply_relocations`, and child-transfer helpers + +Why it matters: + +- the startup/runtime environment is part of the program contract, not an afterthought +- Track A reconstruction must preserve this loader/executable-model reality or replace it deliberately + +### 3. Runtime-patched far-call model + +Strong anchors from [docs/overview.md](docs/overview.md): + +- unresolved inter-segment and external calls appear in raw import as `CALLF 0000:ffff` +- those are NE loader fixup placeholders, not one dispatcher +- repaired raw import already patched `8851` internal literal far-call sites to real targets + +Why it matters: + +- far-call provenance is real ABI evidence +- any future source lift has to preserve which edges are ordinary local methods versus loader-significant far calls/imports + +## Runtime / CRT Fingerprints + +### 1. Phar Lap runtime strings + +Strong anchors from [docs/phar-lap-extender.md](docs/phar-lap-extender.md): + +- `13fc:0016` = `$Id: comhighc.c 1.1 91/08/06...` +- `13fc:0048` = `$Id: comutils.c 1.1 91/08/06...` +- `1760:665c` = `Copyright (C) 1986-93 Phar Lap Software, Inc.` +- `1760:73da` = `-LDTSIZE 4096 -EXTHIGH D0_0000h -NI 18 -ISTKSIZE 3` + +Current safest interpretation: + +- Phar Lap runtime/source provenance is directly embedded in the binary +- `comhighc.c` is the strongest current fingerprint tying part of the runtime story to High C-related runtime material + +### 2. Protected-mode service and memory helpers + +Strong anchors from [docs/phar-lap-extender.md](docs/phar-lap-extender.md): + +- DPMI/interrupt wrappers in segment `1339` +- EMS management in segment `1677` +- task switching and child-process execution paths in `10da` and `1760` + +Why it matters: + +- the executable depends on a real protected-mode runtime layer with memory and interrupt service expectations +- this makes `modern compiler output that merely compiles` a weak reconstruction milestone by itself + +## Binary-Structure Fingerprints That Affect Source Emission + +### 1. Segmented address layout is visible throughout analysis + +Strong anchors from [docs/overview.md](docs/overview.md): + +- raw address model uses `SSSS:OOOO` +- game code begins only after the Phar Lap loader region +- notes repeatedly distinguish extender segments from NE gameplay segments + +Implication: + +- source that immediately collapses every pointer and call edge into one flat host model loses verified structure too early + +### 2. Loader-sensitive call repair already affects function understanding + +Strong anchors from [docs/overview.md](docs/overview.md): + +- callsites had to be repaired before large parts of the raw import became meaningful +- inter-segment and external targets are encoded through relocation records, not fixed immediate addresses in the raw bytes + +Implication: + +- future class lifting should preserve import/far-call comments or metadata, especially for methods that only look local after fixup repair + +## Working Compiler Story: What Is Safe And What Is Not + +### Safe now + +- Phar Lap 286 protected-mode DOS environment is real +- NE segmented executable model is real +- runtime strings directly reference `comhighc.c` and `comutils.c` +- the broad toolchain story includes Phar Lap runtime material and High C-related runtime evidence + +### Not safe to claim as closed yet + +- exact original compiler version for every module +- exact linker flags for the game NE image +- exact near/far defaults and calling-convention flags used by all gameplay modules +- exact rebuild recipe needed for a compatible historical executable + +## Evidence Strength By Question + +### Question: does segmented ABI discipline matter? + +Answer: + +- yes, strongly supported + +Why: + +- NE format, loader-patched far calls, and segment-separated code organization all point the same way + +### Question: is a High C-related runtime story real or speculative? + +Answer: + +- real at the runtime-fingerprint level, still incomplete at the full-build-chain level + +Why: + +- `comhighc.c` string evidence is concrete +- full per-module compiler attribution is not yet closed + +### Question: can Track A and Track B still share the same early source work? + +Answer: + +- yes, if early source keeps exact widths, packing, far-call provenance, and segmented-pointer placeholders visible + +## What This Means For Future Real Work + +When MCP class tools are ready or when hand-written skeletons start, these fingerprints should drive the rules: + +1. keep exact-width aliases mandatory +2. keep packing explicit +3. keep segmented-pointer or far-pointer placeholders available +4. keep calling-convention markers visible even when still provisional +5. keep far-call/import provenance attached to lifted methods where it matters + +## Highest-Value Remaining Fingerprint Questions + +1. collect more direct CRT/helper signatures that distinguish Phar Lap runtime pieces from gameplay-generated code +2. identify which recovered object families most clearly cross near/far ownership boundaries +3. isolate functions whose call shape strongly suggests non-default calling conventions +4. determine the smallest rebuild slice that can test layout and call discipline before whole-program ambitions + +## Bottom Line + +The current toolchain story is strong enough to justify ABI-conservative source emission rules. + +The safe working model remains: Phar Lap protected-mode DOS, bound `MZ -> NE` executable, loader-patched far-call environment, and a real High C-related runtime fingerprint that is informative but not yet the entire historical build recipe. \ No newline at end of file diff --git a/docs/removed_items.md b/docs/removed_items.md index 3104091..cb6359d 100644 --- a/docs/removed_items.md +++ b/docs/removed_items.md @@ -138,8 +138,8 @@ The broader Remorse catalog does contain more unused-looking or obviously non-re ### Placeholder cube family - `0x0251` no longer fits the generic placeholder bucket well. Current best read is `VALUEBOX`, a local data/helper box used by monitors, watcher panels, security displays, and keypads. +- `0x0318` also no longer belongs in the generic placeholder-cube bucket. The older Remorse catalog label `PLACEHOLDER_CUBE` is now weaker than the extracted usecode evidence: both Remorse and Regret name class `0x0318` as `CRUMORPH`, and the recovered `equip` body is a control-transfer pad that compares local `QLo` against mutable actor field `0x63` before bracketing `TRIGGER.slot_20`. - The strongest remaining placeholder-cube entries from the catalog/scene pass are: - - `0x0318` = `PLACEHOLDER_CUBE` - `0x0337` = `PLACEHOLDER_CUBE_BIG` - `0x0361` = `PLACEHOLDER_CUBE_RED_BLACK` - Cached Remorse scenes place these on multiple maps, so they are not catalog-only artifacts. diff --git a/docs/sprite-node-class-layout.md b/docs/sprite-node-class-layout.md new file mode 100644 index 0000000..9aa6822 --- /dev/null +++ b/docs/sprite-node-class-layout.md @@ -0,0 +1,165 @@ +# SpriteNode Class Layout + +## Purpose + +This note captures the current class-level read of the `SpriteNode` family so later Ghidra class work can move quickly and conservatively. + +Compared with `EntityDispatchEntry`, this family has a cleaner explicit virtual/event-dispatch surface and a more bounded ownership model. That makes it an excellent second pilot for class lifting. + +## Current Best Class-Level Read + +`SpriteNode` is a tree-based render/UI node family with: + +- child-chain ownership +- accumulated position and bounds propagation +- dirty-state tracking +- central event dispatch through a small virtual surface +- destructor ownership over child nodes and global focus state + +This already looks much closer to an ordinary C++ object family than many gameplay-side structures. + +## Strongest Evidence Anchors + +### Destructor + +#### `000b:326e` `sprite_node_destroy` + +Current best read: + +- destructor-style path +- sets vtable ptr to `0x501a` +- clears global focus pointer `[0x4fd0:0x4fd2]` if `self` +- releases child nodes +- frees object memory through `mem_free` + +This is the strongest current single proof that `SpriteNode` should be lifted as an owned object family. + +### Event dispatch + +#### `000b:3ab2` `sprite_node_dispatch_event` + +Current best read: + +- large event dispatch switch +- checks event types `2/4/8/0x100` +- updates global focus pointer at `[0x4fd0:0x4fd2]` +- dispatches through virtual slots `+0x14`, `+0x18`, `+0x20`, `+0x24` + +This is the strongest current proof of a stable virtual method surface. + +### Dirty/update family + +- `000b:3380 sprite_node_is_dirty` +- `000b:33a6 sprite_node_mark_dirty` +- `000b:40ee sprite_node_update_and_dispatch` + +These show that node state changes and redraw/update dispatch are methods on the same family, not just free helper functions wandering across unrelated data. + +### Recursive tree helpers + +- `000a:b988 sprite_node_get_or_traverse` +- `000b:358d sprite_tree_accumulate_pos` +- `000b:3a00 sprite_tree_sum_x_offset` +- `000b:3a35 sprite_tree_sum_y_offset` + +These are strong evidence for a child-linked tree object with inherited coordinate accumulation. + +## Candidate Layout + +This layout is intentionally conservative. + +| Offset | Current name | Confidence | Current meaning | +|---|---|---|---| +| `+0x19/+0x1b` | `child_or_next_ptr` | High | Child-chain pointer pair used by recursive traversal and offset accumulation. | +| `+0x21` | `local_x_offset` | High | Summed by `sprite_tree_sum_x_offset` / accumulate helpers. | +| `+0x23` | `local_y_offset` | High | Summed by `sprite_tree_sum_y_offset` / accumulate helpers. | +| `+0x29` | `dirty_flags` | High | Checked by `sprite_node_is_dirty`; manipulated by mark/update paths. | +| `+0x17e` | `redraw_flag` | Medium | Cleared by `sprite_clear_redraw_flag`; likely a subtype or larger-object field tied to one SpriteNode family variant. | + +## Layout Caveat + +The current notes likely mix a compact core `SpriteNode` with one or more larger derived UI/display objects. The evidence for `+0x17e` strongly suggests there are bigger family members or wrapper objects in the same virtual ecosystem. + +So the safe future modeling strategy is: + +- define a small `SpriteNodeBase` +- keep larger UI/display fields in derived or sibling structs until more offsets are closed + +## Candidate Method Map + +### Strong instance methods + +| Address | Current function | Candidate method role | +|---|---|---| +| `000b:326e` | `sprite_node_destroy` | `Destroy()` | +| `000b:3380` | `sprite_node_is_dirty` | `IsDirty()` | +| `000b:33a6` | `sprite_node_mark_dirty` | `MarkDirty()` | +| `000b:3ab2` | `sprite_node_dispatch_event` | `DispatchEvent()` | +| `000b:40ee` | `sprite_node_update_and_dispatch` | `UpdateAndDispatch()` | +| `000a:b988` | `sprite_node_get_or_traverse` | `GetOrTraverse()` | + +### Strong family-local helpers that may remain free/static + +| Address | Current function | Why it may stay non-method | +|---|---|---| +| `000b:3a00` | `sprite_tree_sum_x_offset` | Pure recursive accumulation helper; method status depends on later decompile readability. | +| `000b:3a35` | `sprite_tree_sum_y_offset` | Same as above. | +| `000b:330c` | `sprite_tree_dispatch_wrapper` | Looks like a pure thunk wrapper rather than a meaningful source-level method. | +| `000b:3362` | `sprite_tree_unwind_check` | Stack-segment guard helper; probably not worth presenting as a class method. | + +## Candidate Virtual Slot Map + +The currently verified slots are already good enough for a first typed vtable. + +| Slot offset | Current best role | Evidence | +|---|---|---| +| `+0x14` | event handler A | `sprite_node_dispatch_event` dispatches here for one event class | +| `+0x18` | event handler B | same dispatcher | +| `+0x20` | event handler C | same dispatcher | +| `+0x24` | event handler D | same dispatcher | + +The seg091 default-slot helpers are also useful evidence: + +- `000a:7b44`, `000a:7b49`, `000a:7b53`, `000a:7b4e`, `000a:7b78`, `000a:7b7d`, `000a:7b30`, `000a:7b3f`, `000a:7b35`, `000a:7b3a` +- `000a:7b58` returns zero and behaves like a default no-op boolean slot +- `000a:7b5f` is a forwarding trampoline slot + +These likely belong to one or more shared/default node vtables and should be preserved as vtable evidence even if they never become pretty source-level methods. + +## Ownership And Global State + +### Focus/global state + +Global focus pointer `[0x4fd0:0x4fd2]` is updated in the dispatch family and cleared in the destructor. + +That gives the family a real interaction with global UI focus/state, but the key point for class work is simpler: + +- focus ownership is tied to the node family itself +- this is not just an arbitrary free helper changing global UI state + +### Child ownership + +The destructor and recursive sum/traverse helpers strongly suggest real child ownership or at least managed child linkage. + +That means later class modeling should preserve a node/tree mental model rather than flattening everything into stand-alone display items. + +## Candidate Ghidra Modeling Plan + +When class authoring begins, the safest sequence for this family is: + +1. create class namespace `SpriteNode` +2. move `Destroy`, `IsDirty`, `MarkDirty`, `DispatchEvent`, `UpdateAndDispatch`, and `GetOrTraverse` first +3. create minimal `SpriteNodeBase` struct with the stable offsets around `+0x19`, `+0x21`, `+0x23`, and `+0x29` +4. create provisional vtable with slots `+0x14`, `+0x18`, `+0x20`, `+0x24` +5. keep recursive tree helpers outside the class until decompiler output shows they benefit from becoming methods + +## Open Questions + +- exact root vtable address or addresses for the main SpriteNode family +- whether the `+0x17e` redraw flag belongs to a derived display node rather than the compact base node +- which event-code cases map to which slot semantically beyond the current `A/B/C/D` placeholder naming +- whether `sprite_tree_accumulate_pos` should become a class method, a static helper, or a separate geometry utility + +## Immediate Follow-Up Value + +The most useful next companion work after this note is not more sprite detail by itself. It is the rebuild-ABI note, because once the first few class families are documented this well, the next real risk is drifting away from the original memory and calling-convention model before any code is emitted. \ No newline at end of file diff --git a/ghidra_mcp_wishlist.md b/ghidra_mcp_wishlist.md index ad966ee..a3719de 100644 --- a/ghidra_mcp_wishlist.md +++ b/ghidra_mcp_wishlist.md @@ -267,4 +267,27 @@ Short, concrete gaps hit during live Crusader work. Each entry records what MCP - `list_project_programs(project_dir?, project_name?, folder_path?, recursive=true)` - returns folder path, program name, read-only/writable/versioned state, and whether it is currently open - Why it matters: comparing or porting across programs is awkward without a discoverable inventory of assemblies already in the Ghidra project. -- Status update (2026-03-26): local fork now exposes `list_project_programs(project_dir?, project_name?, folder_path?, recursive=true)` plus a `project_programs` alias; it walks project folders and returns machine-friendly program inventory lines with folder path, program name, content type, read-only/versioned flags, and current-open state. \ No newline at end of file +- Status update (2026-03-26): local fork now exposes `list_project_programs(project_dir?, project_name?, folder_path?, recursive=true)` plus a `project_programs` alias; it walks project folders and returns machine-friendly program inventory lines with folder path, program name, content type, read-only/versioned flags, and current-open state. + +### Class / namespace authoring for C++ lifting + +- Missing MCP capability: create and manage Ghidra class or namespace symbols, then move existing functions under those owners as methods. +- Current fallback: manual Ghidra GUI edits in the Symbol Tree or one-off local scripts outside the normal MCP workflow. +- Why it matters: the Remorse binary already shows repeated ctor/dtor patterns, stable vtable roots, and class-like object families, but the current MCP workflow can only rename flat functions. That blocks a disciplined shift from procedural naming toward grouped C++-style ownership. +- Proposed MCP addition: + - `create_namespace(name, parent_path?, kind=namespace|class)` + - `move_symbol_to_namespace(symbol_address_or_name, namespace_path, new_name?)` + - `set_function_class(function_address, class_path, method_name?, this_param_name?, calling_convention?)` + - machine-friendly responses that include the final symbol path and any rename collisions. + +### Vtable / OO recovery helpers for class reconstruction + +- Missing MCP capability: first-class helpers for identifying vtables, attaching function slots to candidate classes, and materializing class/instance layouts from evidence-backed data. +- Current fallback: manual note collation from decompiler/disassembly output plus ad hoc datatype work in the GUI. +- Why it matters: the repo already has enough evidence to start lifting major families into C++ classes, but a recompilable source path needs more than renamed functions. It needs reproducible vtable maps, `this`-pointer typing, field layouts, inheritance guesses, and explicit provenance for each class model. +- Proposed MCP addition: + - `analyze_vtable(address, slot_count?, namespace_path?)` + - `create_or_update_struct(name, size?, fields)` + - `set_function_this_type(function_address, struct_name, this_storage=stack|register|farptr)` + - `apply_class_layout(class_path, instance_struct, vtable_struct?, methods)` + - optional dry-run output showing inferred slots, unresolved targets, and conflicting field/size evidence. \ No newline at end of file diff --git a/plan-mid.md b/plan-mid.md index bd302b3..869d546 100644 --- a/plan-mid.md +++ b/plan-mid.md @@ -15,284 +15,129 @@ Detailed completed analysis belongs in the files under `docs/`, not in this plan ## Progress Snapshot -- Overall useful decompilation progress: about 50% -- Reasonable uncertainty band: about 45% to 52% -- Top 100 far-call target coverage: about 80% -- Segment spread with meaningful analysis: about 26% to 32% -- Tooling maturity for continued work: about 77% +Latest verified batch: [docs/combat-dat.md](docs/combat-dat.md) now closes the shipped combat-tactic data file as a documentation target instead of leaving it as a scratch-note reference. Current best read is that all local Remorse/Regret variants share one identical `14`-record `COMBAT.DAT`, the live NE database now already has the right tactic/process field anchors (`combatDatTacticPtr`, `combatDatTacticCurOffset`, `combatDatBlockNo`, `tacticNo`) plus setup helpers, and the shipped opcode subset is now decoded into a full human-readable tactic catalog using direct binary parsing plus the ScummVM Crusader attack-process interpreter as a reference model. -### Why The Estimate Moves Slightly +- Overall useful decompilation progress: about 58% +- Reasonable uncertainty band: about 55% to 63% +- Top 100 far-call target coverage: about 86% +- Segment spread with meaningful analysis: about 34% to 40% +- Tooling maturity for continued work: about 83% -- Recent work materially improved semantic confidence inside the startup/display, cache/allocator, callback-object, and USECODE/VM lanes. -- The startup/display lane is now materially complete as an active major section: the shared `g_active_dispatch_entry_farptr[+0x40]` hold token is separated from the seg108-local `0x4f38` bit-`0x40` lane, the seg126 control stream is confirmed as file-backed, the paired `0x8c5c/0x8c60` renderer objects are narrowed to two script-selected preset text lanes, and the neighboring seg127 fade controller now has an exact local contract at `0x630a..0x6316`. -- The current VM/loader batch also justified a small bump: `000d:ebe3` is now a named ordered opcode sequencer with a tighter entry/exit contract, the masked-create hub at `000d:463a` is now a verified owner-table gate rather than an inferred wrapper sink, and the seg070 twin loops under `entity_vm_runtime_owner_resource_create` now read as paired file-family loaders writing into separate temporary buffers rather than one ambiguous callback shard. -- The latest live-NE caller-family batch justified another small confidence bump: the remaining direct `0005:295f -> 10a0:275f` callers now close to `Item_ReceiveHit` and `SuperSprite_HitAndFinish` non-NPC damage lanes, which removes the last direct-caller ambiguity in that selector-consumer island even though the upstream class-family selector is still unresolved. -- The latest USECODE pass justified another small VM-lane bump: the gameplay-side wrapper ladder now extends through slots `0x10..0x14` with verified mixed payload shapes (`none` vs extra signed word), the new slot-only Ghidra names keep that taxonomy visible without overpromoting event labels, and the `000d:22bc` stage is now comment-backed as a sequencer-internal link-matrix/pushback consumer over decoded workspace bytes rather than a direct descriptor-row reader. -- The immortality follow-up justified another small tooling-and-confidence bump: the extractor now emits a dedicated target-body scan, the strongest current USECODE candidates show no inline `0x410` / `0x00000410` literal, and the remaining frontier is narrowed to data-driven decoding of `EVENT` slot `0x0a` plus `NPCTRIG` slots `0x0a` / `0x20` rather than the older wider trigger family set. -- The latest owner-loaded range pass justified another small confidence bump too: the owner-resource child selector now matches extracted `class_id + 2` exactly, the class header/subentry math at `000d:5066/51fd/53b4` is closed against the extractor's raw headers and event rows, and the surviving immortality uncertainty has moved from `can the loader fit NPCTRIG arithmetic at all?` to the narrower `which class family is actually selected upstream?` question. -- The PSX sprite-extraction side is also less speculative now: a dump-grounded pass proved the known-colored wall-console bundle `bundle_000A1B04` already exists verbatim in live VRAM at texture page `(1,1)`, and the corrected working color formula is the top-left live CLUT candidate from the atlas, namely the contiguous `256`-entry slice at GPU row `0xF0`, `x=0`; the same rule now produces plausible output across a wider `92`-bundle `mode 1` batch instead of only the single cabinet proof case. -- The PSX executable-side catalog lane is tighter too: `SLUS_002.68` now has comment-backed proof that `wdl_resource_bundle_load_by_index` selects seven hardcoded `\LSETn\L` prefixes across thresholds `10/20/30/40/50/60`, the extracted disc currently ships `62` level bundles (`L0..L58`, `L62..L64`) with a real gap at `L59..L61`, the executable exposes only `15` plain-text `Mission Briefing ^Mission N` strings, and the mission-complete passcode path now has a closed `4`-character consonant/digit alphabet at `80063ef0` plus direct ammo/item/weapon name tables. The remaining PSX passcode gap is now narrower: public cheat-password candidates `XXXX` and `L0SR`/`L0SER` are not stored as plain ASCII in `SLUS_002.68`, so the compare path likely uses numeric or transformed validation instead of a flat string table. -- The F7-overlay lane is tighter again after the latest live/exported cross-check. New note `docs/f7-overlays.md` now separates the three cheat-gated overlays by their actual geometry source: plain `F7` is the coarse origin-aligned `0x200`-unit world lattice; `Ctrl+F7` is the egg-hatcher trigger-footprint overlay driven by `EggHatcher_1090_0921`; and `Alt+F7` is narrower than the earlier viewer approximation because the runtime only feeds `Snap_AddSnapEgg` from shape `0x04fe`, with `Snap_GetSnapEggRange` deriving each snap rectangle from that item's `QHi`, `mapNum`, and `npcNum` bytes. The practical viewer implication is also closed more cleanly now: do not center-snap the plain grid, and do not treat `Alt+F7` as generic egg-family coverage. -- The new PSX pre-alpha comparison lane is also anchored now: `/psx/prealpha/SLUS_002.68` still carries direct `Crusader: No Remorse` branding, the same retail-style `wdl_resource_bundle_load_by_index` `\LSET1\L .. \LSET7\L` threshold ladder, and the same `15` mission-briefing/passcode shell, but the unpacked `Crusader 2 Pre-Pre Alpha` disc currently ships only `3` level bundles, `1` XA, and no `.STR` movies. The most interesting current mismatches are architectural leftovers that no longer match the disc literally, especially the missing-file `\AUDIO\TALK1.XA;1` path and the surviving `LoadExec` helper for `MENU.EXE` / `ENGINE.EXE` / `PSX.EXE`. -- That closes one live top-priority section and justifies a small headline increase even though the remaining work is still breadth-heavy. +### Why The Estimate Moved + +- The NE `CRUSADER.EXE` database now has materially more named functions, better caller-role coverage, and broader comment-backed provenance than when this tracker was first drafted. +- The startup/display lane is no longer a top active section. Its outer ownership and control flow are stable enough that it should stay closed unless new caller evidence changes the model. +- The cheat/debug lane is also much tighter: the `jassica16` latch, the broader `-laurie` gate, the `~` runtime toggle, the F7-family overlays, the F10/Ctrl behavior, and the `0x410` CD-transfer-display branch are now separated well enough that this lane is mostly documentation and cleanup, not architecture recovery. +- The USECODE/VM lane has moved from broad structure guesses to a partial runtime model: core loader/runtime helpers are named, owner-loaded slot arithmetic is verified against extracted corpora, several masked-create helpers have real contracts, and the major remaining uncertainty is now the upstream selector/caller path rather than the storage format itself. +- The map-renderer crosswalk lane also removed a lot of lingering shape ambiguity by closing more controller/helper families directly from extracted corpora plus scene evidence. +- The combat-tactic data lane is also now materially tighter: `COMBAT.DAT` is no longer just a named-tactic hint source, but a documented bytecode archive with stable per-record names, verified block structure, a decoded shipped opcode subset, and a practical family-level behavior map for the `Dumb`, `Pivot`, `Advance`, `Careful`, marker-shuttle, and step-out-shoot tactics. ## Current Verified State ### Primary Tracking Assets -- `crusader_segment_coverage_ledger.csv` now exists for all 145 NE segments and should remain the primary coverage tracker. -- `crusader_decompilation_notes.md` is now only an index; detailed evidence lives in `docs/`. -- `CRUSADER.EXE` is now the default live Ghidra target for ongoing work; verified `CRUSADER-RAW.EXE` results remain a cross-reference evidence base, especially for seg001/seg021 and earlier cheat/VM batches. -- The raw full-EXE porting workflow remains stable as a supporting evidence path for the verified seg001 and seg021 mappings. +- `crusader_segment_coverage_ledger.csv` remains the main executable-wide coverage tracker and should be updated after each verified batch. +- `crusader_decompilation_notes.md` is an index, not the place for long-form analysis. +- `CRUSADER.EXE` remains the default live Ghidra target. +- Verified `CRUSADER-RAW.EXE` work remains a supporting evidence base for ports, naming provenance, and caller/context cross-checks. ### Strong Or Stable Areas -- seg001 gameplay/input/projectile work is deep enough to support verified raw-name ports. -- raw 0007 rendering/camera/tile-visibility work is structurally strong. -- 0008 dispatch-entry helpers and 000c state-machine helpers have broad partial coverage. -- 000a/000d tracked-handle, cache, allocator, dispatch-entry, and startup/display support lanes now have a coherent partial map. -- 000e parser and animation subsystems have a real partial map. -- The auxiliary local disassembly corpus at `K:/ghidra/crusader-disasm` is now inventoried and integrated as a separate evidence source for shape metadata, static map/object dumps, opcode names, and older Remorse/Regret intrinsic-function vocabularies; its safe-reuse rules and porting implications are captured in `docs/crusader-disasm-reference.md`. -- The PSX side now has a first explicit pre-alpha comparison note too. `docs/psx/prealpha.md` records that `/psx/prealpha/SLUS_002.68` is still much closer to a reduced No Remorse PSX branch than to a visibly rebranded sequel executable: the live database now has `wdl_resource_bundle_load_by_index` renamed at `80038084`, and comment-backed notes on the stale `TALK1.XA` selector helper and the split-`LoadExec` `MENU.EXE` / `ENGINE.EXE` / `PSX.EXE` path that no longer matches the current unpacked disc tree. -- The workspace now also has a first dedicated offline map-rendering/tooling lane: `tools/render_crusader_map.py` can load a chosen `FIXED.DAT`, expand `GLOB.FLX`, decode required `SHAPES.FLX` frames, apply `GAMEPAL.PAL`, and emit a first-pass PNG from either static set, while `docs/map-rendering.md` captures the current format contracts, the `--fixed-dat` override, and the intentionally limited compositor model. -- The map/editor-visibility lane is now tighter too. New note `docs/editor-object-visibility.md` records live `CRUSADER.EXE` proof that the downstream item draw helper `1198:02e4` (`Item_PaintSprite`) explicitly returns early on `ShapeData.flags2 & 1` (`SI_EDITOR`), but the follow-up render-path pass also found the controlling upstream skip at `1180:0951..095c` in the world-item builder. Current best read is therefore `editor-tagged shapes are filtered before draw-node allocation in the normal world-item renderer, with a second downstream paint-time guard still present`, which also explains why a first patch that only flipped `1198:033b` produced no visible change in-game. No recovered retail `-debug`, cheat/debug hotkey, Laurie/usecode-debugger path, or `0x410` lane currently re-enables those objects. The closest confirmed toggle remains ScummVM's own `_showEditorItems` debugger command, which is engine-added rather than retail. -- The localized-build comparison lane now covers the Japanese Windows-native executable too. New note `docs/jp-remorse-windows9x-investigation.md` records that `/ja/CRUSADER.EXE` is a PE-style Win32 image with native window creation, DirectDraw/DirectSound init, registry-backed config under `Software\Electronic Arts\Crusader: No Remorse\J1.21`, IME/DBCS-facing imports, and a `GetVersion`-driven Win9x compatibility branch that retries `TlsAlloc()` until the slot is above `2` when the classic Win9x version bit is set. Current best read is `real Windows 9x-native port with likely Win95 intent`, with runtime prerequisites still left to test. -- The removed-item lane is tighter now too. `docs/removed_items.md` now records a live `CRUSADER.EXE` close on the inventory/display name path: retail `1118:056A` is `DTable_GetNameForShapeNo`, `1118:05D5` is its `INVALID` fallback returning `1478:238C`, and `Weasel_OnPaint` uses that same lookup family. The backing `1478:22BC` `char *[41]` array preserves exact explosive names inline, including `CONCUSSION GRENADE`, `NERVE GAS GRENADE`, `EMP GRENADE`, `SPIDER BOMB`, `LAND MINE`, `BLAST PAC`, and `FUSION PAC`, while repeating `INVALID` at slots `0/14/26/32`. The reusable Remorse dump at `out/dtable_get_name_dump.json` plus companion CSVs now closes the direct-table question too: `0548` is not a named dtable entry, so its in-game `Invalid` label is best explained as a plain fallback for an unmapped shape. The same tooling now also closes the Regret comparison side: live `REGRET.EXE` uses helper `1130:056a`, its recovered segment-`1480` dtable island expands the slot layout to `52` names with repeated `INVALID` at `0/17/36/44`, preserves Regret-only names such as `BK-16`, `LNR-81`, `XP-5`, `IONIC SHIELD`, `PLASMA SHIELD`, `RADIATION SHIELD`, and `VIR IMAGER`, and resolves bomb rows `0343`, `0350`, `0560`, `039A`, and `039C` while notably omitting `BLAST PAC`. Current best read remains narrower than the first pass: the removed grenade variants are real retail dtable names plus map leftovers, `LANDMINE`/`BLASTPAC`/`FUSPAC` are active Remorse classes rather than new removed items, and `SPIDER BOMB` is currently stronger as a cross-game dtable/comparison signal than as a fully closed Remorse finding. -- The Japanese localized-build lane now also covers surviving cheat/debug and startup-argument behavior. New note `docs/jp-remorse-cheats-and-launch-params.md` records that the JP Win32 build still has a live `-laurie` special-case, a live `JASSICA16` cheat-state matcher, a still-executable immortality toggle path, and a working Win32 parser for `-debug`, `-u`, `-warp`, `-skill`, `-mapoff`, `-egg`, and `-demo`. The same pass also adds one important caveat relative to the older DOS-side docs: the JP Win32 parser is only directly closed for mission-only `-warp ` so far, not for positional `-warp `. -- The startup map-selection lane is now tighter across both retail games too: No Remorse still hardcodes `Teleporter_CreateProcessDirect(1, 0x1e, 1)` inside `Game_Start`, while No Regret keeps the same literal selector in two live places, the early `Game_Start` site at `1008:1448` and the later authoritative new-game hop in `Game_RunNewGameFlow` at `1030:05c5`. The separate `-warp mission` path still uses an executable-embedded word table plus `-mapoff`, and the repo docs now include the dedicated REGRET-side note `docs/regret-game-start.md`. Current best read remains `startup map choice in code, map contents in external FIXED.DAT resources`, not `mission-start map configured in CRUSADER.CFG`. -- That same warp-table lane is now exact across both retail DOS executables too. Byte checks against `CRUSADER.EXE` and `REGRET.EXE` now show matching 17-word `-warp mission` base-map tables (`0,1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,40`) at `1478:0488` and `1480:075c`, each followed by a `0,0` terminator. The public map renderer now also has a dedicated mission-table extractor and generated JSON cache, so scene metadata no longer has to treat mission/base-map usage as an unknown ownership question. -- That same startup lane is now tighter at the argument level too. Current best parser/control-flow read in `REGRET.EXE` is `-warp [x y z]`, with X/Y/Z carried as positional argv tokens after the mission number rather than as separate recovered switches. The corresponding runtime branch in `Game_RunNewGameFlow` is also clearer: nonnegative `-egg` overrides beat the coordinate path, while the real eggless-map workaround is `-warp ` plus `-mapoff` with `-egg` omitted so the game falls into direct `NPC_Teleport` instead of the teleporter-egg lookup. -- The matching No Remorse cross-check is now closed too. Live `CRUSADER.EXE` `HandleCommandlineArgs` at `1048:0adc` uses the same positional `-warp [x y z]` parser shape, and `Game_Start` at `1020:029e` / `1020:02d0` uses the same runtime precedence: direct coordinates only win when the egg override is still negative, otherwise the code falls back to `Teleporter_CreateProcessDirect`. The parameter-only eggless-map workaround is therefore shared across both retail games, not Regret-specific. -- The public map-renderer link lane is tighter again. Cross-game `0x01DB` support now covers both the earlier frame-`1` teleporter-light helpers and the remaining Regret map `3` frame-`0` telepad helper placements that carry destination ids `27/28` in `quality`. The same pass also adds the checked same-map `ELEVATOR` lane: frame-`0` `shape:542` sources now link to local teleport-destination eggs by verified `QLo` rules (`1..0x0f -> same egg id`, `0x10 -> egg 4`). A new Regret follow-up closes the remaining map-`3` egg-`102` gap too: Regret `shape:400` (`0x0190`) is a second `ELEVATOR` family, `ELEVATOR::gotHit` uses a generic same-map lane for `QLo >= 100 && < 0x00c8`, and map `3` contains concrete source `item:664:fixed:400:0:44030:9662:0` with `quality 614` (`QLo 102`) linking to destination egg `102`. A second Regret-only editor-object follow-up now also promotes `WATCHNS`, `WATCHEW`, `CRYOBOX`, `CRAZYEW`, `CRAZYNS`, `VIDEOBOX`, and the `SECRET_DOOR_POST` / `PRESSURE_BARRIER_*` target shapes into the viewer, including cautious local arrows for `WATCH* -> 0x0510` and `CRYOBOX -> 0x05DF/0x05E0`. -- The map-13 wall-jump follow-up is tighter too. The suspicious nearby start tile `fixed:4767` is now closed as `FFFLOOR` (`shape 0x0135` / `shape:309`) rather than as an editor/helper collision override: decoded reference data still marks it as ordinary `terrain` (`4 x 4 x 0`, `solid`, `fixed`, `land`), but the extracted EUSECODE corpus shows `FFFLOOR::gotHit/equip/unequip` as an environmental hazard/sensor lane that toggles nearby same-shape floors and pushes standing actors through `NPC.slot_2d` into the normal hit/damage path. The nearest same-level trigger companion in the retail cache is family-4 egg `fixed:4770`, which currently resolves to `CHANGER`, so the local map-13 setup now reads better as a small scripted floor/trigger cluster than as proof that the rare jump-through wall has an authored per-instance non-solid flag. -- That same map-13 trigger companion is tighter now too. The nearby family-4 egg `fixed:4770` is no longer just a subtype label: retail Remorse clearly uses `QLo 4 -> CHANGER`, and the extracted `CHANGER::hatch` body plus the local decoded scene now line up as a keyed roof-destruction trigger (`mapNum` egg id `37`, nearby roof tiles with `QLo 37`). Current safest local read is `FFFLOOR hazard tile plus CHANGER roof-removal cluster` on the same upper platform, still not a direct wall-solidity override. -- The editor-helper overlay lane is tighter too. A broader exported-scene sweep now shows that `BRO_BOOT` (`0x04FE`) really does form a repeatable local helper lane into nearby same-`QLo` `SPANEL` items, with concrete Remorse examples on maps `9`, `10`, `11`, `21`, `23`, `160`, and `246`, so the renderer now promotes `BRO_BOOT -> SPANEL` alongside the existing cmd-link, alarm, steam, door, and flame helper arrows. A later decompressed `.cache` pass tightens two Regret trigger families as well: `NPC_ONLY` (`0x0366`) and `CRUMORPH` (`0x0318`) now promote cautious local `-> 0x04B1` same-`QLo` arrows where authored matches exist, while `NPC_ONLY -> actor` and `DEATHBOX -> 0x04B1` still stay out of the overlay. The latest tooltip pass also upgrades `0x04B1` from a mostly structural decode to concrete operation notes: helper dispatch via nearby `0x0476`, direct target mutation, timed pulses through `TRIGGER.slot_22` / `DOOR.slot_21`, verified link rewrites, and a create-and-drop lane. -- The same Regret controller lane is now better separated into `static helper links` versus `runtime actor-key links`. Current best read is that the withheld actor-target arrows (`CRUMORPH`, `NPC_ONLY`, and the wider sibling family) depend on mutable actor field `0x63`, not on a stable DTABLE export: sampled Regret DTABLE rows still show `0x00` at record byte `0x63`, while recovered `TRIGGER.slot_29` / `slot_2B` can rewrite field `0x63` on nearby matched NPCs after startup. The newly identified sibling families using that same hidden actor-key mechanism are `WATCHNS` / `WATCHEW`, `THRMBCKN` / `THRMBCKE`, and `SURCAMNS` / `SURCAMEW`. Current viewer/tooling stance remains `metadata + cautious helper arrows only` until a runtime or spawn-time export can close actor field `0x63` directly. -- The skill-controller lane is tighter too. Shape `0x0120` is now closed as `FASTSKIL`, distinct from `SKILLBOX`: `enterFastArea` waits briefly, only runs while map-array is clear, uses frame `0/1` as difficulty thresholds for `TRIGGER.slot_20` lane `0` versus `1`, and uses frame `2` as an explicit `QLo/+1/+2` difficulty router. The renderer now exposes that decode in tooltips and adds conservative local `FASTSKIL -> 0x04B1` helper arrows, with frame-`2` variants for the recovered `QLo + 1` and `QLo + 2` lanes. -- The switch/pad clarification lane is tighter too. Shape `0x0080` now closes as `BOX_EW`, and sampled exported scenes are strong enough to promote a conservative `BOX_EW frame 0 -> nearby same-QLo 0x04B1` helper arrow rule. Shape `0x04CD` now closes as `TRIGPAD`, but its broader occupancy/elevator behavior and the negative scene sweep keep it metadata-only instead of promoting a generic cmd-link overlay. Shape `0x033A` now reads best as a tiny `NUMBERS` readout/display helper family clustered with nearby `0x0501/0x0502/0x0503/0x0505/0x0507` pieces, so it also stays label-only. -- The readable-usecode viewer lane is tighter too. New note `docs/map_renderer/trigger-usecode-links.md` records the evidence-backed class/event mapping now used for pinned controller tooltips: `BOX_EW`, `PANELNS`, `CARD_NS`, and `SPANEL` open `use`; `CRUMORPH`, `SKILLBOX`, `EVENT`, `ALARMHAT`, and `ALRMTRIG` open `equip`; `FASTSKIL` opens `enterFastArea`; `TRIGPAD` and `NPC_ONLY` open `gotHit`; and the `0x04B1` cmd helper jumps directly to `TRIGGER.slot_20`, the shared high-slot fan-out lane recovered from the extracted corpus and existing trigger notes. -- The command-line lane is tighter around `-u` now too. In live non-Japanese `CRUSADER.EXE`, the parser case at `1048:0a46` copies the following token into `1478:065a`, and the renamed `startup_apply_u_override_if_present` at `1420:0cdf` later consumes that buffer to resolve/load an alternate usecode/EUSECODE source into `1478:6611/6613`, mark `1478:6615`, and rebuild the cumulative slot-base words at `1478:8c7c..8c82`. Current best read is `real retail startup usecode override`, not `JP-only` and not `dead string-table residue`; the paired consequence is that the older CRUSADER-side `-setver` attribution should now be treated as reopened until its exact retail consumer is isolated directly. -- That same `-u` lane is now tighter at the runtime-scope level too. The follow-up note `docs/usecode-startup-override.md` now records that retail `-u` appears to replace the single live usecode root at `1478:6611/6613`, not add a sidecar table: `startup_apply_u_override_if_present` overwrites that root directly, rebuilds the cumulative slot-base words, and later consumers including `Usecode_ItemCallEvent`, `UsecodeProcess_CreateProcess`, `Interpreter_NextUsecodeOp`, and `Item_GetDamaged` all read the same replacement root. Current safest tooling implication is `runtime swap for the existing Crusader usecode VM`, which makes `-u` a potentially important future validation hook for round-tripped/custom usecode archives once the accepted source format is nailed down. -- The same `-u` lane is tighter at the token-shape level now too. Live `1420:0cdf` does not use the copied argv token as an arbitrary final filename; it treats `1478:065a` as the `Filespec_GetFullPath` path component while loading the fixed mutable filename template `eusecode.flx` from `1478:07a0` through `1478:06d6/06d8` and forcing the first byte to `'e'` before both the existence probe and the final load call. Current safest read is therefore `path/root override for standard EUSECODE archive naming`, not `free-form filename override`. The stock bootstrap side is also better scoped: `1478:6611/6613` starts zero in the live NE image and the only currently recovered explicit writer there is the `-u` helper, so the normal non-`-u` seed remains only cross-referenced through the verified raw-side VM bootstrap note rather than fully live-NE-closed. -- The same `-u` lane is tighter at the practical experiment level now too. The live helper shape plus direct bytes at `1478:079a` make the fixed filename template concrete as `eusecode.flx`, which materially weakens the older `maybe it accepts arbitrary archive filenames` hope. Current safest user-facing read is now `pass a directory/root to -u and place a complete replacement EUSECODE.FLX there`; failed forms like `-u USECODE/FLICTEST.FLX` and `-u FLICTEST.FLX` are best explained as path/root misuse rather than as evidence that the `-u` switch itself is dead. -- The same override lane now has a concrete live-NE constructor pair too. `1420:1499` is now renamed `entity_vm_runtime_create` and currently reads as a `0x1319`-byte runtime-object allocator that zeroes a `0x1300`-byte front region behaving like `0x80` stride-`0x26` slot/runtime records before storing an attached helper pointer at `+0x1315/+0x1317`. `1430:0000` is now renamed `entity_vm_runtime_owner_resource_create` and currently reads as the compact `0x14`-byte file-backed helper that opens the resolved `eusecode.flx` path, queries entry count through vtable `+0x04`, allocates a backing buffer at `+0x10/+0x12`, and materializes indexed owner/resource records through vtable `+0x0c`. Current safest implication is that `-u` swaps the live VM runtime object graph, not just a raw archive handle. -- The USECODE/VM owner/resource/runtime lane now has a workable partial model, a named sequencer entry, paired external file-family loader evidence, and supporting extraction/reporting tooling. -- The USECODE/VM tooling lane now also has a concrete near-term implementation path: a Pentagram-derived proof-of-concept parser can reuse opcode decoding while swapping in the locally verified owner-loaded class and slot arithmetic, with a hybrid Ghidra comment/bookmark import path instead of a premature custom processor module. -- The USECODE tooling lane now also has a first full readable corpus export: `tools/export_usecode_pseudocode.py` writes `977` current pseudocode bodies into `USECODE/EUSECODE_extracted/pseudocode`, and the first focused read of that corpus now shows `JELYHACK::use` / `JELYH2::use` as tiny shared `set_info(0x0207) -> process_exclude -> return` stubs rather than hidden active event cores. -- The USECODE tooling lane now also has two new follow-up notes grounded in the exported corpus: `docs/usecode-tool-improvement-plan.md` turns the Pentagram/`crusader-disasm` comparison into a concrete parser roadmap, and `docs/usecode-alarmhat-analysis.md` records the current best evidence-backed read of `ALARMHAT::equip` as a frame-driven local alarm-state controller that equips nearby `shape 0x04D0` helper objects in different modes. -- That same `0x04D0` lane is tighter again after the latest map-1/map-248 follow-up: decompressed cache evidence plus extracted `MONSTER.slot_0F`, `MONSTER.slot_0A`, and `ITEM.slot_2D` still show that frame-`0` / frame-`1` `0x04D0` pairs are authored on purpose and keyed locally by `Item.getQLo(...)`, but the old `frame 0 always equals the spawned NPC row` reading is no longer strong enough. Confirmed auto-enabled pairs now line up better if frame `0` is treated as the verified controller lane while frame `1` is treated as the practical visible-NPC cue. -- The public renderer follow-up now reflects that narrower stance: the `Monster Spawners` panel still lists `0x04D0` records directly, fixed-record spawners expose copyable stable ids, tooltip/list semantics distinguish the verified frame-0 control path from the current frame-1 practical-preview heuristic, and paired previews now render once per pair instead of twice. Current preview rule is `blue active carrier / red dormant controller`, with the tooltip keeping the explicit `☑/☒` state label and the scene overlay dropping the older on-map checkbox badge. Exported-usecode corroboration still reaches beyond `ALARMHAT` too: `ITEM.slot_2D`, `FUSPAC.slot_01`, and `MISS8.slot_20` all show nearby `0x04D0` scans keyed by frame and/or `Item.getQLo(...)`, which strengthens the low-quality-byte-as-local-signal-key model without promoting it into a universal object pointer. -- The USECODE tooling lane now also has a broader equipment-event note: `docs/usecode-equipment-system.md` records live binary proof that `Item_Equip` / `Item_Unequip` are real generic usecode event dispatchers gated by owner-row capability masks (`0x400` / `0x800`), and that the exported corpus currently contains `77` `equip` bodies plus `50` `unequip` bodies spread across actor, turret, alarm, conveyor, camera, and hazard classes. Current best read is `surviving Ultima-style event vocabulary generalized into activation/setup/state-change semantics`, not yet `fully proven paper-doll RPG gear subsystem`. -- The USECODE tooling lane now also has its first implemented readability follow-through from that improvement list: `tools/poc_crusader_usecode_parser.py` and `tools/export_usecode_pseudocode.py` now regenerate the full `977`-body corpus with one verified wrapper alias seed (`FREE.waitNTimerTicks` for `0A0C:0032`), class-name-aware target rendering (`FREE.slot_21`, `BLASTPAC.slot_20`, `TRIGGER.slot_20`, etc.), first-pass selector decoding that upgrades the simpler alarm/trigger `loopscr` runs into `for ... in nearby_items(shape=..., origin=...)` / `for ... in nearby_items(family=..., origin=...)` loops, and a second readable selector-family fallback that collapses raw `loopscr 0x42` runs into `selector_0x42(arg0=..., arg1=..., arg2=..., origin=...)` annotations or `for ... in selector_0x42(...)` loops where the control flow is simple enough. -- That same renderer lane is tighter again after the BLASTPAC follow-up: ScummVM `uc_machine.cpp` keeps the VM-side semantics anchored (`0x51` branch-on-false, `0x73` loopnext pushes a validity flag and frees exhausted search lists, `0x75/0x76` are real foreach iterators), and the map-viewer structurer now also treats jumps to the current region end label as structured exits. After regenerating the cache, `BLASTPAC.slot_01` no longer falls back to `block_0171` / `block_0415` style islands; the `shape 0x053A` scan is a real `for item in nearby_items(...)` loop and the later `target` / crouch lane is one nested `if/else` tree. The focused renderer regressions now cover both the synthetic region-end join case and the real `BLASTPAC.slot_01` body. -- The USECODE/VM lane now also has a verified generic masked-context creation hub (`000d:463a`) plus two concrete sequencer-internal consumer blocks (`000d:208b`, `000d:21ed`) built directly on `entity_vm_context_create_from_slot_index`. -- The USECODE/VM lane now also has first caller-role evidence outside the older seg021 wrapper island: the new seg004 callers keep masks `0x8000:0x0007` and `0x2000:0x0015` in gameplay-side materialization lanes, while the newly named seg006 helpers now separate one extra-word masked lane with a real local class-state transition fallback (`0x0008:0x0030`) from a guarded `0x0010:0x0008` materializer that simply returns `0` on miss after readiness checks. -- The USECODE/VM lane now also has a wider verified higher-slot wrapper ladder: the `0005` island reaches slot ordinals `0x10..0x14`, slot `0x12` is a zero-extra-word lane, slots `0x11/0x13/0x14` carry extra-word payloads, and the current safest read is `slot-stable payload-shape taxonomy` rather than direct event-name promotion. -- The same higher-slot batch now has its first outward binary anchors: slot `0x12` wrapper `0005:3171` is directly called at `0005:1776` and `0005:1945`, the slot `0x10` guarded lane at `0005:3115..3129` is still fenced by the `0005:30f2..3113` class-nibble-`4` check, and the dark slot `0x0a` / `0x0b` wrappers are now instruction-verified as exact signed-additive shims over masks `0x00000400` / `0x00000800` even though their outward callers remain unrecovered. -- The compiled-side immortality lane is slightly tighter too: `000b:b3b1` / `000b:b62c` are now a cheat-event listener constructor/handler pair for the shared cheat/control bundle rather than a hidden `0x410` producer, and the extractor-side `TELEPAD` slot-`0x20` `raw_code_offset = 0x00000410` hit is closed as an offset collision rather than direct immortality evidence. -- The compiled-side immortality lane is tighter again after the follow-up pass: `000c:8a62 -> 000c:8c56` is now a verified generic event-object dispatcher reading the emitted event id from field `+0x6`, seg109 helper `000b:3d2a` is now comment-backed as generic listener-registration infrastructure rather than an emitter, and the strongest remaining player-trigger family is the event-bearing `NPCTRIG` / `EVENT` neighborhood rather than `TRIGPAD`, `SPECIAL`, `REB_PAD`, or `TELEPAD`. -- The immortality lane is tighter again after the extractor extension: generated report `USECODE/EUSECODE_extracted/immortality_target_body_scan.md` now proves that `EVENT`, `NPCTRIG`, `COR_BOOT`, `REE_BOOT`, `SFXTRIG`, `SPECIAL`, and `TRIGPAD` bodies contain no inline little-endian `0x0410`, no dword `0x00000410`, and no byte-swapped `0x1004`; the best surviving frontier is now the monolithic `EVENT` slot `0x0a` body plus compact `NPCTRIG` slots `0x0a` / `0x20`. -- The immortality lane is tighter again after the structure pass: new report `USECODE/EUSECODE_extracted/immortality_body_structure.md` now shows `EVENT` slot `0x0a` as a broad hub clause stream (`90` internal `0x53 0x5c EVENT` subheaders, `383` local labels, wide `event/item/source/dest/door/counter/counter2/link/time/post1/post2/floor/flicMan` tail), while `NPCTRIG` stays compact (`5` subheaders for slot `0x0a`, `1` for slot `0x20`, with narrow `referent/event/item/item2` vs `referent/typeNpc/item/item2` tails). Current best surviving emitter frontier is therefore `NPCTRIG` slot `0x0a` with `NPCTRIG` slot `0x20` as its nearest typed/setup companion, while `EVENT` now reads more like the generic hub body behind the same active-event lane. -- The immortality lane is tighter again after the clause pass: new report `USECODE/EUSECODE_extracted/immortality_npctrig_clauses.md` now fixes the open-header decode (`NPCTRIG 0x0a` event-code byte `0x11`, `NPCTRIG 0x20` event-code byte `0x01`) and shows slot `0x0a` as a five-step fixed-width clause ladder (`0x2f` subheader stride, backward-walking `0x2f` targets, per-clause `branch_3f_0a` + `push_24_51` + `writeback_57_02` motifs) while slot `0x20` stays typeNpc-heavy (`10` `field_4b_fe_0f` hits, no `push_24_51`, no `writeback_57_02`). The best remaining descriptor-side frontier is therefore no longer the `NPCTRIG` pair symmetrically; it is specifically `NPCTRIG` slot `0x0a` as the live event-bearing ladder, with slot `0x20` as a typed/setup companion body. -- The immortality lane is tighter again after the runtime-fit follow-up: the regenerated clause report now records the per-clause motif offsets and the selector-family fit against `000d:21ed -> 000d:22bc`. `000d:5572` proves the extra word carried by `0005:2c35` is additive (`slot_value + offset`), `000d:21ed` now has an exact `A x B` matrix contract (byte A = lead-word row count, byte B = shared target-list width), and `NPCTRIG` slot `0x0a` is the only surviving compact body that exposes a natural five-row additive selector family (`0x0064/0x0093/0x00c2/0x00f1/0x0120`, uniform stride `0x2f`) instead of a one-clause typeNpc gate. -- The immortality caller-path follow-up tightened the runtime bridge again: MCP xrefs now show only three entries into `entity_vm_context_create_from_slot_index` (`000d:46ac`, `000d:208b`, `000d:21ed`), while `0005:2c35` itself still has no recovered code or data xrefs. Stack setup at `000d:208b` hardcodes the `000d:5572` additive word to `0`, which does not match the `NPCTRIG` slot `0x0a` clause-start or target families. The remaining live selector frontier is therefore the still-overlapped `000d:21ed` caller frame rather than a normal caller of `0005:2c35`. -- The immortality downstream-use follow-up weakens the remaining direct-selector hypothesis again: `000d:46ec` stores the dynamic word from the `000d:21ed` lane into context field `+0x34`, but `000d:21ed -> 000d:22bc` never rereads `+0x34` or `+0x32` after creation. The durable uses are the object save/load path instead: `000d:498f` serializes only the derived low word at `+0x10c`, `000d:4a78` reloads that saved word as the additive argument to `000d:5572`, and `000d:4c2d..4c4d` rebuilds `+0x10c/+0x10e` from the live slot value plus that saved offset. The only recovered post-load consumers are a tiny sentinel predicate (`FUN_0001_a772` checks for exactly `0000:0001`) and a normalization block (`FUN_0002_1860` clamps `0000:xxxx` values below `0x0080` up to `0x0080`). No recovered compare or dispatch branch matches the `NPCTRIG` slot `0x0a` clause-start or target families, so the direct derived-value fit is weaker again. -- The persisted-context contract is tighter again after the latest pass: `entity_vm_context_save` (`000d:498f`) serializes `+0x11f`, `+0x121`, `+0x10c`, `+0x34`, and the `0x80`-byte local buffer, while `entity_vm_context_load` (`000d:4a78`) rebuilds the frame pointers, replays `entity_vm_slot_load_value_plus_offset` from saved `(slot, additive_word)`, restores `+0x10c/+0x10e`, and refreshes owner-source pair `+0x117/+0x119`. That is stronger evidence for `post-selector persistence of derived value state` than for any hidden upstream class discriminator. -- The immortality upstream-source follow-up removes most of the caller-frame ambiguity. Direct program-memory bytes for `000d:2131..21ed` now show the hidden pre-call layout explicitly: the seeded `+0xd6/+0xd8` stream is consumed as `word slot_index`, `word add_a`, `word add_b`, `byte setup_len`, `byte inline_len`, and `000d:21d0` pushes `add_a + add_b` as the dynamic word later stored at context `+0x34`. The same window now proves the caller-side frame shape too: frame base is `caller + [caller+0xd4]`, `[frame+0x0a/+0x0c]` is the far pointer passed into `entity_vm_context_setup`, and `[frame+0x0e..]` is a separate inline tail blob copied after creation. That rules out runtime owner-table fields or raw caller-object fields as the immediate source of `+0x34` and reframes the open question one level earlier: where that frame-local far pointer is seeded from, and whether the summed stream pair still maps to `NPCTRIG` slot `0x0a` clause-base/delta structure or only to a more generic descriptor-relative offset pair. -- The immortality frame-producer follow-up narrows the upstream writer one step further. Raw bytes at `000c:fbf7..fc47` (`caseD_0`) now show the nearest non-overlapped producer reading one signed placement byte from the seeded `+0xd6/+0xd8` stream, popping a far-pointer dword from the caller stream at `[caller+0xcc/+0xce]`, computing `frame_base = caller + [caller+0xd4]`, and storing that dword at `[frame_base + placement + 0x4/+0x6]`. That means the `000d:21ed` source lane is immediately caller-stream-backed rather than owner-row-backed; if its consumed `[frame+0x0a/+0x0c]` pair comes from this family, the relevant placement byte is `0x0006`, and any surviving `NPCTRIG` linkage must already have been predecoded into the generic caller stream before the frame record is materialized. -- The next producer-path pass tightens that split again. `000d:46ec -> 000c:f844 -> 000c:f6e8` now shows that a new context's `+0xcc/+0xce` stream is seeded by copying a caller-supplied setup blob into the object-local buffer, while the slot/additive record from `entity_vm_slot_load_value_plus_offset` seeds the separate `+0xd6/+0xd8` lane and the owner-table row `(+0x10/+0x12) + 0x0d*slot + 4` is mirrored separately through `0x39ca`. Linear raw-byte recovery across `000c:f98b..000d:000d` also closes the forward/reverse frame-record family around that lane: `000c:fc4b..fcbb` is the caller-stream -> frame blob producer that best matches inline-tail placement `0x000a`, while `000c:ff1f..ff83` is the frame -> caller-stream dword copier matching the `000c:fbf7..fc47` far-pointer writer at placement `0x0006`. The surviving open question is therefore narrower again: not which generic parent-frame materializer exists, but where the first non-recursive decoder originates the setup far pointer before this `ff1f/ff9f -> fbf7/fc4b -> 000d:21ed` propagation chain repeats it, and whether that origin still maps specifically to `NPCTRIG` slot `0x0a` or to a broader predecoded VM workspace. -- The next immortality pass closes the immediate far-pointer source classification too. Hidden raw bytes at `000c:fa2f..fa5b` recover an inner opcode dispatcher on the seeded `+0xd6/+0xd8` lane, and the same case family now exposes non-recursive caller-stream seeders at `000c:fd51`, `000c:fd91`, `000c:fdd1`, and `000c:fe11`. The dword case at `000c:fe11..fe59` reads an inline dword literal from that control stream, subtracts `4` from `[caller+0xcc]`, and writes the literal dword onto the caller stream before the recursive `ff1f/fbf7` replay family touches it. That means the immediate compiled-side source for the `000d:21ed` setup far pointer is now an inline VM control-stream literal, not an owner-row lookup or generic scratch buffer; any surviving `NPCTRIG` tie has to explain how slot `0x0a` is decoded into that literal-bearing stream upstream, while slot `0x20` still reads as the typed/setup companion body. -- The next immortality pass separates that literal-bearing stream from the owner-row path cleanly enough to retune the working model. Instruction recovery at `000d:46ec` now shows the owner-table row `(+0x10/+0x12) + 0x0d*slot + 4` feeding only the separate `0x39ca[slot]` mirror, while the live `+0xd6/+0xd8` control stream passed into `entity_vm_context_setup` continues to come from `entity_vm_slot_load_value_plus_offset`. The hidden `000d:21ed` pre-call span is now explicit as `word slot_index`, `word add_a`, `word add_b`, `byte setup_len`, `byte inline_len`, and the `000c:fa2f` case family now separates immediate literal seeders (`000c:fd51` byte, `000c:fd91` sign-extended byte->word, `000c:fdd1` word, `000c:fe11` dword) from the recursive replay stages (`000c:ff1f`, `000c:ff9f`). Current best read is therefore `decoded per-slot VM workspace plus frame replay`, not `direct NPCTRIG clause stream`, even though `NPCTRIG` slot `0x0a` remains the strongest surviving upstream descriptor family and slot `0x20` still reads as the typed/setup companion. -- The next immortality pass closes the workspace-materialization side of that boundary too. `entity_vm_slot_load_value` (`000d:51fd`) is now instruction-verified as the first concrete writer of the later `+0xd6/+0xd8` buffer on a cache miss: `000d:5066` loads a slot header plus cached `6`-byte subentry table through the owner-resource wrapper `000d:714c`, and `000d:5305..53d4` then reads the selected subentry's byte range directly into a newly allocated value-object buffer at `+0x0a/+0x0c`, which `000d:51fd` returns as the live far pair. That means the immediate workspace is file-backed owner-loaded slot data copied into memory before `000c:fa2f` interprets it. The remaining open question is no longer who first materializes the buffer at all, but whether the loaded slot family can be tied specifically to `NPCTRIG` slot `0x0a` or only to the broader owner-loaded descriptor workspace, with slot `0x20` still the best typed/setup companion. -- The next immortality pass closes the header/range-arithmetic blocker itself. The owner-resource callbacks operate on `class_id + 2`, which matches extracted `object_index` exactly; the first class-header dword is now constrained as the extra-slot count beyond a fixed `0x20` base table; bytes `8..11` remain the first code-byte offset; and `000d:53b4` reads body windows using the same `(word len, dword raw_code_offset, code_base)` arithmetic emitted by the extractor. `NPCTRIG` therefore now has exact owner-loaded body windows in the live runtime format: slot `0x0a` = `0x00da..0x024e` (`373` bytes) and slot `0x20` = `0x024f..0x03a7` (`345` bytes), while `EVENT` slot `0x0a` likewise fits `0x00d4..0x20a9`. The remaining immortality uncertainty is no longer range translation but upstream class selection into that now-verified loader path. -- The selector-side follow-up tightens that last uncertainty without closing it. `entity_vm_slot_index_from_entity` (`000d:45c5`) is now instruction-verified as a three-way category mapper only: `(1)` entity-id lane `1..255` with class bit `0x0002` clear -> `entity_id + 0x8c7e`, `(2)` class-nibble `4` lane -> `class_byte_0x7e05 + 0x8c80`, `(3)` fallback type lane -> `type_word_0x7df9 + 0x8c7c`. `entity_vm_runtime_init_from_path_if_configured` seeds those bases cumulatively from `0x6608..0x660e`, and direct caller `0005:295f` independently reuses the same slot index to test owner-row bit `0x0040`. That strengthens the read that the compiled side sees category spans plus generic row-capability masks, not a hard `NPCTRIG` / `EVENT` class-family discriminator, before the owner-loaded slot body is decoded. -- The first focused NE `CRUSADER.EXE` hole-filling pass tightens that same wall one step further without breaking it. In the live NE session, `0005:295f` is now confirmed as the only recovered non-hub consumer of `entity_vm_slot_index_from_entity`, and its only currently recovered callers are `0006:43c3`, `0006:c5f0`, and `0007:3584`. That gives the selector lane three concrete gameplay-side caller families to classify next, while `0005:2c35` remains outward-xref-dark and therefore still does not prove a class-family choice by itself. -- The next focused NE pass closes the first of those caller families structurally. Repaired wrapper `0006:4379` is now a verified seg031 dispatch-entry subtype gate over objects created by `0006:42d9` with event type `0x236`, source type `8`, subtype/tag at `+0x3c`, payload/source far pointer at `+0x32`, and aux words at `+0x36/+0x38`. Within that family, subtype `0x20c` at `0006:43c3` routes into `0005:295f`, while sibling subtype `0x20b` at `0006:43e5` routes into `0005:2918` using the same aux pair. That localizes the owner-row bit-`0x0040` consumer to one subtype-tagged dispatch-entry family, but still does not identify the upstream owner-loaded class family. -- The first doc-to-live-NE integration batch is now applied in the open `CRUSADER.EXE` database too. Comment-backed anchors landed on the live selector/core pair `1420:0dc5` / `1420:0e3a`, the consumer pair `10a0:2718` / `10a0:275f`, and the first closed caller-family runner `10f0:02d9` / `10f0:0379`, with branch comments at `10f0:03c3` and `10f0:03e5` preserving the verified `0x20c -> 10a0:275f` and `0x20b -> 10a0:2718` split. This improves the live NE handoff without justifying a headline progress-estimate change yet. -- The next live-NE caller-family pass closes the remaining direct `0005:295f` callers too. Old `0006:c5f0` now lands at `1128:0ff0` inside `Item_ReceiveHit`, where the non-NPC damage path probes `Item_GetDamaged` with hitter sentinel `0x4000`, packed `(damagetype << 8) | damage_lo`, and a local flag-out byte; old `0007:3584` now lands at `1138:1384` inside `SuperSprite_HitAndFinish`, where the non-NPC collision lane probes the same helper with packed `(firetype << 8) | damage` before optionally falling through to local `Item_ReceiveHit` knockback logic. Live comments now anchor both sites, so the selector frontier has moved upstream again to an earlier subtype/class-family producer rather than another direct caller search. -- The compiled cheat/control lane is now split more cleanly. `cheat_code_check` (`0007:0d0a`) is still the sole hidden cheat-sequence matcher (5-byte table via `DS:0x2833`, index `DS:0x283d`), and it toggles `DS:0x844` (`cheats_enabled`) plus mirror `DS:0x6045`, then emits event `0x103`. The matcher bytes themselves are now rechecked in the live NE image as scan codes `24 1e 1f 1f 17 2e 1e 02 07` = `j a s s i c a 1 6`, with the trailing digits specifically using top-row scan codes `0x02` / `0x07`. Live data-use recovery also tightens the latch story: `0x6045` is written only by `Key_CheckCheatToggle` (`1130:2b72`) and the event-`0x7e` runtime toggle at `13e8:203d`. The live NE F10 proof is stronger than the earlier folklore-level read: inside `Key_HandleOptionKeys` (`1130:0896`), the F10 cheat branch first checks `DAT_1478_085f`, then `0x6045`, then reaches `1130:0afd` and calls helper `11c8:01a8`; the `11c8:018a` helper call in the same function appears later at `1130:0cad`, in a different branch. The helper identity is now closed from the code too: `KeyboardGetExtendedShiftStates` (`11d0:39e6`) uses BIOS `INT 16h, AH=12h`, whose AH bits are `0=left Ctrl`, `1=left Alt`, `2=right Ctrl`, `3=right Alt`, so `11c8:01a8` testing `0x0100|0x0400` is really `KeyEvent_IsCtrlDown`, and `11c8:018a` testing `0x0200|0x0800` is really `KeyEvent_IsAltDown`. Upstream keyboard-path recovery also closes the practical behavior too: the held-key repeat builder at `11b8:0129..022b` samples BIOS extended-shift state through `11d0:39e6`, stores the current `31a4` modifier snapshot into each repeated `KeyEvent`, and queues that event through `11d0:3533`, so holding `F10` first and then pressing physical `Ctrl` lets later repeated F10 events reach the immortality branch with refreshed modifier bits. The same repeated F10 event synthesis plus missing debounce explains the multi-modal on/off spam. The F10 immortality sub-branch also only runs for a live current NPC (`NPC_IsDead` gate at `10e8:1fed`). `DAT_1478_085f` is now tighter too: it is set during `Game_Start` (`1020:0127`), cleared at the end of `ComputerGump_CreateGump` (`1398:01f5`), and restored by `ComputerGump_CloseAndResumeGameplay` (`1398:0212`) during the paired computer-gump teardown path before falling into generic gump cleanup. Current safest read is a broader gameplay-input / option-key-active state rather than any cheat-state bit. Separately, event `0x410` at `000c:9703` does **not** toggle immortality; it boolean-toggles `DS:0x604f` / `g_cdTransferDisplayActive` and posts the `CD TRANSFER DISPLAY ACTIVE/INACTIVE` notifications under the broader `0x844` gate, which matches both the user's runtime observation and the old `crusader-disasm` note `CTRL-Q = 0x410`. The older `DS:0x6050` lane at `immortality_activate` (`000c:8231`) remains a separate secondary entity/process path. The older seg109 hidden-menu label is now narrowed further: in the live NE database, `000b:9a86`, `000b:9c0d`, `000b:b3b1`, `000b:b62c`, `000b:15ac`, `000b:0b52`, `000b:0b06`, and `000b:2882` now read more defensibly as `usecode_debugger_*` helpers, with menu labels like `Open Unit`, `View File`, `Watch`, `Inspect`, `Find`, and `Break to TDP`. Current best read is a hidden usecode debugger / unit inspector, not a retail scrollable cheat list. This also tightens the `-laurie` split: `-laurie` enables `0x844`-gated event cheats and debugger-side paths, but not the low-level `0x6045` keyboard latch, which matches the observed `F`-overlay-on / `F10`-refill-off behavior. Renamed in this area: `FUN_000c_8231` -> `immortality_activate`, `FUN_000c_834a` -> `immortality_conditional_activate`, `FUN_000c_8486` -> `immortality_activate_and_reset`, `FUN_000c_743f` -> `immortality_entity_process_create`, `FUN_000b_9a86` -> `usecode_debugger_open_for_current_unit`, `FUN_000b_9c0d` -> `usecode_debugger_open_modal`, `FUN_000b_b3b1` -> `usecode_debugger_gump_create`, `FUN_000b_b62c` -> `usecode_debugger_handle_event`, `FUN_000b_15ac` -> `usecode_debugger_load_unit_file`, `FUN_000b_0b52` -> `usecode_debugger_center_on_line`, `FUN_000b_0b06` -> `usecode_debugger_set_line_selection`, `FUN_000b_2882` -> `usecode_debugger_build_menubar`, `FUN_1398_0212` -> `ComputerGump_CloseAndResumeGameplay`. -- The same cheat/control lane is now a little cleaner at the user-facing hotkey level too. A focused live NE pass closed three folklore items: `~` is a real runtime cheat-latch toggle at `13e8:203d` under the broader `0x844` gate; the online `Ctrl+C = show current location` claim is wrong for this build and is really `Ctrl+L`, whose popup branch formats `1478:610c` at `13e8:255e`; and the missing third overlay is not bogus after all, because a separate `Ctrl+F7` branch at `13e8:1a20` toggles `1478:0ee0` while the other two F7-family toggles write `1478:2bc9` and `1478:2bca`. -- The follow-up pass closes the `~` versus `jassica16` confusion more tightly too. `jassica16` is the earlier raw scan-code matcher that toggles both `1478:0844` and `1478:6045`, sets `1478:8c52`, and can therefore bootstrap the whole cheat state from cold; `~` is only the later translated logical-`0x7e` branch, so Shift is the expected normal gesture on a US layout and that hotkey can only flip `1478:6045` after `1478:0844` is already enabled. The same pass also improves the third-overlay classification: `Ctrl+F7` is not a third generic camera grid but an `EggHatcherProcess` trigger-range overlay, which can legitimately appear blank on maps without eligible live egg/hatcher processes. -- The next cheat/overlay refinement pass is now folded back into the docs too. `docs/ne-segment1.md` now has a consolidated live-NE cheat/debug key matrix, the practical `-laurie` plus `Shift+~` bootstrap recipe for full keyboard cheats, and a fuller egg-hatcher note grounded in `EggHatcher_CreateProcess` / `EggHatcherProcess_Run`: non-monster egg families are enter/leave trigger items with X/Y/Z range checks, while `Ctrl+F7` visualizes the live egg-hatcher ranges and `Alt+F7` visualizes the related snap-process egg list. -- The `0x85f` reader side is now clearer too. The live NE database now names the paired `13e8` transition wrappers as `Game_DisableGameplayInputAndRefreshCamera` (`13e8:0e7d`) and `Game_RestoreGameplayInputAndClearModalState` (`13e8:0ef9`), which matches their concrete behavior: `13e8:0e7d` clears the controller/key-input latch `1478:27cb`, raises the modal overlay-suppression state at `1478:2c64` / `1478:8c53`, preserves `1478:8c54` from `1478:2d24`, and refreshes camera state; `13e8:0ef9` performs the inverse restore path and clears the secondary `1478:6050` latch. The Laurie-only wrapper side is clearer as well: `Game_ShowLaurieHintComputerGump` (`13e8:0e31`) is the hidden `-laurie` computer-gump hint path, while `Game_ShowLaurieHintIfGameplayInputActive` (`13e8:0f4a`) only calls it when `0x85f` is high. The main camera pass consuming the same gate is now `Camera_RedrawViewportAndGameplayOverlays` (`1180:19c1`), with comment-backed `1188:010f` / `1188:0394` overlay helpers bracketing the viewport redraw. -- The next blocker layer is narrower too. Those modal wrappers are not abstract helpers; inside `World_HandleKeyboardInput_13e8_14b4` they already wrap concrete user-facing lanes including exit-to-DOS confirmation (`0x22d`), quick save (`0x13f`), quick load (`0x13e`), restart/main-menu handling (`Game_RestartMaybe`), and the neighboring load/menu gump lanes. Separately, event `0x7e` remains the only other recovered writer of `0x6045` besides `Key_CheckCheatToggle`, so a successful `jassica16` match can still be undone later by that independent runtime path. `Key_CheckCheatToggle` itself is now comment-backed as keydown-only and still requires top-row `1` / `6` scan codes at the tail, leaving keypad digits and other non-matching input routes as a still-live explanation for failed tests. -- Cross-game verification against the currently opened `REGRET.EXE` now has a runtime correction too. The F10 branch at `1148:0d0e` still reaches the same modifier helper at `11e0:01a8`, and live testing shows the practical gesture is hold `F10` first and then press `Ctrl`, not `Alt`. The same BIOS-backed helper swap should be verified directly in that target before promoting renames there. The same runtime test also explains the repeated immortality popups: the F10 branch is not debounced, so holding the keys lets repeated F10 keydown events flip immortality on and off multiple times. The real gameplay difference remains the latch code: `1148:34d2` (`Key_CheckSecretCodeSequences`) still contains a `jassica16` table at `1480:2ff0`, but the latch-enabling sequence in No Regret is the second table at `1480:2ffc`, decoded as `loosecannon`, which toggles `1480:0ac0` and mirrors the result into the F10 latch byte `1480:009b`. -- Retail hidden-menu patching remains open, but the failed branches are now better separated from the current writable candidate. Verified file/fixup anchors are `0007:0d75` / `0007:0d79` (file `0x70d75` / relocation entry `0x71d68`) and `000c:99dd` / `000c:99e0` (file `0xc99dd`, seg126 chain `0x25e0`). The deferred `0x42f -> 000c:99dd -> 000b:9c0d` design remains explicitly rejected: it visibly entered the hidden UI path, but it halted with the retail `FILE\FLEX.C, line 83` failure and dropped into the quit line, so `0x42f` is the wrong deferred context even though the modal wrapper address itself was valid. The newer direct `0007:0d79 -> 000b:9a86` current-slot retarget with the narrowed `000b:9a8d` arg patch was also runtime-tested and produced no hidden menu, so the writable `/Writable/CRUSADER-PATCHED.EXE` test build is now moved to the next defensible variant instead: restore the direct hook to `000a:5276`, keep the current-slot wrapper unpatched, and retarget the later controller-side `000c:99e0` call to `000b:9c0d` while zeroing only the inherited modal-wrapper words at `000b:9c4a`. -- The next retail test build is narrower still. User runtime feedback on the first `Ctrl+Q` patch is: the mouse pointer appears, then gameplay hangs with only a single right-edge pixel still updating. That makes the remaining failure look more like post-entry control-flow fallout than a bad entry address. The PowerShell patch therefore now also rewrites raw `000c:99e8` / live `13e8:25e8` from `PUSH 0x3e8` to a near jump into the shared epilogue at `13e8:29a7`, so the reused `13e8:25dd` deferred lane exits immediately after the retargeted `13e8:25e0 -> 13a0:020d` call instead of falling through into the original `0x42f` branch tail logic. -- DOSBox-X debugger capture now shows the same hang surviving that tail-skip patch, and the stop point is materially informative: the live runtime state matches seg131 `Interpreter_NextUsecodeOp` at `1418:04c3..051d`, specifically just before the `1408:02f5` call at `1418:051d`. That means the blunt `Ctrl+Q -> 13a0:020d` patch is not merely stuck in the seg109 modal wrapper; it has activated the interpreter-side debugger-state path guarded by non-null `0x659c/0x659e`, and the freeze now looks like a bad or incomplete seg1408 debugger-state lifecycle rather than a simple wrong branch tail. Current best implication: stop iterating on the blunt modal force-open patch and pivot the next patch design toward constructing or safely emulating a real `usecode_debugger_break_state_create` object at `1408:0000` before relying on the seg109 UI lane. -- The next executable patch still follows that pivot, but the boot-time callback rewrite is now explicitly retired. The current PowerShell build repurposes the gated `0x410` body at `13e8:230d` to lazily construct a seg1408 state object through the existing far-call slot at `13e8:2352 -> 1408:0000`, stores the returned far pointer into `0x659c/0x659e`, and then reuses the **second** existing far-call slot in that same body (`13e8:235c`) to jump directly to `usecode_debugger_open_for_current_unit` at `13a0:0086` with zeroed wrapper arguments. This keeps the patch hotkey-local instead of rewriting the shared seg1408 callback table at `1478:65ab`, while the older direct and deferred modal-force-open sites remain restored. -- The callback-table design is now negative evidence rather than the live candidate. Even after fixing an NE-segment indexing mistake (`1478:65ab` had first been retargeted to segment `109` instead of `117` for `13a0:0086`), the global callback rewrite still caused startup failure. The surviving script fixes from that pass remain important: the large `13e8:230d` body must use on-disk `FF FF 00 00` placeholders rather than disassembly-resolved far operands, and its patched byte array must include the final trailing `0xC7` so patch/restore verification matches the retail executable length. With the global callback rewrite removed and the second local call slot retargeted instead, the script now round-trips cleanly on a fresh copied retail EXE (`apply -> patched`, `restore -> original`) and also cleans up the stale old `1478:65ab` callback retarget if that earlier crashing build had already been applied. -- The direct hotkey-to-wrapper retarget is now negative evidence too. The local-call redesign fixed startup and let the game reach gameplay, but pressing `Ctrl+Q` immediately quit through the normal `"No pity. No mercy. No remorse."` shutdown line, which is more consistent with entering the modal UI while the original keypress is still live than with a boot-time relocation problem. The next patch therefore keeps the hotkey-local object creation but stops calling `13a0:0086` on the keypress itself. -- The live candidate is now a per-object callback redirect. The `0x410` body at `13e8:230d` still creates/stores the seg1408 debugger-state object at `0x659c/0x659e`, but the second existing far-call slot in that body (`13e8:235c`) is now retargeted to `1408:0419` (`usecode_debugger_enable_single_step`) instead of directly opening the UI. The created object's first word is rewritten from the shared callback-table offset `0x65ab` to the private relocated slot `0x65af`, and the private dword at `1478:65af` is retargeted from `1408:0474` to `13a0:0086`. That should let the *next* interpreter-side debugger callback open the current-unit UI without inheriting the live `Ctrl+Q` key event, while the original shared `1478:65ab` slot stays restored to the retail no-op. -- The PowerShell patcher now round-trips cleanly for this per-object callback design on a fresh copied retail EXE too: `13e8:230d` body patched/restored, `13e8:235c` step-arm call patched/restored, private callback slot `1478:65af` patched/restored, and legacy shared callback slot `1478:65ab` held at original in both states. -- User runtime on that per-object single-step variant is now also informative negative evidence: the game boots and reaches gameplay, but pressing `Ctrl+Q` produces no visible effect at all, not even the original CD-transfer toast, which implies the hotkey body is being intercepted but the deferred break still is not surfacing. Current best read is that the single-step path at `+0x75` remains gated by the seg1418 nesting counters `+0x76/+0x78` often enough that the callback never fires in the observed test path. -- The live patch candidate therefore now sets **break-next** mode directly instead of single-step mode. The repurposed `13e8:230d` body still constructs/stores the seg1408 debugger-state object and repoints that object to the private callback slot `1478:65af -> 13a0:0086`, but it now writes `+0x75 = 0` and `+0x74 = 1` in the object itself rather than retargeting the second `13e8:235c` call slot to `1408:0419`. That matches the surviving UI-side control path at `13a0:1e5d`, where `+0x74` is the unconditional break-on-next-callback mode while `+0x75` is the nesting-sensitive single-step mode. -- The PowerShell patcher also round-trips cleanly for this break-next design on a fresh copied retail EXE: `13e8:230d` body patched/restored, private callback slot `1478:65af` patched/restored, shared callback slot `1478:65ab` held at original, and the stale second-call-slot cleanup removed from the write path because those bytes now belong to the patched body itself. -- User runtime on that `1478:65af` break-next variant is now negative evidence as well: the game crashes on launch again, so even the supposedly private `65af` slot now looks too globally visible to repurpose. Current best implication is that the object-local `0x65af` first-word rewrite can stay as the arm point, but the actual callback entry must move off the live callback-table dword itself. -- The live candidate is therefore now a **guarded trampoline** at the original seg1408 no-op callback code. The PowerShell patcher keeps the `13e8:230d` break-next object creation/store path, but restores the shared `1478:65ab` slot, stops repointing `1478:65af` to `13a0:0086`, patches `1408:0474` into a tiny guard that returns immediately unless `0x659c/0x659e` is armed, and uses the apparently unused relocated dword at `1478:6597` as the far target slot for `13a0:0086`. This newer `6597`/`1408:0474` build now also round-trips cleanly on a fresh copied retail EXE: `13e8:230d` body patched/restored, guarded callback code at file `0xCEE6F` patched/restored, wrapper target slot at file `0xEA197` patched/restored, and all older direct/deferred experiment sites held at original bytes. -- The root-cause read on the `65af` startup failures is now sharper: `0x65af` is not an alternate vtable base at all. The constructor at `1408:0000` writes `0x65ab` to object offset `+0`, and the dispatch sites prove that object `CALLF [BX]` uses the dword at `65ab -> 1408:046f` while object `CALLF [BX+4]` uses the next dword at `65af -> 1408:0474`. Rewriting the object first word to `0x65af` therefore corrupts the second method lookup (`[BX+4]`) instead of selecting a “private callback table”, which explains the launch-time instability and the other inconsistent runtime fallout from the earlier single-step and break-next builds. -- The live candidate is now the narrower **method-0 deferred callback** design. The PowerShell patcher still keeps the `13e8:230d` lazy object creation/store path and still arms break-next mode by writing `+0x75 = 0` / `+0x74 = 1`, but it explicitly preserves the object's vtable base as `0x65ab`, restores the method-1 helper at `1408:0474`, patches only the method-0 break callback at `1408:046f` to indirect through the spare relocated dword `1478:6597`, and uses that slot as the far target for `13a0:0086`. This corrected `046f`/`6597` build also round-trips cleanly on a fresh copied retail EXE: `13e8:230d` body patched/restored, break callback code at file `0xCEE6F` patched/restored, deferred target slot at file `0xEA197` patched/restored, and all older direct/deferred experiment sites held at original bytes. -- User runtime on that shared-`046f` method-0 build is now negative evidence too: startup still crashes, which makes the shared method body just as globally sensitive as the shared `65ab/65af` vtable slots. Current implication: the deferred path still looks right, but the callback implementation must move to a truly private per-object table instead of any shared seg1408 body or shared vtable dword. -- The live candidate is now a **private two-entry vtable** built from unused relocated dwords with no current data uses. The PowerShell patcher still keeps the `13e8:230d` lazy object creation/store path and still arms break-next mode with `+0x75 = 0` / `+0x74 = 1`, but it now rewrites the created object's vtable base to `0x658f`, retargets private method 0 `1478:658f -> 13a0:0086`, retargets private method 1 `1478:6593 -> 1408:0474`, and leaves the shared callback bodies and shared `65ab/65af` table entries untouched. This private-vtable build also round-trips cleanly on a fresh copied retail EXE: `13e8:230d` body patched/restored, private method 0 slot at file `0xEA18F` patched/restored, private method 1 slot at file `0xEA193` patched/restored, and all older direct/deferred experiment sites held at original bytes. -- User runtime on that first private-vtable placement is now negative evidence too: startup still crashes, which proves the `658f/6593` pair is also startup-visible despite the lack of direct data-use hits. Current best implication is that the private-vtable strategy itself still looks structurally right, but the specific dword pair must move farther away from the debugger-global cluster and any hidden boot-time consumers. -- The full six-candidate private-vtable harness is now retired. User runtime results: - - Candidate A (`1478:6724/6728`) = DOSBox closes on start - - Candidate B (`1478:672c/6730`) = fatal `Load program failed -- error code 201 -- C:\CRUSADER.EXE` - - Candidate C (`1478:6734/6738`) = DOSBox closes on start - - Candidate D (`1478:6718/671c`) = startup crash - - Candidate E (`1478:6720/6724`) = startup crash - - Candidate F (`1478:6738/673c`) = startup crash - Ghidra follow-up now explains why: `1478:6718..673c` is a live function-pointer table containing `UsecodeProcess_*`, `Process_Terminate`, `Process_Fail`, and nearby null handlers, not spare relocated dwords. The script no longer offers those candidates. -- The first guarded shared-callback pair is now negative evidence too. Candidates G/H still crashed on startup, and the best new explanation is structural: that design overwrote both `1408:046f` and the adjacent `1408:0474`, but `0474` is a real helper that returns `DX:AX = 0`, not dead padding. Destroying that zero-return behavior may itself be enough to destabilize startup. -- The method-0-only shared-callback pair is now negative evidence too. User runtime on Patch 1 / Patch 2 showed both startup-crashing, which means preserving `1408:0474` was necessary but not sufficient: the shared `1408:046f` body is still too broad if it jumps straight into debugger UI code. -- The live patch family is now an **interpreter callsite retarget** design. Candidate M/N are retired negative evidence: both startup-crashed, the embedded private stub inside the patched `13e8:230d` body was malformed, and the supposed deferred target slot at `1478:6597` is no longer treated as spare storage. The current PowerShell build still keeps the retail debugger object's real `1478:65ab` vtable base and still arms break-next through the patched `13e8:230d` body, but it now avoids both the shared seg1408 callback bodies and the `1478:6597` data slot entirely. Instead, it patches the existing interpreter `CALLF usecode_debugger_maybe_break_on_current_line` at `1418:04b5` to a corrected private stub at `13e8:232d`, and it also reuses the second retail far-call slot inside `13e8:230d` (`13e8:235c`) as the actual private UI-call target. The `13e8:230d` body itself now correctly handles both cases: reuse and arm an existing debugger-state object at `0x659c/659e`, or lazily create/store one before arming break-next. One implementation bug from the first O/P refactor is now fixed too: the second `13e8:235c` relocation write is part of candidate application and verification, so the live build now really routes to the selected wrapper instead of accidentally leaving that slot on retail `Dispatch_ModalGump`. Current candidates: - - Candidate O = interpreter callsite retarget -> `13a0:020d`, with `13a0:024a` zeroed inherited modal-wrapper words - - Candidate P = interpreter callsite retarget -> `13a0:0086`, with `13a0:008f` zeroed inherited current-unit-wrapper words - Both apply/restore cleanly on a disposable retail copy and are the next runtime tests. -- Fresh live-Ghidra re-checks now tighten the lower bound on this patch family too. Retail still shows no recovered writer that seeds `1478:659c/659e`, the constructor at `1408:0000` still only returns the debugger object and seeds the inert shared callbacks `1478:65ab -> 1408:046f` / `1478:65af -> 1408:0474`, and the interpreter pre-call guard at `1418:049e..04b5` still only checks for a non-null debugger pointer before handing off to `1408:0053`. Current best implication: there is still no evidence-backed one-site direct jump that can safely load the hidden debugger. Candidate O/P are now the smallest structurally defensible executable patches because they are the first design that preserves all four required pieces together: object bootstrap, global pointer seeding, one-shot deferred entry, and sanitized wrapper arguments. -- Full chronology for this patch line now lives in `docs/retail-debugger-patch-attempts.md`, including the failed global callback rewrite, direct wrapper call, single-step `65af` build, break-next `65af` build, guarded `0474` trampoline, shared `046f` method patch, and the current private-vtable candidate. -- The hidden-menu orphan model is now materially stronger too. New live renames in seg1408 (`usecode_debugger_break_state_create`, `usecode_debugger_maybe_break_on_current_line`, `usecode_debugger_breakpoint_insert_sorted`, `usecode_debugger_has_breakpoint`, `usecode_debugger_callstack_push_entry`, `usecode_debugger_callstack_pop_entry`, `usecode_debugger_enable_single_step`, `usecode_debugger_clear_step_state`, `usecode_debugger_current_entry_get_unit_name`) line up with the seg109 UI in a way the cheat-only hook never did. The concrete interpreter-side handoff at `1418:04aa..04b5` now calls `usecode_debugger_maybe_break_on_current_line` whenever the far pointer at `0x659c/0x659e` is non-null, and that helper checks `(file,line)` breakpoints before callbacking through the debugger-state object's vtable. Current best read is therefore that the retail orphan happened one layer earlier than the cheat/event experiments: the seg109 current-unit debugger UI likely used to be entered from this seg1408 breakpoint object, but retail no longer appears to instantiate/store that object at `0x659c/0x659e`. That makes the breakpoint callback lane a stronger original-entry candidate than direct event `0x103` retargeting. -- The follow-up doc reconciliation is now closed too. `docs/ne-segment1.md` no longer presents the seg109/raw-reference UI addresses (`000b:*`) and the live seg1408 breakpoint-state addresses (`1408:*`) as if they were competing versions of one table; it now uses one combined component map that makes the layering explicit and preserves the interpreter callback at `1418:04aa..04b5` as the bridge between them. -- The live NE `CRUSADER.EXE` mapping for that hidden-menu lane is now explicit and comment-backed in Ghidra too: direct hook `1130:2b75/2b78`, current-slot wrapper `13a0:0086` with constructor arg site `13a0:008d`, modal wrapper `13a0:020d` with inherited-arg patch subsite `13a0:024a`, listener create/dispatch `13a0:19b1` / `13a0:1df3`, compiled `0x410` CD-transfer-display body `13e8:2303`, deferred controller-side hook `13e8:25dd/25e0`, and the supporting cheat-state data cells at `1020:2833`, `1020:283d`, `1020:0844`, `1020:6045`, `1020:604f`, and `1020:6050`. The `0x410` body is still documented in place rather than renamed because it remains embedded inside the oversized `World_HandleKeyboardInput_13e8_14b4` function object. This improved live handoff and patch reproducibility still does not justify a headline estimate change by itself. -- The retail `-debug` switch is now separated cleanly from that hidden debugger lane too. Live `HandleCommandlineArgs` recovery in `CRUSADER.EXE` confirms a real `"-debug"` branch at `1048:0a93` that sets `g_debugMsgLevel = 10` (`1478:87e0`), prints `Debugging mode ON.`, and writes `1478:0845/0859` (`g_someDebugFlag` / `g_someDebugFlag2`). The `0x87e0` threshold is read by `ConsolePrintf` / `DebugPrintAndWaitForInput`, and `0x0859` is read by the segment `1468` `VideoPlayer_*` neighborhood (`1468:2869`, `1468:2af4`, helper `1468:2de9`). Current best read is `surviving debug-output / media-instrumentation switch`, not `dead parser stub`, and still not `the missing seg109/seg1408 usecode debugger bootstrap`, because the same pass found no evidence that `-debug` constructs or stores the real debugger-state pointer at `1478:659c/659e`. Focused write-up now lives in `docs/retail-debug-arg.md`. -- That `-debug` lane is tighter again after the follow-up deep dive and live Ghidra refinements. The seg1468 readers are now renamed/comment-backed as `VideoPlayer_AdvanceFrameAndHandleSkip`, `VideoPlayer_StreamChunks`, and `VideoPlayer_DrawDebugTimingOverlay`, and the helper body is no longer only `instrumentation-looking`: it writes two `500`-byte marker traces into adjacent scanlines near the bottom of a `0x280`-wide AVI playback buffer, using timing deltas scaled by `6000`. Current best read is therefore `built-in movie-playback timing overlay` plus the separate `g_debugMsgLevel` console/positioned-print threshold, still explicitly distinct from the hidden seg109/seg1408 usecode debugger path at `1478:659c/659e`. -- The same `-debug` lane is now bounded more comprehensively too. The user-confirmed moving bottom-of-video dots match the static two-scanline overlay model, so that AVI timing overlay is now the first closed visible effect. Outside the video lane, `-debug` also changes the global seg12d0 print threshold (`1478:87e0 = 10`) used by `ConsolePrintf`, `DebugPrintAndWaitForInput`, and the positioned print helpers, but the sink side is now tighter as well: `ProbablyPrintDebugMessage` formats through the static stdio-style table at `1478:6c32..6c81` and writes to the handle-`1` entry at `1478:6c46`, so the non-video side is ordinary DOS `stdout` gated by the threshold, not a separate hidden debugger console. Current xrefs still show that lane mostly as existing startup/config/cache/joystick/process diagnostics and a small set of dispatch/gump allocation failure-stop paths, not as a second confirmed hidden feature. The unresolved leftover is still `1478:0845`, which remains a parser-set latch with no recovered downstream consumer. -- The print inventory behind that same `-debug` lane is now materially tighter too. A focused pass recovered concrete `ConsolePrintf` / `DebugPrintAndWaitForInput` strings instead of only caller families: startup/arg strings such as `Debugging mode ON.`, `You DO need help!`, `Enabling ENHANCED mode. (NOT!)`, `Warping to mission %d ...`, `Defaulting to skill level %d`, and `Demo mode.`; init/config strings such as `Using map patch file.`, `Running with partial installation.`, `Running with full installation.`, and `Redirecting mission %d tune to '%s'`; cache/swap progress scaffolding such as `Creating Swap file [` and repeated `.` / bracket fragments; plus stronger failure/debug-stop fingerprints like `COULD NOT CREATE GLOB ITEM!`, `No room for Dispatcher Record/Playback.`, `End of script! (press any key)`, and `Out of Memory! [%u]`. Recovered call levels so far are `0x32` and `0xff`, both above the `-debug` threshold of `10`, which reinforces that the practical scarcity of visible text is about path frequency and graphics-mode presentation, not about the threshold still filtering these known messages out. -- The older folklore claim about flat offset `E69FB` and a possible secondary monochrome monitor is now materially weaker too. Local NE-segment mapping puts `0xE69FB` at live address `1478:2dfb`, which falls inside the `SYSTIMER.C` string in a data/name table (`KeyboardProcess`, `KEYIO.C`, `PRIORITY.C`, `SystemTimer`, `SYSTIMER.C`, `AccWait`), not inside executable instructions. The current retail print lane still points to ordinary `stdout` at `1478:6c46`, and targeted searches found no direct `mono`/`monochrome`/`hercules`/`MDA` strings or obvious monochrome-adapter port/memory references (`0x3b4/0x3b5/0x3b8/0x3ba`, `B000`). Current best read is therefore `folklore or address-mapping mistake`, not evidence for a hidden retail secondary-monitor debug display. -- A focused localized-build comparison is now tighter too. The live `/es/CRUSADER.EXE` pass still shows the same broad cheat/debug framework with shifted addresses rather than a rewritten system: `-laurie` sets the broad master gate at `1478:0910`, the gameplay-input gate still exists at `1478:0927`, the lower keyboard-cheat latch still exists at `1478:5fb3`, event `0x410` at `13e8:2211` still toggles the CD transfer display, and `13e8:24a5` still toggles Hack Mover with English-facing strings. But the sequence side is narrower and more specific now: a direct live byte scan found no exact `jassica16` table `24 1e 1f 1f 17 2e 1e 02 07 00` anywhere in Spanish data `1478:0000-8c3f` or `1480:0000-1fff`, and the old English-side slot at `1478:2833` now holds pointer-like words instead of the matcher bytes. The same pass also surfaced a likely Spanish post-sequence latch analog: Hack Mover is pre-gated by `1478:8ad6` before the broad gate check. So the remaining open question is no longer "does Spanish have the same cheat/debug family"; it is "where did the Spanish secret matcher move, and what writes `1478:8ad6`?" Detailed notes now live in `docs/spanish-cheat-differences.md`. -- The follow-up Spanish-cheat pass narrows that further: in the live `/es/CRUSADER.EXE` database, `1478:0910` still has only the `-laurie` write at `1050:0985`, `1478:5fb3` still has only the Laurie-hint helper writes at `13e8:0071/0077`, and `1478:8ad6` still has no recovered direct writer even though `13e8:249b` tests it before Hack Mover. The old English matcher slot at `1478:2833` remains repurposed as pointer-like words, and the only explicit multi-key helper recovered in this pass (`11d0:024b`) is just a generic key-list membership check used from a movement/control cluster, not a cheat-toggle lane. Current best read is therefore now stronger than "unknown moved Spanish matcher": no live replacement cheat-trigger byte matcher has been recovered at all, and the remaining proof frontier is any non-`-laurie` writer of `1478:0910` or a real writer for `1478:8ad6`. -- The next localized-build pass narrows the practical keyboard side too. Full decompilation of `World_HandleKeyboardInput` (`13e8:14b4`) still shows the Spanish broad-gate debug family (`0x141`, `0x241`, `0x410`, Hack Mover), but no recovered translated `~` / `0x7e` cheat-latch branch. That means the old English `-laurie` plus tilde bootstrap is no longer a defensible Spanish assumption: `-laurie` still raises `1478:0910`, but no runtime tilde writer of `1478:5fb3` has been recovered, and `1478:5fb3` itself now reads more like a widely consulted gate byte than a proven English-style keyboard-cheat latch. -- The deeper keyboard-handler pass strengthens that again: `1478:5fb3` is not just missing a recovered tilde writer, it no longer behaves like a positive enable latch at all. Every recovered consumer in `World_HandleKeyboardInput` requires `1478:5fb3 == 0`, and the only recovered writers are the Laurie-hint helper pulse `13e8:0071` then `13e8:0077`, which leaves the byte cleared. Current best localized-build answer to the user-facing cheat question is therefore: `-laurie` is the only recovered positive enabler for the surviving broad Spanish cheat/debug hotkeys, while Hack Mover remains separately blocked behind the still-unwritten `1478:8ad6` gate. -- The next Spanish follow-up narrows two remaining folklore assumptions too. First, the old English immortality-string slots at `1478:2850/2866` are repurposed as pointer-like data in `/es/CRUSADER.EXE`, and no direct uses of those addresses were recovered, so the English `F10` replenish / immortality path is now `unproven in Spanish` rather than merely `not yet re-closed`. Second, the new `8ad6` hunt found a nearby runtime-state cluster at `8ad7/8ad8/8ad9`, but the actual neighbor writes belong to gameplay-input modal helpers and the Hack-Mover-adjacent runtime helper `13e8:282f`; they still do not touch `8ad6`, which keeps the best writer hypothesis pointed at an indirect or script-driven path rather than ordinary compiled keyboard logic. +- seg001 gameplay/input/projectile work is stable enough to support verified raw-name ports into live NE work. +- The raw `0007` rendering/camera/tile-visibility lane has a strong structural map and now acts more as supporting evidence than as a primary unknown. +- The `0008` dispatch-helper and `000c` state/transition lanes have broad partial coverage, including enough caller-side structure to support practical NE naming work. +- The VM/USECODE lane now also has one earlier compiled-side producer anchored beyond the old direct `Item_GetDamaged` / `StorageDataProcess_Run` callers: `AreaSearch_CollideMove` is now verified as a paired `0x20b` / `0x20c` collision-process producer, and the local seg031 queue helpers are named structurally in the live database. +- That same collision-storage producer surface is now wider too: current direct callers are all movement/physics/animation-side (`Item_LegalMoveToPoint`, `Item_LegalMoveToPointWithCollisionInfo`, gravity, animation, supersprite, and fast-area gravity cleanup), and no verified non-collision producer reaches the `0x236` queue yet. +- The movement/collision lane is tighter at the helper level too: the step-aware seg029 sweep wrappers, the seg031 release-side queue cleanup pair, and the adjacent seg090 directional cache-offset helper are now named in the live database, so the remaining uncertainty in this lane sits earlier in caller policy rather than in the local helper layer. +- The startup/display lane is materially closed. Shared dispatch-entry ownership, seg126 file-backed control flow, seg127 fade control, and the surrounding palette/presentation helpers are now understood well enough that they should not stay in the live critical path. +- The cheat/debug lane is mostly closed at the behavior level. The secret-sequence matcher, broader cheat gates, F7 overlays, F10 modifier path, `Ctrl+L` location popup, `Ctrl+Q = 0x410` CD-transfer-display toggle, `-debug`, and `-laurie` are all separated far more cleanly than before. +- The hidden usecode-debugger lane is now structurally understood as a layered orphaned subsystem: seg109 UI pieces, seg1408 break-state helpers, and the seg1418 interpreter handoff are no longer conflated. +- The USECODE/VM lane now has a workable compiled-side model around `entity_vm_runtime_create`, `entity_vm_runtime_owner_resource_create`, `entity_vm_context_create_from_slot_index`, the masked-create hub at `000d:463a`, the persistence/load helpers, and the owner-loaded slot/value arithmetic. +- The owner-loaded body/range model is no longer speculative. Class-selection uses `class_id + 2`, header/subentry math matches extracted corpus output, and concrete body windows for `NPCTRIG`, `EVENT`, and related families are now verified. +- The map-renderer/documentation lane now has a stronger shape/controller crosswalk. Recent closures include `CRUMORPH`, `NPC_ONLY`, `WATCHNS`, `WATCHEW`, `CRYOBOX`, `CRAZYEW`, `CRAZYNS`, `VIDEOBOX`, `PANELEW`, `GENERATR`, and cross-game `DEATHBOX`, with viewer-side links kept conservative where actor-side state is still runtime-only. +- The command-line/startup lane is much tighter across both games: `-warp [x y z]`, `-mapoff`, `-egg`, startup teleporter selection, and the `-u` EUSECODE root override all now have practical behavior models instead of folklore-level descriptions. +- The PSX lane is no longer just side inventory. Retail/pre-alpha bundle loading, mission-briefing/passcode structure, and the reduced-content pre-alpha disc now have dedicated notes and enough stable naming to support future targeted passes. +- The Remorse class-lift preparation lane now has a usable document cluster: overall plan, candidate inventory, endpoint spec, ABI constraints, family notes for `EntityDispatchEntry` and `SpriteNode`, a conservative `Entity` family split, a VM runtime/owner-resource layout note, a compatibility-header draft, and one grouped resume index. +- The same class-lift prep lane is now more execution-ready: the `0x4588` broker family has its own focused object note, the toolchain story has a dedicated fingerprint-evidence note, and there is now a concrete first-batch class-authoring checklist ready for the first MCP-backed namespace/struct/vtable pass. -### Recently Closed Or No Longer Live +### Areas That Are No Longer Live Priorities -- The most reusable `misc_crusader_notes.txt` scratch items are no longer loose leads. `STEAM2` event hints are now checked against extracted USECODE rows, the old labels `FUN_1130_0896` / `FUN_1130_32af` / `FUN_1020_0000` / `FUN_1128_026b` are closed against live NE names, `ItemNPC_AnotherCreate` is now explicitly documented as the area-search-gated helper `NPC_CreateIfAreaSearchValid`, `Kernel_11d0_2491` is narrowed to a kernel/process snapshot writer, `FREE::ordinal3C` is constrained to an alert-clearing random `FREE::ordinal21` spawner, and `Int01E` is at least tightened from `unknown fire intrinsic` to `Actor::I_maybeFire` plus live export `1128:11da`. -- `ASYLUM.24` is resolved as `_ASS_StopAllSFX`; it is no longer an open plan item. -- The cheat/input side lane is complete enough to leave the live queue. -- The segment coverage ledger is no longer a missing artifact; only refinement remains. -- The startup/display lane is now materially complete as a major section: the outer seg005 shells, seg126 setup/script/fade path, seg127 fade controller, seg136 owner split, seg137 palette-emission helpers, and seg138 late cleanup/handoff bodies all have stable structural roles. -- The top startup/display ownership question is closed tightly enough for planning: `active_dispatch_entry_create_default` owns `g_active_dispatch_entry_farptr`, while seg049/seg126/seg138 helpers only borrow or clear the shared byte `+0x40`; the seg108 `0x4f38` lane is separate local sprite/object state. -- The shared seg126 base-path question is effectively closed: literal-address search still shows no store into `0x6aa:0x6ac`, seg004 only mutates the pointed buffer while separately assigning sibling root `0x6ae:0x6b0`, and the startup/display family continues to treat `0x6aa:0x6ac` as an inherited mutable external/default base path. -- The in-scope `0x31a2` transition/presentation reader pass is complete: the remaining reads in this lane now split into edge wait, modal break, deferred dispatch/state advance, and cleanup-abort roles. -- The remaining startup/display residuals are now low-impact: the exact higher-level UI label of preset pair `0x10/0x11` is still open, and the `000c:db68` overlap still blocks clean function hygiene for `transition_preentry_step_script` even though it no longer blocks semantic recovery. +- Startup/display transition recovery is no longer a front-line blocker unless overlap repair becomes necessary for adjacent work. +- The general cheat/debug key matrix no longer needs broad exploratory work. +- The `-debug` switch is no longer an open mystery; remaining work there is mostly sink-side cleanup and documentation. +- The earlier executable-patch experiments around the hidden debugger are documented history, not a current decompilation priority unless new evidence changes the entry model. ## Live Blockers -1. The oversized overlap rooted at `000c:db68` still blocks clean recovery of the real `transition_preentry_step_script` function object, even though it no longer blocks startup/display semantics. -2. The `0x4588` callback object is better constrained and now leans toward a video/presentation-state broker, but it still is not behaviorally classified enough for a confident subsystem rename. -3. The USECODE/VM sequencer still lacks the real upstream selector/caller path into `entity_vm_opcode_sequence_run`, and wrappers `entity_vm_context_try_create_mask_0400_slot0a_with_offset` / `entity_vm_context_try_create_mask_0800_slot0b_with_offset` remain outward-caller-dark even though their exact signed-additive `(slot, mask)` contracts are now closed, the generic masked hub at `000d:463a` is verified, and slot-`0x12` now has two concrete caller anchors at `0005:1776` / `0005:1945`. -4. High-value missing or weak function objects still exist in hot ranges such as `000b:2e00`, `0007:5a00`, and `000e:ffb0`; `000e:ffb0` is now caller-side constrained to the overlapped video-frame chunk lane (`00db` / `00dc`) paired with `anim_load_audio_frame`, but the overlap still blocks clean recovery. -5. Non-CALLF far-pointer relocations and weakly covered resource/data loaders remain real second-pass blockers, even though they are not the first thing to attack. -6. The `Ctrl+Q` / `0x410` lane still lacks a verified USECODE or higher-level emitter body, and the current blocker is now sharper. The owner-loaded format no longer blocks comparison: the class selector is now known to be `class_id + 2`, the header/subentry arithmetic at `000d:5066/51fd/53b4` matches extracted class headers and event rows exactly, and `NPCTRIG` slot `0x0a` / `0x20` now have concrete owner-loaded body ranges instead of only motif-level fits. But the compiled selector path is now also constrained enough to show what it does not provide: `000d:45c5` only maps entities into three generic category spans, `000d:44df` seeds those spans from `0x6608..0x660e`, `0005:295f` reuses the same slot index to test owner-row bit `0x0040`, and `0005:2c35` still has no caller/xref recovery. The remaining unresolved step is therefore a real upstream class-selector or caller-provenance recovery that can prove which class family is chosen before the slot body is decoded into the later `+0xd6/+0xd8` control stream and then into the `000c:fa2f` literal/replay lane. +1. The main remaining VM uncertainty is the real upstream selector/caller path into `entity_vm_opcode_sequence_run` and adjacent masked-create helpers. One earlier producer is now closed at `AreaSearch_CollideMove` for the `0x236` collision-storage family, but the owner-loaded class-family chooser and any broader non-collision producers are still upstream-dark. +2. The dark masked-materializer wrappers still need caller-role recovery, especially the signed-additive slot-`0x0a` / slot-`0x0b` pair and the surrounding higher-slot wrapper ladder. +3. The callback object rooted at `0x4588` still lacks a behaviorally safe subsystem name even though its allocation/finalize neighborhood is better constrained. +4. A few hot or awkward function ranges still lack clean function objects or good boundaries, especially around `000c:db68`, `000e:ffb0`, and several caller-dense gaps in `0007`, `000b`, and `000e`. +5. Weakly covered resource/data-loader families and non-`CALLF` far-pointer relocations are still a second-pass blocker for some object/table recovery work. +6. The segment ledger has improved, but it still trails the actual verified state in the notes and Ghidra database. Promoting known segments from documented evidence remains real work, not bookkeeping trivia. ## Current Focus -1. Continue the NE `CRUSADER.EXE` lane, using verified raw full-EXE and standalone-segment work as cross-reference evidence rather than as the active execution target. -2. Continue the USECODE/VM lane where the verified masked-create hub (`000d:463a`), the internal consumer blocks (`000d:208b`, `000d:21ed`), or the newly separated `extra-word masked materializer` subfamily can still yield concrete caller, selector, or record-shape evidence rather than repeated direct-xref dead ends. -3. Refine the coverage ledger from already-verified notes before broadening into fresh segment sweeps. -4. Use boundary repair only on active blockers with clear payoff, with `000c:db68` now downgraded to optional hygiene unless it blocks adjacent work again. -5. Revisit the `0x4588` callback object only when caller-side evidence is strong enough to support behavioral naming. -6. Use the new offline map-rendering lane to cross-check shape ids, map placements, and visible world composition against `crusader-disasm` shape/map notes before promoting additional rendering- or static-object-related names in `CRUSADER.EXE`. +1. Keep the live NE `CRUSADER.EXE` lane as the default working surface, using raw/full-EXE and standalone-segment work only as supporting evidence. +2. Keep the VM/USECODE lane focused on selector recovery, caller-role recovery, and record-shape confirmation rather than repeating storage-format validation that is already closed. +3. Promote ledger coverage from existing verified notes before broadening into fresh executable-wide sweeps. +4. Use overlap repair only where it unlocks an active high-payoff lane. +5. Use the map-renderer/tooling lane to validate shape ids, map placements, and viewer semantics before promoting additional static-object names in Ghidra. ## Next Resume Point -1. Continue the NE `CRUSADER.EXE` lane from `docs/ne-hole-filling-priorities.md`, using `docs/crusader-disasm-reference.md`, the raw-focused docs, and prior `CRUSADER-RAW.EXE` notes as supporting handoff material: prioritize one small segment or subsystem from the ranked list where the old disasm vocabulary, shape/map evidence, and verified raw names all overlap cleanly. -2. Build one conservative shape-id / map-placement crosswalk from `shapedata_more_complete.txt` and `mapdump/map-item-dump.txt` into the current trigger-heavy class families before promoting any new NE names. -3. Use the `unkcoffs/` Remorse/Regret function and intrinsic dumps as hint-only candidate generators for still-positional NE functions, but only when segment-local caller/data evidence agrees. -4. Keep the USECODE/VM lane moving where the verified masked-create hub (`000d:463a`), the internal consumer blocks (`000d:208b`, `000d:21ed`), or the newly separated `extra-word masked materializer` subfamily can still yield concrete caller, selector, or record-shape evidence rather than repeated direct-xref dead ends. -5. Refine the coverage ledger from already-verified notes before broadening into fresh segment sweeps. -6. Use boundary repair only on active blockers with clear payoff, with `000c:db68` now downgraded to optional hygiene unless it blocks adjacent work again. -7. Revisit the `0x4588` callback object only when caller-side evidence is strong enough to support behavioral naming. -8. Exercise `tools/render_crusader_map.py` on a few representative No Remorse and No Regret maps, then tighten the paint order using `TYPEFLAG.DAT` footpads and any mismatches visible against in-game screenshots or `crusader-disasm` map evidence. -9. If the map/editor-visibility lane is revisited, start from `docs/editor-object-visibility.md` and the upstream `1180:0951..095c` world-item builder gate first; rule in or rule out a second debug-only world-item builder before spending more time on cheat or command-line searches. -10. Continue the PSX pre-alpha lane from `docs/psx/prealpha.md`: classify the surviving `LoadExec` callers around `80046aac`, confirm whether the `TALK1.XA` path is still reachable in practice, and compare the three shipped `LSET1` bundles against the retail extractor outputs before assuming the build is only a content-pruned No Remorse branch. - -11. Recover the real upstream caller/selector path into `entity_vm_opcode_sequence_run`, most likely by finding the first non-recursive `0x6714` context-method caller or vtable dispatch site rather than by repeating raw xref queries that still return no direct edges. -12. Recover real caller roles for `entity_vm_context_try_create_mask_0400_slot0a_with_offset` and `entity_vm_context_try_create_mask_0800_slot0b_with_offset` by treating them as the remaining dark members of the now-verified signed-additive masked-materializer subfamily and comparing them against the newly anchored slot-`0x12` caller pattern. -13. Tighten the newly surfaced higher-slot wrapper ladder around `0005:3115..31da`, especially the two slot-`0x12` caller sites at `0005:1776` / `0005:1945` and the slot-`0x10` guarded callsite, so any future promotion to `leaveFastArea` / `func11|cast` / `justMoved` / `AvatarStoleSomething` / `animGetHit` is driven by binary caller behavior rather than by external tables alone. -14. Tighten the outward caller chains around the renamed seg006 masked helpers `entity_vm_context_try_create_mask_0008_slot30_with_offset` (`0006:0ba4`) and `entity_vm_context_try_create_mask_0010_slot08_with_offset_if_ready` (`0006:108c`) so the local state-selector lane and the adjacent class-linked value family can be tied back to concrete gameplay subsystems rather than only to class-detail fields. -15. Tighten the paired-file-family reading of the seg070 twin loops at `0009:67b6` and `0009:6916` by recovering which temporary buffer and record schema each family populates behind `entity_vm_runtime_owner_resource_create`. -16. Promote additional ledger rows where the current docs already justify `Foothold`, `Partial`, or `Deep`. -17. If the VM lane stalls again, revisit `000e:ffb0` from the now-verified `00db/00dc` caller windows and try to recover an adjacent non-overlapped helper before attempting any boundary repair. -18. If the immortality lane is revisited, stay focused on `NPCTRIG` slot `0x0a` first, with slot `0x20` still treated as the typed/setup companion and `EVENT` only as the generic hub baseline; the three currently recovered direct `0005:295f` caller families are now all closed and comment-backed in the live NE program at `10f0:02d9`, `10f0:0379`, `10f0:03c3`, `10f0:03e5`, `1128:0ff0`, and `1138:1384`, so the next defensible step is an earlier producer that assigns subtype `0x20b/0x20c` into field `+0x3c` or otherwise chooses the owner-loaded class family before these generic damage consumers run. -19. Use the new Pentagram-derived parser proof of concept as the first tooling bridge for raw class/slot bodies: extend opcode coverage conservatively, emit IR v1 artifacts, and only then prototype a Ghidra-side annotation importer against compiled anchors like `000d:51fd`, `000d:5572`, `000d:46ec`, `000d:22bc`, and `000d:ebe3`. +1. Resume from `docs/ne-hole-filling-priorities.md` and pick one small NE cluster where the old disasm vocabulary, extracted corpus evidence, and live NE callers overlap cleanly. +2. Stay on the VM lane and move one step earlier than the now-mapped movement/collision helper set around `AreaSearch_CollideMove`: the local seg029/031/090 helper layer is now named, so the next work is the policy/dispatch layer that decides when those legal-move, gravity, animation, or supersprite paths instantiate the local `0x236` collision-storage queue, plus verification of whether any non-collision producer feeds the same `StorageDataProcess_Create` / `Run` family. +3. Recover caller roles for the remaining dark signed-additive masked wrappers, especially the slot-`0x0a` / slot-`0x0b` pair, and compare them against the now-anchored slot-`0x12` caller pattern. +4. Tighten the higher-slot wrapper ladder around `0005:3115..31da` so future event-label promotion depends on compiled caller behavior instead of external tables. +5. Tighten the seg006 masked-helper caller chains so the local state-selector/value family can be tied to concrete gameplay subsystems. +6. Classify the paired seg070 loops behind `entity_vm_runtime_owner_resource_create`, especially which temporary buffers and record schemas each family populates. +7. Promote additional ledger rows directly from already-verified docs and live comments, especially where segments already deserve `Foothold`, `Partial`, or `Deep`; the new seg029 step-aware sweep batch, seg031 queue-release batch, and seg090 movement-helper batch should be the immediate template. +8. If the VM lane stalls, revisit `000e:ffb0` from the now-better-constrained video/audio caller windows and try to recover an adjacent non-overlapped helper before attempting broad boundary repair. +9. Continue the map-renderer cross-check lane by building one conservative shape-id/map-placement crosswalk from `shapedata_more_complete.txt`, extracted corpora, and authored scene evidence before promoting more trigger-heavy classes in NE. +10. Keep the PSX pre-alpha lane alive as a secondary target: classify the `LoadExec` callers, test whether the stale `TALK1.XA` path is still reachable, and compare the shipped `LSET1` bundles against the retail extractor outputs. ## Remaining Work To Reach A Reasonably Complete Decompilation State ### 1. Coverage And Tracker Completion -- Promote the existing 145-row ledger from a seeded first pass into a trustworthy executable-wide coverage dashboard. -- Sweep untouched segments cluster-by-cluster instead of one-off function hunting, using adjacency and call relationships. -- Convert more segments from `None` to `Foothold` / `Partial` where current notes already support it. -- Close the largest remaining hot-target gaps so the far-call ranking list stays representative of real coverage. -- Keep the plan, docs, and ledger synchronized after each verified batch. +- Keep turning the seeded 145-row ledger into a trustworthy whole-program dashboard. +- Sweep remaining lightly covered segment clusters by adjacency and call relationships rather than one-off function hunting. +- Keep the plan, the docs, the ledger, and the live Ghidra comments synchronized after each verified batch. -### 2. Startup/Display And Presentation Lane +### 2. VM / USECODE / Scripting Lane -- Keep the startup/display lane closed unless new caller evidence materially changes its current model. -- Classify the exact higher-level UI label of preset pair `0x10/0x11` only if stronger caller or string evidence appears. -- Revisit the remaining seg049/seg108/seg138 naming ambiguity only when it supports a defensible behavioral rename rather than another structural pass. -- Repair `000c:db68` only when a clean `transition_preentry_step_script` function object or adjacent active work makes the overlap fix worth the risk. +- Close the upstream selector/caller path into the sequencer and masked-create families. +- Finish separating owner-row-backed data from runtime-decoded control streams and dispatch-entry seed records. +- Expand caller-backed event-label promotion only where binary behavior and slot reuse agree. +- Keep maturing the tooling bridge from extracted corpora into compiled-side annotation/import workflows. -### 3. VM / USECODE / Scripting Lane +### 3. Callback / Allocator / Object-Role Lane -- Recover the upstream selector into `entity_vm_opcode_sequence_run` and map payload-shape handlers to real opcode dispatch. -- Recover real caller roles for the dark mask wrappers `entity_vm_context_try_create_mask_0400_slot0a_with_offset` and `entity_vm_context_try_create_mask_0800_slot0b_with_offset`. -- Keep separating owner-table-backed `0x39ca` rows from static dispatch-entry seed rows. -- Finish classifying the seg069/070 helper behind `entity_vm_runtime_owner_resource_create`. -- Broaden owner-loaded class/event validation beyond the first strong sample families. -- Keep event-label mapping conservative: only promote ScummVM event names where binary behavior and slot reuse agree. -- Mature the reversible script IR until it can represent raw headers, event rows, payload forms, and unresolved opcodes without information loss. -- Continue extracting readable descriptor-family artifacts, but treat them as evidence aids rather than rename authority. +- Classify the `0x4588` callback object strongly enough for a real subsystem name. +- Separate generic cache/allocator mechanics from game-specific client behavior where caller evidence supports it. +- Keep low-level helper names conservative until behavior, not just structure, is clear. -### 4. Cache / Allocator / Callback-Object Lane +### 4. Rendering / Animation / UI Support Lanes -- Finish classifying the object rooted at `0x4588` so the allocator finalize path and callback emissions can receive behaviorally meaningful names. -- Tighten the role of `allocator_phase_finalize_pass` only where it intersects callback-object semantics or active runtime users. -- Separate generic cache-manager mechanics from game-specific client behavior wherever caller evidence supports it. -- Clarify remaining object-role names around tracked handles, dispatch-entry lifecycle helpers, and palette-backed state builders. -- Keep `_ASS_StopAllSFX` and the resolved audio-import lane closed; do not treat it as an open blocker again. +- Keep the rendering/palette/animation lanes focused on caller-side semantics and cleanup, not exploratory renaming in isolation. +- Revisit `000e:ffb0` and adjacent overlap-heavy video helpers only when the payoff is clear. +- Use map-renderer evidence and extracted corpora to validate static-object and helper/controller naming before promoting it into live NE work. -### 5. Rendering, Palette, Animation, And UI Support Lanes +### 5. Data / Resource / Relocation Coverage -- Finish the remaining caller-side semantics for raw 0007 rendering helpers, seg049 controller dispatch, seg108 sprite/object helpers, and seg137/138 palette state builders. -- Revisit `000e:ffb0` and adjacent 000e video/animation overlap only when it blocks active analysis or offers a strong isolated win. -- Expand the palette/VGA helper family only where it clarifies higher-level behavior rather than duplicating low-level helper names. -- Keep validating startup/display assumptions against raw 0007/0008/000d caller behavior instead of renaming isolated helpers in a vacuum. - -### 6. Boundary Repair And Function Hygiene - -- Create or repair missing function objects in the highest-traffic unresolved ranges first. -- Fix only overlaps that block live lanes or high-caller targets. -- Preserve conservative naming for repaired functions until direct caller or data evidence justifies promotion. -- Continue rejecting disproven ports or stale hypotheses instead of preserving them in live work queues. - -### 7. Data, Imports, And Resource-Format Coverage - -- Work through the deferred non-CALLF far-pointer relocations when they become necessary for object/table recovery. -- Expand coverage of weakly mapped resource/data loaders such as FLEX-derived descriptors, tables, caches, and per-shape data files. -- Cross-check current data-structure assumptions against external references like ScummVM only as supporting evidence, not as rename authority. -- Keep external import identities synchronized with verified import-table evidence. - -### 8. Completion Criteria - -A reasonably complete decompilation state should mean: - -- most actively used subsystems are behaviorally named rather than only structurally named, -- the major live blockers (`000c:db68`, `000e:ffb0`, hot missing function objects, dark VM selector path, `0x4588` object role) are either resolved or reduced to low-impact residuals, -- the far-call hot list has very few meaningful unknowns left, -- the ledger gives a credible whole-program view rather than a sparse seed set, -- and the remaining gaps are mostly long-tail cleanup, low-traffic helpers, or data polish instead of core architecture uncertainty. +- Tackle deferred non-`CALLF` far-pointer relocations when they are needed for active table/object recovery. +- Broaden weakly covered resource/data-loader families where they block real subsystem classification. +- Keep external references like ScummVM or older disasm corpora as evidence aids, not rename authority. ## Priority Order -1. Startup/display transition lane -2. VM / USECODE selector and loader lane -3. Coverage-ledger refinement from already-verified notes -4. High-value overlap repair (`000c:db68`, then `000e:ffb0` when justified) -5. `0x4588` callback-object classification -6. Broader segment sweeps and second-pass data/relocation work +1. VM / USECODE selector and caller recovery +2. Coverage-ledger refinement from already-verified notes +3. Callback-object classification around `0x4588` +4. High-value boundary repair when it unlocks active work +5. Broader segment sweeps and second-pass data/relocation work +6. Secondary map-renderer and PSX follow-up lanes ## Evidence Anchors @@ -301,15 +146,14 @@ Primary files backing this plan state: - `crusader_segment_coverage_ledger.csv` - `crusader_decompilation_notes.md` - `docs/overview.md` +- `docs/ne-hole-filling-priorities.md` +- `docs/crusader-disasm-reference.md` - `docs/raw-porting-progress.md` - `docs/raw-0008-000c.md` - `docs/raw-000a-000d.md` - `docs/raw-000e.md` -- `docs/crusader-disasm-reference.md` -- `docs/ne-hole-filling-priorities.md` - `docs/far-call-targets.md` - `docs/usecode-roundtrip-ir.md` -- `docs/scummvm-crusader-reference.md` ## Update Rule @@ -321,4 +165,4 @@ Update this file when one of the following happens: - a segment cluster is promoted materially in the ledger, - or the next resume point changes enough that the current handoff would mislead the next pass. -Keep the file short. Move detailed completed analysis into the appropriate file under `docs/` and leave only the current state, blockers, and forward path here. +Keep this file short. Move detailed completed analysis into the appropriate file under `docs/` and leave only the current state, blockers, and forward path here.