From 6be1aa73f48f790ee82e9b3a3aa20fd26e9f8b67 Mon Sep 17 00:00:00 2001 From: bobtista Date: Tue, 20 Jan 2026 17:58:52 -0600 Subject: [PATCH 1/4] bugfix(savegame): Fix weapon timing corruption after load by comparing WeaponSet flags instead of pointer Co-Authored-By: Claude Opus 4.5 --- .../Source/GameLogic/Object/WeaponSet.cpp | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/WeaponSet.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/WeaponSet.cpp index 96b6da59e4..8b77c3f8f3 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/WeaponSet.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/WeaponSet.cpp @@ -296,7 +296,22 @@ void WeaponSet::updateWeaponSet(const Object* obj) { const WeaponTemplateSet* set = obj->getTemplate()->findWeaponTemplateSet(obj->getWeaponSetFlags()); DEBUG_ASSERTCRASH(set, ("findWeaponSet should never return null")); - if (set && set != m_curWeaponTemplateSet) + // TheSuperHackers @bugfix bobtista 20/01/2026 After checkpoint load, the m_curWeaponTemplateSet pointer + // may differ from set even though they represent the same weapon set (pointer aliasing after load). + // Compare by flags instead of by pointer to avoid unnecessary weapon reallocation which corrupts + // weapon timing state and causes CRC mismatches during replay. + Bool needsUpdate = set && set != m_curWeaponTemplateSet; + if (needsUpdate && m_curWeaponTemplateSet) + { + // If flags match, the weapon sets are logically equivalent - no need to reallocate + if (obj->getWeaponSetFlags() == m_curWeaponTemplateSet->friend_getWeaponSetFlags()) + { + // Just update the pointer to the correct address without reallocating weapons + m_curWeaponTemplateSet = set; + needsUpdate = false; + } + } + if (needsUpdate) { if( ! set->isWeaponLockSharedAcrossSets() ) { From dea62221ca5d59ab10c6729f19cc0562c858707f Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Fri, 23 Jan 2026 10:27:38 -0800 Subject: [PATCH 2/4] bugfix(savegame): Fix weapon timing corruption by using getFinalOverride in xfer load --- .../Source/GameLogic/Object/WeaponSet.cpp | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/WeaponSet.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/WeaponSet.cpp index 8b77c3f8f3..5809c7398e 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/WeaponSet.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/WeaponSet.cpp @@ -232,6 +232,9 @@ void WeaponSet::xfer( Xfer *xfer ) if (tt == nullptr) throw INI_INVALID_DATA; + // TheSuperHackers @bugfix bobtista 23/01/2026 Use final override to match what Object uses. + tt = (const ThingTemplate*)tt->getFinalOverride(); + m_curWeaponTemplateSet = tt->findWeaponTemplateSet(wsFlags); if (m_curWeaponTemplateSet == nullptr) throw INI_INVALID_DATA; @@ -296,22 +299,7 @@ void WeaponSet::updateWeaponSet(const Object* obj) { const WeaponTemplateSet* set = obj->getTemplate()->findWeaponTemplateSet(obj->getWeaponSetFlags()); DEBUG_ASSERTCRASH(set, ("findWeaponSet should never return null")); - // TheSuperHackers @bugfix bobtista 20/01/2026 After checkpoint load, the m_curWeaponTemplateSet pointer - // may differ from set even though they represent the same weapon set (pointer aliasing after load). - // Compare by flags instead of by pointer to avoid unnecessary weapon reallocation which corrupts - // weapon timing state and causes CRC mismatches during replay. - Bool needsUpdate = set && set != m_curWeaponTemplateSet; - if (needsUpdate && m_curWeaponTemplateSet) - { - // If flags match, the weapon sets are logically equivalent - no need to reallocate - if (obj->getWeaponSetFlags() == m_curWeaponTemplateSet->friend_getWeaponSetFlags()) - { - // Just update the pointer to the correct address without reallocating weapons - m_curWeaponTemplateSet = set; - needsUpdate = false; - } - } - if (needsUpdate) + if (set && set != m_curWeaponTemplateSet) { if( ! set->isWeaponLockSharedAcrossSets() ) { From d9859c731179328a1bdd77398111fce1bb16cfd3 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Tue, 27 Jan 2026 08:16:20 -0800 Subject: [PATCH 3/4] fix(savegame): Address review feedback - use static_cast and improve comment --- .../Code/GameEngine/Source/GameLogic/Object/WeaponSet.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/WeaponSet.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/WeaponSet.cpp index 5809c7398e..9fbaaa65cd 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/WeaponSet.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/WeaponSet.cpp @@ -232,8 +232,10 @@ void WeaponSet::xfer( Xfer *xfer ) if (tt == nullptr) throw INI_INVALID_DATA; - // TheSuperHackers @bugfix bobtista 23/01/2026 Use final override to match what Object uses. - tt = (const ThingTemplate*)tt->getFinalOverride(); + // TheSuperHackers @fix bobtista 27/01/2026 findTemplate returns the base template, but Object + // uses getFinalOverride() in its constructor. We must do the same here so m_curWeaponTemplateSet + // points to the same ThingTemplate's weapon sets, avoiding unnecessary reallocation in updateWeaponSet. + tt = static_cast(tt->getFinalOverride()); m_curWeaponTemplateSet = tt->findWeaponTemplateSet(wsFlags); if (m_curWeaponTemplateSet == nullptr) From d182d6a2e0580b292560cb00d6f884ea8842f8be Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Tue, 27 Jan 2026 08:32:50 -0800 Subject: [PATCH 4/4] replicate to Generals --- .../Code/GameEngine/Source/GameLogic/Object/WeaponSet.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/WeaponSet.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/WeaponSet.cpp index 460e6d2511..15dfaaadd4 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/WeaponSet.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/WeaponSet.cpp @@ -225,6 +225,11 @@ void WeaponSet::xfer( Xfer *xfer ) if (tt == nullptr) throw INI_INVALID_DATA; + // TheSuperHackers @fix bobtista 27/01/2026 findTemplate returns the base template, but Object + // uses getFinalOverride() in its constructor. We must do the same here so m_curWeaponTemplateSet + // points to the same ThingTemplate's weapon sets, avoiding unnecessary reallocation in updateWeaponSet. + tt = static_cast(tt->getFinalOverride()); + m_curWeaponTemplateSet = tt->findWeaponTemplateSet(wsFlags); if (m_curWeaponTemplateSet == nullptr) throw INI_INVALID_DATA;