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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
This commit is contained in:
co-authored by
Claude Opus 5
parent
98082e64a0
commit
29b4d68ba6
@@ -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)
|
||||
//
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user