The world now executes. Entity::Execute only PerformAndWatch's entities in application state RunningMission, reached by two RunMissionMessages (WaitingForLaunch->LaunchingMission->RunningMission). The 2nd is dispatched by Player::ManageApplicationStatus when the launch fade expires -- but that only runs inside PlayerSimulation, not the launch-phase HuntForDropZone. So the player must switch Performance onto PlayerSimulation after it spawns. - BTPLAYER.CPP DropZoneReplyMessageHandler: after CreatePlayerVehicle + InitializePlayerLink, choose the per-vehicle Performance by class -- Mech -> SetPerformance(PlayerSimulation)+SetScoringPlayerFlag; else CameraShipSimulation. - BTPLAYER.CPP PlayerSimulation: chains the authentic base Player::PlayerSimulation (CalcRanking + ManageApplicationStatus + vehicle-pos copy + status service); console SCORE-delta flush deferred to the scoring wave. - SENSOR.CPP SensorSimulation: minimal-safe partial (radarPercent = 1 - GetSubsystemDamageLevel, sensor healthy) so the roster tick path can't Fail once RunningMission engages. The heat/electrical gating awaits the power/heat sim wave (PoweredSubsystem/Generator/HeatSink/HeatWatcher *Simulation staged). Verified: BT_MECH_LOG run reaches "[tick] roster live (first Sensor frame)", two RunMissionHandler transitions, zero Fail/Exception. The mech is executed each frame; its BODY Performance is still DoNothingOnce (real Mech::Simulate = Phase 5.3, now the frontier). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
150 lines
8.4 KiB
Markdown
150 lines
8.4 KiB
Markdown
# 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).
|