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`).
|
the real `MemoryStream` (`ReadBytes`).
|
||||||
- **`mech.cpp` ctor** — replaced the empty-name stub with the real load: `FindResourceDescription(dzRes->
|
- **`mech.cpp` ctor** — replaced the empty-name stub with the real load: `FindResourceDescription(dzRes->
|
||||||
resourceName, DamageLookupTableStreamResourceType/*0x1d*/)` → `DynamicMemoryStream` → `new DamageLookupTable`
|
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
|
- **`Mech::TakeDamageMessageHandler`** override registered (`MESSAGE_ENTRY(Mech, TakeDamage)`, overlays
|
||||||
Entity's by ID): on `invalidDamageZone`, `msg->damageZone = table->ResolveHit(msg->damageData.impactPoint)`,
|
Entity's by ID): on `invalidDamageZone`, `msg->damageZone = table->ResolveHit(msg->damageData.impactPoint)`,
|
||||||
clear the flag, then base-route. Aimed hits pass straight through.
|
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]`),
|
`CylinderReferenceHeight` (`standingTemplateMaxY` = `collisionTemplate->maxY` = the binary `mech+0x2ec[+0xc]`),
|
||||||
`TorsoHeading` (via the `BTGetTorsoTwist` bridge in `torso.cpp` → `Torso::CurrentTwist` = torso+0x1d8;
|
`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).
|
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
|
- **Runtime verify (2026-07-08, POST-Wword-fix — the earlier "verified" claim was PARTIAL: the handler
|
||||||
by name), mech spawns + walks, no asserts/AV/`0xCDCDCDCD`. The live `[cyl] unaimed hit → zone N` path fires
|
wiring was live but the table cache was inert):** with `BT_AUTOFIRE=1 BT_AUTODRIVE=0.5 BT_SPAWN_ENEMY=1`,
|
||||||
on COLLISION damage (zone==−1); not exercised in the solo DEV.EGG (no combat) but the resolver is live +
|
the full chain runs end-to-end: `[cyl] unaimed hit → zone N` resolves across **19 distinct zones**
|
||||||
byte-verified. Env gate `BT_CYL_LOG=1`. The recon's empty-name stub (mech.cpp:1219) must be fixed to the real
|
(front-facing zones 0/6/17/15/4 dominate for head-on fire, as the geometry predicts), structure climbs
|
||||||
name (the binary copies it from a sibling resource, `local_130+0xc` @part_012.c:10361 — reuse the mech's
|
gradually per hit, the enemy dies after **14** hits (not 1) via the authentic cascade, 4+ zones reach
|
||||||
DamageZoneStream/model name). RE phase COMPLETE; the build (containers + handler + load + wire) is next. `Mech__DamageZone::TakeDamage` → engine armor model (`damageLevel
|
`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
|
+= damageAmount*damageScale[type]`) → 1.0 = zone destroyed → death. The Mech ctor populates the
|
||||||
inherited `damageZones[]` from the DamageZoneStream (type 0x14) resource. `class Damage{ damageType
|
inherited `damageZones[]` from the DamageZoneStream (type 0x14) resource. `class Damage{ damageType
|
||||||
(Collision/Ballistic/Explosive/Laser/Energy), damageAmount, damageForce, surfaceNormal, impactPoint,
|
(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`
|
`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
|
(mech+0x650). Already reconstructed (mech2.cpp:550-557/817-822). The active drive path DOES call
|
||||||
`AdvanceBodyAnimation` (mech4.cpp:317) → reachable.
|
`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);
|
- `Mech::IsDestroyed()` (`FUN_0049fb54`) = `movementMode==2||9` (reconstructed as `IsDisabled`, btstubs.cpp);
|
||||||
`FUN_004ab1c8` freezes locomotion (zeros mech+0x298) when destroyed.
|
`FUN_004ab1c8` freezes locomotion (zeros mech+0x298) when destroyed.
|
||||||
- **`MechDeathHandler`** (ctor `FUN_0042a984` + Performance `FUN_0042aa2c`, cached mech+0x850 / `mech[0x214]`) —
|
- **`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 —
|
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,
|
`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.
|
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
|
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
|
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
|
unaimed (zone==−1) hits before base-routing. ⚠ The FIRST "verified" claim was PARTIAL: the table was
|
||||||
live collision-damage path (zone==−1 → resolve) is now UNBLOCKED. Historical investigation notes retained
|
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.
|
below for provenance.
|
||||||
**Re-investigated 2026-07 (see [[combat-damage]] for the full algorithm):** the table (height×angle
|
**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,
|
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()`
|
- **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`
|
(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`).
|
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
|
Full map + citations in [[combat-damage]] "Death SEQUENCE". ✅ (b) `MechDeathHandler` DONE (mechdmg.cpp) +
|
||||||
gap 0x4a9770-0x4ab188 OR a big exported reconstruction): (a) the visible **collapse ANIMATION** latch
|
✅ the render-side **RemakeEntity** destroyed-mesh swap DONE 2026-07-08 (btl4vid.cpp
|
||||||
through the active path (+ spawned mechs don't advance body anim); (b) reconstruct **`MechDeathHandler`**
|
`RemakeEntityRenderables` + `BTRemakeMechModel` bridge; in-place `SetDrawObj` swap — the tree dtor does
|
||||||
(`FUN_0042a984`/`FUN_0042aa2c`) — the EXPORTED per-subsystem destroyed-skin + explosion engine (still a
|
NOT cascade so never rebuild). **Remaining**: (a) the visible **collapse ANIMATION** latch
|
||||||
stub, mech.cpp:221) — the biggest visible payoff; (c) the whole-mech **DeathSplash** radius damage. Do NOT
|
through the active path (+ spawned mechs don't advance body anim); (c) the whole-mech **DeathSplash**
|
||||||
issue DestroyEntityMessage on death.
|
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)
|
## Multiplayer (Phase 7 / P6)
|
||||||
- Cross-pod COMBAT (target a replicant + route damage to the owning master).
|
- 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).
|
reconstructed + smoke-tested on one box (two instances share a world, movement replicates).
|
||||||
|
|
||||||
## Rendering follow-ups (non-blocking)
|
## 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/
|
- 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).
|
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
|
- 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()`).
|
(`statusFlags`→`simulationFlags`, `destroyed`→`simulationState`, `statusBits`→`ForceUpdate()`).
|
||||||
[T2]
|
[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]`.
|
`mechrecon.hpp:226` 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.
|
and `BTVal` is the recon **absorber** type: `operator=` stores NOTHING, every read converts to
|
||||||
`(Mech*)Wword(3)` for a zone's owner → garbage; use `GetOwningSimulation()`. [T2]
|
`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
|
## 3. Databinding trap — raw offsets read garbage
|
||||||
|
|
||||||
|
|||||||
+556
-5
@@ -25,6 +25,7 @@ using namespace std;
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <hash_map>
|
#include <hash_map>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <algorithm> // std::sort (the .PFX particle depth sort)
|
||||||
|
|
||||||
LPDIRECT3D9 gD3D = NULL;
|
LPDIRECT3D9 gD3D = NULL;
|
||||||
|
|
||||||
@@ -58,6 +59,11 @@ struct BTBeamFx
|
|||||||
};
|
};
|
||||||
static std::vector<BTBeamFx> gBTBeams;
|
static std::vector<BTBeamFx> gBTBeams;
|
||||||
|
|
||||||
|
// the decoded beam grit sheet (built lazily in BTDrawBeams; shared with the
|
||||||
|
// .PFX particle layer below)
|
||||||
|
LPDIRECT3DTEXTURE9 gBTBeamGritTex = 0;
|
||||||
|
LPDIRECT3DTEXTURE9 BTGetBeamGritTexture() { return gBTBeamGritTex; }
|
||||||
|
|
||||||
// Called from the game (mech4.cpp) -- external linkage, matched by an extern decl there.
|
// Called from the game (mech4.cpp) -- external linkage, matched by an extern decl there.
|
||||||
// LOD eyepoint feed (game -> renderer): the viewpoint mech's position, the
|
// LOD eyepoint feed (game -> renderer): the viewpoint mech's position, the
|
||||||
// authentic LOD reference (see the ExecuteImplementation banner). mech4 calls
|
// authentic LOD reference (see the ExecuteImplementation banner). mech4 calls
|
||||||
@@ -121,8 +127,10 @@ void BTDrawBeams(LPDIRECT3DDEVICE9 dev, const D3DXMATRIX *view, float dt)
|
|||||||
// once, ramp its luminance to a grayscale grit texture, and MODULATE the beam
|
// once, ramp its luminance to a grayscale grit texture, and MODULATE the beam
|
||||||
// colour by it with a scrolling UV -> the streaming "gritty" look, not a clean
|
// colour by it with a scrolling UV -> the streaming "gritty" look, not a clean
|
||||||
// gradient. BT_BEAM_TEX=0 falls back to the plain additive beam.
|
// gradient. BT_BEAM_TEX=0 falls back to the plain additive beam.
|
||||||
|
// (file-scope so the .PFX particle layer below can share the decoded sheet)
|
||||||
static int s_beamTexTried = 0;
|
static int s_beamTexTried = 0;
|
||||||
static LPDIRECT3DTEXTURE9 s_beamTex = 0;
|
extern LPDIRECT3DTEXTURE9 gBTBeamGritTex;
|
||||||
|
LPDIRECT3DTEXTURE9 &s_beamTex = gBTBeamGritTex;
|
||||||
if (!s_beamTexTried)
|
if (!s_beamTexTried)
|
||||||
{
|
{
|
||||||
s_beamTexTried = 1;
|
s_beamTexTried = 1;
|
||||||
@@ -361,6 +369,484 @@ void BTDrawBeams(LPDIRECT3DDEVICE9 dev, const D3DXMATRIX *view, float dt)
|
|||||||
dev->SetSamplerState(0, D3DSAMP_ADDRESSV, sAddrV);
|
dev->SetSamplerState(0, D3DSAMP_ADDRESSV, sAddrV);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//===========================================================================//
|
||||||
|
// BT PARTICLE EFFECTS (.PFX) -- the 1995 explosion/damage effect layer.
|
||||||
|
//
|
||||||
|
// The pod's damage/explosion visuals are DATA: VIDEO/*.PFX text files (format
|
||||||
|
// documented inside each file), mapped to dpl effect NUMBERS by BTDPL.INI's
|
||||||
|
// [pfx_day]/[pfx_night] pages ("psfxN=file.pfx" -- N is the number embedded in
|
||||||
|
// resources: 2..5 = the mech damage bands, 6 = projectile-gun hit, 7 = the
|
||||||
|
// mech-death explosion, 8 = zone destroyed...). The 1995 board consumed these
|
||||||
|
// through dpl particle calls that were never ported: DPLIndependantEffect's
|
||||||
|
// (<100) arm and ReadPSFX are both stubbed, so every weapon-hit / damage-band /
|
||||||
|
// death explosion rendered NOTHING. Like the weapon-beam layer above, this is
|
||||||
|
// the self-contained D3D9 port of that layer:
|
||||||
|
// BTLoadPfxFile -- parses a .PFX into BTPfxDef (the documented format)
|
||||||
|
// BTStartPfx -- starts an emitter instance at a world position
|
||||||
|
// (called by DPLIndependantEffect for effect_number < 100)
|
||||||
|
// BTDrawPfx -- per-frame sim + camera-facing additive-quad billboards
|
||||||
|
// (called beside BTDrawBeams in the render loop)
|
||||||
|
// Variance convention (decoded from DNBOOM.PFX: vel 150 + var -300 = a
|
||||||
|
// symmetric +-150 burst): sampled = value + variance * rand01().
|
||||||
|
// NOT yet honoured (noted, low-visibility): atten/attenv (distance
|
||||||
|
// attenuation), colorWarp/alphaWarp exponents are applied as t^(1/warp),
|
||||||
|
// the per-file texture name (all BT effects use the firesmoke sheet; we use
|
||||||
|
// the same decoded grit texture as the beams).
|
||||||
|
//===========================================================================//
|
||||||
|
struct BTPfxDef
|
||||||
|
{
|
||||||
|
int valid;
|
||||||
|
unsigned identifier;
|
||||||
|
int maxIssue; // total particles an instance may issue
|
||||||
|
float releasePeriod; // batch interval (s)
|
||||||
|
float rate; // particles per second while releasing
|
||||||
|
float px, py, pz, pv; // spawn offset + positional variance
|
||||||
|
float velx, vely, velz, velxv, velyv, velzv;
|
||||||
|
float rad, radv, exp, expv, dexp, dexpv; // radius, expansion rate, expansion decay
|
||||||
|
float accelx, accely, accelz, accelxv, accelyv, accelzv;
|
||||||
|
float atten, attenv; // (not yet honoured)
|
||||||
|
float sI[4], sIv[4]; // start colour inner RGBA + variance
|
||||||
|
float sO[4], sOv[4]; // start colour outer RGBA + variance
|
||||||
|
float eI[4], eIv[4]; // end colour inner RGBA + variance
|
||||||
|
float eO[4], eOv[4]; // end colour outer RGBA + variance
|
||||||
|
float colorWarp, alphaWarp;
|
||||||
|
float duration, durationv; // particle lifetime
|
||||||
|
};
|
||||||
|
#define BT_PFX_SLOTS 32
|
||||||
|
static BTPfxDef gBTPfxDefs[BT_PFX_SLOTS];
|
||||||
|
|
||||||
|
struct BTPfxEmitter
|
||||||
|
{
|
||||||
|
const BTPfxDef *def;
|
||||||
|
D3DXVECTOR3 pos;
|
||||||
|
// The effect's LOCAL FRAME (.PFX offsets/velocities are authored in the
|
||||||
|
// victim's mech-local space -- e.g. DAFC.PFX sprays -Z, out through the
|
||||||
|
// front armor toward the shooter). Identity when no frame is known.
|
||||||
|
D3DXVECTOR3 ax, ay, az;
|
||||||
|
float sinceRelease;
|
||||||
|
int issued;
|
||||||
|
int active;
|
||||||
|
};
|
||||||
|
struct BTPfxParticle
|
||||||
|
{
|
||||||
|
D3DXVECTOR3 pos, vel, accel;
|
||||||
|
float age, life;
|
||||||
|
float rad, exp, dexp;
|
||||||
|
float colorWarp, alphaWarp; // def-level warps, carried per particle
|
||||||
|
float sI[4], sO[4], eI[4], eO[4];
|
||||||
|
};
|
||||||
|
static std::vector<BTPfxEmitter> gBTPfxEmitters;
|
||||||
|
static std::vector<BTPfxParticle> gBTPfxParticles;
|
||||||
|
|
||||||
|
static float BTPfxRand01() // cheap deterministic LCG
|
||||||
|
{
|
||||||
|
static unsigned s = 0x2545F491u;
|
||||||
|
s = s * 1664525u + 1013904223u;
|
||||||
|
return (float)((s >> 8) & 0xFFFFFF) / 16777215.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse one .PFX text file (VIDEO\<name>) into a def slot. Format per the
|
||||||
|
// spec block carried inside every .PFX (and ReadPSFX's comment).
|
||||||
|
static int BTLoadPfxFile(const char *file_name, BTPfxDef &d)
|
||||||
|
{
|
||||||
|
char path[256];
|
||||||
|
strcpy(path, "VIDEO\\");
|
||||||
|
strcat(path, file_name);
|
||||||
|
FILE *fp = fopen(path, "rt");
|
||||||
|
if (!fp)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "[pfx] could not open " << path << " -- its effects will not draw\n" << std::flush;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
memset(&d, 0, sizeof(d));
|
||||||
|
char line[256];
|
||||||
|
int ok = 1;
|
||||||
|
// line 1: texture name (recorded in the banner note; the shared sheet is used)
|
||||||
|
if (!fgets(line, sizeof(line), fp)) ok = 0;
|
||||||
|
if (ok && fgets(line, sizeof(line), fp))
|
||||||
|
ok = (sscanf(line, "%x %d %f %f", &d.identifier, &d.maxIssue, &d.releasePeriod, &d.rate) == 4);
|
||||||
|
else ok = 0;
|
||||||
|
if (ok && fgets(line, sizeof(line), fp))
|
||||||
|
ok = (sscanf(line, "%f %f %f %f", &d.px, &d.py, &d.pz, &d.pv) == 4);
|
||||||
|
else ok = 0;
|
||||||
|
if (ok && fgets(line, sizeof(line), fp))
|
||||||
|
ok = (sscanf(line, "%f %f %f %f %f %f", &d.velx, &d.vely, &d.velz, &d.velxv, &d.velyv, &d.velzv) == 6);
|
||||||
|
else ok = 0;
|
||||||
|
if (ok && fgets(line, sizeof(line), fp))
|
||||||
|
ok = (sscanf(line, "%f %f %f %f %f %f", &d.rad, &d.radv, &d.exp, &d.expv, &d.dexp, &d.dexpv) == 6);
|
||||||
|
else ok = 0;
|
||||||
|
if (ok && fgets(line, sizeof(line), fp))
|
||||||
|
ok = (sscanf(line, "%f %f %f %f %f %f", &d.accelx, &d.accely, &d.accelz, &d.accelxv, &d.accelyv, &d.accelzv) == 6);
|
||||||
|
else ok = 0;
|
||||||
|
if (ok && fgets(line, sizeof(line), fp))
|
||||||
|
ok = (sscanf(line, "%f %f", &d.atten, &d.attenv) == 2);
|
||||||
|
else ok = 0;
|
||||||
|
float *quads[8] = { d.sI, d.sIv, d.sO, d.sOv, d.eI, d.eIv, d.eO, d.eOv };
|
||||||
|
for (int q = 0; ok && q < 4; ++q) // 4 lines: sI+var, sO+var, eI+var, eO+var
|
||||||
|
{
|
||||||
|
if (fgets(line, sizeof(line), fp))
|
||||||
|
ok = (sscanf(line, "%f %f %f %f %f %f %f %f",
|
||||||
|
&quads[q*2][0], &quads[q*2][1], &quads[q*2][2], &quads[q*2][3],
|
||||||
|
&quads[q*2+1][0], &quads[q*2+1][1], &quads[q*2+1][2], &quads[q*2+1][3]) == 8);
|
||||||
|
else ok = 0;
|
||||||
|
}
|
||||||
|
if (ok && fgets(line, sizeof(line), fp))
|
||||||
|
ok = (sscanf(line, "%f %f", &d.colorWarp, &d.alphaWarp) == 2);
|
||||||
|
else ok = 0;
|
||||||
|
if (ok && fgets(line, sizeof(line), fp))
|
||||||
|
ok = (sscanf(line, "%f %f", &d.duration, &d.durationv) == 2);
|
||||||
|
else ok = 0;
|
||||||
|
fclose(fp);
|
||||||
|
if (!ok)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "[pfx] " << path << " did not parse -- effect disabled\n" << std::flush;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
d.valid = 1;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slot-checked load entry used by the psfx page loader (LoadMission walk).
|
||||||
|
int BTLoadPfxFile_slot(const char *file_name, int slot)
|
||||||
|
{
|
||||||
|
if (slot < 0 || slot >= BT_PFX_SLOTS)
|
||||||
|
return 0;
|
||||||
|
return BTLoadPfxFile(file_name, gBTPfxDefs[slot]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start one effect instance at a world position (the DPLIndependantEffect
|
||||||
|
// contract: renderer-owned, runs to termination on its own). The optional
|
||||||
|
// frame rows orient the .PFX's mech-local offsets/velocities in the world
|
||||||
|
// (pass the victim entity's localToWorld X/Y/Z rows); identity when absent.
|
||||||
|
void BTStartPfxFrame(int effect_number, float x, float y, float z,
|
||||||
|
const float *xrow, const float *yrow, const float *zrow)
|
||||||
|
{
|
||||||
|
if (effect_number < 0 || effect_number >= BT_PFX_SLOTS)
|
||||||
|
return;
|
||||||
|
const BTPfxDef &d = gBTPfxDefs[effect_number];
|
||||||
|
if (!d.valid)
|
||||||
|
return;
|
||||||
|
if (gBTPfxEmitters.size() > 64) // runaway guard
|
||||||
|
return;
|
||||||
|
BTPfxEmitter e;
|
||||||
|
e.def = &d;
|
||||||
|
e.pos = D3DXVECTOR3(x, y, z);
|
||||||
|
e.ax = xrow ? D3DXVECTOR3(xrow[0], xrow[1], xrow[2]) : D3DXVECTOR3(1, 0, 0);
|
||||||
|
e.ay = yrow ? D3DXVECTOR3(yrow[0], yrow[1], yrow[2]) : D3DXVECTOR3(0, 1, 0);
|
||||||
|
e.az = zrow ? D3DXVECTOR3(zrow[0], zrow[1], zrow[2]) : D3DXVECTOR3(0, 0, 1);
|
||||||
|
e.sinceRelease = 1e9f; // first batch releases immediately
|
||||||
|
e.issued = 0;
|
||||||
|
e.active = 1;
|
||||||
|
gBTPfxEmitters.push_back(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BTStartPfx(int effect_number, float x, float y, float z)
|
||||||
|
{
|
||||||
|
BTStartPfxFrame(effect_number, x, y, z, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void BTPfxSpawn(const BTPfxEmitter &e)
|
||||||
|
{
|
||||||
|
const BTPfxDef &d = *e.def;
|
||||||
|
if (gBTPfxParticles.size() > 2048) // global particle cap
|
||||||
|
return;
|
||||||
|
BTPfxParticle p;
|
||||||
|
// Sample in the effect's LOCAL frame, then orient into the world through
|
||||||
|
// the emitter's basis (the victim's localToWorld rows). The position
|
||||||
|
// jitter pv is an isotropic scatter about the base offset -> symmetric
|
||||||
|
// (rand +-pv); the paired variances stay value + var*rand01 (DNBOOM's
|
||||||
|
// vel 150 / var -300 decodes to the symmetric +-150 burst).
|
||||||
|
D3DXVECTOR3 lp(
|
||||||
|
d.px + d.pv * (BTPfxRand01() * 2.0f - 1.0f),
|
||||||
|
d.py + d.pv * (BTPfxRand01() * 2.0f - 1.0f),
|
||||||
|
d.pz + d.pv * (BTPfxRand01() * 2.0f - 1.0f));
|
||||||
|
D3DXVECTOR3 lv(
|
||||||
|
d.velx + d.velxv * BTPfxRand01(),
|
||||||
|
d.vely + d.velyv * BTPfxRand01(),
|
||||||
|
d.velz + d.velzv * BTPfxRand01());
|
||||||
|
D3DXVECTOR3 la(
|
||||||
|
d.accelx + d.accelxv * BTPfxRand01(),
|
||||||
|
d.accely + d.accelyv * BTPfxRand01(),
|
||||||
|
d.accelz + d.accelzv * BTPfxRand01());
|
||||||
|
p.pos = e.pos + e.ax * lp.x + e.ay * lp.y + e.az * lp.z;
|
||||||
|
p.vel = e.ax * lv.x + e.ay * lv.y + e.az * lv.z;
|
||||||
|
p.accel = e.ax * la.x + e.ay * la.y + e.az * la.z;
|
||||||
|
p.age = 0.0f;
|
||||||
|
p.life = d.duration + d.durationv * BTPfxRand01();
|
||||||
|
if (p.life < 0.05f) p.life = 0.05f;
|
||||||
|
p.rad = d.rad + d.radv * BTPfxRand01();
|
||||||
|
p.exp = d.exp + d.expv * BTPfxRand01();
|
||||||
|
p.dexp = d.dexp + d.dexpv * BTPfxRand01();
|
||||||
|
p.colorWarp = (d.colorWarp > 1e-3f) ? d.colorWarp : 1.0f;
|
||||||
|
p.alphaWarp = (d.alphaWarp > 1e-3f) ? d.alphaWarp : 1.0f;
|
||||||
|
for (int i = 0; i < 4; ++i)
|
||||||
|
{
|
||||||
|
p.sI[i] = d.sI[i] + d.sIv[i] * BTPfxRand01();
|
||||||
|
p.sO[i] = d.sO[i] + d.sOv[i] * BTPfxRand01();
|
||||||
|
p.eI[i] = d.eI[i] + d.eIv[i] * BTPfxRand01();
|
||||||
|
p.eO[i] = d.eO[i] + d.eOv[i] * BTPfxRand01();
|
||||||
|
}
|
||||||
|
gBTPfxParticles.push_back(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The beams' decoded grit texture (built in BTDrawBeams) -- shared with the
|
||||||
|
// particles so the fire quads carry the authentic firesmoke-family noise.
|
||||||
|
extern LPDIRECT3DTEXTURE9 BTGetBeamGritTexture();
|
||||||
|
|
||||||
|
void BTDrawPfx(LPDIRECT3DDEVICE9 dev, const D3DXMATRIX *view, float dt)
|
||||||
|
{
|
||||||
|
// ---- sim ----
|
||||||
|
if (dt > 0.1f) dt = 0.1f; // stall guard
|
||||||
|
for (size_t ei = 0; ei < gBTPfxEmitters.size(); ++ei)
|
||||||
|
{
|
||||||
|
BTPfxEmitter &e = gBTPfxEmitters[ei];
|
||||||
|
if (!e.active) continue;
|
||||||
|
const BTPfxDef &d = *e.def;
|
||||||
|
e.sinceRelease += dt;
|
||||||
|
if (e.sinceRelease >= d.releasePeriod)
|
||||||
|
{
|
||||||
|
// one batch: rate particles/sec over the release period, total-capped
|
||||||
|
int batch = (int)(d.rate * (d.releasePeriod > 1e-4f ? d.releasePeriod : dt));
|
||||||
|
if (batch < 1) batch = 1;
|
||||||
|
if (e.issued + batch > d.maxIssue) batch = d.maxIssue - e.issued;
|
||||||
|
for (int b = 0; b < batch; ++b)
|
||||||
|
BTPfxSpawn(e);
|
||||||
|
e.issued += batch;
|
||||||
|
e.sinceRelease = 0.0f;
|
||||||
|
if (e.issued >= d.maxIssue)
|
||||||
|
e.active = 0; // done issuing -> instance ends
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{ // compact finished emitters
|
||||||
|
size_t w = 0;
|
||||||
|
for (size_t i = 0; i < gBTPfxEmitters.size(); ++i)
|
||||||
|
if (gBTPfxEmitters[i].active) gBTPfxEmitters[w++] = gBTPfxEmitters[i];
|
||||||
|
gBTPfxEmitters.resize(w);
|
||||||
|
}
|
||||||
|
{ // advance + expire particles
|
||||||
|
size_t w = 0;
|
||||||
|
for (size_t i = 0; i < gBTPfxParticles.size(); ++i)
|
||||||
|
{
|
||||||
|
BTPfxParticle &p = gBTPfxParticles[i];
|
||||||
|
p.age += dt;
|
||||||
|
if (p.age >= p.life) continue;
|
||||||
|
p.vel += p.accel * dt;
|
||||||
|
p.pos += p.vel * dt;
|
||||||
|
p.rad += p.exp * dt;
|
||||||
|
p.exp += p.dexp * dt;
|
||||||
|
if (p.rad < 0.05f) p.rad = 0.05f;
|
||||||
|
gBTPfxParticles[w++] = p;
|
||||||
|
}
|
||||||
|
gBTPfxParticles.resize(w);
|
||||||
|
}
|
||||||
|
if (gBTPfxParticles.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
// ---- draw: camera-facing additive quads (inner core + outer glow) ----
|
||||||
|
const D3DXVECTOR3 right(view->_11, view->_21, view->_31);
|
||||||
|
const D3DXVECTOR3 up (view->_12, view->_22, view->_32);
|
||||||
|
|
||||||
|
DWORD sLight, sFog, sZW, sBlend, sSrc, sDst, sCull, sCop, sCa1, sCa2, sAop, sAa1, sAa2;
|
||||||
|
dev->GetRenderState(D3DRS_LIGHTING, &sLight);
|
||||||
|
dev->GetRenderState(D3DRS_FOGENABLE, &sFog);
|
||||||
|
dev->GetRenderState(D3DRS_ZWRITEENABLE, &sZW);
|
||||||
|
dev->GetRenderState(D3DRS_ALPHABLENDENABLE, &sBlend);
|
||||||
|
dev->GetRenderState(D3DRS_SRCBLEND, &sSrc);
|
||||||
|
dev->GetRenderState(D3DRS_DESTBLEND, &sDst);
|
||||||
|
dev->GetRenderState(D3DRS_CULLMODE, &sCull);
|
||||||
|
dev->GetTextureStageState(0, D3DTSS_COLOROP, &sCop);
|
||||||
|
dev->GetTextureStageState(0, D3DTSS_COLORARG1, &sCa1);
|
||||||
|
dev->GetTextureStageState(0, D3DTSS_COLORARG2, &sCa2);
|
||||||
|
dev->GetTextureStageState(0, D3DTSS_ALPHAOP, &sAop);
|
||||||
|
dev->GetTextureStageState(0, D3DTSS_ALPHAARG1, &sAa1);
|
||||||
|
dev->GetTextureStageState(0, D3DTSS_ALPHAARG2, &sAa2);
|
||||||
|
|
||||||
|
// PREMULTIPLIED blending (ONE, INVSRCALPHA): the one model that renders BOTH
|
||||||
|
// authored particle families correctly. FIRE (bright colour, the additive
|
||||||
|
// glow) and SMOKE (dark/negative-ramp colour with high alpha -> OCCLUDES the
|
||||||
|
// scene behind it -- DDAM2 is 30% grey, DDTHSMK ramps to negative: both are
|
||||||
|
// invisible under pure additive, which is why damaged mechs never smoked).
|
||||||
|
// Vertex colour carries colour*alpha (premultiplied), vertex alpha carries
|
||||||
|
// the occlusion; the texture's falloff mask modulates both.
|
||||||
|
// BT_PFX_ADD=1 flips back to pure additive for A/B comparison.
|
||||||
|
static int s_pfxAdditive = -1;
|
||||||
|
if (s_pfxAdditive < 0)
|
||||||
|
{
|
||||||
|
const char *av = getenv("BT_PFX_ADD");
|
||||||
|
s_pfxAdditive = (av != 0 && av[0] == '1') ? 1 : 0;
|
||||||
|
}
|
||||||
|
dev->SetRenderState(D3DRS_LIGHTING, FALSE);
|
||||||
|
dev->SetRenderState(D3DRS_FOGENABLE, FALSE);
|
||||||
|
dev->SetRenderState(D3DRS_ZWRITEENABLE, FALSE); // test Z, don't write it
|
||||||
|
dev->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
|
||||||
|
dev->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ONE);
|
||||||
|
dev->SetRenderState(D3DRS_DESTBLEND, s_pfxAdditive ? D3DBLEND_ONE : D3DBLEND_INVSRCALPHA);
|
||||||
|
dev->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
|
||||||
|
|
||||||
|
// Particle sprite texture: the beams' authentic grit sheet MASKED by a
|
||||||
|
// radial falloff -> soft round fiery blobs (an unmasked square sheet on an
|
||||||
|
// additive quad reads as a hard BOX, which the 1995 sprites never did).
|
||||||
|
// Baked once from the decoded grit; pure radial gradient if grit is absent.
|
||||||
|
static LPDIRECT3DTEXTURE9 s_pfxTex = 0;
|
||||||
|
static int s_pfxTexTried = 0;
|
||||||
|
if (!s_pfxTexTried)
|
||||||
|
{
|
||||||
|
s_pfxTexTried = 1;
|
||||||
|
const int TW = 64, TH = 64;
|
||||||
|
LPDIRECT3DTEXTURE9 grit = BTGetBeamGritTexture();
|
||||||
|
unsigned char gritLum[128 * 64]; // grit is 128x64 when present
|
||||||
|
int gw = 0, gh = 0;
|
||||||
|
if (grit)
|
||||||
|
{
|
||||||
|
D3DSURFACE_DESC gd;
|
||||||
|
if (SUCCEEDED(grit->GetLevelDesc(0, &gd)) && gd.Width <= 128 && gd.Height <= 64)
|
||||||
|
{
|
||||||
|
D3DLOCKED_RECT glr;
|
||||||
|
if (SUCCEEDED(grit->LockRect(0, &glr, NULL, D3DLOCK_READONLY)))
|
||||||
|
{
|
||||||
|
gw = gd.Width; gh = gd.Height;
|
||||||
|
for (int y = 0; y < gh; ++y)
|
||||||
|
{
|
||||||
|
const DWORD *src = (const DWORD *)((const char *)glr.pBits + y * glr.Pitch);
|
||||||
|
for (int x = 0; x < gw; ++x)
|
||||||
|
gritLum[y * gw + x] = (unsigned char)(src[x] & 0xFF);
|
||||||
|
}
|
||||||
|
grit->UnlockRect(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (SUCCEEDED(dev->CreateTexture(TW, TH, 1, 0, D3DFMT_A8R8G8B8,
|
||||||
|
D3DPOOL_MANAGED, &s_pfxTex, NULL)))
|
||||||
|
{
|
||||||
|
D3DLOCKED_RECT lr;
|
||||||
|
if (SUCCEEDED(s_pfxTex->LockRect(0, &lr, NULL, 0)))
|
||||||
|
{
|
||||||
|
for (int y = 0; y < TH; ++y)
|
||||||
|
{
|
||||||
|
DWORD *dst = (DWORD *)((char *)lr.pBits + y * lr.Pitch);
|
||||||
|
for (int x = 0; x < TW; ++x)
|
||||||
|
{
|
||||||
|
float dx = (x + 0.5f) / TW * 2.0f - 1.0f;
|
||||||
|
float dy = (y + 0.5f) / TH * 2.0f - 1.0f;
|
||||||
|
float r = sqrtf(dx * dx + dy * dy);
|
||||||
|
float f = 1.0f - r; // radial falloff
|
||||||
|
if (f < 0.0f) f = 0.0f;
|
||||||
|
f = f * f * (3.0f - 2.0f * f); // smoothstep edge
|
||||||
|
float n = 1.0f;
|
||||||
|
if (gw > 0) // authentic grit detail
|
||||||
|
n = 0.35f + 0.65f * (gritLum[(y % gh) * gw + (x % gw)] / 255.0f);
|
||||||
|
int v = (int)(f * n * 255.0f);
|
||||||
|
if (v > 255) v = 255;
|
||||||
|
// the falloff mask lives in ALPHA too: the premultiplied
|
||||||
|
// draw (ONE, INVSRCALPHA) needs soft-edged OCCLUSION for
|
||||||
|
// smoke, not just soft-edged colour for fire
|
||||||
|
dst[x] = (v << 24) | (v << 16) | (v << 8) | v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s_pfxTex->UnlockRect(0);
|
||||||
|
}
|
||||||
|
else { s_pfxTex->Release(); s_pfxTex = 0; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dev->SetTexture(0, s_pfxTex);
|
||||||
|
dev->SetTextureStageState(0, D3DTSS_COLOROP, s_pfxTex ? D3DTOP_MODULATE : D3DTOP_SELECTARG1);
|
||||||
|
dev->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE);
|
||||||
|
dev->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_TEXTURE);
|
||||||
|
// occlusion = vertex alpha x the texture's falloff mask (soft-edged smoke)
|
||||||
|
dev->SetTextureStageState(0, D3DTSS_ALPHAOP, s_pfxTex ? D3DTOP_MODULATE : D3DTOP_SELECTARG1);
|
||||||
|
dev->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_DIFFUSE);
|
||||||
|
dev->SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_TEXTURE);
|
||||||
|
|
||||||
|
D3DXMATRIX ident; D3DXMatrixIdentity(&ident);
|
||||||
|
dev->SetTransform(D3DTS_WORLD, &ident);
|
||||||
|
dev->SetFVF(D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX1);
|
||||||
|
|
||||||
|
struct V { float x, y, z; DWORD c; float u, v; };
|
||||||
|
static std::vector<V> verts;
|
||||||
|
verts.clear();
|
||||||
|
|
||||||
|
// BACK-TO-FRONT order: premultiplied occlusion (smoke) composites in depth
|
||||||
|
// order; unsorted draws pop when a near puff renders before a far one.
|
||||||
|
static std::vector<size_t> order;
|
||||||
|
static std::vector<float> depth;
|
||||||
|
order.resize(gBTPfxParticles.size());
|
||||||
|
depth.resize(gBTPfxParticles.size());
|
||||||
|
for (size_t i = 0; i < gBTPfxParticles.size(); ++i)
|
||||||
|
{
|
||||||
|
const D3DXVECTOR3 &pp = gBTPfxParticles[i].pos;
|
||||||
|
order[i] = i;
|
||||||
|
depth[i] = pp.x * view->_13 + pp.y * view->_23 + pp.z * view->_33; // view-space z
|
||||||
|
}
|
||||||
|
std::sort(order.begin(), order.end(),
|
||||||
|
[](size_t a, size_t b) { return depth[a] > depth[b]; });
|
||||||
|
|
||||||
|
for (size_t oi = 0; oi < order.size(); ++oi)
|
||||||
|
{
|
||||||
|
const BTPfxParticle &p = gBTPfxParticles[order[oi]];
|
||||||
|
float t = p.age / p.life;
|
||||||
|
if (t < 0.0f) t = 0.0f; if (t > 1.0f) t = 1.0f;
|
||||||
|
|
||||||
|
// colour/alpha interpolation start->end, warped (t^(1/warp): a large
|
||||||
|
// warp shifts to the end colour early -- the fast orange->smoke shift)
|
||||||
|
float tc = powf(t, 1.0f / p.colorWarp);
|
||||||
|
float ta = powf(t, 1.0f / p.alphaWarp);
|
||||||
|
|
||||||
|
// two quads: outer glow (2.2x radius) then inner core
|
||||||
|
for (int layer = 0; layer < 2; ++layer)
|
||||||
|
{
|
||||||
|
const float *cs = layer ? p.sI : p.sO;
|
||||||
|
const float *ce = layer ? p.eI : p.eO;
|
||||||
|
float scale = layer ? 1.0f : 2.2f;
|
||||||
|
float r = cs[0] + (ce[0] - cs[0]) * tc;
|
||||||
|
float g = cs[1] + (ce[1] - cs[1]) * tc;
|
||||||
|
float b = cs[2] + (ce[2] - cs[2]) * tc;
|
||||||
|
float a = cs[3] + (ce[3] - cs[3]) * ta;
|
||||||
|
if (a <= 0.0f) continue;
|
||||||
|
if (a > 1.0f) a = 1.0f;
|
||||||
|
// PREMULTIPLIED: vertex rgb = colour x alpha (the framebuffer
|
||||||
|
// contribution), vertex a = alpha (the occlusion). A colour ramped
|
||||||
|
// NEGATIVE (DDTHSMK's smoke tail) clamps to 0 -> pure occluding
|
||||||
|
// black smoke; a bright fire colour with fading alpha dims out.
|
||||||
|
int ir = (int)(r * a * 255.0f); if (ir > 255) ir = 255; if (ir < 0) ir = 0;
|
||||||
|
int ig = (int)(g * a * 255.0f); if (ig > 255) ig = 255; if (ig < 0) ig = 0;
|
||||||
|
int ib = (int)(b * a * 255.0f); if (ib > 255) ib = 255; if (ib < 0) ib = 0;
|
||||||
|
int ia = (int)(a * 255.0f); if (ia > 255) ia = 255; if (ia < 0) ia = 0;
|
||||||
|
if (ia == 0 && ir == 0 && ig == 0 && ib == 0) continue;
|
||||||
|
DWORD c = ((DWORD)ia << 24) | (ir << 16) | (ig << 8) | ib;
|
||||||
|
|
||||||
|
float rad = p.rad * scale;
|
||||||
|
D3DXVECTOR3 rv = right * rad, uv = up * rad;
|
||||||
|
V q[6];
|
||||||
|
q[0].x = p.pos.x - rv.x - uv.x; q[0].y = p.pos.y - rv.y - uv.y; q[0].z = p.pos.z - rv.z - uv.z; q[0].u = 0; q[0].v = 1;
|
||||||
|
q[1].x = p.pos.x - rv.x + uv.x; q[1].y = p.pos.y - rv.y + uv.y; q[1].z = p.pos.z - rv.z + uv.z; q[1].u = 0; q[1].v = 0;
|
||||||
|
q[2].x = p.pos.x + rv.x + uv.x; q[2].y = p.pos.y + rv.y + uv.y; q[2].z = p.pos.z + rv.z + uv.z; q[2].u = 1; q[2].v = 0;
|
||||||
|
q[3] = q[0];
|
||||||
|
q[4] = q[2];
|
||||||
|
q[5].x = p.pos.x + rv.x - uv.x; q[5].y = p.pos.y + rv.y - uv.y; q[5].z = p.pos.z + rv.z - uv.z; q[5].u = 1; q[5].v = 1;
|
||||||
|
for (int k = 0; k < 6; ++k) { q[k].c = c; verts.push_back(q[k]); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!verts.empty())
|
||||||
|
dev->DrawPrimitiveUP(D3DPT_TRIANGLELIST, (UINT)verts.size() / 3,
|
||||||
|
&verts[0], sizeof(V));
|
||||||
|
|
||||||
|
dev->SetTexture(0, NULL);
|
||||||
|
dev->SetRenderState(D3DRS_LIGHTING, sLight);
|
||||||
|
dev->SetRenderState(D3DRS_FOGENABLE, sFog);
|
||||||
|
dev->SetRenderState(D3DRS_ZWRITEENABLE, sZW);
|
||||||
|
dev->SetRenderState(D3DRS_ALPHABLENDENABLE, sBlend);
|
||||||
|
dev->SetRenderState(D3DRS_SRCBLEND, sSrc);
|
||||||
|
dev->SetRenderState(D3DRS_DESTBLEND, sDst);
|
||||||
|
dev->SetRenderState(D3DRS_CULLMODE, sCull);
|
||||||
|
dev->SetTextureStageState(0, D3DTSS_COLOROP, sCop);
|
||||||
|
dev->SetTextureStageState(0, D3DTSS_COLORARG1, sCa1);
|
||||||
|
dev->SetTextureStageState(0, D3DTSS_COLORARG2, sCa2);
|
||||||
|
dev->SetTextureStageState(0, D3DTSS_ALPHAOP, sAop);
|
||||||
|
dev->SetTextureStageState(0, D3DTSS_ALPHAARG1, sAa1);
|
||||||
|
dev->SetTextureStageState(0, D3DTSS_ALPHAARG2, sAa2);
|
||||||
|
}
|
||||||
|
|
||||||
// BT (task #20): the live window client aspect (0 = never resized -> the
|
// BT (task #20): the live window client aspect (0 = never resized -> the
|
||||||
// projection builders fall back to the configured x_size/y_size). Set by
|
// projection builders fall back to the configured x_size/y_size). Set by
|
||||||
// L4NotifyWindowResized (WM_SIZE) so a user-resized window doesn't stretch
|
// L4NotifyWindowResized (WM_SIZE) so a user-resized window doesn't stretch
|
||||||
@@ -3575,8 +4061,32 @@ void
|
|||||||
Unregister_Object(light_namelist);
|
Unregister_Object(light_namelist);
|
||||||
delete light_namelist;
|
delete light_namelist;
|
||||||
}
|
}
|
||||||
|
//
|
||||||
|
// Get the list of PSFX effects we should load into RAM. RECONSTRUCTED
|
||||||
|
// (was stubbed with ReadPSFX): the "psfxN=file.pfx" entries on the visited
|
||||||
|
// pages ([pfx_day]/[pfx_night], include-reached from the mission's arena
|
||||||
|
// page) bind each dpl effect NUMBER to its authentic .PFX definition,
|
||||||
|
// loaded into the BT particle layer (BTLoadPfxFile / BTStartPfx / BTDrawPfx
|
||||||
|
// at the top of this file) that DPLIndependantEffect's <100 arm consumes.
|
||||||
|
//
|
||||||
|
if ((psfx_namelist = master_notation_file->MakeEntryList(starting_page_name, "psfx")) != NULL)
|
||||||
|
{
|
||||||
|
extern int BTLoadPfxFile_slot(const char *file_name, int slot);
|
||||||
|
int pfx_loaded = 0;
|
||||||
|
for (entry = psfx_namelist->GetFirstEntry();
|
||||||
|
entry;
|
||||||
|
entry = entry->GetNextEntry())
|
||||||
|
{
|
||||||
|
int psfx_number = atoi(entry->GetName() + 4); // "psfxN"
|
||||||
|
const char *psfx_file_name = entry->GetChar();
|
||||||
|
pfx_loaded += BTLoadPfxFile_slot(psfx_file_name, psfx_number);
|
||||||
|
}
|
||||||
|
DEBUG_STREAM << "[pfx] page '" << starting_page_name << "': "
|
||||||
|
<< pfx_loaded << " effect definitions loaded\n" << std::flush;
|
||||||
|
delete psfx_namelist;
|
||||||
|
}
|
||||||
////
|
////
|
||||||
//// Get a the list of PSFX effects we should load into RAM
|
//// (original stubbed loader retained for provenance)
|
||||||
////
|
////
|
||||||
//if ((psfx_namelist = master_notation_file->MakeEntryList(starting_page_name, "psfx")) != NULL)
|
//if ((psfx_namelist = master_notation_file->MakeEntryList(starting_page_name, "psfx")) != NULL)
|
||||||
//{
|
//{
|
||||||
@@ -5894,9 +6404,29 @@ void
|
|||||||
<< entity->localOrigin.linearPosition.x
|
<< entity->localOrigin.linearPosition.x
|
||||||
<< ", " << entity->localOrigin.linearPosition.y
|
<< ", " << entity->localOrigin.linearPosition.y
|
||||||
<< ", " << entity->localOrigin.linearPosition.z << "], " << effect_number << ");" << std::endl << std::flush;
|
<< ", " << entity->localOrigin.linearPosition.z << "], " << effect_number << ");" << std::endl << std::flush;
|
||||||
DPLIndependantEffect(
|
// Both effect-number ENCODINGS resolve to the same psfx slot:
|
||||||
entity->localOrigin.linearPosition,
|
// <100 = the raw dpl board number; >=1000 = the WinTesla-era
|
||||||
effect_number);
|
// "1000+slot" INDIE id carried by the damage-band resources
|
||||||
|
// (1002-1005 = ddam1-4 light..critical damage smoke, 1008 =
|
||||||
|
// ddam5 zone-destroyed -- the SAME [pfx_day] mapping). BT
|
||||||
|
// ships no INDIE descriptors (the version-2 specialfx pages
|
||||||
|
// don't exist), so all slots route to the BT .PFX layer, with
|
||||||
|
// the Explosion entity's ORIENTATION (the victim's frame) --
|
||||||
|
// the .PFX offsets/velocities are authored mech-local.
|
||||||
|
{
|
||||||
|
int pfx_slot = (effect_number >= 1000)
|
||||||
|
? effect_number - 1000 : effect_number;
|
||||||
|
extern void BTStartPfxFrame(int, float, float, float,
|
||||||
|
const float *, const float *, const float *);
|
||||||
|
float xr[3] = { (float)entity->localToWorld(0,0), (float)entity->localToWorld(0,1), (float)entity->localToWorld(0,2) };
|
||||||
|
float yr[3] = { (float)entity->localToWorld(1,0), (float)entity->localToWorld(1,1), (float)entity->localToWorld(1,2) };
|
||||||
|
float zr[3] = { (float)entity->localToWorld(2,0), (float)entity->localToWorld(2,1), (float)entity->localToWorld(2,2) };
|
||||||
|
BTStartPfxFrame(pfx_slot,
|
||||||
|
(float)entity->localOrigin.linearPosition.x,
|
||||||
|
(float)entity->localOrigin.linearPosition.y,
|
||||||
|
(float)entity->localOrigin.linearPosition.z,
|
||||||
|
xr, yr, zr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -6928,6 +7458,13 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte
|
|||||||
// beam is occluded where it enters terrain). See BTDrawBeams above.
|
// beam is occluded where it enters terrain). See BTDrawBeams above.
|
||||||
BTDrawBeams(mDevice, &viewTransform, (float)dT);
|
BTDrawBeams(mDevice, &viewTransform, (float)dT);
|
||||||
|
|
||||||
|
// BT .PFX particle effects (explosions / damage bands / mech death) -- the
|
||||||
|
// same pass as the beams: world proj+view set, Z-test on, additive quads.
|
||||||
|
{
|
||||||
|
extern void BTDrawPfx(LPDIRECT3DDEVICE9 dev, const D3DXMATRIX *view, float dt);
|
||||||
|
BTDrawPfx(mDevice, &viewTransform, (float)dT);
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// And don't forget particles too
|
// And don't forget particles too
|
||||||
//
|
//
|
||||||
@@ -7565,6 +8102,20 @@ void
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Board effect numbers (<100) -- the 1995 dpl explosion/damage effects.
|
||||||
|
// RECONSTRUCTED: routed to the BT .PFX particle layer (the [pfx_day]/
|
||||||
|
// [pfx_night] psfxN mapping loads each number's authentic .PFX definition;
|
||||||
|
// see the layer banner at the top of this file). This is what makes every
|
||||||
|
// weapon-hit / damage-band / mech-death explosion actually VISIBLE -- the
|
||||||
|
// original dpl_Effect(dpl_effect_type_explosion, ...) below was IG-board
|
||||||
|
// hardware and was never ported.
|
||||||
|
//
|
||||||
|
{
|
||||||
|
extern void BTStartPfx(int effect_number, float x, float y, float z);
|
||||||
|
BTStartPfx(effect_number, location.x, location.y, location.z);
|
||||||
|
}
|
||||||
|
|
||||||
//dpl_EXPLOSION_EFFECT_INFO my_explosion;
|
//dpl_EXPLOSION_EFFECT_INFO my_explosion;
|
||||||
//my_explosion.type = effect_number;
|
//my_explosion.type = effect_number;
|
||||||
//my_explosion.x = location.x;
|
//my_explosion.x = location.x;
|
||||||
|
|||||||
@@ -70,6 +70,11 @@ public:
|
|||||||
virtual bool IsStatic() { return false; }
|
virtual bool IsStatic() { return false; }
|
||||||
|
|
||||||
d3d_OBJECT *GetDrawObj() { return this->graphicalObject; }
|
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
|
||||||
|
// frame -- the mechanism the BT damage-model "RemakeEntity" swap uses to show
|
||||||
|
// a destroyed segment without tearing down and rebuilding the whole tree.
|
||||||
|
void SetDrawObj(d3d_OBJECT *obj) { this->graphicalObject = obj; }
|
||||||
protected:
|
protected:
|
||||||
void ExecuteChildren();
|
void ExecuteChildren();
|
||||||
|
|
||||||
|
|||||||
@@ -294,6 +294,19 @@ HierarchicalDrawComponent*
|
|||||||
entity, VideoRenderable::Dynamic, NULL,
|
entity, VideoRenderable::Dynamic, NULL,
|
||||||
inDeathZone, intersect_mode, intersect_mask);
|
inDeathZone, intersect_mode, intersect_mask);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Start (or reset) this mech's RemakeEntity bookkeeping: record the skeleton
|
||||||
|
// variant now; the per-segment renderables + initial graphic states are filled
|
||||||
|
// in as the tree is built below (see RemakeEntityRenderables).
|
||||||
|
//
|
||||||
|
MechRenderTree &render_tree = mMechRenderTrees[entity];
|
||||||
|
render_tree = MechRenderTree();
|
||||||
|
render_tree.skeletonType = (int)skeletonType;
|
||||||
|
if (getenv("BT_DEATH_LOG"))
|
||||||
|
DEBUG_STREAM << "[BTrender] tracking mech tree for entity " << (void*)entity
|
||||||
|
<< " classID=" << entity->GetClassID() << " ("
|
||||||
|
<< mMechRenderTrees.size() << " tracked)\n" << std::flush;
|
||||||
|
|
||||||
//
|
//
|
||||||
// Per-segment renderable array (the parent for each segment's children).
|
// Per-segment renderable array (the parent for each segment's children).
|
||||||
//
|
//
|
||||||
@@ -368,7 +381,20 @@ HierarchicalDrawComponent*
|
|||||||
// Load this segment's geometry (skeleton-variant .bgf), if any.
|
// Load this segment's geometry (skeleton-variant .bgf), if any.
|
||||||
//
|
//
|
||||||
d3d_OBJECT *this_object = NULL;
|
d3d_OBJECT *this_object = NULL;
|
||||||
CString *object_name = segment->GetVideoObjectName(skeletonType); // FUN_00424084
|
// Select the segment's model VARIANT by its damage zone's graphic state.
|
||||||
|
// The engine keys video-object names by {skeleton, damage_graphic_state}
|
||||||
|
// (SEGMENT.h:172): a Destroyed zone (GetGraphicState()==1) returns the
|
||||||
|
// destroyed/damaged model, so a wrecked segment visibly comes apart. The
|
||||||
|
// recon previously passed ONLY the skeleton type, leaving the state at its
|
||||||
|
// default 0 (Exists) -> always the intact model = no visible damage.
|
||||||
|
Enumeration seg_gstate = 0; // ExistsGraphicState
|
||||||
|
{
|
||||||
|
int zone_index = segment->GetPrimaryDamageZone(); // SEGMENT.h:107 (a zone INDEX)
|
||||||
|
if (zone_index >= 0 && zone_index < entity->damageZoneCount
|
||||||
|
&& entity->damageZones[zone_index] != 0)
|
||||||
|
seg_gstate = entity->damageZones[zone_index]->GetGraphicState(); // DAMAGE.h:196
|
||||||
|
}
|
||||||
|
CString *object_name = segment->GetVideoObjectName(skeletonType, seg_gstate); // FUN_00424084
|
||||||
if (object_name != NULL)
|
if (object_name != NULL)
|
||||||
{
|
{
|
||||||
char filename[44];
|
char filename[44];
|
||||||
@@ -454,6 +480,11 @@ HierarchicalDrawComponent*
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
dcs_array[segment_slot] = child;
|
dcs_array[segment_slot] = child;
|
||||||
|
|
||||||
|
// Record this segment's renderable + the graphic state it was built with,
|
||||||
|
// so a later damage-state change can swap its mesh in place (RemakeEntity).
|
||||||
|
render_tree.segRenderable[segment_slot] = child;
|
||||||
|
render_tree.segGState[segment_slot] = (int)seg_gstate;
|
||||||
}
|
}
|
||||||
|
|
||||||
delete [] dcs_array;
|
delete [] dcs_array;
|
||||||
@@ -539,6 +570,140 @@ HierarchicalDrawComponent*
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
// RemakeEntityRenderables (the render "RemakeEntity" state -- damage swap)
|
||||||
|
//#############################################################################
|
||||||
|
//
|
||||||
|
// A damage zone's graphic state changed (a segment became Destroyed or Gone).
|
||||||
|
// Walk this mech's segments and, for any whose graphic state now differs from
|
||||||
|
// what its renderable was built with, re-pick the video-object variant by the
|
||||||
|
// new graphic state and swap it onto the joint renderable IN PLACE. Execute()
|
||||||
|
// re-reads graphicalObject each frame, so the wrecked mesh shows next frame.
|
||||||
|
// No teardown: the component dtor does not cascade to children (L4VIDRND.cpp:104),
|
||||||
|
// so a rebuild would leak -- the authentic behaviour is an in-place mesh swap.
|
||||||
|
//
|
||||||
|
void
|
||||||
|
BTL4VideoRenderer::RemakeEntityRenderables(Entity *entity)
|
||||||
|
{
|
||||||
|
std::map<Entity*, MechRenderTree>::iterator tree_it =
|
||||||
|
mMechRenderTrees.find(entity);
|
||||||
|
if (tree_it == mMechRenderTrees.end())
|
||||||
|
{
|
||||||
|
if (getenv("BT_DEATH_LOG"))
|
||||||
|
DEBUG_STREAM << "[BTrender] RemakeEntity: no render tree for entity "
|
||||||
|
<< (void*)entity << " (" << mMechRenderTrees.size()
|
||||||
|
<< " tracked)\n" << std::flush;
|
||||||
|
return; // tree not built yet -- Make will read the state
|
||||||
|
}
|
||||||
|
MechRenderTree &render_tree = tree_it->second;
|
||||||
|
|
||||||
|
JointedMover *jointed_mover = (JointedMover *)entity;
|
||||||
|
EntitySegment::SkeletonType skeletonType =
|
||||||
|
(EntitySegment::SkeletonType)render_tree.skeletonType;
|
||||||
|
|
||||||
|
EntitySegment::SegmentTableIterator segment_iterator(jointed_mover->segmentTable);
|
||||||
|
EntitySegment *segment;
|
||||||
|
int swapped = 0, checked = 0, mapped = 0;
|
||||||
|
|
||||||
|
while ((segment = segment_iterator.ReadAndNext()) != NULL)
|
||||||
|
{
|
||||||
|
if (segment->IsSiteSegment() != 0)
|
||||||
|
continue;
|
||||||
|
++checked;
|
||||||
|
|
||||||
|
int segment_slot = segment->GetIndex();
|
||||||
|
std::map<int, HierarchicalDrawComponent*>::iterator r =
|
||||||
|
render_tree.segRenderable.find(segment_slot);
|
||||||
|
if (r == render_tree.segRenderable.end() || r->second == NULL)
|
||||||
|
continue;
|
||||||
|
++mapped;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Current graphic state for this segment (from its damage zone).
|
||||||
|
//
|
||||||
|
Enumeration seg_gstate = 0; // ExistsGraphicState
|
||||||
|
int zone_index = segment->GetPrimaryDamageZone();
|
||||||
|
if (zone_index >= 0 && zone_index < entity->damageZoneCount
|
||||||
|
&& entity->damageZones[zone_index] != 0)
|
||||||
|
seg_gstate = entity->damageZones[zone_index]->GetGraphicState();
|
||||||
|
|
||||||
|
if ((int)seg_gstate == render_tree.segGState[segment_slot])
|
||||||
|
continue; // unchanged -- nothing to swap
|
||||||
|
render_tree.segGState[segment_slot] = (int)seg_gstate;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Re-pick + load the segment's video-object variant for the new graphic
|
||||||
|
// state (same construction as the initial build in MakeMechRenderables).
|
||||||
|
//
|
||||||
|
CString *object_name = segment->GetVideoObjectName(skeletonType, seg_gstate);
|
||||||
|
if (getenv("BT_DEATH_LOG"))
|
||||||
|
DEBUG_STREAM << "[BTrender] seg '" << (const char *)segment->GetName()
|
||||||
|
<< "' slot " << segment_slot << " -> gstate " << (int)seg_gstate
|
||||||
|
<< " variant=" << (object_name ? (const char *)*object_name : "(none)")
|
||||||
|
<< "\n" << std::flush;
|
||||||
|
d3d_OBJECT *new_object = NULL;
|
||||||
|
if (object_name != NULL)
|
||||||
|
{
|
||||||
|
char filename[44];
|
||||||
|
strcpy(filename, (const char *)*object_name);
|
||||||
|
int len = (int)strlen(filename);
|
||||||
|
if (len >= 4)
|
||||||
|
filename[len - 4] = '\0'; // strip ".bgf"
|
||||||
|
strcat(filename, ".bgf");
|
||||||
|
new_object = d3d_OBJECT::LoadObject(GetDevice(), filename);
|
||||||
|
if (new_object == NULL && getenv("BT_DEATH_LOG"))
|
||||||
|
DEBUG_STREAM << "[BTrender] damaged variant '" << filename
|
||||||
|
<< "' FAILED to load (expects VIDEO\\*.x)\n" << std::flush;
|
||||||
|
if (new_object != NULL && strstr(filename, "tshd") != NULL)
|
||||||
|
{
|
||||||
|
new_object->SetIsShadow(1);
|
||||||
|
for (int op = 0; op < new_object->GetDrawOpCount(); ++op)
|
||||||
|
new_object->GetDrawOp(op)->alphaTest = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// GoneGraphicState (blown off): no mesh -> hide the segment. Destroyed/
|
||||||
|
// Exists: swap to the variant if it loaded; otherwise keep the current
|
||||||
|
// mesh (don't blank a segment merely because a damaged .bgf is missing).
|
||||||
|
//
|
||||||
|
if (new_object != NULL)
|
||||||
|
r->second->SetDrawObj(new_object);
|
||||||
|
else if ((int)seg_gstate == DamageZone::GoneGraphicState)
|
||||||
|
r->second->SetDrawObj(NULL);
|
||||||
|
|
||||||
|
++swapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (swapped != 0 || getenv("BT_DEATH_LOG"))
|
||||||
|
DEBUG_STREAM << "[BTrender] RemakeEntity: " << swapped
|
||||||
|
<< " mesh(es) swapped (" << mapped << " body segs mapped of "
|
||||||
|
<< checked << " checked)\n" << std::flush;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
// BTRemakeMechModel (sim-side bridge -- see btl4vid.hpp)
|
||||||
|
//#############################################################################
|
||||||
|
//
|
||||||
|
// Reaches the live renderer and refreshes a mech's visible model after its
|
||||||
|
// damage graphic state changed. Called from MechDeathHandler (sim TU). The
|
||||||
|
// frame loop is single-threaded (sim + render share the main thread; only the
|
||||||
|
// network RX socket runs on its own thread), so loading geometry here is safe.
|
||||||
|
//
|
||||||
|
void BTRemakeMechModel(Entity *entity)
|
||||||
|
{
|
||||||
|
if (entity == NULL || application == NULL)
|
||||||
|
return;
|
||||||
|
BTL4VideoRenderer *renderer =
|
||||||
|
(BTL4VideoRenderer *)application->GetVideoRenderer();
|
||||||
|
if (renderer != NULL)
|
||||||
|
renderer->RemakeEntityRenderables(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//
|
//
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
// BTReticleRenderable::AddWeapon
|
// BTReticleRenderable::AddWeapon
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ class DPLRenderer;
|
|||||||
#if !defined(NAMELIST_HPP)
|
#if !defined(NAMELIST_HPP)
|
||||||
# include <namelist.hpp> // NameList / NameList::Entry
|
# include <namelist.hpp> // NameList / NameList::Entry
|
||||||
#endif
|
#endif
|
||||||
|
#include <map> // per-mech render-tree bookkeeping (RemakeEntity)
|
||||||
|
|
||||||
class Entity;
|
class Entity;
|
||||||
class Mission;
|
class Mission;
|
||||||
@@ -534,6 +535,40 @@ class BTReticleRenderable:
|
|||||||
void
|
void
|
||||||
TearDownMaterialSubstitutionList(); // @004d11e8
|
TearDownMaterialSubstitutionList(); // @004d11e8
|
||||||
|
|
||||||
|
//
|
||||||
|
// RemakeEntity (damage-model swap). The 1996 binary drives a per-entity
|
||||||
|
// render state machine (Make / RemakeEntity / DestroyEntity -- the state
|
||||||
|
// name strings survive at .rdata 0x4e3f20). Only "Make" was ported (the
|
||||||
|
// tree is built ONCE at entity creation, with fixed d3d_OBJECTs).
|
||||||
|
// RemakeEntity is the model REFRESH: when a damage zone's graphic state
|
||||||
|
// changes (a segment becomes Destroyed/Gone), re-pick each segment's video-
|
||||||
|
// object variant by its zone graphic state (EntitySegment::GetVideoObjectName
|
||||||
|
// is keyed by {skeleton, graphic_state}) and swap it onto the already-built
|
||||||
|
// joint renderable IN PLACE (HierarchicalDrawComponent::Execute re-reads
|
||||||
|
// graphicalObject each frame). No teardown: the component dtor does not
|
||||||
|
// cascade to children (L4VIDRND.cpp:104) so a rebuild would leak -- the
|
||||||
|
// authentic behaviour is an in-place mesh swap.
|
||||||
|
//
|
||||||
|
void
|
||||||
|
RemakeEntityRenderables(Entity *entity);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
//
|
||||||
|
// Per-mech render-tree bookkeeping so RemakeEntityRenderables can find each
|
||||||
|
// segment's joint renderable (the dcs_array in MakeMechRenderables is local
|
||||||
|
// and freed). Keyed by Entity*; one entry per built mech. segRenderable
|
||||||
|
// maps a segment SLOT (EntitySegment::GetIndex) to its draw component;
|
||||||
|
// segGState is the graphic state last applied to that slot so a swap only
|
||||||
|
// reloads geometry on an actual state change.
|
||||||
|
//
|
||||||
|
struct MechRenderTree
|
||||||
|
{
|
||||||
|
int skeletonType; // EntitySegment::SkeletonType used at build
|
||||||
|
std::map<int, HierarchicalDrawComponent*> segRenderable; // slot -> joint renderable
|
||||||
|
std::map<int, int> segGState; // slot -> last applied graphic state
|
||||||
|
};
|
||||||
|
std::map<Entity*, MechRenderTree> mMechRenderTrees;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
//
|
//
|
||||||
// Renderer-manager overrides
|
// Renderer-manager overrides
|
||||||
@@ -598,6 +633,14 @@ class BTReticleRenderable:
|
|||||||
*tracer_zone; // [0x2e4] projectile tracers
|
*tracer_zone; // [0x2e4] projectile tracers
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Sim-side bridge to RemakeEntityRenderables. MechDeathHandler (sim TU) calls
|
||||||
|
// this when a mech's damage graphic state changes; defined in btl4vid.cpp, it
|
||||||
|
// reaches the live renderer via l4_application->GetVideoRenderer(). A free
|
||||||
|
// function so the sim TU needs no renderer header (just this extern).
|
||||||
|
//
|
||||||
|
extern void BTRemakeMechModel(Entity *entity);
|
||||||
|
|
||||||
#endif // BTL4VID_HPP
|
#endif // BTL4VID_HPP
|
||||||
|
|
||||||
//===========================================================================//
|
//===========================================================================//
|
||||||
|
|||||||
@@ -486,7 +486,13 @@ void
|
|||||||
Mech::TakeDamageMessageHandler(TakeDamageMessage *message)
|
Mech::TakeDamageMessageHandler(TakeDamageMessage *message)
|
||||||
{
|
{
|
||||||
Check(message);
|
Check(message);
|
||||||
DamageLookupTable *table = (DamageLookupTable *)Wword(0x111);
|
// Maintain the last-attacker bookkeeping (mech[0x43c]): read by the
|
||||||
|
// DamageZone LOD router (same-attacker redirect reuse, mechdmg.cpp:374)
|
||||||
|
// and by the damage-band effect orientation. Was declared but never
|
||||||
|
// WRITTEN -- a reconstruction gap; the natural authentic write site is
|
||||||
|
// this handler (every damage message carries the inflictor). [T3]
|
||||||
|
lastInflictingID = message->inflictingEntity;
|
||||||
|
DamageLookupTable *table = (DamageLookupTable *)damageLookupTable; // named member (Wword absorbs!)
|
||||||
if (message->invalidDamageZone && table != 0)
|
if (message->invalidDamageZone && table != 0)
|
||||||
{
|
{
|
||||||
int zone = table->ResolveHit(message->damageData.impactPoint);
|
int zone = table->ResolveHit(message->damageData.impactPoint);
|
||||||
@@ -1323,7 +1329,7 @@ Mech::Mech(
|
|||||||
// cached at mech[0x111] (byte 0x444). (Was an empty-name StandingAnimation
|
// cached at mech[0x111] (byte 0x444). (Was an empty-name StandingAnimation
|
||||||
// stub -> 0 rows; the real class is dmgtable.cpp.)
|
// stub -> 0 rows; the real class is dmgtable.cpp.)
|
||||||
//
|
//
|
||||||
Wword(0x111) = 0;
|
damageLookupTable = 0; // named member (Wword absorbs!)
|
||||||
ResourceDescription *dzForName =
|
ResourceDescription *dzForName =
|
||||||
MechFindResource(creation_message->resourceID,
|
MechFindResource(creation_message->resourceID,
|
||||||
ResourceDescription::DamageZoneStreamResourceType); // type 0x14 (for its name)
|
ResourceDescription::DamageZoneStreamResourceType); // type 0x14 (for its name)
|
||||||
@@ -1339,7 +1345,7 @@ Mech::Mech(
|
|||||||
DynamicMemoryStream cylStream( // FUN_004032dc, offset 0
|
DynamicMemoryStream cylStream( // FUN_004032dc, offset 0
|
||||||
cylRes->resourceAddress, cylRes->resourceSize, 0);
|
cylRes->resourceAddress, cylRes->resourceSize, 0);
|
||||||
DamageLookupTable *table = new DamageLookupTable(this, &cylStream); // FUN_0049ea48
|
DamageLookupTable *table = new DamageLookupTable(this, &cylStream); // FUN_0049ea48
|
||||||
Wword(0x111) = (int)table;
|
damageLookupTable = (int)table; // named member (Wword absorbs!)
|
||||||
DEBUG_STREAM << "[cyl] table '" << dzForName->resourceName
|
DEBUG_STREAM << "[cyl] table '" << dzForName->resourceName
|
||||||
<< "' layers=" << table->LayerCount() << "\n" << std::flush;
|
<< "' layers=" << table->LayerCount() << "\n" << std::flush;
|
||||||
}
|
}
|
||||||
@@ -1407,9 +1413,9 @@ Mech::~Mech()
|
|||||||
<< "\n" << std::flush;
|
<< "\n" << std::flush;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Wword(0x111) != 0) // cylinder hit-location table (STEP 6)
|
if (damageLookupTable != 0) // cylinder hit-location table (STEP 6)
|
||||||
{
|
{
|
||||||
delete (DamageLookupTable *)Wword(0x111); // frees layers/slices/entries
|
delete (DamageLookupTable *)damageLookupTable; // frees layers/slices/entries
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -488,7 +488,14 @@ protected:
|
|||||||
int heatLevel; // @0x518 this[0x146]
|
int heatLevel; // @0x518 this[0x146]
|
||||||
int heatCapacity; // @0x51c this[0x147] = 0.6 * heatLevel
|
int heatCapacity; // @0x51c this[0x147] = 0.6 * heatLevel
|
||||||
Time creationTime; // @0x778 this[0x1de]
|
Time creationTime; // @0x778 this[0x1de]
|
||||||
int ammoExpended; // @0x444 this[0x111] (FUN_0049ea48)
|
// Cylinder hit-location table (STEP 6) -- DamageLookupTable* cached by the
|
||||||
|
// ctor from the type-0x1d resource (FUN_0049ea48 = the table ctor), read by
|
||||||
|
// Mech::TakeDamageMessageHandler to resolve an unaimed (-1) hit's zone from
|
||||||
|
// its impact point. MUST be a real named member: the old home Wword(0x111)
|
||||||
|
// is the recon ABSORBER bank (BTVal stores nothing, reads 0) -> the cached
|
||||||
|
// table silently vanished and every unaimed hit no-op'd. (Was mislabeled
|
||||||
|
// "ammoExpended"; binary slot this[0x111] is this pointer.)
|
||||||
|
int damageLookupTable; // @0x444 this[0x111] (FUN_0049ea48)
|
||||||
int deathHandler; // @0x850 this[0x214] (FUN_0042a984)
|
int deathHandler; // @0x850 this[0x214] (FUN_0042a984)
|
||||||
|
|
||||||
// Three ref-counted creation-name objects (badge/color/insignia).
|
// Three ref-counted creation-name objects (badge/color/insignia).
|
||||||
|
|||||||
+152
-27
@@ -140,6 +140,7 @@
|
|||||||
#include <BOXTREE.hpp> // BoundingBoxTreeNode::FindBoundingBoxUnder / ...ContainingColumn
|
#include <BOXTREE.hpp> // BoundingBoxTreeNode::FindBoundingBoxUnder / ...ContainingColumn
|
||||||
#include <BOXSOLID.hpp> // BoxedSolid / BoxedSolidCollision / BoxedSolidCollisionList
|
#include <BOXSOLID.hpp> // BoxedSolid / BoxedSolidCollision / BoxedSolidCollisionList
|
||||||
#include <cultural.hpp> // CulturalIcon::IsStoppingCollisionVolume / GetClassDerivations
|
#include <cultural.hpp> // CulturalIcon::IsStoppingCollisionVolume / GetClassDerivations
|
||||||
|
#include <hostmgr.hpp> // HostManager::GetEntityPointer (band-effect attacker resolve)
|
||||||
|
|
||||||
static const Scalar kBehindCull = -1.0e-4f; // _DAT_004ac044
|
static const Scalar kBehindCull = -1.0e-4f; // _DAT_004ac044
|
||||||
|
|
||||||
@@ -822,7 +823,10 @@ static void
|
|||||||
Mech *m = (Mech *)tgt;
|
Mech *m = (Mech *)tgt;
|
||||||
if (m->damageZoneCount > 0)
|
if (m->damageZoneCount > 0)
|
||||||
{
|
{
|
||||||
int zone = m->FirstVitalZone(); // concentrated fire -> a kill
|
// UNAIMED (STEP 6): the projectile's world impact position IS the
|
||||||
|
// hit point; Mech::TakeDamageMessageHandler resolves the struck
|
||||||
|
// zone from the cylinder hit-location table. (Previously this
|
||||||
|
// aimed the internal vital zone directly -> invisible insta-kill.)
|
||||||
Damage dmg;
|
Damage dmg;
|
||||||
dmg.damageType = Damage::ExplosiveDamageType;
|
dmg.damageType = Damage::ExplosiveDamageType;
|
||||||
dmg.damageAmount = p.damage;
|
dmg.damageAmount = p.damage;
|
||||||
@@ -830,12 +834,13 @@ static void
|
|||||||
dmg.impactPoint = p.pos;
|
dmg.impactPoint = p.pos;
|
||||||
Entity::TakeDamageMessage take_damage(
|
Entity::TakeDamageMessage take_damage(
|
||||||
Entity::TakeDamageMessageID, sizeof(Entity::TakeDamageMessage),
|
Entity::TakeDamageMessageID, sizeof(Entity::TakeDamageMessage),
|
||||||
0 /*inflictor id: bring-up*/, zone, dmg);
|
0 /*inflictor id: bring-up*/, -1 /*unaimed -> cylinder resolves*/, dmg);
|
||||||
tgt->Dispatch(&take_damage);
|
tgt->Dispatch(&take_damage);
|
||||||
// gauge scoring wave (Step 6): a projectile hit credits SCORE too
|
// gauge scoring wave (Step 6): a projectile hit credits SCORE too
|
||||||
// (tgt == gEnemyMech here; local player is the viewpoint shooter).
|
// (tgt == gEnemyMech here; local player is the viewpoint shooter).
|
||||||
BTPostDamageScore((Entity *)tgt, p.damage);
|
BTPostDamageScore((Entity *)tgt, p.damage);
|
||||||
DEBUG_STREAM << "[projectile] IMPACT damage=" << p.damage << " zone=" << zone << "\n" << std::flush;
|
DEBUG_STREAM << "[projectile] IMPACT damage=" << p.damage
|
||||||
|
<< " zone=" << take_damage.damageZone << " (cyl-resolved)\n" << std::flush;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
p.active = 0;
|
p.active = 0;
|
||||||
@@ -853,7 +858,7 @@ static void
|
|||||||
// derives the position from the subsystem (mech+0x184); we use the mech origin.
|
// derives the position from the subsystem (mech+0x184); we use the mech origin.
|
||||||
//###########################################################################
|
//###########################################################################
|
||||||
void
|
void
|
||||||
BTSpawnDamageEffect(Mech *mech, int effect_resource)
|
BTSpawnDamageEffect(Mech *mech, int effect_resource, int segment_index)
|
||||||
{
|
{
|
||||||
if (mech == 0)
|
if (mech == 0)
|
||||||
return;
|
return;
|
||||||
@@ -862,7 +867,55 @@ void
|
|||||||
res = gExplodeRes; // fall back to the resolved generic explosion
|
res = gExplodeRes; // fall back to the resolved generic explosion
|
||||||
if (res <= 0)
|
if (res <= 0)
|
||||||
return; // nothing to spawn yet
|
return; // nothing to spawn yet
|
||||||
Origin o = mech->localOrigin; // at the mech (binary: per-subsystem)
|
|
||||||
|
//
|
||||||
|
// Effect position: the damaged zone's SEGMENT, in world space (the binary
|
||||||
|
// derives the effect position from the damaged subsystem/zone, not the mech
|
||||||
|
// origin -- an origin-anchored effect burns at ground level between the
|
||||||
|
// feet). Resolve segment_index through the segment table exactly as the
|
||||||
|
// gun-port muzzles do (GetSegmentToEntity x localToWorld); fall back to
|
||||||
|
// torso height over the origin when the zone has no segment.
|
||||||
|
//
|
||||||
|
Origin o = mech->localOrigin;
|
||||||
|
Point3D fxPos = o.linearPosition;
|
||||||
|
fxPos.y += kMuzzleHeight; // default: torso height
|
||||||
|
if (segment_index >= 0)
|
||||||
|
{
|
||||||
|
EntitySegment::SegmentTableIterator it(mech->segmentTable);
|
||||||
|
EntitySegment *seg;
|
||||||
|
while ((seg = it.ReadAndNext()) != NULL)
|
||||||
|
{
|
||||||
|
if (seg->GetIndex() == segment_index)
|
||||||
|
{
|
||||||
|
AffineMatrix mw;
|
||||||
|
mw.Multiply(seg->GetSegmentToEntity(), mech->localToWorld);
|
||||||
|
fxPos = mw; // Point3D = matrix translation
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
o.linearPosition = fxPos;
|
||||||
|
|
||||||
|
// IMPACT FRAME for the band effect: orient local -Z from the victim toward
|
||||||
|
// the LAST ATTACKER (lastInflictingID, maintained by TakeDamageMessageHandler)
|
||||||
|
// -- the .PFX offsets are authored "out of the struck armor". Falls back to
|
||||||
|
// the victim's own frame when the attacker can't be resolved.
|
||||||
|
if (application != 0 && application->GetHostManager() != 0)
|
||||||
|
{
|
||||||
|
Entity *attacker =
|
||||||
|
application->GetHostManager()->GetEntityPointer(mech->lastInflictingID);
|
||||||
|
if (attacker != 0 && attacker != (Entity *)mech)
|
||||||
|
{
|
||||||
|
float adx = (float)(mech->localOrigin.linearPosition.x
|
||||||
|
- attacker->localOrigin.linearPosition.x);
|
||||||
|
float adz = (float)(mech->localOrigin.linearPosition.z
|
||||||
|
- attacker->localOrigin.linearPosition.z);
|
||||||
|
if (adx * adx + adz * adz > 1e-6f)
|
||||||
|
o.angularPosition = // -Z at the attacker
|
||||||
|
EulerAngles(0.0f, (Scalar)atan2((double)adx, (double)adz), 0.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Explosion::MakeMessage m(
|
Explosion::MakeMessage m(
|
||||||
Explosion::MakeMessageID, sizeof(Explosion::MakeMessage),
|
Explosion::MakeMessageID, sizeof(Explosion::MakeMessage),
|
||||||
(Entity::ClassID)RegisteredClass::ExplosionClassID, EntityID::Null,
|
(Entity::ClassID)RegisteredClass::ExplosionClassID, EntityID::Null,
|
||||||
@@ -1088,6 +1141,27 @@ void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DEV: BT_AUTOFIRE=1 holds the trigger (drives the fireForced hook) and
|
||||||
|
// BT_AUTODRIVE=<0..1> holds the throttle (drives the forced hook) so the
|
||||||
|
// full walk->fire->damage->death chain can be exercised headlessly.
|
||||||
|
{
|
||||||
|
static int sAutoFire = -1;
|
||||||
|
static float sAutoDrive = -1.0f;
|
||||||
|
if (sAutoFire < 0)
|
||||||
|
{
|
||||||
|
const char *af = getenv("BT_AUTOFIRE");
|
||||||
|
sAutoFire = (af && *af == '1') ? 1 : 0;
|
||||||
|
const char *ad = getenv("BT_AUTODRIVE");
|
||||||
|
sAutoDrive = ad ? (float)atof(ad) : 0.0f;
|
||||||
|
}
|
||||||
|
gBTDrive.fireForced = sAutoFire;
|
||||||
|
if (sAutoDrive > 0.0f)
|
||||||
|
{
|
||||||
|
gBTDrive.forced = 1;
|
||||||
|
gBTDrive.forcedThrottle = sAutoDrive;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (gBTDrive.allStop) { sLever = 0.0f; sDetent = 0; gBTDrive.allStop = 0; }
|
if (gBTDrive.allStop) { sLever = 0.0f; sDetent = 0; gBTDrive.allStop = 0; }
|
||||||
|
|
||||||
if (getenv("BT_KEY_LOG"))
|
if (getenv("BT_KEY_LOG"))
|
||||||
@@ -2216,7 +2290,32 @@ void
|
|||||||
gFireCooldown = kFireCooldown;
|
gFireCooldown = kFireCooldown;
|
||||||
++gShotCount;
|
++gShotCount;
|
||||||
|
|
||||||
Origin exp_origin = ((Mech *)gEnemyMech)->localOrigin; // at the target
|
// Beam entry point: on the enemy's surface toward the shooter, at
|
||||||
|
// the beam's convergence height. (Exactly the axis point would be
|
||||||
|
// angularly degenerate for the cylinder sector lookup.) Used for
|
||||||
|
// BOTH the hit-explosion position and the unaimed damage dispatch --
|
||||||
|
// the effect fires where the beam lands (chest height, facing side),
|
||||||
|
// not at the mech origin (ground level between the feet).
|
||||||
|
Point3D impact = enemyPos;
|
||||||
|
if (range > 1e-3f)
|
||||||
|
{
|
||||||
|
impact.x -= (ddx / range) * 3.0f; // ~torso radius toward shooter
|
||||||
|
impact.z -= (ddz / range) * 3.0f;
|
||||||
|
}
|
||||||
|
impact.y += kMuzzleHeight; // chest height (beam aim height)
|
||||||
|
|
||||||
|
Origin exp_origin = ((Mech *)gEnemyMech)->localOrigin;
|
||||||
|
exp_origin.linearPosition = impact; // at the hit point
|
||||||
|
// IMPACT FRAME: the .PFX hit effects are authored with local -Z =
|
||||||
|
// "out of the struck armor" (DAFC offsets/velocities spray -Z).
|
||||||
|
// That is the IMPACT normal -- toward the SHOOTER -- not the
|
||||||
|
// victim's body front: with the victim's own quat a rear/side hit
|
||||||
|
// flashed on the FAR (front) side of the mech. Build the frame as
|
||||||
|
// a yaw with -Z aimed from the victim at the shooter. Engine yaw
|
||||||
|
// convention (MATRIX.cpp:196-209): forward -Z = (-sin y, 0, -cos y)
|
||||||
|
// -> yaw = atan2(ddx, ddz) points -Z at the shooter. [T0]
|
||||||
|
exp_origin.angularPosition =
|
||||||
|
EulerAngles(0.0f, (Scalar)atan2((double)ddx, (double)ddz), 0.0f);
|
||||||
|
|
||||||
Explosion::MakeMessage exp_message(
|
Explosion::MakeMessage exp_message(
|
||||||
Explosion::MakeMessageID,
|
Explosion::MakeMessageID,
|
||||||
@@ -2241,21 +2340,19 @@ void
|
|||||||
DEBUG_STREAM << "[fire] Explosion::Make returned NULL\n" << std::flush;
|
DEBUG_STREAM << "[fire] Explosion::Make returned NULL\n" << std::flush;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- DAMAGE (real): dispatch a TakeDamage message to a VALID zone; the
|
// --- DAMAGE (real, STEP 6): dispatch UNAIMED (zone == -1) with the
|
||||||
// engine base handler routes it to Mech__DamageZone::TakeDamage (the real
|
// beam's world entry point; Mech::TakeDamageMessageHandler resolves
|
||||||
// armor/structure model). Aim a rotating zone so the whole mech degrades;
|
// the zone from the cylinder hit-location table (the STEP-6
|
||||||
// read back structureLevel (now valid -- friend access) to show it climb
|
// reconstruction) and the base handler routes it to
|
||||||
// toward 1.0 (destroyed).
|
// Mech__DamageZone::TakeDamage (the real armor/structure model).
|
||||||
|
// Hits therefore land on the EXTERIOR zone facing the shooter
|
||||||
|
// (arm/leg/torso -- zones with visible segments that wreck), and
|
||||||
|
// internal vitals die only through the authentic destruction
|
||||||
|
// cascade (RecurseSegmentTable / SendSubsystemDamage) -- no more
|
||||||
|
// invisible one-shot kills on the soft internal vital zone.
|
||||||
if (gEnemyMech->damageZoneCount > 0)
|
if (gEnemyMech->damageZoneCount > 0)
|
||||||
{
|
{
|
||||||
int zc = gEnemyMech->damageZoneCount;
|
// (impact computed above -- shared with the hit-explosion origin)
|
||||||
// Aim the first VITAL zone so concentrated fire destroys it (-> mech
|
|
||||||
// death). The faithful per-impact aim (cylinder lookup from the hit
|
|
||||||
// point) is STEP 6; until then we target a vital zone directly.
|
|
||||||
int zone = 0;
|
|
||||||
for (int k = 0; k < zc; ++k)
|
|
||||||
if (((Mech *)gEnemyMech)->Zone(k)->vitalDamageZone) { zone = k; break; }
|
|
||||||
|
|
||||||
Damage dmg; // default-constructed
|
Damage dmg; // default-constructed
|
||||||
// Explosive: the weapon effect is an Explosion (explosive), not an
|
// Explosive: the weapon effect is an Explosion (explosive), not an
|
||||||
// energy beam. Also the correct type to exercise the zone armour/
|
// energy beam. Also the correct type to exercise the zone armour/
|
||||||
@@ -2265,23 +2362,32 @@ void
|
|||||||
dmg.damageType = Damage::ExplosiveDamageType;
|
dmg.damageType = Damage::ExplosiveDamageType;
|
||||||
dmg.damageAmount = kShotDamage;
|
dmg.damageAmount = kShotDamage;
|
||||||
dmg.burstCount = 1;
|
dmg.burstCount = 1;
|
||||||
dmg.impactPoint = enemyPos; // world impact point
|
dmg.impactPoint = impact; // world impact point
|
||||||
|
|
||||||
Entity::TakeDamageMessage take_damage(
|
Entity::TakeDamageMessage take_damage(
|
||||||
Entity::TakeDamageMessageID,
|
Entity::TakeDamageMessageID,
|
||||||
sizeof(Entity::TakeDamageMessage),
|
sizeof(Entity::TakeDamageMessage),
|
||||||
GetEntityID(), // inflicting = this (shooter)
|
GetEntityID(), // inflicting = this (shooter)
|
||||||
zone, // valid zone -> base handler applies
|
-1, // UNAIMED -> receiver's cylinder resolves
|
||||||
dmg);
|
dmg);
|
||||||
gEnemyMech->Dispatch(&take_damage);
|
gEnemyMech->Dispatch(&take_damage);
|
||||||
|
|
||||||
// gauge scoring wave (Step 6): credit the local player for damage
|
// gauge scoring wave (Step 6): credit the local player for damage
|
||||||
// dealt -> SCORE climbs per hit (currentScore += tonnageRatio*award).
|
// dealt -> SCORE climbs per hit (currentScore += tonnageRatio*award).
|
||||||
BTPostDamageScore(gEnemyMech, kShotDamage);
|
// No score for pounding the wreck (damage still applies -- zones
|
||||||
|
// clamp at 1.0, further segments wreck visibly).
|
||||||
|
if (!gEnemyDestroyed)
|
||||||
|
BTPostDamageScore(gEnemyMech, kShotDamage);
|
||||||
|
|
||||||
Scalar s = ((Mech *)gEnemyMech)->Zone(zone)->damageLevel; // [0,1], 1.0=destroyed (engine base field)
|
// The handler wrote the resolved zone back into the message.
|
||||||
DEBUG_STREAM << "[damage] hit zone " << zone << "/" << zc
|
int zone = take_damage.damageZone;
|
||||||
<< " structure=" << s << "\n" << std::flush;
|
if (zone >= 0 && zone < gEnemyMech->damageZoneCount)
|
||||||
|
{
|
||||||
|
Scalar s = ((Mech *)gEnemyMech)->Zone(zone)->damageLevel; // [0,1], 1.0=destroyed
|
||||||
|
DEBUG_STREAM << "[damage] hit zone " << zone << "/"
|
||||||
|
<< gEnemyMech->damageZoneCount
|
||||||
|
<< " structure=" << s << "\n" << std::flush;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Death via the REAL damage model: a mech with a destroyed VITAL zone is
|
// Death via the REAL damage model: a mech with a destroyed VITAL zone is
|
||||||
@@ -2304,8 +2410,22 @@ void
|
|||||||
// fires; the ownerless dummy yields no death, DEATHS stays 0).
|
// fires; the ownerless dummy yields no death, DEATHS stays 0).
|
||||||
BTPostKillScore(gEnemyMech, kShotDamage);
|
BTPostKillScore(gEnemyMech, kShotDamage);
|
||||||
|
|
||||||
// Death explosion at the target.
|
// Death effects, per the authentic BTDPL.INI effect-number map:
|
||||||
|
// 7 = "the big explosion used as part of mech death" (dnboom)
|
||||||
|
// 1 = "the mech death/rubble smoke plume" (ddthsmk)
|
||||||
|
// Fired directly into the render effect layer at the wreck (the
|
||||||
|
// unexported death sequence's effect chain dispatched these
|
||||||
|
// numbers through the 0xBD3 manager; the numbers are the data).
|
||||||
|
{
|
||||||
|
extern void BTStartPfx(int effect_number, float x, float y, float z);
|
||||||
|
Point3D wreck = ((Mech *)gEnemyMech)->localOrigin.linearPosition;
|
||||||
|
BTStartPfx(7, wreck.x, wreck.y + kMuzzleHeight, wreck.z); // the death boom
|
||||||
|
BTStartPfx(1, wreck.x, wreck.y + kMuzzleHeight, wreck.z); // the smoke plume
|
||||||
|
}
|
||||||
|
|
||||||
|
// Death explosion at the target (torso height, not ground level).
|
||||||
Origin death_origin = ((Mech *)gEnemyMech)->localOrigin;
|
Origin death_origin = ((Mech *)gEnemyMech)->localOrigin;
|
||||||
|
death_origin.linearPosition.y += kMuzzleHeight;
|
||||||
Explosion::MakeMessage death_exp(
|
Explosion::MakeMessage death_exp(
|
||||||
Explosion::MakeMessageID,
|
Explosion::MakeMessageID,
|
||||||
sizeof(Explosion::MakeMessage),
|
sizeof(Explosion::MakeMessage),
|
||||||
@@ -2358,7 +2478,12 @@ void
|
|||||||
DEBUG_STREAM << "[damage] *** TARGET DESTROYED after "
|
DEBUG_STREAM << "[damage] *** TARGET DESTROYED after "
|
||||||
<< gShotCount << " hits ***\n" << std::flush;
|
<< gShotCount << " hits ***\n" << std::flush;
|
||||||
|
|
||||||
gEnemyMech = 0; // stop targeting/firing the dead entity
|
// KEEP the wreck targeted (do NOT null gEnemyMech): the wreck
|
||||||
|
// STAYS in the world (removal = the P5 teardown crash), so the
|
||||||
|
// beam convergence must keep terminating ON it -- nulling the
|
||||||
|
// lock here made every later beam a "free" ray that visibly
|
||||||
|
// passed through the standing wreck. Damage + kill scoring are
|
||||||
|
// latched off above via gEnemyDestroyed.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -575,6 +575,24 @@ void
|
|||||||
{
|
{
|
||||||
Subsystem *s = criticalSubsystems[i]->subsystemPlug.Resolve(); // +4
|
Subsystem *s = criticalSubsystems[i]->subsystemPlug.Resolve(); // +4
|
||||||
MechCriticalSubsystem *cs = criticalSubsystems[i];
|
MechCriticalSubsystem *cs = criticalSubsystems[i];
|
||||||
|
|
||||||
|
// UNBOUND-PLUG GUARD. Resolve() (FUN_00417ab4) returns the subsystem bound
|
||||||
|
// to the DZSlot, or 0 if plug+8 was never wired. In the 1995 binary every
|
||||||
|
// critical subsystem exists and is bound, so it dereferences s with no check.
|
||||||
|
// In this reconstruction a critical subsystem can be UNBOUND (its type is not
|
||||||
|
// yet built, or its slot never connected), leaving s == 0 -> the original
|
||||||
|
// code AV'd here (mov [edx+0xc], edx=0) as soon as a zone with such a plug was
|
||||||
|
// destroyed. Skip the unbound entry rather than crash; log it so the missing
|
||||||
|
// binding can be tracked. NOT a behavioural stand-in: a bound plug takes the
|
||||||
|
// authentic path below unchanged. [T3 -- see open-questions: crit-subsys binding]
|
||||||
|
if (s == 0)
|
||||||
|
{
|
||||||
|
if (getenv("BT_DEATH_LOG"))
|
||||||
|
DEBUG_STREAM << "[deathfx] crit-subsys " << i << "/" << criticalSubsystemCount
|
||||||
|
<< " UNBOUND (plug not wired) -- skipped\n" << std::flush;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
Mech__DamageZone *zone = (Mech__DamageZone *)((SubProxy2 *)s)->damageZone; // subsystem[0x38] @0xE0
|
Mech__DamageZone *zone = (Mech__DamageZone *)((SubProxy2 *)s)->damageZone; // subsystem[0x38] @0xE0
|
||||||
|
|
||||||
zone->damageLevel += (cs->damagePercentage - cs->damagePercentageUsed);
|
zone->damageLevel += (cs->damagePercentage - cs->damagePercentageUsed);
|
||||||
@@ -911,7 +929,12 @@ void
|
|||||||
// live): spawns the descriptor's explosion resource at the mech. The authentic
|
// live): spawns the descriptor's explosion resource at the mech. The authentic
|
||||||
// path dispatches a class-5 message to the effect manager (app+0x38 -> the 0xBD3
|
// path dispatches a class-5 message to the effect manager (app+0x38 -> the 0xBD3
|
||||||
// SubsystemMessageManager, unreconstructed); we use the established Explosion port.
|
// SubsystemMessageManager, unreconstructed); we use the established Explosion port.
|
||||||
extern void BTSpawnDamageEffect(Mech *mech, int effect_resource);
|
extern void BTSpawnDamageEffect(Mech *mech, int effect_resource, int segment_index);
|
||||||
|
|
||||||
|
// Render bridge (btl4vid.cpp): swap a mech's wrecked segment meshes onto its
|
||||||
|
// already-built render tree when a damage zone's graphic state changes (the
|
||||||
|
// unported "RemakeEntity" render state). Entity* param -> Mech* binds fine.
|
||||||
|
extern void BTRemakeMechModel(Entity *entity);
|
||||||
|
|
||||||
MechDeathHandler::MechDeathHandler(Mech *mech) // @0042a984
|
MechDeathHandler::MechDeathHandler(Mech *mech) // @0042a984
|
||||||
: owner(mech)
|
: owner(mech)
|
||||||
@@ -960,8 +983,27 @@ void
|
|||||||
}
|
}
|
||||||
if (d != 0)
|
if (d != 0)
|
||||||
{
|
{
|
||||||
BTSpawnDamageEffect(owner, d->effectResource); // explosion at the zone
|
BTSpawnDamageEffect(owner, d->effectResource, // explosion AT the zone's
|
||||||
zone->ApplyDamageGraphicState(d->graphicState); // destroyed skin
|
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
|
||||||
|
// the engine's model-rebuild flag so the renderer re-runs
|
||||||
|
// MakeMechRenderables and loads the destroyed segment geometry -- the
|
||||||
|
// same mechanism CulturalIcons use to visibly deform on damage
|
||||||
|
// (CULTURAL.cpp:91; EXPTBL.cpp:549 does this per zone in the engine's
|
||||||
|
// ExplosionTable). Without this the tree stays as built at spawn (intact).
|
||||||
|
// The render's "RemakeEntity" state (which consumes this flag) was
|
||||||
|
// NOT ported -- the mech tree is built once at spawn -- so we drive the
|
||||||
|
// equivalent directly: set the flag (authentic signal; also flags the
|
||||||
|
// zone for net damage replication) AND call the render bridge, which
|
||||||
|
// swaps the wrecked segment mesh onto the already-built tree in place.
|
||||||
|
// The single-threaded frame loop makes the immediate swap safe.
|
||||||
|
if (d->graphicState != DamageZone::ExistsGraphicState)
|
||||||
|
{
|
||||||
|
owner->ForceUpdate(Entity::DamageZoneUpdateModelFlag);
|
||||||
|
BTRemakeMechModel(owner); // RemakeEntity: swap in the destroyed mesh
|
||||||
|
}
|
||||||
if (getenv("BT_DEATH_LOG"))
|
if (getenv("BT_DEATH_LOG"))
|
||||||
DEBUG_STREAM << "[deathfx] zone " << i << " level " << level
|
DEBUG_STREAM << "[deathfx] zone " << i << " level " << level
|
||||||
<< " -> effect " << d->effectResource << " gstate " << d->graphicState
|
<< " -> effect " << d->effectResource << " gstate " << d->graphicState
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import struct
|
||||||
|
data = open(r'C:\git\bt411\content\BTL4.RES', 'rb').read()
|
||||||
|
labOnly, maxID = struct.unpack_from('<ii', data, 4)
|
||||||
|
offs = struct.unpack_from('<%dI' % maxID, data, 12)
|
||||||
|
TN = {1: 'ModelList', 9: 'BoxedSolidStream', 10: 'VideoModel', 11: 'StaticAudioStream',
|
||||||
|
15: 'GameModel', 16: 'Animation', 18: 'GaugeImageStream', 30: 'ExplosionTableStream'}
|
||||||
|
res = {}
|
||||||
|
for o in offs:
|
||||||
|
if o == 0:
|
||||||
|
continue
|
||||||
|
rid, rt = struct.unpack_from('<ii', data, o)
|
||||||
|
nm = data[o + 8:o + 40].split(b'\0')[0].decode('ascii', 'replace')
|
||||||
|
res[rid] = (rt, nm)
|
||||||
|
print('total resources:', len(res), 'maxID:', maxID)
|
||||||
|
print('=== descriptor effect ids (26-31) + weapon explode (13) ===')
|
||||||
|
for i in [13, 26, 27, 28, 29, 30, 31]:
|
||||||
|
r = res.get(i)
|
||||||
|
print(' id=%3d' % i, ('type=%d(%s) name=%r' % (r[0], TN.get(r[0], '?'), r[1])) if r else 'ABSENT')
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import struct, collections
|
||||||
|
|
||||||
|
RES = r"C:\git\bt411\content\BTL4.RES"
|
||||||
|
data = open(RES, "rb").read()
|
||||||
|
|
||||||
|
ver = data[0:4]
|
||||||
|
labOnly, maxID = struct.unpack_from("<ii", data, 4)
|
||||||
|
offsets = struct.unpack_from("<%dI" % maxID, data, 12)
|
||||||
|
|
||||||
|
Res = collections.namedtuple("Res", "rid rtype name prio flags off length")
|
||||||
|
resources = {}
|
||||||
|
for o in offsets:
|
||||||
|
if o == 0: continue
|
||||||
|
rid, rtype = struct.unpack_from("<ii", data, o)
|
||||||
|
name = data[o+8:o+40].split(b"\0")[0].decode("ascii","replace")
|
||||||
|
prio, flags, roff, rlen = struct.unpack_from("<iIII", data, o+40)
|
||||||
|
resources[rid] = Res(rid, rtype, name, prio, flags, roff, rlen)
|
||||||
|
|
||||||
|
t29 = sorted([r for r in resources.values() if r.rtype == 29], key=lambda x: x.name)
|
||||||
|
t20 = {r.name: r for r in resources.values() if r.rtype == 20}
|
||||||
|
print("version", list(ver), "type29 (DamageLookupTableStream) count =", len(t29))
|
||||||
|
print("type20 (DamageZoneStream) count =", len(t20))
|
||||||
|
print("name-match: every t29 name also a t20 name?",
|
||||||
|
all(r.name in t20 for r in t29), "\n")
|
||||||
|
|
||||||
|
def hexdump(b, base=0, n=64):
|
||||||
|
out=[]
|
||||||
|
for i in range(0, min(n,len(b)), 16):
|
||||||
|
chunk=b[i:i+16]
|
||||||
|
hexs=" ".join("%02x"%c for c in chunk)
|
||||||
|
out.append(" %04x %-47s" % (base+i, hexs))
|
||||||
|
return "\n".join(out)
|
||||||
|
|
||||||
|
def parse_table(b, name):
|
||||||
|
print("="*70)
|
||||||
|
print("TABLE '%s' (%d bytes)" % (name, len(b)))
|
||||||
|
print(hexdump(b, 0, 48))
|
||||||
|
p = 0
|
||||||
|
rowCount = struct.unpack_from("<i", b, p)[0]; p += 4
|
||||||
|
print(" rowCount =", rowCount)
|
||||||
|
for r in range(rowCount):
|
||||||
|
rotate = struct.unpack_from("<i", b, p)[0]; p += 4
|
||||||
|
cellCount = struct.unpack_from("<i", b, p)[0]; p += 4
|
||||||
|
print(" row[%d] rotateWithTorso=%d cellCount=%d" % (r, rotate, cellCount))
|
||||||
|
for c in range(cellCount):
|
||||||
|
# cell: [int slen][slen chars]["\0"][int cnt][cnt*(float threshold, int zone)]
|
||||||
|
slen = struct.unpack_from("<i", b, p)[0]; p += 4
|
||||||
|
if slen < 0 or slen > 64 or p+slen > len(b):
|
||||||
|
print(" cell[%d] *** slen sanity fail slen=%d @%d ***" % (c, slen, p-4)); return
|
||||||
|
s = b[p:p+slen].decode("ascii","replace"); p += slen + 1 # +1 = trailing null
|
||||||
|
cnt = struct.unpack_from("<i", b, p)[0]; p += 4
|
||||||
|
entries=[]
|
||||||
|
for _ in range(cnt):
|
||||||
|
thr = struct.unpack_from("<f", b, p)[0]
|
||||||
|
zone = struct.unpack_from("<i", b, p+4)[0]; p += 8
|
||||||
|
entries.append((round(thr,4), zone))
|
||||||
|
if r == 0:
|
||||||
|
print(" cell[%d] str='%s' cnt=%d entries=%s" % (c, s, cnt, entries))
|
||||||
|
status = "EXACT" if p == len(b) else "MISMATCH (off by %d)" % (len(b)-p)
|
||||||
|
print(" consumed %d / %d bytes -> %s" % (p, len(b), status))
|
||||||
|
|
||||||
|
# ground on the first 2 tables
|
||||||
|
for r in t29[:2]:
|
||||||
|
b = data[r.off:r.off+r.length]
|
||||||
|
parse_table(b, r.name)
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import struct
|
||||||
|
data = open(r'C:\git\bt411\content\BTL4OPT.EXE', 'rb').read()
|
||||||
|
e = struct.unpack_from('<I', data, 0x3C)[0]; coff = e+4
|
||||||
|
num = struct.unpack_from('<H', data, coff+2)[0]; osz = struct.unpack_from('<H', data, coff+16)[0]
|
||||||
|
opt = coff+20; base = struct.unpack_from('<I', data, opt+28)[0]; sect = opt+osz
|
||||||
|
secs = []
|
||||||
|
for i in range(num):
|
||||||
|
o = sect+i*40
|
||||||
|
vs, va, rs, rp = struct.unpack_from('<IIII', data, o+8)
|
||||||
|
secs.append((va, vs, rp, rs))
|
||||||
|
|
||||||
|
def off2va(off):
|
||||||
|
for va, vs, rp, rs in secs:
|
||||||
|
if rp <= off < rp+rs:
|
||||||
|
return base+va+(off-rp)
|
||||||
|
return None
|
||||||
|
|
||||||
|
targets = {0x49ed0c: 'FUN_0049ed0c (glue: table->zone) == the handler',
|
||||||
|
0x49de14: 'FUN_0049de14 (cell->zone roll)'}
|
||||||
|
cva, cvs, crp, crs = secs[0]
|
||||||
|
for i in range(crp, crp+crs-5):
|
||||||
|
if data[i] == 0xE8:
|
||||||
|
rel = struct.unpack_from('<i', data, i+1)[0]
|
||||||
|
site = off2va(i)
|
||||||
|
tgt = site+5+rel
|
||||||
|
if tgt in targets:
|
||||||
|
print('call site VA %08x -> %s' % (site, targets[tgt]))
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import struct
|
||||||
|
data = open(r'C:\git\bt411\content\BTL4.RES', 'rb').read()
|
||||||
|
labOnly, maxID = struct.unpack_from('<ii', data, 4)
|
||||||
|
offs = struct.unpack_from('<%dI' % maxID, data, 12)
|
||||||
|
anims = []
|
||||||
|
for o in offs:
|
||||||
|
if o == 0:
|
||||||
|
continue
|
||||||
|
rid, rt = struct.unpack_from('<ii', data, o)
|
||||||
|
nm = data[o + 8:o + 40].split(b'\0')[0].decode('ascii', 'replace')
|
||||||
|
if rt == 16:
|
||||||
|
anims.append((rid, nm))
|
||||||
|
print('type-16 Animation resources:', len(anims))
|
||||||
|
for rid, nm in sorted(anims, key=lambda x: x[1]):
|
||||||
|
print(' id=%4d %s' % (rid, nm))
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import sys
|
||||||
|
data = open(r'C:\git\bt411\content\BTL4.RES', 'rb').read()
|
||||||
|
low = data.lower()
|
||||||
|
for name in [b'blhd.bgf', b'blhdbr', b'blh_tor', b'blhd_', b'blhdlarm', b'blhdlule', b'blhdldle', b'blhdlfot']:
|
||||||
|
i = 0
|
||||||
|
hits = []
|
||||||
|
while True:
|
||||||
|
i = low.find(name.lower(), i)
|
||||||
|
if i < 0:
|
||||||
|
break
|
||||||
|
hits.append(i)
|
||||||
|
i += 1
|
||||||
|
print(name.decode(), 'x', len(hits), hits[:6])
|
||||||
|
# context around blhd.bgf hits
|
||||||
|
i = low.find(b'blhd.bgf')
|
||||||
|
if i >= 0:
|
||||||
|
print(repr(data[i-160:i+40]))
|
||||||
Reference in New Issue
Block a user