Compare commits
10
Commits
96a896ae43
...
d1ce99402f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1ce99402f | ||
|
|
1b5c3bbbd8 | ||
|
|
82e6bb306a | ||
|
|
ac599c0d2a | ||
|
|
aab7a8a137 | ||
|
|
de8f6d02c1 | ||
|
|
ee6e19e86e | ||
|
|
49d73dc8e2 | ||
|
|
23f1532fb0 | ||
|
|
f094d78df1 |
@@ -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]
|
related_topics: [subsystems, decomp-reference, reconstruction-gotchas, locomotion, rendering, translocation-warp, multiplayer]
|
||||||
key_terms: [damage-zone, TakeDamage, viewpoint-entity, master, replicant]
|
key_terms: [damage-zone, TakeDamage, viewpoint-entity, master, replicant]
|
||||||
open_questions:
|
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
|
# 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
|
boresight dead-ahead. Once locked, `Emitter::FireWeapon` converges with NO aim/arc test
|
||||||
(part_013.c:7758). `MechWeapon::UpdateTargetState` (`FUN_004b9bdc` [T1]): `targetWithinRange =
|
(part_013.c:7758). `MechWeapon::UpdateTargetState` (`FUN_004b9bdc` [T1]): `targetWithinRange =
|
||||||
dist < (1 − hostZoneDamage) × weaponRange`. The 0x388 WRITER is in the same un-exported gap.
|
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
|
**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` →
|
cockpit eye inherits the twist AUTHENTICALLY through the draw traversal (`Torso::PushTwist` →
|
||||||
hinge `rotationAmount` → `HingeRenderable::Execute` multiplies the live hinge into the matrix
|
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
|
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).
|
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
|
## Firing arc + muzzles
|
||||||
There is NO firing-arc field in the binary (`Emitter::FireWeapon` fires whenever HasActiveTarget) —
|
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
|
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
|
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`) →
|
is found by the mech's **type-0x14 DamageZoneStream NAME** (`ResourceDescription::resourceName`) →
|
||||||
`ResourceFile::FindResourceDescription(name, DamageLookupTableStreamResourceType/*29*/)`.
|
`ResourceFile::FindResourceDescription(name, DamageLookupTableStreamResourceType/*29*/)`.
|
||||||
So STEP 6 remaining = (6b) fix the name-load (current `ResourceFindByName` is a NO-OP template stub → empty
|
So STEP 6 was = (6b) the name-load; (6c) the table + `ResolveHit→zone`; (6d) the
|
||||||
name → 0 rows); (6c) make the table functional + `ResolveHit→zone`; (6d) register the
|
`Mech::TakeDamageMessageHandler` override. **✅ ALL DONE — see "STEP 6 COMPLETE" below (`d07ac7d`):** the
|
||||||
`Mech::TakeDamageMessageHandler` override + verify. **Nothing is guessed — geometry, roll, handler, and byte
|
name-load BYPASSED the no-op `ResourceFindByName` via `SearchList(type=0x14)` (mech.cpp:1631), so the table
|
||||||
format are all verified.**
|
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 classes ALREADY EXIST — `game/reconstructed/dmgtable.cpp` (2026-07 discovery)
|
||||||
The three container classes are **already reconstructed** in `dmgtable.cpp`/`.hpp` (in the build):
|
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 |
|
| 0xBC3 | **Sensor** | — | `case`=MyomersClassID |
|
||||||
| 0xBC4 | **Gyroscope** | @004b3778 | LIVE (task #56: byte-exact ctor/integrators/writers; hit-bounce fan-out wired) |
|
| 0xBC4 | **Gyroscope** | @004b3778 | LIVE (task #56: byte-exact ctor/integrators/writers; hit-bounce fan-out wired) |
|
||||||
| 0xBC5 | **Torso** | @004b6b0c | sizeof 0x280; `case`=SinkSource |
|
| 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 |
|
| 0xBC8 | **Emitter** | @004b1d18 | sizeof 0x478; energy weapon |
|
||||||
| 0xBCB | **AmmoBin** | — | `case`=JumpJet; sizeof 0x22C |
|
| 0xBCB | **AmmoBin** | — | `case`=JumpJet; sizeof 0x22C |
|
||||||
| 0xBCD | **ProjectileWeapon** | @4bc3fc | sizeof 0x448 (`case`≠ this — see VDATA.h) |
|
| 0xBCD | **ProjectileWeapon** | @4bc3fc | sizeof 0x448 (`case`≠ this — see VDATA.h) |
|
||||||
| 0xBCE | **GaussRifle** | @4bdcb4 | : Emitter; FireWeapon is a **no-op** in this build |
|
| 0xBCE | **GaussRifle** | @4bdcb4 | : Emitter; FireWeapon is a **no-op** in this build |
|
||||||
| 0xBD0 | **MissileLauncher** | @4bcff0 | sizeof 0x44C |
|
| 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` |
|
| 0xBD4 | **PPC** (: Emitter) | — | same FireWeapon as Emitter; `factory calls it GaussRifle` |
|
||||||
| 0xBD6 | **HUD** | — | alloc 0x2a4; `case`=MechDisplay-ish |
|
| 0xBD6 | **HUD** | — | alloc 0x2a4; `case`=MechDisplay-ish |
|
||||||
| 0xBD8 | **Searchlight** | — | `case`=LegSubsystem |
|
| 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 |
|
| +0x11c / +0x120 | `damageZoneCount` / `damageZones[]` | inherited Entity; Mech ctor populates the array |
|
||||||
| +0x124 / +0x128 | `subsystemCount` / `subsystemArray[]` | the [[subsystem-roster]] (NOT the segment table) |
|
| +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`. |
|
| +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 |
|
| +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) |
|
| +0x300 | segment table | skeleton `EntitySegment`s (muzzle sites, joints) |
|
||||||
| +0x37c | target world `Point3D` | aim source |
|
| +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
|
## 4. Damage delivery
|
||||||
|
|
||||||
- `Entity::TakeDamageMessage(id, size, inflictingEntityID, zone, Damage&)` → `target->Dispatch(&msg)`.
|
- `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]
|
- `class Damage { damageType(enum Collision/Ballistic/Explosive/Laser/Energy), damageAmount, damageForce, surfaceNormal, impactPoint, burstCount }`. [T1]
|
||||||
- Weapon effect id: **"explode" = 13** (`Explosion::Make`). [T2]
|
- 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]
|
- `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]
|
||||||
|
|||||||
@@ -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]
|
related_topics: [asset-formats, decomp-reference, reconstruction-gotchas, combat-damage, rendering]
|
||||||
key_terms: [RootTranslation, ground-model, SequenceController, MechControlsMapper, SLD, joint]
|
key_terms: [RootTranslation, ground-model, SequenceController, MechControlsMapper, SLD, joint]
|
||||||
open_questions:
|
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"
|
- "Airborne clip callbacks (FUN_004a7970) deferred"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+16
-6
@@ -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]
|
related_topics: [wintesla-port, combat-damage, locomotion, decomp-reference]
|
||||||
key_terms: [master, replicant, EGG, dead-reckon]
|
key_terms: [master, replicant, EGG, dead-reckon]
|
||||||
open_questions:
|
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
|
# 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
|
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 ×
|
**texture-scroll precision-collapse bug** in `L4D3D::SetTextureScrolling` (offset = `scrollDelta ×
|
||||||
absolute_time` → unbounded → UV precision loss → radial "spokes"), fixed with `fmodf`; that also
|
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
|
cleans the scrolling beam grit. See [[reconstruction-gotchas]] §13. PEER WARP IS WIRED + VISIBLE
|
||||||
path (see OTHERS' spheres) still needs player SimulationState/DropZoneLocation replication — until
|
(`160b78e`): an observer sees a peer's un-wreck warp via the world-anchored `BTStartWarpEffect`
|
||||||
then the local-player sphere stands in; and confirm the COLLAPSE (death-side) warp fires on every
|
(mechdmg.cpp:1074, `ReplicantInstance`-gated; `simulationState` rides every record header).
|
||||||
MP death path (the force-damage test logged EXPAND reliably, COLLAPSE less so).
|
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`
|
NOTE the respawn-cycle SIM path is separate + done: `DropZoneReply` (FUN_004bffd0) → `Mech::Reset`
|
||||||
(@0049fb74) + the level-2 "translocated" cockpit alarm; `CreatePlayerVehicle` (FUN_004bfcac,
|
(@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
|
disassembled — make+link only, matches RP analog). The sphere is a pure RENDER-side watcher on the
|
||||||
SimulationState dial, independent of that sim path.
|
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):**
|
**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
|
(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,
|
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.
|
- 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.
|
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
@@ -154,6 +154,17 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
|
|||||||
centred); (c) pre-burial, the pick still
|
centred); (c) pre-burial, the pick still
|
||||||
tests the sinking wreck's collision box at its parked position (pre-existing wreck-targeting
|
tests the sinking wreck's collision box at its parked position (pre-existing wreck-targeting
|
||||||
behavior).
|
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 +
|
- **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
|
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
|
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,
|
Also: `deathAnimationLatched`/`legResetLatch` were never ctor-initialised (0xCDCDCDCD gate bug,
|
||||||
fixed in mech.cpp ctor). See [[cockpit-view]].
|
fixed in mech.cpp ctor). See [[cockpit-view]].
|
||||||
- **MechControlsMapper look/eyepoint commit** — reconstructed but its offsets collide with declared
|
- **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
|
members; arbitrate before enabling (no port consumer yet). (The fire-trigger-through-mapper is NO
|
||||||
still the `gBTWeaponTrigger` bring-up.
|
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].**
|
- **✅ 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
|
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);
|
`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) +
|
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,
|
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
|
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;
|
as the authored-constant simulation predicts (thermal spam is unsustainable BY DESIGN; the
|
||||||
heatLoad -> effectiveRange 0 = the authentic overheat cutout).
|
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
|
- **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
|
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,
|
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.
|
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
|
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.
|
logged skip — none of the 8 mechs' streams crit-list them in practice.
|
||||||
- **Two dead Wword comparison branches (multiplayer)**: `mech.cpp:1511` + `:1613` (replicant
|
- **✅ Two dead Wword comparison branches — DONE (task #1, `c9f0c2a`).** `Mech::ReadUpdateRecord`
|
||||||
leg-state/stability sync in ReadUpdateRecord) — always-false BTVal comparisons ([[reconstruction-gotchas]] §2);
|
case 3 now reads real named fields off `Mech__StateUpdateRecord` (legState / stability / legResetLatch /
|
||||||
needs slots 0xf/0x10 promoted to named members when P6 replication work resumes.
|
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)
|
## Multiplayer (Phase 7 / P6)
|
||||||
- **OPEN: console death froze peer replication BOTH ways (2026-07-14) [T4 mechanism].** Live 2-node
|
- **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).
|
- Feeds from: every subsystem/render topic (their deferral notes collect here).
|
||||||
- Gating master: [[source-completeness]] (the missing BT source).
|
- Gating master: [[source-completeness]] (the missing BT source).
|
||||||
|
|
||||||
## Peer coupled-motion architecture — COMPLETION PENDING (task #50 tail, 2026-07-14)
|
## Peer coupled-motion architecture — ✅ DONE (task #50, 2026-07-14/15) [T1/T2]
|
||||||
The user's speed-change peer glitch is the last visible MP motion artifact. Root
|
RESOLVED. (This block was the old COMPLETION PLAN from `ea39af1`; it was left stale after
|
||||||
cause is ARCHITECTURAL and decomp-confirmed: the original peer (FUN_004ab430 ->
|
the work actually landed — reconciled 2026-07-15.) The coupled single-source gait pipeline
|
||||||
FUN_004ab1c8) drove position FROM the clip's root travel between records (feet
|
is reconstructed, DEFAULT ON, and the residual was root-caused. See [[multiplayer]] two
|
||||||
locked to ground by construction) + pose-sync offset decay; our port dead-reckons
|
sections: **"Authentic coupled peer motion — DONE"** (linear position = BODY gait channel
|
||||||
position from velocity while legs run on commanded speed — they disagree exactly
|
integrated into `projectedOrigin@0x260`; angular = replicated-velocity slerp; master
|
||||||
during accel/decel. The PEER half is implemented env-gated (BT_ROOT_POS=1,
|
SEND-mirror runs the SAME `IntegrateMotion` predictor fed the last-sent `bodyTargetSpeed`)
|
||||||
mech4.cpp replicant branch) but measured worse alone: with sparse records the
|
and **"Peer motion: the 'random shakiness' is single-box packet jitter, NOT the game"**.
|
||||||
master's VELOCITY mirror under-sends for a gait-driven peer (11u snaps); with
|
- Landed: coupled path promoted to default (`c52a1ad` + `b013742`); peer poses from the
|
||||||
dense records the two authorities tug-of-war (path/net 1.86). COMPLETION NEEDS
|
BODY channel (`96a896a`); turn-step on the body channel (`f094d78`); true-mirror cadence
|
||||||
THE MASTER HALF: (1) master send-gate linear mirror driven by the same gait
|
driving the clip from actual replicated motion (`23f1532`). Groundwork: exact quaternion
|
||||||
travel (its own channel-B IntegrateMotion projection, as the binary's), so
|
integration (`a8eb8a4`), incremental peer heading (`d78e77b`), clock guard (`93456be`),
|
||||||
records model the peer; (2) then sparse authentic deadband cadence + offset
|
commanded-speed cadence feed (`6374efc`), Abs-macro fix (`7615ecd`).
|
||||||
decay. All fixed-and-committed groundwork: exact quaternion integration
|
- **The old "needs the MASTER HALF" conclusion was SUPERSEDED:** the leftover accel/decel
|
||||||
(a8eb8a4), incremental peer heading (d78e77b), clock guard (93456be), commanded-
|
shakiness was ROOT-CAUSED as single-box **packet jitter** (`49d73dc`) — two Debug nodes on
|
||||||
speed cadence feed (6374efc), Abs-macro fix (7615ecd). Speed model decomp-settled
|
one box batch TCP delivery, so records arrive bursty (BT_RXJIT burstiness 3-7x) and the peer
|
||||||
(2c6db6a): analog throttle, '5 speeds' false, throttleState@0x4a4 is actually the
|
dead-reckons across the gaps then snaps. Pinning each node to a disjoint core set
|
||||||
fall-surface material (RENAME PENDING).
|
(`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).
|
||||||
|
|||||||
@@ -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]
|
related_topics: [bgf-format, asset-formats, wintesla-port, locomotion, pod-hardware, cockpit-view]
|
||||||
key_terms: [L4D3D, material-ramp, BSL, LOD, DPL, beam]
|
key_terms: [L4D3D, material-ramp, BSL, LOD, DPL, beam]
|
||||||
open_questions:
|
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
|
# Rendering
|
||||||
|
|||||||
@@ -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]
|
related_topics: [decomp-reference, reconstruction-gotchas, combat-damage, gauges-hud]
|
||||||
key_terms: [subsystem, factory, ClassID, subsystem-roster, PoweredSubsystem, bridge]
|
key_terms: [subsystem, factory, ClassID, subsystem-roster, PoweredSubsystem, bridge]
|
||||||
open_questions:
|
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
|
# 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;
|
- **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
|
integrators + `FUN_004b2980` damage fan-out; joint dispatch from the Mech performance tail
|
||||||
@0x4aaf74/83; `BT_GYRO_LOG`/`BT_GYRO_TRACE`) — see [[cockpit-view]].
|
@0x4aaf74/83; `BT_GYRO_LOG`/`BT_GYRO_TRACE`) — see [[cockpit-view]].
|
||||||
- **DEFERRED** — SubsystemMessageManager 0xBD3 (WAVE 8 — a
|
- **✅ DONE (task #7, `afefaee`)** — SubsystemMessageManager 0xBD3 (a
|
||||||
**damage/explosion consolidation hub** cached to `Mech[0x10d]`=0x434; recon MISLABELS the slot
|
**damage/explosion consolidation hub** cached to `Mech[0x10d]`=0x434; the factory builds the REAL
|
||||||
`controlsMapper` + the drive squats there while the real mapper is roster slot 0, so wiring it needs
|
class and `mech.hpp` names the slot `messageManager` — the old `controlsMapper` mislabel is swept,
|
||||||
an untangle. NOT the valve/advanced-damage gate — that's the owning **BTPlayer** @mech+0x190, see
|
the real mapper is roster slot 0. NOT the valve/advanced-damage gate — that's the owning **BTPlayer**
|
||||||
[[combat-damage]]/[[open-questions]]). [T2]
|
@mech+0x190, see [[combat-damage]]/[[open-questions]]). [T2]
|
||||||
|
|
||||||
## The watcher electrical chain (task #57, 2026-07-13 — the torso power gate)
|
## 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
|
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]
|
related_topics: [rendering, multiplayer, combat-damage, reconstruction-gotchas, asset-formats, bgf-format]
|
||||||
key_terms: [material-ramp]
|
key_terms: [material-ramp]
|
||||||
open_questions:
|
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."
|
- "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]
|
## Trigger wiring (summary; full detail in [[multiplayer]]) [T2]
|
||||||
Local player's own death → `BTStartWarpCollapsePOV()` (btplayer.cpp `VehicleDeadMessageHandler`,
|
Local player's own death → `BTStartWarpCollapsePOV()` (btplayer.cpp `VehicleDeadMessageHandler`,
|
||||||
`deathCount==-1`, guarded `this==GetMissionPlayer()`). Respawn `DropZoneReply` → `BTStartWarpExpandPOV()`
|
`deathCount==-1`, guarded `this==GetMissionPlayer()`). Respawn `DropZoneReply` → `BTStartWarpExpandPOV()`
|
||||||
(local-guarded). `BTWarpForceUnmask()` on mission-end / no-DropZones. Observer replicant un-wreck fires
|
(local-guarded). `BTWarpForceUnmask()` on mission-end / no-DropZones. **Peer warp IS wired + visible
|
||||||
the world-anchored `BTStartWarpEffect(x,y,z)` (mechdmg.cpp). The peer path is a stand-in until player
|
(`160b78e`):** an observer sees a peer's death/respawn warp — the replicant un-wreck fires the
|
||||||
`SimulationState`/`DropZoneLocation` replication exists.
|
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]
|
## How it was solved (method) + verification [T2]
|
||||||
1. **Dump the real asset** (`dump_tsphere.py`) — established it's a bicone with cylindrical UVs, ending
|
1. **Dump the real asset** (`dump_tsphere.py`) — established it's a bicone with cylindrical UVs, ending
|
||||||
|
|||||||
@@ -1137,6 +1137,34 @@ BoxedSolid*
|
|||||||
return solid;
|
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*
|
BoxedSolidCollisionList*
|
||||||
|
|||||||
@@ -300,6 +300,16 @@ public:
|
|||||||
Line *line,
|
Line *line,
|
||||||
Entity *except_by
|
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*
|
BoxedSolidCollisionList*
|
||||||
CollideCenterOfMotion(
|
CollideCenterOfMotion(
|
||||||
Line *line,
|
Line *line,
|
||||||
|
|||||||
@@ -227,9 +227,43 @@ void
|
|||||||
// to terrain).
|
// to terrain).
|
||||||
{
|
{
|
||||||
extern Entity *gBTTerrainEntity;
|
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
|
if (gBTTerrainEntity == 0
|
||||||
&& entity->IsDerivedFrom(*Terrain::GetClassDerivations()))
|
|| !gBTTerrainEntity->IsDerivedFrom(*Terrain::GetClassDerivations()))
|
||||||
gBTTerrainEntity = entity;
|
{
|
||||||
|
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(
|
DPLRenderer::MakeEntityRenderables(
|
||||||
entity, model_resource, view_type);
|
entity, model_resource, view_type);
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ struct VgMesh
|
|||||||
{
|
{
|
||||||
std::vector<VgTri> tris;
|
std::vector<VgTri> tris;
|
||||||
float minx, maxx, minz, maxz; // model-local XZ bound
|
float minx, maxx, minz, maxz; // model-local XZ bound
|
||||||
|
float miny, maxy; // model-local Y bound (dome vs flat-ground test)
|
||||||
};
|
};
|
||||||
|
|
||||||
struct VgInst
|
struct VgInst
|
||||||
@@ -89,8 +90,8 @@ int LoadMeshFor(const char *resource_name)
|
|||||||
if (LoadBgfFile(file, data) && data.ok && !data.indices.empty())
|
if (LoadBgfFile(file, data) && data.ok && !data.indices.empty())
|
||||||
{
|
{
|
||||||
VgMesh m;
|
VgMesh m;
|
||||||
m.minx = m.minz = 1e9f;
|
m.minx = m.minz = m.miny = 1e9f;
|
||||||
m.maxx = m.maxz = -1e9f;
|
m.maxx = m.maxz = m.maxy = -1e9f;
|
||||||
// The loader emits DOUBLE-SIDED triangles: emitTri pushes (a,b,c) then
|
// The loader emits DOUBLE-SIDED triangles: emitTri pushes (a,b,c) then
|
||||||
// (a,c,b) -- 6 indices per source triangle. Take the forward one.
|
// (a,c,b) -- 6 indices per source triangle. Take the forward one.
|
||||||
for (size_t i = 0; i + 5 < data.indices.size(); i += 6)
|
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]];
|
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 };
|
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);
|
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)
|
for (int k = 0; k < 3; ++k)
|
||||||
{
|
{
|
||||||
if (xs[k] < m.minx) m.minx = xs[k];
|
if (xs[k] < m.minx) m.minx = xs[k];
|
||||||
if (xs[k] > m.maxx) m.maxx = xs[k];
|
if (xs[k] > m.maxx) m.maxx = xs[k];
|
||||||
if (zs[k] < m.minz) m.minz = zs[k];
|
if (zs[k] < m.minz) m.minz = zs[k];
|
||||||
if (zs[k] > m.maxz) m.maxz = 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 (!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);
|
gMeshes.push_back(m);
|
||||||
idx = (int)gMeshes.size() - 1;
|
idx = (int)gMeshes.size() - 1;
|
||||||
}
|
}
|
||||||
@@ -151,16 +168,44 @@ bool BuildTables()
|
|||||||
const float *pos = (const float *)(base + p + 48);
|
const float *pos = (const float *)(base + p + 48);
|
||||||
p += (mlen + 3) & ~3u;
|
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 (mflags & 0x2) continue; // include marker
|
||||||
if (cls != 42) continue; // terrain/static cultural only
|
if (cls != 42) continue; // terrain/static cultural only
|
||||||
|
|
||||||
ResourceDescription *rd = rf->FindResourceDescription(rid);
|
ResourceDescription *rd = rf->FindResourceDescription(rid);
|
||||||
if (rd == 0) continue;
|
if (rd == 0) continue;
|
||||||
if (SkippedName(rd->resourceName)) { ++skipped; continue; }
|
|
||||||
|
|
||||||
int mi = LoadMeshFor(rd->resourceName);
|
int mi = LoadMeshFor(rd->resourceName);
|
||||||
if (mi < 0) { ++skipped; continue; }
|
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;
|
VgInst inst;
|
||||||
inst.mesh = mi;
|
inst.mesh = mi;
|
||||||
inst.px = pos[0]; inst.py = pos[1]; inst.pz = pos[2];
|
inst.px = pos[0]; inst.py = pos[1]; inst.pz = pos[2];
|
||||||
|
|||||||
@@ -271,6 +271,15 @@ void
|
|||||||
Scalar dist = (Scalar)Sqrt(delta.x*delta.x + delta.y*delta.y + delta.z*delta.z); // FUN_004dd138
|
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
|
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
|
if (dist <= effectiveRange) // this+0x328
|
||||||
{
|
{
|
||||||
// THE AUTHENTIC DAMAGE SUBMISSION (task #8; binary @004bace8:7758-7764):
|
// THE AUTHENTIC DAMAGE SUBMISSION (task #8; binary @004bace8:7758-7764):
|
||||||
@@ -349,6 +358,7 @@ void
|
|||||||
<< " pct=" << (float)rechargeLevel
|
<< " pct=" << (float)rechargeLevel
|
||||||
<< " tgt=" << (void *)((owner != 0)
|
<< " tgt=" << (void *)((owner != 0)
|
||||||
? *(Entity **)((char *)owner + 0x388) : 0)
|
? *(Entity **)((char *)owner + 0x388) : 0)
|
||||||
|
<< " explID=" << (long)GetExplosionResourceID()
|
||||||
<< std::endl;
|
<< std::endl;
|
||||||
}
|
}
|
||||||
targetWithinRange = UpdateTargeting(); // @004b9bdc -> this+0x34c
|
targetWithinRange = UpdateTargeting(); // @004b9bdc -> this+0x34c
|
||||||
|
|||||||
@@ -539,6 +539,37 @@ Logical
|
|||||||
return True;
|
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 roster torso (sinkSourceSubsystem @0x438, ClassID 0xBC5) -- parity with
|
||||||
// the binary's *(mech+0x438).
|
// the binary's *(mech+0x438).
|
||||||
@@ -916,6 +947,7 @@ Mech::Mech(
|
|||||||
poseSyncLatch = 0; // @0x77c (was absorbed Wword(0x1df))
|
poseSyncLatch = 0; // @0x77c (was absorbed Wword(0x1df))
|
||||||
heatLevelSnapshot = 0; // @0x780 (type-7 deadband baseline)
|
heatLevelSnapshot = 0; // @0x780 (type-7 deadband baseline)
|
||||||
mirrorBodyAdv = 0.0f; // master send-mirror gait travel
|
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)
|
angMirrorYaw = 0.0f; // scalar peer-yaw mirror (port addition)
|
||||||
angMirrorRate = 0.0f;
|
angMirrorRate = 0.0f;
|
||||||
angMirrorValid = 0;
|
angMirrorValid = 0;
|
||||||
@@ -1789,6 +1821,33 @@ Logical
|
|||||||
void
|
void
|
||||||
Mech::ReadUpdateRecord(Simulation::UpdateRecord *message)
|
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;
|
static const int s_mrecLog = getenv("BT_REPL_LOG") ? 1 : 0;
|
||||||
if (s_mrecLog && message->recordID != 0)
|
if (s_mrecLog && message->recordID != 0)
|
||||||
DEBUG_STREAM << "[mrec-rx] type=" << (int)message->recordID
|
DEBUG_STREAM << "[mrec-rx] type=" << (int)message->recordID
|
||||||
|
|||||||
@@ -631,6 +631,12 @@ public:
|
|||||||
// predicts (gait), instead of the constant-velocity deadReckoner. This is the
|
// predicts (gait), instead of the constant-velocity deadReckoner. This is the
|
||||||
// authentic FUN_004a9b5c mirror = IntegrateMotion(mj=0) on projectedOrigin.
|
// authentic FUN_004a9b5c mirror = IntegrateMotion(mj=0) on projectedOrigin.
|
||||||
Scalar mirrorBodyAdv; // body cycleDistance this frame (master)
|
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 angMirrorYaw; // yaw at the last type-4 send (rad)
|
||||||
Scalar angMirrorRate; // yaw rate sent with it (rad/s)
|
Scalar angMirrorRate; // yaw rate sent with it (rad/s)
|
||||||
Time angMirrorTime; // lastPerformance at that send
|
Time angMirrorTime; // lastPerformance at that send
|
||||||
@@ -687,6 +693,14 @@ public:
|
|||||||
Logical PickRayHit(const Point3D &start, const Vector3D &dir,
|
Logical PickRayHit(const Point3D &start, const Vector3D &dir,
|
||||||
Scalar max_range, Point3D *hit_world);
|
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
|
// Per-weapon beam render walk (task #33; extracted task #51 so it runs for
|
||||||
// EVERY mech -- player, dummy masters, and MP replicants whose emitters
|
// EVERY mech -- player, dummy masters, and MP replicants whose emitters
|
||||||
// carry replicated discharge state). Defined in mech4.cpp.
|
// carry replicated discharge state). Defined in mech4.cpp.
|
||||||
|
|||||||
@@ -999,6 +999,9 @@ Scalar
|
|||||||
if (bodyCycleSpeed > bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed;
|
if (bodyCycleSpeed > bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed;
|
||||||
if (bodyCycleSpeed > walkStrideLength) bodyCycleSpeed = walkStrideLength;
|
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(
|
distance = bodyAnimation.Advance(
|
||||||
time_slice * (bodyCycleSpeed / walkStrideLength) * globalTimeScale, loop);
|
time_slice * (bodyCycleSpeed / walkStrideLength) * globalTimeScale, loop);
|
||||||
break;
|
break;
|
||||||
@@ -1019,6 +1022,9 @@ Scalar
|
|||||||
if (bodyCycleSpeed > bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed;
|
if (bodyCycleSpeed > bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed;
|
||||||
if (bodyCycleSpeed > reverseSpeedMax2) bodyCycleSpeed = reverseSpeedMax2;
|
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(
|
distance = bodyAnimation.Advance(
|
||||||
time_slice * (bodyCycleSpeed / reverseStrideLength) * globalTimeScale, loop);
|
time_slice * (bodyCycleSpeed / reverseStrideLength) * globalTimeScale, loop);
|
||||||
break;
|
break;
|
||||||
@@ -1159,6 +1165,9 @@ Scalar
|
|||||||
if (bodyCycleSpeed > bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed;
|
if (bodyCycleSpeed > bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed;
|
||||||
if (bodyCycleSpeed > walkStrideLength) bodyCycleSpeed = walkStrideLength;
|
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(
|
distance = bodyAnimation.Advance(
|
||||||
time_slice * (bodyCycleSpeed / walkStrideLength) * globalTimeScale, loop);
|
time_slice * (bodyCycleSpeed / walkStrideLength) * globalTimeScale, loop);
|
||||||
break;
|
break;
|
||||||
@@ -1179,6 +1188,9 @@ Scalar
|
|||||||
if (bodyCycleSpeed > bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed;
|
if (bodyCycleSpeed > bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed;
|
||||||
if (bodyCycleSpeed > reverseSpeedMax2) bodyCycleSpeed = reverseSpeedMax2;
|
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(
|
distance = bodyAnimation.Advance(
|
||||||
time_slice * (bodyCycleSpeed / reverseStrideLength) * globalTimeScale, loop);
|
time_slice * (bodyCycleSpeed / reverseStrideLength) * globalTimeScale, loop);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -2148,6 +2148,35 @@ void
|
|||||||
// master's exact clips. Swapping the peer to the body channel RESTORES the
|
// 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.
|
// original single-channel design (no new netcode). BT_PEER_LEGCH=1 = old.
|
||||||
static const int s_peerLegCh = getenv("BT_PEER_LEGCH") ? 1 : 0;
|
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
|
const Scalar replLegAdv = s_peerLegCh
|
||||||
? AdvanceLegAnimation(dt) // old: re-derived leg SM
|
? AdvanceLegAnimation(dt) // old: re-derived leg SM
|
||||||
: AdvanceBodyAnimation(dt, 1); // authentic: body channel, mj=1 poses skeleton
|
: 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];
|
float rs[3], rd[3];
|
||||||
if (!BTGetAimRay(gBTAimX, gBTAimY, rs, rd))
|
if (!BTGetAimRay(gBTAimX, gBTAimY, rs, rd))
|
||||||
{
|
{
|
||||||
@@ -4327,16 +4389,62 @@ void
|
|||||||
hotPoint = hp;
|
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;
|
pickPoint = hotPoint;
|
||||||
++gAimHits;
|
++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
|
else
|
||||||
{
|
{
|
||||||
// the ground downrange of the guns (max = the pod's HUD
|
// no mech, no structure -> the flat ground downrange of the
|
||||||
// range scale; past it the shot is a sky shot -> no target)
|
// 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,
|
extern bool BTGroundRayHit(float,float,float, float,float,float,
|
||||||
float, float*,float*,float*);
|
float, float*,float*,float*);
|
||||||
float hx, hy, hz;
|
float hx, hy, hz;
|
||||||
@@ -4368,13 +4476,69 @@ void
|
|||||||
MECH_TARGET_SUBIDX(this) = -1;
|
MECH_TARGET_SUBIDX(this) = -1;
|
||||||
MECH_TARGET_POS(this) = pickPoint; // beam endpoint = the pick
|
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
|
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;
|
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
|
// HUD feeds: the range caret + the hotbox (world point + state) + the
|
||||||
// recovered-Execute instruments (compass, twist tape, group mask).
|
// recovered-Execute instruments (compass, twist tape, group mask).
|
||||||
// The range caret tracks the PICK (authentic: :5639 computes it from
|
// The range caret tracks the PICK (authentic: :5639 computes it from
|
||||||
@@ -4806,7 +4970,8 @@ void
|
|||||||
DEBUG_STREAM << "[target] aim=(" << gBTAimX << "," << gBTAimY << ")"
|
DEBUG_STREAM << "[target] aim=(" << gBTAimX << "," << gBTAimY << ")"
|
||||||
<< (victim != 0 ? " MECH under boresight (aimed)"
|
<< (victim != 0 ? " MECH under boresight (aimed)"
|
||||||
: (pickTarget != 0 ? " terrain downrange (beam at scenery)"
|
: (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
|
<< " range=" << range
|
||||||
<< (anyWeaponInRange ? " IN WEAPON RANGE" : "")
|
<< (anyWeaponInRange ? " IN WEAPON RANGE" : "")
|
||||||
<< " [mechPicks=" << gAimHits << " groundPicks=" << gAimGround
|
<< " [mechPicks=" << gAimHits << " groundPicks=" << gAimGround
|
||||||
|
|||||||
@@ -500,8 +500,21 @@ Logical
|
|||||||
rangeToTarget = (Scalar)Sqrt( // FUN_004dd138
|
rangeToTarget = (Scalar)Sqrt( // FUN_004dd138
|
||||||
delta.x * delta.x + delta.y * delta.y + delta.z * delta.z);
|
delta.x * delta.x + delta.y * delta.y + delta.z * delta.z);
|
||||||
|
|
||||||
// 1.0f == _DAT_004b9c98; heatLoad is the inherited HeatableSubsystem load.
|
// AUTHENTIC (decomp @004b9bdc:6983): effectiveRange = (1 - hostZoneDamage) *
|
||||||
effectiveRange = (1.0f - heatLoad) * weaponRange; // *0x32c
|
// 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;
|
targetWithinRange = (effectiveRange > rangeToTarget) ? True : False;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user