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:
arcattack
2026-07-12 20:04:29 -05:00
co-authored by Claude Fable 5
parent bb795e2805
commit 48c9c8444f
23 changed files with 701 additions and 109 deletions
+15
View File
@@ -170,6 +170,21 @@ AmmoBin::~AmmoBin()
// to Feeding, reloads the feed timer to feedRate, decrements ammoCount (clamped
// 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()
{
if (simulationState == 1) // this+0x40 == 1 (destroyed)
+2
View File
@@ -78,6 +78,8 @@
public HeatWatcher
{
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
//
+17 -5
View File
@@ -1673,7 +1673,11 @@ void WeaponCluster::Execute()
if (getenv("BT_PANEL_LOG"))
{
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 : "?")
<< " percentDone=" << percentDone
<< " warn=" << warningState << "\n" << std::flush;
@@ -1751,11 +1755,18 @@ BallisticWeaponCluster::BallisticWeaponCluster(
reloadSeconds = 0.0f; // @0x3E
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);
// Ammo count off the bin (+0x180); BEST-EFFORT raw if the bin resolves.
int *ammoValue = (ammoBin != NULL) ? (int *)((char *)ammoBin + 0x180) : NULL;
// Ammo count via the complete-type bridge (Streak-gauge fix, 2026-07-12):
// 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
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)
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;
if (reloadState == 0)
{
+101 -6
View File
@@ -802,10 +802,16 @@ void
//
if (render_tree.wreckHulk != NULL) render_tree.wreckHulk->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.wreckDebris = NULL;
render_tree.wreckFlames = NULL;
render_tree.wrecked = 0;
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
@@ -929,6 +935,16 @@ void
}
d3d_OBJECT *hulk = (hulk_name[0] != '\0')
? 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)
{
DEBUG_STREAM << "[BTrender] wreck: '" << hulk_name
@@ -942,10 +958,25 @@ void
//
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 --
// 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 =
render_tree.segRenderable.begin();
@@ -959,21 +990,39 @@ void
dpl_ISECT_MODE isect_mode;
LinearMatrix identity(True);
if (hulk != NULL)
{
render_tree.wreckHulk = new DPLStaticChildRenderable(
victim, false /* main zone */, hulk,
isect_mode, INTERSECT_ALL, identity, render_tree.rootRenderable);
render_tree.wreckHulk->SetDrawObj(NULL); // hidden until the reveal
}
if (debris != NULL)
{
render_tree.wreckDebris = new DPLStaticChildRenderable(
victim, false /* main zone */, debris,
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.wreckAge = 0.0f;
render_tree.wrecked = 1;
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 -> '"
<< (hulk_name[0] ? hulk_name : "gendbr.bgf")
<< (hulk ? "'" : "' (LOAD FAILED -- body hidden only)")
<< (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
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)
{
// 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.wreckDebris) render_tree.wreckDebris->SetDrawObj(NULL);
if (render_tree.wreckFlames) render_tree.wreckFlames->SetDrawObj(NULL);
render_tree.wreckHulk = NULL;
render_tree.wreckDebris = NULL;
render_tree.wreckFlames = NULL;
DEBUG_STREAM << "[BTrender] wreck buried (sink complete)\n" << std::flush;
return 0;
}
@@ -1017,6 +1092,26 @@ int
render_tree.wreckHulk->SetOffsetTranslation(0.0f, sink, 0.0f);
if (render_tree.wreckDebris)
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;
}
+5
View File
@@ -679,7 +679,12 @@ extern void BTDrawReticle(struct IDirect3DDevice9 *device);
int wrecked; // 1 = swapped to the <mech>dbr hulk
DPLStaticChildRenderable *wreckHulk; // the <mech>dbr piece (sinks)
DPLStaticChildRenderable *wreckDebris; // the ldbr scatter (sinks)
DPLStaticChildRenderable *wreckFlames; // flamebig card (billboards, sinks slower)
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, int> segGState; // slot -> last applied graphic state
};
+43 -32
View File
@@ -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 gBTPPCKey = 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 gBTGenSelKey = 0; // task #12: F5..F8 = SelectGeneratorA..D, F9 = mode toggle
// (0 idle; else the message id 4..8)
@@ -786,35 +787,19 @@ void
BTPushProjectile(const Point3D &muzzle, void *shooter, void *target, const Point3D &targetPos,
Scalar speed, Scalar damage, const Vector3D *launch_velocity, int guided)
{
// The weapon's GetMuzzlePoint falls back to the mech ORIGIN (feet) when its mount
// segment doesn't resolve, so a projectile appeared to launch from the mech's feet.
// Resolve the real launch port by NAME off the shooter (same mechanism the visible
// laser beams use for the gun ports), alternating the left/right missile ports for a
// natural salvo look; fall back to a raised (torso-height) muzzle, then the passed one.
// MUZZLE (muzzle wave, 2026-07-12): the passed muzzle is now the weapon's
// AUTHENTIC mount segment (GetMuzzlePoint reads the real segmentIndex --
// the old raw-offset garbage that made it resolve feet/heads is fixed), so
// the port-name rotation hack that compensated for it is RETIRED. Keep
// one safety: a degenerate resolve at/below origin height (an unposed
// segment) launches from torso height instead of underground.
Point3D mz = muzzle;
if (shooter != 0)
{
Mech *sm = (Mech *)shooter;
static const char *const kPorts[] =
{ "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)
if (mz.y - sm->localOrigin.linearPosition.y < 1.0f)
{
s_portRot = (s_portRot + 1) % n;
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 = sm->localOrigin.linearPosition;
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;
Scalar len = (Scalar)sqrtf(d.x*d.x + d.y*d.y + d.z*d.z);
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;
for (int i = 0; i < 64; ++i)
{
@@ -854,14 +846,21 @@ void
int haveLaunch = 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;
UnitVector ax, ay, az;
sm->localToWorld.GetFromAxis(X_Axis, &ax);
sm->localToWorld.GetFromAxis(Y_Axis, &ay);
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.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.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.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);
if (lv > 1.0f) { p.speed = lv; haveLaunch = 1; }
}
@@ -882,6 +881,14 @@ void
p.damage = damage;
p.guided = guided; // autocannon shells fly straight (no seeker in the binary's plain Projectile)
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;
}
}
@@ -1693,6 +1700,10 @@ void
gBTLaserKey = focused && ((pAsync('1') | pAsync(0x20 /*VK_SPACE*/)) & dn) ? 1 : 0;
gBTPPCKey = focused && (pAsync('2') & 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
// weapon (BT_CONFIG_SLOT, default: the first weapon); while
// held, the fire keys TOGGLE its group membership.
@@ -1711,7 +1722,7 @@ void
gBTValveKey = focused && (pAsync('C') & dn) ? 1 : 0;
// gBTDrive.fire = "any weapon trigger down" (feeds the bring-up
// 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;
const int xNow = focused && (pAsync('X') & dn) ? 1 : 0;
if (xNow && !sPrevX) gBTDrive.allStop = 1; // edge -> one all-stop
@@ -1862,7 +1873,7 @@ void
gBTDrive.fire = 0;
gBTDrive.fireForced = 0;
gBTDrive.forced = 0;
gBTLaserKey = gBTPPCKey = gBTMissileKey = 0;
gBTLaserKey = gBTPPCKey = gBTMissileKey = gBTPinkyKey = 0;
}
float throttle = gBTDrive.forced ? gBTDrive.forcedThrottle : gBTDrive.throttle;
@@ -3251,7 +3262,7 @@ void
};
int want[4];
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[3] = (gBTMissileKey && targetInArc) ? 1 : 0;
if (cm != 0)
+10 -6
View File
@@ -572,12 +572,16 @@ void
void
MechWeapon::GetMuzzlePoint(Point3D &point)
{
// @004b9948 -- resolve the weapon's mount segment (its index is the inherited
// subsystem slot at this+0xdc, == binary param_1+0xdc) in the owning Mech's
// segment table and transform it to world (FUN_00424da8). Reads live each call
// (the phantom cached `muzzlePoint` member is removed -- it collided with the
// Emitter subclass's own fields at 0x3F0+).
int segIndex = *(int *)((char *)this + 0xdc); // inherited segment index (byte-exact base)
// @004b9948 -- resolve the weapon's mount segment in the owning Mech's
// segment table and transform it to world (FUN_00424da8).
// DATABINDING FIX (muzzle wave, 2026-07-12): the binary's this+0xdc IS the
// engine Subsystem's segmentIndex -- but the old raw `*(this+0xdc)` read
// OUR compiled layout at that offset (garbage), so every weapon resolved a
// 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)
}
+12
View File
@@ -196,6 +196,18 @@ Logical
// AmmoBin::FeedAmmo). Returns True when a round was dispensed; otherwise raises
// 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
ProjectileWeapon::ConsumeRound()
{
+1
View File
@@ -320,6 +320,7 @@ class NotationFile;
int tracerInterval; // @0x438 resource TracerInterval (+0x1BC)
AmmoBinConnection
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
// @00512424; resolved via the resource AmmoBin
// index at +0x1C0) -- LAST field, object ends 0x448