Model record: the authoritative 200-byte overlay -- struct skew fixed, floor-25 era retired (task #4)

Two size errors (animationPrefix char[12] vs 4 bytes; a FICTITIOUS 64-byte
skeletonName block -- the record has no skl name) skewed every member after
word 0x10 by +2/+15 words and pushed the fieldXX block past the record end.
Full writer map recovered (FUN_004a2da8 + Mover FUN_004238bc + Reticle
FUN_00435ac8 chains) + runtime reader map (ctor pass @0x4a24d1-0x4a2968,
capstone-verified).  Struct rewritten as the exact overlay (static_assert-
locked); all Pass-2 consumers renamed to real fields; retired: ctor +
replicant + master-gait floor-25 hacks, the throttle guard, the fictitious
classID stamp.  Verified: accel=30 superStop=10 throttleAdj=1 (was ~1/OOB/0),
deadbands byte-identical via named fields, full walk->run lifecycle clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-11 12:36:12 -05:00
co-authored by Claude Fable 5
parent 001ca16af9
commit 63206d7f19
4 changed files with 99 additions and 99 deletions
+44 -42
View File
@@ -1221,10 +1221,10 @@ Mech::Mech(
ModelResource *model = (ModelResource *)modelResourceDesc->resourceAddress;
Check_Pointer(model);
Wword(0x68) = model->base + 0xa8; // FUN_00408440(this+0x68,...)
Wword(0x130) = model->field74;
Wword(0x148) = model->field78;
Wword(0x149) = model->field7c;
Wword(0x68) = model->cameraOffset; // FUN_00408440(this+0x1a0, rec+0xA8)
Wword(0x130) = model->deathEffectResourceID; // rec+0x74 -> mech+0x4c0
Wword(0x148) = model->deathSplashDamage; // rec+0x78 -> mech+0x520
Wword(0x149) = model->deathSplashRadius; // rec+0x7C -> mech+0x524
Wword(0x195) = Wword(0x196) = 0;
Wword(0xfd) = 0;
masterAlarm.SetLevel(0); // FUN_0041bbd8(this+0xe7,0)
@@ -1237,55 +1237,54 @@ Mech::Mech(
// gimp/ground copies (ResetToInitialState restores 0x344 from 0x5b8 -- raw
// part_012.c:9441); +0x48 -> airborneCycleRate; +0x4c -> forwardThrottleScale
// (the ctor's "=1.0, writer not in decomp" note is resolved: THIS is the writer).
forwardCycleRate = model->maxAcceleration; // @0x344 (this[0xd1] = +0x44)
// FLOOR all cycle slew rates (the record field decodes to 1.0 -- see the
// maxAcceleration note): an unfloored gimpCycleRate made REVERSE accelerate
// at 1 u/s^2 (12+ seconds to back up -- the user's '10-20s to change
// directions'). Same floor as the mech4 forward floor (25 u/s^2).
if (!(forwardCycleRate >= 25.0f && forwardCycleRate < 10000.0f))
forwardCycleRate = 25.0f;
forwardCycleRate = model->maxAcceleration; // @0x344 (this[0xd1] = rec+0x44 = 30
// for the madcat -- the struct skew
// used to land this on
// ThrottleAdjustment ~1.0)
// (The "floor 25" era is over: the old struct skew landed this read on
// ThrottleAdjustment ~1.0 -- the authentic MaxAcceleration (madcat: 30)
// now reads directly. task #4)
gimpCycleRate = forwardCycleRate; // @0x5b0 (this[0x16c])
groundCycleRate = forwardCycleRate; // @0x5b8 (this[0x16e])
airborneCycleRate = model->field48; // @0x5bc (this[0x16f])
if (!(airborneCycleRate >= 25.0f && airborneCycleRate < 10000.0f))
airborneCycleRate = 25.0f;
// ⚠ forwardThrottleScale: the record field at +0x4c decodes to 0 for the
// Blackhawk (same undecoded-ModelResource-layout problem as maxAcceleration
// == 1.0 above) -- a literal read ZEROES the forward speed demand
// (speedDemand = topSpeed * throttle * THIS) and the mech can never walk
// forward. Guard until the record layout is decoded against the raw parser.
forwardThrottleScale = model->field4c; // @0x5c0 (this[0x170])
if (!(forwardThrottleScale > 0.01f && forwardThrottleScale < 100.0f))
forwardThrottleScale = 1.0f;
Wword(0x12f) = model->field70;
Wword(0x1e1) = model->field80; Wword(0x1e2) = model->field84;
Wword(0x1e3) = model->field88; Wword(0x1e4) = model->field8c;
Wword(0x1e5) = model->field90; Wword(0x1e6) = model->field94;
airborneCycleRate = model->superStopAcceleration; // @0x5bc (this[0x16f] = rec+0x48)
forwardThrottleScale = model->throttleAdjustment; // @0x5c0 (this[0x170] = rec+0x4C;
// the old "decodes to 0" was the
// out-of-bounds struct read)
if (getenv("BT_REPL_LOG"))
DEBUG_STREAM << "[model] accel=" << forwardCycleRate
<< " superStop=" << airborneCycleRate
<< " throttleAdj=" << forwardThrottleScale << std::endl;
Wword(0x12f) = model->relativeMechValue; // rec+0x70 -> mech+0x4bc
Wword(0x1e1) = model->maxUnstableAcceleration; // rec+0x80-0x94 -> mech+0x784..0x798
Wword(0x1e2) = model->unstableAccelerationEffect; // (the six Unstable* effects)
Wword(0x1e3) = model->unstableGunTheEngineEffect;
Wword(0x1e4) = model->unstableSuperStopEffect;
Wword(0x1e5) = model->unstableHighVelocityEffect;
Wword(0x1e6) = model->unstableStopedTurnEffect;
// Angle fields converted from degrees to radians.
Wword(0x159) = (int)(model->lookLeftAngle * DegreesToRadians); // +0x50
Wword(0x15a) = (int)(model->lookRightAngle * DegreesToRadians); // +0x54
Wword(0x15b) = (int)(model->lookFrontAngle * DegreesToRadians); // +0x58
Wword(0x15c) = (int)(model->lookBackAngle * DegreesToRadians); // +0x5c
// The update-record deadbands, read at the RAW record offsets the binary
// itself uses ([esi+0x98/0x9c/0xa0], disasm @0x4a26a7-0x4a26ef) -- the
// Mech__ModelResource struct fieldXX members are LAYOUT-SKEWED (audit
// task: struct field98 read -1.5 where raw word 0x26 holds 0.3). A
// resource record is a byte blob, so word-indexed reads are the correct
// idiom (not the object databinding trap).
updateTurnAngleDeadband = ((Scalar *)model)[0x28] * DegreesToRadians; // @0x770 <- UpdateTurnDegreeDiffrence
updateTurnVelocityDeadband = ((Scalar *)model)[0x27]; // @0x76c <- UpdateTurnVelocityDiffrence
updatePositionDeadband = ((Scalar *)model)[0x26]; // @0x768 <- UpdatePositionDiffrence
// The update-record deadbands (task #3; the struct is now the verified
// 200-byte overlay, so the named fields ARE the raw offsets).
updateTurnAngleDeadband = model->updateTurnDegreeDiffrence * DegreesToRadians; // @0x770
updateTurnVelocityDeadband = model->updateTurnVelocityDiffrence; // @0x76c
updatePositionDeadband = model->updatePositionDiffrence; // @0x768
if (getenv("BT_REPL_LOG"))
DEBUG_STREAM << "[deadband] pos=" << updatePositionDeadband
<< " turnVel=" << updateTurnVelocityDeadband
<< " turnAngle=" << updateTurnAngleDeadband << " (rad)" << std::endl;
Wword(0x10c) = model->fieldA4;
Wword(0x10c) = model->timeDelay; // rec+0xA4 -> mech+0x430
// NOTE: the decomp writes word slots this[0x15d]/this[0x15e]. Routed to the
// scratch bank (unnamed pose-angle fields) -- writing through (float*)(this +
// 0x15d) would be typed-pointer arithmetic on Mech* (== this + 0x15d*sizeof(Mech)).
Wword(0x15d) = (int)(model->field60 * DegreesToRadians);
Wword(0x15e) = (int)(model->field64 * DegreesToRadians);
// The AUTHENTIC per-mech turn rates (rec+0x60/0x64, deg->rad ->
// mech+0x574/0x578) -- still absorbed pending the turn-code wiring
// (locomotion polish item: the port turns at a bring-up constant rate).
Wword(0x15d) = model->walkingTurnRate * DegreesToRadians;
Wword(0x15e) = model->runningTurnRate * DegreesToRadians;
//
// SHADOW JOINT resolve (task #20; CORRECTED -- the earlier draft misread
@@ -1413,8 +1412,8 @@ Mech::Mech(
Wword(0xe5) = 0;
Wword(0xdd) = Wword(0xde) = 1;
Wword(0xdb) = model->field68;
Wword(0xdc) = model->field6c;
Wword(0xdb) = model->reticleX; // rec+0x68 -> mech+0x36c
Wword(0xdc) = model->reticleY; // rec+0x6C -> mech+0x370
//
// ---- Pass 3: build the per-zone Mech::DamageZone objects (resource type 0x14) ----
@@ -2120,7 +2119,9 @@ ResourceDescription::ResourceID
resource_file, model_name, model_file, directories, model) == -1
|| ParseJointResource( // FUN_00435ac8
resource_file, model_name, model_file, directories,
model->skeletonName) == -1)
(char *)&model->reticleX) == -1) // rec words 0x1A-0x1B (Reticle level;
// the old 'skeletonName' block was
// fictitious -- no skl name in this record)
{
goto fail;
}
@@ -2258,7 +2259,8 @@ Logical
{
return False;
}
model->classID = MechClassID; // model[7] = 0xBB9
// (the 200-byte record has NO classID field -- the old struct appended a
// fictitious one; the class identity rides the resource TYPE/segment.)
return True;
}