832bec0966fe163c89999a7eb717bc1cb14815fc
11
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 |
||
|
|
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
|
||
|
|
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
|
||
|
|
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
|
||
|
|
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
|