Fire VISUALS wave: the authored firesmoke sheet, vertex-alpha effect cards, the case-4 wreck dressing
The "fireballs like the demo vids" arc, decomp/content-grounded end to end,
plus the live-play UX batch verified over the same sessions:
FIRESMOKE SHEET (the PFX fireball fix): every firesmokeN_scr_tex in BTFX.VMF
maps the SAME 64x64 tileable noise image bintA (variants differ only in SCROLL
rate) and firesmoke1_mtl colours it through the "fiery" ramp (0.3,0.1,0.1)->
(0.9,0.7,0.3). The particle layer now bakes ramp(lum(bintA)) as its sprite
colour (noise detail in alpha) and SCROLLS it at firesmoke1's authored rate
via a texture-transform; the port's radial soft-edge mask moved to a second
CLAMPed stage so the WRAPPED scroll rolls flame through the sprite without
scrolling the edge away. Old grit x radial bake kept as the no-BINTA fallback.
Impact hits, damage-band smoke and death booms all ride this layer.
AUTHORED TEXTURE SCROLL in the model path: the BMF TEXTURE records carry
SPECIAL " SCROLL u0 v0 du dv" (tag 0x2037); the draw path always supported
per-op scrolling (SetTextureScrolling) but the BGF loader never parsed it, so
every scrolling material rendered frozen. Wired TexRef -> MatInfo -> batch ->
L4TEXOP.doScroll: the flame cards (flamebig/fire5) now roll fire noise.
VERTEX-ALPHA EFFECT CARDS (the "twisted drill bit of fire" fix): FLAMEBIG's
verts carry authored float RGBA -- white-hot base (1.0,0.99,0.97) -> dark-red
tip fading to alpha -0.2 (the DPL clamp convention). The loader kept a flat
batch colour and drew it OPAQUE = a solid orange spike. Corpus sweep: exactly
14 shipped BGFs carry vertex alpha, ALL effect cards (flames, MUZFLASH,
EXDISK_A/B/C, TMST_A/B/C, beam models, DECLOUDS). Such batches now keep the
authored per-vertex gradient and route to the alpha-blend pass, unlit,
colour = texture x gradient, alpha = the vertex fade; sky objects excluded
(drawAsSky + alphaTest passes NEITHER pass filter -- DECLOUDS stays in the
sky pass). MUZFLASH/EXDISK render correctly for free when the muzzle-model
work lands.
WRECK DRESSING (the 1996 ExplosionScripts case-4 transcription): pieces spawn
HIDDEN and reveal 0.25s after the boom (the InstanceSwitch delay, behind the
dnboom flash); flamebig hangs over the pile, Y-BILLBOARDED at the camera
(SetOffsetYaw + a camera-pos getter -- the dpl_SetDCSReorientAxes analog);
the MakeDCSFall settle arms at the reveal with the two authored rates (hulk/
debris -0.025 t^2, fires -0.01 t^2 -- the flames ride above the sinking pile
and die with it at burial). EMPTY-PLACEHOLDER hulk guard: THRDBR.BGF is a
153-byte zero-geometry stub that "loads fine" -- vertex-count check now routes
it to the gendbr fallback (a Thor wreck was invisible). Hulk content census
recorded: AVADBR==MADDBR==VULDBR geometry (palette-only prefix diffs),
RAPDBR==SNDDBR==STIDBR byte-identical -- wreck variety is materials + the
dressing, not unique piles.
LIVE-PLAY BATCH: muzzle resolve uses the named segmentIndex (raw +0xdc read
was layout garbage); forward launch frame (authored MuzzleVelocity +Z vs the
mech's -Z facing); dock-bottom single window (gauge strip appended below the
world viewport, 1100x600 default, BT_DEV_GAUGES_WINDOW=1 restores the separate
window); portrait sec surface unrotated CW; ammo counters live via typed
bridges (BTAmmoBinCountPtr/BTAmmoBinFeeding/BTWeaponAmmoBin -- raw bin+0x180
and a hand-rolled link walk were garbage); fourth fire key ('4' = Pinky);
panel/arc probes de-aliased (%61 prime).
KB: rendering.md (vertex-alpha card family + scroll), combat-damage.md (hulk
census + THRDBR stub), gauges-hud.md (ammo bridges).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
bb795e2805
commit
48c9c8444f
@@ -292,6 +292,19 @@ Full decomp map of the death path. Two tiers:
|
|||||||
`<prefix>dbr.bgf` on the tree root (gendbr fallback; pending-swap if death precedes tree build).
|
`<prefix>dbr.bgf` on the tree root (gendbr fallback; pending-swap if death precedes tree build).
|
||||||
Mesh flames + the settle motion are noted follow-ups; burning reads via 1001/1007 + the wreck-smoke
|
Mesh flames + the settle motion are noted follow-ups; burning reads via 1001/1007 + the wreck-smoke
|
||||||
re-arm. Verified: `[BTrender] wreck swap: victim -> 'blhdbr.bgf'`. [T2]
|
re-arm. Verified: `[BTrender] wreck swap: victim -> 'blhdbr.bgf'`. [T2]
|
||||||
|
**HULK CONTENT CENSUS (2026-07-12) [T1, byte-diffed]:** the shipped `*DBR.BGF` set shares
|
||||||
|
geometry heavily — `AVADBR`/`MADDBR`/`VULDBR` are byte-identical EXCEPT the material-library
|
||||||
|
prefix (`avafx:`/`madfx:`/`vulfx:` — same shape, per-mech palette); `RAPDBR`/`SNDDBR`/`STIDBR`
|
||||||
|
are fully byte-identical (same md5). Distinct shapes: BLH (1537v), LOK, OWN, GEN (659v), the
|
||||||
|
AVA/MAD/VUL shape (1351v), the RAP/SND/STI shape, `MDBR` (90KB), `LDBR` (46v scatter). All are
|
||||||
|
the same authored style (gray `genNmedgry`/`blakskn` + orange `dam3orng` slump piles), so two
|
||||||
|
different mechs' wrecks legitimately read alike; the original's visual differentiation came
|
||||||
|
from the case-4 DRESSING (flamesml/flamebig flames + chunk/fireball stagger + the MakeDCSFall
|
||||||
|
torso-height collapse), which is still a follow-up. ⚠ `THRDBR.BGF` is a 153-byte EMPTY
|
||||||
|
placeholder (one zero-geometry patch, `btfx:sparkred_mtl`) — it loads "successfully" as an
|
||||||
|
invisible object, so the gendbr fallback (which only fires on load FAILURE) never triggers
|
||||||
|
for a Thor: a Thor wreck would show only ldbr debris. [T1 content; T4 whether the pod binary
|
||||||
|
special-cased it]
|
||||||
⚠ **The death-effects DISPATCH lives at the VICTIM's death transition** (`UpdateDeathState`'s
|
⚠ **The death-effects DISPATCH lives at the VICTIM's death transition** (`UpdateDeathState`'s
|
||||||
movementMode→9 moment, task #42) — NOT in any shooter's fire path. It originally sat in the
|
movementMode→9 moment, task #42) — NOT in any shooter's fire path. It originally sat in the
|
||||||
laser fire block's kill check, which a MISSILE killing blow (landing frames later in
|
laser fire block's kill check, which a MISSILE killing blow (landing frames later in
|
||||||
|
|||||||
@@ -100,6 +100,17 @@ locked**. **RESOLVED 2026-07-12 [T2]:** deaths now tally per node from LOCALLY O
|
|||||||
own VehicleDead path counts its node), and `BTPilotDeaths` clamps the −2/−1 pre-acquire seed
|
own VehicleDead path counts its node), and `BTPilotDeaths` clamps the −2/−1 pre-acquire seed
|
||||||
to 0 for display. Own row counts correctly as before.
|
to 0 for display. Own row counts correctly as before.
|
||||||
|
|
||||||
|
## Launcher-panel recharge dial — authentically STATIC [T1, writer census]
|
||||||
|
The weapon panels' SegmentArc270 tick ring reads MechWeapon::rechargeLevel (+0x320). A FULL
|
||||||
|
writer census of the binary (all decomp spellings: `0x320) =`, `+ 800) =`, `[200] =`) finds
|
||||||
|
exactly four writers in the weapon family: the ctor (@004b99a8, 1.0), the stream init
|
||||||
|
(@004b8fec), ResetToInitialState (@004b96ec, 1.0), and the ONE live writer —
|
||||||
|
`Emitter::ComputeOutputVoltage` @004ba738, reached only through the Emitter vtable. Projectile
|
||||||
|
weapons (MissileLauncher/autocannon) never write it: their dial draws full ONCE and never moves
|
||||||
|
on the pod. The launcher panel's live indicators are the AMMO DIGITS (AmmoBin::ammoCount via
|
||||||
|
the complete-type bridges, 2026-07-12 — the old raw bin+0x180/0x18c reads were layout garbage),
|
||||||
|
the jam/fire lamps and the eject wipe. Do NOT "fix" the static ring — it is the authentic look.
|
||||||
|
|
||||||
## ConfigMapGauge (the weapon panel's regroup lamp column) — authentically DORMANT
|
## ConfigMapGauge (the weapon panel's regroup lamp column) — authentically DORMANT
|
||||||
The per-weapon btjoy.pcc joystick image + 4 cm_* state lamps (off/other/only/both) showing,
|
The per-weapon btjoy.pcc joystick image + 4 cm_* state lamps (off/other/only/both) showing,
|
||||||
for each mappable fire button (Pinky/ThumbLow/Trigger/ThumbHigh), whether THIS panel's weapon
|
for each mappable fire button (Pinky/ThumbLow/Trigger/ThumbHigh), whether THIS panel's weapon
|
||||||
|
|||||||
@@ -25,6 +25,18 @@ white blobs" bug (gate on `hasNormals`). [T2]
|
|||||||
Exception: cockpit-frame (`*_cop`) batches are pure-emissive constant colour (0.13,0.12,0.15),
|
Exception: cockpit-frame (`*_cop`) batches are pure-emissive constant colour (0.13,0.12,0.15),
|
||||||
`BT_COP_FRAME` overrides — see [[cockpit-view]] (task #55; the old `BT_COP_RAMP_L` ramp-index
|
`BT_COP_FRAME` overrides — see [[cockpit-view]] (task #55; the old `BT_COP_RAMP_L` ramp-index
|
||||||
approach is retired). [T2]
|
approach is retired). [T2]
|
||||||
|
**VERTEX-ALPHA EFFECT CARDS (2026-07-12) [T1 corpus-swept]:** exactly 14 shipped BGFs carry
|
||||||
|
per-vertex float RGBA with alpha < 1 (tags 0x0082/0x008A, RGBA at +12, values OUTSIDE [0,1] —
|
||||||
|
clamp): FLAMEBIG/FIRE5 (flames), MUZFLASH, EXDISK_A/B/C, TMST_A/B/C, BLUEBEAM/GREEBEAM/RED_BEAM/
|
||||||
|
PPC, DECLOUDS — the EFFECT-CARD family. Their authored gradient (white-hot base → dark tip fading
|
||||||
|
to alpha −0.2) IS the shading: drawn opaque with a flat batch colour they render as solid shapes
|
||||||
|
(the wreck flame read as a "twisted drill bit"). The loader now keeps the authored per-vertex
|
||||||
|
RGBA for any batch with vertex alpha < 1 (`BgfDrawBatch.vertexAlpha`) and L4D3D routes it to the
|
||||||
|
ALPHA-BLEND pass, unlit, colour = texture × gradient, alpha = vertex fade (SRCALPHA/INVSRCALPHA).
|
||||||
|
SKY objects are excluded from the routing (drawAsSky + alphaTest passes NEITHER pass filter —
|
||||||
|
DECLOUDS must stay in the sky pass). Authored TEXTURE **SCROLL** (BMF TEXTURE tag 0x2037
|
||||||
|
`" SCROLL u0 v0 du dv"`, e.g. firesmoke1 = 0.05,−0.331 uv/s) is now parsed and propagated to
|
||||||
|
`L4TEXOP::doScroll` — scrolling materials (firesmoke flame cards) animate in the model path. [T2]
|
||||||
|
|
||||||
## The big render-fidelity fixes (dbase/desert matched to pod footage)
|
## The big render-fidelity fixes (dbase/desert matched to pod footage)
|
||||||
- **Geometry:** CONN is a flat trilist (not fan); CONN+PCONN coexist — see [[bgf-format]]. Malformed
|
- **Geometry:** CONN is a flat trilist (not fan); CONN+PCONN coexist — see [[bgf-format]]. Malformed
|
||||||
|
|||||||
@@ -423,7 +423,14 @@ d3d_OBJECT* d3d_OBJECT::LoadObjectBGF(LPDIRECT3DDEVICE9 device, char *fileName)
|
|||||||
m.Diffuse.a = 1.0f;
|
m.Diffuse.a = 1.0f;
|
||||||
m.Ambient = m.Diffuse; // matches the .x path; lit by scene ambient even w/o direct lights
|
m.Ambient = m.Diffuse; // matches the .x path; lit by scene ambient even w/o direct lights
|
||||||
|
|
||||||
object->mDrawOps[i].alphaTest = object->mIsShadow != 0; // shadow -> blend pass
|
// shadow -> blend pass; vertex-alpha effect cards (flame fans) too.
|
||||||
|
// SKY objects are excluded: a drawAsSky op with alphaTest set passes
|
||||||
|
// NEITHER pass filter (sky pass rejects alphaTest, blend pass rejects
|
||||||
|
// drawAsSky) and vanishes -- DECLOUDS carries vertex alpha but must
|
||||||
|
// stay in the sky pass.
|
||||||
|
const bool vtxAlphaOp = data.batches[i].vertexAlpha && !isSkyObj;
|
||||||
|
object->mDrawOps[i].alphaTest = (object->mIsShadow != 0) || vtxAlphaOp;
|
||||||
|
object->mDrawOps[i].vertexAlphaBlend = vtxAlphaOp;
|
||||||
object->mDrawOps[i].drawAsDecal = false;
|
object->mDrawOps[i].drawAsDecal = false;
|
||||||
object->mDrawOps[i].drawAsSky = isSkyObj; // sky dome -> PASS_SKY (fullbright)
|
object->mDrawOps[i].drawAsSky = isSkyObj; // sky dome -> PASS_SKY (fullbright)
|
||||||
// PUNCH (dpl_Punchize; BT_PUNCH=0 disables): cutout batch -- black texels
|
// PUNCH (dpl_Punchize; BT_PUNCH=0 disables): cutout batch -- black texels
|
||||||
@@ -435,7 +442,12 @@ d3d_OBJECT* d3d_OBJECT::LoadObjectBGF(LPDIRECT3DDEVICE9 device, char *fileName)
|
|||||||
object->mDrawOps[i].texture.texture = NULL;
|
object->mDrawOps[i].texture.texture = NULL;
|
||||||
object->mDrawOps[i].texture.wrap_u = L4TEXOP::REPEAT;
|
object->mDrawOps[i].texture.wrap_u = L4TEXOP::REPEAT;
|
||||||
object->mDrawOps[i].texture.wrap_v = L4TEXOP::REPEAT;
|
object->mDrawOps[i].texture.wrap_v = L4TEXOP::REPEAT;
|
||||||
object->mDrawOps[i].texture.doScroll = false;
|
// Authored TEXTURE SCROLL (BMF SPECIAL " SCROLL u0 v0 du dv"): the
|
||||||
|
// firesmoke family (flamebig/fire5 flame cards) rolls its fire noise.
|
||||||
|
// SetTextureScrolling in the draw path honours these per-op.
|
||||||
|
object->mDrawOps[i].texture.doScroll = data.batches[i].texScroll;
|
||||||
|
object->mDrawOps[i].texture.scrollUDelta = data.batches[i].texScrollU;
|
||||||
|
object->mDrawOps[i].texture.scrollVDelta = data.batches[i].texScrollV;
|
||||||
|
|
||||||
const std::string &tp = data.batches[i].texPath;
|
const std::string &tp = data.batches[i].texPath;
|
||||||
if (!tp.empty())
|
if (!tp.empty())
|
||||||
@@ -863,6 +875,15 @@ void d3d_OBJECT::SetCameraPosition(float x, float y, float z)
|
|||||||
gCamPos.x = x; gCamPos.y = y; gCamPos.z = z;
|
gCamPos.x = x; gCamPos.y = y; gCamPos.z = z;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The camera's world position (set once per frame above) -- the wreck flame
|
||||||
|
// billboard (dpl_SetDCSReorientAxes analog) yaws its card toward this.
|
||||||
|
void d3d_OBJECT::GetCameraPosition(float *x, float *y, float *z)
|
||||||
|
{
|
||||||
|
if (x) *x = gCamPos.x;
|
||||||
|
if (y) *y = gCamPos.y;
|
||||||
|
if (z) *z = gCamPos.z;
|
||||||
|
}
|
||||||
|
|
||||||
void d3d_OBJECT::SetCullFrustum(const D3DXMATRIX *viewProj)
|
void d3d_OBJECT::SetCullFrustum(const D3DXMATRIX *viewProj)
|
||||||
{
|
{
|
||||||
if (viewProj == NULL) { gCullValid = 0; return; }
|
if (viewProj == NULL) { gCullValid = 0; return; }
|
||||||
@@ -1203,6 +1224,34 @@ void d3d_OBJECT::DrawMesh(int pass, const D3DXMATRIX *viewTransform, Time target
|
|||||||
// 3. re-draw the mask writing stencil 0 (clean up for other punch groups)
|
// 3. re-draw the mask writing stencil 0 (clean up for other punch groups)
|
||||||
// The stencil never touches z, so entities drawn later still show through
|
// The stencil never touches z, so entities drawn later still show through
|
||||||
// the apertures correctly.
|
// the apertures correctly.
|
||||||
|
// VERTEX-ALPHA effect card (the FLAMEBIG flame fan): unlit -- the
|
||||||
|
// authored per-vertex gradient (white-hot base -> dark faded tip) IS
|
||||||
|
// the shading -- colour = texture x gradient, alpha = the vertex fade
|
||||||
|
// (SRCALPHA/INVSRCALPHA). Runs only in the blend pass (the op filter
|
||||||
|
// above routed it there; ALPHABLEND is on and z-write off pass-wide).
|
||||||
|
DWORD sVLight = 0, sVSrc = 0, sVDst = 0, sVCop = 0, sVCa1 = 0, sVCa2 = 0, sVAop = 0, sVAa2 = 0;
|
||||||
|
const bool vtxAlphaCard = (drawOp->vertexAlphaBlend && pass == PASS_ALPHABLEND);
|
||||||
|
if (vtxAlphaCard)
|
||||||
|
{
|
||||||
|
mDevice->GetRenderState(D3DRS_LIGHTING, &sVLight);
|
||||||
|
mDevice->GetRenderState(D3DRS_SRCBLEND, &sVSrc);
|
||||||
|
mDevice->GetRenderState(D3DRS_DESTBLEND, &sVDst);
|
||||||
|
mDevice->GetTextureStageState(0, D3DTSS_COLOROP, &sVCop);
|
||||||
|
mDevice->GetTextureStageState(0, D3DTSS_COLORARG1, &sVCa1);
|
||||||
|
mDevice->GetTextureStageState(0, D3DTSS_COLORARG2, &sVCa2);
|
||||||
|
mDevice->GetTextureStageState(0, D3DTSS_ALPHAOP, &sVAop);
|
||||||
|
mDevice->GetTextureStageState(0, D3DTSS_ALPHAARG2, &sVAa2);
|
||||||
|
mDevice->SetRenderState(D3DRS_LIGHTING, FALSE);
|
||||||
|
mDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
|
||||||
|
mDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
|
||||||
|
mDevice->SetTextureStageState(0, D3DTSS_COLOROP,
|
||||||
|
drawOp->texture.texture != NULL ? D3DTOP_MODULATE : D3DTOP_SELECTARG2);
|
||||||
|
mDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
|
||||||
|
mDevice->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);
|
||||||
|
mDevice->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG2);
|
||||||
|
mDevice->SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_DIFFUSE);
|
||||||
|
}
|
||||||
|
|
||||||
const bool copCut = (drawOp->copRole == 2 && iOp > 0
|
const bool copCut = (drawOp->copRole == 2 && iOp > 0
|
||||||
&& mDrawOps[iOp - 1].copRole == 1
|
&& mDrawOps[iOp - 1].copRole == 1
|
||||||
&& mDrawOps[iOp - 1].bgfPrimCount > 0
|
&& mDrawOps[iOp - 1].bgfPrimCount > 0
|
||||||
@@ -1266,6 +1315,17 @@ void d3d_OBJECT::DrawMesh(int pass, const D3DXMATRIX *viewTransform, Time target
|
|||||||
mDevice->SetRenderState(D3DRS_ALPHAREF, sARef);
|
mDevice->SetRenderState(D3DRS_ALPHAREF, sARef);
|
||||||
mDevice->SetRenderState(D3DRS_ALPHAFUNC, sAFunc);
|
mDevice->SetRenderState(D3DRS_ALPHAFUNC, sAFunc);
|
||||||
}
|
}
|
||||||
|
if (vtxAlphaCard)
|
||||||
|
{
|
||||||
|
mDevice->SetRenderState(D3DRS_LIGHTING, sVLight);
|
||||||
|
mDevice->SetRenderState(D3DRS_SRCBLEND, sVSrc);
|
||||||
|
mDevice->SetRenderState(D3DRS_DESTBLEND, sVDst);
|
||||||
|
mDevice->SetTextureStageState(0, D3DTSS_COLOROP, sVCop);
|
||||||
|
mDevice->SetTextureStageState(0, D3DTSS_COLORARG1, sVCa1);
|
||||||
|
mDevice->SetTextureStageState(0, D3DTSS_COLORARG2, sVCa2);
|
||||||
|
mDevice->SetTextureStageState(0, D3DTSS_ALPHAOP, sVAop);
|
||||||
|
mDevice->SetTextureStageState(0, D3DTSS_ALPHAARG2, sVAa2);
|
||||||
|
}
|
||||||
if (drawOp->lodDepthBias != 0.0f)
|
if (drawOp->lodDepthBias != 0.0f)
|
||||||
mDevice->SetRenderState(D3DRS_DEPTHBIAS, sDB);
|
mDevice->SetRenderState(D3DRS_DEPTHBIAS, sDB);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,13 +109,48 @@ static bool DevGaugeComposite()
|
|||||||
return v != 0;
|
return v != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DOCK-BOTTOM (2026-07-12, single-window mode): the gauge strip is APPENDED to
|
||||||
|
// the main window's bottom band -- the world renders in the top region via a
|
||||||
|
// restricted viewport, the strip in the remainder, everything scales with the
|
||||||
|
// window. Enabled by btl4main when BT_DEV_GAUGES is set (BT_DEV_GAUGES_WINDOW=1
|
||||||
|
// restores the separate-window mode). Strip height = width x (480/1320), the
|
||||||
|
// panel's design aspect.
|
||||||
|
int gBTGaugeDockBottom = 0;
|
||||||
|
static const float kBTDockStripRatio = 480.0f / 1320.0f;
|
||||||
|
|
||||||
|
// The strip's height for a given window width: proportional to width at the
|
||||||
|
// panel's design aspect, CAPPED at the design height (480) -- a very wide
|
||||||
|
// window renders the strip at native 1:1 instead of upscaling it.
|
||||||
|
int BTGaugeStripHeightFor(int width)
|
||||||
|
{
|
||||||
|
int h = (int)((float)width * kBTDockStripRatio);
|
||||||
|
return (h > 480) ? 480 : h;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restrict rasterization to the WORLD region (target minus the bottom strip).
|
||||||
|
// Called by the main renderer right after BeginScene; no-op unless dock-bottom.
|
||||||
|
void BTApplyWorldViewport(LPDIRECT3DDEVICE9 device)
|
||||||
|
{
|
||||||
|
if (!gBTGaugeDockBottom || device == NULL) return;
|
||||||
|
IDirect3DSurface9 *rt = NULL;
|
||||||
|
if (FAILED(device->GetRenderTarget(0, &rt)) || rt == NULL) return;
|
||||||
|
D3DSURFACE_DESC d;
|
||||||
|
rt->GetDesc(&d);
|
||||||
|
rt->Release();
|
||||||
|
DWORD stripH = (DWORD)BTGaugeStripHeightFor((int)d.Width);
|
||||||
|
if (stripH == 0 || stripH >= d.Height) return;
|
||||||
|
D3DVIEWPORT9 vp = { 0, 0, d.Width, d.Height - stripH, 0.0f, 1.0f };
|
||||||
|
device->SetViewport(&vp);
|
||||||
|
}
|
||||||
|
|
||||||
// DEV-COMPOSITE: BT_DEV_GAUGES_DOCK=1 docks the 6-surface panel INTO the main 800x600
|
// DEV-COMPOSITE: BT_DEV_GAUGES_DOCK=1 docks the 6-surface panel INTO the main 800x600
|
||||||
// window (occludes the 3D view); default (unset) presents them in a SEPARATE window.
|
// window (occludes the 3D view); dock-bottom (above) also counts as docked --
|
||||||
|
// both suppress the separate gauge window and draw via the main-window inset.
|
||||||
static bool DevGaugeDocked()
|
static bool DevGaugeDocked()
|
||||||
{
|
{
|
||||||
static int v = -1;
|
static int v = -1;
|
||||||
if (v < 0) v = (getenv("BT_DEV_GAUGES_DOCK") != NULL) ? 1 : 0;
|
if (v < 0) v = (getenv("BT_DEV_GAUGES_DOCK") != NULL) ? 1 : 0;
|
||||||
return v != 0;
|
return v != 0 || gBTGaugeDockBottom != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
//===========================================================================//
|
//===========================================================================//
|
||||||
@@ -285,6 +320,22 @@ void BTDrawGaugeSurfaces(LPDIRECT3DDEVICE9 device, float px, float py, float pw,
|
|||||||
IDirect3DStateBlock9 *stateBlock = NULL;
|
IDirect3DStateBlock9 *stateBlock = NULL;
|
||||||
device->CreateStateBlock(D3DSBT_ALL, &stateBlock);
|
device->CreateStateBlock(D3DSBT_ALL, &stateBlock);
|
||||||
|
|
||||||
|
// DOCK-BOTTOM: the world pass restricted the viewport to the top region;
|
||||||
|
// the strip's pretransformed quads land BELOW it and would be clipped.
|
||||||
|
// Draw with the FULL target viewport (the captured state block restores
|
||||||
|
// the world viewport afterwards).
|
||||||
|
{
|
||||||
|
IDirect3DSurface9 *rt = NULL;
|
||||||
|
if (SUCCEEDED(device->GetRenderTarget(0, &rt)) && rt != NULL)
|
||||||
|
{
|
||||||
|
D3DSURFACE_DESC d;
|
||||||
|
rt->GetDesc(&d);
|
||||||
|
rt->Release();
|
||||||
|
D3DVIEWPORT9 vp = { 0, 0, d.Width, d.Height, 0.0f, 1.0f };
|
||||||
|
device->SetViewport(&vp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Reach the shared display once (any present port shares the same SVGA16/pixelBuffer).
|
// Reach the shared display once (any present port shares the same SVGA16/pixelBuffer).
|
||||||
SVGA16 *svga = NULL;
|
SVGA16 *svga = NULL;
|
||||||
for (int i = 0; i < 6 && svga == NULL; i++)
|
for (int i = 0; i < 6 && svga == NULL; i++)
|
||||||
@@ -335,8 +386,23 @@ void BTDrawGaugeSurfaces(LPDIRECT3DDEVICE9 device, float px, float py, float pw,
|
|||||||
void BTDrawGaugeInset(LPDIRECT3DDEVICE9 device)
|
void BTDrawGaugeInset(LPDIRECT3DDEVICE9 device)
|
||||||
{
|
{
|
||||||
if (!DevGaugeComposite() || !DevGaugeDocked() || device == NULL) return;
|
if (!DevGaugeComposite() || !DevGaugeDocked() || device == NULL) return;
|
||||||
// DOCKED mode only: draw into the main backbuffer (this runs before EndScene).
|
if (gBTGaugeDockBottom)
|
||||||
// Keep the new panel's 1320:480 aspect (2.75) so nothing distorts.
|
{
|
||||||
|
// DOCK-BOTTOM: the strip owns the bottom band of the target (the world
|
||||||
|
// viewport excludes it) -- full width, design-aspect height.
|
||||||
|
IDirect3DSurface9 *rt = NULL;
|
||||||
|
if (FAILED(device->GetRenderTarget(0, &rt)) || rt == NULL) return;
|
||||||
|
D3DSURFACE_DESC d;
|
||||||
|
rt->GetDesc(&d);
|
||||||
|
rt->Release();
|
||||||
|
float stripH = (float)BTGaugeStripHeightFor((int)d.Width);
|
||||||
|
if (stripH <= 0.0f || stripH >= (float)d.Height) return;
|
||||||
|
BTDrawGaugeSurfaces(device, 0.0f, (float)d.Height - stripH,
|
||||||
|
(float)d.Width, stripH);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Legacy overlay dock (BT_DEV_GAUGES_DOCK): fixed inset over the 3D view.
|
||||||
|
// Keep the panel's 1320:480 aspect (2.75) so nothing distorts.
|
||||||
BTDrawGaugeSurfaces(device, 2.0f, 400.0f, 540.0f, 540.0f / 2.75f);
|
BTDrawGaugeSurfaces(device, 2.0f, 400.0f, 540.0f, 540.0f / 2.75f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+206
-51
@@ -20,6 +20,7 @@
|
|||||||
#include "l4particles.h"
|
#include "l4particles.h"
|
||||||
#include "DXUtils.h"
|
#include "DXUtils.h"
|
||||||
#include "bgfload.h" // BT: SetVideoPathPriority (day/night path priority)
|
#include "bgfload.h" // BT: SetVideoPathPriority (day/night path priority)
|
||||||
|
#include "image.h" // BT: decodeImage (the authored firesmoke sheet bintA)
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
#include <conio.h>
|
#include <conio.h>
|
||||||
@@ -936,6 +937,9 @@ void BTDrawPfx(LPDIRECT3DDEVICE9 dev, const D3DXMATRIX *view, float dt)
|
|||||||
dev->GetTextureStageState(0, D3DTSS_ALPHAOP, &sAop);
|
dev->GetTextureStageState(0, D3DTSS_ALPHAOP, &sAop);
|
||||||
dev->GetTextureStageState(0, D3DTSS_ALPHAARG1, &sAa1);
|
dev->GetTextureStageState(0, D3DTSS_ALPHAARG1, &sAa1);
|
||||||
dev->GetTextureStageState(0, D3DTSS_ALPHAARG2, &sAa2);
|
dev->GetTextureStageState(0, D3DTSS_ALPHAARG2, &sAa2);
|
||||||
|
DWORD sAdrU, sAdrV; // the scroll WRAPs sampler 0
|
||||||
|
dev->GetSamplerState(0, D3DSAMP_ADDRESSU, &sAdrU);
|
||||||
|
dev->GetSamplerState(0, D3DSAMP_ADDRESSV, &sAdrV);
|
||||||
|
|
||||||
// PREMULTIPLIED blending (ONE, INVSRCALPHA): the one model that renders BOTH
|
// PREMULTIPLIED blending (ONE, INVSRCALPHA): the one model that renders BOTH
|
||||||
// authored particle families correctly. FIRE (bright colour, the additive
|
// authored particle families correctly. FIRE (bright colour, the additive
|
||||||
@@ -959,43 +963,62 @@ void BTDrawPfx(LPDIRECT3DDEVICE9 dev, const D3DXMATRIX *view, float dt)
|
|||||||
dev->SetRenderState(D3DRS_DESTBLEND, s_pfxAdditive ? D3DBLEND_ONE : D3DBLEND_INVSRCALPHA);
|
dev->SetRenderState(D3DRS_DESTBLEND, s_pfxAdditive ? D3DBLEND_ONE : D3DBLEND_INVSRCALPHA);
|
||||||
dev->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
|
dev->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
|
||||||
|
|
||||||
// Particle sprite texture: the beams' authentic grit sheet MASKED by a
|
// Particle sprite texture -- the AUTHORED firesmoke sheet. Every
|
||||||
// radial falloff -> soft round fiery blobs (an unmasked square sheet on an
|
// firesmokeN_scr_tex in BTFX.VMF maps the SAME 64x64 tileable noise image
|
||||||
// additive quad reads as a hard BOX, which the 1995 sprites never did).
|
// "bintA" (the variants differ only in SCROLL rate: firesmoke1 = 0.05,-0.331
|
||||||
// Baked once from the decoded grit; pure radial gradient if grit is absent.
|
// uv/s, firesmoke2 = 0,0.131, firesmoke4 = 0.05,-0.131), and firesmoke1_mtl
|
||||||
static LPDIRECT3DTEXTURE9 s_pfxTex = 0;
|
// colours it through the "fiery" ramp (0.3,0.1,0.1)->(0.9,0.7,0.3) -- the
|
||||||
|
// rolling orange fireball of the original. Bake ramp(lum) as the sprite
|
||||||
|
// colour with the noise detail in alpha; the RADIAL falloff lives in a
|
||||||
|
// SECOND (CLAMPed) texture stage so the WRAPPED scroll can roll flame
|
||||||
|
// through the sprite without scrolling the soft edge away. (The mask is
|
||||||
|
// port-shaped: we draw billboards where the original drew flame geometry,
|
||||||
|
// and a square scrolled sheet on a quad reads as a hard box.)
|
||||||
|
// Fallback when BINTA.VTX is absent: the old grit x radial single-tex bake.
|
||||||
|
static LPDIRECT3DTEXTURE9 s_pfxTex = 0; // fallback single-texture
|
||||||
|
static LPDIRECT3DTEXTURE9 s_pfxNoise = 0; // bintA through the "fiery" ramp
|
||||||
|
static LPDIRECT3DTEXTURE9 s_pfxMask = 0; // radial falloff (stage 1, CLAMP)
|
||||||
static int s_pfxTexTried = 0;
|
static int s_pfxTexTried = 0;
|
||||||
if (!s_pfxTexTried)
|
if (!s_pfxTexTried)
|
||||||
{
|
{
|
||||||
s_pfxTexTried = 1;
|
s_pfxTexTried = 1;
|
||||||
const int TW = 64, TH = 64;
|
const int TW = 64, TH = 64;
|
||||||
LPDIRECT3DTEXTURE9 grit = BTGetBeamGritTexture();
|
Image fireImg = decodeImage("VIDEO\\TEX\\BINTA.VTX");
|
||||||
unsigned char gritLum[128 * 64]; // grit is 128x64 when present
|
if (fireImg.ok && fireImg.w > 0 && fireImg.h > 0 &&
|
||||||
int gw = 0, gh = 0;
|
SUCCEEDED(dev->CreateTexture(fireImg.w, fireImg.h, 1, 0, D3DFMT_A8R8G8B8,
|
||||||
if (grit)
|
D3DPOOL_MANAGED, &s_pfxNoise, NULL)))
|
||||||
{
|
|
||||||
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;
|
D3DLOCKED_RECT lr;
|
||||||
if (SUCCEEDED(s_pfxTex->LockRect(0, &lr, NULL, 0)))
|
if (SUCCEEDED(s_pfxNoise->LockRect(0, &lr, NULL, 0)))
|
||||||
|
{
|
||||||
|
const float lo[3] = { 0.3f, 0.1f, 0.1f }; // RAMP "fiery" (BTFX.VMF)
|
||||||
|
const float hi[3] = { 0.9f, 0.7f, 0.3f };
|
||||||
|
for (int y = 0; y < fireImg.h; ++y)
|
||||||
|
{
|
||||||
|
DWORD *dst = (DWORD *)((char *)lr.pBits + y * lr.Pitch);
|
||||||
|
for (int x = 0; x < fireImg.w; ++x)
|
||||||
|
{
|
||||||
|
uint32_t s = fireImg.argb[(size_t)y * fireImg.w + x];
|
||||||
|
float lum = (((s >> 16) & 0xFF) + ((s >> 8) & 0xFF) + (s & 0xFF)) / (3.0f * 255.0f);
|
||||||
|
int r = (int)((lo[0] + (hi[0] - lo[0]) * lum) * 255.0f);
|
||||||
|
int g = (int)((lo[1] + (hi[1] - lo[1]) * lum) * 255.0f);
|
||||||
|
int b = (int)((lo[2] + (hi[2] - lo[2]) * lum) * 255.0f);
|
||||||
|
// alpha carries the noise detail (same 0.35 floor the grit
|
||||||
|
// bake used) -> the occlusion edge breaks up like flame
|
||||||
|
int a = (int)((0.35f + 0.65f * lum) * 255.0f);
|
||||||
|
dst[x] = ((DWORD)a << 24) | (r << 16) | (g << 8) | b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s_pfxNoise->UnlockRect(0);
|
||||||
|
}
|
||||||
|
else { s_pfxNoise->Release(); s_pfxNoise = 0; }
|
||||||
|
}
|
||||||
|
if (s_pfxNoise &&
|
||||||
|
SUCCEEDED(dev->CreateTexture(TW, TH, 1, 0, D3DFMT_A8R8G8B8,
|
||||||
|
D3DPOOL_MANAGED, &s_pfxMask, NULL)))
|
||||||
|
{
|
||||||
|
D3DLOCKED_RECT lr;
|
||||||
|
if (SUCCEEDED(s_pfxMask->LockRect(0, &lr, NULL, 0)))
|
||||||
{
|
{
|
||||||
for (int y = 0; y < TH; ++y)
|
for (int y = 0; y < TH; ++y)
|
||||||
{
|
{
|
||||||
@@ -1008,30 +1031,125 @@ void BTDrawPfx(LPDIRECT3DDEVICE9 dev, const D3DXMATRIX *view, float dt)
|
|||||||
float f = 1.0f - r; // radial falloff
|
float f = 1.0f - r; // radial falloff
|
||||||
if (f < 0.0f) f = 0.0f;
|
if (f < 0.0f) f = 0.0f;
|
||||||
f = f * f * (3.0f - 2.0f * f); // smoothstep edge
|
f = f * f * (3.0f - 2.0f * f); // smoothstep edge
|
||||||
float n = 1.0f;
|
int v = (int)(f * 255.0f);
|
||||||
if (gw > 0) // authentic grit detail
|
dst[x] = ((DWORD)v << 24) | (v << 16) | (v << 8) | v;
|
||||||
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);
|
s_pfxMask->UnlockRect(0);
|
||||||
|
}
|
||||||
|
else { s_pfxMask->Release(); s_pfxMask = 0; }
|
||||||
|
}
|
||||||
|
if (s_pfxNoise && !s_pfxMask) { s_pfxNoise->Release(); s_pfxNoise = 0; }
|
||||||
|
if (!s_pfxNoise)
|
||||||
|
{
|
||||||
|
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; }
|
||||||
}
|
}
|
||||||
else { s_pfxTex->Release(); s_pfxTex = 0; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dev->SetTexture(0, s_pfxTex);
|
|
||||||
dev->SetTextureStageState(0, D3DTSS_COLOROP, s_pfxTex ? D3DTOP_MODULATE : D3DTOP_SELECTARG1);
|
// The authored SCROLL clock (firesmoke1's rate; the shipped variants share
|
||||||
dev->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE);
|
// the sheet and differ only in rate, so the port rolls every sprite at the
|
||||||
dev->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_TEXTURE);
|
// dominant one). Accumulate the OFFSETS and wrap each into [0,1) -- an
|
||||||
// occlusion = vertex alpha x the texture's falloff mask (soft-edged smoke)
|
// unbounded time * rate loses float precision over a long session (the
|
||||||
dev->SetTextureStageState(0, D3DTSS_ALPHAOP, s_pfxTex ? D3DTOP_MODULATE : D3DTOP_SELECTARG1);
|
// tsphere scroll-precision lesson).
|
||||||
dev->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_DIFFUSE);
|
static float s_pfxScrollU = 0.0f, s_pfxScrollV = 0.0f;
|
||||||
dev->SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_TEXTURE);
|
s_pfxScrollU += 0.05f * dt; s_pfxScrollU -= floorf(s_pfxScrollU);
|
||||||
|
s_pfxScrollV += -0.331f * dt; s_pfxScrollV -= floorf(s_pfxScrollV);
|
||||||
|
|
||||||
|
if (s_pfxNoise)
|
||||||
|
{
|
||||||
|
// stage 0: diffuse x the scrolled fire noise (WRAPped)
|
||||||
|
dev->SetTexture(0, s_pfxNoise);
|
||||||
|
dev->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
|
||||||
|
dev->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE);
|
||||||
|
dev->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_TEXTURE);
|
||||||
|
dev->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_MODULATE);
|
||||||
|
dev->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_DIFFUSE);
|
||||||
|
dev->SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_TEXTURE);
|
||||||
|
D3DXMATRIX tm; D3DXMatrixIdentity(&tm);
|
||||||
|
tm._31 = s_pfxScrollU; tm._32 = s_pfxScrollV; // (u,v) translation
|
||||||
|
dev->SetTransform(D3DTS_TEXTURE0, &tm);
|
||||||
|
dev->SetTextureStageState(0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_COUNT2);
|
||||||
|
dev->SetSamplerState(0, D3DSAMP_ADDRESSU, D3DTADDRESS_WRAP);
|
||||||
|
dev->SetSamplerState(0, D3DSAMP_ADDRESSV, D3DTADDRESS_WRAP);
|
||||||
|
// stage 1: x the CLAMPed radial mask (same UV set, UNscrolled) -- both
|
||||||
|
// colour and alpha, so the premultiplied occlusion stays soft-edged
|
||||||
|
dev->SetTexture(1, s_pfxMask);
|
||||||
|
dev->SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_MODULATE);
|
||||||
|
dev->SetTextureStageState(1, D3DTSS_COLORARG1, D3DTA_CURRENT);
|
||||||
|
dev->SetTextureStageState(1, D3DTSS_COLORARG2, D3DTA_TEXTURE);
|
||||||
|
dev->SetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_MODULATE);
|
||||||
|
dev->SetTextureStageState(1, D3DTSS_ALPHAARG1, D3DTA_CURRENT);
|
||||||
|
dev->SetTextureStageState(1, D3DTSS_ALPHAARG2, D3DTA_TEXTURE);
|
||||||
|
dev->SetTextureStageState(1, D3DTSS_TEXCOORDINDEX, 0);
|
||||||
|
dev->SetSamplerState(1, D3DSAMP_ADDRESSU, D3DTADDRESS_CLAMP);
|
||||||
|
dev->SetSamplerState(1, D3DSAMP_ADDRESSV, D3DTADDRESS_CLAMP);
|
||||||
|
dev->SetSamplerState(1, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
|
||||||
|
dev->SetSamplerState(1, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
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);
|
D3DXMATRIX ident; D3DXMatrixIdentity(&ident);
|
||||||
dev->SetTransform(D3DTS_WORLD, &ident);
|
dev->SetTransform(D3DTS_WORLD, &ident);
|
||||||
@@ -1108,6 +1226,17 @@ void BTDrawPfx(LPDIRECT3DDEVICE9 dev, const D3DXMATRIX *view, float dt)
|
|||||||
&verts[0], sizeof(V));
|
&verts[0], sizeof(V));
|
||||||
|
|
||||||
dev->SetTexture(0, NULL);
|
dev->SetTexture(0, NULL);
|
||||||
|
if (s_pfxNoise)
|
||||||
|
{
|
||||||
|
// tear down the two-stage fire pipe: stage 1 back to the D3D default
|
||||||
|
// (DISABLE), the scroll transform off, sampler 0 addressing restored
|
||||||
|
dev->SetTexture(1, NULL);
|
||||||
|
dev->SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE);
|
||||||
|
dev->SetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE);
|
||||||
|
dev->SetTextureStageState(0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_DISABLE);
|
||||||
|
}
|
||||||
|
dev->SetSamplerState(0, D3DSAMP_ADDRESSU, sAdrU);
|
||||||
|
dev->SetSamplerState(0, D3DSAMP_ADDRESSV, sAdrV);
|
||||||
dev->SetRenderState(D3DRS_LIGHTING, sLight);
|
dev->SetRenderState(D3DRS_LIGHTING, sLight);
|
||||||
dev->SetRenderState(D3DRS_FOGENABLE, sFog);
|
dev->SetRenderState(D3DRS_FOGENABLE, sFog);
|
||||||
dev->SetRenderState(D3DRS_ZWRITEENABLE, sZW);
|
dev->SetRenderState(D3DRS_ZWRITEENABLE, sZW);
|
||||||
@@ -7445,6 +7574,14 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte
|
|||||||
|
|
||||||
hr = mDevice->BeginScene();
|
hr = mDevice->BeginScene();
|
||||||
|
|
||||||
|
// DOCK-BOTTOM (single-window gauges): keep the world in the top region;
|
||||||
|
// the gauge strip owns the bottom band (drawn by BTDrawGaugeInset with a
|
||||||
|
// full-target viewport before EndScene). No-op unless the mode is on.
|
||||||
|
{
|
||||||
|
extern void BTApplyWorldViewport(LPDIRECT3DDEVICE9 device);
|
||||||
|
BTApplyWorldViewport(mDevice);
|
||||||
|
}
|
||||||
|
|
||||||
mDevice->SetFVF(L4VERTEX_FVF);
|
mDevice->SetFVF(L4VERTEX_FVF);
|
||||||
|
|
||||||
D3DXMATRIX viewTransform;
|
D3DXMATRIX viewTransform;
|
||||||
@@ -8558,11 +8695,29 @@ void
|
|||||||
// backbuffer into the client area, so rendering with the CLIENT aspect cancels
|
// backbuffer into the client area, so rendering with the CLIENT aspect cancels
|
||||||
// the stretch.
|
// the stretch.
|
||||||
//
|
//
|
||||||
|
// DOCK-BOTTOM (single-window gauges): the world occupies the client MINUS the
|
||||||
|
// gauge strip (strip height tracks width at the panel's 480/1320 design ratio),
|
||||||
|
// so the projection aspect must come from the WORLD region or the scene
|
||||||
|
// stretches vertically by the strip's share.
|
||||||
|
static float BTWorldAspectOf(int client_w, int client_h)
|
||||||
|
{
|
||||||
|
extern int gBTGaugeDockBottom;
|
||||||
|
extern int BTGaugeStripHeightFor(int width);
|
||||||
|
float world_h = (float)client_h;
|
||||||
|
if (gBTGaugeDockBottom)
|
||||||
|
{
|
||||||
|
float strip = (float)BTGaugeStripHeightFor(client_w);
|
||||||
|
if (strip > 0.0f && strip < world_h - 8.0f)
|
||||||
|
world_h -= strip;
|
||||||
|
}
|
||||||
|
return (float)client_w / world_h;
|
||||||
|
}
|
||||||
|
|
||||||
void L4NotifyWindowResized(int client_w, int client_h)
|
void L4NotifyWindowResized(int client_w, int client_h)
|
||||||
{
|
{
|
||||||
if (client_w <= 0 || client_h <= 0)
|
if (client_w <= 0 || client_h <= 0)
|
||||||
return;
|
return;
|
||||||
gWindowAspect = (float)client_w / (float)client_h;
|
gWindowAspect = BTWorldAspectOf(client_w, client_h);
|
||||||
DEBUG_STREAM << "[resize] client " << client_w << "x" << client_h
|
DEBUG_STREAM << "[resize] client " << client_w << "x" << client_h
|
||||||
<< " aspect=" << gWindowAspect
|
<< " aspect=" << gWindowAspect
|
||||||
<< " app=" << (void *)l4_application << "\n" << std::flush;
|
<< " app=" << (void *)l4_application << "\n" << std::flush;
|
||||||
@@ -8579,7 +8734,7 @@ void
|
|||||||
{
|
{
|
||||||
if (client_w <= 0 || client_h <= 0)
|
if (client_w <= 0 || client_h <= 0)
|
||||||
return;
|
return;
|
||||||
gWindowAspect = (float)client_w / (float)client_h;
|
gWindowAspect = BTWorldAspectOf(client_w, client_h);
|
||||||
if (viewAngle <= 0.0f || clipFar <= clipNear)
|
if (viewAngle <= 0.0f || clipFar <= clipNear)
|
||||||
{
|
{
|
||||||
DEBUG_STREAM << "[resize] projection not built yet (viewAngle="
|
DEBUG_STREAM << "[resize] projection not built yet (viewAngle="
|
||||||
|
|||||||
@@ -1910,6 +1910,17 @@ public:
|
|||||||
OrientationMatrix(3, 1) = y;
|
OrientationMatrix(3, 1) = y;
|
||||||
OrientationMatrix(3, 2) = z;
|
OrientationMatrix(3, 2) = z;
|
||||||
}
|
}
|
||||||
|
// Overwrite the offset's ROTATION rows with a yaw (rotation about local Y).
|
||||||
|
// The BT wreck-flame billboard (the 1996 dpl_SetDCSReorientAxes(dcs,
|
||||||
|
// dpl_reorient_axes_y) on flamebig) yaws the flame card toward the camera
|
||||||
|
// each frame; translation row is left untouched.
|
||||||
|
void SetOffsetYaw(float yaw)
|
||||||
|
{
|
||||||
|
float c = cosf(yaw), s = sinf(yaw);
|
||||||
|
OrientationMatrix(0, 0) = c; OrientationMatrix(0, 1) = 0.0f; OrientationMatrix(0, 2) = -s;
|
||||||
|
OrientationMatrix(1, 0) = 0.0f; OrientationMatrix(1, 1) = 1.0f; OrientationMatrix(1, 2) = 0.0f;
|
||||||
|
OrientationMatrix(2, 0) = s; OrientationMatrix(2, 1) = 0.0f; OrientationMatrix(2, 2) = c;
|
||||||
|
}
|
||||||
protected:
|
protected:
|
||||||
dpl_DCS *myDCS, *myParentDCS;
|
dpl_DCS *myDCS, *myParentDCS;
|
||||||
|
|
||||||
|
|||||||
@@ -295,6 +295,9 @@ struct MatInfo {
|
|||||||
bool hasDiffuse = false; // material carried an explicit DIFFUSE/AMBIENT tag
|
bool hasDiffuse = false; // material carried an explicit DIFFUSE/AMBIENT tag
|
||||||
std::string texPath;
|
std::string texPath;
|
||||||
int texChannel = 0; // BSL bit-slice (BMF TEXTURE tag 0x18; absent = 0)
|
int texChannel = 0; // BSL bit-slice (BMF TEXTURE tag 0x18; absent = 0)
|
||||||
|
bool texScroll = false; // TEXTURE SPECIAL " SCROLL" (tag 0x2037): UV animation
|
||||||
|
float texScrollU = 0.0f; // cycles/second
|
||||||
|
float texScrollV = 0.0f;
|
||||||
// RAMP (task #20): the material's 2-endpoint colour ramp (dpl_SetMaterialRamp).
|
// RAMP (task #20): the material's 2-endpoint colour ramp (dpl_SetMaterialRamp).
|
||||||
// The grayscale texture's luminance indexes a low->high colour gradient -- this
|
// The grayscale texture's luminance indexes a low->high colour gradient -- this
|
||||||
// is how the IG board coloured terrain (rock 0.25,0.21,0.16->0.8,0.5,0.4=warm
|
// is how the IG board coloured terrain (rock 0.25,0.21,0.16->0.8,0.5,0.4=warm
|
||||||
@@ -316,6 +319,11 @@ struct MatInfo {
|
|||||||
struct TexRef {
|
struct TexRef {
|
||||||
std::string map;
|
std::string map;
|
||||||
int channel = 0;
|
int channel = 0;
|
||||||
|
// TEXTURE-record SPECIAL " SCROLL u0 v0 du dv" (tag 0x2037): authored UV
|
||||||
|
// scroll rates in cycles/second (the firesmoke fire-noise roll).
|
||||||
|
bool scroll = false;
|
||||||
|
float scrollU = 0.0f;
|
||||||
|
float scrollV = 0.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct MaterialResolver {
|
struct MaterialResolver {
|
||||||
@@ -333,6 +341,22 @@ struct MaterialResolver {
|
|||||||
if (ch.id == TAG_NAME) name = chunkStr(ch);
|
if (ch.id == TAG_NAME) name = chunkStr(ch);
|
||||||
else if (ch.id == TAG_TEXTURE_MAP) ref.map = chunkStr(ch);
|
else if (ch.id == TAG_TEXTURE_MAP) ref.map = chunkStr(ch);
|
||||||
else if (ch.id == TAG_BITSLICE && ch.len >= 1) ref.channel = ch.data[0];
|
else if (ch.id == TAG_BITSLICE && ch.len >= 1) ref.channel = ch.data[0];
|
||||||
|
else if (ch.id == TAG_SV_SPECIAL && ch.len > 8) {
|
||||||
|
// " SCROLL u0 v0 du dv" -- authored UV scroll (BTFX's
|
||||||
|
// firesmoke family). u0/v0 initial offsets are 0 in every
|
||||||
|
// shipped record; only the rates matter.
|
||||||
|
std::string sp = chunkStr(ch);
|
||||||
|
size_t at = sp.find("SCROLL");
|
||||||
|
if (at != std::string::npos) {
|
||||||
|
float u0 = 0, v0 = 0, du = 0, dv = 0;
|
||||||
|
if (sscanf(sp.c_str() + at + 6, "%f %f %f %f", &u0, &v0, &du, &dv) == 4
|
||||||
|
&& (du != 0.0f || dv != 0.0f)) {
|
||||||
|
ref.scroll = true;
|
||||||
|
ref.scrollU = du;
|
||||||
|
ref.scrollV = dv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!name.empty() && !ref.map.empty()) out[name] = ref;
|
if (!name.empty() && !ref.map.empty()) out[name] = ref;
|
||||||
}
|
}
|
||||||
@@ -442,7 +466,13 @@ struct MaterialResolver {
|
|||||||
if (tm != texMaps.end()) {
|
if (tm != texMaps.end()) {
|
||||||
const auto& imgs = imageIndex();
|
const auto& imgs = imageIndex();
|
||||||
auto f = imgs.find(stemLower(tm->second.map));
|
auto f = imgs.find(stemLower(tm->second.map));
|
||||||
if (f != imgs.end()) { info.texPath = f->second; info.texChannel = tm->second.channel; }
|
if (f != imgs.end()) {
|
||||||
|
info.texPath = f->second;
|
||||||
|
info.texChannel = tm->second.channel;
|
||||||
|
info.texScroll = tm->second.scroll;
|
||||||
|
info.texScrollU = tm->second.scrollU;
|
||||||
|
info.texScrollV = tm->second.scrollV;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
out[name] = info;
|
out[name] = info;
|
||||||
}
|
}
|
||||||
@@ -501,6 +531,8 @@ struct Builder {
|
|||||||
bool currentHasDiffuse = false; // material carried an explicit diffuse tag
|
bool currentHasDiffuse = false; // material carried an explicit diffuse tag
|
||||||
std::string currentTex;
|
std::string currentTex;
|
||||||
int currentTexChannel = 0; // BSL bit-slice (BMF tag 0x18)
|
int currentTexChannel = 0; // BSL bit-slice (BMF tag 0x18)
|
||||||
|
bool currentTexScroll = false; // TEXTURE SPECIAL " SCROLL" (tag 0x2037)
|
||||||
|
float currentTexScrollU = 0.0f, currentTexScrollV = 0.0f;
|
||||||
bool currentHasRamp = false;
|
bool currentHasRamp = false;
|
||||||
float currentRampLo[3] = {0,0,0};
|
float currentRampLo[3] = {0,0,0};
|
||||||
float currentRampHi[3] = {1,1,1};
|
float currentRampHi[3] = {1,1,1};
|
||||||
@@ -569,18 +601,32 @@ struct Builder {
|
|||||||
|
|
||||||
void buildPmesh(const Chunk& pmesh) {
|
void buildPmesh(const Chunk& pmesh) {
|
||||||
std::vector<float> lx, ly, lz, lu, lv;
|
std::vector<float> lx, ly, lz, lu, lv;
|
||||||
|
// Authored per-vertex RGBA (tags 0x0082/0x008A: 4 floats at offset 12).
|
||||||
|
// Values run OUTSIDE [0,1] (the flame card FLAMEBIG.BGF fades its tip
|
||||||
|
// to alpha -0.2 and over-brightens its base to 1.2 -- the DPL clamp
|
||||||
|
// convention, same as the .PFX negative colour ramps).
|
||||||
|
std::vector<uint32_t> lrgba;
|
||||||
|
bool vertexAlpha = false; // any authored alpha < 1 -> BLENDED batch
|
||||||
uint16_t vtag = 0;
|
uint16_t vtag = 0;
|
||||||
for (const Chunk& ch : pmesh.children) {
|
for (const Chunk& ch : pmesh.children) {
|
||||||
int stride = vertexStride(ch.id);
|
int stride = vertexStride(ch.id);
|
||||||
if (!stride) continue;
|
if (!stride) continue;
|
||||||
vtag = ch.id;
|
vtag = ch.id;
|
||||||
int uvOff = uvOffset(ch.id);
|
int uvOff = uvOffset(ch.id);
|
||||||
|
const bool hasRGBA = (ch.id == TAG_VERTEX_XYZ_RGBA || ch.id == TAG_VERTEX_XYZ_RGBA_UV);
|
||||||
size_t count = ch.len / (size_t)stride;
|
size_t count = ch.len / (size_t)stride;
|
||||||
for (size_t i = 0; i < count; ++i) {
|
for (size_t i = 0; i < count; ++i) {
|
||||||
const uint8_t* v = ch.data + i * stride;
|
const uint8_t* v = ch.data + i * stride;
|
||||||
lx.push_back(rdF32(v)); ly.push_back(rdF32(v + 4)); lz.push_back(rdF32(v + 8));
|
lx.push_back(rdF32(v)); ly.push_back(rdF32(v + 4)); lz.push_back(rdF32(v + 8));
|
||||||
if (uvOff >= 0) { lu.push_back(rdF32(v + uvOff)); lv.push_back(rdF32(v + uvOff + 4)); }
|
if (uvOff >= 0) { lu.push_back(rdF32(v + uvOff)); lv.push_back(rdF32(v + uvOff + 4)); }
|
||||||
else { lu.push_back(0.0f); lv.push_back(0.0f); }
|
else { lu.push_back(0.0f); lv.push_back(0.0f); }
|
||||||
|
if (hasRGBA) {
|
||||||
|
float cr = rdF32(v + 12), cg = rdF32(v + 16), cb = rdF32(v + 20), ca = rdF32(v + 24);
|
||||||
|
if (ca < 0.999f) vertexAlpha = true;
|
||||||
|
auto C8 = [](float f) { int n = (int)(f * 255.0f + 0.5f); return (uint32_t)(n < 0 ? 0 : n > 255 ? 255 : n); };
|
||||||
|
lrgba.push_back((C8(ca) << 24) | (C8(cr) << 16) | (C8(cg) << 8) | C8(cb));
|
||||||
|
} else
|
||||||
|
lrgba.push_back(0xFFFFFFFFu);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -670,7 +716,13 @@ struct Builder {
|
|||||||
px.push_back(lx[i]); py.push_back(ly[i]); pz.push_back(lz[i]);
|
px.push_back(lx[i]); py.push_back(ly[i]); pz.push_back(lz[i]);
|
||||||
nx.push_back(0); ny.push_back(0); nz.push_back(0);
|
nx.push_back(0); ny.push_back(0); nz.push_back(0);
|
||||||
uu.push_back(lu[i] * tileU + twist * lv[i]); vv.push_back(lv[i] * tileV);
|
uu.push_back(lu[i] * tileU + twist * lv[i]); vv.push_back(lv[i] * tileV);
|
||||||
col.push_back(vcol);
|
// VERTEX-ALPHA batches (the flame cards) keep their AUTHORED
|
||||||
|
// per-vertex gradient (white-hot base -> dark faded tip) -- it IS
|
||||||
|
// the flame's shading, modulated by the (ramped, scrolling)
|
||||||
|
// texture in the blend pass. Opaque batches keep the flat
|
||||||
|
// tint/diffuse rule unchanged (alpha==1 skins carry no gradient
|
||||||
|
// the verified look depends on).
|
||||||
|
col.push_back(vertexAlpha ? lrgba[i] : vcol);
|
||||||
}
|
}
|
||||||
|
|
||||||
const uint32_t idxStart = (uint32_t)mesh->indices.size();
|
const uint32_t idxStart = (uint32_t)mesh->indices.size();
|
||||||
@@ -729,6 +781,10 @@ struct Builder {
|
|||||||
: (useRamp ? rampTint : currentColor); // pure-emissive test keys on it
|
: (useRamp ? rampTint : currentColor); // pure-emissive test keys on it
|
||||||
batch.texPath = currentTex;
|
batch.texPath = currentTex;
|
||||||
batch.texChannel = currentTexChannel;
|
batch.texChannel = currentTexChannel;
|
||||||
|
batch.texScroll = currentTexScroll;
|
||||||
|
batch.texScrollU = currentTexScrollU;
|
||||||
|
batch.texScrollV = currentTexScrollV;
|
||||||
|
batch.vertexAlpha = vertexAlpha;
|
||||||
batch.hasRamp = useRamp;
|
batch.hasRamp = useRamp;
|
||||||
// tsphere warp flag: warpBands>0 signals L4D3D's ramp bake to CONTRAST-
|
// tsphere warp flag: warpBands>0 signals L4D3D's ramp bake to CONTRAST-
|
||||||
// COMPRESS the bintA cloud toward mid-grey (BT_WARP_CONTRAST) before applying
|
// COMPRESS the bintA cloud toward mid-grey (BT_WARP_CONTRAST) before applying
|
||||||
@@ -966,6 +1022,8 @@ struct Builder {
|
|||||||
bool savedHasDiffuse = currentHasDiffuse;
|
bool savedHasDiffuse = currentHasDiffuse;
|
||||||
std::string savedTex = currentTex;
|
std::string savedTex = currentTex;
|
||||||
int savedTexChannel = currentTexChannel;
|
int savedTexChannel = currentTexChannel;
|
||||||
|
bool savedTexScroll = currentTexScroll;
|
||||||
|
float savedTexScrollU = currentTexScrollU, savedTexScrollV = currentTexScrollV;
|
||||||
bool savedHasRamp = currentHasRamp;
|
bool savedHasRamp = currentHasRamp;
|
||||||
bool savedPunch = currentPunch;
|
bool savedPunch = currentPunch;
|
||||||
bool savedShadowMat = currentShadowMat;
|
bool savedShadowMat = currentShadowMat;
|
||||||
@@ -1048,12 +1106,15 @@ struct Builder {
|
|||||||
currentColor = info.color; currentTex = info.texPath;
|
currentColor = info.color; currentTex = info.texPath;
|
||||||
currentHasDiffuse = info.hasDiffuse;
|
currentHasDiffuse = info.hasDiffuse;
|
||||||
currentTexChannel = info.texChannel;
|
currentTexChannel = info.texChannel;
|
||||||
|
currentTexScroll = info.texScroll;
|
||||||
|
currentTexScrollU = info.texScrollU;
|
||||||
|
currentTexScrollV = info.texScrollV;
|
||||||
currentHasRamp = info.hasRamp;
|
currentHasRamp = info.hasRamp;
|
||||||
for (int i = 0; i < 3; ++i) { currentRampLo[i] = info.rampLo[i]; currentRampHi[i] = info.rampHi[i]; }
|
for (int i = 0; i < 3; ++i) { currentRampLo[i] = info.rampLo[i]; currentRampHi[i] = info.rampHi[i]; }
|
||||||
currentHasEmissive = info.hasEmissive;
|
currentHasEmissive = info.hasEmissive;
|
||||||
for (int i = 0; i < 3; ++i) currentEmissive[i] = info.emissive[i];
|
for (int i = 0; i < 3; ++i) currentEmissive[i] = info.emissive[i];
|
||||||
}
|
}
|
||||||
else { currentColor = colorForMaterial(full); currentTex.clear(); currentHasDiffuse = false; currentTexChannel = 0; }
|
else { currentColor = colorForMaterial(full); currentTex.clear(); currentHasDiffuse = false; currentTexChannel = 0; currentTexScroll = false; currentTexScrollU = currentTexScrollV = 0.0f; }
|
||||||
// TRANSLOCATION WARP: override the material's coarse "sky" ramp with the
|
// TRANSLOCATION WARP: override the material's coarse "sky" ramp with the
|
||||||
// NARROW LAVENDER ramp that matched the original (capture.png). The swirl
|
// NARROW LAVENDER ramp that matched the original (capture.png). The swirl
|
||||||
// is the bintA cloud coloured lo->hi by luminance; a wide sky ramp
|
// is the bintA cloud coloured lo->hi by luminance; a wide sky ramp
|
||||||
@@ -1085,6 +1146,9 @@ struct Builder {
|
|||||||
currentHasDiffuse = savedHasDiffuse;
|
currentHasDiffuse = savedHasDiffuse;
|
||||||
currentTex = savedTex;
|
currentTex = savedTex;
|
||||||
currentTexChannel = savedTexChannel;
|
currentTexChannel = savedTexChannel;
|
||||||
|
currentTexScroll = savedTexScroll;
|
||||||
|
currentTexScrollU = savedTexScrollU;
|
||||||
|
currentTexScrollV = savedTexScrollV;
|
||||||
currentHasRamp = savedHasRamp;
|
currentHasRamp = savedHasRamp;
|
||||||
currentPunch = savedPunch;
|
currentPunch = savedPunch;
|
||||||
currentShadowMat = savedShadowMat;
|
currentShadowMat = savedShadowMat;
|
||||||
|
|||||||
@@ -31,6 +31,19 @@ struct BgfDrawBatch {
|
|||||||
// holds blkhwk1..4); decoding the packed word as one RGBA image overlaid
|
// holds blkhwk1..4); decoding the packed word as one RGBA image overlaid
|
||||||
// 2-3 different sheets as color channels = the rainbow "graffiti" mechs.
|
// 2-3 different sheets as color channels = the rainbow "graffiti" mechs.
|
||||||
int texChannel = 0;
|
int texChannel = 0;
|
||||||
|
// TEXTURE SCROLL (the BMF TEXTURE record's SPECIAL " SCROLL u0 v0 du dv",
|
||||||
|
// tag 0x2037): authored UV animation in cycles/second -- the firesmoke
|
||||||
|
// family (flamebig/fire5 flame cards, the burning-wreck dressing) rolls
|
||||||
|
// its fire noise this way. Propagated to L4TEXOP::doScroll, which the
|
||||||
|
// draw path (d3d_OBJECT::SetTextureScrolling) already honours.
|
||||||
|
bool texScroll = false;
|
||||||
|
float texScrollU = 0.0f;
|
||||||
|
float texScrollV = 0.0f;
|
||||||
|
// VERTEX ALPHA (tags 0x0082/0x008A with any authored alpha < 1): the batch
|
||||||
|
// is a BLENDED effect card (FLAMEBIG's flame fan fades its tip to negative
|
||||||
|
// alpha). The verts keep their authored RGBA gradient and the draw routes
|
||||||
|
// to the alpha-blend pass, unlit, colour = texture x vertex gradient.
|
||||||
|
bool vertexAlpha = false;
|
||||||
// RAMP (task #20): colorize the grayscale texture by luminance across
|
// RAMP (task #20): colorize the grayscale texture by luminance across
|
||||||
// rampLo->rampHi (the authentic IG-board terrain colouring). When set, the
|
// rampLo->rampHi (the authentic IG-board terrain colouring). When set, the
|
||||||
// renderer bakes lerp(rampLo, rampHi, texLum) into the texture and draws with
|
// renderer bakes lerp(rampLo, rampHi, texLum) into the texture and draws with
|
||||||
|
|||||||
@@ -98,6 +98,11 @@ struct L4DRAWOP
|
|||||||
// black texels were loaded with alpha 0; draw with alpha TEST in the opaque pass
|
// black texels were loaded with alpha 0; draw with alpha TEST in the opaque pass
|
||||||
// (z-write preserved), rejecting hole texels.
|
// (z-write preserved), rejecting hole texels.
|
||||||
bool punchThrough;
|
bool punchThrough;
|
||||||
|
// VERTEX-ALPHA BLEND (BGF 0x0082/0x008A verts with authored alpha < 1): an
|
||||||
|
// effect card (the FLAMEBIG flame fan). Drawn in the alpha-blend pass,
|
||||||
|
// UNLIT, colour = texture x the authored per-vertex gradient, alpha = the
|
||||||
|
// per-vertex fade (SRCALPHA/INVSRCALPHA).
|
||||||
|
bool vertexAlphaBlend;
|
||||||
// COCKPIT PUNCH STENCIL-CUT (task #55, i860-firmware-decoded): 0=normal,
|
// COCKPIT PUNCH STENCIL-CUT (task #55, i860-firmware-decoded): 0=normal,
|
||||||
// 1=aperture MASK (drawn stencil-only, paired with the following hull op),
|
// 1=aperture MASK (drawn stencil-only, paired with the following hull op),
|
||||||
// 2=HULL (drawn stencil-rejected under the mask = window cutouts).
|
// 2=HULL (drawn stencil-rejected under the mask = window cutouts).
|
||||||
@@ -158,6 +163,7 @@ public:
|
|||||||
// frustum once (SetCullFrustum); Draw() then skips objects fully outside it.
|
// frustum once (SetCullFrustum); Draw() then skips objects fully outside it.
|
||||||
static void SetCullFrustum(const D3DXMATRIX *viewProj); // NULL disables for the frame
|
static void SetCullFrustum(const D3DXMATRIX *viewProj); // NULL disables for the frame
|
||||||
static void SetCameraPosition(float x, float y, float z); // for LOD distance selection
|
static void SetCameraPosition(float x, float y, float z); // for LOD distance selection
|
||||||
|
static void GetCameraPosition(float *x, float *y, float *z); // wreck-flame billboard yaw
|
||||||
D3DXVECTOR3 mCullCenter; // model-space bounding sphere (computed at load)
|
D3DXVECTOR3 mCullCenter; // model-space bounding sphere (computed at load)
|
||||||
float mCullRadius; // <= 0 -> unknown, never culled
|
float mCullRadius; // <= 0 -> unknown, never culled
|
||||||
|
|
||||||
|
|||||||
@@ -389,6 +389,30 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// DOCK-BOTTOM (single-window gauges, 2026-07-12): under BT_DEV_GAUGES the
|
||||||
|
// gauge strip is APPENDED below the world view in THIS window (strip height
|
||||||
|
// = width x 480/1320, the panel's design aspect); BT_DEV_GAUGES_WINDOW=1
|
||||||
|
// restores the old separate MFD window.
|
||||||
|
{
|
||||||
|
extern int gBTGaugeDockBottom;
|
||||||
|
extern int BTGaugeStripHeightFor(int width);
|
||||||
|
if (getenv("BT_DEV_GAUGES") != 0 && getenv("BT_DEV_GAUGES_WINDOW") == 0)
|
||||||
|
{
|
||||||
|
gBTGaugeDockBottom = 1;
|
||||||
|
// Readability default (user-reported: the strip at 800 wide is a
|
||||||
|
// 0.6x downscale, hard to read): a WIDER world region (the
|
||||||
|
// projection is aspect-correct for any shape) buys strip pixels --
|
||||||
|
// 1100x600 world + 1100x400 strip = a 1000-tall window that fits a
|
||||||
|
// 1080p desktop with the panel at 83% of design size. -res still
|
||||||
|
// overrides both.
|
||||||
|
if (strstr(lpCmdLine ? lpCmdLine : "", "-res") == 0)
|
||||||
|
{
|
||||||
|
winW = 1100;
|
||||||
|
winH = 600;
|
||||||
|
}
|
||||||
|
winH += BTGaugeStripHeightFor(winW);
|
||||||
|
}
|
||||||
|
}
|
||||||
RECT wr = { 0, 0, winW, winH };
|
RECT wr = { 0, 0, winW, winH };
|
||||||
AdjustWindowRect(&wr, WS_OVERLAPPEDWINDOW, FALSE);
|
AdjustWindowRect(&wr, WS_OVERLAPPEDWINDOW, FALSE);
|
||||||
|
|
||||||
|
|||||||
@@ -170,6 +170,21 @@ AmmoBin::~AmmoBin()
|
|||||||
// to Feeding, reloads the feed timer to feedRate, decrements ammoCount (clamped
|
// to Feeding, reloads the feed timer to feedRate, decrements ammoCount (clamped
|
||||||
// at 0) and raises the Empty alarm if that was the last round.
|
// at 0) and raises the Empty alarm if that was the last round.
|
||||||
//
|
//
|
||||||
|
// Panel bridges (Streak-gauge fix, 2026-07-12): the BallisticWeaponCluster
|
||||||
|
// read the bin's rounds via raw `bin+0x180` and the reload state via raw
|
||||||
|
// `bin+0x18c` -- the BINARY's offsets, garbage on our compiled layout (the
|
||||||
|
// HeatWatcher base sizes differ), so the ammo counter showed a constant and
|
||||||
|
// the reload wipe never armed. Complete-type reads of the NAMED members.
|
||||||
|
int *BTAmmoBinCountPtr(void *bin)
|
||||||
|
{
|
||||||
|
return (bin != 0) ? &((AmmoBin *)bin)->ammoCount : 0;
|
||||||
|
}
|
||||||
|
int BTAmmoBinFeeding(void *bin)
|
||||||
|
{
|
||||||
|
// "reloading" for the panel = the bin mid-feed (alarm state Feeding).
|
||||||
|
return (bin != 0 && ((AmmoBin *)bin)->ammoAlarm.Level() == AmmoBin::Feeding) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
int AmmoBin::FeedAmmo()
|
int AmmoBin::FeedAmmo()
|
||||||
{
|
{
|
||||||
if (simulationState == 1) // this+0x40 == 1 (destroyed)
|
if (simulationState == 1) // this+0x40 == 1 (destroyed)
|
||||||
|
|||||||
@@ -78,6 +78,8 @@
|
|||||||
public HeatWatcher
|
public HeatWatcher
|
||||||
{
|
{
|
||||||
friend struct AmmoBinLayoutCheck; // compile-time offset locks (ammobin.cpp)
|
friend struct AmmoBinLayoutCheck; // compile-time offset locks (ammobin.cpp)
|
||||||
|
friend int *BTAmmoBinCountPtr(void *bin); // panel ammo counter bridge (Streak fix)
|
||||||
|
friend int BTAmmoBinFeeding(void *bin); // panel reload-state bridge
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
// Shared Data Support
|
// Shared Data Support
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -1673,7 +1673,11 @@ void WeaponCluster::Execute()
|
|||||||
if (getenv("BT_PANEL_LOG"))
|
if (getenv("BT_PANEL_LOG"))
|
||||||
{
|
{
|
||||||
static int s_pl = 0;
|
static int s_pl = 0;
|
||||||
if ((s_pl++ % 60) == 0)
|
if ((s_pl++ % 61) == 0) // 61 PRIME: a shared counter at %60 ALIASES
|
||||||
|
// to one cluster when the panel count divides
|
||||||
|
// 60 (the Avatar's 6 panels -> only AFC100
|
||||||
|
// ever printed; the emitter probe's %97
|
||||||
|
// comment taught this lesson already)
|
||||||
DEBUG_STREAM << "[panel] " << (diagWeaponName ? diagWeaponName : "?")
|
DEBUG_STREAM << "[panel] " << (diagWeaponName ? diagWeaponName : "?")
|
||||||
<< " percentDone=" << percentDone
|
<< " percentDone=" << percentDone
|
||||||
<< " warn=" << warningState << "\n" << std::flush;
|
<< " warn=" << warningState << "\n" << std::flush;
|
||||||
@@ -1751,11 +1755,18 @@ BallisticWeaponCluster::BallisticWeaponCluster(
|
|||||||
reloadSeconds = 0.0f; // @0x3E
|
reloadSeconds = 0.0f; // @0x3E
|
||||||
jammed = firing = reloading = False;
|
jammed = firing = reloading = False;
|
||||||
|
|
||||||
void *ammoBin = ResolveLink((char *)subsystem_in + 0x43c); // FUN_00417ab4 (raw)
|
// AFC100-counter fix (2026-07-12): the raw ResolveLink walk assumed the
|
||||||
|
// 1995 connection internals -- resolve through the weapon's own typed
|
||||||
|
// connection instead (the same call ConsumeRound fires through).
|
||||||
|
extern void *BTWeaponAmmoBin(void *weapon);
|
||||||
|
void *ammoBin = BTWeaponAmmoBin(subsystem_in);
|
||||||
int engPort = renderer_in->FindGraphicsPort(eng_port_name);
|
int engPort = renderer_in->FindGraphicsPort(eng_port_name);
|
||||||
|
|
||||||
// Ammo count off the bin (+0x180); BEST-EFFORT raw if the bin resolves.
|
// Ammo count via the complete-type bridge (Streak-gauge fix, 2026-07-12):
|
||||||
int *ammoValue = (ammoBin != NULL) ? (int *)((char *)ammoBin + 0x180) : NULL;
|
// the old raw `bin+0x180` was the BINARY's ammoCount offset -- garbage on
|
||||||
|
// our compiled AmmoBin (base sizes differ), so the counter never moved.
|
||||||
|
extern int *BTAmmoBinCountPtr(void *bin);
|
||||||
|
int *ammoValue = BTAmmoBinCountPtr(ammoBin);
|
||||||
|
|
||||||
ammoCountA = new NumericDisplayInteger(ChildRate(), mfd_mode, renderer_in, // @00470cfc
|
ammoCountA = new NumericDisplayInteger(ChildRate(), mfd_mode, renderer_in, // @00470cfc
|
||||||
owner_ID, mfd_port, x + 0x27, y + 100, x + 0x5f, y + 0x76, font_a, 4,
|
owner_ID, mfd_port, x + 0x27, y + 100, x + 0x5f, y + 0x76, font_a, 4,
|
||||||
@@ -1823,7 +1834,8 @@ void BallisticWeaponCluster::Execute()
|
|||||||
{
|
{
|
||||||
jammed = (*(int *)((char *)subsystem + 0x364) == 5); // BEST-EFFORT raw (weaponAlarm==Jammed)
|
jammed = (*(int *)((char *)subsystem + 0x364) == 5); // BEST-EFFORT raw (weaponAlarm==Jammed)
|
||||||
void *ammoBin = ResolveLink((char *)subsystem + 0x43c); // FUN_00417ab4
|
void *ammoBin = ResolveLink((char *)subsystem + 0x43c); // FUN_00417ab4
|
||||||
int reloadState = (ammoBin != NULL) ? *(int *)((char *)ammoBin + 0x18c) : 0;
|
extern int BTAmmoBinFeeding(void *bin);
|
||||||
|
int reloadState = BTAmmoBinFeeding(ammoBin); // was raw bin+0x18c (garbage on our layout)
|
||||||
firing = reloadState;
|
firing = reloadState;
|
||||||
if (reloadState == 0)
|
if (reloadState == 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -802,10 +802,16 @@ void
|
|||||||
//
|
//
|
||||||
if (render_tree.wreckHulk != NULL) render_tree.wreckHulk->SetDrawObj(NULL);
|
if (render_tree.wreckHulk != NULL) render_tree.wreckHulk->SetDrawObj(NULL);
|
||||||
if (render_tree.wreckDebris != NULL) render_tree.wreckDebris->SetDrawObj(NULL);
|
if (render_tree.wreckDebris != NULL) render_tree.wreckDebris->SetDrawObj(NULL);
|
||||||
|
if (render_tree.wreckFlames != NULL) render_tree.wreckFlames->SetDrawObj(NULL);
|
||||||
render_tree.wreckHulk = NULL;
|
render_tree.wreckHulk = NULL;
|
||||||
render_tree.wreckDebris = NULL;
|
render_tree.wreckDebris = NULL;
|
||||||
|
render_tree.wreckFlames = NULL;
|
||||||
render_tree.wrecked = 0;
|
render_tree.wrecked = 0;
|
||||||
render_tree.wreckAge = 0.0f;
|
render_tree.wreckAge = 0.0f;
|
||||||
|
render_tree.wreckRevealed = 0;
|
||||||
|
render_tree.wreckHulkObj = NULL;
|
||||||
|
render_tree.wreckDebrisObj = NULL;
|
||||||
|
render_tree.wreckFlamesObj = NULL;
|
||||||
|
|
||||||
//
|
//
|
||||||
// Restore every body segment to its now-intact mesh via the shared view-skeleton
|
// Restore every body segment to its now-intact mesh via the shared view-skeleton
|
||||||
@@ -929,6 +935,16 @@ void
|
|||||||
}
|
}
|
||||||
d3d_OBJECT *hulk = (hulk_name[0] != '\0')
|
d3d_OBJECT *hulk = (hulk_name[0] != '\0')
|
||||||
? d3d_OBJECT::LoadObject(GetDevice(), hulk_name) : NULL;
|
? d3d_OBJECT::LoadObject(GetDevice(), hulk_name) : NULL;
|
||||||
|
// EMPTY-PLACEHOLDER guard: some shipped hulks are 153-byte stubs with ZERO
|
||||||
|
// geometry (THRDBR.BGF, FLAMESML.BGF) -- they "load" fine and draw nothing,
|
||||||
|
// so the missing-file fallback alone never fires. Treat no-vertex hulks
|
||||||
|
// as missing.
|
||||||
|
if (hulk != NULL && hulk->GetVertCount() == 0)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "[BTrender] wreck: '" << hulk_name
|
||||||
|
<< "' is an EMPTY placeholder -> gendbr.bgf fallback\n" << std::flush;
|
||||||
|
hulk = NULL;
|
||||||
|
}
|
||||||
if (hulk == NULL)
|
if (hulk == NULL)
|
||||||
{
|
{
|
||||||
DEBUG_STREAM << "[BTrender] wreck: '" << hulk_name
|
DEBUG_STREAM << "[BTrender] wreck: '" << hulk_name
|
||||||
@@ -942,10 +958,25 @@ void
|
|||||||
//
|
//
|
||||||
d3d_OBJECT *debris = d3d_OBJECT::LoadObject(GetDevice(), "ldbr.bgf");
|
d3d_OBJECT *debris = d3d_OBJECT::LoadObject(GetDevice(), "ldbr.bgf");
|
||||||
|
|
||||||
|
//
|
||||||
|
// The burning-wreck FLAMES (the 1996 case-4 "fires" branch): flamesml +
|
||||||
|
// flamebig hung over the pile; flamebig is Y-BILLBOARDED toward the camera
|
||||||
|
// (dpl_SetDCSReorientAxes) and the fires DCS falls at -0.01 (slower than
|
||||||
|
// the hulk's -0.025, so the flames ride above the sinking pile). In this
|
||||||
|
// content build FLAMESML.BGF is an empty placeholder -- only flamebig
|
||||||
|
// carries geometry (btfx:firesmoke1_mtl, the scrolling fire-noise card,
|
||||||
|
// animated by the authored SCROLL now honoured in the BGF path).
|
||||||
|
//
|
||||||
|
d3d_OBJECT *flames = d3d_OBJECT::LoadObject(GetDevice(), "flamebig.bgf");
|
||||||
|
if (flames != NULL && flames->GetVertCount() == 0)
|
||||||
|
flames = NULL;
|
||||||
|
|
||||||
//
|
//
|
||||||
// Hide the body; hang the wreck pieces on the tree root (identity offset --
|
// Hide the body; hang the wreck pieces on the tree root (identity offset --
|
||||||
// the root renderable already pushes the wreck's localToWorld, so they sit
|
// the root renderable already pushes the wreck's localToWorld, so they sit
|
||||||
// at the mech's ground position with its death yaw).
|
// at the mech's ground position with its death yaw). All pieces start
|
||||||
|
// HIDDEN: the 1996 script's InstanceSwitchRenderables reveal them 0.25s
|
||||||
|
// after the boom (behind the dnboom flash) -- TickWreck runs the reveal.
|
||||||
//
|
//
|
||||||
for (std::map<int, HierarchicalDrawComponent*>::iterator r =
|
for (std::map<int, HierarchicalDrawComponent*>::iterator r =
|
||||||
render_tree.segRenderable.begin();
|
render_tree.segRenderable.begin();
|
||||||
@@ -959,21 +990,39 @@ void
|
|||||||
dpl_ISECT_MODE isect_mode;
|
dpl_ISECT_MODE isect_mode;
|
||||||
LinearMatrix identity(True);
|
LinearMatrix identity(True);
|
||||||
if (hulk != NULL)
|
if (hulk != NULL)
|
||||||
|
{
|
||||||
render_tree.wreckHulk = new DPLStaticChildRenderable(
|
render_tree.wreckHulk = new DPLStaticChildRenderable(
|
||||||
victim, false /* main zone */, hulk,
|
victim, false /* main zone */, hulk,
|
||||||
isect_mode, INTERSECT_ALL, identity, render_tree.rootRenderable);
|
isect_mode, INTERSECT_ALL, identity, render_tree.rootRenderable);
|
||||||
|
render_tree.wreckHulk->SetDrawObj(NULL); // hidden until the reveal
|
||||||
|
}
|
||||||
if (debris != NULL)
|
if (debris != NULL)
|
||||||
|
{
|
||||||
render_tree.wreckDebris = new DPLStaticChildRenderable(
|
render_tree.wreckDebris = new DPLStaticChildRenderable(
|
||||||
victim, false /* main zone */, debris,
|
victim, false /* main zone */, debris,
|
||||||
isect_mode, INTERSECT_ALL, identity, render_tree.rootRenderable);
|
isect_mode, INTERSECT_ALL, identity, render_tree.rootRenderable);
|
||||||
|
render_tree.wreckDebris->SetDrawObj(NULL);
|
||||||
|
}
|
||||||
|
if (flames != NULL)
|
||||||
|
{
|
||||||
|
render_tree.wreckFlames = new DPLStaticChildRenderable(
|
||||||
|
victim, false /* main zone */, flames,
|
||||||
|
isect_mode, INTERSECT_ALL, identity, render_tree.rootRenderable);
|
||||||
|
render_tree.wreckFlames->SetDrawObj(NULL);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
render_tree.wrecked = 1;
|
render_tree.wrecked = 1;
|
||||||
render_tree.wreckAge = 0.0f;
|
render_tree.wreckAge = 0.0f;
|
||||||
|
render_tree.wreckRevealed = 0;
|
||||||
|
render_tree.wreckHulkObj = hulk;
|
||||||
|
render_tree.wreckDebrisObj = debris;
|
||||||
|
render_tree.wreckFlamesObj = flames;
|
||||||
DEBUG_STREAM << "[BTrender] wreck swap: victim -> '"
|
DEBUG_STREAM << "[BTrender] wreck swap: victim -> '"
|
||||||
<< (hulk_name[0] ? hulk_name : "gendbr.bgf")
|
<< (hulk_name[0] ? hulk_name : "gendbr.bgf")
|
||||||
<< (hulk ? "'" : "' (LOAD FAILED -- body hidden only)")
|
<< (hulk ? "'" : "' (LOAD FAILED -- body hidden only)")
|
||||||
<< (debris ? " + ldbr debris" : "")
|
<< (debris ? " + ldbr debris" : "")
|
||||||
<< "\n" << std::flush;
|
<< (flames ? " + flamebig flames" : "")
|
||||||
|
<< " (reveal in 0.25s)\n" << std::flush;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1002,14 +1051,40 @@ int
|
|||||||
return 0; // already buried
|
return 0; // already buried
|
||||||
|
|
||||||
render_tree.wreckAge += dt;
|
render_tree.wreckAge += dt;
|
||||||
float sink = -0.025f * render_tree.wreckAge * render_tree.wreckAge; // the authored rate
|
|
||||||
|
//
|
||||||
|
// The 0.25s REVEAL (the 1996 InstanceSwitch delay): the pieces appear
|
||||||
|
// behind the dnboom flash, then MakeDCSFall arms and the burial starts.
|
||||||
|
//
|
||||||
|
const float kRevealDelay = 0.25f; // static_debris/fires_delay
|
||||||
|
if (!render_tree.wreckRevealed)
|
||||||
|
{
|
||||||
|
if (render_tree.wreckAge < kRevealDelay)
|
||||||
|
return 1;
|
||||||
|
render_tree.wreckRevealed = 1;
|
||||||
|
if (render_tree.wreckHulk)
|
||||||
|
render_tree.wreckHulk->SetDrawObj(render_tree.wreckHulkObj);
|
||||||
|
if (render_tree.wreckDebris)
|
||||||
|
render_tree.wreckDebris->SetDrawObj(render_tree.wreckDebrisObj);
|
||||||
|
if (render_tree.wreckFlames)
|
||||||
|
render_tree.wreckFlames->SetDrawObj(render_tree.wreckFlamesObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeDCSFall: offsetY = -1/2 g t^2 from the reveal; hulk/debris g=0.025,
|
||||||
|
// fires g=0.01 (the flames ride above the sinking pile).
|
||||||
|
float t = render_tree.wreckAge - kRevealDelay;
|
||||||
|
if (t < 0.0f) t = 0.0f;
|
||||||
|
float sink = -0.025f * t * t; // the authored hulk rate
|
||||||
if (sink < -8.0f)
|
if (sink < -8.0f)
|
||||||
{
|
{
|
||||||
// fully buried -> hide + stop ticking
|
// fully buried -> hide + stop ticking (the flames die with the pile:
|
||||||
|
// the original removed the whole death entity here)
|
||||||
if (render_tree.wreckHulk) render_tree.wreckHulk->SetDrawObj(NULL);
|
if (render_tree.wreckHulk) render_tree.wreckHulk->SetDrawObj(NULL);
|
||||||
if (render_tree.wreckDebris) render_tree.wreckDebris->SetDrawObj(NULL);
|
if (render_tree.wreckDebris) render_tree.wreckDebris->SetDrawObj(NULL);
|
||||||
|
if (render_tree.wreckFlames) render_tree.wreckFlames->SetDrawObj(NULL);
|
||||||
render_tree.wreckHulk = NULL;
|
render_tree.wreckHulk = NULL;
|
||||||
render_tree.wreckDebris = NULL;
|
render_tree.wreckDebris = NULL;
|
||||||
|
render_tree.wreckFlames = NULL;
|
||||||
DEBUG_STREAM << "[BTrender] wreck buried (sink complete)\n" << std::flush;
|
DEBUG_STREAM << "[BTrender] wreck buried (sink complete)\n" << std::flush;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -1017,6 +1092,26 @@ int
|
|||||||
render_tree.wreckHulk->SetOffsetTranslation(0.0f, sink, 0.0f);
|
render_tree.wreckHulk->SetOffsetTranslation(0.0f, sink, 0.0f);
|
||||||
if (render_tree.wreckDebris)
|
if (render_tree.wreckDebris)
|
||||||
render_tree.wreckDebris->SetOffsetTranslation(0.0f, sink, 0.0f);
|
render_tree.wreckDebris->SetOffsetTranslation(0.0f, sink, 0.0f);
|
||||||
|
if (render_tree.wreckFlames)
|
||||||
|
{
|
||||||
|
render_tree.wreckFlames->SetOffsetTranslation(0.0f, -0.01f * t * t, 0.0f);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Y-BILLBOARD the flame card at the camera (dpl_SetDCSReorientAxes
|
||||||
|
// analog). The parent DCS applies the victim's localToWorld (its
|
||||||
|
// death yaw), so the camera direction is taken in the victim's LOCAL
|
||||||
|
// frame: d_local = R^T * (cam - wreck) using the orthonormal rotation
|
||||||
|
// rows of localToWorld.
|
||||||
|
//
|
||||||
|
float cx, cy, cz;
|
||||||
|
d3d_OBJECT::GetCameraPosition(&cx, &cy, &cz);
|
||||||
|
float dx = cx - (float)victim->localOrigin.linearPosition.x;
|
||||||
|
float dz = cz - (float)victim->localOrigin.linearPosition.z;
|
||||||
|
float lx = dx * (float)victim->localToWorld(0, 0) + dz * (float)victim->localToWorld(0, 2);
|
||||||
|
float lz = dx * (float)victim->localToWorld(2, 0) + dz * (float)victim->localToWorld(2, 2);
|
||||||
|
if (lx * lx + lz * lz > 1e-6f)
|
||||||
|
render_tree.wreckFlames->SetOffsetYaw(atan2f(lx, lz));
|
||||||
|
}
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -679,7 +679,12 @@ extern void BTDrawReticle(struct IDirect3DDevice9 *device);
|
|||||||
int wrecked; // 1 = swapped to the <mech>dbr hulk
|
int wrecked; // 1 = swapped to the <mech>dbr hulk
|
||||||
DPLStaticChildRenderable *wreckHulk; // the <mech>dbr piece (sinks)
|
DPLStaticChildRenderable *wreckHulk; // the <mech>dbr piece (sinks)
|
||||||
DPLStaticChildRenderable *wreckDebris; // the ldbr scatter (sinks)
|
DPLStaticChildRenderable *wreckDebris; // the ldbr scatter (sinks)
|
||||||
|
DPLStaticChildRenderable *wreckFlames; // flamebig card (billboards, sinks slower)
|
||||||
float wreckAge; // seconds since the swap
|
float wreckAge; // seconds since the swap
|
||||||
|
int wreckRevealed; // 1 once the 0.25s reveal delay elapsed
|
||||||
|
d3d_OBJECT *wreckHulkObj; // pieces held hidden until the reveal
|
||||||
|
d3d_OBJECT *wreckDebrisObj;
|
||||||
|
d3d_OBJECT *wreckFlamesObj;
|
||||||
std::map<int, HierarchicalDrawComponent*> segRenderable; // slot -> joint renderable
|
std::map<int, HierarchicalDrawComponent*> segRenderable; // slot -> joint renderable
|
||||||
std::map<int, int> segGState; // slot -> last applied graphic state
|
std::map<int, int> segGState; // slot -> last applied graphic state
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -644,6 +644,7 @@ int gBTMissileTrigger = 0; // channel 3: missiles (key 3 / CTRL)
|
|||||||
static int gBTLaserKey = 0; // raw key states (set by the keyboard poll)
|
static int gBTLaserKey = 0; // raw key states (set by the keyboard poll)
|
||||||
static int gBTPPCKey = 0;
|
static int gBTPPCKey = 0;
|
||||||
static int gBTMissileKey = 0;
|
static int gBTMissileKey = 0;
|
||||||
|
static int gBTPinkyKey = 0; // key '4' = the pod's 4th fire button (Pinky 0x45)
|
||||||
static int gBTConfigKey = 0; // task #6: HOLD 'G' = the weapon-configure button
|
static int gBTConfigKey = 0; // task #6: HOLD 'G' = the weapon-configure button
|
||||||
static int gBTGenSelKey = 0; // task #12: F5..F8 = SelectGeneratorA..D, F9 = mode toggle
|
static int gBTGenSelKey = 0; // task #12: F5..F8 = SelectGeneratorA..D, F9 = mode toggle
|
||||||
// (0 idle; else the message id 4..8)
|
// (0 idle; else the message id 4..8)
|
||||||
@@ -786,35 +787,19 @@ void
|
|||||||
BTPushProjectile(const Point3D &muzzle, void *shooter, void *target, const Point3D &targetPos,
|
BTPushProjectile(const Point3D &muzzle, void *shooter, void *target, const Point3D &targetPos,
|
||||||
Scalar speed, Scalar damage, const Vector3D *launch_velocity, int guided)
|
Scalar speed, Scalar damage, const Vector3D *launch_velocity, int guided)
|
||||||
{
|
{
|
||||||
// The weapon's GetMuzzlePoint falls back to the mech ORIGIN (feet) when its mount
|
// MUZZLE (muzzle wave, 2026-07-12): the passed muzzle is now the weapon's
|
||||||
// segment doesn't resolve, so a projectile appeared to launch from the mech's feet.
|
// AUTHENTIC mount segment (GetMuzzlePoint reads the real segmentIndex --
|
||||||
// Resolve the real launch port by NAME off the shooter (same mechanism the visible
|
// the old raw-offset garbage that made it resolve feet/heads is fixed), so
|
||||||
// laser beams use for the gun ports), alternating the left/right missile ports for a
|
// the port-name rotation hack that compensated for it is RETIRED. Keep
|
||||||
// natural salvo look; fall back to a raised (torso-height) muzzle, then the passed one.
|
// one safety: a degenerate resolve at/below origin height (an unposed
|
||||||
|
// segment) launches from torso height instead of underground.
|
||||||
Point3D mz = muzzle;
|
Point3D mz = muzzle;
|
||||||
if (shooter != 0)
|
if (shooter != 0)
|
||||||
{
|
{
|
||||||
Mech *sm = (Mech *)shooter;
|
Mech *sm = (Mech *)shooter;
|
||||||
static const char *const kPorts[] =
|
if (mz.y - sm->localOrigin.linearPosition.y < 1.0f)
|
||||||
{ "sitermissleport", "sitelmissleport", "siterutorsoport", "sitelutorsoport",
|
|
||||||
"siterugunport", "sitelugunport" };
|
|
||||||
static int s_portRot = 0;
|
|
||||||
EntitySegment *seg = 0;
|
|
||||||
int n = (int)(sizeof(kPorts)/sizeof(kPorts[0]));
|
|
||||||
for (int k = 0; k < n && seg == 0; ++k)
|
|
||||||
{
|
{
|
||||||
s_portRot = (s_portRot + 1) % n;
|
mz = sm->localOrigin.linearPosition;
|
||||||
seg = sm->GetSegment(CString(kPorts[s_portRot]));
|
|
||||||
}
|
|
||||||
if (seg != 0)
|
|
||||||
{
|
|
||||||
AffineMatrix mw;
|
|
||||||
mw.Multiply(seg->GetSegmentToEntity(), sm->localToWorld); // port -> world
|
|
||||||
mz = mw; // W_Axis translation
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
mz = sm->localOrigin.linearPosition; // raise to torso height
|
|
||||||
mz.y += 12.0f;
|
mz.y += 12.0f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -834,8 +819,15 @@ void
|
|||||||
d.x = tpos.x - mz.x; d.y = tpos.y - mz.y; d.z = tpos.z - mz.z;
|
d.x = tpos.x - mz.x; d.y = tpos.y - mz.y; d.z = tpos.z - mz.z;
|
||||||
Scalar len = (Scalar)sqrtf(d.x*d.x + d.y*d.y + d.z*d.z);
|
Scalar len = (Scalar)sqrtf(d.x*d.x + d.y*d.y + d.z*d.z);
|
||||||
if (getenv("BT_PROJ_LOG"))
|
if (getenv("BT_PROJ_LOG"))
|
||||||
DEBUG_STREAM << "[projectile] PUSH target=" << (void*)target << " enemy=" << (void*)gEnemyMech
|
{
|
||||||
<< " len=" << len << " speed=" << speed << " dmg=" << damage << "\n" << std::flush;
|
Scalar relY = (shooter != 0)
|
||||||
|
? (mz.y - ((Mech *)shooter)->localOrigin.linearPosition.y) : mz.y;
|
||||||
|
DEBUG_STREAM << "[projectile] PUSH target=" << (void*)target
|
||||||
|
<< " len=" << len << " speed=" << speed << " dmg=" << damage
|
||||||
|
<< " mz=(" << mz.x << "," << mz.y << "," << mz.z << ") relY=" << relY
|
||||||
|
<< " lv=" << (launch_velocity
|
||||||
|
? "(auth)" : "(fallback)") << "\n" << std::flush;
|
||||||
|
}
|
||||||
if (len < 0.001f) return;
|
if (len < 0.001f) return;
|
||||||
for (int i = 0; i < 64; ++i)
|
for (int i = 0; i < 64; ++i)
|
||||||
{
|
{
|
||||||
@@ -854,14 +846,21 @@ void
|
|||||||
int haveLaunch = 0;
|
int haveLaunch = 0;
|
||||||
if (launch_velocity != 0 && shooter != 0)
|
if (launch_velocity != 0 && shooter != 0)
|
||||||
{
|
{
|
||||||
|
// FRAME CONVENTION (telemetry-verified 2026-07-12): the authored
|
||||||
|
// MuzzleVelocity is (0,0,+100) -- the launcher frame's +Z is
|
||||||
|
// FORWARD, but the mech's body frame faces -Z. Transforming +Z
|
||||||
|
// through the body basis launched every round BACKWARD (then the
|
||||||
|
// seeker looped it through the ground onto the target --
|
||||||
|
// user-reported). Map launcher-forward (+Z) onto body-forward
|
||||||
|
// (-Z basis): negate the z term.
|
||||||
Mech *sm = (Mech *)shooter;
|
Mech *sm = (Mech *)shooter;
|
||||||
UnitVector ax, ay, az;
|
UnitVector ax, ay, az;
|
||||||
sm->localToWorld.GetFromAxis(X_Axis, &ax);
|
sm->localToWorld.GetFromAxis(X_Axis, &ax);
|
||||||
sm->localToWorld.GetFromAxis(Y_Axis, &ay);
|
sm->localToWorld.GetFromAxis(Y_Axis, &ay);
|
||||||
sm->localToWorld.GetFromAxis(Z_Axis, &az);
|
sm->localToWorld.GetFromAxis(Z_Axis, &az);
|
||||||
p.vel.x = ax.x*launch_velocity->x + ay.x*launch_velocity->y + az.x*launch_velocity->z;
|
p.vel.x = ax.x*launch_velocity->x + ay.x*launch_velocity->y - az.x*launch_velocity->z;
|
||||||
p.vel.y = ax.y*launch_velocity->x + ay.y*launch_velocity->y + az.y*launch_velocity->z;
|
p.vel.y = ax.y*launch_velocity->x + ay.y*launch_velocity->y - az.y*launch_velocity->z;
|
||||||
p.vel.z = ax.z*launch_velocity->x + ay.z*launch_velocity->y + az.z*launch_velocity->z;
|
p.vel.z = ax.z*launch_velocity->x + ay.z*launch_velocity->y - az.z*launch_velocity->z;
|
||||||
Scalar lv = (Scalar)sqrtf(p.vel.x*p.vel.x + p.vel.y*p.vel.y + p.vel.z*p.vel.z);
|
Scalar lv = (Scalar)sqrtf(p.vel.x*p.vel.x + p.vel.y*p.vel.y + p.vel.z*p.vel.z);
|
||||||
if (lv > 1.0f) { p.speed = lv; haveLaunch = 1; }
|
if (lv > 1.0f) { p.speed = lv; haveLaunch = 1; }
|
||||||
}
|
}
|
||||||
@@ -882,6 +881,14 @@ void
|
|||||||
p.damage = damage;
|
p.damage = damage;
|
||||||
p.guided = guided; // autocannon shells fly straight (no seeker in the binary's plain Projectile)
|
p.guided = guided; // autocannon shells fly straight (no seeker in the binary's plain Projectile)
|
||||||
p.active = 1;
|
p.active = 1;
|
||||||
|
if (getenv("BT_PROJ_LOG"))
|
||||||
|
DEBUG_STREAM << "[projectile] vel=(" << p.vel.x << "," << p.vel.y
|
||||||
|
<< "," << p.vel.z << ")"
|
||||||
|
<< (launch_velocity
|
||||||
|
? " authored=(" : " none=(")
|
||||||
|
<< (launch_velocity ? launch_velocity->x : 0.0f) << ","
|
||||||
|
<< (launch_velocity ? launch_velocity->y : 0.0f) << ","
|
||||||
|
<< (launch_velocity ? launch_velocity->z : 0.0f) << ")\n" << std::flush;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1693,6 +1700,10 @@ void
|
|||||||
gBTLaserKey = focused && ((pAsync('1') | pAsync(0x20 /*VK_SPACE*/)) & dn) ? 1 : 0;
|
gBTLaserKey = focused && ((pAsync('1') | pAsync(0x20 /*VK_SPACE*/)) & dn) ? 1 : 0;
|
||||||
gBTPPCKey = focused && (pAsync('2') & dn) ? 1 : 0;
|
gBTPPCKey = focused && (pAsync('2') & dn) ? 1 : 0;
|
||||||
gBTMissileKey = focused && ((pAsync('3') | pAsync(0x11 /*VK_CONTROL*/)) & dn) ? 1 : 0;
|
gBTMissileKey = focused && ((pAsync('3') | pAsync(0x11 /*VK_CONTROL*/)) & dn) ? 1 : 0;
|
||||||
|
// The pod's 4TH fire button (Pinky, 0x45) -- previously
|
||||||
|
// unmapped on desktop, so any weapon the authored groups
|
||||||
|
// put there (the Avatar's NARC etc.) was unreachable.
|
||||||
|
gBTPinkyKey = focused && (pAsync('4') & dn) ? 1 : 0;
|
||||||
// task #6: HOLD 'G' opens the config session on the selected
|
// task #6: HOLD 'G' opens the config session on the selected
|
||||||
// weapon (BT_CONFIG_SLOT, default: the first weapon); while
|
// weapon (BT_CONFIG_SLOT, default: the first weapon); while
|
||||||
// held, the fire keys TOGGLE its group membership.
|
// held, the fire keys TOGGLE its group membership.
|
||||||
@@ -1711,7 +1722,7 @@ void
|
|||||||
gBTValveKey = focused && (pAsync('C') & dn) ? 1 : 0;
|
gBTValveKey = focused && (pAsync('C') & dn) ? 1 : 0;
|
||||||
// gBTDrive.fire = "any weapon trigger down" (feeds the bring-up
|
// gBTDrive.fire = "any weapon trigger down" (feeds the bring-up
|
||||||
// damage dispatcher + the beam-visual keepalive)
|
// damage dispatcher + the beam-visual keepalive)
|
||||||
gBTDrive.fire = (gBTLaserKey || gBTPPCKey || gBTMissileKey) ? 1 : 0;
|
gBTDrive.fire = (gBTLaserKey || gBTPPCKey || gBTMissileKey || gBTPinkyKey) ? 1 : 0;
|
||||||
static int sPrevX = 0;
|
static int sPrevX = 0;
|
||||||
const int xNow = focused && (pAsync('X') & dn) ? 1 : 0;
|
const int xNow = focused && (pAsync('X') & dn) ? 1 : 0;
|
||||||
if (xNow && !sPrevX) gBTDrive.allStop = 1; // edge -> one all-stop
|
if (xNow && !sPrevX) gBTDrive.allStop = 1; // edge -> one all-stop
|
||||||
@@ -1862,7 +1873,7 @@ void
|
|||||||
gBTDrive.fire = 0;
|
gBTDrive.fire = 0;
|
||||||
gBTDrive.fireForced = 0;
|
gBTDrive.fireForced = 0;
|
||||||
gBTDrive.forced = 0;
|
gBTDrive.forced = 0;
|
||||||
gBTLaserKey = gBTPPCKey = gBTMissileKey = 0;
|
gBTLaserKey = gBTPPCKey = gBTMissileKey = gBTPinkyKey = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
float throttle = gBTDrive.forced ? gBTDrive.forcedThrottle : gBTDrive.throttle;
|
float throttle = gBTDrive.forced ? gBTDrive.forcedThrottle : gBTDrive.throttle;
|
||||||
@@ -3251,7 +3262,7 @@ void
|
|||||||
};
|
};
|
||||||
int want[4];
|
int want[4];
|
||||||
want[0] = ((gBTLaserKey && targetInArc) || autofire) ? 1 : 0;
|
want[0] = ((gBTLaserKey && targetInArc) || autofire) ? 1 : 0;
|
||||||
want[1] = 0; // pinky: unmapped on desktop
|
want[1] = (gBTPinkyKey && targetInArc) ? 1 : 0; // key '4' (was unmapped)
|
||||||
want[2] = (gBTPPCKey && targetInArc) ? 1 : 0;
|
want[2] = (gBTPPCKey && targetInArc) ? 1 : 0;
|
||||||
want[3] = (gBTMissileKey && targetInArc) ? 1 : 0;
|
want[3] = (gBTMissileKey && targetInArc) ? 1 : 0;
|
||||||
if (cm != 0)
|
if (cm != 0)
|
||||||
|
|||||||
@@ -572,12 +572,16 @@ void
|
|||||||
void
|
void
|
||||||
MechWeapon::GetMuzzlePoint(Point3D &point)
|
MechWeapon::GetMuzzlePoint(Point3D &point)
|
||||||
{
|
{
|
||||||
// @004b9948 -- resolve the weapon's mount segment (its index is the inherited
|
// @004b9948 -- resolve the weapon's mount segment in the owning Mech's
|
||||||
// subsystem slot at this+0xdc, == binary param_1+0xdc) in the owning Mech's
|
// segment table and transform it to world (FUN_00424da8).
|
||||||
// segment table and transform it to world (FUN_00424da8). Reads live each call
|
// DATABINDING FIX (muzzle wave, 2026-07-12): the binary's this+0xdc IS the
|
||||||
// (the phantom cached `muzzlePoint` member is removed -- it collided with the
|
// engine Subsystem's segmentIndex -- but the old raw `*(this+0xdc)` read
|
||||||
// Emitter subclass's own fields at 0x3F0+).
|
// OUR compiled layout at that offset (garbage), so every weapon resolved a
|
||||||
int segIndex = *(int *)((char *)this + 0xdc); // inherited segment index (byte-exact base)
|
// RANDOM segment or none: missiles launched from heads/feet/below terrain
|
||||||
|
// (user-hit on the Avatar) and the beam/missile paths grew gun-port
|
||||||
|
// name-lookup hacks to compensate. GetSegmentIndex() is the named member
|
||||||
|
// [T0 SUBSYSTM.h:108] -- authentic mounts on every chassis.
|
||||||
|
int segIndex = GetSegmentIndex();
|
||||||
BTResolveWeaponMuzzle(owner, segIndex, point); // mech4.cpp bridge (null-guarded)
|
BTResolveWeaponMuzzle(owner, segIndex, point); // mech4.cpp bridge (null-guarded)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -196,6 +196,18 @@ Logical
|
|||||||
// AmmoBin::FeedAmmo). Returns True when a round was dispensed; otherwise raises
|
// AmmoBin::FeedAmmo). Returns True when a round was dispensed; otherwise raises
|
||||||
// the NoAmmo alarm and returns False. Shared by both FireWeapon spawn paths.
|
// the NoAmmo alarm and returns False. Shared by both FireWeapon spawn paths.
|
||||||
//
|
//
|
||||||
|
// Panel bridge (AFC100-counter fix, 2026-07-12): resolve the weapon's ammo
|
||||||
|
// bin through the TYPED connection -- the gauge cluster's hand-rolled raw walk
|
||||||
|
// (*(plug+8) twice at subsystem+0x43c) assumed the 1995 connection-object
|
||||||
|
// internals and returned NULL/garbage on our layout, so the panel's ammo
|
||||||
|
// digits never bound (fired shells consumed rounds invisibly).
|
||||||
|
void *
|
||||||
|
BTWeaponAmmoBin(void *weapon)
|
||||||
|
{
|
||||||
|
return (weapon != 0)
|
||||||
|
? (void *)((ProjectileWeapon *)weapon)->ammoBinLink.Resolve() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
Logical
|
Logical
|
||||||
ProjectileWeapon::ConsumeRound()
|
ProjectileWeapon::ConsumeRound()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -320,6 +320,7 @@ class NotationFile;
|
|||||||
int tracerInterval; // @0x438 resource TracerInterval (+0x1BC)
|
int tracerInterval; // @0x438 resource TracerInterval (+0x1BC)
|
||||||
AmmoBinConnection
|
AmmoBinConnection
|
||||||
ammoBinLink; // @0x43C embedded 0xC connection to the AmmoBin subsystem
|
ammoBinLink; // @0x43C embedded 0xC connection to the AmmoBin subsystem
|
||||||
|
friend void *BTWeaponAmmoBin(void *weapon); // panel ammo-bin bridge (AFC100-counter fix)
|
||||||
// (ctor @004bcbb0, dtor @004bcbcf, link vtable
|
// (ctor @004bcbb0, dtor @004bcbcf, link vtable
|
||||||
// @00512424; resolved via the resource AmmoBin
|
// @00512424; resolved via the resource AmmoBin
|
||||||
// index at +0x1C0) -- LAST field, object ends 0x448
|
// index at +0x1C0) -- LAST field, object ends 0x448
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 7.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Reference in New Issue
Block a user