9e78d1cc238005186ea2f91ecba97c1b52e64a88
869
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9e78d1cc23 |
#149: [glassperf] -- per-window glass paint timing, one line per second, default on
Oracle confirmed the separate-window glass mode WAS fast and tanked ~2 builds back, so the regression is real and expresses only on his machine. This names the cost from inside his own session log: [glassperf] ticks=N tickMs=T | <window> n=paints ms=cost | ... tickMs is the synchronous UpdateWindow sweep measured INSIDE the render frame (L4VIDEO calls the tick), so on a machine where GDI serialises against D3D present it IS the per-frame tax, and the per-window split names the panel. Each WM_PAINT is QPC-timed at the dispatch chokepoint (the HALFTONE StretchDIBits whose cost is driver-dependent). Default ON like [segperf]; BT_PERF_LOG=0 opts out. Smoke-tested: lines live in a 60s panel run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC |
||
|
|
2b6b0276fd |
#152: the real hole -- glass/RIO rigs had NO torso-centre control; wire pod button 0x42
The reported "torso no longer recentres on its own" decomposed into two parts:
the auto-recentre in Mid/Adv was never authentic (it was the stuck-centerCommand
bug acting as a phantom feature since ~674 -- analysis on the ticket, awaiting
Oracle's 1995-memory verdict), but underneath it sat a REAL defect: with the
phantom gone, a glass/RIO player had no way to recentre the torso at all.
WHY. The only centerCommand writer lived inside the desktop key-bridge block,
which is OFF whenever a RIO/PadRIO is present -- glass and the pod both. The
pod's dedicated CENTER button (0x42, "the shipped .RES name", UP arrow via
bindings.txt) reached nothing: benched two scripted 0x42 holds on the RIO path,
ctrCmd=0 throughout, twist parked forever. (Keyboard X/NumPad5 recentred only
as a side effect of ALL-STOP -- you could not recentre without stopping.)
FIX, two pieces, both existing patterns:
* L4PADRIO::EmitButton -- the documented single chokepoint every button
source funnels through -- publishes the 0x42 HOLD state
(gBTTorsoCenterHeld), exactly the 0x3F ReverseThrust precedent.
* mechmppr gains ONE unified centerCommand writer, deliberately OUTSIDE the
key-bridge gate (the same placement lesson as the mode-cycle hook):
hold = torsoCenter(@0x154 databound) OR gBTTorsoCenterHeld(0x42) OR the
one-frame X pulse; asserts while held, clears on release. Single writer
== the sources can never stomp each other's clear.
BENCHED (scratchpad/night14/center42.sh, RIO path, bridge off):
before: ctrCmd=0 in all samples across two 0x42 holds; twist parked at 1.478
after: hold -> ctrCmd=1 recen=1, twist slews 2.22->0.44 at the authored
0.87 rad/s; twist input DURING the hold cancels sim-side (authentic);
release clears; no button -> aim holds (authentic Std/Vet).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
|
||
|
|
aa250c4e22 |
#149: [segperf] telemetry DEFAULT ON for every player (BT_PERF_LOG=0 opts out)
Operator call: an opt-in flag on the one machine we most need data from is the silent-diagnostic mistake this project's doctrine exists to prevent. Every session log now carries [segperf] beside every [rstat] window, so the next playtest gives the 817->857 stall hunt cross-machine baselines for free -- Oracle's stalling rig and Sauron's clean one, same night, same build. Cost: ~2 QPC reads per segment query, microseconds per second. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC |
||
|
|
a77c5a1407 |
#149: measure the filed suspect -- the #141 segment sweep is EXONERATED; ship [segperf] telemetry
A/B on identical 2-node beam-heavy runs (scratchpad/night14/segperf.sh),
BT_BEAM_SEGFRESH=1 (swept behaviour) vs =0 (pre-sweep compose at the beam
site, the only per-frame swept call):
fresh(swept) legacy(pre-sweep)
rstat blocks>50ms 0 0
maxDraw worst 730ms 646ms (mission-load spike, BOTH)
segperf dirty-passes 38/s 4/s <- the sweep DOES multiply
segperf accessor ms 0.97/s 0.57/s <- ...by 0.4 ms/s. Noise.
So the invalidation-storm hypothesis I filed on #149 is wrong by three orders
of magnitude, and Oracle's sustained 50-104ms stall window does NOT reproduce
on this rig at all. Refusing to guess a third time: the build now carries the
telemetry to answer it on the machine that actually regresses --
[segperf] calls= dirty= ms= printed beside every [rstat] window under
BT_PERF_LOG (JMOVER counters; two integer increments when unset), and
BT_BEAM_SEGFRESH=0 remains as a one-env A/B for the beam site.
Default stays FRESH (the swept accessor): its measured cost is trivial and it
is the correctness-cautious side while the peer-beam-staleness question is
unmeasured.
Next for #149: Oracle runs one session with BT_PERF_LOG=1. If [segperf] ms is
large inside his stalled windows, segment work is implicated on HIS
configuration and BT_BEAM_SEGFRESH=0 gives the immediate A/B; if it is small
(as here), the stall is elsewhere in the 817->857 delta and we hunt with his
numbers instead of my theories.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
|
||
|
|
84f8b1c415 |
night14: #137 fix write-up + close
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC |
||
|
|
0d6ed40db9 |
#137 FIXED: restore the binary Reset's +0x58c re-seed -- the respawn freeze was a
teleport-poisoned finite difference, not heat, not the reset, not velocity
The one-line fix is the binary's own SECOND Reset instruction, dropped in
transcription: FUN_00408440(mech+0x58c, param_2) ==
accelPrevPos = origin.linearPosition;
+0x58c is the previous-position memory of the AccelerationLastFrame ring feed
(+0x81c/0x824/0x828/0x82c). The port reconstructed the ring faithfully (ctor
part_012.c:9836, derivative :15169) but Reset never re-seeded its cache, so the
first post-respawn sample computed |newPos - prevPos|/dt = TELEPORT DISTANCE/dt
(~1e5) into the velocity ring; the ring-mean derivative spiked
AccelerationLastFrame (pure forward, with an opposite-sign echo ~15 frames later
as the sample rotated out of the 15-ring); and the myomer integrator @004b8d18
turned it into a one-tick pendingHeat deposit of ~3e9:
termAccel = (1-accEff) * |v| * |a| * m * dt
= 0.2 * 40 * 1.04e5 * 75000 * 0.044 = 2.75e9
snapping the freshly-reset myomers from T=77 to T~9000 against failT=2000 ->
speedEffect 0 -> speedDemand *= 0 -> "respawned unable to move until it cools".
WHY ~8% IN THE FIELD: 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 -- had gait-republished speed in the spike
frames. Idle-throttle respawns deposit ~nothing. (And frozen-subset-of-
died-hot: died hot == was running hard == lever still forward.)
MEASURED, same abusive bench (0.95 throttle + continuous autofire):
before: deposits up to 3.35e9, every one 3-4 lines after a Mech::Reset;
4-6 of 7 respawns frozen; post-reset myomers T 7700-12100
after: deposits >1e7: ZERO across 7 respawns; frozen: ZERO;
post-reset myomers T 77-178 (vs degradeT=1000)
THEORIES KILLED ON THE WAY, each by operand data, in order:
* stale pendingHeat carryover (bounded to 1 frame, +1.2K -- gates agent)
* slow accumulation during the death window (consumers tick the wreck)
* conduction from a hot neighbour (roster-wide snapshot: ALL partners 77;
flow trap: zero e6 flows into the myomers, ever)
* drag/impulse writers (both trapped: never fired)
* my own earlier "velocity-driven, players accelerate to top speed" close of
the ticket -- arithmetically impossible (input ceiling ~6.5e5/frame ~=
60 deg/s; the observed snap was 1,100-21,000 deg/s) and corrected in
context/subsystems.md, which carried the wrong paragraph.
* the localAcceleration zero-fill (+0x1dc) restored along the way is KEPT --
the binary does it -- but it was NOT the cause; the snapshot is rebuilt
from the position difference one frame later.
Probes kept (all BT_HEAT_LOG-gated): roster-wide [myofreeze] at-death/at-reset/
post-reset (T + heatEnergy + pendingHeat per subsystem), the [heatflow]
conduction trap with full operands, the [myodep] deposit trap with mech
identity + acceleration components. Engine MOVER.cpp traps reverted -- they
proved their negative (drag/impulse innocent) and do not belong in engine
source.
Gotcha #30 records the class: when the binary's Reset writes a cell you don't
recognize, that write IS the spec -- transcribe the whole zero/seed list; any
prev-value cell backing a finite difference must be re-seeded at every
teleport; and derived state (T) sampled at the reset proves nothing about the
backing producers.
The operator called the shape of this two days ago: "maybe the math gets screwy
in respawning while some systems are ticking while values are being reset."
That is precisely what it was.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
|
||
|
|
df1b4651b3 |
#137 CALIBRATION SETTLED: the myomer heat math is faithful -- constants read byte-exact
Read the integrator's constants from .rdata rather than inferring them. Row
` 4b8ee0 5dc30000 0000003f 00000000 0000803f` gives:
_DAT_004b8ee4 = 0.5f the kinetic half (work = mass * |v|^2 * 0.5)
_DAT_004b8ee8 = 0.0f the Abs() idiom zero
_DAT_004b8eec = 1.0f the (1 - efficiency) complements + gear clamp floor
All three are exactly what the port computes, and the logged terms reproduce
from the authored tuning to the digit:
work = 75000 * 54^2 * 0.5 = 109.3e6
complement = 1 - VelocityEfficiency(0.995) = 0.005
termKinetic = 109.3e6 * 0.005 * (dt*28 = 0.728) = 398e3 (logged 399003)
THE ONE DEVIATION IS DELIBERATE AND IS THE FAITHFUL CHOICE. The binary applies
NO time_slice to the kinetic term (`fVar5 * fVar1`) while climb and accel both
carry param_2 -- a per-frame energy add at the pod's FIXED ~28 Hz. The port's
`work * (time_slice * 28)` is identical at 28 Hz (dt*28 = 1.0) and holds the same
heat-per-SECOND at any frame rate. Transcribing it literally would add the full
term once per frame, so at Oracle's measured 170 fps it would inject ~6x the heat
the pod ever did. Preserving behaviour beats preserving the artifact of a fixed
timestep. BT_MYO_HZ still brackets the reference rate.
SO #137 IS NOT A CALIBRATION DEFECT. Heat is QUADRATIC in speed, so v~50 on open
ground after a respawn is ~9x the input of v~10-18 in a fight -- which is why the
overshoot correlates with respawns without being caused by them. It is also
self-limiting: effectiveness reaches 0, the mech stops, speed falls, it cools.
That is the authentic governor. Whether the cliff is too punishing for players
is a DESIGN call for the operator, not a fidelity bug.
Recorded in context/subsystems.md so the next reader does not re-litigate the
dt-normalisation as a bug.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
|
||
|
|
6a122f6cdf |
#137: the post-respawn heat spike is VELOCITY-driven, not reset corruption
Operator hypothesis tested: does something tick mid-reset and compute across
half-reset values (the teleport discontinuity) and inject a heat slug?
NOT SUPPORTED, measured. [myoheat] terms straddling a respawn:
BEFORE (-334) v=10.7 kinetic= 21941
BEFORE (-116) v=18.8 kinetic= 59560
AFTER ( +17) v= 6.1 kinetic= 3517 <- SMALL at the discontinuity
AFTER ( +129) v=46.7 kinetic=297349
AFTER ( +337) v=54.1 kinetic=399003
The sample immediately after the reset is small. Nothing computes across the
teleport; there is no injected slug.
WHAT IT ACTUALLY IS. work = mass * v^2 * 0.5, so heat is quadratic in speed.
After a respawn the mech reaches v~50 against v~10-18 while fighting before it
died -- roughly 9x the heat input -- because it comes back on open ground with
nothing to fight and accelerates to top speed. That is why the earlier
correlation looked like causation: temperature is highest just after a reset and
decays with distance from it, but the driver is SPEED, not the reset.
Reconciles both benches: 0.95-throttle runs overshoot, 0.50-throttle runs never
approach failT, and the operator's point that ordinary gameplay feels fine holds
-- at combat speeds the model behaves. It only runs away at sustained top speed.
Term arithmetic checks out against the authored tuning:
work = 75000 * 54^2 * 0.5 = 109.3e6
workComplement = 1 - VelocityEfficiency(0.995) = 0.005
termKinetic = 109.3e6 * 0.005 * (dt*28 = 0.728) = 398e3 (logged 399003)
so the port is computing what it intends to. The open question is whether the
INTENT is right -- i.e. whether (1 - 0.995) against the FULL kinetic energy is
the authentic scaling, or whether the binary's dt-less per-frame add at the
pod's fixed ~28 Hz means something different from our rate-normalised form at
100-170 fps. That is now a calibration question with a specific target, not a
hunt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
|
||
|
|
abea159b23 |
night14: #137 mechanism write-up to the tracker
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC |
||
|
|
8c612e4e18 |
#137 SOLVED (mechanism): not a respawn bug -- the myomer heat RATE under load is
Controlled A/B on the same bench, only the load differs:
0.95 throttle + continuous missile autofire : 4 respawns, 17 frozen samples,
T climbs 77 -> ~11,600
0.50 throttle, no weapons : 2 respawns, ZERO post-reset
freezes; T stays ~77
So the respawn is exonerated on measurement, not on argument:
* the reset works -- T == startingTemperature at every reset, both runs;
* the stale Myomers::speedEffect (@0x31C, which no RTIS writes) is real but
self-heals on the next tick (T=77.13 -> speedEffect=1);
* with ordinary load the myomers never approach failT=2000 after a respawn.
WHAT PLAYERS ARE ACTUALLY SEEING. Overheat while running hard -> myomers pass
failT=2000 -> derating curve @004b8ac0 returns 0.0 -> chain MAX 0 ->
speedDemand *= 0 -> bogged down -> die (often BECAUSE bogged down). Respawn
correctly resets to 77. Resume high throttle + firing and it climbs back over
the cliff within seconds, which reads as "respawned with the heat bar maxed".
That is why it looks like a reset bug and why it is intermittent (~8% of
respawns in the field): it tracks how hard you were driving, not the respawn.
THE REMAINING DEFECT is the CLIMB RATE, not the reset. Blowing ~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. Suspect under review: 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
that is NOT yet a proven discrepancy -- it needs a term-by-term dump against
the authored tuning (VelocityEfficiency 0.995, AccelerationEfficiency 0.8,
thermalMass 2.5e5, myomers linked Condenser5 = Oracle's "loop 5").
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
|
||
|
|
5556329807 |
#137: REPRODUCE the myomer freeze -- reset is innocent, the myomers RUN AWAY after it
First reproduction of #137 in a bench. The reset path is exonerated and the real defect is located, though not yet explained. DECOMP RE-READ (what the reset actually does): Mech::Reset @0049fb74 walks the roster from index 2 calling vtable +0x28 (slot 10 = ResetToInitialState), then @0049f788. Myomers::RTIS @004b8aa4 -> PoweredSubsystem::RTIS @004b0e6c -> ALWAYS HeatSink::RTIS @004ad760, whose first act is param_1[0x45] = param_1[0x4f] // currentTemperature = startingTemperature (byte 0x114 = 0x13C). Our port matches AND additionally resets heatEnergy, which it must, since HeatSinkSimulation derives currentTemperature = heatEnergy / thermalMass. The freeze itself is the derating curve @004b8ac0: temp >= degradation(@0x118) -> falls off temp >= FAILURE(@0x11C) -> 0.0 -> chain MAX 0 -> speedDemand *= 0. Nothing in the reset chain touches Myomers::speedEffect (@0x31C). MEASURED (scratchpad/night14/myofreeze.sh -- hot mech, repeated self-kill): at-reset Myomers T=77 deg=1000 fail=2000 speedEffect=0 <- stale post-reset Myomers T=77.14 speedEffect=1 <- 1 frame post-reset Myomers T=9297.8 fail=2000 speedEffect=0 post-reset Myomers T=11620.4 fail=2000 speedEffect=0 So, in order: * THE RESET WORKS. T is exactly startingTemperature at the reset. My original bench was right about that much; it just stopped looking there. * THE STALE speedEffect IS REAL BUT HARMLESS -- it survives the reset (no RTIS writes it) yet self-heals on the very next tick. Not the bug. * THE MYOMERS THEN RUN AWAY: 77 -> ~11,600 against a FAILURE point of 2,000, nearly 6x over, in seconds. That is not heat earned by running; that is a runaway, and it is what pins speedDemand at 0 until it cools -- exactly Oracle's "maxed heat bar ... unable to move until it cools off". * Rate: 7 of 105 census samples over the failure temp (~6.7%), against the field's 5-of-61 respawns (~8%). Same order, so the bench is reproducing the field condition and not a bench artifact. * The myomers link to Condenser5 (mass=250000 k=190000) -- literally Oracle's "loop 5 and generator D heating up as all the excess heat goes into the loop". NOT YET ESTABLISHED, and the reason this is a checkpoint and not a fix: whether the runaway is CAUSED by the respawn or is a heat-model calibration problem that merely CORRELATES with it (you die when you overheat, so respawns cluster around hot periods). The bench drives at 95% throttle with continuous missile autofire, which is abusive, and the [heat-t] census has no pre-first-reset samples to compare against. Next step is to instrument the heat INPUT and diff a respawn-adjacent window against a steady-running window. Probe added: [myofreeze] prints T / degradation / FAILURE / speedEffect per myomers plus the chain MAX the mover multiplies by, AT the reset and for ~4 s after (armed by Mech::Reset, sampled where the multiplier is formed). The post-reset window is what nothing was watching -- sampling only at the reset is what made this look innocent and got the ticket wrongly closed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC |
||
|
|
92783b9935 |
night14: tracker housekeeping for the 4.11.857 playtest
Closed 5 confirmed-fixed (#52 #108 #140 #141 #142), commented 3 (#135 #146 #147), REOPENED #137, filed #149-#158. #137 reopened because I closed it wrongly. Field scan of all four logs: 5 of 61 respawns froze (throttle up, speedDemand pinned at 0) across THREE machines -- ~8%, which is why a bench that reset cleanly every time never saw it. The myomers come up Overheating in the same breath as Mech::Reset, before any running could earn the heat. Oracle's 'unable to move' was the detail that disproved my 'it earns the heat by respawning under throttle' explanation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC |
||
|
|
0961a0b3f3 |
Revert the key G action ModeCycle default -- F7 already does it, on both keymaps
|
||
|
|
06d3b93507 |
pod: fix the podkit INFINITE LOOP, version-control podkit.ps1, and write the remote runbook
Deploying 4.11.854 to the cart hit a wall that was entirely self-inflicted and
entirely undocumented. Both are fixed here.
THE BUG. podkit.ps1 (which pushes the frozen rig config into a fresh install)
had, at line 30:
while ($keep.Count -gt 0 -and $keep[-1].Trim() -eq '') { $keep = $keep[0..($keep.Count-2)] }
Once $keep trims to a SINGLE blank line, $keep.Count-2 is -1, and PowerShell's
$keep[0..-1] returns TWO elements (index 0 and index -1) instead of shrinking --
so the array GROWS and the loop never ends, 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 -- which is exactly what you get when you
carry config forward from the previous install, the normal upgrade path. So this
would have bitten every future deploy, not just this one.
Signature: a BLANK cmd console on the cab, BT411Run stuck "Running", no btl4.exe,
no podrun.log. And it CASCADES: each hung run holds environ.ini so every later
attempt blocks behind it -- while over SSH your client times out and the REMOTE
powershell keeps running, so "it returned instantly and did nothing" actually
means "it is still hung". Six stuck processes had piled up before I spotted it.
Fixed to `-gt 1`, patched on the pod (podkit.ps1.bak is the original), and the
script is now IN THE REPO (tools/podkit.ps1) with the trap explained inline --
previously it existed only on the cab, so a restore would silently bring the bug
back. Pod and repo copies are byte-identical.
THE RUNBOOK (context/pod-hardware.md). Written because I improvised instead of
reading the one paragraph that already existed, on a live stream. Now covers:
* ssh bt411-pod -- and WHY it looked like auth was never set up: the key has
existed since 2026-08-06, but ssh will not OFFER it without a ~/.ssh/config
entry, so you get "Permission denied (publickey,password,...)". Also that
the Tailscale NODE KeyExpiry is not an SSH credential and Tailscale SSH is
not enabled on the pod.
* bare taskkill/setx return "The system cannot find the path specified" over
this SSH+cmd session -- call System32 tools by ABSOLUTE path.
* the deploy sequence, identical to a tester's: mkdist -> scp -> Expand-Archive
-> podkit. Local config is never clobbered because mkdist packs git-TRACKED
content only and bindings/environ/glass_layout are gitignored.
* PODTEST.EGG is not in the repo -- only podkit carries it; without it the
launcher runs and nothing appears, with no error.
* schtasks /run /tn BT411Run for GUI work (session 1); /end first, because a
task already Running refuses /run with 2147946720.
* the measured panel identities (below).
PANEL IDENTITY, measured over SSH (WMI is session-independent, so no GUI needed):
the cab's two RAR0005 panels SHARE one EDID code and have blank serials --
exactly the collision flagged as unproven in
|
||
|
|
c1a87b602c |
bindings: ship key G action ModeCycle in the built-in default board
ModeCycle was bound ONLY to the gamepad Start (CONTROLS.MAP), and bindings.txt carried no action bindings at all -- so a KEYBOARD-ONLY player could not reach the control mode. That is the control the torso-twist fix has to be tested through, and it is a shipped gameplay feature (Basic steers with the stick and auto-centres the torso; Standard/Veteran twist the torso and steer with the pedals), so it should not be gamepad-only. G is free. M is taken -- it is keypad button 0x02. DELIBERATELY the default TEMPLATE only, not a bindings-board bump. content/ bindings.txt is a per-machine runtime file (gitignored) and an existing one is never overwritten, so: * a FRESH extract writes the new board and gets G -- which is the zip flow; * an EXISTING tester keeps their file, and their customizations, untouched. Shipping it to existing files means bumping "# bindings-board" and extending kHistoricalDefaultRows so board-2 rows are recognised as "the old board" rather than player customizations. That is the designed path, but it rewrites every tester's bindings file, and doing that in the same build as a large unverified fix pile is how you lose a playtest evening. Left for a build where it is the headline change. Testers on an existing install can add the one line by hand. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC |
||
|
|
990ad52563 |
podprobe: emit boot-STABLE panel identities + ready-to-paste monitor🆔 lines
Adds a section resolving each screen's panel identity via the SAME Win32 API the game uses (EnumDisplayDevices on the display's MONITOR child) rather than the WmiMonitorID query above -- if probe and engine read different sources the printed fragments could fail to match what the engine tests. Verified on the dev box: probe and engine emit byte-identical identities. Answers the open [T3] multi-panel question WITHOUT deploying a build (pure OS data). When one EDID code appears on SEVERAL panels (identical MFD models) it detects the collision and emits the per-connector UID form instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC |
||
|
|
ec080cd61d |
pod displays: bind glass panels to EDID identity, not to Windows' shifting display numbers
Nick, after re-cabling + rebooting the pod: "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."
Both existing binding forms are boot-fragile: `monitor:2` is an ENUMERATION
INDEX and `monitor:\.\DISPLAY4` is a GDI name Windows reassigns. Neither
survives a panel power-cycle. (His gos-displays.txt shows the same trap next
door in GameOS: -tmon takes DIRECTDRAW device indices -- not Windows monitor
numbers -- and a NULL-device merge shifts every index down by one on top.)
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.
DISCOVER BT_GLASS_IDS=1 logs every attached panel's stable-id AND a
ready-to-paste `cfg form = monitor🆔<fragment>`. It prints the
VOLATILE identifiers alongside on purpose: run it either side of a
power-cycle and index/device move while stable-id does not.
BIND Heat MFD=monitor:id:AUO10ED,bare
NOTHING CHANGES BY DEFAULT -- no env and no `id:` prefix means identical
behaviour; `monitor:<name|index>` and raw x,y keep working, so playtester glass
builds are untouched.
An `id:` that matches nothing WARNS and falls back to computed placement.
Silence would put a picture on the wrong glass and look exactly like the bug
this form exists to prevent.
VERIFIED on a 1-monitor dev box (the pod is offline), end to end:
* discovery printed
stable-id = \?\DISPLAY#AUO10ED#4&31323a6c&1&UID265988#{e6f07b5f-...}
cfg form = monitor:id:AUO10ED
* `Heat MFD=monitor:id:AUO10ED,bare` resolved and CENTRED correctly
[glasswin] 'Heat MFD' bound to monitor 0,0 1920x1080 -> window at 640,300
* a bogus id warned instead of misplacing.
The EDID-code extractor is deliberately STRUCTURAL (3 letters + 4 hex digits,
tokenising on \ # ?) rather than positional: the first cut walked separators by
position and returned EMPTY for the `\?\DISPLAY#...` interface-name form, which
is exactly the form this machine produces. Which form you get depends on
whether EDD_GET_DEVICE_INTERFACE_NAME succeeds, so both must parse.
STILL UNPROVEN [T3] -- the pod is offline: multi-panel disambiguation when
several MFDs share one model (EDID codes collide). The documented answer is a
longer fragment from stable-id, whose UID/instance tail differs per connector,
but that needs the cab to confirm.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
|
||
|
|
9657fbb11e |
control mode: REPRODUCE the "centering fought my control" fight, and prove the fix in Sauron's config
Follow-up to
|
||
|
|
4ccc2a7eec |
control mode: Basic re-centre used the STICKY held-button cell -- and the elevation-limit swap was never ported
Sauron: "toggled through advanced controls from standard to advanced and back
to standard -- lost torso control."
The cycle is 0 Basic -> 1 Standard -> 2 Veteran -> WRAPS TO BASIC, so getting
from "advanced" back to Standard PASSES THROUGH BASIC, whose arm re-centres the
torso. @004afbe0 is a complete spec and the port got three things wrong:
iVar1 = mech+0x438 (TORSO) iVar2 = mech+0x5b4 (HUD)
if (mode == 0) { // BASIC
*(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. WRONG CELL. Basic called CommandRecenter() -> centerCommand (@0x208), the
HELD-BUTTON cell: TorsoSimulation re-arms recenterActive from it EVERY frame
it is non-zero, and only the input path clears it -- a mode switch has no
button release to follow. Digital twist commands are processed BEFORE the
centerCommand block, so while it is set they are overridden as fast as they
are applied: the torso stops responding. 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 members were ctor-written and read by NOTHING.
Basic never restricted downward travel; the assisted modes never restored it.
3. Basic also raises the HUD's flickerActive (@0x2A0) so the horizon re-settles
with the torso it just re-centred. Not ported. (New BTSetHudFlickerActive
bridge in hud.cpp -- mechmppr sees Subsystem*, not HUD.)
Also removed an invented SetAnalogElevationAxis(0); the binary zeroes only 0x1F0.
MEASURED A/B (scratchpad/night13/modecycle.sh, LEGACY=1 for the old path;
BT_LEGACY_MODE_RECENTER=1 is the revert switch):
ctrCmd=1 samples legacy 26 fixed 0
vLim pairs fixed run shows BOTH -- (-0.698..0.349) = -40..20 deg
assisted, and (-0.349..0.349) = -20..20 deg Basic.
Before this commit only the ctor pair ever appeared.
WHAT IS *NOT* PROVEN. I did not reproduce Sauron's PERMANENT loss. In this
bench the legacy latch is periodic, not sticky:
..........LLLL......LLLL......LLLL......LLLL......LLLL......LL
because the desktop key bridge writes centerCommand every frame and zeroes it
when no button is held, so it self-recovers. The torso IS locked while the cell
is set, which is the symptom -- but whether it stays locked depends on the input
path OWNING that cell. On the glass/pad route (Sauron's) nothing may clear it,
which would make it permanent. So: mechanism fixed and binary-grounded, exact
field persistence unverified. Field-verify by cycling modes on a pad build.
Probe: the BT_TORSO_LOG gate line now carries ctrCmd / recen / vLim.
Bench hook: BT_MODECYCLE_EVERY=<n> cycles the mode from the mapper (the pod's
own route is console key 0x13d -- not a RIO button, so BT_BTNTEST cannot press
it, and mech4's BT_MODECYCLE_TEST counter did not advance in a solo run).
NOTE the bench needs BT_KEY_BRIDGE=1: with a PadRIO present the key-bridge
block that consumes the cycle is skipped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
|
||
|
|
43777569f9 |
night13: close #148 as not-a-bug on the tracker
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC |
||
|
|
cacca58836 |
#148 is NOT A BUG: a peer mech does not tick before RunningMission -- by engine design
Chased to the bottom instead of stopping. The answer is that there was
nothing to fix, and my bench was lying to me.
Entity::Execute (ENTITY.cpp:556, real engine source [T0]) calls PerformAndWatch
ONLY when the app state is RunningMission/EndingMission or the entity
IsPreRunnable(); otherwise it merely WriteSimulationUpdate()s.
Entity::DefaultFlags is DynamicFlag|MasterInstance -- no PreRunFlag. Only
Player and Director add it, 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,
however much correctly-replicated data is arriving. Measured on the observer:
235 [perf-first] mech 3:161 master <- own mech, immediately
402 [torso-rec-rx] <- peer torso records 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.
WHICH MEANS THE PREFIX WAS A BENCH ARTIFACT. BT_AUTOFIRE starts shooting
immediately, during WaitingForLaunch -- something no player can do in a real
match -- so those 60 leading salvos measured a peer whose torso had never run.
Every "ZZZZ...XXXX" pattern in this investigation was that, and the first X
lands within a few lines of the state transition. #141's fix is unaffected and
remains verified: the segment-cache defect was real and mid-match.
Chain of things ruled out on the way, all measured:
* record CADENCE is authentic -- sends on RATE CHANGE (payloads are the sweep
extremes, rate flips sign), peer dead-reckons between them. 12 records for
12 reversals is correct, not starved. My "only 13 records" premise was wrong.
* ComputeTargetTwist clamp -- limits load fine on the copy (+/-2.44346).
* the torso's own executable flag -- restoring the engine's instance branch
(
|
||
|
|
bb6605d53b |
night13: #148 tracker correction script
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC |
||
|
|
f36f0136c8 |
#148: restore the ENGINE's replicant instance-branch in the Mech subsystem tick
Correct on its own merits as a fidelity fix; it is NOT the cause of #148, and I am not claiming it is. Entity::Perform (ENTITY.cpp:733-793, real engine source [T0]) picks the executable predicate BY INSTANCE: if (GetInstance() != ReplicantInstance) IsNonReplicantExecutable() else IsReplicantExecutable() and the two 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". Mech's reconstructed tick loop used the NonReplicant predicate for EVERY mech, dropping the branch, so on a replicant any ExecuteOnUpdate subsystem could never run however many records arrived. Restored. Measured: it does NOT move #148 (first TorsoCopySimulation call 1014 -> 1006, noise). So the torso's own flag was not the gate. Keeping it because the engine source is unambiguous about what the loop is supposed to do. WHAT #148 ACTUALLY IS, now much better characterised: * The record CADENCE is authentic -- my original "only 13 records" framing was wrong. Payloads are the sweep EXTREMES with `rate` flipping sign each time (atUpd 0.0437, 2.3558, -2.3928, 2.3854, ...): 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 real defect is that 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 integrated by nobody. 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, in replicant model/clip init. Not yet found; #148 stays OPEN. Also: [torso-copy] logs on call #0 (s_cl++ % 120), so its first line IS the first Performance call -- that is what makes the 205-vs-1006 gap readable, and it is why the earlier "first copy currentTwist != 0 at 1016" reading was a SAMPLING artifact, not a measurement of when the twist started. Probe additions kept: [torso-copy] now prints limL/limR/enab (which ruled out the ComputeTargetTwist clamp -- limits load correctly at +/-2.44346 on the copy), and [launchframe] now prints the shooter's live torso twist so twistDelta and its driver sit on the SAME line. That pairing is what proved #141 is fully fixed: every zero-twistDelta peer launch reads liveTwist=0, and the first launch with liveTwist=-1.84061 reads twistDelta=-1.83813. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC |
||
|
|
eebe7e61a4 |
gotcha #28: hand-composing an engine-derived transform reads a stale cache (replicant-only)
The #141 bug class, written up so it is not re-introduced. GetSegmentToEntity recomputes ONLY when segmentModified is set; JointedMover::GetSegmentToWorld is what sets it -- and the binary's own GetMuzzlePoint @004b9948 goes through it (FUN_00424da8), so every muzzle query in the 1995 image performs the joints->segments refresh. Four port sites hand-composed instead, one of them commented "the faithful FUN_004b9948". Records the four rules the investigation actually cost: (a) never hand-compose; call GetSegmentToWorld (b) never force the dirty flag to fix a stale read -- that stand-in scored IDENTICALLY to the faithful fix while patching only one consumer (c) "peer POV only" geometry bugs = suspect a cache the local render pass refreshes for free, before suspecting replication (it was provably fine) (d) a partial-looking score: check PREFIX vs interleaved before calling it partial -- these were a clean prefix ending when the peer first had a twist to carry, so the fix was complete and "64% fixed" was wrong (e) the probe trap: one shared static sampled every Nth call hides one of two alternating instances entirely #141 closed with the full write-up; #148 filed for the torso replication cadence (13 records in a 5-minute run) which is a separate, real problem and likely bears on #37 and #70. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC |
||
|
|
e6c5ac951e |
#141 sweep: every hand-composed segment->world now goes through the engine accessor
Finishing the audit the #141 fix implied. The unfaithful pattern
mw.Multiply(seg->GetSegmentToEntity(), mech->localToWorld);
appeared at FOUR sites, not one. GetSegmentToEntity only recomputes when
segmentModified is already set (SEGMENT.cpp:262); the thing that sets it is the
binary's FUN_00424da8 == JointedMover::GetSegmentToWorld, which tests
AreJointsModified() and marks the whole segment table dirty. Compose by hand
and you read whatever cache is there -- fresh on the local mech (the render pass
refreshes it every frame), BIND POSE on any replicant.
Swept (the muzzle path was fixed in
|
||
|
|
f01de8cbfa |
#141 follow-up: do it the BINARY's way -- the muzzle query IS the segment refresh
The previous commit's fix worked but was NOT faithful: it set
ModifyJoints(True) to force the engine's dirty flag before reading the
segment. The binary never does that. Called out by the user; corrected.
WHAT THE BINARY ACTUALLY DOES. MechWeapon::GetMuzzlePoint @004b9948 ends in
`FUN_00424da8(owner, segment, out)`, which is JointedMover::GetSegmentToWorld
instruction-for-instruction:
iVar1 = FUN_00417ab4(param_1 + 0x31c); // GetJointSubsystem()
if (*(int *)(iVar1 + 0xfc) != 0) { // AreJointsModified() <- TESTED
... walk owner+0x300, 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,
and the flag is only ever TESTED, never set.
THE REAL DEFECT. BTResolveWeaponMuzzle -- labelled "the faithful FUN_004b9948"
-- hand-composed `seg->GetSegmentToEntity() x localToWorld` and skipped
@00424da8 entirely. GetSegmentToEntity only recomputes when segmentModified is
already set (SEGMENT.cpp:262), so it returned a stale cache. On the MASTER the
render pass refreshes the local mech every frame and hid it; a REPLICANT got no
refresh, so peer muzzles sat at the BIND POSE and the missile left along the leg
facing. Fixed at the muzzle path, where the binary puts it -- and the forced
flag in BTPushProjectile is REMOVED (the launcher calls GetMuzzlePoint just
above, so the cache is already current when the launch frame is composed).
MEASURED -- the faithful path scores exactly what the hack did, so the hack
bought nothing and is gone:
master n=165 max 2.1389 mean 1.2718 >0.1rad 100%
REPLICANT n=165 max 1.9426 mean 0.8051 >0.1rad 64%
AND THE 64% IS NOT A PARTIAL FIX -- I called that wrong last commit. The
failures are a contiguous PREFIX, not interleaved:
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 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. #141 is fixed.
SEPARATE ISSUE FOUND, not fixed here: the peer's copy torso takes far too long
to first reflect the master's twist -- the master was twisted from the start,
only 13 torso records arrived across the whole run, and the copy's twist stayed
0 until line 1016. That is a torso REPLICATION CADENCE problem, and it would
also make peer torsos visibly lag -- likely relevant to #37 (MadCat torso
backwards) and #70 (twist stops after respawn).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
|
||
|
|
05d7b5890a |
#141 peer missiles: the launch frame read a STALE segment cache on replicants
Oracle: "missiles are firing in the direction the mech feet are facing ...
and then coming around to track the target", peer POV only -- the shooter's
own view is correct.
REPRODUCED AND MEASURED (scratchpad/night13/missileframe.sh, 2-node: only A
sweeps its torso and only A fires, so every REPLICANT line in B's log is the
mirror of one A salvo). New [launchframe] receipt prints the yaw of the
launch forward vs the BODY forward on both nodes:
master n=165 |twistDelta| max=2.2962 mean=1.2283 >0.1rad: 100%
REPLICANT n=165 |twistDelta| max=0.0000 mean=0.0000 >0.1rad: 0%
segResolved=1 on BOTH, and segYaw == bodyYaw EXACTLY on the peer.
WHAT IT IS NOT. Both sides already pass the mount segment (mislanch.cpp:363
master, :478 replicant mirror, both `GetSegmentIndex()` from task #67), and
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 is identical too: same seg 18, same parentIdx 4,
non-null parent + joint subsystem on both.
ROOT CAUSE. BTPushProjectile composed the frame BY HAND --
`mw.Multiply(seg->GetSegmentToEntity(), localToWorld)`. But
EntitySegment::GetSegmentToEntity (SEGMENT.cpp:262) recomputes ONLY when
`segmentModified` is set, and the thing that sets it after a joint moves is
JointedMover::GetSegmentToWorld (JMOVER.cpp:136-146), which tests
AreJointsModified() and then marks every segment dirty. Hand-composing skips
that, so you read whatever cache is sitting there. On the MASTER that was
invisible -- the renderer/cockpit camera call GetSegmentToWorld for the local
mech every frame, AFTER the local torso pushes its joint, so the cache was
already correct. A REPLICANT gets no such refresh: its cache stayed at the
BIND POSE, and the twist never reached the launch direction.
FIX. Use the engine accessor, and set the joints-dirty flag first so it
actually refreshes (by fire time the frame's render pass has already consumed
and cleared it -- measured jointsDirty=0 on BOTH nodes).
RESULT (same bench):
REPLICANT max 0.0000 -> 2.1145 mean 0.0000 -> 0.8252 0% -> 64%
PARTIAL, and I am not claiming otherwise. 36% of peer salvos still read the
exact-zero stale signature while the master is 100%. Forcing every per-joint
`jointModified` flag as well (GetSegmentToParent's own gate, SEGMENT.cpp:196)
was tried and moved the number by NOTHING -- 64% either way -- so the residual
is a different cause, most likely frame ORDER (the salvo mirror running before
the copy torso has posed that frame). Cheap form kept.
Also fixes a SAMPLING TRAP in the torso probe: PushTwist sampled one shared
static every 30th call, and with a master torso and a copy torso ticking 1:1
every 30th call is always the SAME instance -- so the probe showed only the
local untwisted torso and hid the copy's writes entirely. Now sampled per
instance-kind, which is what made the copy's correct joint writes visible and
moved the search downstream to the segment cache.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
|
||
|
|
1ae57398f1 |
#147 range caret: NaN poisons a process-lifetime static -- and the caret's input was never logged
Oracle: "no range finder on this drop" + a screenshot -- tick marks present,
moving caret absent, one drop, only tester affected.
Not the host, not the chassis, not his destroyed HUD. From the four field
logs: he WAS hosting (`[lobby] host:` appears only in his log) but range
computed fine on his node (1806 nonzero samples) and the reticle built on all
6 drops; a second tester flew a Thor the same night without hosting and saw
nothing, and the ladder is shared HudSimulation/BTReticleRenderable, not
per-chassis content; his HUD was destroyed twice but for 11s and 26s only, and
a destroyed HUD costs the LOCK (_DAT_004b7ec4 = 0.75), not the caret.
THE DEFECT. sShownRange -- what the caret binds to -- is a function-level
static in mech4's targeting step: one cell for the whole process, shared by
every mech, carried across drops, never re-seeded. NaN is ABSORBING in
step = trueRange - sShownRange;
if (step > maxStep) step = maxStep; // false for NaN
if (step < -maxStep) step = -maxStep; // false for NaN
sShownRange += step;
so one poisoned frame makes it NaN for the life of the process. The consumer
repeats the mistake -- BTReticleRenderable::Draw clamps with the same two
comparisons -- so NaN reaches AddPoint/ConcatMatrix and the caret + its bar
become degenerate geometry that STOPS RENDERING, while every static reticle
element including the tick marks still draws. That is the reported symptom
exactly, and it is sticky until relaunch.
WHY NO LOG COULD SETTLE IT. 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 -- neither is
sShownRange or 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.
FIX (4 parts):
1. re-seed sShownRange when the viewpoint mech CHANGES, so a new drop starts
at the binary's 1200 default. Deliberately NOT on respawn -- that reuses
the entity, and the binary does not reset the readout on respawn either.
2. producer NaN trap -> re-seed to 1200 instead of propagating.
3. NaN-safe consumer clamp (test x == x first) -> fall back to the authentic
no-target peg rather than rendering nothing.
4. BT_RANGE_LOG now prints the caret's real input at 1 Hz plus a
"[range] NaN TRAPPED" receipt, so the next field log CAN settle it.
STATUS [T3 on the field link]. The defect and the symptom match exactly and
the fix stands on its own merits -- a process-lifetime static feeding unguarded
float geometry is a bug regardless. But the causal link to Oracle's report is
INFERENCE: the NaN source is unidentified and this has not been reproduced.
Field-verify with BT_RANGE_LOG=1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
|
||
|
|
832bec0966 |
hud.cpp: byte-ground the HudSimulation constants -- all five were stand-ins under guessed names
Found chasing the Thor "no range finder" report: _DAT_004b7ec4 was documented
as two incompatible things -- the 0.75 LOCK damage threshold (mech4.cpp:6325)
and a "heat threshold for HUD page visibility" valued 0.0f (hud.cpp:59).
The .rdata settles it (reference/decomp/section_dump.txt):
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
mech4.cpp was right on both thresholds. hud.cpp's whole tuning block was
wrong -- every entry a 0.0f/500.0f stand-in, and three of five names named the
wrong mechanism:
* ec4/ec8 are the fire-control LOCK limits (own HUD host zone < 0.75 damage,
targeted zone < 1.0), NOT heat/page-visibility. A shot-up cockpit drops to
"target held, no lock"; a dead zone cannot be re-locked.
* ed0 is the shared ZERO -- the right-hand side of the range-slide Abs()
idiom (`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 (0x43fa0000). The old
"MaxTorsoSlew = 500.0f" read that backwards.
* f90 (FlickerFloor 0.0f) was the only correct entry. Its decay RATE is the
object's own @0x298, not a constant -- the step-6 banner said "up to
MaxTorsoSlew (500/sec)" and is corrected too (hud.cpp:229 already had it
right, so the file disagreed with itself).
All four wrong constants were DEAD (zero code uses; MaxTorsoSlew appeared only
in a comment), so this changes no behaviour -- it stops the next reader
trusting them. Renamed to what they are: LockOwnZoneDamageLimit,
LockTargetZoneDamageLimit, RangeBias, HudZero. Builds clean.
GAP FOUND, filed not fixed: HudSimulation subtracts _DAT_004b7ecc (100.0f)
from RangeToTarget@0x1EC every frame while the timed flag @0x22C is set
(timer @0x21C accumulates to @0x1D8, then both clear). Our targeting step
does the 500 m/s slide but never this bias, so the authentic timed -100 m
range offset is missing. What sets @0x22C is unidentified. -> open-questions.
KB swept: no context/ or docs/ file repeated the wrong constants (gauges-hud's
0-1200 ladder / 500 m/s / pegs-at-1200 claims are all correct); the error was
confined to hud.cpp. gauges-hud.md gains the byte-grounded table + the gap.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
|
||
|
|
7b003243ae |
#146 respawn: release the DESKTOP throttle -- and close #137, which was never a bug
#137 ("respawn came back with MYOMERS heat MAXED", Oracle; "overheating generator D", Sauron) sent us through a full two-sided audit of Mech::Reset and the whole RTIS chain. Both sides were correct. The answer was in the field log all along, one line after the reset: [respawn] Mech::Reset 3:30 healed+moved to (...) alive=1 [techstat] ... every live condition CLEARED [techstat] Myomers condition 3 SET <- Overheating, immediately [mppr] in thr=1 -> ... [gaitSM] cycleSpeed=14.6 state=12 <- already RUNNING [techstat] Condenser5 condition 3 SET <- "dumping into coolant loop 5" [techstat] GeneratorD condition 3 SET <- Sauron's generator D The mech respawns STILL UNDER POWER and earns the heat honestly. Two facts close it: 1. condition 3 is an OPERATING flag, not an alarm. Census over one match: LLaser_2 33 SET / 33 CLEARED, LLaser_1 31/31, SRM4 26/26, PPC_2 18/18 -- every volley trips it and clears it. EVERY subsystem is balanced (GeneratorD 5/4, Myomers 5/4, Condenser5 1/1; the extra SET is only the log ending mid-heat). cond 6 BadPower behaves the same (Myomers 8/8). Nothing latches. A post-respawn SET is not evidence of anything. 2. 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. That is right for a pod: the throttle is a PHYSICAL lever still under the pilot's hand. Respawning under power is authentic and stays. Oracle's read that the myomer heat rate "felt right" was correct. WHAT IS a real defect (#146), desktop only: the glass bridge merely EMULATES that lever, with the static ramp accumulator sLever (mech4.cpp:3250) zeroed ONLY by the X all-stop and a direction-crossing snap. A pad/keyboard pilot is physically holding nothing and cannot see the lever, so they respawned at speed for no reason they could perceive -- and ate the heat load above. The Thrustmaster/RIO path was never affected: InterpretControls (@004d2150) rebuilds throttlePosition every frame from the databound throttleForward. Fix: queue the existing all-stop at Mech::Reset, reusing the proven path (it already clears the zero-crossing detent too). LOCAL VIEWPOINT MECH ONLY -- gBTDrive is the local bridge's state and Reset also runs for replicants, so an ungated write would all-stop the player whenever a REMOTE mech respawned. Pod-safe besides: with a RIO present the key bridge is off and gBTDrive.throttle is never read. BT_NO_RESPAWN_THROTTLE_RELEASE=1 reverts. Benched 2-node (scratchpad/night13/throttlerespawn.sh): the release fires 1:1 with local respawns on both nodes independently (A 2/2, B 1/1) and never spuriously. HONEST LIMIT: the viewpoint gate was NOT stressed -- B ran Mech::Reset 0 times for A's mech, so the remote-respawn path never fired. The gate is correct by construction (the isPlayerMech idiom), not proven. BT_AUTODRIVE cannot test the lever itself (forced mode reads forcedThrottle, never sLever), and the zeroing path is the X button, proven in the field. Also keeps BTReportHeatAtReset (heat.cpp, BT_HEAT_LOG): the [heat-t] census runs on a 5s timer, far too coarse to sample AT the reset. It is what proved every roster subsystem including all six Condensers sits at T=77 start=77, and it corrected an earlier false negative from filtering on IsDerivedFrom(HeatSink). KB: context/decomp-reference.md gains the routine/self-clearing condition semantics + this post-mortem, so it is not re-chased; cross-ref in context/gauges-hud.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC |
||
|
|
5b7e481913 |
scoring: mech+0x354 is VESTIGIAL -- label it so nobody "finishes" it
MECH_DAMAGE_BIAS(m) returned 0.0f under a comment reading "bring-up: factor =
0*bias+1 = 1", which invites a future session to wire it up. Auditing
Mech::Reset settled what it actually is, and 0.0f turns out to be EXACT:
* mech+0x354 has exactly ONE writer in the image -- Mech::Reset (@0049fb74,
part_012.c:14340). Nothing touches it during play.
* Reset computes mean(zone + 0x158) across every damage zone, AFTER the zone
heal has already zeroed those cells. So it is ~0 the moment it is written,
stays ~0 for the mech's whole life, and is recomputed as ~0 next respawn.
* It has exactly ONE reader -- CalcInflictedScore (@004c052c,
part_013.c:19055) -- as `avg * role.damageBias + 1.0`.
So the factor is 1.0 for the entire game and the stand-in reproduces the
binary exactly. 0x358 and 0x35c are the same computation over subsystem zones
and have NO reader at all.
This also raises confidence in the night-13 scoring work: the 505.88 kill award
was not right DESPITE a missing term -- the term genuinely is 1.0. Wiring
0x354 to live accumulated damage would silently inflate every inflicted and
kill award, and both chart-verified numbers (+1 a damage point, +500 a kill)
assume 1.0.
Comment rewritten at the macro; combat-damage.md carries the same finding [T1].
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
|
||
|
|
1f0923747b |
Mech::Reset: restore the POSTURE clears the port had dropped (#142)
Oracle: "crouch wasn't resetting on respawn ... mechs always spawn standing".
Correct -- Mech::Reset (@0049fb74) stands the mech up and the port cleared
none of it:
*(this+0x398) = 0 duckState
Set_Alarm_Level(this+0x39c,0) legStateAlarm -> standing
Set_Alarm_Level(this+0x714,0) bodyStateAlarm -> standing
*(this+0x650/0x654/0x658) = 0 death + leg/body reset latches
*(this+0x5ac) = 1.0f idleStrideScale
A pilot who died CROUCHED came back crouched -- leg parked in 'sqd' -- and now
that the cockpit strip works, showing the up-arrow "press to rise" frame on a
standing mech.
Benched (crouchrespawn.sh): A squats, dies while down, respawns -> legLvl 0
(standing) after Mech::Reset. Weak but the failure mode (stuck legLvl=1) is
absent. NB the first attempt was void: force-damage kept A dying before it
could crouch (legLvl 22/24 = death clips), so the run tested a STANDING death.
Switched to self-damage so the mech is stopped long enough to crouch.
Also carries the #142 gauge work: the crouch strip is a BUTTON-STATE indicator
(grey unavailable / orange down-arrow ready / orange up-arrow crouched),
decoded by rendering BDUCK.PCC rather than inferring it; and the gauge
factory's missing-image path no longer uses the no-op DebugStream.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
|
||
|
|
03c4d55672 |
#142 crouch: duckState is a THREE-state posture -- the strip is an animation
Operator confirmed on screen: bduck.pcc is a real duck ANIMATION, and stepping
duckState 0->1->2 plays it. So the attribute is not a flag:
0 = standing 1 = moving between 2 = crouched
Everything else was already right -- asset, element (OneOfSeveralPixInt
@004c5204), factory registration, L4GAUGE.CFG:5001, and the attribute binding
(new [gauge] receipt confirms 'bduck.pcc' frames=3x1 attr=BOUND). We were
writing a two-value flag into a three-frame strip, so frame 2 was unreachable
and the cockpit saw a snap: "it lights up and sticks, no animation".
The handler is back to the binary's exact write (duckState = 1, @0049fa00).
That value now MEANS the middle frame, so the press gives immediate visual
feedback and the earlier toggle divergence is retired.
Needed a separate duckRequest cell, which I tried twice to avoid:
* duckState cannot be both the request and the display. Settling it to the
real posture destroys the request, so on the frame the squat clip parked
the consumer read "crouched + pending" and issued the opposite direction --
69 transitions from 2 presses, benched, twice.
* reading the CACHED legAnimationState instead of the alarm made it worse:
the cache refreshes only at the top of AdvanceLegAnimation, so right after
SetLegAnimation it still reads the old state. Read the alarm.
duckRequest is port-only, appended, never read by offset.
Also fixes a silent failure in the gauge factory: the missing-image path used
DebugStream -- the no-op ReconStream (project gotcha) -- so a strip that failed
to load reported NOTHING. Now DEBUG_STREAM, plus an ungated one-line receipt
per element naming the image, frame grid, port and whether the attribute BOUND
or came back NULL. That receipt is what proved the element was healthy and
sent me looking at the value instead of the plumbing.
Benched (crouch142.sh, madcat): 2 presses -> exactly 2 transitions,
SQUAT -> parked (settles to 2) then RISE (settles to 0). Refusal while moving
still holds (posture=0, authentic per Lynx).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
|
||
|
|
fcd1a0ca8d |
#142 crouch: refuse-and-snap, not queue -- and honour the must-be-stopped rule
Fixes a regression I introduced in
|
||
|
|
59f53da07b |
#142 crouch: duckState is the POSTURE the cockpit animation reads
The crouch symbol animation is fully present and we were starving it.
content/GAUGE/BDUCK.PCC the 3-frame strip
OneOfSeveralPixInt @004c5204/@004c52d8 the element, reconstructed
btl4grnd.cpp:144 registered in the factory
L4GAUGE.CFG:5001 oneOfSeveralPixInt(
E,ModeAlwaysActive,
bduck.pcc,3,1,DuckState)
ATTRIBUTE_ENTRY(Mech, DuckState, duckState) attribute 0x37
A 3-frame mech symbol beside the CROUCH button, indexed by duckState -- the
standing<->crouching animation Lynx and Draco describe. It never played
because the 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. Remains in stand mode."
THE ZEROING WAS OURS. Every writer of +0x398 in the export is the
DuckRequest handler (=1) and Mech::Reset (=0). FUN_004a9b5c -- the master
perf, which contains the address the old comment cited as "the DuckRequest
consumer (@0x4aa011)" -- does not reference 0x398 at all. 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, and we were clearing it behind the gauge's back.
Restructure: drive on DESIRED vs ACTUAL. duckState is the desired posture;
the parked leg alarm is the actual. Act only on a mismatch -- no re-fire, and
nothing clears the attribute. A frame where mapPosture is not ready now
RETRIES (throttled [duck] WAITING) instead of silently dropping the request,
which retires the old "request consumed, posture=N" miss as well.
ONE DOCUMENTED DIVERGENCE: the handler now TOGGLES. The binary writes a bare
1 and clears the cell only in Mech::Reset, with no per-frame reader, so a
second press could never rise -- and a pod pilot's second press must un-crouch
(Lynx: "Mech is immobilized until crouch is pushed again, and mech rises").
One cell, same meaning, noted at the site.
Benched (crouch142.sh, madcat):
duckState -> 1 (crouch) -> SQUAT -> [holds 1 while crouched] ->
duckState -> 0 (rise) -> RISE
Value now persists across the crouched period instead of blipping, so frames
0/1 of the strip are reachable and stable. Also removed the interim REQUEST
DROPPED receipt: after the restructure nothing is dropped, and a receipt that
says otherwise is a trap for the next session.
STILL OPEN on #142: no immobilization while crouched (Lynx) -- nothing gates
movement on duckState or the parked leg alarm. A driven mech with a parked
leg channel is the [skate] signature (#52), so it may not be cosmetic.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
|
||
|
|
0530366687 |
#142 crouch: the mech is fine -- it is a missing PANEL ANIMATION
Benched solo AND in MP on the same chassis: both presses reach
DuckRequestMessageHandler, zero drops, SQUAT -> squat clip parked -> RISE.
Locomotion is not the bug, and MP is not refusing it.
Added an ungated [duck] REQUEST DROPPED receipt at the consumer's silent miss.
The squatCapable==0 path skips the consumer entirely AND leaves duckState
latched at 1 with NO log today; the posture-gate miss logged only under
BT_DUCK_LOG, which no player sets. Neither fired on madcat.
What the pilot sees, traced with BT_LAMP_LOG: the button lamp is momentary
press feedback, not state --
PRESS -> [lamp] 0x13 <- 0x3c
SQUAT -> mech crouches, clip parked
RELEASE -> [lamp] 0x13 <- 0x14 <-- while still CROUCHED
so crouched and standing look identical.
Era testimony corrects the scope: the button should ANIMATE A MECH SYMBOL
beside it, standing <-> crouching (operator). 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 concurs.
Two real gaps, neither fixed here:
1. no immobilization while crouched -- nothing gates movement on duckState
or the parked leg alarm. NB a driven mech with a parked leg channel is
the [skate] signature (#52), so this may not be cosmetic.
2. no stance symbol -- no gauge element draws one, and the decomp carries no
crouch/squat/stance/duck graphic string, so it is an authored IMAGE on the
secondary MFD; find it in that gauge's element list, not by string.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
|
||
|
|
61f21107b4 |
#108 THE EJECT GHOST: the death-edge latch tested the wrong field
One substitution, three field symptoms. Mech::TakeDamageMessageHandler arms
the whole death tail -- kill report, VehicleDead, death blast -- from a
was-alive-at-entry latch:
const int deathBlastArmed = !IsMechDestroyed(); // graphicAlarm >= 9
The binary tests movementMode 9|10 there (@0x4a0303). The port swapped in the
graphic alarm and justified it: "the death transition sets mode 9 synchronously
with the structural flag on every path through here, so the edges coincide".
True of every DAMAGE path. 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 death tail was skipped entirely -- including VehicleDead, which IS the
respawn trigger.
Everything the field reported on night 13 follows from that:
* "they all self destructed with panic button and didn't respawn properly"
-- no VehicleDead, so no drop-zone hunt, so no respawn;
* the EJECT GHOST -- the peer wrecks the mech and never un-wrecks it, because
the un-wreck rides the master's respawn. Normal deaths replicated fine all
along (9 deaths -> 8 un-wrecks, benched), which is why only ejects ghosted;
* the manual chart's "-1000 ejecting" never materialised -- the negated kill
award and the death cost both live in the tail that never ran.
Fix: use the binary's own predicate. MovementMode is untouched by the eject's
alarm write, so the latch arms on an eject exactly as on a combat death.
WHY SEVEN RIGS MISSED IT: the punch-out was being REFUSED, not undelivered.
EvaluateEjectPermission (@0049fa1c) grants only on
liveWeapons < ejectMinWeapons || liveGenerators == 0 || coolantFrac < 0.05
|| (leg-gimped && !simLive)
-- armour damage satisfies none of them, and every bench ejected a healthy
mech. An [ejecttest] receipt (2 lines) proved the dispatch fired every time
and the handler declined; the "[eject] REFUSED (mech not crippled enough)" line
was sitting in the very first bench log, ungrepped. BT_KILL_SUBSYS's
comma-list form ("GeneratorA,GeneratorB,...") was already built for this bench.
Verified 2-node (scratchpad/night13/ejectreal.sh), before -> after:
PUNCH-OUT landed 0 (785 refusals) -> 1, charge=500
peer wreck-enters 1 -> 1
peer UN-WRECKS 0 (the ghost) -> 1
eject score (type=2) absent -> award=-1000.00, score 1000 -> 0
death cost never ran -> APPLYING, penalty=500
That -1000 is the manual chart's eject row to the digit: killBonus 500 plus the
500 self-damage tally, negated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
|
||
|
|
29b4d68ba6 |
scoring: the +1000 START grant -- BT's MissionStarting override was never ported
Seventh chart row. BT overrides MissionStarting purely to seed the score, and
the override was missing, so MESSAGE_ENTRY(BTPlayer, MissionStarting) resolved
to the inherited engine handler (which only does the fade-in) and the grant
never happened.
FUN_004bfbe8(player):
base_MissionStarting(player);
if (app->state == 4 && (player[0x29] & 0x40) == 0)
player[0x1c8] = 0x447a0000; // = 1000.0f
Both operands decode exactly against engine headers: application state 4 is
LaunchingMission (APP.h -- same enum whose 6 is EndingMission, already used by
the console flush), and simulationFlags bit 14 is NonScoringPlayerBit
(PLAYER.h: NonScoringPlayerBit = Entity::NextBit), so `(+0x29 & 0x40) == 0` IS
IsScoringPlayer(). Camera-ship/spectator players are non-scoring and correctly
get nothing.
CELL NOTE: the binary seeds the ENGINE cell (+0x1c8), not BT's own (+0x278) --
1995 carried two accumulators, which is why the KB suspected the pod's death
cost "may never have displayed". Our port has one currentScore, so grant,
awards and death cost land together and the chart reads coherently.
Also resets the console watermark so a fresh mission REPORTS the grant rather
than a difference from last round's tally.
Benched: both players "[score] mission start: player N:1 seeded to 1000",
scores run 1001.98 -> 1908.64 with kills=1 (1000 + ~400 damage + 505 kill).
Also corrects a FOURTH copy of the dead-code claim, in btplayer.hpp's ScoreType
enum ("type 0 has NO scoring arm ... per-hit inflicted credit never existed").
Its byte-scan was right that no TABLE entry binds @004c0200 and wrong to
conclude unreachable -- the vtable Dispatch override calls it directly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
|
||
|
|
98082e64a0 |
KB sweep: retire the three claims that WERE the scoring bugs
Night 13 turned up three confident KB/source notes that each closed off a
working path, and each one was the defect:
1. context/combat-damage.md -- "@0x4c0200 is in NO table entry: dead code",
used to justify retiring per-hit inflicted credit in build 787. It is
reached through BTPlayer's Dispatch override (vtable @00513300 slot 3).
(corrected in
|
||
|
|
e0b91df3e1 |
scoring: CORRECTION -- the death cost was never missing; my arithmetic was
Retracts the "open item" claimed in
|
||
|
|
2fcce53bb2 |
scoring benches: self-inflicted + deaths counter verified (night 13)
scoreself2.sh -- one rig covering three unverified items, using a SELF-DESTRUCT to reach the same paths an eject does without the panic button that defeated five earlier rigs. VERIFIED: chart '-1 each self-inflicted point' type=0 award=-40.00 x11, total -440 self-kill negation (#134) type=2 award=-539.00, kills NOT incremented deaths counter PLAYER_DEAD deaths=1 tally=1 OPEN, found by arithmetic: the -500 death cost fires on a COMBAT death (prior run: victim total exactly -500.00) but NOT on a self-kill -- A's total is exactly -440 + -539 = -979, with no -500 in it, despite advDamage=1 and the role bound. The cost is dispatched by a DIRECT Player::ScoreMessageHandler() base call, bypassing the BT handler, so it never reaches the matchlog and only the totals expose it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ |
||
|
|
a4bfb64ace |
scoring: BIND the scenario role -- one commented-out line zeroed the whole chart
BTPlayer::scenarioRole was never assigned. The lookup sat commented out with
"the BT role registry (BTMission::GetRoleRegistry()->Lookup) has no WinTesla
analog, so the scenarioRole set by the base Player ctor stands" -- and the base
ctor sets it to NULL (PLAYER.cpp:680). So it stood NULL forever.
Every scoring value the game has hangs off that pointer, and the shipped
content authors them correctly. New ungated receipt in the ScenarioRole ctor
prints what a real mission loads:
[role] 'Role::Default' model='dfltrole' killBonus=500 deathPenalty=500
dmgInf=1 dmgRcv=0 bias=1 ff=1 return=1000
That IS the original manual's scoring chart -- +500 a kill, -500 a special-case
death, +1 per damage point. With the pointer NULL every award multiplied
against zero: kills scored the damage tally alone (4.88), the eject charge read
0 (the field log's "PUNCH-OUT: charge=0 (role killBonus)" = #134's missing
penalty), and the death-cost block was skipped.
The analog DOES exist: Mission::GetScenarioRole(name) (MISSION.h:162) walks
scenarioRoleChain -- the same dictionary BTL4Mission fills via AddScenarioRole()
when it parses the role pages, whose own comment says the WinTesla base exposes
it. Same lookup, same key. Falls back to Role::Default when a creation
message names an unknown role (shipped content authors exactly one page), and
logs BOUND/NULL so this cannot fail silently again.
Benched cross-node:
role binding player 2:1 BOUND, player 3:1 BOUND
KILL AWARD 505.88 (was 4.88) <- chart's +500, verified
death cost victim total -500.00 <- chart's -500
inflicted still tracking, killer total 1017.32 kills=1
The -500 on an ORDINARY combat death is AUTHENTIC, not a bug: the binary's gate
is advancedDamageOn alone (@004c05c4 tail: `if (player+0x264 != 0) { -role+0x20 }`),
verified in the decomp. It only shows now because the role finally binds. It
also reconciles the chart's two death rows: an EJECT costs -500 (death) plus its
self-kill negating its own ~500 award = -1000, and an ammo death costs -500.
CORRECTION to my own earlier note: returnFromDeath=1000 is NOT the chart's
"+1000 starting the game" -- role+0x28 is a lives/return gate (`if (< 1)` ->
mission review, else respawn). The 1000 is coincidence. That row is still
unlocated and is most likely console-side.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
|
||
|
|
b2498ca39a |
scoring: the type-0 arm must RETURN, not break -- it was clobbering scoreAward
Chasing the duplicate rows from
|
||
|
|
1324c81719 |
scoring: land inflicted credit on the OWNER's machine (Steam + console safe)
Completes 2772175/e82f54c. The interceptor restored the credit; this puts it
on the right node, so a player's score accumulates again.
The operator corrected two of my claims, and both were load-bearing:
1. Scores DID accumulate before build 787. Checked: build 774 already had
`currentScore = 0` in the console flush, so the flush was never eating
score. My "score zeroed every interval" theory is dropped. The watermark
from
|
||
|
|
e82f54c957 |
scoring: the score AUTHORITY is the operator console -- and our port has none
Follow-up to
|
||
|
|
27721754da |
scoring: restore the type-0 INTERCEPTOR -- per-hit inflicted credit was live all along
Players reported scoring and K/D going screwy on 4.11.817. Cause: build 787
(#45/#134) retired the port's per-hit inflicted crediting as an "invention",
on the strength of a KB claim that the type-0 score handler was dead code.
That claim was wrong.
BTPlayer overrides Dispatch -- vtable @00513300 slot 3 = FUN_004bffa0 -- and
splits type 0 off BEFORE base dispatch:
if (msg->id == 0x16 && msg->type == 0) FUN_004c0200(...); // ScoreInflicted
else base dispatch;
@004c0200 names itself in its own Verify string
("BTPlayer::ScoreInflictedMessageHandler") and computes
CalcInflicted(basis) -> negate if target==self -> x (targetTonnage/ownTonnage)
-> accumulate into +0x278. ScoreMessageHandler's type-0 arm Verify-rejects
precisely BECAUSE this interceptor guarantees type 0 never reaches it.
The port had the handler, faithfully reconstructed, and no interceptor -- so
Block B's inflicted reports all landed in the rejecting arm and banked 0.
Per-hit damage credit was silently deleted.
Independently corroborated by the ORIGINAL MANUAL'S SCORING CHART (filed as
reference/manual/scoring_chart.webp, from Lynx): "+1 each damage point scored
on opponent's armor" and "-1 each self-inflicted point of armor damage" -- the
negate-if-target-is-self arm exactly. Without that chart the dead-code note
would probably have stood.
Verified (scratchpad/night13/scoreverify.sh, cross-node kill, 2 nodes):
type-0 Verify rejections 0 (was firing on every non-lethal hit)
inflicted score rows 83 awards 0.98..25.00, all positive, tracking damage
kill path un-regressed type=2 award=4.88 kills=1, victim respawn x1
KB: combat-damage.md report B and the score-model paragraph rewritten, with
the full chart and THREE unreconciled rows flagged [T4] -- flat +500 kill vs
the benched 4.88, +1000 at game start, and -1000 eject / -500 ammo (which
would live in ScenarioRole::specialCaseDeathPenalty @role+0x20, read by the
port but authored nowhere in shipped content).
KNOWN, NOT FIXED HERE: in MP the running total does not persist -- currentScore
is flushed to the operator console and ZEROED (btplayer.cpp ~1219) because the
binary treats it as a console DELTA. Restoring the credit makes that very
visible (bench: totals climb to ~35 then reset). Needs its own decision; the
chart's "+1000 starting the game" implies a persistent total lives somewhere.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
|
||
|
|
4642129e76 |
#108 peer-side WRECK receipt: make ghosts countable
The un-wreck receipt 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 found ONE ghost while testers
reported many, and there was no way to separate a real count from a chassis
accident.
Emit one ungated line for every REPLICANT entering the wreck state, symmetric
with the existing un-wreck line, so a log's ghost count is exactly
(wreck-enters minus un-wrecks) per entity:
[wreck] replicant H:E entered wreck state (mode X->9) at (x,z)
[respawn] replicant H:E un-wrecked + warp (mode 9->1) at (x,z)
Verified 2-node (200s, force-damage victim): 5 enters, 5 exits, exactly
paired -- while the old marker printed ZERO times in the same run. That gap
is the point: five real deaths, invisible to what the census was reading.
Also lands the night-13 census tooling (ghostcensus.py) and the eject benches
that did NOT reproduce, with their failure modes in the headers so the next
attempt does not repeat them: five rigs failed to trigger a punch-out at all
(BT_BTNTEST never reached the mapper for 0x3D or 0x14; BT_EJECT_AT did not
fire either). Panic-eject replication remains UNTESTED by bench.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
|
||
|
|
43d30ca7e4 |
#140 receipts: name the corruption case in a FIELD log
Two ungated one-liners, because no bench here reached the failing path and
the next playtest is a better instrument than more automation:
[glasswin] destroy entry #N windows=M -- N=2,M=0 is the double-destroy
[glasswin] saved ... (live=L remembered=R) -- live=0 IS the corruption case
(pre-cache that wrote a file
holding only the plasma line)
Also lands the benches that did NOT reproduce it, with their failure modes
recorded in the headers so the next attempt does not repeat them:
layoutsave.sh (round trip -- passes on the fixed build), layoutteardown.sh
(graceful WM_CLOSE; still never reaches the dtor chain), layoutround.sh (MP
round boundary; the relay never started the mission inside the window).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
|
||
|
|
c04bec0a52 |
#140 glass_layout.cfg lost every MFD line on a desktop teardown
Regression from |
||
|
|
6a96fb6420 |
#52 the peer STANDING-LOCK: case 0's fallthrough was intercepted
A replicant could not start walking between gait-change records. The port's
body case 4 (the task-#64 lockstep twin) is an INSERTION sitting between case 0
and the advance group; in the binary case 4 is a MEMBER of that group
(FUN_004a5678 @004a5678: case 2,3,4,5,8,... -- no turn block, no speed exit),
so case 0's fallthrough is meant to land on Advance(). The insertion caught it.
On a replicant that is not a race but an identity: case 0 arms walk iff
standSpeed < bodyTargetSpeed, and the inserted block resets iff standSpeed <
bspd -- where bspd IS bodyTargetSpeed for a replicant. Same expression, so arm
and reset fire on the same frame, forever, and a peer parked at Standing with a
live replicated demand never cycles. bodyCycleSpeed stays 0 while position
advances from dead reckoning: the skate.
This is the sequel to
|