#87: mech armour PANELS now darken with damage -- the .DZM material system was loaded and unused
Players: "the actual enemy mech in external view is not showing darkened armor panels". We swapped destroyed LIMB meshes but never darkened a panel. The 1995 game darkens armour through the MATERIALS. MakeMechRenderables (FUN_004cef28) built, per (damage zone, material), a watcher FUN_004573e4(material, &zone->damageLevel, 0.1f) that snapshotted the materials
This commit is contained in:
@@ -48,7 +48,11 @@ spec is `docs/ASSET_PIPELINE.md`. Full list: `docs/PROGRESS_LOG.md §5`.
|
||||
## Collision / damage / subsystems
|
||||
- **`.SLD`** collision solids (Block/YCyl/Cone/Ramp/Wedge). Terrain is modeled AS collision volumes;
|
||||
see [[locomotion]] ground model + [[combat-damage]]. [T1]
|
||||
- **`.DZM`** = INI damage-zone→material map (mech skin damage states). **`.DMG`/`.TBL`** damage
|
||||
- **`.DZM`** = INI damage-zone→material map (mech skin damage states) — `[dz_<zone>]` sections each
|
||||
listing `material=<lib>:<name>_mtl`. One file per SKIN VARIANT (`dzm`/`dzms`/`dzmt`/`dzmo`/`dzma`/
|
||||
`dzmb`/`dzmc`/`dzmd`). This is the ARMOUR-DARKENING registry: the renderer lerps every listed
|
||||
material toward 0.1× as that zone's `damageLevel` runs 0→1. Loaded by MUNGA's `DamageZone` stream
|
||||
ctor into `materialTable` (`GetMaterialList(skl_type)`); full chain in [[rendering]]. [T1] **`.DMG`/`.TBL`** damage
|
||||
zones/tables, **`.SUB`** subsystems (weapon params — DischargeTime/RechargeRate/WeaponRange),
|
||||
**`.CTL`** control maps. [T1]
|
||||
|
||||
|
||||
@@ -474,6 +474,8 @@ default-ON (`'0'` disables).
|
||||
| `BT_POWER_DETACH_TEST=<name\|1>` | drop a subsystem's voltage link + force Auto, so the auto-hunt must recover it. `1` = first powered subsystem to tick; a NAME (`PPC_1`, `Myomers`) targets one, which is what proves FAILOVER to a different generator rather than a same-generator re-attach |
|
||||
| `BT_AUDIO_SOURCES=<n>` | request `n` OpenAL mono sources instead of the driver default (~256). **Opt-in on purpose** — the cap doubles as a governor, and with EFX reverb live a higher ceiling means more simultaneous voices mixing during heavy combat. Measure frame time. See [[wintesla-port]] |
|
||||
| `BT_SELF_DAMAGE_ZONE=<n>` | aim the BT_SELF_DAMAGE harness at an explicit zone (-1/unset = the lottery) -- ramps one zone deterministically past thresholds (#78 bench) |
|
||||
| `BT_ARMOR_LOG` | mech ARMOUR-DARKENING trace (#87): per-zone `.DZM` list -> draw-op binding counts at tree build, any `unmatched` material name, and every change of a bound zone's level with the resulting brightness |
|
||||
| `BT_ARMOR_FORCE=<0..1>` | pin EVERY bound armour zone to a fixed damage level, so the same scene can be rendered pristine and fully-damaged and diffed pixel-wise (`scratchpad/night7/armor_ab.sh`) |
|
||||
| `BT_DRIVE_LOG` | the demand-site drive scale: `[drive] n/drive/mm/dmd/mech` ~1Hz (drive = myomers speedEffect x gimp; mm = the gimp level via BTMechGimpLevel) |
|
||||
| `BT_GIMP_SPEED=<0..1>` | EXTRA gimp demand multiplier (default 1.0 = authentic since 2026-07-30: the real slowdown is the gimp gait machines' clamp to the wg clip speed — see [[locomotion]] §visible limp; the old 0.5 T3 stand-in is retired) |
|
||||
| `BT_PICK_LOG` | #73 aimed-pick diagnostics: `[segpick]` the segment→zone map at tree build (index, name, zone, sphere), `[pickwin]` the winning zone/score/t per pick (score ~0 = threading the part core, ~1 = envelope graze) |
|
||||
@@ -497,6 +499,20 @@ recoil @4bc136-4bc19c (damage>3 → (0,0.6,−1.5) × damage/16; `gyroRumbleTime
|
||||
ex-"clipLoadGuard"). Byte tooling: `scratchpad/dis_4b2980.py` / `dis_range.py` over
|
||||
`reference/decomp/section_dump.txt`.
|
||||
|
||||
**ARMOUR-DARKENING addresses** [T1] (#87; full story: [[rendering]]): `.DZM` res compiler
|
||||
`FUN_0041e4e0` (part_002.c:7341, reads the `material` key list per `[dz_*]` section); runtime
|
||||
consumer inside `BTL4VideoRenderer::MakeMechRenderables` **`FUN_004cef28`** (part_014.c:5190-5272);
|
||||
per-zone node ctor `FUN_00455c7c(...,0.04f)`; the per-material watcher
|
||||
**`FUN_004573e4(material, &zone->damageLevel, 0.1f)`** (part_007.c:9133, vtable `PTR_LAB_004f1730`,
|
||||
232 bytes) with Perform **`FUN_00457784`** (lerp constant `@0x4579a4` = 1.0; damaged factor
|
||||
`0x3dcccccd` = 0.1). Material accessors: `dpl_GetMaterialAmbient` `FUN_0048baf4` /
|
||||
`Set` `FUN_0048ba98`, `Emissive` `FUN_0048bbb0`/`FUN_0048bb54`, `Diffuse` `FUN_0048bc6c`/
|
||||
`FUN_0048bc10`, `Specular`(4) `FUN_0048bd2c`/`FUN_0048bccc`, `Opacity` `FUN_0048bdf0` (READ ONLY --
|
||||
lerped but never pushed), flush `FUN_0048d4d4`; `dpl_SetMaterialRamp` `@0x48bfb0` (writes
|
||||
`material+0x1c`; ONE caller, load-time only). Material lookup by name `FUN_00497678`. The UNUSED
|
||||
sibling path: `dpl_Damagize` `FUN_004902b0` (tokens `{0x92,0x05,0x04}`), `SV_SPECIAL DAMAGE`
|
||||
dispatch @0x4599ae -- **zero shipped BGFs carry a DAMAGE token**.
|
||||
|
||||
**PUNCH firmware decode** [T1] (the definitive mechanism, 2026-07-11): cmd 0x20 =
|
||||
vr_damage_action (VR_PROT.H enum); the i860 firmware content/VREND.MNG (code @0xf0400000)
|
||||
handler 'damageize' @0xf040f6f8 writes the token triple onto the geogroup's first THREE
|
||||
|
||||
@@ -737,3 +737,21 @@ a sibling scalar — read it through the object that owns it, or (if a duplicate
|
||||
exists) make every gate read BOTH and every writer write BOTH. Sibling of gotcha #1 (shadowed
|
||||
base field): same failure shape — two cells where the binary has one, and the readers pick the
|
||||
dead one.
|
||||
|
||||
## §23 — Scaling `D3DMATERIAL9` does NOTHING for BGF geometry (the vertex-colour diffuse source)
|
||||
**(2026-07-31, gitea #87 "mech armour panels don't darken".)** The 1995 armour damage darkens a
|
||||
mech by scaling its MATERIALS' colour terms (`dpl_SetMaterialAmbient/Emissive/Diffuse/Specular`).
|
||||
The obvious port translation — scale the draw op's `D3DMATERIAL9` before `SetMaterial` — renders
|
||||
**byte-identically**: measured **0 changed pixels** at a full 0.1x scale.
|
||||
**Why:** D3D9 defaults `D3DRS_DIFFUSEMATERIALSOURCE` to **`D3DMCS_COLOR1`** (and `D3DRS_COLORVERTEX`
|
||||
to TRUE), so when a vertex carries a diffuse colour the material's Diffuse is never consulted.
|
||||
Every BGF vertex carries a baked colour (that IS the 1995 shading model — no-normal geometry is
|
||||
unlit and coloured by vertex/ramp), so for essentially all world geometry `SetMaterial` is inert
|
||||
for colour. The material only matters for lit, normal-bearing meshes.
|
||||
**Fix pattern:** modulate the FINAL fragment instead — `D3DRS_TEXTUREFACTOR` + a
|
||||
`MODULATE(CURRENT, TFACTOR)` on texture **stage 1** (unused in `d3d_OBJECT::Draw`), restored to
|
||||
`D3DTOP_DISABLE` after the op. That darkens the result whatever the diffuse source was, and
|
||||
equally covers the ramp-baked-texture path and pure-emissive batches.
|
||||
**Detection smell:** a colour change that logs perfectly at the source and produces zero visible
|
||||
difference. Diff two runs pixel-wise against a control region before believing a colour path
|
||||
works — "the log says 0.1x" is not evidence that anything reached the screen.
|
||||
|
||||
@@ -321,6 +321,57 @@ Diags: `BT_CULT_LOG` (census/damage/death + `[cultvis]` + `[cultobj]` per-object
|
||||
flags). MP: replicants transition via `ReadUpdateRecord` → same SetState watcher path (untested
|
||||
cross-pod).
|
||||
|
||||
## Mech ARMOUR DARKENING (the .DZM material-damage system) — RECONSTRUCTED 2026-07-31 (Gitea #87) [T2]
|
||||
A mech's armour panels **darken as their damage zone takes damage** — a per-zone, per-material
|
||||
colour ramp-down, NOT a texture swap, a filter, or geometry. Players reported the port showing only
|
||||
the limb swap ("the actual enemy mech in external view is not showing darkened armor panels").
|
||||
|
||||
**The 1995 chain** [T1]:
|
||||
- **`.DZM`** (`VIDEO\<mech>SKIN.DZM`, 40 shipped) lists, per damage zone, the materials that zone
|
||||
paints: `[dz_ltorso] material=avaskin:avat2_dz_ltorso_mtl`. One file per SKIN VARIANT — the
|
||||
`dzm`/`dzms`/`dzmt`/`dzmo`/`dzma`/`dzmb`/`dzmc`/`dzmd` key family (same suffix set as the
|
||||
`destroyed`…`destroyedd` video variants); the res compiler is `FUN_0041e4e0`.
|
||||
- Compiled into BTL4.RES and parsed by **MUNGA's own `DamageZone` stream ctor** into
|
||||
`materialTable`, keyed by `EntitySegment::SkeletonType` (`engine/MUNGA/DAMAGE.cpp:311-336`,
|
||||
`GetMaterialList(skl_type)`) — real engine source [T0]. **This data was loaded and unused in the
|
||||
port all along**; only the consumer was missing.
|
||||
- **`BTL4VideoRenderer::MakeMechRenderables` (`FUN_004cef28`)** walks the zones and builds, per
|
||||
(zone, material), a 232-byte watcher **`FUN_004573e4(material, &zone->damageLevel, 0.1f)`**
|
||||
(a 60-byte per-zone parent first, factor 0.04). Error string: *"BTL4VideoRenderer: Material name
|
||||
<X> could not be found"* @0x51d6f8.
|
||||
- The watcher snapshots the material's **16 authored colour floats** — Ambient(3), Emissive(3),
|
||||
Diffuse(3), Specular+shininess(4), Opacity(3) — precomputes a DAMAGED set = **pristine x 0.1**
|
||||
(`0x3dcccccd`), and **`FUN_00457784`** re-pushes `lerp(pristine, damaged, damageLevel)` whenever
|
||||
the level changes (constant `@0x4579a4` = **1.0**). So a zone at full damage renders at **10%**
|
||||
of authored brightness, continuously interpolated.
|
||||
- **Faithful quirk:** Opacity is read and lerped but **never written back** — only 13 of the 16
|
||||
values are pushed (`dpl_GetMaterialOpacity` has no matching Set call in the watcher).
|
||||
|
||||
**NOT the mechanism** [T2]: the engine also has `SV_SPECIAL DAMAGE <material>` /
|
||||
**`dpl_Damagize` `FUN_004902b0`** (punch's sibling — same VPX cmd 0x20, tokens
|
||||
`f_t_dam(0x92)/sca(0x05)/undamage(0x04)`, dispatch @0x4599ae; error *"SPECIAL GEOGROUP DAMAGE
|
||||
couldn't find material"*). A scan of **all 879 shipped BGFs finds ZERO `DAMAGE` tokens** — the path
|
||||
exists in the engine but no shipped asset uses it. Don't chase it.
|
||||
|
||||
**The port** (`game/reconstructed/btl4vid.cpp` `BindArmourDamage`/`TickArmourDamage`,
|
||||
`engine/MUNGA_L4/L4D3D.cpp`): batches now carry their AUTHORED (pre-substitution) material name
|
||||
(`BgfDrawBatch::matName` -> `L4DRAWOP::dzMatName`); binding resolves the .DZM lists onto real draw
|
||||
ops once per tree build, and the per-frame tick (driven from `Mech::PerformAndWatch`, EVERY mech)
|
||||
copies `zone->damageLevel` onto them. Two hard-won details:
|
||||
- **Key the material list on `viewSkeleton`, not the build `skeletonType`.** Each skeleton variant
|
||||
is painted from its own skin library (Black Hawk N = `blhskin:`, X = `blxskin:`) and
|
||||
`ApplyViewSkeleton` reloads every segment mesh; keying on the build skeleton matched a list
|
||||
belonging to geometry that is not on screen -> **zero bindings, no darkening**.
|
||||
- **Scaling the `D3DMATERIAL9` is inert** — see gotcha **§23** (`DIFFUSEMATERIALSOURCE` defaults to
|
||||
vertex colour). The darkening is applied as a `TEXTUREFACTOR` modulate on texture stage 1.
|
||||
Rebinding runs after every geometry swap (`RemakeEntityRenderables`, `ApplyViewSkeleton`), which
|
||||
also keeps `segPick` (the #73 aimed pick) off discarded meshes.
|
||||
Bench: `scratchpad/night7/armor_darken.sh` (self/enemy/ext legs) + `armor_ab.sh` (forced-level
|
||||
pixel A/B). Gates `BT_ARMOR_LOG` / `BT_ARMOR_FORCE=<0..1>`. Verified: 57 bindings on the enemy
|
||||
mech, brightness tracking `1 - 0.9*level` exactly, hull 0.35x vs terrain control 0.98x at level 1.
|
||||
The `damcolor_`/`gen*gry_` arm+gun names go unmatched until the DESTROYED limb variant loads —
|
||||
correct: those materials live on the `*D*.BGF` damaged meshes.
|
||||
|
||||
## Key Relationships
|
||||
- Geometry/LOD: [[bgf-format]]. Base: [[wintesla-port]] (L4D3D). Shadow/visual-conform: [[locomotion]].
|
||||
- Renders on: [[pod-hardware]] (main 3D view).
|
||||
|
||||
@@ -393,6 +393,17 @@ d3d_OBJECT* d3d_OBJECT::LoadObjectBGF(LPDIRECT3DDEVICE9 device, char *fileName)
|
||||
object->mDrawOps[i].lodNear = data.batches[i].lodNear;
|
||||
object->mDrawOps[i].lodFar = data.batches[i].lodFar;
|
||||
object->mDrawOps[i].lodDepthBias = data.batches[i].lodBias;
|
||||
// MECH ARMOUR DAMAGE (issue #87): carry the authored material name so the
|
||||
// mech's .DZM zone->material lists can drive this batch's darkening.
|
||||
{
|
||||
const std::string &mn = data.batches[i].matName;
|
||||
size_t n = mn.size();
|
||||
if (n >= sizeof(object->mDrawOps[i].dzMatName))
|
||||
n = sizeof(object->mDrawOps[i].dzMatName) - 1;
|
||||
memcpy(object->mDrawOps[i].dzMatName, mn.c_str(), n);
|
||||
object->mDrawOps[i].dzMatName[n] = '\0';
|
||||
object->mDrawOps[i].dzDamageLevel = 0.0f;
|
||||
}
|
||||
|
||||
const bool useRamp = (s_ramp && data.batches[i].hasRamp);
|
||||
uint32_t c = data.batches[i].color;
|
||||
@@ -1198,6 +1209,33 @@ void d3d_OBJECT::DrawMesh(int pass, const D3DXMATRIX *viewTransform, Time target
|
||||
|
||||
mDevice->SetMaterial(&drawOp->material);
|
||||
|
||||
// MECH ARMOUR DAMAGE (issue #87): the 1995 renderer kept a per-material
|
||||
// watcher that lerped the material's colour terms from the authored values
|
||||
// toward 0.1x as the owning damage zone's damageLevel ran 0->1
|
||||
// (FUN_004573e4 precomputed pristine*0.1; FUN_00457784 re-pushed
|
||||
// lerp(pristine, damaged, level) on every change, constant @0x4579a4 = 1.0).
|
||||
//
|
||||
// We cannot express that by scaling this op's D3DMATERIAL9: D3D9 defaults
|
||||
// DIFFUSEMATERIALSOURCE to D3DMCS_COLOR1, and every BGF vertex carries a
|
||||
// baked colour, so the material's Diffuse is never consulted for this
|
||||
// geometry -- scaling it renders identically (measured: 0 changed pixels).
|
||||
// Instead modulate the FINAL colour by k on texture stage 1, which darkens
|
||||
// the result whatever the diffuse source was, and equally covers the
|
||||
// ramp-baked-texture path (the mech's own case) and pure-emissive batches.
|
||||
const bool damageTint = (drawOp->dzDamageLevel > 0.0f);
|
||||
if (damageTint)
|
||||
{
|
||||
float d = drawOp->dzDamageLevel;
|
||||
if (d > 1.0f) d = 1.0f;
|
||||
const float k = 1.0f - (1.0f - kDamagedMaterialScale) * d; // lerp(1, 0.1, d)
|
||||
mDevice->SetRenderState(D3DRS_TEXTUREFACTOR, D3DCOLOR_COLORVALUE(k, k, k, 1.0f));
|
||||
mDevice->SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_MODULATE);
|
||||
mDevice->SetTextureStageState(1, D3DTSS_COLORARG1, D3DTA_CURRENT);
|
||||
mDevice->SetTextureStageState(1, D3DTSS_COLORARG2, D3DTA_TFACTOR);
|
||||
mDevice->SetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);
|
||||
mDevice->SetTextureStageState(1, D3DTSS_ALPHAARG1, D3DTA_CURRENT);
|
||||
}
|
||||
|
||||
#ifndef RP3_EMULATE
|
||||
SetTextureScrolling(&(drawOp->texture), targetRenderFrame);
|
||||
SetTexture(drawOp->texture.texture);
|
||||
@@ -1337,6 +1375,13 @@ void d3d_OBJECT::DrawMesh(int pass, const D3DXMATRIX *viewTransform, Time target
|
||||
mDevice->SetTextureStageState(0, D3DTSS_ALPHAOP, sVAop);
|
||||
mDevice->SetTextureStageState(0, D3DTSS_ALPHAARG2, sVAa2);
|
||||
}
|
||||
if (damageTint)
|
||||
{
|
||||
// stage 1 is otherwise unused in this draw path -- put it back to
|
||||
// DISABLE so the next (undamaged) op is not tinted too.
|
||||
mDevice->SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE);
|
||||
mDevice->SetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE);
|
||||
}
|
||||
if (drawOp->lodDepthBias != 0.0f)
|
||||
mDevice->SetRenderState(D3DRS_DEPTHBIAS, sDB);
|
||||
}
|
||||
|
||||
@@ -619,6 +619,9 @@ struct Builder {
|
||||
uint32_t currentColor = 0xFFB0B0B8u;
|
||||
bool currentHasDiffuse = false; // material carried an explicit diffuse tag
|
||||
std::string currentTex;
|
||||
// authored (pre-substitution) "library:material_mtl" of the material in force --
|
||||
// the .DZM armour-damage lists key on this (issue #87)
|
||||
std::string currentMatName;
|
||||
int currentTexChannel = 0; // BSL bit-slice (BMF tag 0x18)
|
||||
bool currentTexScroll = false; // TEXTURE SPECIAL " SCROLL" (tag 0x2037)
|
||||
float currentTexScrollU = 0.0f, currentTexScrollV = 0.0f;
|
||||
@@ -872,6 +875,7 @@ struct Builder {
|
||||
batch.color = pureEmissive ? currentColor // keep BLACK: L4D3D's
|
||||
: (useRamp ? rampTint : currentColor); // pure-emissive test keys on it
|
||||
batch.texPath = currentTex;
|
||||
batch.matName = currentMatName; // .DZM armour-damage key (issue #87)
|
||||
batch.texChannel = currentTexChannel;
|
||||
batch.texScroll = currentTexScroll;
|
||||
batch.texScrollU = currentTexScrollU;
|
||||
@@ -1185,6 +1189,9 @@ struct Builder {
|
||||
size_t maxn = ch.len - 1, n = 0;
|
||||
while (n < maxn && s[n]) ++n;
|
||||
std::string full(s, n);
|
||||
// remember the AUTHORED name (before the paint callback rewrites
|
||||
// it) -- the .DZM zone material lists are authored against it
|
||||
currentMatName = full;
|
||||
// baked ground-shadow material (basev:shadow_mtl etc.)
|
||||
currentShadowMat = false;
|
||||
for (size_t si = 0; si + 6 <= full.size(); ++si)
|
||||
|
||||
@@ -106,6 +106,17 @@ struct BgfDrawBatch {
|
||||
// routed through the mech-shadow pipeline (translucent dark, depth-biased,
|
||||
// no z-write) instead.
|
||||
bool shadowMat = false;
|
||||
// AUTHORED MATERIAL NAME ("library:material_mtl", e.g. "avaskin:avat2_dz_ltorso_mtl"),
|
||||
// recorded PRE-substitution -- the per-pilot paint callback rewrites names, but the
|
||||
// .DZM damage-zone material lists are authored against these base names.
|
||||
//
|
||||
// This is the key the mech ARMOUR-DARKENING system matches on (issue #87): the 1995
|
||||
// BTL4VideoRenderer built one material-damage watcher per (zone, material) pair
|
||||
// (FUN_004573e4) that lerped the material's colour terms from pristine toward 0.1x
|
||||
// as that zone's damageLevel ran 0->1. Our draw path bakes colour into a
|
||||
// D3DMATERIAL9 at load, so the equivalent modulation is applied per draw op --
|
||||
// see L4D3D d3d_OBJECT::SetDamageScaleForMaterials.
|
||||
std::string matName;
|
||||
};
|
||||
|
||||
struct BgfData {
|
||||
|
||||
@@ -71,6 +71,12 @@ struct L4RAMP
|
||||
float r1, g1, b1;
|
||||
};
|
||||
|
||||
// MECH ARMOUR DAMAGE (issue #87): the factor the 1995 material-damage watcher
|
||||
// (FUN_004573e4) precomputed as the FULLY-DAMAGED colour -- pristine x 0.1, the
|
||||
// literal 0x3dcccccd passed at every construction site. A zone at damageLevel 1
|
||||
// renders its panels at 10% of the authored brightness.
|
||||
const float kDamagedMaterialScale = 0.1f;
|
||||
|
||||
struct L4DRAWOP
|
||||
{
|
||||
D3DMATERIAL9 material;
|
||||
@@ -112,6 +118,18 @@ struct L4DRAWOP
|
||||
// coplanar duplicated surfaces resolve to the detail layer instead of
|
||||
// venetian-blind z-fighting (the board's submission-order rule, in D3D terms).
|
||||
float lodDepthBias;
|
||||
// MECH ARMOUR DAMAGE (issue #87). The 1995 BTL4VideoRenderer bound one
|
||||
// material-damage watcher per (damage zone, material) pair -- FUN_004573e4 --
|
||||
// which lerped that material's colour terms from their authored values toward
|
||||
// 0.1x as the zone's damageLevel ran 0->1 (FUN_00457784 re-pushed them whenever
|
||||
// the level changed). The zone->material lists come from the per-mech .DZM
|
||||
// files, already parsed into DamageZone::materialTable (MUNGA DAMAGE.cpp).
|
||||
//
|
||||
// Our draw path bakes colour into this D3DMATERIAL9 at load, so the equivalent
|
||||
// modulation is applied at SetMaterial time from the level cached here.
|
||||
// Both fields are memset-0 safe: empty name = not zone-mapped, level 0 = pristine.
|
||||
char dzMatName[64]; // authored "library:material_mtl" for this batch
|
||||
float dzDamageLevel; // owning zone's damageLevel [0..1] (0 = undamaged)
|
||||
};
|
||||
|
||||
class d3d_OBJECT
|
||||
|
||||
@@ -743,6 +743,11 @@ HierarchicalDrawComponent*
|
||||
<< (mEyeCockpit ? "" : " (COCKPIT EYE MISSING)") << "\n" << std::flush;
|
||||
}
|
||||
|
||||
// ARMOUR DARKENING (issue #87): the segment geometry is loaded and segPick is
|
||||
// populated, so the mech's .DZM zone->material lists can now be resolved onto
|
||||
// real draw ops. The binary does this in the same function.
|
||||
BindArmourDamage(entity, render_tree);
|
||||
|
||||
return this_root;
|
||||
}
|
||||
|
||||
@@ -852,9 +857,30 @@ void
|
||||
else if ((int)seg_gstate == DamageZone::GoneGraphicState)
|
||||
r->second->SetDrawObj(NULL);
|
||||
|
||||
// The swap replaced the DRAWN object, so the segment's recorded geometry
|
||||
// is now stale. Re-point it: the armour-damage bindings (issue #87) hold
|
||||
// draw-op addresses inside these objects and would otherwise keep tinting
|
||||
// the mesh that was just swapped out. (#73's aimed pick reads the same
|
||||
// map, so it stops ray-testing a discarded mesh too.)
|
||||
{
|
||||
std::map<int, MechRenderTree::SegPick>::iterator sp =
|
||||
render_tree.segPick.find(segment_slot);
|
||||
if (sp != render_tree.segPick.end())
|
||||
{
|
||||
if (new_object != NULL && new_object->GetIsShadow() == 0)
|
||||
sp->second.obj = new_object;
|
||||
else if ((int)seg_gstate == DamageZone::GoneGraphicState)
|
||||
render_tree.segPick.erase(sp); // blown off: no geometry to hit or tint
|
||||
}
|
||||
}
|
||||
|
||||
++swapped;
|
||||
}
|
||||
|
||||
// Rebind the .DZM armour materials onto whatever geometry is now drawn.
|
||||
if (swapped != 0)
|
||||
BindArmourDamage(entity, render_tree);
|
||||
|
||||
if (swapped != 0 || getenv("BT_DEATH_LOG"))
|
||||
DEBUG_STREAM << "[BTrender] RemakeEntity: " << swapped
|
||||
<< " mesh(es) swapped (" << mapped << " body segs mapped of "
|
||||
@@ -1319,6 +1345,190 @@ int
|
||||
}
|
||||
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// ARMOUR DARKENING -- BindArmourDamage / TickArmourDamage (issue #87)
|
||||
//
|
||||
// The 1995 renderer darkened a mech's armour panels as they took damage, and it
|
||||
// did it through the MATERIALS, not through geometry. In MakeMechRenderables
|
||||
// (FUN_004cef28) it walked the mech's damage zones and, for every material that
|
||||
// zone paints, built a watcher (FUN_004573e4) holding {material, &zone->damageLevel,
|
||||
// 0.1}. The watcher snapshotted the material's sixteen authored colour floats
|
||||
// (ambient 3, emissive 3, diffuse 3, specular+shininess 4, opacity 3), precomputed
|
||||
// a DAMAGED set = pristine x 0.1, and on every change of the level (FUN_00457784)
|
||||
// re-pushed lerp(pristine, damaged, level) -- the constant at 0x4579a4 is 1.0.
|
||||
// (Faithful quirk: opacity is read and lerped but never written back -- only 13 of
|
||||
// the 16 values are pushed. We scale colour terms only, which matches.)
|
||||
//
|
||||
// The zone->material lists are the per-mech .DZM files (VIDEO\<mech>SKIN.DZM,
|
||||
// "[dz_ltorso] material=avaskin:avat2_dz_ltorso_mtl"), compiled into BTL4.RES and
|
||||
// already parsed by MUNGA's own DamageZone stream ctor into materialTable, keyed
|
||||
// by skeleton type (DAMAGE.cpp:311-336) -- the data has been loaded and unused all
|
||||
// along. What was missing was purely the consumer.
|
||||
//
|
||||
// Our draw path bakes material colour into each draw op's D3DMATERIAL9 at load, so
|
||||
// instead of holding material pointers we bind (draw object, op index) pairs here
|
||||
// once, and copy the live level onto them each frame; L4D3D applies the same lerp
|
||||
// at SetMaterial time. Binding by op index keeps the per-frame cost to a pointer
|
||||
// write -- no string compare in the frame loop.
|
||||
//
|
||||
void
|
||||
BTL4VideoRenderer::BindArmourDamage(Entity *entity, MechRenderTree &tree)
|
||||
{
|
||||
tree.dmgBinds.clear();
|
||||
if (entity == NULL)
|
||||
return;
|
||||
|
||||
const int log = (getenv("BT_ARMOR_LOG") != NULL);
|
||||
|
||||
for (int zone_index = 0; zone_index < entity->damageZoneCount; ++zone_index)
|
||||
{
|
||||
DamageZone *zone = entity->damageZones[zone_index];
|
||||
if (zone == NULL)
|
||||
continue;
|
||||
|
||||
// The .DZM list for the skeleton currently DISPLAYED -- not the one the
|
||||
// tree was built with. Each skeleton variant is painted from its own
|
||||
// skin library (the Black Hawk's N set is blhskin:, its X set blxskin:),
|
||||
// and ApplyViewSkeleton reloads every segment mesh when the view changes,
|
||||
// so keying on the build skeleton matches a material list belonging to
|
||||
// geometry that is not on screen -> zero bindings and no darkening.
|
||||
// A zone with no list for this skeleton simply never darkens, as on the pod.
|
||||
MaterialList *material_list =
|
||||
zone->GetMaterialList((Enumeration)tree.viewSkeleton);
|
||||
if (material_list == NULL)
|
||||
continue;
|
||||
|
||||
int zone_binds = 0;
|
||||
material_list->First();
|
||||
CString *material_name;
|
||||
while ((material_name = material_list->ReadAndNext()) != NULL)
|
||||
{
|
||||
const char *want = (const char *)*material_name;
|
||||
if (want == NULL || want[0] == '\0')
|
||||
continue;
|
||||
int name_hits = 0;
|
||||
|
||||
// Every non-shadow segment object of this mech is a candidate: one
|
||||
// zone's materials can span several segments (the torso zones paint
|
||||
// across the torso and door pieces), and one segment can carry
|
||||
// several zones' materials.
|
||||
for (std::map<int, MechRenderTree::SegPick>::iterator si = tree.segPick.begin();
|
||||
si != tree.segPick.end(); ++si)
|
||||
{
|
||||
d3d_OBJECT *obj = si->second.obj;
|
||||
if (obj == NULL)
|
||||
continue;
|
||||
for (int op = 0; op < obj->GetDrawOpCount(); ++op)
|
||||
{
|
||||
L4DRAWOP *draw_op = obj->GetDrawOp(op);
|
||||
if (draw_op->dzMatName[0] == '\0')
|
||||
continue;
|
||||
// Both sides are hand-authored; case does not agree on every mech.
|
||||
if (_stricmp(draw_op->dzMatName, want) != 0)
|
||||
continue;
|
||||
MechRenderTree::DmgBind bind;
|
||||
bind.obj = obj;
|
||||
bind.op = op;
|
||||
bind.zone = zone_index;
|
||||
tree.dmgBinds.push_back(bind);
|
||||
draw_op->dzDamageLevel = 0.0f;
|
||||
++zone_binds;
|
||||
++name_hits;
|
||||
}
|
||||
}
|
||||
// A .DZM name that matches NOTHING on the drawn geometry is the
|
||||
// failure mode worth naming: it means the material list and the mesh
|
||||
// disagree (wrong skeleton key, or a mech re-skinned since authoring),
|
||||
// and that zone silently stops darkening.
|
||||
if (log && name_hits == 0)
|
||||
DEBUG_STREAM << "[armor] unmatched '" << want << "' (zone "
|
||||
<< zone_index << ")\n" << std::flush;
|
||||
}
|
||||
if (log)
|
||||
DEBUG_STREAM << "[armor] zone " << zone_index << " '"
|
||||
<< (const char *)zone->damageZoneName << "' -> "
|
||||
<< zone_binds << " draw op(s)\n" << std::flush;
|
||||
}
|
||||
|
||||
if (log)
|
||||
DEBUG_STREAM << "[armor] entity " << (void *)entity << " skel="
|
||||
<< tree.skeletonType << ": " << tree.dmgBinds.size()
|
||||
<< " material binding(s)\n" << std::flush;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
BTL4VideoRenderer::TickArmourDamage(Entity *entity)
|
||||
{
|
||||
std::map<Entity*, MechRenderTree>::iterator tree_it =
|
||||
mMechRenderTrees.find(entity);
|
||||
if (tree_it == mMechRenderTrees.end())
|
||||
return;
|
||||
MechRenderTree &tree = tree_it->second;
|
||||
if (tree.dmgBinds.empty())
|
||||
return;
|
||||
// A wrecked mech has been swapped to the <mech>dbr hulk -- the bound segment
|
||||
// objects are gone from the draw; leave them alone.
|
||||
if (tree.wrecked)
|
||||
return;
|
||||
|
||||
const int log = (getenv("BT_ARMOR_LOG") != NULL);
|
||||
|
||||
// BENCH (BT_ARMOR_FORCE=<0..1>): pin EVERY bound zone to a fixed level, so the
|
||||
// same scene can be rendered undamaged and fully-damaged and diffed pixel-wise
|
||||
// without smoke, motion or death confounding the comparison.
|
||||
static int s_forceInit = 0;
|
||||
static int s_forceOn = 0;
|
||||
static float s_forceLevel = 0.0f;
|
||||
if (!s_forceInit)
|
||||
{
|
||||
s_forceInit = 1;
|
||||
const char *fv = getenv("BT_ARMOR_FORCE");
|
||||
if (fv != NULL && *fv != '\0')
|
||||
{
|
||||
s_forceOn = 1;
|
||||
s_forceLevel = (float)atof(fv);
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < tree.dmgBinds.size(); ++i)
|
||||
{
|
||||
MechRenderTree::DmgBind &bind = tree.dmgBinds[i];
|
||||
if (bind.zone < 0 || bind.zone >= entity->damageZoneCount)
|
||||
continue;
|
||||
DamageZone *zone = entity->damageZones[bind.zone];
|
||||
if (zone == NULL || bind.obj == NULL)
|
||||
continue;
|
||||
L4DRAWOP *draw_op = bind.obj->GetDrawOp(bind.op);
|
||||
const float level = s_forceOn ? s_forceLevel : (float)zone->damageLevel;
|
||||
// Change-gated, like the binary's watcher Perform (FUN_00457784 compares the
|
||||
// cached level against the live one and only then re-pushes the colours).
|
||||
if (log && fabs(level - draw_op->dzDamageLevel) > 0.001f)
|
||||
DEBUG_STREAM << "[armor] '" << draw_op->dzMatName << "' zone "
|
||||
<< bind.zone << " level " << draw_op->dzDamageLevel
|
||||
<< " -> " << level << " (brightness "
|
||||
<< (1.0f - (1.0f - kDamagedMaterialScale) * (level > 1.0f ? 1.0f : level))
|
||||
<< "x)\n" << std::flush;
|
||||
draw_op->dzDamageLevel = level;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Sim-side bridge (per-mech, every frame, from Mech::PerformAndWatch).
|
||||
//
|
||||
void BTArmourDamageTick(Entity *mech)
|
||||
{
|
||||
if (mech == NULL || application == NULL)
|
||||
return;
|
||||
BTL4VideoRenderer *renderer =
|
||||
(BTL4VideoRenderer *)application->GetVideoRenderer();
|
||||
if (renderer == NULL)
|
||||
return;
|
||||
renderer->TickArmourDamage(mech);
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Sim-side bridge (UpdateDeathState drives the sink each dead frame).
|
||||
//
|
||||
@@ -2673,8 +2883,21 @@ int
|
||||
}
|
||||
r->second->SetDrawObj(obj);
|
||||
render_tree.segGState[slot] = (int)gstate;
|
||||
// Keep the per-segment geometry record in step with what is actually drawn
|
||||
// (the armour-damage bindings and the #73 pick both key off it).
|
||||
if (obj != NULL && obj->GetIsShadow() == 0)
|
||||
{
|
||||
MechRenderTree::SegPick sp;
|
||||
sp.obj = obj;
|
||||
sp.zone = zone_index;
|
||||
render_tree.segPick[slot] = sp;
|
||||
}
|
||||
else
|
||||
render_tree.segPick.erase(slot);
|
||||
if (obj) ++shown; else ++hidden;
|
||||
}
|
||||
// This reloaded every segment mesh, so every armour-damage binding is stale.
|
||||
BindArmourDamage(viewpoint, render_tree);
|
||||
if (reinstall_paint)
|
||||
{
|
||||
TearDownMaterialSubstitutionList();
|
||||
|
||||
@@ -712,6 +712,14 @@ extern void BTDrawReticle(struct IDirect3DDevice9 *device);
|
||||
// (per-segment bounding spheres on the live posed skeleton).
|
||||
struct SegPick { d3d_OBJECT *obj; int zone; };
|
||||
std::map<int, SegPick> segPick; // segment idx -> pickable
|
||||
// ARMOUR DARKENING (issue #87): the resolved (object, damage zone)
|
||||
// bindings for this mech -- one entry per draw object that carries at
|
||||
// least one material listed in a zone's .DZM material list. Built once
|
||||
// with the tree (BindArmourDamage), replayed each frame by
|
||||
// TickArmourDamage. The binary's equivalent is the per-material
|
||||
// watcher list it builds in MakeMechRenderables (FUN_004573e4).
|
||||
struct DmgBind { d3d_OBJECT *obj; int op; int zone; };
|
||||
std::vector<DmgBind> dmgBinds;
|
||||
std::map<int, int> segGState; // slot -> last applied graphic state
|
||||
char paintSerno; // the %serno% this mech was BUILT with
|
||||
// (0 = none) -- ApplyViewSkeleton re-parses
|
||||
@@ -732,6 +740,18 @@ extern void BTDrawReticle(struct IDirect3DDevice9 *device);
|
||||
int
|
||||
TickWreck(Entity *victim, float dt);
|
||||
|
||||
//
|
||||
// ARMOUR DARKENING (issue #87). BindArmourDamage resolves the mech's
|
||||
// .DZM zone->material lists (DamageZone::GetMaterialList, already parsed
|
||||
// by MUNGA's DamageZone stream ctor) against the draw ops of the segment
|
||||
// geometry just loaded, recording one binding per match. TickArmourDamage
|
||||
// copies each bound zone's live damageLevel onto those ops.
|
||||
//
|
||||
void
|
||||
BindArmourDamage(Entity *entity, MechRenderTree &tree);
|
||||
void
|
||||
TickArmourDamage(Entity *entity);
|
||||
|
||||
//
|
||||
// VIEW TOGGLE: the player's mech builds BOTH cameras -- the authentic
|
||||
// cockpit eyepoint (DPLEyeRenderable at 'siteeyepoint', the pod's only
|
||||
|
||||
@@ -2379,6 +2379,17 @@ void
|
||||
if (deathHandler != 0)
|
||||
((MechDeathHandler *)deathHandler)->Tick();
|
||||
|
||||
// ARMOUR DARKENING (issue #87): push this mech's per-zone damageLevel onto the
|
||||
// materials that zone paints, so the panels darken toward 0.1x as they take
|
||||
// damage. The binary hung one watcher per (zone, material) off the mech's
|
||||
// renderable (FUN_004573e4, built in MakeMechRenderables) and let the renderer
|
||||
// Perform them; our equivalent update is driven from here for EVERY mech --
|
||||
// the enemy mech in external view is exactly the case players reported.
|
||||
{
|
||||
extern void BTArmourDamageTick(Entity *mech);
|
||||
BTArmourDamageTick((Entity *)this);
|
||||
}
|
||||
|
||||
// WAVE 7 Phase B: advance/render/impact the flying projectiles once per frame (viewpoint).
|
||||
if (isPlayerMech && dt > 0.0001f)
|
||||
BTUpdateProjectiles(dt);
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
# #87 PIXEL A/B: render the SAME standing scene twice -- every armour zone pinned
|
||||
# to level 0 (pristine) and level 1 (fully damaged) via BT_ARMOR_FORCE -- so the
|
||||
# only difference between the two frame sets is the darkening itself. No damage,
|
||||
# no smoke, no motion, no death to confound the comparison.
|
||||
set -x
|
||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
||||
cd /c/git/bt411/content || exit 1
|
||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
||||
sleep 2
|
||||
sed "s/^map=.*/map=grass/; s/^time=.*/time=day/" MP.EGG > ARMOR.EGG
|
||||
|
||||
run () { # run <level> <prefix>
|
||||
taskkill //F //IM btl4.exe > /dev/null 2>&1; sleep 2
|
||||
rm -f ${2}_*.png
|
||||
( bt_player_env
|
||||
unset BT_START_INSIDE # presence-only gate -- external view
|
||||
export BT_LOG=armor_ab_$2.log BT_AFFINITY=0x03
|
||||
export BT_ARMOR_FORCE=$1 BT_ARMOR_LOG=1
|
||||
export BT_SHOT_EVERY=400 BT_SHOT_PREFIX=$2
|
||||
"$BT_EXE" -egg ARMOR.EGG & )
|
||||
sleep 45
|
||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
||||
sleep 2
|
||||
}
|
||||
|
||||
run 0.0 abpristine
|
||||
run 1.0 abdamaged
|
||||
ls abpristine_*.png abdamaged_*.png 2>/dev/null | head
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
# #87 bench: MECH ARMOUR DARKENING.
|
||||
#
|
||||
# The 1995 renderer lerped every material a damage zone paints from its authored
|
||||
# colour toward 0.1x as that zone's damageLevel ran 0->1 (FUN_004573e4 built one
|
||||
# watcher per (zone, material) from the mech's .DZM list; FUN_00457784 re-pushed
|
||||
# the blend on every change). Our renderer swapped destroyed LIMB meshes but
|
||||
# never darkened a panel -- "the actual enemy mech in external view is not
|
||||
# showing darkened armor panels".
|
||||
#
|
||||
# Two legs:
|
||||
# self -- self-damage one named zone on the player's own mech; the cleanest
|
||||
# read of the level->brightness chain (deterministic, one zone).
|
||||
# enemy -- walk up to a spawned target and shoot it: the REPORTED case, an
|
||||
# enemy hull darkening under fire, with screenshots to pixel-verify.
|
||||
#
|
||||
# Verdict lines:
|
||||
# [armor] zone N '<dzname>' -> M draw op(s) .DZM list resolved onto geometry
|
||||
# [armor] '<mat>' zone N level a -> b (brightness kx) level reaching the ops
|
||||
#
|
||||
# usage: armor_darken.sh [self|enemy] [zone]
|
||||
set -x
|
||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
||||
cd /c/git/bt411/content || exit 1
|
||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
||||
sleep 2
|
||||
|
||||
LEG="${1:-self}"
|
||||
ZONE="${2:-dz_ltorso}"
|
||||
LOG=armor_${LEG}.log
|
||||
rm -f "$LOG" armorshot_*.tga
|
||||
|
||||
sed "s/^map=.*/map=grass/; s/^time=.*/time=day/" MP.EGG > ARMOR.EGG
|
||||
|
||||
if [ "$LEG" = "enemy" ]; then
|
||||
# The reported case: an ENEMY hull taking fire. Beeline to the spawned target
|
||||
# and hold the trigger; BT_SHOT_EVERY dumps backbuffer frames so the panels can
|
||||
# be compared pixel-wise before/after.
|
||||
BT_ARMOR_LOG=1 BT_DMG_LOG=1 \
|
||||
BT_SPAWN_ENEMY=1 BT_GOTO=enemy BT_GOTO_STOP=2 BT_AUTOFIRE=1 \
|
||||
BT_SHOT_EVERY=300 BT_SHOT_PREFIX=armorshot \
|
||||
bt_launch "$LOG" ARMOR.EGG 0x03
|
||||
elif [ "$LEG" = "ext" ]; then
|
||||
# PIXEL leg. Same single-zone ramp, but in the EXTERNAL chase camera so the
|
||||
# hull fills the frame and one zone can be compared before/after.
|
||||
#
|
||||
# NOTE: this deliberately overrides the shipped player env's BT_START_INSIDE=1
|
||||
# (bench_common) -- players start in the cockpit and cannot see their own hull.
|
||||
# This leg exists ONLY to photograph the panels; the `self` and `enemy` legs
|
||||
# are the player-authentic ones.
|
||||
( bt_player_env
|
||||
# presence-only gate (btl4vid.cpp:657) -- "=0" still reads as set, so UNSET it
|
||||
unset BT_START_INSIDE
|
||||
export BT_LOG="$LOG" BT_AFFINITY=0x03
|
||||
export BT_ARMOR_LOG=1 BT_DMG_LOG=1
|
||||
export BT_SELF_DAMAGE=3 BT_SELF_DAMAGE_ZONE="$ZONE"
|
||||
export BT_SHOT_EVERY=600 BT_SHOT_PREFIX=armorext
|
||||
"$BT_EXE" -egg ARMOR.EGG & )
|
||||
else
|
||||
# Deterministic single-zone ramp on our own mech. Small dose so the level
|
||||
# climbs in steps instead of saturating in one tick.
|
||||
BT_ARMOR_LOG=1 BT_DMG_LOG=1 \
|
||||
BT_SELF_DAMAGE=3 BT_SELF_DAMAGE_ZONE="$ZONE" \
|
||||
bt_launch "$LOG" ARMOR.EGG 0x03
|
||||
fi
|
||||
|
||||
sleep 90
|
||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
||||
sleep 2
|
||||
|
||||
echo "=== per-entity binding totals ==="
|
||||
grep -E "^\[armor\] entity" "$LOG" | sort -u
|
||||
echo "=== level changes ==="
|
||||
grep -E "^\[armor\] '" "$LOG" | head -30
|
||||
echo "=== distinct materials darkened ==="
|
||||
grep -E "^\[armor\] '" "$LOG" | sed "s/.*\['armor'\] //" | awk '{print $2}' | sort -u | head -20
|
||||
echo "=== shots ==="
|
||||
ls armorshot_*.tga 2>/dev/null | head
|
||||
@@ -0,0 +1,57 @@
|
||||
"""Raw-image xref sweep (gotcha #21: the decomp export has gaps).
|
||||
|
||||
Finds every CALL/JMP rel32 in .text whose target is one of the VAs given on the
|
||||
command line, plus any absolute immediate referencing them.
|
||||
|
||||
python xref.py 0x4902b0 0x490308
|
||||
"""
|
||||
import struct, sys
|
||||
|
||||
PATH = r"C:\git\bt411\content\BTL4OPT.EXE"
|
||||
data = open(PATH, "rb").read()
|
||||
e_lfanew = struct.unpack_from("<I", data, 0x3C)[0]
|
||||
coff = e_lfanew + 4
|
||||
num_sec = struct.unpack_from("<H", data, coff + 2)[0]
|
||||
opt_size = struct.unpack_from("<H", data, coff + 16)[0]
|
||||
opt = coff + 20
|
||||
image_base = struct.unpack_from("<I", data, opt + 28)[0]
|
||||
sec_tbl = opt + opt_size
|
||||
secs = []
|
||||
for i in range(num_sec):
|
||||
off = sec_tbl + i * 40
|
||||
name = data[off:off + 8].rstrip(b"\0").decode("latin1")
|
||||
vsize, va, rawsize, rawptr = struct.unpack_from("<IIII", data, off + 8)
|
||||
secs.append((name, va, vsize, rawptr, rawsize))
|
||||
|
||||
|
||||
def off_to_va(off):
|
||||
for name, va, vsize, rawptr, rawsize in secs:
|
||||
if rawptr <= off < rawptr + rawsize:
|
||||
return image_base + va + (off - rawptr)
|
||||
return None
|
||||
|
||||
|
||||
targets = set(int(a, 16) for a in sys.argv[1:])
|
||||
hits = []
|
||||
|
||||
for name, va, vsize, rawptr, rawsize in secs:
|
||||
blob = data[rawptr:rawptr + rawsize]
|
||||
base_va = image_base + va
|
||||
# rel32 CALL (E8) / JMP (E9)
|
||||
for i in range(len(blob) - 5):
|
||||
op = blob[i]
|
||||
if op not in (0xE8, 0xE9):
|
||||
continue
|
||||
rel = struct.unpack_from("<i", blob, i + 1)[0]
|
||||
tgt = base_va + i + 5 + rel
|
||||
if tgt in targets:
|
||||
hits.append((base_va + i, "call" if op == 0xE8 else "jmp", tgt, name))
|
||||
# absolute immediates (vtable slots, push offset)
|
||||
for i in range(len(blob) - 4):
|
||||
v = struct.unpack_from("<I", blob, i)[0]
|
||||
if v in targets:
|
||||
hits.append((base_va + i, "abs32", v, name))
|
||||
|
||||
for addr, kind, tgt, sec in sorted(hits):
|
||||
print("%08x %-6s -> %08x [%s]" % (addr, kind, tgt, sec))
|
||||
print("\n%d xrefs" % len(hits))
|
||||
Reference in New Issue
Block a user