From 29b4d68ba6c2831316697c2b7110c8db5ce81a11 Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Fri, 7 Aug 2026 11:43:54 -0500 Subject: [PATCH] scoring: the +1000 START grant -- BT's MissionStarting override was never ported Seventh chart row. BT overrides MissionStarting purely to seed the score, and the override was missing, so MESSAGE_ENTRY(BTPlayer, MissionStarting) resolved to the inherited engine handler (which only does the fade-in) and the grant never happened. FUN_004bfbe8(player): base_MissionStarting(player); if (app->state == 4 && (player[0x29] & 0x40) == 0) player[0x1c8] = 0x447a0000; // = 1000.0f Both operands decode exactly against engine headers: application state 4 is LaunchingMission (APP.h -- same enum whose 6 is EndingMission, already used by the console flush), and simulationFlags bit 14 is NonScoringPlayerBit (PLAYER.h: NonScoringPlayerBit = Entity::NextBit), so `(+0x29 & 0x40) == 0` IS IsScoringPlayer(). Camera-ship/spectator players are non-scoring and correctly get nothing. CELL NOTE: the binary seeds the ENGINE cell (+0x1c8), not BT's own (+0x278) -- 1995 carried two accumulators, which is why the KB suspected the pod's death cost "may never have displayed". Our port has one currentScore, so grant, awards and death cost land together and the chart reads coherently. Also resets the console watermark so a fresh mission REPORTS the grant rather than a difference from last round's tally. Benched: both players "[score] mission start: player N:1 seeded to 1000", scores run 1001.98 -> 1908.64 with kills=1 (1000 + ~400 damage + 505 kill). Also corrects a FOURTH copy of the dead-code claim, in btplayer.hpp's ScoreType enum ("type 0 has NO scoring arm ... per-hit inflicted credit never existed"). Its byte-scan was right that no TABLE entry binds @004c0200 and wrong to conclude unreachable -- the vtable Dispatch override calls it directly. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ --- game/reconstructed/btplayer.cpp | 49 +++++++++++++++++++++++++++++++++ game/reconstructed/btplayer.hpp | 32 +++++++++++++++------ 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/game/reconstructed/btplayer.cpp b/game/reconstructed/btplayer.cpp index 884bcdb..4661e0f 100644 --- a/game/reconstructed/btplayer.cpp +++ b/game/reconstructed/btplayer.cpp @@ -748,6 +748,55 @@ void suppressConsole = 0; // this+0x258 } +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// MissionStartingMessageHandler (@004bfbe8) +// +// THE "+1000 STARTING THE GAME" ROW of the original manual's scoring chart, +// decoded 2026-08-07. BT overrides the engine's MissionStarting purely to +// seed the score, and the override was never reconstructed -- the +// MESSAGE_ENTRY resolved to the inherited Player:: handler, so the grant +// simply never happened. The binary: +// +// FUN_004bfbe8(player): +// base_MissionStarting(player); +// if (app->state == 4 && (player[0x29] & 0x40) == 0) +// player[0x1c8] = 0x447a0000; // = 1000.0f +// +// Both operands decode exactly: application state 4 is LaunchingMission +// (APP.h -- the same enum whose 6 is EndingMission, already used by the +// console flush), and simulationFlags bit 14 is NonScoringPlayerBit +// (PLAYER.h: `NonScoringPlayerBit = Entity::NextBit`), so the byte test +// `(+0x29 & 0x40) == 0` IS `IsScoringPlayer()`. Camera-ship and spectator +// players are non-scoring and correctly get nothing. +// +// CELL NOTE: the binary seeds the ENGINE score cell (+0x1c8), not BT's own +// (+0x278) -- the 1995 build carried two accumulators, which is why the KB +// suspected the pod's death cost "may never have displayed". Our port has a +// single currentScore, so the grant, the awards and the death cost all land +// together, and the chart reads coherently for a player: start at 1000, +1 a +// damage point, +500 a kill, -500 a special-case death. +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +void + BTPlayer::MissionStartingMessageHandler(Entity::Message *message) +{ + Player::MissionStartingMessageHandler(message); // FUN_0042d9c0 + + if (application->GetApplicationState() == Application::LaunchingMission // app+0x88 == 4 + && IsScoringPlayer()) // !(simulationFlags & NonScoringPlayerFlag) + { + currentScore = 1000.0f; // this+0x1c8 = 0x447a0000 + // The console watermark is what we have already reported; a fresh + // mission must report the grant, not the difference from the last + // round's tally. + BTScoreWatermarkSet(ownerID, 0.0f); + DEBUG_STREAM << "[score] mission start: player " + << BTMatchHostOf(GetEntityID()) << ":" << (int)GetEntityID() + << " seeded to " << (float)currentScore + << " (chart: +1000 starting the game)\n" << std::flush; + } +} + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Console score watermark (port-side, 2026-08-07) // diff --git a/game/reconstructed/btplayer.hpp b/game/reconstructed/btplayer.hpp index 1f56c9f..56bde2c 100644 --- a/game/reconstructed/btplayer.hpp +++ b/game/reconstructed/btplayer.hpp @@ -84,16 +84,24 @@ class DropZone__ReplyMessage; public: // // Kind of scoring event. Recovered from the branch selector at - // @004c02e4 (this->scoreType, message+0x20). NOTE: type 0 has NO - // scoring arm in the binary -- @004c02e4 Verify-rejects it (line 662) - // and @004c0200, the only function that accepts it, appears in no - // handler-table entry (byte-scan 2026-08-05: the BTPlayer table at - // file 0x112dxx has exactly 6 entries, none binding it). 1995 pod - // scoring = kills + received-damage penalties; per-hit inflicted - // credit never existed. + // @004c02e4 (this->scoreType, message+0x20). + // + // CORRECTED 2026-08-07. This note used to read "type 0 has NO scoring + // arm in the binary ... @004c0200 appears in no handler-table entry ... + // per-hit inflicted credit never existed", and build 787 deleted the + // credit on that basis. The byte-scan was right that no TABLE entry + // binds @004c0200 and wrong to conclude it is unreachable: BTPlayer + // overrides Dispatch (vtable @00513300 slot 3 = FUN_004bffa0), which + // splits type 0 off BEFORE base dispatch and calls it directly. That + // is also WHY @004c02e4 Verify-rejects type 0 -- the interceptor + // guarantees it never arrives there. @004c0200 names itself + // "BTPlayer::ScoreInflictedMessageHandler" in its own Verify string, + // and the original manual's scoring chart independently confirms what + // it computes: "+1 each damage point scored on opponent's armor", + // "-1 each self-inflicted point". // enum ScoreType { - DamageInflictedScore = 0, // sent, but scores nothing (see above) + DamageInflictedScore = 0, // per-hit inflicted credit (LIVE, see above) DamageReceivedScore = 1, // I took damage KillScore = 2 // I destroyed / was destroyed }; @@ -319,6 +327,14 @@ class DropZone__ReplyMessage; void ScoreMessageHandler(ScoreMessage *message); // @004c02e4 + // + // @004bfbe8 -- BT's MissionStarting override. Seeds the starting score + // ("+1000 Starting the game", original manual scoring chart). The base + // Player handler does the fade-in; BT adds the grant. + // + void + MissionStartingMessageHandler(Entity::Message *message); // @004bfbe8 + // // @004bffd0 -- the spawn / respawn handshake. When the drop zone // replies with our spawn location we create (or reset) the player's