Torso: the TWIST goes LIVE -- electrical watchdog chain, centered crosshair, coherent controls (task #57/#58)
The MadCat torso twists, the view turns with it, and targeting follows. Three reconstruction fronts closed: THE ELECTRICAL WATCHDOG CHAIN (why the torso never powered up): - PowerWatcher::UpdateWatch reconstructed (@004b181c, the REAL registered Performance -- PTR @0050f5fc; Ghidra missed the fn start): the watchdog MIRRORS the watched subsystem's electrical level (+0x278), brownout downgrade when gen output <= minVoltage% x rated. @004b1804 relabeled ResetToInitialState (slot 10) -- the old "Simulation" tag was wrong. - The factory watcher-CONNECT pass reconstructed (vtable slot +0x38, @004aee2c/@004b1a40 byte-identical, recovered from raw exe bytes): watchedLink.Add(roster[watchedSubsystem]) on the master node. Was the SubProxy::Start() no-op -- every watchdog sat at 0 forever. - MinVoltageScale = 0.01 (a 10-byte x87 literal @0x4b1924; was 1.0f = permanent brownout) and PowerWatcher's Derivation chains its REAL base HeatWatcher (the HeatableSubsystem stand-in broke IsDerivedFrom for the whole Torso/Searchlight/ThermalSight family). - KB correction swept: derivation tag 0x50e604 = HEATWATCHER (not "HeatSink"); the btl4gaug heat-widget gate now tests it via the BTIsHeatWatcher bridge. THE CROSSHAIR (task #58 forensics, 6-agent workflow + live probes): - The VIEW is TORSO-MOUNTED: jointtorso -> jointeye -> siteeyepoint in every twist-capable .SKL; the camera + canopy ride the same hinge subtree through HingeRenderable's live matrix-stack compose -- ALREADY WORKING in the port. The crosshair stays screen-centered (center IS the boresight); the twist reads on the tape carets/compass/radar. - The real bug was the port's gBTAimX = tan(twist) slew (the falsified "body-mounted view" model): the camera already carried the twist, so the crosshair counter-slid to hull-forward and the fire ray with it. Deleted; the pick ray inherits the twist from the yawing eye basis. - Two instrumentation traps documented (chase-eye-as-default-camera, BT_FORCE_TORSO clobbering real joints -> the hook now only fills unresolved ones); an over-correcting explicit eye compose was added on those false readings and retired the same day. CONTROLS + REPLICATION: - Q/E spring-center on release (the axis is a twist-RATE demand; the old hold-deflection model drifted forever); X also zeroes the axis and pulses the authentic torso Recenter (@004b6918). M cycles control mode via the real CycleControlMode body. - Torso update-record DIRECTION fixed: engine truth is Write=serialize / Read=apply; @004b6a78 is the READ (was mislabeled Write) and the missing WRITE @004b6a1c recovered from raw disasm (recordLength 0x1C, twist/vel/rate at +0x10/14/18) -- kills the replicant's 0xCDCDCDCD -140-degree ghost twist. - Marching-ghost desync: 4 Standing-case guards zero stale reverse cycleSpeed (negative cadence passed the <= ZeroSpeed stop gate). - Kill credit rerouted to the OBSERVED killer (lastInflictingID -> killer's player link) -- kills count, target K/D populates. KB: subsystems.md (watcher chain), multiplayer.md (record direction), combat-damage.md + gauges-hud.md + cockpit-view.md (torso-mounted view re-correction), decomp-reference.md (new addresses + tag fix), open-questions.md (dead capability-roster loops 2-4, snapshot CD read). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
065c114590
commit
02cdfd6576
+14
-1
@@ -61,7 +61,20 @@ inverse(eyeWorld)`. No LookAt anywhere.
|
||||
- offset matrix = the segment's **GetBaseOffset()** (segment+0x74, the LOCAL rest transform) —
|
||||
NOT GetSegmentToEntity, NOT an upright basis;
|
||||
- parent = the eyepoint segment's **parent draw component** (`dcs_array[GetParentIndex()]`) —
|
||||
liveness (torso pose, gait, gyro) reaches the eye ONLY through this parent chain;
|
||||
liveness reaches the eye through this parent chain in the PORT TOO (task #58 FINAL): the
|
||||
twist path is `Torso::PushTwist → Joint::SetRotation → hinge rotationAmount →
|
||||
HingeRenderable::Execute` (multiplies the live hinge into the matrix stack), and the eye
|
||||
executes inside that scope (`jointtorso → jointeye → siteeyepoint`; VERIFIED: with the real
|
||||
jointtorso swept, the active cockpit `[eyefwd]` swings ~60° and the torso hinge renderable
|
||||
sits in the eye's parent chain). The CAMERA yaws with the twist, the canopy rides the same
|
||||
subtree, the crosshair stays screen-centered — see [[combat-damage]] Targeting.
|
||||
⚠ TWO instrumentation traps produced false "eye is frozen" readings first: (1) the headless
|
||||
default camera is the CHASE eye (root-parented, same DPLEyeRenderable class — gate probes on
|
||||
the COCKPIT eye being live); (2) `BT_FORCE_TORSO` used to override the joint names with the
|
||||
BLH demo pair (`jointshakey2`/`jointtshadow`), silently redirecting a MadCat sweep into the
|
||||
SHADOW hinge — the hook now only fills joints the resource left unresolved. An explicit
|
||||
eye-side twist compose added on those false readings DOUBLE-ROTATED the view (2× vs the 1×
|
||||
canopy) and is retired;
|
||||
- the `EyepointRotation` entity attribute (EulerAngles; Mech member, init 0).
|
||||
- **Combine order:** `FUN_0040b104(dst,A,B)` computes `dst = B × A` (translation from B) →
|
||||
the eye local = `baseOffset × R`, R applied FIRST in eye-local space. Recomputed only when the
|
||||
|
||||
+39
-23
@@ -32,29 +32,45 @@ player input). [T1]
|
||||
`mech+0x38c` = targeted sub-zone (−1=whole). Weapons cache hasTarget/targetPoint/muzzlePoint,
|
||||
refreshed each frame. [T1]
|
||||
|
||||
**The AUTHENTIC acquisition model (recovered 2026-07-08; corrected 2026-07-08, task #39):** THERE
|
||||
IS NO FREE-AIM MOUSE CURSOR. The pod had a stick whose yaw drove the **torso twist**
|
||||
(`MechControlsMapper` writes `HUD::SetFreeAimSlew(stick_x)` → cockpit+0x28C, gated on the torso's
|
||||
horizontal-twist being enabled — `mechmppr.cpp:735/762`, hud.hpp:167 [T1]); the crosshair marks
|
||||
where the **torso guns boresight** relative to the body-mounted view. You aim by **steering the
|
||||
mech / twisting the torso**, not by moving a cursor. The engine `Reticle` struct
|
||||
(`engine/MUNGA/RETICLE.h`) carries `reticlePosition`, `pickPointingOn`, `rayIntersection`,
|
||||
`targetEntity`, `targetDamageZone` — a general struct shared with Red Planet — but in BT
|
||||
`reticlePosition`/HotBoxVector (@mech-HUD+0x1FC) is **COMPUTED by HudSimulation** from the mech
|
||||
pose quaternion (`FUN_0040954c` quat→euler of mech+0x10C) + the target geometry, and **zeroed to
|
||||
centre when there is no target** (part_013.c:5680, `FUN_00408440(...,&DAT_004e0f74)` [T1]) — it is
|
||||
NOT a free-floating cursor. The pick then hits whatever the guns point at; that entity + its
|
||||
DAMAGE ZONE become the target (`0x37c` rayIntersection, `0x388` targetEntity, `0x38c`
|
||||
targetDamageZone → aimed-zone damage). Fixed-torso mechs (the BLH: `TorsoHorizontalEnabled=0`)
|
||||
boresight dead-ahead — the crosshair sits centred and you point the whole mech. 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 (the per-frame reticle→mech copy) is in an un-exported decomp gap.
|
||||
**Port status: RECONSTRUCTED + runtime-verified (tasks #36/#39, 2026-07-08) [T2].** The chain:
|
||||
the crosshair = **torso boresight** (`BTTwistToReticleX(torsoTwist)`, L4VIDEO — tan(twist)
|
||||
projected through the live per-axis projection; **0 = dead-centre on the fixed-torso BLH**;
|
||||
`BT_AIM="x y"` pins it for headless tests) → `BTGetAimRay` (the ACTIVE eye publishes pos+LookAtRH
|
||||
basis via `BTSetAimCamera`, the render loop publishes proj._11/_22 + backbuffer dims) →
|
||||
**The AUTHENTIC acquisition model (recovered 2026-07-08; task #39; RE-CORRECTED 2026-07-13, task
|
||||
#58 crosshair-twist forensics):** THERE IS NO FREE-AIM MOUSE CURSOR — and **the VIEW is
|
||||
TORSO-MOUNTED, not body-mounted**. The cockpit eye hangs off the torso yaw hinge in every
|
||||
twist-capable skeleton (`jointtorso` (hinge-Y) → `jointeye` → `siteeyepoint`; all 14 twist-enabled
|
||||
Torso records name `TorsoHorizontalJoint=jointtorso`; `Torso::UpdateJoints` writes currentTwist
|
||||
into that joint each frame; VIEW = inverse of the LIVE joint chain, `FUN_004c22c4`
|
||||
part_013.c:11742, no de-twist anywhere [T1]). So when the torso twists, **the CAMERA yaws with
|
||||
the guns and the crosshair stays SCREEN-CENTERED** — truthfully, because screen center always IS
|
||||
the boresight; the world rotates past it. The twist reads on INSTRUMENTS: the bottom 21-tick tape
|
||||
carets, the compass (= body yaw + twist, part_013.c:5674-5676), the radar wedge
|
||||
(part_013.c:11836-11859) [T1]. The old "crosshair = torso boresight offset in a body-mounted
|
||||
view" model was falsified — it was never discriminated before because every prior live test used
|
||||
the fixed-torso BLH, where both models center the crosshair.
|
||||
The engine `Reticle` struct (`engine/MUNGA/RETICLE.h`) carries `reticlePosition` (a variable
|
||||
screen position −1..+1 [T0]) — the reticle CAN move, but its writer is in an un-exported decomp
|
||||
gap (`s_TargetReticle` has exactly ONE xref binary-wide: the read-side lookup part_014.c:5132);
|
||||
the only coherent twist-era mover is the **fixed-torso free-aim channel**: `MechControlsMapper`
|
||||
writes `HUD::SetFreeAimSlew(stick_x)` → cockpit+0x28C **only when torso twist is disabled**
|
||||
(part_013.c:339-345/381-387 [T1]); HudSimulation integrates it (negated, part_013.c:5717) into
|
||||
+0x294 clamped ±(+0x29C) → `mech+0x36c` every frame (:5735) — no exported consumer; the
|
||||
mech+0x36c → reticlePosition link is [T4]. NB: an earlier version of this section conflated
|
||||
`reticlePosition` with **HotBoxVector** (HUD+0x1FC — the TARGET designator HudSimulation zeroes
|
||||
with no target, part_013.c:5680); they are different things.
|
||||
The pick hits whatever the guns point at; that entity + its DAMAGE ZONE become the target
|
||||
(`0x37c` rayIntersection, `0x388` targetEntity, `0x38c` targetDamageZone → aimed-zone damage).
|
||||
Fixed-torso mechs (the BLH: `TorsoHorizontalEnabled=0` — no jointtorso in the skeleton at all)
|
||||
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.
|
||||
**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
|
||||
stack; the eye executes under `jointtorso → jointeye`; verified `[eyefwd]` sweeps with the real
|
||||
jointtorso — NO explicit eye-side compose exists, one briefly added on false "frozen eye"
|
||||
probe readings double-rotated the view and is retired, see [[cockpit-view]]); the crosshair sits
|
||||
at screen center (`gBTAimX = 0` — the old `BTTwistToReticleX` slew was THE bug: the camera
|
||||
already yawed, so the crosshair counter-slid to HULL-forward, "twisting leaves the crosshairs
|
||||
behind"; `BT_AIM="x y"` still pins it for headless tests) → `BTGetAimRay` (the ACTIVE eye
|
||||
publishes pos+basis via `BTSetAimCamera`, leveled in pitch, the twist yaw riding in the basis) →
|
||||
`Mech::PickRayHit` (world ray → local frame → `BoundingBox::HitBy` slab test vs the collision
|
||||
template's ExtentBox → world hit point).
|
||||
|
||||
|
||||
@@ -83,12 +83,24 @@ Identify a class at runtime with `sub->GetClassID()` (== `sub+4`). [T1]
|
||||
| **0x511830** | **MechWeapon**::ClassDerivations | The reticle AddWeapon loop (part_014.c:5386) hard-aborts on missing WeaponRange/PipPosition/… attrs for every member; part_012.c:9344 counts ready weapons; the Mech ctor roster at this[0x1ef] (`weaponRoster`, ex-mislabel "poweredSubsystems") ORs weapon `capabilityFlags`@+0x334. [T1] |
|
||||
| 0x50f4bc | PoweredSubsystem | btl4gau2 gauge wave. [T1] |
|
||||
| 0x50e3ec | HeatableSubsystem | btl4gaug widget gate. [T1] |
|
||||
| 0x50e604 | HeatSink | btl4gaug widget gate. [T1] |
|
||||
| 0x50e604 | **HeatWatcher** (task #57 CORRECTION — was "HeatSink") | `HeatWatcher::TestInstance` @4aec38 tests its OWN class vs this tag; the factory watcher-CONNECT loop (slot +0x38) selects on it; the btl4gaug widget gate ORs it after HeatableSubsystem (a HeatSink test there would be redundant — HeatSink : HeatableSubsystem). 0x50e590 = the HeatSinkBank (0xBBE) tag. [T1] |
|
||||
| 0x51155c | roster at Mech this[0x1eb] (`heatableSubsystems` label) | tag ≠ 0x50e3ec — the label is UNVERIFIED [T4]; arbitrate before relying on it. |
|
||||
| 0x50e4fc | roster at Mech this[499] (`damageableSubsystems` label) | [T3] |
|
||||
| 0x50bdb4 | Mech | emitter.cpp destroyed-check. [T1] |
|
||||
| 0x5121a8 | ProjectileWeapon-family (checked after 0x511830; reads ammo state @+0x364) | [T3] |
|
||||
|
||||
### Watcher electrical chain (task #57 — all [T1], several from RAW-BYTE recovery)
|
||||
| Address | What |
|
||||
|---|---|
|
||||
| @004aec54 | `HeatWatcher::CreateStreamedSubsystem` — parses `WatchedSubsystem=<name>` → segment index **+2** at resource+0xE4 |
|
||||
| @004aee2c / @004b1a40 | vtable slot 14 (+0x38) CONNECT bodies (HeatWatcher / PowerWatcher+Torso, byte-identical): master-gated `watchedLink(+0x114).Add(owner->roster[watchedSubsystem(+0x128)])`. **Ghidra missed both starts** — recovered from raw exe bytes (vtables.tsv rows have GAPS at skipped slots; dump vtable+slot*4 bytes when a slot looks absent) |
|
||||
| @004b181c | `PowerWatcher::UpdateWatch` — THE registered Performance (PTR @0050f5fc); watchdogAlarm(+0x184) MIRRORS watched subsystem's electrical level (+0x278); brownout→1 when `gen outputVoltage(+0x1DC) <= minVoltage(+0x180) × ratedVoltage(+0x1D8)`. Torso sims call it first-line |
|
||||
| @004b1804 | `PowerWatcher::ResetToInitialState` (slot 10) — old recon MISLABELED it "Simulation" |
|
||||
| 0x4b1924 | `MinVoltageScale` — a **10-byte x87 literal** (`0a d7 a3 70 3d 0a d7 a3 f8 3f` = **0.01**); the port had 1.0f → permanent brownout |
|
||||
| @004b6a78 | `Torso::ReadUpdateRecord` (slot 6; base 41bd34) — REPLICANT apply of twist extras +0x10/14/18. Old recon mislabeled it Write |
|
||||
| @004b6a1c | `Torso::WriteUpdateRecord` (slot 7; base 41c500) — MASTER serialize; recordLength=0x1C; **Ghidra missed the start** |
|
||||
| 41bd34 / 41c500 | base Subsystem Read (apply) / Write (produce) update-record fns — engine [T0]: **Write=produce, Read=apply**; don't flip |
|
||||
|
||||
---
|
||||
|
||||
## 3. Mech struct offsets (`Mech*`, binary layout)
|
||||
|
||||
+10
-3
@@ -146,7 +146,8 @@ queue. `SeekVoltageGraph`'s 4 Seek* attrs are a cluster-child (not config-called
|
||||
view only, via the recovered **dpl2d** 2D display-list API (recorders @0x487f34-0x488630; opcode
|
||||
map + coordinate model in `phases/phase-02-dpl2d-reticle.md`; port: `game/reconstructed/dpl2d.cpp`).
|
||||
Geometry is the ctor's hardcoded calibration (originX 0.35, originY 0.25, scaleY 0.5, right range
|
||||
ladder 0..1200 m, bottom heading tape, center cross + dot; tick ladders via FUN_004cd938). The
|
||||
ladder 0..1200 m, bottom TORSO-TWIST tape (NOT a heading tape — stale wording swept task #58; see
|
||||
the tape entry below), center cross + dot; tick ladders via FUN_004cd938). The
|
||||
range caret binds to the live target range (`BTSetHudTargetRange`, fed by mech4's targeting step).
|
||||
**Weapon pips:** the build loop (part_014.c:5386) registers EVERY subsystem
|
||||
`IsDerivedFrom(0x511830 = MechWeapon)` — lasers, PPCs AND missile launchers (BLH = 7 pips) — via
|
||||
@@ -196,8 +197,14 @@ and every instrument is now live [T2]:**
|
||||
around the hotbox point, +1/−11.5 vertical (K=2.8145 baked projection; the port uses the live
|
||||
per-axis projection) — switching to the left/right edge ARROW past ±1.6 or behind.
|
||||
- Reticle state Off/On + `PrimaryHudOn` (mask 0x20) picks full HUD vs the "simple X" list;
|
||||
the aim group translates by `reticlePosition` = the TORSO BORESIGHT (SetMatrix; NOT a free
|
||||
cursor — see [[combat-damage]] Targeting: the stick twists the torso, no mouse-aim exists).
|
||||
the aim group SetMatrix-translates by `Reticle::reticlePosition` (screen −1..+1 [T0]) every
|
||||
frame — but **NOT by torso twist** (task #58 CORRECTION): the VIEW is torso-mounted (the eye
|
||||
hangs off jointtorso → jointeye → siteeyepoint), so the crosshair stays SCREEN-CENTERED through
|
||||
a twist — screen center IS the boresight; the twist reads on the bottom tape/compass/radar
|
||||
wedge instead. The reticlePosition writer is un-exported (one xref binary-wide: the read-side
|
||||
lookup part_014.c:5132); its coherent use is the FIXED-torso free-aim channel (mech+0x36c
|
||||
[T4]). The old "translates to the torso boresight" wording here was the falsified
|
||||
body-mounted-view model — see [[combat-damage]] Targeting for the full re-correction.
|
||||
This recovery also CONFIRMS the HUD attr-table ids 4/5/6/8/0xA/0xB/0xC/0xD name↔use pairings
|
||||
(hud.hpp had flagged them uncertain). Deferred: PNAME1-8.bgf 3D marker chain. (The canopy shell
|
||||
is now authentic and shows by default — see [[cockpit-view]]; `BT_HIDE_COCKPIT=1` hides it.)
|
||||
|
||||
@@ -22,6 +22,21 @@ engine `AnimationInstance::Animate(dt, move_joints)` integrates `movement += Δt
|
||||
moves the body that far → feet plant by construction (no stride constant; k=1). Idle anims have
|
||||
z=0. The `.MOD` physics governed thrust VEHICLES, NOT walk gait. [T1]
|
||||
|
||||
## The MARCHING-GHOST desync — ROOT-CAUSED + FIXED (2026-07-13, live diagnosis) [T2]
|
||||
The long-parked "replicant walks in place while its master stands still" reproduced live with
|
||||
telemetry running: the master's `[gaitSM]` showed `state=0 cycleSpeed=-2.50725 kfCur=19` — BOTH
|
||||
channels stuck at a REVERSE cadence inside Standing. Mechanism: the walk-family stop gate
|
||||
(`cycleSpeed <= ZeroSpeed`, mech2.cpp) passes trivially for NEGATIVE (reverse) cadences, and
|
||||
several stand-entry paths (turn exit, terminal poses) never touch the cycle — so a reverse-stop
|
||||
enters Standing at full reverse cycle speed. The master looks still (case-0 never advances the
|
||||
clip; `kfCur` freezes) but the stale cycle REPLICATES, and the peer's replicant animates it =
|
||||
the marching ghost. A clean FORWARD stop decays the cycle first (`legSum ~3e-8` on the healthy
|
||||
node) — which is why the bug was intermittent and direction-dependent. FIX: all four
|
||||
Advance*Animation Standing cases zero their channel's cycle on entry + ForceUpdate(8) so the
|
||||
peer hears the stop immediately. (The signed-demand change (task #15) is what let negative
|
||||
cycles reach machinery written for forward decel.) The earlier "bob reduction + stutter"
|
||||
symptom family likely shares this root — re-observe after this fix before chasing separately.
|
||||
|
||||
## The two-channel gait (real controls)
|
||||
The BT `SequenceController` (`seqctl.cpp`, from `SelectSequence@004277a8`+`Advance@0042790c`) drives
|
||||
locomotion. Under `BT_REAL_CONTROLS` two channels run — **roles corrected 2026-07-09 (task #49) from
|
||||
|
||||
@@ -237,6 +237,30 @@ engine/port member. Wire sizes verified live (0x14/0x20/0x2c/0x78). Key mechan
|
||||
A TU-local stub type forced the `BTMapperSpeedDemandRaw(void*)` bridge (mech.cpp carries a local
|
||||
`struct MechControlsMapper` recon stub — typed cross-TU signatures mangle differently).
|
||||
|
||||
## Subsystem update-record DIRECTION + the Torso pair (task #57, 2026-07-13) [T0/T1]
|
||||
|
||||
**Engine semantics [T0 SIMULATE.cpp:270-297]: `WriteUpdateRecord(record, model)` PRODUCES the
|
||||
outgoing record from the object (master serialize; base sets `recordLength=sizeof`, stream
|
||||
advances by `recordLength` — variable-length records are the mechanism); `ReadUpdateRecord(record)`
|
||||
APPLIES the record into the object (replicant).** The names read backwards — do not flip them.
|
||||
Base fns in every subsystem vtable: slot 6 = 41bd34 (Read/apply), slot 7 = 41c500 (Write/produce,
|
||||
3 args).
|
||||
|
||||
- **Torso pair:** slot 6 `@004b6a78` = `ReadUpdateRecord` (apply: lastUpdateTime clock stamp +
|
||||
twistAtUpdate/twistVelocity/twistRate ← record +0x10/14/18). Slot 7 `@004b6a1c` =
|
||||
`WriteUpdateRecord` — **Ghidra missed the function start**; raw-disasm recovery: chains base,
|
||||
sets `recordLength=0x1C`, writes currentTwist/twistVelocity/twistRate → +0x10/14/18, snapshots
|
||||
`twistAtUpdate = currentTwist`.
|
||||
- **The bug this fixed:** the old recon had the APPLY body on the WRITE virtual (and no Write at
|
||||
all) — the master consumed its own uninitialized stream buffer (0xCDCDCDCD → twistAtUpdate =
|
||||
−4.316e8) and never serialized twist; the replicant MadCat pinned at −140° (the "torso ghost
|
||||
twist"). With the pair correct the replicant tracks `targetTwist = twistAtUpdate +
|
||||
twistRate·elapsed` cleanly (cur=0 when master still). [T2 live]
|
||||
- **Residual (authentic):** the initial full-state snapshot record does NOT carry the torso
|
||||
extras; the replicant's blind +0x10 read can pick up 0xCD fill ONCE at spawn. The binary does
|
||||
the same read; the twist clamp contains it (on the fixed-torso BLH it pins to ±0.01° —
|
||||
invisible). First real twist record overwrites it.
|
||||
|
||||
## Remaining (P6 phase 4 / Phase 7)
|
||||
The pod-LAN config (real IPs, bare-IP pilot entries); update-record velocity sourced from the body-channel
|
||||
projection (above). See [[open-questions]]. [T3]
|
||||
|
||||
@@ -21,7 +21,43 @@ authentic path scoped.
|
||||
- Pod specifics for Phase 8: the 7-monitor driver setup, the RIO cockpit I/O protocol, current
|
||||
Win10+wrapper pod config. Status: OPEN.
|
||||
|
||||
## BINARY-COVERAGE AUDIT discoveries (2026-07-13, 6-agent decomp census) [T1 verified-uncited]
|
||||
New unaccounted functionality no prior list knew (addresses verified absent from game/+context/+docs):
|
||||
- **`BTL4VideoRenderer::StartEntityEffectImplementation` @004d097c** (+helpers @4d0c14/@4d0c5c,
|
||||
~93 ln) — the per-zone EFFECT DISPATCHER, vtable-dispatched from the engine's
|
||||
StartEntityEffectMessageHandler (hence invisible to address-grep). Body fully mapped: zone
|
||||
position (+0x74), video index (+0xc4→+0xc0), jointToDCS socket (+0x3a4, in CLASSMAP),
|
||||
SearchList type 10, atoi-dispatch into the SAME effect-number bands the port already serves
|
||||
(>=1000 psfx ✓, 3-15 specialfx ✓, 104 SwapToWreck ✓). **The whole per-zone damage/destruction
|
||||
visual chain is dead without it. RECOMMENDED NEXT TARGET (S/M).**
|
||||
- **Six L4VIDRND effect-renderable bodies** @4540ac/45447c/455eb8/456cf0/4589e0/458e5c (~576 ln)
|
||||
incl. the 40-param ScalingExplosionRenderable ctor ExplosionScripts calls 6× — the live 1995
|
||||
bodies behind the STUBBED markers (wreck flames/booms land into these).
|
||||
- **FUN_00472480** (273 ln) — unnamed analog→discrete stepper Execute in the gauge band
|
||||
(hysteresis + key-repeat), no caller found; the one hole in "gauge system complete".
|
||||
Settle via vtable lookup (plausibly a config/roster scroller).
|
||||
- **FUN_00454a70** (193 ln) — the PNAME1-8/PLACE1-8.bgf MP name-billboard loader (the known
|
||||
"MP target identification" gap's exact function).
|
||||
- **Explosion::SplashDamage @0042fad0** — substantial, unreferenced, directly in the combat
|
||||
path; one-time diff vs engine EXPLODE.cpp would rule out 1995-vs-WinTesla falloff drift.
|
||||
- **The binary's own VelociRender BGF/BMF/BSL/VTX loader (~4,550 ln, dead code)** — a free
|
||||
ground-truth cross-check for [[bgf-format]]/[[asset-formats]] claims.
|
||||
Full ranked audit: the workflow output (2026-07-13); top-10 ranking cross-checked against this
|
||||
register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTORY in the KB
|
||||
(task #8 "landed" vs the kShotDamage=12 bring-up residue) — audit before reworking.
|
||||
|
||||
## Deferred subsystems / feeds (authentic path scoped, marked in code)
|
||||
- **Factory capability-roster loops 2-4 are STILL DEAD (task #57 discovery).** mech.cpp's
|
||||
post-roster loops add to `heatableSubsystems`(0x51155c)/`weaponRoster`(0x511830)/
|
||||
`damageableSubsystems`(0x50e4fc) through the local `SubProxy` stub whose `IsDerivedFrom`
|
||||
RETURNS 0 — so those three mech-level rosters stay EMPTY (whatever consumes them sees
|
||||
nothing; weapons demonstrably work through other paths, so audit consumers before assuming
|
||||
impact). Loop 1 (the watcher CONNECT, 0x50e604) was reconstructed for real in task #57 —
|
||||
use the same bridge pattern (family-side derivation test + typed body) for the other three. [T2]
|
||||
- **Initial-snapshot torso record carries no twist extras**: the replicant's blind +0x10 read
|
||||
can latch 0xCDCDCDCD ONCE at spawn until the first real twist record; clamps contain it
|
||||
(invisible on fixed-torso mechs). Binary-authentic blind read — fix only if a visibly wrong
|
||||
spawn pose ever surfaces. See [[multiplayer]]. [T2]
|
||||
- **✅ 0xBD3 SubsystemMessageManager — UNTANGLED + LIVE (task #7, 2026-07-11) [T1/T2].**
|
||||
Both halves landed: (1) the factory case builds the REAL messmgr (ctor @0049bca4, 0x130,
|
||||
static reconstruction messmgr.cpp) cached at `mech+0x434` — the binary-wide census found
|
||||
|
||||
@@ -55,6 +55,35 @@ Making a base byte-exact GROWS every subclass — they must be re-based TOGETHER
|
||||
an untangle. 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
|
||||
its electrical state. The full chain, byte-verified [T1]:
|
||||
- **Data:** the model entry `WatchedSubsystem=<name>` → segment index **+2** at resource+0xE4
|
||||
(`HeatWatcher::CreateStreamedSubsystem` @004aec54); the ctor @004aeb40 stores it at
|
||||
watcher+0x128 (`watchedSubsystem`).
|
||||
- **Bind (factory post-roster loop 1):** vtable slot 14 (+0x38) — `@004aee2c` (HeatWatcher) /
|
||||
`@004b1a40` (PowerWatcher/Torso override, byte-identical); **Ghidra missed both function
|
||||
starts** (recovered from raw bytes; the vtables.tsv rows have GAPS where the exporter skipped
|
||||
slots — dump the exe bytes at vtable+slot*4 when a slot looks missing). Master-gated
|
||||
(`(owner->simulationFlags & 0xC)==0 && (flags & 0x100)`); binds `watchedLink(+0x114).Add(
|
||||
owner->roster[+0x128][watchedSubsystem])`. Port: `BTWatcherWatchedIndex`/`BTWatcherBindTarget`
|
||||
bridges (heatfamily_reslice.cpp) called from the mech.cpp factory loop.
|
||||
- **Tick:** `@004b181c` = the REAL `PowerWatcher` Performance (PTR @0050f5fc → 004b181c) =
|
||||
`UpdateWatch()`: heat mirror (FUN_004aeac4) + `watchdogAlarm.SetLevel(watched->electrical
|
||||
level @+0x278)` + brownout downgrade to 1 when `gen->outputVoltage(+0x1DC) <= minVoltage(+0x180)
|
||||
× gen->ratedVoltage(+0x1D8)`. The Torso sims (@004b5cf0/@004b65f8) call it first-line.
|
||||
(`@004b1804` is slot-10 **ResetToInitialState**, NOT the Simulation — old recon mislabel, fixed.)
|
||||
- **MinVoltageScale = 0.01** — a 10-byte x87 literal at 0x4b1924 (`0a d7 a3 70 3d 0a d7 a3 f8 3f`);
|
||||
the port had 1.0f, making minVoltage 100× too big → the brownout latched every watchdog at 1.
|
||||
- **PowerWatcher::GetClassDerivations chains HeatWatcher** (real base) — the old HeatableSubsystem
|
||||
stand-in broke `IsDerivedFrom(HeatWatcher)` for Torso/Searchlight/ThermalSight and silently
|
||||
skipped them in the connect pass.
|
||||
- **Effect:** `Torso::ElectricalStateLevel()==Ready(4)` un-gates `effectiveTwistRate` — the MadCat
|
||||
torso twists at its authored 50°/s (±140° limits, roster 17 watching 15 → generator @10000V);
|
||||
the BLH is authentically fixed (`horizontalEnabled=0`, limits ±0.01°). [T2 live-verified]
|
||||
- **STILL DEAD:** factory loops 2-4 (heatable/weapon/damageable capability rosters) go through the
|
||||
`SubProxy` stub whose `IsDerivedFrom` returns 0 — they add NOTHING. See [[open-questions]].
|
||||
|
||||
## The four systemic checks (every subsystem)
|
||||
See [[reconstruction-gotchas]]: (1) shadowing (re-declared engine-base fields), (2) the `Wword` trap,
|
||||
(3) message-handler chaining, (4) entity validity. Plus resource-struct layout (must mirror the class
|
||||
|
||||
@@ -669,6 +669,12 @@ struct BTPfxEmitter
|
||||
float emitAccum; // fractional particles owed (rate * dt integration)
|
||||
int issued;
|
||||
int active;
|
||||
// ATTACHED effects (the @004d097c per-zone dispatcher): the emitter rides
|
||||
// the owner entity's SEGMENT -- zone smoke follows the walking mech, and
|
||||
// StopAllEntityEffects (@004d0c14: the respawn cleanup) kills every
|
||||
// emitter tagged to the entity. ownerTag==0 = a free world effect.
|
||||
void *ownerTag;
|
||||
int followSeg;
|
||||
};
|
||||
struct BTPfxParticle
|
||||
{
|
||||
@@ -798,6 +804,8 @@ void BTStartPfxFrame(int effect_number, float x, float y, float z,
|
||||
e.emitAccum = 1.0f; // first particle immediately
|
||||
e.issued = 0;
|
||||
e.active = 1;
|
||||
e.ownerTag = 0;
|
||||
e.followSeg = -1;
|
||||
gBTPfxEmitters.push_back(e);
|
||||
}
|
||||
|
||||
@@ -806,6 +814,52 @@ void BTStartPfx(int effect_number, float x, float y, float z)
|
||||
BTStartPfxFrame(effect_number, x, y, z, 0, 0, 0);
|
||||
}
|
||||
|
||||
// ATTACHED effect start (the @004d097c per-zone dispatcher): like
|
||||
// BTStartPfxFrame, but the emitter rides the owner entity's segment -- the
|
||||
// per-frame sim re-resolves the segment's world transform through the
|
||||
// game-side bridge below, so a damage-band smoke plume TRAILS the walking
|
||||
// mech instead of hanging at the hit point. rows9 = the initial 3x3 basis
|
||||
// (rows = local axes in world), pos = the segment's current world position.
|
||||
extern int BTResolveSegmentWorld(void *entity, int seg_index,
|
||||
float *pos3, float *rows9); // mech4.cpp (game side)
|
||||
|
||||
void BTStartPfxAttached(int effect_number, void *owner, int seg_index,
|
||||
float x, float y, float z, const float *rows9)
|
||||
{
|
||||
if (effect_number < 0 || effect_number >= BT_PFX_SLOTS)
|
||||
return;
|
||||
const BTPfxDef &d = gBTPfxDefs[effect_number];
|
||||
if (!d.valid)
|
||||
return;
|
||||
if (gBTPfxEmitters.size() > 256)
|
||||
return;
|
||||
BTPfxEmitter e;
|
||||
e.def = &d;
|
||||
e.pos = D3DXVECTOR3(x, y, z);
|
||||
e.ax = rows9 ? D3DXVECTOR3(rows9[0], rows9[1], rows9[2]) : D3DXVECTOR3(1, 0, 0);
|
||||
e.ay = rows9 ? D3DXVECTOR3(rows9[3], rows9[4], rows9[5]) : D3DXVECTOR3(0, 1, 0);
|
||||
e.az = rows9 ? D3DXVECTOR3(rows9[6], rows9[7], rows9[8]) : D3DXVECTOR3(0, 0, 1);
|
||||
e.emitAccum = 1.0f;
|
||||
e.issued = 0;
|
||||
e.active = 1;
|
||||
e.ownerTag = owner;
|
||||
e.followSeg = seg_index;
|
||||
gBTPfxEmitters.push_back(e);
|
||||
}
|
||||
|
||||
// StopAllEntityEffects (@004d0c14 analog): the firmware killed every effect
|
||||
// instance tagged (playerIdx<<16 .. |0xffff); the port kills every emitter
|
||||
// tagged to the entity (the respawn cleanup -- a respawned mech must not
|
||||
// trail its corpse's zone smoke). Emitted particles fade out naturally.
|
||||
void BTStopEntityPfx(void *owner)
|
||||
{
|
||||
if (owner == 0)
|
||||
return;
|
||||
for (size_t i = 0; i < gBTPfxEmitters.size(); ++i)
|
||||
if (gBTPfxEmitters[i].ownerTag == owner)
|
||||
gBTPfxEmitters[i].active = 0;
|
||||
}
|
||||
|
||||
// Spawn a few particles of a slot's effect DIRECTLY at a moving point (no
|
||||
// emitter instance) -- the per-frame projectile SMOKE TRAIL (psfx 0 = dsrm,
|
||||
// "the lrm smoke trail": its velocities stream +Z = BEHIND the round, so the
|
||||
@@ -832,6 +886,7 @@ void BTPfxTrailPuff(int effect_number, float x, float y, float z,
|
||||
D3DXVECTOR3 ay; D3DXVec3Cross(&ay, &az, &ax);
|
||||
e.ax = ax; e.ay = ay; e.az = az;
|
||||
e.emitAccum = 0.0f; e.issued = 0; e.active = 0;
|
||||
e.ownerTag = 0; e.followSeg = -1;
|
||||
for (int i = 0; i < count; ++i)
|
||||
BTPfxSpawn(e);
|
||||
}
|
||||
@@ -1064,6 +1119,18 @@ void BTDrawPfx(LPDIRECT3DDEVICE9 dev, const D3DXMATRIX *view, float dt)
|
||||
{
|
||||
BTPfxEmitter &e = gBTPfxEmitters[ei];
|
||||
if (!e.active) continue;
|
||||
// attached emitters ride their owner's segment (@004d097c dispatcher)
|
||||
if (e.ownerTag != 0)
|
||||
{
|
||||
float p3[3], r9[9];
|
||||
if (BTResolveSegmentWorld(e.ownerTag, e.followSeg, p3, r9))
|
||||
{
|
||||
e.pos = D3DXVECTOR3(p3[0], p3[1], p3[2]);
|
||||
e.ax = D3DXVECTOR3(r9[0], r9[1], r9[2]);
|
||||
e.ay = D3DXVECTOR3(r9[3], r9[4], r9[5]);
|
||||
e.az = D3DXVECTOR3(r9[6], r9[7], r9[8]);
|
||||
}
|
||||
}
|
||||
const BTPfxDef &d = *e.def;
|
||||
// CONTINUOUS emission at `rate` particles/second until maximum_issue is
|
||||
// exhausted. Data-verified semantics: in EVERY shipped .PFX,
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
// (Authentic path = the gyro-driven eye-joint DCS chain -- deferred.)
|
||||
float gBTEyeBobY = 0.0f;
|
||||
float gBTEyeSwayX = 0.0f; // lateral weight-shift (the walk "swagger"), same source
|
||||
// task #58: the player torso's live twist (rad), published by mech4.cpp's HUD
|
||||
// tick. NOT consumed by the eye anymore -- the cockpit eye inherits the twist
|
||||
// through the jointtorso HingeRenderable in the draw traversal (see the note in
|
||||
// DPLEyeRenderable::Execute). Kept published for diagnostics/correlation.
|
||||
float gBTEyeTwist = 0.0f;
|
||||
#pragma hdrstop
|
||||
|
||||
#include "l4vidrnd.h"
|
||||
@@ -1340,6 +1345,21 @@ void
|
||||
//
|
||||
if(oldHinge != *myHinge)
|
||||
{
|
||||
// task #58 probe (BT_HINGE_LOG): which hinge renderables actually see
|
||||
// their joint move (standing + BT_FORCE_TORSO => only the torso hinge
|
||||
// and any doors change). kids = child renderables that would inherit
|
||||
// the rotation through the stack.
|
||||
{
|
||||
static const int s_hl = getenv("BT_HINGE_LOG") ? 1 : 0;
|
||||
static int s_hln = 0;
|
||||
if (s_hl && (s_hln++ % 60) == 0)
|
||||
DEBUG_STREAM << "[hinge] this=" << (void*)this
|
||||
<< " hinge=" << (void*)myHinge
|
||||
<< " rot=" << (float)(Radian)(myHinge->rotationAmount)
|
||||
<< " kids=" << (int)(End() - Enumerate())
|
||||
<< " par=" << (void*)GetParentProbe()
|
||||
<< "\n" << std::flush;
|
||||
}
|
||||
oldHinge = *myHinge;
|
||||
hingeOffsetMatrix.BuildIdentity();
|
||||
//#if SINGLE_AXIS_HINGE
|
||||
@@ -5604,6 +5624,24 @@ void
|
||||
// that assumed +Z=forward/+Y=up (which mis-framed the cockpit); the look/up axes now
|
||||
// fall out of the eye's own basis. pos/at/up above are kept only for the aim-boresight.
|
||||
D3DXMATRIX eyeW = mat4.ToD3DMatrix();
|
||||
// task #58 NOTE (torso twist vs the camera): the COCKPIT eye needs
|
||||
// NO explicit twist compose -- it executes inside the mech's draw
|
||||
// traversal under jointtorso's HingeRenderable (Torso::PushTwist ->
|
||||
// Joint::SetRotation -> hinge rotationAmount; HingeRenderable
|
||||
// multiplies the live hinge into the matrix stack), so `prev` above
|
||||
// already carries the twist and the view yaws authentically with
|
||||
// the canopy. VERIFIED live: with the real jointtorso swept, the
|
||||
// active cockpit [eyefwd] swings ~60 deg and the torso hinge sits
|
||||
// in the eye's parent chain (kids=2). An explicit
|
||||
// W' = W*E^-1*R_y(twist)*E compose was briefly added here and
|
||||
// DOUBLE-ROTATED the cockpit view (2x view vs 1x canopy: "the HUD
|
||||
// passes through the cockpit frame") and swung the CHASE camera
|
||||
// (parented on the root, same class) with the twist ("torso fixed,
|
||||
// legs twisting"). Both "eye is frozen" measurements that motivated
|
||||
// it were instrumentation artifacts: (1) the chase eye was the
|
||||
// active camera in the headless default view; (2) BT_FORCE_TORSO
|
||||
// used to redirect the sweep into the SHADOW hinge (see the hook's
|
||||
// trap note in torso.cpp).
|
||||
D3DXMATRIX view;
|
||||
D3DXMatrixInverse(&view, NULL, &eyeW);
|
||||
|
||||
@@ -5618,6 +5656,30 @@ void
|
||||
D3DXMatrixInverse(&view, NULL, &eyeW);
|
||||
}
|
||||
}
|
||||
// TWIST-FOLLOW PROBE (task #58, BT_EYE_LOG): does the ACTIVE eye's
|
||||
// forward yaw with the torso twist? Correlate with [torso] twist.
|
||||
// Also print the STACK TOP the eye composed with (prev) -- its
|
||||
// translation tells whether the eye executed inside the full
|
||||
// parent-chain traversal (t ~= eye rest position) or with a bare
|
||||
// stack (t == entity origin / zero).
|
||||
{
|
||||
static const int s_eyeLog = getenv("BT_EYE_LOG") ? 1 : 0;
|
||||
static int s_eyeLogN = 0;
|
||||
if (s_eyeLog && myRenderer->mCamera == this && (s_eyeLogN++ % 120) == 0)
|
||||
{
|
||||
DEBUG_STREAM << "[eyefwd] this=" << (void*)this
|
||||
<< " fwd=(" << eyeFwdW.x << "," << eyeFwdW.y
|
||||
<< "," << eyeFwdW.z << ") stackT=(" << prev(3,0) << ","
|
||||
<< prev(3,1) << "," << prev(3,2) << ") chain:";
|
||||
HierarchicalDrawComponent *p = GetParentProbe();
|
||||
for (int pc = 0; p != NULL && pc < 8; ++pc)
|
||||
{
|
||||
DEBUG_STREAM << " " << (void*)p;
|
||||
p = p->GetParentProbe();
|
||||
}
|
||||
DEBUG_STREAM << "\n" << std::flush;
|
||||
}
|
||||
}
|
||||
if (dbgEyeExec < 8)
|
||||
{
|
||||
DEBUG_STREAM << "[EYECHK] eyeW=(" << eyeW._41 << "," << eyeW._42 << "," << eyeW._43
|
||||
@@ -5650,9 +5712,10 @@ void
|
||||
// on a slope (body pitched ~8 deg to conform to the ground) aim
|
||||
// its pick ray INTO the ground, short of a distant target
|
||||
// (mechPicks=0). Level the boresight: drop the view direction's
|
||||
// pitch and rebuild an upright basis (world +Y up). The reticle
|
||||
// X still carries the torso twist (BTTwistToReticleX); the reticle
|
||||
// Y carries any aim elevation. Falls back to the raw basis only
|
||||
// pitch and rebuild an upright basis (world +Y up). The torso
|
||||
// twist rides IN eyeFwdW now (task #58 eye compose above) --
|
||||
// yaw survives the leveling, so the centered crosshair's pick
|
||||
// ray follows the twist. Falls back to the raw basis only
|
||||
// if the view is (degenerately) near-vertical.
|
||||
// Derive the boresight from the AUTHORITATIVE view matrix
|
||||
// (eyeFwdW = the world look direction from the inverse view),
|
||||
|
||||
@@ -69,6 +69,9 @@ public:
|
||||
|
||||
virtual bool IsStatic() { return false; }
|
||||
|
||||
// task #58 diagnostics: expose the parent link so probes can walk the tree.
|
||||
HierarchicalDrawComponent *GetParentProbe() const { return m_parent; }
|
||||
|
||||
d3d_OBJECT *GetDrawObj() { return this->graphicalObject; }
|
||||
// Swap this component's drawable in place. Execute() re-reads graphicalObject
|
||||
// every frame, so changing it makes the segment draw a different mesh next
|
||||
|
||||
@@ -52,7 +52,8 @@
|
||||
// FUN_0044a5b4/5dc GraphicsViewRecord ctor/dtor
|
||||
// FUN_0044a650/630 GraphicsViewRecord Erase / Reset
|
||||
// DAT_00524e20 DebugStream (warning channel)
|
||||
// 0x50e3ec / 0x50e604 heat-subsystem class-derivation tables
|
||||
// 0x50e3ec / 0x50e604 heat class-derivation tags (HeatableSubsystem /
|
||||
// HEATWATCHER -- task #57 relabel; was "HeatSink")
|
||||
//
|
||||
// GraphicsView vtable slots used below (this+0x48):
|
||||
// +0x08 SetExtent +0x10 SetOrigin +0x18 SetColor +0x24 MoveTo
|
||||
@@ -363,9 +364,12 @@ void
|
||||
//
|
||||
// HeatConnection -- @004c3664 ctor / @004c3720 Transfer.
|
||||
// ctor: source = the heat subsystem; flag@0x14 records whether it is the
|
||||
// "primary" heat class (IsDerivedFrom 0x50e3ec) vs. the alternate (0x50e604);
|
||||
// destination@0x18. Transfer: no subsystem -> 100 ; subsystem operational
|
||||
// (+0x40 == 1) -> 100 ; else Round( Resolve(subsys+0x114 currentTemperature) ).
|
||||
// "primary" heat class (IsDerivedFrom 0x50e3ec HeatableSubsystem) vs. the
|
||||
// alternate (0x50e604 = HEATWATCHER, task #57 relabel); destination@0x18.
|
||||
// Transfer: no subsystem -> 100 ; subsystem operational (+0x40 == 1) -> 100 ;
|
||||
// else Round( Resolve(subsys+0x114) ) -- NB for a Heatable that offset is
|
||||
// currentTemperature, for a WATCHER it is watchedLink (the flag@0x14 likely
|
||||
// picks the read; the watcher path is unverified [T4]).
|
||||
// Used by ColorMapperHeat -- THIS is the heat gauge's data feed.
|
||||
//
|
||||
// All Transfer bodies are reproduced inside the owning gauge's notes; the
|
||||
@@ -925,8 +929,13 @@ ColorMapperHeat::ColorMapperHeat(
|
||||
// The subsystem MUST be heat-bearing (one of the two heat class
|
||||
// derivation tables) or the data feed is meaningless.
|
||||
//
|
||||
extern int BTIsHeatWatcher(Subsystem *sub); // heatfamily_reslice.cpp bridge
|
||||
if (!subsystem->IsDerivedFrom(*HeatableSubsystem::GetClassDerivations()) && // FUN_0041a1a4 (0x50e3ec)
|
||||
!subsystem->IsDerivedFrom(*HeatSink::GetClassDerivations())) // (0x50e604)
|
||||
!BTIsHeatWatcher(subsystem)) // (0x50e604 = HEATWATCHER -- task #57 relabel:
|
||||
// the old "HeatSink" reading was redundant after
|
||||
// the HeatableSubsystem test; the binary's
|
||||
// alternate branch is the disjoint WATCHER family
|
||||
// (ammo bins / torso carry a mirrored heatAlarm))
|
||||
{
|
||||
Verify(False); // FUN_0040385c -- "Bad subsystem type" BTL4GAUG.CPP:0x68a
|
||||
}
|
||||
|
||||
@@ -203,7 +203,8 @@
|
||||
// ColorMapperHeat -- the cockpit heat tint. ANCHOR @004c3f6c.
|
||||
// Connection: @004c3664 (reads HeatableSubsystem::currentTemperature@0x114
|
||||
// as a 0..100 percentage). The named subsystem must derive from a heat
|
||||
// class (IsDerivedFrom 0x50e3ec or 0x50e604) or the ctor asserts (line 0x68a).
|
||||
// class (IsDerivedFrom 0x50e3ec HeatableSubsystem or 0x50e604 HEATWATCHER
|
||||
// -- task #57 relabel) or the ctor asserts (line 0x68a).
|
||||
//
|
||||
class ColorMapperHeat :
|
||||
public ColorMapper
|
||||
|
||||
@@ -49,6 +49,9 @@
|
||||
#if !defined(MECHWEAP_HPP)
|
||||
# include <mechweap.hpp> // MechWeapon::GetClassDerivations (reticle pip)
|
||||
#endif
|
||||
#if !defined(MECHDMG_HPP)
|
||||
# include <mechdmg.hpp> // Mech__DamageZone::segmentIndex (the @004d097c dispatcher)
|
||||
#endif
|
||||
#if !defined(NOTATION_HPP)
|
||||
# include <notation.hpp>
|
||||
#endif
|
||||
@@ -368,6 +371,22 @@ HierarchicalDrawComponent*
|
||||
LinearMatrix offset_matrix;
|
||||
offset_matrix = segment->GetBaseOffset(); // [0x74]
|
||||
|
||||
// task #58 diagnostics (BT_SEG_DUMP): the draw-tree topology -- which
|
||||
// segment carries which joint, and who parents whom.
|
||||
if (getenv("BT_SEG_DUMP"))
|
||||
{
|
||||
int ji = segment->GetJointIndex();
|
||||
DEBUG_STREAM << "[seg] " << segment->GetIndex()
|
||||
<< " '" << (const char *)segment->GetName()
|
||||
<< "' parent=" << (segment->GetParent()
|
||||
? segment->GetParentIndex() : -1)
|
||||
<< " joint=" << ji
|
||||
<< " jtype=" << (ji == -1 ? -1
|
||||
: (int)joint_subsystem->GetJoint(ji)->GetJointType())
|
||||
<< " site=" << (int)(segment->IsSiteSegment() != 0)
|
||||
<< "\n" << std::flush;
|
||||
}
|
||||
|
||||
//
|
||||
// Parent renderable: root if the segment has no parent, else the
|
||||
// renderable already built for its parent segment.
|
||||
@@ -866,6 +885,116 @@ void BTRebuildMechModel(Entity *entity)
|
||||
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// StartEntityEffectImplementation @004d097c (coverage-audit reconstruction)
|
||||
//#############################################################################
|
||||
//
|
||||
// The per-zone EFFECT DISPATCHER -- the target of the whole authentic chain:
|
||||
// MechDeathHandler's class-5 -> RendererManager::StartEntityEffect ->
|
||||
// Renderer::StartEntityEffectMessageHandler [T0: resolves the GameModel +
|
||||
// applies the ExplosionResourceTable graphic-state remap] -> this virtual.
|
||||
// The binary body (@004d097c, part_014.c:5839):
|
||||
// 1. resolve the zone -> its transform (+0x74, row 3 = the world position)
|
||||
// and its VIDEO INDEX (+0xc4->+0xc0) = the segment slot;
|
||||
// 2. look the entity up in the renderer's per-entity tree (renderer+0x3a4,
|
||||
// == our mMechRenderTrees) -> the segment's DCS = the effect SOCKET;
|
||||
// 3. SearchList(resource, VideoModelResourceType) -> walk the video-object
|
||||
// records; atoi(name) < 1000 WARNS (the authentic gate: only INDIE/psfx
|
||||
// ids attach per-zone), >= 1000 starts the effect ON the socket, tagged
|
||||
// with the owning player (+0x190 -> +0x1e0) for the stop-all sweep.
|
||||
// Port mapping: the socket attachment = BTStartPfxAttached (the emitter rides
|
||||
// the segment via BTResolveSegmentWorld each frame); the tag = the entity
|
||||
// (BTStopEntityPfx kills by entity on respawn). Layout access is by NAMED
|
||||
// members only (zone->segmentIndex; the binary micro-offsets differ on our
|
||||
// compiled classes -- the databinding trap).
|
||||
//
|
||||
void
|
||||
BTL4VideoRenderer::StartEntityEffectImplementation(
|
||||
Entity *entity,
|
||||
DamageZone *damage_zone,
|
||||
ResourceDescription::ResourceID resource_ID
|
||||
)
|
||||
{
|
||||
if (entity == NULL || damage_zone == NULL)
|
||||
{
|
||||
DEBUG_STREAM << "StartEntityEffectImplementation: no entity/zone" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. the zone's segment slot (binary +0xc4->+0xc0; our named member)
|
||||
int seg_index = ((Mech__DamageZone *)damage_zone)->EffectSegmentIndex();
|
||||
|
||||
// 2. the segment's world position + frame (binary: zone transform +0x74)
|
||||
extern int BTResolveSegmentWorld(void *entity, int seg_index, float *pos3, float *rows9);
|
||||
float pos[3], rows[9];
|
||||
if (!BTResolveSegmentWorld(entity, seg_index, pos, rows))
|
||||
{
|
||||
DEBUG_STREAM << "StartEntityEffectImplementation: entity has no segment table" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. the effect resource's VIDEO MODEL (type 10) -- the effect-number list
|
||||
Check(application);
|
||||
ResourceDescription *res = application->GetResourceFile()->SearchList(
|
||||
resource_ID, ResourceDescription::VideoModelResourceType);
|
||||
if (res == NULL)
|
||||
{
|
||||
DEBUG_STREAM << "StartEntityEffectImplementation: " << (long)resource_ID
|
||||
<< " has no video resource" << std::endl;
|
||||
return;
|
||||
}
|
||||
res->Lock();
|
||||
// VideoModel payload [T0 L4VIDEO.h L4VideoObject, RES byte-verified]:
|
||||
// int32 count + count x 32-byte records {char name[15]; pad; int type;
|
||||
// int modes; float blinkPeriod; float pctOn} -- atoi(name) = the effect id.
|
||||
const unsigned char *pay = (const unsigned char *)res->resourceAddress;
|
||||
int count = *(const int *)pay;
|
||||
if (count < 0 || count > 16)
|
||||
count = 0; // malformed -- refuse
|
||||
const unsigned char *rec = pay + 4;
|
||||
for (int i = 0; i < count; ++i, rec += 32)
|
||||
{
|
||||
char name[16];
|
||||
memcpy(name, rec, 15);
|
||||
name[15] = 0;
|
||||
int fx = atoi(name);
|
||||
if (fx < 1000)
|
||||
{
|
||||
// authentic gate (@004d097c: "< 1000" warns) -- per-zone effects
|
||||
// are INDIE/psfx-band only
|
||||
DEBUG_STREAM << "StartEntityEffectImplementation: non-INDIE effect "
|
||||
<< fx << " in zone effect list" << std::endl;
|
||||
continue;
|
||||
}
|
||||
extern void BTStartPfxAttached(int, void *, int, float, float, float, const float *);
|
||||
BTStartPfxAttached(fx - 1000, (void *)entity, seg_index,
|
||||
pos[0], pos[1], pos[2], rows);
|
||||
if (getenv("BT_DEATH_LOG"))
|
||||
DEBUG_STREAM << "[zonefx] entity " << entity->GetEntityID()
|
||||
<< " seg " << seg_index << " psfx " << (fx - 1000)
|
||||
<< " at(" << pos[0] << "," << pos[1] << "," << pos[2] << ")" << std::endl;
|
||||
}
|
||||
res->Unlock();
|
||||
}
|
||||
|
||||
//
|
||||
// StopAllEntityEffectsImplementation @004d0c14 -- the respawn cleanup: the
|
||||
// binary kills every effect tagged (playerIdx<<16 .. |0xffff); the port kills
|
||||
// every emitter tagged to the entity.
|
||||
//
|
||||
void
|
||||
BTL4VideoRenderer::StopAllEntityEffectsImplementation(Entity *entity)
|
||||
{
|
||||
if (entity == NULL)
|
||||
{
|
||||
DEBUG_STREAM << "StopAllEntityEffectsImplementation: no entity" << std::endl;
|
||||
return;
|
||||
}
|
||||
extern void BTStopEntityPfx(void *owner);
|
||||
BTStopEntityPfx((void *)entity);
|
||||
}
|
||||
|
||||
|
||||
//#############################################################################
|
||||
// SwapToWreck (ExplosionScripts effect 104, reconstructed)
|
||||
//#############################################################################
|
||||
|
||||
@@ -623,6 +623,26 @@ extern void BTDrawReticle(struct IDirect3DDevice9 *device);
|
||||
Logical
|
||||
TestInstance() const;
|
||||
|
||||
//
|
||||
// The per-zone EFFECT DISPATCHER (@004d097c, coverage-audit discovery
|
||||
// 2026-07-13) -- the missing target of the engine's whole authentic
|
||||
// StartEntityEffect chain (MechDeathHandler class-5 -> RendererManager
|
||||
// -> Renderer::StartEntityEffectMessageHandler [T0, applies the
|
||||
// ExplosionResourceTable graphic-state remap] -> THIS virtual).
|
||||
// Resolves the zone's SEGMENT socket, walks the effect resource's
|
||||
// VideoModel (type 10), and starts each >=1000 effect ATTACHED to the
|
||||
// segment (the smoke follows the walking mech); <1000 warns
|
||||
// (byte-faithful to the binary's gate). StopAll kills every effect
|
||||
// tagged to the entity (the respawn cleanup, @004d0c14 analog).
|
||||
//
|
||||
virtual void
|
||||
StartEntityEffectImplementation(
|
||||
Entity *entity,
|
||||
DamageZone *damage_zone,
|
||||
ResourceDescription::ResourceID resource_ID); // @004d097c
|
||||
virtual void
|
||||
StopAllEntityEffectsImplementation(Entity *entity); // @004d0c14
|
||||
|
||||
//
|
||||
// Material substitution (mirrors RPL4VideoRenderer).
|
||||
//
|
||||
|
||||
@@ -1494,12 +1494,31 @@ void BTPostKillScore(Entity *victim, Scalar damage) // Step 7: KILL (+ MP deat
|
||||
{
|
||||
return;
|
||||
}
|
||||
BTPlayer *local_player = (BTPlayer *)application->GetMissionPlayer();
|
||||
if (local_player != 0)
|
||||
// OBSERVED-KILL credit (MP KILLS fix, 2026-07-13; the symmetric twin of
|
||||
// BTPlayerCountObservedDeath): every node maintains LOCAL score copies,
|
||||
// so the kill must land on the KILLER's player-link object -- the same
|
||||
// object the PilotList gauge reads (the roster resolves pilots via the
|
||||
// mechs' GetPlayerLink()). The old GetMissionPlayer() dispatch credited
|
||||
// a copy the scoreboard never displays (kills stuck at 0 while deaths --
|
||||
// which already flow through the player link -- counted; user-reported).
|
||||
// Resolving the killer from the victim's lastInflictingID also credits
|
||||
// the REMOTE player's local copy when THEY score ("target's kills
|
||||
// display 0") -- each node witnesses the death transition and tallies
|
||||
// its own copies self-consistently.
|
||||
BTPlayer *killer_player = 0;
|
||||
if (application->GetHostManager() != 0)
|
||||
{
|
||||
// KillScore (scoreType 2): senderMechID MUST be the VICTIM (!= our mech) so
|
||||
// the local player is credited via `this->killCount++` -- if it were our own
|
||||
// mech, ScoreMessageHandler takes the suicide branch (award negated, no kill).
|
||||
Entity *killer = application->GetHostManager()->GetEntityPointer(
|
||||
((Mech *)victim)->lastInflictingID);
|
||||
extern int BTIsRegisteredMech(Entity *e);
|
||||
if (killer != 0 && killer != victim && BTIsRegisteredMech(killer))
|
||||
killer_player = (BTPlayer *)((Mech *)killer)->GetPlayerLink();
|
||||
}
|
||||
if (killer_player != 0)
|
||||
{
|
||||
// KillScore (scoreType 2): senderMechID MUST be the VICTIM (!= the
|
||||
// receiver's mech) so the handler credits `killCount++` -- a suicide
|
||||
// (killer == victim) never reaches here.
|
||||
BTPlayer::ScoreMessage kill(
|
||||
Player::ScoreMessageID, // 0x12
|
||||
sizeof(BTPlayer::ScoreMessage),
|
||||
@@ -1507,13 +1526,12 @@ void BTPostKillScore(Entity *victim, Scalar damage) // Step 7: KILL (+ MP deat
|
||||
0.0f, // scoreAward (killBonus; 0 for bring-up)
|
||||
damage, // damageAmount (killing-blow)
|
||||
victim->GetEntityID()); // senderMechID = victim
|
||||
local_player->Dispatch(&kill);
|
||||
killer_player->Dispatch(&kill);
|
||||
if (getenv("BT_SCORE_LOG"))
|
||||
DEBUG_STREAM << "[score] *** KILL *** localPlayer=" << (void *)local_player
|
||||
<< " killCount=" << local_player->GetKillCount()
|
||||
<< " deaths=" << local_player->GetDeaths()
|
||||
<< " score=" << (Scalar)local_player->GetScore()
|
||||
<< " rank=" << local_player->GetRanking() << "\n" << std::flush;
|
||||
DEBUG_STREAM << "[score] *** KILL *** killerPlayer=" << (void *)killer_player
|
||||
<< " killCount=" << killer_player->GetKillCount()
|
||||
<< " deaths=" << killer_player->GetDeaths()
|
||||
<< " score=" << (Scalar)killer_player->GetScore() << std::endl;
|
||||
}
|
||||
|
||||
// MP DEATH: credit a death to the VICTIM's own player. NULL for the solo
|
||||
|
||||
@@ -337,16 +337,14 @@ void
|
||||
{
|
||||
HeatableSubsystem *watched = (HeatableSubsystem *)watchedLink.Resolve(); // FUN_00417ab4(this+0x114)
|
||||
|
||||
// BRING-UP GUARD (marked deviation): in the shipped game watchedLink ALWAYS
|
||||
// resolves -- the HeatWatcher resource's "WatchedSubsystem" name binds to a
|
||||
// roster segment at load -- so the binary (FUN_004aeac4) derefs `watched`
|
||||
// unconditionally. Our port does not yet resolve the watch link, so Resolve()
|
||||
// returns null; the deref AVs the moment a HeatWatcher-derived subsystem's
|
||||
// Performance actually runs (exposed by the WAVE 4 un-stub: AmmoBinSimulation
|
||||
// calls this directly; PowerWatcher::Simulation for Searchlight/ThermalSight).
|
||||
// With no resolved source, hold the heat alarm at Normal (same established
|
||||
// pattern as Emitter::TrackSeekVoltage's null-source guard). FAITHFUL
|
||||
// FOLLOW-UP = resolve watchedLink so the alarm tracks the real temperature.
|
||||
// The watch link is now BOUND by the authentic factory connect pass (task
|
||||
// #57: the mech factory's post-roster loop calls the binary's vtable slot
|
||||
// +0x38 -- FUN_004aee2c / FUN_004b1a40 -> BTWatcherBindTarget below), so
|
||||
// Resolve() succeeds on every master-node watcher whose resource named a
|
||||
// WatchedSubsystem. The binary derefs `watched` unconditionally; the null
|
||||
// guard stays for replicant nodes (the binary never binds there either --
|
||||
// the +0x38 body is master-gated -- and their watcher Performances don't
|
||||
// run) and for bring-up safety.
|
||||
if (watched == 0)
|
||||
{
|
||||
heatAlarm.SetLevel(0); // NormalHeat (no watched source yet)
|
||||
@@ -369,6 +367,51 @@ void
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// @004aee2c (HeatWatcher vtable slot 14, offset +0x38; the PowerWatcher/Torso
|
||||
// override @004b1a40 is byte-identical) -- the factory post-roster CONNECT
|
||||
// pass, disassembled from the raw binary (Ghidra missed both function starts):
|
||||
//
|
||||
// owner = this->owner (+0xD0)
|
||||
// if ((owner->simulationFlags & 0xC) == 0 && (owner->simulationFlags & 0x100))
|
||||
// watchedLink(+0x114).Add( owner->roster(+0x128)[ watchedSubsystem(+0x128) ] )
|
||||
//
|
||||
// The mech factory loop tests IsDerivedFrom(HeatWatcher @0x50e604) then calls
|
||||
// the slot. Split across the TU boundary the same way as the other
|
||||
// cross-family helpers: the OWNER-side master gate + roster lookup live in the
|
||||
// mech family (complete Mech type); these two bridges are the family-side
|
||||
// derivation test and the link Add. [T1: bytes @004aee2c/@004b1a40]
|
||||
//
|
||||
int
|
||||
BTWatcherWatchedIndex(Subsystem *sub) // -1 = not a HeatWatcher
|
||||
{
|
||||
if (sub == 0 || !sub->IsDerivedFrom(*HeatWatcher::GetClassDerivations()))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
return ((HeatWatcher *)sub)->watchedSubsystem; // @0x128 (resource +0xE4, name index +2)
|
||||
}
|
||||
|
||||
void
|
||||
BTWatcherBindTarget(Subsystem *sub, Subsystem *target)
|
||||
{
|
||||
((HeatWatcher *)sub)->watchedLink.Add(target); // (**(link+4))(link, target) = FUN_00417a80
|
||||
}
|
||||
|
||||
//
|
||||
// Cross-family derivation probe: 0x50e604 is the HEATWATCHER derivation tag
|
||||
// (TestInstance @4aec38 checks its own class against it) -- NOT HeatSink, as
|
||||
// an old btl4gaug label claimed (HeatSink : HeatableSubsystem would make an
|
||||
// OR-test after HeatableSubsystem redundant; the binary's alternate branch is
|
||||
// the disjoint watcher family). Bridged so gauge TUs can test without
|
||||
// including this family's headers.
|
||||
//
|
||||
int
|
||||
BTIsHeatWatcher(Subsystem *sub)
|
||||
{
|
||||
return sub != 0 && sub->IsDerivedFrom(*HeatWatcher::GetClassDerivations());
|
||||
}
|
||||
|
||||
//
|
||||
// @4aec54 (468 bytes) -- parse the HeatWatcher resource. Stamps classID 0x0BBF
|
||||
// / size 0xF0; defaults the watched index to -1; requires DegradationTemperature,
|
||||
|
||||
@@ -1187,12 +1187,40 @@ Mech::Mech(
|
||||
//
|
||||
// Build the capability sub-rosters by IsDerivedFrom() class tests.
|
||||
//
|
||||
for (int id = 2; id < subsystemCount; ++id) // "start" each subsystem
|
||||
// Loop 1 = the watcher CONNECT pass (task #57). The binary calls vtable
|
||||
// slot +0x38 on every HeatWatcher-derived subsystem (@0x50e604 test);
|
||||
// the slot bodies (FUN_004aee2c, PowerWatcher/Torso override @004b1a40,
|
||||
// byte-identical -- Ghidra missed both starts, recovered from raw bytes)
|
||||
// bind watchedLink to roster[watchedSubsystem], gated on the owner being
|
||||
// the live master node ((flags & 0xC)==0 && (flags & 0x100)). The gate is
|
||||
// hoisted out of the loop unchanged (constant per mech). This replaces
|
||||
// the old SubProxy::Start() no-op that left every watchdogAlarm at 0 --
|
||||
// which held the Torso's ElectricalStateLevel() below Ready and zeroed the
|
||||
// twist rate forever.
|
||||
//
|
||||
if ((simulationFlags & 0xC) == 0 // SegmentCopyMask
|
||||
&& (simulationFlags & 0x100) != 0) // MasterHeatSinkFlag
|
||||
{
|
||||
SubProxy *s = (SubProxy *)subsystemArray[id];
|
||||
if (s != 0 && s->IsDerivedFrom(0x50e604)) // FUN_0041a1a4
|
||||
extern int BTWatcherWatchedIndex(Subsystem *sub); // heatfamily_reslice.cpp
|
||||
extern void BTWatcherBindTarget(Subsystem *sub, Subsystem *target);
|
||||
for (int id = 2; id < subsystemCount; ++id)
|
||||
{
|
||||
s->Start(); // (**(s+0x38))(s)
|
||||
int watched = BTWatcherWatchedIndex(subsystemArray[id]); // -1 = not a HeatWatcher
|
||||
if (watched < 0)
|
||||
continue;
|
||||
if (watched < subsystemCount && subsystemArray[watched] != 0)
|
||||
{
|
||||
BTWatcherBindTarget(subsystemArray[id], subsystemArray[watched]);
|
||||
DEBUG_STREAM << "[watch] subsystem " << id << " watches "
|
||||
<< watched << std::endl;
|
||||
}
|
||||
else
|
||||
{
|
||||
// The binary has no range check (CreateStreamed validated the
|
||||
// name); flag data drift honestly instead of a wild read.
|
||||
DEBUG_STREAM << "[watch] subsystem " << id
|
||||
<< " BAD WatchedSubsystem index " << watched << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int id = 2; id < subsystemCount; ++id) // heatable roster
|
||||
|
||||
@@ -570,6 +570,20 @@ Scalar
|
||||
switch (legAnimationState) // this+0x3b0
|
||||
{
|
||||
case StandingAnimation: // 0
|
||||
// STANDING ZEROES THE CYCLE (reverse-stop desync, live-diagnosed
|
||||
// 2026-07-13): a REVERSE cadence is NEGATIVE, so the walk-family stop
|
||||
// gate (cycleSpeed <= ZeroSpeed) passes while still cycling at full
|
||||
// reverse speed, and several stand-entry paths (turn exit, terminal
|
||||
// poses) never touch the cycle -- Standing could be entered with a
|
||||
// stale legCycleSpeed = -2.507 ([gaitSM] state=0 evidence). The master
|
||||
// LOOKS still (case 0 never advances the clip) but the stale cycle
|
||||
// REPLICATES and the peer's replicant marches in place. A standing
|
||||
// mech's cycle is 0 (the clean forward-stop log: legSum ~3e-8).
|
||||
if (legCycleSpeed != 0.0f)
|
||||
{
|
||||
legCycleSpeed = 0.0f;
|
||||
ForceUpdate(8); // type-3 record: legs stopped
|
||||
}
|
||||
// RAW (part_012.c FUN_004a5028 case 0): standSpeed < commandedSpeed ->
|
||||
// begin WALKING (state 5); 0 <= commanded < standSpeed -> stay standing;
|
||||
// commanded < 0 -> stand-to-reverse (0x10). (The earlier draft had the
|
||||
@@ -823,6 +837,20 @@ Scalar
|
||||
switch (bodyAnimationState) // this+0x728
|
||||
{
|
||||
case StandingAnimation: // 0
|
||||
// STANDING ZEROES THE CYCLE (reverse-stop desync, live-diagnosed
|
||||
// 2026-07-13): a REVERSE cadence is NEGATIVE, so the walk-family stop
|
||||
// gate (cycleSpeed <= ZeroSpeed) passes while still cycling at full
|
||||
// reverse speed, and several stand-entry paths (turn exit, terminal
|
||||
// poses) never touch the cycle -- Standing could be entered with a
|
||||
// stale bodyCycleSpeed = -2.507 ([gaitSM] state=0 evidence). The master
|
||||
// LOOKS still (case 0 never advances the clip) but the stale cycle
|
||||
// REPLICATES and the peer's replicant marches in place. A standing
|
||||
// mech's cycle is 0 (the clean forward-stop log: legSum ~3e-8).
|
||||
if (bodyCycleSpeed != 0.0f)
|
||||
{
|
||||
bodyCycleSpeed = 0.0f;
|
||||
ForceUpdate(8); // type-3 record: legs stopped
|
||||
}
|
||||
// RAW (FUN_004a5678 case 0): standSpeed < bodyTargetSpeed -> begin WALKING
|
||||
// (5); 0 <= target < standSpeed -> stay standing; target < 0 -> reverse
|
||||
// (0x10). (The earlier draft had the comparison INVERTED.)
|
||||
@@ -979,6 +1007,20 @@ Scalar
|
||||
switch (bodyAnimationState)
|
||||
{
|
||||
case StandingAnimation: // 0
|
||||
// STANDING ZEROES THE CYCLE (reverse-stop desync, live-diagnosed
|
||||
// 2026-07-13): a REVERSE cadence is NEGATIVE, so the walk-family stop
|
||||
// gate (cycleSpeed <= ZeroSpeed) passes while still cycling at full
|
||||
// reverse speed, and several stand-entry paths (turn exit, terminal
|
||||
// poses) never touch the cycle -- Standing could be entered with a
|
||||
// stale bodyCycleSpeed = -2.507 ([gaitSM] state=0 evidence). The master
|
||||
// LOOKS still (case 0 never advances the clip) but the stale cycle
|
||||
// REPLICATES and the peer's replicant marches in place. A standing
|
||||
// mech's cycle is 0 (the clean forward-stop log: legSum ~3e-8).
|
||||
if (bodyCycleSpeed != 0.0f)
|
||||
{
|
||||
bodyCycleSpeed = 0.0f;
|
||||
ForceUpdate(8); // type-3 record: legs stopped
|
||||
}
|
||||
if (bodyTargetSpeed <= standSpeed) // 0x6b4 <= 0x530
|
||||
{
|
||||
distance = 0.0f;
|
||||
@@ -1166,6 +1208,20 @@ Scalar
|
||||
switch (legAnimationState)
|
||||
{
|
||||
case StandingAnimation: // 0
|
||||
// STANDING ZEROES THE CYCLE (reverse-stop desync, live-diagnosed
|
||||
// 2026-07-13): a REVERSE cadence is NEGATIVE, so the walk-family stop
|
||||
// gate (cycleSpeed <= ZeroSpeed) passes while still cycling at full
|
||||
// reverse speed, and several stand-entry paths (turn exit, terminal
|
||||
// poses) never touch the cycle -- Standing could be entered with a
|
||||
// stale legCycleSpeed = -2.507 ([gaitSM] state=0 evidence). The master
|
||||
// LOOKS still (case 0 never advances the clip) but the stale cycle
|
||||
// REPLICATES and the peer's replicant marches in place. A standing
|
||||
// mech's cycle is 0 (the clean forward-stop log: legSum ~3e-8).
|
||||
if (legCycleSpeed != 0.0f)
|
||||
{
|
||||
legCycleSpeed = 0.0f;
|
||||
ForceUpdate(8); // type-3 record: legs stopped
|
||||
}
|
||||
if (motionSource->commandedSpeed <= standSpeed) // +0x128 <= 0x530
|
||||
{
|
||||
distance = 0.0f;
|
||||
|
||||
+152
-16
@@ -646,6 +646,9 @@ static int gBTLaserKey = 0; // raw key states (set by the keyboar
|
||||
static int gBTPPCKey = 0;
|
||||
static int gBTMissileKey = 0;
|
||||
static int gBTPinkyKey = 0; // key '4' = the pod's 4th fire button (Pinky 0x45)
|
||||
int gBTModeCycle = 0; // 'M' edge: cycle the control mode (mapper consumes)
|
||||
float gBTTwistAxis = 0.0f; // Q/E torso-twist deflection (assisted-mode stick X)
|
||||
int gBTTorsoRecenter = 0; // 'X' edge: pulse the authentic torso recenter (mapper consumes)
|
||||
static int gBTConfigKey = 0; // task #6: HOLD 'G' = the weapon-configure button
|
||||
static int gBTGenSelKey = 0; // task #12: F5..F8 = SelectGeneratorA..D, F9 = mode toggle
|
||||
// (0 idle; else the message id 4..8)
|
||||
@@ -785,6 +788,69 @@ static BTProjectile gProjectiles[64];
|
||||
|
||||
extern void BTPushBeam(float,float,float, float,float,float, unsigned, float, float);
|
||||
|
||||
//###########################################################################
|
||||
// Zone-effect bridge (mechdmg has no `application` access): route the damage-
|
||||
// band effect through the AUTHENTIC RendererManager::StartEntityEffect chain
|
||||
// (the @004d097c dispatcher; the AudioRenderer hears the same broadcast).
|
||||
// Returns 0 when the manager isn't up so the caller can fall back.
|
||||
//###########################################################################
|
||||
int
|
||||
BTStartZoneEffect(Mech *mech, void *zone, int resource)
|
||||
{
|
||||
if (mech == 0 || zone == 0 || resource <= 0
|
||||
|| application == 0 || application->GetRendererManager() == 0)
|
||||
return 0;
|
||||
application->GetRendererManager()->StartEntityEffect(
|
||||
(Entity *)mech, (DamageZone *)zone,
|
||||
(ResourceDescription::ResourceID)resource);
|
||||
return 1;
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
// Segment world-transform bridge (the @004d097c per-zone effect dispatcher +
|
||||
// the attached-emitter follow in L4VIDEO's PFX layer). Resolves the entity's
|
||||
// segment (by index) to its world position + 3x3 basis rows; falls back to
|
||||
// the mech origin at torso height when the segment doesn't resolve. Returns
|
||||
// 0 for a non-mech / unregistered entity (the follow then keeps its last
|
||||
// frame -- and the emitter dies with its authored window anyway).
|
||||
//###########################################################################
|
||||
int
|
||||
BTResolveSegmentWorld(void *entity, int seg_index, float *pos3, float *rows9)
|
||||
{
|
||||
extern int BTIsRegisteredMech(Entity *e);
|
||||
if (entity == 0 || !BTIsRegisteredMech((Entity *)entity))
|
||||
return 0;
|
||||
Mech *m = (Mech *)entity;
|
||||
|
||||
Point3D p = m->localOrigin.linearPosition;
|
||||
p.y += kMuzzleHeight; // fallback: torso height
|
||||
if (seg_index >= 0)
|
||||
{
|
||||
EntitySegment::SegmentTableIterator it(m->segmentTable);
|
||||
EntitySegment *seg;
|
||||
while ((seg = it.ReadAndNext()) != NULL)
|
||||
{
|
||||
if (seg->GetIndex() == seg_index)
|
||||
{
|
||||
AffineMatrix mw;
|
||||
mw.Multiply(seg->GetSegmentToEntity(), m->localToWorld);
|
||||
p = mw; // Point3D = matrix translation
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
pos3[0] = (float)p.x; pos3[1] = (float)p.y; pos3[2] = (float)p.z;
|
||||
|
||||
UnitVector ax, ay, az;
|
||||
m->localToWorld.GetFromAxis(X_Axis, &ax);
|
||||
m->localToWorld.GetFromAxis(Y_Axis, &ay);
|
||||
m->localToWorld.GetFromAxis(Z_Axis, &az);
|
||||
rows9[0] = (float)ax.x; rows9[1] = (float)ax.y; rows9[2] = (float)ax.z;
|
||||
rows9[3] = (float)ay.x; rows9[4] = (float)ay.y; rows9[5] = (float)ay.z;
|
||||
rows9[6] = (float)az.x; rows9[7] = (float)az.y; rows9[8] = (float)az.z;
|
||||
return 1;
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
// PER-ROUND DETONATION (the binary's Missile::MoveAndCollide @004bef78: every
|
||||
// round spawns ITS OWN ExplosionModelFile at its impact point, resource
|
||||
@@ -1299,6 +1365,11 @@ void
|
||||
frameEntryWorldVelocity = Vector3D(0.0f, 0.0f, 0.0f);
|
||||
ramLastVictim = 0;
|
||||
ramContactLinger = 0.0f;
|
||||
// StopAllEntityEffects (@004d0c14): a respawned mech must not trail its
|
||||
// corpse's attached zone effects -- the authentic per-entity effect
|
||||
// cleanup, broadcast to every renderer.
|
||||
if (application != 0 && application->GetRendererManager() != 0)
|
||||
application->GetRendererManager()->StopAllEntityEffects((Entity *)this);
|
||||
|
||||
// --- our RELOCATED gait/motion accumulators -> identity (the 1995 offsets
|
||||
// the decomp zeroes map to these named members in our layout) ---
|
||||
@@ -1823,6 +1894,56 @@ void
|
||||
// task #13: 'C' cycles the coolant valve (BT_VALVE_SLOT
|
||||
// picks the condenser roster slot; default = Condenser1).
|
||||
gBTValveKey = focused && (pAsync('C') & dn) ? 1 : 0;
|
||||
// TORSO CONTROLS (2026-07-13): 'M' cycles the control mode
|
||||
// (Basic -> Standard -> Veteran -- the pod console button,
|
||||
// CycleControlModeMessageHandler); Q/E deflect the torso
|
||||
// twist axis (the STICK in Standard/Veteran, where A/D
|
||||
// become the pedals). Ramped like the turn stick.
|
||||
{
|
||||
static int sPrevM = 0;
|
||||
const int mNow = focused && (pAsync('M') & dn) ? 1 : 0;
|
||||
if (mNow && !sPrevM) gBTModeCycle = 1; // edge -> one cycle
|
||||
sPrevM = mNow;
|
||||
const int tw = (focused && (pAsync('E') & dn) ? 1 : 0)
|
||||
- (focused && (pAsync('Q') & dn) ? 1 : 0);
|
||||
static float sTwist = 0.0f;
|
||||
if (tw != 0)
|
||||
{
|
||||
sTwist += tw * kStickRate * dt;
|
||||
if (sTwist > 1.0f) sTwist = 1.0f;
|
||||
if (sTwist < -1.0f) sTwist = -1.0f;
|
||||
}
|
||||
else if (sTwist != 0.0f)
|
||||
{
|
||||
// SPRING-CENTER on release (user fix 2026-07-13: the
|
||||
// old hold-deflection model left a residual axis ->
|
||||
// the torso drifted forever with no way to stop).
|
||||
// The pod stick is spring-centered: the axis is a
|
||||
// twist-RATE demand, so release = rate 0 = the torso
|
||||
// HOLDS where you aimed it (position is kept by
|
||||
// Torso::currentTwist, not by the axis). Same model
|
||||
// as the A/D turn stick (kStickCenterRate).
|
||||
const float step = kStickCenterRate * dt;
|
||||
if (sTwist > step) sTwist -= step;
|
||||
else if (sTwist < -step) sTwist += step;
|
||||
else sTwist = 0.0f;
|
||||
}
|
||||
// X = all-stop for the torso too: zero the axis NOW and
|
||||
// pulse the AUTHENTIC recenter (torso centerCommand ->
|
||||
// Recenter @004b6918 slews currentTwist back to 0; any
|
||||
// new Q/E deflection cancels it, sim-side). Separate
|
||||
// edge detector from the drive all-stop below -- both
|
||||
// fire on the same press.
|
||||
static int sPrevXT = 0;
|
||||
const int xtNow = focused && (pAsync('X') & dn) ? 1 : 0;
|
||||
if (xtNow && !sPrevXT)
|
||||
{
|
||||
sTwist = 0.0f;
|
||||
gBTTorsoRecenter = 1; // mapper consumes -> CommandRecenter()
|
||||
}
|
||||
sPrevXT = xtNow;
|
||||
gBTTwistAxis = sTwist;
|
||||
}
|
||||
// gBTDrive.fire = "any weapon trigger down" (feeds the bring-up
|
||||
// damage dispatcher + the beam-visual keepalive)
|
||||
gBTDrive.fire = (gBTLaserKey || gBTPPCKey || gBTMissileKey || gBTPinkyKey) ? 1 : 0;
|
||||
@@ -1842,18 +1963,24 @@ void
|
||||
}
|
||||
sPrevV = vNow;
|
||||
|
||||
// RETICLE = TORSO BORESIGHT (task #39 correction): the pod had
|
||||
// NO free-aim cursor -- the binary's HudSimulation computes
|
||||
// reticlePosition from the mech's POSE quaternions
|
||||
// (part_013.c:5652+, a rate-limited quat->euler of the torso
|
||||
// aim), i.e. the crosshair marks where the TORSO GUNS point
|
||||
// relative to the view, and you aim by steering the mech /
|
||||
// twisting the torso (the pod's right stick). The earlier
|
||||
// mouse-cursor slew was a mis-sourced stand-in and is
|
||||
// REMOVED. Our cockpit view is body-mounted, so the
|
||||
// crosshair deflection = the torso twist projected to screen
|
||||
// -- identically ZERO on the fixed-torso BLH (dead-centre
|
||||
// boresight). BT_AIM="x y" remains as the headless harness.
|
||||
// RETICLE = SCREEN CENTER (task #58 correction, supersedes the
|
||||
// task #39 "body-mounted view" model): the CAMERA yaws with
|
||||
// the torso (the cockpit sits on jointtorso -- see the
|
||||
// gBTEyeTwist publisher), so screen center always IS the gun
|
||||
// boresight and the crosshair stays centered while the WORLD
|
||||
// rotates past it -- the twist reads on the bottom tape
|
||||
// carets / compass / radar wedge, not the crosshair. [T1:
|
||||
// jointtorso->jointeye->siteeyepoint chain + FUN_004c22c4
|
||||
// inverse-chain view; crosshair-twist forensics 2026-07-13.]
|
||||
// The old gBTAimX = tan(twist) slew was a port invention on
|
||||
// the falsified body-mounted premise -- with the yawing eye
|
||||
// it double-counts (crosshair pinned to HULL-forward, fire
|
||||
// ray resolving body-forward: "twisting leaves the
|
||||
// crosshairs behind"). Reticle mobility exists in the
|
||||
// binary (Reticle::reticlePosition, RETICLE.h:42) but its
|
||||
// twist-era use is the FIXED-TORSO free-aim channel
|
||||
// (mech+0x36c, writer un-exported) -- deferred.
|
||||
// BT_AIM="x y" remains as the headless harness.
|
||||
{
|
||||
static int sAimEnv = -1;
|
||||
static float sAimEnvX = 0.0f, sAimEnvY = 0.0f;
|
||||
@@ -1870,10 +1997,10 @@ void
|
||||
}
|
||||
else
|
||||
{
|
||||
// boresight = torso twist vs the body-mounted view,
|
||||
// projected through the live per-axis projection.
|
||||
extern float BTTwistToReticleX(float twist_rad);
|
||||
gBTAimX = BTTwistToReticleX(gBTHudTwist); // BLH: 0
|
||||
// Boresight = screen center: the eye carries the
|
||||
// twist (task #58), the pick ray inherits it from
|
||||
// the published eye basis.
|
||||
gBTAimX = 0.0f;
|
||||
gBTAimY = 0.0f;
|
||||
}
|
||||
}
|
||||
@@ -3299,6 +3426,15 @@ void
|
||||
gBTHudTwistLimit = (float)GetHorizontalFiringReach();// HorizontalTorsoLimit (attrs 5/6)
|
||||
gBTHudGroupMask = (int)targetReticle.reticleElementMask & 0xF;
|
||||
gBTHudPrimary = ((int)targetReticle.reticleElementMask & 0x20) != 0;
|
||||
// task #58: publish the live twist for renderer-side DIAGNOSTICS
|
||||
// (correlating [eyefwd] against the twist). The cockpit eye does
|
||||
// NOT consume this -- it inherits the twist authentically through
|
||||
// jointtorso's HingeRenderable in the draw traversal (see the note
|
||||
// in DPLEyeRenderable::Execute).
|
||||
{
|
||||
extern float gBTEyeTwist;
|
||||
gBTEyeTwist = gBTHudTwist;
|
||||
}
|
||||
}
|
||||
|
||||
// task #6 ORDER FIX: the scripted block must run BEFORE the fire-push
|
||||
|
||||
@@ -1092,8 +1092,21 @@ void
|
||||
}
|
||||
if (d != 0)
|
||||
{
|
||||
BTSpawnDamageEffect(owner, d->effectResource, // explosion AT the zone's
|
||||
zone->segmentIndex); // segment (world position)
|
||||
// THE AUTHENTIC CHAIN (@004d097c dispatcher, landed 2026-07-13):
|
||||
// RendererManager::StartEntityEffect -> the Renderer message
|
||||
// handler [T0: GameModel resolve + ExplosionResourceTable
|
||||
// graphic-state remap] -> BTL4VideoRenderer::
|
||||
// StartEntityEffectImplementation -> the effect ATTACHED to the
|
||||
// zone's segment (smoke follows the walking mech) -- and the
|
||||
// AudioRenderer's implementation hears the same message (zone
|
||||
// sounds ride free). Explosion::Make fallback only when the
|
||||
// manager isn't up (early bring-up).
|
||||
extern int BTStartZoneEffect(Mech *mech, void *zone, int resource);
|
||||
if (!BTStartZoneEffect(owner, zone, d->effectResource))
|
||||
{
|
||||
BTSpawnDamageEffect(owner, d->effectResource, // explosion AT the zone's
|
||||
zone->segmentIndex); // segment (world position)
|
||||
}
|
||||
zone->ApplyDamageGraphicState(d->graphicState); // destroyed skin (graphic state)
|
||||
// A graphic-state change means the segment's MODEL changed (intact ->
|
||||
// destroyed variant, keyed by GetVideoObjectName(skl, gstate)). Fire
|
||||
|
||||
@@ -211,6 +211,12 @@
|
||||
int
|
||||
segmentIndex; // @0x194 GetSegmentIndex() result
|
||||
|
||||
public:
|
||||
// The zone's segment slot -- consumed by the @004d097c per-zone effect
|
||||
// dispatcher (BTL4VideoRenderer::StartEntityEffectImplementation).
|
||||
int EffectSegmentIndex() const { return segmentIndex; }
|
||||
protected:
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Damage Support
|
||||
//
|
||||
|
||||
@@ -440,6 +440,18 @@ void
|
||||
Check(message);
|
||||
|
||||
if (message->dataContents > 0)
|
||||
{
|
||||
CycleControlModeNow();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// The mode-cycle body, shared by the console-button message handler above and
|
||||
// the desktop 'M' key (mech4 key poll -> gBTModeCycle).
|
||||
//
|
||||
void
|
||||
MechControlsMapper::CycleControlModeNow()
|
||||
{
|
||||
{
|
||||
controlMode = (ControlMode)(controlMode + 1);
|
||||
if (controlMode > VeteranMode)
|
||||
@@ -448,23 +460,28 @@ void
|
||||
}
|
||||
NotifyOfControlModeChange(controlMode); // vtable+0x48
|
||||
|
||||
Mech *mech = GetMech();
|
||||
int torso = *(int *)((int)mech + 0x438);
|
||||
int cockpit = *(int *)((int)mech + 0x5b4);
|
||||
|
||||
if (controlMode == BasicMode)
|
||||
// TYPED torso reconfiguration (2026-07-13): the raw block this
|
||||
// replaces wrote the BINARY's offsets (torso+0x1f0/0x274/0x220...)
|
||||
// straight onto OUR compiled Torso -- the databinding trap: garbage
|
||||
// writes into whatever members live there in this build. The
|
||||
// observable semantics via named members: Basic clears the analog
|
||||
// axes and recenters (the sim's centerCommand -> Recenter); the
|
||||
// assisted modes just free the torso (the sim clamps to the authored
|
||||
// limits on its own).
|
||||
Mech *mech = GetMech();
|
||||
Torso *torso = (mech != 0) ? (Torso *)mech->GetTorsoSubsystem() : 0;
|
||||
if (torso != 0)
|
||||
{
|
||||
*(LWord *)(torso + 0x1f0) = 0; // no free aim
|
||||
*(LWord *)(torso + 0x274) = 1; // auto-center on
|
||||
*(LWord *)(torso + 0x220) = *(LWord *)(torso + 0x228); // recenter yaw
|
||||
*(LWord *)(torso + 0x224) = *(LWord *)(torso + 0x22c); // recenter pitch
|
||||
*(LWord *)(cockpit + 0x2a0) = 1;
|
||||
}
|
||||
else if ((unsigned)(controlMode - 1) < 2) // Standard / Veteran
|
||||
{
|
||||
*(LWord *)(torso + 0x220) = *(LWord *)(torso + 0x230); // yaw -> limit
|
||||
*(LWord *)(torso + 0x224) = *(LWord *)(torso + 0x234); // pitch -> limit
|
||||
if (controlMode == BasicMode)
|
||||
{
|
||||
torso->SetAnalogTwistAxis(0.0f);
|
||||
torso->SetAnalogElevationAxis(0.0f);
|
||||
torso->CommandRecenter();
|
||||
}
|
||||
// Standard/Veteran: nothing to force -- the sim's limits govern.
|
||||
}
|
||||
DEBUG_STREAM << "[mode] control mode -> " << (int)controlMode
|
||||
<< " (0=Basic 1=Standard 2=Veteran)" << std::endl;
|
||||
}
|
||||
Check_Fpu();
|
||||
}
|
||||
@@ -655,7 +672,54 @@ void
|
||||
}
|
||||
throttlePosition = (key_throttle >= 0.0f) ? key_throttle : -key_throttle;
|
||||
reverseThrust = (key_throttle < 0.0f) ? 1 : 0;
|
||||
stickPosition.x = key_turn;
|
||||
// CONTROL-MODE AXIS ROUTING (2026-07-13, the pod mapping): in
|
||||
// BASIC the stick steers the legs (turn) and the torso auto-
|
||||
// centers; in STANDARD/VETERAN the stick is the TORSO (free aim /
|
||||
// twist) and the PEDALS steer -- so the keyboard's A/D feed the
|
||||
// pedals and Q/E ('the stick') feed the twist. 'M' cycles modes
|
||||
// (the same body the pod console button message drives, so the
|
||||
// CONTROL MODE gauge tracks).
|
||||
{
|
||||
extern int gBTModeCycle;
|
||||
extern float gBTTwistAxis;
|
||||
extern int gBTTorsoRecenter;
|
||||
if (gBTModeCycle)
|
||||
{
|
||||
gBTModeCycle = 0;
|
||||
CycleControlModeNow();
|
||||
}
|
||||
if (controlMode == BasicMode)
|
||||
{
|
||||
stickPosition.x = key_turn;
|
||||
pedalsPosition = 0.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
stickPosition.x = gBTTwistAxis; // the torso axis
|
||||
pedalsPosition = key_turn; // A/D = the pedals
|
||||
// 'X' recenter pulse (2026-07-13): one-frame centerCommand
|
||||
// -> the sim arms recenterActive and Recenter (@004b6918)
|
||||
// slews the torso home; any Q/E input cancels it sim-side.
|
||||
// centerCommand is a pod BUTTON state, so the writer clears
|
||||
// it while unpressed (Basic's own path re-asserts every
|
||||
// frame; this branch owns it in Standard/Veteran).
|
||||
{
|
||||
Torso *rcTorso = (Torso *)mech->GetTorsoSubsystem();
|
||||
if (rcTorso != 0)
|
||||
{
|
||||
if (gBTTorsoRecenter)
|
||||
{
|
||||
gBTTorsoRecenter = 0;
|
||||
rcTorso->CommandRecenter();
|
||||
}
|
||||
else
|
||||
{
|
||||
rcTorso->ClearRecenterCommand();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stickPosition.y = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +104,11 @@ class Pilot;
|
||||
CycleControlModeMessageHandler( // @004afbe0
|
||||
ReceiverDataMessageOf<ControlsButton> *message
|
||||
);
|
||||
|
||||
// The shared mode-cycle body (the console-button handler above + the
|
||||
// desktop 'M' key both drive it).
|
||||
void
|
||||
CycleControlModeNow();
|
||||
void
|
||||
CycleDisplayModeMessageHandler( // @004afcac
|
||||
ReceiverDataMessageOf<ControlsButton> *message
|
||||
|
||||
@@ -75,7 +75,12 @@ static const Scalar MaxOutputVoltage = 1.0f; // _DAT_004b2154 / _DAT_004b2258
|
||||
static const Scalar RatedVoltageRef = 1.0f; // _DAT_004b1d10 (myomer rated reference)
|
||||
static const Scalar DegradedDriveScale = 0.5f; // _DAT_004b1d14 (heat-degradation throttle)
|
||||
static const Scalar ResourceUnset = -1.0f; // _DAT_004b177c / _DAT_004b1a3c / _DAT_004b2580
|
||||
static const Scalar MinVoltageScale = 1.0f; // _DAT_004b1924 (PowerWatcher threshold scale)
|
||||
static const Scalar MinVoltageScale = 0.01f; // _DAT_004b1924 -- a 10-byte x87 literal
|
||||
// (0a d7 a3 70 3d 0a d7 a3 f8 3f = 0.01,
|
||||
// byte-read from the exe, task #57): converts
|
||||
// the authored MinVoltagePercent to a fraction.
|
||||
// Was 1.0f, which made minVoltage 100x too big
|
||||
// and latched the watchdog brownout (level 1).
|
||||
|
||||
|
||||
//###########################################################################
|
||||
@@ -1191,8 +1196,12 @@ PowerWatcher::SharedData
|
||||
Derivation*
|
||||
PowerWatcher::GetClassDerivations()
|
||||
{
|
||||
// Real base ctor is @004aeb40 (HeatWatcher); HeatableSubsystem stands in.
|
||||
static Derivation classDerivations(HeatableSubsystem::GetClassDerivations(), "PowerWatcher");
|
||||
// Chains to the REAL base HeatWatcher (@004aeb40) -- task #57: the old
|
||||
// HeatableSubsystem stand-in predated the base-chain re-base and broke
|
||||
// IsDerivedFrom(HeatWatcher) for every PowerWatcher descendant, which
|
||||
// silently skipped the Torso/Searchlight/ThermalSight in the factory
|
||||
// watcher-CONNECT pass (their watchdogs stayed at 0 = never Ready).
|
||||
static Derivation classDerivations(HeatWatcher::GetClassDerivations(), "PowerWatcher");
|
||||
return &classDerivations;
|
||||
}
|
||||
|
||||
@@ -1254,13 +1263,68 @@ Logical
|
||||
}
|
||||
|
||||
//
|
||||
// @004b1804 -- delegate to the base simulation.
|
||||
// @004b1804 (slot 10) -- ResetToInitialState: chain to the HeatWatcher base.
|
||||
// (This body was previously MISLABELED as the Simulation. The vtable
|
||||
// @0050f99c slot 10 (offset 0x28) is 004b1804, exactly where every other
|
||||
// subsystem carries ResetToInitialState; the registered Performance PTR
|
||||
// @0050f5fc points at 004b181c -- UpdateWatch below.)
|
||||
//
|
||||
void
|
||||
PowerWatcher::Simulation(Scalar time_slice)
|
||||
PowerWatcher::ResetToInitialState(Logical /*powered*/)
|
||||
{
|
||||
HeatWatcher::ResetToInitialState(True); // FUN_004aea9c = HeatWatcher::ResetToInitialState
|
||||
(void)time_slice;
|
||||
HeatWatcher::ResetToInitialState(True); // FUN_004aea9c
|
||||
}
|
||||
|
||||
//
|
||||
// @004b181c -- the per-tick watch update: THE registered Performance
|
||||
// (PTR @0050f5fc -> 004b181c), and called directly by the Torso master/copy
|
||||
// simulations (@004b5cf0/@004b65f8 first line). Ghidra missed the function
|
||||
// start; recovered from the raw pseudocode at part_013.c:1645:
|
||||
//
|
||||
// FUN_004aeac4(this); // heat mirror
|
||||
// watched = watchedLink.Resolve(); // +0x114
|
||||
// gen = watched->voltageSource.Resolve(); // +0x1D0
|
||||
// watchdogAlarm.SetLevel(watched->elecLevel); // mirror +0x278
|
||||
// if (elecLevel == 4 && gen && gen->outputVoltage(+0x1DC)
|
||||
// <= minVoltage(+0x180) * gen->ratedVoltage(+0x1D8))
|
||||
// watchdogAlarm.SetLevel(1); // brownout
|
||||
//
|
||||
// The watchdog MIRRORS the watched subsystem's electrical state -- this is
|
||||
// what drives ElectricalStateLevel()==Ready on the Torso (the twist-rate
|
||||
// power gate) and WatchedVoltageLevel on Searchlight/ThermalSight.
|
||||
//
|
||||
void
|
||||
PowerWatcher::UpdateWatch()
|
||||
{
|
||||
WatchSimulation(0.0f); // FUN_004aeac4 -- heat-alarm mirror
|
||||
|
||||
PoweredSubsystem *watched = (PoweredSubsystem *)watchedLink.Resolve(); // FUN_00417ab4(this+0x114)
|
||||
if (watched == 0)
|
||||
{
|
||||
// The binary derefs unconditionally (the link always binds on the
|
||||
// master node). Null only on replicants / bring-up: hold Not-Ready.
|
||||
watchdogAlarm.SetLevel(0);
|
||||
return;
|
||||
}
|
||||
|
||||
int level = watched->electricalStateAlarm.GetLevel(); // *(watched+0x278)
|
||||
watchdogAlarm.SetLevel(level); // FUN_0041bbd8(this+0x184, level)
|
||||
|
||||
Generator *source = (Generator *)watched->ResolveVoltageSource(); // FUN_00417ab4(watched+0x1d0)
|
||||
if (level == PoweredSubsystem::Ready && source != 0
|
||||
&& source->MeasuredVoltage() <= minVoltage * source->RatedVoltageOf())
|
||||
{
|
||||
watchdogAlarm.SetLevel(1); // brownout: Ready but sagging source
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// The Performance wrapper (PTR @0050f5fc -> @004b181c takes no dt).
|
||||
//
|
||||
void
|
||||
PowerWatcher::Simulation(Scalar /*time_slice*/)
|
||||
{
|
||||
UpdateWatch();
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -553,7 +553,14 @@ class Generator;
|
||||
Logical
|
||||
HandleMessage(int message); // @004b179c (chains to base @004aea84)
|
||||
void
|
||||
Simulation(Scalar time_slice); // @004b1804 (chains to base @004aea9c)
|
||||
ResetToInitialState(Logical powered); // @004b1804 (slot 10; chains to base @004aea9c)
|
||||
void
|
||||
UpdateWatch(); // @004b181c -- watchdog mirror of the watched
|
||||
// subsystem's electrical state (+ brownout);
|
||||
// the Torso sims call this directly
|
||||
void
|
||||
Simulation(Scalar time_slice); // the registered Performance (PTR @0050f5fc
|
||||
// -> @004b181c): forwards to UpdateWatch()
|
||||
|
||||
public:
|
||||
// BASE-CHAIN RE-BASE: the shadow watchedLink was DELETED -- the binary has a
|
||||
|
||||
+101
-14
@@ -94,6 +94,9 @@ namespace {
|
||||
inline Scalar
|
||||
RecordField(Simulation::UpdateRecord *record, int byte_offset)
|
||||
{ return *(const Scalar*)((const char*)record + byte_offset); }
|
||||
inline void
|
||||
WriteRecordField(Simulation::UpdateRecord *record, int byte_offset, Scalar value)
|
||||
{ *(Scalar*)((char*)record + byte_offset) = value; }
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
@@ -243,19 +246,29 @@ Torso::Torso(
|
||||
// rate. TorsoSimulation drives the sweep (below). Remove after verification.
|
||||
if (isDamagedCopy == 0 && getenv("BT_FORCE_TORSO"))
|
||||
{
|
||||
// ⚠ HARNESS TRAP (task #58 post-mortem): this hook used to override the
|
||||
// joint nodes UNCONDITIONALLY with the BLH demo names -- on a mech whose
|
||||
// resource already resolved REAL torso joints (the MadCat's jointtorso)
|
||||
// that silently redirected the whole sweep into the SHADOW hinge, and a
|
||||
// probe run "proved" the cockpit camera ignored the twist (it doesn't;
|
||||
// the joint chain delivers it). Now: only fill joints that did NOT
|
||||
// resolve from the resource; a twist-capable mech sweeps its real ones.
|
||||
horizontalEnabled = True; // @0x250
|
||||
const char *mj = getenv("BT_FORCE_TORSO_JOINT");
|
||||
if (mj == 0 || *mj == '\0') mj = "jointshakey2";
|
||||
horizontalJointNode = ResolveJoint(mj); // torso body (ball)
|
||||
horizontalShadowJointNode = ResolveJoint("jointtshadow"); // shadow twist (hingey)
|
||||
horizontalLimitLeft = 0.7f; // @0x1E0 ~40 deg
|
||||
horizontalLimitRight = -0.7f; // @0x1DC
|
||||
if (horizontalJointNode == 0)
|
||||
{
|
||||
const char *mj = getenv("BT_FORCE_TORSO_JOINT");
|
||||
if (mj == 0 || *mj == '\0') mj = "jointshakey2";
|
||||
horizontalJointNode = ResolveJoint(mj); // torso body (ball)
|
||||
horizontalShadowJointNode = ResolveJoint("jointtshadow"); // shadow twist (hingey)
|
||||
horizontalLimitLeft = 0.7f; // @0x1E0 ~40 deg
|
||||
horizontalLimitRight = -0.7f; // @0x1DC
|
||||
}
|
||||
baseTwistRate = 1.0f; // @0x23C rad/s slew
|
||||
if (getenv("BT_TORSO_LOG"))
|
||||
{
|
||||
DEBUG_STREAM << "[torso] FORCE-ENABLE '" << mj << "' -> " << (void*)horizontalJointNode;
|
||||
DEBUG_STREAM << "[torso] FORCE-ENABLE node=" << (void*)horizontalJointNode;
|
||||
if (horizontalJointNode) DEBUG_STREAM << " type=" << (int)horizontalJointNode->GetJointType();
|
||||
DEBUG_STREAM << " ; shadow 'jointtshadow' -> " << (void*)horizontalShadowJointNode;
|
||||
DEBUG_STREAM << " ; shadow node=" << (void*)horizontalShadowJointNode;
|
||||
if (horizontalShadowJointNode) DEBUG_STREAM << " type=" << (int)horizontalShadowJointNode->GetJointType();
|
||||
DEBUG_STREAM << "\n" << std::flush;
|
||||
}
|
||||
@@ -387,14 +400,17 @@ Logical
|
||||
}
|
||||
|
||||
//
|
||||
// @004b6a78 (slot 6) [CONFIDENT] -- network/replay update record. Samples the
|
||||
// @004b6a78 (slot 6) [CONFIDENT] -- the REPLICANT-side apply (engine semantics
|
||||
// [T0]: ReadUpdateRecord = write the object FROM the record). Samples the
|
||||
// clock (FUN_00414b60) into lastUpdateTime, biasing it forward by one interval
|
||||
// when the elapsed window is below MinSlewMs, chains to Subsystem::WriteUpdateRecord
|
||||
// (FUN_0041bd34), then writes twistAtUpdate / twistVelocity / twistRate from the
|
||||
// record fields (record +0x10 / +0x14 / +0x18).
|
||||
// when the elapsed window is below MinSlewMs, chains to the base apply
|
||||
// (FUN_0041bd34 = ReadUpdateRecord, 2 args), then applies twistAtUpdate /
|
||||
// twistVelocity / twistRate from the record extras (+0x10 / +0x14 / +0x18).
|
||||
// (Task #57: previously mislabeled as WriteUpdateRecord -- the direction flip
|
||||
// made the MASTER consume its own blank record.)
|
||||
//
|
||||
void
|
||||
Torso::WriteUpdateRecord(UpdateRecord *message, int update_model)
|
||||
Torso::ReadUpdateRecord(UpdateRecord *message)
|
||||
{
|
||||
lastUpdateTime = GetCurrentTime(); // @0x254
|
||||
if ((Scalar)(lastUpdateTime - GetCreationTime()) / MsPerSecond < MinSlewMs)
|
||||
@@ -402,13 +418,41 @@ void
|
||||
lastUpdateTime += (lastUpdateTime - GetCreationTime()); // stretch tiny windows
|
||||
}
|
||||
|
||||
Subsystem::WriteUpdateRecord(message, update_model); // FUN_0041bd34
|
||||
Subsystem::ReadUpdateRecord(message); // FUN_0041bd34
|
||||
|
||||
twistAtUpdate = RecordField(message, 0x10); // @0x21C
|
||||
twistVelocity = RecordField(message, 0x14); // @0x1E8
|
||||
twistRate = RecordField(message, 0x18); // @0x238
|
||||
}
|
||||
|
||||
//
|
||||
// @004b6a1c (slot 7) [CONFIDENT, raw-disasm recovery -- Ghidra missed the
|
||||
// function start] -- the MASTER-side serialize (engine semantics [T0]:
|
||||
// WriteUpdateRecord = fill the record FROM the object). Chains the base
|
||||
// producer (FUN_0041c500(this, record, model), 3 args), extends the record to
|
||||
// 0x1C bytes and appends currentTwist / twistVelocity / twistRate at
|
||||
// +0x10/+0x14/+0x18 (exactly what the replicant's @004b6a78 applies), then
|
||||
// snapshots twistAtUpdate = currentTwist:
|
||||
//
|
||||
// 004b6a2e call 0041c500 ; base WriteUpdateRecord
|
||||
// 004b6a38 mov [record], 0x1C ; recordLength = 0x1C
|
||||
// 004b6a44+ record+0x10 = this+0x1D8 ; currentTwist
|
||||
// 004b6a51 record+0x14 = this+0x1E8 ; twistVelocity
|
||||
// 004b6a5a record+0x18 = this+0x238 ; twistRate
|
||||
// 004b6a63+ this+0x21C = this+0x1D8 ; twistAtUpdate = currentTwist
|
||||
//
|
||||
void
|
||||
Torso::WriteUpdateRecord(UpdateRecord *message, int update_model)
|
||||
{
|
||||
Subsystem::WriteUpdateRecord(message, update_model); // FUN_0041c500
|
||||
|
||||
message->recordLength = 0x1C; // base header 0x10 + 3 Scalars
|
||||
WriteRecordField(message, 0x10, currentTwist); // @0x1D8
|
||||
WriteRecordField(message, 0x14, twistVelocity); // @0x1E8
|
||||
WriteRecordField(message, 0x18, twistRate); // @0x238
|
||||
twistAtUpdate = currentTwist; // @0x21C snapshot at send
|
||||
}
|
||||
|
||||
|
||||
//#############################################################################
|
||||
// Per-frame simulation
|
||||
@@ -464,6 +508,34 @@ void
|
||||
analogTwistAxis = ((++s_sweep / 90) & 1) ? -1.0f : 1.0f; // +/- every ~90 frames
|
||||
}
|
||||
|
||||
// TORSO GATE PROBE (BT_TORSO_LOG): why is the twist rate zero?
|
||||
if (getenv("BT_TORSO_LOG"))
|
||||
{
|
||||
static int s_tl = 0;
|
||||
if ((s_tl++ % 120) == 0)
|
||||
{
|
||||
PoweredSubsystem *w = (PoweredSubsystem *)watchedLink.Resolve();
|
||||
DEBUG_STREAM << "[torso] hmOff=" << (int)HeatModelOff()
|
||||
<< " elec=" << (int)ElectricalStateLevel()
|
||||
<< " heatState=" << (int)HeatStateLevel()
|
||||
<< " rate=" << effectiveTwistRate
|
||||
<< " base=" << baseTwistRate
|
||||
<< " hEn=" << (int)horizontalEnabled
|
||||
<< " limits=(" << horizontalLimitRight << ".." << horizontalLimitLeft << ")"
|
||||
<< " axis=" << analogTwistAxis
|
||||
<< " twist=" << currentTwist
|
||||
<< " wIdx=" << watchedSubsystem
|
||||
<< " w=" << (void*)w
|
||||
<< " wElec=" << (w ? w->electricalStateAlarm.GetLevel() : -1)
|
||||
<< " wSrc=" << (void*)(w ? w->ResolveVoltageSource() : 0);
|
||||
Generator *g = (w ? (Generator *)w->ResolveVoltageSource() : 0);
|
||||
if (g)
|
||||
DEBUG_STREAM << " gOut=" << g->MeasuredVoltage()
|
||||
<< " gRated=" << g->RatedVoltageOf()
|
||||
<< " minV=" << minVoltage;
|
||||
DEBUG_STREAM << std::endl;
|
||||
}
|
||||
}
|
||||
Scalar twistStep = effectiveTwistRate * time_slice;
|
||||
Scalar elevStep = effectiveElevationRate * time_slice;
|
||||
|
||||
@@ -616,6 +688,21 @@ void
|
||||
// external joint pass as the master path (see TorsoSimulation for the @004b67ec
|
||||
// note). Harmless in single-player bring-up (no copies); correct for MP.
|
||||
UpdateJoints(); // FUN_004b67ec
|
||||
|
||||
// COPY-SIDE PROBE (BT_TORSO_LOG): where does the replicant's twist come from?
|
||||
if (getenv("BT_TORSO_LOG"))
|
||||
{
|
||||
static int s_cl = 0;
|
||||
if ((s_cl++ % 120) == 0)
|
||||
DEBUG_STREAM << "[torso-copy] cur=" << currentTwist
|
||||
<< " target=" << targetTwist
|
||||
<< " atUpd=" << twistAtUpdate
|
||||
<< " rate=" << twistRate
|
||||
<< " vel=" << twistVelocity
|
||||
<< " lastUpd=" << lastUpdateTime
|
||||
<< " now=" << GetCurrentTime()
|
||||
<< " copy=" << (int)isDamagedCopy << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -156,11 +156,13 @@ class Joint; // engine skeleton node (JOINT.h); the twist target
|
||||
void SetMovedFlag() { ForceUpdate(); }
|
||||
// Local helper: "is value pinned at this software limit?".
|
||||
Logical AtLimit(Scalar value, Scalar limit) const { return fabsf(value - limit) <= 0.0001f; }
|
||||
// CROSS-FAMILY delegate shims -- still no-ops (no layout impact; STEP-4 will
|
||||
// chain them to the real PowerWatcher/HeatWatcher base once the Torso is live).
|
||||
// CROSS-FAMILY delegate shims (STEP-4): UpdateWatch now chains to the real
|
||||
// PowerWatcher per-tick watch body (@004b181c, task #57) -- this is what
|
||||
// raises watchdogAlarm to Ready and un-gates the twist rate. The other
|
||||
// two remain no-ops (no layout impact).
|
||||
void WatcherResetToInitialState() {}
|
||||
Logical WatcherHandleDeathMessage(Message & /*m*/) { return False; }
|
||||
void WatcherUpdateWatch() {}
|
||||
void WatcherUpdateWatch() { UpdateWatch(); } // FUN_004b181c (body: powersub.cpp)
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Test Class Support
|
||||
@@ -189,6 +191,8 @@ class Joint; // engine skeleton node (JOINT.h); the twist target
|
||||
// Controls (@0x1F0 twist, @0x1F4 elevation); proportional, no button ramp.
|
||||
void SetAnalogTwistAxis(Scalar v) { analogTwistAxis = v; }
|
||||
void SetAnalogElevationAxis(Scalar v) { analogElevationAxis = v; }
|
||||
void CommandRecenter() { centerCommand = 1; } // @0x208 (Basic-mode re-center)
|
||||
void ClearRecenterCommand() { centerCommand = 0; } // button released (writer-owned state)
|
||||
Logical GetHorizontalEnabled() const { return horizontalEnabled; } // @0x250 (mapper free-aim gate @004afd10)
|
||||
|
||||
// Reachable horizontal (yaw) half-arc the guns can be brought to bear by
|
||||
@@ -224,7 +228,17 @@ class Joint; // engine skeleton node (JOINT.h); the twist target
|
||||
// Subsystem virtual overrides (slots on vtable @0051103c)
|
||||
//
|
||||
public:
|
||||
void WriteUpdateRecord(UpdateRecord *message, int update_model); // slot 6, @004b6a78
|
||||
// Update-record pair (task #57 direction fix): the ENGINE's semantics
|
||||
// [T0 SIMULATE.cpp] are Write = PRODUCE the outgoing record (master),
|
||||
// Read = APPLY the incoming record (replicant). @004b6a78 (slot 6,
|
||||
// base chain 41bd34 = ReadUpdateRecord) CONSUMES record+0x10/14/18 --
|
||||
// it is the READ side, previously mislabeled Write; the real WRITE
|
||||
// side is @004b6a1c (slot 7, base chain 41c500, 3 args), which Ghidra
|
||||
// missed -- recovered from raw disasm. With the old flip the master
|
||||
// never serialized twist and clobbered its own fields from the
|
||||
// uninitialized stream buffer (the replicant's 0xCDCDCDCD twist pin).
|
||||
void ReadUpdateRecord(UpdateRecord *message); // slot 6, @004b6a78 (apply)
|
||||
void WriteUpdateRecord(UpdateRecord *message, int update_model); // slot 7, @004b6a1c (serialize)
|
||||
Logical HandleDeathMessage(Message &message); // slot 9, @004b5be0 -> @004b179c
|
||||
void ResetToInitialState(); // slot 10, @004b5bf8
|
||||
|
||||
|
||||
@@ -9,8 +9,11 @@ the BLH (3 lasers + 2 PPCs + 2 MissileLaunchers), AddWeapon @004cdac0 store map
|
||||
(part_014.c:4827-4837; both state attrs are named "SimulationState", strings @51d526/51d577).
|
||||
Draw hook = `BTDrawReticle(device)` after the 3D scene, cockpit view only. The binary Execute
|
||||
@004cdcf0 is an UN-EXPORTED gap → Draw dynamics are best-effort [T3] (caret slide from live
|
||||
target range; pip A lit on TargetWithinRange else ring B). Step 6 (blx_cop canopy shell) and the
|
||||
PNAME1-8.bgf 3D pip meshes remain DEFERRED — tracked in context/open-questions.md.
|
||||
target range; pip A lit on TargetWithinRange else ring B). **[SUPERSEDED: Execute @004cdcf0 was
|
||||
RECOVERED task #37 (capstone disasm → reference/decomp/reticle_execute_004cdcf0.disasm.txt) —
|
||||
Draw dynamics are now [T1]; pip lighting is FIRE-CYCLE state, not TargetWithinRange; crosshair
|
||||
positioning re-corrected task #58 — see context/gauges-hud.md.]** Step 6 (blx_cop canopy shell)
|
||||
and the PNAME1-8.bgf 3D pip meshes remain DEFERRED — tracked in context/open-questions.md.
|
||||
|
||||
## The recovered dpl2d recorder API (libDPL 2D display lists) [T1]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user