diff --git a/GeneralsMD/Code/GameEngine/Include/Common/Geometry.h b/GeneralsMD/Code/GameEngine/Include/Common/Geometry.h index 221ca370ce0..8a87989e786 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/Geometry.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/Geometry.h @@ -171,9 +171,10 @@ 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 makeGameLogicRandomOffsetWithinFootprint(Coord3D& pt) const; + void makeGameLogicRandomOffsetOnPerimeter(Coord3D& pt) const; + void makeGameClientRandomOffsetWithinFootprint(Coord3D& pt) const; + void makeGameClientRandomOffsetOnPerimeter(Coord3D& pt) const; 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..18ac386af23 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::makeGameLogicRandomOffsetWithinFootprint(Coord3D& pt) const { switch(m_type) { @@ -416,14 +416,54 @@ void GeometryInfo::makeRandomOffsetWithinFootprint(Coord3D& pt) const } //============================================================================= -void GeometryInfo::makeRandomOffsetOnPerimeter(Coord3D& pt) const +void GeometryInfo::makeGameClientRandomOffsetWithinFootprint(Coord3D& pt) const { switch(m_type) { case GEOMETRY_SPHERE: case GEOMETRY_CYLINDER: { - DEBUG_CRASH( ("GeometryInfo::makeRandomOffsetOnPerimeter() not implemented for SPHERE or CYLINDER extents. Using position.") ); +#if 1 + // this is a better technique than the more obvious radius-and-angle + // one, below, because the latter tends to clump more towards the center. + Real maxDistSqr = sqr(m_majorRadius); + Real distSqr; + do + { + pt.x = GameClientRandomValueReal(-m_majorRadius, m_majorRadius); + pt.y = GameClientRandomValueReal(-m_majorRadius, m_majorRadius); + pt.z = 0.0f; + distSqr = sqr(pt.x) + sqr(pt.y); + } while (distSqr > maxDistSqr); +#else + Real radius = GameClientRandomValueReal(0.0f, m_boundingCircleRadius); + Real angle = GameClientRandomValueReal(-PI, PI); + pt.x = radius * Cos(angle); + pt.y = radius * Sin(angle); + pt.z = 0.0f; +#endif + break; + } + + case GEOMETRY_BOX: + { + pt.x = GameClientRandomValueReal(-m_majorRadius, m_majorRadius); + pt.y = GameClientRandomValueReal(-m_minorRadius, m_minorRadius); + pt.z = 0.0f; + break; + } + }; +} + +//============================================================================= +void GeometryInfo::makeGameLogicRandomOffsetOnPerimeter(Coord3D& pt) const +{ + switch(m_type) + { + case GEOMETRY_SPHERE: + case GEOMETRY_CYLINDER: + { + DEBUG_CRASH( ("GeometryInfo::makeGameLogicRandomOffsetOnPerimeter() not implemented for SPHERE or CYLINDER extents. Using position.") ); //Kris: Did not have time nor need to support non-box extents. I added this feature for script placement // of boobytraps. @@ -462,6 +502,53 @@ void GeometryInfo::makeRandomOffsetOnPerimeter(Coord3D& pt) const }; } +//============================================================================= +void GeometryInfo::makeGameClientRandomOffsetOnPerimeter(Coord3D& pt) const +{ + switch(m_type) + { + case GEOMETRY_SPHERE: + case GEOMETRY_CYLINDER: + { + DEBUG_CRASH( ("GeometryInfo::makeGameClientRandomOffsetOnPerimeter() not implemented for SPHERE or CYLINDER extents. Using position.") ); + + //Kris: Did not have time nor need to support non-box extents. I added this feature for script placement + // of boobytraps. + pt.x = 0.0f; + pt.y = 0.0f; + break; + } + + case GEOMETRY_BOX: + { + if( GameClientRandomValueReal( 0.0f, 1.0f ) < 0.5f ) + { + //Pick random point on x axis. + pt.x = GameClientRandomValueReal(-m_majorRadius, m_majorRadius); + + //Min or max the y axis value + if( GameClientRandomValueReal( 0.0f, 1.0f ) < 0.5f ) + pt.y = -m_minorRadius; + else + pt.y = m_minorRadius; + } + else + { + //Pick random point on y axis. + pt.y = GameClientRandomValueReal(-m_minorRadius, m_minorRadius); + + //Min or max the x axis value + if( GameClientRandomValueReal( 0.0f, 1.0f ) < 0.5f ) + pt.x = -m_majorRadius; + else + pt.x = m_majorRadius; + } + pt.z = 0.0f; + break; + } + }; +} + //============================================================================= Real GeometryInfo::getFootprintArea() const { diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/GenerateMinefieldBehavior.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/GenerateMinefieldBehavior.cpp index 03194dc1981..c091e8766d4 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/GenerateMinefieldBehavior.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/GenerateMinefieldBehavior.cpp @@ -350,7 +350,7 @@ void GenerateMinefieldBehavior::placeMinesInFootprint(const GeometryInfo& geom, Int maxRetry = 100; do { - geom.makeRandomOffsetWithinFootprint(pt); + geom.makeGameLogicRandomOffsetWithinFootprint(pt); pt.x += target->x; pt.y += target->y; pt.z += target->z; diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Damage/TransitionDamageFX.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Damage/TransitionDamageFX.cpp index 9e201c1508f..646a5e0b98a 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, Bool useGameLogicRandom = TRUE) { 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 = useGameLogicRandom ? GameLogicRandomValue( 0, boneCount - 1 ) : GameClientRandomValue( 0, boneCount - 1 ); return positions[ pick ]; } @@ -387,14 +387,12 @@ void TransitionDamageFX::onBodyDamageStateChange( const DamageInfo* damageInfo, if( lastDamageInfo == nullptr || getDamageTypeFlag( modData->m_damageParticleTypes, lastDamageInfo->in.m_damageType ) ) { - // create a new particle system based on the template provided ParticleSystem* pSystem = TheParticleSystemManager->createParticleSystem( pSystemT ); if( pSystem ) { - // 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, FALSE); // // set position on system given any bone position provided, the bone position is @@ -409,13 +407,25 @@ void TransitionDamageFX::onBodyDamageStateChange( const DamageInfo* damageInfo, // save the id of this particle system so we can remove it later if it still exists m_particleSystemID[ newState ][ i ] = pSystem->getSystemID(); - } - } } +#ifdef RETAIL_COMPATIBLE_CRC + // TheSuperHackers @fix stephanmeesters 18/05/2026 Fix issue where the creation of a certain particle system + // would influence game logic CRC due to the incorrect usage of game logic RNG. This code block is required to + // forward the game logic RNG and keep things consistent. + if(pSystemT) // todo: add || TheParticleSystemManager->isDummy() + { + if( lastDamageInfo == nullptr || + getDamageTypeFlag( modData->m_damageParticleTypes, lastDamageInfo->in.m_damageType ) ) + { + static_cast(getLocalEffectPos(&modData->m_particleSystem[newState][i].locInfo, draw)); + } + } +#endif + } } diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/EMPUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/EMPUpdate.cpp index 7bb0a907522..9ac1e663d30 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/EMPUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/EMPUpdate.cpp @@ -304,14 +304,12 @@ void EMPUpdate::doDisableAttack() for (UnsignedInt e = 0 ; e < emitterCount; ++e) { - ParticleSystem *sys = TheParticleSystemManager->createParticleSystem(tmp); - if (sys) { Coord3D offs = {0,0,0}; - curVictim->getGeometryInfo().makeRandomOffsetWithinFootprint( offs ); - offs.z = GameLogicRandomValue(3, victimHeight); + curVictim->getGeometryInfo().makeGameClientRandomOffsetWithinFootprint( offs ); + 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,12 +326,31 @@ 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)); } } } - } +#ifdef RETAIL_COMPATIBLE_CRC + // TheSuperHackers @fix stephanmeesters 18/05/2026 Fix issue where the creation of a certain particle system + // would influence game logic CRC due to the incorrect usage of game logic RNG. This code block is required to + // forward the game logic RNG and keep things consistent. + if(tmp) // todo: add || TheParticleSystemManager->isDummy() + { + Real victimHeight = curVictim->getGeometryInfo().getMaxHeightAbovePosition(); + Real victimFootprintArea = curVictim->getGeometryInfo().getFootprintArea(); + Real victimVolume = victimFootprintArea * MIN(victimHeight, 10.0f); + UnsignedInt emitterCount = MAX(15, REAL_TO_INT_CEIL(data->m_sparksPerCubicFoot * victimVolume)); + for (UnsignedInt e = 0 ; e < emitterCount; ++e) + { + Coord3D offs = { 0,0,0 }; + curVictim->getGeometryInfo().makeGameLogicRandomOffsetWithinFootprint(offs); + static_cast(GameLogicRandomValue(0, 1)); + static_cast(GameLogicRandomValue(0, 1)); + } + } +#endif + } } } diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/SpecialAbilityUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/SpecialAbilityUpdate.cpp index fbbc907f31b..cc267c0df44 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/SpecialAbilityUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/SpecialAbilityUpdate.cpp @@ -1404,13 +1404,23 @@ void SpecialAbilityUpdate::triggerAbilityEffect() if (sys) { Coord3D offs = {0,0,0}; - target->getGeometryInfo().makeRandomOffsetWithinFootprint( offs ); + target->getGeometryInfo().makeGameClientRandomOffsetWithinFootprint( offs ); sys->attachToObject(target); sys->setPosition( &offs ); sys->setSystemLifetime( data->m_effectDuration * durationInterleaveFactor ); //lifetime of the system, not the particles + } +#ifdef RETAIL_COMPATIBLE_CRC + // TheSuperHackers @fix stephanmeesters 18/05/2026 Fix issue where the creation of a certain particle system + // would influence game logic CRC due to the incorrect usage of game logic RNG. This code block is required to + // forward the game logic RNG and keep things consistent. + if(sys) // todo: add || TheParticleSystemManager->isDummy() + { + Coord3D offs = { 0,0,0 }; + target->getGeometryInfo().makeGameLogicRandomOffsetWithinFootprint(offs); } +#endif } } break; diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptActions.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptActions.cpp index 023533d1b4a..abbbd21cd96 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptActions.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptActions.cpp @@ -3729,7 +3729,7 @@ void ScriptActions::doNamedSetBoobytrapped( const AsciiString& thingTemplateName { //The charge gets positioned randomly on the outside of the perimeter of the victim. Coord3D pos; - obj->getGeometryInfo().makeRandomOffsetOnPerimeter( pos ); + obj->getGeometryInfo().makeGameLogicRandomOffsetOnPerimeter( pos ); //Get the angle and transform matrix from the obj... then transform the calculated //position @@ -3768,7 +3768,7 @@ void ScriptActions::doTeamSetBoobytrapped( const AsciiString& thingTemplateName, { //The charge gets positioned randomly on the outside of the perimeter of the victim. Coord3D pos; - obj->getGeometryInfo().makeRandomOffsetOnPerimeter( pos ); + obj->getGeometryInfo().makeGameLogicRandomOffsetOnPerimeter( pos ); //Get the angle and transform matrix from the obj... then transform the calculated //position