From 5cfb8a932166c0346c8324a0b654d38586062348 Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Sat, 3 Oct 2026 15:56:15 +0200 Subject: [PATCH 1/5] bugfix(saveload): Fix the unconditional xfer version bump for 'm_status'. --- .../Code/GameEngine/Include/Common/BitFlags.h | 4 +-- .../Source/GameLogic/Object/Object.cpp | 27 ++++++++++++------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/Generals/Code/GameEngine/Include/Common/BitFlags.h b/Generals/Code/GameEngine/Include/Common/BitFlags.h index 4878368ee22..8b212f062c9 100644 --- a/Generals/Code/GameEngine/Include/Common/BitFlags.h +++ b/Generals/Code/GameEngine/Include/Common/BitFlags.h @@ -162,9 +162,9 @@ class BitFlags return (m_bits & that.m_bits).none(); } - Int size() const + static constexpr Int size() { - return m_bits.size(); + return NUMBITS; } Int count() const diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp index 7aa4a442db6..7d518e8ece2 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp @@ -3573,14 +3573,18 @@ void Object::crc( Xfer *xfer ) * 5: m_isReceivingDifficultyBonus * 6: We do indeed need to save m_containedBy. The comment misrepresents what the contain module will do. * 7: save full mtx, not pos+orient. - * 8: Kris: Conversion of object status bits from UnsignedInt to BitFlags<> + * 8: TheSuperHackers @tweak Conversion of object status bits from UnsignedInt to BitFlags<> (backported from Zero Hour). */ //------------------------------------------------------------------------------------------------- void Object::xfer( Xfer *xfer ) { // version +#if RETAIL_COMPATIBLE_XFER_SAVE + const XferVersion currentVersion = 7; +#else const XferVersion currentVersion = 8; +#endif XferVersion version = currentVersion; xfer->xferVersion( &version, currentVersion ); @@ -3643,20 +3647,23 @@ void Object::xfer( Xfer *xfer ) } else { - //We are loading an old version, so we must convert it from a 32-bit int to a bitflag - UnsignedInt oldStatus; - xfer->xferUnsignedInt( &oldStatus ); + static_assert(m_status.size() == OBJECT_STATUS_COUNT && OBJECT_STATUS_COUNT == 29 + 16, + "Generals object status count was 29 originally & 16 types from Zero Hour"); + + // TheSuperHackers @info Ignore the upper 3 bits because they're unused in Generals. + // Bitset / 32-bit int conversion is offset by one bit for OBJECT_STATUS_NONE (0). + UnsignedInt status = (m_status.toUnsignedInt() & 0x1FFFFFFF) >> 1; + xfer->xferUnsignedInt(&status); - //Clear our status m_status.clear(); - for( int i = 0; i < 32; i++ ) + for( int i = 0; i < 29; i++ ) { - UnsignedInt bit = 1< Date: Sat, 3 Oct 2026 15:56:38 +0200 Subject: [PATCH 2/5] bugfix(saveload): Fix changes to the disabled types (m_disabledTillFrame). --- .../GameEngine/Source/GameLogic/Object/Object.cpp | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp index 7d518e8ece2..ec0273f6bd8 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp @@ -3574,6 +3574,7 @@ void Object::crc( Xfer *xfer ) * 6: We do indeed need to save m_containedBy. The comment misrepresents what the contain module will do. * 7: save full mtx, not pos+orient. * 8: TheSuperHackers @tweak Conversion of object status bits from UnsignedInt to BitFlags<> (backported from Zero Hour). + * TheSuperHackers @tweak Serialize all disabled types (m_disabledTillFrame) including Zero Hour only types. */ //------------------------------------------------------------------------------------------------- void Object::xfer( Xfer *xfer ) @@ -3714,7 +3715,19 @@ void Object::xfer( Xfer *xfer ) } // disabled till frame - xfer->xferUser( m_disabledTillFrame, sizeof( UnsignedInt ) * DISABLED_COUNT ); + if (version <= 7) + { + static_assert(ARRAYSIZE(m_disabledTillFrame) == DISABLED_COUNT && DISABLED_COUNT == 10 + 3, + "Generals disabled count was 10 originally & 3 types from Zero Hour"); + + xfer->xferUser(m_disabledTillFrame, 8 * sizeof(UnsignedInt)); + xfer->xferUnsignedInt(&m_disabledTillFrame[11]); + xfer->xferUnsignedInt(&m_disabledTillFrame[12]); + } + else + { + xfer->xferUser(m_disabledTillFrame, sizeof(m_disabledTillFrame)); + } // OK, now that we have xferred our status bits and disabled data, it's safe to set the team... // TheSuperHackers @todo Refactor so that this code can be moved to loadPostProcess. From 9269923853295469f06dadb6c0e9bb1cea856f10 Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Sat, 3 Oct 2026 17:08:26 +0200 Subject: [PATCH 3/5] Addressed feedback (1). --- .../Code/GameEngine/Include/Common/BitFlags.h | 4 +- .../Source/GameLogic/Object/Object.cpp | 42 ++++++++++++------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/Generals/Code/GameEngine/Include/Common/BitFlags.h b/Generals/Code/GameEngine/Include/Common/BitFlags.h index 8b212f062c9..4878368ee22 100644 --- a/Generals/Code/GameEngine/Include/Common/BitFlags.h +++ b/Generals/Code/GameEngine/Include/Common/BitFlags.h @@ -162,9 +162,9 @@ class BitFlags return (m_bits & that.m_bits).none(); } - static constexpr Int size() + Int size() const { - return NUMBITS; + return m_bits.size(); } Int count() const diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp index ec0273f6bd8..11e894f4b42 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp @@ -3581,7 +3581,7 @@ void Object::xfer( Xfer *xfer ) { // version -#if RETAIL_COMPATIBLE_XFER_SAVE +#if RTS_GENERALS && RETAIL_COMPATIBLE_XFER_SAVE const XferVersion currentVersion = 7; #else const XferVersion currentVersion = 8; @@ -3648,17 +3648,19 @@ void Object::xfer( Xfer *xfer ) } else { - static_assert(m_status.size() == OBJECT_STATUS_COUNT && OBJECT_STATUS_COUNT == 29 + 16, - "Generals object status count was 29 originally & 16 types from Zero Hour"); + // TheSuperHackers @info Originally OBJECT_STATUS_IS_CARBOMB (28) + 1 would equal OBJECT_STATUS_COUNT, + // but the enum has been expanded with Zero Hour specific entries. Ignore the upper 3 bits as they're unused. + constexpr const UnsignedInt count = OBJECT_STATUS_IS_CARBOMB + 1; + constexpr const UnsignedInt mask = (1 << count) - 1; + static_assert(count == 29, "This code needs to be updated when inserting new entries into ObjectStatusTypes"); - // TheSuperHackers @info Ignore the upper 3 bits because they're unused in Generals. // Bitset / 32-bit int conversion is offset by one bit for OBJECT_STATUS_NONE (0). - UnsignedInt status = (m_status.toUnsignedInt() & 0x1FFFFFFF) >> 1; + UnsignedInt status = (m_status.toUnsignedInt() & mask) >> 1; xfer->xferUnsignedInt(&status); m_status.clear(); - for( int i = 0; i < 29; i++ ) + for( int i = 0; i < count; i++ ) { UnsignedInt bit = 1u<xferUnsignedInt(&m_disabledTillFrame[DISABLED_DEFAULT]); + xfer->xferUnsignedInt(&m_disabledTillFrame[DISABLED_HACKED]); + xfer->xferUnsignedInt(&m_disabledTillFrame[DISABLED_EMP]); + xfer->xferUnsignedInt(&m_disabledTillFrame[DISABLED_HELD]); + xfer->xferUnsignedInt(&m_disabledTillFrame[DISABLED_PARALYZED]); + xfer->xferUnsignedInt(&m_disabledTillFrame[DISABLED_UNMANNED]); + xfer->xferUnsignedInt(&m_disabledTillFrame[DISABLED_UNDERPOWERED]); + xfer->xferUnsignedInt(&m_disabledTillFrame[DISABLED_FREEFALL]); - xfer->xferUser(m_disabledTillFrame, 8 * sizeof(UnsignedInt)); - xfer->xferUnsignedInt(&m_disabledTillFrame[11]); - xfer->xferUnsignedInt(&m_disabledTillFrame[12]); - } - else - { - xfer->xferUser(m_disabledTillFrame, sizeof(m_disabledTillFrame)); + if (version >= 8) + { + // TheSuperHackers @info These 3 types are Zero Hour specific, but inserted in the middle of the enum. + xfer->xferUnsignedInt(&m_disabledTillFrame[DISABLED_AWESTRUCK]); + xfer->xferUnsignedInt(&m_disabledTillFrame[DISABLED_BRAINWASHED]); + xfer->xferUnsignedInt(&m_disabledTillFrame[DISABLED_SUBDUED]); + } + + xfer->xferUnsignedInt(&m_disabledTillFrame[DISABLED_SCRIPT_DISABLED]); + xfer->xferUnsignedInt(&m_disabledTillFrame[DISABLED_SCRIPT_UNDERPOWERED]); } // OK, now that we have xferred our status bits and disabled data, it's safe to set the team... From ea8a75d171ab5ba769e9d56184b4256cc5a12d34 Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Sat, 3 Oct 2026 17:39:43 +0200 Subject: [PATCH 4/5] Addressed feedback (2). --- .../Source/GameLogic/Object/Object.cpp | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp index 11e894f4b42..a20dec2bb29 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp @@ -3654,7 +3654,7 @@ void Object::xfer( Xfer *xfer ) constexpr const UnsignedInt mask = (1 << count) - 1; static_assert(count == 29, "This code needs to be updated when inserting new entries into ObjectStatusTypes"); - // Bitset / 32-bit int conversion is offset by one bit for OBJECT_STATUS_NONE (0). + // bitset / 32-bit int conversion is offset by one bit for OBJECT_STATUS_NONE (0) UnsignedInt status = (m_status.toUnsignedInt() & mask) >> 1; xfer->xferUnsignedInt(&status); @@ -3718,6 +3718,21 @@ void Object::xfer( Xfer *xfer ) // disabled till frame { + static_assert(DISABLED_DEFAULT == 0, "Unexpected enum value"); + static_assert(DISABLED_HACKED == 1, "Unexpected enum value"); + static_assert(DISABLED_EMP == 2, "Unexpected enum value"); + static_assert(DISABLED_HELD == 3, "Unexpected enum value"); + static_assert(DISABLED_PARALYZED == 4, "Unexpected enum value"); + static_assert(DISABLED_UNMANNED == 5, "Unexpected enum value"); + static_assert(DISABLED_UNDERPOWERED == 6, "Unexpected enum value"); + static_assert(DISABLED_FREEFALL == 7, "Unexpected enum value"); + static_assert(DISABLED_AWESTRUCK == 8, "Unexpected enum value"); + static_assert(DISABLED_BRAINWASHED == 9, "Unexpected enum value"); + static_assert(DISABLED_SUBDUED == 10, "Unexpected enum value"); + static_assert(DISABLED_SCRIPT_DISABLED == 11, "Unexpected enum value"); + static_assert(DISABLED_SCRIPT_UNDERPOWERED == 12, "Unexpected enum value"); + static_assert(DISABLED_COUNT == 13, "Unexpected enum value"); + xfer->xferUnsignedInt(&m_disabledTillFrame[DISABLED_DEFAULT]); xfer->xferUnsignedInt(&m_disabledTillFrame[DISABLED_HACKED]); xfer->xferUnsignedInt(&m_disabledTillFrame[DISABLED_EMP]); From 55e357090736da67fb0809fd25d0abbac55e64ee Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Sat, 3 Oct 2026 22:23:05 +0200 Subject: [PATCH 5/5] Addressed feedback (3). --- .../Code/GameEngine/Source/GameLogic/Object/Object.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp index a20dec2bb29..bd2b73b96a9 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp @@ -3573,8 +3573,9 @@ void Object::crc( Xfer *xfer ) * 5: m_isReceivingDifficultyBonus * 6: We do indeed need to save m_containedBy. The comment misrepresents what the contain module will do. * 7: save full mtx, not pos+orient. - * 8: TheSuperHackers @tweak Conversion of object status bits from UnsignedInt to BitFlags<> (backported from Zero Hour). - * TheSuperHackers @tweak Serialize all disabled types (m_disabledTillFrame) including Zero Hour only types. + * 8: Kris: Conversion of object status bits from UnsignedInt to BitFlags<>. Added in Zero Hour + * TheSuperHackers @tweak Serialize all object status types (m_status) and disabled types (m_disabledTillFrame) + * including Zero Hour specific entries. */ //------------------------------------------------------------------------------------------------- void Object::xfer( Xfer *xfer ) @@ -3654,7 +3655,7 @@ void Object::xfer( Xfer *xfer ) constexpr const UnsignedInt mask = (1 << count) - 1; static_assert(count == 29, "This code needs to be updated when inserting new entries into ObjectStatusTypes"); - // bitset / 32-bit int conversion is offset by one bit for OBJECT_STATUS_NONE (0) + // shift by one bit: OBJECT_STATUS_NONE (0) has a dedicated bit in the bitset but not in the integer UnsignedInt status = (m_status.toUnsignedInt() & mask) >> 1; xfer->xferUnsignedInt(&status);