diff --git a/engine/MUNGA/SCNROLE.cpp b/engine/MUNGA/SCNROLE.cpp index 69ad18f..e41f6d7 100644 --- a/engine/MUNGA/SCNROLE.cpp +++ b/engine/MUNGA/SCNROLE.cpp @@ -55,9 +55,32 @@ ScenarioRole::ScenarioRole(const CString &role_name, const CString &model_file) damageBias = player_data->damageBias; friendlyFirePenalty = player_data->friendlyFirePenalty; player_res_des->Unlock(); + + // SCORE PROVENANCE (ungated, 2026-08-07). Every unmatched row of the + // original manual's scoring chart -- +500 a kill, -1000 an eject, -500 + // an ammo death -- is sourced HERE, from the role's GameModel record + // (type 0xf, 7 dwords; the binary's ctor @00429a9c copies rec[0..6]). + // A field log showed `charge=0 (role killBonus)`, and the two ways that + // happens -- record authored zero, or resource lookup missed -- are + // indistinguishable in release because the miss path's Warn/Tell compile + // out (DEBUGOFF.h). One line per role settles it. + DEBUG_STREAM << "[role] '" << (const char *)role_name + << "' model='" << (const char *)model_file + << "' killBonus=" << (float)killBonus + << " deathPenalty=" << (float)specialCaseDeathPenalty + << " dmgInf=" << (float)damageInflictedModifier + << " dmgRcv=" << (float)damageReceivedModifier + << " bias=" << (float)damageBias + << " ff=" << (float)friendlyFirePenalty + << " return=" << (int)returnFromDeath + << "\n" << std::flush; } else { + DEBUG_STREAM << "[role] '" << (const char *)role_name + << "' model='" << (const char *)model_file + << "' -- RESOURCE NOT FOUND, all score values default to 0" + << "\n" << std::flush; Tell(role_name); Warn(" does not exists in resource! "); damageReceivedModifier = 0.0f; diff --git a/game/reconstructed/btplayer.cpp b/game/reconstructed/btplayer.cpp index 5cb9f36..d44ffe8 100644 --- a/game/reconstructed/btplayer.cpp +++ b/game/reconstructed/btplayer.cpp @@ -1952,15 +1952,51 @@ BTPlayer::BTPlayer( } // - // Look the scoring role up in the role registry (keyed by the role name - // in the creation message, +0x90) and stash it as our scenarioRole. The - // BT role registry (BTMission::GetRoleRegistry()->Lookup) has no WinTesla - // analog, so the scenarioRole set by the base Player ctor stands. - // CROSS-FAMILY: needs BTMission role-registry access. BEST-EFFORT. + // Look the scoring role up in the role registry (keyed by the role name in + // the creation message, +0x90) and stash it as our scenarioRole. + // + // WIRED 2026-08-07. This was commented out with "the BT role registry + // (BTMission::GetRoleRegistry()->Lookup) has no WinTesla analog, so the + // scenarioRole set by the base Player ctor stands". The base ctor sets it + // to NULL (PLAYER.cpp:680), so it stood NULL forever -- and EVERY scoring + // value the game has lives on that pointer: + // + // killBonus -> the kill award basis (authored 500) + // specialCaseDeathPenalty-> the death cost (authored 500) + // damageInflictedModifier-> the per-hit multiplier (authored 1) + // returnFromDeath -> the entry credit (authored 1000) + // + // With it NULL: kill_bonus reads 0 (a kill scored the damage tally alone -- + // benched 4.88 instead of ~500), the eject charge is 0 (field log: + // "PUNCH-OUT: charge=0 (role killBonus)" = #134's missing penalty), and the + // death cost block is skipped entirely. That is four rows of the original + // manual's scoring chart, all from one commented-out line. + // + // The analog DOES exist: Mission::GetScenarioRole(name) (MISSION.h:162) + // walks scenarioRoleChain -- the very dictionary BTL4Mission fills via + // AddScenarioRole() when it parses the role pages (btl4mssn.cpp), whose own + // comment already says the WinTesla base exposes it. Same lookup, same + // key, no cross-family gap. // CString role_key(creation_message->roleName); // make+0x90 - (void)role_key; - // scenarioRole = playerMission->GetRoleRegistry()->Lookup(&role_key); // this[0x7e]+0x50, this[0x82] + if (playerMission != 0) + { + ScenarioRole *found = playerMission->GetScenarioRole(role_key); // this[0x7e]+0x50 + if (found == 0) + { + // The shipped content authors ONE role page, "Role::Default" + // (model dfltrole). A creation message naming anything else -- or + // naming nothing -- must still score, so fall back to it rather + // than leave the pointer NULL and silently zero every award. + found = playerMission->GetScenarioRole(CString("Role::Default")); + } + scenarioRole = found; // this[0x82] + DEBUG_STREAM << "[role] player " << BTMatchHostOf(GetEntityID()) + << ":" << (int)GetEntityID() << " key='" << (const char *)role_key + << "' -> " << (scenarioRole != 0 ? "BOUND" : "NULL (scores will be 0)") + << (scenarioRole != 0 ? "" : " !!") + << "\n" << std::flush; + } if ((simulationFlags & 0xc) == 4) { diff --git a/scratchpad/night13/scoreverify.sh b/scratchpad/night13/scoreverify.sh index 65e7161..1196909 100644 --- a/scratchpad/night13/scoreverify.sh +++ b/scratchpad/night13/scoreverify.sh @@ -50,7 +50,7 @@ sleep 2 sleep 5 python ../tools/btconsole.py SV.EGG 127.0.0.1:1501 127.0.0.1:1601 > sv_relay.log 2>&1 & RELAY=$! -sleep 260 +sleep 400 kill $RELAY 2>/dev/null sleep 3 bt_kill_ours