Compare commits

...
10 Commits
Author SHA1 Message Date
arcattackandClaude Opus 4.8 d1ce99402f KB: code-first sweep of stub/stand-in claims -> 2 more STALE fixed
Targeted pass on "stand-in / not-wired / stub / no-op" claims, verified against
CODE + runtime (not KB wording -- the trap that hid the peer-warp staleness):

- combat-damage.md STEP-6 "remaining = fix ResourceFindByName no-op -> empty
  table" (reads present-tense above the "STEP 6 COMPLETE" header): STALE. The
  table LOADS live ([cyl] table 'bhk1'/'madcat'/'ava1' layers=7); the name-load
  BYPASSED the no-op ResourceFindByName via SearchList(type=0x14) (mech.cpp:1631).
  d07ac7d. ResolveHit resolves unaimed hits.
- multiplayer.md "Mech::Reset full subsystem-reset sweep is still a bring-up TODO":
  STALE. mech4.cpp:1616 loops every subsystem -> DeathReset(mode) (heat/power/ammo/
  charge) + heals zones + ForceUpdate(0x1f). Per-subsystem DeathReset bodies are
  authentically trivial for some classes -- not a missing sweep.

Verified ACCURATE (not stale, left as-is): GaussRifle FireWeapon no-op (a decomp
fact -- inert in the 1995 binary; btl4gau2 faithfully marks it "not yet supported");
StatusMessagePool NULL stub (kill ticker live, non-kill status messages genuinely
deferred); Myomers coupling inert (genuinely open); searchlight fog-swap (open by
decision). checkctx CLEAN.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 15:01:17 -05:00
arcattackandClaude Opus 4.8 1b5c3bbbd8 KB: correct STALE "peer warp not wired" claim -> it IS wired + visible (user-caught)
User observed peer warp spheres in 2-player mode; the KB (translocation-warp.md
frontmatter+body, multiplayer.md frontmatter+body) still called the peer path a
"local-player sphere stand-in until SimulationState/DropZoneLocation replication."
STALE: mechdmg.cpp:1074 fires BTStartWarpEffect at the peer's position gated to
ReplicantInstance, and simulationState rides every update-record header -- so an
observer DOES see a peer's un-wreck/respawn warp. Committed 160b78e ("observer
sees peer un-wreck"). The only genuine remainder is a [T3] fidelity nuance: the
peer sphere is anchored to the peer's WORLD position rather than the peer's
authentic (un-replicated) DropZoneLocation. (The staleness audit itself missed
this -- it trusted the file's own stale wording over its body + the code.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 14:46:12 -05:00
arcattackandClaude Opus 4.8 82e6bb306a KB: sweep STALE "unfinished" claims -> match landed commits (context-system audit)
4-agent audit cross-checked every open/pending/stub/deferred claim in the context
system against git + code. Fixed ~13 stale spots (currency only, no code change),
each contradicted by its own file body + a landed commit:

- 0xBD3 SubsystemMessageManager (task #7, afefaee): subsystems.md frontmatter+body
  line 57; decomp-reference.md 0xBD3 row + +0x434 row -- all still said
  "MISLABELS controlsMapper / WAVE 8 untangle" (done; mech.hpp names it messageManager).
- Cylinder hit-location STEP 6 (d07ac7d): combat-damage.md frontmatter;
  decomp-reference.md "not reconstructed" line.
- Per-mech turn-rate wiring (task #64b, a83995a): multiplayer.md + locomotion.md
  frontmatter (still "bring-up constant / not yet wired").
- Mech-level update records (task #1, c9f0c2a): open-questions.md "dead Wword
  branches mech.cpp:1511/1613" (those branches are gone; named fields now).
- Collision damage + DeathShutdown (2026-07-08/12): combat-damage.md frontmatter.
- Myomers un-stub (8b36440): decomp-reference.md "INERT un-stub" row.
- Day/night BT_MATPRI (shipped, default-on): rendering.md frontmatter (LOD half kept).
- Fire-trigger path (task #5, 8ed6184): open-questions.md "still gBTWeaponTrigger
  bring-up" (globals retired/never-read).
- HEAT entry FACTUAL fix: "heatLoad -> effectiveRange 0 = overheat cutout" was false
  per aab7a8a -- effectiveRange reads host-zone damage; the real cutout is the
  FailureHeat charge-hold.

Frontmatter open_questions re-pointed at genuinely-open items. VERIFIED NOT stale
(left as-is): multiplayer body-channel-projection notes (a real open [T3] residual
per the coupled-motion section's ~2.9u leg-vs-body snap). Genuinely-open items
(factory roster loops 2-4, subsystem-panel gate, missile cluster, console-death
freeze, gyro tails, etc.) untouched. checkctx CLEAN.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 14:35:41 -05:00
arcattackandClaude Opus 4.8 ac599c0d2a KB: reconcile STALE peer coupled-motion "COMPLETION PENDING" entry -> DONE (task #50)
The open-questions.md block was the old completion PLAN (ea39af1) and was never
updated after the work actually landed + the residual was root-caused. It
contradicted multiplayer.md (which correctly documents both "Authentic coupled
peer motion -- DONE" and the packet-jitter resolution). Reconciled to DONE:
coupled single-source gait pipeline default-on (c52a1ad/b013742/96a896a/f094d78/
23f1532); the "needs master half" conclusion superseded by 49d73dc (residual
shakiness = single-box packet jitter, a test-rig artifact, fixed with CPU
affinity, user-confirmed -- NOT a game bug). Minor follow-ups (2.9u channel-snap,
decay constant) tracked in multiplayer.md. No code change -- KB currency only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 14:19:43 -05:00
arcattackandClaude Opus 4.8 aab7a8a137 Combat: FIX weapon effectiveRange -- it degraded with heatLoad, not host-zone damage (task #50)
MechWeapon::UpdateTargeting computed effectiveRange = (1 - heatLoad) * weaponRange,
reading the weapon's own inherited HeatableSubsystem heatLoad. The authentic decomp
(@004b9bdc:6983) reads *(weapon+0xE0)+0x158 = Subsystem::damageZone->damageLevel --
i.e. effectiveRange = (1 - HOST-ZONE DAMAGE) * weaponRange. Same @0xE0-DamageZone-vs-
heat misattribution already corrected in HeatSink::UpdateCoolant (heat.cpp:803).

Impact: for a charge/discharge weapon (ER laser) the weapon's OWN heatLoad swings
0..1 every fire cycle, so effectiveRange collapsed toward 0 and the weapon was
perpetually "out of range" -> Emitter::FireWeapon's `if (dist <= effectiveRange)`
gate skipped SendDamageMessage -> NO damage submission and hence NO impact explosion.
The beam still rendered (beamFlag/beamEndpoint set before the gate), so the shot LOOKED
like a hit but did nothing -- the user-reported "lackluster/absent laser hits, esp.
the ER medium, on mechs AND buildings". PPCs mostly worked only because their heatLoad
happened to sit low/stable.

Fix: read the QUALIFIED this->Subsystem::damageZone->damageLevel (the MechSubsystem
shadow is a shim -- heat.cpp:812) so an UNDAMAGED weapon holds its full, STABLE
weaponRange, and range shortens only as the weapon's host zone takes battle damage.

Verified (parked in range of a building, autofire): laser effRange 500 STABLE
(was fluctuating 0/59/340/424 -> mostly out of range); impact explosions 13 in 22s
(11 laser id=16 + 2 PPC), up from ~2. Lasers now consistently damage + spawn FX.

Also adds env-gated diagnostics used to root-cause this: [fireW] range trace +
per-weapon explID (emitter.cpp), and BT_FIRE_AT_STRUCT (mech4.cpp) which designates
the nearest world structure so weapon-vs-structure fire can be tested without the
screen aim ray.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 11:31:47 -05:00
arcattackandClaude Opus 4.8 de8f6d02c1 MP: world structures (garages/walls) TARGETABLE -- boresight ray-tests the static collision tree (task #50)
The boresight's non-mech pick only sampled the VISUAL heightfield (BTGroundRayHit),
which on arena1 is a single flat 'sky'-named ground mesh (and class-42 BuildTables
likewise holds only 'sky') -- so shots passed THROUGH the garages/walls and only mechs
moved the HUD range axis (user-reported regression: "buildings used to move the range
axis; now only mechs do, and you can only fire at a peer").

Real fix: the boresight now ALSO ray-tests the ZONE'S STATIC COLLISION SOLID TREE --
the same geometry that already blocks the mech's walk. Authentic engine mechanism:
Mover::FindBoxedSolidHitBy already tests the static world via
zone->GetCollisionRoot()->FindBoundingBoxHitBy(line). Factored its "static world" tail
into Mover::FindStaticSolidHitBy(Line*) (static solids only, no movers), wrapped by
Mech::WorldStructurePick(start,dir,range,&hit) (builds the world-space Line, reads the
entry point back via line->FindEnd since HitByBounded clips line->length=enter).

Boresight pick order is now: closest MECH (PickRayHit, damage zones + lock) -> closest
STRUCTURE (WorldStructurePick; occludes a mech BEHIND it; designates the gBTTerrainEntity
sentinel + entry point, so the range caret reads the structure distance and NO lock ring
draws, mech4.cpp:4529) -> flat ground (BTGroundRayHit) -> sky (fire-at-nothing). Also
un-skips arena1's misnamed-'sky' flat ground so the ground tier works (btvisgnd
geometry-aware skip + [mapent]/[rendent] census).

Verified headless: 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 an enclosing box), no crash/assert/AV. Interactive aim
(BTGetAimRay) can't run headless (no window -> noRay), so the sweep is the headless proof;
interactive aim is user-verified.

Note: FindBoundingBoxUnder (the ground/containedByNode BoundingBoxTree) is DOWNWARD-only
(gravity/ground-snap: *height = FindDistanceBelowBounded), useless for a horizontal
boresight; the static SOLID tree's FindBoundingBoxHitBy is the only ray-vs-world query.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 10:08:45 -05:00
arcattackandClaude Opus 4.8 ee6e19e86e Combat: RESTORE authentic fire-at-nothing + non-mech range axis (task #50 / Discord report)
Determined the authentic behavior from the decomp (HudSimulation + Emitter,
part_013.c:5619-5670 / 7689-7778) and fixed two regressions the players hit:
 (A) You could only fire when a MECH was locked (couldn't fire at nothing).
 (B) Buildings/structures/ground stopped moving the range axis; only mechs did.

AUTHENTIC (T1): a weapon discharges iff the target slot mech+0x388 != 0
(Emitter::FireWeapon FUN_004bace8:7727), and 0x388 is whatever the BORESIGHT
designates -- mech OR world geometry -- NOT a manual mech lock.  The RANGE readout
(HUD+0x1ec) moves for ANY designated target (mech OR structure/ground); the LOCK
ring is mech-only (target has a damage-zone table, HudSim :5619).  So the pod
'fired freely at nothing' and buildings moved the range caret.

ROOT CAUSES (both content-triggered, not a targeting-code change):
 1. arena1's ONLY class-42 world entity is the 10000x10000 FLAT GROUND plane at
    y=0, MISNAMED 'sky' -- SkippedName filtered it out by name, so BuildTables
    ingested 0 geometry and gBTTerrainEntity stayed null -> nothing groundable ->
    mech-only targeting.  (LAST.EGG rewrite ~bb795e2 lost whatever terrain the
    working scene had.)
 2. A boresight that hit nothing set MECH_TARGET_ENTITY=0 -> no discharge.

FIXES:
 - btvisgnd.cpp: geometry-aware skip -- keep a wide, near-flat, near-ground plane
   even if name-skipped (it is the arena floor); still skip true domes/backdrops.
   Adds mesh y-bounds.  arena1 now ingests 1 ground instance (was 0).
 - btl4vid.cpp: capture ANY world-geometry entity as the non-mech pick sentinel
   (the default render case is world-only), not only Terrain-derived ones.
 - mech4.cpp: FIRE-AT-NOTHING -- when nothing pickable is hit, designate a
   max-range point (1200, the HUD default 0x44960000) along the boresight using
   the world sentinel, so 0x388 != 0 and the weapon discharges (no zone damage /
   no lock ring, since the sentinel has no damage-zone table -- mechs only).

Verified autonomous: arena1 ground ingested; [target] shows 'fire-at-nothing
(max-range designate)'; weapon discharges at empty space ([fire] explode
resolved); range axis (sShownRange->BTSetHudTargetRange) slides to the designated
distance.  Diagnostic: BT_GROUND_LOG (world-geometry ingest + skip reasons).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 09:10:48 -05:00
arcattackandClaude Opus 4.8 49d73dc8e2 MP: peer shakiness ROOT-CAUSED as single-box packet jitter (rig, not game) + affinity launcher (task #50)
The residual RANDOM peer shakiness on accel/decel was proven (BT_RXJIT record
inter-arrival probe) to be TEST-RIG packet jitter, not the game: two Debug btl4
nodes on one box contend for CPU, so Windows batches their TCP delivery -- records
arrive in bursts (max 56-226ms gaps, burstiness 3-7x) instead of even ~17ms.  The
peer dead-reckons across the gaps then snaps -> random shake.  Pinning the nodes to
disjoint cores restored even ~17ms delivery (burstiness ~1.0) and the shakiness
vanished (user-confirmed 'that was it').  Real pods = dedicated machines, no
contention -> never see it.  So NO un-authentic jitter buffer -- the coupled
body-channel peer (96a896a/f094d78/23f1532) is the authentic + correct finish.

 - mech.cpp: BT_RXJIT record-arrival-jitter probe (env-gated).
 - tools/mp_launch.sh: 2-node launcher that pins nodes to disjoint cores (bakes in
   the fair-delivery condition; documents why).
 - context/multiplayer.md: the finding, so it is not re-litigated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 08:14:22 -05:00
arcattackandClaude Opus 4.8 23f1532fb0 MP: peer true-mirror cadence -- drive the body clip from actual replicated motion (task #50)
Toward a truer mirror (user: peer 'hesitates/skips on accel/decel, master has
smoother transitions').  The body channel FOLLOWS the master's replicated STATE
(96a896a) but its CADENCE (bodyCycleSpeed) is still slewed LOCALLY toward the
replicated commanded speed -- which drifts from the master's actual cadence, so
the animation phase wanders until a record snaps it (the hesitation).

FIX (replicant-gated, single-player untouched): peerMirrorSpeed member = the
actual replicated ground speed (|updateVelocity|, set in the peer branch); the
body channel walk (6/7) + reverse (0xc/0xd) cases override bodyCycleSpeed with
it (clamped to the state's band) right before the clip advance, so the peer's
clip advances at the master's REAL rate instead of a locally-drifting slew.
peerMirrorSpeed = -1 on the master/single-player -> the authentic slew runs.
BT_NO_MIRROR_CAD / BT_PEER_LEGCH revert.

Verified autonomous (through-zero sweep): no crash, body channel advances forward
(back-steps ~0), feet track (position ratio 1.05, maxStep 1.68u).  Perceived
smoothness is the visual test (harness saturates).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 07:58:11 -05:00
arcattackandClaude Opus 4.8 f094d78df1 MP: restore peer turn-step on the body channel -- arm turn state 4 from replicated turn (task #50)
The body-channel swap (96a896a) fixed the stop-slide but regressed turn-in-place
to a rotating statue: the body Standing case only enters walk/reverse/stand, never
the turn clip (state 4).  On the master that arming comes from the LEG Standing
case cross-arming both channels (mech2.cpp:937) -- which the peer no longer runs.

FIX (peer branch): arm/exit the body turn state from the replicated turn
(replMppr->turnDemand, already derived from the replicated yaw rate with
hysteresis): standing + turning -> SetBodyAnimation(4) so the body case 4 advances
the trn clip (peer STEPS through the pivot); turn stops -> back to Standing.
Walk/reverse transitions still own their own exits.

Verified autonomous (forced spin): peer body channel holds state 4, trn clip
advances (frmAvg 0.2-0.3, frmMax 1-2), back-steps 0, no crash.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 07:38:33 -05:00
19 changed files with 569 additions and 66 deletions
+42 -5
View File
@@ -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
@@ -201,10 +237,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):
+4 -4
View File
@@ -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 |
@@ -132,7 +132,7 @@ valid for reading the decomp, NOT for our compiled layout (see [[reconstruction-
## 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]
+1 -1
View File
@@ -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"
---
+16 -6
View File
@@ -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
@@ -342,15 +342,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,
@@ -405,3 +411,7 @@ Decomp-verified (workflows w0odszxro/wh1h5gnmc, 3 make-or-break claims adversari
- 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): occasional ~2.9u snap from the leg(peer)-vs-body(mirror) channel mismatch -- to close, run the master mirror on a leg-channel prediction fed last-sent speed, matching the peer's poser. Also open: 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).
+49 -25
View File
@@ -154,6 +154,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 +212,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 +367,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,
@@ -536,9 +552,10 @@ 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)
- **OPEN: console death froze peer replication BOTH ways (2026-07-14) [T4 mechanism].** Live 2-node
@@ -611,21 +628,28 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
- Feeds from: every subsystem/render topic (their deferral notes collect here).
- Gating master: [[source-completeness]] (the missing BT source).
## Peer coupled-motion architecture — COMPLETION PENDING (task #50 tail, 2026-07-14)
The user's speed-change peer glitch is the last visible MP motion artifact. Root
cause is ARCHITECTURAL and decomp-confirmed: the original peer (FUN_004ab430 ->
FUN_004ab1c8) drove position FROM the clip's root travel between records (feet
locked to ground by construction) + pose-sync offset decay; our port dead-reckons
position from velocity while legs run on commanded speed — they disagree exactly
during accel/decel. The PEER half is implemented env-gated (BT_ROOT_POS=1,
mech4.cpp replicant branch) but measured worse alone: with sparse records the
master's VELOCITY mirror under-sends for a gait-driven peer (11u snaps); with
dense records the two authorities tug-of-war (path/net 1.86). COMPLETION NEEDS
THE MASTER HALF: (1) master send-gate linear mirror driven by the same gait
travel (its own channel-B IntegrateMotion projection, as the binary's), so
records model the peer; (2) then sparse authentic deadband cadence + offset
decay. All fixed-and-committed groundwork: exact quaternion integration
(a8eb8a4), incremental peer heading (d78e77b), clock guard (93456be), commanded-
speed cadence feed (6374efc), Abs-macro fix (7615ecd). Speed model decomp-settled
(2c6db6a): analog throttle, '5 speeds' false, throttleState@0x4a4 is actually the
fall-surface material (RENAME PENDING).
## 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]]): an occasional ~2.9u
leg(peer)-vs-body(mirror) channel-mismatch snap; 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).
+1 -1
View File
@@ -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
+6 -6
View File
@@ -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
+8 -4
View File
@@ -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
+28
View File
@@ -1137,6 +1137,34 @@ BoxedSolid*
return solid;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// The "static world" tail of FindBoxedSolidHitBy on its own: ray-test ONLY the
// zone's static solid tree (world structures -- garages, walls, props), NOT the
// tangible movers/doors. Same tree the mech's walk collides against, so a shot
// designated through this lands on exactly the geometry that blocks the mech.
// FindBoundingBoxHitBy clips line->length to the hit distance (HitByBounded ->
// line->length = enter), so the caller reads the entry point via line->FindEnd.
//
BoxedSolid*
Mover::FindStaticSolidHitBy(
Line *line
)
{
Check(this);
Check(line);
InterestManager *interest_mgr = application->GetInterestManager();
Check(interest_mgr);
InterestZone *zone = interest_mgr->GetInterestZone(interestZoneID);
Check(zone);
BoxedSolidTree* tree = zone->GetCollisionRoot();
Check(tree);
BoxedSolid *result = (BoxedSolid*)tree->FindBoundingBoxHitBy(line);
Check_Fpu();
return result;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
BoxedSolidCollisionList*
+10
View File
@@ -300,6 +300,16 @@ public:
Line *line,
Entity *except_by
);
// STATIC-WORLD-ONLY ray query: the "test against the static world" tail of
// FindBoxedSolidHitBy factored out -- ray-tests ONLY the zone's static solid
// tree (the world structures: garages, walls, props), skipping the tangible
// movers/doors. Used by the weapon boresight pick so a shot lands on the SAME
// static geometry that already blocks the mech's walk (mechs are picked
// separately, with their damage zones/lock). Clips line->length to the entry.
BoxedSolid*
FindStaticSolidHitBy(
Line *line
);
BoxedSolidCollisionList*
CollideCenterOfMotion(
Line *line,
+36 -2
View File
@@ -227,9 +227,43 @@ void
// to terrain).
{
extern Entity *gBTTerrainEntity;
// The boresight ground/structure pick needs a non-null world Entity to
// cite in mech+0x388 (the geometry rides in the pick POINT; the entity is
// only the non-mech designation -- the decomp's HudSimulation handles a
// target with no damage-zone table, part_013.c:5620). Previously this
// required a TERRAIN-derived entity, but maps whose world geometry is
// buildings/cultural/props (class-42, NOT Terrain-derived) captured
// nothing -> gBTTerrainEntity stayed 0 -> the ground pick was skipped
// (mech4.cpp:4372) -> only mechs were targetable/firable (task #50
// regression: LAST.EGG rewrite lost its Terrain entity). This `default`
// case is world-geometry ONLY (mechs route elsewhere), so capture the
// first entity reaching it -- preferring a real Terrain, else any world
// entity (building/prop/landmark) as the non-mech pick sentinel.
if (gBTTerrainEntity == 0
&& entity->IsDerivedFrom(*Terrain::GetClassDerivations()))
gBTTerrainEntity = entity;
|| !gBTTerrainEntity->IsDerivedFrom(*Terrain::GetClassDerivations()))
{
if (entity->IsDerivedFrom(*Terrain::GetClassDerivations()))
gBTTerrainEntity = entity; // prefer a true Terrain entity
else if (gBTTerrainEntity == 0)
gBTTerrainEntity = entity; // else the first world entity
}
// CENSUS: what world-geometry entities does the RENDER tree walk?
// (the garages/structures may be here even if not in the map's class-42
// instance stream that BuildTables reads.)
if (getenv("BT_GROUND_LOG"))
{
Derivation *dv = entity->GetClassDerivations();
const int isTer = entity->IsDerivedFrom(*Terrain::GetClassDerivations())?1:0;
const Point3D &ep = entity->localOrigin.linearPosition;
// tag NON-terrain entities distinctly -- these are the structures
// (garages/walls/props); log their world XZ so we can spawn beside
// one and confirm it is a collision solid the boresight now hits.
DEBUG_STREAM << (isTer ? "[rendent]" : "[rendent-STRUCT]")
<< " class='" << (dv && dv->className ? dv->className : "?")
<< "' terrain=" << isTer
<< " pos=(" << ep.x << "," << ep.y << "," << ep.z << ")"
<< "\n" << std::flush;
}
}
DPLRenderer::MakeEntityRenderables(
entity, model_resource, view_type);
+49 -4
View File
@@ -53,6 +53,7 @@ struct VgMesh
{
std::vector<VgTri> tris;
float minx, maxx, minz, maxz; // model-local XZ bound
float miny, maxy; // model-local Y bound (dome vs flat-ground test)
};
struct VgInst
@@ -89,8 +90,8 @@ int LoadMeshFor(const char *resource_name)
if (LoadBgfFile(file, data) && data.ok && !data.indices.empty())
{
VgMesh m;
m.minx = m.minz = 1e9f;
m.maxx = m.maxz = -1e9f;
m.minx = m.minz = m.miny = 1e9f;
m.maxx = m.maxz = m.maxy = -1e9f;
// The loader emits DOUBLE-SIDED triangles: emitTri pushes (a,b,c) then
// (a,c,b) -- 6 indices per source triangle. Take the forward one.
for (size_t i = 0; i + 5 < data.indices.size(); i += 6)
@@ -100,17 +101,33 @@ int LoadMeshFor(const char *resource_name)
const BgfVtx &C = data.verts[data.indices[i + 2]];
VgTri t = { A.x, A.y, A.z, B.x, B.y, B.z, C.x, C.y, C.z };
m.tris.push_back(t);
float xs[3] = { A.x, B.x, C.x }, zs[3] = { A.z, B.z, C.z };
float xs[3] = { A.x, B.x, C.x }, zs[3] = { A.z, B.z, C.z }, ys[3] = { A.y, B.y, C.y };
for (int k = 0; k < 3; ++k)
{
if (xs[k] < m.minx) m.minx = xs[k];
if (xs[k] > m.maxx) m.maxx = xs[k];
if (zs[k] < m.minz) m.minz = zs[k];
if (zs[k] > m.maxz) m.maxz = zs[k];
if (ys[k] < m.miny) m.miny = ys[k];
if (ys[k] > m.maxy) m.maxy = ys[k];
}
}
if (!m.tris.empty())
{
if (getenv("BT_GROUND_LOG"))
{
float miny = 1e9f, maxy = -1e9f;
for (size_t j = 0; j < m.tris.size(); ++j)
{
const VgTri &t = m.tris[j];
const float ys[3] = { t.ay, t.by, t.cy };
for (int k = 0; k < 3; ++k)
{ if (ys[k] < miny) miny = ys[k]; if (ys[k] > maxy) maxy = ys[k]; }
}
DEBUG_STREAM << "[visgnd] mesh '" << key << "' tris=" << m.tris.size()
<< " x[" << m.minx << "," << m.maxx << "] z[" << m.minz << "," << m.maxz
<< "] y[" << miny << "," << maxy << "]\n" << std::flush;
}
gMeshes.push_back(m);
idx = (int)gMeshes.size() - 1;
}
@@ -151,16 +168,44 @@ bool BuildTables()
const float *pos = (const float *)(base + p + 48);
p += (mlen + 3) & ~3u;
// CENSUS (BT_GROUND_LOG): log EVERY map entity's class + name, so we can see
// what class the garages/structures are (the class-42 filter may drop them).
if (getenv("BT_GROUND_LOG"))
{
ResourceDescription *crd = rf->FindResourceDescription(rid);
DEBUG_STREAM << "[mapent] cls=" << cls << " mflags=" << mflags
<< " name='" << (crd ? crd->resourceName : "?") << "'\n" << std::flush;
}
if (mflags & 0x2) continue; // include marker
if (cls != 42) continue; // terrain/static cultural only
ResourceDescription *rd = rf->FindResourceDescription(rid);
if (rd == 0) continue;
if (SkippedName(rd->resourceName)) { ++skipped; continue; }
int mi = LoadMeshFor(rd->resourceName);
if (mi < 0) { ++skipped; continue; }
// GEOMETRY-AWARE skip: the name filter (sky/backdrop) is right for a high
// vertical DOME, but arena1's ground is a 10000x10000 FLAT plane at y=0
// misnamed "sky" -- name-skipping it removed the only groundable geometry
// (task #50: buildings/ground stopped moving the range axis). Keep a
// name-skipped mesh IF it is a wide, near-flat, near-ground plane (the
// arena floor); still skip true domes / tall vertical backdrops.
if (SkippedName(rd->resourceName))
{
const VgMesh &gm = gMeshes[mi];
const float yext = gm.maxy - gm.miny;
const float xext = gm.maxx - gm.minx, zext = gm.maxz - gm.minz;
const bool flatWideGround =
(yext < 200.0f) && (xext > 500.0f) && (zext > 500.0f) && (gm.miny < 200.0f);
if (getenv("BT_GROUND_LOG"))
DEBUG_STREAM << "[visgnd] name-skip '" << rd->resourceName
<< "' yext=" << yext << " xz=" << xext << "x" << zext
<< " miny=" << gm.miny << " -> keepAsGround=" << (flatWideGround?1:0)
<< "\n" << std::flush;
if (!flatWideGround) { ++skipped; continue; }
}
VgInst inst;
inst.mesh = mi;
inst.px = pos[0]; inst.py = pos[1]; inst.pz = pos[2];
+10
View File
@@ -271,6 +271,15 @@ void
Scalar dist = (Scalar)Sqrt(delta.x*delta.x + delta.y*delta.y + delta.z*delta.z); // FUN_004dd138
beamScale.z = dist / graphicLength; // 0x434 (== beamScale[2]) = dist / 0x438
if (getenv("BT_FIRE_LOG"))
{
Entity *st = (owner != 0) ? *(Entity **)((char *)owner + 0x388) : 0;
DEBUG_STREAM << "[fireW] " << GetName() << " dist=" << (float)dist
<< " effRange=" << (float)effectiveRange
<< " inRange=" << (int)(dist <= effectiveRange)
<< " explID=" << (long)GetExplosionResourceID()
<< " tgt=" << (void *)st << "\n" << std::flush;
}
if (dist <= effectiveRange) // this+0x328
{
// THE AUTHENTIC DAMAGE SUBMISSION (task #8; binary @004bace8:7758-7764):
@@ -349,6 +358,7 @@ void
<< " pct=" << (float)rechargeLevel
<< " tgt=" << (void *)((owner != 0)
? *(Entity **)((char *)owner + 0x388) : 0)
<< " explID=" << (long)GetExplosionResourceID()
<< std::endl;
}
targetWithinRange = UpdateTargeting(); // @004b9bdc -> this+0x34c
+59
View File
@@ -539,6 +539,37 @@ Logical
return True;
}
//
// World-STRUCTURE boresight pick. Ray-test the zone's STATIC collision solid
// tree (the world geometry -- garages, walls, props -- that the mech's walk
// already collides against) along the boresight, and hand back the entry point
// of the closest structure the ray strikes. This is the authentic non-mech
// world pick: Mover::FindStaticSolidHitBy is the "test against the static world"
// tail of the engine's own FindBoxedSolidHitBy (MOVER.cpp), which is what the
// mover-vs-world collision uses -- so a shot lands on EXACTLY the geometry that
// blocks the walk (the user's "structures block the mech, but weapons don't").
//
// The ray is WORLD-space (the static tree is world-space, unlike a mech's local
// collision template). FindBoundingBoxHitBy clips the Line's length to the
// entry distance (BoundingBox::HitByBounded -> line->length = enter), so
// line->FindEnd yields the world entry point on the struck solid.
//
Logical
Mech::WorldStructurePick(const Point3D &start, const Vector3D &dir,
Scalar max_range, Point3D *hit_world)
{
Scalar dlen = (Scalar)Sqrt(dir.x*dir.x + dir.y*dir.y + dir.z*dir.z);
if (dlen < 1e-6f)
return False;
Line ray(start, UnitVector(dir.x/dlen, dir.y/dlen, dir.z/dlen), max_range);
BoxedSolid *solid = FindStaticSolidHitBy(&ray); // static world only
if (solid == 0)
return False;
if (hit_world != 0)
ray.FindEnd(hit_world); // clipped length -> entry point
return True;
}
//
// The roster torso (sinkSourceSubsystem @0x438, ClassID 0xBC5) -- parity with
// the binary's *(mech+0x438).
@@ -916,6 +947,7 @@ Mech::Mech(
poseSyncLatch = 0; // @0x77c (was absorbed Wword(0x1df))
heatLevelSnapshot = 0; // @0x780 (type-7 deadband baseline)
mirrorBodyAdv = 0.0f; // master send-mirror gait travel
peerMirrorSpeed = -1.0f; // peer true-mirror cadence (disabled by default)
angMirrorYaw = 0.0f; // scalar peer-yaw mirror (port addition)
angMirrorRate = 0.0f;
angMirrorValid = 0;
@@ -1789,6 +1821,33 @@ Logical
void
Mech::ReadUpdateRecord(Simulation::UpdateRecord *message)
{
// RECORD-CADENCE probe (BT_RXJIT): quantify how EVENLY records arrive (wall-clock
// ms between arrivals) -- the jitter that makes the peer's corrections random.
if (getenv("BT_RXJIT") && GetInstance() == ReplicantInstance)
{
LARGE_INTEGER _now, _freq;
QueryPerformanceCounter(&_now); QueryPerformanceFrequency(&_freq);
static double s_lastMs = 0.0, s_min = 1e9, s_max = 0.0, s_acc = 0.0;
static int s_n = 0, s_rep = 0;
const double nowMs = (double)_now.QuadPart * 1000.0 / (double)_freq.QuadPart;
if (s_lastMs > 0.0)
{
const double gap = nowMs - s_lastMs;
s_n++; s_acc += gap;
if (gap > s_max) s_max = gap;
if (gap < s_min) s_min = gap;
}
s_lastMs = nowMs;
if (++s_rep >= 120 && s_n > 0)
{
DEBUG_STREAM << "[rxjit] records/win=" << s_n
<< " gapMs min=" << s_min << " avg=" << (s_acc / s_n)
<< " max=" << s_max
<< " burstiness(max/avg)=" << (s_max / (s_acc / s_n))
<< "\n" << std::flush;
s_rep = 0; s_n = 0; s_acc = 0.0; s_min = 1e9; s_max = 0.0;
}
}
static const int s_mrecLog = getenv("BT_REPL_LOG") ? 1 : 0;
if (s_mrecLog && message->recordID != 0)
DEBUG_STREAM << "[mrec-rx] type=" << (int)message->recordID
+14
View File
@@ -631,6 +631,12 @@ public:
// predicts (gait), instead of the constant-velocity deadReckoner. This is the
// authentic FUN_004a9b5c mirror = IntegrateMotion(mj=0) on projectedOrigin.
Scalar mirrorBodyAdv; // body cycleDistance this frame (master)
// Peer true-mirror cadence: the replicant sets this to the actual replicated
// ground speed (|updateVelocity|) each frame so the body clip advances at the
// master's real rate instead of the locally-slewed bodyCycleSpeed (which drifts
// and makes the animation phase wander -> the accel/decel hesitation). -1 =
// disabled (master/single-player always -1, so the authentic slew runs).
Scalar peerMirrorSpeed;
Scalar angMirrorYaw; // yaw at the last type-4 send (rad)
Scalar angMirrorRate; // yaw rate sent with it (rad/s)
Time angMirrorTime; // lastPerformance at that send
@@ -687,6 +693,14 @@ public:
Logical PickRayHit(const Point3D &start, const Vector3D &dir,
Scalar max_range, Point3D *hit_world);
// World-STRUCTURE boresight pick: ray-test the zone's STATIC collision solid
// tree (garages/walls/props -- the geometry that blocks the walk) along the
// boresight and return the entry point of the closest structure hit. The
// authentic non-mech world pick (Mover::FindStaticSolidHitBy, the static-world
// half of the engine's FindBoxedSolidHitBy). Defined in mech.cpp.
Logical WorldStructurePick(const Point3D &start, const Vector3D &dir,
Scalar max_range, Point3D *hit_world);
// Per-weapon beam render walk (task #33; extracted task #51 so it runs for
// EVERY mech -- player, dummy masters, and MP replicants whose emitters
// carry replicated discharge state). Defined in mech4.cpp.
+12
View File
@@ -999,6 +999,9 @@ Scalar
if (bodyCycleSpeed > bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed;
if (bodyCycleSpeed > walkStrideLength) bodyCycleSpeed = walkStrideLength;
}
if (peerMirrorSpeed >= 0.0f) // peer: cadence == actual mirrored ground speed
bodyCycleSpeed = (peerMirrorSpeed < standSpeed) ? standSpeed
: ((peerMirrorSpeed > walkStrideLength) ? walkStrideLength : peerMirrorSpeed);
distance = bodyAnimation.Advance(
time_slice * (bodyCycleSpeed / walkStrideLength) * globalTimeScale, loop);
break;
@@ -1019,6 +1022,9 @@ Scalar
if (bodyCycleSpeed > bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed;
if (bodyCycleSpeed > reverseSpeedMax2) bodyCycleSpeed = reverseSpeedMax2;
}
if (peerMirrorSpeed >= 0.0f) // peer: cadence == actual mirrored ground speed
bodyCycleSpeed = (peerMirrorSpeed < reverseSpeedMax) ? reverseSpeedMax
: ((peerMirrorSpeed > reverseSpeedMax2) ? reverseSpeedMax2 : peerMirrorSpeed);
distance = bodyAnimation.Advance(
time_slice * (bodyCycleSpeed / reverseStrideLength) * globalTimeScale, loop);
break;
@@ -1159,6 +1165,9 @@ Scalar
if (bodyCycleSpeed > bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed;
if (bodyCycleSpeed > walkStrideLength) bodyCycleSpeed = walkStrideLength;
}
if (peerMirrorSpeed >= 0.0f) // peer: cadence == actual mirrored ground speed
bodyCycleSpeed = (peerMirrorSpeed < standSpeed) ? standSpeed
: ((peerMirrorSpeed > walkStrideLength) ? walkStrideLength : peerMirrorSpeed);
distance = bodyAnimation.Advance(
time_slice * (bodyCycleSpeed / walkStrideLength) * globalTimeScale, loop);
break;
@@ -1179,6 +1188,9 @@ Scalar
if (bodyCycleSpeed > bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed;
if (bodyCycleSpeed > reverseSpeedMax2) bodyCycleSpeed = reverseSpeedMax2;
}
if (peerMirrorSpeed >= 0.0f) // peer: cadence == actual mirrored ground speed
bodyCycleSpeed = (peerMirrorSpeed < reverseSpeedMax) ? reverseSpeedMax
: ((peerMirrorSpeed > reverseSpeedMax2) ? reverseSpeedMax2 : peerMirrorSpeed);
distance = bodyAnimation.Advance(
time_slice * (bodyCycleSpeed / reverseStrideLength) * globalTimeScale, loop);
break;
+171 -6
View File
@@ -2148,6 +2148,35 @@ void
// master's exact clips. Swapping the peer to the body channel RESTORES the
// original single-channel design (no new netcode). BT_PEER_LEGCH=1 = old.
static const int s_peerLegCh = getenv("BT_PEER_LEGCH") ? 1 : 0;
// TRUE-MIRROR CADENCE: drive the body clip from the ACTUAL replicated ground
// speed so the animation phase tracks the master exactly instead of drifting
// via the local bodyCycleSpeed slew (the accel/decel hesitation). Consumed in
// AdvanceBodyAnimation walk/reverse. -1 disables -> authentic slew.
if (!s_peerLegCh && !getenv("BT_NO_MIRROR_CAD"))
{
const Vector3D &mcv = updateVelocity.linearMotion;
peerMirrorSpeed = (Scalar)sqrtf((float)(mcv.x * mcv.x + mcv.z * mcv.z));
}
else
peerMirrorSpeed = -1.0f;
// TURN-STEP ARMING for the body channel: the body Standing case only enters
// walk/reverse/stand -- it never arms the turn-in-place clip (state 4); on the
// master that arming comes from the LEG Standing case cross-arming both
// channels (mech2.cpp:937 note), which the peer no longer runs. Arm/exit the
// body turn state here from the replicated turn (replMppr->turnDemand, from the
// replicated yaw rate + hysteresis above): standing + turning -> state 4 (body
// case 4 then advances the trn clip -> the peer STEPS through the pivot); turn
// stops -> back to Standing. Walk/reverse transitions still own their exits.
if (!s_peerLegCh)
{
const int bs = (int)bodyStateAlarm.GetLevel();
const bool turning = (replMppr->turnDemand > 0.05f
|| replMppr->turnDemand < -0.05f);
if (turning && bs == 0) // standing + turning -> enter turn
SetBodyAnimation(4);
else if (!turning && bs == 4) // turn stopped -> back to stand
SetBodyAnimation(0);
}
const Scalar replLegAdv = s_peerLegCh
? AdvanceLegAnimation(dt) // old: re-derived leg SM
: AdvanceBodyAnimation(dt, 1); // authentic: body channel, mj=1 poses skeleton
@@ -4296,6 +4325,39 @@ void
}
}
// WORLD-STRUCTURE SWEEP PROBE (BT_WSWEEP, task #50 verification): once a
// second cast a horizontal fan of rays from the eyepoint against the
// STATIC collision solid tree (WorldStructurePick -> FindStaticSolidHitBy)
// and log the hit count + nearest range. Confirms arena1's structures
// (garages/walls) ARE in the collision tree the boresight now queries --
// independent of the render sentinel or interactive aim. Diag, off by default.
if (getenv("BT_WSWEEP"))
{
static float s_ws = 0.0f; s_ws += dt;
if (s_ws >= 1.0f)
{
s_ws = 0.0f;
Point3D eye = localOrigin.linearPosition; eye.y += 30.0f;
int hits = 0; float nearest = 1e30f; float ndir = -1.0f;
for (int a = 0; a < 24; ++a)
{
double th = (double)a * (6.2831853071795862 / 24.0);
Vector3D d((float)cos(th), 0.0f, (float)sin(th));
Point3D hp;
if (WorldStructurePick(eye, d, 2000.0f, &hp))
{
++hits;
float dx = hp.x-eye.x, dz = hp.z-eye.z;
float r = (float)Sqrt(dx*dx + dz*dz);
if (r < nearest) { nearest = r; ndir = (float)(th * 57.29578); }
}
}
DEBUG_STREAM << "[wsweep] eye=(" << eye.x << "," << eye.z
<< ") struct-hits=" << hits << "/24 nearest="
<< (hits ? nearest : -1.0f) << " dir=" << ndir << "deg\n" << std::flush;
}
}
float rs[3], rd[3];
if (!BTGetAimRay(gBTAimX, gBTAimY, rs, rd))
{
@@ -4327,16 +4389,62 @@ void
hotPoint = hp;
}
}
if (hotTarget != 0)
// WORLD-STRUCTURE pick (task #50): ray-test the STATIC collision
// solid tree (garages/walls/props -- the geometry that already
// BLOCKS the mech's walk) along the boresight. A structure CLOSER
// than the mech hit occludes it (the shot stops at the wall); with
// no mech, a structure hit IS the designation. This is the
// authentic non-mech world pick: Mech::WorldStructurePick ->
// Mover::FindStaticSolidHitBy, the "test against the static world"
// half of the engine's own FindBoxedSolidHitBy (MOVER.cpp) -- so a
// shot lands on exactly the geometry the walk collides against
// (the user's "structures block the mech, but weapons pass through").
Point3D structPoint;
float structDist = 1e30f;
bool haveStruct = false;
if (gBTTerrainEntity != 0)
{
pickTarget = hotTarget;
Point3D sp;
if (WorldStructurePick(rayStart, rayDir, 1200.0f, &sp))
{
float dx = sp.x-rs[0], dy = sp.y-rs[1], dz = sp.z-rs[2];
structDist = dx*dx + dy*dy + dz*dz;
structPoint = sp;
haveStruct = true;
static float s_wp = 0.0f; s_wp += dt;
if (getenv("BT_GROUND_LOG") && s_wp >= 1.0f)
{
s_wp = 0.0f;
DEBUG_STREAM << "[wpick] structure hit at ("
<< sp.x << "," << sp.y << "," << sp.z << ") dist="
<< (float)Sqrt(structDist)
<< (hotTarget ? " (mech also under boresight)" : "")
<< "\n" << std::flush;
}
}
}
if (hotTarget != 0 && (!haveStruct || bestDist <= structDist))
{
pickTarget = hotTarget; // mech is the closest hit
pickPoint = hotPoint;
++gAimHits;
}
else if (haveStruct)
{
// a world structure is the closest hit (garage/wall) -- or it
// occludes a mech behind it. Designate it as the non-mech world
// pick (the geometry rides in the POINT; the sentinel entity has
// no damage-zone table -> no lock ring, HudSim part_013.c:5620).
pickTarget = gBTTerrainEntity;
pickPoint = structPoint;
++gAimGround;
}
else
{
// the ground downrange of the guns (max = the pod's HUD
// range scale; past it the shot is a sky shot -> no target)
// no mech, no structure -> the flat ground downrange of the
// guns (max = the pod's HUD range scale; past it the shot is a
// sky shot -> no target)
extern bool BTGroundRayHit(float,float,float, float,float,float,
float, float*,float*,float*);
float hx, hy, hz;
@@ -4368,13 +4476,69 @@ void
MECH_TARGET_SUBIDX(this) = -1;
MECH_TARGET_POS(this) = pickPoint; // beam endpoint = the pick
}
else if (gBTTerrainEntity != 0)
{
// FIRE-AT-NOTHING (authentic, decomp part_013.c:7727): the pod fired
// freely at empty space -- the discharge gate is 0x388!=0 (the boresight
// always DESIGNATES a point, not a manual mech lock). When nothing pickable
// is hit (a level shot flies over the flat ground, or open sky), designate a
// MAX-RANGE point using the world sentinel so the weapon discharges to max
// range. Sentinel has no damage-zone table -> no aimed-zone damage, no lock
// ring (mechs only), per HudSimulation part_013.c:5620.
MECH_TARGET_ENTITY(this) = gBTTerrainEntity;
MECH_TARGET_SUBIDX(this) = -1;
Point3D farPt;
farPt.x = rs[0] + rd[0] * 1200.0f; // 1200 = HUD range default (0x44960000)
farPt.y = rs[1] + rd[1] * 1200.0f;
farPt.z = rs[2] + rd[2] * 1200.0f;
MECH_TARGET_POS(this) = farPt;
targetReticle.targetEntity = gBTTerrainEntity;
targetReticle.rayIntersection = farPt;
}
else
{
MECH_TARGET_ENTITY(this) = 0; // sky: no target, no discharge
MECH_TARGET_ENTITY(this) = 0; // no world sentinel -> no target
MECH_TARGET_SUBIDX(this) = -1;
}
}
// DETERMINISTIC FIRE-AT-STRUCTURE TEST (BT_FIRE_AT_STRUCT, task #50 verify):
// the headless aim ray (BTGetAimRay) is flaky without a focused window, so
// this bypasses it -- designate the NEAREST world structure (360 sweep) as
// the target every frame so BT_AUTOFIRE can be tested against a real, stable,
// in-range building. Diag only, off by default.
if (getenv("BT_FIRE_AT_STRUCT"))
{
extern Entity *gBTTerrainEntity;
Point3D eye = localOrigin.linearPosition; eye.y += 30.0f;
Point3D best; float bestR = 1e30f; bool got = false;
for (int a = 0; a < 24; ++a)
{
double th = (double)a * (6.2831853071795862 / 24.0);
Vector3D d((float)cos(th), 0.0f, (float)sin(th));
Point3D hp;
if (WorldStructurePick(eye, d, 2000.0f, &hp))
{
float dx = hp.x-eye.x, dz = hp.z-eye.z;
float r = (float)Sqrt(dx*dx + dz*dz);
if (r < bestR) { bestR = r; best = hp; got = true; }
}
}
if (got && gBTTerrainEntity != 0)
{
MECH_TARGET_ENTITY(this) = gBTTerrainEntity;
MECH_TARGET_SUBIDX(this) = -1;
MECH_TARGET_POS(this) = best;
static float s_fas = 0.0f; s_fas += dt;
if (getenv("BT_FIRE_LOG") && s_fas >= 1.0f)
{
s_fas = 0.0f;
DEBUG_STREAM << "[fireat] nearest struct (" << best.x << ","
<< best.y << "," << best.z << ") r=" << bestR << "\n" << std::flush;
}
}
}
// HUD feeds: the range caret + the hotbox (world point + state) + the
// recovered-Execute instruments (compass, twist tape, group mask).
// The range caret tracks the PICK (authentic: :5639 computes it from
@@ -4806,7 +4970,8 @@ void
DEBUG_STREAM << "[target] aim=(" << gBTAimX << "," << gBTAimY << ")"
<< (victim != 0 ? " MECH under boresight (aimed)"
: (pickTarget != 0 ? " terrain downrange (beam at scenery)"
: " sky (no target, no discharge)"))
: (MECH_TARGET_ENTITY(this) != 0 ? " fire-at-nothing (max-range designate)"
: " no target (no world sentinel)")))
<< " range=" << range
<< (anyWeaponInRange ? " IN WEAPON RANGE" : "")
<< " [mechPicks=" << gAimHits << " groundPicks=" << gAimGround
+15 -2
View File
@@ -500,8 +500,21 @@ Logical
rangeToTarget = (Scalar)Sqrt( // FUN_004dd138
delta.x * delta.x + delta.y * delta.y + delta.z * delta.z);
// 1.0f == _DAT_004b9c98; heatLoad is the inherited HeatableSubsystem load.
effectiveRange = (1.0f - heatLoad) * weaponRange; // *0x32c
// AUTHENTIC (decomp @004b9bdc:6983): effectiveRange = (1 - hostZoneDamage) *
// weaponRange, where hostZoneDamage is this weapon's OWN DamageZone.damageLevel
// (the QUALIFIED Subsystem::damageZone @0xE0 -> damageLevel +0x158), NOT
// heatLoad. The old `heatLoad` read was the SAME @0xE0-vs-heat misattribution
// already corrected in HeatSink::UpdateCoolant (heat.cpp:803): for a charge/
// discharge weapon (ER laser) the weapon's OWN heatLoad swings 0..1 every cycle,
// so effectiveRange collapsed toward 0 and the weapon was perpetually "out of
// range" -- no damage submission, hence no impact explosion (user report:
// lackluster/absent laser hits on mechs AND buildings, esp. the ER medium).
// An UNDAMAGED weapon has damageLevel 0 -> full, STABLE weaponRange. Use the
// QUALIFIED engine zone -- MechSubsystem::damageZone is a shim SHADOW (heat.cpp:812).
// 1.0f == _DAT_004b9c98.
::DamageZone *hostZone = this->Subsystem::damageZone; // @0xE0
Scalar hostZoneDamage = (hostZone != 0) ? hostZone->damageLevel : 0.0f; // +0x158
effectiveRange = (1.0f - hostZoneDamage) * weaponRange; // *0x32c
targetWithinRange = (effectiveRange > rangeToTarget) ? True : False;
}
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# mp_launch.sh -- launch a 2-node local multiplayer test (FOGDAY.EGG) with the two
# game nodes PINNED TO DISJOINT CPU CORES + the console relay.
#
# WHY THE AFFINITY MATTERS (task #50, 2026-07-15): two Debug btl4 nodes on one box
# fight for the same cores, and Windows punishes the contention by BATCHING their
# TCP packet delivery -- update records that should arrive every ~17ms instead
# arrive in bursts (measured gaps up to 226ms, burstiness 3-7x, BT_RXJIT). A peer
# dead-reckons across those gaps and then snaps when a record finally lands -> the
# "random" shaky peer motion. Pinning each node to its own core set restores even
# ~17ms delivery (burstiness ~1.0) and the shakiness vanishes. This is a SINGLE-BOX
# TEST-RIG artifact -- real pods are dedicated machines and never see it; do NOT
# "fix" it in the game with an interpolation buffer.
#
# Usage: tools/mp_launch.sh [extra env for BOTH nodes...]
# e.g. tools/mp_launch.sh BT_SLIDE=1
# Run from the repo root; content/ must hold FOGDAY.EGG and the build in build/Debug.
set -u
cd "$(dirname "$0")/../content" || exit 1
EXE=../build/Debug/btl4.exe
EXTRA="$*"
powershell -NoProfile -Command "Get-Process btl4,python -ErrorAction SilentlyContinue | Stop-Process -Force" 2>/dev/null
sleep 2
rm -f fa.log fb.log console.log
env $EXTRA BT_DEV_GAUGES=1 BT_LOG=fb.log BT_START_INSIDE=1 BT_SPAWN_AT="40 -150" "$EXE" -egg FOGDAY.EGG -net 1601 &
sleep 2
env $EXTRA BT_DEV_GAUGES=1 BT_LOG=fa.log BT_START_INSIDE=1 BT_SPAWN_AT="-40 -150" "$EXE" -egg FOGDAY.EGG -net 1501 &
sleep 3
# Pin the two nodes to disjoint core sets (0x00F = cores 0-3, 0x3C0 = cores 6-9).
powershell -NoProfile -Command "\$ps=@(Get-Process btl4 | Sort-Object Id); if(\$ps.Count -ge 2){ \$ps[0].ProcessorAffinity=[IntPtr]0x00F; \$ps[1].ProcessorAffinity=[IntPtr]0x3C0; Write-Output ('PINNED '+\$ps[0].Id+'->cores0-3, '+\$ps[1].Id+'->cores6-9') } else { Write-Output ('WARN only '+\$ps.Count+' btl4 up') }"
sleep 6
python -u ../tools/btconsole.py FOGDAY.EGG 127.0.0.1:1501 127.0.0.1:1601 > console.log 2>&1 &
echo "console streaming; nodes on 1501/1601. Logs: content/fa.log content/fb.log"
wait