Torso: the TWIST goes LIVE -- electrical watchdog chain, centered crosshair, coherent controls (task #57/#58)
The MadCat torso twists, the view turns with it, and targeting follows. Three reconstruction fronts closed: THE ELECTRICAL WATCHDOG CHAIN (why the torso never powered up): - PowerWatcher::UpdateWatch reconstructed (@004b181c, the REAL registered Performance -- PTR @0050f5fc; Ghidra missed the fn start): the watchdog MIRRORS the watched subsystem's electrical level (+0x278), brownout downgrade when gen output <= minVoltage% x rated. @004b1804 relabeled ResetToInitialState (slot 10) -- the old "Simulation" tag was wrong. - The factory watcher-CONNECT pass reconstructed (vtable slot +0x38, @004aee2c/@004b1a40 byte-identical, recovered from raw exe bytes): watchedLink.Add(roster[watchedSubsystem]) on the master node. Was the SubProxy::Start() no-op -- every watchdog sat at 0 forever. - MinVoltageScale = 0.01 (a 10-byte x87 literal @0x4b1924; was 1.0f = permanent brownout) and PowerWatcher's Derivation chains its REAL base HeatWatcher (the HeatableSubsystem stand-in broke IsDerivedFrom for the whole Torso/Searchlight/ThermalSight family). - KB correction swept: derivation tag 0x50e604 = HEATWATCHER (not "HeatSink"); the btl4gaug heat-widget gate now tests it via the BTIsHeatWatcher bridge. THE CROSSHAIR (task #58 forensics, 6-agent workflow + live probes): - The VIEW is TORSO-MOUNTED: jointtorso -> jointeye -> siteeyepoint in every twist-capable .SKL; the camera + canopy ride the same hinge subtree through HingeRenderable's live matrix-stack compose -- ALREADY WORKING in the port. The crosshair stays screen-centered (center IS the boresight); the twist reads on the tape carets/compass/radar. - The real bug was the port's gBTAimX = tan(twist) slew (the falsified "body-mounted view" model): the camera already carried the twist, so the crosshair counter-slid to hull-forward and the fire ray with it. Deleted; the pick ray inherits the twist from the yawing eye basis. - Two instrumentation traps documented (chase-eye-as-default-camera, BT_FORCE_TORSO clobbering real joints -> the hook now only fills unresolved ones); an over-correcting explicit eye compose was added on those false readings and retired the same day. CONTROLS + REPLICATION: - Q/E spring-center on release (the axis is a twist-RATE demand; the old hold-deflection model drifted forever); X also zeroes the axis and pulses the authentic torso Recenter (@004b6918). M cycles control mode via the real CycleControlMode body. - Torso update-record DIRECTION fixed: engine truth is Write=serialize / Read=apply; @004b6a78 is the READ (was mislabeled Write) and the missing WRITE @004b6a1c recovered from raw disasm (recordLength 0x1C, twist/vel/rate at +0x10/14/18) -- kills the replicant's 0xCDCDCDCD -140-degree ghost twist. - Marching-ghost desync: 4 Standing-case guards zero stale reverse cycleSpeed (negative cadence passed the <= ZeroSpeed stop gate). - Kill credit rerouted to the OBSERVED killer (lastInflictingID -> killer's player link) -- kills count, target K/D populates. KB: subsystems.md (watcher chain), multiplayer.md (record direction), combat-damage.md + gauges-hud.md + cockpit-view.md (torso-mounted view re-correction), decomp-reference.md (new addresses + tag fix), open-questions.md (dead capability-roster loops 2-4, snapshot CD read). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
065c114590
commit
02cdfd6576
@@ -52,7 +52,8 @@
|
||||
// FUN_0044a5b4/5dc GraphicsViewRecord ctor/dtor
|
||||
// FUN_0044a650/630 GraphicsViewRecord Erase / Reset
|
||||
// DAT_00524e20 DebugStream (warning channel)
|
||||
// 0x50e3ec / 0x50e604 heat-subsystem class-derivation tables
|
||||
// 0x50e3ec / 0x50e604 heat class-derivation tags (HeatableSubsystem /
|
||||
// HEATWATCHER -- task #57 relabel; was "HeatSink")
|
||||
//
|
||||
// GraphicsView vtable slots used below (this+0x48):
|
||||
// +0x08 SetExtent +0x10 SetOrigin +0x18 SetColor +0x24 MoveTo
|
||||
@@ -363,9 +364,12 @@ void
|
||||
//
|
||||
// HeatConnection -- @004c3664 ctor / @004c3720 Transfer.
|
||||
// ctor: source = the heat subsystem; flag@0x14 records whether it is the
|
||||
// "primary" heat class (IsDerivedFrom 0x50e3ec) vs. the alternate (0x50e604);
|
||||
// destination@0x18. Transfer: no subsystem -> 100 ; subsystem operational
|
||||
// (+0x40 == 1) -> 100 ; else Round( Resolve(subsys+0x114 currentTemperature) ).
|
||||
// "primary" heat class (IsDerivedFrom 0x50e3ec HeatableSubsystem) vs. the
|
||||
// alternate (0x50e604 = HEATWATCHER, task #57 relabel); destination@0x18.
|
||||
// Transfer: no subsystem -> 100 ; subsystem operational (+0x40 == 1) -> 100 ;
|
||||
// else Round( Resolve(subsys+0x114) ) -- NB for a Heatable that offset is
|
||||
// currentTemperature, for a WATCHER it is watchedLink (the flag@0x14 likely
|
||||
// picks the read; the watcher path is unverified [T4]).
|
||||
// Used by ColorMapperHeat -- THIS is the heat gauge's data feed.
|
||||
//
|
||||
// All Transfer bodies are reproduced inside the owning gauge's notes; the
|
||||
@@ -925,8 +929,13 @@ ColorMapperHeat::ColorMapperHeat(
|
||||
// The subsystem MUST be heat-bearing (one of the two heat class
|
||||
// derivation tables) or the data feed is meaningless.
|
||||
//
|
||||
extern int BTIsHeatWatcher(Subsystem *sub); // heatfamily_reslice.cpp bridge
|
||||
if (!subsystem->IsDerivedFrom(*HeatableSubsystem::GetClassDerivations()) && // FUN_0041a1a4 (0x50e3ec)
|
||||
!subsystem->IsDerivedFrom(*HeatSink::GetClassDerivations())) // (0x50e604)
|
||||
!BTIsHeatWatcher(subsystem)) // (0x50e604 = HEATWATCHER -- task #57 relabel:
|
||||
// the old "HeatSink" reading was redundant after
|
||||
// the HeatableSubsystem test; the binary's
|
||||
// alternate branch is the disjoint WATCHER family
|
||||
// (ammo bins / torso carry a mirrored heatAlarm))
|
||||
{
|
||||
Verify(False); // FUN_0040385c -- "Bad subsystem type" BTL4GAUG.CPP:0x68a
|
||||
}
|
||||
|
||||
@@ -203,7 +203,8 @@
|
||||
// ColorMapperHeat -- the cockpit heat tint. ANCHOR @004c3f6c.
|
||||
// Connection: @004c3664 (reads HeatableSubsystem::currentTemperature@0x114
|
||||
// as a 0..100 percentage). The named subsystem must derive from a heat
|
||||
// class (IsDerivedFrom 0x50e3ec or 0x50e604) or the ctor asserts (line 0x68a).
|
||||
// class (IsDerivedFrom 0x50e3ec HeatableSubsystem or 0x50e604 HEATWATCHER
|
||||
// -- task #57 relabel) or the ctor asserts (line 0x68a).
|
||||
//
|
||||
class ColorMapperHeat :
|
||||
public ColorMapper
|
||||
|
||||
@@ -49,6 +49,9 @@
|
||||
#if !defined(MECHWEAP_HPP)
|
||||
# include <mechweap.hpp> // MechWeapon::GetClassDerivations (reticle pip)
|
||||
#endif
|
||||
#if !defined(MECHDMG_HPP)
|
||||
# include <mechdmg.hpp> // Mech__DamageZone::segmentIndex (the @004d097c dispatcher)
|
||||
#endif
|
||||
#if !defined(NOTATION_HPP)
|
||||
# include <notation.hpp>
|
||||
#endif
|
||||
@@ -368,6 +371,22 @@ HierarchicalDrawComponent*
|
||||
LinearMatrix offset_matrix;
|
||||
offset_matrix = segment->GetBaseOffset(); // [0x74]
|
||||
|
||||
// task #58 diagnostics (BT_SEG_DUMP): the draw-tree topology -- which
|
||||
// segment carries which joint, and who parents whom.
|
||||
if (getenv("BT_SEG_DUMP"))
|
||||
{
|
||||
int ji = segment->GetJointIndex();
|
||||
DEBUG_STREAM << "[seg] " << segment->GetIndex()
|
||||
<< " '" << (const char *)segment->GetName()
|
||||
<< "' parent=" << (segment->GetParent()
|
||||
? segment->GetParentIndex() : -1)
|
||||
<< " joint=" << ji
|
||||
<< " jtype=" << (ji == -1 ? -1
|
||||
: (int)joint_subsystem->GetJoint(ji)->GetJointType())
|
||||
<< " site=" << (int)(segment->IsSiteSegment() != 0)
|
||||
<< "\n" << std::flush;
|
||||
}
|
||||
|
||||
//
|
||||
// Parent renderable: root if the segment has no parent, else the
|
||||
// renderable already built for its parent segment.
|
||||
@@ -866,6 +885,116 @@ void BTRebuildMechModel(Entity *entity)
|
||||
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// StartEntityEffectImplementation @004d097c (coverage-audit reconstruction)
|
||||
//#############################################################################
|
||||
//
|
||||
// The per-zone EFFECT DISPATCHER -- the target of the whole authentic chain:
|
||||
// MechDeathHandler's class-5 -> RendererManager::StartEntityEffect ->
|
||||
// Renderer::StartEntityEffectMessageHandler [T0: resolves the GameModel +
|
||||
// applies the ExplosionResourceTable graphic-state remap] -> this virtual.
|
||||
// The binary body (@004d097c, part_014.c:5839):
|
||||
// 1. resolve the zone -> its transform (+0x74, row 3 = the world position)
|
||||
// and its VIDEO INDEX (+0xc4->+0xc0) = the segment slot;
|
||||
// 2. look the entity up in the renderer's per-entity tree (renderer+0x3a4,
|
||||
// == our mMechRenderTrees) -> the segment's DCS = the effect SOCKET;
|
||||
// 3. SearchList(resource, VideoModelResourceType) -> walk the video-object
|
||||
// records; atoi(name) < 1000 WARNS (the authentic gate: only INDIE/psfx
|
||||
// ids attach per-zone), >= 1000 starts the effect ON the socket, tagged
|
||||
// with the owning player (+0x190 -> +0x1e0) for the stop-all sweep.
|
||||
// Port mapping: the socket attachment = BTStartPfxAttached (the emitter rides
|
||||
// the segment via BTResolveSegmentWorld each frame); the tag = the entity
|
||||
// (BTStopEntityPfx kills by entity on respawn). Layout access is by NAMED
|
||||
// members only (zone->segmentIndex; the binary micro-offsets differ on our
|
||||
// compiled classes -- the databinding trap).
|
||||
//
|
||||
void
|
||||
BTL4VideoRenderer::StartEntityEffectImplementation(
|
||||
Entity *entity,
|
||||
DamageZone *damage_zone,
|
||||
ResourceDescription::ResourceID resource_ID
|
||||
)
|
||||
{
|
||||
if (entity == NULL || damage_zone == NULL)
|
||||
{
|
||||
DEBUG_STREAM << "StartEntityEffectImplementation: no entity/zone" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. the zone's segment slot (binary +0xc4->+0xc0; our named member)
|
||||
int seg_index = ((Mech__DamageZone *)damage_zone)->EffectSegmentIndex();
|
||||
|
||||
// 2. the segment's world position + frame (binary: zone transform +0x74)
|
||||
extern int BTResolveSegmentWorld(void *entity, int seg_index, float *pos3, float *rows9);
|
||||
float pos[3], rows[9];
|
||||
if (!BTResolveSegmentWorld(entity, seg_index, pos, rows))
|
||||
{
|
||||
DEBUG_STREAM << "StartEntityEffectImplementation: entity has no segment table" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. the effect resource's VIDEO MODEL (type 10) -- the effect-number list
|
||||
Check(application);
|
||||
ResourceDescription *res = application->GetResourceFile()->SearchList(
|
||||
resource_ID, ResourceDescription::VideoModelResourceType);
|
||||
if (res == NULL)
|
||||
{
|
||||
DEBUG_STREAM << "StartEntityEffectImplementation: " << (long)resource_ID
|
||||
<< " has no video resource" << std::endl;
|
||||
return;
|
||||
}
|
||||
res->Lock();
|
||||
// VideoModel payload [T0 L4VIDEO.h L4VideoObject, RES byte-verified]:
|
||||
// int32 count + count x 32-byte records {char name[15]; pad; int type;
|
||||
// int modes; float blinkPeriod; float pctOn} -- atoi(name) = the effect id.
|
||||
const unsigned char *pay = (const unsigned char *)res->resourceAddress;
|
||||
int count = *(const int *)pay;
|
||||
if (count < 0 || count > 16)
|
||||
count = 0; // malformed -- refuse
|
||||
const unsigned char *rec = pay + 4;
|
||||
for (int i = 0; i < count; ++i, rec += 32)
|
||||
{
|
||||
char name[16];
|
||||
memcpy(name, rec, 15);
|
||||
name[15] = 0;
|
||||
int fx = atoi(name);
|
||||
if (fx < 1000)
|
||||
{
|
||||
// authentic gate (@004d097c: "< 1000" warns) -- per-zone effects
|
||||
// are INDIE/psfx-band only
|
||||
DEBUG_STREAM << "StartEntityEffectImplementation: non-INDIE effect "
|
||||
<< fx << " in zone effect list" << std::endl;
|
||||
continue;
|
||||
}
|
||||
extern void BTStartPfxAttached(int, void *, int, float, float, float, const float *);
|
||||
BTStartPfxAttached(fx - 1000, (void *)entity, seg_index,
|
||||
pos[0], pos[1], pos[2], rows);
|
||||
if (getenv("BT_DEATH_LOG"))
|
||||
DEBUG_STREAM << "[zonefx] entity " << entity->GetEntityID()
|
||||
<< " seg " << seg_index << " psfx " << (fx - 1000)
|
||||
<< " at(" << pos[0] << "," << pos[1] << "," << pos[2] << ")" << std::endl;
|
||||
}
|
||||
res->Unlock();
|
||||
}
|
||||
|
||||
//
|
||||
// StopAllEntityEffectsImplementation @004d0c14 -- the respawn cleanup: the
|
||||
// binary kills every effect tagged (playerIdx<<16 .. |0xffff); the port kills
|
||||
// every emitter tagged to the entity.
|
||||
//
|
||||
void
|
||||
BTL4VideoRenderer::StopAllEntityEffectsImplementation(Entity *entity)
|
||||
{
|
||||
if (entity == NULL)
|
||||
{
|
||||
DEBUG_STREAM << "StopAllEntityEffectsImplementation: no entity" << std::endl;
|
||||
return;
|
||||
}
|
||||
extern void BTStopEntityPfx(void *owner);
|
||||
BTStopEntityPfx((void *)entity);
|
||||
}
|
||||
|
||||
|
||||
//#############################################################################
|
||||
// SwapToWreck (ExplosionScripts effect 104, reconstructed)
|
||||
//#############################################################################
|
||||
|
||||
@@ -623,6 +623,26 @@ extern void BTDrawReticle(struct IDirect3DDevice9 *device);
|
||||
Logical
|
||||
TestInstance() const;
|
||||
|
||||
//
|
||||
// The per-zone EFFECT DISPATCHER (@004d097c, coverage-audit discovery
|
||||
// 2026-07-13) -- the missing target of the engine's whole authentic
|
||||
// StartEntityEffect chain (MechDeathHandler class-5 -> RendererManager
|
||||
// -> Renderer::StartEntityEffectMessageHandler [T0, applies the
|
||||
// ExplosionResourceTable graphic-state remap] -> THIS virtual).
|
||||
// Resolves the zone's SEGMENT socket, walks the effect resource's
|
||||
// VideoModel (type 10), and starts each >=1000 effect ATTACHED to the
|
||||
// segment (the smoke follows the walking mech); <1000 warns
|
||||
// (byte-faithful to the binary's gate). StopAll kills every effect
|
||||
// tagged to the entity (the respawn cleanup, @004d0c14 analog).
|
||||
//
|
||||
virtual void
|
||||
StartEntityEffectImplementation(
|
||||
Entity *entity,
|
||||
DamageZone *damage_zone,
|
||||
ResourceDescription::ResourceID resource_ID); // @004d097c
|
||||
virtual void
|
||||
StopAllEntityEffectsImplementation(Entity *entity); // @004d0c14
|
||||
|
||||
//
|
||||
// Material substitution (mirrors RPL4VideoRenderer).
|
||||
//
|
||||
|
||||
@@ -1494,12 +1494,31 @@ void BTPostKillScore(Entity *victim, Scalar damage) // Step 7: KILL (+ MP deat
|
||||
{
|
||||
return;
|
||||
}
|
||||
BTPlayer *local_player = (BTPlayer *)application->GetMissionPlayer();
|
||||
if (local_player != 0)
|
||||
// OBSERVED-KILL credit (MP KILLS fix, 2026-07-13; the symmetric twin of
|
||||
// BTPlayerCountObservedDeath): every node maintains LOCAL score copies,
|
||||
// so the kill must land on the KILLER's player-link object -- the same
|
||||
// object the PilotList gauge reads (the roster resolves pilots via the
|
||||
// mechs' GetPlayerLink()). The old GetMissionPlayer() dispatch credited
|
||||
// a copy the scoreboard never displays (kills stuck at 0 while deaths --
|
||||
// which already flow through the player link -- counted; user-reported).
|
||||
// Resolving the killer from the victim's lastInflictingID also credits
|
||||
// the REMOTE player's local copy when THEY score ("target's kills
|
||||
// display 0") -- each node witnesses the death transition and tallies
|
||||
// its own copies self-consistently.
|
||||
BTPlayer *killer_player = 0;
|
||||
if (application->GetHostManager() != 0)
|
||||
{
|
||||
// KillScore (scoreType 2): senderMechID MUST be the VICTIM (!= our mech) so
|
||||
// the local player is credited via `this->killCount++` -- if it were our own
|
||||
// mech, ScoreMessageHandler takes the suicide branch (award negated, no kill).
|
||||
Entity *killer = application->GetHostManager()->GetEntityPointer(
|
||||
((Mech *)victim)->lastInflictingID);
|
||||
extern int BTIsRegisteredMech(Entity *e);
|
||||
if (killer != 0 && killer != victim && BTIsRegisteredMech(killer))
|
||||
killer_player = (BTPlayer *)((Mech *)killer)->GetPlayerLink();
|
||||
}
|
||||
if (killer_player != 0)
|
||||
{
|
||||
// KillScore (scoreType 2): senderMechID MUST be the VICTIM (!= the
|
||||
// receiver's mech) so the handler credits `killCount++` -- a suicide
|
||||
// (killer == victim) never reaches here.
|
||||
BTPlayer::ScoreMessage kill(
|
||||
Player::ScoreMessageID, // 0x12
|
||||
sizeof(BTPlayer::ScoreMessage),
|
||||
@@ -1507,13 +1526,12 @@ void BTPostKillScore(Entity *victim, Scalar damage) // Step 7: KILL (+ MP deat
|
||||
0.0f, // scoreAward (killBonus; 0 for bring-up)
|
||||
damage, // damageAmount (killing-blow)
|
||||
victim->GetEntityID()); // senderMechID = victim
|
||||
local_player->Dispatch(&kill);
|
||||
killer_player->Dispatch(&kill);
|
||||
if (getenv("BT_SCORE_LOG"))
|
||||
DEBUG_STREAM << "[score] *** KILL *** localPlayer=" << (void *)local_player
|
||||
<< " killCount=" << local_player->GetKillCount()
|
||||
<< " deaths=" << local_player->GetDeaths()
|
||||
<< " score=" << (Scalar)local_player->GetScore()
|
||||
<< " rank=" << local_player->GetRanking() << "\n" << std::flush;
|
||||
DEBUG_STREAM << "[score] *** KILL *** killerPlayer=" << (void *)killer_player
|
||||
<< " killCount=" << killer_player->GetKillCount()
|
||||
<< " deaths=" << killer_player->GetDeaths()
|
||||
<< " score=" << (Scalar)killer_player->GetScore() << std::endl;
|
||||
}
|
||||
|
||||
// MP DEATH: credit a death to the VICTIM's own player. NULL for the solo
|
||||
|
||||
@@ -337,16 +337,14 @@ void
|
||||
{
|
||||
HeatableSubsystem *watched = (HeatableSubsystem *)watchedLink.Resolve(); // FUN_00417ab4(this+0x114)
|
||||
|
||||
// BRING-UP GUARD (marked deviation): in the shipped game watchedLink ALWAYS
|
||||
// resolves -- the HeatWatcher resource's "WatchedSubsystem" name binds to a
|
||||
// roster segment at load -- so the binary (FUN_004aeac4) derefs `watched`
|
||||
// unconditionally. Our port does not yet resolve the watch link, so Resolve()
|
||||
// returns null; the deref AVs the moment a HeatWatcher-derived subsystem's
|
||||
// Performance actually runs (exposed by the WAVE 4 un-stub: AmmoBinSimulation
|
||||
// calls this directly; PowerWatcher::Simulation for Searchlight/ThermalSight).
|
||||
// With no resolved source, hold the heat alarm at Normal (same established
|
||||
// pattern as Emitter::TrackSeekVoltage's null-source guard). FAITHFUL
|
||||
// FOLLOW-UP = resolve watchedLink so the alarm tracks the real temperature.
|
||||
// The watch link is now BOUND by the authentic factory connect pass (task
|
||||
// #57: the mech factory's post-roster loop calls the binary's vtable slot
|
||||
// +0x38 -- FUN_004aee2c / FUN_004b1a40 -> BTWatcherBindTarget below), so
|
||||
// Resolve() succeeds on every master-node watcher whose resource named a
|
||||
// WatchedSubsystem. The binary derefs `watched` unconditionally; the null
|
||||
// guard stays for replicant nodes (the binary never binds there either --
|
||||
// the +0x38 body is master-gated -- and their watcher Performances don't
|
||||
// run) and for bring-up safety.
|
||||
if (watched == 0)
|
||||
{
|
||||
heatAlarm.SetLevel(0); // NormalHeat (no watched source yet)
|
||||
@@ -369,6 +367,51 @@ void
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// @004aee2c (HeatWatcher vtable slot 14, offset +0x38; the PowerWatcher/Torso
|
||||
// override @004b1a40 is byte-identical) -- the factory post-roster CONNECT
|
||||
// pass, disassembled from the raw binary (Ghidra missed both function starts):
|
||||
//
|
||||
// owner = this->owner (+0xD0)
|
||||
// if ((owner->simulationFlags & 0xC) == 0 && (owner->simulationFlags & 0x100))
|
||||
// watchedLink(+0x114).Add( owner->roster(+0x128)[ watchedSubsystem(+0x128) ] )
|
||||
//
|
||||
// The mech factory loop tests IsDerivedFrom(HeatWatcher @0x50e604) then calls
|
||||
// the slot. Split across the TU boundary the same way as the other
|
||||
// cross-family helpers: the OWNER-side master gate + roster lookup live in the
|
||||
// mech family (complete Mech type); these two bridges are the family-side
|
||||
// derivation test and the link Add. [T1: bytes @004aee2c/@004b1a40]
|
||||
//
|
||||
int
|
||||
BTWatcherWatchedIndex(Subsystem *sub) // -1 = not a HeatWatcher
|
||||
{
|
||||
if (sub == 0 || !sub->IsDerivedFrom(*HeatWatcher::GetClassDerivations()))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
return ((HeatWatcher *)sub)->watchedSubsystem; // @0x128 (resource +0xE4, name index +2)
|
||||
}
|
||||
|
||||
void
|
||||
BTWatcherBindTarget(Subsystem *sub, Subsystem *target)
|
||||
{
|
||||
((HeatWatcher *)sub)->watchedLink.Add(target); // (**(link+4))(link, target) = FUN_00417a80
|
||||
}
|
||||
|
||||
//
|
||||
// Cross-family derivation probe: 0x50e604 is the HEATWATCHER derivation tag
|
||||
// (TestInstance @4aec38 checks its own class against it) -- NOT HeatSink, as
|
||||
// an old btl4gaug label claimed (HeatSink : HeatableSubsystem would make an
|
||||
// OR-test after HeatableSubsystem redundant; the binary's alternate branch is
|
||||
// the disjoint watcher family). Bridged so gauge TUs can test without
|
||||
// including this family's headers.
|
||||
//
|
||||
int
|
||||
BTIsHeatWatcher(Subsystem *sub)
|
||||
{
|
||||
return sub != 0 && sub->IsDerivedFrom(*HeatWatcher::GetClassDerivations());
|
||||
}
|
||||
|
||||
//
|
||||
// @4aec54 (468 bytes) -- parse the HeatWatcher resource. Stamps classID 0x0BBF
|
||||
// / size 0xF0; defaults the watched index to -1; requires DegradationTemperature,
|
||||
|
||||
@@ -1187,12 +1187,40 @@ Mech::Mech(
|
||||
//
|
||||
// Build the capability sub-rosters by IsDerivedFrom() class tests.
|
||||
//
|
||||
for (int id = 2; id < subsystemCount; ++id) // "start" each subsystem
|
||||
// Loop 1 = the watcher CONNECT pass (task #57). The binary calls vtable
|
||||
// slot +0x38 on every HeatWatcher-derived subsystem (@0x50e604 test);
|
||||
// the slot bodies (FUN_004aee2c, PowerWatcher/Torso override @004b1a40,
|
||||
// byte-identical -- Ghidra missed both starts, recovered from raw bytes)
|
||||
// bind watchedLink to roster[watchedSubsystem], gated on the owner being
|
||||
// the live master node ((flags & 0xC)==0 && (flags & 0x100)). The gate is
|
||||
// hoisted out of the loop unchanged (constant per mech). This replaces
|
||||
// the old SubProxy::Start() no-op that left every watchdogAlarm at 0 --
|
||||
// which held the Torso's ElectricalStateLevel() below Ready and zeroed the
|
||||
// twist rate forever.
|
||||
//
|
||||
if ((simulationFlags & 0xC) == 0 // SegmentCopyMask
|
||||
&& (simulationFlags & 0x100) != 0) // MasterHeatSinkFlag
|
||||
{
|
||||
SubProxy *s = (SubProxy *)subsystemArray[id];
|
||||
if (s != 0 && s->IsDerivedFrom(0x50e604)) // FUN_0041a1a4
|
||||
extern int BTWatcherWatchedIndex(Subsystem *sub); // heatfamily_reslice.cpp
|
||||
extern void BTWatcherBindTarget(Subsystem *sub, Subsystem *target);
|
||||
for (int id = 2; id < subsystemCount; ++id)
|
||||
{
|
||||
s->Start(); // (**(s+0x38))(s)
|
||||
int watched = BTWatcherWatchedIndex(subsystemArray[id]); // -1 = not a HeatWatcher
|
||||
if (watched < 0)
|
||||
continue;
|
||||
if (watched < subsystemCount && subsystemArray[watched] != 0)
|
||||
{
|
||||
BTWatcherBindTarget(subsystemArray[id], subsystemArray[watched]);
|
||||
DEBUG_STREAM << "[watch] subsystem " << id << " watches "
|
||||
<< watched << std::endl;
|
||||
}
|
||||
else
|
||||
{
|
||||
// The binary has no range check (CreateStreamed validated the
|
||||
// name); flag data drift honestly instead of a wild read.
|
||||
DEBUG_STREAM << "[watch] subsystem " << id
|
||||
<< " BAD WatchedSubsystem index " << watched << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int id = 2; id < subsystemCount; ++id) // heatable roster
|
||||
|
||||
@@ -570,6 +570,20 @@ Scalar
|
||||
switch (legAnimationState) // this+0x3b0
|
||||
{
|
||||
case StandingAnimation: // 0
|
||||
// STANDING ZEROES THE CYCLE (reverse-stop desync, live-diagnosed
|
||||
// 2026-07-13): a REVERSE cadence is NEGATIVE, so the walk-family stop
|
||||
// gate (cycleSpeed <= ZeroSpeed) passes while still cycling at full
|
||||
// reverse speed, and several stand-entry paths (turn exit, terminal
|
||||
// poses) never touch the cycle -- Standing could be entered with a
|
||||
// stale legCycleSpeed = -2.507 ([gaitSM] state=0 evidence). The master
|
||||
// LOOKS still (case 0 never advances the clip) but the stale cycle
|
||||
// REPLICATES and the peer's replicant marches in place. A standing
|
||||
// mech's cycle is 0 (the clean forward-stop log: legSum ~3e-8).
|
||||
if (legCycleSpeed != 0.0f)
|
||||
{
|
||||
legCycleSpeed = 0.0f;
|
||||
ForceUpdate(8); // type-3 record: legs stopped
|
||||
}
|
||||
// RAW (part_012.c FUN_004a5028 case 0): standSpeed < commandedSpeed ->
|
||||
// begin WALKING (state 5); 0 <= commanded < standSpeed -> stay standing;
|
||||
// commanded < 0 -> stand-to-reverse (0x10). (The earlier draft had the
|
||||
@@ -823,6 +837,20 @@ Scalar
|
||||
switch (bodyAnimationState) // this+0x728
|
||||
{
|
||||
case StandingAnimation: // 0
|
||||
// STANDING ZEROES THE CYCLE (reverse-stop desync, live-diagnosed
|
||||
// 2026-07-13): a REVERSE cadence is NEGATIVE, so the walk-family stop
|
||||
// gate (cycleSpeed <= ZeroSpeed) passes while still cycling at full
|
||||
// reverse speed, and several stand-entry paths (turn exit, terminal
|
||||
// poses) never touch the cycle -- Standing could be entered with a
|
||||
// stale bodyCycleSpeed = -2.507 ([gaitSM] state=0 evidence). The master
|
||||
// LOOKS still (case 0 never advances the clip) but the stale cycle
|
||||
// REPLICATES and the peer's replicant marches in place. A standing
|
||||
// mech's cycle is 0 (the clean forward-stop log: legSum ~3e-8).
|
||||
if (bodyCycleSpeed != 0.0f)
|
||||
{
|
||||
bodyCycleSpeed = 0.0f;
|
||||
ForceUpdate(8); // type-3 record: legs stopped
|
||||
}
|
||||
// RAW (FUN_004a5678 case 0): standSpeed < bodyTargetSpeed -> begin WALKING
|
||||
// (5); 0 <= target < standSpeed -> stay standing; target < 0 -> reverse
|
||||
// (0x10). (The earlier draft had the comparison INVERTED.)
|
||||
@@ -979,6 +1007,20 @@ Scalar
|
||||
switch (bodyAnimationState)
|
||||
{
|
||||
case StandingAnimation: // 0
|
||||
// STANDING ZEROES THE CYCLE (reverse-stop desync, live-diagnosed
|
||||
// 2026-07-13): a REVERSE cadence is NEGATIVE, so the walk-family stop
|
||||
// gate (cycleSpeed <= ZeroSpeed) passes while still cycling at full
|
||||
// reverse speed, and several stand-entry paths (turn exit, terminal
|
||||
// poses) never touch the cycle -- Standing could be entered with a
|
||||
// stale bodyCycleSpeed = -2.507 ([gaitSM] state=0 evidence). The master
|
||||
// LOOKS still (case 0 never advances the clip) but the stale cycle
|
||||
// REPLICATES and the peer's replicant marches in place. A standing
|
||||
// mech's cycle is 0 (the clean forward-stop log: legSum ~3e-8).
|
||||
if (bodyCycleSpeed != 0.0f)
|
||||
{
|
||||
bodyCycleSpeed = 0.0f;
|
||||
ForceUpdate(8); // type-3 record: legs stopped
|
||||
}
|
||||
if (bodyTargetSpeed <= standSpeed) // 0x6b4 <= 0x530
|
||||
{
|
||||
distance = 0.0f;
|
||||
@@ -1166,6 +1208,20 @@ Scalar
|
||||
switch (legAnimationState)
|
||||
{
|
||||
case StandingAnimation: // 0
|
||||
// STANDING ZEROES THE CYCLE (reverse-stop desync, live-diagnosed
|
||||
// 2026-07-13): a REVERSE cadence is NEGATIVE, so the walk-family stop
|
||||
// gate (cycleSpeed <= ZeroSpeed) passes while still cycling at full
|
||||
// reverse speed, and several stand-entry paths (turn exit, terminal
|
||||
// poses) never touch the cycle -- Standing could be entered with a
|
||||
// stale legCycleSpeed = -2.507 ([gaitSM] state=0 evidence). The master
|
||||
// LOOKS still (case 0 never advances the clip) but the stale cycle
|
||||
// REPLICATES and the peer's replicant marches in place. A standing
|
||||
// mech's cycle is 0 (the clean forward-stop log: legSum ~3e-8).
|
||||
if (legCycleSpeed != 0.0f)
|
||||
{
|
||||
legCycleSpeed = 0.0f;
|
||||
ForceUpdate(8); // type-3 record: legs stopped
|
||||
}
|
||||
if (motionSource->commandedSpeed <= standSpeed) // +0x128 <= 0x530
|
||||
{
|
||||
distance = 0.0f;
|
||||
|
||||
+152
-16
@@ -646,6 +646,9 @@ static int gBTLaserKey = 0; // raw key states (set by the keyboar
|
||||
static int gBTPPCKey = 0;
|
||||
static int gBTMissileKey = 0;
|
||||
static int gBTPinkyKey = 0; // key '4' = the pod's 4th fire button (Pinky 0x45)
|
||||
int gBTModeCycle = 0; // 'M' edge: cycle the control mode (mapper consumes)
|
||||
float gBTTwistAxis = 0.0f; // Q/E torso-twist deflection (assisted-mode stick X)
|
||||
int gBTTorsoRecenter = 0; // 'X' edge: pulse the authentic torso recenter (mapper consumes)
|
||||
static int gBTConfigKey = 0; // task #6: HOLD 'G' = the weapon-configure button
|
||||
static int gBTGenSelKey = 0; // task #12: F5..F8 = SelectGeneratorA..D, F9 = mode toggle
|
||||
// (0 idle; else the message id 4..8)
|
||||
@@ -785,6 +788,69 @@ static BTProjectile gProjectiles[64];
|
||||
|
||||
extern void BTPushBeam(float,float,float, float,float,float, unsigned, float, float);
|
||||
|
||||
//###########################################################################
|
||||
// Zone-effect bridge (mechdmg has no `application` access): route the damage-
|
||||
// band effect through the AUTHENTIC RendererManager::StartEntityEffect chain
|
||||
// (the @004d097c dispatcher; the AudioRenderer hears the same broadcast).
|
||||
// Returns 0 when the manager isn't up so the caller can fall back.
|
||||
//###########################################################################
|
||||
int
|
||||
BTStartZoneEffect(Mech *mech, void *zone, int resource)
|
||||
{
|
||||
if (mech == 0 || zone == 0 || resource <= 0
|
||||
|| application == 0 || application->GetRendererManager() == 0)
|
||||
return 0;
|
||||
application->GetRendererManager()->StartEntityEffect(
|
||||
(Entity *)mech, (DamageZone *)zone,
|
||||
(ResourceDescription::ResourceID)resource);
|
||||
return 1;
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
// Segment world-transform bridge (the @004d097c per-zone effect dispatcher +
|
||||
// the attached-emitter follow in L4VIDEO's PFX layer). Resolves the entity's
|
||||
// segment (by index) to its world position + 3x3 basis rows; falls back to
|
||||
// the mech origin at torso height when the segment doesn't resolve. Returns
|
||||
// 0 for a non-mech / unregistered entity (the follow then keeps its last
|
||||
// frame -- and the emitter dies with its authored window anyway).
|
||||
//###########################################################################
|
||||
int
|
||||
BTResolveSegmentWorld(void *entity, int seg_index, float *pos3, float *rows9)
|
||||
{
|
||||
extern int BTIsRegisteredMech(Entity *e);
|
||||
if (entity == 0 || !BTIsRegisteredMech((Entity *)entity))
|
||||
return 0;
|
||||
Mech *m = (Mech *)entity;
|
||||
|
||||
Point3D p = m->localOrigin.linearPosition;
|
||||
p.y += kMuzzleHeight; // fallback: torso height
|
||||
if (seg_index >= 0)
|
||||
{
|
||||
EntitySegment::SegmentTableIterator it(m->segmentTable);
|
||||
EntitySegment *seg;
|
||||
while ((seg = it.ReadAndNext()) != NULL)
|
||||
{
|
||||
if (seg->GetIndex() == seg_index)
|
||||
{
|
||||
AffineMatrix mw;
|
||||
mw.Multiply(seg->GetSegmentToEntity(), m->localToWorld);
|
||||
p = mw; // Point3D = matrix translation
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
pos3[0] = (float)p.x; pos3[1] = (float)p.y; pos3[2] = (float)p.z;
|
||||
|
||||
UnitVector ax, ay, az;
|
||||
m->localToWorld.GetFromAxis(X_Axis, &ax);
|
||||
m->localToWorld.GetFromAxis(Y_Axis, &ay);
|
||||
m->localToWorld.GetFromAxis(Z_Axis, &az);
|
||||
rows9[0] = (float)ax.x; rows9[1] = (float)ax.y; rows9[2] = (float)ax.z;
|
||||
rows9[3] = (float)ay.x; rows9[4] = (float)ay.y; rows9[5] = (float)ay.z;
|
||||
rows9[6] = (float)az.x; rows9[7] = (float)az.y; rows9[8] = (float)az.z;
|
||||
return 1;
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
// PER-ROUND DETONATION (the binary's Missile::MoveAndCollide @004bef78: every
|
||||
// round spawns ITS OWN ExplosionModelFile at its impact point, resource
|
||||
@@ -1299,6 +1365,11 @@ void
|
||||
frameEntryWorldVelocity = Vector3D(0.0f, 0.0f, 0.0f);
|
||||
ramLastVictim = 0;
|
||||
ramContactLinger = 0.0f;
|
||||
// StopAllEntityEffects (@004d0c14): a respawned mech must not trail its
|
||||
// corpse's attached zone effects -- the authentic per-entity effect
|
||||
// cleanup, broadcast to every renderer.
|
||||
if (application != 0 && application->GetRendererManager() != 0)
|
||||
application->GetRendererManager()->StopAllEntityEffects((Entity *)this);
|
||||
|
||||
// --- our RELOCATED gait/motion accumulators -> identity (the 1995 offsets
|
||||
// the decomp zeroes map to these named members in our layout) ---
|
||||
@@ -1823,6 +1894,56 @@ void
|
||||
// task #13: 'C' cycles the coolant valve (BT_VALVE_SLOT
|
||||
// picks the condenser roster slot; default = Condenser1).
|
||||
gBTValveKey = focused && (pAsync('C') & dn) ? 1 : 0;
|
||||
// TORSO CONTROLS (2026-07-13): 'M' cycles the control mode
|
||||
// (Basic -> Standard -> Veteran -- the pod console button,
|
||||
// CycleControlModeMessageHandler); Q/E deflect the torso
|
||||
// twist axis (the STICK in Standard/Veteran, where A/D
|
||||
// become the pedals). Ramped like the turn stick.
|
||||
{
|
||||
static int sPrevM = 0;
|
||||
const int mNow = focused && (pAsync('M') & dn) ? 1 : 0;
|
||||
if (mNow && !sPrevM) gBTModeCycle = 1; // edge -> one cycle
|
||||
sPrevM = mNow;
|
||||
const int tw = (focused && (pAsync('E') & dn) ? 1 : 0)
|
||||
- (focused && (pAsync('Q') & dn) ? 1 : 0);
|
||||
static float sTwist = 0.0f;
|
||||
if (tw != 0)
|
||||
{
|
||||
sTwist += tw * kStickRate * dt;
|
||||
if (sTwist > 1.0f) sTwist = 1.0f;
|
||||
if (sTwist < -1.0f) sTwist = -1.0f;
|
||||
}
|
||||
else if (sTwist != 0.0f)
|
||||
{
|
||||
// SPRING-CENTER on release (user fix 2026-07-13: the
|
||||
// old hold-deflection model left a residual axis ->
|
||||
// the torso drifted forever with no way to stop).
|
||||
// The pod stick is spring-centered: the axis is a
|
||||
// twist-RATE demand, so release = rate 0 = the torso
|
||||
// HOLDS where you aimed it (position is kept by
|
||||
// Torso::currentTwist, not by the axis). Same model
|
||||
// as the A/D turn stick (kStickCenterRate).
|
||||
const float step = kStickCenterRate * dt;
|
||||
if (sTwist > step) sTwist -= step;
|
||||
else if (sTwist < -step) sTwist += step;
|
||||
else sTwist = 0.0f;
|
||||
}
|
||||
// X = all-stop for the torso too: zero the axis NOW and
|
||||
// pulse the AUTHENTIC recenter (torso centerCommand ->
|
||||
// Recenter @004b6918 slews currentTwist back to 0; any
|
||||
// new Q/E deflection cancels it, sim-side). Separate
|
||||
// edge detector from the drive all-stop below -- both
|
||||
// fire on the same press.
|
||||
static int sPrevXT = 0;
|
||||
const int xtNow = focused && (pAsync('X') & dn) ? 1 : 0;
|
||||
if (xtNow && !sPrevXT)
|
||||
{
|
||||
sTwist = 0.0f;
|
||||
gBTTorsoRecenter = 1; // mapper consumes -> CommandRecenter()
|
||||
}
|
||||
sPrevXT = xtNow;
|
||||
gBTTwistAxis = sTwist;
|
||||
}
|
||||
// gBTDrive.fire = "any weapon trigger down" (feeds the bring-up
|
||||
// damage dispatcher + the beam-visual keepalive)
|
||||
gBTDrive.fire = (gBTLaserKey || gBTPPCKey || gBTMissileKey || gBTPinkyKey) ? 1 : 0;
|
||||
@@ -1842,18 +1963,24 @@ void
|
||||
}
|
||||
sPrevV = vNow;
|
||||
|
||||
// RETICLE = TORSO BORESIGHT (task #39 correction): the pod had
|
||||
// NO free-aim cursor -- the binary's HudSimulation computes
|
||||
// reticlePosition from the mech's POSE quaternions
|
||||
// (part_013.c:5652+, a rate-limited quat->euler of the torso
|
||||
// aim), i.e. the crosshair marks where the TORSO GUNS point
|
||||
// relative to the view, and you aim by steering the mech /
|
||||
// twisting the torso (the pod's right stick). The earlier
|
||||
// mouse-cursor slew was a mis-sourced stand-in and is
|
||||
// REMOVED. Our cockpit view is body-mounted, so the
|
||||
// crosshair deflection = the torso twist projected to screen
|
||||
// -- identically ZERO on the fixed-torso BLH (dead-centre
|
||||
// boresight). BT_AIM="x y" remains as the headless harness.
|
||||
// RETICLE = SCREEN CENTER (task #58 correction, supersedes the
|
||||
// task #39 "body-mounted view" model): the CAMERA yaws with
|
||||
// the torso (the cockpit sits on jointtorso -- see the
|
||||
// gBTEyeTwist publisher), so screen center always IS the gun
|
||||
// boresight and the crosshair stays centered while the WORLD
|
||||
// rotates past it -- the twist reads on the bottom tape
|
||||
// carets / compass / radar wedge, not the crosshair. [T1:
|
||||
// jointtorso->jointeye->siteeyepoint chain + FUN_004c22c4
|
||||
// inverse-chain view; crosshair-twist forensics 2026-07-13.]
|
||||
// The old gBTAimX = tan(twist) slew was a port invention on
|
||||
// the falsified body-mounted premise -- with the yawing eye
|
||||
// it double-counts (crosshair pinned to HULL-forward, fire
|
||||
// ray resolving body-forward: "twisting leaves the
|
||||
// crosshairs behind"). Reticle mobility exists in the
|
||||
// binary (Reticle::reticlePosition, RETICLE.h:42) but its
|
||||
// twist-era use is the FIXED-TORSO free-aim channel
|
||||
// (mech+0x36c, writer un-exported) -- deferred.
|
||||
// BT_AIM="x y" remains as the headless harness.
|
||||
{
|
||||
static int sAimEnv = -1;
|
||||
static float sAimEnvX = 0.0f, sAimEnvY = 0.0f;
|
||||
@@ -1870,10 +1997,10 @@ void
|
||||
}
|
||||
else
|
||||
{
|
||||
// boresight = torso twist vs the body-mounted view,
|
||||
// projected through the live per-axis projection.
|
||||
extern float BTTwistToReticleX(float twist_rad);
|
||||
gBTAimX = BTTwistToReticleX(gBTHudTwist); // BLH: 0
|
||||
// Boresight = screen center: the eye carries the
|
||||
// twist (task #58), the pick ray inherits it from
|
||||
// the published eye basis.
|
||||
gBTAimX = 0.0f;
|
||||
gBTAimY = 0.0f;
|
||||
}
|
||||
}
|
||||
@@ -3299,6 +3426,15 @@ void
|
||||
gBTHudTwistLimit = (float)GetHorizontalFiringReach();// HorizontalTorsoLimit (attrs 5/6)
|
||||
gBTHudGroupMask = (int)targetReticle.reticleElementMask & 0xF;
|
||||
gBTHudPrimary = ((int)targetReticle.reticleElementMask & 0x20) != 0;
|
||||
// task #58: publish the live twist for renderer-side DIAGNOSTICS
|
||||
// (correlating [eyefwd] against the twist). The cockpit eye does
|
||||
// NOT consume this -- it inherits the twist authentically through
|
||||
// jointtorso's HingeRenderable in the draw traversal (see the note
|
||||
// in DPLEyeRenderable::Execute).
|
||||
{
|
||||
extern float gBTEyeTwist;
|
||||
gBTEyeTwist = gBTHudTwist;
|
||||
}
|
||||
}
|
||||
|
||||
// task #6 ORDER FIX: the scripted block must run BEFORE the fire-push
|
||||
|
||||
@@ -1092,8 +1092,21 @@ void
|
||||
}
|
||||
if (d != 0)
|
||||
{
|
||||
BTSpawnDamageEffect(owner, d->effectResource, // explosion AT the zone's
|
||||
zone->segmentIndex); // segment (world position)
|
||||
// THE AUTHENTIC CHAIN (@004d097c dispatcher, landed 2026-07-13):
|
||||
// RendererManager::StartEntityEffect -> the Renderer message
|
||||
// handler [T0: GameModel resolve + ExplosionResourceTable
|
||||
// graphic-state remap] -> BTL4VideoRenderer::
|
||||
// StartEntityEffectImplementation -> the effect ATTACHED to the
|
||||
// zone's segment (smoke follows the walking mech) -- and the
|
||||
// AudioRenderer's implementation hears the same message (zone
|
||||
// sounds ride free). Explosion::Make fallback only when the
|
||||
// manager isn't up (early bring-up).
|
||||
extern int BTStartZoneEffect(Mech *mech, void *zone, int resource);
|
||||
if (!BTStartZoneEffect(owner, zone, d->effectResource))
|
||||
{
|
||||
BTSpawnDamageEffect(owner, d->effectResource, // explosion AT the zone's
|
||||
zone->segmentIndex); // segment (world position)
|
||||
}
|
||||
zone->ApplyDamageGraphicState(d->graphicState); // destroyed skin (graphic state)
|
||||
// A graphic-state change means the segment's MODEL changed (intact ->
|
||||
// destroyed variant, keyed by GetVideoObjectName(skl, gstate)). Fire
|
||||
|
||||
@@ -211,6 +211,12 @@
|
||||
int
|
||||
segmentIndex; // @0x194 GetSegmentIndex() result
|
||||
|
||||
public:
|
||||
// The zone's segment slot -- consumed by the @004d097c per-zone effect
|
||||
// dispatcher (BTL4VideoRenderer::StartEntityEffectImplementation).
|
||||
int EffectSegmentIndex() const { return segmentIndex; }
|
||||
protected:
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Damage Support
|
||||
//
|
||||
|
||||
@@ -440,6 +440,18 @@ void
|
||||
Check(message);
|
||||
|
||||
if (message->dataContents > 0)
|
||||
{
|
||||
CycleControlModeNow();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// The mode-cycle body, shared by the console-button message handler above and
|
||||
// the desktop 'M' key (mech4 key poll -> gBTModeCycle).
|
||||
//
|
||||
void
|
||||
MechControlsMapper::CycleControlModeNow()
|
||||
{
|
||||
{
|
||||
controlMode = (ControlMode)(controlMode + 1);
|
||||
if (controlMode > VeteranMode)
|
||||
@@ -448,23 +460,28 @@ void
|
||||
}
|
||||
NotifyOfControlModeChange(controlMode); // vtable+0x48
|
||||
|
||||
Mech *mech = GetMech();
|
||||
int torso = *(int *)((int)mech + 0x438);
|
||||
int cockpit = *(int *)((int)mech + 0x5b4);
|
||||
|
||||
if (controlMode == BasicMode)
|
||||
// TYPED torso reconfiguration (2026-07-13): the raw block this
|
||||
// replaces wrote the BINARY's offsets (torso+0x1f0/0x274/0x220...)
|
||||
// straight onto OUR compiled Torso -- the databinding trap: garbage
|
||||
// writes into whatever members live there in this build. The
|
||||
// observable semantics via named members: Basic clears the analog
|
||||
// axes and recenters (the sim's centerCommand -> Recenter); the
|
||||
// assisted modes just free the torso (the sim clamps to the authored
|
||||
// limits on its own).
|
||||
Mech *mech = GetMech();
|
||||
Torso *torso = (mech != 0) ? (Torso *)mech->GetTorsoSubsystem() : 0;
|
||||
if (torso != 0)
|
||||
{
|
||||
*(LWord *)(torso + 0x1f0) = 0; // no free aim
|
||||
*(LWord *)(torso + 0x274) = 1; // auto-center on
|
||||
*(LWord *)(torso + 0x220) = *(LWord *)(torso + 0x228); // recenter yaw
|
||||
*(LWord *)(torso + 0x224) = *(LWord *)(torso + 0x22c); // recenter pitch
|
||||
*(LWord *)(cockpit + 0x2a0) = 1;
|
||||
}
|
||||
else if ((unsigned)(controlMode - 1) < 2) // Standard / Veteran
|
||||
{
|
||||
*(LWord *)(torso + 0x220) = *(LWord *)(torso + 0x230); // yaw -> limit
|
||||
*(LWord *)(torso + 0x224) = *(LWord *)(torso + 0x234); // pitch -> limit
|
||||
if (controlMode == BasicMode)
|
||||
{
|
||||
torso->SetAnalogTwistAxis(0.0f);
|
||||
torso->SetAnalogElevationAxis(0.0f);
|
||||
torso->CommandRecenter();
|
||||
}
|
||||
// Standard/Veteran: nothing to force -- the sim's limits govern.
|
||||
}
|
||||
DEBUG_STREAM << "[mode] control mode -> " << (int)controlMode
|
||||
<< " (0=Basic 1=Standard 2=Veteran)" << std::endl;
|
||||
}
|
||||
Check_Fpu();
|
||||
}
|
||||
@@ -655,7 +672,54 @@ void
|
||||
}
|
||||
throttlePosition = (key_throttle >= 0.0f) ? key_throttle : -key_throttle;
|
||||
reverseThrust = (key_throttle < 0.0f) ? 1 : 0;
|
||||
stickPosition.x = key_turn;
|
||||
// CONTROL-MODE AXIS ROUTING (2026-07-13, the pod mapping): in
|
||||
// BASIC the stick steers the legs (turn) and the torso auto-
|
||||
// centers; in STANDARD/VETERAN the stick is the TORSO (free aim /
|
||||
// twist) and the PEDALS steer -- so the keyboard's A/D feed the
|
||||
// pedals and Q/E ('the stick') feed the twist. 'M' cycles modes
|
||||
// (the same body the pod console button message drives, so the
|
||||
// CONTROL MODE gauge tracks).
|
||||
{
|
||||
extern int gBTModeCycle;
|
||||
extern float gBTTwistAxis;
|
||||
extern int gBTTorsoRecenter;
|
||||
if (gBTModeCycle)
|
||||
{
|
||||
gBTModeCycle = 0;
|
||||
CycleControlModeNow();
|
||||
}
|
||||
if (controlMode == BasicMode)
|
||||
{
|
||||
stickPosition.x = key_turn;
|
||||
pedalsPosition = 0.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
stickPosition.x = gBTTwistAxis; // the torso axis
|
||||
pedalsPosition = key_turn; // A/D = the pedals
|
||||
// 'X' recenter pulse (2026-07-13): one-frame centerCommand
|
||||
// -> the sim arms recenterActive and Recenter (@004b6918)
|
||||
// slews the torso home; any Q/E input cancels it sim-side.
|
||||
// centerCommand is a pod BUTTON state, so the writer clears
|
||||
// it while unpressed (Basic's own path re-asserts every
|
||||
// frame; this branch owns it in Standard/Veteran).
|
||||
{
|
||||
Torso *rcTorso = (Torso *)mech->GetTorsoSubsystem();
|
||||
if (rcTorso != 0)
|
||||
{
|
||||
if (gBTTorsoRecenter)
|
||||
{
|
||||
gBTTorsoRecenter = 0;
|
||||
rcTorso->CommandRecenter();
|
||||
}
|
||||
else
|
||||
{
|
||||
rcTorso->ClearRecenterCommand();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stickPosition.y = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +104,11 @@ class Pilot;
|
||||
CycleControlModeMessageHandler( // @004afbe0
|
||||
ReceiverDataMessageOf<ControlsButton> *message
|
||||
);
|
||||
|
||||
// The shared mode-cycle body (the console-button handler above + the
|
||||
// desktop 'M' key both drive it).
|
||||
void
|
||||
CycleControlModeNow();
|
||||
void
|
||||
CycleDisplayModeMessageHandler( // @004afcac
|
||||
ReceiverDataMessageOf<ControlsButton> *message
|
||||
|
||||
@@ -75,7 +75,12 @@ static const Scalar MaxOutputVoltage = 1.0f; // _DAT_004b2154 / _DAT_004b2258
|
||||
static const Scalar RatedVoltageRef = 1.0f; // _DAT_004b1d10 (myomer rated reference)
|
||||
static const Scalar DegradedDriveScale = 0.5f; // _DAT_004b1d14 (heat-degradation throttle)
|
||||
static const Scalar ResourceUnset = -1.0f; // _DAT_004b177c / _DAT_004b1a3c / _DAT_004b2580
|
||||
static const Scalar MinVoltageScale = 1.0f; // _DAT_004b1924 (PowerWatcher threshold scale)
|
||||
static const Scalar MinVoltageScale = 0.01f; // _DAT_004b1924 -- a 10-byte x87 literal
|
||||
// (0a d7 a3 70 3d 0a d7 a3 f8 3f = 0.01,
|
||||
// byte-read from the exe, task #57): converts
|
||||
// the authored MinVoltagePercent to a fraction.
|
||||
// Was 1.0f, which made minVoltage 100x too big
|
||||
// and latched the watchdog brownout (level 1).
|
||||
|
||||
|
||||
//###########################################################################
|
||||
@@ -1191,8 +1196,12 @@ PowerWatcher::SharedData
|
||||
Derivation*
|
||||
PowerWatcher::GetClassDerivations()
|
||||
{
|
||||
// Real base ctor is @004aeb40 (HeatWatcher); HeatableSubsystem stands in.
|
||||
static Derivation classDerivations(HeatableSubsystem::GetClassDerivations(), "PowerWatcher");
|
||||
// Chains to the REAL base HeatWatcher (@004aeb40) -- task #57: the old
|
||||
// HeatableSubsystem stand-in predated the base-chain re-base and broke
|
||||
// IsDerivedFrom(HeatWatcher) for every PowerWatcher descendant, which
|
||||
// silently skipped the Torso/Searchlight/ThermalSight in the factory
|
||||
// watcher-CONNECT pass (their watchdogs stayed at 0 = never Ready).
|
||||
static Derivation classDerivations(HeatWatcher::GetClassDerivations(), "PowerWatcher");
|
||||
return &classDerivations;
|
||||
}
|
||||
|
||||
@@ -1254,13 +1263,68 @@ Logical
|
||||
}
|
||||
|
||||
//
|
||||
// @004b1804 -- delegate to the base simulation.
|
||||
// @004b1804 (slot 10) -- ResetToInitialState: chain to the HeatWatcher base.
|
||||
// (This body was previously MISLABELED as the Simulation. The vtable
|
||||
// @0050f99c slot 10 (offset 0x28) is 004b1804, exactly where every other
|
||||
// subsystem carries ResetToInitialState; the registered Performance PTR
|
||||
// @0050f5fc points at 004b181c -- UpdateWatch below.)
|
||||
//
|
||||
void
|
||||
PowerWatcher::Simulation(Scalar time_slice)
|
||||
PowerWatcher::ResetToInitialState(Logical /*powered*/)
|
||||
{
|
||||
HeatWatcher::ResetToInitialState(True); // FUN_004aea9c = HeatWatcher::ResetToInitialState
|
||||
(void)time_slice;
|
||||
HeatWatcher::ResetToInitialState(True); // FUN_004aea9c
|
||||
}
|
||||
|
||||
//
|
||||
// @004b181c -- the per-tick watch update: THE registered Performance
|
||||
// (PTR @0050f5fc -> 004b181c), and called directly by the Torso master/copy
|
||||
// simulations (@004b5cf0/@004b65f8 first line). Ghidra missed the function
|
||||
// start; recovered from the raw pseudocode at part_013.c:1645:
|
||||
//
|
||||
// FUN_004aeac4(this); // heat mirror
|
||||
// watched = watchedLink.Resolve(); // +0x114
|
||||
// gen = watched->voltageSource.Resolve(); // +0x1D0
|
||||
// watchdogAlarm.SetLevel(watched->elecLevel); // mirror +0x278
|
||||
// if (elecLevel == 4 && gen && gen->outputVoltage(+0x1DC)
|
||||
// <= minVoltage(+0x180) * gen->ratedVoltage(+0x1D8))
|
||||
// watchdogAlarm.SetLevel(1); // brownout
|
||||
//
|
||||
// The watchdog MIRRORS the watched subsystem's electrical state -- this is
|
||||
// what drives ElectricalStateLevel()==Ready on the Torso (the twist-rate
|
||||
// power gate) and WatchedVoltageLevel on Searchlight/ThermalSight.
|
||||
//
|
||||
void
|
||||
PowerWatcher::UpdateWatch()
|
||||
{
|
||||
WatchSimulation(0.0f); // FUN_004aeac4 -- heat-alarm mirror
|
||||
|
||||
PoweredSubsystem *watched = (PoweredSubsystem *)watchedLink.Resolve(); // FUN_00417ab4(this+0x114)
|
||||
if (watched == 0)
|
||||
{
|
||||
// The binary derefs unconditionally (the link always binds on the
|
||||
// master node). Null only on replicants / bring-up: hold Not-Ready.
|
||||
watchdogAlarm.SetLevel(0);
|
||||
return;
|
||||
}
|
||||
|
||||
int level = watched->electricalStateAlarm.GetLevel(); // *(watched+0x278)
|
||||
watchdogAlarm.SetLevel(level); // FUN_0041bbd8(this+0x184, level)
|
||||
|
||||
Generator *source = (Generator *)watched->ResolveVoltageSource(); // FUN_00417ab4(watched+0x1d0)
|
||||
if (level == PoweredSubsystem::Ready && source != 0
|
||||
&& source->MeasuredVoltage() <= minVoltage * source->RatedVoltageOf())
|
||||
{
|
||||
watchdogAlarm.SetLevel(1); // brownout: Ready but sagging source
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// The Performance wrapper (PTR @0050f5fc -> @004b181c takes no dt).
|
||||
//
|
||||
void
|
||||
PowerWatcher::Simulation(Scalar /*time_slice*/)
|
||||
{
|
||||
UpdateWatch();
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -553,7 +553,14 @@ class Generator;
|
||||
Logical
|
||||
HandleMessage(int message); // @004b179c (chains to base @004aea84)
|
||||
void
|
||||
Simulation(Scalar time_slice); // @004b1804 (chains to base @004aea9c)
|
||||
ResetToInitialState(Logical powered); // @004b1804 (slot 10; chains to base @004aea9c)
|
||||
void
|
||||
UpdateWatch(); // @004b181c -- watchdog mirror of the watched
|
||||
// subsystem's electrical state (+ brownout);
|
||||
// the Torso sims call this directly
|
||||
void
|
||||
Simulation(Scalar time_slice); // the registered Performance (PTR @0050f5fc
|
||||
// -> @004b181c): forwards to UpdateWatch()
|
||||
|
||||
public:
|
||||
// BASE-CHAIN RE-BASE: the shadow watchedLink was DELETED -- the binary has a
|
||||
|
||||
+101
-14
@@ -94,6 +94,9 @@ namespace {
|
||||
inline Scalar
|
||||
RecordField(Simulation::UpdateRecord *record, int byte_offset)
|
||||
{ return *(const Scalar*)((const char*)record + byte_offset); }
|
||||
inline void
|
||||
WriteRecordField(Simulation::UpdateRecord *record, int byte_offset, Scalar value)
|
||||
{ *(Scalar*)((char*)record + byte_offset) = value; }
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
@@ -243,19 +246,29 @@ Torso::Torso(
|
||||
// rate. TorsoSimulation drives the sweep (below). Remove after verification.
|
||||
if (isDamagedCopy == 0 && getenv("BT_FORCE_TORSO"))
|
||||
{
|
||||
// ⚠ HARNESS TRAP (task #58 post-mortem): this hook used to override the
|
||||
// joint nodes UNCONDITIONALLY with the BLH demo names -- on a mech whose
|
||||
// resource already resolved REAL torso joints (the MadCat's jointtorso)
|
||||
// that silently redirected the whole sweep into the SHADOW hinge, and a
|
||||
// probe run "proved" the cockpit camera ignored the twist (it doesn't;
|
||||
// the joint chain delivers it). Now: only fill joints that did NOT
|
||||
// resolve from the resource; a twist-capable mech sweeps its real ones.
|
||||
horizontalEnabled = True; // @0x250
|
||||
const char *mj = getenv("BT_FORCE_TORSO_JOINT");
|
||||
if (mj == 0 || *mj == '\0') mj = "jointshakey2";
|
||||
horizontalJointNode = ResolveJoint(mj); // torso body (ball)
|
||||
horizontalShadowJointNode = ResolveJoint("jointtshadow"); // shadow twist (hingey)
|
||||
horizontalLimitLeft = 0.7f; // @0x1E0 ~40 deg
|
||||
horizontalLimitRight = -0.7f; // @0x1DC
|
||||
if (horizontalJointNode == 0)
|
||||
{
|
||||
const char *mj = getenv("BT_FORCE_TORSO_JOINT");
|
||||
if (mj == 0 || *mj == '\0') mj = "jointshakey2";
|
||||
horizontalJointNode = ResolveJoint(mj); // torso body (ball)
|
||||
horizontalShadowJointNode = ResolveJoint("jointtshadow"); // shadow twist (hingey)
|
||||
horizontalLimitLeft = 0.7f; // @0x1E0 ~40 deg
|
||||
horizontalLimitRight = -0.7f; // @0x1DC
|
||||
}
|
||||
baseTwistRate = 1.0f; // @0x23C rad/s slew
|
||||
if (getenv("BT_TORSO_LOG"))
|
||||
{
|
||||
DEBUG_STREAM << "[torso] FORCE-ENABLE '" << mj << "' -> " << (void*)horizontalJointNode;
|
||||
DEBUG_STREAM << "[torso] FORCE-ENABLE node=" << (void*)horizontalJointNode;
|
||||
if (horizontalJointNode) DEBUG_STREAM << " type=" << (int)horizontalJointNode->GetJointType();
|
||||
DEBUG_STREAM << " ; shadow 'jointtshadow' -> " << (void*)horizontalShadowJointNode;
|
||||
DEBUG_STREAM << " ; shadow node=" << (void*)horizontalShadowJointNode;
|
||||
if (horizontalShadowJointNode) DEBUG_STREAM << " type=" << (int)horizontalShadowJointNode->GetJointType();
|
||||
DEBUG_STREAM << "\n" << std::flush;
|
||||
}
|
||||
@@ -387,14 +400,17 @@ Logical
|
||||
}
|
||||
|
||||
//
|
||||
// @004b6a78 (slot 6) [CONFIDENT] -- network/replay update record. Samples the
|
||||
// @004b6a78 (slot 6) [CONFIDENT] -- the REPLICANT-side apply (engine semantics
|
||||
// [T0]: ReadUpdateRecord = write the object FROM the record). Samples the
|
||||
// clock (FUN_00414b60) into lastUpdateTime, biasing it forward by one interval
|
||||
// when the elapsed window is below MinSlewMs, chains to Subsystem::WriteUpdateRecord
|
||||
// (FUN_0041bd34), then writes twistAtUpdate / twistVelocity / twistRate from the
|
||||
// record fields (record +0x10 / +0x14 / +0x18).
|
||||
// when the elapsed window is below MinSlewMs, chains to the base apply
|
||||
// (FUN_0041bd34 = ReadUpdateRecord, 2 args), then applies twistAtUpdate /
|
||||
// twistVelocity / twistRate from the record extras (+0x10 / +0x14 / +0x18).
|
||||
// (Task #57: previously mislabeled as WriteUpdateRecord -- the direction flip
|
||||
// made the MASTER consume its own blank record.)
|
||||
//
|
||||
void
|
||||
Torso::WriteUpdateRecord(UpdateRecord *message, int update_model)
|
||||
Torso::ReadUpdateRecord(UpdateRecord *message)
|
||||
{
|
||||
lastUpdateTime = GetCurrentTime(); // @0x254
|
||||
if ((Scalar)(lastUpdateTime - GetCreationTime()) / MsPerSecond < MinSlewMs)
|
||||
@@ -402,13 +418,41 @@ void
|
||||
lastUpdateTime += (lastUpdateTime - GetCreationTime()); // stretch tiny windows
|
||||
}
|
||||
|
||||
Subsystem::WriteUpdateRecord(message, update_model); // FUN_0041bd34
|
||||
Subsystem::ReadUpdateRecord(message); // FUN_0041bd34
|
||||
|
||||
twistAtUpdate = RecordField(message, 0x10); // @0x21C
|
||||
twistVelocity = RecordField(message, 0x14); // @0x1E8
|
||||
twistRate = RecordField(message, 0x18); // @0x238
|
||||
}
|
||||
|
||||
//
|
||||
// @004b6a1c (slot 7) [CONFIDENT, raw-disasm recovery -- Ghidra missed the
|
||||
// function start] -- the MASTER-side serialize (engine semantics [T0]:
|
||||
// WriteUpdateRecord = fill the record FROM the object). Chains the base
|
||||
// producer (FUN_0041c500(this, record, model), 3 args), extends the record to
|
||||
// 0x1C bytes and appends currentTwist / twistVelocity / twistRate at
|
||||
// +0x10/+0x14/+0x18 (exactly what the replicant's @004b6a78 applies), then
|
||||
// snapshots twistAtUpdate = currentTwist:
|
||||
//
|
||||
// 004b6a2e call 0041c500 ; base WriteUpdateRecord
|
||||
// 004b6a38 mov [record], 0x1C ; recordLength = 0x1C
|
||||
// 004b6a44+ record+0x10 = this+0x1D8 ; currentTwist
|
||||
// 004b6a51 record+0x14 = this+0x1E8 ; twistVelocity
|
||||
// 004b6a5a record+0x18 = this+0x238 ; twistRate
|
||||
// 004b6a63+ this+0x21C = this+0x1D8 ; twistAtUpdate = currentTwist
|
||||
//
|
||||
void
|
||||
Torso::WriteUpdateRecord(UpdateRecord *message, int update_model)
|
||||
{
|
||||
Subsystem::WriteUpdateRecord(message, update_model); // FUN_0041c500
|
||||
|
||||
message->recordLength = 0x1C; // base header 0x10 + 3 Scalars
|
||||
WriteRecordField(message, 0x10, currentTwist); // @0x1D8
|
||||
WriteRecordField(message, 0x14, twistVelocity); // @0x1E8
|
||||
WriteRecordField(message, 0x18, twistRate); // @0x238
|
||||
twistAtUpdate = currentTwist; // @0x21C snapshot at send
|
||||
}
|
||||
|
||||
|
||||
//#############################################################################
|
||||
// Per-frame simulation
|
||||
@@ -464,6 +508,34 @@ void
|
||||
analogTwistAxis = ((++s_sweep / 90) & 1) ? -1.0f : 1.0f; // +/- every ~90 frames
|
||||
}
|
||||
|
||||
// TORSO GATE PROBE (BT_TORSO_LOG): why is the twist rate zero?
|
||||
if (getenv("BT_TORSO_LOG"))
|
||||
{
|
||||
static int s_tl = 0;
|
||||
if ((s_tl++ % 120) == 0)
|
||||
{
|
||||
PoweredSubsystem *w = (PoweredSubsystem *)watchedLink.Resolve();
|
||||
DEBUG_STREAM << "[torso] hmOff=" << (int)HeatModelOff()
|
||||
<< " elec=" << (int)ElectricalStateLevel()
|
||||
<< " heatState=" << (int)HeatStateLevel()
|
||||
<< " rate=" << effectiveTwistRate
|
||||
<< " base=" << baseTwistRate
|
||||
<< " hEn=" << (int)horizontalEnabled
|
||||
<< " limits=(" << horizontalLimitRight << ".." << horizontalLimitLeft << ")"
|
||||
<< " axis=" << analogTwistAxis
|
||||
<< " twist=" << currentTwist
|
||||
<< " wIdx=" << watchedSubsystem
|
||||
<< " w=" << (void*)w
|
||||
<< " wElec=" << (w ? w->electricalStateAlarm.GetLevel() : -1)
|
||||
<< " wSrc=" << (void*)(w ? w->ResolveVoltageSource() : 0);
|
||||
Generator *g = (w ? (Generator *)w->ResolveVoltageSource() : 0);
|
||||
if (g)
|
||||
DEBUG_STREAM << " gOut=" << g->MeasuredVoltage()
|
||||
<< " gRated=" << g->RatedVoltageOf()
|
||||
<< " minV=" << minVoltage;
|
||||
DEBUG_STREAM << std::endl;
|
||||
}
|
||||
}
|
||||
Scalar twistStep = effectiveTwistRate * time_slice;
|
||||
Scalar elevStep = effectiveElevationRate * time_slice;
|
||||
|
||||
@@ -616,6 +688,21 @@ void
|
||||
// external joint pass as the master path (see TorsoSimulation for the @004b67ec
|
||||
// note). Harmless in single-player bring-up (no copies); correct for MP.
|
||||
UpdateJoints(); // FUN_004b67ec
|
||||
|
||||
// COPY-SIDE PROBE (BT_TORSO_LOG): where does the replicant's twist come from?
|
||||
if (getenv("BT_TORSO_LOG"))
|
||||
{
|
||||
static int s_cl = 0;
|
||||
if ((s_cl++ % 120) == 0)
|
||||
DEBUG_STREAM << "[torso-copy] cur=" << currentTwist
|
||||
<< " target=" << targetTwist
|
||||
<< " atUpd=" << twistAtUpdate
|
||||
<< " rate=" << twistRate
|
||||
<< " vel=" << twistVelocity
|
||||
<< " lastUpd=" << lastUpdateTime
|
||||
<< " now=" << GetCurrentTime()
|
||||
<< " copy=" << (int)isDamagedCopy << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -156,11 +156,13 @@ class Joint; // engine skeleton node (JOINT.h); the twist target
|
||||
void SetMovedFlag() { ForceUpdate(); }
|
||||
// Local helper: "is value pinned at this software limit?".
|
||||
Logical AtLimit(Scalar value, Scalar limit) const { return fabsf(value - limit) <= 0.0001f; }
|
||||
// CROSS-FAMILY delegate shims -- still no-ops (no layout impact; STEP-4 will
|
||||
// chain them to the real PowerWatcher/HeatWatcher base once the Torso is live).
|
||||
// CROSS-FAMILY delegate shims (STEP-4): UpdateWatch now chains to the real
|
||||
// PowerWatcher per-tick watch body (@004b181c, task #57) -- this is what
|
||||
// raises watchdogAlarm to Ready and un-gates the twist rate. The other
|
||||
// two remain no-ops (no layout impact).
|
||||
void WatcherResetToInitialState() {}
|
||||
Logical WatcherHandleDeathMessage(Message & /*m*/) { return False; }
|
||||
void WatcherUpdateWatch() {}
|
||||
void WatcherUpdateWatch() { UpdateWatch(); } // FUN_004b181c (body: powersub.cpp)
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Test Class Support
|
||||
@@ -189,6 +191,8 @@ class Joint; // engine skeleton node (JOINT.h); the twist target
|
||||
// Controls (@0x1F0 twist, @0x1F4 elevation); proportional, no button ramp.
|
||||
void SetAnalogTwistAxis(Scalar v) { analogTwistAxis = v; }
|
||||
void SetAnalogElevationAxis(Scalar v) { analogElevationAxis = v; }
|
||||
void CommandRecenter() { centerCommand = 1; } // @0x208 (Basic-mode re-center)
|
||||
void ClearRecenterCommand() { centerCommand = 0; } // button released (writer-owned state)
|
||||
Logical GetHorizontalEnabled() const { return horizontalEnabled; } // @0x250 (mapper free-aim gate @004afd10)
|
||||
|
||||
// Reachable horizontal (yaw) half-arc the guns can be brought to bear by
|
||||
@@ -224,7 +228,17 @@ class Joint; // engine skeleton node (JOINT.h); the twist target
|
||||
// Subsystem virtual overrides (slots on vtable @0051103c)
|
||||
//
|
||||
public:
|
||||
void WriteUpdateRecord(UpdateRecord *message, int update_model); // slot 6, @004b6a78
|
||||
// Update-record pair (task #57 direction fix): the ENGINE's semantics
|
||||
// [T0 SIMULATE.cpp] are Write = PRODUCE the outgoing record (master),
|
||||
// Read = APPLY the incoming record (replicant). @004b6a78 (slot 6,
|
||||
// base chain 41bd34 = ReadUpdateRecord) CONSUMES record+0x10/14/18 --
|
||||
// it is the READ side, previously mislabeled Write; the real WRITE
|
||||
// side is @004b6a1c (slot 7, base chain 41c500, 3 args), which Ghidra
|
||||
// missed -- recovered from raw disasm. With the old flip the master
|
||||
// never serialized twist and clobbered its own fields from the
|
||||
// uninitialized stream buffer (the replicant's 0xCDCDCDCD twist pin).
|
||||
void ReadUpdateRecord(UpdateRecord *message); // slot 6, @004b6a78 (apply)
|
||||
void WriteUpdateRecord(UpdateRecord *message, int update_model); // slot 7, @004b6a1c (serialize)
|
||||
Logical HandleDeathMessage(Message &message); // slot 9, @004b5be0 -> @004b179c
|
||||
void ResetToInitialState(); // slot 10, @004b5bf8
|
||||
|
||||
|
||||
Reference in New Issue
Block a user