# 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), 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. ## 5.3.16 — BT_SPAWN_ENEMY dev harness DropZoneReplyMessageHandler, after the fresh-spawn block: `BT_SPAWN_ENEMY=1` clones the CreatePlayerVehicle MakeMessage recipe (same game-model resource, badge/color/patch, `Mech::DefaultFlags`) at dropZone+150 on Z, calls `Mech::Make()` directly (the Entity ctor self-registers + uniquifies the ID) and sets it as the player mech's target. The enemy has NO player link, so its experience gates read dev-permissive; it never fires back (no target of its own — set one for a mutual fight). DEV-ONLY: the real opponent path is the network mesh / mission roster. ## 5.3.17 — BT_SPAWN_ENEMY is now a MUTUAL fight The harness also sets the enemy's target back to the player: under BT_FORCE_FIRE the enemy returns fire (its gates read dev-permissive with no player link), so the damage cascade gets a live incoming side — used to verify the player's own weapons fall silent when destroyed.