the crit system, complete: two gap functions recovered, the dead sink revived, and a wrong verdict reversed (#80)
The whole critical-hit pipeline was dark, three layers deep, and one of those
layers had fooled us into a false conclusion about the 1995 binary itself.
LAYER 1 -- the trigger, recovered from the un-exported gap. The Mech MESSAGE
TABLE at 0x50bdf8 ({id, name, handler} rows) names the real
Mech::TakeDamageMessageHandler at 0x4a0230 -- message 0x12 "TakeDamage" --
plus seven sibling handlers (PlayerLink, RealMaxSpeed, BalanceCoolant,
Set/ClearBurningState, EjectPilot, DuckRequest). Inside it, the crit chance
at 0x4a0164: p = clamp(0.7 * damageLevel^2 + 0.01, 0..1), gated on the
player's simLive flag (+0x25c -- novice never crits), rolled PER BURST on the
current zone, skipping a zone already burning. Chance is ~1% on fresh armour,
~18% at half-stripped, ~58% at 90% -- crits arrive exactly as armour fails.
The handler's application loop replaces the engine base's single call, which
ignored burstCount entirely (multi-burst damage under-applied (burst-1)x).
Faithful shape: per burst, crit-roll -> CriticalHit @0049ccc4 (which routes
half the amount through the armour internally and picks ONE critical
subsystem by criticalWeight) else zone->TakeDamage -- then RE-RUN the
cylinder lottery from the impact point for the next burst, stopping early
once the mech is disabled. Multi-burst damage sprays across zones by design.
LAYER 2 -- the sink. MechSubsystem::TakeDamage was an empty btstubs stand-in;
the real body is at 0x4ac0bc (CLASSMAP had that address mislabeled
"HandleMessage"): zone damage, then on level >= 1.0 the Destroyed alarm, the
PrintState gate, the 1.0 pin, and -- for a vital subsystem -- the owner
mech's graphicAlarm to level 9, the same fall/death level the leg path
raises. That is the #28 vital-subsystem kill machinery, now real.
LAYER 3 -- the one that rewrites yesterday. The subsystem ctor DID copy
armour points + per-type scales into the private zone -- through the
ReconDamageZone PROXY, whose fields sit at struct offsets +4/+8, not the
binary's +0x140/+0x144. The floats landed on the engine object's header and
the real damageScale[] stayed zero. The 2026-07-28 experiment that "proved"
subsystem zones cannot be damaged -- and that the Myomers un-powered
self-repair was dead code in the original -- was measuring exactly this port
bug. Both verdicts reversed: the binary ctor (0x4ac7bb) initializes the zone
from the resource keys WeaponDamagePoints (required) + CriticalHitScoreBonus
(required) + Collision/Ballistic/Explosive/Laser/EnergyDamagePoints, none of
which the CSS parsed. Now parsed (with the binary's own error strings), and
the ctor writes the engine's NAMED members -- layout-parity holds, so they
land on +0x140/+0x144 faithfully. The Myomers repair branch is LIVE, in 1995
and here. KB corrected and swept (combat-damage, subsystems WAVE 6,
myomers.cpp, CLASSMAP).
Live-verified twice: [subarmor] prints real parsed scales for every subsystem
at spawn (HeatSink pts=10 scale=0.1x5, Condensers pts=5 scale=0.2x5, ...);
[critroll] landed full-chain crits in both runs (zone -> weighted pick ->
subsystem's own zone driven to 1.0 -> Destroyed); mech death/respawn and the
ammo gates un-regressed; zero crashes/asserts. Honest gaps: burst>1 spraying
is transcribed but not yet exercised live (self-damage fires burst=1), the
damageType==0 COLLISION divert (@0x49ffcc) is documented-not-reconstructed,
and the id-0x16 damage/kill report messages to the players (the authentic
stats plumbing, decoded to field level in the KB) are deferred to the #45
work.
Diags: BT_CRIT_LOG ([subarmor] + [critroll]), the existing BT_DMG_LOG.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
f7cf9850b1
commit
a5fb96ae96
+111
-14
@@ -192,21 +192,45 @@ MechSubsystem::MechSubsystem(
|
||||
damageZone = (ReconDamageZone *)new DamageZone(this, 0); // this[0x38]
|
||||
BindName((char *)damageZone + 0x15c, GetName()); // FUN_00402a98(., dz+0x15c, name)
|
||||
|
||||
// structure reference and per-facing armour into the DamageZone
|
||||
damageZone->structureReference = subsystem_resource->structureReference; // dz+0x140 = res+0x44
|
||||
for (int i = 0; i < 5; ++i)
|
||||
//
|
||||
// #80 ROOT CAUSE, half one: armour points + per-damage-TYPE scales into the
|
||||
// private zone (binary @0x4ac7bb-0x4ac853). The old code wrote these
|
||||
// through the ReconDamageZone PROXY -- whose structureReference/armour[]
|
||||
// sit at struct offsets +4/+8, NOT the binary's +0x140/+0x144 -- so the
|
||||
// floats landed on the ENGINE object's header (owningSimulation / the
|
||||
// StateIndicators) and the engine's REAL damageScale[] stayed at the
|
||||
// ctor's zeros. Every subsystem-zone TakeDamage was therefore
|
||||
// `damageLevel += amount * 0` -- the dead crit sink, and the reason the
|
||||
// 2026-07-28 "cannot damage a subsystem's own zone -- in the original too"
|
||||
// verdict was WRONG about the original. The engine base layout is
|
||||
// binary-parity (Mech__DamageZone locks its derived fields from 0x160 up),
|
||||
// so the engine's NAMED members land exactly on +0x140/+0x144.
|
||||
//
|
||||
{
|
||||
damageZone->armour[i] = subsystem_resource->armorByFacing[i]; // dz+0x144+i = res+0x30+i
|
||||
}
|
||||
// invert each facing into an absorption coefficient
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
Scalar a = damageZone->armour[i] - ArmourNumerator / damageZone->structureReference;
|
||||
if (ArmourEpsilon < fabsf(a)) // FUN_004dcd00
|
||||
DamageZone *dz = (DamageZone *)damageZone;
|
||||
dz->defaultArmorPoints = subsystem_resource->weaponDamagePoints; // dz+0x140 = res+0x44
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
damageZone->armour[i] =
|
||||
ArmourNumerator / (damageZone->armour[i] * damageZone->structureReference);
|
||||
dz->damageScale[i] = subsystem_resource->damageTypePoints[i]; // dz+0x144+i*4 = res+0x30+i*4
|
||||
}
|
||||
// normalize: points -> absorption coefficient, exactly the mech-zone
|
||||
// math (no *0.5 here -- that halving is the MECH zones' extra step)
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
Scalar a = dz->damageScale[i] - ArmourNumerator / dz->defaultArmorPoints;
|
||||
if (ArmourEpsilon < fabsf(a)) // FUN_004dcd00, eps @0x4ac864
|
||||
{
|
||||
dz->damageScale[i] =
|
||||
ArmourNumerator / (dz->damageScale[i] * dz->defaultArmorPoints);
|
||||
}
|
||||
}
|
||||
if (BTEnvOn("BT_CRIT_LOG", 0))
|
||||
DEBUG_STREAM << "[subarmor] " << (GetName() ? GetName() : "?")
|
||||
<< " pts=" << dz->defaultArmorPoints
|
||||
<< " scale={" << dz->damageScale[0] << "," << dz->damageScale[1]
|
||||
<< "," << dz->damageScale[2] << "," << dz->damageScale[3]
|
||||
<< "," << dz->damageScale[4] << "}"
|
||||
<< " critBonus=" << criticalReference << "\n" << std::flush;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,6 +461,47 @@ void MechSubsystem::ApplyZoneDamage(Damage &damage)
|
||||
((DamageZone *)damageZone)->TakeDamage(damage); // zone vtable +0x18
|
||||
}
|
||||
|
||||
//
|
||||
// @0x4ac0bc -- the REAL virtual TakeDamage (vtable slot +0x24), raw-disasm
|
||||
// 2026-07-29 (#80). CLASSMAP had this address mislabeled "HandleMessage"; the
|
||||
// body unambiguously consumes a Damage&. Was an empty btstubs stand-in, so
|
||||
// ApplyDamageAndMeasure always measured a delta of 0 and no crit ever landed.
|
||||
//
|
||||
// zone->TakeDamage(damage) ; zone vtbl+0x18
|
||||
// if (zone->damageLevel >= 1.0) ; vs [0x4ac140] = 1.0f
|
||||
// statusAlarm.SetLevel(1) ; Destroyed (0x41bbd8, this+0x2C)
|
||||
// if (printSimulationState) PrintState() ; vtbl+0x34, gate this+0x104
|
||||
// zone->damageLevel = 1.0f ; pin
|
||||
// if (vitalSubsystem) ; this+0xE4
|
||||
// owner(+0xD0) alarm+0x2C SetLevel(9) ; the VITAL-SUBSYSTEM KILL (#28)
|
||||
//
|
||||
void MechSubsystem::TakeDamage(Damage &damage)
|
||||
{
|
||||
DamageZone *dz = (DamageZone *)damageZone;
|
||||
if (dz == 0) // port guard (binary derefs)
|
||||
return;
|
||||
|
||||
dz->TakeDamage(damage); // zone vtable +0x18
|
||||
|
||||
if (dz->damageLevel >= 1.0f) // _DAT_004ac140
|
||||
{
|
||||
statusAlarm.SetLevel(1); // Destroyed
|
||||
if (printSimulationState) // +0x104
|
||||
{
|
||||
PrintState(); // vtable +0x34 @4ac8c0
|
||||
}
|
||||
dz->damageLevel = 1.0f;
|
||||
if (vitalSubsystem) // +0xE4
|
||||
{
|
||||
// the owner Mech's graphicAlarm (+0x2C) to level 9 -- the same
|
||||
// fall/death level the leg-destruction path raises. Mech is an
|
||||
// incomplete type here; the bridge lives in mechdmg.cpp.
|
||||
extern void BTMechVitalSubsystemKill(void *owner_mech);
|
||||
BTMechVitalSubsystemKill(owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// @0x4ac274 -- the AMMO-EXPLOSION fan-out (Gitea #46, re-read from the raw
|
||||
// decomp). The old reconstruction here was wrong in kind: it "re-applied the
|
||||
@@ -536,8 +601,8 @@ int
|
||||
// first pass: prime to "unset"
|
||||
subsystem_resource->criticalReference = ResourceUnset; // res+0xe0 = -1.0f
|
||||
for (int i = 0; i < 5; ++i)
|
||||
subsystem_resource->armorByFacing[i] = ResourceUnset; // res+0x30.. = -1.0f
|
||||
subsystem_resource->structureReference = ResourceUnset; // res+0x44
|
||||
subsystem_resource->damageTypePoints[i] = ResourceUnset; // res+0x30.. = -1.0f
|
||||
subsystem_resource->weaponDamagePoints = ResourceUnset; // res+0x44
|
||||
subsystem_resource->vitalSubsystemIndex = -1; // res+0x48
|
||||
memset(subsystem_resource->videoObjectName, 0, 128); // res+0x4c
|
||||
strcpy(subsystem_resource->videoObjectName, "None"); // DAT_0050dfc0
|
||||
@@ -570,6 +635,38 @@ int
|
||||
Get_Segment_Index(model_file, model_name, directories, vitalName); // FUN_004274f8
|
||||
}
|
||||
|
||||
// #80 -- the armour/crit keys the parse never consumed (binary key strings
|
||||
// @0x50e09d..0x50e15d, in this order). Their absence is the second half of
|
||||
// why the subsystem's private zone could never take damage: the ctor's
|
||||
// scale block normalized ResourceUnset garbage (and wrote it through the
|
||||
// wrong layout besides). WeaponDamagePoints and CriticalHitScoreBonus are
|
||||
// REQUIRED by the binary (" Must have a ..." @0x50e0b0/@0x50e0e6); the five
|
||||
// per-damage-type keys are optional.
|
||||
if (!model_file->GetEntry(subsystem_name, "WeaponDamagePoints",
|
||||
&subsystem_resource->weaponDamagePoints) // 0x50e09d
|
||||
&& subsystem_resource->weaponDamagePoints == ResourceUnset)
|
||||
{
|
||||
DebugStream << subsystem_name << " Must have a WeaponDamagePoints"; // 0x50e0b0
|
||||
return False;
|
||||
}
|
||||
if (!model_file->GetEntry(subsystem_name, "CriticalHitScoreBonus",
|
||||
&subsystem_resource->criticalReference) // 0x50e0d0
|
||||
&& subsystem_resource->criticalReference == ResourceUnset)
|
||||
{
|
||||
DebugStream << subsystem_name << " Must have a CriticalHitScoreBonus"; // 0x50e0e6
|
||||
return False;
|
||||
}
|
||||
model_file->GetEntry(subsystem_name, "CollisionDamagePoints",
|
||||
&subsystem_resource->damageTypePoints[0]); // 0x50e109
|
||||
model_file->GetEntry(subsystem_name, "BallisticDamagePoints",
|
||||
&subsystem_resource->damageTypePoints[1]); // 0x50e11f
|
||||
model_file->GetEntry(subsystem_name, "ExplosiveDamagePoints",
|
||||
&subsystem_resource->damageTypePoints[2]); // 0x50e135
|
||||
model_file->GetEntry(subsystem_name, "LaserDamagePoints",
|
||||
&subsystem_resource->damageTypePoints[3]); // 0x50e14b
|
||||
model_file->GetEntry(subsystem_name, "EnergyDamagePoints",
|
||||
&subsystem_resource->damageTypePoints[4]); // 0x50e15d
|
||||
|
||||
Check_Fpu();
|
||||
return True;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user