Merge BT411 audio-fidelity + combat work (4e72f0c..abed41e) into BT412
Brings the post-fork BT411 line forward via a local-path merge (never touches the BT411 gitea remote): the full audio-fidelity system (engine AUD*/L4AUD* + audiopresets.cpp + ~600 content wavs + AUDIO_FIDELITY.md), missiles/rear-fire/HUD/gyro/gait tasks (#66-68), FOGDAY.EGG, and refreshed context docs -- 91 commits, ~688 files clean. Only 5 files overlapped the steamification; resolved keeping BOTH: - L4NET.CPP: took BT411's task-#50 fix (don't close the game listener on console loss) over the seam's adaptation of the old buggy close; sends stay on NetTransport_Get(). - L4NETTRANSPORT.cpp: folded BT411's TCP_NODELAY latency fix into WinsockNetTransport::Connect (the seam already had retry + nonblocking). - mechmppr.cpp: combined the device_owns_input gating with BT411's task-#68 look-behind, gating the lookBehind write too. - .gitignore / CMakeLists.txt / mech4.cpp: trivial / auto-merged (deviceOwnsInput gating preserved). Verified: clean build (default + implicitly the Steam TU untouched); solo front-end mode; loopback MP through the seam (mesh completes, both tick, replication works, no NODELAY warnings). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@ status: living
|
||||
source_sections: "task #55 (2026-07-10); reference/decomp part_007.c/part_013.c/part_014.c/part_011.c; engine/MUNGA_L4/bgfload.cpp, L4VIDRND.cpp; game/reconstructed/btl4vid.cpp"
|
||||
related_topics: [rendering, bgf-format, gauges-hud, subsystems, decomp-reference]
|
||||
key_terms: [material-ramp, siteeyepoint]
|
||||
open_questions: ["gyro tail: gait jolt/rumble [T3 gates], sway model, torso-pitch eye writer, damageForce fill", "exact authored frame colour: the unlit constant (0.13,0.12,0.15) is T3 tuned-to-footage (BT_COP_FRAME overrides; the ramp-index approach + BT_COP_RAMP_L are RETIRED)"]
|
||||
open_questions: ["gyro tail: gait jolt/rumble [T3 gates], torso-pitch eye writer, damageForce fill (sway model DONE: the mech instability model @0x3F0 feeds swayBias -- task #66)", "exact authored frame colour: the unlit constant (0.13,0.12,0.15) is T3 tuned-to-footage (BT_COP_FRAME overrides; the ramp-index approach + BT_COP_RAMP_L are RETIRED)"]
|
||||
---
|
||||
# First-Person Cockpit: the *_cop Canopy + the Authentic Eyepoint
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ source_sections: "PROGRESS_LOG.md §10c; docs/HARD_PROBLEMS.md (P5)"
|
||||
related_topics: [subsystems, decomp-reference, reconstruction-gotchas, locomotion, rendering, translocation-warp, multiplayer]
|
||||
key_terms: [damage-zone, TakeDamage, viewpoint-entity, master, replicant]
|
||||
open_questions:
|
||||
- "Per-impact aim (cylinder lookup); collision damage application; DeathShutdown + collapse anim"
|
||||
- "Missile cluster-hit variance/concentration (task #60-D deferred); per-player splash enable sub-gate (Missile+0x360 [T3]); ~100pt ram lethality vs the pod unverified [T3]"
|
||||
---
|
||||
|
||||
# Combat & Damage
|
||||
@@ -61,6 +61,16 @@ Fixed-torso mechs (the BLH: `TorsoHorizontalEnabled=0` — no jointtorso in the
|
||||
boresight dead-ahead. Once locked, `Emitter::FireWeapon` converges with NO aim/arc test
|
||||
(part_013.c:7758). `MechWeapon::UpdateTargetState` (`FUN_004b9bdc` [T1]): `targetWithinRange =
|
||||
dist < (1 − hostZoneDamage) × weaponRange`. The 0x388 WRITER is in the same un-exported gap.
|
||||
⚠ **`hostZoneDamage` is `Subsystem::damageZone->damageLevel` (weapon `@0xE0 → +0x158`), NOT
|
||||
heatLoad** — the port originally computed `effectiveRange = (1 − heatLoad) × weaponRange`
|
||||
(mechweap.cpp), the SAME `@0xE0`-DamageZone-vs-heat misattribution corrected in
|
||||
`HeatSink::UpdateCoolant`. For a charge/discharge weapon (ER laser) the weapon's own `heatLoad`
|
||||
swings 0→1 every cycle, so `effectiveRange` collapsed to ~0 and the weapon was perpetually "out
|
||||
of range" → `SendDamageMessage` never ran → NO damage and NO impact explosion (the "lackluster/
|
||||
absent laser hits, esp. the ER medium, on mechs AND buildings" report). FIXED 2026-07-15: read
|
||||
the QUALIFIED `this->Subsystem::damageZone->damageLevel`; an undamaged weapon now holds full
|
||||
STABLE `weaponRange`. Verified: laser `effRange` 500 stable (was 0/59/340/424), impact explosions
|
||||
13/22s (was ~2). [T2] See [[reconstruction-gotchas]] (`@0xE0` shadow/heat class).
|
||||
**Port status: RECONSTRUCTED + runtime-verified (tasks #36/#39/#58) [T2].** The chain: the
|
||||
cockpit eye inherits the twist AUTHENTICALLY through the draw traversal (`Torso::PushTwist` →
|
||||
hinge `rotationAmount` → `HingeRenderable::Execute` multiplies the live hinge into the matrix
|
||||
@@ -104,6 +114,32 @@ bug was fixed en route: every
|
||||
renderable rebuild stomped `mCamera` back to the chase eye (btl4vid `mViewInside` now persists
|
||||
the chosen view — the aim camera feed and the V toggle both depend on it).
|
||||
|
||||
**WORLD STRUCTURES ARE TARGETABLE via the STATIC COLLISION TREE (task #50, 2026-07-15) [T2].**
|
||||
The `BTGroundRayHit` "terrain" tier only samples the VISUAL heightfield, which on arena1 is a
|
||||
single flat `'sky'`-named ground mesh (`[visgnd] sampler ready: 1 terrain instances`) — and the
|
||||
class-42 map-instance stream (`BuildTables`) likewise holds only `'sky'`. So the garages/walls
|
||||
were invisible to the pick → shots passed through them, and only mechs moved the range axis (the
|
||||
user-reported regression). The FIX: the boresight now also ray-tests the **zone's static collision
|
||||
solid tree** — the SAME geometry that blocks the mech's walk. Mechanism is authentic engine code:
|
||||
`Mover::FindBoxedSolidHitBy` (MOVER.cpp) already tests movers, doors, AND the static world
|
||||
(`application->GetInterestManager()->GetInterestZone(interestZoneID)->GetCollisionRoot()->FindBoundingBoxHitBy(line)`).
|
||||
We factored its "test against the static world" tail into **`Mover::FindStaticSolidHitBy(Line*)`**
|
||||
(static solids only, no movers), wrapped by **`Mech::WorldStructurePick(start,dir,range,&hit)`**
|
||||
(mech.cpp — builds the world-space `Line`, reads the entry point back via `line->FindEnd` since
|
||||
`FindBoundingBoxHitBy`→`HitByBounded` clips `line->length = enter`). The pick order in mech4.cpp is
|
||||
now: closest MECH (`PickRayHit`, damage zones + lock) → closest STRUCTURE (`WorldStructurePick`,
|
||||
occludes a mech BEHIND it) → flat ground (`BTGroundRayHit`) → sky (fire-at-nothing). A structure
|
||||
hit designates `mech+0x388 = gBTTerrainEntity` (the no-damage-zone sentinel) + `0x37c` = the entry
|
||||
point, so the range caret reads the structure distance and NO lock ring draws (mech4.cpp:4529, the
|
||||
`des != hotTarget` branch), exactly the reported authentic behavior. ⚠ There is NO ray query
|
||||
against the ground/`containedByNode` `BoundingBoxTree` — `FindBoundingBoxUnder` is DOWNWARD-only
|
||||
(gravity/ground-snap: `*height = FindDistanceBelowBounded`), useless for a horizontal boresight;
|
||||
the static SOLID tree (`BoxedSolidTree::FindBoundingBoxHitBy`) is the only ray-vs-world query.
|
||||
Verified: a `BT_WSWEEP` horizontal ray-fan on arena1 tracks position (3/24 hits near the boundary
|
||||
→ 17-21/24 inside the interior garage cluster → 3/24 past it — discrete interior solids, not just
|
||||
an enclosing box), no crash. The interactive boresight (`BTGetAimRay`) can't be exercised headless
|
||||
(no window → `noRay`), so the SWEEP is the headless proof; interactive aim is user-verified.
|
||||
|
||||
## Firing arc + muzzles
|
||||
There is NO firing-arc field in the binary (`Emitter::FireWeapon` fires whenever HasActiveTarget) —
|
||||
the 1995 game got away with it because it was **cockpit-only** (a beam behind you is off-camera). The
|
||||
@@ -112,6 +148,35 @@ port's external camera exposes it, so the arc is gated on the **torso twist rang
|
||||
(`BT_FIRE_ARC`, a labeled PORT presentation param). Beams fire from the mech's real gun-port SITE
|
||||
segments (siterugunport etc.) resolved via `GetSegment(name)→segmentToEntity×localToWorld`. [T2]
|
||||
|
||||
## REAR-FIRE + look views (task #68, 2026-07-16) [T1]
|
||||
The pod had a rear arsenal + a five-state LOOK-VIEW system (all reconstructed + live):
|
||||
- **weapon.rearFiring @0x334** = the MOUNT SEGMENT page name contains 'b' (ctor marker @0x511aa2;
|
||||
the back ports `sitelbgunport`/`siterbgunport`). Roster survey: **the Blackhawk's ERMLaser_2/3
|
||||
are the ONLY rear weapons in the game** (both loadout variants; the Owens model HAS back ports
|
||||
but mounts nothing on them; every missile rack is forward).
|
||||
- **mech.rearFiring @0x410** = OR of the weapons' flags (ctor part_012.c:10220).
|
||||
- The mapper look machine (part_013.c:396-459; modes 0=fwd 1=left 2=right 3=BACK 4=down from the
|
||||
look buttons): on change it writes **EyepointRotation (mech+0x360)** — side yaw = the authored
|
||||
per-mech look angles (model rec +0x50-0x5c → mech 0x564-0x570), look-back = yaw π + lookBackAngle
|
||||
pitch — and re-arms each weapon's **viewFireEnable @0x3E0** (fwd = !rear, back = rear, else 0).
|
||||
0x3E0 is the fire gate (the Loaded→Firing transition, part_013.c:7689 — the old
|
||||
`useConfiguredPip` label) AND selects the HUD pips: the reticle registers each pip in group
|
||||
1(front)/2(rear) from the RearFiring attr (part_014.c:5429-34) and draws the group in the mech's
|
||||
reticleElementMask low bits (mech+0x390: fwd |1&~2, back |2&~1, part_013.c:5588-95).
|
||||
- Port: keyboard **HOLD 'V' = look back**; side/down looks not yet key-bound. `[rearfire]`/
|
||||
`[look]` traces under BT_PROJ_LOG/BT_KEY_LOG.
|
||||
- Missile LAUNCH direction: rounds leave along the **mount segment's world frame** (the binary
|
||||
spawns the missile with the full muzzle-segment AffineMatrix, fire builder @0x4bcc60:8762-64,
|
||||
authored MuzzleVelocity composed with z NEGATED + the mech's own velocity) — was the
|
||||
"missiles fire out of his back" torso-twist bug (launch followed the LEGS while the muzzle
|
||||
followed the twist).
|
||||
- BANKED for the full Missile-entity revival (decoded, not yet ported — commit ab6ea83): the three
|
||||
authentic performances — Seeker 0x4be9a0 (aim = target-frame offset / 100u-ahead-of-own-frame
|
||||
dumb + loft/lead 0x4beae4), Thruster 0x4be474 (quaternion-slerp BODY turn toward the aim at
|
||||
turnRate°/s), MoveAndCollide 0x4bef78 (velocity aerodynamically aligned to thrust via
|
||||
signed-square per-axis gains, ballistic droop as fuel burns, PROXIMITY FUSE on seeker
|
||||
rangeToTarget, max-range + altitude-floor retire).
|
||||
|
||||
## Weapon fire is a PORT reconstruction (the dpl_* beam layer is unported)
|
||||
`Emitter::FireWeapon` builds a muzzle→target beam but the `dpl_*` beam renderable was never ported →
|
||||
the beam is drawn by `BTPushBeam`/`BTDrawBeams` (L4VIDEO — an additive quad / the real `ermlaser.bgf`
|
||||
@@ -201,10 +266,11 @@ Real sample (`ava1`, 7 rows × 8 cells): rows 0-2 `rotateWithTorso=0` (feet/legs
|
||||
rows 3-6 `=1` (upper body — rotates with torso twist); a foot cell = `{0.5→16, 0.8→10, 1.0→5}`. The table
|
||||
is found by the mech's **type-0x14 DamageZoneStream NAME** (`ResourceDescription::resourceName`) →
|
||||
`ResourceFile::FindResourceDescription(name, DamageLookupTableStreamResourceType/*29*/)`.
|
||||
So STEP 6 remaining = (6b) fix the name-load (current `ResourceFindByName` is a NO-OP template stub → empty
|
||||
name → 0 rows); (6c) make the table functional + `ResolveHit→zone`; (6d) register the
|
||||
`Mech::TakeDamageMessageHandler` override + verify. **Nothing is guessed — geometry, roll, handler, and byte
|
||||
format are all verified.**
|
||||
So STEP 6 was = (6b) the name-load; (6c) the table + `ResolveHit→zone`; (6d) the
|
||||
`Mech::TakeDamageMessageHandler` override. **✅ ALL DONE — see "STEP 6 COMPLETE" below (`d07ac7d`):** the
|
||||
name-load BYPASSED the no-op `ResourceFindByName` via `SearchList(type=0x14)` (mech.cpp:1631), so the table
|
||||
loads live (`[cyl] table 'bhk1'/'madcat'/'ava1' layers=7`) and `ResolveHit` resolves unaimed hits to zones.
|
||||
**Nothing was guessed — geometry, roll, handler, and byte format all verified.**
|
||||
|
||||
## The classes ALREADY EXIST — `game/reconstructed/dmgtable.cpp` (2026-07 discovery)
|
||||
The three container classes are **already reconstructed** in `dmgtable.cpp`/`.hpp` (in the build):
|
||||
|
||||
@@ -58,13 +58,13 @@ reconciled via `CLASSMAP.md`, not the case label. [T1]
|
||||
| 0xBC3 | **Sensor** | — | `case`=MyomersClassID |
|
||||
| 0xBC4 | **Gyroscope** | @004b3778 | LIVE (task #56: byte-exact ctor/integrators/writers; hit-bounce fan-out wired) |
|
||||
| 0xBC5 | **Torso** | @004b6b0c | sizeof 0x280; `case`=SinkSource |
|
||||
| 0xBC6 | **Myomers** | @4b8fec | `case`=Actuator; INERT un-stub |
|
||||
| 0xBC6 | **Myomers** | @4b8fec | `case`=Actuator; un-stubbed (`8b36440`, full recon; per-tick Sim SetPerformance call-site still best-effort) |
|
||||
| 0xBC8 | **Emitter** | @004b1d18 | sizeof 0x478; energy weapon |
|
||||
| 0xBCB | **AmmoBin** | — | `case`=JumpJet; sizeof 0x22C |
|
||||
| 0xBCD | **ProjectileWeapon** | @4bc3fc | sizeof 0x448 (`case`≠ this — see VDATA.h) |
|
||||
| 0xBCE | **GaussRifle** | @4bdcb4 | : Emitter; FireWeapon is a **no-op** in this build |
|
||||
| 0xBD0 | **MissileLauncher** | @4bcff0 | sizeof 0x44C |
|
||||
| 0xBD3 | **SubsystemMessageManager** | @0049bca4 | damage/explosion consolidation hub (ConsolidateAndSendDamage, weaponExplosions); cached to `Mech[0x10d]`=**0x434** (recon MISLABELS it `controlsMapper`; the drive squats there — the real mapper is roster slot 0). NOT the valve/Myomers gate (that's the owning **BTPlayer** @mech+0x190). WAVE 8 = untangle + damage-hub. |
|
||||
| 0xBD3 | **SubsystemMessageManager** | @0049bca4 | damage/explosion consolidation hub (ConsolidateAndSendDamage, weaponExplosions); cached to `Mech[0x10d]`=**0x434** (✅ task #7 `afefaee`: factory builds the real class + `mech.hpp` names it `messageManager`; the `controlsMapper` mislabel is swept — the real mapper is roster slot 0). NOT the valve/Myomers gate (that's the owning **BTPlayer** @mech+0x190). |
|
||||
| 0xBD4 | **PPC** (: Emitter) | — | same FireWeapon as Emitter; `factory calls it GaussRifle` |
|
||||
| 0xBD6 | **HUD** | — | alloc 0x2a4; `case`=MechDisplay-ish |
|
||||
| 0xBD8 | **Searchlight** | — | `case`=LegSubsystem |
|
||||
@@ -113,7 +113,7 @@ valid for reading the decomp, NOT for our compiled layout (see [[reconstruction-
|
||||
| +0x11c / +0x120 | `damageZoneCount` / `damageZones[]` | inherited Entity; Mech ctor populates the array |
|
||||
| +0x124 / +0x128 | `subsystemCount` / `subsystemArray[]` | the [[subsystem-roster]] (NOT the segment table) |
|
||||
| +0x190 | **owning `BTPlayer`** (`GetPlayerLink()`) | set by `FUN_0049f624` (mech↔player bind; also `player->playerVehicle(+0x1fc)=mech`). The valve guard (`FUN_004ac9c8 →player+0x274`) + Myomers gate (`FUN_004ad7d4 →player+0x260`) read the owning player's game-mode flags (NOT the messmgr @0x434). `MECH_OWNING_PLAYER`. |
|
||||
| +0x434 (word 0x10d) | `SubsystemMessageManager` cache | the 0xBD3 damage/explosion hub (factory @10142). Our recon MISLABELS this `controlsMapper` + the drive squats here; the real mapper is roster slot 0. |
|
||||
| +0x434 (word 0x10d) | `SubsystemMessageManager` cache | the 0xBD3 damage/explosion hub (factory @10142). ✅ task #7 (`afefaee`): `mech.hpp` names this `messageManager`; the `controlsMapper` mislabel is swept — the real mapper is roster slot 0. |
|
||||
| +0x260 / +0x26c | `localOrigin` = { Point3D linearPosition; Quaternion angularPosition } | ORIGIN.h:15; position@+0x100 in some subsystem views |
|
||||
| +0x300 | segment table | skeleton `EntitySegment`s (muzzle sites, joints) |
|
||||
| +0x37c | target world `Point3D` | aim source |
|
||||
@@ -127,12 +127,41 @@ valid for reading the decomp, NOT for our compiled layout (see [[reconstruction-
|
||||
|
||||
`mech+0x100` (a subsystem-view of localOrigin.linearPosition): `.x@+0x100, .z@+0x108`. [T1]
|
||||
|
||||
### Binary ATTRIBUTE TABLES (2026-07-16, walked from the image) [T1]
|
||||
|
||||
**Row format: 16-byte `{id, namePtr, memberOffset+1, 0}`** (MIND THE ALIGNMENT — an 8-off walk
|
||||
invents a fake one-slot shift; verified against the live-tested ConfigureActivePress row
|
||||
@0x50de54). Walker: find the attr NAME string in DATA, find the pointer to it, decode ±16-byte
|
||||
rows until sanity fails (`python + struct`, see the session commits `cc2b109`/`21378ec`).
|
||||
|
||||
- **Mech entity table @0x50c0xx, ids 21–56**: MaxAcceleration@0x344 (=forwardCycleRate),
|
||||
CurrentSpeed@0x348 (=legCycleSpeed), MaxRunSpeed@0x34c (=run-cycle avg = the mapper's demand
|
||||
multiplier), Collision{State,Normal,Speed,MaterialType}@0x450/0x4a8/0x4b4/0x4a4,
|
||||
EyepointRotation@0x360 (the eye renderable's view rotation — the old `mechName@0x360` label is a
|
||||
MISREAD), TargetReticle@0x36c, FootStep@0x394, AnimationState@0x39c, ReplicantAnimationState
|
||||
@0x714, LinearSpeed/AngularSpeed/ClimbRate/AccelerationLastFrame@0x81c/0x824/0x828/0x82c (the
|
||||
15-sample-ring velocity means + the accel snapshot), damage levels @0x354-0x35c, TestButton1-6
|
||||
@0x328-0x33c, ReduceButton@0x340, Radar{Range,LinearPosition,AngularPosition}@0x404-0x40c,
|
||||
**RearFiring@0x410** (OR of the weapons' rear flags; old `stateFlags` label was wrong),
|
||||
RequestDuckAnimation@0x398, UnstablePercentage@0x3f0, SuperStop@0x3f4, DuckState@0x3f8,
|
||||
IncomingLock@0x3fc, DistanceToMissile@0x400 (FLT_MAX far default; old `maxSpeed` label wrong).
|
||||
- **HeatSink table @0x50e438, ids 3–12**: HeatSink@0x164, CurrentTemperature@0x114,
|
||||
Degradation/FailureTemperature@0x118/0x11c, Normalized/DegradationPressure@0x120/0x124,
|
||||
CoolantCapacity@0x128, CoolantMass@0x12c, CoolantMassLeakRate@0x130, **ReportLeak@0x138**
|
||||
(= coolantActive, the leak hysteresis flag). Confirms every port heat binding.
|
||||
- **Torso table, ids 3–15**: RotationOfTorsoVertical/Horizontal@0x1e4/0x1d8, HorizontalLimit
|
||||
R/L@0x1dc/0x1e0, SpeedOfTorsoVertical/Horizontal@0x1ec/0x1e8 (|rate|, abs'd),
|
||||
StickPosition@0x1f0, TorsoUp/Down/Left/Right/Center@0x1f8-0x208, MotionState@0x20c
|
||||
(statusFlags; ==2 = the limit-hit frame).
|
||||
- **MechWeapon rows**: RearFiring id 0x1B @0x334 = "the mount segment page name contains 'b'"
|
||||
(marker @0x511aa2 — the back gun ports `sitelbgunport`/`siterbgunport`; ctor part_013.c:6913-30).
|
||||
|
||||
---
|
||||
|
||||
## 4. Damage delivery
|
||||
|
||||
- `Entity::TakeDamageMessage(id, size, inflictingEntityID, zone, Damage&)` → `target->Dispatch(&msg)`.
|
||||
- Base `Entity::TakeDamageMessageHandler` **IGNORES zone==-1** — send a VALID zone index (the Mech cylinder-lookup override for unaimed hits is not reconstructed). [T1]
|
||||
- Base `Entity::TakeDamageMessageHandler` **IGNORES zone==-1** — the Mech STEP-6 cylinder-lookup override (✅ done, `d07ac7d`; `dmgtable.cpp` `ResolveHit` + `Mech::damageLookupTable`) resolves an unaimed (zone==-1) hit to a zone before base-routing. [T1/T2]
|
||||
- `class Damage { damageType(enum Collision/Ballistic/Explosive/Laser/Energy), damageAmount, damageForce, surfaceNormal, impactPoint, burstCount }`. [T1]
|
||||
- Weapon effect id: **"explode" = 13** (`Explosion::Make`). [T2]
|
||||
- `DestroyEntityMessage(id,size)` removes an entity — but a killed mech STAYS (a WRECK); death = a STATE transition (`SetGraphicState(DestroyedGraphicState)`), NOT removal. Issuing removal-on-death is the P5 teardown bug (do not). [T2]
|
||||
|
||||
+14
-1
@@ -6,7 +6,7 @@ source_sections: "PROGRESS_LOG.md §7, §10 (gait/ground/collision/shadow); docs
|
||||
related_topics: [asset-formats, decomp-reference, reconstruction-gotchas, combat-damage, rendering]
|
||||
key_terms: [RootTranslation, ground-model, SequenceController, MechControlsMapper, SLD, joint]
|
||||
open_questions:
|
||||
- "Authentic per-mech turn-rate constant (currently bring-up constant; the authored WalkingTurnRate/RunningTurnRate now STREAM correctly since the task #4 record-layout fix -- rec words 0x18/0x19 deg->rad -> mech+0x574/0x578, Wword-absorbed -- wiring them in retires the bring-up rate)"
|
||||
- "Turn-in-place FEEL re-verification (authentic release at standSpeed vs the earlier 0.25x stand-in -- subjective playtest, non-gating)"
|
||||
- "Airborne clip callbacks (FUN_004a7970) deferred"
|
||||
---
|
||||
|
||||
@@ -114,6 +114,19 @@ through walk/run caps (`LoadLocomotionClips`, mech3.cpp:326 — measures `walkSt
|
||||
`legAnimation.SelectSequence`). Transition callbacks (`Mech::BodyClipFinished @0x4a6d8c`,
|
||||
`LegClipFinished @0x4a6928` — PE-parsed jump tables) alternate walk 6↔7 / run 12↔13 + walk→run. [T2]
|
||||
|
||||
**Gait DEAD BAND (2026-07-16, byte-decoded) [T1]:** the SM has NO stable state for a demand in
|
||||
(`walkStrideLength`@0x534, `reverseSpeedMax`@0x538). Cap semantics (LoadLocomotionClips @0x4a80d4):
|
||||
0x534 = walk-cycle avg root speed (clips 6+7); **0x538 = the LAST KEYFRAME SPEED of clip slot 10 =
|
||||
the walk→run transition clip's exit speed — the run ENGAGE threshold, not a "reverse max"**; 0x34c
|
||||
= run-cycle avg root speed (clips 12+13) — and 0x34c is the mapper's demand multiplier
|
||||
(`FUN_004afd10`: `speedDemand = mech->0x34c × throttle × 0x5c0`, continuous). Walk cyc clamps to
|
||||
[standSpeed, 0x534]; run cyc clamps to [0x538, 0x7a0]. The finished-callback up-shifts when
|
||||
`tgt > 0x534` but enters/sustains run only when `tgt ≥ 0x538` (x87 decode of 0x4a6f11/0x4a6fc7/
|
||||
0x4a7041) → a demand parked between them cycles 7→10→14→7 forever ("gallop hunt"), firing the
|
||||
authored EngineShiftFwd/Rev audio each swing (Blackhawk: 22.02–30.87 = throttle 36–50%). Faithful
|
||||
mechanics — likely masked on real hardware by MECHANICAL lever detents ([[pod-hardware]] [T4]).
|
||||
Port: keyboard lever snaps out of the band at rest (mech4.cpp "GAIT DETENT" accommodation).
|
||||
|
||||
## Turn-in-place (trn, state 4) — AUTHENTIC dispatcher decoded (task #64, 2026-07-14)
|
||||
**The entry dispatcher is NOT in the Ghidra decomp** — it lives in the master-perf gap
|
||||
`0x4a9b5c–0x4ab188` (functions_index.tsv jumps FUN_004a9770→FUN_004ab188; the SOLE reader of
|
||||
|
||||
+40
-8
@@ -6,7 +6,7 @@ source_sections: "PROGRESS_LOG.md §7 (Phase 7), §8 (P6); docs/HARD_PROBLEMS.md
|
||||
related_topics: [wintesla-port, combat-damage, locomotion, decomp-reference]
|
||||
key_terms: [master, replicant, EGG, dead-reckon]
|
||||
open_questions:
|
||||
- "pod-LAN config (real IPs); update-record velocity from the body-channel projection; peer SimulationState/DropZoneLocation replication (authentic peer warp spheres); authentic per-mech turn rates now streamed (task #4) but not yet wired into the turn code"
|
||||
- "pod-LAN config (real IPs); peer warp DropZoneLocation-anchoring [T3] (world-anchored peer sphere already visible, 160b78e); console-death replication freeze (pending recurrence to diagnose)"
|
||||
---
|
||||
|
||||
# Multiplayer
|
||||
@@ -262,8 +262,18 @@ Base fns in every subsystem vtable: slot 6 = 41bd34 (Read/apply), slot 7 = 41c50
|
||||
invisible). First real twist record overwrites it.
|
||||
|
||||
## Remaining (P6 phase 4 / Phase 7)
|
||||
The pod-LAN config (real IPs, bare-IP pilot entries); update-record velocity sourced from the body-channel
|
||||
projection (above). See [[open-questions]]. [T3]
|
||||
Pod-LAN config: **real-IP path VALIDATED (2026-07-15)** — an egg with the machine's real LAN IP in
|
||||
`[pilots]` (not `127.0.0.1`) connected + replicated + moved end-to-end (`Connected to
|
||||
GameMachineHost at 10.0.0.46:1602`, `All connections completed!`, peer telemetry flowing). So
|
||||
`gethostbyname` local-address matching (L4NET.CPP:2612, which also appends 127.0.0.1) + the
|
||||
`WSAStringToAddressA` `IP:PORT` parse (ResolveAddress) both work with real addresses — NOT
|
||||
localhost-locked. Recipe: egg `[pilots]` = each machine's LAN IP + its GAME port (`= -net port + 1`,
|
||||
e.g. `10.0.0.46:1502`); each machine runs `btl4 -egg lan.egg -net <consolePort>`; console relay
|
||||
(`btconsole.py lan.egg A_IP:1501 B_IP:1601`) from any machine; open the TCP console+game ports in
|
||||
the Windows firewall. STILL UNTESTED: an ACTUAL two-physical-machine run (validated single-box via
|
||||
the real LAN IP), so firewall is the main first-attempt unknown. Affinity pinning is NOT needed
|
||||
across separate machines (the packet-jitter artifact is single-box only). Also: update-record
|
||||
velocity sourced from the body-channel projection (above). See [[open-questions]]. [T2 path / T3 two-box]
|
||||
|
||||
## MP-front scout (task #45, 2026-07-09) — the P6 chain SURVIVES the combat/HUD rework [T2]
|
||||
Re-ran the one-box smoke test on the current build (world-pick targeting, weapon groups, death
|
||||
@@ -342,15 +352,21 @@ transition, HUD all landed since P6): console egg → mesh → RunningMission on
|
||||
defect that hid it for the whole effort was NOT the warp code — it was a general
|
||||
**texture-scroll precision-collapse bug** in `L4D3D::SetTextureScrolling` (offset = `scrollDelta ×
|
||||
absolute_time` → unbounded → UV precision loss → radial "spokes"), fixed with `fmodf`; that also
|
||||
cleans the scrolling beam grit. See [[reconstruction-gotchas]] §13. REMAINING: the AUTHENTIC peer
|
||||
path (see OTHERS' spheres) still needs player SimulationState/DropZoneLocation replication — until
|
||||
then the local-player sphere stands in; and confirm the COLLAPSE (death-side) warp fires on every
|
||||
MP death path (the force-damage test logged EXPAND reliably, COLLAPSE less so).
|
||||
cleans the scrolling beam grit. See [[reconstruction-gotchas]] §13. PEER WARP IS WIRED + VISIBLE
|
||||
(`160b78e`): an observer sees a peer's un-wreck warp via the world-anchored `BTStartWarpEffect`
|
||||
(mechdmg.cpp:1074, `ReplicantInstance`-gated; `simulationState` rides every record header).
|
||||
REMAINING [T3, non-gating]: the peer sphere is anchored to the peer's WORLD position, not the
|
||||
peer's authentic `DropZoneLocation` (that attribute isn't replicated) — a fidelity refinement.
|
||||
Also confirm the COLLAPSE (death-side) warp fires on every MP death path (force-damage test logged
|
||||
EXPAND reliably, COLLAPSE less so).
|
||||
NOTE the respawn-cycle SIM path is separate + done: `DropZoneReply` (FUN_004bffd0) → `Mech::Reset`
|
||||
(@0049fb74) + the level-2 "translocated" cockpit alarm; `CreatePlayerVehicle` (FUN_004bfcac,
|
||||
disassembled — make+link only, matches RP analog). The sphere is a pure RENDER-side watcher on the
|
||||
SimulationState dial, independent of that sim path.
|
||||
Mech::Reset (@0049fb74) full subsystem-reset sweep is still a bring-up TODO.
|
||||
Mech::Reset (@0049fb74) does the full subsystem-reset sweep (mech4.cpp:1616 loops every subsystem →
|
||||
`DeathReset(mode)`, restoring heat/power/ammo/charge) + heals all damage zones + ForceUpdate(0x1f).
|
||||
(Per-subsystem `DeathReset` bodies are authentically trivial/empty for some classes — the shipped
|
||||
build folds those overrides — so "fuller" reset behavior is per-subsystem, not a missing sweep.)
|
||||
**Three respawn bugs found + fixed on the way (all [T2], 2-node force-damage verified):**
|
||||
(i) **death-latch un-latch** — `IsMechDestroyed` tested `graphicAlarm>=9` alone; a later leg hit
|
||||
on the wreck rewrites the alarm to 4/3, un-latching → the death transition RE-RAN (double kill,
|
||||
@@ -393,3 +409,19 @@ transition, HUD all landed since P6): console egg → mesh → RunningMission on
|
||||
## Key Relationships
|
||||
- Base: [[wintesla-port]] (L4NET). Depends on: [[locomotion]] (update writer), [[combat-damage]]
|
||||
(entity lifecycle). Detail: `docs/HARD_PROBLEMS.md` (P6).
|
||||
|
||||
|
||||
## Authentic coupled peer motion — DONE (task #50, 2026-07-14) [T1]
|
||||
Decomp-verified (workflows w0odszxro/wh1h5gnmc, 3 make-or-break claims adversarially CONFIRMED; committed c52a1ad + default-promoted). The 1995 peer per-frame path FUN_004ab430 -> FUN_004ab1c8 (IntegrateMotion) drives motion SINGLE-SOURCE:
|
||||
- LINEAR position = GAIT. IntegrateMotion advances the BODY gait channel FUN_004a5678 (FUN_004a5028 is DEAD CODE, zero call sites), turns its cycleDistance into velocity `{0,0,-cd/dt}`, rotates by heading, integrates into projectedOrigin.linear@0x260 (part_012.c:14994/14997/15008); localOrigin.linear@0x100 is copied VERBATIM from it (FUN_00408440, part_012.c:15131 -- NOT a lerp). updateVelocity.linear is never used for position.
|
||||
- ANGULAR/heading = replicated-velocity slerp (the ONLY thing Mover::DeadReckon-style velocity extrapolation drives): projectedOrigin.angular@0x26c built from updateVelocity.angular@0x2d4 (FUN_004ab188), localOrigin.angular slerped toward it.
|
||||
- 0x260 aliasing: "motionDelta@0x260" and "projectedOrigin@0x260" are LITERALLY THE SAME field (base Mover::projectedOrigin, Origin 0x1c); "worldPose@0x26c" is its quaternion half. Our reconstruction's shadow names hid this and drove the false velocity-vs-gait "contradiction".
|
||||
- Re-anchor: motionEventVector@0x598 = updateOrigin - localOrigin captured on poseSyncLatch, DECAYED into localOrigin over ~_DAT_004ab9cc (~0.2s) -- no snap. Zeroed in idle states.
|
||||
- MASTER + PEER run the SAME predictor. The master's SEND-mirror (FUN_004a9b5c @0x4aab9c) advances projectedOrigin by IntegrateMotion(mj=0) fed the LAST-SENT bodyTargetSpeed, then deadbands |localOrigin-projectedOrigin|^2 vs UpdatePositionDiffrence@0x768. A gait-driven peer REQUIRES this gait mirror (a constant-velocity mirror under-sends -> tug-of-war, measured ratio 1.86).
|
||||
- T4 CLOSED: both leg(0x65c) and body(0x6bc) animators are AnimationInstances of the SAME JointedMover/JointSubsystem (JMOVER.cpp:1382) -- either poses the WHOLE skeleton. Our port keeps the peer on the LEG channel (body unbound on the peer); faithful functional equivalent.
|
||||
|
||||
RECONSTRUCTION STATE: coupled path is DEFAULT ON (mech4.cpp: s_drPos gait-coupled, s_gaitMirror/Send). Reverts: BT_DR_POS=1 (peer velocity), BT_NO_MASTER_GAITMIRROR (master const-vel mirror). Measured: single-source coupling backward-steps 0.1% (vs the two-source split's churn), position ratio 1.043. RESIDUAL (follow-up): MEASURED SMALL / effectively resolved (2026-07-15). With the peer now on the BODY channel (`96a896a`, matching the master's body-channel mirror) + affinity pinning, an accel/decel soak (node B `BT_AUTODRIVE`+`BT_DRIVE_SWEEP0` through STOP, node A `BT_SNAPLOG`) measures peer drift maxing at **~0.64u** (median 0.54u, 18 events >0.5u over 48s), gently absorbed at k~0.24/frame -- SUB-UNIT, not the earlier ~2.9u (which predated the body-channel swap AND was measured without CPU-affinity, i.e. with packet-jitter-sparse records). At the noise floor; no fix warranted. The old "run the master mirror on a leg-channel prediction" plan is moot now the peer is on the body channel. Still open (cosmetic): exact `_DAT_004ab9cc` decay constant; whether IntegrateMotion's 2-stage angular integrate is intended.
|
||||
|
||||
|
||||
## Peer motion: the "random shakiness" is single-box packet jitter, NOT the game (task #50, 2026-07-15) [T2]
|
||||
After the coupled body-channel peer landed (96a896a + turn-step f094d78 + cadence-mirror 23f1532), residual peer shakiness on accel/decel was RANDOM -- sometimes perfect, sometimes shaky. Root cause is the TEST RIG, proven with BT_RXJIT (record inter-arrival ms on the peer): two Debug btl4 nodes on ONE box contend for CPU, so Windows BATCHES their TCP delivery -- update records arrive in bursts (min ~0ms back-to-back, max 56-226ms gaps, burstiness max/avg 3-7x) instead of an even ~17ms. A peer dead-reckons across the long gaps then snaps when a record lands -> random shake. Pinning each node to a DISJOINT core set (ProcessorAffinity 0x00F / 0x3C0) restored even ~17ms delivery (burstiness ~1.0) and the shakiness vanished (user-confirmed). REAL pods are dedicated machines with no contention -- they never see this. DO NOT add an interpolation/jitter buffer or other un-authentic netcode to mask a single-box artifact. Use tools/mp_launch.sh (bakes in the affinity) for all 2-node tests. Diagnostic env: BT_RXJIT (arrival jitter), BT_SLIDE ([slide]/[mslide] slide-in-stand), BT_GAITEV, BT_MIRDIV, BT_NO_MIRROR_CAD / BT_PEER_LEGCH (revert the coupled-peer fixes).
|
||||
+104
-33
@@ -20,6 +20,14 @@ authentic path scoped.
|
||||
`BTL4.RES` already has 8 maps + the full mech anim set.) Status: OPEN.
|
||||
- Pod specifics for Phase 8: the 7-monitor driver setup, the RIO cockpit I/O protocol, current
|
||||
Win10+wrapper pod config. Status: OPEN.
|
||||
- **Did the pod throttle quadrant have MECHANICAL detents** (how many / lever positions)? The
|
||||
software path is notch-free [T1, [[pod-hardware]]], but the gait SM's walk/run dead band
|
||||
([[locomotion]]) only makes sense if the hardware discouraged parking the lever mid-band —
|
||||
"5 speeds" pod lore suggests physical notches. Status: OPEN.
|
||||
- ~~Which mechs used rear fire~~ **ANSWERED from data (2026-07-16, task #68)**: the Blackhawk's
|
||||
ERMLaser_2/3 (back gun ports) are the only rear weapons in the roster; see [[combat-damage]].
|
||||
Remaining: bind the side/down look views to keys (only 'V' look-back is bound); pod look-button
|
||||
mappings for Phase 8.
|
||||
|
||||
## BINARY-COVERAGE AUDIT discoveries (2026-07-13, 6-agent decomp census) [T1 verified-uncited]
|
||||
New unaccounted functionality no prior list knew (addresses verified absent from game/+context/+docs):
|
||||
@@ -154,6 +162,17 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
|
||||
centred); (c) pre-burial, the pick still
|
||||
tests the sinking wreck's collision box at its parked position (pre-existing wreck-targeting
|
||||
behavior).
|
||||
- **✅ WORLD STRUCTURES (garages/walls) now targetable (task #50, 2026-07-15)** — the world-pick's
|
||||
non-mech tier previously only sampled the visual heightfield (`BTGroundRayHit`), which on arena1
|
||||
is a single flat `'sky'` ground mesh → shots passed THROUGH the garages and only mechs moved the
|
||||
range axis (user-reported regression). Resolved by ray-testing the **zone's static collision
|
||||
solid tree** (the geometry that already blocks the walk) via the authentic engine query
|
||||
`Mover::FindBoxedSolidHitBy`'s static-world tail — factored into `Mover::FindStaticSolidHitBy` +
|
||||
`Mech::WorldStructurePick`. See [[combat-damage]] "WORLD STRUCTURES ARE TARGETABLE". This also
|
||||
closes the old "0x388 writer is un-exported / unknown world-pick mechanism" residue: the
|
||||
authentic non-mech world pick is the static-solid-tree ray query (`BoxedSolidTree::FindBoundingBoxHitBy`),
|
||||
the same one `FindBoxedSolidHitBy` uses for mover-vs-world collision. Interactive aim is
|
||||
user-verified (headless has no `BTGetAimRay`); the `BT_WSWEEP` ray-fan is the headless proof.
|
||||
- **Cockpit HUD leftovers (tasks #35-#38 residue, 2026-07-08)** — the reticle + 7 weapon pips +
|
||||
ALL the instrument dynamics are LIVE (the Execute @004cdcf0 gap was RECOVERED via capstone
|
||||
disasm, task #37; the authentic Lock producer + the simple-X mode landed in #38 — see
|
||||
@@ -201,8 +220,9 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
|
||||
Also: `deathAnimationLatched`/`legResetLatch` were never ctor-initialised (0xCDCDCDCD gate bug,
|
||||
fixed in mech.cpp ctor). See [[cockpit-view]].
|
||||
- **MechControlsMapper look/eyepoint commit** — reconstructed but its offsets collide with declared
|
||||
members; arbitrate before enabling (no port consumer yet). The fire-trigger-through-mapper is
|
||||
still the `gBTWeaponTrigger` bring-up.
|
||||
members; arbitrate before enabling (no port consumer yet). (The fire-trigger-through-mapper is NO
|
||||
LONGER the bring-up: the `gBT*Trigger` globals are retired / never-read — the authentic LBE4
|
||||
buttonGroup path replaced them, task #5 `8ed6184`.)
|
||||
- **✅ Weapon groups — the AUTHENTIC defaults + fire path LIVE (task #5, 2026-07-11) [T1/T2].**
|
||||
The system decoded end-to-end: a fire "channel" = a physical `buttonGroup` slot in the global
|
||||
`LBE4ControlsManager` (fire buttons 0x40 Trigger / 0x45 Pinky / 0x46 ThumbLow / 0x47 ThumbHigh);
|
||||
@@ -355,8 +375,12 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
|
||||
three coolant epsilon corrections (0.0025/0.003/1e-4 for the old single 1e-4) +
|
||||
CoolantCapacityScale = 0.05 float80 (both prior readings wrong). Verified: heat flows,
|
||||
exits (~2/3 of a 1e10 spam run drained), weapons overheat under max-rate autofire exactly
|
||||
as the authored-constant simulation predicts (thermal spam is unsustainable BY DESIGN;
|
||||
heatLoad -> effectiveRange 0 = the authentic overheat cutout).
|
||||
as the authored-constant simulation predicts (thermal spam is unsustainable BY DESIGN; the
|
||||
authentic overheat cutout is the FailureHeat CHARGE-HOLD -- `ResetFiringState` + currentLevel=0
|
||||
until it cools, @004baa88 -- NOT a range collapse. ⚠ CORRECTION (`aab7a8a`, task #50):
|
||||
effectiveRange = (1 - HOST-ZONE damage) x weaponRange reads `Subsystem::damageZone->damageLevel`
|
||||
(weapon+0xE0), NOT heatLoad; the old "heatLoad -> effectiveRange 0" mechanism here was the same
|
||||
@0xE0-DamageZone-vs-heat misattribution fixed in heat.cpp:803).
|
||||
- **Task #10 -- the "scrambled routing" RESOLVED (2026-07-11) [T2].** The routing was NEVER
|
||||
scrambled: a `[heat-link]` attach log proved every subsystem links its authored sink
|
||||
exactly (bhk1: PPC_1->C4, PPC_2->C6, ERM_1/2/3->C1/C6/C4, SRM6s->C2, generators->C1/C2/C3/C5,
|
||||
@@ -413,32 +437,32 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
|
||||
under reconstruction: config-mode regrouping (task #6), generator reassignment (ids 4-8),
|
||||
coolant valves.
|
||||
|
||||
- **AUTOCANNON JAM / coolant-priority — mechanism FAITHFUL + COMPLETE; a CALIBRATION question
|
||||
pending QA ground-truth (2026-07-14) [T1 mechanism / T3 pacing].** An original QA dev reports:
|
||||
"all AFCs jam on their 3rd shot if coolant priority isn't boosted; boosted → almost never jam."
|
||||
Full decomp hunt (2 agents) confirms the whole chain is reconstructed end-to-end and the port is
|
||||
FAITHFUL — **do NOT "fix" it by driving a weapon's `coolantFlowScale` from the valve; that field
|
||||
being a constant 1.0 for weapons is AUTHENTIC** (the ONLY writer of any HeatSink `+0x15C` from a
|
||||
valve value is `RecomputeCondenserValves`/FUN_0049f788, which iterates ONLY the condenser chain
|
||||
`mech+0x7cc`). Authentic mechanism: a weapon dumps `heatCostToFire` (AFC100=6.5e7 raw) into its
|
||||
OWN linked condenser; the condenser sheds to the shared bank at a rate scaled by its valve SHARE
|
||||
(`coolantFlowScale = valveState_i / Σ valveState`, MoveValve msg id 4, detents 1/5/50/0). Boosting
|
||||
ONE condenser's valve (share ~0.17→~0.9 = ~5× conductance) drops its equilibrium temp so the
|
||||
weapon dumping into it stays below `degradationTemperature`(1000) → `heatAlarm`=0 → CheckForJam
|
||||
early-returns → no jam. Starved → condenser saturates → weapon crosses 1000 → the STICKY
|
||||
probabilistic jam (`p=clamp(0.41·T/failT,0.05,1)`, JammedState 5, clears only on ResetToInitialState)
|
||||
latches. Two distinct "jam" states: **degradation 1000 → probabilistic sticky jam** (the pilot's
|
||||
"jam"); **failure 2000 → deterministic self-clearing overheat lockout** (recoil-pinned + weaponAlarm 7,
|
||||
re-fires each frame until it cools). ALL thermal constants load strictly from the resource (no
|
||||
stand-in fallback — a missing value ERRORS). **Empirical (BT_JAM_LOG/BT_VALVE, madcat AFC100):**
|
||||
uncooled the weapon crosses 1000 at ~shot 4 and PLATEAUS ~1400 (conduction balances input), giving
|
||||
~25% probabilistic jam — directionally right but NOT the QA's deterministic "3rd shot". Gap is
|
||||
calibration (condenser thermalMass/conductance live in LAST.EGG, unread) + scenario (fire rate;
|
||||
which condenser boosted — a uniform BT_VALVE is normalized away, need a targeted per-condenser
|
||||
boost). NEXT: get QA to clarify (a) deterministic lockout vs probabilistic jam, (b) per-weapon vs
|
||||
global coolant boost + which control; THEN verify calibration — do NOT tune the heat economy to
|
||||
force "3rd shot" without ground-truth (would be un-faithful). Diag probes committed: `BT_JAM_LOG`
|
||||
(projweap.cpp), `BT_VALVE` (heatfamily_reslice.cpp). See [[combat-damage]], [[subsystems]] heat.
|
||||
- **✅ AUTOCANNON JAM / coolant-priority — FAITHFUL + CONFIRMED against QA ground-truth, SOLVED
|
||||
purely from the decomp (2026-07-14) [T1/T2].** An original QA dev reported: "all AFCs jam on their
|
||||
3rd shot if coolant priority isn't boosted; boosted → almost never jam." VERDICT: the whole chain
|
||||
is reconstructed end-to-end, FAITHFUL, and reproduces the reported behavior — no bug, no stand-in.
|
||||
**DO NOT "fix" it by driving a weapon's `coolantFlowScale` from the valve; that field being a
|
||||
constant 1.0 for weapons is AUTHENTIC** (the ONLY writer of any HeatSink `+0x15C` from a valve value
|
||||
is `RecomputeCondenserValves`/FUN_0049f788, which iterates ONLY the condenser chain `mech+0x7cc`).
|
||||
Mechanism: a weapon dumps `heatCostToFire` (AFC100=6.5e7 raw) into its OWN linked condenser (AFC100→
|
||||
Condenser4); the condenser sheds to the shared bank at a rate scaled by its valve SHARE
|
||||
(`coolantFlowScale = valveState_i / Σ valveState`, MoveValve msg id 4, detents 1/5/50/0). The
|
||||
weapon's OWN thermal conductance (35500) caps how fast it sheds into its condenser, so coolant
|
||||
priority shifts the weapon's equilibrium only ~100 K — but that is EXACTLY enough to straddle the
|
||||
`degradationTemperature`(1000) jam line at combat fire rates. Cross `1000` → `heatAlarm`≥1 → the
|
||||
jam roll arms (`p=clamp(0.41·T/failT, 0.05, 1)`); on a hit → sticky `JammedState`(5), clears only on
|
||||
ResetToInitialState. (Distinct: `failureTemperature`(2000) → self-clearing overheat lockout,
|
||||
recoil-pinned + weaponAlarm 7.) **ALL constants VERIFIED AUTHENTIC vs the raw BTL4.RES bytes**
|
||||
(`scratchpad/afc_dump.py`): AFC100 startT=77, degradT=1000, failT=2000, thermalConductance=35500,
|
||||
thermalMass=94700, heatCostToFire(+0x1A4)=6.5e7, recharge(+0x190)=8, damage=25, minJamChance=0.05 —
|
||||
no mis-scaling. **THE ANSWER IS FIRE-RATE-DEPENDENT** (measured, BT_JAM_LOG/BT_VALVE_BOOST/BT_AF_PERIOD,
|
||||
madcat AFC100 → Condenser4): at MAX spam (8s recharge) the AFC overheats regardless (~1460 boosted
|
||||
~1360 — both jam); at a SLOW cadence (16s) it plateaus ~770 and never jams; at the ~11s COMBAT cadence
|
||||
it is DECISIVE — **not boosted → 1058 K (jams), boosted (Condenser4 valve→50, others→1) → 953 K (never
|
||||
jams)**. So the QA is right and the reconstruction is faithful; the earlier "boost is weak" read was
|
||||
an artifact of testing at max spam (above the window). Diag probes committed: `BT_JAM_LOG`
|
||||
(projweap.cpp), `BT_VALVE`/`BT_VALVE_BOOST=<condenserNum>` (heatfamily_reslice.cpp), `BT_AF_PERIOD=<sec>`
|
||||
(mech4.cpp). See [[combat-damage]], [[subsystems]] heat.
|
||||
|
||||
- **Subsystem-panel online/offline gate — fields unidentified [T4] (2026-07-12).** The binary's
|
||||
SubsystemCluster draw-state reads `*(subsystem+0x40)==1` and `*(subsystem+0x278)!=4`; neither
|
||||
@@ -536,11 +560,30 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
|
||||
ctor per mech, zone destroyed → crits damaged/DESTROYED with authentic accumulation.
|
||||
Residue: roster idx 0/1 (mapper installed post-ctor / voltage-bus stub) stay unbound with a
|
||||
logged skip — none of the 8 mechs' streams crit-list them in practice.
|
||||
- **Two dead Wword comparison branches (multiplayer)**: `mech.cpp:1511` + `:1613` (replicant
|
||||
leg-state/stability sync in ReadUpdateRecord) — always-false BTVal comparisons ([[reconstruction-gotchas]] §2);
|
||||
needs slots 0xf/0x10 promoted to named members when P6 replication work resumes.
|
||||
- **✅ Two dead Wword comparison branches — DONE (task #1, `c9f0c2a`).** `Mech::ReadUpdateRecord`
|
||||
case 3 now reads real named fields off `Mech__StateUpdateRecord` (legState / stability / legResetLatch /
|
||||
speedDemand); the always-false Wword/BTVal branches are gone (the old `mech.cpp:1511`/`:1613` line
|
||||
numbers no longer point at that code).
|
||||
|
||||
## Multiplayer (Phase 7 / P6)
|
||||
- **✅ Console-death peer-replication freeze — NOT REPRODUCIBLE + the one real bug FIXED
|
||||
(2026-07-15) [T2].** The 2026-07-14 report (btconsole.py died mid-match → peer↔peer replication
|
||||
froze both ways) did NOT reproduce in the current build under EITHER failure mode, tested directly
|
||||
(BT_REPL_LOG on a circling peer, affinity-pinned): (1) a clean relay kill — the replicant kept
|
||||
circling smoothly through the kill, NO disconnect logged, node survived; (2) a deliberately STUCK
|
||||
console (`scratchpad/btconsole_stuck.py` — connects, starts the mission, then stops recv()'ing while
|
||||
holding the socket OPEN = the exact "receive pad full → close never seen" mode hypothesized) — the
|
||||
replicant kept moving the entire run. So **peer replication is INDEPENDENT of the console**: the
|
||||
pods replicate peer-to-peer over the GAME socket; the console is a separate egg/mission/status
|
||||
channel. The original freeze was a transient (plausibly the same single-box packet-jitter /
|
||||
CPU-contention artifact root-caused 2026-07-15, `49d73dc`) or was fixed by later MP work.
|
||||
**The flagged latent bug IS real and is now FIXED:** `HostDisconnectedMessageHandler`'s
|
||||
ConsoleHostType branch closed **`gameListenerSocket`** (the GAME listener) on a CONSOLE disconnect —
|
||||
a naming bug; the comment + the commented-out `OpenConnection` intended a CONSOLE re-listen, which
|
||||
`CreateConsoleHost()` already does. Removed (L4NET.CPP:969). Harmless to established peer sockets
|
||||
(why a live match keeps replicating through console loss), but it would have blocked a NEW peer
|
||||
from joining after a console cycle. Verified: 2-node still connects (`All connections completed!`)
|
||||
+ the peer circles. Diag harness retained: `scratchpad/btconsole_stuck.py`.
|
||||
- ✅ Cross-pod COMBAT — DONE (tasks #46/#47: replicant targeting + damage rerouted to the owning
|
||||
master, full cross-pod kill verified). ✅ Replicant GAIT animation — DONE (task #50: replicant
|
||||
derives speedDemand from the replicated velocity, full stand→walk→run lifecycle verified).
|
||||
@@ -597,3 +640,31 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
|
||||
## Key Relationships
|
||||
- Feeds from: every subsystem/render topic (their deferral notes collect here).
|
||||
- Gating master: [[source-completeness]] (the missing BT source).
|
||||
|
||||
## Peer coupled-motion architecture — ✅ DONE (task #50, 2026-07-14/15) [T1/T2]
|
||||
RESOLVED. (This block was the old COMPLETION PLAN from `ea39af1`; it was left stale after
|
||||
the work actually landed — reconciled 2026-07-15.) The coupled single-source gait pipeline
|
||||
is reconstructed, DEFAULT ON, and the residual was root-caused. See [[multiplayer]] two
|
||||
sections: **"Authentic coupled peer motion — DONE"** (linear position = BODY gait channel
|
||||
integrated into `projectedOrigin@0x260`; angular = replicated-velocity slerp; master
|
||||
SEND-mirror runs the SAME `IntegrateMotion` predictor fed the last-sent `bodyTargetSpeed`)
|
||||
and **"Peer motion: the 'random shakiness' is single-box packet jitter, NOT the game"**.
|
||||
- Landed: coupled path promoted to default (`c52a1ad` + `b013742`); peer poses from the
|
||||
BODY channel (`96a896a`); turn-step on the body channel (`f094d78`); true-mirror cadence
|
||||
driving the clip from actual replicated motion (`23f1532`). Groundwork: exact quaternion
|
||||
integration (`a8eb8a4`), incremental peer heading (`d78e77b`), clock guard (`93456be`),
|
||||
commanded-speed cadence feed (`6374efc`), Abs-macro fix (`7615ecd`).
|
||||
- **The old "needs the MASTER HALF" conclusion was SUPERSEDED:** the leftover accel/decel
|
||||
shakiness was ROOT-CAUSED as single-box **packet jitter** (`49d73dc`) — two Debug nodes on
|
||||
one box batch TCP delivery, so records arrive bursty (BT_RXJIT burstiness 3-7x) and the peer
|
||||
dead-reckons across the gaps then snaps. Pinning each node to a disjoint core set
|
||||
(`ProcessorAffinity 0x00F/0x3C0`, baked into `tools/mp_launch.sh`) restored even ~17ms
|
||||
delivery and the shakiness vanished (user-confirmed). Real pods are dedicated machines and
|
||||
never see it. **DO NOT add an interpolation/jitter buffer** to mask a rig artifact.
|
||||
- Minor non-gating follow-ups (tracked in [[multiplayer]]): the peer accel/decel drift
|
||||
(flagged ~2.9u, but **MEASURED ~0.64u max** on the affinity rig with the peer on the body
|
||||
channel -- `BT_SNAPLOG` + `BT_DRIVE_SWEEP0`, 2026-07-15; sub-unit, effectively resolved,
|
||||
no fix warranted); the exact `_DAT_004ab9cc` offset-decay constant; whether
|
||||
`IntegrateMotion`'s 2-stage angular integrate is intended.
|
||||
- Speed model decomp-settled (`2c6db6a`): analog throttle, '5 speeds' false, `throttleState@0x4a4`
|
||||
is actually the fall-surface material (RENAME PENDING).
|
||||
|
||||
@@ -25,6 +25,49 @@ to keyboard/gamepad on a dev box; real wiring is a pod bring-up task (Phase 8).
|
||||
model — `LBE4ControlsManager` groups are fed by all devices (RIO on the pod, DirectInput on dev);
|
||||
the `MechControlsMapper` interprets them ([[locomotion]]). [T2]
|
||||
|
||||
### The pod throttle is ANALOG-CONTINUOUS, not notched (verified end-to-end) [T1]
|
||||
The authentic pod throttle path — traced 2026-07 (task #50 throttle-fidelity question):
|
||||
1. **Hardware → RIO:** serial `AnalogReply` packet → `Ranger("Throttle", 0, 800, .05)` — raw counts
|
||||
0–800, 5% deadband, auto-ranging offset, output a CONTINUOUS Scalar; the sign is inverted
|
||||
("Throttle counts BACKWARDS", `engine/MUNGA_L4/L4RIO.cpp:1374-1377`, Ranger @L4RIO.cpp:461-701).
|
||||
No quantization/notching anywhere in `Ranger::Update`.
|
||||
2. **Manager:** `LBE4ControlsManager::Execute` → `scalarGroup[ScalarThrottle].Update(&rioPointer->
|
||||
Throttle, mode_mask)` on every AnalogReply (`L4CTRL.cpp:1379-1382`). ScalarThrottle = index 0 →
|
||||
manager+0x24 (scalarGroup base 0x24, 0x20/entry; buttonGroup base 0x1c0, keyboardGroup 0x160).
|
||||
3. **Streamed `.CTL` mapping (the "handled elsewhere"):** `MechRIOMapper`'s ctor @004d266c binds NO
|
||||
throttle — the bind comes from the type-19 `ControlMappingStream` resource named **"L4"** (child
|
||||
of the per-mech type-6 `ControlMappingsList`; installed by `BTL4APP MakeViewpointEntity` via
|
||||
`CreateStreamedMappings` @0047703c). BTL4.RES "L4" record [1]:
|
||||
`Scalar Throttle → subsystemID 0 (ControlsMapper slot), DirectMapping, attr 4, mask 0xffffffff`.
|
||||
Attr 4 = "ThrottlePosition" @ mapper+0x11c (binary IndexEntry table @0050efd8: id 4 → offset
|
||||
0x11d-1 = 0x11c, name "ThrottlePosition" @0050f28f). Reverse = record [2]: `Button Throttle1
|
||||
(0x3F, on the throttle handle) → attr 6 ReverseThrust@0x124`. Turn = pedals value-bound in the
|
||||
ctor (manager+0x44/+0x64 → mapper+0x1b4/+0x1b8).
|
||||
4. **Interpretation:** `L4MechControlsMapper::InterpretControls` @004d196c applies the ONLY software
|
||||
detent — snap to 1.0 when |t−1.0| ≤ 0.05 — then `MechControlsMapper::InterpretControls` @004afd10
|
||||
computes `speedDemand@0x128 = maxSpeed(mech+0x34c) × throttlePosition(0x11c) × scale(mech+0x5c0)`
|
||||
(forward) or `−maxSpeed × throttlePosition` (reverse flag 0x124).
|
||||
|
||||
**Consequence:** the original pod produced a continuously varying speedDemand while the lever moved
|
||||
(updated per serial AnalogReply, NOT per render frame). There are NO "5 throttle notches" anywhere
|
||||
in the software path. (Keyboard keys '1'-'5' in @004d1bf0 set controls-manager MODE masks; the
|
||||
'+'/'-' pair steps a [0,5] value that drives `pow(2,x)` into mech+0x404 = HUD zoom 1×–32× — neither
|
||||
is a speed setting.) The mech's `throttleState@0x4a4` writer remains un-exported (likely in the
|
||||
0x4a9b5a–0x4ab188 gap) — [[open-questions]].
|
||||
|
||||
**MECHANICAL-notch hypothesis (2026-07-16) [T4, hardware]:** the "5 speeds" pod lore may still be
|
||||
true — as detents in the throttle QUADRANT hardware, invisible to the software (the pot reads
|
||||
continuous counts regardless of where the lever mechanically rests). Independent corroboration
|
||||
from the gait math ([[locomotion]]): the gait SM has NO stable state for a demand between the walk
|
||||
cap (`walkStrideLength`@0x534) and the run engage speed (`reverseSpeedMax`@0x538, the walk→run
|
||||
transition clip's exit speed) — a demand PARKED in that band hunts walk→shift-up→shift-down
|
||||
forever, firing the authored EngineShiftFwd/Rev sounds each swing (binary-verified, all-authentic
|
||||
data: Blackhawk band = demand 22.02–30.87 = throttle 36–50%). A design like that only ships if the
|
||||
hardware discourages parking in the band. Port accommodation: the keyboard lever snaps out of the
|
||||
dead band AT REST (`mech4.cpp` "GAIT DETENT"; sweeping through while held stays continuous =
|
||||
authentic moving lever). Ask Nick: did the pod throttle quadrant have mechanical detents
|
||||
(how many / positions)? — [[open-questions]].
|
||||
|
||||
## Multi-surface gauge path (intact, pod-only by default)
|
||||
The pod multi-surface path EXISTS and is intact: `DPLRenderer::FindBestAdapterIndices` (multi-
|
||||
adapter selector, honors PRIMGAUGE/SECGAUGE/MFDGAUGE/SPANDISABLE), `SVGA16::BuildWindows` (a
|
||||
|
||||
@@ -42,7 +42,11 @@ fire → damage → destroy → respawn** single-player loop (task #52). **2-nod
|
||||
end-to-end** — replication, cross-pod targeting/damage/kill, beam visuals, replicant gait (tasks
|
||||
#46-#51, [[multiplayer]]). All 8 cockpit canopies are authentic + the horizontal-FOV fix (task #55,
|
||||
[[cockpit-view]]); the Gyroscope is live byte-exact with hit-bounce (task #56); the **gauge system
|
||||
is complete** ([[gauges-hud]]). The engine/renderer/HAL/audio are done (WinTesla); the active work
|
||||
is complete** ([[gauges-hud]]). The engine/renderer/HAL are done (WinTesla); AUDIO backend is now REAL
|
||||
(the repo's OpenAL32.dll + libsndfile-1.dll were both no-op STUBS — replaced with real OpenAL Soft +
|
||||
an in-tree WAV loader, 2026-07-15) and the 241-sample soundbank is cracked from AUDIO1/2.RES + loading;
|
||||
the ONLY remaining audio gap is game-triggering (no AudioEntities fire PlayNote yet), so gameplay is
|
||||
still silent apart from a proof-of-life hook (see [[wintesla-port]] Audio). The active work
|
||||
is reconstructing each BT subsystem's authentic behavior from the binary. Remaining: pod-LAN
|
||||
config, Mech-level update records, per-subsystem waves. [T2]
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ source_sections: "PROGRESS_LOG.md §10 (render-fidelity notes); docs/BGF_FORMAT.
|
||||
related_topics: [bgf-format, asset-formats, wintesla-port, locomotion, pod-hardware, cockpit-view]
|
||||
key_terms: [L4D3D, material-ramp, BSL, LOD, DPL, beam]
|
||||
open_questions:
|
||||
- "Replacement-LOD for NON-additive multi-LOD models; day/night material path priority (BT_MATPRI)"
|
||||
- "Replacement-LOD for NON-additive multi-LOD models"
|
||||
---
|
||||
|
||||
# Rendering
|
||||
|
||||
@@ -20,8 +20,31 @@ grouped roughly: toolchain → PadRIO input → MFD-split cockpit → front end/
|
||||
→ NetTransport/Steam sockets → lobby → bindings/KeyLight → packaging.
|
||||
|
||||
**Repo rule [T0]:** `origin` = `VWE/BT412.git` only. This repo must never touch the
|
||||
BT411 remote (no cross-pull remote — deliberate difference from RP412's `rp411`
|
||||
pattern). Forked at BT411 `4e72f0c` (2026-07-14).
|
||||
BT411 **remote** (the gitea URL) — no remote, no fetch/push to it. Forked at BT411
|
||||
`4e72f0c` (2026-07-14). *Bringing BT411 work forward is allowed via a **local-path**
|
||||
merge (`git fetch C:/vwe/BT411 master`) — that never contacts the gitea BT411 remote;
|
||||
see the merge note below.*
|
||||
|
||||
## BT411 catch-up merge (2026-07-14)
|
||||
|
||||
Merged BT411's post-fork line (`4e72f0c..abed41e`, 91 commits) into BT412 via a
|
||||
**local-path** merge — brought the whole **audio-fidelity system** (engine `AUD*` +
|
||||
`L4AUD*` + `audiopresets.cpp` + ~600 content `.wav`s, `docs/AUDIO_FIDELITY.md`),
|
||||
missiles/rear-fire/HUD/gyro/gait tasks (#66–68), `FOGDAY.EGG`, and refreshed context
|
||||
docs. Only **5 files** overlapped the steamification; resolutions:
|
||||
- `.gitignore`, `CMakeLists.txt`, `mech4.cpp` — trivial / auto-merged (the
|
||||
`deviceOwnsInput` gating survived).
|
||||
- `L4NET.CPP` — took BT411's task-#50 fix (don't close the game listener on console
|
||||
loss) over the seam's adaptation of the old buggy close; the point-to-point send
|
||||
stays on `NetTransport_Get()->Connect()`.
|
||||
- `L4NETTRANSPORT.cpp` — folded BT411's **TCP_NODELAY** latency fix into
|
||||
`WinsockNetTransport::Connect` (the seam already had BT411's retry-while-refused +
|
||||
non-blocking). Accept-side game-socket NODELAY stayed at the L4NET call site.
|
||||
- `mechmppr.cpp` — combined the `device_owns_input` gating with BT411's task-#68
|
||||
look-behind ('V' rear-view), gating the `lookBehind` write too (a live RIO device
|
||||
feeds it through its mapped button).
|
||||
Verified after merge: clean build; solo front-end mode; loopback MP through the seam
|
||||
(mesh completes, both tick, replication, no NODELAY warnings). [T2]
|
||||
|
||||
## Status ledger
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ source_sections: "PROGRESS_LOG.md §10d; docs/SUBSYS_PLAN.md; docs/VEHICLE_SUBSY
|
||||
related_topics: [decomp-reference, reconstruction-gotchas, combat-damage, gauges-hud]
|
||||
key_terms: [subsystem, factory, ClassID, subsystem-roster, PoweredSubsystem, bridge]
|
||||
open_questions:
|
||||
- "Myomers coupling (gates on owning-player @mech+0x190); 0xBD3 damage-hub untangle (WAVE 8)"
|
||||
- "Myomers coupling (gates on owning-player @mech+0x190); factory capability-roster loops 2-4 still dead (SubProxy stub)"
|
||||
---
|
||||
|
||||
# Subsystems
|
||||
@@ -54,11 +54,11 @@ Making a base byte-exact GROWS every subclass — they must be re-based TOGETHER
|
||||
- **TASK #56 (2026-07-11)** — Gyroscope LIVE byte-exact (ctor @004b3778, sizeof 0x3D0 locked;
|
||||
integrators + `FUN_004b2980` damage fan-out; joint dispatch from the Mech performance tail
|
||||
@0x4aaf74/83; `BT_GYRO_LOG`/`BT_GYRO_TRACE`) — see [[cockpit-view]].
|
||||
- **DEFERRED** — SubsystemMessageManager 0xBD3 (WAVE 8 — a
|
||||
**damage/explosion consolidation hub** cached to `Mech[0x10d]`=0x434; recon MISLABELS the slot
|
||||
`controlsMapper` + the drive squats there while the real mapper is roster slot 0, so wiring it needs
|
||||
an untangle. NOT the valve/advanced-damage gate — that's the owning **BTPlayer** @mech+0x190, see
|
||||
[[combat-damage]]/[[open-questions]]). [T2]
|
||||
- **✅ DONE (task #7, `afefaee`)** — SubsystemMessageManager 0xBD3 (a
|
||||
**damage/explosion consolidation hub** cached to `Mech[0x10d]`=0x434; the factory builds the REAL
|
||||
class and `mech.hpp` names the slot `messageManager` — the old `controlsMapper` mislabel is swept,
|
||||
the real mapper is roster slot 0. NOT the valve/advanced-damage gate — that's the owning **BTPlayer**
|
||||
@mech+0x190, see [[combat-damage]]/[[open-questions]]). [T2]
|
||||
|
||||
## The watcher electrical chain (task #57, 2026-07-13 — the torso power gate)
|
||||
The Watcher branch is POWERED indirectly: a watcher WATCHES another roster subsystem and mirrors
|
||||
|
||||
@@ -6,7 +6,7 @@ source_sections: "multiplayer.md (lifecycle + trigger wiring, task #52); referen
|
||||
related_topics: [rendering, multiplayer, combat-damage, reconstruction-gotchas, asset-formats, bgf-format]
|
||||
key_terms: [material-ramp]
|
||||
open_questions:
|
||||
- "AUTHENTIC peer path (seeing OTHERS' spheres) needs player SimulationState/DropZoneLocation replication; until then the LOCAL-player sphere stands in."
|
||||
- "Peer warp IS wired + visible -- observer sees a peer's un-wreck warp (world-anchored BTStartWarpEffect, 160b78e). Remaining nuance [T3]: anchor to the peer's authentic DropZoneLocation (not replicated) vs the peer's world position."
|
||||
- "In the 2-node force-damage test only the EXPAND (respawn) warp logs reliably; confirm the COLLAPSE (death-side) fires on every MP death path."
|
||||
---
|
||||
|
||||
@@ -118,9 +118,13 @@ it impossible to ignore. Generalized in [[reconstruction-gotchas]] §13.
|
||||
## Trigger wiring (summary; full detail in [[multiplayer]]) [T2]
|
||||
Local player's own death → `BTStartWarpCollapsePOV()` (btplayer.cpp `VehicleDeadMessageHandler`,
|
||||
`deathCount==-1`, guarded `this==GetMissionPlayer()`). Respawn `DropZoneReply` → `BTStartWarpExpandPOV()`
|
||||
(local-guarded). `BTWarpForceUnmask()` on mission-end / no-DropZones. Observer replicant un-wreck fires
|
||||
the world-anchored `BTStartWarpEffect(x,y,z)` (mechdmg.cpp). The peer path is a stand-in until player
|
||||
`SimulationState`/`DropZoneLocation` replication exists.
|
||||
(local-guarded). `BTWarpForceUnmask()` on mission-end / no-DropZones. **Peer warp IS wired + visible
|
||||
(`160b78e`):** an observer sees a peer's death/respawn warp — the replicant un-wreck fires the
|
||||
world-anchored `BTStartWarpEffect(x,y,z)` at the peer's position (mechdmg.cpp:1074, gated to
|
||||
`ReplicantInstance`; `simulationState` rides every update-record header so the observer tracks the
|
||||
peer's state). The ONLY remaining nuance [T3, non-gating]: the observer's peer sphere is anchored to
|
||||
the peer's WORLD position rather than the peer's authentic `DropZoneLocation` (that attribute isn't
|
||||
replicated) — a fidelity refinement, not a missing effect.
|
||||
|
||||
## How it was solved (method) + verification [T2]
|
||||
1. **Dump the real asset** (`dump_tsphere.py`) — established it's a bicone with cylindrical UVs, ending
|
||||
|
||||
+111
-1
@@ -16,7 +16,117 @@ on top of it. Full detail: `docs/PROGRESS_LOG.md §5b, §8`.
|
||||
## What WinTesla already did (do NOT rebuild)
|
||||
- **Renderer bypass: DONE** — `MUNGA_L4/L4D3D.cpp` + `DXUtils` replace libDPL / the IG board with
|
||||
Direct3D9. No `libdpl.lib` in the tree. (The early from-scratch D3D9 viewer was retired and removed.) [T1]
|
||||
- **Audio: DONE** — OpenAL + libsndfile replacing the HMI "SOS" engine. [T1]
|
||||
- **Audio: BACKEND now REAL + soundbank loaded; only game-triggering remains** [T2, updated
|
||||
2026-07-15]. OpenAL replaces the HMI "SOS" engine and the renderer→device→buffer→source→play chain
|
||||
IS all implemented (`L4AUDRND`/`L4AUDRES`/`L4AUDLVL`; `PatchLevelOfDetail::PlayNote` really calls
|
||||
`alSourcePlay`). The "no sound, ever" root cause was DEEPER than a gate — **both audio backend DLLs
|
||||
shipped in `engine/lib/` were no-op STUBS**: (a) `libsndfile-1.dll` exports 15 funcs BY ORDINAL
|
||||
ONLY (no names), `sf_open()` always returns NULL; (b) `OpenAL32.dll` (72KB) imported **only
|
||||
KERNEL32** — no dsound/wasapi/winmm — so it returned fake handles (`ctx=0x00000001`,
|
||||
`alGenSources→0`) and never touched the hardware. So the chain ran clean and silent. FIXES (commit
|
||||
5b46655): **OpenAL32.dll → real OpenAL Soft 1.25.2 Win32** (drop-in: the exe imports the 25 AL funcs
|
||||
by NAME; `alSourcePlay` now reaches `AL_PLAYING`). **libsndfile DROPPED** — replaced by `LoadWavPCM()`
|
||||
in `L4AUDRES` (a tiny in-tree RIFF/WAVE fmt+data reader, no external dep). Gates fixed earlier: the
|
||||
`AWE_FRONT`/`AWE_REAR` env-var gate in `MakeAudioRenderer` was **relaxed to default-on** (`BT_NO_AUDIO=1`
|
||||
disables). **Soundbank recovered**: 241 samples cracked from `AUDIO1/2.RES` (SF2 v1.0) by
|
||||
`tools/sf2extract.py` → `content/AUDIO/*.wav` + the `allPresets[2][128]` table (`audiopresets.cpp`,
|
||||
replacing the zero-init btstubs stub); all 241 load (`alErr=0`). **In-game triggering now WORKS** (commit 7b1e469): enabling audio unlocked
|
||||
`L4AudioRenderer::CreateEntityAudioObjects`, which binds `AudioStateWatcher`s to entity/subsystem
|
||||
state attrs BY NAME (the **attribute-pointer** databinding, same as gauges) and calls
|
||||
`AddAudioWatcher` on the resolved `StateIndicator`. The reconstruction had pointed the Mech's state
|
||||
attrs at the scalar read-pad `attrPad` (fine for a gauge that READs a value, fatal for a state watcher
|
||||
that calls a METHOD → AV in `SChainOf::Add` on 0xCDCDCDCD). FIXED: the Mech's `AnimationState`/
|
||||
`ReplicantAnimationState` (driven from `SetBodyAnimation`) and `CollisionState` (driven from
|
||||
`ProcessCollision` via `collisionTemporaryState`) are now real `StateIndicator` members — **verified:
|
||||
walking fires `SetupPatch`+`PlayNote`/`alSourcePlay` (footstep/gait patches 78/80/116)**. So mech
|
||||
movement/animation/collision audio is live. **Subsystem STATE audio DONE** (commits 5ba541e/0e05a22/dcdd7dd):
|
||||
the audio watcher TYPE per attribute was recovered via a `MakeObjectImplementation` ClassID trace
|
||||
(class 28 = `AudioStateTrigger`/StateIndicator). KEY: the binary's 0x54-byte subsystem alarm
|
||||
(`FUN_0041b9ec`, reconstructed as `GaugeAlarm54`) **IS a StateIndicator** — its "three sub-indicators"
|
||||
@0x18/0x2c/0x40 are the audio/video/gauge watcher SChains, `level@0x14` = currentState. The
|
||||
reconstruction had modeled that tail as an opaque `_tail` (never constructed → AddAudioWatcher AV'd on
|
||||
0xCDCDCDCD). FIX: `GaugeAlarm54` now has three REAL constructed sockets + `AddAudioWatcher`; `SetLevel`
|
||||
fires them on change (empty sockets = no-op, so other alarms/weapons unaffected; `levelB`/`LevelCountB`
|
||||
untouched; sizeof stays 0x54). Then registered each state attr → its subsystem's own alarm (own
|
||||
`AttributePointers[]` chained to the parent): `Generator.GeneratorState`→stateAlarm,
|
||||
`Condenser.CondenserState`→condenserAlarm, `Reservoir.ReservoirState`→reservoirAlarm,
|
||||
`AmmoBin.AmmoState`→ammoAlarm (+ chained AmmoBin's bare index so `SimulationState` resolves too).
|
||||
`GeneratorOn`→generatorOn. **audiostate skips 85 → 0** — every StateIndicator audio attribute across
|
||||
the mech + all subsystems binds to a real indicator, and the alarms are already `SetLevel`'d by the
|
||||
sims, so generator/condenser/reservoir/ammo state sounds fire on transition. **Still inert** (read 0,
|
||||
silent, non-crashing — no modeled backing member in the size-locked layouts, a polish follow-up):
|
||||
`ReportLeak` (Logical), `Torso.MotionState` (Enum), `Torso.SpeedOfTorsoHorizontal` (Scalar).
|
||||
NOW REAL (fidelity Phase 1, commit fc7f311): `ConfigureActivePress` published at the **MechSubsystem
|
||||
BASE** (binary attr id 2, descriptor @0x50de5c → member @+0x110 — the field previously misnamed
|
||||
`vitalSubsystemIndex`; idle −1, driven 0/−1 by `MechWeapon::ConfigureMappablesMessageHandler`),
|
||||
`AmmoBin.FireCountdownStarted` → `cookOffArmed`@0x18C, `L4MechControlsMapper.TargetRangeExponent` →
|
||||
`targetRangeExponent`@0x1a4. The **bring-up guards**
|
||||
[T3, self-clearing] stay: a NULL subsystem attr → inert pad (was a fatal `Fail`); an
|
||||
`AudioStateWatcher` on an unconstructed indicator (chain@+0x18 null/0xCDCDCDCD) → skip. Both pass
|
||||
automatically once a subsystem is built. Diagnostics: `BT_AUDIO_LOG` (chain + skips/redirects), `BT_ATTRBIND_LOG` (every
|
||||
attr bind), `BT_AUDIO_TEST` (proof-of-life buffer0).
|
||||
- **Audio FIDELITY pass (2026-07-16, commits fc7f311 + 691e885; the full audit ledger is
|
||||
`docs/AUDIO_FIDELITY.md`, 37 findings).** Settled facts [T1 unless noted]:
|
||||
- **Patch numbering = SF2 `wPreset`.** Any tooling must key presets on `wPreset`, never on phdr
|
||||
record order. `AUDIO1/2.RES` are SF2 **v1.0** (16-byte shdr, NO name/rate/pitch field); pitch
|
||||
metadata rides generators: 55 = SBK samplePitch (`root*100+cents`, on every zone), 58 =
|
||||
overridingRootKey, 51/52 = coarse/fineTune. **EMU8000 v1 base rate = 44100 Hz** → a zone's true
|
||||
data rate is baked into its WAV: `rate = round(44100·2^(((6000−rootCents)+tune)/1200))`
|
||||
(`tools/sf2extract.py`); the engine's note-60 pitch model (`BTNotePitchFactor`) then reproduces
|
||||
original playback at every note. SBK `initialAttenuation`(48) is **INVERTED** (127 = full
|
||||
volume; 0.375 dB/step [T3 exact curve], baked into the PCM).
|
||||
- **Full-zone table**: 603 zones / 241 presets in `audiopresets.cpp`; the authored MIDI note
|
||||
SELECTS zones by keyRange in `SetupPatch/PlayNote` (key-splits: Loaded/Jam family low-36
|
||||
clunk vs high-84 blip; Warnings01 8-way klaxon split), layers attach together,
|
||||
`MAX_PRESET_SAMPLES=25` (AllExplosion). Authored loop REGIONS apply via `AL_SOFT_loop_points`;
|
||||
authored `releaseVolEnv` (1.1–3.9 s on ~20 looping presets) fades on Stop
|
||||
(`PRESET_serviceReleaseFades`, serviced from `AudioHead::Execute`).
|
||||
- **Distance/volume/doppler**: OpenAL distance model is `AL_NONE` — the AUTHORED
|
||||
`GetDistanceVolumeScale()` curve multiplies in `CalculateSourceVolumeScale` (Dynamic3D +
|
||||
Static3D); per-source `AL_GAIN = volume_scale²` (GM CC7 squared law); `alDopplerFactor(0)` —
|
||||
the authored `GetDopplerCents()` (AUDIO.INI model, consumer decomp part_008.c:7466) adds into
|
||||
the Dynamic3D pitch chain.
|
||||
- **Audio clock**: `FUN_0044e19c` is `ApplicationManager::GetFrameRate` — the original audio
|
||||
"frames" were SYSTEM CLOCK TICKS; `MakeAudioRenderer` calibrates at
|
||||
`SystemClock::GetTicksPerSecond()` (fallback 1000), never the old garbage
|
||||
`divisionParameters+0x10` read.
|
||||
- **Published `localVelocity` smoothing is authentic** [T1]: `Mech::Execute` (FUN_004ab430) runs
|
||||
the localVelocity components through `AverageOf` running-average filters
|
||||
(part_012.c:15169-15179); the port's ~0.25 s exponential filter in `mech4.cpp` reconstructs it.
|
||||
- **Phases 3+4 SHIPPED (2026-07-16, commits 0ca7d26..0e2401f).** Phase 3 EFX: `L4AUDEFX`
|
||||
bridge — per-source lowpass (Dyn3D = highFreqCutoffScale×brightness UNGATED, Static/Direct =
|
||||
brightness; AWE 100–8000 Hz → 5 kHz-ref gainhf [T3 curve]), one EAXReverb slot at
|
||||
global_reverb_scale 0.3 (3D wet / cockpit Direct dry), Direct position-enum placement.
|
||||
Phase 4 attributes (all [T1] from binary attribute tables, walked as 16-byte
|
||||
{id,name,off+1,pad} rows): **HeatSink table @0x50e438 (ids 3..12)** confirms every heat
|
||||
binding + adds ReportLeak id 12 @0x138 = coolantActive (serves all 19 leak watchers via
|
||||
PoweredSubsystem⊂HeatSink); **Mech table @0x50c0xx (ids 21..56)**: IncomingLock@0x3fc /
|
||||
DistanceToMissile@0x400 (real members, Missile::MoveAndCollide reports, PerformAndWatch
|
||||
latches; old `maxSpeed@0x400` was a misread of the FLT_MAX far default),
|
||||
CollisionSpeed@0x4B4 (=|impact velocity| at contact-arm), ReduceButton@0x340,
|
||||
FootStep@0x394, RearFiring@0x410 (old `stateFlags` misread); **Torso table (ids 3..15)**:
|
||||
full publish onto existing members (SpeedOfTorsoHorizontal=twistVelocity@0x1E8 abs'd,
|
||||
MotionState=statusFlags@0x20C, ==2 = limit-hit frame). **F5 footStep = CONTACT LEVEL**:
|
||||
(jointlocal.y <= *ANI hdr[2] threshold) per frame, states 0/1 retain (SelectSequence now
|
||||
captures hdr[2]; the 150 ms pulse is retired). **F19**: the authored footstep volume =
|
||||
LocalAcceleration |lin| [0,10]→ctl100 + LocalVelocity |lin| [0,0.6]→ctl101 (ALL motion
|
||||
watchers extract |linearMotion|, motionValue=3); localAcceleration.linear = d(published
|
||||
velocity)/dt (binary part_012.c:15186-15195); the mech2.cpp step broadcast is REMOVED.
|
||||
**attrnull count = 0** — every authored audio attribute binds a real member.
|
||||
- **Post-Phase-4 closures (2026-07-16):** UnstablePercentage is LIVE (the instability model
|
||||
@0x3F0 byte-decoded + reconstructed — accel-sway² + gun-the-engine + turn-in-place terms,
|
||||
feeds gyro swayBias + the audio alarm; commit c51d64a). The F14 per-zone static resonant
|
||||
low-pass is BAKED (RBJ biquad at the AWE 100–8000 Hz curve, float pipeline + peak
|
||||
normalization; 232 zones incl the LaserLoaded charge hum fc=57 Q=87; c581553). The
|
||||
**footstep 10–20 s warm-up bug** is fixed (a11a697): an idle source's watcher chain executes
|
||||
only at Start attempts and each hop is audio-frame-gated, so the authored fill-0 N=30
|
||||
smoothers starved behind the 0.3 transient drop gate — fixes: AudioScaleOf sends EVERY poll
|
||||
(the bitwise change-gate froze on our deterministic gait floats), recursive gate-free
|
||||
Component::PrimeWatchers(30) on every transient Start request (AUDREND), and
|
||||
localAcceleration via the binary's exact 15-sample ring buffers. LESSON: trace caps
|
||||
saturate early and fake "per-stride" rates — always timestamp before inferring cadence.
|
||||
- Still deferred: F21 attack velocity/time (all captured events were default 1), F22 HRTF
|
||||
(user has 2 speakers; enable via alsoft.ini `hrtf=true` for headphones).
|
||||
- **L4 HAL on Windows + the VS build system: DONE.** [T1]
|
||||
- **Red Planet game logic: COMPLETE & buildable** — `VTV/VTVMPPR/WEAPSYS/...`. RP is at/near playable. [T1]
|
||||
- **Multiplayer: the DOS `NETNUB` driver is already replaced by a ~3.5k-line WinSock2 TCP
|
||||
|
||||
Reference in New Issue
Block a user