Files
TeslaRel410/restoration/source410/BT/BTPLAYER.NOTES.md
T
CydandClaude Fable 5 e1e5a9a6db BT410 Phase 5.3.12: experience gates + coolant system -- novice/expert modes REAL
The player-experience system now gates the entire heat/jam economy, verified in
BOTH directions with a one-token egg edit (TESTNOV.EGG, experience=novice):
NOVICE = 212 fire cycles all pinned at T=77, zero jams, zero shutdowns (the
heat model authentically absent); EXPERT = the full economy (duty cycles, 119
shutdowns + 2 authentic jams under forced fire). Zero Fail in both.

- BTPLAYER: the flag block renamed to its TRUE semantics (simLive @0x25c,
  heatModelOn @0x260 -- the FUN_004ad7d4 master switch, advancedDamageOn pair,
  levelFlag26c/270, experienceLevel); the ctor rows were already
  binary-accurate (nov 0000 / std 1011 / vet 1111 / exp 1101); accessors +
  [exp] sentinel.
- HeatSink::HeatModelActive() + ProjectileWeapon::LiveFireEnabled(): owner
  mech -> Entity::GetPlayerLink() -> the BTPlayer flags, NULL-permissive.
  Gates: both HeatSinkSimulation phases, every weapon fire-heat dump, and
  CheckForJam is now the AUTHENTIC form (LiveFireEnabled + heatLoad<=0
  early-outs + the minJamChance floor; the interim heat-degraded gate retired).

- THE LOAD-BEARING FIX: Mech ctor SetValidFlag(). Every 1995 entity ctor tail
  marks itself valid; ours didn't -- Entity::Dispatch routes messages to an
  INVALID entity into the deferred event queue, so the PlayerLink bind (and
  every directly-dispatched mech message) silently never landed and the gates
  read a NULL player forever.

- THE COOLANT SYSTEM (authentic bodies, byte-verified constants: HeatLoadScale
  0.002 -> heatLoad now in [0,1]; equalize eps 1e-4; draw floor/ON
  0.0025/0.003): UpdateCoolant (damage-scaled draw -- an undamaged mech leaks
  nothing), BalanceCoolant (full clamp chain from ConductHeat), DrawCoolant
  base=0 with the RESERVOIR override as THE SOURCE; Reservoir reconstructed
  (capacity overlays thermalCapacity, CoolantSimulation + the full
  InjectCoolant flush distribution, BT_FORCE_FLUSH dev hook); Condenser
  RefrigerationSimulation (massScale = (1-damage)*refrigerationFactor >= 1 --
  the heat pump that chills the bank; valveState inits 1; digit-suffix number
  fix). Deferred: AggregateHeatSink family, cockpit-button handlers
  (MoveValve/ToggleCooling/InjectCoolant), TrackSeekVoltage charge model.

Research driven by a 4-agent workflow dossier over the BT411 RE (verbatim
bodies for every function above + the egg experience plumbing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 22:15:07 -05:00

9.2 KiB
Raw Blame History

BTPLAYER.CPP / .HPP — reconstruction notes

Status: constructor / dtor / Make / TestInstance / GetExperienceLevel + message-handler table + DropZoneReply dispatch RECONSTRUCTED; the scoring/death handlers, PlayerSimulation, and CreatePlayerVehicle/InitializePlayerLink (the Mech-subsystem frontier) still staged.

Message-handler table (wired 2026-07-19)

DefaultData now uses BTPlayer::MessageHandlers (chained onto Player::MessageHandlers) instead of reusing the base set — the earlier reuse left BTPlayer's own handlers unwired, so a DropZoneReply reached the base Player::DropZoneReplyMessageHandler stub that Fails "should not be handled by base player class!" (PLAYER.CPP:179). Table entries (BT411 .data @005130c0): DropZoneReply, VehicleDead, Score, ScoreInflicted, ScoreUpdate. ScoreInflicted/ ScoreUpdate add two BT message IDs (Player::NextMessageID+0/+1). MissionStarting/ MissionEnding are inherited from Player unchanged (not re-listed — BT411 lists them but they point at the same base handlers).

DropZoneReplyMessageHandler is the real spawn/respawn dispatch skeleton; its mech-placement tail (vehicle-class performance select, Mech::Reset respawn) is deferred with the Mech subsystem. On the first fresh-drop reply it calls CreatePlayerVehicle → the current boot frontier.

Sources

  • BT411 game/reconstructed/btplayer.cpp (Ghidra-recovered ctor @004c0bc8, Make @004c0ecc, dtor @004c0efc, TestInstance @004c0f28) — byte-offset annotated body.
  • Surviving 1995 headers pin every base API: PLAYER.HPP (base members currentScore/scenarioRole/deathCount/playerMission, GetMission), SIMULATE.HPP (lastPerformance/lastUpdate/simulationFlags/activePerformance — lastPerformance is a BASE member, not BT-own), ENTITY.HPP (GetInstance/ReplicantInstance), NTTMGR.HPP (EntityManager::FindGroup, EntityGroup::groupMembers = ChainOf<Node*>), TEAM.HPP (Team::teamName[64], public — BTTeam inherits it), BTMSSN.HPP (BTMission::ExperienceLevel()/AdvancedDamageOn(), enum Novice/Standard/ Veteran/Expert = 0..3), SCNROLE.HPP.

Member layout note

The staged header's int reserved[38] placeholder was replaced with the real BT-own members (btMission, teamName[64], teamEntity, freeForAll, consoleAttached, suppressConsole, the experience-flag block, killCount/padScore, objectiveMech, lastConsoleUpdate, deathPending). Byte offsets do NOT need to match the shipped 0x294-byte object — this is a fresh self-consistent build; only wire-format structs (MakeMessage) must match the binary, and those are unchanged.

Experience-level flag mapping — the [T1] correction

The ctor derives the game-mode flags from btMission->ExperienceLevel() (mission+0xe4, the egg's per-pilot "experience" key), NOT from the scenario role's returnFromDeath — see bt-experience-levels. BT411's ported ctor uses scenarioRole->GetReturnFromDeath() as a stand-in and marks it [T3]; this 1995 reconstruction encodes the binary-accurate reading instead.

Binary switch rows (member ← value per level), from the [T1] disassembly:

level showDamageReceived (0x25c) showKills (0x260) roleReturnDelay2 (0x26c) showScore (0x270)
novice 0 0 0 0
standard 1 0 1 1
veteran 1 1 1 1
expert 1 1 0 1

Plus showDamageInflicted (0x264) = roleReturnDelay (0x268) = advancedDamageOn.

Member NAMES are wrong-but-kept (they date from the earlier scoring work): showKills is really the HEAT-MODEL master switch (FUN_004ad7d4), showDamageReceived is the "sim live / novice lockout" gate, showScore/roleReturnDelay2 consumers not fully located ([T4]). Rename is a follow-up wiring task; the VALUES are correct.

Deliberate differences from BT411's port

  • Make: the 1995 return new BTPlayer(creation_message); — BT411's replicant SetValidFlag() is a P6 network-bring-up addition, dropped.
  • Replicant-mission caching uses application->GetCurrentMission() (the real 1995 accessor, PLAYER.CPP:689) instead of BT411's raw application+0xc8.

Reconstructed 2026-07-20 (boot reaches the live game loop)

  • InitializePlayerLink (btplayer.cpp, was @004bfee0) — DONE. Dispatches a PlayerLinkMessage carrying our own GetEntityID() to playerVehicle and its network replicants, binding the freshly-made mech to its owning player. Byte-for-byte the same tail the engine's own CameraDirector::CreateCameraShip runs (DIRECTOR.CPP): BTPlayer/Player/CameraDirector all derive from Entity, so PlayerLinkMessage / PlayerLinkMessageID / GetEntityID resolve through the Entity base. No staging.
  • VehicleDeadMessageHandler (btplayer.cpp) — PARTIAL. The non-death flavours are real: the engine's -2 "did the drop-zone reply arrive?" probe (posted by Player::HuntForDropZone during every spawn) and the >=0 respawn re-post both delegate to the authentic base Player::VehicleDeadMessageHandler, which only re-hunts a drop zone when the death is still current and one has not been acquired — so a freshly-spawned player falls straight through (this is what unblocks bring-up boot). Only the deathCount == -1 immediate death notification is still Fail-staged: its full cycle (scoring-role life debit, +5 s respawn re-post, warp collapse) is downstream of the per-frame mech sim (mech4) + scoring roles.

With these two, the reconstructed tree boots into the running per-frame game loop with zero Fail() reached: engine → app → mission → BTPlayer → CreatePlayerVehicle → Mech ctor (33 subsystems from the real TEST.EGG model) → InitializePlayerLink → DropZone acquire → the -2 probe returns harmlessly → LBE4ControlsManager::Execute spins per-frame. The only thing it now waits on is RIO cockpit hardware (absent in the headless smoke test).

Reconstructed 2026-07-20 (entity simulation goes LIVE)

  • DropZoneReplyMessageHandler now, after the fresh spawn (CreatePlayerVehicle
    • InitializePlayerLink), CHOOSES the per-vehicle Performance from the vehicle class: a Mech gets SetPerformance(&BTPlayer::PlayerSimulation) + SetScoringPlayerFlag(); anything else (camera ship) gets Player::CameraShipSimulation + rank 1. This is load-bearing for mission flow (see below). The mech PLACEMENT tail (Mech::Reset heal/move to the drop-zone origin, translocation alarm, warp-sphere reveal) stays deferred with the mech per-frame sim; the fresh mech sits at its ctor origin.
  • PlayerSimulation (btplayer.cpp) — chains the authentic engine base Player::PlayerSimulation (CalcRanking + ManageApplicationStatus + vehicle- position copy + StatusMessageUpdate). The BT console SCORE-delta flush (ConsolePlayerVTVScoreUpdateMessage, only meaningful with a live console host) is deferred to the scoring wave.

Why this is the piece that starts the world executing

Entity::Execute only calls PerformAndWatch (i.e. simulates entities) when Application::GetApplicationState() == RunningMission. The app is a state machine advanced one step per RunMissionMessage: WaitingForLaunch → LaunchingMission → RunningMission. With no console, CheckLoadMessageHandler posts the FIRST RunMission (→ LaunchingMission, which dispatches MissionStarting → the player starts a 3 s fade). The SECOND RunMission (→ RunningMission) is dispatched by Player::ManageApplicationStatus when that fade expires — but ManageApplicationStatus only runs inside PlayerSimulation/CameraShipSimulation, NOT inside the launch-phase HuntForDropZone. So the player must switch its Performance onto PlayerSimulation after it spawns, or the fade never counts down and the world never runs. That switch (above) + a non-Fail PlayerSimulation is what takes the boot from "controls loop only" to a live per-frame entity simulation: verified — the mech is executed each frame and its subsystem roster ticks (Sensor first-frame marker), zero Fail. The mech BODY is still DoNothingOnce (its real Mech::Simulate is Phase 5.3).

Still staged (next bricks)

  1. Mech::Simulate — the mech's real per-frame Performance (Phase 5.3): now the frontier, since the entity tick path is live and calls the mech's Performance each frame.
  2. VehicleDeadMessageHandler(-1) death cycle — with mech4 + scoring roles.
  3. Score / ScoreInflicted / ScoreUpdate handlers (scoring wave); the console SCORE-delta flush inside PlayerSimulation.
  4. DropZoneReply respawn tail (Mech::Reset in-place heal/move).

Experience flags renamed to TRUE semantics (2026-07-22, Phase 5.3.12)

The show* / roleReturn* block was the old "per-role display toggles" misread (the memory note flagged it). Renamed + retyped to the binary truth: simLive @0x25c (novice lockout: jams/lights/powersub short events), heatModelOn @0x260 (THE heat-model master switch, FUN_004ad7d4), advancedDamageOn/advancedDamageOn2 (the egg technician flag pair), levelFlag26c (0/1/1/0, consumer unlocated), levelFlag270 (0/1/1/1), experienceLevel (raw 0..3). The ctor rows were already binary-accurate. Public accessors IsSimLive()/IsHeatModelOn()/IsAdvancedDamageOn(); a gated [exp] sentinel prints the derived flags per master player. Consumers: HeatSink::HeatModelActive() (HEAT.CPP), ProjectileWeapon::LiveFireEnabled() (PROJWEAP.CPP) -- both walk mech->GetPlayerLink(), NULL-permissive.