Files
arcattackandClaude Fable 5 bb795e2805 MP live-play wave: collision economy, missiles, radar transform, panel polarity, comm ticker
The interactive 2-node playtest wave -- every fix decomp-grounded and live-verified:

COLLISION ECONOMY (the ram one-shot): StaticBounce mutates worldLinearVelocity
per contact and ProcessCollisionList walks EVERY touched solid per frame; with
2007 terrain-as-solids the reflections compounded x4-x40 within one frame and a
walking bump one-shot a pristine mech for 112,375 pts (62-pt authentic economy).
Fix: frameEntryWorldVelocity restore per contact (damage always priced at the
real approach speed -- all the binary's physics ever saw); Mech::Reset zeroes
the mover motion (respawn = teleport); [collide-tx]/[mp-hdlr] telemetry.
Gotcha #16 (engine-facility drift class).

MISSILES: peer-visible salvos (the launcher record extension carries a salvo
counter + aim point; ForceUpdate actually enqueues it -- the dirty flag alone
never serialized), the authentic arc (authored MuzzleVelocity vector + the
Seeker's 200m/0.1/300 loft + gain-4 steering, decoded from @004beae4/@004bef78),
world-impact bursts (rounds detonate on cave geometry instead of phasing
through), contact-only damage (flight-cap expiry = fizzle, no more teleport
damage), live re-lead, and ballistic (unguided) shells for autocannons.
projweap's stale BTPushProjectile extern (the /FORCE signature trap, gotcha #6
corollary) crashed the Avatar's first AFC100 shot -- fixed + sweep rule recorded.

RADAR: two transcription bugs made the scope permanently empty -- FUN_0040b244
is the affine INVERSE (not a copy) and FUN_0040adec writes ONLY the 3x3 rotation
(never the translation); worldToView now Invert(view) built rotation-first.
CulturalIcons sorted out of the moving grid (the phantom red pips were map
props), visible-radius culls on all three draw passes, live pip verified at
|delta| x ppm px.  Gotcha #17 (verify the FUN_ body, not its call shape).

WEAPON PANELS (the frozen-dial hunt): the binary's *(subsystem+0x40) means
FAILED -- the recon's 'operating' name was backwards, inverting the destroyed-X
lamps, the panel look, the children enable and the ready-lamp gate (which had
NEVER executed).  Polarity chain corrected end-to-end (failedState, fed by real
damage saturation).  Root cause of the freezes: MFD page-mode gating -- the dev
composite shows ALL pages at once, so off-page dials legitimately stopped; under
BT_DEV_GAUGES the 15 page-plane bits stay active (the exclusive secondary trio
untouched).  The SEH gauge guard now names its kills; repaint-heal resets the
incremental arc after panel repaints; [panel]/[arc] probes added.

COMM/SCORE: MessageBoard LIVE (the engine already shipped the whole
Player__StatusMessage queue; wired the binary's one producer -- the kill branch,
victim's name, 6s -- plus the consumer bridge and a lazy source bind); MP DEATHS
counted via the observed-death tally (each node scores every pilot from locally
observed events, the same model as the KILLS credit) and the -2/-1 engine seed
clamped for display.

DEV UX: node-tagged window titles (-net port), gauge panel reworked (1320x480,
true 4:3 MFD cells, the portrait secondary UNROTATED upright, linear filtering,
BT_GAUGE_SCALE), fixed close spawns via BT_SPAWN_XZ, Boreas flies an Avatar
(first second-chassis live outing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 17:24:15 -05:00

828 lines
18 KiB
C++

#include "munga.h"
#pragma hdrstop
#include "gauge.h"
#include "gaugrend.h"
extern void
indent(int level); // in graph2d.cc
//#########################################################################
//############################# GaugeBase #################################
//#########################################################################
GaugeBase::GaugeBase(
ModeMask mode_mask,
GaugeRenderer *new_renderer,
unsigned int owner_ID,
const char *identification_string
) :
Node(Gauge::GaugeClassID),
alreadyActivatedFlag(False)
{
Check_Pointer(this);
modeMask = mode_mask;
ownerID = owner_ID;
renderer = new_renderer;
identificationString = identification_string;
stayInactive = True; // used by GaugeRenderer during activation
//---------------------------------------------------
// If renderer exists, add to object list
// (Not having a renderer allows for independent testing)
//---------------------------------------------------
if (renderer != NULL)
{
Check(renderer);
renderer->Add(this);
}
Check_Fpu();
}
GaugeBase::~GaugeBase()
{
Check(this);
Check_Fpu();
}
Logical
GaugeBase::TestInstance() const
{
return True;
}
void
GaugeBase::BecameActive()
{
Check(this);
//------------------------------------------------------
// Default method immediately inactivates the gaugeBase
//------------------------------------------------------
DEBUG_STREAM <<
"BecameActive() has not been defined for '" << identificationString <<
"'. About to call Inactivate()\n";
Inactivate();
Check_Fpu();
}
void
GaugeBase::BecameInactive()
{
Check(this);
Check_Fpu();
}
void
GaugeBase::Inactivate()
{
Check(this);
if (renderer != NULL)
{
Check(renderer);
renderer->Inactivate(this);
}
Check_Fpu();
}
Logical
GaugeBase::Update(GaugeRate /*rate_mask*/)
{
Check(this);
//------------------------------------------------------
// Default method immediately inactivates the gaugeBase
//------------------------------------------------------
DEBUG_STREAM <<
"Update() has not been defined for '" << identificationString <<
"'. About to call Inactivate()\n";
Inactivate();
Check_Fpu();
return False;
}
void
GaugeBase::UpdateProfile(Scalar /*frame_percentage*/)
{
Check(this);
Check_Fpu();
}
void
GaugeBase::ReportProfile()
{
Check(this);
Check_Fpu();
}
void
GaugeBase::LinkToEntity(Entity */*entity*/)
{
Check(this);
Check_Fpu();
}
void
GaugeBase::NotifyOfNewInterestingEntity(Entity */*entity*/)
{
Check(this);
Check_Fpu();
}
void
GaugeBase::NotifyOfBecomingUninterestingEntity(Entity */*entity*/)
{
Check(this);
Check_Fpu();
}
int
GaugeBase::DiscernTier()
{
Check(this);
Check_Fpu();
return 0;
}
//#########################################################################
//########################## GaugeBackground ##############################
//#########################################################################
GaugeBackground::GaugeBackground(
ModeMask mode_mask,
GaugeRenderer *new_renderer,
unsigned int owner_ID,
const char *identification_string
) :
GaugeBase(mode_mask, new_renderer, owner_ID, identification_string)
{
Check_Pointer(this);
Check_Fpu();
}
//#########################################################################
//######################## GraphicGaugeBackground #########################
//#########################################################################
GraphicGaugeBackground::GraphicGaugeBackground(
ModeMask mode_mask,
GaugeRenderer *renderer,
unsigned int owner_ID,
int graphics_port_number,
const char *identification_string
) :
localView(
renderer == NULL ?
NULL :
renderer->GetGraphicsPort(graphics_port_number)
),
GaugeBackground(mode_mask, renderer, owner_ID, identification_string)
{
Check_Fpu();
}
//#########################################################################
//########################## GaugeConnection ##############################
//#########################################################################
GaugeConnection::GaugeConnection(int parameter_id)
{
Check_Pointer(this);
parameterID = parameter_id;
Check_Fpu();
}
GaugeConnection::~GaugeConnection()
{
Check_Fpu();
}
Logical
GaugeConnection::TestInstance() const
{
Check_Pointer(this);
return True;
}
void GaugeConnection::Update()
{
Fail("GaugeConnection::Update should not be called!\n");
}
void
GaugeConnection::ShowInstance(char *indent)
{
Check(this);
std::cout << indent << "GaugeConnection:\n";
char
temp[80];
Str_Copy(temp,indent, 80);
Str_Cat(temp,"...", 80);
std::cout << temp << "parameterID=" << parameterID << "\n" << std::flush;
Check_Fpu();
}
//#########################################################################
//################################# Gauge #################################
//#########################################################################
//----------------------------------------------------------------------------
// GaugeRate masks are used to spread out workload in a priority-based
// manner. Each GaugeRate refers to the branch of a binary tree as shown:
//
// A = every frame
// /--------------^--------------\ :
// FIRST TIER: B C = 1/2
// /------^-------\ /------^-------\ :
// 2ND: D E F G = 1/4
// /--^---\ /--^---\ /--^---\ /--^---\ :
// H I J K L M N O = 1/8
// / \ / \ / \ / \ / \ / \ / \ / \ :
// P Q R S T U V W X Y Z Z0 Z1 Z2 Z3 Z4 = 1/16
//
// Each 'tier' down indicates an update rate half that of the one above it.
//
// Note that rates Z0..Z4 cannot be specified through the gauge renderer's
// interpreter. They are available to automatically generated gauges, though.
//----------------------------------------------------------------------------
GaugeRate
Gauge::rateTable[31] =
{
gaugeRate_A, // every frame
gaugeRate_B, // every other frame
gaugeRate_C,
gaugeRate_D, // every 4th frame
gaugeRate_E,
gaugeRate_F,
gaugeRate_G,
gaugeRate_H, // every 8th frame
gaugeRate_I,
gaugeRate_J,
gaugeRate_K,
gaugeRate_L,
gaugeRate_M,
gaugeRate_N,
gaugeRate_O,
gaugeRate_P, // every 16th frame
gaugeRate_Q,
gaugeRate_R,
gaugeRate_S,
gaugeRate_T,
gaugeRate_U,
gaugeRate_V,
gaugeRate_W,
gaugeRate_X,
gaugeRate_Y,
gaugeRate_Z,
gaugeRate_Z0,
gaugeRate_Z1,
gaugeRate_Z2,
gaugeRate_Z3,
gaugeRate_Z4
};
//-------------------------------------------------------------------
// Gauge::NextXXXTierGaugeRate()
// - These functions attempt to spead out workload by returning the
// next available 'GaugeRate' in a given 'tier'.
//
// The higher level tiers bump the lower level tier indices to
// attempt to avoid placing too much load on the same higher-order
// branch of the tree.
//-------------------------------------------------------------------
static int
first_index = -1,
second_index = -1,
third_index = -1,
fourth_index = -1;
GaugeRate
Gauge::NextFirstTierGaugeRate()
{
//----------------------------------------
// Generate first index
//----------------------------------------
first_index = (++first_index) & 1;
//----------------------------------------
// Bump other indices to avoid same branch
//----------------------------------------
second_index += 2;
third_index += 4;
fourth_index += 8;
//----------------------------------------
// Return rate mask
//----------------------------------------
return rateTable[first_index + 1];
}
GaugeRate
Gauge::NextSecondTierGaugeRate()
{
//----------------------------------------
// Generate first index
//----------------------------------------
second_index = (++second_index) & 3;
//----------------------------------------
// Bump other indices to avoid same branch
//----------------------------------------
third_index += 2;
fourth_index += 4;
//----------------------------------------
// Return rate mask
//----------------------------------------
return rateTable[second_index + 3];
}
GaugeRate
Gauge::NextThirdTierGaugeRate()
{
//----------------------------------------
// Generate first index
//----------------------------------------
third_index = (++third_index) & 7;
//----------------------------------------
// Bump other indices to avoid same branch
//----------------------------------------
fourth_index += 2;
//----------------------------------------
// Return rate mask
//----------------------------------------
return rateTable[third_index + 7];
}
GaugeRate
Gauge::NextFourthTierGaugeRate()
{
//----------------------------------------
// Generate first index
//----------------------------------------
fourth_index = (++fourth_index) & 15;
//----------------------------------------
// Return rate mask
//----------------------------------------
return rateTable[fourth_index + 15];
}
GaugeRate
Gauge::ConvertIndexToRate(int index)
{
Verify(index >= 0);
Verify(index < 32);
return rateTable[index];
}
Gauge::Gauge(
GaugeRate new_rate,
ModeMask mode_mask,
GaugeRenderer *renderer,
unsigned int owner_ID,
const char *identification_string
):
GaugeBase(mode_mask, renderer, owner_ID, identification_string),
instanceList(this)
{
Check_Pointer(this);
rate = new_rate;
oldRate = new_rate;
stayInactive = False;
profileFramePercentage = (Scalar) 0;
profileMaxFramePercentage = (Scalar) 0;
profileCycles = 0;
Check_Fpu();
}
Gauge::~Gauge()
{
Check(this);
RemoveAllConnections();
Check_Fpu();
}
Logical
Gauge::TestInstance() const
{
Check(&instanceList);
# if 0
//--------------------------------------------------------------------
// Check all owned GaugeConnections
//--------------------------------------------------------------------
ChainIteratorOf<GaugeConnection*>
i(instanceList);
GaugeConnection
*the_connection;
while ((the_connection=i.ReadAndNext()) != NULL)
{
Check(the_connection);
}
# endif
//--------------------------------------------------------------------
// Check the base object
//--------------------------------------------------------------------
return GaugeBase::TestInstance();
}
void
Gauge::ShowInstance(char *indent)
{
Check(this);
ChainIteratorOf<GaugeConnection*>
i(instanceList);
GaugeConnection
*the_connection;
std::cout << indent << "Gauge:\n";
char
temp[80];
Str_Copy(temp,indent, 80);
Str_Cat(temp,"...", 80);
std::cout << temp << "modeMask =" << modeMask << "\n";
std::cout << temp << "renderer =" << renderer << "\n";
std::cout << temp << "ownerID =" << ownerID << "\n";
std::cout << temp << "InstanceList -\n" << std::flush;
Str_Cat(temp,"...", 80);
while ((the_connection=i.ReadAndNext()) != NULL)
{
Check(the_connection);
the_connection->ShowInstance(temp);
}
Check_Fpu();
}
void
Gauge::AddConnection(GaugeConnection *connection)
{
Check(this);
Check(connection);
instanceList.Add(connection);
Check_Fpu();
}
void
Gauge::RemoveConnection(int parameter_ID)
{
Check(this);
ChainIteratorOf<GaugeConnection*>
i(instanceList);
GaugeConnection
*gauge_connection;
while ((gauge_connection=i.ReadAndNext()) != NULL)
{
Check(gauge_connection);
if (gauge_connection->parameterID == parameter_ID)
{
Unregister_Object(gauge_connection);
delete gauge_connection;
}
}
Check_Fpu();
}
void
Gauge::RemoveAllConnections()
{
Check(this);
ChainIteratorOf<GaugeConnection*>
i(instanceList);
GaugeConnection
*gauge_connection;
while ((gauge_connection=i.ReadAndNext()) != NULL)
{
Check(gauge_connection);
Unregister_Object(gauge_connection);
delete gauge_connection;
}
Check_Fpu();
}
void
Gauge::Disable(Logical disable_state)
{
Check(this);
if (disable_state)
{
rate = 0;
BecameInactive();
}
else
{
rate = oldRate;
BecameActive();
}
Check_Fpu();
}
Logical
Gauge::Update(GaugeRate rate_mask)
{
Check(this);
Logical
actually_ran = False;
//-----------------------------------------
// Update only if rate mask allows it
//-----------------------------------------
if (rate & rate_mask)
{
//-----------------------------------------
// Update parameters
//-----------------------------------------
ChainIteratorOf<GaugeConnection*>
i(instanceList);
GaugeConnection
*gauge_connection;
while ((gauge_connection=i.ReadAndNext()) != NULL)
{
Check(gauge_connection);
gauge_connection->Update();
}
//-----------------------------------------
// Execute gauge code
//-----------------------------------------
{
// BT DEV-GAUGES bring-up: run Execute() under SEH so a gauge whose
// game-state binding isn't reconstructed yet is DISABLED (rate=0)
// after its first fault instead of AV'ing every frame. Gated on
// BT_DEV_GAUGES; the pod build calls Execute() directly.
static int s_devGaugesUpd = -1;
if (s_devGaugesUpd < 0)
s_devGaugesUpd = (getenv("BT_DEV_GAUGES") != NULL) ? 1 : 0;
if (s_devGaugesUpd)
{
if (!GuardedExecute())
{
// LOUD KILL (2026-07-12): the silent version made a faulted
// gauge simply FREEZE on screen (user-hit: a weapon panel's
// dial + lamp died mid-session with no trace). Name the
// casualty so the log convicts the faulting Execute.
DEBUG_STREAM << "[gauge-fault] '"
<< (identificationString ? identificationString : "?")
<< "' Execute FAULTED -> gauge DISABLED (this="
<< (void *)this << ")\n" << std::flush;
Disable(True);
}
}
else
{
Execute();
}
}
actually_ran = True;
}
Check_Fpu();
return actually_ran;
}
void
Gauge::Execute()
{
Fail("Gauge::Execute not overridden");
}
//
// GuardedExecute -- BT DEV-GAUGES bring-up. Run Execute() under a structured
// exception handler so a gauge whose game-state data binding is not yet
// reconstructed is skipped instead of AV'ing the whole game. (A separate
// function because __try/__except may not share a frame with C++ objects that
// require unwinding, which Gauge::Update does.) Returns False on fault.
//
Logical
Gauge::GuardedExecute()
{
__try
{
Execute();
return True;
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
return False;
}
}
struct TierLookup
{
GaugeRate rate;
int tier;
};
int
Gauge::DiscernTier()
{
Check(this);
TierLookup
*tier_pointer;
static TierLookup
tier_lookup[] =
{
{gaugeRate_A, 0 },
{gaugeRate_B, 1 },
{gaugeRate_C, 1 },
{gaugeRate_D, 2 },
{gaugeRate_E, 2 },
{gaugeRate_F, 2 },
{gaugeRate_G, 2 },
{gaugeRate_H, 3 },
{gaugeRate_I, 3 },
{gaugeRate_J, 3 },
{gaugeRate_K, 3 },
{gaugeRate_L, 3 },
{gaugeRate_M, 3 },
{gaugeRate_N, 3 },
{gaugeRate_O, 3 },
{gaugeRate_P, 4 },
{gaugeRate_Q, 4 },
{gaugeRate_R, 4 },
{gaugeRate_S, 4 },
{gaugeRate_T, 4 },
{gaugeRate_U, 4 },
{gaugeRate_V, 4 },
{gaugeRate_W, 4 },
{gaugeRate_X, 4 },
{gaugeRate_Y, 4 },
{gaugeRate_Z, 4 },
{gaugeRate_Z0,4 },
{gaugeRate_Z1,4 },
{gaugeRate_Z2,4 },
{gaugeRate_Z3,4 },
{gaugeRate_Z4,4 },
{0,0}
};
for (tier_pointer=tier_lookup; tier_pointer->rate != 0; ++tier_pointer)
{
if (tier_pointer->rate == rate)
{
Check_Fpu();
return tier_pointer->tier;
}
}
Check_Fpu();
return 0;
}
void
Gauge::UpdateProfile(Scalar frame_percentage)
{
Check(this);
//---------------------------------------------
// Keep worst case
//---------------------------------------------
if (profileMaxFramePercentage < frame_percentage)
{
profileMaxFramePercentage = frame_percentage;
}
//---------------------------------------------
// Keep running total
//---------------------------------------------
profileFramePercentage += frame_percentage;
//---------------------------------------------
// Prevent overflow
//---------------------------------------------
if (++profileCycles > 16384)
{
profileFramePercentage /= (Scalar) 2;
profileCycles >>= 1;
}
Check_Fpu();
}
void
Gauge::ReportProfile()
{
Check(this);
int
i;
DEBUG_STREAM << identificationString << std::flush;
for(i=strlen(identificationString); i<32; ++i)
{
DEBUG_STREAM << "." << std::flush;
}
if (profileCycles > 0)
{
Scalar
average = profileFramePercentage / (Scalar)profileCycles;
char
buffer[80];
sprintf(buffer, "%6d %6.4f %6.4f\n",
profileCycles,
average,
profileMaxFramePercentage
);
DEBUG_STREAM << buffer << std::flush;
}
else
{
DEBUG_STREAM << "never ran.\n" << std::flush;
}
//----------------------------------------
// Reset maximum percentage
//----------------------------------------
profileMaxFramePercentage = (Scalar) 0;
Check_Fpu();
}
//#########################################################################
//############################# GraphicGauge #############################
//#########################################################################
GraphicGauge::GraphicGauge(
GaugeRate new_rate,
ModeMask mode_mask,
GaugeRenderer *renderer,
unsigned int owner_ID,
int graphics_port_number,
const char *identification_string
) :
Gauge(new_rate, mode_mask, renderer, owner_ID, identification_string),
localView(
renderer == NULL ?
NULL :
renderer->GetGraphicsPort(graphics_port_number)
)
{
Check_Fpu();
}
GraphicGauge::~GraphicGauge()
{
Check(this);
Check_Fpu();
}
Logical
GraphicGauge::TestInstance() const
{
Check(&localView);
Check_Fpu();
return Gauge::TestInstance();
}
void
GraphicGauge::ShowInstance(char *indent)
{
Check(this);
std::cout << indent << "GraphicGauge:\n";
char
temp[80];
Str_Copy(temp,indent, 80);
Str_Cat(temp,"...", 80);
localView.ShowInstance(temp);
Gauge::ShowInstance(temp);
Check_Fpu();
}
#if defined(TEST_CLASS)
# include "gauge.tcp"
#endif