From 9a402fbe23c5a9196f48c720ef5a1d661a385f87 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Mon, 25 May 2026 12:50:55 +0200 Subject: [PATCH 1/4] fix(particlesys): Decouple particle systems from logic crc (#2742) --- Core/GameEngine/Include/Common/RandomValue.h | 20 ++++++++++++++++++ Core/GameEngine/Source/Common/RandomValue.cpp | 21 +++++++++++++++++++ .../Code/GameEngine/Include/Common/Geometry.h | 6 +++--- .../Source/Common/System/Geometry.cpp | 14 ++++++------- .../Object/Damage/TransitionDamageFX.cpp | 11 +++++++--- .../GameLogic/Object/Update/EMPUpdate.cpp | 16 +++++++++++--- .../Object/Update/SpecialAbilityUpdate.cpp | 11 +++++++++- .../Code/GameEngine/Include/Common/Geometry.h | 6 +++--- .../Source/Common/System/Geometry.cpp | 14 ++++++------- .../Object/Damage/TransitionDamageFX.cpp | 11 +++++++--- .../GameLogic/Object/Update/EMPUpdate.cpp | 16 +++++++++++--- .../Object/Update/SpecialAbilityUpdate.cpp | 11 +++++++++- 12 files changed, 123 insertions(+), 34 deletions(-) diff --git a/Core/GameEngine/Include/Common/RandomValue.h b/Core/GameEngine/Include/Common/RandomValue.h index 8cc97d5d844..572a3b713e5 100644 --- a/Core/GameEngine/Include/Common/RandomValue.h +++ b/Core/GameEngine/Include/Common/RandomValue.h @@ -35,4 +35,24 @@ extern void InitRandom( UnsignedInt seed ); extern UnsignedInt GetGameLogicRandomSeed(); ///< Get the seed (used for replays) extern UnsignedInt GetGameLogicRandomSeedCRC();///< Get the seed (used for CRCs) +struct RandomValueClass +{ + virtual Int GetRandomValueInt( Int lo, Int hi, const char *file, Int line ) const = 0; + virtual Real GetRandomValueReal( Real lo, Real hi, const char *file, Int line ) const = 0; +}; +struct LogicRandomValueClass final : RandomValueClass +{ + virtual Int GetRandomValueInt( Int lo, Int hi, const char *file, Int line ) const override; + virtual Real GetRandomValueReal( Real lo, Real hi, const char *file, Int line ) const override; +}; +struct ClientRandomValueClass final : RandomValueClass +{ + virtual Int GetRandomValueInt( Int lo, Int hi, const char *file, Int line ) const override; + virtual Real GetRandomValueReal( Real lo, Real hi, const char *file, Int line ) const override; +}; + +// use these macros to access the random value functions +#define RandomValueInt(randomValueClass, lo, hi) randomValueClass.GetRandomValueInt( lo, hi, __FILE__, __LINE__ ) +#define RandomValueReal(randomValueClass, lo, hi) randomValueClass.GetRandomValueReal( lo, hi, __FILE__, __LINE__ ) + //-------------------------------------------------------------------------------------------------------------- diff --git a/Core/GameEngine/Source/Common/RandomValue.cpp b/Core/GameEngine/Source/Common/RandomValue.cpp index d5317a52852..6e52d4f8f91 100644 --- a/Core/GameEngine/Source/Common/RandomValue.cpp +++ b/Core/GameEngine/Source/Common/RandomValue.cpp @@ -465,3 +465,24 @@ Real GameLogicRandomVariable::getValue() const return 0.0f; } } + + +Int LogicRandomValueClass::GetRandomValueInt( Int lo, Int hi, const char *file, Int line ) const +{ + return GetGameLogicRandomValue(lo, hi, file, line); +} + +Real LogicRandomValueClass::GetRandomValueReal( Real lo, Real hi, const char *file, Int line ) const +{ + return GetGameLogicRandomValueReal(lo, hi, file, line); +} + +Int ClientRandomValueClass::GetRandomValueInt( Int lo, Int hi, const char *file, Int line ) const +{ + return GetGameClientRandomValue(lo, hi, file, line); +} + +Real ClientRandomValueClass::GetRandomValueReal( Real lo, Real hi, const char *file, Int line ) const +{ + return GetGameClientRandomValueReal(lo, hi, file, line); +} diff --git a/Generals/Code/GameEngine/Include/Common/Geometry.h b/Generals/Code/GameEngine/Include/Common/Geometry.h index 88dcd9555d2..e743afa1672 100644 --- a/Generals/Code/GameEngine/Include/Common/Geometry.h +++ b/Generals/Code/GameEngine/Include/Common/Geometry.h @@ -31,6 +31,7 @@ #include "Lib/BaseType.h" #include "Common/AsciiString.h" +#include "Common/RandomValue.h" #include "Common/Snapshot.h" class INI; @@ -171,9 +172,8 @@ class GeometryInfo : public Snapshot /// get the 2d bounding box void get2DBounds(const Coord3D& geomCenter, Real angle, Region2D& bounds ) const; - /// note that the pt is generated using game logic random, not game client random! - void makeRandomOffsetWithinFootprint(Coord3D& pt) const; - void makeRandomOffsetOnPerimeter(Coord3D& pt) const; //Chooses a random point on the extent border. + void makeRandomOffsetWithinFootprint(Coord3D& pt, const RandomValueClass& random = LogicRandomValueClass()) const; + void makeRandomOffsetOnPerimeter(Coord3D& pt) const; ///< Chooses a random point on the extent border. Uses game logic random! void clipPointToFootprint(const Coord3D& geomCenter, Coord3D& ptToClip) const; diff --git a/Generals/Code/GameEngine/Source/Common/System/Geometry.cpp b/Generals/Code/GameEngine/Source/Common/System/Geometry.cpp index f33da731b42..2e4c35a2421 100644 --- a/Generals/Code/GameEngine/Source/Common/System/Geometry.cpp +++ b/Generals/Code/GameEngine/Source/Common/System/Geometry.cpp @@ -376,7 +376,7 @@ Bool GeometryInfo::isPointInFootprint(const Coord3D& geomCenter, const Coord3D& } //============================================================================= -void GeometryInfo::makeRandomOffsetWithinFootprint(Coord3D& pt) const +void GeometryInfo::makeRandomOffsetWithinFootprint(Coord3D& pt, const RandomValueClass& random) const { switch(m_type) { @@ -390,14 +390,14 @@ void GeometryInfo::makeRandomOffsetWithinFootprint(Coord3D& pt) const Real distSqr; do { - pt.x = GameLogicRandomValueReal(-m_majorRadius, m_majorRadius); - pt.y = GameLogicRandomValueReal(-m_majorRadius, m_majorRadius); + pt.x = RandomValueReal(random, -m_majorRadius, m_majorRadius); + pt.y = RandomValueReal(random, -m_majorRadius, m_majorRadius); pt.z = 0.0f; distSqr = sqr(pt.x) + sqr(pt.y); } while (distSqr > maxDistSqr); #else - Real radius = GameLogicRandomValueReal(0.0f, m_boundingCircleRadius); - Real angle = GameLogicRandomValueReal(-PI, PI); + Real radius = RandomValueReal(random, 0.0f, m_boundingCircleRadius); + Real angle = RandomValueReal(random, -PI, PI); pt.x = radius * Cos(angle); pt.y = radius * Sin(angle); pt.z = 0.0f; @@ -407,8 +407,8 @@ void GeometryInfo::makeRandomOffsetWithinFootprint(Coord3D& pt) const case GEOMETRY_BOX: { - pt.x = GameLogicRandomValueReal(-m_majorRadius, m_majorRadius); - pt.y = GameLogicRandomValueReal(-m_minorRadius, m_minorRadius); + pt.x = RandomValueReal(random, -m_majorRadius, m_majorRadius); + pt.y = RandomValueReal(random, -m_minorRadius, m_minorRadius); pt.z = 0.0f; break; } diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Damage/TransitionDamageFX.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Damage/TransitionDamageFX.cpp index a78a1b4ddd5..c9837afe55f 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Damage/TransitionDamageFX.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Damage/TransitionDamageFX.cpp @@ -253,7 +253,7 @@ void TransitionDamageFX::onDelete() /** Given an FXLoc info struct, return the effect position that we are supposed to use. * The position is local to to the object */ //------------------------------------------------------------------------------------------------- -static Coord3D getLocalEffectPos( const FXLocInfo *locInfo, Drawable *draw ) +static Coord3D getLocalEffectPos( const FXLocInfo *locInfo, Drawable *draw, const RandomValueClass &random = LogicRandomValueClass() ) { DEBUG_ASSERTCRASH( locInfo, ("getLocalEffectPos: locInfo is null") ); @@ -290,7 +290,7 @@ static Coord3D getLocalEffectPos( const FXLocInfo *locInfo, Drawable *draw ) return locInfo->loc; // pick one of the bone positions - Int pick = GameLogicRandomValue( 0, boneCount - 1 ); + Int pick = RandomValueInt( random, 0, boneCount - 1 ); return positions[ pick ]; } @@ -387,6 +387,11 @@ void TransitionDamageFX::onBodyDamageStateChange( const DamageInfo* damageInfo, if( lastDamageInfo == nullptr || getDamageTypeFlag( modData->m_damageParticleTypes, lastDamageInfo->in.m_damageType ) ) { +#if RETAIL_COMPATIBLE_CRC + // TheSuperHackers @fix The particle system is now decoupled from the logic crc + // and the side effects on the logic random seed values are preserved for retail compatibility. + getLocalEffectPos( &modData->m_particleSystem[ newState ][ i ].locInfo, draw, LogicRandomValueClass() ); +#endif // create a new particle system based on the template provided ParticleSystem* pSystem = TheParticleSystemManager->createParticleSystem( pSystemT ); @@ -394,7 +399,7 @@ void TransitionDamageFX::onBodyDamageStateChange( const DamageInfo* damageInfo, { // get the what is the position we're going to played the effect at - pos = getLocalEffectPos( &modData->m_particleSystem[ newState ][ i ].locInfo, draw ); + pos = getLocalEffectPos( &modData->m_particleSystem[ newState ][ i ].locInfo, draw, ClientRandomValueClass() ); // // set position on system given any bone position provided, the bone position is diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/EMPUpdate.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/EMPUpdate.cpp index e7bd1b54f78..ca7736e5c86 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/EMPUpdate.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/EMPUpdate.cpp @@ -235,14 +235,24 @@ void EMPUpdate::doDisableAttack() for (UnsignedInt e = 0 ; e < emitterCount; ++e) { +#if RETAIL_COMPATIBLE_CRC + // TheSuperHackers @fix The particle system is now decoupled from the logic crc + // and the side effects on the logic random seed values are preserved for retail compatibility. + { + Coord3D offs = {0,0,0}; + curVictim->getGeometryInfo().makeRandomOffsetWithinFootprint( offs, LogicRandomValueClass() ); + GameLogicRandomValue(3, victimHeight); + GameLogicRandomValue(1, 100); + } +#endif ParticleSystem *sys = TheParticleSystemManager->createParticleSystem(tmp); if (sys) { Coord3D offs = {0,0,0}; - curVictim->getGeometryInfo().makeRandomOffsetWithinFootprint( offs ); - offs.z = GameLogicRandomValue(3, victimHeight); + curVictim->getGeometryInfo().makeRandomOffsetWithinFootprint( offs, ClientRandomValueClass() ); + offs.z = GameClientRandomValue(3, victimHeight); //This puts all the sparks within a quadrahemicycloid (rectangular dome) volume //The same shape as a four cornered camping dome tent, for those with less Greek @@ -259,7 +269,7 @@ void EMPUpdate::doDisableAttack() sys->attachToObject(curVictim); sys->setPosition( &offs ); sys->setSystemLifetime(MAX(0, data->m_disabledDuration - 30)); - sys->setInitialDelay(GameLogicRandomValue(1,100)); + sys->setInitialDelay(GameClientRandomValue(1,100)); } } } diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/SpecialAbilityUpdate.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/SpecialAbilityUpdate.cpp index 453331703eb..95c568a029c 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/SpecialAbilityUpdate.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/SpecialAbilityUpdate.cpp @@ -1263,11 +1263,20 @@ void SpecialAbilityUpdate::triggerAbilityEffect() const ParticleSystemTemplate *tmp = data->m_disableFXParticleSystem; if (tmp) { +#if RETAIL_COMPATIBLE_CRC + // TheSuperHackers @fix The particle system is now decoupled from the logic crc + // and the side effects on the logic random seed values are preserved for retail compatibility. + { + Coord3D offs = {0,0,0}; + target->getGeometryInfo().makeRandomOffsetWithinFootprint( offs, LogicRandomValueClass() ); + } +#endif + ParticleSystem *sys = TheParticleSystemManager->createParticleSystem(tmp); if (sys) { Coord3D offs = {0,0,0}; - target->getGeometryInfo().makeRandomOffsetWithinFootprint( offs ); + target->getGeometryInfo().makeRandomOffsetWithinFootprint( offs, ClientRandomValueClass() ); sys->attachToObject(target); sys->setPosition( &offs ); diff --git a/GeneralsMD/Code/GameEngine/Include/Common/Geometry.h b/GeneralsMD/Code/GameEngine/Include/Common/Geometry.h index 221ca370ce0..907b1ae72cb 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/Geometry.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/Geometry.h @@ -31,6 +31,7 @@ #include "Lib/BaseType.h" #include "Common/AsciiString.h" +#include "Common/RandomValue.h" #include "Common/Snapshot.h" class INI; @@ -171,9 +172,8 @@ class GeometryInfo : public Snapshot /// get the 2d bounding box void get2DBounds(const Coord3D& geomCenter, Real angle, Region2D& bounds ) const; - /// note that the pt is generated using game logic random, not game client random! - void makeRandomOffsetWithinFootprint(Coord3D& pt) const; - void makeRandomOffsetOnPerimeter(Coord3D& pt) const; //Chooses a random point on the extent border. + void makeRandomOffsetWithinFootprint(Coord3D& pt, const RandomValueClass& random = LogicRandomValueClass()) const; + void makeRandomOffsetOnPerimeter(Coord3D& pt) const; ///< Chooses a random point on the extent border. Uses game logic random! void clipPointToFootprint(const Coord3D& geomCenter, Coord3D& ptToClip) const; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/System/Geometry.cpp b/GeneralsMD/Code/GameEngine/Source/Common/System/Geometry.cpp index b69c01de560..1f927ae2615 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/System/Geometry.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/System/Geometry.cpp @@ -376,7 +376,7 @@ Bool GeometryInfo::isPointInFootprint(const Coord3D& geomCenter, const Coord3D& } //============================================================================= -void GeometryInfo::makeRandomOffsetWithinFootprint(Coord3D& pt) const +void GeometryInfo::makeRandomOffsetWithinFootprint(Coord3D& pt, const RandomValueClass& random) const { switch(m_type) { @@ -390,14 +390,14 @@ void GeometryInfo::makeRandomOffsetWithinFootprint(Coord3D& pt) const Real distSqr; do { - pt.x = GameLogicRandomValueReal(-m_majorRadius, m_majorRadius); - pt.y = GameLogicRandomValueReal(-m_majorRadius, m_majorRadius); + pt.x = RandomValueReal(random, -m_majorRadius, m_majorRadius); + pt.y = RandomValueReal(random, -m_majorRadius, m_majorRadius); pt.z = 0.0f; distSqr = sqr(pt.x) + sqr(pt.y); } while (distSqr > maxDistSqr); #else - Real radius = GameLogicRandomValueReal(0.0f, m_boundingCircleRadius); - Real angle = GameLogicRandomValueReal(-PI, PI); + Real radius = RandomValueReal(random, 0.0f, m_boundingCircleRadius); + Real angle = RandomValueReal(random, -PI, PI); pt.x = radius * Cos(angle); pt.y = radius * Sin(angle); pt.z = 0.0f; @@ -407,8 +407,8 @@ void GeometryInfo::makeRandomOffsetWithinFootprint(Coord3D& pt) const case GEOMETRY_BOX: { - pt.x = GameLogicRandomValueReal(-m_majorRadius, m_majorRadius); - pt.y = GameLogicRandomValueReal(-m_minorRadius, m_minorRadius); + pt.x = RandomValueReal(random, -m_majorRadius, m_majorRadius); + pt.y = RandomValueReal(random, -m_minorRadius, m_minorRadius); pt.z = 0.0f; break; } diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Damage/TransitionDamageFX.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Damage/TransitionDamageFX.cpp index 9e201c1508f..ce57bd0a2f6 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Damage/TransitionDamageFX.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Damage/TransitionDamageFX.cpp @@ -253,7 +253,7 @@ void TransitionDamageFX::onDelete() /** Given an FXLoc info struct, return the effect position that we are supposed to use. * The position is local to to the object */ //------------------------------------------------------------------------------------------------- -static Coord3D getLocalEffectPos( const FXLocInfo *locInfo, Drawable *draw ) +static Coord3D getLocalEffectPos( const FXLocInfo *locInfo, Drawable *draw, const RandomValueClass &random = LogicRandomValueClass() ) { DEBUG_ASSERTCRASH( locInfo, ("getLocalEffectPos: locInfo is null") ); @@ -290,7 +290,7 @@ static Coord3D getLocalEffectPos( const FXLocInfo *locInfo, Drawable *draw ) return locInfo->loc; // pick one of the bone positions - Int pick = GameLogicRandomValue( 0, boneCount - 1 ); + Int pick = RandomValueInt( random, 0, boneCount - 1 ); return positions[ pick ]; } @@ -387,6 +387,11 @@ void TransitionDamageFX::onBodyDamageStateChange( const DamageInfo* damageInfo, if( lastDamageInfo == nullptr || getDamageTypeFlag( modData->m_damageParticleTypes, lastDamageInfo->in.m_damageType ) ) { +#if RETAIL_COMPATIBLE_CRC + // TheSuperHackers @fix The particle system is now decoupled from the logic crc + // and the side effects on the logic random seed values are preserved for retail compatibility. + getLocalEffectPos( &modData->m_particleSystem[ newState ][ i ].locInfo, draw, LogicRandomValueClass() ); +#endif // create a new particle system based on the template provided ParticleSystem* pSystem = TheParticleSystemManager->createParticleSystem( pSystemT ); @@ -394,7 +399,7 @@ void TransitionDamageFX::onBodyDamageStateChange( const DamageInfo* damageInfo, { // get the what is the position we're going to played the effect at - pos = getLocalEffectPos( &modData->m_particleSystem[ newState ][ i ].locInfo, draw ); + pos = getLocalEffectPos( &modData->m_particleSystem[ newState ][ i ].locInfo, draw, ClientRandomValueClass() ); // // set position on system given any bone position provided, the bone position is diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/EMPUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/EMPUpdate.cpp index aba51b4c2ee..d07e08795d1 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/EMPUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/EMPUpdate.cpp @@ -304,14 +304,24 @@ void EMPUpdate::doDisableAttack() for (UnsignedInt e = 0 ; e < emitterCount; ++e) { +#if RETAIL_COMPATIBLE_CRC + // TheSuperHackers @fix The particle system is now decoupled from the logic crc + // and the side effects on the logic random seed values are preserved for retail compatibility. + { + Coord3D offs = {0,0,0}; + curVictim->getGeometryInfo().makeRandomOffsetWithinFootprint( offs, LogicRandomValueClass() ); + GameLogicRandomValue(3, victimHeight); + GameLogicRandomValue(1, 100); + } +#endif ParticleSystem *sys = TheParticleSystemManager->createParticleSystem(tmp); if (sys) { Coord3D offs = {0,0,0}; - curVictim->getGeometryInfo().makeRandomOffsetWithinFootprint( offs ); - offs.z = GameLogicRandomValue(3, victimHeight); + curVictim->getGeometryInfo().makeRandomOffsetWithinFootprint( offs, ClientRandomValueClass() ); + offs.z = GameClientRandomValue(3, victimHeight); //This puts all the sparks within a quadrahemicycloid (rectangular dome) volume //The same shape as a four cornered camping dome tent, for those with less Greek @@ -328,7 +338,7 @@ void EMPUpdate::doDisableAttack() sys->attachToObject(curVictim); sys->setPosition( &offs ); sys->setSystemLifetime(MAX(0, data->m_disabledDuration - 30)); - sys->setInitialDelay(GameLogicRandomValue(1,100)); + sys->setInitialDelay(GameClientRandomValue(1,100)); } } } diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/SpecialAbilityUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/SpecialAbilityUpdate.cpp index fbbc907f31b..ba596b32152 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/SpecialAbilityUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/SpecialAbilityUpdate.cpp @@ -1400,11 +1400,20 @@ void SpecialAbilityUpdate::triggerAbilityEffect() const ParticleSystemTemplate *tmp = data->m_disableFXParticleSystem; if (tmp) { +#if RETAIL_COMPATIBLE_CRC + // TheSuperHackers @fix The particle system is now decoupled from the logic crc + // and the side effects on the logic random seed values are preserved for retail compatibility. + { + Coord3D offs = {0,0,0}; + target->getGeometryInfo().makeRandomOffsetWithinFootprint( offs, LogicRandomValueClass() ); + } +#endif + ParticleSystem *sys = TheParticleSystemManager->createParticleSystem(tmp); if (sys) { Coord3D offs = {0,0,0}; - target->getGeometryInfo().makeRandomOffsetWithinFootprint( offs ); + target->getGeometryInfo().makeRandomOffsetWithinFootprint( offs, ClientRandomValueClass() ); sys->attachToObject(target); sys->setPosition( &offs ); From 92df9775166087c72fc4a05dbcb09471380dbbb4 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Mon, 25 May 2026 18:11:47 +0200 Subject: [PATCH 2/4] fix(particlesys): Simplify ParticleSystemManagerDummy setup (#2740) --- .../Include/GameClient/ParticleSys.h | 18 ++++++++++++++++-- .../Source/Common/ReplaySimulation.cpp | 2 -- .../Drawable/Update/BeaconClientUpdate.cpp | 6 ++---- Core/GameEngine/Source/GameClient/FXList.cpp | 2 +- .../GameEngine/Include/GameClient/GameClient.h | 2 -- .../Source/GameClient/GameClient.cpp | 9 --------- .../GameEngine/Include/GameClient/GameClient.h | 2 -- .../Source/GameClient/GameClient.cpp | 9 --------- 8 files changed, 19 insertions(+), 31 deletions(-) diff --git a/Core/GameEngine/Include/GameClient/ParticleSys.h b/Core/GameEngine/Include/GameClient/ParticleSys.h index 5054cd2b8f8..e65d8773675 100644 --- a/Core/GameEngine/Include/GameClient/ParticleSys.h +++ b/Core/GameEngine/Include/GameClient/ParticleSys.h @@ -753,6 +753,8 @@ class ParticleSystemManager : public SubsystemInterface, virtual void reset() override; ///< reset the manager and all particle systems virtual void update() override; ///< update all particle systems + virtual Bool isDummy() const { return false; } + virtual Int getOnScreenParticleCount() = 0; ///< returns the number of particles on screen virtual void setOnScreenParticleCount(int count); @@ -761,8 +763,7 @@ class ParticleSystemManager : public SubsystemInterface, ParticleSystemTemplate *newTemplate( const AsciiString &name ); /// given a template, instantiate a particle system - ParticleSystem *createParticleSystem( const ParticleSystemTemplate *sysTemplate, - Bool createSlaves = TRUE ); + ParticleSystem *createParticleSystem( const ParticleSystemTemplate *sysTemplate, Bool createSlaves = TRUE ); /** given a template, instantiate a particle system. if attachTo is not null, attach the particle system to the given object. @@ -835,11 +836,24 @@ class ParticleSystemManager : public SubsystemInterface, ParticleSystemIDMap m_systemMap; ///< a hash map of all particle systems }; + // TheSuperHackers @feature bobtista 31/01/2026 // ParticleSystemManager that does nothing. Used for Headless Mode. +// Generally does not load particle system templates. Certainly does not create particle systems. class ParticleSystemManagerDummy : public ParticleSystemManager { public: +#if RETAIL_COMPATIBLE_CRC + // Must not overload init to keep loading the particle system templates, + // which are unfortunately needed to preserve the correct logic crc. +#else + virtual void init() override {} + virtual void reset() override {} +#endif + virtual void update() override {} + + virtual Bool isDummy() const override { return true; } + virtual Int getOnScreenParticleCount() override { return 0; } virtual void doParticles(RenderInfoClass &rinfo) override {} virtual void queueParticleRender() override {} diff --git a/Core/GameEngine/Source/Common/ReplaySimulation.cpp b/Core/GameEngine/Source/Common/ReplaySimulation.cpp index 7d18b5cb58f..e6871769a94 100644 --- a/Core/GameEngine/Source/Common/ReplaySimulation.cpp +++ b/Core/GameEngine/Source/Common/ReplaySimulation.cpp @@ -86,8 +86,6 @@ int ReplaySimulation::simulateReplaysInThisProcess(const std::vectorgetPlaybackFrameCount() / LOGICFRAMES_PER_SECOND; while (TheRecorder->isPlaybackInProgress()) { - TheGameClient->updateHeadless(); - const int progressFrameInterval = 10*60*LOGICFRAMES_PER_SECOND; if (TheGameLogic->getFrame() != 0 && TheGameLogic->getFrame() % progressFrameInterval == 0) { diff --git a/Core/GameEngine/Source/GameClient/Drawable/Update/BeaconClientUpdate.cpp b/Core/GameEngine/Source/GameClient/Drawable/Update/BeaconClientUpdate.cpp index 63071f8340c..77a594ab1df 100644 --- a/Core/GameEngine/Source/GameClient/Drawable/Update/BeaconClientUpdate.cpp +++ b/Core/GameEngine/Source/GameClient/Drawable/Update/BeaconClientUpdate.cpp @@ -94,9 +94,7 @@ static ParticleSystem* createParticleSystem( Drawable *draw ) AsciiString templateName; templateName.format("BeaconSmoke%6.6X", (0xffffff & obj->getIndicatorColor())); const ParticleSystemTemplate *particleTemplate = TheParticleSystemManager->findTemplate( templateName ); - - DEBUG_ASSERTCRASH(particleTemplate, ("Could not find particle system %s", templateName.str())); - + DEBUG_ASSERTCRASH(TheParticleSystemManager->isDummy() || particleTemplate, ("Could not find particle system %s", templateName.str())); if (particleTemplate) { system = TheParticleSystemManager->createParticleSystem( particleTemplate ); @@ -107,7 +105,7 @@ static ParticleSystem* createParticleSystem( Drawable *draw ) {// THis this will whip up a new particle system to match the house color provided templateName.format("BeaconSmokeFFFFFF"); const ParticleSystemTemplate *failsafeTemplate = TheParticleSystemManager->findTemplate( templateName ); - DEBUG_ASSERTCRASH(failsafeTemplate, ("Doh, this is bad \n I Could not even find the white particle system to make a failsafe system out of.")); + DEBUG_ASSERTCRASH(TheParticleSystemManager->isDummy() || failsafeTemplate, ("Doh, this is bad \n I Could not even find the white particle system to make a failsafe system out of.")); system = TheParticleSystemManager->createParticleSystem( failsafeTemplate ); if (system) { diff --git a/Core/GameEngine/Source/GameClient/FXList.cpp b/Core/GameEngine/Source/GameClient/FXList.cpp index 50ab4135ae0..a9f630638fd 100644 --- a/Core/GameEngine/Source/GameClient/FXList.cpp +++ b/Core/GameEngine/Source/GameClient/FXList.cpp @@ -600,7 +600,7 @@ class ParticleSystemFXNugget : public FXNugget } const ParticleSystemTemplate *tmp = TheParticleSystemManager->findTemplate(m_name); - DEBUG_ASSERTCRASH(tmp, ("ParticleSystem %s not found",m_name.str())); + DEBUG_ASSERTCRASH(TheParticleSystemManager->isDummy() || tmp, ("ParticleSystem %s not found",m_name.str())); if (tmp) { for (Int i = 0; i < m_count; i++ ) diff --git a/Generals/Code/GameEngine/Include/GameClient/GameClient.h b/Generals/Code/GameEngine/Include/GameClient/GameClient.h index 7d42104edb4..7183371d887 100644 --- a/Generals/Code/GameEngine/Include/GameClient/GameClient.h +++ b/Generals/Code/GameEngine/Include/GameClient/GameClient.h @@ -94,8 +94,6 @@ class GameClient : public SubsystemInterface, void step(); ///< Do one fixed time step - void updateHeadless(); - void addDrawableToLookupTable( Drawable *draw ); ///< add drawable ID to hash lookup table void removeDrawableFromLookupTable( Drawable *draw ); ///< remove drawable ID from hash lookup table diff --git a/Generals/Code/GameEngine/Source/GameClient/GameClient.cpp b/Generals/Code/GameEngine/Source/GameClient/GameClient.cpp index b9678a641ef..e2d293df015 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GameClient.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GameClient.cpp @@ -750,15 +750,6 @@ void GameClient::step() TheDisplay->step(); } -void GameClient::updateHeadless() -{ - // TheSuperHackers @info helmutbuhler 03/05/2025 bobtista 02/02/2026 - // Update particles to prevent accumulation in headless mode. Particles are generated - // during GameLogic and only cleaned up during rendering. update() lets particles finish - // their lifecycle naturally instead of abruptly removing them with reset(). - TheParticleSystemManager->update(); -} - Bool GameClient::isMovieAbortRequested() { if (TheGameEngine) diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h b/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h index ea8e3a59c5d..3650be5ee24 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h @@ -98,8 +98,6 @@ class GameClient : public SubsystemInterface, void step(); ///< Do one fixed time step - void updateHeadless(); - void addDrawableToLookupTable( Drawable *draw ); ///< add drawable ID to hash lookup table void removeDrawableFromLookupTable( Drawable *draw ); ///< remove drawable ID from hash lookup table diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp index cb91389555b..d1c956e533a 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp @@ -787,15 +787,6 @@ void GameClient::step() TheDisplay->step(); } -void GameClient::updateHeadless() -{ - // TheSuperHackers @info helmutbuhler 03/05/2025 bobtista 02/02/2026 - // Update particles to prevent accumulation in headless mode. Particles are generated - // during GameLogic and only cleaned up during rendering. update() lets particles finish - // their lifecycle naturally instead of abruptly removing them with reset(). - TheParticleSystemManager->update(); -} - Bool GameClient::isMovieAbortRequested() { if (TheGameEngine) From adcf69ed18b37bdeaaedde1f5a33010c1186e4b0 Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Thu, 28 May 2026 21:09:25 +0200 Subject: [PATCH 3/4] fix(memory): Fix audio event related memory leaks when pausing the game (#2731) --- Core/GameEngine/Include/Common/AudioRequest.h | 2 ++ .../Source/Common/Audio/AudioRequest.cpp | 15 +++++++++++++++ .../Include/MilesAudioDevice/MilesAudioManager.h | 2 +- .../Source/MilesAudioDevice/MilesAudioManager.cpp | 14 +++++++++----- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/Core/GameEngine/Include/Common/AudioRequest.h b/Core/GameEngine/Include/Common/AudioRequest.h index a0321fdb481..3174fe85a8a 100644 --- a/Core/GameEngine/Include/Common/AudioRequest.h +++ b/Core/GameEngine/Include/Common/AudioRequest.h @@ -45,6 +45,8 @@ struct AudioRequest : public MemoryPoolObject MEMORY_POOL_GLUE_WITH_USERLOOKUP_CREATE( AudioRequest, "AudioRequest" ) public: + AudioEventRTS* releasePendingEvent(); + RequestType m_request; union { diff --git a/Core/GameEngine/Source/Common/Audio/AudioRequest.cpp b/Core/GameEngine/Source/Common/Audio/AudioRequest.cpp index a3235e1b69a..36876f35449 100644 --- a/Core/GameEngine/Source/Common/Audio/AudioRequest.cpp +++ b/Core/GameEngine/Source/Common/Audio/AudioRequest.cpp @@ -30,5 +30,20 @@ AudioRequest::~AudioRequest() { + if (m_usePendingEvent) + { + delete m_pendingEvent; + } +} +AudioEventRTS* AudioRequest::releasePendingEvent() +{ + if (m_usePendingEvent) + { + m_usePendingEvent = false; + AudioEventRTS* event = m_pendingEvent; + m_pendingEvent = nullptr; + return event; + } + return nullptr; } diff --git a/Core/GameEngineDevice/Include/MilesAudioDevice/MilesAudioManager.h b/Core/GameEngineDevice/Include/MilesAudioDevice/MilesAudioManager.h index 71707bd3054..2f4b2d7f66b 100644 --- a/Core/GameEngineDevice/Include/MilesAudioDevice/MilesAudioManager.h +++ b/Core/GameEngineDevice/Include/MilesAudioDevice/MilesAudioManager.h @@ -251,7 +251,7 @@ class MilesAudioManager : public AudioManager void initSamplePools(); void processRequest( AudioRequest *req ); - void playAudioEvent( AudioEventRTS *event ); + void playAudioEvent( AudioRequest* req ); void stopAudioEvent( AudioHandle handle ); void pauseAudioEvent( AudioHandle handle ); diff --git a/Core/GameEngineDevice/Source/MilesAudioDevice/MilesAudioManager.cpp b/Core/GameEngineDevice/Source/MilesAudioDevice/MilesAudioManager.cpp index 3a541494959..d76113e8342 100644 --- a/Core/GameEngineDevice/Source/MilesAudioDevice/MilesAudioManager.cpp +++ b/Core/GameEngineDevice/Source/MilesAudioDevice/MilesAudioManager.cpp @@ -652,8 +652,12 @@ void MilesAudioManager::pauseAmbient( Bool shouldPause ) } //------------------------------------------------------------------------------------------------- -void MilesAudioManager::playAudioEvent( AudioEventRTS *event ) +void MilesAudioManager::playAudioEvent( AudioRequest* req ) { + DEBUG_ASSERTCRASH(req->m_usePendingEvent && req->m_pendingEvent, ("audio request was expected to contain a valid audio event")); + + AudioEventRTS* event = req->m_pendingEvent; + #ifdef INTENSIVE_AUDIO_DEBUG DEBUG_LOG(("MILES (%d) - Processing play request: %d (%s)", TheGameLogic->getFrame(), event->getPlayingHandle(), event->getEventName().str())); #endif @@ -709,7 +713,7 @@ void MilesAudioManager::playAudioEvent( AudioEventRTS *event ) } // Put this on here, so that the audio event RTS will be cleaned up regardless. - audio->m_audioEventRTS = event; + audio->m_audioEventRTS = event = req->releasePendingEvent(); audio->m_stream = stream; audio->m_type = PAT_Stream; @@ -778,7 +782,7 @@ void MilesAudioManager::playAudioEvent( AudioEventRTS *event ) sample3D = nullptr; } // Push it onto the list of playing things - audio->m_audioEventRTS = event; + audio->m_audioEventRTS = event = req->releasePendingEvent(); audio->m_3DSample = sample3D; audio->m_file = nullptr; audio->m_type = PAT_3DSample; @@ -849,7 +853,7 @@ void MilesAudioManager::playAudioEvent( AudioEventRTS *event ) } // Push it onto the list of playing things - audio->m_audioEventRTS = event; + audio->m_audioEventRTS = event = req->releasePendingEvent(); audio->m_sample = sample; audio->m_file = nullptr; audio->m_type = PAT_Sample; @@ -2930,7 +2934,7 @@ void MilesAudioManager::processRequest( AudioRequest *req ) { case AR_Play: { - playAudioEvent(req->m_pendingEvent); + playAudioEvent(req); break; } case AR_Pause: From cc4a683fd32282a7b4ebaa1d97891412b5aa77ed Mon Sep 17 00:00:00 2001 From: stm <14291421+stephanmeesters@users.noreply.github.com> Date: Thu, 28 May 2026 21:10:08 +0200 Subject: [PATCH 4/4] bugfix(water): Fix river visuals in black shroud (#2749) --- .../Source/W3DDevice/GameClient/Water/W3DWater.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/Water/W3DWater.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/Water/W3DWater.cpp index 564cc008e42..f3f33aac10e 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/Water/W3DWater.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/Water/W3DWater.cpp @@ -179,7 +179,7 @@ static Int getRiverVertexDiffuse(W3DShroud *shroud, Real x, Real y, Real shadeR, (Int)(shadeR * shroudScale), (Int)(shadeG * shroudScale), (Int)(shadeB * shroudScale), - (diffuse >> 24) & 0xff); + ((diffuse >> 24) & 0xff) * shroudScale); } void doSkyBoxSet(Bool startDraw) @@ -926,9 +926,11 @@ void WaterRenderObjClass::ReAcquireResources() tex t1 \n\ tex t2 \n\ tex t3\n\ - mul r0,v0,t0 ; blend vertex color into t0. \n\ + mul r0.rgb, v0, t0 ; blend vertex color into t0. \n\ + mov r0.a, t0 ; keep vertex alpha from fading the base water. \n\ mul r1, t1, t2 ; mul\n\ - add r0.rgb, r0, t3\n\ + add r1.rgb, r1, t3\n\ + mul r1.rgb, r1, v0.a\n\ +mul r0.a, r0, t3\n\ add r0.rgb, r0, r1\n"; hr = D3DXAssembleShader( shader, strlen(shader), 0, nullptr, &compiledShader, nullptr);