2df1369168e9952f2d720783a1783255c6d340d5
706
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2df1369168 |
#116 THE REAL STEAM CAUSE: experience never rode the lobby -- joiners' pages were authored by the HOST as the "veteran" fallback
The user's question broke my first story: "did lynx host tho?" He did (Oracle joined LYNX's lobby that night) -- but more importantly the affected players' logs read mode=steam, which gets the FULL menu, so the FeJoinOnly fix in |
||
|
|
3109cfc49a |
#116: steam JOINERS were silently locked to VETERAN -- restore their experience selector
Oracle isolated it: "standard mode still having heat and leaks only occurred in
steam not in solo. Sauron and Lynx did not have the issue but Conn Man, Rajel
and myself did." The night-9 logs close the case:
affected (Oracle/ConnMan/Rajel): [exp] experience=2 heatModelOn=1
clean (Sauron's std sessions): [exp] experience=1 heatModelOn=0
CAUSE: the glass front end's JOIN layout trimmed the menu to "the mech list +
the JOIN button; everything else is the operator's call" -- and the experience
selector's hidden default is 2 (veteran). So HOSTS got whatever they picked
(Sauron hosted the lobbies all night -> his standard landed), while every
JOINER launched as veteran regardless of anything they did -- which is why
Oracle's cycling-the-settings experiment changed nothing, and why solo (full
menu) never showed the problem. heat >= veteran, hence "standard but heating".
DESIGN CONFIRMED BY THE USER before fixing: experience is PER-PLAYER by the
original design -- the sysop set each user's tier and mixed-experience matches
were legal. Architecture already supports it end to end: each node's master
player reads its OWN egg's experience (btMission+0xE4 -> BTPlayer @004c0bc8),
so per-node choice IS per-player choice.
FIX: the join layout gains the GroupExperience selector (the selection->egg
write path is the pre-existing host path, field-proven). Also: the front end
now prints "[fe] pilot experience=<x> (join|host/solo)" at launch, so every
future field log answers this class of report without asking anyone.
VERIFICATION BOUNDARY, stated plainly: builds both configs; the write path is
shared with the host flow which the field already exercises; the visible
selector + the [fe] line need one join-mode launch to eyeball, and the field
re-test is Oracle running a standard steam match as a JOINER and seeing no
heat.
Tooling note recorded in test-harness.md: bash-heredoc python collapses one
backslash level even single-quoted -- a "\n" arrives as a real newline and
replaces silently no-op. Build backslashes from bytes([92]); verify replaces
by length delta, not by the script saying "fixed". (This burned four edit
rounds tonight and several earlier C2001 hunts.)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
1bcee1f9eb |
#96: the effective-rate theory is corroborated -- upgrade the calibration provenance T3 -> T2
Frame-rate testimony from the morning follow-up, now on record beside the bytes:
* Oracle, asked if the pod frame rate was rock solid: "I think I could do as
well with a flipbook"; heavy fights "would turn to a slide show".
* Lynx (original era): "RP had a target frame rate of 30FPS. It wasn't hard
coded or locked. BT never had a locked frame rate in 1st release
testing... coming from 3.0, where 20FPS was a miracle."
* The engine pacer's own design agrees: fixed budget + background fill + NO
overrun catch-up (APPMGR.cpp, T0) = "target 28 but allowed to slip", with
the 18.2065 BIOS fallback anticipating degraded timing.
Also settles epilectrik's open confirmation ("frame rate is advancing at least
some of the subsystems -- i have to fully confirm that"): ALL of them --
UpdateManager::Execute runs every interesting master every frame,
Entity::PerformAndWatch runs every executable subsystem per entity tick, no
throttle at any layer. The crisp rule: CADENCE is frame-linked for
everything; only terms missing a dt multiply are rate-SENSITIVE.
Corollary worth keeping: a choking 486 generated LESS heat/s exactly during
the biggest fights -- the slideshow was mercy. Veteran memory of seek-4
endurance encodes the LOADED rate, which is why bracketing by their consensus
is the correct instrument rather than a concession.
Follow-up filed in the KB: sweep the reconstruction for OTHER dt-less per-tick
accumulators (known: particle trail density, rendering.md; the 15-sample
heatFilter window is rate-dependent smoothing, benign). Each found one should
normalize against the same 28-nominal/bracketed-effective pair.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
ac94ce6a31 |
#96: DISSIPATION AUDIT -- byte-faithful end to end; the cooling-too-weak hypothesis is DEAD
"no guesses. we want to decompile and find the right answer" -- done. Every
function in the heat-removal chain read from the decomp and diffed against our
reconstruction:
HeatSinkSimulation @004ad924 E += pending; T = E/mass; alarms EXACT
UpdateHeatLoad @004ad7f0 radiated = T x coolant; clamp(0.002x) EXACT
ConductHeat @004ad8ac exchange into linked + rebalance EXACT
ComputeHeatFlow @004ad9ec (T1*mScale - (E1+E2+pend2)/den) * tau
* (1 - e^(-dt*k*(cool/cap)*flow/den)) EXACT
BalanceCoolant @004ada94 radiated-equalising move, 3 clamps EXACT
Condenser sim @004ae4d8 mScale = clamp>=1((1-zoneDmg)*refrig) EXACT
Reservoir sim @004aef78 inject-only; thermally passive EXACT
Every term is properly dt-scaled -- the dissipation side is rate-INDEPENDENT.
The only rate-dependent energy term in the whole thermal system remains the
myomer kinetic generation, i.e. the calibration knob is pointed at the right
factor and nothing on the cooling side can absorb the blame.
LIVE chain verification (new [heatflow] census, incl. a RADIATOR tag on the
bank's own Performance, which the base-sim census could not see):
Myomers 490 -> Condenser5 420 -> bank 337 -> ambient (relaxing onto the
300K setpoint). The exit exists, runs, and holds the bank just above 300.
AND ONE REAL FINDING -- my night-8 "no per-chassis cooling variation" claim was
WRONG, and Oracle's stated mechanism is vindicated to the digit:
bank k = 0.1 x HeatSinkCount x 231000: Owens count=5 -> k=115,500
Thor count=13 -> k=300,300
The Thor sheds 2.6x the Owens' heat through the mech's ONLY ambient exit --
"a thor could run seek 4 for extended periods because of the number of
heatsinks" is exactly the authored model. My hsparm chassis comparison had
printed RESOURCE constants BEFORE the ctor's count scaling; the dump now
carries a warning to stop the next person misreading it the same way.
(The KB was never wrong -- open-questions.md:593 documented the radiator
scaling when task #9 landed it. The wrong claim lived in my #96 comment.)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
c28555bdc9 |
#96: TRACE THE CLOCK ORIGIN -- the pod ran at 28 fps (byte-proven); myomer heat re-bracketed to 14 Hz effective
epilectrik: "the issue seems to be related to a tick rate that advances the
heat accumulation... i need to trace out what the clock origin was." Traced.
THE CLOCK ORIGIN [T1]: the DOS binary keeps its engine frame rate in the
global DAT_0052140c, set once at startup:
0x401ace: mov [0x52140c], 0x41E00000 = 28.0f (nominal)
0x401ada: mov [0x52140c], 0x4191A6E0 = 18.2065f (BIOS-tick fallback)
The DOS main pushes it straight into the ApplicationManager ctor (0x401189:
push [0x52140c]), and ~90 sites across the image fmul/fdiv by it -- it is THE
per-frame<->per-second conversion scalar of the whole 1995 engine. The engine
executes every interesting master every frame and every executable subsystem
every entity tick (UPDATE.cpp / ENTITY.cpp, T0 -- no throttle at any layer),
so subsystem Performance cadence == this rate: the pod's myomer tick was
NOMINALLY 28 Hz. My previous 30 Hz reference (the i860 BOARD frame) was
wrong by 7%, not the 2x the veterans hear.
THE RESIDUAL IS EFFECTIVE RATE, NOT NOMINAL: Oracle, on the halved build,
still asks "If you could please halve the myomer heat rate again. We will
bracket to something reasonable." A 486 host missing beats halves the real
Performance cadence without changing any constant -- and the 18.2 fallback
existing at all says slow paths were expected. Statics cannot settle the
in-pod effective rate, so his bracket is the right instrument:
effective default = 28 [T1 nominal] x 0.5 [T3 testimony calib] = 14 Hz
BT_MYO_HZ=<hz> still overrides absolutely for bracketing
the resolved rate now logs under BT_MYO_LOG, tier tags inline
Measured (Owens, seek 4, flat out): peak T 1007 -> ~640, degradation never
reached -- the halving Oracle requested, delivered as one auditable knob
instead of a silent constant edit.
KB: decomp-reference.md gains the DAT_0052140c section (~90 consumer sites --
whenever the decomp shows an unexplained mul/div by _DAT_0052140c it is
per-second<->per-frame at 28); btl4main.cpp's "30 = pod authentic" pacer note
corrected (30 kept deliberately for display smoothness, the divergence now
documented). Candidate follow-up recorded: the frame-rate-dependent particle
trail density (rendering.md) should normalize against the same 28, which may
bear on Ronin's smoke-density report (#114).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
c791fae0f8 |
KB: new topic test-harness.md -- the bench machinery + the verification doctrine
User mandate (2026-08-02): "do tests like this from now on" -- after the #110 grind bench, where the field composition (peer fire destroying an arm over the wire) replaced the constructed proxy that had let #86 be called fixed while players kept hitting it. The topic carries two halves on purpose: DOCTRINE -- what counts as VERIFIED: 1. reproduce the REPORTED scenario, not a convenient adjacent one 2. scalpel hooks (BT_SELF_DAMAGE_ZONE / BT_KILL_SUBSYS / BT_FORCE_*) locate defects; they support a "fixed" claim only with proven path-identity to the field composition -- and the field composition still gets run 3. MP symptom -> two-node proof (master-side correctness says nothing about what a peer sees) 4. visual symptom -> pixel proof (gotcha 23) 5. coverage claims need the axis enumerated and measured (all gates, all chassis), because per-chassis behaviour lives in authored data 6. an unexplained extra effect in a passing run means the run has not passed MACHINERY -- the bench_common.sh contract (summarized, file = source of truth), single-node and two-node skeletons (relay, ports, affinity, fire cadence, GOTO_STOP standoff), process hygiene (stale-node taskkill first, never double-background, teardown kill order, stale-exe tells), and log-reading rules (capped diagnostics are not evidence of absence; alarm lines are not trends; name the actor at every refusal; field logs have no gates set -- spawn-time summaries ungated, per-frame traces gated). Routed: Quick Lookup row, CLAUDE.md reasoning step 4, build-and-run parity section, reconstruction-method Key Relationships. checkctx CLEAN. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
5d6ded73f0 |
#110: cross-pod verified BOTH ways -- the cascade replicates, and peer fire alone drives it
Two two-node benches close the last stated gap ("no two-node run has watched a
peer's view of an arm death"), the second at the user's insistence on the REAL
field composition after the first used the self-damage scalpel:
1. mp_armchain.sh (scalpel): B self-destroys dz_rarm mid-fight. A's screen
receives the whole cascade -- [zone-repl] dz_rarm lvl 0->1 AND the cascaded
dz_rgun lvl 0->1 -- plus the respawn reset coming back clean (1->0). B's
post-cascade AFC pushes turned out to be the RESPAWN RE-ARM, correct
behavior, not a gate failure. (Also confirmed: the self-damage harness
dispatches a real Entity::TakeDamageMessage -- the identical path a peer's
round takes, so the scalpel and the field share every line from the handler
down.)
2. mp_armgrind.sh (the field composition): stand-off at 180, NO self-damage
anywhere, A grinding B with real cross-pod fire for six minutes. On B:
* 25 incremental arm-zone hits from A's weapons via the cylinder tables
(dz_rgun reached 1.0 from pure accumulation -- 15 cascades on rehits)
* dz_rarm finished by B's OWN AFC bay cooking off (AmmoBinAFC100 BAY FIRE
350, half into dz_rarm) -- itself lit by A's fire crits: peer-originated
end to end
* [cascade] zone 9 -> AFC100 + AmmoBinAFC100 + Condenser6 force-failed,
fire gate refusals logged
On A: [zone-repl] dz_rarm 0->1 and dz_rgun 0->1 -- the shooter SEES the arm
and pod die. B symmetrically received A's torso deaths.
Also: DamageZone::ReadUpdateRecord gets a change-only [zone-repl] diagnostic
(BT_MP_LOG) -- the probe that made the peer side measurable; mp_armgrind.sh
opens with the stale-node taskkill the other benches already had (a leftover
node from an aborted launch joined a later lobby as a third instance).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
614849b4a6 |
#110 coverage: all 8 chassis + all 3 weapon-class gates verified through ZONE death
Answering "are you 100% positive this works for every weapon type" -- it was not a yes when asked, and the gaps were closed with measurement rather than assurance: 1. AUTHORED DATA, all eight chassis (zonesweep.sh; the [zone-armor] dump now carries the zone NAME): every arm zone AND every missile-rack zone on every chassis authors descend=1 (ava/bhk/lok/mad/own/snd/thr/vul); every gun/rack zone lists 1-4 critical subsystems; doors are inert (0/0/0); bhk uniquely adds destroySibs=1 on its arm chain. No chassis is missing the cascade authoring. 2. WEAPON-CLASS GATES, end to end through a ZONE death (not direct weapon destruction, which was #86's verification gap): ballistic AFC100 (projweap gate1) -- dz_rarm, ava1 [prior commit] energy PPC (emitter gate) -- dz_larm, ava1 [prior commit] missile NRK5_2 (projweap gate1) -- dz_rmissle, own1 [this pass] [cascade] zone 12 -> NRK5_2 + AmmoBinNRK5_2 force-failed [ammo] NRK5_2 -> NoAmmo (gate1): destroyed=1 All three fire-gate code paths are now exercised by the cascade. 3. One anomaly chased to ground rather than waved off: the Owens rack cascade also killed Avionics/HUD/Searchlight/ThermalSight -- zone 15 dz_searchlight, an authored CHILD SEGMENT of the missile rack (the sensor mast sits on the shoulder pod). Authored skeleton + authored descend flag, walked faithfully. Remaining surface NOT bench-verified, stated plainly: cross-pod -- the cascade is master-authoritative (crit plugs bind on masters only) and the resulting alarm states ride the existing weapon update records, but no two-node run has watched a peer's view of an arm death. The field re-test (Lynx/Conn Man's own audit, in MP) covers exactly that. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
06adaee523 |
#110: the zone destruction cascade is LIVE -- an arm now takes its gun pod with it
Lynx: "You can destroy an upper arm, and the gun pod is intact and can still fire." Conn Man's audit: three chassis, both arms, reproducible. And the user is right that this family was claimed fixed before: #86 gated fire on the WEAPON's own destruction and was verified by destroying the weapon directly (BT_KILL_SUBSYS) -- the field scenario was the ARM ZONE dying with the weapon subsystem healthy, which that bench never reproduced. Oracle's 693-night "I can still fire destroyed energy weapons" was almost certainly this mechanism. ROOT CAUSE -- a silent stub. Mech__DamageZone::RecurseSegmentTable @0049cad4 was transcribed faithfully... against three local shim types whose iterators unconditionally return NULL: struct SegmentIterator { DZRef *Next() { return 0; } }; struct SegTableX { SegmentRecord *operator[](int) { return 0; } }; The cascade fired on zone death (bench: 72 [cascade] lines), "descended", and touched nothing. Every line read correct; none of it did anything. THE REAL WALK (decomp re-read, FUN_0049cad4): iterate the mech's segment table (mech+0x300, vtbl+0x34 GetNth) to this zone's segment, then destroySiblingsOnDestruction (Wword 0x68): recurse every other zone on the SAME segment (seg+0xD0), setting graphic state 2 (Gone) descendOnDestruction (Wword 0x67): recurse every zone on every CHILD segment (seg+0xE8 -> child+0xD0) -- the arm takes the gun The engine ALREADY HAS both structures: EntitySegment::damageZoneTable @0xD0 / childIndexTable @0xE8 (TableOf is 0x18 -- byte-identical to the decomp offsets), populated at stream time by JMOVER.cpp:373 [T0]. The stub was never necessary. Implemented with the engine iterators via two new read accessors on EntitySegment (SEGMENT.h); the binary's asymmetry is reproduced as-is (the sibling loop checks graphic state != 1, the child loop does not; re-hits on a 1.0 zone re-run the cascade -- idempotent in effect). VERIFIED (ava1, self-damage to one zone, autofire everything): dz_rarm -> [cascade] zone 9 -> zone 17; AFC100 + AmmoBinAFC100 + Condenser6 ForceCriticalFailure'd; "[ammo] AFC100 -> NoAmmo (gate1): destroyed=1"; torso LRM5/SRM2 keep firing (correct) dz_larm -> [cascade] zone 2 -> zone 6; PPC + Condenser4 force-failed; "[emitter] 'PPC' fire REFUSED (destroyed=1)" x4998 -- the emitter gate now logs refusals BY NAME (the FIRED line never named its weapon, which is how #86's verification gap survived) regression smoke: frame time 7.13/7.20ms, 0 asserts, 0 cascades in ordinary combat (they fire only on genuine zone deaths) Authored data (now dumped under BT_DMG_LOG): descend=1 on exactly the four arm zones (ava1: 2/6/9/17), destroySibs=0 everywhere -- so legs/torso behavior is untouched by this change. KB: combat-damage.md cascade section; reconstruction-gotchas #26 (the silent-stub trap: a shim that returns EMPTY converts a reconstruction into fiction that reads correct -- shims must Fail() loudly or log their emptiness). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
da70bd58b2 |
#84: the replicant salvo now HOMES AT THE LIVE TARGET -- the stale-point double detonation is gone
Oracle (674): "missile appear to register hit explosions twice, once where
target was and again where the target is". Rajel (693): "once where the mech
was when I fired and on the expected impact".
MECHANISM, measured on two nodes (BT_PROJ_LOG "DET at" now logs EVERY contact
detonation, including damage-0 rounds, which were invisible before): the
replicant salvo mirror pushed rounds with NO entity handle at the FROZEN
fire-time aim point, and the pool's contact test is proximity to p.targetPos --
so mirror rounds "contacted" the empty air where the target USED to be and
detonated there, on every peer, every salvo (45 salvos -> 234 frozen-point
air bursts in a 200s bench). The true impact appears at the live position
(the shooter's own homing rounds on his node; the victim's damage reaction on
peers) -- two sites per salvo, exactly as reported.
THE BINARY'S MODEL (CLASSMAP, Missile flight-entity cluster): missiles are
ENTITIES with authoritative/GHOST Performance variants (Projectile
PTR_LAB_005129e8 / 005129f4) and their own WriteUpdateRecord (@4bef4c, slot 7)
-- peers ran ghost missiles updated from the wire and saw the TRUE trajectory.
One explosion, correct place, every node. The frozen-point mirror is the port
infidelity (the entity Missile itself stays blocked by the documented 2007
Entity-base mismatch; mislanch.cpp:301).
FIX: the MissileLauncher update record now carries the locked target's EntityID
(salvoTargetID; EntityID::Null = point fire). The mirror resolves it on the
receiving node via HostManager::GetEntityPointer (engine T0, an index-socket
Find -- NULL-safe on any ID), refuses non-mechs via BTIsRegisteredMech, and
pushes the mirror rounds WITH the handle -- they re-lead on the live local
replicant exactly as the master's own rounds do, and detonate at the true
position. Guards: record-length gate (a short record from an older build has
no ID field -- its tail would be garbage and must not resolve) + Null check +
registered-mech check.
VERIFIED (two nodes, 200s, missile autofire both ways):
* every record arrives len=52/52 with sane aims ([mlrec] diag)
* 22/46 salvos resolve (tgtID=3:22 -> the victim's local master) and their
mirror rounds home: the frozen-air DET population fell 234 -> 102, the
live-homing population rose to 155/207 per node
* the unresolved remainder is BENIGN and correct: EntityID::Null point fire
plus hostID=-1 LOCAL entities -- i.e. the victim's WRECK, which autofire
keeps shooting after the kill; a wreck does not move, so the frozen aim IS
its true position
Downstream expectations: Ronin's smoke-screen (#114) should drop by ~the mirror
half, and the audio census (#32) loses the duplicate Static3DPatchSource
explosion sounds on peers -- both to be read from the next field logs.
Also in this commit: [projectile] DET diagnostic (every contact detonation with
position/damage/handle/aim), [mlrec] record-arrival diagnostic (len/recID/
subsys/counter/aim, capped 200), scratchpad/night9/mp_double.sh.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
3354db4bd1 |
#32: RETRACT the retention diagnosis; count what matters (steals + true drops); name the saturating class
The reopen said "the pool fills and never returns a source". The 30s census in
the SAME field logs disproves it: free returns to ~227-230 between bursts and
reuses climbs ~20/s all session. free=0 on the failure line is true by
DEFINITION at the instant of a failed acquire -- the third instance of the
counter-sampling trap (live=256-vs-6, then hsparm greps, now this), read off the
alarm line instead of the trend line.
WHAT THE LOGS ACTUALLY SHOW
* The pool cycles; release-on-stop exists and works (the engine steal loop).
* During firefights CONCURRENT demand exceeds 240 and the priority steal loop
services each new sound by killing an old one -- continuously through
combat (failures spread across every decile of every combat session).
* Idle standing demand is ~13 sources. My "each component holds its SourceSet
to entity teardown, ~20-25 per mech" narrative was wrong.
* The raw ACQUIRE FAILED line count (6.8k-19k per log) is NOISE: the steal
loop retries after every failed attempt, so lines accumulate per EVENT and
most events still play via a steal. True drops were never counted.
CHANGES
1. Census now carries steals= and drops= (drops = the steal loop ran dry and
the sound NEVER played) plus a per-class drop histogram
("[audio] dropped by class: {class 1005 x4v: N} ...") -- all ungated, so
the next field logs are decisive instead of suggestive.
2. The ACQUIRE FAILED print is rate-limited to 1/30s and now names the
requesting class + voice count. 19k-line log spam distorted this triage.
3. BT_AUDIO_SOURCES=<n> now raises the POOL cap too (it previously raised the
AL context budget while the pool stayed at 240, making the field
experiment impossible to run).
MEASURED (9 mechs, missile autofire, 150s)
* cap 240: peak 130 sources, 0 fails -- demand tracks SHOOTER count, not mech
count; one shooter cannot saturate. A 6-shooter lobby pins 240.
* cap 48 (BT_AUDIO_SOURCES=64): saturation reproduced -- census
steals=223 drops=455, histogram names the classes.
* Dominant field requester (requested=4) = class 1005 Static3DPatchSource:
world-placed effect sounds, i.e. EXPLOSIONS. 1001 DirectPatchSource x1v
dominates drops at low cap; 1002 Dynamic3DPatchSource x3v present.
CONSEQUENCE FOR THE FIX ORDER: #84's stale-aim double detonation duplicates
exactly the saturating class on observer nodes. Fix #84 FIRST, then re-read the
field census; only if it still saturates does the budget experiment
(BT_AUDIO_SOURCES with frame time measured) become the play.
KB: the wrong night-9 entry in open-questions.md replaced with the corrected
diagnosis; gotcha candidate noted -- an alarm-line counter is not a trend.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
202ebbf84a |
KB + tracker: night-9 playtest (build 4.11.693)
CONFIRMED FIXED and closed: #95 (missile salvo damage), #97 (leak gauge 3 levels), #98 (loop-6 lamp), #99 (leak alarm audible). #95 deserves a note: a tester reported "LRMs still doing 3.3 damage", but that is the PER-MISSILE figure. 382 impacts across three 693 logs show the cluster being delivered correctly -- LRM20 3.25 x burst 5..20 (total up to 65), LRM15 3.33 x 3..15, LRM10 3.50 x 2..10 -- and those burst ranges match the binary's Random(n)+n/4 roll exactly. #32 REOPENED. The pooling change removed the alloc/free churn (the ~10% frame time is real) but every 693 field log still shows ACQUIRE FAILED (requested=4 live=240 pooled=240 free=0 ...) 19,182 fails (Sauron) / 15,333 (Rajel) / 6,818 (Ronin), high-water 225 of 240, starting ~1% into every session. `free=0` is the diagnosis: sources are never returned. This is precisely the risk flagged when the fix landed -- retention, not pool size. Players called audio "fine" the same night; the log disagrees. #84 still present, but the surviving explosion is a DIFFERENT mechanism from the bundled one that was fixed: "where the mech was when I fired" is the stale fire-time aim point already recorded as an open in rendering.md, i.e. an MP visual, not the message-manager path. Needs a two-node bench logging the OBSERVER -- the earlier two-node run verified damage delivery but never the visual. New: #108 ghost mech (peer never sees the death; shots still register, so it is a visual/replication desync, the inverse of #94), #109 panic/eject, #110 arm parent/child linkage (gun pod survives its mount -- three chassis, both arms; parentArtifactZone exists but only drives LOD), #111 death warp bubble shown to peers, #112 AFC shotgun spread (may be authentic Ultra-AFC burst), #113 LRM burn duration, #114 missile smoke as a smoke screen, #115 hot box, #116 Standard-vs- Expert heat gating, #117 per-chassis cockpit bounce. ⚠ A MEMBER OF THE ORIGINAL TEAM IS NOW TESTING -- "Ronin" (log user torak). He worked on the 1995 game and has played the pods recently. Treat his pod comparisons as primary evidence above reconstruction inference. First two contributions recorded in the KB: per-chassis cockpit bounce was deliberate, and NARC likely shipped as data without an implementation. ⚠ Conn Man ran the OLD build (.674) for all seven of his sessions, so his reports are not evidence about 693. His arm-linkage audit is still valuable -- nothing in 693 touched that path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9623399ec4 |
gitignore: never commit player field logs (Steam IDs / machine names / usernames)
Field logs have been kept out by hand until now -- nothing enforced it, so a stray 'git add -A' would have published player Steam IDs and machine names. Ignore *.log/*.rar/*.zip under scratchpad/night*/ while leaving the bench scripts in those directories committable, which is the existing convention. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
5a1a407ed1 |
pre-ship: resolve the new audio diagnostic gates ONCE instead of per frame
RunSequence and AudioIdleWatcher::Execute run per sequence per frame, so the BT_ATTRBIND_LOG probes added while chasing #99 were calling getenv ~2700x/sec on a 15-sequence mech. Small, but this is the same path the source-pooling fix (#32) just bought 10% of frame time in, and an unset diagnostic should cost nothing. Cached in function-static flags. Pre-ship smoke (combat, 2 kills, a respawn, clean teardown): frame time 7.18 / 8.27 / 8.71 ms -- matches the post-pooling 7.79ms baseline 0 asserts, 0 access violations, 0 audio acquireFails Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7c08e8bd03 |
#99: commit the missed audseq.h probe member (case-mismatched path in the previous commit)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
5a425320b7 |
#92: map the direct-fire hit->zone path; the FOOT is pickable over only 3% of a mech's height
Oracle, night 7: "Unable to damage the foot panels on Loki via direct fire from
front or side. Was able to damage on Thor."
FIRST, THE MECHANISM -- it is not what the issue assumed (missing collision
geometry). A direct-fire hit gets its zone one of two ways, decided at
Mech::TakeDamageMessageHandler by `invalidDamageZone`:
AIMED (invalidDamageZone=0): the zone rides in on the message from the
per-part aim pick, MechSegmentPick (#73, btl4vid.cpp).
UNAIMED (invalidDamageZone=1): DamageLookupTable::ResolveHit runs the authored
cylinder lottery -- impact HEIGHT picks a layer, ANGLE picks a pie
slice, and a weighted random roll picks the zone from that slice.
Measured live, both paths are in use: the Thor logged 16 unaimed hits
(invalidZone=1 zone=-1 -> ResolveHit) alongside aimed ones carrying real zones.
THE FINDING. MechSegmentPick is a BOUNDING-SPHERE test whose primary key is
SMALLEST RADIUS WINS -- a larger sphere can never beat a smaller one the ray also
grazes. The leg spheres, dumped in world space and IDENTICAL on both chassis:
knee centre y=1.868 r=1.505 spans y 0.364 .. 3.373
toe centre y=0.302 r=1.689 spans y -1.387 .. 1.991
The toe sphere is BIGGER than the knee's and they overlap heavily, so the toe can
only win where the ray misses the knee sphere outright -- i.e. below y=0.364.
Against a reference height of 11.16 that is a 0.36-unit window, **3.3% of the
mech's height**, and it sits right on the ground. Everywhere else a shot at the
foot is credited to the LEG. That is the reported symptom.
This is a PORT ARTIFACT, not authentic: btl4vid.cpp's own comment concedes the
sphere test approximates "the per-part semantic the 1995 mesh intersection
produced". Real mesh intersection has no such interference -- aiming at the foot
mesh hits the foot.
NOT EXPLAINED, and stated plainly: the per-CHASSIS asymmetry. Loki and Thor have
identical leg spheres, identical foot geometry (LOK_LFOT.BGF and THR_LFOT.BGF are
both 3082 bytes with the same token layout), and identical foot layers in their
damage tables (only the upper/cockpit layers differ). So nothing found here says
the Loki should behave differently from the Thor. The bench could not settle it
because BT_AIM moves the drawn RETICLE, not the pick ray -- there is currently no
harness to aim the pick at a chosen height. That harness is the next step.
Diagnostics added:
[pickgeom] one-shot dump of every pick sphere in WORLD space (zone, r, centre)
[pickcand] which spheres a ray actually threaded, their perpendicular d, and
which won -- the probe that makes "smallest wins" visible
[dmgtable] the whole authored DamageLookupTable: layers, slices, zone weights
[dmgresolve] per hit: localY, heightRef, layer, theta, resolved zone
[cylgate] invalidDamageZone / table pointer / incoming zone at the gate
(the last three under BT_DMGTABLE_LOG, the first two under BT_PICK_LOG)
plus scratchpad/night8/footpick.sh.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
8daac37e40 |
#99: VERIFY IN THE IMAGE -- the shared-alarm Stop is authentic; do not diverge
Answering "did you check the decomp or disassembly?" -- I had not. I had checked
the authored data (BTL4.RES) and the MUNGA engine source and called it authentic
on source lineage alone. Now verified in BTL4OPT.EXE:
same sources compiled in : d:\tesla_bt\munga\AUDSEQ.CPP / AUDWTHR.CPP strings
object factory : @00466184 case 0x4b -> alloc 0x5C -> ctor @0043ccec
vtable : 0x4ead48
ReceiveControl : @0043d3a4 -- switch on control IDs 1 / 2 / 9 / 12,
which is exactly our enum Start / Stop / Idle / Tempo
Stop case : @0043d3ca -> unconditional call StopSequence @0043d2d4
StopSequence : tests only its OWN isRunning (+0x28) then the Chase
loop -- NO refcount, NO awareness of other leak sources
Structurally identical to engine/MUNGA/AUDSEQ.cpp. So the arcade behaved the same
way: with 19 subsystems sharing one alarm sequence, any one of them clearing its
leak silences the alarm while the others still leak. AUTHENTIC -- do not "fix".
(The tempo bound check could not be used as an anchor: Verify() compiles out of the
retail build, so the AUDSEQ.CPP assert strings are present in .data but unreferenced
by code and invisible to the decomp export -- gotcha 21 again. The object-factory
route via the numeric ClassID is what actually located the class.)
Recorded in context/gauges-hud.md with the full chain and the address table, so the
next person does not re-chase it or "fix" it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
e058b6acb7 |
#99: trace the coolant-leak alarm end to end -- it IS implemented; found the real defect
Oracle: "alarm is not sounding despite an active leak" ... "was sounding,
resolved a leak, but there was another".
RETRACTION FIRST. I previously told the user the coolant alarm was "genuinely
unbuilt -- there is no alarm implementation to starve". That was wrong, and it
was asserted without checking. The alarm is fully implemented and the entire
chain works. Traced live, in order:
Condenser6.ReportLeak (= HeatSink::coolantActive) changes 0 -> 1
-> [watchpoll] CHANGE on that exact address
-> [matchfire] val=1 -> ctl 1 (StartAudioControlID)
-> [seqstart] the alarm AudioControlSequence, events=25 looped=1
-> [seqsend] ctl 8/16 (select), 6/1 (volume), 1/0 (START), 2/0 (STOP), ...
a three-part looping alarm: two chirps then an 8s sustained tone.
The authored design, read from BTL4.RES: 19 subsystems per mech each bind TWO
AudioLogicalTriggers to their ReportLeak flag -- match 1 -> Start, match 0 ->
Stop -- and ALL of them drive ONE shared alarm sequence.
SO WHY WAS IT SILENT IN .674? Almost certainly the audio source pool, fixed
after that build in
|
||
|
|
e6103d02b2 |
#97: leak gauge reaches all THREE levels -- the normalisation was dropped with an x87 expression
Players: "you can get up to three traingles which drains really quickly" ...
"only seeing one level (lowest) right now" (Oracle), corroborated by Draco
("3 triple delta being the top level").
The display was never the problem: BitMapInverseWipe is already built with
frames=3 and stacks three segments at two levels each (0..6). The LEVEL
computation was wrong.
Our Execute rounded the raw leak rate:
level = round(value)
The binary's @004c5d08 does NOT. Ghidra renders the round as a bare
`FUN_004dcd94()` because it drops the x87 expression feeding __ftol -- exactly
KB gotcha 19, which we already had written down. The real prologue, disassembled:
fild dword ptr [ebp-0x14] ; ST0 = (float)fullWidth (frames*2 = 6)
fmul dword ptr [ebx+0xb4] ; ST0 *= value (CoolantMassLeakRate)
fdiv dword ptr [ebx+0xb0] ; ST0 /= third (full-scale divisor)
call 0x4dcd94 ; level = round(ST0)
i.e. level = round(fullWidth * leakRate / fullScale).
Without the normalisation the gauge rounded a value that never exceeds ~1.0
(coolantDraw = zoneDamage * heatLoad, and heatLoad is clamp(0.002*T, 0, 1) --
constants re-verified from the image, including the 80-bit extended 0.002). So
level could only ever be 0, or 1 via the >0.0025 floor: ONE triangle, always, no
matter how bad the leak. Three of the six condensers being mis-lamped (#98) hid
how systematic this was.
Also corrected on the way:
* `third` was declared int but the binary FDIVs it -- it is a float. The two
call sites passed *(int *)(subsystem+0x150), i.e. the BIT PATTERN of a float
read from a RAW OFFSET into our own layout (the databinding trap): at +0x150
our layout has filterDecay, not the binary's field. Now resolved through a
complete-type bridge, BTHeatSinkLeakFullScale.
* filterDecay was initialised 0.4f; @004b8fec writes 0.15f (param_1[0x54] =
0x3e19999a). It is written-once-never-read elsewhere in the port, so the
correction is behaviourally safe and it IS the gauge's full scale.
Verified live: Condenser6 at 10% zone damage -> draw 0.0358 -> level 1 (one
triangle); the same formula reaches all three at heavier damage, and the mapping
spans draw 0.0026 (half of one) to 0.15 (three full).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
ee079083a3 |
#96: FIX -- normalise the myomer kinetic heat term to a reference frame rate
The previous commit only instrumented. This is the behavioural change.
The binary's drive-heat integrator (FUN_004b8d18) multiplies its climb and accel
terms by the time slice but NOT the kinetic one:
heat += ratio^2 * (1+dmg) * [ (1-accEff)*|vy|*m*g*dt
+ (1-velEff)*(0.5*m*|v|^2) <-- no dt
+ (1-accEff)*|v|*|a|*m*dt ]
A per-TICK energy is a constant on a fixed-frame 1995 machine and a variable on a
modern PC: heat/second scales with frame rate, so two players on different
hardware -- or one player in a heavy scene versus an empty one -- get different
heat from identical throttle. That is a defect independent of what the pod's
rate actually was.
The kinetic term is now multiplied by (dt * kPodFrameHz), which is exactly 1.0
when dt == 1/30 and makes the result machine-independent. This is the one
deliberate divergence from a verbatim transcription of @004b8d18, and it is
marked as such in the source.
Measured, Owens at seek 4 flat out:
before crossed degradeT(1000) at ~27s, peaked 1213, settled ~710
after peak 1007, settled ~525
i.e. roughly halved at our measured ~59Hz, which is the direction the players
asked for ("too much IMO", "def need to tone down the myomer heat some").
⚠ CALIBRATION IS NOT PROVEN. 30Hz is the i860 BOARD frame ([T1],
rendering.md:177); the HOST simulation rate is not separately established, and no
fixed-timestep constant exists in the image (searched 1/15..1/60 and 30/60 Hz --
the arcade computed dt from a clock, as we do). So the reference rate is the
best-documented value, not a proven one. BT_MYO_HZ overrides it with no rebuild
so it can be calibrated by feel.
Precedent for the bug class, already logged in the KB: "trail density is
frame-rate-dependent (2/frame @60fps = 2x pod density)" (rendering.md:222).
NOT settled: whether the per-chassis ordering matches player memory (a Thor
sustaining seek 4 where a light chicken-walker cannot). Post-fix benches put the
Thor HOTTER (crosses at 40s vs the Owens at 47s), but those runs are BT_GOTO
path-driven and the acceleration term is large and path-dependent, so they are
too noisy to conclude from. The cooling data being identical across chassis IS
solid (measured, previous commit).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
9dd7d48b41 |
#96: instrument the myomer heat model -- per-mech profile CONFIRMED, overheat cause LOCATED
Two player claims, both now answered from the decomp + measurement rather than
inference.
THE EQUATION (FUN_004b8d18, constants read from the image: _DAT_004b8ee4=0.5,
_DAT_004b8ee8=0.0 (the fabs), _DAT_004b8eec=1.0):
heat += ratio^2 * (1+damageLevel) *
[ (1-accEff)*|vy|*m*g*dt climb POWER
+ (1-velEff)*(0.5*m*|v|^2) kinetic ENERGY -- NO dt
+ (1-accEff)*|v|*|a|*m*dt ] accel POWER
Our implementation already reproduces this verbatim, dt-less term included.
CLAIM 1 -- "each mech has a unique heating profile". TRUE, and working, but NOT
by the mechanism the player described. Measured across thr1/own1/mad1/vul1:
* the myomer record is IDENTICAL on every chassis
(velEff 0.995, accEff 0.8, gears 3000/5000/7000/9999, rec 2,
degradeT 1000, failT 2000, thermalMass 250000)
* the heat-family COUNT is identical too -- 6 Condensers, 1 HeatSinkBank,
1 Reservoir, 4 Generators on all four
* every cooling parameter is byte-identical Thor vs Owens (condenser
conductance 315000 / mass 420000, bank 231000 / 1.39e6, reservoir
190000 / 3.42e6)
So there is NO authored per-chassis cooling variation. The profile emerges from
the equation instead: heat ~ m*v^2, and light mechs are faster. Measured at
seek 4, flat out:
thr1 mass 70000 |v| 11.34 kinetic/tick 49026
own1 mass 35000 |v| 17.22 kinetic/tick 57350
The Owens is HALF the mass and generates 17% MORE drive heat, because v^2 beats
m. That reproduces the player's OUTCOME (a Thor sustains seek 4, a light
chicken-walker cannot) via speed, not heatsink count.
CLAIM 2 -- "it runs too hot". The kinetic term carries NO dt: it adds an ENERGY
every TICK, so its contribution per SECOND scales with the tick rate. Measured
dt here is ~0.017 (~59Hz) and variable. The other two terms are power terms and
are rate-independent. On flat ground the climb term is additionally dead --
gravity reads 0 (the carried "environment gravity unwired" open), so hills do not
heat at all right now.
NOT yet established: the 1995 tick rate the dt-less term was calibrated against.
Until that is pinned the OVERHEAT FACTOR is unquantified -- flagged, not guessed.
Adds three diagnostics, all under BT_MYO_LOG:
[myoheat] now splits climb/kinetic/accel + dt + the kinetic share
[myoparm] one line per myomer: efficiencies, gears, thermal thresholds
[hsparm] one line per heat subsystem: conductance + thermal mass
and three benches (myoheat/myoparm/myocmp) that produced the tables above.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
56f15b569a |
#98: prove the lamp fix -- table pinned to the image, three condensers verified live
Follows the request to prove the whole fix rather than the one case I had.
1. TABLE, all indices: scratchpad/night8/lamptable_check.py asserts the
reconstructed arrays against BTL4OPT.EXE and exits nonzero on drift.
kBTCondenserLamp @0051d058 MATCH 07 2F 2E 2D 2B 2A 29
kBTPlacementLamp @0051d070 MATCH 29 1A 1B 1C 1D
kFixed (FUN_004cc148 switch) MATCH 2F 2E 2D 2C 2B 2A
This covers condensers 2/3/5 without hunting zones for them: there is no
per-index code path, only the array contents, and those are now pinned.
2. LOOKUP PATH, live: condensers 1, 4 and 6 resolve 0x2F, 0x2B and 0x29 --
exactly the table. Condenser 4 is the meaningful control: my reverted "fix"
would have given 0x2C. Myomers (eng-page path, 0x25/0x21) and SRM6_1 (quad
button, 0xd) still annunciate, so other subsystem classes are unregressed.
3. GUARD: condenserNumber is parsed from the name's trailing digit
(NameTrailingNumber, mirroring the binary's atoi), and every condenser in
BTL4.RES is Condenser1..Condenser6 -- no Condenser0, no bare name. So the
1..6 guard covers every shipped case and slot 0 (0x7) is unreachable. This
was the real risk in changing `n >= 0` to `n >= 1`; it is closed.
4. PIXELS: leaklamp_pixel.{sh,py} capture a leaking run and an undamaged control
run from the cockpit and difference their per-pixel temporal variance. The
leak is visibly real -- the COOLANT reservoir drains on screen (S 331->330
while the control sits at 329).
⚠ WHAT THE PIXELS DID NOT SETTLE, and it is not a testing gap. Lamp 0x29 is
ALSO kBTPlacementLamp[0]: DAT_0051d058[6] and DAT_0051d070[0] are the SAME int32
-- the two tables abut. So condenser 6's lamp may not be its own loop button at
all, and slot 6 may be an overrun in the BINARY too (its read is unchecked).
Reproducing it is the faithful choice either way, and we now do exactly what the
binary computes -- but whether a pilot sees loop 6's own button light is a
question only someone who played the original can answer. Asked on the issue.
CORRECTION:
|
||
|
|
c50236a5e6 |
bench: shorten the leak-lamp run; verified condensers 1/4/6 resolve 0x2F/0x2B/0x29 (the binary's table), plus Myomers + SRM6 eng/quad lamps unregressed
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0b983703a4 |
#98: CORRECTION -- restore the byte-verified condenser lamp table; my previous mapping was a guess and was wrong
|
||
|
|
0254d9ef34 |
#98: leaking condensers now flash the RIGHT button -- half of them flashed the wrong one, one flashed nothing
Field reports were all "intermittent": "the display buttons aren't always
flashing or lighting up on leaking components" (Oracle), "I'm getting leaks but
no indicators" (Lynx), "I am getting indicators sometimes" (Sauron). It is not
intermittent -- it is per-condenser, and three of the six were wrong.
condenserNumber is 1-BASED (verified live: 'Condenser6' reports 6), which the
lamp table's own comment stated. The guard was `n >= 0 && n < 6` -- a 0-based
bound -- and the table it indexed was missing 0x2C:
condenser 1..3 -> 0x2F 0x2E 0x2D correct
condenser 4 -> 0x2B WRONG (loop 5's button)
condenser 5 -> 0x2A WRONG (loop 6's button)
condenser 6 -> rejected NOTHING flashes
So a leak in loop 6 annunciated nowhere, loops 4-5 lit a neighbour's button, and
loops 1-3 were fine -- which from the cockpit reads exactly as "sometimes".
Condenser N drives cooling-loop N, whose six lamps are the same ones the
coolingLoop1..6 codes already resolve through, so the fix routes condensers
through that verified map (BTFixedLampOf(N-1)) and retires the duplicate,
partly-wrong table rather than patching it.
⚠ PROVENANCE: the retired table cited @0051d058 as byte-verified. That address
holds gauge-type NAME STRINGS, not lamp ids, and a byte search for the six loop
ids as consecutive int32 finds nothing -- so neither the old table nor the new
mapping is byte-verified. The correction rests on three checkable things: the
1-based numbering (live), the guard contradicting its own documented indexing,
and six condensers mapping onto the six cooling-loop lamps of the verified fixed
map. [T2 -- behaviour verified, not byte-grounded.]
Also adds the missing diagnostic on the silent path: an alarm item that matched
its condition but resolved no lamp now names the subsystem and why, instead of
returning quietly. That is what found this, and it immediately surfaced a
SECOND gap for someone to pick up: a destroyed HeatSink (condition 0) resolves
no lamp either, because it is neither Condenser nor Generator nor a
PoweredSubsystem with an aux screen.
Verified (scratchpad/night8/leaklamp.sh, BT_LAMP_LOG):
before [galarm] condition 2 ... sub 'Condenser6' -> NO LAMP RESOLVED
after [galarm] condition 2 ... -> lamp 0x2a FLASH
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
bb7a6abe70 |
#32: correct the stale "no field complaints since" note + record the frame-time A/B
Found while checking whether a player had reported this before: the code already
carried a prior analysis of this exact error which deliberately decided NOT to
act on it, concluding the 2026-07-23 atomic-delete fix (
|
||
|
|
ad9dfade88 |
#32: POOL the OpenAL sources instead of creating and destroying one per sound
Every 4.11.674 player log is saturated with acquisition failures -- 3031, 4657, 5245, 6275, 6571 across five machines. In Lynx's largest session the first failure lands 9.3% in and they continue to 96.8%: once it starts it never recovers for the rest of the match. CAUSE: RequestAudioChannels called alGenSources() per sound event and ReleaseSourceSet called alDeleteSources() on release -- create and destroy per sound. OpenAL sources are a scarce driver resource (OpenAL Soft caps a context at 256) and a combat burst churned straight through the ceiling. The two counters looked contradictory and were the tell: ACQUIRE FAILED always printed live=256 while the 30s census printed live=6. Same global, sampled at different moments -- sources spike to the cap during a burst and drain back between them. Churn, not a steady leak. FIX: generate sources once, up to a cap, and recycle them through a free list. Release scrubs and parks instead of deleting. Steady-state play performs no AL allocation at all. The scrub is load-bearing, not hygiene: a recycled source carries whatever the previous owner set, and the engine sets AL_LOOPING per sound (L4AUDLVL.cpp:327). Hand a looping source to a one-shot and it plays forever -- which is the "sound stuck looping" family (#51, #5). Every reusable property is reset at the single point where a source changes owner. VERIFIED (scratchpad/night8/audiopool.sh + the two-node mp_burst.sh): solo, sustained fire : 0 failures, pooled 152, reuses 21998 two nodes, 4 min : 0 failures on both, pooled 148/149, reuses ~7000 each ⚠ HONEST LIMIT: the bench does NOT reproduce the field failure -- the PRE-fix binary also scores 0 on it (peak 49 live), because solo/2-node combat is not dense enough to reach 256. So this verifies the pool works and allocates nothing in steady state; it does NOT by itself prove the field failures are gone. The five field logs remain the "before". Peak demand is set by how many audio COMPONENTS are alive (each reserves a SourceSet of up to 25 voices and holds them), not by audible sounds: measured high-water 138 solo, 149 two-node. That scales with player count, so the cap is set near the driver ceiling (240) and the pool now logs its high-water mark once per 25-source band -- so the next playtest sizes this from field data instead of a guess. Growth also self-limits: if a driver offers fewer sources than the cap, alGenSources simply fails, growth stops, and the pool recycles what it has. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3a22c334ac |
bench: two-node CROSS-POD damage verification for the #95 burst change
Projectile damage now Dispatches directly at the victim instead of going through
the shooter's SubsystemMessageManager, so cross-pod delivery had to be proven on
real nodes rather than assumed.
RESULT: cross-pod delivery intact, and the cluster count survives the wire.
Node A fired 38 missile rounds with bursts {1:5,2:6,3:6,4:7,5:7,6:7}; node B
received exactly the matching zone applications {2:12,3:18,4:28,5:35,6:42} --
i.e. rounds x burst, per burst band, exact. 135 of B's 142 explosive
applications carried burst > 1 (the manager path used to drop it to 1).
Energy stayed at burst 1 on both nodes; no crash on either.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
c23d06cbb5 |
KB: correct the burstCount claim -- it is honoured by the CALLER, not cosmetic
The KB asserted as [T1] that "burstCount is cosmetic for zone damage". Half of that is byte-verified (DamageZone::TakeDamage @0041e4e0 really does ignore it); the other half was an inference that inherited the [T1] tag and then justified task #62's salvo-lead design -- which silently divided every missile salvo by its missile count (gitea #95). Corrected in both places it appeared (combat-damage.md:334 and :884): the CALLER, Mech::TakeDamageMessageHandler @0x4a0423-0x4a04d8, applies TakeDamage burstCount times and re-rolls the struck zone per burst. burstCount = number of applications; load-bearing for missile cluster count, splash falloff and the gyro bounce. Also records that the arcade Missile dispatches DIRECTLY at the victim rather than through the message manager (whose consolidation drops burstCount). New gotcha 24: a verified fact, over-generalised, becomes a wrong design premise. Detection smell -- a "cosmetic/unused" claim about a field other code still computes carefully. Nobody spends instructions randomising a decorative value. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3b7c19c232 |
#95: REVERT the salvo-damage hack; deliver the cluster the way the binary does
Supersedes the mislanch change in |
||
|
|
efc3e9ff1a |
#95/#84: missile salvos deliver their FULL authored damage, and stop double-exploding
THE SALVO DAMAGE (#95). Players measured an LRM 15 landing "3ish points". The logs agreed: [projectile] IMPACT damage=3.33333 (Oracle, LRM15) and 3.5 (Rajel, LRM10). Those are right PER MISSILE -- 50/15 and 35/10 -- and the bench shows why the salvo still under-delivers: each launcher pushes N rounds of which exactly ONE carries damage. Two individually-correct changes composed into an N-fold shortfall: * The ctor does what the binary's MissileLauncher ctor does (@0x3ac/@0x3d4): damageData.burstCount = missileCount; damageData.damageAmount /= missileCount; The record holds the PER-MISSILE amount plus the count; the arcade reconstitutes amount x burstCount when it applies the hit. * Task #62 then correctly stopped the port applying the hit once per visual round (that was ~missileCount-x too lethal) by damaging only the lead round -- but handed it the already-divided amount. Our DamageZone::TakeDamage is `damageLevel += amount * scale` and drops burstCount, so the salvo delivered amount/missileCount. Since the port collapses the cluster to one damaging round, multiply the count back in there. Bench: an SRM6 salvo now lands amt=35 (the authored total) taking a zone 0 -> 0.556, where it previously landed 5.83. THE DOUBLE EXPLOSION (#84). Oracle: "missile appear to register hit explosions twice, once where target was and again where the target is." There are two spawn sites: BTSpawnRoundDetonation at the round's own impact point, and the message manager's bundled explosion at the CONSOLIDATED point a frame later. The duplicate was known and thought harmless -- "among a rippled volley it is invisible" -- which held only while a salvo landed N detonations. A projectile now marks its weapon (MarkRoundDetonated) and the consolidation skips queueing a second blast for it; direct-fire weapons never mark, so lasers/AC keep the bundled explosion they rely on. Bench: 4 missile impacts -> 4 SKIPPED, while 11 direct-fire hits still queue normally. SWEPT CONTACT (#84 tail). Contact was a 10-unit sphere sampled only at the END of each step. With the authored thruster live (#84) field rounds arrive at v=955 -- a ~16 unit step at 60fps, larger than the radius -- so samples can straddle the target. (Pre-#84 rounds flew ~100-300 = a 1.7-5 unit step and could never skip it: the velocity fix exposed this, it did not cause it.) Now tests the whole segment travelled and bursts at the point of NEAREST APPROACH, which also stops the detonation being flung past the target at speed. RETRACTION: I posted tunnelling as the leading explanation for the lost salvo. The bench disproves it -- zero fizzles, and the "missing" rounds are the dmg=0 visual rounds of the cluster, which never registered damage by design. The sweep is kept as speed-independent robustness, not as the #95 fix. Bench: scratchpad/night8/salvo.sh (BT_PROJ_LOG + BT_FIRE_LOG). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
839b040619 |
KB: night-8 playtest intelligence (build 4.11.674)
Player testimony worth keeping out of the issue tracker alone: * SENSOR PANEL model, from VGL Lynx -- the last major system without one. Sensor damage flickers the HUD (corroborated by period footage), degrades radar/map, affects the searchlight, and possibly the hot box. Gitea #105. * COOLANT LEAK severity is a THREE-level display (1/2/3 triangles + alarm); we only ever produce the lowest. Our leak rate is a continuous scalar, so either the banding is missing or the magnitude never leaves the bottom band. * MYOMER heat calibration is CHASSIS-RELATIVE: a Thor sustains seek 4 indefinitely, a chicken-walker cooks in under a minute. That spread is the acceptance test -- a uniform "too hot" indicts the drive-heat coefficient rather than the sink capacities. * RAM damage: the binary's internal-only collision divert MATCHES what players observe (gyro/myomer damage, coolant leak, NO armor damage, grinding inert). May be working as intended; awaiting a player ruling. Gitea #103. * MP stability: 5-player match, no stalls or lag spikes -- bounds how often the carried cross-fire-stall / silent-node-crash opens occur. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
137c151951 |
#87: mech armour PANELS now darken with damage -- the .DZM material system was loaded and unused
Players: "the actual enemy mech in external view is not showing darkened armor panels". We swapped destroyed LIMB meshes but never darkened a panel. The 1995 game darkens armour through the MATERIALS. MakeMechRenderables (FUN_004cef28) built, per (damage zone, material), a watcher FUN_004573e4(material, &zone->damageLevel, 0.1f) that snapshotted the materials |
||
|
|
5410371b0c |
#86: destroyed weapons can no longer fire -- the fire gates read a
never-written cell (the split-cell gotcha) Both weapon fire gates (ProjectileWeapon gate 1 @4bbd36, Emitter hard-failure @4baab9) test the binary's subsystem+0x40 for Destroyed(1). In the 1995 layout that offset sits INSIDE the embedded status alarm (statusAlarm@0x2C + level@+0x14 = 0x40) -- ONE cell, written by ForceCriticalFailure when a zone's crit cascade kills the subsystem. The port models the same address as TWO members: the AlarmIndicator AND a plain int simulationState@0x40. Every destruction path writes the ALARM (so the MFD draws its X correctly) while the gates read the plain int, which nothing ever writes -- so a weapon on a blown-off arm showed destroyed on every panel and kept firing and scoring, locally and on peers (night-7: all three testers, screenshots of a missile leaving a destroyed pod). The tell had been sitting in our own logs for weeks: [ammo] SRM6_1 -> NoAmmo (gate1): destroyed=0 ... on a mech whose launcher was X'd out. Fix: both gates now read statusAlarm.GetLevel()==1 as well as the int. Also added: the crit-cascade log names the destroyed subsystem, a BT_SELF_DAMAGE_ZONE=dz_* named-zone bench mode, and BT_KILL_SUBSYS=<name> (force ForceCriticalFailure on one named subsystem -- the exact call the zone cascade makes, so a bench can ask 'the panel says dead, does it still shoot?' without hunting for the zone that carries a given weapon). Verified A/B in ONE run: before the kill both SRM6 launchers fired 2 salvos each; after, the destroyed launcher fired ZERO (gate log destroyed=1) while its twin kept firing normally. KB: new gotcha #22 (the SPLIT CELL -- one binary offset, two port members, only one written; sibling of #1) + combat-damage entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J |
||
|
|
62fc8409b6 |
seek-4 verification harness (BT_FORCE_SEEK) + the full supercharge/overheat
arc measured live BT_FORCE_SEEK=<idx> pins the seek gear in MyomersSimulation (env-gated bench; the eng-page toggle stays the authentic path) -- the supercharge loop was unreachable on a headless rig. Measured (Black Hawk, WALLTEST, full autodrive at gear 4): - cold: drive=1.42843 (exactly 0.9999/0.7), demand 87.85, actual run speed 53.7 u/s = 193 kph -- SUPERCHARGE (Oracle's pod figure: 182 for a 75-tonner; the 60t Black Hawk runs faster); - heat at ratio^2 = 2.04: myomer T 531 -> 1765 (deep past the 1000 degradation line); - heatFactor governor: drive 1.43 -> 1.10 -> 0.62 (the supercharged mech slows BELOW normal while cooked), coolant pulls T back (1765 -> 740), drive recovers to ~0.77, oscillates -- the mech self-settles into a thermally-limited cruise: 'top speed for less than a minute' exactly; - the hard freeze (drive -> 0: demand AND turn zeroed) is the same code path past failure temp, reachable when the coolant loop is compromised. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J |
||
|
|
905a4a86c0 |
RETRACTION + FIX: the myomer seek->speed coupling IS in the 4.10 binary --
restored byte-grounded; the morning's removal was export-gap blindness Player testimony (Oracle, pod veteran: seek-4 humanoid = 182 kph -- the manual's printed Super Charged figure; overheat = 'freezing up') forced a re-audit of the morning's 'no dynamic myomer->speed feed' verdict. The verdict was WRONG: a raw-image capstone sweep for FPU reads of [reg+0x31C] found the consumer inside the UN-EXPORTED master-perf region -- the same decomp gap that hid the #93 crash block. Decoded @0x4a9cf2-0x4a9da4 [T1]: per frame: MAX speedEffect@0x31C over the myomer chain (+0x7AC) -> mech+0x79C mapper->speedDemand@0x128 *= it (the SPEED coupling) if |drive| <= 1e-4 (@0x4ab16c): mapper->turnDemand@0x12C = 0 (the FREEZE) Everything the manual + the veterans describe emerges: gear 4 rides speedEffect ~1.43 into the RegisterMaxOutput gait cap (base x 1.4284 -- the two mechanisms interlock: 61.5 x 1.43 = 87.9 vs cap 87.8 measured) = SUPERCHARGE; myomer heatFactor degradation slows you; overheat freezes speed AND turn; damage slows (speedEffect carries 1-damage -- #75's original premise was RIGHT and its 'falsified' close was wrong). Restored in mechmppr.cpp, byte-grounded and better than the pre-removal draft: MAX over the chain (was first-found), NO 1.0 clamp (the old clamp ate the overdrive), turn-freeze included. BTMyomersSpeedEffectOf (unclamped) replaces the deleted BTMyomersDriveOf. Verified live: healthy drive=1.0 (no speed change), collision-rattled myomers drive 0.999 -> 0.892 -> 0.742 with demand scaling in lockstep (61.5 -> 54.9 -> 45.7). KB corrected in place (subsystems WAVE 6, pod-hardware manual audit retraction, open-questions resolved) + NEW GOTCHA #21: export-gap blindness -- 'no callers in the decomp' is not 'no callers in the binary'; sweep the raw image for the operand pattern before declaring a data path dead, and when a primary source disagrees with your decomp sweep, suspect your evidence first. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J |
||
|
|
4704ab3d41 |
#84: missiles ACCELERATE like the binary -- the pool flew at constant
rack-eject speed; the authored MissileThruster was never applied The binary Missile hosts a MissileThruster subsystem: authored acceleration for BurnTime seconds after the MuzzleVelocity eject. Authored values decoded from BTL4.RES (type-15 missile model records, reached from the AmmoBin's ammoModelFile via the type-1 MODELLIST indirection): SRM 2.5s @ 600 u/s^2, LRM 10s @ 300 (climb 50), Streak 3s @ 300 (turn 360deg/s), NARC 10s @ 300; splash 30 all (port constant was already right); authored drag ~0.001 = negligible. The port pool flew every round at CONSTANT |MuzzleVelocity| -- SRMs 100 u/s, LRMs 30 u/s (10x slower than authored) -- the entirety of the night-7 "missiles are very slow" report, and the driver of the "explosion before the missiles arrive" perception (the salvo-lead detonates while the slow spread rounds straggle in). Fix: BTMissileThrustOf (mech4.cpp) lazily parses + caches the RES thruster table (ModelList ids aliased to their type-15 member's burn/accel); both launch paths (master FireWeapon + the replicant salvo mirror -- peers see the same speed) resolve through the launcher's bin and pass burn/accel into the pool; the advance integrates speed += accel*dt while burn remains, heading preserved. Ballistic rounds (autocannon/gauss) pass 0/0 -- unchanged. Impact log now prints v=/burnLeft= evidence. Verified live (solo, BT_SPAWN_ENEMY + BT_AF_MISSILE): thrust table 8 entries cached; Black Hawk Streak resolve burn=3 accel=300; impacts at v=277 u/s (was a constant 100). Fun authenticity note: the Black Hawk's "SRM6" fires strk (STREAK) ammo per its authored bin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J |
||
|
|
ac7c46b87a |
#93: fix the end-of-round PerformAndWatch crash -- the STATIC projectile
pool held dangling entity pointers across round teardown Root cause (disasm-pinned on the shipped 4.11.659 exe): RajelAran's crash (call to 0x65676769 = ASCII 'igge', EBP-walk return btl4+0x2b91a) is the tgt->Dispatch vtable call in BTUpdateProjectiles' NON-MECH impact branch (mech4.cpp:1598; the site matches the disasm literally -- the 0x12/0x64 TakeDamageMessage ctor, the EntityID::Null ternary, the getenv gate after). The trap: the mech branch is guarded by BTIsRegisteredMech(tgt) -- but at round teardown a destroyed mech is DEREGISTERED, so a round still in flight (the pool is a static array that outlives the round; missiles are slow, #84) holding it as p.target now FAILS the mech check and falls into the !BTIsRegisteredMech branch, which treats the freed mech as a cultural icon and dispatches into freed memory. The liveness check itself routed the dangling pointer into the unguarded branch. Freed heap reused by a string -> vtable slot +0x10 read 'igge' -> call 0x65676769. (The EBP walker explains the stack shape: the faulting call pushed its return address on ESP, but the walk reads [EBP+4] = PerformAndWatch's frame.) Fix -- scrub at the source of truth: - BTProjectilesDropEntity(e): every pool entry drops a dying entity from p.target/p.shooter; called from ~Mech and ~CulturalIcon (the only free paths for targetable entities). Flight + impact code is already null-guarded on both fields. - BTProjectilesClearAll(): kills all rounds at RunMissions exit (cross- mission hygiene for the static pool). Verified: 6 short-mission cycles (45s missions with a spawned dummy + autofire, mission expiring mid-combat) -- 0 exceptions, 6/6 clean 'RunMissions returned'. The original crash was a heap-reuse race with no deterministic repro; the scrub eliminates the dangling-pointer class by construction. (Bench note: blind autofire never launches missiles -- the launcher needs a target lock -- so the exact in-flight race was not re-created; the non-regression + the pinned mechanism carry the verdict.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J |
||
|
|
ba4af90d6b |
phase-13 log: night-7 session research state (collision economy, myomer system complete, coolant leaks, the seek 4.0-vs-4.10 drift) + open-questions entry for the seek player version-probe
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J |
||
|
|
c18929b23a |
KB: manual cross-check -- myomer seek 'Super Charge' top speed is 4.0-manual behavior DROPPED by the 4.10 binary (stat sheets: Normal 143 / Super Charged 182 / Gimped 40 kph; printed ratio 1.27 vs 4.10's vestigial gear ratio 1.43)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J |
||
|
|
58f49ed942 |
KB: the seek dial's real stake is TORSO TWIST (PowerWatcher zeroes twist while myomers not Ready); generator has no load model -- gears 1-3 identical on a healthy mech
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J |
||
|
|
cf8618bcb9 |
myomer system COMPLETE -- the 'WAVE 6 mover cutover' was a phantom built on
a misattribution; the authentic couplings are all live
THE CORRECTION [T1]: @004b9550/@004b95b8 ('ConnectToMover'/'Disconnect
FromMover') are MechWeapon::ConfigureMappables/ChooseButton -- their +0x31C
is the weapon TriggerState and **(mech+0x128) is the CONTROLS MAPPER's
button roster (the MECHWEAP.CPP assert string + the trigger-edge detector
@004b9608 sit adjacent in the image). The binary has NO dynamic myomer->
speed feed: AvailableOutput has exactly TWO callers in the whole image --
RegisterMaxOutput (assembly) and the SeekVoltageGraph sampler -- and
speedEffect@0x31C is a published GAUGE attribute.
The authentic myomer system (now fully live):
1. drive heat (#85, this morning);
2. VITAL death -- myomers are the only authored vital=1 subsystem
(BTL4.RES res+0x48 scan): destroying them kills the mech via the #80
vital-crit path (this is also the ram-death mechanism -- myomers are a
0.35-weight collision-rattle target);
3. the assembly-time gait cap: RegisterMaxOutput @004b8ef0 raises
mech+0x7A0 to AvailableOutput(top gear) = base x ~1.4284 -- now called
(idempotent max, per tick; the 0x358 layout lock leaves no latch room)
with real OwnerBaseSpeed/OwnerMaxSpeed bridges; the old per-frame
reverseSpeedMax2 heal that would have clobbered it is garbage-guarded;
4. the ENG-page power curve at correct absolute scale (real base speed);
5. electrical sourcing (low gears run on a browned-out generator).
REMOVED (inventions): the mechmppr `speedDemand *= speedEffect` demand
multiplier + BTMyomersDriveOf + the Myomers ConnectToMover/MoverAttach
family. Damaged-but-alive myomers heat faster and drop the power curve
but do NOT slow the mech (gitea #75's premise is falsified by the binary;
the real damage slowdown is the LEG-damage gimp gait). Seek gears change
heat/sourcing/graph -- never speed.
Verified live: a mech at myomer dmg=0.234 (speedEffect attr 0.766)
beelines at full commanded speed with dmgGain=1.234 heat; 0 faults.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J
|
||
|
|
31552c0444 |
#88 part 2: the central coolant pull-through -- HeatSink::DrawCoolant was a
return-0 stub; the Reservoir BAR now drops when a damaged mech leaks The user watched the coolant panel's level bar during the leak bench and it never moved -- correctly: a leaking subsystem drained only its own loop, because the slot-14 top-up (DrawCoolant) was stubbed. Disassembled the real base @0x4add00: the draw RECURSES UP the sink linkage, scaling by coolantFlowScale@0x15C per hop, terminating at the Reservoir's supply (@0x4af3b0 -- clamp to [0, coolantLevel], drain the tank, return granted -- already faithfully ported as Reservoir::DrawCoolant). The binary's terminal hop is the bank's slot-14 override @0x4ae8b0 resolving its reservoir connection @0x1D8 (the port's 'helper' member -- not vestigial after all); the port's existing Reservoir-ctor Attach + Reservoir override terminate the same chain equivalently (the extra bank hop scales by ctor-default 1.0). Implemented the base recursion (null-guarded -- the binary calls through the resolved link unguarded; authored topology always links) + a BT_COOL_LOG [resdraw] probe at the supply. Verified live: a destroyed myomer's damage leak pulled the central tank 6.0 -> 5.58 over ~30s ([resdraw] granted lines) -- Reservoir/CoolantMass, the cockpit coolant bar, now visibly drops as a damaged mech bleeds coolant, and DeathReset refills it on respawn. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J |
||
|
|
a8a0042f28 |
#88: coolant leaks LIVE -- the #64 damageZone shadow was the single break
in the leak chain HeatSink::UpdateCoolant prices the damage-driven leak from the QUALIFIED engine member (Subsystem::damageZone @0xE0): coolantDraw = zoneDamage * heatLoad, with the coolantActive hysteresis (= the ReportLeak attribute the 19 authored leak watchers ride). But the MechSubsystem ctor only ever filled its re-declared SHADOW member (gitea #64, gotcha #1), so the engine member stayed NULL forever, zoneDamage pinned 0, and a coolant leak was STRUCTURALLY IMPOSSIBLE no matter how much damage landed -- Oracle's night-7 report exactly. The #80 crit fix and the #83 collision rattle write real subsystem-zone damage, but into an object the leak reader could never see. Fix: alias the ENGINE base member to the same zone in both MechSubsystem ctors. Every shadow reader keeps working (same object); the engine base Subsystem::TakeDamage latent null-AV (#64 consequence 2) is disarmed; the full #64 de-shadow sweep remains the long-term cleanup. Verified live (solo, BT_COOL_LOG/BT_CRIT_LOG): collision rattle drove a damaged Myomers to [cool] draw = dmg*heatLoad with the level draining; sustained weapon fire rolled real crits ([critroll]) and produced 588 leak-pricing lines. Authored routing extracted from BTL4.RES: collision rattle targets HeatSinkBank 0.3 / Gyro 0.35 / Torso 0.25 / Myomers 0.35 (Condenser+Reservoir authored 0 for collisions); weapon crits select via the zone's crit-entry list. Known-remaining fidelity item (documented): HeatSink::DrawCoolant (slot 14, central top-up) is still a return-0 TODO. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J |
||
|
|
1570983fde |
#85: myomer drive heat LIVE -- the five Mech motion operands were return-0
shims, so the reconstructed integrator @004b8d18 accumulated zero forever
The integrator, its gates and the heat plumbing were all reconstructed and
running -- but OwnerVelocityMag/OwnerDriveMag/OwnerVelocityY/OwnerMotionGain/
OwnerMass were cross-family compile shims returning 0.0f, so the myomers
priced ratio^2 * damageGain * 0 every tick: ice cold at any seek (Oracle's
night-7 panel-confirmed report).
Operands re-grounded against the raw disasm + decomp and mapped to NAMED
members via a complete-Mech-TU bridge (BTMechMyomerMotionSample, mech4.cpp),
per the databinding rule:
+0x1C4 vec -> localVelocity.linearMotion
+0x82C vec -> localAcceleration.linearMotion (the authentic 15-ring
smoothed AccelerationLastFrame the port already maintains)
+0x20C -> moverMass (the collision divert's cell; the old "motion
gain" label was a misread)
*(+0x250) -> environment gravity (FUN_00421e2c: vy -= **(+0x250)/tick),
via GetEnvironment()->gravityConstant
So the heat physics reads: 0.005 * 1/2 m v^2 (kinetic work, per-tick/no-dt
in the binary -- kept verbatim, frame-rate note documented) + 0.2 * m g
|vy| dt (climb power) + 0.2 * m |v||a| dt (acceleration power), all scaled
by ratio^2 and (1 + zone damage). Restored the binary's fabs(v.y) (the
draft had signed vy -- descending would have COOLED).
Authored tuning extracted from BTL4.RES (18 mech records, one shared
tuning): VelocityEfficiency=0.995, AccelerationEfficiency=0.8, gears
0.3/0.5/0.7/0.9999 rec idx 2; myomer thermal profile 77/1000/2000,
conductance 1.9e5, thermalMass 2.5e5.
Live-verified (solo arena, BT_MYO_LOG + BT_HEAT_LOG): standstill generates
~0; hard circling drove Myomers T 77 -> 1225 (past the 1000 degradation
line) and the coolant loop conducted it back to ~520 equilibrium on
slowing; damageGain live at 1.048 from grind rattle.
Known-inert remainder (filed in open-questions): the climb term reads g=0
because Mover::localEnvironment is never populated in the port
(EnvironmentZone res type 23 unwired); the mover feed (speedEffect ->
locomotion) stays WAVE 6.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J
|
||
|
|
37dd7f9663 |
collision-pricing audit: restore the binary's crash economy -- two fixes
A. KNOCKDOWN PACING: the 0.4s gBlockCooldown contact-hysteresis is gone. It suppressed the crash knockdown for as long as contact was held, letting the kinematic drive re-slam the obstacle at full commanded speed EVERY FRAME -- continuous full-price crash self-damage, a healthy 65-tonner dead in ~2-3s of grinding (the night-7 SAURON ram death, 110 priced frames at |v|~34.7). The binary needs no guard (crash threshold @0x4ab178 read from the exe = 0.0, dispatch per blocked frame is authentic): the bmp clip zeroes the drive, and a pressed mech re-triggers the knockdown at v>6.3 long BEFORE its grind can price anything -- the type-0 divert's 0.5-pt free floor needs v>~18.5 at 65t. Wall grinding is free taps + paced staggers; only genuine fast arrivals pay. B. RAW RAM DISPATCH: the *0.001 "ram economy normalization" (2026-07-12) predated the #83 type-0 divert and became a double-normalization -- the victim's divert scale (~4e-5) compounded with it, so NO physically possible ram could clear the 0.5-pt free floor: rams were a no-op against the victim while the rammer's own un-scaled crash path rattled for real. The binary dispatches raw [T1 @part_012:15324]; the divert IS the normalizer. Also un-starves cultural-icon crunches (trucks now crush on contact per their authored armor, not after ~19 bumps). Bench (scratchpad/night7/mp_rampricing.sh): - wall leg: 150s full-throttle wall push = 125 paced knockdown binds (~1.2s cadence, iv2 40-43), 4767 crash frames ALL under the free floor, 0 rattle, 0 deaths (pre-fix: dead in seconds). - ram leg: BT_GOTO=enemy + BT_GOTO_STOP=2 rams the peer; raw amount arrives byte-identical on the victim (66239.1 tx == rx), victim prices 2.55 pts internal rattle (first ram damage to a victim in the port's history), pressed follow-ups free, 0 deaths both nodes, 108 knockdowns -> exactly 108 type-5 records on the observer (no skating regression). KB swept: combat-damage.md (the 1.3e6 moverMass mis-attribution corrected to the measured 60-90k tonnage scale), locomotion.md, rendering.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J |
||
|
|
054c3f2eee |
#94: replicant un-wreck now rides the DEATH-STATE EXIT edge -- the zone
falling-edge latch was blind to zone-less (collision) deaths The peer wreck lifecycle listened to two different signals: wreck ON came from the replicated death explosion (unconditional), wreck OFF from a damage-zone falling edge (wasWrecked latch, armed only when a zone crossed >= 1.0 on the observer). The #83 collision damage prices as internal rattle and never moves a zone, so a fresh mech killed by ramming could NEVER un-wreck on peers -- the live respawned mech drove around wearing the hulk (night-7 field report: SAURON's Loki, screenshotted by both observers at the exact minute the log analysis places the ram death). MechDeathHandler::Tick now tracks prevMode and fires the rebuild + warp when the replicated MovementMode leaves the death modes {2,9} -- only Mech::Reset ever does that, and the state rides every record header, so the trigger is cause-agnostic. The zone falling edge just refreshes the cache. Side effect fixed for free: the peer respawn warp vortex now plays for zone-less deaths too (same gate). Bench (scratchpad/night7/mp_zoneless.sh + the new BT_SELF_DAMAGE_TYPE=collision harness option, which dispatches type-0 through the real TakeDamage divert): 14/14 zone-less deaths un-wrecked (mode 9->1) with zero zone rises on the observer; explosive regression leg 12/12 with zone destruction active. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J |
||
|
|
e91d447405 |
#82 FINAL LAYER: the peer body-channel trn-lock -- a circling limper skated because the peer's turn-arm had no walk-demand yield and the trn speed exit read a dead replicant mapper cell
The master limped clean (218x gimp-cycle records, zero trn) -- a _ReturnAddress trap on SetAnimationState(4) proved ALL peer trn arming came from the port's own mech4 turn-step block, not the type-3 reader. Two dead ends, one root: - mech4 peer arm: gated on !wantsWalk (standSpeed < bodyTargetSpeed), exits trn when walking demand appears -- the leg twin's authentic precedence (part_012.c:12013, the Standing speed test outranks the turn test). - mech2 body case 4: bspd reads bodyTargetSpeed on ReplicantInstance; the local mapper's speedDemand is a dead cell on a peer (reads 0 forever -> no exit). Verified 2-node timeline: arena circle replicant 218x state-24 / 0x state-4 (was 116x trn churn), grass circle 235-clean, knockdowns bounded, 0 deaths. Diagnostics kept: BT_TRNTRAP ra-trap, BT_ANIMIND_CAP, [gimpfeed], [replgimp] extension. KB: locomotion.md trn-lock entry + the dead-mapper-cell lesson. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRjrTQpJd6u9XyfnUbTB3v |
||
|
|
14ff3519ac |
#83: the COLLISION DIVERT -- type-0 damage prices as internal rattle, never armor
The crash self-damage reconstruction (
|
||
|
|
9c83a468db |
#82 ROOT FIX: reconstruct the crash SELF-DAMAGE -- wall-grinding now hurts,
the knockdown storm self-limits, peers keep the limp
The 'peers see a limping mech skating' report decomposed into a wall-grind
KNOCKDOWN STORM: a gimped mech held against a blocking solid re-crashes each
time its gg cycle rebuilds speed (the gg ceiling is authentically the clip's
natural speed -- bhk1 ~14.9 u/s, ceiling+divisor @0x544/0x548, loader formula
byte-matched -- so iv2 ~222 >> the 40.0 threshold @0x4ab184), and every
type-5 KNOCKDOWN broadcast floors all peers' replicants into knockdown/trn
churn: gliding + leg-lifts = skating. Turning and crushable cars were
A/B-exonerated (grass circling: 198 replicant gimp entries, 17 crunches,
zero broadcasts).
What the binary has that the port lacked -- recovered from the EXPORT GAP by
raw disasm (scratchpad/night6/gap_4a9770.txt, @4aa89f-4aaab4) -- is the crash
branch's tail: fallDirection = worldToLocal(damageForce) (FUN_0040879c, M^T v,
un-normalized), fallScalar = -(fallDirection . localVelocity.linearMotion),
and a self-dispatched TakeDamageMessage{0x64, zone=-1, the engine-computed
collision Damage verbatim}. A wall crash COSTS ARMOR, so the grind degrades
the mech instead of looping forever. That dispatch was the long-standing
'DEFERRED' note in the response policy; now reconstructed.
Falsified en route (comments + KB corrected, do not revive): the stagger's
FUN_004a4c54(1)/(0x20) 'action-request flags' feed NO drive suppressor --
image-wide sweep + full gap disasm show word[this+0x18] is read only by the
net record emitter; the bmp clip applies NO root motion (adv=0 measured); the
'gimp cap 5.8' figure was the BACK-cycle stride printed under a misleading
label (now ggCapL/R; @0x52c has no binary consumer).
Verified (mp_gimpAB rigs): arena1 wall-grind now 4 bounded strikes, limper
dies of its crashes and respawns cleanly twice (START->RESET, no strand);
grass circling unchanged-clean (198 replicant gimp states, 0 knocks).
Rig: pid capture race fixed (a missed winpid left a stale node holding the
-net port -> null runs), per-config sweep added.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|