Skip to content

fix(particlesys): Decouple particle systems from logic crc#2742

Merged
xezon merged 6 commits into
TheSuperHackers:mainfrom
xezon:xezon/fix-particlesystem-logic-crc
May 25, 2026
Merged

fix(particlesys): Decouple particle systems from logic crc#2742
xezon merged 6 commits into
TheSuperHackers:mainfrom
xezon:xezon/fix-particlesystem-logic-crc

Conversation

@xezon
Copy link
Copy Markdown

@xezon xezon commented May 22, 2026

This change decouples particle systems from logic crc and is an alternative to #2717.

The implementation adds a tiny runtime cost for virtual table lookups, but it provides an easy to use runtime switch for logic and client random values. Using it requires no refactors, template functions or static functions.

TODO

  • Replicate in Generals

@xezon xezon added Minor Severity: Minor < Major < Critical < Blocker Gen Relates to Generals ZH Relates to Zero Hour Fix Is fixing something, but is not user facing labels May 22, 2026
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 22, 2026

Greptile Summary

This PR decouples particle system rendering from the game-logic CRC by introducing a small RandomValueClass polymorphic interface (LogicRandomValueClass / ClientRandomValueClass) and migrating affected particle placement calls in EMPUpdate, SpecialAbilityUpdate, and TransitionDamageFX to use the client-side random stream. RETAIL_COMPATIBLE_CRC blocks preserve the original logic-RNG consumption to keep replay CRCs compatible.

  • Adds RandomValueClass abstract base with two concrete implementations and RandomValueInt/RandomValueReal helper macros in Core/GameEngine/Include/Common/RandomValue.h and its .cpp.
  • Updates GeometryInfo::makeRandomOffsetWithinFootprint with a defaulted RandomValueClass parameter (defaults to LogicRandomValueClass) so all existing call sites are backward-compatible, and applies the same change identically to both Generals/ and GeneralsMD/.
  • Adds RETAIL_COMPATIBLE_CRC dummy logic-random calls before each createParticleSystem invocation in the three updated modules, then uses ClientRandomValueClass for the real particle positioning inside the if (sys) guard.

Confidence Score: 5/5

Safe to merge; the change is a clean extraction of random-number dispatch into a small virtual interface with no correctness regressions in the updated call sites.

All call sites correctly route particle positioning to the client random stream, RETAIL_COMPATIBLE_CRC dummy calls are properly scoped inside the if(pSystemT/tmp) guards, and the default parameter preserves backward compatibility for unchanged call sites. The only open item is the missing protected destructor on the abstract base, which has no runtime impact given the exclusively stack-based usage.

Core/GameEngine/Include/Common/RandomValue.h — the abstract base class lacks a protected destructor, which should be addressed before the interface is used more broadly.

Important Files Changed

Filename Overview
Core/GameEngine/Include/Common/RandomValue.h Introduces RandomValueClass abstract base, LogicRandomValueClass, ClientRandomValueClass, and helper macros; missing virtual destructor on the base class.
Core/GameEngine/Source/Common/RandomValue.cpp Thin virtual-method implementations delegating to existing free functions; straightforward and correct.
GeneralsMD/Code/GameEngine/Include/Common/Geometry.h Same signature change as Generals; mirrors expected parallel structure between the two targets.
GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Damage/TransitionDamageFX.cpp Same changes as Generals counterpart; dummy CRC call correctly placed inside if(pSystemT) guard.
GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/EMPUpdate.cpp Mirrors Generals EMPUpdate changes; RETAIL_COMPATIBLE_CRC block scoped correctly.
GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/SpecialAbilityUpdate.cpp Mirrors Generals SpecialAbilityUpdate changes.

Class Diagram

%%{init: {'theme': 'neutral'}}%%
classDiagram
    class RandomValueClass {
        <<abstract>>
        +GetRandomValueInt(lo, hi, file, line) Int
        +GetRandomValueReal(lo, hi, file, line) Real
    }
    class LogicRandomValueClass {
        <<final>>
        +GetRandomValueInt(lo, hi, file, line) Int
        +GetRandomValueReal(lo, hi, file, line) Real
    }
    class ClientRandomValueClass {
        <<final>>
        +GetRandomValueInt(lo, hi, file, line) Int
        +GetRandomValueReal(lo, hi, file, line) Real
    }
    RandomValueClass <|-- LogicRandomValueClass : delegates to GameLogicRandom
    RandomValueClass <|-- ClientRandomValueClass : delegates to GameClientRandom
    class GeometryInfo {
        +makeRandomOffsetWithinFootprint(pt, random)
    }
    class TransitionDamageFX {
        +onBodyDamageStateChange(...)
    }
    class EMPUpdate {
        +doDisableAttack()
    }
    class SpecialAbilityUpdate {
        +triggerAbilityEffect()
    }
    GeometryInfo ..> RandomValueClass : uses
    TransitionDamageFX ..> LogicRandomValueClass : CRC dummy
    TransitionDamageFX ..> ClientRandomValueClass : actual particle pos
    EMPUpdate ..> LogicRandomValueClass : CRC dummy
    EMPUpdate ..> ClientRandomValueClass : actual particle pos/delay
    SpecialAbilityUpdate ..> LogicRandomValueClass : CRC dummy
    SpecialAbilityUpdate ..> ClientRandomValueClass : actual particle pos
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
Core/GameEngine/Include/Common/RandomValue.h:38-42
`RandomValueClass` has virtual methods but no virtual destructor. While the two concrete types are always used as stack temporaries here and pose no immediate risk, the absence of a virtual destructor is undefined behaviour if any caller ever destroys a derived instance through a base-class pointer. A protected non-virtual destructor is the idiomatic way to express "not for polymorphic deletion" without the overhead of a virtual destructor.

```suggestion
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;
protected:
	~RandomValueClass() = default;
};
```

Reviews (3): Last reviewed commit: "Replicate in Generals" | Re-trigger Greptile

Comment thread Core/GameEngine/Include/Common/RandomValue.h Outdated
Comment on lines +307 to +315
#if RETAIL_COMPATIBLE_CRC
// TheSuperHackers @fix The particle system is now decoupled from the logic crc
// and the legacy logic random values are preserved here.
{
Coord3D offs = {0,0,0};
curVictim->getGeometryInfo().makeRandomOffsetWithinFootprint( offs, LogicRandomValueClass() );
GameLogicRandomValue(3, victimHeight);
GameLogicRandomValue(1, 100);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 CRC dummy calls placed outside the if (sys) null-check

In the original code all three logic-random calls (makeRandomOffsetWithinFootprint, GameLogicRandomValue(3, victimHeight), GameLogicRandomValue(1, 100)) were inside if (sys). The new RETAIL_COMPATIBLE_CRC block runs them unconditionally, before createParticleSystem is even called. If createParticleSystem returns null the original code would not have advanced the logic RNG at all, but the CRC block advances it anyway, producing a divergent RNG state and breaking the replay CRC that the block is supposed to preserve.

The same structural mistake is present in TransitionDamageFX.cpp (the getLocalEffectPos dummy call before if (pSystem)) and SpecialAbilityUpdate.cpp (the makeRandomOffsetWithinFootprint dummy call before if (sys)). All three blocks should be moved inside their respective if (sys/pSystem) guards to exactly mirror the original code path.

Prompt To Fix With AI
This is a comment left during a code review.
Path: GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/EMPUpdate.cpp
Line: 307-315

Comment:
**CRC dummy calls placed outside the `if (sys)` null-check**

In the original code all three logic-random calls (`makeRandomOffsetWithinFootprint`, `GameLogicRandomValue(3, victimHeight)`, `GameLogicRandomValue(1, 100)`) were inside `if (sys)`. The new `RETAIL_COMPATIBLE_CRC` block runs them unconditionally, before `createParticleSystem` is even called. If `createParticleSystem` returns null the original code would not have advanced the logic RNG at all, but the CRC block advances it anyway, producing a divergent RNG state and breaking the replay CRC that the block is supposed to preserve.

The same structural mistake is present in `TransitionDamageFX.cpp` (the `getLocalEffectPos` dummy call before `if (pSystem)`) and `SpecialAbilityUpdate.cpp` (the `makeRandomOffsetWithinFootprint` dummy call before `if (sys)`). All three blocks should be moved inside their respective `if (sys/pSystem)` guards to exactly mirror the original code path.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This is fine for as long as it is inside the if (tmp) branch.

Copy link
Copy Markdown

@Caball009 Caball009 May 24, 2026

Choose a reason for hiding this comment

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

Do we need a comment for that? Otherwise it might be tempting to combine both checks in a similar effort as #2724 I don't have a strong opinion on it either way FWIW.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I dont think a comment is needed. Moving logic out of conditions will always the carry risk of logic mismatch and this one is not special.

Copy link
Copy Markdown

@Caball009 Caball009 left a comment

Choose a reason for hiding this comment

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

Nice to see a clean implementation.

I tested this against ~2500 (short) replays and saw no mismatches because of this change.

Comment thread Core/GameEngine/Include/Common/RandomValue.h
Comment thread GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Damage/TransitionDamageFX.cpp Outdated
Comment thread GeneralsMD/Code/GameEngine/Include/Common/Geometry.h Outdated
Comment thread Core/GameEngine/Include/Common/RandomValue.h
@xezon
Copy link
Copy Markdown
Author

xezon commented May 25, 2026

Replicated in Generals with conflict in SpecialAbilityUpdate because of whitespace differences.

D:\Projects\TheSuperHackers\GeneralsGameCode>FOR /F "delims=" %b IN ('git merge-base --fork-point main') DO git diff %b  1>changes.patch

D:\Projects\TheSuperHackers\GeneralsGameCode>git diff bf8d5be0294fc71b601444ff9446c12f4720022f  1>changes.patch

D:\Projects\TheSuperHackers\GeneralsGameCode>git apply -p2 --directory=Generals --reject --whitespace=fix changes.patch
Checking patch Generals/GameEngine/Include/Common/RandomValue.h...
error: Generals/GameEngine/Include/Common/RandomValue.h: No such file or directory
Checking patch Generals/GameEngine/Source/Common/RandomValue.cpp...
error: Generals/GameEngine/Source/Common/RandomValue.cpp: No such file or directory
Checking patch Generals/Code/GameEngine/Include/Common/Geometry.h...
Checking patch Generals/Code/GameEngine/Source/Common/System/Geometry.cpp...
Checking patch Generals/Code/GameEngine/Source/GameLogic/Object/Damage/TransitionDamageFX.cpp...
Checking patch Generals/Code/GameEngine/Source/GameLogic/Object/Update/EMPUpdate.cpp...
Hunk #1 succeeded at 235 (offset -69 lines).
Hunk #2 succeeded at 269 (offset -69 lines).
Checking patch Generals/Code/GameEngine/Source/GameLogic/Object/Update/SpecialAbilityUpdate.cpp...
error: while searching for:
        const ParticleSystemTemplate *tmp = data->m_disableFXParticleSystem;
        if (tmp)
        {
          ParticleSystem *sys = TheParticleSystemManager->createParticleSystem(tmp);
          if (sys)
          {
            Coord3D offs = {0,0,0};
            target->getGeometryInfo().makeRandomOffsetWithinFootprint( offs );

            sys->attachToObject(target);
            sys->setPosition( &offs );

error: patch failed: Generals/Code/GameEngine/Source/GameLogic/Object/Update/SpecialAbilityUpdate.cpp:1400
Applied patch Generals/Code/GameEngine/Include/Common/Geometry.h cleanly.
Applied patch Generals/Code/GameEngine/Source/Common/System/Geometry.cpp cleanly.
Applied patch Generals/Code/GameEngine/Source/GameLogic/Object/Damage/TransitionDamageFX.cpp cleanly.
Applied patch Generals/Code/GameEngine/Source/GameLogic/Object/Update/EMPUpdate.cpp cleanly.
Applying patch Generals/Code/GameEngine/Source/GameLogic/Object/Update/SpecialAbilityUpdate.cpp with 1 reject...
Rejected hunk #1.

@xezon xezon merged commit 9a402fb into TheSuperHackers:main May 25, 2026
17 checks passed
@xezon xezon deleted the xezon/fix-particlesystem-logic-crc branch May 25, 2026 10:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Fix Is fixing something, but is not user facing Gen Relates to Generals Minor Severity: Minor < Major < Critical < Blocker ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Particle systems cause game logic / CRC changes

2 participants