9b1353bfbfe499cc5fd2d54c36cb86502a949ebe
327
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6dbeb3d266 |
wreck smoke dies WITH the wreck; the audio/effect clock is calibrated to the byte-proven 28
USER REPORT: "when you destroy a mech, it continues to smoke for a very long
time after the mech disappears... up to 30secs to a minute."
MEASURED (new [pfx] timed census with per-def particle attribution): the
death plume (psfx 1 DDTHSMK) was re-armed on a 10s cadence as a WORLD effect
(ownerTag=0) -- so neither the burial transition nor the respawn cleanup
(StopAllEntityEffects) could touch it. The last-armed window kept emitting
over the empty spot for up to 10s after the hulk vanished, plus 6+-2s authored
particle lives, and back-to-back windows stretched the visible tail to the
reported range. The re-arm itself is a marked PORT ADDITION [T3]; the binary
fired DDTHSMK once -- its 10s window + particle lives ended right around the
~17s hulk sink. The authored design: the smoke dies WITH the burial.
FIX: the plume is spawned ATTACHED (BTStartPfxAttached, tagged to the dying
entity -- it also rides the sinking hulk now), and the burial transition calls
BTStopEntityPfx alongside the respawn cleanup that already did. Verified over
a 175s run containing FOUR wreck events (the enemy kill + three player bay-
fire deaths): every plume's emission ends at its wreck's burial or respawn,
particles fade within ~8-11s, no orphan emitters remain.
THE COUPLING (user relay of Oracle: "prolonged smoke and explosion INCLUDING
SOUND... same lever?") -- CONFIRMED as a family: the binary's audio layer
times in FRAME COUNTS converted by the engine frame-rate global:
AudioTime::Seconds_To_Frames == fmul [0x52140c]=28.0; fadd 0.5; round
(@0x42c611, @0x42dd86 -- the 0x42xxxx cluster of the ~90 global consumers)
Our engine's DefaultRendererRate said 30: every audio duration, sequence
delay and compression window ran ~7% fast. Calibrated to 28 [T1]. NOT yet
explained by this: a truly SUSTAINED/looping explosion sound (7% is not
"prolonged") -- the effect stop-path trace stays open on #114/#51.
Tooling: BTPfxParticle carries defIndex; [pfx] census (BT_PFX_LOG, 2s) prints
emitters + particles PER EFFECT SLOT with a timestamp; wrecksmoke.sh.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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 |
||
|
|
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 |
||
|
|
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>
|
||
|
|
19fe1e5fa2 |
KB: the wall-grind knockdown storm IS the surviving #82 skating -- A/B-isolated
Four-config matrix (mp_gimpAB.sh, [animind] replicant-state verdict): grass straight/circle = clean replicant limp (198 gimp entries; 17 car crunches during it -- crushable path broadcasts nothing, cars exonerated); arena1 straight = one wall strike, replicant recovers into the limp; arena1 circle = wall GRIND -- the bmp stagger clip's ~6.5 u/s root motion re-strikes the wall (6.5^2 > iv2 threshold 40), each rebind re-broadcasts the type-5 knockdown (26x in the user's run), and every peer's replicant lives in knockdown/recovery churn: gliding + trn leg-lifts = "skating". Turning while gimped is innocent; single strikes are benign. Authentic fix filed in open-questions: reconstruct the consumer of the stagger's action-request flags (FUN_004a4c54 bits 1/0x20 -- the drive suppressor while the bmp clip plays); the rebind guard at mech4.cpp:6861 is already marked bring-up-pending exactly that. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4e89baeff3 |
bench parity: local nodes now launch exactly like a player's shortcut
Local benches had drifted from the shipped launchers and every divergence
cost a debugging session:
* no BT_START_INSIDE -> benches opened in the EXTERNAL CHASE camera while
every player starts in the COCKPIT. The first first-person look at a
bench window was misread as a broken HUD.
* novice bench eggs -> every SHIPPED egg is expert; novice gates off the
entire heat model, crits and jams, so bench combat was not field combat.
(Verified: the aligned node now boots experience=3 simLive=1
heatModelOn=1, where it used to boot 0/0/0.)
* 1-LP affinity pins -> starved the gauge executive and produced a false
"the comms panel never counts deaths" reading. Two LPs per node keeps
the documented single-box jitter fix without the starvation.
scratchpad/night6/bench_common.sh now owns the contract (bt_player_env,
bt_launch, bt_expert_egg, bt_novice_egg) copied verbatim from play_solo.bat,
with bt_assert_player_env warning if the shipped bat ever drifts. All three
4-node benches source it. The limp bench keeps a novice egg -- expert crits
the aimed leg and the mech turns-but-never-moves -- and now says so loudly.
Also: BT_SHOT_EVERY=<n> headless backbuffer capture (btl4vid.cpp), the tool
this was diagnosed with. It must sit above the warp phase-0 early-out since
that fn is the per-frame alpha-pass hook. OS screen-capture is off-limits:
a foreground-lock failure photographed the user's browser instead of the game.
KB: build-and-run.md gains the bench-parity rule + the capture diag;
cockpit-view.md gains the measured aspect/FOV finding (the surround's 2.05:1
view collapses vertical FOV to 31.5 deg vs the pod's 46.8, so fixed canopy
geometry covers 68.6% of the lower half -- NOT a regression: the shipped 643
binary reports identical geometry).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
494f32efb4 |
#45 ROOT CAUSE + FIX: the gauge executive starves in MP -- panels froze at healthy fps
Instruments repaint via the background task pump, which authentically runs
ONLY in the frame's leftover time with a floor of ONE pump per frame; the
gauge renderer is 1 of ~7 round-robin tasks and each turn advances ONE gauge
of ~140 active, with the rate mask advancing once per full sweep. On a busy
MP mission the foreground eats the whole frame budget, the floor becomes the
norm, and the whole instrument stack rotates once in MINUTES at perfect fps:
comms-panel K/D stuck at 0, recharge tickers frozen, while the tallies and
their replication underneath were exactly right. Measured: 4-node bench at
70-90 fps gave the PilotList ~2 Execute turns in six minutes.
Fix, both env-tunable, authentic behavior restorable:
BT_BG_MIN (APPMGR.cpp, default 32, 0=authentic) minimum background
pumps per frame regardless of slack;
BT_GAUGE_BATCH (GAUGREND.cpp, default 32, 1=authentic) gauges advanced per
gauge-renderer turn (a visit is only a rate-mask check
unless the gauge is due).
Verified 4-node self-damage bench: sweeps 0.006/s -> 18-20/s, PilotList ~10
Exec/s, ALL FOUR panels tracked every death live (0->11) within ~1s, bg cost
2-4 ms/frame, respawn ledger clean (40 cycles, 0 swallow / 0 mismatch).
Also: BT_PERF now reports gaugeTurns/sweeps/active alongside bgTasks;
BT_AF_PERIOD now throttles the missile autofire group too (unthrottled spam
trips the documented FailureHeat all-weapons brick, which froze run 1 of the
cross-fire bench).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
bfa1b04990 |
#81 follow-ups: peer warp must not stomp the POV vortex; panel draw probe; mech3 ghost symbols
Two field observations from watching the 4-node stress, both run down: 1. "A respawn happened without the blue vortex" -- REAL. The translocation effect is ONE global slot shared by the POV lifecycle and the world-anchored peer sphere (a port extension). A peer's un-wreck arriving while the local pilot's own collapse/wait/expand was in flight overwrote gWarpPhase/gWarpPOV and killed the POV vortex. Invisible before the respawn fix only because overlapping respawns barely existed; now they are routine. Fix: the POV lifecycle owns the slot -- BTStartWarpEffect self-skips while it is active ([tloc] peer warp SKIPPED). 2-node bench: 20/20 POV collapse+expand pairs, 9 peer spheres played, 11 correctly skipped; solo: 8/8 pairs unchanged. 2. "Comms panel counted no deaths" -- panel machinery CORRECT; the 4-node zeros were the bench's own CPU crush (4 core-pinned instances starved the PilotList to <0.6 Hz, so rows redrew minutes-stale). Added the arbiter: [score] panel DRAW slot/pilot/kills/deaths edge log (BT_SCORE_LOG) -- the 2-node rerun drew 0->9 / 0->11 live on both nodes, local AND replicated (SBMIRROR rows confirm owner->replicant tally flow on all 4 stress nodes). Also: the mech3 offline-authoring stubs declared every <Subsystem>::DefaultData as Entity__SharedData while the real statics are Simulation__SharedData (this engine derives Entity FROM Simulation) -- ~20 ghost symbols /FORCE silently resolved to garbage. DefaultData half fixed (SubsystemDefaultData now returns the true common base Simulation::SharedData); the CreateStreamedSubsystem stub signatures remain wrong (nested SubsystemResource* + ResourceFile*), are cold (no callers), and are tracked in open-questions + gotchas §6 stub-typedef corollary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e1c15eb806 |
#82 peers see a limping mech SKATING: the mech erased its own gimp cell every frame
The binary's one mech+0x40 IS Simulation::simulationState, which rides EVERY
update record header -- so in 1995 a peer's replicant learned the gimp level on
every packet and its gait limped with no gimp-specific replication anywhere.
The port's Mech::PerformAndWatch wrote SetMovementMode(1) every frame ("ground,
non-death, non-airborne"), which erased the level once it was mirrored in for
the warning voice (#78): the wire carried 1, bystanders walked while sliding at
limp speed, and the voice sequence restarted on every damage event (1->4 edge
per tick) instead of announcing once.
- mech4: that per-frame write now writes the AUTHORITATIVE level
(gimped ? 3/4 : 1) via a new alarm-only bridge BTMechGimpAlarmLevel -- which
deliberately never consults the cell it feeds, so a respawn-cleared alarm
cannot re-latch stale gimp out of it.
- BTMechGimpLevel: falls back to the replicated cell, with ONE-CELL precedence
(a fall/death/limbo state wins, so the normal drivers and their death latch
run -- what the binary's single cell enforced structurally).
- Reverted the #78 record guard: on a replicant the master's records are
authoritative, so pinning 3/4 against them would keep a peer limping through
a respawn. Fixed at the writer instead.
- [simstomp] trap now scoped to the watched mech with a module-relative return
address (symcrash-able) -- that is what named the writer.
- Also learned + recorded: zone damage levels replicate only when the EXPLOSION
TABLE's tier is crossed (peer measured at 0.428 vs master 0.857), so peer-side
damage state must never be inferred from them.
Verified two-node (scratchpad/night6/mp_skate.sh): the observer's replicant gets
sim=4 and its gait runs 23 -> 25 (wgr entry -> ggl limp cycle). Field clue that
cracked it: "after respawning a peer DID see the limp" (epilectrik/SAURON).
KB: locomotion.md + gotcha #25.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
cfa52a28d8 | Merge remote-tracking branch 'origin/glass-cockpit-refit' | ||
|
|
18766a167b |
locomotion.md: correct the gimp-audio verdict -- the reverse-disabled voice is real and wired
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7838df2924 |
locomotion.md: the gimp audio verdict (servo loop, no voice line) + rebind watch item
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0fd33531a4 |
locomotion.md: record the gimped turn-step relocation find
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3db1062eb6 |
limp demo bat + the glass controls doc fix (SHIFT is throttle, not W)
run\limp.cmd: double-click #78 demo -- novice copy of DEV.EGG (expert crits can kill the myomers and fake a no-drive bug), 2x60 into a bhk1 leg zone via BT_SELF_DAMAGE_TICKS, BT_KEY_NOFOCUS for the glass plasma-window focus trap, and the REAL controls in the header. build-and-run.md: the "WASD drive" line predated the glass merge -- the default profile drives with the 1995 throttle lever (SHIFT/CTRL slew + stick, ALT reverse). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
36f68718c2 |
the visible limp (#78): there was never a jump-jet clip set -- the 'Airborne' drivers ARE the gimp gait machines
The port had FUN_004a5bf8/71f4 fully reconstructed as AdvanceBody/Leg-
AnimationAirborne, gated on (MovementMode()==3||4) && jumpCapable@0x580 and
believed dead ("the test mech never jumps"). The binary says otherwise:
mech+0x40 in that gate is the graphicAlarm LEVEL (3=left-leg gimp, 4=right)
and +0x580 is hasGimpClips, set by the conditional loader block that probes
'wgl' and fills clip slots 22-27 (wgl/wgr/ggr/ggl/gsl/gsr) plus the four
measurements at 0x53c-0x548 (wg entry strides = speed caps, gg cycle strides).
Renamed the five jump* members + both drivers accordingly.
New: GimpBodyClipFinished @004a6344 / GimpLegClipFinished @004a7970 -- the
gimp transition machines, branched from the normal finished-callbacks. Phase-
correct limp entry (left-gimp enters 0x16/wgl only from a RIGHT step, right-
gimp 0x17/wgr from a LEFT step), gg cycles at gimp cadence, gs exits, and the
demand clamp to the gimped side's speed cap (leg cb writes it back into the
mapper -- the binary's authentic slowdown; the T3 x0.5 stand-in in mechmppr is
retired, BT_GIMP_SPEED now defaults 1.0). The binary's gimp machines have no
reverse entry -- the "reverse disabled" behavior is now binary-proven.
Reviving the dead drivers replayed two port-glue bugs (gotcha #24): the raw
*(controlSource) mapper read (null -> crash at first engagement) and the
missing alarm->member state re-sync (machine pinned in one run state). Both
fixed; bench harness gained BT_SELF_DAMAGE_TICKS=<n> to hold a zone past
LegHalfStructure without destroying it.
Bench-verified (madcat, novice, zone 16): crossing -> alarm 4 -> wgr entry
from a left step -> 8k+ frames stable in the ggl limp cycle at cadence 14.77
(vs 18.5 walk / 22+ run) with raw demand still 50. [T2]
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
520f6eecd3 |
myomer damage reaches the wheels, legs gimp at half structure, reverse refuses -- and an ODR trap unmasked (#75/#78)
The speed-demand site (MechControlsMapper::InterpretControls) now applies the
drive scale the mover's feed roster applied in 1995: speedDemand multiplies by
the myomers' live speedEffect (gear ratio, thermal curve, 1 - zone damage, via
the BTMyomersDriveOf bridge) and, while GIMPED, by 0.5 [T3: VGL Lynx's
"roughly 50%", BT_GIMP_SPEED overrides]. The gimp states were already being
raised -- mechdmg sets graphicAlarm 3 (left) / 4 (right) when a LegDamageZone-
flagged zone crosses half structure -- but nothing downstream ever saw them.
While gimped, reverse input is refused ("reverse disabled"), matching the
old-timers' account; the pod's audio cue rides the alarm's watchers.
Bench, one trajectory, arithmetically exact: a deterministic right-leg ramp
(the new BT_SELF_DAMAGE_ZONE harness) crosses 0.5 and the demand goes
44.837 -> 6.726 = 44.837 x 0.5 (gimp) x 0.3 (a crit-chewed myomers from the
same ramp -- the #80 crits composing with #75's scale, unprompted).
The reason "nothing downstream ever saw them" is the real find of the night,
now gotcha #23: AlarmIndicator is typedef'd to DIFFERENT TYPES per header
family -- mech.hpp says ReconAlarm (4 bytes), heat.hpp says GaugeAlarm (0x54)
-- so Mech::graphicAlarm and EVERY member after it sit at different offsets
depending on a TU's include order. mechdmg wrote level 4 and read it back;
mechmppr read 0 from the same object, same expression. No compiler error can
catch it: each TU only ever sees one definition. Until the split is audited,
cross-TU reads of the gimp level go through BTMechGimpLevel (compiled in
mechdmg's TU) -- and the same split-brain explains why the port carries the
binary's ONE movementMode cell as two live members (engine simulationState vs
graphicAlarm level) that never meet.
Also landed en route: the Myomers un-powered self-repair observed healing in
the field logs at exactly 0.011 x the authored Explosive scale per tick --
the 2026-07-29 reversal confirmed live; and Mech message 0x15 "RealMaxSpeed"
raw-decoded (@0x49f604: sets mech+0x7a0 from the message unless the +0x7a4
latch holds -- a console-tunable top speed).
Open on #78, documented in locomotion.md: the Gimp animation clips (authored
keys in the binary's model-record parser; mech2's state enum vs mech3's
reverse-fix disagree about slots 0x12-0x17 -- reconcile before wiring) and
the audio-cue binding.
Diags: BT_SELF_DAMAGE_ZONE, BT_DRIVE_LOG, BT_GIMP_SPEED.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
8a99972f22 |
aimed fire strikes the part under the crosshair: the per-segment pick (#73)
The port's target pick was a whole-mech bounding-box slab test, and every hit -- aimed or not -- dispatched zone -1 into the victim's cylinder lottery. The recovered 1995 model (the division-card scene intersection) struck a SEGMENT and credited that segment's own damage zone. This is the port's equivalent. At render-tree build, each segment's draw object and its PrimaryDamageZone -- authored per segment in the skeleton stream, read by JMOVER.cpp:290 -- are recorded in MechRenderTree::segPick. The pick (BTL4VideoRenderer:: MechSegmentPick) ray-tests the per-segment bounding spheres on the live posed skeleton, using the draw-cached mLocalToWorld (at most one frame stale, fine for aiming). Selection is SPECIFICITY-FIRST: among the spheres the ray pierces, the smallest radius wins, normalized-distance tie-break. Both obvious rules were measured failing the same way before this one: the torso mesh's sphere (r~4.1 on the MadCat, vs shoulders at r~1.0) envelops nearly the whole mech, so its front face is nearest for any aim AND any near-body ray normalizes to ~0 against it. Limb spheres nest inside the envelope; smallest-pierced picks the most specific part on the aim line, and the torso wins only when no limb is threaded -- the per-part semantic the pod's mesh test produced. mech4.cpp tries the segment pick per candidate; the box PickRayHit survives only as the fallback (no tree yet, wrecked, spectator), still carrying zone -1 into the lottery, and a structure occlusion clears the zone. The winner's zone rides MECH_TARGET_SUBIDX + targetReticle.targetDamageZone into SendDamageMessage, so aimed hits now dispatch a real zone; the victim's handler applies it directly (bursts 2+ still re-lottery, authentic per the recovered @0x4a0230 loop). Bench, the same L/C/R sweep that exposed the bug: aiming left now lands 36/41 hits on zone 2 = jointlshoulder -- the left arm -- with a 0.30 thread score, where the same aim was a 6-way lottery spray before. The MadCat's authored segment->zone map is rich (shoulders 2/9, guns 6/17, hip 1, six leg zones, torso 0). Known approximations, flagged for field verification: sphere bounds rather than triangles, and a torso-envelope graze credits the torso where the pod's exact mesh test would have missed into air. The field protocol is the one the testers already ran on night 6: stationary mechs, short range, fire only at one arm -- the paper doll should now damage THAT arm. Diag: BT_PICK_LOG ([segpick] map at build, [pickwin] per pick). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
aa788bc56d |
noframe: drop the drag-by-surface -- it is a finishing step, not a mode
Author's correction to the shape of yesterday's option. The intended workflow is: place the displays where you want them WITH their frames, quit, then edit glass_layout.cfg to turn the frame off. So a ,noframe window should be pinned, not draggable -- removed the WM_NCHITTEST -> HTCAPTION handler that made the surface a drag handle. The arrangement being finished is the point: with no caption there is nothing to drag it by, which is exactly what you want on a wall of monitors. Delete the flag to get the frame (and the dragging) back. Unchanged and still needed: SaveLayout writes the flag back, because any later drag of a FRAMED window rewrites the whole file and would otherwise strip it. Re-verified live: - Heat MFD comes up with WS_CAPTION clear and answers WM_NCHITTEST on its surface with HTCLIENT (pinned) -- no window reports HTCAPTION any more. - Comm MFD (unflagged) keeps its caption. - A framed window's WM_EXITSIZEMOVE rewrote the file and BOTH ,noframe flags survived, with the new header explaining the arrange-first workflow. - surround / exploded / pod / dev boot and simulate clean. Docs updated to describe the two-stage workflow rather than surface dragging: the file's own header, environ.ini's BT_GLASS_LAYOUT block, context/ glass-cockpit.md and the ledger (which records the removed behaviour so the next reader does not re-add it). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e110b10ac8 |
the #73 pick dig, part 2: there was never a software pick -- the division card cast the ray
The hunt for the un-decompiled pick writer ends with the reason no scan could
find it: it does not exist. In 1995 the pick was a DPL SCENE INTERSECTION run
by the video board. The evidence converges from five directions:
- The WinTesla renderer still carries the result slots: dplHitInstance /
dplHitDCS / dplHitGeoGroup / dplHitGeometry + vehicleReticle, NULL-inited
in the ctor and never fed by the port.
- The stubbed 1995-era renderable constructors each took dpl_isect_mode_obj
("type of intersections to do on this object") plus an intersection MASK
-- every scene object was configured for ray queries.
- Auric, quoted in the KB long before this dig: "the pod's division card
cast from the view."
- VGL Lynx's night-6 LOD warning ("the hit test may run against a different
LOD") reads as firsthand knowledge: the ray tested the DRAWN geometry.
- Exhaustive byte- and pseudocode-level scans: nothing in the binary writes
rayIntersection/targetEntity/targetDamageZone. Game code only reads them,
constructs them (the Mech ctor @0x4a1674 -- identified this dig, along
with vtable +0x18/+0x1c = Mech::Read/WriteUpdateRecord, nine replication
groups, reticle not among them), gates them (FUN_004afd10, the look-state
machine: per-view crosshair positions, the pi rear case, the per-weapon
rear-fire mask walk), and ships them to the board for drawing
(FUN_00460a7c packages rayIntersection + elementMask into dpl).
What this means for #73: aimed fire in 1995 had PER-PART precision -- scene
ray, struck triangle on the active LOD, the DCS is the segment, the segment's
dzone is the credited zone. The cylinder lottery was only ever the UNAIMED
path. The port's whole-mech box pick funnels aimed fire through the unaimed
lottery, which is exactly what three testers documented on night 6: aim at
the arm, get the spray.
Fix design recorded in the KB: a per-SEGMENT ray test on the shooter side
(inverse(segmentWorld) * ray vs each segment BGF's local extent box, nearest
wins, its dzone dispatched as the aimed zone), falling back to box+lottery
when no segment resolves.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
bcbc7cff12 |
the #73 pick dig, part 1: mech+0x36c is an embedded engine Reticle -- the writer is still at large
Chasing the un-decompiled targeting pick (the residual of #73). The hunt for "who writes mech+0x37c/0x388/0x38c" kept coming back empty for a structural reason now understood: those three fields are the tail of an embedded ENGINE Reticle struct at mech+0x36c (RETICLE.h lays out position/state/pickPointingOn/ rayIntersection/targetEntity/targetDamageZone/elementMask, landing exactly on 0x37c/0x388/0x38c). Writers carry &mech->reticle and use small reticle-relative offsets, invisible to any mech-relative displacement scan. T0 corroboration, ENTITY3.h:131: "For BattleTech, damage zones are only valid via reticle based weapons." Ruled out as the writer: the engine Reticle itself (passive container -- ctor and resource parse only); HudSimulation @0x4b7830 (holds &owner->reticle in esi for its whole body but only READS the pick -- range caret, designator transform -- and slews reticlePosition via the @0x4b7ed4 ease); the 0x482xxx sites (mission-table target bookkeeping, address-of computations); the gyro coefficient reads at 0x4b2c24/5c/6e (that class's own +0x370..0x38c table). Still dark, with the candidates mapped: the un-exported 0x4a1674-0x4a2d48 stretch (reticle-touching at 0x4a16a5 in the Make-time init, 0x4a1f93, and a state+pickPointing+base trio at 0x4a294a-61 near entry 0x4a2971), the two unidentified Mech vtable overrides +0x18/@0x4a122c and +0x1c/@0x4a0c2c (both big switch functions), and a few lone sites. One of these computes the authentic pick; recovering it answers whether the pod tested the aim ray against the mech's cylinder (as the damage table's geometry suggests) or a box like the port's stand-in. Recorded in combat-damage.md \xc2\xa7Targeting so the next session starts from the map instead of the empty scans. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f5f4c6c198 |
glass_layout.cfg: ",noframe" per-line option strips a window's title bar
Append it to a window's line and that window becomes a bare WS_POPUP -- no
caption, no border, just the display and its buttons -- for a multi-monitor
wall where the chrome is only noise:
Heat MFD=1920,0,657,539,noframe
Options are comma-separated after the four numbers and unknown ones are
ignored, so an older build reading a newer file loses the option but never the
line (the bindings.txt grammar rule, applied here).
TWO THINGS THAT WOULD HAVE MADE IT A TRAP, both handled:
- A frameless window has NO TITLE BAR TO DRAG, which is the entire point of
the sticky layout. So a noframe window is dragged BY ITS SURFACE:
WM_NCHITTEST returns HTCAPTION anywhere that is not a button, HTCLIENT over
one. Buttons stay clickable, and because Windows drives the move the drag
still ends in WM_EXITSIZEMOVE -- so it still saves.
- SaveLayout rewrites the WHOLE file, so it writes the flag back. Without
that, the first finished-drag after adding the option would have silently
stripped it.
Ordering: the flag is read BEFORE frame sizing (a quiet LoadLayout pre-pass,
then the normal pass after ComputeLayout), because AdjustWindowRect -- and
therefore the ring placement -- depends on whether a window carries chrome.
The pre-pass is quiet so the restore still logs exactly once.
Verified live: two flagged windows came up with WS_CAPTION clear while the
other five kept it (read back with GetWindowLong(GWL_STYLE)); a click still
dispatched on the frameless radar (CLICK 'Secondary / Radar' addr=0x18); and a
WM_EXITSIZEMOVE save round-tripped both flags back into the rewritten file.
surround / exploded / layout=off / pod / dev all boot and simulate clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
a5fb96ae96 |
the crit system, complete: two gap functions recovered, the dead sink revived, and a wrong verdict reversed (#80)
The whole critical-hit pipeline was dark, three layers deep, and one of those
layers had fooled us into a false conclusion about the 1995 binary itself.
LAYER 1 -- the trigger, recovered from the un-exported gap. The Mech MESSAGE
TABLE at 0x50bdf8 ({id, name, handler} rows) names the real
Mech::TakeDamageMessageHandler at 0x4a0230 -- message 0x12 "TakeDamage" --
plus seven sibling handlers (PlayerLink, RealMaxSpeed, BalanceCoolant,
Set/ClearBurningState, EjectPilot, DuckRequest). Inside it, the crit chance
at 0x4a0164: p = clamp(0.7 * damageLevel^2 + 0.01, 0..1), gated on the
player's simLive flag (+0x25c -- novice never crits), rolled PER BURST on the
current zone, skipping a zone already burning. Chance is ~1% on fresh armour,
~18% at half-stripped, ~58% at 90% -- crits arrive exactly as armour fails.
The handler's application loop replaces the engine base's single call, which
ignored burstCount entirely (multi-burst damage under-applied (burst-1)x).
Faithful shape: per burst, crit-roll -> CriticalHit @0049ccc4 (which routes
half the amount through the armour internally and picks ONE critical
subsystem by criticalWeight) else zone->TakeDamage -- then RE-RUN the
cylinder lottery from the impact point for the next burst, stopping early
once the mech is disabled. Multi-burst damage sprays across zones by design.
LAYER 2 -- the sink. MechSubsystem::TakeDamage was an empty btstubs stand-in;
the real body is at 0x4ac0bc (CLASSMAP had that address mislabeled
"HandleMessage"): zone damage, then on level >= 1.0 the Destroyed alarm, the
PrintState gate, the 1.0 pin, and -- for a vital subsystem -- the owner
mech's graphicAlarm to level 9, the same fall/death level the leg path
raises. That is the #28 vital-subsystem kill machinery, now real.
LAYER 3 -- the one that rewrites yesterday. The subsystem ctor DID copy
armour points + per-type scales into the private zone -- through the
ReconDamageZone PROXY, whose fields sit at struct offsets +4/+8, not the
binary's +0x140/+0x144. The floats landed on the engine object's header and
the real damageScale[] stayed zero. The 2026-07-28 experiment that "proved"
subsystem zones cannot be damaged -- and that the Myomers un-powered
self-repair was dead code in the original -- was measuring exactly this port
bug. Both verdicts reversed: the binary ctor (0x4ac7bb) initializes the zone
from the resource keys WeaponDamagePoints (required) + CriticalHitScoreBonus
(required) + Collision/Ballistic/Explosive/Laser/EnergyDamagePoints, none of
which the CSS parsed. Now parsed (with the binary's own error strings), and
the ctor writes the engine's NAMED members -- layout-parity holds, so they
land on +0x140/+0x144 faithfully. The Myomers repair branch is LIVE, in 1995
and here. KB corrected and swept (combat-damage, subsystems WAVE 6,
myomers.cpp, CLASSMAP).
Live-verified twice: [subarmor] prints real parsed scales for every subsystem
at spawn (HeatSink pts=10 scale=0.1x5, Condensers pts=5 scale=0.2x5, ...);
[critroll] landed full-chain crits in both runs (zone -> weighted pick ->
subsystem's own zone driven to 1.0 -> Destroyed); mech death/respawn and the
ammo gates un-regressed; zero crashes/asserts. Honest gaps: burst>1 spraying
is transcribed but not yet exercised live (self-damage fires burst=1), the
damageType==0 COLLISION divert (@0x49ffcc) is documented-not-reconstructed,
and the id-0x16 damage/kill report messages to the players (the authentic
stats plumbing, decoded to field level in the KB) are deferred to the #45
work.
Diags: BT_CRIT_LOG ([subarmor] + [critroll]), the existing BT_DMG_LOG.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
f7cf9850b1 |
crit panel: the roll has no caller and the sink is a stub -- filed as #80
Night-6 observation (Conn Man, with screenshot): armor panel showing damage,
Critical damage display showing no crits. Investigated; it is a real port gap
in three layers, stacked on one authentic fact.
The authentic fact: the paper doll shows per-ZONE armor tint and the Critical
view is a per-SUBSYSTEM list. Different data by design -- the panel staying
dark while armor accumulates is correct right up until a subsystem takes
critical damage.
The gap: subsystem critical damage essentially cannot happen.
1. Mech__DamageZone::CriticalHit @0049ccc4 -- the authored roll, half the
hit to armour, half to one critical subsystem by criticalWeight -- has
ZERO callers in the port. A raw byte-scan of the binary finds exactly
one call site, @0x4a0461, and it sits in the un-exported decomp gap
(nothing covers 0x4a03xx-0x4a05xx, the same dark region as the
targeting-pick writer). The trigger conditions are unknown.
2. MechSubsystem::TakeDamage is an empty bring-up stub (btstubs.cpp:179),
so even a wired caller would measure a delta of zero through
ApplyDamageAndMeasure.
3. The only live crit sources are zone destruction (SendSubsystemDamage)
and ammo cook-off (DistributeCriticalHit), which pin subsystem damage
directly -- so the panel can light after a zone is destroyed outright,
never from accumulating fire.
#28 (the vital-subsystem-crit death path) very likely shares this root and is
cross-linked. Recovery plan on #80: raw-disasm the 0x4a04xx container for the
trigger, dump MechSubsystem vtable 0050e210 slot +0x24 for the real TakeDamage
body, wire both.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
a5dd0eabc5 |
issue #73 investigated: zone selection is a weighted lottery -- per-limb aiming never existed
Night-6 report: three players, controlled conditions, "fired only at the left arm, damage credited all over the paper doll, no crits". Chased it to the bottom and the bottom is the authored 1995 model, working as designed. The zone a hit credits is chosen by dice, in binary-faithful code reading binary-shipped tables (dmgtable.cpp, stream format byte-verified): impact point -> height layer (floor(layerCount*y/heightRef)) -> pie slice (atan2(z,x) around the vertical axis, optionally rotating with live torso twist) -> DamageZonePercentTable::SelectZone() = RandomUnit() rolled against cumulative percent thresholds (@0x49de14). Every slice carries an authored DISTRIBUTION of zones. Aiming at a limb at best biases which slice you strike; the zone inside it is a weighted roll. Pixel-precise limb damage does not exist in Tesla 4.10. Measured live to be sure the code read was real: solo dummy, walked to 8u, aim pinned left/center/right across the silhouette, trigger held. 24-46 hits per aim point, each spread across 6+ zones (the percent tables), with the distribution shifting by aim point (the slice selection responding). Both halves of the model visibly working. What remains genuinely open, and is now the whole of #73: the shooter's pick point comes from Mech::PickRayHit, a whole-mech AABB slab test -- a port stand-in, since the binary's 0x37c/0x388/0x38c writer sits in an un-exported gap nobody has decompiled. Theta computed from a flat box FACE clusters toward the slices facing the shooter, so flank slices (presumably arm-heavy) may be under-reachable from frontal shots compared with the pod, which plausibly intersected the mech's cylinder -- the damage table is literally cylindrical. Deciding that needs a per-hit theta probe and a one-shot wheel dump; if confirmed, the fix is a ray-vs-cylinder pick replacing the box slab. Filed on #73 with the plan. KB: combat-damage.md gets the lottery model as a load-bearing triage fact -- "damage landed somewhere I didn't aim" now has a documented base rate, and crit expectations from aimed fire are probabilistic by design. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
dca2586aa8 |
the Owens crash: a device reset that never waited for the device (#35)
Eight byte-identical field stacks from night 6, all one player, all in an
Owens: ParticleEngine::Destroy +0x11, access=0 target=0x0, from the plain
per-frame render path. Nothing in the stack touches weapons or the Owens.
Conn Man's Surface Pro 9 (Iris Xe, 128 MB shared) is simply the only GPU in
the fleet that ever actually LOSES the D3D9 device -- his two-trigger
missile+laser bursts are what provoke the timeout, not what crashes.
What crashed is our device-loss handling, which was wrong three ways at once,
in two inline copies (the scene Present and the wait-screen Present):
1. On D3DERR_DEVICELOST it called Reset() IMMEDIATELY. Reset on a
still-lost device ALWAYS fails, and V() only logs. There was no
TestCooperativeLevel gate at all.
2. It then ran ParticleEngine::Initialize against the lost device. The
creates fail there and NULL their out-params -- proven, not assumed:
the bench repro faults at target=0x0, not at a dangling address.
3. The next lost frame called ParticleEngine::Destroy again, which
Release()d those NULLs blind. Read of vtable at 0x0. Dead.
So: lost frame 1 tears down and leaves NULLs, lost frame 2 crashes. Two
frames, every time, deterministic -- which is exactly why all 8 field stacks
are byte-identical.
Reproduced before fixing. BT_DEVICELOST_TEST=<frame>,crashrepro runs the
field sequence on the bench; on the unfixed build it died at Destroy +0x11,
access=0 target=0x0, and symbolized to the same four frames as the field
logs. Same shape, same offsets-modulo-hook. That run also proved the
out-param-nulling assumption the whole diagnosis rested on.
The fix -- one shared DPLRenderer::BTResetLostDevice() replacing both inline
copies:
- Destroy() is idempotent and null-safe, and nulls after release.
- Reset() is gated on TestCooperativeLevel() != D3DERR_DEVICELOST; while
the driver still says lost, skip the frame and retry.
- The Reset HRESULT is checked; on failure, log and retry next frame
instead of driving on.
- On success, re-create via the new CreateDeviceObjects(), NOT
Initialize(): Initialize memsets the installed-effects table, so every
reset that DID succeed silently killed all particle effects for the rest
of the mission. The quieter sibling bug, fixed by the same split.
- Initialize checks its HRESULTs and defends MAXPARTICLES<=0; the draw
paths guard the NULL buffer, and ExecuteParticles keeps draining
particles while the engine is dormant so they cannot pile up.
Verified: the crashrepro shape now logs SURVIVED and play continues; three
forced full loss/reset cycles each log "[render] device reset OK"; a plain
run is assert-free.
Found while verifying, worth its own line: VIDEO\particles.png has NEVER
existed -- not in the tree, not in BTL4.RES, not anywhere in git history.
The texture load has failed on every machine since the engine was written,
and every billboard particle ever rendered was untextured quads via
SetTexture(0, NULL). RenderParticles deliberately does NOT gate on the
texture -- that would disable all particles everywhere; untextured IS the
shipped look. Filed separately; a real particle sheet is a content task.
The field verification that counts is Conn Man flying his exact crash
loadout on this build: instead of a dead process he should see at worst a
brief hitch and "[render] device reset OK" in his log. #35 stays open until
that happens.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
29c502df83 |
Glass windows remember where you drag them (BT_GLASS_LAYOUT)
The BT_GLASS_PANELS windows self-place in a pod-faithful ring and re-snap
once the main window is up. They carry WS_CAPTION so a dev can drag them,
but the drag never survived the menu->mission->menu relaunch loop.
BT_GLASS_LAYOUT (L4GLASSWIN.cpp) adds opt-in persistence to a cwd-relative
glass_layout.cfg beside bindings.txt (gitignored):
off/0/unset computed ring only, no file I/O (default, pod-faithful)
load restore saved positions on startup (per-window fallback
to computed); never writes
save/adjust restore first, then rewrite on each finished drag
(WM_EXITSIZEMOVE) and on teardown -- the round trip
One "<title>=x,y,w,h" line per window; position restored, size ignored
(frame size is deterministic from content, so an old w,h can't distort a
later geometry change). A restored window is flagged so ComputeLayout's
post-main-window re-snap leaves hand-placed windows alone. Native
analogue of TeslaRel410 pod-launch's per-rig --bridge-pos/--layout args.
Verified [T2 runtime round-trip]: load restored 7 from a seeded cfg;
save wrote all 7 with a window moved to 777,333 (GetWindowRect read the
live window); relaunch in load put that window physically at 777,333
after the re-snap fired. Release links clean (only the 40 tolerated
/FORCE externals).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|