diff --git a/Core/GameEngine/Include/Common/GameDefines.h b/Core/GameEngine/Include/Common/GameDefines.h index be20d29c292..ee104b46ed3 100644 --- a/Core/GameEngine/Include/Common/GameDefines.h +++ b/Core/GameEngine/Include/Common/GameDefines.h @@ -91,6 +91,26 @@ #define PRESERVE_RETAIL_PARTICLES (1) // Preserve original look of particles present in retail Generals 1.08 and Zero Hour 1.04 #endif +// Whether to preserve the 1.41x speed discrepancy between straight and diagonal movements of all objects that move via a Locomotor. +// Set this to 0 when world objects need to move at consistent speed in all directions. +#ifndef PRESERVE_RETAIL_PHYSICS_FORWARD_SPEED_DISCREPANCY +#define PRESERVE_RETAIL_PHYSICS_FORWARD_SPEED_DISCREPANCY (1) +#endif + +// Whether to preserve the arithmetic mean speed based on the original forward speed discrepancy bug. +// The locomotor speeds from the INI files are effectively scaled up a bit so that on average the world objects travel at comparable speeds. +// Set this to 0 when speeds are set correctly by INI settings (recommended). +#ifndef PRESERVE_RETAIL_PHYSICS_FORWARD_SPEED_AVERAGE +#define PRESERVE_RETAIL_PHYSICS_FORWARD_SPEED_AVERAGE (1) +#endif + +// Whether to preserve the 1.41x speed discrepancy between straight and diagonal movements of all objects during cinematics. +// Is mostly relevant for the original campaign missions. +#ifndef PRESERVE_RETAIL_PHYSICS_FORWARD_SPEED_DISCREPANCY_IN_CINEMATICS +#define PRESERVE_RETAIL_PHYSICS_FORWARD_SPEED_DISCREPANCY_IN_CINEMATICS (1) +#endif + + #ifndef RETAIL_COMPATIBLE_CRC #define RETAIL_COMPATIBLE_CRC (1) // Game is expected to be CRC compatible with retail Generals 1.08, Zero Hour 1.04 #endif @@ -184,3 +204,19 @@ #define DEFAULT_DISPLAY_BIT_DEPTH 32 #define DEFAULT_DISPLAY_WIDTH 800 // The standard resolution this game was designed for #define DEFAULT_DISPLAY_HEIGHT 600 // The standard resolution this game was designed for + + +// NON-TWEAKABLE DEFINES ARE DOWN HERE + +// Whether the retail forward speed is used unconditionally, for every object at all times. +#define USE_RETAIL_PHYSICS_FORWARD_SPEED_DISCREPANCY() \ + (PRESERVE_RETAIL_PHYSICS_FORWARD_SPEED_DISCREPANCY || RETAIL_COMPATIBLE_CRC) + +// Whether the forward speed is scaled to a former averaged value. +#define USE_RETAIL_PHYSICS_FORWARD_SPEED_AVERAGE() \ + (PRESERVE_RETAIL_PHYSICS_FORWARD_SPEED_AVERAGE && !USE_RETAIL_PHYSICS_FORWARD_SPEED_DISCREPANCY()) + +// Whether the retail forward speed is used for the duration of a cinematic event. +// Is only meaningful when the retail forward speed discrepancy is not preserved and the forward speed is scaled. +#define USE_RETAIL_PHYSICS_FORWARD_SPEED_DISCREPANCY_IN_CINEMATICS() \ + (PRESERVE_RETAIL_PHYSICS_FORWARD_SPEED_DISCREPANCY_IN_CINEMATICS && !USE_RETAIL_PHYSICS_FORWARD_SPEED_DISCREPANCY() && USE_RETAIL_PHYSICS_FORWARD_SPEED_AVERAGE()) diff --git a/Core/GameEngine/Include/Common/GameUtility.h b/Core/GameEngine/Include/Common/GameUtility.h index 86790ff4847..800a4b515fb 100644 --- a/Core/GameEngine/Include/Common/GameUtility.h +++ b/Core/GameEngine/Include/Common/GameUtility.h @@ -36,4 +36,6 @@ PlayerIndex getObservedOrLocalPlayerIndex_Safe(); ///< Get the current observed void changeLocalPlayer(Player* player); //< Change local player during game. Must not pass null. void changeObservedPlayer(Player* player); ///< Change observed player during game. Can pass null: is identical to passing the "ReplayObserver" player. +void enableLetterBox(Bool enable); ///< Enable or disable the letter box for cinematics. Hides the control bar. + } // namespace rts diff --git a/Core/GameEngine/Source/Common/GameUtility.cpp b/Core/GameEngine/Source/Common/GameUtility.cpp index cffb87f4842..9dfe64821ca 100644 --- a/Core/GameEngine/Source/Common/GameUtility.cpp +++ b/Core/GameEngine/Source/Common/GameUtility.cpp @@ -24,7 +24,9 @@ #include "Common/Radar.h" #include "GameClient/ControlBar.h" +#include "GameClient/Display.h" #include "GameClient/GameClient.h" +#include "GameClient/GUICallbacks.h" #include "GameClient/InGameUI.h" #include "GameClient/ParticleSys.h" @@ -130,4 +132,18 @@ void changeObservedPlayer(Player* player) } } +void enableLetterBox(Bool enable) +{ + if (enable) + { + HideControlBar(TRUE); + TheDisplay->enableLetterBox(TRUE); + } + else + { + ShowControlBar(FALSE); + TheDisplay->enableLetterBox(FALSE); + } +} + } // namespace rts diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Locomotor.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Locomotor.h index 3649866a957..4382c3811de 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Locomotor.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Locomotor.h @@ -139,10 +139,21 @@ class LocomotorTemplate : public Overridable void validate(); -protected: +private: + /// TheSuperHackers @bugfix Speeds authored in INI are understated by the forward speed the Locomotor measures + /// itself with, by up to 1/sqrt(2) in 2d and 1/sqrt(3) in 3d, which is what made objects move faster on diagonal + /// headings than on axis aligned ones. Each authored speed therefore also gets a "scaled" twin, in world distance + /// per logic frame, computed once at INI load. These accessors hand out whichever of the two the mover should be + /// commanded with. They all return the authored value in retail compatible builds. + Real getActualMaxSpeed() const; + Real getActualMaxSpeedDamaged() const; + Real getActualMinSpeed() const; + Real getActualMinTurnSpeed() const; + + /// Scale a speed that has no stored counterpart because it was not authored on this template. + Real scaleSpeed(Real speed) const; -private: /** Units check: @@ -163,6 +174,12 @@ class LocomotorTemplate : public Overridable Real m_liftDamaged; ///< max lift when damaged Real m_braking; ///< max braking (deceleration) Real m_minTurnSpeed; ///< we must be going >= this speed in order to turn +#if USE_RETAIL_PHYSICS_FORWARD_SPEED_AVERAGE() + Real m_maxSpeedScaled; ///< compensated max speed + Real m_maxSpeedDamagedScaled;///< compensated speed when "damaged" + Real m_minSpeedScaled; ///< compensated min speed; we should never brake past this + Real m_minTurnSpeedScaled; ///< compensated min turn speed; we must be going >= this speed in order to turn +#endif Real m_preferredHeight; ///< our preferred height (if flying) Real m_preferredHeightDamping; ///< how aggressively to adjust to preferred height: 1.0 = very much so, 0.1 = gradually, etc Real m_circlingRadius; ///< for flying things, the radius at which they circle their "maintain" destination. (pos = cw, neg = ccw, 0 = smallest possible) @@ -258,7 +275,8 @@ class Locomotor : public MemoryPoolObject, public Snapshot LocomotorSurfaceTypeMask getLegalSurfaces() const { return m_template->m_surfaces; } AsciiString getTemplateName() const { return m_template->m_name;} - Real getMinSpeed() const { return m_template->m_minSpeed;} + Real getMinSpeed() const; + Real getMinTurnSpeed() const; Real getAccelPitchLimit() const { return m_template->m_accelPitchLimit;} ///< Maximum amount we will pitch up or down under acceleration (including recoil.) Real getDecelPitchLimit() const { return m_template->m_decelPitchLimit;} ///< Maximum amount we will pitch down under deceleration (including recoil.) Real getBounceKick() const { return m_template->m_bounceKick;} ///< How much simulating rough terrain "bounces" a wheel up. @@ -310,7 +328,11 @@ class Locomotor : public MemoryPoolObject, public Snapshot { DEBUG_ASSERTCRASH(!(speed <= 0.0f && m_template->m_appearance == LOCO_THRUST), ("THRUST locos may not have zero speeds!")); m_maxSpeed = speed; +#if USE_RETAIL_PHYSICS_FORWARD_SPEED_AVERAGE() + m_maxSpeedScaled = m_template->scaleSpeed(speed); +#endif } + void setMaxSpeedToMinSpeed() { setMaxSpeed(m_template->m_minSpeed); } void setMaxAcceleration(Real accel) { m_maxAccel = accel; } void setMaxBraking(Real braking) { m_maxBraking = braking; } void setMaxTurnRate(Real turn) { m_maxTurnRate = turn; } @@ -367,8 +389,9 @@ class Locomotor : public MemoryPoolObject, public Snapshot void startMove(); ///< Indicates that a move is starting, primarily to reset the donut timer. jba. protected: + Real getMaxSpeedOverride() const; + void moveTowardsPositionLegs(Object* obj, PhysicsBehavior *physics, const Coord3D& goalPos, Real onPathDistToGoal, Real desiredSpeed); - void moveTowardsPositionLegsWander(Object* obj, PhysicsBehavior *physics, const Coord3D& goalPos, Real onPathDistToGoal, Real desiredSpeed); void moveTowardsPositionClimb(Object* obj, PhysicsBehavior *physics, const Coord3D& goalPos, Real onPathDistToGoal, Real desiredSpeed); void moveTowardsPositionWheels(Object* obj, PhysicsBehavior *physics, const Coord3D& goalPos, Real onPathDistToGoal, Real desiredSpeed); void moveTowardsPositionTreads(Object* obj, PhysicsBehavior *physics, const Coord3D& goalPos, Real onPathDistToGoal, Real desiredSpeed); @@ -445,6 +468,9 @@ class Locomotor : public MemoryPoolObject, public Snapshot Real m_brakingFactor; Real m_maxLift; Real m_maxSpeed; +#if USE_RETAIL_PHYSICS_FORWARD_SPEED_AVERAGE() + Real m_maxSpeedScaled; +#endif Real m_maxAccel; Real m_maxBraking; Real m_maxTurnRate; diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/PhysicsUpdate.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/PhysicsUpdate.h index 75a41c7da42..c706a640ae9 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/PhysicsUpdate.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/PhysicsUpdate.h @@ -141,6 +141,10 @@ class PhysicsBehavior : public UpdateModule, Real getForwardSpeed2D() const; ///< compute speed along object's 2d direction vector Real getForwardSpeed3D() const; ///< compute speed along object's 3d direction vector +#if USE_RETAIL_PHYSICS_FORWARD_SPEED_DISCREPANCY_IN_CINEMATICS() + static Bool useLegacyForwardSpeed(); +#endif + ObjectID getCurrentOverlap() const; ///< return object(s) being overlapped ObjectID getPreviousOverlap() const; ///< return object(s) that were overlapped last frame ObjectID getLastCollidee() const; ///< return object that was last collided with... can be quite old diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/ScriptEngine.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/ScriptEngine.h index 3ed3d9fc00c..88671d3ca54 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/ScriptEngine.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/ScriptEngine.h @@ -310,6 +310,9 @@ class ScriptEngine : public SubsystemInterface, void doFreezeTime(); void doUnfreezeTime(); + void friend_notifyLetterBoxActive(Bool active); + Bool isLetterBoxActive() const; ///< Ask whether the letterbox has been activated by a script; indicates an active cinematic + /// The following functions are used to update and query the debug window Bool isTimeFrozenDebug(); ///< Ask whether the debug window has requested a pause. Bool isTimeFast(); ///< Ask whether the debug window has requested a fast forward. @@ -433,6 +436,7 @@ class ScriptEngine : public SubsystemInterface, Int m_numAttackInfo; Int m_endGameTimer; Int m_closeWindowTimer; + Bool m_letterBoxActive; ///< A scripted letterbox sequence is running Team *m_callingTeam; ///< Team that is calling script, used for THIS_TEAM Object *m_callingObject; ///< Object that is calling script, used for THIS_OBJECT Team *m_conditionTeam; ///< Team that is being used to evaluate conditions, used for THIS_TEAM diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Locomotor.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Locomotor.cpp index 205c1a99457..fba9d09ecff 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Locomotor.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Locomotor.cpp @@ -77,6 +77,47 @@ static_assert(ARRAY_SIZE(TheLocomotorPriorityNames) == LOCOMOTOR_PRIORITY_COUNT // PRIVATE FUNCTIONS ////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////// +#if USE_RETAIL_PHYSICS_FORWARD_SPEED_AVERAGE() + +// TheSuperHackers @bugfix xezon 30/07/2026 The compensation that equalizes straight and diagonal +// movement speeds by the arithmetic mean. +// +// Retail measured forward speed as sqrt(sum of (vi * di)^2) rather than the true projection of the +// velocity onto the heading, sum of (vi * di). For a unit heading d that understates the speed by +// sqrt(sum of di^4), which is 1 on an axis aligned heading and falls to 1/sqrt(2) on a 2d diagonal +// and 1/sqrt(3) on a 3d body diagonal. The Locomotor therefore kept accelerating until the real +// speed was between 1x and sqrt(2)x (2d), or between 1x and sqrt(3)x (3d), the authored speed, +// decided by nothing but which way the object happened to face. +// +// getForwardSpeed2D and getForwardSpeed3D now report the true projection, so the real speed equals +// the commanded speed on every heading. The speeds commanded by the Locomotor are scaled by a single +// constant per dimension, which picks where inside the old range that now uniform speed sits. +// +// Each constant is the mean of the old factor over the headings the movers actually take, so the +// average movement speed of the game is preserved and only the spread between headings collapses. +// It is deliberately not the midpoint of the old range. The factor is weighted heavily toward its +// low end, spending far more of the circle near 1x than near sqrt(2)x, so the midpoint sits above +// the mean and would quietly speed the whole game up. +// +// 2d movers take an arbitrary heading, so the mean is taken over a uniformly random angle. It has a +// closed form as a complete elliptic integral of the first kind, and equals 1.18034060. +// +// 3d movers are THRUST only, which in practice means missiles. Those fly level most of the time and +// use all three axes only while arcing, so the mean is taken over a mix assumed to be 80% level +// flight and 20% an arbitrary 3d direction. Level flight has dz = 0, which makes the 3d factor +// degenerate to the 2d one exactly, so the level part contributes the 2d constant unchanged. The +// mean over a uniformly random direction on the sphere is 1.33122576. The result barely depends on +// the assumed split: anything from 90/10 to 70/30 lands between 1.195 and 1.226, and even spending +// the whole 20% at the worst possible heading would only reach 1.291. + +constexpr const Real DiagonalCompensation2D = 1.18034060f; // (2/pi)*K(1/2) = Gamma(1/4)^2 / (2*pi^(3/2)) +constexpr const Real DiagonalCompensation3D = 1.21051763f; // 0.8 * 1.18034060 + 0.2 * 1.33122576 + +static Real scaleSpeed2D(Real iniSpeed) { return iniSpeed * DiagonalCompensation2D; } +static Real scaleSpeed3D(Real iniSpeed) { return iniSpeed * DiagonalCompensation3D; } + +#endif + //------------------------------------------------------------------------------------------------- static Real calcSlowDownDist(Real curSpeed, Real desiredSpeed, Real maxBraking) { @@ -289,6 +330,12 @@ LocomotorTemplate::LocomotorTemplate() m_braking = BIGNUM; m_minSpeed = 0.0f; m_minTurnSpeed = BIGNUM; +#if USE_RETAIL_PHYSICS_FORWARD_SPEED_AVERAGE() + m_maxSpeedScaled = 0.0f; + m_maxSpeedDamagedScaled = 0.0f; + m_minSpeedScaled = 0.0f; + m_minTurnSpeedScaled = BIGNUM; +#endif m_behaviorZ = Z_NO_Z_MOTIVE_FORCE; m_appearance = LOCO_OTHER; m_movePriority = LOCO_MOVES_MIDDLE; @@ -429,6 +476,105 @@ void LocomotorTemplate::validate() if (m_decelPitchLimit == 0.0f) m_decelPitchLimit = m_accelPitchLimit; #endif + +#if USE_RETAIL_PHYSICS_FORWARD_SPEED_AVERAGE() + // TheSuperHackers @info THRUST is the only appearance whose mover measures itself with getForwardSpeed3D, + // so the dimension is decided here, once, rather than every time a speed is read. This runs last so that + // the twins are computed from the healed and defaulted values above, and it is safe to run again on an + // INI override because each twin is assigned from its untouched source rather than multiplied in place. + if (m_appearance == LOCO_THRUST) + { + m_maxSpeedScaled = scaleSpeed3D(m_maxSpeed); + m_maxSpeedDamagedScaled = scaleSpeed3D(m_maxSpeedDamaged); + m_minSpeedScaled = scaleSpeed3D(m_minSpeed); + m_minTurnSpeedScaled = scaleSpeed3D(m_minTurnSpeed); + } + else + { + m_maxSpeedScaled = scaleSpeed2D(m_maxSpeed); + m_maxSpeedDamagedScaled = scaleSpeed2D(m_maxSpeedDamaged); + m_minSpeedScaled = scaleSpeed2D(m_minSpeed); + m_minTurnSpeedScaled = scaleSpeed2D(m_minTurnSpeed); + } +#endif +} + +//------------------------------------------------------------------------------------------------- +Real LocomotorTemplate::getActualMaxSpeed() const +{ +#if USE_RETAIL_PHYSICS_FORWARD_SPEED_DISCREPANCY() || !USE_RETAIL_PHYSICS_FORWARD_SPEED_AVERAGE() + return m_maxSpeed; +#else + +#if USE_RETAIL_PHYSICS_FORWARD_SPEED_DISCREPANCY_IN_CINEMATICS() + if (PhysicsBehavior::useLegacyForwardSpeed()) + return m_maxSpeed; +#endif + + return m_maxSpeedScaled; +#endif +} + +//------------------------------------------------------------------------------------------------- +Real LocomotorTemplate::getActualMaxSpeedDamaged() const +{ +#if USE_RETAIL_PHYSICS_FORWARD_SPEED_DISCREPANCY() || !USE_RETAIL_PHYSICS_FORWARD_SPEED_AVERAGE() + return m_maxSpeedDamaged; +#else + +#if USE_RETAIL_PHYSICS_FORWARD_SPEED_DISCREPANCY_IN_CINEMATICS() + if (PhysicsBehavior::useLegacyForwardSpeed()) + return m_maxSpeedDamaged; +#endif + + return m_maxSpeedDamagedScaled; +#endif +} + +//------------------------------------------------------------------------------------------------- +Real LocomotorTemplate::getActualMinSpeed() const +{ +#if USE_RETAIL_PHYSICS_FORWARD_SPEED_DISCREPANCY() || !USE_RETAIL_PHYSICS_FORWARD_SPEED_AVERAGE() + return m_minSpeed; +#else + +#if USE_RETAIL_PHYSICS_FORWARD_SPEED_DISCREPANCY_IN_CINEMATICS() + if (PhysicsBehavior::useLegacyForwardSpeed()) + return m_minSpeed; +#endif + + return m_minSpeedScaled; +#endif +} + +//------------------------------------------------------------------------------------------------- +Real LocomotorTemplate::getActualMinTurnSpeed() const +{ +#if USE_RETAIL_PHYSICS_FORWARD_SPEED_DISCREPANCY() || !USE_RETAIL_PHYSICS_FORWARD_SPEED_AVERAGE() + return m_minTurnSpeed; +#else + +#if USE_RETAIL_PHYSICS_FORWARD_SPEED_DISCREPANCY_IN_CINEMATICS() + if (PhysicsBehavior::useLegacyForwardSpeed()) + return m_minTurnSpeed; +#endif + + return m_minTurnSpeedScaled; +#endif +} + +//------------------------------------------------------------------------------------------------- +Real LocomotorTemplate::scaleSpeed(Real speed) const +{ +#if USE_RETAIL_PHYSICS_FORWARD_SPEED_DISCREPANCY() || !USE_RETAIL_PHYSICS_FORWARD_SPEED_AVERAGE() + return speed; +#else + if (m_appearance == LOCO_THRUST) + { + return scaleSpeed3D(speed); + } + return scaleSpeed2D(speed); +#endif } //------------------------------------------------------------------------------------------------- @@ -658,6 +804,9 @@ Locomotor::Locomotor(const LocomotorTemplate* tmpl) m_brakingFactor = 1.0f; m_maxLift = BIGNUM; m_maxSpeed = BIGNUM; +#if USE_RETAIL_PHYSICS_FORWARD_SPEED_AVERAGE() + m_maxSpeedScaled = m_template->scaleSpeed(BIGNUM); +#endif m_maxAccel = BIGNUM; m_maxBraking = BIGNUM; m_maxTurnRate = BIGNUM; @@ -685,6 +834,9 @@ Locomotor::Locomotor(const Locomotor& that) m_brakingFactor = that.m_brakingFactor; m_maxLift = that.m_maxLift; m_maxSpeed = that.m_maxSpeed; +#if USE_RETAIL_PHYSICS_FORWARD_SPEED_AVERAGE() + m_maxSpeedScaled = that.m_maxSpeedScaled; +#endif m_maxAccel = that.m_maxAccel; m_maxBraking = that.m_maxBraking; m_maxTurnRate = that.m_maxTurnRate; @@ -708,6 +860,9 @@ Locomotor& Locomotor::operator=(const Locomotor& that) m_brakingFactor = that.m_brakingFactor; m_maxLift = that.m_maxLift; m_maxSpeed = that.m_maxSpeed; +#if USE_RETAIL_PHYSICS_FORWARD_SPEED_AVERAGE() + m_maxSpeedScaled = that.m_maxSpeedScaled; +#endif m_maxAccel = that.m_maxAccel; m_maxBraking = that.m_maxBraking; m_maxTurnRate = that.m_maxTurnRate; @@ -755,6 +910,10 @@ void Locomotor::xfer( Xfer *xfer ) xfer->xferReal(&m_brakingFactor); xfer->xferReal(&m_maxLift); xfer->xferReal(&m_maxSpeed); +#if USE_RETAIL_PHYSICS_FORWARD_SPEED_AVERAGE() + // TheSuperHackers @info The scaled twin is derived, so recompute it rather than transfer it. + m_maxSpeedScaled = m_template->scaleSpeed(m_maxSpeed); +#endif xfer->xferReal(&m_maxAccel); xfer->xferReal(&m_maxBraking); xfer->xferReal(&m_maxTurnRate); @@ -791,16 +950,45 @@ Real Locomotor::getMaxSpeedForCondition(BodyDamageType condition) const Real speed; if( IS_CONDITION_BETTER( condition, TheGlobalData->m_movementPenaltyDamageState ) ) - speed = m_template->m_maxSpeed; + speed = m_template->getActualMaxSpeed(); else - speed = m_template->m_maxSpeedDamaged; + speed = m_template->getActualMaxSpeedDamaged(); - if (speed > m_maxSpeed) - speed = m_maxSpeed; + Real maxSpeed = getMaxSpeedOverride(); + if (speed > maxSpeed) + speed = maxSpeed; return speed; } +//------------------------------------------------------------------------------------------------- +Real Locomotor::getMaxSpeedOverride() const +{ +#if USE_RETAIL_PHYSICS_FORWARD_SPEED_DISCREPANCY() || !USE_RETAIL_PHYSICS_FORWARD_SPEED_AVERAGE() + return m_maxSpeed; +#else + +#if USE_RETAIL_PHYSICS_FORWARD_SPEED_DISCREPANCY_IN_CINEMATICS() + if (PhysicsBehavior::useLegacyForwardSpeed()) + return m_maxSpeed; +#endif + + return m_maxSpeedScaled; +#endif +} + +//------------------------------------------------------------------------------------------------- +Real Locomotor::getMinSpeed() const +{ + return m_template->getActualMinSpeed(); +} + +//------------------------------------------------------------------------------------------------- +Real Locomotor::getMinTurnSpeed() const +{ + return m_template->getActualMinTurnSpeed(); +} + //------------------------------------------------------------------------------------------------- Real Locomotor::getMaxTurnRate(BodyDamageType condition) const { @@ -1291,7 +1479,7 @@ void Locomotor::moveTowardsPositionWheels(Object* obj, PhysicsBehavior *physics, // // See if we are turning. If so, use the min turn speed. // - Real turnSpeed = m_template->m_minTurnSpeed; + Real turnSpeed = getMinTurnSpeed(); Real angle = obj->getOrientation(); // Real relAngle = ThePartitionManager->getRelativeAngle2D( obj, &goalPos ); // Real desiredAngle = angle + relAngle; @@ -1667,10 +1855,10 @@ void Locomotor::moveTowardsPositionLegs(Object* obj, PhysicsBehavior *physics, c Real goalSpeed = (1.0f - angleCoeff) * desiredSpeed; //Real slowDownDist = (actualSpeed - m_template->m_minSpeed) / getBraking(); - Real slowDownDist = calcSlowDownDist(actualSpeed, m_template->m_minSpeed, getBraking()); + Real slowDownDist = calcSlowDownDist(actualSpeed, getMinSpeed(), getBraking()); if (onPathDistToGoal < slowDownDist && !getFlag(NO_SLOW_DOWN_AS_APPROACHING_DEST)) { - goalSpeed = m_template->m_minSpeed; + goalSpeed = getMinSpeed(); } @@ -1794,10 +1982,10 @@ void Locomotor::moveTowardsPositionClimb(Object* obj, PhysicsBehavior *physics, } //Real slowDownDist = (actualSpeed - m_template->m_minSpeed) / getBraking(); - Real slowDownDist = calcSlowDownDist(actualSpeed, m_template->m_minSpeed, getBraking()); + Real slowDownDist = calcSlowDownDist(actualSpeed, getMinSpeed(), getBraking()); if (onPathDistToGoal < slowDownDist && !getFlag(NO_SLOW_DOWN_AS_APPROACHING_DEST)) { - goalSpeed = m_template->m_minSpeed; + goalSpeed = getMinSpeed(); } // @@ -1914,15 +2102,15 @@ void Locomotor::moveTowardsPositionThrust(Object* obj, PhysicsBehavior *physics, BodyDamageType bdt = obj->getBodyModule()->getDamageState(); Real maxForwardSpeed = getMaxSpeedForCondition(bdt); - desiredSpeed = clamp(m_template->m_minSpeed, desiredSpeed, maxForwardSpeed); + desiredSpeed = clamp(getMinSpeed(), desiredSpeed, maxForwardSpeed); Real actualForwardSpeed = physics->getForwardSpeed3D(); if (getBraking() > 0) { //Real slowDownDist = (actualForwardSpeed - m_template->m_minSpeed) / getBraking(); - Real slowDownDist = calcSlowDownDist(actualForwardSpeed, m_template->m_minSpeed, getBraking()); + Real slowDownDist = calcSlowDownDist(actualForwardSpeed, getMinSpeed(), getBraking()); if (onPathDistToGoal < slowDownDist && !getFlag(NO_SLOW_DOWN_AS_APPROACHING_DEST)) - desiredSpeed = m_template->m_minSpeed; + desiredSpeed = getMinSpeed(); } Coord3D localGoalPos = goalPos; @@ -2388,10 +2576,10 @@ void Locomotor::moveTowardsPositionOther(Object* obj, PhysicsBehavior *physics, if (!getFlag(NO_SLOW_DOWN_AS_APPROACHING_DEST)) { - Real slowDownDist = calcSlowDownDist(actualSpeed, m_template->m_minSpeed, getBraking()); + Real slowDownDist = calcSlowDownDist(actualSpeed, getMinSpeed(), getBraking()); if (onPathDistToGoal < slowDownDist) { - goalSpeed = m_template->m_minSpeed; + goalSpeed = getMinSpeed(); } } @@ -2540,7 +2728,7 @@ void Locomotor::maintainCurrentPositionWings(Object* obj, PhysicsBehavior *physi Coord3D desiredPos = m_maintainPos; desiredPos.x += Cos(angleTowardMaintainPos) * turnRadius; desiredPos.y += Sin(angleTowardMaintainPos) * turnRadius; - moveTowardsPositionWings(obj, physics, desiredPos, 0, m_template->m_minSpeed); + moveTowardsPositionWings(obj, physics, desiredPos, 0, getMinSpeed()); } } @@ -2558,7 +2746,7 @@ void Locomotor::maintainCurrentPositionHover(Object* obj, PhysicsBehavior *physi // // Stop // - Real minSpeed = max( 1.0E-10f, m_template->m_minSpeed ); + Real minSpeed = max( 1.0E-10f, getMinSpeed() ); Real speedDelta = minSpeed - actualSpeed; if (fabs(speedDelta) > minSpeed) { diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/JetAIUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/JetAIUpdate.cpp index 998825c9648..ccf173e9288 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/JetAIUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/JetAIUpdate.cpp @@ -739,7 +739,7 @@ class JetTakeoffOrLandingState : public AIFollowPathState m_landingSoundPlayed = FALSE; if (m_landing) { - loco->setMaxSpeed(loco->getMinSpeed()); + loco->setMaxSpeedToMinSpeed(); } else { diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/PhysicsUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/PhysicsUpdate.cpp index 3ee2c96c1ff..8f7db86ce39 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/PhysicsUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/PhysicsUpdate.cpp @@ -954,21 +954,37 @@ Real PhysicsBehavior::getVelocityMagnitude() const Real PhysicsBehavior::getForwardSpeed2D() const { const Coord3D *dir = getObject()->getUnitDirectionVector2D(); - Real vx = m_vel.x * dir->x; Real vy = m_vel.y * dir->y; - Real dot = vx + vy; - Real speedSquared = vx*vx + vy*vy; -// DEBUG_ASSERTCRASH( speedSquared != 0, ("zero speedSquared will overflow sqrtf()!") );// lorenzen... sanity check - - Real speed = (Real)sqrtf( speedSquared ); +#if USE_RETAIL_PHYSICS_FORWARD_SPEED_DISCREPANCY() + Real speed = (Real)sqrtf( vx*vx + vy*vy ); if (dot >= 0.0f) return speed; - return -speed; + +#else + +#if USE_RETAIL_PHYSICS_FORWARD_SPEED_DISCREPANCY_IN_CINEMATICS() + if (useLegacyForwardSpeed()) + { + Real speed = (Real)sqrtf( vx*vx + vy*vy ); + if (dot >= 0.0f) + return speed; + return -speed; + } +#endif + + // TheSuperHackers @bugfix xezon 30/07/2026 Now returns the dot product instead of +-sqrtf(vx*vx+vy*vy). + // The retail formula understates the forward speed by up to 1/sqrt(2) on diagonal headings, which made + // the Locomotor overshoot its goal speed there. The dot product is the true projection of the velocity + // onto the facing vector, so this now reports real distance per logic frame and can be used for distance + // and time calculations. The speeds the Locomotor commands are compensated to match, in LocomotorTemplate. + return dot; + +#endif } //------------------------------------------------------------------------------------------------- @@ -979,20 +995,46 @@ Real PhysicsBehavior::getForwardSpeed2D() const Real PhysicsBehavior::getForwardSpeed3D() const { Vector3 dir = getObject()->getTransformMatrix()->Get_X_Vector(); - Real vx = m_vel.x * dir.X; Real vy = m_vel.y * dir.Y; Real vz = m_vel.z * dir.Z; - Real dot = vx + vy + vz; - Real speed = (Real)sqrtf( vx*vx + vy*vy + vz*vz ); +#if USE_RETAIL_PHYSICS_FORWARD_SPEED_DISCREPANCY() + Real speed = (Real)sqrtf( vx*vx + vy*vy + vz*vz ); if (dot >= 0.0f) return speed; - return -speed; + +#else + +#if USE_RETAIL_PHYSICS_FORWARD_SPEED_DISCREPANCY_IN_CINEMATICS() + if (useLegacyForwardSpeed()) + { + Real speed = (Real)sqrtf( vx*vx + vy*vy + vz*vz ); + if (dot >= 0.0f) + return speed; + return -speed; + } +#endif + + // TheSuperHackers @bugfix xezon 30/07/2026 Now returns the dot product instead of + // +-sqrtf(vx*vx+vy*vy+vz*vz). See getForwardSpeed2D for the rationale. + return dot; + +#endif +} + +#if USE_RETAIL_PHYSICS_FORWARD_SPEED_DISCREPANCY_IN_CINEMATICS() +//------------------------------------------------------------------------------------------------- +Bool PhysicsBehavior::useLegacyForwardSpeed() +{ + // TheSuperHackers @info The retail speeds are kept for the duration of a cinematic, because + // legacy missions time their cinematics against them. + return TheScriptEngine->isLetterBoxActive(); } +#endif //------------------------------------------------------------------------------------------------- Bool PhysicsBehavior::isCurrentlyOverlapped(Object *obj) const diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptActions.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptActions.cpp index 023533d1b4a..d431ad9d446 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptActions.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptActions.cpp @@ -33,6 +33,7 @@ #include "Common/AudioHandleSpecialValues.h" #include "Common/FramePacer.h" #include "Common/GameAudio.h" +#include "Common/GameUtility.h" #include "Common/MapObject.h" // For MAP_XY_FACTOR #include "Common/PartitionSolver.h" #include "Common/Player.h" @@ -3834,16 +3835,8 @@ void ScriptActions::doPlayerExitAllBuildings(const AsciiString& playerName) //------------------------------------------------------------------------------------------------- void ScriptActions::doLetterBoxMode(Bool startLetterbox) { - if (startLetterbox) - { - HideControlBar(TRUE); - TheDisplay->enableLetterBox(TRUE); - } - else - { - ShowControlBar(FALSE); - TheDisplay->enableLetterBox(FALSE); - } + TheScriptEngine->friend_notifyLetterBoxActive(startLetterbox); + rts::enableLetterBox(startLetterbox); } //------------------------------------------------------------------------------------------------- diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptEngine.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptEngine.cpp index d10dff653a1..f9b22fc2c3a 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptEngine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptEngine.cpp @@ -33,6 +33,7 @@ #include "Common/FileSystem.h" #include "Common/FramePacer.h" #include "Common/GameState.h" +#include "Common/GameUtility.h" #include "Common/LatchRestore.h" #include "Common/MessageStream.h" #include "Common/PerfTimer.h" @@ -446,6 +447,7 @@ m_fade(FADE_NONE), m_freezeByScript(FALSE), m_frameObjectCountChanged(0), m_closeWindowTimer(0), +m_letterBoxActive(FALSE), m_curFadeFrame(0), m_curFadeValue(0.0f), m_endGameTimer(0), @@ -5276,6 +5278,7 @@ void ScriptEngine::reset() m_numFlags = 1; m_endGameTimer = -1; m_closeWindowTimer = -1; + m_letterBoxActive = FALSE; m_callingTeam = nullptr; m_callingObject = nullptr; @@ -8429,6 +8432,22 @@ void ScriptEngine::doUnfreezeTime() m_freezeByScript = FALSE; } +//------------------------------------------------------------------------------------------------- +/** Report the scripted letterbox opening or closing. */ +//------------------------------------------------------------------------------------------------- +void ScriptEngine::friend_notifyLetterBoxActive(Bool active) +{ + m_letterBoxActive = active; +} + +//------------------------------------------------------------------------------------------------- +/** Is the letterbox active right now? */ +//------------------------------------------------------------------------------------------------- +Bool ScriptEngine::isLetterBoxActive() const +{ + return m_letterBoxActive; +} + //------------------------------------------------------------------------------------------------- /** For Debug and Internal builds, returns whether to continue (!pause), for release, returns false */ //------------------------------------------------------------------------------------------------- @@ -8837,6 +8856,7 @@ void ScriptEngine::setGlobalDifficulty( GameDifficulty difficulty ) * 3: Added m_objectsShouldReceiveDifficultyBonus (JKMCD) * 4: current music track info * 5: add ChooseVictimAlwaysUsesNormal + * 6: TheSuperHackers @tweak Adds xfer for m_letterBoxActive */ // ------------------------------------------------------------------------------------------------ void ScriptEngine::xfer( Xfer *xfer ) @@ -8844,7 +8864,11 @@ void ScriptEngine::xfer( Xfer *xfer ) Int i; // version +#if RETAIL_COMPATIBLE_XFER_SAVE const XferVersion currentVersion = 5; +#else + const XferVersion currentVersion = 6; +#endif XferVersion version = currentVersion; xfer->xferVersion( &version, currentVersion ); @@ -9303,6 +9327,18 @@ void ScriptEngine::xfer( Xfer *xfer ) m_ChooseVictimAlwaysUsesNormal = false; } + if (version >= 6) + { + xfer->xferBool(&m_letterBoxActive); + } + else + { + if (xfer->getXferMode() == XFER_LOAD) + { + m_letterBoxActive = FALSE; + } + } + if( xfer->getXferMode() == XFER_LOAD ) { // We are doing a load. If there is no fade active, do a black fade in to start. if (m_fade == FADE_NONE) { @@ -9335,6 +9371,11 @@ void ScriptEngine::loadPostProcess() TheAudio->addAudioEvent(&event); } + if (m_letterBoxActive) + { + rts::enableLetterBox(TRUE); + } + } //#if defined(RTS_DEBUG)