diff --git a/CREDITS.md b/CREDITS.md
index f8cb8f4bcf..2400bada28 100644
--- a/CREDITS.md
+++ b/CREDITS.md
@@ -161,6 +161,7 @@ This page lists all the individual contributions to the project by their author.
- Unit & infantry auto-conversion on ammo change
- Restore the ScriptType action#24 `Play speech` from Tiberian Sun
- Modify ammo on impact
+ - New AI teams selector
- **Starkku**:
- Misc. minor bugfixes & improvements
- AI script actions:
diff --git a/Phobos.vcxproj b/Phobos.vcxproj
index 67dc28e6ee..e6276dc725 100644
--- a/Phobos.vcxproj
+++ b/Phobos.vcxproj
@@ -102,6 +102,7 @@
+
@@ -128,6 +129,7 @@
+
@@ -159,6 +161,7 @@
+
diff --git a/docs/AI-Scripting-and-Mapping.md b/docs/AI-Scripting-and-Mapping.md
index 03db7be045..cf835b463d 100644
--- a/docs/AI-Scripting-and-Mapping.md
+++ b/docs/AI-Scripting-and-Mapping.md
@@ -166,6 +166,82 @@ ShowBriefing=true ; boolean
- You can use `NextScenario` and `AltNextScenario` to specify the map names required to enter a new campaign, thereby forcing the game to proceed to the next campaign.
- Now, setting a local variable named `` will also trigger `AltNextScenario`.
+## New AI Teams Selector
+
+- New AI system for selecting valid triggers in multiplayer randomly. Unlike the original method this one checks prerequisites and take care of other details.
+- It can split the valid triggers into 5 categories: ground vehicles, infantry, air, naval & mixed categories. If set, AI picks a random trigger in a random category.
+- Categories can have different chance probabilities. It can be set globally or customized per house. By default each category has a 20% chance to be selected.
+- `NewTeamsSelector.MergeUnclassifiedCategoryWith` can be used for merging the mixed category (units are from different categories) into one of the main categories.
+- In case of picking a category without valid triggers exist a fallback mode that allow picking a trigger from all valid triggers like if categories were disabled.
+- If `Autocreate=yes` AI will care about all units prerequisites so if the house's tech tree is incomplete for the trigger it gets discarded. It understands Ares tags like `Prerequisite.RequiredTheaters`, `Prerequisite.Negative`, `Prerequisite.Lists` & `Generic prerequisites` section.
+- `NewTeamsSelector.VIPWeight` controls the VIP trigger threshold. If a trigger meets this threshold, AI discards non-VIP candidates and picks exclusively from VIP triggers with equal chance (categories are bypassed in VIP mode).
+ - Defaults to `5000.0` (vanilla behavior).
+ - Setting `NewTeamsSelector.VIPWeight=-1` enables dynamic VIP mode where each trigger becomes VIP when its `Weight_Current >= Weight_Maximum`.
+ - Setting `NewTeamsSelector.VIPWeight=0` disables VIP mode completely.
+- Units can override the category using `ConsideredVehicle` and `ConsideredNaval` boolean tags.
+- AI is able to use unlocked units in captured Secret Labs.
+
+In `rulesmd.ini`:
+```ini
+[AI]
+NewTeamsSelector=false ; boolean
+NewTeamsSelector.SplitTriggersByCategory=true ; boolean
+NewTeamsSelector.EnableFallback=false ; boolean
+NewTeamsSelector.GroundCategoryPercentage=0.20 ; floating point value, percents or absolute
+NewTeamsSelector.InfantryCategoryPercentage=0.20 ; floating point value, percents or absolute
+NewTeamsSelector.NavalCategoryPercentage=0.20 ; floating point value, percents or absolute
+NewTeamsSelector.AirCategoryPercentage=0.20 ; floating point value, percents or absolute
+NewTeamsSelector.UnclassifiedCategoryPercentage=0.20 ; floating point value, percents or absolute
+NewTeamsSelector.MergeUnclassifiedCategoryWith=-1 ; Integer - Ground: 1, Air: 2, Naval: 3, Infantry: 4
+NewTeamsSelector.VIPWeight=5000.0 ; floating point value, -1 for dynamic max weight, 0 to disable
+
+[SOMEHOUSE] ; HouseType
+NewTeamsSelector.MergeUnclassifiedCategoryWith= ; Integer - Ground: 1, Air: 2, Naval: 3, Infantry: 4
+NewTeamsSelector.UnclassifiedCategoryPercentage= ; floating point value, percents or absolute
+NewTeamsSelector.GroundCategoryPercentage= ; floating point value, percents or absolute
+NewTeamsSelector.InfantryCategoryPercentage= ; floating point value, percents or absolute
+NewTeamsSelector.AirCategoryPercentage= ; floating point value, percents or absolute
+NewTeamsSelector.NavalCategoryPercentage= ; floating point value, percents or absolute
+NewTeamsSelector.VIPWeight= ; floating point value, -1 for dynamic max weight, 0 to disable
+
+[SOMETECHNO] ; TechnoType
+ConsideredNaval= ; boolean
+ConsideredVehicle= ; boolean
+```
+
+- Modified slightly AI Trigger conditions 0 (enemy owns ...), 1 (house owns ...) and 7 (civilian owns ...) and added 10 new conditions (from 8 to 17) that check lists of objects instead of only 1 unit. The first 3 were modified because the objects counters weren't updated in real-time. The possible modified and new cases are:
+
+| *Condition* | *List of objects* | *Description* |
+| :---------: | :---------------: | :---------------------------------------------: |
+| 0 (changed) | No | Count objects of the main enemy. If AI house doesn't have a main enemy It will search in all enemies. In vanilla YR if house doesn't have a main enemy this condition fail |
+| 1 (changed) | No | Count own objects |
+| 7 (changed) | No | Count civilian objects |
+| 8 | No | Count enemy objects. It will search in all enemies |
+| 9 | Yes | Count enemy objects. If AI house doesn't have a main enemy It will search in all enemies. |
+| 10 | Yes | Count own objects |
+| 11 | Yes | Count civilian objects |
+| 12 | Yes | Count enemy objects. It will search in all enemies |
+| 13 | Yes | Count objects from allies |
+| 14 | Yes | Count enemy objects. If AI house doesn't have a main enemy It will search in all enemies. It uses `AND` comparator so each object must obey the same comparations |
+| 15 | Yes | Count own objects. It uses `AND` comparator so each object must obey the same comparations |
+| 16 | Yes | Count civilian objects. It uses `AND` comparator so each object must obey the same comparations |
+| 17 | Yes | Count enemy objects. It will search in all enemies. It uses `AND` comparator so each object must obey the same comparations |
+| 18 | No | Count structures with `BridgeRepairHut=yes` linked with destroyed bridges |
+| 19 | No | Count structures with `BridgeRepairHut=yes` linked with undamaged bridges |
+
+- Some trigger conditions need to specify a 0-based list from `[AITargetTypes]`. The index of the list is written in hexadecimal and in little endian format. The value must be written at the end of the trigger:
+`00000000000000000000000000000000000000000000000000000000AABBCCDD`
+For example: list 0 is written 00 (put it in AA), list 1 is written 01 (put it in AA), list 10 is written 0A (put it in AA), 255 is 0xFF (put it in AA) and 256 is 0x0001 (put it in AABB), etc.
+- The *`AITargetTypes` index#* values are obtained in the new `AITargetTypes` section that must be declared in `rulesmd.ini`:
+
+In `rulesmd.ini`:
+```ini
+[AITargetTypes] ; List of TechnoType lists
+0=SOMETECHNOTYPE,SOMEOTHERTECHNOTYPE,SAMPLETECHNOTYPE
+1=ANOTHERTECHNOTYPE,YETANOTHERTECHNOTYPE
+; ...
+```
+
## Script Actions
### `10000-10999` Ingame Actions
diff --git a/docs/Whats-New.md b/docs/Whats-New.md
index 661d60777b..3fd13d498d 100644
--- a/docs/Whats-New.md
+++ b/docs/Whats-New.md
@@ -968,6 +968,7 @@ HideShakeEffects=false ; boolean
- Customizable ROF random delay (by Starkku)
- Animation with `Tiled=yes` now supports `CustomPalette` (by ststl)
- Toggleable `DieSound` when grinding (by Trsdy)
+- New AI teams selector (by FS-21)
- Shields can inherit Techno ArmorType (by Starkku)
- Income money flying-string display when harvesters or slaves are docking to refineries or when spies steal credits (by Trsdy)
- Allow random crates to be generated only on lands (by Trsdy)
diff --git a/src/Ext/Building/Body.cpp b/src/Ext/Building/Body.cpp
index a1481731cf..c84b90b7f6 100644
--- a/src/Ext/Building/Body.cpp
+++ b/src/Ext/Building/Body.cpp
@@ -1,6 +1,8 @@
#include "Body.h"
#include
+#include
+#include
#include
#include
@@ -442,6 +444,121 @@ const std::vector BuildingExt::GetFoundationCells(BuildingClass* con
return foundationCells;
}
+// Assigns a secret production option to the AI building (Ares doesn't handle the AI case).
+void BuildingExt::UpdateSecretLabAI()
+{
+ auto pThis = this->OwnerObject();
+ auto pOwner = pThis->Owner;
+
+ if (!pOwner || pOwner->Type->MultiplayPassive || pOwner->IsControlledByHuman())
+ return;
+
+ auto pType = pThis->Type;
+
+ // Fixed item, no need to randomize
+ if (pType->SecretInfantry || pType->SecretUnit || pType->SecretBuilding)
+ return;
+
+ auto pDataType = BuildingTypeExt::Fetch(pType);
+
+ // If Secret Lab already picked a techno and isn't allowed to recalculate it again the function ends
+ if (this->SecretLab_Placed && (!pDataType || !pDataType->Secret_RecalcOnCapture))
+ return;
+
+ DynamicVectorClass validCandidates;
+ DynamicVectorClass possibleCandidates;
+
+ if (pDataType && pDataType->PossibleBoons.size() > 0)
+ {
+ for (const auto& boon : pDataType->PossibleBoons)
+ {
+ possibleCandidates.AddItem(boon);
+ }
+ }
+ else
+ {
+ for (const auto& boon : RulesClass::Instance->SecretInfantry)
+ {
+ possibleCandidates.AddItem(boon);
+ }
+
+ for (const auto& boon : RulesClass::Instance->SecretUnits)
+ {
+ possibleCandidates.AddItem(boon);
+ }
+
+ for (const auto& boon : RulesClass::Instance->SecretBuildings)
+ {
+ possibleCandidates.AddItem(boon);
+ }
+ }
+
+ if (possibleCandidates.Count > 0)
+ {
+ for (const auto& boon : possibleCandidates)
+ {
+ auto pExt = TechnoTypeExt::Fetch(boon);
+ bool isRequiredHouse = true; // Default value if Secret.RequiredHouses isn't declared
+
+ if (pExt && pExt->Secret_RequiredHouses.size() > 0)
+ isRequiredHouse = false;
+
+ if (pExt)
+ {
+ for (const auto houseId : pExt->Secret_RequiredHouses)
+ {
+ int houseIdx = HouseTypeClass::FindIndex(houseId.c_str());
+ if (houseIdx < 0)
+ continue;
+
+ if (pOwner->Type->ArrayIndex == houseIdx)
+ {
+ bool canBeBuilt = HouseExt::PrerequisitesMet(pOwner, boon, true);
+
+ if (canBeBuilt)
+ {
+ isRequiredHouse = true;
+ break;
+ }
+ }
+ }
+
+ bool isForbiddenHouse = false; // Default value if Secret.ForbiddenHouses isn't declared
+
+ for (const auto houseId : pExt->Secret_ForbiddenHouses)
+ {
+ int houseIdx = HouseTypeClass::FindIndex(houseId.c_str());
+ if (houseIdx < 0)
+ continue;
+
+ if (pOwner->Type->ArrayIndex == houseIdx)
+ {
+ isForbiddenHouse = true;
+ break;
+ }
+ }
+
+ if (isRequiredHouse && !isForbiddenHouse)
+ validCandidates.AddItem(boon);
+ }
+ }
+ }
+
+ // pick one of all eligible items
+ if (validCandidates.Count > 0)
+ {
+ auto result = validCandidates[ScenarioClass::Instance->Random.RandomRanged(0, validCandidates.Count - 1)];
+ Debug::Log("[Secret Lab AI] rolled %s for %s\n", result->ID, pType->ID);
+ pThis->SecretProduction = result;
+ this->SecretLab_Placed = true;
+ }
+ else
+ {
+ Debug::Log("[Secret Lab AI] %s has no boons applicable to country [%s]!\n",
+ pType->ID, pOwner->Type->ID);
+ }
+}
+
WeaponStruct* BuildingExt::GetLaserWeapon(BuildingClass* pThis)
{
auto const pExt = BuildingExt::Fetch(pThis);
@@ -595,6 +712,7 @@ void BuildingExt::Serialize(T& Stm)
.Process(this->GrindingWeapon_LastFiredFrame)
.Process(this->GrindingWeapon_AccumulatedCredits)
.Process(this->CurrentAirFactory)
+ .Process(this->SecretLab_Placed)
.Process(this->AccumulatedIncome)
.Process(this->CurrentLaserWeaponIndex)
.Process(this->PoweredUpToLevel)
diff --git a/src/Ext/Building/Body.h b/src/Ext/Building/Body.h
index c1fcdc3b85..e7676ae4fc 100644
--- a/src/Ext/Building/Body.h
+++ b/src/Ext/Building/Body.h
@@ -20,6 +20,7 @@ class BuildingExt final : public TechnoExt, public Detach::Listener CurrentLaserWeaponIndex;
int PoweredUpToLevel; // Distinct from UpgradeLevel, and set to highest PowersUpToLevel out of applied upgrades regardless of how many are currently applied to this building.
@@ -39,6 +40,7 @@ class BuildingExt final : public TechnoExt, public Detach::ListenerType->SecretLab && !pThis->Owner->IsControlledByHuman())
+ {
+ auto pExt = BuildingExt::Fetch(pThis);
+ pExt->UpdateSecretLabAI();
+ }
+
+ return 0;
+}
+
DEFINE_HOOK(0x4400F9, BuildingClass_AI_UpdateOverpower, 0x6)
{
enum { SkipGameCode = 0x44019D };
diff --git a/src/Ext/BuildingType/Body.cpp b/src/Ext/BuildingType/Body.cpp
index 1d37508cc5..90dbe1f2a1 100644
--- a/src/Ext/BuildingType/Body.cpp
+++ b/src/Ext/BuildingType/Body.cpp
@@ -283,6 +283,22 @@ void BuildingTypeExt::LoadFromINIFile(CCINIClass* const pINI)
this->RevealToAll_Radius.Read(exINI, pSection, "RevealToAll.Radius");
+ this->Secret_RecalcOnCapture.Read(exINI, pSection, "SecretLab.GenerateOnCapture");
+
+ // Secret.Boons contains a list of TechnoTypeClass IDs
+ const char* key = "SecretLab.PossibleBoons";
+ char* context = nullptr;
+ pINI->ReadString(pSection, key, "", Phobos::readBuffer);
+
+ for (char* cur = strtok_s(Phobos::readBuffer, Phobos::readDelims, &context); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context))
+ {
+ int index = TechnoTypeClass::FindIndex(cur);
+ if (index != -1)
+ this->PossibleBoons.push_back(TechnoTypeClass::Array.GetItem(index));
+ }
+
+ key = nullptr;
+
if (pThis->NumberOfDocks > 0)
{
std::optional empty;
@@ -439,6 +455,8 @@ void BuildingTypeExt::Serialize(T& Stm)
.Process(this->ConsideredVehicle)
.Process(this->ZShapePointMove_OnBuildup)
.Process(this->SellBuildupLength)
+ .Process(this->Secret_RecalcOnCapture)
+ .Process(this->PossibleBoons)
.Process(this->AircraftDockingDirs)
.Process(this->AircraftDockingDir_DefaultToPoseDir)
.Process(this->FactoryPlant_AllowTypes)
diff --git a/src/Ext/BuildingType/Body.h b/src/Ext/BuildingType/Body.h
index 254c604546..4bce579ec7 100644
--- a/src/Ext/BuildingType/Body.h
+++ b/src/Ext/BuildingType/Body.h
@@ -68,8 +68,11 @@ class BuildingTypeExt final : public TechnoTypeExt
Valueable IsAnimDelayedBurst;
+ Valueable Secret_RecalcOnCapture;
+ NullableVector PossibleBoons;
+
std::vector> AircraftDockingDirs;
- Nullable AircraftDockingDir_DefaultToPoseDir;
+ Nullable AircraftDockingDir_DefaultToPoseDir;
ValueableVector FactoryPlant_AllowTypes;
ValueableVector FactoryPlant_DisallowTypes;
@@ -263,6 +266,9 @@ class BuildingTypeExt final : public TechnoTypeExt
// Ares 0.E
, Tunnel { false }
+ , Secret_RecalcOnCapture { false }
+ , PossibleBoons {}
+
// Ares 3.0
, UnitSell {}
{ }
diff --git a/src/Ext/House/Body.Prerequisites.cpp b/src/Ext/House/Body.Prerequisites.cpp
new file mode 100644
index 0000000000..bd3864c17f
--- /dev/null
+++ b/src/Ext/House/Body.Prerequisites.cpp
@@ -0,0 +1,285 @@
+#include "body.h"
+#include
+#include
+#include
+
+bool HouseExt::HasBuildingPrerequisite(HouseClass* const pHouse, int const idxBuildingType)
+{
+ if (!pHouse || idxBuildingType < 0 || idxBuildingType >= BuildingTypeClass::Array.Count)
+ return false;
+
+ if (pHouse->ActiveBuildingTypes.GetItemCount(idxBuildingType) > 0)
+ return true;
+
+ // Check if this building type is an upgrade attached to an active building
+ auto const pType = BuildingTypeClass::Array.GetItem(idxBuildingType);
+ if (!pType)
+ return false;
+
+ auto const pTypeExt = BuildingTypeExt::Fetch(pType);
+ bool const isUpgrade = (pType->PowersUpBuilding[0] != '\0') || (pTypeExt && !pTypeExt->PowersUp_Buildings.empty());
+
+ if (isUpgrade)
+ {
+ for (auto const pBld : pHouse->Buildings)
+ {
+ if (!pBld || !pBld->IsAlive || pBld->Health <= 0)
+ continue;
+
+ for (auto const pUpgrade : pBld->Upgrades)
+ {
+ if (pUpgrade == pType)
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+bool HouseExt::HasGenericPrerequisite(int idx, HouseClass* const pHouse)
+{
+ if (idx >= 0 || !pHouse)
+ return false;
+
+ int absoluteIndex = std::abs(idx);
+ auto const pRulesExt = RulesExt::Global();
+ if (absoluteIndex >= pRulesExt->GenericPrerequisites.Count)
+ return false;
+
+ // Check buildings and nested generic prerequisites matching this generic prerequisite
+ DynamicVectorClass const& selectedPrerequisite = pRulesExt->GenericPrerequisites.GetItem(absoluteIndex);
+ for (auto idxItem : selectedPrerequisite)
+ {
+ if (idxItem < 0)
+ {
+ if (HouseExt::HasGenericPrerequisite(idxItem, pHouse))
+ return true;
+ }
+ else
+ {
+ if (HouseExt::HasBuildingPrerequisite(pHouse, idxItem))
+ return true;
+ }
+ }
+
+ // Check alternate technos (vehicles, infantry, aircraft, etc.)
+ if (absoluteIndex < pRulesExt->GenericPrerequisitesAlternates.Count)
+ {
+ DynamicVectorClass const& selectedAlternates = pRulesExt->GenericPrerequisitesAlternates.GetItem(absoluteIndex);
+ for (auto pTechnoType : selectedAlternates)
+ {
+ if (pHouse->CountOwnedNow(pTechnoType) > 0)
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool HouseExt::HasPrerequisite(HouseClass* const pHouse, int const idx)
+{
+ if (idx < 0)
+ return HouseExt::HasGenericPrerequisite(idx, pHouse);
+
+ return HouseExt::HasBuildingPrerequisite(pHouse, idx);
+}
+
+int HouseExt::FindGenericPrerequisite(const char* id)
+{
+ if (BuildingTypeClass::FindIndex(id) >= 0)
+ return INT32_MAX;
+
+ if (RulesExt::Global()->GenericPrerequisitesNames.Count == 0)
+ RulesExt::FillDefaultPrerequisites();
+
+ int i = 0;
+ for (auto str : RulesExt::Global()->GenericPrerequisitesNames)
+ {
+ if (_strcmpi(id, str) == 0)
+ return i;
+
+ --i;
+ }
+
+ return INT32_MAX; // Error
+}
+
+bool HouseExt::IsAvailableToHouse(HouseClass* const pHouse, TechnoTypeClass* const pItem)
+{
+ if (!pHouse || !pItem)
+ return false;
+
+ const auto pType = pHouse->Type;
+ if (!pType)
+ return false;
+
+ DWORD const bitHouse = 1u << pType->ArrayIndex2;
+
+ bool inOwners = pItem->InOwners(bitHouse);
+ bool inRequired = pItem->InRequiredHouses(bitHouse);
+ bool inForbidden = pItem->InForbiddenHouses(bitHouse);
+
+ if (!inOwners || !inRequired || inForbidden)
+ {
+ if (auto const pParent = pType->FindParentCountry())
+ {
+ DWORD const bitParent = 1u << pParent->ArrayIndex2;
+
+ if (!inOwners && pItem->InOwners(bitParent))
+ inOwners = true;
+
+ if (!inRequired && pItem->InRequiredHouses(bitParent))
+ inRequired = true;
+
+ if (!inForbidden && pItem->InForbiddenHouses(bitParent))
+ inForbidden = true;
+ }
+ }
+
+ return inOwners && inRequired && !inForbidden;
+}
+
+bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const pItem, bool skipSecretLabChecks)
+{
+ if (!pThis || !pItem)
+ return false;
+
+ auto pHouseExt = HouseExt::Fetch(pThis);
+ if (!pHouseExt)
+ return false;
+
+ auto pItemExt = TechnoTypeExt::Fetch(pItem);
+ if (!pItemExt)
+ return false;
+
+ // Check if it appears in Owner=, RequiredHouses= and ForbiddenHouses= (including ParentCountry support)
+ if (!HouseExt::IsAvailableToHouse(pThis, pItem))
+ return false;
+
+ // If the unit is available after capturing a SecretLab=yes must be evaluated if meets the prerequisite
+ if (!skipSecretLabChecks && pItemExt->ConsideredSecretLabTech && !pThis->HasFromSecretLab(pItem))
+ return false;
+
+ // Stolen Tech checks (Chrono Commando, Psi Commando, etc.)
+ if ((pItem->RequiresStolenAlliedTech && !pThis->Side0TechInfiltrated) ||
+ (pItem->RequiresStolenSovietTech && !pThis->Side1TechInfiltrated) ||
+ (pItem->RequiresStolenThirdTech && !pThis->Side2TechInfiltrated))
+ {
+ return false;
+ }
+
+ // Prerequisite.RequiredTheaters check
+ if (!(pItemExt->PrerequisiteTheaters & (1u << static_cast(ScenarioClass::Instance->Theater))))
+ return false;
+
+ // TechLevel check
+ if (pThis->TechLevel < pItem->TechLevel)
+ return false;
+
+ // BuildLimit checks
+ if (pItem->BuildLimit > 0)
+ {
+ int nInstances = 0;
+ for (const auto pTechno : TechnoClass::Array)
+ {
+ if (pTechno->Owner == pThis
+ && pTechno->GetTechnoType() == pItem
+ && pTechno->IsAlive
+ && pTechno->Health > 0)
+ {
+ nInstances++;
+
+ if (nInstances >= pItem->BuildLimit)
+ return false;
+ }
+ }
+ }
+ else if (pItem->BuildLimit < 0)
+ {
+ if (pThis->CountOwnedEver(pItem) >= -pItem->BuildLimit)
+ return false;
+ }
+ else if (pItem->BuildLimit == 0)
+ {
+ return false;
+ }
+
+ // Phobos BuildLimitGroup check
+ if (HouseExt::ReachedBuildLimit(pThis, pItem, true))
+ return false;
+
+ // Ares Prerequisite.Negative list
+ if (!pItemExt->Prerequisite_Negative.empty())
+ {
+ for (int idx : pItemExt->Prerequisite_Negative)
+ {
+ if (HouseExt::HasPrerequisite(pThis, idx))
+ return false;
+ }
+ }
+
+ // Main prerequisite checks are skipped if a new secret lab object is in process to be unlocked
+ if (skipSecretLabChecks)
+ return true;
+
+ // PrerequisiteOverride (OR logic: if ANY entry is owned, prerequisites are considered met)
+ DynamicVectorClass const& prerequisiteOverride = pItem->PrerequisiteOverride;
+ for (int idx : prerequisiteOverride)
+ {
+ if (HouseExt::HasPrerequisite(pThis, idx))
+ return true;
+ }
+
+ // Main Prerequisite list (AND logic: ALL entries must be satisfied)
+ bool prerequisiteMet = true;
+ if (!pItemExt->Prerequisite.empty())
+ {
+ for (int idx : pItemExt->Prerequisite)
+ {
+ if (!HouseExt::HasPrerequisite(pThis, idx))
+ {
+ prerequisiteMet = false;
+ break;
+ }
+ }
+ }
+
+ // Ares Prerequisite lists (OR logic between lists, AND logic within each list)
+ bool prerequisiteListsMet = false;
+ if (pItemExt->Prerequisite_Lists.Get() > 0 && !pItemExt->Prerequisite_ListVector.empty())
+ {
+ for (const auto& list : pItemExt->Prerequisite_ListVector)
+ {
+ bool listSatisfied = true;
+ for (int idx : list)
+ {
+ if (!HouseExt::HasPrerequisite(pThis, idx))
+ {
+ listSatisfied = false;
+ break;
+ }
+ }
+
+ if (listSatisfied)
+ {
+ prerequisiteListsMet = true;
+ break;
+ }
+ }
+ }
+
+ bool hasPrereq = !pItemExt->Prerequisite.empty();
+ bool hasLists = pItemExt->Prerequisite_Lists.Get() > 0 && !pItemExt->Prerequisite_ListVector.empty();
+
+ if (!hasPrereq && !hasLists)
+ return true;
+
+ if (hasPrereq && !hasLists)
+ return prerequisiteMet;
+
+ if (!hasPrereq && hasLists)
+ return prerequisiteListsMet;
+
+ return prerequisiteMet || prerequisiteListsMet;
+}
diff --git a/src/Ext/House/Body.cpp b/src/Ext/House/Body.cpp
index e53d1d2ad9..6337995ace 100644
--- a/src/Ext/House/Body.cpp
+++ b/src/Ext/House/Body.cpp
@@ -706,6 +706,7 @@ void HouseExt::Serialize(T& Stm)
.Process(this->SuspendedEMPulseSWs)
.Process(this->SuperExts)
.Process(this->ForceEnemyIndex)
+ .Process(this->AITriggers_ValidList)
.Process(this->ForceOnlyTargetHouseEnemy)
.Process(this->ForceOnlyTargetHouseEnemyMode)
.Process(this->TeamDelay)
diff --git a/src/Ext/House/Body.h b/src/Ext/House/Body.h
index df5ba629b6..64e404b16f 100644
--- a/src/Ext/House/Body.h
+++ b/src/Ext/House/Body.h
@@ -43,6 +43,8 @@ class HouseExt final : public AbstractExt, public Detach::Listener AITriggers_ValidList;
+
CDTimerClass CombatAlertTimer;
CDTimerClass AISuperWeaponDelayTimer;
CDTimerClass AIFireSaleDelayTimer;
@@ -173,6 +175,12 @@ class HouseExt final : public AbstractExt, public Detach::ListenerEVATag.Read(pINI, pSection, "EVA.Tag");
+
+ this->NewTeamsSelector_MergeUnclassifiedCategoryWith.Read(exINI, pSection, "NewTeamsSelector.MergeUnclassifiedCategoryWith");
+ this->NewTeamsSelector_UnclassifiedCategoryPercentage.Read(exINI, pSection, "NewTeamsSelector.UnclassifiedCategoryPercentage");
+ this->NewTeamsSelector_GroundCategoryPercentage.Read(exINI, pSection, "NewTeamsSelector.GroundCategoryPercentage");
+ this->NewTeamsSelector_AirCategoryPercentage.Read(exINI, pSection, "NewTeamsSelector.AirCategoryPercentage");
+ this->NewTeamsSelector_NavalCategoryPercentage.Read(exINI, pSection, "NewTeamsSelector.NavalCategoryPercentage");
+ this->NewTeamsSelector_InfantryCategoryPercentage.Read(exINI, pSection, "NewTeamsSelector.InfantryCategoryPercentage");
+ this->NewTeamsSelector_VIPWeight.Read(exINI, pSection, "NewTeamsSelector.VIPWeight");
}
template
@@ -25,6 +33,13 @@ void HouseTypeExt::Serialize(T& Stm)
{
Stm
.Process(this->EVATag)
+ .Process(this->NewTeamsSelector_MergeUnclassifiedCategoryWith)
+ .Process(this->NewTeamsSelector_UnclassifiedCategoryPercentage)
+ .Process(this->NewTeamsSelector_GroundCategoryPercentage)
+ .Process(this->NewTeamsSelector_AirCategoryPercentage)
+ .Process(this->NewTeamsSelector_NavalCategoryPercentage)
+ .Process(this->NewTeamsSelector_InfantryCategoryPercentage)
+ .Process(this->NewTeamsSelector_VIPWeight)
;
}
diff --git a/src/Ext/HouseType/Body.h b/src/Ext/HouseType/Body.h
index c843286266..8a520ab196 100644
--- a/src/Ext/HouseType/Body.h
+++ b/src/Ext/HouseType/Body.h
@@ -27,8 +27,23 @@ class HouseTypeExt final : public AbstractTypeExt
EVAType EVATag;
+ Nullable NewTeamsSelector_MergeUnclassifiedCategoryWith;
+ Nullable NewTeamsSelector_UnclassifiedCategoryPercentage;
+ Nullable NewTeamsSelector_GroundCategoryPercentage;
+ Nullable NewTeamsSelector_NavalCategoryPercentage;
+ Nullable NewTeamsSelector_AirCategoryPercentage;
+ Nullable NewTeamsSelector_InfantryCategoryPercentage;
+ Nullable NewTeamsSelector_VIPWeight;
+
HouseTypeExt(HouseTypeClass* OwnerObject) : AbstractTypeExt(OwnerObject)
, EVATag { -2 }
+ , NewTeamsSelector_MergeUnclassifiedCategoryWith { }
+ , NewTeamsSelector_UnclassifiedCategoryPercentage { }
+ , NewTeamsSelector_GroundCategoryPercentage { }
+ , NewTeamsSelector_NavalCategoryPercentage { }
+ , NewTeamsSelector_AirCategoryPercentage { }
+ , NewTeamsSelector_InfantryCategoryPercentage { }
+ , NewTeamsSelector_VIPWeight { }
{ }
virtual ~HouseTypeExt() = default;
@@ -65,4 +80,3 @@ class HouseTypeExt final : public AbstractTypeExt
return AbstractExt::TryFetch(pThis);
}
};
-
diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp
index bc86659c57..aaf24a2c99 100644
--- a/src/Ext/Rules/Body.cpp
+++ b/src/Ext/Rules/Body.cpp
@@ -3,6 +3,8 @@
#include
#include
+#include
+#include
#include
#include
#include
@@ -665,15 +667,90 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI)
this->AIScriptsLists.emplace_back(std::move(objectsList));
}
+
+ this->NewTeamsSelector.Read(exINI, "AI", "NewTeamsSelector");
+ this->NewTeamsSelector_SplitTriggersByCategory.Read(exINI, "AI", "NewTeamsSelector.SplitTriggersByCategory");
+ this->NewTeamsSelector_EnableFallback.Read(exINI, "AI", "NewTeamsSelector.EnableFallback");
+ this->NewTeamsSelector_MergeUnclassifiedCategoryWith.Read(exINI, "AI", "NewTeamsSelector.MergeUnclassifiedCategoryWith");
+ this->NewTeamsSelector_UnclassifiedCategoryPercentage.Read(exINI, "AI", "NewTeamsSelector.UnclassifiedCategoryPercentage");
+ this->NewTeamsSelector_GroundCategoryPercentage.Read(exINI, "AI", "NewTeamsSelector.GroundCategoryPercentage");
+ this->NewTeamsSelector_AirCategoryPercentage.Read(exINI, "AI", "NewTeamsSelector.AirCategoryPercentage");
+ this->NewTeamsSelector_NavalCategoryPercentage.Read(exINI, "AI", "NewTeamsSelector.NavalCategoryPercentage");
+ this->NewTeamsSelector_InfantryCategoryPercentage.Read(exINI, "AI", "NewTeamsSelector.InfantryCategoryPercentage");
+ this->NewTeamsSelector_VIPWeight.Read(exINI, "AI", "NewTeamsSelector.VIPWeight");
+
+ // Section Generic Prerequisites
+ FillDefaultPrerequisites();
}
-// this should load everything that TypeData is not dependant on
-// i.e. InfantryElectrocuted= can go here since nothing refers to it
-// but [GenericPrerequisites] have to go earlier because they're used in parsing TypeData
void RulesExt::ExtData::LoadAfterTypeData(RulesClass* pThis, CCINIClass* pINI)
{
INI_EX exINI(pINI);
+ this->GenericPrerequisitesAlternates.Clear();
+
+ auto loadAlternates = [&](const char* name) -> DynamicVectorClass
+ {
+ DynamicVectorClass alternates;
+ char keyName[0x80];
+ char formattedName[0x80];
+ strcpy_s(formattedName, name);
+
+ if (formattedName[0] >= 'a' && formattedName[0] <= 'z')
+ formattedName[0] = static_cast(formattedName[0] - 'a' + 'A');
+ for (size_t k = 1; formattedName[k] != '\0'; ++k)
+ {
+ if (formattedName[k] >= 'A' && formattedName[k] <= 'Z')
+ formattedName[k] = static_cast(formattedName[k] - 'A' + 'a');
+ }
+
+ bool found = false;
+ _snprintf_s(keyName, _TRUNCATE, "Prerequisite%sAlternate", formattedName);
+ if (pINI->ReadString("General", keyName, "", Phobos::readBuffer) > 0)
+ {
+ found = true;
+ }
+ else
+ {
+ _snprintf_s(keyName, _TRUNCATE, "Prerequisite%sAlternate", name);
+ if (pINI->ReadString("General", keyName, "", Phobos::readBuffer) > 0)
+ found = true;
+ }
+
+ if (found)
+ {
+ char* context = nullptr;
+ for (char* cur = strtok_s(Phobos::readBuffer, Phobos::readDelims, &context); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context))
+ {
+ if (auto const pType = TechnoTypeClass::Find(cur))
+ {
+ alternates.AddItem(pType);
+ }
+ }
+ }
+
+ return alternates;
+ };
+
+ // Index 0: "-" dummy
+ DynamicVectorClass emptyList;
+ this->GenericPrerequisitesAlternates.AddItem(emptyList);
+
+ for (int i = 1; i < this->GenericPrerequisitesNames.Count; ++i)
+ {
+ const char* name = this->GenericPrerequisitesNames[i];
+ auto alternates = loadAlternates(name);
+
+ if (_stricmp(name, "PROC") == 0)
+ {
+ if (alternates.Count == 0 && pThis->PrerequisiteProcAlternate)
+ {
+ alternates.AddItem(pThis->PrerequisiteProcAlternate);
+ }
+ }
+
+ this->GenericPrerequisitesAlternates.AddItem(alternates);
+ }
}
// this runs between the before and after type data loading methods for rules ini
@@ -692,6 +769,84 @@ void RulesExt::ExtData::InitializeAfterAllLoaded()
this->TintColorBerserk = GeneralUtils::GetColorFromColorAdd(pRules->BerserkColor);
}
+void RulesExt::FillDefaultPrerequisites()
+{
+ if (RulesExt::Global()->GenericPrerequisitesNames.Count != 0)
+ return;
+
+ DynamicVectorClass empty;
+ RulesExt::Global()->GenericPrerequisitesNames.AddItem("-"); // Official index: 0
+ RulesExt::Global()->GenericPrerequisites.AddItem(empty);
+
+ RulesExt::Global()->GenericPrerequisitesNames.AddItem("POWER"); // Official index: -1
+ RulesExt::Global()->GenericPrerequisites.AddItem(RulesClass::Instance->PrerequisitePower);
+ RulesExt::Global()->GenericPrerequisitesNames.AddItem("FACTORY"); // -2
+ RulesExt::Global()->GenericPrerequisites.AddItem(RulesClass::Instance->PrerequisiteFactory);
+ RulesExt::Global()->GenericPrerequisitesNames.AddItem("BARRACKS"); // -3
+ RulesExt::Global()->GenericPrerequisites.AddItem(RulesClass::Instance->PrerequisiteBarracks);
+ RulesExt::Global()->GenericPrerequisitesNames.AddItem("RADAR"); // -4
+ RulesExt::Global()->GenericPrerequisites.AddItem(RulesClass::Instance->PrerequisiteRadar);
+ RulesExt::Global()->GenericPrerequisitesNames.AddItem("TECH"); // -5
+ RulesExt::Global()->GenericPrerequisites.AddItem(RulesClass::Instance->PrerequisiteTech);
+ RulesExt::Global()->GenericPrerequisitesNames.AddItem("PROC"); // -6
+ RulesExt::Global()->GenericPrerequisites.AddItem(RulesClass::Instance->PrerequisiteProc);
+
+ // If [GenericPrerequisites] is present will be added after these.
+ // Also the originals can be replaced by new ones
+ int genericPreqsCount = CCINIClass::INI_Rules->GetKeyCount("GenericPrerequisites");
+
+ for (int i = 0; i < genericPreqsCount; ++i)
+ {
+ DynamicVectorClass objectsList;
+ char* context = nullptr;
+ const char* keyName = CCINIClass::INI_Rules->GetKeyName("GenericPrerequisites", i);
+ if (!keyName || keyName[0] == '\0')
+ continue;
+
+ CCINIClass::INI_Rules->ReadString("GenericPrerequisites", keyName, "", Phobos::readBuffer);
+
+ for (char* cur = strtok_s(Phobos::readBuffer, Phobos::readDelims, &context); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context))
+ {
+ int idx = BuildingTypeClass::FindIndex(cur);
+ if (idx >= 0)
+ {
+ objectsList.AddItem(idx);
+ }
+ else
+ {
+ int genIdx = HouseExt::FindGenericPrerequisite(cur);
+ if (genIdx < 0)
+ objectsList.AddItem(genIdx);
+ }
+ }
+
+ // Find existing name using case-insensitive comparison
+ int existingIndex = -1;
+ for (int k = 0; k < RulesExt::Global()->GenericPrerequisitesNames.Count; ++k)
+ {
+ if (_strcmpi(RulesExt::Global()->GenericPrerequisitesNames[k], keyName) == 0)
+ {
+ existingIndex = k;
+ break;
+ }
+ }
+
+ if (existingIndex > 0)
+ {
+ // Overwrites an existing generic prerequisite (vanilla or custom)
+ RulesExt::Global()->GenericPrerequisites[existingIndex] = objectsList;
+ }
+ else
+ {
+ // New generic prerequisite
+ RulesExt::Global()->GenericPrerequisitesNames.AddItem(keyName);
+ RulesExt::Global()->GenericPrerequisites.AddItem(objectsList);
+ }
+
+ objectsList.Clear();
+ }
+}
+
// =============================
// load / save
@@ -883,6 +1038,19 @@ void RulesExt::ExtData::Serialize(T& Stm)
.Process(this->ShowDesignatorRange)
.Process(this->ShowPowerPlantEnhancerRange)
.Process(this->ShowGameTime)
+ .Process(this->GenericPrerequisites)
+ .Process(this->GenericPrerequisitesNames)
+ .Process(this->GenericPrerequisitesAlternates)
+ .Process(this->NewTeamsSelector)
+ .Process(this->NewTeamsSelector_SplitTriggersByCategory)
+ .Process(this->NewTeamsSelector_EnableFallback)
+ .Process(this->NewTeamsSelector_MergeUnclassifiedCategoryWith)
+ .Process(this->NewTeamsSelector_UnclassifiedCategoryPercentage)
+ .Process(this->NewTeamsSelector_GroundCategoryPercentage)
+ .Process(this->NewTeamsSelector_AirCategoryPercentage)
+ .Process(this->NewTeamsSelector_NavalCategoryPercentage)
+ .Process(this->NewTeamsSelector_InfantryCategoryPercentage)
+ .Process(this->NewTeamsSelector_VIPWeight)
.Process(this->DropPodTrailer)
.Process(this->DropPodDefaultTrailer)
.Process(this->PodImage)
diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h
index 993f1ca9a7..f3a1c70795 100644
--- a/src/Ext/Rules/Body.h
+++ b/src/Ext/Rules/Body.h
@@ -333,6 +333,21 @@ class RulesExt
Valueable AnimCraterDestroyTiberium;
+ DynamicVectorClass> GenericPrerequisites;
+ DynamicVectorClass GenericPrerequisitesNames;
+ DynamicVectorClass> GenericPrerequisitesAlternates;
+
+ Valueable NewTeamsSelector;
+ Valueable NewTeamsSelector_SplitTriggersByCategory;
+ Valueable NewTeamsSelector_EnableFallback;
+ Valueable NewTeamsSelector_MergeUnclassifiedCategoryWith;
+ Valueable NewTeamsSelector_UnclassifiedCategoryPercentage;
+ Valueable NewTeamsSelector_GroundCategoryPercentage;
+ Valueable NewTeamsSelector_NavalCategoryPercentage;
+ Valueable NewTeamsSelector_AirCategoryPercentage;
+ Valueable NewTeamsSelector_InfantryCategoryPercentage;
+ Valueable NewTeamsSelector_VIPWeight;
+
Valueable BerzerkTargeting;
Valueable AllowBerzerkOnAllies;
@@ -746,6 +761,19 @@ class RulesExt
, ShowDesignatorRange { true }
, ShowPowerPlantEnhancerRange { true }
, ShowGameTime { true }
+ , GenericPrerequisites { }
+ , GenericPrerequisitesNames { }
+ , GenericPrerequisitesAlternates { }
+ , NewTeamsSelector { false }
+ , NewTeamsSelector_SplitTriggersByCategory { true }
+ , NewTeamsSelector_EnableFallback { false }
+ , NewTeamsSelector_MergeUnclassifiedCategoryWith { -1 }
+ , NewTeamsSelector_UnclassifiedCategoryPercentage { 0.20 }
+ , NewTeamsSelector_GroundCategoryPercentage { 0.20 }
+ , NewTeamsSelector_NavalCategoryPercentage { 0.20 }
+ , NewTeamsSelector_AirCategoryPercentage { 0.20 }
+ , NewTeamsSelector_InfantryCategoryPercentage { 0.20 }
+ , NewTeamsSelector_VIPWeight { 5000.0 }
, DropPodTrailer { }
, DropPodDefaultTrailer { }
, PodImage { }
@@ -1079,4 +1107,6 @@ class RulesExt
{
Allocate(RulesClass::Instance);
}
+
+ static void FillDefaultPrerequisites();
};
diff --git a/src/Ext/Scenario/Hooks.NewTeamsSelector.cpp b/src/Ext/Scenario/Hooks.NewTeamsSelector.cpp
new file mode 100644
index 0000000000..0fb35377f1
--- /dev/null
+++ b/src/Ext/Scenario/Hooks.NewTeamsSelector.cpp
@@ -0,0 +1,83 @@
+#include "Body.h"
+
+#include
+#include
+#include
+
+DEFINE_HOOK(0x687C9B, ReadScenarioINI_AITeamSelector_PreloadValidTriggers, 0x7)
+{
+ if (!RulesExt::Global()->NewTeamsSelector)
+ return 0;
+
+ bool ignoreGlobalAITriggers = ScenarioClass::Instance->IgnoreGlobalAITriggers;
+
+ struct HousePreloadInfo
+ {
+ HouseExt* pExt;
+ int HouseTypeIdx;
+ int SideTypeIdx;
+ AIDifficulty Difficulty;
+ };
+
+ std::vector houses;
+ houses.reserve(HouseClass::Array.Count);
+
+ // Pre-cache house metadata once
+ for (HouseClass* pHouse : HouseClass::Array)
+ {
+ auto pHouseExt = HouseExt::Fetch(pHouse);
+ if (!pHouseExt)
+ continue;
+
+ pHouseExt->AITriggers_ValidList.clear();
+
+ int parentCountryTypeIdx = pHouse->Type->FindParentCountryIndex();
+ int houseTypeIdx = parentCountryTypeIdx >= 0 ? parentCountryTypeIdx : pHouse->Type->ArrayIndex;
+
+ int parentCountrySideTypeIdx = parentCountryTypeIdx >= 0 ? pHouse->Type->FindParentCountry()->SideIndex : pHouse->Type->SideIndex;
+ int sideTypeIdx = parentCountrySideTypeIdx >= 0 ? parentCountrySideTypeIdx + 1 : pHouse->Type->SideIndex + 1;
+
+ houses.push_back({ pHouseExt, houseTypeIdx, sideTypeIdx, pHouse->AIDifficulty });
+ }
+
+ // Single pass over all triggers
+ for (int i = 0; i < AITriggerTypeClass::Array.Count; i++)
+ {
+ auto pTrigger = AITriggerTypeClass::Array.GetItem(i);
+
+ if (!pTrigger || (ignoreGlobalAITriggers && pTrigger->IsGlobal && !pTrigger->IsEnabled) || !pTrigger->Team1)
+ continue;
+
+ int triggerHouse = pTrigger->HouseIndex;
+ int triggerSide = pTrigger->SideIndex;
+
+ for (const auto& house : houses)
+ {
+ // Check difficulty compatibility
+ if (((int)house.Difficulty == 0 && !pTrigger->Enabled_Hard)
+ || ((int)house.Difficulty == 1 && !pTrigger->Enabled_Normal)
+ || ((int)house.Difficulty == 2 && !pTrigger->Enabled_Easy))
+ {
+ continue;
+ }
+
+ // Check house and side compatibility
+ if ((triggerHouse == -1 || house.HouseTypeIdx == triggerHouse) && (triggerSide == 0 || house.SideTypeIdx == triggerSide))
+ {
+ house.pExt->AITriggers_ValidList.push_back(i);
+ }
+ }
+ }
+
+ for (HouseClass* pHouse : HouseClass::Array)
+ {
+ auto pHouseExt = HouseExt::Fetch(pHouse);
+ if (pHouseExt)
+ {
+ Debug::Log("AITeamsSelector - The house %d [%s](%s) should be able to use %d AI triggers in this map.\n",
+ pHouse->ArrayIndex, pHouse->Type->ID, pHouse->PlainName, pHouseExt->AITriggers_ValidList.size());
+ }
+ }
+
+ return 0;
+}
diff --git a/src/Ext/Team/Body.NewTeamsSelector.cpp b/src/Ext/Team/Body.NewTeamsSelector.cpp
new file mode 100644
index 0000000000..ec5a044a0b
--- /dev/null
+++ b/src/Ext/Team/Body.NewTeamsSelector.cpp
@@ -0,0 +1,998 @@
+#include "Body.h"
+#include
+#include
+
+enum class teamCategory : int
+{
+ None = 0, // No category. Should be default value
+ Ground = 1, // Ground vehicles (Tanks, etc.)
+ Air = 2, // Aircrafts & jumpjets
+ Naval = 3, // Naval units
+ Infantry = 4, // Infantry units
+ Unclassified = 5 // Mixed teams
+};
+
+struct CandidateTrigger
+{
+ AITriggerTypeClass* Trigger = nullptr;
+ double Weight = 0.0;
+ teamCategory Category = teamCategory::None;
+};
+
+DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5)
+{
+ enum { UseOriginalSelector = 0x4F8A63, SkipCode = 0x4F8B08 };
+
+ GET(HouseClass*, pHouse, ESI);
+
+ if (!RulesExt::Global()->NewTeamsSelector)
+ return UseOriginalSelector;
+
+ bool houseIsHuman = pHouse->IsHumanPlayer;
+ bool isCampaign = SessionClass::IsCampaign();
+
+ if (isCampaign)
+ houseIsHuman = pHouse->IsHumanPlayer || pHouse->IsInPlayerControl;
+
+ if (houseIsHuman || pHouse->Type->MultiplayPassive || !pHouse->AITriggersActive)
+ return SkipCode;
+
+ auto pHouseTypeExt = HouseTypeExt::Fetch(pHouse->Type);
+ if (!pHouseTypeExt)
+ return SkipCode;
+
+ // Reset Team selection countdown
+ int countdown = RulesClass::Instance->TeamDelays[(int)pHouse->AIDifficulty];
+ pHouse->TeamDelayTimer.Start(countdown);
+
+ int totalActiveTeams = 0;
+ int activeTeams = 0;
+
+ std::vector validCandidates;
+
+ int dice = ScenarioClass::Instance->Random.RandomRanged(1, 100);
+
+ // This house must have the triggers enabled
+ if (dice > pHouse->RatioAITriggerTeam || !pHouse->AITriggersActive)
+ return SkipCode;
+
+ bool splitTriggersByCategory = RulesExt::Global()->NewTeamsSelector_SplitTriggersByCategory;
+ bool isFallbackEnabled = RulesExt::Global()->NewTeamsSelector_EnableFallback;
+ teamCategory validCategory = teamCategory::None;
+ int mergeUnclassifiedCategoryWith = -1;
+
+ double percentageUnclassifiedTriggers = 0.0;
+ double percentageGroundTriggers = 0.0;
+ double percentageNavalTriggers = 0.0;
+ double percentageAirTriggers = 0.0;
+ double percentageInfantryTriggers = 0.0;
+
+ if (splitTriggersByCategory)
+ {
+ mergeUnclassifiedCategoryWith = pHouseTypeExt->NewTeamsSelector_MergeUnclassifiedCategoryWith.isset() ? pHouseTypeExt->NewTeamsSelector_MergeUnclassifiedCategoryWith.Get() : RulesExt::Global()->NewTeamsSelector_MergeUnclassifiedCategoryWith; // Should mixed teams be merged into another category?
+ percentageUnclassifiedTriggers = pHouseTypeExt->NewTeamsSelector_UnclassifiedCategoryPercentage.isset() ? pHouseTypeExt->NewTeamsSelector_UnclassifiedCategoryPercentage.Get() : RulesExt::Global()->NewTeamsSelector_UnclassifiedCategoryPercentage; // Mixed teams
+ percentageGroundTriggers = pHouseTypeExt->NewTeamsSelector_GroundCategoryPercentage.isset() ? pHouseTypeExt->NewTeamsSelector_GroundCategoryPercentage.Get() : RulesExt::Global()->NewTeamsSelector_GroundCategoryPercentage; // Only ground vehicles
+ percentageNavalTriggers = pHouseTypeExt->NewTeamsSelector_NavalCategoryPercentage.isset() ? pHouseTypeExt->NewTeamsSelector_NavalCategoryPercentage.Get() : RulesExt::Global()->NewTeamsSelector_NavalCategoryPercentage; // Only Naval=yes
+ percentageAirTriggers = pHouseTypeExt->NewTeamsSelector_AirCategoryPercentage.isset() ? pHouseTypeExt->NewTeamsSelector_AirCategoryPercentage.Get() : RulesExt::Global()->NewTeamsSelector_AirCategoryPercentage; // Only Aircrafts & jumpjets
+ percentageInfantryTriggers = pHouseTypeExt->NewTeamsSelector_InfantryCategoryPercentage.isset() ? pHouseTypeExt->NewTeamsSelector_InfantryCategoryPercentage.Get() : RulesExt::Global()->NewTeamsSelector_InfantryCategoryPercentage; // Only infantry
+
+ // Merge mixed category with another category, if set
+ if (mergeUnclassifiedCategoryWith >= 0)
+ {
+ switch (mergeUnclassifiedCategoryWith)
+ {
+ case (int)teamCategory::Ground:
+ percentageGroundTriggers += percentageUnclassifiedTriggers;
+ break;
+
+ case (int)teamCategory::Air:
+ percentageAirTriggers += percentageUnclassifiedTriggers;
+ break;
+
+ case (int)teamCategory::Naval:
+ percentageNavalTriggers += percentageUnclassifiedTriggers;
+ break;
+
+ case (int)teamCategory::Infantry:
+ percentageInfantryTriggers += percentageUnclassifiedTriggers;
+ break;
+
+ default:
+ break;
+ }
+
+ percentageUnclassifiedTriggers = 0.0;
+ }
+
+ percentageUnclassifiedTriggers = (percentageUnclassifiedTriggers < 0.0) ? 0.0 : percentageUnclassifiedTriggers;
+ percentageGroundTriggers = (percentageGroundTriggers < 0.0) ? 0.0 : percentageGroundTriggers;
+ percentageNavalTriggers = (percentageNavalTriggers < 0.0) ? 0.0 : percentageNavalTriggers;
+ percentageAirTriggers = (percentageAirTriggers < 0.0) ? 0.0 : percentageAirTriggers;
+ percentageInfantryTriggers = (percentageInfantryTriggers < 0.0) ? 0.0 : percentageInfantryTriggers;
+
+ double const totalPercentages = percentageUnclassifiedTriggers + percentageGroundTriggers
+ + percentageNavalTriggers + percentageAirTriggers + percentageInfantryTriggers;
+
+ if (totalPercentages <= 0.0)
+ {
+ splitTriggersByCategory = false;
+ }
+ else
+ {
+ double const categoryRoll = ScenarioClass::Instance->Random.RandomDouble() * totalPercentages;
+ double accumulated = 0.0;
+
+ // Pick what type of team will be selected in this round
+ if (percentageUnclassifiedTriggers > 0.0 && categoryRoll < (accumulated += percentageUnclassifiedTriggers))
+ {
+ validCategory = teamCategory::Unclassified;
+ }
+ else if (percentageGroundTriggers > 0.0 && categoryRoll < (accumulated += percentageGroundTriggers))
+ {
+ validCategory = teamCategory::Ground;
+ }
+ else if (percentageAirTriggers > 0.0 && categoryRoll < (accumulated += percentageAirTriggers))
+ {
+ validCategory = teamCategory::Air;
+ }
+ else if (percentageNavalTriggers > 0.0 && categoryRoll < (accumulated += percentageNavalTriggers))
+ {
+ validCategory = teamCategory::Naval;
+ }
+ else if (percentageInfantryTriggers > 0.0 && categoryRoll < (accumulated += percentageInfantryTriggers))
+ {
+ validCategory = teamCategory::Infantry;
+ }
+ else
+ {
+ splitTriggersByCategory = false;
+ }
+ }
+ }
+
+ if (splitTriggersByCategory)
+ {
+ switch (validCategory)
+ {
+ case teamCategory::Ground:
+ Debug::Log("AITeamsSelector - This time only GROUND VEHICLE teams will be picked.\n");
+ break;
+
+ case teamCategory::Infantry:
+ Debug::Log("AITeamsSelector - This time only INFANTRY teams will be picked.\n");
+ break;
+
+ case teamCategory::Unclassified:
+ Debug::Log("AITeamsSelector - This time only MIXED teams will be picked.\n");
+ break;
+
+ case teamCategory::Naval:
+ Debug::Log("AITeamsSelector - This time only NAVAL teams will be picked.\n");
+ break;
+
+ case teamCategory::Air:
+ Debug::Log("AITeamsSelector - This time only AIR teams will be picked.\n");
+ break;
+
+ default:
+ Debug::Log("AITeamsSelector - This time teams categories are DISABLED. Anyone can be picked\n");
+ break;
+ }
+ }
+
+ auto houseDifficulty = pHouse->AIDifficulty;
+ int minBaseDefenseTeams = RulesClass::Instance->MinimumAIDefensiveTeams.GetItem((int)houseDifficulty);
+ int maxBaseDefenseTeams = RulesClass::Instance->MaximumAIDefensiveTeams.GetItem((int)houseDifficulty);
+ int activeDefenseTeamsCount = 0;
+ int maxTeamsLimit = RulesClass::Instance->TotalAITeamCap.GetItem((int)houseDifficulty);
+
+ // Check running teams owned by this house
+ std::unordered_map activeTeamCounts;
+
+ for (auto const pRunningTeam : TeamClass::Array)
+ {
+ totalActiveTeams++;
+ if (pRunningTeam->Owner != pHouse)
+ continue;
+
+ activeTeams++;
+ activeTeamCounts[pRunningTeam->Type]++;
+
+ if (pRunningTeam->Type->IsBaseDefense)
+ activeDefenseTeamsCount++;
+ }
+
+ // We will use these values for discarding triggers
+ int defensiveTeamsLimit = RulesClass::Instance->UseMinDefenseRule ? minBaseDefenseTeams : maxBaseDefenseTeams;
+ bool hasReachedMaxTeamsLimit = activeTeams >= maxTeamsLimit;
+ bool hasReachedMaxDefensiveTeamsLimit = activeDefenseTeamsCount >= defensiveTeamsLimit;
+
+ // Check if the next team must be a defensive team
+ bool onlyPickDefensiveTeams = false;
+ int defensiveDice = ScenarioClass::Instance->Random.RandomRanged(0, 99);
+ int defenseTeamSelectionThreshold = 50;
+
+ if ((defensiveDice < defenseTeamSelectionThreshold) && !hasReachedMaxDefensiveTeamsLimit && !isCampaign)
+ onlyPickDefensiveTeams = true;
+
+ if (hasReachedMaxTeamsLimit)
+ {
+ Debug::Log("AITeamsSelector - House %d [%s](%s) reached the TotalAITeamCap (%d)! Skipping operation this time...\n", pHouse->ArrayIndex, pHouse->PlainName, pHouse->Type->ID, maxTeamsLimit);
+ return SkipCode;
+ }
+
+ if (onlyPickDefensiveTeams)
+ Debug::Log("AITeamsSelector - House %d [%s](%s) currently has %d/%d active DEFENSIVE teams and in total %d/%d active teams...\n", pHouse->ArrayIndex, pHouse->PlainName, pHouse->Type->ID, activeDefenseTeamsCount, defensiveTeamsLimit, totalActiveTeams, maxTeamsLimit);
+ else
+ Debug::Log("AITeamsSelector - House %d [%s](%s) currently has %d/%d active teams...\n", pHouse->ArrayIndex, pHouse->PlainName, pHouse->Type->ID, totalActiveTeams, maxTeamsLimit);
+
+ int destroyedBridgesCount = 0;
+ int undamagedBridgesCount = 0;
+ std::unordered_map ownedRecruitables;
+ bool hasInfantryFactory = false;
+ bool hasUnitFactory = false;
+ bool hasNavalFactory = false;
+ bool hasAircraftFactory = false;
+ bool canAutocreate = false;
+
+ for (auto const pTechno : TechnoClass::Array)
+ {
+ if (!TechnoExt::IsValidTechno(pTechno) || !pTechno->IsAlive || pTechno->Health <= 0)
+ continue;
+
+ if (pTechno->WhatAmI() == AbstractType::Building)
+ {
+ auto const pBuildingType = static_cast(pTechno->GetTechnoType());
+
+ if (!pBuildingType)
+ continue;
+
+ if (pTechno->Owner == pHouse)
+ {
+ switch (pBuildingType->Factory)
+ {
+ case AbstractType::InfantryType:
+ hasInfantryFactory = true;
+ break;
+
+ case AbstractType::AircraftType:
+ hasAircraftFactory = true;
+ break;
+
+ case AbstractType::UnitType:
+ if (pBuildingType->Naval)
+ hasNavalFactory = true;
+ else
+ hasUnitFactory = true;
+ break;
+
+ default:
+ break;
+ }
+ }
+ else if (pBuildingType->BridgeRepairHut)
+ {
+ CellStruct cell = pTechno->GetCell()->MapCoords;
+
+ if (MapClass::Instance.IsLinkedBridgeDestroyed(cell))
+ destroyedBridgesCount++;
+ else
+ undamagedBridgesCount++;
+ }
+
+ continue;
+ }
+
+ auto const pFoot = static_cast(pTechno);
+
+ if (!pFoot
+ || !pTechno->IsOnMap // Note: underground movement is considered "IsOnMap == false"
+ || pTechno->Transporter
+ || pTechno->Absorbed
+ || !pFoot->CanBeRecruited(pHouse))
+ {
+ continue;
+ }
+
+ ++ownedRecruitables[pTechno->GetTechnoType()];
+ }
+
+ if (hasInfantryFactory || hasUnitFactory || hasAircraftFactory || hasNavalFactory)
+ canAutocreate = true;
+
+ HouseClass* targetHouse = nullptr;
+ if (pHouse->EnemyHouseIndex >= 0)
+ targetHouse = HouseClass::Array.GetItem(pHouse->EnemyHouseIndex);
+
+ bool onlyCheckImportantTriggers = false;
+
+ const auto pHouseExt = HouseExt::Fetch(pHouse);
+ double const vipWeight = pHouseTypeExt->NewTeamsSelector_VIPWeight.isset()
+ ? pHouseTypeExt->NewTeamsSelector_VIPWeight.Get()
+ : RulesExt::Global()->NewTeamsSelector_VIPWeight;
+
+ auto const isTriggerVIP = [vipWeight](AITriggerTypeClass* pTrig) -> bool
+ {
+ if (vipWeight < 0.0)
+ return pTrig->Weight_Current >= pTrig->Weight_Maximum && pTrig->Weight_Maximum > 0.0;
+ if (vipWeight > 0.0)
+ return pTrig->Weight_Current >= vipWeight;
+ return false;
+ };
+
+ // Gather all the trigger candidates into one place for posterior fast calculations
+ for (int triggerIdx : pHouseExt->AITriggers_ValidList)
+ {
+ const auto pTrigger = AITriggerTypeClass::Array.GetItem(triggerIdx);
+
+ if (!pTrigger || !pTrigger->Team1 || !pTrigger->IsEnabled)
+ continue;
+
+ // If VIP triggers were found, discard any non-VIP trigger immediately
+ if (onlyCheckImportantTriggers && !isTriggerVIP(pTrigger))
+ continue;
+
+ if (pTrigger->Team1->TechLevel > pHouse->TechLevel)
+ continue;
+
+ // Ignore offensive teams if the next trigger must be defensive
+ if ((onlyPickDefensiveTeams && !pTrigger->IsForBaseDefense) || (hasReachedMaxDefensiveTeamsLimit && pTrigger->IsForBaseDefense))
+ continue;
+
+ // If this type of Team reached the max then skip it immediately
+ auto const itTeam1 = activeTeamCounts.find(pTrigger->Team1);
+ int const currentTeam1Count = (itTeam1 != activeTeamCounts.end()) ? itTeam1->second : 0;
+
+ if (currentTeam1Count >= pTrigger->Team1->Max)
+ continue;
+
+ teamCategory teamIsCategory = teamCategory::None;
+
+ // Analyze what kind of category is this main team if the feature is enabled
+ if (splitTriggersByCategory)
+ {
+ // TaskForces are limited to 6 entries
+ for (int i = 0; i < 6; i++)
+ {
+ const auto& entry = pTrigger->Team1->TaskForce->Entries[i];
+ teamCategory entryIsCategory = teamCategory::Ground;
+
+ if (entry.Amount > 0)
+ {
+ if (!entry.Type)
+ continue;
+
+ if (entry.Type->WhatAmI() == AbstractType::AircraftType
+ || entry.Type->ConsideredAircraft)
+ {
+ // This unit is from air category
+ entryIsCategory = teamCategory::Air;
+ }
+ else
+ {
+ auto pTechnoTypeExt = TechnoTypeExt::Fetch(entry.Type);
+
+ if (pTechnoTypeExt && (pTechnoTypeExt->ConsideredNaval
+ || (entry.Type->Naval
+ && (entry.Type->MovementZone != MovementZone::Amphibious
+ && entry.Type->MovementZone != MovementZone::AmphibiousDestroyer
+ && entry.Type->MovementZone != MovementZone::AmphibiousCrusher))))
+ {
+ // This unit is from naval category
+ entryIsCategory = teamCategory::Naval;
+ }
+ else if (entry.Type->WhatAmI() == AbstractType::InfantryType
+ && (!pTechnoTypeExt || !pTechnoTypeExt->ConsideredVehicle))
+ {
+ // This unit is from infantry category
+ entryIsCategory = teamCategory::Infantry;
+ }
+ else
+ {
+ // This unit is from ground vehicles category
+ entryIsCategory = teamCategory::Ground;
+ }
+ }
+
+ // If a team has multiple categories it will be a mixed category
+ teamIsCategory = (teamIsCategory == teamCategory::None || teamIsCategory == entryIsCategory) ? entryIsCategory : teamCategory::Unclassified;
+
+ if (teamIsCategory == teamCategory::Unclassified)
+ break;
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ // If this value is set and the team is MIXED, override category type
+ if (teamIsCategory == teamCategory::Unclassified
+ && mergeUnclassifiedCategoryWith >= 0)
+ {
+ teamIsCategory = (teamCategory)mergeUnclassifiedCategoryWith;
+ }
+
+ // Early discard if fallback is disabled and category does not match
+ if (!isFallbackEnabled && validCategory != teamCategory::None && validCategory != teamIsCategory)
+ continue;
+ }
+
+ if (!TeamExt::EvaluateTriggerCondition(pTrigger, pHouse, targetHouse, hasReachedMaxDefensiveTeamsLimit, destroyedBridgesCount, undamagedBridgesCount))
+ continue;
+
+ if (!pTrigger->Team1->Autocreate && !pTrigger->Team1->Recruiter)
+ continue;
+
+ bool canProduce = false;
+
+ // Check if the team can be produced via autocreate
+ if (pTrigger->Team1->Autocreate && canAutocreate)
+ {
+ canProduce = true;
+
+ for (const auto& entry : pTrigger->Team1->TaskForce->Entries)
+ {
+ if (!entry.Type || entry.Amount == 0)
+ break;
+
+ bool hasFactory = false;
+ switch (entry.Type->WhatAmI())
+ {
+ case AbstractType::InfantryType:
+ hasFactory = hasInfantryFactory;
+ break;
+
+ case AbstractType::AircraftType:
+ hasFactory = hasAircraftFactory;
+ break;
+
+ case AbstractType::UnitType:
+ hasFactory = entry.Type->Naval ? hasNavalFactory : hasUnitFactory;
+ break;
+
+ default:
+ break;
+ }
+
+ if (!hasFactory || !HouseExt::PrerequisitesMet(pHouse, entry.Type, false))
+ {
+ canProduce = false;
+ break;
+ }
+ }
+ }
+
+ // Check if the team can be recruited from existing map units
+ if (!canProduce && pTrigger->Team1->Recruiter)
+ {
+ bool canRecruit = true;
+
+ for (const auto& entry : pTrigger->Team1->TaskForce->Entries)
+ {
+ if (!entry.Type || entry.Amount == 0)
+ break;
+
+ auto const it = ownedRecruitables.find(entry.Type);
+ int const availableRecruits = (it != ownedRecruitables.end()) ? it->second : 0;
+
+ if (availableRecruits < entry.Amount)
+ {
+ canRecruit = false;
+ break;
+ }
+ }
+
+ canProduce = canRecruit;
+ }
+
+ if (!canProduce)
+ continue;
+
+ // Special case: triggers become VIP if they meet the configured VIP weight threshold.
+ // They get stored in an elitist list and all previous non-VIP triggers are discarded.
+ if (isTriggerVIP(pTrigger) && !onlyCheckImportantTriggers)
+ {
+ validCandidates.clear();
+ onlyCheckImportantTriggers = true;
+ validCategory = teamCategory::None;
+ splitTriggersByCategory = false; // VIP teams break the categories logic (on purpose)
+ }
+
+ // Passed all checks, save this trigger for later.
+ double const weight = (pTrigger->Weight_Current < 1.0) ? 1.0 : pTrigger->Weight_Current;
+ validCandidates.push_back({ pTrigger, weight, teamIsCategory });
+ }
+
+ if (validCandidates.empty())
+ {
+ Debug::Log("AITeamsSelector - The house %d [%s](%s) has no valid triggers for now. A new attempt will be done later...\n", pHouse->ArrayIndex, pHouse->PlainName, pHouse->Type->ID);
+ return SkipCode;
+ }
+
+ // Calculate category stats and candidate counts
+ double totalPoolWeight = 0.0;
+ int poolCandidatesCount = 0;
+
+ for (const auto& candidate : validCandidates)
+ {
+ if (validCategory == teamCategory::None || candidate.Category == validCategory)
+ {
+ totalPoolWeight += candidate.Weight;
+ poolCandidatesCount++;
+ }
+ }
+
+ if (validCategory != teamCategory::None && poolCandidatesCount == 0)
+ {
+ Debug::Log("AITeamsSelector - The house %d [%s](%s) has no valid triggers of category %d. A new attempt should be done later...\n",
+ pHouse->ArrayIndex, pHouse->PlainName, pHouse->Type->ID, static_cast(validCategory));
+
+ if (!isFallbackEnabled)
+ return SkipCode;
+
+ Debug::Log("... but FALLBACK MODE is enabled so now all available triggers will be checked.\n");
+ validCategory = teamCategory::None;
+
+ for (const auto& candidate : validCandidates)
+ totalPoolWeight += candidate.Weight;
+
+ poolCandidatesCount = static_cast(validCandidates.size());
+ }
+
+ AITriggerTypeClass* selectedTrigger = nullptr;
+
+ // Roulette Wheel Selection (Discrete Distribution)
+ if (totalPoolWeight > 0.0)
+ {
+ double const weightDice = ScenarioClass::Instance->Random.RandomDouble() * totalPoolWeight;
+
+ if (validCategory != teamCategory::None)
+ {
+ Debug::Log("AITeamsSelector - Picking 1 team (of category %d) from the %d available (Total Weight: %f, Roll: %f)...\n",
+ static_cast(validCategory), poolCandidatesCount, totalPoolWeight, weightDice);
+ }
+ else
+ {
+ Debug::Log("AITeamsSelector - Picking 1 team from the %d available (Total Weight: %f, Roll: %f)...\n",
+ poolCandidatesCount, totalPoolWeight, weightDice);
+ }
+
+ double accumulated = 0.0;
+ for (const auto& candidate : validCandidates)
+ {
+ if (validCategory == teamCategory::None || candidate.Category == validCategory)
+ {
+ accumulated += candidate.Weight;
+ if (weightDice < accumulated)
+ {
+ selectedTrigger = candidate.Trigger;
+ break;
+ }
+ }
+ }
+ }
+
+ if (!selectedTrigger)
+ {
+ Debug::Log("AITeamsSelector - Failed picking a new trigger for the House %d [%s](%s). A new attempt will be done later...\n", pHouse->ArrayIndex, pHouse->PlainName, pHouse->Type->ID);
+ return SkipCode;
+ }
+
+ if (isTriggerVIP(selectedTrigger))
+ {
+ if (vipWeight < 0.0)
+ {
+ if (selectedTrigger->Weight_Minimum < selectedTrigger->Weight_Current)
+ selectedTrigger->Weight_Current -= 1.0;
+ }
+ else if (vipWeight > 0.0)
+ {
+ if (selectedTrigger->Weight_Minimum <= (vipWeight - 1.0))
+ selectedTrigger->Weight_Current = vipWeight - 1.0;
+ }
+ }
+
+ // We have a winner trigger here
+ Debug::Log("AITeamsSelector - House %d [%s](%s) selected trigger [%s]: %s.\n", pHouse->ArrayIndex, pHouse->PlainName, pHouse->Type->ID, selectedTrigger->ID, selectedTrigger->Team1->Name);
+
+ // Team 1 creation
+ auto const itTeam1 = activeTeamCounts.find(selectedTrigger->Team1);
+ int const count1 = (itTeam1 != activeTeamCounts.end()) ? itTeam1->second : 0;
+
+ if (count1 < selectedTrigger->Team1->Max)
+ {
+ if (TeamClass* newTeam1 = selectedTrigger->Team1->CreateTeam(pHouse))
+ newTeam1->NeedsToDisappear = false;
+ }
+
+ // Team 2 creation (if set)
+ if (auto const pTriggerTeam2Type = selectedTrigger->Team2)
+ {
+ auto const itTeam2 = activeTeamCounts.find(pTriggerTeam2Type);
+ int const count2 = (itTeam2 != activeTeamCounts.end()) ? itTeam2->second : 0;
+
+ if (count2 < pTriggerTeam2Type->Max)
+ {
+ if (TeamClass* newTeam2 = pTriggerTeam2Type->CreateTeam(pHouse))
+ newTeam2->NeedsToDisappear = false;
+ }
+ }
+
+ return SkipCode;
+}
+
+bool TeamExt::HouseOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, bool allies, TechnoTypeClass* pItem)
+{
+ int counter = 0;
+ if (pItem && pHouse)
+ {
+ if (!allies)
+ {
+ counter = pHouse->CountOwnedAndPresent(pItem);
+ }
+ else
+ {
+ for (auto const pOtherHouse : HouseClass::Array)
+ {
+ if (pOtherHouse != pHouse && pHouse->IsAlliedWith(pOtherHouse) && !pOtherHouse->Type->MultiplayPassive)
+ {
+ counter += pOtherHouse->CountOwnedAndPresent(pItem);
+ }
+ }
+ }
+ }
+ return TeamExt::CountConditionMet(pThis, counter);
+}
+
+bool TeamExt::HouseOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, bool allies, const std::vector& list)
+{
+ int counter = 0;
+ if (pHouse)
+ {
+ for (auto const pItem : list)
+ {
+ if (!pItem) continue;
+
+ if (!allies)
+ {
+ counter += pHouse->CountOwnedAndPresent(pItem);
+ }
+ else
+ {
+ for (auto const pOtherHouse : HouseClass::Array)
+ {
+ if (pOtherHouse != pHouse && pHouse->IsAlliedWith(pOtherHouse) && !pOtherHouse->Type->MultiplayPassive)
+ {
+ counter += pOtherHouse->CountOwnedAndPresent(pItem);
+ }
+ }
+ }
+ }
+ }
+ return TeamExt::CountConditionMet(pThis, counter);
+}
+
+bool TeamExt::EnemyOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, bool onlySelectedEnemy, TechnoTypeClass* pItem)
+{
+ int counter = 0;
+ if (pItem && pHouse)
+ {
+ if (onlySelectedEnemy && pEnemy && !pHouse->IsAlliedWith(pEnemy))
+ {
+ counter = pEnemy->CountOwnedAndPresent(pItem);
+ }
+ else
+ {
+ for (auto const pOtherHouse : HouseClass::Array)
+ {
+ if (pOtherHouse != pHouse && !pHouse->IsAlliedWith(pOtherHouse) && !pOtherHouse->Type->MultiplayPassive)
+ {
+ counter += pOtherHouse->CountOwnedAndPresent(pItem);
+ }
+ }
+ }
+ }
+ return TeamExt::CountConditionMet(pThis, counter);
+}
+
+bool TeamExt::EnemyOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, bool onlySelectedEnemy, const std::vector& list)
+{
+ int counter = 0;
+ if (pHouse)
+ {
+ for (auto const pItem : list)
+ {
+ if (!pItem) continue;
+
+ if (onlySelectedEnemy && pEnemy && !pHouse->IsAlliedWith(pEnemy))
+ {
+ counter += pEnemy->CountOwnedAndPresent(pItem);
+ }
+ else
+ {
+ for (auto const pOtherHouse : HouseClass::Array)
+ {
+ if (pOtherHouse != pHouse && !pHouse->IsAlliedWith(pOtherHouse) && !pOtherHouse->Type->MultiplayPassive)
+ {
+ counter += pOtherHouse->CountOwnedAndPresent(pItem);
+ }
+ }
+ }
+ }
+ }
+ return TeamExt::CountConditionMet(pThis, counter);
+}
+
+bool TeamExt::NeutralOwns(AITriggerTypeClass* pThis, TechnoTypeClass* pItem)
+{
+ int counter = 0;
+ if (pItem)
+ {
+ int const civilianSideIndex = SideClass::FindIndex("Civilian");
+ for (auto const pOtherHouse : HouseClass::Array)
+ {
+ if (pOtherHouse->Type->SideIndex == civilianSideIndex)
+ {
+ counter += pOtherHouse->CountOwnedAndPresent(pItem);
+ }
+ }
+ }
+ return TeamExt::CountConditionMet(pThis, counter);
+}
+
+bool TeamExt::NeutralOwns(AITriggerTypeClass* pThis, const std::vector& list)
+{
+ int counter = 0;
+ int const civilianSideIndex = SideClass::FindIndex("Civilian");
+ for (auto const pItem : list)
+ {
+ if (!pItem) continue;
+
+ for (auto const pOtherHouse : HouseClass::Array)
+ {
+ if (pOtherHouse->Type->SideIndex == civilianSideIndex)
+ {
+ counter += pOtherHouse->CountOwnedAndPresent(pItem);
+ }
+ }
+ }
+ return TeamExt::CountConditionMet(pThis, counter);
+}
+
+bool TeamExt::HouseOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, const std::vector& list)
+{
+ if (list.empty() || !pHouse)
+ return false;
+
+ for (auto const pItem : list)
+ {
+ if (!pItem) continue;
+
+ int const counter = pHouse->CountOwnedAndPresent(pItem);
+ if (!TeamExt::CountConditionMet(pThis, counter))
+ return false;
+ }
+
+ return true;
+}
+
+bool TeamExt::EnemyOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, const std::vector& list)
+{
+ if (list.empty() || !pHouse)
+ return false;
+
+ if (pEnemy && pHouse->IsAlliedWith(pEnemy))
+ pEnemy = nullptr;
+
+ for (auto const pItem : list)
+ {
+ if (!pItem) continue;
+
+ int counter = 0;
+ if (pEnemy)
+ {
+ counter = pEnemy->CountOwnedAndPresent(pItem);
+ }
+ else
+ {
+ for (auto const pOtherHouse : HouseClass::Array)
+ {
+ if (pOtherHouse != pHouse && !pHouse->IsAlliedWith(pOtherHouse) && !pOtherHouse->Type->MultiplayPassive)
+ {
+ counter += pOtherHouse->CountOwnedAndPresent(pItem);
+ }
+ }
+ }
+
+ if (!TeamExt::CountConditionMet(pThis, counter))
+ return false;
+ }
+
+ return true;
+}
+
+bool TeamExt::NeutralOwnsAll(AITriggerTypeClass* pThis, const std::vector& list)
+{
+ if (list.empty())
+ return false;
+
+ int const civilianSideIndex = SideClass::FindIndex("Civilian");
+ for (auto const pOtherHouse : HouseClass::Array)
+ {
+ if (pOtherHouse->Type->SideIndex != civilianSideIndex)
+ continue;
+
+ bool allMet = true;
+ for (auto const pItem : list)
+ {
+ if (!pItem) continue;
+
+ int const counter = pOtherHouse->CountOwnedAndPresent(pItem);
+ if (!TeamExt::CountConditionMet(pThis, counter))
+ {
+ allMet = false;
+ break;
+ }
+ }
+
+ if (allMet)
+ return true;
+ }
+
+ return false;
+}
+
+bool TeamExt::CountConditionMet(AITriggerTypeClass* pThis, int nObjects)
+{
+ if (!pThis || nObjects < 0)
+ return false;
+
+ switch (pThis->Conditions[0].ComparatorType)
+ {
+ case AITriggerConditionComparatorType::Less:
+ return nObjects < pThis->Conditions[0].ComparatorOperand;
+ case AITriggerConditionComparatorType::LessOrEqual:
+ return nObjects <= pThis->Conditions[0].ComparatorOperand;
+ case AITriggerConditionComparatorType::Equal:
+ return nObjects == pThis->Conditions[0].ComparatorOperand;
+ case AITriggerConditionComparatorType::GreaterOrEqual:
+ return nObjects >= pThis->Conditions[0].ComparatorOperand;
+ case AITriggerConditionComparatorType::Greater:
+ return nObjects > pThis->Conditions[0].ComparatorOperand;
+ case AITriggerConditionComparatorType::NotEqual:
+ return nObjects != pThis->Conditions[0].ComparatorOperand;
+ default:
+ return true;
+ }
+}
+
+bool TeamExt::EvaluateTriggerCondition(
+ AITriggerTypeClass* pTrigger,
+ HouseClass* pHouse,
+ HouseClass* pTargetHouse,
+ bool hasReachedMaxDefensiveTeamsLimit,
+ int destroyedBridgesCount,
+ int undamagedBridgesCount)
+{
+ if (!pTrigger || !pHouse)
+ return false;
+
+ if ((int)pTrigger->ConditionType < 0)
+ return true;
+
+ switch ((int)pTrigger->ConditionType)
+ {
+ case 0:
+ // Target enemy owns specified object
+ return pTrigger->ConditionObject && TeamExt::EnemyOwns(pTrigger, pHouse, pTargetHouse, true, pTrigger->ConditionObject);
+
+ case 1:
+ // House owns specified object
+ return pTrigger->ConditionObject && TeamExt::HouseOwns(pTrigger, pHouse, false, pTrigger->ConditionObject);
+
+ case 4:
+ // Target enemy credits threshold
+ return pTrigger->HouseCredits(nullptr, pTargetHouse);
+
+ case 5:
+ // Iron Curtain is charged
+ return pTrigger->IronCurtainCharged(pHouse, nullptr);
+
+ case 6:
+ // Chronosphere is charged
+ return pTrigger->ChronoSphereCharged(pHouse, nullptr);
+
+ case 7:
+ // Neutral/civilian house owns specified object
+ return pTrigger->ConditionObject && TeamExt::NeutralOwns(pTrigger, pTrigger->ConditionObject);
+
+ case 8:
+ // Any enemy owns specified object
+ return pTrigger->ConditionObject && TeamExt::EnemyOwns(pTrigger, pHouse, nullptr, false, pTrigger->ConditionObject);
+
+ case 9:
+ // Target enemy owns at least one object from list
+ {
+ auto const listIdx = pTrigger->Conditions[3].ComparatorOperand;
+ return (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size())
+ && TeamExt::EnemyOwns(pTrigger, pHouse, pTargetHouse, false, RulesExt::Global()->AITargetTypesLists[listIdx]);
+ }
+
+ case 10:
+ // House owns at least one object from list
+ {
+ auto const listIdx = pTrigger->Conditions[3].ComparatorOperand;
+ return (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size())
+ && TeamExt::HouseOwns(pTrigger, pHouse, false, RulesExt::Global()->AITargetTypesLists[listIdx]);
+ }
+
+ case 11:
+ // Neutral/civilian house owns at least one object from list
+ {
+ auto const listIdx = pTrigger->Conditions[3].ComparatorOperand;
+ return (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size())
+ && TeamExt::NeutralOwns(pTrigger, RulesExt::Global()->AITargetTypesLists[listIdx]);
+ }
+
+ case 12:
+ // Any enemy owns at least one object from list
+ {
+ auto const listIdx = pTrigger->Conditions[3].ComparatorOperand;
+ return (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size())
+ && TeamExt::EnemyOwns(pTrigger, pHouse, nullptr, false, RulesExt::Global()->AITargetTypesLists[listIdx]);
+ }
+
+ case 13:
+ // House or allies own at least one object from list
+ {
+ auto const listIdx = pTrigger->Conditions[3].ComparatorOperand;
+ return (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size())
+ && TeamExt::HouseOwns(pTrigger, pHouse, true, RulesExt::Global()->AITargetTypesLists[listIdx]);
+ }
+
+ case 14:
+ // Target enemy owns all objects from list
+ {
+ auto const listIdx = pTrigger->Conditions[3].ComparatorOperand;
+ return (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size())
+ && TeamExt::EnemyOwnsAll(pTrigger, pHouse, pTargetHouse, RulesExt::Global()->AITargetTypesLists[listIdx]);
+ }
+
+ case 15:
+ // House owns all objects from list
+ {
+ auto const listIdx = pTrigger->Conditions[3].ComparatorOperand;
+ return (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size())
+ && TeamExt::HouseOwnsAll(pTrigger, pHouse, RulesExt::Global()->AITargetTypesLists[listIdx]);
+ }
+
+ case 16:
+ // Neutral/civilian house owns all objects from list
+ {
+ auto const listIdx = pTrigger->Conditions[3].ComparatorOperand;
+ return (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size())
+ && TeamExt::NeutralOwnsAll(pTrigger, RulesExt::Global()->AITargetTypesLists[listIdx]);
+ }
+
+ case 17:
+ // Any enemy owns all objects from list
+ {
+ auto const listIdx = pTrigger->Conditions[3].ComparatorOperand;
+ return (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size())
+ && TeamExt::EnemyOwnsAll(pTrigger, pHouse, nullptr, RulesExt::Global()->AITargetTypesLists[listIdx]);
+ }
+
+ case 18:
+ // Destroyed bridges count
+ return TeamExt::CountConditionMet(pTrigger, destroyedBridgesCount);
+
+ case 19:
+ // Undamaged bridges count
+ return TeamExt::CountConditionMet(pTrigger, undamagedBridgesCount);
+
+ default:
+ // Fallback to vanilla condition evaluator
+ return pTrigger->ConditionMet(pHouse, pTargetHouse, hasReachedMaxDefensiveTeamsLimit);
+ }
+}
diff --git a/src/Ext/Team/Body.h b/src/Ext/Team/Body.h
index 5d6b9916b5..cc37f0073b 100644
--- a/src/Ext/Team/Body.h
+++ b/src/Ext/Team/Body.h
@@ -1,7 +1,13 @@
#pragma once
#include
+#include
#include
+#include
+#include
+#include
+#include
+#include
#include
#include
@@ -75,6 +81,24 @@ class TeamExt final : public AbstractExt, public Detach::Listener
static ExtContainer ExtMap;
+ static bool HouseOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, bool allies, TechnoTypeClass* pItem);
+ static bool HouseOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, bool allies, const std::vector& list);
+ static bool HouseOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, const std::vector& list);
+ static bool EnemyOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, bool onlySelectedEnemy, TechnoTypeClass* pItem);
+ static bool EnemyOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, bool onlySelectedEnemy, const std::vector& list);
+ static bool EnemyOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, const std::vector& list);
+ static bool NeutralOwns(AITriggerTypeClass* pThis, TechnoTypeClass* pItem);
+ static bool NeutralOwns(AITriggerTypeClass* pThis, const std::vector& list);
+ static bool NeutralOwnsAll(AITriggerTypeClass* pThis, const std::vector& list);
+ static bool CountConditionMet(AITriggerTypeClass* pThis, int nObjects);
+ static bool EvaluateTriggerCondition(
+ AITriggerTypeClass* pTrigger,
+ HouseClass* pHouse,
+ HouseClass* pTargetHouse = nullptr,
+ bool hasReachedMaxDefensiveTeamsLimit = false,
+ int destroyedBridgesCount = 0,
+ int undamagedBridgesCount = 0);
+
static TeamExt* Fetch(const TeamClass* pThis)
{
return AbstractExt::Fetch(pThis);
@@ -86,4 +110,3 @@ class TeamExt final : public AbstractExt, public Detach::Listener
}
};
-
diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp
index 424598bdd7..34c958ee96 100644
--- a/src/Ext/Techno/Body.cpp
+++ b/src/Ext/Techno/Body.cpp
@@ -6,6 +6,7 @@
#include
#include
#include
+#include
#include
#include
@@ -590,6 +591,28 @@ int TechnoExt::GetAttachedEffectCumulativeCount(AttachEffectTypeClass* pAttachEf
return foundCount;
}
+bool TechnoExt::IsValidTechno(AbstractClass* pObject, bool checkIfInTransportOrAbsorbed)
+{
+ const auto pTechno = abstract_cast(pObject);
+ return pTechno ? IsValidTechno(pTechno, checkIfInTransportOrAbsorbed) : false;
+}
+
+bool TechnoExt::IsValidTechno(TechnoClass* pTechno, bool checkIfInTransportOrAbsorbed)
+{
+ if (!pTechno)
+ return false;
+
+ bool isValid = !pTechno->Dirty
+ && ScriptExt::IsUnitAvailable(pTechno, checkIfInTransportOrAbsorbed)
+ && pTechno->Owner
+ && (pTechno->WhatAmI() == AbstractType::Infantry
+ || pTechno->WhatAmI() == AbstractType::Unit
+ || pTechno->WhatAmI() == AbstractType::Building
+ || pTechno->WhatAmI() == AbstractType::Aircraft);
+
+ return isValid;
+}
+
// Check adjacent cells from the center
// The current MapClass::Instance.PlacePowerupCrate(...) doesn't like slopes and maybe other cases
bool TechnoExt::TryToCreateCrate(CoordStruct location, Powerup selectedPowerup, int maxCellRange)
diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h
index 57a79aa69e..1b87e6ed7d 100644
--- a/src/Ext/Techno/Body.h
+++ b/src/Ext/Techno/Body.h
@@ -258,6 +258,8 @@ class TechnoExt : public RadioExt, public Detach::Listener
static void ProcessDigitalDisplays(TechnoClass* pThis);
static int GetDropCrateIndex(TechnoClass* pThis);
static void GetValuesForDisplay(TechnoClass* pThis, TechnoTypeClass* pType, DisplayInfoType infoType, int& value, int& maxValue, int infoIndex);
+ static bool IsValidTechno(TechnoClass* pTechno, bool checkIfInTransportOrAbsorbed = true);
+ static bool IsValidTechno(AbstractClass* pObject, bool checkIfInTransportOrAbsorbed = true);
static void GetDigitalDisplayFakeHealth(TechnoClass* pThis, int& value, int& maxValue);
static void CreateDelayedFireAnim(TechnoClass* pThis, AnimTypeClass* pAnimType, int weaponIndex, bool attach, bool center, bool removeOnNoDelay, bool onTurret, CoordStruct firingCoords);
static bool HandleDelayedFireWithPauseSequence(TechnoClass* pThis, WeaponTypeClass* pWeapon, int weaponIndex, int frame, int firingFrame);
diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp
index ae05a36241..975cefdcb9 100644
--- a/src/Ext/TechnoType/Body.cpp
+++ b/src/Ext/TechnoType/Body.cpp
@@ -6,6 +6,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -1433,6 +1434,146 @@ void TechnoTypeExt::LoadFromINIFile(CCINIClass* const pINI)
DropCrate.Read(exINI, pSection, "DropCrate");
+ this->ConsideredNaval.Read(exINI, pSection, "ConsideredNaval");
+ this->ConsideredVehicle.Read(exINI, pSection, "ConsideredVehicle");
+ this->ConsideredSecretLabTech.Read(exINI, pSection, "ConsideredSecretLabTech");
+
+ // Secret.RequiredHouses contains a list of HouseTypeClass indexes
+ if (pINI->ReadString(pSection, "SecretLab.RequiredHouses", "", Phobos::readBuffer) > 0)
+ {
+ char* context = nullptr;
+ this->Secret_RequiredHouses.clear();
+
+ for (char* cur = strtok_s(Phobos::readBuffer, Phobos::readDelims, &context); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context))
+ {
+ std::string item(cur);
+ this->Secret_RequiredHouses.push_back(item);
+ }
+ }
+
+ // Secret.ForbiddenHouses contains a list of HouseTypeClass indexes
+ if (pINI->ReadString(pSection, "SecretLab.ForbiddenHouses", "", Phobos::readBuffer) > 0)
+ {
+ char* context = nullptr;
+ this->Secret_ForbiddenHouses.clear();
+
+ for (char* cur = strtok_s(Phobos::readBuffer, Phobos::readDelims, &context); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context))
+ {
+ std::string item(cur);
+ this->Secret_ForbiddenHouses.push_back(item);
+ }
+ }
+
+ // Prerequisite.RequiredTheaters contains a list of theater names
+ if (pINI->ReadString(pSection, "Prerequisite.RequiredTheaters", "", Phobos::readBuffer) > 0)
+ {
+ char* context = nullptr;
+ this->PrerequisiteTheaters = 0;
+
+ for (char* cur = strtok_s(Phobos::readBuffer, Phobos::readDelims, &context); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context))
+ {
+ int index = -1;
+ for (size_t i = 0; i < std::size(Theater::Array); ++i)
+ {
+ if (_strcmpi(Theater::Array[i].ID, cur) == 0)
+ {
+ index = static_cast(i);
+ break;
+ }
+ }
+
+ if (index != -1)
+ this->PrerequisiteTheaters |= (1u << index);
+ else
+ Debug::INIParseFailed(pSection, "Prerequisite.RequiredTheaters", cur);
+ }
+ }
+
+ // Prerequisite with Generic Prerequistes support.
+ if (pINI->ReadString(pSection, "Prerequisite", "", Phobos::readBuffer) > 0)
+ {
+ char* context = nullptr;
+ this->Prerequisite.clear();
+
+ for (char* cur = strtok_s(Phobos::readBuffer, Phobos::readDelims, &context); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context))
+ {
+ int idx = TechnoTypeClass::FindIndex(cur);
+
+ if (idx >= 0)
+ {
+ this->Prerequisite.push_back(idx);
+ }
+ else
+ {
+ int index = HouseExt::FindGenericPrerequisite(cur);
+
+ if (index < 0)
+ this->Prerequisite.push_back(index);
+ }
+ }
+ }
+
+ // Prerequisite.Negative with Generic Prerequistes support
+ if (pINI->ReadString(pSection, "Prerequisite.Negative", "", Phobos::readBuffer) > 0)
+ {
+ char* context = nullptr;
+ this->Prerequisite_Negative.clear();
+
+ for (char* cur = strtok_s(Phobos::readBuffer, Phobos::readDelims, &context); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context))
+ {
+ int idx = TechnoTypeClass::FindIndex(cur);
+
+ if (idx >= 0)
+ {
+ this->Prerequisite_Negative.push_back(idx);
+ }
+ else
+ {
+ int index = HouseExt::FindGenericPrerequisite(cur);
+
+ if (index < 0)
+ this->Prerequisite_Negative.push_back(index);
+ }
+ }
+ }
+
+ // Prerequisite.ListX with Generic Prerequistes support
+ this->Prerequisite_Lists.Read(exINI, pSection, "Prerequisite.Lists");
+
+ if (this->Prerequisite_Lists.Get() >= 0)
+ {
+ this->Prerequisite_ListVector.clear();
+
+ for (int i = 1; i <= Prerequisite_Lists.Get(); i++)
+ {
+ char keySection[32];
+ _snprintf_s(keySection, sizeof(keySection), "Prerequisite.List%d", i);
+
+ DynamicVectorClass objectsList;
+ char* context = nullptr;
+ pINI->ReadString(pSection, keySection, "", Phobos::readBuffer);
+
+ for (char* cur = strtok_s(Phobos::readBuffer, Phobos::readDelims, &context); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context))
+ {
+ int idx = TechnoTypeClass::FindIndex(cur);
+
+ if (idx >= 0)
+ {
+ objectsList.AddItem(idx);
+ }
+ else
+ {
+ int index = HouseExt::FindGenericPrerequisite(cur);
+
+ if (index < 0)
+ objectsList.AddItem(index);
+ }
+ }
+
+ this->Prerequisite_ListVector.push_back(objectsList);
+ }
+ }
+
// VoiceIFVRepair from Ares 0.2
this->VoiceIFVRepair.Read(exINI, pSection, "VoiceIFVRepair");
this->ParseVoiceWeaponAttacks(exINI, pSection, this->VoiceWeaponAttacks, this->VoiceEliteWeaponAttacks);
@@ -1852,6 +1993,17 @@ void TechnoTypeExt::Serialize(T& Stm)
// Ares 3.0
.Process(this->Unsellable)
.Process(this->KeepAlive)
+
+ .Process(this->ConsideredNaval)
+ .Process(this->ConsideredVehicle)
+ .Process(this->ConsideredSecretLabTech)
+ .Process(this->Secret_RequiredHouses)
+ .Process(this->Secret_ForbiddenHouses)
+ .Process(this->PrerequisiteTheaters)
+ .Process(this->Prerequisite)
+ .Process(this->Prerequisite_Negative)
+ .Process(this->Prerequisite_Lists)
+ .Process(this->Prerequisite_ListVector)
;
}
void TechnoTypeExt::LoadFromStream(PhobosStreamReader& Stm)
diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h
index c7c2ba9ed6..4d231b1d25 100644
--- a/src/Ext/TechnoType/Body.h
+++ b/src/Ext/TechnoType/Body.h
@@ -461,6 +461,17 @@ class TechnoTypeExt : public ObjectTypeExt
Nullable Unsellable;
Nullable KeepAlive;
+ Nullable ConsideredNaval;
+ Nullable ConsideredVehicle;
+ Valueable ConsideredSecretLabTech;
+ std::vector Secret_RequiredHouses;
+ std::vector Secret_ForbiddenHouses;
+ DWORD PrerequisiteTheaters;
+ ValueableVector Prerequisite;
+ ValueableVector Prerequisite_Negative;
+ Valueable Prerequisite_Lists;
+ std::vector> Prerequisite_ListVector;
+
TechnoTypeExt(TechnoTypeClass* OwnerObject) : ObjectTypeExt(OwnerObject)
, HealthBar_Hide { false }
, HealthBar_HidePips { false }
@@ -872,6 +883,17 @@ class TechnoTypeExt : public ObjectTypeExt
// Ares 3.0
, Unsellable {}
, KeepAlive {}
+
+ , ConsideredNaval {}
+ , ConsideredVehicle {}
+ , ConsideredSecretLabTech { false }
+ , Secret_RequiredHouses {}
+ , Secret_ForbiddenHouses {}
+ , PrerequisiteTheaters { 0xFFFFFFFF }
+ , Prerequisite {}
+ , Prerequisite_Negative {}
+ , Prerequisite_Lists { -1 }
+ , Prerequisite_ListVector {}
{ }
virtual ~TechnoTypeExt() = default;
diff --git a/src/Phobos.Ext.cpp b/src/Phobos.Ext.cpp
index bfd410084f..a1e2445668 100644
--- a/src/Phobos.Ext.cpp
+++ b/src/Phobos.Ext.cpp
@@ -9,6 +9,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -294,6 +295,7 @@ using PhobosTypeRegistry = TypeRegistry <
InfantryExt,
InfantryTypeExt,
OverlayTypeExt,
+ HouseTypeExt,
ParticleSystemTypeExt,
RadSiteExt,
RulesExt,