From 815130a34a99c576ce905b50ff63e1335c071dc9 Mon Sep 17 00:00:00 2001 From: Andi Date: Sat, 11 Jul 2026 15:51:26 +0200 Subject: [PATCH 1/4] Extra verancy levels, initial work --- Core/GameEngine/Include/Common/GameCommon.h | 4 +- .../Source/Common/System/GameCommon.cpp | 2 + .../GameEngine/Include/Common/ModelState.h | 4 ++ .../GameEngine/Include/Common/ThingTemplate.h | 3 + .../GameEngine/Include/GameLogic/ArmorSet.h | 4 ++ .../GameLogic/WeaponBonusConditionType.h | 4 ++ .../GameEngine/Include/GameLogic/WeaponSet.h | 4 +- .../Include/GameLogic/WeaponSetType.h | 5 ++ .../GameEngine/Source/Common/BitFlags.cpp | 9 ++- .../Source/Common/System/Upgrade.cpp | 6 ++ .../Source/Common/Thing/ThingTemplate.cpp | 58 +++++++++++++++++-- .../GameLogic/Object/Body/ActiveBody.cpp | 22 ++++--- .../Source/GameLogic/Object/Object.cpp | 39 +++++++------ .../Source/GameLogic/Object/Weapon.cpp | 12 ++++ .../Source/GameLogic/Object/WeaponSet.cpp | 3 + 15 files changed, 145 insertions(+), 34 deletions(-) diff --git a/Core/GameEngine/Include/Common/GameCommon.h b/Core/GameEngine/Include/Common/GameCommon.h index 52a21083197..74954aa6de5 100644 --- a/Core/GameEngine/Include/Common/GameCommon.h +++ b/Core/GameEngine/Include/Common/GameCommon.h @@ -201,12 +201,14 @@ enum VeterancyLevel CPP_11(: Int) LEVEL_VETERAN, LEVEL_ELITE, LEVEL_HEROIC, + LEVEL_FOUR, + LEVEL_FIVE, LEVEL_COUNT, LEVEL_INVALID, LEVEL_FIRST = 0, - LEVEL_LAST = LEVEL_HEROIC + LEVEL_LAST = LEVEL_FIVE }; // TheVeterancyNames is defined in GameCommon.cpp diff --git a/Core/GameEngine/Source/Common/System/GameCommon.cpp b/Core/GameEngine/Source/Common/System/GameCommon.cpp index 50a9066cdb6..8eaca63d541 100644 --- a/Core/GameEngine/Source/Common/System/GameCommon.cpp +++ b/Core/GameEngine/Source/Common/System/GameCommon.cpp @@ -36,6 +36,8 @@ const char *const TheVeterancyNames[] = "VETERAN", "ELITE", "HEROIC", + "FOUR", + "FIVE", nullptr }; static_assert(ARRAY_SIZE(TheVeterancyNames) == LEVEL_COUNT + 1, "Incorrect array size"); diff --git a/GeneralsMD/Code/GameEngine/Include/Common/ModelState.h b/GeneralsMD/Code/GameEngine/Include/Common/ModelState.h index 21eef862cee..99f7fd4ffbe 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/ModelState.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/ModelState.h @@ -288,6 +288,10 @@ enum ModelConditionFlagType CPP_11(: Int) // Ship death MODELCONDITION_SHIP_TOPPLING, MODELCONDITION_SHIP_SINKING, + + // Veterancy ranks beyond HEROIC (appended to preserve existing save values). + MODELCONDITION_WEAPONSET_FOUR, + MODELCONDITION_WEAPONSET_FIVE, // // Note: these values are saved in save files, so you MUST NOT REMOVE OR CHANGE // existing values! diff --git a/GeneralsMD/Code/GameEngine/Include/Common/ThingTemplate.h b/GeneralsMD/Code/GameEngine/Include/Common/ThingTemplate.h index dcfd219b73d..b9212e684fa 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/ThingTemplate.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/ThingTemplate.h @@ -699,6 +699,9 @@ class ThingTemplate : public Overridable static void parsePrerequisites( INI* ini, void *instance, void * /*store*/, const void* /*userData*/ ); static void parseModuleName(INI* ini, void *instance, void* /*store*/, const void* userData); static void parseIntList(INI* ini, void *instance, void* store, const void* userData); + static void parseExperienceValueList(INI* ini, void *instance, void* store, const void* userData); + static void parseExperienceRequiredList(INI* ini, void *instance, void* store, const void* userData); + static void parseSkillPointValueList(INI* ini, void *instance, void* store, const void* userData); static void parsePerUnitSounds(INI* ini, void *instance, void* store, const void* userData); static void parsePerUnitFX(INI* ini, void *instance, void* store, const void* userData); diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/ArmorSet.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/ArmorSet.h index 528fd55165d..4032b2b98a0 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/ArmorSet.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/ArmorSet.h @@ -55,6 +55,10 @@ enum ArmorSetType CPP_11(: Int) ARMORSET_PLAYER_UPGRADE3, ARMORSET_PLAYER_UPGRADE4, + // Veterancy ranks beyond HEROIC. Appended (not inserted) to preserve existing save values. + ARMORSET_FOUR, + ARMORSET_FIVE, + ARMORSET_COUNT }; diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/WeaponBonusConditionType.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/WeaponBonusConditionType.h index 433ed78c55d..b72160d47b9 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/WeaponBonusConditionType.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/WeaponBonusConditionType.h @@ -99,6 +99,10 @@ enum WeaponBonusConditionType CPP_11(: Int) WEAPONBONUSCONDITION_EXTRA7, WEAPONBONUSCONDITION_EXTRA8, + // Veterancy ranks beyond HEROIC. Appended (not inserted) to preserve existing save values. + WEAPONBONUSCONDITION_VETERANCY_FOUR, + WEAPONBONUSCONDITION_VETERANCY_FIVE, + WEAPONBONUSCONDITION_COUNT }; diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/WeaponSet.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/WeaponSet.h index 88a60b94fb3..6788e75871f 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/WeaponSet.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/WeaponSet.h @@ -116,7 +116,9 @@ static const ModelConditionFlagType TheWeaponSetTypeToModelConditionTypeMap[WEAP /*WEAPONSET_PLAYER_UPGRADE3*/ MODELCONDITION_WEAPONSET_PLAYER_UPGRADE3, /*WEAPONSET_PLAYER_UPGRADE4*/ MODELCONDITION_WEAPONSET_PLAYER_UPGRADE4, /*WEAPONSET_GARRISONED*/ MODELCONDITION_INVALID, //No actual conditionstates needed for Garrisoned and contained - /*WEAPONSET_CONTAINED*/ MODELCONDITION_INVALID + /*WEAPONSET_CONTAINED*/ MODELCONDITION_INVALID, + /*WEAPONSET_FOUR*/ MODELCONDITION_WEAPONSET_FOUR, + /*WEAPONSET_FIVE*/ MODELCONDITION_WEAPONSET_FIVE }; #endif diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/WeaponSetType.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/WeaponSetType.h index 617c99d31d4..c63af0a2c6d 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/WeaponSetType.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/WeaponSetType.h @@ -64,5 +64,10 @@ enum WeaponSetType CPP_11(: Int) WEAPONSET_GARRISONED, WEAPONSET_CONTAINED, + // Veterancy ranks beyond HEROIC. Appended (not inserted) to preserve existing save values and the + // positional TheWeaponSetTypeToModelConditionTypeMap ordering. + WEAPONSET_FOUR, + WEAPONSET_FIVE, + WEAPONSET_COUNT }; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/BitFlags.cpp b/GeneralsMD/Code/GameEngine/Source/Common/BitFlags.cpp index 3a9d105c22d..43837823777 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/BitFlags.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/BitFlags.cpp @@ -220,7 +220,10 @@ const char* const ModelConditionFlags::s_bitNameList[] = "SHIP_TOPPLING", "SHIP_SINKING", - + + "WEAPONSET_FOUR", + "WEAPONSET_FIVE", + nullptr }; static_assert(ARRAY_SIZE(ModelConditionFlags::s_bitNameList) == ModelConditionFlags::NumBits + 1, "Incorrect array size"); @@ -239,6 +242,8 @@ const char* const ArmorSetFlags::s_bitNameList[] = "PLAYER_UPGRADE2", "PLAYER_UPGRADE3", "PLAYER_UPGRADE4", + "FOUR", + "FIVE", nullptr }; @@ -304,6 +309,8 @@ const char* const WeaponBonusConditionFlags::s_bitNameList[] = "EXTRA6", "EXTRA7", "EXTRA8", + "VETERANCY_FOUR", + "VETERANCY_FIVE", nullptr }; static_assert(ARRAY_SIZE(WeaponBonusConditionFlags::s_bitNameList) == WEAPONBONUSCONDITION_COUNT + 1, "Incorrect array size"); diff --git a/GeneralsMD/Code/GameEngine/Source/Common/System/Upgrade.cpp b/GeneralsMD/Code/GameEngine/Source/Common/System/Upgrade.cpp index e5733ad66db..ade361fbeb2 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/System/Upgrade.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/System/Upgrade.cpp @@ -282,6 +282,12 @@ void UpgradeCenter::init( void ) up = newUpgrade(""); up->friend_makeVeterancyUpgrade(LEVEL_HEROIC); + up = newUpgrade(""); + up->friend_makeVeterancyUpgrade(LEVEL_FOUR); + + up = newUpgrade(""); + up->friend_makeVeterancyUpgrade(LEVEL_FIVE); + } //------------------------------------------------------------------------------------------------- diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Thing/ThingTemplate.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Thing/ThingTemplate.cpp index 38b8660a316..40826fd32a1 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Thing/ThingTemplate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Thing/ThingTemplate.cpp @@ -128,9 +128,9 @@ const FieldParse ThingTemplate::s_objectFieldParseTable[] = { "FactoryExitWidth", INI::parseReal, nullptr, offsetof( ThingTemplate, m_factoryExitWidth ) }, { "FactoryExtraBibWidth", INI::parseReal, nullptr, offsetof( ThingTemplate, m_factoryExtraBibWidth ) }, - { "SkillPointValue", ThingTemplate::parseIntList, (void*)LEVEL_COUNT, offsetof( ThingTemplate, m_skillPointValues ) }, - { "ExperienceValue", ThingTemplate::parseIntList, (void*)LEVEL_COUNT, offsetof( ThingTemplate, m_experienceValues ) }, - { "ExperienceRequired", ThingTemplate::parseIntList, (void*)LEVEL_COUNT, offsetof( ThingTemplate, m_experienceRequired ) }, + { "SkillPointValue", ThingTemplate::parseSkillPointValueList, nullptr, offsetof( ThingTemplate, m_skillPointValues ) }, + { "ExperienceValue", ThingTemplate::parseExperienceValueList, nullptr, offsetof( ThingTemplate, m_experienceValues ) }, + { "ExperienceRequired", ThingTemplate::parseExperienceRequiredList, nullptr, offsetof( ThingTemplate, m_experienceRequired ) }, { "IsTrainable", INI::parseBool, nullptr, offsetof( ThingTemplate, m_isTrainable ) }, { "EnterGuard", INI::parseBool, nullptr, offsetof( ThingTemplate, m_enterGuard ) }, { "HijackGuard", INI::parseBool, nullptr, offsetof( ThingTemplate, m_hijackGuard ) }, @@ -638,6 +638,54 @@ void ThingTemplate::parseIntList(INI* ini, void *instance, void* store, const vo } } +//------------------------------------------------------------------------------------------------- +/** Read a variable-length list of ints (up to LEVEL_COUNT) into a per-veterancy-level array. Returns + the number of values actually provided. Used by the veterancy experience parsers so that INI lines + which only specify the original four ranks still parse after LEVEL_FOUR/LEVEL_FIVE were added. */ +//------------------------------------------------------------------------------------------------- +static Int parseVeterancyIntList(INI* ini, Int* intList) +{ + Int count = 0; + for( const char* token = ini->getNextTokenOrNull(); token != nullptr && count < LEVEL_COUNT; token = ini->getNextTokenOrNull() ) + { + intList[count++] = INI::scanInt(token); + } + return count; +} + +//------------------------------------------------------------------------------------------------- +void ThingTemplate::parseExperienceValueList(INI* ini, void *instance, void* store, const void* userData) +{ + // Trailing (unspecified) levels inherit the last specified value, so a unit granted FOUR/FIVE is + // worth the same as its highest defined rank (normally HEROIC). + Int *intList = (Int*)store; + Int n = parseVeterancyIntList(ini, intList); + Int fill = (n > 0) ? intList[n - 1] : 0; + for( Int i = n; i < LEVEL_COUNT; ++i ) + intList[i] = fill; +} + +//------------------------------------------------------------------------------------------------- +void ThingTemplate::parseExperienceRequiredList(INI* ini, void *instance, void* store, const void* userData) +{ + // Trailing (unspecified) levels are unreachable by default (INT_MAX), so objects cannot climb into + // FOUR/FIVE naturally unless the INI explicitly provides a requirement for them. + Int *intList = (Int*)store; + Int n = parseVeterancyIntList(ini, intList); + for( Int i = n; i < LEVEL_COUNT; ++i ) + intList[i] = INT_MAX; +} + +//------------------------------------------------------------------------------------------------- +void ThingTemplate::parseSkillPointValueList(INI* ini, void *instance, void* store, const void* userData) +{ + // Trailing (unspecified) levels fall back to "use experience value" (which itself inherits HEROIC). + Int *intList = (Int*)store; + Int n = parseVeterancyIntList(ini, intList); + for( Int i = n; i < LEVEL_COUNT; ++i ) + intList[i] = USE_EXP_VALUE_FOR_SKILL_VALUE; +} + //------------------------------------------------------------------------------------------------- static void parsePrerequisiteUnit( INI* ini, void *instance, void * /*store*/, const void* /*userData*/ ) { @@ -1037,7 +1085,9 @@ ThingTemplate::ThingTemplate() : for( Int levelIndex = 0; levelIndex < LEVEL_COUNT; levelIndex++ ) { m_experienceValues[levelIndex] = 0; - m_experienceRequired[levelIndex] = 0; + // Levels beyond HEROIC (FOUR/FIVE) default to an unreachable experience requirement, so objects + // can never climb into them naturally -- they only apply when granted explicitly. + m_experienceRequired[levelIndex] = (levelIndex > LEVEL_HEROIC) ? INT_MAX : 0; // -1 means "same value as experienceValues for that level" m_skillPointValues[levelIndex] = USE_EXP_VALUE_FOR_SKILL_VALUE; } diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp index 22f88f1a2e3..20c5864a08b 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp @@ -1696,28 +1696,32 @@ void ActiveBody::onVeterancyLevelChanged( VeterancyLevel oldLevel, VeterancyLeve // now change the cur (setMaxHealth now handles it) //internalChangeHealth( newHealth - m_currentHealth ); + // Clear every veterancy armor-set flag first, then set the one for the new level, so promotions and + // demotions (including into/out of the new FOUR/FIVE ranks) stay clean. + clearArmorSetFlag(ARMORSET_VETERAN); + clearArmorSetFlag(ARMORSET_ELITE); + clearArmorSetFlag(ARMORSET_HERO); + clearArmorSetFlag(ARMORSET_FOUR); + clearArmorSetFlag(ARMORSET_FIVE); switch (newLevel) { case LEVEL_REGULAR: - clearArmorSetFlag(ARMORSET_VETERAN); - clearArmorSetFlag(ARMORSET_ELITE); - clearArmorSetFlag(ARMORSET_HERO); break; case LEVEL_VETERAN: setArmorSetFlag(ARMORSET_VETERAN); - clearArmorSetFlag(ARMORSET_ELITE); - clearArmorSetFlag(ARMORSET_HERO); break; case LEVEL_ELITE: - clearArmorSetFlag(ARMORSET_VETERAN); setArmorSetFlag(ARMORSET_ELITE); - clearArmorSetFlag(ARMORSET_HERO); break; case LEVEL_HEROIC: - clearArmorSetFlag(ARMORSET_VETERAN); - clearArmorSetFlag(ARMORSET_ELITE); setArmorSetFlag(ARMORSET_HERO); break; + case LEVEL_FOUR: + setArmorSetFlag(ARMORSET_FOUR); + break; + case LEVEL_FIVE: + setArmorSetFlag(ARMORSET_FIVE); + break; } } diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp index 9172f7850c1..4c8ac7d6291 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp @@ -3322,40 +3322,43 @@ void Object::onVeterancyLevelChanged( VeterancyLevel oldLevel, VeterancyLevel ne if (body) body->onVeterancyLevelChanged( oldLevel, newLevel, provideFeedback ); + // Clear every veterancy weapon-set flag / bonus condition first, then set the one for the new level. + // This keeps promotions and demotions (including into/out of the new FOUR/FIVE ranks) clean. + clearWeaponSetFlag(WEAPONSET_VETERAN); + clearWeaponSetFlag(WEAPONSET_ELITE); + clearWeaponSetFlag(WEAPONSET_HERO); + clearWeaponSetFlag(WEAPONSET_FOUR); + clearWeaponSetFlag(WEAPONSET_FIVE); + clearWeaponBonusCondition(WEAPONBONUSCONDITION_VETERAN); + clearWeaponBonusCondition(WEAPONBONUSCONDITION_ELITE); + clearWeaponBonusCondition(WEAPONBONUSCONDITION_HERO); + clearWeaponBonusCondition(WEAPONBONUSCONDITION_VETERANCY_FOUR); + clearWeaponBonusCondition(WEAPONBONUSCONDITION_VETERANCY_FIVE); + switch (newLevel) { case LEVEL_REGULAR: - clearWeaponSetFlag(WEAPONSET_VETERAN); - clearWeaponSetFlag(WEAPONSET_ELITE); - clearWeaponSetFlag(WEAPONSET_HERO); - clearWeaponBonusCondition(WEAPONBONUSCONDITION_VETERAN); - clearWeaponBonusCondition(WEAPONBONUSCONDITION_ELITE); - clearWeaponBonusCondition(WEAPONBONUSCONDITION_HERO); break; case LEVEL_VETERAN: setWeaponSetFlag(WEAPONSET_VETERAN); - clearWeaponSetFlag(WEAPONSET_ELITE); - clearWeaponSetFlag(WEAPONSET_HERO); setWeaponBonusCondition(WEAPONBONUSCONDITION_VETERAN); - clearWeaponBonusCondition(WEAPONBONUSCONDITION_ELITE); - clearWeaponBonusCondition(WEAPONBONUSCONDITION_HERO); break; case LEVEL_ELITE: - clearWeaponSetFlag(WEAPONSET_VETERAN); setWeaponSetFlag(WEAPONSET_ELITE); - clearWeaponSetFlag(WEAPONSET_HERO); - clearWeaponBonusCondition(WEAPONBONUSCONDITION_VETERAN); setWeaponBonusCondition(WEAPONBONUSCONDITION_ELITE); - clearWeaponBonusCondition(WEAPONBONUSCONDITION_HERO); break; case LEVEL_HEROIC: - clearWeaponSetFlag(WEAPONSET_VETERAN); - clearWeaponSetFlag(WEAPONSET_ELITE); setWeaponSetFlag(WEAPONSET_HERO); - clearWeaponBonusCondition(WEAPONBONUSCONDITION_VETERAN); - clearWeaponBonusCondition(WEAPONBONUSCONDITION_ELITE); setWeaponBonusCondition(WEAPONBONUSCONDITION_HERO); break; + case LEVEL_FOUR: + setWeaponSetFlag(WEAPONSET_FOUR); + setWeaponBonusCondition(WEAPONBONUSCONDITION_VETERANCY_FOUR); + break; + case LEVEL_FIVE: + setWeaponSetFlag(WEAPONSET_FIVE); + setWeaponBonusCondition(WEAPONBONUSCONDITION_VETERANCY_FIVE); + break; } Bool doAnimation = provideFeedback diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp index db591bf5c73..69c1366a91c 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp @@ -540,6 +540,18 @@ void WeaponTemplate::postProcessLoad() DEBUG_ASSERTCRASH(m_projectileTmpl, ("projectile %s not found!",m_projectileName.str())); } + // Veterancy ranks beyond HEROIC (FOUR/FIVE) inherit HEROIC's per-level FX/OCL/exhaust entries unless + // they were explicitly defined. Do the OCL name copy here, before the name->pointer resolution below. + for (Int i = LEVEL_HEROIC + 1; i <= LEVEL_LAST; ++i) + { + if (m_fireFXs[i] == nullptr) m_fireFXs[i] = m_fireFXs[LEVEL_HEROIC]; + if (m_projectileDetonateFXs[i] == nullptr) m_projectileDetonateFXs[i] = m_projectileDetonateFXs[LEVEL_HEROIC]; + if (m_projectileExhausts[i] == nullptr) m_projectileExhausts[i] = m_projectileExhausts[LEVEL_HEROIC]; + if (m_preAttackFXs[i] == nullptr) m_preAttackFXs[i] = m_preAttackFXs[LEVEL_HEROIC]; + if (m_fireOCLNames[i].isEmpty()) m_fireOCLNames[i] = m_fireOCLNames[LEVEL_HEROIC]; + if (m_projectileDetonationOCLNames[i].isEmpty()) m_projectileDetonationOCLNames[i] = m_projectileDetonationOCLNames[LEVEL_HEROIC]; + } + for (Int i = LEVEL_FIRST; i <= LEVEL_LAST; ++i) { // And the OCL if there is one diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/WeaponSet.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/WeaponSet.cpp index 3df7b2ecd29..3aa46237443 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/WeaponSet.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/WeaponSet.cpp @@ -90,6 +90,9 @@ const char* const WeaponSetFlags::s_bitNameList[] = "GARRISONED", "CONTAINED", + "FOUR", + "FIVE", + nullptr }; static_assert(ARRAY_SIZE(WeaponSetFlags::s_bitNameList) == WeaponSetFlags::NumBits + 1, "Incorrect array size"); From 3fa5a304bad35367b9eca67eacb953fea0863faf Mon Sep 17 00:00:00 2001 From: Andi Date: Sat, 11 Jul 2026 19:26:42 +0200 Subject: [PATCH 2/4] Bitflags number safeguard and veterancy++ --- .../GameEngine/Include/Common/ModelState.h | 5 +++ .../GameEngine/Source/Common/BitFlags.cpp | 38 +++++++++++++++++++ .../GameEngine/Source/Common/GlobalData.cpp | 2 + .../GameEngine/Source/GameClient/Drawable.cpp | 2 + 4 files changed, 47 insertions(+) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/ModelState.h b/GeneralsMD/Code/GameEngine/Include/Common/ModelState.h index 99f7fd4ffbe..9b149c76e19 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/ModelState.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/ModelState.h @@ -292,6 +292,11 @@ enum ModelConditionFlagType CPP_11(: Int) // Veterancy ranks beyond HEROIC (appended to preserve existing save values). MODELCONDITION_WEAPONSET_FOUR, MODELCONDITION_WEAPONSET_FIVE, + + // Reserved padding: keeps MODELCONDITION_COUNT distinct from KINDOF_COUNT. BitFlags is keyed only + // on its bit count, so two flag enums with an equal COUNT would share one name list (see the guard in + // BitFlags.cpp). Do not reuse this slot for a real condition without re-checking the counts. + MODELCONDITION_RESERVED_UNIQUE_SIZE_PAD, // // Note: these values are saved in save files, so you MUST NOT REMOVE OR CHANGE // existing values! diff --git a/GeneralsMD/Code/GameEngine/Source/Common/BitFlags.cpp b/GeneralsMD/Code/GameEngine/Source/Common/BitFlags.cpp index 43837823777..1f8052c659a 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/BitFlags.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/BitFlags.cpp @@ -37,6 +37,43 @@ #include "Common/ModelState.h" #include "GameLogic/ArmorSet.h" #include "GameLogic/WeaponBonusConditionFlags.h" +#include "Common/KindOf.h" +#include "GameLogic/WeaponSetType.h" +#include "Common/DisabledTypes.h" +#include "Common/ObjectStatusTypes.h" +#include "GameLogic/Damage.h" +#include "Common/SpecialPowerMaskType.h" +#include "Common/Upgrade.h" +#include "GameClient/TintStatus.h" + +//------------------------------------------------------------------------------------------------- +// BitFlags is templated ONLY on its bit count, so two enums with the same *_COUNT collapse to the +// SAME type and share a single static s_bitNameList. When that happens, INI parsing for one flag type +// silently looks up names in the other's list (e.g. a KindOf parse reading ModelCondition names). Guard +// against it: every BitFlags specialization's size must be unique. If this assert fires, nudge one enum's +// COUNT to a free value (e.g. add a reserved padding entry) -- see the free/used counts in this list. +//------------------------------------------------------------------------------------------------- +namespace +{ + constexpr int s_bitFlagsSizes[] = + { + KINDOF_COUNT, MODELCONDITION_COUNT, WEAPONBONUSCONDITION_COUNT, WEAPONSET_COUNT, + ARMORSET_COUNT, DISABLED_COUNT, OBJECT_STATUS_COUNT, DAMAGE_NUM_TYPES, + SPECIALPOWER_COUNT, UPGRADE_MAX_COUNT, TINT_STATUS_COUNT + }; + constexpr bool areBitFlagsSizesUnique() + { + for (size_t i = 0; i < ARRAY_SIZE(s_bitFlagsSizes); ++i) + for (size_t j = i + 1; j < ARRAY_SIZE(s_bitFlagsSizes); ++j) + if (s_bitFlagsSizes[i] == s_bitFlagsSizes[j]) + return false; + return true; + } + static_assert(areBitFlagsSizesUnique(), + "Two BitFlags<> specializations share the same bit count; equal counts collapse to the same type " + "and share one s_bitNameList, so INI parsing for one flag type will use another's names. Nudge one " + "enum's *_COUNT to a unique value (e.g. add a reserved padding entry)."); +} template<> const char* const ModelConditionFlags::s_bitNameList[] = @@ -223,6 +260,7 @@ const char* const ModelConditionFlags::s_bitNameList[] = "WEAPONSET_FOUR", "WEAPONSET_FIVE", + "RESERVED_UNIQUE_SIZE_PAD", nullptr }; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index b077bbf061c..c6fabd65a97 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -432,6 +432,8 @@ GlobalData* GlobalData::m_theOriginal = nullptr; { "HealthBonus_Veteran", INI::parsePercentToReal, nullptr, offsetof( GlobalData, m_healthBonus[LEVEL_VETERAN]) }, { "HealthBonus_Elite", INI::parsePercentToReal, nullptr, offsetof( GlobalData, m_healthBonus[LEVEL_ELITE]) }, { "HealthBonus_Heroic", INI::parsePercentToReal, nullptr, offsetof( GlobalData, m_healthBonus[LEVEL_HEROIC]) }, + { "HealthBonus_Four", INI::parsePercentToReal, nullptr, offsetof( GlobalData, m_healthBonus[LEVEL_FOUR]) }, + { "HealthBonus_Five", INI::parsePercentToReal, nullptr, offsetof( GlobalData, m_healthBonus[LEVEL_FIVE]) }, { "HumanSoloPlayerHealthBonus_Easy", INI::parsePercentToReal, nullptr, offsetof( GlobalData, m_soloPlayerHealthBonusForDifficulty[PLAYER_HUMAN][DIFFICULTY_EASY] ) }, { "HumanSoloPlayerHealthBonus_Normal", INI::parsePercentToReal, nullptr, offsetof( GlobalData, m_soloPlayerHealthBonusForDifficulty[PLAYER_HUMAN][DIFFICULTY_NORMAL] ) }, diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp index 225ef7818d5..96a92248256 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -327,6 +327,8 @@ const Int MAX_ENABLED_MODULES = 16; s_veterancyImage[1] = TheMappedImageCollection->findImageByName("SCVeter1"); s_veterancyImage[2] = TheMappedImageCollection->findImageByName("SCVeter2"); s_veterancyImage[3] = TheMappedImageCollection->findImageByName("SCVeter3"); + s_veterancyImage[4] = TheMappedImageCollection->findImageByName("SCVeter4"); + s_veterancyImage[5] = TheMappedImageCollection->findImageByName("SCVeter5"); s_fullAmmo = TheMappedImageCollection->findImageByName("SCPAmmoFull"); s_emptyAmmo = TheMappedImageCollection->findImageByName("SCPAmmoEmpty"); From 75a24c27652eba681e7f5ae8b66379db7d0703c8 Mon Sep 17 00:00:00 2001 From: Andi Date: Sat, 11 Jul 2026 21:19:47 +0200 Subject: [PATCH 3/4] Veterancy fixes and improvements --- .../Source/Common/System/GameCommon.cpp | 4 +-- .../GameEngine/Include/Common/ThingTemplate.h | 2 ++ .../Include/GameClient/ControlBar.h | 2 ++ .../Include/GameLogic/ExperienceTracker.h | 4 +++ .../Module/ExperienceScalarUpgrade.h | 1 + .../GameEngine/Include/GameLogic/Object.h | 2 ++ .../GameEngine/Source/Common/BitFlags.cpp | 8 ++--- .../Source/Common/Thing/ThingTemplate.cpp | 3 ++ .../GameClient/GUI/ControlBar/ControlBar.cpp | 4 +++ .../GUI/ControlBar/ControlBarCommand.cpp | 8 +++++ .../GameLogic/Object/ExperienceTracker.cpp | 29 ++++++++++++++++++- .../Source/GameLogic/Object/Object.cpp | 13 +++++++++ .../Upgrade/ExperienceScalarUpgrade.cpp | 6 ++++ .../Source/GameLogic/Object/WeaponSet.cpp | 4 +-- 14 files changed, 81 insertions(+), 9 deletions(-) diff --git a/Core/GameEngine/Source/Common/System/GameCommon.cpp b/Core/GameEngine/Source/Common/System/GameCommon.cpp index 8eaca63d541..9b4ef35f0b7 100644 --- a/Core/GameEngine/Source/Common/System/GameCommon.cpp +++ b/Core/GameEngine/Source/Common/System/GameCommon.cpp @@ -36,8 +36,8 @@ const char *const TheVeterancyNames[] = "VETERAN", "ELITE", "HEROIC", - "FOUR", - "FIVE", + "LEVEL_FOUR", + "LEVEL_FIVE", nullptr }; static_assert(ARRAY_SIZE(TheVeterancyNames) == LEVEL_COUNT + 1, "Incorrect array size"); diff --git a/GeneralsMD/Code/GameEngine/Include/Common/ThingTemplate.h b/GeneralsMD/Code/GameEngine/Include/Common/ThingTemplate.h index b9212e684fa..79c5bb01bf6 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/ThingTemplate.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/ThingTemplate.h @@ -509,6 +509,7 @@ class ThingTemplate : public Overridable Int getExperienceValue(Int level) const { return m_experienceValues[level]; } Int getExperienceRequired(Int level) const {return m_experienceRequired[level]; } + VeterancyLevel getMaxVeterancyLevel() const { return m_maxVeterancyLevel; } Bool isTrainable() const{return m_isTrainable; } Bool isEnterGuard() const{return m_enterGuard; } Bool isHijackGuard() const{return m_hijackGuard; } @@ -752,6 +753,7 @@ class ThingTemplate : public Overridable Int m_skillPointValues[LEVEL_COUNT]; Int m_experienceValues[LEVEL_COUNT]; ///< How much I am worth at each experience level Int m_experienceRequired[LEVEL_COUNT]; ///< How many experience points I need for each level + VeterancyLevel m_maxVeterancyLevel; ///< highest veterancy level this object may ever reach //Code renderer handles these states now. //AsciiString m_inventoryImage[ INV_IMAGE_NUM_IMAGES ]; ///< portrait inventory pictures diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/ControlBar.h b/GeneralsMD/Code/GameEngine/Include/GameClient/ControlBar.h index ad97421c322..5b986605ff0 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/ControlBar.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/ControlBar.h @@ -1018,6 +1018,8 @@ class ControlBar : public SubsystemInterface static const Image *m_rankVeteranIcon; static const Image *m_rankEliteIcon; static const Image *m_rankHeroicIcon; + static const Image *m_rankFourIcon; + static const Image *m_rankFiveIcon; const Image *m_generalButtonEnable; const Image *m_generalButtonHighlight; diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/ExperienceTracker.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/ExperienceTracker.h index e794de93538..ed55e547a33 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/ExperienceTracker.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/ExperienceTracker.h @@ -52,6 +52,9 @@ class ExperienceTracker : public MemoryPoolObject, public Snapshot void setVeterancyLevel( VeterancyLevel newLevel, Bool provideFeedback = TRUE ); ///< Set Level to this void setMinVeterancyLevel( VeterancyLevel newLevel, Bool provideFeedback = TRUE ); ///< Set Level to AT LEAST this... if we are already >= this level, do nothing. + + VeterancyLevel getMaxVeterancyLevel() const { return m_maxVeterancyLevel; } ///< Highest level this object may reach (hard cap, regardless of ExperienceRequired). + void setMaxVeterancyLevel( VeterancyLevel maxLevel, Bool provideFeedback = TRUE ); ///< Change the cap; demotes the current level if it now exceeds the cap. void addExperiencePoints( Int experienceGain, Bool canScaleForBonus = TRUE ); ///< Gain this many exp. Bool gainExpForLevel(Int levelsToGain, Bool canScaleForBonus = TRUE ); ///< Gain enough exp to gain a level. return false if can't gain a level. Bool canGainExpForLevel(Int levelsToGain) const; ///< return same value as gainExpForLevel, but don't change anything @@ -72,6 +75,7 @@ class ExperienceTracker : public MemoryPoolObject, public Snapshot private: Object* m_parent; ///< Object I am owned by VeterancyLevel m_currentLevel; ///< Level of experience + VeterancyLevel m_maxVeterancyLevel; ///< Hard cap on the level I can reach (from template, overridable) Int m_currentExperience; ///< Number of experience points ObjectID m_experienceSink; ///< ID of object I have pledged my experience point gains to Real m_experienceScalar; ///< Scales any experience gained by this multiplier. diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ExperienceScalarUpgrade.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ExperienceScalarUpgrade.h index 16ca70609e3..bd7afd7e41a 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ExperienceScalarUpgrade.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ExperienceScalarUpgrade.h @@ -49,6 +49,7 @@ class ExperienceScalarUpgradeModuleData: public UpgradeModuleData Bool m_initiallyActive; // Apply upgrade immediately Real m_addXPScalar; ///< Additive bonus to scalar for XP this unit gains Real m_addXPValueScalar; ///< Additive bonus to scalar for XP this unit gives when killed + VeterancyLevel m_setMaxVeterancyLevel; ///< if not LEVEL_INVALID, override the object's max veterancy cap }; diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h index 03d601d28e8..aa0cf3ddc06 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h @@ -242,6 +242,8 @@ class Object : public Thing, public Snapshot ExperienceTracker* getExperienceTracker() {return m_experienceTracker;} const ExperienceTracker* getExperienceTracker() const {return m_experienceTracker;} VeterancyLevel getVeterancyLevel() const; + VeterancyLevel getMaxVeterancyLevel() const; ///< highest veterancy level this object may reach (template default, overridable) + void setMaxVeterancyLevel( VeterancyLevel maxLevel, Bool provideFeedback = TRUE ); ///< override the veterancy cap (e.g. from an upgrade) inline const AsciiString& getName() const { return m_name; } inline void setName( const AsciiString& newName ) { m_name = newName; } diff --git a/GeneralsMD/Code/GameEngine/Source/Common/BitFlags.cpp b/GeneralsMD/Code/GameEngine/Source/Common/BitFlags.cpp index 1f8052c659a..ced74ab0393 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/BitFlags.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/BitFlags.cpp @@ -280,8 +280,8 @@ const char* const ArmorSetFlags::s_bitNameList[] = "PLAYER_UPGRADE2", "PLAYER_UPGRADE3", "PLAYER_UPGRADE4", - "FOUR", - "FIVE", + "LEVEL_FOUR", + "LEVEL_FIVE", nullptr }; @@ -347,8 +347,8 @@ const char* const WeaponBonusConditionFlags::s_bitNameList[] = "EXTRA6", "EXTRA7", "EXTRA8", - "VETERANCY_FOUR", - "VETERANCY_FIVE", + "LEVEL_FOUR", + "LEVEL_FIVE", nullptr }; static_assert(ARRAY_SIZE(WeaponBonusConditionFlags::s_bitNameList) == WEAPONBONUSCONDITION_COUNT + 1, "Incorrect array size"); diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Thing/ThingTemplate.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Thing/ThingTemplate.cpp index 40826fd32a1..b2bcf45722f 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Thing/ThingTemplate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Thing/ThingTemplate.cpp @@ -131,6 +131,7 @@ const FieldParse ThingTemplate::s_objectFieldParseTable[] = { "SkillPointValue", ThingTemplate::parseSkillPointValueList, nullptr, offsetof( ThingTemplate, m_skillPointValues ) }, { "ExperienceValue", ThingTemplate::parseExperienceValueList, nullptr, offsetof( ThingTemplate, m_experienceValues ) }, { "ExperienceRequired", ThingTemplate::parseExperienceRequiredList, nullptr, offsetof( ThingTemplate, m_experienceRequired ) }, + { "MaxVeterancyLevel", INI::parseIndexList, TheVeterancyNames, offsetof( ThingTemplate, m_maxVeterancyLevel ) }, { "IsTrainable", INI::parseBool, nullptr, offsetof( ThingTemplate, m_isTrainable ) }, { "EnterGuard", INI::parseBool, nullptr, offsetof( ThingTemplate, m_enterGuard ) }, { "HijackGuard", INI::parseBool, nullptr, offsetof( ThingTemplate, m_hijackGuard ) }, @@ -1091,6 +1092,8 @@ ThingTemplate::ThingTemplate() : // -1 means "same value as experienceValues for that level" m_skillPointValues[levelIndex] = USE_EXP_VALUE_FOR_SKILL_VALUE; } + // By default an object may reach the highest vanilla rank; MaxVeterancyLevel can cap it lower. + m_maxVeterancyLevel = LEVEL_HEROIC; m_isTrainable = FALSE; m_enterGuard = FALSE; m_hijackGuard = FALSE; diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp index 4152649ddc3..9d4a08d53f2 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp @@ -91,6 +91,8 @@ ControlBar *TheControlBar = nullptr; const Image* ControlBar::m_rankVeteranIcon = nullptr; const Image* ControlBar::m_rankEliteIcon = nullptr; const Image* ControlBar::m_rankHeroicIcon = nullptr; +const Image* ControlBar::m_rankFourIcon = nullptr; +const Image* ControlBar::m_rankFiveIcon = nullptr; /////////////////////////////////////////////////////////////////////////////////////////////////// // CommandButton ////////////////////////////////////////////////////////////////////////////////// @@ -1307,6 +1309,8 @@ void ControlBar::init( void ) m_rankVeteranIcon = TheMappedImageCollection ? TheMappedImageCollection->findImageByName( "SSChevron1L" ) : nullptr; m_rankEliteIcon = TheMappedImageCollection ? TheMappedImageCollection->findImageByName( "SSChevron2L" ) : nullptr; m_rankHeroicIcon = TheMappedImageCollection ? TheMappedImageCollection->findImageByName( "SSChevron3L" ) : nullptr; + m_rankFourIcon = TheMappedImageCollection ? TheMappedImageCollection->findImageByName( "SSChevron4L" ) : nullptr; + m_rankFiveIcon = TheMappedImageCollection ? TheMappedImageCollection->findImageByName( "SSChevron5L" ) : nullptr; // if(!m_controlBarResizer) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/ControlBar/ControlBarCommand.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/ControlBar/ControlBarCommand.cpp index 6740198e215..5bcb15762b4 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/ControlBar/ControlBarCommand.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/ControlBar/ControlBarCommand.cpp @@ -959,6 +959,10 @@ const Image* ControlBar::calculateVeterancyOverlayForThing( const ThingTemplate return m_rankEliteIcon; case LEVEL_HEROIC: return m_rankHeroicIcon; + case LEVEL_FOUR: + return m_rankFourIcon; + case LEVEL_FIVE: + return m_rankFiveIcon; } return nullptr; } @@ -981,6 +985,10 @@ const Image* ControlBar::calculateVeterancyOverlayForObject( const Object *obj ) return m_rankEliteIcon; case LEVEL_HEROIC: return m_rankHeroicIcon; + case LEVEL_FOUR: + return m_rankFourIcon; + case LEVEL_FIVE: + return m_rankFiveIcon; } return nullptr; } diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/ExperienceTracker.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/ExperienceTracker.cpp index feff348a7d1..63be136017d 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/ExperienceTracker.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/ExperienceTracker.cpp @@ -41,6 +41,7 @@ ExperienceTracker::ExperienceTracker(Object *parent) : m_parent(parent), m_currentLevel(LEVEL_REGULAR), + m_maxVeterancyLevel( (parent && parent->getTemplate()) ? parent->getTemplate()->getMaxVeterancyLevel() : LEVEL_LAST ), m_experienceSink(INVALID_ID), m_experienceScalar( 1.0f ), m_experienceValueScalar(1.0f ), @@ -100,6 +101,22 @@ ObjectID ExperienceTracker::getExperienceSink() const return m_experienceSink; } +//------------------------------------------------------------------------------------------------- +// Clamp a level to the current max cap. No object may exceed m_maxVeterancyLevel by any means. +static inline VeterancyLevel clampToMax( VeterancyLevel level, VeterancyLevel maxLevel ) +{ + return (level > maxLevel) ? maxLevel : level; +} + +//------------------------------------------------------------------------------------------------- +void ExperienceTracker::setMaxVeterancyLevel( VeterancyLevel maxLevel, Bool provideFeedback ) +{ + m_maxVeterancyLevel = maxLevel; + // If we are already above the new cap, demote down to it. + if (m_currentLevel > m_maxVeterancyLevel) + setVeterancyLevel( m_maxVeterancyLevel, provideFeedback ); +} + //------------------------------------------------------------------------------------------------- // Set Level to AT LEAST this... if we are already >= this level, do nothing. void ExperienceTracker::setMinVeterancyLevel( VeterancyLevel newLevel, Bool provideFeedback ) @@ -107,6 +124,7 @@ void ExperienceTracker::setMinVeterancyLevel( VeterancyLevel newLevel, Bool prov // This does not check for IsTrainable, because this function is for explicit setting, // so the setter is assumed to know what they are doing. The game function // of addExperiencePoints cares about Trainability. + newLevel = clampToMax( newLevel, m_maxVeterancyLevel ); if (m_currentLevel < newLevel) { VeterancyLevel oldLevel = m_currentLevel; @@ -123,6 +141,7 @@ void ExperienceTracker::setVeterancyLevel( VeterancyLevel newLevel, Bool provide // This does not check for IsTrainable, because this function is for explicit setting, // so the setter is assumed to know what they are doing. The game function // of addExperiencePoints cares about Trainability, if flagged thus. + newLevel = clampToMax( newLevel, m_maxVeterancyLevel ); if (m_currentLevel != newLevel) { VeterancyLevel oldLevel = m_currentLevel; @@ -139,6 +158,7 @@ Bool ExperienceTracker::gainExpForLevel(Int levelsToGain, Bool canScaleForBonus) Int newLevel = (Int)m_currentLevel + levelsToGain; if (newLevel > LEVEL_LAST) newLevel = LEVEL_LAST; + newLevel = clampToMax( (VeterancyLevel)newLevel, m_maxVeterancyLevel ); // gain what levels we can, even if we can't use 'em all if (newLevel > m_currentLevel) { @@ -156,6 +176,7 @@ Bool ExperienceTracker::canGainExpForLevel(Int levelsToGain) const // return true if we can gain levels, even if we can't gain ALL the levels requested if (newLevel > LEVEL_LAST) newLevel = LEVEL_LAST; + newLevel = clampToMax( (VeterancyLevel)newLevel, m_maxVeterancyLevel ); return (newLevel > m_currentLevel); } @@ -188,6 +209,7 @@ void ExperienceTracker::addExperiencePoints( Int experienceGain, Bool canScaleFo Int levelIndex = 0; while( ( (levelIndex + 1) < LEVEL_COUNT) + && ( (levelIndex + 1) <= m_maxVeterancyLevel ) // never climb past the max cap, regardless of ExperienceRequired && m_currentExperience >= m_parent->getTemplate()->getExperienceRequired(levelIndex + 1) ) { @@ -228,6 +250,7 @@ void ExperienceTracker::setExperienceAndLevel( Int experienceIn, Bool provideFee Int levelIndex = 0; while( ( (levelIndex + 1) < LEVEL_COUNT) + && ( (levelIndex + 1) <= m_maxVeterancyLevel ) // never climb past the max cap, regardless of ExperienceRequired && m_currentExperience >= m_parent->getTemplate()->getExperienceRequired(levelIndex + 1) ) { @@ -260,6 +283,7 @@ void ExperienceTracker::crc( Xfer *xfer ) * Version Info: * 1: Initial version * 2: TheSuperHackers @tweak Serialize m_isTrainable + * 3: Serialize m_maxVeterancyLevel (overridable veterancy cap) */ // ---------------------------------------------------------------------------- void ExperienceTracker::xfer( Xfer *xfer ) @@ -269,7 +293,7 @@ void ExperienceTracker::xfer( Xfer *xfer ) #if RETAIL_COMPATIBLE_XFER_SAVE XferVersion currentVersion = 1; #else - XferVersion currentVersion = 2; + XferVersion currentVersion = 3; #endif XferVersion version = currentVersion; xfer->xferVersion( &version, currentVersion ); @@ -294,6 +318,9 @@ void ExperienceTracker::xfer( Xfer *xfer ) if (version >= 2) xfer->xferBool(&m_isTrainable); + + if (version >= 3) + xfer->xferUser( &m_maxVeterancyLevel, sizeof( VeterancyLevel ) ); } //----------------------------------------------------------------------------- diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp index 4c8ac7d6291..070f4c1e9fd 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp @@ -3190,6 +3190,19 @@ VeterancyLevel Object::getVeterancyLevel() const return m_experienceTracker ? m_experienceTracker->getVeterancyLevel() : LEVEL_REGULAR; } +//------------------------------------------------------------------------------------------------- +VeterancyLevel Object::getMaxVeterancyLevel() const +{ + return m_experienceTracker ? m_experienceTracker->getMaxVeterancyLevel() : LEVEL_LAST; +} + +//------------------------------------------------------------------------------------------------- +void Object::setMaxVeterancyLevel( VeterancyLevel maxLevel, Bool provideFeedback ) +{ + if (m_experienceTracker) + m_experienceTracker->setMaxVeterancyLevel( maxLevel, provideFeedback ); +} + //------------------------------------------------------------------------------------------------- void Object::friend_bindToDrawable( Drawable *draw ) { diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Upgrade/ExperienceScalarUpgrade.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Upgrade/ExperienceScalarUpgrade.cpp index d88463636f9..95dbff349dc 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Upgrade/ExperienceScalarUpgrade.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Upgrade/ExperienceScalarUpgrade.cpp @@ -42,6 +42,7 @@ ExperienceScalarUpgradeModuleData::ExperienceScalarUpgradeModuleData( void ) m_initiallyActive = false; m_addXPScalar = 0.0f; m_addXPValueScalar = 0.0f; + m_setMaxVeterancyLevel = LEVEL_INVALID; // don't change the cap unless specified } //------------------------------------------------------------------------------------------------- @@ -56,6 +57,7 @@ void ExperienceScalarUpgradeModuleData::buildFieldParse(MultiIniFieldParse& p) { "StartsActive", INI::parseBool, NULL, offsetof(ExperienceScalarUpgradeModuleData, m_initiallyActive) }, { "AddXPScalar", INI::parseReal, NULL, offsetof( ExperienceScalarUpgradeModuleData, m_addXPScalar ) }, { "AddXPValueScalar", INI::parseReal, NULL, offsetof( ExperienceScalarUpgradeModuleData, m_addXPValueScalar ) }, + { "SetMaxVeterancyLevel", INI::parseIndexList, TheVeterancyNames, offsetof( ExperienceScalarUpgradeModuleData, m_setMaxVeterancyLevel ) }, { nullptr, nullptr, nullptr, 0 } }; @@ -93,6 +95,10 @@ void ExperienceScalarUpgrade::upgradeImplementation( ) xpTracker->setExperienceScalar( xpTracker->getExperienceScalar() + data->m_addXPScalar ); xpTracker->setExperienceValueScalar( xpTracker->getExperienceValueScalar() + data->m_addXPValueScalar ); } + + // Optionally raise/lower the object's veterancy cap. + if( data->m_setMaxVeterancyLevel != LEVEL_INVALID ) + obj->setMaxVeterancyLevel( data->m_setMaxVeterancyLevel ); } // ------------------------------------------------------------------------------------------------ diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/WeaponSet.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/WeaponSet.cpp index 3aa46237443..1831f8855c1 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/WeaponSet.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/WeaponSet.cpp @@ -90,8 +90,8 @@ const char* const WeaponSetFlags::s_bitNameList[] = "GARRISONED", "CONTAINED", - "FOUR", - "FIVE", + "LEVEL_FOUR", + "LEVEL_FIVE", nullptr }; From 91d71b365a3fe28a675b4e3b989ba68fdcfbad99 Mon Sep 17 00:00:00 2001 From: Andi Date: Sun, 12 Jul 2026 10:18:24 +0200 Subject: [PATCH 4/4] veterancy FX fix --- .../Code/GameEngine/Source/GameLogic/Object/Weapon.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp index 69c1366a91c..c62ed8afc39 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp @@ -104,7 +104,9 @@ static void parseAllVetLevelsAsciiString( INI* ini, void* /*instance*/, void * s { AsciiString* s = (AsciiString*)store; AsciiString a = ini->getNextAsciiString(); - for (Int i = LEVEL_FIRST; i <= LEVEL_LAST; ++i) + // Only fill through HEROIC; the ranks beyond it (FOUR/FIVE) are resolved from HEROIC in + // postProcessLoad, so an explicit per-level HEROIC override still propagates to them. + for (Int i = LEVEL_FIRST; i <= LEVEL_HEROIC; ++i) s[i] = a; } @@ -126,7 +128,8 @@ static void parseAllVetLevelsFXList( INI* ini, void* /*instance*/, void * store, ConstFXListPtr* s = (ConstFXListPtr*)store; const FXList* fx = nullptr; INI::parseFXList(ini, nullptr, &fx, nullptr); - for (Int i = LEVEL_FIRST; i <= LEVEL_LAST; ++i) + // Only fill through HEROIC; FOUR/FIVE are resolved from HEROIC in postProcessLoad. + for (Int i = LEVEL_FIRST; i <= LEVEL_HEROIC; ++i) s[i] = fx; } @@ -148,7 +151,8 @@ static void parseAllVetLevelsPSys( INI* ini, void* /*instance*/, void * store, c ConstParticleSystemTemplatePtr* s = (ConstParticleSystemTemplatePtr*)store; ConstParticleSystemTemplatePtr pst = nullptr; INI::parseParticleSystemTemplate(ini, nullptr, &pst, nullptr); - for (Int i = LEVEL_FIRST; i <= LEVEL_LAST; ++i) + // Only fill through HEROIC; FOUR/FIVE are resolved from HEROIC in postProcessLoad. + for (Int i = LEVEL_FIRST; i <= LEVEL_HEROIC; ++i) s[i] = pst; }