Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions GeneralsMD/Code/GameEngine/Include/Common/Geometry.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
93 changes: 90 additions & 3 deletions GeneralsMD/Code/GameEngine/Source/Common/System/Geometry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -462,6 +502,53 @@ void GeometryInfo::makeRandomOffsetOnPerimeter(Coord3D& pt) const
};
}

//=============================================================================
void GeometryInfo::makeGameClientRandomOffsetOnPerimeter(Coord3D& pt) const
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can avoid making a duplicate function by passing a function pointer for GameClientRandomValueReal or GameLogicRandomValueReal. Same for the other function.

{
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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do this more elegantly? I do not have better idea but it is a bit sad to have this as argument.

{

DEBUG_ASSERTCRASH( locInfo, ("getLocalEffectPos: locInfo is null") );
Expand Down Expand Up @@ -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 ];

}
Expand Down Expand Up @@ -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
Expand All @@ -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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#if RETAIL_COMPATIBLE_CRC

Otherwise I think it won't work if it's defined as 0. Multiple times.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch

// 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<void>(getLocalEffectPos(&modData->m_particleSystem[newState][i].locInfo, draw));
}
}
#endif

}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont think || TheParticleSystemManager->isDummy() would be correct. It would mean a null template would pass. I suspect this means we need to preserve templates in the dummy manager, at least for RETAIL_COMPATIBLE_CRC.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

|| TheParticleSystemManager->isDummy() here assumes that the template exists in the original data (which for the three cases in this PR it does), which isn't great to assume that I suppose, it does pass the replay test.

{
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<void>(GameLogicRandomValue(0, 1));
static_cast<void>(GameLogicRandomValue(0, 1));
}
}
#endif
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or just unroll the relevant calls to GameLogicRandomValue here?

}
#endif
}
}
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down