diff --git a/game/reconstructed/btl4gau3.cpp b/game/reconstructed/btl4gau3.cpp index ce18348..7621a53 100644 --- a/game/reconstructed/btl4gau3.cpp +++ b/game/reconstructed/btl4gau3.cpp @@ -187,6 +187,11 @@ Logical // the bridge (mechmppr.cpp, a complete-Mech TU) resolves it -- this TU only sees // the pilot as an opaque pointer read at BTPlayer raw offsets. extern void *BTResolveRosterPilot(int slot); +// gauge scoring wave: read a roster pilot's scoreboard via its compiled BTPlayer +// members (btplayer.cpp), not raw offsets (which don't match our layout). +extern int BTPilotKills(void *pilot); +extern int BTPilotDeaths(void *pilot); +extern int BTPilotIsSelected(void *pilot, void *local_player); // SELECT-TARGET highlight (safe) // App+0xC8 player-name-bitmap cache is not wired in the port (same deferral // PlayerStatus uses for its nameImage) -> NULL routes DrawMechIcon to the binary's @@ -344,6 +349,11 @@ void PilotList::Execute() { void *local = BTResolveRosterPilot(0); // slot-0 (local) pilot gates the body + { + static int s_pl = 0; + if (getenv("BT_SCORE_LOG") && (s_pl++ % 120) == 0) + DEBUG_STREAM << "[score] PilotList::Execute local=" << local << "\n" << std::flush; + } if (local == NULL) return; @@ -375,8 +385,11 @@ void } else { - void *tgt = *(void **)((char *)local + 0x284); // local pilot's target (objectiveMech) - int selected = (tgt != NULL) && (pilot == *(void **)((char *)tgt + 0x190)); // target's owningPlayer + // SELECT-TARGET highlight -- via the safe bridge (accessors), NOT raw offsets: + // the old `*(local+0x284)` objectiveMech + deref `*(tgt+0x190)` read garbage in + // our compiled layout and AV'd inside the SEH-guarded Execute, silently aborting + // it before the KILLS/DEATHS draw below (= the empty scoreboard). + int selected = BTPilotIsSelected(pilot, local); if ((int)(size_t)pilot != e.resolvedMech || e.selected != selected) { localView.MoveToAbsolute(e.x, e.y); // vtbl+0x24 @@ -385,13 +398,16 @@ void // (byte-faithful: the binary does NOT latch e.resolvedMech here, so the // icon redraws each occupied frame.) } - // KILLS = killCount@0x27c (real; ScoreMessageHandler kill branch increments it). - e.nameDisplay->Draw(&localView, (Scalar)(*(int *)((char *)pilot + 0x27c))); - // DEATHS: the binary reads pad_0x280 (NO writer anywhere -> perpetual 0 = a - // shipped dead field). FIX (correctness deviation, per "if it doesn't work, - // fix it"): read the real deaths counter Player::deathCount@0x200 - // (VehicleDeadMessageHandler increments it), so DEATHS is meaningful in MP. - e.mechDisplay->Draw(&localView, (Scalar)(*(int *)((char *)pilot + 0x200))); + // KILLS / DEATHS -- gauge scoring wave (Step 1, the databinding fix): read the + // pilot's COMPILED named members via the BTPlayer bridge, NOT raw binary offsets. + // The earlier pilot+0x27c / +0x200 reads assumed the 1995 layout; our compiled + // BTPlayer/Player layout differs (engine base != binary), so those offsets read a + // stale/garbage slot -> a silent 0 even after the handlers increment the members. + // The bridge reads the SAME members the ScoreMessageHandler/VehicleDead handlers + // write (killCount / Player::deathCount). (DEATHS uses the real deathCount, not + // the binary's dead pad_0x280.) + e.nameDisplay->Draw(&localView, (Scalar)BTPilotKills(pilot)); // KILLS (killCount) + e.mechDisplay->Draw(&localView, (Scalar)BTPilotDeaths(pilot)); // DEATHS (Player::deathCount) } ++currentSlot; diff --git a/game/reconstructed/btplayer.cpp b/game/reconstructed/btplayer.cpp index 40b1530..64206fa 100644 --- a/game/reconstructed/btplayer.cpp +++ b/game/reconstructed/btplayer.cpp @@ -154,9 +154,16 @@ static const Scalar TicksPerSecond = 1.0f; // (see note in PlayerSimulation) // mech.hpp (owned by the mech family). Accessed through the documented byte // offsets. CROSS-FAMILY -- ideally Mech would expose these accessors. // -#define MECH_TONNAGE(m) (*(Scalar *)((char *)(m) + 0x4bc)) // +0x4bc -#define MECH_DAMAGE_BIAS(m) (*(Scalar *)((char *)(m) + 0x354)) // +0x354 -#define MECH_OWNING_PLAYER(m) (*(BTPlayer **)((char *)(m) + 0x190)) // +0x190 +// gauge scoring wave: the raw binary offsets are garbage in our compiled Mech +// (engine Entity base ~0x1BC vs the binary's 0x300; 0x638 Mech vs 0x854), and the +// scoring handlers deref them -> a fault the moment a ScoreMessage is dispatched. +// Reconcile: the owning player via the engine playerLink accessor (NULL for the +// uncontrolled BT_SPAWN_ENEMY dummy -- exactly what the null-guards below rely on); +// tonnage ratio stubbed 1.0 + damage bias stubbed 0.0 for bring-up (SCORE == raw +// damage, un-tonnage-scaled; the real per-mech tonnage/bias accessors are a follow-up). +#define MECH_TONNAGE(m) (1.0f) // bring-up: ratio == 1 +#define MECH_DAMAGE_BIAS(m) (0.0f) // bring-up: factor = 0*bias+1 = 1 +#define MECH_OWNING_PLAYER(m) ((BTPlayer *)((Mech *)(m))->GetPlayerLink()) // ENTITY.h:430 (NULL for the dummy) //############################################################################# //############################### BTPlayer ############################## @@ -406,7 +413,7 @@ void // // Points lost for damage we took (unless we hit ourselves). // - if (sender_mech != our_mech) + if (sender_mech != our_mech && scenarioRole != 0) // scoring wave: guard the NULL bring-up role { Scalar received = scenarioRole->CalcDamageReceivedScore(message->damageAmount); // FUN_00429b94 @@ -442,6 +449,10 @@ void case BTPlayer::ScoreMessage::KillScore: // 2 { + // gauge scoring wave: the attacking mech's owner (NULL for the ownerless + // BT_SPAWN_ENEMY dummy). Resolved once, guarded everywhere below. + BTPlayer *sender_owner = MECH_OWNING_PLAYER(sender_mech); // *(sender+0x190), NULL for the dummy + // // A kill (or a self-kill). Inflicted value scaled by the // sender/self tonnage ratio. @@ -457,29 +468,35 @@ void else { // - // Credit a kill to us and to the attacking player. + // Credit a kill to us and (if it has one) to the attacking player. + // The solo demo posts this to the LOCAL player with senderMechID=the + // victim, so `this->killCount++` fires (KILLS -> 1); the ownerless + // dummy's sender_owner is NULL -> the second increment is guarded away. // ++killCount; // this+0x27c - ++MECH_OWNING_PLAYER(sender_mech)->killCount; // *(sender+0x190)+0x27c + if (sender_owner) + ++sender_owner->killCount; // *(sender+0x190)+0x27c } // - // Pop a "destroyed by" status message onto the pilot HUD. + // Pop a "destroyed by " status message onto the pilot HUD. + // Gated on a real attacker player (+ the status pool, a NULL stub in + // bring-up), so the ownerless-dummy solo path skips it (no player to name). // - StatusMessage *status = (StatusMessage *)StatusMessagePool->New(); // FUN_00402f74(0x512f6c) - if (status) + if (sender_owner && StatusMessagePool) { - new (status) Player__StatusMessage( - MECH_OWNING_PLAYER(sender_mech), // *(sender+0x190) - 0, - STATUS_DISPLAY_TIME // 0x40c00000 == 6.0f - ); - // install the BT status-message vtable + clear its flag word - // (Player__StatusMessage exposes neither in WinTesla -- raw). - /* removed decompiler vtable artifact -- C++ ctor sets the real vtable (was: *(void**)status = &BTStatusMessageVTable). */ - *(int *)((char *)status + 0x18) = 0; // status[6] = 0 + StatusMessage *status = (StatusMessage *)StatusMessagePool->New(); // FUN_00402f74(0x512f6c) + if (status) + { + new (status) Player__StatusMessage( + sender_owner, // *(sender+0x190) + 0, + STATUS_DISPLAY_TIME // 0x40c00000 == 6.0f + ); + *(int *)((char *)status + 0x18) = 0; // status[6] = 0 + AddStatusMessage(status); // FUN_0042e580 + } } - AddStatusMessage(status); // FUN_0042e580 // // If we just killed our designated objective mech, notify it. @@ -518,6 +535,16 @@ Scalar Scalar bias ) { + // gauge scoring wave: scenarioRole is NULL in bring-up (the BTMission role + // registry has no WinTesla analog -- ctor lookup stubbed), and every modifier + // deref below would fault the moment a score message is dispatched. A NULL + // role == the neutral role (all modifiers 1, no penalties, bias 0), so the + // award is just (damage + bias). Real per-role modifiers are a follow-up. + if (scenarioRole == 0) + { + return damage_amount + bias; + } + Scalar factor; // @@ -575,8 +602,18 @@ void score ); // FUN_00420ea4(0x20, 0x1a, 1, ...) - Dispatch(&score_message); // (**(*this+0xc))(this, &msg) - currentScore = 0; // this[0x9e] = 0 + // gauge scoring wave: the binary's currentScore is a console DELTA that is + // flushed to the operator console then zeroed. Our SCORE/RANK gauges read + // currentScore as the RUNNING total, so only flush+zero when a console host + // is actually present (MP / pod); in solo there is no console -> keep the + // running score so the SCORE gauge + CalcRanking don't reset every 10s. + Host *console_host = + application->GetHostManager()->GetConsoleHost(); // FUN_00429078 + if (console_host) + { + Dispatch(&score_message); // (**(*this+0xc))(this, &msg) + currentScore = 0; // this[0x9e] = 0 + } } } } @@ -1013,3 +1050,111 @@ Logical { return IsDerivedFrom(*GetClassDerivations()); // FUN_0041a1a4(**this[3], 0x512f94) } + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// gauge scoring wave: scoreboard bridges for the PilotList gauge (btl4gau3.cpp). +// The pilot roster entries are BTPlayer* delivered as void* (BTResolveRosterPilot); +// these read the COMPILED named members the scoring handlers write, replacing the +// earlier raw-offset reads (pilot+0x27c / +0x200) that don't match our layout. +// +int BTPilotKills(void *pilot) +{ + int k = pilot ? ((BTPlayer *)pilot)->GetKillCount() : 0; + static int s_n = 0; + if (getenv("BT_SCORE_LOG") && (s_n++ % 120) == 0) // ~throttled + DEBUG_STREAM << "[score] gauge read: pilot=" << pilot << " kills=" << k << "\n" << std::flush; + return k; +} +int BTPilotDeaths(void *pilot) { return pilot ? ((BTPlayer *)pilot)->GetDeaths() : 0; } + +// Is `pilot` the owner of `local_player`'s current target? (the SELECT-TARGET row +// highlight). Computed here via accessors -- the PilotList's raw-offset version +// (local+0x284 objectiveMech, then deref target+0x190) reads garbage in our layout +// and AV'd inside the SEH-guarded Execute, silently aborting it BEFORE the KILLS/ +// DEATHS draw. GetPlayerLink() is NULL for the ownerless dummy target -> not selected. +int BTPilotIsSelected(void *pilot, void *local_player) +{ + if (pilot == 0 || local_player == 0) + { + return 0; + } + Mech *target = ((BTPlayer *)local_player)->GetObjectiveMech(); // @0x284 + if (target == 0) + { + return 0; + } + return ((void *)((BTPlayer *)pilot) == (void *)target->GetPlayerLink()) ? 1 : 0; +} + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// gauge scoring wave: PRODUCERS -- posted by the combat path (mech4.cpp) to feed +// the scoreboard. The inflictor is always the viewpoint mech in bring-up, so the +// local (crediting) player is application->GetMissionPlayer(). Bridges (not inline +// in mech4.cpp) so the message construction lives in this complete-BTPlayer TU. +// +void BTPostDamageScore(Entity *victim, Scalar damage) // Step 6: per-hit SCORE +{ + if (application == 0 || victim == 0) + { + return; + } + BTPlayer *local_player = (BTPlayer *)application->GetMissionPlayer(); + if (local_player == 0) + { + return; + } + // ScoreInflicted (scoreType 0): senderMechID = the mech that TOOK the damage. + // -> ScoreInflictedMessageHandler: currentScore += tonnageRatio*CalcInflictedScore. + BTPlayer::ScoreMessage message( + BTPlayer::ScoreInflictedMessageID, // 0x16 + sizeof(BTPlayer::ScoreMessage), + BTPlayer::ScoreMessage::DamageInflictedScore, // 0 + 0.0f, // scoreAward (unused for inflicted) + damage, // damageAmount + victim->GetEntityID()); // senderMechID = victim + local_player->Dispatch(&message); + if (getenv("BT_SCORE_LOG")) + DEBUG_STREAM << "[score] +damage " << damage << " -> currentScore=" + << (Scalar)local_player->GetScore() << "\n" << std::flush; +} + +void BTPostKillScore(Entity *victim, Scalar damage) // Step 7: KILL (+ MP death) +{ + if (application == 0 || victim == 0) + { + return; + } + BTPlayer *local_player = (BTPlayer *)application->GetMissionPlayer(); + if (local_player != 0) + { + // KillScore (scoreType 2): senderMechID MUST be the VICTIM (!= our mech) so + // the local player is credited via `this->killCount++` -- if it were our own + // mech, ScoreMessageHandler takes the suicide branch (award negated, no kill). + BTPlayer::ScoreMessage kill( + Player::ScoreMessageID, // 0x12 + sizeof(BTPlayer::ScoreMessage), + BTPlayer::ScoreMessage::KillScore, // 2 + 0.0f, // scoreAward (killBonus; 0 for bring-up) + damage, // damageAmount (killing-blow) + victim->GetEntityID()); // senderMechID = victim + local_player->Dispatch(&kill); + if (getenv("BT_SCORE_LOG")) + DEBUG_STREAM << "[score] *** KILL *** localPlayer=" << (void *)local_player + << " killCount=" << local_player->GetKillCount() + << " deaths=" << local_player->GetDeaths() + << " score=" << (Scalar)local_player->GetScore() + << " rank=" << local_player->GetRanking() << "\n" << std::flush; + } + + // MP DEATH: credit a death to the VICTIM's own player. NULL for the solo + // BT_SPAWN_ENEMY dummy (GetPlayerLink()==0) -> skipped, so DEATHS stays 0 in + // solo (authentic -- DEATHS only lands on a real pilot in multiplayer). + BTPlayer *victim_player = (BTPlayer *)((Mech *)victim)->GetPlayerLink(); + if (victim_player != 0) + { + Player::VehicleDeadMessage dead( + Player::VehicleDeadMessageID, // 0x13 + sizeof(Player::VehicleDeadMessage)); + victim_player->Dispatch(&dead); + } +} diff --git a/game/reconstructed/btplayer.hpp b/game/reconstructed/btplayer.hpp index 3e81194..75ad6b4 100644 --- a/game/reconstructed/btplayer.hpp +++ b/game/reconstructed/btplayer.hpp @@ -312,6 +312,19 @@ class DropZone__ReplyMessage; Logical TestInstance() const; // @004c0f28 + // + // gauge scoring wave: public scoreboard accessors so the cockpit gauges + // (PilotList KILLS/DEATHS) read the SAME compiled members the scoring + // handlers write -- the earlier raw-offset reads (pilot+0x27c / +0x200) + // don't match our compiled layout (engine base != the 1995 binary) and + // silently read 0. currentScore/playerRanking are inherited Player fields. + // + int GetKillCount() const { return killCount; } // @0x27c + int GetDeaths() const { return deathCount; } // @0x200 (Player) + Scalar GetScore() const { return currentScore; } // @0x278 (Player) + int GetRanking() const { return playerRanking; } // (Player) + Mech *GetObjectiveMech() const { return objectiveMech; } // @0x284 (current target) + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Local data // diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 0357ee8..6f3e9bc 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -611,6 +611,9 @@ static int gEnemyDestroyed = 0; // BRING-UP: the spawned target/enemy mech (defined in btplayer.cpp). The player // mech locks onto it as its current target each frame (see targeting step below). extern Entity *gEnemyMech; +// gauge scoring wave: producers (btplayer.cpp) -- feed the scoreboard from combat. +extern void BTPostDamageScore(Entity *victim, Scalar damage); // per-hit SCORE (ScoreInflicted) +extern void BTPostKillScore(Entity *victim, Scalar damage); // KILL (+ MP death) // Mech target slots (verified vs the binary's weapon/fire path, part_013.c): // mech+0x37c Point3D current target world position (range/aim source) @@ -829,6 +832,9 @@ static void Entity::TakeDamageMessageID, sizeof(Entity::TakeDamageMessage), 0 /*inflictor id: bring-up*/, zone, dmg); tgt->Dispatch(&take_damage); + // gauge scoring wave (Step 6): a projectile hit credits SCORE too + // (tgt == gEnemyMech here; local player is the viewpoint shooter). + BTPostDamageScore((Entity *)tgt, p.damage); DEBUG_STREAM << "[projectile] IMPACT damage=" << p.damage << " zone=" << zone << "\n" << std::flush; } } @@ -2170,6 +2176,10 @@ void dmg); gEnemyMech->Dispatch(&take_damage); + // gauge scoring wave (Step 6): credit the local player for damage + // dealt -> SCORE climbs per hit (currentScore += tonnageRatio*award). + BTPostDamageScore(gEnemyMech, kShotDamage); + Scalar s = ((Mech *)gEnemyMech)->Zone(zone)->damageLevel; // [0,1], 1.0=destroyed (engine base field) DEBUG_STREAM << "[damage] hit zone " << zone << "/" << zc << " structure=" << s << "\n" << std::flush; @@ -2190,6 +2200,11 @@ void { gEnemyDestroyed = 1; + // gauge scoring wave (Step 7): credit the KILL to the local player + // -> KILLS 0->1 (senderMechID = the VICTIM, so the !=our-mech branch + // fires; the ownerless dummy yields no death, DEATHS stays 0). + BTPostKillScore(gEnemyMech, kShotDamage); + // Death explosion at the target. Origin death_origin = ((Mech *)gEnemyMech)->localOrigin; Explosion::MakeMessage death_exp(