diff --git a/context/gauges-hud.md b/context/gauges-hud.md index 7225b7c..6adc5da 100644 --- a/context/gauges-hud.md +++ b/context/gauges-hud.md @@ -304,13 +304,29 @@ screen paint over the shared Eng plane, pinning it on the highest screen). Pixel `BT_PRESET_TEST=`, `BT_CTRLMAP_LOG`. ## pilotList (Comm KILLS/DEATHS) row semantics + the −1 [T1/T2] -**BUG (Gitea #43, 2026-07-24, rig-reproduced): in LIVE MP only the LOCAL row renders — remote -players never get a row.** The "2-player MP = 2 rows" below is decomp row SEMANTICS [T1], not a -verified MP observation (the audit's live evidence was solo-only). Suspect: -`BuildPilotArray`'s count loop (mechmppr.cpp:1133) sizes the roster via the binary's RAW -`entry+0x29 & 0x40` spectator-flag read on our compiled objects (databinding trap; the fill -loop's same check was already TODO-removed) → pilotCount latches 1 (build-once latch), -`GetPilot(1)` bounds-rejects the remote. Fix + MP re-verify pending — see the issue. +**FIXED + RIG-VERIFIED (Gitea #43, 2026-07-24; awaiting live playtest).** Field report +("never lists anyone in MP") reproduced on a 2-node rig, THREE stacked causes, all fixed: +1. **Raw-flag miscount**: `BuildPilotArray`'s count loop used the binary's raw `entry+0x29 & + 0x40` read on our compiled objects (databinding trap) → garbage → roster latched at 1 row. + DECODED: +0x29 bit 0x40 = bit 14 of `simulationFlags` @+0x28 = **`Player:: + NonScoringPlayerFlag`** (= `Entity::NextBit`, PLAYER.h:392) — the roster authentically + lists SCORING players (flag cleared when the mech links its player, mech.cpp:688, all + nodes). Both loops now use `Player::IsScoringPlayer()`; also closes a `pilotIDs[1]` heap + overrun the undercount exposed. +2. **Latch race**: the binary's build-once latch assumes 1995 synchronous pod loads; over + async relay TCP the faster node ticks InterpretControls before the peer's replicants + arrive (rig-proven: staggered node logged roster 1 → then peer arrival). [T3 + accommodation, demand-latch precedent]: rebuild whenever the scoring CENSUS changes — + also un-dangles a departed peer's freed Player from the roster. Forensic: always-on + `[score] pilot roster built: N scoring pilot(s)` per census change. +3. **Invisible rows**: `LookupPlayerNameBitmap` was a NULL stub + numerals are authentic + `signedBlankedZerosFormat` (zeros draw blank) → an unselected 0/0 row rendered as a black + box: invisible even with a correct roster. Wired `BTPilotNameBitmap` (btplayer.cpp bridge: + compiled `playerBitmapIndex` → `Mission::GetSmallNameBitmap`, the same 64×16 egg rasters + the radar labels use; replaces the binary's raw pilot+0x1e0 key read). Rig-verified: + Aeolus (local, center box) + Boreas (remote, roster strip) both render with callsigns. + NOTE: rows need the egg to carry name bitmaps (operator-console eggs do; bare rig eggs + like MP2.EGG don't → authentic cache-miss blank box). One ROW PER PILOT in the mission (2-player MP = 2 rows — not duplicate displays). KILLS = `killCount` (the victim's ScoreMessageHandler credits the shooter cross-player, works for both rows); DEATHS = `Player::deathCount`: engine-inits to **−2** (PLAYER.cpp:759), the LOCAL diff --git a/game/reconstructed/btl4gau3.cpp b/game/reconstructed/btl4gau3.cpp index c1ac965..f5bf3b0 100644 --- a/game/reconstructed/btl4gau3.cpp +++ b/game/reconstructed/btl4gau3.cpp @@ -299,11 +299,11 @@ extern void *BTResolveRosterPilot(int slot); 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 -// own cache-miss branch (the tinted name box), never an AV. -static BitMap *LookupPlayerNameBitmap(int /*name_id*/) { return NULL; } +// Gitea #43: the pilot's SMALL callsign raster (Mission small-bitmap chain via +// the pilot's compiled playerBitmapIndex -- bridge in btplayer.cpp). Replaces +// the port's old NULL-stub cache (which left every row an invisible blank box) +// AND the binary's raw pilot+0x1e0 key read (our compiled layout differs). +extern BitMap *BTPilotNameBitmap(void *pilot); MethodDescription PilotList::methodDescription = @@ -429,8 +429,7 @@ Logical void PilotList::DrawMechIcon(void *pilot, int selected) { - int nameId = *(int *)((char *)pilot + 0x1e0); // pilot name/id key - BitMap *bmp = LookupPlayerNameBitmap(nameId); + BitMap *bmp = BTPilotNameBitmap(pilot); // small callsign raster int boxColor = selected ? 0xff : 0; int blitColor = selected ? 0 : 0xff; if (bmp == NULL) diff --git a/game/reconstructed/btplayer.cpp b/game/reconstructed/btplayer.cpp index 9824ab0..c98405c 100644 --- a/game/reconstructed/btplayer.cpp +++ b/game/reconstructed/btplayer.cpp @@ -1497,6 +1497,23 @@ int BTPilotDeaths(void *pilot) int deaths = ((BTPlayer *)pilot)->GetDeaths(); return (deaths < 0) ? 0 : deaths; } +// Gitea #43 completion: the pilot-list NAME icon. The binary blits the +// pilot's SMALL callsign raster -- the same 64x16 egg bitmaps the radar +// labels use (Mission's small-bitmap chain, keyed 1-based by the egg +// 'bitmapindex') -- but the port's PilotList had a NULL-stub lookup, so a +// roster row was a blank box + blanked-zero numerals: invisible until the +// pilot scored. Camera players carry index -1 (PLAYER.cpp:744) -> NULL -> +// DrawMechIcon's authentic cache-miss box branch. +BitMap *BTPilotNameBitmap(void *pilot) +{ + if (pilot == 0 || application == 0) + return 0; + Mission *mission = application->GetCurrentMission(); + int index = ((BTPlayer *)pilot)->playerBitmapIndex; + if (mission == 0 || index <= 0) + return 0; + return mission->GetSmallNameBitmap(index); +} // OBSERVED-DEATH tally (MP DEATHS fix): each node maintains every player's // score from LOCALLY OBSERVED events -- exactly how the KILLS credit already diff --git a/game/reconstructed/mechmppr.cpp b/game/reconstructed/mechmppr.cpp index 4d0a9ce..5695c8b 100644 --- a/game/reconstructed/mechmppr.cpp +++ b/game/reconstructed/mechmppr.cpp @@ -1110,18 +1110,33 @@ void //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // @004b0600 -- build the pilot roster once, lazily, from the application's -// "Players" group (skipping spectators flagged 0x40). +// "Players" group, counting SCORING players only. The binary's raw check +// `(entry+0x29 & 0x40) == 0` decoded (Gitea #43): +0x29 is byte 1 of the +// simulationFlags LWord @+0x28, so 0x40 there == bit 14 == Entity::NextBit == +// Player::NonScoringPlayerFlag (PLAYER.h:392). Players START non-scoring +// (DefaultFlags) and become scoring when their mech links its owning player +// (mech.cpp:688 / btplayer.cpp:1210) -- so camera ships and unseated players +// never get a roster row. Read via the named accessor, NOT the raw offset +// (our compiled layout differs -- the raw read is the databinding trap and +// was THE #43 bug: garbage count latched the roster at 1 row in live MP). // void MechControlsMapper::BuildPilotArray() { - if (pilotArrayBuilt) - { - return; - } - pilotArrayBuilt = True; - - pilotCount = 0; + // + // Scoring census. The binary latches the roster on the FIRST call + // (pilotArrayBuilt, build-once) -- correct on 1995 pod hardware where + // every pod's entities exist before any mission tick. In the port the + // peers' Player/PlayerLink messages arrive over async TCP, and the rig + // proved a frame-level race (Gitea #43): the faster-loading node ticks + // InterpretControls once BEFORE the net queue creates the replicants, + // latching a 1-row roster forever. [T3 accommodation, precedent = + // the demand-latch]: re-run the build whenever the SCORING census + // changes -- late-arriving pilots get their row, and a departed peer's + // destroyed Player leaves the roster instead of dangling (the latched + // design kept a freed pointer for the rest of the mission). + // + int scoring = 0; EntityGroup *players = application->GetEntityManager()->FindGroup("Players"); // @0050f44b @@ -1130,9 +1145,9 @@ void ChainIteratorOf pilots(players->groupMembers); // FUN_00421414 for (Node *entry; (entry = pilots.ReadAndNext()) != 0; ) { - if ((*(byte *)((int)entry + 0x29) & 0x40) == 0) // not a spectator + if (((Player *)entry)->IsScoringPlayer()) // binary: (+0x29 & 0x40) == 0 { - ++pilotCount; + ++scoring; } } } @@ -1141,10 +1156,18 @@ void // an 8-pod game fits). pilotCount beyond that would overrun the block in // the binary too -- clamp so the `<= pilotCount` zero-loop (which zeroes // the local slot 0 PLUS pilotCount remotes, faithful) stays in bounds. - if (pilotCount > PilotArraySlots - 1) - pilotCount = PilotArraySlots - 1; + if (scoring > PilotArraySlots - 1) + scoring = PilotArraySlots - 1; - pilotIDs = new int[pilotCount]; // FUN_004022b0 + if (pilotArrayBuilt && scoring == pilotCount) + { + return; // steady state -- the binary's built-once path + } + pilotArrayBuilt = True; + pilotCount = scoring; + + delete[] pilotIDs; // 0 on first build (ctor) + pilotIDs = new int[pilotCount > 0 ? pilotCount : 1]; // FUN_004022b0 for (int i = 0; i <= pilotCount; ++i) { @@ -1153,6 +1176,10 @@ void FillPilotArray(); // FUN_004b06cc ChooseDefaultPilot(); // FUN_004b07f0 + + // Always-on forensic (Gitea #43): one line per census change. + DEBUG_STREAM << "[score] pilot roster built: " << pilotCount + << " scoring pilot(s)" << std::endl; } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -1203,10 +1230,11 @@ void int slot = 1; for (Node *entry; (entry = pilots.ReadAndNext()) != 0; ) { - // TODO(mp-roster): the binary skips spectators via a flag byte at - // entry+0x29 (bit 0x40) -- its WinTesla home is unmapped; solo has no - // spectators, so the check is omitted until the flag is reconciled. - if (entry == (Node *)local_pilot) // already slot 0 + // Binary @004b0790: skip non-scoring players (the same +0x29 & 0x40 + // == NonScoringPlayerFlag check as the count loop -- resolved, Gitea + // #43) and the local pilot (already slot 0). + if (!((Player *)entry)->IsScoringPlayer() + || entry == (Node *)local_pilot) { continue; }