diff --git a/code/components/gta-core-five/src/PatchVehiclePhysicsFrameRate.cpp b/code/components/gta-core-five/src/PatchVehiclePhysicsFrameRate.cpp index 5274478ba9..012cec358a 100644 --- a/code/components/gta-core-five/src/PatchVehiclePhysicsFrameRate.cpp +++ b/code/components/gta-core-five/src/PatchVehiclePhysicsFrameRate.cpp @@ -8,6 +8,8 @@ #include #include +#include "CrossBuildRuntime.h" + static bool g_wheelFpsFixEnabled = false; static constexpr float kPhysicsStep = 1.0f / 60.0f; @@ -33,15 +35,27 @@ struct IntegrationState // Last step's output force; skipped frames report this held value float lastOutput[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; + // m_Force/m_Torque deltas the last run added to a rigid collider; skipped frames re-apply them + float heldForce[8] = {}; + uint32_t heldForceFrame = 0; + + // Audio/vfx slip readouts (eff/fwd/side) + float slipTarget[kMaxIntegrationWheels][3] = {}; + float slipDisplay[kMaxIntegrationWheels][3] = {}; + bool hasRunOnce = false; uint32_t lastFrame = 0; }; + +constexpr uint32_t kSlipFieldOffsets[3] = { 0, 8, 12 }; } static uint32_t g_wheelCompressionOffset; static char kColliderTypeOffset; static uint32_t kColliderForceOffset; +static uint32_t kWheelDynamicFlagsOffset; +static uint32_t g_wheelEffSlipOffset; static std::mutex g_stateMutex; static std::unordered_map g_state; @@ -97,6 +111,48 @@ static void ProcessIntegrationTask(void* collider, void* internalForce, void* in locals->outputVel[2] = state->lastOutput[2]; locals->outputVel[3] = state->lastOutput[3]; + // Re-apply the last run's force so the step's impulse spreads across the window + if ((frameCount - state->heldForceFrame) <= 4 && *reinterpret_cast(static_cast(collider) + kColliderTypeOffset) == 0) + { + auto forceAccum = reinterpret_cast(static_cast(collider) + kColliderForceOffset); + + for (int lane = 0; lane < 8; lane++) + { + if (lane != 3 && lane != 7) + { + forceAccum[lane] += state->heldForce[lane]; + } + } + } + + auto wheels = static_cast(ppWheels); + int trackedWheels = std::min(numWheels, kMaxIntegrationWheels); + + // Contact-flag shuffle the skipped run would have done (WF_HIT_PREV = WF_HIT, WF_HIT + // cleared); stale WF_HIT makes the still-vehicle contact filter reject ground contacts + for (int i = 0; i < trackedWheels; i++) + { + auto flags = reinterpret_cast(wheels[i] + kWheelDynamicFlagsOffset); + uint32_t value = *flags; + *flags = (value & ~3u) | ((value & 1u) << 1); + } + + // Chase the slip readouts toward the last runs values + if (state->hasRunOnce) + { + float alpha = std::min(timeStep / (2.0f * kPhysicsStep), 1.0f); + + for (int i = 0; i < trackedWheels; i++) + { + for (int j = 0; j < 3; j++) + { + float& display = state->slipDisplay[i][j]; + display += (state->slipTarget[i][j] - display) * alpha; + *reinterpret_cast(wheels[i] + g_wheelEffSlipOffset + kSlipFieldOffsets[j]) = display; + } + } + } + return; } @@ -112,14 +168,14 @@ static void ProcessIntegrationTask(void* collider, void* internalForce, void* in { auto compression = reinterpret_cast(wheels[i] + g_wheelCompressionOffset); float trueDelta = compression[0] - state->lastCompression[i]; - compression[1] = compression[0] - trueDelta * (kPhysicsStep / state->sinceLastRun); + float damperDelta = trueDelta * (kPhysicsStep / state->sinceLastRun); + + compression[1] = compression[0] - damperDelta; } } - // Rigid colliders only accumulate force into m_Force/m_Torque; the simulator - // later integrates it with the real frame dt, so the 1/60 dt would deliver only a - // fraction of the impulse. Snapshot now and rescale what the run adds by 1/60 / frame dt - // Other collider types apply the impulse immediately with our dt and don't need fixup + // Rigid colliders accumulate into m_Force/m_Torque, integrated by the simulator with each + // slice's real dt; snapshot to capture what the run adds (other types apply immediately) int colliderType = *reinterpret_cast(static_cast(collider) + kColliderTypeOffset); auto forceAccum = reinterpret_cast(static_cast(collider) + kColliderForceOffset); @@ -138,16 +194,13 @@ static void ProcessIntegrationTask(void* collider, void* internalForce, void* in if (colliderType == 0) { - float scale = kPhysicsStep / timeStep; - // xyz of m_Force (0-2) and m_Torque (4-6); leave the W lanes alone. for (int lane = 0; lane < 8; lane++) { - if (lane != 3 && lane != 7) - { - forceAccum[lane] = forceBefore[lane] + (forceAccum[lane] - forceBefore[lane]) * scale; - } + state->heldForce[lane] = (lane != 3 && lane != 7) ? forceAccum[lane] - forceBefore[lane] : 0.0f; } + + state->heldForceFrame = frameCount; } for (int i = 0; i < trackedWheels; i++) @@ -155,6 +208,27 @@ static void ProcessIntegrationTask(void* collider, void* internalForce, void* in state->lastCompression[i] = *reinterpret_cast(wheels[i] + g_wheelCompressionOffset); } + for (int i = 0; i < trackedWheels; i++) + { + for (int j = 0; j < 3; j++) + { + auto field = reinterpret_cast(wheels[i] + g_wheelEffSlipOffset + kSlipFieldOffsets[j]); + state->slipTarget[i][j] = *field; + + if (state->hasRunOnce) + { + float alpha = std::min(timeStep / (2.0f * kPhysicsStep), 1.0f); + float& display = state->slipDisplay[i][j]; + display += (state->slipTarget[i][j] - display) * alpha; + *field = display; + } + else + { + state->slipDisplay[i][j] = *field; + } + } + } + state->lastOutput[0] = locals->outputVel[0]; state->lastOutput[1] = locals->outputVel[1]; state->lastOutput[2] = locals->outputVel[2]; @@ -171,5 +245,7 @@ static HookFunction hookFunction([]() g_wheelCompressionOffset = *hook::get_pattern("45 0F 57 ? F3 0F 11 ? ? ? 00 00 F3 0F 5C", 8); kColliderTypeOffset = *hook::get_pattern("8B 4B ? 8D 41 ? 83 F8 ? 76 ? 0F 28 43", 2); kColliderForceOffset = *hook::get_pattern("0F 28 AB ? ? ? ? F3 0F 10 B3", 3); + kWheelDynamicFlagsOffset = *hook::get_pattern(xbr::IsGameBuildOrGreater<2802>() ? "83 A3 ? ? ? ? ? F6 83 ? ? ? ? ? 74 ? 83 8B" : "83 A3 ? ? ? ? ? 44 84 B3", 2); + g_wheelEffSlipOffset = *hook::get_pattern("0F 2F 81 ? ? ? ? 0F 28 F1", 3); g_origProcessIntegrationTask = hook::trampoline(hook::get_call(hook::get_pattern("E8 ? ? ? ? 4C 8D 9C 24 ? ? ? ? B0 ? F3 0F 10 45")), ProcessIntegrationTask); });