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 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-20 14:51:35 -05:00
co-authored by Claude Fable 5
parent a0cec48e3f
commit d7158f1f82
14 changed files with 294 additions and 130 deletions
+34 -15
View File
@@ -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 - **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. 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: The wiring landed (uncommitted during the coordination pause; issue #2):
- `btplayer.cpp` ctor seeds `roleClassIndex` from `scenarioRole->GetReturnFromDeath()` (default 2) - **Seed**: the `BTPlayer` master ctor (`btplayer.cpp` ~1305) now reads
— the binary seeds it from `btMission->ExperienceLevel()`. Bring-up therefore runs as VETERAN. `((BTMission *)playerMission)->ExperienceLevel()` + `->AdvancedDamageOn()`. The binary's +0x1f8
Two slot errors in the port's switch vs the binary: the binary's switch never touches +0x264 mission pointer IS the inherited `Player::playerMission` (master: set by the base ctor;
(that's advancedDamage, set after), and expert's `0` lands on +0x26c, not +0x270. replicant: back-filled from `Application::GetCurrentMission()` = app+0xc8) — the old separate
- `HeatModelActive()` stubbed permissive `return 1` (emitter/heat/mislanch/projweap) while `btMission` member was a phantom duplicate, never set on the master branch; removed. The two
`Myomers::OwnerAdvancedDamage()` returns `False` — the SAME binary gate (+0x260) answered both switch slot errors are fixed (binary rows verified against part_013.c:10843-10870). A `[exp]`
ways: heat accrues from weapons but never from movement, and no experience below veteran can sentinel logs the seeded flags per master player.
switch it off. - **Renames** (btplayer.hpp): showDamageReceived→`simLive` (0x25c), showKills→`heatModelOn`
- `LiveFireEnabled` / `ControlsAllowLights` / powersub's short-event flag = the +0x25c not-novice (0x260), showDamageInflicted→`advancedDamageOn` (0x264), roleReturnDelay→`advancedDamageOn2`
flag under three names. (0x268, was mistyped Scalar), roleReturnDelay2→`levelFlag26c` (0x26c), showScore→`levelFlag270`
- **The fix**: one accessor family on BTPlayer named members (databinding trap — never raw-read (0x270), roleClassIndex→`experienceLevel` (0x274).
`player+0x260`), seeded from `BTMission::ExperienceLevel()` + `AdvancedDamageOn()`, with all five - **Bridges** (btplayer.cpp, complete-type TU): `BTPlayerExperienceSimLive` (+0x25c) and
stub sites pointed at it; then an egg `experience` line actually selects the sim tier as shipped. `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) ## Corrections (swept 2026-07-18)
+23 -22
View File
@@ -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, 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 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 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; stands in). `ControlsAllowLights()` (searchlight.hpp) is WIRED since issue #2 to the
2026-07-18: identified as the `player+0x25c` not-novice experience flag — [[experience-levels]]). `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)`. 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 - **Factory capability-roster loops 2-4 are STILL DEAD (task #57 discovery).** mech.cpp's
post-roster loops add to `heatableSubsystems`(0x51155c)/`weaponRoster`(0x511830)/ 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 behavior in [[experience-levels]] (+ [[decomp-reference]] §3 flag table).** Resolved en route: the
old "resolve first" items — `FUN_004ad7d4` (labeled both `HeatModelActive` and `OwnerAdvancedDamage`) 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` 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 reads `player+0x274` = the novice lockout.
names indeed do NOT match the gate semantics (comments corrected in place, member names kept). **✅ THE WIRING LANDED (issue #2, 2026-07-20) [T2]:** the ctor now seeds from
**The wiring (small, still open):** ONE accessor family on BTPlayer named members seeded from `BTMission::ExperienceLevel()`/`AdvancedDamageOn()` via the inherited `Player::playerMission`
`BTMission::ExperienceLevel()`/`AdvancedDamageOn()` (databinding trap: do NOT raw-read (the binary's +0x1f8; the old `btMission` phantom member removed), the members are renamed to
`player+0x260`), then point the five stub sites at it (`HeatModelActive` permissive-1 in the true semantics (`simLive`/`heatModelOn`/`experienceLevel`/…), and all five gate stubs +
emitter/heat/mislanch/projweap vs `OwnerAdvancedDamage` False in myomers — currently the SAME gate the FUN_004ac9c8 family (`MechSubsystem::IsDamaged`, `BT_IsBusLive`, powersub call sites) route
answered both ways; `LiveFireEnabled`/`ControlsAllowLights`/powersub short-event = the +0x25c through the three complete-type bridges (`BTPlayerExperienceSimLive`/`…HeatModelOn`/
flag under three names; ctor seeding from role->returnFromDeath is a bring-up stand-in that runs `BTPlayerRoleLocksAdvanced`). Every shipped egg says `experience=expert`, so default behavior is
every mission as VETERAN). NOTE: these are MODE flags — an egg with `experience=standard` should unchanged. Detail: [[experience-levels]] §Port state.
authentically have inert movement-heat/jams; wiring makes the tier selectable, not the basic
mission different. The MessageBoard feed is separate (StatusMessagePool, below).
- **✅ Authentic target acquisition RECONSTRUCTED (tasks #36/#39, 2026-07-08)** — the `Reticle` - **✅ 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 = 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) → **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 -> +0x274 == 0` = the NOVICE-experience lockout for advanced cockpit systems
(2026-07-18 correction: +0x274 = the egg experience level, NOT a role class -- (2026-07-18 correction: +0x274 = the egg experience level, NOT a role class --
see [[experience-levels]]; port bridge BTPlayerRoleLocksAdvanced, btplayer.cpp; see [[experience-levels]]; port bridge BTPlayerRoleLocksAdvanced, btplayer.cpp;
NULL player = unlocked [T3]; bring-up seeding defaults to 2 ≈ veteran = unlocked). NULL player = unlocked [T3]; seeded from the egg experience since issue #2).
AUDIT TAIL: the other AUDIT TAIL CLOSED (issue #2, 2026-07-20): every powersub.cpp site annotated
powersub.cpp sites annotated FUN_004ac9c8 (the coolant-draw gate ~:427, FUN_004ac9c8 (PoweredSubsystem::HandleMessage @4b0efc, ForceShortRecovery
ForceShortRecovery ~:444, the Generator/PowerWatcher site ~:1085) still call the @4b11bc, Generator::HandleMessage @4b21d0, PowerWatcher::HandleMessage @4b179c)
heat-family IsDamaged (simulationState != 0) stand-in -- each needs its raw fn plus MechSubsystem::IsDamaged and BT_IsBusLive now call the bridge; the
re-checked and swapped to the role bridge where the binary calls 4ac9c8. 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). - **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 The heat-model gate (`OwnerAdvancedDamage`/`FUN_004ad7d4` → the owning **BTPlayer**
**BTPlayer** `mech+0x190`+0x260 = veteran/expert experience, NOT 0xBD3 — see [[experience-levels]]) `mech+0x190`+0x260 = veteran/expert experience — see [[experience-levels]]) is WIRED since
+ `MoverAttach` routing into the LIVE JointedMover (must be reconciled with the gait cutover first). 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). - **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: - **✅ DAMAGE ECONOMY — AUTHENTIC (task #8, 2026-07-11) [T1/T2].** The whole economy closes:
+92 -34
View File
@@ -1273,13 +1273,14 @@ BTPlayer::BTPlayer(
// WinTesla analog, so the dead compare is dropped. @005132f4 // WinTesla analog, so the dead compare is dropped. @005132f4
// //
// On a replicant copy, cache the application's mission/registry pointer // On a replicant copy, back-fill the mission pointer (the base Player ctor
// (application+0xc8 in the binary; no GetMissionRegistry accessor in the // leaves playerMission NULL for replicants; the binary's @0x1f8 slot IS
// WinTesla Application, so read through the documented offset). // 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 CString role_key(creation_message->roleName); // make+0x90
(void)role_key; (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) if ((simulationFlags & 0xc) == 4)
{ {
@@ -1305,42 +1306,54 @@ BTPlayer::BTPlayer(
// //
// Master: derive the game-mode flags from the EXPERIENCE level. // Master: derive the game-mode flags from the EXPERIENCE level.
// //
// CORRECTED SOURCE (2026-07-18 [T1]): the binary reads // WIRED (issue #2, 2026-07-20 [T1]): the binary @004c0bc8
// btMission->experienceLevel (mission+0xe4, the egg's per-pilot // (part_013.c:10843-10870) reads btMission->experienceLevel
// "experience" 0..3 = novice/standard/veteran/expert) — NOT the role's // (mission+0xe4, the egg's per-pilot "experience" 0..3 =
// returnFromDeath (the old reading). See context/experience-levels.md. // 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 BTMission *bt_mission = (BTMission *)playerMission; // this[0x7e]
// the wiring task points this at BTMission::ExperienceLevel(). Known experienceLevel = bt_mission != 0
// slot drift vs the binary switch: the binary never touches ? bt_mission->ExperienceLevel() // mission+0xe4
// showDamageInflicted(0x264) here (that's the advancedDamage copy, set : (int)BTMission::VeteranMode; // [T3] dev-permissive; the binary derefs unguarded (master always has a mission)
// below), and expert's 0 lands on roleReturnDelay2(0x26c), not switch (experienceLevel) // this[0x9d]
// 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)
{ {
case 0: case BTMission::NoviceMode: // 0: everything off
showKills = showDamageReceived = showDamageInflicted = showScore = 0; heatModelOn = simLive = levelFlag26c = levelFlag270 = 0;
break; break;
case 1: case BTMission::StandardMode: // 1: live, no heat model
showKills = 0; heatModelOn = 0;
showDamageReceived = showDamageInflicted = showScore = 1; simLive = levelFlag26c = levelFlag270 = 1;
break; break;
case 2: case BTMission::VeteranMode: // 2: everything on
showKills = showDamageReceived = showDamageInflicted = showScore = 1; heatModelOn = simLive = levelFlag26c = levelFlag270 = 1;
break; break;
case 3: case BTMission::ExpertMode: // 3: on, but 0x26c off
showKills = showDamageReceived = showDamageInflicted = 1; heatModelOn = simLive = levelFlag270 = 1;
showScore = 0; levelFlag26c = 0;
break; 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 // (0x264/0x268) = btMission->advancedDamageOn (mission+0xf0, the egg
// "advancedDamage" technician flag) — an int pair, not role floats. // "advancedDamage" technician flag; an int pair). [T1]
// 0 here = advanced damage OFF, which matches the bring-up eggs. [T3] advancedDamageOn = advancedDamageOn2 = // this[0x99],[0x9a]
roleReturnDelay = roleReturnDelay2 = 0.0f; // this[0x99],[0x9a] <- mission+0xf0 in the binary 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] killCount = 0; // this[0x9f]
@@ -1617,7 +1630,52 @@ int BTPlayerRoleLocksAdvanced(void *owner_mech)
BTPlayer *player = MECH_OWNING_PLAYER(owner_mech); BTPlayer *player = MECH_OWNING_PLAYER(owner_mech);
if (player == 0) if (player == 0)
return 0; // dev-permissive [T3] 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
} }
+22 -16
View File
@@ -342,8 +342,12 @@ class DropZone__ReplyMessage;
// Scalar currentScore; // @0x278 (Player) -- running score // Scalar currentScore; // @0x278 (Player) -- running score
// --- BT-specific --- // --- BT-specific ---
Mission // (The binary's @0x1f8 mission pointer IS the inherited
*btMission; // @0x1f8 cached mission / role registry source // 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 char
teamName[64]; // @0x20c copied from MakeMessage (+0x50) teamName[64]; // @0x20c copied from MakeMessage (+0x50)
BTTeam BTTeam
@@ -358,28 +362,30 @@ class DropZone__ReplyMessage;
// //
// EXPERIENCE-LEVEL game-mode flags (2026-07-18 [T1] — the old "per-role // EXPERIENCE-LEVEL game-mode flags (2026-07-18 [T1] — the old "per-role
// display toggles / returnFromDeath" reading was WRONG). The ctor // 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/ // (+0xe4, the egg's per-pilot "experience" novice/standard/veteran/
// expert) and ->advancedDamageOn(+0xf0) — NOT from the scenario role. // expert) and ->advancedDamageOn(+0xf0) — NOT from the scenario role.
// The member NAMES below date from the scoring work and do not match // Renamed to the true semantics by the issue-#2 wiring (old scoring-era
// the semantics; kept until the wiring task renames them. Binary // names in the trailing comments). Binary mapping (nov/std/vet/exp) +
// mapping (nov/std/vet/exp) + consumers: context/experience-levels.md. // consumers: context/experience-levels.md.
// //
Logical 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 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 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 Logical
showScore; // @0x270 0/1/1/1 consumer not located levelFlag270; // @0x270 0/1/1/1 consumer not located (was showScore)
Scalar Logical
roleReturnDelay; // @0x268 = mission advancedDamageOn (copy 2; int in the binary) advancedDamageOn2; // @0x268 = mission advancedDamageOn (copy 2; int in the binary) (was roleReturnDelay, mistyped Scalar)
Scalar Logical
roleReturnDelay2; // @0x26c 0/1/1/0 consumer not located (supercharge-clamp hypothesis [T4]) levelFlag26c; // @0x26c 0/1/1/0 consumer not located (supercharge-clamp hypothesis [T4]) (was roleReturnDelay2, mistyped Scalar)
int int
roleClassIndex; // @0x274 raw experience level 0..3 (FUN_004ac9c8 = ==0 novice predicate) experienceLevel; // @0x274 raw experience level 0..3 (FUN_004ac9c8 = ==0 novice predicate) (was roleClassIndex)
friend int BTPlayerRoleLocksAdvanced(void *); // the FUN_004ac9c8 bridge (task #12) 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) friend void BTPlayerCountObservedDeath(void *); // observed-death tally (MP DEATHS fix)
int int
+8 -4
View File
@@ -222,11 +222,15 @@ SubsystemMessageManager *BT_GetMessageManager(Mech * /*owner*/)
return 0; return 0;
} }
// The running player mech is a live, powered master -> its electrical bus is up. // CORRECTED (issue #2 [T1]): *(*(owner+0x190)+0x274) != 0 is the owning
// (Real impl read *(*(owner+0x190)+0x274); see layout note above.) // BTPlayer's raw EXPERIENCE level being non-zero -- i.e. NOT-NOVICE (the
Logical BT_IsBusLive(Mech * /*owner*/) // 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. // No known-offset myomer back-pointer in the reconstructed Mech to clear.
+33 -8
View File
@@ -49,7 +49,7 @@
// FUN_004b9948 MechWeapon::GetMuzzlePoint FUN_004b9864 MechWeapon::ComputeAimOrientation // FUN_004b9948 MechWeapon::GetMuzzlePoint FUN_004b9864 MechWeapon::ComputeAimOrientation
// FUN_004b9728 MechWeapon::DrawWeaponPip FUN_004b9cbc MechWeapon::GetTargetPosition // FUN_004b9728 MechWeapon::DrawWeaponPip FUN_004b9cbc MechWeapon::GetTargetPosition
// FUN_004b0bd0 PoweredSubsystem simulation step FUN_004b0d50 PoweredSubsystem scalar query // 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_00417ab4 follow a SharedData link (voltage source / ammo bin)
// FUN_0049fb54 Mech::IsDestroyed (movementMode 2||9) FUN_0041a1a4 IsDerivedFrom(classDerivations) // FUN_0049fb54 Mech::IsDestroyed (movementMode 2||9) FUN_0041a1a4 IsDerivedFrom(classDerivations)
// FUN_0041bbd8 AlarmIndicator::SetLevel(n) (weaponAlarm @0x350, state @0x364) // FUN_0041bbd8 AlarmIndicator::SetLevel(n) (weaponAlarm @0x350, state @0x364)
@@ -74,6 +74,8 @@
#endif #endif
#include <hostmgr.hpp> // HostManager::GetEntityPointer -- the update-record target resolve (task #51) #include <hostmgr.hpp> // HostManager::GetEntityPointer -- the update-record target resolve (task #51)
#include <math.h> // sqrt -- the SeekVoltageResponse sampler (issue #11) #include <math.h> // sqrt -- the SeekVoltageResponse sampler (issue #11)
#include <map> // [aud-tail] per-weapon previous-state tracking (Gitea #5, instrumentation only)
#include <ctime> // [aud-tail] clock() ms timestamps (matches L4AUDLVL.cpp)
// E8 (bring-up): the player fire input. The controls mapper that would normally write // 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 // 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 MakeIdentity(LinearMatrix &m) { m.BuildIdentity(); }
static void CopyColor(void *, const void *) {} static void CopyColor(void *, const void *) {}
// engine-internal artifacts the decomp called by address (stand-ins) // (issue #2: the local FUN_004ad7d4 `return 1` / FUN_004ac9c8 `return 0`
static int FUN_004ad7d4(void *) { return 1; } // HeatableSubsystem active? // 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 // FUN_0049fb54 == Mech::IsDestroyed (movementMode 2||9; reconstructed as
// Mech::IsDisabled). Was a mislabeled "cockpit/HUD query" no-op stub -- so a // 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 // 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 // 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 // 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.) // 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 int FUN_00421070(void *, void *) { return 0; } // damage source resolve
static void *ResolveSource(void *) { return 0; } // named source registry lookup 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 // E5: pendingHeat (HeatSink @0x1C8) is the field the heat sim absorbs each frame
// (heatEnergy += pendingHeat -> temperature); the old Emitter-local heatAccumulator // (heatEnergy += pendingHeat -> temperature); the old Emitter-local heatAccumulator
// was a SHADOW at the wrong offset, so firing never reached the heat model. // 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 pendingHeat += heatPortion; // inherited HeatSink::pendingHeat @0x1c8
} }
@@ -502,6 +507,26 @@ void
default: default:
break; 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<const void *, int> s_prevState;
int st = (int)weaponAlarm.GetState();
std::map<const void *, int>::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)/.. seekRate = (src->MeasuredVoltage() - currentLevel) / dtScale; // 0x45c = (src+0x1dc - 0x414)/..
currentLevel = (seekRate / energyCoefficient) * time_slice + currentLevel; // 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 // @004ba838:7544 -- the charging I^2R loss lands in the GENERATOR's
// pendingHeat (src+0x1c8): recharging weapons is what heats generators // pendingHeat (src+0x1c8): recharging weapons is what heats generators
@@ -628,11 +653,11 @@ void
int int
Emitter::AdvanceSeekVoltage(UpdateRecord *message) 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; 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; return 0;
} }
+11 -2
View File
@@ -417,9 +417,18 @@ inline int
void void
HeatSink_Step(Scalar time_slice) { HeatSinkSimulation(time_slice); } 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 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) // Subsystem virtual overrides (slots on vtable @0050edc4)
+11 -3
View File
@@ -366,13 +366,21 @@ void
} }
// //
// @0x4ac9c8 -- IsDamaged: true (dead) when the owning mech's electrical bus is // @0x4ac9c8 -- the NOVICE-experience lockout predicate (issue #2 [T1]):
// down -- *(*(*(owner+0xd0)+0x190)+0x274) == 0. // *(*(*(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 Logical
MechSubsystem::IsDamaged() 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;
} }
+1 -1
View File
@@ -243,7 +243,7 @@ class Damage;
virtual void virtual void
TakeDamage(Damage &damage); // slot via this+0x24 (see DamageDelta) TakeDamage(Damage &damage); // slot via this+0x24 (see DamageDelta)
Logical 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): // Critical-state readouts for ColorMapperCritical (CriticalConnection):
// the raw simulation state (DefaultState=0 / Destroyed=1 / Exploding=2) and // the raw simulation state (DefaultState=0 / Destroyed=1 / Exploding=2) and
+1 -1
View File
@@ -278,7 +278,7 @@ void MissileLauncher::FireWeapon()
// THE FIRING HEAT (task #9): heatCostToFire RAW into the launcher's own // THE FIRING HEAT (task #9): heatCostToFire RAW into the launcher's own
// pendingHeat (pre-scaled 1e7-unit resources: SRM6 = 5e7, LRM15 = 6.5e7) // 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. // -- 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 pendingHeat += heatCostToFire; // HeatSink @0x1C8, raw
} }
+11 -1
View File
@@ -312,7 +312,17 @@ class Mech;
Scalar OwnerVelocityY() const { return 0.0f; } // Mech velocity.y Scalar OwnerVelocityY() const { return 0.0f; } // Mech velocity.y
Scalar OwnerMotionGain() const { return 0.0f; } // Mech +0x20C Scalar OwnerMotionGain() const { return 0.0f; } // Mech +0x20C
Scalar OwnerMass() const { return 0.0f; } // *Mech +0x250 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 Scalar DamageStructureLevel() const{ return 0.0f; } // DamageZone +0x158
void MoverAttach(int index, Scalar *feed) { (void)index; (void)feed; } // JointedMover vtable +0x38 (no-op) void MoverAttach(int index, Scalar *feed) { (void)index; (void)feed; } // JointedMover vtable +0x38 (no-op)
+24 -13
View File
@@ -449,20 +449,22 @@ void
// @004b0efc -- pass the message to the HeatSink base; if the part is alive, // @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. // 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 Logical
PoweredSubsystem::HandleMessage(int message) PoweredSubsystem::HandleMessage(int message)
{ {
HeatSink::HandleMessage(message); // FUN_004ad748 HeatSink::HandleMessage(message); // FUN_004ad748
// Raw @004b0efc reads *(int*)(*(owner+0x190) + 0x25c) -- the message-manager's // CORRECTED (issue #2 [T1]): @004b0efc reads *(int*)(*(owner+0x190)+0x25c)
// short-event flag (NOT a Generator field). The manager is null in bring-up // -- owner mech+0x190 is the PLAYER LINK, and +0x25c is the BTPlayer's
// (BT_GetMessageManager stub), so guard the deref; the real read resumes once // "sim live" EXPERIENCE flag (0 only for novice), NOT a message-manager
// the SubsystemMessageManager (0xBD3) roster lookup is wired. // field (the old misattribution left this path dead behind the null
SubsystemMessageManager *msgMgr = BT_GetMessageManager((Mech *)owner); // *(this[0x34]+0x190) // BT_GetMessageManager stub). The leading FUN_004ac9c8==0 term is the
// not-novice predicate (BTPlayerRoleLocksAdvanced), not a damage query.
if (!IsDamaged() // FUN_004ac9c8 == 0 if (!BTPlayerRoleLocksAdvanced(owner) // FUN_004ac9c8 == 0 (not novice)
&& msgMgr != 0 && BTPlayerExperienceSimLive(owner) != 0 // *(player+0x25c) != 0
&& *(LWord *)((char *)msgMgr + 0x25c) != 0 // message-manager short-event flag
&& message == 4) && message == 4)
{ {
ForceShortRecovery(); // FUN_004b11bc(this) ForceShortRecovery(); // FUN_004b11bc(this)
@@ -477,10 +479,15 @@ Logical
void void
PoweredSubsystem::ForceShortRecovery() // internal helper for @004b0efc 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; return;
Generator *source = (Generator *)voltageSource.Resolve(); // FUN_00417ab4(this+0x1d0) 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->stateAlarm.SetLevel(Generator::GeneratorShorted); // source+0x1fc -> 3
source->outputVoltage = 0.0f; // source+0x1f8 = 0 source->outputVoltage = 0.0f; // source+0x1f8 = 0
@@ -1086,7 +1093,8 @@ Logical
{ {
if (message == 4) 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 stateAlarm.SetLevel(GeneratorShorted); // 3
shortTimer = 0.0f; // this[0x7e] shortTimer = 0.0f; // this[0x7e]
@@ -1255,7 +1263,10 @@ Logical
Generator *source = (watched != 0) // FUN_00417ab4(watched+0x1d0) Generator *source = (watched != 0) // FUN_00417ab4(watched+0x1d0)
? (Generator *)((PoweredSubsystem *)watched)->ResolveVoltageSource() ? (Generator *)((PoweredSubsystem *)watched)->ResolveVoltageSource()
: 0; : 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->stateAlarm.SetLevel(Generator::GeneratorShorted); // source+0x1fc -> 3
source->outputVoltage = 0.0f; // source+0x1f8 = 0 source->outputVoltage = 0.0f; // source+0x1f8 = 0
+12 -5
View File
@@ -60,7 +60,7 @@
// FUN_004b9d00 MechWeapon::PrintState (slot 13 base) // FUN_004b9d00 MechWeapon::PrintState (slot 13 base)
// FUN_004b9948 MechWeapon::GetMuzzlePoint FUN_004b9bdc MechWeapon::UpdateTargeting // FUN_004b9948 MechWeapon::GetMuzzlePoint FUN_004b9bdc MechWeapon::UpdateTargeting
// FUN_004b9cbc MechWeapon::GetTargetPosition FUN_004b0d50 voltage helper // 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_0041bbd8 AlarmIndicator::SetLevel(n) FUN_0041db7c Damage init
// FUN_004179d4 / FUN_004179f8 SharedData::Connection ctor/dtor // FUN_004179d4 / FUN_004179f8 SharedData::Connection ctor/dtor
// FUN_004022d0 operator delete FUN_004215b0 resolve subsystem name->index // FUN_004022d0 operator delete FUN_004215b0 resolve subsystem name->index
@@ -227,7 +227,13 @@ Subsystem *
Logical Logical
ProjectileWeapon::LiveFireEnabled() const 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: // uniform random sample falls under the current jam probability:
// p = clamp(0.41 * currentTemperature / failureTemperature, // p = clamp(0.41 * currentTemperature / failureTemperature,
// minJamChance, 1.0) // minJamChance, 1.0)
// Gated on the weapon being heat-active (this+0x184) and the owning Mech's // Gated on the weapon being heat-active (this+0x184) and the owning PLAYER's
// "live fire" flag (mech.controls+0x25c). // "sim live" experience flag (mech+0x190 playerLink -> player+0x25c; novice
// never jams -- issue #2).
// //
Logical Logical
ProjectileWeapon::CheckForJam() ProjectileWeapon::CheckForJam()
@@ -772,7 +779,7 @@ void
// binary adds heatCostToFire RAW to the weapon's own pendingHeat -- // binary adds heatCostToFire RAW to the weapon's own pendingHeat --
// projectile resources author PRE-scaled 1e7-unit heat (AFC100 = 6.5e7) // projectile resources author PRE-scaled 1e7-unit heat (AFC100 = 6.5e7)
// -- gated on the heatable check (part_013.c:8744-8747 idiom). // -- 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 pendingHeat += heatCostToFire; // HeatSink @0x1C8, raw
} }
+11 -5
View File
@@ -148,14 +148,20 @@
// SetGraphicsDirty -> engine Simulation::ForceUpdate() (this+0x18) // SetGraphicsDirty -> engine Simulation::ForceUpdate() (this+0x18)
// SetInstanceFlag -> engine Simulation::simulationFlags |= bits (this+0x28) // SetInstanceFlag -> engine Simulation::simulationFlags |= bits (this+0x28)
// GetSegmentFlags() removed (the ctor gate now reads owner->simulationFlags). // GetSegmentFlags() removed (the ctor gate now reads owner->simulationFlags).
// ControlsAllowLights(): no ported base member (the real read is the // ControlsAllowLights() [T1]: the ToggleLamp @004b860c gate is the
// owner controls-mapper lightsEnabled via an unsafe raw owner-offset // owning BTPlayer's +0x25c "sim live" EXPERIENCE flag (mech+0x190
// chain) -> return True for bring-up; the only consumer (ToggleLamp) is // playerLink -> player+0x25c; the old "controls-mapper lightsEnabled"
// dormant. Faithful follow-up = a real Mech::ControlsAllowLights(). // 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 */ } Logical HostShutDown() const { return simulationState == 1; /* Destroyed */ }
int WatchedVoltageLevel() const { return watchdogAlarm.GetLevel(); } // @0x198 int WatchedVoltageLevel() const { return watchdogAlarm.GetLevel(); } // @0x198
int HeatStateLevel() const { return heatAlarm.GetLevel(); } // @0x140 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 SetGraphicsDirty() { ForceUpdate(); } // this[0x18] |= 1
void SetInstanceFlag(int bits) { simulationFlags |= bits; } // this[10] |= bits void SetInstanceFlag(int bits) { simulationFlags |= bits; } // this[10] |= bits
// The "ToggleLamp" command arg is carried at message+0xC (decomp). // The "ToggleLamp" command arg is carried at message+0xC (decomp).