Combat visible + killable: Wword root-cause fix, .PFX effect layer, RemakeEntity swap
The 'can't kill the enemy / no visible damage' cluster, root-caused and fixed faithfully: - STEP-6 unaimed path was INERT: the cylinder table was 'cached' at Wword(0x111) -- the recon ABSORBER bank (stores nothing, reads 0) -- so every unaimed hit silently no-op'd. Promoted to the named member Mech::damageLookupTable (binary this[0x111], was mislabeled ammoExpended). New gotcha class recorded (reconstruction-gotchas §2) + sweep; 2 dead multiplayer branches logged. - Fire path migrated off the stale vital-zone aim onto the completed STEP-6 unaimed dispatch (zone=-1 + beam entry point -> cylinder resolves the exterior zone). No more invisible 1-shot kills; death via the authentic cascade (~14 center-mass hits). Wreck stays TARGETED on kill (beams stop on it); scoring latches off. - SendSubsystemDamage AV fixed: unbound critical-subsystem plug guard (43 unbound plugs/mech logged as an open question -- the binding itself is a gap). - RemakeEntity (render damage swap): the 1996 render state machine's missing Remake state, reconstructed as an in-place SetDrawObj mesh swap keyed by each segment's damage-zone graphic state (tree dtor doesn't cascade -> never rebuild). Destroyed arms/guns visibly wreck (the only variants the RES registers). - BT .PFX particle layer (L4VIDEO.cpp): the 1995 explosion/damage effect layer, unported since 2007 (DPLIndependantEffect/ReadPSFX/ExplosionScripts all stubs). Parses the authentic VIDEO/*.PFX definitions via the [pfx_day] psfxN mapping; premultiplied blending renders BOTH families from the same data (additive-style fire + occluding smoke -- DDAM2 is 30% grey, DDTHSMK ramps negative: impossible additively); depth-sorted billboards with a radial-masked grit sprite; impact-frame orientation (.PFX offsets are authored mech-local, -Z = out of the struck armor toward the shooter) for weapon hits AND damage bands (via lastInflictingID, now maintained -- was declared but never written). Both effect-number encodings route (raw dpl <100 + WinTesla 1000+slot carried by the band resources). Death fires the authentic dnboom (7) + ddthsmk smoke plume (1). - Effects anchor at the impact point / damaged zone's segment, not the mech origin (no more fire at the feet). - Dev force-input gates BT_AUTOFIRE / BT_AUTODRIVE for headless fire-chain verification; BT_PFX_ADD=1 flips the particle blend for A/B. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
7c455303bd
commit
a3d67cc639
@@ -139,7 +139,10 @@ Built + runtime-verified. Changes:
|
||||
the real `MemoryStream` (`ReadBytes`).
|
||||
- **`mech.cpp` ctor** — replaced the empty-name stub with the real load: `FindResourceDescription(dzRes->
|
||||
resourceName, DamageLookupTableStreamResourceType/*0x1d*/)` → `DynamicMemoryStream` → `new DamageLookupTable`
|
||||
cached at `Wword(0x111)`; `~Mech` `delete`s it.
|
||||
cached in the **named member `Mech::damageLookupTable`** (binary `this[0x111]`/byte 0x444; was mislabeled
|
||||
`ammoExpended` in the offset map); `~Mech` `delete`s it. ⚠ It was ORIGINALLY "cached" at `Wword(0x111)` —
|
||||
the absorber bank — which stores NOTHING, so the whole unaimed path was inert until 2026-07-08 (the
|
||||
"can't kill the enemy" bug). See [[reconstruction-gotchas]] §2. [T2]
|
||||
- **`Mech::TakeDamageMessageHandler`** override registered (`MESSAGE_ENTRY(Mech, TakeDamage)`, overlays
|
||||
Entity's by ID): on `invalidDamageZone`, `msg->damageZone = table->ResolveHit(msg->damageData.impactPoint)`,
|
||||
clear the flag, then base-route. Aimed hits pass straight through.
|
||||
@@ -147,12 +150,22 @@ Built + runtime-verified. Changes:
|
||||
`CylinderReferenceHeight` (`standingTemplateMaxY` = `collisionTemplate->maxY` = the binary `mech+0x2ec[+0xc]`),
|
||||
`TorsoHeading` (via the `BTGetTorsoTwist` bridge in `torso.cpp` → `Torso::CurrentTwist` = torso+0x1d8;
|
||||
torso.hpp can't be `#include`d into mech.cpp — subsystem-stub collision).
|
||||
- **Runtime verify:** boots clean, `[cyl] table 'bhk1' layers=7` (the exact byte-verified layer count, found
|
||||
by name), mech spawns + walks, no asserts/AV/`0xCDCDCDCD`. The live `[cyl] unaimed hit → zone N` path fires
|
||||
on COLLISION damage (zone==−1); not exercised in the solo DEV.EGG (no combat) but the resolver is live +
|
||||
byte-verified. Env gate `BT_CYL_LOG=1`. The recon's empty-name stub (mech.cpp:1219) must be fixed to the real
|
||||
name (the binary copies it from a sibling resource, `local_130+0xc` @part_012.c:10361 — reuse the mech's
|
||||
DamageZoneStream/model name). RE phase COMPLETE; the build (containers + handler + load + wire) is next. `Mech__DamageZone::TakeDamage` → engine armor model (`damageLevel
|
||||
- **Runtime verify (2026-07-08, POST-Wword-fix — the earlier "verified" claim was PARTIAL: the handler
|
||||
wiring was live but the table cache was inert):** with `BT_AUTOFIRE=1 BT_AUTODRIVE=0.5 BT_SPAWN_ENEMY=1`,
|
||||
the full chain runs end-to-end: `[cyl] unaimed hit → zone N` resolves across **19 distinct zones**
|
||||
(front-facing zones 0/6/17/15/4 dominate for head-on fire, as the geometry predicts), structure climbs
|
||||
gradually per hit, the enemy dies after **14** hits (not 1) via the authentic cascade, 4+ zones reach
|
||||
`DestroyedGraphicState`, and the destroyed segment meshes visibly swap (RemakeEntity). Env gates
|
||||
`BT_CYL_LOG=1`, `BT_AUTOFIRE`/`BT_AUTODRIVE` (dev force-inputs, mech4.cpp). [T2]
|
||||
- **Fire path now UNAIMED (2026-07-08):** both bring-up hit paths (the SHOT block + the projectile
|
||||
impact, mech4.cpp) dispatch `zone=−1` + a world impact point (beam entry point = enemy origin shifted
|
||||
~3u toward the shooter at muzzle height; the projectile uses its own impact position) → the cylinder
|
||||
resolves the struck EXTERIOR zone. The old code aimed the internal VITAL zone directly →
|
||||
invisible 1-shot kills (internal zones are soft; the binary never aims them — they die only via the
|
||||
destruction cascade `RecurseSegmentTable`/`SendSubsystemDamage`). On kill the wreck STAYS TARGETED
|
||||
(`gEnemyMech` is NOT nulled) so beams keep terminating on it — nulling it made every later beam a
|
||||
"free" ray that visibly passed through the wreck; scoring latches off via `gEnemyDestroyed`. [T2]
|
||||
`Mech__DamageZone::TakeDamage` → engine armor model (`damageLevel
|
||||
+= damageAmount*damageScale[type]`) → 1.0 = zone destroyed → death. The Mech ctor populates the
|
||||
inherited `damageZones[]` from the DamageZoneStream (type 0x14) resource. `class Damage{ damageType
|
||||
(Collision/Ballistic/Explosive/Laser/Energy), damageAmount, damageForce, surfaceNormal, impactPoint,
|
||||
@@ -178,6 +191,13 @@ Full decomp map of the death path. Two tiers:
|
||||
`SetLegAnimation`/`SetBodyAnimation` = `FUN_004a7fc4`/`FUN_004a800c`), one-shot via `deathAnimationLatched`
|
||||
(mech+0x650). Already reconstructed (mech2.cpp:550-557/817-822). The active drive path DOES call
|
||||
`AdvanceBodyAnimation` (mech4.cpp:317) → reachable.
|
||||
⚠ **CONFLICT with the shipped clip inventory (2026-07-08) [T4 until re-read]:** BTL4.RES holds exactly
|
||||
**27 named clip codes per mech** (`swr wwl wwr wsl wsr wrl wrr rrl rrr rwl rwr bmp bbl bbr bsl bsr sbl
|
||||
sbr ggl ggr gsl gsr wgl wgr sqd squ trn`, + `*i` inside-skeleton variants) — NO explicit death/fall clip
|
||||
name, and indexes 0x1c-0x1f (28-31) exceed a 27-entry table unless the table interleaves the i-variants
|
||||
or carries non-resource entries. The "clips 0x1c-0x1f" claim needs the FUN_004a5028 latch region re-read
|
||||
against the real StandingAnimation table before the collapse is built (task #32). Candidate authentic
|
||||
visuals: the `bmp` (knockdown) family or `sqd` (squat/crumple).
|
||||
- `Mech::IsDestroyed()` (`FUN_0049fb54`) = `movementMode==2||9` (reconstructed as `IsDisabled`, btstubs.cpp);
|
||||
`FUN_004ab1c8` freezes locomotion (zeros mech+0x298) when destroyed.
|
||||
- **`MechDeathHandler`** (ctor `FUN_0042a984` + Performance `FUN_0042aa2c`, cached mech+0x850 / `mech[0x214]`) —
|
||||
|
||||
@@ -82,11 +82,14 @@ authentic path scoped.
|
||||
and composed of runtime-verified pieces. **Note:** validated STEP 6's height ref along the way —
|
||||
`collisionTemplate->maxY ≈ 7.1` (a real mech height), confirming `CylinderReferenceHeight` reads a height,
|
||||
not the heat value the `mech+0x2ec` dual-labeling (heat-gauge sink vs groundRef, mech4.cpp:2697) hinted at.
|
||||
- **✅ Cylinder hit-location (STEP 6) — DONE 2026-07-08 [T2].** Built + runtime-verified: the `dmgtable.cpp`
|
||||
- **✅ Cylinder hit-location (STEP 6) — DONE + LIVE 2026-07-08 [T2].** Built + runtime-verified: the `dmgtable.cpp`
|
||||
classes now have real storage + a working `ResolveHit→zone`, the mech ctor loads the type-0x1d table by the
|
||||
DamageZoneStream name (`[cyl] table 'bhk1' layers=7`), and `Mech::TakeDamageMessageHandler` resolves
|
||||
unaimed (zone==−1) hits before base-routing. Full change list in [[combat-damage]] "STEP 6 COMPLETE". The
|
||||
live collision-damage path (zone==−1 → resolve) is now UNBLOCKED. Historical investigation notes retained
|
||||
unaimed (zone==−1) hits before base-routing. ⚠ The FIRST "verified" claim was PARTIAL: the table was
|
||||
cached via `Wword(0x111)` (the ABSORBER — stores nothing) so every unaimed hit silently no-op'd; fixed
|
||||
2026-07-08 by promoting the cache to the named member `Mech::damageLookupTable`
|
||||
([[reconstruction-gotchas]] §2). Now verified end-to-end (19 zones resolving, `BT_AUTOFIRE` harness).
|
||||
Full change list in [[combat-damage]] "STEP 6 COMPLETE". Historical investigation notes retained
|
||||
below for provenance.
|
||||
**Re-investigated 2026-07 (see [[combat-damage]] for the full algorithm):** the table (height×angle
|
||||
grid, resource type 0x1d) IS built (recon `StandingAnimation` @Mech[0x111], but with an EMPTY name → 0 rows,
|
||||
@@ -101,12 +104,22 @@ authentic path scoped.
|
||||
- **Death sequence — CORE DONE 2026-07-08 [T2]; effects+anim deferred.** `Mech::UpdateDeathState()`
|
||||
(mech4.cpp) reconstructs the death STATE machine (collapse `movementMode` → RP `VTV::DeathShutdown`
|
||||
subsystem loop → settle to disabled/frozen; wreck stays, no crash) — runtime-verified (`BT_DEATH_LOG`).
|
||||
Full map + citations in [[combat-damage]] "Death SEQUENCE". **Remaining** (all in the un-exported master-perf
|
||||
gap 0x4a9770-0x4ab188 OR a big exported reconstruction): (a) the visible **collapse ANIMATION** latch
|
||||
through the active path (+ spawned mechs don't advance body anim); (b) reconstruct **`MechDeathHandler`**
|
||||
(`FUN_0042a984`/`FUN_0042aa2c`) — the EXPORTED per-subsystem destroyed-skin + explosion engine (still a
|
||||
stub, mech.cpp:221) — the biggest visible payoff; (c) the whole-mech **DeathSplash** radius damage. Do NOT
|
||||
issue DestroyEntityMessage on death.
|
||||
Full map + citations in [[combat-damage]] "Death SEQUENCE". ✅ (b) `MechDeathHandler` DONE (mechdmg.cpp) +
|
||||
✅ the render-side **RemakeEntity** destroyed-mesh swap DONE 2026-07-08 (btl4vid.cpp
|
||||
`RemakeEntityRenderables` + `BTRemakeMechModel` bridge; in-place `SetDrawObj` swap — the tree dtor does
|
||||
NOT cascade so never rebuild). **Remaining**: (a) the visible **collapse ANIMATION** latch
|
||||
through the active path (+ spawned mechs don't advance body anim); (c) the whole-mech **DeathSplash**
|
||||
radius damage. Do NOT issue DestroyEntityMessage on death.
|
||||
- **Critical-subsystem plugs UNBOUND (43 skips/mech) [T3].** `MechCriticalSubsystem::subsystemPlug`
|
||||
never gets WIRED to its live subsystem (the ctor's Resolve() at mechdmg.cpp:276 only READS; the
|
||||
binding write is elsewhere/unreconstructed), so `SendSubsystemDamage` skips every entry via the
|
||||
unbound-plug guard (mechdmg.cpp, added 2026-07-08 after a live NULL-deref AV in
|
||||
`SendSubsystemDamage+0x93` — the 1995 binary derefs unchecked because there every plug is bound).
|
||||
Consequence: zone destruction does NOT yet propagate damage into carried subsystems (generator/
|
||||
gyro failures etc.). Also latent there: `SubProxy2` raw-offset reads (databinding trap) once s≠0.
|
||||
- **Two dead Wword comparison branches (multiplayer)**: `mech.cpp:1511` + `:1613` (replicant
|
||||
leg-state/stability sync in ReadUpdateRecord) — always-false BTVal comparisons ([[reconstruction-gotchas]] §2);
|
||||
needs slots 0xf/0x10 promoted to named members when P6 replication work resumes.
|
||||
|
||||
## Multiplayer (Phase 7 / P6)
|
||||
- Cross-pod COMBAT (target a replicant + route damage to the owning master).
|
||||
@@ -115,6 +128,15 @@ authentic path scoped.
|
||||
reconstructed + smoke-tested on one box (two instances share a world, movement replicates).
|
||||
|
||||
## Rendering follow-ups (non-blocking)
|
||||
- **.PFX effect-layer polish (deferred 2026-07-08; the layer itself is LIVE + verified).** The
|
||||
reconstructed BT particle layer (L4VIDEO.cpp, see its banner) renders the authentic .PFX content
|
||||
(fire + smoke, premultiplied blend, impact-frame oriented — weapon hits AND damage bands via
|
||||
`lastInflictingID`, now written by `Mech::TakeDamageMessageHandler`). Remaining nuances, all
|
||||
cosmetic: (a) `atten/attenv` distance attenuation not honoured; (b) the per-file texture name
|
||||
(`btfx:firesmoke1_scr_tex`) — all effects share the radial-masked grit sheet; decoding FIRE.BSL
|
||||
would give the authored sheet; (c) beams draw before particles, so a beam through thick smoke
|
||||
reads too bright (pass-ordering); (d) `colorWarp/alphaWarp` applied as `t^(1/warp)` [T4 convention];
|
||||
(e) emission uses rate×releasePeriod batching [T4 convention vs the exact 1995 emitter].
|
||||
- Replacement-LOD selection for NON-additive multi-LOD models (needs the board's hot-spot/
|
||||
reference-point semantics; leads: `s_dplobject.lod_ranges[16]/lod_hot_spot`, 0x2047/0x2048).
|
||||
- Day/night MATERIAL path priority (BMF/IMG) — infrastructure done, gated `BT_MATPRI` (over-applies
|
||||
|
||||
@@ -39,11 +39,26 @@ copy; (b) it lands at a **different offset** than the binary assumed.
|
||||
(`statusFlags`→`simulationFlags`, `destroyed`→`simulationState`, `statusBits`→`ForceUpdate()`).
|
||||
[T2]
|
||||
|
||||
## 2. Wword(N) — a global scratch bank, NOT this+N
|
||||
## 2. Wword(N) — an ABSORBER, not storage (state cached there VANISHES)
|
||||
|
||||
`mechrecon.hpp:192` defines `Wword(int i)` as `static BTVal bank[0x400]; return bank[i&0x3ff]`.
|
||||
Any `Wword(N)` used for OBJECT access reads/writes a shared global, not `this+i*4`. e.g.
|
||||
`(Mech*)Wword(3)` for a zone's owner → garbage; use `GetOwningSimulation()`. [T2]
|
||||
`mechrecon.hpp:226` defines `Wword(int i)` as `static BTVal bank[0x400]; return bank[i&0x3ff]`,
|
||||
and `BTVal` is the recon **absorber** type: `operator=` stores NOTHING, every read converts to
|
||||
`T()` (zero), and ALL comparisons (`==` and `!=`, vs BTVal or int) return **false**. Consequences: [T2]
|
||||
- Any state CACHED via `Wword(N) = x` silently vanishes; the later read is always 0/null.
|
||||
**Archetype:** the STEP-6 cylinder table was "cached" at `Wword(0x111)` → the unaimed
|
||||
TakeDamage path was totally inert (every hit no-op'd, "can't kill the enemy") while the
|
||||
ctor log looked fine. Fix = a real named member (`Mech::damageLookupTable`). If a Wword
|
||||
slot must hold real state, PROMOTE it to a named member mapped to that binary offset —
|
||||
check `mech.hpp`'s offset map first (the slot may already exist under a best-effort
|
||||
mislabel; 0x111 was mislabeled `ammoExpended`).
|
||||
- `if (Wword(a) != Wword(b))` and `if (Wword(a) == 2)` are BOTH always-false → the guarded
|
||||
branch is dead code. Known dead sites: `mech.cpp:1511` + `mech.cpp:1613` (replicant
|
||||
leg-state / stability-alarm sync in ReadUpdateRecord — multiplayer-only, deferred).
|
||||
- Any `Wword(N)` used for OBJECT access reads a shared global, not `this+i*4`; e.g.
|
||||
`(Mech*)Wword(3)` for a zone's owner → garbage; use `GetOwningSimulation()`.
|
||||
|
||||
Sweep recipe: `grep -nE "\((int|void|[A-Z]\w+) ?\*\)\s*Wword\(|if \(Wword\(|Wword\([^)]+\)\s*(!=|==)"` —
|
||||
every hit is either dead code or a vanished cache.
|
||||
|
||||
## 3. Databinding trap — raw offsets read garbage
|
||||
|
||||
|
||||
Reference in New Issue
Block a user