Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70ada2789f |
@@ -82,7 +82,6 @@ precise than anything you can infer.
|
|||||||
| Multiplayer, replication, netcode | `context/multiplayer.md` |
|
| Multiplayer, replication, netcode | `context/multiplayer.md` |
|
||||||
| **The OPERATOR CONSOLE + RELAY** — how to launch it, ports, seats, the round/launch lifecycle, re-arm | `context/operator-console.md` |
|
| **The OPERATOR CONSOLE + RELAY** — how to launch it, ports, seats, the round/launch lifecycle, re-arm | `context/operator-console.md` |
|
||||||
| Running a session as sysop (operator-facing how-to) | `docs/OPERATOR_GUIDE.md` |
|
| Running a session as sysop (operator-facing how-to) | `docs/OPERATOR_GUIDE.md` |
|
||||||
| The tester-facing OPEN-ISSUES handout (for Discord) | `docs/OPEN_ISSUES_FOR_TESTERS.txt` — regenerate from the tracker + update the header date before each playtest |
|
|
||||||
| Pod hardware, monitors, RIO, MFD surfaces, input remap (CONTROLS.MAP/XInput) | `context/pod-hardware.md` |
|
| Pod hardware, monitors, RIO, MFD surfaces, input remap (CONTROLS.MAP/XInput) | `context/pod-hardware.md` |
|
||||||
| Glass cockpit desktop dev layer (BT_GLASS/BT_STEAM gates, PadRIO, miniconsole, Steam) | `context/glass-cockpit.md` |
|
| Glass cockpit desktop dev layer (BT_GLASS/BT_STEAM gates, PadRIO, miniconsole, Steam) | `context/glass-cockpit.md` |
|
||||||
| Steam internet MP (the wire seam, identity tokens, lobby) | `context/steam-networking.md` |
|
| Steam internet MP (the wire seam, identity tokens, lobby) | `context/steam-networking.md` |
|
||||||
|
|||||||
@@ -1384,7 +1384,7 @@ cmHeat(K,ModeSecondaryHeat,58,heatpal.pcc,heatpal2.pcc,Myomers);
|
|||||||
}
|
}
|
||||||
|
|
||||||
#=======================================================================
|
#=======================================================================
|
||||||
# Denkou (was: Sunder V1)
|
# Sunder V1
|
||||||
#=======================================================================
|
#=======================================================================
|
||||||
Snd1Init
|
Snd1Init
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -69,23 +69,6 @@ run\run.cmd [EGG] # default DEV.EGG; cd's to content\ and runs btl4.exe -eg
|
|||||||
⚠ **Keep the `if /I "%%P"=="btl4.exe"` guard** — parsing tasklist with `tokens=2` alone reads its
|
⚠ **Keep the `if /I "%%P"=="btl4.exe"` guard** — parsing tasklist with `tokens=2` alone reads its
|
||||||
*"INFO: No tasks are running"* line as a PID and spins forever with nothing running. Full
|
*"INFO: No tasks are running"* line as a PID and spins forever with nothing running. Full
|
||||||
investigation, including which windows exit vs merely hide: `phases/phase-12-orphan-processes.md`.
|
investigation, including which windows exit vs merely hide: `phases/phase-12-orphan-processes.md`.
|
||||||
- **Field-log anatomy — reading a steam night (#157 forensics, 2026-08-10) [T2].** A night's log is
|
|
||||||
a CHAIN of `===== BT411 SESSION` blocks: menu stubs (`args=''`, "first process") alternating with
|
|
||||||
glass generations (`-net 1501 -platform glass`, "relaunched generation"); one glass block = one
|
|
||||||
drop, and boundaries are synchronized across every machine to ±2 s (mission-clock expiry relaunches
|
|
||||||
all peers' menus in the same second). **Stub anatomy:** the steam lobby is CLICK-driven
|
|
||||||
(`steamAction`, btl4fe.cpp), so a menu closed without a click logs exactly **4 boot lines** and
|
|
||||||
exits 0 — a 4-line stub at end-of-night is a player going to bed, NOT a crash (all four machines
|
|
||||||
ended night-14 with identical 4-line stubs at 00:31:2x). 7–8-line stubs = a lobby cycle
|
|
||||||
(`[steamnet] up` + `[lobby] join` + map). `BTCrashFilter` is armed before the first log line and
|
|
||||||
writes a `[crash]` block + module-relative stack on ANY unhandled exception — a silent log end
|
|
||||||
means quit/kill/hang, never an unlogged AV. **`lastrun_<stem>.txt` caveats:** appends stop silently
|
|
||||||
if the file gets locked (the `is_open` guard skips) — a lastrun ending mid-night while the log
|
|
||||||
continues means the file was captured/locked during live troubleshooting, not that the chain died.
|
|
||||||
And the bat sign-off ("The game has exited. If it closed unexpectedly, send…") prints after ANY
|
|
||||||
chain end including a normal quit — it primes players to report quits as crashes; #157 ("did not
|
|
||||||
exit cleanly") was exactly this echo. The arcade-authentic round-end (mission process exits →
|
|
||||||
menu relaunch) itself reads as a crash to first-night players.
|
|
||||||
- **Why this is a BT411-only hazard [T1].** The 1995 pod shipped ONE folder — `BTL4OPT.EXE` sits
|
- **Why this is a BT411-only hazard [T1].** The 1995 pod shipped ONE folder — `BTL4OPT.EXE` sits
|
||||||
next to `BTL4.RES`/`VIDEO\`/`GAUGE\`/`AUDIO\` (it is still there in `content\`) — and RP411/RP412
|
next to `BTL4.RES`/`VIDEO\`/`GAUGE\`/`AUDIO\` (it is still there in `content\`) — and RP411/RP412
|
||||||
keep that shape (`pack-dist.ps1` copies the exe and every asset dir into one dist root). BT411's
|
keep that shape (`pack-dist.ps1` copies the exe and every asset dir into one dist root). BT411's
|
||||||
|
|||||||
@@ -606,21 +606,10 @@ btplayer.hpp, static_assert-locked; `scoreAward`=APPLIED tally in all three):
|
|||||||
the VICTIM role's `killBonus` (role+0x1c). senderMechID = the victim. Suicide (eject charge,
|
the VICTIM role's `killBonus` (role+0x1c). senderMechID = the victim. Suicide (eject charge,
|
||||||
self-damage) IS dispatched — the handler negates the award (@0x4c03ab `fchs`) and skips
|
self-damage) IS dispatched — the handler negates the award (@0x4c03ab `fchs`) and skips
|
||||||
`killCount++`: **the #134 panic penalty, live** (bench: `type=2 award=-39.00 kills=0`).
|
`killCount++`: **the #134 panic penalty, live** (bench: `type=2 award=-39.00 kills=0`).
|
||||||
- **B @0x4a05d9, not newly killed && tally≠0** → type 0 to the shooter: the **per-hit INFLICTED
|
- **B @0x4a05d9, not newly killed && tally≠0** → type 0 to the shooter. The ONLY registered 0x16
|
||||||
credit**, and it is LIVE. ⚠ **CORRECTED 2026-08-07** — this entry previously read "the ONLY
|
receiver Verify-rejects type 0 → 1995 folded an UNINITIALIZED stack float into the shooter's
|
||||||
registered 0x16 receiver Verify-rejects type 0 → 1995 folded an UNINITIALIZED stack float into
|
score on every non-lethal hit (real 1995 bug — @0x4c0200, the handler that accepts type 0, is
|
||||||
the shooter's score on every non-lethal hit (real 1995 bug — @0x4c0200 … is in NO table entry:
|
in NO table entry: dead code). Port sends it for wire fidelity, banks award 0.
|
||||||
dead code)". **That was wrong**, and build 787 retired the port's per-hit crediting on the
|
|
||||||
strength of it (the scoring regression players reported on 4.11.817). `BTPlayer` overrides
|
|
||||||
**`Dispatch` — vtable `@00513300` slot 3 = `FUN_004bffa0`** — and splits type 0 off BEFORE base
|
|
||||||
dispatch: `if (id==0x16 && type==0) FUN_004c0200(...); else base;`. `@004c0200` names itself in
|
|
||||||
its own Verify string (`BTPlayer::ScoreInflictedMessageHandler`) and computes
|
|
||||||
`CalcInflicted(basis) → negate if target==self → × (targetTonnage/ownTonnage) → += +0x278`.
|
|
||||||
`ScoreMessageHandler`'s type-0 arm Verify-rejects precisely BECAUSE the interceptor guarantees
|
|
||||||
type 0 never reaches it. Independently corroborated by the original manual's SCORING CHART
|
|
||||||
(`reference/manual/scoring_chart.webp`, via Lynx): "+1 each damage point scored on opponent's armor" and "-1 each
|
|
||||||
self-inflicted point of armor damage" — the negate-if-self arm exactly. Interceptor restored;
|
|
||||||
benched 83 inflicted rows, awards 0.98–25.00, zero type-0 Verify rejections. [T1]
|
|
||||||
- **C @0x4a06c0, tally>0 (kills included)** → type 1 DamageReceivedScore to the VICTIM's player.
|
- **C @0x4a06c0, tally>0 (kills included)** → type 1 DamageReceivedScore to the VICTIM's player.
|
||||||
Basis = INTENDED damage (burstCount×amount). senderMechID = the INFLICTOR. Feeds the received
|
Basis = INTENDED damage (burstCount×amount). senderMechID = the INFLICTOR. Feeds the received
|
||||||
penalty (`CalcDamageReceivedScore` returns the NEGATIVE) + the operator-console VTVDamaged line
|
penalty (`CalcDamageReceivedScore` returns the NEGATIVE) + the operator-console VTVDamaged line
|
||||||
@@ -628,61 +617,9 @@ btplayer.hpp, static_assert-locked; `scoreAward`=APPLIED tally in all three):
|
|||||||
Reports carry the LOOP-ENTRY zone (msg+0x24, never rewritten mid-loop), the vital-wreck flag, and
|
Reports carry the LOOP-ENTRY zone (msg+0x24, never rewritten mid-loop), the vital-wreck flag, and
|
||||||
`inflictingSubsystemID` (msg+0x5c, engine T0 name).
|
`inflictingSubsystemID` (msg+0x5c, engine T0 name).
|
||||||
|
|
||||||
**Score model consequence** [T1, ⚠ REWRITTEN 2026-08-07]: 1995 pod scoring = **per-hit inflicted
|
**Score model consequence** [T1]: 1995 pod scoring = **kill awards + received-damage penalties +
|
||||||
credit + kill awards + received-damage penalties + death costs**. The previous text here claimed
|
death costs. No per-hit inflicted credit** (the port's old per-hit crediting — and the #95 salvo
|
||||||
"No per-hit inflicted credit … inventions riding the dead @0x4c0200 channel; both retired" — that
|
fix on top of it — were inventions riding the dead @0x4c0200 channel; both retired).
|
||||||
followed from the dead-code misreading corrected in report B above, and retiring the credit IS the
|
|
||||||
scoring regression reported on 4.11.817. The **original manual's SCORING CHART** (`reference/manual/scoring_chart.webp`)
|
|
||||||
is the cross-check for every row and should be consulted before touching this path again:
|
|
||||||
|
|
||||||
| Points | Event |
|
|
||||||
|---|---|
|
|
||||||
| +1000 | Starting the game |
|
|
||||||
| +1 | Each damage point scored on opponent's armor |
|
|
||||||
| +10..+30 | Destroying an opponent's internal system |
|
|
||||||
| +500 | Destroying an opponent's 'Mech |
|
|
||||||
| −1 | Each self-inflicted point of armor damage |
|
|
||||||
| −10..−30 | Knocking out one of your own internal systems |
|
|
||||||
| −500 | Destroying your own 'Mech by an ammo explosion |
|
|
||||||
| −1000 | Destroying your own 'Mech by ejecting |
|
|
||||||
|
|
||||||
⚠ **THE SCORE AUTHORITY IS THE OPERATOR CONSOLE, not the player object** [T1, 2026-08-07]. The
|
|
||||||
binary sends `ConsolePlayerVTVScoreUpdate(ownerID, currentScore)` every `CONSOLE_UPDATE_INTERVAL`
|
|
||||||
and then does `param_1[0x9e] = 0` — **ungated**. So `+0x278` is a *console DELTA*, never a running
|
|
||||||
total, and it does not matter which NODE computed a delta: every node's contribution is flushed
|
|
||||||
stamped with the scoring player's `ownerID` and the console accumulates. This is almost certainly
|
|
||||||
where the chart's **+1000 starting the game** was seeded, which is why no game-side code grants it.
|
|
||||||
|
|
||||||
**Consequence for the port** (no console as score authority): `GetScore()` (SCORE gauge),
|
|
||||||
`Player::CalcRanking()` and the replicated `Player__UpdateRecord` all read `+0x278` **on the owning
|
|
||||||
node**. Damage is applied on the VICTIM's node, so block B dispatches the inflicted report to the
|
|
||||||
SHOOTER's player object there — a **REPLICANT** — and the credit is banked on the wrong machine,
|
|
||||||
where the master's next update record overwrites it (benched: totals climb to ~35, snap back every
|
|
||||||
few seconds = the field "scoring is screwy"). ⚠ OPEN. **Tried and rejected:** gating the type-0
|
|
||||||
interception to `MasterInstance` so a replicant reroutes — the message arrives but the BT extension
|
|
||||||
fields (`damageAmount`@+0x24, `senderMechID`@+0x34) do NOT survive the wire, only the base
|
|
||||||
`scoreAward`, so every award computes 0.00. That is also WHY the kill report (type 2) already
|
|
||||||
credits cross-node correctly: its value rides `scoreAward`. **Fix shape:** compute the award on the
|
|
||||||
victim's node (where the damage data is) and ship the RESULT in `scoreAward`, as the kill report
|
|
||||||
does — do not ship the basis and recompute where it cannot be seen.
|
|
||||||
|
|
||||||
**The damage-bias term is VESTIGIAL — do not "finish" it** [T1, audit 2026-08-08]. The kill/inflicted
|
|
||||||
formula's `(victimAvgZoneDamage@0x354 × damageBias + 1.0)` factor is **always 1.0** in the shipped
|
|
||||||
binary. `mech+0x354` has exactly one writer — `Mech::Reset` (@0049fb74, part_012.c:14340), which
|
|
||||||
computes `mean(zone+0x158)` across every damage zone *after* the zone heal has zeroed those cells —
|
|
||||||
and exactly one reader, `CalcInflictedScore` (@004c052c). Nothing recomputes it during play, so it
|
|
||||||
holds ~0 for the mech's whole life. `0x358`/`0x35c` are the same computation over subsystem zones
|
|
||||||
and have **no reader at all**. The port's `MECH_DAMAGE_BIAS(m) → 0.0f` therefore reproduces the
|
|
||||||
binary exactly; wiring it to live damage would look like completing a stub and would silently
|
|
||||||
inflate every award (the chart-verified +1/point and +500/kill both assume 1.0).
|
|
||||||
|
|
||||||
⚠ **Three chart rows are NOT yet reconciled with the reconstruction** — treat as open [T4]:
|
|
||||||
(a) a kill benches at `award=4.88`, two orders off the chart's flat **+500**; (b) **+1000 at
|
|
||||||
game start** has no known implementation; (c) **−1000 eject / −500 ammo** would live in
|
|
||||||
`ScenarioRole::specialCaseDeathPenalty` (role+0x20), which the port reads
|
|
||||||
(`GetSpecialCaseDeathPenalty`, the death-cost block) but which **shipped content authors
|
|
||||||
nowhere**, so it is 0 in the field — the #134 symptom by another route. Do not "fix" these by
|
|
||||||
hard-coding chart numbers; find where the binary sources them.
|
|
||||||
Kill award = `(victimKillBonus + tally) × killerRole.damageInflictedModifier ×
|
Kill award = `(victimKillBonus + tally) × killerRole.damageInflictedModifier ×
|
||||||
(victimAvgZoneDamage@0x354 × damageBias + 1.0) × (victimTonnage/killerTonnage)`; same-team kill in
|
(victimAvgZoneDamage@0x354 × damageBias + 1.0) × (victimTonnage/killerTonnage)`; same-team kill in
|
||||||
a non-FFA game = `-friendlyFirePenalty` basis (inline strcmp of `teamName@0x20c`, gate
|
a non-FFA game = `-friendlyFirePenalty` basis (inline strcmp of `teamName@0x20c`, gate
|
||||||
|
|||||||
@@ -430,31 +430,6 @@ From the weapon `.SUB` records + the charge-curve `.data` constants (PE-parsed a
|
|||||||
recomputes output via `(1 − damage) × rated` and zeroes it. stateAlarm 4's
|
recomputes output via `(1 − damage) × rated` and zeroes it. stateAlarm 4's
|
||||||
producer = the THERMAL BREAKER in GeneratorSimulation itself; a 2026-08-03
|
producer = the THERMAL BREAKER in GeneratorSimulation itself; a 2026-08-03
|
||||||
destruction→state-4 bridge was removed as unfounded.
|
destruction→state-4 bridge was removed as unfounded.
|
||||||
- **THE CONDITION FLAGS ARE ROUTINE, NOT ALARMS — field-verified 2026-08-08 [T2]:** a
|
|
||||||
`[techstat] <sub> condition <n> SET` is an *operating* flag ("this component is above
|
|
||||||
nominal / browned out RIGHT NOW"), not a fault report. Census over a full Steam match
|
|
||||||
(`steam_20260806_c_michael_XIAOLONG.log`): cond 3 **Overheating** fires 33× on LLaser_2,
|
|
||||||
31× LLaser_1, 26× SRM4, 18× PPC_2 — every weapon trips it on each volley and clears it on
|
|
||||||
cooldown; cond 6 **BadPower** fires 8× on Myomers, 3× each on SLaser_1/PPC_1/MLaser_1 as
|
|
||||||
the bus browns out under simultaneous draw. **Every subsystem's SET/CLEARED counts are
|
|
||||||
balanced** (GeneratorD 5/4, GeneratorA 8/7, Myomers 5/4, Condenser5 1/1 — the odd extra SET
|
|
||||||
is only the log ending mid-heat). Nothing latches. **Never read a post-respawn `condition 3
|
|
||||||
SET` as a reset failure** — that inference cost a full two-sided reset audit (#137 below).
|
|
||||||
Diagnose a heat complaint from the SET/CLEARED *balance* and the throttle state, not from
|
|
||||||
the presence of a SET.
|
|
||||||
- **#137 "respawn came back with MYOMERS heat MAXED" = NOT A BUG, closed 2026-08-08 [T2]:**
|
|
||||||
`Mech::Reset` is clean on both sides — bench: every roster subsystem incl. all six
|
|
||||||
Condensers at `T=77 start=77`; field log: every live condition CLEARs 12–16 lines after each
|
|
||||||
reset. The heat is real and immediate because **the mech respawns still under power**: at the
|
|
||||||
reset the log reads `thr=1`, `cycleSpeed=14.6`, gait state 12 (running), and the myomers →
|
|
||||||
Condenser5 → GeneratorD trip Overheating within ~1–2 s. `Mech::Reset`'s subsystem loop starts
|
|
||||||
at **index 2** and the ControlsMapper is **index 0**, so the throttle is never reset — and the
|
|
||||||
binary does the same, which is correct for a pod whose throttle is a PHYSICAL lever still
|
|
||||||
under the pilot's hand. **Desktop caveat (real, and separate):** the glass bridge emulates
|
|
||||||
that lever with a file-static ramp accumulator `sLever` (mech4.cpp:3250), zeroed ONLY by the
|
|
||||||
X all-stop and a direction-crossing snap — so a pad/keyboard pilot, who is physically holding
|
|
||||||
nothing, likewise respawns at speed with the lever state invisible. Port-layer question, not
|
|
||||||
a heat bug.
|
|
||||||
- **DEATH SCORE COST decoded 2026-08-02 (#118 tail) [T1]:** the death handler tail
|
- **DEATH SCORE COST decoded 2026-08-02 (#118 tail) [T1]:** the death handler tail
|
||||||
`@004c07cd-0x4c0828` (inside the @004c05c4 export gap — missing from the #52
|
`@004c07cd-0x4c0828` (inside the @004c05c4 export gap — missing from the #52
|
||||||
reconstruction) gates on `advancedDamageOn`(+0x264) and hands the ENGINE base
|
reconstruction) gates on `advancedDamageOn`(+0x264) and hands the ENGINE base
|
||||||
@@ -464,17 +439,7 @@ From the weapon `.SUB` records + the charge-curve `.data` constants (PE-parsed a
|
|||||||
Direct call, NOT dispatched — bypasses the BT handlers' type Verifies. NUANCE [T4]:
|
Direct call, NOT dispatched — bypasses the BT handlers' type Verifies. NUANCE [T4]:
|
||||||
the 1995 engine adds at Player+0x1c8 while every BT scoreboard reads +0x278 — the
|
the 1995 engine adds at Player+0x1c8 while every BT scoreboard reads +0x278 — the
|
||||||
pod's death cost may never have displayed; our single-cell port shows it. Shipped
|
pod's death cost may never have displayed; our single-cell port shows it. Shipped
|
||||||
content authors NO role keys, so the cost is 0 in the field. **CORRECTED 2026-08-07 —
|
content authors NO role keys, so the cost is 0 in the field. Other role+0x1c reader:
|
||||||
that sentence was wrong on both halves.** The role's scoring fields are not authored via
|
|
||||||
notation keys at all: the ctor `@00429a9c` loads them from the role MODEL's GameModel
|
|
||||||
record (type 0xf, 7 dwords — rec[0]=killBonus, rec[1]=specialCaseDeathPenalty,
|
|
||||||
rec[2]=dmgRcvd, rec[3]=dmgInflctd, rec[4]=bias, rec[5]=friendlyFire,
|
|
||||||
rec[6]=returnFromDeath); the notation keys are optional OVERRIDES. Shipped content
|
|
||||||
authors `Role::Default` (model `dfltrole`) with **killBonus=500, deathPenalty=500,
|
|
||||||
dmgInf=1, dmgRcv=0, bias=1, ff=1, return=1000** — the original manual's scoring chart
|
|
||||||
verbatim (`reference/manual/scoring_chart.webp`). The cost read 0 only because
|
|
||||||
`BTPlayer::scenarioRole` was never BOUND (the registry lookup sat commented out); wired
|
|
||||||
2026-08-07 and the cost is verified APPLYING at 500. Other role+0x1c reader:
|
|
||||||
`@0x4a0506` inside the deferred id-0x16 report tail (#45) reads killBonus.
|
`@0x4a0506` inside the deferred id-0x16 report tail (#45) reads killBonus.
|
||||||
- **THE SIM TIME MODEL — CLOSED 2026-08-02 (issue #96) [T1]:** the arcade
|
- **THE SIM TIME MODEL — CLOSED 2026-08-02 (issue #96) [T1]:** the arcade
|
||||||
`Simulation::PerformAndWatch` is **`FUN_0041c018`** (part_002.c:5101):
|
`Simulation::PerformAndWatch` is **`FUN_0041c018`** (part_002.c:5101):
|
||||||
|
|||||||
+16
-40
@@ -414,14 +414,6 @@ Verified live: bay fire → lamp 0xD (the LRM's select button) flashes 0x37 + en
|
|||||||
on detonation/purge. Diagnostics: `BT_LAMP_LOG` → `[techstat]`/`[galarm]`/`[lamp]`. Details +
|
on detonation/purge. Diagnostics: `BT_LAMP_LOG` → `[techstat]`/`[galarm]`/`[lamp]`. Details +
|
||||||
the four load-bearing fixes en route: [[open-questions]] + [[decomp-reference]] §GaugeAlarm.
|
the four load-bearing fixes en route: [[open-questions]] + [[decomp-reference]] §GaugeAlarm.
|
||||||
|
|
||||||
**These conditions are ROUTINE and SELF-CLEARING [T2, field-verified 2026-08-08].** A `SET` is an
|
|
||||||
operating state, not a fault: every laser volley trips Overheating (cond 3) and clears it on
|
|
||||||
cooldown (33× on one LLaser in a single match), and BadPower (cond 6) flickers whenever
|
|
||||||
simultaneous draw browns the bus. Across a full match every subsystem's SET/CLEARED counts are
|
|
||||||
balanced — nothing latches. So a lamp flashing after a respawn is the mech *operating*, not a
|
|
||||||
failed reset; diagnose from the SET/CLEARED balance, never from a lone SET. Full census + the
|
|
||||||
#137 post-mortem it settled: [[decomp-reference]] §TechStatus.
|
|
||||||
|
|
||||||
## ConfigMapGauge (the weapon panel's trigger-config joystick) — LIVE via LinkToEntity (2026-07-21)
|
## ConfigMapGauge (the weapon panel's trigger-config joystick) — LIVE via LinkToEntity (2026-07-21)
|
||||||
The per-weapon btjoy.pcc joystick image + 4 cm_* state lamps (off/other/only/both) showing,
|
The per-weapon btjoy.pcc joystick image + 4 cm_* state lamps (off/other/only/both) showing,
|
||||||
for each mappable fire button (Pinky/ThumbLow/Trigger/ThumbHigh), whether THIS panel's weapon
|
for each mappable fire button (Pinky/ThumbLow/Trigger/ThumbHigh), whether THIS panel's weapon
|
||||||
@@ -557,38 +549,6 @@ and every instrument is now live [T2]:**
|
|||||||
transcription color bug caught by a period reference screenshot, 2026-07-09; same for the
|
transcription color bug caught by a period reference screenshot, 2026-07-09; same for the
|
||||||
bottom bowtie carets @4569-4570); pegs at 1200 with no target; the DISPLAYED range slides at
|
bottom bowtie carets @4569-4570); pegs at 1200 with no target; the DISPLAYED range slides at
|
||||||
**500 m/s** toward the true pick range (HudSimulation :5652 [T1]).
|
**500 m/s** toward the true pick range (HudSimulation :5652 [T1]).
|
||||||
**The HudSimulation tuning constants, read off .rdata 2026-08-08 [T1]** (`section_dump.txt`
|
|
||||||
rows ` 4b7ec0 8be55dc3 0000403f 0000803f 0000c842` / ` 4b7ed0 00000000`) —
|
|
||||||
`_DAT_004b7ec4` = **0.75f**, `_DAT_004b7ec8` = **1.0f**, `_DAT_004b7ecc` = **100.0f**,
|
|
||||||
`_DAT_004b7ed0` = **0.0f**, `_DAT_004b7f90` = **0.0f**. ec4/ec8 are the fire-control **LOCK**
|
|
||||||
limits (own HUD host zone < 0.75 damage, targeted zone < 1.0 — so a shot-up cockpit drops to
|
|
||||||
"target held, no lock", and a dead zone can't be re-locked); ed0 is the shared **zero** in the
|
|
||||||
range-slide `Abs()` idiom (the 500 is an immediate `0x43fa0000`, NOT a global). `hud.cpp` had
|
|
||||||
carried all five as 0.0f/500.0f stand-ins under guessed names ("SegmentTempLimit … heat
|
|
||||||
threshold for HUD page visibility" was neither heat nor page visibility) — corrected in place,
|
|
||||||
with the live lock/slide implementation staying in mech4.cpp's targeting step, which had both
|
|
||||||
thresholds right all along.
|
|
||||||
**THE CARET CAN DIE FOR THE WHOLE SESSION — #147, fixed 2026-08-08 [T2 code, T3 field link]:**
|
|
||||||
`sShownRange` (mech4's targeting step) is a **function-level static** — one cell per process,
|
|
||||||
shared by every mech, carried across drops. **NaN is absorbing** in `step = trueRange −
|
|
||||||
sShownRange; sShownRange += step`, and neither the producer's clamps nor
|
|
||||||
`BTReticleRenderable::Draw`'s (`range < minRange` / `range > maxRange`) catch it — both
|
|
||||||
comparisons are **false for NaN**. A poisoned value therefore reaches `AddPoint`/`ConcatMatrix`,
|
|
||||||
and the caret + its bar become **degenerate geometry that stops rendering while the static tick
|
|
||||||
marks keep drawing** — the exact reported symptom ("no range finder on this drop", ticks there,
|
|
||||||
caret gone), sticky until relaunch. Fixed four ways: re-seed on mech change (not on respawn —
|
|
||||||
that reuses the entity, and the binary doesn't reset the readout either), a producer NaN trap, a
|
|
||||||
NaN-safe consumer clamp falling back to the 1200 peg, and — the reason no log could settle it —
|
|
||||||
**`BT_RANGE_LOG` now prints the caret's actual input** (`[range] caret input shown=`). It never
|
|
||||||
did before: `BT_RANGE_LOG` instrumented the PICK (#4) and `[target]`'s `range=` is a *separate*
|
|
||||||
locally-recomputed Sqrt in the weapon-range check, so grepping field logs for NaN found nothing
|
|
||||||
because the poisoned variable was never printed. Causal link to the field report is INFERENCE.
|
|
||||||
**GAP — the 100 m RANGE BIAS is NOT reconstructed [T1 read, unimplemented]:** HudSimulation
|
|
||||||
subtracts `_DAT_004b7ecc` (100.0f) from `RangeToTarget@0x1EC` **every frame while the timed
|
|
||||||
flag @0x22C is set**, accumulating @0x21C by `time_slice` until it reaches @0x1D8, then clearing
|
|
||||||
both. The port's targeting step (mech4.cpp) does the slide but never the bias, so whatever
|
|
||||||
in-game state sets @0x22C currently produces no range offset. Trigger for @0x22C not yet
|
|
||||||
identified — see [[open-questions]].
|
|
||||||
**VERDICT (Gitea #4, 2026-07-20): the "range slides in/out crazily while walking" report is
|
**VERDICT (Gitea #4, 2026-07-20): the "range slides in/out crazily while walking" report is
|
||||||
AUTHENTIC behavior, not a bug [T2 measured].** Per-frame `BT_RANGE_LOG` traces (mech4.cpp, with
|
AUTHENTIC behavior, not a bug [T2 measured].** Per-frame `BT_RANGE_LOG` traces (mech4.cpp, with
|
||||||
an independent Möller-Trumbore cross-check `BTGroundRayHitExact` in btvisgnd.cpp) on scripted
|
an independent Möller-Trumbore cross-check `BTGroundRayHitExact` in btvisgnd.cpp) on scripted
|
||||||
@@ -823,6 +783,22 @@ pattern across entries [2^bits..255] (high-index art degrades to its
|
|||||||
index-mod-2^bits colour IN-PLANE, can never leak). The 1995 binary ships the
|
index-mod-2^bits colour IN-PLANE, can never leak). The 1995 binary ships the
|
||||||
SAME 64-entry fill and relied on 6-bit art discipline -- garbage is not a
|
SAME 64-entry fill and relied on 6-bit art discipline -- garbage is not a
|
||||||
preservable behaviour, so the cycle-fill is a guarded PORT deviation.
|
preservable behaviour, so the cycle-fill is a guarded PORT deviation.
|
||||||
|
|
||||||
|
**REFINED 2026-08-10 [T1] -- the cycle-fill itself made phantoms.** The
|
||||||
|
in-plane wrap mapped art index 254 onto plane slot 62 = the LIVE
|
||||||
|
`colorMapperMultiArmor` right-armor damage slot, and idx-254 art EXISTS:
|
||||||
|
every SMODE.PCC frame fills the INACTIVE control-mode box interiors with 254
|
||||||
|
(active border/text idx 9 yellow, inactive borders idx 5 orange-red), and
|
||||||
|
BTSEC1.PCX carries a stray 52x13 idx-254 bar at port (199-250, 101-113) --
|
||||||
|
between the heading dial and the armor rosette, geometrically a scratch
|
||||||
|
duplicate of the rosette quadrant bars. Both lit up in the current
|
||||||
|
right-armor colour (adpal ramp green/orange/red) on EVERY render path --
|
||||||
|
playtester-reported as "red boxes around MID/ADV" + "a block between Armor
|
||||||
|
and Heading". On the shipped machine those regions rendered BLACK (the
|
||||||
|
garbage entry's low plane bits were 0 -> in-plane index 0), so the fill now
|
||||||
|
maps [2^bits..255] to `translationTable[0]` (plane BACKGROUND) -- authentic
|
||||||
|
on-screen result, same no-leak guarantee. NB the 2026-07-19 audit verified
|
||||||
|
smode BEFORE the 07-25 cycle-fill landed, which is why the row said CORRECT.
|
||||||
Post-fix: **0 leaks over 60s** on the same probe; sim3 3-pod regression clean.
|
Post-fix: **0 leaks over 60s** on the same probe; sim3 3-pod regression clean.
|
||||||
**The tripwires are DEFAULT-ON in every build** (BT_PLANE_AUDIT=0 opts out): the
|
**The tripwires are DEFAULT-ON in every build** (BT_PLANE_AUDIT=0 opts out): the
|
||||||
plane-leak trap, plus an out-of-bounds draw-start trap in `buildDestPointer`
|
plane-leak trap, plus an out-of-bounds draw-start trap in `buildDestPointer`
|
||||||
|
|||||||
@@ -203,6 +203,31 @@ appears in the cfg like any panel and honours `,noframe`. Verified: a drag wrote
|
|||||||
at 321,222. NB the plasma window blits directly every frame (`GetDC`+`StretchDIBits`), so unlike
|
at 321,222. NB the plasma window blits directly every frame (`GetDC`+`StretchDIBits`), so unlike
|
||||||
the panels it has no `WM_TIMER` focus-throttle to worry about.
|
the panels it has no `WM_TIMER` focus-throttle to worry about.
|
||||||
|
|
||||||
|
## Glass-panel repaint pump — perf + dirty-skip (2026-08-09/10) [T2 measured]
|
||||||
|
|
||||||
|
The exploded per-display windows are pure CPU/GDI (`ExpandPlaneToBGRA` +
|
||||||
|
`StretchDIBits`), repainted synchronously on the MAIN render thread by
|
||||||
|
`BTGlassPanels_Tick` (~16 Hz). Three layered changes (branch `glass-panel-perf`)
|
||||||
|
after a playtester reported ~20 fps in panels mode vs ~130 fps in the surround:
|
||||||
|
|
||||||
|
1. **HALFTONE → COLORONCOLOR** (`f3d27f5`): the `HALFTONE` stretch (GDI's
|
||||||
|
per-output-pixel resample) × 7 windows per pump was the primary sink; nearest
|
||||||
|
is crisper for the low-res MFD pixels anyway. `BT_GLASS_SMOOTH=1` restores.
|
||||||
|
2. **Per-window dirty-skip** (`c9e25e5`): each window carries a change token =
|
||||||
|
FNV over `SVGA16::PlaneChecksum(mask)` (the shared pixelBuffer masked to every
|
||||||
|
port that can feed the window) + each button's RENDERED lamp brightness +
|
||||||
|
held/latch. The pump re-blits only windows whose token moved: ~31 pumps/2s →
|
||||||
|
4-15 window repaints vs 217-224 always-on. `BT_GLASS_DIRTY=1` logs the tally.
|
||||||
|
3. **Palette generation in the token** (2026-08-10): the ColorMapper family
|
||||||
|
(armor rosette tints, adpal/adpal2 damage flash) animates by CLUT writes with
|
||||||
|
ZERO pixel churn — invisible to a pixel checksum. `SVGA16::paletteGeneration`
|
||||||
|
is bumped by the palette writers (`BuildSecondaryColor` only on a REAL entry
|
||||||
|
change, since the flash alternates palettes every Execute writing identical
|
||||||
|
RGB at zero damage; full rebuilds unconditionally) and folded into the token
|
||||||
|
for palette-expanding windows (`monoTint < 0`, or all under `BT_GLASS_MFD_PAL`).
|
||||||
|
Without it the glass radar held stale armor tints between pixel repaints while
|
||||||
|
the D3D surround (which re-expands every frame) tracked live.
|
||||||
|
|
||||||
**Turning it OFF: `L4PLASMA=NONE` (also `OFF`/`0`, 2026-08-06) [T2].** `L4GREND` creates a
|
**Turning it OFF: `L4PLASMA=NONE` (also `OFF`/`0`, 2026-08-06) [T2].** `L4GREND` creates a
|
||||||
marquee whenever `L4PLASMA` is set at all (`SCREEN` → the desktop window, anything else → a real
|
marquee whenever `L4PLASMA` is set at all (`SCREEN` → the desktop window, anything else → a real
|
||||||
`PlasmaDisplay` on that serial port), and the GLASS profile force-defaults it to `SCREEN` — so on
|
`PlasmaDisplay` on that serial port), and the GLASS profile force-defaults it to `SCREEN` — so on
|
||||||
|
|||||||
@@ -273,43 +273,6 @@ number the peer Standing case walks on). Verified: arena circle replicant 218×s
|
|||||||
replicant, every `MechControlsMapper` demand cell except those explicitly re-derived
|
replicant, every `MechControlsMapper` demand cell except those explicitly re-derived
|
||||||
(`turnDemand`) is DEAD — any peer-side state machine judging a local mapper read is judging 0.**
|
(`turnDemand`) is DEAD — any peer-side state machine judging a local mapper read is judging 0.**
|
||||||
|
|
||||||
**The #52 SEQUEL: the peer body-channel STANDING-LOCK (root-caused + fixed 2026-08-07) [T2].**
|
|
||||||
Fixing (2) above closed an ACCIDENTAL escape hatch and the skate came back in a new shape. The
|
|
||||||
port's body case 4 is an INSERTION (the task-#64 lockstep twin) sitting between case 0 and the
|
|
||||||
advance group — but in the binary `case 4` is a **member of that advance group**
|
|
||||||
(`FUN_004a5678` @004a5678: `case 2,3,`**`4`**`,5,8,…`, no turn block, no speed exit) [T1], so
|
|
||||||
case 0's fallthrough is supposed to land on `Advance()`. The insertion intercepted it. On a
|
|
||||||
REPLICANT that is fatal and not a race: case 0 arms walk iff `standSpeed < bodyTargetSpeed`, and
|
|
||||||
the inserted block's exit tests `standSpeed < bspd` where `bspd` **IS** `bodyTargetSpeed` on a
|
|
||||||
replicant — the *same expression*. Arm and reset therefore fire on the same frame, every frame,
|
|
||||||
and a peer parked at Standing with a live replicated demand can never start cycling (reverse
|
|
||||||
likewise: both sides test `< ZeroSpeed`). Before e91d447 the replicant branch read the dead mapper
|
|
||||||
cell (0 forever) so the exit never fired and the fallthrough worked by accident. **Fix:** case 0
|
|
||||||
`goto advance_body_normally` — the leg twin's own idiom (`goto advance_normally`, mech2.cpp) —
|
|
||||||
restoring the binary's structure without touching the #64/#82 turn logic. `BT_NO_BODY_FALLTHRU=1`
|
|
||||||
reverts. Measured: legacy 336 consecutive locked seconds with `bspd=39.2324 bts=39.2324` identical
|
|
||||||
on every line; fixed 0 locks across every pass; the MASTER's body-Standing samples also fell 52→21
|
|
||||||
(it was locking too, invisibly — mj=0 writes no joints, and its two tests read *different* cells so
|
|
||||||
it only stalls in the window where they disagree). Turn-in-place re-verified under the fix (pivoter
|
|
||||||
reached body state 4 ×9 / leg state 4 ×8 — armed in lockstep).
|
|
||||||
|
|
||||||
**Why a peer must be able to self-arm walking at all** (the load-bearing bit behind
|
|
||||||
`mech4.cpp` "stand; case 0 walk-begins next tick"): the peer's body state is set directly from
|
|
||||||
`record->legState` only on **type-3 edges** (`ReadUpdateRecord`), and entering Standing emits one
|
|
||||||
while *leaving* it does not. So between gait-change records a replicant is REQUIRED to derive
|
|
||||||
walking itself from the replicated `bodyTargetSpeed`. That is why the lock needs a mech holding a
|
|
||||||
*steady* demand — a mech whose gait keeps changing keeps getting rescued by records, which is why
|
|
||||||
free-walking and wall-jammed benches each reproduce only half the symptom. [T2]
|
|
||||||
|
|
||||||
⚠ **The field symptom link is [T3], not T2.** The Standing-lock is proven and proven removed; that
|
|
||||||
it accounts for the night-13 episodes is inference (a locked peer has `bodyCycleSpeed==0` and never
|
|
||||||
advances its clip, so locked + translating *is* the `[skate]` signature by construction) — but no
|
|
||||||
bench caught the two together. The `[skate]` line now carries `bstate=`, so the next playtest
|
|
||||||
settles it: episodes gone → confirmed; any survivor names its own state. **NB the night-12
|
|
||||||
`skatebench` "reproductions" were a DETECTOR ARTIFACT** — the first detector build tested only
|
|
||||||
`legCycleSpeed==0`, which is normal on a peer (the body channel poses it), so it fired on every
|
|
||||||
healthy movement phase. Old-format lines (`legCycleSpeed=`, no `bodyCyc=`) are not evidence.
|
|
||||||
|
|
||||||
## Controls (`BT_REAL_CONTROLS`, default-on)
|
## Controls (`BT_REAL_CONTROLS`, default-on)
|
||||||
`MechControlsMapper` (mechmppr.cpp @004afbe0; btl4mppr.cpp mappers) interprets input → `speedDemand`
|
`MechControlsMapper` (mechmppr.cpp @004afbe0; btl4mppr.cpp mappers) interprets input → `speedDemand`
|
||||||
/ `turnDemand`. ⚠ **WndProc NEVER receives WM_KEYUP** (the engine's per-frame reader `GetMessage`s
|
/ `turnDemand`. ⚠ **WndProc NEVER receives WM_KEYUP** (the engine's per-frame reader `GetMessage`s
|
||||||
|
|||||||
@@ -954,18 +954,6 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
|
|||||||
deterministically leave ~0 at [ebp-0xc]; or replicant-player reroute delivery differs). Port
|
deterministically leave ~0 at [ebp-0xc]; or replicant-player reroute delivery differs). Port
|
||||||
deviates safely (award=0). Curiosity, not a blocker.
|
deviates safely (award=0). Curiosity, not a blocker.
|
||||||
|
|
||||||
## HUD range-bias @0x22C — read but NOT reconstructed (2026-08-08) [T1 read, unimplemented]
|
|
||||||
`HudSimulation` subtracts `_DAT_004b7ecc` = **100.0f** from `RangeToTarget@0x1EC` on **every frame
|
|
||||||
that the flag @0x22C is non-zero**, while accumulating a timer @0x21C by `time_slice` until it
|
|
||||||
reaches the limit @0x1D8, at which point BOTH the timer and the flag are cleared. So the authentic
|
|
||||||
HUD has a timed **−100 m range offset** state that our targeting step (mech4.cpp — which does
|
|
||||||
reconstruct the 500 m/s slide correctly) never applies.
|
|
||||||
**Open:** what SETS @0x22C. Candidates not yet checked — a targeting-computer damage/jam effect, an
|
|
||||||
ECM/spoof, or a weapon-lock transient. Found while correcting hud.cpp's stand-in constant block
|
|
||||||
(the values are now byte-grounded: ec4 0.75 / ec8 1.0 / ecc 100.0 / ed0 0.0 / f90 0.0). Details:
|
|
||||||
[[gauges-hud]] §Right ladder. Worth resolving before trusting any field report about the range
|
|
||||||
readout being wrong — it is a real behavioural gap, not a cosmetic one.
|
|
||||||
|
|
||||||
## Rendering follow-ups (non-blocking)
|
## Rendering follow-ups (non-blocking)
|
||||||
- ~~Per-pilot mech PAINT (color/badge/patch)~~ — **DONE 2026-07-17, verified live** (crimson MadCat +
|
- ~~Per-pilot mech PAINT (color/badge/patch)~~ — **DONE 2026-07-17, verified live** (crimson MadCat +
|
||||||
yellow VGL emblems + hip hazard stripes). Mechanics + the vehicletable color/badge/patch name
|
yellow VGL emblems + hip hazard stripes). Mechanics + the vehicletable color/badge/patch name
|
||||||
|
|||||||
@@ -254,55 +254,6 @@ desktops. Two pieces were added for the cab:
|
|||||||
once, and over CRD you cannot see the panels at all: the log IS the confirmation that a picture
|
once, and over CRD you cannot see the panels at all: the log IS the confirmation that a picture
|
||||||
landed on the right glass.
|
landed on the right glass.
|
||||||
|
|
||||||
### Boot-STABLE panel binding — `monitor:id:` (DEPLOYED + POD-VERIFIED 2026-08-10)
|
|
||||||
**Live on ALPHA-MR since 2026-08-10** — mapping eye-verified over livestream, then the master
|
|
||||||
`C:\bt411\glass_layout.cfg` flipped to the id form (old cfg saved as
|
|
||||||
`glass_layout.cfg.displaynbak`) and the relaunch bound all three windows to the SAME rects with
|
|
||||||
zero mismatch shouts [T2]. The shipped binding:
|
|
||||||
```
|
|
||||||
Secondary / Radar=monitor:id:Default_Monitor,bare # colour radar LCD, USB adapter
|
|
||||||
VGA Port A=monitor:id:UID224795,bare # RAR0005: Heat + Comm + Mfd2 CRTs
|
|
||||||
VGA Port B=monitor:id:UID200195,bare # RAR0005: Mfd1 + Mfd3 CRTs
|
|
||||||
```
|
|
||||||
⚠ **The radar LCD's USB display adapter presents NO EDID** — its only stable identity is the
|
|
||||||
`Default_Monitor` placeholder (`…#Default_Monitor#…&UID256#`). Unique today; a second EDID-less
|
|
||||||
panel would collide, and the per-boot `[glassid]` dump (BT_GLASS_IDS=1, now PERMANENT in
|
|
||||||
`podprofile.ini`) plus the mismatch shout are the diagnosis tools. The Dell (`DEL4025`) is the
|
|
||||||
PRIMARY main-view panel (BT_FIT owns it; not in the layout cfg) — the 08-08 note calling it a
|
|
||||||
"1908FP" service monitor mis-guessed its role.
|
|
||||||
**Windows renumbers displays.** Nick, after re-cabling + a reboot: *"the order changed … sometimes
|
|
||||||
they change when one gets turned off and back on, at least how windows SEEs them, even if the
|
|
||||||
visual desktop tool looks the same."* So the two original binding forms are both boot-fragile —
|
|
||||||
`monitor:2` is an enumeration index and `monitor:\.\DISPLAY4` is a GDI name Windows reassigns.
|
|
||||||
(Same trap next door in GameOS: its `-tmon` takes **DirectDraw device indices**, which are neither
|
|
||||||
Windows monitor numbers nor stable, and a NULL-device merge shifts every index down by one on top
|
|
||||||
— see Nick's `gos-displays.txt`.)
|
|
||||||
|
|
||||||
**The fix: bind to the panel's own hardware identity.** `EnumDisplayDevices` on a display's MONITOR
|
|
||||||
child returns a DeviceID embedding the **EDID manufacturer + product code** and the connector
|
|
||||||
instance — neither moves across a reboot or a power-cycle.
|
|
||||||
|
|
||||||
- **Discover:** run once on the pod with `BT_GLASS_IDS=1`. Every attached panel logs its
|
|
||||||
`stable-id` **and a ready-to-paste `cfg form`**:
|
|
||||||
```
|
|
||||||
[glassid] index=0 device=\\.\DISPLAY1 PRIMARY rect=0,0 1920x1080
|
|
||||||
stable-id = \\?\DISPLAY#AUO10ED#4&31323a6c&1&UID265988#{e6f07b5f-…}
|
|
||||||
cfg form = monitor:id:AUO10ED
|
|
||||||
```
|
|
||||||
The volatile identifiers print beside the stable one deliberately: run it twice across a
|
|
||||||
power-cycle and `index`/`device` move while `stable-id` does not — that IS the proof.
|
|
||||||
- **Bind:** `Heat MFD=monitor:id:AUO10ED,bare` in `glass_layout.cfg`.
|
|
||||||
- **Identical panels** (the pod's mono MFDs are likely one model, so EDID codes collide): use a
|
|
||||||
longer fragment from `stable-id` — the `UID…`/instance tail differs per connector, so
|
|
||||||
`monitor:id:UID265988` picks exactly one.
|
|
||||||
- **An `id:` that matches nothing WARNS and falls back** to computed placement:
|
|
||||||
`[glasswin] monitor id 'X' matched NO attached panel`. Silence there would put a picture on the
|
|
||||||
wrong glass and look exactly like the bug this form exists to prevent.
|
|
||||||
- **Nothing changes by default.** No env, no `id:` → identical behaviour; `monitor:<name|index>`
|
|
||||||
and raw `x,y` keep working. Playtester glass builds are untouched. [T2 verified on a 1-monitor
|
|
||||||
dev box — discovery, binding, centring and the mismatch warning; **T3 on the pod**, which was
|
|
||||||
offline: multi-panel disambiguation is unproven.]
|
|
||||||
|
|
||||||
**Runbook** (`tools/podprobe.ps1`, PowerShell, no install/admin — run it ON the pod PC):
|
**Runbook** (`tools/podprobe.ps1`, PowerShell, no install/admin — run it ON the pod PC):
|
||||||
1. Probe: GPUs, every monitor's virtual-desktop rect, EDID make/model (identifies the original
|
1. Probe: GPUs, every monitor's virtual-desktop rect, EDID make/model (identifies the original
|
||||||
panels), serial ports (the RIO board), session type, and a PROPOSED `glass_layout.cfg` that
|
panels), serial ports (the RIO board), session type, and a PROPOSED `glass_layout.cfg` that
|
||||||
@@ -439,124 +390,6 @@ returns nothing rather than failing. Kill `btl4.exe` BEFORE scp'ing a new exe or
|
|||||||
locked. The receipts in `podrun.log` are the remote eyes; a missing `DEBUG_STREAM` line in an
|
locked. The receipts in `podrun.log` are the remote eyes; a missing `DEBUG_STREAM` line in an
|
||||||
otherwise-logging run is real evidence that code path did not execute.
|
otherwise-logging run is real evidence that code path did not execute.
|
||||||
|
|
||||||
### The remote runbook — READ THIS BEFORE IMPROVISING (2026-08-08)
|
|
||||||
Every line below cost real time to rediscover on a live stream. Follow it in order.
|
|
||||||
|
|
||||||
**1. Connect.** `ssh bt411-pod` (alias in `~/.ssh/config`) → `bt411-pod.tail840fa4.ts.net`, user
|
|
||||||
`user`, key `~/.ssh/bt411_pod`.
|
|
||||||
⚠ **The key has existed since 2026-08-06 but ssh will NOT offer it without the config entry** — it
|
|
||||||
only tries default names (`id_rsa`/`id_ed25519`). Without it you get
|
|
||||||
`Permission denied (publickey,password,keyboard-interactive)`, which reads exactly like "auth was
|
|
||||||
never set up". It was. Also: the Tailscale **node** KeyExpiry (2027-02-02) is what keeps the cab on
|
|
||||||
the tailnet — it is **not** an SSH credential, and Tailscale SSH is **not** enabled on the pod (the
|
|
||||||
peer advertises no `sshHostKeys`), so port 22 is the pod's own Windows OpenSSH.
|
|
||||||
|
|
||||||
**2. Absolute paths for System32 tools.** Over this SSH+cmd session, bare `taskkill` / `setx`
|
|
||||||
return `The system cannot find the path specified.` and silently do nothing. Use
|
|
||||||
`C:\Windows\System32\taskkill.exe`. (`tasklist`, `schtasks`, `dir`, `copy` happen to resolve.)
|
|
||||||
|
|
||||||
**3. Deploy — the SAME procedure a tester uses.** No pod-special exe drops.
|
|
||||||
```
|
|
||||||
python tools/mkdist.py # -> dist/BT411_4.11.NNN.zip
|
|
||||||
scp dist/BT411_4.11.NNN.zip bt411-pod:C:/bt411/
|
|
||||||
ssh bt411-pod "powershell -NoProfile -Command \"Expand-Archive C:\bt411\BT411_4.11.NNN.zip -DestinationPath C:\bt411 -Force\""
|
|
||||||
ssh bt411-pod "powershell -NoProfile -ExecutionPolicy Bypass -File C:\bt411\podkit.ps1 -Content C:\bt411\BT411_4.11.NNN\content"
|
|
||||||
```
|
|
||||||
`runpod.bat` auto-resolves the **newest** `BT411_*` by date, so nothing else needs pointing.
|
|
||||||
Local config is **never clobbered**: `mkdist.py` packs git-TRACKED content only, and
|
|
||||||
`bindings.txt`/`environ.ini`/`glass_layout.cfg` are all gitignored. `podkit.ps1` pushes the frozen
|
|
||||||
rig masters (`podprofile.ini`, `glass_layout.cfg`, `PODTEST.EGG`) from `C:\bt411\` into the install.
|
|
||||||
⚠ **`PODTEST.EGG` is not in the repo** — only podkit carries it. If podkit fails, the launcher runs
|
|
||||||
and *nothing appears*, with no error.
|
|
||||||
|
|
||||||
**4. Launch.** `schtasks /run /tn BT411Run` → runs `C:\bt411\runpod.bat` as `user`,
|
|
||||||
LogonType=Interactive, so it lands in **session 1** and is visible on the panels. Log:
|
|
||||||
`<install>\content\podrun.log`.
|
|
||||||
|
|
||||||
**5. ⚠ THE PODKIT INFINITE LOOP — the trap that ate an evening.** `podkit.ps1` line 30 was
|
|
||||||
```powershell
|
|
||||||
while ($keep.Count -gt 0 -and $keep[-1].Trim() -eq '') { $keep = $keep[0..($keep.Count-2)] }
|
|
||||||
```
|
|
||||||
When `$keep` trims to ONE blank line, `$keep.Count-2` is `-1` and PowerShell's `$keep[0..-1]`
|
|
||||||
returns **two** elements instead of shrinking — infinite loop, RSS climbing past 60 MB. It fires
|
|
||||||
whenever everything outside `environ.ini`'s marker block is blank, i.e. **any `environ.ini` that was
|
|
||||||
already kitted** — exactly what you get carrying it forward from the previous install. Patched to
|
|
||||||
`-gt 1` on the pod 2026-08-08 (`podkit.ps1.bak` is the original); **this fix is NOT in any repo**,
|
|
||||||
so a restored/replaced podkit brings the bug back.
|
|
||||||
*Signature:* a **blank cmd console** on the pod, task stuck `Status: Running`, **no `btl4.exe`, no
|
|
||||||
`podrun.log`**. *The cascade:* each hung run holds `environ.ini`, so every later attempt blocks too —
|
|
||||||
and over SSH your client times out while the REMOTE powershell keeps running, so "it returned
|
|
||||||
instantly and did nothing" actually means "it is still hung". Recover with
|
|
||||||
`taskkill /F /PID <pid>` (absolute path) on the session-1 `cmd`+`powershell` pair, then
|
|
||||||
`schtasks /end /tn BT411Run` before re-running — a task already Running refuses `/run` with
|
|
||||||
`2147946720` (`0x800710E0`, "operator refused the request").
|
|
||||||
|
|
||||||
**6. Panel identity (measured 2026-08-08, over SSH — WMI is session-independent so this works
|
|
||||||
without a GUI):** `DISPLAY\RAR0005\…UID224795`, `DISPLAY\DEL4025\…UID249395` (DELL 1908FP),
|
|
||||||
`DISPLAY\RAR0005\…UID200195`. **The two RAR panels share one EDID code and have blank serials**, so
|
|
||||||
`monitor:id:RAR0005` is ambiguous — they must be bound by the per-connector form
|
|
||||||
(`monitor:id:UID224795` / `monitor:id:UID200195`). **RESOLVED 2026-08-10:** the full map is
|
|
||||||
UID224795 = VGA Port A (Heat+Comm+Mfd2), UID200195 = VGA Port B (Mfd1+Mfd3), `Default_Monitor`
|
|
||||||
(EDID-less USB adapter, UID256) = the colour radar LCD, DEL4025 = the PRIMARY main-view panel
|
|
||||||
(not a service monitor). The master cfg now ships the id form (see the binding section above);
|
|
||||||
`glass_layout.cfg.displaynbak` preserves the old DISPLAYn cfg.
|
|
||||||
|
|
||||||
**7. ⚠ cmd redirect trap when scripting over SSH:** `echo VAR=1>> file` appends `VAR=` — cmd
|
|
||||||
parses the digit-adjacent `1>>` as a stream-handle redirect and eats the value (and MSVC putenv
|
|
||||||
treats an EMPTY value as UNSET, so the flag silently never arms). Space it (`echo VAR=1 >>file`,
|
|
||||||
trailing-space payload) or use PowerShell `Add-Content`. Bit us arming BT_GLASS_IDS 2026-08-10.
|
|
||||||
|
|
||||||
**8. Tester-procedure deploy VERIFIED end-to-end (2026-08-10, build 876):** scp zip →
|
|
||||||
`Expand-Archive` → podkit → `schtasks /run` came up clean on the first try — rig config survived,
|
|
||||||
newest-install auto-resolve worked, no hand-edits. The cab genuinely upgrades like a tester box.
|
|
||||||
|
|
||||||
**9a. ⚠ THE RIO's "COM1" IS A USB-SERIAL ADAPTER — the axis-dropout / frozen-boot fault
|
|
||||||
(2026-08-10, minidump-proven) [T1].** `\Device\VCP0` = an FTDI adapter (FTE7PGA5A); COM2 is a
|
|
||||||
second FTDI (A995JTOYA); **two ghost Prolific PL2303 entries also claim "COM1"** from earlier
|
|
||||||
plug-ins (clean them in Device Manager). When the adapter's **USB side** drops mid-operation:
|
|
||||||
brief drop = analog polls go mute (the field "encoder dropouts", revived by hand-jostle /
|
|
||||||
re-enumeration — NOT by button protocol); drop with a write in flight = the 1995 code's
|
|
||||||
**unbounded `GetOverlappedResult` wait in `PCSerialPacket::SendPacket` parked the whole game at
|
|
||||||
0 CPU forever** (three boots on 2026-08-10; stack: NtWaitForSingleObject ← GetOverlappedResult ←
|
|
||||||
SendPacket ← RIO ctor). **Hardened in 4.11.881**: 500 ms bounded write + CancelIo + 3 s sick
|
|
||||||
window (fast-drop, game keeps frame rate, keyboard fallback lives); `txTO=` in the `[rio]` health
|
|
||||||
line counts the events — proven the same evening (`txTO=1 rRetry=4` on a boot that SURVIVED).
|
|
||||||
USB **selective suspend disabled** system-wide on the cab the same day (powercfg) — prime suspect
|
|
||||||
for the random-period idle dropouts. Hardware to-do: FTDI adapter into a REAR motherboard USB
|
|
||||||
port, reseat its USB end. ⚠ A first "print-deadlock" diagnosis of the boot wedge was WRONG
|
|
||||||
(confounded A/B) and is retracted — the dump is the authority.
|
|
||||||
**Day-end state (2026-08-10 evening) + the pick-up plan.** The 881 soak measured the fault
|
|
||||||
precisely: the board answers for **~13 s after each fresh game start** (254 replies at 20 Hz),
|
|
||||||
then goes MUTE on analog for minutes while host→board writes keep completing (`req=3846 rep=254
|
|
||||||
age=240s`, txTO frozen at 1, abandon=0). Direct hand-poll (`tools/probe_rio.ps1`, also at
|
|
||||||
`C:\bt411\probe_rio.ps1` — raw COM1 CHECK/VERSION/ANALOG with correct [cmd][sum&0x7F] framing,
|
|
||||||
DTR/RTS raised): **zero bytes**. USB re-enumeration of BOTH FTDI converters + immediate re-poll:
|
|
||||||
**still zero** — which argues AGAINST the adapter (a wedged adapter RX clears on re-enum) and AT
|
|
||||||
the board. ⚠ [T4 hypothesis for tomorrow]: 13 s ≈ when the mission render load lights up — a
|
|
||||||
**power rail sagging under the cab's full load** would also explain the original mid-combat
|
|
||||||
dropouts (explosion/load transients) and the button "revival" (load dips + harness jostle).
|
|
||||||
Tomorrow, with hands: (1) loopback plug on the FTDI (TX→RX jumper, probe echoes = adapter fully
|
|
||||||
exonerated); (2) 5 V/12 V at the RIO board while a mission loads (the 13 s correlation is the
|
|
||||||
test); (3) swap in one of the spare Prolific adapters as cheap elimination; (4) rear-mobo USB
|
|
||||||
port + ghost-COM cleanup regardless. ⚠ My STALL/RECOVER edge detector in 881 keys on
|
|
||||||
request-age, which the 50 ms poll keeps resetting — it never fires; the `age=` field carries the
|
|
||||||
real signal (fix the keying to reply-age at the next build).
|
|
||||||
**Remote minidump procedure (worked over SSH, no tools install):** 32-bit dump of the wedged pid
|
|
||||||
via `C:\Windows\SysWOW64\rundll32.exe C:\Windows\SysWOW64\comsvcs.dll, MiniDump <pid>
|
|
||||||
C:\bt411\w.dmp full` → scp home → local x86 cdb (`where.exe cdb` resolves the bt411-tools copy)
|
|
||||||
with `-y C:\git\bt411\dist` (the archived per-build PDBs) → `~*k`. A 64-bit comsvcs dump of the
|
|
||||||
WOW64 process shows only 64-bit frames — use the SysWOW64 pair.
|
|
||||||
|
|
||||||
**9. ⚠ `schtasks /end` ORPHANS the game — the dead-RIO trap (2026-08-10):** `/end` kills the
|
|
||||||
batch, but `runpod.bat` `start ""`s the exe DETACHED, so the old btl4.exe survives, keeps COM1
|
|
||||||
open, and the NEXT launch boots with a dead cockpit board: `InitSerialPort() failed
|
|
||||||
[GetLastError() = 5]` (access denied) → `RIO initialization failed!` → buttons dead while the
|
|
||||||
banner still says "hardware RIO" (the banner is the PROFILE, not the init result). Diagnosis:
|
|
||||||
`tasklist /FI "IMAGENAME eq btl4.exe"` showing ≥2 instances. FIXED at the root the same day:
|
|
||||||
`runpod.bat` now `taskkill /F /IM btl4.exe`s FIRST (original saved as `runpod.bat.bak`), making
|
|
||||||
the BT411Run task an idempotent restart — but any hand-rolled launcher on a future cab needs the
|
|
||||||
same line, and a mid-mission `/run` now RESTARTS the game rather than failing quietly.
|
|
||||||
|
|
||||||
## The 1995 player manual — alignment audit (2026-07-18) [T1, primary source]
|
## The 1995 player manual — alignment audit (2026-07-18) [T1, primary source]
|
||||||
`reference/manual/Tesla40_BT_manual.pdf` (34pp, from Nick). CONFIRMS the reconstruction on
|
`reference/manual/Tesla40_BT_manual.pdf` (34pp, from Nick). CONFIRMS the reconstruction on
|
||||||
every checked control behavior:
|
every checked control behavior:
|
||||||
|
|||||||
@@ -863,137 +863,3 @@ warning"), list what STARTS DRAWING at that event -- alarm-driven redraws,
|
|||||||
state-change repaints -- before suspecting the event's logic; (d) the
|
state-change repaints -- before suspecting the event's logic; (d) the
|
||||||
operator's screenshot is worth ten theories: the red-faces capture identified
|
operator's screenshot is worth ten theories: the red-faces capture identified
|
||||||
in one frame what three log-side hypotheses missed.
|
in one frame what three log-side hypotheses missed.
|
||||||
|
|
||||||
## 28. HAND-COMPOSING an engine-derived transform reads a STALE CACHE — and it only bites REPLICANTS (#141, 2026-08-08)
|
|
||||||
|
|
||||||
`EntitySegment::GetSegmentToEntity()` (`SEGMENT.cpp:262`) **recomputes only when
|
|
||||||
`segmentModified` is already set** — otherwise it hands back the cached matrix,
|
|
||||||
and if the segment has no parent it can never recompute at all. The thing that
|
|
||||||
sets that flag after a joint moves is `JointedMover::GetSegmentToWorld`
|
|
||||||
(`JMOVER.cpp:136-146`): it tests `AreJointsModified()` and, when set, walks the
|
|
||||||
whole segment table marking every entry dirty, then clears the joint flag.
|
|
||||||
|
|
||||||
The binary agrees exactly. `MechWeapon::GetMuzzlePoint @004b9948` ends in
|
|
||||||
`FUN_00424da8(owner, segment, out)`, which IS `GetSegmentToWorld`
|
|
||||||
instruction-for-instruction (`GetJointSubsystem` → `if (AreJointsModified())` →
|
|
||||||
mark all → `ModifyJoints(False)` → `× localToWorld`). **So in the 1995 image
|
|
||||||
every muzzle query performs the joints→segments refresh.** [T1]
|
|
||||||
|
|
||||||
Four port sites had replaced that with `mw.Multiply(seg->GetSegmentToEntity(),
|
|
||||||
mech->localToWorld)` — including one commented "the faithful FUN_004b9948".
|
|
||||||
They skip the refresh and read whatever cache is present.
|
|
||||||
|
|
||||||
**Why it hid for a year:** the local mech is refreshed every frame anyway — the
|
|
||||||
renderer and cockpit camera call `GetSegmentToWorld` on it, *after* its torso
|
|
||||||
pushes the joint. So master-side output is correct and solo testing is clean.
|
|
||||||
A **replicant** gets no such pass: its cache stays at the BIND POSE. Measured on
|
|
||||||
a 2-node bench, peer missiles left along the LEG facing with `segYaw == bodyYaw`
|
|
||||||
EXACTLY (`twistDelta` 0.0000 over 165 salvos) while that same peer's copy torso
|
|
||||||
was demonstrably writing its joint (`PushTwist COPY twist=-1.49601`) from
|
|
||||||
correctly replicated records. Twist arrived, joint moved, segment never
|
|
||||||
re-derived.
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
(a) **Never hand-compose `GetSegmentToEntity() × localToWorld`.** Call
|
|
||||||
`GetSegmentToWorld` — it is the binary's own path and it does the refresh.
|
|
||||||
(b) **Do NOT "fix" a stale transform by forcing the dirty flag.** Setting
|
|
||||||
`ModifyJoints(True)` at the read site made the symptom go away and scored
|
|
||||||
identically to the faithful fix — it was a stand-in that patched ONE
|
|
||||||
consumer and left every other peer segment reader stale. The binary only
|
|
||||||
ever *tests* that flag.
|
|
||||||
(c) A cached-transform bug is **master/replicant asymmetric by construction**.
|
|
||||||
If a geometry symptom is reported "peer POV only", suspect a cache that the
|
|
||||||
local render pass refreshes for free — before suspecting replication. Here
|
|
||||||
the replication was provably fine.
|
|
||||||
(d) When a fix lands at a partial percentage, **check whether the failures are
|
|
||||||
interleaved or a PREFIX** before calling it partial. These were a clean
|
|
||||||
prefix that ended the moment the peer first had a non-zero twist to carry —
|
|
||||||
i.e. the fix was complete and the remainder was correct behaviour. Reporting
|
|
||||||
it as "64% fixed" was wrong.
|
|
||||||
(e) Related probe trap: `Torso::PushTwist` sampled ONE shared static every 30th
|
|
||||||
call. With a master torso and a copy torso ticking 1:1, every 30th call is
|
|
||||||
always the SAME instance — the probe showed only the local untwisted torso
|
|
||||||
and hid the copy's writes entirely. Sample **per instance-kind** whenever
|
|
||||||
master and replicant objects share a diagnostic. (See also §gotcha on
|
|
||||||
process-wide statics serving the player's data as the replicant's.)
|
|
||||||
|
|
||||||
## 29. A peer mech does NOT tick before RunningMission — bench-only, and it fakes a replication bug (#148, 2026-08-08)
|
|
||||||
|
|
||||||
`Entity::Execute` (`ENTITY.cpp:556`, real engine source [T0]) calls
|
|
||||||
`PerformAndWatch` **only** when
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
application->GetApplicationState() == Application::RunningMission
|
|
||||||
|| application->GetApplicationState() == Application::EndingMission
|
|
||||||
|| IsPreRunnable()
|
|
||||||
```
|
|
||||||
|
|
||||||
otherwise it just does `WriteSimulationUpdate`. `Entity::DefaultFlags` is
|
|
||||||
`DynamicFlag|MasterInstance` — **no `PreRunFlag`**; only `Player` and `Director`
|
|
||||||
add it in their DefaultFlags, and `Mech::Reset` sets it for a reset MASTER
|
|
||||||
("a reset master must tick"). A **replicant mech never gets it.**
|
|
||||||
|
|
||||||
So during `LoadingMission` / `WaitingForLaunch` / `LaunchingMission` a peer mech
|
|
||||||
performs **zero** subsystem ticks, no matter how much correctly-replicated data
|
|
||||||
is arriving for it. Measured on the observer node:
|
|
||||||
|
|
||||||
```
|
|
||||||
235 [perf-first] mech 3:161 master <- own mech, immediately
|
|
||||||
402 [torso-rec-rx] <- peer's torso records start arriving
|
|
||||||
2754 [perf-first] mech 2:55 REPLICANT <- peer's FIRST performance
|
|
||||||
2758 [torso] PushTwist COPY <- its torso ticks 4 lines later
|
|
||||||
2761 [ent-exec] state=5 <- RunningMission
|
|
||||||
```
|
|
||||||
|
|
||||||
This is correct engine behaviour, **but it silently corrupts any bench that acts
|
|
||||||
before the round starts.** `BT_AUTOFIRE`/`BT_GOTO` begin immediately, so early
|
|
||||||
salvos measure a peer whose torso, gait and subsystems have never run — and the
|
|
||||||
result reads exactly like a replication failure. It cost a full investigation
|
|
||||||
(filed as #148) before the app-state trace showed the peer was simply not
|
|
||||||
executing yet.
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
(a) **Judge a 2-node bench by PREFIX vs INTERLEAVED, never by raw percentage.**
|
|
||||||
A clean leading run of failures that stops for good is almost always the
|
|
||||||
pre-`RunningMission` window; interleaved failures are the real thing.
|
|
||||||
(b) When a peer looks inert, check `[ent-exec] state=` before suspecting
|
|
||||||
replication. States: `2` LoadingMission, `3` WaitingForLaunch,
|
|
||||||
`4` LaunchingMission, `5` RunningMission.
|
|
||||||
(c) Prefer benches that wait for `RunningMission` before acting — or slice the
|
|
||||||
log at the transition — otherwise every peer-side metric carries this bias.
|
|
||||||
(d) The receipts that make this legible: `BT_NET_TRACE` gives `[upd-repl]`
|
|
||||||
(offered to the performer), `[ent-exec]` (state / preRun / instance) and
|
|
||||||
`[perf-first]` (one-shot per mech: entity ID + instance at its FIRST
|
|
||||||
performance). Anonymous per-frame receipts are useless in a 2-node log —
|
|
||||||
**name the mech.**
|
|
||||||
|
|
||||||
## 30. A finite-difference cache the binary re-seeds at Reset — transcribe the WHOLE seed list (#137, 2026-08-09)
|
|
||||||
|
|
||||||
The binary `Mech::Reset @0049fb74` opens with more than the obvious origin writes: its SECOND
|
|
||||||
instruction is `FUN_00408440(mech+0x58c, param_2)` — re-seeding the **previous-position memory**
|
|
||||||
of the AccelerationLastFrame ring feed (+0x81c/0x824/0x828/0x82c) to the new origin. The port
|
|
||||||
reconstructed the ring itself faithfully (ctor `part_012.c:9836`, derivative `:15169`) but its
|
|
||||||
Reset never got that one line. Result: the first post-respawn sample computed
|
|
||||||
`|newPos − prevPos| / dt` = **teleport distance / dt ≈ 1e5** into the velocity ring; the
|
|
||||||
ring-mean derivative turned it into an acceleration spike (with an opposite-sign ECHO ~15 frames
|
|
||||||
later as the sample rotated out of the mean); the myomer heat integrator's
|
|
||||||
`termAccel = (1−accEff)·|v|·|a|·m·dt` turned THAT into a ~3e9 one-tick heat deposit; and the
|
|
||||||
freshly-reset myomers snapped from 77 to ~9000 against failT=2000 — the #137 respawn freeze.
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
(a) **When the binary's Reset writes a cell you don't recognize, that write IS the spec.** The
|
|
||||||
+0x58c re-seed looked like bookkeeping and was silently dropped; it was the only thing
|
|
||||||
standing between a teleport and a position-derivative spike. Transcribe the whole zero/seed
|
|
||||||
list, then map each cell — never the recognizable subset.
|
|
||||||
(b) **Any prev-value cell backing a finite difference must be re-seeded at every discontinuity**
|
|
||||||
(teleport, warp, respawn). If you add such a cache port-side, grep the binary's reset for its
|
|
||||||
analog before assuming none exists.
|
|
||||||
(c) **Derived state hides stale backing state.** `currentTemperature` sampled AT the reset read
|
|
||||||
77 (clean) because RTIS wrote it — while the freeze arrived one frame later through
|
|
||||||
`heatEnergy += pendingHeat` from a live producer. Probing the derived cell at the reset
|
|
||||||
instant proves nothing about the producers; trace the WINDOW after, per producer.
|
|
||||||
(d) The diagnosis chain that worked, for reuse: roster-wide state snapshot (at-death / at-reset /
|
|
||||||
post-reset) → eliminate conduction by trapping flows with full operands → trap the remaining
|
|
||||||
producer's deposits with operands → cross-reference the operand SHAPE (pure local −z,
|
|
||||||
magnitude = distance/dt, echo at ring-length) against the writers. Each trap eliminated a
|
|
||||||
theory the previous data had made plausible; three plausible theories died on operands.
|
|
||||||
|
|||||||
@@ -299,39 +299,3 @@ the `BTGetSubsystemAuxScreen` bridge. See `docs/VEHICLE_SUBSYSTEMS.md` + [[gauge
|
|||||||
- Data: [[decomp-reference]] (ClassIDs/hierarchy). Bugs: [[reconstruction-gotchas]].
|
- Data: [[decomp-reference]] (ClassIDs/hierarchy). Bugs: [[reconstruction-gotchas]].
|
||||||
- Feeds: [[combat-damage]] (weapons/damage), [[gauges-hud]] (attribute state).
|
- Feeds: [[combat-damage]] (weapons/damage), [[gauges-hud]] (attribute state).
|
||||||
- Plan: `docs/SUBSYS_PLAN.md`.
|
- Plan: `docs/SUBSYS_PLAN.md`.
|
||||||
|
|
||||||
## Myomer drive-heat calibration — VERIFIED FAITHFUL (2026-08-09, #137) [T1]
|
|
||||||
The integrator `@004b8d18` accumulates into `pendingHeat@0x1C8`:
|
|
||||||
`gear² × (1 + X) × [ (1−velEff)·|vy|·m·g·dt + (1−velEff)·work + (1−accEff)·|v|·|a|·m·dt ]`
|
|
||||||
with `work = mass · |v|² · 0.5`. Its constants, read byte-exact from `.rdata`
|
|
||||||
(`section_dump.txt` row ` 4b8ee0 5dc30000 0000003f 00000000 0000803f`):
|
|
||||||
**`_DAT_004b8ee4` = 0.5f** (the kinetic ½), **`_DAT_004b8ee8` = 0.0f** (the `Abs()` idiom),
|
|
||||||
**`_DAT_004b8eec` = 1.0f** (the `1 − efficiency` complements and the gear-ratio clamp floor).
|
|
||||||
All three match what the port computes — the formula and its authored inputs
|
|
||||||
(VelocityEfficiency 0.995, AccelerationEfficiency 0.8, thermalMass 2.5e5, myomers linked
|
|
||||||
Condenser5) are reconstructed correctly.
|
|
||||||
|
|
||||||
**The one deliberate deviation, and why it is the faithful choice.** The binary applies **no
|
|
||||||
`time_slice`** to the kinetic term (`fVar5 * fVar1`) while the climb and accel terms both carry
|
|
||||||
`param_2` — it is a per-frame energy add at the pod's **fixed ~28 Hz**. The port uses
|
|
||||||
`work × (time_slice × 28)`, which is *identical* at 28 Hz (`dt·28 = 1.0`) but holds the same
|
|
||||||
heat-per-SECOND at any frame rate. A literal transcription would add the full term once per
|
|
||||||
frame, so at 170 fps it would inject ~6× the heat the pod ever did. `BT_MYO_HZ` overrides the
|
|
||||||
reference rate for bracketing.
|
|
||||||
|
|
||||||
**Consequence for #137 — CORRECTED 2026-08-09, the paragraph that stood here was wrong.** The
|
|
||||||
"players read 'respawned with heat maxed' as acceleration to top speed" claim did not survive
|
|
||||||
the data: the deposits were e9-scale within 30 frames of the reset, physically impossible from
|
|
||||||
motion input (~6.5e5/frame ceiling). The actual cause was a **dropped binary re-seed**: the
|
|
||||||
binary Reset's second instruction (`FUN_00408440(mech+0x58c, origin)`) re-seeds the
|
|
||||||
previous-position memory of the AccelerationLastFrame ring feed (+0x81c..+0x82c); the port
|
|
||||||
reconstructed the ring but not the re-seed, so the first post-respawn sample computed
|
|
||||||
TELEPORT-DISTANCE/dt (~1e5) into the velocity ring, the ring-mean derivative spiked
|
|
||||||
`AccelerationLastFrame`, and `termAccel = (1-accEff)·|v|·|a|·m·dt` deposited ~3e9 into
|
|
||||||
`pendingHeat` in one tick → myomers snapped from 77 to ~9000 (failT 2000) → speedEffect 0 →
|
|
||||||
frozen until cooled. Fixed by restoring the re-seed (`accelPrevPos = origin.linearPosition` in
|
|
||||||
Mech::Reset). The ~8% field rate was the |v| factor: only pilots whose throttle was still
|
|
||||||
forward at the respawn (physical lever / HOTAS — exactly who reported it) had gait-republished
|
|
||||||
speed in the spike frames. The calibration facts above (constants byte-exact, dt-normalised
|
|
||||||
kinetic term) all STAND; the in-life governor (running hot at sustained top speed derates the
|
|
||||||
myomers) is authentic and remains.
|
|
||||||
|
|||||||
@@ -696,7 +696,7 @@ reconfigure/externalConfigure); parse-skip list EMPTY ([gskip]=0), all 50 attr b
|
|||||||
| 30 | sec: schematic CRITICAL view (cmCrit) | subsystem simulationState/damage | LIVE this audit: N-cycle shows the full subsystem list (GEN A-D, LOOP 1-6, HUD, SENSORS, GYRO, TORSO, weapons) | T2 | CORRECT |
|
| 30 | sec: schematic CRITICAL view (cmCrit) | subsystem simulationState/damage | LIVE this audit: N-cycle shows the full subsystem list (GEN A-D, LOOP 1-6, HUD, SENSORS, GYRO, TORSO, weapons) | T2 | CORRECT |
|
||||||
| 31 | sec: schematic HEAT view (cmHeat) | subsystem currentTemperature tint | #6 pixel-verified; re-cycled this audit (mask 0x450421→0x490421→0x510421) | T2 | CORRECT |
|
| 31 | sec: schematic HEAT view (cmHeat) | subsystem currentTemperature tint | #6 pixel-verified; re-cycled this audit (mask 0x450421→0x490421→0x510421) | T2 | CORRECT |
|
||||||
| 32 | sec: view cycling (N / pod 0x15) | CycleDisplayMode → vtbl+0x4C @4d1ae4 | #6 resolution re-verified live ([mode] display notify 0/1/2) | T2 | CORRECT |
|
| 32 | sec: view cycling (N / pod 0x15) | CycleDisplayMode → vtbl+0x4C @4d1ae4 | #6 resolution re-verified live ([mode] display notify 0/1/2) | T2 | CORRECT |
|
||||||
| 33 | sec: CONTROL MODE lamp (BAS/MID/ADV) | ControlsMapper/DisplayMode oneOfSeveralPixInt | attr wave incr.5; M-cycle verified #6 | T2 | CORRECT |
|
| 33 | sec: CONTROL MODE lamp (BAS/MID/ADV) | ControlsMapper/**ControlMode** oneOfSeveralPixInt (row previously mislabeled DisplayMode — that is the sibling sdspmod) | attr wave incr.5; M-cycle verified #6. **2026-08-10:** this row's verification PREDATED the 07-25 #48 cycle-fill, which then lit the inactive box interiors (art idx 254 → live armor slot 62) — playtester-reported, re-fixed by mapping out-of-range indices to plane background (see context/gauges-hud.md §#48 REFINED); both render paths re-verified against the reference | T2 | CORRECT |
|
||||||
| 34 | sec: duck / searchlight button lamps | duckState (ctor-zeroed only) / Searchlight LightOn | duckState writer missing (P3 leftover) | T3 | DEFERRED-FEED (duck); CORRECT (light attr published) |
|
| 34 | sec: duck / searchlight button lamps | duckState (ctor-zeroed only) / Searchlight LightOn | duckState writer missing (P3 leftover) | T3 | DEFERRED-FEED (duck); CORRECT (light attr published) |
|
||||||
| 35 | sec: messageBoard ticker | StatusMessagePool (NULL stub) + kill ticker strip 0 | 7fc4acb; kill ticker live 2026-07-12, other strips unsurveyed | T2/T3 | DEFERRED-FEED (partial) |
|
| 35 | sec: messageBoard ticker | StatusMessagePool (NULL stub) + kill ticker strip 0 | 7fc4acb; kill ticker live 2026-07-12, other strips unsurveyed | T2/T3 | DEFERRED-FEED (partial) |
|
||||||
| 36 | MFD preset paging (J/K/L, pod RIO banks) | SetPresetMode table @0051dbf0 (little-endian re-decode) | #9; re-verified live this audit: cycles visit EXACTLY the populated set (MFD1: 1,2,4; MFD2: 1-4; MFD3: 1,2) | T2 | CORRECT |
|
| 36 | MFD preset paging (J/K/L, pod RIO banks) | SetPresetMode table @0051dbf0 (little-endian re-decode) | #9; re-verified live this audit: cycles visit EXACTLY the populated set (MFD1: 1,2,4; MFD2: 1-4; MFD3: 1,2) | T2 | CORRECT |
|
||||||
|
|||||||
@@ -339,7 +339,7 @@ cavern.egg`. This is every option the original console exposed for a BattleTech
|
|||||||
- experience (4 [T1 game decode]): novice, standard, veteran, expert ← was hardcoded `expert`
|
- experience (4 [T1 game decode]): novice, standard, veteran, expert ← was hardcoded `expert`
|
||||||
- vehicle (18, friendly→tag): Black Hawk=blkhawk, Loki=loki, Nova=bhk1, Mad Cat=madcat, Thor=thor,
|
- vehicle (18, friendly→tag): Black Hawk=blkhawk, Loki=loki, Nova=bhk1, Mad Cat=madcat, Thor=thor,
|
||||||
Owens=owens, Kotori=own1, Summoner=thr1, Hellbringer=lok1, Timber Wolf=mad1, Avatar=avatar,
|
Owens=owens, Kotori=own1, Summoner=thr1, Hellbringer=lok1, Timber Wolf=mad1, Avatar=avatar,
|
||||||
Satyr=ava1, Sunder=sunder, Denkou=snd1 (#69), Vulture=vulture, Mad Dog=vul1, Mischief=lok2,
|
Satyr=ava1, Sunder=sunder, Sunder V1=snd1, Vulture=vulture, Mad Dog=vul1, Mischief=lok2,
|
||||||
Zanin Neko=mad2 (the 18 ModelList names; matches the 2026-07-18 vehicle sweep)
|
Zanin Neko=mad2 (the 18 ModelList names; matches the 2026-07-18 vehicle sweep)
|
||||||
- color/camo (7, display→tag): White=White, Black=Black, Brown=Brown, Red=**Crimson**, Green=Green,
|
- color/camo (7, display→tag): White=White, Black=Black, Brown=Brown, Red=**Crimson**, Green=Green,
|
||||||
Gray=**Grey**, Tan=Tan ← was White/Black/Crimson only
|
Gray=**Grey**, Tan=Tan ← was White/Black/Crimson only
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
=============================================================================
|
|
||||||
BT411 -- ALL OPEN TRACKER ISSUES (59), SORTED FOR PLAYTESTERS
|
|
||||||
Updated 2026-08-10 (evening) -- after Oracle's full annotated sweep, which
|
|
||||||
closed 35 issues. Thank you Oracle: that is what this list is for.
|
|
||||||
=============================================================================
|
|
||||||
HOW TO REPORT: drop a line in Discord with the issue number and PASS /
|
|
||||||
FAIL / what you saw. Screenshots and rough times help; logs get collected
|
|
||||||
as usual.
|
|
||||||
=============================================================================
|
|
||||||
|
|
||||||
-----------------------------------------------------------------------------
|
|
||||||
A. FIXED -- PLEASE VERIFY (headliners first: new fixes fielded THIS build)
|
|
||||||
-----------------------------------------------------------------------------
|
|
||||||
#149 Glass cockpit stutter / hitching ** HEADLINER **
|
|
||||||
First fielded build with the fix. Glass users -- especially Oracle:
|
|
||||||
mission load and play should feel smooth again (repaints now skip
|
|
||||||
windows whose gauges did not change).
|
|
||||||
#150 Double kill credit (one kill counted twice) ** HEADLINER **
|
|
||||||
First fielded build with the fix (868's double-kills were pre-fix).
|
|
||||||
TEST: kill someone with a missile volley -- scoreboard +1, not +2.
|
|
||||||
#69 "Sunder V1" renamed to DENKOU
|
|
||||||
TEST: vehicle picker shows Denkou -- era witnesses, confirm spelling.
|
|
||||||
#71 Closing the Plasma window lost it until restart
|
|
||||||
TEST: X the plasma window -- it minimizes to the taskbar; click restores.
|
|
||||||
#25 Gamepad turn profile (stick X could not turn the mech)
|
|
||||||
TEST: RajelAran -- bind an axis to "Turn" in content\bindings.txt.
|
|
||||||
#27 Autocannon/Gauss dealt the wrong damage type
|
|
||||||
Oracle asked for a RETEST before closing: ballistics should feel/score
|
|
||||||
like solid shot (no blast splash).
|
|
||||||
#38 Mech colors reverted on respawn
|
|
||||||
TEST: custom paint, die, respawn -- colors stay.
|
|
||||||
#39 Controls were live before the game screen appeared
|
|
||||||
TEST: mash fire during load -- nothing happens until the scene shows.
|
|
||||||
#41 (David/Auric) launcher killed before it could run -- forensics armed
|
|
||||||
TEST: if join.bat ever fails again, send content\lastrun_join.txt.
|
|
||||||
#56 MFD buttons drift away from where they are drawn
|
|
||||||
TEST: alt-tab / focus changes mid-round -- buttons stay aligned.
|
|
||||||
#147 Range caret vanished for the session (ticks stayed)
|
|
||||||
Monitoring: if it happens, the log traps the culprit -- just report it.
|
|
||||||
#158 (Sauron) could not move after respawn until throttle cycled
|
|
||||||
TEST when it happens: does cycling the throttle free you?
|
|
||||||
YES -> the #146 release mechanic (expected). NO -> map + spawn spot.
|
|
||||||
|
|
||||||
-----------------------------------------------------------------------------
|
|
||||||
B. OPEN -- HELP US REPRODUCE / NARROW (what to note when it happens)
|
|
||||||
-----------------------------------------------------------------------------
|
|
||||||
#24 Controller hat/look view sticks (recovers after wiggling)
|
|
||||||
#37 Mech torso appears BACKWARDS to other players (transient)
|
|
||||||
#74 8BitDo gamepad works solo, dead in Steam MP (VexUbiquity)
|
|
||||||
#102 Owens: full-throttle run -> hold reverse = animation/audio hang
|
|
||||||
REPRO known (exterior cam, savannah); self-clears on a torso twist.
|
|
||||||
#125 Peers do not see the FIRST destroyed-zone graphic (2nd hit shows it)
|
|
||||||
#126 Peer weapon fire invisible AND silent from some positions
|
|
||||||
#128 Ramming terrain = INSTANT coolant leak -- Oracle re-confirms STILL LIVE
|
|
||||||
#130 Vulture "skating" (gliding without walk animation)
|
|
||||||
#132 (Conn Man) persistent yellow line on the HUD in every mech
|
|
||||||
#135 Coolant leak with NO flashing annunciators -- Oracle re-confirms LIVE
|
|
||||||
#136 A SHUT leaking loop starts/stops leaking with COMPONENT toggles
|
|
||||||
(Oracle marked "Clarify" -- one more description pass requested.)
|
|
||||||
#143 One panel hit -> smoke erupts from MULTIPLE places at once
|
|
||||||
#153 Basic-mode elevation limit only applies after cycling modes (fix pending)
|
|
||||||
#154 Generator Out warning fires on the WRONG EDGE (at recovery, not trip)
|
|
||||||
Refined by Oracle: the stuck-forever gen did NOT reproduce; era rule =
|
|
||||||
gens return at HALF-cooled (red bar portion gone). Threshold audit due.
|
|
||||||
#155 PANIC lit: Eng + Weapon MFD banks dead; Score/Radar/Coolant stay live
|
|
||||||
(Oracle mapped the exact bank split; #50 closed into this ticket.
|
|
||||||
Flicker half = authentic. Next: is the bank capture authentic too?)
|
|
||||||
#159 (Conn Man) lag during drops -- missiles / fire exploding in empty space
|
|
||||||
NOTE: shooter + target + rough time; match receipts measure the rest.
|
|
||||||
#160 Pod cab: RIO axis dropouts + boot wedges (hardware investigation)
|
|
||||||
Cab-side only. Bounded-write hardening is in this build; the cage
|
|
||||||
hardware work continues with Nick.
|
|
||||||
|
|
||||||
-----------------------------------------------------------------------------
|
|
||||||
C. STILL-OPEN ERA QUESTIONS
|
|
||||||
-----------------------------------------------------------------------------
|
|
||||||
#103 Mech-to-mech RAM: armor damage or internals only? (Oracle: don't know;
|
|
||||||
Lynx / Ronin / Draco -- your call. Binary does internals-only.)
|
|
||||||
#133 Generator X-out on the MFD: Oracle recalls an X "under certain
|
|
||||||
circumstances" -- WHICH circumstances? (shutdown / overheat / destroyed)
|
|
||||||
ALSO (#152 rider): did Mid/Adv auto-recentre the torso in the pod, or only
|
|
||||||
Basic? (Oracle: don't know. Binary says: no auto-recentre.)
|
|
||||||
|
|
||||||
-----------------------------------------------------------------------------
|
|
||||||
D. DESIGN CALLS -- group opinions wanted
|
|
||||||
-----------------------------------------------------------------------------
|
|
||||||
#151 Panic eject costs -499: verified REAL 1995 scoring (your death scored
|
|
||||||
as suicide + death penalty). Keep authentic, or house-rule a flat cost?
|
|
||||||
#23 Non-glass screen layout: Oracle proposes widescreen-aware sizing --
|
|
||||||
world view wider + taller, moved up, allowed to overlap the radar.
|
|
||||||
Opinions welcome before anyone prototypes it.
|
|
||||||
#77 Resizable Secondary/Radar window (touch-panel cockpits). Want it?
|
|
||||||
|
|
||||||
-----------------------------------------------------------------------------
|
|
||||||
E. KNOWN GAPS -- confirmed and on the list, no need to re-report
|
|
||||||
-----------------------------------------------------------------------------
|
|
||||||
#49 Radar/map does not draw the arena WALL geometry
|
|
||||||
#76 Glass layout save/restore rework (some windows not saved)
|
|
||||||
#83 INCOMING MISSILE alarm not implemented yet
|
|
||||||
#90 Flames/smoke from serious damage: less than the original (tuning)
|
|
||||||
#104 Mad Dog "NARC 15" label: NARC postdates our 4.10 binary (Oracle: added
|
|
||||||
in a late 4.x release; zero NARC in our decomp) -- content rename due.
|
|
||||||
#105 Sensor damage effects: Oracle's era spec captured (HUD flicker/loss,
|
|
||||||
radar loss) -- implementation pending.
|
|
||||||
#111 Peers see a dying mech's blue warp bubble (should see explosion only)
|
|
||||||
#112 AFC rounds shotgun across zones (should land as one projectile)
|
|
||||||
#113 LRM burn/hit state lasts too long
|
|
||||||
#115 Hot box / target select not connected (Oracle era note: hotbox was a
|
|
||||||
STANDARD-mode feature; absent in Expert -- gating to reconcile)
|
|
||||||
#121 Sensor HEAT has no consequences yet
|
|
||||||
#123 Searchlight: the visible LIGHT + night-fog swap still to come
|
|
||||||
#145 IR "predator vision": decode done, display not implemented
|
|
||||||
#156 Fade-to-black at mission end not happening
|
|
||||||
|
|
||||||
-----------------------------------------------------------------------------
|
|
||||||
F. INTERNAL ENGINEERING BACKLOG (nothing to test)
|
|
||||||
-----------------------------------------------------------------------------
|
|
||||||
#14 Glass panel unreconstructed button handler backlog (census)
|
|
||||||
#17 Cavern upper-butte halves lack a collision stream (authenticity TBD)
|
|
||||||
#21 Weapon recharge arc dark after eng-page use (symptom trail -> #62)
|
|
||||||
#28 Confirm the vital-subsystem-crit death path can fire (machinery in)
|
|
||||||
#30 Weapon "unavailable" (level-7) display verification + accessor cleanup
|
|
||||||
#63 PlayerStatus reads raw 1995 offsets (databinding trap, dormant)
|
|
||||||
#64 damageZone shadow field (half-fixed; consequences disarmed)
|
|
||||||
#65 HandleMessage non-virtual across 10 classes (latent dispatch trap)
|
|
||||||
#72 BTLoadPump swallows WM_QUIT during load (latent)
|
|
||||||
#107 Destroyed HeatSink annunciates nothing (no lamp for condition 0)
|
|
||||||
#117 KB note: per-chassis cockpit bounce was deliberate (Ronin) -- FYI
|
|
||||||
|
|
||||||
=============================================================================
|
|
||||||
End of list. 59 open issues total:
|
|
||||||
12 verify (2 headliners), 17 reproduce, 3 era questions, 3 design calls,
|
|
||||||
14 known gaps, 11 internal.
|
|
||||||
35 issues closed 2026-08-10 on Oracle's annotated sweep.
|
|
||||||
=============================================================================
|
|
||||||
+2
-9
@@ -14,15 +14,8 @@ Resolve AFTER all 7 families report (Stage 1 reconciliation), then again at link
|
|||||||
- **Player__VehicleDeadMessage**: BT build carried `killerName` (+0x1c) the engine base lacks; read via offset.
|
- **Player__VehicleDeadMessage**: BT build carried `killerName` (+0x1c) the engine base lacks; read via offset.
|
||||||
|
|
||||||
### Stubbed (no WinTesla analog) — revisit at integration
|
### Stubbed (no WinTesla analog) — revisit at integration
|
||||||
- ~~BTPlayer ctor role-registry resolution~~ — **RESOLVED 2026-08-07.** The claim "no WinTesla
|
- BTPlayer ctor role/mission-registry resolution: `GetMissionRegistry()` / `GetRoleRegistry()->Lookup()` /
|
||||||
analog → stubbed; base-set `scenarioRole` stands" was wrong twice over: the base ctor sets
|
`GetMission()->GetGameModel()` have no WinTesla analog → stubbed; base-set `scenarioRole` stands.
|
||||||
`scenarioRole` to **NULL** (PLAYER.cpp:680), so nothing "stood"; and the analog exists —
|
|
||||||
`Mission::GetScenarioRole(name)` (MISSION.h:162) walks the same `scenarioRoleChain` that
|
|
||||||
`BTL4Mission` fills via `AddScenarioRole()` when it parses the role pages. A NULL role zeroed
|
|
||||||
EVERY scoring value (a kill scored 4.88 instead of 505.88, the eject charge read 0, the death
|
|
||||||
cost was skipped). Now bound, with a `Role::Default` fallback and a BOUND/NULL receipt.
|
|
||||||
`GetMission()->GetGameModel()` (the freeForAll compare) stays genuinely stubbed — the shipped
|
|
||||||
code discarded its result anyway.
|
|
||||||
|
|
||||||
### Link-time externs to be PROVIDED by owning modules (Stage 3)
|
### Link-time externs to be PROVIDED by owning modules (Stage 3)
|
||||||
- `ToggleVoiceAssist`, `Is_Destroyed` (mechmppr)
|
- `ToggleVoiceAssist`, `Is_Destroyed` (mechmppr)
|
||||||
|
|||||||
@@ -117,16 +117,6 @@ EntitySegment*
|
|||||||
}
|
}
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
// #149 perf telemetry (BT_PERF_LOG; counters cost two increments when unset).
|
|
||||||
// gBTSegWDirty counts the EXPENSIVE arm -- the mark-every-segment pass that
|
|
||||||
// invalidates the whole table -- separately from plain calls, because the #141
|
|
||||||
// sweep put this accessor on per-frame weapon/beam paths and the open question
|
|
||||||
// is whether that multiplied the dirty-pass rate (each pass forces the next
|
|
||||||
// query of EVERY segment to re-derive its parent chain).
|
|
||||||
int gBTSegWCalls = 0;
|
|
||||||
int gBTSegWDirty = 0;
|
|
||||||
double gBTSegWMs = 0.0;
|
|
||||||
|
|
||||||
//
|
//
|
||||||
void
|
void
|
||||||
JointedMover::GetSegmentToWorld(
|
JointedMover::GetSegmentToWorld(
|
||||||
@@ -135,13 +125,6 @@ void
|
|||||||
)
|
)
|
||||||
{
|
{
|
||||||
Check(this);
|
Check(this);
|
||||||
// DEFAULT ON (operator, 2026-08-09): every player's session log should
|
|
||||||
// carry this -- cross-machine comparison is the point. BT_PERF_LOG=0
|
|
||||||
// opts out. Cost: ~2 QPC reads per call, microseconds per second.
|
|
||||||
static const int sPerf = !(getenv("BT_PERF_LOG") && *getenv("BT_PERF_LOG") == '0');
|
|
||||||
LARGE_INTEGER t0, t1, fq;
|
|
||||||
++gBTSegWCalls;
|
|
||||||
if (sPerf) QueryPerformanceCounter(&t0);
|
|
||||||
JointSubsystem *joints = GetJointSubsystem();
|
JointSubsystem *joints = GetJointSubsystem();
|
||||||
Check(joints);
|
Check(joints);
|
||||||
|
|
||||||
@@ -152,7 +135,6 @@ void
|
|||||||
//
|
//
|
||||||
if (joints->AreJointsModified())
|
if (joints->AreJointsModified())
|
||||||
{
|
{
|
||||||
++gBTSegWDirty;
|
|
||||||
EntitySegment::SegmentTableIterator iterator(segmentTable);
|
EntitySegment::SegmentTableIterator iterator(segmentTable);
|
||||||
EntitySegment *current_segment;
|
EntitySegment *current_segment;
|
||||||
while( (current_segment = iterator.ReadAndNext() ) != NULL)
|
while( (current_segment = iterator.ReadAndNext() ) != NULL)
|
||||||
@@ -171,12 +153,6 @@ void
|
|||||||
my_segment.GetSegmentToEntity(),
|
my_segment.GetSegmentToEntity(),
|
||||||
localToWorld
|
localToWorld
|
||||||
);
|
);
|
||||||
if (sPerf)
|
|
||||||
{
|
|
||||||
QueryPerformanceCounter(&t1);
|
|
||||||
QueryPerformanceFrequency(&fq);
|
|
||||||
gBTSegWMs += 1000.0 * (double)(t1.QuadPart - t0.QuadPart) / (double)fq.QuadPart;
|
|
||||||
}
|
|
||||||
Check_Fpu();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,32 +55,9 @@ ScenarioRole::ScenarioRole(const CString &role_name, const CString &model_file)
|
|||||||
damageBias = player_data->damageBias;
|
damageBias = player_data->damageBias;
|
||||||
friendlyFirePenalty = player_data->friendlyFirePenalty;
|
friendlyFirePenalty = player_data->friendlyFirePenalty;
|
||||||
player_res_des->Unlock();
|
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
|
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);
|
Tell(role_name);
|
||||||
Warn(" does not exists in resource! ");
|
Warn(" does not exists in resource! ");
|
||||||
damageReceivedModifier = 0.0f;
|
damageReceivedModifier = 0.0f;
|
||||||
|
|||||||
@@ -1118,22 +1118,11 @@ LBE4ControlsManager::~LBE4ControlsManager()
|
|||||||
|
|
||||||
#ifdef BT_GLASS
|
#ifdef BT_GLASS
|
||||||
//
|
//
|
||||||
// Symmetric with the Create above (2026-08-06). The HARDWARE RIO's dtor
|
// Symmetric with the Create above (2026-08-06). ~PadRIO tears the panels
|
||||||
// knows nothing about the panels, and windows that outlive the surfaces
|
// down itself, so this is a no-op there -- but the HARDWARE RIO's dtor
|
||||||
// they blit are a crash waiting for the next mission cycle.
|
// knows nothing about them, and windows that outlive the surfaces they
|
||||||
//
|
// blit are a crash waiting for the next mission cycle. Destroy is safe
|
||||||
// CORRECTION (2026-08-07, #140): the original comment here claimed this was
|
// if they were never created.
|
||||||
// "a no-op" on the PadRIO path because ~PadRIO tears the panels down itself.
|
|
||||||
// It was not. ~PadRIO runs first (delete rioPointer, above), zeroing the
|
|
||||||
// window list -- and BTGlassPanels_Destroy unconditionally ran SaveLayout
|
|
||||||
// BEFORE looking at whether anything was left, so this second call rewrote
|
|
||||||
// glass_layout.cfg with every MFD and radar line missing. Only the plasma
|
|
||||||
// window survived, because external windows cached a last-known rect and the
|
|
||||||
// per-display windows did not. That is the regression testers hit on the
|
|
||||||
// desktop the same day the pod panels were wired up; the pod itself was
|
|
||||||
// unaffected (no PadRIO, so only ONE destroy, and it runs BT_GLASS_LAYOUT=
|
|
||||||
// load anyway). Fixed on both sides in L4GLASSWIN: the geometry is now
|
|
||||||
// remembered across teardown, and the teardown save is guarded.
|
|
||||||
//
|
//
|
||||||
BTGlassPanels_Destroy();
|
BTGlassPanels_Destroy();
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+27
-334
@@ -96,8 +96,6 @@ struct GButton
|
|||||||
|
|
||||||
struct GWin
|
struct GWin
|
||||||
{
|
{
|
||||||
double perfMs; // [glassperf] paint ms this window, this second
|
|
||||||
int perfN; // [glassperf] paints this second
|
|
||||||
const char *title;
|
const char *title;
|
||||||
HWND hwnd;
|
HWND hwnd;
|
||||||
|
|
||||||
@@ -752,70 +750,7 @@ void
|
|||||||
// Heat MFD=monitor:2,bare (index into the enumeration order)
|
// Heat MFD=monitor:2,bare (index into the enumeration order)
|
||||||
// The surface is centred on that monitor; an exact-size panel fills it.
|
// The surface is centred on that monitor; an exact-size panel fills it.
|
||||||
//---------------------------------------------------------------------------
|
//---------------------------------------------------------------------------
|
||||||
struct MonScan { int index; const char *want; const char *wantId; int wantIndex; RECT rect; int found; };
|
struct MonScan { int index; const char *want; int wantIndex; RECT rect; int found; };
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// STABLE PANEL IDENTITY (2026-08-08) -- the fix for "the monitors came back in a
|
|
||||||
// different order after I moved cables and rebooted".
|
|
||||||
//
|
|
||||||
// Windows renumbers \\.\DISPLAYn, and reorders the enumeration, when a panel is
|
|
||||||
// power-cycled or re-cabled -- even when the visible desktop arrangement is
|
|
||||||
// unchanged. So BOTH existing binding forms are boot-fragile: `monitor:2` is an
|
|
||||||
// enumeration index and `monitor:\.\DISPLAY4` is a GDI name Windows reassigns.
|
|
||||||
// (The same trap bites GameOS next door: its -tmon takes DirectDraw device
|
|
||||||
// indices, which are neither Windows monitor numbers nor stable -- and its
|
|
||||||
// NULL-device merge shifts every index down by one on top of that.)
|
|
||||||
//
|
|
||||||
// What IS stable is the panel's own hardware identity. EnumDisplayDevices on a
|
|
||||||
// display's MONITOR child returns a DeviceID like
|
|
||||||
// MONITOR\DEL4231\{4d36e96e-e325-11ce-bfc1-08002be10318}\0002
|
|
||||||
// or, with EDD_GET_DEVICE_INTERFACE_NAME,
|
|
||||||
// \\?\DISPLAY#DEL4231#5&1a2b3c&0&UID4353#{e6f07b5f-...}
|
|
||||||
// The `DEL4231` field is the EDID manufacturer + product code, and the UID/
|
|
||||||
// instance identifies the physical connector. Neither moves on a reboot.
|
|
||||||
//
|
|
||||||
// Fill `out` with that string for a \\.\DISPLAYn adapter name. Empty on failure
|
|
||||||
// (no monitor child / remote session) -- callers must treat "" as "no identity"
|
|
||||||
// and fall back, never as a match.
|
|
||||||
static void
|
|
||||||
MonitorStableId(const char *displayName, char *out, size_t outLen)
|
|
||||||
{
|
|
||||||
if (outLen == 0) return;
|
|
||||||
out[0] = '\0';
|
|
||||||
if (displayName == NULL || displayName[0] == '\0') return;
|
|
||||||
|
|
||||||
DISPLAY_DEVICEA mon;
|
|
||||||
memset(&mon, 0, sizeof(mon));
|
|
||||||
mon.cb = sizeof(mon);
|
|
||||||
// index 0 = the attached monitor child. The interface-name flag gives the
|
|
||||||
// richer path (includes the connector UID); it is Vista+, and if the call
|
|
||||||
// fails we retry without it for the plain MONITOR\... form.
|
|
||||||
if (!EnumDisplayDevicesA(displayName, 0, &mon, EDD_GET_DEVICE_INTERFACE_NAME))
|
|
||||||
{
|
|
||||||
memset(&mon, 0, sizeof(mon));
|
|
||||||
mon.cb = sizeof(mon);
|
|
||||||
if (!EnumDisplayDevicesA(displayName, 0, &mon, 0))
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
strncpy(out, mon.DeviceID, outLen - 1);
|
|
||||||
out[outLen - 1] = '\0';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Case-insensitive substring test -- the cfg quotes a FRAGMENT of the identity
|
|
||||||
// (usually just the EDID code, e.g. `id:DEL4231`) rather than the whole path,
|
|
||||||
// because the full string is long and contains characters a config file and a
|
|
||||||
// shell each mangle differently.
|
|
||||||
static int
|
|
||||||
IdContains(const char *haystack, const char *needle)
|
|
||||||
{
|
|
||||||
if (haystack == NULL || needle == NULL || *needle == '\0') return 0;
|
|
||||||
size_t hl = strlen(haystack), nl = strlen(needle);
|
|
||||||
if (nl > hl) return 0;
|
|
||||||
for (size_t i = 0; i + nl <= hl; ++i)
|
|
||||||
if (_strnicmp(haystack + i, needle, nl) == 0)
|
|
||||||
return 1;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static BOOL CALLBACK
|
static BOOL CALLBACK
|
||||||
MonScanProc(HMONITOR mon, HDC, LPRECT, LPARAM param)
|
MonScanProc(HMONITOR mon, HDC, LPRECT, LPARAM param)
|
||||||
@@ -827,17 +762,7 @@ static BOOL CALLBACK
|
|||||||
if (GetMonitorInfoA(mon, (MONITORINFO *)&mi))
|
if (GetMonitorInfoA(mon, (MONITORINFO *)&mi))
|
||||||
{
|
{
|
||||||
int hit = 0;
|
int hit = 0;
|
||||||
// `id:<fragment>` -- match the panel's HARDWARE identity, not its
|
if (sc->want != NULL)
|
||||||
// current \\.\DISPLAYn or enumeration slot. Boot-stable; this is the
|
|
||||||
// form the pod should use.
|
|
||||||
if (sc->wantId != NULL)
|
|
||||||
{
|
|
||||||
char sid[256];
|
|
||||||
MonitorStableId(mi.szDevice, sid, sizeof(sid));
|
|
||||||
if (sid[0] != '\0' && IdContains(sid, sc->wantId))
|
|
||||||
hit = 1;
|
|
||||||
}
|
|
||||||
if (!hit && sc->want != NULL)
|
|
||||||
{
|
{
|
||||||
// Match the full device name OR just its tail, so a hand-written
|
// Match the full device name OR just its tail, so a hand-written
|
||||||
// cfg can say `monitor:DISPLAY4` and skip the \\.\ prefix entirely
|
// cfg can say `monitor:DISPLAY4` and skip the \\.\ prefix entirely
|
||||||
@@ -862,96 +787,6 @@ static BOOL CALLBACK
|
|||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// BT_GLASS_IDS=1 -- dump every attached panel with its BOOT-STABLE identity, and
|
|
||||||
// a ready-to-paste `monitor:id:` fragment for glass_layout.cfg. This is the
|
|
||||||
// discovery half of the fix: run it once on the pod, copy the fragments into the
|
|
||||||
// cfg, and the assignment survives reboots and re-cabling.
|
|
||||||
//
|
|
||||||
// Prints the volatile identifiers too, deliberately side by side -- seeing
|
|
||||||
// \\.\DISPLAYn and the enumeration index MOVE between two runs while the id
|
|
||||||
// stays put is the proof that the id form is the right one.
|
|
||||||
struct MonDump { int index; };
|
|
||||||
|
|
||||||
static BOOL CALLBACK
|
|
||||||
MonDumpProc(HMONITOR mon, HDC, LPRECT, LPARAM param)
|
|
||||||
{
|
|
||||||
MonDump *d = (MonDump *)param;
|
|
||||||
MONITORINFOEXA mi;
|
|
||||||
memset(&mi, 0, sizeof(mi));
|
|
||||||
mi.cbSize = sizeof(mi);
|
|
||||||
if (GetMonitorInfoA(mon, (MONITORINFO *)&mi))
|
|
||||||
{
|
|
||||||
char sid[256];
|
|
||||||
MonitorStableId(mi.szDevice, sid, sizeof(sid));
|
|
||||||
|
|
||||||
// The EDID make+product sits between the first two separators of the
|
|
||||||
// DeviceID (MONITOR\DEL4231\... or \\?\DISPLAY#DEL4231#...). Offer it
|
|
||||||
// as the suggested fragment -- short, and unique when the panels are
|
|
||||||
// different models. Identical models need more (see the note below).
|
|
||||||
// Find it STRUCTURALLY rather than by position: an EDID PnP code is
|
|
||||||
// exactly 3 letters + 4 hex digits (AUO10ED, DEL4231). Walking the
|
|
||||||
// separators positionally broke on the interface-name form, whose
|
|
||||||
// `\\?\DISPLAY#...` prefix has a different number of leading segments
|
|
||||||
// than the plain `MONITOR\...` form -- and which form you get depends on
|
|
||||||
// whether EDD_GET_DEVICE_INTERFACE_NAME succeeded. Tokenising on all of
|
|
||||||
// \ # ? handles every variant the same way.
|
|
||||||
char frag[64];
|
|
||||||
frag[0] = '\0';
|
|
||||||
for (const char *t = sid; *t != '\0'; )
|
|
||||||
{
|
|
||||||
while (*t == '\\' || *t == '#' || *t == '?') ++t;
|
|
||||||
const char *e = t;
|
|
||||||
while (*e != '\0' && *e != '\\' && *e != '#' && *e != '?') ++e;
|
|
||||||
size_t n = (size_t)(e - t);
|
|
||||||
if (n == 7)
|
|
||||||
{
|
|
||||||
int ok = 1;
|
|
||||||
for (int i = 0; i < 3 && ok; ++i)
|
|
||||||
if (!isalpha((unsigned char)t[i])) ok = 0;
|
|
||||||
for (int i = 3; i < 7 && ok; ++i)
|
|
||||||
if (!isxdigit((unsigned char)t[i])) ok = 0;
|
|
||||||
if (ok)
|
|
||||||
{
|
|
||||||
memcpy(frag, t, 7);
|
|
||||||
frag[7] = '\0';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
t = e;
|
|
||||||
}
|
|
||||||
|
|
||||||
DEBUG_STREAM << "[glassid] index=" << d->index
|
|
||||||
<< " device=" << mi.szDevice
|
|
||||||
<< (((mi.dwFlags & MONITORINFOF_PRIMARY) != 0) ? " PRIMARY" : "")
|
|
||||||
<< " rect=" << (int)mi.rcMonitor.left << "," << (int)mi.rcMonitor.top
|
|
||||||
<< " " << (int)(mi.rcMonitor.right - mi.rcMonitor.left)
|
|
||||||
<< "x" << (int)(mi.rcMonitor.bottom - mi.rcMonitor.top)
|
|
||||||
<< "\n stable-id = " << (sid[0] ? sid : "(unavailable)")
|
|
||||||
<< "\n cfg form = monitor:id:" << (frag[0] ? frag : "<see stable-id>")
|
|
||||||
<< "\n" << std::flush;
|
|
||||||
}
|
|
||||||
++d->index;
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
BTGlassDumpMonitorIds()
|
|
||||||
{
|
|
||||||
if (getenv("BT_GLASS_IDS") == NULL)
|
|
||||||
return;
|
|
||||||
DEBUG_STREAM << "[glassid] ---- attached panels, boot-STABLE identities ----\n"
|
|
||||||
<< "[glassid] index and device= move across reboots / power-cycles;\n"
|
|
||||||
<< "[glassid] stable-id does not. Bind the pod with monitor:id:<fragment>.\n"
|
|
||||||
<< "[glassid] If two panels are the SAME MODEL their EDID codes match --\n"
|
|
||||||
<< "[glassid] use a longer fragment from stable-id (the UID/instance tail\n"
|
|
||||||
<< "[glassid] differs per connector) so each line matches exactly one.\n"
|
|
||||||
<< std::flush;
|
|
||||||
MonDump d;
|
|
||||||
d.index = 0;
|
|
||||||
EnumDisplayMonitors(NULL, NULL, MonDumpProc, (LPARAM)&d);
|
|
||||||
DEBUG_STREAM << "[glassid] ---- " << d.index << " panel(s) ----\n" << std::flush;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve "monitor:<spec>" to a rect. Returns 1 on success.
|
// Resolve "monitor:<spec>" to a rect. Returns 1 on success.
|
||||||
static int
|
static int
|
||||||
ResolveMonitorSpec(const char *spec, RECT *out)
|
ResolveMonitorSpec(const char *spec, RECT *out)
|
||||||
@@ -960,31 +795,13 @@ static int
|
|||||||
MonScan sc;
|
MonScan sc;
|
||||||
memset(&sc, 0, sizeof(sc));
|
memset(&sc, 0, sizeof(sc));
|
||||||
sc.wantIndex = -1;
|
sc.wantIndex = -1;
|
||||||
// `monitor:id:<fragment>` -- BOOT-STABLE hardware identity (preferred on the
|
if (spec[0] >= '0' && spec[0] <= '9')
|
||||||
// pod). Anything else keeps its historic meaning exactly: a leading digit is
|
|
||||||
// the enumeration index, otherwise a \\.\DISPLAYn device name (or its tail).
|
|
||||||
if (_strnicmp(spec, "id:", 3) == 0)
|
|
||||||
{
|
|
||||||
sc.wantId = spec + 3;
|
|
||||||
while (*sc.wantId == ' ' || *sc.wantId == '\t') ++sc.wantId;
|
|
||||||
}
|
|
||||||
else if (spec[0] >= '0' && spec[0] <= '9')
|
|
||||||
sc.wantIndex = atoi(spec);
|
sc.wantIndex = atoi(spec);
|
||||||
else
|
else
|
||||||
sc.want = spec;
|
sc.want = spec;
|
||||||
EnumDisplayMonitors(NULL, NULL, MonScanProc, (LPARAM)&sc);
|
EnumDisplayMonitors(NULL, NULL, MonScanProc, (LPARAM)&sc);
|
||||||
if (sc.found)
|
if (sc.found)
|
||||||
*out = sc.rect;
|
*out = sc.rect;
|
||||||
else if (sc.wantId != NULL)
|
|
||||||
{
|
|
||||||
// An id: binding that matched nothing is worth shouting about: the panel
|
|
||||||
// is unplugged, asleep, or the cfg fragment is wrong. Silently falling
|
|
||||||
// back to computed placement would put a picture on the wrong glass and
|
|
||||||
// look like the very bug this form exists to prevent.
|
|
||||||
DEBUG_STREAM << "[glasswin] monitor id '" << sc.wantId
|
|
||||||
<< "' matched NO attached panel -- check BT_GLASS_IDS=1 output"
|
|
||||||
<< "\n" << std::flush;
|
|
||||||
}
|
|
||||||
return sc.found;
|
return sc.found;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1105,50 +922,9 @@ static void
|
|||||||
<< layoutFileName << "\n" << std::flush;
|
<< layoutFileName << "\n" << std::flush;
|
||||||
}
|
}
|
||||||
|
|
||||||
// LAST-KNOWN GEOMETRY, kept ACROSS teardown (2026-08-07, #140).
|
// Write every window's current on-screen frame rect. Whole-file rewrite (it is
|
||||||
//
|
// tiny), so partial/hard kills never leave a half-written file for long. Called
|
||||||
// SaveLayout rewrites the whole file, so it can only be as complete as what it
|
// on finished-drag and on teardown in save mode.
|
||||||
// can see -- and it could only see LIVE windows. Destroy() nulls every hwnd and
|
|
||||||
// zeroes gWinCount, so any save that ran after a teardown wrote a file with
|
|
||||||
// every MFD and radar line MISSING. The plasma window survived that because
|
|
||||||
// external windows already cached a last-known rect (gExtern[].haveLast); the
|
|
||||||
// per-display windows had no such cache, which is exactly the reported
|
|
||||||
// signature: "all the MFDs and secondary lines missing, but plasma was still
|
|
||||||
// there". Give the glass windows the same guarantee, so the file is monotonic
|
|
||||||
// -- a save can update a line or add one, never drop one.
|
|
||||||
struct SavedGeom
|
|
||||||
{
|
|
||||||
char title[64];
|
|
||||||
RECT r;
|
|
||||||
int noFrame;
|
|
||||||
};
|
|
||||||
static SavedGeom gLastGeom[16];
|
|
||||||
static int gLastGeomCount = 0;
|
|
||||||
|
|
||||||
static void
|
|
||||||
RememberGeom(const char *title, const RECT &r, int noFrame)
|
|
||||||
{
|
|
||||||
if (title == NULL || title[0] == '\0')
|
|
||||||
return;
|
|
||||||
for (int i = 0; i < gLastGeomCount; ++i)
|
|
||||||
if (strcmp(gLastGeom[i].title, title) == 0)
|
|
||||||
{
|
|
||||||
gLastGeom[i].r = r; gLastGeom[i].noFrame = noFrame;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (gLastGeomCount >= (int)(sizeof(gLastGeom) / sizeof(gLastGeom[0])))
|
|
||||||
return;
|
|
||||||
SavedGeom &g = gLastGeom[gLastGeomCount++];
|
|
||||||
strncpy(g.title, title, sizeof(g.title) - 1);
|
|
||||||
g.title[sizeof(g.title) - 1] = '\0';
|
|
||||||
g.r = r; g.noFrame = noFrame;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write every window's frame rect. Whole-file rewrite (it is tiny), so
|
|
||||||
// partial/hard kills never leave a half-written file for long. Called on
|
|
||||||
// finished-drag and on teardown in save mode. Live windows refresh the
|
|
||||||
// remembered geometry first; the FILE is then written from the remembered set,
|
|
||||||
// so a window that has already been torn down keeps its line.
|
|
||||||
static void
|
static void
|
||||||
SaveLayout()
|
SaveLayout()
|
||||||
{
|
{
|
||||||
@@ -1175,7 +951,6 @@ static void
|
|||||||
"# this list too -- it can be dragged, remembered and set ,noframe.\n",
|
"# this list too -- it can be dragged, remembered and set ,noframe.\n",
|
||||||
f);
|
f);
|
||||||
int wrote = 0;
|
int wrote = 0;
|
||||||
// 1. refresh the remembered geometry from whatever is currently alive
|
|
||||||
for (int i = 0; i < gWinCount; ++i)
|
for (int i = 0; i < gWinCount; ++i)
|
||||||
{
|
{
|
||||||
GWin &gw = gWins[i];
|
GWin &gw = gWins[i];
|
||||||
@@ -1184,16 +959,10 @@ static void
|
|||||||
RECT r;
|
RECT r;
|
||||||
if (!GetWindowRect(gw.hwnd, &r))
|
if (!GetWindowRect(gw.hwnd, &r))
|
||||||
continue;
|
continue;
|
||||||
RememberGeom(gw.title, r, gw.noFrame); // keep the hand-added option
|
fprintf(f, "%s=%ld,%ld,%ld,%ld%s\n", gw.title,
|
||||||
}
|
(long)r.left, (long)r.top,
|
||||||
// 2. write the remembered set -- including windows already torn down
|
(long)(r.right - r.left), (long)(r.bottom - r.top),
|
||||||
for (int i = 0; i < gLastGeomCount; ++i)
|
gw.noFrame ? ",noframe" : ""); // keep the hand-added option
|
||||||
{
|
|
||||||
const SavedGeom &g = gLastGeom[i];
|
|
||||||
fprintf(f, "%s=%ld,%ld,%ld,%ld%s\n", g.title,
|
|
||||||
(long)g.r.left, (long)g.r.top,
|
|
||||||
(long)(g.r.right - g.r.left), (long)(g.r.bottom - g.r.top),
|
|
||||||
g.noFrame ? ",noframe" : "");
|
|
||||||
++wrote;
|
++wrote;
|
||||||
}
|
}
|
||||||
// External windows (the plasma window) ride the same file. Cache the last
|
// External windows (the plasma window) ride the same file. Cache the last
|
||||||
@@ -1219,15 +988,8 @@ static void
|
|||||||
++wrote;
|
++wrote;
|
||||||
}
|
}
|
||||||
fclose(f);
|
fclose(f);
|
||||||
// #140 receipt (ungated): `live=` is the diagnostic that matters. A save
|
|
||||||
// that runs with live=0 is the corruption case -- before the remembered-
|
|
||||||
// geometry cache it wrote a file containing ONLY the plasma line, which is
|
|
||||||
// what testers reported. It stays in the log so the FIELD can tell us which
|
|
||||||
// caller does that (teardown ordering, a drag after a round boundary, ...),
|
|
||||||
// which no bench here managed to reach.
|
|
||||||
DEBUG_STREAM << "[glasswin] saved " << wrote << " window position(s) to "
|
DEBUG_STREAM << "[glasswin] saved " << wrote << " window position(s) to "
|
||||||
<< layoutFileName << " (live=" << gWinCount
|
<< layoutFileName << "\n" << std::flush;
|
||||||
<< " remembered=" << gLastGeomCount << ")\n" << std::flush;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public save trigger for registered external windows -- their WndProc calls
|
// Public save trigger for registered external windows -- their WndProc calls
|
||||||
@@ -1712,20 +1474,7 @@ static LRESULT CALLBACK
|
|||||||
switch (message)
|
switch (message)
|
||||||
{
|
{
|
||||||
case WM_PAINT:
|
case WM_PAINT:
|
||||||
if (w != NULL)
|
if (w != NULL) { PaintGlass(window, w); return 0; }
|
||||||
{
|
|
||||||
// [glassperf] (#149): time EVERY panel paint. The blit is a
|
|
||||||
// HALFTONE StretchDIBits whose cost is strongly driver-dependent
|
|
||||||
// -- the whole point is to measure it on the machine that pays it.
|
|
||||||
LARGE_INTEGER t0, t1, fq;
|
|
||||||
QueryPerformanceCounter(&t0);
|
|
||||||
PaintGlass(window, w);
|
|
||||||
QueryPerformanceCounter(&t1);
|
|
||||||
QueryPerformanceFrequency(&fq);
|
|
||||||
w->perfMs += 1000.0 * (double)(t1.QuadPart - t0.QuadPart) / (double)fq.QuadPart;
|
|
||||||
++w->perfN;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WM_ERASEBKGND:
|
case WM_ERASEBKGND:
|
||||||
@@ -1827,11 +1576,6 @@ void
|
|||||||
if (gWinCount != 0)
|
if (gWinCount != 0)
|
||||||
return; // already up
|
return; // already up
|
||||||
|
|
||||||
// BT_GLASS_IDS=1: dump every panel's boot-stable identity before any window
|
|
||||||
// is placed, so the log shows what the cfg COULD bind to right next to what
|
|
||||||
// it actually did. No-op without the env -- playtesters see nothing new.
|
|
||||||
BTGlassDumpMonitorIds();
|
|
||||||
|
|
||||||
// BT_GAUGE_SEC_ROT: how far to turn the secondary/radar surface.
|
// BT_GAUGE_SEC_ROT: how far to turn the secondary/radar surface.
|
||||||
// 0 = none, 1 = 90 CCW, 2 = 180, 3 = 90 CW (default -- the pod's
|
// 0 = none, 1 = 90 CCW, 2 = 180, 3 = 90 CW (default -- the pod's
|
||||||
// portrait CRT, user-verified upright).
|
// portrait CRT, user-verified upright).
|
||||||
@@ -2012,24 +1756,8 @@ void
|
|||||||
void
|
void
|
||||||
BTGlassPanels_Destroy()
|
BTGlassPanels_Destroy()
|
||||||
{
|
{
|
||||||
// #140 receipt (ungated, one line per teardown): this function has two
|
SaveLayout(); // backstop for a clean teardown (WM_EXITSIZEMOVE already
|
||||||
// callers on the desktop path and the ORDER is what broke the layout file.
|
// caught every finished drag); no-op unless mode==save
|
||||||
// A run that shows `entry #2 windows=0` is the double-destroy, on the
|
|
||||||
// record, without needing to catch the cfg mid-corruption.
|
|
||||||
{
|
|
||||||
static int s_destroyN = 0;
|
|
||||||
DEBUG_STREAM << "[glasswin] destroy entry #" << ++s_destroyN
|
|
||||||
<< " windows=" << gWinCount << "\n" << std::flush;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backstop for a clean teardown (WM_EXITSIZEMOVE already caught every
|
|
||||||
// finished drag); no-op unless mode==save. GUARDED on there being windows:
|
|
||||||
// this function has TWO callers (~PadRIO and ~LBE4ControlsManager), so on
|
|
||||||
// the desktop path it runs twice, and the second pass has nothing live to
|
|
||||||
// report. The remembered-geometry cache above already makes that harmless,
|
|
||||||
// but there is no reason to rewrite the file to say the same thing.
|
|
||||||
if (gWinCount > 0)
|
|
||||||
SaveLayout();
|
|
||||||
|
|
||||||
for (int i = 0; i < gWinCount; ++i)
|
for (int i = 0; i < gWinCount; ++i)
|
||||||
{
|
{
|
||||||
@@ -2104,7 +1832,19 @@ static unsigned long
|
|||||||
if (svga == NULL) svga = static_cast<SVGA16*>(p->graphicsDisplay);
|
if (svga == NULL) svga = static_cast<SVGA16*>(p->graphicsDisplay);
|
||||||
}
|
}
|
||||||
if (svga != NULL && combined != 0)
|
if (svga != NULL && combined != 0)
|
||||||
|
{
|
||||||
token ^= svga->PlaneChecksum(combined);
|
token ^= svga->PlaneChecksum(combined);
|
||||||
|
// PALETTE-ANIMATED content (2026-08-10): a palette-expanding window
|
||||||
|
// (the radar; MFDs under BT_GLASS_MFD_PAL) changes colour with ZERO
|
||||||
|
// pixel writes -- the ColorMapper family (armor rosette tints, the
|
||||||
|
// adpal/adpal2 damage flash) writes CLUT entries. Fold the palette
|
||||||
|
// write generation in so those changes repaint; mono-tint windows
|
||||||
|
// don't read the palette and keep their pixel-only token.
|
||||||
|
static int sPalTok = -1;
|
||||||
|
if (sPalTok < 0) sPalTok = getenv("BT_GLASS_MFD_PAL") ? 1 : 0;
|
||||||
|
if (w->monoTint < 0 || sPalTok)
|
||||||
|
token = (token ^ svga->PaletteGeneration()) * 16777619UL;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lamps: each button's RENDERED brightness + held/latched, so a flash toggle or a
|
// Lamps: each button's RENDERED brightness + held/latched, so a flash toggle or a
|
||||||
@@ -2121,36 +1861,18 @@ static unsigned long
|
|||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
// [rio] health printer (L4RIO.cpp): the serial poll path only COUNTS (kept
|
|
||||||
// I/O-free by design; the 2026-08-10 wedges once blamed on prints there were
|
|
||||||
// really the USB-serial adapter hanging an unbounded overlapped write --
|
|
||||||
// see PCSerialPacket::SendPacket); the report rides this tick, which
|
|
||||||
// already streams [glassperf] every second.
|
|
||||||
extern "C" void BTRioHealthReport();
|
|
||||||
|
|
||||||
void
|
void
|
||||||
BTGlassPanels_Tick()
|
BTGlassPanels_Tick()
|
||||||
{
|
{
|
||||||
if (gWinCount == 0)
|
if (gWinCount == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
BTRioHealthReport();
|
|
||||||
|
|
||||||
static unsigned long sLastPaint = 0;
|
static unsigned long sLastPaint = 0;
|
||||||
unsigned long now = GetTickCount();
|
unsigned long now = GetTickCount();
|
||||||
if (now - sLastPaint < (unsigned long)RepaintMilliseconds)
|
if (now - sLastPaint < (unsigned long)RepaintMilliseconds)
|
||||||
return;
|
return;
|
||||||
sLastPaint = now;
|
sLastPaint = now;
|
||||||
|
|
||||||
// MERGED: Cyd's dirty-skip (glass-panel-perf) + the #149 [glassperf]
|
|
||||||
// telemetry. BT_GLASS_SWEEP=1 bypasses the skip entirely -- the legacy
|
|
||||||
// always-repaint escape hatch, kept so the field can A/B in one env var.
|
|
||||||
static const int sGlassPerf =
|
|
||||||
!(getenv("BT_PERF_LOG") && *getenv("BT_PERF_LOG") == '0');
|
|
||||||
static const int sLegacySweep =
|
|
||||||
(getenv("BT_GLASS_SWEEP") && *getenv("BT_GLASS_SWEEP") == '1');
|
|
||||||
LARGE_INTEGER gt0, gt1, gfq;
|
|
||||||
QueryPerformanceCounter(>0);
|
|
||||||
GaugeRenderer *gr = BTResolveGaugeRenderer();
|
GaugeRenderer *gr = BTResolveGaugeRenderer();
|
||||||
int repainted = 0;
|
int repainted = 0;
|
||||||
for (int i = 0; i < gWinCount; ++i)
|
for (int i = 0; i < gWinCount; ++i)
|
||||||
@@ -2159,7 +1881,7 @@ void
|
|||||||
if (w.hwnd == NULL)
|
if (w.hwnd == NULL)
|
||||||
continue;
|
continue;
|
||||||
unsigned long token = GlassWindowToken(gr, &w, now);
|
unsigned long token = GlassWindowToken(gr, &w, now);
|
||||||
if (!sLegacySweep && w.haveToken && token == w.lastToken)
|
if (w.haveToken && token == w.lastToken)
|
||||||
continue; // gauges + lamps unchanged -> skip this window
|
continue; // gauges + lamps unchanged -> skip this window
|
||||||
w.lastToken = token;
|
w.lastToken = token;
|
||||||
w.haveToken = 1;
|
w.haveToken = 1;
|
||||||
@@ -2185,33 +1907,4 @@ void
|
|||||||
sWin = now; sPumps = 0; sPaints = 0;
|
sWin = now; sPumps = 0; sPaints = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
QueryPerformanceCounter(>1);
|
|
||||||
QueryPerformanceFrequency(&gfq);
|
|
||||||
|
|
||||||
// [glassperf] (#149): one line per second -- the tick's total synchronous
|
|
||||||
// cost plus each window's own paint time. This runs INSIDE the render
|
|
||||||
// frame (L4VIDEO calls the tick), so on a machine where GDI serialises
|
|
||||||
// against D3D present, tickMs IS the per-frame tax and the per-window
|
|
||||||
// split names the guilty panel. Default ON like [segperf]; BT_PERF_LOG=0
|
|
||||||
// opts out.
|
|
||||||
static double sTickMs = 0.0; static int sTicks = 0; static int sPaintAcc = 0;
|
|
||||||
sPaintAcc += repainted;
|
|
||||||
static unsigned long sLastReport = 0;
|
|
||||||
sTickMs += 1000.0 * (double)(gt1.QuadPart - gt0.QuadPart) / (double)gfq.QuadPart;
|
|
||||||
++sTicks;
|
|
||||||
if (sGlassPerf && now - sLastReport >= 1000)
|
|
||||||
{
|
|
||||||
sLastReport = now;
|
|
||||||
DEBUG_STREAM << "[glassperf] ticks=" << sTicks << " tickMs=" << sTickMs
|
|
||||||
<< " paints=" << sPaintAcc;
|
|
||||||
for (int i = 0; i < gWinCount; ++i)
|
|
||||||
{
|
|
||||||
if (gWins[i].perfN > 0)
|
|
||||||
DEBUG_STREAM << " | " << gWins[i].title
|
|
||||||
<< " n=" << gWins[i].perfN << " ms=" << gWins[i].perfMs;
|
|
||||||
gWins[i].perfMs = 0.0; gWins[i].perfN = 0;
|
|
||||||
}
|
|
||||||
DEBUG_STREAM << "\n" << std::flush;
|
|
||||||
sTickMs = 0.0; sTicks = 0; sPaintAcc = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ int gBTPadViewToggleEdges = 0;
|
|||||||
// desktop bridge, which owns `reverseThrust` (mapper attr 6 @0x124) every frame.
|
// desktop bridge, which owns `reverseThrust` (mapper attr 6 @0x124) every frame.
|
||||||
//
|
//
|
||||||
int gBTReverseHeld = 0;
|
int gBTReverseHeld = 0;
|
||||||
int gBTTorsoCenterHeld = 0; // button 0x42 hold (#152; same seam as 0x3F)
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// The desktop per-MFD preset-page cycle edges (J/K/L -> Mfd1/2/3), consumed
|
// The desktop per-MFD preset-page cycle edges (J/K/L -> Mfd1/2/3), consumed
|
||||||
@@ -459,17 +458,6 @@ void
|
|||||||
// via SetScreenButton), so the desktop bridge can honour the button exactly
|
// via SetScreenButton), so the desktop bridge can honour the button exactly
|
||||||
// like the pod's RIO board did.
|
// like the pod's RIO board did.
|
||||||
//
|
//
|
||||||
// TORSO CENTER (pod button 0x42, 'the shipped .RES name' -- UP arrow via
|
|
||||||
// bindings.txt). Same chokepoint pattern as 0x3F below: publish the HOLD
|
|
||||||
// state so the mapper's unified recenter writer (#152) can honour it on
|
|
||||||
// every rig. Before this, no RIO/glass path reached centerCommand at all
|
|
||||||
// -- bench: two scripted 0x42 holds, ctrCmd=0 throughout.
|
|
||||||
if (address == 0x42)
|
|
||||||
{
|
|
||||||
extern int gBTTorsoCenterHeld;
|
|
||||||
gBTTorsoCenterHeld = pressed ? 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (address == 0x3F)
|
if (address == 0x3F)
|
||||||
{
|
{
|
||||||
gBTReverseHeld = pressed ? 1 : 0;
|
gBTReverseHeld = pressed ? 1 : 0;
|
||||||
|
|||||||
@@ -410,20 +410,6 @@ int PCSerialPacket::ReceivePacket(BYTE *destPtr) // returns len (zero if none)
|
|||||||
// Returns:
|
// Returns:
|
||||||
// void
|
// void
|
||||||
//-------------------------------------------------------------------------
|
//-------------------------------------------------------------------------
|
||||||
//
|
|
||||||
// Serial-write health (see the bounded wait below): after a write timeout
|
|
||||||
// the link is SICK for 3 s and writes fast-drop -- one stall, not one per
|
|
||||||
// poll. BTPcspakTxTimeouts feeds the [rio] telemetry.
|
|
||||||
//
|
|
||||||
static unsigned gPcspakTxTimeout = 0;
|
|
||||||
static DWORD gPcspakTxSickUntil = 0;
|
|
||||||
|
|
||||||
extern "C" unsigned
|
|
||||||
BTPcspakTxTimeouts()
|
|
||||||
{
|
|
||||||
return gPcspakTxTimeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
void PCSerialPacket::SendPacket(BYTE *srcPtr) //sends packet
|
void PCSerialPacket::SendPacket(BYTE *srcPtr) //sends packet
|
||||||
{
|
{
|
||||||
//--------------------------------------------------------------
|
//--------------------------------------------------------------
|
||||||
@@ -432,11 +418,6 @@ void PCSerialPacket::SendPacket(BYTE *srcPtr) //sends packet
|
|||||||
if(!enabled) //if disabled, don't even try
|
if(!enabled) //if disabled, don't even try
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (gPcspakTxSickUntil != 0 && GetTickCount() < gPcspakTxSickUntil)
|
|
||||||
{
|
|
||||||
return; // adapter sick: drop, don't stall
|
|
||||||
}
|
|
||||||
|
|
||||||
BYTE cmd = *srcPtr; //get the command byte
|
BYTE cmd = *srcPtr; //get the command byte
|
||||||
cmd &= 0x7F; //remove MSB
|
cmd &= 0x7F; //remove MSB
|
||||||
int length = txLengthPtr[cmd]; //get the length in al
|
int length = txLengthPtr[cmd]; //get the length in al
|
||||||
@@ -478,37 +459,11 @@ void PCSerialPacket::SendPacket(BYTE *srcPtr) //sends packet
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//
|
DWORD bytesWritten;
|
||||||
// BOUNDED completion wait (2026-08-10, ALPHA-MR field minidump). The
|
GetOverlappedResult(hComm, &overlapSend, &bytesWritten, TRUE);
|
||||||
// RIO's "COM1" is a USB-serial adapter; when its USB link drops with a
|
if(bytesWritten != length + 1)
|
||||||
// write in flight the overlapped write NEVER completes, and the original
|
|
||||||
// GetOverlappedResult(..., TRUE) parked the whole game at 0 CPU forever
|
|
||||||
// (cdb-proven: NtWaitForSingleObject <- GetOverlappedResult <-
|
|
||||||
// SendPacket <- RIO ctor). Now: 500 ms bound (9600 baud needs ~21 ms),
|
|
||||||
// CancelIo on expiry, and a 3 s SICK window that fast-drops writes so a
|
|
||||||
// dead adapter costs one stall, not one per 50 ms poll -- the game stays
|
|
||||||
// at frame rate with a degraded RIO and the keyboard fallback alive.
|
|
||||||
// gPcspakTxTimeout feeds the [rio] health line (txTO=).
|
|
||||||
//
|
|
||||||
DWORD bytesWritten = 0;
|
|
||||||
DWORD wait_result = WaitForSingleObject(overlapSend.hEvent, 500);
|
|
||||||
if (wait_result == WAIT_OBJECT_0)
|
|
||||||
{
|
{
|
||||||
GetOverlappedResult(hComm, &overlapSend, &bytesWritten, FALSE);
|
DEBUG_STREAM << "Not all data was written to serial port!\n" << std::flush;
|
||||||
gPcspakTxSickUntil = 0;
|
|
||||||
if (bytesWritten != (DWORD)(length + 1))
|
|
||||||
{
|
|
||||||
DEBUG_STREAM << "Not all data was written to serial port!\n"
|
|
||||||
<< std::flush;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// No print here -- serial-path discipline; the counter surfaces in
|
|
||||||
// the [rio] health line (txTO=) from the glass-tick printer.
|
|
||||||
CancelIo(hComm);
|
|
||||||
++gPcspakTxTimeout;
|
|
||||||
gPcspakTxSickUntil = GetTickCount() + 3000;
|
|
||||||
}
|
}
|
||||||
delete buffer;
|
delete buffer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,11 +25,7 @@ static LRESULT CALLBACK
|
|||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
case WM_CLOSE:
|
case WM_CLOSE:
|
||||||
// Minimize, never hide (#71): SW_HIDE left no way back short of a
|
ShowWindow(window, SW_HIDE); // hide only; the renderer owns it
|
||||||
// restart. A minimized window keeps its taskbar button (both framed
|
|
||||||
// and ,noframe styles), so the player can recover the display. The
|
|
||||||
// renderer still owns the window either way.
|
|
||||||
ShowWindow(window, SW_MINIMIZE);
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return DefWindowProcW(window, message, wparam, lparam);
|
return DefWindowProcW(window, message, wparam, lparam);
|
||||||
|
|||||||
@@ -1240,105 +1240,6 @@ RIO::~RIO()
|
|||||||
Check_Fpu();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// [rio] telemetry state (see the tracking block in GetNextEvent). File-scope
|
|
||||||
// statics on purpose: one hardware RIO per process, no header/layout churn --
|
|
||||||
// and the PRINTER (BTRioHealthReport, called from the glass tick) reads ONLY
|
|
||||||
// these statics, never RIO members, so it needs no instance and touches
|
|
||||||
// nothing the serial layer locks.
|
|
||||||
//
|
|
||||||
static int gRioPresent = 0;
|
|
||||||
static unsigned gRioReqCount = 0;
|
|
||||||
static unsigned gRioRepCount = 0;
|
|
||||||
static unsigned gRioBtnCount = 0;
|
|
||||||
static int gRioReqPending = 0;
|
|
||||||
static DWORD gRioLastReqTick = 0;
|
|
||||||
static DWORD gRioLastReplyTick = 0;
|
|
||||||
static int gRioStallActive = 0;
|
|
||||||
static DWORD gRioStallStart = 0;
|
|
||||||
static unsigned gRioStallBtnBase = 0;
|
|
||||||
static unsigned gRioStallCount = 0;
|
|
||||||
static DWORD gRioLastStallMs = 0;
|
|
||||||
static unsigned gRioLastStallBtns = 0;
|
|
||||||
static unsigned gRioLineErrMirror = 0;
|
|
||||||
static unsigned gRioOverrunMirror = 0;
|
|
||||||
static unsigned gRioAbandonMirror = 0;
|
|
||||||
static unsigned gRioRRetryMirror = 0;
|
|
||||||
static unsigned gRioRAbandonMirror = 0;
|
|
||||||
static unsigned gRioRFullMirror = 0;
|
|
||||||
|
|
||||||
extern "C" unsigned BTPcspakTxTimeouts(); // L4PCSPAK.cpp: bounded-write stat
|
|
||||||
|
|
||||||
//
|
|
||||||
// The printer half -- called from BTGlassPanels_Tick beside the [glassperf]
|
|
||||||
// report (a stream site that prints every second without incident), NEVER
|
|
||||||
// from the serial path. Edge lines land at the tick's ~1 s granularity,
|
|
||||||
// ample for the multi-second field dropouts this exists to catch.
|
|
||||||
// BT_RIO_LOG=0 opts out; silent when no hardware RIO ever came up.
|
|
||||||
//
|
|
||||||
extern "C" void
|
|
||||||
BTRioHealthReport()
|
|
||||||
{
|
|
||||||
static int sLog = -1;
|
|
||||||
static DWORD sLastLine = 0;
|
|
||||||
static unsigned sSeenStalls = 0;
|
|
||||||
static int sSawActive = 0;
|
|
||||||
|
|
||||||
if (!gRioPresent)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (sLog < 0)
|
|
||||||
{
|
|
||||||
const char *e = getenv("BT_RIO_LOG");
|
|
||||||
sLog = (e != 0 && *e == '0') ? 0 : 1;
|
|
||||||
}
|
|
||||||
if (!sLog)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DWORD now = GetTickCount();
|
|
||||||
if (gRioStallActive && !sSawActive)
|
|
||||||
{
|
|
||||||
sSawActive = 1;
|
|
||||||
DEBUG_STREAM << "[rio] STALL analog silent "
|
|
||||||
<< (now - gRioStallStart) << "ms and counting req="
|
|
||||||
<< gRioReqCount << " rep=" << gRioRepCount
|
|
||||||
<< "\n" << std::flush;
|
|
||||||
}
|
|
||||||
else if (!gRioStallActive && sSawActive)
|
|
||||||
{
|
|
||||||
sSawActive = 0;
|
|
||||||
}
|
|
||||||
if (sSeenStalls != gRioStallCount)
|
|
||||||
{
|
|
||||||
sSeenStalls = gRioStallCount;
|
|
||||||
DEBUG_STREAM << "[rio] RECOVER stall#" << gRioStallCount
|
|
||||||
<< " lasted " << gRioLastStallMs
|
|
||||||
<< "ms btnDuring=" << gRioLastStallBtns
|
|
||||||
<< "\n" << std::flush;
|
|
||||||
}
|
|
||||||
if (now - sLastLine >= 5000)
|
|
||||||
{
|
|
||||||
sLastLine = now;
|
|
||||||
DEBUG_STREAM << "[rio] req=" << gRioReqCount
|
|
||||||
<< " rep=" << gRioRepCount
|
|
||||||
<< " btn=" << gRioBtnCount
|
|
||||||
<< " age=" << (gRioLastReplyTick != 0
|
|
||||||
? (now - gRioLastReplyTick) : 0)
|
|
||||||
<< "ms stalls=" << gRioStallCount
|
|
||||||
<< " txTO=" << BTPcspakTxTimeouts()
|
|
||||||
<< " lineErr=" << gRioLineErrMirror
|
|
||||||
<< " overrun=" << gRioOverrunMirror
|
|
||||||
<< " abandon=" << gRioAbandonMirror
|
|
||||||
<< " rRetry=" << gRioRRetryMirror
|
|
||||||
<< " rAbandon=" << gRioRAbandonMirror
|
|
||||||
<< " rFull=" << gRioRFullMirror
|
|
||||||
<< "\n" << std::flush;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Logical
|
Logical
|
||||||
RIO::TestInstance() const
|
RIO::TestInstance() const
|
||||||
{
|
{
|
||||||
@@ -1363,36 +1264,6 @@ Logical
|
|||||||
Logical reply, looping;
|
Logical reply, looping;
|
||||||
Word errors;
|
Word errors;
|
||||||
|
|
||||||
//
|
|
||||||
// [rio] link-health TRACKING -- pure arithmetic, no I/O here by design.
|
|
||||||
// CORRECTED HISTORY (2026-08-10): the boot wedges first blamed on prints
|
|
||||||
// from this path were actually the USB-serial adapter dropping its link
|
|
||||||
// with a write in flight -- the unbounded GetOverlappedResult in
|
|
||||||
// PCSerialPacket::SendPacket parked the game (minidump-proven; the
|
|
||||||
// print-deadlock A/B was confounded by the adapter behaving that boot).
|
|
||||||
// The count-here / print-from-the-glass-tick split stays anyway: it
|
|
||||||
// keeps this hot polled path I/O-free and the printer at one known
|
|
||||||
// stream site (BTRioHealthReport below, beside [glassperf]).
|
|
||||||
//
|
|
||||||
if (operational)
|
|
||||||
{
|
|
||||||
DWORD now = GetTickCount();
|
|
||||||
if (!gRioStallActive && gRioReqPending
|
|
||||||
&& now - gRioLastReqTick > 500)
|
|
||||||
{
|
|
||||||
gRioStallActive = 1;
|
|
||||||
gRioStallStart = gRioLastReqTick;
|
|
||||||
gRioStallBtnBase = gRioBtnCount;
|
|
||||||
}
|
|
||||||
gRioPresent = 1;
|
|
||||||
gRioLineErrMirror = (unsigned) lineErrorCount;
|
|
||||||
gRioOverrunMirror = (unsigned) overrunCount;
|
|
||||||
gRioAbandonMirror = (unsigned) abandonCount;
|
|
||||||
gRioRRetryMirror = (unsigned) remoteRetryCount;
|
|
||||||
gRioRAbandonMirror = (unsigned) remoteAbandonCount;
|
|
||||||
gRioRFullMirror = (unsigned) remoteFullBufferCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
//PCSPAKState(this);
|
//PCSPAKState(this);
|
||||||
//
|
//
|
||||||
//cout << flush;
|
//cout << flush;
|
||||||
@@ -1506,20 +1377,6 @@ Logical
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case AnalogReply:
|
case AnalogReply:
|
||||||
++gRioRepCount;
|
|
||||||
gRioReqPending = 0;
|
|
||||||
gRioLastReplyTick = GetTickCount();
|
|
||||||
if (gRioStallActive)
|
|
||||||
{
|
|
||||||
// The reply that ended the drought -- record only; the
|
|
||||||
// glass-tick printer emits the RECOVER line. btnDuring
|
|
||||||
// is the field theory's verdict: >0 on every recover =
|
|
||||||
// button traffic resyncs the link; 0 = it self-heals.
|
|
||||||
gRioStallActive = 0;
|
|
||||||
++gRioStallCount;
|
|
||||||
gRioLastStallMs = gRioLastReplyTick - gRioStallStart;
|
|
||||||
gRioLastStallBtns = gRioBtnCount - gRioStallBtnBase;
|
|
||||||
}
|
|
||||||
destination->Type = RIO::AnalogEvent;
|
destination->Type = RIO::AnalogEvent;
|
||||||
//
|
//
|
||||||
// NOTE: no data is sent in this packet.
|
// NOTE: no data is sent in this packet.
|
||||||
@@ -1566,13 +1423,11 @@ Logical
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case ButtonPressed:
|
case ButtonPressed:
|
||||||
++gRioBtnCount;
|
|
||||||
destination->Type = RIO::ButtonPressedEvent;
|
destination->Type = RIO::ButtonPressedEvent;
|
||||||
destination->Data.Unit = receive_buffer[1];
|
destination->Data.Unit = receive_buffer[1];
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ButtonReleased:
|
case ButtonReleased:
|
||||||
++gRioBtnCount;
|
|
||||||
destination->Type = RIO::ButtonReleasedEvent;
|
destination->Type = RIO::ButtonReleasedEvent;
|
||||||
destination->Data.Unit = receive_buffer[1];
|
destination->Data.Unit = receive_buffer[1];
|
||||||
break;
|
break;
|
||||||
@@ -1713,9 +1568,6 @@ void
|
|||||||
if (operational && !TestModeActive)
|
if (operational && !TestModeActive)
|
||||||
{
|
{
|
||||||
SendPacket((Byte *) request_analog_string);
|
SendPacket((Byte *) request_analog_string);
|
||||||
++gRioReqCount;
|
|
||||||
gRioReqPending = 1;
|
|
||||||
gRioLastReqTick = GetTickCount();
|
|
||||||
}
|
}
|
||||||
Check_Fpu();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5445,6 +5445,7 @@ SVGA16::SVGA16(
|
|||||||
BuildWindows(init_width,init_height,windowed, secondaryIndex, aux1Index, aux2Index);
|
BuildWindows(init_width,init_height,windowed, secondaryIndex, aux1Index, aux2Index);
|
||||||
for (int _i = 0; _i < 10; _i++) // DEV-COMPOSITE: lazily created on first surface draw
|
for (int _i = 0; _i < 10; _i++) // DEV-COMPOSITE: lazily created on first surface draw
|
||||||
mDevSurfaceTex[_i] = NULL;
|
mDevSurfaceTex[_i] = NULL;
|
||||||
|
paletteGeneration = 0; // GLASS dirty-skip palette tracking
|
||||||
//STUBBED: VIDEO RB 1/15/07
|
//STUBBED: VIDEO RB 1/15/07
|
||||||
# if defined(DEBUG)
|
# if defined(DEBUG)
|
||||||
Tell("SVGA16::SVGA16()\n");
|
Tell("SVGA16::SVGA16()\n");
|
||||||
@@ -7190,6 +7191,7 @@ void
|
|||||||
|
|
||||||
svga_palette->paletteData.Valid = True;
|
svga_palette->paletteData.Valid = True;
|
||||||
svga_palette->modified = True;
|
svga_palette->modified = True;
|
||||||
|
((SVGA16 *) graphicsDisplay)->paletteGeneration++; // glass dirty-skip (full rebuild)
|
||||||
|
|
||||||
Check_Fpu();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
@@ -7234,6 +7236,13 @@ void
|
|||||||
SVGA16Palette
|
SVGA16Palette
|
||||||
*svga_palette(&((SVGA16 *) graphicsDisplay)->palette[paletteID]);
|
*svga_palette(&((SVGA16 *) graphicsDisplay)->palette[paletteID]);
|
||||||
|
|
||||||
|
// GLASS dirty-skip: track whether this write CHANGES anything -- the
|
||||||
|
// ColorMapper family calls this every Execute (the adpal/adpal2 flash
|
||||||
|
// alternates palettes even at zero damage, usually writing identical RGB),
|
||||||
|
// so bump the generation only on a real change or every glass window that
|
||||||
|
// palette-expands would repaint every pump for nothing.
|
||||||
|
int palette_changed = 0;
|
||||||
|
|
||||||
//-------------------------------------------
|
//-------------------------------------------
|
||||||
// If any of the ...TransparentZero modes are used,
|
// If any of the ...TransparentZero modes are used,
|
||||||
// leave color zero undefined for this bit group by
|
// leave color zero undefined for this bit group by
|
||||||
@@ -7275,21 +7284,31 @@ void
|
|||||||
{
|
{
|
||||||
case RedChannel:
|
case RedChannel:
|
||||||
case RedChannelTransparentZero:
|
case RedChannelTransparentZero:
|
||||||
|
if (destination_triplet->Red != source_triplet->Red)
|
||||||
|
palette_changed = 1;
|
||||||
destination_triplet->Red = source_triplet->Red;
|
destination_triplet->Red = source_triplet->Red;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case GreenChannel:
|
case GreenChannel:
|
||||||
case GreenChannelTransparentZero:
|
case GreenChannelTransparentZero:
|
||||||
|
if (destination_triplet->Green != source_triplet->Green)
|
||||||
|
palette_changed = 1;
|
||||||
destination_triplet->Green = source_triplet->Green;
|
destination_triplet->Green = source_triplet->Green;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case BlueChannel:
|
case BlueChannel:
|
||||||
case BlueChannelTransparentZero:
|
case BlueChannelTransparentZero:
|
||||||
|
if (destination_triplet->Blue != source_triplet->Blue)
|
||||||
|
palette_changed = 1;
|
||||||
destination_triplet->Blue = source_triplet->Blue;
|
destination_triplet->Blue = source_triplet->Blue;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AllChannels:
|
case AllChannels:
|
||||||
case AllChannelsTransparentZero:
|
case AllChannelsTransparentZero:
|
||||||
|
if (destination_triplet->Red != source_triplet->Red
|
||||||
|
|| destination_triplet->Green != source_triplet->Green
|
||||||
|
|| destination_triplet->Blue != source_triplet->Blue)
|
||||||
|
palette_changed = 1;
|
||||||
*destination_triplet = *source_triplet;
|
*destination_triplet = *source_triplet;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -7310,6 +7329,8 @@ void
|
|||||||
|
|
||||||
svga_palette->paletteData.Valid = True;
|
svga_palette->paletteData.Valid = True;
|
||||||
svga_palette->modified = True;
|
svga_palette->modified = True;
|
||||||
|
if (palette_changed)
|
||||||
|
((SVGA16 *) graphicsDisplay)->paletteGeneration++; // glass dirty-skip
|
||||||
|
|
||||||
Check_Fpu();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
@@ -7413,6 +7434,7 @@ void
|
|||||||
|
|
||||||
svga_palette->paletteData.Valid = True;
|
svga_palette->paletteData.Valid = True;
|
||||||
svga_palette->modified = True;
|
svga_palette->modified = True;
|
||||||
|
((SVGA16 *) graphicsDisplay)->paletteGeneration++; // glass dirty-skip (full rebuild)
|
||||||
|
|
||||||
# if defined(TESTPALETTE)
|
# if defined(TESTPALETTE)
|
||||||
std::cout << "L4GraphicsPort::BuildAuxiliaryPalette for port " <<
|
std::cout << "L4GraphicsPort::BuildAuxiliaryPalette for port " <<
|
||||||
@@ -7523,6 +7545,7 @@ void
|
|||||||
|
|
||||||
svga_palette->paletteData.Valid = True;
|
svga_palette->paletteData.Valid = True;
|
||||||
svga_palette->modified = True;
|
svga_palette->modified = True;
|
||||||
|
((SVGA16 *) graphicsDisplay)->paletteGeneration++; // glass dirty-skip (full rebuild)
|
||||||
|
|
||||||
Check_Fpu();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
@@ -7557,17 +7580,28 @@ void
|
|||||||
// LEFT AS HEAP GARBAGE and any pixmap pixel >= that count wrote the
|
// LEFT AS HEAP GARBAGE and any pixmap pixel >= that count wrote the
|
||||||
// garbage's high bits into other displays' planes (the #48 stray
|
// garbage's high bits into other displays' planes (the #48 stray
|
||||||
// blocks; convicted live by BT_PLANE_AUDIT -- the 480x640 radar
|
// blocks; convicted live by BT_PLANE_AUDIT -- the 480x640 radar
|
||||||
// background carries index 217). Cycle the in-plane pattern across
|
// background carries index 217). Map the remainder to the plane
|
||||||
// the remainder: high-index art degrades to its (index mod 2^bits)
|
// BACKGROUND (translationTable[0]): high-index art renders as the
|
||||||
// colour IN-PLANE, and can never leak. (The 1995 binary shipped the
|
// port's background colour in-plane, and can never leak.
|
||||||
// same 64-entry fill and relied on art discipline; garbage is not a
|
//
|
||||||
// preservable behaviour, so this is a guarded PORT deviation.)
|
// REFINED 2026-08-10 (the sec-surface phantoms): the first #48 fill
|
||||||
|
// cycled the remainder IN-PLANE (index mod 2^bits), which mapped art
|
||||||
|
// index 254 onto plane slot 62 -- a LIVE colorMapperMultiArmor damage
|
||||||
|
// slot. SMODE.PCC's inactive control-mode box interiors and a stray
|
||||||
|
// 52x13 idx-254 bar baked into BTSEC1.PCX (between the heading dial
|
||||||
|
// and the armor rosette) lit up in the current right-armor damage
|
||||||
|
// colour on every render path. On the shipped machine those regions
|
||||||
|
// rendered BLACK (the heap garbage's low plane bits were 0 ->
|
||||||
|
// in-plane index 0), so background IS the authentic on-screen result.
|
||||||
|
// (The 1995 binary shipped the same 64-entry fill and relied on art
|
||||||
|
// discipline; garbage is not a preservable behaviour, so this stays a
|
||||||
|
// guarded PORT deviation.)
|
||||||
{
|
{
|
||||||
int filled = 1;
|
int filled = 1;
|
||||||
for (int b = bitMask & 0xFF; b != 0; b &= (b - 1))
|
for (int b = bitMask & 0xFF; b != 0; b &= (b - 1))
|
||||||
filled <<= 1;
|
filled <<= 1;
|
||||||
for (int i = filled; i < 256; ++i)
|
for (int i = filled; i < 256; ++i)
|
||||||
translationTable[i] = translationTable[i & (filled - 1)];
|
translationTable[i] = translationTable[0];
|
||||||
}
|
}
|
||||||
Check_Fpu();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -382,6 +382,15 @@ protected:
|
|||||||
SVGA16Palette
|
SVGA16Palette
|
||||||
palette[PaletteCount];
|
palette[PaletteCount];
|
||||||
|
|
||||||
|
// GLASS dirty-skip (2026-08-10): bumped by the palette writers
|
||||||
|
// (L4GraphicsPort::BuildSecondaryColor on a REAL entry change; the full
|
||||||
|
// palette rebuilds unconditionally) so the glass repaint token can see
|
||||||
|
// palette-only animation -- the ColorMapper family (armor rosette tints,
|
||||||
|
// damage flash) changes COLORS with zero pixel writes, which the pixel
|
||||||
|
// checksum alone can never catch.
|
||||||
|
unsigned long
|
||||||
|
paletteGeneration;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int NUMGAUGEWINDOWS;
|
int NUMGAUGEWINDOWS;
|
||||||
HWND *gaugeWindows;
|
HWND *gaugeWindows;
|
||||||
@@ -425,6 +434,11 @@ public:
|
|||||||
// glass repaint pump re-blit ONLY the windows whose plane actually changed
|
// glass repaint pump re-blit ONLY the windows whose plane actually changed
|
||||||
// (idle MFDs / static panels skip; the sweeping radar keeps updating).
|
// (idle MFDs / static panels skip; the sweeping radar keeps updating).
|
||||||
unsigned long PlaneChecksum(int mask) const;
|
unsigned long PlaneChecksum(int mask) const;
|
||||||
|
|
||||||
|
// GLASS dirty-skip: the palette write generation (see paletteGeneration).
|
||||||
|
// Folded into the repaint token of palette-expanding windows so ColorMapper
|
||||||
|
// palette animation (armor tints / damage flash) repaints without pixel churn.
|
||||||
|
unsigned long PaletteGeneration() const { return paletteGeneration; }
|
||||||
};
|
};
|
||||||
|
|
||||||
//########################################################################
|
//########################################################################
|
||||||
|
|||||||
@@ -9034,20 +9034,6 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte
|
|||||||
DEBUG_STREAM << "[rstat] frames=" << sFrames << " avg=" << (sAcc / sFrames)
|
DEBUG_STREAM << "[rstat] frames=" << sFrames << " avg=" << (sAcc / sFrames)
|
||||||
<< "ms maxDraw=" << sMaxD << " maxPresent=" << sMaxP
|
<< "ms maxDraw=" << sMaxD << " maxPresent=" << sMaxP
|
||||||
<< " batches=" << gNumBatches << " culled=" << gBTNumCulled << "\n" << std::flush;
|
<< " batches=" << gNumBatches << " culled=" << gBTNumCulled << "\n" << std::flush;
|
||||||
// #149: segment-refresh telemetry on the same cadence (BT_PERF_LOG).
|
|
||||||
// calls = GetSegmentToWorld entries; dirty = the mark-every-segment
|
|
||||||
// invalidation passes (the expensive arm the #141 sweep may have
|
|
||||||
// multiplied); ms = time inside the accessor for the whole window.
|
|
||||||
{
|
|
||||||
static const int sSegPerf = !(getenv("BT_PERF_LOG") && *getenv("BT_PERF_LOG") == '0');
|
|
||||||
extern int gBTSegWCalls, gBTSegWDirty;
|
|
||||||
extern double gBTSegWMs;
|
|
||||||
if (sSegPerf)
|
|
||||||
DEBUG_STREAM << "[segperf] calls=" << gBTSegWCalls
|
|
||||||
<< " dirty=" << gBTSegWDirty
|
|
||||||
<< " ms=" << gBTSegWMs << "\n" << std::flush;
|
|
||||||
gBTSegWCalls = 0; gBTSegWDirty = 0; gBTSegWMs = 0.0;
|
|
||||||
}
|
|
||||||
sAcc = 0.0; sFrames = 0; sMaxD = 0.0; sMaxP = 0.0;
|
sAcc = 0.0; sFrames = 0; sMaxD = 0.0; sMaxP = 0.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,12 +31,6 @@ void
|
|||||||
void
|
void
|
||||||
BTGlassPanels_Destroy();
|
BTGlassPanels_Destroy();
|
||||||
|
|
||||||
// BT_GLASS_IDS=1 -- log every attached panel's BOOT-STABLE hardware identity
|
|
||||||
// plus a ready-to-paste `monitor:id:<fragment>` line for glass_layout.cfg.
|
|
||||||
// Called from BTGlassPanels_Create; no-op unless the env is set.
|
|
||||||
void
|
|
||||||
BTGlassDumpMonitorIds();
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Per-frame repaint pump. Call once per frame from the main render loop so the
|
// Per-frame repaint pump. Call once per frame from the main render loop so the
|
||||||
// per-display windows' lamp flash keeps animating even when they are in the
|
// per-display windows' lamp flash keeps animating even when they are in the
|
||||||
|
|||||||
@@ -234,36 +234,6 @@ static LONG WINAPI
|
|||||||
return EXCEPTION_EXECUTE_HANDLER; // die (after the evidence is out)
|
return EXCEPTION_EXECUTE_HANDLER; // die (after the evidence is out)
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// lastrun_<stem>.txt clean-exit markers (#157, 2026-08-10): the launch record
|
|
||||||
// only ever said "exe reached WinMain", so a night that ended with the player
|
|
||||||
// closing the menu was indistinguishable from a silent death -- and the bat's
|
|
||||||
// sign-off taught players to report normal quits as crashes. The deliberate
|
|
||||||
// exit paths stamp the record themselves now, so the file answers "crash or
|
|
||||||
// quit?" on its own. Name captured at breadcrumb time (cwd is content\ by
|
|
||||||
// then -- the same file the bats bracket).
|
|
||||||
//
|
|
||||||
static char gBTLastrunName[MAX_PATH] = "";
|
|
||||||
|
|
||||||
void
|
|
||||||
BTLastrunNote(const char *note)
|
|
||||||
{
|
|
||||||
if (gBTLastrunName[0] == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
SYSTEMTIME lt;
|
|
||||||
GetLocalTime(<);
|
|
||||||
std::ofstream f(gBTLastrunName, std::ios::out | std::ios::app);
|
|
||||||
if (f.is_open())
|
|
||||||
{
|
|
||||||
char stamp[64];
|
|
||||||
wsprintfA(stamp, "[%04d-%02d-%02d %02d:%02d:%02d] ",
|
|
||||||
lt.wYear, lt.wMonth, lt.wDay, lt.wHour, lt.wMinute, lt.wSecond);
|
|
||||||
f << stamp << note << "\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//===========================================================================//
|
//===========================================================================//
|
||||||
// environ.ini -- the player's settings file (cwd = content\). One KEY=VALUE
|
// environ.ini -- the player's settings file (cwd = content\). One KEY=VALUE
|
||||||
// per line; the real environment always WINS, so a launcher .bat or a shell
|
// per line; the real environment always WINS, so a launcher .bat or a shell
|
||||||
@@ -863,7 +833,6 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
|||||||
GetComputerNameA(machine, &machine_len);
|
GetComputerNameA(machine, &machine_len);
|
||||||
char lastrun_name[MAX_PATH];
|
char lastrun_name[MAX_PATH];
|
||||||
wsprintfA(lastrun_name, "lastrun_%s.txt", logStem);
|
wsprintfA(lastrun_name, "lastrun_%s.txt", logStem);
|
||||||
lstrcpynA(gBTLastrunName, lastrun_name, MAX_PATH);
|
|
||||||
std::ofstream breadcrumb(lastrun_name, std::ios::out | std::ios::app);
|
std::ofstream breadcrumb(lastrun_name, std::ios::out | std::ios::app);
|
||||||
if (breadcrumb.is_open())
|
if (breadcrumb.is_open())
|
||||||
{
|
{
|
||||||
@@ -1240,7 +1209,6 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
|||||||
BTFeLaunchSpec fe_spec;
|
BTFeLaunchSpec fe_spec;
|
||||||
if (BTFrontEnd_Run(&fe_spec) != 0 || fe_spec.mode == BTFeLaunchNone)
|
if (BTFrontEnd_Run(&fe_spec) != 0 || fe_spec.mode == BTFeLaunchNone)
|
||||||
{
|
{
|
||||||
BTLastrunNote("clean exit: player quit from the menu");
|
|
||||||
return 0; // quit from the menu
|
return 0; // quit from the menu
|
||||||
}
|
}
|
||||||
char fe_arguments[192];
|
char fe_arguments[192];
|
||||||
|
|||||||
@@ -174,8 +174,6 @@ void
|
|||||||
{
|
{
|
||||||
MarshalLog("window closed by the user -- exiting for real "
|
MarshalLog("window closed by the user -- exiting for real "
|
||||||
"(no relaunch)");
|
"(no relaunch)");
|
||||||
extern void BTLastrunNote(const char *);
|
|
||||||
BTLastrunNote("clean exit: player closed the game window");
|
|
||||||
ExitProcess(0);
|
ExitProcess(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -259,16 +257,6 @@ void
|
|||||||
if (CreateProcessW(exe_path, command_line, NULL, NULL, FALSE,
|
if (CreateProcessW(exe_path, command_line, NULL, NULL, FALSE,
|
||||||
0, NULL, NULL, &startup, &process))
|
0, NULL, NULL, &startup, &process))
|
||||||
{
|
{
|
||||||
// Hand the child our foreground right BEFORE we die (#157): Windows
|
|
||||||
// refuses SetForegroundWindow to a process spawned by an exiting
|
|
||||||
// background parent, so the round-end menu could open BEHIND the
|
|
||||||
// desktop clutter -- which is how a normal between-rounds relaunch
|
|
||||||
// read as "the game crashed" from the cockpit seat.
|
|
||||||
if (!AllowSetForegroundWindow(process.dwProcessId))
|
|
||||||
{
|
|
||||||
MarshalLog("foreground handoff denied -- the next window may "
|
|
||||||
"open behind");
|
|
||||||
}
|
|
||||||
CloseHandle(process.hThread);
|
CloseHandle(process.hThread);
|
||||||
CloseHandle(process.hProcess);
|
CloseHandle(process.hProcess);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -412,7 +412,7 @@ static const CatalogEntry kVehicles[] =
|
|||||||
{ "avatar", "Avatar" },
|
{ "avatar", "Avatar" },
|
||||||
{ "ava1", "Satyr" },
|
{ "ava1", "Satyr" },
|
||||||
{ "sunder", "Sunder" },
|
{ "sunder", "Sunder" },
|
||||||
{ "snd1", "Denkou" }, // #69: the 4.x-era name (two era witnesses)
|
{ "snd1", "Sunder V1" },
|
||||||
{ "vulture", "Vulture" },
|
{ "vulture", "Vulture" },
|
||||||
{ "vul1", "Mad Dog" },
|
{ "vul1", "Mad Dog" },
|
||||||
{ "lok2", "Mischief" },
|
{ "lok2", "Mischief" },
|
||||||
|
|||||||
@@ -1744,27 +1744,10 @@ Logical
|
|||||||
L4Warehouse *warehouse = (L4Warehouse *)gauge_renderer->warehousePointer;
|
L4Warehouse *warehouse = (L4Warehouse *)gauge_renderer->warehousePointer;
|
||||||
if (warehouse->pixelMap8Bin.Get(p[2].data.string) == NULL) // FUN_00442d2b
|
if (warehouse->pixelMap8Bin.Get(p[2].data.string) == NULL) // FUN_00442d2b
|
||||||
{
|
{
|
||||||
// WAS DebugStream -- the no-op ReconStream (gotcha: use DEBUG_STREAM).
|
DebugStream << "OneOfSeveralPixInt: Missing image '" << p[2].data.string << "'\n";
|
||||||
// A missing strip therefore failed COMPLETELY SILENTLY, which is
|
|
||||||
// exactly the state #142 was stuck in: the crouch symbol never drew and
|
|
||||||
// nothing anywhere said why.
|
|
||||||
DEBUG_STREAM << "[gauge] oneOfSeveralPixInt: MISSING IMAGE '"
|
|
||||||
<< p[2].data.string << "' -- element not created\n" << std::flush;
|
|
||||||
return False;
|
return False;
|
||||||
}
|
}
|
||||||
warehouse->pixelMap8Bin.Release(p[2].data.string); // FUN_00442e51
|
warehouse->pixelMap8Bin.Release(p[2].data.string); // FUN_00442e51
|
||||||
|
|
||||||
// #142 receipt (ungated, one line per element): does this strip exist, and
|
|
||||||
// did its integer attribute actually RESOLVE? A NULL attributePointer
|
|
||||||
// leaves the connection reading nothing, so the strip pins to frame 0 and
|
|
||||||
// looks like "no animation at all" -- indistinguishable, from outside, from
|
|
||||||
// a missing image or an unbuilt page.
|
|
||||||
DEBUG_STREAM << "[gauge] oneOfSeveralPixInt '" << p[2].data.string
|
|
||||||
<< "' frames=" << p[3].data.integer << "x" << p[4].data.integer
|
|
||||||
<< " port=" << display_port_index
|
|
||||||
<< " at(" << position.x << "," << position.y << ")"
|
|
||||||
<< " attr=" << (p[5].data.attributePointer != 0 ? "BOUND" : "NULL !!")
|
|
||||||
<< "\n" << std::flush;
|
|
||||||
return True;
|
return True;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2622,14 +2622,6 @@ void
|
|||||||
{
|
{
|
||||||
// the range caret translate, from the live target range
|
// the range caret translate, from the live target range
|
||||||
Scalar range = (rangeAttr2 != 0) ? *rangeAttr2 : 0.0f;
|
Scalar range = (rangeAttr2 != 0) ? *rangeAttr2 : 0.0f;
|
||||||
// #147: NaN-SAFE clamp. `range < minRange` and `range > maxRange` are BOTH
|
|
||||||
// false for NaN, so the old pair let a poisoned value straight through into
|
|
||||||
// AddPoint/ConcatMatrix below -- degenerate geometry, and the caret + its
|
|
||||||
// bar silently STOP DRAWING while the static ticks remain. Test for NaN
|
|
||||||
// first (x == x is false only for NaN) and fall back to the binary's
|
|
||||||
// no-target default rather than rendering nothing.
|
|
||||||
if (!(range == range))
|
|
||||||
range = maxRange; // 1200: the authentic no-target peg
|
|
||||||
if (range < minRange) range = minRange;
|
if (range < minRange) range = minRange;
|
||||||
if (range > maxRange) range = maxRange;
|
if (range > maxRange) range = maxRange;
|
||||||
Scalar frac = (range - minRange) / (maxRange - minRange);
|
Scalar frac = (range - minRange) / (maxRange - minRange);
|
||||||
|
|||||||
+28
-328
@@ -305,38 +305,13 @@ static const Scalar TicksPerSecond = 1.0f; // (see note in PlayerSimulation)
|
|||||||
// tonnage ratio stubbed 1.0 + damage bias stubbed 0.0 for bring-up (SCORE == raw
|
// 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).
|
// 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_TONNAGE(m) (1.0f) // bring-up: ratio == 1
|
||||||
// mech+0x354 -- the score formula's per-target damage-bias factor.
|
#define MECH_DAMAGE_BIAS(m) (0.0f) // bring-up: factor = 0*bias+1 = 1
|
||||||
//
|
|
||||||
// ⚠ DO NOT "FINISH" THIS. 0.0f is not a stand-in; it is the value the shipped
|
|
||||||
// binary always has, established by audit 2026-08-08:
|
|
||||||
//
|
|
||||||
// * mech+0x354 has exactly ONE writer in the whole image -- Mech::Reset
|
|
||||||
// (@0049fb74, part_012.c:14340). Nothing touches it during play.
|
|
||||||
// * Reset computes it as mean(zone + 0x158) across every damage zone, and it
|
|
||||||
// does that AFTER the zone heal has already zeroed those cells. So it is
|
|
||||||
// ~0 the instant it is written, stays ~0 for the mech's entire life, and is
|
|
||||||
// recomputed as ~0 on the next respawn.
|
|
||||||
// * It has exactly ONE reader -- CalcInflictedScore (@004c052c,
|
|
||||||
// part_013.c:19055) -- where it appears as `avg * role.damageBias + 1.0`.
|
|
||||||
//
|
|
||||||
// So the factor is 1.0 for the whole game, and returning 0.0f here reproduces
|
|
||||||
// the binary EXACTLY. It is a vestigial aggregate (0x358 and 0x35c are the
|
|
||||||
// same computation and have NO reader at all).
|
|
||||||
//
|
|
||||||
// Wiring this to live accumulated damage would look like completing an unfinished
|
|
||||||
// port and would silently inflate every inflicted and kill award -- the numbers
|
|
||||||
// verified against the original manual's scoring chart (+1 a damage point, +500
|
|
||||||
// a kill) all assume this term is 1.0.
|
|
||||||
#define MECH_DAMAGE_BIAS(m) (0.0f) // = the binary's value; see above
|
|
||||||
#define MECH_OWNING_PLAYER(m) ((BTPlayer *)((Mech *)(m))->GetPlayerLink()) // ENTITY.h:430 (NULL for the dummy)
|
#define MECH_OWNING_PLAYER(m) ((BTPlayer *)((Mech *)(m))->GetPlayerLink()) // ENTITY.h:430 (NULL for the dummy)
|
||||||
|
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
//############################### BTPlayer ##############################
|
//############################### BTPlayer ##############################
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
|
|
||||||
Scalar BTScoreWatermarkOf(int owner);
|
|
||||||
void BTScoreWatermarkSet(int owner, Scalar sent);
|
|
||||||
|
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
// Message Support
|
// Message Support
|
||||||
//
|
//
|
||||||
@@ -709,20 +684,6 @@ void
|
|||||||
// scoreboard (they read +0x278), so the pod's penalty may never have been
|
// scoreboard (they read +0x278), so the pod's penalty may never have been
|
||||||
// visible; our port has ONE currentScore, so it shows.
|
// visible; our port has ONE currentScore, so it shows.
|
||||||
//
|
//
|
||||||
// #45 receipt (ungated): this cost is dispatched by a DIRECT base-handler
|
|
||||||
// call, so it never reaches the BT matchlog and only the running total
|
|
||||||
// exposes it -- which is how a combat death costing -500 while a SELF-KILL
|
|
||||||
// cost nothing went unnoticed. Say out loud whether it fires and why not.
|
|
||||||
DEBUG_STREAM << "[deathcost] player " << BTMatchHostOf(GetEntityID())
|
|
||||||
<< ":" << (int)GetEntityID()
|
|
||||||
<< " advDmg=" << (int)advancedDamageOn
|
|
||||||
<< " role=" << (scenarioRole != 0 ? "bound" : "NULL")
|
|
||||||
<< " penalty=" << (scenarioRole != 0
|
|
||||||
? (float)scenarioRole->GetSpecialCaseDeathPenalty() : 0.0f)
|
|
||||||
<< " scoreBefore=" << (float)currentScore
|
|
||||||
<< ((advancedDamageOn && scenarioRole != 0) ? " -> APPLYING" : " -> SKIPPED")
|
|
||||||
<< "\n" << std::flush;
|
|
||||||
|
|
||||||
if (advancedDamageOn && scenarioRole != 0) // this+0x264 (binary derefs role unguarded)
|
if (advancedDamageOn && scenarioRole != 0) // this+0x264 (binary derefs role unguarded)
|
||||||
{
|
{
|
||||||
BTPlayer::ScoreMessage death_cost(
|
BTPlayer::ScoreMessage death_cost(
|
||||||
@@ -770,187 +731,6 @@ void
|
|||||||
suppressConsole = 0; // this+0x258
|
suppressConsole = 0; // this+0x258
|
||||||
}
|
}
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
// MissionStartingMessageHandler (@004bfbe8)
|
|
||||||
//
|
|
||||||
// THE "+1000 STARTING THE GAME" ROW of the original manual's scoring chart,
|
|
||||||
// decoded 2026-08-07. BT overrides the engine's MissionStarting purely to
|
|
||||||
// seed the score, and the override was never reconstructed -- the
|
|
||||||
// MESSAGE_ENTRY resolved to the inherited Player:: handler, so the grant
|
|
||||||
// simply never happened. The binary:
|
|
||||||
//
|
|
||||||
// FUN_004bfbe8(player):
|
|
||||||
// base_MissionStarting(player);
|
|
||||||
// if (app->state == 4 && (player[0x29] & 0x40) == 0)
|
|
||||||
// player[0x1c8] = 0x447a0000; // = 1000.0f
|
|
||||||
//
|
|
||||||
// Both operands decode exactly: application state 4 is LaunchingMission
|
|
||||||
// (APP.h -- the same enum whose 6 is EndingMission, already used by the
|
|
||||||
// console flush), and simulationFlags bit 14 is NonScoringPlayerBit
|
|
||||||
// (PLAYER.h: `NonScoringPlayerBit = Entity::NextBit`), so the byte test
|
|
||||||
// `(+0x29 & 0x40) == 0` IS `IsScoringPlayer()`. Camera-ship and spectator
|
|
||||||
// players are non-scoring and correctly get nothing.
|
|
||||||
//
|
|
||||||
// CELL NOTE: the binary seeds the ENGINE score cell (+0x1c8), not BT's own
|
|
||||||
// (+0x278) -- the 1995 build carried two accumulators, which is why the KB
|
|
||||||
// suspected the pod's death cost "may never have displayed". Our port has a
|
|
||||||
// single currentScore, so the grant, the awards and the death cost all land
|
|
||||||
// together, and the chart reads coherently for a player: start at 1000, +1 a
|
|
||||||
// damage point, +500 a kill, -500 a special-case death.
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
void
|
|
||||||
BTPlayer::MissionStartingMessageHandler(Entity::Message *message)
|
|
||||||
{
|
|
||||||
Player::MissionStartingMessageHandler(message); // FUN_0042d9c0
|
|
||||||
|
|
||||||
if (application->GetApplicationState() == Application::LaunchingMission // app+0x88 == 4
|
|
||||||
&& IsScoringPlayer()) // !(simulationFlags & NonScoringPlayerFlag)
|
|
||||||
{
|
|
||||||
currentScore = 1000.0f; // this+0x1c8 = 0x447a0000
|
|
||||||
// The console watermark is what we have already reported; a fresh
|
|
||||||
// mission must report the grant, not the difference from the last
|
|
||||||
// round's tally.
|
|
||||||
BTScoreWatermarkSet(ownerID, 0.0f);
|
|
||||||
DEBUG_STREAM << "[score] mission start: player "
|
|
||||||
<< BTMatchHostOf(GetEntityID()) << ":" << (int)GetEntityID()
|
|
||||||
<< " seeded to " << (float)currentScore
|
|
||||||
<< " (chart: +1000 starting the game)\n" << std::flush;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
// Console score watermark (port-side, 2026-08-07)
|
|
||||||
//
|
|
||||||
// How much of a player's running score has already been reported to the
|
|
||||||
// operator console. Keyed by ownerID and kept OUTSIDE BTPlayer: sizeof
|
|
||||||
// (BTPlayer) is static_assert-locked at 652 against the binary, so a new data
|
|
||||||
// member is not available (the console timer above is a file static for the
|
|
||||||
// same reason). A pod round is a handful of players; linear scan is free.
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
namespace {
|
|
||||||
struct ScoreWatermark { int owner; Scalar sent; };
|
|
||||||
ScoreWatermark gScoreWatermarks[16];
|
|
||||||
int gScoreWatermarkCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
Scalar
|
|
||||||
BTScoreWatermarkOf(int owner)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < gScoreWatermarkCount; ++i)
|
|
||||||
if (gScoreWatermarks[i].owner == owner)
|
|
||||||
return gScoreWatermarks[i].sent;
|
|
||||||
return 0.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
BTScoreWatermarkSet(int owner, Scalar sent)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < gScoreWatermarkCount; ++i)
|
|
||||||
if (gScoreWatermarks[i].owner == owner)
|
|
||||||
{
|
|
||||||
gScoreWatermarks[i].sent = sent;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (gScoreWatermarkCount
|
|
||||||
< (int)(sizeof(gScoreWatermarks) / sizeof(gScoreWatermarks[0])))
|
|
||||||
{
|
|
||||||
gScoreWatermarks[gScoreWatermarkCount].owner = owner;
|
|
||||||
gScoreWatermarks[gScoreWatermarkCount].sent = sent;
|
|
||||||
++gScoreWatermarkCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
// Dispatch (@004bffa0, vtable @00513300 slot 3)
|
|
||||||
//
|
|
||||||
// THE TYPE-0 INTERCEPTOR. Restored 2026-08-07 -- it was missing, and its
|
|
||||||
// absence silently deleted per-hit inflicted scoring:
|
|
||||||
//
|
|
||||||
// * the binary carries all three score reports under ONE id (0x16) and
|
|
||||||
// splits type 0 off here, BEFORE base dispatch, straight into
|
|
||||||
// ScoreInflictedMessageHandler (@004c0200 -- which names itself in its own
|
|
||||||
// Verify string, "BTPlayer::ScoreInflictedMessageHandler");
|
|
||||||
// * ScoreMessageHandler's type-0 arm Verify-rejects ON PURPOSE, because this
|
|
||||||
// interceptor guarantees type 0 never gets that far;
|
|
||||||
// * the port had the handler, faithfully reconstructed, and NO interceptor.
|
|
||||||
// Block B sends its inflicted report under Player::ScoreMessageID, so every
|
|
||||||
// one of them landed in the rejecting arm and banked 0.
|
|
||||||
//
|
|
||||||
// The KB previously recorded @004c0200 as "in NO table entry: dead code" and
|
|
||||||
// concluded 1995 folded an uninitialised stack float into the shooter's score
|
|
||||||
// on every non-lethal hit -- and the port's per-hit crediting was retired as an
|
|
||||||
// "invention" on that basis (#45/#134, build 787, the build players report
|
|
||||||
// scoring regressed in). That reading was wrong: the handler is live through
|
|
||||||
// THIS vtable slot, and the original manual's SCORING CHART independently
|
|
||||||
// corroborates what it computes -- "+1 each damage point scored on opponent's
|
|
||||||
// armor" and "-1 each self-inflicted point of armor damage", which is exactly
|
|
||||||
// this handler's negate-if-target-is-self arm. combat-damage.md is corrected.
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
void
|
|
||||||
BTPlayer::Dispatch(Receiver::Message *what)
|
|
||||||
{
|
|
||||||
if (what != 0
|
|
||||||
&& what->messageID == Player::ScoreMessageID
|
|
||||||
&& ((BTPlayer::ScoreMessage *)what)->scoreType
|
|
||||||
== BTPlayer::ScoreMessage::DamageInflictedScore
|
|
||||||
//
|
|
||||||
// MASTER ONLY. The binary intercepts unconditionally because its
|
|
||||||
// +0x278 is a console DELTA -- every node's contribution is flushed
|
|
||||||
// under the scoring player's ownerID and the CONSOLE totals it, so the
|
|
||||||
// computing node is irrelevant. Our port has no console tally
|
|
||||||
// (btconsole.py/btoperator.py handle no score at all) and reads +0x278
|
|
||||||
// on the OWNING node for the SCORE gauge, CalcRanking and the replicated
|
|
||||||
// Player__UpdateRecord. Banking on the victim's replicant copy
|
|
||||||
// therefore loses the credit to the master's next update record.
|
|
||||||
// Falling through lets Entity::Dispatch reroute to the owner, where the
|
|
||||||
// wire delivery lands in ScoreMessageHandler's type-0 arm (which now
|
|
||||||
// delegates back to the inflicted handler).
|
|
||||||
//
|
|
||||||
&& GetInstance() == Entity::MasterInstance)
|
|
||||||
{
|
|
||||||
ScoreInflictedMessageHandler((BTPlayer::ScoreMessage *)what);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Player::Dispatch(what);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// ⚠ OPEN, cross-node credit routing (2026-08-07). The interception above is
|
|
||||||
// binary-faithful and restores per-hit inflicted credit -- benched, awards
|
|
||||||
// track damage. What it does NOT yet solve is WHICH MACHINE banks it.
|
|
||||||
//
|
|
||||||
// Damage is applied on the VICTIM's node, so block B dispatches the inflicted
|
|
||||||
// report to the SHOOTER's player object THERE, which on that node is a
|
|
||||||
// REPLICANT. In 1995 that was fine: +0x278 is only ever a console DELTA, and
|
|
||||||
// it is flushed to the operator console stamped with the shooter's ownerID --
|
|
||||||
// the CONSOLE holds the authoritative total, so it does not matter which node
|
|
||||||
// computed a delta. (That is almost certainly where the manual chart's "+1000
|
|
||||||
// starting the game" was seeded, which is why no game-side code grants it.)
|
|
||||||
//
|
|
||||||
// Our port has no console as score authority: GetScore() (the SCORE gauge),
|
|
||||||
// CalcRanking() and the replicated Player__UpdateRecord all read +0x278 on the
|
|
||||||
// OWNING node. So a credit banked on the victim's replicant copy is
|
|
||||||
// overwritten by the master's next update record -- benched as totals climbing
|
|
||||||
// to ~35 and snapping back every few seconds.
|
|
||||||
//
|
|
||||||
// TRIED AND REJECTED: gating the interception to MasterInstance so a replicant
|
|
||||||
// falls through and reroutes to the master. The message arrives, but the BT
|
|
||||||
// extension fields (damageAmount@+0x24, senderMechID@+0x34) do NOT survive the
|
|
||||||
// wire -- only the base Player::ScoreMessage `scoreAward` does -- so every
|
|
||||||
// award computed to 0.00. That is also WHY the kill path (type 2) already
|
|
||||||
// credits cross-node correctly: its value rides `scoreAward`.
|
|
||||||
//
|
|
||||||
// THE FIX SHAPE, therefore: compute the award on the victim's node (where the
|
|
||||||
// damage data lives, exactly as now) and ship the RESULT to the owner in
|
|
||||||
// `scoreAward`, the way the kill report already does -- rather than shipping
|
|
||||||
// the basis and recomputing on a machine that cannot see it.
|
|
||||||
//
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
// ScoreInflictedMessageHandler
|
// ScoreInflictedMessageHandler
|
||||||
//
|
//
|
||||||
@@ -1085,34 +865,16 @@ void
|
|||||||
{
|
{
|
||||||
case BTPlayer::ScoreMessage::DamageInflictedScore: // 0
|
case BTPlayer::ScoreMessage::DamageInflictedScore: // 0
|
||||||
//
|
//
|
||||||
// Inflicted-damage messages belong to ScoreInflictedMessageHandler, and
|
// Inflicted-damage messages belong to ScoreInflictedMessageHandler.
|
||||||
// the binary's Dispatch override (@004bffa0) guarantees they never reach
|
|
||||||
// here -- which is why the original arm is a bare Verify.
|
|
||||||
//
|
//
|
||||||
// PORT DIVERGENCE (2026-08-07), and it is a DELIVERY-PATH difference,
|
Verify(
|
||||||
// not a scoring one. Damage is applied on the VICTIM's node, so block B
|
False,
|
||||||
// dispatches the inflicted report to the SHOOTER's player object there --
|
"BTPlayer::ScoreMessageHandler should not be "
|
||||||
// a REPLICANT -- and Entity::Dispatch reroutes it to the owning host so
|
"given DamageInflictedScoreMessages!", // @0051324a
|
||||||
// the credit lands on the shooter's OWN machine (the same reroute that
|
"d:\\tesla_bt\\bt\\btplayer.cpp", // @0051329a
|
||||||
// carries kill credit, ENTITY.cpp:244-251). But a message arriving over
|
0x296
|
||||||
// the WIRE is delivered through Receive(), straight to this handler
|
);
|
||||||
// table: the virtual Dispatch override is never called on the receiving
|
break;
|
||||||
// side. So the rerouted report lands HERE, and Verify-rejecting it
|
|
||||||
// threw away every cross-node inflicted credit (benched: award=0.00).
|
|
||||||
//
|
|
||||||
// Delegate instead. Local deliveries are still intercepted by Dispatch
|
|
||||||
// exactly as the binary does; wire deliveries land here and get the same
|
|
||||||
// handler. One accumulator, on the machine that owns the score.
|
|
||||||
//
|
|
||||||
// RETURN, not break. The post-switch tail folds the local `award` into
|
|
||||||
// message->scoreAward and hands it to the base handler -- and for this
|
|
||||||
// arm `award` is still 0, so falling through clobbered scoreAward to
|
|
||||||
// zero, added nothing, and emitted a second SCORE row reading
|
|
||||||
// "type=0 award=0.00" (the 80 real + 80 zero rows in the bench).
|
|
||||||
// ScoreInflictedMessageHandler is self-contained: it accumulates,
|
|
||||||
// ForceUpdate()s and logs its own receipt.
|
|
||||||
ScoreInflictedMessageHandler(message);
|
|
||||||
return;
|
|
||||||
|
|
||||||
case BTPlayer::ScoreMessage::DamageReceivedScore: // 1
|
case BTPlayer::ScoreMessage::DamageReceivedScore: // 1
|
||||||
{
|
{
|
||||||
@@ -1381,38 +1143,20 @@ void
|
|||||||
//
|
//
|
||||||
// Only bother if our score actually changed since last time.
|
// Only bother if our score actually changed since last time.
|
||||||
//
|
//
|
||||||
// DELTA vs RUNNING TOTAL (2026-08-07). The binary sends currentScore
|
if ((Scalar)currentScore != 0.0f) // this[0x9e] != _DAT_004c0900 (0.0f)
|
||||||
// and then ZEROES it, ungated (@FUN_..., `param_1[0x9e] = 0` right after
|
|
||||||
// the send) -- so in 1995 +0x278 was a CONSOLE DELTA and the running
|
|
||||||
// total lived on the operator console, which is also where the manual's
|
|
||||||
// "+1000 starting the game" would have been seeded.
|
|
||||||
//
|
|
||||||
// Our port cannot copy that literally: THREE port-side consumers read
|
|
||||||
// +0x278 as a running total -- GetScore() (the SCORE gauge),
|
|
||||||
// Player::CalcRanking(), and Player__UpdateRecord (the only score field
|
|
||||||
// we replicate to peers). Zeroing it made all three reset every
|
|
||||||
// CONSOLE_UPDATE_INTERVAL, which is what players saw as "scoring went
|
|
||||||
// screwy" (bench: totals climbed to ~35 and dropped back). It was
|
|
||||||
// mostly invisible until the type-0 interceptor was restored, because
|
|
||||||
// before that currentScore barely moved.
|
|
||||||
//
|
|
||||||
// So: keep the WIRE authentic (the console still receives a DELTA) and
|
|
||||||
// keep the OBJECT sane (currentScore stays a true running total). The
|
|
||||||
// last-sent watermark is a file static keyed by player -- a new data
|
|
||||||
// member would change sizeof(BTPlayer) and break the offset locks (same
|
|
||||||
// reason the console timer above is a static).
|
|
||||||
//
|
|
||||||
const Scalar sent_already = BTScoreWatermarkOf(ownerID);
|
|
||||||
const Scalar delta = (Scalar)currentScore - sent_already;
|
|
||||||
if (delta != 0.0f)
|
|
||||||
{
|
{
|
||||||
int score = (int)delta;
|
int score = (int)currentScore;
|
||||||
|
|
||||||
ConsolePlayerVTVScoreUpdateMessage score_message(
|
ConsolePlayerVTVScoreUpdateMessage score_message(
|
||||||
ownerID,
|
ownerID,
|
||||||
score
|
score
|
||||||
); // FUN_00420ea4(0x20, 0x1a, 1, ...)
|
); // FUN_00420ea4(0x20, 0x1a, 1, ...)
|
||||||
|
|
||||||
|
// 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 =
|
Host *console_host =
|
||||||
application->GetHostManager()->GetConsoleHost(); // FUN_00429078
|
application->GetHostManager()->GetConsoleHost(); // FUN_00429078
|
||||||
if (console_host)
|
if (console_host)
|
||||||
@@ -1431,10 +1175,7 @@ void
|
|||||||
NetworkClient::ConsoleClientID, // 5
|
NetworkClient::ConsoleClientID, // 5
|
||||||
&score_message
|
&score_message
|
||||||
);
|
);
|
||||||
// The binary does `currentScore = 0` here. We advance the
|
currentScore = 0; // this[0x9e] = 0
|
||||||
// watermark by the amount actually SENT instead, so the console
|
|
||||||
// sees the same deltas while the object keeps the total.
|
|
||||||
BTScoreWatermarkSet(ownerID, sent_already + (Scalar)score);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2037,51 +1778,15 @@ BTPlayer::BTPlayer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Look the scoring role up in the role registry (keyed by the role name in
|
// Look the scoring role up in the role registry (keyed by the role name
|
||||||
// the creation message, +0x90) and stash it as our scenarioRole.
|
// in the creation message, +0x90) and stash it as our scenarioRole. The
|
||||||
//
|
// BT role registry (BTMission::GetRoleRegistry()->Lookup) has no WinTesla
|
||||||
// WIRED 2026-08-07. This was commented out with "the BT role registry
|
// analog, so the scenarioRole set by the base Player ctor stands.
|
||||||
// (BTMission::GetRoleRegistry()->Lookup) has no WinTesla analog, so the
|
// CROSS-FAMILY: needs BTMission role-registry access. BEST-EFFORT.
|
||||||
// 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
|
CString role_key(creation_message->roleName); // make+0x90
|
||||||
if (playerMission != 0)
|
(void)role_key;
|
||||||
{
|
// scenarioRole = playerMission->GetRoleRegistry()->Lookup(&role_key); // this[0x7e]+0x50, this[0x82]
|
||||||
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)
|
if ((simulationFlags & 0xc) == 4)
|
||||||
{
|
{
|
||||||
@@ -2646,14 +2351,9 @@ void BTMechPostCombatReports(
|
|||||||
else if (damage_tally != 0.0f && shooter_player != 0)
|
else if (damage_tally != 0.0f && shooter_player != 0)
|
||||||
{
|
{
|
||||||
//
|
//
|
||||||
// Block B: the inflicted report -- "+1 each damage point scored on
|
// Block B: the plain inflicted report. Wire fidelity only -- the 0x16
|
||||||
// opponent's armor" (original manual scoring chart). Goes out under
|
// handler Verify-rejects type 0 and banks award 0 (1995 banked an
|
||||||
// Player::ScoreMessageID with type 0, exactly as the binary does; the
|
// uninitialized stack float; see the handler's type-0 arm note).
|
||||||
// Dispatch override (@004bffa0) intercepts it into
|
|
||||||
// ScoreInflictedMessageHandler. It is NOT wire-fidelity-only -- the
|
|
||||||
// old note here claimed the handler banks 0 because @004c0200 was
|
|
||||||
// "dead code", which was a misreading of the vtable; corrected
|
|
||||||
// 2026-08-07 and the interceptor restored.
|
|
||||||
//
|
//
|
||||||
BTPlayer::ScoreMessage inflicted(
|
BTPlayer::ScoreMessage inflicted(
|
||||||
Player::ScoreMessageID,
|
Player::ScoreMessageID,
|
||||||
|
|||||||
@@ -84,24 +84,16 @@ class DropZone__ReplyMessage;
|
|||||||
public:
|
public:
|
||||||
//
|
//
|
||||||
// Kind of scoring event. Recovered from the branch selector at
|
// Kind of scoring event. Recovered from the branch selector at
|
||||||
// @004c02e4 (this->scoreType, message+0x20).
|
// @004c02e4 (this->scoreType, message+0x20). NOTE: type 0 has NO
|
||||||
//
|
// scoring arm in the binary -- @004c02e4 Verify-rejects it (line 662)
|
||||||
// CORRECTED 2026-08-07. This note used to read "type 0 has NO scoring
|
// and @004c0200, the only function that accepts it, appears in no
|
||||||
// arm in the binary ... @004c0200 appears in no handler-table entry ...
|
// handler-table entry (byte-scan 2026-08-05: the BTPlayer table at
|
||||||
// per-hit inflicted credit never existed", and build 787 deleted the
|
// file 0x112dxx has exactly 6 entries, none binding it). 1995 pod
|
||||||
// credit on that basis. The byte-scan was right that no TABLE entry
|
// scoring = kills + received-damage penalties; per-hit inflicted
|
||||||
// binds @004c0200 and wrong to conclude it is unreachable: BTPlayer
|
// credit never existed.
|
||||||
// overrides Dispatch (vtable @00513300 slot 3 = FUN_004bffa0), which
|
|
||||||
// splits type 0 off BEFORE base dispatch and calls it directly. That
|
|
||||||
// is also WHY @004c02e4 Verify-rejects type 0 -- the interceptor
|
|
||||||
// guarantees it never arrives there. @004c0200 names itself
|
|
||||||
// "BTPlayer::ScoreInflictedMessageHandler" in its own Verify string,
|
|
||||||
// and the original manual's scoring chart independently confirms what
|
|
||||||
// it computes: "+1 each damage point scored on opponent's armor",
|
|
||||||
// "-1 each self-inflicted point".
|
|
||||||
//
|
//
|
||||||
enum ScoreType {
|
enum ScoreType {
|
||||||
DamageInflictedScore = 0, // per-hit inflicted credit (LIVE, see above)
|
DamageInflictedScore = 0, // sent, but scores nothing (see above)
|
||||||
DamageReceivedScore = 1, // I took damage
|
DamageReceivedScore = 1, // I took damage
|
||||||
KillScore = 2 // I destroyed / was destroyed
|
KillScore = 2 // I destroyed / was destroyed
|
||||||
};
|
};
|
||||||
@@ -296,23 +288,6 @@ class DropZone__ReplyMessage;
|
|||||||
private:
|
private:
|
||||||
static const HandlerEntry MessageHandlerEntries[];
|
static const HandlerEntry MessageHandlerEntries[];
|
||||||
|
|
||||||
public:
|
|
||||||
//
|
|
||||||
// @004bffa0 -- the DISPATCH OVERRIDE (vtable @00513300 slot 3). The
|
|
||||||
// binary uses ONE message id (0x16) for all three score reports and
|
|
||||||
// splits type 0 off HERE, before base dispatch:
|
|
||||||
//
|
|
||||||
// if (msg->id == 0x16 && msg->type == 0) ScoreInflicted(msg);
|
|
||||||
// else base dispatch;
|
|
||||||
//
|
|
||||||
// which is why ScoreMessageHandler's own type-0 arm can Verify-reject:
|
|
||||||
// the interceptor guarantees type 0 never reaches it. Without this
|
|
||||||
// override every inflicted report lands in the rejecting arm and scores
|
|
||||||
// nothing -- see the note in btplayer.cpp.
|
|
||||||
//
|
|
||||||
virtual void
|
|
||||||
Dispatch(Receiver::Message *what); // @004bffa0
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
static MessageHandlerSet& GetMessageHandlers();
|
static MessageHandlerSet& GetMessageHandlers();
|
||||||
|
|
||||||
@@ -327,14 +302,6 @@ class DropZone__ReplyMessage;
|
|||||||
void
|
void
|
||||||
ScoreMessageHandler(ScoreMessage *message); // @004c02e4
|
ScoreMessageHandler(ScoreMessage *message); // @004c02e4
|
||||||
|
|
||||||
//
|
|
||||||
// @004bfbe8 -- BT's MissionStarting override. Seeds the starting score
|
|
||||||
// ("+1000 Starting the game", original manual scoring chart). The base
|
|
||||||
// Player handler does the fade-in; BT adds the grant.
|
|
||||||
//
|
|
||||||
void
|
|
||||||
MissionStartingMessageHandler(Entity::Message *message); // @004bfbe8
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// @004bffd0 -- the spawn / respawn handshake. When the drop zone
|
// @004bffd0 -- the spawn / respawn handshake. When the drop zone
|
||||||
// replies with our spawn location we create (or reset) the player's
|
// replies with our spawn location we create (or reset) the player's
|
||||||
|
|||||||
@@ -947,26 +947,6 @@ void
|
|||||||
if (other != 0 && coolantAvailable != 0)
|
if (other != 0 && coolantAvailable != 0)
|
||||||
{
|
{
|
||||||
Scalar flow = ComputeHeatFlow(other, time_slice); // FUN_004ad9ec
|
Scalar flow = ComputeHeatFlow(other, time_slice); // FUN_004ad9ec
|
||||||
// #137 FLOW TRAP (BT_HEAT_LOG): the myomers gains ~2e9 of energy within
|
|
||||||
// 30 frames of a respawn while every partner reads T=77 -- and the only
|
|
||||||
// writers into its pendingHeat are its (dead-at-spawn) integrator and
|
|
||||||
// THIS line. Any e6-scale single flow is the bug caught in the act;
|
|
||||||
// print the complete operand set so the arithmetic can be re-run by
|
|
||||||
// hand instead of guessed at.
|
|
||||||
if (getenv("BT_HEAT_LOG") != 0 && (flow > 1.0e6f || flow < -1.0e6f))
|
|
||||||
{
|
|
||||||
DEBUG_STREAM << "[heatflow] " << (GetName() ? GetName() : "?")
|
|
||||||
<< " -> " << (other->GetName() ? other->GetName() : "?")
|
|
||||||
<< " flow=" << flow << " dt=" << time_slice
|
|
||||||
<< " | this: T=" << currentTemperature << " E=" << heatEnergy
|
|
||||||
<< " pend=" << pendingHeat << " m=" << thermalMass
|
|
||||||
<< " mScale=" << massScale << " k=" << thermalConductance
|
|
||||||
<< " lvl=" << coolantLevel << " cap=" << thermalCapacity
|
|
||||||
<< " fScale=" << coolantFlowScale
|
|
||||||
<< " | other: T=" << other->currentTemperature
|
|
||||||
<< " E=" << other->heatEnergy << " pend=" << other->pendingHeat
|
|
||||||
<< " m=" << other->thermalMass << "\n" << std::flush;
|
|
||||||
}
|
|
||||||
other->pendingHeat += flow;
|
other->pendingHeat += flow;
|
||||||
pendingHeat -= flow;
|
pendingHeat -= flow;
|
||||||
BalanceCoolant(time_slice); // FUN_004ada94
|
BalanceCoolant(time_slice); // FUN_004ada94
|
||||||
@@ -1466,117 +1446,3 @@ int BTHeatSinkBankCoolantFraction(Subsystem *sub, Scalar *out)
|
|||||||
*out = bank->CoolantFractionOf();
|
*out = bank->CoolantFractionOf();
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
//===========================================================================//
|
|
||||||
// BTReportHeatAtReset -- #137 forensic (ungated when BT_HEAT_LOG is set).
|
|
||||||
//
|
|
||||||
// The [heat-t] census runs on a 5-second per-instance timer, which is far too
|
|
||||||
// coarse to answer the question #137 actually poses: "respawn came back with
|
|
||||||
// MYOMERS heat MAXED". Is the temperature high BECAUSE the reset did not
|
|
||||||
// clear it, or because it climbs again within the first second? Those need a
|
|
||||||
// sample taken AT the reset, which is what this is. Called from Mech::Reset
|
|
||||||
// immediately after the subsystem sweep, so every heat-bearing subsystem
|
|
||||||
// reports the temperature the reset actually left it at.
|
|
||||||
//===========================================================================//
|
|
||||||
void BTReportHeatAtReset(void *mech_v)
|
|
||||||
{
|
|
||||||
if (mech_v == 0 || getenv("BT_HEAT_LOG") == 0)
|
|
||||||
return;
|
|
||||||
Entity *mech = (Entity *)mech_v;
|
|
||||||
const int count = mech->GetSubsystemCount();
|
|
||||||
for (int i = 0; i < count; ++i)
|
|
||||||
{
|
|
||||||
Subsystem *s = mech->GetSubsystem(i);
|
|
||||||
if (s == 0)
|
|
||||||
continue;
|
|
||||||
// UNFILTERED first: the earlier pass filtered on IsDerivedFrom(HeatSink)
|
|
||||||
// and reported no Condensers. That test rides a hand-built Derivation
|
|
||||||
// chain, so a false negative there is indistinguishable from "not in the
|
|
||||||
// roster" -- name every roster entry and say whether the test passed.
|
|
||||||
if (!s->IsDerivedFrom(*HeatableSubsystem::GetClassDerivations()))
|
|
||||||
{
|
|
||||||
DEBUG_STREAM << "[heat-reset] roster[" << i << "] "
|
|
||||||
<< (s->GetName() ? s->GetName() : "?")
|
|
||||||
<< " (not HeatSink-derived)" << "\n" << std::flush;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
HeatableSubsystem *sink = (HeatableSubsystem *)s;
|
|
||||||
DEBUG_STREAM << "[heat-reset] " << (s->GetName() ? s->GetName() : "?")
|
|
||||||
<< " T=" << (float)sink->currentTemperature
|
|
||||||
<< "\n" << std::flush;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//===========================================================================//
|
|
||||||
// BTReportMyomerFreeze -- #137 forensic (BT_HEAT_LOG), the POST-reset trace.
|
|
||||||
//
|
|
||||||
// BTReportHeatAtReset samples AT the reset and showed every subsystem at
|
|
||||||
// T == startingTemperature, which is what made the reset path look innocent and
|
|
||||||
// got #137 wrongly closed. The field then proved the freeze is real (5 of 61
|
|
||||||
// respawns: throttle up, speedDemand pinned at 0) -- so the interesting window
|
|
||||||
// is the frames immediately AFTER the reset, which nothing was sampling.
|
|
||||||
//
|
|
||||||
// Three explanations survive the decomp read and only data separates them:
|
|
||||||
// (a) RESET DIDN'T TAKE -> temp is high right after the reset
|
|
||||||
// (b) STALE CACHE -> temp is at start but speedEffect is still 0
|
|
||||||
// (HeatSink::RTIS @004ad760 writes only bytes
|
|
||||||
// 0x114/0x12C/0x130/0x134/0x138/0x158/0x15C --
|
|
||||||
// it does NOT touch Myomers::speedEffect @0x31C,
|
|
||||||
// and Myomers::RTIS @004b8aa4 only chains to the
|
|
||||||
// PoweredSubsystem one, so the pre-death value
|
|
||||||
// survives until the myomers next ticks)
|
|
||||||
// (c) INSTANT RE-HEAT -> temp starts at start and climbs back at once
|
|
||||||
//
|
|
||||||
// Prints per myomers: temperature, its own speedEffect, and the mech-level MAX
|
|
||||||
// the mover actually multiplies by. Freeze == that MAX at 0.
|
|
||||||
//===========================================================================//
|
|
||||||
void BTReportMyomerFreeze(void *mech_v, const char *when)
|
|
||||||
{
|
|
||||||
if (mech_v == 0 || getenv("BT_HEAT_LOG") == 0)
|
|
||||||
return;
|
|
||||||
Entity *mech = (Entity *)mech_v;
|
|
||||||
extern Scalar BTMyomersSpeedEffectOf(void *subsystem);
|
|
||||||
const int count = mech->GetSubsystemCount();
|
|
||||||
Scalar best = -1.0f;
|
|
||||||
for (int i = 0; i < count; ++i)
|
|
||||||
{
|
|
||||||
Subsystem *s = mech->GetSubsystem(i);
|
|
||||||
if (s == 0)
|
|
||||||
continue;
|
|
||||||
// ROSTER-WIDE now (2026-08-09): the myomers-only version proved the
|
|
||||||
// myomers is being COOKED FROM OUTSIDE -- +2.3e9 of energy arrives in
|
|
||||||
// <=30 frames while its own integrator reads near-zero v (our Reset
|
|
||||||
// zeroes localVelocity, matching the binary's +0x1c4 zero-fill, so
|
|
||||||
// termKinetic/termAccel are dead at spawn). ConductHeat flow is
|
|
||||||
// bounded by deltaT, so an e9 slug through it demands a NEIGHBOUR at
|
|
||||||
// extreme temperature. Print EVERY heat-bearing subsystem's T plus
|
|
||||||
// the two cells T is actually derived from (heatEnergy @0x158,
|
|
||||||
// pendingHeat @0x1C8) to NAME that neighbour.
|
|
||||||
if (!s->IsDerivedFrom(*HeatableSubsystem::GetClassDerivations()))
|
|
||||||
continue;
|
|
||||||
HeatableSubsystem *h = (HeatableSubsystem *)s;
|
|
||||||
// Every Heatable-positive roster member in this game IS a HeatSink
|
|
||||||
// (the Watcher branch -- Torso/HUD/Gyro -- rides HeatWatcher, which is
|
|
||||||
// not HeatableSubsystem-derived), so the downcast for E/pend is safe.
|
|
||||||
// Deliberately NOT IsDerivedFrom(HeatSink): that hand-built chain
|
|
||||||
// returns false for Condenser (the night-13 trap).
|
|
||||||
HeatSink *hs = (HeatSink *)s;
|
|
||||||
Scalar f = BTMyomersSpeedEffectOf(s); // >= 0 only for a Myomers
|
|
||||||
if (f > best) best = f;
|
|
||||||
DEBUG_STREAM << "[myofreeze] " << when << " " << (s->GetName() ? s->GetName() : "?")
|
|
||||||
<< " T=" << h->currentTemperature
|
|
||||||
<< " E=" << hs->heatEnergy
|
|
||||||
<< " pend=" << hs->pendingHeat
|
|
||||||
<< " fail=" << h->failureTemperature;
|
|
||||||
if (f >= 0.0f)
|
|
||||||
{
|
|
||||||
DEBUG_STREAM << " speedEffect=" << f
|
|
||||||
<< ((f <= 1.0e-4f && h->currentTemperature < h->failureTemperature)
|
|
||||||
? " <<<< STALE (cold but zero)" : "");
|
|
||||||
}
|
|
||||||
DEBUG_STREAM << "\n" << std::flush;
|
|
||||||
}
|
|
||||||
if (best >= 0.0f)
|
|
||||||
DEBUG_STREAM << "[myofreeze] " << when << " CHAIN MAX=" << best
|
|
||||||
<< (best <= 1.0e-4f ? " <<<< FROZEN" : "") << "\n" << std::flush;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -52,40 +52,14 @@ struct HUDLayoutCheck
|
|||||||
};
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
// Tuning constants read as read-only float globals adjacent to the
|
// Tuning constants observed as read-only float globals adjacent to the
|
||||||
// HudSimulation body. The VALUES BELOW ARE THE BINARY'S, read straight off
|
// HudSimulation body (.rdata, recovered from section_dump.txt).
|
||||||
// the .rdata rows in reference/decomp/section_dump.txt [T1]:
|
|
||||||
// 4b7ec0 8be55dc3 0000403f 0000803f 0000c842
|
|
||||||
// 4b7ed0 00000000
|
|
||||||
// -> ec4 = 0.75f ec8 = 1.0f ecc = 100.0f ed0 = 0.0f
|
|
||||||
//
|
|
||||||
// CORRECTED 2026-08-08. Every entry here used to be a 0.0f/500.0f STAND-IN
|
|
||||||
// under a guessed name, and the names described the wrong mechanism: there is
|
|
||||||
// no "heat threshold for HUD page visibility" at 0x4b7ec4 -- ec4/ec8 are the
|
|
||||||
// fire-control LOCK damage limits, and "MaxTorsoSlew = 500.0f" at ed0 read the
|
|
||||||
// range-slide Abs() idiom backwards (ed0 is the ZERO; 500.0 is an immediate).
|
|
||||||
// The LIVE implementation of the lock rule and the range slide is mech4.cpp's
|
|
||||||
// targeting step, which had both thresholds right all along. These stay so
|
|
||||||
// the addresses resolve to the truth for the next reader.
|
|
||||||
//
|
//
|
||||||
static const Point3D HudZeroVector(0.0f, 0.0f, 0.0f); // DAT_004e0f74/78/7c
|
static const Point3D HudZeroVector(0.0f, 0.0f, 0.0f); // DAT_004e0f74/78/7c
|
||||||
// LOCK gate (a): your OWN HUD's host zone must be BELOW 75% damage, else
|
static const Scalar SegmentTempLimit = 0.0f; // _DAT_004b7ec4 (heat threshold for HUD page visibility)
|
||||||
// fire-control lock is lost -- the `_DAT_004b7ec4 <= ownZone->damageLevel`
|
static const Scalar TargetTempLimit = 0.0f; // _DAT_004b7ec8
|
||||||
// arm of HudSimulation. A shot-up cockpit drops to "target held, no lock".
|
static const Scalar RangeBias = 0.0f; // _DAT_004b7ecc
|
||||||
static const Scalar LockOwnZoneDamageLimit = 0.75f; // _DAT_004b7ec4
|
static const Scalar MaxTorsoSlew = 500.0f; // _DAT_004b7ed0
|
||||||
// LOCK gate (b): the TARGETED zone must be below 1.0 damage (a whole-mech
|
|
||||||
// target checks zone 0), so a dead zone cannot be re-locked.
|
|
||||||
static const Scalar LockTargetZoneDamageLimit = 1.0f; // _DAT_004b7ec8
|
|
||||||
// Subtracted from RangeToTarget (@0x1EC) every frame while the timed flag
|
|
||||||
// @0x22C is set (timer @0x21C accumulates to @0x1D8, then both clear).
|
|
||||||
// NOT reconstructed in the port's targeting step -- tracked, not implemented.
|
|
||||||
static const Scalar RangeBias = 100.0f; // _DAT_004b7ecc
|
|
||||||
// The shared ZERO: the right-hand side of the Abs() idiom on the range slide
|
|
||||||
// (`dt * 500.0 <= 0.0` picks the sign) and of an `== 0.0f` test at @0x28C.
|
|
||||||
// The 500 m/s slide RATE is an immediate literal (0x43fa0000), never this.
|
|
||||||
static const Scalar HudZero = 0.0f; // _DAT_004b7ed0
|
|
||||||
// The decay FLOOR for horizontalTorsoOffset (@0x294) in FUN_004b7ed4. The
|
|
||||||
// decay RATE is the object's own @0x298, not a constant. (Value verified.)
|
|
||||||
static const Scalar FlickerFloor = 0.0f; // _DAT_004b7f90
|
static const Scalar FlickerFloor = 0.0f; // _DAT_004b7f90
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -348,13 +322,9 @@ Logical
|
|||||||
// (+0x100), sliding at 500 m/s (:5652-5670), default 1200.0 with no
|
// (+0x100), sliding at 500 m/s (:5652-5670), default 1200.0 with no
|
||||||
// target; the compass Scalar @0x214 = yaw euler[0] + torso twist (:5676).
|
// target; the compass Scalar @0x214 = yaw euler[0] + torso twist (:5676).
|
||||||
// 6. Torso-horizon slew: horizontalTorsoOffset (@0x294) is moved toward the
|
// 6. Torso-horizon slew: horizontalTorsoOffset (@0x294) is moved toward the
|
||||||
// commanded torso heading, clamped to +/- horizontalLimit (@0x29C), then
|
// commanded torso heading at up to MaxTorsoSlew (500/sec), clamped to
|
||||||
// written to the graphic at mech +0x36C. The flicker helper (@004b7ed4)
|
// +/- horizontalLimit (@0x29C), then written to the graphic at
|
||||||
// damps the settle: it decays @0x294 toward ZERO at the object's own
|
// mech +0x36C. The flicker helper (@004b7ed4) damps the settle.
|
||||||
// @0x298 (horizontalMovementPerSecond) x time_slice. (CORRECTED
|
|
||||||
// 2026-08-08: this used to read "at up to MaxTorsoSlew (500/sec)" -- the
|
|
||||||
// rate is that per-object field, and there is no 500 constant here. The
|
|
||||||
// 500 m/s belongs to the RANGE slide in step 5, as an immediate.)
|
|
||||||
//
|
//
|
||||||
void
|
void
|
||||||
HUD::HudSimulation(Scalar time_slice)
|
HUD::HudSimulation(Scalar time_slice)
|
||||||
@@ -505,20 +475,3 @@ void
|
|||||||
{
|
{
|
||||||
ResetToInitialState(reset_command != 0); // @004b77bc
|
ResetToInitialState(reset_command != 0); // @004b77bc
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// BTSetHudFlickerActive -- complete-type bridge for the CONTROL-MODE switch
|
|
||||||
// (mechmppr.cpp treats the mech's subsystems as opaque pointers, so it cannot
|
|
||||||
// touch HUD members directly; same pattern as torso.cpp's BTGetTorsoTwistAddr).
|
|
||||||
//
|
|
||||||
// @004afbe0's BASIC arm ends in `*(mech+0x5b4 + 0x2a0) = 1` -- mech+0x5b4 is the
|
|
||||||
// HUD subsystem cache and +0x2A0 is flickerActive. Basic mode re-centres the
|
|
||||||
// torso, so the HUD horizon is kicked into its settle animation to follow it
|
|
||||||
// (UpdateFlicker @004b7ed4 decays horizontalTorsoOffset and reports whether it
|
|
||||||
// is still moving). The port never made this call.
|
|
||||||
//
|
|
||||||
void BTSetHudFlickerActive(Subsystem *hud)
|
|
||||||
{
|
|
||||||
if (hud != 0)
|
|
||||||
((HUD *)hud)->SetFlickerActive(1);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -196,12 +196,6 @@
|
|||||||
// Simulation Support
|
// Simulation Support
|
||||||
//
|
//
|
||||||
public:
|
public:
|
||||||
// @0x2A0 -- raised by the CONTROL-MODE switch's BASIC arm
|
|
||||||
// (`*(mech+0x5b4 + 0x2a0) = 1`, @004afbe0) so the HUD horizon re-settles
|
|
||||||
// with the torso that Basic just re-centred. Reached from mechmppr via
|
|
||||||
// hud.cpp's BTSetHudFlickerActive bridge (that TU sees Subsystem*, not HUD).
|
|
||||||
void SetFlickerActive(int on) { Check(this); flickerActive = on; }
|
|
||||||
|
|
||||||
typedef void
|
typedef void
|
||||||
(HUD::*Performance)(Scalar time_slice);
|
(HUD::*Performance)(Scalar time_slice);
|
||||||
|
|
||||||
|
|||||||
@@ -510,25 +510,8 @@ void
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// #142: duckState is the POSTURE the cockpit's crouch-symbol animation
|
duckState = 1;
|
||||||
// reads -- L4GAUGE.CFG:5001 binds attribute 0x37 to a THREE-frame
|
DEBUG_STREAM << "[duck] DuckRequest: duckState -> 1" << std::endl << std::flush;
|
||||||
// bduck.pcc strip, confirmed on screen as a duck animation:
|
|
||||||
// 0 = standing 1 = moving between 2 = crouched
|
|
||||||
//
|
|
||||||
// A bare 1 here is therefore exactly right, and is what the binary writes:
|
|
||||||
// it means "in transition", which is both the request AND the middle frame.
|
|
||||||
// The consumer (mech4.cpp) reads the parked leg alarm to decide DIRECTION
|
|
||||||
// -- parked means the pending move is a rise, not parked means a squat --
|
|
||||||
// and settles duckState to 0 or 2 when the clip finishes. No separate
|
|
||||||
// request cell, no toggle, no divergence from @0049fa00.
|
|
||||||
//
|
|
||||||
// (An earlier revision toggled 0<->1 here. That produced a two-pose snap,
|
|
||||||
// which is what the cockpit reported as "it lights up and sticks, no
|
|
||||||
// animation": frame 2 was never reachable.)
|
|
||||||
duckState = 1; // show the MIDDLE frame at once (the binary's write)
|
|
||||||
duckRequest = 1; // and remember that a move is pending (#142)
|
|
||||||
DEBUG_STREAM << "[duck] DuckRequest: duckState -> 1 (in transition)"
|
|
||||||
<< std::endl << std::flush;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -1032,12 +1015,6 @@ void
|
|||||||
{
|
{
|
||||||
Check(message);
|
Check(message);
|
||||||
|
|
||||||
// #150 -- the binary's ONCE-PER-DEATH latch (local_14 @0x4a0230 body,
|
|
||||||
// part_012.c:14625-14629): IsMechDestroyed() sampled AT ENTRY. The kill
|
|
||||||
// report posts only on the 0->1 edge (!destroyedAtEntry && destroyed-now);
|
|
||||||
// a later salvo of the killing volley takes the inflicted-only path.
|
|
||||||
const int destroyedAtEntry = IsMechDestroyed();
|
|
||||||
|
|
||||||
// MP DIAGNOSTIC (task #47): confirm the handler runs on the OWNING MASTER
|
// MP DIAGNOSTIC (task #47): confirm the handler runs on the OWNING MASTER
|
||||||
// for a cross-pod (network-delivered) TakeDamage + what state it carries.
|
// for a cross-pod (network-delivered) TakeDamage + what state it carries.
|
||||||
if (getenv("BT_MP_NET"))
|
if (getenv("BT_MP_NET"))
|
||||||
@@ -1093,31 +1070,7 @@ void
|
|||||||
// Capturing it after the divert (where #89 first placed it) meant a
|
// Capturing it after the divert (where #89 first placed it) meant a
|
||||||
// COLLISION death could never arm the tail.
|
// COLLISION death could never arm the tail.
|
||||||
//
|
//
|
||||||
// WAS-ALIVE-AT-ENTRY, on the BINARY's predicate (movementMode), not the
|
const int deathBlastArmed = !IsMechDestroyed(); // [ebp-0x10], inverted
|
||||||
// graphic alarm. FIXED 2026-08-07 -- this substitution was the eject-ghost.
|
|
||||||
//
|
|
||||||
// The binary tests movementMode 9|10 here; the port used IsMechDestroyed()
|
|
||||||
// (graphicAlarm >= 9) and justified it with "the death transition sets mode
|
|
||||||
// 9 synchronously with the structural flag on every path through here, so
|
|
||||||
// the edges coincide". That is true of every DAMAGE path and false of the
|
|
||||||
// one that matters: Mech::EjectPilotMessageHandler raises graphicAlarm to
|
|
||||||
// 10 (the EJECT state) BEFORE dispatching its self-damage, while
|
|
||||||
// movementMode is still 1. So on an eject the handler entered already
|
|
||||||
// reading "destroyed", the latch never armed, and the whole death tail was
|
|
||||||
// skipped -- no VehicleDead, which IS the respawn trigger.
|
|
||||||
//
|
|
||||||
// Consequences, all three reported from the field on the same night:
|
|
||||||
// * the ejecting player never respawns ("panic button, didn't respawn");
|
|
||||||
// * the peer wrecks the mech and never un-wrecks it, because the un-wreck
|
|
||||||
// rides the master's respawn -> the permanent EJECT GHOST (#108);
|
|
||||||
// * the eject scores only its -500 self-damage: no negated kill award and
|
|
||||||
// no -500 death cost, because both live in the tail that never ran --
|
|
||||||
// which is why the manual chart's "-1000 ejecting" never materialised.
|
|
||||||
//
|
|
||||||
// MovementMode 9|10 is untouched by the eject's alarm write, so the latch
|
|
||||||
// now arms on the eject exactly as it does on a combat death.
|
|
||||||
const int deathBlastArmed =
|
|
||||||
!(MovementMode() == 9 || MovementMode() == 10); // [ebp-0x10], inverted (@0x4a0303)
|
|
||||||
//
|
//
|
||||||
// The zone the reports + VehicleDead carry: msg+0x24 as of loop entry.
|
// The zone the reports + VehicleDead carry: msg+0x24 as of loop entry.
|
||||||
// The binary never rewrites msg+0x24 after the initial cylinder resolve;
|
// The binary never rewrites msg+0x24 after the initial cylinder resolve;
|
||||||
@@ -1248,31 +1201,11 @@ void
|
|||||||
// vital-wreck flag, and the loop-ENTRY zone. This retires
|
// vital-wreck flag, and the loop-ENTRY zone. This retires
|
||||||
// BTPostDamageScore/BTPostKillScore -- see the btplayer.cpp tombstone.
|
// BTPostDamageScore/BTPostKillScore -- see the btplayer.cpp tombstone.
|
||||||
//
|
//
|
||||||
// #150 DOUBLE-KILL FIX, two binary-grounded gates (2026-08-10):
|
|
||||||
//
|
|
||||||
// (1) MASTER ONLY. The binary OPENS this handler with the assertion
|
|
||||||
// `if ((flags & 0xc) == 4) Fail("Replicant Mech recieving takedam...",
|
|
||||||
// MECH.CPP, 0x3da)` (part_012.c:14613-14616) -- in 1995 a replicant
|
|
||||||
// executing TakeDamage was a BUG, so the report tail could only ever
|
|
||||||
// run on the victim's master, once. This port deliberately applies
|
|
||||||
// damage on replicants too (local visual responsiveness), so the tail
|
|
||||||
// ALSO ran there -- and the replicant's movement mode can't flip to
|
|
||||||
// wreck until the death record round-trips, so a multi-round killing
|
|
||||||
// volley posted a kill report PER ROUND inside that window. Measured:
|
|
||||||
// one death -> two type-2 arms 1ms apart, kills 0->2 (the field's
|
|
||||||
// "one kill counted twice", #150). Gate the REPORTS to the master;
|
|
||||||
// the local damage application stays.
|
|
||||||
//
|
|
||||||
// (2) ENTRY LATCH. The binary's newly-destroyed test is the 0->1 edge of
|
|
||||||
// IsMechDestroyed() across THIS handler call (local_14), not the
|
|
||||||
// movement-mode-derived deathBlastArmed -- on the master both agree,
|
|
||||||
// but the binary's form is the spec.
|
|
||||||
if (GetInstance() != ReplicantInstance)
|
|
||||||
{
|
{
|
||||||
extern void BTMechPostCombatReports(void *, void *, float, int, int, int);
|
extern void BTMechPostCombatReports(void *, void *, float, int, int, int);
|
||||||
BTMechPostCombatReports((void *)this, (void *)message,
|
BTMechPostCombatReports((void *)this, (void *)message,
|
||||||
(float)damageTally, zoneDestroyed, reportZone,
|
(float)damageTally, zoneDestroyed, reportZone,
|
||||||
!destroyedAtEntry && IsMechDestroyed());
|
deathBlastArmed && IsMechDestroyed());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1611,7 +1544,6 @@ Mech::Mech(
|
|||||||
radarLinearPosition = &localOrigin.linearPosition; // map reads the mech's live world position...
|
radarLinearPosition = &localOrigin.linearPosition; // map reads the mech's live world position...
|
||||||
radarAngularPosition= &localOrigin.angularPosition; // ...and orientation (pointers into the base origin)
|
radarAngularPosition= &localOrigin.angularPosition; // ...and orientation (pointers into the base origin)
|
||||||
duckState = 0; // not crouching
|
duckState = 0; // not crouching
|
||||||
duckRequest = 0; // no pending duck request (#142)
|
|
||||||
// (AUDIO_FIDELITY F7) missile alarm: the binary reset writes 0 / FLT_MAX
|
// (AUDIO_FIDELITY F7) missile alarm: the binary reset writes 0 / FLT_MAX
|
||||||
// (part_012.c:9446-9447; FLT_MAX = "no missile" far default)
|
// (part_012.c:9446-9447; FLT_MAX = "no missile" far default)
|
||||||
incomingLock = 0;
|
incomingLock = 0;
|
||||||
|
|||||||
@@ -964,15 +964,7 @@ protected:
|
|||||||
Scalar radarRange; // 0x2f RadarRange (scale/max)
|
Scalar radarRange; // 0x2f RadarRange (scale/max)
|
||||||
Point3D *radarLinearPosition; // 0x30 RadarLinearPosition
|
Point3D *radarLinearPosition; // 0x30 RadarLinearPosition
|
||||||
Quaternion *radarAngularPosition; // 0x31 RadarAngularPosition
|
Quaternion *radarAngularPosition; // 0x31 RadarAngularPosition
|
||||||
int duckState; // 0x37 DuckState (crouch POSTURE the cockpit
|
int duckState; // 0x37 DuckState (crouch posture)
|
||||||
// strip draws: 0 stand, 1 moving, 2 crouched)
|
|
||||||
// PORT-ONLY (#142): the pending duck REQUEST, kept separate from the
|
|
||||||
// posture above. duckState cannot carry both -- settling it to the
|
|
||||||
// real posture destroys the request, and the consumer then re-issues
|
|
||||||
// the opposite direction on the very frame the clip parks (benched:
|
|
||||||
// 69 squat/rise transitions from 2 presses). Not a binary field; it
|
|
||||||
// is appended, never read by offset.
|
|
||||||
int duckRequest;
|
|
||||||
// (AUDIO_FIDELITY F7) the incoming-missile alarm attributes. Binary
|
// (AUDIO_FIDELITY F7) the incoming-missile alarm attributes. Binary
|
||||||
// Mech table [T1]: IncomingLock id 54 @0x3fc (Logical; authored match
|
// Mech table [T1]: IncomingLock id 54 @0x3fc (Logical; authored match
|
||||||
// ==1 Start / ==0 Stop of the looped beeper), DistanceToMissile id 56
|
// ==1 Start / ==0 Stop of the looped beeper), DistanceToMissile id 56
|
||||||
|
|||||||
@@ -1246,10 +1246,6 @@ Scalar
|
|||||||
Mech::AdvanceBodyAnimation(Scalar time_slice, int loop)
|
Mech::AdvanceBodyAnimation(Scalar time_slice, int loop)
|
||||||
{
|
{
|
||||||
Scalar distance = 0.0f;
|
Scalar distance = 0.0f;
|
||||||
// #52 probe (BT_BODY_SM_LOG): case 0 and the inserted turn block below run
|
|
||||||
// in the SAME invocation, so a plain local proves the arm->reset pair --
|
|
||||||
// no cross-frame state, no per-mech bookkeeping.
|
|
||||||
int armedFromStanding = 0;
|
|
||||||
|
|
||||||
// In the binary `bodyAnimationState`@0x728 IS `bodyStateAlarm`'s level (one field);
|
// In the binary `bodyAnimationState`@0x728 IS `bodyStateAlarm`'s level (one field);
|
||||||
// the reconstruction split them, so SetBodyAnimation's `bodyStateAlarm.SetLevel(state)`
|
// the reconstruction split them, so SetBodyAnimation's `bodyStateAlarm.SetLevel(state)`
|
||||||
@@ -1301,35 +1297,7 @@ Scalar
|
|||||||
}
|
}
|
||||||
SetBodyAnimation(0x10);
|
SetBodyAnimation(0x10);
|
||||||
}
|
}
|
||||||
// FALLTHROUGH -- into the ADVANCE GROUP, which is where the binary
|
// FALLTHROUGH
|
||||||
// sends it. #52 SKATE ROOT CAUSE (2026-08-07): in FUN_004a5678 case 4
|
|
||||||
// is a MEMBER of the advance list (case 2,3,*4*,5,8,...), so a state
|
|
||||||
// just armed away from Standing lands on Advance(). The port's turn
|
|
||||||
// block below is an INSERTION (task #64 lockstep twin) and, sitting
|
|
||||||
// between case 0 and the advance group, it intercepted that fallthrough.
|
|
||||||
// On a REPLICANT that is fatal and not a race: case 0 arms walk iff
|
|
||||||
// `standSpeed < bodyTargetSpeed`, and the inserted block's exit tests
|
|
||||||
// `standSpeed < bspd` where bspd IS bodyTargetSpeed for a replicant --
|
|
||||||
// the SAME expression. Arm and reset therefore fire on the same frame,
|
|
||||||
// every frame, and a peer parked at Standing with a live replicated
|
|
||||||
// demand can never start cycling (reverse likewise: both sides test
|
|
||||||
// `< ZeroSpeed`). It cycles again only when a record sets the state
|
|
||||||
// directly (ReadUpdateRecord, mech.cpp) -- the observed self-recovery.
|
|
||||||
// Masters escape because their two tests read DIFFERENT cells
|
|
||||||
// (bodyTargetSpeed = last-sent vs the live mapper speedDemand) and
|
|
||||||
// because the body channel is mj=0 there, so its stall is invisible.
|
|
||||||
// Introduced by e91d447 (#82): before it the replicant branch read the
|
|
||||||
// dead mapper cell (0 forever), so the exit never fired and this
|
|
||||||
// fallthrough worked BY ACCIDENT. Fixing the dead cell closed the
|
|
||||||
// accidental escape hatch and the trn-lock skate came back as a
|
|
||||||
// Standing-lock skate. BT_NO_BODY_FALLTHRU=1 restores the old path.
|
|
||||||
armedFromStanding = (int)bodyStateAlarm.GetLevel();
|
|
||||||
{
|
|
||||||
static const int s_bodyFallthru = getenv("BT_NO_BODY_FALLTHRU") ? 0 : 1;
|
|
||||||
if (s_bodyFallthru)
|
|
||||||
goto advance_body_normally;
|
|
||||||
}
|
|
||||||
// FALLTHROUGH (legacy path only)
|
|
||||||
|
|
||||||
case 4: // TURN-IN-PLACE, LOCKSTEP twin (task #64)
|
case 4: // TURN-IN-PLACE, LOCKSTEP twin (task #64)
|
||||||
// The body channel runs trn in LOCKSTEP with the leg: armed together at
|
// The body channel runs trn in LOCKSTEP with the leg: armed together at
|
||||||
@@ -1356,23 +1324,6 @@ Scalar
|
|||||||
: (bm != 0) ? bm->speedDemand : 0.0f;
|
: (bm != 0) ? bm->speedDemand : 0.0f;
|
||||||
if (standSpeed < bspd || bspd < ZeroSpeed) // walk / reverse (leg-symmetric)
|
if (standSpeed < bspd || bspd < ZeroSpeed) // walk / reverse (leg-symmetric)
|
||||||
{
|
{
|
||||||
// #52 probe: when this fires on a state case 0 JUST armed, the
|
|
||||||
// mech is being pushed straight back to Standing on the same
|
|
||||||
// frame it tried to leave it -- the Standing-lock. On a
|
|
||||||
// replicant `bspd` IS the same cell case 0 tested, so the pair
|
|
||||||
// is unconditional, not a race.
|
|
||||||
if (armedFromStanding != 0 && getenv("BT_BODY_SM_LOG"))
|
|
||||||
{
|
|
||||||
static float s_bsm = 0.0f; s_bsm += time_slice;
|
|
||||||
if (s_bsm >= 1.0f) { s_bsm = 0.0f;
|
|
||||||
DEBUG_STREAM << "[bodySM] " << (GetInstance() == ReplicantInstance
|
|
||||||
? "REPLICANT " : "master ")
|
|
||||||
<< GetEntityID() << " case0 armed " << armedFromStanding
|
|
||||||
<< " -> turn-block RESET to Standing bspd=" << (float)bspd
|
|
||||||
<< " bts=" << (float)bodyTargetSpeed
|
|
||||||
<< " standSpeed=" << (float)standSpeed
|
|
||||||
<< " (STANDING-LOCK)\n" << std::flush; }
|
|
||||||
}
|
|
||||||
bodyStateAlarm.SetLevel(0);
|
bodyStateAlarm.SetLevel(0);
|
||||||
ForceUpdate(8);
|
ForceUpdate(8);
|
||||||
distance = 0.0f;
|
distance = 0.0f;
|
||||||
@@ -1396,7 +1347,6 @@ Scalar
|
|||||||
case 2: case 3: case 5: case 8: case 9: case 10: case 0x0b:
|
case 2: case 3: case 5: case 8: case 9: case 10: case 0x0b:
|
||||||
case 0x0e: case 0x0f: case 0x10: case 0x11: case 0x14: case 0x15:
|
case 0x0e: case 0x0f: case 0x10: case 0x11: case 0x14: case 0x15:
|
||||||
case 0x1c: case 0x1d: case 0x1e: case 0x1f: case 0x20:
|
case 0x1c: case 0x1d: case 0x1e: case 0x1f: case 0x20:
|
||||||
advance_body_normally: // case 0's fallthrough target (leg twin: advance_normally)
|
|
||||||
distance = bodyAnimation.Advance( // FUN_0042790c(this+0x6bc, ...)
|
distance = bodyAnimation.Advance( // FUN_0042790c(this+0x6bc, ...)
|
||||||
time_slice * globalTimeScale * idleStrideScale, loop);
|
time_slice * globalTimeScale * idleStrideScale, loop);
|
||||||
bodyCycleSpeed = distance / time_slice; // this+0x6b8
|
bodyCycleSpeed = distance / time_slice; // this+0x6b8
|
||||||
|
|||||||
+21
-502
@@ -666,7 +666,6 @@ static int gBTPPCKey = 0;
|
|||||||
static int gBTMissileKey = 0;
|
static int gBTMissileKey = 0;
|
||||||
static int gBTPinkyKey = 0; // key '4' = the pod's 4th fire button (Pinky 0x45)
|
static int gBTPinkyKey = 0; // key '4' = the pod's 4th fire button (Pinky 0x45)
|
||||||
int gBTModeCycle = 0; // 'M' edge: cycle the control mode (mapper consumes)
|
int gBTModeCycle = 0; // 'M' edge: cycle the control mode (mapper consumes)
|
||||||
int gBTMyoTrace = 0; // #137: frames of post-reset myomer tracing left (armed by Mech::Reset)
|
|
||||||
int gBTDisplayCycle = 0; // 'N' edge: cycle the secondary schematic (Gitea #6, mapper consumes)
|
int gBTDisplayCycle = 0; // 'N' edge: cycle the secondary schematic (Gitea #6, mapper consumes)
|
||||||
int gBTPresetCycle[3] = {0,0,0}; // J/K/L edges: cycle an upper-MFD preset page (Gitea #9, L4 mapper consumes)
|
int gBTPresetCycle[3] = {0,0,0}; // J/K/L edges: cycle an upper-MFD preset page (Gitea #9, L4 mapper consumes)
|
||||||
//
|
//
|
||||||
@@ -791,26 +790,8 @@ void
|
|||||||
EntitySegment *seg = m->GetSegment(segIndex); // owner+0x300 table, GetNth(index)
|
EntitySegment *seg = m->GetSegment(segIndex); // owner+0x300 table, GetNth(index)
|
||||||
if (seg != 0)
|
if (seg != 0)
|
||||||
{
|
{
|
||||||
// #141 -- THE BINARY GOES THROUGH FUN_00424da8, AND SO MUST WE.
|
AffineMatrix mw;
|
||||||
// @004b9948 ends in `FUN_00424da8(owner, segment, out)`, which is
|
mw.Multiply(seg->GetSegmentToEntity(), m->localToWorld); // segment -> world (== mech4 gun-port path)
|
||||||
// JointedMover::GetSegmentToWorld instruction-for-instruction:
|
|
||||||
// iVar1 = FUN_00417ab4(param_1 + 0x31c); // GetJointSubsystem()
|
|
||||||
// if (*(int *)(iVar1 + 0xfc) != 0) { // AreJointsModified()
|
|
||||||
// ...walk owner+0x300 setting seg+0xc = 1... // ModifySegment()
|
|
||||||
// *(int *)(iVar1 + 0xfc) = 0; // ModifyJoints(False)
|
|
||||||
// }
|
|
||||||
// FUN_0040b104(out, FUN_004244dc(seg), owner+0xd0); // x localToWorld
|
|
||||||
// So in the 1995 image EVERY muzzle query performs the joints->segments
|
|
||||||
// refresh. This port hand-composed GetSegmentToEntity() x localToWorld
|
|
||||||
// and skipped it -- and GetSegmentToEntity only recomputes when
|
|
||||||
// segmentModified is already set (SEGMENT.cpp:262), so it returned a
|
|
||||||
// stale cache. On the MASTER that was invisible (the render pass
|
|
||||||
// refreshes the local mech every frame); on a REPLICANT nothing did, so
|
|
||||||
// peer muzzles sat at the BIND POSE -- the missile launched along the
|
|
||||||
// leg facing (#141). Use the engine accessor; do NOT force the dirty
|
|
||||||
// flag, the binary does not.
|
|
||||||
LinearMatrix mw;
|
|
||||||
m->GetSegmentToWorld(*seg, &mw);
|
|
||||||
out = mw; // Point3D = matrix W_Axis translation
|
out = mw; // Point3D = matrix W_Axis translation
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -1063,13 +1044,8 @@ int
|
|||||||
{
|
{
|
||||||
if (seg->GetIndex() == seg_index)
|
if (seg->GetIndex() == seg_index)
|
||||||
{
|
{
|
||||||
// #141 sweep: go through the engine accessor, which is the
|
AffineMatrix mw;
|
||||||
// binary's FUN_00424da8 (the joints->segments refresh). A hand
|
mw.Multiply(seg->GetSegmentToEntity(), m->localToWorld);
|
||||||
// composed GetSegmentToEntity() x localToWorld reads a STALE
|
|
||||||
// cache on any mech whose segments were not refreshed this frame
|
|
||||||
// -- i.e. every REPLICANT. See BTResolveWeaponMuzzle.
|
|
||||||
LinearMatrix mw;
|
|
||||||
m->GetSegmentToWorld(*seg, &mw);
|
|
||||||
p = mw; // Point3D = matrix translation
|
p = mw; // Point3D = matrix translation
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1517,57 +1493,8 @@ void
|
|||||||
EntitySegment *seg = sm->GetSegment(muzzle_seg);
|
EntitySegment *seg = sm->GetSegment(muzzle_seg);
|
||||||
if (seg != 0)
|
if (seg != 0)
|
||||||
{
|
{
|
||||||
// #141 -- USE THE ENGINE ACCESSOR, not a hand-rolled
|
AffineMatrix mw;
|
||||||
// GetSegmentToEntity() x localToWorld.
|
mw.Multiply(seg->GetSegmentToEntity(), sm->localToWorld);
|
||||||
//
|
|
||||||
// EntitySegment::GetSegmentToEntity (SEGMENT.cpp:262)
|
|
||||||
// recomputes ONLY when `segmentModified` is set; otherwise it
|
|
||||||
// returns the CACHED matrix. The thing that sets that flag
|
|
||||||
// after a joint moves is JointedMover::GetSegmentToWorld
|
|
||||||
// (JMOVER.cpp:136-146): it tests AreJointsModified() and, when
|
|
||||||
// set, marks EVERY segment dirty and clears the flag. Compose
|
|
||||||
// the matrix by hand and you skip that entirely -- you read
|
|
||||||
// whatever cache happens to be sitting there.
|
|
||||||
//
|
|
||||||
// On the MASTER that was invisible: the renderer / cockpit
|
|
||||||
// camera call GetSegmentToWorld for the LOCAL mech every frame,
|
|
||||||
// so the cache was already fresh when we fired. A REPLICANT
|
|
||||||
// gets no such refresh, so its cache stayed at the BIND POSE
|
|
||||||
// and the torso twist never reached the launch frame.
|
|
||||||
//
|
|
||||||
// Measured (scratchpad/night13/missileframe.sh, 165 salvos
|
|
||||||
// mirrored 1:1): master |twistDelta| max 2.2962 / mean 1.2283,
|
|
||||||
// 100% > 0.1 rad -- REPLICANT max 0.0000, mean 0.0000, 0%,
|
|
||||||
// with segResolved=1 and segYaw == bodyYaw EXACTLY, while that
|
|
||||||
// same peer's copy torso was demonstrably writing its joint
|
|
||||||
// (`PushTwist COPY ... twist=-1.49601`) off correctly
|
|
||||||
// replicated records (`cur=-2.13987 target=-2.13987 copy=1`).
|
|
||||||
// Twist arrived, joint moved, segment cache never refreshed.
|
|
||||||
// That is #141: "missiles launch along the LEG/FOOT facing,
|
|
||||||
// then curve to the target -- peer POV only".
|
|
||||||
// FORCE the recompute. GetSegmentToWorld only refreshes when
|
|
||||||
// AreJointsModified() is set, and by fire time the frame's
|
|
||||||
// renderer/camera pass has already consumed and cleared that
|
|
||||||
// flag on BOTH nodes (measured: jointsDirty=0 master AND peer).
|
|
||||||
// On the master the cache it left behind is correct, because
|
|
||||||
// that pass ran AFTER the local torso pushed its joint. On a
|
|
||||||
// replicant the cache is stale, so seg 18 returned its
|
|
||||||
// bind-pose matrix (segYaw == bodyYaw EXACTLY) even though the
|
|
||||||
// hierarchy is identical -- same parentIdx 4, same non-null
|
|
||||||
// parent + joint subsystem. Setting the flag makes
|
|
||||||
// GetSegmentToWorld mark every segment dirty so the whole
|
|
||||||
// chain re-derives from the CURRENT joint angles. Costs one
|
|
||||||
// segment-table walk per salvo.
|
|
||||||
// NO forced dirty flag here. An earlier pass set
|
|
||||||
// ModifyJoints(True) before this read; it bought 64% of
|
|
||||||
// salvos but it is NOT what the binary does -- @00424da8
|
|
||||||
// tests AreJointsModified() and never sets it. The authentic
|
|
||||||
// refresh happens in the MUZZLE query (GetMuzzlePoint ->
|
|
||||||
// @00424da8), which the launcher calls just above this, so by
|
|
||||||
// the time we compose the launch frame the segment cache is
|
|
||||||
// already current. See BTResolveWeaponMuzzle.
|
|
||||||
LinearMatrix mw;
|
|
||||||
sm->GetSegmentToWorld(*seg, &mw);
|
|
||||||
mw.GetFromAxis(X_Axis, &ax);
|
mw.GetFromAxis(X_Axis, &ax);
|
||||||
mw.GetFromAxis(Y_Axis, &ay);
|
mw.GetFromAxis(Y_Axis, &ay);
|
||||||
mw.GetFromAxis(Z_Axis, &az);
|
mw.GetFromAxis(Z_Axis, &az);
|
||||||
@@ -1580,55 +1507,6 @@ void
|
|||||||
sm->localToWorld.GetFromAxis(Y_Axis, &ay);
|
sm->localToWorld.GetFromAxis(Y_Axis, &ay);
|
||||||
sm->localToWorld.GetFromAxis(Z_Axis, &az);
|
sm->localToWorld.GetFromAxis(Z_Axis, &az);
|
||||||
}
|
}
|
||||||
// #141 DIAGNOSTIC (BT_PROJ_LOG). The peer-POV report is that the
|
|
||||||
// round leaves along the LEG facing, ignoring torso twist, while the
|
|
||||||
// shooter's own view is correct. Both nodes pass GetSegmentIndex()
|
|
||||||
// as the mount frame, so if this is real the difference is whether
|
|
||||||
// the SEGMENT actually carries the twist on a replicant. Print the
|
|
||||||
// frame we launched through on BOTH sides: twistDelta is the yaw of
|
|
||||||
// the launch forward vs the BODY forward, so it should equal the
|
|
||||||
// torso twist on the master and MUST match on the replicant. A
|
|
||||||
// replicant reading ~0 while the master reads non-zero IS the bug.
|
|
||||||
if (getenv("BT_PROJ_LOG"))
|
|
||||||
{
|
|
||||||
UnitVector bz;
|
|
||||||
sm->localToWorld.GetFromAxis(Z_Axis, &bz);
|
|
||||||
const float kPi = 3.14159265f;
|
|
||||||
float segYaw = atan2f(-(float)az.x, -(float)az.z);
|
|
||||||
float bodyYaw = atan2f(-(float)bz.x, -(float)bz.z);
|
|
||||||
float dYaw = segYaw - bodyYaw;
|
|
||||||
while (dYaw > kPi) dYaw -= 2.0f * kPi;
|
|
||||||
while (dYaw < -kPi) dYaw += 2.0f * kPi;
|
|
||||||
// #141 probe 2: GetSegmentToEntity only RECOMPUTES when
|
|
||||||
// (segmentModified && parentSegment). A null parent means it can
|
|
||||||
// never recompute -- it returns the bind-pose baseOffset forever,
|
|
||||||
// which would read as segYaw == bodyYaw exactly. Print the
|
|
||||||
// hierarchy + joint-dirty state so master and peer can be diffed.
|
|
||||||
EntitySegment *pseg = (muzzle_seg >= 0) ? sm->GetSegment(muzzle_seg) : 0;
|
|
||||||
const void *parent = (pseg != 0) ? (const void *)pseg->GetParent() : 0;
|
|
||||||
int parentIdx = (pseg != 0) ? pseg->GetParentIndex() : -99;
|
|
||||||
JointSubsystem *js = sm->GetJointSubsystem();
|
|
||||||
// #148: the SHOOTER's live torso twist AT THIS INSTANT. The
|
|
||||||
// [torso-copy] probe samples every 120th call, so its "first
|
|
||||||
// non-zero" tells you when it first SAMPLED, not when the twist
|
|
||||||
// started -- that is exactly the artifact that made the earlier
|
|
||||||
// "the peer had no twist to carry" reading look right. Read the
|
|
||||||
// cell directly instead, so twistDelta and the twist that should
|
|
||||||
// be driving it are on the SAME line.
|
|
||||||
extern Scalar *BTGetTorsoTwistAddr(Subsystem *torso);
|
|
||||||
Scalar *twp = BTGetTorsoTwistAddr(sm->GetTorsoSubsystem());
|
|
||||||
DEBUG_STREAM << "[launchframe] "
|
|
||||||
<< (sm->GetInstance() == Entity::ReplicantInstance
|
|
||||||
? "REPLICANT" : "master ")
|
|
||||||
<< " liveTwist=" << (twp != 0 ? (float)*twp : -99.0f)
|
|
||||||
<< " seg=" << muzzle_seg << " segResolved=" << haveFrame
|
|
||||||
<< " segYaw=" << segYaw << " bodyYaw=" << bodyYaw
|
|
||||||
<< " twistDelta=" << dYaw
|
|
||||||
<< " parent=" << parent << " parentIdx=" << parentIdx
|
|
||||||
<< " joints=" << (void *)js
|
|
||||||
<< " jointsDirty=" << (js != 0 ? (int)js->AreJointsModified() : -1)
|
|
||||||
<< "\n" << std::flush;
|
|
||||||
}
|
|
||||||
p.vel.x = ax.x*launch_velocity->x + ay.x*launch_velocity->y - az.x*launch_velocity->z;
|
p.vel.x = ax.x*launch_velocity->x + ay.x*launch_velocity->y - az.x*launch_velocity->z;
|
||||||
p.vel.y = ax.y*launch_velocity->x + ay.y*launch_velocity->y - az.y*launch_velocity->z;
|
p.vel.y = ax.y*launch_velocity->x + ay.y*launch_velocity->y - az.y*launch_velocity->z;
|
||||||
p.vel.z = ax.z*launch_velocity->x + ay.z*launch_velocity->y - az.z*launch_velocity->z;
|
p.vel.z = ax.z*launch_velocity->x + ay.z*launch_velocity->y - az.z*launch_velocity->z;
|
||||||
@@ -2160,11 +2038,8 @@ void
|
|||||||
{
|
{
|
||||||
if (seg->GetIndex() == segment_index)
|
if (seg->GetIndex() == segment_index)
|
||||||
{
|
{
|
||||||
// #141 sweep: engine accessor (== the binary's FUN_00424da8),
|
AffineMatrix mw;
|
||||||
// not a hand-composed product -- otherwise a peer's damage
|
mw.Multiply(seg->GetSegmentToEntity(), mech->localToWorld);
|
||||||
// effect anchors to the BIND-POSE segment.
|
|
||||||
LinearMatrix mw;
|
|
||||||
mech->GetSegmentToWorld(*seg, &mw);
|
|
||||||
fxPos = mw; // Point3D = matrix translation
|
fxPos = mw; // Point3D = matrix translation
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -2271,39 +2146,6 @@ void
|
|||||||
worldLinearVelocity = Vector3D(0.0f, 0.0f, 0.0f);
|
worldLinearVelocity = Vector3D(0.0f, 0.0f, 0.0f);
|
||||||
localVelocity = Motion::Identity;
|
localVelocity = Motion::Identity;
|
||||||
frameEntryWorldVelocity = Vector3D(0.0f, 0.0f, 0.0f);
|
frameEntryWorldVelocity = Vector3D(0.0f, 0.0f, 0.0f);
|
||||||
// Binary zero-fills restored (Reset @0049fb74 zero-fills FOUR Motion cells:
|
|
||||||
// +0x1c4/+0x1dc/+0x298/+0x2c8; +0x1dc = localAcceleration by the engine
|
|
||||||
// Mover layout). NOTE: zeroing these did NOT fix #137 on its own -- the
|
|
||||||
// acceleration snapshot is REBUILT from a position finite-difference one
|
|
||||||
// frame later (see accelPrevPos below), so the stale-carryover story first
|
|
||||||
// written here was wrong. The zero-fills stay because the binary does them.
|
|
||||||
localAcceleration = Motion::Identity; // binary +0x1dc zero-fill
|
|
||||||
worldLinearAcceleration = Vector3D(0.0f, 0.0f, 0.0f); // world-space mirror
|
|
||||||
|
|
||||||
// #137 ROOT CAUSE -- the DROPPED RE-SEED. The binary Reset's SECOND
|
|
||||||
// instruction is FUN_00408440(mech+0x58c, param_2): re-seed the Point3D at
|
|
||||||
// +0x58c to the NEW ORIGIN. +0x58c is the previous-position memory of the
|
|
||||||
// AccelerationLastFrame ring feed (+0x81c/0x824/0x828/0x82c -- the F19
|
|
||||||
// block below, port member accelPrevPos). This port reconstructed the
|
|
||||||
// ring (ctor part_012.c:9836-9840, derivative :15169-15195) but its Reset
|
|
||||||
// never got the +0x58c line -- so the first post-respawn sample computed
|
|
||||||
// |newPos - accelPrevPos| / dt = TELEPORT DISTANCE / dt ~ 1e5
|
|
||||||
// into the velocity ring, and the ring-mean derivative turned that into an
|
|
||||||
// AccelerationLastFrame spike of 3e4..2.4e5 (pure forward/-z; with a +z
|
|
||||||
// ECHO ~15 frames later as the garbage sample rotates out of the mean).
|
|
||||||
// The myomer drive-heat integrator (@004b8d18) then computed
|
|
||||||
// termAccel = (1-accEff) * |v| * |a| * mass * dt
|
|
||||||
// with |a|~1e5 while the gait re-published |v|~40 under a still-held
|
|
||||||
// throttle: ONE tick deposited ~2.7e9 into pendingHeat -> heatEnergy,
|
|
||||||
// snapping the freshly-reset myomers from T=77 to T~9000 (failT=2000) ->
|
|
||||||
// speedEffect 0 -> speedDemand *= 0 -> "respawned unable to move until it
|
|
||||||
// cools" (#137). Measured: top deposits 3.35e9/3.29e9/3.05e9, every one
|
|
||||||
// 3-4 log lines after a Mech::Reset, aXYZ pure z, master mech.
|
|
||||||
// Why ~8% in the field: the deposit needs |v| in the SAME 1-2 frames, so
|
|
||||||
// only pilots whose throttle is still forward at the respawn (physical
|
|
||||||
// lever / HOTAS -- exactly who reported it) get the freeze; idle-throttle
|
|
||||||
// respawns read v~0 and deposit nothing.
|
|
||||||
accelPrevPos = origin.linearPosition; // binary +0x58c re-seed
|
|
||||||
ramLastVictim = 0;
|
ramLastVictim = 0;
|
||||||
ramContactLinger = 0.0f;
|
ramContactLinger = 0.0f;
|
||||||
// StopAllEntityEffects (@004d0c14): a respawned mech must not trail its
|
// StopAllEntityEffects (@004d0c14): a respawned mech must not trail its
|
||||||
@@ -2330,26 +2172,6 @@ void
|
|||||||
poseSyncLatch = 0; // @0x77c
|
poseSyncLatch = 0; // @0x77c
|
||||||
graphicAlarm.SetLevel(0); // clear >=9 (the vital-kill trigger)
|
graphicAlarm.SetLevel(0); // clear >=9 (the vital-kill trigger)
|
||||||
|
|
||||||
// --- STAND THE MECH UP. Restored 2026-08-07 (#142, Oracle: "crouch wasn't
|
|
||||||
// resetting on respawn"). These are in the binary's own Reset sweep and the
|
|
||||||
// port had dropped all of them, so a pilot who died CROUCHED respawned
|
|
||||||
// crouched -- leg parked in 'sqd', the cockpit strip still showing the
|
|
||||||
// up-arrow "press to rise" frame. Verbatim from FUN_0049fb74:
|
|
||||||
// *(this+0x398) = 0 duckState -- not crouching
|
|
||||||
// FUN_0041bbd8(this+0x39c, 0) legStateAlarm -> 0 (standing)
|
|
||||||
// FUN_0041bbd8(this+0x714, 0) bodyStateAlarm -> 0 (standing)
|
|
||||||
// *(this+0x650/0x654/0x658) = 0 death + leg/body reset latches
|
|
||||||
// *(this+0x5ac) = 1.0f idleStrideScale
|
|
||||||
duckState = 0; // @0x398
|
|
||||||
duckRequest = 0; // port-only pending flag (#142)
|
|
||||||
legStateAlarm.SetLevel(0); // @0x39c -- stand
|
|
||||||
bodyStateAlarm.SetLevel(0); // @0x714 -- stand
|
|
||||||
stabilityAlarm.SetLevel(1); // risen (the rise path's value)
|
|
||||||
deathAnimationLatched = 0; // @0x650
|
|
||||||
legResetLatch = 0; // @0x654
|
|
||||||
bodyResetLatch = 0; // @0x658
|
|
||||||
idleStrideScale = 1.0f; // @0x5ac = 0x3f800000
|
|
||||||
|
|
||||||
// --- HEAL every damage zone: full structure, intact skin, no burning ---
|
// --- HEAL every damage zone: full structure, intact skin, no burning ---
|
||||||
for (int z = 0; z < damageZoneCount; ++z)
|
for (int z = 0; z < damageZoneCount; ++z)
|
||||||
{
|
{
|
||||||
@@ -2407,62 +2229,6 @@ void
|
|||||||
BTRecomputeCondenserValves((Entity *)this);
|
BTRecomputeCondenserValves((Entity *)this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// #137 forensic: sample every heat-bearing subsystem's temperature AT the
|
|
||||||
// reset, so "respawn came back with heat MAXED" can be split into "the
|
|
||||||
// reset did not clear it" vs "it climbs again immediately".
|
|
||||||
{
|
|
||||||
extern void BTReportHeatAtReset(void *mech_v);
|
|
||||||
BTReportHeatAtReset((void *)this);
|
|
||||||
// #137: sample the myomers AT the reset, and arm the POST-reset trace.
|
|
||||||
// Sampling only here is what got this bug wrongly closed -- every
|
|
||||||
// subsystem reads T == start at this instant, which looks innocent.
|
|
||||||
// The freeze shows up in the frames AFTER.
|
|
||||||
extern void BTReportMyomerFreeze(void *mech_v, const char *when);
|
|
||||||
extern int gBTMyoTrace;
|
|
||||||
BTReportMyomerFreeze((void *)this, "at-reset");
|
|
||||||
gBTMyoTrace = 240; // ~4 s of post-reset frames
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- DESKTOP THROTTLE RELEASE (#146) -- PORT LAYER, desktop-only ---------
|
|
||||||
// The pod's throttle is a PHYSICAL lever and the binary deliberately
|
|
||||||
// leaves it alone across a respawn: Reset's subsystem loop starts at
|
|
||||||
// index 2 and the ControlsMapper is index 0, so a pod pilot comes back
|
|
||||||
// under whatever power their hand is still holding. Authentic; it stays.
|
|
||||||
//
|
|
||||||
// The desktop bridge only EMULATES that lever, with the persistent ramp
|
|
||||||
// accumulator sLever below -- and a pad/keyboard pilot is physically
|
|
||||||
// holding nothing, with the lever position invisible to them. So they
|
|
||||||
// respawned at speed for no reason they could see, and the mech earned a
|
|
||||||
// real heat load straight out of the drop zone (myomers -> Condenser5 ->
|
|
||||||
// GeneratorD all tripping Overheating inside 1-2s). That is the true
|
|
||||||
// cause of the "#137 respawn came back with heat MAXED" reports -- the
|
|
||||||
// reset itself was always clean. Field-diagnosed from Sauron's
|
|
||||||
// 2026-08-06 log, which reads thr=1 / cycleSpeed=14.6 AT the reset.
|
|
||||||
//
|
|
||||||
// Reuse the existing all-stop path instead of touching sLever directly:
|
|
||||||
// it already zeroes the lever AND clears the zero-crossing detent, and it
|
|
||||||
// lives inside the virtual-controls block that owns that state.
|
|
||||||
//
|
|
||||||
// LOCAL VIEWPOINT MECH ONLY -- gBTDrive is the local bridge's state and
|
|
||||||
// Reset also runs for replicants, so an ungated write here would all-stop
|
|
||||||
// the player every time a REMOTE mech respawned. (Same guard idiom as
|
|
||||||
// the isPlayerMech test in PerformAndWatch.) Pod-safe besides: with a RIO
|
|
||||||
// present mechmppr's key bridge is off (BTRIODevicePresent, mechmppr.cpp
|
|
||||||
// :672) and gBTDrive.throttle is never read at all.
|
|
||||||
// BT_NO_RESPAWN_THROTTLE_RELEASE=1 reverts.
|
|
||||||
if (application != 0 && (Entity *)this == application->GetViewpointEntity())
|
|
||||||
{
|
|
||||||
static const int s_releaseThrottle =
|
|
||||||
getenv("BT_NO_RESPAWN_THROTTLE_RELEASE") ? 0 : 1;
|
|
||||||
if (s_releaseThrottle)
|
|
||||||
{
|
|
||||||
gBTDrive.allStop = 1;
|
|
||||||
if (getenv("BT_DEATH_LOG"))
|
|
||||||
DEBUG_STREAM << "[respawn] desktop throttle released (all-stop queued)\n"
|
|
||||||
<< std::flush;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- locomotion pre-run + interest gates (a reset master must tick) ---
|
// --- locomotion pre-run + interest gates (a reset master must tick) ---
|
||||||
SetPreRunFlag();
|
SetPreRunFlag();
|
||||||
if (interestCount == 0) interestCount = 1;
|
if (interestCount == 0) interestCount = 1;
|
||||||
@@ -2631,14 +2397,6 @@ void
|
|||||||
// (wreck shape: alarms/state settle, ammo
|
// (wreck shape: alarms/state settle, ammo
|
||||||
// bins do NOT refill the corpse)
|
// bins do NOT refill the corpse)
|
||||||
}
|
}
|
||||||
// #137 forensic: roster heat state right AFTER the death shutdown sweep --
|
|
||||||
// whatever is still hot here is what the wreck period starts from, and a
|
|
||||||
// member that stays hot through BOTH sweeps is the conduction source that
|
|
||||||
// cooks the fresh myomers after the respawn.
|
|
||||||
{
|
|
||||||
extern void BTReportMyomerFreeze(void *mech_v, const char *when);
|
|
||||||
BTReportMyomerFreeze((void *)this, "at-death");
|
|
||||||
}
|
|
||||||
// Request the DEATH record BEFORE entering the disabled state (the
|
// Request the DEATH record BEFORE entering the disabled state (the
|
||||||
// ForceUpdate filter masks types 2..8 once IsDisabled) -- the binary
|
// ForceUpdate filter masks types 2..8 once IsDisabled) -- the binary
|
||||||
// death sender is Force(1) + Force(0x40) (@0x4aab2f/@0x4aab3a). The
|
// death sender is Force(1) + Force(0x40) (@0x4aab2f/@0x4aab3a). The
|
||||||
@@ -2754,27 +2512,6 @@ volatile float gBTReplRenderYaw = -999.0f;
|
|||||||
void
|
void
|
||||||
Mech::PerformAndWatch(const Time& till, MemoryStream *update_stream)
|
Mech::PerformAndWatch(const Time& till, MemoryStream *update_stream)
|
||||||
{
|
{
|
||||||
// #148 probe: one-shot per mech, the FIRST time this mech's per-frame
|
|
||||||
// performance runs. Every other receipt in this file is anonymous, so
|
|
||||||
// master and replicant lines are indistinguishable in a 2-node log -- which
|
|
||||||
// is exactly what made the "when does the peer torso start ticking?" search
|
|
||||||
// go in circles. Name the mech.
|
|
||||||
if (getenv("BT_NET_TRACE"))
|
|
||||||
{
|
|
||||||
static const Mech *s_seen[16]; static int s_seenN = 0;
|
|
||||||
int known = 0;
|
|
||||||
for (int si = 0; si < s_seenN; ++si) if (s_seen[si] == this) { known = 1; break; }
|
|
||||||
if (!known && s_seenN < 16)
|
|
||||||
{
|
|
||||||
s_seen[s_seenN++] = this;
|
|
||||||
DEBUG_STREAM << "[perf-first] mech " << GetEntityID()
|
|
||||||
<< " instance=" << (GetInstance() == Entity::ReplicantInstance
|
|
||||||
? "REPLICANT" : "master")
|
|
||||||
<< " this=" << (const void *)this
|
|
||||||
<< " subsysCount=" << subsystemCount << "\n" << std::flush;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Frame time slice from the simulation clock (same idiom as Mover::Perform).
|
// Frame time slice from the simulation clock (same idiom as Mover::Perform).
|
||||||
Scalar dt = till - lastPerformance;
|
Scalar dt = till - lastPerformance;
|
||||||
lastPerformance = till;
|
lastPerformance = till;
|
||||||
@@ -3574,23 +3311,9 @@ void
|
|||||||
sEjAt = (e && *e) ? atoi(e) : -1;
|
sEjAt = (e && *e) ? atoi(e) : -1;
|
||||||
}
|
}
|
||||||
++sEjFrame;
|
++sEjFrame;
|
||||||
// Receipt: five separate rigs failed to fire a punch-out
|
|
||||||
// and it was never established whether this hook is even
|
|
||||||
// REACHED. Announce once a second while armed.
|
|
||||||
if (sEjAt > 0)
|
|
||||||
{
|
|
||||||
static int sEjLog = 0;
|
|
||||||
if ((++sEjLog % 60) == 0)
|
|
||||||
DEBUG_STREAM << "[ejecttest] armed at " << sEjAt
|
|
||||||
<< ", frame " << sEjFrame << "\n" << std::flush;
|
|
||||||
}
|
|
||||||
if (sEjAt > 0 && sEjFrame >= sEjAt
|
if (sEjAt > 0 && sEjFrame >= sEjAt
|
||||||
&& ((sEjFrame - sEjAt) % 300) == 0)
|
&& ((sEjFrame - sEjAt) % 300) == 0)
|
||||||
{
|
|
||||||
ejectPress = 1;
|
ejectPress = 1;
|
||||||
DEBUG_STREAM << "[ejecttest] FIRING punch-out at frame "
|
|
||||||
<< sEjFrame << "\n" << std::flush;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (ejectPress)
|
if (ejectPress)
|
||||||
{
|
{
|
||||||
@@ -4683,60 +4406,9 @@ void
|
|||||||
// stability, so peers pose the squat for free) and flip the
|
// stability, so peers pose the squat for free) and flip the
|
||||||
// stability alarm (ducked = 0, risen = 1). The request is
|
// stability alarm (ducked = 0, risen = 1). The request is
|
||||||
// consumed whenever both gates passed, hit or miss.
|
// consumed whenever both gates passed, hit or miss.
|
||||||
// #142 RESTRUCTURE 2026-08-07 -- duckState is the POSTURE, not a
|
if (duckState != 0 && squatCapable != 0)
|
||||||
// one-shot request, because the COCKPIT ANIMATION reads it.
|
|
||||||
//
|
|
||||||
// L4GAUGE.CFG:5001 authors
|
|
||||||
// oneOfSeveralPixInt(E,ModeAlwaysActive,bduck.pcc,3,1,DuckState)
|
|
||||||
// -- a 3-frame mech-symbol strip beside the CROUCH button, indexed by
|
|
||||||
// attribute 0x37 (= duckState). Asset (content/GAUGE/BDUCK.PCC),
|
|
||||||
// element (OneOfSeveralPixInt @004c5204) and factory registration
|
|
||||||
// (btl4grnd.cpp) are all present. The animation never played because
|
|
||||||
// the old consumer zeroed duckState the frame after the press, in
|
|
||||||
// BOTH directions -- so the strip sat on frame 0 with a one-frame
|
|
||||||
// blip to frame 1. That is the field report verbatim: "button
|
|
||||||
// flickers sometimes on press ... state does not change".
|
|
||||||
//
|
|
||||||
// The binary does NOT zero it per frame. Every writer of +0x398 in
|
|
||||||
// the export is: the DuckRequest handler (=1) and Mech::Reset (=0).
|
|
||||||
// The master perf FUN_004a9b5c -- which contains the address the old
|
|
||||||
// comment cited as "the DuckRequest consumer (@0x4aa011)" -- does not
|
|
||||||
// reference 0x398 anywhere, and mech.hpp's own note already said
|
|
||||||
// "duckState has NO code reader anywhere in the decomp ... whatever
|
|
||||||
// consumes it consumes it through DATABINDING". The databinding
|
|
||||||
// consumer is this gauge strip. The per-frame zeroing was ours.
|
|
||||||
//
|
|
||||||
// So: drive on DESIRED vs ACTUAL instead of on a latch. duckState is
|
|
||||||
// the desired posture (the handler now toggles it); the parked leg
|
|
||||||
// alarm is the actual one. Act only on a mismatch -- no re-fire, and
|
|
||||||
// nothing clears the attribute behind the gauge's back. A frame
|
|
||||||
// where mapPosture is not ready RETRIES next frame instead of
|
|
||||||
// silently dropping the request, which also retires the old
|
|
||||||
// "request consumed, posture=N" miss.
|
|
||||||
// THE POSTURE MACHINE (#142). duckState is the cockpit strip's frame:
|
|
||||||
// 0 = standing 1 = moving between 2 = crouched
|
|
||||||
// The handler writes 1 (the binary's exact behaviour) meaning "a move is
|
|
||||||
// pending", which doubles as the middle frame. Direction comes from the
|
|
||||||
// parked leg alarm, so no separate request cell is needed:
|
|
||||||
// parked -> the pending move is a RISE
|
|
||||||
// !parked -> the pending move is a SQUAT
|
|
||||||
// While the clip runs we hold 1; when it settles we write 0 or 2.
|
|
||||||
// Read the ALARM, not the cached legAnimationState member: the cache
|
|
||||||
// is only refreshed at the top of AdvanceLegAnimation, so in the
|
|
||||||
// frame right after SetLegAnimation it still reads the OLD state.
|
|
||||||
// With the cached read, "am I already moving?" answered no on the
|
|
||||||
// frame after issuing, the consumer re-issued, and the machine
|
|
||||||
// ping-ponged squat/rise -- 68 transitions from 2 presses, benched.
|
|
||||||
// SetLegAnimation writes the alarm synchronously, so the alarm is
|
|
||||||
// true the instant the clip is armed.
|
|
||||||
const int duckLegLvl = (int)legStateAlarm.GetLevel();
|
|
||||||
const int duckParked = (duckLegLvl == 1);
|
|
||||||
const int duckMoving = (duckLegLvl == 2 || duckLegLvl == 3); // 'sqd' / 'squ'
|
|
||||||
|
|
||||||
if (duckRequest != 0 && !duckMoving && squatCapable != 0)
|
|
||||||
{
|
{
|
||||||
duckRequest = 0; // one shot, whatever happens
|
if (mapPosture == 1)
|
||||||
if (!duckParked && mapPosture == 1)
|
|
||||||
{
|
{
|
||||||
SetLegAnimation(2); // 'sqd' -- squat down
|
SetLegAnimation(2); // 'sqd' -- squat down
|
||||||
ForceUpdate(8);
|
ForceUpdate(8);
|
||||||
@@ -4745,7 +4417,7 @@ void
|
|||||||
if (getenv("BT_DUCK_LOG") || getenv("BT_GAIT_LOG"))
|
if (getenv("BT_DUCK_LOG") || getenv("BT_GAIT_LOG"))
|
||||||
DEBUG_STREAM << "[duck] SQUAT (posture 1 -> leg clip 2)\n" << std::flush;
|
DEBUG_STREAM << "[duck] SQUAT (posture 1 -> leg clip 2)\n" << std::flush;
|
||||||
}
|
}
|
||||||
else if (duckParked && mapPosture == 2)
|
else if (mapPosture == 2)
|
||||||
{
|
{
|
||||||
SetLegAnimation(3); // 'squ' -- rise
|
SetLegAnimation(3); // 'squ' -- rise
|
||||||
ForceUpdate(8);
|
ForceUpdate(8);
|
||||||
@@ -4754,76 +4426,14 @@ void
|
|||||||
if (getenv("BT_DUCK_LOG") || getenv("BT_GAIT_LOG"))
|
if (getenv("BT_DUCK_LOG") || getenv("BT_GAIT_LOG"))
|
||||||
DEBUG_STREAM << "[duck] RISE (posture 2 -> leg clip 3)\n" << std::flush;
|
DEBUG_STREAM << "[duck] RISE (posture 2 -> leg clip 3)\n" << std::flush;
|
||||||
}
|
}
|
||||||
else
|
else if (getenv("BT_DUCK_LOG"))
|
||||||
{
|
DEBUG_STREAM << "[duck] request consumed, posture=" << mapPosture
|
||||||
// Gate refuses -- posture reads 0 for a MOVING mech, which is the
|
<< " (mode=" << MovementMode()
|
||||||
// authentic rule (Lynx: "when a mech STOPS, crouch button lowers
|
<< " legLvl=" << (int)legStateAlarm.GetLevel()
|
||||||
// its stance"; benched: crouch at a walk gives posture=0). Settle
|
<< " simLive=" << 1 // re-read below costs a bridge call; posture already folded it
|
||||||
// the strip back to the truth instead of holding the mid frame or
|
<< " myo=" << myomerEffectiveness
|
||||||
// queueing the request for the next time the pilot stops.
|
<< " squat=" << squatCapable << ")\n" << std::flush;
|
||||||
static int s_duckRefuse = 0;
|
duckState = 0; // consumed (@0x4aa0a9)
|
||||||
if ((s_duckRefuse++ % 30) == 0)
|
|
||||||
DEBUG_STREAM << "[duck] REFUSED (not stopped): posture="
|
|
||||||
<< mapPosture << " mode=" << MovementMode()
|
|
||||||
<< " myo=" << myomerEffectiveness << "\n" << std::flush;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SETTLE, unconditionally, re-reading the alarm AFTER any issue
|
|
||||||
// above. This is the part that was missing: duckState stayed 1
|
|
||||||
// forever, so once a clip completed the block above kept firing and
|
|
||||||
// flipped direction every frame -- 68 transitions from 2 presses.
|
|
||||||
// The squat/rise clips finish fast enough that the alarm is already
|
|
||||||
// back to 0/1 by the next visit, so "am I still moving?" has to be
|
|
||||||
// asked fresh, and the strip settled whenever the answer is no.
|
|
||||||
{
|
|
||||||
// SETTLE to the binary's VALUE RANGE: duckState is 0 or 1, never
|
|
||||||
// 2. Corrected 2026-08-07 after checking FUN_004a9b5c -- the
|
|
||||||
// mech4 master perf is fully indexed and exported (5645 bytes,
|
|
||||||
// no dark region inside) and never references +0x398. So the
|
|
||||||
// binary has NO code reader of duckState at all: the gauge
|
|
||||||
// attribute is the only consumer, and the only writers are the
|
|
||||||
// handler (=1) and Mech::Reset (=0). Frame 2 of bduck.pcc was
|
|
||||||
// unreachable in the original too.
|
|
||||||
//
|
|
||||||
// The strip is therefore a TWO-STATE indicator -- standing and
|
|
||||||
// crouched -- and the "crouch animation" is the MECH's 'sqd'
|
|
||||||
// clip, not the symbol stepping. An earlier revision here made
|
|
||||||
// duckState a 3-state posture (0/1/2); that was an invention on
|
|
||||||
// top of a stand-in and it put the crouched pose on the wrong
|
|
||||||
// frame.
|
|
||||||
// #142: report EVERY change of the value the cockpit strip is fed,
|
|
||||||
// so 'which frames actually got shown, and for how long' stops
|
|
||||||
// being guesswork. Ungated: one line per posture change.
|
|
||||||
static int s_lastDuck = -1;
|
|
||||||
// THE STRIP IS A BUTTON-STATE INDICATOR, not a pose animation.
|
|
||||||
// Decoded 2026-08-07 by RENDERING THE ASSET itself
|
|
||||||
// (content/GAUGE/BDUCK.PCC -- PCX, 108x102, three 36x102 frames;
|
|
||||||
// see scratchpad/night13/bduck_frames.png):
|
|
||||||
//
|
|
||||||
// frame 0 GREY mech standing, GREY down-arrow -- UNAVAILABLE
|
|
||||||
// frame 1 ORANGE standing, YELLOW down -- ready to crouch
|
|
||||||
// frame 2 ORANGE CROUCHED, YELLOW up -- press to rise
|
|
||||||
//
|
|
||||||
// There is NO mid-transition pose. The arrow tells the pilot
|
|
||||||
// what the next press will do, and the grey frame says the
|
|
||||||
// button is inert -- which is the visual half of the
|
|
||||||
// must-be-stopped rule (Lynx: "when a mech STOPS").
|
|
||||||
//
|
|
||||||
// Two earlier readings were wrong: a 2-state flag (which
|
|
||||||
// ignores frame 2's up-arrow entirely) and a stand/moving/
|
|
||||||
// crouched pose animation (there is no mid pose). Both were
|
|
||||||
// inferred from code and logs; only the ART settled it, and it
|
|
||||||
// also explains the field report -- a stopped mech that COULD
|
|
||||||
// crouch was drawing the grey "unavailable" frame.
|
|
||||||
const int lvlNow = (int)legStateAlarm.GetLevel();
|
|
||||||
if (lvlNow != 2 && lvlNow != 3) // hold through the clip
|
|
||||||
{
|
|
||||||
if (lvlNow == 1) duckState = 2; // crouched
|
|
||||||
else if (squatCapable != 0 && mapPosture == 1)
|
|
||||||
duckState = 1; // ready
|
|
||||||
else duckState = 0; // inert
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// (3b) AIRBORNE AUTO-RISE -- recovered 2026-08-06 by the #60
|
// (3b) AIRBORNE AUTO-RISE -- recovered 2026-08-06 by the #60
|
||||||
@@ -6481,21 +6091,7 @@ void
|
|||||||
// clamp(true - shown, +-dt*500) -- so the caret sweeps smoothly as
|
// clamp(true - shown, +-dt*500) -- so the caret sweeps smoothly as
|
||||||
// the boresight crosses near/far ground instead of teleporting.
|
// the boresight crosses near/far ground instead of teleporting.
|
||||||
// (Applies to the no-target 1200 default too.)
|
// (Applies to the no-target 1200 default too.)
|
||||||
// #147: sShownRange is DISPLAY state, but it is a function-level
|
|
||||||
// static -- one cell for the whole process, shared by every mech and
|
|
||||||
// carried across drops. Re-seed it whenever the viewpoint mech
|
|
||||||
// CHANGES (a new drop hands us a new entity) so a fresh drop starts
|
|
||||||
// at the binary's 1200 default instead of inheriting the last
|
|
||||||
// mission's slid value. A respawn REUSES the entity (Mech::Reset
|
|
||||||
// heals in place), so this deliberately does not fire there -- the
|
|
||||||
// binary does not reset the readout on respawn either.
|
|
||||||
static float sShownRange = 1200.0f;
|
static float sShownRange = 1200.0f;
|
||||||
static const void *sShownOwner = 0;
|
|
||||||
if (sShownOwner != (const void *)this)
|
|
||||||
{
|
|
||||||
sShownOwner = (const void *)this;
|
|
||||||
sShownRange = 1200.0f;
|
|
||||||
}
|
|
||||||
float trueRange = 1200.0f; // no target: the binary default
|
float trueRange = 1200.0f; // no target: the binary default
|
||||||
Entity *des = MECH_TARGET_ENTITY(this);
|
Entity *des = MECH_TARGET_ENTITY(this);
|
||||||
if (des != 0 && des != hotTarget)
|
if (des != 0 && des != hotTarget)
|
||||||
@@ -6552,26 +6148,6 @@ void
|
|||||||
|
|
||||||
// the 500 m/s slide toward trueRange (see the banner above)
|
// the 500 m/s slide toward trueRange (see the banner above)
|
||||||
{
|
{
|
||||||
// #147 NaN TRAP. NaN is ABSORBING here and the clamps below
|
|
||||||
// cannot catch it: `step > maxStep` and `step < -maxStep` are
|
|
||||||
// BOTH false for NaN, so a single poisoned frame makes
|
|
||||||
// sShownRange NaN and it stays NaN for the life of the process
|
|
||||||
// (the static is never re-seeded except on a mech change).
|
|
||||||
// Downstream, BTReticleRenderable::Draw clamps the same way, so
|
|
||||||
// the NaN reaches AddPoint/ConcatMatrix and the caret + its bar
|
|
||||||
// render as degenerate geometry -- i.e. they VANISH while every
|
|
||||||
// static reticle element (the tick marks) still draws. That is
|
|
||||||
// exactly the reported "no range finder on this drop: ticks
|
|
||||||
// there, moving caret gone". Re-seed instead of propagating.
|
|
||||||
if (!(trueRange == trueRange) || !(sShownRange == sShownRange))
|
|
||||||
{
|
|
||||||
if (getenv("BT_RANGE_LOG"))
|
|
||||||
DEBUG_STREAM << "[range] NaN TRAPPED (true=" << trueRange
|
|
||||||
<< " shown=" << sShownRange << ") -- re-seeded to 1200\n"
|
|
||||||
<< std::flush;
|
|
||||||
trueRange = 1200.0f;
|
|
||||||
sShownRange = 1200.0f;
|
|
||||||
}
|
|
||||||
float maxStep = (float)dt * 500.0f;
|
float maxStep = (float)dt * 500.0f;
|
||||||
if (maxStep < 0.0f) maxStep = -maxStep;
|
if (maxStep < 0.0f) maxStep = -maxStep;
|
||||||
float step = trueRange - sShownRange;
|
float step = trueRange - sShownRange;
|
||||||
@@ -6579,22 +6155,6 @@ void
|
|||||||
if (step < -maxStep) step = -maxStep;
|
if (step < -maxStep) step = -maxStep;
|
||||||
sShownRange += step;
|
sShownRange += step;
|
||||||
BTSetHudTargetRange((Scalar)sShownRange);
|
BTSetHudTargetRange((Scalar)sShownRange);
|
||||||
// The caret's ACTUAL input had NO diagnostic anywhere: BT_RANGE_LOG
|
|
||||||
// instruments the PICK (#4), and [target]'s `range=` is a separate
|
|
||||||
// locally-recomputed Sqrt in the weapon-range check -- so a field
|
|
||||||
// log could neither confirm nor refute a dead caret. Fixed.
|
|
||||||
if (getenv("BT_RANGE_LOG"))
|
|
||||||
{
|
|
||||||
static float sRlog = 0.0f;
|
|
||||||
sRlog += (float)dt;
|
|
||||||
if (sRlog >= 1.0f)
|
|
||||||
{
|
|
||||||
sRlog = 0.0f;
|
|
||||||
DEBUG_STREAM << "[range] caret input shown=" << sShownRange
|
|
||||||
<< " true=" << trueRange << " lock=" << gBTHudLockState
|
|
||||||
<< "\n" << std::flush;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// BT_RANGE_LOG (Gitea #4 VERDICT instrumentation -- uncommitted diag):
|
// BT_RANGE_LOG (Gitea #4 VERDICT instrumentation -- uncommitted diag):
|
||||||
@@ -8021,33 +7581,7 @@ void
|
|||||||
continue;
|
continue;
|
||||||
if (i != 0) // slot 0 = the mapper (task #7)
|
if (i != 0) // slot 0 = the mapper (task #7)
|
||||||
++subsystemsPresent;
|
++subsystemsPresent;
|
||||||
// #148 -- THE INSTANCE BRANCH. Entity::Perform (ENTITY.cpp:733-793,
|
if (!subsystem->IsNonReplicantExecutable())
|
||||||
// real engine source [T0]) picks the predicate by instance:
|
|
||||||
// if (GetInstance() != ReplicantInstance) IsNonReplicantExecutable()
|
|
||||||
// else IsReplicantExecutable()
|
|
||||||
// and they differ exactly on the replicant case (SIMULATE.h:195-206):
|
|
||||||
// NonReplicant: (flags & DontExecuteFlag) == 0
|
|
||||||
// Replicant : (flags & DontExecuteFlag) == 0
|
|
||||||
// || lastUpdate >= lastPerformance
|
|
||||||
// `ExecuteOnUpdate()` SETS DontExecuteFlag -- it means "do not tick me
|
|
||||||
// every frame, tick me when an UPDATE ARRIVES". This loop used the
|
|
||||||
// NonReplicant predicate for every mech, so on a REPLICANT any
|
|
||||||
// ExecuteOnUpdate subsystem never ran at all, no matter how many
|
|
||||||
// records arrived for it.
|
|
||||||
//
|
|
||||||
// Measured (scratchpad/night13/missileframe2.sh): the peer's copy
|
|
||||||
// TORSO received its first record at log line 205 but its Performance
|
|
||||||
// did not run until line 1014 -- ~800 lines of arriving twist data
|
|
||||||
// integrated by nobody, so the peer's torso sat at 0 and its missiles
|
|
||||||
// launched along the body facing (the tail of #141). The records
|
|
||||||
// themselves were fine: they are sent on RATE CHANGE (the sweep's
|
|
||||||
// direction flips -- atUpd +/-2.39 with rate flipping sign), and the
|
|
||||||
// peer dead-reckons `atUpd + rate * elapsed` between them.
|
|
||||||
const Logical execOK =
|
|
||||||
(GetInstance() != Entity::ReplicantInstance)
|
|
||||||
? subsystem->IsNonReplicantExecutable()
|
|
||||||
: subsystem->IsReplicantExecutable();
|
|
||||||
if (!execOK)
|
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// The controls-mapping subsystem (roster slot 0 via Mech::SetMapping
|
// The controls-mapping subsystem (roster slot 0 via Mech::SetMapping
|
||||||
@@ -9027,22 +8561,7 @@ void
|
|||||||
}
|
}
|
||||||
if (s_portCache[energyOrdinal] != 0)
|
if (s_portCache[energyOrdinal] != 0)
|
||||||
{
|
{
|
||||||
// #141 sweep: engine accessor (== the binary's FUN_00424da8).
|
AffineMatrix mw;
|
||||||
// This is the BEAM muzzle -- the same stale-cache exposure the
|
|
||||||
// missile launch had, so a peer's beam would also originate
|
|
||||||
// from the bind-pose gun port instead of the twisted torso.
|
|
||||||
// #149 A/B (BT_BEAM_SEGFRESH=0): revert THIS site to the
|
|
||||||
// pre-sweep plain compose, to measure whether the per-beam
|
|
||||||
// per-frame dirty-pass is the 857 draw-stall regression.
|
|
||||||
// This site runs inside the DRAW path per emitter per frame;
|
|
||||||
// the other swept sites are per-salvo/per-hit and cannot be
|
|
||||||
// a per-frame cost. Default = fresh (the swept behaviour).
|
|
||||||
static const int sBeamFresh =
|
|
||||||
!(getenv("BT_BEAM_SEGFRESH") && *getenv("BT_BEAM_SEGFRESH") == '0');
|
|
||||||
LinearMatrix mw;
|
|
||||||
if (sBeamFresh)
|
|
||||||
GetSegmentToWorld(*s_portCache[energyOrdinal], &mw);
|
|
||||||
else
|
|
||||||
mw.Multiply(s_portCache[energyOrdinal]->GetSegmentToEntity(),
|
mw.Multiply(s_portCache[energyOrdinal]->GetSegmentToEntity(),
|
||||||
localToWorld);
|
localToWorld);
|
||||||
mz = mw; // Point3D = matrix translation
|
mz = mw; // Point3D = matrix translation
|
||||||
|
|||||||
@@ -1255,50 +1255,26 @@ void
|
|||||||
+ fabsf((float)owner->bodyCycleSpeed);
|
+ fabsf((float)owner->bodyCycleSpeed);
|
||||||
const int movingNoLegs = (step > 0.08f && step < 5.0f // 5+: teleport/warp
|
const int movingNoLegs = (step > 0.08f && step < 5.0f // 5+: teleport/warp
|
||||||
&& cyc < 0.05f) ? 1 : 0;
|
&& cyc < 0.05f) ? 1 : 0;
|
||||||
// #52 A/B probe (BT_BODY_SM_LOG): the POSITIVE half of the Standing-lock
|
|
||||||
// evidence. The [skate] line only speaks once a lock has ALSO produced
|
|
||||||
// 90 sustained moving frames; this says every second what a moving peer's
|
|
||||||
// body channel is actually doing. Locked: bstate=0, bodyCyc=0. Healthy:
|
|
||||||
// bstate in the walk/run family with a live cycle.
|
|
||||||
if (step > 0.08f && step < 5.0f && getenv("BT_BODY_SM_LOG"))
|
|
||||||
{
|
|
||||||
static float s_pg = 0.0f; s_pg += 1.0f / 30.0f;
|
|
||||||
if (s_pg >= 1.0f) { s_pg = 0.0f;
|
|
||||||
DEBUG_STREAM << "[peergait] replicant " << owner->GetEntityID()
|
|
||||||
<< " step=" << step
|
|
||||||
<< " bstate=" << (int)owner->bodyAnimationState
|
|
||||||
<< " bodyCyc=" << (float)owner->bodyCycleSpeed
|
|
||||||
<< " bts=" << (float)owner->bodyTargetSpeed
|
|
||||||
<< (movingNoLegs ? " <-- IDLE CHANNELS" : "")
|
|
||||||
<< "\n" << std::flush; }
|
|
||||||
}
|
|
||||||
if (movingNoLegs)
|
if (movingNoLegs)
|
||||||
{
|
{
|
||||||
if (++skateFrames > 90 && !skateLogged)
|
if (++skateFrames > 90 && !skateLogged)
|
||||||
{
|
{
|
||||||
skateLogged = 1;
|
skateLogged = 1;
|
||||||
// bstate = the peer's BODY animation state (@0x728), the channel
|
|
||||||
// that poses a replicant (s_peerLegCh=0). Added 2026-08-07 after
|
|
||||||
// night 13: the field lines proved "moving with both channels
|
|
||||||
// idle" but not WHICH state it was idling in, and the answer
|
|
||||||
// (0 = Standing, pinned) is the whole diagnosis -- see #52.
|
|
||||||
DEBUG_STREAM << "[skate] replicant " << owner->GetEntityID()
|
DEBUG_STREAM << "[skate] replicant " << owner->GetEntityID()
|
||||||
<< " SKATING: " << skateFrames << " frames moving ("
|
<< " SKATING: " << skateFrames << " frames moving ("
|
||||||
<< step << " u/frame) with legCyc="
|
<< step << " u/frame) with legCyc="
|
||||||
<< (float)owner->legCycleSpeed
|
<< (float)owner->legCycleSpeed
|
||||||
<< " bodyCyc=" << (float)owner->bodyCycleSpeed
|
<< " bodyCyc=" << (float)owner->bodyCycleSpeed
|
||||||
<< " bodyTargetSpeed=" << (float)owner->bodyTargetSpeed
|
<< " bodyTargetSpeed=" << (float)owner->bodyTargetSpeed
|
||||||
<< " bstate=" << (int)owner->bodyAnimationState
|
|
||||||
<< " destroyed=" << (int)owner->IsMechDestroyed()
|
<< " destroyed=" << (int)owner->IsMechDestroyed()
|
||||||
<< " mode=" << (int)owner->MovementMode()
|
<< " mode=" << (int)owner->MovementMode()
|
||||||
<< " at (" << px << "," << pz << ")\n" << std::flush;
|
<< " at (" << px << "," << pz << ")\n" << std::flush;
|
||||||
if (BTMatchLogActive())
|
if (BTMatchLogActive())
|
||||||
BTMatchLog("SKATE", "mech=%d:%d frames=%d step=%.3f cyc=%.3f "
|
BTMatchLog("SKATE", "mech=%d:%d frames=%d step=%.3f cyc=%.3f "
|
||||||
"cmdSpd=%.2f bstate=%d destroyed=%d mode=%d x=%.1f z=%.1f",
|
"cmdSpd=%.2f destroyed=%d mode=%d x=%.1f z=%.1f",
|
||||||
BTMatchHostOf(owner->GetEntityID()), (int)owner->GetEntityID(),
|
BTMatchHostOf(owner->GetEntityID()), (int)owner->GetEntityID(),
|
||||||
skateFrames, step, cyc,
|
skateFrames, step, cyc,
|
||||||
(float)owner->bodyTargetSpeed,
|
(float)owner->bodyTargetSpeed,
|
||||||
(int)owner->bodyAnimationState,
|
|
||||||
(int)owner->IsMechDestroyed(), (int)owner->MovementMode(),
|
(int)owner->IsMechDestroyed(), (int)owner->MovementMode(),
|
||||||
px, pz);
|
px, pz);
|
||||||
}
|
}
|
||||||
@@ -1358,31 +1334,6 @@ void
|
|||||||
const bool nowDead = (mode == 2 || mode == 9);
|
const bool nowDead = (mode == 2 || mode == 9);
|
||||||
const bool prevDead = (oldMode == 2 || oldMode == 9);
|
const bool prevDead = (oldMode == 2 || oldMode == 9);
|
||||||
prevMode = mode;
|
prevMode = mode;
|
||||||
// #108 GHOST CENSUS, the ENTER edge (ungated, 2026-08-07). The
|
|
||||||
// un-wreck receipt below has had no partner, so counting ghosts in a
|
|
||||||
// field log meant pairing it against
|
|
||||||
// "[BTrender] wreck: 'thrdbr.bgf' missing -> gendbr.bgf fallback" --
|
|
||||||
// which is a MISSING-ASSET WARNING, not a death: it only prints for
|
|
||||||
// chassis whose wreck model is absent. Night 13's census therefore
|
|
||||||
// found ONE ghost while testers reported many, and there was no way to
|
|
||||||
// tell a real count from a chassis accident. This line is emitted for
|
|
||||||
// EVERY replicant that enters the wreck state, so a log's ghost count
|
|
||||||
// is exactly (wreck-enters minus un-wrecks) per entity.
|
|
||||||
if (!prevDead && nowDead
|
|
||||||
&& owner->GetInstance() == Entity::ReplicantInstance)
|
|
||||||
{
|
|
||||||
DEBUG_STREAM << "[wreck] replicant " << owner->GetEntityID()
|
|
||||||
<< " entered wreck state (mode "
|
|
||||||
<< oldMode << "->" << mode << ") at ("
|
|
||||||
<< owner->localOrigin.linearPosition.x << ","
|
|
||||||
<< owner->localOrigin.linearPosition.z << ")\n" << std::flush;
|
|
||||||
if (BTMatchLogActive())
|
|
||||||
BTMatchLog("WRECK", "mech=%d:%d mode=%d->%d x=%.1f z=%.1f",
|
|
||||||
BTMatchHostOf(owner->GetEntityID()), (int)owner->GetEntityID(),
|
|
||||||
oldMode, mode,
|
|
||||||
(float)owner->localOrigin.linearPosition.x,
|
|
||||||
(float)owner->localOrigin.linearPosition.z);
|
|
||||||
}
|
|
||||||
if (prevDead && !nowDead
|
if (prevDead && !nowDead
|
||||||
&& owner->GetInstance() == Entity::ReplicantInstance)
|
&& owner->GetInstance() == Entity::ReplicantInstance)
|
||||||
{
|
{
|
||||||
|
|||||||
+27
-128
@@ -508,69 +508,25 @@ void
|
|||||||
}
|
}
|
||||||
NotifyOfControlModeChange(controlMode); // vtable+0x48
|
NotifyOfControlModeChange(controlMode); // vtable+0x48
|
||||||
|
|
||||||
// TYPED torso reconfiguration. The raw block this replaces wrote the
|
// TYPED torso reconfiguration (2026-07-13): the raw block this
|
||||||
// BINARY's offsets straight onto OUR compiled Torso (the databinding
|
// replaces wrote the BINARY's offsets (torso+0x1f0/0x274/0x220...)
|
||||||
// trap); the typed rewrite that followed then got the SEMANTICS wrong in
|
// straight onto OUR compiled Torso -- the databinding trap: garbage
|
||||||
// three ways. Corrected 2026-08-08 against @004afbe0, which is a
|
// writes into whatever members live there in this build. The
|
||||||
// complete spec:
|
// observable semantics via named members: Basic clears the analog
|
||||||
//
|
// axes and recenters (the sim's centerCommand -> Recenter); the
|
||||||
// iVar1 = mech+0x438 (TORSO) iVar2 = mech+0x5b4 (HUD)
|
// assisted modes just free the torso (the sim clamps to the authored
|
||||||
// if (mode == 0) { // BASIC
|
// limits on its own).
|
||||||
// *(iVar1 + 0x1f0) = 0; // analogTwistAxis
|
|
||||||
// *(iVar1 + 0x274) = 1; // recenterActive
|
|
||||||
// *(iVar1 + 0x220) = *(iVar1 + 0x228); // vertLimitTop
|
|
||||||
// *(iVar1 + 0x224) = *(iVar1 + 0x22c); // vertLimitBottom
|
|
||||||
// *(iVar2 + 0x2a0) = 1; // HUD flickerActive
|
|
||||||
// } else if (mode - 1U < 2) { // STANDARD/VETERAN
|
|
||||||
// *(iVar1 + 0x220) = *(iVar1 + 0x230);
|
|
||||||
// *(iVar1 + 0x224) = *(iVar1 + 0x234);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// (1) THE BUG Sauron hit. Basic set `centerCommand` (@0x208) via
|
|
||||||
// CommandRecenter(). That is the HELD-BUTTON cell: TorsoSimulation
|
|
||||||
// re-arms recenterActive from it EVERY frame it is non-zero, and only
|
|
||||||
// the input path clears it -- and a MODE SWITCH has no button release
|
|
||||||
// to follow. So one visit to Basic pinned it at 1 forever, the torso
|
|
||||||
// re-centred every frame, and the digital twist commands (processed
|
|
||||||
// BEFORE the centerCommand block) were overridden as fast as they were
|
|
||||||
// applied. Cycling Standard -> Veteran -> (wraps through BASIC) ->
|
|
||||||
// Standard is enough to trigger it, which is exactly the reported
|
|
||||||
// "toggled to advanced and back, lost torso control". The binary sets
|
|
||||||
// recenterActive (@0x274) directly: a ONE-SHOT that self-clears on
|
|
||||||
// settle (`recenterActive = Recenter(dt)`) and is cancelled by any
|
|
||||||
// twist input.
|
|
||||||
// (2) The ELEVATION LIMIT SWAP was missing entirely. Two authored pairs
|
|
||||||
// exist -- BASIC @0x228/@0x22C (full top, HALF bottom) vs
|
|
||||||
// STANDARD/VETERAN @0x230/@0x234 (the full pair) -- and all four were
|
|
||||||
// ctor-written and never read by anything. So Basic never restricted
|
|
||||||
// downward travel and the assisted modes never restored it.
|
|
||||||
// (3) Basic also raises the HUD's flickerActive (@0x2A0) so the horizon
|
|
||||||
// re-settles with the torso. Not ported.
|
|
||||||
// Also: the binary zeroes ONLY analogTwistAxis (@0x1F0). The extra
|
|
||||||
// SetAnalogElevationAxis(0) was invented; removed.
|
|
||||||
Mech *mech = GetMech();
|
Mech *mech = GetMech();
|
||||||
Torso *torso = (mech != 0) ? (Torso *)mech->GetTorsoSubsystem() : 0;
|
Torso *torso = (mech != 0) ? (Torso *)mech->GetTorsoSubsystem() : 0;
|
||||||
if (torso != 0)
|
if (torso != 0)
|
||||||
{
|
{
|
||||||
if (controlMode == BasicMode)
|
if (controlMode == BasicMode)
|
||||||
{
|
{
|
||||||
torso->SetAnalogTwistAxis(0.0f); // @0x1F0
|
torso->SetAnalogTwistAxis(0.0f);
|
||||||
// BT_LEGACY_MODE_RECENTER=1 restores the defective pre-2026-08-08
|
torso->SetAnalogElevationAxis(0.0f);
|
||||||
// behaviour (the sticky centerCommand) for A/B measurement.
|
torso->CommandRecenter();
|
||||||
static const int s_legacyRecenter =
|
|
||||||
getenv("BT_LEGACY_MODE_RECENTER") ? 1 : 0;
|
|
||||||
if (s_legacyRecenter)
|
|
||||||
torso->CommandRecenter(); // @0x208 STICKY -- the bug
|
|
||||||
else
|
|
||||||
torso->BeginRecenterOnce(); // @0x274 (NOT centerCommand)
|
|
||||||
torso->ApplyBasicElevationLimits(); // @0x220/@0x224 <- @0x228/@0x22C
|
|
||||||
extern void BTSetHudFlickerActive(Subsystem *hud);
|
|
||||||
BTSetHudFlickerActive(mech->GetHudSubsystem()); // HUD @0x2A0 = 1
|
|
||||||
}
|
|
||||||
else // StandardMode / VeteranMode -- `mode - 1U < 2` in the binary
|
|
||||||
{
|
|
||||||
torso->ApplyAssistedElevationLimits(); // @0x220/@0x224 <- @0x230/@0x234
|
|
||||||
}
|
}
|
||||||
|
// Standard/Veteran: nothing to force -- the sim's limits govern.
|
||||||
}
|
}
|
||||||
DEBUG_STREAM << "[mode] control mode -> " << (int)controlMode
|
DEBUG_STREAM << "[mode] control mode -> " << (int)controlMode
|
||||||
<< " (0=Basic 1=Standard 2=Veteran)" << std::endl;
|
<< " (0=Basic 1=Standard 2=Veteran)" << std::endl;
|
||||||
@@ -704,58 +660,6 @@ void
|
|||||||
// after the push, immediately before interpretation -- making the keyboard
|
// after the push, immediately before interpretation -- making the keyboard
|
||||||
// authoritative on the dev box. Interpretation below stays 100% authentic.
|
// authoritative on the dev box. Interpretation below stays 100% authentic.
|
||||||
//
|
//
|
||||||
// BENCH (BT_MODECYCLE_EVERY=<n>): cycle the control mode every n
|
|
||||||
// InterpretControls calls, driving the SAME body the 'M' key and the pod
|
|
||||||
// console button (key 0x13d -- not a RIO button, so BT_BTNTEST cannot press
|
|
||||||
// it) drive. Dev-only; default off.
|
|
||||||
//
|
|
||||||
// ⚠ DELIBERATELY OUTSIDE the key-bridge block below. The ONLY caller of
|
|
||||||
// ClearRecenterCommand() lives inside that block, so forcing BT_KEY_BRIDGE=1
|
|
||||||
// to make this hook run would ALSO switch on the one thing that clears
|
|
||||||
// centerCommand -- masking the very bug under test. That is exactly how the
|
|
||||||
// first run of modecycle.sh came back clean. Keeping the hook out here lets
|
|
||||||
// the bench reproduce the RIO-present (glass/PadRIO) configuration, where the
|
|
||||||
// bridge is OFF and nothing clears the cell.
|
|
||||||
{
|
|
||||||
static const char *s_mcEvery = getenv("BT_MODECYCLE_EVERY");
|
|
||||||
if (s_mcEvery != 0)
|
|
||||||
{
|
|
||||||
static int s_mcN = 0;
|
|
||||||
int period = atoi(s_mcEvery);
|
|
||||||
if (period < 1) period = 300;
|
|
||||||
if (++s_mcN % period == 0)
|
|
||||||
CycleControlModeNow();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// (#152) TORSO-CENTER -- the ONE writer of the torso's centerCommand
|
|
||||||
// (@0x208, HELD-button semantics: writer asserts while held, clears on
|
|
||||||
// release; TorsoSimulation re-arms recenterActive from it each frame).
|
|
||||||
// Sources OR'd here, deliberately OUTSIDE the key-bridge gate:
|
|
||||||
// * torsoCenter (@0x154) -- this mapper's databound "TorsoCenter" cell,
|
|
||||||
// the streamed pod-button route (button 0x42);
|
|
||||||
// * gBTTorsoRecenter -- the desktop 'X' one-frame pulse (mech4 key poll).
|
|
||||||
// The old writer lived INSIDE the key-bridge block, so on any rig with a
|
|
||||||
// RIO/PadRIO present (glass + the pod -- the bridge is off there) NO path
|
|
||||||
// could reach centerCommand: with the stuck-cell phantom auto-recentre
|
|
||||||
// fixed, those players had no way to recentre the torso in Std/Vet at all
|
|
||||||
// (Oracle's #152 report). Single-writer here also means the two sources
|
|
||||||
// can never stomp each other's clear.
|
|
||||||
{
|
|
||||||
Mech *rcMech = GetMech();
|
|
||||||
Torso *rcTorso = (rcMech != 0) ? (Torso *)rcMech->GetTorsoSubsystem() : 0;
|
|
||||||
if (rcTorso != 0)
|
|
||||||
{
|
|
||||||
int hold = (torsoCenter != 0);
|
|
||||||
extern int gBTTorsoRecenter;
|
|
||||||
extern int gBTTorsoCenterHeld; // pod button 0x42 (L4PADRIO chokepoint)
|
|
||||||
if (gBTTorsoCenterHeld) hold = 1;
|
|
||||||
if (gBTTorsoRecenter) { gBTTorsoRecenter = 0; hold = 1; }
|
|
||||||
if (hold)
|
|
||||||
rcTorso->CommandRecenter();
|
|
||||||
else
|
|
||||||
rcTorso->ClearRecenterCommand();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
// STAND-DOWN (glass-cockpit step 2c): BT_KEY_BRIDGE unset = AUTO --
|
// STAND-DOWN (glass-cockpit step 2c): BT_KEY_BRIDGE unset = AUTO --
|
||||||
// the bridge runs only when NO live cockpit device (serial RIO /
|
// the bridge runs only when NO live cockpit device (serial RIO /
|
||||||
@@ -967,12 +871,21 @@ void
|
|||||||
// centerCommand is a pod BUTTON state, so the writer clears
|
// centerCommand is a pod BUTTON state, so the writer clears
|
||||||
// it while unpressed (Basic's own path re-asserts every
|
// it while unpressed (Basic's own path re-asserts every
|
||||||
// frame; this branch owns it in Standard/Veteran).
|
// frame; this branch owns it in Standard/Veteran).
|
||||||
// (#152) the recenter writer moved OUT of this key-bridge
|
{
|
||||||
// block to the unified consumer below -- inside here it was
|
Torso *rcTorso = (Torso *)mech->GetTorsoSubsystem();
|
||||||
// DEAD on every RIO/glass rig (bridge off), which left those
|
if (rcTorso != 0)
|
||||||
// players with no torso-centre control at all once the
|
{
|
||||||
// stuck-cell phantom auto-recentre was fixed. Bench: two
|
if (gBTTorsoRecenter)
|
||||||
// scripted 0x42 holds on the RIO path, ctrCmd=0 throughout.
|
{
|
||||||
|
gBTTorsoRecenter = 0;
|
||||||
|
rcTorso->CommandRecenter();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rcTorso->ClearRecenterCommand();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// (stickPosition.y no longer zeroed here -- the bridge above
|
// (stickPosition.y no longer zeroed here -- the bridge above
|
||||||
@@ -1110,20 +1023,6 @@ void
|
|||||||
// reads the same live factor (dead/overheated myomers cannot squat
|
// reads the same live factor (dead/overheated myomers cannot squat
|
||||||
// or rise; the posture selector tests |factor| <= 1e-4).
|
// or rise; the posture selector tests |factor| <= 1e-4).
|
||||||
mech->myomerEffectiveness = drive;
|
mech->myomerEffectiveness = drive;
|
||||||
// #137 POST-RESET TRACE. Mech::Reset arms gBTMyoTrace; sample here,
|
|
||||||
// where the mover's actual multiplier is formed, for a few seconds
|
|
||||||
// after a respawn. This is the window nothing was watching -- the
|
|
||||||
// at-reset sample always looks clean.
|
|
||||||
{
|
|
||||||
extern int gBTMyoTrace;
|
|
||||||
extern void BTReportMyomerFreeze(void *mech_v, const char *when);
|
|
||||||
if (gBTMyoTrace > 0)
|
|
||||||
{
|
|
||||||
--gBTMyoTrace;
|
|
||||||
if ((gBTMyoTrace % 30) == 0)
|
|
||||||
BTReportMyomerFreeze((void *)mech, "post-reset");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (fabsf(drive) <= 1.0e-4f) // @0x4a9d89 vs _DAT_004ab16c
|
if (fabsf(drive) <= 1.0e-4f) // @0x4a9d89 vs _DAT_004ab16c
|
||||||
turnDemand = 0.0f; // @0x4a9d9e: mapper+0x12C -- the FREEZE
|
turnDemand = 0.0f; // @0x4a9d9e: mapper+0x12C -- the FREEZE
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -809,31 +809,6 @@ void Myomers::MyomersDriveHeat(Scalar time_slice)
|
|||||||
Scalar termAccel = velComplement * velMag * accMag * mass * time_slice;
|
Scalar termAccel = velComplement * velMag * accMag * mass * time_slice;
|
||||||
Scalar gain = ratio * ratio * damageGain;
|
Scalar gain = ratio * ratio * damageGain;
|
||||||
|
|
||||||
// #137 DEPOSIT TRAP (BT_HEAT_LOG): conduction into the myomers is measured
|
|
||||||
// ZERO post-respawn (the [heatflow] trap), weapons deposit into themselves,
|
|
||||||
// so THIS add is the only writer left that can carry the observed one-shot
|
|
||||||
// slug (9e7..2.3e9, varying per respawn). The [myoheat] receipt is
|
|
||||||
// time-sampled and would miss a 1-2 frame spike; this prints EVERY add
|
|
||||||
// over 1e6 with the full operand set, unconditionally.
|
|
||||||
{
|
|
||||||
Scalar deposit = gain * (termClimb + termKinetic + termAccel);
|
|
||||||
if (getenv("BT_HEAT_LOG") != 0 && (deposit > 1.0e6f || deposit < -1.0e6f))
|
|
||||||
{
|
|
||||||
// WHOSE mech (master vs the peer's replicant shares this log!) and
|
|
||||||
// the acceleration COMPONENTS (pure-y = gravity accumulation on the
|
|
||||||
// wreck; planar = teleport/warp-derived).
|
|
||||||
Mech *om = (Mech *)owner;
|
|
||||||
const Vector3D &av = om->localAcceleration.linearMotion;
|
|
||||||
DEBUG_STREAM << "[myodep] mech=" << om->GetEntityID()
|
|
||||||
<< (om->GetInstance() == Entity::ReplicantInstance ? " REPL" : " mstr")
|
|
||||||
<< " deposit=" << deposit
|
|
||||||
<< " v=" << velMag << " a=" << accMag
|
|
||||||
<< " aXYZ=(" << av.x << "," << av.y << "," << av.z << ")"
|
|
||||||
<< " vy=" << vy << " dt=" << time_slice << " gain=" << gain
|
|
||||||
<< " climb=" << termClimb << " kinetic=" << termKinetic
|
|
||||||
<< " accel=" << termAccel << "\n" << std::flush;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pendingHeat /* @0x1C8 */ += gain * (termClimb + termKinetic + termAccel);
|
pendingHeat /* @0x1C8 */ += gain * (termClimb + termKinetic + termAccel);
|
||||||
|
|
||||||
if (getenv("BT_MYO_LOG"))
|
if (getenv("BT_MYO_LOG"))
|
||||||
|
|||||||
@@ -618,30 +618,6 @@ void
|
|||||||
if (lsw != 0 && s_lockSweep <= 0.0f) s_lockSweep = 0.12f;
|
if (lsw != 0 && s_lockSweep <= 0.0f) s_lockSweep = 0.12f;
|
||||||
if (s_lockSweep > 1.0f) s_lockSweep = 1.0f;
|
if (s_lockSweep > 1.0f) s_lockSweep = 1.0f;
|
||||||
}
|
}
|
||||||
// BENCH (BT_TWIST_PULSE=<n>): deflect the analog twist axis for n ticks,
|
|
||||||
// then RELEASE it for n ticks, repeating. BT_LOCK_SWEEP never releases,
|
|
||||||
// so it cannot show the reported symptom: with a stuck centerCommand the
|
|
||||||
// torso holds while you are actively pushing (the analog arm clears
|
|
||||||
// recenterActive) and snaps back the moment you let go (centerCommand
|
|
||||||
// re-arms it) -- "the torso centering FOUGHT my control". Measure the
|
|
||||||
// RELEASE windows: currentTwist should HOLD, not decay toward 0.
|
|
||||||
{
|
|
||||||
static const char *s_tp = getenv("BT_TWIST_PULSE");
|
|
||||||
if (s_tp != 0)
|
|
||||||
{
|
|
||||||
static int s_tpN = 0;
|
|
||||||
int period = atoi(s_tp);
|
|
||||||
if (period < 1) period = 120;
|
|
||||||
const int phase = (s_tpN++ / period) % 2;
|
|
||||||
analogTwistAxis = phase ? 0.0f : 0.6f;
|
|
||||||
if ((s_tpN % 30) == 0)
|
|
||||||
DEBUG_STREAM << "[twistpulse] phase=" << (phase ? "RELEASE" : "deflect")
|
|
||||||
<< " axis=" << analogTwistAxis
|
|
||||||
<< " twist=" << currentTwist
|
|
||||||
<< " ctrCmd=" << centerCommand
|
|
||||||
<< " recen=" << recenterActive << "\n" << std::flush;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (s_lockSweep > 0.0f)
|
if (s_lockSweep > 0.0f)
|
||||||
{
|
{
|
||||||
effectiveTwistRate = baseTwistRate;
|
effectiveTwistRate = baseTwistRate;
|
||||||
@@ -670,13 +646,6 @@ void
|
|||||||
<< " limits=(" << horizontalLimitRight << ".." << horizontalLimitLeft << ")"
|
<< " limits=(" << horizontalLimitRight << ".." << horizontalLimitLeft << ")"
|
||||||
<< " axis=" << analogTwistAxis
|
<< " axis=" << analogTwistAxis
|
||||||
<< " twist=" << currentTwist
|
<< " twist=" << currentTwist
|
||||||
// control-mode recenter state. centerCommand (@0x208) is the
|
|
||||||
// HELD-button cell -- if it reads 1 with no button down, the
|
|
||||||
// torso re-arms recenterActive every frame and digital twist is
|
|
||||||
// dead (Sauron's "lost torso control" after cycling modes).
|
|
||||||
<< " ctrCmd=" << centerCommand
|
|
||||||
<< " recen=" << recenterActive
|
|
||||||
<< " vLim=(" << verticalLimitBottom << ".." << verticalLimitTop << ")"
|
|
||||||
<< " wIdx=" << watchedSubsystem
|
<< " wIdx=" << watchedSubsystem
|
||||||
<< " w=" << (void*)w
|
<< " w=" << (void*)w
|
||||||
<< " wElec=" << (w ? w->electricalStateAlarm.GetLevel() : -1)
|
<< " wElec=" << (w ? w->electricalStateAlarm.GetLevel() : -1)
|
||||||
@@ -854,13 +823,6 @@ void
|
|||||||
<< " vel=" << twistVelocity
|
<< " vel=" << twistVelocity
|
||||||
<< " lastUpd=" << lastUpdateTime
|
<< " lastUpd=" << lastUpdateTime
|
||||||
<< " now=" << GetCurrentTime()
|
<< " now=" << GetCurrentTime()
|
||||||
// #148: ComputeTargetTwist ends in Min(limitLeft)/Max(limitRight).
|
|
||||||
// If the COPY's limits never loaded they are 0/0, which pins
|
|
||||||
// targetTwist to EXACTLY 0 no matter what the record carried --
|
|
||||||
// which is what a peer stuck at zero twist would look like.
|
|
||||||
<< " limL=" << horizontalLimitLeft
|
|
||||||
<< " limR=" << horizontalLimitRight
|
|
||||||
<< " enab=" << (int)horizontalEnabled
|
|
||||||
<< " copy=" << (int)isDamagedCopy << std::endl;
|
<< " copy=" << (int)isDamagedCopy << std::endl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -935,21 +897,14 @@ void
|
|||||||
|
|
||||||
// bring-up verification (env BT_TORSO_LOG; default OFF): show the first few
|
// bring-up verification (env BT_TORSO_LOG; default OFF): show the first few
|
||||||
// joint writes so the per-frame path can be confirmed in a headless run.
|
// joint writes so the per-frame path can be confirmed in a headless run.
|
||||||
// ⚠ SAMPLING TRAP (fixed 2026-08-08, #141): this used to sample ONE shared
|
|
||||||
// static every 30th call. With a master torso and a replicant COPY torso
|
|
||||||
// both ticking, the calls alternate 1:1 -- so every 30th call is always the
|
|
||||||
// SAME instance, and the probe reported only the local (untwisted) torso
|
|
||||||
// while the copy's writes were invisible. Sample per instance-kind instead.
|
|
||||||
static const int s_log = getenv("BT_TORSO_LOG") ? 1 : 0;
|
static const int s_log = getenv("BT_TORSO_LOG") ? 1 : 0;
|
||||||
static int s_count[2] = { 0, 0 };
|
static int s_count = 0;
|
||||||
const int kind = isDamagedCopy ? 1 : 0;
|
if (s_log && (s_count % 30) == 0 && s_count < 1800) // sample periodically to show the sweep
|
||||||
if (s_log && (s_count[kind] % 30) == 0 && s_count[kind] < 1800)
|
|
||||||
{
|
{
|
||||||
DEBUG_STREAM << "[torso] PushTwist " << (kind ? "COPY " : "master")
|
DEBUG_STREAM << "[torso] PushTwist node=" << (void*)node << " type=" << (int)jt
|
||||||
<< " node=" << (void*)node << " type=" << (int)jt
|
|
||||||
<< " twist=" << (float)twist << "\n" << std::flush;
|
<< " twist=" << (float)twist << "\n" << std::flush;
|
||||||
}
|
}
|
||||||
++s_count[kind];
|
++s_count;
|
||||||
|
|
||||||
switch (jt) // node+0x10
|
switch (jt) // node+0x10
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -235,30 +235,8 @@ class Joint; // engine skeleton node (JOINT.h); the twist target
|
|||||||
// Controls (@0x1F0 twist, @0x1F4 elevation); proportional, no button ramp.
|
// Controls (@0x1F0 twist, @0x1F4 elevation); proportional, no button ramp.
|
||||||
void SetAnalogTwistAxis(Scalar v) { analogTwistAxis = v; }
|
void SetAnalogTwistAxis(Scalar v) { analogTwistAxis = v; }
|
||||||
void SetAnalogElevationAxis(Scalar v) { analogElevationAxis = v; }
|
void SetAnalogElevationAxis(Scalar v) { analogElevationAxis = v; }
|
||||||
void CommandRecenter() { centerCommand = 1; } // @0x208 HELD button -- writer MUST clear it
|
void CommandRecenter() { centerCommand = 1; } // @0x208 (Basic-mode re-center)
|
||||||
void ClearRecenterCommand() { centerCommand = 0; } // button released (writer-owned state)
|
void ClearRecenterCommand() { centerCommand = 0; } // button released (writer-owned state)
|
||||||
|
|
||||||
// ⚠ centerCommand (@0x208) is a HELD-BUTTON cell: TorsoSimulation re-arms
|
|
||||||
// `recenterActive` from it EVERY frame it is non-zero, and only the input
|
|
||||||
// path clears it. Do NOT use CommandRecenter() for a one-shot recenter --
|
|
||||||
// nothing releases it and the torso re-centres forever, which reads to the
|
|
||||||
// pilot as "lost torso control" (Sauron, control-mode cycle).
|
|
||||||
//
|
|
||||||
// The one-shot the mode switch actually wants is recenterActive (@0x274)
|
|
||||||
// itself: TorsoSimulation runs `recenterActive = Recenter(dt)`, so it
|
|
||||||
// SELF-CLEARS on settle, and any twist input cancels it. This is exactly
|
|
||||||
// what the binary writes -- `*(torso + 0x274) = 1` @004afbe0.
|
|
||||||
void BeginRecenterOnce() { recenterActive = 1; } // @0x274 one-shot (@004afbe0)
|
|
||||||
|
|
||||||
// The TWO authored elevation-limit pairs the control mode swaps between
|
|
||||||
// (@004afbe0). BASIC gets @0x228/@0x22C (full top, HALF bottom -- reduced
|
|
||||||
// downward travel); STANDARD/VETERAN get @0x230/@0x234 (the full authored
|
|
||||||
// pair). Before 2026-08-08 all four were written by the ctor and never
|
|
||||||
// read by anything -- the port simply never implemented the swap.
|
|
||||||
void ApplyBasicElevationLimits()
|
|
||||||
{ verticalLimitTop = elevationCenter; verticalLimitBottom = elevationHalfBottom; }
|
|
||||||
void ApplyAssistedElevationLimits()
|
|
||||||
{ verticalLimitTop = twistCenterHigh; verticalLimitBottom = twistCenterLow; }
|
|
||||||
Logical GetHorizontalEnabled() const { return horizontalEnabled; } // @0x250 (mapper free-aim gate @004afd10)
|
Logical GetHorizontalEnabled() const { return horizontalEnabled; } // @0x250 (mapper free-aim gate @004afd10)
|
||||||
|
|
||||||
// Reachable horizontal (yaw) half-arc the guns can be brought to bear by
|
// Reachable horizontal (yaw) half-arc the guns can be brought to bear by
|
||||||
|
|||||||
@@ -93,9 +93,8 @@ if defined BT_STILL_RUNNING (
|
|||||||
goto btwait
|
goto btwait
|
||||||
)
|
)
|
||||||
echo.
|
echo.
|
||||||
echo The game has exited. (Quitting from the menu lands here -- normal.)
|
echo The game has exited. If it closed unexpectedly, send the operator
|
||||||
echo If it CRASHED or would not start, send the operator BOTH of these
|
echo BOTH of these from the content folder:
|
||||||
echo from the content folder:
|
|
||||||
echo content\lastrun_solo.txt (tiny -- send it EVEN IF there is
|
echo content\lastrun_solo.txt (tiny -- send it EVEN IF there is
|
||||||
echo no solo_*.log at all: that combination
|
echo no solo_*.log at all: that combination
|
||||||
echo is how we spot a blocked launch)
|
echo is how we spot a blocked launch)
|
||||||
|
|||||||
@@ -97,10 +97,7 @@ if defined BT_STILL_RUNNING (
|
|||||||
goto btwait
|
goto btwait
|
||||||
)
|
)
|
||||||
echo.
|
echo.
|
||||||
echo The game has exited.
|
echo The game has exited. If it closed unexpectedly, send the operator BOTH
|
||||||
echo (Normal exits land here too: round ends return everyone to the menu,
|
|
||||||
echo and closing the menu window quits -- neither is a crash.)
|
|
||||||
echo If it CRASHED or would not start, send the operator BOTH:
|
|
||||||
echo content\lastrun_steam.txt (tiny -- send it EVEN IF there is no
|
echo content\lastrun_steam.txt (tiny -- send it EVEN IF there is no
|
||||||
echo steam_*.log at all: that is how we spot
|
echo steam_*.log at all: that is how we spot
|
||||||
echo a launch blocked before it ran)
|
echo a launch blocked before it ran)
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 66 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.5 KiB |
@@ -1,127 +0,0 @@
|
|||||||
"""Night 13: close #137 (not a bug) and file the desktop throttle-latch follow-up. ASCII only."""
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0, r"C:\git\bt411\scratchpad\night7")
|
|
||||||
import gitea
|
|
||||||
|
|
||||||
BODY_137 = """**NOT A BUG -- closing (2026-08-08).** The reset is correct on both sides. The
|
|
||||||
heat is real, and the mech earned it: it respawns still under power.
|
|
||||||
|
|
||||||
## What the field log actually shows
|
|
||||||
|
|
||||||
From Sauron's match log (`steam_20260806_c_michael_XIAOLONG.log`), the respawn at line 39450:
|
|
||||||
|
|
||||||
```
|
|
||||||
39450 [respawn] Mech::Reset 3:30 healed+moved to (...) alive=1 zones=21 subsys=33
|
|
||||||
[techstat] SLaser_1 / LLaser_1 / HUD / Gyroscope / Avionics / GeneratorA / HeatSink
|
|
||||||
... condition CLEARED <- every live condition clears
|
|
||||||
[techstat] Myomers condition 3 SET <- Overheating, immediately
|
|
||||||
[mppr] in thr=1 pre=1 -> ...
|
|
||||||
[gaitSM] cycleSpeed=14.6 legCycle=14.6 state=12 <- already RUNNING
|
|
||||||
[techstat] Condenser5 condition 3 SET <- "dumping into coolant loop 5"
|
|
||||||
39519 [techstat] GeneratorD condition 3 SET <- Sauron's generator D
|
|
||||||
```
|
|
||||||
|
|
||||||
`[rstat]` fires about once per second (98 frames @ 10.2ms), and the reset plus the myomers trip sit
|
|
||||||
inside one rstat block -- so myomers overheat in well under a second, generator D in one to two.
|
|
||||||
|
|
||||||
The reset itself is clean: **every** live condition CLEARs 12-16 lines after each `Mech::Reset`, at
|
|
||||||
every respawn in the log. The synthetic bench agrees -- every roster subsystem, including all six
|
|
||||||
Condensers, reads `T=77 start=77` at the reset.
|
|
||||||
|
|
||||||
## Why it looked like maxed heat
|
|
||||||
|
|
||||||
**`condition 3` is an operating flag, not an alarm.** Census over the whole match:
|
|
||||||
|
|
||||||
| subsystem | cond 3 SET | CLEARED |
|
|
||||||
|---|---|---|
|
|
||||||
| LLaser_2 | 33 | 33 |
|
|
||||||
| LLaser_1 | 31 | 31 |
|
|
||||||
| SRM4 | 26 | 26 |
|
|
||||||
| PPC_2 | 18 | 18 |
|
|
||||||
| GeneratorD | 5 | 4 |
|
|
||||||
| Myomers | 5 | 4 |
|
|
||||||
| Condenser5 | 1 | 1 |
|
|
||||||
|
|
||||||
Every weapon trips Overheating on each volley and clears it on cooldown. **Every subsystem is
|
|
||||||
balanced** -- the odd extra SET is only the log ending mid-heat. Nothing latches, nothing sticks.
|
|
||||||
`condition 6` (BadPower) behaves the same way (Myomers 8/8, PPC_1 3/3, MLaser_1 3/3) as the bus
|
|
||||||
browns out under simultaneous draw. GeneratorD's longest continuous overheat came out normally.
|
|
||||||
|
|
||||||
So the post-respawn SET is the mech *operating*, not a failed reset.
|
|
||||||
|
|
||||||
## Why the heat arrives instantly
|
|
||||||
|
|
||||||
The mech comes back **at whatever throttle the pilot left**: `thr=1`, `cycleSpeed=14.6`, gait state
|
|
||||||
12 at the instant of reset. `Mech::Reset`'s subsystem loop starts at **index 2**, and the
|
|
||||||
ControlsMapper is **index 0** -- so the throttle is never reset.
|
|
||||||
|
|
||||||
**The binary does exactly the same.** That is correct for a pod: the throttle is a physical lever
|
|
||||||
still under the pilot's hand. Respawning under power is authentic behaviour.
|
|
||||||
|
|
||||||
Oracle's read that the myomer heat rate "felt right" was correct, and matches the data.
|
|
||||||
|
|
||||||
## Ruled out along the way
|
|
||||||
|
|
||||||
`Mech::Reset` dispatch, the full subsystem RTIS chain (Generator `@004b215c` is an
|
|
||||||
instruction-for-instruction match; HeatSink `@004ad760` faithful; every RTIS class has a
|
|
||||||
DeathReset), stale coolant-loop links (`linkedSinks` is written only in the streaming ctor and
|
|
||||||
cannot drift), and the valve fractions (`BTRecomputeCondenserValves == @0049f788`).
|
|
||||||
|
|
||||||
## Follow-up
|
|
||||||
|
|
||||||
The one genuine defect found is a desktop-only input issue, filed separately -- see the throttle
|
|
||||||
latch ticket. It does not affect the pod.
|
|
||||||
|
|
||||||
Knowledge base updated so this is not re-chased: `context/decomp-reference.md` §TechStatus (the
|
|
||||||
routine/self-clearing semantics + this post-mortem) and `context/gauges-hud.md`."""
|
|
||||||
|
|
||||||
TITLE_NEW = "Desktop/Steam: virtual throttle lever survives death -- you respawn already running"
|
|
||||||
|
|
||||||
BODY_NEW = """Split out of #137, which was closed as not-a-bug. The heat model and the reset are
|
|
||||||
both correct; this is a port-layer input issue and it affects **desktop/Steam only, never the pod**.
|
|
||||||
|
|
||||||
## The defect
|
|
||||||
|
|
||||||
On the pod the throttle is a physical lever. Respawning under power is authentic -- the pilot's hand
|
|
||||||
is on it, they can see and feel where it is, and `Mech::Reset` deliberately does not touch it (the
|
|
||||||
subsystem loop starts at index 2; the ControlsMapper is index 0). The binary behaves the same way.
|
|
||||||
|
|
||||||
The desktop glass bridge emulates that lever with a **file-static ramp accumulator**:
|
|
||||||
|
|
||||||
* `sLever` -- `static float sLever = 0.0f;` (`game/reconstructed/mech4.cpp:3250`)
|
|
||||||
* published each frame as `gBTDrive.throttle = sLever` (mech4.cpp:3892/3897)
|
|
||||||
* consumed as `key_throttle` -> `throttlePosition` (mechmppr.cpp:677, :780)
|
|
||||||
|
|
||||||
The only things that zero `sLever` are the **X-button all-stop** (mech4.cpp:3754) and a
|
|
||||||
direction-crossing snap (:3842). **Nothing on death or respawn touches it.**
|
|
||||||
|
|
||||||
So a pad/keyboard pilot who dies at speed respawns at speed -- while physically holding nothing, and
|
|
||||||
with the lever position invisible to them. The pod's authenticity argument does not cover this case:
|
|
||||||
there is no lever to feel, and a gamepad stick self-centers, so the input affordance actively
|
|
||||||
contradicts the latched state.
|
|
||||||
|
|
||||||
Observed consequence in the field (Sauron's log, the #137 evidence): the mech leaves the drop zone
|
|
||||||
at `thr=1` / `cycleSpeed=14.6` the instant it spawns, and the myomers, Condenser5 and GeneratorD all
|
|
||||||
trip Overheating within one to two seconds. Reads to the player as "respawned with heat maxed".
|
|
||||||
|
|
||||||
Note this is **not** the same as the Thrustmaster/RIO path, which is already correct:
|
|
||||||
`MechThrustmasterMapper::InterpretControls` (`@004d2150`) recomputes `throttlePosition` from scratch
|
|
||||||
every frame off the live `throttleForward`/`throttleReverse` states, and `throttleForward` is
|
|
||||||
databound (written by the input layer each frame). Only the desktop `sLever` accumulator latches.
|
|
||||||
|
|
||||||
## Suggested fix
|
|
||||||
|
|
||||||
Zero `sLever` (and the detent) on respawn in the desktop bridge only -- the same treatment the
|
|
||||||
X all-stop already applies -- so a desktop pilot comes back stopped. Gate it so pod builds keep the
|
|
||||||
authentic physical-lever behaviour.
|
|
||||||
|
|
||||||
This is a gameplay-behaviour decision as much as a bug fix: it changes whether desktop players
|
|
||||||
respawn moving or stopped. Worth a call before it ships.
|
|
||||||
|
|
||||||
## Not yet done
|
|
||||||
|
|
||||||
Not fixed, not benched. Filed from a static read of the input path plus the field log."""
|
|
||||||
|
|
||||||
gitea.close(137, BODY_137)
|
|
||||||
num = gitea.create(TITLE_NEW, BODY_NEW)
|
|
||||||
print("closed 137; created:", num)
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
"""Night 13: report #141 (fixed) and file the torso-cadence follow-up. ASCII only."""
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0, r"C:\git\bt411\scratchpad\night7")
|
|
||||||
import gitea
|
|
||||||
|
|
||||||
BODY_141 = """**ROOT CAUSE FOUND AND FIXED** (`f01de8c` + sweep `e6c5ac9`), reproduced and measured on a
|
|
||||||
2-node bench (`scratchpad/night13/missileframe.sh`).
|
|
||||||
|
|
||||||
## Reproduced
|
|
||||||
|
|
||||||
Only A sweeps its torso and only A fires, so every REPLICANT line in B's log mirrors one A salvo.
|
|
||||||
New `[launchframe]` receipt (`BT_PROJ_LOG`) prints the yaw of the launch forward vs the BODY forward:
|
|
||||||
|
|
||||||
| | n | max abs(twistDelta) | mean | >0.1 rad |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| master | 165 | 2.2962 | 1.2283 | **100%** |
|
|
||||||
| REPLICANT | 165 | **0.0000** | 0.0000 | **0%** |
|
|
||||||
|
|
||||||
`segResolved=1` on both, and `segYaw == bodyYaw` EXACTLY on the peer -- the launch frame was the
|
|
||||||
bind pose.
|
|
||||||
|
|
||||||
## What it was NOT
|
|
||||||
|
|
||||||
Everything upstream was already correct, which is why it looked like netcode. Both sides pass the
|
|
||||||
mount segment (`GetSegmentIndex()`, task #67 -- master `mislanch.cpp:363`, replicant mirror `:478`).
|
|
||||||
The peer's torso data is fine end to end: records arrive (`atUpd=2.44/-2.39`, `rate=0.305`), the
|
|
||||||
copy extrapolates correctly (`cur=-2.13987 target=-2.13987 copy=1`), and the copy torso
|
|
||||||
demonstrably writes its joint (`PushTwist COPY twist=-1.49601`). Hierarchy identical on both nodes
|
|
||||||
-- same seg 18, same `parentIdx=4`, non-null parent and joint subsystem.
|
|
||||||
|
|
||||||
The twist reached the joint and died at the **segment cache**.
|
|
||||||
|
|
||||||
## Root cause
|
|
||||||
|
|
||||||
`MechWeapon::GetMuzzlePoint` `@004b9948` ends in `FUN_00424da8(owner, segment, out)`, which is
|
|
||||||
`JointedMover::GetSegmentToWorld` instruction-for-instruction:
|
|
||||||
|
|
||||||
```c
|
|
||||||
iVar1 = FUN_00417ab4(param_1 + 0x31c); // GetJointSubsystem()
|
|
||||||
if (*(int *)(iVar1 + 0xfc) != 0) { // AreJointsModified() <- TESTED, never set
|
|
||||||
... walk owner+0x300, seg+0xc = 1 ... // ModifySegment() on every segment
|
|
||||||
*(int *)(iVar1 + 0xfc) = 0; // ModifyJoints(False)
|
|
||||||
}
|
|
||||||
FUN_0040b104(out, FUN_004244dc(seg), owner+0xd0); // x localToWorld
|
|
||||||
```
|
|
||||||
|
|
||||||
**In the 1995 image every muzzle query performs the joints->segments refresh.** Our
|
|
||||||
`BTResolveWeaponMuzzle` -- labelled "the faithful FUN_004b9948" -- hand-composed
|
|
||||||
`GetSegmentToEntity() x localToWorld` and skipped it. `GetSegmentToEntity` only recomputes when
|
|
||||||
`segmentModified` is already set (`SEGMENT.cpp:262`), so it returned a stale cache. On the MASTER
|
|
||||||
that was invisible (the render pass refreshes the local mech every frame, after its torso pushes
|
|
||||||
the joint); a REPLICANT gets no such refresh, so peer muzzles sat at the bind pose.
|
|
||||||
|
|
||||||
## Fix
|
|
||||||
|
|
||||||
Route the muzzle path through the engine accessor, where the binary puts it. **No forced dirty
|
|
||||||
flag** -- an earlier attempt set `ModifyJoints(True)` and scored the same, so it bought nothing and
|
|
||||||
was removed; the binary only ever tests that flag.
|
|
||||||
|
|
||||||
Swept the same unfaithful pattern at three more sites: the generic segment->world bridge, the
|
|
||||||
damage-effect anchor, and **the energy-beam gun port** -- a peer's BEAM had the identical exposure
|
|
||||||
and would also have originated from the untwisted gun port. Repo-wide there is now exactly one
|
|
||||||
`GetSegmentToEntity` call outside `SEGMENT.cpp`: inside `GetSegmentToWorld` itself, after the
|
|
||||||
refresh.
|
|
||||||
|
|
||||||
## Result
|
|
||||||
|
|
||||||
| | n | max | mean | >0.1 rad |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| master | 165 | 2.1719 | 1.2781 | 100% |
|
|
||||||
| REPLICANT | 165 | **2.0907** | 0.8201 | **64%** |
|
|
||||||
|
|
||||||
**The 64% is not a partial fix.** The failures are a contiguous PREFIX with zero interleaved cases:
|
|
||||||
|
|
||||||
```
|
|
||||||
ZZZZ...(60)...ZZZZXXXX...(105)...XXXX
|
|
||||||
```
|
|
||||||
|
|
||||||
and they end exactly when the peer acquires a twist to carry:
|
|
||||||
|
|
||||||
```
|
|
||||||
first torso RECORD received : line 206
|
|
||||||
first copy currentTwist != 0 : line 1016
|
|
||||||
first CORRECT launch frame : line 1054 (38 lines = probe sampling granularity)
|
|
||||||
```
|
|
||||||
|
|
||||||
Those 60 salvos fired while the replicated twist was genuinely 0, so launching along the body
|
|
||||||
facing was CORRECT. Once the peer has a twist, 100% of launches carry it.
|
|
||||||
|
|
||||||
## Field-verify
|
|
||||||
|
|
||||||
Unreleased. Next playtest: have a peer watch a twisted mech fire missiles -- rounds should leave
|
|
||||||
along the torso, not the feet. Also worth checking beams for the same reason (same fix).
|
|
||||||
|
|
||||||
Follow-up filed separately: the peer's torso takes far too long to FIRST acquire the master's
|
|
||||||
twist."""
|
|
||||||
|
|
||||||
TITLE_NEW = "Peer torso twist takes far too long to first sync -- only 13 update records across a 5-minute run"
|
|
||||||
|
|
||||||
BODY_NEW = """Split out of #141, whose launch-frame defect is fixed. This is a separate, measured
|
|
||||||
problem in the torso REPLICATION CADENCE.
|
|
||||||
|
|
||||||
## Measurement
|
|
||||||
|
|
||||||
From the #141 bench (`scratchpad/night13/missileframe.sh` / `missileframe2.sh`, 2 nodes, node A
|
|
||||||
sweeping its torso continuously at 0.35 rad/s for the whole run):
|
|
||||||
|
|
||||||
```
|
|
||||||
first torso RECORD received on the peer : line 206
|
|
||||||
first copy currentTwist != 0 : line 1016
|
|
||||||
```
|
|
||||||
|
|
||||||
and across the entire ~5 minute run the peer received only **13** `[torso-rec-rx]` records, despite
|
|
||||||
the master's twist changing continuously the whole time.
|
|
||||||
|
|
||||||
So the master was twisted from very early on, while the peer's copy torso reported `currentTwist`
|
|
||||||
of exactly 0 for a long stretch afterwards. The extrapolator itself is fine once fed --
|
|
||||||
`ComputeTargetTwist` predicts `twistAtUpdate + twistRate * elapsed` and the copy tracks its target
|
|
||||||
exactly (`cur=-2.13987 target=-2.13987 copy=1`). The problem is how rarely it is fed, and how late
|
|
||||||
the first useful feed arrives.
|
|
||||||
|
|
||||||
## Why it matters
|
|
||||||
|
|
||||||
* It is the entire reason #141's fix reads 64% instead of 100% on the bench -- 60 salvos fired
|
|
||||||
before the peer had any twist to carry.
|
|
||||||
* A peer's torso will visibly LAG or sit straight while the mech is actually twisted. That is
|
|
||||||
plausibly relevant to **#37** (MadCat torso is BACKWARDS) and **#70** (torso twist stops working
|
|
||||||
after respawn) -- worth re-testing both against this once it is understood.
|
|
||||||
|
|
||||||
## Not yet investigated
|
|
||||||
|
|
||||||
Whether 13 records is the authentic cadence (the binary may deliberately send torso updates rarely
|
|
||||||
and lean on `twistRate` extrapolation to cover the gaps -- in which case the bug is that our
|
|
||||||
extrapolation is not running or not seeded until late), or whether our send-side gate is simply too
|
|
||||||
conservative. `Torso::WriteUpdateRecord` snapshots `twistAtUpdate = currentTwist` at send, so the
|
|
||||||
send trigger is the thing to read first.
|
|
||||||
|
|
||||||
Diagnostics already in place: `BT_TORSO_LOG` gives `[torso-rec-rx]` (receive), `[torso-copy]`
|
|
||||||
(the copy's cur/target/atUpd/rate), and `[torso] PushTwist master|COPY` (per instance-kind -- note
|
|
||||||
that probe previously sampled one shared static every 30th call, which with two torsos ticking 1:1
|
|
||||||
always reported the SAME instance and hid the copy entirely; fixed in `05d7b58`)."""
|
|
||||||
|
|
||||||
gitea.comment(141, BODY_141)
|
|
||||||
gitea.call("/issues/141", method="PATCH", payload={"state": "closed"})
|
|
||||||
print("commented + closed #141")
|
|
||||||
num = gitea.create(TITLE_NEW, BODY_NEW)
|
|
||||||
print("created #%d" % num["number"])
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
"""Night 13: close #148 as not-a-bug with the measured chain. ASCII only."""
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0, r"C:\git\bt411\scratchpad\night7")
|
|
||||||
import gitea
|
|
||||||
|
|
||||||
BODY = """**NOT A BUG -- closing (2026-08-08).** There was nothing to fix here. The bench was lying,
|
|
||||||
and this ticket's own premise was wrong twice over.
|
|
||||||
|
|
||||||
## The answer
|
|
||||||
|
|
||||||
`Entity::Execute` (`ENTITY.cpp:556`, real engine source [T0]) calls `PerformAndWatch` **only** when
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
application->GetApplicationState() == Application::RunningMission
|
|
||||||
|| application->GetApplicationState() == Application::EndingMission
|
|
||||||
|| IsPreRunnable()
|
|
||||||
```
|
|
||||||
|
|
||||||
and otherwise merely `WriteSimulationUpdate()`s. `Entity::DefaultFlags` is
|
|
||||||
`DynamicFlag|MasterInstance` -- **no `PreRunFlag`**. Only `Player` and `Director` add it in their
|
|
||||||
DefaultFlags, and `Mech::Reset` sets it for a reset MASTER ("a reset master must tick"). A
|
|
||||||
**replicant mech never gets it.**
|
|
||||||
|
|
||||||
So a peer mech performs **zero** subsystem ticks until the round actually starts, no matter how
|
|
||||||
much correctly-replicated data is arriving for it. Measured on the observer node:
|
|
||||||
|
|
||||||
```
|
|
||||||
235 [perf-first] mech 3:161 master <- own mech, immediately
|
|
||||||
402 [torso-rec-rx] <- peer's torso records start arriving
|
|
||||||
2754 [perf-first] mech 2:55 REPLICANT <- peer's FIRST performance
|
|
||||||
2758 [torso] PushTwist COPY <- its torso ticks 4 lines later
|
|
||||||
2761 [ent-exec] state=5 <- RunningMission
|
|
||||||
```
|
|
||||||
|
|
||||||
The peer starts performing exactly at the RunningMission transition. That is the engine doing
|
|
||||||
what it says it does.
|
|
||||||
|
|
||||||
## So the symptom was a BENCH ARTIFACT
|
|
||||||
|
|
||||||
`BT_AUTOFIRE` starts shooting immediately, during `WaitingForLaunch` -- something no player can do
|
|
||||||
in a real match. Those leading salvos measured a peer whose torso, gait and subsystems had never
|
|
||||||
run. Every `ZZZZ...XXXX` prefix in this investigation was that, and the first `X` lands within a
|
|
||||||
few lines of the state transition.
|
|
||||||
|
|
||||||
**#141 is unaffected and stays fixed** -- its segment-cache defect was real and mid-match.
|
|
||||||
|
|
||||||
## Ruled out along the way (all measured, all recorded so nobody repeats them)
|
|
||||||
|
|
||||||
* **The record cadence is authentic.** My "only 13 records in 5 minutes" premise was wrong. The
|
|
||||||
payloads are the sweep EXTREMES with `rate` flipping sign at each one -- the master sends on
|
|
||||||
**rate change** and the peer dead-reckons `atUpd + rate * elapsed` between them. 12 records for
|
|
||||||
12 direction reversals is correct, not starved.
|
|
||||||
* **The `ComputeTargetTwist` clamp.** The copy's limits load correctly (`limL=2.44346
|
|
||||||
limR=-2.44346 enab=1`), so `Min/Max` was not pinning `targetTwist` to zero.
|
|
||||||
* **The torso's own executable flag.** `Entity::Perform` picks its predicate by instance
|
|
||||||
(`IsNonReplicantExecutable` vs `IsReplicantExecutable`, differing on
|
|
||||||
`|| lastUpdate >= lastPerformance`), and Mech's tick loop had dropped that branch. Restoring it
|
|
||||||
(`f36f013`) is a genuine fidelity fix and is kept -- but it moved this bug by nothing.
|
|
||||||
* **The scheduler.** The replicant entity IS offered to the performer with `executable=1` from
|
|
||||||
line 171, ~2500 lines before its first `PerformAndWatch`. The gate was inside `Execute`, not in
|
|
||||||
who gets offered.
|
|
||||||
|
|
||||||
## What came out of it
|
|
||||||
|
|
||||||
* `[perf-first]` -- a one-shot per-mech receipt naming entity ID + instance at a mech's first
|
|
||||||
performance. Every other per-frame receipt in mech4 is anonymous, which is exactly why this took
|
|
||||||
so long to see in a 2-node log.
|
|
||||||
* `[torso-copy]` now prints `limL/limR/enab`; `[launchframe]` prints the shooter's live torso twist.
|
|
||||||
* **Gotcha #29** in `context/reconstruction-gotchas.md`: judge a 2-node bench by PREFIX vs
|
|
||||||
INTERLEAVED, never by raw percentage; check `[ent-exec] state=` before suspecting replication.
|
|
||||||
`missileframe.sh` carries the same warning inline.
|
|
||||||
|
|
||||||
## Still worth checking separately
|
|
||||||
|
|
||||||
`#37` (MadCat torso BACKWARDS) and `#70` (torso twist stops after respawn) were flagged here as
|
|
||||||
possibly sharing a cause. They do **not** share this one -- it is not a defect. They should be
|
|
||||||
re-tested against the #141 segment-cache fix instead, which is a real mid-match change."""
|
|
||||||
|
|
||||||
gitea.comment(148, BODY)
|
|
||||||
gitea.call("/issues/148", method="PATCH", payload={"state": "closed"})
|
|
||||||
print("closed #148")
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
"""Night 13: report the #52 root cause on the tracker. ASCII only."""
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0, r"C:\git\bt411\scratchpad\night7")
|
|
||||||
import gitea
|
|
||||||
|
|
||||||
BODY = """**ROOT CAUSE FOUND AND FIXED (2026-08-07, commit `6a96fb6`)** -- but read the
|
|
||||||
"what is NOT proven" section before treating this as closed.
|
|
||||||
|
|
||||||
## The defect
|
|
||||||
|
|
||||||
A replicant cannot start walking between gait-change records.
|
|
||||||
|
|
||||||
The port's body `case 4` (the task-#64 lockstep twin, mech2.cpp) is an INSERTION sitting between
|
|
||||||
`case 0` and the advance group. In the binary it is a MEMBER of that group -- `FUN_004a5678`
|
|
||||||
@004a5678 reads `case 2,3,`**`4`**`,5,8,...` with no turn block and no speed exit [T1] -- so case
|
|
||||||
0's fallthrough is meant to land on `Advance()`. The insertion intercepted it.
|
|
||||||
|
|
||||||
On a replicant that is not a race, it is an identity:
|
|
||||||
|
|
||||||
* case 0 arms walk iff `standSpeed < bodyTargetSpeed`
|
|
||||||
* the inserted block resets iff `standSpeed < bspd`, and `bspd` **IS** `bodyTargetSpeed` on a replicant
|
|
||||||
|
|
||||||
Same expression. Arm and reset fire on the same frame, every frame. A peer parked at Standing with a
|
|
||||||
live replicated demand never cycles: `bodyCycleSpeed` stays 0 while position advances from dead
|
|
||||||
reckoning. That is the skate. (Reverse is dead the same way -- both sides test `< ZeroSpeed`.)
|
|
||||||
|
|
||||||
## Why it appeared when it did
|
|
||||||
|
|
||||||
This is the sequel to `e91d447` (#82). Before that commit the replicant branch read the LOCAL
|
|
||||||
mapper's `speedDemand` -- a dead cell on a peer, 0 forever -- so the exit never fired and the
|
|
||||||
fallthrough worked BY ACCIDENT. Fixing the dead cell (correctly) closed the accidental escape
|
|
||||||
hatch, and the trn-lock skate came back as a Standing-lock skate.
|
|
||||||
|
|
||||||
## Why masters were unaffected
|
|
||||||
|
|
||||||
Two reasons, either sufficient: their two tests read DIFFERENT cells (`bodyTargetSpeed` held at
|
|
||||||
last-sent by the gait mirror, vs the live mapper `speedDemand`), so they only stall in the window
|
|
||||||
where those disagree; and the master's body channel runs `mj=0` and writes no joints, so its stall
|
|
||||||
is invisible -- the leg channel, whose case 0 falls through correctly, drives pose and travel.
|
|
||||||
|
|
||||||
## The load-bearing detail
|
|
||||||
|
|
||||||
`mech4.cpp`'s "stand; case 0 walk-begins next tick" is not an aside. A peer's body state is set from
|
|
||||||
`record->legState` only on **type-3 edges**, and ENTERING Standing emits one while LEAVING it does
|
|
||||||
not. Between gait-change records a replicant is REQUIRED to derive walking itself from the
|
|
||||||
replicated demand. The insertion removed that ability.
|
|
||||||
|
|
||||||
This also explains the shape of the field data: the lock needs a mech holding a STEADY demand, so a
|
|
||||||
mech whose gait keeps changing is continually rescued by records. Night 13's four episodes all carry
|
|
||||||
`bodyTargetSpeed` 39-48 held across 100-400 frames, and each ended when that mech next changed gait.
|
|
||||||
|
|
||||||
## Fix
|
|
||||||
|
|
||||||
`case 0` -> `goto advance_body_normally`, the leg twin's own idiom, restoring the binary's structure
|
|
||||||
without touching the #64/#82 turn logic. `BT_NO_BODY_FALLTHRU=1` reverts.
|
|
||||||
|
|
||||||
## Measured (2-node, `scratchpad/night13/skatelock.sh`)
|
|
||||||
|
|
||||||
| | legacy | fixed |
|
|
||||||
|---|---|---|
|
|
||||||
| STANDING-LOCK seconds | **336 consecutive**, `bspd=39.2324 bts=39.2324` identical every line | **0**, every pass |
|
|
||||||
| master body-Standing samples | 52 | 21 |
|
|
||||||
| turn-in-place | -- | body state 4 x9 / leg state 4 x8, armed in lockstep |
|
|
||||||
|
|
||||||
## What is NOT proven [T3]
|
|
||||||
|
|
||||||
That this accounts for the night-13 episodes. The lock is proven and proven removed; the link to the
|
|
||||||
field symptom is INFERENCE -- a locked peer has `bodyCycleSpeed==0` and never advances its clip, so
|
|
||||||
locked + translating IS the `[skate]` signature by construction -- but no bench caught the two
|
|
||||||
together. Four rigs failed to reproduce the symptom end-to-end.
|
|
||||||
|
|
||||||
The `[skate]` line now carries `bstate=`, which is the diagnostic tonight's logs lacked. Next
|
|
||||||
playtest settles it: episodes gone -> confirmed; any survivor names its own state.
|
|
||||||
|
|
||||||
**Correction for the record:** the night-12 `skatebench` "reproductions" (6 episodes, `sk_run.out`)
|
|
||||||
were a DETECTOR ARTIFACT, not this bug. The first detector build tested only `legCycleSpeed==0`,
|
|
||||||
which is NORMAL on a peer -- the body channel poses it -- so it fired on every healthy movement
|
|
||||||
phase. It was corrected the same day to require both channels idle. Old-format lines
|
|
||||||
(`legCycleSpeed=`, no `bodyCyc=`) are not evidence of anything.
|
|
||||||
|
|
||||||
Related: #130 (Vulture skating) is very likely the same defect -- re-test it against this build
|
|
||||||
before spending separate effort."""
|
|
||||||
|
|
||||||
gitea.comment(52, BODY)
|
|
||||||
gitea.comment(130,
|
|
||||||
"Cross-ref: #52's root cause was found and fixed 2026-08-07 (`6a96fb6`) -- a replicant could not "
|
|
||||||
"start walking between gait-change records, so a peer parked at Standing with a live replicated "
|
|
||||||
"demand kept its position advancing with a dead animation channel. That is the same shape as the "
|
|
||||||
"skating reported here. **Re-test this against a build newer than `6a96fb6` before investigating "
|
|
||||||
"separately.** Full write-up in #52.")
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# =========================================================================
|
|
||||||
# #142 CROUCH does not toggle -- name the gate.
|
|
||||||
#
|
|
||||||
# FIELD (Oracle, night 13): "the crouch button did not toggle to display
|
|
||||||
# crouched ... no state change ... the light next to it always flashes when
|
|
||||||
# pressed, but state does not change. Remains in stand mode."
|
|
||||||
#
|
|
||||||
# That is the EJECT bug's signature: the press reaches the handler (lamp
|
|
||||||
# responds) and a GATE silently declines it. DuckRequestMessageHandler
|
|
||||||
# always succeeds -- it just sets duckState=1. The CONSUMER (mech4.cpp) is
|
|
||||||
# where it dies:
|
|
||||||
#
|
|
||||||
# if (duckState != 0 && squatCapable != 0) {
|
|
||||||
# if (mapPosture == 1) squat;
|
|
||||||
# else if (mapPosture == 2) rise;
|
|
||||||
# else if (BT_DUCK_LOG) log; <- only diagnostic, env-gated
|
|
||||||
# duckState = 0;
|
|
||||||
# }
|
|
||||||
#
|
|
||||||
# squatCapable == 0 skips the whole block: no log, and the latch is not even
|
|
||||||
# consumed. A new ungated [duck] REQUEST DROPPED receipt covers both misses.
|
|
||||||
#
|
|
||||||
# Runs the SAME chassis twice is pointless -- squatCapable is per-model
|
|
||||||
# ('squ'/'sqd' clips shipped), so sweep several. CROUCH is button 0x13
|
|
||||||
# (shipped bindings: left column 0x10-0x15 = map+/map-/IR/CROUCH/searchlight/
|
|
||||||
# display). BT_BTNTEST2 exists precisely for "crouch then rise".
|
|
||||||
# =========================================================================
|
|
||||||
set -x
|
|
||||||
V="${1:-madcat}"
|
|
||||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
|
||||||
cd /c/git/bt411/content || exit 1
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 2
|
|
||||||
rm -f cr_${V}.log
|
|
||||||
bt_expert_egg MP.EGG CR.EGG
|
|
||||||
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=${V}/" CR.EGG
|
|
||||||
|
|
||||||
( export BT_BTNTEST=0x13,900,960 # CROUCH press
|
|
||||||
export BT_BTNTEST2=0x13,1500,1560 # and again (toggle back / retry)
|
|
||||||
export BT_DUCK_LOG=1 BT_GAIT_LOG=1 BT_KEY_NOFOCUS=1
|
|
||||||
bt_launch cr_${V}.log CR.EGG 0x03 )
|
|
||||||
for i in $(seq 1 60); do grep -aq "btntest" cr_${V}.log 2>/dev/null && break; sleep 2; done
|
|
||||||
sleep 45
|
|
||||||
bt_kill_ours; sleep 2; taskkill //F //IM btl4.exe > /dev/null 2>&1; sleep 2
|
|
||||||
|
|
||||||
echo "=================== #142 CROUCH vehicle=$V ==================="
|
|
||||||
echo "--- 1. did the press reach the handler? ---"
|
|
||||||
grep -a "btntest" cr_${V}.log | head -4
|
|
||||||
grep -a "DuckRequest" cr_${V}.log | head -3
|
|
||||||
echo
|
|
||||||
echo "--- 2. THE GATE: why was it dropped? ---"
|
|
||||||
grep -a "REQUEST DROPPED" cr_${V}.log | head -4
|
|
||||||
echo -n "dropped-receipt count: "; grep -ac "REQUEST DROPPED" cr_${V}.log
|
|
||||||
echo
|
|
||||||
echo "--- 3. did a posture change actually happen? ---"
|
|
||||||
grep -aE "\[duck\] (SQUAT|RISE)" cr_${V}.log | head -4
|
|
||||||
echo -n "SQUAT/RISE events: "; grep -acE "\[duck\] (SQUAT|RISE)" cr_${V}.log
|
|
||||||
echo
|
|
||||||
echo "--- 4. does this chassis even ship the squat clips? ---"
|
|
||||||
grep -aiE "squatCapable|squ.*clip|sqd" cr_${V}.log | head -4
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
"""#142 -- correct the report and record what benching established. ASCII only."""
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0, r"C:\git\bt411\scratchpad\night7")
|
|
||||||
import gitea
|
|
||||||
|
|
||||||
gitea.comment(142, """**RE-SCOPED 2026-08-07 -- the mech crouches fine; this is a missing PANEL ANIMATION.**
|
|
||||||
|
|
||||||
The original report ("the crouch button did not toggle to display crouched ... the light next to
|
|
||||||
it always flashes when pressed, but state does not change") reads like a locomotion or lamp bug.
|
|
||||||
Benching says it is neither.
|
|
||||||
|
|
||||||
## What works (benched, `scratchpad/night13/crouch142.sh` + `crouchmp.sh`)
|
|
||||||
|
|
||||||
* SOLO, madcat: both presses reach `DuckRequestMessageHandler`, zero drops, `[duck] SQUAT (posture
|
|
||||||
1 -> leg clip 2)` then `squat clip parked`, then `[duck] RISE`. A full toggle cycle.
|
|
||||||
* MULTIPLAYER, same chassis: identical -- 2 requests, 0 drops, SQUAT + RISE. So the MP path is not
|
|
||||||
refusing it either.
|
|
||||||
|
|
||||||
A receipt was added at the consumer's silent miss (`[duck] REQUEST DROPPED`, ungated) covering both
|
|
||||||
gates -- `squatCapable == 0` (which skips the consumer entirely AND leaves `duckState` latched at 1,
|
|
||||||
with no log at all today) and `mapPosture` not 1/2 (previously logged only under `BT_DUCK_LOG`,
|
|
||||||
which no player sets). Neither fired on madcat. Chassis without `squ`/`sqd` clips are still
|
|
||||||
untested; the bench takes a vehicle argument for that sweep.
|
|
||||||
|
|
||||||
## What the pilot actually sees
|
|
||||||
|
|
||||||
The crouch button's LAMP is momentary press feedback, not state. Traced with `BT_LAMP_LOG`:
|
|
||||||
|
|
||||||
PRESS -> [lamp] 0x13 <- 0x3c (lit)
|
|
||||||
SQUAT -> mech crouches, clip parked
|
|
||||||
RELEASE -> [lamp] 0x13 <- 0x14 (unlit) <-- while still CROUCHED
|
|
||||||
PRESS2 -> [lamp] 0x13 <- 0x3c
|
|
||||||
RISE -> mech stands
|
|
||||||
RELEASE -> [lamp] 0x13 <- 0x14
|
|
||||||
|
|
||||||
Crouched and standing are visually identical, so the lamp can never carry posture.
|
|
||||||
|
|
||||||
## What it is SUPPOSED to be (era testimony, this day)
|
|
||||||
|
|
||||||
The operator, correcting the framing: the crouch button is supposed to **animate a MECH SYMBOL next
|
|
||||||
to the button**, standing <-> crouching. Not a two-state lamp -- a missing animation.
|
|
||||||
|
|
||||||
VGL Lynx: *"When a mech stops, crouch button lowers its stance and plays crouch animation. Mech is
|
|
||||||
immobilized until crouch is pushed again, and mech rises."* Draco: *"Checks out with my memories."*
|
|
||||||
|
|
||||||
## Two gaps, and the second is the reported one
|
|
||||||
|
|
||||||
1. **No immobilization while crouched.** Nothing in the port gates movement on `duckState` or the
|
|
||||||
parked leg alarm -- grep of every consumer of both across `game/reconstructed/` finds no
|
|
||||||
speed/throttle/demand gate. Per Lynx the mech should be immobile until the button is pushed
|
|
||||||
again. ⚠ Worth checking what a crouched mech that IS driven does today, since the leg clip is
|
|
||||||
parked -- a moving mech with a parked leg channel is the [skate] signature (#52).
|
|
||||||
2. **No stance symbol.** No gauge element anywhere in `btl4gau*.cpp` draws a posture/stance icon,
|
|
||||||
and the decomp has no crouch/squat/stance/duck graphic STRING (only substring false positives:
|
|
||||||
"existance", "distance"). So it is likely an authored IMAGE element on the secondary/radar MFD
|
|
||||||
rather than something findable by name -- next step is the secondary MFD's element list in the
|
|
||||||
binary, not another string search.
|
|
||||||
|
|
||||||
Not fixed. Re-scoped, with the locomotion half cleared and the real target named.""")
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# =========================================================================
|
|
||||||
# #142 CROUCH in MULTIPLAYER -- solo works, so MP is the variable.
|
|
||||||
#
|
|
||||||
# crouch142.sh proved a madcat crouches and rises cleanly SOLO: both presses
|
|
||||||
# reached DuckRequest, zero REQUEST DROPPED, SQUAT then RISE fired. The
|
|
||||||
# reporters were in MP, and the wording matters:
|
|
||||||
# Oracle: "the crouch button did not toggle to DISPLAY crouched ... no state
|
|
||||||
# change ... the light next to it always flashes when pressed, but
|
|
||||||
# state does not change. Remains in stand mode."
|
|
||||||
#
|
|
||||||
# Three things that could produce that with the mech itself working:
|
|
||||||
# (a) MP-only refusal -- some gate differs on a networked master
|
|
||||||
# (b) replication -- master squats, PEER never poses it
|
|
||||||
# (c) indicator -- mech squats, but the button LAMP / state readout
|
|
||||||
# never latches, so the pilot sees "stand"
|
|
||||||
#
|
|
||||||
# Same chassis as the passing solo run, so MP is the only changed variable.
|
|
||||||
# A presses crouch twice; B observes. Read all three layers.
|
|
||||||
# =========================================================================
|
|
||||||
set -x
|
|
||||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
|
||||||
cd /c/git/bt411/content || exit 1
|
|
||||||
bt_assert_player_env
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 2
|
|
||||||
rm -f cm_a.log cm_b.log cm_relay.log
|
|
||||||
bt_expert_egg MP.EGG CM.EGG
|
|
||||||
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" CM.EGG
|
|
||||||
|
|
||||||
( export BT_DUCK_LOG=1 BT_GAIT_LOG=1 BT_MP_LOG=1
|
|
||||||
bt_launch cm_b.log CM.EGG 0x0C -net 1601 )
|
|
||||||
sleep 2
|
|
||||||
( export BT_BTNTEST=0x13,900,960 BT_BTNTEST2=0x13,1800,1860
|
|
||||||
export BT_DUCK_LOG=1 BT_GAIT_LOG=1 BT_MP_LOG=1 BT_KEY_NOFOCUS=1
|
|
||||||
bt_launch cm_a.log CM.EGG 0x03 -net 1501 )
|
|
||||||
sleep 5
|
|
||||||
python ../tools/btconsole.py CM.EGG 127.0.0.1:1501 127.0.0.1:1601 > cm_relay.log 2>&1 &
|
|
||||||
RELAY=$!
|
|
||||||
sleep 240
|
|
||||||
kill $RELAY 2>/dev/null
|
|
||||||
sleep 3
|
|
||||||
bt_kill_ours; sleep 2; taskkill //F //IM btl4.exe > /dev/null 2>&1; sleep 3
|
|
||||||
|
|
||||||
AID=$(grep -aoE "MY mech entityID=[0-9]+:[0-9]+" cm_a.log | head -1 | cut -d= -f2)
|
|
||||||
echo "=================== #142 CROUCH in MP (A=${AID:-?}) ==================="
|
|
||||||
echo "--- (a) did A's press reach the handler, and was it dropped? ---"
|
|
||||||
grep -a "btntest" cm_a.log | head -4
|
|
||||||
echo -n "DuckRequests : "; grep -ac "DuckRequest" cm_a.log
|
|
||||||
echo -n "REQUEST DROPPED: "; grep -ac "REQUEST DROPPED" cm_a.log
|
|
||||||
grep -a "REQUEST DROPPED" cm_a.log | head -3
|
|
||||||
echo
|
|
||||||
echo "--- (b) did A's own mech actually change posture? ---"
|
|
||||||
grep -aE "\[duck\] (SQUAT|RISE|squat clip)" cm_a.log | head -6
|
|
||||||
echo -n "SQUAT/RISE on A: "; grep -acE "\[duck\] (SQUAT|RISE)" cm_a.log
|
|
||||||
echo
|
|
||||||
echo "--- (c) did the PEER pose the squat? (legState ships in the type-3 record) ---"
|
|
||||||
echo -n "B duck lines for A: "; grep -ac "\[duck\]" cm_b.log
|
|
||||||
grep -a "\[duck\]" cm_b.log | head -4
|
|
||||||
echo -n "B legState-2/3 sightings: "; grep -aoE "legState=[23]" cm_b.log | wc -l
|
|
||||||
echo
|
|
||||||
echo "--- (d) the INDICATOR: does anything latch a crouched state? ---"
|
|
||||||
grep -aiE "lamp.*0x13|stability|duckState" cm_a.log | head -6
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# =========================================================================
|
|
||||||
# #142 -- does CROUCH reset on respawn? (Oracle: "crouch wasn't resetting
|
|
||||||
# on respawn ... i guess mechs always spawn standing")
|
|
||||||
#
|
|
||||||
# Mech::Reset (@0049fb74) stands the mech up in the binary:
|
|
||||||
# *(this+0x398) = 0 duckState
|
|
||||||
# legStateAlarm -> 0 (this+0x39c)
|
|
||||||
# bodyStateAlarm -> 0 (this+0x714)
|
|
||||||
# death + leg/body reset latches, idleStrideScale = 1.0
|
|
||||||
# The port had dropped every one of those, so a pilot who died CROUCHED came
|
|
||||||
# back crouched.
|
|
||||||
#
|
|
||||||
# THE TEST: A crouches, then is killed while crouched, then respawns.
|
|
||||||
# PASS: after "[respawn] Mech::Reset", the leg alarm is 0 (standing) and
|
|
||||||
# duckState is 0 -- and no squat clip is parked.
|
|
||||||
# FAIL: legLvl stays 1 (parked in 'sqd') across the respawn.
|
|
||||||
#
|
|
||||||
# B force-damages A so the death lands while A is parked in the squat.
|
|
||||||
# =========================================================================
|
|
||||||
set -x
|
|
||||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
|
||||||
cd /c/git/bt411/content || exit 1
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 2
|
|
||||||
rm -f dr_a.log dr_b.log dr_relay.log
|
|
||||||
bt_expert_egg MP.EGG DR.EGG
|
|
||||||
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" DR.EGG
|
|
||||||
|
|
||||||
( export BT_DEATH_LOG=1 BT_MP_LOG=1
|
|
||||||
bt_launch dr_b.log DR.EGG 0x0C -net 1601 )
|
|
||||||
sleep 2
|
|
||||||
# A: crouch early and STAY down, so the kill lands on a crouched mech
|
|
||||||
( export BT_BTNTEST=0x13,900,960 BT_SELF_DAMAGE=6
|
|
||||||
export BT_DUCK_LOG=1 BT_DEATH_LOG=1 BT_MP_LOG=1 BT_KEY_NOFOCUS=1
|
|
||||||
bt_launch dr_a.log DR.EGG 0x03 -net 1501 )
|
|
||||||
sleep 5
|
|
||||||
python ../tools/btconsole.py DR.EGG 127.0.0.1:1501 127.0.0.1:1601 > dr_relay.log 2>&1 &
|
|
||||||
RELAY=$!
|
|
||||||
sleep 260
|
|
||||||
kill $RELAY 2>/dev/null
|
|
||||||
sleep 3
|
|
||||||
bt_kill_ours; sleep 2; taskkill //F //IM btl4.exe > /dev/null 2>&1; sleep 3
|
|
||||||
|
|
||||||
echo "=================== #142 CROUCH ACROSS RESPAWN ==================="
|
|
||||||
echo "--- did A crouch, then die, then respawn? ---"
|
|
||||||
grep -aE "\[duck\] (SQUAT|RISE)|death cycle START|Mech::Reset" dr_a.log | head -8
|
|
||||||
echo
|
|
||||||
echo "--- leg alarm around the respawn (1 = parked in 'sqd', 0 = standing) ---"
|
|
||||||
grep -aE "probe legLvl|Mech::Reset" dr_a.log | tail -12
|
|
||||||
echo
|
|
||||||
echo "--- the strip's frame after respawn (must NOT be the up-arrow) ---"
|
|
||||||
grep -a "probe legLvl" dr_a.log | tail -3
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# =========================================================================
|
|
||||||
# PANIC EJECT -> does the respawn replicate? (#108, night 13)
|
|
||||||
#
|
|
||||||
# Fourth attempt at the trigger. The first three drove the panic button
|
|
||||||
# through BT_BTNTEST (EmitButton -> RIO queue). It never reached the
|
|
||||||
# mapper: no [eject], no PUNCH-OUT, not even a lamp change -- and the same
|
|
||||||
# seam failed to toggle the searchlight (0x14), so it was the SEAM, not the
|
|
||||||
# button address. BT_EJECT_AT is the purpose-built hook (mech4.cpp:3310):
|
|
||||||
# it synthesizes the identical press inside the input path -- "one message,
|
|
||||||
# one dispatch", the same Mech::EjectPilotMessageID the key sends -- and
|
|
||||||
# repeats every 300 frames, so one run yields many punch-outs.
|
|
||||||
#
|
|
||||||
# PASS : for every punch-out on A, B logs
|
|
||||||
# "[respawn] replicant <A> un-wrecked + warp (mode 9->1)"
|
|
||||||
# FAIL : punch-outs on A with no matching un-wreck <- the ghost
|
|
||||||
#
|
|
||||||
# Control already banked (ejectghost.sh): 9 force-kill deaths -> 8 peer
|
|
||||||
# un-wrecks, 0 ghost lines. NORMAL death replication is good on this build,
|
|
||||||
# so an eject-only shortfall here is the defect, not a broken rig.
|
|
||||||
# =========================================================================
|
|
||||||
set -x
|
|
||||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
|
||||||
cd /c/git/bt411/content || exit 1
|
|
||||||
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 2
|
|
||||||
rm -f ea_a.log ea_b.log ea_relay.log
|
|
||||||
|
|
||||||
bt_expert_egg MP.EGG EA.EGG
|
|
||||||
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" EA.EGG
|
|
||||||
|
|
||||||
( export BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1 BT_FOG_LOG=1
|
|
||||||
bt_launch ea_b.log EA.EGG 0x0C -net 1601 )
|
|
||||||
sleep 2
|
|
||||||
( export BT_AUTODRIVE=0.6 BT_EJECT_AT=1800
|
|
||||||
export BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1
|
|
||||||
bt_launch ea_a.log EA.EGG 0x03 -net 1501 )
|
|
||||||
sleep 5
|
|
||||||
python ../tools/btconsole.py EA.EGG 127.0.0.1:1501 127.0.0.1:1601 > ea_relay.log 2>&1 &
|
|
||||||
RELAY=$!
|
|
||||||
sleep 300
|
|
||||||
kill $RELAY 2>/dev/null
|
|
||||||
sleep 3
|
|
||||||
bt_kill_ours
|
|
||||||
sleep 2
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 3
|
|
||||||
|
|
||||||
AID=$(grep -aoE "MY mech entityID=[0-9]+:[0-9]+" ea_a.log | head -1 | cut -d= -f2)
|
|
||||||
echo "=================== PANIC-EJECT REPLICATION ==================="
|
|
||||||
echo "A entity: ${AID:-UNKNOWN}"
|
|
||||||
echo
|
|
||||||
echo "--- 1. did punch-outs actually happen? (if 0 the run is VOID) ---"
|
|
||||||
echo -n "DeathWithoutHonor notices : "; grep -ac "DeathWithoutHonor" ea_a.log
|
|
||||||
echo -n "PUNCH-OUT lines : "; grep -ac "PUNCH-OUT" ea_a.log
|
|
||||||
grep -a "PUNCH-OUT" ea_a.log | head -6
|
|
||||||
echo
|
|
||||||
echo "--- 2. A's own death/respawn cycles ---"
|
|
||||||
echo -n "death cycle STARTs : "; grep -ac "death cycle START" ea_a.log
|
|
||||||
echo -n "Mech::Reset : "; grep -ac "Mech::Reset ${AID:-@@@}" ea_a.log
|
|
||||||
echo
|
|
||||||
echo "--- 3. B: the peer's un-wrecks for A (THE NUMBER THAT MATTERS) ---"
|
|
||||||
echo -n "un-wrecks seen : "; grep -a "respawn\] replicant" ea_b.log | grep -ac "${AID:-@@@}"
|
|
||||||
grep -a "respawn\] replicant" ea_b.log | grep -a "${AID:-@@@}" | head -8
|
|
||||||
echo
|
|
||||||
echo "--- 4. B: ghost detector + searchlight cones ---"
|
|
||||||
echo -n "ghost lines : "; grep -ac "\[ghost\]" ea_b.log
|
|
||||||
grep -a "\[ghost\]" ea_b.log | head -4
|
|
||||||
echo -n "cone SHOWN/HIDDEN : "; grep -ac "\[spot\] cone" ea_b.log
|
|
||||||
echo
|
|
||||||
echo "--- 5. VERDICT INPUTS ---"
|
|
||||||
echo "punch-outs=$(grep -ac 'PUNCH-OUT' ea_a.log) A-respawns=$(grep -ac 'death cycle START' ea_a.log) B-unwrecks=$(grep -a 'respawn\] replicant' ea_b.log | grep -ac "${AID:-@@@}")"
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# =========================================================================
|
|
||||||
# EJECT-GHOST bench (#108 / night 13) -- does a PANIC EJECT replicate its
|
|
||||||
# respawn to peers?
|
|
||||||
#
|
|
||||||
# FIELD REPORT: many ghost mechs tonight, all from panic-button self
|
|
||||||
# destructs, none respawning properly -- and the operator has seen
|
|
||||||
# eject-respawn work every time before tonight, so this is a REGRESSION
|
|
||||||
# (night 12 = build 774, tonight = 817).
|
|
||||||
#
|
|
||||||
# FIELD EVIDENCE (staged logs): the ejecting mech's OWNER ran the whole
|
|
||||||
# death path and respawned and kept driving in the same round, while BOTH
|
|
||||||
# peers created his wreck and never processed the un-wreck. The same mech's
|
|
||||||
# NORMAL deaths replicated their respawns 4-6 times in that same session.
|
|
||||||
#
|
|
||||||
# THE TEST: A punches out via the real click seam (button 0x3D, the panic
|
|
||||||
# button -- the same addr the field log shows). B watches.
|
|
||||||
# PASS : B logs "[respawn] replicant <A> un-wrecked + warp (mode 9->1)"
|
|
||||||
# FAIL : B logs the wreck and never the un-wreck <- the ghost
|
|
||||||
# A CONTROL kill comes first (B force-damages A), so the SAME run shows a
|
|
||||||
# normal death replicating correctly -- otherwise a silent B proves nothing.
|
|
||||||
#
|
|
||||||
# Press polls: >=900 per the harness contract (round-start jitter eats
|
|
||||||
# earlier presses). Panic twice, so one missed press does not read as a
|
|
||||||
# pass.
|
|
||||||
# =========================================================================
|
|
||||||
set -x
|
|
||||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
|
||||||
cd /c/git/bt411/content || exit 1
|
|
||||||
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 2
|
|
||||||
rm -f eg_a.log eg_b.log eg_relay.log
|
|
||||||
|
|
||||||
bt_assert_player_env
|
|
||||||
bt_expert_egg MP.EGG EG.EGG
|
|
||||||
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" EG.EGG
|
|
||||||
|
|
||||||
# ---- node B: OBSERVER + the control killer -------------------------------
|
|
||||||
( export BT_MP_FORCE_DMG=1
|
|
||||||
export BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1
|
|
||||||
bt_launch eg_b.log EG.EGG 0x0C -net 1601 )
|
|
||||||
sleep 2
|
|
||||||
# ---- node A: walks, gets killed (control), then PUNCHES OUT twice --------
|
|
||||||
( export BT_AUTODRIVE=0.6
|
|
||||||
export BT_BTNTEST=0x3d,900,960 # panic eject #1
|
|
||||||
export BT_BTNTEST2=0x3d,2400,2460 # panic eject #2
|
|
||||||
export BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1
|
|
||||||
bt_launch eg_a.log EG.EGG 0x03 -net 1501 )
|
|
||||||
sleep 5
|
|
||||||
python ../tools/btconsole.py EG.EGG 127.0.0.1:1501 127.0.0.1:1601 > eg_relay.log 2>&1 &
|
|
||||||
RELAY=$!
|
|
||||||
sleep 300
|
|
||||||
kill $RELAY 2>/dev/null
|
|
||||||
sleep 3
|
|
||||||
bt_kill_ours
|
|
||||||
sleep 2
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 3
|
|
||||||
|
|
||||||
AID=$(grep -aoE "MY mech entityID=[0-9]+:[0-9]+" eg_a.log | head -1 | cut -d= -f2)
|
|
||||||
echo "=================== EJECT-GHOST RESULT ==================="
|
|
||||||
echo "A's entity id: ${AID:-UNKNOWN}"
|
|
||||||
echo
|
|
||||||
echo "--- did the scripted panic actually press? ---"
|
|
||||||
grep -a "btntest" eg_a.log
|
|
||||||
echo
|
|
||||||
echo "--- A: punch-outs + its own death/respawn cycle ---"
|
|
||||||
grep -aE "\[eject\]|PUNCH-OUT|death cycle START|dz\] GRANTED|Mech::Reset" eg_a.log | head -20
|
|
||||||
echo
|
|
||||||
echo "--- B: the peer's view of A, in order (wreck / un-wreck) ---"
|
|
||||||
grep -aE "wreck:|respawn\] replicant" eg_b.log | grep -a "${AID:-@@@}" | head -20
|
|
||||||
echo
|
|
||||||
echo "--- B: totals for A ---"
|
|
||||||
echo -n "wrecks seen : "; grep -a "wreck:" eg_b.log | grep -ac "${AID:-@@@}"
|
|
||||||
echo -n "respawns seen : "; grep -a "respawn\] replicant" eg_b.log | grep -ac "${AID:-@@@}"
|
|
||||||
echo
|
|
||||||
echo "--- B: ghost detector ---"
|
|
||||||
grep -a "\[ghost\]" eg_b.log | head -5
|
|
||||||
echo -n "ghost lines: "; grep -ac "\[ghost\]" eg_b.log
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# =========================================================================
|
|
||||||
# EJECT + SEARCHLIGHT (#108, night 13) -- the operator's observation was
|
|
||||||
# that the ghost mechs all had their SEARCHLIGHTS ON. A plain panic eject
|
|
||||||
# respawns fine (ejectghost.sh), so the searchlight is the variable.
|
|
||||||
#
|
|
||||||
# WHY IT IS PLAUSIBLE, mechanically: the searchlight is the only thing that
|
|
||||||
# attaches EXTRA GEOMETRY into a peer's mech render tree -- btl4vid.cpp
|
|
||||||
# builds a spot.bgf cone as a DPLStaticChildRenderable parented to the
|
|
||||||
# lamp's mount-segment renderable. The death path then does a wreck swap
|
|
||||||
# ('victim -> thrdbr.bgf + debris') and respawn does 'rebuilt intact model
|
|
||||||
# (N segs restored, hulk dropped)'. A child renderable held across those
|
|
||||||
# two rebuilds is a lifetime hazard, and it would only bite mechs whose
|
|
||||||
# lamp was ON -- which is exactly the reported population.
|
|
||||||
#
|
|
||||||
# SEQUENCE on A: searchlight ON (0x14) -> confirm it lit -> panic eject
|
|
||||||
# (0x3D) -> respawn. B watches for the un-wreck.
|
|
||||||
# PASS : B logs "[respawn] replicant <A> un-wrecked + warp"
|
|
||||||
# FAIL : wreck with no un-wreck <- the ghost, and the searchlight is it
|
|
||||||
#
|
|
||||||
# NB 0x14 is the searchlight per the shipped bindings comment (left column
|
|
||||||
# 0x10-0x15 = map+/map-/IR/CROUCH/searchlight/display). The hardware name
|
|
||||||
# table calls it "Secondary5" -- the FUNCTION comes from the streamed .CTL
|
|
||||||
# rows, so the script VERIFIES the lamp actually lit before trusting the
|
|
||||||
# result; a silent B with a lamp that never came on proves nothing.
|
|
||||||
# =========================================================================
|
|
||||||
#
|
|
||||||
# MODE=light -- searchlight ON, then punch out
|
|
||||||
# MODE=nolight -- punch out with the lamp OFF (the A/B control)
|
|
||||||
#
|
|
||||||
# NB no BT_MP_FORCE_DMG here. ejectghost.sh had it, and that is why its
|
|
||||||
# panic presses did nothing: the "control killer" had A dead or mid-respawn
|
|
||||||
# for most of the run, and a punch-out press on a dead mech is a no-op.
|
|
||||||
# That run still produced its control result -- 8 force-kill deaths, 8 peer
|
|
||||||
# un-wrecks, so NORMAL death replication is proven good on this build.
|
|
||||||
set -x
|
|
||||||
MODE="${1:-light}"
|
|
||||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
|
||||||
cd /c/git/bt411/content || exit 1
|
|
||||||
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 2
|
|
||||||
rm -f el_${MODE}_a.log el_${MODE}_b.log el_${MODE}_relay.log
|
|
||||||
|
|
||||||
bt_expert_egg MP.EGG EL.EGG
|
|
||||||
# NIGHT map: the searchlight is a night system and the fog swap only has
|
|
||||||
# meaning there; also the field population was night-map rounds.
|
|
||||||
sed -i "s/^map=.*/map=cavern/; s/^time=.*/time=night/; s/^vehicle=.*/vehicle=madcat/" EL.EGG
|
|
||||||
|
|
||||||
( export BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1 BT_SPLASH_LOG=1
|
|
||||||
bt_launch el_${MODE}_b.log EL.EGG 0x0C -net 1601 )
|
|
||||||
sleep 2
|
|
||||||
( export BT_AUTODRIVE=0.6
|
|
||||||
if [ "$MODE" = "light" ]; then
|
|
||||||
export BT_BTNTEST=0x14,600,660 # SEARCHLIGHT on
|
|
||||||
export BT_BTNTEST2=0x3d,1500,1560 # then punch out
|
|
||||||
else
|
|
||||||
export BT_BTNTEST=0x3d,1500,1560 # punch out, lamp OFF
|
|
||||||
fi
|
|
||||||
export BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1
|
|
||||||
bt_launch el_${MODE}_a.log EL.EGG 0x03 -net 1501 )
|
|
||||||
sleep 5
|
|
||||||
python ../tools/btconsole.py EL.EGG 127.0.0.1:1501 127.0.0.1:1601 > el_${MODE}_relay.log 2>&1 &
|
|
||||||
RELAY=$!
|
|
||||||
sleep 300
|
|
||||||
kill $RELAY 2>/dev/null
|
|
||||||
sleep 3
|
|
||||||
bt_kill_ours
|
|
||||||
sleep 2
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 3
|
|
||||||
|
|
||||||
AID=$(grep -aoE "MY mech entityID=[0-9]+:[0-9]+" el_${MODE}_a.log | head -1 | cut -d= -f2)
|
|
||||||
echo "=================== EJECT+SEARCHLIGHT RESULT ==================="
|
|
||||||
echo "A's entity id: ${AID:-UNKNOWN}"
|
|
||||||
echo
|
|
||||||
echo "--- 1. did the scripted presses land? ---"
|
|
||||||
grep -a "btntest" el_${MODE}_a.log
|
|
||||||
echo
|
|
||||||
echo "--- 2. DID THE SEARCHLIGHT ACTUALLY COME ON? (if not, the run is void) ---"
|
|
||||||
grep -aiE "searchlight|\[spot\]|lightState" el_${MODE}_a.log | head -10
|
|
||||||
echo
|
|
||||||
echo "--- 3. A: punch-out + own respawn ---"
|
|
||||||
grep -aE "\[eject\]|PUNCH-OUT|dz\] GRANTED" el_${MODE}_a.log | head -10
|
|
||||||
echo
|
|
||||||
echo "--- 4. B: the peer's view of A (wreck / un-wreck, in order) ---"
|
|
||||||
grep -aE "wreck:|respawn\] replicant" el_${MODE}_b.log | grep -a "${AID:-@@@}" | head -20
|
|
||||||
echo -n "wrecks: "; grep -a "wreck:" el_${MODE}_b.log | grep -ac "${AID:-@@@}"
|
|
||||||
echo -n "respawns: "; grep -a "respawn\] replicant" el_${MODE}_b.log | grep -ac "${AID:-@@@}"
|
|
||||||
echo
|
|
||||||
echo "--- 5. B: searchlight cones + ghost detector ---"
|
|
||||||
grep -aE "\[spot\]" el_${MODE}_b.log | head -6
|
|
||||||
echo -n "cone skipped: "; grep -ac "cone skipped" el_${MODE}_b.log
|
|
||||||
echo -n "ghost lines : "; grep -ac "\[ghost\]" el_${MODE}_b.log
|
|
||||||
grep -a "\[ghost\]" el_${MODE}_b.log | head -3
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# =========================================================================
|
|
||||||
# A REAL PANIC EJECT -- at last. (#108 ghost + the chart's "-1000 ejecting")
|
|
||||||
#
|
|
||||||
# WHY SIX RIGS FAILED. It was never the trigger. BT_EJECT_AT reaches the
|
|
||||||
# dispatch every time (proved with an [ejecttest] receipt: "FIRING punch-out
|
|
||||||
# at frame 600/900/1200"), and the HANDLER refuses it:
|
|
||||||
#
|
|
||||||
# [eject] 1:139 REFUSED (mech not crippled enough)
|
|
||||||
#
|
|
||||||
# Mech::EjectPilotMessageHandler gates on EvaluateEjectPermission() (@0x414) --
|
|
||||||
# a healthy mech cannot punch out. Every bench so far ejected a pristine mech.
|
|
||||||
# In the field players eject BECAUSE they are wrecked, which is why it works
|
|
||||||
# for them and never for me. (The button seam was never broken either; that
|
|
||||||
# earlier conclusion was wrong too.)
|
|
||||||
#
|
|
||||||
# So: CRIPPLE FIRST, then eject. BT_SELF_DAMAGE grinds A down; BT_EJECT_AT
|
|
||||||
# retries every 300 frames, so the first retry after permission is granted
|
|
||||||
# takes it.
|
|
||||||
#
|
|
||||||
# WHAT THIS SETTLES:
|
|
||||||
# chart "-1000 ejecting" -- the eject total, currently arithmetic only
|
|
||||||
# #108 eject-ghost -- does the peer un-wreck after an EJECT death?
|
|
||||||
# (normal deaths replicate fine: 9 deaths -> 8
|
|
||||||
# un-wrecks, benched)
|
|
||||||
# =========================================================================
|
|
||||||
set -x
|
|
||||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
|
||||||
cd /c/git/bt411/content || exit 1
|
|
||||||
bt_assert_player_env
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 2
|
|
||||||
rm -f er_a.log er_b.log er_relay.log matchlog_*.txt
|
|
||||||
bt_expert_egg MP.EGG ER.EGG
|
|
||||||
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" ER.EGG
|
|
||||||
|
|
||||||
( export BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1
|
|
||||||
bt_launch er_b.log ER.EGG 0x0C -net 1601 )
|
|
||||||
sleep 2
|
|
||||||
# A: grind itself down, then punch out once permission is granted
|
|
||||||
# THE GATE, decoded from Mech::EvaluateEjectPermission (@0049fa1c):
|
|
||||||
# permitted = liveWeapons < ejectMinWeapons || liveGenerators == 0
|
|
||||||
# || coolantFrac < 0.05 || (leg-gimped && !simLive)
|
|
||||||
# Armour damage satisfies NONE of them -- which is why grinding A down gave
|
|
||||||
# 53 attempts and 785 refusals. Killing the GENERATORS is the direct lever,
|
|
||||||
# and BT_KILL_SUBSYS's comma-list form was built for precisely this bench.
|
|
||||||
# It fires at frame 900, so arm the eject after that.
|
|
||||||
( export BT_KILL_SUBSYS=GeneratorA,GeneratorB,GeneratorC,GeneratorD BT_EJECT_AT=1200
|
|
||||||
export BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1 BT_DMG_LOG=1
|
|
||||||
bt_launch er_a.log ER.EGG 0x03 -net 1501 )
|
|
||||||
sleep 5
|
|
||||||
python ../tools/btconsole.py ER.EGG 127.0.0.1:1501 127.0.0.1:1601 > er_relay.log 2>&1 &
|
|
||||||
RELAY=$!
|
|
||||||
sleep 300
|
|
||||||
kill $RELAY 2>/dev/null
|
|
||||||
sleep 3
|
|
||||||
bt_kill_ours
|
|
||||||
sleep 2
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 3
|
|
||||||
|
|
||||||
AID=$(grep -aoE "MY mech entityID=[0-9]+:[0-9]+" er_a.log | head -1 | cut -d= -f2)
|
|
||||||
echo "=================== REAL EJECT ==================="
|
|
||||||
echo "A entity: ${AID:-UNKNOWN}"
|
|
||||||
echo "--- 1. did a punch-out finally LAND? ---"
|
|
||||||
echo -n "FIRING attempts : "; grep -ac "FIRING punch-out" er_a.log
|
|
||||||
echo -n "REFUSED : "; grep -ac "REFUSED" er_a.log
|
|
||||||
echo -n "PUNCH-OUT : "; grep -ac "PUNCH-OUT" er_a.log
|
|
||||||
grep -aE "PUNCH-OUT|DeathWithoutHonor" er_a.log | head -3
|
|
||||||
echo
|
|
||||||
echo "--- 2. CHART '-1000 ejecting': the score trajectory around it ---"
|
|
||||||
grep -ah "player=2:1 type=" matchlog_*.txt | tail -6 | cut -c1-115
|
|
||||||
grep -a "\[deathcost\]" er_a.log | head -2 | cut -c1-140
|
|
||||||
echo
|
|
||||||
echo "--- 3. #108: does the peer UN-WRECK after an eject death? ---"
|
|
||||||
echo -n "B wreck-enters for A : "; grep -a "entered wreck state" er_b.log | grep -ac "${AID:-@@@}"
|
|
||||||
echo -n "B un-wrecks for A : "; grep -a "un-wrecked + warp" er_b.log | grep -ac "${AID:-@@@}"
|
|
||||||
grep -aE "entered wreck state|un-wrecked \+ warp" er_b.log | tail -6 | cut -c1-120
|
|
||||||
echo -n "B ghost lines : "; grep -ac "\[ghost\]" er_b.log
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import sys, re
|
|
||||||
sys.path.insert(0, r"C:\git\bt411\scratchpad\night7")
|
|
||||||
import gitea
|
|
||||||
|
|
||||||
rows = gitea.all_issues("all")
|
|
||||||
rows = [r for r in rows if not r.get("pull_request")]
|
|
||||||
print("TOTAL ISSUES: %d" % len(rows))
|
|
||||||
terms = {
|
|
||||||
"crouch": r"crouch",
|
|
||||||
"missile-dir": r"missile.*(direction|facing|foot|feet|track)|emitter",
|
|
||||||
"night-vis": r"night|darkness|visibilit|fog|thermal|infrared|\bIR\b|predator",
|
|
||||||
"smoke-all": r"smoke",
|
|
||||||
"layout-save": r"layout|glass_layout|\bsave\b",
|
|
||||||
"eject-splash": r"eject|panic|suicide|splash",
|
|
||||||
"torso-yaw": r"torso.*(twist|yaw)|feet.*fac",
|
|
||||||
}
|
|
||||||
for k, pat in terms.items():
|
|
||||||
print("\n=== %s ===" % k)
|
|
||||||
rx = re.compile(pat, re.I)
|
|
||||||
for r in rows:
|
|
||||||
if rx.search(r["title"]):
|
|
||||||
print(" #%-4s %-7s %s" % (r["number"], r["state"], r["title"][:110]))
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
"""Night 13: post the ghost/eject findings to #108 and settle #144. ASCII only."""
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0, r"C:\git\bt411\scratchpad\night7")
|
|
||||||
import gitea
|
|
||||||
|
|
||||||
gitea.comment(108, """Night 13 (2026-08-06, build **4.11.817**) -- eject-ghosts, and why the logs barely saw them.
|
|
||||||
|
|
||||||
## Field finding
|
|
||||||
|
|
||||||
Testers reported MANY ghost mechs, all from panic-button self-destructs, none respawning
|
|
||||||
properly -- and the operator has seen eject-respawn work every time before this night, so this is a
|
|
||||||
REGRESSION, not a gap (night 12 = build 774).
|
|
||||||
|
|
||||||
The logs show it happening, on three independent witnesses. Dave punch-ejected; on HIS machine the
|
|
||||||
whole death path ran (`DeathWithoutHonor`, `PUNCH-OUT`, death transition, wreck, explosion), he
|
|
||||||
respawned, took a drop-zone slot, and drove another 193 logged frames **in the same round**, which
|
|
||||||
then ended normally -- no crash, no disconnect. On BOTH peers his mech's history ends at the wreck
|
|
||||||
and never resumes; host 2 vanishes from santo's world for the remaining ~4300 lines of that round.
|
|
||||||
|
|
||||||
The discriminator is clean: the SAME mech's NORMAL deaths replicated their respawns 4-6 times in
|
|
||||||
that same session. Only the eject failed.
|
|
||||||
|
|
||||||
## Why the ghost detector said almost nothing
|
|
||||||
|
|
||||||
One `[ghost]` line all night, for an unrelated live mech. The gate is
|
|
||||||
|
|
||||||
&& !owner->WreckBuried() // "buried wrecks are expected-silent"
|
|
||||||
WreckBuried() { return collisionVolumeCount == 0; }
|
|
||||||
|
|
||||||
An eject-ghost IS a wreck that stopped receiving records, so the detector files it under
|
|
||||||
expected-silent. It can only ever catch a LIVE replicant going quiet -- structurally blind to this
|
|
||||||
failure.
|
|
||||||
|
|
||||||
## Why the census said "one" when testers saw many
|
|
||||||
|
|
||||||
Counting ghosts meant pairing the un-wreck receipt against
|
|
||||||
`[BTrender] wreck: 'thrdbr.bgf' missing -> gendbr.bgf fallback` -- a MISSING-ASSET warning, not a
|
|
||||||
death, which only prints for chassis whose wreck model is absent. Fixed in `4642129`: every
|
|
||||||
replicant entering the wreck state now emits an ungated
|
|
||||||
|
|
||||||
[wreck] replicant H:E entered wreck state (mode X->9) at (x,z)
|
|
||||||
|
|
||||||
symmetric with the existing un-wreck line, so the count is exactly (enters - un-wrecks) per entity.
|
|
||||||
Verified 2-node: 5 enters / 5 exits, exactly paired -- **while the old marker printed ZERO times in
|
|
||||||
the same run**. Five real deaths, invisible to what the census was reading. Treat the "one ghost on
|
|
||||||
night 13" number as a floor, not a count.
|
|
||||||
|
|
||||||
Census tooling: `scratchpad/night13/ghostcensus.py` (NB: filter the log owner's OWN mech -- an
|
|
||||||
owner's own wreck can never pair, since `un-wrecked` only logs for replicants; not doing so
|
|
||||||
manufactures false positives in every log).
|
|
||||||
|
|
||||||
## NOT reproduced by bench -- five rigs failed to trigger a punch-out at all
|
|
||||||
|
|
||||||
`BT_BTNTEST` never reached the mapper for the panic button (0x3D) or the searchlight (0x14): no
|
|
||||||
`[eject]`, no `PUNCH-OUT`, not even a lamp change -- so it is the SEAM, not the address.
|
|
||||||
`BT_EJECT_AT` (mech4.cpp:3310, the purpose-built hook) did not fire either. Benches are staged
|
|
||||||
(`ejectghost.sh`, `ejectlight.sh`, `ejectat.sh`) with their failure modes in the headers.
|
|
||||||
|
|
||||||
What the benches DID establish: **normal death replication is healthy on 817** -- 9 force-kill
|
|
||||||
deaths, 8 peer un-wrecks, 0 ghost lines. So an eject-only shortfall is the defect, not a broken rig.
|
|
||||||
|
|
||||||
## Open lead
|
|
||||||
|
|
||||||
The operator observed that the ghost mechs all had their SEARCHLIGHTS ON. Untested -- there is
|
|
||||||
currently no key or env that toggles the searchlight headlessly, so it needs a small bench hook.
|
|
||||||
Mechanically plausible: the searchlight is the only thing that attaches extra geometry into a peer's
|
|
||||||
mech render tree (btl4vid.cpp builds a `spot.bgf` cone as a `DPLStaticChildRenderable` parented to
|
|
||||||
the lamp's mount-segment renderable), and the death path does a wreck swap while respawn does
|
|
||||||
"rebuilt intact model (N segs restored, hulk dropped)" -- a child renderable held across those two
|
|
||||||
rebuilds is a lifetime hazard, and it would only bite mechs whose lamp was ON.
|
|
||||||
|
|
||||||
## Regression window
|
|
||||||
|
|
||||||
Commits 775-817. Two touch the death path: `91bd286` (787) rewrote the VehicleDead dispatch --
|
|
||||||
the message that drives the respawn cycle -- including the `killer == victim` eject case and the
|
|
||||||
filter that had blocked the panic-eject path; and `297127d` (784, death blast). Unverified.""")
|
|
||||||
|
|
||||||
gitea.close(144, """**NOT A BUG -- authentic. Closing.**
|
|
||||||
|
|
||||||
The eject sets `suppressConsole` (+0x258, `BTPlayerEjectBookkeeping`), and the #89 death-blast gate
|
|
||||||
reads it:
|
|
||||||
|
|
||||||
gates : owning player's advancedDamageOn (+0x264) AND
|
|
||||||
NOT suppressConsole (+0x258 -- eject sets it: punch-outs never blast)
|
|
||||||
[T1 @0x4a0aa8-0x4a0ad6]
|
|
||||||
|
|
||||||
So a mech that punches out does not splash its neighbourhood, by design, in the 1995 binary. SAURON's
|
|
||||||
observation ("splash worked on mechs that died close, but no splash on a panic / eject / suicide
|
|
||||||
death") is a correct reading of authentic behaviour.
|
|
||||||
|
|
||||||
Closing so nobody "fixes" it. Found while investigating the night-13 eject ghosts (#108).""")
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
"""Night 13: census every 'wreck with no following un-wreck' across all logs.
|
|
||||||
|
|
||||||
A peer's view of a mech death is a pair:
|
|
||||||
[BTrender] wreck: ... (entity=H:E) <- the wreck appears
|
|
||||||
[respawn] replicant H:E un-wrecked + warp <- it comes back
|
|
||||||
An UNPAIRED wreck -- one with no un-wreck before that log's session ends --
|
|
||||||
is the ghost signature. Report per SESSION so 'was it just one game?' is
|
|
||||||
answerable, and dump the lines immediately before the first one.
|
|
||||||
"""
|
|
||||||
import re, sys, os, io
|
|
||||||
|
|
||||||
LOGDIR = r"C:\git\bt411\scratchpad\night13"
|
|
||||||
SESS = re.compile(r"===== BT411 SESSION.*?local=(\S+ \S+)")
|
|
||||||
WRECK = re.compile(r"\[BTrender\] wreck:.*?entity=(\d+:\d+)")
|
|
||||||
UNWRECK = re.compile(r"\[respawn\] replicant (\d+:\d+) un-wrecked")
|
|
||||||
EJECT = re.compile(r"\[eject\]|PUNCH-OUT")
|
|
||||||
|
|
||||||
def scan(path):
|
|
||||||
sessions = [] # (startline, stamp)
|
|
||||||
events = [] # (line, kind, ent)
|
|
||||||
with io.open(path, "r", encoding="latin-1", errors="replace") as f:
|
|
||||||
for n, line in enumerate(f, 1):
|
|
||||||
m = SESS.search(line)
|
|
||||||
if m:
|
|
||||||
sessions.append((n, m.group(1)))
|
|
||||||
continue
|
|
||||||
m = WRECK.search(line)
|
|
||||||
if m:
|
|
||||||
events.append((n, "wreck", m.group(1))); continue
|
|
||||||
m = UNWRECK.search(line)
|
|
||||||
if m:
|
|
||||||
events.append((n, "unwreck", m.group(1))); continue
|
|
||||||
if EJECT.search(line):
|
|
||||||
events.append((n, "eject", "-"))
|
|
||||||
return sessions, events
|
|
||||||
|
|
||||||
def session_of(sessions, line):
|
|
||||||
idx, stamp = 0, "?"
|
|
||||||
for i, (sl, st) in enumerate(sessions):
|
|
||||||
if sl <= line:
|
|
||||||
idx, stamp = i + 1, st
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
return idx, stamp
|
|
||||||
|
|
||||||
for fn in sorted(os.listdir(LOGDIR)):
|
|
||||||
if not fn.endswith(".log") or fn.startswith("FAILURE"):
|
|
||||||
continue
|
|
||||||
path = os.path.join(LOGDIR, fn)
|
|
||||||
sessions, events = scan(path)
|
|
||||||
# pair wrecks to the next un-wreck of the same entity IN THE SAME SESSION
|
|
||||||
pending = {} # ent -> (line, sessidx)
|
|
||||||
unpaired = []
|
|
||||||
for (n, kind, ent) in events:
|
|
||||||
si = session_of(sessions, n)[0]
|
|
||||||
if kind == "wreck":
|
|
||||||
if ent in pending and pending[ent][1] == si:
|
|
||||||
unpaired.append(pending[ent]) # wreck superseded by another wreck
|
|
||||||
pending[ent] = (n, si)
|
|
||||||
elif kind == "unwreck":
|
|
||||||
if ent in pending and pending[ent][1] == si:
|
|
||||||
del pending[ent]
|
|
||||||
for ent, (n, si) in pending.items():
|
|
||||||
unpaired.append((n, si, ent))
|
|
||||||
norm = []
|
|
||||||
for u in unpaired:
|
|
||||||
norm.append(u if len(u) == 3 else (u[0], u[1], "?"))
|
|
||||||
norm.sort()
|
|
||||||
print("=" * 72)
|
|
||||||
print("%s sessions=%d wrecks=%d unwrecks=%d ejects=%d"
|
|
||||||
% (fn, len(sessions),
|
|
||||||
sum(1 for e in events if e[1] == "wreck"),
|
|
||||||
sum(1 for e in events if e[1] == "unwreck"),
|
|
||||||
sum(1 for e in events if e[1] == "eject")))
|
|
||||||
if not norm:
|
|
||||||
print(" no unpaired wrecks")
|
|
||||||
continue
|
|
||||||
bysess = {}
|
|
||||||
for (n, si, ent) in norm:
|
|
||||||
bysess.setdefault(si, []).append((n, ent))
|
|
||||||
for si in sorted(bysess):
|
|
||||||
stamp = sessions[si - 1][1] if 0 < si <= len(sessions) else "?"
|
|
||||||
print(" SESSION %d (%s): %d unpaired -> %s"
|
|
||||||
% (si, stamp, len(bysess[si]),
|
|
||||||
", ".join("%s@%d" % (e, n) for n, e in bysess[si][:6])))
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
"""Night 13 (2026-08-06, build 4.11.817) tracker housekeeping. ASCII only."""
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0, r"C:\git\bt411\scratchpad\night7")
|
|
||||||
import gitea
|
|
||||||
|
|
||||||
BUG, INV, WIP, HUD, AUDIO, NET, WORLD, AWAIT = 1, 2, 3, 4, 5, 6, 7, 8
|
|
||||||
SRC = ("Source: playtest night 13 (2026-08-06, build **4.11.817 (6fcff95+)**), "
|
|
||||||
"Discord #play-testing. Field logs staged in `scratchpad/night13/` "
|
|
||||||
"(4 players + the pod cart).\n\n")
|
|
||||||
new = {}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- NEW ISSUES
|
|
||||||
i = gitea.create(
|
|
||||||
"REGRESSION (817): BT_GLASS_LAYOUT=save writes a layout file with the MFD and Secondary lines MISSING",
|
|
||||||
SRC +
|
|
||||||
"SAURON (Michael), repeatedly: \"the save command is no longer writing to the glass_layout.cfg "
|
|
||||||
"file since this latest build\" ... \"got my glass panels set, saved and borders off again now\" "
|
|
||||||
"-> \"and then relaunched and it reset again\" -> \"looks like it rewrote the glass_layout.cfg "
|
|
||||||
"again\".\n\n"
|
|
||||||
"**The diagnostic detail:** \"the glass_layout.cfg had all the MFDs and secondary lines missing, "
|
|
||||||
"but plasma was still there\". So `save` is not failing to write -- it writes a file in which the "
|
|
||||||
"MFD and Secondary/Radar entries are simply absent, and the Plasma entry survives. On the next "
|
|
||||||
"launch there is nothing to restore, so every panel resets and the border/bare state is lost too.\n\n"
|
|
||||||
"**Workaround (confirmed by SAURON):** set `BT_GLASS_LAYOUT=load` and restore a hand-kept backup "
|
|
||||||
"copy of `glass_layout.cfg` -- \"set to load and used backup copy of CFG file and working aok so far\".\n\n"
|
|
||||||
"Probably the same root cause: vwe_propwash the same night -- \"my screen order borked again so "
|
|
||||||
"I'll need the trick to re-align them\".\n\n"
|
|
||||||
"**Suspect [T4, unverified]:** today's pod-MFD work is the only thing that touched this area. "
|
|
||||||
"Candidates, in order: (a) `e179c70` / `67a4f09` -- `BT_POD_RGB` and the bare-panel mode changed "
|
|
||||||
"how panel surfaces are enumerated and added the new `monitor:<name|index>` and `,bare` line "
|
|
||||||
"forms, so the WRITER may no longer emit a line for a surface it cannot express (or the panel "
|
|
||||||
"list it walks is now populated differently); (b) `d213c98` -- panel create/destroy moved out of "
|
|
||||||
"the PadRIO ctor into `LBE4ControlsManager`, which changes panel LIFETIME: if the save runs after "
|
|
||||||
"`BTGlassPanels_Destroy()`, the windows are already gone and there is nothing to serialize, which "
|
|
||||||
"would explain exactly this signature (Plasma is not a glass panel and is destroyed elsewhere, so "
|
|
||||||
"it alone survives).\n\n"
|
|
||||||
"Related: #76 (main cockpit + Plasma positions not restored) -- that is the older, much smaller "
|
|
||||||
"version of this; fix this one first, #76 may be a subset. Desktop glass mode only; the pod cart "
|
|
||||||
"is unaffected because it runs `BT_GLASS_LAYOUT=load` off a frozen master.",
|
|
||||||
labels=[BUG, HUD])
|
|
||||||
new['layoutsave'] = i['number']
|
|
||||||
|
|
||||||
i = gitea.create(
|
|
||||||
"Missiles launch along the LEG/FOOT facing, then curve to the target -- peer POV only",
|
|
||||||
SRC +
|
|
||||||
"Oracle: \"the missile thing we observed where missiles are firing in the direction the mech feet "
|
|
||||||
"are facing\" ... \"and then coming around to track the target\" ... \"**the emitter is following "
|
|
||||||
"the foot facing**\".\n\n"
|
|
||||||
"Confirmed by Oracle as **peer POV only** (epilectrik: \"only from peer pov though right\" -> "
|
|
||||||
"\"yes correct\"): from your own cockpit the launch looks right; on a REPLICANT the launch vector "
|
|
||||||
"comes off the leg/hip yaw instead of the torso/turret yaw. The homing itself works -- the missile "
|
|
||||||
"curves onto the target after launch -- so this is the muzzle/emitter TRANSFORM on the replicated "
|
|
||||||
"mech, not the guidance.\n\n"
|
|
||||||
"Reads as the replicant's weapon hardpoint being attached to (or composed against) the wrong node "
|
|
||||||
"in the segment hierarchy -- the leg/root segment rather than the twisted torso. Compare against "
|
|
||||||
"the master-side emitter, which is correct.\n\n"
|
|
||||||
"Related: #37 (MadCat torso is BACKWARDS), #70 (torso twist stops working after respawn) -- all "
|
|
||||||
"three are torso-yaw composition on a replicated model, and may share a cause.",
|
|
||||||
labels=[BUG, NET])
|
|
||||||
new['missiledir'] = i['number']
|
|
||||||
|
|
||||||
i = gitea.create(
|
|
||||||
"CROUCH does not toggle: the lamp flashes on every press but the mech stays STANDING",
|
|
||||||
SRC +
|
|
||||||
"Reported by Oracle and previously by SAURON. Oracle: \"the crouch button did not toggle to "
|
|
||||||
"display crouched\" ... \"no state change\" ... \"button flickers sometimes on press, **the light "
|
|
||||||
"next to it always flashes when pressed**, but state does not change. Remains in stand mode\".\n\n"
|
|
||||||
"So the INPUT is arriving and the annunciator responds -- the press is seen all the way to the "
|
|
||||||
"lamp -- but the crouch state itself never changes. That narrows it to the state transition / "
|
|
||||||
"gait request rather than the button wiring.\n\n"
|
|
||||||
"**Not to be confused with (and NOT a bug):** throttle-up does not stand you back up. Settled this "
|
|
||||||
"night by primary source -- Lynx, quoting the original manual: \"Throttle up should not make you "
|
|
||||||
"stand. Verified in manual. You must push the button again to stand. I agree that throttle up "
|
|
||||||
"'should' do that, but it's not designed that way.\" Standing requires a second CROUCH press, "
|
|
||||||
"which makes this bug worse than it looks: with no working toggle there is no designed way out of "
|
|
||||||
"crouch. Locomotion/CROUCH background: `context/locomotion.md`.",
|
|
||||||
labels=[BUG])
|
|
||||||
new['crouch'] = i['number']
|
|
||||||
|
|
||||||
i = gitea.create(
|
|
||||||
"One panel hit makes smoke erupt from MULTIPLE/ALL locations at once (seen on the sensor special panel)",
|
|
||||||
SRC +
|
|
||||||
"Oracle: \"the smoke sometimes emitting from multiple locations all at once when a location like "
|
|
||||||
"the sensor special panel is hit during testing. I saw this several times. It's not just with the "
|
|
||||||
"special panel, but it might have something to do with reaching a certain damage threshold which "
|
|
||||||
"is fairly easy to do quickly with a special panel. So you hit the special with an alpha and "
|
|
||||||
"suddenly the entire target mech emits smoke from all panels.\"\n\n"
|
|
||||||
"**Open question from the reporter, and the key discriminator:** \"What I'm not sure about is if "
|
|
||||||
"it's emitting only from previously damaged panels.\" If it is only previously-damaged panels, "
|
|
||||||
"this is a threshold that re-triggers the effect on every already-damaged zone at once (a "
|
|
||||||
"retrigger/latch bug). If it is genuinely ALL panels, the emitter is being attached per-mech "
|
|
||||||
"instead of per-zone. Answer that first -- it picks the fix.\n\n"
|
|
||||||
"Suspected trigger is a whole-mech damage THRESHOLD being crossed rather than the specific zone, "
|
|
||||||
"which a special-panel alpha reaches quickly.\n\n"
|
|
||||||
"Related: #90 (flames/smoke render less often and shorter than the original), #114 (missile impact "
|
|
||||||
"smoke too thick), #129 (closed: respawned mech emitted the wreck plume) -- same damage-effect "
|
|
||||||
"emitter family.",
|
|
||||||
labels=[BUG, INV])
|
|
||||||
new['smokeall'] = i['number']
|
|
||||||
|
|
||||||
i = gitea.create(
|
|
||||||
"No SPLASH damage from a PANIC/EJECT/suicide death (splash from normal deaths now confirmed working)",
|
|
||||||
SRC +
|
|
||||||
"SAURON: \"Splash damage looked good to me, splash worked on mechs that died close, **but no "
|
|
||||||
"splash on a panic / eject / suicide death**.\"\n\n"
|
|
||||||
"Filed as the leftover sub-case of #89, which is otherwise verified working this night and closed.\n\n"
|
|
||||||
"**Authenticity is NOT established** -- do not 'fix' this before deciding what the original did. A "
|
|
||||||
"panic eject is plausibly a different death path (the pilot leaves; whether the chassis still "
|
|
||||||
"detonates with an explosion payload is an open era question), so a self-destruct that does no "
|
|
||||||
"splash could be correct. Decide from the binary's eject/death path first: does the eject route "
|
|
||||||
"raise the same explosion object as a combat death, and does that object carry the splash payload?\n\n"
|
|
||||||
"Related: #89, #106 (closed: splash burstCount dropped), #118/#109 (EJECT/PANIC wiring), #134 "
|
|
||||||
"(closed: panic eject score penalty).",
|
|
||||||
labels=[INV])
|
|
||||||
new['ejectsplash'] = i['number']
|
|
||||||
|
|
||||||
i = gitea.create(
|
|
||||||
"ThermalSight / IR ('predator vision'): the VISIBLE half is unimplemented -- and it was never a heat image",
|
|
||||||
SRC +
|
|
||||||
"epilectrik: \"IR we solved but I think haven't implemented yet\" -- decode exists, presentation "
|
|
||||||
"does not, and there was no tracker item for the visible half (#61 covers the dead ToggleLamp "
|
|
||||||
"handler; #123 is the same shape for the searchlight).\n\n"
|
|
||||||
"**Primary-source description of what it should look like** -- Oracle, who played the original "
|
|
||||||
"pods at Lazer Park, this night:\n"
|
|
||||||
"- \"the IR was not what you would expect ... it was called predator vision, but it just looked "
|
|
||||||
"more like a **random palette shift**\"\n"
|
|
||||||
"- \"it was **not a heat image**\"\n"
|
|
||||||
"- \"which is why we generally called it LSD vision in Lazer Park\"\n"
|
|
||||||
"- \"IMHO it did nothing to improve visibility. All I found was that you had to look for some "
|
|
||||||
"movement and go after that\"\n"
|
|
||||||
"- epilectrik's recollection: it was a **test mode in the TH division hardware** that they "
|
|
||||||
"switched on, so it was not a specifically designed effect. Oracle: \"hardware optimized\".\n\n"
|
|
||||||
"**Why this matters for the port [T3]:** do not build a thermal/heat-gradient shader. The target "
|
|
||||||
"is a palette/colour-table transform on the existing image -- most likely whatever the 1995 "
|
|
||||||
"hardware path did when its test mode was enabled. Scope it from the decomp (and, if it really is "
|
|
||||||
"a hardware mode, decide what the honest modern equivalent is) before writing anything.\n\n"
|
|
||||||
"Related: #61 (ThermalSight ToggleLamp), #123 (searchlight visible half -- the same "
|
|
||||||
"toggle-works/presentation-deferred split).",
|
|
||||||
labels=[INV, WORLD])
|
|
||||||
new['ir'] = i['number']
|
|
||||||
|
|
||||||
print()
|
|
||||||
print("NEW:", new)
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
"""Night 13 housekeeping, part 2: comments + state changes on existing issues."""
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0, r"C:\git\bt411\scratchpad\night7")
|
|
||||||
import gitea
|
|
||||||
|
|
||||||
N13 = "Playtest night 13 (2026-08-06, build **4.11.817 (6fcff95+)**). "
|
|
||||||
|
|
||||||
# ---- #89 SPLASH: verified working, close -----------------------------------
|
|
||||||
gitea.close(89,
|
|
||||||
N13 + "**VERIFIED WORKING -- closing.**\n\n"
|
|
||||||
"SAURON: \"Splash damage looked good to me, **splash worked on mechs that died close**\". "
|
|
||||||
"epilectrik: \"splash damage seems ok?\" -- no dissent from any tester.\n\n"
|
|
||||||
"Oracle (original-pod player) also gave the era calibration that explains why splash reads as "
|
|
||||||
"inconsistent rather than absent, and says our current behaviour matches it: \"sometimes you "
|
|
||||||
"really catch it and sometimes not. It jogged my memory and I vividly recall that **being at "
|
|
||||||
"different elevations on a hillside sometimes made a real hash of it** -- the effect could be much "
|
|
||||||
"stronger or weaker\" ... \"it was **not** the mechwarrior 3/4 splash model\". SAURON: \"that seems "
|
|
||||||
"correct to me\". So an unpredictable, elevation-sensitive splash is AUTHENTIC and should not be "
|
|
||||||
"'fixed' into a clean radius falloff.\n\n"
|
|
||||||
"One sub-case survives and is filed separately as **#144**: no splash on a panic/eject/suicide "
|
|
||||||
"death (authenticity not yet established).")
|
|
||||||
|
|
||||||
# ---- #108 GHOST MECH: recurred ---------------------------------------------
|
|
||||||
gitea.comment(108,
|
|
||||||
N13 + "**Still present, and it was the headline problem of the night.** epilectrik: \"**ghost mech "
|
|
||||||
"biggest surprise**\" when reviewing regressions.\n\n"
|
|
||||||
"No new detail beyond the existing repro in this issue -- recording the recurrence on 817 so the "
|
|
||||||
"issue is not read as stale. Field logs for all four players (3 desktop + the pod cart) are staged "
|
|
||||||
"in `scratchpad/night13/`, including the pod's matchlogs, which is the first time this issue has "
|
|
||||||
"log coverage from a hardware seat as well. Analysis: `docs/GHOST_MECH_ANALYSIS.md`.")
|
|
||||||
|
|
||||||
# ---- #76 layout restore: superseded by #140 --------------------------------
|
|
||||||
gitea.comment(76,
|
|
||||||
N13 + "**Overtaken by #140.** As of 817 the problem is no longer 'two windows fail to restore' -- "
|
|
||||||
"`BT_GLASS_LAYOUT=save` now writes a `glass_layout.cfg` with the MFD and Secondary lines missing "
|
|
||||||
"entirely (Plasma survives), so nothing restores at all. See #140 for the reports and the suspect "
|
|
||||||
"commits.\n\n"
|
|
||||||
"Fix #140 first; this issue may turn out to be a subset of it. Re-verify the two windows named "
|
|
||||||
"here only once saving writes a complete file again.")
|
|
||||||
|
|
||||||
# ---- #123 searchlight / night visibility: era testimony --------------------
|
|
||||||
gitea.comment(123,
|
|
||||||
N13 + "**Primary-source testimony on night visibility** from Oracle, who played the original pods "
|
|
||||||
"at Lazer Park. This is directly about the deferred half of this issue (the fog swap), and it "
|
|
||||||
"raises the priority of the fog over the beam:\n\n"
|
|
||||||
"- \"some maps were not very dark, some were very dark\"\n"
|
|
||||||
"- \"**the FOG was what really killed visibility**\"\n"
|
|
||||||
"- \"I do recall basically stumbling into each other point blank at times and **having to use "
|
|
||||||
"radar to navigate**\"\n"
|
|
||||||
"- \"it could be very difficult to make a shot in those conditions\"\n"
|
|
||||||
"- \"it was 90's tech there\"\n\n"
|
|
||||||
"Takeaway for the implementation: the authentic night experience is driven by FOG DENSITY, not by "
|
|
||||||
"ambient darkness, and it was severe enough that radar navigation was the norm. The "
|
|
||||||
"`searchlightfog=` / `nosearchlightfog=` environment swap described above is therefore the "
|
|
||||||
"high-value half of this work, not the projected beam.\n\n"
|
|
||||||
"Operator note the same night: general night-visibility tuning is **deferred** until the correct "
|
|
||||||
"behaviour is known -- epilectrik: \"the night time visibility I might defer for now since we "
|
|
||||||
"don't really know the correct behavior ... I'll try to retrieve it from the binary again but "
|
|
||||||
"might be a hardware side thing\". This comment is that missing calibration.")
|
|
||||||
|
|
||||||
# ---- #61 ThermalSight: point at the new visible-half issue -----------------
|
|
||||||
gitea.comment(61,
|
|
||||||
N13 + "Cross-ref: the **visible** half of ThermalSight now has its own issue, **#145**, with "
|
|
||||||
"primary-source description of what the IR mode actually looked like (a palette shift, explicitly "
|
|
||||||
"NOT a heat image -- possibly a hardware test mode rather than a designed effect). Worth reading "
|
|
||||||
"before anyone implements a presentation off the toggle this issue restored.")
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# =========================================================================
|
|
||||||
# #140 -- reach the teardown the way the FIELD does: a MISSION that ENDS.
|
|
||||||
#
|
|
||||||
# Three earlier attempts never exercised BTGlassPanels_Destroy at all:
|
|
||||||
# bt_kill_ours uses `taskkill /F` (no dtors), and a graceful WM_CLOSE on a
|
|
||||||
# solo -egg run did not reach it either. The path that matters is the one
|
|
||||||
# testers hit constantly -- the controls manager is destroyed and rebuilt at
|
|
||||||
# every ROUND boundary, which is exactly what the relay drives in MP.
|
|
||||||
#
|
|
||||||
# Panels ON for both nodes; the relay starts a round and the round ends.
|
|
||||||
# The receipt to read is `[glasswin] destroy entry #N windows=M`:
|
|
||||||
# #1 windows=7 then #2 windows=0 -> the double-destroy, confirmed
|
|
||||||
# and then exactly ONE "saved N window position(s)" line, with the cfg intact.
|
|
||||||
# =========================================================================
|
|
||||||
set -x
|
|
||||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
|
||||||
cd /c/git/bt411/content || exit 1
|
|
||||||
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 2
|
|
||||||
rm -f glass_layout.cfg lr_a.log lr_b.log lr_relay.log
|
|
||||||
|
|
||||||
bt_expert_egg MP.EGG LR.EGG
|
|
||||||
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" LR.EGG
|
|
||||||
|
|
||||||
( export BT_GLASS_PANELS=1 BT_GLASS_LAYOUT=save BT_MP_LOG=1
|
|
||||||
bt_launch lr_b.log LR.EGG 0x0C -net 1601 )
|
|
||||||
sleep 2
|
|
||||||
( export BT_GLASS_PANELS=1 BT_GLASS_LAYOUT=save BT_MP_LOG=1
|
|
||||||
bt_launch lr_a.log LR.EGG 0x03 -net 1501 )
|
|
||||||
sleep 5
|
|
||||||
python ../tools/btconsole.py LR.EGG 127.0.0.1:1501 127.0.0.1:1601 > lr_relay.log 2>&1 &
|
|
||||||
RELAY=$!
|
|
||||||
sleep 180
|
|
||||||
kill $RELAY 2>/dev/null
|
|
||||||
sleep 5
|
|
||||||
bt_kill_ours
|
|
||||||
sleep 2
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 3
|
|
||||||
|
|
||||||
echo "=================== #140 TEARDOWN RECEIPTS ==================="
|
|
||||||
for n in a b; do
|
|
||||||
echo "--- node $n: destroy entries ---"
|
|
||||||
grep -a "destroy entry" lr_${n}.log
|
|
||||||
echo "--- node $n: SaveLayout receipts ---"
|
|
||||||
grep -a "window position" lr_${n}.log
|
|
||||||
done
|
|
||||||
echo
|
|
||||||
echo "--- glass_layout.cfg entries ---"
|
|
||||||
grep -aE "^[^#]+=" glass_layout.cfg 2>/dev/null || echo "(no file)"
|
|
||||||
echo -n "entry count: "; grep -acE "^[^#]+=" glass_layout.cfg 2>/dev/null
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# =========================================================================
|
|
||||||
# #140 glass_layout.cfg save regression -- the FIELD composition.
|
|
||||||
#
|
|
||||||
# SAURON's report: "set my glass panels, saved and borders off ... relaunched
|
|
||||||
# and it reset again ... the glass_layout.cfg had all the MFDs and secondary
|
|
||||||
# lines missing, but plasma was still there".
|
|
||||||
#
|
|
||||||
# So the acceptance test is the full ROUND TRIP a tester does, not just a
|
|
||||||
# single launch:
|
|
||||||
# run 1 -- BT_GLASS_LAYOUT=save, panels come up, quit cleanly
|
|
||||||
# check -- the cfg must list every MFD + the radar, not just plasma
|
|
||||||
# run 2 -- relaunch; the panels must come back where they were
|
|
||||||
#
|
|
||||||
# The teardown is what mattered: BTGlassPanels_Destroy has TWO callers on the
|
|
||||||
# desktop path (~PadRIO, then ~LBE4ControlsManager), and it ran SaveLayout
|
|
||||||
# BEFORE checking whether any windows were left -- so the second pass rewrote
|
|
||||||
# the file from an empty list.
|
|
||||||
# =========================================================================
|
|
||||||
set -x
|
|
||||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
|
||||||
cd /c/git/bt411/content || exit 1
|
|
||||||
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 2
|
|
||||||
rm -f glass_layout.cfg ls_run1.log ls_run2.log
|
|
||||||
|
|
||||||
# ---- run 1: create the layout ------------------------------------------
|
|
||||||
( export BT_GLASS_PANELS=1 BT_GLASS_LAYOUT=save
|
|
||||||
bt_launch ls_run1.log ARENA1.EGG 0x03 )
|
|
||||||
sleep 55
|
|
||||||
bt_kill_ours
|
|
||||||
sleep 2
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 3
|
|
||||||
|
|
||||||
echo "############ RUN 1 SAVE RESULT ############"
|
|
||||||
echo "--- every 'saved N window position(s)' line (the double-save shows here) ---"
|
|
||||||
grep -a "window position" ls_run1.log
|
|
||||||
echo
|
|
||||||
echo "--- glass_layout.cfg AFTER a clean quit ---"
|
|
||||||
if [ -f glass_layout.cfg ]; then cat glass_layout.cfg; else echo "!!! NO FILE WRITTEN"; fi
|
|
||||||
echo
|
|
||||||
echo "--- line census (comments excluded) ---"
|
|
||||||
echo -n "total entries : "; grep -acE "^[^#]+=" glass_layout.cfg 2>/dev/null
|
|
||||||
echo -n "MFD lines : "; grep -acE "^(Heat|Comm|Mfd|Eng|Weap|Sec)" glass_layout.cfg 2>/dev/null
|
|
||||||
echo -n "plasma line : "; grep -ac "Plasma" glass_layout.cfg 2>/dev/null
|
|
||||||
cp glass_layout.cfg /tmp/ls_after_run1.cfg 2>/dev/null
|
|
||||||
|
|
||||||
# ---- run 2: does it RESTORE? -------------------------------------------
|
|
||||||
( export BT_GLASS_PANELS=1 BT_GLASS_LAYOUT=save
|
|
||||||
bt_launch ls_run2.log ARENA1.EGG 0x03 )
|
|
||||||
sleep 55
|
|
||||||
bt_kill_ours
|
|
||||||
sleep 2
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 3
|
|
||||||
|
|
||||||
echo "############ RUN 2 (RELAUNCH) ############"
|
|
||||||
echo "--- restore receipts ---"
|
|
||||||
grep -aE "restored|glass_layout|window position" ls_run2.log | head -12
|
|
||||||
echo
|
|
||||||
echo "--- cfg after run 2 -- must still hold every line ---"
|
|
||||||
cat glass_layout.cfg 2>/dev/null
|
|
||||||
echo
|
|
||||||
echo "--- DIFF run1 -> run2 (empty = layout survived the round trip) ---"
|
|
||||||
diff /tmp/ls_after_run1.cfg glass_layout.cfg && echo "IDENTICAL"
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# =========================================================================
|
|
||||||
# #140 -- exercise the TEARDOWN path, which is where the bug lives.
|
|
||||||
#
|
|
||||||
# WHY THIS EXISTS: bench_common's bt_kill_ours uses `taskkill /F`. That is a
|
|
||||||
# HARD kill -- no destructors, so ~LBE4ControlsManager / ~PadRIO never run and
|
|
||||||
# BTGlassPanels_Destroy (the function that saves, and the one that ran twice)
|
|
||||||
# is never reached. Both of the first attempts died before teardown and the
|
|
||||||
# result looked like a difference between builds when it was really a
|
|
||||||
# difference in how far each run got. A graceful `taskkill` (NO /F) posts
|
|
||||||
# WM_CLOSE and lets the dtor chain run.
|
|
||||||
#
|
|
||||||
# Pass a label; run it once on the pre-fix build and once on the fixed one.
|
|
||||||
# =========================================================================
|
|
||||||
LABEL="${1:-run}"
|
|
||||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
|
||||||
cd /c/git/bt411/content || exit 1
|
|
||||||
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 2
|
|
||||||
rm -f glass_layout.cfg lt_${LABEL}.log
|
|
||||||
|
|
||||||
( export BT_GLASS_PANELS=1 BT_GLASS_LAYOUT=save
|
|
||||||
bt_launch lt_${LABEL}.log ARENA1.EGG 0x03 )
|
|
||||||
|
|
||||||
# wait for the panels to actually exist before asking for a shutdown
|
|
||||||
for i in $(seq 1 60); do
|
|
||||||
grep -aq "per-display cockpit up" lt_${LABEL}.log 2>/dev/null && break
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
grep -aq "per-display cockpit up" lt_${LABEL}.log || { echo "PANELS NEVER CAME UP"; }
|
|
||||||
sleep 20 # let it settle into the mission
|
|
||||||
|
|
||||||
PID=$(cat "$BT_PIDFILE" 2>/dev/null | head -1)
|
|
||||||
echo "graceful close of pid $PID"
|
|
||||||
taskkill //PID "$PID" > /dev/null 2>&1 # NO /F -- WM_CLOSE, dtors run
|
|
||||||
for i in $(seq 1 30); do
|
|
||||||
tasklist //FI "PID eq $PID" 2>/dev/null | grep -q btl4 || break
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1 # backstop
|
|
||||||
rm -f "$BT_PIDFILE"
|
|
||||||
sleep 2
|
|
||||||
|
|
||||||
echo "=============== $LABEL ==============="
|
|
||||||
echo "--- every SaveLayout receipt (TWO lines = the double-save) ---"
|
|
||||||
grep -a "window position" lt_${LABEL}.log
|
|
||||||
echo
|
|
||||||
echo "--- glass_layout.cfg entries after teardown ---"
|
|
||||||
grep -aE "^[^#]+=" glass_layout.cfg 2>/dev/null || echo "(no file)"
|
|
||||||
echo
|
|
||||||
echo -n "entry count: "; grep -acE "^[^#]+=" glass_layout.cfg 2>/dev/null || echo 0
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import sys
|
|
||||||
sys.path.insert(0, r"C:\git\bt411\scratchpad\night7")
|
|
||||||
import gitea
|
|
||||||
|
|
||||||
labels = gitea.call("/labels")
|
|
||||||
print("LABELS:", ", ".join("%s=%s" % (l["id"], l["name"]) for l in labels))
|
|
||||||
print()
|
|
||||||
rows = [r for r in gitea.all_issues("open") if "pull_request" not in r or not r.get("pull_request")]
|
|
||||||
rows.sort(key=lambda r: r["number"])
|
|
||||||
print("OPEN ISSUES: %d" % len(rows))
|
|
||||||
for r in rows:
|
|
||||||
lab = ",".join(l["name"] for l in r.get("labels", []))
|
|
||||||
print("#%-4s [%-22s] %s" % (r["number"], lab, r["title"][:120]))
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# =========================================================================
|
|
||||||
# #141 -- "missiles launch along the LEG/FOOT facing, then curve to the
|
|
||||||
# target -- PEER POV ONLY" (Oracle, night 13: "the emitter is following the
|
|
||||||
# foot facing"; shooter's own view correct).
|
|
||||||
#
|
|
||||||
# WHAT THE CODE SAYS SO FAR. Both sides already pass the mount segment:
|
|
||||||
# MissileLauncher::FireWeapon (mislanch.cpp:363) and the REPLICANT salvo
|
|
||||||
# mirror (mislanch.cpp:478) each hand BTPushProjectile
|
|
||||||
# `GetSegmentIndex() /*task #67 mount frame*/`. BTPushProjectile then
|
|
||||||
# rotates the authored MuzzleVelocity through
|
|
||||||
# `seg->GetSegmentToEntity() * localToWorld` -- so the launch direction IS
|
|
||||||
# the segment's world frame on BOTH nodes. Task #67 fixed exactly this
|
|
||||||
# symptom once already, master-side ("missiles fire out of his back").
|
|
||||||
#
|
|
||||||
# So if the peer report is real, the difference is NOT which frame is asked
|
|
||||||
# for -- it is whether the replicant's SEGMENT actually carries the torso
|
|
||||||
# twist. Torso pushes currentTwist into the skeleton on both paths
|
|
||||||
# (TorsoSimulation and TorsoCopySimulation both call UpdateJoints), so this
|
|
||||||
# has to be measured, not reasoned about.
|
|
||||||
#
|
|
||||||
# THE MEASUREMENT. New [launchframe] receipt (BT_PROJ_LOG) prints, on both
|
|
||||||
# nodes, the yaw of the launch forward vs the BODY forward:
|
|
||||||
# [launchframe] master seg=N segResolved=1 segYaw=.. bodyYaw=.. twistDelta=..
|
|
||||||
# [launchframe] REPLICANT seg=N segResolved=1 segYaw=.. bodyYaw=.. twistDelta=..
|
|
||||||
# twistDelta is the torso twist expressed in the launch direction.
|
|
||||||
#
|
|
||||||
# BUG CONFIRMED: A (master) shows |twistDelta| sweeping well away from 0
|
|
||||||
# while B (replicant mirror) stays pinned near 0
|
|
||||||
# -- or B shows segResolved=0 (fell back to the body basis).
|
|
||||||
# NOT REPRODUCED: both sides show the same twistDelta spread.
|
|
||||||
#
|
|
||||||
# ⚠ READ THE PREFIX, NOT THE PERCENTAGE (learned the hard way, 2026-08-08).
|
|
||||||
# BT_AUTOFIRE starts shooting IMMEDIATELY, while the app is still in
|
|
||||||
# WaitingForLaunch/LaunchingMission -- and Entity::Execute (ENTITY.cpp:556,
|
|
||||||
# real engine source) only calls PerformAndWatch when the state is
|
|
||||||
# RunningMission/EndingMission or the entity IsPreRunnable(). A REPLICANT mech
|
|
||||||
# is not pre-runnable (Entity::DefaultFlags has no PreRunFlag; only Player /
|
|
||||||
# Director add it, and Mech::Reset sets it for a reset MASTER), so a peer mech
|
|
||||||
# does not tick at all until the round actually starts. Every salvo fired
|
|
||||||
# before that reads twistDelta=0 legitimately -- the peer has no twist yet.
|
|
||||||
# That is a BENCH artifact (nobody can fire pre-round in a real match), not a
|
|
||||||
# defect: it showed up as a clean leading run of zeros, e.g.
|
|
||||||
# ZZZZ...(60)...ZZZZXXXX...(105)...XXXX
|
|
||||||
# and the first X lands within a few lines of the RunningMission transition.
|
|
||||||
# So: judge this bench by whether the failures are a PREFIX (fine) or
|
|
||||||
# INTERLEAVED (real), never by the raw percentage.
|
|
||||||
#
|
|
||||||
# Only A fires and only A sweeps its torso, so every REPLICANT line in B's
|
|
||||||
# log is a mirror of an A salvo and the comparison is unambiguous.
|
|
||||||
# =========================================================================
|
|
||||||
set -x
|
|
||||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
|
||||||
cd /c/git/bt411/content || exit 1
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 2
|
|
||||||
rm -f mf_a.log mf_b.log mf_relay.log
|
|
||||||
bt_expert_egg MP.EGG MF.EGG
|
|
||||||
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" MF.EGG
|
|
||||||
|
|
||||||
# B: the OBSERVER. Drives at A so it stays in range, but does NOT fire and
|
|
||||||
# does NOT sweep -- so every [launchframe] REPLICANT line in mf_b.log is a
|
|
||||||
# mirror of one of A's salvos.
|
|
||||||
( export BT_GOTO=enemy BT_GOTO_STOP=150
|
|
||||||
export BT_PROJ_LOG=1 BT_MP_LOG=1
|
|
||||||
bt_launch mf_b.log MF.EGG 0x0C -net 1601 )
|
|
||||||
sleep 2
|
|
||||||
# A: the SHOOTER. Sweeps the torso hard so twistDelta is unmistakably
|
|
||||||
# non-zero at fire time, and autofires missiles at the designated enemy.
|
|
||||||
( export BT_GOTO=enemy BT_GOTO_STOP=150
|
|
||||||
export BT_AUTOFIRE=1 BT_AF_MISSILE=1 BT_AF_PERIOD=7
|
|
||||||
export BT_LOCK_SWEEP=0.35
|
|
||||||
export BT_PROJ_LOG=1 BT_TORSO_LOG=1 BT_MP_LOG=1
|
|
||||||
bt_launch mf_a.log MF.EGG 0x03 -net 1501 )
|
|
||||||
sleep 5
|
|
||||||
python ../tools/btconsole.py MF.EGG 127.0.0.1:1501 127.0.0.1:1601 > mf_relay.log 2>&1 &
|
|
||||||
RELAY=$!
|
|
||||||
sleep 300
|
|
||||||
kill $RELAY 2>/dev/null
|
|
||||||
sleep 3
|
|
||||||
bt_kill_ours; sleep 2; taskkill //F //IM btl4.exe > /dev/null 2>&1; sleep 3
|
|
||||||
|
|
||||||
echo "=================== #141 MISSILE LAUNCH FRAME ==================="
|
|
||||||
echo "--- did A fire, and did B mirror? ---"
|
|
||||||
echo -n " A [launchframe] master lines ....... "; grep -ac "launchframe\] master" mf_a.log
|
|
||||||
echo -n " B [launchframe] REPLICANT lines .... "; grep -ac "launchframe\] REPLICANT" mf_b.log
|
|
||||||
echo
|
|
||||||
echo "--- did the segment RESOLVE on each side? (segResolved=0 would be the bug) ---"
|
|
||||||
echo -n " A segResolved=0 ... "; grep -a "launchframe\] master" mf_a.log | grep -ac "segResolved=0"
|
|
||||||
echo -n " B segResolved=0 ... "; grep -a "launchframe\] REPLICANT" mf_b.log | grep -ac "segResolved=0"
|
|
||||||
echo
|
|
||||||
echo "--- THE COMPARISON: twistDelta spread on each side ---"
|
|
||||||
python - <<'PY'
|
|
||||||
import re, io
|
|
||||||
def stats(path, tag):
|
|
||||||
v = []
|
|
||||||
try:
|
|
||||||
for ln in io.open(path, encoding="latin-1", errors="replace"):
|
|
||||||
if "[launchframe] " + tag in ln:
|
|
||||||
m = re.search(r"twistDelta=([-\d.e+]+)", ln)
|
|
||||||
if m:
|
|
||||||
try: v.append(float(m.group(1)))
|
|
||||||
except ValueError: pass
|
|
||||||
except IOError:
|
|
||||||
print(" %s: no log" % tag); return
|
|
||||||
if not v:
|
|
||||||
print(" %-9s no samples" % tag); return
|
|
||||||
a = [abs(x) for x in v]
|
|
||||||
big = sum(1 for x in a if x > 0.10) # ~5.7 deg -- clearly twisted
|
|
||||||
print(" %-9s n=%-4d |twistDelta| max=%.4f mean=%.4f >0.10rad: %d (%.0f%%)"
|
|
||||||
% (tag, len(v), max(a), sum(a)/len(a), big, 100.0*big/len(a)))
|
|
||||||
stats(r"C:\git\bt411\content\mf_a.log", "master")
|
|
||||||
stats(r"C:\git\bt411\content\mf_b.log", "REPLICANT")
|
|
||||||
print()
|
|
||||||
print(" VERDICT: master twisted + REPLICANT pinned near 0 => #141 CONFIRMED.")
|
|
||||||
print(" both twisted alike => NOT reproduced.")
|
|
||||||
PY
|
|
||||||
echo
|
|
||||||
echo "--- sample lines, both sides ---"
|
|
||||||
grep -a "launchframe\] master" mf_a.log | head -4
|
|
||||||
grep -a "launchframe\] REPLICANT" mf_b.log | head -4
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# =========================================================================
|
|
||||||
# #141 -- "missiles launch along the LEG/FOOT facing, then curve to the
|
|
||||||
# target -- PEER POV ONLY" (Oracle, night 13: "the emitter is following the
|
|
||||||
# foot facing"; shooter's own view correct).
|
|
||||||
#
|
|
||||||
# WHAT THE CODE SAYS SO FAR. Both sides already pass the mount segment:
|
|
||||||
# MissileLauncher::FireWeapon (mislanch.cpp:363) and the REPLICANT salvo
|
|
||||||
# mirror (mislanch.cpp:478) each hand BTPushProjectile
|
|
||||||
# `GetSegmentIndex() /*task #67 mount frame*/`. BTPushProjectile then
|
|
||||||
# rotates the authored MuzzleVelocity through
|
|
||||||
# `seg->GetSegmentToEntity() * localToWorld` -- so the launch direction IS
|
|
||||||
# the segment's world frame on BOTH nodes. Task #67 fixed exactly this
|
|
||||||
# symptom once already, master-side ("missiles fire out of his back").
|
|
||||||
#
|
|
||||||
# So if the peer report is real, the difference is NOT which frame is asked
|
|
||||||
# for -- it is whether the replicant's SEGMENT actually carries the torso
|
|
||||||
# twist. Torso pushes currentTwist into the skeleton on both paths
|
|
||||||
# (TorsoSimulation and TorsoCopySimulation both call UpdateJoints), so this
|
|
||||||
# has to be measured, not reasoned about.
|
|
||||||
#
|
|
||||||
# THE MEASUREMENT. New [launchframe] receipt (BT_PROJ_LOG) prints, on both
|
|
||||||
# nodes, the yaw of the launch forward vs the BODY forward:
|
|
||||||
# [launchframe] master seg=N segResolved=1 segYaw=.. bodyYaw=.. twistDelta=..
|
|
||||||
# [launchframe] REPLICANT seg=N segResolved=1 segYaw=.. bodyYaw=.. twistDelta=..
|
|
||||||
# twistDelta is the torso twist expressed in the launch direction.
|
|
||||||
#
|
|
||||||
# BUG CONFIRMED: A (master) shows |twistDelta| sweeping well away from 0
|
|
||||||
# while B (replicant mirror) stays pinned near 0
|
|
||||||
# -- or B shows segResolved=0 (fell back to the body basis).
|
|
||||||
# NOT REPRODUCED: both sides show the same twistDelta spread.
|
|
||||||
#
|
|
||||||
# Only A fires and only A sweeps its torso, so every REPLICANT line in B's
|
|
||||||
# log is a mirror of an A salvo and the comparison is unambiguous.
|
|
||||||
# =========================================================================
|
|
||||||
set -x
|
|
||||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
|
||||||
cd /c/git/bt411/content || exit 1
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 2
|
|
||||||
rm -f mf_a.log mf_b.log mf_relay.log
|
|
||||||
bt_expert_egg MP.EGG MF.EGG
|
|
||||||
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" MF.EGG
|
|
||||||
|
|
||||||
# B: the OBSERVER. Drives at A so it stays in range, but does NOT fire and
|
|
||||||
# does NOT sweep -- so every [launchframe] REPLICANT line in mf_b.log is a
|
|
||||||
# mirror of one of A's salvos.
|
|
||||||
( export BT_GOTO=enemy BT_GOTO_STOP=150
|
|
||||||
export BT_PROJ_LOG=1 BT_MP_LOG=1 BT_TORSO_LOG=1
|
|
||||||
bt_launch mf_b.log MF.EGG 0x0C -net 1601 )
|
|
||||||
sleep 2
|
|
||||||
# A: the SHOOTER. Sweeps the torso hard so twistDelta is unmistakably
|
|
||||||
# non-zero at fire time, and autofires missiles at the designated enemy.
|
|
||||||
( export BT_GOTO=enemy BT_GOTO_STOP=150
|
|
||||||
export BT_AUTOFIRE=1 BT_AF_MISSILE=1 BT_AF_PERIOD=7
|
|
||||||
export BT_LOCK_SWEEP=0.35
|
|
||||||
export BT_PROJ_LOG=1 BT_TORSO_LOG=1 BT_MP_LOG=1
|
|
||||||
bt_launch mf_a.log MF.EGG 0x03 -net 1501 )
|
|
||||||
sleep 5
|
|
||||||
python ../tools/btconsole.py MF.EGG 127.0.0.1:1501 127.0.0.1:1601 > mf_relay.log 2>&1 &
|
|
||||||
RELAY=$!
|
|
||||||
sleep 170
|
|
||||||
kill $RELAY 2>/dev/null
|
|
||||||
sleep 3
|
|
||||||
bt_kill_ours; sleep 2; taskkill //F //IM btl4.exe > /dev/null 2>&1; sleep 3
|
|
||||||
|
|
||||||
echo "=================== #141 MISSILE LAUNCH FRAME ==================="
|
|
||||||
echo "--- did A fire, and did B mirror? ---"
|
|
||||||
echo -n " A [launchframe] master lines ....... "; grep -ac "launchframe\] master" mf_a.log
|
|
||||||
echo -n " B [launchframe] REPLICANT lines .... "; grep -ac "launchframe\] REPLICANT" mf_b.log
|
|
||||||
echo
|
|
||||||
echo "--- did the segment RESOLVE on each side? (segResolved=0 would be the bug) ---"
|
|
||||||
echo -n " A segResolved=0 ... "; grep -a "launchframe\] master" mf_a.log | grep -ac "segResolved=0"
|
|
||||||
echo -n " B segResolved=0 ... "; grep -a "launchframe\] REPLICANT" mf_b.log | grep -ac "segResolved=0"
|
|
||||||
echo
|
|
||||||
echo "--- THE COMPARISON: twistDelta spread on each side ---"
|
|
||||||
python - <<'PY'
|
|
||||||
import re, io
|
|
||||||
def stats(path, tag):
|
|
||||||
v = []
|
|
||||||
try:
|
|
||||||
for ln in io.open(path, encoding="latin-1", errors="replace"):
|
|
||||||
if "[launchframe] " + tag in ln:
|
|
||||||
m = re.search(r"twistDelta=([-\d.e+]+)", ln)
|
|
||||||
if m:
|
|
||||||
try: v.append(float(m.group(1)))
|
|
||||||
except ValueError: pass
|
|
||||||
except IOError:
|
|
||||||
print(" %s: no log" % tag); return
|
|
||||||
if not v:
|
|
||||||
print(" %-9s no samples" % tag); return
|
|
||||||
a = [abs(x) for x in v]
|
|
||||||
big = sum(1 for x in a if x > 0.10) # ~5.7 deg -- clearly twisted
|
|
||||||
print(" %-9s n=%-4d |twistDelta| max=%.4f mean=%.4f >0.10rad: %d (%.0f%%)"
|
|
||||||
% (tag, len(v), max(a), sum(a)/len(a), big, 100.0*big/len(a)))
|
|
||||||
stats(r"C:\git\bt411\content\mf_a.log", "master")
|
|
||||||
stats(r"C:\git\bt411\content\mf_b.log", "REPLICANT")
|
|
||||||
print()
|
|
||||||
print(" VERDICT: master twisted + REPLICANT pinned near 0 => #141 CONFIRMED.")
|
|
||||||
print(" both twisted alike => NOT reproduced.")
|
|
||||||
PY
|
|
||||||
echo
|
|
||||||
echo "--- sample lines, both sides ---"
|
|
||||||
grep -a "launchframe\] master" mf_a.log | head -4
|
|
||||||
grep -a "launchframe\] REPLICANT" mf_b.log | head -4
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# =========================================================================
|
|
||||||
# #141 -- "missiles launch along the LEG/FOOT facing, then curve to the
|
|
||||||
# target -- PEER POV ONLY" (Oracle, night 13: "the emitter is following the
|
|
||||||
# foot facing"; shooter's own view correct).
|
|
||||||
#
|
|
||||||
# WHAT THE CODE SAYS SO FAR. Both sides already pass the mount segment:
|
|
||||||
# MissileLauncher::FireWeapon (mislanch.cpp:363) and the REPLICANT salvo
|
|
||||||
# mirror (mislanch.cpp:478) each hand BTPushProjectile
|
|
||||||
# `GetSegmentIndex() /*task #67 mount frame*/`. BTPushProjectile then
|
|
||||||
# rotates the authored MuzzleVelocity through
|
|
||||||
# `seg->GetSegmentToEntity() * localToWorld` -- so the launch direction IS
|
|
||||||
# the segment's world frame on BOTH nodes. Task #67 fixed exactly this
|
|
||||||
# symptom once already, master-side ("missiles fire out of his back").
|
|
||||||
#
|
|
||||||
# So if the peer report is real, the difference is NOT which frame is asked
|
|
||||||
# for -- it is whether the replicant's SEGMENT actually carries the torso
|
|
||||||
# twist. Torso pushes currentTwist into the skeleton on both paths
|
|
||||||
# (TorsoSimulation and TorsoCopySimulation both call UpdateJoints), so this
|
|
||||||
# has to be measured, not reasoned about.
|
|
||||||
#
|
|
||||||
# THE MEASUREMENT. New [launchframe] receipt (BT_PROJ_LOG) prints, on both
|
|
||||||
# nodes, the yaw of the launch forward vs the BODY forward:
|
|
||||||
# [launchframe] master seg=N segResolved=1 segYaw=.. bodyYaw=.. twistDelta=..
|
|
||||||
# [launchframe] REPLICANT seg=N segResolved=1 segYaw=.. bodyYaw=.. twistDelta=..
|
|
||||||
# twistDelta is the torso twist expressed in the launch direction.
|
|
||||||
#
|
|
||||||
# BUG CONFIRMED: A (master) shows |twistDelta| sweeping well away from 0
|
|
||||||
# while B (replicant mirror) stays pinned near 0
|
|
||||||
# -- or B shows segResolved=0 (fell back to the body basis).
|
|
||||||
# NOT REPRODUCED: both sides show the same twistDelta spread.
|
|
||||||
#
|
|
||||||
# Only A fires and only A sweeps its torso, so every REPLICANT line in B's
|
|
||||||
# log is a mirror of an A salvo and the comparison is unambiguous.
|
|
||||||
# =========================================================================
|
|
||||||
set -x
|
|
||||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
|
||||||
cd /c/git/bt411/content || exit 1
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 2
|
|
||||||
rm -f mf_a.log mf_b.log mf_relay.log
|
|
||||||
bt_expert_egg MP.EGG MF.EGG
|
|
||||||
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" MF.EGG
|
|
||||||
|
|
||||||
# B: the OBSERVER. Drives at A so it stays in range, but does NOT fire and
|
|
||||||
# does NOT sweep -- so every [launchframe] REPLICANT line in mf_b.log is a
|
|
||||||
# mirror of one of A's salvos.
|
|
||||||
( export BT_GOTO=enemy BT_GOTO_STOP=150
|
|
||||||
export BT_PROJ_LOG=1 BT_MP_LOG=1 BT_TORSO_LOG=1 BT_NET_TRACE=1
|
|
||||||
bt_launch mf_b.log MF.EGG 0x0C -net 1601 )
|
|
||||||
sleep 2
|
|
||||||
# A: the SHOOTER. Sweeps the torso hard so twistDelta is unmistakably
|
|
||||||
# non-zero at fire time, and autofires missiles at the designated enemy.
|
|
||||||
( export BT_GOTO=enemy BT_GOTO_STOP=150
|
|
||||||
export BT_AUTOFIRE=1 BT_AF_MISSILE=1 BT_AF_PERIOD=7
|
|
||||||
export BT_LOCK_SWEEP=0.35
|
|
||||||
export BT_PROJ_LOG=1 BT_TORSO_LOG=1 BT_MP_LOG=1
|
|
||||||
bt_launch mf_a.log MF.EGG 0x03 -net 1501 )
|
|
||||||
sleep 5
|
|
||||||
python ../tools/btconsole.py MF.EGG 127.0.0.1:1501 127.0.0.1:1601 > mf_relay.log 2>&1 &
|
|
||||||
RELAY=$!
|
|
||||||
sleep 170
|
|
||||||
kill $RELAY 2>/dev/null
|
|
||||||
sleep 3
|
|
||||||
bt_kill_ours; sleep 2; taskkill //F //IM btl4.exe > /dev/null 2>&1; sleep 3
|
|
||||||
|
|
||||||
echo "=================== #141 MISSILE LAUNCH FRAME ==================="
|
|
||||||
echo "--- did A fire, and did B mirror? ---"
|
|
||||||
echo -n " A [launchframe] master lines ....... "; grep -ac "launchframe\] master" mf_a.log
|
|
||||||
echo -n " B [launchframe] REPLICANT lines .... "; grep -ac "launchframe\] REPLICANT" mf_b.log
|
|
||||||
echo
|
|
||||||
echo "--- did the segment RESOLVE on each side? (segResolved=0 would be the bug) ---"
|
|
||||||
echo -n " A segResolved=0 ... "; grep -a "launchframe\] master" mf_a.log | grep -ac "segResolved=0"
|
|
||||||
echo -n " B segResolved=0 ... "; grep -a "launchframe\] REPLICANT" mf_b.log | grep -ac "segResolved=0"
|
|
||||||
echo
|
|
||||||
echo "--- THE COMPARISON: twistDelta spread on each side ---"
|
|
||||||
python - <<'PY'
|
|
||||||
import re, io
|
|
||||||
def stats(path, tag):
|
|
||||||
v = []
|
|
||||||
try:
|
|
||||||
for ln in io.open(path, encoding="latin-1", errors="replace"):
|
|
||||||
if "[launchframe] " + tag in ln:
|
|
||||||
m = re.search(r"twistDelta=([-\d.e+]+)", ln)
|
|
||||||
if m:
|
|
||||||
try: v.append(float(m.group(1)))
|
|
||||||
except ValueError: pass
|
|
||||||
except IOError:
|
|
||||||
print(" %s: no log" % tag); return
|
|
||||||
if not v:
|
|
||||||
print(" %-9s no samples" % tag); return
|
|
||||||
a = [abs(x) for x in v]
|
|
||||||
big = sum(1 for x in a if x > 0.10) # ~5.7 deg -- clearly twisted
|
|
||||||
print(" %-9s n=%-4d |twistDelta| max=%.4f mean=%.4f >0.10rad: %d (%.0f%%)"
|
|
||||||
% (tag, len(v), max(a), sum(a)/len(a), big, 100.0*big/len(a)))
|
|
||||||
stats(r"C:\git\bt411\content\mf_a.log", "master")
|
|
||||||
stats(r"C:\git\bt411\content\mf_b.log", "REPLICANT")
|
|
||||||
print()
|
|
||||||
print(" VERDICT: master twisted + REPLICANT pinned near 0 => #141 CONFIRMED.")
|
|
||||||
print(" both twisted alike => NOT reproduced.")
|
|
||||||
PY
|
|
||||||
echo
|
|
||||||
echo "--- sample lines, both sides ---"
|
|
||||||
grep -a "launchframe\] master" mf_a.log | head -4
|
|
||||||
grep -a "launchframe\] REPLICANT" mf_b.log | head -4
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# =========================================================================
|
|
||||||
# Sauron: "toggled through advanced controls from standard to advanced and
|
|
||||||
# back to standard -- lost torso control."
|
|
||||||
#
|
|
||||||
# THE MECHANISM (@004afbe0, the binary's CycleControlModeMessageHandler):
|
|
||||||
# the mode cycles 0 Basic -> 1 Standard -> 2 Veteran -> WRAPS TO BASIC. So
|
|
||||||
# getting from Veteran/"advanced" back to Standard PASSES THROUGH BASIC, and
|
|
||||||
# the Basic arm re-centres the torso.
|
|
||||||
#
|
|
||||||
# The port set that re-centre with CommandRecenter() -> centerCommand (@0x208).
|
|
||||||
# That is the HELD-BUTTON cell: TorsoSimulation re-arms `recenterActive` from
|
|
||||||
# it EVERY frame it is non-zero and only the input path clears it -- and a mode
|
|
||||||
# switch has no button release to follow. One visit to Basic pinned it at 1
|
|
||||||
# forever. Digital twist commands are processed BEFORE the centerCommand
|
|
||||||
# block, so they were overridden as fast as they were applied = "lost torso
|
|
||||||
# control". The binary writes recenterActive (@0x274) instead: a ONE-SHOT that
|
|
||||||
# self-clears on settle and is cancelled by any twist input.
|
|
||||||
#
|
|
||||||
# THE MEASUREMENT. BT_MODECYCLE_EVERY=<n> cycles the control mode every n mapper ticks
|
|
||||||
# frames from that frame. BT_TORSO_LOG's gate probe now prints the two cells:
|
|
||||||
# [torso] ... ctrCmd=<centerCommand> recen=<recenterActive> vLim=(lo..hi)
|
|
||||||
#
|
|
||||||
# PASS: ctrCmd stays 0 across every cycle (the one-shot is used instead), and
|
|
||||||
# vLim SWAPS between the Basic pair and the assisted pair as the mode
|
|
||||||
# changes -- proving the elevation-limit swap (@0x228/@0x22C vs
|
|
||||||
# @0x230/@0x234) that the port previously never implemented.
|
|
||||||
# FAIL: ctrCmd latches to 1 after the first pass through Basic and never
|
|
||||||
# returns to 0 -> the torso re-centres forever.
|
|
||||||
#
|
|
||||||
# Single node: this is entirely local control state, no peer needed.
|
|
||||||
# =========================================================================
|
|
||||||
set -x
|
|
||||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
|
||||||
cd /c/git/bt411/content || exit 1
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 2
|
|
||||||
rm -f mc_a.log
|
|
||||||
bt_expert_egg MP.EGG MC.EGG
|
|
||||||
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" MC.EGG
|
|
||||||
|
|
||||||
( export BT_MODECYCLE_EVERY=400
|
|
||||||
export BT_TORSO_LOG=1 BT_KEY_NOFOCUS=1 BT_KEY_BRIDGE=0 BT_TWIST_PULSE=150 ${LEGACY:+BT_LEGACY_MODE_RECENTER=1}
|
|
||||||
bt_launch mc_a.log MC.EGG 0x03 )
|
|
||||||
sleep 150
|
|
||||||
bt_kill_ours; sleep 2; taskkill //F //IM btl4.exe > /dev/null 2>&1; sleep 3
|
|
||||||
|
|
||||||
echo "=================== CONTROL-MODE TORSO STATE ==================="
|
|
||||||
echo "--- the mode cycles that happened ---"
|
|
||||||
grep -a "\[mode\] control mode" mc_a.log | head -10
|
|
||||||
echo
|
|
||||||
echo "--- centerCommand must NEVER latch (ctrCmd=1 with no button = the bug) ---"
|
|
||||||
echo -n " samples with ctrCmd=1 : "; grep -ao "ctrCmd=[0-9]*" mc_a.log | grep -c "ctrCmd=1"
|
|
||||||
echo -n " samples with ctrCmd=0 : "; grep -ao "ctrCmd=[0-9]*" mc_a.log | grep -c "ctrCmd=0"
|
|
||||||
echo
|
|
||||||
echo "--- the elevation-limit SWAP (should differ between Basic and assisted) ---"
|
|
||||||
grep -ao "vLim=([^)]*)" mc_a.log | sort | uniq -c | sort -rn | head -5
|
|
||||||
echo
|
|
||||||
echo "--- torso state around each mode change ---"
|
|
||||||
grep -aE "\[mode\] control mode|ctrCmd=" mc_a.log | grep -aA1 "\[mode\]" | head -12
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
"""Night 13: rewrite #147's body (the inline attempt was mangled by the shell). ASCII only."""
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0, r"C:\git\bt411\scratchpad\night7")
|
|
||||||
import gitea
|
|
||||||
|
|
||||||
BODY = """**Reported by Oracle (2026-08-06 session):** "no range finder on this drop" + a screenshot.
|
|
||||||
Symptom confirmed by eye: **the ladder tick marks were present, the moving caret was not.**
|
|
||||||
Intermittent -- one drop, not every drop. He was the only tester who hit it.
|
|
||||||
|
|
||||||
## What it is NOT
|
|
||||||
|
|
||||||
Ruled out from the four field logs:
|
|
||||||
|
|
||||||
* **Not a Steam-host bug.** Oracle *was* hosting (`[lobby] host: lobby up` / `GO with N member(s)`
|
|
||||||
appears in his log and no other), but the host path is uninvolved: the range **computed** fine on
|
|
||||||
his node (1806 nonzero `[target] range=` samples) and the reticle **built** fine on every drop
|
|
||||||
(`[hud] reticle built: 7 weapon pip(s) registered` -- 6 drops, 6 builds).
|
|
||||||
* **Not the Thor chassis.** He flew a Thor, which is why the report reads as chassis-specific -- but
|
|
||||||
a second tester flew a Thor the same night *without* hosting and reported nothing, and the range
|
|
||||||
ladder is drawn by the shared `HudSimulation` / `BTReticleRenderable`, not per-chassis cockpit
|
|
||||||
content. No Thor asset-missing warnings in any log (only the known wreck-model fallbacks).
|
|
||||||
* **Not his destroyed HUD.** His HUD subsystem *was* destroyed twice -- the only tester to reach
|
|
||||||
`condition 0` all night -- but only for ~11 s and ~26 s, repaired by respawn each time. And a
|
|
||||||
destroyed HUD costs you the fire-control LOCK (own host zone >= 0.75 damage, `_DAT_004b7ec4`),
|
|
||||||
not the caret.
|
|
||||||
|
|
||||||
## The defect
|
|
||||||
|
|
||||||
`sShownRange` -- the value the caret ultimately binds to -- is a **function-level static** in
|
|
||||||
mech4.cpp's targeting step. One cell for the whole process, shared by every mech, carried across
|
|
||||||
drops, never re-seeded. Its update:
|
|
||||||
|
|
||||||
```c
|
|
||||||
float step = trueRange - sShownRange;
|
|
||||||
if (step > maxStep) step = maxStep;
|
|
||||||
if (step < -maxStep) step = -maxStep;
|
|
||||||
sShownRange += step;
|
|
||||||
```
|
|
||||||
|
|
||||||
**NaN is absorbing here, and the clamps cannot catch it** -- `step > maxStep` and `step < -maxStep`
|
|
||||||
are BOTH false for NaN. So a single poisoned frame makes `sShownRange` NaN and it stays NaN *for
|
|
||||||
the life of the process*.
|
|
||||||
|
|
||||||
The consumer repeats the same mistake -- `BTReticleRenderable::Draw`:
|
|
||||||
|
|
||||||
```c
|
|
||||||
Scalar range = (rangeAttr2 != 0) ? *rangeAttr2 : 0.0f;
|
|
||||||
if (range < minRange) range = minRange; // false for NaN
|
|
||||||
if (range > maxRange) range = maxRange; // false for NaN
|
|
||||||
Scalar frac = (range - minRange) / (maxRange - minRange);
|
|
||||||
```
|
|
||||||
|
|
||||||
NaN flows into `AddPoint` / `ConcatMatrix`, so the caret and its bar become **degenerate geometry
|
|
||||||
and stop rendering** -- while every static reticle element, tick marks included, still draws.
|
|
||||||
|
|
||||||
That is exactly the reported symptom: ticks present, caret gone, sticky until relaunch.
|
|
||||||
|
|
||||||
## Why no log could confirm it
|
|
||||||
|
|
||||||
**The caret's actual input had no diagnostic anywhere.** `BT_RANGE_LOG` instruments the *pick*
|
|
||||||
(#4), and the `range=` field in `[target]` is a separate, locally recomputed
|
|
||||||
`Sqrt(ddx*ddx + ddy*ddy + ddz*ddz)` inside the weapon-range check -- it is neither `sShownRange`
|
|
||||||
nor `gBTHudRangeStorage`. Grepping the field logs for NaN returns nothing because **the poisoned
|
|
||||||
variable was never printed.** Absence of the signal was not evidence of absence.
|
|
||||||
|
|
||||||
## Fixed (unreleased)
|
|
||||||
|
|
||||||
1. **Re-seed on mech change** -- a new drop starts at the binary's 1200 default instead of
|
|
||||||
inheriting the previous mission's slid value. Deliberately does NOT fire on respawn: that
|
|
||||||
reuses the entity, and the binary does not reset the readout on respawn either.
|
|
||||||
2. **NaN trap at the producer** -- re-seed to 1200 rather than propagate.
|
|
||||||
3. **NaN-safe clamp at the consumer** -- test `x == x` first, and fall back to the authentic
|
|
||||||
no-target peg (1200) instead of rendering nothing.
|
|
||||||
4. **`BT_RANGE_LOG` now prints the caret's real input** -- `[range] caret input shown=... true=...
|
|
||||||
lock=...` at 1 Hz, plus a `[range] NaN TRAPPED` receipt.
|
|
||||||
|
|
||||||
## Status [T3 -- honest]
|
|
||||||
|
|
||||||
The defect and the symptom match exactly, and the fix is correct on its own merits: a
|
|
||||||
process-lifetime static feeding unguarded float geometry is a bug regardless of who reported what.
|
|
||||||
|
|
||||||
But **the causal link to Oracle's report is INFERENCE, not proof.** The NaN source is unidentified
|
|
||||||
and the failure has not been reproduced. What would settle it: fly with `BT_RANGE_LOG=1` -- if the
|
|
||||||
caret dies again, the log now names the frame it happened on.
|
|
||||||
"""
|
|
||||||
|
|
||||||
gitea.call("/issues/147", method="PATCH", payload={"body": BODY})
|
|
||||||
print("rewrote #147 body")
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
"""Audit Mech::Reset (@0049fb74) -- every field the binary clears vs ours.
|
|
||||||
|
|
||||||
Motivated by finding EIGHT dropped posture clears (duckState, both state alarms,
|
|
||||||
the death/reset latches, idleStrideScale) in the port's Reset, and by #137
|
|
||||||
"respawn came back with MYOMERS heat MAXED" + Sauron's overheating generator D:
|
|
||||||
if the reconstruction dropped the posture clears it may have dropped others.
|
|
||||||
"""
|
|
||||||
import io, re
|
|
||||||
|
|
||||||
DECOMP = r"C:\git\bt411\reference\decomp\all\part_012.c"
|
|
||||||
HPP = r"C:\git\bt411\game\reconstructed\mech.hpp"
|
|
||||||
CPP = r"C:\git\bt411\game\reconstructed\mech4.cpp"
|
|
||||||
|
|
||||||
# --- 1. every offset the BINARY's Reset touches ---------------------------
|
|
||||||
lines = io.open(DECOMP, encoding="latin-1", errors="replace").readlines()
|
|
||||||
body = "".join(lines[14191:14400]) # FUN_0049fb74
|
|
||||||
bin_off = sorted({int(m, 16) for m in re.findall(r"param_1 \+ (0x[0-9a-f]+)", body)})
|
|
||||||
|
|
||||||
# --- 2. offset -> our field name, from mech.hpp's offset comments ---------
|
|
||||||
name_of = {}
|
|
||||||
for ln in io.open(HPP, encoding="latin-1", errors="replace"):
|
|
||||||
m = re.search(r"^\s*(?:[A-Za-z_][\w:<>* ]*?)\s+(\w+)\s*(?:\[\d+\])?\s*;.*?(?:@|//\s*)0x([0-9a-fA-F]+)", ln)
|
|
||||||
if m:
|
|
||||||
name_of.setdefault(int(m.group(2), 16), m.group(1))
|
|
||||||
|
|
||||||
# --- 3. what OUR Reset assigns -------------------------------------------
|
|
||||||
cpp = io.open(CPP, encoding="latin-1", errors="replace").read()
|
|
||||||
i = cpp.find("Mech::Reset(const Origin &origin, int mode)")
|
|
||||||
ours_body = cpp[i:i + 9000] if i != -1 else ""
|
|
||||||
ours = set(re.findall(r"^\s*(\w+)\s*(?:\.\w+\([^)]*\)|=)", ours_body, re.M))
|
|
||||||
|
|
||||||
print("Mech::Reset @0049fb74 -- %d distinct fields touched by the binary\n" % len(bin_off))
|
|
||||||
missing, covered, unknown = [], [], []
|
|
||||||
for off in bin_off:
|
|
||||||
nm = name_of.get(off)
|
|
||||||
if nm is None:
|
|
||||||
unknown.append(off)
|
|
||||||
elif nm in ours:
|
|
||||||
covered.append((off, nm))
|
|
||||||
else:
|
|
||||||
missing.append((off, nm))
|
|
||||||
|
|
||||||
print("COVERED by our Reset (%d):" % len(covered))
|
|
||||||
print(" " + ", ".join("%s@0x%x" % (n, o) for o, n in covered) + "\n")
|
|
||||||
print("*** NOT CLEARED by our Reset (%d) -- named fields the binary resets ***" % len(missing))
|
|
||||||
for o, n in missing:
|
|
||||||
print(" 0x%-5x %s" % (o, n))
|
|
||||||
print("\nUNMAPPED offsets (%d) -- no named field at that offset in mech.hpp:" % len(unknown))
|
|
||||||
print(" " + " ".join("0x%x" % o for o in unknown))
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# =========================================================================
|
|
||||||
# SELF-INFLICTED scoring + DEATHS counter (night 13).
|
|
||||||
#
|
|
||||||
# Two chart rows and one field complaint are still unverified:
|
|
||||||
# "-1 each self-inflicted point of armor damage" -- code-verified only
|
|
||||||
# (ScoreInflictedMessageHandler's negate-if-target-is-self arm); every
|
|
||||||
# previous run reported negative(self)=0 because nobody self-damaged.
|
|
||||||
# "-1000 destroying your own 'Mech by ejecting" -- arithmetic only
|
|
||||||
# (-500 death cost + a self-kill negating its own ~500 award). A
|
|
||||||
# SELF-DESTRUCT reaches the same two paths without needing the panic
|
|
||||||
# button, which five rigs failed to trigger.
|
|
||||||
# "kills/deaths counts were screwy" -- kills=1 is verified, deathTally is not.
|
|
||||||
#
|
|
||||||
# A self-destructs (BT_SELF_DAMAGE, inflictor = SELF); B stands off.
|
|
||||||
# Composition from night12/scoreself.sh, which was built for #134.
|
|
||||||
# =========================================================================
|
|
||||||
set -x
|
|
||||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
|
||||||
cd /c/git/bt411/content || exit 1
|
|
||||||
bt_assert_player_env
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 2
|
|
||||||
rm -f s2_a.log s2_b.log s2_relay.log matchlog_*.txt
|
|
||||||
bt_expert_egg MP.EGG S2.EGG
|
|
||||||
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" S2.EGG
|
|
||||||
|
|
||||||
( export BT_DMG_LOG=1 BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1
|
|
||||||
bt_launch s2_b.log S2.EGG 0x0C -net 1601 )
|
|
||||||
sleep 2
|
|
||||||
( export BT_SELF_DAMAGE=40
|
|
||||||
export BT_DMG_LOG=1 BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1 BT_SCORE_LOG=1
|
|
||||||
bt_launch s2_a.log S2.EGG 0x03 -net 1501 )
|
|
||||||
sleep 5
|
|
||||||
python ../tools/btconsole.py S2.EGG 127.0.0.1:1501 127.0.0.1:1601 > s2_relay.log 2>&1 &
|
|
||||||
RELAY=$!
|
|
||||||
sleep 280
|
|
||||||
kill $RELAY 2>/dev/null
|
|
||||||
sleep 3
|
|
||||||
bt_kill_ours
|
|
||||||
sleep 2
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 3
|
|
||||||
|
|
||||||
echo "=================== SELF-SCORE + DEATHS ==================="
|
|
||||||
echo "--- 0. role bound? (if NULL every number below is meaningless) ---"
|
|
||||||
grep -a "\[role\] player" s2_a.log | head -3
|
|
||||||
echo
|
|
||||||
echo "--- 1. CHART '-1 each self-inflicted point': type=0 with award < 0 ---"
|
|
||||||
grep -ah "type=0 award=-" matchlog_*.txt | head -6 | cut -c1-115
|
|
||||||
echo -n "negative type=0 rows: "; grep -ahc "type=0 award=-" matchlog_*.txt | paste -sd+ | bc 2>/dev/null || grep -ah "type=0 award=-" matchlog_*.txt | wc -l
|
|
||||||
echo
|
|
||||||
echo "--- 2. CHART '-1000 ejecting' components: self-kill negation + death cost ---"
|
|
||||||
echo "self-kill (type=2, award must be NEGATIVE, kills must NOT increment):"
|
|
||||||
grep -ah "SCORE.*type=2" matchlog_*.txt | head -4 | cut -c1-115
|
|
||||||
echo "death cost (-specialCaseDeathPenalty, expect -500):"
|
|
||||||
grep -ah "type=1 award=-5" matchlog_*.txt | head -3 | cut -c1-115
|
|
||||||
echo
|
|
||||||
echo "--- 3. DEATHS counter (the other half of the field report) ---"
|
|
||||||
grep -ah "PLAYER_DEAD" matchlog_*.txt | head -5 | cut -c1-115
|
|
||||||
echo -n "death cycles on A: "; grep -ac "death cycle START" s2_a.log
|
|
||||||
echo
|
|
||||||
echo "--- 4. net score trajectory for the self-destructor ---"
|
|
||||||
grep -ah "player=2:1 type=" matchlog_*.txt | tail -4 | cut -c1-115
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# =========================================================================
|
|
||||||
# SCORING verify -- the type-0 interceptor (@004bffa0) restored.
|
|
||||||
#
|
|
||||||
# Composition lifted from night12/scorekill.sh, which is known to produce a
|
|
||||||
# clean cross-node kill: A (madcat, shooter) zone-walk-hammers B (loki,
|
|
||||||
# spinner) at 90u until B dies.
|
|
||||||
#
|
|
||||||
# THE A/B IS READABLE IN ONE RUN, because the old behaviour left a receipt:
|
|
||||||
# BEFORE every inflicted report hit ScoreMessageHandler's type-0 arm and
|
|
||||||
# tripped Verify "ScoreMessageHandler should not be given
|
|
||||||
# DamageInflictedScoreMessages!" -- night12's bench listed those
|
|
||||||
# Verify prints as an expected PASS signal.
|
|
||||||
# AFTER the interceptor routes type 0 to ScoreInflictedMessageHandler, so
|
|
||||||
# those Verify prints must be GONE and matchlog SCORE type=0 rows
|
|
||||||
# with non-zero awards must appear instead.
|
|
||||||
#
|
|
||||||
# Chart cross-check (original manual, via Lynx): "+1 each damage point scored
|
|
||||||
# on opponent's armor". The handler scales by tonnage ratio and the role's
|
|
||||||
# damageInflictedModifier, so award != damage exactly -- but it must TRACK
|
|
||||||
# damage, not sit at zero, and must be NEGATIVE for self-damage.
|
|
||||||
# =========================================================================
|
|
||||||
set -x
|
|
||||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
|
||||||
cd /c/git/bt411/content || exit 1
|
|
||||||
bt_assert_player_env
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 2
|
|
||||||
rm -f sv_a.log sv_b.log sv_relay.log matchlog_*.txt
|
|
||||||
bt_expert_egg MP.EGG SV.EGG
|
|
||||||
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/" SV.EGG
|
|
||||||
python - << 'EOF'
|
|
||||||
lines = open('SV.EGG').read().splitlines(True)
|
|
||||||
n = 0
|
|
||||||
for i, l in enumerate(lines):
|
|
||||||
if l.startswith('vehicle='):
|
|
||||||
n += 1
|
|
||||||
lines[i] = 'vehicle=madcat\n' if n == 1 else 'vehicle=loki\n'
|
|
||||||
open('SV.EGG', 'w').writelines(lines)
|
|
||||||
print('vehicles set:', n)
|
|
||||||
EOF
|
|
||||||
|
|
||||||
( export BT_DMG_LOG=1 BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1 BT_SCORE_LOG=1
|
|
||||||
bt_launch sv_b.log SV.EGG 0x0C -net 1601 )
|
|
||||||
sleep 2
|
|
||||||
( export BT_ZONE_WALK=8 BT_WALK_ZONES=dz_ldleg
|
|
||||||
export BT_GOTO=enemy BT_GOTO_STOP=90 BT_KEY_NOFOCUS=1
|
|
||||||
export BT_DMG_LOG=1 BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1 BT_SCORE_LOG=1
|
|
||||||
bt_launch sv_a.log SV.EGG 0x03 -net 1501 )
|
|
||||||
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 400
|
|
||||||
kill $RELAY 2>/dev/null
|
|
||||||
sleep 3
|
|
||||||
bt_kill_ours
|
|
||||||
sleep 2
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 3
|
|
||||||
|
|
||||||
echo "=================== SCORING VERIFY ==================="
|
|
||||||
echo "--- 0. did combat happen at all? (if 0 hits the run is VOID) ---"
|
|
||||||
echo -n "damage rows on victim B : "; grep -ac "dmghit\|DMG" sv_b.log
|
|
||||||
echo
|
|
||||||
echo "--- 1. THE OLD SYMPTOM: type-0 rejections (must be ZERO now) ---"
|
|
||||||
echo -n "'should not be given DamageInflictedScore' Verify prints: "
|
|
||||||
cat sv_a.log sv_b.log | grep -ac "should not be given DamageInflictedScore"
|
|
||||||
echo
|
|
||||||
echo "--- 2. THE FIX: inflicted score rows (matchlog SCORE type=0) ---"
|
|
||||||
echo -n "type=0 rows: "; cat matchlog_*.txt 2>/dev/null | grep -ac "type=0"
|
|
||||||
cat matchlog_*.txt 2>/dev/null | grep -a "type=0" | head -8
|
|
||||||
echo
|
|
||||||
echo "--- 3. award vs damage: does the credit TRACK damage? ---"
|
|
||||||
python - << 'EOF'
|
|
||||||
import glob, re
|
|
||||||
aw = []
|
|
||||||
for fn in glob.glob('matchlog_*.txt'):
|
|
||||||
for line in open(fn, errors='replace'):
|
|
||||||
m = re.search(r'type=0 award=(-?[\d.]+) total=(-?[\d.]+)', line)
|
|
||||||
if m:
|
|
||||||
aw.append((float(m.group(1)), float(m.group(2))))
|
|
||||||
if not aw:
|
|
||||||
print(' NO type=0 rows -- interceptor did not fire')
|
|
||||||
else:
|
|
||||||
pos = [a for a, t in aw if a > 0]
|
|
||||||
neg = [a for a, t in aw if a < 0]
|
|
||||||
print(' rows=%d positive=%d negative(self)=%d' % (len(aw), len(pos), len(neg)))
|
|
||||||
print(' award range: %.2f .. %.2f running total ends at %.2f'
|
|
||||||
% (min(a for a, t in aw), max(a for a, t in aw), aw[-1][1]))
|
|
||||||
EOF
|
|
||||||
echo
|
|
||||||
echo "--- 4. kill path un-regressed (type=2) + respawn ---"
|
|
||||||
echo -n "type=2 kill rows: "; cat matchlog_*.txt 2>/dev/null | grep -ac "type=2"
|
|
||||||
cat matchlog_*.txt 2>/dev/null | grep -a "type=2" | head -3
|
|
||||||
echo -n "victim death cycles: "; grep -ac "death cycle START" sv_b.log
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import sys
|
|
||||||
sys.path.insert(0, r"C:\git\bt411\scratchpad\night7")
|
|
||||||
import gitea
|
|
||||||
for n in sys.argv[1:]:
|
|
||||||
i = gitea.call("/issues/%s" % n)
|
|
||||||
print("=" * 78)
|
|
||||||
print("#%s [%s] %s" % (i["number"], i["state"], i["title"]))
|
|
||||||
print("labels:", ",".join(l["name"] for l in i.get("labels", [])))
|
|
||||||
print("-" * 78)
|
|
||||||
print((i.get("body") or "")[:1800])
|
|
||||||
print()
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# =========================================================================
|
|
||||||
# #52 STANDING-LOCK bench -- the night-13 field composition.
|
|
||||||
#
|
|
||||||
# FIELD COMPOSITION (night 13, 4.11.817): a healthy PEER that pivots at
|
|
||||||
# stand and then accelerates. The peer's turn-arm block parks its body
|
|
||||||
# channel at Standing on the walk-demand yield (mech4.cpp:2853, "case 0
|
|
||||||
# walk-begins next tick"); the claim under test is that case 0 CANNOT
|
|
||||||
# walk-begin on a replicant because the port's inserted turn block resets
|
|
||||||
# the state case 0 just armed, using the SAME expression.
|
|
||||||
#
|
|
||||||
# PROVOCATION: B chases A and holds at BT_GOTO_STOP. Every time A walks
|
|
||||||
# back out of that radius B re-acquires -- goto turns hard at 0.2 throttle
|
|
||||||
# (mech4.cpp:4108-4113) = TURNING AT SUB-WALK SPEED, which arms the peer's
|
|
||||||
# body state 4 -- then the heading aligns, throttle goes to 1.0, and the
|
|
||||||
# walk-demand yield fires. That is the trap, once per re-acquisition.
|
|
||||||
#
|
|
||||||
# READ IT ON A: B's replicant lives on A, so A's log carries [bodySM] and
|
|
||||||
# [skate] for B. Both nodes carry both gates anyway.
|
|
||||||
#
|
|
||||||
# MODE=legacy -> BT_NO_BODY_FALLTHRU=1 (pre-fix path; expect the lock)
|
|
||||||
# MODE=fixed -> default (expect no lock)
|
|
||||||
# =========================================================================
|
|
||||||
set -x
|
|
||||||
MODE="${1:-fixed}"
|
|
||||||
DUR="${2:-230}"
|
|
||||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
|
||||||
cd /c/git/bt411/content || exit 1
|
|
||||||
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 2
|
|
||||||
rm -f sl_${MODE}_a.log sl_${MODE}_b.log sl_${MODE}_relay.log
|
|
||||||
|
|
||||||
bt_assert_player_env
|
|
||||||
bt_expert_egg MP.EGG SL.EGG
|
|
||||||
# map=CAVERN on purpose (not the usual grass/day combat default): the lock needs
|
|
||||||
# a mech holding a STEADY walk demand without its gait changing, and the
|
|
||||||
# reliable way to get that is a mech pushing into geometry -- throttle up, leg
|
|
||||||
# SM parked, no gait edges, so no type-3 record ever refreshes the peer. On
|
|
||||||
# open grass whether the autodriver finds a wall is luck: the first run locked
|
|
||||||
# for 336 consecutive seconds, the second for 1. Cavern guarantees it, and its
|
|
||||||
# walls also let the jammed mech SLIDE, which is what turns a lock into a
|
|
||||||
# visible skate. time=day only so the windows are watchable.
|
|
||||||
sed -i "s/^map=.*/map=cavern/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" SL.EGG
|
|
||||||
|
|
||||||
LEGACY=""
|
|
||||||
[ "$MODE" = "legacy" ] && LEGACY=1
|
|
||||||
|
|
||||||
# ROLES (v2, after the pass-1 miss): the mech we need MOVING WHILE LOCKED is
|
|
||||||
# the one being observed. v1 made the observer autodrive and it walked into
|
|
||||||
# the arena wall -- 336 locked seconds but ~zero translation, so the lock
|
|
||||||
# reproduced and the SKATE (which needs 90 sustained MOVING frames) did not.
|
|
||||||
# v2 puts the chaser on A: BT_GOTO with a tight stop radius keeps A walking at
|
|
||||||
# a target that keeps moving, so A gets a stop/turn/walk cycle (the lock entry)
|
|
||||||
# AND continuous travel (the symptom). B observes; B's own wall-bumping is
|
|
||||||
# irrelevant because we read A's replicant on B's log.
|
|
||||||
# v3 = back to the v1 roles, which is the rig that ACTUALLY reproduces.
|
|
||||||
# v2 (chaser observed) gave zero locks, and that is itself the finding: a mech
|
|
||||||
# whose gait keeps changing keeps emitting type-3 records, and each one sets the
|
|
||||||
# peer's body state directly (ReadUpdateRecord), so the peer never has to
|
|
||||||
# self-arm and never meets case 0. The lock needs the opposite -- a peer PARKED
|
|
||||||
# at Standing while the master holds a STEADY demand, so no refreshing record
|
|
||||||
# ever comes. A wall-jammed autodriver is exactly that, which is why v1 locked
|
|
||||||
# for 336 consecutive seconds. Keep it.
|
|
||||||
# ---- node B (back window): the OBSERVER -- chases, so it stays engaged ------
|
|
||||||
(
|
|
||||||
export BT_GOTO=enemy BT_GOTO_STOP=150 BT_GOTO_LOG=1
|
|
||||||
export BT_BODY_SM_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1
|
|
||||||
[ -n "$LEGACY" ] && export BT_NO_BODY_FALLTHRU=1
|
|
||||||
bt_launch sl_${MODE}_b.log SL.EGG 0x0C -net 1601
|
|
||||||
)
|
|
||||||
sleep 2
|
|
||||||
# ---- node A (front window): the OBSERVED mech -- steady demand, held up -----
|
|
||||||
(
|
|
||||||
export BT_AUTODRIVE=0.7
|
|
||||||
export BT_BODY_SM_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1
|
|
||||||
[ -n "$LEGACY" ] && export BT_NO_BODY_FALLTHRU=1
|
|
||||||
bt_launch sl_${MODE}_a.log SL.EGG 0x03 -net 1501
|
|
||||||
)
|
|
||||||
sleep 5
|
|
||||||
python ../tools/btconsole.py SL.EGG 127.0.0.1:1501 127.0.0.1:1601 > sl_${MODE}_relay.log 2>&1 &
|
|
||||||
RELAY=$!
|
|
||||||
sleep "$DUR"
|
|
||||||
kill $RELAY 2>/dev/null
|
|
||||||
bt_kill_ours
|
|
||||||
sleep 2
|
|
||||||
# bt_launch's winpid poll can MISS (documented) -- pass 1 orphaned a node that
|
|
||||||
# then sat holding a -net port. Passes run strictly one at a time here, so a
|
|
||||||
# blanket sweep is safe and is the only thing that guarantees a clean slate.
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 3
|
|
||||||
|
|
||||||
echo "=================== MODE=$MODE ==================="
|
|
||||||
echo "--- THE LOCK: [bodySM] arm->reset pairs (1 Hz throttled) ---"
|
|
||||||
echo -n "on B (observing A, the chaser): "; grep -ac "STANDING-LOCK" sl_${MODE}_b.log
|
|
||||||
echo -n "on A (observing B): "; grep -ac "STANDING-LOCK" sl_${MODE}_a.log
|
|
||||||
grep -a "STANDING-LOCK" sl_${MODE}_b.log | sort -u | head -4
|
|
||||||
echo
|
|
||||||
echo "--- THE PEER GAIT: what a MOVING replicant's body channel is doing ---"
|
|
||||||
echo -n "samples on B: "; grep -ac "peergait" sl_${MODE}_b.log
|
|
||||||
echo "body states seen while moving (B's view of A):"
|
|
||||||
grep -a "\[peergait\]" sl_${MODE}_b.log | grep -oaE "bstate=[0-9-]+" | sort | uniq -c | sort -rn | head -10
|
|
||||||
echo "idle-channel samples (the skate condition):"
|
|
||||||
grep -ac "IDLE CHANNELS" sl_${MODE}_b.log
|
|
||||||
echo
|
|
||||||
echo "--- THE SYMPTOM: [skate] episodes ---"
|
|
||||||
echo -n "on B: "; grep -ac "SKATING" sl_${MODE}_b.log
|
|
||||||
grep -a "\[skate\]" sl_${MODE}_b.log | head -6
|
|
||||||
echo -n "on A: "; grep -ac "SKATING" sl_${MODE}_a.log
|
|
||||||
echo
|
|
||||||
echo "--- A's drive cycle (arrive/re-acquire churn = lock entries) ---"
|
|
||||||
grep -a "\[goto\]" sl_${MODE}_a.log | grep -oaE "arr=[01]" | uniq -c | wc -l
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# =========================================================================
|
|
||||||
# #52 STANDING-LOCK -- A/B on the rig that is KNOWN to reproduce.
|
|
||||||
#
|
|
||||||
# Composition lifted verbatim from scratchpad/night12/skatebench.sh, which
|
|
||||||
# produced 6 skate episodes in one 260s run (sk_run.out) and 4 in another.
|
|
||||||
# A walks continuously (autodrive) and is killed every ~40s by B, so it
|
|
||||||
# respawns WHILE MOVING -- and that is the field correlation both nights:
|
|
||||||
# night 12's skating clustered in the deaths-heavy final drop, and all four
|
|
||||||
# of night 13's episodes were in the last, deaths-heavy session.
|
|
||||||
#
|
|
||||||
# My own rigs (skatelock.sh v1/v2/v3) each failed to reproduce the SYMPTOM:
|
|
||||||
# a wall-jammed mech locks but does not translate, and a freely-walking mech
|
|
||||||
# transitions gait constantly, so records keep rescuing its peer copy. The
|
|
||||||
# kill cycle gives both halves at once -- a steady post-respawn walk demand
|
|
||||||
# with no gait edges, on a mech that is actually moving.
|
|
||||||
#
|
|
||||||
# B is the OBSERVER: A's replicant lives on B, so B's log carries [skate],
|
|
||||||
# [bodySM] and [peergait] for A.
|
|
||||||
#
|
|
||||||
# MODE=legacy -> BT_NO_BODY_FALLTHRU=1 (pre-fix; expect episodes)
|
|
||||||
# MODE=fixed -> default (expect none)
|
|
||||||
# =========================================================================
|
|
||||||
set -x
|
|
||||||
MODE="${1:-fixed}"
|
|
||||||
DUR="${2:-260}"
|
|
||||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
|
||||||
cd /c/git/bt411/content || exit 1
|
|
||||||
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 2
|
|
||||||
rm -f s2_${MODE}_a.log s2_${MODE}_b.log s2_${MODE}_relay.log
|
|
||||||
|
|
||||||
bt_assert_player_env
|
|
||||||
bt_expert_egg MP.EGG S2.EGG
|
|
||||||
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" S2.EGG
|
|
||||||
|
|
||||||
LEGACY=""
|
|
||||||
[ "$MODE" = "legacy" ] && LEGACY=1
|
|
||||||
|
|
||||||
# ---- node B: OBSERVER + killer (back window) ------------------------------
|
|
||||||
(
|
|
||||||
export BT_MP_FORCE_DMG=1
|
|
||||||
export BT_BODY_SM_LOG=1 BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1
|
|
||||||
[ -n "$LEGACY" ] && export BT_NO_BODY_FALLTHRU=1
|
|
||||||
bt_launch s2_${MODE}_b.log S2.EGG 0x0C -net 1601
|
|
||||||
)
|
|
||||||
sleep 2
|
|
||||||
# ---- node A: the walking victim; autodrive persists across respawns -------
|
|
||||||
(
|
|
||||||
export BT_AUTODRIVE=0.8
|
|
||||||
export BT_BODY_SM_LOG=1 BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1
|
|
||||||
[ -n "$LEGACY" ] && export BT_NO_BODY_FALLTHRU=1
|
|
||||||
bt_launch s2_${MODE}_a.log S2.EGG 0x03 -net 1501
|
|
||||||
)
|
|
||||||
sleep 5
|
|
||||||
python ../tools/btconsole.py S2.EGG 127.0.0.1:1501 127.0.0.1:1601 > s2_${MODE}_relay.log 2>&1 &
|
|
||||||
RELAY=$!
|
|
||||||
sleep "$DUR"
|
|
||||||
kill $RELAY 2>/dev/null
|
|
||||||
bt_kill_ours
|
|
||||||
sleep 2
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 3
|
|
||||||
|
|
||||||
echo "=================== MODE=$MODE ==================="
|
|
||||||
echo -n "death cycles on A (the provocation actually ran): "
|
|
||||||
grep -acE "death cycle START" s2_${MODE}_a.log
|
|
||||||
echo
|
|
||||||
echo "--- THE SYMPTOM: [skate] episodes on B (A's replicant) ---"
|
|
||||||
echo -n "SKATING lines: "; grep -ac "SKATING" s2_${MODE}_b.log
|
|
||||||
grep -a "\[skate\]" s2_${MODE}_b.log | head -14
|
|
||||||
echo
|
|
||||||
echo "--- THE MECHANISM: [bodySM] arm->reset pairs (1 Hz throttled) ---"
|
|
||||||
echo -n "STANDING-LOCK on B: "; grep -ac "STANDING-LOCK" s2_${MODE}_b.log
|
|
||||||
grep -a "STANDING-LOCK" s2_${MODE}_b.log | grep -a REPLICANT | sort -u | head -3
|
|
||||||
echo
|
|
||||||
echo "--- THE POSITIVE: moving replicant's body state, 1 Hz ---"
|
|
||||||
grep -a "\[peergait\]" s2_${MODE}_b.log | grep -oaE "bstate=[0-9-]+" | sort | uniq -c | sort -rn | head -8
|
|
||||||
echo -n "samples flagged IDLE CHANNELS: "; grep -ac "IDLE CHANNELS" s2_${MODE}_b.log
|
|
||||||
echo
|
|
||||||
echo "--- control: skate on A (B stands still -- expect 0) ---"
|
|
||||||
grep -ac "SKATING" s2_${MODE}_a.log
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# =========================================================================
|
|
||||||
# #146 -- desktop virtual throttle must be RELEASED on respawn.
|
|
||||||
#
|
|
||||||
# The pod's throttle is a physical lever and Mech::Reset deliberately leaves
|
|
||||||
# it alone (subsystem loop starts at index 2; ControlsMapper is index 0) --
|
|
||||||
# authentic, kept. The desktop bridge only EMULATES that lever with the
|
|
||||||
# static ramp accumulator sLever, so a pad/keyboard pilot respawned at speed
|
|
||||||
# while holding nothing. Fix: queue the existing all-stop at Mech::Reset,
|
|
||||||
# LOCAL VIEWPOINT MECH ONLY.
|
|
||||||
#
|
|
||||||
# WHAT THIS BENCH ACTUALLY TESTS. It does NOT test "does zeroing sLever
|
|
||||||
# stop the mech" -- that path is the X all-stop button, proven in the field
|
|
||||||
# every day, and BT_AUTODRIVE cannot exercise it anyway (forced mode reads
|
|
||||||
# gBTDrive.forcedThrottle, never sLever -- mechmppr.cpp:677).
|
|
||||||
#
|
|
||||||
# It tests the one thing the fix could genuinely get WRONG: the viewpoint
|
|
||||||
# gate. Mech::Reset also runs for REPLICANTS, so an ungated write to the
|
|
||||||
# global gBTDrive would all-stop the local player every time a REMOTE mech
|
|
||||||
# respawned -- a far worse bug than the one being fixed.
|
|
||||||
#
|
|
||||||
# PASS: A (who dies) logs "[respawn] desktop throttle released" once per
|
|
||||||
# Mech::Reset, and B logs it ZERO times while A is respawning.
|
|
||||||
# FAIL: any occurrence in B's log -> the gate leaks and remote respawns
|
|
||||||
# stop the local pilot.
|
|
||||||
#
|
|
||||||
# A self-damages to death on a timer; B just flies and watches.
|
|
||||||
# =========================================================================
|
|
||||||
set -x
|
|
||||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
|
||||||
cd /c/git/bt411/content || exit 1
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 2
|
|
||||||
rm -f tr_a.log tr_b.log tr_relay.log
|
|
||||||
bt_expert_egg MP.EGG TR.EGG
|
|
||||||
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" TR.EGG
|
|
||||||
|
|
||||||
# B: the OBSERVER. Also driving, so a leaked all-stop would be doubly visible.
|
|
||||||
( export BT_AUTODRIVE=0.6
|
|
||||||
export BT_DEATH_LOG=1 BT_MP_LOG=1 BT_KEY_LOG=1
|
|
||||||
bt_launch tr_b.log TR.EGG 0x0C -net 1601 )
|
|
||||||
sleep 2
|
|
||||||
# A: drives, dies repeatedly, respawns.
|
|
||||||
( export BT_AUTODRIVE=0.8 BT_SELF_DAMAGE=8
|
|
||||||
export BT_DEATH_LOG=1 BT_MP_LOG=1 BT_KEY_LOG=1
|
|
||||||
bt_launch tr_a.log TR.EGG 0x03 -net 1501 )
|
|
||||||
sleep 5
|
|
||||||
python ../tools/btconsole.py TR.EGG 127.0.0.1:1501 127.0.0.1:1601 > tr_relay.log 2>&1 &
|
|
||||||
RELAY=$!
|
|
||||||
sleep 280
|
|
||||||
kill $RELAY 2>/dev/null
|
|
||||||
sleep 3
|
|
||||||
bt_kill_ours; sleep 2; taskkill //F //IM btl4.exe > /dev/null 2>&1; sleep 3
|
|
||||||
|
|
||||||
echo "=================== #146 THROTTLE RELEASE ON RESPAWN ==================="
|
|
||||||
echo "--- A: how many respawns, how many releases? (must be 1:1) ---"
|
|
||||||
echo -n " A Mech::Reset .......... "; grep -ac "Mech::Reset" tr_a.log
|
|
||||||
echo -n " A throttle released .... "; grep -ac "desktop throttle released" tr_a.log
|
|
||||||
echo
|
|
||||||
echo "--- B: the VIEWPOINT GATE. Must be 0 releases despite seeing A respawn. ---"
|
|
||||||
echo -n " B Mech::Reset (incl. replicant) .... "; grep -ac "Mech::Reset" tr_b.log
|
|
||||||
echo -n " B throttle released (MUST BE 0) .... "; grep -ac "desktop throttle released" tr_b.log
|
|
||||||
echo
|
|
||||||
echo "--- A: the release must sit INSIDE the reset block ---"
|
|
||||||
grep -aE "Mech::Reset|desktop throttle released" tr_a.log | head -8
|
|
||||||
echo
|
|
||||||
echo "--- B: same window, to show B saw the respawn and still did not release ---"
|
|
||||||
grep -aE "Mech::Reset|desktop throttle released" tr_b.log | head -8
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# =========================================================================
|
|
||||||
# TURN-IN-PLACE regression check for the #52 fallthrough fix.
|
|
||||||
#
|
|
||||||
# The fix only changes ONE path: case 0 no longer falls into the port's
|
|
||||||
# turn block. Entry INTO state 4 is unaffected -- masters arm it via the
|
|
||||||
# leg twin's lockstep (mech2.cpp:1014) and peers via the #82 block
|
|
||||||
# (mech4.cpp:2851), and both set the state so the NEXT frame dispatches
|
|
||||||
# straight to case 4. This bench exercises that claim instead of asserting
|
|
||||||
# it: force sustained PIVOTING and confirm state 4 is still entered, still
|
|
||||||
# advances, and still exits -- on both the master and the peer.
|
|
||||||
#
|
|
||||||
# PROVOCATION: BT_GOTO with a tiny throttle and a stop radius it can never
|
|
||||||
# reach -- the mech steers at the enemy forever while creeping below
|
|
||||||
# standSpeed, which is exactly the trn entry gate (turning + speed in
|
|
||||||
# [0, standSpeed] + turnCapable).
|
|
||||||
# =========================================================================
|
|
||||||
set -x
|
|
||||||
MODE="${1:-fixed}"
|
|
||||||
DUR="${2:-150}"
|
|
||||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
|
||||||
cd /c/git/bt411/content || exit 1
|
|
||||||
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 2
|
|
||||||
rm -f tr_${MODE}_a.log tr_${MODE}_b.log tr_${MODE}_relay.log
|
|
||||||
|
|
||||||
bt_expert_egg MP.EGG TR.EGG
|
|
||||||
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" TR.EGG
|
|
||||||
|
|
||||||
LEGACY=""
|
|
||||||
[ "$MODE" = "legacy" ] && LEGACY=1
|
|
||||||
|
|
||||||
# v2: MUTUAL goto converged instantly -- both mechs faced each other, err~0,
|
|
||||||
# nothing ever turned (146 samples parked at Standing). A pivot needs a target
|
|
||||||
# that keeps MOVING, so B autodrives away and A creeps-and-steers after it:
|
|
||||||
# heading error stays live while A's speed stays under standSpeed, which is the
|
|
||||||
# trn entry gate.
|
|
||||||
( export BT_AUTODRIVE=0.6 # B: the moving target
|
|
||||||
export BT_BODY_SM_LOG=1 BT_MP_LOG=1
|
|
||||||
[ -n "$LEGACY" ] && export BT_NO_BODY_FALLTHRU=1
|
|
||||||
bt_launch tr_${MODE}_b.log TR.EGG 0x0C -net 1601 )
|
|
||||||
sleep 2
|
|
||||||
( export BT_GOTO=enemy BT_GOTO_STOP=5 BT_GOTO_THR=0.02 BT_GOTO_LOG=1
|
|
||||||
export BT_BODY_SM_LOG=1 BT_MP_LOG=1 # A: the pivoter
|
|
||||||
[ -n "$LEGACY" ] && export BT_NO_BODY_FALLTHRU=1
|
|
||||||
bt_launch tr_${MODE}_a.log TR.EGG 0x03 -net 1501 )
|
|
||||||
sleep 5
|
|
||||||
python ../tools/btconsole.py TR.EGG 127.0.0.1:1501 127.0.0.1:1601 > tr_${MODE}_relay.log 2>&1 &
|
|
||||||
RELAY=$!
|
|
||||||
sleep "$DUR"
|
|
||||||
kill $RELAY 2>/dev/null
|
|
||||||
bt_kill_ours
|
|
||||||
sleep 2
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 3
|
|
||||||
|
|
||||||
echo "=================== TURN REGRESSION MODE=$MODE ==================="
|
|
||||||
echo "--- MASTER: does the turn-in-place clip still get entered + advanced? ---"
|
|
||||||
for n in a b; do
|
|
||||||
echo "node $n body state=4 samples : $(grep -a '\[gaitSM\] adv=' tr_${MODE}_${n}.log | grep -caE ' state=4 ')"
|
|
||||||
echo "node $n leg state=4 samples : $(grep -a '\[gaitSM\] adv=' tr_${MODE}_${n}.log | grep -caE ' legState=4')"
|
|
||||||
echo "node $n full body distribution:"
|
|
||||||
grep -a "\[gaitSM\] adv=" tr_${MODE}_${n}.log | grep -oaE "state=[0-9]+" | sort | uniq -c | sort -rn | head -6
|
|
||||||
done
|
|
||||||
echo
|
|
||||||
echo "--- PEER: does a replicant still pivot (body state 4 while moving)? ---"
|
|
||||||
grep -a "\[peergait\]" tr_${MODE}_b.log | grep -oaE "bstate=[0-9-]+" | sort | uniq -c | sort -rn | head -8
|
|
||||||
echo
|
|
||||||
echo "--- no new lock / no skate ---"
|
|
||||||
echo -n "STANDING-LOCK a/b: "; echo "$(grep -ac 'STANDING-LOCK' tr_${MODE}_a.log) / $(grep -ac 'STANDING-LOCK' tr_${MODE}_b.log)"
|
|
||||||
echo -n "SKATING a/b: "; echo "$(grep -ac 'SKATING' tr_${MODE}_a.log) / $(grep -ac 'SKATING' tr_${MODE}_b.log)"
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
"""Night 13: correct #148's framing with the measured findings. ASCII only."""
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0, r"C:\git\bt411\scratchpad\night7")
|
|
||||||
import gitea
|
|
||||||
|
|
||||||
BODY = """**Investigated, narrowed a long way, NOT fixed. Correcting this ticket's own framing first.**
|
|
||||||
|
|
||||||
## The title/premise was WRONG -- the cadence is authentic
|
|
||||||
|
|
||||||
I filed this as "only 13 update records across a 5-minute run". That is not a defect. Reading the
|
|
||||||
payloads with line numbers:
|
|
||||||
|
|
||||||
```
|
|
||||||
205 RECORD atUpd=0.0436769 rate=0.305433
|
|
||||||
502 RECORD atUpd=2.3558 rate=-0.305433
|
|
||||||
1150 RECORD atUpd=-2.39277 rate=0.305433
|
|
||||||
1698 RECORD atUpd=2.38543 rate=-0.305431
|
|
||||||
2044 RECORD atUpd=-2.39124 rate=0.305427
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
Those `atUpd` values are the sweep's EXTREMES and `rate` flips sign at each one. **The master sends
|
|
||||||
a torso record on RATE CHANGE**, and the peer dead-reckons `atUpd + rate * elapsed` in between
|
|
||||||
(`ComputeTargetTwist`). 12 records for 12 direction reversals is exactly right -- this is
|
|
||||||
extrapolation-based replication working as designed, not a starved channel.
|
|
||||||
|
|
||||||
Also ruled out: the `ComputeTargetTwist` clamp. The copy's limits load correctly
|
|
||||||
(`limL=2.44346 limR=-2.44346 enab=1 copy=1`), so `Min/Max` is not pinning `targetTwist` to zero.
|
|
||||||
And the extrapolator itself is exact -- the copy reports `cur == target` on every sample.
|
|
||||||
|
|
||||||
## The real defect
|
|
||||||
|
|
||||||
**The peer's copy torso Performance does not run at all until log line ~1006, while its first
|
|
||||||
record arrived at line 205.** ~800 lines of correctly-replicated twist data integrated by nobody.
|
|
||||||
|
|
||||||
This is now directly readable because `[torso-copy]` logs on call #0 (`s_cl++ % 120`), so its first
|
|
||||||
line IS the first `TorsoCopySimulation` call. (That also means my earlier "first copy currentTwist
|
|
||||||
!= 0 at line 1016" was a SAMPLING artifact -- first *sample*, not first non-zero.)
|
|
||||||
|
|
||||||
The first tick coincides with the replicant's MODEL bring-up, not with record arrival:
|
|
||||||
|
|
||||||
```
|
|
||||||
[loadclips] end: fScale=0.8 ... hasGimpClips=1
|
|
||||||
[clipfix] mech 05769358 -> EXTERIOR (lean)
|
|
||||||
[torso] PushTwist COPY node=057A3C68 type=1 twist=-1.52319
|
|
||||||
```
|
|
||||||
|
|
||||||
So the gate is **above the subsystem level**, somewhere in replicant model/clip initialisation.
|
|
||||||
Not yet found.
|
|
||||||
|
|
||||||
## Tried and rejected
|
|
||||||
|
|
||||||
`Entity::Perform` (ENTITY.cpp:733-793, real engine source) picks the executable predicate by
|
|
||||||
instance -- `IsNonReplicantExecutable()` for masters, `IsReplicantExecutable()` for replicants --
|
|
||||||
and they differ exactly on `|| lastUpdate >= lastPerformance`, which is what makes an
|
|
||||||
`ExecuteOnUpdate` subsystem run when a record arrives. Mech's reconstructed tick loop used the
|
|
||||||
NonReplicant predicate for every mech, dropping the branch.
|
|
||||||
|
|
||||||
That is a genuine fidelity gap and it is now **fixed** (`f36f013`) -- but it does **not** move this
|
|
||||||
bug (first copy tick 1014 -> 1006, noise). The torso's own flag was not the gate.
|
|
||||||
|
|
||||||
## Consequence, and what it is NOT
|
|
||||||
|
|
||||||
This is the tail of #141: the peer's missiles launch along the body facing until its torso starts
|
|
||||||
ticking. #141's own fix is complete and verified separately -- every zero-`twistDelta` peer launch
|
|
||||||
now reads `liveTwist=0` on the same log line, and the first launch with `liveTwist=-1.84061` reads
|
|
||||||
`twistDelta=-1.83813`. The launch frame tracks the twist perfectly; there is simply no twist to
|
|
||||||
track until the copy torso wakes up.
|
|
||||||
|
|
||||||
Likely also relevant to **#37** (MadCat torso BACKWARDS) and **#70** (twist stops after respawn) --
|
|
||||||
a peer torso that does not tick would sit at its bind pose, and a respawn re-runs model bring-up.
|
|
||||||
Re-test both once this is found.
|
|
||||||
|
|
||||||
## Diagnostics now in place
|
|
||||||
|
|
||||||
`BT_TORSO_LOG`: `[torso-rec-rx]` (receive + raw payload), `[torso-copy]` (cur/target/atUpd/rate plus
|
|
||||||
`limL/limR/enab`), `[torso] PushTwist master|COPY` (per instance-kind).
|
|
||||||
`BT_PROJ_LOG`: `[launchframe]` now carries the shooter's live torso twist, so `twistDelta` and its
|
|
||||||
driver appear on the same line."""
|
|
||||||
|
|
||||||
gitea.call("/issues/148", method="PATCH", payload={
|
|
||||||
"title": "Peer copy TORSO does not tick until replicant model bring-up -- ~800 lines of replicated twist integrated by nobody"})
|
|
||||||
gitea.comment(148, BODY)
|
|
||||||
print("updated #148")
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# #152: is the AUTHENTIC torso-CENTER button (0x42) alive on the RIO/glass path
|
|
||||||
# (key bridge OFF)? Cycle to Standard, deflect/release the twist axis, and
|
|
||||||
# hold 0x42 during release windows. ctrCmd=1 while held + twist slewing to 0
|
|
||||||
# = route works (ticket becomes player education). ctrCmd stuck 0 = the
|
|
||||||
# streamed 0x42 route is dead on glass and needs the forward implemented.
|
|
||||||
set -x
|
|
||||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
|
||||||
cd /c/git/bt411/content || exit 1
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 2
|
|
||||||
rm -f c42_a.log
|
|
||||||
bt_expert_egg MP.EGG C42.EGG
|
|
||||||
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" C42.EGG
|
|
||||||
( export BT_MODECYCLE_EVERY=400 BT_TWIST_PULSE=150
|
|
||||||
export BT_BTNTEST=0x42,900,1150 BT_BTNTEST2=0x42,1500,1750
|
|
||||||
export BT_TORSO_LOG=1 BT_KEY_NOFOCUS=1 BT_KEY_BRIDGE=0
|
|
||||||
bt_launch c42_a.log C42.EGG 0x03 )
|
|
||||||
sleep 130
|
|
||||||
bt_kill_ours; sleep 2; taskkill //F //IM btl4.exe > /dev/null 2>&1; sleep 3
|
|
||||||
echo "============ RESULT ============"
|
|
||||||
grep -a "\[mode\] control mode" c42_a.log | head -4
|
|
||||||
echo "--- ctrCmd seen (want some =1 during the 0x42 holds) ---"
|
|
||||||
grep -ao "ctrCmd=[01]" c42_a.log | sort | uniq -c
|
|
||||||
echo "--- twist trajectory around the first hold ---"
|
|
||||||
grep -aE "twistpulse|ctrCmd=1" c42_a.log | head -8
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
"""#137: post the fix write-up. ASCII only."""
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0, r"C:\git\bt411\scratchpad\night7")
|
|
||||||
import gitea
|
|
||||||
|
|
||||||
BODY = """**FIXED** (`0d6ed40`) -- and my previous two analyses of this ticket were both wrong, so here is
|
|
||||||
the final, measured story.
|
|
||||||
|
|
||||||
## The fix is ONE LINE, and it is the binary's own
|
|
||||||
|
|
||||||
The binary Reset's SECOND instruction is `FUN_00408440(mech+0x58c, param_2)` -- re-seed the
|
|
||||||
Point3D at `+0x58c` to the new origin. `+0x58c` is the **previous-position memory** of the
|
|
||||||
AccelerationLastFrame ring feed (`+0x81c/0x824/0x828/0x82c`). The port reconstructed the ring
|
|
||||||
faithfully but its Reset never got that line. Restored as:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
accelPrevPos = origin.linearPosition; // binary +0x58c re-seed
|
|
||||||
```
|
|
||||||
|
|
||||||
## What actually happened at a respawn
|
|
||||||
|
|
||||||
1. The respawn TELEPORTS the mech; `accelPrevPos` still held the death position.
|
|
||||||
2. First post-respawn sample: `|newPos - prevPos| / dt` = **teleport distance / dt ~ 1e5** enters
|
|
||||||
the velocity ring; the ring-mean derivative spikes `AccelerationLastFrame` (pure forward, with
|
|
||||||
an opposite-sign echo ~15 frames later as the sample rotates out of the 15-ring).
|
|
||||||
3. The myomer drive-heat integrator `@004b8d18`:
|
|
||||||
`termAccel = (1-accEff) * |v| * |a| * m * dt = 0.2 * 40 * 1.04e5 * 75000 * 0.044 = 2.75e9`
|
|
||||||
-- one tick deposits ~3e9 into `pendingHeat -> heatEnergy`.
|
|
||||||
4. The freshly-reset myomers snaps 77 -> ~9000 against `failT=2000` -> `speedEffect 0` ->
|
|
||||||
`speedDemand *= 0` -> **"respawned with heat maxed, unable to move until it cools"**. The
|
|
||||||
excess then sheds into Condenser5 and GeneratorD -- Oracle's "loop 5 and generator D heating
|
|
||||||
up" clause, literally.
|
|
||||||
|
|
||||||
**Why ~8% and why it eluded everyone:** the deposit needs `|v|` in the same 1-2 frames, so only
|
|
||||||
pilots whose throttle was still forward at the respawn (physical lever / HOTAS -- exactly who
|
|
||||||
reported it) got the freeze. Idle-throttle respawns deposit nothing. That is also why frozen
|
|
||||||
respawns were a strict subset of died-hot ones: died hot = was running hard = lever still forward.
|
|
||||||
|
|
||||||
## Measured (same abusive bench: 0.95 throttle, continuous autofire)
|
|
||||||
|
|
||||||
| | before | after |
|
|
||||||
|---|---|---|
|
|
||||||
| deposits >1e7 near resets | up to **3.35e9**, every one 3-4 lines after a reset | **0** |
|
|
||||||
| frozen respawns | 4-6 of 7 | **0 of 7** |
|
|
||||||
| post-reset myomers T | 7,700-12,100 | 77-178 (degradeT is 1000) |
|
|
||||||
|
|
||||||
## For the record: what it was NOT (each killed by operand data)
|
|
||||||
|
|
||||||
Not the reset (T=77 at every reset, roster-wide). Not stale pendingHeat (1-frame bound, +1.2K).
|
|
||||||
Not death-window accumulation (consumers tick the wreck). Not conduction (roster snapshot: all
|
|
||||||
partners at 77; flow trap: zero e6 flows into the myomers). Not drag or impulses (both trapped:
|
|
||||||
never fired). And not my earlier "players accelerate to top speed" close -- arithmetically
|
|
||||||
impossible (input ceiling ~60 deg/s vs the observed 1,100-21,000 deg/s snap); the KB paragraph
|
|
||||||
carrying that claim is corrected.
|
|
||||||
|
|
||||||
The in-life governor is untouched and authentic: sustained top-speed running still derates the
|
|
||||||
myomers (constants byte-verified). What is gone is the respawn injecting a teleport-sized heat
|
|
||||||
slug.
|
|
||||||
|
|
||||||
Credit where due: the operator called the shape of this from the start -- *"maybe the math gets
|
|
||||||
screwy in respawning while some systems are ticking while values are being reset."*
|
|
||||||
|
|
||||||
**Unreleased; needs a build cut. Field-verify:** respawn while holding the throttle forward on a
|
|
||||||
HOTAS -- the mech should drive off cleanly with a cold heat bar every time. Gotcha #30 records the
|
|
||||||
class so the next dropped re-seed gets caught in review."""
|
|
||||||
|
|
||||||
gitea.comment(137, BODY)
|
|
||||||
gitea.call("/issues/137", method="PATCH", payload={"state": "closed"})
|
|
||||||
print("posted + closed #137")
|
|
||||||
@@ -1,299 +0,0 @@
|
|||||||
"""Night 14 (build 4.11.857) housekeeping: close what the field confirmed, reopen
|
|
||||||
#137 with evidence, and file the new reports. ASCII only."""
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0, r"C:\git\bt411\scratchpad\night7")
|
|
||||||
import gitea
|
|
||||||
|
|
||||||
FIELD = ("Field-verified on **4.11.857** (playtest 2026-08-08, 4 testers: Oracle/SCREECH, "
|
|
||||||
"Sauron/XIAOLONG, santo/MS-FIREFLY, RajelAran/GAMERSLAB).")
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- CLOSE ----
|
|
||||||
CLOSE = {
|
|
||||||
52: "**CONFIRMED FIXED.** " + FIELD + "\n\nOracle: *\"no skating observed\"*.\n\n"
|
|
||||||
"Cross-check: the `[skate]` detector fires zero times across all four logs.\n\n"
|
|
||||||
"**#130 (Vulture skating) should be re-tested against this build** -- it was always "
|
|
||||||
"suspected to be the same defect.",
|
|
||||||
108: "**CONFIRMED FIXED.** " + FIELD + "\n\nOracle: *\"Lynx panic ejected and didn't ghost\"*.\n\n"
|
|
||||||
"The eject path now runs the full death tail, so the respawn trigger fires.",
|
|
||||||
142: "**CONFIRMED FIXED.** " + FIELD + "\n\nOracle: *\"Fixed, button indicating state and mech "
|
|
||||||
"is standing after respawn\"* -- both halves: the indicator animates, and the "
|
|
||||||
"`Mech::Reset` posture clear brings you back standing.",
|
|
||||||
140: "**CONFIRMED FIXED.** " + FIELD + "\n\nOracle: *\"It is not overwriting the saved settings\"*.",
|
|
||||||
141: "**CONFIRMED FIXED.** " + FIELD + "\n\nOracle: *\"Fixed\"*. VGL Lynx, independently and "
|
|
||||||
"before seeing the notes: *\"Missiles seem to be launching from weapon ports\"* -- which is "
|
|
||||||
"the symptom stated positively.\n\n"
|
|
||||||
"Worth noting the same stale-segment-cache defect was fixed on the **beam muzzle** path in "
|
|
||||||
"the same sweep; nobody had reported that one, and a peer's beams would have had the "
|
|
||||||
"identical wrong origin whenever the shooter was twisted.",
|
|
||||||
}
|
|
||||||
|
|
||||||
# --------------------------------------------------------------- COMMENT ----
|
|
||||||
COMMENT = {
|
|
||||||
147: "**Not reproduced on 4.11.857** -- Oracle: *\"I have been unable to recreate the range caret "
|
|
||||||
"issue today ... but only tried in Solo\"*, and later *\"Not observed\"*.\n\n"
|
|
||||||
"Cross-check: zero `NaN TRAPPED` receipts in any of the four field logs, so the trap never "
|
|
||||||
"had to fire either.\n\n"
|
|
||||||
"**Keeping this OPEN.** One clean night is not proof for a defect whose whole signature is "
|
|
||||||
"'rare, and sticky once it happens' -- and solo exercises far less of the targeting path "
|
|
||||||
"than a 4-way drop. If the caret dies again, `BT_RANGE_LOG=1` now names the frame.",
|
|
||||||
146: "**Working on 4.11.857, with a nuance worth recording.** Oracle: *\"Initial spawn/respawn "
|
|
||||||
"throttle is idle and then begins to respond to match my throttle position on my X56 "
|
|
||||||
"without moving it.\"* That is the fix behaving correctly on a PHYSICAL throttle: the "
|
|
||||||
"virtual lever is released at respawn, then re-syncs to where the hardware lever actually "
|
|
||||||
"sits.\n\n"
|
|
||||||
"**But see the new report about being unable to move after respawn until the throttle is "
|
|
||||||
"cycled.** A HOTAS user who respawns with the lever already forward now gets no motion "
|
|
||||||
"until the axis next changes -- that may be this fix's side effect rather than a separate "
|
|
||||||
"bug, and the two need separating before either is 'fixed'.",
|
|
||||||
135: "**Related field report on 4.11.857** (Oracle): *\"buttons for gens are not flashing when "
|
|
||||||
"they leak\"* -- observed in GLASS mode this time, where this ticket was originally about "
|
|
||||||
"the normal (non-glass) screen path. Same annunciator chain, so treat them together: the "
|
|
||||||
"coolant-leak condition (cond 2) is supposed to light the per-generator lamps.",
|
|
||||||
}
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ NEW ----
|
|
||||||
NEW = [
|
|
||||||
("REOPEN #137 -- placeholder", None), # handled separately below
|
|
||||||
]
|
|
||||||
|
|
||||||
def main():
|
|
||||||
for num, body in CLOSE.items():
|
|
||||||
gitea.close(num, body)
|
|
||||||
for num, body in COMMENT.items():
|
|
||||||
gitea.comment(num, body)
|
|
||||||
|
|
||||||
# ---- #137: reopen. I closed this wrongly; the field disproved my analysis.
|
|
||||||
gitea.comment(137,
|
|
||||||
"**REOPENING -- I closed this incorrectly, and the field data disproves my analysis.**\n\n"
|
|
||||||
"I closed it as 'not a bug: the mech respawns still under throttle and earns the heat "
|
|
||||||
"honestly'. Oracle pushed back: *\"not sure if the bug report was understood ... the mech "
|
|
||||||
"is initially unable to move until it cools off sufficiently to start moving. This is "
|
|
||||||
"abnormal.\"* He is right, and 'unable to move' is the detail that kills my explanation -- "
|
|
||||||
"a mech that cannot move cannot be earning heat by running.\n\n"
|
|
||||||
"## Measured on 4.11.857, all four field logs\n\n"
|
|
||||||
"Scanning every respawn for the freeze signature (throttle up, speedDemand pinned at 0):\n\n"
|
|
||||||
"| player | respawns | FROZE |\n|---|---|---|\n"
|
|
||||||
"| Oracle / SCREECH | 16 | **2** |\n| Sauron / XIAOLONG | 19 | **2** |\n"
|
|
||||||
"| RajelAran / GAMERSLAB | 16 | **1** |\n| santo / MS-FIREFLY | 10 | 0 |\n\n"
|
|
||||||
"**5 of 61 respawns (~8%)**, across three independent machines.\n\n"
|
|
||||||
"The sequence at one of them (Oracle, log line 48584) is unambiguous:\n\n"
|
|
||||||
"```\n"
|
|
||||||
"[mppr] thr=0.847 -> speedDemand=44.87 <- moving normally, pre-death\n"
|
|
||||||
"[respawn] Mech::Reset 2:21 healed+moved\n"
|
|
||||||
"[techstat] Myomers condition 1 CLEARED <- Damaged cleared OK\n"
|
|
||||||
"[techstat] Myomers condition 2 CLEARED <- Leaking cleared OK\n"
|
|
||||||
"[techstat] Myomers condition 3 SET <- OVERHEATING, AT the reset\n"
|
|
||||||
"[mppr] thr=0.762 -> speedDemand=0 <- FROZEN\n"
|
|
||||||
"```\n\n"
|
|
||||||
"The reset clears the damage and leak flags correctly and then the myomers come up "
|
|
||||||
"**already overheating**, in the same breath as the reset -- before any running could have "
|
|
||||||
"happened. The myomer heat-freeze (`speedEffect` -> `speedDemand *= it`) then pins speed "
|
|
||||||
"at zero, which is exactly the reported 'cannot move until it cools'.\n\n"
|
|
||||||
"## Why my bench missed it\n\n"
|
|
||||||
"It is **intermittent (~8%)**. My synthetic bench reset cleanly every single time and "
|
|
||||||
"reported `T=77 start=77` for every heat-bearing subsystem including all six Condensers -- "
|
|
||||||
"so I concluded the reset path was correct and closed the ticket. A bench that never "
|
|
||||||
"reproduces the failure is not evidence the failure does not exist; I treated it as such.\n\n"
|
|
||||||
"## Next\n\n"
|
|
||||||
"The temperature is NOT logged in the field (`BT_HEAT_LOG` is off), so the next step is a "
|
|
||||||
"bench that reproduces the freeze rather than the happy path -- repeated death/respawn "
|
|
||||||
"under load until a reset lands hot -- and then reads `[heat-reset]` on the failing one. "
|
|
||||||
"The ~8% rate suggests a race or an ordering dependency rather than a plain missing reset, "
|
|
||||||
"since the plain path demonstrably works.")
|
|
||||||
gitea.call("/issues/137", method="PATCH", payload={"state": "open"})
|
|
||||||
print("REOPENED #137")
|
|
||||||
|
|
||||||
made = []
|
|
||||||
def new(title, body, ):
|
|
||||||
i = gitea.create(title, body)
|
|
||||||
made.append(i["number"])
|
|
||||||
|
|
||||||
new("PERFORMANCE REGRESSION 817 -> 857: hard stalls at mission load (glass)",
|
|
||||||
"Oracle: *\"frame rate very poor with glass cockpit starting with this build. Only "
|
|
||||||
"observed by Oracle, Sauron using glass had no issue. Oracle had no performance issues in "
|
|
||||||
"past with glass.\"*\n\n"
|
|
||||||
"**Confirmed, and it is a real regression -- but it is STUTTER, not low framerate.**\n\n"
|
|
||||||
"## Same machine, previous build vs this one (Oracle / SCREECH)\n\n"
|
|
||||||
"| build | median frame | stalls >50ms | worst |\n|---|---|---|---|\n"
|
|
||||||
"| 4.11.817 (Aug 6) | 5.79 ms | **0** (0.00%) | 15.4 ms |\n"
|
|
||||||
"| 4.11.857 (Aug 8) | 5.87 ms | **97** (6.70%) | 103.7 ms |\n\n"
|
|
||||||
"His steady state is **identical** (5.79 -> 5.87 ms). 817 had literally zero stalls; 857 "
|
|
||||||
"has 97, and the worst frame went 15 ms -> 104 ms.\n\n"
|
|
||||||
"## Why only Oracle noticed\n\n"
|
|
||||||
"| player | median | stalls >50ms |\n|---|---|---|\n"
|
|
||||||
"| Oracle | 5.87 ms (170 fps) | 97 |\n| santo | 3.55 ms | 0 |\n"
|
|
||||||
"| Sauron | 12.53 ms (80 fps) | 0 |\n| RajelAran | 6.70 ms | 0 |\n\n"
|
|
||||||
"Oracle has the **best median of the group** and Sauron -- who reported no problem -- has "
|
|
||||||
"the worst. Smooth-but-slower reads as fine; fast-with-hitches reads as broken. The "
|
|
||||||
"distribution is bimodal: **all 97 of Oracle's slow blocks are >50 ms**, nothing in "
|
|
||||||
"between.\n\n"
|
|
||||||
"## Where\n\n"
|
|
||||||
"All 97 stalls are one contiguous window (log lines 30307-32979, 30-32% through the "
|
|
||||||
"session), and it begins exactly at **mission load**, as peer mechs stream in:\n\n"
|
|
||||||
"```\n"
|
|
||||||
"[clipfix] mech 15A1FF38 -> EXTERIOR (lean)\n"
|
|
||||||
"[loadclips] end: fScale=0.8 ...\n"
|
|
||||||
"[spike] dt=0.356 turn=0 thr=0\n"
|
|
||||||
"[rstat] frames=10 avg=103.713ms maxDraw=518.497 maxPresent=7.046 batches=547 culled=226\n"
|
|
||||||
"```\n\n"
|
|
||||||
"`maxDraw=518ms` against `maxPresent=7ms` -- it is **draw time**, not vsync/present.\n\n"
|
|
||||||
"## NOT the instrumentation\n\n"
|
|
||||||
"The standing theory was that accumulated logging is slowing things down. The data says "
|
|
||||||
"no: the **median frame time is unchanged between builds** (5.79 -> 5.87 ms) and log "
|
|
||||||
"volumes are comparable across players (6.1-6.9 MB). Logging would raise the floor "
|
|
||||||
"everywhere, not add isolated 100 ms draw spikes in one window.\n\n"
|
|
||||||
"## Prime suspect\n\n"
|
|
||||||
"The #141 stale-segment-cache fix in this same build. It routes muzzle/beam queries "
|
|
||||||
"through `JointedMover::GetSegmentToWorld`, which -- when joints are modified, i.e. every "
|
|
||||||
"animating frame -- walks the whole segment table and marks every segment dirty, forcing "
|
|
||||||
"the hierarchy to re-derive. That is now on per-frame paths (`BTResolveWeaponMuzzle`, and "
|
|
||||||
"the energy-beam gun-port loop which runs per weapon per mech per frame). With several "
|
|
||||||
"mechs newly loaded it multiplies out. Faithful to the binary, but the binary was not "
|
|
||||||
"calling it this often.\n\n"
|
|
||||||
"**Next:** time `GetSegmentToWorld` per frame and count calls per mech; if it is the "
|
|
||||||
"cause, cache the resolved frame per (mech, segment) for the duration of a frame rather "
|
|
||||||
"than re-deriving per weapon.")
|
|
||||||
|
|
||||||
new("Double KILL credit: one kill counted twice",
|
|
||||||
"Two independent reports on 4.11.857:\n\n"
|
|
||||||
"* RajelAran: *\"in match beginning 20:33, I got a double kill on Oracle. One kill, "
|
|
||||||
"counted twice\"*\n"
|
|
||||||
"* Oracle: *\"12:12AM ET - Oracle killed Conn Man and got 2 kills credited. Rajel also "
|
|
||||||
"the same when killing me.\"*\n\n"
|
|
||||||
"Log cross-check (Oracle/SCREECH): the scoreboard `kills=` reading jumps **2 -> 4**, "
|
|
||||||
"skipping 3.\n\n"
|
|
||||||
"## Suspect (identified 2026-08-07, never fixed)\n\n"
|
|
||||||
"In the type-2 (Kill) arm of the score handler:\n\n"
|
|
||||||
"```c\n"
|
|
||||||
"++killCount; // this player\n"
|
|
||||||
"if (sender_owner) ++sender_owner->killCount; // the mech named by senderMechID\n"
|
|
||||||
"```\n\n"
|
|
||||||
"For type 2, `senderMechID` is the **victim**. The second increment is justified in the "
|
|
||||||
"comment by the SOLO case, where the dummy target has no owning player -- but in MP the "
|
|
||||||
"victim *does* have an owner, so the credit lands twice. In a 1v1 exchange both "
|
|
||||||
"increments can even land on the same scoreboard row, which is what 'one kill counted "
|
|
||||||
"twice' looks like.\n\n"
|
|
||||||
"Needs a 2-node bench confirming the victim's `killCount` moves on a kill.")
|
|
||||||
|
|
||||||
new("PANIC eject scores -499 instead of 0 (double-charged)",
|
|
||||||
"Oracle, 4.11.857, **confirmed twice**: *\"Had 1000 points, leaked out all coolant, hit "
|
|
||||||
"'panic' and ended up with -499 points. Should it have been zero points because "
|
|
||||||
"1000 - 1000 = 0 for ejecting? Why the extra -499 points?\"*\n\n"
|
|
||||||
"The shipped `Role::Default` carries `deathPenalty=500`, and the death tail applies "
|
|
||||||
"`-specialCaseDeathPenalty`. 1000 - 500 = 500, so -499 is roughly **1000 - 500 - 999**, "
|
|
||||||
"i.e. the penalty looks like it is being applied more than once, or the eject path "
|
|
||||||
"charges both the normal death cost and a separate panic cost.\n\n"
|
|
||||||
"Note this is the tail of **#134** (panic eject carried NO score penalty) -- that fix "
|
|
||||||
"landed and now over-charges. The two should be read together.\n\n"
|
|
||||||
"Also from the same report, lower confidence / subjective: *\"Missiles seem to be scoring "
|
|
||||||
"a little low (twin LRM15s scored ~30-40 points)\"* -- worth a damage-vs-score comparison "
|
|
||||||
"rather than a code change on its own.")
|
|
||||||
|
|
||||||
new("REGRESSION: torso no longer auto-recentres in ANY control mode",
|
|
||||||
"Oracle, 4.11.857: *\"Torso is no longer recentering on its own in any mode "
|
|
||||||
"Bas/Mid/Adv.\"*\n\n"
|
|
||||||
"**This is a regression from the control-mode fix in this build.** That change replaced "
|
|
||||||
"the Basic-mode re-centre write from `centerCommand` (@0x208, the HELD-button cell that "
|
|
||||||
"nothing released -- the bug where the centering *fought* the pilot) with "
|
|
||||||
"`recenterActive` (@0x274), a ONE-SHOT that self-clears on settle, matching @004afbe0.\n\n"
|
|
||||||
"The one-shot is what the binary does on the mode CHANGE. What it does not do is provide "
|
|
||||||
"the CONTINUOUS auto-centring that Basic mode is supposed to have per the 1995 manual "
|
|
||||||
"(BAS = joystick turns the mech, **no torso twist**). So the fix removed a behaviour that "
|
|
||||||
"was previously coming -- incorrectly, but coming -- from the stuck cell.\n\n"
|
|
||||||
"Both halves need to be true at once: Basic must hold the torso centred, without the "
|
|
||||||
"held-cell latch that made it fight the stick in Standard/Veteran.\n\n"
|
|
||||||
"The original complaint (Sauron: cycling to advanced and back made the centering fight "
|
|
||||||
"the controls) is fixed and should stay fixed -- verify any change against both.")
|
|
||||||
|
|
||||||
new("Basic-mode elevation limit does not clamp the CURRENT view",
|
|
||||||
"Oracle, 4.11.857: *\"Basic mode does limit downward view to 'half', but only after "
|
|
||||||
"cycling off 'Basic' and then back to 'Basic'. If in Mid or Advanced and move view past "
|
|
||||||
"'half' down and then switch to Basic, nothing happens until I move the control stick in "
|
|
||||||
"the pitch axis and then the view immediately snaps up to 'half' limit.\"*\n\n"
|
|
||||||
"The elevation-limit swap (Basic gets @0x228/@0x22C = full top, HALF bottom; "
|
|
||||||
"Standard/Veteran get @0x230/@0x234) is new in this build and previously was never "
|
|
||||||
"implemented at all. It writes the LIMITS but does not re-clamp `currentElevation`, so an "
|
|
||||||
"already-out-of-range view stays until the next pitch input clamps it.\n\n"
|
|
||||||
"Small fix: clamp current elevation into the new pair at the moment of the swap. Worth "
|
|
||||||
"checking against the binary whether it clamps on switch or leaves it to the sim -- the "
|
|
||||||
"snap-on-next-input behaviour may be authentic.\n\n"
|
|
||||||
"Also: Oracle asked what 'settles the HUD horizon' meant in the release note -- that was "
|
|
||||||
"my wording for Basic raising the HUD's `flickerActive` (@0x2A0) so the horizon "
|
|
||||||
"re-settles with the re-centred torso. Needs a plainer description for testers.")
|
|
||||||
|
|
||||||
new("Overheated generator never came back online; no generator-out alarm on thermal trip",
|
|
||||||
"Oracle, 4.11.857: *\"Noticed a generator that overheated never came back online even "
|
|
||||||
"after cooling off completely. Also noticed the generator out message doesn't sound after "
|
|
||||||
"a generator fails due to overheat. The second and third time I overheated a generator "
|
|
||||||
"for the test it did come back online after cooling halfway, and I heard the generator "
|
|
||||||
"out warning upon the generator coming back online.\"*\n\n"
|
|
||||||
"Two defects, and the intermittency is the interesting part -- the same test gave a stuck "
|
|
||||||
"generator once and a recovering one twice.\n\n"
|
|
||||||
"1. **Stuck offline after thermal trip.** The thermal breaker in `GeneratorSimulation` "
|
|
||||||
"produces `stateAlarm 4` (GeneratorOut); recovery should re-arm once temperature falls. "
|
|
||||||
"Related known nuance (context/decomp-reference.md): the generator sim has NO Ready-case "
|
|
||||||
"recompute, so a generator can hold stale state until *some* transition recomputes it.\n"
|
|
||||||
"2. **Alarm timing.** The generator-out warning did not sound on the trip, but DID sound "
|
|
||||||
"later on the way back online -- so the annunciator appears tied to the wrong edge.\n\n"
|
|
||||||
"Cross-check available: `[techstat] GeneratorX condition 3` (Overheating) transitions are "
|
|
||||||
"in the field logs and are balanced overall, so this is about the stateAlarm/voltage path "
|
|
||||||
"rather than the heat flag.")
|
|
||||||
|
|
||||||
new("PANIC lamp lit: rapid flicker, and engineering + weapon MFD buttons stop responding",
|
|
||||||
"Oracle, 4.11.857: *\"When the panic button lights it flickers very rapidly and the "
|
|
||||||
"engineering and weapon MFDs buttons stop responding. Other MFDs continue to respond to "
|
|
||||||
"button presses (using mouse). I also have mapped controls on my HOTAS for some of those "
|
|
||||||
"MFD buttons and they also stop working when the panic button is lit. Radar, Scoring and "
|
|
||||||
"coolant MFDs keep working.\"*\n\n"
|
|
||||||
"Two symptoms, probably one cause. Arming eject raises the panic-armed mode "
|
|
||||||
"**0x200000**, which drives the physical PANIC lamp (`MakeLinkedLamp`), the eject-mode "
|
|
||||||
"gauge elements, **and keypad routing** -- so eng/weapon buttons going dead while radar/"
|
|
||||||
"scoring/coolant keep working is consistent with the eject mode capturing exactly those "
|
|
||||||
"banks. That part may well be AUTHENTIC (the pod wants your hand on the eject decision, "
|
|
||||||
"not the weapon page).\n\n"
|
|
||||||
"The **rapid flicker is not** authentic: per the decode, the panic lamp is SOLID on arm "
|
|
||||||
"(engine linked-lamp semantic); FLASH is the GaugeAlarm `SetAlertState` overlay, which is "
|
|
||||||
"not authored for panic. A flickering panic lamp suggests the arm state is oscillating -- "
|
|
||||||
"and if it is oscillating, the keypad routing is being torn down and rebuilt repeatedly, "
|
|
||||||
"which would also explain buttons that 'stop responding' rather than cleanly switching.\n\n"
|
|
||||||
"Check `EvaluateEjectPermission` for a condition that chatters at the threshold "
|
|
||||||
"(coolantFrac < 0.05 is a likely candidate while coolant is draining).")
|
|
||||||
|
|
||||||
new("Fade-to-black at mission end is not happening",
|
|
||||||
"Oracle, 4.11.857: *\"Fade to black effect when mission ends is not happening\"*.\n\n"
|
|
||||||
"No log signature to cross-check against -- filing on the report. Worth confirming "
|
|
||||||
"whether this ever worked in a recent build or has been absent for a while.")
|
|
||||||
|
|
||||||
new("Crash at drop end (~23:59 ET, MS-FIREFLY / santo)",
|
|
||||||
"Conn Man / santo, 4.11.857: *\"Did not exit cleanly\"*, and Oracle: *\"Conn Man crashed "
|
|
||||||
"when drop ended ~11:59PM ET\"*.\n\n"
|
|
||||||
"`lastrun_steam.txt` is staged with the field logs. The launch bracketing shows the "
|
|
||||||
"process reaching WinMain normally, so this is a crash at teardown rather than a blocked "
|
|
||||||
"launch.\n\n"
|
|
||||||
"Teardown crashes have bitten this project before (the P5 teardown path). Needs the log "
|
|
||||||
"tail read against the mission-end sequence; the field log for that machine is staged as "
|
|
||||||
"`scratchpad/night14/steam_20260808_b_santo_MSFIREFLY.log`.\n\n"
|
|
||||||
"Also reported for the same player: *\"Conn Man was lagging sometimes during drops and "
|
|
||||||
"that caused the effect of missiles or direct fire exploding/hitting empty space\"* -- "
|
|
||||||
"likely a separate networking symptom, noted here only so the two are not conflated.")
|
|
||||||
|
|
||||||
new("Cannot move after respawn until the throttle is cycled (Abandoned Arena)",
|
|
||||||
"Sauron, 4.11.857: *\"respawned several times in Abandoned Arena and couldn't move until "
|
|
||||||
"throttle down/up or reversing out. Was near map edge and structure.\"*\n\n"
|
|
||||||
"**Two candidate causes, and they must be separated before either is called fixed:**\n\n"
|
|
||||||
"1. **A side effect of the #146 throttle fix in this build.** The virtual lever is now "
|
|
||||||
"released at respawn; a pilot whose PHYSICAL throttle is already forward gets no motion "
|
|
||||||
"until the axis next changes -- i.e. exactly 'couldn't move until throttle down/up'. "
|
|
||||||
"Oracle described the same mechanic positively (*\"throttle is idle and then begins to "
|
|
||||||
"respond to match my throttle position\"*), so the behaviour is real; the question is "
|
|
||||||
"whether it should re-sync immediately instead of on next change.\n"
|
|
||||||
"2. **Geometry.** 'Near map edge and structure' suggests spawning into or against "
|
|
||||||
"collision, which would also read as being stuck.\n\n"
|
|
||||||
"Note this is NOT the same as #137: there the mech is frozen by myomer heat with the "
|
|
||||||
"throttle up, and cycling the throttle does not help. Here cycling the throttle DOES "
|
|
||||||
"free it. The two will look identical to a player, so keep the distinguishing test in "
|
|
||||||
"mind: does throttle-cycling free it (this) or not (#137)?")
|
|
||||||
|
|
||||||
print("created:", made)
|
|
||||||
|
|
||||||
main()
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# =========================================================================
|
|
||||||
# #137 -- "respawns with the myomers overheated ... initially unable to move
|
|
||||||
# until it cools off" (Oracle, across several builds).
|
|
||||||
#
|
|
||||||
# WHY THE OLD BENCH PROVED NOTHING. heatrespawn.sh sampled AT the reset and
|
|
||||||
# found every heat-bearing subsystem at T == startingTemperature, so I closed
|
|
||||||
# #137 as not-a-bug. The field then measured the freeze at 5 of 61 respawns
|
|
||||||
# (~8%) across THREE machines -- throttle up, speedDemand pinned at 0. A
|
|
||||||
# bench that never reproduces the failure is not evidence the failure does not
|
|
||||||
# exist, which is the mistake that closed the ticket.
|
|
||||||
#
|
|
||||||
# WHAT THE DECOMP SAYS (re-read 2026-08-09):
|
|
||||||
# Mech::Reset @0049fb74 walks the roster from index 2 calling vtable +0x28
|
|
||||||
# (slot 10 = ResetToInitialState) on each subsystem, then @0049f788.
|
|
||||||
# Myomers::RTIS @004b8aa4 -> PoweredSubsystem::RTIS @004b0e6c -> ALWAYS
|
|
||||||
# HeatSink::RTIS @004ad760, which does `param_1[0x45] = param_1[0x4f]`
|
|
||||||
# i.e. currentTemperature(@0x114) = startingTemperature(@0x13C).
|
|
||||||
# The freeze itself is the derating curve @004b8ac0:
|
|
||||||
# temp >= degradation(@0x118) -> falls off; temp >= FAILURE(@0x11C) -> 0.0
|
|
||||||
# and 0.0 reaches the mover as the chain MAX -> speedDemand *= 0.
|
|
||||||
# CRUCIALLY: nothing in that reset chain touches Myomers::speedEffect
|
|
||||||
# (@0x31C). It keeps its pre-death value until the myomers next ticks.
|
|
||||||
#
|
|
||||||
# So three explanations survive, and only data separates them:
|
|
||||||
# (a) RESET DIDN'T TAKE -> post-reset T is high (>= fail)
|
|
||||||
# (b) STALE CACHE -> T is at start but speedEffect is still 0
|
|
||||||
# ("<<<< STALE (cold but zero)" in the receipt)
|
|
||||||
# (c) INSTANT RE-HEAT -> T starts at start and climbs back immediately
|
|
||||||
#
|
|
||||||
# THE MEASUREMENT. Mech::Reset now arms a ~4 s post-reset trace sampled where
|
|
||||||
# the mover's multiplier is actually formed:
|
|
||||||
# [myofreeze] at-reset Myomers T=.. deg=.. fail=.. speedEffect=..
|
|
||||||
# [myofreeze] post-reset Myomers T=.. deg=.. fail=.. speedEffect=..
|
|
||||||
# [myofreeze] post-reset CHAIN MAX=0 <<<< FROZEN
|
|
||||||
#
|
|
||||||
# A drives hard (heat) and self-damages to death repeatedly, so deaths land on
|
|
||||||
# a HOT mech -- the field composition. Long run: at ~8% we need many respawns.
|
|
||||||
# =========================================================================
|
|
||||||
set -x
|
|
||||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
|
||||||
cd /c/git/bt411/content || exit 1
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 2
|
|
||||||
rm -f mf137_a.log mf137_b.log mf137_relay.log
|
|
||||||
bt_expert_egg MP.EGG MF137.EGG
|
|
||||||
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" MF137.EGG
|
|
||||||
|
|
||||||
( export BT_DEATH_LOG=1 BT_MP_LOG=1
|
|
||||||
bt_launch mf137_b.log MF137.EGG 0x0C -net 1601 )
|
|
||||||
sleep 2
|
|
||||||
# A: run hot (autodrive) + fire continuously (weapon heat) + die often.
|
|
||||||
( export BT_AUTODRIVE=0.95 BT_SELF_DAMAGE=7
|
|
||||||
export BT_AUTOFIRE=1 BT_AF_MISSILE=1 BT_AF_PERIOD=4
|
|
||||||
export BT_HEAT_LOG=1 BT_DEATH_LOG=1 BT_MP_LOG=1
|
|
||||||
bt_launch mf137_a.log MF137.EGG 0x03 -net 1501 )
|
|
||||||
sleep 5
|
|
||||||
python ../tools/btconsole.py MF137.EGG 127.0.0.1:1501 127.0.0.1:1601 > mf137_relay.log 2>&1 &
|
|
||||||
RELAY=$!
|
|
||||||
sleep 540
|
|
||||||
kill $RELAY 2>/dev/null
|
|
||||||
sleep 3
|
|
||||||
bt_kill_ours; sleep 2; taskkill //F //IM btl4.exe > /dev/null 2>&1; sleep 3
|
|
||||||
|
|
||||||
echo "=================== #137 MYOMER FREEZE ==================="
|
|
||||||
echo -n "respawns: "; grep -ac "Mech::Reset" mf137_a.log
|
|
||||||
echo -n "post-reset FROZEN samples: "; grep -ac "FROZEN" mf137_a.log
|
|
||||||
echo -n "STALE (cold but zero) samples: "; grep -ac "STALE (cold but zero)" mf137_a.log
|
|
||||||
echo
|
|
||||||
echo "--- any reset where the myomers came back AT or OVER the failure temp? ---"
|
|
||||||
python - <<'PY'
|
|
||||||
import re, io
|
|
||||||
bad = 0
|
|
||||||
for ln in io.open(r"C:\git\bt411\content\mf137_a.log", encoding="latin-1", errors="replace"):
|
|
||||||
m = re.search(r"\[myofreeze\] (\S+)\s+(\S+)\s+T=([-\d.e+]+) deg=([-\d.e+]+) fail=([-\d.e+]+)\s+speedEffect=([-\d.e+]+)", ln)
|
|
||||||
if not m:
|
|
||||||
continue
|
|
||||||
when, name, t, deg, fail, se = m.group(1), m.group(2), float(m.group(3)), float(m.group(4)), float(m.group(5)), float(m.group(6))
|
|
||||||
if se <= 1e-4:
|
|
||||||
bad += 1
|
|
||||||
if bad <= 12:
|
|
||||||
why = "TEMP >= fail (reset did not take / re-heated)" if t >= fail else "STALE CACHE (temp fine, effect 0)"
|
|
||||||
print(" %-10s %-12s T=%8.1f fail=%8.1f effect=%.4f -> %s" % (when, name, t, fail, se, why))
|
|
||||||
print(" zero-effect samples: %d" % bad)
|
|
||||||
PY
|
|
||||||
echo
|
|
||||||
echo "--- the first frozen episode in context ---"
|
|
||||||
grep -aE "Mech::Reset|myofreeze" mf137_a.log | grep -aB2 -A6 "FROZEN" | head -20
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# =========================================================================
|
|
||||||
# #137 -- "respawns with the myomers overheated ... initially unable to move
|
|
||||||
# until it cools off" (Oracle, across several builds).
|
|
||||||
#
|
|
||||||
# WHY THE OLD BENCH PROVED NOTHING. heatrespawn.sh sampled AT the reset and
|
|
||||||
# found every heat-bearing subsystem at T == startingTemperature, so I closed
|
|
||||||
# #137 as not-a-bug. The field then measured the freeze at 5 of 61 respawns
|
|
||||||
# (~8%) across THREE machines -- throttle up, speedDemand pinned at 0. A
|
|
||||||
# bench that never reproduces the failure is not evidence the failure does not
|
|
||||||
# exist, which is the mistake that closed the ticket.
|
|
||||||
#
|
|
||||||
# WHAT THE DECOMP SAYS (re-read 2026-08-09):
|
|
||||||
# Mech::Reset @0049fb74 walks the roster from index 2 calling vtable +0x28
|
|
||||||
# (slot 10 = ResetToInitialState) on each subsystem, then @0049f788.
|
|
||||||
# Myomers::RTIS @004b8aa4 -> PoweredSubsystem::RTIS @004b0e6c -> ALWAYS
|
|
||||||
# HeatSink::RTIS @004ad760, which does `param_1[0x45] = param_1[0x4f]`
|
|
||||||
# i.e. currentTemperature(@0x114) = startingTemperature(@0x13C).
|
|
||||||
# The freeze itself is the derating curve @004b8ac0:
|
|
||||||
# temp >= degradation(@0x118) -> falls off; temp >= FAILURE(@0x11C) -> 0.0
|
|
||||||
# and 0.0 reaches the mover as the chain MAX -> speedDemand *= 0.
|
|
||||||
# CRUCIALLY: nothing in that reset chain touches Myomers::speedEffect
|
|
||||||
# (@0x31C). It keeps its pre-death value until the myomers next ticks.
|
|
||||||
#
|
|
||||||
# So three explanations survive, and only data separates them:
|
|
||||||
# (a) RESET DIDN'T TAKE -> post-reset T is high (>= fail)
|
|
||||||
# (b) STALE CACHE -> T is at start but speedEffect is still 0
|
|
||||||
# ("<<<< STALE (cold but zero)" in the receipt)
|
|
||||||
# (c) INSTANT RE-HEAT -> T starts at start and climbs back immediately
|
|
||||||
#
|
|
||||||
# THE MEASUREMENT. Mech::Reset now arms a ~4 s post-reset trace sampled where
|
|
||||||
# the mover's multiplier is actually formed:
|
|
||||||
# [myofreeze] at-reset Myomers T=.. deg=.. fail=.. speedEffect=..
|
|
||||||
# [myofreeze] post-reset Myomers T=.. deg=.. fail=.. speedEffect=..
|
|
||||||
# [myofreeze] post-reset CHAIN MAX=0 <<<< FROZEN
|
|
||||||
#
|
|
||||||
# A drives hard (heat) and self-damages to death repeatedly, so deaths land on
|
|
||||||
# a HOT mech -- the field composition. Long run: at ~8% we need many respawns.
|
|
||||||
# =========================================================================
|
|
||||||
set -x
|
|
||||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
|
||||||
cd /c/git/bt411/content || exit 1
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 2
|
|
||||||
rm -f mf137_a.log mf137_b.log mf137_relay.log
|
|
||||||
bt_expert_egg MP.EGG MF137.EGG
|
|
||||||
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" MF137.EGG
|
|
||||||
|
|
||||||
( export BT_DEATH_LOG=1 BT_MP_LOG=1
|
|
||||||
bt_launch mf137_b.log MF137.EGG 0x0C -net 1601 )
|
|
||||||
sleep 2
|
|
||||||
# A: run hot (autodrive) + fire continuously (weapon heat) + die often.
|
|
||||||
( export BT_AUTODRIVE=0.5 BT_SELF_DAMAGE=7
|
|
||||||
|
|
||||||
export BT_HEAT_LOG=1 BT_DEATH_LOG=1 BT_MP_LOG=1
|
|
||||||
bt_launch mf137_a.log MF137.EGG 0x03 -net 1501 )
|
|
||||||
sleep 5
|
|
||||||
python ../tools/btconsole.py MF137.EGG 127.0.0.1:1501 127.0.0.1:1601 > mf137_relay.log 2>&1 &
|
|
||||||
RELAY=$!
|
|
||||||
sleep 330
|
|
||||||
kill $RELAY 2>/dev/null
|
|
||||||
sleep 3
|
|
||||||
bt_kill_ours; sleep 2; taskkill //F //IM btl4.exe > /dev/null 2>&1; sleep 3
|
|
||||||
|
|
||||||
echo "=================== #137 MYOMER FREEZE ==================="
|
|
||||||
echo -n "respawns: "; grep -ac "Mech::Reset" mf137_a.log
|
|
||||||
echo -n "post-reset FROZEN samples: "; grep -ac "FROZEN" mf137_a.log
|
|
||||||
echo -n "STALE (cold but zero) samples: "; grep -ac "STALE (cold but zero)" mf137_a.log
|
|
||||||
echo
|
|
||||||
echo "--- any reset where the myomers came back AT or OVER the failure temp? ---"
|
|
||||||
python - <<'PY'
|
|
||||||
import re, io
|
|
||||||
bad = 0
|
|
||||||
for ln in io.open(r"C:\git\bt411\content\mf137_a.log", encoding="latin-1", errors="replace"):
|
|
||||||
m = re.search(r"\[myofreeze\] (\S+)\s+(\S+)\s+T=([-\d.e+]+) deg=([-\d.e+]+) fail=([-\d.e+]+)\s+speedEffect=([-\d.e+]+)", ln)
|
|
||||||
if not m:
|
|
||||||
continue
|
|
||||||
when, name, t, deg, fail, se = m.group(1), m.group(2), float(m.group(3)), float(m.group(4)), float(m.group(5)), float(m.group(6))
|
|
||||||
if se <= 1e-4:
|
|
||||||
bad += 1
|
|
||||||
if bad <= 12:
|
|
||||||
why = "TEMP >= fail (reset did not take / re-heated)" if t >= fail else "STALE CACHE (temp fine, effect 0)"
|
|
||||||
print(" %-10s %-12s T=%8.1f fail=%8.1f effect=%.4f -> %s" % (when, name, t, fail, se, why))
|
|
||||||
print(" zero-effect samples: %d" % bad)
|
|
||||||
PY
|
|
||||||
echo
|
|
||||||
echo "--- the first frozen episode in context ---"
|
|
||||||
grep -aE "Mech::Reset|myofreeze" mf137_a.log | grep -aB2 -A6 "FROZEN" | head -20
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# =========================================================================
|
|
||||||
# #137 -- "respawns with the myomers overheated ... initially unable to move
|
|
||||||
# until it cools off" (Oracle, across several builds).
|
|
||||||
#
|
|
||||||
# WHY THE OLD BENCH PROVED NOTHING. heatrespawn.sh sampled AT the reset and
|
|
||||||
# found every heat-bearing subsystem at T == startingTemperature, so I closed
|
|
||||||
# #137 as not-a-bug. The field then measured the freeze at 5 of 61 respawns
|
|
||||||
# (~8%) across THREE machines -- throttle up, speedDemand pinned at 0. A
|
|
||||||
# bench that never reproduces the failure is not evidence the failure does not
|
|
||||||
# exist, which is the mistake that closed the ticket.
|
|
||||||
#
|
|
||||||
# WHAT THE DECOMP SAYS (re-read 2026-08-09):
|
|
||||||
# Mech::Reset @0049fb74 walks the roster from index 2 calling vtable +0x28
|
|
||||||
# (slot 10 = ResetToInitialState) on each subsystem, then @0049f788.
|
|
||||||
# Myomers::RTIS @004b8aa4 -> PoweredSubsystem::RTIS @004b0e6c -> ALWAYS
|
|
||||||
# HeatSink::RTIS @004ad760, which does `param_1[0x45] = param_1[0x4f]`
|
|
||||||
# i.e. currentTemperature(@0x114) = startingTemperature(@0x13C).
|
|
||||||
# The freeze itself is the derating curve @004b8ac0:
|
|
||||||
# temp >= degradation(@0x118) -> falls off; temp >= FAILURE(@0x11C) -> 0.0
|
|
||||||
# and 0.0 reaches the mover as the chain MAX -> speedDemand *= 0.
|
|
||||||
# CRUCIALLY: nothing in that reset chain touches Myomers::speedEffect
|
|
||||||
# (@0x31C). It keeps its pre-death value until the myomers next ticks.
|
|
||||||
#
|
|
||||||
# So three explanations survive, and only data separates them:
|
|
||||||
# (a) RESET DIDN'T TAKE -> post-reset T is high (>= fail)
|
|
||||||
# (b) STALE CACHE -> T is at start but speedEffect is still 0
|
|
||||||
# ("<<<< STALE (cold but zero)" in the receipt)
|
|
||||||
# (c) INSTANT RE-HEAT -> T starts at start and climbs back immediately
|
|
||||||
#
|
|
||||||
# THE MEASUREMENT. Mech::Reset now arms a ~4 s post-reset trace sampled where
|
|
||||||
# the mover's multiplier is actually formed:
|
|
||||||
# [myofreeze] at-reset Myomers T=.. deg=.. fail=.. speedEffect=..
|
|
||||||
# [myofreeze] post-reset Myomers T=.. deg=.. fail=.. speedEffect=..
|
|
||||||
# [myofreeze] post-reset CHAIN MAX=0 <<<< FROZEN
|
|
||||||
#
|
|
||||||
# A drives hard (heat) and self-damages to death repeatedly, so deaths land on
|
|
||||||
# a HOT mech -- the field composition. Long run: at ~8% we need many respawns.
|
|
||||||
# =========================================================================
|
|
||||||
set -x
|
|
||||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
|
||||||
cd /c/git/bt411/content || exit 1
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 2
|
|
||||||
rm -f mf137_a.log mf137_b.log mf137_relay.log
|
|
||||||
bt_expert_egg MP.EGG MF137.EGG
|
|
||||||
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" MF137.EGG
|
|
||||||
|
|
||||||
( export BT_DEATH_LOG=1 BT_MP_LOG=1
|
|
||||||
bt_launch mf137_b.log MF137.EGG 0x0C -net 1601 )
|
|
||||||
sleep 2
|
|
||||||
# A: run hot (autodrive) + fire continuously (weapon heat) + die often.
|
|
||||||
( export BT_AUTODRIVE=0.95 BT_SELF_DAMAGE=7
|
|
||||||
export BT_AUTOFIRE=1 BT_AF_MISSILE=1 BT_AF_PERIOD=4
|
|
||||||
export BT_HEAT_LOG=1 BT_MYO_LOG=1 BT_DEATH_LOG=1 BT_MP_LOG=1
|
|
||||||
bt_launch mf137_a.log MF137.EGG 0x03 -net 1501 )
|
|
||||||
sleep 5
|
|
||||||
python ../tools/btconsole.py MF137.EGG 127.0.0.1:1501 127.0.0.1:1601 > mf137_relay.log 2>&1 &
|
|
||||||
RELAY=$!
|
|
||||||
sleep 540
|
|
||||||
kill $RELAY 2>/dev/null
|
|
||||||
sleep 3
|
|
||||||
bt_kill_ours; sleep 2; taskkill //F //IM btl4.exe > /dev/null 2>&1; sleep 3
|
|
||||||
|
|
||||||
echo "=================== #137 MYOMER FREEZE ==================="
|
|
||||||
echo -n "respawns: "; grep -ac "Mech::Reset" mf137_a.log
|
|
||||||
echo -n "post-reset FROZEN samples: "; grep -ac "FROZEN" mf137_a.log
|
|
||||||
echo -n "STALE (cold but zero) samples: "; grep -ac "STALE (cold but zero)" mf137_a.log
|
|
||||||
echo
|
|
||||||
echo "--- any reset where the myomers came back AT or OVER the failure temp? ---"
|
|
||||||
python - <<'PY'
|
|
||||||
import re, io
|
|
||||||
bad = 0
|
|
||||||
for ln in io.open(r"C:\git\bt411\content\mf137_a.log", encoding="latin-1", errors="replace"):
|
|
||||||
m = re.search(r"\[myofreeze\] (\S+)\s+(\S+)\s+T=([-\d.e+]+) deg=([-\d.e+]+) fail=([-\d.e+]+)\s+speedEffect=([-\d.e+]+)", ln)
|
|
||||||
if not m:
|
|
||||||
continue
|
|
||||||
when, name, t, deg, fail, se = m.group(1), m.group(2), float(m.group(3)), float(m.group(4)), float(m.group(5)), float(m.group(6))
|
|
||||||
if se <= 1e-4:
|
|
||||||
bad += 1
|
|
||||||
if bad <= 12:
|
|
||||||
why = "TEMP >= fail (reset did not take / re-heated)" if t >= fail else "STALE CACHE (temp fine, effect 0)"
|
|
||||||
print(" %-10s %-12s T=%8.1f fail=%8.1f effect=%.4f -> %s" % (when, name, t, fail, se, why))
|
|
||||||
print(" zero-effect samples: %d" % bad)
|
|
||||||
PY
|
|
||||||
echo
|
|
||||||
echo "--- the first frozen episode in context ---"
|
|
||||||
grep -aE "Mech::Reset|myofreeze" mf137_a.log | grep -aB2 -A6 "FROZEN" | head -20
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# #150/#151: count type-2 arm executions per kill + measure the eject score arithmetic
|
|
||||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
|
||||||
cd /c/git/bt411/content || exit 1
|
|
||||||
taskkill //F //IM btl4.exe >/dev/null 2>&1; sleep 3
|
|
||||||
rm -f sc_a.log sc_b.log sc_r.log
|
|
||||||
mkdir -p /c/git/bt411/scratchpad/night14/mlbak && mv matchlog_*.txt /c/git/bt411/scratchpad/night14/mlbak/ 2>/dev/null
|
|
||||||
bt_expert_egg MP.EGG SC.EGG
|
|
||||||
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" SC.EGG
|
|
||||||
# B: unarmed slow target, respawns on death
|
|
||||||
( export BT_GOTO=enemy BT_GOTO_STOP=80 BT_SCORE_LOG=1 BT_DEATH_LOG=1 BT_MATCHLOG=1 BT_MP_LOG=1
|
|
||||||
bt_launch sc_b.log SC.EGG 0x0C -net 1601 )
|
|
||||||
sleep 2
|
|
||||||
# A: kills B with autofire, then panic-ejects late
|
|
||||||
( export BT_GOTO=enemy BT_GOTO_STOP=100 BT_AUTOFIRE=1 BT_AF_MISSILE=1 BT_AF_PERIOD=4
|
|
||||||
export BT_EJECT_AT=9000
|
|
||||||
export BT_SCORE_LOG=1 BT_DEATH_LOG=1 BT_MATCHLOG=1 BT_MP_LOG=1
|
|
||||||
bt_launch sc_a.log SC.EGG 0x03 -net 1501 )
|
|
||||||
sleep 5
|
|
||||||
python ../tools/btconsole.py SC.EGG 127.0.0.1:1501 127.0.0.1:1601 > sc_r.log 2>&1 &
|
|
||||||
R=$!; sleep 300; kill $R 2>/dev/null; sleep 2
|
|
||||||
bt_kill_ours; sleep 2; taskkill //F //IM btl4.exe >/dev/null 2>&1
|
|
||||||
echo RESULT
|
|
||||||
echo "--- B deaths (kills A should have earned) ---"
|
|
||||||
grep -ac "Mech::Reset" sc_b.log
|
|
||||||
echo "--- A: kill-arm executions + score sequence (matchlog) ---"
|
|
||||||
grep -ah "SCORE\|KILL\|EJECT\|DEATH" matchlog_*.txt 2>/dev/null | head -25
|
|
||||||
echo "--- A: scorepost receipts (producer side) ---"
|
|
||||||
grep -a "scorepost" sc_a.log | head -10
|
|
||||||
grep -a "scorepost" sc_b.log | head -10
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# =========================================================================
|
|
||||||
# #149 -- 817 -> 857 stall regression (Oracle: 97 blocks >50ms, all in the
|
|
||||||
# mission-load window, maxDraw up to 518ms; zero such blocks on 817, same
|
|
||||||
# machine). My filed suspect was the #141 segment-cache sweep -- but that is
|
|
||||||
# a HYPOTHESIS, and #137 just taught us what plausible-but-unmeasured
|
|
||||||
# theories are worth. So: measure first.
|
|
||||||
#
|
|
||||||
# INSTRUMENTATION (this build):
|
|
||||||
# [segperf] calls=<GetSegmentToWorld entries> dirty=<mark-every-segment
|
|
||||||
# invalidation passes> ms=<time inside the accessor> -- per rstat window,
|
|
||||||
# BT_PERF_LOG-gated. The DIRTY count is the tell: if the beam path
|
|
||||||
# multiplies invalidations, dirty >> mechs-per-frame and ms tracks maxDraw.
|
|
||||||
#
|
|
||||||
# A/B: identical 2-node runs, beams firing continuously.
|
|
||||||
# run A: default (swept behaviour: beam muzzle via GetSegmentToWorld)
|
|
||||||
# run B: BT_BEAM_SEGFRESH=0 (pre-sweep plain compose at the BEAM site only)
|
|
||||||
# If A's stalls/dirty collapse in B, the beam-site sweep is the regression.
|
|
||||||
# If A == B and both are clean, the suspect is EXONERATED and the real 857
|
|
||||||
# load-stall cause is elsewhere (next: diff the load path).
|
|
||||||
# =========================================================================
|
|
||||||
set -x
|
|
||||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
|
||||||
cd /c/git/bt411/content || exit 1
|
|
||||||
|
|
||||||
run_one () { # $1 = tag, $2 = extra env value for BT_BEAM_SEGFRESH
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 2
|
|
||||||
rm -f sp_${1}_a.log sp_${1}_b.log sp_${1}_relay.log
|
|
||||||
bt_expert_egg MP.EGG SP.EGG
|
|
||||||
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" SP.EGG
|
|
||||||
( export BT_GOTO=enemy BT_GOTO_STOP=120 BT_AUTOFIRE=1 BT_AF_PERIOD=3
|
|
||||||
export BT_MP_LOG=1
|
|
||||||
bt_launch sp_${1}_b.log SP.EGG 0x0C -net 1601 )
|
|
||||||
sleep 2
|
|
||||||
( export BT_GOTO=enemy BT_GOTO_STOP=120 BT_AUTOFIRE=1 BT_AF_PERIOD=3
|
|
||||||
export BT_PERF_LOG=1 BT_MP_LOG=1
|
|
||||||
export BT_BEAM_SEGFRESH=${2}
|
|
||||||
bt_launch sp_${1}_a.log SP.EGG 0x03 -net 1501 )
|
|
||||||
sleep 5
|
|
||||||
python ../tools/btconsole.py SP.EGG 127.0.0.1:1501 127.0.0.1:1601 > sp_${1}_relay.log 2>&1 &
|
|
||||||
local RELAY=$!
|
|
||||||
sleep 260
|
|
||||||
kill $RELAY 2>/dev/null
|
|
||||||
sleep 3
|
|
||||||
bt_kill_ours; sleep 2; taskkill //F //IM btl4.exe > /dev/null 2>&1; sleep 3
|
|
||||||
}
|
|
||||||
|
|
||||||
run_one fresh 1
|
|
||||||
run_one legacy 0
|
|
||||||
|
|
||||||
echo "=================== #149 SEGPERF A/B ==================="
|
|
||||||
for t in fresh legacy; do
|
|
||||||
echo "--- $t ---"
|
|
||||||
python - <<PY
|
|
||||||
import re, io
|
|
||||||
v=[]; seg=[]
|
|
||||||
for ln in io.open(r"C:\git\bt411\content\sp_${t}_a.log", encoding="latin-1", errors="replace"):
|
|
||||||
m=re.search(r"\[rstat\] frames=\d+ avg=([\d.]+)ms maxDraw=([\d.]+)", ln)
|
|
||||||
if m: v.append((float(m.group(1)), float(m.group(2))))
|
|
||||||
m=re.search(r"\[segperf\] calls=(\d+) dirty=(\d+) ms=([\d.]+)", ln)
|
|
||||||
if m: seg.append((int(m.group(1)), int(m.group(2)), float(m.group(3))))
|
|
||||||
if v:
|
|
||||||
import statistics
|
|
||||||
avgs=[a for a,_ in v]; draws=[d for _,d in v]
|
|
||||||
stall=sum(1 for a,_ in v if a>50)
|
|
||||||
print(" rstat blocks=%d avg-med=%.2fms maxDraw-med=%.2f maxDraw-worst=%.1f blocks>50ms=%d"
|
|
||||||
% (len(v), statistics.median(avgs), statistics.median(draws), max(draws), stall))
|
|
||||||
if seg:
|
|
||||||
c=[x[0] for x in seg]; d=[x[1] for x in seg]; ms=[x[2] for x in seg]
|
|
||||||
print(" segperf windows=%d calls-med=%d dirty-med=%d ms-med=%.2f ms-worst=%.2f"
|
|
||||||
% (len(seg), sorted(c)[len(c)//2], sorted(d)[len(d)//2], sorted(ms)[len(ms)//2], max(ms)))
|
|
||||||
PY
|
|
||||||
done
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
"""#137: post the clean mechanism write-up (the inline attempt was shell-mangled)."""
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0, r"C:\git\bt411\scratchpad\night7")
|
|
||||||
import gitea
|
|
||||||
|
|
||||||
BODY = """*(Reposting -- the previous comment was mangled in transit. This is the readable version.)*
|
|
||||||
|
|
||||||
**MECHANISM SOLVED -- and it is not the respawn.** Controlled A/B on one bench, only the load differs:
|
|
||||||
|
|
||||||
| load | respawns | post-reset runaway |
|
|
||||||
|---|---|---|
|
|
||||||
| 0.95 throttle + continuous missile fire | 4 | **17 frozen samples, T climbs 77 -> ~11,600** |
|
|
||||||
| 0.50 throttle, no weapons | 2 | **0** |
|
|
||||||
|
|
||||||
Measured, not argued:
|
|
||||||
|
|
||||||
* **The reset works.** `T == startingTemperature` (77) at every single reset, in both runs.
|
|
||||||
`HeatSink::RTIS @004ad760` does `param_1[0x45] = param_1[0x4f]`, and our port matches --
|
|
||||||
additionally resetting `heatEnergy`, which it must, since the sim derives
|
|
||||||
`currentTemperature = heatEnergy / thermalMass`.
|
|
||||||
* **The stale cache is real but harmless.** Nothing in the reset chain writes
|
|
||||||
`Myomers::speedEffect` (@0x31C) -- `Myomers::RTIS @004b8aa4` only chains to the
|
|
||||||
PoweredSubsystem one -- so it survives the reset reading 0. It self-heals on the very next
|
|
||||||
tick (`T=77.13` -> `speedEffect=1`). Not the bug.
|
|
||||||
* **With ordinary load the myomers never approach `failT=2000` after a respawn.**
|
|
||||||
|
|
||||||
## What players are actually experiencing
|
|
||||||
|
|
||||||
Run hard -> myomers pass `failT=2000` -> the derating curve `@004b8ac0` returns 0.0 -> chain
|
|
||||||
MAX 0 -> `speedDemand *= 0` -> bogged down -> die, often *because* bogged down. The respawn
|
|
||||||
correctly resets to 77. Resume high throttle and firing and it climbs back over the cliff within
|
|
||||||
seconds -- which reads as "respawned with the heat bar maxed".
|
|
||||||
|
|
||||||
That also explains the intermittency (5 of 61 field respawns, ~8%): it tracks how hard you were
|
|
||||||
driving into and out of the respawn, not the respawn itself.
|
|
||||||
|
|
||||||
Oracle's "loop 5 and generator D heating up as all the excess heat goes into the loop" is
|
|
||||||
confirmed literally -- the myomers link to **Condenser5** (`mass=250000 k=190000`).
|
|
||||||
|
|
||||||
## The remaining defect: the climb RATE
|
|
||||||
|
|
||||||
Overshooting roughly 6x past a cliff the design treats as coolant-managed (`degradeT=1000`
|
|
||||||
governor onset, `failT=2000`) is not a lever a player can work with. That is the real ticket now.
|
|
||||||
|
|
||||||
Suspect under review, **not yet proven**: the kinetic term. The binary `@004b8d18` applies **no**
|
|
||||||
`time_slice` to it (`fVar5 * fVar1`), while the climb and accel terms both carry `param_2` -- i.e.
|
|
||||||
it is a per-frame energy add at the pod's fixed ~28 Hz. Our port rate-normalises it
|
|
||||||
(`work * (time_slice * 28)`), which agrees per-second at any frame rate, so this is not yet a
|
|
||||||
demonstrated discrepancy. Next step is a term-by-term dump (`BT_MYO_LOG`) against the authored
|
|
||||||
tuning: VelocityEfficiency 0.995, AccelerationEfficiency 0.8, thermalMass 2.5e5.
|
|
||||||
|
|
||||||
**Retitling suggestion:** this should stop being "respawn comes back hot" and become
|
|
||||||
"myomer heat rate under sustained load overshoots the failure cliff"."""
|
|
||||||
|
|
||||||
gitea.comment(137, BODY)
|
|
||||||
gitea.call("/issues/137", method="PATCH", payload={
|
|
||||||
"title": "Myomer heat rate under sustained load overshoots the failure cliff (was: respawn came back with MYOMERS heat MAXED)"})
|
|
||||||
print("posted + retitled #137")
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
# Apply the ALPHA-MR pod kit to a BT411 install's content directory.
|
|
||||||
#
|
|
||||||
# WHY THIS EXISTS: the frozen rig config lives in content\environ.ini and
|
|
||||||
# glass_layout.cfg -- both INSIDE the versioned install folder, and neither is
|
|
||||||
# shipped in the zip (environ.ini is generated on first run). So extracting a
|
|
||||||
# new build gives you a cab that comes up wrong with no error anywhere. The
|
|
||||||
# MASTERS live beside this script at a stable path; this pushes them into
|
|
||||||
# whichever install you point it at. Idempotent -- safe to re-run.
|
|
||||||
param([Parameter(Mandatory=$true)][string]$Content)
|
|
||||||
|
|
||||||
$here = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
|
||||||
$prof = Join-Path $here 'podprofile.ini'
|
|
||||||
$lay = Join-Path $here 'glass_layout.cfg'
|
|
||||||
$ini = Join-Path $Content 'environ.ini'
|
|
||||||
|
|
||||||
if (-not (Test-Path $Content)) { Write-Output "NO SUCH CONTENT DIR: $Content"; exit 1 }
|
|
||||||
if (-not (Test-Path $prof)) { Write-Output "MISSING MASTER: $prof"; exit 1 }
|
|
||||||
|
|
||||||
# --- the env block: replace anything between the markers, keep the rest ------
|
|
||||||
$block = Get-Content $prof
|
|
||||||
$keep = @()
|
|
||||||
if (Test-Path $ini) {
|
|
||||||
$inBlock = $false
|
|
||||||
foreach ($line in (Get-Content $ini)) {
|
|
||||||
if ($line -match '^# ==== BT411 POD PROFILE') { $inBlock = $true; continue }
|
|
||||||
if ($line -match '^# ==== END BT411 POD PROFILE') { $inBlock = $false; continue }
|
|
||||||
if (-not $inBlock) { $keep += $line }
|
|
||||||
}
|
|
||||||
# ⚠ `-gt 1`, NOT `-gt 0` (fixed 2026-08-08). With `-gt 0`, once $keep trims
|
|
||||||
# down to a SINGLE blank line, $keep.Count-2 is -1 and PowerShell's
|
|
||||||
# $keep[0..-1] returns TWO elements (index 0 and index -1 = the last) instead
|
|
||||||
# of shrinking -- so the array grows and this loops FOREVER, RSS climbing past
|
|
||||||
# 60 MB. It fires whenever everything outside environ.ini's marker block is
|
|
||||||
# blank, i.e. any environ.ini that was ALREADY kitted -- exactly what you get
|
|
||||||
# carrying config forward from the previous install. Symptom on the cab: a
|
|
||||||
# blank cmd console, BT411Run stuck "Running", no btl4.exe and no podrun.log,
|
|
||||||
# and each hung run holds environ.ini so every later attempt blocks too.
|
|
||||||
while ($keep.Count -gt 1 -and $keep[-1].Trim() -eq '') { $keep = $keep[0..($keep.Count-2)] }
|
|
||||||
}
|
|
||||||
($keep + @('') + $block) | Set-Content $ini -Encoding ascii
|
|
||||||
|
|
||||||
# --- the layout: the master always wins -------------------------------------
|
|
||||||
# Re-tuning the cab means editing the MASTER beside this script, not the copy
|
|
||||||
# in the install -- a BT_GLASS_LAYOUT=save drag inside an install is overwritten
|
|
||||||
# on the next apply, on purpose. One place to look when a panel moves.
|
|
||||||
if (Test-Path $lay) { Copy-Item $lay (Join-Path $Content 'glass_layout.cfg') -Force }
|
|
||||||
|
|
||||||
# --- the pod test mission ----------------------------------------------------
|
|
||||||
# PODTEST.EGG is NOT in the repo -- it was made on the cart, so a fresh extract
|
|
||||||
# has no mission for runpod.bat to launch (found the first time a new build was
|
|
||||||
# dropped: the launcher ran and nothing came up). Carry it in the kit.
|
|
||||||
$egg = Join-Path $here 'PODTEST.EGG'
|
|
||||||
if (Test-Path $egg) { Copy-Item $egg (Join-Path $Content 'PODTEST.EGG') -Force }
|
|
||||||
|
|
||||||
Write-Output "pod kit applied to $Content"
|
|
||||||
Get-Content $ini | Select-String '^(BT_|L4)' | ForEach-Object { " " + $_.Line }
|
|
||||||
if (Test-Path $lay) { Get-Content (Join-Path $Content 'glass_layout.cfg') | Select-String '^[A-Z]' | ForEach-Object { " " + $_.Line } }
|
|
||||||
|
|
||||||
@@ -82,94 +82,6 @@ try {
|
|||||||
} catch { W " (EDID query failed -- normal over some remote sessions: $_)" }
|
} catch { W " (EDID query failed -- normal over some remote sessions: $_)" }
|
||||||
W ""
|
W ""
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# BOOT-STABLE panel identity. Windows renumbers \\.\DISPLAYn and reorders the
|
|
||||||
# enumeration when a panel is power-cycled or re-cabled, so binding the pod by
|
|
||||||
# index or device name is fragile (Nick, 2026-08-07: "the order changed ... even
|
|
||||||
# if the visual desktop tool looks the same"). Bind by the panel's own EDID
|
|
||||||
# identity instead: glass_layout.cfg accepts `monitor:id:<fragment>`.
|
|
||||||
#
|
|
||||||
# This deliberately calls the SAME Win32 API the game does (EnumDisplayDevices
|
|
||||||
# on the display's MONITOR child) rather than the WmiMonitorID above -- if the
|
|
||||||
# probe and the engine read different sources, the fragments printed here might
|
|
||||||
# not be what the engine actually matches against.
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
W "---- BOOT-STABLE panel identity (paste these into glass_layout.cfg) ----"
|
|
||||||
try {
|
|
||||||
if (-not ("BTDisp" -as [type])) {
|
|
||||||
Add-Type -TypeDefinition @'
|
|
||||||
using System;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
public class BTDisp {
|
|
||||||
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)]
|
|
||||||
public struct DISPLAY_DEVICE {
|
|
||||||
public int cb;
|
|
||||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=32)] public string DeviceName;
|
|
||||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)] public string DeviceString;
|
|
||||||
public int StateFlags;
|
|
||||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)] public string DeviceID;
|
|
||||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)] public string DeviceKey;
|
|
||||||
}
|
|
||||||
[DllImport("user32.dll", CharSet=CharSet.Ansi)]
|
|
||||||
public static extern bool EnumDisplayDevicesA(string dev, uint num, ref DISPLAY_DEVICE dd, uint flags);
|
|
||||||
public static string MonitorId(string display) {
|
|
||||||
DISPLAY_DEVICE dd = new DISPLAY_DEVICE();
|
|
||||||
dd.cb = Marshal.SizeOf(typeof(DISPLAY_DEVICE));
|
|
||||||
// 0x1 = EDD_GET_DEVICE_INTERFACE_NAME (richer path, includes connector UID)
|
|
||||||
if (!EnumDisplayDevicesA(display, 0, ref dd, 0x1)) {
|
|
||||||
dd = new DISPLAY_DEVICE();
|
|
||||||
dd.cb = Marshal.SizeOf(typeof(DISPLAY_DEVICE));
|
|
||||||
if (!EnumDisplayDevicesA(display, 0, ref dd, 0)) return "";
|
|
||||||
}
|
|
||||||
return dd.DeviceID;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'@
|
|
||||||
}
|
|
||||||
$codes = @{}
|
|
||||||
$rows = @()
|
|
||||||
foreach ($s in [System.Windows.Forms.Screen]::AllScreens) {
|
|
||||||
$sid = [BTDisp]::MonitorId($s.DeviceName)
|
|
||||||
# EDID PnP code = 3 letters + 4 hex digits (AUO10ED, DEL4231)
|
|
||||||
$code = ""
|
|
||||||
if ($sid -match '[\\#\?]([A-Za-z]{3}[0-9A-Fa-f]{4})[\\#\?]') { $code = $Matches[1] }
|
|
||||||
elseif ($sid -match '([A-Za-z]{3}[0-9A-Fa-f]{4})') { $code = $Matches[1] }
|
|
||||||
$rows += [pscustomobject]@{ Dev=$s.DeviceName; Prim=$s.Primary; Sid=$sid; Code=$code
|
|
||||||
X=$s.Bounds.X; Y=$s.Bounds.Y; W=$s.Bounds.Width; H=$s.Bounds.Height }
|
|
||||||
if ($code -ne "") { $codes[$code] = 1 + $(if ($codes.ContainsKey($code)) { $codes[$code] } else { 0 }) }
|
|
||||||
}
|
|
||||||
foreach ($r in $rows) {
|
|
||||||
W (" {0}{1} {2},{3} {4}x{5}" -f $r.Dev, $(if ($r.Prim) { " *PRIMARY*" } else { "" }), $r.X, $r.Y, $r.W, $r.H)
|
|
||||||
W (" stable-id : {0}" -f $(if ($r.Sid) { $r.Sid } else { "(unavailable)" }))
|
|
||||||
if ($r.Code -ne "" -and $codes[$r.Code] -gt 1) {
|
|
||||||
# Same model on more than one output: the EDID code alone is ambiguous.
|
|
||||||
# The connector UID in the tail is what separates them.
|
|
||||||
$uid = ""
|
|
||||||
if ($r.Sid -match '(UID[0-9]+)') { $uid = $Matches[1] }
|
|
||||||
if ($uid -ne "") {
|
|
||||||
W (" cfg form : monitor:id:{0} <-- code '{1}' is on {2} panels, so use the UID" -f $uid, $r.Code, $codes[$r.Code])
|
|
||||||
} else {
|
|
||||||
W (" cfg form : (AMBIGUOUS -- '{0}' appears on {1} panels and no UID found;" -f $r.Code, $codes[$r.Code])
|
|
||||||
W " use a longer unique substring of stable-id above)"
|
|
||||||
}
|
|
||||||
} elseif ($r.Code -ne "") {
|
|
||||||
W (" cfg form : monitor:id:{0}" -f $r.Code)
|
|
||||||
} else {
|
|
||||||
W " cfg form : (no EDID code parsed -- use a substring of stable-id)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$dupes = ($codes.GetEnumerator() | Where-Object { $_.Value -gt 1 } | Measure-Object).Count
|
|
||||||
if ($dupes -gt 0) {
|
|
||||||
W ""
|
|
||||||
W (" NOTE: {0} EDID code(s) appear on more than one panel (identical models)." -f $dupes)
|
|
||||||
W " Those lines use the connector UID instead, which is per-output."
|
|
||||||
}
|
|
||||||
W ""
|
|
||||||
W " Run this again AFTER a reboot or a panel power-cycle: the device names"
|
|
||||||
W " and ordering above may move, but stable-id / cfg form must NOT."
|
|
||||||
} catch { W " (stable-identity probe failed: $_)" }
|
|
||||||
W ""
|
|
||||||
|
|
||||||
W "---- SERIAL PORTS (the RIO board lives on one of these) ----"
|
W "---- SERIAL PORTS (the RIO board lives on one of these) ----"
|
||||||
try {
|
try {
|
||||||
$sp = Get-CimInstance Win32_SerialPort
|
$sp = Get-CimInstance Win32_SerialPort
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
# Hand-poll the RIO board over COM1 -- no game involved. Wire format per
|
|
||||||
# PCSerialPacket::SendPacket: [cmd][checksum = sum & 0x7F].
|
|
||||||
$p = New-Object System.IO.Ports.SerialPort COM1,9600,None,8,One
|
|
||||||
$p.DtrEnable = $true
|
|
||||||
$p.RtsEnable = $true
|
|
||||||
$p.ReadTimeout = 200
|
|
||||||
$p.Open()
|
|
||||||
'PORT OPEN CTS=' + $p.CtsHolding + ' DSR=' + $p.DsrHolding
|
|
||||||
|
|
||||||
function Send-Cmd([byte[]]$bytes, [string]$name) {
|
|
||||||
$p.DiscardInBuffer()
|
|
||||||
$p.Write($bytes, 0, $bytes.Length)
|
|
||||||
Start-Sleep -Milliseconds 700
|
|
||||||
$got = @()
|
|
||||||
while ($p.BytesToRead -gt 0) { $got += [byte]$p.ReadByte() }
|
|
||||||
$hex = ($got | ForEach-Object { $_.ToString('X2') }) -join ' '
|
|
||||||
"$name -> $($got.Count) byte(s): $hex"
|
|
||||||
}
|
|
||||||
|
|
||||||
Send-Cmd @([byte]0x80, [byte]0x00) 'CHECK 0x80'
|
|
||||||
Send-Cmd @([byte]0x81, [byte]0x01) 'VERSION 0x81'
|
|
||||||
Send-Cmd @([byte]0x82, [byte]0x02) 'ANALOG 0x82'
|
|
||||||
Start-Sleep -Seconds 2
|
|
||||||
Send-Cmd @([byte]0x82, [byte]0x02) 'ANALOG again'
|
|
||||||
$p.Close()
|
|
||||||
'DONE'
|
|
||||||
Reference in New Issue
Block a user