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
+92 -34
View File
@@ -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
}
+22 -16
View File
@@ -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
+8 -4
View File
@@ -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.
+33 -8
View File
@@ -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 <hostmgr.hpp> // HostManager::GetEntityPointer -- the update-record target resolve (task #51)
#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
// 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<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)/..
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;
}
+11 -2
View File
@@ -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)
+11 -3
View File
@@ -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;
}
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
}
+11 -1
View File
@@ -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)
+24 -13
View File
@@ -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
+12 -5
View File
@@ -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
}
+11 -5
View File
@@ -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).