SeekVoltageGraph: full reconstruction -- the eng-page POWER graph + top-box eraser (Gitea #11)
The #10 audit's one WRONG: the port Execute was a bring-up no-op, so the emitter/myomer engineering pages never erased their top data box -> stale sibling-page ghosts on the shared Eng bit-plane (SYSTEM 10 PPC showing the Streak ammo box, etc). Full faithful widget landed: - btl4gau2: ctor/BecameActive/Execute/clear/ticks/cursor recovered from the capstone disasm of @004c6798/@004c6920/@004c6934/@004c6be4/@004c6c30/ @004c6c6c (Ghidra dropped every x87 arg). Plot: v=0..12000 step 1200, x=Round(response(v)*230), y=Round(v*(1/12000)*187) (ld80 @004c6bd0/@004c6d74 = exactly 1/12000); change-test samples the response at 12000V vs the 9999 activation sentinel; XOR op for tick/cursor move-by-redraw; destroyed branch centres edestryd.pcc and revives via own vtbl+0xC (BecameActive, slot 3 of PTR_0051a1fc -- vtable-dump verified). - The vtbl+0x3C sampler identified from the binary vtables: Emitter slot 15 @004bb42c = sqrt(P(v)/2.0e8), P @004bb3f4 = damageFraction*v^2*0.5* energyCoefficient; Myomers slot 15 @004b8f94 = sqrt(AvailableOutput(v)*3.6/ 350). FUN_004dd138 == sqrt (part_015.c:4026): myomers' fabs reading corrected, GetSpeedReading renamed SeekVoltageResponse. Port dispatch via complete-type bridges BTSeekVoltageSample/BTMyomersSeekSample + BTSubsystemDestroyed (databinding rule). - Emitter's AUTHENTIC attribute table recovered (binary @0x511dd4, ids 0x1D-0x25) and published: Laser*/Seek*/OutputVoltage@0x414 (currentLevel, RAW volts -- the live-cursor feed). Field renames per the table: 0x3F8 minSeekVoltageIndex / 0x3FC maxSeekVoltageIndex (static_assert-locked). The MechWeapon 0x1D OutputVoltage PORT ALIAS retired (binary table ends 0x1C; Find walks lowest-id-first, the alias shadowed the authentic row). PPC::DefaultData -> Emitter::GetAttributeIndex(). - mech4: BT_PRESET_HOLD=<n> (freeze the #9 preset cycler after n pulses) for steady-state pixel verification; BT_SEEK_LOG diag. Verified live (BLH, autofire): both #10 repro pairs held ghost-free for minutes (SYS09->SYS10 PPC, SYS02->SYS04 ERMed, SYS05->SYS06 Myomers); curves draw with live cursors; one replot per activation; quad panels/J-K-L/sec panel un-regressed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
f1a6ccbaaf
commit
63b168cb92
+216
-41
@@ -57,7 +57,9 @@
|
||||
// 0x463b8000=12000.0f generator max voltage
|
||||
// 0x42c80000=100.0f HeatSinkCluster numeric scale base
|
||||
// _DAT_004c8df0 heat numeric scale _DAT_004c92e0 warning threshold
|
||||
// _DAT_004c6bdc/_DAT_004c6be0 seek-curve plot step/limit
|
||||
// _DAT_004c6bdc=1200.0f / _DAT_004c6be0=12000.0f seek-curve plot step/limit
|
||||
// ld80 @004c6bd0 / @004c6d74 = 8.3333333333333e-05 (exactly 1/12000) --
|
||||
// the voltage->y normaliser (y = v * (1/12000) * yScale)
|
||||
// DAT_0051936c PrepEngr engineer-screen number (1..12)
|
||||
//
|
||||
|
||||
@@ -84,6 +86,10 @@ static const Scalar SeekVoltageYScale = 187.0f; // 0x433b0000
|
||||
static const Scalar RedrawSentinel = 9999.0f; // 0x461c3c00
|
||||
static const Scalar GeneratorMaxVoltage = 12000.0f; // 0x463b8000
|
||||
static const Scalar HeatNumericBase = 100.0f; // 0x42c80000
|
||||
// SeekVoltageGraph plot constants (capstone disasm of @004c6934, task/issue #11):
|
||||
static const Scalar SeekCurveStep = 1200.0f; // _DAT_004c6bdc (10 segments)
|
||||
static const Scalar SeekCurveLimit = 12000.0f; // _DAT_004c6be0 (y-axis full scale)
|
||||
static const Scalar SeekCurveYNorm = (Scalar)(1.0 / 12000.0); // ld80 @004c6bd0/@004c6d74
|
||||
|
||||
// (DebugStream warning channel, DAT_00524e20, is provided by the shared
|
||||
// reconstruction headers -- heat.hpp / mechrecon.hpp.)
|
||||
@@ -97,15 +103,6 @@ static char DAT_00518f24[] = "GeneratorCluster"; // subsystem name to resolve
|
||||
static int DAT_00518ee0 = 0; // GaugeRate
|
||||
static char DAT_00518f68[] = ""; // background image name
|
||||
|
||||
//
|
||||
// Original FUN_0041bfc0 = Entity::FindAttributeIndex(name) -> ordinal. The
|
||||
// WinTesla engine exposes attribute lookup via Simulation::AttributeIndexSet
|
||||
// (Find/FindEntry by name) rather than a name->ordinal call, so this best-effort
|
||||
// shim stands in for it; the stored indices are consumed by the (summarised)
|
||||
// SeekVoltageGraph::Execute plot.
|
||||
//
|
||||
static int FindAttributeIndex(Entity *, const char *) { return -1; }
|
||||
|
||||
//
|
||||
// FUN_0041bfc0 == Entity::FindAttributeIndex(name) returns a POINTER into the
|
||||
// subsystem (subsystem + attributeMemberOffset), or 0 on miss. The composite
|
||||
@@ -250,14 +247,31 @@ protected:
|
||||
//###########################################################################
|
||||
//###########################################################################
|
||||
// SeekVoltageGraph @004c6798
|
||||
// (issue #11: fully reconstructed from the capstone disasm of
|
||||
// @004c6798/@004c6920/@004c6934/@004c6be4/@004c6c30/@004c6c6c --
|
||||
// Ghidra had dropped every x87 argument; tools/disas2.py recovered them.)
|
||||
//###########################################################################
|
||||
//###########################################################################
|
||||
|
||||
//
|
||||
// The subsystem's voltage-response sampler == the binary subsystem vtbl+0x3C
|
||||
// (slot 15): Emitter @004bb42c = sqrt(seekPower(v)/2.0e8), Myomers @004b8f94 =
|
||||
// sqrt(speed(v)*3.6/350). Reached through a complete-type dispatch bridge
|
||||
// (emitter.cpp -> myomers.cpp) per the databinding rule (gotcha 8).
|
||||
//
|
||||
extern Scalar BTSeekVoltageSample(void *subsystem, Scalar voltage);
|
||||
//
|
||||
// The destroyed test == *(subsystem+0x40) == 1 (MechSubsystem::simulationState,
|
||||
// 1 = Destroyed). Named-member bridge in powersub.cpp (databinding rule).
|
||||
//
|
||||
extern int BTSubsystemDestroyed(Subsystem *subsystem);
|
||||
|
||||
//
|
||||
// @004c6798 -- ctor (vtable PTR_FUN_0051a1fc). GraphicGauge base, interns and
|
||||
// ref-counts the "destroyed" bitmap, resolves the four SeekVoltage attribute
|
||||
// indices off the weapon subsystem, sets the port extent (0x97,0x80,0x17d,0x13b)
|
||||
// and stashes the 230x187 plot scale.
|
||||
// POINTERS off the weapon subsystem (FUN_0041bfc0 returns subsystem+offset or
|
||||
// 0), sets the view XOR draw op + the port extent (0x97,0x80,0x17d,0x13b) and
|
||||
// stashes the 230x187 plot scale.
|
||||
//
|
||||
SeekVoltageGraph::SeekVoltageGraph(
|
||||
GaugeRate rate,
|
||||
@@ -278,22 +292,34 @@ SeekVoltageGraph::SeekVoltageGraph(
|
||||
destroyedImage = (char *)destroyed_image;
|
||||
renderer->warehousePointer->bitMapBin.Get(destroyedImage); // FUN_00442aec (AddRef)
|
||||
|
||||
currentSeekIndex = FindAttributeIndex(subsystem, "CurrentSeekVoltageIndex"); // FUN_0041bfc0
|
||||
minSeekIndex = FindAttributeIndex(subsystem, "MinSeekVoltageIndex");
|
||||
maxSeekIndex = FindAttributeIndex(subsystem, "MaxSeekVoltageIndex");
|
||||
seekVoltageTable = FindAttributeIndex(subsystem, "SeekVoltage");
|
||||
// FUN_0041bfc0(subsystem, name) == the engine's name-keyed attribute-pointer
|
||||
// resolve (subsystem+memberOffset, or 0 on a miss); the composite ctors gate
|
||||
// the tick/cursor draws on a 0 result.
|
||||
currentSeekIndexPtr = (int *)AttributePointerOf(subsystem, "CurrentSeekVoltageIndex"); // this[0x25]
|
||||
minSeekIndexPtr = (int *)AttributePointerOf(subsystem, "MinSeekVoltageIndex"); // this[0x26]
|
||||
maxSeekIndexPtr = (int *)AttributePointerOf(subsystem, "MaxSeekVoltageIndex"); // this[0x27]
|
||||
seekVoltageTablePtr = (Scalar *)AttributePointerOf(subsystem, "SeekVoltage"); // this[0x28]
|
||||
|
||||
voltageSubsystem = (Entity *)seek_voltage_pointer; // this[0x29]
|
||||
liveVoltage = (Scalar *)seek_voltage_pointer; // this[0x29]
|
||||
this->subsystem = (Subsystem *)subsystem; // this[0x2A]
|
||||
|
||||
// graphicsView -> GraphicGauge::localView. SetExtent == SetPositionWithinPort;
|
||||
// the original SetClip() (vtbl+0x0C) constrained drawing to that extent.
|
||||
// @004c6798: (vtbl+0x0C)(3) then (vtbl+0x08)(0x97,0x80,0x17d,0x13b) --
|
||||
// SetOperation(Xor) + SetPositionWithinPort (GRAPH2D.h vtable order). The
|
||||
// XOR op is what lets DrawTicks/DrawCursorBox erase by redrawing.
|
||||
localView.SetOperation((GraphicsDisplay::Operation)3); // vtbl+0x0C Xor
|
||||
localView.SetPositionWithinPort(0x97, 0x80, 0x17d, 0x13b); // vtbl+0x08
|
||||
|
||||
xScale = SeekVoltageXScale; // this[0x2C]
|
||||
yScale = SeekVoltageYScale; // this[0x2D]
|
||||
drawCursor = draw_cursor; // this[0x2F]
|
||||
destroyedShown = 0; // this[0x2E]
|
||||
// (binary leaves this[0x2B]/[0x30..0x32] uninitialised -- activation's
|
||||
// BecameActive sentinel forces the first replot, which seeds them before
|
||||
// any read. Seeded here for determinism.)
|
||||
previousVoltage = RedrawSentinel; // this[0x2B]
|
||||
cursorX = -1; // this[0x30]
|
||||
cursorY = -1; // this[0x31]
|
||||
tickIndexShown = -1; // this[0x32]
|
||||
}
|
||||
|
||||
//
|
||||
@@ -311,38 +337,187 @@ Logical SeekVoltageGraph::TestInstance() const
|
||||
}
|
||||
|
||||
//
|
||||
// @004c6920 -- BecameActive: previousVoltage = 9999.0f (force first redraw).
|
||||
// @004c6920 -- BecameActive: previousVoltage = 9999.0f (poison the cached
|
||||
// response so the next Execute clears + replots the whole box -- THE top-box
|
||||
// eraser on a page switch; the binary body writes ONLY this[0x2B]).
|
||||
//
|
||||
void SeekVoltageGraph::BecameActive()
|
||||
{
|
||||
previousVoltage = RedrawSentinel; // @0xAC force first redraw
|
||||
destroyedShown = 0;
|
||||
previousVoltage = RedrawSentinel; // @0xAC force full redraw
|
||||
if (getenv("BT_SEEK_LOG"))
|
||||
DEBUG_STREAM << "[seek] BecameActive sub="
|
||||
<< (subsystem ? subsystem->GetName() : "?") << "\n" << std::flush;
|
||||
}
|
||||
|
||||
//
|
||||
// @004c6934 -- Execute (the seek-voltage curve plot). BRING-UP: the full plot
|
||||
// (SetupClip/DrawTicks/DrawCursor over a 0..1 voltage polyline) needs the live
|
||||
// seek-voltage sampler (subsys vtbl+0x3c) + the four attribute indices; the
|
||||
// attribute resolve is summarised (FindAttributeIndex returns -1), so the plot is
|
||||
// deferred and Execute is inert until the sampler + attribute reads land. Kept as
|
||||
// a defined no-op so the gauge's vtable is complete (no /FORCE crash when the
|
||||
// Myomer/Energy cluster ticks its SeekVoltageGraph child).
|
||||
// @004c6be4 -- the full-box erase ("SetupClip" in the old notes; it is really
|
||||
// a clear): SetOperation(Replace), SetColor(0), MoveToAbsolute(0,0),
|
||||
// DrawFilledRectangleToAbsolute(1000,1000) -- clipped to the view = the whole
|
||||
// (0x97,0x80)-(0x17d,0x13b) top data box painted black. NOTE: leaves the view
|
||||
// in Replace mode (the polyline plot relies on that; Execute restores Xor).
|
||||
//
|
||||
void SeekVoltageGraph::ClearGraph()
|
||||
{
|
||||
localView.SetOperation((GraphicsDisplay::Operation)0); // vtbl+0x0C Replace
|
||||
localView.SetColor(0); // vtbl+0x18
|
||||
localView.MoveToAbsolute(0, 0); // vtbl+0x24
|
||||
localView.DrawFilledRectangleToAbsolute(1000, 1000); // vtbl+0x48
|
||||
}
|
||||
|
||||
//
|
||||
// @004c6c30 -- the live cursor: an 8x8 filled square centred on
|
||||
// (cursorX,cursorY), drawn in the view's current (Xor) op so a second call
|
||||
// erases it. MoveToAbsolute(x-4,y-4) + DrawFilledRectangleToRelative(8,8).
|
||||
//
|
||||
void SeekVoltageGraph::DrawCursorBox()
|
||||
{
|
||||
localView.MoveToAbsolute(cursorX - 4, cursorY - 4); // vtbl+0x24
|
||||
localView.DrawFilledRectangleToRelative(8, 8); // vtbl+0x4C
|
||||
}
|
||||
|
||||
//
|
||||
// @004c6c6c -- the per-gear tick marks (XOR). For each seek index i in
|
||||
// [*min..*max]: V = seekVoltage[i]; x = Round(sample(V) * xScale);
|
||||
// y = Round(V * (1/12000) * yScale) [ld80 @004c6d74]. The gear the highlight
|
||||
// was last drawn at (this[0x32]) gets the full L (axis->point->axis); the rest
|
||||
// get 10-pixel axis stubs. Called in pairs to move the highlight (XOR erase +
|
||||
// redraw).
|
||||
//
|
||||
void SeekVoltageGraph::DrawTicks()
|
||||
{
|
||||
// (port guard: the binary derefs min/max unguarded -- its call sites only
|
||||
// guarantee current+table non-null; all four always resolve on the
|
||||
// emitter/myomer subsystems that own a graph.)
|
||||
if (minSeekIndexPtr == 0 || maxSeekIndexPtr == 0)
|
||||
return;
|
||||
int last = *maxSeekIndexPtr; // this[0x27] deref
|
||||
for (int i = *minSeekIndexPtr; i <= last; i++) // this[0x26] deref
|
||||
{
|
||||
Scalar gearVoltage = seekVoltageTablePtr[i]; // this[0x28][i]
|
||||
int x = Round(BTSeekVoltageSample(subsystem, gearVoltage) * xScale); // (vtbl+0x3C)(V)*230
|
||||
int y = Round(gearVoltage * SeekCurveYNorm * yScale); // V/12000*187
|
||||
if (i == tickIndexShown) // this+0xC8
|
||||
{
|
||||
localView.MoveToAbsolute(0, y); // vtbl+0x24
|
||||
localView.DrawLineToAbsolute(x, y); // vtbl+0x30
|
||||
localView.DrawLineToAbsolute(x, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
localView.MoveToAbsolute(0, y);
|
||||
localView.DrawLineToAbsolute(10, y);
|
||||
localView.MoveToAbsolute(x, 0);
|
||||
localView.DrawLineToAbsolute(x, 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// @004c6934 -- Execute (the seek-voltage response plot; full x87 recovery).
|
||||
//
|
||||
// DESTROYED branch (subsys simulationState==1): once, clear the box and draw
|
||||
// the centred "destroyed" bitmap (edestryd.pcc) at
|
||||
// ((Round(xScale)-w)/2, (Round(yScale)-h)/2), color 0xFF.
|
||||
//
|
||||
// LIVE branch:
|
||||
// * on the destroyed->alive edge, call BecameActive (own vtbl+0xC == slot 3
|
||||
// of PTR_0051a1fc = @004c6920) to poison the cache;
|
||||
// * change-test: sample the response at 12000 V (0x463b8000) and compare with
|
||||
// the cached this[0x2B]; on change (or the 9999 activation sentinel):
|
||||
// CLEAR (@004c6be4 -- the ghost eraser), SetColor(0xFF), MoveTo(0,0), then
|
||||
// the polyline v = 0,1200,...,10800 (step _DAT_004c6bdc=1200 while
|
||||
// v < _DAT_004c6be0=12000): LineTo(Round(sample(v)*230),
|
||||
// Round(v * [1/12000 * 187])), reset the cursor slots to -1, restore
|
||||
// SetOperation(Xor), and draw the gear ticks at the current index;
|
||||
// * tick maintenance: when the current seek index moved, XOR-erase the old
|
||||
// highlight + redraw at the new (two DrawTicks calls);
|
||||
// * cursor (draw_cursor pages only): XOR-erase the old box (if any), sample
|
||||
// at the LIVE voltage (*this[0x29]) and draw the new 8x8 box at
|
||||
// (Round(sample(V)*230), Round(V * [1/12000 * 187])).
|
||||
//
|
||||
void SeekVoltageGraph::Execute()
|
||||
{
|
||||
// (seek-voltage plot deferred -- see note above.)
|
||||
}
|
||||
if (BTSubsystemDestroyed(subsystem)) // *(subsys+0x40) == 1
|
||||
{
|
||||
if (destroyedShown == 0)
|
||||
{
|
||||
destroyedShown = 1; // this[0x2E]
|
||||
ClearGraph(); // @004c6be4
|
||||
L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
|
||||
BitMap *image = warehouse->bitMapBin.Get(destroyedImage); // FUN_00442aec
|
||||
if (image != NULL) // (port guard; binary derefs unchecked)
|
||||
{
|
||||
localView.MoveToAbsolute( // vtbl+0x24 centre the bitmap
|
||||
(Round(xScale) - image->Data.Size.x) >> 1,
|
||||
(Round(yScale) - image->Data.Size.y) >> 1);
|
||||
localView.SetColor(0xff); // vtbl+0x18
|
||||
localView.DrawBitMap(0, image, // vtbl+0x54
|
||||
0, 0, image->Data.Size.x - 1, image->Data.Size.y - 1);
|
||||
}
|
||||
warehouse->bitMapBin.Release(destroyedImage); // FUN_00442c12
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// @004c6934 (original notes preserved) -- Execute. If the subsystem is destroyed
|
||||
// (subsys+0x40 == 1) draw
|
||||
// the centred "destroyed" bitmap once. Otherwise, when the live seek voltage
|
||||
// (subsys vtbl+0x3c sampler) changes: clear (SetupClip @004c6be4), re-plot the
|
||||
// 0..1 voltage curve as a polyline (step _DAT_004c6bdc up to _DAT_004c6be0,
|
||||
// Round each point), then draw the min/max tick marks (DrawTicks @004c6c6c)
|
||||
// and, if drawCursor, the live cursor at the current seek index
|
||||
// (DrawCursor @004c6c30).
|
||||
//
|
||||
if (destroyedShown != 0)
|
||||
{
|
||||
destroyedShown = 0;
|
||||
BecameActive(); // own vtbl+0xC (@004c6920)
|
||||
}
|
||||
|
||||
Scalar yStep = SeekCurveYNorm * yScale; // fld tbyte @004c6bd0 * this[0x2D]
|
||||
|
||||
// change-test: the response curve's value at max voltage.
|
||||
Scalar response = BTSeekVoltageSample(subsystem, GeneratorMaxVoltage); // (vtbl+0x3C)(12000.0f)
|
||||
if (previousVoltage != response) // this[0x2B]
|
||||
{
|
||||
if (getenv("BT_SEEK_LOG"))
|
||||
DEBUG_STREAM << "[seek] replot sub="
|
||||
<< (subsystem ? subsystem->GetName() : "?")
|
||||
<< " response12k=" << response << "\n" << std::flush;
|
||||
previousVoltage = response;
|
||||
ClearGraph(); // @004c6be4 -- erases the WHOLE box
|
||||
localView.SetColor(0xff); // vtbl+0x18 (Replace mode from the clear)
|
||||
localView.MoveToAbsolute(0, 0); // vtbl+0x24
|
||||
for (Scalar v = 0.0f; v < SeekCurveLimit; v += SeekCurveStep)
|
||||
{
|
||||
int x = Round(BTSeekVoltageSample(subsystem, v) * xScale);
|
||||
int y = Round(v * yStep);
|
||||
localView.DrawLineToAbsolute(x, y); // vtbl+0x30
|
||||
}
|
||||
cursorX = -1; // this[0x30]
|
||||
cursorY = -1; // this[0x31]
|
||||
localView.SetOperation((GraphicsDisplay::Operation)3); // vtbl+0x0C back to Xor
|
||||
if (currentSeekIndexPtr != 0 && seekVoltageTablePtr != 0)
|
||||
{
|
||||
tickIndexShown = *currentSeekIndexPtr; // this[0x32]
|
||||
DrawTicks(); // @004c6c6c
|
||||
}
|
||||
}
|
||||
|
||||
// gear-change tick maintenance (XOR pair: erase old highlight, draw new).
|
||||
if (currentSeekIndexPtr != 0 && seekVoltageTablePtr != 0)
|
||||
{
|
||||
int currentIndex = *currentSeekIndexPtr;
|
||||
if (currentIndex != tickIndexShown)
|
||||
{
|
||||
DrawTicks(); // erase at the old index
|
||||
tickIndexShown = currentIndex;
|
||||
DrawTicks(); // draw at the new
|
||||
}
|
||||
}
|
||||
|
||||
// live-voltage cursor (drawCursor pages: the emitter/PPC graphs).
|
||||
if (drawCursor != 0) // this[0x2F]
|
||||
{
|
||||
if (cursorX >= 0)
|
||||
DrawCursorBox(); // XOR-erase the old box
|
||||
Scalar live = *liveVoltage; // *this[0x29]
|
||||
cursorX = Round(BTSeekVoltageSample(subsystem, live) * xScale);
|
||||
cursorY = Round(live * yStep);
|
||||
DrawCursorBox(); // @004c6c30
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//###########################################################################
|
||||
|
||||
Reference in New Issue
Block a user