From d7158f1f82ab594982daee6c24a0639d5dd487ce Mon Sep 17 00:00:00 2001 From: arcattack Date: Mon, 20 Jul 2026 14:51:35 -0500 Subject: [PATCH] Experience levels WIRED: the egg field drives the simulation tier (Gitea #2) BTPlayer's master ctor now seeds the flag block from btMission->ExperienceLevel() + AdvancedDamageOn() (accessors from the SURVIVED 1995 BTMSSN.HPP [T0]) instead of the NULL-stubbed scenario role -- the egg experience knob works for the first time. Both switch slot errors fixed vs the binary @4c0bc8; phantom btMission duplicate member removed (+0x1f8 IS Player::playerMission). SEVEN gate stubs unified onto the real flags (two found empirically): HeatModelActive, OwnerAdvancedDamage, LiveFireEnabled, ControlsAllowLights, powersub @4b0efc, emitter's file-local FUN_004ad7d4 + FUN_004ac9c8 stubs. FUN_004ac9c8 family swept (incl. a live raw-offset databinding trap in MechSubsystem::IsDamaged). Flag block renamed to its true semantics (simLive/heatModelOn/...), scoring-era names kept as comments. Includes the #5 [aud-tail] diag in emitter.cpp. ALL 19 shipped eggs say experience=expert -> default behavior UNCHANGED (per-tier verified: novice/standard = no heat/jams, authentic; veteran/ expert = today's exact behavior). Real delta: egg advancedDamage=1 now reaches player+0x264/0x268 (was hardwired 0). Follow-up flagged: the glass FE defaults empty experience to veteran (one tier below shipped). Awaiting human verification. Co-Authored-By: Claude Fable 5 --- context/experience-levels.md | 49 +++++++---- context/open-questions.md | 45 ++++++----- game/reconstructed/btplayer.cpp | 126 +++++++++++++++++++++-------- game/reconstructed/btplayer.hpp | 38 +++++---- game/reconstructed/btstubs.cpp | 12 ++- game/reconstructed/emitter.cpp | 41 ++++++++-- game/reconstructed/heat.hpp | 13 ++- game/reconstructed/mechsub.cpp | 14 +++- game/reconstructed/mechsub.hpp | 2 +- game/reconstructed/mislanch.cpp | 2 +- game/reconstructed/myomers.hpp | 12 ++- game/reconstructed/powersub.cpp | 37 ++++++--- game/reconstructed/projweap.cpp | 17 ++-- game/reconstructed/searchlight.hpp | 16 ++-- 14 files changed, 294 insertions(+), 130 deletions(-) diff --git a/context/experience-levels.md b/context/experience-levels.md index ac0e208..c5bcf97 100644 --- a/context/experience-levels.md +++ b/context/experience-levels.md @@ -86,22 +86,41 @@ the `Tesla40_BT_manual.pdf` scan along with p3): - **Movement heat is veteran+expert in 4.10** (the +0x260 gate), not expert-only as the 4.0 manual states. Whether a coefficient still differentiates them is open. -## Port state (as of 2026-07-18) — wiring needed [T3] +## Port state (as of 2026-07-20) — WIRED (issue #2) [T2] -The reconstruction met this system piecemeal and stubbed the same gates inconsistently: -- `btplayer.cpp` ctor seeds `roleClassIndex` from `scenarioRole->GetReturnFromDeath()` (default 2) - — the binary seeds it from `btMission->ExperienceLevel()`. Bring-up therefore runs as VETERAN. - Two slot errors in the port's switch vs the binary: the binary's switch never touches +0x264 - (that's advancedDamage, set after), and expert's `0` lands on +0x26c, not +0x270. -- `HeatModelActive()` stubbed permissive `return 1` (emitter/heat/mislanch/projweap) while - `Myomers::OwnerAdvancedDamage()` returns `False` — the SAME binary gate (+0x260) answered both - ways: heat accrues from weapons but never from movement, and no experience below veteran can - switch it off. -- `LiveFireEnabled` / `ControlsAllowLights` / powersub's short-event flag = the +0x25c not-novice - flag under three names. -- **The fix**: one accessor family on BTPlayer named members (databinding trap — never raw-read - `player+0x260`), seeded from `BTMission::ExperienceLevel()` + `AdvancedDamageOn()`, with all five - stub sites pointed at it; then an egg `experience` line actually selects the sim tier as shipped. +The wiring landed (uncommitted during the coordination pause; issue #2): +- **Seed**: the `BTPlayer` master ctor (`btplayer.cpp` ~1305) now reads + `((BTMission *)playerMission)->ExperienceLevel()` + `->AdvancedDamageOn()`. The binary's +0x1f8 + mission pointer IS the inherited `Player::playerMission` (master: set by the base ctor; + replicant: back-filled from `Application::GetCurrentMission()` = app+0xc8) — the old separate + `btMission` member was a phantom duplicate, never set on the master branch; removed. The two + switch slot errors are fixed (binary rows verified against part_013.c:10843-10870). A `[exp]` + sentinel logs the seeded flags per master player. +- **Renames** (btplayer.hpp): showDamageReceived→`simLive` (0x25c), showKills→`heatModelOn` + (0x260), showDamageInflicted→`advancedDamageOn` (0x264), roleReturnDelay→`advancedDamageOn2` + (0x268, was mistyped Scalar), roleReturnDelay2→`levelFlag26c` (0x26c), showScore→`levelFlag270` + (0x270), roleClassIndex→`experienceLevel` (0x274). +- **Bridges** (btplayer.cpp, complete-type TU): `BTPlayerExperienceSimLive` (+0x25c) and + `BTPlayerExperienceHeatModelOn` (+0x260) join `BTPlayerRoleLocksAdvanced` (+0x274==0). All + consumers route mech→`GetPlayerLink()`→named member; NULL player reads permissive [T3]. +- **Gates unified**: `HeatSink::HeatModelActive()` (heat.hpp; also serves mislanch/projweap/ + heatfamily call sites) + `Myomers::OwnerAdvancedDamage()` (myomers.hpp) → the +0x260 bridge; + `ProjectileWeapon::LiveFireEnabled()` (projweap.cpp) + `Searchlight::ControlsAllowLights()` + (searchlight.hpp) → the +0x25c bridge; `PoweredSubsystem::HandleMessage` @4b0efc (powersub.cpp) + → not-novice + the +0x25c bridge (the old "message-manager short-event flag" misattribution and + its dead `BT_GetMessageManager` guard are gone). +- **FUN_004ac9c8 family corrected**: `MechSubsystem::IsDamaged` (mechsub.cpp — was a live + raw-offset databinding trap `*(*(*(owner+0xd0)+0x190)+0x274)`), `BT_IsBusLive` (btstubs.cpp — + was `return True`), and powersub's `IsDamaged()` call sites @4b11bc/@4b21d0/@4b179c now all + route through `BTPlayerRoleLocksAdvanced`. `HeatSink::IsDamaged` (heat.hpp) deliberately keeps + the simulationState shadow for the gauge tint (btl4gaug.cpp:172). +- **SHIPPED EGGS ARE ALL `expert`** (every pilot page in all 19 content/*.EGG, camera pages + included) — so the default felt behavior is unchanged by the wiring (expert and the old + hardwired veteran agree on every LOCATED consumer: sim live, heat on, controls unlocked; they + differ only in the unlocated +0x26c flag). The glass front-end writes `veteran` when a pilot's + experience is empty (btl4fe.cpp:307). +- Myomers movement heat remains arithmetically inert (the Owner* motion accessors still return + neutral 0s — the gate is wired, the couplings are the reconciled follow-up). ## Corrections (swept 2026-07-18) diff --git a/context/open-questions.md b/context/open-questions.md index 390aefc..f227a5a 100644 --- a/context/open-questions.md +++ b/context/open-questions.md @@ -164,8 +164,9 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR btl4vid.cpp `MakeMechRenderables` reticle-build/inside pass (== arcade part_014.c:5173, Dynamic, bound per Searchlight `lightState` via a new `LightStatePtr()` accessor); (3) a toggle input (the authentic cockpit button-5→`ToggleLamp` dispatch is a controls-family reconstruction; a dev key - stands in). Also flags a deferred **`Mech::ControlsAllowLights()`** (searchlight.hpp:158 stub; - 2026-07-18: identified as the `player+0x25c` not-novice experience flag — [[experience-levels]]). + stands in). `ControlsAllowLights()` (searchlight.hpp) is WIRED since issue #2 to the + `player+0x25c` not-novice experience flag via the BTPlayerExperienceSimLive bridge + ([[experience-levels]]); the consumer (ToggleLamp) still needs the cockpit button dispatch. See [[rendering]] fog section. Verified inert live: BT_FOG_LOG shows zero `SetFogStyle(2/3)`. - **Factory capability-roster loops 2-4 are STILL DEAD (task #57 discovery).** mech.cpp's post-roster loops add to `heatableSubsystems`(0x51155c)/`weaponRoster`(0x511830)/ @@ -208,17 +209,15 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR behavior in [[experience-levels]] (+ [[decomp-reference]] §3 flag table).** Resolved en route: the old "resolve first" items — `FUN_004ad7d4` (labeled both `HeatModelActive` and `OwnerAdvancedDamage`) is ONE body reading `player+0x260` = the heat-model master switch (veteran+expert); `FUN_004ac9c8` - reads `player+0x274` = the novice lockout; the btplayer.hpp `showKills`/`roleClassIndex` scoring - names indeed do NOT match the gate semantics (comments corrected in place, member names kept). - **The wiring (small, still open):** ONE accessor family on BTPlayer named members seeded from - `BTMission::ExperienceLevel()`/`AdvancedDamageOn()` (databinding trap: do NOT raw-read - `player+0x260`), then point the five stub sites at it (`HeatModelActive` permissive-1 in - emitter/heat/mislanch/projweap vs `OwnerAdvancedDamage` False in myomers — currently the SAME gate - answered both ways; `LiveFireEnabled`/`ControlsAllowLights`/powersub short-event = the +0x25c - flag under three names; ctor seeding from role->returnFromDeath is a bring-up stand-in that runs - every mission as VETERAN). NOTE: these are MODE flags — an egg with `experience=standard` should - authentically have inert movement-heat/jams; wiring makes the tier selectable, not the basic - mission different. The MessageBoard feed is separate (StatusMessagePool, below). + reads `player+0x274` = the novice lockout. + **✅ THE WIRING LANDED (issue #2, 2026-07-20) [T2]:** the ctor now seeds from + `BTMission::ExperienceLevel()`/`AdvancedDamageOn()` via the inherited `Player::playerMission` + (the binary's +0x1f8; the old `btMission` phantom member removed), the members are renamed to + the true semantics (`simLive`/`heatModelOn`/`experienceLevel`/…), and all five gate stubs + + the FUN_004ac9c8 family (`MechSubsystem::IsDamaged`, `BT_IsBusLive`, powersub call sites) route + through the three complete-type bridges (`BTPlayerExperienceSimLive`/`…HeatModelOn`/ + `BTPlayerRoleLocksAdvanced`). Every shipped egg says `experience=expert`, so default behavior is + unchanged. Detail: [[experience-levels]] §Port state. - **✅ Authentic target acquisition RECONSTRUCTED (tasks #36/#39, 2026-07-08)** — the `Reticle` pick-ray chain is LIVE (see [[combat-damage]] Targeting for the full port map): the crosshair = **torso boresight** (NO free-aim mouse — the pod stick twisted the torso; you steer to aim) → @@ -373,16 +372,18 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR -> +0x274 == 0` = the NOVICE-experience lockout for advanced cockpit systems (2026-07-18 correction: +0x274 = the egg experience level, NOT a role class -- see [[experience-levels]]; port bridge BTPlayerRoleLocksAdvanced, btplayer.cpp; - NULL player = unlocked [T3]; bring-up seeding defaults to 2 ≈ veteran = unlocked). - AUDIT TAIL: the other - powersub.cpp sites annotated FUN_004ac9c8 (the coolant-draw gate ~:427, - ForceShortRecovery ~:444, the Generator/PowerWatcher site ~:1085) still call the - heat-family IsDamaged (simulationState != 0) stand-in -- each needs its raw fn - re-checked and swapped to the role bridge where the binary calls 4ac9c8. + NULL player = unlocked [T3]; seeded from the egg experience since issue #2). + ✅ AUDIT TAIL CLOSED (issue #2, 2026-07-20): every powersub.cpp site annotated + FUN_004ac9c8 (PoweredSubsystem::HandleMessage @4b0efc, ForceShortRecovery + @4b11bc, Generator::HandleMessage @4b21d0, PowerWatcher::HandleMessage @4b179c) + plus MechSubsystem::IsDamaged and BT_IsBusLive now call the bridge; the + state-based HeatSink::IsDamaged shadow remains only for the gauge tint. - **Myomers authentic coupling** — the structural un-stub is INERT (mover feed + heat-gen no-op). - Real coupling needs the heat-model gate (`OwnerAdvancedDamage`/`FUN_004ad7d4` → the owning - **BTPlayer** `mech+0x190`+0x260 = veteran/expert experience, NOT 0xBD3 — see [[experience-levels]]) - + `MoverAttach` routing into the LIVE JointedMover (must be reconciled with the gait cutover first). + The heat-model gate (`OwnerAdvancedDamage`/`FUN_004ad7d4` → the owning **BTPlayer** + `mech+0x190`+0x260 = veteran/expert experience — see [[experience-levels]]) is WIRED since + issue #2, but the sim stays arithmetically inert (the Owner* motion accessors return neutral + 0s). Real coupling still needs those accessors + `MoverAttach` routing into the LIVE + JointedMover (must be reconciled with the gait cutover first). - **SeekVoltageGraph** — 4 Seek* attrs unpublished (a cluster-child, not config-called; non-blocking). - **✅ DAMAGE ECONOMY — AUTHENTIC (task #8, 2026-07-11) [T1/T2].** The whole economy closes: diff --git a/game/reconstructed/btplayer.cpp b/game/reconstructed/btplayer.cpp index 96b8240..1ec19a0 100644 --- a/game/reconstructed/btplayer.cpp +++ b/game/reconstructed/btplayer.cpp @@ -1273,13 +1273,14 @@ BTPlayer::BTPlayer( // WinTesla analog, so the dead compare is dropped. @005132f4 // - // On a replicant copy, cache the application's mission/registry pointer - // (application+0xc8 in the binary; no GetMissionRegistry accessor in the - // WinTesla Application, so read through the documented offset). + // On a replicant copy, back-fill the mission pointer (the base Player ctor + // leaves playerMission NULL for replicants; the binary's @0x1f8 slot IS + // Player::playerMission -- app+0xc8 == Application::GetCurrentMission()). + // The old separate `btMission` phantom member is gone (issue #2). // - if ((simulationFlags & 0xc) == 4 && btMission == 0) // this+0x28, this[0x7e] + if ((simulationFlags & 0xc) == 4 && playerMission == 0) // this+0x28, this[0x7e] { - btMission = (Mission *)*(void **)((char *)application + 0xc8); // app+0xc8 + playerMission = application->GetCurrentMission(); // app+0xc8 } // @@ -1291,7 +1292,7 @@ BTPlayer::BTPlayer( // CString role_key(creation_message->roleName); // make+0x90 (void)role_key; - // scenarioRole = btMission->GetRoleRegistry()->Lookup(&role_key); // this[0x7e]+0x50, this[0x82] + // scenarioRole = playerMission->GetRoleRegistry()->Lookup(&role_key); // this[0x7e]+0x50, this[0x82] if ((simulationFlags & 0xc) == 4) { @@ -1305,42 +1306,54 @@ BTPlayer::BTPlayer( // // Master: derive the game-mode flags from the EXPERIENCE level. // - // CORRECTED SOURCE (2026-07-18 [T1]): the binary reads - // btMission->experienceLevel (mission+0xe4, the egg's per-pilot - // "experience" 0..3 = novice/standard/veteran/expert) — NOT the role's - // returnFromDeath (the old reading). See context/experience-levels.md. + // WIRED (issue #2, 2026-07-20 [T1]): the binary @004c0bc8 + // (part_013.c:10843-10870) reads btMission->experienceLevel + // (mission+0xe4, the egg's per-pilot "experience" 0..3 = + // novice/standard/veteran/expert) — NOT the role's returnFromDeath (the + // old stand-in, which NULL-defaulted every pilot to veteran). The + // mission pointer is the inherited Player::playerMission (= the + // binary's this+0x1f8), set by the base ctor on the master branch; the + // concrete mission is a BTL4Mission (is-a BTMission). Binary rows for + // (0x25c,0x260,0x26c,0x270): nov 0,0,0,0 / std 1,0,1,1 / vet 1,1,1,1 / + // exp 1,1,0,1. Note the two old slot errors, now fixed: the binary + // switch never touches advancedDamageOn(0x264) (set below), and + // expert's 0 lands on levelFlag26c(0x26c), not levelFlag270(0x270). + // See context/experience-levels.md. // - // STAND-IN [T3]: still seeded from the role (default 2 ≈ VETERAN) until - // the wiring task points this at BTMission::ExperienceLevel(). Known - // slot drift vs the binary switch: the binary never touches - // showDamageInflicted(0x264) here (that's the advancedDamage copy, set - // below), and expert's 0 lands on roleReturnDelay2(0x26c), not - // showScore(0x270) — binary rows: nov 0,0,0,0 / std 1,0,1,1 / - // vet 1,1,1,1 / exp 1,1,0,1 for (0x25c,0x260,0x26c,0x270). - roleClassIndex = scenarioRole ? scenarioRole->GetReturnFromDeath() : 2; // this[0x9d] <- mission+0xe4 in the binary - switch (roleClassIndex) + BTMission *bt_mission = (BTMission *)playerMission; // this[0x7e] + experienceLevel = bt_mission != 0 + ? bt_mission->ExperienceLevel() // mission+0xe4 + : (int)BTMission::VeteranMode; // [T3] dev-permissive; the binary derefs unguarded (master always has a mission) + switch (experienceLevel) // this[0x9d] { - case 0: - showKills = showDamageReceived = showDamageInflicted = showScore = 0; + case BTMission::NoviceMode: // 0: everything off + heatModelOn = simLive = levelFlag26c = levelFlag270 = 0; break; - case 1: - showKills = 0; - showDamageReceived = showDamageInflicted = showScore = 1; + case BTMission::StandardMode: // 1: live, no heat model + heatModelOn = 0; + simLive = levelFlag26c = levelFlag270 = 1; break; - case 2: - showKills = showDamageReceived = showDamageInflicted = showScore = 1; + case BTMission::VeteranMode: // 2: everything on + heatModelOn = simLive = levelFlag26c = levelFlag270 = 1; break; - case 3: - showKills = showDamageReceived = showDamageInflicted = 1; - showScore = 0; + case BTMission::ExpertMode: // 3: on, but 0x26c off + heatModelOn = simLive = levelFlag270 = 1; + levelFlag26c = 0; break; } - // CORRECTED (2026-07-18 [T1]): the binary sets this[0x99]/[0x9a] + // The binary then unconditionally copies this[0x99]/[0x9a] // (0x264/0x268) = btMission->advancedDamageOn (mission+0xf0, the egg - // "advancedDamage" technician flag) — an int pair, not role floats. - // 0 here = advanced damage OFF, which matches the bring-up eggs. [T3] - roleReturnDelay = roleReturnDelay2 = 0.0f; // this[0x99],[0x9a] <- mission+0xf0 in the binary + // "advancedDamage" technician flag; an int pair). [T1] + advancedDamageOn = advancedDamageOn2 = // this[0x99],[0x9a] + bt_mission != 0 ? bt_mission->AdvancedDamageOn() : False; + + // Verification sentinel (issue #2): one line per master player. + DEBUG_STREAM << "[exp] master player experience=" << experienceLevel + << " simLive=" << (int)simLive + << " heatModelOn=" << (int)heatModelOn + << " advDamage=" << (int)advancedDamageOn + << "\n" << std::flush; } killCount = 0; // this[0x9f] @@ -1617,7 +1630,52 @@ int BTPlayerRoleLocksAdvanced(void *owner_mech) BTPlayer *player = MECH_OWNING_PLAYER(owner_mech); if (player == 0) return 0; // dev-permissive [T3] - return (player->roleClassIndex == 0) ? 1 : 0; // player+0x274 == 0 + return (player->experienceLevel == 0) ? 1 : 0; // player+0x274 == 0 +} + + +//############################################################################# +// BTPlayerExperienceSimLive -- complete-type bridge (issue #2) +// +// The player+0x25c "sim live" flag [T1]: 0 only for NOVICE experience. Binary +// consumers reached through the owner mech's playerLink (mech+0x190): +// * CheckForJam @004bbfcc (projweap.cpp LiveFireEnabled) -- no jam rolls; +// * Searchlight::ToggleLamp @004b860c (searchlight.hpp ControlsAllowLights); +// * PoweredSubsystem::HandleMessage @004b0efc short-event path (powersub.cpp). +// A NULL player (target dummy / unbound dev mech) reads as LIVE so dev rigs +// keep today's behavior [T3]; the binary derefs unguarded. +// +int BTPlayerExperienceSimLive(void *owner_mech) +{ + if (owner_mech == 0) + return 1; // dev-permissive [T3] + BTPlayer *player = MECH_OWNING_PLAYER(owner_mech); + if (player == 0) + return 1; // dev-permissive [T3] + return player->simLive ? 1 : 0; // player+0x25c +} + + +//############################################################################# +// BTPlayerExperienceHeatModelOn -- complete-type bridge (issue #2) +// +// FUN_004ad7d4 [T1]: `return *(*(*(sub+0xD0)+0x190)+0x260)` -- the HEAT-MODEL +// master switch, ON only for VETERAN and EXPERT experience. The single +// most-called gate in the subsystem family: HeatSink::HeatModelActive() +// (heat.hpp -- weapon-fire heat, the heat.cpp simulation, coolant), missile +// launch heat (mislanch.cpp), projectile firing heat (projweap.cpp), and +// Myomers::OwnerAdvancedDamage() (movement heat, myomers.hpp). Standard mode +// has NO heat consequences -- authentic, not a port gap. A NULL player reads +// as ON (today's permissive stub behavior) so dev rigs are unchanged [T3]. +// +int BTPlayerExperienceHeatModelOn(void *owner_mech) +{ + if (owner_mech == 0) + return 1; // dev-permissive [T3] + BTPlayer *player = MECH_OWNING_PLAYER(owner_mech); + if (player == 0) + return 1; // dev-permissive [T3] + return player->heatModelOn ? 1 : 0; // player+0x260 } diff --git a/game/reconstructed/btplayer.hpp b/game/reconstructed/btplayer.hpp index 548c292..0e3bbe5 100644 --- a/game/reconstructed/btplayer.hpp +++ b/game/reconstructed/btplayer.hpp @@ -342,8 +342,12 @@ class DropZone__ReplyMessage; // Scalar currentScore; // @0x278 (Player) -- running score // --- BT-specific --- - Mission - *btMission; // @0x1f8 cached mission / role registry source + // (The binary's @0x1f8 mission pointer IS the inherited + // Player::playerMission -- the master ctor leaves the base's value and + // the replicant branch back-fills it from Application::GetCurrentMission. + // The old separate `btMission` member here was a phantom duplicate of + // the base field, never set on the master branch; removed in the + // issue-#2 experience wiring.) char teamName[64]; // @0x20c copied from MakeMessage (+0x50) BTTeam @@ -358,28 +362,30 @@ class DropZone__ReplyMessage; // // EXPERIENCE-LEVEL game-mode flags (2026-07-18 [T1] — the old "per-role // display toggles / returnFromDeath" reading was WRONG). The ctor - // @004c0bc8 seeds this block from btMission(+0x1f8)->experienceLevel + // @004c0bc8 seeds this block from playerMission(+0x1f8)->experienceLevel // (+0xe4, the egg's per-pilot "experience" novice/standard/veteran/ // expert) and ->advancedDamageOn(+0xf0) — NOT from the scenario role. - // The member NAMES below date from the scoring work and do not match - // the semantics; kept until the wiring task renames them. Binary - // mapping (nov/std/vet/exp) + consumers: context/experience-levels.md. + // Renamed to the true semantics by the issue-#2 wiring (old scoring-era + // names in the trailing comments). Binary mapping (nov/std/vet/exp) + + // consumers: context/experience-levels.md. // Logical - showDamageReceived; // @0x25c 0/1/1/1 "sim live" (novice lockout: jams, searchlight, powersub @4b0efc) + simLive; // @0x25c 0/1/1/1 "sim live" novice lockout: jams, searchlight, powersub @4b0efc (was showDamageReceived) Logical - showKills; // @0x260 0/0/1/1 HEAT-MODEL master switch (FUN_004ad7d4) + heatModelOn; // @0x260 0/0/1/1 HEAT-MODEL master switch (FUN_004ad7d4) (was showKills) Logical - showDamageInflicted;// @0x264 = mission advancedDamageOn (copy 1; NOT part of the level switch) + advancedDamageOn; // @0x264 = mission advancedDamageOn (copy 1; NOT part of the level switch) (was showDamageInflicted) Logical - showScore; // @0x270 0/1/1/1 consumer not located - Scalar - roleReturnDelay; // @0x268 = mission advancedDamageOn (copy 2; int in the binary) - Scalar - roleReturnDelay2; // @0x26c 0/1/1/0 consumer not located (supercharge-clamp hypothesis [T4]) + levelFlag270; // @0x270 0/1/1/1 consumer not located (was showScore) + Logical + advancedDamageOn2; // @0x268 = mission advancedDamageOn (copy 2; int in the binary) (was roleReturnDelay, mistyped Scalar) + Logical + levelFlag26c; // @0x26c 0/1/1/0 consumer not located (supercharge-clamp hypothesis [T4]) (was roleReturnDelay2, mistyped Scalar) int - roleClassIndex; // @0x274 raw experience level 0..3 (FUN_004ac9c8 = ==0 novice predicate) - friend int BTPlayerRoleLocksAdvanced(void *); // the FUN_004ac9c8 bridge (task #12) + experienceLevel; // @0x274 raw experience level 0..3 (FUN_004ac9c8 = ==0 novice predicate) (was roleClassIndex) + friend int BTPlayerRoleLocksAdvanced(void *); // the FUN_004ac9c8 bridge (task #12): experienceLevel == 0 + friend int BTPlayerExperienceSimLive(void *); // +0x25c reader (issue #2): jams / lights / powersub short path + friend int BTPlayerExperienceHeatModelOn(void *); // +0x260 reader (issue #2): FUN_004ad7d4 heat-model gate friend void BTPlayerCountObservedDeath(void *); // observed-death tally (MP DEATHS fix) int diff --git a/game/reconstructed/btstubs.cpp b/game/reconstructed/btstubs.cpp index 503a025..5db8455 100644 --- a/game/reconstructed/btstubs.cpp +++ b/game/reconstructed/btstubs.cpp @@ -222,11 +222,15 @@ SubsystemMessageManager *BT_GetMessageManager(Mech * /*owner*/) return 0; } -// The running player mech is a live, powered master -> its electrical bus is up. -// (Real impl read *(*(owner+0x190)+0x274); see layout note above.) -Logical BT_IsBusLive(Mech * /*owner*/) +// CORRECTED (issue #2 [T1]): *(*(owner+0x190)+0x274) != 0 is the owning +// BTPlayer's raw EXPERIENCE level being non-zero -- i.e. NOT-NOVICE (the +// Generator::HandleMessage @004b21d0 outer gate), not an electrical-bus +// probe. Routed through the complete-type bridge (databinding rule); a NULL +// player reads unlocked -> "live", preserving the old permissive dev behavior. +Logical BT_IsBusLive(Mech *owner) { - return True; + extern int BTPlayerRoleLocksAdvanced(void *owner_mech); // btplayer.cpp (FUN_004ac9c8) + return BTPlayerRoleLocksAdvanced(owner) ? False : True; } // No known-offset myomer back-pointer in the reconstructed Mech to clear. diff --git a/game/reconstructed/emitter.cpp b/game/reconstructed/emitter.cpp index 6e715d2..9786774 100644 --- a/game/reconstructed/emitter.cpp +++ b/game/reconstructed/emitter.cpp @@ -49,7 +49,7 @@ // FUN_004b9948 MechWeapon::GetMuzzlePoint FUN_004b9864 MechWeapon::ComputeAimOrientation // FUN_004b9728 MechWeapon::DrawWeaponPip FUN_004b9cbc MechWeapon::GetTargetPosition // FUN_004b0bd0 PoweredSubsystem simulation step FUN_004b0d50 PoweredSubsystem scalar query -// FUN_004ad7d4 HeatableSubsystem "is online/active" FUN_004ac9c8 HeatableSubsystem heat-state query +// FUN_004ad7d4 heat-model experience gate (player+0x260; HeatModelActive) FUN_004ac9c8 novice lockout (player+0x274==0; MechSubsystem::IsDamaged) // FUN_00417ab4 follow a SharedData link (voltage source / ammo bin) // FUN_0049fb54 Mech::IsDestroyed (movementMode 2||9) FUN_0041a1a4 IsDerivedFrom(classDerivations) // FUN_0041bbd8 AlarmIndicator::SetLevel(n) (weaponAlarm @0x350, state @0x364) @@ -74,6 +74,8 @@ #endif #include // HostManager::GetEntityPointer -- the update-record target resolve (task #51) #include // sqrt -- the SeekVoltageResponse sampler (issue #11) +#include // [aud-tail] per-weapon previous-state tracking (Gitea #5, instrumentation only) +#include // [aud-tail] clock() ms timestamps (matches L4AUDLVL.cpp) // E8 (bring-up): the player fire input. The controls mapper that would normally write // the weapon's fireImpulse is bypassed (mech4.cpp), so EmitterSimulation reads this @@ -108,8 +110,12 @@ static void Normalize(EulerAngles &) {} static void MakeIdentity(LinearMatrix &m) { m.BuildIdentity(); } static void CopyColor(void *, const void *) {} -// engine-internal artifacts the decomp called by address (stand-ins) -static int FUN_004ad7d4(void *) { return 1; } // HeatableSubsystem active? +// (issue #2: the local FUN_004ad7d4 `return 1` / FUN_004ac9c8 `return 0` +// stand-ins are RETIRED -- FUN_004ad7d4 is the player+0x260 HEAT-MODEL +// experience gate (HeatSink::HeatModelActive(), heat.hpp -> the btplayer.cpp +// bridge) and FUN_004ac9c8 is the player+0x274==0 NOVICE lockout +// (MechSubsystem::IsDamaged() -> the same bridge family); call sites now use +// the real members. See context/experience-levels.md.) // FUN_0049fb54 == Mech::IsDestroyed (movementMode 2||9; reconstructed as // Mech::IsDisabled). Was a mislabeled "cockpit/HUD query" no-op stub -- so a // live beam never CUT when its target mech died (@004ba8d0/@004baa20 check @@ -131,7 +137,6 @@ static int BTMechDestroyed(Entity *e) // TrackSeekVoltage never charged. The E7 one-frame force-charge that covered // for it made every emitter recycle in ~0.3s instead of the authored 2-6s // RechargeRate -- the actual root cause of the task-#10 heat runaway.) -static int FUN_004ac9c8(void *) { return 0; } // heat-state query static int FUN_00421070(void *, void *) { return 0; } // damage source resolve static void *ResolveSource(void *) { return 0; } // named source registry lookup @@ -281,7 +286,7 @@ void // E5: pendingHeat (HeatSink @0x1C8) is the field the heat sim absorbs each frame // (heatEnergy += pendingHeat -> temperature); the old Emitter-local heatAccumulator // was a SHADOW at the wrong offset, so firing never reached the heat model. - if (FUN_004ad7d4(this)) // HeatableSubsystem active + if (HeatModelActive()) // FUN_004ad7d4 -- player+0x260 heat-model experience gate (issue #2) { pendingHeat += heatPortion; // inherited HeatSink::pendingHeat @0x1c8 } @@ -502,6 +507,26 @@ void default: break; } + + // [aud-tail] (Gitea #5 verdict harness, additive): timestamped WeaponState + // transitions. Leave-0 (Firing) == the beam-visual end (firingActive is + // cleared on the same path); 3->2 (Loading->Loaded) is where the authored + // charge-loop stop watcher fires StopNote and the release fade begins. + // Correlate against the [aud-tail] logs in L4AUDLVL.cpp (same CRT clock() + // ms). BT_AUD_TAIL=1 gates; instrumentation only, no behavior change. + if (getenv("BT_AUD_TAIL")) + { + static std::map s_prevState; + int st = (int)weaponAlarm.GetState(); + std::map::iterator it = s_prevState.find(this); + if (it == s_prevState.end() || it->second != st) + { + DEBUG_STREAM << "[aud-tail] t=" << (long)clock() << "ms " << GetName() + << " state " << ((it == s_prevState.end()) ? -1 : it->second) + << " -> " << st << "\n" << std::flush; + s_prevState[this] = st; + } + } } @@ -530,7 +555,7 @@ void seekRate = (src->MeasuredVoltage() - currentLevel) / dtScale; // 0x45c = (src+0x1dc - 0x414)/.. currentLevel = (seekRate / energyCoefficient) * time_slice + currentLevel; // 0x414 - if (FUN_004ad7d4(this)) // heatable/online + if (HeatModelActive()) // FUN_004ad7d4 -- player+0x260 heat-model experience gate (issue #2) { // @004ba838:7544 -- the charging I^2R loss lands in the GENERATOR's // pendingHeat (src+0x1c8): recharging weapons is what heats generators @@ -628,11 +653,11 @@ void int Emitter::AdvanceSeekVoltage(UpdateRecord *message) { - if (FUN_004ac9c8(this)) // heat-state set -> skip + if (MechSubsystem::IsDamaged()) // FUN_004ac9c8 -- NOVICE lockout (seek-voltage cycling is an advanced cockpit control; issue #2) { return /*non-zero status*/ 1; } - if (!FUN_004ad7d4(this) || message->payloadCount <= 0) // message+0xc + if (!HeatModelActive() || message->payloadCount <= 0) // FUN_004ad7d4 -- heat-model gate; message+0xc { return 0; } diff --git a/game/reconstructed/heat.hpp b/game/reconstructed/heat.hpp index 0cb0949..5f40ae7 100644 --- a/game/reconstructed/heat.hpp +++ b/game/reconstructed/heat.hpp @@ -417,9 +417,18 @@ inline int void HeatSink_Step(Scalar time_slice) { HeatSinkSimulation(time_slice); } - // FUN_004ad7d4 -- entity heat-model active flag. + // FUN_004ad7d4 -- the heat-model master switch [T1]: the owning + // BTPlayer's +0x260 experience flag (mech+0x190 playerLink), ON only + // for veteran/expert experience (issue #2 wiring; the old permissive + // `return True` stub ran the heat model for every tier). Routed + // through the complete-type bridge in btplayer.cpp (databinding rule: + // never raw-read player offsets). NULL player reads ON [T3]. Logical - HeatModelActive() { return True; } + HeatModelActive() + { + extern int BTPlayerExperienceHeatModelOn(void *owner_mech); // btplayer.cpp (FUN_004ad7d4) + return BTPlayerExperienceHeatModelOn(owner) ? True : False; + } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Subsystem virtual overrides (slots on vtable @0050edc4) diff --git a/game/reconstructed/mechsub.cpp b/game/reconstructed/mechsub.cpp index 0328282..8396f38 100644 --- a/game/reconstructed/mechsub.cpp +++ b/game/reconstructed/mechsub.cpp @@ -366,13 +366,21 @@ void } // -// @0x4ac9c8 -- IsDamaged: true (dead) when the owning mech's electrical bus is -// down -- *(*(*(owner+0xd0)+0x190)+0x274) == 0. +// @0x4ac9c8 -- the NOVICE-experience lockout predicate (issue #2 [T1]): +// *(*(*(sub+0xd0)+0x190)+0x274) == 0, i.e. owner mech -> playerLink -> +// BTPlayer experienceLevel == 0. NOT a damage/bus query (the old label), and +// the old body raw-read the binary offset chain off `owner` (already the +// Mech, so the extra +0xd0 hop was doubly wrong) -- the databinding trap. +// Routed through the complete-type bridge in btplayer.cpp. NOTE: +// HeatSink::IsDamaged (heat.hpp) deliberately shadows this with a +// simulationState check for the gauge tint (btl4gaug.cpp); the FUN_004ac9c8 +// call sites in powersub.cpp call the bridge directly. // Logical MechSubsystem::IsDamaged() { - return (*(int *)(*(int *)(*(int *)((char *)owner + 0xd0) + 0x190) + 0x274) == 0); + extern int BTPlayerRoleLocksAdvanced(void *owner_mech); // btplayer.cpp (FUN_004ac9c8) + return BTPlayerRoleLocksAdvanced(owner) ? True : False; } diff --git a/game/reconstructed/mechsub.hpp b/game/reconstructed/mechsub.hpp index cd7333b..fd53efd 100644 --- a/game/reconstructed/mechsub.hpp +++ b/game/reconstructed/mechsub.hpp @@ -243,7 +243,7 @@ class Damage; virtual void TakeDamage(Damage &damage); // slot via this+0x24 (see DamageDelta) Logical - IsDamaged(); // @0x4ac9c8 (bus-state query) + IsDamaged(); // @0x4ac9c8 -- NOVICE-experience lockout predicate (player+0x274 == 0), NOT a damage query (issue #2); see mechsub.cpp // Critical-state readouts for ColorMapperCritical (CriticalConnection): // the raw simulation state (DefaultState=0 / Destroyed=1 / Exploding=2) and diff --git a/game/reconstructed/mislanch.cpp b/game/reconstructed/mislanch.cpp index 531b215..83c4b70 100644 --- a/game/reconstructed/mislanch.cpp +++ b/game/reconstructed/mislanch.cpp @@ -278,7 +278,7 @@ void MissileLauncher::FireWeapon() // 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. - if (HeatModelActive()) // FUN_004ad7d4 (heat.hpp; [T3] permissive) + if (HeatModelActive()) // FUN_004ad7d4 (heat.hpp -> player+0x260 experience gate; issue #2) { pendingHeat += heatCostToFire; // HeatSink @0x1C8, raw } diff --git a/game/reconstructed/myomers.hpp b/game/reconstructed/myomers.hpp index 0ef5416..88847e4 100644 --- a/game/reconstructed/myomers.hpp +++ b/game/reconstructed/myomers.hpp @@ -312,7 +312,17 @@ class Mech; Scalar OwnerVelocityY() const { return 0.0f; } // Mech velocity.y Scalar OwnerMotionGain() const { return 0.0f; } // Mech +0x20C Scalar OwnerMass() const { return 0.0f; } // *Mech +0x250 - Logical OwnerAdvancedDamage() const { return False; } // advanced-damage gate (keeps sim INERT) + // FUN_004ad7d4 [T1] -- the SAME player+0x260 heat-model experience gate + // as HeatSink::HeatModelActive() (the "advanced damage" name was a + // misread; the old `return False` stub kept movement heat off for + // every tier). Issue #2: routed through the btplayer.cpp bridge. + // (MyomersSimulation stays arithmetically inert until the Owner* + // accessors above are wired -- every heat term multiplies a 0.) + Logical OwnerAdvancedDamage() const + { + extern int BTPlayerExperienceHeatModelOn(void *owner_mech); // btplayer.cpp (FUN_004ad7d4) + return BTPlayerExperienceHeatModelOn(owner) ? True : False; + } Scalar DamageStructureLevel() const{ return 0.0f; } // DamageZone +0x158 void MoverAttach(int index, Scalar *feed) { (void)index; (void)feed; } // JointedMover vtable +0x38 (no-op) diff --git a/game/reconstructed/powersub.cpp b/game/reconstructed/powersub.cpp index 4bd0696..cedf078 100644 --- a/game/reconstructed/powersub.cpp +++ b/game/reconstructed/powersub.cpp @@ -449,20 +449,22 @@ void // @004b0efc -- pass the message to the HeatSink base; if the part is alive, // the source short flag is set and message 4 arrives, force a short-recovery. // +extern int BTPlayerExperienceSimLive(void *owner_mech); // btplayer.cpp (player+0x25c; issue #2) +extern int BTPlayerRoleLocksAdvanced(void *owner_mech); // btplayer.cpp (FUN_004ac9c8: player+0x274 == 0) + Logical PoweredSubsystem::HandleMessage(int message) { HeatSink::HandleMessage(message); // FUN_004ad748 - // Raw @004b0efc reads *(int*)(*(owner+0x190) + 0x25c) -- the message-manager's - // short-event flag (NOT a Generator field). The manager is null in bring-up - // (BT_GetMessageManager stub), so guard the deref; the real read resumes once - // the SubsystemMessageManager (0xBD3) roster lookup is wired. - SubsystemMessageManager *msgMgr = BT_GetMessageManager((Mech *)owner); // *(this[0x34]+0x190) - - if (!IsDamaged() // FUN_004ac9c8 == 0 - && msgMgr != 0 - && *(LWord *)((char *)msgMgr + 0x25c) != 0 // message-manager short-event flag + // CORRECTED (issue #2 [T1]): @004b0efc reads *(int*)(*(owner+0x190)+0x25c) + // -- owner mech+0x190 is the PLAYER LINK, and +0x25c is the BTPlayer's + // "sim live" EXPERIENCE flag (0 only for novice), NOT a message-manager + // field (the old misattribution left this path dead behind the null + // BT_GetMessageManager stub). The leading FUN_004ac9c8==0 term is the + // not-novice predicate (BTPlayerRoleLocksAdvanced), not a damage query. + if (!BTPlayerRoleLocksAdvanced(owner) // FUN_004ac9c8 == 0 (not novice) + && BTPlayerExperienceSimLive(owner) != 0 // *(player+0x25c) != 0 && message == 4) { ForceShortRecovery(); // FUN_004b11bc(this) @@ -477,10 +479,15 @@ Logical void PoweredSubsystem::ForceShortRecovery() // internal helper for @004b0efc { - if (IsDamaged()) // FUN_004ac9c8 + // CORRECTED (issue #2 [T1]): both FUN_004ac9c8 calls here are the + // not-novice EXPERIENCE predicate on the owning player (each subsystem + // hops sub->owner mech->playerLink; this part and its source share the + // same mech, hence the same player) -- NOT damage-state queries (the old + // HeatSink::IsDamaged stand-in read simulationState). + if (BTPlayerRoleLocksAdvanced(owner)) // FUN_004ac9c8(this) return; Generator *source = (Generator *)voltageSource.Resolve(); // FUN_00417ab4(this+0x1d0) - if (source != 0 && !source->IsDamaged()) + if (source != 0 && !BTPlayerRoleLocksAdvanced(owner)) // FUN_004ac9c8(source): same mech -> same player { source->stateAlarm.SetLevel(Generator::GeneratorShorted); // source+0x1fc -> 3 source->outputVoltage = 0.0f; // source+0x1f8 = 0 @@ -1086,7 +1093,8 @@ Logical { if (message == 4) { - if ((simulationState == 2 || simulationState == 0) && !IsDamaged()) // this[0x10]; FUN_004ac9c8 + if ((simulationState == 2 || simulationState == 0) // this[0x10] + && !BTPlayerRoleLocksAdvanced(owner)) // FUN_004ac9c8 == 0 (not novice; issue #2 -- was the state-based IsDamaged stand-in, which wrongly blocked state 2) { stateAlarm.SetLevel(GeneratorShorted); // 3 shortTimer = 0.0f; // this[0x7e] @@ -1255,7 +1263,10 @@ Logical Generator *source = (watched != 0) // FUN_00417ab4(watched+0x1d0) ? (Generator *)((PoweredSubsystem *)watched)->ResolveVoltageSource() : 0; - if (source != 0 && !source->IsDamaged()) + // @004b179c's FUN_004ac9c8(source) [T1] = the not-novice EXPERIENCE + // predicate on the source's owner (same mech as ours; issue #2 -- was + // the state-based IsDamaged stand-in). + if (source != 0 && !BTPlayerRoleLocksAdvanced(owner)) { source->stateAlarm.SetLevel(Generator::GeneratorShorted); // source+0x1fc -> 3 source->outputVoltage = 0.0f; // source+0x1f8 = 0 diff --git a/game/reconstructed/projweap.cpp b/game/reconstructed/projweap.cpp index a6b579e..343fb83 100644 --- a/game/reconstructed/projweap.cpp +++ b/game/reconstructed/projweap.cpp @@ -60,7 +60,7 @@ // FUN_004b9d00 MechWeapon::PrintState (slot 13 base) // FUN_004b9948 MechWeapon::GetMuzzlePoint FUN_004b9bdc MechWeapon::UpdateTargeting // FUN_004b9cbc MechWeapon::GetTargetPosition FUN_004b0d50 voltage helper -// FUN_004ad7d4 HeatableSubsystem present? FUN_00417ab4 Connection::Resolve +// FUN_004ad7d4 heat-model experience gate (player+0x260; HeatModelActive) FUN_00417ab4 Connection::Resolve // FUN_0041bbd8 AlarmIndicator::SetLevel(n) FUN_0041db7c Damage init // FUN_004179d4 / FUN_004179f8 SharedData::Connection ctor/dtor // FUN_004022d0 operator delete FUN_004215b0 resolve subsystem name->index @@ -227,7 +227,13 @@ Subsystem * Logical ProjectileWeapon::LiveFireEnabled() const { - return True; + // CheckForJam @004bbfcc's early-out [T1]: the owning BTPlayer's +0x25c + // "sim live" experience flag (mech+0x190 playerLink) -- 0 only for NOVICE + // experience, whose ballistics never jam. Issue #2: the old permissive + // `return True` stub let every tier roll jams; routed through the + // complete-type bridge in btplayer.cpp (databinding rule). + extern int BTPlayerExperienceSimLive(void *owner_mech); // btplayer.cpp (player+0x25c) + return BTPlayerExperienceSimLive(owner) ? True : False; } // @@ -507,8 +513,9 @@ void // uniform random sample falls under the current jam probability: // p = clamp(0.41 * currentTemperature / failureTemperature, // minJamChance, 1.0) -// Gated on the weapon being heat-active (this+0x184) and the owning Mech's -// "live fire" flag (mech.controls+0x25c). +// Gated on the weapon being heat-active (this+0x184) and the owning PLAYER's +// "sim live" experience flag (mech+0x190 playerLink -> player+0x25c; novice +// never jams -- issue #2). // Logical ProjectileWeapon::CheckForJam() @@ -772,7 +779,7 @@ void // binary adds heatCostToFire RAW to the weapon's own pendingHeat -- // projectile resources author PRE-scaled 1e7-unit heat (AFC100 = 6.5e7) // -- gated on the heatable check (part_013.c:8744-8747 idiom). - if (HeatModelActive()) // FUN_004ad7d4 (heat.hpp; [T3] permissive) + if (HeatModelActive()) // FUN_004ad7d4 (heat.hpp -> player+0x260 experience gate; issue #2) { pendingHeat += heatCostToFire; // HeatSink @0x1C8, raw } diff --git a/game/reconstructed/searchlight.hpp b/game/reconstructed/searchlight.hpp index 2b626e2..bc81f3c 100644 --- a/game/reconstructed/searchlight.hpp +++ b/game/reconstructed/searchlight.hpp @@ -148,14 +148,20 @@ // SetGraphicsDirty -> engine Simulation::ForceUpdate() (this+0x18) // SetInstanceFlag -> engine Simulation::simulationFlags |= bits (this+0x28) // GetSegmentFlags() removed (the ctor gate now reads owner->simulationFlags). - // ControlsAllowLights(): no ported base member (the real read is the - // owner controls-mapper lightsEnabled via an unsafe raw owner-offset - // chain) -> return True for bring-up; the only consumer (ToggleLamp) is - // dormant. Faithful follow-up = a real Mech::ControlsAllowLights(). + // ControlsAllowLights() [T1]: the ToggleLamp @004b860c gate is the + // owning BTPlayer's +0x25c "sim live" EXPERIENCE flag (mech+0x190 + // playerLink -> player+0x25c; the old "controls-mapper lightsEnabled" + // reading was wrong) -- 0 only for NOVICE experience. Issue #2: + // routed through the complete-type bridge in btplayer.cpp + // (databinding rule: never raw-read player offsets). Logical HostShutDown() const { return simulationState == 1; /* Destroyed */ } int WatchedVoltageLevel() const { return watchdogAlarm.GetLevel(); } // @0x198 int HeatStateLevel() const { return heatAlarm.GetLevel(); } // @0x140 - Logical ControlsAllowLights() const { return True; } // bring-up (see above) + Logical ControlsAllowLights() const + { + extern int BTPlayerExperienceSimLive(void *owner_mech); // btplayer.cpp (player+0x25c) + return BTPlayerExperienceSimLive(owner) ? True : False; + } void SetGraphicsDirty() { ForceUpdate(); } // this[0x18] |= 1 void SetInstanceFlag(int bits) { simulationFlags |= bits; } // this[10] |= bits // The "ToggleLamp" command arg is carried at message+0xC (decomp).