From 63206d7f19a3ed6a950167a77f7923ce9093a08e Mon Sep 17 00:00:00 2001 From: arcattack Date: Sat, 11 Jul 2026 12:36:12 -0500 Subject: [PATCH] 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 --- context/multiplayer.md | 2 +- game/reconstructed/mech.cpp | 86 ++++++++++++++++++----------------- game/reconstructed/mech.hpp | 88 ++++++++++++++++++++---------------- game/reconstructed/mech4.cpp | 22 +++------ 4 files changed, 99 insertions(+), 99 deletions(-) diff --git a/context/multiplayer.md b/context/multiplayer.md index cdba6fa..99828e6 100644 --- a/context/multiplayer.md +++ b/context/multiplayer.md @@ -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 diff --git a/game/reconstructed/mech.cpp b/game/reconstructed/mech.cpp index c683c34..70b549e 100644 --- a/game/reconstructed/mech.cpp +++ b/game/reconstructed/mech.cpp @@ -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; } diff --git a/game/reconstructed/mech.hpp b/game/reconstructed/mech.hpp index b9f7cb2..9dd6ecf 100644 --- a/game/reconstructed/mech.hpp +++ b/game/reconstructed/mech.hpp @@ -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 ########################## //########################################################################### diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 40454d4..0aa19af 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -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