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:
co-authored by
Claude Fable 5
parent
a0cec48e3f
commit
d7158f1f82
@@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user