diff --git a/docs/KD_SCOREBOARD_PLAN.md b/docs/KD_SCOREBOARD_PLAN.md index 0cc15c3..0dfa421 100644 --- a/docs/KD_SCOREBOARD_PLAN.md +++ b/docs/KD_SCOREBOARD_PLAN.md @@ -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. 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". \ No newline at end of file +**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=`, 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). diff --git a/docs/RECONCILE.md b/docs/RECONCILE.md index 8970b9a..c4e9baa 100644 --- a/docs/RECONCILE.md +++ b/docs/RECONCILE.md @@ -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). 5. THEN instantiate the heat family together (Condenser+Reservoir+bank) and verify the tick rises by 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. diff --git a/game/reconstructed/btplayer.cpp b/game/reconstructed/btplayer.cpp index a01177d..c61aa29 100644 --- a/game/reconstructed/btplayer.cpp +++ b/game/reconstructed/btplayer.cpp @@ -234,6 +234,22 @@ void BTPlayerLayoutSelfCheck() // never called -- a compile-time lock only static_assert(sizeof(BTPlayer) == 0x28c, "BTPlayer size changed -- re-measure the offsets above before trusting any " "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 @@ -1770,6 +1786,82 @@ void BTPlayerCountObservedDeath(void *player) << " 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 // highlight). Computed here via accessors -- the PilotList's raw-offset version // (local+0x284 objectiveMech, then deref target+0x190) reads garbage in our layout diff --git a/game/reconstructed/btplayer.hpp b/game/reconstructed/btplayer.hpp index 968506c..8636538 100644 --- a/game/reconstructed/btplayer.hpp +++ b/game/reconstructed/btplayer.hpp @@ -296,6 +296,41 @@ class DropZone__ReplyMessage; void 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 // diff --git a/scratchpad/rig45_down.ps1 b/scratchpad/rig45_down.ps1 new file mode 100644 index 0000000..4bb985c --- /dev/null +++ b/scratchpad/rig45_down.ps1 @@ -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" diff --git a/scratchpad/rig45_up.ps1 b/scratchpad/rig45_up.ps1 new file mode 100644 index 0000000..565b8e8 --- /dev/null +++ b/scratchpad/rig45_up.ps1 @@ -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"