K/D scoreboard ROOT-CAUSED for real (#45): the kill credit was rerouted, not lost

Kills/deaths only ever appeared on ONE machine.  The cause is not a missing
tally -- it is Entity::Dispatch:

    if (GetInstance() == ReplicantInstance)              // ENTITY.cpp:244-251
        application->SendMessage(ownerID, EntityManagerClientID, message);

BTPostKillScore runs on the VICTIM's node (the only node whose mech carries a
populated lastInflictingID), resolves the killer's Player -- a REPLICANT there --
and Dispatch()es to it.  The engine reroutes that to the owning host, so
++killCount lands on the killer's own PC and nowhere else, and nothing carried it
back out: Player__UpdateRecord is currentScore + dropZoneLocation only.  Every
other pod's copy therefore read 0 all mission.  playerLink was never NULL for
these kills -- the dispatch proves it resolved.

Field proof over the whole corpus (255 node-logs): 125 of 125 SCORE type=2 rows
credit the LOGGING node's own player, not one credits a remote pilot; and 0 of
8800 DMG rows target a replicant, so no other node can even know the killer.

This means the owner's counter is already the single authoritative copy,
incremented exactly once per kill.  So the "design decision" the plan was blocked
on collapses: there is no second writer to reconcile, only a one-way
owner->replicant mirror to add.

  * BTPlayer__UpdateRecord = Player::UpdateRecord + killTally + deathTally, with
    Read/WriteUpdateRecord overrides.  No new data member, no new virtual ->
    sizeof(BTPlayer)==0x28c and every existing offset lock still holds.
  * Both counter writes already ForceUpdate() (:508 death, :829/:840 score), so
    no dirty-bit edit was needed (plan risk 6 avoided).
  * recordLength guard in ReadUpdateRecord: a pod on a build without the
    extension degrades to "remote counters don't move" instead of reading a
    neighbouring record as a kill count.
  * Size locks added per plan risk 2 (no false offsetof assert).

Rig-verified, 2-node loopback (scratchpad/rig45_up.ps1, BT_MP_FORCE_DMG):
owner 3:1 finished kills 3/deaths 2 and the peer read kills=3 deaths=2; owner 2:1
finished kills 2/deaths 3 and the peer read kills=2 deaths=3.  Exact convergence
where a remote column previously never left 0.  3 respawns per side with intact
death sequences (deathCount is only overwritten on replicant copies; the
handshake runs on masters), no crash, no GLITCH rows.

Kept byte-faithful: the kill handler's partner increment (the binary's
inc [ebx+0x27c] / inc [edx+0x27c] wrong-column slip) is still reproduced.  It
always lands on a replicant copy, so the owner's record now overwrites it -- the
rig caught the correction repeatedly (wasKills=3 -> kills=2).  The phantom kill
stops being visible without silently deciding the fidelity question.

Still open and deliberately untouched: which field the LOCAL pod's DEATHS column
should read (+0x280 vs deathCount), last-hitter-takes-all/ram kills, and the
slip's fidelity.  Awaiting live multi-pod verification by a human.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-25 21:40:59 -05:00
co-authored by Claude Opus 5
parent da617e6f8f
commit 4fa7eee54f
6 changed files with 372 additions and 1 deletions
+109 -1
View File
@@ -228,4 +228,112 @@ Use an egg that carries name bitmaps (OPERATOR-class), or the rows render as aut
9. **Deleting `BTPlayerCountObservedDeath` is safe but must be complete.** It survives today only via the `extern` + the `friend` at btplayer.hpp:389; remove the definition, the call site, the extern and the friend together, or `/FORCE` hides the stragglers again. 9. **Deleting `BTPlayerCountObservedDeath` is safe but must be complete.** It survives today only via the `extern` + the `friend` at btplayer.hpp:389; remove the definition, the call site, the extern and the friend together, or `/FORCE` hides the stragglers again.
10. **Deviation ledger.** Two intentional departures ship here — dropping btplayer.cpp:704 (a byte-faithful 1995 increment) and adding the update-record override the binary does not have. Both must land in `docs/RECONCILE.md` with tiers (`[T3]` intent, `[T1]` addresses) so a future reader does not "restore fidelity" and re-open #45. 10. **Deviation ledger.** Two intentional departures ship here — dropping btplayer.cpp:704 (a byte-faithful 1995 increment) and adding the update-record override the binary does not have. Both must land in `docs/RECONCILE.md` with tiers (`[T3]` intent, `[T1]` addresses) so a future reader does not "restore fidelity" and re-open #45.
**Residual unknowns, each with the one check that settles it.** (a) *Why `GetPlayerLink()` was NULL for those four deaths* — the `PLAYER_LINK`/`DEAD_NOTIFY` records of Step 0 in one 2-node run; `link=0` convicts it, `link!=0` with no `death cycle START` moves it to the handler. (b) *Whether the rerouted `BTPlayer` ScoreMessage is reliable-ordered on the Steam/relay seam* — nothing retries it, so a dropped packet loses a kill forever today; with Step 4 the record heals it, which is a further argument for replication. Confirm `Application::SendMessage` to `EntityManagerClientID` in `engine/MUNGA_L4/L4NET.CPP`. (c) *The printed column labels* — only needed to name the columns for the user, not to place the fields; read the Comm background pixmap or `reference/manual/Tesla40_BT_manual.pdf`'s Comm page. (d) *Whether the six corpus-wide excess kill awards (53 `SCORE type=2` vs 47 `DEATH inst=M`, all six outside the multi-node rounds) are a second double-scoring path* — the `TALLY` records of §4 answer it in one solo `BT_SPAWN_ENEMY` run; run it down before anyone claims "kills are awarded exactly once". **Residual unknowns, each with the one check that settles it.** (a) *Why `GetPlayerLink()` was NULL for those four deaths* — the `PLAYER_LINK`/`DEAD_NOTIFY` records of Step 0 in one 2-node run; `link=0` convicts it, `link!=0` with no `death cycle START` moves it to the handler. (b) *Whether the rerouted `BTPlayer` ScoreMessage is reliable-ordered on the Steam/relay seam* — nothing retries it, so a dropped packet loses a kill forever today; with Step 4 the record heals it, which is a further argument for replication. Confirm `Application::SendMessage` to `EntityManagerClientID` in `engine/MUNGA_L4/L4NET.CPP`. (c) *The printed column labels* — only needed to name the columns for the user, not to place the fields; read the Comm background pixmap or `reference/manual/Tesla40_BT_manual.pdf`'s Comm page. (d) *Whether the six corpus-wide excess kill awards (53 `SCORE type=2` vs 47 `DEATH inst=M`, all six outside the multi-node rounds) are a second double-scoring path* — the `TALLY` records of §4 answer it in one solo `BT_SPAWN_ENEMY` run; run it down before anyone claims "kills are awarded exactly once".
---
# ADDENDUM 2026-07-25 (late) — the missing mechanism; the DESIGN DECISION is resolved
**Status change: ROOT-CAUSED, FIXED, and RIG-VERIFIED on a 2-node loopback.
Awaiting live multi-pod verification by a human.** This addendum supersedes the §3
framing that a convergence rule had to be *invented*.
## What was missing
§2/§3 established that neither counter is replicated, and reasoned that a per-node
observed tally therefore had to be added — noting only in passing (residual unknown
(b)) that the ScoreMessage is "rerouted". That reroute IS the mechanism, and it
changes the conclusion:
`BTPostKillScore` runs on the **victim's** node (the only node whose mech carries a
populated `lastInflictingID`). It resolves the killer's `Player` — which on that node
is a **REPLICANT** — and calls `Dispatch()` on it. `Entity::Dispatch` reroutes a
replicant's message to the owning host:
```cpp
if (GetInstance() == ReplicantInstance) // ENTITY.cpp:244-251
application->SendMessage(ownerID, EntityManagerClientID, message);
```
So the `++killCount` is **not lost — it is relocated to the killer's own machine**,
and it is correct there. Nothing carries it back out, and `Player__UpdateRecord` is
`currentScore + dropZoneLocation` only, so every other pod's copy stays 0 all
mission. `playerLink` was never NULL for these kills — the dispatch proves it
resolved. [T0 on the reroute and the record contents.]
## Field proof
- **125 of 125** `SCORE type=2` records across **255 node-logs** credit the LOGGING
node's OWN player; **not one** credits a remote pilot.
- **0 of 8800** `DMG` rows target a replicant — so a non-victim node genuinely
cannot know the killer, which is why every `DEATH inst=R` row reads `killer=0:0`
and why §4's "unreachable observed tally" could never have worked anyway.
- Tonight's 3 nodes, one kill: host 3 logged `player=3:1 type=2 award=5.88`; the
award equals the `killdmg=5.881` recorded on **host 4** — the value crossed the
wire. Host 4 (the victim, the user's pod) logged the death and no kill at all.
## Why the design decision collapses
The owner's counter is already the single authoritative copy, incremented exactly
once per kill through the engine's own reroute. There is no second writer to
reconcile and no rule to choose: the fix is a **one-way owner→replicant mirror**.
Step 1 (a new `deaths` member, changing the displayed field) is NOT needed for the
user-visible bug — `deathCount` already tallies deaths correctly on the owner
(`++deathCount`, btplayer.cpp:469; `PLAYER_DEAD deaths=1..4` matches).
## What landed (unbuilt)
`BTPlayer__UpdateRecord` (btplayer.hpp) = `Player::UpdateRecord` + `killTally` +
`deathTally`, with `Read/WriteUpdateRecord` overrides (btplayer.cpp). Adds no data
member and no new vtable slot, so `sizeof(BTPlayer)==0x28c` is untouched. Both
counter writes already `ForceUpdate()` (:508 death, :829/:840 score), so the record
ships without a new dirty-bit edit — §5 risk 6 is avoided entirely.
Satisfies §5: risk 1 avoided (no new member → no shadow), risk 2 locks added
(`sizeof(UpdateRecord) > sizeof(Player::UpdateRecord)` and `== base + 2*sizeof(int)`),
risk 4 respected (exactly two ints), risk 5 **improved** — a `recordLength` guard in
`ReadUpdateRecord` makes a mixed-build session degrade to "counters don't update"
instead of reading a neighbouring record as a kill count.
**Bonus:** the §Headline-6 phantom kill (the binary's partner `inc [+0x27c]` on the
VICTIM's player) always lands on a replicant copy, so the owner's authoritative
value now overwrites it on the next record — the phantom stops being visible
without touching the faithfully-reconstructed handler.
## Verification (2026-07-25, 2-node loopback, Release, `BT_MP_FORCE_DMG=1`)
Rig: `scratchpad/rig45_up.ps1` / `rig45_down.ps1` — two Release nodes on 1501/1601
pinned to disjoint cores (the task #50 TCP-batching artifact) plus the console relay,
recording only its own PIDs so teardown never kills by name. Logs:
`content/matchlog_20260725_213627_{28896,22980}.txt`.
- **Build clean.** 0 `error C` (so both new `static_assert`s hold), and **no
`BTPlayer::*UpdateRecord` among the unresolved externals** — §5 risk 3 cleared. The
40 `LNK2019`s in that log are the pre-existing `/FORCE`-tolerated
`CreateStreamedSubsystem` family from `mech3.obj`, untouched by this change.
- **Cross-node convergence exact.** Owner 3:1 finished kills 3 / deaths 2 and the peer
read `kills=3 deaths=2`; owner 2:1 finished kills 2 / deaths 3 and the peer read
`kills=2 deaths=3`. Before the fix a remote pilot's column never left 0.
- **The `-2` seed is gone off-node**: the first mirror on each side shows
`wasDeaths=-2 -> deaths=<real>`, so a remote pilot's DEATHS no longer displays the
clamped fake 0 that §Headline-1 describes. (§Headline-1's *own-pod* field-choice
question is untouched and still open.)
- **Respawn unaffected** (the §5 risk-8 worry): 3 respawns per side, `deaths=` sequences
intact, no drop-zone or latch anomaly. `deathCount` is only ever overwritten on a
REPLICANT copy, and the handshake runs on masters.
- **Phantom kill now self-corrects**: the rig captured the owner's value overwriting the
locally-applied partner increment repeatedly (`wasKills=3 -> kills=2`,
`wasKills=2 -> kills=1`).
- No crash, no `GLITCH` (plane-audit) rows, both nodes alive through 3 death/respawn
cycles.
## Still to do
1. **Live multi-pod verification by a human** — the memory rule: this stays "fix landed,
awaiting verification" until confirmed in a real session.
2. All pods must run the same build for a scoreboard test (§5 risk 5) — the guard makes a
mixed session degrade safely rather than corrupt, but the numbers only converge when
both ends have the extension.
3. Still open, deliberately untouched: §Headline-1 (which field the LOCAL pod's DEATHS
column should read, `+0x280` vs `deathCount`), §Headline-5 (last-hitter-takes-all and
ram kills), and §Headline-6's fidelity question (the wrong-column slip is still
reproduced; it is merely no longer visible).
+44
View File
@@ -1562,3 +1562,47 @@ absent inter-sink linkage (interdependence). Reservoir (0xBC0) and HeatSink bank
RefrigerationSimulation in its ctor) — REQUIRES heat.cpp edits (out of scope for this pilot's file set). RefrigerationSimulation in its ctor) — REQUIRES heat.cpp edits (out of scope for this pilot's file set).
5. THEN instantiate the heat family together (Condenser+Reservoir+bank) and verify the tick rises by 5. THEN instantiate the heat family together (Condenser+Reservoir+bank) and verify the tick rises by
their count without the UpdateCoolant fault. their count without the UpdateCoolant fault.
---
## Deliberate divergence from the binary: BTPlayer update-record override (Gitea #45)
**2026-07-25. One departure, verified live. Do NOT "restore fidelity" here — doing so re-opens #45.**
**What the binary does.** `BTPlayer`'s vtable slots 6/7 (`@0x513300`) point at the ENGINE's
`Player::Read/WriteUpdateRecord`; the 1995 class adds no override, and `Player__UpdateRecord`
carries `currentScore + dropZoneLocation` only (`PLAYER.h:73-80`). [T0/T1]
**What we ship.** `BTPlayer__UpdateRecord` = `Player::UpdateRecord` + `killTally` + `deathTally`,
with `Read/WriteUpdateRecord` overrides (`btplayer.hpp`, `btplayer.cpp`). Our vtable slots 6/7
therefore point at BTPlayer's versions. No new data member and no new virtual, so
`sizeof(BTPlayer) == 0x28c` is unchanged and the existing offset locks still hold. **[T3 on the
divergence being *necessary*; T0 on the mechanism that makes it necessary.]**
**Why it is necessary.** A kill is credited by `BTPostKillScore` on the VICTIM's node, which
resolves the killer's `Player` — a REPLICANT there — and calls `Dispatch()`. `Entity::Dispatch`
reroutes a replicant's message to the owning host (`ENTITY.cpp:244-251`), so `++killCount` lands
on the killer's own machine and nowhere else, and nothing ever carried it back out. Field proof
before the fix: across 255 node-logs, **125 of 125** `SCORE type=2` rows credit the LOGGING node's
own player and **not one** credits a remote pilot; **0 of 8800** `DMG` rows target a replicant, so
no other node can even know the killer. The pods' closed LAN made the one-way engine record
sufficient in 1995 only because nothing ever needed the counters off-node — the arcade cabinet
scoreboard each pod drew was its own.
**Verified live** (2-node loopback, Release, `BT_MP_FORCE_DMG`, 2026-07-25): both nodes converged
exactly — owner 3:1 (kills 3 / deaths 2) read `kills=3 deaths=2` on the peer, owner 2:1
(kills 2 / deaths 3) read `kills=2 deaths=3` on the peer. 3 respawns per side, death sequences
intact, no crash.
**What we did NOT change (kept byte-faithful).** The kill handler's partner increment
`++sender_owner->killCount` (btplayer.cpp, the binary's `inc [ebx+0x27c]` / `inc [edx+0x27c]`
@0x4c0397/@0x4c03a3 wrong-column slip) is still reproduced as shipped. It always lands on a
REPLICANT copy of the victim, so the owner's authoritative record now overwrites it on the next
update — the rig captured the correction repeatedly (`wasKills=3 -> kills=2`,
`wasKills=2 -> kills=1`). The phantom kill stops being *visible* without editing the
reconstructed handler, so the fidelity question stays open rather than being silently decided.
**Mixed-build note.** `ReadUpdateRecord` early-returns when `recordLength < sizeof(UpdateRecord)`,
so a pod on a build without the extension degrades to "remote counters don't move" instead of
reading a neighbouring record as a kill count. All pods should still run the same build for a
scoreboard test.
+92
View File
@@ -234,6 +234,22 @@ void BTPlayerLayoutSelfCheck() // never called -- a compile-time lock only
static_assert(sizeof(BTPlayer) == 0x28c, static_assert(sizeof(BTPlayer) == 0x28c,
"BTPlayer size changed -- re-measure the offsets above before trusting any " "BTPlayer size changed -- re-measure the offsets above before trusting any "
"raw-offset code that touches this class"); "raw-offset code that touches this class");
//
// Scoreboard replication (Gitea #45). There is no offsetof lock to make here
// -- our BTPlayer is not offset-faithful -- so lock what IS real: the record
// must stay strictly larger than the base (or the two counters are not being
// carried at all) and must stay exactly two ints wider (the update stream is
// a single fixed 1400-byte buffer, ENTITY.cpp:472-474, and this record must
// not become the precedent for a fat player record).
//
static_assert(sizeof(BTPlayer::UpdateRecord) > sizeof(Player::UpdateRecord),
"BTPlayer::UpdateRecord no longer extends Player::UpdateRecord -- the "
"KILLS/DEATHS columns would silently stop replicating (Gitea #45)");
static_assert(sizeof(BTPlayer::UpdateRecord)
== sizeof(Player::UpdateRecord) + 2 * sizeof(int),
"BTPlayer::UpdateRecord grew beyond the two scoreboard counters -- "
"re-check the 1400-byte update buffer before widening the player record");
} }
static const char *SelfDestructName = "self destruct"; // &DAT_00524b38 static const char *SelfDestructName = "self destruct"; // &DAT_00524b38
@@ -1770,6 +1786,82 @@ void BTPlayerCountObservedDeath(void *player)
<< " deaths=" << p->deathCount << "\n" << std::flush; << " deaths=" << p->deathCount << "\n" << std::flush;
} }
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// WriteUpdateRecord / ReadUpdateRecord (Gitea #45)
//
// SCOREBOARD REPLICATION -- port record extension; the full root-cause banner is
// on the record struct in btplayer.hpp. In one line: a kill is credited by
// `BTPostKillScore` on the VICTIM's node, which Dispatch()es to the killer's
// Player -- a replicant there -- and `Entity::Dispatch` reroutes a replicant's
// message to the master (ENTITY.cpp:244-251), so `++killCount` lands on the
// killer's OWN machine only. Nothing carried it back out, so every other pod's
// copy read 0 all mission. The owner's counter is therefore already the single
// authoritative value; this is a plain one-way owner->replicant mirror.
//
// Sends for free: both counter writes already dirty the record --
// `ForceUpdate()` on the death path (:508) and on every score award (:829/:840).
//
// Side effect worth knowing: the binary's kill handler also bumps killCount on
// the VICTIM's player (the @0x4c0397/@0x4c03a3 wrong-column slip we reproduce
// faithfully). That partner increment always lands on a REPLICANT copy of the
// victim, so the owner's authoritative value now overwrites it on the next
// record -- the phantom kill stops being visible without touching the
// reconstructed handler.
//
void
BTPlayer::WriteUpdateRecord(Simulation__UpdateRecord *message, int update_model)
{
Player::WriteUpdateRecord(message, update_model);
UpdateRecord *rec = (UpdateRecord *)message;
rec->recordLength = sizeof(UpdateRecord);
rec->killTally = killCount;
rec->deathTally = deathCount;
}
void
BTPlayer::ReadUpdateRecord(Simulation__UpdateRecord *message)
{
Player::ReadUpdateRecord(message);
UpdateRecord *rec = (UpdateRecord *)message;
//
// MIXED-VERSION GUARD: a peer on a build without this extension writes a
// plain Player-sized record, and reading the two fields off the end would
// pick up whatever record was packed after it in the update stream and
// display it as a kill count. Trust the length the writer stamped.
//
if (rec->recordLength < sizeof(UpdateRecord))
{
return;
}
int was_kills = killCount;
int was_deaths = deathCount;
killCount = rec->killTally;
deathCount = rec->deathTally;
//
// Log only the EDGE -- this runs at update rate, so an unconditional line
// would drown the log (and the matchlog) in per-frame noise.
//
if (killCount != was_kills || deathCount != was_deaths)
{
if (BTMatchLogActive())
BTMatchLog("SBMIRROR",
"player=%d:%d kills=%d deaths=%d wasKills=%d wasDeaths=%d",
BTMatchHostOf(GetEntityID()), (int)GetEntityID(),
killCount, deathCount, was_kills, was_deaths);
if (getenv("BT_SCORE_LOG"))
DEBUG_STREAM << "[score] scoreboard mirror: pilot "
<< (int)GetEntityID() << " kills " << was_kills << "->"
<< killCount << " deaths " << was_deaths << "->" << deathCount
<< "\n" << std::flush;
}
}
// Is `pilot` the owner of `local_player`'s current target? (the SELECT-TARGET row // 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 // highlight). Computed here via accessors -- the PilotList's raw-offset version
// (local+0x284 objectiveMech, then deref target+0x190) reads garbage in our layout // (local+0x284 objectiveMech, then deref target+0x190) reads garbage in our layout
+35
View File
@@ -296,6 +296,41 @@ class DropZone__ReplyMessage;
void void
PlayerSimulation(Scalar time_slice); // @004c083c (console update) PlayerSimulation(Scalar time_slice); // @004c083c (console update)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Scoreboard replication (PORT record extension -- Gitea #45)
//
// The two scoreboard counters live ONLY on the owning pod. A kill is
// credited by `BTPostKillScore` on the VICTIM's node, which resolves the
// killer's Player -- a REPLICANT there -- and calls Dispatch() on it;
// `Entity::Dispatch` reroutes a replicant's message to the master
// (ENTITY.cpp:244-251), so the `++killCount` lands on the killer's OWN
// machine and nowhere else. DEATHS is the same shape (the victim's own
// VehicleDead path). Neither counter rode an update record --
// `Player__UpdateRecord` is currentScore + dropZoneLocation only
// (PLAYER.h:73-80) -- so every OTHER node's copy stayed 0 for the whole
// mission, which is exactly why the comm panel never showed a pilot's
// kills. [T0 the reroute + the record contents; T1 the field evidence:
// across 255 node-logs, 125 of 125 type=2 KILL records credit the LOGGING
// node's own player and not one credits a remote pilot.]
//
// The owner's value is already the single authoritative copy -- incremented
// exactly once per kill, through the engine's own reroute -- so completing
// the loop is a one-directional owner->replicant replication with no
// convergence rule to invent. Same pattern as the port's MissileLauncher
// record extension (mislanch.hpp:110-120).
//
public:
struct BTPlayer__UpdateRecord:
public Player::UpdateRecord
{
int killTally; // @0x27c killCount -- the KILLS column
int deathTally; // deathCount -- the DEATHS column
};
typedef BTPlayer__UpdateRecord UpdateRecord;
void WriteUpdateRecord(Simulation__UpdateRecord *message, int update_model); // base-typed param (override rule)
void ReadUpdateRecord(Simulation__UpdateRecord *message);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Construction and Destruction // Construction and Destruction
// //
+28
View File
@@ -0,0 +1,28 @@
# rig45_down.ps1 -- tear down ONLY the PIDs rig45_up.ps1 recorded.
# Deliberately does NOT use Get-Process -Name / Stop-Process by name: a blanket
# name kill once destroyed a live tester session. If a PID has been recycled to
# a different image, it is skipped.
$ErrorActionPreference = 'Continue'
$scratch = 'C:\Users\EPILECTRIK\AppData\Local\Temp\claude\C--git-bt411\89ab96e9-6cf0-4555-939d-05bf3708745a\scratchpad'
$pidfile = Join-Path $scratch 'rig45_pids.txt'
if (-not (Test-Path $pidfile)) { "no pidfile -- nothing recorded, nothing killed"; exit 0 }
foreach ($line in Get-Content $pidfile) {
$procId = 0
if (-not [int]::TryParse($line.Trim(), [ref]$procId)) { continue }
$p = Get-Process -Id $procId -ErrorAction SilentlyContinue
if (-not $p) { "pid $procId already gone"; continue }
# Only kill it if it is still one of the images this rig launches.
if ($p.ProcessName -notin @('btl4','python')) {
"pid $procId is now '$($p.ProcessName)' -- PID recycled, SKIPPING"
continue
}
Stop-Process -Id $procId -Confirm:$false -ErrorAction SilentlyContinue
Start-Sleep -Milliseconds 300
if (Get-Process -Id $procId -ErrorAction SilentlyContinue) { "pid $procId ($($p.ProcessName)) DID NOT DIE" }
else { "killed pid $procId ($($p.ProcessName))" }
}
Remove-Item $pidfile -ErrorAction SilentlyContinue
"teardown complete"
+64
View File
@@ -0,0 +1,64 @@
# rig45_up.ps1 -- 2-node loopback MP rig for Gitea #45 (scoreboard replication).
# Launches two Release nodes + the console relay, pins them to disjoint cores
# (the task #50 TCP-batching artifact), and records ONLY the PIDs it spawned so
# teardown can kill exactly those and nothing else. Never kills by name.
$ErrorActionPreference = 'Continue'
$repo = 'C:\git\bt411'
$content = Join-Path $repo 'content'
$exe = Join-Path $repo 'build\Release\btl4.exe'
$scratch = 'C:\Users\EPILECTRIK\AppData\Local\Temp\claude\C--git-bt411\89ab96e9-6cf0-4555-939d-05bf3708745a\scratchpad'
$pidfile = Join-Path $scratch 'rig45_pids.txt'
if (-not (Test-Path $exe)) { throw "no exe at $exe" }
# Guard: refuse to run if any btl4 is already up -- that would be someone else's.
$pre = @(Get-Process -Name btl4 -ErrorAction SilentlyContinue)
if ($pre.Count -gt 0) {
"ABORT: btl4 already running (pids: $($pre.Id -join ',')) -- not mine, refusing to start"
exit 1
}
Push-Location $content
Remove-Item fa.log, fb.log, console.log, console.err -ErrorAction SilentlyContinue
# Stamp the start so I can pick out only the matchlogs this run produces.
$stamp = Get-Date
Set-Content -Path (Join-Path $scratch 'rig45_t0.txt') -Value $stamp.ToString('o')
# Shared env: the cross-pod damage hook drives the kill without a human on a
# boresight; the score/death logs give the per-node narrative.
$env:BT_MP_FORCE_DMG = '1'
$env:BT_SCORE_LOG = '1'
$env:BT_DEATH_LOG = '1'
$env:BT_MP_LOG = '1'
$env:BT_START_INSIDE = '1'
$env:BT_DEV_GAUGES = '1'
$env:BT_MATCHLOG = '1'
$env:BT_LOG = 'fb.log'; $env:BT_SPAWN_AT = '40 -150'
$b = Start-Process -FilePath $exe -ArgumentList '-egg','FOGDAY.EGG','-net','1601' `
-WorkingDirectory $content -PassThru
Start-Sleep -Seconds 2
$env:BT_LOG = 'fa.log'; $env:BT_SPAWN_AT = '-40 -150'
$a = Start-Process -FilePath $exe -ArgumentList '-egg','FOGDAY.EGG','-net','1501' `
-WorkingDirectory $content -PassThru
Start-Sleep -Seconds 3
try { $b.ProcessorAffinity = [IntPtr]0x00F; $a.ProcessorAffinity = [IntPtr]0x3C0; $pinned = 'yes' }
catch { $pinned = "failed: $($_.Exception.Message)" }
Start-Sleep -Seconds 6
$c = Start-Process -FilePath 'python' `
-ArgumentList '-u', (Join-Path $repo 'tools\btconsole.py'), 'FOGDAY.EGG', '127.0.0.1:1501', '127.0.0.1:1601' `
-WorkingDirectory $content -PassThru `
-RedirectStandardOutput (Join-Path $content 'console.log') `
-RedirectStandardError (Join-Path $content 'console.err')
Set-Content -Path $pidfile -Value @($a.Id, $b.Id, $c.Id)
Pop-Location
"node_a(1501)=$($a.Id) node_b(1601)=$($b.Id) console=$($c.Id) pinned=$pinned"
"pids recorded in $pidfile"