diff --git a/emulator/render-bridge/pod_render_joints.conf b/emulator/render-bridge/pod_render_joints.conf index e366695b..fa465ee0 100644 --- a/emulator/render-bridge/pod_render_joints.conf +++ b/emulator/render-bridge/pod_render_joints.conf @@ -65,6 +65,7 @@ set TEMP=c:\ rem arena1 city mission (TESTARN.EGG: map=arena1, time=day) with the RIO rem attached; stdout redirected so mission-load progress survives kills. set BT_JOINTS=1 +set L4VIEWEXT=1 set BT_MECH_LOG=1 set BT_MAP_LOG=1 set BT_MER_LOG=1 diff --git a/emulator/render-bridge/pod_render_limp.conf b/emulator/render-bridge/pod_render_limp.conf index 2ff378fa..87721a6d 100644 --- a/emulator/render-bridge/pod_render_limp.conf +++ b/emulator/render-bridge/pod_render_limp.conf @@ -65,6 +65,7 @@ set TEMP=c:\ rem arena1 city mission (TESTARN.EGG: map=arena1, time=day) with the RIO rem attached; stdout redirected so mission-load progress survives kills. set BT_JOINTS=1 +set L4VIEWEXT=1 set BT_FORCE_LIMP=3 set BT_MECH_LOG=1 set BT_MAP_LOG=1 diff --git a/restoration/source410/BT/MECH.CPP b/restoration/source410/BT/MECH.CPP index 4ef9bdd3..dfb1d066 100644 --- a/restoration/source410/BT/MECH.CPP +++ b/restoration/source410/BT/MECH.CPP @@ -246,6 +246,9 @@ Mech::Mech( gimpStrideLength = -1.0f; // the measured value is negative too globalTimeScale = 1.0f; hasGimpClips = 0; + squatCapable = 0; + turnCapable = 0; + usingExteriorClips = 0; gimpLeftSpeedMax = 1.0f; gimpRightSpeedMax = 1.0f; gimpLeftStrideLength = 1.0f; @@ -609,7 +612,31 @@ Mech::Mech( model->animationPrefix[0] <= 'z' ) { - LoadLocomotionClips(model); + // + // The view dispatch (binary ctor tail @0x4a1674 region): + // a REPLICANT loads the exterior gait clips -- you see + // it from outside -- while the MASTER loads the INTERIOR + // 'i' set, the gait as seen from the cockpit. L4VIEWEXT + // (a 1995 dev switch) forces the exterior set on a + // master for external-camera work, which is exactly what + // the render-bridge verification rigs want. + // + if (getenv("L4VIEWEXT") == NULL) + { + if (GetInstance() == ReplicantInstance) + { + LoadLocomotionClips(model); + } + else + { + LoadLocomotionClipsExt(model); + } + } + else + { + usingExteriorClips = 1; + LoadLocomotionClips(model); + } if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[mech] clips '" diff --git a/restoration/source410/BT/MECH.HPP b/restoration/source410/BT/MECH.HPP index d43657f0..f39f7fa3 100644 --- a/restoration/source410/BT/MECH.HPP +++ b/restoration/source410/BT/MECH.HPP @@ -426,6 +426,16 @@ MeasureClipStride(int slot, Scalar *total, Scalar *last_key); void LoadLocomotionClips(ModelResource *model); + // + // The INTERIOR twin (binary @004a86c8): the identical loader over the + // 'i'-suffixed clip table (swri, wwri, ...). The interior clips + // animate the inside-view skeleton -- they never bind the hip -- and + // the MASTER (viewpoint) mech loads them; replicants load the + // exterior set; the L4VIEWEXT env (a 1995 dev switch) forces the + // exterior set on a master for external-camera work. + // + void + LoadLocomotionClipsExt(ModelResource *model); int LoadClipSlot(int slot, const char *prefix, const char *suffix); @@ -717,6 +727,11 @@ // are measured from the REVERSE clips. See MECH2.NOTES.md.) // int hasGimpClips; + int squatCapable; // +0x584: squ/sqd resolved + int turnCapable; // +0x588: trn resolved -- the + // turn-in-place dispatcher's gate + int usingExteriorClips; // +0x57c: L4VIEWEXT forced + // the exterior set on a master Scalar gimpLeftSpeedMax; Scalar gimpRightSpeedMax; Scalar gimpLeftStrideLength; @@ -881,11 +896,11 @@ // // 60 ints carved out for the gait channel above (2 AlarmIndicators = 4, // 8 Scalars, animationClips[0x21] = 33, the optional limp set = 6, the - // per-frame gait state = 9 counting CString loosely); 191 -> 131. The + // per-frame gait state = 9 counting CString loosely); 191 -> 128. The // block is a layout BUDGET, not offset-exact -- MECH-LAYOUT.md holds // the real offsets. // - int reservedState[131]; + int reservedState[128]; }; #endif diff --git a/restoration/source410/BT/MECH2.CPP b/restoration/source410/BT/MECH2.CPP index 84fb57da..91e9b5ff 100644 --- a/restoration/source410/BT/MECH2.CPP +++ b/restoration/source410/BT/MECH2.CPP @@ -2046,5 +2046,202 @@ void LoadClipSlot(27, prefix, "gsr"); } + // + // The OPTIONAL squat (duck) and turn-in-place sets (binary loader tail + // @0x4a84xx). The slots explain three state numbers that never had + // clips before: squ -> slot 3, sqd -> slot 2 (the duck: state 2 plays + // the squat-down and holds at state 1; state 3 stands back up), and + // trn -> SLOT 4 -- which is why state 4 is turn-in-place. + // + squatCapable = 0; + if (ResolveAnimationClip(prefix, "squ") != NULL) + { + squatCapable = 1; + LoadClipSlot(3, prefix, "squ"); + LoadClipSlot(2, prefix, "sqd"); + } + turnCapable = 0; + if (ResolveAnimationClip(prefix, "trn") != NULL) + { + turnCapable = 1; + LoadClipSlot(4, prefix, "trn"); + } + + Check_Fpu(); +} +// +//############################################################################# +// @004a86c8 -- the INTERIOR twin. Byte-identical structure to +// LoadLocomotionClips over the 'i'-suffixed table (verbatim from the exe at +// .data:0050d94d: swri wwri ... squi sqdi trni). The interior clips animate +// the inside-view skeleton -- the operator's own cockpit and arms -- and +// never bind the hip. KEEP THE TWO TWINS: like the ClipFinished pair, each +// is the other's check. +//############################################################################# +// +void + Mech::LoadLocomotionClipsExt(ModelResource *model) +{ + Check(this); + Check_Pointer(model); + + const char + *prefix = model->animationPrefix; + // + // Zero-initialized because the guarded skips below can reach the reverse + // divide with the run pair unmeasured -- a path the (unguarded) binary + // does not have, so the stale-pair reproduction must not become an + // uninitialized read on top of it. + // + Scalar + total_a = 0.0f, last_a = 0.0f, + total_b = 0.0f, last_b = 0.0f; + + gyroRumbleTimer = 0.0f; + + // + // Stand -> walk. standSpeed is the clip's final-entry stride. + // + if (LoadClipSlot(5, prefix, "swri")) + { + legAnimation.SelectSequence(animationClips[5], NULL, 0, 0); + standSpeed = + legAnimation.keyframeData[legAnimation.keyframeCount].stride; + } + + // + // The forward walk cycle: stride = (s6 + s7) / (d6 + d7). + // + if ( + LoadClipSlot(6, prefix, "wwri") && + LoadClipSlot(7, prefix, "wwli") + ) + { + MeasureClipStride(6, &total_a, &last_a); + MeasureClipStride(7, &total_b, &last_b); + walkStrideLength = (total_a + total_b) / (last_a + last_b); + } + + LoadClipSlot(8, prefix, "wsri"); + LoadClipSlot(9, prefix, "wsli"); + + // + // Walk -> run. reverseSpeedMax is measured from wrr the same way + // standSpeed is from swr. + // + if (LoadClipSlot(10, prefix, "wrri")) + { + legAnimation.SelectSequence(animationClips[10], NULL, 0, 0); + reverseSpeedMax = + legAnimation.keyframeData[legAnimation.keyframeCount].stride; + } + LoadClipSlot(11, prefix, "wrli"); + + // + // The run cycle. + // + if ( + LoadClipSlot(12, prefix, "rrri") && + LoadClipSlot(13, prefix, "rrli") + ) + { + MeasureClipStride(12, &total_a, &last_a); + MeasureClipStride(13, &total_b, &last_b); + reverseStrideLength = (total_a + total_b) / (last_a + last_b); + } + + LoadClipSlot(14, prefix, "rwri"); + LoadClipSlot(15, prefix, "rwli"); + + // + // The bump/crash stagger clip, slot 0x20 -- the reason the clip array is + // bigger than the state-name table. + // + LoadClipSlot(0x20, prefix, "bmpi"); + + // + // The reverse set. gimpSpeedMax is measured from the entry clip; the + // cycle stride divide below reproduces the binary's stale-pair bug (see + // the header comment) and is negated exactly where the binary negates. + // + if (LoadClipSlot(16, prefix, "sbri")) + { + legAnimation.SelectSequence(animationClips[16], NULL, 0, 0); + gimpSpeedMax = + legAnimation.keyframeData[legAnimation.keyframeCount].stride; + } + LoadClipSlot(17, prefix, "sbli"); + LoadClipSlot(20, prefix, "bsri"); + LoadClipSlot(21, prefix, "bsli"); + + if ( + LoadClipSlot(18, prefix, "bbri") && + LoadClipSlot(19, prefix, "bbli") + ) + { + MeasureClipStride(18, &total_a, &last_a); + MeasureClipStride(19, &total_a, &last_a); // the binary's stale pair: + // total_b/last_b still hold + // the run-cycle figures + gimpStrideLength = (total_a + total_b) / (last_a + last_b); + gimpStrideLength = -gimpStrideLength; + } + + // + // The OPTIONAL limp set. Probe for wgl; a model without it has no limp + // clips at all, and the limp machine must never be entered for it. + // + hasGimpClips = 0; + if (ResolveAnimationClip(prefix, "wgli") != NULL) + { + hasGimpClips = 1; + + if (LoadClipSlot(22, prefix, "wgli")) + { + legAnimation.SelectSequence(animationClips[22], NULL, 0, 0); + gimpLeftSpeedMax = + legAnimation.keyframeData[legAnimation.keyframeCount].stride; + } + if (LoadClipSlot(23, prefix, "wgri")) + { + legAnimation.SelectSequence(animationClips[23], NULL, 0, 0); + gimpRightSpeedMax = + legAnimation.keyframeData[legAnimation.keyframeCount].stride; + } + if (LoadClipSlot(24, prefix, "ggri")) + { + MeasureClipStride(24, &total_a, &last_a); + gimpLeftStrideLength = total_a / last_a; + } + if (LoadClipSlot(25, prefix, "ggli")) + { + MeasureClipStride(25, &total_a, &last_a); + gimpRightStrideLength = total_a / last_a; + } + LoadClipSlot(26, prefix, "gsli"); + LoadClipSlot(27, prefix, "gsri"); + } + + // + // The OPTIONAL squat (duck) and turn-in-place sets (binary loader tail + // @0x4a84xx). The slots explain three state numbers that never had + // clips before: squ -> slot 3, sqd -> slot 2 (the duck: state 2 plays + // the squat-down and holds at state 1; state 3 stands back up), and + // trn -> SLOT 4 -- which is why state 4 is turn-in-place. + // + squatCapable = 0; + if (ResolveAnimationClip(prefix, "squi") != NULL) + { + squatCapable = 1; + LoadClipSlot(3, prefix, "squi"); + LoadClipSlot(2, prefix, "sqdi"); + } + turnCapable = 0; + if (ResolveAnimationClip(prefix, "trni") != NULL) + { + turnCapable = 1; + LoadClipSlot(4, prefix, "trni"); + } + Check_Fpu(); }