diff --git a/.gitignore b/.gitignore index a9ab479..2c54099 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ cdb_*.txt .vs/ *.user Thumbs.db +__pycache__/ # per-user input profile, written by the game on first PAD run (cwd=content) /content/bindings.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index 478527e..429076a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -164,7 +164,12 @@ add_library(munga_engine STATIC "engine/MUNGA/TERRAIN.cpp" "engine/MUNGA/TESTALL.cpp" "engine/MUNGA/TIME.cpp" - "engine/MUNGA/TIMESTUB.cpp" + # TIMESTUB.cpp REMOVED (2026-07-14): its GetRTC/GetHiRes were 2007 call-counter + # stubs (`return time++` -- the "clock" advanced per CALL, not per ms) that WON + # the /FORCE duplicate-symbol race over the REAL QueryPerformanceCounter clock + # in L4TIME.cpp (LNK4006 "second definition ignored"). Every Now()-domain + # consumer (dead-reckoning above all) ran on call-count pseudo-time -> the + # replicant stall/jump chop. L4TIME.cpp covers every symbol TIMESTUB defined. "engine/MUNGA/TOOL.cpp" "engine/MUNGA/TRACE.cpp" "engine/MUNGA/TRACSTUB.cpp" @@ -181,6 +186,7 @@ add_library(munga_engine STATIC "engine/MUNGA/WRHOUS.cpp" "engine/MUNGA_L4/DXUtils.cpp" "engine/MUNGA_L4/L4APP.cpp" + "engine/MUNGA_L4/L4AUDEFX.cpp" "engine/MUNGA_L4/L4AUDHDW.cpp" "engine/MUNGA_L4/L4AUDIO.cpp" "engine/MUNGA_L4/L4AUDLVL.cpp" @@ -308,6 +314,7 @@ add_library(bt410_l4 STATIC "game/reconstructed/thermalsight.cpp" "game/reconstructed/torso.cpp" "game/reconstructed/btstubs.cpp" + "game/reconstructed/audiopresets.cpp" "game/reconstructed/dpl2d.cpp" "game/original/BT/BTMSSN.CPP" "game/original/BT/BTREG.CPP" @@ -346,7 +353,6 @@ target_link_libraries(btl4 PRIVATE bt410_l4 munga_engine "${CMAKE_SOURCE_DIR}/engine/lib/OpenAL32.lib" - "${CMAKE_SOURCE_DIR}/engine/lib/libsndfile-1.lib" "${DXSDK}/Lib/x86/d3d9.lib" "${DXSDK}/Lib/x86/d3dx9.lib" "${DXSDK}/Lib/x86/dinput8.lib" @@ -372,9 +378,10 @@ if(BT412_STEAM) "$") endif() -# Copy the third-party runtime DLLs next to the built exe. +# Copy the OpenAL runtime DLL next to the built exe. (libsndfile is gone: its +# repo DLL was a no-op STUB -- L4AUDRES now loads the soundbank WAVs with an +# in-tree RIFF/PCM reader, no external dependency.) add_custom_command(TARGET btl4 POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_SOURCE_DIR}/engine/lib/OpenAL32.dll" - "${CMAKE_SOURCE_DIR}/engine/lib/libsndfile-1.dll" "$") diff --git a/content/AUDIO/AllAcceleration_z0.wav b/content/AUDIO/AllAcceleration_z0.wav new file mode 100644 index 0000000..d64927a Binary files /dev/null and b/content/AUDIO/AllAcceleration_z0.wav differ diff --git a/content/AUDIO/AllAcceleration_z1.wav b/content/AUDIO/AllAcceleration_z1.wav new file mode 100644 index 0000000..e0b0fc9 Binary files /dev/null and b/content/AUDIO/AllAcceleration_z1.wav differ diff --git a/content/AUDIO/AllAcceleration_z2.wav b/content/AUDIO/AllAcceleration_z2.wav new file mode 100644 index 0000000..a8a081e Binary files /dev/null and b/content/AUDIO/AllAcceleration_z2.wav differ diff --git a/content/AUDIO/AllAcceleration_z3.wav b/content/AUDIO/AllAcceleration_z3.wav new file mode 100644 index 0000000..224f728 Binary files /dev/null and b/content/AUDIO/AllAcceleration_z3.wav differ diff --git a/content/AUDIO/AllAlarms.wav b/content/AUDIO/AllAlarms.wav new file mode 100644 index 0000000..009d7c2 Binary files /dev/null and b/content/AUDIO/AllAlarms.wav differ diff --git a/content/AUDIO/AllButtons_z0.wav b/content/AUDIO/AllButtons_z0.wav new file mode 100644 index 0000000..70ccb57 Binary files /dev/null and b/content/AUDIO/AllButtons_z0.wav differ diff --git a/content/AUDIO/AllButtons_z1.wav b/content/AUDIO/AllButtons_z1.wav new file mode 100644 index 0000000..58558b3 Binary files /dev/null and b/content/AUDIO/AllButtons_z1.wav differ diff --git a/content/AUDIO/AllButtons_z10.wav b/content/AUDIO/AllButtons_z10.wav new file mode 100644 index 0000000..3902ab9 Binary files /dev/null and b/content/AUDIO/AllButtons_z10.wav differ diff --git a/content/AUDIO/AllButtons_z11.wav b/content/AUDIO/AllButtons_z11.wav new file mode 100644 index 0000000..7975d19 Binary files /dev/null and b/content/AUDIO/AllButtons_z11.wav differ diff --git a/content/AUDIO/AllButtons_z12.wav b/content/AUDIO/AllButtons_z12.wav new file mode 100644 index 0000000..db52457 Binary files /dev/null and b/content/AUDIO/AllButtons_z12.wav differ diff --git a/content/AUDIO/AllButtons_z13.wav b/content/AUDIO/AllButtons_z13.wav new file mode 100644 index 0000000..9c5dcde Binary files /dev/null and b/content/AUDIO/AllButtons_z13.wav differ diff --git a/content/AUDIO/AllButtons_z2.wav b/content/AUDIO/AllButtons_z2.wav new file mode 100644 index 0000000..02173f0 Binary files /dev/null and b/content/AUDIO/AllButtons_z2.wav differ diff --git a/content/AUDIO/AllButtons_z3.wav b/content/AUDIO/AllButtons_z3.wav new file mode 100644 index 0000000..d20be83 Binary files /dev/null and b/content/AUDIO/AllButtons_z3.wav differ diff --git a/content/AUDIO/AllButtons_z4.wav b/content/AUDIO/AllButtons_z4.wav new file mode 100644 index 0000000..fa799c6 Binary files /dev/null and b/content/AUDIO/AllButtons_z4.wav differ diff --git a/content/AUDIO/AllButtons_z5.wav b/content/AUDIO/AllButtons_z5.wav new file mode 100644 index 0000000..3d3fcb7 Binary files /dev/null and b/content/AUDIO/AllButtons_z5.wav differ diff --git a/content/AUDIO/AllButtons_z6.wav b/content/AUDIO/AllButtons_z6.wav new file mode 100644 index 0000000..9f3e30a Binary files /dev/null and b/content/AUDIO/AllButtons_z6.wav differ diff --git a/content/AUDIO/AllButtons_z7.wav b/content/AUDIO/AllButtons_z7.wav new file mode 100644 index 0000000..fc14908 Binary files /dev/null and b/content/AUDIO/AllButtons_z7.wav differ diff --git a/content/AUDIO/AllButtons_z8.wav b/content/AUDIO/AllButtons_z8.wav new file mode 100644 index 0000000..ab6a199 Binary files /dev/null and b/content/AUDIO/AllButtons_z8.wav differ diff --git a/content/AUDIO/AllButtons_z9.wav b/content/AUDIO/AllButtons_z9.wav new file mode 100644 index 0000000..e597a9a Binary files /dev/null and b/content/AUDIO/AllButtons_z9.wav differ diff --git a/content/AUDIO/AllCoolant_z0.wav b/content/AUDIO/AllCoolant_z0.wav new file mode 100644 index 0000000..2998de5 Binary files /dev/null and b/content/AUDIO/AllCoolant_z0.wav differ diff --git a/content/AUDIO/AllCoolant_z1.wav b/content/AUDIO/AllCoolant_z1.wav new file mode 100644 index 0000000..b3009c9 Binary files /dev/null and b/content/AUDIO/AllCoolant_z1.wav differ diff --git a/content/AUDIO/AllCoolant_z2.wav b/content/AUDIO/AllCoolant_z2.wav new file mode 100644 index 0000000..00e8c10 Binary files /dev/null and b/content/AUDIO/AllCoolant_z2.wav differ diff --git a/content/AUDIO/AllCoolant_z3.wav b/content/AUDIO/AllCoolant_z3.wav new file mode 100644 index 0000000..75fce86 Binary files /dev/null and b/content/AUDIO/AllCoolant_z3.wav differ diff --git a/content/AUDIO/AllCoolant_z4.wav b/content/AUDIO/AllCoolant_z4.wav new file mode 100644 index 0000000..c16548e Binary files /dev/null and b/content/AUDIO/AllCoolant_z4.wav differ diff --git a/content/AUDIO/AllCoolant_z5.wav b/content/AUDIO/AllCoolant_z5.wav new file mode 100644 index 0000000..9ff9a13 Binary files /dev/null and b/content/AUDIO/AllCoolant_z5.wav differ diff --git a/content/AUDIO/AllCoolant_z6.wav b/content/AUDIO/AllCoolant_z6.wav new file mode 100644 index 0000000..9a7b806 Binary files /dev/null and b/content/AUDIO/AllCoolant_z6.wav differ diff --git a/content/AUDIO/AllCoolant_z7.wav b/content/AUDIO/AllCoolant_z7.wav new file mode 100644 index 0000000..ca8faa1 Binary files /dev/null and b/content/AUDIO/AllCoolant_z7.wav differ diff --git a/content/AUDIO/AllCoolant_z8.wav b/content/AUDIO/AllCoolant_z8.wav new file mode 100644 index 0000000..43d7173 Binary files /dev/null and b/content/AUDIO/AllCoolant_z8.wav differ diff --git a/content/AUDIO/AllDamage.wav b/content/AUDIO/AllDamage.wav new file mode 100644 index 0000000..21e61e9 Binary files /dev/null and b/content/AUDIO/AllDamage.wav differ diff --git a/content/AUDIO/AllEngine_z0.wav b/content/AUDIO/AllEngine_z0.wav new file mode 100644 index 0000000..08685b8 Binary files /dev/null and b/content/AUDIO/AllEngine_z0.wav differ diff --git a/content/AUDIO/AllEngine_z1.wav b/content/AUDIO/AllEngine_z1.wav new file mode 100644 index 0000000..87327d0 Binary files /dev/null and b/content/AUDIO/AllEngine_z1.wav differ diff --git a/content/AUDIO/AllEnviron.wav b/content/AUDIO/AllEnviron.wav new file mode 100644 index 0000000..4fc7a4a Binary files /dev/null and b/content/AUDIO/AllEnviron.wav differ diff --git a/content/AUDIO/AllExplosion_z0.wav b/content/AUDIO/AllExplosion_z0.wav new file mode 100644 index 0000000..c543deb Binary files /dev/null and b/content/AUDIO/AllExplosion_z0.wav differ diff --git a/content/AUDIO/AllExplosion_z1.wav b/content/AUDIO/AllExplosion_z1.wav new file mode 100644 index 0000000..c401b89 Binary files /dev/null and b/content/AUDIO/AllExplosion_z1.wav differ diff --git a/content/AUDIO/AllExplosion_z10.wav b/content/AUDIO/AllExplosion_z10.wav new file mode 100644 index 0000000..acf4405 Binary files /dev/null and b/content/AUDIO/AllExplosion_z10.wav differ diff --git a/content/AUDIO/AllExplosion_z11.wav b/content/AUDIO/AllExplosion_z11.wav new file mode 100644 index 0000000..4b4f3ae Binary files /dev/null and b/content/AUDIO/AllExplosion_z11.wav differ diff --git a/content/AUDIO/AllExplosion_z12.wav b/content/AUDIO/AllExplosion_z12.wav new file mode 100644 index 0000000..cbad416 Binary files /dev/null and b/content/AUDIO/AllExplosion_z12.wav differ diff --git a/content/AUDIO/AllExplosion_z13.wav b/content/AUDIO/AllExplosion_z13.wav new file mode 100644 index 0000000..7135930 Binary files /dev/null and b/content/AUDIO/AllExplosion_z13.wav differ diff --git a/content/AUDIO/AllExplosion_z14.wav b/content/AUDIO/AllExplosion_z14.wav new file mode 100644 index 0000000..8b04f12 Binary files /dev/null and b/content/AUDIO/AllExplosion_z14.wav differ diff --git a/content/AUDIO/AllExplosion_z15.wav b/content/AUDIO/AllExplosion_z15.wav new file mode 100644 index 0000000..6ee722c Binary files /dev/null and b/content/AUDIO/AllExplosion_z15.wav differ diff --git a/content/AUDIO/AllExplosion_z16.wav b/content/AUDIO/AllExplosion_z16.wav new file mode 100644 index 0000000..af131ef Binary files /dev/null and b/content/AUDIO/AllExplosion_z16.wav differ diff --git a/content/AUDIO/AllExplosion_z17.wav b/content/AUDIO/AllExplosion_z17.wav new file mode 100644 index 0000000..6f7aba6 Binary files /dev/null and b/content/AUDIO/AllExplosion_z17.wav differ diff --git a/content/AUDIO/AllExplosion_z18.wav b/content/AUDIO/AllExplosion_z18.wav new file mode 100644 index 0000000..5be68ca Binary files /dev/null and b/content/AUDIO/AllExplosion_z18.wav differ diff --git a/content/AUDIO/AllExplosion_z19.wav b/content/AUDIO/AllExplosion_z19.wav new file mode 100644 index 0000000..843bc30 Binary files /dev/null and b/content/AUDIO/AllExplosion_z19.wav differ diff --git a/content/AUDIO/AllExplosion_z2.wav b/content/AUDIO/AllExplosion_z2.wav new file mode 100644 index 0000000..d137952 Binary files /dev/null and b/content/AUDIO/AllExplosion_z2.wav differ diff --git a/content/AUDIO/AllExplosion_z20.wav b/content/AUDIO/AllExplosion_z20.wav new file mode 100644 index 0000000..3930a07 Binary files /dev/null and b/content/AUDIO/AllExplosion_z20.wav differ diff --git a/content/AUDIO/AllExplosion_z21.wav b/content/AUDIO/AllExplosion_z21.wav new file mode 100644 index 0000000..8dcd9b2 Binary files /dev/null and b/content/AUDIO/AllExplosion_z21.wav differ diff --git a/content/AUDIO/AllExplosion_z22.wav b/content/AUDIO/AllExplosion_z22.wav new file mode 100644 index 0000000..237f77c Binary files /dev/null and b/content/AUDIO/AllExplosion_z22.wav differ diff --git a/content/AUDIO/AllExplosion_z23.wav b/content/AUDIO/AllExplosion_z23.wav new file mode 100644 index 0000000..d0a9543 Binary files /dev/null and b/content/AUDIO/AllExplosion_z23.wav differ diff --git a/content/AUDIO/AllExplosion_z24.wav b/content/AUDIO/AllExplosion_z24.wav new file mode 100644 index 0000000..a38265a Binary files /dev/null and b/content/AUDIO/AllExplosion_z24.wav differ diff --git a/content/AUDIO/AllExplosion_z3.wav b/content/AUDIO/AllExplosion_z3.wav new file mode 100644 index 0000000..aa2be0d Binary files /dev/null and b/content/AUDIO/AllExplosion_z3.wav differ diff --git a/content/AUDIO/AllExplosion_z4.wav b/content/AUDIO/AllExplosion_z4.wav new file mode 100644 index 0000000..a36629f Binary files /dev/null and b/content/AUDIO/AllExplosion_z4.wav differ diff --git a/content/AUDIO/AllExplosion_z5.wav b/content/AUDIO/AllExplosion_z5.wav new file mode 100644 index 0000000..e20338f Binary files /dev/null and b/content/AUDIO/AllExplosion_z5.wav differ diff --git a/content/AUDIO/AllExplosion_z6.wav b/content/AUDIO/AllExplosion_z6.wav new file mode 100644 index 0000000..a89e632 Binary files /dev/null and b/content/AUDIO/AllExplosion_z6.wav differ diff --git a/content/AUDIO/AllExplosion_z7.wav b/content/AUDIO/AllExplosion_z7.wav new file mode 100644 index 0000000..d54aaad Binary files /dev/null and b/content/AUDIO/AllExplosion_z7.wav differ diff --git a/content/AUDIO/AllExplosion_z8.wav b/content/AUDIO/AllExplosion_z8.wav new file mode 100644 index 0000000..1c9a6c2 Binary files /dev/null and b/content/AUDIO/AllExplosion_z8.wav differ diff --git a/content/AUDIO/AllExplosion_z9.wav b/content/AUDIO/AllExplosion_z9.wav new file mode 100644 index 0000000..5f131f0 Binary files /dev/null and b/content/AUDIO/AllExplosion_z9.wav differ diff --git a/content/AUDIO/AllHits_z0.wav b/content/AUDIO/AllHits_z0.wav new file mode 100644 index 0000000..feaeec6 Binary files /dev/null and b/content/AUDIO/AllHits_z0.wav differ diff --git a/content/AUDIO/AllHits_z1.wav b/content/AUDIO/AllHits_z1.wav new file mode 100644 index 0000000..23f75a7 Binary files /dev/null and b/content/AUDIO/AllHits_z1.wav differ diff --git a/content/AUDIO/AllHits_z2.wav b/content/AUDIO/AllHits_z2.wav new file mode 100644 index 0000000..02c8d51 Binary files /dev/null and b/content/AUDIO/AllHits_z2.wav differ diff --git a/content/AUDIO/AllHits_z3.wav b/content/AUDIO/AllHits_z3.wav new file mode 100644 index 0000000..787d735 Binary files /dev/null and b/content/AUDIO/AllHits_z3.wav differ diff --git a/content/AUDIO/AllHits_z4.wav b/content/AUDIO/AllHits_z4.wav new file mode 100644 index 0000000..e68dfb8 Binary files /dev/null and b/content/AUDIO/AllHits_z4.wav differ diff --git a/content/AUDIO/AllHits_z5.wav b/content/AUDIO/AllHits_z5.wav new file mode 100644 index 0000000..c9cd7f2 Binary files /dev/null and b/content/AUDIO/AllHits_z5.wav differ diff --git a/content/AUDIO/AllHits_z6.wav b/content/AUDIO/AllHits_z6.wav new file mode 100644 index 0000000..4f72288 Binary files /dev/null and b/content/AUDIO/AllHits_z6.wav differ diff --git a/content/AUDIO/AllHits_z7.wav b/content/AUDIO/AllHits_z7.wav new file mode 100644 index 0000000..d54aaad Binary files /dev/null and b/content/AUDIO/AllHits_z7.wav differ diff --git a/content/AUDIO/AllProjectile_z0.wav b/content/AUDIO/AllProjectile_z0.wav new file mode 100644 index 0000000..d80b82a Binary files /dev/null and b/content/AUDIO/AllProjectile_z0.wav differ diff --git a/content/AUDIO/AllProjectile_z1.wav b/content/AUDIO/AllProjectile_z1.wav new file mode 100644 index 0000000..4d2c6bf Binary files /dev/null and b/content/AUDIO/AllProjectile_z1.wav differ diff --git a/content/AUDIO/AllProjectile_z2.wav b/content/AUDIO/AllProjectile_z2.wav new file mode 100644 index 0000000..cb8ab04 Binary files /dev/null and b/content/AUDIO/AllProjectile_z2.wav differ diff --git a/content/AUDIO/AllProjectile_z3.wav b/content/AUDIO/AllProjectile_z3.wav new file mode 100644 index 0000000..7f87552 Binary files /dev/null and b/content/AUDIO/AllProjectile_z3.wav differ diff --git a/content/AUDIO/AllProjectile_z4.wav b/content/AUDIO/AllProjectile_z4.wav new file mode 100644 index 0000000..a40b5a9 Binary files /dev/null and b/content/AUDIO/AllProjectile_z4.wav differ diff --git a/content/AUDIO/AllProjectile_z5.wav b/content/AUDIO/AllProjectile_z5.wav new file mode 100644 index 0000000..f70a0b4 Binary files /dev/null and b/content/AUDIO/AllProjectile_z5.wav differ diff --git a/content/AUDIO/AllProjectile_z6.wav b/content/AUDIO/AllProjectile_z6.wav new file mode 100644 index 0000000..4e4dd91 Binary files /dev/null and b/content/AUDIO/AllProjectile_z6.wav differ diff --git a/content/AUDIO/AllProjectile_z7.wav b/content/AUDIO/AllProjectile_z7.wav new file mode 100644 index 0000000..d04afed Binary files /dev/null and b/content/AUDIO/AllProjectile_z7.wav differ diff --git a/content/AUDIO/AllProjectile_z8.wav b/content/AUDIO/AllProjectile_z8.wav new file mode 100644 index 0000000..937c0a6 Binary files /dev/null and b/content/AUDIO/AllProjectile_z8.wav differ diff --git a/content/AUDIO/AllWarning_z0.wav b/content/AUDIO/AllWarning_z0.wav new file mode 100644 index 0000000..31552ad Binary files /dev/null and b/content/AUDIO/AllWarning_z0.wav differ diff --git a/content/AUDIO/AllWarning_z1.wav b/content/AUDIO/AllWarning_z1.wav new file mode 100644 index 0000000..b500ee7 Binary files /dev/null and b/content/AUDIO/AllWarning_z1.wav differ diff --git a/content/AUDIO/AllWarning_z2.wav b/content/AUDIO/AllWarning_z2.wav new file mode 100644 index 0000000..c6fc47c Binary files /dev/null and b/content/AUDIO/AllWarning_z2.wav differ diff --git a/content/AUDIO/AllWarning_z3.wav b/content/AUDIO/AllWarning_z3.wav new file mode 100644 index 0000000..81ed9c8 Binary files /dev/null and b/content/AUDIO/AllWarning_z3.wav differ diff --git a/content/AUDIO/AllWarning_z4.wav b/content/AUDIO/AllWarning_z4.wav new file mode 100644 index 0000000..b9856c1 Binary files /dev/null and b/content/AUDIO/AllWarning_z4.wav differ diff --git a/content/AUDIO/AllWarning_z5.wav b/content/AUDIO/AllWarning_z5.wav new file mode 100644 index 0000000..d8d4325 Binary files /dev/null and b/content/AUDIO/AllWarning_z5.wav differ diff --git a/content/AUDIO/AllWarning_z6.wav b/content/AUDIO/AllWarning_z6.wav new file mode 100644 index 0000000..b46dcd4 Binary files /dev/null and b/content/AUDIO/AllWarning_z6.wav differ diff --git a/content/AUDIO/AutoCanonAction01_z0.wav b/content/AUDIO/AutoCanonAction01_z0.wav new file mode 100644 index 0000000..a40b5a9 Binary files /dev/null and b/content/AUDIO/AutoCanonAction01_z0.wav differ diff --git a/content/AUDIO/AutoCanonAction01_z1.wav b/content/AUDIO/AutoCanonAction01_z1.wav new file mode 100644 index 0000000..5b6315b Binary files /dev/null and b/content/AUDIO/AutoCanonAction01_z1.wav differ diff --git a/content/AUDIO/AutoCanonAction01_z2.wav b/content/AUDIO/AutoCanonAction01_z2.wav new file mode 100644 index 0000000..02f535f Binary files /dev/null and b/content/AUDIO/AutoCanonAction01_z2.wav differ diff --git a/content/AUDIO/AutoCanonFire01_z0.wav b/content/AUDIO/AutoCanonFire01_z0.wav new file mode 100644 index 0000000..4b82794 Binary files /dev/null and b/content/AUDIO/AutoCanonFire01_z0.wav differ diff --git a/content/AUDIO/AutoCanonFire01_z1.wav b/content/AUDIO/AutoCanonFire01_z1.wav new file mode 100644 index 0000000..102fa66 Binary files /dev/null and b/content/AUDIO/AutoCanonFire01_z1.wav differ diff --git a/content/AUDIO/AutoCanonFire01_z2.wav b/content/AUDIO/AutoCanonFire01_z2.wav new file mode 100644 index 0000000..7f87552 Binary files /dev/null and b/content/AUDIO/AutoCanonFire01_z2.wav differ diff --git a/content/AUDIO/AutoCanonFireExt01.wav b/content/AUDIO/AutoCanonFireExt01.wav new file mode 100644 index 0000000..102fa66 Binary files /dev/null and b/content/AUDIO/AutoCanonFireExt01.wav differ diff --git a/content/AUDIO/AutoCanonJam01_z0.wav b/content/AUDIO/AutoCanonJam01_z0.wav new file mode 100644 index 0000000..7c4dcf5 Binary files /dev/null and b/content/AUDIO/AutoCanonJam01_z0.wav differ diff --git a/content/AUDIO/AutoCanonJam01_z1.wav b/content/AUDIO/AutoCanonJam01_z1.wav new file mode 100644 index 0000000..a618083 Binary files /dev/null and b/content/AUDIO/AutoCanonJam01_z1.wav differ diff --git a/content/AUDIO/AutoCanonMisfire01_z0.wav b/content/AUDIO/AutoCanonMisfire01_z0.wav new file mode 100644 index 0000000..c0d22f6 Binary files /dev/null and b/content/AUDIO/AutoCanonMisfire01_z0.wav differ diff --git a/content/AUDIO/AutoCanonMisfire01_z1.wav b/content/AUDIO/AutoCanonMisfire01_z1.wav new file mode 100644 index 0000000..c00a907 Binary files /dev/null and b/content/AUDIO/AutoCanonMisfire01_z1.wav differ diff --git a/content/AUDIO/AuxExplosion01LOD2_z0.wav b/content/AUDIO/AuxExplosion01LOD2_z0.wav new file mode 100644 index 0000000..bf9bbfa Binary files /dev/null and b/content/AUDIO/AuxExplosion01LOD2_z0.wav differ diff --git a/content/AUDIO/AuxExplosion01LOD2_z1.wav b/content/AUDIO/AuxExplosion01LOD2_z1.wav new file mode 100644 index 0000000..2b3f2e2 Binary files /dev/null and b/content/AUDIO/AuxExplosion01LOD2_z1.wav differ diff --git a/content/AUDIO/AuxExplosion01LOD2_z2.wav b/content/AUDIO/AuxExplosion01LOD2_z2.wav new file mode 100644 index 0000000..4e226a3 Binary files /dev/null and b/content/AUDIO/AuxExplosion01LOD2_z2.wav differ diff --git a/content/AUDIO/AuxExplosion01LOD2_z3.wav b/content/AUDIO/AuxExplosion01LOD2_z3.wav new file mode 100644 index 0000000..bf58752 Binary files /dev/null and b/content/AUDIO/AuxExplosion01LOD2_z3.wav differ diff --git a/content/AUDIO/AuxExplosion01_z0.wav b/content/AUDIO/AuxExplosion01_z0.wav new file mode 100644 index 0000000..9413351 Binary files /dev/null and b/content/AUDIO/AuxExplosion01_z0.wav differ diff --git a/content/AUDIO/AuxExplosion01_z1.wav b/content/AUDIO/AuxExplosion01_z1.wav new file mode 100644 index 0000000..bf9bbfa Binary files /dev/null and b/content/AUDIO/AuxExplosion01_z1.wav differ diff --git a/content/AUDIO/AuxExplosion01_z2.wav b/content/AUDIO/AuxExplosion01_z2.wav new file mode 100644 index 0000000..556394a Binary files /dev/null and b/content/AUDIO/AuxExplosion01_z2.wav differ diff --git a/content/AUDIO/AuxExplosion01_z3.wav b/content/AUDIO/AuxExplosion01_z3.wav new file mode 100644 index 0000000..2b3f2e2 Binary files /dev/null and b/content/AUDIO/AuxExplosion01_z3.wav differ diff --git a/content/AUDIO/AuxExplosion01_z4.wav b/content/AUDIO/AuxExplosion01_z4.wav new file mode 100644 index 0000000..fac9f4f Binary files /dev/null and b/content/AUDIO/AuxExplosion01_z4.wav differ diff --git a/content/AUDIO/AuxExplosion01_z5.wav b/content/AUDIO/AuxExplosion01_z5.wav new file mode 100644 index 0000000..4e226a3 Binary files /dev/null and b/content/AUDIO/AuxExplosion01_z5.wav differ diff --git a/content/AUDIO/AuxExplosion01_z6.wav b/content/AUDIO/AuxExplosion01_z6.wav new file mode 100644 index 0000000..b1d6f23 Binary files /dev/null and b/content/AUDIO/AuxExplosion01_z6.wav differ diff --git a/content/AUDIO/AuxExplosion01_z7.wav b/content/AUDIO/AuxExplosion01_z7.wav new file mode 100644 index 0000000..bf58752 Binary files /dev/null and b/content/AUDIO/AuxExplosion01_z7.wav differ diff --git a/content/AUDIO/BasicBeep01.wav b/content/AUDIO/BasicBeep01.wav new file mode 100644 index 0000000..1bffbe8 Binary files /dev/null and b/content/AUDIO/BasicBeep01.wav differ diff --git a/content/AUDIO/BasicBeep02.wav b/content/AUDIO/BasicBeep02.wav new file mode 100644 index 0000000..3b6bb16 Binary files /dev/null and b/content/AUDIO/BasicBeep02.wav differ diff --git a/content/AUDIO/BasicClick01.wav b/content/AUDIO/BasicClick01.wav new file mode 100644 index 0000000..35bc25b Binary files /dev/null and b/content/AUDIO/BasicClick01.wav differ diff --git a/content/AUDIO/BigExpInt01_z0.wav b/content/AUDIO/BigExpInt01_z0.wav new file mode 100644 index 0000000..a97dcdf Binary files /dev/null and b/content/AUDIO/BigExpInt01_z0.wav differ diff --git a/content/AUDIO/BigExpInt01_z1.wav b/content/AUDIO/BigExpInt01_z1.wav new file mode 100644 index 0000000..f8e7bea Binary files /dev/null and b/content/AUDIO/BigExpInt01_z1.wav differ diff --git a/content/AUDIO/BigExpInt01_z2.wav b/content/AUDIO/BigExpInt01_z2.wav new file mode 100644 index 0000000..cd6ad54 Binary files /dev/null and b/content/AUDIO/BigExpInt01_z2.wav differ diff --git a/content/AUDIO/BigExpInt01_z3.wav b/content/AUDIO/BigExpInt01_z3.wav new file mode 100644 index 0000000..ee59bf7 Binary files /dev/null and b/content/AUDIO/BigExpInt01_z3.wav differ diff --git a/content/AUDIO/BigExpInt02_z0.wav b/content/AUDIO/BigExpInt02_z0.wav new file mode 100644 index 0000000..f55b520 Binary files /dev/null and b/content/AUDIO/BigExpInt02_z0.wav differ diff --git a/content/AUDIO/BigExpInt02_z1.wav b/content/AUDIO/BigExpInt02_z1.wav new file mode 100644 index 0000000..57d0bc9 Binary files /dev/null and b/content/AUDIO/BigExpInt02_z1.wav differ diff --git a/content/AUDIO/BigExpInt02_z2.wav b/content/AUDIO/BigExpInt02_z2.wav new file mode 100644 index 0000000..929b6c1 Binary files /dev/null and b/content/AUDIO/BigExpInt02_z2.wav differ diff --git a/content/AUDIO/BigExpInt02_z3.wav b/content/AUDIO/BigExpInt02_z3.wav new file mode 100644 index 0000000..05cf476 Binary files /dev/null and b/content/AUDIO/BigExpInt02_z3.wav differ diff --git a/content/AUDIO/BigExpInt03_z0.wav b/content/AUDIO/BigExpInt03_z0.wav new file mode 100644 index 0000000..096d6d5 Binary files /dev/null and b/content/AUDIO/BigExpInt03_z0.wav differ diff --git a/content/AUDIO/BigExpInt03_z1.wav b/content/AUDIO/BigExpInt03_z1.wav new file mode 100644 index 0000000..72b4b19 Binary files /dev/null and b/content/AUDIO/BigExpInt03_z1.wav differ diff --git a/content/AUDIO/BigExpInt03_z2.wav b/content/AUDIO/BigExpInt03_z2.wav new file mode 100644 index 0000000..2c219b7 Binary files /dev/null and b/content/AUDIO/BigExpInt03_z2.wav differ diff --git a/content/AUDIO/BigExpInt03_z3.wav b/content/AUDIO/BigExpInt03_z3.wav new file mode 100644 index 0000000..80c1d4c Binary files /dev/null and b/content/AUDIO/BigExpInt03_z3.wav differ diff --git a/content/AUDIO/BigExpInt04_z0.wav b/content/AUDIO/BigExpInt04_z0.wav new file mode 100644 index 0000000..adc4909 Binary files /dev/null and b/content/AUDIO/BigExpInt04_z0.wav differ diff --git a/content/AUDIO/BigExpInt04_z1.wav b/content/AUDIO/BigExpInt04_z1.wav new file mode 100644 index 0000000..6fd419f Binary files /dev/null and b/content/AUDIO/BigExpInt04_z1.wav differ diff --git a/content/AUDIO/BigExpInt04_z2.wav b/content/AUDIO/BigExpInt04_z2.wav new file mode 100644 index 0000000..0a3162f Binary files /dev/null and b/content/AUDIO/BigExpInt04_z2.wav differ diff --git a/content/AUDIO/BigExpInt04_z3.wav b/content/AUDIO/BigExpInt04_z3.wav new file mode 100644 index 0000000..0c71e88 Binary files /dev/null and b/content/AUDIO/BigExpInt04_z3.wav differ diff --git a/content/AUDIO/BigExpInt05_z0.wav b/content/AUDIO/BigExpInt05_z0.wav new file mode 100644 index 0000000..adc4909 Binary files /dev/null and b/content/AUDIO/BigExpInt05_z0.wav differ diff --git a/content/AUDIO/BigExpInt05_z1.wav b/content/AUDIO/BigExpInt05_z1.wav new file mode 100644 index 0000000..2f44617 Binary files /dev/null and b/content/AUDIO/BigExpInt05_z1.wav differ diff --git a/content/AUDIO/BigExpInt05_z2.wav b/content/AUDIO/BigExpInt05_z2.wav new file mode 100644 index 0000000..8dad7b9 Binary files /dev/null and b/content/AUDIO/BigExpInt05_z2.wav differ diff --git a/content/AUDIO/BigExpInt05_z3.wav b/content/AUDIO/BigExpInt05_z3.wav new file mode 100644 index 0000000..2dbcf22 Binary files /dev/null and b/content/AUDIO/BigExpInt05_z3.wav differ diff --git a/content/AUDIO/BigExplosion01LOD2_z0.wav b/content/AUDIO/BigExplosion01LOD2_z0.wav new file mode 100644 index 0000000..79f7f83 Binary files /dev/null and b/content/AUDIO/BigExplosion01LOD2_z0.wav differ diff --git a/content/AUDIO/BigExplosion01LOD2_z1.wav b/content/AUDIO/BigExplosion01LOD2_z1.wav new file mode 100644 index 0000000..8ced7b8 Binary files /dev/null and b/content/AUDIO/BigExplosion01LOD2_z1.wav differ diff --git a/content/AUDIO/BigExplosion01_z0.wav b/content/AUDIO/BigExplosion01_z0.wav new file mode 100644 index 0000000..ee0e0af Binary files /dev/null and b/content/AUDIO/BigExplosion01_z0.wav differ diff --git a/content/AUDIO/BigExplosion01_z1.wav b/content/AUDIO/BigExplosion01_z1.wav new file mode 100644 index 0000000..f602657 Binary files /dev/null and b/content/AUDIO/BigExplosion01_z1.wav differ diff --git a/content/AUDIO/BigExplosion01_z2.wav b/content/AUDIO/BigExplosion01_z2.wav new file mode 100644 index 0000000..8ced7b8 Binary files /dev/null and b/content/AUDIO/BigExplosion01_z2.wav differ diff --git a/content/AUDIO/BigExplosion01_z3.wav b/content/AUDIO/BigExplosion01_z3.wav new file mode 100644 index 0000000..cd6ad54 Binary files /dev/null and b/content/AUDIO/BigExplosion01_z3.wav differ diff --git a/content/AUDIO/BigExplosion02LOD2_z0.wav b/content/AUDIO/BigExplosion02LOD2_z0.wav new file mode 100644 index 0000000..1ccbb1f Binary files /dev/null and b/content/AUDIO/BigExplosion02LOD2_z0.wav differ diff --git a/content/AUDIO/BigExplosion02LOD2_z1.wav b/content/AUDIO/BigExplosion02LOD2_z1.wav new file mode 100644 index 0000000..b2298ef Binary files /dev/null and b/content/AUDIO/BigExplosion02LOD2_z1.wav differ diff --git a/content/AUDIO/BigExplosion02_z0.wav b/content/AUDIO/BigExplosion02_z0.wav new file mode 100644 index 0000000..c027bb1 Binary files /dev/null and b/content/AUDIO/BigExplosion02_z0.wav differ diff --git a/content/AUDIO/BigExplosion02_z1.wav b/content/AUDIO/BigExplosion02_z1.wav new file mode 100644 index 0000000..1ccbb1f Binary files /dev/null and b/content/AUDIO/BigExplosion02_z1.wav differ diff --git a/content/AUDIO/BigExplosion02_z2.wav b/content/AUDIO/BigExplosion02_z2.wav new file mode 100644 index 0000000..8b6e0b6 Binary files /dev/null and b/content/AUDIO/BigExplosion02_z2.wav differ diff --git a/content/AUDIO/BigExplosion02_z3.wav b/content/AUDIO/BigExplosion02_z3.wav new file mode 100644 index 0000000..b2298ef Binary files /dev/null and b/content/AUDIO/BigExplosion02_z3.wav differ diff --git a/content/AUDIO/BigExplosion03LOD2_z0.wav b/content/AUDIO/BigExplosion03LOD2_z0.wav new file mode 100644 index 0000000..086ddea Binary files /dev/null and b/content/AUDIO/BigExplosion03LOD2_z0.wav differ diff --git a/content/AUDIO/BigExplosion03LOD2_z1.wav b/content/AUDIO/BigExplosion03LOD2_z1.wav new file mode 100644 index 0000000..a1c446d Binary files /dev/null and b/content/AUDIO/BigExplosion03LOD2_z1.wav differ diff --git a/content/AUDIO/BigExplosion03_z0.wav b/content/AUDIO/BigExplosion03_z0.wav new file mode 100644 index 0000000..ee0e0af Binary files /dev/null and b/content/AUDIO/BigExplosion03_z0.wav differ diff --git a/content/AUDIO/BigExplosion03_z1.wav b/content/AUDIO/BigExplosion03_z1.wav new file mode 100644 index 0000000..086ddea Binary files /dev/null and b/content/AUDIO/BigExplosion03_z1.wav differ diff --git a/content/AUDIO/BigExplosion03_z2.wav b/content/AUDIO/BigExplosion03_z2.wav new file mode 100644 index 0000000..a1c446d Binary files /dev/null and b/content/AUDIO/BigExplosion03_z2.wav differ diff --git a/content/AUDIO/BigExplosion03_z3.wav b/content/AUDIO/BigExplosion03_z3.wav new file mode 100644 index 0000000..9fea9a1 Binary files /dev/null and b/content/AUDIO/BigExplosion03_z3.wav differ diff --git a/content/AUDIO/BigExplosion04LOD2_z0.wav b/content/AUDIO/BigExplosion04LOD2_z0.wav new file mode 100644 index 0000000..cedd551 Binary files /dev/null and b/content/AUDIO/BigExplosion04LOD2_z0.wav differ diff --git a/content/AUDIO/BigExplosion04LOD2_z1.wav b/content/AUDIO/BigExplosion04LOD2_z1.wav new file mode 100644 index 0000000..f38726e Binary files /dev/null and b/content/AUDIO/BigExplosion04LOD2_z1.wav differ diff --git a/content/AUDIO/BigExplosion04_z0.wav b/content/AUDIO/BigExplosion04_z0.wav new file mode 100644 index 0000000..adc4909 Binary files /dev/null and b/content/AUDIO/BigExplosion04_z0.wav differ diff --git a/content/AUDIO/BigExplosion04_z1.wav b/content/AUDIO/BigExplosion04_z1.wav new file mode 100644 index 0000000..cedd551 Binary files /dev/null and b/content/AUDIO/BigExplosion04_z1.wav differ diff --git a/content/AUDIO/BigExplosion04_z2.wav b/content/AUDIO/BigExplosion04_z2.wav new file mode 100644 index 0000000..f38726e Binary files /dev/null and b/content/AUDIO/BigExplosion04_z2.wav differ diff --git a/content/AUDIO/BigExplosion04_z3.wav b/content/AUDIO/BigExplosion04_z3.wav new file mode 100644 index 0000000..fb31211 Binary files /dev/null and b/content/AUDIO/BigExplosion04_z3.wav differ diff --git a/content/AUDIO/BigExplosion05LOD2_z0.wav b/content/AUDIO/BigExplosion05LOD2_z0.wav new file mode 100644 index 0000000..efd0c16 Binary files /dev/null and b/content/AUDIO/BigExplosion05LOD2_z0.wav differ diff --git a/content/AUDIO/BigExplosion05LOD2_z1.wav b/content/AUDIO/BigExplosion05LOD2_z1.wav new file mode 100644 index 0000000..d137952 Binary files /dev/null and b/content/AUDIO/BigExplosion05LOD2_z1.wav differ diff --git a/content/AUDIO/BigExplosion05_z0.wav b/content/AUDIO/BigExplosion05_z0.wav new file mode 100644 index 0000000..adc4909 Binary files /dev/null and b/content/AUDIO/BigExplosion05_z0.wav differ diff --git a/content/AUDIO/BigExplosion05_z1.wav b/content/AUDIO/BigExplosion05_z1.wav new file mode 100644 index 0000000..efd0c16 Binary files /dev/null and b/content/AUDIO/BigExplosion05_z1.wav differ diff --git a/content/AUDIO/BigExplosion05_z2.wav b/content/AUDIO/BigExplosion05_z2.wav new file mode 100644 index 0000000..d137952 Binary files /dev/null and b/content/AUDIO/BigExplosion05_z2.wav differ diff --git a/content/AUDIO/BigExplosion05_z3.wav b/content/AUDIO/BigExplosion05_z3.wav new file mode 100644 index 0000000..7e28e84 Binary files /dev/null and b/content/AUDIO/BigExplosion05_z3.wav differ diff --git a/content/AUDIO/Collision01_z0.wav b/content/AUDIO/Collision01_z0.wav new file mode 100644 index 0000000..e9f4b06 Binary files /dev/null and b/content/AUDIO/Collision01_z0.wav differ diff --git a/content/AUDIO/Collision01_z1.wav b/content/AUDIO/Collision01_z1.wav new file mode 100644 index 0000000..ca115d4 Binary files /dev/null and b/content/AUDIO/Collision01_z1.wav differ diff --git a/content/AUDIO/Collision01_z2.wav b/content/AUDIO/Collision01_z2.wav new file mode 100644 index 0000000..ecbe3a9 Binary files /dev/null and b/content/AUDIO/Collision01_z2.wav differ diff --git a/content/AUDIO/Collision02_z0.wav b/content/AUDIO/Collision02_z0.wav new file mode 100644 index 0000000..2488177 Binary files /dev/null and b/content/AUDIO/Collision02_z0.wav differ diff --git a/content/AUDIO/Collision02_z1.wav b/content/AUDIO/Collision02_z1.wav new file mode 100644 index 0000000..a01c615 Binary files /dev/null and b/content/AUDIO/Collision02_z1.wav differ diff --git a/content/AUDIO/Collision02_z2.wav b/content/AUDIO/Collision02_z2.wav new file mode 100644 index 0000000..667720a Binary files /dev/null and b/content/AUDIO/Collision02_z2.wav differ diff --git a/content/AUDIO/Collision03_z0.wav b/content/AUDIO/Collision03_z0.wav new file mode 100644 index 0000000..1235f9f Binary files /dev/null and b/content/AUDIO/Collision03_z0.wav differ diff --git a/content/AUDIO/Collision03_z1.wav b/content/AUDIO/Collision03_z1.wav new file mode 100644 index 0000000..e1f01e9 Binary files /dev/null and b/content/AUDIO/Collision03_z1.wav differ diff --git a/content/AUDIO/Collision03_z2.wav b/content/AUDIO/Collision03_z2.wav new file mode 100644 index 0000000..52dce36 Binary files /dev/null and b/content/AUDIO/Collision03_z2.wav differ diff --git a/content/AUDIO/Collision04_z0.wav b/content/AUDIO/Collision04_z0.wav new file mode 100644 index 0000000..64330cc Binary files /dev/null and b/content/AUDIO/Collision04_z0.wav differ diff --git a/content/AUDIO/Collision04_z1.wav b/content/AUDIO/Collision04_z1.wav new file mode 100644 index 0000000..767dd51 Binary files /dev/null and b/content/AUDIO/Collision04_z1.wav differ diff --git a/content/AUDIO/Collision04_z2.wav b/content/AUDIO/Collision04_z2.wav new file mode 100644 index 0000000..319f0b4 Binary files /dev/null and b/content/AUDIO/Collision04_z2.wav differ diff --git a/content/AUDIO/Collision05_z0.wav b/content/AUDIO/Collision05_z0.wav new file mode 100644 index 0000000..49427c7 Binary files /dev/null and b/content/AUDIO/Collision05_z0.wav differ diff --git a/content/AUDIO/Collision05_z1.wav b/content/AUDIO/Collision05_z1.wav new file mode 100644 index 0000000..73593ea Binary files /dev/null and b/content/AUDIO/Collision05_z1.wav differ diff --git a/content/AUDIO/Collision05_z2.wav b/content/AUDIO/Collision05_z2.wav new file mode 100644 index 0000000..e9dac44 Binary files /dev/null and b/content/AUDIO/Collision05_z2.wav differ diff --git a/content/AUDIO/Collision06_z0.wav b/content/AUDIO/Collision06_z0.wav new file mode 100644 index 0000000..d453c7c Binary files /dev/null and b/content/AUDIO/Collision06_z0.wav differ diff --git a/content/AUDIO/Collision06_z1.wav b/content/AUDIO/Collision06_z1.wav new file mode 100644 index 0000000..b3e2c54 Binary files /dev/null and b/content/AUDIO/Collision06_z1.wav differ diff --git a/content/AUDIO/Collision06_z2.wav b/content/AUDIO/Collision06_z2.wav new file mode 100644 index 0000000..e164c1c Binary files /dev/null and b/content/AUDIO/Collision06_z2.wav differ diff --git a/content/AUDIO/Collision07_z0.wav b/content/AUDIO/Collision07_z0.wav new file mode 100644 index 0000000..3806712 Binary files /dev/null and b/content/AUDIO/Collision07_z0.wav differ diff --git a/content/AUDIO/Collision07_z1.wav b/content/AUDIO/Collision07_z1.wav new file mode 100644 index 0000000..b805880 Binary files /dev/null and b/content/AUDIO/Collision07_z1.wav differ diff --git a/content/AUDIO/Collision07_z2.wav b/content/AUDIO/Collision07_z2.wav new file mode 100644 index 0000000..e1f01e9 Binary files /dev/null and b/content/AUDIO/Collision07_z2.wav differ diff --git a/content/AUDIO/Collision08_z0.wav b/content/AUDIO/Collision08_z0.wav new file mode 100644 index 0000000..9274dc2 Binary files /dev/null and b/content/AUDIO/Collision08_z0.wav differ diff --git a/content/AUDIO/Collision08_z1.wav b/content/AUDIO/Collision08_z1.wav new file mode 100644 index 0000000..a01c615 Binary files /dev/null and b/content/AUDIO/Collision08_z1.wav differ diff --git a/content/AUDIO/Collision08_z2.wav b/content/AUDIO/Collision08_z2.wav new file mode 100644 index 0000000..e7a6d5c Binary files /dev/null and b/content/AUDIO/Collision08_z2.wav differ diff --git a/content/AUDIO/Collision09_z0.wav b/content/AUDIO/Collision09_z0.wav new file mode 100644 index 0000000..3671575 Binary files /dev/null and b/content/AUDIO/Collision09_z0.wav differ diff --git a/content/AUDIO/Collision09_z1.wav b/content/AUDIO/Collision09_z1.wav new file mode 100644 index 0000000..82a9e33 Binary files /dev/null and b/content/AUDIO/Collision09_z1.wav differ diff --git a/content/AUDIO/Collision09_z2.wav b/content/AUDIO/Collision09_z2.wav new file mode 100644 index 0000000..d2cf4a3 Binary files /dev/null and b/content/AUDIO/Collision09_z2.wav differ diff --git a/content/AUDIO/Collision10_z0.wav b/content/AUDIO/Collision10_z0.wav new file mode 100644 index 0000000..7841211 Binary files /dev/null and b/content/AUDIO/Collision10_z0.wav differ diff --git a/content/AUDIO/Collision10_z1.wav b/content/AUDIO/Collision10_z1.wav new file mode 100644 index 0000000..f374449 Binary files /dev/null and b/content/AUDIO/Collision10_z1.wav differ diff --git a/content/AUDIO/Collision10_z2.wav b/content/AUDIO/Collision10_z2.wav new file mode 100644 index 0000000..d236eb6 Binary files /dev/null and b/content/AUDIO/Collision10_z2.wav differ diff --git a/content/AUDIO/Collision11_z0.wav b/content/AUDIO/Collision11_z0.wav new file mode 100644 index 0000000..1d03b5c Binary files /dev/null and b/content/AUDIO/Collision11_z0.wav differ diff --git a/content/AUDIO/Collision11_z1.wav b/content/AUDIO/Collision11_z1.wav new file mode 100644 index 0000000..49b60be Binary files /dev/null and b/content/AUDIO/Collision11_z1.wav differ diff --git a/content/AUDIO/Collision11_z2.wav b/content/AUDIO/Collision11_z2.wav new file mode 100644 index 0000000..95a7c7d Binary files /dev/null and b/content/AUDIO/Collision11_z2.wav differ diff --git a/content/AUDIO/CollisionExt01LOD2.wav b/content/AUDIO/CollisionExt01LOD2.wav new file mode 100644 index 0000000..ca115d4 Binary files /dev/null and b/content/AUDIO/CollisionExt01LOD2.wav differ diff --git a/content/AUDIO/CollisionExt01_z0.wav b/content/AUDIO/CollisionExt01_z0.wav new file mode 100644 index 0000000..eb5e659 Binary files /dev/null and b/content/AUDIO/CollisionExt01_z0.wav differ diff --git a/content/AUDIO/CollisionExt01_z1.wav b/content/AUDIO/CollisionExt01_z1.wav new file mode 100644 index 0000000..4409419 Binary files /dev/null and b/content/AUDIO/CollisionExt01_z1.wav differ diff --git a/content/AUDIO/CollisionExt02.wav b/content/AUDIO/CollisionExt02.wav new file mode 100644 index 0000000..e6dc0fe Binary files /dev/null and b/content/AUDIO/CollisionExt02.wav differ diff --git a/content/AUDIO/CollisionExt02LOD2.wav b/content/AUDIO/CollisionExt02LOD2.wav new file mode 100644 index 0000000..64330cc Binary files /dev/null and b/content/AUDIO/CollisionExt02LOD2.wav differ diff --git a/content/AUDIO/CollisionExt03LOD2.wav b/content/AUDIO/CollisionExt03LOD2.wav new file mode 100644 index 0000000..49427c7 Binary files /dev/null and b/content/AUDIO/CollisionExt03LOD2.wav differ diff --git a/content/AUDIO/CollisionExt03_z0.wav b/content/AUDIO/CollisionExt03_z0.wav new file mode 100644 index 0000000..1cddd92 Binary files /dev/null and b/content/AUDIO/CollisionExt03_z0.wav differ diff --git a/content/AUDIO/CollisionExt03_z1.wav b/content/AUDIO/CollisionExt03_z1.wav new file mode 100644 index 0000000..ff2bd0a Binary files /dev/null and b/content/AUDIO/CollisionExt03_z1.wav differ diff --git a/content/AUDIO/CollisionExt04LOD2.wav b/content/AUDIO/CollisionExt04LOD2.wav new file mode 100644 index 0000000..d453c7c Binary files /dev/null and b/content/AUDIO/CollisionExt04LOD2.wav differ diff --git a/content/AUDIO/CollisionExt04_z0.wav b/content/AUDIO/CollisionExt04_z0.wav new file mode 100644 index 0000000..4e3ec0b Binary files /dev/null and b/content/AUDIO/CollisionExt04_z0.wav differ diff --git a/content/AUDIO/CollisionExt04_z1.wav b/content/AUDIO/CollisionExt04_z1.wav new file mode 100644 index 0000000..423f999 Binary files /dev/null and b/content/AUDIO/CollisionExt04_z1.wav differ diff --git a/content/AUDIO/CollisionExt05LOD2.wav b/content/AUDIO/CollisionExt05LOD2.wav new file mode 100644 index 0000000..3806712 Binary files /dev/null and b/content/AUDIO/CollisionExt05LOD2.wav differ diff --git a/content/AUDIO/CollisionExt05_z0.wav b/content/AUDIO/CollisionExt05_z0.wav new file mode 100644 index 0000000..60e8707 Binary files /dev/null and b/content/AUDIO/CollisionExt05_z0.wav differ diff --git a/content/AUDIO/CollisionExt05_z1.wav b/content/AUDIO/CollisionExt05_z1.wav new file mode 100644 index 0000000..afabb67 Binary files /dev/null and b/content/AUDIO/CollisionExt05_z1.wav differ diff --git a/content/AUDIO/CollisionExt06LOD2.wav b/content/AUDIO/CollisionExt06LOD2.wav new file mode 100644 index 0000000..9274dc2 Binary files /dev/null and b/content/AUDIO/CollisionExt06LOD2.wav differ diff --git a/content/AUDIO/CollisionExt06_z0.wav b/content/AUDIO/CollisionExt06_z0.wav new file mode 100644 index 0000000..b893dc9 Binary files /dev/null and b/content/AUDIO/CollisionExt06_z0.wav differ diff --git a/content/AUDIO/CollisionExt06_z1.wav b/content/AUDIO/CollisionExt06_z1.wav new file mode 100644 index 0000000..d709a5e Binary files /dev/null and b/content/AUDIO/CollisionExt06_z1.wav differ diff --git a/content/AUDIO/CollisionExt07LOD2.wav b/content/AUDIO/CollisionExt07LOD2.wav new file mode 100644 index 0000000..d2cf4a3 Binary files /dev/null and b/content/AUDIO/CollisionExt07LOD2.wav differ diff --git a/content/AUDIO/CollisionExt07_z0.wav b/content/AUDIO/CollisionExt07_z0.wav new file mode 100644 index 0000000..4da69ff Binary files /dev/null and b/content/AUDIO/CollisionExt07_z0.wav differ diff --git a/content/AUDIO/CollisionExt07_z1.wav b/content/AUDIO/CollisionExt07_z1.wav new file mode 100644 index 0000000..c6f40aa Binary files /dev/null and b/content/AUDIO/CollisionExt07_z1.wav differ diff --git a/content/AUDIO/CollisionExt08_z0.wav b/content/AUDIO/CollisionExt08_z0.wav new file mode 100644 index 0000000..79c8984 Binary files /dev/null and b/content/AUDIO/CollisionExt08_z0.wav differ diff --git a/content/AUDIO/CollisionExt08_z1.wav b/content/AUDIO/CollisionExt08_z1.wav new file mode 100644 index 0000000..c41fbc1 Binary files /dev/null and b/content/AUDIO/CollisionExt08_z1.wav differ diff --git a/content/AUDIO/CollisionExt09_z0.wav b/content/AUDIO/CollisionExt09_z0.wav new file mode 100644 index 0000000..ef3a586 Binary files /dev/null and b/content/AUDIO/CollisionExt09_z0.wav differ diff --git a/content/AUDIO/CollisionExt09_z1.wav b/content/AUDIO/CollisionExt09_z1.wav new file mode 100644 index 0000000..22baeda Binary files /dev/null and b/content/AUDIO/CollisionExt09_z1.wav differ diff --git a/content/AUDIO/CoolantDump01_z0.wav b/content/AUDIO/CoolantDump01_z0.wav new file mode 100644 index 0000000..03cf508 Binary files /dev/null and b/content/AUDIO/CoolantDump01_z0.wav differ diff --git a/content/AUDIO/CoolantDump01_z1.wav b/content/AUDIO/CoolantDump01_z1.wav new file mode 100644 index 0000000..12014ad Binary files /dev/null and b/content/AUDIO/CoolantDump01_z1.wav differ diff --git a/content/AUDIO/CoolantDump01_z2.wav b/content/AUDIO/CoolantDump01_z2.wav new file mode 100644 index 0000000..e804a49 Binary files /dev/null and b/content/AUDIO/CoolantDump01_z2.wav differ diff --git a/content/AUDIO/CoolantDump02_z0.wav b/content/AUDIO/CoolantDump02_z0.wav new file mode 100644 index 0000000..03cf508 Binary files /dev/null and b/content/AUDIO/CoolantDump02_z0.wav differ diff --git a/content/AUDIO/CoolantDump02_z1.wav b/content/AUDIO/CoolantDump02_z1.wav new file mode 100644 index 0000000..23bb134 Binary files /dev/null and b/content/AUDIO/CoolantDump02_z1.wav differ diff --git a/content/AUDIO/CoolantDump02_z2.wav b/content/AUDIO/CoolantDump02_z2.wav new file mode 100644 index 0000000..e804a49 Binary files /dev/null and b/content/AUDIO/CoolantDump02_z2.wav differ diff --git a/content/AUDIO/CoolantDump03_z0.wav b/content/AUDIO/CoolantDump03_z0.wav new file mode 100644 index 0000000..36d6960 Binary files /dev/null and b/content/AUDIO/CoolantDump03_z0.wav differ diff --git a/content/AUDIO/CoolantDump03_z1.wav b/content/AUDIO/CoolantDump03_z1.wav new file mode 100644 index 0000000..206af4e Binary files /dev/null and b/content/AUDIO/CoolantDump03_z1.wav differ diff --git a/content/AUDIO/CoolantDump04_z0.wav b/content/AUDIO/CoolantDump04_z0.wav new file mode 100644 index 0000000..36d6960 Binary files /dev/null and b/content/AUDIO/CoolantDump04_z0.wav differ diff --git a/content/AUDIO/CoolantDump04_z1.wav b/content/AUDIO/CoolantDump04_z1.wav new file mode 100644 index 0000000..206af4e Binary files /dev/null and b/content/AUDIO/CoolantDump04_z1.wav differ diff --git a/content/AUDIO/CoolantDump04_z2.wav b/content/AUDIO/CoolantDump04_z2.wav new file mode 100644 index 0000000..dfa6c74 Binary files /dev/null and b/content/AUDIO/CoolantDump04_z2.wav differ diff --git a/content/AUDIO/CoolantDump05_z0.wav b/content/AUDIO/CoolantDump05_z0.wav new file mode 100644 index 0000000..57831f2 Binary files /dev/null and b/content/AUDIO/CoolantDump05_z0.wav differ diff --git a/content/AUDIO/CoolantDump05_z1.wav b/content/AUDIO/CoolantDump05_z1.wav new file mode 100644 index 0000000..6ece528 Binary files /dev/null and b/content/AUDIO/CoolantDump05_z1.wav differ diff --git a/content/AUDIO/CoolantDump05_z2.wav b/content/AUDIO/CoolantDump05_z2.wav new file mode 100644 index 0000000..a5a7380 Binary files /dev/null and b/content/AUDIO/CoolantDump05_z2.wav differ diff --git a/content/AUDIO/CoolantDumpExt01.wav b/content/AUDIO/CoolantDumpExt01.wav new file mode 100644 index 0000000..cebd1f3 Binary files /dev/null and b/content/AUDIO/CoolantDumpExt01.wav differ diff --git a/content/AUDIO/CoolantDumpStart01.wav b/content/AUDIO/CoolantDumpStart01.wav new file mode 100644 index 0000000..03cf508 Binary files /dev/null and b/content/AUDIO/CoolantDumpStart01.wav differ diff --git a/content/AUDIO/CoolantLeak01.wav b/content/AUDIO/CoolantLeak01.wav new file mode 100644 index 0000000..c95d355 Binary files /dev/null and b/content/AUDIO/CoolantLeak01.wav differ diff --git a/content/AUDIO/CoolantPresDcr01_z0.wav b/content/AUDIO/CoolantPresDcr01_z0.wav new file mode 100644 index 0000000..445f535 Binary files /dev/null and b/content/AUDIO/CoolantPresDcr01_z0.wav differ diff --git a/content/AUDIO/CoolantPresDcr01_z1.wav b/content/AUDIO/CoolantPresDcr01_z1.wav new file mode 100644 index 0000000..317a618 Binary files /dev/null and b/content/AUDIO/CoolantPresDcr01_z1.wav differ diff --git a/content/AUDIO/CoolantPresDcr01_z2.wav b/content/AUDIO/CoolantPresDcr01_z2.wav new file mode 100644 index 0000000..4951e42 Binary files /dev/null and b/content/AUDIO/CoolantPresDcr01_z2.wav differ diff --git a/content/AUDIO/CoolantPresDcr02_z0.wav b/content/AUDIO/CoolantPresDcr02_z0.wav new file mode 100644 index 0000000..445f535 Binary files /dev/null and b/content/AUDIO/CoolantPresDcr02_z0.wav differ diff --git a/content/AUDIO/CoolantPresDcr02_z1.wav b/content/AUDIO/CoolantPresDcr02_z1.wav new file mode 100644 index 0000000..65729c8 Binary files /dev/null and b/content/AUDIO/CoolantPresDcr02_z1.wav differ diff --git a/content/AUDIO/CoolantPresDcr03_z0.wav b/content/AUDIO/CoolantPresDcr03_z0.wav new file mode 100644 index 0000000..6d991f0 Binary files /dev/null and b/content/AUDIO/CoolantPresDcr03_z0.wav differ diff --git a/content/AUDIO/CoolantPresDcr03_z1.wav b/content/AUDIO/CoolantPresDcr03_z1.wav new file mode 100644 index 0000000..65729c8 Binary files /dev/null and b/content/AUDIO/CoolantPresDcr03_z1.wav differ diff --git a/content/AUDIO/CoolantPresDcr03_z2.wav b/content/AUDIO/CoolantPresDcr03_z2.wav new file mode 100644 index 0000000..aac1bcb Binary files /dev/null and b/content/AUDIO/CoolantPresDcr03_z2.wav differ diff --git a/content/AUDIO/CoolantPresInc01_z0.wav b/content/AUDIO/CoolantPresInc01_z0.wav new file mode 100644 index 0000000..644e36a Binary files /dev/null and b/content/AUDIO/CoolantPresInc01_z0.wav differ diff --git a/content/AUDIO/CoolantPresInc01_z1.wav b/content/AUDIO/CoolantPresInc01_z1.wav new file mode 100644 index 0000000..d2c52bb Binary files /dev/null and b/content/AUDIO/CoolantPresInc01_z1.wav differ diff --git a/content/AUDIO/CoolantPresInc01_z2.wav b/content/AUDIO/CoolantPresInc01_z2.wav new file mode 100644 index 0000000..017e56d Binary files /dev/null and b/content/AUDIO/CoolantPresInc01_z2.wav differ diff --git a/content/AUDIO/CoolantPresInc02_z0.wav b/content/AUDIO/CoolantPresInc02_z0.wav new file mode 100644 index 0000000..644e36a Binary files /dev/null and b/content/AUDIO/CoolantPresInc02_z0.wav differ diff --git a/content/AUDIO/CoolantPresInc02_z1.wav b/content/AUDIO/CoolantPresInc02_z1.wav new file mode 100644 index 0000000..65729c8 Binary files /dev/null and b/content/AUDIO/CoolantPresInc02_z1.wav differ diff --git a/content/AUDIO/CoolantPresInc03_z0.wav b/content/AUDIO/CoolantPresInc03_z0.wav new file mode 100644 index 0000000..87d4f98 Binary files /dev/null and b/content/AUDIO/CoolantPresInc03_z0.wav differ diff --git a/content/AUDIO/CoolantPresInc03_z1.wav b/content/AUDIO/CoolantPresInc03_z1.wav new file mode 100644 index 0000000..65729c8 Binary files /dev/null and b/content/AUDIO/CoolantPresInc03_z1.wav differ diff --git a/content/AUDIO/CoolantPresInc03_z2.wav b/content/AUDIO/CoolantPresInc03_z2.wav new file mode 100644 index 0000000..aac1bcb Binary files /dev/null and b/content/AUDIO/CoolantPresInc03_z2.wav differ diff --git a/content/AUDIO/Crunch01LOD2.wav b/content/AUDIO/Crunch01LOD2.wav new file mode 100644 index 0000000..94150ed Binary files /dev/null and b/content/AUDIO/Crunch01LOD2.wav differ diff --git a/content/AUDIO/Crunch01_z0.wav b/content/AUDIO/Crunch01_z0.wav new file mode 100644 index 0000000..2445429 Binary files /dev/null and b/content/AUDIO/Crunch01_z0.wav differ diff --git a/content/AUDIO/Crunch01_z1.wav b/content/AUDIO/Crunch01_z1.wav new file mode 100644 index 0000000..20c98f2 Binary files /dev/null and b/content/AUDIO/Crunch01_z1.wav differ diff --git a/content/AUDIO/Crunch01_z2.wav b/content/AUDIO/Crunch01_z2.wav new file mode 100644 index 0000000..94150ed Binary files /dev/null and b/content/AUDIO/Crunch01_z2.wav differ diff --git a/content/AUDIO/Damage01.wav b/content/AUDIO/Damage01.wav new file mode 100644 index 0000000..655c2ec Binary files /dev/null and b/content/AUDIO/Damage01.wav differ diff --git a/content/AUDIO/Damage02.wav b/content/AUDIO/Damage02.wav new file mode 100644 index 0000000..06418d4 Binary files /dev/null and b/content/AUDIO/Damage02.wav differ diff --git a/content/AUDIO/Damage03.wav b/content/AUDIO/Damage03.wav new file mode 100644 index 0000000..b458ee9 Binary files /dev/null and b/content/AUDIO/Damage03.wav differ diff --git a/content/AUDIO/Death01_z0.wav b/content/AUDIO/Death01_z0.wav new file mode 100644 index 0000000..3123ae1 Binary files /dev/null and b/content/AUDIO/Death01_z0.wav differ diff --git a/content/AUDIO/Death01_z1.wav b/content/AUDIO/Death01_z1.wav new file mode 100644 index 0000000..3ad7c22 Binary files /dev/null and b/content/AUDIO/Death01_z1.wav differ diff --git a/content/AUDIO/Death01_z10.wav b/content/AUDIO/Death01_z10.wav new file mode 100644 index 0000000..f2b567f Binary files /dev/null and b/content/AUDIO/Death01_z10.wav differ diff --git a/content/AUDIO/Death01_z11.wav b/content/AUDIO/Death01_z11.wav new file mode 100644 index 0000000..144b952 Binary files /dev/null and b/content/AUDIO/Death01_z11.wav differ diff --git a/content/AUDIO/Death01_z12.wav b/content/AUDIO/Death01_z12.wav new file mode 100644 index 0000000..0fc0e83 Binary files /dev/null and b/content/AUDIO/Death01_z12.wav differ diff --git a/content/AUDIO/Death01_z13.wav b/content/AUDIO/Death01_z13.wav new file mode 100644 index 0000000..1446342 Binary files /dev/null and b/content/AUDIO/Death01_z13.wav differ diff --git a/content/AUDIO/Death01_z14.wav b/content/AUDIO/Death01_z14.wav new file mode 100644 index 0000000..ea34e55 Binary files /dev/null and b/content/AUDIO/Death01_z14.wav differ diff --git a/content/AUDIO/Death01_z15.wav b/content/AUDIO/Death01_z15.wav new file mode 100644 index 0000000..a992d0d Binary files /dev/null and b/content/AUDIO/Death01_z15.wav differ diff --git a/content/AUDIO/Death01_z2.wav b/content/AUDIO/Death01_z2.wav new file mode 100644 index 0000000..a722a2d Binary files /dev/null and b/content/AUDIO/Death01_z2.wav differ diff --git a/content/AUDIO/Death01_z3.wav b/content/AUDIO/Death01_z3.wav new file mode 100644 index 0000000..5889471 Binary files /dev/null and b/content/AUDIO/Death01_z3.wav differ diff --git a/content/AUDIO/Death01_z4.wav b/content/AUDIO/Death01_z4.wav new file mode 100644 index 0000000..2e3451b Binary files /dev/null and b/content/AUDIO/Death01_z4.wav differ diff --git a/content/AUDIO/Death01_z5.wav b/content/AUDIO/Death01_z5.wav new file mode 100644 index 0000000..69959fd Binary files /dev/null and b/content/AUDIO/Death01_z5.wav differ diff --git a/content/AUDIO/Death01_z6.wav b/content/AUDIO/Death01_z6.wav new file mode 100644 index 0000000..17a00ea Binary files /dev/null and b/content/AUDIO/Death01_z6.wav differ diff --git a/content/AUDIO/Death01_z7.wav b/content/AUDIO/Death01_z7.wav new file mode 100644 index 0000000..ab97c8c Binary files /dev/null and b/content/AUDIO/Death01_z7.wav differ diff --git a/content/AUDIO/Death01_z8.wav b/content/AUDIO/Death01_z8.wav new file mode 100644 index 0000000..cca725c Binary files /dev/null and b/content/AUDIO/Death01_z8.wav differ diff --git a/content/AUDIO/Death01_z9.wav b/content/AUDIO/Death01_z9.wav new file mode 100644 index 0000000..7283f9f Binary files /dev/null and b/content/AUDIO/Death01_z9.wav differ diff --git a/content/AUDIO/DefaultButton01.wav b/content/AUDIO/DefaultButton01.wav new file mode 100644 index 0000000..50f2650 Binary files /dev/null and b/content/AUDIO/DefaultButton01.wav differ diff --git a/content/AUDIO/DestroyedInt01_z0.wav b/content/AUDIO/DestroyedInt01_z0.wav new file mode 100644 index 0000000..bc3860b Binary files /dev/null and b/content/AUDIO/DestroyedInt01_z0.wav differ diff --git a/content/AUDIO/DestroyedInt01_z1.wav b/content/AUDIO/DestroyedInt01_z1.wav new file mode 100644 index 0000000..02347c8 Binary files /dev/null and b/content/AUDIO/DestroyedInt01_z1.wav differ diff --git a/content/AUDIO/DestroyedInt02_z0.wav b/content/AUDIO/DestroyedInt02_z0.wav new file mode 100644 index 0000000..1f63fb9 Binary files /dev/null and b/content/AUDIO/DestroyedInt02_z0.wav differ diff --git a/content/AUDIO/DestroyedInt02_z1.wav b/content/AUDIO/DestroyedInt02_z1.wav new file mode 100644 index 0000000..66666e9 Binary files /dev/null and b/content/AUDIO/DestroyedInt02_z1.wav differ diff --git a/content/AUDIO/DestroyedInt03_z0.wav b/content/AUDIO/DestroyedInt03_z0.wav new file mode 100644 index 0000000..04fd700 Binary files /dev/null and b/content/AUDIO/DestroyedInt03_z0.wav differ diff --git a/content/AUDIO/DestroyedInt03_z1.wav b/content/AUDIO/DestroyedInt03_z1.wav new file mode 100644 index 0000000..987e1dc Binary files /dev/null and b/content/AUDIO/DestroyedInt03_z1.wav differ diff --git a/content/AUDIO/DestroyedInt04.wav b/content/AUDIO/DestroyedInt04.wav new file mode 100644 index 0000000..447376a Binary files /dev/null and b/content/AUDIO/DestroyedInt04.wav differ diff --git a/content/AUDIO/DestroyedInt05.wav b/content/AUDIO/DestroyedInt05.wav new file mode 100644 index 0000000..601c7a5 Binary files /dev/null and b/content/AUDIO/DestroyedInt05.wav differ diff --git a/content/AUDIO/DestroyedInt06_z0.wav b/content/AUDIO/DestroyedInt06_z0.wav new file mode 100644 index 0000000..d98a406 Binary files /dev/null and b/content/AUDIO/DestroyedInt06_z0.wav differ diff --git a/content/AUDIO/DestroyedInt06_z1.wav b/content/AUDIO/DestroyedInt06_z1.wav new file mode 100644 index 0000000..0e587d0 Binary files /dev/null and b/content/AUDIO/DestroyedInt06_z1.wav differ diff --git a/content/AUDIO/DestroyedInt06_z2.wav b/content/AUDIO/DestroyedInt06_z2.wav new file mode 100644 index 0000000..c57a6f1 Binary files /dev/null and b/content/AUDIO/DestroyedInt06_z2.wav differ diff --git a/content/AUDIO/DuckServo01.wav b/content/AUDIO/DuckServo01.wav new file mode 100644 index 0000000..0b1c4c3 Binary files /dev/null and b/content/AUDIO/DuckServo01.wav differ diff --git a/content/AUDIO/EjectButton01_z0.wav b/content/AUDIO/EjectButton01_z0.wav new file mode 100644 index 0000000..101e94b Binary files /dev/null and b/content/AUDIO/EjectButton01_z0.wav differ diff --git a/content/AUDIO/EjectButton01_z1.wav b/content/AUDIO/EjectButton01_z1.wav new file mode 100644 index 0000000..8d2aef3 Binary files /dev/null and b/content/AUDIO/EjectButton01_z1.wav differ diff --git a/content/AUDIO/EjectEnd01.wav b/content/AUDIO/EjectEnd01.wav new file mode 100644 index 0000000..21b187e Binary files /dev/null and b/content/AUDIO/EjectEnd01.wav differ diff --git a/content/AUDIO/EjectStart01_z0.wav b/content/AUDIO/EjectStart01_z0.wav new file mode 100644 index 0000000..b7ca907 Binary files /dev/null and b/content/AUDIO/EjectStart01_z0.wav differ diff --git a/content/AUDIO/EjectStart01_z1.wav b/content/AUDIO/EjectStart01_z1.wav new file mode 100644 index 0000000..1eec777 Binary files /dev/null and b/content/AUDIO/EjectStart01_z1.wav differ diff --git a/content/AUDIO/ElectricDamage01.wav b/content/AUDIO/ElectricDamage01.wav new file mode 100644 index 0000000..2d85f45 Binary files /dev/null and b/content/AUDIO/ElectricDamage01.wav differ diff --git a/content/AUDIO/EngButton01_z0.wav b/content/AUDIO/EngButton01_z0.wav new file mode 100644 index 0000000..45bf88c Binary files /dev/null and b/content/AUDIO/EngButton01_z0.wav differ diff --git a/content/AUDIO/EngButton01_z1.wav b/content/AUDIO/EngButton01_z1.wav new file mode 100644 index 0000000..8a0cbf5 Binary files /dev/null and b/content/AUDIO/EngButton01_z1.wav differ diff --git a/content/AUDIO/EngineAccel01_z0.wav b/content/AUDIO/EngineAccel01_z0.wav new file mode 100644 index 0000000..0cbe2e0 Binary files /dev/null and b/content/AUDIO/EngineAccel01_z0.wav differ diff --git a/content/AUDIO/EngineAccel01_z1.wav b/content/AUDIO/EngineAccel01_z1.wav new file mode 100644 index 0000000..80b1d0a Binary files /dev/null and b/content/AUDIO/EngineAccel01_z1.wav differ diff --git a/content/AUDIO/EngineAccel02_z0.wav b/content/AUDIO/EngineAccel02_z0.wav new file mode 100644 index 0000000..72bc384 Binary files /dev/null and b/content/AUDIO/EngineAccel02_z0.wav differ diff --git a/content/AUDIO/EngineAccel02_z1.wav b/content/AUDIO/EngineAccel02_z1.wav new file mode 100644 index 0000000..51e7b23 Binary files /dev/null and b/content/AUDIO/EngineAccel02_z1.wav differ diff --git a/content/AUDIO/EngineAccel03_z0.wav b/content/AUDIO/EngineAccel03_z0.wav new file mode 100644 index 0000000..1c77960 Binary files /dev/null and b/content/AUDIO/EngineAccel03_z0.wav differ diff --git a/content/AUDIO/EngineAccel03_z1.wav b/content/AUDIO/EngineAccel03_z1.wav new file mode 100644 index 0000000..1c77960 Binary files /dev/null and b/content/AUDIO/EngineAccel03_z1.wav differ diff --git a/content/AUDIO/EngineAccel04_z0.wav b/content/AUDIO/EngineAccel04_z0.wav new file mode 100644 index 0000000..8f186a8 Binary files /dev/null and b/content/AUDIO/EngineAccel04_z0.wav differ diff --git a/content/AUDIO/EngineAccel04_z1.wav b/content/AUDIO/EngineAccel04_z1.wav new file mode 100644 index 0000000..8f186a8 Binary files /dev/null and b/content/AUDIO/EngineAccel04_z1.wav differ diff --git a/content/AUDIO/EngineAccel05_z0.wav b/content/AUDIO/EngineAccel05_z0.wav new file mode 100644 index 0000000..629d08e Binary files /dev/null and b/content/AUDIO/EngineAccel05_z0.wav differ diff --git a/content/AUDIO/EngineAccel05_z1.wav b/content/AUDIO/EngineAccel05_z1.wav new file mode 100644 index 0000000..629d08e Binary files /dev/null and b/content/AUDIO/EngineAccel05_z1.wav differ diff --git a/content/AUDIO/EngineAccel06_z0.wav b/content/AUDIO/EngineAccel06_z0.wav new file mode 100644 index 0000000..6e013a0 Binary files /dev/null and b/content/AUDIO/EngineAccel06_z0.wav differ diff --git a/content/AUDIO/EngineAccel06_z1.wav b/content/AUDIO/EngineAccel06_z1.wav new file mode 100644 index 0000000..da05661 Binary files /dev/null and b/content/AUDIO/EngineAccel06_z1.wav differ diff --git a/content/AUDIO/EngineAccel06_z2.wav b/content/AUDIO/EngineAccel06_z2.wav new file mode 100644 index 0000000..6e013a0 Binary files /dev/null and b/content/AUDIO/EngineAccel06_z2.wav differ diff --git a/content/AUDIO/EngineAccel07_z0.wav b/content/AUDIO/EngineAccel07_z0.wav new file mode 100644 index 0000000..ce47875 Binary files /dev/null and b/content/AUDIO/EngineAccel07_z0.wav differ diff --git a/content/AUDIO/EngineAccel07_z1.wav b/content/AUDIO/EngineAccel07_z1.wav new file mode 100644 index 0000000..afa481e Binary files /dev/null and b/content/AUDIO/EngineAccel07_z1.wav differ diff --git a/content/AUDIO/EngineAccel07_z2.wav b/content/AUDIO/EngineAccel07_z2.wav new file mode 100644 index 0000000..ce47875 Binary files /dev/null and b/content/AUDIO/EngineAccel07_z2.wav differ diff --git a/content/AUDIO/EngineDamage01_z0.wav b/content/AUDIO/EngineDamage01_z0.wav new file mode 100644 index 0000000..662ccc3 Binary files /dev/null and b/content/AUDIO/EngineDamage01_z0.wav differ diff --git a/content/AUDIO/EngineDamage01_z1.wav b/content/AUDIO/EngineDamage01_z1.wav new file mode 100644 index 0000000..3c086f1 Binary files /dev/null and b/content/AUDIO/EngineDamage01_z1.wav differ diff --git a/content/AUDIO/EngineMotor01.wav b/content/AUDIO/EngineMotor01.wav new file mode 100644 index 0000000..b0502da Binary files /dev/null and b/content/AUDIO/EngineMotor01.wav differ diff --git a/content/AUDIO/EngineMotor02_z0.wav b/content/AUDIO/EngineMotor02_z0.wav new file mode 100644 index 0000000..a1922b9 Binary files /dev/null and b/content/AUDIO/EngineMotor02_z0.wav differ diff --git a/content/AUDIO/EngineMotor02_z1.wav b/content/AUDIO/EngineMotor02_z1.wav new file mode 100644 index 0000000..c52b004 Binary files /dev/null and b/content/AUDIO/EngineMotor02_z1.wav differ diff --git a/content/AUDIO/EnginePower01.wav b/content/AUDIO/EnginePower01.wav new file mode 100644 index 0000000..d251ce1 Binary files /dev/null and b/content/AUDIO/EnginePower01.wav differ diff --git a/content/AUDIO/EngineShiftFwd01_z0.wav b/content/AUDIO/EngineShiftFwd01_z0.wav new file mode 100644 index 0000000..af201fd Binary files /dev/null and b/content/AUDIO/EngineShiftFwd01_z0.wav differ diff --git a/content/AUDIO/EngineShiftFwd01_z1.wav b/content/AUDIO/EngineShiftFwd01_z1.wav new file mode 100644 index 0000000..b571dd7 Binary files /dev/null and b/content/AUDIO/EngineShiftFwd01_z1.wav differ diff --git a/content/AUDIO/EngineShiftFwd01_z2.wav b/content/AUDIO/EngineShiftFwd01_z2.wav new file mode 100644 index 0000000..03d2491 Binary files /dev/null and b/content/AUDIO/EngineShiftFwd01_z2.wav differ diff --git a/content/AUDIO/EngineShiftRev01_z0.wav b/content/AUDIO/EngineShiftRev01_z0.wav new file mode 100644 index 0000000..b46b503 Binary files /dev/null and b/content/AUDIO/EngineShiftRev01_z0.wav differ diff --git a/content/AUDIO/EngineShiftRev01_z1.wav b/content/AUDIO/EngineShiftRev01_z1.wav new file mode 100644 index 0000000..44994b5 Binary files /dev/null and b/content/AUDIO/EngineShiftRev01_z1.wav differ diff --git a/content/AUDIO/EngineShiftRev01_z2.wav b/content/AUDIO/EngineShiftRev01_z2.wav new file mode 100644 index 0000000..d69c997 Binary files /dev/null and b/content/AUDIO/EngineShiftRev01_z2.wav differ diff --git a/content/AUDIO/EngineWindUp01_z0.wav b/content/AUDIO/EngineWindUp01_z0.wav new file mode 100644 index 0000000..6d7d7f0 Binary files /dev/null and b/content/AUDIO/EngineWindUp01_z0.wav differ diff --git a/content/AUDIO/EngineWindUp01_z1.wav b/content/AUDIO/EngineWindUp01_z1.wav new file mode 100644 index 0000000..f103440 Binary files /dev/null and b/content/AUDIO/EngineWindUp01_z1.wav differ diff --git a/content/AUDIO/FootFallExt01_z0.wav b/content/AUDIO/FootFallExt01_z0.wav new file mode 100644 index 0000000..36896c3 Binary files /dev/null and b/content/AUDIO/FootFallExt01_z0.wav differ diff --git a/content/AUDIO/FootFallExt01_z1.wav b/content/AUDIO/FootFallExt01_z1.wav new file mode 100644 index 0000000..d709a5e Binary files /dev/null and b/content/AUDIO/FootFallExt01_z1.wav differ diff --git a/content/AUDIO/FootFallExt02.wav b/content/AUDIO/FootFallExt02.wav new file mode 100644 index 0000000..36896c3 Binary files /dev/null and b/content/AUDIO/FootFallExt02.wav differ diff --git a/content/AUDIO/FootFallExt03_z0.wav b/content/AUDIO/FootFallExt03_z0.wav new file mode 100644 index 0000000..7feb205 Binary files /dev/null and b/content/AUDIO/FootFallExt03_z0.wav differ diff --git a/content/AUDIO/FootFallExt03_z1.wav b/content/AUDIO/FootFallExt03_z1.wav new file mode 100644 index 0000000..3cca3f3 Binary files /dev/null and b/content/AUDIO/FootFallExt03_z1.wav differ diff --git a/content/AUDIO/FootFallExt03_z2.wav b/content/AUDIO/FootFallExt03_z2.wav new file mode 100644 index 0000000..c7718b9 Binary files /dev/null and b/content/AUDIO/FootFallExt03_z2.wav differ diff --git a/content/AUDIO/FootFallExt04_z0.wav b/content/AUDIO/FootFallExt04_z0.wav new file mode 100644 index 0000000..7feb205 Binary files /dev/null and b/content/AUDIO/FootFallExt04_z0.wav differ diff --git a/content/AUDIO/FootFallExt04_z1.wav b/content/AUDIO/FootFallExt04_z1.wav new file mode 100644 index 0000000..2fe6b7d Binary files /dev/null and b/content/AUDIO/FootFallExt04_z1.wav differ diff --git a/content/AUDIO/FootFallInt01.wav b/content/AUDIO/FootFallInt01.wav new file mode 100644 index 0000000..47f38f3 Binary files /dev/null and b/content/AUDIO/FootFallInt01.wav differ diff --git a/content/AUDIO/GeneratorSelect01_z0.wav b/content/AUDIO/GeneratorSelect01_z0.wav new file mode 100644 index 0000000..6c34d47 Binary files /dev/null and b/content/AUDIO/GeneratorSelect01_z0.wav differ diff --git a/content/AUDIO/GeneratorSelect01_z1.wav b/content/AUDIO/GeneratorSelect01_z1.wav new file mode 100644 index 0000000..8ecb950 Binary files /dev/null and b/content/AUDIO/GeneratorSelect01_z1.wav differ diff --git a/content/AUDIO/IncomingAlarm01.wav b/content/AUDIO/IncomingAlarm01.wav new file mode 100644 index 0000000..dbbcb55 Binary files /dev/null and b/content/AUDIO/IncomingAlarm01.wav differ diff --git a/content/AUDIO/LaserACharge01.wav b/content/AUDIO/LaserACharge01.wav new file mode 100644 index 0000000..309a84f Binary files /dev/null and b/content/AUDIO/LaserACharge01.wav differ diff --git a/content/AUDIO/LaserACharge02.wav b/content/AUDIO/LaserACharge02.wav new file mode 100644 index 0000000..309a84f Binary files /dev/null and b/content/AUDIO/LaserACharge02.wav differ diff --git a/content/AUDIO/LaserACharge03.wav b/content/AUDIO/LaserACharge03.wav new file mode 100644 index 0000000..8baa1dc Binary files /dev/null and b/content/AUDIO/LaserACharge03.wav differ diff --git a/content/AUDIO/LaserACharge04.wav b/content/AUDIO/LaserACharge04.wav new file mode 100644 index 0000000..d412e7b Binary files /dev/null and b/content/AUDIO/LaserACharge04.wav differ diff --git a/content/AUDIO/LaserAFire01_z0.wav b/content/AUDIO/LaserAFire01_z0.wav new file mode 100644 index 0000000..e42eafd Binary files /dev/null and b/content/AUDIO/LaserAFire01_z0.wav differ diff --git a/content/AUDIO/LaserAFire01_z1.wav b/content/AUDIO/LaserAFire01_z1.wav new file mode 100644 index 0000000..a504c98 Binary files /dev/null and b/content/AUDIO/LaserAFire01_z1.wav differ diff --git a/content/AUDIO/LaserAFire01_z2.wav b/content/AUDIO/LaserAFire01_z2.wav new file mode 100644 index 0000000..7086c0e Binary files /dev/null and b/content/AUDIO/LaserAFire01_z2.wav differ diff --git a/content/AUDIO/LaserAFire02_z0.wav b/content/AUDIO/LaserAFire02_z0.wav new file mode 100644 index 0000000..51b9a8d Binary files /dev/null and b/content/AUDIO/LaserAFire02_z0.wav differ diff --git a/content/AUDIO/LaserAFire02_z1.wav b/content/AUDIO/LaserAFire02_z1.wav new file mode 100644 index 0000000..a0ac8c4 Binary files /dev/null and b/content/AUDIO/LaserAFire02_z1.wav differ diff --git a/content/AUDIO/LaserAFire02_z2.wav b/content/AUDIO/LaserAFire02_z2.wav new file mode 100644 index 0000000..f602edd Binary files /dev/null and b/content/AUDIO/LaserAFire02_z2.wav differ diff --git a/content/AUDIO/LaserAFire03_z0.wav b/content/AUDIO/LaserAFire03_z0.wav new file mode 100644 index 0000000..4dfadca Binary files /dev/null and b/content/AUDIO/LaserAFire03_z0.wav differ diff --git a/content/AUDIO/LaserAFire03_z1.wav b/content/AUDIO/LaserAFire03_z1.wav new file mode 100644 index 0000000..53dbe4b Binary files /dev/null and b/content/AUDIO/LaserAFire03_z1.wav differ diff --git a/content/AUDIO/LaserAFire03_z2.wav b/content/AUDIO/LaserAFire03_z2.wav new file mode 100644 index 0000000..b0590f0 Binary files /dev/null and b/content/AUDIO/LaserAFire03_z2.wav differ diff --git a/content/AUDIO/LaserAMisfire01.wav b/content/AUDIO/LaserAMisfire01.wav new file mode 100644 index 0000000..596d5d5 Binary files /dev/null and b/content/AUDIO/LaserAMisfire01.wav differ diff --git a/content/AUDIO/LaserARelease01.wav b/content/AUDIO/LaserARelease01.wav new file mode 100644 index 0000000..f5d15d5 Binary files /dev/null and b/content/AUDIO/LaserARelease01.wav differ diff --git a/content/AUDIO/LaserARelease02.wav b/content/AUDIO/LaserARelease02.wav new file mode 100644 index 0000000..1e33612 Binary files /dev/null and b/content/AUDIO/LaserARelease02.wav differ diff --git a/content/AUDIO/LaserARelease03.wav b/content/AUDIO/LaserARelease03.wav new file mode 100644 index 0000000..35e67dc Binary files /dev/null and b/content/AUDIO/LaserARelease03.wav differ diff --git a/content/AUDIO/LaserASustain01_z0.wav b/content/AUDIO/LaserASustain01_z0.wav new file mode 100644 index 0000000..9e2cda5 Binary files /dev/null and b/content/AUDIO/LaserASustain01_z0.wav differ diff --git a/content/AUDIO/LaserASustain01_z1.wav b/content/AUDIO/LaserASustain01_z1.wav new file mode 100644 index 0000000..10d5d88 Binary files /dev/null and b/content/AUDIO/LaserASustain01_z1.wav differ diff --git a/content/AUDIO/LaserASustain01_z2.wav b/content/AUDIO/LaserASustain01_z2.wav new file mode 100644 index 0000000..d951815 Binary files /dev/null and b/content/AUDIO/LaserASustain01_z2.wav differ diff --git a/content/AUDIO/LaserASustain02_z0.wav b/content/AUDIO/LaserASustain02_z0.wav new file mode 100644 index 0000000..ccf7cfa Binary files /dev/null and b/content/AUDIO/LaserASustain02_z0.wav differ diff --git a/content/AUDIO/LaserASustain02_z1.wav b/content/AUDIO/LaserASustain02_z1.wav new file mode 100644 index 0000000..10d5d88 Binary files /dev/null and b/content/AUDIO/LaserASustain02_z1.wav differ diff --git a/content/AUDIO/LaserASustain02_z2.wav b/content/AUDIO/LaserASustain02_z2.wav new file mode 100644 index 0000000..f4193f1 Binary files /dev/null and b/content/AUDIO/LaserASustain02_z2.wav differ diff --git a/content/AUDIO/LaserASustain03_z0.wav b/content/AUDIO/LaserASustain03_z0.wav new file mode 100644 index 0000000..e65bc87 Binary files /dev/null and b/content/AUDIO/LaserASustain03_z0.wav differ diff --git a/content/AUDIO/LaserASustain03_z1.wav b/content/AUDIO/LaserASustain03_z1.wav new file mode 100644 index 0000000..10d5d88 Binary files /dev/null and b/content/AUDIO/LaserASustain03_z1.wav differ diff --git a/content/AUDIO/LaserASustain03_z2.wav b/content/AUDIO/LaserASustain03_z2.wav new file mode 100644 index 0000000..f4193f1 Binary files /dev/null and b/content/AUDIO/LaserASustain03_z2.wav differ diff --git a/content/AUDIO/LaserASustainExt01.wav b/content/AUDIO/LaserASustainExt01.wav new file mode 100644 index 0000000..435c744 Binary files /dev/null and b/content/AUDIO/LaserASustainExt01.wav differ diff --git a/content/AUDIO/LaserASustainExt02.wav b/content/AUDIO/LaserASustainExt02.wav new file mode 100644 index 0000000..ddfcec8 Binary files /dev/null and b/content/AUDIO/LaserASustainExt02.wav differ diff --git a/content/AUDIO/LaserASustainExt03.wav b/content/AUDIO/LaserASustainExt03.wav new file mode 100644 index 0000000..435c744 Binary files /dev/null and b/content/AUDIO/LaserASustainExt03.wav differ diff --git a/content/AUDIO/LaserBFire01_z0.wav b/content/AUDIO/LaserBFire01_z0.wav new file mode 100644 index 0000000..9bdd8b2 Binary files /dev/null and b/content/AUDIO/LaserBFire01_z0.wav differ diff --git a/content/AUDIO/LaserBFire01_z1.wav b/content/AUDIO/LaserBFire01_z1.wav new file mode 100644 index 0000000..32beacf Binary files /dev/null and b/content/AUDIO/LaserBFire01_z1.wav differ diff --git a/content/AUDIO/LaserBFire01_z2.wav b/content/AUDIO/LaserBFire01_z2.wav new file mode 100644 index 0000000..5f3a841 Binary files /dev/null and b/content/AUDIO/LaserBFire01_z2.wav differ diff --git a/content/AUDIO/LaserBFire02_z0.wav b/content/AUDIO/LaserBFire02_z0.wav new file mode 100644 index 0000000..369c050 Binary files /dev/null and b/content/AUDIO/LaserBFire02_z0.wav differ diff --git a/content/AUDIO/LaserBFire02_z1.wav b/content/AUDIO/LaserBFire02_z1.wav new file mode 100644 index 0000000..9d2ab55 Binary files /dev/null and b/content/AUDIO/LaserBFire02_z1.wav differ diff --git a/content/AUDIO/LaserBFire02_z2.wav b/content/AUDIO/LaserBFire02_z2.wav new file mode 100644 index 0000000..5c5ed21 Binary files /dev/null and b/content/AUDIO/LaserBFire02_z2.wav differ diff --git a/content/AUDIO/LaserBFire03_z0.wav b/content/AUDIO/LaserBFire03_z0.wav new file mode 100644 index 0000000..37b43c6 Binary files /dev/null and b/content/AUDIO/LaserBFire03_z0.wav differ diff --git a/content/AUDIO/LaserBFire03_z1.wav b/content/AUDIO/LaserBFire03_z1.wav new file mode 100644 index 0000000..32beacf Binary files /dev/null and b/content/AUDIO/LaserBFire03_z1.wav differ diff --git a/content/AUDIO/LaserBFire03_z2.wav b/content/AUDIO/LaserBFire03_z2.wav new file mode 100644 index 0000000..2132a45 Binary files /dev/null and b/content/AUDIO/LaserBFire03_z2.wav differ diff --git a/content/AUDIO/LaserBSustain01_z0.wav b/content/AUDIO/LaserBSustain01_z0.wav new file mode 100644 index 0000000..32c032d Binary files /dev/null and b/content/AUDIO/LaserBSustain01_z0.wav differ diff --git a/content/AUDIO/LaserBSustain01_z1.wav b/content/AUDIO/LaserBSustain01_z1.wav new file mode 100644 index 0000000..f1dc7b7 Binary files /dev/null and b/content/AUDIO/LaserBSustain01_z1.wav differ diff --git a/content/AUDIO/LaserBSustain01_z2.wav b/content/AUDIO/LaserBSustain01_z2.wav new file mode 100644 index 0000000..13d3372 Binary files /dev/null and b/content/AUDIO/LaserBSustain01_z2.wav differ diff --git a/content/AUDIO/LaserBSustain02_z0.wav b/content/AUDIO/LaserBSustain02_z0.wav new file mode 100644 index 0000000..7eb3f49 Binary files /dev/null and b/content/AUDIO/LaserBSustain02_z0.wav differ diff --git a/content/AUDIO/LaserBSustain02_z1.wav b/content/AUDIO/LaserBSustain02_z1.wav new file mode 100644 index 0000000..51b216c Binary files /dev/null and b/content/AUDIO/LaserBSustain02_z1.wav differ diff --git a/content/AUDIO/LaserBSustain02_z2.wav b/content/AUDIO/LaserBSustain02_z2.wav new file mode 100644 index 0000000..13d3372 Binary files /dev/null and b/content/AUDIO/LaserBSustain02_z2.wav differ diff --git a/content/AUDIO/LaserBSustain03_z0.wav b/content/AUDIO/LaserBSustain03_z0.wav new file mode 100644 index 0000000..7eb3f49 Binary files /dev/null and b/content/AUDIO/LaserBSustain03_z0.wav differ diff --git a/content/AUDIO/LaserBSustain03_z1.wav b/content/AUDIO/LaserBSustain03_z1.wav new file mode 100644 index 0000000..89f51da Binary files /dev/null and b/content/AUDIO/LaserBSustain03_z1.wav differ diff --git a/content/AUDIO/LaserBSustain03_z2.wav b/content/AUDIO/LaserBSustain03_z2.wav new file mode 100644 index 0000000..13d3372 Binary files /dev/null and b/content/AUDIO/LaserBSustain03_z2.wav differ diff --git a/content/AUDIO/LaserBSustainExt01.wav b/content/AUDIO/LaserBSustainExt01.wav new file mode 100644 index 0000000..ec650fc Binary files /dev/null and b/content/AUDIO/LaserBSustainExt01.wav differ diff --git a/content/AUDIO/LaserBSustainExt02.wav b/content/AUDIO/LaserBSustainExt02.wav new file mode 100644 index 0000000..11fe537 Binary files /dev/null and b/content/AUDIO/LaserBSustainExt02.wav differ diff --git a/content/AUDIO/LaserBSustainExt03.wav b/content/AUDIO/LaserBSustainExt03.wav new file mode 100644 index 0000000..7eb3f49 Binary files /dev/null and b/content/AUDIO/LaserBSustainExt03.wav differ diff --git a/content/AUDIO/LaserCFire01_z0.wav b/content/AUDIO/LaserCFire01_z0.wav new file mode 100644 index 0000000..03c9d88 Binary files /dev/null and b/content/AUDIO/LaserCFire01_z0.wav differ diff --git a/content/AUDIO/LaserCFire01_z1.wav b/content/AUDIO/LaserCFire01_z1.wav new file mode 100644 index 0000000..7086c0e Binary files /dev/null and b/content/AUDIO/LaserCFire01_z1.wav differ diff --git a/content/AUDIO/LaserCFire01_z2.wav b/content/AUDIO/LaserCFire01_z2.wav new file mode 100644 index 0000000..3ff0e3a Binary files /dev/null and b/content/AUDIO/LaserCFire01_z2.wav differ diff --git a/content/AUDIO/LaserCFire02_z0.wav b/content/AUDIO/LaserCFire02_z0.wav new file mode 100644 index 0000000..03c9d88 Binary files /dev/null and b/content/AUDIO/LaserCFire02_z0.wav differ diff --git a/content/AUDIO/LaserCFire02_z1.wav b/content/AUDIO/LaserCFire02_z1.wav new file mode 100644 index 0000000..7086c0e Binary files /dev/null and b/content/AUDIO/LaserCFire02_z1.wav differ diff --git a/content/AUDIO/LaserCFire02_z2.wav b/content/AUDIO/LaserCFire02_z2.wav new file mode 100644 index 0000000..3ff0e3a Binary files /dev/null and b/content/AUDIO/LaserCFire02_z2.wav differ diff --git a/content/AUDIO/LaserCFire03_z0.wav b/content/AUDIO/LaserCFire03_z0.wav new file mode 100644 index 0000000..8ae142b Binary files /dev/null and b/content/AUDIO/LaserCFire03_z0.wav differ diff --git a/content/AUDIO/LaserCFire03_z1.wav b/content/AUDIO/LaserCFire03_z1.wav new file mode 100644 index 0000000..7086c0e Binary files /dev/null and b/content/AUDIO/LaserCFire03_z1.wav differ diff --git a/content/AUDIO/LaserCFire03_z2.wav b/content/AUDIO/LaserCFire03_z2.wav new file mode 100644 index 0000000..3ff0e3a Binary files /dev/null and b/content/AUDIO/LaserCFire03_z2.wav differ diff --git a/content/AUDIO/LaserCSustain01_z0.wav b/content/AUDIO/LaserCSustain01_z0.wav new file mode 100644 index 0000000..771b79e Binary files /dev/null and b/content/AUDIO/LaserCSustain01_z0.wav differ diff --git a/content/AUDIO/LaserCSustain01_z1.wav b/content/AUDIO/LaserCSustain01_z1.wav new file mode 100644 index 0000000..edfda3b Binary files /dev/null and b/content/AUDIO/LaserCSustain01_z1.wav differ diff --git a/content/AUDIO/LaserCSustain01_z2.wav b/content/AUDIO/LaserCSustain01_z2.wav new file mode 100644 index 0000000..ec5c1d3 Binary files /dev/null and b/content/AUDIO/LaserCSustain01_z2.wav differ diff --git a/content/AUDIO/LaserCSustain02_z0.wav b/content/AUDIO/LaserCSustain02_z0.wav new file mode 100644 index 0000000..771b79e Binary files /dev/null and b/content/AUDIO/LaserCSustain02_z0.wav differ diff --git a/content/AUDIO/LaserCSustain02_z1.wav b/content/AUDIO/LaserCSustain02_z1.wav new file mode 100644 index 0000000..edfda3b Binary files /dev/null and b/content/AUDIO/LaserCSustain02_z1.wav differ diff --git a/content/AUDIO/LaserCSustain02_z2.wav b/content/AUDIO/LaserCSustain02_z2.wav new file mode 100644 index 0000000..ec5c1d3 Binary files /dev/null and b/content/AUDIO/LaserCSustain02_z2.wav differ diff --git a/content/AUDIO/LaserCSustain03_z0.wav b/content/AUDIO/LaserCSustain03_z0.wav new file mode 100644 index 0000000..88cb7a6 Binary files /dev/null and b/content/AUDIO/LaserCSustain03_z0.wav differ diff --git a/content/AUDIO/LaserCSustain03_z1.wav b/content/AUDIO/LaserCSustain03_z1.wav new file mode 100644 index 0000000..a03108c Binary files /dev/null and b/content/AUDIO/LaserCSustain03_z1.wav differ diff --git a/content/AUDIO/LaserCSustain03_z2.wav b/content/AUDIO/LaserCSustain03_z2.wav new file mode 100644 index 0000000..ec5c1d3 Binary files /dev/null and b/content/AUDIO/LaserCSustain03_z2.wav differ diff --git a/content/AUDIO/LaserCSustainExt01.wav b/content/AUDIO/LaserCSustainExt01.wav new file mode 100644 index 0000000..edfda3b Binary files /dev/null and b/content/AUDIO/LaserCSustainExt01.wav differ diff --git a/content/AUDIO/LaserCSustainExt02.wav b/content/AUDIO/LaserCSustainExt02.wav new file mode 100644 index 0000000..edfda3b Binary files /dev/null and b/content/AUDIO/LaserCSustainExt02.wav differ diff --git a/content/AUDIO/LaserCSustainExt03.wav b/content/AUDIO/LaserCSustainExt03.wav new file mode 100644 index 0000000..edfda3b Binary files /dev/null and b/content/AUDIO/LaserCSustainExt03.wav differ diff --git a/content/AUDIO/LaserCSustainExt04.wav b/content/AUDIO/LaserCSustainExt04.wav new file mode 100644 index 0000000..c1bdae1 Binary files /dev/null and b/content/AUDIO/LaserCSustainExt04.wav differ diff --git a/content/AUDIO/LaserExp01LOD2_z0.wav b/content/AUDIO/LaserExp01LOD2_z0.wav new file mode 100644 index 0000000..3df0c24 Binary files /dev/null and b/content/AUDIO/LaserExp01LOD2_z0.wav differ diff --git a/content/AUDIO/LaserExp01LOD2_z1.wav b/content/AUDIO/LaserExp01LOD2_z1.wav new file mode 100644 index 0000000..79bcf1d Binary files /dev/null and b/content/AUDIO/LaserExp01LOD2_z1.wav differ diff --git a/content/AUDIO/LaserExp02LOD2_z0.wav b/content/AUDIO/LaserExp02LOD2_z0.wav new file mode 100644 index 0000000..e482ad4 Binary files /dev/null and b/content/AUDIO/LaserExp02LOD2_z0.wav differ diff --git a/content/AUDIO/LaserExp02LOD2_z1.wav b/content/AUDIO/LaserExp02LOD2_z1.wav new file mode 100644 index 0000000..c7165c1 Binary files /dev/null and b/content/AUDIO/LaserExp02LOD2_z1.wav differ diff --git a/content/AUDIO/LaserExp03LOD2_z0.wav b/content/AUDIO/LaserExp03LOD2_z0.wav new file mode 100644 index 0000000..4a49140 Binary files /dev/null and b/content/AUDIO/LaserExp03LOD2_z0.wav differ diff --git a/content/AUDIO/LaserExp03LOD2_z1.wav b/content/AUDIO/LaserExp03LOD2_z1.wav new file mode 100644 index 0000000..0a3162f Binary files /dev/null and b/content/AUDIO/LaserExp03LOD2_z1.wav differ diff --git a/content/AUDIO/LaserExp04LOD2_z0.wav b/content/AUDIO/LaserExp04LOD2_z0.wav new file mode 100644 index 0000000..74ff628 Binary files /dev/null and b/content/AUDIO/LaserExp04LOD2_z0.wav differ diff --git a/content/AUDIO/LaserExp04LOD2_z1.wav b/content/AUDIO/LaserExp04LOD2_z1.wav new file mode 100644 index 0000000..8ced7b8 Binary files /dev/null and b/content/AUDIO/LaserExp04LOD2_z1.wav differ diff --git a/content/AUDIO/LaserExp05LOD2_z0.wav b/content/AUDIO/LaserExp05LOD2_z0.wav new file mode 100644 index 0000000..0d17a91 Binary files /dev/null and b/content/AUDIO/LaserExp05LOD2_z0.wav differ diff --git a/content/AUDIO/LaserExp05LOD2_z1.wav b/content/AUDIO/LaserExp05LOD2_z1.wav new file mode 100644 index 0000000..84a2f3c Binary files /dev/null and b/content/AUDIO/LaserExp05LOD2_z1.wav differ diff --git a/content/AUDIO/LaserExp06LOD2_z0.wav b/content/AUDIO/LaserExp06LOD2_z0.wav new file mode 100644 index 0000000..4a49140 Binary files /dev/null and b/content/AUDIO/LaserExp06LOD2_z0.wav differ diff --git a/content/AUDIO/LaserExp06LOD2_z1.wav b/content/AUDIO/LaserExp06LOD2_z1.wav new file mode 100644 index 0000000..0a3162f Binary files /dev/null and b/content/AUDIO/LaserExp06LOD2_z1.wav differ diff --git a/content/AUDIO/LaserExpInt01_z0.wav b/content/AUDIO/LaserExpInt01_z0.wav new file mode 100644 index 0000000..77ea3f5 Binary files /dev/null and b/content/AUDIO/LaserExpInt01_z0.wav differ diff --git a/content/AUDIO/LaserExpInt01_z1.wav b/content/AUDIO/LaserExpInt01_z1.wav new file mode 100644 index 0000000..eee00e1 Binary files /dev/null and b/content/AUDIO/LaserExpInt01_z1.wav differ diff --git a/content/AUDIO/LaserExpInt01_z2.wav b/content/AUDIO/LaserExpInt01_z2.wav new file mode 100644 index 0000000..867e19c Binary files /dev/null and b/content/AUDIO/LaserExpInt01_z2.wav differ diff --git a/content/AUDIO/LaserExpInt02_z0.wav b/content/AUDIO/LaserExpInt02_z0.wav new file mode 100644 index 0000000..6010776 Binary files /dev/null and b/content/AUDIO/LaserExpInt02_z0.wav differ diff --git a/content/AUDIO/LaserExpInt02_z1.wav b/content/AUDIO/LaserExpInt02_z1.wav new file mode 100644 index 0000000..5a244b0 Binary files /dev/null and b/content/AUDIO/LaserExpInt02_z1.wav differ diff --git a/content/AUDIO/LaserExpInt02_z2.wav b/content/AUDIO/LaserExpInt02_z2.wav new file mode 100644 index 0000000..867e19c Binary files /dev/null and b/content/AUDIO/LaserExpInt02_z2.wav differ diff --git a/content/AUDIO/LaserExpInt03_z0.wav b/content/AUDIO/LaserExpInt03_z0.wav new file mode 100644 index 0000000..adc4909 Binary files /dev/null and b/content/AUDIO/LaserExpInt03_z0.wav differ diff --git a/content/AUDIO/LaserExpInt03_z1.wav b/content/AUDIO/LaserExpInt03_z1.wav new file mode 100644 index 0000000..3df0c24 Binary files /dev/null and b/content/AUDIO/LaserExpInt03_z1.wav differ diff --git a/content/AUDIO/LaserExpInt03_z2.wav b/content/AUDIO/LaserExpInt03_z2.wav new file mode 100644 index 0000000..ee59bf7 Binary files /dev/null and b/content/AUDIO/LaserExpInt03_z2.wav differ diff --git a/content/AUDIO/LaserExpInt04_z0.wav b/content/AUDIO/LaserExpInt04_z0.wav new file mode 100644 index 0000000..2a8ba0b Binary files /dev/null and b/content/AUDIO/LaserExpInt04_z0.wav differ diff --git a/content/AUDIO/LaserExpInt04_z1.wav b/content/AUDIO/LaserExpInt04_z1.wav new file mode 100644 index 0000000..f389650 Binary files /dev/null and b/content/AUDIO/LaserExpInt04_z1.wav differ diff --git a/content/AUDIO/LaserExpInt04_z2.wav b/content/AUDIO/LaserExpInt04_z2.wav new file mode 100644 index 0000000..867e19c Binary files /dev/null and b/content/AUDIO/LaserExpInt04_z2.wav differ diff --git a/content/AUDIO/LaserExpInt05_z0.wav b/content/AUDIO/LaserExpInt05_z0.wav new file mode 100644 index 0000000..a9852c4 Binary files /dev/null and b/content/AUDIO/LaserExpInt05_z0.wav differ diff --git a/content/AUDIO/LaserExpInt05_z1.wav b/content/AUDIO/LaserExpInt05_z1.wav new file mode 100644 index 0000000..8af105b Binary files /dev/null and b/content/AUDIO/LaserExpInt05_z1.wav differ diff --git a/content/AUDIO/LaserExpInt05_z2.wav b/content/AUDIO/LaserExpInt05_z2.wav new file mode 100644 index 0000000..867e19c Binary files /dev/null and b/content/AUDIO/LaserExpInt05_z2.wav differ diff --git a/content/AUDIO/LaserExpInt06_z0.wav b/content/AUDIO/LaserExpInt06_z0.wav new file mode 100644 index 0000000..adc4909 Binary files /dev/null and b/content/AUDIO/LaserExpInt06_z0.wav differ diff --git a/content/AUDIO/LaserExpInt06_z1.wav b/content/AUDIO/LaserExpInt06_z1.wav new file mode 100644 index 0000000..423c6dd Binary files /dev/null and b/content/AUDIO/LaserExpInt06_z1.wav differ diff --git a/content/AUDIO/LaserExpInt06_z2.wav b/content/AUDIO/LaserExpInt06_z2.wav new file mode 100644 index 0000000..867e19c Binary files /dev/null and b/content/AUDIO/LaserExpInt06_z2.wav differ diff --git a/content/AUDIO/LaserExplosion01_z0.wav b/content/AUDIO/LaserExplosion01_z0.wav new file mode 100644 index 0000000..c027bb1 Binary files /dev/null and b/content/AUDIO/LaserExplosion01_z0.wav differ diff --git a/content/AUDIO/LaserExplosion01_z1.wav b/content/AUDIO/LaserExplosion01_z1.wav new file mode 100644 index 0000000..3df0c24 Binary files /dev/null and b/content/AUDIO/LaserExplosion01_z1.wav differ diff --git a/content/AUDIO/LaserExplosion01_z2.wav b/content/AUDIO/LaserExplosion01_z2.wav new file mode 100644 index 0000000..79bcf1d Binary files /dev/null and b/content/AUDIO/LaserExplosion01_z2.wav differ diff --git a/content/AUDIO/LaserExplosion02_z0.wav b/content/AUDIO/LaserExplosion02_z0.wav new file mode 100644 index 0000000..6010776 Binary files /dev/null and b/content/AUDIO/LaserExplosion02_z0.wav differ diff --git a/content/AUDIO/LaserExplosion02_z1.wav b/content/AUDIO/LaserExplosion02_z1.wav new file mode 100644 index 0000000..e482ad4 Binary files /dev/null and b/content/AUDIO/LaserExplosion02_z1.wav differ diff --git a/content/AUDIO/LaserExplosion02_z2.wav b/content/AUDIO/LaserExplosion02_z2.wav new file mode 100644 index 0000000..c7165c1 Binary files /dev/null and b/content/AUDIO/LaserExplosion02_z2.wav differ diff --git a/content/AUDIO/LaserExplosion03_z0.wav b/content/AUDIO/LaserExplosion03_z0.wav new file mode 100644 index 0000000..adc4909 Binary files /dev/null and b/content/AUDIO/LaserExplosion03_z0.wav differ diff --git a/content/AUDIO/LaserExplosion03_z1.wav b/content/AUDIO/LaserExplosion03_z1.wav new file mode 100644 index 0000000..4a49140 Binary files /dev/null and b/content/AUDIO/LaserExplosion03_z1.wav differ diff --git a/content/AUDIO/LaserExplosion03_z2.wav b/content/AUDIO/LaserExplosion03_z2.wav new file mode 100644 index 0000000..0a3162f Binary files /dev/null and b/content/AUDIO/LaserExplosion03_z2.wav differ diff --git a/content/AUDIO/LaserExplosion04_z0.wav b/content/AUDIO/LaserExplosion04_z0.wav new file mode 100644 index 0000000..95658e1 Binary files /dev/null and b/content/AUDIO/LaserExplosion04_z0.wav differ diff --git a/content/AUDIO/LaserExplosion04_z1.wav b/content/AUDIO/LaserExplosion04_z1.wav new file mode 100644 index 0000000..74ff628 Binary files /dev/null and b/content/AUDIO/LaserExplosion04_z1.wav differ diff --git a/content/AUDIO/LaserExplosion04_z2.wav b/content/AUDIO/LaserExplosion04_z2.wav new file mode 100644 index 0000000..8ced7b8 Binary files /dev/null and b/content/AUDIO/LaserExplosion04_z2.wav differ diff --git a/content/AUDIO/LaserExplosion05_z0.wav b/content/AUDIO/LaserExplosion05_z0.wav new file mode 100644 index 0000000..997328a Binary files /dev/null and b/content/AUDIO/LaserExplosion05_z0.wav differ diff --git a/content/AUDIO/LaserExplosion05_z1.wav b/content/AUDIO/LaserExplosion05_z1.wav new file mode 100644 index 0000000..0d17a91 Binary files /dev/null and b/content/AUDIO/LaserExplosion05_z1.wav differ diff --git a/content/AUDIO/LaserExplosion05_z2.wav b/content/AUDIO/LaserExplosion05_z2.wav new file mode 100644 index 0000000..84a2f3c Binary files /dev/null and b/content/AUDIO/LaserExplosion05_z2.wav differ diff --git a/content/AUDIO/LaserExplosion06_z0.wav b/content/AUDIO/LaserExplosion06_z0.wav new file mode 100644 index 0000000..adc4909 Binary files /dev/null and b/content/AUDIO/LaserExplosion06_z0.wav differ diff --git a/content/AUDIO/LaserExplosion06_z1.wav b/content/AUDIO/LaserExplosion06_z1.wav new file mode 100644 index 0000000..4a49140 Binary files /dev/null and b/content/AUDIO/LaserExplosion06_z1.wav differ diff --git a/content/AUDIO/LaserExplosion06_z2.wav b/content/AUDIO/LaserExplosion06_z2.wav new file mode 100644 index 0000000..0a3162f Binary files /dev/null and b/content/AUDIO/LaserExplosion06_z2.wav differ diff --git a/content/AUDIO/LaserLoaded01_z0.wav b/content/AUDIO/LaserLoaded01_z0.wav new file mode 100644 index 0000000..a9559b8 Binary files /dev/null and b/content/AUDIO/LaserLoaded01_z0.wav differ diff --git a/content/AUDIO/LaserLoaded01_z1.wav b/content/AUDIO/LaserLoaded01_z1.wav new file mode 100644 index 0000000..4d3a668 Binary files /dev/null and b/content/AUDIO/LaserLoaded01_z1.wav differ diff --git a/content/AUDIO/MapZoom01_z0.wav b/content/AUDIO/MapZoom01_z0.wav new file mode 100644 index 0000000..ae6ed9b Binary files /dev/null and b/content/AUDIO/MapZoom01_z0.wav differ diff --git a/content/AUDIO/MapZoom01_z1.wav b/content/AUDIO/MapZoom01_z1.wav new file mode 100644 index 0000000..fff4774 Binary files /dev/null and b/content/AUDIO/MapZoom01_z1.wav differ diff --git a/content/AUDIO/MchGunExp01.wav b/content/AUDIO/MchGunExp01.wav new file mode 100644 index 0000000..575da95 Binary files /dev/null and b/content/AUDIO/MchGunExp01.wav differ diff --git a/content/AUDIO/MchGunExp02.wav b/content/AUDIO/MchGunExp02.wav new file mode 100644 index 0000000..3e8fa7f Binary files /dev/null and b/content/AUDIO/MchGunExp02.wav differ diff --git a/content/AUDIO/MchGunExp03.wav b/content/AUDIO/MchGunExp03.wav new file mode 100644 index 0000000..38dff3f Binary files /dev/null and b/content/AUDIO/MchGunExp03.wav differ diff --git a/content/AUDIO/MchGunExp04.wav b/content/AUDIO/MchGunExp04.wav new file mode 100644 index 0000000..cd3ff39 Binary files /dev/null and b/content/AUDIO/MchGunExp04.wav differ diff --git a/content/AUDIO/MchGunExpCraft01_z0.wav b/content/AUDIO/MchGunExpCraft01_z0.wav new file mode 100644 index 0000000..575da95 Binary files /dev/null and b/content/AUDIO/MchGunExpCraft01_z0.wav differ diff --git a/content/AUDIO/MchGunExpCraft01_z1.wav b/content/AUDIO/MchGunExpCraft01_z1.wav new file mode 100644 index 0000000..b048334 Binary files /dev/null and b/content/AUDIO/MchGunExpCraft01_z1.wav differ diff --git a/content/AUDIO/MchGunExpCraft02_z0.wav b/content/AUDIO/MchGunExpCraft02_z0.wav new file mode 100644 index 0000000..3e8fa7f Binary files /dev/null and b/content/AUDIO/MchGunExpCraft02_z0.wav differ diff --git a/content/AUDIO/MchGunExpCraft02_z1.wav b/content/AUDIO/MchGunExpCraft02_z1.wav new file mode 100644 index 0000000..c46f4ff Binary files /dev/null and b/content/AUDIO/MchGunExpCraft02_z1.wav differ diff --git a/content/AUDIO/MchGunExpCraft03_z0.wav b/content/AUDIO/MchGunExpCraft03_z0.wav new file mode 100644 index 0000000..38dff3f Binary files /dev/null and b/content/AUDIO/MchGunExpCraft03_z0.wav differ diff --git a/content/AUDIO/MchGunExpCraft03_z1.wav b/content/AUDIO/MchGunExpCraft03_z1.wav new file mode 100644 index 0000000..5a471ad Binary files /dev/null and b/content/AUDIO/MchGunExpCraft03_z1.wav differ diff --git a/content/AUDIO/MchGunExpCraft04_z0.wav b/content/AUDIO/MchGunExpCraft04_z0.wav new file mode 100644 index 0000000..cd3ff39 Binary files /dev/null and b/content/AUDIO/MchGunExpCraft04_z0.wav differ diff --git a/content/AUDIO/MchGunExpCraft04_z1.wav b/content/AUDIO/MchGunExpCraft04_z1.wav new file mode 100644 index 0000000..d328916 Binary files /dev/null and b/content/AUDIO/MchGunExpCraft04_z1.wav differ diff --git a/content/AUDIO/MchGunFire01_z0.wav b/content/AUDIO/MchGunFire01_z0.wav new file mode 100644 index 0000000..6e602b7 Binary files /dev/null and b/content/AUDIO/MchGunFire01_z0.wav differ diff --git a/content/AUDIO/MchGunFire01_z1.wav b/content/AUDIO/MchGunFire01_z1.wav new file mode 100644 index 0000000..570945d Binary files /dev/null and b/content/AUDIO/MchGunFire01_z1.wav differ diff --git a/content/AUDIO/MchGunFire02_z0.wav b/content/AUDIO/MchGunFire02_z0.wav new file mode 100644 index 0000000..4181120 Binary files /dev/null and b/content/AUDIO/MchGunFire02_z0.wav differ diff --git a/content/AUDIO/MchGunFire02_z1.wav b/content/AUDIO/MchGunFire02_z1.wav new file mode 100644 index 0000000..c7c3e67 Binary files /dev/null and b/content/AUDIO/MchGunFire02_z1.wav differ diff --git a/content/AUDIO/MchGunFire03_z0.wav b/content/AUDIO/MchGunFire03_z0.wav new file mode 100644 index 0000000..2799a12 Binary files /dev/null and b/content/AUDIO/MchGunFire03_z0.wav differ diff --git a/content/AUDIO/MchGunFire03_z1.wav b/content/AUDIO/MchGunFire03_z1.wav new file mode 100644 index 0000000..7e5d05a Binary files /dev/null and b/content/AUDIO/MchGunFire03_z1.wav differ diff --git a/content/AUDIO/MchGunFire04_z0.wav b/content/AUDIO/MchGunFire04_z0.wav new file mode 100644 index 0000000..5368015 Binary files /dev/null and b/content/AUDIO/MchGunFire04_z0.wav differ diff --git a/content/AUDIO/MchGunFire04_z1.wav b/content/AUDIO/MchGunFire04_z1.wav new file mode 100644 index 0000000..4bbbca0 Binary files /dev/null and b/content/AUDIO/MchGunFire04_z1.wav differ diff --git a/content/AUDIO/MchGunFire05_z0.wav b/content/AUDIO/MchGunFire05_z0.wav new file mode 100644 index 0000000..e6740de Binary files /dev/null and b/content/AUDIO/MchGunFire05_z0.wav differ diff --git a/content/AUDIO/MchGunFire05_z1.wav b/content/AUDIO/MchGunFire05_z1.wav new file mode 100644 index 0000000..5d074aa Binary files /dev/null and b/content/AUDIO/MchGunFire05_z1.wav differ diff --git a/content/AUDIO/MchGunFire06_z0.wav b/content/AUDIO/MchGunFire06_z0.wav new file mode 100644 index 0000000..d04afed Binary files /dev/null and b/content/AUDIO/MchGunFire06_z0.wav differ diff --git a/content/AUDIO/MchGunFire06_z1.wav b/content/AUDIO/MchGunFire06_z1.wav new file mode 100644 index 0000000..8d8525c Binary files /dev/null and b/content/AUDIO/MchGunFire06_z1.wav differ diff --git a/content/AUDIO/MchGunFireExt01.wav b/content/AUDIO/MchGunFireExt01.wav new file mode 100644 index 0000000..d8b7ea4 Binary files /dev/null and b/content/AUDIO/MchGunFireExt01.wav differ diff --git a/content/AUDIO/MchGunJam01.wav b/content/AUDIO/MchGunJam01.wav new file mode 100644 index 0000000..14ecd67 Binary files /dev/null and b/content/AUDIO/MchGunJam01.wav differ diff --git a/content/AUDIO/MchGunMisfire01.wav b/content/AUDIO/MchGunMisfire01.wav new file mode 100644 index 0000000..10fd9a9 Binary files /dev/null and b/content/AUDIO/MchGunMisfire01.wav differ diff --git a/content/AUDIO/MechExplosion01_z0.wav b/content/AUDIO/MechExplosion01_z0.wav new file mode 100644 index 0000000..3123ae1 Binary files /dev/null and b/content/AUDIO/MechExplosion01_z0.wav differ diff --git a/content/AUDIO/MechExplosion01_z1.wav b/content/AUDIO/MechExplosion01_z1.wav new file mode 100644 index 0000000..3ad7c22 Binary files /dev/null and b/content/AUDIO/MechExplosion01_z1.wav differ diff --git a/content/AUDIO/MechExplosion01_z10.wav b/content/AUDIO/MechExplosion01_z10.wav new file mode 100644 index 0000000..f2b567f Binary files /dev/null and b/content/AUDIO/MechExplosion01_z10.wav differ diff --git a/content/AUDIO/MechExplosion01_z11.wav b/content/AUDIO/MechExplosion01_z11.wav new file mode 100644 index 0000000..144b952 Binary files /dev/null and b/content/AUDIO/MechExplosion01_z11.wav differ diff --git a/content/AUDIO/MechExplosion01_z12.wav b/content/AUDIO/MechExplosion01_z12.wav new file mode 100644 index 0000000..6192a17 Binary files /dev/null and b/content/AUDIO/MechExplosion01_z12.wav differ diff --git a/content/AUDIO/MechExplosion01_z13.wav b/content/AUDIO/MechExplosion01_z13.wav new file mode 100644 index 0000000..549b82b Binary files /dev/null and b/content/AUDIO/MechExplosion01_z13.wav differ diff --git a/content/AUDIO/MechExplosion01_z14.wav b/content/AUDIO/MechExplosion01_z14.wav new file mode 100644 index 0000000..526e363 Binary files /dev/null and b/content/AUDIO/MechExplosion01_z14.wav differ diff --git a/content/AUDIO/MechExplosion01_z15.wav b/content/AUDIO/MechExplosion01_z15.wav new file mode 100644 index 0000000..5f1a902 Binary files /dev/null and b/content/AUDIO/MechExplosion01_z15.wav differ diff --git a/content/AUDIO/MechExplosion01_z2.wav b/content/AUDIO/MechExplosion01_z2.wav new file mode 100644 index 0000000..a722a2d Binary files /dev/null and b/content/AUDIO/MechExplosion01_z2.wav differ diff --git a/content/AUDIO/MechExplosion01_z3.wav b/content/AUDIO/MechExplosion01_z3.wav new file mode 100644 index 0000000..5889471 Binary files /dev/null and b/content/AUDIO/MechExplosion01_z3.wav differ diff --git a/content/AUDIO/MechExplosion01_z4.wav b/content/AUDIO/MechExplosion01_z4.wav new file mode 100644 index 0000000..2e3451b Binary files /dev/null and b/content/AUDIO/MechExplosion01_z4.wav differ diff --git a/content/AUDIO/MechExplosion01_z5.wav b/content/AUDIO/MechExplosion01_z5.wav new file mode 100644 index 0000000..69959fd Binary files /dev/null and b/content/AUDIO/MechExplosion01_z5.wav differ diff --git a/content/AUDIO/MechExplosion01_z6.wav b/content/AUDIO/MechExplosion01_z6.wav new file mode 100644 index 0000000..17a00ea Binary files /dev/null and b/content/AUDIO/MechExplosion01_z6.wav differ diff --git a/content/AUDIO/MechExplosion01_z7.wav b/content/AUDIO/MechExplosion01_z7.wav new file mode 100644 index 0000000..ab97c8c Binary files /dev/null and b/content/AUDIO/MechExplosion01_z7.wav differ diff --git a/content/AUDIO/MechExplosion01_z8.wav b/content/AUDIO/MechExplosion01_z8.wav new file mode 100644 index 0000000..cca725c Binary files /dev/null and b/content/AUDIO/MechExplosion01_z8.wav differ diff --git a/content/AUDIO/MechExplosion01_z9.wav b/content/AUDIO/MechExplosion01_z9.wav new file mode 100644 index 0000000..7283f9f Binary files /dev/null and b/content/AUDIO/MechExplosion01_z9.wav differ diff --git a/content/AUDIO/MechFireLoop01_z0.wav b/content/AUDIO/MechFireLoop01_z0.wav new file mode 100644 index 0000000..7ba976c Binary files /dev/null and b/content/AUDIO/MechFireLoop01_z0.wav differ diff --git a/content/AUDIO/MechFireLoop01_z1.wav b/content/AUDIO/MechFireLoop01_z1.wav new file mode 100644 index 0000000..80dd142 Binary files /dev/null and b/content/AUDIO/MechFireLoop01_z1.wav differ diff --git a/content/AUDIO/MedExpInt01_z0.wav b/content/AUDIO/MedExpInt01_z0.wav new file mode 100644 index 0000000..fcfe806 Binary files /dev/null and b/content/AUDIO/MedExpInt01_z0.wav differ diff --git a/content/AUDIO/MedExpInt01_z1.wav b/content/AUDIO/MedExpInt01_z1.wav new file mode 100644 index 0000000..5b619ed Binary files /dev/null and b/content/AUDIO/MedExpInt01_z1.wav differ diff --git a/content/AUDIO/MedExpInt01_z2.wav b/content/AUDIO/MedExpInt01_z2.wav new file mode 100644 index 0000000..2e023d6 Binary files /dev/null and b/content/AUDIO/MedExpInt01_z2.wav differ diff --git a/content/AUDIO/MedExpInt02_z0.wav b/content/AUDIO/MedExpInt02_z0.wav new file mode 100644 index 0000000..c027bb1 Binary files /dev/null and b/content/AUDIO/MedExpInt02_z0.wav differ diff --git a/content/AUDIO/MedExpInt02_z1.wav b/content/AUDIO/MedExpInt02_z1.wav new file mode 100644 index 0000000..5dd3e99 Binary files /dev/null and b/content/AUDIO/MedExpInt02_z1.wav differ diff --git a/content/AUDIO/MedExpInt02_z2.wav b/content/AUDIO/MedExpInt02_z2.wav new file mode 100644 index 0000000..f1657d2 Binary files /dev/null and b/content/AUDIO/MedExpInt02_z2.wav differ diff --git a/content/AUDIO/MedExpInt03_z0.wav b/content/AUDIO/MedExpInt03_z0.wav new file mode 100644 index 0000000..44cf8c5 Binary files /dev/null and b/content/AUDIO/MedExpInt03_z0.wav differ diff --git a/content/AUDIO/MedExpInt03_z1.wav b/content/AUDIO/MedExpInt03_z1.wav new file mode 100644 index 0000000..9fea9a1 Binary files /dev/null and b/content/AUDIO/MedExpInt03_z1.wav differ diff --git a/content/AUDIO/MedExpInt03_z2.wav b/content/AUDIO/MedExpInt03_z2.wav new file mode 100644 index 0000000..1d47f1a Binary files /dev/null and b/content/AUDIO/MedExpInt03_z2.wav differ diff --git a/content/AUDIO/MedExpInt04_z0.wav b/content/AUDIO/MedExpInt04_z0.wav new file mode 100644 index 0000000..adc4909 Binary files /dev/null and b/content/AUDIO/MedExpInt04_z0.wav differ diff --git a/content/AUDIO/MedExpInt04_z1.wav b/content/AUDIO/MedExpInt04_z1.wav new file mode 100644 index 0000000..0f415bd Binary files /dev/null and b/content/AUDIO/MedExpInt04_z1.wav differ diff --git a/content/AUDIO/MedExpInt04_z2.wav b/content/AUDIO/MedExpInt04_z2.wav new file mode 100644 index 0000000..1f09e8b Binary files /dev/null and b/content/AUDIO/MedExpInt04_z2.wav differ diff --git a/content/AUDIO/MedExpInt05_z0.wav b/content/AUDIO/MedExpInt05_z0.wav new file mode 100644 index 0000000..adc4909 Binary files /dev/null and b/content/AUDIO/MedExpInt05_z0.wav differ diff --git a/content/AUDIO/MedExpInt05_z1.wav b/content/AUDIO/MedExpInt05_z1.wav new file mode 100644 index 0000000..2f44617 Binary files /dev/null and b/content/AUDIO/MedExpInt05_z1.wav differ diff --git a/content/AUDIO/MedExpInt05_z2.wav b/content/AUDIO/MedExpInt05_z2.wav new file mode 100644 index 0000000..a52f3d5 Binary files /dev/null and b/content/AUDIO/MedExpInt05_z2.wav differ diff --git a/content/AUDIO/MedExplosion01LOD2.wav b/content/AUDIO/MedExplosion01LOD2.wav new file mode 100644 index 0000000..1c42715 Binary files /dev/null and b/content/AUDIO/MedExplosion01LOD2.wav differ diff --git a/content/AUDIO/MedExplosion01_z0.wav b/content/AUDIO/MedExplosion01_z0.wav new file mode 100644 index 0000000..fcfe806 Binary files /dev/null and b/content/AUDIO/MedExplosion01_z0.wav differ diff --git a/content/AUDIO/MedExplosion01_z1.wav b/content/AUDIO/MedExplosion01_z1.wav new file mode 100644 index 0000000..1c42715 Binary files /dev/null and b/content/AUDIO/MedExplosion01_z1.wav differ diff --git a/content/AUDIO/MedExplosion01_z2.wav b/content/AUDIO/MedExplosion01_z2.wav new file mode 100644 index 0000000..43d1093 Binary files /dev/null and b/content/AUDIO/MedExplosion01_z2.wav differ diff --git a/content/AUDIO/MedExplosion02LOD2.wav b/content/AUDIO/MedExplosion02LOD2.wav new file mode 100644 index 0000000..9c0a413 Binary files /dev/null and b/content/AUDIO/MedExplosion02LOD2.wav differ diff --git a/content/AUDIO/MedExplosion02_z0.wav b/content/AUDIO/MedExplosion02_z0.wav new file mode 100644 index 0000000..c027bb1 Binary files /dev/null and b/content/AUDIO/MedExplosion02_z0.wav differ diff --git a/content/AUDIO/MedExplosion02_z1.wav b/content/AUDIO/MedExplosion02_z1.wav new file mode 100644 index 0000000..7e28e84 Binary files /dev/null and b/content/AUDIO/MedExplosion02_z1.wav differ diff --git a/content/AUDIO/MedExplosion02_z2.wav b/content/AUDIO/MedExplosion02_z2.wav new file mode 100644 index 0000000..9c0a413 Binary files /dev/null and b/content/AUDIO/MedExplosion02_z2.wav differ diff --git a/content/AUDIO/MedExplosion03LOD2.wav b/content/AUDIO/MedExplosion03LOD2.wav new file mode 100644 index 0000000..a1a7e10 Binary files /dev/null and b/content/AUDIO/MedExplosion03LOD2.wav differ diff --git a/content/AUDIO/MedExplosion03_z0.wav b/content/AUDIO/MedExplosion03_z0.wav new file mode 100644 index 0000000..ee0e0af Binary files /dev/null and b/content/AUDIO/MedExplosion03_z0.wav differ diff --git a/content/AUDIO/MedExplosion03_z1.wav b/content/AUDIO/MedExplosion03_z1.wav new file mode 100644 index 0000000..a1a7e10 Binary files /dev/null and b/content/AUDIO/MedExplosion03_z1.wav differ diff --git a/content/AUDIO/MedExplosion03_z2.wav b/content/AUDIO/MedExplosion03_z2.wav new file mode 100644 index 0000000..9fea9a1 Binary files /dev/null and b/content/AUDIO/MedExplosion03_z2.wav differ diff --git a/content/AUDIO/MedExplosion04LOD2.wav b/content/AUDIO/MedExplosion04LOD2.wav new file mode 100644 index 0000000..cedd551 Binary files /dev/null and b/content/AUDIO/MedExplosion04LOD2.wav differ diff --git a/content/AUDIO/MedExplosion04_z0.wav b/content/AUDIO/MedExplosion04_z0.wav new file mode 100644 index 0000000..adc4909 Binary files /dev/null and b/content/AUDIO/MedExplosion04_z0.wav differ diff --git a/content/AUDIO/MedExplosion04_z1.wav b/content/AUDIO/MedExplosion04_z1.wav new file mode 100644 index 0000000..cedd551 Binary files /dev/null and b/content/AUDIO/MedExplosion04_z1.wav differ diff --git a/content/AUDIO/MedExplosion04_z2.wav b/content/AUDIO/MedExplosion04_z2.wav new file mode 100644 index 0000000..fb31211 Binary files /dev/null and b/content/AUDIO/MedExplosion04_z2.wav differ diff --git a/content/AUDIO/MedExplosion05LOD2.wav b/content/AUDIO/MedExplosion05LOD2.wav new file mode 100644 index 0000000..efd0c16 Binary files /dev/null and b/content/AUDIO/MedExplosion05LOD2.wav differ diff --git a/content/AUDIO/MedExplosion05_z0.wav b/content/AUDIO/MedExplosion05_z0.wav new file mode 100644 index 0000000..adc4909 Binary files /dev/null and b/content/AUDIO/MedExplosion05_z0.wav differ diff --git a/content/AUDIO/MedExplosion05_z1.wav b/content/AUDIO/MedExplosion05_z1.wav new file mode 100644 index 0000000..efd0c16 Binary files /dev/null and b/content/AUDIO/MedExplosion05_z1.wav differ diff --git a/content/AUDIO/MedExplosion05_z2.wav b/content/AUDIO/MedExplosion05_z2.wav new file mode 100644 index 0000000..7e28e84 Binary files /dev/null and b/content/AUDIO/MedExplosion05_z2.wav differ diff --git a/content/AUDIO/MissileJam01_z0.wav b/content/AUDIO/MissileJam01_z0.wav new file mode 100644 index 0000000..b3f5f9f Binary files /dev/null and b/content/AUDIO/MissileJam01_z0.wav differ diff --git a/content/AUDIO/MissileJam01_z1.wav b/content/AUDIO/MissileJam01_z1.wav new file mode 100644 index 0000000..a618083 Binary files /dev/null and b/content/AUDIO/MissileJam01_z1.wav differ diff --git a/content/AUDIO/MissileLaunch01_z0.wav b/content/AUDIO/MissileLaunch01_z0.wav new file mode 100644 index 0000000..7304fe3 Binary files /dev/null and b/content/AUDIO/MissileLaunch01_z0.wav differ diff --git a/content/AUDIO/MissileLaunch01_z1.wav b/content/AUDIO/MissileLaunch01_z1.wav new file mode 100644 index 0000000..3fea076 Binary files /dev/null and b/content/AUDIO/MissileLaunch01_z1.wav differ diff --git a/content/AUDIO/MissileLaunch01_z2.wav b/content/AUDIO/MissileLaunch01_z2.wav new file mode 100644 index 0000000..0788546 Binary files /dev/null and b/content/AUDIO/MissileLaunch01_z2.wav differ diff --git a/content/AUDIO/MissileLaunch01_z3.wav b/content/AUDIO/MissileLaunch01_z3.wav new file mode 100644 index 0000000..993b9c4 Binary files /dev/null and b/content/AUDIO/MissileLaunch01_z3.wav differ diff --git a/content/AUDIO/MissileLoaded01_z0.wav b/content/AUDIO/MissileLoaded01_z0.wav new file mode 100644 index 0000000..7557e0b Binary files /dev/null and b/content/AUDIO/MissileLoaded01_z0.wav differ diff --git a/content/AUDIO/MissileLoaded01_z1.wav b/content/AUDIO/MissileLoaded01_z1.wav new file mode 100644 index 0000000..4d3a668 Binary files /dev/null and b/content/AUDIO/MissileLoaded01_z1.wav differ diff --git a/content/AUDIO/MissileLoading01.wav b/content/AUDIO/MissileLoading01.wav new file mode 100644 index 0000000..f2f1c17 Binary files /dev/null and b/content/AUDIO/MissileLoading01.wav differ diff --git a/content/AUDIO/MissileMisfire01_z0.wav b/content/AUDIO/MissileMisfire01_z0.wav new file mode 100644 index 0000000..01a41f1 Binary files /dev/null and b/content/AUDIO/MissileMisfire01_z0.wav differ diff --git a/content/AUDIO/MissileMisfire01_z1.wav b/content/AUDIO/MissileMisfire01_z1.wav new file mode 100644 index 0000000..c00a907 Binary files /dev/null and b/content/AUDIO/MissileMisfire01_z1.wav differ diff --git a/content/AUDIO/MissileTravel01.wav b/content/AUDIO/MissileTravel01.wav new file mode 100644 index 0000000..962abc2 Binary files /dev/null and b/content/AUDIO/MissileTravel01.wav differ diff --git a/content/AUDIO/MyomerServo01.wav b/content/AUDIO/MyomerServo01.wav new file mode 100644 index 0000000..a986a0b Binary files /dev/null and b/content/AUDIO/MyomerServo01.wav differ diff --git a/content/AUDIO/PPCExp01LOD2_z0.wav b/content/AUDIO/PPCExp01LOD2_z0.wav new file mode 100644 index 0000000..fd36898 Binary files /dev/null and b/content/AUDIO/PPCExp01LOD2_z0.wav differ diff --git a/content/AUDIO/PPCExp01LOD2_z1.wav b/content/AUDIO/PPCExp01LOD2_z1.wav new file mode 100644 index 0000000..f459353 Binary files /dev/null and b/content/AUDIO/PPCExp01LOD2_z1.wav differ diff --git a/content/AUDIO/PPCExp02LOD2_z0.wav b/content/AUDIO/PPCExp02LOD2_z0.wav new file mode 100644 index 0000000..1fd8462 Binary files /dev/null and b/content/AUDIO/PPCExp02LOD2_z0.wav differ diff --git a/content/AUDIO/PPCExp02LOD2_z1.wav b/content/AUDIO/PPCExp02LOD2_z1.wav new file mode 100644 index 0000000..2cd12e6 Binary files /dev/null and b/content/AUDIO/PPCExp02LOD2_z1.wav differ diff --git a/content/AUDIO/PPCExp03LOD2_z0.wav b/content/AUDIO/PPCExp03LOD2_z0.wav new file mode 100644 index 0000000..67bf46c Binary files /dev/null and b/content/AUDIO/PPCExp03LOD2_z0.wav differ diff --git a/content/AUDIO/PPCExp03LOD2_z1.wav b/content/AUDIO/PPCExp03LOD2_z1.wav new file mode 100644 index 0000000..67f7292 Binary files /dev/null and b/content/AUDIO/PPCExp03LOD2_z1.wav differ diff --git a/content/AUDIO/PPCExpInt01_z0.wav b/content/AUDIO/PPCExpInt01_z0.wav new file mode 100644 index 0000000..77ea3f5 Binary files /dev/null and b/content/AUDIO/PPCExpInt01_z0.wav differ diff --git a/content/AUDIO/PPCExpInt01_z1.wav b/content/AUDIO/PPCExpInt01_z1.wav new file mode 100644 index 0000000..a9c8cd6 Binary files /dev/null and b/content/AUDIO/PPCExpInt01_z1.wav differ diff --git a/content/AUDIO/PPCExpInt01_z2.wav b/content/AUDIO/PPCExpInt01_z2.wav new file mode 100644 index 0000000..84730e1 Binary files /dev/null and b/content/AUDIO/PPCExpInt01_z2.wav differ diff --git a/content/AUDIO/PPCExpInt02_z0.wav b/content/AUDIO/PPCExpInt02_z0.wav new file mode 100644 index 0000000..6f7c959 Binary files /dev/null and b/content/AUDIO/PPCExpInt02_z0.wav differ diff --git a/content/AUDIO/PPCExpInt02_z1.wav b/content/AUDIO/PPCExpInt02_z1.wav new file mode 100644 index 0000000..f459353 Binary files /dev/null and b/content/AUDIO/PPCExpInt02_z1.wav differ diff --git a/content/AUDIO/PPCExpInt02_z2.wav b/content/AUDIO/PPCExpInt02_z2.wav new file mode 100644 index 0000000..84730e1 Binary files /dev/null and b/content/AUDIO/PPCExpInt02_z2.wav differ diff --git a/content/AUDIO/PPCExpInt03_z0.wav b/content/AUDIO/PPCExpInt03_z0.wav new file mode 100644 index 0000000..c027bb1 Binary files /dev/null and b/content/AUDIO/PPCExpInt03_z0.wav differ diff --git a/content/AUDIO/PPCExpInt03_z1.wav b/content/AUDIO/PPCExpInt03_z1.wav new file mode 100644 index 0000000..a9c8cd6 Binary files /dev/null and b/content/AUDIO/PPCExpInt03_z1.wav differ diff --git a/content/AUDIO/PPCExpInt03_z2.wav b/content/AUDIO/PPCExpInt03_z2.wav new file mode 100644 index 0000000..1fd8462 Binary files /dev/null and b/content/AUDIO/PPCExpInt03_z2.wav differ diff --git a/content/AUDIO/PPCExplosion01_z0.wav b/content/AUDIO/PPCExplosion01_z0.wav new file mode 100644 index 0000000..c027bb1 Binary files /dev/null and b/content/AUDIO/PPCExplosion01_z0.wav differ diff --git a/content/AUDIO/PPCExplosion01_z1.wav b/content/AUDIO/PPCExplosion01_z1.wav new file mode 100644 index 0000000..6924c59 Binary files /dev/null and b/content/AUDIO/PPCExplosion01_z1.wav differ diff --git a/content/AUDIO/PPCExplosion01_z2.wav b/content/AUDIO/PPCExplosion01_z2.wav new file mode 100644 index 0000000..3a07f9a Binary files /dev/null and b/content/AUDIO/PPCExplosion01_z2.wav differ diff --git a/content/AUDIO/PPCExplosion02_z0.wav b/content/AUDIO/PPCExplosion02_z0.wav new file mode 100644 index 0000000..6010776 Binary files /dev/null and b/content/AUDIO/PPCExplosion02_z0.wav differ diff --git a/content/AUDIO/PPCExplosion02_z1.wav b/content/AUDIO/PPCExplosion02_z1.wav new file mode 100644 index 0000000..d63f55f Binary files /dev/null and b/content/AUDIO/PPCExplosion02_z1.wav differ diff --git a/content/AUDIO/PPCExplosion02_z2.wav b/content/AUDIO/PPCExplosion02_z2.wav new file mode 100644 index 0000000..3a07f9a Binary files /dev/null and b/content/AUDIO/PPCExplosion02_z2.wav differ diff --git a/content/AUDIO/PPCExplosion03_z0.wav b/content/AUDIO/PPCExplosion03_z0.wav new file mode 100644 index 0000000..adc4909 Binary files /dev/null and b/content/AUDIO/PPCExplosion03_z0.wav differ diff --git a/content/AUDIO/PPCExplosion03_z1.wav b/content/AUDIO/PPCExplosion03_z1.wav new file mode 100644 index 0000000..fcb5b84 Binary files /dev/null and b/content/AUDIO/PPCExplosion03_z1.wav differ diff --git a/content/AUDIO/PPCExplosion03_z2.wav b/content/AUDIO/PPCExplosion03_z2.wav new file mode 100644 index 0000000..3a07f9a Binary files /dev/null and b/content/AUDIO/PPCExplosion03_z2.wav differ diff --git a/content/AUDIO/ProgramButton01.wav b/content/AUDIO/ProgramButton01.wav new file mode 100644 index 0000000..f779797 Binary files /dev/null and b/content/AUDIO/ProgramButton01.wav differ diff --git a/content/AUDIO/ProjectileATravel01.wav b/content/AUDIO/ProjectileATravel01.wav new file mode 100644 index 0000000..f70a0b4 Binary files /dev/null and b/content/AUDIO/ProjectileATravel01.wav differ diff --git a/content/AUDIO/ProjectileLoaded01_z0.wav b/content/AUDIO/ProjectileLoaded01_z0.wav new file mode 100644 index 0000000..a1be2fd Binary files /dev/null and b/content/AUDIO/ProjectileLoaded01_z0.wav differ diff --git a/content/AUDIO/ProjectileLoaded01_z1.wav b/content/AUDIO/ProjectileLoaded01_z1.wav new file mode 100644 index 0000000..4d3a668 Binary files /dev/null and b/content/AUDIO/ProjectileLoaded01_z1.wav differ diff --git a/content/AUDIO/ProjectileLoading01.wav b/content/AUDIO/ProjectileLoading01.wav new file mode 100644 index 0000000..007277d Binary files /dev/null and b/content/AUDIO/ProjectileLoading01.wav differ diff --git a/content/AUDIO/SmallExplosion01.wav b/content/AUDIO/SmallExplosion01.wav new file mode 100644 index 0000000..ec84e16 Binary files /dev/null and b/content/AUDIO/SmallExplosion01.wav differ diff --git a/content/AUDIO/SmallExplosion02.wav b/content/AUDIO/SmallExplosion02.wav new file mode 100644 index 0000000..9c0a413 Binary files /dev/null and b/content/AUDIO/SmallExplosion02.wav differ diff --git a/content/AUDIO/SmallExplosion03.wav b/content/AUDIO/SmallExplosion03.wav new file mode 100644 index 0000000..a1a7e10 Binary files /dev/null and b/content/AUDIO/SmallExplosion03.wav differ diff --git a/content/AUDIO/SmallExplosion04.wav b/content/AUDIO/SmallExplosion04.wav new file mode 100644 index 0000000..cedd551 Binary files /dev/null and b/content/AUDIO/SmallExplosion04.wav differ diff --git a/content/AUDIO/SmallFireLoop01.wav b/content/AUDIO/SmallFireLoop01.wav new file mode 100644 index 0000000..16fcdf2 Binary files /dev/null and b/content/AUDIO/SmallFireLoop01.wav differ diff --git a/content/AUDIO/TorsoStop01_z0.wav b/content/AUDIO/TorsoStop01_z0.wav new file mode 100644 index 0000000..27f808d Binary files /dev/null and b/content/AUDIO/TorsoStop01_z0.wav differ diff --git a/content/AUDIO/TorsoStop01_z1.wav b/content/AUDIO/TorsoStop01_z1.wav new file mode 100644 index 0000000..6843c84 Binary files /dev/null and b/content/AUDIO/TorsoStop01_z1.wav differ diff --git a/content/AUDIO/TorsoTwistExt01.wav b/content/AUDIO/TorsoTwistExt01.wav new file mode 100644 index 0000000..2e43b20 Binary files /dev/null and b/content/AUDIO/TorsoTwistExt01.wav differ diff --git a/content/AUDIO/TorsoTwistInt01.wav b/content/AUDIO/TorsoTwistInt01.wav new file mode 100644 index 0000000..a069b01 Binary files /dev/null and b/content/AUDIO/TorsoTwistInt01.wav differ diff --git a/content/AUDIO/TorsoTwistStop01_z0.wav b/content/AUDIO/TorsoTwistStop01_z0.wav new file mode 100644 index 0000000..9a1eca0 Binary files /dev/null and b/content/AUDIO/TorsoTwistStop01_z0.wav differ diff --git a/content/AUDIO/TorsoTwistStop01_z1.wav b/content/AUDIO/TorsoTwistStop01_z1.wav new file mode 100644 index 0000000..6843c84 Binary files /dev/null and b/content/AUDIO/TorsoTwistStop01_z1.wav differ diff --git a/content/AUDIO/Translocate01_z0.wav b/content/AUDIO/Translocate01_z0.wav new file mode 100644 index 0000000..a060e77 Binary files /dev/null and b/content/AUDIO/Translocate01_z0.wav differ diff --git a/content/AUDIO/Translocate01_z1.wav b/content/AUDIO/Translocate01_z1.wav new file mode 100644 index 0000000..a060e77 Binary files /dev/null and b/content/AUDIO/Translocate01_z1.wav differ diff --git a/content/AUDIO/TriggerDurLoad01.wav b/content/AUDIO/TriggerDurLoad01.wav new file mode 100644 index 0000000..326e13d Binary files /dev/null and b/content/AUDIO/TriggerDurLoad01.wav differ diff --git a/content/AUDIO/ViewSwitch01.wav b/content/AUDIO/ViewSwitch01.wav new file mode 100644 index 0000000..8f1d672 Binary files /dev/null and b/content/AUDIO/ViewSwitch01.wav differ diff --git a/content/AUDIO/Warnings01_z0.wav b/content/AUDIO/Warnings01_z0.wav new file mode 100644 index 0000000..2485ebc Binary files /dev/null and b/content/AUDIO/Warnings01_z0.wav differ diff --git a/content/AUDIO/Warnings01_z1.wav b/content/AUDIO/Warnings01_z1.wav new file mode 100644 index 0000000..cad4103 Binary files /dev/null and b/content/AUDIO/Warnings01_z1.wav differ diff --git a/content/AUDIO/Warnings01_z2.wav b/content/AUDIO/Warnings01_z2.wav new file mode 100644 index 0000000..ce687b2 Binary files /dev/null and b/content/AUDIO/Warnings01_z2.wav differ diff --git a/content/AUDIO/Warnings01_z3.wav b/content/AUDIO/Warnings01_z3.wav new file mode 100644 index 0000000..2b2c8bb Binary files /dev/null and b/content/AUDIO/Warnings01_z3.wav differ diff --git a/content/AUDIO/Warnings01_z4.wav b/content/AUDIO/Warnings01_z4.wav new file mode 100644 index 0000000..adb3340 Binary files /dev/null and b/content/AUDIO/Warnings01_z4.wav differ diff --git a/content/AUDIO/Warnings01_z5.wav b/content/AUDIO/Warnings01_z5.wav new file mode 100644 index 0000000..7653b14 Binary files /dev/null and b/content/AUDIO/Warnings01_z5.wav differ diff --git a/content/AUDIO/Warnings01_z6.wav b/content/AUDIO/Warnings01_z6.wav new file mode 100644 index 0000000..435c4f7 Binary files /dev/null and b/content/AUDIO/Warnings01_z6.wav differ diff --git a/content/AUDIO/Warnings01_z7.wav b/content/AUDIO/Warnings01_z7.wav new file mode 100644 index 0000000..f4420ee Binary files /dev/null and b/content/AUDIO/Warnings01_z7.wav differ diff --git a/content/AUDIO/WindLoop01.wav b/content/AUDIO/WindLoop01.wav new file mode 100644 index 0000000..4f661a5 Binary files /dev/null and b/content/AUDIO/WindLoop01.wav differ diff --git a/content/FOGDAY.EGG b/content/FOGDAY.EGG new file mode 100644 index 0000000..ca86964 --- /dev/null +++ b/content/FOGDAY.EGG @@ -0,0 +1,244 @@ +[mission] +adventure=BattleTech +map=arena1 +scenario=freeforall +time=day +weather=fog +temperature=27 +length=600 +[ordinals] +bitmap=Ordinal::BitMap::1 +bitmap=Ordinal::BitMap::2 +bitmap=Ordinal::BitMap::3 +bitmap=Ordinal::BitMap::4 +[Ordinal::BitMap::1] +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=000038000003C000001FF00000000F00 +bitmap=0000F8000003C000003FF80000000F00 +bitmap=0001F8000003C00000707C0000000F00 +bitmap=0001F8000003C00000603C0000000F00 +bitmap=00007801FC0FF00000003C3DF807FF00 +bitmap=00007803FE0FF00000003C3FFC0FFF00 +bitmap=00007803C703C00000003C3E3C1F0F00 +bitmap=000078078303C0000000783C1E1E0F00 +bitmap=000078078003C0000000783C1E1E0F00 +bitmap=00007807C003C0000000F03C1E1E0F00 +bitmap=00007807F003C0000001E03C1E1E0F00 +bitmap=00007803FC03C0000003C03C1E1E0F00 +bitmap=00007801FE03C0000007803C1E1E0F00 +bitmap=000078007F03C000000F003C1E1E0F00 +bitmap=000078001F03C000001E003C1E1E0F00 +bitmap=000078000F03C000003C003C1E1E0F00 +bitmap=000078060F03C0000078003C1E1E0F00 +bitmap=000078071E03E0000078003C1E1F1F00 +bitmap=00007803FE01F000007FFC3C1E0FFF00 +bitmap=00007801FC00F000007FFC3C1E07EF00 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +x=128 +y=32 +width=8 +[Ordinal::BitMap::2] +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=001FFF0000003C0000003C03C0780000 +bitmap=001FFF0000003C0000007C03C0780000 +bitmap=00000E0000003C000000FC03C0780000 +bitmap=00003C0000003C000001BC03C0780000 +bitmap=0000700F3E1FFC000003BC0FF07BF000 +bitmap=0001E00F7E3FFC0000073C0FF07FF800 +bitmap=0003800FFE7C3C0000063C03C07C7800 +bitmap=0007F80FFE783C00000C3C03C0783C00 +bitmap=0007FE0F80783C0000183C03C0783C00 +bitmap=00001E0F00783C0000383C03C0783C00 +bitmap=00000F0F00783C0000703C03C0783C00 +bitmap=00000F0F00783C00007FFF03C0783C00 +bitmap=00000F0F00783C00007FFF03C0783C00 +bitmap=00000F0F00783C0000003C03C0783C00 +bitmap=00000F0F00783C0000003C03C0783C00 +bitmap=00000F0F00783C0000003C03C0783C00 +bitmap=00180F0F00783C0000003C03C0783C00 +bitmap=001C1F0F007C7C0000003C03E0783C00 +bitmap=000FFE0F003FFC0000003C01F0783C00 +bitmap=0007FC0F001FBC0000003C00F0783C00 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +x=128 +y=32 +width=8 +[Ordinal::BitMap::3] +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=001FFF03C07800000000FC03C0780000 +bitmap=001FFF03C07800000003FC03C0780000 +bitmap=001E0003C07800000007C003C0780000 +bitmap=001E0003C0780000000F0003C0780000 +bitmap=001E000FF07BF000000F000FF07BF000 +bitmap=001E000FF07FF800001E000FF07FF800 +bitmap=001E0003C07C7800001E0003C07C7800 +bitmap=001FFC03C0783C00001EFC03C0783C00 +bitmap=001FFE03C0783C00001FFE03C0783C00 +bitmap=00001F03C0783C00001F1F03C0783C00 +bitmap=00000F03C0783C00001E0F03C0783C00 +bitmap=00000F03C0783C00001E0F03C0783C00 +bitmap=00000F03C0783C00001E0F03C0783C00 +bitmap=00000F03C0783C00001E0F03C0783C00 +bitmap=00000F03C0783C00001E0F03C0783C00 +bitmap=00000F03C0783C00001E0F03C0783C00 +bitmap=00180F03C0783C00001E0F03C0783C00 +bitmap=001C1F03E0783C00001F1F03E0783C00 +bitmap=000FFE01F0783C00000FFE01F0783C00 +bitmap=0007FC00F0783C000007FC00F0783C00 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +x=128 +y=32 +width=8 +[Ordinal::BitMap::4] +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=001FFF03C07800000007FC03C0780000 +bitmap=001FFF03C0780000000FFE03C0780000 +bitmap=00000F03C0780000001F1F03C0780000 +bitmap=00000F03C0780000001E0F03C0780000 +bitmap=00000F0FF07BF000001E0F0FF07BF000 +bitmap=00001F0FF07FF800001E0F0FF07FF800 +bitmap=00001E03C07C7800001E0F03C07C7800 +bitmap=00003E03C0783C00001E0F03C0783C00 +bitmap=00003C03C0783C00000F1E03C0783C00 +bitmap=00003C03C0783C000007FC03C0783C00 +bitmap=00007803C0783C000007FC03C0783C00 +bitmap=00007803C0783C00000F1E03C0783C00 +bitmap=00007803C0783C00001E0F03C0783C00 +bitmap=0000F003C0783C00001E0F03C0783C00 +bitmap=0000F003C0783C00001E0F03C0783C00 +bitmap=0000F003C0783C00001E0F03C0783C00 +bitmap=0000F003C0783C00001E0F03C0783C00 +bitmap=0000F003E0783C00000F1E03E0783C00 +bitmap=0000F001F0783C00000FFE01F0783C00 +bitmap=0000F000F0783C000007FC00F0783C00 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +x=128 +y=32 +width=8 +[pilots] +pilot=127.0.0.1:1502 +pilot=127.0.0.1:1602 +[127.0.0.1:1502] +hostType=0 +advancedDamage=1 +loadzones=1 +name=Aeolus +bitmapindex=1 +experience=expert +badge=VGL +patch=Yellow +role=Role::Default +dropzone=one +vehicle=bhk1 +vehicleValue=1000 +color=White +[127.0.0.1:1602] +hostType=0 +advancedDamage=1 +loadzones=1 +name=Boreas +bitmapindex=2 +experience=expert +badge=VGL +patch=Yellow +role=Role::Default +dropzone=one +vehicle=ava1 +vehicleValue=1000 +color=Red +[largebitmap] +bitmap=BitMap::Large::Aeolus +[BitMap::Large::Aeolus] +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=000003FE0000000001E0000000000000 +bitmap=000007FF0000000001E0000000000000 +bitmap=00000F8F8000000001E0000000000000 +bitmap=00000F078000000001E0000000000000 +bitmap=00000F0781FF00FF81E0F0781FC00000 +bitmap=00000F0783FF81FFC1E0F0783FE00000 +bitmap=00000F0787C7C3E3E1E0F0783C700000 +bitmap=00000F078783C3C1E1E0F07878300000 +bitmap=00000F078783C3C1E1E0F07878000000 +bitmap=00000FFF8783C3C1E1E0F0787C000000 +bitmap=00000FFF8783C3C1E1E0F0787F000000 +bitmap=00000F0787FFC3C1E1E0F0783FC00000 +bitmap=00000F0787FFC3C1E1E0F0781FE00000 +bitmap=00000F07878003C1E1E0F07807F00000 +bitmap=00000F07878003C1E1E0F07801F00000 +bitmap=00000F07878003C1E1E0F07800F00000 +bitmap=00000F078780C3C1E1E0F07860F00000 +bitmap=00000F0787C1C3E3E1E078F871E00000 +bitmap=00000F0783FF81FFC1E07FF83FE00000 +bitmap=00000F0781FF00FF81E03F781FC00000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +bitmap=00000000000000000000000000000000 +x=128 +y=32 +width=8 +[smallbitmap] +bitmap=BitMap::Small::Aeolus +[BitMap::Small::Aeolus] +bitmap=00000000000000000000000000000000 +bitmap=0000000000000000003E00000C000000 +bitmap=006300000C00000000631F0F8CC63C00 +bitmap=00633198CCC66200007F3198CCC66000 +bitmap=00633F98CCC6780000633018CCC61E00 +bitmap=00633018CCC6060000633098CCC64600 +bitmap=00631F0F8C7E3C000000000000000000 +bitmap=00000000000000000000000000000000 +x=64 +y=16 +width=4 +[Role::Default] +model=dfltrole +[Role::NoReturn] +model=noretun diff --git a/content/LAST.EGG b/content/LAST.EGG index 61255bd..72e7c5a 100644 --- a/content/LAST.EGG +++ b/content/LAST.EGG @@ -1,8 +1,8 @@ [mission] adventure=BattleTech -map=grass +map=cavern scenario=freeforall -time=day +time=night weather=clear temperature=27 length=600 @@ -156,8 +156,9 @@ x=128 y=32 width=8 [pilots] -pilot=200.0.0.96 -[200.0.0.96] +pilot=127.0.0.1:1502 +pilot=127.0.0.1:1602 +[127.0.0.1:1502] hostType=0 advancedDamage=1 loadzones=1 @@ -171,6 +172,20 @@ dropzone=one vehicle=bhk1 vehicleValue=1000 color=White +[127.0.0.1:1602] +hostType=0 +advancedDamage=1 +loadzones=1 +name=Boreas +bitmapindex=2 +experience=expert +badge=VGL +patch=Yellow +role=Role::Default +dropzone=one +vehicle=ava1 +vehicleValue=1000 +color=Red [largebitmap] bitmap=BitMap::Large::Aeolus [BitMap::Large::Aeolus] @@ -227,3 +242,4 @@ width=4 model=dfltrole [Role::NoReturn] model=noretun + diff --git a/context/cockpit-view.md b/context/cockpit-view.md index f787911..46a79f7 100644 --- a/context/cockpit-view.md +++ b/context/cockpit-view.md @@ -5,7 +5,7 @@ status: living source_sections: "task #55 (2026-07-10); reference/decomp part_007.c/part_013.c/part_014.c/part_011.c; engine/MUNGA_L4/bgfload.cpp, L4VIDRND.cpp; game/reconstructed/btl4vid.cpp" related_topics: [rendering, bgf-format, gauges-hud, subsystems, decomp-reference] key_terms: [material-ramp, siteeyepoint] -open_questions: ["gyro tail: gait jolt/rumble [T3 gates], sway model, torso-pitch eye writer, damageForce fill", "exact authored frame colour: the unlit constant (0.13,0.12,0.15) is T3 tuned-to-footage (BT_COP_FRAME overrides; the ramp-index approach + BT_COP_RAMP_L are RETIRED)"] +open_questions: ["gyro tail: gait jolt/rumble [T3 gates], torso-pitch eye writer, damageForce fill (sway model DONE: the mech instability model @0x3F0 feeds swayBias -- task #66)", "exact authored frame colour: the unlit constant (0.13,0.12,0.15) is T3 tuned-to-footage (BT_COP_FRAME overrides; the ramp-index approach + BT_COP_RAMP_L are RETIRED)"] --- # First-Person Cockpit: the *_cop Canopy + the Authentic Eyepoint diff --git a/context/combat-damage.md b/context/combat-damage.md index 372dae1..803ba8f 100644 --- a/context/combat-damage.md +++ b/context/combat-damage.md @@ -6,7 +6,7 @@ source_sections: "PROGRESS_LOG.md §10c; docs/HARD_PROBLEMS.md (P5)" related_topics: [subsystems, decomp-reference, reconstruction-gotchas, locomotion, rendering, translocation-warp, multiplayer] key_terms: [damage-zone, TakeDamage, viewpoint-entity, master, replicant] open_questions: - - "Per-impact aim (cylinder lookup); collision damage application; DeathShutdown + collapse anim" + - "Missile cluster-hit variance/concentration (task #60-D deferred); per-player splash enable sub-gate (Missile+0x360 [T3]); ~100pt ram lethality vs the pod unverified [T3]" --- # Combat & Damage @@ -61,6 +61,16 @@ Fixed-torso mechs (the BLH: `TorsoHorizontalEnabled=0` — no jointtorso in the boresight dead-ahead. Once locked, `Emitter::FireWeapon` converges with NO aim/arc test (part_013.c:7758). `MechWeapon::UpdateTargetState` (`FUN_004b9bdc` [T1]): `targetWithinRange = dist < (1 − hostZoneDamage) × weaponRange`. The 0x388 WRITER is in the same un-exported gap. +âš  **`hostZoneDamage` is `Subsystem::damageZone->damageLevel` (weapon `@0xE0 → +0x158`), NOT +heatLoad** — the port originally computed `effectiveRange = (1 − heatLoad) × weaponRange` +(mechweap.cpp), the SAME `@0xE0`-DamageZone-vs-heat misattribution corrected in +`HeatSink::UpdateCoolant`. For a charge/discharge weapon (ER laser) the weapon's own `heatLoad` +swings 0→1 every cycle, so `effectiveRange` collapsed to ~0 and the weapon was perpetually "out +of range" → `SendDamageMessage` never ran → NO damage and NO impact explosion (the "lackluster/ +absent laser hits, esp. the ER medium, on mechs AND buildings" report). FIXED 2026-07-15: read +the QUALIFIED `this->Subsystem::damageZone->damageLevel`; an undamaged weapon now holds full +STABLE `weaponRange`. Verified: laser `effRange` 500 stable (was 0/59/340/424), impact explosions +13/22s (was ~2). [T2] See [[reconstruction-gotchas]] (`@0xE0` shadow/heat class). **Port status: RECONSTRUCTED + runtime-verified (tasks #36/#39/#58) [T2].** The chain: the cockpit eye inherits the twist AUTHENTICALLY through the draw traversal (`Torso::PushTwist` → hinge `rotationAmount` → `HingeRenderable::Execute` multiplies the live hinge into the matrix @@ -104,6 +114,32 @@ bug was fixed en route: every renderable rebuild stomped `mCamera` back to the chase eye (btl4vid `mViewInside` now persists the chosen view — the aim camera feed and the V toggle both depend on it). +**WORLD STRUCTURES ARE TARGETABLE via the STATIC COLLISION TREE (task #50, 2026-07-15) [T2].** +The `BTGroundRayHit` "terrain" tier only samples the VISUAL heightfield, which on arena1 is a +single flat `'sky'`-named ground mesh (`[visgnd] sampler ready: 1 terrain instances`) — and the +class-42 map-instance stream (`BuildTables`) likewise holds only `'sky'`. So the garages/walls +were invisible to the pick → shots passed through them, and only mechs moved the range axis (the +user-reported regression). The FIX: the boresight now also ray-tests the **zone's static collision +solid tree** — the SAME geometry that blocks the mech's walk. Mechanism is authentic engine code: +`Mover::FindBoxedSolidHitBy` (MOVER.cpp) already tests movers, doors, AND the static world +(`application->GetInterestManager()->GetInterestZone(interestZoneID)->GetCollisionRoot()->FindBoundingBoxHitBy(line)`). +We factored its "test against the static world" tail into **`Mover::FindStaticSolidHitBy(Line*)`** +(static solids only, no movers), wrapped by **`Mech::WorldStructurePick(start,dir,range,&hit)`** +(mech.cpp — builds the world-space `Line`, reads the entry point back via `line->FindEnd` since +`FindBoundingBoxHitBy`→`HitByBounded` clips `line->length = enter`). The pick order in mech4.cpp is +now: closest MECH (`PickRayHit`, damage zones + lock) → closest STRUCTURE (`WorldStructurePick`, +occludes a mech BEHIND it) → flat ground (`BTGroundRayHit`) → sky (fire-at-nothing). A structure +hit designates `mech+0x388 = gBTTerrainEntity` (the no-damage-zone sentinel) + `0x37c` = the entry +point, so the range caret reads the structure distance and NO lock ring draws (mech4.cpp:4529, the +`des != hotTarget` branch), exactly the reported authentic behavior. âš  There is NO ray query +against the ground/`containedByNode` `BoundingBoxTree` — `FindBoundingBoxUnder` is DOWNWARD-only +(gravity/ground-snap: `*height = FindDistanceBelowBounded`), useless for a horizontal boresight; +the static SOLID tree (`BoxedSolidTree::FindBoundingBoxHitBy`) is the only ray-vs-world query. +Verified: a `BT_WSWEEP` horizontal ray-fan on arena1 tracks position (3/24 hits near the boundary +→ 17-21/24 inside the interior garage cluster → 3/24 past it — discrete interior solids, not just +an enclosing box), no crash. The interactive boresight (`BTGetAimRay`) can't be exercised headless +(no window → `noRay`), so the SWEEP is the headless proof; interactive aim is user-verified. + ## Firing arc + muzzles There is NO firing-arc field in the binary (`Emitter::FireWeapon` fires whenever HasActiveTarget) — the 1995 game got away with it because it was **cockpit-only** (a beam behind you is off-camera). The @@ -112,6 +148,35 @@ port's external camera exposes it, so the arc is gated on the **torso twist rang (`BT_FIRE_ARC`, a labeled PORT presentation param). Beams fire from the mech's real gun-port SITE segments (siterugunport etc.) resolved via `GetSegment(name)→segmentToEntity×localToWorld`. [T2] +## REAR-FIRE + look views (task #68, 2026-07-16) [T1] +The pod had a rear arsenal + a five-state LOOK-VIEW system (all reconstructed + live): +- **weapon.rearFiring @0x334** = the MOUNT SEGMENT page name contains 'b' (ctor marker @0x511aa2; + the back ports `sitelbgunport`/`siterbgunport`). Roster survey: **the Blackhawk's ERMLaser_2/3 + are the ONLY rear weapons in the game** (both loadout variants; the Owens model HAS back ports + but mounts nothing on them; every missile rack is forward). +- **mech.rearFiring @0x410** = OR of the weapons' flags (ctor part_012.c:10220). +- The mapper look machine (part_013.c:396-459; modes 0=fwd 1=left 2=right 3=BACK 4=down from the + look buttons): on change it writes **EyepointRotation (mech+0x360)** — side yaw = the authored + per-mech look angles (model rec +0x50-0x5c → mech 0x564-0x570), look-back = yaw Ï€ + lookBackAngle + pitch — and re-arms each weapon's **viewFireEnable @0x3E0** (fwd = !rear, back = rear, else 0). + 0x3E0 is the fire gate (the Loaded→Firing transition, part_013.c:7689 — the old + `useConfiguredPip` label) AND selects the HUD pips: the reticle registers each pip in group + 1(front)/2(rear) from the RearFiring attr (part_014.c:5429-34) and draws the group in the mech's + reticleElementMask low bits (mech+0x390: fwd |1&~2, back |2&~1, part_013.c:5588-95). +- Port: keyboard **HOLD 'V' = look back**; side/down looks not yet key-bound. `[rearfire]`/ + `[look]` traces under BT_PROJ_LOG/BT_KEY_LOG. +- Missile LAUNCH direction: rounds leave along the **mount segment's world frame** (the binary + spawns the missile with the full muzzle-segment AffineMatrix, fire builder @0x4bcc60:8762-64, + authored MuzzleVelocity composed with z NEGATED + the mech's own velocity) — was the + "missiles fire out of his back" torso-twist bug (launch followed the LEGS while the muzzle + followed the twist). +- BANKED for the full Missile-entity revival (decoded, not yet ported — commit ab6ea83): the three + authentic performances — Seeker 0x4be9a0 (aim = target-frame offset / 100u-ahead-of-own-frame + dumb + loft/lead 0x4beae4), Thruster 0x4be474 (quaternion-slerp BODY turn toward the aim at + turnRate°/s), MoveAndCollide 0x4bef78 (velocity aerodynamically aligned to thrust via + signed-square per-axis gains, ballistic droop as fuel burns, PROXIMITY FUSE on seeker + rangeToTarget, max-range + altitude-floor retire). + ## Weapon fire is a PORT reconstruction (the dpl_* beam layer is unported) `Emitter::FireWeapon` builds a muzzle→target beam but the `dpl_*` beam renderable was never ported → the beam is drawn by `BTPushBeam`/`BTDrawBeams` (L4VIDEO — an additive quad / the real `ermlaser.bgf` @@ -201,10 +266,11 @@ Real sample (`ava1`, 7 rows × 8 cells): rows 0-2 `rotateWithTorso=0` (feet/legs rows 3-6 `=1` (upper body — rotates with torso twist); a foot cell = `{0.5→16, 0.8→10, 1.0→5}`. The table is found by the mech's **type-0x14 DamageZoneStream NAME** (`ResourceDescription::resourceName`) → `ResourceFile::FindResourceDescription(name, DamageLookupTableStreamResourceType/*29*/)`. -So STEP 6 remaining = (6b) fix the name-load (current `ResourceFindByName` is a NO-OP template stub → empty -name → 0 rows); (6c) make the table functional + `ResolveHit→zone`; (6d) register the -`Mech::TakeDamageMessageHandler` override + verify. **Nothing is guessed — geometry, roll, handler, and byte -format are all verified.** +So STEP 6 was = (6b) the name-load; (6c) the table + `ResolveHit→zone`; (6d) the +`Mech::TakeDamageMessageHandler` override. **✅ ALL DONE — see "STEP 6 COMPLETE" below (`d07ac7d`):** the +name-load BYPASSED the no-op `ResourceFindByName` via `SearchList(type=0x14)` (mech.cpp:1631), so the table +loads live (`[cyl] table 'bhk1'/'madcat'/'ava1' layers=7`) and `ResolveHit` resolves unaimed hits to zones. +**Nothing was guessed — geometry, roll, handler, and byte format all verified.** ## The classes ALREADY EXIST — `game/reconstructed/dmgtable.cpp` (2026-07 discovery) The three container classes are **already reconstructed** in `dmgtable.cpp`/`.hpp` (in the build): diff --git a/context/decomp-reference.md b/context/decomp-reference.md index 890bf31..0b701f7 100644 --- a/context/decomp-reference.md +++ b/context/decomp-reference.md @@ -58,13 +58,13 @@ reconciled via `CLASSMAP.md`, not the case label. [T1] | 0xBC3 | **Sensor** | — | `case`=MyomersClassID | | 0xBC4 | **Gyroscope** | @004b3778 | LIVE (task #56: byte-exact ctor/integrators/writers; hit-bounce fan-out wired) | | 0xBC5 | **Torso** | @004b6b0c | sizeof 0x280; `case`=SinkSource | -| 0xBC6 | **Myomers** | @4b8fec | `case`=Actuator; INERT un-stub | +| 0xBC6 | **Myomers** | @4b8fec | `case`=Actuator; un-stubbed (`8b36440`, full recon; per-tick Sim SetPerformance call-site still best-effort) | | 0xBC8 | **Emitter** | @004b1d18 | sizeof 0x478; energy weapon | | 0xBCB | **AmmoBin** | — | `case`=JumpJet; sizeof 0x22C | | 0xBCD | **ProjectileWeapon** | @4bc3fc | sizeof 0x448 (`case`≠ this — see VDATA.h) | | 0xBCE | **GaussRifle** | @4bdcb4 | : Emitter; FireWeapon is a **no-op** in this build | | 0xBD0 | **MissileLauncher** | @4bcff0 | sizeof 0x44C | -| 0xBD3 | **SubsystemMessageManager** | @0049bca4 | damage/explosion consolidation hub (ConsolidateAndSendDamage, weaponExplosions); cached to `Mech[0x10d]`=**0x434** (recon MISLABELS it `controlsMapper`; the drive squats there — the real mapper is roster slot 0). NOT the valve/Myomers gate (that's the owning **BTPlayer** @mech+0x190). WAVE 8 = untangle + damage-hub. | +| 0xBD3 | **SubsystemMessageManager** | @0049bca4 | damage/explosion consolidation hub (ConsolidateAndSendDamage, weaponExplosions); cached to `Mech[0x10d]`=**0x434** (✅ task #7 `afefaee`: factory builds the real class + `mech.hpp` names it `messageManager`; the `controlsMapper` mislabel is swept — the real mapper is roster slot 0). NOT the valve/Myomers gate (that's the owning **BTPlayer** @mech+0x190). | | 0xBD4 | **PPC** (: Emitter) | — | same FireWeapon as Emitter; `factory calls it GaussRifle` | | 0xBD6 | **HUD** | — | alloc 0x2a4; `case`=MechDisplay-ish | | 0xBD8 | **Searchlight** | — | `case`=LegSubsystem | @@ -113,7 +113,7 @@ valid for reading the decomp, NOT for our compiled layout (see [[reconstruction- | +0x11c / +0x120 | `damageZoneCount` / `damageZones[]` | inherited Entity; Mech ctor populates the array | | +0x124 / +0x128 | `subsystemCount` / `subsystemArray[]` | the [[subsystem-roster]] (NOT the segment table) | | +0x190 | **owning `BTPlayer`** (`GetPlayerLink()`) | set by `FUN_0049f624` (mech↔player bind; also `player->playerVehicle(+0x1fc)=mech`). The valve guard (`FUN_004ac9c8 →player+0x274`) + Myomers gate (`FUN_004ad7d4 →player+0x260`) read the owning player's game-mode flags (NOT the messmgr @0x434). `MECH_OWNING_PLAYER`. | -| +0x434 (word 0x10d) | `SubsystemMessageManager` cache | the 0xBD3 damage/explosion hub (factory @10142). Our recon MISLABELS this `controlsMapper` + the drive squats here; the real mapper is roster slot 0. | +| +0x434 (word 0x10d) | `SubsystemMessageManager` cache | the 0xBD3 damage/explosion hub (factory @10142). ✅ task #7 (`afefaee`): `mech.hpp` names this `messageManager`; the `controlsMapper` mislabel is swept — the real mapper is roster slot 0. | | +0x260 / +0x26c | `localOrigin` = { Point3D linearPosition; Quaternion angularPosition } | ORIGIN.h:15; position@+0x100 in some subsystem views | | +0x300 | segment table | skeleton `EntitySegment`s (muzzle sites, joints) | | +0x37c | target world `Point3D` | aim source | @@ -127,12 +127,41 @@ valid for reading the decomp, NOT for our compiled layout (see [[reconstruction- `mech+0x100` (a subsystem-view of localOrigin.linearPosition): `.x@+0x100, .z@+0x108`. [T1] +### Binary ATTRIBUTE TABLES (2026-07-16, walked from the image) [T1] + +**Row format: 16-byte `{id, namePtr, memberOffset+1, 0}`** (MIND THE ALIGNMENT — an 8-off walk +invents a fake one-slot shift; verified against the live-tested ConfigureActivePress row +@0x50de54). Walker: find the attr NAME string in DATA, find the pointer to it, decode ±16-byte +rows until sanity fails (`python + struct`, see the session commits `cc2b109`/`21378ec`). + +- **Mech entity table @0x50c0xx, ids 21–56**: MaxAcceleration@0x344 (=forwardCycleRate), + CurrentSpeed@0x348 (=legCycleSpeed), MaxRunSpeed@0x34c (=run-cycle avg = the mapper's demand + multiplier), Collision{State,Normal,Speed,MaterialType}@0x450/0x4a8/0x4b4/0x4a4, + EyepointRotation@0x360 (the eye renderable's view rotation — the old `mechName@0x360` label is a + MISREAD), TargetReticle@0x36c, FootStep@0x394, AnimationState@0x39c, ReplicantAnimationState + @0x714, LinearSpeed/AngularSpeed/ClimbRate/AccelerationLastFrame@0x81c/0x824/0x828/0x82c (the + 15-sample-ring velocity means + the accel snapshot), damage levels @0x354-0x35c, TestButton1-6 + @0x328-0x33c, ReduceButton@0x340, Radar{Range,LinearPosition,AngularPosition}@0x404-0x40c, + **RearFiring@0x410** (OR of the weapons' rear flags; old `stateFlags` label was wrong), + RequestDuckAnimation@0x398, UnstablePercentage@0x3f0, SuperStop@0x3f4, DuckState@0x3f8, + IncomingLock@0x3fc, DistanceToMissile@0x400 (FLT_MAX far default; old `maxSpeed` label wrong). +- **HeatSink table @0x50e438, ids 3–12**: HeatSink@0x164, CurrentTemperature@0x114, + Degradation/FailureTemperature@0x118/0x11c, Normalized/DegradationPressure@0x120/0x124, + CoolantCapacity@0x128, CoolantMass@0x12c, CoolantMassLeakRate@0x130, **ReportLeak@0x138** + (= coolantActive, the leak hysteresis flag). Confirms every port heat binding. +- **Torso table, ids 3–15**: RotationOfTorsoVertical/Horizontal@0x1e4/0x1d8, HorizontalLimit + R/L@0x1dc/0x1e0, SpeedOfTorsoVertical/Horizontal@0x1ec/0x1e8 (|rate|, abs'd), + StickPosition@0x1f0, TorsoUp/Down/Left/Right/Center@0x1f8-0x208, MotionState@0x20c + (statusFlags; ==2 = the limit-hit frame). +- **MechWeapon rows**: RearFiring id 0x1B @0x334 = "the mount segment page name contains 'b'" + (marker @0x511aa2 — the back gun ports `sitelbgunport`/`siterbgunport`; ctor part_013.c:6913-30). + --- ## 4. Damage delivery - `Entity::TakeDamageMessage(id, size, inflictingEntityID, zone, Damage&)` → `target->Dispatch(&msg)`. -- Base `Entity::TakeDamageMessageHandler` **IGNORES zone==-1** — send a VALID zone index (the Mech cylinder-lookup override for unaimed hits is not reconstructed). [T1] +- Base `Entity::TakeDamageMessageHandler` **IGNORES zone==-1** — the Mech STEP-6 cylinder-lookup override (✅ done, `d07ac7d`; `dmgtable.cpp` `ResolveHit` + `Mech::damageLookupTable`) resolves an unaimed (zone==-1) hit to a zone before base-routing. [T1/T2] - `class Damage { damageType(enum Collision/Ballistic/Explosive/Laser/Energy), damageAmount, damageForce, surfaceNormal, impactPoint, burstCount }`. [T1] - Weapon effect id: **"explode" = 13** (`Explosion::Make`). [T2] - `DestroyEntityMessage(id,size)` removes an entity — but a killed mech STAYS (a WRECK); death = a STATE transition (`SetGraphicState(DestroyedGraphicState)`), NOT removal. Issuing removal-on-death is the P5 teardown bug (do not). [T2] diff --git a/context/locomotion.md b/context/locomotion.md index b184b5f..3736377 100644 --- a/context/locomotion.md +++ b/context/locomotion.md @@ -6,7 +6,7 @@ source_sections: "PROGRESS_LOG.md §7, §10 (gait/ground/collision/shadow); docs related_topics: [asset-formats, decomp-reference, reconstruction-gotchas, combat-damage, rendering] key_terms: [RootTranslation, ground-model, SequenceController, MechControlsMapper, SLD, joint] open_questions: - - "Authentic per-mech turn-rate constant (currently bring-up constant; the authored WalkingTurnRate/RunningTurnRate now STREAM correctly since the task #4 record-layout fix -- rec words 0x18/0x19 deg->rad -> mech+0x574/0x578, Wword-absorbed -- wiring them in retires the bring-up rate)" + - "Turn-in-place FEEL re-verification (authentic release at standSpeed vs the earlier 0.25x stand-in -- subjective playtest, non-gating)" - "Airborne clip callbacks (FUN_004a7970) deferred" --- @@ -114,6 +114,19 @@ through walk/run caps (`LoadLocomotionClips`, mech3.cpp:326 — measures `walkSt `legAnimation.SelectSequence`). Transition callbacks (`Mech::BodyClipFinished @0x4a6d8c`, `LegClipFinished @0x4a6928` — PE-parsed jump tables) alternate walk 6↔7 / run 12↔13 + walk→run. [T2] +**Gait DEAD BAND (2026-07-16, byte-decoded) [T1]:** the SM has NO stable state for a demand in +(`walkStrideLength`@0x534, `reverseSpeedMax`@0x538). Cap semantics (LoadLocomotionClips @0x4a80d4): +0x534 = walk-cycle avg root speed (clips 6+7); **0x538 = the LAST KEYFRAME SPEED of clip slot 10 = +the walk→run transition clip's exit speed — the run ENGAGE threshold, not a "reverse max"**; 0x34c += run-cycle avg root speed (clips 12+13) — and 0x34c is the mapper's demand multiplier +(`FUN_004afd10`: `speedDemand = mech->0x34c × throttle × 0x5c0`, continuous). Walk cyc clamps to +[standSpeed, 0x534]; run cyc clamps to [0x538, 0x7a0]. The finished-callback up-shifts when +`tgt > 0x534` but enters/sustains run only when `tgt ≥ 0x538` (x87 decode of 0x4a6f11/0x4a6fc7/ +0x4a7041) → a demand parked between them cycles 7→10→14→7 forever ("gallop hunt"), firing the +authored EngineShiftFwd/Rev audio each swing (Blackhawk: 22.02–30.87 = throttle 36–50%). Faithful +mechanics — likely masked on real hardware by MECHANICAL lever detents ([[pod-hardware]] [T4]). +Port: keyboard lever snaps out of the band at rest (mech4.cpp "GAIT DETENT" accommodation). + ## Turn-in-place (trn, state 4) — AUTHENTIC dispatcher decoded (task #64, 2026-07-14) **The entry dispatcher is NOT in the Ghidra decomp** — it lives in the master-perf gap `0x4a9b5c–0x4ab188` (functions_index.tsv jumps FUN_004a9770→FUN_004ab188; the SOLE reader of diff --git a/context/multiplayer.md b/context/multiplayer.md index 2e3561f..70fa49b 100644 --- a/context/multiplayer.md +++ b/context/multiplayer.md @@ -6,7 +6,7 @@ source_sections: "PROGRESS_LOG.md §7 (Phase 7), §8 (P6); docs/HARD_PROBLEMS.md related_topics: [wintesla-port, combat-damage, locomotion, decomp-reference] key_terms: [master, replicant, EGG, dead-reckon] open_questions: - - "pod-LAN config (real IPs); update-record velocity from the body-channel projection; peer SimulationState/DropZoneLocation replication (authentic peer warp spheres); authentic per-mech turn rates now streamed (task #4) but not yet wired into the turn code" + - "pod-LAN config (real IPs); peer warp DropZoneLocation-anchoring [T3] (world-anchored peer sphere already visible, 160b78e); console-death replication freeze (pending recurrence to diagnose)" --- # Multiplayer @@ -262,8 +262,18 @@ Base fns in every subsystem vtable: slot 6 = 41bd34 (Read/apply), slot 7 = 41c50 invisible). First real twist record overwrites it. ## Remaining (P6 phase 4 / Phase 7) -The pod-LAN config (real IPs, bare-IP pilot entries); update-record velocity sourced from the body-channel -projection (above). See [[open-questions]]. [T3] +Pod-LAN config: **real-IP path VALIDATED (2026-07-15)** — an egg with the machine's real LAN IP in +`[pilots]` (not `127.0.0.1`) connected + replicated + moved end-to-end (`Connected to +GameMachineHost at 10.0.0.46:1602`, `All connections completed!`, peer telemetry flowing). So +`gethostbyname` local-address matching (L4NET.CPP:2612, which also appends 127.0.0.1) + the +`WSAStringToAddressA` `IP:PORT` parse (ResolveAddress) both work with real addresses — NOT +localhost-locked. Recipe: egg `[pilots]` = each machine's LAN IP + its GAME port (`= -net port + 1`, +e.g. `10.0.0.46:1502`); each machine runs `btl4 -egg lan.egg -net `; console relay +(`btconsole.py lan.egg A_IP:1501 B_IP:1601`) from any machine; open the TCP console+game ports in +the Windows firewall. STILL UNTESTED: an ACTUAL two-physical-machine run (validated single-box via +the real LAN IP), so firewall is the main first-attempt unknown. Affinity pinning is NOT needed +across separate machines (the packet-jitter artifact is single-box only). Also: update-record +velocity sourced from the body-channel projection (above). See [[open-questions]]. [T2 path / T3 two-box] ## MP-front scout (task #45, 2026-07-09) — the P6 chain SURVIVES the combat/HUD rework [T2] Re-ran the one-box smoke test on the current build (world-pick targeting, weapon groups, death @@ -342,15 +352,21 @@ transition, HUD all landed since P6): console egg → mesh → RunningMission on defect that hid it for the whole effort was NOT the warp code — it was a general **texture-scroll precision-collapse bug** in `L4D3D::SetTextureScrolling` (offset = `scrollDelta × absolute_time` → unbounded → UV precision loss → radial "spokes"), fixed with `fmodf`; that also - cleans the scrolling beam grit. See [[reconstruction-gotchas]] §13. REMAINING: the AUTHENTIC peer - path (see OTHERS' spheres) still needs player SimulationState/DropZoneLocation replication — until - then the local-player sphere stands in; and confirm the COLLAPSE (death-side) warp fires on every - MP death path (the force-damage test logged EXPAND reliably, COLLAPSE less so). + cleans the scrolling beam grit. See [[reconstruction-gotchas]] §13. PEER WARP IS WIRED + VISIBLE + (`160b78e`): an observer sees a peer's un-wreck warp via the world-anchored `BTStartWarpEffect` + (mechdmg.cpp:1074, `ReplicantInstance`-gated; `simulationState` rides every record header). + REMAINING [T3, non-gating]: the peer sphere is anchored to the peer's WORLD position, not the + peer's authentic `DropZoneLocation` (that attribute isn't replicated) — a fidelity refinement. + Also confirm the COLLAPSE (death-side) warp fires on every MP death path (force-damage test logged + EXPAND reliably, COLLAPSE less so). NOTE the respawn-cycle SIM path is separate + done: `DropZoneReply` (FUN_004bffd0) → `Mech::Reset` (@0049fb74) + the level-2 "translocated" cockpit alarm; `CreatePlayerVehicle` (FUN_004bfcac, disassembled — make+link only, matches RP analog). The sphere is a pure RENDER-side watcher on the SimulationState dial, independent of that sim path. - Mech::Reset (@0049fb74) full subsystem-reset sweep is still a bring-up TODO. + Mech::Reset (@0049fb74) does the full subsystem-reset sweep (mech4.cpp:1616 loops every subsystem → + `DeathReset(mode)`, restoring heat/power/ammo/charge) + heals all damage zones + ForceUpdate(0x1f). + (Per-subsystem `DeathReset` bodies are authentically trivial/empty for some classes — the shipped + build folds those overrides — so "fuller" reset behavior is per-subsystem, not a missing sweep.) **Three respawn bugs found + fixed on the way (all [T2], 2-node force-damage verified):** (i) **death-latch un-latch** — `IsMechDestroyed` tested `graphicAlarm>=9` alone; a later leg hit on the wreck rewrites the alarm to 4/3, un-latching → the death transition RE-RAN (double kill, @@ -393,3 +409,19 @@ transition, HUD all landed since P6): console egg → mesh → RunningMission on ## Key Relationships - Base: [[wintesla-port]] (L4NET). Depends on: [[locomotion]] (update writer), [[combat-damage]] (entity lifecycle). Detail: `docs/HARD_PROBLEMS.md` (P6). + + +## Authentic coupled peer motion — DONE (task #50, 2026-07-14) [T1] +Decomp-verified (workflows w0odszxro/wh1h5gnmc, 3 make-or-break claims adversarially CONFIRMED; committed c52a1ad + default-promoted). The 1995 peer per-frame path FUN_004ab430 -> FUN_004ab1c8 (IntegrateMotion) drives motion SINGLE-SOURCE: +- LINEAR position = GAIT. IntegrateMotion advances the BODY gait channel FUN_004a5678 (FUN_004a5028 is DEAD CODE, zero call sites), turns its cycleDistance into velocity `{0,0,-cd/dt}`, rotates by heading, integrates into projectedOrigin.linear@0x260 (part_012.c:14994/14997/15008); localOrigin.linear@0x100 is copied VERBATIM from it (FUN_00408440, part_012.c:15131 -- NOT a lerp). updateVelocity.linear is never used for position. +- ANGULAR/heading = replicated-velocity slerp (the ONLY thing Mover::DeadReckon-style velocity extrapolation drives): projectedOrigin.angular@0x26c built from updateVelocity.angular@0x2d4 (FUN_004ab188), localOrigin.angular slerped toward it. +- 0x260 aliasing: "motionDelta@0x260" and "projectedOrigin@0x260" are LITERALLY THE SAME field (base Mover::projectedOrigin, Origin 0x1c); "worldPose@0x26c" is its quaternion half. Our reconstruction's shadow names hid this and drove the false velocity-vs-gait "contradiction". +- Re-anchor: motionEventVector@0x598 = updateOrigin - localOrigin captured on poseSyncLatch, DECAYED into localOrigin over ~_DAT_004ab9cc (~0.2s) -- no snap. Zeroed in idle states. +- MASTER + PEER run the SAME predictor. The master's SEND-mirror (FUN_004a9b5c @0x4aab9c) advances projectedOrigin by IntegrateMotion(mj=0) fed the LAST-SENT bodyTargetSpeed, then deadbands |localOrigin-projectedOrigin|^2 vs UpdatePositionDiffrence@0x768. A gait-driven peer REQUIRES this gait mirror (a constant-velocity mirror under-sends -> tug-of-war, measured ratio 1.86). +- T4 CLOSED: both leg(0x65c) and body(0x6bc) animators are AnimationInstances of the SAME JointedMover/JointSubsystem (JMOVER.cpp:1382) -- either poses the WHOLE skeleton. Our port keeps the peer on the LEG channel (body unbound on the peer); faithful functional equivalent. + +RECONSTRUCTION STATE: coupled path is DEFAULT ON (mech4.cpp: s_drPos gait-coupled, s_gaitMirror/Send). Reverts: BT_DR_POS=1 (peer velocity), BT_NO_MASTER_GAITMIRROR (master const-vel mirror). Measured: single-source coupling backward-steps 0.1% (vs the two-source split's churn), position ratio 1.043. RESIDUAL (follow-up): MEASURED SMALL / effectively resolved (2026-07-15). With the peer now on the BODY channel (`96a896a`, matching the master's body-channel mirror) + affinity pinning, an accel/decel soak (node B `BT_AUTODRIVE`+`BT_DRIVE_SWEEP0` through STOP, node A `BT_SNAPLOG`) measures peer drift maxing at **~0.64u** (median 0.54u, 18 events >0.5u over 48s), gently absorbed at k~0.24/frame -- SUB-UNIT, not the earlier ~2.9u (which predated the body-channel swap AND was measured without CPU-affinity, i.e. with packet-jitter-sparse records). At the noise floor; no fix warranted. The old "run the master mirror on a leg-channel prediction" plan is moot now the peer is on the body channel. Still open (cosmetic): exact `_DAT_004ab9cc` decay constant; whether IntegrateMotion's 2-stage angular integrate is intended. + + +## Peer motion: the "random shakiness" is single-box packet jitter, NOT the game (task #50, 2026-07-15) [T2] +After the coupled body-channel peer landed (96a896a + turn-step f094d78 + cadence-mirror 23f1532), residual peer shakiness on accel/decel was RANDOM -- sometimes perfect, sometimes shaky. Root cause is the TEST RIG, proven with BT_RXJIT (record inter-arrival ms on the peer): two Debug btl4 nodes on ONE box contend for CPU, so Windows BATCHES their TCP delivery -- update records arrive in bursts (min ~0ms back-to-back, max 56-226ms gaps, burstiness max/avg 3-7x) instead of an even ~17ms. A peer dead-reckons across the long gaps then snaps when a record lands -> random shake. Pinning each node to a DISJOINT core set (ProcessorAffinity 0x00F / 0x3C0) restored even ~17ms delivery (burstiness ~1.0) and the shakiness vanished (user-confirmed). REAL pods are dedicated machines with no contention -- they never see this. DO NOT add an interpolation/jitter buffer or other un-authentic netcode to mask a single-box artifact. Use tools/mp_launch.sh (bakes in the affinity) for all 2-node tests. Diagnostic env: BT_RXJIT (arrival jitter), BT_SLIDE ([slide]/[mslide] slide-in-stand), BT_GAITEV, BT_MIRDIV, BT_NO_MIRROR_CAD / BT_PEER_LEGCH (revert the coupled-peer fixes). \ No newline at end of file diff --git a/context/open-questions.md b/context/open-questions.md index 9223999..7d8ee17 100644 --- a/context/open-questions.md +++ b/context/open-questions.md @@ -20,6 +20,14 @@ authentic path scoped. `BTL4.RES` already has 8 maps + the full mech anim set.) Status: OPEN. - Pod specifics for Phase 8: the 7-monitor driver setup, the RIO cockpit I/O protocol, current Win10+wrapper pod config. Status: OPEN. +- **Did the pod throttle quadrant have MECHANICAL detents** (how many / lever positions)? The + software path is notch-free [T1, [[pod-hardware]]], but the gait SM's walk/run dead band + ([[locomotion]]) only makes sense if the hardware discouraged parking the lever mid-band — + "5 speeds" pod lore suggests physical notches. Status: OPEN. +- ~~Which mechs used rear fire~~ **ANSWERED from data (2026-07-16, task #68)**: the Blackhawk's + ERMLaser_2/3 (back gun ports) are the only rear weapons in the roster; see [[combat-damage]]. + Remaining: bind the side/down look views to keys (only 'V' look-back is bound); pod look-button + mappings for Phase 8. ## BINARY-COVERAGE AUDIT discoveries (2026-07-13, 6-agent decomp census) [T1 verified-uncited] New unaccounted functionality no prior list knew (addresses verified absent from game/+context/+docs): @@ -154,6 +162,17 @@ register. âš  The audit also flags the damage-economy item as SELF-CONTRADICTOR centred); (c) pre-burial, the pick still tests the sinking wreck's collision box at its parked position (pre-existing wreck-targeting behavior). +- **✅ WORLD STRUCTURES (garages/walls) now targetable (task #50, 2026-07-15)** — the world-pick's + non-mech tier previously only sampled the visual heightfield (`BTGroundRayHit`), which on arena1 + is a single flat `'sky'` ground mesh → shots passed THROUGH the garages and only mechs moved the + range axis (user-reported regression). Resolved by ray-testing the **zone's static collision + solid tree** (the geometry that already blocks the walk) via the authentic engine query + `Mover::FindBoxedSolidHitBy`'s static-world tail — factored into `Mover::FindStaticSolidHitBy` + + `Mech::WorldStructurePick`. See [[combat-damage]] "WORLD STRUCTURES ARE TARGETABLE". This also + closes the old "0x388 writer is un-exported / unknown world-pick mechanism" residue: the + authentic non-mech world pick is the static-solid-tree ray query (`BoxedSolidTree::FindBoundingBoxHitBy`), + the same one `FindBoxedSolidHitBy` uses for mover-vs-world collision. Interactive aim is + user-verified (headless has no `BTGetAimRay`); the `BT_WSWEEP` ray-fan is the headless proof. - **Cockpit HUD leftovers (tasks #35-#38 residue, 2026-07-08)** — the reticle + 7 weapon pips + ALL the instrument dynamics are LIVE (the Execute @004cdcf0 gap was RECOVERED via capstone disasm, task #37; the authentic Lock producer + the simple-X mode landed in #38 — see @@ -201,8 +220,9 @@ register. âš  The audit also flags the damage-economy item as SELF-CONTRADICTOR Also: `deathAnimationLatched`/`legResetLatch` were never ctor-initialised (0xCDCDCDCD gate bug, fixed in mech.cpp ctor). See [[cockpit-view]]. - **MechControlsMapper look/eyepoint commit** — reconstructed but its offsets collide with declared - members; arbitrate before enabling (no port consumer yet). The fire-trigger-through-mapper is - still the `gBTWeaponTrigger` bring-up. + members; arbitrate before enabling (no port consumer yet). (The fire-trigger-through-mapper is NO + LONGER the bring-up: the `gBT*Trigger` globals are retired / never-read — the authentic LBE4 + buttonGroup path replaced them, task #5 `8ed6184`.) - **✅ Weapon groups — the AUTHENTIC defaults + fire path LIVE (task #5, 2026-07-11) [T1/T2].** The system decoded end-to-end: a fire "channel" = a physical `buttonGroup` slot in the global `LBE4ControlsManager` (fire buttons 0x40 Trigger / 0x45 Pinky / 0x46 ThumbLow / 0x47 ThumbHigh); @@ -355,8 +375,12 @@ register. âš  The audit also flags the damage-economy item as SELF-CONTRADICTOR three coolant epsilon corrections (0.0025/0.003/1e-4 for the old single 1e-4) + CoolantCapacityScale = 0.05 float80 (both prior readings wrong). Verified: heat flows, exits (~2/3 of a 1e10 spam run drained), weapons overheat under max-rate autofire exactly - as the authored-constant simulation predicts (thermal spam is unsustainable BY DESIGN; - heatLoad -> effectiveRange 0 = the authentic overheat cutout). + as the authored-constant simulation predicts (thermal spam is unsustainable BY DESIGN; the + authentic overheat cutout is the FailureHeat CHARGE-HOLD -- `ResetFiringState` + currentLevel=0 + until it cools, @004baa88 -- NOT a range collapse. âš  CORRECTION (`aab7a8a`, task #50): + effectiveRange = (1 - HOST-ZONE damage) x weaponRange reads `Subsystem::damageZone->damageLevel` + (weapon+0xE0), NOT heatLoad; the old "heatLoad -> effectiveRange 0" mechanism here was the same + @0xE0-DamageZone-vs-heat misattribution fixed in heat.cpp:803). - **Task #10 -- the "scrambled routing" RESOLVED (2026-07-11) [T2].** The routing was NEVER scrambled: a `[heat-link]` attach log proved every subsystem links its authored sink exactly (bhk1: PPC_1->C4, PPC_2->C6, ERM_1/2/3->C1/C6/C4, SRM6s->C2, generators->C1/C2/C3/C5, @@ -413,32 +437,32 @@ register. âš  The audit also flags the damage-economy item as SELF-CONTRADICTOR under reconstruction: config-mode regrouping (task #6), generator reassignment (ids 4-8), coolant valves. -- **AUTOCANNON JAM / coolant-priority — mechanism FAITHFUL + COMPLETE; a CALIBRATION question - pending QA ground-truth (2026-07-14) [T1 mechanism / T3 pacing].** An original QA dev reports: - "all AFCs jam on their 3rd shot if coolant priority isn't boosted; boosted → almost never jam." - Full decomp hunt (2 agents) confirms the whole chain is reconstructed end-to-end and the port is - FAITHFUL — **do NOT "fix" it by driving a weapon's `coolantFlowScale` from the valve; that field - being a constant 1.0 for weapons is AUTHENTIC** (the ONLY writer of any HeatSink `+0x15C` from a - valve value is `RecomputeCondenserValves`/FUN_0049f788, which iterates ONLY the condenser chain - `mech+0x7cc`). Authentic mechanism: a weapon dumps `heatCostToFire` (AFC100=6.5e7 raw) into its - OWN linked condenser; the condenser sheds to the shared bank at a rate scaled by its valve SHARE - (`coolantFlowScale = valveState_i / Σ valveState`, MoveValve msg id 4, detents 1/5/50/0). Boosting - ONE condenser's valve (share ~0.17→~0.9 = ~5× conductance) drops its equilibrium temp so the - weapon dumping into it stays below `degradationTemperature`(1000) → `heatAlarm`=0 → CheckForJam - early-returns → no jam. Starved → condenser saturates → weapon crosses 1000 → the STICKY - probabilistic jam (`p=clamp(0.41·T/failT,0.05,1)`, JammedState 5, clears only on ResetToInitialState) - latches. Two distinct "jam" states: **degradation 1000 → probabilistic sticky jam** (the pilot's - "jam"); **failure 2000 → deterministic self-clearing overheat lockout** (recoil-pinned + weaponAlarm 7, - re-fires each frame until it cools). ALL thermal constants load strictly from the resource (no - stand-in fallback — a missing value ERRORS). **Empirical (BT_JAM_LOG/BT_VALVE, madcat AFC100):** - uncooled the weapon crosses 1000 at ~shot 4 and PLATEAUS ~1400 (conduction balances input), giving - ~25% probabilistic jam — directionally right but NOT the QA's deterministic "3rd shot". Gap is - calibration (condenser thermalMass/conductance live in LAST.EGG, unread) + scenario (fire rate; - which condenser boosted — a uniform BT_VALVE is normalized away, need a targeted per-condenser - boost). NEXT: get QA to clarify (a) deterministic lockout vs probabilistic jam, (b) per-weapon vs - global coolant boost + which control; THEN verify calibration — do NOT tune the heat economy to - force "3rd shot" without ground-truth (would be un-faithful). Diag probes committed: `BT_JAM_LOG` - (projweap.cpp), `BT_VALVE` (heatfamily_reslice.cpp). See [[combat-damage]], [[subsystems]] heat. +- **✅ AUTOCANNON JAM / coolant-priority — FAITHFUL + CONFIRMED against QA ground-truth, SOLVED + purely from the decomp (2026-07-14) [T1/T2].** An original QA dev reported: "all AFCs jam on their + 3rd shot if coolant priority isn't boosted; boosted → almost never jam." VERDICT: the whole chain + is reconstructed end-to-end, FAITHFUL, and reproduces the reported behavior — no bug, no stand-in. + **DO NOT "fix" it by driving a weapon's `coolantFlowScale` from the valve; that field being a + constant 1.0 for weapons is AUTHENTIC** (the ONLY writer of any HeatSink `+0x15C` from a valve value + is `RecomputeCondenserValves`/FUN_0049f788, which iterates ONLY the condenser chain `mech+0x7cc`). + Mechanism: a weapon dumps `heatCostToFire` (AFC100=6.5e7 raw) into its OWN linked condenser (AFC100→ + Condenser4); the condenser sheds to the shared bank at a rate scaled by its valve SHARE + (`coolantFlowScale = valveState_i / Σ valveState`, MoveValve msg id 4, detents 1/5/50/0). The + weapon's OWN thermal conductance (35500) caps how fast it sheds into its condenser, so coolant + priority shifts the weapon's equilibrium only ~100 K — but that is EXACTLY enough to straddle the + `degradationTemperature`(1000) jam line at combat fire rates. Cross `1000` → `heatAlarm`≥1 → the + jam roll arms (`p=clamp(0.41·T/failT, 0.05, 1)`); on a hit → sticky `JammedState`(5), clears only on + ResetToInitialState. (Distinct: `failureTemperature`(2000) → self-clearing overheat lockout, + recoil-pinned + weaponAlarm 7.) **ALL constants VERIFIED AUTHENTIC vs the raw BTL4.RES bytes** + (`scratchpad/afc_dump.py`): AFC100 startT=77, degradT=1000, failT=2000, thermalConductance=35500, + thermalMass=94700, heatCostToFire(+0x1A4)=6.5e7, recharge(+0x190)=8, damage=25, minJamChance=0.05 — + no mis-scaling. **THE ANSWER IS FIRE-RATE-DEPENDENT** (measured, BT_JAM_LOG/BT_VALVE_BOOST/BT_AF_PERIOD, + madcat AFC100 → Condenser4): at MAX spam (8s recharge) the AFC overheats regardless (~1460 boosted + ~1360 — both jam); at a SLOW cadence (16s) it plateaus ~770 and never jams; at the ~11s COMBAT cadence + it is DECISIVE — **not boosted → 1058 K (jams), boosted (Condenser4 valve→50, others→1) → 953 K (never + jams)**. So the QA is right and the reconstruction is faithful; the earlier "boost is weak" read was + an artifact of testing at max spam (above the window). Diag probes committed: `BT_JAM_LOG` + (projweap.cpp), `BT_VALVE`/`BT_VALVE_BOOST=` (heatfamily_reslice.cpp), `BT_AF_PERIOD=` + (mech4.cpp). See [[combat-damage]], [[subsystems]] heat. - **Subsystem-panel online/offline gate — fields unidentified [T4] (2026-07-12).** The binary's SubsystemCluster draw-state reads `*(subsystem+0x40)==1` and `*(subsystem+0x278)!=4`; neither @@ -536,11 +560,30 @@ register. âš  The audit also flags the damage-economy item as SELF-CONTRADICTOR ctor per mech, zone destroyed → crits damaged/DESTROYED with authentic accumulation. Residue: roster idx 0/1 (mapper installed post-ctor / voltage-bus stub) stay unbound with a logged skip — none of the 8 mechs' streams crit-list them in practice. -- **Two dead Wword comparison branches (multiplayer)**: `mech.cpp:1511` + `:1613` (replicant - leg-state/stability sync in ReadUpdateRecord) — always-false BTVal comparisons ([[reconstruction-gotchas]] §2); - needs slots 0xf/0x10 promoted to named members when P6 replication work resumes. +- **✅ Two dead Wword comparison branches — DONE (task #1, `c9f0c2a`).** `Mech::ReadUpdateRecord` + case 3 now reads real named fields off `Mech__StateUpdateRecord` (legState / stability / legResetLatch / + speedDemand); the always-false Wword/BTVal branches are gone (the old `mech.cpp:1511`/`:1613` line + numbers no longer point at that code). ## Multiplayer (Phase 7 / P6) +- **✅ Console-death peer-replication freeze — NOT REPRODUCIBLE + the one real bug FIXED + (2026-07-15) [T2].** The 2026-07-14 report (btconsole.py died mid-match → peer↔peer replication + froze both ways) did NOT reproduce in the current build under EITHER failure mode, tested directly + (BT_REPL_LOG on a circling peer, affinity-pinned): (1) a clean relay kill — the replicant kept + circling smoothly through the kill, NO disconnect logged, node survived; (2) a deliberately STUCK + console (`scratchpad/btconsole_stuck.py` — connects, starts the mission, then stops recv()'ing while + holding the socket OPEN = the exact "receive pad full → close never seen" mode hypothesized) — the + replicant kept moving the entire run. So **peer replication is INDEPENDENT of the console**: the + pods replicate peer-to-peer over the GAME socket; the console is a separate egg/mission/status + channel. The original freeze was a transient (plausibly the same single-box packet-jitter / + CPU-contention artifact root-caused 2026-07-15, `49d73dc`) or was fixed by later MP work. + **The flagged latent bug IS real and is now FIXED:** `HostDisconnectedMessageHandler`'s + ConsoleHostType branch closed **`gameListenerSocket`** (the GAME listener) on a CONSOLE disconnect — + a naming bug; the comment + the commented-out `OpenConnection` intended a CONSOLE re-listen, which + `CreateConsoleHost()` already does. Removed (L4NET.CPP:969). Harmless to established peer sockets + (why a live match keeps replicating through console loss), but it would have blocked a NEW peer + from joining after a console cycle. Verified: 2-node still connects (`All connections completed!`) + + the peer circles. Diag harness retained: `scratchpad/btconsole_stuck.py`. - ✅ Cross-pod COMBAT — DONE (tasks #46/#47: replicant targeting + damage rerouted to the owning master, full cross-pod kill verified). ✅ Replicant GAIT animation — DONE (task #50: replicant derives speedDemand from the replicated velocity, full stand→walk→run lifecycle verified). @@ -597,3 +640,31 @@ register. âš  The audit also flags the damage-economy item as SELF-CONTRADICTOR ## Key Relationships - Feeds from: every subsystem/render topic (their deferral notes collect here). - Gating master: [[source-completeness]] (the missing BT source). + +## Peer coupled-motion architecture — ✅ DONE (task #50, 2026-07-14/15) [T1/T2] +RESOLVED. (This block was the old COMPLETION PLAN from `ea39af1`; it was left stale after +the work actually landed — reconciled 2026-07-15.) The coupled single-source gait pipeline +is reconstructed, DEFAULT ON, and the residual was root-caused. See [[multiplayer]] two +sections: **"Authentic coupled peer motion — DONE"** (linear position = BODY gait channel +integrated into `projectedOrigin@0x260`; angular = replicated-velocity slerp; master +SEND-mirror runs the SAME `IntegrateMotion` predictor fed the last-sent `bodyTargetSpeed`) +and **"Peer motion: the 'random shakiness' is single-box packet jitter, NOT the game"**. +- Landed: coupled path promoted to default (`c52a1ad` + `b013742`); peer poses from the + BODY channel (`96a896a`); turn-step on the body channel (`f094d78`); true-mirror cadence + driving the clip from actual replicated motion (`23f1532`). Groundwork: exact quaternion + integration (`a8eb8a4`), incremental peer heading (`d78e77b`), clock guard (`93456be`), + commanded-speed cadence feed (`6374efc`), Abs-macro fix (`7615ecd`). +- **The old "needs the MASTER HALF" conclusion was SUPERSEDED:** the leftover accel/decel + shakiness was ROOT-CAUSED as single-box **packet jitter** (`49d73dc`) — two Debug nodes on + one box batch TCP delivery, so records arrive bursty (BT_RXJIT burstiness 3-7x) and the peer + dead-reckons across the gaps then snaps. Pinning each node to a disjoint core set + (`ProcessorAffinity 0x00F/0x3C0`, baked into `tools/mp_launch.sh`) restored even ~17ms + delivery and the shakiness vanished (user-confirmed). Real pods are dedicated machines and + never see it. **DO NOT add an interpolation/jitter buffer** to mask a rig artifact. +- Minor non-gating follow-ups (tracked in [[multiplayer]]): the peer accel/decel drift + (flagged ~2.9u, but **MEASURED ~0.64u max** on the affinity rig with the peer on the body + channel -- `BT_SNAPLOG` + `BT_DRIVE_SWEEP0`, 2026-07-15; sub-unit, effectively resolved, + no fix warranted); the exact `_DAT_004ab9cc` offset-decay constant; whether + `IntegrateMotion`'s 2-stage angular integrate is intended. +- Speed model decomp-settled (`2c6db6a`): analog throttle, '5 speeds' false, `throttleState@0x4a4` + is actually the fall-surface material (RENAME PENDING). diff --git a/context/pod-hardware.md b/context/pod-hardware.md index b0125b4..59fe3fc 100644 --- a/context/pod-hardware.md +++ b/context/pod-hardware.md @@ -25,6 +25,49 @@ to keyboard/gamepad on a dev box; real wiring is a pod bring-up task (Phase 8). model — `LBE4ControlsManager` groups are fed by all devices (RIO on the pod, DirectInput on dev); the `MechControlsMapper` interprets them ([[locomotion]]). [T2] +### The pod throttle is ANALOG-CONTINUOUS, not notched (verified end-to-end) [T1] +The authentic pod throttle path — traced 2026-07 (task #50 throttle-fidelity question): +1. **Hardware → RIO:** serial `AnalogReply` packet → `Ranger("Throttle", 0, 800, .05)` — raw counts + 0–800, 5% deadband, auto-ranging offset, output a CONTINUOUS Scalar; the sign is inverted + ("Throttle counts BACKWARDS", `engine/MUNGA_L4/L4RIO.cpp:1374-1377`, Ranger @L4RIO.cpp:461-701). + No quantization/notching anywhere in `Ranger::Update`. +2. **Manager:** `LBE4ControlsManager::Execute` → `scalarGroup[ScalarThrottle].Update(&rioPointer-> + Throttle, mode_mask)` on every AnalogReply (`L4CTRL.cpp:1379-1382`). ScalarThrottle = index 0 → + manager+0x24 (scalarGroup base 0x24, 0x20/entry; buttonGroup base 0x1c0, keyboardGroup 0x160). +3. **Streamed `.CTL` mapping (the "handled elsewhere"):** `MechRIOMapper`'s ctor @004d266c binds NO + throttle — the bind comes from the type-19 `ControlMappingStream` resource named **"L4"** (child + of the per-mech type-6 `ControlMappingsList`; installed by `BTL4APP MakeViewpointEntity` via + `CreateStreamedMappings` @0047703c). BTL4.RES "L4" record [1]: + `Scalar Throttle → subsystemID 0 (ControlsMapper slot), DirectMapping, attr 4, mask 0xffffffff`. + Attr 4 = "ThrottlePosition" @ mapper+0x11c (binary IndexEntry table @0050efd8: id 4 → offset + 0x11d-1 = 0x11c, name "ThrottlePosition" @0050f28f). Reverse = record [2]: `Button Throttle1 + (0x3F, on the throttle handle) → attr 6 ReverseThrust@0x124`. Turn = pedals value-bound in the + ctor (manager+0x44/+0x64 → mapper+0x1b4/+0x1b8). +4. **Interpretation:** `L4MechControlsMapper::InterpretControls` @004d196c applies the ONLY software + detent — snap to 1.0 when |t−1.0| ≤ 0.05 — then `MechControlsMapper::InterpretControls` @004afd10 + computes `speedDemand@0x128 = maxSpeed(mech+0x34c) × throttlePosition(0x11c) × scale(mech+0x5c0)` + (forward) or `−maxSpeed × throttlePosition` (reverse flag 0x124). + +**Consequence:** the original pod produced a continuously varying speedDemand while the lever moved +(updated per serial AnalogReply, NOT per render frame). There are NO "5 throttle notches" anywhere +in the software path. (Keyboard keys '1'-'5' in @004d1bf0 set controls-manager MODE masks; the +'+'/'-' pair steps a [0,5] value that drives `pow(2,x)` into mech+0x404 = HUD zoom 1×–32× — neither +is a speed setting.) The mech's `throttleState@0x4a4` writer remains un-exported (likely in the +0x4a9b5a–0x4ab188 gap) — [[open-questions]]. + +**MECHANICAL-notch hypothesis (2026-07-16) [T4, hardware]:** the "5 speeds" pod lore may still be +true — as detents in the throttle QUADRANT hardware, invisible to the software (the pot reads +continuous counts regardless of where the lever mechanically rests). Independent corroboration +from the gait math ([[locomotion]]): the gait SM has NO stable state for a demand between the walk +cap (`walkStrideLength`@0x534) and the run engage speed (`reverseSpeedMax`@0x538, the walk→run +transition clip's exit speed) — a demand PARKED in that band hunts walk→shift-up→shift-down +forever, firing the authored EngineShiftFwd/Rev sounds each swing (binary-verified, all-authentic +data: Blackhawk band = demand 22.02–30.87 = throttle 36–50%). A design like that only ships if the +hardware discourages parking in the band. Port accommodation: the keyboard lever snaps out of the +dead band AT REST (`mech4.cpp` "GAIT DETENT"; sweeping through while held stays continuous = +authentic moving lever). Ask Nick: did the pod throttle quadrant have mechanical detents +(how many / positions)? — [[open-questions]]. + ## Multi-surface gauge path (intact, pod-only by default) The pod multi-surface path EXISTS and is intact: `DPLRenderer::FindBestAdapterIndices` (multi- adapter selector, honors PRIMGAUGE/SECGAUGE/MFDGAUGE/SPANDISABLE), `SVGA16::BuildWindows` (a diff --git a/context/project-overview.md b/context/project-overview.md index 0c87aa1..f3991eb 100644 --- a/context/project-overview.md +++ b/context/project-overview.md @@ -42,7 +42,11 @@ fire → damage → destroy → respawn** single-player loop (task #52). **2-nod end-to-end** — replication, cross-pod targeting/damage/kill, beam visuals, replicant gait (tasks #46-#51, [[multiplayer]]). All 8 cockpit canopies are authentic + the horizontal-FOV fix (task #55, [[cockpit-view]]); the Gyroscope is live byte-exact with hit-bounce (task #56); the **gauge system -is complete** ([[gauges-hud]]). The engine/renderer/HAL/audio are done (WinTesla); the active work +is complete** ([[gauges-hud]]). The engine/renderer/HAL are done (WinTesla); AUDIO backend is now REAL +(the repo's OpenAL32.dll + libsndfile-1.dll were both no-op STUBS — replaced with real OpenAL Soft + +an in-tree WAV loader, 2026-07-15) and the 241-sample soundbank is cracked from AUDIO1/2.RES + loading; +the ONLY remaining audio gap is game-triggering (no AudioEntities fire PlayNote yet), so gameplay is +still silent apart from a proof-of-life hook (see [[wintesla-port]] Audio). The active work is reconstructing each BT subsystem's authentic behavior from the binary. Remaining: pod-LAN config, Mech-level update records, per-subsystem waves. [T2] diff --git a/context/rendering.md b/context/rendering.md index 0cc598a..ecd0976 100644 --- a/context/rendering.md +++ b/context/rendering.md @@ -6,7 +6,7 @@ source_sections: "PROGRESS_LOG.md §10 (render-fidelity notes); docs/BGF_FORMAT. related_topics: [bgf-format, asset-formats, wintesla-port, locomotion, pod-hardware, cockpit-view] key_terms: [L4D3D, material-ramp, BSL, LOD, DPL, beam] open_questions: - - "Replacement-LOD for NON-additive multi-LOD models; day/night material path priority (BT_MATPRI)" + - "Replacement-LOD for NON-additive multi-LOD models" --- # Rendering diff --git a/context/steamification.md b/context/steamification.md index ed9332d..b361d3a 100644 --- a/context/steamification.md +++ b/context/steamification.md @@ -20,8 +20,31 @@ grouped roughly: toolchain → PadRIO input → MFD-split cockpit → front end/ → NetTransport/Steam sockets → lobby → bindings/KeyLight → packaging. **Repo rule [T0]:** `origin` = `VWE/BT412.git` only. This repo must never touch the -BT411 remote (no cross-pull remote — deliberate difference from RP412's `rp411` -pattern). Forked at BT411 `4e72f0c` (2026-07-14). +BT411 **remote** (the gitea URL) — no remote, no fetch/push to it. Forked at BT411 +`4e72f0c` (2026-07-14). *Bringing BT411 work forward is allowed via a **local-path** +merge (`git fetch C:/vwe/BT411 master`) — that never contacts the gitea BT411 remote; +see the merge note below.* + +## BT411 catch-up merge (2026-07-14) + +Merged BT411's post-fork line (`4e72f0c..abed41e`, 91 commits) into BT412 via a +**local-path** merge — brought the whole **audio-fidelity system** (engine `AUD*` + +`L4AUD*` + `audiopresets.cpp` + ~600 content `.wav`s, `docs/AUDIO_FIDELITY.md`), +missiles/rear-fire/HUD/gyro/gait tasks (#66–68), `FOGDAY.EGG`, and refreshed context +docs. Only **5 files** overlapped the steamification; resolutions: +- `.gitignore`, `CMakeLists.txt`, `mech4.cpp` — trivial / auto-merged (the + `deviceOwnsInput` gating survived). +- `L4NET.CPP` — took BT411's task-#50 fix (don't close the game listener on console + loss) over the seam's adaptation of the old buggy close; the point-to-point send + stays on `NetTransport_Get()->Connect()`. +- `L4NETTRANSPORT.cpp` — folded BT411's **TCP_NODELAY** latency fix into + `WinsockNetTransport::Connect` (the seam already had BT411's retry-while-refused + + non-blocking). Accept-side game-socket NODELAY stayed at the L4NET call site. +- `mechmppr.cpp` — combined the `device_owns_input` gating with BT411's task-#68 + look-behind ('V' rear-view), gating the `lookBehind` write too (a live RIO device + feeds it through its mapped button). +Verified after merge: clean build; solo front-end mode; loopback MP through the seam +(mesh completes, both tick, replication, no NODELAY warnings). [T2] ## Status ledger diff --git a/context/subsystems.md b/context/subsystems.md index 3a3097c..b1962fb 100644 --- a/context/subsystems.md +++ b/context/subsystems.md @@ -6,7 +6,7 @@ source_sections: "PROGRESS_LOG.md §10d; docs/SUBSYS_PLAN.md; docs/VEHICLE_SUBSY related_topics: [decomp-reference, reconstruction-gotchas, combat-damage, gauges-hud] key_terms: [subsystem, factory, ClassID, subsystem-roster, PoweredSubsystem, bridge] open_questions: - - "Myomers coupling (gates on owning-player @mech+0x190); 0xBD3 damage-hub untangle (WAVE 8)" + - "Myomers coupling (gates on owning-player @mech+0x190); factory capability-roster loops 2-4 still dead (SubProxy stub)" --- # Subsystems @@ -54,11 +54,11 @@ Making a base byte-exact GROWS every subclass — they must be re-based TOGETHER - **TASK #56 (2026-07-11)** — Gyroscope LIVE byte-exact (ctor @004b3778, sizeof 0x3D0 locked; integrators + `FUN_004b2980` damage fan-out; joint dispatch from the Mech performance tail @0x4aaf74/83; `BT_GYRO_LOG`/`BT_GYRO_TRACE`) — see [[cockpit-view]]. -- **DEFERRED** — SubsystemMessageManager 0xBD3 (WAVE 8 — a - **damage/explosion consolidation hub** cached to `Mech[0x10d]`=0x434; recon MISLABELS the slot - `controlsMapper` + the drive squats there while the real mapper is roster slot 0, so wiring it needs - an untangle. NOT the valve/advanced-damage gate — that's the owning **BTPlayer** @mech+0x190, see - [[combat-damage]]/[[open-questions]]). [T2] +- **✅ DONE (task #7, `afefaee`)** — SubsystemMessageManager 0xBD3 (a + **damage/explosion consolidation hub** cached to `Mech[0x10d]`=0x434; the factory builds the REAL + class and `mech.hpp` names the slot `messageManager` — the old `controlsMapper` mislabel is swept, + the real mapper is roster slot 0. NOT the valve/advanced-damage gate — that's the owning **BTPlayer** + @mech+0x190, see [[combat-damage]]/[[open-questions]]). [T2] ## The watcher electrical chain (task #57, 2026-07-13 — the torso power gate) The Watcher branch is POWERED indirectly: a watcher WATCHES another roster subsystem and mirrors diff --git a/context/translocation-warp.md b/context/translocation-warp.md index 09faead..c119b58 100644 --- a/context/translocation-warp.md +++ b/context/translocation-warp.md @@ -6,7 +6,7 @@ source_sections: "multiplayer.md (lifecycle + trigger wiring, task #52); referen related_topics: [rendering, multiplayer, combat-damage, reconstruction-gotchas, asset-formats, bgf-format] key_terms: [material-ramp] open_questions: - - "AUTHENTIC peer path (seeing OTHERS' spheres) needs player SimulationState/DropZoneLocation replication; until then the LOCAL-player sphere stands in." + - "Peer warp IS wired + visible -- observer sees a peer's un-wreck warp (world-anchored BTStartWarpEffect, 160b78e). Remaining nuance [T3]: anchor to the peer's authentic DropZoneLocation (not replicated) vs the peer's world position." - "In the 2-node force-damage test only the EXPAND (respawn) warp logs reliably; confirm the COLLAPSE (death-side) fires on every MP death path." --- @@ -118,9 +118,13 @@ it impossible to ignore. Generalized in [[reconstruction-gotchas]] §13. ## Trigger wiring (summary; full detail in [[multiplayer]]) [T2] Local player's own death → `BTStartWarpCollapsePOV()` (btplayer.cpp `VehicleDeadMessageHandler`, `deathCount==-1`, guarded `this==GetMissionPlayer()`). Respawn `DropZoneReply` → `BTStartWarpExpandPOV()` -(local-guarded). `BTWarpForceUnmask()` on mission-end / no-DropZones. Observer replicant un-wreck fires -the world-anchored `BTStartWarpEffect(x,y,z)` (mechdmg.cpp). The peer path is a stand-in until player -`SimulationState`/`DropZoneLocation` replication exists. +(local-guarded). `BTWarpForceUnmask()` on mission-end / no-DropZones. **Peer warp IS wired + visible +(`160b78e`):** an observer sees a peer's death/respawn warp — the replicant un-wreck fires the +world-anchored `BTStartWarpEffect(x,y,z)` at the peer's position (mechdmg.cpp:1074, gated to +`ReplicantInstance`; `simulationState` rides every update-record header so the observer tracks the +peer's state). The ONLY remaining nuance [T3, non-gating]: the observer's peer sphere is anchored to +the peer's WORLD position rather than the peer's authentic `DropZoneLocation` (that attribute isn't +replicated) — a fidelity refinement, not a missing effect. ## How it was solved (method) + verification [T2] 1. **Dump the real asset** (`dump_tsphere.py`) — established it's a bicone with cylindrical UVs, ending diff --git a/context/wintesla-port.md b/context/wintesla-port.md index 84378bc..57e99b2 100644 --- a/context/wintesla-port.md +++ b/context/wintesla-port.md @@ -16,7 +16,117 @@ on top of it. Full detail: `docs/PROGRESS_LOG.md §5b, §8`. ## What WinTesla already did (do NOT rebuild) - **Renderer bypass: DONE** — `MUNGA_L4/L4D3D.cpp` + `DXUtils` replace libDPL / the IG board with Direct3D9. No `libdpl.lib` in the tree. (The early from-scratch D3D9 viewer was retired and removed.) [T1] -- **Audio: DONE** — OpenAL + libsndfile replacing the HMI "SOS" engine. [T1] +- **Audio: BACKEND now REAL + soundbank loaded; only game-triggering remains** [T2, updated + 2026-07-15]. OpenAL replaces the HMI "SOS" engine and the renderer→device→buffer→source→play chain + IS all implemented (`L4AUDRND`/`L4AUDRES`/`L4AUDLVL`; `PatchLevelOfDetail::PlayNote` really calls + `alSourcePlay`). The "no sound, ever" root cause was DEEPER than a gate — **both audio backend DLLs + shipped in `engine/lib/` were no-op STUBS**: (a) `libsndfile-1.dll` exports 15 funcs BY ORDINAL + ONLY (no names), `sf_open()` always returns NULL; (b) `OpenAL32.dll` (72KB) imported **only + KERNEL32** — no dsound/wasapi/winmm — so it returned fake handles (`ctx=0x00000001`, + `alGenSources→0`) and never touched the hardware. So the chain ran clean and silent. FIXES (commit + 5b46655): **OpenAL32.dll → real OpenAL Soft 1.25.2 Win32** (drop-in: the exe imports the 25 AL funcs + by NAME; `alSourcePlay` now reaches `AL_PLAYING`). **libsndfile DROPPED** — replaced by `LoadWavPCM()` + in `L4AUDRES` (a tiny in-tree RIFF/WAVE fmt+data reader, no external dep). Gates fixed earlier: the + `AWE_FRONT`/`AWE_REAR` env-var gate in `MakeAudioRenderer` was **relaxed to default-on** (`BT_NO_AUDIO=1` + disables). **Soundbank recovered**: 241 samples cracked from `AUDIO1/2.RES` (SF2 v1.0) by + `tools/sf2extract.py` → `content/AUDIO/*.wav` + the `allPresets[2][128]` table (`audiopresets.cpp`, + replacing the zero-init btstubs stub); all 241 load (`alErr=0`). **In-game triggering now WORKS** (commit 7b1e469): enabling audio unlocked + `L4AudioRenderer::CreateEntityAudioObjects`, which binds `AudioStateWatcher`s to entity/subsystem + state attrs BY NAME (the **attribute-pointer** databinding, same as gauges) and calls + `AddAudioWatcher` on the resolved `StateIndicator`. The reconstruction had pointed the Mech's state + attrs at the scalar read-pad `attrPad` (fine for a gauge that READs a value, fatal for a state watcher + that calls a METHOD → AV in `SChainOf::Add` on 0xCDCDCDCD). FIXED: the Mech's `AnimationState`/ + `ReplicantAnimationState` (driven from `SetBodyAnimation`) and `CollisionState` (driven from + `ProcessCollision` via `collisionTemporaryState`) are now real `StateIndicator` members — **verified: + walking fires `SetupPatch`+`PlayNote`/`alSourcePlay` (footstep/gait patches 78/80/116)**. So mech + movement/animation/collision audio is live. **Subsystem STATE audio DONE** (commits 5ba541e/0e05a22/dcdd7dd): + the audio watcher TYPE per attribute was recovered via a `MakeObjectImplementation` ClassID trace + (class 28 = `AudioStateTrigger`/StateIndicator). KEY: the binary's 0x54-byte subsystem alarm + (`FUN_0041b9ec`, reconstructed as `GaugeAlarm54`) **IS a StateIndicator** — its "three sub-indicators" + @0x18/0x2c/0x40 are the audio/video/gauge watcher SChains, `level@0x14` = currentState. The + reconstruction had modeled that tail as an opaque `_tail` (never constructed → AddAudioWatcher AV'd on + 0xCDCDCDCD). FIX: `GaugeAlarm54` now has three REAL constructed sockets + `AddAudioWatcher`; `SetLevel` + fires them on change (empty sockets = no-op, so other alarms/weapons unaffected; `levelB`/`LevelCountB` + untouched; sizeof stays 0x54). Then registered each state attr → its subsystem's own alarm (own + `AttributePointers[]` chained to the parent): `Generator.GeneratorState`→stateAlarm, + `Condenser.CondenserState`→condenserAlarm, `Reservoir.ReservoirState`→reservoirAlarm, + `AmmoBin.AmmoState`→ammoAlarm (+ chained AmmoBin's bare index so `SimulationState` resolves too). + `GeneratorOn`→generatorOn. **audiostate skips 85 → 0** — every StateIndicator audio attribute across + the mech + all subsystems binds to a real indicator, and the alarms are already `SetLevel`'d by the + sims, so generator/condenser/reservoir/ammo state sounds fire on transition. **Still inert** (read 0, + silent, non-crashing — no modeled backing member in the size-locked layouts, a polish follow-up): + `ReportLeak` (Logical), `Torso.MotionState` (Enum), `Torso.SpeedOfTorsoHorizontal` (Scalar). + NOW REAL (fidelity Phase 1, commit fc7f311): `ConfigureActivePress` published at the **MechSubsystem + BASE** (binary attr id 2, descriptor @0x50de5c → member @+0x110 — the field previously misnamed + `vitalSubsystemIndex`; idle −1, driven 0/−1 by `MechWeapon::ConfigureMappablesMessageHandler`), + `AmmoBin.FireCountdownStarted` → `cookOffArmed`@0x18C, `L4MechControlsMapper.TargetRangeExponent` → + `targetRangeExponent`@0x1a4. The **bring-up guards** + [T3, self-clearing] stay: a NULL subsystem attr → inert pad (was a fatal `Fail`); an + `AudioStateWatcher` on an unconstructed indicator (chain@+0x18 null/0xCDCDCDCD) → skip. Both pass + automatically once a subsystem is built. Diagnostics: `BT_AUDIO_LOG` (chain + skips/redirects), `BT_ATTRBIND_LOG` (every + attr bind), `BT_AUDIO_TEST` (proof-of-life buffer0). +- **Audio FIDELITY pass (2026-07-16, commits fc7f311 + 691e885; the full audit ledger is + `docs/AUDIO_FIDELITY.md`, 37 findings).** Settled facts [T1 unless noted]: + - **Patch numbering = SF2 `wPreset`.** Any tooling must key presets on `wPreset`, never on phdr + record order. `AUDIO1/2.RES` are SF2 **v1.0** (16-byte shdr, NO name/rate/pitch field); pitch + metadata rides generators: 55 = SBK samplePitch (`root*100+cents`, on every zone), 58 = + overridingRootKey, 51/52 = coarse/fineTune. **EMU8000 v1 base rate = 44100 Hz** → a zone's true + data rate is baked into its WAV: `rate = round(44100·2^(((6000−rootCents)+tune)/1200))` + (`tools/sf2extract.py`); the engine's note-60 pitch model (`BTNotePitchFactor`) then reproduces + original playback at every note. SBK `initialAttenuation`(48) is **INVERTED** (127 = full + volume; 0.375 dB/step [T3 exact curve], baked into the PCM). + - **Full-zone table**: 603 zones / 241 presets in `audiopresets.cpp`; the authored MIDI note + SELECTS zones by keyRange in `SetupPatch/PlayNote` (key-splits: Loaded/Jam family low-36 + clunk vs high-84 blip; Warnings01 8-way klaxon split), layers attach together, + `MAX_PRESET_SAMPLES=25` (AllExplosion). Authored loop REGIONS apply via `AL_SOFT_loop_points`; + authored `releaseVolEnv` (1.1–3.9 s on ~20 looping presets) fades on Stop + (`PRESET_serviceReleaseFades`, serviced from `AudioHead::Execute`). + - **Distance/volume/doppler**: OpenAL distance model is `AL_NONE` — the AUTHORED + `GetDistanceVolumeScale()` curve multiplies in `CalculateSourceVolumeScale` (Dynamic3D + + Static3D); per-source `AL_GAIN = volume_scale²` (GM CC7 squared law); `alDopplerFactor(0)` — + the authored `GetDopplerCents()` (AUDIO.INI model, consumer decomp part_008.c:7466) adds into + the Dynamic3D pitch chain. + - **Audio clock**: `FUN_0044e19c` is `ApplicationManager::GetFrameRate` — the original audio + "frames" were SYSTEM CLOCK TICKS; `MakeAudioRenderer` calibrates at + `SystemClock::GetTicksPerSecond()` (fallback 1000), never the old garbage + `divisionParameters+0x10` read. + - **Published `localVelocity` smoothing is authentic** [T1]: `Mech::Execute` (FUN_004ab430) runs + the localVelocity components through `AverageOf` running-average filters + (part_012.c:15169-15179); the port's ~0.25 s exponential filter in `mech4.cpp` reconstructs it. + - **Phases 3+4 SHIPPED (2026-07-16, commits 0ca7d26..0e2401f).** Phase 3 EFX: `L4AUDEFX` + bridge — per-source lowpass (Dyn3D = highFreqCutoffScale×brightness UNGATED, Static/Direct = + brightness; AWE 100–8000 Hz → 5 kHz-ref gainhf [T3 curve]), one EAXReverb slot at + global_reverb_scale 0.3 (3D wet / cockpit Direct dry), Direct position-enum placement. + Phase 4 attributes (all [T1] from binary attribute tables, walked as 16-byte + {id,name,off+1,pad} rows): **HeatSink table @0x50e438 (ids 3..12)** confirms every heat + binding + adds ReportLeak id 12 @0x138 = coolantActive (serves all 19 leak watchers via + PoweredSubsystem⊂HeatSink); **Mech table @0x50c0xx (ids 21..56)**: IncomingLock@0x3fc / + DistanceToMissile@0x400 (real members, Missile::MoveAndCollide reports, PerformAndWatch + latches; old `maxSpeed@0x400` was a misread of the FLT_MAX far default), + CollisionSpeed@0x4B4 (=|impact velocity| at contact-arm), ReduceButton@0x340, + FootStep@0x394, RearFiring@0x410 (old `stateFlags` misread); **Torso table (ids 3..15)**: + full publish onto existing members (SpeedOfTorsoHorizontal=twistVelocity@0x1E8 abs'd, + MotionState=statusFlags@0x20C, ==2 = limit-hit frame). **F5 footStep = CONTACT LEVEL**: + (jointlocal.y <= *ANI hdr[2] threshold) per frame, states 0/1 retain (SelectSequence now + captures hdr[2]; the 150 ms pulse is retired). **F19**: the authored footstep volume = + LocalAcceleration |lin| [0,10]→ctl100 + LocalVelocity |lin| [0,0.6]→ctl101 (ALL motion + watchers extract |linearMotion|, motionValue=3); localAcceleration.linear = d(published + velocity)/dt (binary part_012.c:15186-15195); the mech2.cpp step broadcast is REMOVED. + **attrnull count = 0** — every authored audio attribute binds a real member. + - **Post-Phase-4 closures (2026-07-16):** UnstablePercentage is LIVE (the instability model + @0x3F0 byte-decoded + reconstructed — accel-sway² + gun-the-engine + turn-in-place terms, + feeds gyro swayBias + the audio alarm; commit c51d64a). The F14 per-zone static resonant + low-pass is BAKED (RBJ biquad at the AWE 100–8000 Hz curve, float pipeline + peak + normalization; 232 zones incl the LaserLoaded charge hum fc=57 Q=87; c581553). The + **footstep 10–20 s warm-up bug** is fixed (a11a697): an idle source's watcher chain executes + only at Start attempts and each hop is audio-frame-gated, so the authored fill-0 N=30 + smoothers starved behind the 0.3 transient drop gate — fixes: AudioScaleOf sends EVERY poll + (the bitwise change-gate froze on our deterministic gait floats), recursive gate-free + Component::PrimeWatchers(30) on every transient Start request (AUDREND), and + localAcceleration via the binary's exact 15-sample ring buffers. LESSON: trace caps + saturate early and fake "per-stride" rates — always timestamp before inferring cadence. + - Still deferred: F21 attack velocity/time (all captured events were default 1), F22 HRTF + (user has 2 speakers; enable via alsoft.ini `hrtf=true` for headphones). - **L4 HAL on Windows + the VS build system: DONE.** [T1] - **Red Planet game logic: COMPLETE & buildable** — `VTV/VTVMPPR/WEAPSYS/...`. RP is at/near playable. [T1] - **Multiplayer: the DOS `NETNUB` driver is already replaced by a ~3.5k-line WinSock2 TCP diff --git a/docs/AUDIO_FIDELITY.md b/docs/AUDIO_FIDELITY.md new file mode 100644 index 0000000..efa6a73 --- /dev/null +++ b/docs/AUDIO_FIDELITY.md @@ -0,0 +1,621 @@ +# Audio Fidelity Audit — btl4.exe OpenAL Port vs. Original AWE32/HMI-SOS + +> **STATUS (2026-07-16): Phases 1-4 SHIPPED** (commits fc7f311, 691e885, 0ca7d26, +> cc2b109..0e2401f). Fixed: F1-F5, F7, F9-F13, F15, F16, F18-F20, F23, KB items; +> F6 fixed (ReportLeak -> coolantActive@0x138, binary table [T1]); F14 FIXED +> (attenuation + the static resonant fc/Q low-pass both baked, c581553); +> F17 FIXED (CollisionSpeed + ReduceButton real; UnstablePercentage live -- +> the instability model @0x3F0 is byte-decoded and reconstructed, task #66); +> F8/F21/F22 open (voice budget unhit / attack polish / HRTF option). +> Post-audit: the footstep cold-start warm-up bug (idle watcher chains + +> fill-0 smoothers vs the transient drop gate) fixed in a11a697. +> attrnull = 0: every authored audio attribute binds a real member. + + +**Date:** 2026-07-15 +**Scope:** Full fidelity audit of the OpenAL audio reconstruction against the original 1995 +semantics: the authored audio object streams (`content/audit_capture.log` traces), the WinTesla +engine source (`engine/MUNGA`, `engine/MUNGA_L4`), the Ghidra decomp of `BTL4OPT.EXE` +(`reference/decomp/all/part_*.c`), and the SF2 v1.0 soundbanks (`content/AUDIO/AUDIO1.RES`, +`AUDIO2.RES`). +**Method:** Parallel findings independently generated, then adversarially verified (every count +re-derived from the banks with fresh parsers, every file:line re-read, decomp cross-checked, live +capture re-parsed). Verdicts: CONFIRMED = independently reproduced; PARTIAL = core claim holds +with material corrections (the corrected form is what appears below); UNVERIFIED-LOW = low-impact, +not independently re-verified. +**Excluded (already known-fixed):** note-pitch application (`BTNotePitchFactor`), calibration rate +1000, ExecuteWatchers poll restoration, configure-ticker -1 pad, published-velocity smoothing. + +--- + +## 1. Executive Summary + +Honestly: the **control architecture is in good shape, the rendering back-end is not.** The +watcher/sequence/mixer graph is substantially reconstructed and live — all 118 authored state +watchers bind real StateIndicators, weapons/footfalls/generators demonstrably fire, and the +patch-numbering and calibration questions are settled in the port's favor. But roughly a quarter +of attribute-gated watchers sit on dead attributes (the coolant-leak warning, the accelerating +missile-lock beeper, the torso-twist servo, the instability alarm can **never** sound), and on the +synthesis side nearly every axis diverges from what the AWE32/EMU8000 actually did: the SF2 +extractor keeps one zone per preset (discarding key-splits, layers, and stereo pairs the engine +already has plumbing for), drops all tuning metadata (~83% of presets play >= 1 semitone off, +worst -36 st), the volume law is linear where the original was the squared GM CC7 curve, the +authored distance/doppler/reverb/filter/pan models are all computed and then discarded in favor of +wrong or absent OpenAL defaults. + +**Rough fidelity per dimension:** + +| Dimension | Est. fidelity | Basis | +|---|---|---| +| Triggers / databinding | **~75%** | 54 of ~225 attribute-gated watchers dead (24%); all 118 state watchers live; footstep timing semantics wrong; one port invention actively fights an authored feed | +| Volume / attenuation | **~40%** | Per-source gain curve wrong everywhere (linear vs squared), distance curve wrong shape for all 3D sounds, per-zone layer gains dropped, cull/steal/ducking side-effects | +| Looping | **~85%** | Authored loop regions ~= whole sample for everything looped today (worst artifact ~5% period error / wrap click); missing 1.1-3.9 s release fades on ~20 looped presets; one **latent** catastrophic loop once multi-zone lands | +| Pitch / timbre | **~25%** | ~83% of presets >= 1 semitone off (root-key/tuning dropped); zero filtering (brightness, distance muffling); doppler wrong sign and depth | + +--- + +## 2. Ranked Findings (audible impact x confidence) + +| # | Finding | Impact | Verdict | One-line effect | +|---|---|---|---|---| +| 1 | [Multi-zone preset collapse](#f1) | High | CONFIRMED | Key-splits/layers/stereo dropped: wrong klaxons, looping charge-hum instead of clunk-then-blip, explosions lose body and rumble | +| 2 | [Root-key & tuning dropped](#f2) | High | CONFIRMED | 91/115 + ~102/126 presets play >= 1 semitone off (worst -36 st; klaxons ~6x slow, footsteps 27% fast) | +| 3 | [Distance attenuation wrong](#f3) | High | CONFIRMED | Authored knee/rolloff curve dead; AL linear ramp makes mid/far battle audio far too quiet; cull/steal/ducking distance-blind | +| 4 | [Volume curve linear vs CC7 squared](#f4) | High | CONFIRMED | Every intermediate volume ~+6 dB too loud at mid-scale; authored dynamic range compressed | +| 5 | [FootStep trigger semantics](#f5) | High | CONFIRMED | Binary: per-frame root-height contact level vs threshold; port: fixed 150 ms pulse per clip transition — wrong step timing/counts | +| 6 | [ReportLeak dead — coolant warning](#f6) | High | CONFIRMED | Largest dead block (38/54 match watchers, 19 subsystems); leak klaxon can never sound | +| 7 | [Missile-lock alarm dead](#f7) | High | CONFIRMED | IncomingLock + DistanceToMissile on read-only pad; accelerating lock beeper can never start | +| 8 | [Dead-watcher coverage matrix](#f8) | High | PARTIAL | 54 of ~225 attribute-gated watchers on dead attributes (~24%); itemized worklist | +| 9 | [Brightness / low-pass chain dead](#f9) | Medium | CONFIRMED | AWE NRPN-21 filter path computed then discarded; no distance muffling, ctl-5 brightness inert | +| 10 | [Doppler triply wrong](#f10) | Medium | CONFIRMED | Authored cents model dead; AL doppler at wrong constants with sign-inverted velocity — approaching sources pitch DOWN | +| 11 | [Reverb dead](#f11) | Medium | CONFIRMED | Original: CC91=38 wet exterior / 0 dry cockpit (decomp-proven); port: bone-dry everywhere | +| 12 | [Front/rear + pan placement dead](#f12) | Medium | CONFIRMED | DirectPatch position enum and internal spatialization discarded; all cockpit/static sounds dead-center | +| 13 | [Loop regions & release envelopes](#f13) | Medium | CONFIRMED/PARTIAL | Today: minor wrap artifacts (worst ~2.4 Hz tick in beam hum) + instant cuts where 1.1-3.9 s releases were authored; latent boom-loop bug once multi-zone lands | +| 14 | [Zone gain/filter/envelope generators dropped](#f14) | Medium | PARTIAL | Per-zone attenuation (inverted SBK scale), authored resonant low-pass on charge hum, volume envelopes all lost | +| 15 | [ConfigureActivePress mispublished](#f15) | Medium | CONFIRMED | Weapon-configure ticker can never fire; field exists but is misnamed and unpublished | +| 16 | [Torso-twist servo whir dead](#f16) | Medium | CONFIRMED | Torso publishes no attributes; whir never starts, pitch tracking dead | +| 17 | [Entity attrPad family](#f17) | Medium | CONFIRMED | Instability alarm never sounds; impact sounds never scale with speed; ReduceButton feedback dead | +| 18 | [FireCountdownStarted = cook-off warning](#f18) | Medium | PARTIAL | Dead binding confirmed; it is the ammo COOK-OFF countdown (member exists: `cookOffArmed`), not a salvo cue | +| 19 | [Step-intensity invention fights authored feed](#f19) | Medium | PARTIAL | Unauthored curve + direct mixer writes clobbered by the live authored LocalVelocity feed every step (drops footsteps) | +| 20 | [Zoom blip unpublished](#f20) | Low | UNVERIFIED-LOW | Live member exists, one ATTRIBUTE_ENTRY missing | +| 21 | [Attack velocity / attack time dead](#f21) | Low | UNVERIFIED-LOW | Mechanism dead but authored data barely uses it today | +| 22 | [ITD / between-ears spatialization dead](#f22) | Low | UNVERIFIED-LOW | 4-channel pod model computed and discarded; matters mostly for pod-hardware target | +| 23 | [Benign inventions check](#f23) | Low | UNVERIFIED-LOW | Footstep pulse OK; AnimationState numbering unverified (17 authored triggers never fired); CollisionState 0/1 vs 4-state | + +**No-gap confirmations (validated, no change needed):** patch numbers are SoundFont wPreset +values (the port's mapping is correct — never "fix" toward file index); the calibration rate 1000 +is structurally equivalent to the original clock-tick rate (but the `DPLARG+0x10` read is baseless +and the KB claim should be corrected); the port's published-velocity smoothing matches the +binary's `AverageOf` filter chain in kind. See [Section 4](#validations). + +--- + +## 3. Findings in Detail + +### A. Sample bank / synthesis fidelity + + +#### F1. Multi-zone preset collapse — key-splits, layers, stereo pairs all dropped (High, CONFIRMED) + +The SF2 extractor (`tools/sf2extract.py:49-62`) keeps only the FIRST sample-bearing instrument +zone per preset, so the generated table (`game/reconstructed/audiopresets.cpp`) has +`sampleNum=1`/`samples[0]` everywhere — discarding authored multi-zone content that the engine +already supports (`PRESETINFO.samples[5]`, `GetVoiceCount=PRESET_getNumSamples`, SetupPatch +buffer-per-source; `engine/MUNGA_L4/L4AUDLVL.h:33-34,68-70`, `L4AUDLVL.cpp:30,130-167`). + +Verified against the banks: **68/115 bank1 and 94/126 bank2 presets are multi-zone.** + +- **Key-splits (7+7 presets)** — the authored MIDI note SELECTS the sample. `Warnings01` (bank2 + p113) is an 8-way split (keyRanges 16-17/19-21/23-24/26-28/29-31/33-35/36-38/40-41, 8 distinct + klaxon samples, three looped); the weapon Loaded/Jam/Misfire family (bank1 p36,40,41,56,70,71,75) + splits at key 59|60: low clunk/charge zone (root 36) vs high ready-blip zone (root 84). The + captured streams author exactly these selector notes: five sequences send [36,36,84,84] + (`audit_capture.log:501,566,681-683`, plus one [36,36,70,70]); the warnings sequence (log:343) + sends 16,16,19,19,23... matching `Warnings01`'s split points exactly. +- **Layers (~55+84 presets)** — 2-4 simultaneous samples (LaserAFire01=3, MissileLaunch01=4, + AutoCanonFire01=3, MechExplosion01=16 zones in 4 key bands). Roughly two dozen bank1 / + three-dozen-plus bank2 presets mix looped sustain layers under one-shot attacks — every + LaserExplosion/BigExplosion/PPCExplosion has a looped tail-rumble layer the port drops. +- **Stereo pairs (6+3)** — pan 0/127 zones, some with different L/R samples (EngineAccel02 + sid2/sid38), plus 5 bank1 L/C/R 3-zone presets. + +**Net effect verified live:** on weapon-ready the port loops the charge hum at the wrong pitch +instead of clunk-then-blip (log:832-840: note-36 START then `SetupPatch ENTRY bank=1 patch=56 +file=LaserLoaded01.wav`, table loop=LoopAtWill); all 8 cockpit warning klaxons play the same +sample; explosions/lasers lose their layered body and sustained rumble. + +**Fix:** regenerate the table with ALL zones — per-zone WAV, keyLo/keyHi, loop mode, pan — into +`samples[0..n]` (cap 4 per the AWE comment for layers sharing a keyRange). In SetupPatch/PlayNote, +attach only zones whose keyRange contains `GetCurrentNoteValue()`, attach all matching layers, set +position/pan for 0/127 pairs. Table + SetupPatch work only; voice-count plumbing already exists. + + +#### F2. Root-key and tuning metadata dropped — most of the bank plays at the wrong pitch (High, CONFIRMED) + +The port plays every sample as if its root were MIDI 60 at 22050 Hz (`tools/sf2extract.py:24` +hardcodes SAMPLE_RATE=22050; the generator scan at lines 50-62 reads only opers 41/53/54; +`L4AUDIO.cpp:21-24` BTNotePitchFactor=2^((note-60)/12)). But the SF v1.0 banks carry full SBK +pitch metadata the extractor drops: + +- **Generator 55** (SBK-only samplePitch, per awesfx `sfitem.c`/`parsesf.c`: root=amount/100 plus + fractional cents) — present on **all 603 sample zones**, constant per sample. +- **Generator 58** (overridingRootKey) on 183+247 zones; **coarseTune(51)** on 138+258 zones + (modal value -12); **fineTune(52)** on 30+9. +- EMU8000 v1 base rate is 44100 Hz (awesfx `sffile.c`: v1 samplerate hardcoded 44100), so a zone's + true data rate is 44100*2^(tune/1200). Recovered rates land exactly on 22050/44100/11025/5512 Hz + plus integer-semitone offsets — corroborating the model. There is **no rate generator**: 22050 + is only the modal effective rate, not a bank-wide truth. + +Per-zone port error is **constant across notes**: err_cents = (effRoot-72)*100 - tune; zero only +for the common root-60/coarse -12 configurations (why the note-pitch fix sounded right on those). +**Prevalence: 91/115 bank1 and 102/126 bank2 presets >= 1 semitone off.** Worst verified cases: +Death01/MechExplosion01 -36 st; Warnings01 -31 st (klaxons ~5.9x too slow at authored note 16); +ProjectileLoaded01 -30 st; TorsoTwistExt01/CoolantDumpExt01/LaserCSustain01-03 worst-layer +24 st; +LaserACharge01-04 +17 st; MissileLoaded01 -24 st; FootFallInt01 +4.2 st (footsteps ~27% fast). +Authored sequence notes equal zone roots exactly (36/84 splits; Warnings roots 16-40) — final +cross-check. + +**Fix (algebraically exact, no engine change):** bake per-zone tuning into each extracted WAV's +declared rate = round(44100*2^(((60-effRoot)*100+tune)/1200)); OpenAL resamples from the declared +rate, so the existing note-60 pitch model reproduces original playback at every note. Note the +extreme recovered rates (~162 Hz to 176.4 kHz on a few Death01/torso zones) and the dependency on +per-zone WAVs (F1) for key-split/layered presets. + + +#### F13. Loop regions and release envelopes (Medium, CONFIRMED with an important correction) + +Two verified sub-findings, one of which corrects the naive reading: + +1. **Whole-buffer looping is close to faithful today.** SetupPatch sets AL_LOOPING on the entire + buffer (`L4AUDLVL.cpp:158-164`) while authored loops are [dwStartLoop,dwEndLoop] sub-regions + that `sf2extract.py` reads but discards (line 64 vs 70). However, every looping sample in the + banks is sampleModes=1 (loop continuously; none are mode-3) with loop regions covering + **95-100%** of the sample — the whole sample effectively IS the loop. No attack transient + replays per cycle. Worst real artifacts among currently-looped zone0s: LaserBSustain family at + 95.0% (348-sample lead-in + 106 tail per ~0.41 s => ~2.4 Hz discontinuity in the beam hum), + LaserCSustainExt01-04 (283+41/11657), EngineMotor02/EngineAccel02/06 (324+271/35747), + MechFireLoop01/SmallFireLoop01 (406+94/47586) — plus a wrap-point click (port wraps end->start + instead of loopEnd->loopStart). +2. **The genuine gap is Stop.** `StopNote` (`L4AUDLVL.cpp:216-217`) is + alSourceStopv+alSourceRewindv — an instant cut — but **~20 looping presets carry an authored + releaseVolEnv of 1.1-3.9 s** (EnginePower/Motor/Accel/Damage ~1.55 s, EngineAccel01 3.9 s, + TorsoTwist 1.2-1.4 s, LaserCharge 1.3 s, MissileTravel/Loading ~1.3 s, Coolant/Wind/Fire loops + ~1.1 s) that the AWE32 applied as a fade on note-off while the loop continued. One-shots have + no authored release; instant stop is faithful there. +3. **Latent hazard:** the only catastrophic (<60%) loop regions sit in currently-dropped layers: + MechExplosion01/AuxExplosion01 share sample sid39 with loop [23517,23865] of 23875 (**1.5%** — + a ~16 ms sustain slice at the end of the boom). Today these presets are one-shot zone0 + (ForceStatic, `audiopresets.cpp:805-808,949-952`); once multi-zone extraction (F1) lands, + whole-buffer looping would replay the full ~1.08 s boom every cycle. **Loop-region support must + ship with or before F1.** + +**Fix:** extract loopStart/loopEnd per zone into the table; use AL_SOFT_loop_points (a **buffer** +property: `alBufferiv(AL_LOOP_POINTS_SOFT,...)` before attach) or split attack+loop buffers via +queueing. On Stop, ramp AL_GAIN over the per-preset authored release time before stopping. + + +#### F14. Per-zone gain / filter / envelope generators dropped (Medium, PARTIAL) + +`tools/sf2extract.py` (reads only gens 41/53/54) and the playback path drop everything the EMU8000 +applied in hardware: + +- **initialAttenuation** on 198 bank1 / 309 bank2 zones. SBK units are **INVERTED vs SF2** — 127 = + full volume, lower = quieter (observed range 90-127); used for layer balance (LaserBFire01's + panned side layers at 100 vs center at 120 — note: the sides are the *quieter* layers). +- **initialFilterFc** on 116 zones/bank, **initialFilterQ** on 56/23. Example: LaserLoaded01's + looped charge-hum zone — the very zone the extractor picks — is fc=57 Q=87, an authored resonant + low-pass the port plays unfiltered and therefore much brighter/harsher. No filter is ever + applied (`L4AUDIO.cpp:1067-1086` computes a cutoff, sets nothing). +- **Volume envelopes** (attackVolEnv 167/211 zones, releaseVolEnv 34/6, decayVolEnv 41/127) — see + F13 for the release consequence. SAMPLEINFO (`L4AUDLVL.h:22-29`) has no fields for any of this. + +**Fix:** add per-zone gain scale (mind the inverted scale: gain ~ f(127-atten); verify the exact +dB mapping against awesfx before trusting "0..-9.6 dB" [T3]), bake or EFX-apply the static +low-pass, and map release to a Stop-path gain ramp. + + +#### F21. Note velocity / attack time dead (Low, UNVERIFIED-LOW) + +The original started every note with MIDI velocity = attackVolumeScale*127 and (when flagged) sent +AWE NRPN 11 attack time (decomp @004638a8, part_008.c:7144-7154; @00463ddc :7367-7392). The port +computes `midi_velocity`/`midi_attack_time` and discards both (`L4AUDIO.cpp:980-991`; PlayNote +takes no velocity); ctl 6/7 handlers store scales nothing reads. Mitigation: all 41 captured +AttackVolume events are value 1 (= default) and zero ctl=7 events, so today's audible loss is +small. Fix when convenient: multiply start-time AL_GAIN by attackVolumeScale (through the F4 +squared curve) and implement attack time as a gain ramp (range 0..5.94 s per +`L4AUDHDW.h:63,67`). + +### B. Mixing and spatial model + + +#### F3. Distance attenuation: authored curve dead, wrong AL model live (High, CONFIRMED) + +AUDIO.INI authors amplitude_rolloff=2.0 (exponent), knee=60, distance_scale=0.003, +clipping_radius=450 (AUDIOMR: 0.002): flat volume inside 60 units, then 1/(1+(0.003*(d-60))^2) — +still ~42% at the clip edge. The engine computes this into `distanceVolumeScale` on every source +execute (`AUDLOC.cpp:358-377` via `AUDSRC.cpp:822`), but its only consumer — +`volume_scale *= GetAudioLocation()->GetDistanceVolumeScale()` in +`Dynamic3DPatchSource::CalculateSourceVolumeScale` — is **commented out** behind an early return +(`L4AUDIO.cpp:1470-1496`, comment at :1493-1495). Instead: hardcoded +`alDistanceModel(AL_LINEAR_DISTANCE)` (`AUDIO.cpp:98`) with never-set +AL_REFERENCE_DISTANCE/AL_ROLLOFF_FACTOR (defaults 1.0) and AL_MAX_DISTANCE=clipRadius*clipScale +(`L4AUDIO.cpp:1109,1461,1997`) — a straight-line fade to zero. + +Numbers (clippingScale=1): d=60: AL 0.87 vs authored 1.0; d=100: 0.78 vs 0.99; d=300: 0.33 vs +0.66 (-6 dB); d=440: 0.02 vs 0.44. **Distant battle sounds are drastically too quiet.** +Confirmed side-effects of the distance-free engine volume: (a) the LowAudioVolumeThreshold=0.3 +transient-start cull (`AUDREND.cpp:197,223-237`) is distance-blind; (b) AudioWeighting voice-steal +(`AUDSRC.h:526-536`, `L4AUDRND.cpp:1175`) no longer favors near sources; (c) the CalculateMix +ducking chain (`L4AUDRND.cpp:684-811`, compression 0.92/8.5) treats far sources as full-presence. + +**Fix (near one-liner, highest leverage in the audit):** `alDistanceModel(AL_NONE)`, restore the +commented distance multiply (the spatial model is already updated each Execute), apply +`distanceVolumeScale` in Static3D's gain write too, drop the AL_MAX_DISTANCE attenuation writes. +Restores curve + cull + steal + ducking simultaneously. + + +#### F4. Volume curve: linear AL_GAIN vs the CC7 squared law (High, CONFIRMED) + +The original's volume path ended in MIDI CC7 = ftol(volume_scale*127+0.5) on the AWE32 +(decomp @00463bbc part_008.c:7205-7218; @004641e0 :7538-7583 — the 127.0/0.5 constants +byte-verified in the EXE; the engine's own retained MIDI source at `L4AUDIO.cpp:2146-2239` +confirms). The GM/SoundFont-standard CC7 curve is concave: amplitude ~ (v/127)^2, i.e. +40*log10(v/127) dB (standard-based inference for the binary Creative driver — flagged, not +decompilable). The port writes the identical volume_scale LINEARLY to AL_GAIN at **three** sites: +`L4AUDIO.cpp:1110` (DirectPatch), `:1459` (Dynamic3D), `:1995` (Static3D). + +Result: every intermediate volume plays ~+6 dB louder than the arcade at mid-scale (0.5: -6 dB vs +-12 dB; 0.3: -10.5 dB vs -21 dB) — the authored dynamic range is compressed and quiet +ambience/servo layers sit far too high against weapons/engine. + +**Fix:** square volume_scale (or apply the exact concave transform, optionally quantized to 127 CC +steps) at all three AL_GAIN sites. Do NOT square the master listener gain (a common factor; the +port's 0.6 is calibration, not authored). + + +#### F9. Brightness / low-pass filter chain dead — no distance muffling, ctl-5 inert (Medium, CONFIRMED) + +The original continuously drove the AWE32 initial-filter-cutoff NRPN 21 (100-8000 Hz per +`L4AUDHDW.h:65-68`, scaled by the patch's authored MaxMIDIFilterCutoff) from ExecuteModel — +decomp-verified per source type: DirectPatch = brightnessScale (gated on useBrightness; +@00463bbc, part_008.c:7191-7203); **Dynamic3D = highFreqCutoffScale x brightnessScale, ungated, +all four quadrant channels** (part_008.c:7496,7589-7604) — every moving 3D sound got duller with +distance; Static3D = brightnessScale only (part_008.c:7831-7884). + +The port computes the values and applies nothing: `L4AUDIO.cpp:1067-1086` only updates the +`lastMIDIFilterCutoff` bookkeeping member; Dynamic3D's ExecuteModel has no filter path; Static3D's +NRPN block is inside the comment spanning :2020-2245; **no alFilter/EFX call exists anywhere** +(only stock SDK headers). The authored distance HF rolloff (AUDIO.INI:23-25: exponent 2.0, knee +60, scale 0.0035; AUDIOMR 0.003) is still computed per frame into `highFreqCutoffScale` +(`AUDLOC.cpp:386-405`) — `GetHighFreqCutoffScale()` has zero callers. Brightness ctl 5 (2 authored +bindings, log:199,285), use_brightness_scale, per-patch maxMIDIFilterCutoff, and the reconstructed +footstep Brightness-mixer feed (`mech2.cpp:318`) all drive a dead sink. Everything plays +spectrally full-bright at all distances. + +**Fix:** one EFX AL_FILTER_LOWPASS per source; AL_DIRECT_FILTER with GAINHF from +highFreqCutoffScale x brightnessScale (Dynamic3D), brightnessScale alone (Static3D/DirectPatch, +respecting useBrightness), mapped through the AWE curve (~100 + cutoff*7900/127 Hz). + + +#### F10. Doppler: authored model dead, AL doppler wrong constants AND wrong sign (Medium, CONFIRMED) + +Triply wrong, decomp-verified intent: + +1. The authored model (AUDIO.INI:30-31 doppler_range=600 cents, speed_of_sound=250 u/s) is + computed every spatial update into `dopplerCents` = 600*(1-250/(250-v)) with near-ear fade and + +/-c clamp (`AUDLOC.cpp:451-508`) — and **the decomp proves the original consumed it**: the + dynamic patch source ExecuteModel adds audioLocation+0x58 (dopplerCents) to + CalculateSourcePitchOffset before the MIDI pitch-bend clamp (part_008.c:7466). In the port, + `GetDopplerCents()` has zero callers. +2. Instead AL doppler is enabled with hardcoded `alDopplerFactor(0.3f)` (`AUDIO.cpp:99`) and + `alSpeedOfSound` never called (default 343.3) — neither constant nor curve shape matches. +3. The velocity feeds are sign-inverted relative to the position frame: AL_POSITION = source + relative to head, but AL_VELOCITY = **negated** source world velocity with head velocity never + subtracted (`L4AUDIO.cpp:1429-1433,1458,1460`); listener velocity is also negated + (`AUDIO.cpp:155-158`) but cancels under AL_SOURCE_RELATIVE. + +Net: an approaching missile/mech pitches **DOWN** (~-88 cents at 60 u/s, ~-145 at 100 u/s) where +the original gave +116 cents at 60 u/s approaching, and the player's own motion produces no +doppler at all. + +**Fix (faithful, per the decomp):** disable AL doppler and add GetDopplerCents() into the +Dynamic3D pitch-cents chain (2^((cents_chain + dopplerCents)/1200) x note factor). The original +applied doppler only on the dynamic multi-channel path — keep static/direct sources doppler-free. + + +#### F11. Reverb: authored wet-exterior / dry-cockpit split dead (Medium, CONFIRMED) + +Decomp-proven original behavior: the 3D patch-source StartImplementation (FUN_00463ddc, +part_008.c:7278-7394) reads global_reverb_scale=0.3 (AUDIO.INI:47, stored at head+0x4C) and sends +MIDI CC91 = 38 on all four AWE32 channels; DirectPatchSource (FUN_004638a8, part_008.c:7104-7140) +sends CC91 = 0 — deliberate wet-exterior vs dry-cockpit contrast. In the port all send sites are +commented out (`L4AUDIO.cpp:949, 1253-1263, 1759-1771`), `GetGlobalReverbScale` has no live +callers, and no EFX effect/aux slot is ever created. Everything plays bone-dry; the arena-space +cue separating outside sounds from cockpit UI is gone. (Note: the code was already commented in +the repo's initial commit — an inherited gap from the WinTesla conversion, still a fidelity gap.) + +**Fix:** one EFX EAXReverb aux slot at init (gain from global_reverb_scale, AWE32-like preset); +AL_AUXILIARY_SEND_FILTER on Dynamic3D/Static3D sources only; Direct sources dry. + + +#### F12. Front/rear + pan placement: all cockpit and static sounds dead-center (Medium, CONFIRMED) + +The original routed every DirectPatchSource by its authored 6-value position enum +(Front/Rear/FL/FR/RL/RR, `L4AUDIO.h:355-362`): positions 0/2/3 to the FRONT sound card, 1/4/5 to +the REAR (decomp @00463848, part_008.c:7051-7062; the pod required two cards, AWE_FRONT/AWE_REAR), +with MIDI pan CC10 = 0x3F center / 0x00 left / 0x7F right (@004638a8, part_008.c:7119-7140). The +port reads audioPosition from the stream (`L4AUDIO.cpp:795`) but every use is commented out +(:854-868, :902-916, :952-966); SetupPatch pins all sources AL_SOURCE_RELATIVE at (0,0,0) +(`L4AUDLVL.cpp:150-156`) and DirectPatch ExecuteModel never writes AL_POSITION. Similarly, +Static3DPatchSource with useInternalSpatialization (the BuildFromPage default) computes its +quadrant spatialization at start (:1613-1641) but consumes it only in commented code; the live +position member is written solely by the collision trigger (`L4AUDWTR.cpp:149`) — all other +internal statics also play centered. (Caveat: which authored content is off-center is inferred +from the enum + original routing code; the mechanism centers ALL of it regardless.) + +**Fix:** since sources are already AL_SOURCE_RELATIVE, set AL_POSITION per enum in +DirectPatchSource::StartImplementation (front=-Z, rear=+Z, left=-X, right=+X; MUNGA azimuth 0 +maps to +Z=rear per the CalculateSpatialization +180 shift). For internal-spatialization statics, +derive AL_POSITION from the already-computed azimuth. Keep distance ~1 so the distance model does +not attenuate them. + + +#### F22. ITD / between-ears model dead (Low, UNVERIFIED-LOW) + +itd_difference=0.0015 and distance_between_ears=2.0 feed the L4AudioSpatialization 4-channel model +(per-quadrant delays, transient ITD pitch offsets, the between-ears front/rear power crossfade) — +all computed, all discarded (Get*ITDPitchOffset: zero callers; the quadrant-gain consumers are the +commented MIDI sends). Lost: interaural time cues and the smooth blend when a source passes +through the head (it now snaps across the stereo field). Faithful minimum: blend AL_POSITION +toward the origin inside distance_between_ears/2; OpenAL Soft HRTF supersedes the ITD constant for +desktop; full quadrant gains only matter when driving the pod's physical 4-speaker pairs. + +### C. Triggers and databinding + + +#### F8. Coverage matrix: ~24% of attribute-gated watchers are dead (High, PARTIAL — corrected counts) + +From the 35 s capture (118 statecfg + 54 matchcfg + 23 trigcfg + 29 scalecfg = 224 attribute-gated +configs; attrbind=227, attrnull=53): **53 (~24%) sit on dead attributes**, plus a 54th outside the +denominator (ControlsMapper.TargetRangeExponent, class-81 delta trigger, log:330-331). Breakdown: +45/54 match watchers (38 ReportLeak on 19 subsystems — F6; 4 FireCountdownStarted — F18; 1 +Torso.MotionState — F16; 2 Entity.IncomingLock — F7, whose match=0 Stop fires once spuriously), +5/29 scale watchers (2 CollisionSpeed, 1 DistanceToMissile, 1 UnstablePercentage, 1 +Torso.SpeedOfTorsoHorizontal), 3/23 trig watchers (UnstablePercentage, ReduceButton, +SpeedOfTorsoHorizontal). Of the 9 ConfigureActivePress watchers, 7 are on the -1 pad and 2 +(Avionics, Myomers) bind real members reading the authored idle -1. + +Two dead mechanisms: the engine inert pad for unpublished names (`WATCHER.cpp:118-137`) and +`Mech::attrPad`, a shared read-only-0 Scalar aliased by five Entity attrs (ptr 0E514DD4; +`mech.cpp:708,730,736,738,740`). **All 118 state watchers bind real StateIndicators.** Liveness +verified: LocalVelocity (120 trigger polls), FootStep (14 matchfires), GeneratorOn x4, +Myomers.SpeedEffect, HeatSink.CurrentTemperature, PercentDone x5, WeaponState-driven sequences +started 21/4/2 times. Entity.AnimationState's 17 authored trigger states produced zero fires +(consistent with autodrive lacking falls/jumps — unverified, see F23). + +**Fix path:** publish missing attributes in owning subsystems' ATTRIBUTE_ENTRY tables (the proven +AmmoState/GeneratorState pattern) and replace the five attrPad aliases with driven members. Keep +`scratchpad/audio_coverage.py` as the regression check; include TargetRangeExponent in the +worklist. + + +#### F6. ReportLeak: the coolant-leak warning can never sound (High, CONFIRMED) + +All 19 subsystems that authored a ReportLeak Logical match-watcher pair (Condenser1-6, +GeneratorA-D, Avionics, Myomers, PPC_1/2, ERMLaser_1-3, SRM6_1/2) bind ReportLeak to NULL -> the +constant-zero pad. The 38 pad-bound match watchers (1->Start, 0->Stop of the looped leak-warning +sequence 0E4ACE50 — 3-note pattern 16/19/23, tempo 160; log:343) can never fire Start; the 19 +match=0 halves each fire one spurious Stop at prime. No other authored path targets the sequence; +its seqstart count is 0 for the whole session. **Largest single block of dead authored audio +(38 of 54 match watchers).** The underlying condition already exists in the port (`heat.cpp:418` +publishes CoolantMassLeakRate/coolantDraw for the LeakGauge). + +**Fix:** register ReportLeak on the subsystem attribute tables per the decomp's original binding +(read the binary's subsystem attribute table for the member it pointed at — likely the +leak-rate/leaking flag — rather than inventing a threshold). + + +#### F7. Missile-lock alarm impossible (High, CONFIRMED) + +Entity.IncomingLock (matchcfg 1->Start / 0->Stop of looped beeper 0E4AC548, note 58; log:299, +302-306) and Entity.DistanceToMissile (scalecfg ctlID=12 = **Tempo**, mapping distance 100..800 to +tempo 600..10 — the beep accelerates as the missile closes; log:300-301) both bind to +`Mech::attrPad` (read-only 0). The original backed these with real distinct members — **binary +attribute table verified: IncomingLock @mech+0x3FC, DistanceToMissile @mech+0x400**, initialized +in the mech reset (part_012.c:9446-9447) — a genuine regression, not dead content. + +**Fix:** real backing members driven by the missile/threat system: IncomingLock set while an +inbound lock/launch on the player exists; DistanceToMissile = range to nearest inbound missile +(far default when none). The authored watchers then reproduce the accelerating alarm unchanged. + + +#### F5. FootStep is a contact LEVEL, not a clip-transition pulse (High, CONFIRMED) + +Direct disassembly of BTL4OPT.EXE (both functions fall in decomp-export gaps): inside the mech's +per-frame performance, footStep@mech+0x394 = (rootJointTranslation.y <= *footStepThreshold) ? 1:0, +where the threshold pointer is SequenceController+0x20 = &animation_data[2] — the ANI header +word[2] that `JMOVER.cpp:1415` parses and nothing else reads — and the root joint is the gamedata +"jointlocal" joint (mech+0x5C8). Evaluation is skipped (value retained) while the channel's anim +state is 0/1. Present in both performance variants: master perf entry 0x4a9b5c (code +@0x4a9e80-0x4a9eb6, leg channel) and alternate entry 0x4ab9d8 (@0x4aba86-0x4abab9, body channel). + +The port instead pulses footStep=1 for a fixed 150 ms on every locomotion clip TRANSITION +(`mech2.cpp:265-270`, decay `mech4.cpp:5219-5230`) and its SequenceController never captures +animation_data[2] (`seqctl.cpp:153` skips hdr[2]). Step sounds fire at clip boundaries with fixed +width instead of authored root-height contact crossings — wrong counts for clips whose root Y +crosses the threshold twice or never. + +**Fix:** in the port's authoritative per-frame path, while leg-channel gait state not in {0,1}, +set footStep from rootY vs *footStepThreshold (captured at SelectSequence from &hdr[2]); remove +the 150 ms pulse/decay; replicate the state-0/1 value retention; re-export the two gap functions +(0x4a9b5c, 0x4ab9d8) into the decomp. + + +#### F16. Torso-twist servo whir entirely dead (Medium, CONFIRMED) + +Torso.SpeedOfTorsoHorizontal (scalecfg pitch -200..+200 cents over 0.5..0.9 + trigcfg start/stop +at 0.25) and Torso.MotionState (matchcfg ==2 -> Start@1.5) bind NULL (log:289-297) because the +reconstructed Torso publishes no attributes (`torso.cpp:143-144` default-constructs an empty +AttributeIndex). TorsoTwistInt01/Ext01/Stop01 are unreachable; none played. **Correction:** +hud.hpp:103's "id 7 -> @0x1E4" is the HUD class's own table (a HUD read-out), NOT a Torso member — +on the binary Torso, +0x1E4 is currentElevation. Do not publish the yaw rate there. + +**Fix:** register SpeedOfTorsoHorizontal on Torso as |yaw rate| (binary backing member plausibly +twistVelocity@0x1E8 or effTwistRate@0x244 — confirm the Torso attribute table in the decomp first) +and MotionState as the torso motion enum (authored trigger value 2; meaning inferred [T4]). + + +#### F17. Entity attrPad family: instability alarm, impact scaling, ReduceButton (Medium, CONFIRMED) + +Three authored Entity bindings alias the shared read-only attrPad while the binary bound each to a +distinct live member (binary attr table @0x50bea0: UnstablePercentage->+0x3F0, +CollisionSpeed->+0x4B4, ReduceButton->+0x340): + +1. **UnstablePercentage** — volume scale (0..1) and start trigger (thresh 0.01) both stuck; the + instability/gyro-stagger alarm never sounds. The driver is the mech+0x3F0 sway/overspeed + accumulator the port already flags as unreconstructed for the gyro — one model fixes both. +2. **CollisionSpeed** — authored AttackVolume [0.9,1] and Brightness [0.7,1] over impact speed + [0,25] pinned at minimum; impacts play (live CollisionState) but never scale with how hard you + hit. (Original +0x4B4 writer not located in the export; intent rests on the authored scales + + distinct member.) +3. **ReduceButton** — click-feedback trigger can never fire (whether the original cab drove it is + unverified; the dead binding is fact). + +**Fix:** real members: instability fraction from the 0x3F0 model; collisionSpeed = |impact +velocity| stored when collisionTemporaryState goes 0->1 (`mech4.cpp:5332-5382` already has it); +ReduceButton wired to the mapper like the Look* attrs. + + +#### F15. ConfigureActivePress: weapon-configure ticker can never fire (Medium, CONFIRMED) + +Binary ground truth: ConfigureActivePress is a **MechSubsystem-BASE** attribute (id 2, descriptor +@0x50de5c -> subsystem+0x110, registered by the MechSubsystem class stub @0x4acf10). Its only +runtime driver is MechWeapon's ConfigureMappables handler @004b9550: **0 while the configure +button is held, -1 on release** — a held-session flag, not a button index. The port misnames the +correctly-driven weapon field `vitalSubsystemIndex` ("no confirmed reader" — the reader is the +authored audio trigger watchers at thresh=-1), omits the attribute from the chain so every weapon +binds the constant -1 pad (log:529-531), and appends undriven duplicates on Myomers/Sensor at +invented offsets 0x358/0x328 (layout departure; behaviorally harmless). + +**Fix:** publish ConfigureActivePress at the MechSubsystem level bound to the existing +0x110 +field (rename vitalSubsystemIndex -> configureActivePress); drop or ignore the Myomers/Sensor +duplicates. + + +#### F18. FireCountdownStarted is the ammo COOK-OFF warning — and it's dead (Medium, PARTIAL, corrected) + +AmmoBinSRM6_1/2.FireCountdownStarted binds NULL -> pad (log:674-679, 721-726); its 4 authored +start/stop match watchers can never fire. **Corrected identification:** the binary's AmmoBin +attribute table (@0x512600) maps FireCountdownStarted -> AmmoBin+0x18C — the member the port +already reconstructs as **`cookOffArmed`** (set when the heat alarm hits FAILURE, FUN_004bd394; +armed via HandleMessage(1), FUN_004bdb94; cleared on CookOff/empty). It is the ammo-explosion +countdown warning, NOT an SRM salvo launch cue — do not touch projweap.cpp. + +**Fix:** one Logical ATTRIBUTE_ENTRY on AmmoBin pointing at the existing cookOffArmed member. + + +#### F19. Step-intensity invention fights a live authored feed (Medium, PARTIAL, corrected) + +The mech2.cpp foot-plant broadcast (`mech2.cpp:272-338`) is unauthored in its curve (speed/25 with +a 0.55 floor) and delivery (patch-sniffing the footfall mixers directly, zeroing the sibling +input). **The premise that the original feed was lost game code is wrong:** the authored stream +wires LocalAcceleration -> ctl100 ([0,10]->[0,1]) and LocalVelocity -> ctl101 ([0,0.6]->[0,0.4]) +scale watchers into the authored splitter, and the ctl101 watcher **fires on every captured +footstep** (log:1591, 2147, 2459, ...), zeroing the invention's just-written mixer input right +after the FootStep matchfire — in log:1574-1598 this drops the footstep start entirely +("[spatial] DROP transient start src=0E6BC948 vol=0"). + +**Fix:** remove the game-code broadcast; make the port's published LocalVelocity/LocalAcceleration +attributes carry the motion components the authored AudioMotionScale watchers extract (one +currently reads 0 while linear speed is 30.7 in the same poll — the port publishes the wrong/flat +component) and let the wholly-authored scale->splitter->mixer chain drive footfall volume and +brightness. + + +#### F20. Zoom blip: TargetRangeExponent unpublished (Low, UNVERIFIED-LOW) + +ControlsMapper.TargetRangeExponent binds NULL (log:330-331) so the authored AudioScalarDeltaTrigger +(blip per zoom step) is dead — yet the live member exists and slews every frame +(`btl4mppr.cpp:313-314, 391-407`). Pure bookkeeping: add the ATTRIBUTE_ENTRY (requires giving +BTL4ControlsMapper its own AttributePointers[] chained to MechControlsMapper's index). + + +#### F23. Benign-invention check (Low, UNVERIFIED-LOW) + +(1) FootStep 150 ms pulse fired the authored watcher fine in-session (superseded by F5 anyway). +(2) **AnimationState numbering is the one worth closing:** none of the 17 authored trigger states +{1,2,5,8-11,14-17,20-23,26,27} fired in 35 s of walking/turning; if the port's runtime clip +numbering differs from the binary's AnimationState numbering, all 17 sounds are silently mis-keyed +— diff the binary's state writer vs the port's clip ids, then force a fall/jump and confirm. +(3) CollisionState is driven 0/1 while authored as 4-state (NoCollision/InitialHit/Slide/Rest) — +nothing lost in THIS stream (listens on 1) but Slide/Rest scrape audio elsewhere would be dead. +(4) Master listener gain 0.6 (env-tunable) is a uniform port knob; no relative distortion. + +--- + + +## 4. Validations and KB corrections (no fidelity gap) + +- **Patch numbering = wPreset (settled).** The original SetupPatch @00465ba8 sends the authored + patch_ID verbatim as a MIDI Program Change (+ CC0 bank, + CC121 reset); the SBK resolves program + N by wPreset. `tools/sf2extract.py` already keys on phdr wPreset. Live traces confirm (patch 83 + -> ProgramButton01, patch 56 -> LaserLoaded01). **Any future tooling must key on wPreset, never + phdr record order** — bank1 has 38 presets where wPreset != file index. Record in + `context/decomp-reference.md`. +- **Calibration rate origin (KB correction).** FUN_0044e19c is + ApplicationManager::GetFrameRate(); the original renderer rate was the system CLOCK TICK rate + (18.2065 Hz / 28 Hz), i.e. audio "frames" were clock ticks. The port's `btl4app.cpp:283-303` + DPLARG+0x10 read is garbage-with-a-clamp; 1000 (Windows ticks/ms) is structurally equivalent and + correct, but delete the bogus read and pass SystemClock::GetTicksPerSecond() with a comment. +- **Published-velocity smoothing is faithful in kind.** Mech::Execute (FUN_004ab430) runs the + localVelocity components through AverageOf running-average filters (part_012.c:15169-15179); + annotate the port's smoothing with this evidence -> T1. + +--- + +## 5. Recommended Fix Order + +### Quick wins (small, isolated, high leverage) + +1. **F3 distance:** `alDistanceModel(AL_NONE)` + un-comment the distance multiply + apply in + Static3D + drop AL_MAX_DISTANCE attenuation writes. Near one-line; also fixes cull, voice + steal, and ducking. +2. **F4 volume curve:** square volume_scale at the three AL_GAIN sites. +3. **F10 doppler:** alDopplerFactor(0); add GetDopplerCents() into the Dynamic3D pitch chain. +4. **F15 ConfigureActivePress:** publish the base attribute on the existing +0x110 member. +5. **F18 cook-off:** one ATTRIBUTE_ENTRY -> cookOffArmed. +6. **F20 zoom blip:** one ATTRIBUTE_ENTRY -> targetRangeExponent. +7. **KB hygiene:** record wPreset confirmation; fix the calibration-rate claim; delete the DPLARG + read. + +### Structural — bank regeneration (ship as one unit) + +8. **F1 + F2 + F13 + F14 extractor rewrite:** emit ALL zones (per-zone WAV, keyLo/keyHi, loop + points, pan, attenuation), bake per-zone tuning into each WAV's declared rate + (44100*2^(((60-effRoot)*100+tune)/1200)), extend SAMPLEINFO, key-range selection in + SetupPatch/PlayNote, AL_SOFT_loop_points (alBufferiv). **Loop regions must land with or before + multi-zone** (the MechExplosion 1.5% loop layer otherwise becomes a repeating-boom bug). + This one unit fixes most of the pitch and layering fidelity at once. +9. **F13 release:** gain-ramp on Stop over the per-preset authored releaseVolEnv. + +### Structural — spatial/EFX + +10. **F9 filters:** EFX AL_FILTER_LOWPASS per source (brightness x distance HF rolloff per the + per-type decomp semantics). +11. **F11 reverb:** one EAXReverb aux slot; 3D sources wet (0.3), Direct dry. +12. **F12 placement:** AL_POSITION from the DirectPatch position enum; internal-spatialization + statics positioned from their computed azimuth. (F22 ITD/HRTF optional after.) + +### Structural — attribute reconstruction (decomp-guided, per finding) + +13. **F6 ReportLeak** (biggest dead block; read the binary's subsystem table for the member). +14. **F7 IncomingLock/DistanceToMissile** (members @+0x3FC/+0x400; drive from the missile system). +15. **F5 FootStep contact level** (+ re-export the two decomp gap functions). +16. **F19 footstep feed:** publish correct motion components; delete the mech2.cpp broadcast. +17. **F16 Torso attrs** (confirm the binary Torso table first), **F17 attrPad family** + (instability model @0x3F0, collisionSpeed, ReduceButton). +18. **F23(2) AnimationState numbering audit**, then F21 attack velocity/time as polish. + +**Regression harness:** re-run the audit capture after each block and assert with +`scratchpad/audio_coverage.py` that the dead-watcher count monotonically drops; A/B pitch/volume +spot checks against the recovered-rate table for the bank regeneration. diff --git a/engine/MUNGA/AUDCMP.cpp b/engine/MUNGA/AUDCMP.cpp index 50224e0..9c2c3b9 100644 --- a/engine/MUNGA/AUDCMP.cpp +++ b/engine/MUNGA/AUDCMP.cpp @@ -1,3 +1,4 @@ +#include #include "munga.h" #pragma hdrstop @@ -290,6 +291,11 @@ AudioControlSend::AudioControlSend( MemoryStream_Read(stream, &control_ID); MemoryStream_Read(stream, &control_value); + if (getenv("BT_ATTRBIND_LOG")) { static int s_sd=0; if (s_sd++<80) + DEBUG_STREAM << "[sendcfg] tgt=" << (void*)audio_component + << " ctl=" << (int)control_ID << "/" << control_value + << " entity=" << (void*)entity << "\n" << std::flush; } + Check(entity); entity->AddAudioComponent(this); @@ -472,6 +478,9 @@ void while ((audio_component = iterator.ReadAndNext()) != NULL) { Check(audio_component); + if (getenv("BT_AUDIO_SPATIAL")) { static int s_sp2=0; if (s_sp2++<200) + DEBUG_STREAM << "[split] " << (void*)this << " -> tgt=" << (void*)audio_component + << " ctl=" << (int)control_ID << "/" << control_value << "\n" << std::flush; } audio_component->ReceiveControl(control_ID, control_value); } } @@ -631,6 +640,10 @@ void } Check(audioComponentSocket.GetCurrent()); + if (getenv("BT_AUDIO_SPATIAL")) { static int s_mix=0; if (s_mix++<80) + DEBUG_STREAM << "[mix] mixer=" << (void*)this << " -> tgt=" << (void*)audioComponentSocket.GetCurrent() + << " outCtl=" << (int)outputControlID << " in[" << index << "]=" << control_value + << " sum=" << mixed_value << "\n" << std::flush; } audioComponentSocket.GetCurrent()->ReceiveControl( outputControlID, mixed_value @@ -793,6 +806,9 @@ void } Check(audioComponentSocket.GetCurrent()); + if (getenv("BT_AUDIO_SPATIAL") && mult_value <= 0.0f) { static int s_mx=0; if (s_mx++<300) + DEBUG_STREAM << "[mult0] mult=" << (void*)this << " -> tgt=" << (void*)audioComponentSocket.GetCurrent() + << " in[" << index << "]=" << control_value << " product=" << mult_value << "\n" << std::flush; } audioComponentSocket.GetCurrent()->ReceiveControl( outputControlID, mult_value @@ -840,6 +856,11 @@ AudioControlSmoother::AudioControlSmoother( MemoryStream_Read(stream, &initial_fill_value); audioControlAverage.SetSize(number_of_samples, initial_fill_value); + if (getenv("BT_ATTRBIND_LOG")) { static int s_sm=0; if (s_sm++<40) + DEBUG_STREAM << "[smoothcfg] this=" << (void*)this << " ctlID=" << (int)control_ID + << " samples=" << (int)number_of_samples << " fill=" << initial_fill_value + << "\n" << std::flush; } + AudioControlSmootherX( audio_component, entity, @@ -939,6 +960,14 @@ void { audioControlAverage.Add(control_value); + if (getenv("BT_AUDIO_SPATIAL")) { static int s_sa=0; + if ((controlID == 100 || controlID == 101) && s_sa++ < 3000) + DEBUG_STREAM << "[smooth] t=" << (GetTickCount() % 1000000) + << " this=" << (void*)this << " ctl=" << (int)controlID + << " in=" << control_value + << " avg=" << audioControlAverage.CalculateOlympicAverage() + << "\n" << std::flush; } + Check(audioComponentSocket.GetCurrent()); audioComponentSocket.GetCurrent()->ReceiveControl( controlID, @@ -1315,6 +1344,10 @@ void nextSampleTime += sampleDuration; Check(audioComponentSocket.GetCurrent()); + if (getenv("BT_AUDIO_SPATIAL")) { static int s_sh=0; if (s_sh++<60) + DEBUG_STREAM << "[snh] this=" << (void*)this << " -> tgt=" << (void*)audioComponentSocket.GetCurrent() + << " ctlID=" << (int)audioControlID << " val=" << currentValue + << " range=[" << minValue << "," << maxValue << "]\n" << std::flush; } audioComponentSocket.GetCurrent()->ReceiveControl( audioControlID, currentValue diff --git a/engine/MUNGA/AUDCMP.h b/engine/MUNGA/AUDCMP.h index 180aa68..3f6d98d 100644 --- a/engine/MUNGA/AUDCMP.h +++ b/engine/MUNGA/AUDCMP.h @@ -232,6 +232,12 @@ class AudioControlMixer: public AudioComponent { public: + // DIAG/game-facing: expose the authored output control so game-side control + // broadcasts (the foot-plant intensity send) can target only volume/brightness + // mix stages and never Start-outputting stages. + AudioControlID GetOutputControlID() const { return outputControlID; } + AudioComponent *GetTargetComponent() { return audioComponentSocket.GetCurrent(); } + // //-------------------------------------------------------------------- // Construction, Destruction, Testing @@ -307,6 +313,12 @@ class AudioControlMultiplier: public AudioComponent { public: + // DIAG/game-facing: expose the authored output control so game-side control + // broadcasts (the foot-plant intensity send) can target only volume/brightness + // mix stages and never Start-outputting stages. + AudioControlID GetOutputControlID() const { return outputControlID; } + AudioComponent *GetTargetComponent() { return audioComponentSocket.GetCurrent(); } + // //-------------------------------------------------------------------- // Construction, Destruction, Testing diff --git a/engine/MUNGA/AUDIO.cpp b/engine/MUNGA/AUDIO.cpp index b017424..93aa768 100644 --- a/engine/MUNGA/AUDIO.cpp +++ b/engine/MUNGA/AUDIO.cpp @@ -1,3 +1,4 @@ +#include #include "munga.h" #pragma hdrstop @@ -94,8 +95,15 @@ void } headEntitySocket.Add(entity); - alDistanceModel(AL_LINEAR_DISTANCE); - alDopplerFactor(0.3f); + // FIDELITY (AUDIO_FIDELITY.md F3/F10): the engine computes the AUTHORED + // distance-attenuation curve (AUDIO.INI amplitude_rolloff knee/exponent -> + // AudioLocation::distanceVolumeScale) and the authored doppler-cents model. + // Disable OpenAL's own models so they can't double-apply / fight them: + // AL_LINEAR_DISTANCE made far battle audio fade to zero on a straight line + // (-6 dB vs authored at 300u), and AL doppler ran with the wrong constants + // AND a sign-inverted velocity feed (approaching sources pitched DOWN). + alDistanceModel(AL_NONE); + alDopplerFactor(0.0f); #if 0 // @@ -131,8 +139,39 @@ void // // Increment frame counter // - audioFrameCount = Now().ticks; + // AUDIO CLOCK CALIBRATION FIX: AudioTime consumers (sequence event timing, + // compression curves) assume audioFrameCount advances at the renderer's + // calibrationRate (DefaultRendererRate = 30 frames/sec: + // Seconds_To_Frames = s * rate). The WinTesla port set raw Now().ticks here + // (~hundreds/sec), running sequences ~18x off the authored timing -- the + // AudioControlSequence events carrying the real footstep volumes never + // landed where authored. Convert ticks -> calibrated frames properly. + { + double tps = (double)SystemClock::GetTicksPerSecond(); + if (tps <= 0.0) tps = 1000.0; // GetTickCount ms fallback (static not yet measured) + audioFrameCount = (AudioFrameCount)( + (double)Now().ticks + * (double)application->GetAudioRenderer()->GetCalibrationRate() + / tps); + } Verify(audioFrameCount < LONG_MAX); + if (getenv("BT_AUDIO_SPATIAL")) { static long s_hx=0; if ((++s_hx % 300)==0) + DEBUG_STREAM << "[audioclock] frame=" << audioFrameCount << " execs=" << s_hx << "\n" << std::flush; } + + // (task #50, AUDIO_FIDELITY F13) service the authored release fades + // registered by PatchLevelOfDetail::StopNote (dB-linear note-off ramps). + { + extern void PRESET_serviceReleaseFades(float elapsed_seconds); + static long s_lastFadeTicks = 0; + long now_ticks = Now().ticks; + if (s_lastFadeTicks != 0 && now_ticks > s_lastFadeTicks) + { + double tps = (double)SystemClock::GetTicksPerSecond(); + if (tps <= 0.0) tps = 1000.0; + PRESET_serviceReleaseFades((float)((now_ticks - s_lastFadeTicks) / tps)); + } + s_lastFadeTicks = now_ticks; + } //set current listener orientation Vector3D headVelocity; @@ -140,6 +179,32 @@ void headVelocity.MultiplyByInverse(this->GetHeadEntity()->GetWorldLinearVelocity(), this->GetHeadEntity()->localToWorld); alListener3f(AL_VELOCITY, -headVelocity.x, -headVelocity.y, -headVelocity.z); + // LIVE-PLAYING DUMP (BT_AUDIO_DUMP): once a second list every playing AL + // source with its sample name / gain / pitch / loop -- catches "mystery + // sounds" (wrong sample, wrong pitch, chopped) red-handed. + if (getenv("BT_AUDIO_DUMP")) { + static long s_dumpTick = 0; + if ((++s_dumpTick % 45) == 0) { + extern ALuint *g_buffers; extern int g_numBuffers; extern const char *g_bufferNames[512]; + for (ALuint sid = 1; sid <= 40; ++sid) { + if (!alIsSource(sid)) continue; + ALint st = 0; alGetSourcei(sid, AL_SOURCE_STATE, &st); + if (st != AL_PLAYING) continue; + ALint buf = 0, looping = 0; ALfloat gain = 0, pitch = 0; + alGetSourcei(sid, AL_BUFFER, &buf); + alGetSourcei(sid, AL_LOOPING, &looping); + alGetSourcef(sid, AL_GAIN, &gain); + alGetSourcef(sid, AL_PITCH, &pitch); + const char *nm = "?"; + for (int b = 0; b < g_numBuffers && b < 512; ++b) + if ((ALint)g_buffers[b] == buf) { nm = g_bufferNames[b] ? g_bufferNames[b] : "?"; break; } + DEBUG_STREAM << "[playing] src=" << sid << " " << nm + << " gain=" << gain << " pitch=" << pitch + << " loop=" << looping << "\n" << std::flush; + } + } + } + #if 0 // // Get the entity @@ -398,6 +463,29 @@ void } } +// +//############################################################################# +//############################################################################# +// +void + AudioComponent::PrimeWatchers(int passes) +{ + Check(this); + for (int pass = 0; pass < passes; ++pass) + { + ChainIteratorOf iterator(&audioWatcherSocket); + Component *component; + while ((component = iterator.ReadAndNext()) != NULL) + { + Check(component); + // recurse gate-free: an AudioComponent child (mixer/smoother/ + // splitter) pumps ITS chain; an attribute watcher re-reads + + // re-sends. One pass per level keeps total work = passes x chain. + component->PrimeWatchers(1); + } + } +} + // //############################################################################# //############################################################################# diff --git a/engine/MUNGA/AUDIO.h b/engine/MUNGA/AUDIO.h index bd74762..9f95c5d 100644 --- a/engine/MUNGA/AUDIO.h +++ b/engine/MUNGA/AUDIO.h @@ -454,6 +454,14 @@ public: void ExecuteWatchers(); + // (task #50, AUDIO_FIDELITY F19) gate-free watcher pump for the transient + // cold-start prime: ExecuteWatchers above is frame-gated + // (DefaultAudioFrameDelay), so N calls in one tick collapse to one. + // Recurses through the watcher CHAIN (source <- mixer <- smoother <- + // scale), each hop gate-free. + virtual void + PrimeWatchers(int passes); + void Execute(); diff --git a/engine/MUNGA/AUDLVL.h b/engine/MUNGA/AUDLVL.h index 8312df0..8ca6f9d 100644 --- a/engine/MUNGA/AUDLVL.h +++ b/engine/MUNGA/AUDLVL.h @@ -162,6 +162,11 @@ protected: AudioLevelOfDetail* GetAudioLevelOfDetail(); +public: + // (task #50) public read of the active LOD -- the game-side footstep + // intensity send identifies footstep sources by their patch bank/id. + AudioLevelOfDetail* PeekAudioLevelOfDetail() { return GetAudioLevelOfDetail(); } + private: // //----------------------------------------------------------------------- diff --git a/engine/MUNGA/AUDREND.cpp b/engine/MUNGA/AUDREND.cpp index 2a2420f..ea5fb94 100644 --- a/engine/MUNGA/AUDREND.cpp +++ b/engine/MUNGA/AUDREND.cpp @@ -174,6 +174,10 @@ void if (audio_source->IsAudioSourceClipped(GetAudioHead())) { + if (getenv("BT_AUDIO_SPATIAL")) { static int s_cl=0; if (s_cl++<40) + DEBUG_STREAM << "[spatial] CLIPPED src=" << (void*)audio_source + << " headPos=(" << GetAudioHead()->GetHeadEntity()->localOrigin.linearPosition.x + << "," << GetAudioHead()->GetHeadEntity()->localOrigin.linearPosition.z << ")\n" << std::flush; } // // If it is a transient source then ignore request // @@ -181,7 +185,7 @@ void { #ifdef LAB_ONLY sourceClippedCount++; - #endif + #endif return; } audio_source_priority = audio_source->GetAudioSourcePriority(); @@ -190,7 +194,25 @@ void else { audio_source_priority = audio_source->GetAudioSourcePriority(); + // (task #50, AUDIO_FIDELITY F19) COLD-START PRIME: an idle source's own + // watcher socket only executes at Start attempts, so its authored + // AudioControlSmoothers (footstep volume: N=30/15, fill 0) warmed ONE + // sample per attempt -- and the transient drop gate below (vol < 0.3) + // rejected the first ~25 footfalls (~10-20 s of silent steps) before + // the average could cross the gate. On a Start request, pump the + // source's watchers a full smoother window so the volume chain is + // evaluated at its true steady state; the smoother keeps its authored + // smoothing role for live variation once the source is playing. + if (message->controlID == StartAudioControlID) + { + audio_source->PrimeWatchers(30); + } audio_source_volume_scale = audio_source->CalculateSourceVolumeScale(); + if (getenv("BT_AUDIO_SPATIAL")) { static int s_vs=0; if ((s_vs++ % 120)==0) + DEBUG_STREAM << "[spatial] request src=" << (void*)audio_source + << " vol=" << audio_source_volume_scale + << " headPos=(" << GetAudioHead()->GetHeadEntity()->localOrigin.linearPosition.x + << "," << GetAudioHead()->GetHeadEntity()->localOrigin.linearPosition.z << ")\n" << std::flush; } } // @@ -199,17 +221,38 @@ void // Then return //-------------------------------------------------------------------------- // + // DIAG (BT_AUDIO_NODROP): deliver low-volume transient starts anyway (at a + // floor volume) to isolate volume-feed problems from the rest of the chain. + if (getenv("BT_AUDIO_NODROP") && + message->controlID == StartAudioControlID && + audio_source->GetAudioRenderType() == TransientAudioRenderType && + audio_source_volume_scale < LowAudioVolumeThreshold) + { + DEBUG_STREAM << "[spatial] NODROP forcing start src=" << (void*)audio_source + << " vol=" << audio_source_volume_scale << "\n" << std::flush; + audio_source->ReceiveControl(VolumeAudioControlID, 0.7f); + audio_source_volume_scale = 0.7f; + } if ( message->controlID == StartAudioControlID && audio_source->GetAudioRenderType() == TransientAudioRenderType && audio_source_volume_scale < LowAudioVolumeThreshold ) { + if (getenv("BT_AUDIO_SPATIAL")) { static int s_dr=0; if (s_dr++<40) + DEBUG_STREAM << "[spatial] DROP transient start t=" << (GetTickCount() % 1000000) + << " src=" << (void*)audio_source + << " vol=" << audio_source_volume_scale + << " (below threshold " << LowAudioVolumeThreshold << ")\n" << std::flush; } #ifdef LAB_ONLY sourceClippedCount++; - #endif + #endif return; } + if (getenv("BT_AUDIO_SPATIAL") && message->controlID == StartAudioControlID) { + static int s_st=0; if (s_st++<40) + DEBUG_STREAM << "[spatial] START request src=" << (void*)audio_source + << " vol=" << audio_source_volume_scale << "\n" << std::flush; } // //-------------------------------------------------------------------------- diff --git a/engine/MUNGA/AUDSEQ.cpp b/engine/MUNGA/AUDSEQ.cpp index 24a6d73..9f83104 100644 --- a/engine/MUNGA/AUDSEQ.cpp +++ b/engine/MUNGA/AUDSEQ.cpp @@ -1,3 +1,4 @@ +#include #include "munga.h" #pragma hdrstop @@ -67,6 +68,9 @@ void { Check(this); Check(audio_component); + if (getenv("BT_AUDIO_SPATIAL")) { static int s_ev=0; if (s_ev++<200) + DEBUG_STREAM << "[seqev] tgt=" << (void*)audio_component + << " ctl=" << (int)audioControlID << "/" << audioControlValue << "\n" << std::flush; } audio_component->ReceiveControl(audioControlID, audioControlValue); } @@ -189,6 +193,16 @@ AudioControlSequence::AudioControlSequence( audioControlEventSocket.Add(audio_control_event); } + if (getenv("BT_ATTRBIND_LOG")) { static int s_sq=0; if (s_sq++<60) { + DEBUG_STREAM << "[seqcfg] seq=" << (void*)this << " tgt=" << (void*)audio_component + << " looped=" << (int)is_looped << " div=" << (int)divisions_per_beat + << " tempo=" << (int)tempo << " events=" << (int)number_of_control_events << " "; + { SChainIteratorOf it(&audioControlEventSocket); + AudioControlEvent *e; int n=0; + while ((e = it.ReadAndNext()) != NULL && n++ < 24) + DEBUG_STREAM << *e; } + DEBUG_STREAM << "\n" << std::flush; } } + AudioControlSequenceX( audio_component, entity, @@ -378,6 +392,9 @@ Logical void AudioControlSequence::StartSequence() { + if (getenv("BT_ATTRBIND_LOG")) { static int s_ss=0; if (s_ss++<40) + DEBUG_STREAM << "[seqstart] seq=" << (void*)this << "\n" << std::flush; } + Check(this); // diff --git a/engine/MUNGA/AUDSRC.cpp b/engine/MUNGA/AUDSRC.cpp index 486e3c2..765a4f2 100644 --- a/engine/MUNGA/AUDSRC.cpp +++ b/engine/MUNGA/AUDSRC.cpp @@ -1,3 +1,4 @@ +#include #include "munga.h" #pragma hdrstop @@ -416,6 +417,12 @@ AudioControlValue volume_scale; volume_scale = volumeScale * volumeMixScale * volumeCompressionScale; + if (getenv("BT_AUDIO_SPATIAL") && volume_scale <= 0.0f) { + static int s_z=0; if (s_z++<40) + DEBUG_STREAM << "[spatial] vol=0 breakdown src=" << (void*)this + << " volumeScale=" << volumeScale + << " mixScale=" << volumeMixScale + << " compression=" << volumeCompressionScale << "\n" << std::flush; } Clamp(volume_scale, MinAudioVolume, MaxAudioVolume); return volume_scale; } @@ -656,6 +663,8 @@ void Check(this); Clamp(control_value, MinAudioVolume, MaxAudioVolume); volumeScale = control_value / (MaxAudioVolume - MinAudioVolume); + if (getenv("BT_AUDIO_SPATIAL")) { static int s_vh=0; if (s_vh++<300) + DEBUG_STREAM << "[volset] src=" << (void*)this << " vol=" << volumeScale << "\n" << std::flush; } } // diff --git a/engine/MUNGA/AUDWTHR.cpp b/engine/MUNGA/AUDWTHR.cpp index b360b91..2f4fcb6 100644 --- a/engine/MUNGA/AUDWTHR.cpp +++ b/engine/MUNGA/AUDWTHR.cpp @@ -1,3 +1,4 @@ +#include #include "munga.h" #pragma hdrstop @@ -134,6 +135,12 @@ AudioMotionTrigger::AudioMotionTrigger( MemoryStream_Read(stream, &motionType); MemoryStream_Read(stream, &motionValue); + if (getenv("BT_ATTRBIND_LOG")) { static int s_mt=0; if (s_mt++<40) + DEBUG_STREAM << "[motiontrigcfg] attrPtr=" << (void*)attributePointer + << " motionType=" << (int)motionType << " (0=linear,1=angular)" + << " motionValue=" << (int)motionValue << " (0=X,1=Y,2=Z,3=len)" + << "\n" << std::flush; } + PrimeWatcher(); } @@ -283,6 +290,12 @@ AudioMotionScale::AudioMotionScale( MemoryStream_Read(stream, &motionType); MemoryStream_Read(stream, &motionValue); + if (getenv("BT_ATTRBIND_LOG")) { static int s_ms=0; if (s_ms++<40) + DEBUG_STREAM << "[motionscalecfg] attrPtr=" << (void*)attributePointer + << " motionType=" << (int)motionType << " (0=linear,1=angular)" + << " motionValue=" << (int)motionValue << " (0=X,1=Y,2=Z,3=len)" + << "\n" << std::flush; } + PrimeWatcher(); } @@ -873,6 +886,31 @@ AudioStateWatcher::AudioStateWatcher( AudioWatcherOf(stream, entity) { Check_Pointer(attributePointer); + // BRING-UP GUARD [T3, temporary]: an AudioStateWatcher binds to a StateIndicator + // attribute BY NAME. The Mech's own state indicators (SimulationState, Animation/ + // ReplicantAnimationState, CollisionState) are real; but audio also binds state + // attrs on subsystems that are not fully reconstructed yet (GeneratorState, + // CondenserState, Torso MotionState, ...). Those resolve to an unconstructed + // object -- a null/garbage vtable at +0 or a debug-fill (0xCDCDCDCD) watcher chain + // at +0x18 -- and AddAudioWatcher would AV in SChainOf::Add. Skip the register + // (that subsystem's state audio stays silent) until the subsystem is built; this + // is SELF-CLEARING (a real StateIndicator passes). See docs: audio subsystem wave. + // Validate the AUDIO SOCKET at +0x18 (what AddAudioWatcher touches), NOT the + // object's +0 vtable: a real StateIndicator has a vtable at +0, but the binary's + // 0x54 subsystem alarm (GaugeAlarm54) is non-polymorphic there (a raw header) yet + // has a real, constructed SChainOf socket at +0x18. A skip means the socket is + // unconstructed (null / debug-fill 0xCDCDCDCD) -- the inert pad or a subsystem not + // yet reconstructed. Registering there would AV in SChainOf::Add. + { + unsigned chain = *(unsigned*)((char*)attributePointer + 0x18); + if (chain == 0 || chain == 0xCDCDCDCD) + { + if (getenv("BT_AUDIO_LOG")) + DEBUG_STREAM << "[audiostate] skip watcher on unbuilt StateIndicator " + << attributePointer << " (chain=" << (void*)chain << ")\n" << std::flush; + return; + } + } Cast_Object(StateIndicator*, attributePointer)->AddAudioWatcher(this); } @@ -936,6 +974,14 @@ AudioStateTrigger::AudioStateTrigger( MemoryStream_Read(stream, &excludeTransition); MemoryStream_Read(stream, &excludeState); + if (getenv("BT_ATTRBIND_LOG")) { static int s_stc=0; if (s_stc++<120) + DEBUG_STREAM << "[statecfg] attrPtr=" << (void*)attributePointer + << " comp=" << (void*)audioComponentSocket.GetCurrent() + << " trigState=" << triggerState << " inv=" << (int)inverseTrigger + << " ctl=" << (int)controlID << "/" << controlValue + << " excl=" << (int)excludeTransition << "/" << excludeState + << "\n" << std::flush; } + PrimeWatcher(); } diff --git a/engine/MUNGA/AUDWTHR.h b/engine/MUNGA/AUDWTHR.h index 0ed3634..52555ab 100644 --- a/engine/MUNGA/AUDWTHR.h +++ b/engine/MUNGA/AUDWTHR.h @@ -1,4 +1,5 @@ #pragma once +#include #include "watcher.h" #include "audio.h" @@ -304,6 +305,14 @@ template MemoryStream_Read(stream, &controlValueOff); triggerOn = False; + + if (getenv("BT_ATTRBIND_LOG")) { static int s_tc=0; if (s_tc++<80) + DEBUG_STREAM << "[trigcfg] attrPtr=" << (void*)attributePointer << " comp=" << (void*)audioComponentSocket.GetCurrent() + << " thresh=" << attributeValueThreshold + << " inverse=" << (int)inverseTrigger + << " onID=" << (int)controlIDOn << "/" << controlValueOn + << " offID=" << (int)controlIDOff << "/" << controlValueOff + << "\n" << std::flush; } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -357,6 +366,10 @@ template void Check_Pointer(attribute_ptr); Scalar current_value = ExtractInterestingValue(attribute_ptr); + if (getenv("BT_AUDIO_SPATIAL") && current_value > 0.0f) { static int s_tg=0; if (s_tg++<120) + DEBUG_STREAM << "[trigger] attrPtr=" << (void*)attributePointer + << " val=" << current_value << " thresh=" << attributeValueThreshold + << " armed=" << (int)!triggerOn << "\n" << std::flush; } if (triggerOn) { if ( @@ -432,6 +445,30 @@ public: ); ~AudioScaleOf(); + // + //-------------------------------------------------------------------- + // Execute -- scales send EVERY poll (task #50, AUDIO_FIDELITY F19). + // + // The base watcher gate is a BITWISE compare (Motion::operator== is + // memcmp) that existed to skip truly static values. In the original, + // scale-watched attributes (velocities, accelerations, temperatures) + // were noisy physics floats that practically changed every poll, so + // scales streamed per-poll values into their authored consumers -- the + // footstep AudioControlSmoothers (N=30/15, fill 0) are SIZED for that + // cadence. Our reconstruction's math can be deterministic (the gait + // integrator lands on bit-identical derived values during smooth + // acceleration), which froze the gate and starved the smoothers (the + // 10-20 s footstep warm-up). Sending unconditionally restores the + // original's practical behavior; triggers/matchers keep the change + // gate (their semantics are edge-based). + //-------------------------------------------------------------------- + // + void + Execute() + { + GrabCurrentValue(); + } + // //-------------------------------------------------------------------- // BuildFromPage @@ -508,6 +545,13 @@ template MemoryStream_Read(stream, &controlValueBoundary1); MemoryStream_Read(stream, &controlValueBoundary2); MemoryStream_Read(stream, &exponent); + + if (getenv("BT_ATTRBIND_LOG")) { static int s_scf=0; if (s_scf++<160) + DEBUG_STREAM << "[scalecfg] attrPtr=" << (void*)attributePointer + << " comp=" << (void*)audioComponentSocket.GetCurrent() + << " ctlID=" << (int)controlID + << " aB=[" << attributeValueBoundary1 << "," << attributeValueBoundary2 << "]" + << " cB=[" << controlValueBoundary1 << "," << controlValueBoundary2 << "]\n" << std::flush; } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -625,6 +669,16 @@ template void Check(&audioComponentSocket); Check(audioComponentSocket.GetCurrent()); + if (getenv("BT_AUDIO_SPATIAL") && control_value <= 0.0f) { + static int s_sc=0; if (s_sc++<400) + DEBUG_STREAM << "[spatial] scale->0 attrPtr=" << (void*)attributePointer << " comp=" << (void*)audioComponentSocket.GetCurrent() << " raw=" << current_value << " ctlID=" << (int)controlID << " ctl=" << control_value << " aB=[" << attributeValueBoundary1 << "," << attributeValueBoundary2 << "]" << " cB=[" << controlValueBoundary1 << "," << controlValueBoundary2 << "]\n" << std::flush; } + if (getenv("BT_AUDIO_SPATIAL") && (controlID == 100 || controlID == 101)) { + static int s_fs2=0; if (s_fs2++<2000) + DEBUG_STREAM << "[fsscale] t=" << (GetTickCount() % 1000000) + << " attrPtr=" << (void*)attributePointer + << " comp=" << (void*)audioComponentSocket.GetCurrent() + << " ctl" << (int)controlID << " raw=" << current_value + << " out=" << control_value << "\n" << std::flush; } audioComponentSocket.GetCurrent()->ReceiveControl( controlID, control_value @@ -721,6 +775,12 @@ template MemoryStream_Read(stream, &attributeMatchValue); MemoryStream_Read(stream, &controlID); MemoryStream_Read(stream, &controlValue); + + if (getenv("BT_ATTRBIND_LOG")) { static int s_mc=0; if (s_mc++<80) + DEBUG_STREAM << "[matchcfg] attrPtr=" << (void*)attributePointer + << " match=" << attributeMatchValue + << " ctlID=" << (int)controlID << " ctlVal=" << controlValue + << "\n" << std::flush; } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -767,6 +827,10 @@ template void if (current_value == attributeMatchValue) { + if (getenv("BT_AUDIO_SPATIAL")) { static int s_mf=0; if (s_mf++<40) + DEBUG_STREAM << "[matchfire] attrPtr=" << (void*)attributePointer << " comp=" << (void*)audioComponentSocket.GetCurrent() << " compClass=" << (int)audioComponentSocket.GetCurrent()->GetClassID() + << " val=" << current_value << " -> ctl " << (int)controlID + << "/" << controlValue << "\n" << std::flush; } Check(&audioComponentSocket); Check(audioComponentSocket.GetCurrent()); #if 1 diff --git a/engine/MUNGA/CMPNNT.h b/engine/MUNGA/CMPNNT.h index e760bd1..b475e4c 100644 --- a/engine/MUNGA/CMPNNT.h +++ b/engine/MUNGA/CMPNNT.h @@ -11,6 +11,17 @@ public: virtual void Execute(); + // (task #50, AUDIO_FIDELITY F19) gate-free watcher pump for the audio + // transient cold-start prime. Default: run Execute() N times (attribute + // watchers re-read + re-send each pass). AudioComponent overrides it to + // RECURSE through its watcher chain, bypassing the per-component + // audio-frame gate that otherwise collapses the passes to one. + virtual void PrimeWatchers(int passes) + { + for (int pass = 0; pass < passes; ++pass) + Execute(); + } + static Derivation *GetClassDerivations(); static SharedData DefaultData; diff --git a/engine/MUNGA/MOVER.cpp b/engine/MUNGA/MOVER.cpp index cb1a062..67eaaef 100644 --- a/engine/MUNGA/MOVER.cpp +++ b/engine/MUNGA/MOVER.cpp @@ -11,6 +11,34 @@ #include "line.h" #include "app.h" #include "notation.h" +#include + +// +// EXACT axis-angle rotation composition -- matches the 1995 BT binary's angular +// integrator (FUN_00409f58): build a unit rotation quaternion from the rotation +// VECTOR `rotVec` (angle = |rotVec|, axis = rotVec/angle) as { axis*sin(angle/2), +// cos(angle/2) } and Hamilton-multiply it onto `base`. The dead-reckoner previously +// did `out.Add(base, rotVec)` -- adding a scaled angular-velocity vector to the heading +// quaternion. That is only a small-angle approximation: fine per-frame (tiny angle), +// but over a long replicant dead-reckon gap it DIVERGES (the heading drifts to ~180deg +// then snaps -- the spinning-peer hesitation). This composition is exact for any angle +// and stays on the unit sphere. +// +static void ExactAngularProject(Quaternion &out, const Quaternion &base, const Vector3D &rotVec) +{ + const Scalar ang = rotVec.Length(); + if (ang > 1.0e-6f) + { + const Scalar h = 0.5f * (Scalar)fmodf((float)ang, 6.2831853f); // half of angle mod 2pi + const Scalar s = (Scalar)(sinf((float)h) / ang); // sin(angle/2)/angle + const Quaternion dq(rotVec.x * s, rotVec.y * s, rotVec.z * s, (Scalar)cosf((float)h)); + out.Multiply(base, dq); // base (X) dq + } + else + { + out = base; + } +} //############################################################################# //############################### Mover ################################# @@ -395,10 +423,8 @@ Logical //------------------------------- // position_delta.Multiply(updateVelocity.angularMotion, time_slice); - projectedOrigin.angularPosition.Add( - updateOrigin.angularPosition, - position_delta - ); + ExactAngularProject(projectedOrigin.angularPosition, // was .Add (diverging vector-add) + updateOrigin.angularPosition, position_delta); projectedVelocity = updateVelocity; Check_Fpu(); @@ -460,10 +486,8 @@ Logical updateVelocity.angularMotion, time_slice ); - projectedOrigin.angularPosition.Add( - updateOrigin.angularPosition, - position_delta - ); + ExactAngularProject(projectedOrigin.angularPosition, // was .Add (diverging vector-add) + updateOrigin.angularPosition, position_delta); // //----------------------------------- @@ -1113,6 +1137,34 @@ BoxedSolid* return solid; } +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// The "static world" tail of FindBoxedSolidHitBy on its own: ray-test ONLY the +// zone's static solid tree (world structures -- garages, walls, props), NOT the +// tangible movers/doors. Same tree the mech's walk collides against, so a shot +// designated through this lands on exactly the geometry that blocks the mech. +// FindBoundingBoxHitBy clips line->length to the hit distance (HitByBounded -> +// line->length = enter), so the caller reads the entry point via line->FindEnd. +// +BoxedSolid* + Mover::FindStaticSolidHitBy( + Line *line + ) +{ + Check(this); + Check(line); + + InterestManager *interest_mgr = application->GetInterestManager(); + Check(interest_mgr); + InterestZone *zone = interest_mgr->GetInterestZone(interestZoneID); + Check(zone); + BoxedSolidTree* tree = zone->GetCollisionRoot(); + Check(tree); + BoxedSolid *result = (BoxedSolid*)tree->FindBoundingBoxHitBy(line); + Check_Fpu(); + return result; +} + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // BoxedSolidCollisionList* diff --git a/engine/MUNGA/MOVER.h b/engine/MUNGA/MOVER.h index 5dffe01..314b0b3 100644 --- a/engine/MUNGA/MOVER.h +++ b/engine/MUNGA/MOVER.h @@ -300,6 +300,16 @@ public: Line *line, Entity *except_by ); + // STATIC-WORLD-ONLY ray query: the "test against the static world" tail of + // FindBoxedSolidHitBy factored out -- ray-tests ONLY the zone's static solid + // tree (the world structures: garages, walls, props), skipping the tangible + // movers/doors. Used by the weapon boresight pick so a shot lands on the SAME + // static geometry that already blocks the mech's walk (mechs are picked + // separately, with their damage zones/lock). Clips line->length to the entry. + BoxedSolid* + FindStaticSolidHitBy( + Line *line + ); BoxedSolidCollisionList* CollideCenterOfMotion( Line *line, diff --git a/engine/MUNGA/SIMULATE.cpp b/engine/MUNGA/SIMULATE.cpp index 33fe5c7..0d7d1ef 100644 --- a/engine/MUNGA/SIMULATE.cpp +++ b/engine/MUNGA/SIMULATE.cpp @@ -1,3 +1,4 @@ +#include #include "munga.h" #pragma hdrstop @@ -486,6 +487,14 @@ void //############################################################################# // Watcher Support // +int + Simulation::DebugAudioWatcherCount() +{ + SChainIteratorOf iterator(audioWatcherSocket); + return (int)iterator.GetSize(); +} + + void Simulation::ExecuteWatchers() { @@ -496,6 +505,8 @@ void // Audio { SChainIteratorOf iterator(audioWatcherSocket); + if (getenv("BT_AUDIO_SPATIAL")) { static int s_ec=0; if ((s_ec++ % 600)==0) + DEBUG_STREAM << "[watchpoll] sim=" << (void*)this << " audioSocket size=" << iterator.GetSize() << "\n" << std::flush; } while ((watcher = iterator.ReadAndNext()) != NULL) { watcher->Execute(); diff --git a/engine/MUNGA/SIMULATE.h b/engine/MUNGA/SIMULATE.h index ab8acf5..314fcdf 100644 --- a/engine/MUNGA/SIMULATE.h +++ b/engine/MUNGA/SIMULATE.h @@ -247,6 +247,9 @@ public: void ExecuteWatchers(); + int // DEBUG (BT_AUDIO_LOG): how many audio watchers are registered on this sim + DebugAudioWatcherCount(); + private: SChainOf audioWatcherSocket; diff --git a/engine/MUNGA/WATCHER.cpp b/engine/MUNGA/WATCHER.cpp index 45f0b32..fd0227d 100644 --- a/engine/MUNGA/WATCHER.cpp +++ b/engine/MUNGA/WATCHER.cpp @@ -1,3 +1,4 @@ +#include #include "munga.h" #pragma hdrstop @@ -95,21 +96,45 @@ AttributeWatcher::AttributeWatcher( attributePointer = simulation->GetAttributePointer(attribute_name); - #if DEBUG_LEVEL>0 - if (attributePointer == NULL) - { - Dump(attribute_name); - } - #else - if (attributePointer == NULL) - { - DEBUG_STREAM << - "AttributeWatcher::AttributeWatcher - attribute " << - attribute_name << - "\n"; - Fail("AttributeWatcher::AttributeWatcher - attribute not found\n"); - } - #endif + if (getenv("BT_ATTRBIND_LOG")) + { + extern int g_curAudioWatcherClass; + DEBUG_STREAM << "[attrbind] class=" << g_curAudioWatcherClass + << " subsys=[" << subsystem_name + << "] attr=[" << attribute_name << "] -> ptr=" << attributePointer + << " vtbl=" << (attributePointer ? *(void**)attributePointer : (void*)0) + << "\n" << std::flush; + } + + // BRING-UP GUARD [T3, temporary]: audio references attributes on subsystems that + // are not fully reconstructed yet (GeneratorState/On, CondenserState, ReportLeak, + // Torso SpeedOfTorsoHorizontal/MotionState, Reservoir/Avionics/ControlsMapper ...). + // GetAttributePointer returns NULL for those, and the original engine Fail()s + // (fatal) -- which would abort every audio-enabled run. Redirect NULL to a shared + // inert zero pad so scalar/vector watchers read 0 (that sound stays silent) instead + // of crashing; state watchers on it are skipped by the AudioStateWatcher guard. + // Self-clearing: a registered attribute resolves to its real member. Remove once + // the subsystem attribute tables are reconstructed (the audio subsystem wave). + if (attributePointer == NULL) + { + static char s_missingAttrPad[64] = {0}; + // TYPED pad: ConfigureActivePress is the held-configure-button INDEX whose + // authored idle is -1 (none) -- NINE audio triggers gate the configure-mode + // ticker on it with threshold -1 across the configurable subsystems. The + // zero pad read as "button 0 held" and started the ticker at load (the + // eternal 2.7/s chirp). Redirect this attribute to a -1 pad; others keep 0. + static int s_configureIdlePad = -1; + CString configure_name("ConfigureActivePress"); + if (attribute_name == configure_name) + attributePointer = &s_configureIdlePad; + else + attributePointer = s_missingAttrPad; + if (getenv("BT_AUDIO_LOG")) + DEBUG_STREAM << "[attrnull] " << subsystem_name << "." << attribute_name + << " not reconstructed -> " + << (attributePointer == (void*)&s_configureIdlePad ? "-1 pad" : "inert pad") + << "\n" << std::flush; + } Check_Pointer(attributePointer); #endif diff --git a/engine/MUNGA/WATCHER.h b/engine/MUNGA/WATCHER.h index 289c053..654e465 100644 --- a/engine/MUNGA/WATCHER.h +++ b/engine/MUNGA/WATCHER.h @@ -1,4 +1,5 @@ #pragma once +#include #include "cmpnnt.h" #include "slot.h" @@ -155,14 +156,18 @@ private: void InitializeCurrentValue(); +protected: // //----------------------------------------------------------------------- - // GrabCurrentValue + // GrabCurrentValue -- protected (task #50): AudioScaleOf's per-poll + // Execute override calls it directly (see AUDWTHR.h). //----------------------------------------------------------------------- // void GrabCurrentValue(); +private: + // //----------------------------------------------------------------------- // DumpValue @@ -262,8 +267,32 @@ template void Check(this); Check_Pointer(attributePointer); + if (getenv("BT_AUDIO_SPATIAL")) { // poll-rate probe: total watcher polls + change events + static long s_polls=0; if ((++s_polls % 2000)==0) + DEBUG_STREAM << "[watchpoll] total polls=" << s_polls << "\n" << std::flush; + extern void *g_btFootStepAddr; // DIAG: uncapped tracer on THE footstep watcher + if ((void*)attributePointer == g_btFootStepAddr) { + static long s_fsp=0; + if ((++s_fsp % 120)==0 || !(currentValue == *(T*)attributePointer)) + DEBUG_STREAM << "[fswatch] poll#" << s_fsp << " cur=" << (int)*(int*)¤tValue + << " mem=" << *(int*)attributePointer << "\n" << std::flush; + } + extern void *g_btAccelAddr; // DIAG: poll-vs-change split on the accel attr + if ((void*)attributePointer == g_btAccelAddr) { + static long s_ap=0, s_ac=0; + int changed = !(currentValue == *(T*)attributePointer); + if (changed) ++s_ac; + if ((++s_ap % 120)==0 || (changed && (s_ac % 30)==0)) + DEBUG_STREAM << "[accwatch] t=" << (GetTickCount() % 1000000) + << " poll#" << s_ap << " changes=" << s_ac + << " memY=" << ((float*)attributePointer)[1] + << " curY=" << ((float*)¤tValue)[1] << "\n" << std::flush; + } + } if (!(currentValue == *(T*)attributePointer)) { + if (getenv("BT_AUDIO_SPATIAL")) { static int s_chg=0; if (s_chg++<60) + DEBUG_STREAM << "[watchpoll] CHANGE attrPtr=" << (void*)attributePointer << "\n" << std::flush; } #if DEBUG_LEVEL>0 if (dumpValue) { diff --git a/engine/MUNGA_L4/L4AUDEFX.cpp b/engine/MUNGA_L4/L4AUDEFX.cpp new file mode 100644 index 0000000..d5019ab --- /dev/null +++ b/engine/MUNGA_L4/L4AUDEFX.cpp @@ -0,0 +1,129 @@ +//########################################################################### +// +// L4AUDEFX.cpp -- OpenAL EFX bridge (task #50, AUDIO_FIDELITY F9/F11). +// See L4AUDEFX.h for the fidelity rationale. +// +//########################################################################### +#include +#include "mungal4.h" +#pragma hdrstop + +#include "l4audefx.h" +#include "openal/alc.h" +#include "openal/efx.h" + +#ifndef AL_EFFECT_EAXREVERB +#define AL_EFFECT_EAXREVERB 0x8000 // newer efx.h constant; OpenAL Soft supports it +#endif + +namespace +{ + bool s_available = false; + ALuint s_reverbSlot = 0; + ALuint s_reverbEffect = 0; + ALuint s_scratchFilter = 0; + + LPALGENEFFECTS p_alGenEffects = 0; + LPALEFFECTI p_alEffecti = 0; + LPALEFFECTF p_alEffectf = 0; + LPALGENAUXILIARYEFFECTSLOTS p_alGenAuxiliaryEffectSlots = 0; + LPALAUXILIARYEFFECTSLOTI p_alAuxiliaryEffectSloti = 0; + LPALAUXILIARYEFFECTSLOTF p_alAuxiliaryEffectSlotf = 0; + LPALGENFILTERS p_alGenFilters = 0; + LPALFILTERI p_alFilteri = 0; + LPALFILTERF p_alFilterf = 0; +} + +bool EFX_Available() +{ + return s_available; +} + +bool EFX_Initialize(float global_reverb_scale) +{ + ALCcontext *context = alcGetCurrentContext(); + if (context == 0) + { + return false; + } + ALCdevice *device = alcGetContextsDevice(context); + if (device == 0 || !alcIsExtensionPresent(device, "ALC_EXT_EFX")) + { + if (getenv("BT_AUDIO_LOG")) + DEBUG_STREAM << "[audio] EFX: ALC_EXT_EFX NOT present -- filters/reverb inert\n" << std::flush; + return false; + } + + p_alGenEffects = (LPALGENEFFECTS)alGetProcAddress("alGenEffects"); + p_alEffecti = (LPALEFFECTI)alGetProcAddress("alEffecti"); + p_alEffectf = (LPALEFFECTF)alGetProcAddress("alEffectf"); + p_alGenAuxiliaryEffectSlots = (LPALGENAUXILIARYEFFECTSLOTS)alGetProcAddress("alGenAuxiliaryEffectSlots"); + p_alAuxiliaryEffectSloti = (LPALAUXILIARYEFFECTSLOTI)alGetProcAddress("alAuxiliaryEffectSloti"); + p_alAuxiliaryEffectSlotf = (LPALAUXILIARYEFFECTSLOTF)alGetProcAddress("alAuxiliaryEffectSlotf"); + p_alGenFilters = (LPALGENFILTERS)alGetProcAddress("alGenFilters"); + p_alFilteri = (LPALFILTERI)alGetProcAddress("alFilteri"); + p_alFilterf = (LPALFILTERF)alGetProcAddress("alFilterf"); + + if (!p_alGenEffects || !p_alEffecti || !p_alEffectf + || !p_alGenAuxiliaryEffectSlots || !p_alAuxiliaryEffectSloti || !p_alAuxiliaryEffectSlotf + || !p_alGenFilters || !p_alFilteri || !p_alFilterf) + { + if (getenv("BT_AUDIO_LOG")) + DEBUG_STREAM << "[audio] EFX: entry points missing -- filters/reverb inert\n" << std::flush; + return false; + } + + alGetError(); + p_alGenAuxiliaryEffectSlots(1, &s_reverbSlot); + p_alGenEffects(1, &s_reverbEffect); + if (alGetError() != AL_NO_ERROR) + { + return false; + } + + // EAXReverb where available (OpenAL Soft: yes), plain reverb otherwise. + p_alEffecti(s_reverbEffect, AL_EFFECT_TYPE, AL_EFFECT_EAXREVERB); + if (alGetError() != AL_NO_ERROR) + { + p_alEffecti(s_reverbEffect, AL_EFFECT_TYPE, AL_EFFECT_REVERB); + } + p_alAuxiliaryEffectSloti(s_reverbSlot, AL_EFFECTSLOT_EFFECT, (ALint)s_reverbEffect); + // The authentic wet level: the original sent CC91 = global_reverb_scale + // (0.3 -> value 38) on every 3D channel; one global slot gain reproduces + // the same uniform send. + p_alAuxiliaryEffectSlotf(s_reverbSlot, AL_EFFECTSLOT_GAIN, + (global_reverb_scale < 0.0f) ? 0.0f : + (global_reverb_scale > 1.0f) ? 1.0f : global_reverb_scale); + + p_alGenFilters(1, &s_scratchFilter); + p_alFilteri(s_scratchFilter, AL_FILTER_TYPE, AL_FILTER_LOWPASS); + + s_available = (alGetError() == AL_NO_ERROR); + if (getenv("BT_AUDIO_LOG")) + DEBUG_STREAM << "[audio] EFX: " << (s_available ? "READY" : "FAILED") + << " (reverb slot gain=" << global_reverb_scale << ")\n" << std::flush; + return s_available; +} + +void EFX_SetSourceLowpassGainHF(ALuint source, float gainhf) +{ + if (!s_available) + { + return; + } + if (gainhf < 0.001f) gainhf = 0.001f; + if (gainhf > 1.0f) gainhf = 1.0f; + // filter params are COPIED at attach, so one scratch filter serves all + p_alFilterf(s_scratchFilter, AL_LOWPASS_GAIN, 1.0f); + p_alFilterf(s_scratchFilter, AL_LOWPASS_GAINHF, gainhf); + alSourcei(source, AL_DIRECT_FILTER, (ALint)s_scratchFilter); +} + +void EFX_AttachReverbSend(ALuint source) +{ + if (!s_available) + { + return; + } + alSource3i(source, AL_AUXILIARY_SEND_FILTER, (ALint)s_reverbSlot, 0, AL_FILTER_NULL); +} diff --git a/engine/MUNGA_L4/L4AUDEFX.h b/engine/MUNGA_L4/L4AUDEFX.h new file mode 100644 index 0000000..423c85c --- /dev/null +++ b/engine/MUNGA_L4/L4AUDEFX.h @@ -0,0 +1,44 @@ +#pragma once +//########################################################################### +// +// L4AUDEFX.h -- OpenAL EFX bridge for the authored filter/reverb chains +// (task #50, AUDIO_FIDELITY F9/F11). +// +// The original drove the AWE32 initial-filter-cutoff NRPN per frame +// (brightness x distance HF rolloff) and sent CC91 reverb on the 3D +// channels (global_reverb_scale=0.3, AUDIO.INI) while keeping the cockpit +// Direct channels dry. This bridge reproduces both through OpenAL Soft's +// EFX extension: one EAXReverb aux slot + a scratch AL_FILTER_LOWPASS +// whose parameters are copied at attach time. +// +//########################################################################### + +#include "openal/al.h" + +// Load the EFX entry points, create the reverb slot (gain = the authentic +// global_reverb_scale) and the scratch lowpass. Call once, with the AL +// context current. Returns false (and stays inert) without ALC_EXT_EFX. +bool EFX_Initialize(float global_reverb_scale); + +bool EFX_Available(); + +// Per-frame direct-path lowpass: gainhf is the linear HF gain at the EFX +// 5 kHz reference. Callers map the AWE cutoff (100 + scale*7900/127 Hz) +// through EFX_CutoffScaleToGainHF below. +void EFX_SetSourceLowpassGainHF(ALuint source, float gainhf); + +// AWE NRPN 21 curve -> EFX gainhf: cutoff_scale in [0,1] of the 100-8000 Hz +// span; approximated as the attenuation of a 2-pole lowpass at the 5 kHz +// reference [T3 -- curve shape approximate, endpoints exact]. +inline float EFX_CutoffScaleToGainHF(float cutoff_scale) +{ + if (cutoff_scale < 0.0f) cutoff_scale = 0.0f; + if (cutoff_scale > 1.0f) cutoff_scale = 1.0f; + float cutoff_hz = 100.0f + cutoff_scale * 7900.0f; + float g = (cutoff_hz / 5000.0f) * (cutoff_hz / 5000.0f); + return (g > 1.0f) ? 1.0f : ((g < 0.001f) ? 0.001f : g); +} + +// Wet-exterior routing: attach the source's aux send to the reverb slot +// (Dynamic3D/Static3D). Direct cockpit sources stay dry by default. +void EFX_AttachReverbSend(ALuint source); diff --git a/engine/MUNGA_L4/L4AUDIO.cpp b/engine/MUNGA_L4/L4AUDIO.cpp index 9932a79..942dc41 100644 --- a/engine/MUNGA_L4/L4AUDIO.cpp +++ b/engine/MUNGA_L4/L4AUDIO.cpp @@ -1,14 +1,29 @@ +#include +#include #include "mungal4.h" #pragma hdrstop #include "l4audio.h" #include "l4audlvl.h" +#include "l4audefx.h" #include "l4app.h" #include "l4audrnd.h" #include "..\munga\namelist.h" #include "..\munga\player.h" #include "..\rp\vtv.h" + +// NOTE -> PITCH (task #50): the AWE32 played every patch at the requested MIDI +// note relative to the sample root (60); sequences author notes (klaxon/ambience +// rhythms are PITCHED patterns). The port captured noteValue but never applied +// any pitch to the AL sources -- everything played at root (the "chirping" +// ambience: an authored deep tick at note 51 sounded as a high click). Apply +// pitch = 2^((note-60)/12) x the control-chain cents offset at start + per frame. +static inline float BTNotePitchFactor(int note_value) +{ + return (float)pow(2.0, ((double)note_value - 60.0) / 12.0); +} + //############################################################################# //####################### L4AudioSpatialization ######################### //############################################################################# @@ -921,7 +936,13 @@ void patch_resource = Cast_Object(PatchResource*, GetAudioResource()); Check(patch_resource); patch_resource->SetDistance(GetDistanceToSource()); - patch_resource->SetupPatch(channelSet); + patch_resource->SetupPatch(channelSet, (int)GetCurrentNoteValue()); + + { // apply the requested note's pitch to the freshly attached sources + float note_pitch = BTNotePitchFactor((int)GetCurrentNoteValue()); + for (int _i = 0; _i < channelSet.count; ++_i) + alSourcef(channelSet.sources[_i], AL_PITCH, note_pitch); + } // // Set the channel to default control values @@ -944,6 +965,31 @@ void channel->SendController(MIDI_PAN_CONTROL, MIDI_RIGHT_PAN_VALUE); break; }*/ + // (task #50, AUDIO_FIDELITY F12) the original routed each Direct cockpit + // source by its authored 6-value position enum (front/rear CARD + pan + // CC10 left/center/right). Reproduce as a listener-relative placement; + // a zone's own L/R pan (stereo-pair presets) offsets on top. The + // distance model is AL_NONE, so this affects direction only, never gain. + { + float px = 0.0f, pz = -1.0f; // Front, centered + switch (audioPosition) + { + case FrontDirectPatchPosition: px = 0.0f; pz = -1.0f; break; + case RearDirectPatchPosition: px = 0.0f; pz = 1.0f; break; + case FrontLeftDirectPatchPosition: px = -0.7f; pz = -0.7f; break; + case FrontRightDirectPatchPosition: px = 0.7f; pz = -0.7f; break; + case RearLeftDirectPatchPosition: px = -0.7f; pz = 0.7f; break; + case RearRightDirectPatchPosition: px = 0.7f; pz = 0.7f; break; + } + for (int _i = 0; _i < channelSet.count; ++_i) + { + SAMPLEINFO zinfo = PRESET_getSampleInfo( + patch_resource->GetBankID(), patch_resource->GetPatchID(), _i); + float pan_x = (zinfo.chan == CHANNEL_LEFT) ? -0.5f : + (zinfo.chan == CHANNEL_RIGHT) ? 0.5f : 0.0f; + alSource3f(channelSet.sources[_i], AL_POSITION, px + pan_x, 0.0f, pz); + } + } // // Set midi history to default control values @@ -979,7 +1025,7 @@ void // //TODO: Start OpenAL source playing // channel->SendNoteOn(GetCurrentNoteValue(), midi_velocity); - patch_resource->PlayNote(channelSet); + patch_resource->PlayNote(channelSet, (int)GetCurrentNoteValue()); #if 0 Tell("On " << (int)GetCurrentNoteValue() << "\n"); @@ -1033,6 +1079,11 @@ void pitch_offset = CalculateSourcePitchOffset(); double relativePitch = pow(2.0,pitch_offset/1200.0); Clamp(relativePitch,0.5,2.0); + { // apply the combined control-chain + note pitch per frame + float _np = BTNotePitchFactor((int)GetCurrentNoteValue()); + for (int _i = 0; _i < channelSet.count; ++_i) + alSourcef(channelSet.sources[_i], AL_PITCH, (float)relativePitch * _np); + } // //-------------------------------------------------------------------------- @@ -1057,6 +1108,13 @@ void if (Abs(filter_difference) >= filter_resolution) { lastMIDIFilterCutoff = midi_filter_cutoff; + // (task #50, AUDIO_FIDELITY F9) the original sent this cutoff as + // AWE NRPN 21; the EFX lowpass is the modern sink (was a dead + // bookkeeping write -- ctl-5 Brightness played full-bright). + float gainhf = EFX_CutoffScaleToGainHF( + (float)lastMIDIFilterCutoff / (float)MIDI_MAX_CONTROL_VALUE); + for (int _i = 0; _i < channelSet.count; ++_i) + EFX_SetSourceLowpassGainHF(channelSet.sources[_i], gainhf); } } @@ -1076,10 +1134,12 @@ void Check(audio_renderer); AudioHead *audio_head = audio_renderer->GetAudioHead(); Check(audio_head); + if (getenv("BT_AUDIO_SPATIAL")) { static int s_dp=0; if ((s_dp++ % 120)==0) + DEBUG_STREAM << "[spatial] direct src=" << (channelSet.count>0?channelSet.sources[0]:0) + << " vol=" << volume_scale << "\n" << std::flush; } for (int i=0; i < channelSet.count; i++) { - alSourcef(channelSet.sources[i],AL_MAX_DISTANCE,audio_location->getMaxDistance(audio_head)); - alSourcef(channelSet.sources[i], AL_GAIN, volume_scale); + alSourcef(channelSet.sources[i], AL_GAIN, volume_scale * volume_scale); // FIDELITY F4: CC7 squared law (linear was ~+6 dB at mid volumes) } } @@ -1204,7 +1264,13 @@ void patch_resource = Cast_Object(PatchResource*, GetAudioResource()); Check(patch_resource); patch_resource->SetDistance(GetDistanceToSource()); - patch_resource->SetupPatch(channelSet); + patch_resource->SetupPatch(channelSet, (int)GetCurrentNoteValue()); + + { // apply the requested note's pitch to the freshly attached sources + float note_pitch = BTNotePitchFactor((int)GetCurrentNoteValue()); + for (int _i = 0; _i < channelSet.count; ++_i) + alSourcef(channelSet.sources[_i], AL_PITCH, note_pitch); + } /*patch_resource->SetDistance(GetDistanceToSource()); for (i = 0; i < AudioChannelSetSize; i++) @@ -1227,6 +1293,12 @@ void channel->SendController(MIDI_REVERB_CONTROL, midi_reverb_level); channel->SendController(MIDI_CHORUS_CONTROL, MIDI_MIN_CONTROL_VALUE); }*/ + // (task #50, AUDIO_FIDELITY F11) the CC91 send above is the authentic + // wet-exterior contrast (global_reverb_scale on every 3D channel, decomp + // part_008.c:7278-7394); the EFX aux slot is the modern sink. Cockpit + // Direct sources stay dry (their CC91 was 0). + for (int _i = 0; _i < channelSet.count; ++_i) + EFX_AttachReverbSend(channelSet.sources[_i]); // // Set the channels to correct pan @@ -1299,7 +1371,7 @@ void Check(channel); channel->SendNoteOn(GetCurrentNoteValue(), midi_velocity); }*/ - patch_resource->PlayNote(channelSet); + patch_resource->PlayNote(channelSet, (int)GetCurrentNoteValue()); } // @@ -1405,15 +1477,43 @@ void pitch_offset = CalculateSourcePitchOffset(); + // FIDELITY (AUDIO_FIDELITY.md F10): the AUTHORED doppler (AUDIO.INI + // doppler_range/speed_of_sound -> AudioLocation::dopplerCents, computed each + // spatial update) -- the decomp proves the original added it to the pitch + // cents on THIS dynamic path (part_008.c:7466). AL's own doppler is now + // disabled (it ran with wrong constants and a sign-inverted velocity feed). + pitch_offset += GetAudioLocation()->GetDopplerCents(); + double relativePitch = pow(2.0,pitch_offset/1200.0); Clamp(relativePitch,0.5,2.0); + { // apply the combined control-chain + note pitch per frame + float _np = BTNotePitchFactor((int)GetCurrentNoteValue()); + for (int _i = 0; _i < channelSet.count; ++_i) + alSourcef(channelSet.sources[_i], AL_PITCH, (float)relativePitch * _np); + } + + if (getenv("BT_AUDIO_SPATIAL")) { static int s_sp=0; if ((s_sp++ % 120) == 0) + DEBUG_STREAM << "[spatial] dyn src=" << (channelSet.count>0?channelSet.sources[0]:0) + << " head=(" << headPosition.x << "," << headPosition.z << ")" + << " loc=(" << locationPosition.x << "," << locationPosition.z << ")" + << " dist=" << posMag << " vol=" << volume_scale << "\n" << std::flush; } + + // (task #50, AUDIO_FIDELITY F9) the original drove the AWE filter cutoff + // from highFreqCutoffScale x brightnessScale on this dynamic path -- + // UNGATED, all channels (part_008.c:7496,7589-7604): every moving 3D + // sound gets duller with distance (AUDIO.INI knee 60 / exponent 2.0). + float dyn_gainhf = EFX_CutoffScaleToGainHF( + (float)(GetAudioLocation()->GetHighFreqCutoffScale() + * CalculateSourceBrightnessScale() + * (Scalar)patch_resource->GetMaxMIDIFilterCutoff() + / (Scalar)MIDI_MAX_CONTROL_VALUE)); for (int i=0; i < channelSet.count; i++) { alSource3f(channelSet.sources[i],AL_POSITION,pos.x,pos.y,pos.z); - alSourcef(channelSet.sources[i], AL_GAIN, volume_scale); + alSourcef(channelSet.sources[i], AL_GAIN, volume_scale * volume_scale); // FIDELITY F4: CC7 squared law (linear was ~+6 dB at mid volumes) alSource3f(channelSet.sources[i],AL_VELOCITY,-relative_velocity.x,-relative_velocity.y,-relative_velocity.z); - alSourcef(channelSet.sources[i],AL_MAX_DISTANCE,audio_location->getMaxDistance(audio_head)); + EFX_SetSourceLowpassGainHF(channelSet.sources[i], dyn_gainhf); } } @@ -1432,22 +1532,18 @@ AudioControlValue // Scalar volume_scale = L4AudioSource::CalculateSourceVolumeScale(); - return volume_scale; - - // - // Update the spatial model that will result in the value - // for distance related volume attenuation - // - /*Check(application); - Check(application->GetAudioRenderer()); - UpdateSpatialModel(application->GetAudioRenderer()->GetAudioHead()); // - // Apply distance attenuation to the volume scale - // + // FIDELITY (AUDIO_FIDELITY.md F3): apply the AUTHORED distance attenuation + // (AUDIO.INI knee/rolloff curve, computed into distanceVolumeScale on every + // spatial update). This multiply was commented out and AL_LINEAR_DISTANCE + // substituted -- far sounds faded on a straight line to zero instead of the + // authored 1/(1+(k(d-knee))^2) curve, and the volume-based cull / voice + // steal / ducking chains were distance-blind. AL's model is now AL_NONE. + // Check(GetAudioLocation()); volume_scale *= GetAudioLocation()->GetDistanceVolumeScale(); - return volume_scale;*/ + return volume_scale; } //############################################################################# @@ -1637,6 +1733,22 @@ void // StartRequest(this); } +// +//############################################################################# +//############################################################################# +// +AudioControlValue + Static3DPatchSource::CalculateSourceVolumeScale() +{ + Check(this); + // FIDELITY (AUDIO_FIDELITY.md F3): same authored distance attenuation as the + // dynamic path -- the spatial model computes distanceVolumeScale per execute. + Scalar volume_scale = L4AudioSource::CalculateSourceVolumeScale(); + Check(GetAudioLocation()); + volume_scale *= GetAudioLocation()->GetDistanceVolumeScale(); + return volume_scale; +} + // //############################################################################# //############################################################################# @@ -1693,7 +1805,13 @@ void patch_resource = Cast_Object(PatchResource*, GetAudioResource()); Check(patch_resource); patch_resource->SetDistance(GetDistanceToSource()); - patch_resource->SetupPatch(channelSet); + patch_resource->SetupPatch(channelSet, (int)GetCurrentNoteValue()); + + { // apply the requested note's pitch to the freshly attached sources + float note_pitch = BTNotePitchFactor((int)GetCurrentNoteValue()); + for (int _i = 0; _i < channelSet.count; ++_i) + alSourcef(channelSet.sources[_i], AL_PITCH, note_pitch); + } /*for (i = 0; i < AudioChannelSetSize; i++) { if ((channel = channelSet.GetNth(i)) != NULL) @@ -1718,6 +1836,10 @@ void channel->SendController(MIDI_CHORUS_CONTROL, MIDI_MIN_CONTROL_VALUE); } }*/ + // (task #50, AUDIO_FIDELITY F11) static 3D sources are wet like the + // dynamic ones (the CC91 block above); Direct cockpit stays dry. + for (int _i = 0; _i < channelSet.count; ++_i) + EFX_AttachReverbSend(channelSet.sources[_i]); // // Set the channels to correct pan @@ -1850,7 +1972,7 @@ void channel->SendNoteOn(GetCurrentNoteValue(), midi_velocity); } }*/ - patch_resource->PlayNote(channelSet); + patch_resource->PlayNote(channelSet, (int)GetCurrentNoteValue()); } @@ -1921,6 +2043,11 @@ void pitch_offset = CalculateSourcePitchOffset(); double relativePitch = pow(2.0,pitch_offset/1200.0); Clamp(relativePitch,0.5,2.0); + { // apply the combined control-chain + note pitch per frame + float _np = BTNotePitchFactor((int)GetCurrentNoteValue()); + for (int _i = 0; _i < channelSet.count; ++_i) + alSourcef(channelSet.sources[_i], AL_PITCH, (float)relativePitch * _np); + } Vector3D relative_position; Vector3D relative_velocity; @@ -1933,12 +2060,19 @@ void relative_position = audio_location->GetVectorToSource(); } + // (task #50, AUDIO_FIDELITY F9) static sources: brightness-driven cutoff + // only (part_008.c:7831-7884 -- the NRPN block commented below). + float static_gainhf = EFX_CutoffScaleToGainHF( + (float)(CalculateSourceBrightnessScale() + * (Scalar)patch_resource->GetMaxMIDIFilterCutoff() + / (Scalar)MIDI_MAX_CONTROL_VALUE)); + //Static models have their position freely available as relative positions and stand still for (int i=0; i < channelSet.count; i++) { - alSourcef(channelSet.sources[i], AL_GAIN, volume_scale); + alSourcef(channelSet.sources[i], AL_GAIN, volume_scale * volume_scale); // FIDELITY F4: CC7 squared law (linear was ~+6 dB at mid volumes) alSource3f(channelSet.sources[i],AL_POSITION,relative_position.x,relative_position.y,relative_position.z); - alSourcef(channelSet.sources[i],AL_MAX_DISTANCE,audio_location->getMaxDistance(audio_head)); + EFX_SetSourceLowpassGainHF(channelSet.sources[i], static_gainhf); } // diff --git a/engine/MUNGA_L4/L4AUDIO.h b/engine/MUNGA_L4/L4AUDIO.h index b0eaa1e..67a634b 100644 --- a/engine/MUNGA_L4/L4AUDIO.h +++ b/engine/MUNGA_L4/L4AUDIO.h @@ -552,6 +552,10 @@ public: // State implementations // public: + // FIDELITY (AUDIO_FIDELITY.md F3): statics get the authored distance curve too + AudioControlValue + CalculateSourceVolumeScale(); + void StartImplementation(); void diff --git a/engine/MUNGA_L4/L4AUDLVL.cpp b/engine/MUNGA_L4/L4AUDLVL.cpp index 35baa6b..73285e1 100644 --- a/engine/MUNGA_L4/L4AUDLVL.cpp +++ b/engine/MUNGA_L4/L4AUDLVL.cpp @@ -1,3 +1,4 @@ +#include #include "mungal4.h" #pragma hdrstop @@ -8,6 +9,100 @@ #include "..\munga\namelist.h" #include "openal/al.h" +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Release fades ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// (task #50, AUDIO_FIDELITY F13) ~20 looping presets author a releaseVolEnv of +// 1.1-3.9 s the AWE32 applied as a note-off fade while the loop continued; +// StopNote registers a dB-linear ramp here instead of cutting. Serviced once +// per frame from AudioHead::Execute; a fade finishing (or being reclaimed by a +// restart) stops + rewinds the source so the next SetupPatch sees AL_INITIAL. +// +namespace +{ + struct ReleaseFade + { + ALuint source; + float gain0; + float duration; + float elapsed; + bool active; + }; + const int MAX_RELEASE_FADES = 64; + ReleaseFade s_releaseFades[MAX_RELEASE_FADES]; + + void FinishFade(ReleaseFade &fade) + { + alSourceStop(fade.source); + alSourceRewind(fade.source); + alSourcef(fade.source, AL_GAIN, fade.gain0); + fade.active = false; + } + + void StartReleaseFade(ALuint source, float duration) + { + for (int i = 0; i < MAX_RELEASE_FADES; i++) + { + if (s_releaseFades[i].active && s_releaseFades[i].source == source) + { + return; // already releasing + } + } + for (int i = 0; i < MAX_RELEASE_FADES; i++) + { + if (!s_releaseFades[i].active) + { + ReleaseFade &fade = s_releaseFades[i]; + fade.source = source; + alGetSourcef(source, AL_GAIN, &fade.gain0); + fade.duration = duration; + fade.elapsed = 0.0f; + fade.active = true; + return; + } + } + // table full -- fall back to the instant cut + alSourceStop(source); + alSourceRewind(source); + } + + // A restart wants this source back NOW: finalize any fade so the caller + // sees AL_INITIAL with the pre-fade gain restored. + void ReclaimFadingSource(ALuint source) + { + for (int i = 0; i < MAX_RELEASE_FADES; i++) + { + if (s_releaseFades[i].active && s_releaseFades[i].source == source) + { + FinishFade(s_releaseFades[i]); + return; + } + } + } +} + +void PRESET_serviceReleaseFades(float elapsed_seconds) +{ + for (int i = 0; i < MAX_RELEASE_FADES; i++) + { + ReleaseFade &fade = s_releaseFades[i]; + if (!fade.active) + { + continue; + } + fade.elapsed += elapsed_seconds; + if (fade.elapsed >= fade.duration) + { + FinishFade(fade); + } + else + { + // dB-linear ramp to -96 dB over the authored release time + float attenuation = powf(10.0f, -4.8f * (fade.elapsed / fade.duration)); + alSourcef(fade.source, AL_GAIN, fade.gain0 * attenuation); + } + } +} + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PatchLevelOfDetail ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #if DEBUG_LEVEL>0 @@ -26,7 +121,10 @@ PatchLevelOfDetail::PatchLevelOfDetail(PlugStream *stream): MemoryStream_Read(stream, &patchID); MemoryStream_Read(stream, &maxMIDIFilterCutoff); - Warn(GetVoiceCount() > 4); // HACK - AWE appears to only play 1st 4 voices + // (task #50) the AWE 4-voice comment applied to layers SHARING a key range; + // full-zone presets legitimately carry up to MAX_PRESET_SAMPLES zones + // (key-splits select at most a few per note). + Warn(GetVoiceCount() > MAX_PRESET_SAMPLES); #ifdef LAB_ONLY setupCount = 0; @@ -118,7 +216,7 @@ Logical //############################################################################# // void - PatchLevelOfDetail::SetupPatch(SourceSet sourceSet) + PatchLevelOfDetail::SetupPatch(SourceSet sourceSet, int note) { // Check(this); //// Check(channel); @@ -128,24 +226,44 @@ void // #endif SAMPLEINFO info; - //Attach buffers + // (task #50, AUDIO_FIDELITY F1) the authored MIDI note SELECTS zones: + // attach only samples whose [keyLo,keyHi] contains the note (key-splits + // pick one band, layers attach together); detach the rest so a rewound + // source can't replay a stale buffer from a previous note. for (int i=0; i < sourceSet.count; i++) { info = PRESET_getSampleInfo(bankID,patchID,i); if (info.bufferIndex >= 0) { + ReclaimFadingSource(sourceSet.sources[i]); + ALenum sourceState; alGetSourcei(sourceSet.sources[i], AL_SOURCE_STATE, &sourceState); + bool zone_matches = (note >= info.keyLo && note <= info.keyHi); + + if (getenv("BT_AUDIO_SPATIAL")) { static int s_se=0; if (s_se++<200) + DEBUG_STREAM << "[audio] SetupPatch ENTRY bank=" << (int)bankID << " patch=" << (int)patchID << " src=" << sourceSet.sources[i] + << " file=" << (info.file?info.file:"?") << " note=" << note << (zone_matches?"":" ZONESKIP") + << " state=" << sourceState << " (INITIAL=" << AL_INITIAL << ")" << "\n" << std::flush; } + if (sourceState == AL_INITIAL) { - alSourcei(sourceSet.sources[i],AL_BUFFER,AL_getBuffer(info.bufferIndex)); + alSourcei(sourceSet.sources[i],AL_BUFFER, + zone_matches ? AL_getBuffer(info.bufferIndex) : 0); + { static int s_a=0; if (getenv("BT_AUDIO_LOG") && s_a++<24) DEBUG_STREAM << "[audio] SetupPatch src=" << sourceSet.sources[i] << " file=" << (info.file?info.file:"?") << " loop=" << (info.loop==ForceStatic?0:1) << "\n" << std::flush; } alSourcei(sourceSet.sources[i],AL_SOURCE_RELATIVE,AL_TRUE); AudioRenderer *render = application->GetAudioRenderer(); AudioHead *head = render->GetAudioHead(); - alSource3f(sourceSet.sources[i],AL_POSITION,0,0,0); + // (F1) authored stereo pairs: pan hard-left/right zones via a + // listener-relative offset (the distance model is AL_NONE, so + // this affects direction only, never gain). + float pan_x = + (info.chan == CHANNEL_LEFT) ? -0.5f : + (info.chan == CHANNEL_RIGHT) ? 0.5f : 0.0f; + alSource3f(sourceSet.sources[i],AL_POSITION,pan_x,0,0); alSource3f(sourceSet.sources[i],AL_VELOCITY,0,0,0); if (info.loop == ForceStatic) @@ -173,7 +291,7 @@ void #endif*/ } -void PatchLevelOfDetail::PlayNote(SourceSet sourceSet) +void PatchLevelOfDetail::PlayNote(SourceSet sourceSet, int note) { if (sourceSet.count > 0 && sourceSet.sources != NULL) { @@ -182,12 +300,21 @@ void PatchLevelOfDetail::PlayNote(SourceSet sourceSet) for (int i = 0; i < sourceSet.count; i++) { + // (F1) play only the zones SetupPatch attached for this note + ALint attached = 0; + alGetSourcei(sourceSet.sources[i], AL_BUFFER, &attached); + if (attached == 0) + { + continue; + } + ALenum state; alGetSourcei(sourceSet.sources[i], AL_SOURCE_STATE, &state); if (state != AL_PLAYING) { alSourcePlay(sourceSet.sources[i]); + { static int s_p=0; if (getenv("BT_AUDIO_LOG") && s_p++<30) DEBUG_STREAM << "[audio] PlayNote alSourcePlay src=" << sourceSet.sources[i] << " note=" << note << "\n" << std::flush; } } ALenum error = alGetError(); @@ -202,10 +329,27 @@ void PatchLevelOfDetail::PlayNote(SourceSet sourceSet) void PatchLevelOfDetail::StopNote(SourceSet sourceSet) { + if (getenv("BT_AUDIO_LOG")) { static int s_s=0; if (s_s++<400) { DEBUG_STREAM << "[audio] StopNote"; for (int _i=0;_i0 && sourceSet.sources != NULL) { - alSourceStopv(sourceSet.count, sourceSet.sources); - alSourceRewindv(sourceSet.count, sourceSet.sources); + // (task #50, AUDIO_FIDELITY F13) honor the authored releaseVolEnv: + // a playing zone with a release time fades out dB-linearly instead of + // cutting; everything else stops instantly (faithful for one-shots). + for (int i = 0; i < sourceSet.count; i++) + { + SAMPLEINFO info = PRESET_getSampleInfo(bankID,patchID,i); + ALenum state; + alGetSourcei(sourceSet.sources[i], AL_SOURCE_STATE, &state); + if (state == AL_PLAYING && info.releaseSec > 0.05f) + { + StartReleaseFade(sourceSet.sources[i], info.releaseSec); + } + else + { + alSourceStop(sourceSet.sources[i]); + alSourceRewind(sourceSet.sources[i]); + } + } } } @@ -259,7 +403,7 @@ Logical //############################################################################# // void - PatchResource::SetupPatch(SourceSet sourceSet) + PatchResource::SetupPatch(SourceSet sourceSet, int note) { Check(this); // Check(channel); @@ -268,10 +412,10 @@ void Cast_Object(PatchLevelOfDetail*, GetAudioLevelOfDetail()); Check(patch_level_of_detail); - patch_level_of_detail->SetupPatch(sourceSet); + patch_level_of_detail->SetupPatch(sourceSet, note); } -void PatchResource::PlayNote(SourceSet sourceSet) +void PatchResource::PlayNote(SourceSet sourceSet, int note) { Check(this); // Check(channel); @@ -280,7 +424,7 @@ void PatchResource::PlayNote(SourceSet sourceSet) Cast_Object(PatchLevelOfDetail*, GetAudioLevelOfDetail()); Check(patch_level_of_detail); - patch_level_of_detail->PlayNote(sourceSet); + patch_level_of_detail->PlayNote(sourceSet, note); } void PatchResource::StopNote(SourceSet sourceSet) @@ -295,6 +439,24 @@ void PatchResource::StopNote(SourceSet sourceSet) patch_level_of_detail->StopNote(sourceSet); } +// +//############################################################################# +//############################################################################# +// +int PatchResource::GetBankID() +{ + PatchLevelOfDetail *patch_level_of_detail = + Cast_Object(PatchLevelOfDetail*, GetAudioLevelOfDetail()); + return (patch_level_of_detail != 0) ? patch_level_of_detail->GetBankID() : 0; +} + +int PatchResource::GetPatchID() +{ + PatchLevelOfDetail *patch_level_of_detail = + Cast_Object(PatchLevelOfDetail*, GetAudioLevelOfDetail()); + return (patch_level_of_detail != 0) ? patch_level_of_detail->GetPatchID() : 0; +} + // //############################################################################# //############################################################################# diff --git a/engine/MUNGA_L4/L4AUDLVL.h b/engine/MUNGA_L4/L4AUDLVL.h index 73dca59..934aea8 100644 --- a/engine/MUNGA_L4/L4AUDLVL.h +++ b/engine/MUNGA_L4/L4AUDLVL.h @@ -26,22 +26,38 @@ struct SAMPLEINFO const char *file; SampleChannel chan; SampleLoop loop; + // (task #50, AUDIO_FIDELITY F1/F13) full-zone metadata from the SF2 banks: + // the authored MIDI note SELECTS zones by [keyLo,keyHi]; loop regions are + // sub-ranges in sample frames (AL_SOFT_loop_points); releaseSec is the + // authored releaseVolEnv fade applied on Stop instead of an instant cut. + int keyLo; + int keyHi; + int loopStart; + int loopEnd; + float releaseSec; }; +// AllExplosion (bank2 p125) authors 25 layered zones -- the largest preset. +const int MAX_PRESET_SAMPLES = 25; + struct PRESETINFO { int sampleNum; - SAMPLEINFO samples[5]; + SAMPLEINFO samples[MAX_PRESET_SAMPLES]; bool is3d; }; -extern PRESETINFO allPresets[2][100]; +extern PRESETINFO allPresets[2][128]; bool PRESET_isImplemented(int bank, int preset); int PRESET_getNumSamples(int bank, int preset); SAMPLEINFO PRESET_getSampleInfo(int bank, int preset, int sampleInd); void PRESET_setBufferIndex(int bank, int preset, int sampleInd, int index); +// (task #50, AUDIO_FIDELITY F13) authored release fades: StopNote registers a +// dB-linear gain ramp instead of cutting; AudioHead::Execute services them. +void PRESET_serviceReleaseFades(float elapsed_seconds); + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PatchLevelOfDetail ~~~~~~~~~~~~~~~~~~~~~~~~~~ typedef MIDIValue SBKPatchID; @@ -59,7 +75,7 @@ public: PatchLevelOfDetail(PlugStream *stream); ~PatchLevelOfDetail(); - void PlayNote(SourceSet sourceSet); + void PlayNote(SourceSet sourceSet, int note); void StopNote(SourceSet sourceSet); Logical @@ -69,6 +85,11 @@ public: GetVoiceCount() {return PRESET_getNumSamples(bankID,patchID);} + // (task #50) expose the authored bank/patch so the game-side footstep + // intensity send can identify footstep sources precisely. + int GetBankID() const { return (int)bankID; } + int GetPatchID() const { return (int)patchID; } + // //----------------------------------------------------------------------- // BuildFromPage @@ -88,7 +109,7 @@ public: //----------------------------------------------------------------------- // void - SetupPatch(SourceSet sourceSet); + SetupPatch(SourceSet sourceSet, int note); MIDINRPNValue GetMaxMIDIFilterCutoff() @@ -136,7 +157,7 @@ public: PatchResource(PlugStream *stream); ~PatchResource(); - void PlayNote(SourceSet sourceSet); + void PlayNote(SourceSet sourceSet, int note); void StopNote(SourceSet sourceSet); Logical @@ -161,9 +182,14 @@ public: //----------------------------------------------------------------------- // void - SetupPatch(SourceSet sourceSet); + SetupPatch(SourceSet sourceSet, int note); MIDINRPNValue GetMaxMIDIFilterCutoff(); + + // (task #50) authored bank/patch pass-throughs (the LOD accessor is + // protected on AudioResource) -- zone metadata lookups at the source layer. + int GetBankID(); + int GetPatchID(); }; diff --git a/engine/MUNGA_L4/L4AUDRES.cpp b/engine/MUNGA_L4/L4AUDRES.cpp index 4244eb6..bbb1fb9 100644 --- a/engine/MUNGA_L4/L4AUDRES.cpp +++ b/engine/MUNGA_L4/L4AUDRES.cpp @@ -1,3 +1,7 @@ +#include +#include +#include +#include #include "mungal4.h" #pragma hdrstop @@ -13,10 +17,66 @@ #include "..\munga\app.h" #include "..\munga\notation.h" #include "openal\al.h" -#include "sndfile.h" +#include "openal\alc.h" ALuint *g_buffers; int g_numBuffers; +const char *g_bufferNames[512]; // bufferInd -> sample file (diag: the live-playing dump) +int g_curAudioWatcherClass = 0; + +// +// Minimal canonical-PCM WAV reader. The repo's libsndfile-1.dll is a STUB +// (exports 15 funcs by ordinal only, no names -- sf_open always returns NULL +// and sf_strerror(NULL) faults), so the original sf_open path loaded nothing. +// Our soundbank samples (AUDIO1/2.RES, cracked by scratchpad/sf2extract.py) +// are written as plain 16-bit mono PCM WAVs, so a tiny RIFF/WAVE fmt+data +// parser loads the real sample data directly into the AL buffer -- no +// external dependency, no stub. Handles 8/16-bit mono/stereo PCM. +// +static bool LoadWavPCM(const char *path, char **outData, int *outBytes, + int *outChannels, int *outBits, int *outRate) +{ + FILE *fp = fopen(path, "rb"); + if (!fp) return false; + fseek(fp, 0, SEEK_END); long flen = ftell(fp); fseek(fp, 0, SEEK_SET); + if (flen < 44) { fclose(fp); return false; } + unsigned char *buf = new unsigned char[flen]; + size_t got = fread(buf, 1, flen, fp); + fclose(fp); + if ((long)got != flen) { delete [] buf; return false; } + if (memcmp(buf, "RIFF", 4) != 0 || memcmp(buf + 8, "WAVE", 4) != 0) { delete [] buf; return false; } + int channels = 0, bits = 0, rate = 0; + char *pcm = 0; int pcmBytes = 0; + bool haveFmt = false; + long p = 12; + while (p + 8 <= flen) + { + const unsigned char *id = buf + p; + unsigned int csz = buf[p+4] | (buf[p+5]<<8) | (buf[p+6]<<16) | ((unsigned)buf[p+7]<<24); + long body = p + 8; + if (memcmp(id, "fmt ", 4) == 0 && csz >= 16) + { + channels = buf[body+2] | (buf[body+3]<<8); + rate = buf[body+4] | (buf[body+5]<<8) | (buf[body+6]<<16) | ((unsigned)buf[body+7]<<24); + bits = buf[body+14] | (buf[body+15]<<8); + haveFmt = true; + } + else if (memcmp(id, "data", 4) == 0) + { + long avail = flen - body; + pcmBytes = ((long)csz <= avail) ? (int)csz : (int)avail; + if (pcmBytes < 0) pcmBytes = 0; + pcm = new char[pcmBytes > 0 ? pcmBytes : 1]; + memcpy(pcm, buf + body, pcmBytes); + } + p = body + csz + (csz & 1); // RIFF chunks are word-aligned + } + delete [] buf; + if (!haveFmt || pcm == 0) { delete [] pcm; return false; } + *outData = pcm; *outBytes = pcmBytes; + *outChannels = channels; *outBits = bits; *outRate = rate; + return true; +} //############################################################################# //####################### AudioObjectStream ############################# @@ -82,6 +142,11 @@ RegisteredClass* Check(this); Verify(class_ID != RegisteredClass::NullClassID); + // DIAG (BT_ATTRBIND_LOG): record which audio-object ClassID is being built so the + // AttributeWatcher trace can print the watcher TYPE alongside the bound attribute. + extern int g_curAudioWatcherClass; + g_curAudioWatcherClass = (int)class_ID; + // //----------------------------------------------------------------------- // HACK - Constructors should be referenced through registration, for @@ -542,7 +607,7 @@ void for(int i=1; i <= 2; i++) { - for (int j=0; j < 100; j++) + for (int j=0; j < 128; j++) { if (PRESET_isImplemented(i,j)) { @@ -558,6 +623,7 @@ void alGetError(); g_buffers = new ALuint[g_numBuffers]; alGenBuffers(g_numBuffers,g_buffers); + if (getenv("BT_AUDIO_LOG")) DEBUG_STREAM << "[audio] alGenBuffers count=" << g_numBuffers << "\n" << std::flush; ALenum error; if ((error = alGetError()) != AL_NO_ERROR) { @@ -572,7 +638,7 @@ void //Load buffers for(int i=1; i<=2 && bufferInd < g_numBuffers; i++) { - for(int j=0; j<100 && bufferInd < g_numBuffers; j++) + for(int j=0; j<128 && bufferInd < g_numBuffers; j++) { if (PRESET_isImplemented(i,j)) { @@ -580,84 +646,86 @@ void { SAMPLEINFO info = PRESET_getSampleInfo(i,j,k); - //Load WAV to buffer - char *data; - int size; - //Open WAV char fullname[50]; - strcpy_s(fullname,50,"AUDIO\\"); + strcpy_s(fullname,50,"AUDIO/"); strcat_s(fullname,50,info.file); - SF_INFO *sfInfo = new SF_INFO[2]; - //SF_INFO is working different than expected! - sfInfo->format = 0; - SNDFILE *file = sf_open(fullname,SFM_READ,sfInfo); - - if (file == NULL) + char *data = 0; int size = 0; + int wavCh = 0, wavBits = 0, wavRate = 0; + if (!LoadWavPCM(fullname, &data, &size, &wavCh, &wavBits, &wavRate)) { - DEBUG_STREAM << "Failed to open." << std::endl; + if (getenv("BT_AUDIO_LOG") && bufferInd == 0) { + char cwd[512]; cwd[0]=0; _getcwd(cwd, sizeof(cwd)); + DEBUG_STREAM << "[audio] DIAG cwd=[" << cwd << "] path=[" << fullname << "] WAV load FAILED\n" << std::flush; + } + DEBUG_STREAM << "[audio] Failed to open AUDIO/" << info.file << " -- skipping\n" << std::flush; + PRESET_setBufferIndex(i,j,k,bufferInd); + bufferInd++; + continue; } - unsigned long formatBits = 0; - unsigned long sampleRate = sfInfo->samplerate; - size = sfInfo->frames; - - bool isMono = (sfInfo->channels == 1); - - if (sfInfo->format & SF_FORMAT_PCM_S8) - { - formatBits = 8; - } else if (sfInfo->format & SF_FORMAT_PCM_16) - { - formatBits = 16; - } else - { - DEBUG_STREAM << "BAD FORMAT" << std::endl; - } - ALenum format; - if (formatBits == 8) + if (wavBits == 8) + format = (wavCh == 1) ? AL_FORMAT_MONO8 : AL_FORMAT_STEREO8; + else + format = (wavCh == 1) ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16; + + //Feed the buffer with the real PCM sample data + alBufferData(g_buffers[bufferInd], format, data, size, wavRate); + + // (task #50, AUDIO_FIDELITY F13) honor the authored loop + // REGION -- the EMU8000 looped [dwStartLoop,dwEndLoop], not + // the whole sample (MechExplosion's boom carries a ~16 ms + // sustain slice that must not replay the full boom). + #ifndef AL_LOOP_POINTS_SOFT + #define AL_LOOP_POINTS_SOFT 0x2015 + #endif + if (info.loop != ForceStatic && info.loopEnd > info.loopStart) { - if (isMono) - { - format = AL_FORMAT_MONO8; - } else - { - size *= 2; - format = AL_FORMAT_STEREO8; - } - } else if (formatBits == 16) - { - size *= 2; - if (isMono) - { - format = AL_FORMAT_MONO16; - } else - { - size *= 2; - format = AL_FORMAT_STEREO16; - } + ALint loop_points[2] = { info.loopStart, info.loopEnd }; + alGetError(); + alBufferiv(g_buffers[bufferInd], AL_LOOP_POINTS_SOFT, loop_points); + ALenum lpErr = alGetError(); + if (lpErr != AL_NO_ERROR && getenv("BT_AUDIO_LOG")) + DEBUG_STREAM << "[audio] loop-points REJECTED buf" << bufferInd + << " " << info.file << " [" << info.loopStart << "," + << info.loopEnd << "] err=" << lpErr << "\n" << std::flush; } - ALsizei alSampleRate = sampleRate; - - //Load size & data - delete [] sfInfo; - data = new char[size]; - sf_read_raw(file,data,size); - sf_close(file); - - //Feed the buffer - alBufferData(g_buffers[bufferInd],format,data,size,alSampleRate); + if (bufferInd < 512) g_bufferNames[bufferInd] = info.file; + if (getenv("BT_AUDIO_LOG") && bufferInd < 3) + DEBUG_STREAM << "[audio] loaded buf" << bufferInd << " " << info.file + << " ch=" << wavCh << " bits=" << wavBits << " rate=" << wavRate + << " bytes=" << size << " alErr=" << alGetError() << "\n" << std::flush; PRESET_setBufferIndex(i,j,k,bufferInd); bufferInd++; - - delete [] data; + delete [] data; } } } } + if (getenv("BT_AUDIO_TEST") && g_numBuffers > 0) + { + // PROOF OF LIFE: play the first loaded sample so we can confirm the + // SF2 -> WAV -> OpenAL buffer -> speakers path works end to end. + ALCcontext *ctx = alcGetCurrentContext(); + alGetError(); + ALuint testsrc = 0; alGenSources(1, &testsrc); + ALenum genErr = alGetError(); + alSourcei(testsrc, AL_BUFFER, g_buffers[0]); + ALenum bufErr = alGetError(); + alSourcei(testsrc, AL_LOOPING, AL_TRUE); + alSourcef(testsrc, AL_GAIN, 1.0f); + alSourcePlay(testsrc); + ALenum playErr = alGetError(); + ALint st = 0; alGetSourcei(testsrc, AL_SOURCE_STATE, &st); + DEBUG_STREAM << "[audio] TEST-PLAY buffer0 src=" << testsrc + << " ctx=" << (void*)ctx + << " genErr=" << genErr << " bufErr=" << bufErr << " playErr=" << playErr + << " state=" << st << " playing=" << (int)(st==AL_PLAYING) << "\n" << std::flush; + } + // //---------------------------------------------------------------------- // Load the sound banks diff --git a/engine/MUNGA_L4/L4AUDRND.cpp b/engine/MUNGA_L4/L4AUDRND.cpp index 9521f87..8077135 100644 --- a/engine/MUNGA_L4/L4AUDRND.cpp +++ b/engine/MUNGA_L4/L4AUDRND.cpp @@ -1,3 +1,4 @@ +#include #include "mungal4.h" #pragma hdrstop @@ -98,6 +99,7 @@ void L4AudioRenderer::Initialize() { Check(this); + if (getenv("BT_AUDIO_LOG")) DEBUG_STREAM << "[audio] L4AudioRenderer::Initialize ENTERED\n" << std::flush; // //---------------------------------------------------------------------- @@ -133,6 +135,7 @@ void NotationFile notation_file(audio_file_name); Check(¬ation_file); + if (getenv("BT_AUDIO_LOG")) DEBUG_STREAM << "[audio] notation file opened; about to read scalars\n" << std::flush; // //---------------------------------------------------------------------- @@ -379,6 +382,23 @@ void { ALCcontext *context = alcCreateContext(device,NULL); alcMakeContextCurrent(context); + // Master volume: AL_GAIN on the listener scales EVERY source. Default 0.6 + // (the raw samples are hot); override with BT_AUDIO_VOLUME=<0.0..1.0+>. + float masterVol = 0.6f; + if (const char *v = getenv("BT_AUDIO_VOLUME")) { float f = (float)atof(v); if (f >= 0.0f) masterVol = f; } + alListenerf(AL_GAIN, masterVol); + if (getenv("BT_AUDIO_LOG")) DEBUG_STREAM << "[audio] OpenAL device OPENED + context current; master gain=" << masterVol << "\n" << std::flush; + // (task #50, AUDIO_FIDELITY F9/F11) EFX bridge: the authored lowpass + // chains + the wet-exterior/dry-cockpit reverb split, at the authentic + // AUDIO.INI global_reverb_scale read above. + { + extern bool EFX_Initialize(float global_reverb_scale); + EFX_Initialize(global_reverb_scale); + } + } + else + { + if (getenv("BT_AUDIO_LOG")) DEBUG_STREAM << "[audio] OpenAL device FAILED to open (alcOpenDevice NULL)\n" << std::flush; } // diff --git a/engine/MUNGA_L4/L4NET.CPP b/engine/MUNGA_L4/L4NET.CPP index 4294529..a6b0c34 100644 --- a/engine/MUNGA_L4/L4NET.CPP +++ b/engine/MUNGA_L4/L4NET.CPP @@ -970,8 +970,17 @@ void L4NetworkManager::HostDisconnectedMessageHandler(HostDisconnectedMessage* H // // Post a listen for a console so it can reconnect if it wants to. // - NetTransport_Get()->Close(gameListenerSocket); - gameListenerSocket = NULL; + // FIX (task #50, 2026-07-15): this used to close the GAME listener here + // (shutdown/closesocket(gameListenerSocket)) on a CONSOLE disconnect -- a + // naming bug: the comment above + the commented-out OpenConnection below + // intended to re-post a CONSOLE listen, which CreateConsoleHost() (below) + // already does. Closing the game listener on console loss needlessly + // bounced the game-accept path (harmless to already-established peer + // sockets -- which is why a live 2-node match keeps replicating through a + // console loss -- but wrong, and it would have blocked a NEW peer from + // joining after a console cycle). Removed; CreateConsoleHost re-listens. + // (BT412: the NetTransport seam close that briefly lived here is dropped + // with the bug it wrapped.) //unsigned long console_socket = OpenConnection( // NETNUB_TCP_LISTEN, // ((L4Application *)application)->GetNetworkCommonFlatAddress(), // Local port @@ -2138,6 +2147,12 @@ Logical L4NetworkManager::CheckBuffers(NetworkPacket *network_packet) { continue; } + // TCP_NODELAY on the accepted GAME socket (not the console): + // without it Nagle coalesces the inbound per-frame update + // records into bursts -> the peer is received in lurches. + BOOL gameNoDelay = TRUE; + if (setsockopt(tempSocket, IPPROTO_TCP, TCP_NODELAY, (char*)&gameNoDelay, sizeof(gameNoDelay))) + DEBUG_STREAM << "WARN: could not set TCP_NODELAY on accepted game socket; setsockopt() failed with " << WSAGetLastError() << "\n" << std::flush; } remote_host->SetNetworkSocket(tempSocket); @@ -2683,6 +2698,9 @@ SOCKET L4NetworkManager::OpenConnection( remoteEndpoint.sin_addr.S_un.S_addr = internet_address; remoteEndpoint.sin_port = htons(remote_port); + // BT412: the raw connect/retry/nonblocking/TCP_NODELAY sequence that + // lived here now lives in WinsockNetTransport::Connect (the seam) -- + // including BT411's task-#50 TCP_NODELAY latency fix, folded in there. return (SOCKET) NetTransport_Get()->Connect(&remoteEndpoint, local_port); } else if(connection_type == NETNUB_TCP_LISTEN) diff --git a/engine/MUNGA_L4/L4NETTRANSPORT.cpp b/engine/MUNGA_L4/L4NETTRANSPORT.cpp index 166d421..8e68b8c 100644 --- a/engine/MUNGA_L4/L4NETTRANSPORT.cpp +++ b/engine/MUNGA_L4/L4NETTRANSPORT.cpp @@ -164,6 +164,18 @@ namespace closesocket(sock); return InvalidConnection; } + // TCP_NODELAY (BT411 task #50, folded into the seam): disable + // Nagle so the small per-frame update records ship immediately + // instead of being coalesced. Nagle + delayed-ACK batch the + // tiny position packets into ~40-200ms bursts, so a peer moving + // at a steady speed is RECEIVED in lurches (dead-reckoned ground + // speed swinging 3x per 0.25s window). The pod net carries + // steady real-time state where latency, not throughput, matters. + BOOL no_delay = TRUE; + if (setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, + (char *) &no_delay, sizeof(no_delay))) + DEBUG_STREAM << "WARN: could not set TCP_NODELAY on active socket; setsockopt() failed with " + << WSAGetLastError() << "\n" << std::flush; return (Connection) sock; } diff --git a/engine/MUNGA_L4/L4VIDEO.cpp b/engine/MUNGA_L4/L4VIDEO.cpp index 81eb4cb..bbc9ef3 100644 --- a/engine/MUNGA_L4/L4VIDEO.cpp +++ b/engine/MUNGA_L4/L4VIDEO.cpp @@ -8577,6 +8577,37 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte const double presentMs = (double)(_rt2.QuadPart - _rt1.QuadPart) * 1000.0 / (double)_rf.QuadPart; static double sAcc = 0.0, sMaxD = 0.0, sMaxP = 0.0; static int sFrames = 0; sAcc += drawMs + presentMs; ++sFrames; + // RENDER-frame heading probe (BT_RENDHDG): count render frames that redraw + // the SAME peer heading (stale between sim updates) vs frames where it moved. + // sameHeading >> moved => the peer sim updates slower than the render draws, + // so the rotation stutters regardless of how smooth each sim step is. + if (getenv("BT_RENDHDG")) + { + extern volatile float gBTReplRenderYaw; + static float sPrevY = -999.0f, sMaxStep = 0.0f, sSumStep = 0.0f; + static int sN = 0, sBack = 0; static double sHAcc = 0.0; + const float y = gBTReplRenderYaw; + if (sPrevY > -900.0f) + { + float d = y - sPrevY; // per-RENDER-frame heading change + if (d > 3.14159f) d -= 6.28319f; // unwrap + if (d < -3.14159f) d += 6.28319f; + const float ad = (d < 0.0f) ? -d : d; + sSumStep += ad; if (ad > sMaxStep) sMaxStep = ad; + if (d > 0.0f) ++sBack; // spin is -yaw here; +step == backward + ++sN; + } + sPrevY = y; sHAcc += drawMs + presentMs; + if (sHAcc >= 1000.0 && sN > 0) + { + const float avg = sSumStep / sN; + DEBUG_STREAM << "[rendhdg] avgStep=" << avg << " maxStep=" << sMaxStep + << " max/avg=" << (avg > 1e-6f ? sMaxStep / avg : 0.0f) + << " backwardFrames=" << sBack << "/" << sN + << " (smooth: max/avg~1, backward~0)\n" << std::flush; + sMaxStep = 0.0f; sSumStep = 0.0f; sN = 0; sBack = 0; sHAcc = 0.0; + } + } if (drawMs > sMaxD) sMaxD = drawMs; if (presentMs > sMaxP) sMaxP = presentMs; if (drawMs + presentMs > 150.0) diff --git a/engine/MUNGA_L4/WTPresets.cpp b/engine/MUNGA_L4/WTPresets.cpp index 047f309..bd57f53 100644 --- a/engine/MUNGA_L4/WTPresets.cpp +++ b/engine/MUNGA_L4/WTPresets.cpp @@ -4,7 +4,7 @@ bool PRESET_isImplemented(int bank, int presetn) { PRESETINFO preset = allPresets[bank-1][presetn]; - if (preset.sampleNum <= 0 || preset.sampleNum >= 5) + if (preset.sampleNum <= 0 || preset.sampleNum > MAX_PRESET_SAMPLES) { return false; } @@ -27,15 +27,20 @@ int PRESET_getNumSamples(int bank, int preset) SAMPLEINFO PRESET_getSampleInfo(int bank, int preset, int sampleInd) { - SAMPLEINFO default; - default.chan = SampleChannel::CHANNEL_CENTER; - default.file = ""; - default.implemented = false; - default.loop = SampleLoop::LoopAtWill; - if (sampleInd < 0 || sampleInd >= allPresets[bank-1][preset].sampleNum) { - return default; + SAMPLEINFO none; + none.bufferIndex = -1; + none.implemented = false; + none.file = ""; + none.chan = SampleChannel::CHANNEL_CENTER; + none.loop = SampleLoop::LoopAtWill; + none.keyLo = 0; + none.keyHi = 127; + none.loopStart = 0; + none.loopEnd = 0; + none.releaseSec = 0.0f; + return none; } return allPresets[bank-1][preset].samples[sampleInd]; @@ -44,4 +49,4 @@ SAMPLEINFO PRESET_getSampleInfo(int bank, int preset, int sampleInd) void PRESET_setBufferIndex(int bank, int preset, int sampleInd, int index) { allPresets[bank-1][preset].samples[sampleInd].bufferIndex = index; -} \ No newline at end of file +} diff --git a/engine/lib/OpenAL32.dll b/engine/lib/OpenAL32.dll index 5cf2ff1..188e232 100644 Binary files a/engine/lib/OpenAL32.dll and b/engine/lib/OpenAL32.dll differ diff --git a/engine/lib/libsndfile-1.dll b/engine/lib/libsndfile-1.dll deleted file mode 100644 index d8ba6ba..0000000 Binary files a/engine/lib/libsndfile-1.dll and /dev/null differ diff --git a/engine/lib/libsndfile-1.lib b/engine/lib/libsndfile-1.lib deleted file mode 100644 index e812c2e..0000000 Binary files a/engine/lib/libsndfile-1.lib and /dev/null differ diff --git a/game/reconstructed/ammobin.cpp b/game/reconstructed/ammobin.cpp index 7dbe10e..0125e4f 100644 --- a/game/reconstructed/ammobin.cpp +++ b/game/reconstructed/ammobin.cpp @@ -94,7 +94,22 @@ Derivation ); Receiver::MessageHandlerSet AmmoBin::MessageHandlers; -Simulation::AttributeIndexSet AmmoBin::AttributeIndex; + +// AmmoState -> ammoAlarm (@0x194). Chained to HeatWatcher so the inherited +// SimulationState (and any parent attrs) resolve -- the old bare AttributeIndex +// had no parent, so nothing resolved on an AmmoBin. +const AmmoBin::IndexEntry + AmmoBin::AttributePointers[]= +{ + ATTRIBUTE_ENTRY(AmmoBin, AmmoState, ammoAlarm), // @0x194 (0x54 StateIndicator-compatible feed alarm) + ATTRIBUTE_ENTRY(AmmoBin, FireCountdownStarted, cookOffArmed) // F18 @0x18C -- ammo cook-off warning gate +}; + +Simulation::AttributeIndexSet AmmoBin::AttributeIndex( + ELEMENTS(AmmoBin::AttributePointers), + AmmoBin::AttributePointers, + HeatWatcher::GetAttributeIndex() +); AmmoBin::SharedData AmmoBin::DefaultData( // resolved as &DAT_005125bc diff --git a/game/reconstructed/ammobin.hpp b/game/reconstructed/ammobin.hpp index de82ff7..e37c3d7 100644 --- a/game/reconstructed/ammobin.hpp +++ b/game/reconstructed/ammobin.hpp @@ -113,6 +113,21 @@ StateCount }; + // Attribute Support -- audio binds an AudioStateWatcher to AmmoState; it + // resolves to ammoAlarm (@0x194, a 0x54 StateIndicator-compatible alarm) so + // the feed/reload/empty audio fires on the AmmoState transition. The static + // AttributeIndex (ammobin.cpp) is now chained to HeatWatcher -- previously it + // was a bare, unchained index, so even SimulationState resolved to NULL. + enum { + AmmoStateAttributeID = HeatWatcher::NextAttributeID, + FireCountdownStartedAttributeID, // F18: the ammo COOK-OFF warning (binary + // AmmoBin table @0x512600 -> +0x18C); the 4 + // authored start/stop match watchers gate the + // cook-off countdown klaxon on it + NextAttributeID + }; + static const IndexEntry AttributePointers[]; + typedef void (AmmoBin::*Performance)(Scalar time_slice); diff --git a/game/reconstructed/audiopresets.cpp b/game/reconstructed/audiopresets.cpp new file mode 100644 index 0000000..6fd6c34 --- /dev/null +++ b/game/reconstructed/audiopresets.cpp @@ -0,0 +1,631 @@ +// GENERATED by tools/sf2extract.py -- the audio soundbank table. +// FULL-ZONE extraction (AUDIO_FIDELITY.md F1/F2/F13/F14): every preset +// carries ALL its instrument zones (key-splits, layers, stereo pairs) +// with per-zone key range, loop region (frames), release seconds and +// channel; per-zone tuning + attenuation are baked into the WAVs. +// Regenerate: python tools/sf2extract.py +#include + +PRESETINFO allPresets[2][128] = {}; + +namespace { +void Z(int b, int p, const char *file, int keyLo, int keyHi, SampleLoop loop, + int loopStart, int loopEnd, float releaseSec, SampleChannel chan) +{ + PRESETINFO &pi = allPresets[b][p]; + if (pi.sampleNum >= MAX_PRESET_SAMPLES) return; + SAMPLEINFO &s = pi.samples[pi.sampleNum++]; + s.implemented = true; s.bufferIndex = -1; s.file = file; + s.keyLo = keyLo; s.keyHi = keyHi; s.loop = loop; + s.loopStart = loopStart; s.loopEnd = loopEnd; + s.releaseSec = releaseSec; s.chan = chan; +} + +struct AudioPresetInit { + AudioPresetInit() { + Z(0,0,"EnginePower01.wav",12,107,LoopAtWill,148,42365,1.546f,CHANNEL_CENTER); + Z(0,1,"EngineMotor01.wav",12,107,LoopAtWill,212,42989,1.546f,CHANNEL_CENTER); + Z(0,2,"EngineMotor02_z0.wav",12,107,LoopAtWill,324,35476,1.546f,CHANNEL_LEFT); + Z(0,2,"EngineMotor02_z1.wav",12,107,LoopAtWill,324,35476,1.546f,CHANNEL_RIGHT); + Z(0,3,"EngineDamage01_z0.wav",12,107,LoopAtWill,2,36065,1.546f,CHANNEL_LEFT); + Z(0,3,"EngineDamage01_z1.wav",12,107,LoopAtWill,2,36065,1.546f,CHANNEL_RIGHT); + Z(0,4,"EngineAccel01_z0.wav",12,107,LoopAtWill,0,19726,3.911f,CHANNEL_CENTER); + Z(0,4,"EngineAccel01_z1.wav",12,107,LoopAtWill,0,19726,2.986f,CHANNEL_CENTER); + Z(0,5,"EngineShiftFwd01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,5,"EngineShiftFwd01_z1.wav",12,107,LoopAtWill,0,24082,1.546f,CHANNEL_CENTER); + Z(0,5,"EngineShiftFwd01_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,6,"EngineShiftRev01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,6,"EngineShiftRev01_z1.wav",12,107,LoopAtWill,0,24082,1.546f,CHANNEL_CENTER); + Z(0,6,"EngineShiftRev01_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,7,"TorsoTwistInt01.wav",12,107,LoopAtWill,56,13656,1.398f,CHANNEL_CENTER); + Z(0,8,"TorsoTwistExt01.wav",12,107,LoopAtWill,56,13656,1.193f,CHANNEL_CENTER); + Z(0,9,"MyomerServo01.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,10,"LaserAMisfire01.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,11,"LaserAFire01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,11,"LaserAFire01_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,11,"LaserAFire01_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,12,"LaserAFire02_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,12,"LaserAFire02_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,12,"LaserAFire02_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,13,"LaserAFire03_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,13,"LaserAFire03_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,13,"LaserAFire03_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,14,"LaserASustain01_z0.wav",12,107,LoopAtWill,0,7473,0.000f,CHANNEL_CENTER); + Z(0,14,"LaserASustain01_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,14,"LaserASustain01_z2.wav",12,107,LoopAtWill,59,16752,0.000f,CHANNEL_CENTER); + Z(0,15,"LaserASustain02_z0.wav",12,107,LoopAtWill,0,7473,0.000f,CHANNEL_CENTER); + Z(0,15,"LaserASustain02_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,15,"LaserASustain02_z2.wav",12,107,LoopAtWill,59,16752,0.000f,CHANNEL_CENTER); + Z(0,16,"LaserASustain03_z0.wav",12,107,LoopAtWill,0,7473,0.000f,CHANNEL_CENTER); + Z(0,16,"LaserASustain03_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,16,"LaserASustain03_z2.wav",12,107,LoopAtWill,59,16752,0.000f,CHANNEL_CENTER); + Z(0,17,"LaserARelease01.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,18,"LaserARelease02.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,19,"LaserARelease03.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,20,"LaserACharge01.wav",12,107,LoopAtWill,43,32667,1.309f,CHANNEL_CENTER); + Z(0,21,"LaserACharge02.wav",12,107,LoopAtWill,43,32667,1.309f,CHANNEL_CENTER); + Z(0,22,"LaserACharge03.wav",12,107,LoopAtWill,43,32667,1.309f,CHANNEL_CENTER); + Z(0,23,"LaserACharge04.wav",12,107,LoopAtWill,43,32667,1.309f,CHANNEL_CENTER); + Z(0,24,"LaserBFire01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_LEFT); + Z(0,24,"LaserBFire01_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,24,"LaserBFire01_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_RIGHT); + Z(0,25,"LaserBFire02_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_LEFT); + Z(0,25,"LaserBFire02_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,25,"LaserBFire02_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_RIGHT); + Z(0,26,"LaserBFire03_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_LEFT); + Z(0,26,"LaserBFire03_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,26,"LaserBFire03_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_RIGHT); + Z(0,27,"LaserBSustain01_z0.wav",12,107,LoopAtWill,348,9040,0.000f,CHANNEL_CENTER); + Z(0,27,"LaserBSustain01_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,27,"LaserBSustain01_z2.wav",12,107,LoopAtWill,59,16752,0.000f,CHANNEL_CENTER); + Z(0,28,"LaserBSustain02_z0.wav",12,107,LoopAtWill,348,9040,0.000f,CHANNEL_CENTER); + Z(0,28,"LaserBSustain02_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,28,"LaserBSustain02_z2.wav",12,107,LoopAtWill,59,16752,0.000f,CHANNEL_CENTER); + Z(0,29,"LaserBSustain03_z0.wav",12,107,LoopAtWill,348,9040,0.000f,CHANNEL_CENTER); + Z(0,29,"LaserBSustain03_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,29,"LaserBSustain03_z2.wav",12,107,LoopAtWill,59,16752,0.000f,CHANNEL_CENTER); + Z(0,30,"LaserCFire01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,30,"LaserCFire01_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,30,"LaserCFire01_z2.wav",12,107,LoopAtWill,348,9040,0.000f,CHANNEL_CENTER); + Z(0,31,"LaserCFire02_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,31,"LaserCFire02_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,31,"LaserCFire02_z2.wav",12,107,LoopAtWill,348,9040,0.000f,CHANNEL_CENTER); + Z(0,32,"LaserCFire03_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,32,"LaserCFire03_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,32,"LaserCFire03_z2.wav",12,107,LoopAtWill,348,9040,0.000f,CHANNEL_CENTER); + Z(0,33,"LaserCSustain01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,33,"LaserCSustain01_z1.wav",12,107,LoopAtWill,283,11616,0.000f,CHANNEL_CENTER); + Z(0,33,"LaserCSustain01_z2.wav",12,107,LoopAtWill,59,16752,0.000f,CHANNEL_CENTER); + Z(0,34,"LaserCSustain02_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,34,"LaserCSustain02_z1.wav",12,107,LoopAtWill,283,11616,0.000f,CHANNEL_CENTER); + Z(0,34,"LaserCSustain02_z2.wav",12,107,LoopAtWill,59,16752,0.000f,CHANNEL_CENTER); + Z(0,35,"LaserCSustain03_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,35,"LaserCSustain03_z1.wav",12,107,LoopAtWill,283,11616,0.000f,CHANNEL_CENTER); + Z(0,35,"LaserCSustain03_z2.wav",12,107,LoopAtWill,59,16752,0.000f,CHANNEL_CENTER); + Z(0,36,"MissileMisfire01_z0.wav",12,59,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,36,"MissileMisfire01_z1.wav",60,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,37,"MissileLaunch01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,37,"MissileLaunch01_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,37,"MissileLaunch01_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,37,"MissileLaunch01_z3.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,38,"MissileTravel01.wav",12,107,LoopAtWill,0,38274,1.309f,CHANNEL_CENTER); + Z(0,39,"MissileLoading01.wav",12,107,LoopAtWill,27,6991,1.253f,CHANNEL_CENTER); + Z(0,40,"MissileLoaded01_z0.wav",12,59,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,40,"MissileLoaded01_z1.wav",60,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,41,"AutoCanonMisfire01_z0.wav",12,59,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,41,"AutoCanonMisfire01_z1.wav",60,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,42,"AutoCanonFire01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,42,"AutoCanonFire01_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,42,"AutoCanonFire01_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,43,"AutoCanonAction01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,43,"AutoCanonAction01_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,43,"AutoCanonAction01_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,44,"ProjectileATravel01.wav",12,107,LoopAtWill,6,26533,1.124f,CHANNEL_CENTER); + Z(0,45,"TriggerDurLoad01.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,46,"LaserASustainExt01.wav",12,107,LoopAtWill,59,16752,0.000f,CHANNEL_CENTER); + Z(0,47,"LaserASustainExt02.wav",12,107,LoopAtWill,59,16752,0.000f,CHANNEL_CENTER); + Z(0,48,"LaserASustainExt03.wav",12,107,LoopAtWill,59,16752,0.000f,CHANNEL_CENTER); + Z(0,49,"LaserBSustainExt01.wav",12,107,LoopAtWill,348,9040,0.000f,CHANNEL_CENTER); + Z(0,50,"LaserBSustainExt02.wav",12,107,LoopAtWill,348,9040,0.000f,CHANNEL_CENTER); + Z(0,51,"LaserBSustainExt03.wav",12,107,LoopAtWill,348,9040,0.000f,CHANNEL_CENTER); + Z(0,52,"LaserCSustainExt01.wav",12,107,LoopAtWill,283,11616,0.000f,CHANNEL_CENTER); + Z(0,53,"LaserCSustainExt02.wav",12,107,LoopAtWill,283,11616,0.000f,CHANNEL_CENTER); + Z(0,54,"LaserCSustainExt03.wav",12,107,LoopAtWill,283,11616,0.000f,CHANNEL_CENTER); + Z(0,55,"AutoCanonFireExt01.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,56,"LaserLoaded01_z0.wav",12,59,LoopAtWill,43,32667,1.309f,CHANNEL_CENTER); + Z(0,56,"LaserLoaded01_z1.wav",60,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,57,"EngineAccel02_z0.wav",12,107,LoopAtWill,324,35476,1.546f,CHANNEL_LEFT); + Z(0,57,"EngineAccel02_z1.wav",12,107,LoopAtWill,126,45884,1.546f,CHANNEL_RIGHT); + Z(0,58,"EngineAccel03_z0.wav",12,107,LoopAtWill,6,50367,1.546f,CHANNEL_LEFT); + Z(0,58,"EngineAccel03_z1.wav",12,107,LoopAtWill,6,50367,1.546f,CHANNEL_RIGHT); + Z(0,59,"EngineAccel04_z0.wav",12,107,LoopAtWill,424,44918,1.546f,CHANNEL_LEFT); + Z(0,59,"EngineAccel04_z1.wav",12,107,LoopAtWill,424,44918,1.546f,CHANNEL_RIGHT); + Z(0,60,"EngineAccel05_z0.wav",12,107,LoopAtWill,199,44095,1.546f,CHANNEL_LEFT); + Z(0,60,"EngineAccel05_z1.wav",12,107,LoopAtWill,199,44095,1.546f,CHANNEL_RIGHT); + Z(0,61,"EngineAccel06_z0.wav",12,107,LoopAtWill,324,35476,1.546f,CHANNEL_LEFT); + Z(0,61,"EngineAccel06_z1.wav",12,107,LoopAtWill,6,50367,1.546f,CHANNEL_CENTER); + Z(0,61,"EngineAccel06_z2.wav",12,107,LoopAtWill,324,35476,1.546f,CHANNEL_RIGHT); + Z(0,62,"MchGunFire01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,62,"MchGunFire01_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,63,"MchGunFire02_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,63,"MchGunFire02_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,64,"MchGunFire03_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,64,"MchGunFire03_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,65,"MchGunFire04_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,65,"MchGunFire04_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,66,"MchGunFire05_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,66,"MchGunFire05_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,67,"MchGunFire06_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,67,"MchGunFire06_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,68,"MchGunMisfire01.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,69,"MchGunJam01.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,70,"AutoCanonJam01_z0.wav",12,59,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,70,"AutoCanonJam01_z1.wav",60,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,71,"MissileJam01_z0.wav",12,59,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,71,"MissileJam01_z1.wav",60,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,72,"MchGunFireExt01.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,73,"ProjectileLoading01.wav",12,107,LoopAtWill,0,44198,1.253f,CHANNEL_CENTER); + Z(0,74,"IncomingAlarm01.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,75,"ProjectileLoaded01_z0.wav",12,59,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,75,"ProjectileLoaded01_z1.wav",60,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,76,"EngineAccel07_z0.wav",12,107,LoopAtWill,356,49750,0.000f,CHANNEL_LEFT); + Z(0,76,"EngineAccel07_z1.wav",12,107,LoopAtWill,199,44095,0.000f,CHANNEL_CENTER); + Z(0,76,"EngineAccel07_z2.wav",12,107,LoopAtWill,356,49750,0.000f,CHANNEL_RIGHT); + Z(0,78,"BasicBeep01.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,79,"BasicClick01.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,80,"DefaultButton01.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,81,"DuckServo01.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,82,"MapZoom01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,82,"MapZoom01_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,83,"ProgramButton01.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,84,"ViewSwitch01.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,85,"GeneratorSelect01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,85,"GeneratorSelect01_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,86,"BasicBeep02.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,87,"EjectButton01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,87,"EjectButton01_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,88,"EngButton01_z0.wav",12,107,LoopAtWill,0,5276,0.000f,CHANNEL_CENTER); + Z(0,88,"EngButton01_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,89,"CoolantPresInc02_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,89,"CoolantPresInc02_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,90,"CoolantLeak01.wav",12,107,LoopAtWill,0,38711,1.124f,CHANNEL_CENTER); + Z(0,91,"CoolantDump01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,91,"CoolantDump01_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,91,"CoolantDump01_z2.wav",12,107,LoopAtWill,18,32038,0.000f,CHANNEL_CENTER); + Z(0,92,"CoolantPresDcr02_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,92,"CoolantPresDcr02_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,93,"CoolantPresInc01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,93,"CoolantPresInc01_z1.wav",12,107,LoopAtWill,27,6991,0.000f,CHANNEL_CENTER); + Z(0,93,"CoolantPresInc01_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,94,"CoolantPresDcr01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,94,"CoolantPresDcr01_z1.wav",12,107,LoopAtWill,27,6991,0.000f,CHANNEL_CENTER); + Z(0,94,"CoolantPresDcr01_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,95,"EjectStart01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,95,"EjectStart01_z1.wav",12,107,LoopAtWill,2,18399,0.000f,CHANNEL_CENTER); + Z(0,96,"EjectEnd01.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,97,"CoolantDump02_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,97,"CoolantDump02_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,97,"CoolantDump02_z2.wav",12,107,LoopAtWill,18,32038,0.000f,CHANNEL_CENTER); + Z(0,98,"CoolantPresInc03_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,98,"CoolantPresInc03_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,98,"CoolantPresInc03_z2.wav",12,107,LoopAtWill,18,32038,0.000f,CHANNEL_CENTER); + Z(0,99,"CoolantPresDcr03_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,99,"CoolantPresDcr03_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,99,"CoolantPresDcr03_z2.wav",12,107,LoopAtWill,18,32038,0.000f,CHANNEL_CENTER); + Z(0,100,"CoolantDump03_z0.wav",12,107,LoopAtWill,0,69888,0.000f,CHANNEL_CENTER); + Z(0,100,"CoolantDump03_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,101,"CoolantDumpStart01.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,102,"CoolantDump04_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,102,"CoolantDump04_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,102,"CoolantDump04_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,103,"CoolantDump05_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,103,"CoolantDump05_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,103,"CoolantDump05_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,104,"CoolantDumpExt01.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,105,"WindLoop01.wav",12,107,LoopAtWill,2,54555,1.124f,CHANNEL_CENTER); + Z(0,106,"FootFallExt03_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,106,"FootFallExt03_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,106,"FootFallExt03_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,107,"FootFallExt04_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,107,"FootFallExt04_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,108,"LaserCSustainExt04.wav",12,107,LoopAtWill,283,11616,0.000f,CHANNEL_CENTER); + Z(0,121,"AllEnviron.wav",12,107,LoopAtWill,2,54555,0.000f,CHANNEL_CENTER); + Z(0,122,"AllCoolant_z0.wav",12,107,LoopAtWill,0,38711,0.000f,CHANNEL_CENTER); + Z(0,122,"AllCoolant_z1.wav",12,107,LoopAtWill,0,77312,0.000f,CHANNEL_CENTER); + Z(0,122,"AllCoolant_z2.wav",12,107,LoopAtWill,0,9680,0.000f,CHANNEL_CENTER); + Z(0,122,"AllCoolant_z3.wav",12,107,LoopAtWill,0,31360,0.000f,CHANNEL_CENTER); + Z(0,122,"AllCoolant_z4.wav",12,107,LoopAtWill,0,31232,0.000f,CHANNEL_CENTER); + Z(0,122,"AllCoolant_z5.wav",12,107,LoopAtWill,18,32038,0.000f,CHANNEL_CENTER); + Z(0,122,"AllCoolant_z6.wav",12,107,LoopAtWill,0,69888,0.000f,CHANNEL_CENTER); + Z(0,122,"AllCoolant_z7.wav",12,107,LoopAtWill,0,77312,0.000f,CHANNEL_CENTER); + Z(0,122,"AllCoolant_z8.wav",12,107,LoopAtWill,0,14976,0.000f,CHANNEL_CENTER); + Z(0,123,"AllButtons_z0.wav",12,107,LoopAtWill,0,8096,0.000f,CHANNEL_CENTER); + Z(0,123,"AllButtons_z1.wav",12,107,LoopAtWill,0,1952,0.000f,CHANNEL_CENTER); + Z(0,123,"AllButtons_z2.wav",12,107,LoopAtWill,0,3632,0.000f,CHANNEL_CENTER); + Z(0,123,"AllButtons_z3.wav",12,107,LoopAtWill,0,23808,0.000f,CHANNEL_CENTER); + Z(0,123,"AllButtons_z4.wav",12,107,LoopAtWill,0,4976,0.000f,CHANNEL_CENTER); + Z(0,123,"AllButtons_z5.wav",12,107,LoopAtWill,0,2720,0.000f,CHANNEL_CENTER); + Z(0,123,"AllButtons_z6.wav",12,107,LoopAtWill,0,6032,0.000f,CHANNEL_CENTER); + Z(0,123,"AllButtons_z7.wav",12,107,LoopAtWill,0,8192,0.000f,CHANNEL_CENTER); + Z(0,123,"AllButtons_z8.wav",12,107,LoopAtWill,0,36608,0.000f,CHANNEL_CENTER); + Z(0,123,"AllButtons_z9.wav",12,107,LoopAtWill,2,18399,0.000f,CHANNEL_CENTER); + Z(0,123,"AllButtons_z10.wav",12,107,LoopAtWill,0,8192,0.000f,CHANNEL_CENTER); + Z(0,123,"AllButtons_z11.wav",12,107,LoopAtWill,0,10528,0.000f,CHANNEL_CENTER); + Z(0,123,"AllButtons_z12.wav",12,107,LoopAtWill,0,3776,0.000f,CHANNEL_CENTER); + Z(0,123,"AllButtons_z13.wav",12,107,LoopAtWill,0,5856,0.000f,CHANNEL_CENTER); + Z(0,124,"AllEngine_z0.wav",12,107,LoopAtWill,356,49750,0.000f,CHANNEL_CENTER); + Z(0,124,"AllEngine_z1.wav",12,107,LoopAtWill,0,44198,0.000f,CHANNEL_CENTER); + Z(0,125,"AllAlarms.wav",12,107,LoopAtWill,0,16064,0.000f,CHANNEL_CENTER); + Z(0,126,"AllProjectile_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,126,"AllProjectile_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,126,"AllProjectile_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,126,"AllProjectile_z3.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,126,"AllProjectile_z4.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,126,"AllProjectile_z5.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,126,"AllProjectile_z6.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,126,"AllProjectile_z7.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,126,"AllProjectile_z8.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(0,127,"AllAcceleration_z0.wav",12,107,LoopAtWill,126,45884,0.000f,CHANNEL_CENTER); + Z(0,127,"AllAcceleration_z1.wav",12,107,LoopAtWill,6,50367,0.000f,CHANNEL_CENTER); + Z(0,127,"AllAcceleration_z2.wav",12,107,LoopAtWill,424,44918,0.000f,CHANNEL_CENTER); + Z(0,127,"AllAcceleration_z3.wav",12,107,LoopAtWill,199,44095,0.000f,CHANNEL_CENTER); + Z(1,0,"Collision01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,0,"Collision01_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,0,"Collision01_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,1,"Collision02_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,1,"Collision02_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,1,"Collision02_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,2,"Collision03_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,2,"Collision03_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,2,"Collision03_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,3,"Collision04_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,3,"Collision04_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,3,"Collision04_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,4,"Collision05_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,4,"Collision05_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,4,"Collision05_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,5,"Collision06_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,5,"Collision06_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,5,"Collision06_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,6,"Collision07_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,6,"Collision07_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,6,"Collision07_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,7,"Collision08_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,7,"Collision08_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,7,"Collision08_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,8,"Collision09_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,8,"Collision09_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,8,"Collision09_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,9,"Collision10_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,9,"Collision10_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,9,"Collision10_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,10,"Collision11_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,10,"Collision11_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,10,"Collision11_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,11,"CollisionExt01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,11,"CollisionExt01_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,12,"CollisionExt02.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,13,"CollisionExt03_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,13,"CollisionExt03_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,14,"CollisionExt04_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,14,"CollisionExt04_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,15,"CollisionExt05_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,15,"CollisionExt05_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,16,"CollisionExt06_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,16,"CollisionExt06_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,17,"CollisionExt07_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,17,"CollisionExt07_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,18,"CollisionExt08_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,18,"CollisionExt08_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,19,"CollisionExt09_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,19,"CollisionExt09_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,20,"LaserExplosion01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,20,"LaserExplosion01_z1.wav",12,107,LoopAtWill,336,27176,0.000f,CHANNEL_CENTER); + Z(1,20,"LaserExplosion01_z2.wav",12,107,LoopAtWill,0,77812,0.000f,CHANNEL_CENTER); + Z(1,21,"LaserExplosion02_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,21,"LaserExplosion02_z1.wav",12,107,LoopAtWill,336,27176,0.000f,CHANNEL_CENTER); + Z(1,21,"LaserExplosion02_z2.wav",12,107,LoopAtWill,0,89715,0.000f,CHANNEL_CENTER); + Z(1,22,"LaserExplosion03_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,22,"LaserExplosion03_z1.wav",12,107,LoopAtWill,336,27176,0.000f,CHANNEL_CENTER); + Z(1,22,"LaserExplosion03_z2.wav",12,107,LoopAtWill,0,89802,0.000f,CHANNEL_CENTER); + Z(1,23,"LaserExplosion04_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,23,"LaserExplosion04_z1.wav",12,107,LoopAtWill,336,27176,0.000f,CHANNEL_CENTER); + Z(1,23,"LaserExplosion04_z2.wav",12,107,LoopAtWill,0,77812,0.000f,CHANNEL_CENTER); + Z(1,24,"LaserExplosion05_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,24,"LaserExplosion05_z1.wav",12,107,LoopAtWill,336,27176,0.000f,CHANNEL_CENTER); + Z(1,24,"LaserExplosion05_z2.wav",12,107,LoopAtWill,0,89715,0.000f,CHANNEL_CENTER); + Z(1,25,"LaserExplosion06_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,25,"LaserExplosion06_z1.wav",12,107,LoopAtWill,336,27176,0.000f,CHANNEL_CENTER); + Z(1,25,"LaserExplosion06_z2.wav",12,107,LoopAtWill,0,89802,0.000f,CHANNEL_CENTER); + Z(1,26,"BigExplosion01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,26,"BigExplosion01_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,26,"BigExplosion01_z2.wav",12,107,LoopAtWill,0,77812,0.000f,CHANNEL_CENTER); + Z(1,26,"BigExplosion01_z3.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,27,"BigExplosion02_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,27,"BigExplosion02_z1.wav",12,107,LoopAtWill,0,89802,0.000f,CHANNEL_CENTER); + Z(1,27,"BigExplosion02_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,27,"BigExplosion02_z3.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,28,"BigExplosion03_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,28,"BigExplosion03_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,28,"BigExplosion03_z2.wav",12,107,LoopAtWill,0,89715,0.000f,CHANNEL_CENTER); + Z(1,28,"BigExplosion03_z3.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,29,"BigExplosion04_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,29,"BigExplosion04_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,29,"BigExplosion04_z2.wav",12,107,LoopAtWill,0,89802,0.000f,CHANNEL_CENTER); + Z(1,29,"BigExplosion04_z3.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,30,"BigExplosion05_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,30,"BigExplosion05_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,30,"BigExplosion05_z2.wav",12,107,LoopAtWill,0,89802,0.000f,CHANNEL_CENTER); + Z(1,30,"BigExplosion05_z3.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,31,"MedExplosion01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,31,"MedExplosion01_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,31,"MedExplosion01_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,32,"MedExplosion02_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,32,"MedExplosion02_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,32,"MedExplosion02_z2.wav",12,107,LoopAtWill,0,61312,0.000f,CHANNEL_CENTER); + Z(1,33,"MedExplosion03_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,33,"MedExplosion03_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,33,"MedExplosion03_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,34,"MedExplosion04_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,34,"MedExplosion04_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,34,"MedExplosion04_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,35,"MedExplosion05_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,35,"MedExplosion05_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,35,"MedExplosion05_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,36,"TorsoTwistStop01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,36,"TorsoTwistStop01_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,37,"FootFallExt01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,37,"FootFallExt01_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,38,"FootFallExt02.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,39,"FootFallInt01.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,40,"CollisionExt01LOD2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,41,"CollisionExt02LOD2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,42,"CollisionExt03LOD2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,43,"CollisionExt04LOD2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,44,"CollisionExt05LOD2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,45,"CollisionExt06LOD2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,46,"CollisionExt07LOD2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,47,"LaserExp01LOD2_z0.wav",12,107,LoopAtWill,336,27176,0.000f,CHANNEL_CENTER); + Z(1,47,"LaserExp01LOD2_z1.wav",12,107,LoopAtWill,0,77812,0.000f,CHANNEL_CENTER); + Z(1,48,"LaserExp02LOD2_z0.wav",12,107,LoopAtWill,336,27176,0.000f,CHANNEL_CENTER); + Z(1,48,"LaserExp02LOD2_z1.wav",12,107,LoopAtWill,0,89715,0.000f,CHANNEL_CENTER); + Z(1,49,"LaserExp03LOD2_z0.wav",12,107,LoopAtWill,336,27176,0.000f,CHANNEL_CENTER); + Z(1,49,"LaserExp03LOD2_z1.wav",12,107,LoopAtWill,0,89802,0.000f,CHANNEL_CENTER); + Z(1,50,"LaserExp04LOD2_z0.wav",12,107,LoopAtWill,336,27176,0.000f,CHANNEL_CENTER); + Z(1,50,"LaserExp04LOD2_z1.wav",12,107,LoopAtWill,0,77812,0.000f,CHANNEL_CENTER); + Z(1,51,"LaserExp05LOD2_z0.wav",12,107,LoopAtWill,336,27176,0.000f,CHANNEL_CENTER); + Z(1,51,"LaserExp05LOD2_z1.wav",12,107,LoopAtWill,0,89715,0.000f,CHANNEL_CENTER); + Z(1,52,"LaserExp06LOD2_z0.wav",12,107,LoopAtWill,336,27176,0.000f,CHANNEL_CENTER); + Z(1,52,"LaserExp06LOD2_z1.wav",12,107,LoopAtWill,0,89802,0.000f,CHANNEL_CENTER); + Z(1,53,"LaserExpInt01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,53,"LaserExpInt01_z1.wav",12,107,LoopAtWill,336,27176,0.000f,CHANNEL_CENTER); + Z(1,53,"LaserExpInt01_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,54,"LaserExpInt02_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,54,"LaserExpInt02_z1.wav",12,107,LoopAtWill,336,27176,0.000f,CHANNEL_CENTER); + Z(1,54,"LaserExpInt02_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,55,"LaserExpInt03_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,55,"LaserExpInt03_z1.wav",12,107,LoopAtWill,336,27176,0.000f,CHANNEL_CENTER); + Z(1,55,"LaserExpInt03_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,56,"LaserExpInt04_z0.wav",12,107,LoopAtWill,336,27176,0.000f,CHANNEL_CENTER); + Z(1,56,"LaserExpInt04_z1.wav",12,107,LoopAtWill,0,77812,0.000f,CHANNEL_CENTER); + Z(1,56,"LaserExpInt04_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,57,"LaserExpInt05_z0.wav",12,107,LoopAtWill,336,27176,0.000f,CHANNEL_CENTER); + Z(1,57,"LaserExpInt05_z1.wav",12,107,LoopAtWill,0,89715,0.000f,CHANNEL_CENTER); + Z(1,57,"LaserExpInt05_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,58,"LaserExpInt06_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,58,"LaserExpInt06_z1.wav",12,107,LoopAtWill,336,27176,0.000f,CHANNEL_CENTER); + Z(1,58,"LaserExpInt06_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,59,"DestroyedInt01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,59,"DestroyedInt01_z1.wav",12,107,LoopAtWill,194,52869,0.000f,CHANNEL_CENTER); + Z(1,60,"DestroyedInt02_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_LEFT); + Z(1,60,"DestroyedInt02_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_RIGHT); + Z(1,61,"DestroyedInt03_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,61,"DestroyedInt03_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,62,"DestroyedInt04.wav",12,107,LoopAtWill,67,29433,0.000f,CHANNEL_CENTER); + Z(1,63,"DestroyedInt05.wav",12,107,LoopAtWill,194,52869,0.000f,CHANNEL_CENTER); + Z(1,64,"DestroyedInt06_z0.wav",12,60,LoopAtWill,194,52869,0.000f,CHANNEL_CENTER); + Z(1,64,"DestroyedInt06_z1.wav",12,60,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,64,"DestroyedInt06_z2.wav",62,84,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,65,"MchGunExp01.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,66,"MchGunExp02.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,67,"MchGunExp03.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,68,"MchGunExp04.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,69,"MchGunExpCraft01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,69,"MchGunExpCraft01_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,70,"MchGunExpCraft02_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,70,"MchGunExpCraft02_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,71,"MchGunExpCraft03_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,71,"MchGunExpCraft03_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,72,"MchGunExpCraft04_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,72,"MchGunExpCraft04_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,73,"BigExplosion01LOD2_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,73,"BigExplosion01LOD2_z1.wav",12,107,LoopAtWill,0,77812,0.000f,CHANNEL_CENTER); + Z(1,74,"BigExplosion02LOD2_z0.wav",12,107,LoopAtWill,0,89802,0.000f,CHANNEL_CENTER); + Z(1,74,"BigExplosion02LOD2_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,75,"BigExplosion03LOD2_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,75,"BigExplosion03LOD2_z1.wav",12,107,LoopAtWill,0,89715,0.000f,CHANNEL_CENTER); + Z(1,76,"BigExplosion04LOD2_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,76,"BigExplosion04LOD2_z1.wav",12,107,LoopAtWill,0,89802,0.000f,CHANNEL_CENTER); + Z(1,77,"BigExplosion05LOD2_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,77,"BigExplosion05LOD2_z1.wav",12,107,LoopAtWill,0,89802,0.000f,CHANNEL_CENTER); + Z(1,78,"MedExplosion01LOD2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,79,"MedExplosion02LOD2.wav",12,107,LoopAtWill,0,61312,0.000f,CHANNEL_CENTER); + Z(1,80,"MedExplosion03LOD2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,81,"MedExplosion04LOD2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,82,"MedExplosion05LOD2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,83,"MechExplosion01_z0.wav",12,36,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,83,"MechExplosion01_z1.wav",12,36,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,83,"MechExplosion01_z2.wav",12,36,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,83,"MechExplosion01_z3.wav",12,36,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,83,"MechExplosion01_z4.wav",38,60,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,83,"MechExplosion01_z5.wav",38,60,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,83,"MechExplosion01_z6.wav",38,60,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,83,"MechExplosion01_z7.wav",38,60,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,83,"MechExplosion01_z8.wav",62,84,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,83,"MechExplosion01_z9.wav",62,84,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,83,"MechExplosion01_z10.wav",62,84,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,83,"MechExplosion01_z11.wav",62,84,LoopAtWill,0,89715,0.000f,CHANNEL_CENTER); + Z(1,83,"MechExplosion01_z12.wav",86,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,83,"MechExplosion01_z13.wav",86,107,LoopAtWill,23517,23865,0.000f,CHANNEL_CENTER); + Z(1,83,"MechExplosion01_z14.wav",86,107,LoopAtWill,0,47104,0.000f,CHANNEL_CENTER); + Z(1,83,"MechExplosion01_z15.wav",86,107,LoopAtWill,0,89802,0.000f,CHANNEL_CENTER); + Z(1,84,"MedExpInt01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,84,"MedExpInt01_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,84,"MedExpInt01_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,85,"MedExpInt02_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,85,"MedExpInt02_z1.wav",12,107,LoopAtWill,0,61312,0.000f,CHANNEL_CENTER); + Z(1,85,"MedExpInt02_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,86,"MedExpInt03_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,86,"MedExpInt03_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,86,"MedExpInt03_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,87,"MedExpInt04_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,87,"MedExpInt04_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,87,"MedExpInt04_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,88,"MedExpInt05_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,88,"MedExpInt05_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,88,"MedExpInt05_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,89,"Damage01.wav",12,107,LoopAtWill,67,29433,1.124f,CHANNEL_CENTER); + Z(1,90,"Damage03.wav",12,107,LoopAtWill,0,77812,1.124f,CHANNEL_CENTER); + Z(1,91,"Damage02.wav",12,107,LoopAtWill,0,89715,1.124f,CHANNEL_CENTER); + Z(1,92,"BigExpInt01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,92,"BigExpInt01_z1.wav",12,107,LoopAtWill,0,77812,0.000f,CHANNEL_CENTER); + Z(1,92,"BigExpInt01_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,92,"BigExpInt01_z3.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,93,"BigExpInt02_z0.wav",12,107,LoopAtWill,0,89802,0.000f,CHANNEL_CENTER); + Z(1,93,"BigExpInt02_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,93,"BigExpInt02_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,93,"BigExpInt02_z3.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,94,"BigExpInt03_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,94,"BigExpInt03_z1.wav",12,107,LoopAtWill,0,89715,0.000f,CHANNEL_CENTER); + Z(1,94,"BigExpInt03_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,94,"BigExpInt03_z3.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,95,"BigExpInt04_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,95,"BigExpInt04_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,95,"BigExpInt04_z2.wav",12,107,LoopAtWill,0,89802,0.000f,CHANNEL_CENTER); + Z(1,95,"BigExpInt04_z3.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,96,"BigExpInt05_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,96,"BigExpInt05_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,96,"BigExpInt05_z2.wav",12,107,LoopAtWill,0,89802,0.000f,CHANNEL_CENTER); + Z(1,96,"BigExpInt05_z3.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,97,"MechFireLoop01_z0.wav",12,59,LoopAtWill,406,47492,1.124f,CHANNEL_CENTER); + Z(1,97,"MechFireLoop01_z1.wav",60,107,LoopAtWill,406,47492,1.124f,CHANNEL_CENTER); + Z(1,98,"ElectricDamage01.wav",12,107,LoopAtWill,40,21948,0.000f,CHANNEL_CENTER); + Z(1,99,"PPCExplosion01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,99,"PPCExplosion01_z1.wav",12,107,LoopAtWill,336,27176,0.000f,CHANNEL_CENTER); + Z(1,99,"PPCExplosion01_z2.wav",12,107,LoopAtWill,40,21948,0.000f,CHANNEL_CENTER); + Z(1,100,"PPCExplosion02_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,100,"PPCExplosion02_z1.wav",12,107,LoopAtWill,336,27176,0.000f,CHANNEL_CENTER); + Z(1,100,"PPCExplosion02_z2.wav",12,107,LoopAtWill,40,21948,0.000f,CHANNEL_CENTER); + Z(1,101,"PPCExplosion03_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,101,"PPCExplosion03_z1.wav",12,107,LoopAtWill,336,27176,0.000f,CHANNEL_CENTER); + Z(1,101,"PPCExplosion03_z2.wav",12,107,LoopAtWill,40,21948,0.000f,CHANNEL_CENTER); + Z(1,102,"PPCExp01LOD2_z0.wav",12,107,LoopAtWill,336,27176,0.000f,CHANNEL_CENTER); + Z(1,102,"PPCExp01LOD2_z1.wav",12,107,LoopAtWill,40,21948,0.000f,CHANNEL_CENTER); + Z(1,103,"PPCExp02LOD2_z0.wav",12,107,LoopAtWill,336,27176,0.000f,CHANNEL_CENTER); + Z(1,103,"PPCExp02LOD2_z1.wav",12,107,LoopAtWill,40,21948,0.000f,CHANNEL_CENTER); + Z(1,104,"PPCExp03LOD2_z0.wav",12,107,LoopAtWill,336,27176,0.000f,CHANNEL_CENTER); + Z(1,104,"PPCExp03LOD2_z1.wav",12,107,LoopAtWill,40,21948,0.000f,CHANNEL_CENTER); + Z(1,105,"PPCExpInt01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,105,"PPCExpInt01_z1.wav",12,107,LoopAtWill,40,21948,0.000f,CHANNEL_CENTER); + Z(1,105,"PPCExpInt01_z2.wav",12,107,LoopAtWill,336,27176,0.000f,CHANNEL_CENTER); + Z(1,106,"PPCExpInt02_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,106,"PPCExpInt02_z1.wav",12,107,LoopAtWill,40,21948,0.000f,CHANNEL_CENTER); + Z(1,106,"PPCExpInt02_z2.wav",12,107,LoopAtWill,336,27176,0.000f,CHANNEL_CENTER); + Z(1,107,"PPCExpInt03_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,107,"PPCExpInt03_z1.wav",12,107,LoopAtWill,40,21948,0.000f,CHANNEL_CENTER); + Z(1,107,"PPCExpInt03_z2.wav",12,107,LoopAtWill,336,27176,0.000f,CHANNEL_CENTER); + Z(1,108,"SmallExplosion01.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,109,"SmallExplosion02.wav",12,107,LoopAtWill,0,61312,0.000f,CHANNEL_CENTER); + Z(1,110,"SmallExplosion03.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,111,"SmallExplosion04.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,112,"SmallFireLoop01.wav",12,107,LoopAtWill,406,47492,1.124f,CHANNEL_CENTER); + Z(1,113,"Warnings01_z0.wav",16,17,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,113,"Warnings01_z1.wav",19,21,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,113,"Warnings01_z2.wav",23,24,LoopAtWill,12,7230,0.000f,CHANNEL_CENTER); + Z(1,113,"Warnings01_z3.wav",26,28,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,113,"Warnings01_z4.wav",29,31,LoopAtWill,0,3465,0.000f,CHANNEL_CENTER); + Z(1,113,"Warnings01_z5.wav",33,35,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,113,"Warnings01_z6.wav",36,38,LoopAtWill,15,11068,0.000f,CHANNEL_CENTER); + Z(1,113,"Warnings01_z7.wav",40,41,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,114,"Translocate01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_LEFT); + Z(1,114,"Translocate01_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_RIGHT); + Z(1,115,"Death01_z0.wav",12,36,ForceStatic,0,0,0.000f,CHANNEL_LEFT); + Z(1,115,"Death01_z1.wav",12,36,ForceStatic,0,0,0.000f,CHANNEL_RIGHT); + Z(1,115,"Death01_z2.wav",12,36,ForceStatic,0,0,0.000f,CHANNEL_LEFT); + Z(1,115,"Death01_z3.wav",12,36,ForceStatic,0,0,0.000f,CHANNEL_RIGHT); + Z(1,115,"Death01_z4.wav",38,60,ForceStatic,0,0,0.000f,CHANNEL_LEFT); + Z(1,115,"Death01_z5.wav",38,60,ForceStatic,0,0,0.000f,CHANNEL_RIGHT); + Z(1,115,"Death01_z6.wav",38,60,ForceStatic,0,0,0.000f,CHANNEL_LEFT); + Z(1,115,"Death01_z7.wav",38,60,ForceStatic,0,0,0.000f,CHANNEL_RIGHT); + Z(1,115,"Death01_z8.wav",62,84,ForceStatic,0,0,0.000f,CHANNEL_LEFT); + Z(1,115,"Death01_z9.wav",62,84,ForceStatic,0,0,0.000f,CHANNEL_RIGHT); + Z(1,115,"Death01_z10.wav",62,84,ForceStatic,0,0,0.000f,CHANNEL_LEFT); + Z(1,115,"Death01_z11.wav",62,84,LoopAtWill,0,89715,0.000f,CHANNEL_RIGHT); + Z(1,115,"Death01_z12.wav",86,107,ForceStatic,0,0,0.000f,CHANNEL_LEFT); + Z(1,115,"Death01_z13.wav",86,107,ForceStatic,0,0,0.000f,CHANNEL_RIGHT); + Z(1,115,"Death01_z14.wav",86,107,LoopAtWill,288,13743,0.000f,CHANNEL_LEFT); + Z(1,115,"Death01_z15.wav",86,107,LoopAtWill,288,13743,0.000f,CHANNEL_RIGHT); + Z(1,116,"EngineWindUp01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_LEFT); + Z(1,116,"EngineWindUp01_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_RIGHT); + Z(1,117,"Crunch01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,117,"Crunch01_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,117,"Crunch01_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,118,"Crunch01LOD2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,119,"AuxExplosion01_z0.wav",12,36,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,119,"AuxExplosion01_z1.wav",12,36,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,119,"AuxExplosion01_z2.wav",38,60,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,119,"AuxExplosion01_z3.wav",38,60,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,119,"AuxExplosion01_z4.wav",62,84,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,119,"AuxExplosion01_z5.wav",62,84,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,119,"AuxExplosion01_z6.wav",86,107,LoopAtWill,23517,23865,0.000f,CHANNEL_CENTER); + Z(1,119,"AuxExplosion01_z7.wav",86,107,LoopAtWill,0,47104,0.000f,CHANNEL_CENTER); + Z(1,120,"AuxExplosion01LOD2_z0.wav",12,36,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,120,"AuxExplosion01LOD2_z1.wav",38,60,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,120,"AuxExplosion01LOD2_z2.wav",62,84,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,120,"AuxExplosion01LOD2_z3.wav",86,107,LoopAtWill,0,47104,0.000f,CHANNEL_CENTER); + Z(1,123,"AllWarning_z0.wav",12,107,LoopAtWill,0,12128,0.000f,CHANNEL_CENTER); + Z(1,123,"AllWarning_z1.wav",12,107,LoopAtWill,15,11068,0.000f,CHANNEL_CENTER); + Z(1,123,"AllWarning_z2.wav",12,107,LoopAtWill,12,7230,0.000f,CHANNEL_CENTER); + Z(1,123,"AllWarning_z3.wav",12,107,LoopAtWill,0,11376,0.000f,CHANNEL_CENTER); + Z(1,123,"AllWarning_z4.wav",12,107,LoopAtWill,0,13488,0.000f,CHANNEL_CENTER); + Z(1,123,"AllWarning_z5.wav",12,107,LoopAtWill,0,24176,0.000f,CHANNEL_CENTER); + Z(1,123,"AllWarning_z6.wav",12,107,LoopAtWill,0,5824,0.000f,CHANNEL_CENTER); + Z(1,124,"AllDamage.wav",12,107,LoopAtWill,40,21948,0.000f,CHANNEL_CENTER); + Z(1,125,"AllExplosion_z0.wav",12,107,LoopAtWill,0,77812,0.000f,CHANNEL_CENTER); + Z(1,125,"AllExplosion_z1.wav",12,107,LoopAtWill,0,89715,0.000f,CHANNEL_CENTER); + Z(1,125,"AllExplosion_z2.wav",12,107,LoopAtWill,0,89802,0.000f,CHANNEL_CENTER); + Z(1,125,"AllExplosion_z3.wav",12,107,LoopAtWill,0,39296,0.000f,CHANNEL_CENTER); + Z(1,125,"AllExplosion_z4.wav",12,107,LoopAtWill,0,61312,0.000f,CHANNEL_CENTER); + Z(1,125,"AllExplosion_z5.wav",12,107,LoopAtWill,0,33088,0.000f,CHANNEL_CENTER); + Z(1,125,"AllExplosion_z6.wav",12,107,LoopAtWill,0,47104,0.000f,CHANNEL_CENTER); + Z(1,125,"AllExplosion_z7.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,125,"AllExplosion_z8.wav",12,107,LoopAtWill,0,40192,0.000f,CHANNEL_CENTER); + Z(1,125,"AllExplosion_z9.wav",12,107,LoopAtWill,0,30207,0.000f,CHANNEL_CENTER); + Z(1,125,"AllExplosion_z10.wav",12,107,LoopAtWill,0,27392,0.000f,CHANNEL_CENTER); + Z(1,125,"AllExplosion_z11.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,125,"AllExplosion_z12.wav",12,107,LoopAtWill,0,26816,0.000f,CHANNEL_CENTER); + Z(1,125,"AllExplosion_z13.wav",12,107,LoopAtWill,67,29433,0.000f,CHANNEL_CENTER); + Z(1,125,"AllExplosion_z14.wav",12,107,LoopAtWill,406,47492,0.000f,CHANNEL_CENTER); + Z(1,125,"AllExplosion_z15.wav",12,107,LoopAtWill,194,52869,0.000f,CHANNEL_CENTER); + Z(1,125,"AllExplosion_z16.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,125,"AllExplosion_z17.wav",12,107,LoopAtWill,0,11072,0.000f,CHANNEL_CENTER); + Z(1,125,"AllExplosion_z18.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,125,"AllExplosion_z19.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,125,"AllExplosion_z20.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,125,"AllExplosion_z21.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,125,"AllExplosion_z22.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,125,"AllExplosion_z23.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,125,"AllExplosion_z24.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,126,"TorsoStop01_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,126,"TorsoStop01_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,127,"AllHits_z0.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,127,"AllHits_z1.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,127,"AllHits_z2.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,127,"AllHits_z3.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,127,"AllHits_z4.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,127,"AllHits_z5.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,127,"AllHits_z6.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + Z(1,127,"AllHits_z7.wav",12,107,ForceStatic,0,0,0.000f,CHANNEL_CENTER); + } +} s_audioPresetInit; +} diff --git a/game/reconstructed/btl4app.cpp b/game/reconstructed/btl4app.cpp index 88efd1a..2f4a5c2 100644 --- a/game/reconstructed/btl4app.cpp +++ b/game/reconstructed/btl4app.cpp @@ -266,22 +266,35 @@ BTL4Application::SharedData char *front_card = getenv(AWE_FRONT_ENV_VAR); // FUN_004dee74 char *rear_card = getenv(AWE_REAR_ENV_VAR); - if (front_card == NULL || rear_card == NULL) + // AUTHENTIC 1995 gate: the pod carried TWO AWE32 cards (front/rear) and + // audio was OFF unless both AWE_FRONT/AWE_REAR were set. Those cards don't + // exist on modern hardware, so this check silenced audio on every machine + // (root cause of "no sound" -- the renderer was never created). + // MODERNIZED (task #50, 2026-07-15): default audio ON through the WinTesla + // OpenAL renderer (the OS default output device). BT_NO_AUDIO=1 restores + // the silent/no-renderer behavior; the AWE vars still force it on too. + (void)front_card; (void)rear_card; + if (getenv("BT_NO_AUDIO")) { return NULL; } // - // FUN_0044e19c read the sample rate as a float out of the division - // parameter block (@+0x10). In WinTesla that block is the public - // L4Application::divisionParameters char*; fall back to 1000.0f (the - // RP default) when it is absent. Second arg is the mission-review - // flag (the 1995 binary hard-coded it off; DAT_004fd550 == 0). + // AUDIO CLOCK RATE (AUDIO_FIDELITY.md KB correction): FUN_0044e19c is + // ApplicationManager::GetFrameRate() -- the ORIGINAL audio "frames" were + // system CLOCK TICKS (the DOS 18.2/28 Hz timer), not a sample rate. The + // old port read a float from divisionParameters+0x10 (a raw 1995-layout + // offset = garbage, -1.6e14 measured). The Windows equivalent of the + // original semantic is the system tick rate: AudioHead::Execute stamps + // audioFrameCount from SystemClock ticks, so calibrating the renderer to + // the same tick rate keeps every AudioTime conversion unit-consistent. // - RendererRate sample_rate = - divisionParameters - ? (RendererRate)*reinterpret_cast(divisionParameters + 0x10) - : (RendererRate)1000.0f; + long ticks_per_second = SystemClock::GetTicksPerSecond(); + if (ticks_per_second <= 0) ticks_per_second = 1000; // GetTickCount ms (static not yet measured) + RendererRate sample_rate = (RendererRate)ticks_per_second; + if (getenv("BT_AUDIO_LOG")) + DEBUG_STREAM << "[audio] renderer calibration rate=" << sample_rate + << " (SystemClock ticks/sec)" << "\n" << std::flush; return new BTL4AudioRenderer( diff --git a/game/reconstructed/btl4gau2.cpp b/game/reconstructed/btl4gau2.cpp index 9fb51f1..ace13f7 100644 --- a/game/reconstructed/btl4gau2.cpp +++ b/game/reconstructed/btl4gau2.cpp @@ -1411,6 +1411,21 @@ void SubsystemCluster::Execute() failedState = (BTSubsystemDamageLevelOf(subsystem) >= 1.0f); } + if (getenv("BT_TEMPBAR_LOG")) + { + Scalar *t = (Scalar *)AttributePointerOf(subsystem, "CurrentTemperature"); + Scalar *dg = (Scalar *)AttributePointerOf(subsystem, "DegradationTemperature"); + Scalar *fl = (Scalar *)AttributePointerOf(subsystem, "FailureTemperature"); + static int s_tbN = 0; + if (t != 0 && ((++s_tbN) % 30) == 0) + DEBUG_STREAM << "[tempbar] cluster EXECUTING " + << (subsystem ? ((Subsystem *)subsystem)->GetName() : "?") + << " T=" << *t + << " degT=" << (dg ? *dg : -1.0f) + << " failT=" << (fl ? *fl : -1.0f) + << " drawState=" << GetDrawState() << "\n" << std::flush; + } + int state = GetDrawState(); if (state != previousDrawState) { diff --git a/game/reconstructed/btl4mppr.cpp b/game/reconstructed/btl4mppr.cpp index 66baf62..85c9051 100644 --- a/game/reconstructed/btl4mppr.cpp +++ b/game/reconstructed/btl4mppr.cpp @@ -244,11 +244,31 @@ static void //############################################################################# // Shared Data Support // +// TargetRangeExponent -> the live @0x1a4 zoom member (AUDIO_FIDELITY.md F20): +// the authored AudioScalarDeltaTrigger blips per zoom step; it bound NULL until +// this publication. Chained to MechControlsMapper's dense index. +const L4MechControlsMapper::IndexEntry + L4MechControlsMapper::AttributePointers[]= +{ + ATTRIBUTE_ENTRY(L4MechControlsMapper, TargetRangeExponent, targetRangeExponent) // @0x1a4 +}; + +L4MechControlsMapper::AttributeIndexSet& + L4MechControlsMapper::GetAttributeIndex() +{ + static L4MechControlsMapper::AttributeIndexSet attributeIndex( + ELEMENTS(L4MechControlsMapper::AttributePointers), + L4MechControlsMapper::AttributePointers, + MechControlsMapper::GetAttributeIndex() + ); + return attributeIndex; +} + L4MechControlsMapper::SharedData L4MechControlsMapper::DefaultData( L4MechControlsMapper::GetClassDerivations(), L4MechControlsMapper::GetMessageHandlers(), - L4MechControlsMapper::GetAttributeIndex(), // inherited from MechControlsMapper + L4MechControlsMapper::GetAttributeIndex(), // own table -> chained to MechControlsMapper L4MechControlsMapper::StateCount ); @@ -349,7 +369,12 @@ L4MechControlsMapper::MessageHandlerSet& // // (1) Full-throttle detent. // - if (Abs(throttlePosition - 1.0f) <= 0.05f) // _DAT_004d1ac0 / _DAT_004d1ac4 + // NB: the Abs() macro (STYLE.H:118) is unparenthesized -- Abs(throttlePosition + // - 1.0f) mis-expands to -(throttlePosition + 1.0f) on the false branch (always, + // since throttle <= 1), which is <= 0.05 UNCONDITIONALLY -> the detent used to + // snap throttle to full every frame. Diff into a temp so the macro sees one token. + const Scalar _thrOff = throttlePosition - 1.0f; + if (((_thrOff < 0.0f) ? -_thrOff : _thrOff) <= 0.05f) // _DAT_004d1ac0 / _DAT_004d1ac4 { throttlePosition = 1.0f; // @0x11c } diff --git a/game/reconstructed/btl4mppr.hpp b/game/reconstructed/btl4mppr.hpp index ebf81b4..e6a15cf 100644 --- a/game/reconstructed/btl4mppr.hpp +++ b/game/reconstructed/btl4mppr.hpp @@ -159,6 +159,20 @@ ); ~L4MechControlsMapper(); // @004d1814 + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Attribute Support (AUDIO_FIDELITY.md F20): the authored zoom-blip + // AudioScalarDeltaTrigger binds ControlsMapper/TargetRangeExponent -- the + // live @0x1a4 member that slews every frame. Chained to the parent index; + // Thrustmaster/RIO inherit this accessor. + // + public: + enum { + TargetRangeExponentAttributeID = MechControlsMapper::NextAttributeID, + L4NextAttributeID + }; + static const IndexEntry AttributePointers[]; + static AttributeIndexSet& GetAttributeIndex(); + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Protected data (object grows to >= 0x1bc) // diff --git a/game/reconstructed/btl4rdr.cpp b/game/reconstructed/btl4rdr.cpp index f6d4faf..4d2c360 100644 --- a/game/reconstructed/btl4rdr.cpp +++ b/game/reconstructed/btl4rdr.cpp @@ -697,9 +697,16 @@ void // Top-down: keep heading only. Extract the yaw, build a // yaw-only quaternion, SET the rotation block from it. // - EulerAngles euler; - euler = *currentAngularPointer; // FUN_0040954c - Scalar heading = euler.yaw * HeadingHalf; // * 0.5f + YawPitchRoll euler; // FIX: YawPitchRoll (yaw-first), + euler = *currentAngularPointer; // not EulerAngles -- the latter folds + Scalar heading = (Scalar)euler.yaw * HeadingHalf; // at +/-pi under yaw. * 0.5f half-angle + if (getenv("BT_RADAR_LOG")) { + EulerAngles oldE; oldE = *currentAngularPointer; // old buggy path, A/B + static int s_rv = 0; + if ((s_rv++ % 20) == 0) + DEBUG_STREAM << "[radar-view] euler.yaw=" << (Scalar)oldE.yaw + << " ypr.yaw=" << (Scalar)euler.yaw << " (rad)\n" << std::flush; + } SinCosPair sc; sc = Radian(heading); // FUN_00408328 Quaternion yaw(0.0f, sc.sine, 0.0f, sc.cosine); // FUN_00409948 @@ -965,12 +972,25 @@ void Scalar sinH = 0.0f, cosH = 1.0f; if (currentAngularPointer != NULL) { - EulerAngles e; - e = *currentAngularPointer; // Quaternion -> EulerAngles (operator=) + // FIX (user-reported: radar rotation flops every 180deg): the MUNGA + // EulerAngles(Quaternion) decomposition is AMBIGUOUS for a yawing mech -- + // as yaw sweeps past +/-pi the quaternion double-cover flips it onto a + // pitch=roll=pi branch, so euler.yaw REVERSES (the radar spins back). + // YawPitchRoll applies yaw FIRST -> a clean continuous 360deg heading. + // Same fix the compass HeadingPointer already uses (btl4gaug.cpp:2185). + YawPitchRoll ypr; + ypr = *currentAngularPointer; SinCosPair scH; - scH = Radian(e.yaw); // FUN_00408328 + scH = Radian((Scalar)ypr.yaw); sinH = scH.sine; cosH = scH.cosine; + if (getenv("BT_RADAR_LOG")) { + EulerAngles e; e = *currentAngularPointer; // old (buggy) path, for A/B + static int s_rn = 0; + if ((s_rn++ % 20) == 0) + DEBUG_STREAM << "[radar] euler.yaw=" << (Scalar)e.yaw + << " ypr.yaw=" << (Scalar)ypr.yaw << " (rad)\n" << std::flush; + } } Scalar rx = delta.x * cosH + delta.z * sinH; Scalar rz = -delta.x * sinH + delta.z * cosH; diff --git a/game/reconstructed/btl4vid.cpp b/game/reconstructed/btl4vid.cpp index 66f8deb..d7a384c 100644 --- a/game/reconstructed/btl4vid.cpp +++ b/game/reconstructed/btl4vid.cpp @@ -227,9 +227,43 @@ void // to terrain). { extern Entity *gBTTerrainEntity; + // The boresight ground/structure pick needs a non-null world Entity to + // cite in mech+0x388 (the geometry rides in the pick POINT; the entity is + // only the non-mech designation -- the decomp's HudSimulation handles a + // target with no damage-zone table, part_013.c:5620). Previously this + // required a TERRAIN-derived entity, but maps whose world geometry is + // buildings/cultural/props (class-42, NOT Terrain-derived) captured + // nothing -> gBTTerrainEntity stayed 0 -> the ground pick was skipped + // (mech4.cpp:4372) -> only mechs were targetable/firable (task #50 + // regression: LAST.EGG rewrite lost its Terrain entity). This `default` + // case is world-geometry ONLY (mechs route elsewhere), so capture the + // first entity reaching it -- preferring a real Terrain, else any world + // entity (building/prop/landmark) as the non-mech pick sentinel. if (gBTTerrainEntity == 0 - && entity->IsDerivedFrom(*Terrain::GetClassDerivations())) - gBTTerrainEntity = entity; + || !gBTTerrainEntity->IsDerivedFrom(*Terrain::GetClassDerivations())) + { + if (entity->IsDerivedFrom(*Terrain::GetClassDerivations())) + gBTTerrainEntity = entity; // prefer a true Terrain entity + else if (gBTTerrainEntity == 0) + gBTTerrainEntity = entity; // else the first world entity + } + // CENSUS: what world-geometry entities does the RENDER tree walk? + // (the garages/structures may be here even if not in the map's class-42 + // instance stream that BuildTables reads.) + if (getenv("BT_GROUND_LOG")) + { + Derivation *dv = entity->GetClassDerivations(); + const int isTer = entity->IsDerivedFrom(*Terrain::GetClassDerivations())?1:0; + const Point3D &ep = entity->localOrigin.linearPosition; + // tag NON-terrain entities distinctly -- these are the structures + // (garages/walls/props); log their world XZ so we can spawn beside + // one and confirm it is a collision solid the boresight now hits. + DEBUG_STREAM << (isTer ? "[rendent]" : "[rendent-STRUCT]") + << " class='" << (dv && dv->className ? dv->className : "?") + << "' terrain=" << isTer + << " pos=(" << ep.x << "," << ep.y << "," << ep.z << ")" + << "\n" << std::flush; + } } DPLRenderer::MakeEntityRenderables( entity, model_resource, view_type); @@ -1868,7 +1902,12 @@ BTReticleRenderable *BTBuildReticle(Entity *mech) 2, 3, (int *)wp->SimulationStatePtr(), // attr 1: damage state (1 = destroyed) 1, - 1 /* Front group (RearFiring==0 on every BLH weapon) */); + // (task #68) the AUTHENTIC pip group: the binary registration reads + // the weapon's RearFiring attr and passes group 1 (front) / 2 (rear) + // (part_014.c:5429-5434) -- the reticle draws the group matching + // the current look view (mech reticleElementMask low bits). The + // old hardcoded 1 rode the disproven "no BLH weapon is rear" belief. + wp->GetRearFiring() ? 2 : 1); } DEBUG_STREAM << "[hud] reticle built: " << gBTReticle->WeaponCount() << " weapon pip(s) registered\n" << std::flush; diff --git a/game/reconstructed/btstubs.cpp b/game/reconstructed/btstubs.cpp index 81fa1f5..f4d9daa 100644 --- a/game/reconstructed/btstubs.cpp +++ b/game/reconstructed/btstubs.cpp @@ -48,9 +48,10 @@ // Default 1.0 so the mech-age divide in mech.cpp does not divide by zero. Scalar FrameTimeScale = 1.0f; -// TODO(bring-up): audio preset bank table (real data lives in WTPresets/L4AUDLVL). -// Zero-initialised -> PRESET_isImplemented() returns false (no presets) at boot. -PRESETINFO allPresets[2][100] = {}; +// Audio preset bank table -- NOW DEFINED in audiopresets.cpp (241 presets generated +// from the AWE32 SoundFonts AUDIO1/2.RES by scratchpad/sf2extract.py, task #50 +// 2026-07-15). The old zero-init stub here (PRESET_isImplemented -> false -> silent) +// is replaced by the real bank/preset->sample map. //===========================================================================// // BattleTech globals (status-message pool + hand-rolled vtable pointers the @@ -397,3 +398,8 @@ void VideoComponent::ReceiveControl(VideoControlID /*control_ID*/, Scalar /*cont void VideoComponent::Execute() { } + +// DIAG (audio footstep chain): the live &mech->footStep, exported so the +// engine watcher poll can trace THE footstep watcher without per-run pointers. +void *g_btFootStepAddr = 0; +void *g_btAccelAddr = 0; // DIAG: watcher-poll tracer on the LocalAcceleration attr diff --git a/game/reconstructed/btvisgnd.cpp b/game/reconstructed/btvisgnd.cpp index 55ec007..df527df 100644 --- a/game/reconstructed/btvisgnd.cpp +++ b/game/reconstructed/btvisgnd.cpp @@ -53,6 +53,7 @@ struct VgMesh { std::vector tris; float minx, maxx, minz, maxz; // model-local XZ bound + float miny, maxy; // model-local Y bound (dome vs flat-ground test) }; struct VgInst @@ -89,8 +90,8 @@ int LoadMeshFor(const char *resource_name) if (LoadBgfFile(file, data) && data.ok && !data.indices.empty()) { VgMesh m; - m.minx = m.minz = 1e9f; - m.maxx = m.maxz = -1e9f; + m.minx = m.minz = m.miny = 1e9f; + m.maxx = m.maxz = m.maxy = -1e9f; // The loader emits DOUBLE-SIDED triangles: emitTri pushes (a,b,c) then // (a,c,b) -- 6 indices per source triangle. Take the forward one. for (size_t i = 0; i + 5 < data.indices.size(); i += 6) @@ -100,17 +101,33 @@ int LoadMeshFor(const char *resource_name) const BgfVtx &C = data.verts[data.indices[i + 2]]; VgTri t = { A.x, A.y, A.z, B.x, B.y, B.z, C.x, C.y, C.z }; m.tris.push_back(t); - float xs[3] = { A.x, B.x, C.x }, zs[3] = { A.z, B.z, C.z }; + float xs[3] = { A.x, B.x, C.x }, zs[3] = { A.z, B.z, C.z }, ys[3] = { A.y, B.y, C.y }; for (int k = 0; k < 3; ++k) { if (xs[k] < m.minx) m.minx = xs[k]; if (xs[k] > m.maxx) m.maxx = xs[k]; if (zs[k] < m.minz) m.minz = zs[k]; if (zs[k] > m.maxz) m.maxz = zs[k]; + if (ys[k] < m.miny) m.miny = ys[k]; + if (ys[k] > m.maxy) m.maxy = ys[k]; } } if (!m.tris.empty()) { + if (getenv("BT_GROUND_LOG")) + { + float miny = 1e9f, maxy = -1e9f; + for (size_t j = 0; j < m.tris.size(); ++j) + { + const VgTri &t = m.tris[j]; + const float ys[3] = { t.ay, t.by, t.cy }; + for (int k = 0; k < 3; ++k) + { if (ys[k] < miny) miny = ys[k]; if (ys[k] > maxy) maxy = ys[k]; } + } + DEBUG_STREAM << "[visgnd] mesh '" << key << "' tris=" << m.tris.size() + << " x[" << m.minx << "," << m.maxx << "] z[" << m.minz << "," << m.maxz + << "] y[" << miny << "," << maxy << "]\n" << std::flush; + } gMeshes.push_back(m); idx = (int)gMeshes.size() - 1; } @@ -151,16 +168,44 @@ bool BuildTables() const float *pos = (const float *)(base + p + 48); p += (mlen + 3) & ~3u; + // CENSUS (BT_GROUND_LOG): log EVERY map entity's class + name, so we can see + // what class the garages/structures are (the class-42 filter may drop them). + if (getenv("BT_GROUND_LOG")) + { + ResourceDescription *crd = rf->FindResourceDescription(rid); + DEBUG_STREAM << "[mapent] cls=" << cls << " mflags=" << mflags + << " name='" << (crd ? crd->resourceName : "?") << "'\n" << std::flush; + } if (mflags & 0x2) continue; // include marker if (cls != 42) continue; // terrain/static cultural only ResourceDescription *rd = rf->FindResourceDescription(rid); if (rd == 0) continue; - if (SkippedName(rd->resourceName)) { ++skipped; continue; } int mi = LoadMeshFor(rd->resourceName); if (mi < 0) { ++skipped; continue; } + // GEOMETRY-AWARE skip: the name filter (sky/backdrop) is right for a high + // vertical DOME, but arena1's ground is a 10000x10000 FLAT plane at y=0 + // misnamed "sky" -- name-skipping it removed the only groundable geometry + // (task #50: buildings/ground stopped moving the range axis). Keep a + // name-skipped mesh IF it is a wide, near-flat, near-ground plane (the + // arena floor); still skip true domes / tall vertical backdrops. + if (SkippedName(rd->resourceName)) + { + const VgMesh &gm = gMeshes[mi]; + const float yext = gm.maxy - gm.miny; + const float xext = gm.maxx - gm.minx, zext = gm.maxz - gm.minz; + const bool flatWideGround = + (yext < 200.0f) && (xext > 500.0f) && (zext > 500.0f) && (gm.miny < 200.0f); + if (getenv("BT_GROUND_LOG")) + DEBUG_STREAM << "[visgnd] name-skip '" << rd->resourceName + << "' yext=" << yext << " xz=" << xext << "x" << zext + << " miny=" << gm.miny << " -> keepAsGround=" << (flatWideGround?1:0) + << "\n" << std::flush; + if (!flatWideGround) { ++skipped; continue; } + } + VgInst inst; inst.mesh = mi; inst.px = pos[0]; inst.py = pos[1]; inst.pz = pos[2]; diff --git a/game/reconstructed/emitter.cpp b/game/reconstructed/emitter.cpp index de496e9..aa0d31d 100644 --- a/game/reconstructed/emitter.cpp +++ b/game/reconstructed/emitter.cpp @@ -271,6 +271,15 @@ void Scalar dist = (Scalar)Sqrt(delta.x*delta.x + delta.y*delta.y + delta.z*delta.z); // FUN_004dd138 beamScale.z = dist / graphicLength; // 0x434 (== beamScale[2]) = dist / 0x438 + if (getenv("BT_FIRE_LOG")) + { + Entity *st = (owner != 0) ? *(Entity **)((char *)owner + 0x388) : 0; + DEBUG_STREAM << "[fireW] " << GetName() << " dist=" << (float)dist + << " effRange=" << (float)effectiveRange + << " inRange=" << (int)(dist <= effectiveRange) + << " explID=" << (long)GetExplosionResourceID() + << " tgt=" << (void *)st << "\n" << std::flush; + } if (dist <= effectiveRange) // this+0x328 { // THE AUTHENTIC DAMAGE SUBMISSION (task #8; binary @004bace8:7758-7764): @@ -349,6 +358,7 @@ void << " pct=" << (float)rechargeLevel << " tgt=" << (void *)((owner != 0) ? *(Entity **)((char *)owner + 0x388) : 0) + << " explID=" << (long)GetExplosionResourceID() << std::endl; } targetWithinRange = UpdateTargeting(); // @004b9bdc -> this+0x34c @@ -380,7 +390,7 @@ void case 2: // Loaded -- ready; fire on the trigger's rising edge if (fireEdge) { - if (useConfiguredPip && HasActiveTarget()) // this+0x3E0 (Loaded->Firing gate) && entity+0x388 + if (viewFireEnable && HasActiveTarget()) // this+0x3E0 (Loaded->Firing gate: enabled in the CURRENT look view) && entity+0x388 { weaponAlarm.SetLevel(0); // -> Firing FireWeapon(); // (*vtable+0x48)() @@ -823,8 +833,8 @@ Emitter::Emitter( } // (E4 removed) the former `firingArmed = 1` wrote this+0x3e8 == MechWeapon::recoil, - // NOT the Loaded->Firing gate: the binary gate reads useConfiguredPip@0x3E0 (set by - // the MechWeapon ctor from usesExternalModel), so no separate arming is needed. + // NOT the Loaded->Firing gate: the binary gate reads viewFireEnable@0x3E0 (set by + // the MechWeapon ctor / the look-state commit), so no separate arming is needed. weaponAlarm.SetLevel(3); // Loading beamEndpoint = Point3D(0.0f, 0.0f, 0.0f); // 0x460 <- (0,0,0) beamFlag = 0; // 0x46c diff --git a/game/reconstructed/emitter.hpp b/game/reconstructed/emitter.hpp index be61119..8559302 100644 --- a/game/reconstructed/emitter.hpp +++ b/game/reconstructed/emitter.hpp @@ -308,7 +308,7 @@ class NotationFile; // * beamHitPoint/beamImpact/beamImpactScalar -> binary writes into the inherited // Damage damageData (0x3C8/0x3B0/0x3AC); the recon assignments were dead -> deleted // * beamColor -> never read (WriteUpdateRecord builds colour from targetEntity) - // * firingArmed -> the Loaded->Firing gate reads inherited useConfiguredPip@0x3E0 + // * firingArmed -> the Loaded->Firing gate reads inherited viewFireEnable@0x3E0 // * energyRampTime -> binary writes the base slot voltageScale@0x310; recon computes a local // * beamLengthRatio -> is beamScale.z@0x434 int seekVoltageIndex; // @0x3F0 (param_1[0xfc]) current charge-curve index diff --git a/game/reconstructed/heat.cpp b/game/reconstructed/heat.cpp index e79fcd4..5bd5c62 100644 --- a/game/reconstructed/heat.cpp +++ b/game/reconstructed/heat.cpp @@ -80,6 +80,37 @@ static const Scalar CoolantDrawFloor = 0.0025f; // _DAT_0050e3d8 (zero floor static const Scalar CoolantActiveOn = 0.003f; // _DAT_0050e3d4 (ON threshold) +//########################################################################### +// GaugeAlarm54 watcher sockets. The binary's 0x54 alarm (FUN_0041b9ec) IS a +// StateIndicator -- its three sub-indicators @0x18/0x2c/0x40 are the audio/ +// video/gauge watcher chains, level@0x14 is currentState. These out-of-line +// bodies let an AudioStateWatcher bind to any subsystem alarm BY NAME +// (GeneratorState / CondenserState / ReservoirState / ...); SetLevel fires the +// registered watchers on a level change, so the state audio plays on transition. +// Empty sockets iterate to nothing, so alarms with no audio watchers are a no-op. +//########################################################################### +void GaugeAlarm54::AddAudioWatcher(Component *watcher) { audioWatcherSocket.Add(watcher); } +void GaugeAlarm54::AddVideoWatcher(Component *watcher) { videoWatcherSocket.Add(watcher); } +void GaugeAlarm54::AddGaugeWatcher(Component *watcher) { gaugeWatcherSocket.Add(watcher); } + +void GaugeAlarm54::NotifyWatchers() +{ + Component *watcher; + { + SChainIteratorOf iterator(audioWatcherSocket); + while ((watcher = iterator.ReadAndNext()) != NULL) watcher->Execute(); + } + { + SChainIteratorOf iterator(videoWatcherSocket); + while ((watcher = iterator.ReadAndNext()) != NULL) watcher->Execute(); + } + { + SChainIteratorOf iterator(gaugeWatcherSocket); + while ((watcher = iterator.ReadAndNext()) != NULL) watcher->Execute(); + } +} + + //########################################################################### //########################################################################### // HeatableSubsystem @@ -236,6 +267,25 @@ int //############################################################################# // Shared Data Support // +// CondenserState -> condenserAlarm (@0x1DC). Chained to HeatSink so the inherited +// coolant/temp attrs stay reachable; dense from HeatSink::NextAttributeID. +const Condenser::IndexEntry + Condenser::AttributePointers[]= +{ + ATTRIBUTE_ENTRY(Condenser, CondenserState, condenserAlarm) // @0x1DC (0x54 StateIndicator-compatible alarm) +}; + +Condenser::AttributeIndexSet& + Condenser::GetAttributeIndex() +{ + static Condenser::AttributeIndexSet attributeIndex( + ELEMENTS(Condenser::AttributePointers), + Condenser::AttributePointers, + HeatSink::GetAttributeIndex() + ); + return attributeIndex; +} + Condenser::SharedData Condenser::DefaultData( Condenser::GetClassDerivations(), @@ -367,7 +417,13 @@ const HeatSink::IndexEntry ATTRIBUTE_ENTRY(HeatSink, DegradationPressure, coolantEfficiency), // @0x124 ATTRIBUTE_ENTRY(HeatSink, CoolantMassLeakRate, coolantDraw), // @0x130 (LeakGauge; damage-driven) ATTRIBUTE_ENTRY(HeatSink, HeatSink, linkedSinks), // @0x164 (Eng linked-sink temp readout) - ATTRIBUTE_ENTRY(HeatSink, ValveSetting, coolantFlowScale) // @0x15C (condenser valve slider @2; init 1.0f) + ATTRIBUTE_ENTRY(HeatSink, ValveSetting, coolantFlowScale), // @0x15C (condenser valve slider @2; init 1.0f) + // (AUDIO_FIDELITY F6) the coolant-leak warning: binary id 12 @0x138 = the + // coolantActive leak flag (binary table @0x50e4c8; watchers match ==1/==0). + // PoweredSubsystem (weapons/sensor/myomers) DERIVES from HeatSink, so this + // single row serves ALL 19 authored leak watchers through the chained + // index -- exactly like the binary (one ReportLeak row in the image). + ATTRIBUTE_ENTRY(HeatSink, ReportLeak, coolantActive) // @0x138 (leak-active hysteresis flag) }; HeatSink::AttributeIndexSet& @@ -818,6 +874,15 @@ void coolantDraw = 0.0f; } + // DIAG (BT_COOL_LOG): fire whenever THIS heat subsystem carries any damage -- + // answers "does damaging a mech drain its coolant?". Shows the damageLevel the + // leak reads (@0xE0), the heat load, the resulting draw, and the live level. + if (getenv("BT_COOL_LOG") && zoneDamage > 0.001f) + DEBUG_STREAM << "[cool] " << GetName() << " dmg=" << zoneDamage + << " heatLoad=" << heatLoad << " draw=" << coolantDraw + << " coolantLevel=" << coolantLevel << "/" << thermalCapacity + << "\n" << std::flush; + Scalar amount = coolantDraw * time_slice; if (coolantLevel < amount) { diff --git a/game/reconstructed/heat.hpp b/game/reconstructed/heat.hpp index bc3dbb3..8448fe7 100644 --- a/game/reconstructed/heat.hpp +++ b/game/reconstructed/heat.hpp @@ -39,6 +39,8 @@ #include // AverageOf #include // Scalar #include // GaugeAlarm +#include // SChainOf (GaugeAlarm54 watcher sockets) +class Component; // fwd -- AddAudioWatcher(Component*) defined in heat.cpp #include #include @@ -91,26 +93,48 @@ class Mech; class GaugeAlarm54 { public: - GaugeAlarm54(int levels = 0) { levelA = levelB = levels; level = 0; } + GaugeAlarm54(int levels = 0) + : audioWatcherSocket(0), videoWatcherSocket(0), gaugeWatcherSocket(0) + { levelA = levelB = levels; level = 0; } void Initialize(int levels) { levelA = levelB = levels; level = 0; } - void SetLevel(int n) { level = n; } + // StateIndicator::SetState semantics: oldState (levelB@0x10) := currentState + // BEFORE the change, so the audio watcher's StateChanged(oldState,newState) is + // correct -- this is what the inverse/STOP AudioStateTriggers key on (old_state == + // firing/charging state) to StopNote a looping charge/sustain/loading sample when + // the weapon leaves that state. (In the binary levelB@0x10 IS oldState; the + // weapon's LevelCountB() reading @0x10 is that same authentic oldState.) + void SetLevel(int n) { levelB = level; if (n != level) { level = n; NotifyWatchers(); } } int GetLevel() const { return level; } int Level() const { return level; } int LevelCountB() const { return levelB; } // +0x10 (the weapon update-record "reset" source) // ReconAlarm/AlarmIndicator API aliases (callers that predate the retype use these): - void SetState(unsigned n){ level = (int)n; } + void SetState(unsigned n){ SetLevel((int)n); } unsigned GetState() const { return (unsigned)level; } + // The binary's 0x54 alarm (FUN_0041b9ec) IS a StateIndicator: its "three + // sub-indicators" @0x18/0x2c/0x40 are the audio/video/gauge watcher sockets, + // and level@0x14 is currentState. So an AudioStateWatcher can bind to any + // subsystem alarm BY NAME (GeneratorState/CondenserState/ReservoirState/...) and + // AddAudioWatcher registers on the +0x18 socket; SetLevel fires it on change. + // (Defined out-of-line in heat.cpp so this header stays free of Component/iterator.) + void AddAudioWatcher(Component *watcher); + void AddVideoWatcher(Component *watcher); + void AddGaugeWatcher(Component *watcher); +private: + void NotifyWatchers(); // fire audio/video/gauge watchers (empty sockets == no-op) protected: - // Interior mirrors FUN_0041b9ec: base GaugeAlarm header (+0x00), three count - // words at +0x0c/+0x10/+0x14, three sub-indicators at +0x18/+0x2c/+0x40. The - // STATUS level the binary reads (HeatSink+0x184 == alarm+0x14, GetStatusFlags - // @004add30 / PrintState @004ae050) is param_1[5] at +0x14, so `level` lands - // there -- a raw `subsystem+0x184` read now returns GetLevel(). - char _hdr[0x0c]; // +0x00 base GaugeAlarm (vtable + FUN_004178cc header) - int levelA; // +0x0c param_1[3] (level count) - int levelB; // +0x10 param_1[4] - int level; // +0x14 param_1[5] -- the status level (Normal/Deg/Fail) - char _tail[0x54 - 0x18]; // +0x18 three sub-indicators to 0x54 + // Interior mirrors FUN_0041b9ec / StateIndicator: base header (+0x00, a Node in + // the binary), three state words @+0x0c/+0x10/+0x14 (== stateCount/oldState/ + // currentState), three watcher sockets @+0x18/+0x2c/+0x40. level@+0x14 is the + // STATUS level the binary reads (HeatSink+0x184 == alarm+0x14). levelB@+0x10 is + // left as the weapon "reset source" (LevelCountB) -- SetLevel does NOT touch it, + // so weapon alarms are unchanged; only the watcher sockets are now real. + char _hdr[0x0c]; // +0x00 base header (vtable + FUN_004178cc header) + int levelA; // +0x0c stateCount + int levelB; // +0x10 oldState slot / weapon LevelCountB + int level; // +0x14 currentState (status level) + SChainOf audioWatcherSocket; // +0x18 + SChainOf videoWatcherSocket; // +0x2c + SChainOf gaugeWatcherSocket; // +0x40 }; // The 8-byte modeling type kept ONLY for HUD::statusAlarm (still on the old @@ -345,6 +369,14 @@ inline int CoolantMassLeakRateAttributeID, // @0x130 coolantDraw (damage-driven leak) HeatSinkAttributeID, // @0x164 linkedSinks (link to master sink) ValveSettingAttributeID, // @0x15C coolantFlowScale (condenser valve slider @2) + // (AUDIO_FIDELITY F6) the binary heat attribute table (16-byte rows + // {id, name, off+1, pad} @0x50e438..0x50e4c8, ids 3..12) confirms + // EVERY existing binding above [T1] and adds ReportLeak (id 12) -> + // +0x138 = coolantActive, the INT leak hysteresis flag UpdateCoolant + // (@004adbf8 [0x4e]) drives 1 above draw 0.003 / 0 below 0.0025. + // The authored Logical match watchers (19 subsystems, ==1 Start / + // ==0 Stop of the looped 3-note leak sequence) fire on it directly. + ReportLeakAttributeID, NextAttributeID }; private: @@ -543,6 +575,22 @@ inline int static Receiver::MessageHandlerSet& GetMessageHandlers(); static SharedData DefaultData; + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Attribute Support -- audio binds an AudioStateWatcher to CondenserState; + // it resolves to condenserAlarm (@0x1DC, a 0x54 StateIndicator-compatible + // GaugeAlarm54). RefrigerationSimulation SetLevel's it (2/1/0), so the + // condenser cycling audio fires on the flow-change pulse. Chained to HeatSink. + // + public: + enum { + CondenserStateAttributeID = HeatSink::NextAttributeID, + NextAttributeID + }; + private: + static const IndexEntry AttributePointers[]; + public: + static AttributeIndexSet& GetAttributeIndex(); + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Test Class Support // diff --git a/game/reconstructed/heatfamily_reslice.cpp b/game/reconstructed/heatfamily_reslice.cpp index 5fa2f1f..4931bfe 100644 --- a/game/reconstructed/heatfamily_reslice.cpp +++ b/game/reconstructed/heatfamily_reslice.cpp @@ -494,6 +494,25 @@ int //############################################################################# // Shared Data Support // +// ReservoirState -> reservoirAlarm (@0x1D0). Chained to HeatSink so the inherited +// coolant/temp attrs stay reachable; dense from HeatSink::NextAttributeID. +const Reservoir::IndexEntry + Reservoir::AttributePointers[]= +{ + ATTRIBUTE_ENTRY(Reservoir, ReservoirState, reservoirAlarm) // @0x1D0 (0x54 StateIndicator-compatible alarm) +}; + +Reservoir::AttributeIndexSet& + Reservoir::GetAttributeIndex() +{ + static Reservoir::AttributeIndexSet attributeIndex( + ELEMENTS(Reservoir::AttributePointers), + Reservoir::AttributePointers, + HeatSink::GetAttributeIndex() + ); + return attributeIndex; +} + Reservoir::SharedData Reservoir::DefaultData( Reservoir::GetClassDerivations(), diff --git a/game/reconstructed/heatfamily_reslice.hpp b/game/reconstructed/heatfamily_reslice.hpp index aec53ff..af79695 100644 --- a/game/reconstructed/heatfamily_reslice.hpp +++ b/game/reconstructed/heatfamily_reslice.hpp @@ -224,6 +224,19 @@ static Derivation *GetClassDerivations(); // name "Reservoir" static SharedData DefaultData; + // Attribute Support -- audio binds an AudioStateWatcher to ReservoirState; + // it resolves to reservoirAlarm (@0x1D0, a 0x54 StateIndicator-compatible + // GaugeAlarm54; level@0x1e4 is the inject flag). CoolantSimulation SetLevel's + // it, so the coolant-inject audio fires on the inject on/off transition. + enum { + ReservoirStateAttributeID = HeatSink::NextAttributeID, + NextAttributeID + }; + private: + static const IndexEntry AttributePointers[]; + public: + static AttributeIndexSet& GetAttributeIndex(); + typedef Reservoir__SubsystemResource SubsystemResource; typedef void diff --git a/game/reconstructed/mech.cpp b/game/reconstructed/mech.cpp index 0f71b33..6ed9e9c 100644 --- a/game/reconstructed/mech.cpp +++ b/game/reconstructed/mech.cpp @@ -539,6 +539,37 @@ Logical return True; } +// +// World-STRUCTURE boresight pick. Ray-test the zone's STATIC collision solid +// tree (the world geometry -- garages, walls, props -- that the mech's walk +// already collides against) along the boresight, and hand back the entry point +// of the closest structure the ray strikes. This is the authentic non-mech +// world pick: Mover::FindStaticSolidHitBy is the "test against the static world" +// tail of the engine's own FindBoxedSolidHitBy (MOVER.cpp), which is what the +// mover-vs-world collision uses -- so a shot lands on EXACTLY the geometry that +// blocks the walk (the user's "structures block the mech, but weapons don't"). +// +// The ray is WORLD-space (the static tree is world-space, unlike a mech's local +// collision template). FindBoundingBoxHitBy clips the Line's length to the +// entry distance (BoundingBox::HitByBounded -> line->length = enter), so +// line->FindEnd yields the world entry point on the struck solid. +// +Logical + Mech::WorldStructurePick(const Point3D &start, const Vector3D &dir, + Scalar max_range, Point3D *hit_world) +{ + Scalar dlen = (Scalar)Sqrt(dir.x*dir.x + dir.y*dir.y + dir.z*dir.z); + if (dlen < 1e-6f) + return False; + Line ray(start, UnitVector(dir.x/dlen, dir.y/dlen, dir.z/dlen), max_range); + BoxedSolid *solid = FindStaticSolidHitBy(&ray); // static world only + if (solid == 0) + return False; + if (hit_world != 0) + ray.FindEnd(hit_world); // clipped length -> entry point + return True; +} + // // The roster torso (sinkSourceSubsystem @0x438, ClassID 0xBC5) -- parity with // the binary's *(mech+0x438). @@ -672,17 +703,17 @@ const Mech::IndexEntry Mech::AttributePointers[]= { ATTRIBUTE_ENTRY(Mech, MaxAcceleration, attrPad), // 0x15 - ATTRIBUTE_ENTRY(Mech, CollisionState, attrPad), // 0x16 + ATTRIBUTE_ENTRY(Mech, CollisionState, collisionState), // 0x16 real StateIndicator (audio impact/scrape watcher) ATTRIBUTE_ENTRY(Mech, CollisionNormal, attrPad), // 0x17 - ATTRIBUTE_ENTRY(Mech, CollisionSpeed, attrPad), // 0x18 + ATTRIBUTE_ENTRY(Mech, CollisionSpeed, collisionSpeed), // 0x18 (F17: |impact velocity|, binary @0x4B4) ATTRIBUTE_ENTRY(Mech, CollisionMaterialType, attrPad), // 0x19 ATTRIBUTE_ENTRY(Mech, CurrentSpeed, legCycleSpeed), // 0x1a (existing @0x348) ATTRIBUTE_ENTRY(Mech, MaxRunSpeed, reverseStrideLength), // 0x1b (existing @0x34c = run/top speed) ATTRIBUTE_ENTRY(Mech, EyepointRotation, eyepointRotation), // 0x1c (real member -- the eye reads it per frame) ATTRIBUTE_ENTRY(Mech, TargetReticle, targetReticle), // 0x1d (real Reticle struct -- task #36) - ATTRIBUTE_ENTRY(Mech, FootStep, attrPad), // 0x1e - ATTRIBUTE_ENTRY(Mech, AnimationState, attrPad), // 0x1f - ATTRIBUTE_ENTRY(Mech, ReplicantAnimationState, attrPad), // 0x20 + ATTRIBUTE_ENTRY(Mech, FootStep, footStep), // 0x1e real pulse (AudioLogicalTrigger; one rising edge per foot plant) + ATTRIBUTE_ENTRY(Mech, AnimationState, animationState), // 0x1f real StateIndicator (audio state-watcher binds + AddAudioWatcher) + ATTRIBUTE_ENTRY(Mech, ReplicantAnimationState, replicantAnimationState), // 0x20 real StateIndicator ATTRIBUTE_ENTRY(Mech, LinearSpeed, linearSpeed), // 0x21 (live forward speed) ATTRIBUTE_ENTRY(Mech, ClimbRate, attrPad), // 0x22 ATTRIBUTE_ENTRY(Mech, AccelerationLastFrame, attrPad), // 0x23 @@ -696,17 +727,21 @@ const Mech::IndexEntry ATTRIBUTE_ENTRY(Mech, TestButton4, attrPad), // 0x2b ATTRIBUTE_ENTRY(Mech, TestButton5, attrPad), // 0x2c ATTRIBUTE_ENTRY(Mech, TestButton6, attrPad), // 0x2d - ATTRIBUTE_ENTRY(Mech, ReduceButton, attrPad), // 0x2e + ATTRIBUTE_ENTRY(Mech, ReduceButton, reduceButton), // 0x2e (F17: cab REDUCE, binary @0x340) ATTRIBUTE_ENTRY(Mech, RadarRange, radarRange), // 0x2f (radar scale) ATTRIBUTE_ENTRY(Mech, RadarLinearPosition, radarLinearPosition), // 0x30 (Point3D* -> localOrigin) ATTRIBUTE_ENTRY(Mech, RadarAngularPosition, radarAngularPosition),// 0x31 (Quaternion* -> localOrigin) - ATTRIBUTE_ENTRY(Mech, RearFiring, attrPad), // 0x32 + ATTRIBUTE_ENTRY(Mech, RearFiring, rearFiring), // 0x32 (task #68: binary id 50 @0x410) ATTRIBUTE_ENTRY(Mech, RequestDuckAnimation, attrPad), // 0x33 - ATTRIBUTE_ENTRY(Mech, UnstablePercentage, attrPad), // 0x34 + ATTRIBUTE_ENTRY(Mech, UnstablePercentage, unstablePercentage), // 0x34 (task #66: the instability alarm; binary id 52 @0x3F0) ATTRIBUTE_ENTRY(Mech, SuperStop, attrPad), // 0x35 - ATTRIBUTE_ENTRY(Mech, IncomingLock, attrPad), // 0x36 + // (AUDIO_FIDELITY F7) real missile-alarm backing: binary ids 54/56 @0x3fc/ + // 0x400 [T1 table @0x50c0b0/0x50c0d0]; the authored beeper (match 1/0) + + // tempo scale (range 100..800 -> tempo 600..10) read these. Driven by + // Missile::MoveAndCollide via BTReportIncomingMissile (mech4.cpp). + ATTRIBUTE_ENTRY(Mech, IncomingLock, incomingLock), // 0x36 ATTRIBUTE_ENTRY(Mech, DuckState, duckState), // 0x37 (crouch posture, int) - ATTRIBUTE_ENTRY(Mech, DistanceToMissile, attrPad) // 0x38 + ATTRIBUTE_ENTRY(Mech, DistanceToMissile, distanceToMissile) // 0x38 }; Mech::AttributeIndexSet& @@ -818,8 +853,10 @@ Mech::Mech( mechNameFilter.Initialize(); // FUN_00435a7c(this+0xdb) masterAlarm = AlarmIndicator(0x21); // FUN_0041b9ec(this+0xe7,0x21) - stateFlags = 0; // Wword(0xff) - maxSpeed = DAT_005209d0; // FLT_MAX -> Wword(0x100) + rearFiring = 0; // (task #68) ORed from the weapons below + // (F7 correction) the binary's 0x400 = FLT_MAX init is DistanceToMissile's + // "no missile" far default (attr id 56), NOT a maxSpeed -- the old member + // is retired; distanceToMissile (init below) owns the slot's meaning. Wword(0x104) = 0; Wword(0x106).Construct(0); // FUN_004a4d60 (slot member) Wword(0x10f).LinkTo(this + 0x61); // FUN_00420ea4 @@ -870,14 +907,60 @@ Mech::Mech( // dispatch (task #56) gates on it every frame; garbage // nonzero silently disabled the writers until death anims ran legResetLatch = 0; // @0x654 -- same latch family, same fill hazard + legAnimationState = 0; // @0x3b0 -- same hazard (found 2026-07-14, the real-clock + // crash): the type-3 record writer re-dispatches + // SetBodyAnimation(legAnimationState) on itself; a record + // emitted before the leg SM's first tick passed the raw + // 0xCDCDCDCD as a clip index -> AV in SetBodyAnimation. + // (The stub call-counter clock had merely re-ordered record + // emission so the window was never hit.) Standing = 0. attrPad = 0.0f; // shared read pad for un-populated gauge attributes linearSpeed = 0.0f; // live forward ground speed (LinearSpeed gauge); set per frame + // Size the animation StateIndicators to cover clips 0..0x20 (MechAnimationState, + // death variants past AnimationCount=0x1d included) so SetState(state) never trips + // Verify(state Wword(0x101) (override) + // (F7 correction) the old "maxSpeed = 1000.0f override" here targeted + // Wword(0x101) = +0x404 -- the binary table says that is RadarRange + // (id 47), already initialized as radarRange = 1000.0f above. Wword(0x102) = 0; Wword(0x103) = (int)(this + 0x43); @@ -1238,11 +1329,17 @@ Mech::Mech( } for (int id = 2; id < subsystemCount; ++id) // weapon roster (0x511830 = MechWeapon) { - SubProxy *s = (SubProxy *)subsystemArray[id]; - if (s != 0 && s->IsDerivedFrom(0x511830)) + // (task #68) REAL derivation test via the mechweap bridge -- the old + // SubProxy::IsDerivedFrom stub returned 0, so this loop never ran + // (empty weaponRoster, no capability OR). + extern int BTWeaponIsRearFiring(Subsystem *sub); // -1 = not a weapon + int rf = BTWeaponIsRearFiring(subsystemArray[id]); + if (rf >= 0) { - weaponRoster.Add((Subsystem *)s); - stateFlags |= s->capabilityFlags; // |= *(s+0x334) -> Wword(0x104) + weaponRoster.Add(subsystemArray[id]); + // mech.RearFiring |= weapon.rearFiring (binary ctor + // @part_012.c:10220: mech[0x104] |= *(weapon+0x334)) + rearFiring |= rf; } } for (int id = 2; id < subsystemCount; ++id) // damageable roster @@ -1303,18 +1400,21 @@ Mech::Mech( << " superStop=" << airborneCycleRate << " throttleAdj=" << forwardThrottleScale << std::endl; Wword(0x12f) = model->relativeMechValue; // rec+0x70 -> mech+0x4bc - Wword(0x1e1) = model->maxUnstableAcceleration; // rec+0x80-0x94 -> mech+0x784..0x798 - Wword(0x1e2) = model->unstableAccelerationEffect; // (the six Unstable* effects) - Wword(0x1e3) = model->unstableGunTheEngineEffect; - Wword(0x1e4) = model->unstableSuperStopEffect; - Wword(0x1e5) = model->unstableHighVelocityEffect; - Wword(0x1e6) = model->unstableStopedTurnEffect; + // (task #66) the six Unstable* tuning fields are REAL members now (the + // instability model consumes them); binary rec+0x80-0x94 -> mech+0x784..0x798. + maxUnstableAcceleration = model->maxUnstableAcceleration; + unstableAccelerationEffect = model->unstableAccelerationEffect; + unstableGunTheEngineEffect = model->unstableGunTheEngineEffect; + unstableSuperStopEffect = model->unstableSuperStopEffect; + unstableHighVelocityEffect = model->unstableHighVelocityEffect; + unstableStopedTurnEffect = model->unstableStopedTurnEffect; - // Angle fields converted from degrees to radians. - Wword(0x159) = (int)(model->lookLeftAngle * DegreesToRadians); // +0x50 - Wword(0x15a) = (int)(model->lookRightAngle * DegreesToRadians); // +0x54 - Wword(0x15b) = (int)(model->lookFrontAngle * DegreesToRadians); // +0x58 - Wword(0x15c) = (int)(model->lookBackAngle * DegreesToRadians); // +0x5c + // Angle fields converted from degrees to radians. (task #68: REAL members + // now -- the look-state commit consumes them; were Wword scratch parks.) + lookLeftAngle = model->lookLeftAngle * DegreesToRadians; // +0x50 -> @0x564 + lookRightAngle = model->lookRightAngle * DegreesToRadians; // +0x54 -> @0x568 + lookFrontAngle = model->lookFrontAngle * DegreesToRadians; // +0x58 -> @0x56C + lookBackAngle = model->lookBackAngle * DegreesToRadians; // +0x5c -> @0x570 // The update-record deadbands (task #3; the struct is now the verified // 200-byte overlay, so the named fields ARE the raw offsets). updateTurnAngleDeadband = model->updateTurnDegreeDiffrence * DegreesToRadians; // @0x770 @@ -1777,6 +1877,33 @@ Logical void Mech::ReadUpdateRecord(Simulation::UpdateRecord *message) { + // RECORD-CADENCE probe (BT_RXJIT): quantify how EVENLY records arrive (wall-clock + // ms between arrivals) -- the jitter that makes the peer's corrections random. + if (getenv("BT_RXJIT") && GetInstance() == ReplicantInstance) + { + LARGE_INTEGER _now, _freq; + QueryPerformanceCounter(&_now); QueryPerformanceFrequency(&_freq); + static double s_lastMs = 0.0, s_min = 1e9, s_max = 0.0, s_acc = 0.0; + static int s_n = 0, s_rep = 0; + const double nowMs = (double)_now.QuadPart * 1000.0 / (double)_freq.QuadPart; + if (s_lastMs > 0.0) + { + const double gap = nowMs - s_lastMs; + s_n++; s_acc += gap; + if (gap > s_max) s_max = gap; + if (gap < s_min) s_min = gap; + } + s_lastMs = nowMs; + if (++s_rep >= 120 && s_n > 0) + { + DEBUG_STREAM << "[rxjit] records/win=" << s_n + << " gapMs min=" << s_min << " avg=" << (s_acc / s_n) + << " max=" << s_max + << " burstiness(max/avg)=" << (s_max / (s_acc / s_n)) + << "\n" << std::flush; + s_rep = 0; s_n = 0; s_acc = 0.0; s_min = 1e9; s_max = 0.0; + } + } static const int s_mrecLog = getenv("BT_REPL_LOG") ? 1 : 0; if (s_mrecLog && message->recordID != 0) DEBUG_STREAM << "[mrec-rx] type=" << (int)message->recordID @@ -1803,6 +1930,13 @@ void updateVelocity.angularMotion = savedAngV; // FUN_00408440 poseSyncLatch = 1; // @0x77c -- arm the dead-reckon re-base + if (getenv("BT_WIRE")) + DEBUG_STREAM << "[rx0] ent=" << GetEntityID() + << " uvLin=(" << updateVelocity.linearMotion.x << "," + << updateVelocity.linearMotion.z << ")" + << " uPos=(" << updateOrigin.linearPosition.x << "," + << updateOrigin.linearPosition.z << ")\n" << std::flush; + if (bodyAnimationState == 2 || bodyAnimationState == 3) // @0x728 { projectedVelocity.linearMotion = ZeroVector; // FUN_0040a7f4(0x298, zeroMotion) @@ -1830,7 +1964,23 @@ void case 2: // commanded-speed update { Mech__SpeedUpdateRecord *record = (Mech__SpeedUpdateRecord *)message; - Simulation::ReadUpdateRecord(message); // FUN_0041bd34 + { + // CLOCK GUARD (keyboard-skip fix): the base reader stamps lastUpdate=Now() + // on EVERY record ('HACK' per SIMULATE.cpp:276). Non-pose records carry no + // pose, but that stamp shrinks the dead-reckoner's projection span + // (nextUpdate - lastUpdate) WITHOUT refreshing updateOrigin -- so the + // peer's position target jumps backward toward the stale origin, then the + // next pose record yanks it forward: target oscillation every frame while + // records churn. Keyboard driving churns them CONSTANTLY (the throttle + // lever sweeps every frame of a key-hold -> a type-2 speed record per + // frame), which autodrive's pinned throttle never does -- the 'skips when + // I drive, smooth when you drive' report. Preserve the clock; keep the + // real payload (simulationState). BT_T2_CLOCK restores the old stamp (A/B). + static const int s_t2clock = getenv("BT_T2_CLOCK") ? 1 : 0; + const Time savedLastUpdate = lastUpdate; + Simulation::ReadUpdateRecord(message); + if (!s_t2clock) lastUpdate = savedLastUpdate; + } // FUN_0041bd34 bodyTargetSpeed = record->speedDemand; // @0x6b4 <- rec+0x10 } break; @@ -1838,7 +1988,29 @@ void case 3: // leg/body state + stability { Mech__StateUpdateRecord *record = (Mech__StateUpdateRecord *)message; - Simulation::ReadUpdateRecord(message); + if (getenv("BT_GAITEV")) + DEBUG_STREAM << "[t3rx] legState=" << record->legState + << " reset=" << record->legResetLatch + << " myLegState=" << (int)legStateAlarm.GetLevel() + << " myLegFrm=" << legAnimation.currentFrame + << " spd=" << record->speedDemand << "\n" << std::flush; + { + // CLOCK GUARD (keyboard-skip fix): the base reader stamps lastUpdate=Now() + // on EVERY record ('HACK' per SIMULATE.cpp:276). Non-pose records carry no + // pose, but that stamp shrinks the dead-reckoner's projection span + // (nextUpdate - lastUpdate) WITHOUT refreshing updateOrigin -- so the + // peer's position target jumps backward toward the stale origin, then the + // next pose record yanks it forward: target oscillation every frame while + // records churn. Keyboard driving churns them CONSTANTLY (the throttle + // lever sweeps every frame of a key-hold -> a type-2 speed record per + // frame), which autodrive's pinned throttle never does -- the 'skips when + // I drive, smooth when you drive' report. Preserve the clock; keep the + // real payload (simulationState). BT_T2_CLOCK restores the old stamp (A/B). + static const int s_t2clock = getenv("BT_T2_CLOCK") ? 1 : 0; + const Time savedLastUpdate = lastUpdate; + Simulation::ReadUpdateRecord(message); + if (!s_t2clock) lastUpdate = savedLastUpdate; + } bodyResetLatch = record->legResetLatch; // @0x658 <- rec+0x10 stabilityAlarm.SetLevel(record->stability); // @0x4c4 <- rec+0x18 if (record->legState == 0) @@ -1879,13 +2051,30 @@ void } } bodyTargetSpeed = record->speedDemand; // rec+0x28 + angSyncLatch = 1; // arm the peer heading re-anchor } break; case 5: // knockdown { Mech__FallUpdateRecord *record = (Mech__FallUpdateRecord *)message; - Simulation::ReadUpdateRecord(message); + { + // CLOCK GUARD (keyboard-skip fix): the base reader stamps lastUpdate=Now() + // on EVERY record ('HACK' per SIMULATE.cpp:276). Non-pose records carry no + // pose, but that stamp shrinks the dead-reckoner's projection span + // (nextUpdate - lastUpdate) WITHOUT refreshing updateOrigin -- so the + // peer's position target jumps backward toward the stale origin, then the + // next pose record yanks it forward: target oscillation every frame while + // records churn. Keyboard driving churns them CONSTANTLY (the throttle + // lever sweeps every frame of a key-hold -> a type-2 speed record per + // frame), which autodrive's pinned throttle never does -- the 'skips when + // I drive, smooth when you drive' report. Preserve the clock; keep the + // real payload (simulationState). BT_T2_CLOCK restores the old stamp (A/B). + static const int s_t2clock = getenv("BT_T2_CLOCK") ? 1 : 0; + const Time savedLastUpdate = lastUpdate; + Simulation::ReadUpdateRecord(message); + if (!s_t2clock) lastUpdate = savedLastUpdate; + } heatAlarm.SetLevel(record->heatLevel); // @0x450 <- rec+0x10 throttleState = record->throttleState; // @0x4a4 <- rec+0x14 fallDirection = record->fallDirection; // @0x4a8 <- rec+0x18 @@ -1900,7 +2089,23 @@ void case 6: // death { Mech__FallUpdateRecord *record = (Mech__FallUpdateRecord *)message; - Simulation::ReadUpdateRecord(message); + { + // CLOCK GUARD (keyboard-skip fix): the base reader stamps lastUpdate=Now() + // on EVERY record ('HACK' per SIMULATE.cpp:276). Non-pose records carry no + // pose, but that stamp shrinks the dead-reckoner's projection span + // (nextUpdate - lastUpdate) WITHOUT refreshing updateOrigin -- so the + // peer's position target jumps backward toward the stale origin, then the + // next pose record yanks it forward: target oscillation every frame while + // records churn. Keyboard driving churns them CONSTANTLY (the throttle + // lever sweeps every frame of a key-hold -> a type-2 speed record per + // frame), which autodrive's pinned throttle never does -- the 'skips when + // I drive, smooth when you drive' report. Preserve the clock; keep the + // real payload (simulationState). BT_T2_CLOCK restores the old stamp (A/B). + static const int s_t2clock = getenv("BT_T2_CLOCK") ? 1 : 0; + const Time savedLastUpdate = lastUpdate; + Simulation::ReadUpdateRecord(message); + if (!s_t2clock) lastUpdate = savedLastUpdate; + } heatAlarm.SetLevel(record->heatLevel); throttleState = record->throttleState; fallDirection = record->fallDirection; @@ -1917,7 +2122,23 @@ void case 7: // impact (fields only, no side effects) { Mech__FallUpdateRecord *record = (Mech__FallUpdateRecord *)message; - Simulation::ReadUpdateRecord(message); + { + // CLOCK GUARD (keyboard-skip fix): the base reader stamps lastUpdate=Now() + // on EVERY record ('HACK' per SIMULATE.cpp:276). Non-pose records carry no + // pose, but that stamp shrinks the dead-reckoner's projection span + // (nextUpdate - lastUpdate) WITHOUT refreshing updateOrigin -- so the + // peer's position target jumps backward toward the stale origin, then the + // next pose record yanks it forward: target oscillation every frame while + // records churn. Keyboard driving churns them CONSTANTLY (the throttle + // lever sweeps every frame of a key-hold -> a type-2 speed record per + // frame), which autodrive's pinned throttle never does -- the 'skips when + // I drive, smooth when you drive' report. Preserve the clock; keep the + // real payload (simulationState). BT_T2_CLOCK restores the old stamp (A/B). + static const int s_t2clock = getenv("BT_T2_CLOCK") ? 1 : 0; + const Time savedLastUpdate = lastUpdate; + Simulation::ReadUpdateRecord(message); + if (!s_t2clock) lastUpdate = savedLastUpdate; + } heatAlarm.SetLevel(record->heatLevel); throttleState = record->throttleState; fallDirection = record->fallDirection; @@ -1929,7 +2150,23 @@ void case 8: // airborne/reverse cycle-rate selector { Mech__AirborneUpdateRecord *record = (Mech__AirborneUpdateRecord *)message; - Simulation::ReadUpdateRecord(message); + { + // CLOCK GUARD (keyboard-skip fix): the base reader stamps lastUpdate=Now() + // on EVERY record ('HACK' per SIMULATE.cpp:276). Non-pose records carry no + // pose, but that stamp shrinks the dead-reckoner's projection span + // (nextUpdate - lastUpdate) WITHOUT refreshing updateOrigin -- so the + // peer's position target jumps backward toward the stale origin, then the + // next pose record yanks it forward: target oscillation every frame while + // records churn. Keyboard driving churns them CONSTANTLY (the throttle + // lever sweeps every frame of a key-hold -> a type-2 speed record per + // frame), which autodrive's pinned throttle never does -- the 'skips when + // I drive, smooth when you drive' report. Preserve the clock; keep the + // real payload (simulationState). BT_T2_CLOCK restores the old stamp (A/B). + static const int s_t2clock = getenv("BT_T2_CLOCK") ? 1 : 0; + const Time savedLastUpdate = lastUpdate; + Simulation::ReadUpdateRecord(message); + if (!s_t2clock) lastUpdate = savedLastUpdate; + } airborneSelect = record->airborne; // @0x3f4 <- rec+0x10 bodyTargetSpeed = record->speedDemand; // rec+0x14 } @@ -2091,6 +2328,24 @@ void } updateOrigin.angularPosition = localOrigin.angularPosition; // FUN_00409968(0x138 <- 0x10c) updateVelocity.angularMotion = localVelocity.angularMotion; // FUN_00408440(0x2d4 <- 0x1d0) + // RE-BASE the master's peer-estimate mirror (2026-07-14, replicant-chop + // fix): projectedOrigin/projectedVelocity model what the PEER will now + // extrapolate from this record. The send gate compares local vs projected + // (the peer's drift); without the re-base the mirror went stale, the gate + // fired EVERY frame, and the per-frame records re-based the replicant into + // the stall/snap chop. (The binary maintains this in the un-decompiled + // master perf; the writer-side re-base is the coherent reconstruction.) + projectedOrigin.angularPosition = localOrigin.angularPosition; + projectedVelocity.angularMotion = localVelocity.angularMotion; + // SCALAR peer-yaw mirror re-base (see mech.hpp): what the peer will now + // extrapolate -- this yaw, at this rate, from this moment. + { + YawPitchRoll _my; _my = localOrigin.angularPosition; + angMirrorYaw = (Scalar)_my.yaw; + angMirrorRate = (Scalar)localVelocity.angularMotion.y; + angMirrorTime = lastPerformance; + angMirrorValid = 1; + } record->speedDemand = speedDemand; // rec+0x28 bodyTargetSpeed = speedDemand; } diff --git a/game/reconstructed/mech.hpp b/game/reconstructed/mech.hpp index 31d4415..291393a 100644 --- a/game/reconstructed/mech.hpp +++ b/game/reconstructed/mech.hpp @@ -299,6 +299,7 @@ struct ShotDescriptor { friend class Mech__DamageZone; // per-zone damage code reads the roster / segment table friend struct MechBaseLayoutCheck; // base-region layout locks (mech4.cpp; P3 STEP-6 audit) + friend void BTCommitLookState(void *, int); // task #68: the look-state commit (mech4.cpp) //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Shared Data Support // @@ -618,6 +619,32 @@ public: Scalar updatePositionDeadband; // binary @0x768 -- type-0 pose trigger Scalar updateTurnVelocityDeadband; // binary @0x76c -- type-4 yaw-rate trigger Scalar updateTurnAngleDeadband; // binary @0x770 -- type-4 orientation trigger (rad) + // SCALAR peer-yaw mirror (port addition, replicant-spin work): the master's + // estimate of the yaw the PEER is currently rendering -- re-based on every + // type-4 send (writer case 4), advanced analytically as base + rate*elapsed. + // Replaces the quaternion projectedOrigin mirror for the ANGLE deadband: that + // one is recomputed by the master's own reckoner each frame from lastUpdate + // timing it does not control, and its false pi-drift waves FLOODED type-4 + // resyncs (measured maxAng~=pi bursts). Scalars, wrap-safe, self-timed. + // Master send-mirror gait travel (BT_MASTER_GAITMIRROR): the invisible mj=0 + // body channel's per-frame cycleDistance, stashed in the drive block and + // consumed by the send gate to advance projectedOrigin the way the PEER + // predicts (gait), instead of the constant-velocity deadReckoner. This is the + // authentic FUN_004a9b5c mirror = IntegrateMotion(mj=0) on projectedOrigin. + Scalar mirrorBodyAdv; // body cycleDistance this frame (master) + // Peer true-mirror cadence: the replicant sets this to the actual replicated + // ground speed (|updateVelocity|) each frame so the body clip advances at the + // master's real rate instead of the locally-slewed bodyCycleSpeed (which drifts + // and makes the animation phase wander -> the accel/decel hesitation). -1 = + // disabled (master/single-player always -1, so the authentic slew runs). + Scalar peerMirrorSpeed; + Scalar angMirrorYaw; // yaw at the last type-4 send (rad) + Scalar angMirrorRate; // yaw rate sent with it (rad/s) + Time angMirrorTime; // lastPerformance at that send + int angMirrorValid; // 0 until the first type-4 send + int angSyncLatch; // peer: type-4 arrived -> re-anchor heading (the + // angular analog of poseSyncLatch; consumed by + // the replicant's incremental heading integrator) // AUTHENTIC GROUND MODEL ctor products (task #15, ground-model-decode; // binary part_012.c:9938-9940 + 9974-9975). By-name access only; declared // after the layout-locked fields so nothing shifts. @@ -667,6 +694,14 @@ public: Logical PickRayHit(const Point3D &start, const Vector3D &dir, Scalar max_range, Point3D *hit_world); + // World-STRUCTURE boresight pick: ray-test the zone's STATIC collision solid + // tree (garages/walls/props -- the geometry that blocks the walk) along the + // boresight and return the entry point of the closest structure hit. The + // authentic non-mech world pick (Mover::FindStaticSolidHitBy, the static-world + // half of the engine's FindBoxedSolidHitBy). Defined in mech.cpp. + Logical WorldStructurePick(const Point3D &start, const Vector3D &dir, + Scalar max_range, Point3D *hit_world); + // Per-weapon beam render walk (task #33; extracted task #51 so it runs for // EVERY mech -- player, dummy masters, and MP replicants whose emitters // carry replicated discharge state). Defined in mech4.cpp. @@ -689,8 +724,22 @@ protected: // protected: BTVal mechName; // @0x360 this[0xd8] (proxy: .Copy) - Scalar maxSpeed; // @0x400 this[0x100] = FLT_MAX - int stateFlags; // @0x410 this[0x104] + // (F7) `Scalar maxSpeed @0x400` was a MISREAD -- the binary attribute + // table names 0x400 DistanceToMissile (id 56; FLT_MAX = far default); + // the member is retired in favor of distanceToMissile below. + // (task #68) REAR-FIRING (binary attr id 50 @0x410; the old name was + // `stateFlags`): OR of every mounted weapon's rearFiring flag -- "this + // mech carries a rear arsenal". The look-back view arms those weapons. + int rearFiring; // @0x410 this[0x104] + // (task #68) the authored look-view angles (model record +0x50..0x5c, + // ctor converts degrees->radians; binary @0x564..0x570 -- previously + // parked in the Wword scratch bank). Consumed by the look-state + // commit: side views yaw by these, look-back = yaw pi + lookBackAngle + // pitch, look-down pitches by lookFrontAngle. + Scalar lookLeftAngle; // @0x564 + Scalar lookRightAngle; // @0x568 + Scalar lookFrontAngle; // @0x56C + Scalar lookBackAngle; // @0x570 int throttleState; // @0x4a4 this[0x129] = 2 (initial) int heatLevel; // @0x518 this[0x146] int heatCapacity; // @0x51c this[0x147] = 0.6 * heatLevel @@ -795,6 +844,73 @@ protected: // forward ground speed the speed readout / arc / radar consume (binary @0x81c). Scalar attrPad; Scalar linearSpeed; + // Real StateIndicators for the animation-state attributes (0x1f/0x20). + // The audio subsystem binds AudioStateWatchers to these BY NAME and calls + // AddAudioWatcher() on them -- so unlike the scalar gauge attrs they CANNOT + // share the read-only attrPad (a Scalar has no watcher chain -> the watcher + // register AV'd on 0xCDCDCDCD). Driven from SetBodyAnimation so footstep/ + // gait sounds fire on animation transitions. [T2] + StateIndicator animationState; // 0x1f AnimationState + StateIndicator replicantAnimationState; // 0x20 ReplicantAnimationState + // 0x16 CollisionState -- audio binds an AudioStateWatcher here too (impact/ + // scrape sounds). Driven from ProcessCollision via collisionTemporaryState: + // NoCollision(0)/InitialHit(1)/Slide(2)/Rest(3) (RP VTV CollisionStateCount). + StateIndicator collisionState; + // 0x1e FootStep -- audio binds an AudioLogicalTrigger (threshold-crossing + // with hysteresis: rise past threshold -> play the step, fall back -> re-arm). + // Backed by a real pulse: SetBodyAnimation raises it 1 on entering a cyclic + // stride clip (each walk/run/reverse alternation == one foot plant) and the + // per-frame motion tick decays it back to 0 a few frames later, giving one + // clean rising edge per step. [T2] + int footStep; // the Logical the matcher polls (1 = contact) + Scalar fwdSpeedFiltered; // smoothed |v| published via localVelocity (the raw + Scalar vertSpeedFiltered; // per-frame position derivative pulses per stride and + // flapped the audio's speed triggers -- engine-hum + // restarts on every footfall) + // (AUDIO_FIDELITY F5) footStep is the authored CONTACT LEVEL, not a + // clip-transition pulse: per frame (leg gait state not 0/1), + // footStep = (jointlocal.y <= *clip footStepThreshold) -- binary + // disasm @0x4a9e80-0x4a9eb6 (leg) / @0x4aba86-0x4abab9 (body); the + // threshold is the ANI header word[2] SelectSequence now captures. + // States 0/1 RETAIN the previous value (binary skip). The old 150 ms + // pulse + decay members are retired. + Joint *footStepRootJoint; // cached "jointlocal" (binary mech+0x5C8) + int footStepRootResolved; + // (AUDIO_FIDELITY F19, corrected) the binary's localAcceleration feed: + // FIVE 15-sample RING BUFFERS (ctor part_012.c:9836-9840, size 0xF + // fill 0) hold the RAW per-frame velocity components + dt; + // localAcceleration = (ringMean - prevRingMean) / ringMean(dt) + // (part_012.c:15169-15195). A moving-window mean JITTERS every frame + // as samples rotate, so the accel attribute changes every watcher + // poll -- which is what keeps the authored N=30 footstep smoother fed + // (an exponential-filter derivative goes epsilon-flat between strides + // and starved it: the 10-20 s footstep warm-up bug). + Scalar velRingFwd[15]; + Scalar velRingVert[15]; + Scalar velRingDt[15]; + int velRingCursor; + Scalar accelPrevFwdMean; + Scalar accelPrevVertMean; + Point3D accelPrevPos; // raw position memory for the ring feed + // (task #66, the gyro-tail INSTABILITY MODEL -- byte-decoded from the + // master perf @0x4aad3d-0x4aaf14 [T1]): + // instab = (min(|AccelerationLastFrame| / maxUnstableAcceleration, 1) + // x unstableAccelerationEffect)^2 + // instab += clamp((demand - legCycleSpeed)/demand, <=1) ["gun the + // engine": commanded speed the legs haven't reached] + // x unstableGunTheEngineEffect + // if (legAnimationState == 4 /*trn*/) instab += unstableStopedTurnEffect + // clamp to 1 -> mech+0x3F0, then gyro->swayBias (gyro+0x3A8) and the + // UnstablePercentage audio attribute (binary id 52 @0x3F0) read it. + // The six tuning fields come from the model record +0x80..0x94 (ctor + // @0x4a2593-0x4a25d5); previously parked in the Wword scratch bank. + Scalar unstablePercentage; // binary @0x3F0 (reset writes 0) + Scalar maxUnstableAcceleration; // @0x784 <- rec+0x80 + Scalar unstableAccelerationEffect; // @0x788 <- rec+0x84 + Scalar unstableGunTheEngineEffect; // @0x78C <- rec+0x88 + Scalar unstableSuperStopEffect; // @0x790 <- rec+0x8C (writer not yet located; airborne perf?) + Scalar unstableHighVelocityEffect; // @0x794 <- rec+0x90 (writer not yet located) + Scalar unstableStopedTurnEffect; // @0x798 <- rec+0x94 // Radar/map gauge attributes (binary @0x404/0x408/0x40c/0x3f8). The map // widget reads position/angle as POINTERS into the mech's live origin, so // radarLinearPosition/radarAngularPosition point at localOrigin.{linear, @@ -803,6 +919,27 @@ protected: Point3D *radarLinearPosition; // 0x30 RadarLinearPosition Quaternion *radarAngularPosition; // 0x31 RadarAngularPosition int duckState; // 0x37 DuckState (crouch posture) + // (AUDIO_FIDELITY F7) the incoming-missile alarm attributes. Binary + // Mech table [T1]: IncomingLock id 54 @0x3fc (Logical; authored match + // ==1 Start / ==0 Stop of the looped beeper), DistanceToMissile id 56 + // @0x400 (Scalar; authored scale maps range 100..800 -> TEMPO 600..10, + // the beep accelerating as the missile closes). The mech reset inits + // them 0 / FLT_MAX (part_012.c:9446-9447); the live writer is in an + // un-exported gap, so the drive is intent-level [T3]: every homing + // Missile reports its target + range per MoveAndCollide tick into the + // *Next accumulators; PerformAndWatch latches them each frame. + int incomingLock; // 0x36 IncomingLock (1 = missile inbound) + Scalar distanceToMissile; // 0x38 DistanceToMissile (FLT_MAX = none) + int incomingLockNext; // accumulator (missiles OR into it) + Scalar distanceToMissileNext; // accumulator (missiles MIN into it) + // (AUDIO_FIDELITY F17) CollisionSpeed (binary id 24 @0x4B4): |impact + // velocity| captured when the contact accumulator arms (0->1) -- the + // authored AttackVolume [0.9,1] / Brightness [0.7,1] scales over + // impact speed [0,25] read it, so impacts scale with how hard you hit. + // ReduceButton (id 46 @0x340): the pod cab's REDUCE button; a real, + // watchable member -- the keyboard rig simply never presses it. + Scalar collisionSpeed; // 0x18 CollisionSpeed + int reduceButton; // 0x2e ReduceButton // --- raw / engine-shim members touched by the ctor slice ------------- void *vtable; // installed Mech vtable ptr @@ -828,6 +965,15 @@ protected: public: int MovementMode() { return (int)GetSimulationState(); } void SetMovementMode(int m) { SetSimulationState((unsigned)m); } + // (AUDIO_FIDELITY F7) called by each homing Missile per MoveAndCollide + // tick; PerformAndWatch latches the accumulators into the published + // IncomingLock/DistanceToMissile attributes each frame. + void ReportIncomingMissile(Scalar range) + { + incomingLockNext = 1; + if (range < distanceToMissileNext) + distanceToMissileNext = range; + } // (the members below were and stay PUBLIC -- the recon TUs // (btplayer/dmgtable/mechmppr/emitter) reach them directly) int movementFlags; @@ -1013,7 +1159,11 @@ protected: int flags; // entity flags word (this+0x28 region) Scalar lastInflictingDamage; // task #60: killing-blow magnitude (port-only; reuses // the retired phantom `stance` slot -- feeds the kill score) - int ammoState; // @0x44c 0 none / 1 leaking / 2 dry + int collisionTemporaryState; // @0x44c per-frame collision accumulator + // (0 none / 1 InitialHit / 2 Slide) -- CORRECTED from + // the "ammoState 0/1 leaking/2 dry" mislabel; the decomp + // (part_012.c:15406-15413) sets it per resolved contact, + // pushed into collisionState each frame. Never read as ammo. AlarmIndicator graphicAlarm; // body graphic-state alarm EntityID lastInflictingID; // @0x43c last attacker (read by DamageZone routing) Logical IsAirborne(); diff --git a/game/reconstructed/mech2.cpp b/game/reconstructed/mech2.cpp index ea85219..898f368 100644 --- a/game/reconstructed/mech2.cpp +++ b/game/reconstructed/mech2.cpp @@ -90,6 +90,10 @@ // #include +#include // AudioComponent -- the foot-plant step-intensity broadcast +#include // AudioSource -- footstep-source identification +#include // AudioResource::GetAudioLevelOfDetail +#include // PatchLevelOfDetail -- bank/patch of the target source #pragma hdrstop #if !defined(MECH_HPP) @@ -239,6 +243,39 @@ void // binary's DAT_0050d700/704). (void *)&Mech::BodyClipFinished, 0, 0); bodyStateAlarm.SetLevel(state); // FUN_0041bbd8(this+0x714, state) + + // Drive the AnimationState indicators so the audio subsystem's state-watchers + // (footstep/gait/transition sounds) fire on every animation change. Guarded to + // the constructed range (stateCount 0x21) so an out-of-range clip can't trip + // StateIndicator::SetState's Verify(state= 0 && state < 0x21) + { + animationState.SetState(state); + replicantAnimationState.SetState(state); + } + + // FootStep pulse: every locomotion-clip transition is one foot plant (the + // stride R<->L alternation -- MEASURED at runtime: forward walking alternates + // body states 12<->13, so the enum-name numbering above does NOT match the + // runtime clip ids; key on the locomotion RANGE instead). Clips 1..0x17 are + // the gait cycle (stand-to-walk through gimp); 0 = standing, >= 0x18 = falls/ + // knockdown/death (those get collision/fall audio, not a step). Raise the + // Logical the FootStep AudioLogicalTrigger polls; IntegrateMotion decays it + // back to 0 a few frames later so each step is a clean rising edge. [T2] + // (AUDIO_FIDELITY F5) the old per-transition footStep PULSE is GONE: + // footStep is the authored CONTACT LEVEL, evaluated per frame in + // Mech::PerformAndWatch from jointlocal.y vs the clip's authored + // threshold (ANI hdr[2]) -- steps now fire at actual root-height + // contact crossings, not at clip boundaries with a fixed width. + // (F19) the old STEP-INTENSITY SEND is also GONE. Its premise ("the + // mixer feed was lost game code") was wrong: the feed is WHOLLY + // AUTHORED -- LocalAcceleration |linear| [0,10] -> ctl100 and + // LocalVelocity |linear| [0,0.6] -> ctl101 scale watchers drive the + // footstep volume mixer (0.4 base while moving + the per-stride + // acceleration kick). The port's gap was never publishing + // localAcceleration; mech4.cpp derives it exactly as the binary does + // (d(averaged velocity)/dt, part_012.c:15186-15195), and the invented + // broadcast fought that live chain. } // @@ -613,11 +650,25 @@ Scalar // cases and the port never per-frame-clears the latch), so gating entry on // it here would wrongly block trn after every walk. Omitted by design; // see locomotion.md "turn-in-place dispatcher". + // REPLICANT accommodation (regression fix, 2026-07-14): the dispatcher + // above is MASTER-perf logic; a replicant feeds this SM DERIVED signals + // (speed/turn from the dead-reckon stream, mech4.cpp:1948) and runs ONLY + // the leg channel. Two master rules break it: + // (1) the body weld -- a replicant never runs the body SM, so arming the + // body to 4 sticks bodyAnimationState there forever and BLOCKS every + // later trn entry (the peer "rotates as a statue"); + // (2) the full [0,standSpeed] entry -- the derived speed sweeps that band + // on every dead-reckoned start/stop with turnDemand pinned +-1, so + // trn keeps arming mid-locomotion and speed-exiting (jerky walking). + // Replicants: no body weld / no body arm (channel is inert, mj=0) and the + // narrow near-zero entry gate (the pre-#64b accommodation). [T3] + const int trnIsRepl = (GetInstance() == ReplicantInstance); + const Scalar trnEntryMax = trnIsRepl ? standSpeed * 0.25f : standSpeed; if (hasCrashSet != 0 && mppr != 0 - && commandedSpeed >= ZeroSpeed && commandedSpeed <= standSpeed + && commandedSpeed >= ZeroSpeed && commandedSpeed <= trnEntryMax && (mppr->turnDemand > 0.05f || mppr->turnDemand < -0.05f) - && bodyAnimationState == StandingAnimation) // weld: enter only when BOTH can + && (trnIsRepl || bodyAnimationState == StandingAnimation)) // weld: masters only { // LOCKSTEP (task #64): arm BOTH channels on the same frame. Arming // only the leg let the body enter walk on its own schedule ~7-20 @@ -631,7 +682,8 @@ Scalar // (the authentic dispatcher in the un-decompiled 0x4a9b5c gap arms // both -- the body's case-4/finish machinery is dead code otherwise). SetLegAnimation(4); // turn-in-place (trn), channel A - SetBodyAnimation(4); // channel B, same frame [lockstep] + if (!trnIsRepl) + SetBodyAnimation(4); // channel B, same frame [lockstep, masters] goto advance_normally; } if (ZeroSpeed <= commandedSpeed) @@ -693,8 +745,17 @@ Scalar distance = 0.0f; break; } - if (mppr == 0 - || (mppr->turnDemand <= 0.05f && mppr->turnDemand >= -0.05f)) + // MASTER ONLY (regression fix): the master-perf turn-stop exit is master-perf + // logic (FUN_004a9b5c runs on MasterInstance mechs, NOT replicants). A replicant + // derives turnDemand from the REPLICATED yaw rate (mech4.cpp:1968) -- a noisy + // proxy that dips into the deadband between dead-reckon updates; running this + // exit on it kicked the peer out of trn every few frames -> the peer "rotated as + // a statue" + jerky (user-reported). Gate to masters; a replicant leaves trn via + // the speed exits + its own turnDemand-driven re-entry, exactly as it did before + // this exit was added. + if (GetInstance() != ReplicantInstance + && (mppr == 0 + || (mppr->turnDemand <= 0.05f && mppr->turnDemand >= -0.05f))) { legStateAlarm.SetLevel(0); // turn stopped -> Standing (master-perf exit) ForceUpdate(8); @@ -932,8 +993,9 @@ Scalar distance = 0.0f; break; } - if (bm == 0 - || (bm->turnDemand <= 0.05f && bm->turnDemand >= -0.05f)) + if (GetInstance() != ReplicantInstance // MASTER only (see leg twin) + && (bm == 0 + || (bm->turnDemand <= 0.05f && bm->turnDemand >= -0.05f))) { bodyStateAlarm.SetLevel(0); // turn stopped (leg-symmetric) ForceUpdate(8); @@ -974,6 +1036,9 @@ Scalar if (bodyCycleSpeed > bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed; if (bodyCycleSpeed > walkStrideLength) bodyCycleSpeed = walkStrideLength; } + if (peerMirrorSpeed >= 0.0f) // peer: cadence == actual mirrored ground speed + bodyCycleSpeed = (peerMirrorSpeed < standSpeed) ? standSpeed + : ((peerMirrorSpeed > walkStrideLength) ? walkStrideLength : peerMirrorSpeed); distance = bodyAnimation.Advance( time_slice * (bodyCycleSpeed / walkStrideLength) * globalTimeScale, loop); break; @@ -994,6 +1059,9 @@ Scalar if (bodyCycleSpeed > bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed; if (bodyCycleSpeed > reverseSpeedMax2) bodyCycleSpeed = reverseSpeedMax2; } + if (peerMirrorSpeed >= 0.0f) // peer: cadence == actual mirrored ground speed + bodyCycleSpeed = (peerMirrorSpeed < reverseSpeedMax) ? reverseSpeedMax + : ((peerMirrorSpeed > reverseSpeedMax2) ? reverseSpeedMax2 : peerMirrorSpeed); distance = bodyAnimation.Advance( time_slice * (bodyCycleSpeed / reverseStrideLength) * globalTimeScale, loop); break; @@ -1134,6 +1202,9 @@ Scalar if (bodyCycleSpeed > bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed; if (bodyCycleSpeed > walkStrideLength) bodyCycleSpeed = walkStrideLength; } + if (peerMirrorSpeed >= 0.0f) // peer: cadence == actual mirrored ground speed + bodyCycleSpeed = (peerMirrorSpeed < standSpeed) ? standSpeed + : ((peerMirrorSpeed > walkStrideLength) ? walkStrideLength : peerMirrorSpeed); distance = bodyAnimation.Advance( time_slice * (bodyCycleSpeed / walkStrideLength) * globalTimeScale, loop); break; @@ -1154,6 +1225,9 @@ Scalar if (bodyCycleSpeed > bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed; if (bodyCycleSpeed > reverseSpeedMax2) bodyCycleSpeed = reverseSpeedMax2; } + if (peerMirrorSpeed >= 0.0f) // peer: cadence == actual mirrored ground speed + bodyCycleSpeed = (peerMirrorSpeed < reverseSpeedMax) ? reverseSpeedMax + : ((peerMirrorSpeed > reverseSpeedMax2) ? reverseSpeedMax2 : peerMirrorSpeed); distance = bodyAnimation.Advance( time_slice * (bodyCycleSpeed / reverseStrideLength) * globalTimeScale, loop); break; diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 8254360..f95db34 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -292,6 +292,9 @@ Logical // Choose this frame's forward-cycle slew rate. forwardCycleRate = airborneSelect ? airborneCycleRate : groundCycleRate; // 0x344<-0x5bc/0x5b8 + // (FootStep pulse decay moved to PerformAndWatch -- this integrator does not + // run on the current gait path, so a decay here never fired.) + if (IsDisabled()) // FUN_0049fb54 { ReconQuatIdentity(&angularAccum, &kIdentityQuat); // was raw this+0x298 (stomped projectedVelocity) @@ -648,6 +651,7 @@ static int gBTPPCKey = 0; static int gBTMissileKey = 0; static int gBTPinkyKey = 0; // key '4' = the pod's 4th fire button (Pinky 0x45) int gBTModeCycle = 0; // 'M' edge: cycle the control mode (mapper consumes) +int gBTLookBehind = 0; // 'V' held: the pod's rear-view button (task #68) float gBTTwistAxis = 0.0f; // Q/E torso-twist deflection (assisted-mode stick X) int gBTTorsoRecenter = 0; // 'X' edge: pulse the authentic torso recenter (mapper consumes) static int gBTConfigKey = 0; // task #6: HOLD 'G' = the weapon-configure button @@ -1072,7 +1076,7 @@ void void BTPushProjectile(const Point3D &muzzle, void *shooter, void *target, const Point3D &targetPos, Scalar speed, Scalar damage, const Vector3D *launch_velocity, int guided, - int weapon_subsys, int splash_burst) + int weapon_subsys, int splash_burst, int muzzle_seg) { // MUZZLE (muzzle wave, 2026-07-12): the passed muzzle is now the weapon's // AUTHENTIC mount segment (GetMuzzlePoint reads the real segmentIndex -- @@ -1134,20 +1138,50 @@ void if (launch_velocity != 0 && shooter != 0) { // FRAME CONVENTION (telemetry-verified 2026-07-12): the authored - // MuzzleVelocity is (0,0,+100) -- the launcher frame's +Z is - // FORWARD, but the mech's body frame faces -Z. Transforming +Z - // through the body basis launched every round BACKWARD (then the - // seeker looped it through the ground onto the target -- - // user-reported). Map launcher-forward (+Z) onto body-forward - // (-Z basis): negate the z term. + // MuzzleVelocity is (0,0,+100) with a NEGATED z when composed -- + // the binary's own fire builder does `local_40 -= MV.z` + // (FUN_004bcc60 @part_013.c:8761) [T1]. + // TORSO-TWIST FIX (task #67, user-reported "missiles fire out of + // his back"): rotate through the WEAPON MOUNT SEGMENT's world + // frame, not the body basis. The MadCat's racks ride the TORSO: + // the muzzle POINT tracked the twist (segment-resolved) while the + // launch DIRECTION followed the LEGS -- twist far enough and + // rounds left backward. The binary spawns the missile with the + // full muzzle-segment frame (GetMuzzlePoint fills an AffineMatrix + // into the spawn descriptor, @0x4bcc60:8762-8764) and the dumb + // seeker aims 100u ahead of the MISSILE's own frame -- segment + // orientation IS the authentic launch direction. Mech *sm = (Mech *)shooter; UnitVector ax, ay, az; - sm->localToWorld.GetFromAxis(X_Axis, &ax); - sm->localToWorld.GetFromAxis(Y_Axis, &ay); - sm->localToWorld.GetFromAxis(Z_Axis, &az); + int haveFrame = 0; + if (muzzle_seg >= 0) + { + EntitySegment *seg = sm->GetSegment(muzzle_seg); + if (seg != 0) + { + AffineMatrix mw; + mw.Multiply(seg->GetSegmentToEntity(), sm->localToWorld); + mw.GetFromAxis(X_Axis, &ax); + mw.GetFromAxis(Y_Axis, &ay); + mw.GetFromAxis(Z_Axis, &az); + haveFrame = 1; + } + } + if (!haveFrame) + { + sm->localToWorld.GetFromAxis(X_Axis, &ax); + sm->localToWorld.GetFromAxis(Y_Axis, &ay); + sm->localToWorld.GetFromAxis(Z_Axis, &az); + } p.vel.x = ax.x*launch_velocity->x + ay.x*launch_velocity->y - az.x*launch_velocity->z; p.vel.y = ax.y*launch_velocity->x + ay.y*launch_velocity->y - az.y*launch_velocity->z; p.vel.z = ax.z*launch_velocity->x + ay.z*launch_velocity->y - az.z*launch_velocity->z; + // (task #67) inherit the shooter's motion: the binary adds the + // mech's localVelocity into the spawn velocity (FUN_004b9cbc = + // owner+0x1c4 read, composed @0x4bcc60:8758-8761) [T1]. + p.vel.x += sm->worldLinearVelocity.x; + p.vel.y += sm->worldLinearVelocity.y; + p.vel.z += sm->worldLinearVelocity.z; Scalar lv = (Scalar)sqrtf(p.vel.x*p.vel.x + p.vel.y*p.vel.y + p.vel.z*p.vel.z); if (lv > 1.0f) { p.speed = lv; haveLaunch = 1; } } @@ -1863,6 +1897,9 @@ void } } +// Peer heading published each sim update, sampled by the render loop (BT_RENDHDG). +volatile float gBTReplRenderYaw = -999.0f; + void Mech::PerformAndWatch(const Time& till, MemoryStream *update_stream) { @@ -1927,9 +1964,100 @@ void { if (dt > 0.0001f && dt < 0.5f) { - DeadReckon(dt); // engine: reckoner + lerp + // AUTHENTIC PEER HEADING (decomp FUN_004ab188/FUN_00409f58 via FUN_004ab1c8): + // the original replicant integrates its heading INCREMENTALLY from the + // CURRENT pose -- compose an exact rotation of (replicated yaw rate * dt) + // onto the rendered heading each frame -- and re-anchors it when a type-4 + // resync lands. It does NOT slerp toward a projected angular target: the + // engine Mover::DeadReckon lerp uses the shared lastUpdate/nextUpdate + // timebase, which the DENSE type-0 pose stream resets every frame while + // walking while RESTORING a stale orientation (the authentic case-0 strip) + // -- so the angular projection barely advances from a stale base and the + // slerp DRAGS the heading back each frame. Measured: walking+turning, the + // peer yaw advanced at ~40% rate with half the frames stepping BACKWARD + // (the keyboard-steering skip); pure spin/walk never showed it (one stream). + // Save the heading, let DeadReckon own the LINEAR channel, then override + // the angular result with the original's incremental integration. + // ROOT-MOTION peer POSITION (authentic FUN_004ab1c8: the clip's travel + // drives the body between records; pose records re-anchor). Save the + // position so the engine velocity-lerp result can be discarded below + // (BT_DR_POS=1 restores the velocity dead-reckon for A/B). + // DEFAULT: engine velocity dead-reckon (best-measured: ratio 1.0022). The + // coupled root-motion experiment (BT_ROOT_POS=1) needs the MASTER-side + // gait mirror to stop the authority tug-of-war (measured ratio 1.86) -- + // the completion path is mapped in the session notes. + static const int s_drPos = getenv("BT_DR_POS") ? 1 : 0; // default 0 = authentic gait-coupled linear (BT_DR_POS=1 restores velocity) + Point3D replPrevPos = localOrigin.linearPosition; + Quaternion replPrevHeading = localOrigin.angularPosition; + DeadReckon(dt); // engine reckoner: LINEAR position/velocity + if (angSyncLatch) + { + // type-4 arrived: re-anchor on the authoritative orientation + localOrigin.angularPosition = updateOrigin.angularPosition; + angSyncLatch = 0; + } + else + { + Vector3D replAngStep; + replAngStep.Multiply(updateVelocity.angularMotion, dt); + ReconQuatIntegrate(&localOrigin.angularPosition, + &replPrevHeading, &replAngStep); // exact FUN_00409f58 + } + localOrigin.angularPosition.Normalize(); + if (!s_drPos) + localOrigin.linearPosition = replPrevPos; // root-motion mode: gait applies travel below localToWorld = localOrigin; + // RENDER-vs-SIM decoupling probe: publish the heading the RENDER will + // actually draw (localToWorld == localOrigin here). The render loop + // (L4VIDEO.cpp) logs this PER RENDERED FRAME; if it repeats the same + // value across render frames, the peer sim updates slower than the + // render draws -> the rotation stutters even though each sim update is + // smooth (which the per-update [replhdg] metric could not see). + { + extern volatile float gBTReplRenderYaw; + YawPitchRoll _rry; _rry = localOrigin.angularPosition; + gBTReplRenderYaw = (float)_rry.yaw; + } + + // Lightweight dead-reckon smoothness probe (one flush/sec, cheap). + // PATH length (sum of |per-frame step|) vs NET displacement reveals + // jitter: a smooth walk has path==net; oscillation inflates path while + // net stays small. avgHorizon is the lerp target distance in time. + if (getenv("BT_JIT")) + { + static bool s_jinit = false; + static Point3D s_jprev, s_jnetA; + static float s_jacc = 0.0f, s_jpath = 0.0f, s_jmax = 0.0f, s_jhzn = 0.0f; + static int s_jfr = 0; + if (s_jinit) + { + const float dx = (float)(localOrigin.linearPosition.x - s_jprev.x); + const float dz = (float)(localOrigin.linearPosition.z - s_jprev.z); + const float step = sqrtf(dx * dx + dz * dz); + s_jpath += step; + if (step > s_jmax) s_jmax = step; + } + else { s_jnetA = localOrigin.linearPosition; s_jinit = true; } + s_jprev = localOrigin.linearPosition; + s_jhzn += (float)(nextUpdate - lastPerformance); + s_jfr++; s_jacc += dt; + if (s_jacc >= 1.0f) + { + const float ndx = (float)(localOrigin.linearPosition.x - s_jnetA.x); + const float ndz = (float)(localOrigin.linearPosition.z - s_jnetA.z); + const float net = sqrtf(ndx * ndx + ndz * ndz); + DEBUG_STREAM << "[repljit] frames=" << s_jfr + << " path=" << s_jpath << " net=" << net + << " ratio=" << (net > 0.001f ? s_jpath / net : 0.0f) + << " maxStep=" << s_jmax << " avgStep=" << (s_jpath / s_jfr) + << " avgHorizon=" << (s_jhzn / s_jfr) + << "\n" << std::flush; + s_jacc = 0.0f; s_jpath = 0.0f; s_jmax = 0.0f; s_jhzn = 0.0f; + s_jfr = 0; s_jnetA = localOrigin.linearPosition; + } + } + // REPLICANT GAIT (task #50): animate the peer mech's legs at the // REPLICATED speed -- without this the replicant slides around as a // frozen statue. The master publishes worldLinearVelocity in every @@ -1946,27 +2074,81 @@ void MechControlsMapper *replMppr = MappingMapper(); // roster slot 0 (task #7) if (!IsMechDestroyed() && replMppr != 0) { - const Vector3D &wv = updateVelocity.linearMotion; - float spd = sqrtf((float)(wv.x * wv.x + wv.z * wv.z)); - UnitVector zAxR; - localToWorld.GetFromAxis(Z_Axis, &zAxR); - const float fdot = -((float)wv.x * (float)zAxR.x - + (float)wv.z * (float)zAxR.z); // mech faces -Z - replMppr->speedDemand = (fdot < 0.0f) ? -spd : spd; - // REPLICANT TURN-STEP (user-reported: a turning peer - // statue-rotates while the local mech steps): the leg SM's - // Standing case arms the turn-in-place "trn" clip (state 4, - // mech2.cpp:590) from mppr->turnDemand -- which nothing fed on - // a replicant, so pivots never stepped. Feed it from the - // REPLICATED yaw rate (updateVelocity.angularMotion.y, stored - // by Mover::ReadUpdateRecord from the master's localVelocity - // -- the same stream DeadReckon rotates the body with). Only - // the +-0.05 threshold matters to the SM (the trn clip - // advances at fixed cadence), so a signed unit demand - // suffices; the deadband ignores dead-reckon jitter. + // AUTHENTIC LEG-CHANNEL DEMAND (decomp-verified, workflow wv1km7lvc D3): + // the leg channel FUN_004a5028 reads its speed demand from + // mapper->speedDemand == **(mech+0x128)+0x128 (part_012.c:11947/11975/ + // 12028) and slews cadence + selects the stand->walk gate from it. On a + // peer this MUST be the REPLICATED COMMANDED speed bodyTargetSpeed@0x6b4 -- + // the master's own throttle-commanded speedDemand (mechmppr.cpp:749), + // stamped into every record (mech.cpp:2036, record->speedDemand) and read + // back on the peer (mech.cpp type-0 @1833, type-2 @1841/1848, type-3 @1871). + // It is the IDENTICAL value the master's leg channel consumes, so the peer + // clears the stand->walk gate exactly when the master does (the commanded + // speed is what makes the master walk, so it clears standSpeed by + // construction) and ramps cadence CONTINUOUSLY through accel/decel -- + // cadence tracks travel like the master. bodyTargetSpeed is signed + // (negative == reverse); it is 0 only while COASTING (thr=0), which + // authentically winds the gait down to stand, matching the master. + // + // The prior default DERIVED a noisy speed from the dead-reckoned velocity + // and PINNED it flat with a standSpeed*1.05 floor across the walk threshold + // -- the CONFIRMED leg-stutter/skip + cadence-vs-travel desync. Retained + // behind BT_REPL_VEL for A/B comparison only. + static const int s_replVel = getenv("BT_REPL_VEL") ? 1 : 0; + if (s_replVel) + { + const Vector3D &wv = updateVelocity.linearMotion; + float spd = sqrtf((float)(wv.x * wv.x + wv.z * wv.z)); + UnitVector zAxR; + localToWorld.GetFromAxis(Z_Axis, &zAxR); + const float fdot = -((float)wv.x * (float)zAxR.x + + (float)wv.z * (float)zAxR.z); // mech faces -Z + Scalar sd = (fdot < 0.0f) ? -spd : spd; + if (sd > standSpeed * 0.5f && sd < standSpeed * 1.05f) + sd = standSpeed * 1.05f; + replMppr->speedDemand = sd; + } + else + { + replMppr->speedDemand = bodyTargetSpeed; // authentic replicated commanded speed + } + // AUTHENTIC PEER TURNING is NET-DRIVEN (decomp-verified D8): the angular + // slerp inside DeadReckon (MOVER.cpp:521-525) rotates + // localOrigin.angularPosition toward projectedOrigin.angularPosition from + // the replicated yaw rate (decomp angular path part_012.c:15001/15010). + // The leg SM's turn-in-place 'trn' clip is a MASTER-ONLY construct fed by + // the LIVE controls mapper; the old synthetic yawRate->turnDemand + // (quantized +-0.02) flickered the trn clip in/out as the noisy replicated + // rate crossed the band -> the jerky pivot / statue-rotate. Do NOT + // synthesize it -- let the whole body rotate via the slerp. + // TURN-STEP (presentation of the authentic pivot): the authentic peer + // shows a stepping pivot because its BODY channel replays the master's + // replicated turn body-STATE (type-3 SetBodyAnimation, mech.cpp:1869). + // We run the LEG channel (D2 not switched), which has no replicated turn + // state, so we reproduce the visible turn-step by arming the leg SM's + // 'trn' clip from the replicated yaw rate. The old feed used a bare + // +-0.02 threshold that CHATTERED the trn clip in/out as the noisy rate + // wandered near zero (the "jerky pivot", decomp-noted D8). HYSTERESIS + // fixes that: enter the turn only above +-0.08 rad/s, hold it (the + // mapper's turnDemand persists frame-to-frame -- we are its only writer on + // a peer) until the rate clearly collapses below 0.03. A steady turn sits + // well above both, so the clip never flickers. (BT_REPL_NOTURN = + // decomp-strict net-driven rotation with NO step.) const float yawRate = (float)updateVelocity.angularMotion.y; - replMppr->turnDemand = (yawRate > 0.02f) ? 1.0f - : (yawRate < -0.02f) ? -1.0f : 0.0f; + static const int s_noTurn = getenv("BT_REPL_NOTURN") ? 1 : 0; + if (s_noTurn) + { + replMppr->turnDemand = 0.0f; + } + else + { + const float prevTd = (float)replMppr->turnDemand; // last frame (our own write) + if (prevTd != 0.0f) // turning: hold until the rate collapses + replMppr->turnDemand = (fabsf(yawRate) < 0.03f) ? 0.0f : prevTd; + else // idle: start only on a clear turn + replMppr->turnDemand = (yawRate > 0.08f) ? 1.0f + : (yawRate < -0.08f) ? -1.0f : 0.0f; + } // Prime the same clip-advance scalars the master's gait block sets // each frame -- uninitialized on a replicant they read 0, freezing // the clip at advance-time dt*0 (observed: legState engaged at 11, @@ -1986,7 +2168,305 @@ void // once so a direct run-state entry can't slew from -4.3e8. if (legCycleSpeed < -100.0f || legCycleSpeed > 200.0f) legCycleSpeed = 0.0f; - (void)AdvanceLegAnimation(dt); // joints only; travel = DeadReckon + // PEER POSE CHANNEL (decomp workflow w1s9ou02o -- the authentic fix): the + // 1995 peer posed the WHOLE skeleton from the BODY channel (FUN_004a5678) + // FOLLOWING the master's replicated body-anim state (bodyStateAlarm@0x728, + // set by the type-3 reader's SetBodyAnimation, mech.cpp:1913) + replicated + // bodyTargetSpeed@0x6b4. The LEG channel (FUN_004a5028) is DEAD CODE. Our + // port resurrected the leg SM as the peer's poser fed a LOCALLY re-derived + // commanded speed, and the leg SM's phase-independent pre-switch wind-down + // (mech2.cpp:560-568, force-jump {6,7,8,9}->stand when legCycleSpeed<=0) makes + // the peer SKIP the master's decel state 8 and snap to stand while the body + // coasts (the 'slid forward after legs stopped' bug). The body channel has + // NO such wind-down and follows the replicated decel state, playing the + // master's exact clips. Swapping the peer to the body channel RESTORES the + // original single-channel design (no new netcode). BT_PEER_LEGCH=1 = old. + static const int s_peerLegCh = getenv("BT_PEER_LEGCH") ? 1 : 0; + // TRUE-MIRROR CADENCE: drive the body clip from the ACTUAL replicated ground + // speed so the animation phase tracks the master exactly instead of drifting + // via the local bodyCycleSpeed slew (the accel/decel hesitation). Consumed in + // AdvanceBodyAnimation walk/reverse. -1 disables -> authentic slew. + if (!s_peerLegCh && !getenv("BT_NO_MIRROR_CAD")) + { + const Vector3D &mcv = updateVelocity.linearMotion; + peerMirrorSpeed = (Scalar)sqrtf((float)(mcv.x * mcv.x + mcv.z * mcv.z)); + } + else + peerMirrorSpeed = -1.0f; + // TURN-STEP ARMING for the body channel: the body Standing case only enters + // walk/reverse/stand -- it never arms the turn-in-place clip (state 4); on the + // master that arming comes from the LEG Standing case cross-arming both + // channels (mech2.cpp:937 note), which the peer no longer runs. Arm/exit the + // body turn state here from the replicated turn (replMppr->turnDemand, from the + // replicated yaw rate + hysteresis above): standing + turning -> state 4 (body + // case 4 then advances the trn clip -> the peer STEPS through the pivot); turn + // stops -> back to Standing. Walk/reverse transitions still own their exits. + if (!s_peerLegCh) + { + const int bs = (int)bodyStateAlarm.GetLevel(); + const bool turning = (replMppr->turnDemand > 0.05f + || replMppr->turnDemand < -0.05f); + if (turning && bs == 0) // standing + turning -> enter turn + SetBodyAnimation(4); + else if (!turning && bs == 4) // turn stopped -> back to stand + SetBodyAnimation(0); + } + const Scalar replLegAdv = s_peerLegCh + ? AdvanceLegAnimation(dt) // old: re-derived leg SM + : AdvanceBodyAnimation(dt, 1); // authentic: body channel, mj=1 poses skeleton + // AUTHENTIC coupled position (mirror of the master's world-step at + // mech4.cpp:3325-3336 == Mech::IntegrateMotion tail @004ab1c8): between + // records the body advances by the CLIP'S OWN travel rotated by the + // heading -- feet and ground are locked BY CONSTRUCTION, so cadence-vs- + // travel mismatch (the speed-change glitch) is structurally impossible. + // A pose record re-anchors onto the authoritative position + // (poseSyncLatch, armed by every type-0 read). adv is SIGNED by the SM + // (reverse clips return negative travel; master dir==1.0 in SM mode). + if (!s_drPos) + { + // POSE-SYNC ERROR-PROPORTIONAL ABSORPTION: track the error to the + // received authority (updateOrigin) and bleed it into localOrigin at a + // rate that scales with the error, so a small steady offset is absorbed + // gently (no foot-pop) while a large speed-change drift is caught fast. + // The residual snap came from two sources: (a) a fixed slow ~1/3s decay + // let drift reach 3-11u during accel/decel; (b) a hard ground snap popped + // because records are not perfectly dense on this relay. Error-scaled + // absorption is the stable middle: k grows from ~0.15/frame at small + // error toward ~0.6 at large error. (Records re-measure the error every + // frame they arrive, so this converges to authority within a few frames.) + { + Vector3D rmErr; + rmErr.Subtract(updateOrigin.linearPosition, localOrigin.linearPosition); + poseSyncLatch = 0; + const float eMag = (float)rmErr.Length(); + float k = 0.15f + eMag * 0.15f; // error-proportional catch-up + if (k > 0.6f) k = 0.6f; + // DIAG (BT_SNAPLOG, task #50): the peer drift = |authority - dead-reckon| + // absorbed this frame -- the "snap" magnitude. Log > 0.5u so an accel/decel + // soak (BT_DRIVE_SWEEP0) measures the residual. + static const int s_snapLog = getenv("BT_SNAPLOG") ? 1 : 0; + if (s_snapLog && eMag > 0.5f) + DEBUG_STREAM << "[snap] drift=" << eMag << " k=" << k + << " legAdv=" << replLegAdv << "\n" << std::flush; + // AT-REST snap (user "gliding stops"): when the legs have wound down + // (replLegAdv ~0), gliding the body to absorb the residual offset is + // VISIBLE because no leg motion masks it. With the feet planted a + // sub-unit correction is invisible, so resolve it immediately instead + // of gliding it in over several frames. Only when the offset is small + // (a large offset at rest = a real teleport that should still ease). + const Logical atRest = ((replLegAdv < 0.05f && replLegAdv > -0.05f) + && eMag < 1.0f); + if (atRest) k = 1.0f; + localOrigin.linearPosition.x += rmErr.x * k; + localOrigin.linearPosition.y += rmErr.y * k; + localOrigin.linearPosition.z += rmErr.z * k; + } + if (replLegAdv != 0.0f) + { + Vector3D rmLocal(0.0f, 0.0f, -replLegAdv); // forward = -Z, per-frame DISTANCE + Matrix34 rmOrient; + Matrix34::FromQuaternion(&rmOrient, &localOrigin.angularPosition); + Vector3D rmWorld; + FUN_00408744(&rmWorld, (Scalar *)&rmLocal, &rmOrient); + localOrigin.linearPosition.x += rmWorld.x; + localOrigin.linearPosition.y += rmWorld.y; + localOrigin.linearPosition.z += rmWorld.z; + } + localToWorld = localOrigin; // refresh with the coupled position + } + + // SLIDE probe (BT_SLIDE): per-frame, log when the peer position MOVES + // while its legs are in STAND (legState 0) -- the user's "sliding after + // legs stopped". Reports move magnitude, offset to authority, leg state. + if (getenv("BT_SLIDE")) + { + static Point3D s_slPrev; static int s_slInit = 0; + const int ls = getenv("BT_PEER_LEGCH") ? (int)legStateAlarm.GetLevel() : (int)bodyStateAlarm.GetLevel(); + if (s_slInit) + { + const float dx = (float)(localOrigin.linearPosition.x - s_slPrev.x); + const float dz = (float)(localOrigin.linearPosition.z - s_slPrev.z); + const float mv = sqrtf(dx * dx + dz * dz); + if (ls == 0 && mv > 0.02f) // standing but moving = slide + { + Vector3D off; off.Subtract(updateOrigin.linearPosition, localOrigin.linearPosition); + DEBUG_STREAM << "[slide] move=" << mv + << " legAdv=" << replLegAdv + << " offToAuth=" << (float)off.Length() + << " legState=" << ls << "\n" << std::flush; + } + } + s_slPrev = localOrigin.linearPosition; s_slInit = 1; + } + + // PEER-DRIFT probe (BT_MIRDIV, 1s max): how far the peer's coupled + // position wanders from the last-received authority (updateOrigin) + // before a record re-anchors it -- the other end of the snap. + if (getenv("BT_MIRDIV")) + { + static float s_pdAcc = 0.0f, s_pdMax = 0.0f; + Vector3D d; d.Subtract(localOrigin.linearPosition, updateOrigin.linearPosition); + const float dl = (float)d.Length(); + if (dl > s_pdMax) s_pdMax = dl; + s_pdAcc += dt; + if (s_pdAcc >= 1.0f) + { + DEBUG_STREAM << "[peerdrift] maxDriftFromAuthority=" << s_pdMax + << " replLegAdv=" << replLegAdv << "\n" << std::flush; + s_pdAcc = 0.0f; s_pdMax = 0.0f; + } + } + + // GAIT-EVENNESS probe (BT_GAITEV, 1s stats): the metric of the VISIBLE + // speed-change glitch -- per-frame leg-clip advance regularity + leg-state + // transition (flap) rate on the peer. Position dead-reckons smoothly + // regardless; the churn shows in the LEGS. + if (getenv("BT_GAITEV")) + { + static float s_gvPrevF = -1.0f, s_gvAcc = 0.0f, s_gvSum = 0.0f, s_gvMax = 0.0f; + static int s_gvN = 0, s_gvBack = 0, s_gvTrans = 0, s_gvPrevSt = -1; + static float s_gvDemPrev = 0.0f; static int s_gvDemChg = 0; + const int _pb = getenv("BT_PEER_LEGCH") ? 1 : 0; + const float gf = _pb ? (float)legAnimation.currentFrame : (float)bodyAnimation.currentFrame; + const int gs = _pb ? (int)legStateAlarm.GetLevel() : (int)bodyStateAlarm.GetLevel(); + if (s_gvPrevF >= 0.0f) + { + float df = gf - s_gvPrevF; + if (df < -5.0f) df += 20.0f; // clip wrap heal (approx) + const float adf = (df < 0.0f) ? -df : df; + s_gvSum += adf; if (adf > s_gvMax) s_gvMax = adf; + if (df < 0.0f) s_gvBack++; + s_gvN++; + } + if (s_gvPrevSt >= 0 && gs != s_gvPrevSt) s_gvTrans++; + if ((float)bodyTargetSpeed != s_gvDemPrev) { s_gvDemChg++; s_gvDemPrev = (float)bodyTargetSpeed; } + s_gvPrevF = gf; s_gvPrevSt = gs; s_gvAcc += dt; + if (s_gvAcc >= 1.0f && s_gvN > 0) + { + DEBUG_STREAM << "[gaitev] frmAvg=" << (s_gvSum / s_gvN) + << " frmMax=" << s_gvMax + << " back=" << s_gvBack << "/" << s_gvN + << " stateFlips/s=" << s_gvTrans + << " demChanges/s=" << s_gvDemChg + << " state=" << gs << "\n" << std::flush; + s_gvAcc = 0.0f; s_gvSum = 0.0f; s_gvMax = 0.0f; s_gvN = 0; s_gvBack = 0; s_gvTrans = 0; s_gvDemChg = 0; + } + } + + // TURN-STEP rate probe (BT_TRNRATE, 0.5s): body rotation rate vs trn clip advance. + if (getenv("BT_TRNRATE")) + { + static bool s_tinit = false; + static float s_tYaw = 0.0f, s_tFrm = 0.0f, s_tAcc = 0.0f, s_tRot = 0.0f, s_tAdv = 0.0f; + YawPitchRoll _typ; _typ = localOrigin.angularPosition; + const float yaw = (float)_typ.yaw, frm = (float)legAnimation.currentFrame; + if (s_tinit) + { + float dy = yaw - s_tYaw; if (dy > 3.14159f) dy -= 6.28319f; if (dy < -3.14159f) dy += 6.28319f; + s_tRot += (dy < 0.0f) ? -dy : dy; + const float df = frm - s_tFrm; + if (df > 0.0f) s_tAdv += df; + } + s_tYaw = yaw; s_tFrm = frm; s_tinit = true; + s_tAcc += dt; + if (s_tAcc >= 0.5f) + { + DEBUG_STREAM << "[trnrate] bodyRot/s=" << (s_tRot / s_tAcc) << "rad" + << " legFrm/s=" << (s_tAdv / s_tAcc) + << " legState=" << (int)legStateAlarm.GetLevel() + << " turnDemand=" << MappingMapper()->turnDemand << "\n" << std::flush; + s_tAcc = 0.0f; s_tRot = 0.0f; s_tAdv = 0.0f; + } + } + + // Cadence-vs-travel sync probe (0.25s, ~4 flush/s -- cheap). Compares + // the leg CADENCE input (bodyTargetSpeed / legCycleSpeed) against the + // ACTUAL dead-reckoned ground speed; if the legs are in sync the two + // track together and legFrm advances monotonically. + if (getenv("BT_ANIM")) + { + static bool s_ainit = false; + static Point3D s_aprev; + static float s_aacc = 0.0f, s_apath = 0.0f; + if (s_ainit) + { + const float dx = (float)(localOrigin.linearPosition.x - s_aprev.x); + const float dz = (float)(localOrigin.linearPosition.z - s_aprev.z); + s_apath += sqrtf(dx * dx + dz * dz); + } + s_aprev = localOrigin.linearPosition; s_ainit = true; + s_aacc += dt; + if (s_aacc >= 0.25f) + { + DEBUG_STREAM << "[anim] cmd=" << bodyTargetSpeed + << " drSpeed=" << (s_apath / s_aacc) + << " legCyc=" << legCycleSpeed + << " legState=" << (int)legStateAlarm.GetLevel() + << " legFrm=" << legAnimation.currentFrame + << "\n" << std::flush; + s_aacc = 0.0f; s_apath = 0.0f; + } + } + + if (getenv("BT_REPL_TRN")) + { + static float s_rt = 0.0f; s_rt += dt; + if (s_rt >= 0.15f) + { + s_rt = 0.0f; + DEBUG_STREAM << "[repltrn] yawRate=" << yawRate + << " turnDemand=" << replMppr->turnDemand + << " legState=" << (int)legStateAlarm.GetLevel() + << " bodyState=" << (int)bodyStateAlarm.GetLevel() + << " legFrm=" << legAnimation.currentFrame + << " bodyFrm=" << bodyAnimation.currentFrame + << " spd=" << replMppr->speedDemand + << " tgt=" << bodyTargetSpeed << "\n" << std::flush; + } + } + // PER-FRAME heading trace (BT_REPL_HDG, choppy-spin diagnosis): the + // replicant's rendered yaw each frame + dt. A smooth dead-reckoned + // spin advances ~yawRate*dt every frame; choppiness shows as heading + // STALLS followed by SNAPS (records arriving without integration + // between them). + if (getenv("BT_REPL_HDG") && yawRate != 0.0f) + { + YawPitchRoll rh, ph, uh; + rh = localOrigin.angularPosition; + ph = projectedOrigin.angularPosition; + uh = updateOrigin.angularPosition; + DEBUG_STREAM << "[replhdg] yaw=" << (Scalar)rh.yaw + << " proj=" << (Scalar)ph.yaw + << " upd=" << (Scalar)uh.yaw + << " lp-lu=" << (lastPerformance.ticks - lastUpdate.ticks) + << " nu-lp=" << (nextUpdate.ticks - lastPerformance.ticks) + << " yawRate=" << yawRate << " dt=" << dt << "\n" << std::flush; + } + + // PER-FRAME travel/animation coupling probe (BT_REPL_MOV, user: + // "slides forward before it steps"): position vs the dead-reckon target + // vs the derived speed vs the leg SM state, every frame there is + // replicated motion. Reveals travel-vs-animation lag + lerp-gap jitter. + if (getenv("BT_REPL_MOV")) + { + const Scalar uvx = (Scalar)updateVelocity.linearMotion.x; + const Scalar uvz = (Scalar)updateVelocity.linearMotion.z; + const Scalar umag = (Scalar)sqrtf(uvx*uvx + uvz*uvz); + if (umag > 0.01f || replMppr->speedDemand != 0.0f) + DEBUG_STREAM << "[replmov]" + << " pos=(" << localOrigin.linearPosition.x << "," + << localOrigin.linearPosition.z << ")" + << " proj=(" << projectedOrigin.linearPosition.x << "," + << projectedOrigin.linearPosition.z << ")" + << " uvel=" << umag + << " spd=" << replMppr->speedDemand + << " legState=" << (int)legStateAlarm.GetLevel() + << " legFrm=" << legAnimation.currentFrame + << " standSp=" << standSpeed + << " walkStr=" << walkStrideLength + << " nu-lp=" << (nextUpdate.ticks - lastPerformance.ticks) + << " dt=" << dt << "\n" << std::flush; + } // REPLICANT BEAMS (task #51): the emitters carry live replicated // discharge state (Emitter::ReadUpdateRecord); draw them with the @@ -2127,6 +2607,10 @@ void const int mNow = focused && (pAsync('M') & dn) ? 1 : 0; if (mNow && !sPrevM) gBTModeCycle = 1; // edge -> one cycle sPrevM = mNow; + // (task #68) 'V' HELD = look behind (the pod's rear-view + // button; releases back to the forward view). Rear-mounted + // weapons (blackhawk/owens back racks) fire only in it. + gBTLookBehind = focused && (pAsync('V') & dn) ? 1 : 0; const int tw = (focused && (pAsync('E') & dn) ? 1 : 0) - (focused && (pAsync('Q') & dn) ? 1 : 0); static float sTwist = 0.0f; @@ -2271,6 +2755,35 @@ void : (r < 1.0f ? sAutoDrive * r : sAutoDrive); } } + // BT_DRIVE_SWEEP=: triangle-sweep the FORCED throttle + // 0.2..0.9 -- sweeps the ACTUAL drive velocity AND (via mechmppr's + // forced key_throttle) the mapper demand together, the true + // keyboard accel/decel regime. BT_FORCE_STEP=1 quantizes it to + // the RIO Ranger 0.05 grid (the authentic stepped input shape). + { + static float s_dsP = -1.0f, s_dsClock = 0.0f; + static int s_dsStep = -1; + if (s_dsP < 0.0f) + { + const char *v = getenv("BT_DRIVE_SWEEP"); + s_dsP = v ? (float)atof(v) : 0.0f; + s_dsStep = getenv("BT_FORCE_STEP") ? 1 : 0; + } + if (s_dsP > 0.0f) + { + s_dsClock += dt; + float ph = fmodf(s_dsClock, 2.0f * s_dsP) / s_dsP; + float tri = (ph < 1.0f) ? ph : (2.0f - ph); + // BT_DRIVE_SWEEP0=1: sweep 0.0..0.7 (through STOP each cycle -- the + // stand<->walk gauntlet: walk-entry, came-to-rest sends, type-3 stomps). + static int s_dsZero = -1; + if (s_dsZero < 0) s_dsZero = getenv("BT_DRIVE_SWEEP0") ? 1 : 0; + float thr = s_dsZero ? (0.7f * tri) : (0.2f + 0.7f * tri); + if (s_dsStep) + thr = floorf(thr / 0.05f + 0.5f) * 0.05f; + gBTDrive.forcedThrottle = thr; + } + } } else if (sGotoDrive) { @@ -2297,6 +2810,45 @@ void const int fwd = gBTDrive.keyFwd, back = gBTDrive.keyBack; if (!fwd && !back) sDetent = 0; // keys released -> detent re-arms + // GAIT DETENT (keyboard accommodation, 2026-07-16): the gait SM has NO + // stable state for a demand between the walk cycle's cap + // (walkStrideLength @0x534) and the run cycle's engage speed + // (reverseSpeedMax @0x538 = the walk->run transition clip's exit + // speed). A demand PARKED in that band hunts walk -> shift-up -> + // shift-down forever, firing the authored EngineShiftFwd/Rev sounds + // each swing (binary-verified: the BodyClipFinished thresholds at + // 0x4a6f11/0x4a6fc7 + the continuous FUN_004afd10 demand formula + // reproduce the hunt with all-authentic data). The pod's PHYSICAL + // throttle lever plausibly rested only at mechanical notches -- the + // software path is notch-free (pod-hardware.md), so the notches, if + // real, lived in the quadrant hardware [T4]. Reproduce that feel: + // when the keys REST, snap the lever out of the dead band to the + // nearer edge. Sweeping THROUGH the band while a key is held stays + // continuous -- that is an authentic moving lever, and the one + // transition it triggers is the authentic shift. + if (!fwd && !back && sLever > 0.0f + && walkStrideLength > 0.0f + && reverseSpeedMax > walkStrideLength + && reverseStrideLength >= reverseSpeedMax) + { + const float max_demand = reverseStrideLength * forwardThrottleScale; + if (max_demand > 0.0f) + { + const float band_lo = walkStrideLength / max_demand; + // margin: the run cont-check is tgt >= cap, so land the demand + // a hair ABOVE the engage speed against float rounding + const float band_hi = reverseSpeedMax / max_demand + 0.002f; + if (sLever > band_lo && sLever < band_hi) + { + const float snapped = + (sLever - band_lo < (band_hi - band_lo) * 0.5f) ? band_lo : band_hi; + { static int s_dLog = 0; if (getenv("BT_GAIT_TRACE") && s_dLog++ < 40) + DEBUG_STREAM << "[gaitdetent] lever " << sLever << " in dead band [" + << band_lo << "," << band_hi << ") -> " << snapped << "\n" << std::flush; } + sLever = snapped; + } + } + } float sweep = ((fwd ? 1.0f : 0.0f) - (back ? 1.0f : 0.0f)) * kLeverRate * dt; if (sweep != 0.0f && !sDetent) { @@ -2329,8 +2881,37 @@ void else sStick = 0.0f; } - gBTDrive.throttle = sLever; - gBTDrive.turn = sStick; + // RIO-GRID PUBLISH (authentic input shaping; decomp workflow w0odszxro): + // the pod's analog lever/stick reached the mapper through the RIO Ranger + // (800 ADC counts, 0.05 deadband -- L4RIO.cpp:801), so the DEMAND the 1995 + // machinery consumed moved in coarse, settled STEPS with silence between + // them -- one type-2 speed record per step, and each gait threshold + // (standSpeed etc., zero hysteresis) crossed ONCE per maneuver. Our + // keyboard integrator sweeps CONTINUOUSLY (kLeverRate every frame of a + // key-hold): ~86 records per accel, demand DWELLING on the knife edges -> + // transition-clip churn -- the speed-change peer glitch. Publish on the + // Ranger's own 0.05 grid: internal lever feel unchanged, published demand + // steps like the real hardware (a bucket holds ~4 frames at kLeverRate, so + // the send gate self-closes between steps). Also kills -epsilon reverse + // publishes from rest (bucket rounds to 0 until the lever passes -0.025; + // authentic reverse is a BUTTON sign-flip, never a sweep through zero). + // DEFAULT: CONTINUOUS publish (authentic -- the pod lever was analog, + // 800-count ADC ~= continuous; empirical A/B found NO gait/position + // difference between swept and stepped demand after the clock-guard + + // heading-integrator fixes, so the grid gains nothing and makes the + // DRIVE step unauthentically). BT_GRID_LEVER=1 keeps the 0.05-grid + // variant for experiments. + static const int s_gridLever = getenv("BT_GRID_LEVER") ? 1 : 0; + if (s_gridLever) + { + gBTDrive.throttle = floorf(sLever / 0.05f + 0.5f) * 0.05f; + gBTDrive.turn = floorf(sStick / 0.05f + 0.5f) * 0.05f; + } + else + { + gBTDrive.throttle = sLever; + gBTDrive.turn = sStick; + } } // DEATH GATE (task #52): a DESTROYED mech takes no pilot input -- kill @@ -2376,6 +2957,13 @@ void s_spawnAt = 2; localOrigin.linearPosition.x = s_sx; localOrigin.linearPosition.z = s_sz; + // FIX (user-reported: teleport embeds the mech in sloped terrain): + // BT_SPAWN_AT set only X/Z, keeping the OLD (dropzone) Y -- wrong for + // the new ground height. Lift the mech WELL ABOVE local terrain so the + // authentic per-frame ground SNAP (BoxTree FindBoundingBoxUnder, no + // gravity) settles it onto the surface next frame. The snap only + // lowers, so it must start above the surface, not below. + localOrigin.linearPosition.y += 500.0f; if (s_haveH) { gDriveHeading = s_sh; @@ -2780,7 +3368,21 @@ void // while the animation transitioned gradually (the "physics don't // line up with the animation" desync). Signed, the SM decelerates // through stop -> reverse-entry exactly like the leg channel. - if (s_realControls && MappingMapper() != 0) + // BT_MASTER_GAITMIRROR: the invisible mj=0 body channel becomes the + // PEER's predictor, so it must slew toward the LAST-SENT commanded + // speed (which the record writers stamp into bodyTargetSpeed@0x6b4 on + // each send, mech.cpp), NOT the live mapper demand. Leaving + // bodyTargetSpeed at last-sent re-activates the authentic type-2 + // deadband and makes the mirror model exactly what the peer computes. + // The master's VISIBLE motion uses the LEG channel reading live demand + // independently, so this does not change what the local player sees. + // (Default OFF: live overwrite preserved.) + static const int s_gaitMirror = getenv("BT_NO_MASTER_GAITMIRROR") ? 0 : 1; // default ON (authentic coupled) + if (s_gaitMirror) + { + /* leave bodyTargetSpeed at the last-sent value */ + } + else if (s_realControls && MappingMapper() != 0) bodyTargetSpeed = MappingMapper()->speedDemand; else bodyTargetSpeed = ((throttle < 0.0f) ? -1.0f : 1.0f) @@ -2847,6 +3449,7 @@ void // BT_BODY_MJ=1 restores the old double-writer for A/B. static const int s_bodyMj = BTEnvOn("BT_BODY_MJ", 0); adv = AdvanceBodyAnimation(dt, s_bodyMj); // channel B: replication projection (mj=0) + mirrorBodyAdv = adv; // stash for the send-mirror gait advance legAdv = AdvanceLegAnimation(dt); // channel A: local sim -- pose + travel } else @@ -2990,6 +3593,21 @@ void // impact freezes travel as before.) const Scalar travelAdv = s_realControls ? legAdv : adv; const Scalar localAdv = travelAdv * dir; + // MASTER-SLIDE probe (BT_SLIDE): does the MASTER itself advance while its + // legs are in STAND (legState 0)? If yes -> authentic momentum coast, the + // peer correctly mirrors it. If the master travelAdv reaches 0 exactly + // when legState reaches 0 -> the master stops clean and the peer's stand- + // slide is a peer leg-SM-too-fast desync. Logs the decel profile. + if (getenv("BT_SLIDE")) + { + const int mls = (int)legStateAlarm.GetLevel(); + const float ta = (float)(travelAdv < 0 ? -travelAdv : travelAdv); + if (ta > 0.005f) // master still moving + DEBUG_STREAM << "[mslide] travelAdv=" << travelAdv + << " legState=" << mls + << " legCycle=" << legCycleSpeed + << " thr=" << throttle << "\n" << std::flush; + } linearSpeed = (localAdv < 0.0f ? -localAdv : localAdv) * invDt; // forward ground speed -> LinearSpeed gauge Vector3D localVel(0.0f, 0.0f, -localAdv * invDt); // exact frame distance as velocity Matrix34 orient; // rotation from the heading (set @ line ~626) @@ -3112,9 +3730,54 @@ void Scalar fwdSpeed = (Scalar)sqrtf( worldLinearVelocity.x * worldLinearVelocity.x + worldLinearVelocity.z * worldLinearVelocity.z); - localVelocity.linearMotion = Vector3D(0.0f, worldLinearVelocity.y, -fwdSpeed); + // SMOOTH the PUBLISHED velocity (localVelocity feeds the audio speed + // triggers, the update records, and the impact gate -- NOT the physics + // worldLinearVelocity above). The raw per-frame derivative pulses per + // stride (gait acceleration + ground-snap bob): at walking speed the + // oscillation straddled the authored idle-hum band edge (speed 20) and + // Start/Stop-flapped the engine ambience on every footfall (heard as a + // static tick per step). ~0.25s exponential filter kills the stride + // ripple while tracking real speed changes within a couple of strides. + // AUTHENTIC [T1]: the binary's Mech::Execute (FUN_004ab430) runs the + // localVelocity components through AverageOf running-average filters + // (part_012.c:15169-15179) -- the original smoothed the published + // velocity the same way; this filter reconstructs that stage. + { + Scalar alpha = dt / (dt + 0.25f); + fwdSpeedFiltered += alpha * (fwdSpeed - fwdSpeedFiltered); + vertSpeedFiltered += alpha * (worldLinearVelocity.y - vertSpeedFiltered); + // (AUDIO_FIDELITY F19) publish localAcceleration EXACTLY as the + // binary does: FIVE 15-sample RING BUFFERS hold the RAW velocity + // components + dt (ctor part_012.c:9836-9840), and the accel is + // the frame-to-frame derivative of the ring MEAN over the mean + // dt (part_012.c:15169-15195). The ring mean keeps jittering + // as samples rotate, so the accel ATTRIBUTE changes on every + // watcher poll -- feeding the authored N=30 footstep smoother + // continuously (ctl100 |accel| [0,10] -> the per-stride kick on + // top of ctl101's 0.4 moving base). A first cut derived accel + // from the 0.25s-filtered velocity: its derivative goes + // epsilon-flat between strides, the watcher stopped firing, and + // the smoother took 10-20 s to warm from its fill=0 (the + // late-footsteps bug). NOTE: the RAW components feed only the + // rings; the PUBLISHED velocity keeps the 0.25s filter (a [T3] + // accommodation for our ground-snap ripple, see above). + // (the ring push itself lives in the PerformAndWatch tail so it + // provably runs EVERY frame -- this conditional path skips + // frames in some gait states, which starved the accel watcher) + } + localVelocity.linearMotion = Vector3D(0.0f, vertSpeedFiltered, -fwdSpeedFiltered); localVelocity.angularMotion = Vector3D(0.0f, turn * authTurnRate, 0.0f); + // MASTER twin of [replmov]: the velocity this mech PUBLISHES vs its own + // leg state -- compare the master ramp to the replicant response. + if (getenv("BT_REPL_MOV") && (fwdSpeed > 0.01f || throttle != 0.0f)) + DEBUG_STREAM << "[mastervel] fwdSpeed=" << fwdSpeed + << " thr=" << throttle + << " legState=" << (int)legStateAlarm.GetLevel() + << " pos=(" << localOrigin.linearPosition.x << "," + << localOrigin.linearPosition.z << ")" + << " dt=" << dt << "\n" << std::flush; + // CRASH motion suppression (spec: the binary zeroes localVelocity while // crashed). While the stagger clip (legState 0x20) plays, freeze the // velocity so a knocked-down mech does not creep back into the obstacle and @@ -3319,11 +3982,68 @@ void // the PEER's event queue busy, so its CreatingMission->LoadingMission // quiet-timeout never fired -- the pods deadlocked pre-launch. The real // system never moves before launch, so the gate is faithful.) - (this->*deadReckoner)(); + // SEND-MIRROR advance: model where the PEER believes this mech is, so the + // deadband fires exactly when the peer would drift. Authentic (binary + // FUN_004a9b5c @0x4aab9c): advance projectedOrigin by IntegrateMotion(mj=0) + // -- the SAME gait predictor the peer runs -- fed the LAST-SENT commanded + // speed. Our default mirror uses the engine constant-velocity deadReckoner + // (AcceleratedDeadReckoner), which does NOT model a gait-driven peer -> the + // deadband under-sends and the coupled peer drifts (measured). With + // BT_MASTER_GAITMIRROR: increment projectedOrigin.linear by the mj=0 body + // channel's travel (mirrorBodyAdv, = the peer's predicted stride) rotated by + // the mirror heading, and integrate the heading by the last-sent angular + // velocity. Re-seeded to localOrigin on each send by the record writers' + // projectedOrigin re-base, so this is incremental-from-last-send, exactly the + // peer's IntegrateMotion. (Default OFF: constant-velocity deadReckoner.) + static const int s_gaitMirrorSend = getenv("BT_NO_MASTER_GAITMIRROR") ? 0 : 1; // default ON + if (s_gaitMirrorSend) + { + Vector3D mAng; + mAng.Multiply(updateVelocity.angularMotion, dt); // last-sent yaw rate + ReconQuatIntegrate(&projectedOrigin.angularPosition, + &projectedOrigin.angularPosition, &mAng); + projectedOrigin.angularPosition.Normalize(); + if (mirrorBodyAdv != 0.0f) + { + Vector3D mLocal(0.0f, 0.0f, -mirrorBodyAdv); // forward = -Z, frame distance + Matrix34 mOrient; + Matrix34::FromQuaternion(&mOrient, &projectedOrigin.angularPosition); + Vector3D mWorld; + FUN_00408744(&mWorld, (Scalar *)&mLocal, &mOrient); + projectedOrigin.linearPosition.x += mWorld.x; + projectedOrigin.linearPosition.y += mWorld.y; + projectedOrigin.linearPosition.z += mWorld.z; + } + } + else + { + (this->*deadReckoner)(); + } Vector3D error; error.Subtract( projectedOrigin.linearPosition, localOrigin.linearPosition); + // MIRROR-DIVERGENCE probe (BT_MIRDIV, 1s max): how far the master's + // send-mirror (projectedOrigin, body-channel gait) drifts from the true + // localOrigin (leg-channel gait) BEFORE a send fires. This is the source + // of the peer's residual snap -- quantifies the leg-vs-body channel shape + // mismatch. posDb (~0.55u) is the send threshold; anything the mirror + // over/under-shoots beyond it is the channel divergence. + if (getenv("BT_MIRDIV")) + { + static float s_mdAcc = 0.0f, s_mdMax = 0.0f, s_mdAdvB = 0.0f, s_mdAdvL = 0.0f; + const float e = (float)error.Length(); + if (e > s_mdMax) s_mdMax = e; + s_mdAdvB += (float)(mirrorBodyAdv < 0 ? -mirrorBodyAdv : mirrorBodyAdv); + s_mdAcc += dt; + if (s_mdAcc >= 1.0f) + { + DEBUG_STREAM << "[mirdiv] maxErr=" << s_mdMax + << " bodyTravel/s=" << s_mdAdvB + << " bodyTgtSpd=" << bodyTargetSpeed << "\n" << std::flush; + s_mdAcc = 0.0f; s_mdMax = 0.0f; s_mdAdvB = 0.0f; s_mdAdvL = 0.0f; + } + } Quaternion angular_deviation; angular_deviation.Subtract( projectedOrigin.angularPosition, @@ -3334,12 +4054,65 @@ void { Scalar posDb = (updatePositionDeadband > 0.0f) ? updatePositionDeadband : 0.04f; + // LINEAR "came-to-rest" trigger -- the symmetric analog of the + // angular (live yaw-rate==0 && replicated yaw-rate!=0) resync + // trigger in the type-4 gate below. The position deadband alone + // does NOT catch a stop: once the master halts, its localOrigin is + // pinned and its OWN dead-reckon projection is re-based to it each + // frame, so error->0 and no pose record fires -- leaving the peer + // holding the last WALKING velocity (updateVelocity), which the + // replicant keeps dead-reckoning + animating as a phantom walk + // until the 2s heartbeat. Fire a pose record the frame the live + // horizontal speed collapses while the last-SENT speed was still + // non-zero, so the replicant gets velocity=0 immediately. + const Scalar liveSpdSq = + (Scalar)(localVelocity.linearMotion.x * localVelocity.linearMotion.x + + localVelocity.linearMotion.z * localVelocity.linearMotion.z); + const Scalar sentSpdSq = + (Scalar)(updateVelocity.linearMotion.x * updateVelocity.linearMotion.x + + updateVelocity.linearMotion.z * updateVelocity.linearMotion.z); + const Logical cameToRest = (liveSpdSq < 0.01f && sentSpdSq > 0.01f); + // DENSE MOTION SEND (replicant-smoothness): the position deadband + // fires IRREGULARLY (only after ~0.55u of drift), but the replicant's + // dead-reckoner predicts the next record one PAST-interval ahead and + // lerps its localOrigin toward that fixed horizon target -- so when the + // deadband cadence != the predicted cadence, the peer decelerates + // toward a stale target then lurches when a record finally lands + // (measured: dead-reckoned ground speed surging 5<->27 while the master + // walks a steady 6.13). Sending a pose record EVERY frame while moving + // makes the cadence regular + dense (records ~1 frame apart), so the + // horizon prediction matches and the lerp tracks smoothly. On a LAN + // this is a handful of small packets/frame -- negligible. + // Dense while moving: with the gait-driven peer, the master's velocity + // mirror no longer models the peer, so the deadband under-sends and the + // peer diverges (measured 11u snaps). Dense records keep the correction + // offset tiny; the peer absorbs it via the authentic pose-sync decay. + static const int s_denseTx = getenv("BT_NO_DENSE_TX") ? 0 : 1; + const Logical moving = (liveSpdSq > 0.25f); if ( error.LengthSquared() > posDb || lastPerformance - lastUpdate > 2.0f + || cameToRest + || (s_denseTx && moving) ) { ForceUpdate(); // type 0: pose record + // RE-SEED the gait mirror to the just-sent pose: the peer will now + // extrapolate from THIS localOrigin, so the mirror must too. (The + // type-0 writer only re-bases projectedOrigin in the sim-state-2 + // transition, not the normal walk case, so do it here.) + if (s_gaitMirrorSend) + { + projectedOrigin.linearPosition = localOrigin.linearPosition; + projectedOrigin.angularPosition = localOrigin.angularPosition; + } + if (getenv("BT_WIRE")) + DEBUG_STREAM << "[tx0] errSq=" << error.LengthSquared() + << " posDb=" << posDb + << " hb=" << (float)(lastPerformance - lastUpdate) + << (cameToRest ? " REST" : "") + << " lvLin=(" << localVelocity.linearMotion.x << "," + << localVelocity.linearMotion.z << ")\n" << std::flush; } } // ORIENTATION rides ONLY the type-4 resync record (the authentic @@ -3353,20 +4126,74 @@ void // UpdateTurnVelocityDiffrence. [T1 expressions; zero-deadband // guard falls back to the old quat-w stand-in] { + // SCALAR PEER-YAW MIRROR (replaces the quaternion projectedOrigin mirror): + // the peer renders yaw ~= angMirrorYaw + angMirrorRate*(now - angMirrorTime) + // (re-based by the type-4 writer on every send, mech.cpp case 4). The old + // quaternion mirror was ALSO recomputed each frame by the master's own + // reckoner from lastUpdate timing it does not control, so it drifted in + // slow pi-waves and FLOODED angle resyncs in bursts (measured maxAng~=pi) + // -- the walk+turn record churn. Scalars: wrap-safe, self-timed, exact + // for the constant-rate spin the peer's exact integrator reproduces. Scalar angDb = (updateTurnAngleDeadband > 0.0f) ? updateTurnAngleDeadband : -1.0f; Scalar velDb = (updateTurnVelocityDeadband > 0.0f) ? updateTurnVelocityDeadband : -1.0f; Logical resync = False; + int rzn = 0; // 1=angleDrift 2=velDrift 4=cameToRestAng 8=mirrorInvalid + Scalar angDrift = 0.0f; + if (angMirrorValid) + { + YawPitchRoll _yprL; + _yprL = localOrigin.angularPosition; + const Scalar mirrorYaw = angMirrorYaw + + angMirrorRate * (Scalar)(lastPerformance - angMirrorTime); + Scalar _dyaw = (Scalar)_yprL.yaw - mirrorYaw; + while (_dyaw > 3.14159265f) _dyaw -= 6.28318531f; + while (_dyaw < -3.14159265f) _dyaw += 6.28318531f; + angDrift = (_dyaw < 0.0f) ? -_dyaw : _dyaw; + } + else + { + resync = True; rzn |= 8; // no baseline yet -> establish one + } + // Compare the live yaw rate against the LAST-SENT rate (updateVelocity, + // which the type-4 writer copies straight from localVelocity, mech.cpp:2107) + // -- NOT projectedVelocity, which the master's dead-reckoner recomputes in a + // different representation/sign so the scalar-.y compare always read the full + // 2x mismatch and FLOODED a resync every frame (measured: byVel=55/55, + // velDrift=2.618=2x the 1.309 spin rate). The flood reset the peer's + // dead-reckon horizon every frame, pinning its slerp at ~half rate -> the + // "rotates slow then jumps" spin. updateVelocity is the value the peer + // actually extrapolates with, in the same units, so a steady spin now drifts + // 0 (no resync) and the peer extrapolates smoothly; a real rate change still + // diverges past velDb and fires. + // NB: the Abs() macro (STYLE.H:118) is UNPARENTHESIZED -- + // Abs(a-b) mis-expands to (a-b>0 ? a-b : -a-b) == -(a+b) on the false + // branch, NOT |a-b|. For a steady spin (localVy==updVy) that yields + // -(2*rate): on a reverse spin it is +2*rate and FLOODED a resync every + // frame (the confirmed root of the half-rate/freeze). Diff into a temp and + // take an explicit abs so the macro only ever sees a single token. + const Scalar velDiff = (Scalar)localVelocity.angularMotion.y - (Scalar)updateVelocity.angularMotion.y; + const Scalar velDrift = (velDiff < 0.0f) ? -velDiff : velDiff; + // ANGULAR SIGN hunt (BT_ANGSIGN, 0.2s): instantaneous gate values, to + // catch which of local/update/projected angular-Y carries the wrong sign. + if (getenv("BT_ANGSIGN")) + { + static float s_as = 0.0f; s_as += dt; + if (s_as >= 0.2f) + { + s_as = 0.0f; + DEBUG_STREAM << "[angsign] localVy=" << (Scalar)localVelocity.angularMotion.y + << " updVy=" << (Scalar)updateVelocity.angularMotion.y + << " projVy=" << (Scalar)projectedVelocity.angularMotion.y + << " velDrift=" << velDrift << " angDrift=" << angDrift + << " turn=" << turn << "\n" << std::flush; + } + } if (angDb > 0.0f) { - if (Abs(localOrigin.angularPosition.y - - projectedOrigin.angularPosition.y) > angDb) - resync = True; - if (Abs((Scalar)localVelocity.angularMotion.y - - (Scalar)projectedVelocity.angularMotion.y) > velDb) - resync = True; + if (angDrift > angDb) { resync = True; rzn |= 1; } + if (velDrift > velDb) { resync = True; rzn |= 2; } if ((Scalar)localVelocity.angularMotion.y == 0.0f - && (Scalar)updateVelocity.angularMotion.y != 0.0f) - resync = True; + && (Scalar)updateVelocity.angularMotion.y != 0.0f) { resync = True; rzn |= 4; } } else if (Abs(angular_deviation.w) < 0.997f) { @@ -3376,6 +4203,27 @@ void { ForceUpdate(1 << MechResyncUpdateModelBit); // type 4 } + // SPIN diagnosis (BT_SPIN, once/sec): how DENSE are the orientation + // (type-4) records while turning? A stuttery peer spin == too sparse. + if (getenv("BT_SPIN")) + { + static float sAcc = 0.0f; static int sResync = 0, sFrames = 0, sAng = 0, sVel = 0, sRest = 0; + static float sMaxAng = 0.0f, sMaxVel = 0.0f; + sAcc += dt; sFrames++; if (resync) sResync++; + if (rzn & 1) sAng++; if (rzn & 2) sVel++; if (rzn & 4) sRest++; + if (angDrift > sMaxAng) sMaxAng = angDrift; + if (velDrift > sMaxVel) sMaxVel = velDrift; + if (sAcc >= 1.0f) + { + DEBUG_STREAM << "[spin-tx] resyncs=" << sResync << "/" << sFrames + << " byAngle=" << sAng << " byVel=" << sVel << " byRest=" << sRest + << " maxAng=" << sMaxAng << "(db" << angDb << ")" + << " maxVel=" << sMaxVel << "(db" << velDb << ")" + << " liveYaw=" << (Scalar)localVelocity.angularMotion.y + << " updYaw=" << (Scalar)updateVelocity.angularMotion.y << "\n" << std::flush; + sAcc = 0.0f; sResync = 0; sFrames = 0; sAng = 0; sVel = 0; sRest = 0; sMaxAng = 0.0f; sMaxVel = 0.0f; + } + } } // Commanded-speed deadband (binary @0x4aac88): the mapper's live // speedDemand vs the last-replicated bodyTargetSpeed -> the tiny @@ -3613,6 +4461,39 @@ void } } + // WORLD-STRUCTURE SWEEP PROBE (BT_WSWEEP, task #50 verification): once a + // second cast a horizontal fan of rays from the eyepoint against the + // STATIC collision solid tree (WorldStructurePick -> FindStaticSolidHitBy) + // and log the hit count + nearest range. Confirms arena1's structures + // (garages/walls) ARE in the collision tree the boresight now queries -- + // independent of the render sentinel or interactive aim. Diag, off by default. + if (getenv("BT_WSWEEP")) + { + static float s_ws = 0.0f; s_ws += dt; + if (s_ws >= 1.0f) + { + s_ws = 0.0f; + Point3D eye = localOrigin.linearPosition; eye.y += 30.0f; + int hits = 0; float nearest = 1e30f; float ndir = -1.0f; + for (int a = 0; a < 24; ++a) + { + double th = (double)a * (6.2831853071795862 / 24.0); + Vector3D d((float)cos(th), 0.0f, (float)sin(th)); + Point3D hp; + if (WorldStructurePick(eye, d, 2000.0f, &hp)) + { + ++hits; + float dx = hp.x-eye.x, dz = hp.z-eye.z; + float r = (float)Sqrt(dx*dx + dz*dz); + if (r < nearest) { nearest = r; ndir = (float)(th * 57.29578); } + } + } + DEBUG_STREAM << "[wsweep] eye=(" << eye.x << "," << eye.z + << ") struct-hits=" << hits << "/24 nearest=" + << (hits ? nearest : -1.0f) << " dir=" << ndir << "deg\n" << std::flush; + } + } + float rs[3], rd[3]; if (!BTGetAimRay(gBTAimX, gBTAimY, rs, rd)) { @@ -3644,16 +4525,62 @@ void hotPoint = hp; } } - if (hotTarget != 0) + // WORLD-STRUCTURE pick (task #50): ray-test the STATIC collision + // solid tree (garages/walls/props -- the geometry that already + // BLOCKS the mech's walk) along the boresight. A structure CLOSER + // than the mech hit occludes it (the shot stops at the wall); with + // no mech, a structure hit IS the designation. This is the + // authentic non-mech world pick: Mech::WorldStructurePick -> + // Mover::FindStaticSolidHitBy, the "test against the static world" + // half of the engine's own FindBoxedSolidHitBy (MOVER.cpp) -- so a + // shot lands on exactly the geometry the walk collides against + // (the user's "structures block the mech, but weapons pass through"). + Point3D structPoint; + float structDist = 1e30f; + bool haveStruct = false; + if (gBTTerrainEntity != 0) { - pickTarget = hotTarget; + Point3D sp; + if (WorldStructurePick(rayStart, rayDir, 1200.0f, &sp)) + { + float dx = sp.x-rs[0], dy = sp.y-rs[1], dz = sp.z-rs[2]; + structDist = dx*dx + dy*dy + dz*dz; + structPoint = sp; + haveStruct = true; + static float s_wp = 0.0f; s_wp += dt; + if (getenv("BT_GROUND_LOG") && s_wp >= 1.0f) + { + s_wp = 0.0f; + DEBUG_STREAM << "[wpick] structure hit at (" + << sp.x << "," << sp.y << "," << sp.z << ") dist=" + << (float)Sqrt(structDist) + << (hotTarget ? " (mech also under boresight)" : "") + << "\n" << std::flush; + } + } + } + + if (hotTarget != 0 && (!haveStruct || bestDist <= structDist)) + { + pickTarget = hotTarget; // mech is the closest hit pickPoint = hotPoint; ++gAimHits; } + else if (haveStruct) + { + // a world structure is the closest hit (garage/wall) -- or it + // occludes a mech behind it. Designate it as the non-mech world + // pick (the geometry rides in the POINT; the sentinel entity has + // no damage-zone table -> no lock ring, HudSim part_013.c:5620). + pickTarget = gBTTerrainEntity; + pickPoint = structPoint; + ++gAimGround; + } else { - // the ground downrange of the guns (max = the pod's HUD - // range scale; past it the shot is a sky shot -> no target) + // no mech, no structure -> the flat ground downrange of the + // guns (max = the pod's HUD range scale; past it the shot is a + // sky shot -> no target) extern bool BTGroundRayHit(float,float,float, float,float,float, float, float*,float*,float*); float hx, hy, hz; @@ -3685,13 +4612,69 @@ void MECH_TARGET_SUBIDX(this) = -1; MECH_TARGET_POS(this) = pickPoint; // beam endpoint = the pick } + else if (gBTTerrainEntity != 0) + { + // FIRE-AT-NOTHING (authentic, decomp part_013.c:7727): the pod fired + // freely at empty space -- the discharge gate is 0x388!=0 (the boresight + // always DESIGNATES a point, not a manual mech lock). When nothing pickable + // is hit (a level shot flies over the flat ground, or open sky), designate a + // MAX-RANGE point using the world sentinel so the weapon discharges to max + // range. Sentinel has no damage-zone table -> no aimed-zone damage, no lock + // ring (mechs only), per HudSimulation part_013.c:5620. + MECH_TARGET_ENTITY(this) = gBTTerrainEntity; + MECH_TARGET_SUBIDX(this) = -1; + Point3D farPt; + farPt.x = rs[0] + rd[0] * 1200.0f; // 1200 = HUD range default (0x44960000) + farPt.y = rs[1] + rd[1] * 1200.0f; + farPt.z = rs[2] + rd[2] * 1200.0f; + MECH_TARGET_POS(this) = farPt; + targetReticle.targetEntity = gBTTerrainEntity; + targetReticle.rayIntersection = farPt; + } else { - MECH_TARGET_ENTITY(this) = 0; // sky: no target, no discharge + MECH_TARGET_ENTITY(this) = 0; // no world sentinel -> no target MECH_TARGET_SUBIDX(this) = -1; } } + // DETERMINISTIC FIRE-AT-STRUCTURE TEST (BT_FIRE_AT_STRUCT, task #50 verify): + // the headless aim ray (BTGetAimRay) is flaky without a focused window, so + // this bypasses it -- designate the NEAREST world structure (360 sweep) as + // the target every frame so BT_AUTOFIRE can be tested against a real, stable, + // in-range building. Diag only, off by default. + if (getenv("BT_FIRE_AT_STRUCT")) + { + extern Entity *gBTTerrainEntity; + Point3D eye = localOrigin.linearPosition; eye.y += 30.0f; + Point3D best; float bestR = 1e30f; bool got = false; + for (int a = 0; a < 24; ++a) + { + double th = (double)a * (6.2831853071795862 / 24.0); + Vector3D d((float)cos(th), 0.0f, (float)sin(th)); + Point3D hp; + if (WorldStructurePick(eye, d, 2000.0f, &hp)) + { + float dx = hp.x-eye.x, dz = hp.z-eye.z; + float r = (float)Sqrt(dx*dx + dz*dz); + if (r < bestR) { bestR = r; best = hp; got = true; } + } + } + if (got && gBTTerrainEntity != 0) + { + MECH_TARGET_ENTITY(this) = gBTTerrainEntity; + MECH_TARGET_SUBIDX(this) = -1; + MECH_TARGET_POS(this) = best; + static float s_fas = 0.0f; s_fas += dt; + if (getenv("BT_FIRE_LOG") && s_fas >= 1.0f) + { + s_fas = 0.0f; + DEBUG_STREAM << "[fireat] nearest struct (" << best.x << "," + << best.y << "," << best.z << ") r=" << bestR << "\n" << std::flush; + } + } + } + // HUD feeds: the range caret + the hotbox (world point + state) + the // recovered-Execute instruments (compass, twist tape, group mask). // The range caret tracks the PICK (authentic: :5639 computes it from @@ -3842,7 +4825,18 @@ void static int s_prevBtn[4] = {0, 0, 0, 0}; static int s_afPhase = 0; s_afPhase ^= 1; - const int autofire = (gBTDrive.fireForced && targetInArc) ? s_afPhase : 0; + // DIAG (BT_AF_PERIOD=): throttle autofire cadence -- pulse the trigger + // only briefly every seconds, to test a slower-than-max fire rate (the + // coolant/jam mechanic is fire-rate dependent: at max spam the weapon overheats + // regardless; at a realistic cadence coolant priority decides the jam). + static float s_afClock = 0.0f; + s_afClock += dt; + static const float s_afPeriod = + getenv("BT_AF_PERIOD") ? (float)atof(getenv("BT_AF_PERIOD")) : 0.0f; + int afGate = 1; + if (s_afPeriod > 0.0f) + afGate = (fmodf(s_afClock, s_afPeriod) < 0.6f) ? 1 : 0; + const int autofire = (gBTDrive.fireForced && targetInArc && afGate) ? s_afPhase : 0; const int buttons[4] = { LBE4ControlsManager::ButtonJoystickTrigger, // 0x40 LBE4ControlsManager::ButtonJoystickPinky, // 0x45 @@ -4112,7 +5106,8 @@ void DEBUG_STREAM << "[target] aim=(" << gBTAimX << "," << gBTAimY << ")" << (victim != 0 ? " MECH under boresight (aimed)" : (pickTarget != 0 ? " terrain downrange (beam at scenery)" - : " sky (no target, no discharge)")) + : (MECH_TARGET_ENTITY(this) != 0 ? " fire-at-nothing (max-range designate)" + : " no target (no world sentinel)"))) << " range=" << range << (anyWeaponInRange ? " IN WEAPON RANGE" : "") << " [mechPicks=" << gAimHits << " groundPicks=" << gAimGround @@ -4301,13 +5296,54 @@ void // if (GetInstance() == MasterInstance) { + // (task #66) INSTABILITY MODEL -- byte-decoded from the binary master + // perf @0x4aad3d-0x4aaf14 [T1] (the un-exported gap; capstone disasm). + // Runs HERE, right before the gyro dispatch, exactly as the binary + // does: it reads LAST frame's acceleration snapshot (the tail updates + // localAcceleration afterwards -- "AccelerationLastFrame", the same + // vector the binary copies to +0x82c after this point). + { + // term 1: acceleration sway, squared + Scalar a = localAcceleration.linearMotion.Length(); + a = (maxUnstableAcceleration > 0.0f) ? a / maxUnstableAcceleration : 0.0f; + if (a > 1.0f) a = 1.0f; + a *= unstableAccelerationEffect; + unstablePercentage = a * a; + // term 2: "gun the engine" -- commanded speed the legs haven't + // reached, as a fraction of the command (negative when coasting + // past the demand -- the binary does not floor it) + MechControlsMapper *im = MappingMapper(); + Scalar u = (im != 0) ? im->speedDemand : 0.0f; + if (u > reverseSpeedMax2) u = reverseSpeedMax2; // @0x7a0 run cap + if (u < gimpStrideLength) u = gimpStrideLength; // @0x350 (negative reverse cap) + Scalar r = 0.0f; + if (u > 0.0f) + r = (u - legCycleSpeed) / u; + else if (u < 0.0f) + { + r = u - legCycleSpeed; + r = ((r <= 0.0f) ? -r : r) / (-u); + } + if (r > 1.0f) r = 1.0f; + unstablePercentage += r * unstableGunTheEngineEffect; + // term 3: turning in place (trn, leg state 4) + if (legAnimationState == 4) + unstablePercentage += unstableStopedTurnEffect; + if (unstablePercentage > 1.0f) unstablePercentage = 1.0f; + if (getenv("BT_GYRO_TRACE")) { static long s_it=0; if ((++s_it % 120)==0) + DEBUG_STREAM << "[instab] " << unstablePercentage + << " (|a|=" << localAcceleration.linearMotion.Length() + << " demand=" << u << " cyc=" << legCycleSpeed + << " legState=" << legAnimationState << ")\n" << std::flush; } + } + extern void GyroFrameJointWrite(Subsystem *, Scalar, int, int); static int s_gyrodbg = 0; if (getenv("BT_GYRO_LOG") && s_gyrodbg++ == 0) DEBUG_STREAM << "[gyro] dispatch: gyro=" << (void *)gyroSubsystem << " legAnim=" << legAnimationState << " death=" << deathAnimationLatched << "\n" << std::flush; - GyroFrameJointWrite(gyroSubsystem, 0.0f /* mech+0x3F0 model TBD */, + GyroFrameJointWrite(gyroSubsystem, unstablePercentage, legAnimationState, deathAnimationLatched); } else if (getenv("BT_GYRO_LOG")) @@ -4319,8 +5355,112 @@ void } // Keep the simulation/networking bookkeeping consistent (this is exactly - // what the base "no time / stasis" early-out does). - WriteSimulationUpdate(update_stream); + // what the base "no time / stasis" early-out does). MASTER ONLY (2026-07-14): + // replication is master-authoritative -- a replicant must never serialize. + // The port's replicant runs the leg SM for JOINTS (task #50 accommodation), + // whose transitions call ForceUpdate() and mark updateModel; serializing those + // emitted derived/uninitialized state back into the stream (and the type-3 + // writer re-dispatches SetBodyAnimation on the WRITER -- the real-clock crash + // rode this path with legAnimationState still 0xCDCDCDCD). Discard replicant + // marks instead. + // WATCHER POLL (engine Simulation::PerformAndWatch tail, SIMULATE.cpp:460): + // the engine path ends Perform() -> ExecuteWatchers() -> WriteSimulationUpdate(). + // This reconstruction replaced the whole performance but DROPPED the + // ExecuteWatchers() step, so the entity-level POLLED audio watchers + // (AudioLogicalTrigger FootStep, AudioMotionScale/Trigger LocalVelocity, ...) + // never executed -- only the PUSHED StateIndicator watchers fired. That's + // why footsteps/motion-scaled sounds were silent while state sounds worked. [T0] + // (AUDIO_FIDELITY F5) footStep is the authored CONTACT LEVEL (binary + // disasm @0x4a9e80-0x4a9eb6): while the leg channel's gait state is not + // Standing(0)/1, footStep = (jointlocal.y <= *clip threshold); states + // 0/1 RETAIN the previous value. The FootStep matcher fires on the + // CHANGE to 1 -- one clean rising edge per actual foot plant, timed by + // the clip's authored root-height crossing (replaces the 150 ms + // transition pulse, which fired at clip boundaries with a fixed width). + if (legAnimationState != 0 && legAnimationState != 1) + { + if (!footStepRootResolved) + { + footStepRootResolved = 1; + footStepRootJoint = ResolveJoint("jointlocal"); + } + Scalar *fs_thresh = legAnimation.footStepThreshold; + if (footStepRootJoint != 0 && fs_thresh != 0) + { + int contact = + (footStepRootJoint->GetTranslation().y <= *fs_thresh) ? 1 : 0; + if (getenv("BT_AUDIO_LOG")) { static int s_fc=0; + if (contact != footStep && s_fc++ < 40) + DEBUG_STREAM << "[audio] footstep contact " << footStep << "->" << contact + << " rootY=" << footStepRootJoint->GetTranslation().y + << " thresh=" << *fs_thresh << " legState=" << legAnimationState + << "\n" << std::flush; } + footStep = contact; + } + } + + // (AUDIO_FIDELITY F19) the localAcceleration feed, EXACTLY the binary's + // structure: 15-sample RING BUFFERS of the raw per-frame position + // derivative + dt (ctor part_012.c:9836-9840, fill 0); accel = the + // frame-to-frame derivative of the ring MEAN over the mean dt + // (part_012.c:15169-15195). Runs HERE, right before the watcher poll, + // so the accel attribute updates EVERY polled frame -- the ring mean's + // rotation jitter keeps the change-gate open and the authored N=30 + // footstep smoother fed (fed per-stride only, it took 10-20 s to warm + // from fill=0: the late-footsteps bug). + { + Scalar rdx = localOrigin.linearPosition.x - accelPrevPos.x; + Scalar rdy = localOrigin.linearPosition.y - accelPrevPos.y; + Scalar rdz = localOrigin.linearPosition.z - accelPrevPos.z; + accelPrevPos = localOrigin.linearPosition; + if (dt > 1.0e-4f) + { + velRingFwd[velRingCursor] = (Scalar)sqrtf(rdx * rdx + rdz * rdz) / dt; + velRingVert[velRingCursor] = rdy / dt; + velRingDt[velRingCursor] = dt; + velRingCursor = (velRingCursor + 1) % 15; + } + Scalar fwdMean = 0.0f, vertMean = 0.0f, dtMean = 0.0f; + for (int vr = 0; vr < 15; ++vr) + { + fwdMean += velRingFwd[vr]; + vertMean += velRingVert[vr]; + dtMean += velRingDt[vr]; + } + fwdMean *= (1.0f / 15.0f); + vertMean *= (1.0f / 15.0f); + dtMean *= (1.0f / 15.0f); + if (dtMean > 1.0e-4f) // binary: _DAT_004ab9d4 guard + localAcceleration.linearMotion = Vector3D( + 0.0f, + (vertMean - accelPrevVertMean) / dtMean, + -((fwdMean - accelPrevFwdMean) / dtMean)); + accelPrevFwdMean = fwdMean; + accelPrevVertMean = vertMean; + } + + // (AUDIO_FIDELITY F7) latch the incoming-missile report accumulated by + // Missile::MoveAndCollide since our last frame, then re-arm. The authored + // beeper matches incomingLock 1/0 and the tempo scale reads the range -- + // both polled by ExecuteWatchers below. + incomingLock = incomingLockNext; + distanceToMissile = distanceToMissileNext; + incomingLockNext = 0; + distanceToMissileNext = FLT_MAX; + + if (getenv("BT_AUDIO_LOG")) { static int s_wd=0; if ((s_wd++ % 300)==0) + DEBUG_STREAM << "[audio] mech watcher poll t=" << (GetTickCount() % 1000000) + << " n=" << s_wd + << " delayed=" << (int)AreWatchersDelayed() << " audioWatchers=" << DebugAudioWatcherCount() + << " simFlags=0x" << std::hex << (unsigned)simulationFlags << std::dec + << "\n" << std::flush; } + if (!AreWatchersDelayed()) + ExecuteWatchers(); + + if (GetInstance() != ReplicantInstance) + WriteSimulationUpdate(update_stream); + else + updateModel = 0; // drop accommodation-path marks } @@ -4433,7 +5573,9 @@ void const int freshBlock = isPlayer ? (gBlockCooldown <= 0.0f ? 1 : 0) : 1; if (isPlayer && gBlockCooldown > 0.0f) gBlockCooldown -= dt; // decay the out-of-contact window - // BINARY-TAIL-DEFERRED: collisionTemporaryState = 0 here (@4aa741). + // Per-frame collision-state reset (@4aa741): the walk below re-accumulates it + // per resolved contact, then it's pushed into the collisionState StateIndicator. + collisionTemporaryState = 0; // COLLISION-DAMAGE ECONOMY GUARD: snapshot the TRUE frame motion for the // per-contact velocity restore in Mech::ProcessCollision (see mech.hpp) -- // the list walk below reflects worldLinearVelocity at every StaticBounce, @@ -4452,6 +5594,12 @@ void if (cols != 0) ProcessCollisionList(cols, dt, old_position, &dmg); + // Push the frame's accumulated contact state into the audio-bound + // collisionState indicator: 0->1 on a fresh contact fires the impact sound, + // 1->0 when contact breaks; SetState only fires on change so a pressed-against + // contact plays once, not every frame. (@4aa741 sibling of the reset above.) + collisionState.SetState((unsigned)collisionTemporaryState); + // 6. response policy (@4aa76c-4aab5f) if (dmg.damageAmount == 0.00123f) // crushable icon: move STANDS { @@ -4793,9 +5941,21 @@ void damage->damageAmount = 0.00123f; // walk-through sentinel 0x3aa137f4 (:15402-15404) } - // BINARY-TAIL-DEFERRED: collisionTemporaryState tail (:15406-15413) -- - // implement together with the per-frame zero (@4aa741) and the named - // StateIndicator members. + // Contact-state accumulator tail (decomp part_012.c:15406-15413). The + // authentic split is InitialHit(1) vs Slide(2), keyed on the two collision + // floats @0x240/0x244 -- both are still fieldAt() stubs (return 0), so the + // Slide branch can't be computed faithfully yet. A resolved contact IS an + // impact, so accumulate InitialHit(1) -- the decomp's dominant `else` branch; + // upgrade to the full 1-vs-2 split once 0x240/0x244 are reconstructed. [T3] + if (collisionTemporaryState == 0) + { + collisionTemporaryState = 1; + // (AUDIO_FIDELITY F17) capture the impact speed for the authored + // CollisionSpeed volume/brightness scales ([0,25] -> harder hits + // sound louder + brighter). |worldLinearVelocity| at the arming + // contact = the impact speed. + collisionSpeed = worldLinearVelocity.Length(); + } } @@ -4994,3 +6154,107 @@ void << ") rgb=(" << r << "," << g << "," << b << ")" << std::endl; } } + +//########################################################################### +// BTReportIncomingMissile -- complete-type bridge (F7) +// +// Missile::MoveAndCollide (missile.cpp, forward-declared Mech only) reports +// its target + range each tick; the missile-alarm attributes latch in +// Mech::PerformAndWatch. Seeker targets are always mechs in this game +// (the launcher's lock); null-guarded. +//########################################################################### +void BTReportIncomingMissile(Entity *target, Scalar range) +{ + if (target != 0) + { + ((Mech *)target)->ReportIncomingMissile(range); + } +} + +//########################################################################### +// Rear-fire / look-view bridges (task #68) +// +// The binary's controls mapper (part_013.c:396-459) runs a five-state LOOK +// machine off the pod's look buttons: on a state change it re-aims the +// eyepoint (mech+0x360 = EyepointRotation, consumed by DPLEyeRenderable) +// and re-arms each weapon's view-fire enable (+0x3E0): FORWARD view enables +// the non-rear weapons, LOOK-BACK enables the REAR-mounted ones (+0x334, +// set from the mount segment's 'b' marker -- sitelbgunport/siterbgunport), +// side/down views disable all fire. Complete-Mech/Weapon TU bridges; the +// mapper calls BTCommitLookState on each state change. +//########################################################################### +int BTWeaponMountIsRear(void *ownerMech, int segIndex) +{ + Mech *m = (Mech *)ownerMech; + if (m == 0) + return 0; + EntitySegment *seg = m->GetSegment(segIndex); + if (seg == 0) + return 0; + const char *segname = seg->GetName(); // CString -> const char* + if (segname == 0) + return 0; + // the binary marker @0x511aa2 is the single character "b" (the back + // gun-port site names are the only port names containing it) + int rear = (strstr(segname, "b") != 0) ? 1 : 0; + if (getenv("BT_PROJ_LOG")) { static int s_rn=0; if (s_rn++<40) + DEBUG_STREAM << "[rearfire] segIndex=" << segIndex << " name='" + << segname << "' rear=" << rear << "\n" << std::flush; } + return rear; +} + +void BTCommitLookState(void *mech_v, int look_state) +{ + Mech *m = (Mech *)mech_v; + if (m == 0) + return; + + // eyepoint per state (binary case switch: yaw = the authored side angles, + // look-back = yaw pi (0x40490fdb) + lookBackAngle pitch, look-down = + // lookFrontAngle pitch) + Scalar pitch = 0.0f, yaw = 0.0f; + switch (look_state) + { + case 1: /*LookLeft*/ yaw = m->lookLeftAngle; break; + case 2: /*LookRight*/ yaw = m->lookRightAngle; break; + case 3: /*LookBehind*/ yaw = PI; // binary 0x40490fdb + pitch = m->lookBackAngle; break; + case 4: /*LookDown*/ pitch = m->lookFrontAngle; break; + default: break; // LookNone: identity + } + m->eyepointRotation = EulerAngles( + Radian(Radian::Normalize(pitch)), + Radian(Radian::Normalize(yaw)), + Radian(0.0f)); + + // weapon view-fire enables (binary: fwd = !rear, back = rear, else 0) + { + extern int BTWeaponIsRearFiring(Subsystem *sub); // -1 = not a weapon + extern void BTWeaponSetViewFireEnable(Subsystem *sub, int enable); + for (int id = 2; id < m->GetSubsystemCount(); ++id) + { + Subsystem *sub = m->GetSubsystem(id); + int rf = BTWeaponIsRearFiring(sub); + if (rf < 0) + continue; + BTWeaponSetViewFireEnable(sub, + (look_state == 0) ? (rf == 0) : + (look_state == 3) ? rf : 0); + } + } + + // (task #68) the HUD pip GROUP mask (binary mech+0x390, weapon update + // part_013.c:5588-5595): forward view shows the FRONT pip group (|=1,&~2), + // look-back shows the REAR group (|=2,&~1); side/down views leave it. + // mech4's HUD tick publishes the low bits as gBTHudGroupMask each frame. + if (look_state == 0) + m->targetReticle.reticleElementMask = (Reticle::ReticleElements) + (((int)m->targetReticle.reticleElementMask | 1) & ~2); + else if (look_state == 3) + m->targetReticle.reticleElementMask = (Reticle::ReticleElements) + (((int)m->targetReticle.reticleElementMask | 2) & ~1); + if (getenv("BT_KEY_LOG")) + DEBUG_STREAM << "[look] state=" << look_state + << " rearFiring=" << m->rearFiring + << " yaw=" << yaw << " pitch=" << pitch << "\n" << std::flush; +} diff --git a/game/reconstructed/mechmppr.cpp b/game/reconstructed/mechmppr.cpp index e6cb76e..3e6442f 100644 --- a/game/reconstructed/mechmppr.cpp +++ b/game/reconstructed/mechmppr.cpp @@ -681,6 +681,31 @@ void key_throttle = 0.08f; // near-idle half-cycle } } + // BT_FORCE_SWEEP=: triangle-sweep the forced throttle 0.2..0.9 + // continuously -- mimics a keyboard key-hold sweeping the lever every + // frame (a type-2 speed record per frame), the keyboard-skip repro. + { + static float s_swpP = -1.0f, s_swpClock = 0.0f; + if (s_swpP < 0.0f) + { + const char *fw = getenv("BT_FORCE_SWEEP"); + s_swpP = fw ? (float)atof(fw) : 0.0f; + } + if (s_swpP > 0.0f) + { + s_swpClock += time_slice; + float ph = fmodf(s_swpClock, 2.0f * s_swpP) / s_swpP; // 0..2 + float tri = (ph < 1.0f) ? ph : (2.0f - ph); // 0..1..0 + key_throttle = 0.2f + 0.7f * tri; + // BT_FORCE_STEP=1: quantize the swept throttle to the RIO + // Ranger 0.05 grid (the authentic ADC/deadband step) -- the + // stepped-vs-continuous A/B for the speed-change glitch. + static int s_swpStep = -1; + if (s_swpStep < 0) s_swpStep = getenv("BT_FORCE_STEP") ? 1 : 0; + if (s_swpStep) + key_throttle = floorf(key_throttle / 0.05f + 0.5f) * 0.05f; + } + } // BT_FORCE_FLIP=: invert the forced throttle after t sim-seconds // (the mid-stride direction-change repro). static float s_hFlip = -1.0f; @@ -706,6 +731,14 @@ void { throttlePosition = (key_throttle >= 0.0f) ? key_throttle : -key_throttle; reverseThrust = (key_throttle < 0.0f) ? 1 : 0; + // (task #68) look-behind: hold 'V' = the pod's rear-view button. + // (The other look buttons stay unbound on the keyboard rig.) + // Gated with the throttle writes -- a live RIO device (PadRIO) + // feeds lookBehind through its own mapped rear-view button. + { + extern int gBTLookBehind; + lookBehind = gBTLookBehind; + } } // CONTROL-MODE AXIS ROUTING (2026-07-13, the pod mapping): in // BASIC the stick steers the legs (turn) and the torso auto- @@ -915,14 +948,16 @@ void if (lookState != previousLookState) { - // TODO(look-eyepoint): the commit block below writes the eyepoint pose at - // binary offsets whose Mech-member mapping is CONFLICTED (mech+0x360 is - // claimed by mechName, +0x410 by stateFlags, +0x7bc by weaponRoster -- - // and the old draft even routed them through the TORSO pointer). Arbitrate - // those labels against the raw before enabling; the port has no eyepoint - // consumer yet. Look-state SELECTION above stays live (this-only). - DEBUG_STREAM << "[mppr] look state -> " << lookState - << " (eyepoint commit deferred)\n" << std::flush; + // (task #68) the look commit is LIVE: the conflicted labels are + // arbitrated (mech+0x360 = EyepointRotation -- the eye renderable + // consumes it; +0x410 = RearFiring; the +0x7bc children = the WEAPON + // roster, not cameras). BTCommitLookState (mech4.cpp complete-type + // TU) re-aims the eyepoint and re-arms each weapon's viewFireEnable: + // forward view = the non-rear weapons, LOOK-BACK = the rear-mounted + // ones, side/down = none. + extern void BTCommitLookState(void *mech, int look_state); + BTCommitLookState((void *)mech, (int)lookState); + DEBUG_STREAM << "[mppr] look state -> " << lookState << "\n" << std::flush; } else if (0) // DEFERRED body (kept for the reconstruction record) { diff --git a/game/reconstructed/mechrecon.hpp b/game/reconstructed/mechrecon.hpp index 26f4d06..a67b112 100644 --- a/game/reconstructed/mechrecon.hpp +++ b/game/reconstructed/mechrecon.hpp @@ -270,6 +270,11 @@ struct SequenceController // --- clip data parsed by SelectSequence (keyframe accessors read by mech3) --- int keyframeCount; // frameCount (binary +0x14) int jointCount; // animated joints (binary +0x18) + // (AUDIO_FIDELITY F5) the ANI header's footstep CONTACT threshold -- the + // clip-authored root height below which the foot is planted. The engine's + // own AnimationInstance captures the same word (JMOVER.cpp:1415); the + // binary's per-frame perf compares jointlocal.y against it (+0x20). + Scalar *footStepThreshold; // &hdr[2] (binary +0x20) int *jointIndices; // keyframe->skeleton map (binary +0x1c) Scalar *keyframeTimes; // frameTimes[frameCount] (binary +0x24) void *keyframeBase; // keyframe pose data base (binary +0x28) @@ -286,10 +291,10 @@ struct SequenceController unsigned cbArg2, cbArg3; // finished-callback context words (binary +0x4c/0x50) SequenceController() - : keyframeCount(0), jointCount(0), jointIndices(0), keyframeTimes(0), - keyframeBase(0), keyframeCursor(0), keyframeData(0), currentFrame(0), - currentTime(0.0f), jointSubsystem(0), owner(0), clipResource(0), - cbCode(0), cbArg2(0), cbArg3(0) {} + : keyframeCount(0), jointCount(0), footStepThreshold(0), jointIndices(0), + keyframeTimes(0), keyframeBase(0), keyframeCursor(0), keyframeData(0), + currentFrame(0), currentTime(0.0f), jointSubsystem(0), owner(0), + clipResource(0), cbCode(0), cbArg2(0), cbArg3(0) {} void Init(Mech *owner_mech); // @00427768 (ctor logic) void SelectSequence(int clip_id, void *cb, unsigned a2, unsigned a3); // @004277a8 @@ -435,11 +440,33 @@ namespace Vector // // P3 GAIT CUTOVER: backed by the real engine Quaternion (were no-op stubs). // ReconQuatIdentity(q, &id) -> q = Quaternion::Identity -// ReconQuatIntegrate(out,in,d) -> out = in integrated by angular delta d -// (engine Quaternion::Add(source, Vector3D) == FUN_00409f58) +// ReconQuatIntegrate(out,in,d) -> out = in composed with an EXACT rotation by +// angular delta d (= the real FUN_00409f58). // All callers pass (Quaternion*,Quaternion*) / (Quaternion*,Quaternion*,Vector3D*). inline void ReconQuatIdentity(Quaternion *q, const Quaternion * /*id*/) { *q = Quaternion::Identity; } -inline void ReconQuatIntegrate(Quaternion *out, const Quaternion *in, const Vector3D *delta) { out->Add(*in, *delta); } +// EXACT axis-angle quaternion integration -- the real FUN_00409f58 (part_000.c:9359), +// verified from the decomp. A prior stand-in wrongly used `out->Add(*in,*delta)` (a +// small-angle VECTOR add): correct every-frame on the master (tiny angle) but it +// DIVERGES over a long dead-reckon gap -- the spinning-peer heading drifting to ~180deg +// then snapping. The binary builds the unit rotation quaternion from the rotation vector +// `delta` (angle=|delta|, axis=delta/angle) as { axis*sin(angle/2), cos(angle/2) } and +// Hamilton-multiplies it onto `in` (FUN_00409d9c: out = in (X) deltaQuat). +inline void ReconQuatIntegrate(Quaternion *out, const Quaternion *in, const Vector3D *delta) +{ + const Scalar ang = delta->Length(); + if (ang > 1.0e-6f) + { + const Scalar half = (Scalar)(fmodf((float)ang, 6.2831853f) * 0.5f); // reduce2pi, then halve + const Scalar s = (Scalar)(sinf((float)half) / ang); // sin(angle/2)/angle + const Quaternion dq((Scalar)(delta->x * s), (Scalar)(delta->y * s), + (Scalar)(delta->z * s), (Scalar)cosf((float)half)); + out->Multiply(*in, dq); // in (X) dq + } + else + { + *out = *in; + } +} template inline Recon ReconQuatSlerp(A&&...) { return Recon(); } template inline Recon ComputeImpactDamage(A&&...) { return Recon(); } diff --git a/game/reconstructed/mechsub.cpp b/game/reconstructed/mechsub.cpp index cbb452d..0328282 100644 --- a/game/reconstructed/mechsub.cpp +++ b/game/reconstructed/mechsub.cpp @@ -89,11 +89,29 @@ static const StatusNameEntry StatusNameTable[] = //############################################################################# // Shared Data Support // +// ConfigureActivePress -> the +0x110 session flag (AUDIO_FIDELITY.md F15). +const MechSubsystem::IndexEntry + MechSubsystem::AttributePointers[]= +{ + ATTRIBUTE_ENTRY(MechSubsystem, ConfigureActivePress, configureActivePress) // id 2 @0x110 +}; + +MechSubsystem::AttributeIndexSet& + MechSubsystem::GetAttributeIndex() +{ + static MechSubsystem::AttributeIndexSet attributeIndex( + ELEMENTS(MechSubsystem::AttributePointers), + MechSubsystem::AttributePointers, + Simulation::GetAttributeIndex() + ); + return attributeIndex; +} + MechSubsystem::SharedData MechSubsystem::DefaultData( &MechSubsystem::ClassDerivations, ReconMessageHandlers, - ReconAttributeIndex, + MechSubsystem::GetAttributeIndex(), MechSubsystem::StateCount ); @@ -132,7 +150,7 @@ MechSubsystem::MechSubsystem( criticalReference = 0; // this[0x42] collisionCriticalHitWeight = 0; // this[0x43] printSimulationState = 0; // this[0x41] - vitalSubsystemIndex = -1; // this[0x44] + configureActivePress = -1; // this[0x44] configure-session idle damageZone = (ReconDamageZone *)new DamageZone(this, 0); // this[0x38] = FUN_0041de1c(new 0x160, this, 0) } @@ -162,7 +180,7 @@ MechSubsystem::MechSubsystem( controlDestination = 0; // this[0x3a]=param_6 (ctor pass-through; controlMessageID = 0; // this[0x3b]=param_7 weapons override -- task #6) resource = subsystem_resource; // this[0x3c] - vitalSubsystemIndex = -1; // this[0x44] + configureActivePress = -1; // this[0x44] configure-session idle vitalSubsystem = (subsystem_resource->vitalSubsystemIndex == 1); // res+0x48==1 BindName(refCount, subsystem_resource->videoObjectName); // res+0x4c diff --git a/game/reconstructed/mechsub.hpp b/game/reconstructed/mechsub.hpp index c1c5bee..cd7333b 100644 --- a/game/reconstructed/mechsub.hpp +++ b/game/reconstructed/mechsub.hpp @@ -137,6 +137,21 @@ class Damage; static Recon MessageHandlers; // SharedData ctor operand enum { StateCount = 7 }; // TechStatusTypeCount + // Attribute Support (AUDIO_FIDELITY.md F15): the binary registers + // ConfigureActivePress as a MechSubsystem-BASE attribute (id 2, + // descriptor @0x50de5c -> subsystem+0x110) -- the configure-session + // flag every weapon's ConfigureMappables handler drives (0 open / + // -1 idle) and the authored configure-ticker audio watches at + // thresh=-1. Publishing here makes it resolve on EVERY subsystem + // (each reads its own +0x110) and restores the binary-authentic + // dense id numbering (children start at 3, not 2). + enum { + ConfigureActivePressAttributeID = Simulation::NextAttributeID, // 2 + NextAttributeID // 3 + }; + static const IndexEntry AttributePointers[]; + static AttributeIndexSet& GetAttributeIndex(); + // alarm-changed callback (vtable slot +0x34) + host back-pointer void OnAlarmChanged(); Mech *owner; // owning mech (== hostEntity) @@ -307,7 +322,10 @@ class Damage; int printSimulationState; // @0x104 this[0x41] Scalar criticalReference; // @0x108 this[0x42] Scalar collisionCriticalHitWeight; // @0x10C this[0x43] - int vitalSubsystemIndex; // @0x110 this[0x44] (default -1) + int configureActivePress; // @0x110 this[0x44] (default -1) -- the + // configure-session flag (was misnamed + // vitalSubsystemIndex; the reader is the + // authored audio configure-ticker, F15) }; #endif diff --git a/game/reconstructed/mechweap.cpp b/game/reconstructed/mechweap.cpp index 7bfe790..10db641 100644 --- a/game/reconstructed/mechweap.cpp +++ b/game/reconstructed/mechweap.cpp @@ -123,18 +123,14 @@ const MechWeapon::IndexEntry // binary's (layout static_assert-locked); TriggerState stays the streamed // fire-button binding (id 0x13 PINNED). // - // The five PADS fill our chain-vs-binary id gap (0x0D..0x11) with valid, + // The PADS fill our chain-vs-binary id gap (now 0x0F..0x11) with valid, // named, never-bound entries (the binary's own ids here are the powered/ // aux-screen family we don't publish); target = rechargeLevel, harmless. - { (int)MechWeapon::MechWeaponPadFirstAttributeID + 0, "MechWeaponPad0D", + { (int)MechWeapon::MechWeaponPadFirstAttributeID + 0, "MechWeaponPad0F", (Simulation::AttributePointer)&MechWeapon::rechargeLevel }, - { (int)MechWeapon::MechWeaponPadFirstAttributeID + 1, "MechWeaponPad0E", + { (int)MechWeapon::MechWeaponPadFirstAttributeID + 1, "MechWeaponPad10", (Simulation::AttributePointer)&MechWeapon::rechargeLevel }, - { (int)MechWeapon::MechWeaponPadFirstAttributeID + 2, "MechWeaponPad0F", - (Simulation::AttributePointer)&MechWeapon::rechargeLevel }, - { (int)MechWeapon::MechWeaponPadFirstAttributeID + 3, "MechWeaponPad10", - (Simulation::AttributePointer)&MechWeapon::rechargeLevel }, - { (int)MechWeapon::MechWeaponPadFirstAttributeID + 4, "MechWeaponPad11", + { (int)MechWeapon::MechWeaponPadFirstAttributeID + 2, "MechWeaponPad11", (Simulation::AttributePointer)&MechWeapon::rechargeLevel }, ATTRIBUTE_ENTRY(MechWeapon, PercentDone, rechargeLevel), // 0x12 @0x320 ATTRIBUTE_ENTRY(MechWeapon, TriggerState, fireImpulse), // 0x13 @0x31C @@ -145,7 +141,7 @@ const MechWeapon::IndexEntry ATTRIBUTE_ENTRY(MechWeapon, PipColor, pipColor), // 0x18 @0x33C ATTRIBUTE_ENTRY(MechWeapon, PipExtendedRange, pipExtendedRange), // 0x19 @0x348 ATTRIBUTE_ENTRY(MechWeapon, EstimatedReadyTime, estimatedReadyTime),// 0x1A @0x330 - ATTRIBUTE_ENTRY(MechWeapon, RearFiring, usesExternalModel), // 0x1B @0x334 + ATTRIBUTE_ENTRY(MechWeapon, RearFiring, rearFiring), // 0x1B @0x334 (task #68: real semantics) ATTRIBUTE_ENTRY(MechWeapon, WeaponState, weaponAlarm), // 0x1C @0x350 ATTRIBUTE_ENTRY(MechWeapon, OutputVoltage, rechargeLevel) // 0x1D (port alias) }; @@ -183,7 +179,7 @@ MechWeapon::SharedData // Release: close the session (mode restored, temp layer stripped). // The +0x110 write (0 open / -1 idle) is the binary's config-session flag // (ctor default -1, part_012.c:15775); the port field carries the older name -// vitalSubsystemIndex -- no confirmed reader yet (read-site survey pending). +// configureActivePress -- the reader is the authored audio configure ticker (F15). // void MechWeapon::ConfigureMappablesMessageHandler( @@ -205,11 +201,11 @@ void if (value < 1) { mapper->ExitConfiguration(element); // vtbl+0x3C - vitalSubsystemIndex = -1; // this+0x110 -> idle + configureActivePress = -1; // this+0x110 -> idle (audio ticker OFF) } else { - vitalSubsystemIndex = 0; // this+0x110 -> session open + configureActivePress = 0; // this+0x110 -> session open (audio ticker ON) mapper->EnterConfiguration( // vtbl+0x38 element, (ControlsButton *)&fireImpulse, // this+0x31C (TriggerState) @@ -319,9 +315,18 @@ MechWeapon::MechWeapon( // removed. The muzzle segment index is the inherited base slot this+0xdc (used by // GetMuzzlePoint); the pip mount is gated by useConfiguredPip in DrawWeaponPip. - // usesExternalModel: True if the model name contains the marker substring - // (DAT_00511aa2); selects whether the configured pip is drawn. - usesExternalModel = (strstr(GetName(), "EXT") != 0) ? True : False; + // (task #68) REAR-FIRING: the binary ctor (@004b99a8 tail, part_013.c:6913- + // 6930) resolves the weapon's MOUNT SEGMENT page name and tests it for the + // marker 'b' (DAT_00511aa2) -- the back gun ports (sitelbgunport / + // siterbgunport). Rear-mounted weapons fire only into the LOOK-BACK view. + { + extern int BTWeaponMountIsRear(void *ownerMech, int segIndex); + rearFiring = BTWeaponMountIsRear(owner, GetSegmentIndex()) ? True : False; + if (getenv("BT_PROJ_LOG")) { static int s_rf=0; if (s_rf++<40) + DEBUG_STREAM << "[rearfire] weapon '" << GetName() + << "' seg=" << GetSegmentIndex() + << " rear=" << (int)rearFiring << "\n" << std::flush; } + } fireImpulse = 0.0f; // 0x31C // task #6: the binary MechWeapon ctor defaults its controls-destination @@ -333,7 +338,7 @@ MechWeapon::MechWeapon( rechargeLevel = 1.0f; // 0x320 (fully charged) previousFireImpulse = 0.0f; // 0x3A4 recoil = 0.0f; // 0x3E8 - useConfiguredPip = (usesExternalModel == 0); // 0x3E0 + viewFireEnable = (rearFiring == 0); // 0x3E0 (spawn in the forward view) estimatedReadyTime = 0; // 0x330 (binary attr "EstimatedReadyTime") Check_Fpu(); @@ -500,8 +505,21 @@ Logical rangeToTarget = (Scalar)Sqrt( // FUN_004dd138 delta.x * delta.x + delta.y * delta.y + delta.z * delta.z); - // 1.0f == _DAT_004b9c98; heatLoad is the inherited HeatableSubsystem load. - effectiveRange = (1.0f - heatLoad) * weaponRange; // *0x32c + // AUTHENTIC (decomp @004b9bdc:6983): effectiveRange = (1 - hostZoneDamage) * + // weaponRange, where hostZoneDamage is this weapon's OWN DamageZone.damageLevel + // (the QUALIFIED Subsystem::damageZone @0xE0 -> damageLevel +0x158), NOT + // heatLoad. The old `heatLoad` read was the SAME @0xE0-vs-heat misattribution + // already corrected in HeatSink::UpdateCoolant (heat.cpp:803): for a charge/ + // discharge weapon (ER laser) the weapon's OWN heatLoad swings 0..1 every cycle, + // so effectiveRange collapsed toward 0 and the weapon was perpetually "out of + // range" -- no damage submission, hence no impact explosion (user report: + // lackluster/absent laser hits on mechs AND buildings, esp. the ER medium). + // An UNDAMAGED weapon has damageLevel 0 -> full, STABLE weaponRange. Use the + // QUALIFIED engine zone -- MechSubsystem::damageZone is a shim SHADOW (heat.cpp:812). + // 1.0f == _DAT_004b9c98. + ::DamageZone *hostZone = this->Subsystem::damageZone; // @0xE0 + Scalar hostZoneDamage = (hostZone != 0) ? hostZone->damageLevel : 0.0f; // +0x158 + effectiveRange = (1.0f - hostZoneDamage) * weaponRange; // *0x32c targetWithinRange = (effectiveRange > rangeToTarget) ? True : False; } @@ -878,3 +896,22 @@ int return True; } + +//############################################################################# +// Rear-fire bridges (task #68) -- complete-MechWeapon-type helpers for the +// Mech ctor's rear-arsenal OR and the mapper's look-state commit (both TUs +// hold only Subsystem*; the old SubProxy::IsDerivedFrom stub returned 0, so +// the roster loop's capabilityFlags read never ran). +//############################################################################# +int BTWeaponIsRearFiring(Subsystem *sub) // -1 = not a MechWeapon +{ + if (sub == 0 || !sub->IsDerivedFrom(MechWeapon::ClassDerivations)) + return -1; + return (((MechWeapon *)sub)->GetRearFiring() != 0) ? 1 : 0; +} + +void BTWeaponSetViewFireEnable(Subsystem *sub, int enable) +{ + if (sub != 0 && sub->IsDerivedFrom(MechWeapon::ClassDerivations)) + ((MechWeapon *)sub)->SetViewFireEnable(enable); +} diff --git a/game/reconstructed/mechweap.hpp b/game/reconstructed/mechweap.hpp index 446fccd..452bc61 100644 --- a/game/reconstructed/mechweap.hpp +++ b/game/reconstructed/mechweap.hpp @@ -184,8 +184,14 @@ class CockpitHud; // is a latent AV (gotcha #11; the old pinned table survived on // heap luck -- the renumber reshuffled allocations and it fired // live in WeaponCluster's "PercentDone" resolve). Pads below fill - // ids 0x0D..0x11 (the mech.cpp attrPad idiom). - MechWeaponPadFirstAttributeID = 0x0D, // == PoweredSubsystem::NextAttributeID (locked below) + // ids 0x0F..0x11 (the mech.cpp attrPad idiom). F15: the base + // MechSubsystem now publishes ConfigureActivePress (binary id 2), + // shifting the parent chain end 0x0D -> 0x0E -- which matches the + // binary's own numbering (the old 0x0D chain was one LOW because + // the base attribute was missing). One pad absorbed. F6: HeatSink + // now publishes ReportLeak (binary id 12) -> 0x0E -> 0x0F; another + // pad absorbed (each authentic parent attr closes the gap by one). + MechWeaponPadFirstAttributeID = 0x0F, // == PoweredSubsystem::NextAttributeID (locked below) PercentDoneAttributeID = 0x12, // @0x320 rechargeLevel TriggerStateAttributeID, // 0x13 @0x31C (PINNED -- the fire-button binding) DistanceToTargetAttributeID, // 0x14 @0x324 rangeToTarget @@ -392,7 +398,17 @@ class CockpitHud; Scalar weaponRange; // @0x32C resource WeaponRange (configured max) int estimatedReadyTime; // @0x330 reset 0 (binary attr 0x1A "EstimatedReadyTime" // names this slot -- resolves the old pipState TODO) - Logical usesExternalModel; // @0x334 the "EXT" model-name marker; the binary attr + public: + // (task #68) rear-fire accessors for the complete-type bridges + int GetRearFiring() const { return (int)rearFiring; } + void SetViewFireEnable(int enable) { viewFireEnable = enable ? True : False; } + protected: + Logical rearFiring; // @0x334 REAR-MOUNTED weapon (task #68): the binary ctor + // sets it iff the MOUNT SEGMENT's page name contains 'b' + // (marker @0x511aa2; the back ports sitelbgunport/ + // siterbgunport -- blackhawk + owens author them). The + // old "EXT in the weapon name" was a double misread. + // The binary attr // 0x1B names this slot "RearFiring" (an EXT/rear-mount // weapon fires backward) int pipPosition; // @0x338 resource PipPosition @@ -409,7 +425,11 @@ class CockpitHud; Scalar heatCostToFire; // @0x3D8 resource HeatCostToFire Scalar rechargeRate; // @0x3DC resource RechargeRate - Logical useConfiguredPip; // @0x3E0 = (usesExternalModel == 0) (best-effort) + Logical viewFireEnable; // @0x3E0 "enabled in the CURRENT look view" (task #68): + // forward view = !rearFiring, rear view = rearFiring, + // side/down views = 0 (the mapper look-state commit + // toggles it). Gates BOTH firing (the emitter + // Loaded->Firing gate reads it) and the HUD pip. ResourceDescription::ResourceID explosionResourceID; // @0x3E4 resource ExplosionModelFile public: @@ -427,7 +447,7 @@ class CockpitHud; // collided with the Emitter subclass's own fields at 0x3F0+; removed: // * segmentReference -> the muzzle segment is the inherited base slot this+0xdc // (GetMuzzlePoint resolves it live via the mech4 BTResolveWeaponMuzzle bridge) - // * pipSegment -> DrawWeaponPip gates on useConfiguredPip + // * pipSegment -> DrawWeaponPip gates on viewFireEnable // * hasTarget -> dead (HasActiveTarget reads owner+0x388) // * targetPoint -> GetTargetPosition reads owner+0x37c // * muzzlePoint -> resolved live by GetMuzzlePoint diff --git a/game/reconstructed/mislanch.cpp b/game/reconstructed/mislanch.cpp index e755281..1c349eb 100644 --- a/game/reconstructed/mislanch.cpp +++ b/game/reconstructed/mislanch.cpp @@ -73,7 +73,7 @@ extern void BTPushProjectile(const Point3D &muzzle, void *shooter, void *target, const Point3D &targetPos, Scalar speed, Scalar damage, const Vector3D *launch_velocity = 0, int guided = 1, - int weapon_subsys = -1, int splash_burst = 0); + int weapon_subsys = -1, int splash_burst = 0, int muzzle_seg = -1); //########################################################################### // Port-side salvo replication state (missile-visibility wave). @@ -267,6 +267,12 @@ void MissileLauncher::FireWeapon() { Check(this); + // (task #68) the view gate: a weapon disabled in the CURRENT look view + // (rear rack in the forward view, anything in a side view) does not fire + // -- the same +0x3E0 gate the emitter's Loaded->Firing transition reads. + if (!viewFireEnable) + return; + // THE FIRING HEAT (task #9): heatCostToFire RAW into the launcher's own // pendingHeat (pre-scaled 1e7-unit resources: SRM6 = 5e7, LRM15 = 6.5e7) // -- the binary's add @part_013.c:8744-8747, gated on the heatable check. @@ -317,7 +323,8 @@ void MissileLauncher::FireWeapon() (i == 0) ? damageData.damageAmount : 0.0f, &launchVelocity, 1, subsystemID /*messmgr explosion bundling at impact (task #7)*/, - (i == 0) ? nmiss : 0 /*salvo-lead: cluster splash baseBurst*/); + (i == 0) ? nmiss : 0 /*salvo-lead: cluster splash baseBurst*/, + GetSegmentIndex() /*task #67: launch through the MOUNT frame (torso twist)*/); // Salvo replication (missile-visibility wave): bump the fire counter + stamp // the aim point; the extended update record carries both to peer nodes. @@ -389,7 +396,8 @@ void for (int i = 0; i < n; ++i) BTPushProjectile(mz, owner, 0 /*no entity: aim point only*/, rec->salvoTarget, spd, 0.0f /*VISUAL*/, &launchVelocity, 1, - subsystemID /*per-round detonation resolve on this node*/); + subsystemID /*per-round detonation resolve on this node*/, + 0, GetSegmentIndex() /*task #67 mount frame*/); if (getenv("BT_PROJ_LOG")) DEBUG_STREAM << "[projectile] REPLICANT salvo x" << n << " at(" << rec->salvoTarget.x << "," << rec->salvoTarget.y diff --git a/game/reconstructed/missile.cpp b/game/reconstructed/missile.cpp index b4a531a..4b3faa3 100644 --- a/game/reconstructed/missile.cpp +++ b/game/reconstructed/missile.cpp @@ -219,6 +219,17 @@ void Missile::MoveAndCollide(Scalar time_slice) if (thruster != 0) thruster->MissileThrusterSimulation(time_slice); // bleed burnTimeRemaining + // (AUDIO_FIDELITY F7) report the threat to the homing target: the mech's + // IncomingLock/DistanceToMissile attributes (binary @0x3fc/0x400) drive + // the authored missile-alarm beeper whose TEMPO rises as we close. + if (seeker != 0 && seeker->targetEntity != 0 && (simulationFlags & 1) == 0) + { + extern void BTReportIncomingMissile(Entity *target, Scalar range); + Vector3D to_target; + to_target.Subtract(seeker->targetPosition, localOrigin.linearPosition); + BTReportIncomingMissile(seeker->targetEntity, to_target.Length()); + } + // --- guidance: build steering toward the Seeker's lead point ---------- Vector3D forward; GetForward(forward); // FUN_0040d150(.., +0x100) diff --git a/game/reconstructed/myomers.hpp b/game/reconstructed/myomers.hpp index 9bf14ed..60ca9cd 100644 --- a/game/reconstructed/myomers.hpp +++ b/game/reconstructed/myomers.hpp @@ -340,7 +340,6 @@ class Mech; Scalar heatRangeSquared; // @0x34C ctor = heatRange*heatRange (degradation-curve denominator) Scalar velocityEfficiency; // @0x350 resource +0x190 Scalar accelerationEfficiency; // @0x354 resource +0x194 - friend struct MyomersLayoutCheck; }; diff --git a/game/reconstructed/powersub.cpp b/game/reconstructed/powersub.cpp index 95a1a03..fdf9041 100644 --- a/game/reconstructed/powersub.cpp +++ b/game/reconstructed/powersub.cpp @@ -885,7 +885,9 @@ int const Generator::IndexEntry Generator::AttributePointers[]= { - ATTRIBUTE_ENTRY(Generator, OutputVoltage, outputVoltage) // @0x1DC + ATTRIBUTE_ENTRY(Generator, OutputVoltage, outputVoltage), // @0x1DC + ATTRIBUTE_ENTRY(Generator, GeneratorState, stateAlarm), // @0x1FC (0x54 StateIndicator-compatible alarm; SetLevel fires audio) + ATTRIBUTE_ENTRY(Generator, GeneratorOn, generatorOn) // @0x1D4 (AudioLogicalTrigger; init 1 -- static until a shutdown writer drives it to 0) }; Generator::AttributeIndexSet& diff --git a/game/reconstructed/powersub.hpp b/game/reconstructed/powersub.hpp index d764bb3..0d73758 100644 --- a/game/reconstructed/powersub.hpp +++ b/game/reconstructed/powersub.hpp @@ -342,6 +342,8 @@ class Generator; public: enum { OutputVoltageAttributeID = HeatSink::NextAttributeID, + GeneratorStateAttributeID, // audio binds an AudioStateWatcher -> stateAlarm (0x54 StateIndicator) + GeneratorOnAttributeID, // audio binds an AudioLogicalTrigger -> generatorOn (@0x1D4) NextAttributeID }; private: diff --git a/game/reconstructed/projweap.cpp b/game/reconstructed/projweap.cpp index 032adeb..593cd02 100644 --- a/game/reconstructed/projweap.cpp +++ b/game/reconstructed/projweap.cpp @@ -95,7 +95,7 @@ extern void BTPushProjectile(const Point3D &muzzle, void *shooter, void *target, const Point3D &targetPos, Scalar speed, Scalar damage, const Vector3D *launch_velocity = 0, int guided = 1, - int weapon_subsys = -1, int splash_burst = 0); // AC: no cluster splash (default 0) + int weapon_subsys = -1, int splash_burst = 0, int muzzle_seg = -1); // AC: no cluster splash (default 0) //############################################################################# @@ -673,6 +673,18 @@ void if (weaponAlarm.GetState() != NoAmmoState && weaponAlarm.GetState() != JammedState) weaponAlarm.SetLevel(0); // Firing } + else + { + // Idle/reload: drive the ready state so the WeaponState audio is correct. + // The recon set Firing(0)/Loading(3)/Jammed/NoAmmo but NEVER a ready level, + // so at idle the launcher sat in Loading(3) forever and its WeaponState + // audio looped MissileLoading indefinitely (the loop stops only when the + // alarm LEAVES the loading state). Reflect readiness: Loaded(2, charge + + // ammo up = silent ready) once ReadyToFire, else Loading(3, reloading) -- + // SetLevel fires audio only on a real change, so this just yields the + // natural Firing -> Loading(reload) -> Loaded(ready) cycle. [T2] + weaponAlarm.SetLevel(ReadyToFire() ? 2 : 3); + } break; } @@ -691,6 +703,11 @@ void { Check(this); + // (task #68) the view gate: a weapon disabled in the CURRENT look view + // does not fire (see mislanch.cpp / the emitter's Loaded->Firing gate). + if (!viewFireEnable) + return; + // THE FIRING HEAT (task #9; was "a separate pre-existing gap"): the // binary adds heatCostToFire RAW to the weapon's own pendingHeat -- // projectile resources author PRE-scaled 1e7-unit heat (AFC100 = 6.5e7) @@ -834,7 +851,8 @@ void || rec->fireTarget.z != 0.0f); BTPushProjectile(mz, owner, 0 /*aim point only, no entity*/, rec->fireTarget, spd, 0.0f /*VISUAL*/, - haveAim ? 0 : &launchVelocity, 0 /*ballistic*/, subsystemID); + haveAim ? 0 : &launchVelocity, 0 /*ballistic*/, subsystemID, + 0, GetSegmentIndex() /*task #67 mount frame*/); const char *mzf = getenv("BT_MUZZLE"); // default ON; =0 disables if (mzf == 0 || mzf[0] != '0') diff --git a/game/reconstructed/sensor.hpp b/game/reconstructed/sensor.hpp index 9b5db12..8d69969 100644 --- a/game/reconstructed/sensor.hpp +++ b/game/reconstructed/sensor.hpp @@ -168,6 +168,7 @@ badVoltage; // @0x324 ctor init 0 + //########################################################################## // Simulation Support // diff --git a/game/reconstructed/seqctl.cpp b/game/reconstructed/seqctl.cpp index 1a6ab31..9c57aee 100644 --- a/game/reconstructed/seqctl.cpp +++ b/game/reconstructed/seqctl.cpp @@ -149,6 +149,10 @@ void int *hdr = (int *)desc->resourceAddress; // resource data (binary: *(desc+0x3c)) keyframeCount = hdr[0]; // @0x14 frameCount jointCount = hdr[1]; // @0x18 jointCount + // (AUDIO_FIDELITY F5) hdr[2] = the clip's authored footstep contact + // threshold (root height); previously skipped. Same capture as the + // engine's AnimationInstance (JMOVER.cpp:1415). + footStepThreshold = (Scalar *)(hdr + 2); // @0x20 int *p = hdr + 3; // jointIndices follow hdr[0..2] jointIndices = p; // @0x1c (jointCount ints) diff --git a/game/reconstructed/torso.cpp b/game/reconstructed/torso.cpp index 232b5dd..5808b87 100644 --- a/game/reconstructed/torso.cpp +++ b/game/reconstructed/torso.cpp @@ -143,11 +143,45 @@ Receiver::MessageHandlerSet Torso::AttributeIndexSet Torso::AttributeIndex; +// (AUDIO_FIDELITY F16) the real Torso attribute table, binary parity (dense +// ids 3..15 from MechSubsystem::NextAttributeID; offsets are the binary's, +// documented on the members). Un-deadens the torso-twist servo whir: +// TorsoTwistInt01/Ext01 pitch/gate on SpeedOfTorsoHorizontal, the stop clunk +// matches MotionState==2 (the limit-hit frame statusFlags already publishes). +const Torso::IndexEntry + Torso::AttributePointers[]= +{ + ATTRIBUTE_ENTRY(Torso, RotationOfTorsoVertical, currentElevation), // 3 @0x1E4 + ATTRIBUTE_ENTRY(Torso, RotationOfTorsoHorizontal, currentTwist), // 4 @0x1D8 + ATTRIBUTE_ENTRY(Torso, HorizontalLimitRight, horizontalLimitRight), // 5 @0x1DC + ATTRIBUTE_ENTRY(Torso, HorizontalLimitLeft, horizontalLimitLeft), // 6 @0x1E0 + ATTRIBUTE_ENTRY(Torso, SpeedOfTorsoVertical, elevationVelocity), // 7 @0x1EC + ATTRIBUTE_ENTRY(Torso, SpeedOfTorsoHorizontal, twistVelocity), // 8 @0x1E8 + ATTRIBUTE_ENTRY(Torso, StickPosition, analogTwistAxis), // 9 @0x1F0 + ATTRIBUTE_ENTRY(Torso, TorsoUp, elevateUpCommand), // 10 @0x1F8 + ATTRIBUTE_ENTRY(Torso, TorsoDown, elevateDownCommand), // 11 @0x1FC + ATTRIBUTE_ENTRY(Torso, TorsoLeft, twistLeftCommand), // 12 @0x200 + ATTRIBUTE_ENTRY(Torso, TorsoRight, twistRightCommand), // 13 @0x204 + ATTRIBUTE_ENTRY(Torso, TorsoCenter, centerCommand), // 14 @0x208 + ATTRIBUTE_ENTRY(Torso, MotionState, statusFlags) // 15 @0x20C +}; + +Torso::AttributeIndexSet& + Torso::GetAttributeIndex() +{ + static Torso::AttributeIndexSet attributeIndex( + ELEMENTS(Torso::AttributePointers), + Torso::AttributePointers, + MechSubsystem::GetAttributeIndex() + ); + return attributeIndex; +} + Torso::SharedData Torso::DefaultData( &Torso::ClassDerivations, Torso::MessageHandlers, - Torso::AttributeIndex, + Torso::GetAttributeIndex(), Torso::StateCount ); diff --git a/game/reconstructed/torso.hpp b/game/reconstructed/torso.hpp index 25b1cb8..89a8da8 100644 --- a/game/reconstructed/torso.hpp +++ b/game/reconstructed/torso.hpp @@ -100,6 +100,34 @@ class Joint; // engine skeleton node (JOINT.h); the twist target static Receiver::MessageHandlerSet MessageHandlers; static AttributeIndexSet AttributeIndex; + // (AUDIO_FIDELITY F16) the binary Torso attribute table [T1, rows + // keyed on the SpeedOfTorsoHorizontal string]: dense ids 3..15 + // starting at MechSubsystem::NextAttributeID (HeatWatcher/PowerWatcher + // publish nothing -- binary parity). The authored torso-twist servo + // whir reads SpeedOfTorsoHorizontal (scale: pitch over 0.5..0.9 + + // start/stop trigger at 0.25 -- the |yaw rate| the binary abs's) and + // MotionState (match ==2 = limit-hit frame -> the stop clunk). + enum { + RotationOfTorsoVerticalAttributeID = MechSubsystem::NextAttributeID, // 3 @0x1E4 currentElevation + RotationOfTorsoHorizontalAttributeID, // 4 @0x1D8 currentTwist + HorizontalLimitRightAttributeID, // 5 @0x1DC + HorizontalLimitLeftAttributeID, // 6 @0x1E0 + SpeedOfTorsoVerticalAttributeID, // 7 @0x1EC elevationVelocity (|rate|) + SpeedOfTorsoHorizontalAttributeID, // 8 @0x1E8 twistVelocity (|rate|) + StickPositionAttributeID, // 9 @0x1F0 analogTwistAxis + TorsoUpAttributeID, // 10 @0x1F8 elevateUpCommand + TorsoDownAttributeID, // 11 @0x1FC elevateDownCommand + TorsoLeftAttributeID, // 12 @0x200 twistLeftCommand + TorsoRightAttributeID, // 13 @0x204 twistRightCommand + TorsoCenterAttributeID, // 14 @0x208 centerCommand + MotionStateAttributeID, // 15 @0x20C statusFlags + NextAttributeID + }; + private: + static const IndexEntry AttributePointers[]; + public: + static AttributeIndexSet& GetAttributeIndex(); + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // BT segment / base-state / joint compatibility shims. // diff --git a/scratchpad/afc_dump.py b/scratchpad/afc_dump.py new file mode 100644 index 0000000..f885be1 --- /dev/null +++ b/scratchpad/afc_dump.py @@ -0,0 +1,37 @@ +import struct +data = open(r'C:/git/bt411/content/BTL4.RES','rb').read() +n = len(data) +def f32(o): return struct.unpack_from('6} {name:20} f32={f32(p+off):<16g} i32={i32(p+off)}") + print("\n --- full float scan (offset: value) for plausible fields 0xE0..0x1E0 ---") + for off in range(0xE0, 0x1E0, 4): + v=f32(p+off) + if abs(v)>1e-3 and abs(v)<1e12: + print(f" +{hex(off):>6}: {v:g}") diff --git a/scratchpad/btconsole_stuck.py b/scratchpad/btconsole_stuck.py new file mode 100644 index 0000000..a83fbe4 --- /dev/null +++ b/scratchpad/btconsole_stuck.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""btconsole_stuck.py -- a DELIBERATELY-STUCK console for reproducing the +'console death froze peer replication' open question (task #50 / open-questions.md). + +It behaves like tools/btconsole.py (connect, stream egg, send RunMission x2, drain +the pod->console stream) BUT after STUCK_AFTER seconds it STOPS draining while +holding the socket OPEN -- i.e. it stops calling recv(). This is the authentic +'receive pad full -> close never seen' failure mode (NOT a clean disconnect): the +pod keeps queueing pod->console packets, the TCP window closes, and we watch +whether the pod's CheckBuffers loop stalls the GAME peers behind the wedged +console host. + +Usage: python btconsole_stuck.py host:port host:port [STUCK_AFTER_secs] +Env: none. STUCK_AFTER default 12s after the 2nd RunMission. +""" +import socket, struct, sys, time, threading, os + +CHUNK = 1000 +PKT_FMT_HDR = " 4 else 12.0 + +def run_mission_packet(): + pkt = struct.pack(PKT_FMT_HDR, APPLICATION_CLIENT_ID, 0, CONSOLE_HOST_ID, 0) + pkt += struct.pack("= launch_at: + s.sendall(run_mission_packet()); launches_sent += 1 + launch_at = time.time() + LAUNCH_STEP_SECONDS + print(f"[{name}] RunMission #{launches_sent} sent", flush=True) + if launches_sent == 2: + stuck_at = time.time() + STUCK_AFTER + # *** THE FAILURE: once mission is running, stop draining after STUCK_AFTER *** + if stuck_at is not None and time.time() >= stuck_at: + print(f"[{name}] *** GOING STUCK: socket stays OPEN, no more recv() ***", flush=True) + while True: + time.sleep(60) # hold the socket open, never drain it again + try: + data = s.recv(4096) + if not data: + print(f"[{name}] pod closed the console socket", flush=True); return + except socket.timeout: + continue + except OSError as e: + print(f"[{name}] socket error {e!r}", flush=True); return + +def main(): + egg_path = sys.argv[1] + pods = sys.argv[2:4] + with open(egg_path, "rb") as f: + egg = egg_wire_bytes(f.read()) + ts = [threading.Thread(target=serve_pod, args=(p, egg), daemon=True) for p in pods] + for t in ts: t.start() + while True: time.sleep(1) + +if __name__ == "__main__": + main() diff --git a/tools/btconsole.py b/tools/btconsole.py index bdeb29c..55cc238 100644 --- a/tools/btconsole.py +++ b/tools/btconsole.py @@ -122,7 +122,7 @@ def serve_pod(hostport, egg_bytes): try: data = s.recv(4096) if not data: - print(f"[{name}] pod closed the console socket") + print(f"[{name}] pod closed the console socket", flush=True) return # Expect the AcknowledgeEggFile (and possibly other console traffic). if len(data) >= 24: @@ -134,6 +134,14 @@ def serve_pod(hostport, egg_bytes): print(f"[{name}] pod->console {len(data)} bytes") except socket.timeout: continue + except OSError as e: + # Previously UNHANDLED: a reset/abort here killed the thread silently + # (buffered stdout lost) and the console process exited -- leaving the + # pods holding dead console sockets. Log it and keep the thread alive + # (idle) so the process + diagnosis survive. + print(f"[{name}] console socket error: {e!r} -- idling", flush=True) + while True: + time.sleep(60) def main(): diff --git a/tools/mp_launch.sh b/tools/mp_launch.sh new file mode 100644 index 0000000..1892834 --- /dev/null +++ b/tools/mp_launch.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# mp_launch.sh -- launch a 2-node local multiplayer test (FOGDAY.EGG) with the two +# game nodes PINNED TO DISJOINT CPU CORES + the console relay. +# +# WHY THE AFFINITY MATTERS (task #50, 2026-07-15): two Debug btl4 nodes on one box +# fight for the same cores, and Windows punishes the contention by BATCHING their +# TCP packet delivery -- update records that should arrive every ~17ms instead +# arrive in bursts (measured gaps up to 226ms, burstiness 3-7x, BT_RXJIT). A peer +# dead-reckons across those gaps and then snaps when a record finally lands -> the +# "random" shaky peer motion. Pinning each node to its own core set restores even +# ~17ms delivery (burstiness ~1.0) and the shakiness vanishes. This is a SINGLE-BOX +# TEST-RIG artifact -- real pods are dedicated machines and never see it; do NOT +# "fix" it in the game with an interpolation buffer. +# +# Usage: tools/mp_launch.sh [extra env for BOTH nodes...] +# e.g. tools/mp_launch.sh BT_SLIDE=1 +# Run from the repo root; content/ must hold FOGDAY.EGG and the build in build/Debug. +set -u +cd "$(dirname "$0")/../content" || exit 1 +EXE=../build/Debug/btl4.exe +EXTRA="$*" + +powershell -NoProfile -Command "Get-Process btl4,python -ErrorAction SilentlyContinue | Stop-Process -Force" 2>/dev/null +sleep 2 +rm -f fa.log fb.log console.log + +env $EXTRA BT_DEV_GAUGES=1 BT_LOG=fb.log BT_START_INSIDE=1 BT_SPAWN_AT="40 -150" "$EXE" -egg FOGDAY.EGG -net 1601 & +sleep 2 +env $EXTRA BT_DEV_GAUGES=1 BT_LOG=fa.log BT_START_INSIDE=1 BT_SPAWN_AT="-40 -150" "$EXE" -egg FOGDAY.EGG -net 1501 & +sleep 3 + +# Pin the two nodes to disjoint core sets (0x00F = cores 0-3, 0x3C0 = cores 6-9). +powershell -NoProfile -Command "\$ps=@(Get-Process btl4 | Sort-Object Id); if(\$ps.Count -ge 2){ \$ps[0].ProcessorAffinity=[IntPtr]0x00F; \$ps[1].ProcessorAffinity=[IntPtr]0x3C0; Write-Output ('PINNED '+\$ps[0].Id+'->cores0-3, '+\$ps[1].Id+'->cores6-9') } else { Write-Output ('WARN only '+\$ps.Count+' btl4 up') }" + +sleep 6 +python -u ../tools/btconsole.py FOGDAY.EGG 127.0.0.1:1501 127.0.0.1:1601 > console.log 2>&1 & +echo "console streaming; nodes on 1501/1601. Logs: content/fa.log content/fb.log" +wait diff --git a/tools/sf2extract.py b/tools/sf2extract.py new file mode 100644 index 0000000..fcd9bb2 --- /dev/null +++ b/tools/sf2extract.py @@ -0,0 +1,295 @@ +#!/usr/bin/env python3 +"""sf2extract.py -- extract the BattleTech AWE32 SoundFonts (AUDIO1/2.RES) into +loose WAV files + generate the allPresets[] C++ table for the WinTesla port. + +FULL-ZONE extraction (AUDIO_FIDELITY.md F1/F2/F13/F14). The banks are SoundFont +**v1.0** (ifil 1.0, EMU8000): `shdr` records are 16 bytes {dwStart,dwEnd, +dwStartLoop,dwEndLoop} (no name/rate/pitch field). Samples are 16-bit mono PCM +in `smpl`. Preset -> instrument resolves phdr->pbag->pgen(41); every sample- +bearing ibag zone of that instrument becomes a SAMPLEINFO slot. + +Per-zone recovered metadata: + keyRange(43) -> keyLo/keyHi (SetupPatch selects zones by the note) + sampleModes(54) -> loop flag (1/3 = loop) + samplePitch(55, SBK) -> root*100+cents, present on every zone + overridingRootKey(58) -> replaces the gen-55 root when present + coarseTune(51)/fineTune(52) -> added cents + initialAttenuation(48) -> SBK INVERTED scale (127=full); 0.375 dB/step [T3], + baked into the WAV PCM (layer balance) + releaseVolEnv(38) -> timecents -> seconds, for the Stop-path gain fade + pan(17) -> 0..127 -> CHANNEL_LEFT/CENTER/RIGHT + shdr loop points -> loopStart/loopEnd frames (AL_SOFT_loop_points) + +PITCH (F2, algebraically exact): EMU8000 v1 base rate is 44100 Hz; a zone's true +data rate is baked into the WAV header as + rate = round(44100 * 2^(((6000 - effRootCents) + tuneCents) / 1200)) +so the engine's note-60 pitch model (BTNotePitchFactor) reproduces original +playback at every note. effRootCents = gen58*100 if present else gen55. + +Output: + content/AUDIO/[_zK].wav + game/reconstructed/audiopresets.cpp (allPresets[2][128], all zones) + +Regenerate: python tools/sf2extract.py (add --stats for the audit cross-check) +""" +import struct, os, sys, wave, math + +HERE = os.path.dirname(__file__) +AUDIO = os.path.join(HERE, "..", "content", "AUDIO") +OUT_CPP = os.path.join(HERE, "..", "game", "reconstructed", "audiopresets.cpp") +BASE_RATE = 44100 # EMU8000 v1 (awesfx sffile.c hardcodes v1 samplerate 44100) +MAX_ZONES = 25 # PRESETINFO samples[] capacity (AllExplosion = 25 layers) + +G_KEYRANGE, G_PAN, G_RELEASE, G_INST, G_KEYRANGE2 = 43, 17, 38, 41, 43 +G_ATTEN, G_COARSE, G_FINE, G_SAMPLEID, G_MODES, G_PITCH, G_ROOT = 48, 51, 52, 53, 54, 55, 58 +G_FILTER_FC, G_FILTER_Q = 8, 9 +SIGNED_GENS = {G_COARSE, G_FINE, G_RELEASE} + + +def s16(v): + return v - 0x10000 if v >= 0x8000 else v + + +class Bank: + def __init__(self, path): + d = open(path, "rb").read() + assert d[:4] == b"RIFF" and d[8:12] == b"sfbk" + self.d = d + + def find(fc): + p = d.find(fc) + return p + 8, struct.unpack_from(" (name, presetNum, [zone-dict]) zone gens merged: inst-global <- inst-zone <- preset adds""" + name = self.d[self.pho + i * 38:self.pho + i * 38 + 20].split(b"\x00")[0].decode("latin1").strip() + preset, bank, pbag = struct.unpack_from("> 8 + blo, bhi = pk[G_KEYRANGE] & 0xFF, pk[G_KEYRANGE] >> 8 + g[G_KEYRANGE] = max(alo, blo) | (min(ahi, bhi) << 8) + elif G_KEYRANGE in pk: + g[G_KEYRANGE] = pk[G_KEYRANGE] + for add in (G_COARSE, G_FINE, G_RELEASE): + if add in pk: + g[add] = g.get(add, 0) + pk[add] + zones.append(g) + return name, preset, zones + + +def zone_fields(bank, g): + st, en, ls, le = struct.unpack_from("> 8 + sm = g.get(G_MODES, 0) & 3 + looping = sm in (1, 3) + lstart = max(0, min(ls - st, en - st)) if looping else 0 + lend = max(0, min(le - st, en - st)) if looping else 0 + if looping and lend <= lstart: # degenerate loop -> whole buffer + lstart, lend = 0, en - st + # SBK attenuation: INVERTED (127 = full volume), 0.375 dB/step [T3 exact curve] + atten = g.get(G_ATTEN, 127) + gain = 10.0 ** (-((127 - atten) * 0.375) / 20.0) if atten < 127 else 1.0 + rel_tc = g.get(G_RELEASE, None) + release = min(8.0, 2.0 ** (rel_tc / 1200.0)) if rel_tc is not None else 0.0 + pan = g.get(G_PAN, 64) + chan = "CHANNEL_LEFT" if pan <= 42 else ("CHANNEL_RIGHT" if pan >= 85 else "CHANNEL_CENTER") + # (AUDIO_FIDELITY F14) the authored static resonant low-pass the EMU8000 + # applied in hardware: initialFilterFc(8) / initialFilterQ(9), SBK 0..127 + # scale. Absent fc = filter fully open. + filter_fc = g.get(G_FILTER_FC, 127) + filter_q = g.get(G_FILTER_Q, 0) + return dict(pcm=pcm, rate=rate, keylo=keylo, keyhi=keyhi, looping=looping, + lstart=lstart, lend=lend, gain=gain, release=release, chan=chan, + sid=g[G_SAMPLEID], root_cents=root_cents, tune=tune, + fc=filter_fc, q=filter_q) + + +def bake_lowpass(pcm, rate, fc127, q127): + """(AUDIO_FIDELITY F14) bake the zone's authored static resonant low-pass + into the PCM. Cutoff uses the AWE NRPN curve (100 + fc*7900/127 Hz); + resonance maps SBK Q 0..127 -> 0..+12 dB biquad peak [T3 -- curve shape + approximate, off/open endpoints exact]. The filter is designed at the + zone's BAKED rate, so note-60 playback (1 file second == 1 real second) + reproduces the hardware's absolute cutoff exactly; pitch-shifted notes + carry the filter with them (same limitation as the rate-baked tuning).""" + cutoff_hz = 100.0 + fc127 * 7900.0 / 127.0 + if cutoff_hz >= 0.45 * rate: # at/above Nyquist headroom: no-op + return None + q_biquad = 0.7071 * 10.0 ** ((q127 / 127.0 * 12.0) / 20.0) + # RBJ cookbook 2-pole low-pass, run in FLOAT (the caller converts once) + w0 = 2.0 * math.pi * cutoff_hz / rate + alpha = math.sin(w0) / (2.0 * q_biquad) + cw = math.cos(w0) + b0 = (1.0 - cw) / 2.0 + b1 = 1.0 - cw + b2 = (1.0 - cw) / 2.0 + a0 = 1.0 + alpha + a1 = -2.0 * cw + a2 = 1.0 - alpha + b0 /= a0; b1 /= a0; b2 /= a0; a1 /= a0; a2 /= a0 + n = len(pcm) // 2 + vals = struct.unpack("<%dh" % n, pcm[:n * 2]) + out = [0.0] * n + x1 = x2 = y1 = y2 = 0.0 + for i, x0 in enumerate(vals): + y0 = b0 * x0 + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2 + x2 = x1; x1 = x0 + y2 = y1; y1 = y0 + out[i] = y0 + return out + + +def write_wav(fname, pcm, rate, gain, fc127=127, q127=0): + # Full bake pipeline in FLOAT, one int16 conversion at the end: the + # resonance peak can overshoot full scale (165 of 232 filtered zones + # hard-clipped in an int-per-stage draft) -- normalize the whole file + # down to fit instead of clipping (level error <= the overshoot, wave + # shape preserved; the EMU's filter stage had internal headroom). + fvals = None + if fc127 < 127: # authored static low-pass + fvals = bake_lowpass(pcm, rate, fc127, q127) + if fvals is None and gain < 0.999: + n = len(pcm) // 2 + fvals = [float(v) for v in struct.unpack("<%dh" % n, pcm[:n * 2])] + if fvals is not None: + if gain < 0.999: # layer-balance attenuation + fvals = [v * gain for v in fvals] + peak = max(1.0, max(abs(v) for v in fvals)) + norm = (32767.0 / peak) if peak > 32767.0 else 1.0 + pcm = struct.pack("<%dh" % len(fvals), + *[int(v * norm) for v in fvals]) + with wave.open(os.path.join(AUDIO, fname), "wb") as w: + w.setnchannels(1); w.setsampwidth(2); w.setframerate(rate) + w.writeframes(pcm) + + +def main(): + stats = "--stats" in sys.argv + banks = {1: "AUDIO1.RES", 2: "AUDIO2.RES"} + table = {1: {}, 2: {}} + grid_outliers = [] + total_zones = 0 + for bn, fn in banks.items(): + bank = Bank(os.path.join(AUDIO, fn)) + multi = 0 + for i in range(bank.nphdr): + name, preset, zones = bank.preset_zones(i) + if not name or not zones: + continue + zs = [] + for g in zones[:MAX_ZONES]: + z = zone_fields(bank, g) + if not z["pcm"]: + continue + zs.append(z) + if len(zones) > MAX_ZONES: + print(f"WARNING: {name} has {len(zones)} zones, capped at {MAX_ZONES}") + if not zs: + continue + for k, z in enumerate(zs): + z["file"] = f"{name}.wav" if len(zs) == 1 else f"{name}_z{k}.wav" + write_wav(z["file"], z["pcm"], z["rate"], z["gain"], z["fc"], z["q"]) + cents = 1200.0 * math.log2(z["rate"] / 44100.0) + if abs(cents - round(cents / 100.0) * 100) > 3 and not z["tune"] % 100: + grid_outliers.append((name, k, z["rate"], round(cents, 1))) + table[bn][preset] = (name, zs) + total_zones += len(zs) + if len(zs) > 1: + multi += 1 + print(f"bank {bn} ({fn}): {len(table[bn])} presets, {multi} multi-zone") + print(f"total zones: {total_zones}") + if grid_outliers: + print(f"rate-grid outliers (expect few): {len(grid_outliers)}") + for o in grid_outliers[:10]: + print(" ", o) + + if stats: + for bn in (1, 2): + for preset, (name, zs) in sorted(table[bn].items()): + if len(zs) > 1: + print(f"b{bn} p{preset} {name}: " + " ".join( + f"[{z['keylo']}-{z['keyhi']} r{z['rate']} {'L' if z['looping'] else '1'}" + f"{' rel%.1f' % z['release'] if z['release'] else ''}]" for z in zs)) + return + + with open(OUT_CPP, "w", newline="\n") as f: + f.write('// GENERATED by tools/sf2extract.py -- the audio soundbank table.\n') + f.write('// FULL-ZONE extraction (AUDIO_FIDELITY.md F1/F2/F13/F14): every preset\n') + f.write('// carries ALL its instrument zones (key-splits, layers, stereo pairs)\n') + f.write('// with per-zone key range, loop region (frames), release seconds and\n') + f.write('// channel; per-zone tuning + attenuation are baked into the WAVs.\n') + f.write('// Regenerate: python tools/sf2extract.py\n') + f.write('#include \n\n') + f.write('PRESETINFO allPresets[2][128] = {};\n\n') + f.write('namespace {\n') + f.write('void Z(int b, int p, const char *file, int keyLo, int keyHi, SampleLoop loop,\n') + f.write(' int loopStart, int loopEnd, float releaseSec, SampleChannel chan)\n') + f.write('{\n') + f.write('\tPRESETINFO &pi = allPresets[b][p];\n') + f.write('\tif (pi.sampleNum >= MAX_PRESET_SAMPLES) return;\n') + f.write('\tSAMPLEINFO &s = pi.samples[pi.sampleNum++];\n') + f.write('\ts.implemented = true; s.bufferIndex = -1; s.file = file;\n') + f.write('\ts.keyLo = keyLo; s.keyHi = keyHi; s.loop = loop;\n') + f.write('\ts.loopStart = loopStart; s.loopEnd = loopEnd;\n') + f.write('\ts.releaseSec = releaseSec; s.chan = chan;\n') + f.write('}\n\n') + f.write('struct AudioPresetInit {\n\tAudioPresetInit() {\n') + for bn in (1, 2): + for preset, (name, zs) in sorted(table[bn].items()): + for z in zs: + loop = "LoopAtWill" if z["looping"] else "ForceStatic" + f.write(f'\t\tZ({bn-1},{preset},"{z["file"]}",{z["keylo"]},{z["keyhi"]},' + f'{loop},{z["lstart"]},{z["lend"]},{z["release"]:.3f}f,{z["chan"]});\n') + f.write('\t}\n} s_audioPresetInit;\n}\n') + npresets = len(table[1]) + len(table[2]) + print(f"wrote {OUT_CPP} ({npresets} presets, {total_zones} zones)") + + +if __name__ == "__main__": + main() diff --git a/tools/soundboard.py b/tools/soundboard.py new file mode 100644 index 0000000..03d8c7d --- /dev/null +++ b/tools/soundboard.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +"""soundboard.py -- click-to-play board for the BT soundbank (content/AUDIO/*.wav). + +Run: python tools/soundboard.py (from the repo root, or anywhere) + +- Click a button to play that sample (stdlib winsound, async -- click again to + restart, click STOP to silence). +- The label shows [bank:patch] from game/reconstructed/audiopresets.cpp so an + identified sound maps straight back to the game's (bankID, patchID). +- Type in the filter box to narrow (substring, case-insensitive). +- "Play All" steps through the filtered list one per second (ESC/STOP to halt). +""" +import os, re, sys, threading, time +import tkinter as tk +from tkinter import ttk +import winsound + +HERE = os.path.dirname(os.path.abspath(__file__)) +AUDIO = os.path.normpath(os.path.join(HERE, "..", "content", "AUDIO")) +PRESETS_CPP = os.path.normpath(os.path.join(HERE, "..", "game", "reconstructed", "audiopresets.cpp")) + +def load_mapping(): + """file -> (bank, patch) from audiopresets.cpp (allPresets[b][p] ... file="X.wav").""" + mapping = {} + try: + text = open(PRESETS_CPP, encoding="utf-8", errors="replace").read() + # blocks look like: allPresets[0][12].samples[0]; ... s.file = "LaserAFire01.wav"; + for m in re.finditer(r'allPresets\[(\d+)\]\[(\d+)\]\.samples\[0\];.*?s\.file = "([^"]+)";', + text, re.S): + bank, patch, fname = int(m.group(1)) + 1, int(m.group(2)), m.group(3) + mapping[fname] = (bank, patch) + except OSError: + pass + return mapping + +class SoundBoard(tk.Tk): + def __init__(self): + super().__init__() + self.title("BT411 Soundboard -- " + AUDIO) + self.geometry("1150x760") + self.mapping = load_mapping() + self.files = sorted(f for f in os.listdir(AUDIO) if f.lower().endswith(".wav")) + self.playing_all = False + + top = ttk.Frame(self); top.pack(fill="x", padx=6, pady=4) + ttk.Label(top, text="Filter:").pack(side="left") + self.filter_var = tk.StringVar() + ent = ttk.Entry(top, textvariable=self.filter_var, width=32) + ent.pack(side="left", padx=4) + self.filter_var.trace_add("write", lambda *a: self.rebuild()) + ttk.Button(top, text="STOP", command=self.stop).pack(side="left", padx=8) + ttk.Button(top, text="Play All (filtered)", command=self.play_all).pack(side="left") + self.status = tk.StringVar(value=f"{len(self.files)} samples loaded") + ttk.Label(top, textvariable=self.status, foreground="#06c").pack(side="left", padx=12) + + # scrollable button grid + wrap = ttk.Frame(self); wrap.pack(fill="both", expand=True) + self.canvas = tk.Canvas(wrap, highlightthickness=0) + sb = ttk.Scrollbar(wrap, orient="vertical", command=self.canvas.yview) + self.canvas.configure(yscrollcommand=sb.set) + sb.pack(side="right", fill="y"); self.canvas.pack(side="left", fill="both", expand=True) + self.grid_frame = ttk.Frame(self.canvas) + self.canvas.create_window((0, 0), window=self.grid_frame, anchor="nw") + self.grid_frame.bind("", + lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all"))) + self.canvas.bind_all("", + lambda e: self.canvas.yview_scroll(-1 * (e.delta // 120), "units")) + self.bind("", lambda e: self.stop()) + + self.rebuild() + + def label_for(self, f): + bp = self.mapping.get(f) + tag = f"[{bp[0]}:{bp[1]}] " if bp else "[-:-] " + return tag + f[:-4] + + def rebuild(self): + for w in self.grid_frame.winfo_children(): + w.destroy() + flt = self.filter_var.get().lower() + shown = [f for f in self.files if flt in f.lower()] + cols = 4 + for i, f in enumerate(shown): + b = ttk.Button(self.grid_frame, text=self.label_for(f), width=38, + command=lambda ff=f: self.play(ff)) + b.grid(row=i // cols, column=i % cols, padx=2, pady=1, sticky="w") + self.status.set(f"{len(shown)} shown / {len(self.files)} total") + self.canvas.yview_moveto(0) + + def play(self, f): + self.playing_all = False + path = os.path.join(AUDIO, f) + winsound.PlaySound(path, winsound.SND_FILENAME | winsound.SND_ASYNC) + bp = self.mapping.get(f) + self.status.set(f"PLAYING {f}" + (f" (bank {bp[0]}, patch {bp[1]})" if bp else "")) + + def stop(self): + self.playing_all = False + winsound.PlaySound(None, winsound.SND_PURGE) + self.status.set("stopped") + + def play_all(self): + flt = self.filter_var.get().lower() + shown = [f for f in self.files if flt in f.lower()] + self.playing_all = True + def run(): + for f in shown: + if not self.playing_all: + return + self.after(0, lambda ff=f: self.status.set("PLAYING " + ff)) + winsound.PlaySound(os.path.join(AUDIO, f), + winsound.SND_FILENAME | winsound.SND_ASYNC) + time.sleep(1.0) + self.playing_all = False + threading.Thread(target=run, daemon=True).start() + +if __name__ == "__main__": + if not os.path.isdir(AUDIO): + print("AUDIO dir not found:", AUDIO); sys.exit(1) + SoundBoard().mainloop()