Reticle Execute @004cdcf0 RECOVERED: every HUD instrument now live (task #37)

The one un-exported gap in the reticle chain, read via capstone
(tools/disas2.py; the annotated disasm preserved at
reference/decomp/reticle_execute_004cdcf0.disasm.txt). Draw() is now a
transcription of the real per-frame logic, and the HUD attr-table names
(hud.hpp ids 4/5/6/8/A/B/C/D) are CONFIRMED by their Execute usage:

- Range ladder: BAR from ladder-top to the caret + caret translate.
- The bottom 21-tick tape is the TORSO-TWIST indicator (NOT heading):
  deflection = -/+(span/2) x (RotationOfTorsoHorizontal / twist limit),
  attrs 4/5/6. Fixed-torso BLH reads centred -- authentic static.
- The circle-with-stem is the COMPASS (attr 0xD, rad->deg rotation) at
  (botX, botY - 3*tickMajor - 0.03), with the THREAT trail (attr 0xC)
  in its rotated frame: 0.05-unit attack-direction marks, fresh < 2s
  red, expiring at 6s. Port feed: player TakeDamage pushes the impact
  direction (dormant vs the passive test dummy).
- Pips (composed into subB6): hidden when destroyed (attr 1 == 1), LIT
  when the fire cycle is LOADED (attr 0x1c == 2; port source
  rechargeLevel >= 1) else the dark charging ring; filtered by the
  weapon-GROUP bits (weaponMode & elementMask&0xF). Range plays NO part
  (the stored TargetWithinRange slots are never read by Execute).
- Lock ring: subB9 at frame centre SPINNING 4 deg/frame while locked
  ([0x9d] is the spin matrix, not a heading list).
- Target HOTBOX: a rectangle hugging the projected extents (x+-4 around
  the top-centre hotbox point, +1/-11.5 vertical; baked K=2.8145 -- the
  port projects through the live per-axis projection), switching to the
  edge arrows past +-1.6 or behind (BTProjectHotBox, L4VIDEO).
- Reticle state Off/On + PrimaryHudOn full-HUD/simple-X switch, aim
  translate on slew move, 3D marker + PNAME player-name mesh identified
  (chain still deferred).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-08 22:38:50 -05:00
co-authored by Claude Fable 5
parent cf9f56044e
commit 18d49491b7
10 changed files with 1677 additions and 108 deletions
+263 -50
View File
@@ -61,6 +61,7 @@
#include <string.h>
#include <math.h>
#include <time.h> // clock() -- the threat-trail ages (task #37)
//
@@ -1043,6 +1044,9 @@ BTReticleRenderable::BTReticleRenderable(Entity *entity, Scalar *range_attr)
maxRange = kRetMaxRange; minRange = 0.0f;
rangeScale= maxRange - minRange; // [0x8d] -- AddWeapon divides by it
rangeAttr2= range_attr;
pipsBuilt = 0; // recovered-Execute dynamic state
lockShown = 0;
lockSpinDeg= 0.0f;
masterList = dpl2d_NewDisplayList();
aimDotList = dpl2d_NewDisplayList();
@@ -1136,19 +1140,25 @@ BTReticleRenderable::BTReticleRenderable(Entity *entity, Scalar *range_attr)
dpl2d_ClosePolyline(m);
dpl2d_PopState(m);
}
// bottom-anchor group: a small arc + drop line under the centre
// COMPASS group (Execute @4ce6e0-4ce7e4 [T1]): [0x278] bottomAnchor holds a
// rotate(CompassHeading rad->deg) + translate to (botX, botY - 3*tickMajor
// - 0.03) -- the compass rose (circle + north stem, authored at the origin)
// spins with the mech heading at the bottom-left of the twist tape. The
// THREAT trail [0x2e8] draws in the same frame: 0.05-unit direction marks
// from the compass centre toward recent damage sources (fresh = red, aging
// = yellow, expired at 6s -- Execute @4ce3e2-4ce6ce [T1]).
dpl2d_PushState(m);
dpl2d_CallList(m, bottomAnchor);
dpl2d_Circle(m, 0.0f, 0.0f, 0.03f, 0); // 0x3cf5c28f-family arc [T3 radius]
dpl2d_CallList(m, bottomAnchor); // [0x9e] the compass rotate+translate
dpl2d_Circle(m, 0.0f, 0.0f, 0.03f, 0); // the rose ring [T3 radius]
dpl2d_OpenLines(m);
dpl2d_AddPoint(m, 0.0f, -0.04f);
dpl2d_AddPoint(m, 0.0f, -0.04f); // the north stem
dpl2d_AddPoint(m, 0.0f, -0.005f);
dpl2d_CloseLines(m);
dpl2d_CallList(m, subBA);
dpl2d_CallList(m, subBA); // [0xba] the threat-direction trail
dpl2d_PopState(m);
dpl2d_CallList(m, subB6);
dpl2d_CallList(m, headingList);
dpl2d_CallList(m, subB7);
dpl2d_CallList(m, subB6); // [0xb6] the composed weapon pips
dpl2d_CallList(m, headingList); // [0x9d] the LOCK-RING SPIN matrix (4 deg/frame)
dpl2d_CallList(m, subB7); // [0xb7] the lock-ring slot (subB9 when locked)
dpl2d_End(m);
dpl2d_Compile(m);
@@ -1230,14 +1240,24 @@ void
if (range < minRange) range = minRange;
if (range > maxRange) range = maxRange;
Scalar frac = (range - minRange) / (maxRange - minRange);
Scalar six[6] = { 1, 0, 0, 1, 0, -scaleY * frac };
// [0x26c] = the range BAR (a line from the ladder TOP down to the caret
// height) + the caret translate (Execute @4ceb16-4cebf8: AddPoint(originX,
// originY-scaleY), AddPoint(originX, originY-scaleY*frac), then the
// SetMatrix(translate(0, -scaleY*frac)) the caret triangles ride).
dpl2d_Begin(rangeCaretR, 1);
dpl2d_ConcatMatrix(rangeCaretR, six);
dpl2d_OpenLines(rangeCaretR);
dpl2d_AddPoint(rangeCaretR, originX, originY - scaleY);
dpl2d_AddPoint(rangeCaretR, originX, originY - scaleY * frac);
dpl2d_CloseLines(rangeCaretR);
{
Scalar six[6] = { 1, 0, 0, 1, 0, -scaleY * frac };
dpl2d_ConcatMatrix(rangeCaretR, six);
}
dpl2d_End(rangeCaretR);
dpl2d_Compile(rangeCaretR);
// the AIM translate [0x9a] (task #36): the slewed crosshair position, in
// reticle coordinates (fed by the mouse / stick free-aim in mech4).
// the AIM translate [0x9a] (Execute @4cde59-4cdedd [T1]: rebuilt on slew
// move with SetMatrix(translate(reticlePosition))).
{
extern float gBTAimX, gBTAimY;
Scalar t6[6] = { 1, 0, 0, 1, gBTAimX, gBTAimY };
@@ -1247,32 +1267,182 @@ void
dpl2d_Compile(aimDotList);
}
// the TARGET DESIGNATOR slot [0xa1] (task #36, Reticle::TargetDesignatorOn
// [T0]): the designated target's projected screen point gets the lock ring
// (subB9 ring+cross while the crosshair is ON it, subB8 plain ring while
// designated); off-screen / behind, the edge turn ARROW points the way back.
// the TORSO-TWIST TAPE carets [0x9c] (Execute @4ce7e5-4cea9a [T1]): the
// bottom 21-tick tape is the TWIST indicator -- a deflection line from the
// tape centre plus the over/under carets translated by
// offset = -/+(span/2) * (RotationOfTorsoHorizontal / twist limit)
// (attrs 4/5/6: the live twist over the per-side limits; full deflection =
// the torso hard against its stop). The fixed-torso BLH reads 0 (centred).
{
extern int gBTHudLockState; // 0 none / 1 designated / 2 hot
extern float gBTHudLockWorld[3];
extern int BTProjectToReticle(const float world[3], float *rx, float *ry);
extern float gBTHudTwist, gBTHudTwistLimit;
float off = 0.0f;
if (gBTHudTwistLimit > 1e-4f)
{
off = -(kRetBotSpan * 0.5f) * (gBTHudTwist / gBTHudTwistLimit);
if (off < -kRetBotSpan * 0.5f) off = -kRetBotSpan * 0.5f;
if (off > kRetBotSpan * 0.5f) off = kRetBotSpan * 0.5f;
}
const float cx = kRetBotX + kRetBotSpan * 0.5f;
dpl2d_Begin(rangeCaretB, 1);
dpl2d_OpenLines(rangeCaretB);
dpl2d_AddPoint(rangeCaretB, cx, kRetBotY);
dpl2d_AddPoint(rangeCaretB, cx + off, kRetBotY);
dpl2d_CloseLines(rangeCaretB);
{
Scalar t6[6] = { 1, 0, 0, 1, off, 0 };
dpl2d_ConcatMatrix(rangeCaretB, t6);
}
dpl2d_End(rangeCaretB);
dpl2d_Compile(rangeCaretB);
}
// the COMPASS rotate [0x9e] (Execute @4ce6e0-4ce7e4 [T1]): the rose spins
// by CompassHeading (radians; the binary converts x57.2958 for its degree
// recorder) and sits at (botX, botY - 3*tickMajor - 0.03).
{
extern float gBTHudHeading;
const float c = (float)cos((double)gBTHudHeading);
const float s = (float)sin((double)gBTHudHeading);
Scalar r6[6] = { c, s, -s, c,
kRetBotX, kRetBotY - 3.0f * kRetTickMajor - 0.03f };
dpl2d_Begin(bottomAnchor, 1);
dpl2d_ConcatMatrix(bottomAnchor, r6);
dpl2d_End(bottomAnchor);
dpl2d_Compile(bottomAnchor);
}
// the THREAT trail [0xba] (Execute @4ce3e2-4ce6ce [T1]): direction marks
// from the compass centre toward recent damage sources. Each mark is a
// 0.05-unit line along the (mech-local x,z) attack direction; FRESH marks
// (< 2s) draw red, aging ones yellow, expired (> 6s) drop.
{
extern int BTTakeHudThreats(float out_xz[][2], float out_age[], int max_n);
float txz[16][2]; float tage[16];
const int n = BTTakeHudThreats(txz, tage, 16);
dpl2d_Begin(subBA, 1);
if (n > 0)
{
// stale (yellow) first, then fresh (red) -- the binary's color split
dpl2d_SetColor(subBA, 0.75f, 0.75f, 0.0f);
for (int pass = 0; pass < 2; ++pass)
{
const int wantFresh = (pass == 1);
if (pass == 1)
dpl2d_SetColor(subBA, 0.75f, 0.0f, 0.0f);
for (int i = 0; i < n; ++i)
{
const int isFresh = (tage[i] < 2.0f);
if (isFresh != wantFresh)
continue;
dpl2d_OpenLines(subBA);
dpl2d_AddPoint(subBA, 0.0f, 0.0f);
dpl2d_AddPoint(subBA, txz[i][0] * 0.05f, txz[i][1] * 0.05f);
dpl2d_CloseLines(subBA);
}
}
dpl2d_SetColor(subBA, 0.0f, 0.75f, 0.0f); // restore green
}
dpl2d_End(subBA);
dpl2d_Compile(subBA);
}
// the WEAPON PIPS [0xb6] (Execute @4ce2c2-4ce3e1 [T1]): the composed pip
// list the master calls. Per weapon: skip unless its GROUP is displayed
// (weaponMode & elementMask low bits); HIDE it when destroyed (attr 1 ==
// 1); the LIT pip (A) when the fire cycle is LOADED (attr 0x1c == 2, our
// source: rechargeLevel >= 1), else the dark ring (B, charging). Range
// plays NO part -- the binary never reads TargetWithinRange here.
{
extern int gBTHudGroupMask; // element-mask low bits (0xF = all)
int dirty = 0;
for (int i = 0; i < weaponCount; ++i)
{
const int destroyed = (simStateAttr[i] != 0 && *simStateAttr[i] == 1);
const int loaded = (cycleReady[i] != 0 && *cycleReady[i] >= 0.999f);
if (destroyed != simStateCache[i] || loaded != alarmCache[i])
{
simStateCache[i] = destroyed;
alarmCache[i] = loaded;
dirty = 1;
}
}
static int s_lastMask = -1;
if (gBTHudGroupMask != s_lastMask) { s_lastMask = gBTHudGroupMask; dirty = 1; }
if (dirty || !pipsBuilt)
{
pipsBuilt = 1;
dpl2d_Begin(subB6, 1);
for (int i = 0; i < weaponCount; ++i)
{
if ((weaponMode[i] & gBTHudGroupMask) == 0)
continue; // group not displayed
if (simStateCache[i])
continue; // destroyed: no pip at all
dpl2d_CallList(subB6,
alarmCache[i] ? pipDisplayListA[i] : pipDisplayListB[i]);
}
dpl2d_End(subB6);
dpl2d_Compile(subB6);
}
}
// the LOCK RING [0xb7] + its SPIN [0x9d] (Execute @4cebf9-4cee54 [T1]):
// while a target is locked the ring+cross (subB9) draws at the reticle
// frame centre, spinning 4 degrees per frame; unlocked it clears. (The
// binary also hangs the PNAMEx.bgf player-name mesh on the 3D marker here
// -- the 3D marker chain remains deferred.)
{
extern int gBTHudLockState;
const int locked = (gBTHudLockState != 0);
if (locked != lockShown)
{
lockShown = locked;
dpl2d_Begin(subB7, 1);
if (locked)
dpl2d_CallList(subB7, subB9);
dpl2d_End(subB7);
dpl2d_Compile(subB7);
}
if (locked)
{
lockSpinDeg += 4.0f; // per FRAME, the binary's rate
if (lockSpinDeg >= 360.0f) lockSpinDeg -= 360.0f;
const float a = lockSpinDeg * 0.0174532925f;
const float c = (float)cos((double)a), s = (float)sin((double)a);
Scalar r6[6] = { c, s, -s, c, 0, 0 };
dpl2d_Begin(headingList, 1);
dpl2d_ConcatMatrix(headingList, r6);
dpl2d_End(headingList);
dpl2d_Compile(headingList);
}
}
// the TARGET HOTBOX / edge arrows [0xa1] (Execute @4cdf6f-4ce28b [T1]):
// the box is a RECTANGLE hugging the target's projected extents -- x +-4
// around the hotbox point, +1 above / -11.5 below it (the authored pod
// mech envelope) -- switching to the left/right edge arrow when both
// edges pass +-1.6 or the target is behind.
{
extern int gBTHudLockState;
extern float gBTHudLockWorld[3]; // the target's hotbox point (top)
extern int BTProjectHotBox(const float top[3], float *xl, float *xr,
float *yt, float *yb, int *side);
dpl2d_Begin(crossList, 1);
if (gBTHudLockState != 0)
{
float tx = 0.0f, ty = 0.0f;
const int onScreen = BTProjectToReticle(gBTHudLockWorld, &tx, &ty);
const float xLimit = 1.55f; // past the ladders = off-screen
if (onScreen && tx > -xLimit && tx < xLimit && ty > -1.0f && ty < 1.0f)
float xl, xr, yt, yb; int side;
if (BTProjectHotBox(gBTHudLockWorld, &xl, &xr, &yt, &yb, &side))
{
Scalar d6[6] = { 1, 0, 0, 1, tx, ty };
dpl2d_PushState(crossList);
dpl2d_ConcatMatrix(crossList, d6);
dpl2d_CallList(crossList, (gBTHudLockState == 2) ? subB9 : subB8);
dpl2d_PopState(crossList);
dpl2d_OpenPolyline(crossList); // the closed hotbox rectangle
dpl2d_AddPoint(crossList, xl, yt);
dpl2d_AddPoint(crossList, xl, yb);
dpl2d_AddPoint(crossList, xr, yb);
dpl2d_AddPoint(crossList, xr, yt);
dpl2d_ClosePolyline(crossList);
}
else
{
dpl2d_CallList(crossList, (tx < 0.0f) ? leftArrow : rightArrow);
dpl2d_CallList(crossList, (side < 0) ? leftArrow : rightArrow);
}
}
dpl2d_End(crossList);
@@ -1280,14 +1450,6 @@ void
}
dpl2d_ExecuteList(masterList, device);
for (int i = 0; i < weaponCount; ++i)
{
int inRange = 0;
if (withinRangePtr[i] != 0) // [0x18c] TargetWithinRange
inRange = (*withinRangePtr[i] != 0);
dpl2d_ExecuteList(inRange ? pipDisplayListA[i] : pipDisplayListB[i], device);
}
}
//
@@ -1338,11 +1500,11 @@ BTReticleRenderable *BTBuildReticle(Entity *mech)
(int *)wp->WithinRangePtr(),
wp->PipExtendedRange(),
r, g, b,
(int *)wp->SimulationStatePtr(), // attr 0x1c "SimulationState"
wp->RechargeLevelPtr(), // attr 0x1c analog: loaded when >= 1
2, 3,
(int *)wp->SimulationStatePtr(), // attr 1 "SimulationState"
(int *)wp->SimulationStatePtr(), // attr 1: damage state (1 = destroyed)
1,
1 /* front (RearFiring==0 on every BLH weapon) */);
1 /* Front group (RearFiring==0 on every BLH weapon) */);
}
DEBUG_STREAM << "[hud] reticle built: " << gBTReticle->WeaponCount()
<< " weapon pip(s) registered\n" << std::flush;
@@ -1352,6 +1514,56 @@ BTReticleRenderable *BTBuildReticle(Entity *mech)
// the live target-range storage the reticle's caret binds to
Scalar gBTHudRangeStorage = 0.0f;
//
// THREAT trail store (recovered Execute @4ce3e2-4ce6ce): timestamped attack
// directions pushed on player damage (mech.cpp handler); Draw ages them --
// fresh < 2s (red), expired > 6s (dropped). World-frame (x,z) directions:
// they draw inside the compass's rotated frame, so the marks stay
// world-referenced on the rose like a true compass bearing.
//
struct BTHudThreat { float x, z; clock_t born; };
static BTHudThreat gBTHudThreats[16];
static int gBTHudThreatCount = 0;
void BTPushHudThreat(float wx, float wz)
{
float len = sqrtf(wx * wx + wz * wz);
if (len < 1e-4f)
return;
if (gBTHudThreatCount >= 16) // drop the oldest
{
for (int i = 1; i < 16; ++i)
gBTHudThreats[i - 1] = gBTHudThreats[i];
gBTHudThreatCount = 15;
}
BTHudThreat &t = gBTHudThreats[gBTHudThreatCount++];
t.x = wx / len;
t.z = wz / len;
t.born = clock();
}
int BTTakeHudThreats(float out_xz[][2], float out_age[], int max_n)
{
const clock_t now = clock();
int n = 0;
for (int i = 0; i < gBTHudThreatCount; ++i)
{
const float age = (float)(now - gBTHudThreats[i].born) / (float)CLOCKS_PER_SEC;
if (age > 6.0f)
continue; // expired
gBTHudThreats[n] = gBTHudThreats[i]; // compact in place
if (n < max_n)
{
out_xz[n][0] = gBTHudThreats[n].x;
out_xz[n][1] = gBTHudThreats[n].z;
out_age[n] = age;
}
++n;
}
gBTHudThreatCount = n;
return (n < max_n) ? n : max_n;
}
//
//#############################################################################
// BTReticleRenderable::AddWeapon
@@ -1373,12 +1585,12 @@ void
Scalar pip_red, // param_6..8 PipColor
Scalar pip_green,
Scalar pip_blue,
int *alarm_value, // param_9 weapon attr 0x1c
int const2, // param_10 (constant 2)
int const3, // param_11 (constant 3)
Scalar *cycle_ready, // param_9 (port: rechargeLevel)
int const2, // param_10 (2 = loaded)
int const3, // param_11 (3 = charging)
int *sim_state_value, // param_12 weapon attr 1
int const1, // param_13 (constant 1)
int weapon_mode) // param_14 (1 front / 2 rear)
int const1, // param_13 (1 = destroyed)
int weapon_mode) // param_14 (group bit)
{
if (this->weaponCount /* [0x38] */ >= 10)
{
@@ -1389,16 +1601,17 @@ void
//
// Record this weapon's control state in the parallel arrays -- the exact
// store order of @004cdac0 (part_014.c:4827-4837). The binary read the
// two attr objects' values at +0x14; the port takes value pointers direct.
// store order of @004cdac0 (part_014.c:4827-4837). The caches hold the
// pip's DERIVED display flags (loaded / destroyed) for the Execute-style
// change detection in Draw.
//
this->stateConst3[n] /* [0x64+n*4] = param_11 */ = const3;
this->stateConst2[n] /* [0x8c+n*4] = param_10 */ = const2;
this->stateConst1[n] /* [0xb4+n*4] = param_13 */ = const1;
this->alarmAttr[n] /* [0x130+n*4] = param_9 */ = alarm_value;
this->alarmCache[n] /* [0xdc+n*4] = *(param_9+0x14) */ = *alarm_value;
this->cycleReady[n] /* [0x130+n*4] = param_9 */ = cycle_ready;
this->alarmCache[n] /* [0xdc+n*4] */ = (*cycle_ready >= 0.999f);
this->simStateAttr[n] /* [0x158+n*4] = param_12 */ = sim_state_value;
this->simStateCache[n] /* [0x104+n*4] = *(param_12+0x14) */ = *sim_state_value;
this->simStateCache[n] /* [0x104+n*4] */ = (*sim_state_value == const1);
this->withinRangePtr[n] /* [0x18c+n*4] = param_4 */ = within_range_value;
this->withinRangeCache[n] /* [0x1b4+n*4] = *param_4 */ = *within_range_value;
this->weaponMode[n] /* [0x3c+n*4] = param_14 */ = weapon_mode;