Skip to content

bugfix(WeaponTemplate): Fix the override handling and resetting of modified weapon templates - #3478

Open
DrGoldFish1 wants to merge 1 commit into
TheSuperHackers:mainfrom
DrGoldFish1:bugfix(weapon)-allow-map/solo.ini-overrides
Open

DrGoldFish1 wants to merge 1 commit into
TheSuperHackers:mainfrom
DrGoldFish1:bugfix(weapon)-allow-map/solo.ini-overrides

Conversation

@DrGoldFish1

@DrGoldFish1 DrGoldFish1 commented Oct 11, 2026 •

Copy link
Copy Markdown

Weapon templates now get overridden and get reset properly fixing memory leaks and makes it easier for map makers to adjust those weapons without having to do bypassing and stuff.

All code is wrapped in the new RETAIL_COMPATIBLE_SCRIPTING macro.
This is something Mauller was working on.

Used this map.ini change in both retail and this new build.

Weapon CrusaderTankGun
  PrimaryDamage           = 400.0
  PrimaryDamageRadius     = 50.0
  SecondaryDamage         = 20.0
  SecondaryDamageRadius   = 60.0
  ScatterRadiusVsInfantry = 30.0     ;When this weapon is used against infantry, it can randomly miss by as much as this distance.
  AttackRange             = 350.0
  MinimumAttackRange      = 150.0
  DamageType              = EXPLOSION
  DeathType               = EXPLODED
  WeaponSpeed             = 200         ; dist/sec 
  ProjectileDetonationFX = WeaponFX_RocketBuggyMissileDetonation
  ProjectileObject = MissileDefenderMissile
  ProjectileExhaust = MissileDefenderMissileExhaust
  FireFX                  = WeaponFX_NukeCannonMuzzleFlash
  FireSound               = NukeCannonWeapon
  VeterancyFireFX         = HEROIC WeaponFX_HeroicNukeCannonMuzzleFlash
  ProjectileDetonationFX  = WeaponFX_NukeCannon
  ProjectileDetonationOCL = OCL_RadiationFieldMedium
  RadiusDamageAffects     = SUICIDE SELF ALLIES ENEMIES NEUTRALS NOT_SIMILAR
  DelayBetweenShots       = 500        ; time between shots, msec
  ClipSize                = 5            ; how many shots in a Clip (0 == infinite)
  ClipReloadTime          = 5000            ; how long to reload a Clip, msec
End

Before
https://github.com/user-attachments/assets/68215433-d9c3-49fe-85e3-c527e12f17e2

After
https://github.com/user-attachments/assets/b661a13a-42de-4246-8549-0fa27c8a0823

@coderabbitai

coderabbitai Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Walkthrough

The changes add the RETAIL_COMPATIBLE_SCRIPTING build flag. Weapon-template inheritance, override creation and resolution, reset cleanup, and load processing now vary with this flag in Generals and GeneralsMD.

Changes

Weapon template override handling

Layer / File(s) Summary
Compatibility flag and template structure
Core/GameEngine/Include/Common/GameDefines.h, Generals/Code/GameEngine/Include/GameLogic/Weapon.h, Generals/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp, GeneralsMD/Code/GameEngine/Include/GameLogic/Weapon.h, GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp
The new flag defaults to 1 when not already defined. In both game variants, WeaponTemplate retains its prior base class and override-link members when compatibility is enabled; otherwise, it derives from Overridable and excludes those members. Constructor and destructor handling follows the same condition.
Override creation and resolution
Generals/Code/GameEngine/Include/GameLogic/Weapon.h, Generals/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp, GeneralsMD/Code/GameEngine/Include/GameLogic/Weapon.h, GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp
When compatibility is disabled, override creation copies the template and its extra bonus set when present, then links and marks the copy as an override. Lookups and weapon allocation use the final override. Compatibility-enabled builds retain the prior behavior.
Override reset and load processing
Generals/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp, GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp
When compatibility is disabled, reset removes overrides and erases base templates with no remaining overrides from the map and vector. Parsing a weapon definition as an override also calls postProcessLoad.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Suggested reviewers: xezon

Merge Risk: 🟡 Moderate · up to 0b472

Map and solo weapon overrides can use the wrong values or fail to resolve a projectile declared later. Fix override ordering and defer projectile resolution before merging.

Pre-merge checks | Passed 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Title check Passed The title clearly identifies the main change: fixing WeaponTemplate override handling and reset behavior.
Description check Passed The description directly explains weapon-template overrides, reset behavior, memory leaks, the RETAIL_COMPATIBLE_SCRIPTING macro, and map.ini usage.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High

weapon = TheWeaponStore->newOverride(weapon);

Loading a second INI_LOAD_CREATE_OVERRIDES definition for a weapon silently discards changes made by the first override. findWeaponTemplatePrivate returns the base template, and passing it to newOverride starts the next layer from that base; use the current final override as the source instead.

🤖 Copy this AI Prompt to have your agent fix this:
In file @GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp around line 1852:

Loading a second `INI_LOAD_CREATE_OVERRIDES` definition for a weapon silently discards changes made by the first override. `findWeaponTemplatePrivate` returns the base template, and passing it to `newOverride` starts the next layer from that base; use the current final override as the source instead.

{
WeaponTemplate *wt = *it;
NameKeyType nameKey = wt->getNameKey();
if (!wt->deleteOverrides())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High Object/Weapon.cpp:1791

Map/solo-only weapons remain registered after WeaponStore::reset(), so later games can resolve stale map-specific definitions instead of loading a clean set. newWeaponTemplate entries from the weapon == nullptr branch of parseWeaponTemplateDefinition are not marked as overrides, so deleteOverrides() leaves them in the vector and hash map; remove these standalone entries during reset as well.

🤖 Copy this AI Prompt to have your agent fix this:
In file @GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp around line 1791:

Map/solo-only weapons remain registered after `WeaponStore::reset()`, so later games can resolve stale map-specific definitions instead of loading a clean set. `newWeaponTemplate` entries from the `weapon == nullptr` branch of `parseWeaponTemplateDefinition` are not marked as overrides, so `deleteOverrides()` leaves them in the vector and hash map; remove these standalone entries during reset as well.

@greptile-apps

greptile-apps Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 3/5

[Medium impact] Weapon configuration system gains override capability.

Fix the override order and inherited object lists before merging.

Findings

  1. P1 Later weapon changes are ignored ▶
  2. P1 Inherited weapon objects disappear ▶

Summary

This PR adds weapon overrides behind RETAIL_COMPATIBLE_SCRIPTING=0 in both game variants.

  • Map and solo INI files can change weapon templates.
  • Weapon-store resets clear map-only weapon overrides.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Load map.ini] --> B[Copy base weapon]
    B --> C[Link first override]
    C --> D[Load solo.ini]
    D --> E[Copy base and its existing link]
    E --> F[Base points to second override]
    F --> G[Second override points to first]
    G --> H[Weapon lookup returns first override]
Loading

Reviews (1) · Last reviewed commit: "bugfix(weapon) allow map/solo.ini overri..." · Reviewed by Greptile

Comment on lines +1583 to +1590
WeaponTemplate *wt = newInstance(WeaponTemplate);
(*wt) = (*weaponTemplate);
if (weaponTemplate->m_extraBonus)
{
wt->m_extraBonus = newInstance(WeaponBonusSet);
(*wt->m_extraBonus) = (*weaponTemplate->m_extraBonus);
}
weaponTemplate->setNextOverride(wt);

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 Later weapon changes are ignored

With RETAIL_COMPATIBLE_SCRIPTING=0, overriding the same weapon in both map.ini and solo.ini leaves the first override active. The parser passes the base template to newOverride each time. Copying that base also copies its existing m_nextOverride, producing base → second override → first override. getFinalOverride() then returns the first override, ignoring the later changes.

Copy and append from the current final override instead. Apply the same fix in GeneralsMD.

Comment on lines +1738 to +1740
if (ini->getLoadType() == INI_LOAD_CREATE_OVERRIDES)
{
weapon->postProcessLoad();

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 Inherited weapon objects disappear

With RETAIL_COMPATIBLE_SCRIPTING=0, this postProcessLoad() call removes inherited FireOCL and ProjectileDetonationOCL behavior unless the override repeats those fields. Startup already cleared the base template's OCL names while keeping its resolved pointers. newOverride copies those pointers and empty names, then postProcessLoad() replaces the pointers with nullptr.

Even a damage-only override stops the weapon from creating its configured objects. Preserve inherited pointers when resolving unchanged fields, and apply the same fix in GeneralsMD.

Knowledge Base Used: RTS engine core

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a3548402-be56-40d8-9616-7e3c5598ba33
📥 Commits

Reviewing files that changed from the base of the PR and between 0e6a06c and 0b47288.

📒 Files selected for processing (5)
  • Core/GameEngine/Include/Common/GameDefines.h
  • Generals/Code/GameEngine/Include/GameLogic/Weapon.h
  • Generals/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp
  • GeneralsMD/Code/GameEngine/Include/GameLogic/Weapon.h
  • GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.

Comment on lines +1583 to +1591
WeaponTemplate *wt = newInstance(WeaponTemplate);
(*wt) = (*weaponTemplate);
if (weaponTemplate->m_extraBonus)
{
wt->m_extraBonus = newInstance(WeaponBonusSet);
(*wt->m_extraBonus) = (*weaponTemplate->m_extraBonus);
}
weaponTemplate->setNextOverride(wt);
wt->markAsOverride();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Repeated overrides build a broken chain. newOverride always copies the base template. That copy includes the base's m_nextOverride, and the new template is then linked directly to the base. When a weapon is overridden twice (for example, by solo.ini and then map.ini), the chain becomes base -> override2 -> override1. getFinalOverride() then returns override1. The newest override is ignored, and it also never received the first override's values.

  • Generals/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp#L1583-L1591: copy from friend_getFinalOverride(), set m_nextOverride to null, copy m_extraBonus from that final template, and call setNextOverride(wt) on the final template.
  • GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp#L1718-L1726: apply the same change.
📍 Affects 2 files
  • Generals/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp#L1583-L1591 (this comment)
  • GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp#L1718-L1726

Comment on lines +1737 to +1743
#if !RETAIL_COMPATIBLE_SCRIPTING
if (ini->getLoadType() == INI_LOAD_CREATE_OVERRIDES)
{
weapon->postProcessLoad();
}
#endif

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '1680,1748p' Generals/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp
sed -n '2180,2225p' Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp
rg -n '#define DEBUG_ASSERTCRASH|DEBUG_ASSERTCRASH\(' Generals/Code/GameEngine/Include/Common Core/GameEngine/Include/Common | head -40

Repository: TheSuperHackers/GeneralsGameCode

Length of output: 9936


🏁 Script executed:

set -e
printf '%s\n' '--- Generals references ---'
rg -n -F -- 'newOverride' Generals/Code/GameEngine/Include Generals/Code/GameEngine/Source
rg -n -F -- 'm_weaponTemplateVector' Generals/Code/GameEngine/Include Generals/Code/GameEngine/Source
rg -n -F -- 'void WeaponStore::reset' Generals/Code/GameEngine/Source
rg -n -F -- 'WeaponTemplate::postProcessLoad' Generals/Code/GameEngine/Source
printf '%s\n' '--- Generals definitions ---'
sed -n '350,455p' Generals/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp
sed -n '1600,1695p' Generals/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp
sed -n '1670,1760p' Generals/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp
sed -n '380,470p' Generals/Code/GameEngine/Include/GameLogic/Weapon.h
printf '%s\n' '--- GeneralsMD references ---'
rg -n -F -- 'newOverride' GeneralsMD/Code/GameEngine/Include GeneralsMD/Code/GameEngine/Source
rg -n -F -- 'm_weaponTemplateVector' GeneralsMD/Code/GameEngine/Include GeneralsMD/Code/GameEngine/Source
rg -n -F -- 'void WeaponStore::reset' GeneralsMD/Code/GameEngine/Source
rg -n -F -- 'WeaponTemplate::postProcessLoad' GeneralsMD/Code/GameEngine/Source
printf '%s\n' '--- GeneralsMD definitions ---'
sed -n '365,470p' GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp
sed -n '1720,1885p' GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp
sed -n '390,480p' GeneralsMD/Code/GameEngine/Include/GameLogic/Weapon.h
printf '%s\n' '--- DEBUG behavior ---'
sed -n '175,212p' Core/GameEngine/Include/Common/Debug.h
printf '%s\n' '--- all WeaponStore::postProcessLoad callers ---'
rg -n -F -- '->postProcessLoad()' Generals/Code/GameEngine GeneralsMD/Code/GameEngine | rg 'Weapon|weapon|TheWeaponStore|weaponStore' || true

Repository: TheSuperHackers/GeneralsGameCode

Length of output: 37721


🏁 Script executed:

set -e
printf '%s\n' '--- references ---'
rg -n -F -- 'newOverride' Generals/Code/GameEngine/Include Generals/Code/GameEngine/Source GeneralsMD/Code/GameEngine/Include GeneralsMD/Code/GameEngine/Source
rg -n -F -- 'm_weaponTemplateVector' Generals/Code/GameEngine/Include Generals/Code/GameEngine/Source GeneralsMD/Code/GameEngine/Include GeneralsMD/Code/GameEngine/Source
rg -n -F -- 'void WeaponStore::reset' Generals/Code/GameEngine/Source GeneralsMD/Code/GameEngine/Source
rg -n -F -- 'WeaponTemplate::postProcessLoad' Generals/Code/GameEngine/Source GeneralsMD/Code/GameEngine/Source
printf '%s\n' '--- Generals definitions ---'
sed -n '350,455p' Generals/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp
sed -n '1600,1695p' Generals/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp
sed -n '1670,1760p' Generals/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp
sed -n '380,470p' Generals/Code/GameEngine/Include/GameLogic/Weapon.h
printf '%s\n' '--- GeneralsMD definitions ---'
sed -n '365,470p' GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp
sed -n '1720,1885p' GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp
sed -n '390,480p' GeneralsMD/Code/GameEngine/Include/GameLogic/Weapon.h
printf '%s\n' '--- DEBUG behavior ---'
sed -n '175,212p' Core/GameEngine/Include/Common/Debug.h
printf '%s\n' '--- postProcessLoad callers ---'
rg -n -F -- 'postProcessLoad()' Generals/Code/GameEngine GeneralsMD/Code/GameEngine | rg 'Weapon|weapon|TheWeaponStore' || true

Repository: TheSuperHackers/GeneralsGameCode

Length of output: 38439


🏁 Script executed:

set -e
printf '%s\n' '--- exact Generals override and store blocks ---'
nl -ba Generals/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp | sed -n '1435,1585p'
nl -ba Generals/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp | sed -n '1600,1710p'
nl -ba Generals/Code/GameEngine/Include/GameLogic/Weapon.h | sed -n '430,490p;790,890p'
printf '%s\n' '--- exact GeneralsMD override and store blocks ---'
nl -ba GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp | sed -n '1570,1720p'
nl -ba GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp | sed -n '1740,1845p'
nl -ba GeneralsMD/Code/GameEngine/Include/GameLogic/Weapon.h | sed -n '440,500p;815,915p'
printf '%s\n' '--- reset and map/solo load callers ---'
rg -n -F -- 'TheWeaponStore->reset' Generals/Code GeneralsMD/Code || true
rg -n -F -- 'resetWeaponTemplates' Generals/Code GeneralsMD/Code || true
rg -n -F -- 'loadMapINI' Generals/Code/GameEngine/Source GeneralsMD/Code/GameEngine/Source
rg -n -F -- 'loadMapINI(' Generals/Code/GameEngine/Source GeneralsMD/Code/GameEngine/Source

Repository: TheSuperHackers/GeneralsGameCode

Length of output: 41069


🏁 Script executed:

set -e
printf '%s\n' '--- complete newOverride bodies ---'
nl -ba Generals/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp | sed -n '1568,1600p'
nl -ba GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp | sed -n '1703,1740p'
printf '%s\n' '--- override helper symbols ---'
rg -n -F -- 'getFinalOverride' Generals/Code/GameEngine/Include Generals/Code/GameEngine/Source GeneralsMD/Code/GameEngine/Include GeneralsMD/Code/GameEngine/Source
rg -n -F -- 'deleteOverrides' Generals/Code/GameEngine/Include Generals/Code/GameEngine/Source GeneralsMD/Code/GameEngine/Include GeneralsMD/Code/GameEngine/Source
rg -n -F -- 'setNextOverride' Generals/Code/GameEngine/Include Generals/Code/GameEngine/Source GeneralsMD/Code/GameEngine/Include GeneralsMD/Code/GameEngine/Source
rg -n -F -- 'friend_setNextTemplate' Generals/Code/GameEngine/Include Generals/Code/GameEngine/Source GeneralsMD/Code/GameEngine/Include GeneralsMD/Code/GameEngine/Source
printf '%s\n' '--- map load entrypoints and surrounding lifecycle ---'
nl -ba Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp | sed -n '1140,1205p;2140,2240p'
nl -ba GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp | sed -n '1300,1370p;2445,2545p'
printf '%s\n' '--- subsystem reset dispatch ---'
rg -n -F -- 'reset()' Core/GameEngine/Source Generals/Code/GameEngine/Source/GameLogic GeneralsMD/Code/GameEngine/Source/GameLogic | rg 'Subsystem|reset' || true
rg -n -F -- 'SubsystemInterface' Core/GameEngine/Source Core/GameEngine/Include | head -80

Repository: TheSuperHackers/GeneralsGameCode

Length of output: 41757


Defer post-processing for weapon overrides until map loading finishes.

When RETAIL_COMPATIBLE_SCRIPTING is disabled, a [Weapon] override is post-processed before later [Object] blocks are parsed. A forward-referenced projectile therefore caches nullptr and enters DEBUG_ASSERTCRASH. The cached null remains in non-crashing builds.

Calling the existing WeaponStore::postProcessLoad() after map.ini and solo.ini is not sufficient. newOverride() links the override through setNextOverride() but does not add it to m_weaponTemplateVector; the existing loop visits only vector entries. Add a deferred pass that processes each base template's final override, or retain pending override pointers. Do not reprocess only the base templates.

Suggested fix
--- a/Generals/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp
+++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp
@@
-#if !RETAIL_COMPATIBLE_SCRIPTING
-	if (ini->getLoadType() == INI_LOAD_CREATE_OVERRIDES)
-	{
-		weapon->postProcessLoad();
-	}
-#endif
-
 //-------------------------------------------------------------------------------------------------
 void WeaponStore::postProcessLoad()
@@
 }
 
+void WeaponStore::postProcessOverrideLoad()
+{
+#if !RETAIL_COMPATIBLE_SCRIPTING
+	for (size_t i = 0; i < m_weaponTemplateVector.size(); ++i)
+	{
+		WeaponTemplate* wt = m_weaponTemplateVector[i];
+		WeaponTemplate* finalOverride =
+			static_cast<WeaponTemplate*>(wt->friend_getFinalOverride());
+		if (finalOverride != wt)
+			finalOverride->postProcessLoad();
+	}
+#endif
+}

Apply the same change in GeneralsMD, and call postProcessOverrideLoad() after both map.ini and solo.ini have been loaded.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#if !RETAIL_COMPATIBLE_SCRIPTING
if (ini->getLoadType() == INI_LOAD_CREATE_OVERRIDES)
{
weapon->postProcessLoad();
}
#endif

@Mauller Mauller changed the title bugfix(weapon) allow map/solo.ini overrides bugfix(WeaponTemplate): Fix the override handling and resetting of modified weapon templates Oct 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant