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:
arcattack
2026-07-19 20:27:39 -05:00
co-authored by Claude Fable 5
parent f1a6ccbaaf
commit 63b168cb92
15 changed files with 594 additions and 109 deletions
+2 -1
View File
@@ -34,7 +34,8 @@ PPC::SharedData
&PPC::ClassDerivations,
Emitter::GetMessageHandlers(), // task #6: the port's accessor idiom -- inherits
// MechWeapon's ConfigureMappables/ChooseButton
MechWeapon::GetAttributeIndex(), // gauge wave: inherit temps/InputVoltage/OutputVoltage/PercentDone (was empty)
Emitter::GetAttributeIndex(), // issue #11: the authentic Emitter table (Seek*/Laser*/
// OutputVoltage=currentLevel) -- chains MechWeapon's
PPC::StateCount
);
+10 -2
View File
@@ -107,7 +107,7 @@ Subsystem (MUNGA base — have source: RP/MUNGA/SUBSYSTM.HPP)
│ │ resource (size 0x1B0) adds +0x190 VelocityEfficiency, +0x194 AccelerationEfficiency, +0x198 SeekVoltage[5](fractions×ratedVoltage),
│ │ +0x1AC SeekVoltageRecommendedIndex (parser @4b9140). attr table @00511588; string pool @00511660 ("Myomers"@0051166d).
│ │ vtable overrides vs PoweredSubsystem: slot0 dtor@4b9114, slot9 HandleMessage@4b8a8c(→4b0efc), slot10 ResetToInitialState@4b8aa4(→4b0e6c),
│ │ slot15 GetSpeedReading@4b8f94 (base 4b1780; km/h÷350 gauge readout, best-effort), slot16 HasAdequateVoltage@4b8f3c (overrides IsSourceShorted base 4b0b5c).
│ │ slot15 SeekVoltageResponse@4b8f94 (base 4b1780; the SeekVoltageGraph vtbl+0x3C sampler: sqrt(AvailableOutput(v)·3.6/350) — issue #11 resolved the old "GetSpeedReading km/h gauge" guess; FUN_004dd138 = sqrt not fabs), slot16 HasAdequateVoltage@4b8f3c (overrides IsSourceShorted base 4b0b5c).
│ │ helpers: AvailableOutput@4b8ac0 (drive = MechBaseSpeed × V/recommendedSeekV × heatFactor × (1-damage)),
│ │ RegisterMaxOutput@4b8ef0 (→ Mech maxSpeed@0x7A0; loop @part_012:10322 over Mech myomer list@0x1EB),
│ │ MyomersSimulation@4b8d18 (Performance, per-tick work→heat accum@0x1C8, gated by FUN_004ad7d4; best-effort registration),
@@ -487,11 +487,19 @@ Searchlight 0xBD8 and ThermalSight 0xBDE are recovered, not gaps.)
NB: MechWeapon's recharge-level slot @0x320 is reused by `Emitter` as **outputVoltage** (the
normalised discharge readiness; ==1.0 ⇒ fully charged, used in EMITTER.TCP).
- `Emitter` adds from @0x3F0 (object ~0x478): seekVoltageIndex@0x3F0, seekVoltageRecommendedIndex@0x3F4,
seekVoltageCount@0x3FC, seekVoltage[5]@0x400, currentLevel@0x414, firingActive@0x418,
minSeekVoltageIndex@0x3F8, maxSeekVoltageIndex@0x3FC (issue #11 renames — the binary attribute
table @0x511dd4 publishes them as Min/MaxSeekVoltageIndex; the old seekStepCounter/seekVoltageCount
names were guesses), seekVoltage[5]@0x400, currentLevel@0x414, firingActive@0x418,
beamOrientation@0x41C, beamScale@0x42C, beamLengthRatio@0x434, graphicLength@0x438, dischargeTime@0x43C,
dischargeTimer@0x440, damageFraction@0x444, energyTotal@0x448, damagePortion@0x44C, heatPortion@0x450,
energyCoefficient@0x454, seekRate@0x45C, beamEndpoint@0x460, beamFlag@0x46C, targetLocalFlag@0x470,
targetEntity@0x474. Firing state machine carried in weaponAlarm level @0x364 (0=Firing,2=Loaded,3=Loading,4=TriggerDuringLoad).
Attr table @0x511dd4 (ids 0x1D-0x25, issue #11 dump): LaserOn@0x418, LaserScale@0x42C,
LaserRotation@0x41C, CurrentSeekVoltageIndex@0x3F0, RecommendedSeekVoltageIndex@0x3F4,
MinSeekVoltageIndex@0x3F8, MaxSeekVoltageIndex@0x3FC, SeekVoltage@0x400, OutputVoltage@0x414
(currentLevel — RAW volts, the SeekVoltageGraph cursor feed). vtable slot 15 @4bb42c =
SeekVoltageResponse (the graph sampler): sqrt(damageFraction·v²·0.5·energyCoefficient / 2.0e8)
(power helper @4bb3f4; FUN_004dd138 = sqrt).
## Recovered resource structs (parsed by each CreateStreamedSubsystem)
- HeatableSubsystem/HeatSink: +0xE4 StartingTemperature, +0xE8 Degradation, +0xEC Failure, +0xF0 ThermalConductance, +0xF4 ThermalMass, +0xF8 HeatSink index
+216 -41
View File
@@ -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
}
}
//###########################################################################
+33 -13
View File
@@ -86,13 +86,22 @@
//#######################################################################
// SeekVoltageGraph -- the energy-weapon "seek voltage" curve display.
// SeekVoltageGraph -- the energy-weapon / myomer "seek voltage" response
// graph on the engineering detail pages (the "POWER" box).
// @004c6798 ctor / @004c68ac dtor / @004c6920 BecameActive / @004c6934 Execute.
// vtable PTR_FUN_0051a1fc. Reads the four SeekVoltage attribute indices off
// the weapon subsystem ("CurrentSeekVoltageIndex" / "Min" / "Max" /
// "SeekVoltage") and plots a 0..230 x 0..187 curve, with min/max tick marks
// and a live cursor at the current seek index. When the subsystem is
// destroyed (subsys+0x40==1) it shows a centred "destroyed" bitmap instead.
// vtable PTR_FUN_0051a1fc. Resolves the four SeekVoltage attribute
// POINTERS off the weapon subsystem (FUN_0041bfc0 returns subsystem+offset:
// "CurrentSeekVoltageIndex" / "MinSeekVoltageIndex" / "MaxSeekVoltageIndex"
// / "SeekVoltage") and plots the subsystem's voltage-response curve
// (vtbl slot 15 sampler: Emitter @004bb42c / Myomers @004b8f94) over the
// top data box (0x97,0x80)-(0x17d,0x13b): x = response(v)*230 (0..1
// normalised sqrt response), y = v/12000*187 (voltage axis), with per-gear
// tick marks (the current gear drawn as a full L) and, when draw_cursor,
// a live 8x8 XOR cursor at the subsystem's live voltage. When the
// subsystem is destroyed (simulationState@0x40==1) it shows a centred
// "destroyed" bitmap instead. Its Execute's full-box CLEAR (@004c6be4)
// is what erases the sibling eng pages' stale pixels on the shared Eng
// bit-plane -- the widget IS the top-box eraser (Gitea #10 finding A).
//#######################################################################
class SeekVoltageGraph :
public GraphicGauge
@@ -109,19 +118,30 @@
void BecameActive(); // @004c6920
void Execute(); // @004c6934
protected:
void ClearGraph(); // @004c6be4 (full-box erase)
void DrawTicks(); // @004c6c6c (per-gear ticks, XOR)
void DrawCursorBox(); // @004c6c30 (8x8 cursor, XOR)
char *destroyedImage; // @0x90 this[0x24]
int currentSeekIndex; // @0x94 this[0x25] attr idx
int minSeekIndex; // @0x98 this[0x26] attr idx
int maxSeekIndex; // @0x9C this[0x27] attr idx
int seekVoltageTable; // @0xA0 this[0x28] attr idx
Entity *voltageSubsystem; // @0xA4 this[0x29]
int *currentSeekIndexPtr; // @0x94 this[0x25] attr ptr (subsys+off)
int *minSeekIndexPtr; // @0x98 this[0x26] attr ptr
int *maxSeekIndexPtr; // @0x9C this[0x27] attr ptr
Scalar *seekVoltageTablePtr; // @0xA0 this[0x28] attr ptr (array base)
Scalar *liveVoltage; // @0xA4 this[0x29] the ctor's
// seek_voltage_pointer ("OutputVoltage"
// attr ptr / cluster seekValue cache) --
// Execute derefs it for the live cursor
Subsystem *subsystem; // @0xA8 this[0x2A]
Scalar previousVoltage; // @0xAC this[0x2B] (9999 = redraw)
Scalar previousVoltage; // @0xAC this[0x2B] cached response at
// 12000 V (9999 sentinel = force replot)
Scalar xScale; // @0xB0 this[0x2C] =230.0f
Scalar yScale; // @0xB4 this[0x2D] =187.0f
int destroyedShown; // @0xB8 this[0x2E]
Logical drawCursor; // @0xBC this[0x2F]
// helpers: SetupClip @004c6be4, DrawCursor @004c6c30, DrawTicks @004c6c6c
int cursorX; // @0xC0 this[0x30] (-1 = no cursor drawn)
int cursorY; // @0xC4 this[0x31]
int tickIndexShown; // @0xC8 this[0x32] gear index the tick
// highlight was last drawn at
};
+97 -7
View File
@@ -73,6 +73,7 @@
# include <testbt.hpp>
#endif
#include <hostmgr.hpp> // HostManager::GetEntityPointer -- the update-record target resolve (task #51)
#include <math.h> // sqrt -- the SeekVoltageResponse sampler (issue #11)
// E8 (bring-up): the player fire input. The controls mapper that would normally write
// the weapon's fireImpulse is bypassed (mech4.cpp), so EmitterSimulation reads this
@@ -153,13 +154,49 @@ Receiver::MessageHandlerSet&
MechWeapon::GetMessageHandlers()); // copy-inherit
return messageHandlers;
}
Simulation::AttributeIndexSet Emitter::AttributeIndex;
//
// issue #11: the AUTHENTIC Emitter attribute table (binary @0x511dd4, dumped
// records {id, name*, offset|1, 0}, ids 0x1D..0x25 -- a dense run chained
// straight after MechWeapon's 0x1C WeaponState, so no pads needed). The four
// Seek* rows are what the SeekVoltageGraph ctor (@004c6798) resolves by name;
// OutputVoltage here binds currentLevel@0x414 -- the RAW charge voltage (the
// graph's live-cursor feed), which the retired MechWeapon 0x1D port alias
// (rechargeLevel, 0..1) used to shadow. Function-local statics per the
// static-init-order rule (reconstruction-gotchas #9).
//
// The dense-prefix lock (gotcha #11): the run must start exactly one past
// MechWeapon's last id (0x1C WeaponState) == the binary's own numbering.
static_assert((int)Emitter::LaserOnAttributeID == 0x1D,
"Emitter attr run must chain at 0x1D (binary table @0x511dd4) -- "
"did a MechWeapon attribute get added/removed?");
Emitter::AttributeIndexSet&
Emitter::GetAttributeIndex()
{
static const Emitter::IndexEntry entries[] =
{
ATTRIBUTE_ENTRY(Emitter, LaserOn, firingActive), // 0x1D @0x418
ATTRIBUTE_ENTRY(Emitter, LaserScale, beamScale), // 0x1E @0x42C
ATTRIBUTE_ENTRY(Emitter, LaserRotation, beamOrientation), // 0x1F @0x41C
ATTRIBUTE_ENTRY(Emitter, CurrentSeekVoltageIndex, seekVoltageIndex), // 0x20 @0x3F0
ATTRIBUTE_ENTRY(Emitter, RecommendedSeekVoltageIndex, seekVoltageRecommendedIndex), // 0x21 @0x3F4
ATTRIBUTE_ENTRY(Emitter, MinSeekVoltageIndex, minSeekVoltageIndex), // 0x22 @0x3F8
ATTRIBUTE_ENTRY(Emitter, MaxSeekVoltageIndex, maxSeekVoltageIndex), // 0x23 @0x3FC
ATTRIBUTE_ENTRY(Emitter, SeekVoltage, seekVoltage), // 0x24 @0x400 (array base)
ATTRIBUTE_ENTRY(Emitter, OutputVoltage, currentLevel) // 0x25 @0x414 (RAW volts)
};
static Emitter::AttributeIndexSet attributeIndex(
ELEMENTS(entries), entries,
MechWeapon::GetAttributeIndex()
);
return attributeIndex;
}
Emitter::SharedData
Emitter::DefaultData(
&Emitter::ClassDerivations,
Emitter::GetMessageHandlers(),
MechWeapon::GetAttributeIndex(), // gauge wave: inherit temps/InputVoltage/OutputVoltage/PercentDone (was empty)
Emitter::GetAttributeIndex(), // own table (Seek*/Laser*/OutputVoltage) chained to MechWeapon's
Emitter::StateCount
);
@@ -590,7 +627,7 @@ int
}
int prev = seekVoltageIndex; // 0x3f0
int modulus = seekVoltageCount + 1; // 0x3fc + 1
int modulus = maxSeekVoltageIndex + 1; // 0x3fc + 1
int next = (prev + 1) % modulus;
seekVoltageIndex = next; // 0x3f0
if (prev < next)
@@ -663,6 +700,35 @@ void
}
}
//
// @004bb3f4 -- the seek discharge power at a supply voltage (issue #11,
// capstone disasm: fld v; fmul v; fmul [0x4bb428]=0.5f; fmul [this+0x454];
// -> * [this+0x444]):
// P(v) = damageFraction * (v^2 * 0.5 * energyCoefficient)
// i.e. the damage share of the E = 1/2 k V^2 discharge energy at voltage v.
//
Scalar
Emitter::SeekPower(Scalar voltage)
{
return damageFraction * (voltage * voltage * 0.5f * energyCoefficient); // 0x444 / 0x454
}
//
// @004bb42c (vtable slot 15) -- the SeekVoltageGraph voltage-response sampler
// (issue #11). Disasm: locals A = 0x4d3ebc20 (exactly 2.0e8f), B = 0;
// r = (SeekPower(v) - B) / (A - B); FUN_004dd138(r) == sqrt (part_015.c:4026 --
// the old "fp magnitude" reading was wrong). So: the graph's x-axis is the
// square root of the discharge power normalised to a 2.0e8 full scale.
//
Scalar
Emitter::SeekVoltageResponse(Scalar voltage)
{
Scalar r = (SeekPower(voltage) - 0.0f) / (2.0e8f - 0.0f); // 0x4d3ebc20
// (FUN_004dd138 routes a negative operand to the matherr handler; P >= 0
// always here -- guarded for port safety.)
return (r > 0.0f) ? (Scalar)sqrt((double)r) : 0.0f;
}
//#############################################################################
// Subsystem virtual overrides
@@ -855,7 +921,7 @@ Emitter::Emitter(
energyCoefficient = 1.0f; // 0x454
seekVoltageIndex = 0; // 0x3f0
seekVoltageRecommendedIndex = 0; // 0x3f4
seekVoltageCount = 0; // 0x3fc
maxSeekVoltageIndex = 0; // 0x3fc
for (int sv = 0; sv < 5; ++sv)
seekVoltage[sv] = 1.0f; // 0x400[]
@@ -894,7 +960,7 @@ Emitter::Emitter(
{
if (subsystem_resource->seekVoltage[i] == -1.0f) // _DAT_004bb3b0 sentinel
{
seekVoltageCount = i - 1; // 0x3fc
maxSeekVoltageIndex = i - 1; // 0x3fc (= count-1)
break;
}
// the authored values are FRACTIONS of the generator's rated
@@ -904,7 +970,7 @@ Emitter::Emitter(
}
seekVoltageRecommendedIndex = subsystem_resource->seekVoltageRecommendedIndex; // 0x3f4 <- +0x1d8
seekVoltageIndex = seekVoltageRecommendedIndex; // 0x3f0
seekStepCounter = 0; // 0x3f8
minSeekVoltageIndex = 0; // 0x3f8
// energyCoefficient = energyTotal / (seekVoltage[rec]^2 * 0.5)
// -> E = 0.5 * V^2 * EC lands exactly on energyTotal at V=seekV[rec]
@@ -935,7 +1001,7 @@ Emitter::Emitter(
// damageFraction = damageAmount / (damageAmount + heatCostToFire)
damageFraction = damageData.damageAmount /
(damageData.damageAmount + heatCostToFire); // 0x444
seekStepCounter = 0; // 0x3f0/0x45c bookkeeping
minSeekVoltageIndex = 0; // 0x3f8 (always 0 -- attr 0x22)
Check_Fpu();
}
@@ -1063,3 +1129,27 @@ Subsystem *CreateEmitterSubsystem(Mech *owner, int id, void *seg)
return (Subsystem *) new (Memory::Allocate(0x478))
Emitter(owner, id, (Emitter::SubsystemResource *)seg, Emitter::DefaultData);
}
//===========================================================================//
// issue #11 bridge -- the SeekVoltageGraph's voltage-response sample (the
// binary calls the SUBSYSTEM vtbl+0x3C / slot 15 virtually; the port's gauge
// TU cannot include the weapon headers, so the dispatch lives here in a
// complete-type TU per the databinding rule, gotcha 8). Emitter (and PPC,
// which IS an Emitter both by derivation and by factory) -> @004bb42c;
// everything else forwards to the Myomers bridge (myomers.cpp, @004b8f94).
//===========================================================================//
extern Scalar BTMyomersSeekSample(void *subsystem, Scalar voltage); // myomers.cpp
Scalar BTSeekVoltageSample(void *subsystem, Scalar voltage)
{
Entity *entity = (Entity *)subsystem;
if (entity == 0)
{
return 0.0f;
}
if (entity->IsDerivedFrom(Emitter::ClassDerivations))
{
return ((Emitter *)entity)->SeekVoltageResponse(voltage); // @004bb42c
}
return BTMyomersSeekSample(subsystem, voltage); // @004b8f94 (guarded)
}
+52 -3
View File
@@ -89,7 +89,29 @@ class NotationFile;
public:
static Derivation ClassDerivations;
static Receiver::MessageHandlerSet& GetMessageHandlers(); // inherits MechWeapon's (task #6)
static AttributeIndexSet AttributeIndex;
//
// The AUTHENTIC Emitter attribute table (binary @0x511dd4, 9 records
// {id, name, offset|1, 0}, ids 0x1D..0x25 chained after MechWeapon's
// 0x1C WeaponState -- dumped issue #11). The Seek* rows feed the
// SeekVoltageGraph engineering-page widget (ctor @004c6798 resolves
// them by NAME); OutputVoltage binds the RAW charge voltage
// currentLevel@0x414 (the graph's live cursor feed) -- NOT the 0..1
// rechargeLevel (the retired MechWeapon port alias).
//
enum {
LaserOnAttributeID = MechWeapon::NextAttributeID, // 0x1D @0x418 firingActive
LaserScaleAttributeID, // 0x1E @0x42C beamScale
LaserRotationAttributeID, // 0x1F @0x41C beamOrientation
CurrentSeekVoltageIndexAttributeID, // 0x20 @0x3F0
RecommendedSeekVoltageIndexAttributeID, // 0x21 @0x3F4
MinSeekVoltageIndexAttributeID, // 0x22 @0x3F8
MaxSeekVoltageIndexAttributeID, // 0x23 @0x3FC
SeekVoltageAttributeID, // 0x24 @0x400 (array base)
OutputVoltageAttributeID, // 0x25 @0x414 currentLevel (RAW volts)
NextAttributeID // 0x26
};
static AttributeIndexSet& GetAttributeIndex();
static SharedData DefaultData;
// Update record (replication) -- extends the MechWeapon record with the
@@ -172,6 +194,8 @@ class NotationFile;
// slot 9 TakeDamage @004bafc8 (NOT yet reconstructed)
// slot 10 ResetToInitialState (MechWeapon base @004b96ec)
// slot 13 PrintState (MechWeapon base @004b9d00)
// slot 15 SeekVoltageResponse @004bb42c (the SeekVoltageGraph sampler;
// Myomers' analog @004b8f94 shares the slot)
// slot 16 ReadyToDischarge (Simulation interface)
// slot 17 ComputeOutputVoltage (Simulation interface)
// slot 18 FireWeapon
@@ -184,6 +208,13 @@ class NotationFile;
void
PrintState(); // slot 13, @004bb014
// @004bb42c (vtable slot 15) -- the SeekVoltageGraph's voltage-response
// sampler: sqrt((SeekPower(v) - 0) / (2.0e8f - 0)) [FUN_004dd138 = sqrt;
// consts from the capstone disasm, issue #11]. Maps a supply voltage to
// the graph's 0..1 x-axis (normalised sqrt of the discharge power).
Scalar
SeekVoltageResponse(Scalar voltage); // slot 15, @004bb42c
//
// Beam-render accessors (the visible-beam layer draws each weapon's own
// beam from this live sim state -- the authentic per-weapon fire look).
@@ -233,6 +264,12 @@ class NotationFile;
// Internal charge / discharge helpers (non-virtual; called by the sim)
//
protected:
// @004bb3f4 -- the seek discharge power at a supply voltage:
// damageFraction * v^2 * 0.5 [_DAT_004bb428] * energyCoefficient.
// (== the E = 1/2 k V^2 discharge energy scaled by the damage split.)
Scalar
SeekPower(Scalar voltage); // @004bb3f4
// @004ba838 -- track the linked voltage source: derive a seek rate and
// integrate currentLevel; dump the I^2R heat back into the source.
void
@@ -313,8 +350,15 @@ class NotationFile;
// * beamLengthRatio -> is beamScale.z@0x434
int seekVoltageIndex; // @0x3F0 (param_1[0xfc]) current charge-curve index
int seekVoltageRecommendedIndex;// @0x3F4 (param_1[0xfd]) recommended index
int seekStepCounter; // @0x3F8 (param_1[0xfe]) reset 0
int seekVoltageCount; // @0x3FC (param_1[0xff]) index modulus / count-1
// issue #11 IDENTITY CORRECTION (binary attribute table @0x511dd4):
// @0x3F8 is published as "MinSeekVoltageIndex" and @0x3FC as
// "MaxSeekVoltageIndex" -- the old names (seekStepCounter, "reset 0" /
// seekVoltageCount, "count-1") were best-effort guesses that happened
// to carry exactly those values (min is always 0; max = count-1, the
// AdvanceSeekVoltage modulus-1). Renamed to the attribute truth --
// the exact analog of Myomers @0x328/@0x32C.
int minSeekVoltageIndex; // @0x3F8 (param_1[0xfe]) always 0
int maxSeekVoltageIndex; // @0x3FC (param_1[0xff]) count-1 (index modulus - 1)
Scalar seekVoltage[5]; // @0x400 (param_1[0x100..0x104]) per-index target voltages
Scalar currentLevel; // @0x414 (param_1[0x105]) accumulated charge
int firingActive; // @0x418 (param_1[0x106]) reset 0; FireWeapon sets 1
@@ -340,6 +384,11 @@ class NotationFile;
struct EmitterLayoutCheck {
static_assert(offsetof(Emitter, seekVoltageIndex) == 0x3F0, "Emitter::seekVoltageIndex @0x3F0 (MechWeapon must end 0x3F0)");
// issue #11: the four attribute-published Seek* slots (table @0x511dd4)
static_assert(offsetof(Emitter, minSeekVoltageIndex) == 0x3F8, "Emitter::minSeekVoltageIndex @0x3F8 (attr 0x22)");
static_assert(offsetof(Emitter, maxSeekVoltageIndex) == 0x3FC, "Emitter::maxSeekVoltageIndex @0x3FC (attr 0x23)");
static_assert(offsetof(Emitter, seekVoltage) == 0x400, "Emitter::seekVoltage @0x400 (attr 0x24)");
static_assert(offsetof(Emitter, firingActive) == 0x418, "Emitter::firingActive @0x418 (attr 0x1D LaserOn)");
static_assert(offsetof(Emitter, currentLevel) == 0x414, "Emitter::currentLevel @0x414");
static_assert(offsetof(Emitter, beamOrientation) == 0x41C, "Emitter::beamOrientation @0x41C (Quaternion 16B)");
static_assert(offsetof(Emitter, beamScale) == 0x42C, "Emitter::beamScale @0x42C");
+9 -1
View File
@@ -5214,11 +5214,19 @@ void
&& getenv("BT_PRESET_TEST"))
{
static int s_ptFrame = 0;
static int s_ptPulses = 0;
++s_ptFrame;
int t0 = atoi(getenv("BT_PRESET_TEST"));
if (t0 < 1) t0 = 600;
if (s_ptFrame >= t0 && (s_ptFrame - t0) % 120 == 0)
// issue #11: BT_PRESET_HOLD=<n> stops after n pulses so a page can
// be HELD indefinitely (steady-state ghost/graph verification --
// the 120-frame cycle otherwise phase-locks with BT_SHOT's 90-frame
// cadence, so a fixed fraction of shots lands on transition frames).
int holdAfter = getenv("BT_PRESET_HOLD") ? atoi(getenv("BT_PRESET_HOLD")) : 0;
if (s_ptFrame >= t0 && (s_ptFrame - t0) % 120 == 0
&& (holdAfter <= 0 || s_ptPulses < holdAfter))
{
++s_ptPulses;
gBTPresetCycle[0] = 1;
gBTPresetCycle[1] = 1;
gBTPresetCycle[2] = 1;
+10 -6
View File
@@ -118,10 +118,15 @@ static_assert((int)PoweredSubsystem::NextAttributeID
const MechWeapon::IndexEntry
MechWeapon::AttributePointers[]=
{
// task #16: the FULL binary table @0x511890 (eleven entries) + the
// port-extra OutputVoltage alias. Every member offset matches the
// binary's (layout static_assert-locked); TriggerState stays the streamed
// fire-button binding (id 0x13 PINNED).
// task #16: the FULL binary table @0x511890 (eleven entries). Every member
// offset matches the binary's (layout static_assert-locked); TriggerState
// stays the streamed fire-button binding (id 0x13 PINNED).
// issue #11: the port-extra 0x1D "OutputVoltage" alias (-> rechargeLevel) is
// RETIRED -- the binary MechWeapon table ends at 0x1C, and the alias SHADOWED
// the authentic Emitter-level OutputVoltage (currentLevel@0x414, RAW volts --
// the SeekVoltageGraph cursor feed) because AttributeIndexSet::Find walks
// lowest-id-first. Energy weapons now publish their own table (emitter.cpp
// ids 0x1D-0x25); no ballistic-weapon consumer ever bound the alias.
//
// The PADS fill our chain-vs-binary id gap (now 0x0F..0x11) with valid,
// named, never-bound entries (the binary's own ids here are the powered/
@@ -142,8 +147,7 @@ const MechWeapon::IndexEntry
ATTRIBUTE_ENTRY(MechWeapon, PipExtendedRange, pipExtendedRange), // 0x19 @0x348
ATTRIBUTE_ENTRY(MechWeapon, EstimatedReadyTime, estimatedReadyTime),// 0x1A @0x330
ATTRIBUTE_ENTRY(MechWeapon, RearFiring, rearFiring), // 0x1B @0x334 (task #68: real semantics)
ATTRIBUTE_ENTRY(MechWeapon, WeaponState, weaponAlarm), // 0x1C @0x350
ATTRIBUTE_ENTRY(MechWeapon, OutputVoltage, rechargeLevel) // 0x1D (port alias)
ATTRIBUTE_ENTRY(MechWeapon, WeaponState, weaponAlarm) // 0x1C @0x350 (binary table END)
};
MechWeapon::AttributeIndexSet&
+14 -11
View File
@@ -140,13 +140,14 @@ class CockpitHud;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Attribute Support (gauge data-binding wave)
//
// The energy/ballistic weapon engineering clusters read OutputVoltage (the
// charge/ready level -- animates as the weapon recharges after firing) and
// PercentDone (the recharge dial), both onto rechargeLevel (1.0 == ready).
// GetAttributeIndex() chains to PoweredSubsystem's so InputVoltage + the
// HeatSink temps also resolve; Emitter/PPC/ProjectileWeapon/MissileLauncher
// point their DefaultData at MechWeapon::GetAttributeIndex() to inherit it
// (they carried an EMPTY default-constructed index -> resolved NOTHING).
// The weapon engineering clusters read PercentDone (the recharge dial) onto
// rechargeLevel (1.0 == ready). GetAttributeIndex() chains to
// PoweredSubsystem's so InputVoltage + the HeatSink temps also resolve;
// ProjectileWeapon/MissileLauncher point their DefaultData at
// MechWeapon::GetAttributeIndex() to inherit it (they carried an EMPTY
// default-constructed index -> resolved NOTHING). Emitter/PPC now chain
// their OWN authentic table on top (emitter.cpp, ids 0x1D-0x25: Laser*/
// Seek*/OutputVoltage=currentLevel -- issue #11).
//
public:
enum {
@@ -203,10 +204,12 @@ class CockpitHud;
PipExtendedRangeAttributeID, // 0x19 @0x348
EstimatedReadyTimeAttributeID, // 0x1A @0x330
RearFiringAttributeID, // 0x1B @0x334
WeaponStateAttributeID, // 0x1C @0x350 weaponAlarm
OutputVoltageAttributeID, // 0x1D PORT-EXTRA alias -> rechargeLevel (the
// engineering clusters bind it by name)
NextAttributeID
WeaponStateAttributeID, // 0x1C @0x350 weaponAlarm (binary table END)
// issue #11: the 0x1D "OutputVoltage" PORT-EXTRA alias is RETIRED --
// it shadowed the authentic Emitter table's OutputVoltage
// (currentLevel@0x414 RAW volts, ids 0x1D-0x25 chain here exactly
// as in the binary; Find walks lowest-id-first).
NextAttributeID // 0x1D == the binary chain value
};
private:
static const IndexEntry AttributePointers[];
+33 -11
View File
@@ -40,8 +40,9 @@
// ConnectToMover/DisconnectFromMover bodies.
// best-effort : MyomersSimulation (@004b8d18) is the per-tick Performance
// (only reachable via function pointer; SetPerformance call
// site not captured -- see note); GetSpeedReading (@004b8f94,
// slot 15) purpose; ToggleSeekVoltage (@004b8a48) not present
// site not captured -- see note); [slot 15 @004b8f94 RESOLVED
// issue #11: SeekVoltageResponse -- the SeekVoltageGraph
// sampler, sqrt not fabs]; ToggleSeekVoltage (@004b8a48) not present
// in the decompiled shards; the heat-base field @0x150 and
// graphic-state block (this[7..9]) inherit-pattern stores.
// excluded : @0x4b7f94..@0x4b881c (gimbal / torso-horizon display, base
@@ -315,7 +316,7 @@ int Myomers::CreateStreamedSubsystem(
//***************************************************************************
//
// Core drive computation: returns the locomotion drive available for a given
// input voltage. (helper; called by RegisterMaxOutput and GetSpeedReading.)
// input voltage. (helper; called by RegisterMaxOutput and SeekVoltageResponse.)
//
// base = ownerMech[0x34C] * (input_voltage / seekVoltage[recommendedIndex])
// ^ Mech base-speed scalar ^ normalise to the recommended gear
@@ -372,19 +373,40 @@ void Myomers::RegisterMaxOutput()
//***************************************************************************
// Myomers::GetSpeedReading slot 15 @004b8f94
// Myomers::SeekVoltageResponse slot 15 @004b8f94
//***************************************************************************
//
// PoweredSubsystem virtual slot 15 (base @004b1780). Best-effort: converts
// AvailableOutput(input_voltage) into a normalised gauge reading -- output is
// scaled by 3.6 (m/s -> km/h) and divided by 350 (gauge full scale), then run
// through FUN_004dd138 (fp magnitude helper / sqrt). Drives a cockpit
// speed gauge. (Decompiled body discards the result -> returned via FPU.)
// PoweredSubsystem virtual slot 15 (base @004b1780) == the shared subsystem
// vtbl+0x3C sampler the SeekVoltageGraph's Execute (@004c6934) plots -- THIS
// is what draws the myomer engineering page's POWER curve (issue #11; the
// old "drives a cockpit speed gauge" guess and the GetSpeedReading name are
// retired). Converts AvailableOutput(input_voltage) into the graph's 0..1
// x-response: scaled by 3.6 (m/s -> km/h), normalised to a 350 km/h full
// scale, then FUN_004dd138 == SQRT (part_015.c:4026 -- the old "fp magnitude
// / fabs" reading was wrong; sqrt is also what Emitter's analog @004bb42c
// applies to its power ratio). (Decompiled body discards the result ->
// returned via FPU.)
//
Scalar Myomers::GetSpeedReading(Scalar input_voltage)
Scalar Myomers::SeekVoltageResponse(Scalar input_voltage)
{
Scalar output = AvailableOutput(input_voltage); // @004b8ac0
return (Scalar)fabs((double)((output * 3.6f - 0.0f) / 350.0f)); // FUN_004dd138 (fp magnitude)
Scalar r = (output * 3.6f - 0.0f) / 350.0f;
// (FUN_004dd138 routes a negative operand to the matherr handler; output
// >= 0 in practice -- guarded for port safety.)
return (r > 0.0f) ? (Scalar)sqrt((double)r) : 0.0f; // FUN_004dd138 (sqrt)
}
//***************************************************************************
// issue #11 bridge -- the Myomers leg of the SeekVoltageGraph sample dispatch
// (see emitter.cpp BTSeekVoltageSample). Guarded: a non-Myomers subsystem
// samples 0 (only emitter/myomer pages own a graph in the shipped config).
//***************************************************************************
Scalar BTMyomersSeekSample(void *subsystem, Scalar voltage)
{
Entity *entity = (Entity *)subsystem;
if (entity == 0 || !entity->IsDerivedFrom(Myomers::ClassDerivations))
return 0.0f;
return ((Myomers *)entity)->SeekVoltageResponse(voltage); // @004b8f94
}
+8 -3
View File
@@ -245,10 +245,15 @@ class Mech;
HandleMessage(int message); // slot 9, @004b8a8c (-> PoweredSubsystem @004b0efc)
void
ResetToInitialState(Logical powered = True); // slot 10, @004b8aa4 (-> PoweredSubsystem @004b0e6c)
// slot 15 (PoweredSubsystem base @004b1780) -- normalised speed/gauge readout
// (output km/h scaled to 350): GetSpeedReading @004b8f94
// slot 15 (PoweredSubsystem base @004b1780) -- the SeekVoltageGraph's
// voltage-response sampler (issue #11: the shared subsystem
// vtbl+0x3C slot the graph's Execute @004c6934 calls; Emitter's
// analog is @004bb42c): sqrt(AvailableOutput(v)*3.6 / 350)
// -- normalised sqrt of the drive speed in km/h against a
// 350 km/h full scale. (Renamed from the best-effort
// "GetSpeedReading"; FUN_004dd138 is sqrt, not fabs.)
Scalar
GetSpeedReading(Scalar input_voltage); // slot 15, @004b8f94 (name best-effort)
SeekVoltageResponse(Scalar input_voltage); // slot 15, @004b8f94
// slot 16 (PoweredSubsystem::IsSourceShorted base @004b0b5c) -- Myomers
// requires the source to supply at least the selected seek voltage
// AND be in the Ready (==2) state.
+15
View File
@@ -1449,3 +1449,18 @@ bool BTGetSubsystemAuxScreen(Subsystem *sub, int *screen, int *placement, char *
if (label64 != NULL) strcpy(label64, ps->auxScreenLabel); // +0x10C
return true;
}
//
// issue #11 bridge -- the SeekVoltageGraph's destroyed test:
// *(subsystem+0x40) == 1 in the binary (@004c6934) == MechSubsystem::
// simulationState == 1 (Destroyed). Named-member read through the real type
// (databinding rule); every subsystem that owns a graph is MechSubsystem-
// derived (Emitter/PPC/Myomers), so the PoweredSubsystem gate is sufficient
// and a non-match reads "not destroyed".
//
int BTSubsystemDestroyed(Subsystem *sub)
{
if (!BTIsPoweredSubsystem(sub))
return 0;
return (((PoweredSubsystem *)sub)->GetSimulationState() == 1) ? 1 : 0; // +0x40 == 1
}