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
}