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
+1 -1
View File
@@ -6,7 +6,7 @@ source_sections: "PROGRESS_LOG.md §7 (Phase 7), §8 (P6); docs/HARD_PROBLEMS.md
related_topics: [wintesla-port, combat-damage, locomotion, decomp-reference]
key_terms: [master, replicant, EGG, dead-reckon]
open_questions:
- "pod-LAN config (real IPs); update-record velocity from the body-channel projection; peer SimulationState/DropZoneLocation replication (authentic peer warp spheres); stream the model deadband constants (UpdatePositionDiffrence et al) to the update-record senders"
- "pod-LAN config (real IPs); update-record velocity from the body-channel projection; peer SimulationState/DropZoneLocation replication (authentic peer warp spheres); authentic per-mech turn rates now streamed (task #4) but not yet wired into the turn code"
---
# Multiplayer
+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;
}
+48 -40
View File
@@ -105,49 +105,57 @@ struct ShotDescriptor
// parser (e.g. pfVar3+0x11 == MaxAcceleration).
//
struct Mech__ModelResource:
public JointedMover::ModelResource
public JointedMover::ModelResource // == Mover__ModelResource, 0x40 bytes
{
char animationPrefix[12]; // +0x10 "AnimationPrefix" (3-letter)
Scalar maxAcceleration; // +0x11 "MaxAcceleration"
Scalar superStopAcceleration; // +0x12 "SuperStopAcceleration"
Scalar throttleAdjustment; // +0x13 "ThrottleAdjustment"
Scalar lookLeftAngle; // +0x14 "LookLeftAngle"
Scalar lookRightAngle; // +0x15 "LookRightAngle"
Scalar lookFrontAngle; // +0x16 "LookFrontAngle"
Scalar lookBackAngle; // +0x17 "LookBackAngle"
Scalar walkingTurnRate; // +0x18 "WalkingTurnRate"
Scalar runningTurnRate; // +0x19 "RunningTurnRate"
char skeletonName[64]; // +0x1a (.skl spec, 0x10 words)
Scalar deathEffectResourceID; // +0x1d resolved "DeathEffect" id
Scalar deathSplashDamage; // +0x1e "DeathSplashDamage"
Scalar deathSplashRadius; // +0x1f "DeathSplashRadius"
Scalar maxUnstableAcceleration; // +0x20 "MaxUnstableAcceleration"
Scalar unstableAccelerationEffect; // +0x21 "UnstableAccelerationEffect"
Scalar unstableGunTheEngineEffect; // +0x22 "UnstableGunTheEngineEffect"
Scalar unstableSuperStopEffect; // +0x23 "UnstableSuperStopEffect"
Scalar unstableHighVelocityEffect; // +0x24 "UnstableHighVelocityEffect"
Scalar unstableStopedTurnEffect; // +0x25 "UnstableStopedTurnEffect"
Scalar updatePositionDiffrence; // +0x26 "UpdatePositionDiffrence"
Scalar updateTurnVelocityDiffrence;// +0x27 "UpdateTurnVelocityDiffrence"
Scalar updateTurnDegreeDiffrence; // +0x28 "UpdateTurnDegreeDiffrence"
Scalar timeDelay; // +0x29 "TimeDelay"
Vector3D cameraOffset; // +0x2a "CameraOffset"
char shadowJointName[20]; // +0x2d "ShadowJointName" (<20 chars)
Scalar relativeMechValue; // +0x1c "RelativeMechValue"
// --- raw recovered fields read by the ctor (Pass 2, model record) -----
// Offsets are the decomp's record offsets; names are best-effort.
char *base; // record base pointer (model->base + 0xa8)
int deathEffectField; // resolved death-effect handle source
Scalar field48, field4c, field60, field64;
int field68, field6c;
Scalar field70, field74, field78, field7c;
Scalar field80, field84, field88, field8c, field90, field94, field98, field9c;
Scalar fieldA0;
int fieldA4;
int classID; // stamped by CreateModelResourceStub (0xBB9)
//
// AUTHORITATIVE overlay of the 200-byte (0x32-word) segment-0xf record
// (task #4, 2026-07-11). Writer chain [T1]: FUN_004a2da8 (Mech) after
// FUN_004238bc (Mover, words 0x00-0x0F) + FUN_00435ac8 (Reticle, words
// 0x1A-0x1B). Runtime reader: the ctor pass @0x4a24d1-0x4a2968. The
// old struct had two size errors (animationPrefix char[12] vs 4 bytes;
// a FICTITIOUS char skeletonName[64]) that skewed every later member
// +2/+15 words and pushed the fieldXX block past the record end --
// every fieldXX read was out-of-bounds garbage (the forwardCycleRate
// "floor 25" era). There is NO skeleton-name block in this record.
//
char animationPrefix[4]; // 0x40 w0x10 "AnimationPrefix" (3 letters + NUL)
Scalar maxAcceleration; // 0x44 w0x11 "MaxAcceleration"
Scalar superStopAcceleration; // 0x48 w0x12 "SuperStopAcceleration"
Scalar throttleAdjustment; // 0x4C w0x13 "ThrottleAdjustment"
Scalar lookLeftAngle; // 0x50 w0x14 "LookLeftAngle" (degrees)
Scalar lookRightAngle; // 0x54 w0x15 "LookRightAngle"
Scalar lookFrontAngle; // 0x58 w0x16 "LookFrontAngle"
Scalar lookBackAngle; // 0x5C w0x17 "LookBackAngle"
Scalar walkingTurnRate; // 0x60 w0x18 "WalkingTurnRate" (degrees)
Scalar runningTurnRate; // 0x64 w0x19 "RunningTurnRate"
Scalar reticleX; // 0x68 w0x1A "ReticleX" (Reticle level; default 0)
Scalar reticleY; // 0x6C w0x1B "ReticleY"
Scalar relativeMechValue; // 0x70 w0x1C "RelativeMechValue"
int deathEffectResourceID; // 0x74 w0x1D "DeathEffect" (resolved id)
Scalar deathSplashDamage; // 0x78 w0x1E "DeathSplashDamage"
Scalar deathSplashRadius; // 0x7C w0x1F "DeathSplashRadius"
Scalar maxUnstableAcceleration; // 0x80 w0x20 "MaxUnstableAcceleration"
Scalar unstableAccelerationEffect; // 0x84 w0x21 "UnstableAccelerationEffect"
Scalar unstableGunTheEngineEffect; // 0x88 w0x22 "UnstableGunTheEngineEffect"
Scalar unstableSuperStopEffect; // 0x8C w0x23 "UnstableSuperStopEffect"
Scalar unstableHighVelocityEffect; // 0x90 w0x24 "UnstableHighVelocityEffect"
Scalar unstableStopedTurnEffect; // 0x94 w0x25 "UnstableStopedTurnEffect"
Scalar updatePositionDiffrence; // 0x98 w0x26 "UpdatePositionDiffrence"
Scalar updateTurnVelocityDiffrence;// 0x9C w0x27 "UpdateTurnVelocityDiffrence"
Scalar updateTurnDegreeDiffrence; // 0xA0 w0x28 "UpdateTurnDegreeDiffrence" (deg; ctor x pi/180)
Scalar timeDelay; // 0xA4 w0x29 "TimeDelay"
Vector3D cameraOffset; // 0xA8 w0x2A-0x2C "CameraOffset"
char shadowJointName[20]; // 0xB4 w0x2D-0x31 "ShadowJointName" (<=19 chars)
};
static_assert(sizeof(Mover::ModelResource) == 0x40, "Mover model record 0x40");
static_assert(sizeof(Mech__ModelResource) == 0xC8, "Mech model record 200 bytes");
static_assert(offsetof(Mech__ModelResource, maxAcceleration) == 0x44, "MaxAcceleration w0x11");
static_assert(offsetof(Mech__ModelResource, relativeMechValue) == 0x70, "RelativeMechValue w0x1C");
static_assert(offsetof(Mech__ModelResource, updatePositionDiffrence) == 0x98, "UpdatePositionDiffrence w0x26");
static_assert(offsetof(Mech__ModelResource, cameraOffset) == 0xA8, "CameraOffset w0x2A");
static_assert(offsetof(Mech__ModelResource, shadowJointName) == 0xB4, "ShadowJointName w0x2D");
//###########################################################################
//######################### Mech::UpdateRecord ##########################
//###########################################################################
+6 -16
View File
@@ -1377,11 +1377,11 @@ void
// Prime the same clip-advance scalars the master's gait block sets
// each frame -- uninitialized on a replicant they read 0, freezing
// the clip at advance-time dt*0 (observed: legState engaged at 11,
// legCycle stuck 0). Same forwardCycleRate floor as the master
// (the model-record decode reads ~1 u/s^2 -- a 55s ramp; floor 25).
// legCycle stuck 0). (The old forwardCycleRate floor is retired:
// the ctor reads the authentic MaxAcceleration since the task #4
// record-layout fix.)
globalTimeScale = 1.0f;
idleStrideScale = 1.0f;
if (forwardCycleRate < 25.0f) forwardCycleRate = 25.0f;
// reverseSpeedMax2@0x7a0: the run-cycle rise-CLAMP (leg case 12/13);
// LoadLocomotionClips doesn't set it, so a replicant reads 0xCDCD
// (-4.3e8) and the clamp CLOBBERS legCycleSpeed the moment the run
@@ -1982,19 +1982,9 @@ void
// and the clip crawled in slow motion). Either the record
// field is mis-offset or its units differ (per-frame?). Until
// the field decode is verified against the raw parser, FLOOR
// the rate at a pod-plausible 25 u/s^2 (0 -> run in ~2.5s);
// values above that (a genuine tuning read) pass through.
{
static int s_rateLogged = 0;
if (!s_rateLogged)
{
s_rateLogged = 1;
DEBUG_STREAM << "[gait] model accel read=" << forwardCycleRate
<< " (floored to >=25)" << "\n" << std::flush;
}
}
if (!(forwardCycleRate >= 25.0f && forwardCycleRate < 10000.0f))
forwardCycleRate = 25.0f;
// the authentic MaxAcceleration reads directly since the task #4
// record-layout fix (madcat: 30 u/s^2); the old floor-25 block
// and its one-shot log are retired.
if (!IsMechDestroyed()) // a dead mech keeps its death movementMode
SetMovementMode(1); // ground, non-death, non-airborne
// reverseSpeedMax2@0x7a0 is the run-cycle bodyCycleSpeed CLAMP (AdvanceBody