From 9adb0fe6177043bd5804434dd239586ced05176a Mon Sep 17 00:00:00 2001 From: FS-21 Date: Fri, 2 Dec 2022 06:07:58 +0100 Subject: [PATCH 01/59] Initial commit --- Phobos.vcxproj | 2 + src/Ext/House/Body.cpp | 267 +++++++ src/Ext/House/Body.h | 14 + src/Ext/HouseType/Body.cpp | 146 ++++ src/Ext/HouseType/Body.h | 60 ++ src/Ext/Rules/Body.cpp | 77 ++ src/Ext/Rules/Body.h | 24 + src/Ext/Team/Body.cpp | 1325 +++++++++++++++++++++++++++++++++++ src/Ext/Team/Body.h | 69 ++ src/Ext/TechnoType/Body.cpp | 102 +++ src/Ext/TechnoType/Body.h | 10 + src/Phobos.Ext.cpp | 2 + 12 files changed, 2098 insertions(+) create mode 100644 src/Ext/HouseType/Body.cpp create mode 100644 src/Ext/HouseType/Body.h diff --git a/Phobos.vcxproj b/Phobos.vcxproj index 16350ba272..877367f389 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -29,6 +29,7 @@ + @@ -139,6 +140,7 @@ + diff --git a/src/Ext/House/Body.cpp b/src/Ext/House/Body.cpp index 56b73155c3..817c160468 100644 --- a/src/Ext/House/Body.cpp +++ b/src/Ext/House/Body.cpp @@ -73,6 +73,249 @@ HouseClass* HouseExt::GetHouseKind(OwnerHouseKind const kind, bool const allowRa } } +bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const pItem, const DynamicVectorClass ownedBuildingTypes) +{ + if (!pThis || !pItem) + return false; + + auto pHouseExt = HouseExt::ExtMap.Find(pThis); + if (!pHouseExt) + return false; + + auto pItemExt = TechnoTypeExt::ExtMap.Find(pItem); + if (!pItemExt) + return false; + + // Prerequisite.RequiredTheaters check + if (pItemExt->Prerequisite_RequiredTheaters.size() > 0) + { + int currentTheaterIndex = (int)ScenarioClass::Instance->Theater; + if (pItemExt->Prerequisite_RequiredTheaters.IndexOf(currentTheaterIndex) < 0) + return false; + } + + // TechLevel check + if (pThis->TechLevel < pItem->TechLevel) + return false; + + // BuildLimit checks + int nInstances = 0; + + for (auto pTechno : *TechnoClass::Array) + { + if (pTechno->Owner == pThis && pTechno->GetTechnoType() == pItem && pTechno->IsAlive && pTechno->Health > 0) + nInstances++; + } + + if (nInstances >= pItem->BuildLimit) + return false; + + bool prerequisiteNegativeMet = false; // Only one coincidence is needed + + // Ares Prerequisite.Negative list. + if (pItemExt->Prerequisite_Negative.size() > 0) + { + for (int idx : pItemExt->Prerequisite_Negative) + { + if (prerequisiteNegativeMet) + return false; + + if (idx < 0) // Is even possible this case? I have to check it + { + // Default prerequisites like POWER, PROC, BARRACKS, FACTORY, ... + prerequisiteNegativeMet = HouseExt::HasGenericPrerequisite(idx, ownedBuildingTypes); + } + else + { + for (auto pObject : ownedBuildingTypes) + { + if (prerequisiteNegativeMet) + break; + + if (idx == pObject->ArrayIndex) + prerequisiteNegativeMet = true; + } + } + } + } + + //ValueableVector prerequisite = pItemExt->Prerequisite; + DynamicVectorClass prerequisiteOverride = pItem->PrerequisiteOverride; + + //UnitTypeClass* prerequisiteProcAlternate = RulesClass::Instance->PrerequisiteProcAlternate; + + bool prerequisiteMet = false; // All in this list must appear in ownedBuildingTypes + bool prerequisiteOverrideMet = false; // Only one coincidence is needed + + if (prerequisiteOverride.Count > 0) + { + for (int idx : prerequisiteOverride) + { + if (prerequisiteOverrideMet) + break; + + if (idx < 0) + { + // Default prerequisites like POWER, PROC, BARRACKS, FACTORY, ... + prerequisiteOverrideMet = HouseExt::HasGenericPrerequisite(idx, ownedBuildingTypes); + } + else + { + for (auto pObject : ownedBuildingTypes) + { + if (prerequisiteOverrideMet) + break; + + if (idx == pObject->ArrayIndex) + prerequisiteOverrideMet = true; + } + } + } + } + + if (pItemExt->Prerequisite.size() > 0) + { + bool found = false; + //if (pItem) Debug::Log("[%s] prereq check????\n", pItem->ID); + for (int idx : pItemExt->Prerequisite) + { + found = false; + //Debug::Log("\tidx: %d:\n",idx); + if (idx < 0) + { + // Default prerequisites like POWER, PROC, BARRACKS, FACTORY, ... + found = HouseExt::HasGenericPrerequisite(idx, ownedBuildingTypes); + } + else + { + //Debug::Log("\t[%s] (idx: %d):\n", TechnoTypeClass::Array->GetItem(idx)->ID, idx); + for (auto pObject : ownedBuildingTypes) + { + if (found) + break; + + if (idx == pObject->ArrayIndex) + found = true; + + //Debug::Log("\t\t[%s] (idx: %d) Found? %d\n", pObject->ID, pObject->ArrayIndex, found); + } + } + + if (!found) + break; + } + + prerequisiteMet = found; + } + else + { + prerequisiteMet = true; + } + + bool prerequisiteListsMet = false; + + // Ares Prerequisite lists + if (pItemExt->Prerequisite_Lists.Get() > 0) + { + bool found = false; + + for (auto list : pItemExt->Prerequisite_ListVector) + { + if (found) + break; + + for (int idx : list) + { + if (idx < 0) + { + // Default prerequisites like POWER, PROC, BARRACKS, FACTORY, ... + found = HouseExt::HasGenericPrerequisite(idx, ownedBuildingTypes); + } + else + { + found = false; + + for (auto pObject : ownedBuildingTypes) + { + if (idx == pObject->ArrayIndex) + found = true; + + if (found) + break; + } + } + + if (!found) + break; + } + } + + prerequisiteListsMet = found; + } + + //Debug::Log("prerequisiteMet: %d, prerequisiteListsMet: %d, prerequisiteOverrideMet: %d\n", prerequisiteMet, prerequisiteListsMet, prerequisiteOverrideMet); + return prerequisiteMet || prerequisiteListsMet || prerequisiteOverrideMet; +} + +bool HouseExt::HasGenericPrerequisite(int idx, const DynamicVectorClass ownedBuildingTypes) +{ + if (idx >= 0) + return false; + + DynamicVectorClass selectedPrerequisite = RulesExt::Global()->GenericPrerequisites.GetItem(std::abs(idx)); + + /*Debug::Log("DEBUG: SELECTED DEFAULT PREREQUISITES: %s [%d]\n", RulesExt::Global()->GenericPrerequisitesNames.GetItem(std::abs(idx)), idx); + for (int i : selectedPrerequisite) + { + auto aaa = TechnoTypeClass::Array->GetItem(i); + Debug::Log("[%s] -> %d\n", aaa->ID, i); + } + Debug::Log("\n"); + */ + if (selectedPrerequisite.Count == 0) + return false; + + bool found = false; + + for (auto idxItem : selectedPrerequisite) + { + if (found) + break; + + for (auto pObject : ownedBuildingTypes) + { + if (found) + break; + + if (idxItem == pObject->ArrayIndex) + found = true; + } + } + //Debug::Log("idx: %d -> found: %d\n", idx, found); + + return found; +} + +int HouseExt::FindGenericPrerequisite(const char* id) +{ + if (TechnoTypeClass::FindIndex(id) >= 0) + return 0; + + if (RulesExt::Global()->GenericPrerequisitesNames.Count == 0) + RulesExt::FillDefaultPrerequisites(); + + int i = 0; + for (auto str : RulesExt::Global()->GenericPrerequisitesNames) + { + if (_strcmpi(id, str) == 0) + return (-1 * i); + + ++i; + } + + return 0; +} + void HouseExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) { const char* pSection = this->OwnerObject()->PlainName; @@ -88,6 +331,25 @@ void HouseExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->RepairBaseNodes[i] = readBaseNodeRepairInfo[i < nWritten ? i : nWritten - 1]; } + Valueable readNewTeamsSelector_MergeUnclassifiedCategoryWith; + readNewTeamsSelector_MergeUnclassifiedCategoryWith.Read(exINI, pSection, "NewTeamsSelector.MergeUnclassifiedCategoryWith"); + this->NewTeamsSelector_MergeUnclassifiedCategoryWith = readNewTeamsSelector_MergeUnclassifiedCategoryWith.Get(); + + Valueable readNewTeamsSelector_UnclassifiedCategoryPercentage; + readNewTeamsSelector_UnclassifiedCategoryPercentage.Read(exINI, pSection, "NewTeamsSelector.UnclassifiedCategoryPercentage"); + this->NewTeamsSelector_UnclassifiedCategoryPercentage = readNewTeamsSelector_UnclassifiedCategoryPercentage.Get(); + + Valueable readNewTeamsSelector_GroundCategoryPercentage; + readNewTeamsSelector_GroundCategoryPercentage.Read(exINI, pSection, "NewTeamsSelector.GroundCategoryPercentage"); + this->NewTeamsSelector_GroundCategoryPercentage = readNewTeamsSelector_GroundCategoryPercentage.Get(); + + Valueable readNewTeamsSelector_NavalCategoryPercentage; + readNewTeamsSelector_NavalCategoryPercentage.Read(exINI, pSection, "NewTeamsSelector.NavalCategoryPercentage"); + this->NewTeamsSelector_NavalCategoryPercentage = readNewTeamsSelector_NavalCategoryPercentage.Get(); + + Valueable readNewTeamsSelector_AirCategoryPercentage; + readNewTeamsSelector_AirCategoryPercentage.Read(exINI, pSection, "NewTeamsSelector.AirCategoryPercentage"); + this->NewTeamsSelector_AirCategoryPercentage = readNewTeamsSelector_AirCategoryPercentage.Get(); } @@ -105,6 +367,11 @@ void HouseExt::ExtData::Serialize(T& Stm) .Process(this->Factory_NavyType) .Process(this->Factory_AircraftType) .Process(this->RepairBaseNodes) + .Process(this->NewTeamsSelector_MergeUnclassifiedCategoryWith) + .Process(this->NewTeamsSelector_UnclassifiedCategoryPercentage) + .Process(this->NewTeamsSelector_GroundCategoryPercentage) + .Process(this->NewTeamsSelector_NavalCategoryPercentage) + .Process(this->NewTeamsSelector_AirCategoryPercentage) ; } diff --git a/src/Ext/House/Body.h b/src/Ext/House/Body.h index 92af3c0c23..0faaaed6cb 100644 --- a/src/Ext/House/Body.h +++ b/src/Ext/House/Body.h @@ -25,6 +25,12 @@ class HouseExt BuildingClass* Factory_NavyType; BuildingClass* Factory_AircraftType; + int NewTeamsSelector_MergeUnclassifiedCategoryWith; + double NewTeamsSelector_UnclassifiedCategoryPercentage; + double NewTeamsSelector_GroundCategoryPercentage; + double NewTeamsSelector_NavalCategoryPercentage; + double NewTeamsSelector_AirCategoryPercentage; + //Read from INI bool RepairBaseNodes[3]; @@ -36,6 +42,11 @@ class HouseExt , Factory_NavyType { nullptr } , Factory_AircraftType { nullptr } , RepairBaseNodes { false,false,false } + , NewTeamsSelector_MergeUnclassifiedCategoryWith { -1 } + , NewTeamsSelector_UnclassifiedCategoryPercentage { 0.25 } + , NewTeamsSelector_GroundCategoryPercentage { 0.25 } + , NewTeamsSelector_NavalCategoryPercentage { 0.25 } + , NewTeamsSelector_AirCategoryPercentage { 0.25 } { } virtual ~ExtData() = default; @@ -74,4 +85,7 @@ class HouseExt static int ActiveHarvesterCount(HouseClass* pThis); static int TotalHarvesterCount(HouseClass* pThis); static HouseClass* GetHouseKind(OwnerHouseKind kind, bool allowRandom, HouseClass* pDefault, HouseClass* pInvoker = nullptr, HouseClass* pVictim = nullptr); + static bool PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const pItem, const DynamicVectorClass ownedBuildingTypes); + static bool HasGenericPrerequisite(int idx, const DynamicVectorClass ownedBuildingTypes); + static int FindGenericPrerequisite(const char* id); }; diff --git a/src/Ext/HouseType/Body.cpp b/src/Ext/HouseType/Body.cpp new file mode 100644 index 0000000000..abcb0c43b2 --- /dev/null +++ b/src/Ext/HouseType/Body.cpp @@ -0,0 +1,146 @@ +#include "Body.h" + +#include + +template<> const DWORD Extension::Canary = 0x1111111A; +HouseTypeExt::ExtContainer HouseTypeExt::ExtMap; + +void HouseTypeExt::ExtData::Initialize() +{ +} + +// ============================= +// load / save + +void HouseTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) +{ + auto pThis = this->OwnerObject(); + const char* pSection = pThis->ID; + + if (!pINI->GetSection(pSection)) + return; + + INI_EX exINI(pINI); + + 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"); +} + +void HouseTypeExt::ExtData::CompleteInitialization() +{ + auto const pThis = this->OwnerObject(); + UNREFERENCED_PARAMETER(pThis); +} + +template +void HouseTypeExt::ExtData::Serialize(T& Stm) +{ + Stm + .Process(this->NewTeamsSelector_MergeUnclassifiedCategoryWith) + .Process(this->NewTeamsSelector_UnclassifiedCategoryPercentage) + .Process(this->NewTeamsSelector_GroundCategoryPercentage) + .Process(this->NewTeamsSelector_AirCategoryPercentage) + .Process(this->NewTeamsSelector_NavalCategoryPercentage) + ; +} + +void HouseTypeExt::ExtData::LoadFromStream(PhobosStreamReader& Stm) +{ + Extension::LoadFromStream(Stm); + this->Serialize(Stm); +} + +void HouseTypeExt::ExtData::SaveToStream(PhobosStreamWriter& Stm) +{ + Extension::SaveToStream(Stm); + this->Serialize(Stm); +} + +bool HouseTypeExt::ExtContainer::Load(HouseTypeClass* pThis, IStream* pStm) +{ + HouseTypeExt::ExtData* pData = this->LoadKey(pThis, pStm); + return pData != nullptr; +}; + +bool HouseTypeExt::LoadGlobals(PhobosStreamReader& Stm) +{ + return Stm.Success(); +} + +bool HouseTypeExt::SaveGlobals(PhobosStreamWriter& Stm) +{ + return Stm.Success(); +} +// ============================= +// container + +HouseTypeExt::ExtContainer::ExtContainer() : Container("HouseTypeClass") { } + +HouseTypeExt::ExtContainer::~ExtContainer() = default; + +// ============================= +// container hooks + +DEFINE_HOOK(0x511635, HouseTypeClass_CTOR_1, 0x5) +{ + GET(HouseTypeClass*, pItem, EAX); + + HouseTypeExt::ExtMap.FindOrAllocate(pItem); + + return 0; +} + +DEFINE_HOOK(0x511643, HouseTypeClass_CTOR_2, 0x5) +{ + GET(HouseTypeClass*, pItem, EAX); + + HouseTypeExt::ExtMap.FindOrAllocate(pItem); + + return 0; +} + +DEFINE_HOOK(0x5127CF, HouseTypeClass_DTOR, 0x6) +{ + GET(HouseTypeClass*, pItem, ESI); + + HouseTypeExt::ExtMap.Remove(pItem); + + return 0; +} + +DEFINE_HOOK_AGAIN(0x512480, HouseTypeClass_SaveLoad_Prefix, 0x5) +DEFINE_HOOK(0x512290, HouseTypeClass_SaveLoad_Prefix, 0x5) +{ + GET_STACK(HouseTypeClass*, pItem, 0x4); + GET_STACK(IStream*, pStm, 0x8); + + HouseTypeExt::ExtMap.PrepareStream(pItem, pStm); + + return 0; +} + +DEFINE_HOOK(0x51246D, HouseTypeClass_Load_Suffix, 0x5) +{ + HouseTypeExt::ExtMap.LoadStatic(); + return 0; +} + +DEFINE_HOOK(0x51255C, HouseTypeClass_Save_Suffix, 0x5) +{ + HouseTypeExt::ExtMap.SaveStatic(); + return 0; +} + +DEFINE_HOOK_AGAIN(0x51215A, HouseTypeClass_LoadFromINI, 0x5) +DEFINE_HOOK(0x51214F, HouseTypeClass_LoadFromINI, 0x5) +{ + GET(HouseTypeClass*, pItem, EBX); + GET_BASE(CCINIClass*, pINI, 0x8); + + HouseTypeExt::ExtMap.LoadFromINI(pItem, pINI); + + return 0; +} diff --git a/src/Ext/HouseType/Body.h b/src/Ext/HouseType/Body.h new file mode 100644 index 0000000000..701f9bc5c3 --- /dev/null +++ b/src/Ext/HouseType/Body.h @@ -0,0 +1,60 @@ +#pragma once +#include + +#include +#include +#include + +class HouseTypeExt +{ +public: + using base_type = HouseTypeClass; + + class ExtData final : public Extension + { + public: + Nullable NewTeamsSelector_MergeUnclassifiedCategoryWith; + Nullable NewTeamsSelector_UnclassifiedCategoryPercentage; + Nullable NewTeamsSelector_GroundCategoryPercentage; + Nullable NewTeamsSelector_NavalCategoryPercentage; + Nullable NewTeamsSelector_AirCategoryPercentage; + + ExtData(HouseTypeClass* OwnerObject) : Extension(OwnerObject) + , NewTeamsSelector_MergeUnclassifiedCategoryWith { } + , NewTeamsSelector_UnclassifiedCategoryPercentage { } + , NewTeamsSelector_GroundCategoryPercentage { } + , NewTeamsSelector_NavalCategoryPercentage { } + , NewTeamsSelector_AirCategoryPercentage { } + { } + + virtual ~ExtData() = default; + + virtual void LoadFromINIFile(CCINIClass* pINI) override; + virtual void Initialize() override; + virtual void CompleteInitialization(); + + virtual void InvalidatePointer(void* ptr, bool bRemoved) override + { + } + + virtual void LoadFromStream(PhobosStreamReader& Stm) override; + virtual void SaveToStream(PhobosStreamWriter& Stm) override; + + private: + template + void Serialize(T& Stm); + }; + + class ExtContainer final : public Container + { + public: + ExtContainer(); + ~ExtContainer(); + + virtual bool Load(HouseTypeClass* pThis, IStream* pStm) override; + }; + + static ExtContainer ExtMap; + static bool LoadGlobals(PhobosStreamReader& Stm); + static bool SaveGlobals(PhobosStreamWriter& Stm); +}; diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index c32d37eab4..abf2e55b9c 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -144,6 +144,18 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) AIScriptsLists.AddItem(objectsList); objectsList.Clear(); } + + 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"); + + // Section Generic Prerequisites + FillDefaultPrerequisites(); } // this runs between the before and after type data loading methods for rules ini @@ -182,6 +194,61 @@ bool RulesExt::DetailsCurrentlyEnabled(int const minDetailLevel) && DetailsCurrentlyEnabled(); } +void RulesExt::FillDefaultPrerequisites() +{ + if (RulesExt::Global()->GenericPrerequisitesNames.Count != 0) + return; + //RulesExt::Global()->GenericPrerequisitesNames.Clear(); + + CCINIClass::INI_Rules; + DynamicVectorClass empty; + RulesExt::Global()->GenericPrerequisitesNames.AddItem("POWER"); // -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; + CCINIClass::INI_Rules->ReadString("GenericPrerequisites", CCINIClass::INI_Rules->GetKeyName("GenericPrerequisites", i), "", Phobos::readBuffer); + char* name = (char*)CCINIClass::INI_Rules->GetKeyName("GenericPrerequisites", i); + + 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); + } + + int index = RulesExt::Global()->GenericPrerequisitesNames.FindItemIndex(name); + if (index < 7 && index > 0) + { + // Overwrites a vanilla generic prerequisite + RulesExt::Global()->GenericPrerequisites[index] = objectsList; + } + else + { + // New + RulesExt::Global()->GenericPrerequisitesNames.AddItem(name); + RulesExt::Global()->GenericPrerequisites.AddItem(objectsList); + } + + objectsList.Clear(); + } +} + // ============================= // load / save @@ -230,6 +297,16 @@ void RulesExt::ExtData::Serialize(T& Stm) .Process(this->ToolTip_Background_Opacity) .Process(this->ToolTip_Background_BlurSize) .Process(this->RadialIndicatorVisibility) + .Process(this->GenericPrerequisites) + .Process(this->GenericPrerequisitesNames) + .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) ; } diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h index 763e7e5624..ba36be7ffd 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -69,6 +69,19 @@ class RulesExt Valueable RadialIndicatorVisibility; + DynamicVectorClass> GenericPrerequisites; + DynamicVectorClass GenericPrerequisitesNames; + + Valueable NewTeamsSelector; + Valueable NewTeamsSelector_SplitTriggersByCategory; + Valueable NewTeamsSelector_EnableFallback; + Valueable NewTeamsSelector_MergeUnclassifiedCategoryWith; + Valueable NewTeamsSelector_UnclassifiedCategoryPercentage; + Valueable NewTeamsSelector_GroundCategoryPercentage; + Valueable NewTeamsSelector_NavalCategoryPercentage; + Valueable NewTeamsSelector_AirCategoryPercentage; + + ExtData(RulesClass* OwnerObject) : Extension(OwnerObject) , Storage_TiberiumIndex { -1 } , InfantryGainSelfHealCap {} @@ -106,6 +119,16 @@ class RulesExt , ToolTip_Background_Opacity { 100 } , ToolTip_Background_BlurSize { 0.0f } , RadialIndicatorVisibility { AffectedHouse::Allies } + , GenericPrerequisites { } + , GenericPrerequisitesNames { } + , NewTeamsSelector { false } + , NewTeamsSelector_SplitTriggersByCategory { true } + , NewTeamsSelector_EnableFallback { false } + , NewTeamsSelector_MergeUnclassifiedCategoryWith { -1 } + , NewTeamsSelector_UnclassifiedCategoryPercentage { 0.25 } + , NewTeamsSelector_GroundCategoryPercentage { 0.25 } + , NewTeamsSelector_NavalCategoryPercentage { 0.25 } + , NewTeamsSelector_AirCategoryPercentage { 0.25 } { } virtual ~ExtData() = default; @@ -159,4 +182,5 @@ class RulesExt static bool DetailsCurrentlyEnabled(); static bool DetailsCurrentlyEnabled(int minDetailLevel); + static void FillDefaultPrerequisites(); }; diff --git a/src/Ext/Team/Body.cpp b/src/Ext/Team/Body.cpp index 878d9c7995..4e514c6679 100644 --- a/src/Ext/Team/Body.cpp +++ b/src/Ext/Team/Body.cpp @@ -89,3 +89,1328 @@ DEFINE_HOOK(0x6EC55A, TeamClass_Save_Suffix, 0x5) TeamExt::ExtMap.SaveStatic(); return 0; } + +DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) +{ + enum { UseOriginalSelector = 0x4F8A63, SkipCode = 0x4F8B08 }; + + GET(HouseClass*, pHouse, ESI); + + bool houseIsHuman = pHouse->IsHumanPlayer; + if (SessionClass::IsCampaign()) + houseIsHuman = pHouse->IsHumanPlayer || pHouse->IsInPlayerControl; + + if (houseIsHuman || pHouse->Type->MultiplayPassive) + return SkipCode; + + auto pHouseExt = HouseExt::ExtMap.Find(pHouse); + if (!pHouseExt) + return SkipCode; + + auto pHouseTypeExt = HouseTypeExt::ExtMap.Find(pHouse->Type); + if (!pHouseTypeExt) + return SkipCode; + + if (!RulesExt::Global()->NewTeamsSelector) + return UseOriginalSelector; + + // Reset Team selection countdown + int countdown = RulesClass::Instance->TeamDelays[(int)pHouse->AIDifficulty]; + pHouse->TeamDelayTimer.Start(countdown); + + int totalActiveTeams = 0; + int activeTeams = 0; + + int totalGroundCategoryTriggers = 0; + int totalUnclassifiedCategoryTriggers = 0; + int totalNavalCategoryTriggers = 0; + int totalAirCategoryTriggers = 0; + + DynamicVectorClass validTriggerCandidates; + DynamicVectorClass validTriggerCandidatesGroundOnly; + DynamicVectorClass validTriggerCandidatesNavalOnly; + DynamicVectorClass validTriggerCandidatesAirOnly; + DynamicVectorClass validTriggerCandidatesUnclassifiedOnly; + int dice = ScenarioClass::Instance->Random.RandomRanged(1, 100); + + //if (pHouse->IsHumanPlayer || pHouse->IsInPlayerControl) + //return ContinueFlow; + + //This house must have enabled the triggers + if (dice <= pHouse->RatioAITriggerTeam && pHouse->AITriggersActive) + { + bool splitTriggersByCategory = RulesExt::Global()->NewTeamsSelector_SplitTriggersByCategory; + bool isFallbackEnabled = RulesExt::Global()->NewTeamsSelector_EnableFallback; + teamCategory validCategory = teamCategory::None; + + double percentageUnclassifiedTriggers = 0.0; + double percentageGroundTriggers = 0.0; + double percentageNavalTriggers = 0.0; + double percentageAirTriggers = 0.0; + + if (splitTriggersByCategory) + { + int mergeUnclassifiedCategoryWith = pHouseTypeExt->NewTeamsSelector_MergeUnclassifiedCategoryWith.isset() ? pHouseTypeExt->NewTeamsSelector_MergeUnclassifiedCategoryWith.Get() : RulesExt::Global()->NewTeamsSelector_MergeUnclassifiedCategoryWith; + 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 + 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 + + // 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; + + default: + break; + } + + percentageUnclassifiedTriggers = 0.0; + } + + // TO-DO: HERE load new values from Rulesmd.ini, if set + + percentageUnclassifiedTriggers = percentageUnclassifiedTriggers < 0.0 || percentageUnclassifiedTriggers > 1.0 ? 0.0 : percentageUnclassifiedTriggers; + percentageGroundTriggers = percentageGroundTriggers < 0.0 || percentageGroundTriggers > 1.0 ? 0.0 : percentageGroundTriggers; + percentageNavalTriggers = percentageNavalTriggers < 0.0 || percentageNavalTriggers > 1.0 ? 0.0 : percentageNavalTriggers; + percentageAirTriggers = percentageAirTriggers < 0.0 || percentageAirTriggers > 1.0 ? 0.0 : percentageAirTriggers; + + double totalPercengates = percentageUnclassifiedTriggers + percentageGroundTriggers + percentageNavalTriggers + percentageAirTriggers; + if (totalPercengates > 1.0 || totalPercengates <= 0.0) + splitTriggersByCategory = false; + // Note: if the sum of all percentages is less than 100% then that empty space will work like "no categories" + + if (splitTriggersByCategory) + { + int categoryDice = ScenarioClass::Instance->Random.RandomRanged(1, 100); + + if (categoryDice == 0) + { + splitTriggersByCategory = false; + } + else if (categoryDice <= (int)(percentageUnclassifiedTriggers * 100.0)) + { + validCategory = teamCategory::Unclassified; + } + else if (categoryDice <= (int)((percentageUnclassifiedTriggers + percentageGroundTriggers) * 100.0)) + { + validCategory = teamCategory::Ground; + } + else if (categoryDice <= (int)((percentageUnclassifiedTriggers + percentageGroundTriggers + percentageNavalTriggers) * 100.0)) + { + validCategory = teamCategory::Naval; + } + else if (categoryDice <= (int)((percentageUnclassifiedTriggers + percentageGroundTriggers + percentageNavalTriggers + percentageAirTriggers) * 100.0)) + { + validCategory = teamCategory::Air; + } + else + { + splitTriggersByCategory = false; + } + } + } + + // PREPARING... + + int houseIdx = pHouse->ArrayIndex; + int sideIdx = pHouse->SideIndex + 1; + //int enemyHouseIndex = pHouse->EnemyHouseIndex >= 0 ? pHouse->EnemyHouseIndex : -1; + 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); + double totalWeight = 0.0; + double totalWeightGroundOnly = 0.0; + double totalWeightNavalOnly = 0.0; + double totalWeightAirOnly = 0.0; + double totalWeightUnclassifiedOnly = 0.0; + + // Check if the running teams by the house already reached all the limits + DynamicVectorClass activeTeamsList; + + for (auto const pRunningTeam : *TeamClass::Array) + { + totalActiveTeams++; + int teamHouseIdx = pRunningTeam->Owner->ArrayIndex; + //auto teamHouse = pRunningTeam->GetOwningHouse(); + if (teamHouseIdx != houseIdx) + continue; + + activeTeamsList.AddItem(pRunningTeam); + + if (pRunningTeam->Type->IsBaseDefense && activeDefenseTeamsCount < maxBaseDefenseTeams) + activeDefenseTeamsCount++; + } + + activeTeams = activeTeamsList.Count; + // We will use these for discarding triggers + bool hasReachedMaxTeamsLimit = activeTeams < maxTeamsLimit ? false : true; + bool hasReachedMaxDefensiveTeamsLimit = activeDefenseTeamsCount < maxBaseDefenseTeams ? false : true; + + if (hasReachedMaxDefensiveTeamsLimit) + Debug::Log("DEBUG: House [%s] (Idx: %d) reached the MaximumAIDefensiveTeams value!\n", pHouse->Type->ID, pHouse->ArrayIndex); + + if (hasReachedMaxTeamsLimit) + { + Debug::Log("DEBUG: House [%s] (Idx: %d) reached the TotalAITeamCap value!\n", pHouse->Type->ID, pHouse->ArrayIndex); + return SkipCode; + } + + // Build a list of built structures by the house + DynamicVectorClass ownedBuildingTypes; + for (auto building : pHouse->Buildings) + { + ownedBuildingTypes.AddUnique(building->Type); + } + + struct recruitableUnit + { + TechnoTypeClass* object = nullptr; + int count = 1; + + bool operator==(const TechnoTypeClass* other) const + { + return (object == other); + } + + bool operator==(const recruitableUnit other) const + { + return (object == other.object); + } + }; + + // Build a list of recruitable units by the house + DynamicVectorClass recruitableUnits; + + for (auto pTechno : *TechnoClass::Array) + { + if (pTechno->WhatAmI() == AbstractType::Building) + continue; + + FootClass* pFoot = static_cast(pTechno); + + if (!pFoot + || !pTechno->IsAlive + || pTechno->Health <= 0 + || !pTechno->IsOnMap // Note: underground movement is considered "IsOnMap == false" + || pTechno->Transporter + || pTechno->Absorbed + || !pFoot->CanBeRecruited(pHouse)) + { + continue; + } + + auto pTechnoType = pTechno->GetTechnoType(); + bool found = false; + + for (int i = 0; i < recruitableUnits.Count; i++) + { + if (recruitableUnits[i].object == pTechnoType) + { + recruitableUnits[i].count++; + found = true; + break; + } + } + + if (!found) + { + recruitableUnit newRecruitable; + newRecruitable.object = pTechnoType; + recruitableUnits.AddItem(newRecruitable); + } + } + + //Debug::Log("List of recruitable units of this house:\n"); + //for (auto item : recruitableUnits) + //{ + //if (item.object) + //Debug::Log("[%s] = %d\n", item.object->ID, item.count); + //} + //Debug::Log("AAA\n"); + + HouseClass* targetHouse = nullptr; + if (pHouse->EnemyHouseIndex >= 0) + targetHouse = HouseClass::Array->GetItem(pHouse->EnemyHouseIndex); + + bool onlyCheckImportantTriggers = false; + + // Gather all the trigger candidates into one place for posterior fast calculations + for (auto const pTrigger : *AITriggerTypeClass::Array) + { + if (!pTrigger) + continue; + + int triggerHouse = pTrigger->HouseIndex; + int triggerSide = pTrigger->SideIndex; + + // Ignore the deactivated triggers + if (pTrigger->IsEnabled) + { + // The trigger must be compatible with the owner + if ((triggerHouse == -1 || houseIdx == triggerHouse) && (triggerSide == 0 || sideIdx == triggerSide)) + { + // "ConditionType=-1" will be skipped, is always valid (and save CPU) + if ((int)pTrigger->ConditionType >= 0) + { + if ((int)pTrigger->ConditionType == 0) + { + // Simulate case 0: "enemy owns" + if (!pTrigger->ConditionObject) + continue; + + DynamicVectorClass list; + list.AddItem(pTrigger->ConditionObject); + bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, targetHouse, true, list); + + if (!isConditionMet) + continue; + } + else if ((int)pTrigger->ConditionType == 1) + { + // Simulate case 0: "house owns" + if (!pTrigger->ConditionObject) + continue; + + DynamicVectorClass list; + list.AddItem(pTrigger->ConditionObject); + bool isConditionMet = TeamExt::HouseOwns(pTrigger, pHouse, false, list); + + if (!isConditionMet) + continue; + } + else if ((int)pTrigger->ConditionType == 7) + { + // Simulate case 7: "civilian owns" + if (!pTrigger->ConditionObject) + continue; + + DynamicVectorClass list; + list.AddItem(pTrigger->ConditionObject); + bool isConditionMet = TeamExt::NeutralOwns(pTrigger, list); + + if (!isConditionMet) + continue; + } + else if ((int)pTrigger->ConditionType == 8) + { + // Simulate case 0: "enemy owns" but without restrict it against only 1 enemy house + if (!pTrigger->ConditionObject) + continue; + + DynamicVectorClass list; + list.AddItem(pTrigger->ConditionObject); + bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, nullptr, false, list); + + if (!isConditionMet) + continue; + } + else if ((int)pTrigger->ConditionType == 9) + { + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) + { + // New case 9: Like in case 0 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the enemy. + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF and 256 is 0x0001 + DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); + bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, targetHouse, false, list); + + if (!isConditionMet) + continue; + } + } + else if ((int)pTrigger->ConditionType == 10) + { + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) + { + // New case 10: Like in case 1 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the house. + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF and 256 is 0x0001 + DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); + bool isConditionMet = TeamExt::HouseOwns(pTrigger, pHouse, false, list); + + if (!isConditionMet) + continue; + } + } + else if ((int)pTrigger->ConditionType == 11) + { + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) + { + // New case 11: Like in case 7 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the Civilians. + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF and 256 is 0x0001 + DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); + bool isConditionMet = TeamExt::NeutralOwns(pTrigger, list); + + if (!isConditionMet) + continue; + } + } + else if ((int)pTrigger->ConditionType == 12) + { + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) + { + // New case 12: Like in case 0 & 9 but instead of a specific enemy this checks in all enemies. + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF and 256 is 0x0001 + DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); + bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, nullptr, false, list); + + if (!isConditionMet) + continue; + } + } + else if ((int)pTrigger->ConditionType == 13) + { + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) + { + // New case 13: Like in case 1 & 10 but instead checking the house now checks the allies. + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF and 256 is 0x0001 + DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); + bool isConditionMet = TeamExt::HouseOwns(pTrigger, pHouse, true, list); + + if (!isConditionMet) + continue; + } + } + else if ((int)pTrigger->ConditionType == 14) + { + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) + { + // New case 14: Like in case 9 but instead of meet any comparison now is required all. + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF and 256 is 0x0001 + DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); + bool isConditionMet = TeamExt::EnemyOwnsAll(pTrigger, pHouse, targetHouse, list); + + if (!isConditionMet) + continue; + } + } + else if ((int)pTrigger->ConditionType == 15) + { + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) + { + // New case 15: Like in case 10 but instead of meet any comparison now is required all. + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF and 256 is 0x0001 + DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); + bool isConditionMet = TeamExt::HouseOwnsAll(pTrigger, pHouse, list); + + if (!isConditionMet) + continue; + } + } + else if ((int)pTrigger->ConditionType == 16) + { + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) + { + // New case 16: Like in case 11 but instead of meet any comparison now is required all. + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF and 256 is 0x0001 + DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); + bool isConditionMet = TeamExt::NeutralOwnsAll(pTrigger, list); + + if (!isConditionMet) + continue; + } + } + else if ((int)pTrigger->ConditionType == 17) + { + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) + { + // New case 17: Like in case 14 but instead of meet any comparison now is required all. Check all enemies + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF and 256 is 0x0001 + DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); + bool isConditionMet = TeamExt::EnemyOwnsAll(pTrigger, pHouse, nullptr, list); + + if (!isConditionMet) + continue; + } + } + else + { + // Other cases from vanilla game + //bool cm = pTrigger->ConditionMet(pHouse, targetHouse, hasReachedMaxDefensiveTeamsLimit); + //Debug::Log("[%s], \"%s\"\nOld ConditionMet section (%d, %s): %d [-, %d, %d]\n\n", pTrigger->ID, pTrigger->Team1->Name, (int)pTrigger->ConditionType, (pTrigger->ConditionObject ? pTrigger->ConditionObject->ID : ""), cm, pTrigger->Conditions->ComparatorOperand, pTrigger->Conditions->ComparatorType); + + if (!pTrigger->ConditionMet(pHouse, targetHouse, hasReachedMaxDefensiveTeamsLimit)) + continue; + } + } + /*else + { + bool cm = false; + }*/ + + if (onlyCheckImportantTriggers) + { + if (pTrigger->Weight_Current < 5000) + continue; + } + + auto pTriggerTeam1Type = pTrigger->Team1; + if (!pTriggerTeam1Type) + continue; + + // No more defensive teams needed + if (pTriggerTeam1Type->IsBaseDefense && hasReachedMaxDefensiveTeamsLimit) + continue; + + // If this type of Team reached the max then skip it + int count = 0; + + for (auto team : activeTeamsList) + { + if (team->Type == pTriggerTeam1Type) + count++; + } + + if (count >= pTriggerTeam1Type->Max) + continue; + + teamCategory teamIsCategory = teamCategory::None; + + // Analyze what kind of category is this main team if the feature is enabled + if (splitTriggersByCategory) + { + for (auto entry : pTriggerTeam1Type->TaskForce->Entries) + { + // If they team has mixed members there is no need to continue + if (teamIsCategory == teamCategory::Unclassified) + break; + + if (entry.Amount > 0) + { + /* + What category goes each unit? there is the table: + + - Air category magic words: + ConsideredAircraft=yes + JumpJet=yes + BalloonHover=yes + + - Maval category magic words: + Naval=yes + MovementRestrictedTo=Water + + - Mixed category + MovementZone=Amphibious + + + - Ground category magic words: + Anyone that doesn't meet the previous magic words + */ + + if (entry.Type) + { + if (entry.Type->WhatAmI() == AbstractType::AircraftType || entry.Type->ConsideredAircraft) + { + // For now the team is from air category + teamIsCategory = teamIsCategory == teamCategory::None || teamIsCategory == teamCategory::Air ? teamCategory::Air : teamCategory::Unclassified; + } + else if (entry.Type->Naval && (entry.Type->MovementZone != MovementZone::Amphibious && entry.Type->MovementZone != MovementZone::AmphibiousDestroyer || entry.Type->MovementZone != MovementZone::AmphibiousCrusher)) + { + // For now the team is from naval category + teamIsCategory = teamIsCategory == teamCategory::None || teamIsCategory == teamCategory::Naval ? teamCategory::Naval : teamCategory::Unclassified; + } + else if (teamIsCategory != teamCategory::Naval && teamIsCategory != teamCategory::Air) + { + // For now the team doesn't belong to the previous categories + teamIsCategory = teamIsCategory != teamCategory::Unclassified ? teamCategory::Ground : teamCategory::Unclassified; + } + } + } + else + { + break; + } + } + } + + bool allObjectsCanBeBuiltOrRecruited = true; + //Debug::Log("[%s] [%s] is evaluating unit prerequisites...\n", pTrigger->ID, pTriggerTeam1Type->ID); + + if (pTriggerTeam1Type->Autocreate) + { + for (auto entry : pTriggerTeam1Type->TaskForce->Entries) + { + // Check if each unit in the taskforce meets the structure prerequisites + if (entry.Amount > 0) + { + if (!entry.Type) + continue; + + TechnoTypeClass* object = entry.Type; + //bool canBeBuilt = pHouse->AllPrerequisitesAvailable(object, ownedBuildingTypes, ownedBuildingTypes.Count); // Old + + // Experimental + bool canBeBuilt = HouseExt::PrerequisitesMet(pHouse, object, ownedBuildingTypes); + + if (!canBeBuilt) + { + //Debug::Log("[%s] not ok...\n", object->ID); + allObjectsCanBeBuiltOrRecruited = false; + break; + } + else + { + //Debug::Log("[%s] build prerequisites met!\n", object->ID); + } + } + else + { + break; + } + } + } + else + { + allObjectsCanBeBuiltOrRecruited = false; + } + + //if (allObjectsCanBeBuiltOrRecruited) + //Debug::Log("[%s] [%s] can build units!...\n", pTrigger->ID, pTriggerTeam1Type->ID); + + if (!allObjectsCanBeBuiltOrRecruited && pTriggerTeam1Type->Recruiter) + { + allObjectsCanBeBuiltOrRecruited = true; + //Debug::Log("[%s] [%s] checking if it can recruit...\n", pTrigger->ID, pTriggerTeam1Type->ID); + for (auto entry : pTriggerTeam1Type->TaskForce->Entries) + { + // Check if each unit in the taskforce has the available recruitable units in the map + if (allObjectsCanBeBuiltOrRecruited && entry.Amount > 0) + { + bool canBeRecruited = false; + //Debug::Log("[%s] checking if there are recruitable...\n", entry.Type->ID); + + for (auto item : recruitableUnits) + { + if (item.object == entry.Type) + { + if (item.count >= entry.Amount) + canBeRecruited = true; + + break; + } + } + + if (!canBeRecruited) + { + //Debug::Log("[%s] %d can not be recruited!\n", entry.Type->ID, entry.Amount); + allObjectsCanBeBuiltOrRecruited = false; + break; + } + else + { + //Debug::Log("[%s] can recruit %d units!\n", entry.Type->ID, entry.Amount); + } + } + } + } + + // We can't let AI cheat in this trigger because doesn't have the required tech tree available + if (!allObjectsCanBeBuiltOrRecruited) + continue; + + // Special case: triggers become very important if they reach the max priority (value 5000) + // They get stored in a different list and all previous triggers are discarded. + if (pTrigger->Weight_Current >= 5000 && !onlyCheckImportantTriggers) + { + // First time only + if (validTriggerCandidates.Count > 0) + { + validTriggerCandidates.Clear(); + validTriggerCandidatesGroundOnly.Clear(); + validTriggerCandidatesNavalOnly.Clear(); + validTriggerCandidatesAirOnly.Clear(); + validTriggerCandidatesUnclassifiedOnly.Clear(); + + validCategory = teamCategory::None; + } + + // Reset and now only add important triggers to the list + onlyCheckImportantTriggers = true; + totalWeight = 0.0; + splitTriggersByCategory = false; // VIP teams breaks the categories logic (on purpose) + } + + // Passed all checks, save this trigger for later. + // The idea behind this is to simulate an ordered list of weights and once we throw the dice we'll know the winner trigger: The more weight means more possibilities to be selected. + totalWeight += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; + TriggerElementWeight item; + item.Trigger = pTrigger; + item.Weight = totalWeight; + item.Category = teamIsCategory; + + validTriggerCandidates.AddItem(item); + + if (splitTriggersByCategory) + { + TriggerElementWeight itemGroundOnly; + TriggerElementWeight itemAirOnly; + TriggerElementWeight itemNavalOnly; + TriggerElementWeight itemUnclassifiedOnly; + + switch (teamIsCategory) + { + case teamCategory::Ground: + totalWeightGroundOnly += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; + + itemGroundOnly.Trigger = pTrigger; + itemGroundOnly.Weight = totalWeightGroundOnly; + itemGroundOnly.Category = teamIsCategory; + + validTriggerCandidatesGroundOnly.AddItem(itemGroundOnly); + totalGroundCategoryTriggers++; + break; + + case teamCategory::Air: + totalWeightAirOnly += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; + + itemAirOnly.Trigger = pTrigger; + itemAirOnly.Weight = totalWeightAirOnly; + itemAirOnly.Category = teamIsCategory; + + validTriggerCandidatesAirOnly.AddItem(itemAirOnly); + totalAirCategoryTriggers++; + break; + + case teamCategory::Naval: + totalWeightNavalOnly += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; + + itemNavalOnly.Trigger = pTrigger; + itemNavalOnly.Weight = totalWeightNavalOnly; + itemNavalOnly.Category = teamIsCategory; + + validTriggerCandidatesNavalOnly.AddItem(itemNavalOnly); + totalNavalCategoryTriggers++; + break; + + case teamCategory::Unclassified: + totalWeightUnclassifiedOnly += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; + + itemUnclassifiedOnly.Trigger = pTrigger; + itemUnclassifiedOnly.Weight = totalWeightUnclassifiedOnly; + itemUnclassifiedOnly.Category = teamIsCategory; + + validTriggerCandidatesUnclassifiedOnly.AddItem(itemUnclassifiedOnly); + totalUnclassifiedCategoryTriggers++; + break; + + default: + break; + } + } + } + } + } + + if (splitTriggersByCategory) + { + Debug::Log("DEBUG: Category percentages:\nMixed teams: %f\nGround teams: %f\nNaval teams: %f\nAir teams: %f\n", percentageUnclassifiedTriggers, percentageGroundTriggers, percentageNavalTriggers, percentageAirTriggers); + + switch (validCategory) + { + case teamCategory::Ground: + Debug::Log("DEBUG: This time only will be picked GROUND teams.\n"); + break; + + case teamCategory::Unclassified: + Debug::Log("DEBUG: This time only will be picked MIXED teams.\n"); + break; + + case teamCategory::Naval: + Debug::Log("DEBUG: This time only will be picked NAVAL teams.\n"); + break; + + case teamCategory::Air: + Debug::Log("DEBUG: This time only will be picked AIR teams.\n"); + break; + + default: + Debug::Log("DEBUG: This time teams categories are DISABLED.\n"); + break; + } + } + + if (validTriggerCandidates.Count == 0) + { + Debug::Log("DEBUG: [%s] (idx: %d) No valid triggers for now. A new attempt will be done later...\n", pHouse->Type->ID, pHouse->ArrayIndex); + return SkipCode; + } + + if ((validCategory == teamCategory::Ground && totalGroundCategoryTriggers == 0) || + (validCategory == teamCategory::Unclassified && totalUnclassifiedCategoryTriggers == 0) || + (validCategory == teamCategory::Air && totalAirCategoryTriggers == 0) || + (validCategory == teamCategory::Naval && totalNavalCategoryTriggers == 0)) + { + Debug::Log("DEBUG: [%s] (Idx: %d) No valid triggers of this category. A new attempt should be done later...\n", pHouse->Type->ID, pHouse->ArrayIndex); + + if (!isFallbackEnabled) + return SkipCode; + + Debug::Log("... BUT fallback mode is enabled so now it will check all triggers available.\n"); + validCategory = teamCategory::None; + } + + AITriggerTypeClass* selectedTrigger = nullptr; + double weightDice = 0.0; + double lastWeight = 0.0; + bool found = false; + + switch (validCategory) + { + case teamCategory::None: + weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeight) * 1.0; + // Debug::Log("Possible values of the dice [0 - %f]. Dice says: %f:\n", totalWeight, weightDice); + // Debug::Log("Picking the trigger with highest weight from the list (No category set):\n"); + + for (auto element : validTriggerCandidates) + { + // Debug::Log("[%s] Lottery range values: %f - %f", element.Trigger->ID, lastWeight, element.Weight); + lastWeight = element.Weight; + + if (weightDice < element.Weight && !found) + { + // Debug::Log(" ... CANDIDATE!\n"); + selectedTrigger = element.Trigger; + found = true; + } + else + { + // Debug::Log(" ... \n"); + } + } + break; + + case teamCategory::Ground: + weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightGroundOnly) * 1.0; + // Debug::Log("Possible values of the dice [0 - %f]. Dice says: %f:\n", totalWeightGroundOnly, weightDice); + // Debug::Log("Picking the trigger with highest weight from the list (Ground category):\n"); + + for (auto element : validTriggerCandidatesGroundOnly) + { + // Debug::Log("[%s] Lottery range values: %f - %f", element.Trigger->ID, lastWeight, element.Weight); + lastWeight = element.Weight; + + if (weightDice < element.Weight && !found) + { + // Debug::Log(" ... CANDIDATE!\n"); + selectedTrigger = element.Trigger; + found = true; + } + else + { + // Debug::Log(" ... \n"); + } + } + break; + + case teamCategory::Unclassified: + weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightUnclassifiedOnly) * 1.0; + // Debug::Log("Possible values of the dice [0 - %f]. Dice says: %f:\n", totalWeightUnclassifiedOnly, weightDice); + // Debug::Log("Picking the trigger with highest weight from the list (Mixed category):\n"); + + for (auto element : validTriggerCandidatesUnclassifiedOnly) + { + // Debug::Log("[%s] Lottery range values: %f - %f", element.Trigger->ID, lastWeight, element.Weight); + lastWeight = element.Weight; + + if (weightDice < element.Weight && !found) + { + // Debug::Log(" ... CANDIDATE!\n"); + selectedTrigger = element.Trigger; + found = true; + } + else + { + // Debug::Log(" ... \n"); + } + } + break; + + case teamCategory::Naval: + weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightNavalOnly) * 1.0; + // Debug::Log("Possible values of the dice [0 - %f]. Dice says: %f:\n", totalWeightNavalOnly, weightDice); + // Debug::Log("Picking the trigger with highest weight from the list (Naval category):\n"); + + for (auto element : validTriggerCandidatesNavalOnly) + { + // Debug::Log("[%s] Lottery range values: %f - %f", element.Trigger->ID, lastWeight, element.Weight); + lastWeight = element.Weight; + + if (weightDice < element.Weight && !found) + { + // Debug::Log(" ... CANDIDATE!\n"); + selectedTrigger = element.Trigger; + found = true; + } + else + { + // Debug::Log(" ... \n"); + } + } + break; + + case teamCategory::Air: + weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightAirOnly) * 1.0; + // Debug::Log("Possible values of the dice [0 - %f]. Dice says: %f:\n", totalWeightAirOnly, weightDice); + // Debug::Log("Picking the trigger with highest weight from the list (Air category):\n"); + + for (auto element : validTriggerCandidatesAirOnly) + { + // Debug::Log("[%s] Lottery range values: %f - %f", element.Trigger->ID, lastWeight, element.Weight); + lastWeight = element.Weight; + + if (weightDice < element.Weight && !found) + { + // Debug::Log(" ... CANDIDATE!\n"); + selectedTrigger = element.Trigger; + found = true; + } + else + { + // Debug::Log(" ... \n"); + } + } + break; + + default: + break; + } + /* + Debug::Log("[%s] Lottery range values: %f - %f", element.Trigger->ID, lastWeight, element.Weight); + lastWeight = element.Weight; + + if (weightDice < element.WeightOnlyNaval && !found) + { + Debug::Log(" ... CANDIDATE!\n"); + selectedTrigger = element.Trigger; + found = true; + } + else + { + Debug::Log(" ... \n"); + } + */ + + if (!selectedTrigger) + { + Debug::Log("DEBUG: House [%s] (Idx: %d) failed to select Trigger. A new attempt Will be done later...\n", pHouse->Type->ID, pHouse->ArrayIndex); + return SkipCode; + } + + if (selectedTrigger->Weight_Current >= 5000.0 + && selectedTrigger->Weight_Minimum <= 4999.0) + { + // Next time this trigger will be out of the important triggers list + selectedTrigger->Weight_Current = 4999.0; + } + + // We have a winner + Debug::Log("DEBUG: House [%s] (Idx: %d) selected trigger [%s].\n", pHouse->Type->ID, pHouse->ArrayIndex, selectedTrigger->ID); + + //for (auto entry : selectedTrigger->Team1->TaskForce->Entries) + //{ + //if (entry.Amount > 0) + //Debug::Log("[%s] = %d\n", entry.Type->ID, entry.Amount); + //else + //break; + //} + //Debug::Log("\n"); + + auto pTriggerTeam1Type = selectedTrigger->Team1; + if (pTriggerTeam1Type) + { + int count = 0; + + for (auto team : activeTeamsList) + { + if (team->Type == pTriggerTeam1Type) + count++; + } + + if (count < pTriggerTeam1Type->Max) + { + if (auto newTeam = pTriggerTeam1Type->CreateTeam(pHouse)) + newTeam->NeedsToDisappear = false; + } + } + + auto pTriggerTeam2Type = selectedTrigger->Team2; + if (pTriggerTeam2Type) + { + int count = 0; + + for (auto team : activeTeamsList) + { + if (team->Type == pTriggerTeam2Type) + count++; + } + + if (count < pTriggerTeam2Type->Max) + { + if (auto newTeam = pTriggerTeam2Type->CreateTeam(pHouse)) + newTeam->NeedsToDisappear = false; + } + } + + //Debug::Log("TeamClass::Array->Count: %d\n", TeamClass::Array->Count); + } + + + //selectedTeams46 = newTeamsList; + return SkipCode; +} + +bool TeamExt::HouseOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, bool allies, DynamicVectorClass list) +{ + bool result = false; + int counter = 0; + + // Count all objects of the list, like an OR operator + for (auto pItem : list) + { + for (auto pObject : *TechnoClass::Array) + { + if (pObject + && pObject->IsAlive + && pObject->Health > 0 + && ((!allies && pObject->Owner == pHouse) || (allies && pHouse != pObject->Owner && pHouse->IsAlliedWith(pObject->Owner))) + && !pObject->Owner->Type->MultiplayPassive + && pObject->GetTechnoType() == pItem) + { + counter++; + } + } + } + + switch (pThis->Conditions->ComparatorOperand) + { + case 0: + result = counter < pThis->Conditions->ComparatorType; + break; + case 1: + result = counter <= pThis->Conditions->ComparatorType; + break; + case 2: + result = counter == pThis->Conditions->ComparatorType; + break; + case 3: + result = counter >= pThis->Conditions->ComparatorType; + break; + case 4: + result = counter > pThis->Conditions->ComparatorType; + break; + case 5: + result = counter != pThis->Conditions->ComparatorType; + break; + default: + break; + } + + return result; +} + +bool TeamExt::EnemyOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, bool onlySelectedEnemy, DynamicVectorClass list) +{ + bool result = false; + int counter = 0; + + if (pEnemy && pHouse->IsAlliedWith(pEnemy) && !onlySelectedEnemy) + pEnemy = nullptr; + + // Count all objects of the list, like an OR operator + for (auto pItem : list) + { + for (auto pObject : *TechnoClass::Array) + { + if (pObject + && pObject->IsAlive + && pObject->Health > 0 + && pObject->Owner != pHouse + && (!pEnemy || (pEnemy && !pHouse->IsAlliedWith(pEnemy))) + && !pObject->Owner->Type->MultiplayPassive + && pObject->GetTechnoType() == pItem) + { + counter++; + } + } + } + + switch (pThis->Conditions->ComparatorOperand) + { + case 0: + result = counter < pThis->Conditions->ComparatorType; + break; + case 1: + result = counter <= pThis->Conditions->ComparatorType; + break; + case 2: + result = counter == pThis->Conditions->ComparatorType; + break; + case 3: + result = counter >= pThis->Conditions->ComparatorType; + break; + case 4: + result = counter > pThis->Conditions->ComparatorType; + break; + case 5: + result = counter != pThis->Conditions->ComparatorType; + break; + default: + break; + } + + return result; +} + +bool TeamExt::NeutralOwns(AITriggerTypeClass* pThis, DynamicVectorClass list) +{ + bool result = false; + int counter = 0; + + for (auto pHouse : *HouseClass::Array) + { + if (_stricmp(SideClass::Array->GetItem(pHouse->Type->SideIndex)->Name, "Civilian") != 0) + continue; + + // Count all objects of the list, like an OR operator + for (auto pItem : list) + { + for (auto pObject : *TechnoClass::Array) + { + if (pObject + && pObject->IsAlive + && pObject->Health > 0 + && pObject->Owner == pHouse + && pObject->GetTechnoType() == pItem) + { + counter++; + } + } + } + } + + switch (pThis->Conditions->ComparatorOperand) + { + case 0: + result = counter < pThis->Conditions->ComparatorType; + break; + case 1: + result = counter <= pThis->Conditions->ComparatorType; + break; + case 2: + result = counter == pThis->Conditions->ComparatorType; + break; + case 3: + result = counter >= pThis->Conditions->ComparatorType; + break; + case 4: + result = counter > pThis->Conditions->ComparatorType; + break; + case 5: + result = counter != pThis->Conditions->ComparatorType; + break; + default: + break; + } + + return result; +} + +bool TeamExt::HouseOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, DynamicVectorClass list) +{ + bool result = true; + + if (list.Count == 0) + return false; + + // Count all objects of the list, like an AND operator + for (auto pItem : list) + { + if (!result) + break; + + int counter = 0; + result = true; + + for (auto pObject : *TechnoClass::Array) + { + if (pObject && + pObject->IsAlive && + pObject->Health > 0 && + pObject->Owner == pHouse && + pObject->GetTechnoType() == pItem) + { + counter++; + } + } + + switch (pThis->Conditions->ComparatorOperand) + { + case 0: + result = counter < pThis->Conditions->ComparatorType; + break; + case 1: + result = counter <= pThis->Conditions->ComparatorType; + break; + case 2: + result = counter == pThis->Conditions->ComparatorType; + break; + case 3: + result = counter >= pThis->Conditions->ComparatorType; + break; + case 4: + result = counter > pThis->Conditions->ComparatorType; + break; + case 5: + result = counter != pThis->Conditions->ComparatorType; + break; + default: + break; + } + } + + return result; +} + +bool TeamExt::EnemyOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, DynamicVectorClass list) +{ + bool result = true; + + if (pEnemy && pHouse->IsAlliedWith(pEnemy)) + pEnemy = nullptr; + + if (list.Count == 0) + return false; + + // Count all objects of the list, like an AND operator + for (auto pItem : list) + { + if (!result) + break; + + int counter = 0; + result = true; + + for (auto pObject : *TechnoClass::Array) + { + if (pObject + && pObject->IsAlive + && pObject->Health > 0 + && pObject->Owner != pHouse + && (!pEnemy || (pEnemy && !pHouse->IsAlliedWith(pEnemy))) + && !pObject->Owner->Type->MultiplayPassive + && pObject->GetTechnoType() == pItem) + { + counter++; + } + } + + switch (pThis->Conditions->ComparatorOperand) + { + case 0: + result = counter < pThis->Conditions->ComparatorType; + break; + case 1: + result = counter <= pThis->Conditions->ComparatorType; + break; + case 2: + result = counter == pThis->Conditions->ComparatorType; + break; + case 3: + result = counter >= pThis->Conditions->ComparatorType; + break; + case 4: + result = counter > pThis->Conditions->ComparatorType; + break; + case 5: + result = counter != pThis->Conditions->ComparatorType; + break; + default: + break; + } + } + + return result; +} + +bool TeamExt::NeutralOwnsAll(AITriggerTypeClass* pThis, DynamicVectorClass list) +{ + bool result = true; + + if (list.Count == 0) + return false; + + // Any neutral house should be capable to meet the prerequisites + for (auto pHouse : *HouseClass::Array) + { + if (!result) + break; + + bool foundAll = true; + + if (_stricmp(SideClass::Array->GetItem(pHouse->Type->SideIndex)->Name, "Civilian") != 0) + continue; + + // Count all objects of the list, like an AND operator + for (auto pItem : list) + { + if (!foundAll) + break; + + int counter = 0; + + for (auto pObject : *TechnoClass::Array) + { + if (pObject && + pObject->IsAlive && + pObject->Health > 0 && + pObject->Owner == pHouse && + pObject->GetTechnoType() == pItem) + { + counter++; + } + } + + switch (pThis->Conditions->ComparatorOperand) + { + case 0: + foundAll = counter < pThis->Conditions->ComparatorType; + break; + case 1: + foundAll = counter <= pThis->Conditions->ComparatorType; + break; + case 2: + foundAll = counter == pThis->Conditions->ComparatorType; + break; + case 3: + foundAll = counter >= pThis->Conditions->ComparatorType; + break; + case 4: + foundAll = counter > pThis->Conditions->ComparatorType; + break; + case 5: + foundAll = counter != pThis->Conditions->ComparatorType; + break; + default: + break; + } + } + + if (!foundAll) + result = false; + } + + return result; +} diff --git a/src/Ext/Team/Body.h b/src/Ext/Team/Body.h index 01033f3ba6..559e39b185 100644 --- a/src/Ext/Team/Body.h +++ b/src/Ext/Team/Body.h @@ -1,12 +1,74 @@ #pragma once #include +#include #include #include #include #include +#include +#include +#include #include +enum teamCategory +{ + None = 0, // No category. Should be default value + Ground = 1, + Air = 2, + Naval = 3, + Unclassified = 4 +}; + +struct TriggerElementWeight +{ + double Weight = 0.0; + AITriggerTypeClass* Trigger = nullptr; + teamCategory Category = teamCategory::None; + + //need to define a == operator so it can be used in array classes + bool operator==(const TriggerElementWeight& other) const + { + return (Trigger == other.Trigger && Weight == other.Weight && Category == other.Category); + } + + //unequality + bool operator!=(const TriggerElementWeight& other) const + { + return (Trigger != other.Trigger || Weight != other.Weight || Category == other.Category); + } + + bool operator<(const TriggerElementWeight& other) const + { + return (Weight < other.Weight); + } + + bool operator<(const double other) const + { + return (Weight < other); + } + + bool operator>(const TriggerElementWeight& other) const + { + return (Weight > other.Weight); + } + + bool operator>(const double other) const + { + return (Weight > other); + } + + bool operator==(const double other) const + { + return (Weight == other); + } + + bool operator!=(const double other) const + { + return (Weight != other); + } +}; + class TeamExt { public: @@ -81,4 +143,11 @@ class TeamExt static ExtContainer ExtMap; + static bool HouseOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, bool allies, DynamicVectorClass list); + static bool HouseOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, DynamicVectorClass list); + static bool EnemyOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, bool onlySelectedEnemy, DynamicVectorClass list); + static bool EnemyOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, DynamicVectorClass list); + static bool NeutralOwns(AITriggerTypeClass* pThis, DynamicVectorClass list); + static bool NeutralOwnsAll(AITriggerTypeClass* pThis, DynamicVectorClass list); + }; diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index 1f843181d3..749f40db53 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include @@ -271,6 +272,101 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->ProneSecondaryFireFLH.Read(exArtINI, pArtSection, "ProneSecondaryFireFLH"); this->DeployedPrimaryFireFLH.Read(exArtINI, pArtSection, "DeployedPrimaryFireFLH"); this->DeployedSecondaryFireFLH.Read(exArtINI, pArtSection, "DeployedSecondaryFireFLH"); + + // Prerequisite.RequiredTheaters contains a list of theader names + this->Prerequisite_RequiredTheaters.Read(exINI, pSection, "Prerequisite.RequiredTheaters"); + + char* key = "Prerequisite.RequiredTheaters"; + 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 = Theater::FindIndex(cur); + if (index != -1) + Prerequisite_RequiredTheaters.push_back(index); + } + + key = nullptr; + + // Prerequisite with Generic Prerequistes support. + // Note: I have no idea of what could happen in the game engine if I push the negative indexes directly into the original Prerequisite tag... for that reason I duplicate this tag + key = "Prerequisite"; + 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 idx = TechnoTypeClass::FindIndex(cur); + if (idx >= 0) + { + Prerequisite.push_back(idx); + } + else + { + int index = HouseExt::FindGenericPrerequisite(cur); + if (index < 0) + Prerequisite.push_back(index); + } + } + + key = nullptr; + + // Prerequisite.Negative with Generic Prerequistes support + key = "Prerequisite.Negative"; + 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 idx = TechnoTypeClass::FindIndex(cur); + if (idx >= 0) + { + Prerequisite_Negative.push_back(idx); + } + else + { + int index = HouseExt::FindGenericPrerequisite(cur); + if (index < 0) + Prerequisite_Negative.push_back(index); + } + } + + key = nullptr; + + // Prerequisite.ListX with Generic Prerequistes support + this->Prerequisite_Lists.Read(exINI, pSection, "Prerequisite.Lists"); + + if (Prerequisite_Lists.Get() > 0) + { + for (int i = 1; i <= Prerequisite_Lists.Get(); i++) + { + char keySection[32]; + _snprintf_s(keySection, sizeof(keySection), "Prerequisite.List%d", i); + + DynamicVectorClass objectsList; + char* context2 = nullptr; + pINI->ReadString(pSection, keySection, "", Phobos::readBuffer); + + for (char* cur = strtok_s(Phobos::readBuffer, Phobos::readDelims, &context2); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context2)) + { + int idx = TechnoTypeClass::FindIndex(cur); + if (idx >= 0) + { + objectsList.AddItem(idx); + } + else + { + int index = HouseExt::FindGenericPrerequisite(cur); + if (index < 0) + objectsList.AddItem(index); + } + } + + Prerequisite_ListVector.push_back(objectsList); + objectsList.Clear(); + } + } } template @@ -393,6 +489,12 @@ void TechnoTypeExt::ExtData::Serialize(T& Stm) .Process(this->IronCurtain_KeptOnDeploy) .Process(this->Explodes_KillPassengers) + + .Process(this->Prerequisite_RequiredTheaters) + .Process(this->Prerequisite) + .Process(this->Prerequisite_Negative) + .Process(this->Prerequisite_Lists) + .Process(this->Prerequisite_ListVector) ; } void TechnoTypeExt::ExtData::LoadFromStream(PhobosStreamReader& Stm) diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h index 0e01c3e79a..3ebfb6bcf5 100644 --- a/src/Ext/TechnoType/Body.h +++ b/src/Ext/TechnoType/Body.h @@ -158,6 +158,13 @@ class TechnoTypeExt std::vector> DeployedWeaponBurstFLHs; std::vector> EliteDeployedWeaponBurstFLHs; + // Ares 0.1 + ValueableVector Prerequisite_RequiredTheaters; + ValueableVector Prerequisite; + ValueableVector Prerequisite_Negative; + Valueable Prerequisite_Lists; + std::vector> Prerequisite_ListVector; + ExtData(TechnoTypeClass* OwnerObject) : Extension(OwnerObject) , HealthBar_Hide { false } , UIDescription {} @@ -267,6 +274,9 @@ class TechnoTypeExt , DeployedSecondaryFireFLH { } , IronCurtain_KeptOnDeploy{ } , Explodes_KillPassengers { true } + , Prerequisite { } + , Prerequisite_Negative { } + , Prerequisite_Lists { 0 } { } virtual ~ExtData() = default; diff --git a/src/Phobos.Ext.cpp b/src/Phobos.Ext.cpp index 0dcaed68d2..b27037a8ea 100644 --- a/src/Phobos.Ext.cpp +++ b/src/Phobos.Ext.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -232,6 +233,7 @@ auto MassActions = MassAction < BulletExt, BulletTypeExt, HouseExt, + HouseTypeExt, RadSiteExt, RulesExt, ScenarioExt, From bb8171ecf24bf1cdceacf6384d99a438682f6d25 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Fri, 2 Dec 2022 06:34:27 +0100 Subject: [PATCH 02/59] I forgot to merge the unclassified category at the end of the analysis If set this option. --- src/Ext/Team/Body.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Ext/Team/Body.cpp b/src/Ext/Team/Body.cpp index 4e514c6679..64f33104b8 100644 --- a/src/Ext/Team/Body.cpp +++ b/src/Ext/Team/Body.cpp @@ -142,6 +142,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) 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; @@ -180,8 +181,6 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) percentageUnclassifiedTriggers = 0.0; } - // TO-DO: HERE load new values from Rulesmd.ini, if set - percentageUnclassifiedTriggers = percentageUnclassifiedTriggers < 0.0 || percentageUnclassifiedTriggers > 1.0 ? 0.0 : percentageUnclassifiedTriggers; percentageGroundTriggers = percentageGroundTriggers < 0.0 || percentageGroundTriggers > 1.0 ? 0.0 : percentageGroundTriggers; percentageNavalTriggers = percentageNavalTriggers < 0.0 || percentageNavalTriggers > 1.0 ? 0.0 : percentageNavalTriggers; @@ -585,8 +584,14 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) for (auto entry : pTriggerTeam1Type->TaskForce->Entries) { // If they team has mixed members there is no need to continue + // Also, if the category was merged into another one if (teamIsCategory == teamCategory::Unclassified) + { + if (mergeUnclassifiedCategoryWith >= 0) + teamIsCategory = (teamCategory)mergeUnclassifiedCategoryWith; + break; + } if (entry.Amount > 0) { From 66b8a55fa6ae7d050acfae954dc6b0d22834974c Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 3 Dec 2022 12:22:11 +0100 Subject: [PATCH 03/59] Cleaned code And removed comments and commented old code --- src/Ext/House/Body.cpp | 62 +++++------------------- src/Ext/House/Body.h | 11 ----- src/Ext/HouseType/Body.cpp | 1 + src/Ext/Rules/Body.cpp | 7 +-- src/Ext/Team/Body.cpp | 96 ++++++++++++------------------------- src/Ext/TechnoType/Body.cpp | 4 +- 6 files changed, 49 insertions(+), 132 deletions(-) diff --git a/src/Ext/House/Body.cpp b/src/Ext/House/Body.cpp index 817c160468..71a49380ed 100644 --- a/src/Ext/House/Body.cpp +++ b/src/Ext/House/Body.cpp @@ -103,8 +103,12 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const for (auto pTechno : *TechnoClass::Array) { - if (pTechno->Owner == pThis && pTechno->GetTechnoType() == pItem && pTechno->IsAlive && pTechno->Health > 0) + if (pTechno->Owner == pThis + && pTechno->GetTechnoType() == pItem + && pTechno->IsAlive && pTechno->Health > 0) + { nInstances++; + } } if (nInstances >= pItem->BuildLimit) @@ -112,7 +116,7 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const bool prerequisiteNegativeMet = false; // Only one coincidence is needed - // Ares Prerequisite.Negative list. + // Ares Prerequisite.Negative list if (pItemExt->Prerequisite_Negative.size() > 0) { for (int idx : pItemExt->Prerequisite_Negative) @@ -120,7 +124,7 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const if (prerequisiteNegativeMet) return false; - if (idx < 0) // Is even possible this case? I have to check it + if (idx < 0) // Can be used generic prerequisites in this Ares tag? I have to investigate it but for now we support it... { // Default prerequisites like POWER, PROC, BARRACKS, FACTORY, ... prerequisiteNegativeMet = HouseExt::HasGenericPrerequisite(idx, ownedBuildingTypes); @@ -139,13 +143,10 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const } } - //ValueableVector prerequisite = pItemExt->Prerequisite; DynamicVectorClass prerequisiteOverride = pItem->PrerequisiteOverride; - //UnitTypeClass* prerequisiteProcAlternate = RulesClass::Instance->PrerequisiteProcAlternate; - - bool prerequisiteMet = false; // All in this list must appear in ownedBuildingTypes - bool prerequisiteOverrideMet = false; // Only one coincidence is needed + bool prerequisiteMet = false; // All buildings must appear in the buildings list owner by the house + bool prerequisiteOverrideMet = false; // This tag uses an OR comparator: Only one coincidence is needed if (prerequisiteOverride.Count > 0) { @@ -176,11 +177,11 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const if (pItemExt->Prerequisite.size() > 0) { bool found = false; - //if (pItem) Debug::Log("[%s] prereq check????\n", pItem->ID); + for (int idx : pItemExt->Prerequisite) { found = false; - //Debug::Log("\tidx: %d:\n",idx); + if (idx < 0) { // Default prerequisites like POWER, PROC, BARRACKS, FACTORY, ... @@ -188,7 +189,6 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const } else { - //Debug::Log("\t[%s] (idx: %d):\n", TechnoTypeClass::Array->GetItem(idx)->ID, idx); for (auto pObject : ownedBuildingTypes) { if (found) @@ -196,8 +196,6 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const if (idx == pObject->ArrayIndex) found = true; - - //Debug::Log("\t\t[%s] (idx: %d) Found? %d\n", pObject->ID, pObject->ArrayIndex, found); } } @@ -209,6 +207,7 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const } else { + // No prerequisites list means that always is buildable prerequisiteMet = true; } @@ -253,7 +252,6 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const prerequisiteListsMet = found; } - //Debug::Log("prerequisiteMet: %d, prerequisiteListsMet: %d, prerequisiteOverrideMet: %d\n", prerequisiteMet, prerequisiteListsMet, prerequisiteOverrideMet); return prerequisiteMet || prerequisiteListsMet || prerequisiteOverrideMet; } @@ -264,14 +262,6 @@ bool HouseExt::HasGenericPrerequisite(int idx, const DynamicVectorClass selectedPrerequisite = RulesExt::Global()->GenericPrerequisites.GetItem(std::abs(idx)); - /*Debug::Log("DEBUG: SELECTED DEFAULT PREREQUISITES: %s [%d]\n", RulesExt::Global()->GenericPrerequisitesNames.GetItem(std::abs(idx)), idx); - for (int i : selectedPrerequisite) - { - auto aaa = TechnoTypeClass::Array->GetItem(i); - Debug::Log("[%s] -> %d\n", aaa->ID, i); - } - Debug::Log("\n"); - */ if (selectedPrerequisite.Count == 0) return false; @@ -291,7 +281,6 @@ bool HouseExt::HasGenericPrerequisite(int idx, const DynamicVectorClass found: %d\n", idx, found); return found; } @@ -302,7 +291,7 @@ int HouseExt::FindGenericPrerequisite(const char* id) return 0; if (RulesExt::Global()->GenericPrerequisitesNames.Count == 0) - RulesExt::FillDefaultPrerequisites(); + RulesExt::FillDefaultPrerequisites(); // needed! int i = 0; for (auto str : RulesExt::Global()->GenericPrerequisitesNames) @@ -330,26 +319,6 @@ void HouseExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) for (size_t i = 0; i < 3; i++) this->RepairBaseNodes[i] = readBaseNodeRepairInfo[i < nWritten ? i : nWritten - 1]; } - - Valueable readNewTeamsSelector_MergeUnclassifiedCategoryWith; - readNewTeamsSelector_MergeUnclassifiedCategoryWith.Read(exINI, pSection, "NewTeamsSelector.MergeUnclassifiedCategoryWith"); - this->NewTeamsSelector_MergeUnclassifiedCategoryWith = readNewTeamsSelector_MergeUnclassifiedCategoryWith.Get(); - - Valueable readNewTeamsSelector_UnclassifiedCategoryPercentage; - readNewTeamsSelector_UnclassifiedCategoryPercentage.Read(exINI, pSection, "NewTeamsSelector.UnclassifiedCategoryPercentage"); - this->NewTeamsSelector_UnclassifiedCategoryPercentage = readNewTeamsSelector_UnclassifiedCategoryPercentage.Get(); - - Valueable readNewTeamsSelector_GroundCategoryPercentage; - readNewTeamsSelector_GroundCategoryPercentage.Read(exINI, pSection, "NewTeamsSelector.GroundCategoryPercentage"); - this->NewTeamsSelector_GroundCategoryPercentage = readNewTeamsSelector_GroundCategoryPercentage.Get(); - - Valueable readNewTeamsSelector_NavalCategoryPercentage; - readNewTeamsSelector_NavalCategoryPercentage.Read(exINI, pSection, "NewTeamsSelector.NavalCategoryPercentage"); - this->NewTeamsSelector_NavalCategoryPercentage = readNewTeamsSelector_NavalCategoryPercentage.Get(); - - Valueable readNewTeamsSelector_AirCategoryPercentage; - readNewTeamsSelector_AirCategoryPercentage.Read(exINI, pSection, "NewTeamsSelector.AirCategoryPercentage"); - this->NewTeamsSelector_AirCategoryPercentage = readNewTeamsSelector_AirCategoryPercentage.Get(); } @@ -367,11 +336,6 @@ void HouseExt::ExtData::Serialize(T& Stm) .Process(this->Factory_NavyType) .Process(this->Factory_AircraftType) .Process(this->RepairBaseNodes) - .Process(this->NewTeamsSelector_MergeUnclassifiedCategoryWith) - .Process(this->NewTeamsSelector_UnclassifiedCategoryPercentage) - .Process(this->NewTeamsSelector_GroundCategoryPercentage) - .Process(this->NewTeamsSelector_NavalCategoryPercentage) - .Process(this->NewTeamsSelector_AirCategoryPercentage) ; } diff --git a/src/Ext/House/Body.h b/src/Ext/House/Body.h index 0faaaed6cb..9bf1ef4649 100644 --- a/src/Ext/House/Body.h +++ b/src/Ext/House/Body.h @@ -25,12 +25,6 @@ class HouseExt BuildingClass* Factory_NavyType; BuildingClass* Factory_AircraftType; - int NewTeamsSelector_MergeUnclassifiedCategoryWith; - double NewTeamsSelector_UnclassifiedCategoryPercentage; - double NewTeamsSelector_GroundCategoryPercentage; - double NewTeamsSelector_NavalCategoryPercentage; - double NewTeamsSelector_AirCategoryPercentage; - //Read from INI bool RepairBaseNodes[3]; @@ -42,11 +36,6 @@ class HouseExt , Factory_NavyType { nullptr } , Factory_AircraftType { nullptr } , RepairBaseNodes { false,false,false } - , NewTeamsSelector_MergeUnclassifiedCategoryWith { -1 } - , NewTeamsSelector_UnclassifiedCategoryPercentage { 0.25 } - , NewTeamsSelector_GroundCategoryPercentage { 0.25 } - , NewTeamsSelector_NavalCategoryPercentage { 0.25 } - , NewTeamsSelector_AirCategoryPercentage { 0.25 } { } virtual ~ExtData() = default; diff --git a/src/Ext/HouseType/Body.cpp b/src/Ext/HouseType/Body.cpp index abcb0c43b2..9dfeccf13b 100644 --- a/src/Ext/HouseType/Body.cpp +++ b/src/Ext/HouseType/Body.cpp @@ -74,6 +74,7 @@ bool HouseTypeExt::SaveGlobals(PhobosStreamWriter& Stm) { return Stm.Success(); } + // ============================= // container diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index abf2e55b9c..d41a8d0a5d 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -198,11 +198,11 @@ void RulesExt::FillDefaultPrerequisites() { if (RulesExt::Global()->GenericPrerequisitesNames.Count != 0) return; - //RulesExt::Global()->GenericPrerequisitesNames.Clear(); CCINIClass::INI_Rules; DynamicVectorClass empty; - RulesExt::Global()->GenericPrerequisitesNames.AddItem("POWER"); // -1 + + 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); @@ -218,6 +218,7 @@ void RulesExt::FillDefaultPrerequisites() // 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; @@ -240,7 +241,7 @@ void RulesExt::FillDefaultPrerequisites() } else { - // New + // New generic prerequisite RulesExt::Global()->GenericPrerequisitesNames.AddItem(name); RulesExt::Global()->GenericPrerequisites.AddItem(objectsList); } diff --git a/src/Ext/Team/Body.cpp b/src/Ext/Team/Body.cpp index 64f33104b8..9118cbbd08 100644 --- a/src/Ext/Team/Body.cpp +++ b/src/Ext/Team/Body.cpp @@ -97,16 +97,13 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) GET(HouseClass*, pHouse, ESI); bool houseIsHuman = pHouse->IsHumanPlayer; + if (SessionClass::IsCampaign()) houseIsHuman = pHouse->IsHumanPlayer || pHouse->IsInPlayerControl; if (houseIsHuman || pHouse->Type->MultiplayPassive) return SkipCode; - auto pHouseExt = HouseExt::ExtMap.Find(pHouse); - if (!pHouseExt) - return SkipCode; - auto pHouseTypeExt = HouseTypeExt::ExtMap.Find(pHouse->Type); if (!pHouseTypeExt) return SkipCode; @@ -131,12 +128,10 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) DynamicVectorClass validTriggerCandidatesNavalOnly; DynamicVectorClass validTriggerCandidatesAirOnly; DynamicVectorClass validTriggerCandidatesUnclassifiedOnly; - int dice = ScenarioClass::Instance->Random.RandomRanged(1, 100); - //if (pHouse->IsHumanPlayer || pHouse->IsInPlayerControl) - //return ContinueFlow; + int dice = ScenarioClass::Instance->Random.RandomRanged(1, 100); - //This house must have enabled the triggers + // This house must have the triggers enabled if (dice <= pHouse->RatioAITriggerTeam && pHouse->AITriggersActive) { bool splitTriggersByCategory = RulesExt::Global()->NewTeamsSelector_SplitTriggersByCategory; @@ -151,7 +146,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (splitTriggersByCategory) { - int mergeUnclassifiedCategoryWith = pHouseTypeExt->NewTeamsSelector_MergeUnclassifiedCategoryWith.isset() ? pHouseTypeExt->NewTeamsSelector_MergeUnclassifiedCategoryWith.Get() : RulesExt::Global()->NewTeamsSelector_MergeUnclassifiedCategoryWith; + 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 percentageNavalTriggers = pHouseTypeExt->NewTeamsSelector_NavalCategoryPercentage.isset() ? pHouseTypeExt->NewTeamsSelector_NavalCategoryPercentage.Get() : RulesExt::Global()->NewTeamsSelector_NavalCategoryPercentage; // Only Naval=yes @@ -187,6 +182,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) percentageAirTriggers = percentageAirTriggers < 0.0 || percentageAirTriggers > 1.0 ? 0.0 : percentageAirTriggers; double totalPercengates = percentageUnclassifiedTriggers + percentageGroundTriggers + percentageNavalTriggers + percentageAirTriggers; + if (totalPercengates > 1.0 || totalPercengates <= 0.0) splitTriggersByCategory = false; // Note: if the sum of all percentages is less than 100% then that empty space will work like "no categories" @@ -222,13 +218,9 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } } - // PREPARING... - int houseIdx = pHouse->ArrayIndex; int sideIdx = pHouse->SideIndex + 1; - //int enemyHouseIndex = pHouse->EnemyHouseIndex >= 0 ? pHouse->EnemyHouseIndex : -1; 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); @@ -256,20 +248,21 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } activeTeams = activeTeamsList.Count; - // We will use these for discarding triggers + + // We will use these values for discarding triggers bool hasReachedMaxTeamsLimit = activeTeams < maxTeamsLimit ? false : true; bool hasReachedMaxDefensiveTeamsLimit = activeDefenseTeamsCount < maxBaseDefenseTeams ? false : true; if (hasReachedMaxDefensiveTeamsLimit) - Debug::Log("DEBUG: House [%s] (Idx: %d) reached the MaximumAIDefensiveTeams value!\n", pHouse->Type->ID, pHouse->ArrayIndex); + Debug::Log("DEBUG: House [%s] (idx: %d) reached the MaximumAIDefensiveTeams value!\n", pHouse->Type->ID, pHouse->ArrayIndex); if (hasReachedMaxTeamsLimit) { - Debug::Log("DEBUG: House [%s] (Idx: %d) reached the TotalAITeamCap value!\n", pHouse->Type->ID, pHouse->ArrayIndex); + Debug::Log("DEBUG: House [%s] (idx: %d) reached the TotalAITeamCap value!\n", pHouse->Type->ID, pHouse->ArrayIndex); return SkipCode; } - // Build a list of built structures by the house + // Obtain the real list of structures the house have DynamicVectorClass ownedBuildingTypes; for (auto building : pHouse->Buildings) { @@ -334,14 +327,6 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } } - //Debug::Log("List of recruitable units of this house:\n"); - //for (auto item : recruitableUnits) - //{ - //if (item.object) - //Debug::Log("[%s] = %d\n", item.object->ID, item.count); - //} - //Debug::Log("AAA\n"); - HouseClass* targetHouse = nullptr; if (pHouse->EnemyHouseIndex >= 0) targetHouse = HouseClass::Array->GetItem(pHouse->EnemyHouseIndex); @@ -363,7 +348,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) // The trigger must be compatible with the owner if ((triggerHouse == -1 || houseIdx == triggerHouse) && (triggerSide == 0 || sideIdx == triggerSide)) { - // "ConditionType=-1" will be skipped, is always valid (and save CPU) + // "ConditionType=-1" will be skipped, always is valid if ((int)pTrigger->ConditionType >= 0) { if ((int)pTrigger->ConditionType == 0) @@ -407,7 +392,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 8) { - // Simulate case 0: "enemy owns" but without restrict it against only 1 enemy house + // Simulate case 0: "enemy owns" but instead of restrict it against the main enemy house it is done against all enemies if (!pTrigger->ConditionObject) continue; @@ -423,7 +408,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) { // New case 9: Like in case 0 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the enemy. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF and 256 is 0x0001 + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, targetHouse, false, list); @@ -436,7 +421,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) { // New case 10: Like in case 1 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the house. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF and 256 is 0x0001 + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::HouseOwns(pTrigger, pHouse, false, list); @@ -449,7 +434,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) { // New case 11: Like in case 7 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the Civilians. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF and 256 is 0x0001 + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::NeutralOwns(pTrigger, list); @@ -462,7 +447,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) { // New case 12: Like in case 0 & 9 but instead of a specific enemy this checks in all enemies. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF and 256 is 0x0001 + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, nullptr, false, list); @@ -475,7 +460,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) { // New case 13: Like in case 1 & 10 but instead checking the house now checks the allies. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF and 256 is 0x0001 + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::HouseOwns(pTrigger, pHouse, true, list); @@ -488,7 +473,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) { // New case 14: Like in case 9 but instead of meet any comparison now is required all. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF and 256 is 0x0001 + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::EnemyOwnsAll(pTrigger, pHouse, targetHouse, list); @@ -501,7 +486,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) { // New case 15: Like in case 10 but instead of meet any comparison now is required all. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF and 256 is 0x0001 + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::HouseOwnsAll(pTrigger, pHouse, list); @@ -514,7 +499,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) { // New case 16: Like in case 11 but instead of meet any comparison now is required all. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF and 256 is 0x0001 + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::NeutralOwnsAll(pTrigger, list); @@ -527,7 +512,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) { // New case 17: Like in case 14 but instead of meet any comparison now is required all. Check all enemies - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF and 256 is 0x0001 + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::EnemyOwnsAll(pTrigger, pHouse, nullptr, list); @@ -538,18 +523,12 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) else { // Other cases from vanilla game - //bool cm = pTrigger->ConditionMet(pHouse, targetHouse, hasReachedMaxDefensiveTeamsLimit); - //Debug::Log("[%s], \"%s\"\nOld ConditionMet section (%d, %s): %d [-, %d, %d]\n\n", pTrigger->ID, pTrigger->Team1->Name, (int)pTrigger->ConditionType, (pTrigger->ConditionObject ? pTrigger->ConditionObject->ID : ""), cm, pTrigger->Conditions->ComparatorOperand, pTrigger->Conditions->ComparatorType); - if (!pTrigger->ConditionMet(pHouse, targetHouse, hasReachedMaxDefensiveTeamsLimit)) continue; } } - /*else - { - bool cm = false; - }*/ + // All triggers below 5000 in current weight will get discarded if this mode is enabled if (onlyCheckImportantTriggers) { if (pTrigger->Weight_Current < 5000) @@ -595,39 +574,24 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (entry.Amount > 0) { - /* - What category goes each unit? there is the table: - - - Air category magic words: - ConsideredAircraft=yes - JumpJet=yes - BalloonHover=yes - - - Maval category magic words: - Naval=yes - MovementRestrictedTo=Water - - - Mixed category - MovementZone=Amphibious - - - - Ground category magic words: - Anyone that doesn't meet the previous magic words - */ - if (entry.Type) { - if (entry.Type->WhatAmI() == AbstractType::AircraftType || entry.Type->ConsideredAircraft) + if (entry.Type->WhatAmI() == AbstractType::AircraftType + || entry.Type->ConsideredAircraft) { // For now the team is from air category teamIsCategory = teamIsCategory == teamCategory::None || teamIsCategory == teamCategory::Air ? teamCategory::Air : teamCategory::Unclassified; } - else if (entry.Type->Naval && (entry.Type->MovementZone != MovementZone::Amphibious && entry.Type->MovementZone != MovementZone::AmphibiousDestroyer || entry.Type->MovementZone != MovementZone::AmphibiousCrusher)) + else if (entry.Type->Naval + && (entry.Type->MovementZone != MovementZone::Amphibious + && entry.Type->MovementZone != MovementZone::AmphibiousDestroyer + && entry.Type->MovementZone != MovementZone::AmphibiousCrusher)) { // For now the team is from naval category teamIsCategory = teamIsCategory == teamCategory::None || teamIsCategory == teamCategory::Naval ? teamCategory::Naval : teamCategory::Unclassified; } - else if (teamIsCategory != teamCategory::Naval && teamIsCategory != teamCategory::Air) + else if (teamIsCategory != teamCategory::Naval + && teamIsCategory != teamCategory::Air) { // For now the team doesn't belong to the previous categories teamIsCategory = teamIsCategory != teamCategory::Unclassified ? teamCategory::Ground : teamCategory::Unclassified; diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index 749f40db53..6a2fff6a98 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -274,8 +274,6 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->DeployedSecondaryFireFLH.Read(exArtINI, pArtSection, "DeployedSecondaryFireFLH"); // Prerequisite.RequiredTheaters contains a list of theader names - this->Prerequisite_RequiredTheaters.Read(exINI, pSection, "Prerequisite.RequiredTheaters"); - char* key = "Prerequisite.RequiredTheaters"; char* context = nullptr; pINI->ReadString(pSection, key, "", Phobos::readBuffer); @@ -290,7 +288,7 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) key = nullptr; // Prerequisite with Generic Prerequistes support. - // Note: I have no idea of what could happen in the game engine if I push the negative indexes directly into the original Prerequisite tag... for that reason I duplicate this tag + // Note: I have no idea of what could happen in all the game engine logics if I push the negative indexes of the Ares generic prerequisites directly into the original Prerequisite tag... for that reason this tag is duplicated for working with it key = "Prerequisite"; context = nullptr; pINI->ReadString(pSection, key, "", Phobos::readBuffer); From 355c682e02165a9f566717fbd53bf53bb8c43986 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 3 Dec 2022 12:40:24 +0100 Subject: [PATCH 04/59] . --- src/Ext/Team/Body.cpp | 129 +++++++----------------------------------- 1 file changed, 22 insertions(+), 107 deletions(-) diff --git a/src/Ext/Team/Body.cpp b/src/Ext/Team/Body.cpp index 9118cbbd08..38b3a3de5c 100644 --- a/src/Ext/Team/Body.cpp +++ b/src/Ext/Team/Body.cpp @@ -182,11 +182,10 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) percentageAirTriggers = percentageAirTriggers < 0.0 || percentageAirTriggers > 1.0 ? 0.0 : percentageAirTriggers; double totalPercengates = percentageUnclassifiedTriggers + percentageGroundTriggers + percentageNavalTriggers + percentageAirTriggers; - if (totalPercengates > 1.0 || totalPercengates <= 0.0) splitTriggersByCategory = false; - // Note: if the sum of all percentages is less than 100% then that empty space will work like "no categories" + // Note: if the sum of all percentages is less than 100% then that empty space will work like "no categories" if (splitTriggersByCategory) { int categoryDice = ScenarioClass::Instance->Random.RandomRanged(1, 100); @@ -220,7 +219,9 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) int houseIdx = pHouse->ArrayIndex; int sideIdx = pHouse->SideIndex + 1; + //int enemyHouseIndex = pHouse->EnemyHouseIndex >= 0 ? pHouse->EnemyHouseIndex : -1; 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); @@ -522,7 +523,6 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else { - // Other cases from vanilla game if (!pTrigger->ConditionMet(pHouse, targetHouse, hasReachedMaxDefensiveTeamsLimit)) continue; } @@ -562,8 +562,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) { for (auto entry : pTriggerTeam1Type->TaskForce->Entries) { - // If they team has mixed members there is no need to continue - // Also, if the category was merged into another one + // If the team have mixed members there is no need to continue if (teamIsCategory == teamCategory::Unclassified) { if (mergeUnclassifiedCategoryWith >= 0) @@ -606,7 +605,6 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } bool allObjectsCanBeBuiltOrRecruited = true; - //Debug::Log("[%s] [%s] is evaluating unit prerequisites...\n", pTrigger->ID, pTriggerTeam1Type->ID); if (pTriggerTeam1Type->Autocreate) { @@ -619,21 +617,13 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) continue; TechnoTypeClass* object = entry.Type; - //bool canBeBuilt = pHouse->AllPrerequisitesAvailable(object, ownedBuildingTypes, ownedBuildingTypes.Count); // Old - - // Experimental bool canBeBuilt = HouseExt::PrerequisitesMet(pHouse, object, ownedBuildingTypes); if (!canBeBuilt) { - //Debug::Log("[%s] not ok...\n", object->ID); allObjectsCanBeBuiltOrRecruited = false; break; } - else - { - //Debug::Log("[%s] build prerequisites met!\n", object->ID); - } } else { @@ -646,20 +636,16 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) allObjectsCanBeBuiltOrRecruited = false; } - //if (allObjectsCanBeBuiltOrRecruited) - //Debug::Log("[%s] [%s] can build units!...\n", pTrigger->ID, pTriggerTeam1Type->ID); - if (!allObjectsCanBeBuiltOrRecruited && pTriggerTeam1Type->Recruiter) { allObjectsCanBeBuiltOrRecruited = true; - //Debug::Log("[%s] [%s] checking if it can recruit...\n", pTrigger->ID, pTriggerTeam1Type->ID); + for (auto entry : pTriggerTeam1Type->TaskForce->Entries) { // Check if each unit in the taskforce has the available recruitable units in the map if (allObjectsCanBeBuiltOrRecruited && entry.Amount > 0) { bool canBeRecruited = false; - //Debug::Log("[%s] checking if there are recruitable...\n", entry.Type->ID); for (auto item : recruitableUnits) { @@ -674,14 +660,9 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (!canBeRecruited) { - //Debug::Log("[%s] %d can not be recruited!\n", entry.Type->ID, entry.Amount); allObjectsCanBeBuiltOrRecruited = false; break; } - else - { - //Debug::Log("[%s] can recruit %d units!\n", entry.Type->ID, entry.Amount); - } } } } @@ -690,8 +671,8 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (!allObjectsCanBeBuiltOrRecruited) continue; - // Special case: triggers become very important if they reach the max priority (value 5000) - // They get stored in a different list and all previous triggers are discarded. + // Special case: triggers become very important if they reach the max priority (value 5000). + // They get stored in a elitist list and all previous triggers are discarded. if (pTrigger->Weight_Current >= 5000 && !onlyCheckImportantTriggers) { // First time only @@ -706,14 +687,14 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) validCategory = teamCategory::None; } - // Reset and now only add important triggers to the list + // Reset the current ones and now only will be added important triggers to the list onlyCheckImportantTriggers = true; totalWeight = 0.0; splitTriggersByCategory = false; // VIP teams breaks the categories logic (on purpose) } // Passed all checks, save this trigger for later. - // The idea behind this is to simulate an ordered list of weights and once we throw the dice we'll know the winner trigger: The more weight means more possibilities to be selected. + // The idea behind this is to simulate an ordered list of weights and once we throw the dice we'll know the winner trigger: More weight means more possibilities to be selected. totalWeight += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; TriggerElementWeight item; item.Trigger = pTrigger; @@ -785,7 +766,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (splitTriggersByCategory) { - Debug::Log("DEBUG: Category percentages:\nMixed teams: %f\nGround teams: %f\nNaval teams: %f\nAir teams: %f\n", percentageUnclassifiedTriggers, percentageGroundTriggers, percentageNavalTriggers, percentageAirTriggers); + //Debug::Log("DEBUG: Category percentages:\nMixed teams: %f\nGround teams: %f\nNaval teams: %f\nAir teams: %f\n", percentageUnclassifiedTriggers, percentageGroundTriggers, percentageNavalTriggers, percentageAirTriggers); switch (validCategory) { @@ -817,17 +798,17 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) return SkipCode; } - if ((validCategory == teamCategory::Ground && totalGroundCategoryTriggers == 0) || - (validCategory == teamCategory::Unclassified && totalUnclassifiedCategoryTriggers == 0) || - (validCategory == teamCategory::Air && totalAirCategoryTriggers == 0) || - (validCategory == teamCategory::Naval && totalNavalCategoryTriggers == 0)) + if ((validCategory == teamCategory::Ground && totalGroundCategoryTriggers == 0) + || (validCategory == teamCategory::Unclassified && totalUnclassifiedCategoryTriggers == 0) + || (validCategory == teamCategory::Air && totalAirCategoryTriggers == 0) + || (validCategory == teamCategory::Naval && totalNavalCategoryTriggers == 0)) { - Debug::Log("DEBUG: [%s] (Idx: %d) No valid triggers of this category. A new attempt should be done later...\n", pHouse->Type->ID, pHouse->ArrayIndex); + Debug::Log("DEBUG: [%s] (idx: %d) No valid triggers of this category. A new attempt should be done later...\n", pHouse->Type->ID, pHouse->ArrayIndex); if (!isFallbackEnabled) return SkipCode; - Debug::Log("... BUT fallback mode is enabled so now it will check all triggers available.\n"); + Debug::Log("... BUT fallback mode is enabled so now will be checked all available triggers.\n"); validCategory = teamCategory::None; } @@ -840,163 +821,100 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) { case teamCategory::None: weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeight) * 1.0; - // Debug::Log("Possible values of the dice [0 - %f]. Dice says: %f:\n", totalWeight, weightDice); - // Debug::Log("Picking the trigger with highest weight from the list (No category set):\n"); for (auto element : validTriggerCandidates) { - // Debug::Log("[%s] Lottery range values: %f - %f", element.Trigger->ID, lastWeight, element.Weight); lastWeight = element.Weight; if (weightDice < element.Weight && !found) { - // Debug::Log(" ... CANDIDATE!\n"); selectedTrigger = element.Trigger; found = true; } - else - { - // Debug::Log(" ... \n"); - } } break; case teamCategory::Ground: weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightGroundOnly) * 1.0; - // Debug::Log("Possible values of the dice [0 - %f]. Dice says: %f:\n", totalWeightGroundOnly, weightDice); - // Debug::Log("Picking the trigger with highest weight from the list (Ground category):\n"); for (auto element : validTriggerCandidatesGroundOnly) { - // Debug::Log("[%s] Lottery range values: %f - %f", element.Trigger->ID, lastWeight, element.Weight); lastWeight = element.Weight; if (weightDice < element.Weight && !found) { - // Debug::Log(" ... CANDIDATE!\n"); selectedTrigger = element.Trigger; found = true; } - else - { - // Debug::Log(" ... \n"); - } } break; case teamCategory::Unclassified: weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightUnclassifiedOnly) * 1.0; - // Debug::Log("Possible values of the dice [0 - %f]. Dice says: %f:\n", totalWeightUnclassifiedOnly, weightDice); - // Debug::Log("Picking the trigger with highest weight from the list (Mixed category):\n"); for (auto element : validTriggerCandidatesUnclassifiedOnly) { - // Debug::Log("[%s] Lottery range values: %f - %f", element.Trigger->ID, lastWeight, element.Weight); lastWeight = element.Weight; if (weightDice < element.Weight && !found) { - // Debug::Log(" ... CANDIDATE!\n"); selectedTrigger = element.Trigger; found = true; } - else - { - // Debug::Log(" ... \n"); - } } break; case teamCategory::Naval: weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightNavalOnly) * 1.0; - // Debug::Log("Possible values of the dice [0 - %f]. Dice says: %f:\n", totalWeightNavalOnly, weightDice); - // Debug::Log("Picking the trigger with highest weight from the list (Naval category):\n"); for (auto element : validTriggerCandidatesNavalOnly) { - // Debug::Log("[%s] Lottery range values: %f - %f", element.Trigger->ID, lastWeight, element.Weight); lastWeight = element.Weight; if (weightDice < element.Weight && !found) { - // Debug::Log(" ... CANDIDATE!\n"); selectedTrigger = element.Trigger; found = true; } - else - { - // Debug::Log(" ... \n"); - } } break; case teamCategory::Air: weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightAirOnly) * 1.0; - // Debug::Log("Possible values of the dice [0 - %f]. Dice says: %f:\n", totalWeightAirOnly, weightDice); - // Debug::Log("Picking the trigger with highest weight from the list (Air category):\n"); for (auto element : validTriggerCandidatesAirOnly) { - // Debug::Log("[%s] Lottery range values: %f - %f", element.Trigger->ID, lastWeight, element.Weight); lastWeight = element.Weight; if (weightDice < element.Weight && !found) { - // Debug::Log(" ... CANDIDATE!\n"); selectedTrigger = element.Trigger; found = true; } - else - { - // Debug::Log(" ... \n"); - } } break; default: break; } - /* - Debug::Log("[%s] Lottery range values: %f - %f", element.Trigger->ID, lastWeight, element.Weight); - lastWeight = element.Weight; - - if (weightDice < element.WeightOnlyNaval && !found) - { - Debug::Log(" ... CANDIDATE!\n"); - selectedTrigger = element.Trigger; - found = true; - } - else - { - Debug::Log(" ... \n"); - } - */ if (!selectedTrigger) { - Debug::Log("DEBUG: House [%s] (Idx: %d) failed to select Trigger. A new attempt Will be done later...\n", pHouse->Type->ID, pHouse->ArrayIndex); + Debug::Log("DEBUG: House [%s] (idx: %d) failed to select Trigger. A new attempt Will be done later...\n", pHouse->Type->ID, pHouse->ArrayIndex); return SkipCode; } if (selectedTrigger->Weight_Current >= 5000.0 && selectedTrigger->Weight_Minimum <= 4999.0) { - // Next time this trigger will be out of the important triggers list + // Next time this trigger will be out of the elitist triggers list selectedTrigger->Weight_Current = 4999.0; } - // We have a winner - Debug::Log("DEBUG: House [%s] (Idx: %d) selected trigger [%s].\n", pHouse->Type->ID, pHouse->ArrayIndex, selectedTrigger->ID); - - //for (auto entry : selectedTrigger->Team1->TaskForce->Entries) - //{ - //if (entry.Amount > 0) - //Debug::Log("[%s] = %d\n", entry.Type->ID, entry.Amount); - //else - //break; - //} - //Debug::Log("\n"); + // We have a winner trigger here + Debug::Log("DEBUG: House [%s] (idx: %d) selected trigger [%s].\n", pHouse->Type->ID, pHouse->ArrayIndex, selectedTrigger->ID); + // Team 1 creation auto pTriggerTeam1Type = selectedTrigger->Team1; if (pTriggerTeam1Type) { @@ -1015,6 +933,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } } + // Team 2 creation (if set) auto pTriggerTeam2Type = selectedTrigger->Team2; if (pTriggerTeam2Type) { @@ -1032,12 +951,8 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) newTeam->NeedsToDisappear = false; } } - - //Debug::Log("TeamClass::Array->Count: %d\n", TeamClass::Array->Count); } - - //selectedTeams46 = newTeamsList; return SkipCode; } From 5dc2862069d661d1f23753da2fe779d53aaea482 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 3 Dec 2022 12:53:14 +0100 Subject: [PATCH 05/59] . --- src/Ext/Team/Body.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Ext/Team/Body.cpp b/src/Ext/Team/Body.cpp index 38b3a3de5c..08f796bd36 100644 --- a/src/Ext/Team/Body.cpp +++ b/src/Ext/Team/Body.cpp @@ -219,9 +219,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) int houseIdx = pHouse->ArrayIndex; int sideIdx = pHouse->SideIndex + 1; - //int enemyHouseIndex = pHouse->EnemyHouseIndex >= 0 ? pHouse->EnemyHouseIndex : -1; 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); @@ -523,6 +521,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else { + // Other cases from vanilla game if (!pTrigger->ConditionMet(pHouse, targetHouse, hasReachedMaxDefensiveTeamsLimit)) continue; } @@ -672,7 +671,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) continue; // Special case: triggers become very important if they reach the max priority (value 5000). - // They get stored in a elitist list and all previous triggers are discarded. + // They get stored in a elitist list and all previous triggers are discarded if (pTrigger->Weight_Current >= 5000 && !onlyCheckImportantTriggers) { // First time only @@ -683,7 +682,6 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) validTriggerCandidatesNavalOnly.Clear(); validTriggerCandidatesAirOnly.Clear(); validTriggerCandidatesUnclassifiedOnly.Clear(); - validCategory = teamCategory::None; } From ceb07ade06d9aeb073551c268b3c711cb60bbe46 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 3 Dec 2022 13:10:04 +0100 Subject: [PATCH 06/59] . --- src/Ext/Team/Body.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Ext/Team/Body.cpp b/src/Ext/Team/Body.cpp index 08f796bd36..d07c4e67e4 100644 --- a/src/Ext/Team/Body.cpp +++ b/src/Ext/Team/Body.cpp @@ -764,8 +764,6 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (splitTriggersByCategory) { - //Debug::Log("DEBUG: Category percentages:\nMixed teams: %f\nGround teams: %f\nNaval teams: %f\nAir teams: %f\n", percentageUnclassifiedTriggers, percentageGroundTriggers, percentageNavalTriggers, percentageAirTriggers); - switch (validCategory) { case teamCategory::Ground: From f1f43698505377edfdba6b6e3b247dbd4428e2a2 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 3 Dec 2022 15:50:48 +0100 Subject: [PATCH 07/59] Added documentation --- docs/AI-Scripting-and-Mapping.md | 61 ++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/docs/AI-Scripting-and-Mapping.md b/docs/AI-Scripting-and-Mapping.md index 2f0829e368..710bfcab73 100644 --- a/docs/AI-Scripting-and-Mapping.md +++ b/docs/AI-Scripting-and-Mapping.md @@ -17,6 +17,67 @@ RepairBaseNodes=no,no,no ; 3 booleans indicating whether AI repair basenodes in - Teams spawned by trigger action 7,80,107 can use IFV and opentopped logic normally. - `InitialPayload` logic from Ares is not supported yet. +## 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 4 categories: ground, 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 25% 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 understand Ares tags like `Prerequisite.RequiredTheaters`, `Prerequisite.Negative`, `Prerequisite.Lists` & `Generic prerequisites` section. +- If it finds a trigger with 5000 current probability weight then discard valid triggers all and start searching all valid triggers with weight 5000. AI will pick 1 randomly and decrease by 1 the current weight of the selected trigger (so if nothing happens in the next teams selection loop it won't appear in this special list). Under this scenario categories are disabled. + +In `rulesmd.ini`: +```ini +[AI] +NewTeamsSelector=false ; boolean +NewTeamsSelector.SplitTriggersByCategory=true ; boolean +NewTeamsSelector.EnableFallback=false ; boolean +NewTeamsSelector.GroundCategoryPercentage=0.25 ; floating point value, percents or absolute +NewTeamsSelector.NavalCategoryPercentage=0.25 ; floating point value, percents or absolute +NewTeamsSelector.AirCategoryPercentage=0.25 ; floating point value, percents or absolute +NewTeamsSelector.UnclassifiedCategoryPercentage=0.25 ; floating point value, percents or absolute +NewTeamsSelector.MergeUnclassifiedCategoryWith=-1 ; Integer - Ground: 1, Air: 2, Naval: 3 + +[SOMEHOUSE] ; HouseType +NewTeamsSelector.MergeUnclassifiedCategoryWith= ; boolean +NewTeamsSelector.UnclassifiedCategoryPercentage= ; floating point value, percents or absolute +NewTeamsSelector.GroundCategoryPercentage= ; floating point value, percents or absolute +NewTeamsSelector.AirCategoryPercentage ; floating point value, percents or absolute +NewTeamsSelector.NavalCategoryPercentage ; floating point value, percents or absolute +``` + +- 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 | + +- 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 From f0b9a561a3949e375233600a83d20671c4f724bf Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 3 Dec 2022 16:06:18 +0100 Subject: [PATCH 08/59] What's new and credits file changes --- CREDITS.md | 1 + docs/Whats-New.md | 1 + 2 files changed, 2 insertions(+) diff --git a/CREDITS.md b/CREDITS.md index bcc3e2e30e..83db04de96 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -118,6 +118,7 @@ This page lists all the individual contributions to the project by their author. - Shared ammo logic - Customizable FLH when infantry is prone or deployed - Initial strength for cloned infantry + - New AI teams selector - **Starkku**: - Warhead shield penetration & breaking - Strafing aircraft weapon customization diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 0b1ca16a30..682200a93f 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -253,6 +253,7 @@ New: - Forcing specific weapon against cloaked or disguised targets (by Starkku) - Customizable ROF random delay (by Starkku) - Animation with `Tiled=yes` now supports `CustomPalette` (by ststl) +- New AI teams selector (by FS-21) Vanilla fixes: - Allow AI to repair structures built from base nodes/trigger action 125/SW delivery in single player missions (by Trsdy) From ee5ca9defae45f2e4eb93e29695c7f1c7bd96489 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 3 Dec 2022 16:14:29 +0100 Subject: [PATCH 09/59] trailing space --- docs/AI-Scripting-and-Mapping.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/AI-Scripting-and-Mapping.md b/docs/AI-Scripting-and-Mapping.md index 710bfcab73..02cb910cc7 100644 --- a/docs/AI-Scripting-and-Mapping.md +++ b/docs/AI-Scripting-and-Mapping.md @@ -20,7 +20,7 @@ RepairBaseNodes=no,no,no ; 3 booleans indicating whether AI repair basenodes in ## 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 4 categories: ground, air, naval & mixed categories. If set, AI picks a random trigger in a random category. +- It can split the valid triggers into 4 categories: ground, 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 25% 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. From 45b04569ad6e1527bdcc88e0d6c49fb321a20ddf Mon Sep 17 00:00:00 2001 From: FS-21 Date: Wed, 11 Jan 2023 07:11:11 +0100 Subject: [PATCH 10/59] Fixed all the bugs related to the Teams categorization, now all the AI teams go to the right category depending the members of the Taskforce. Added 2 new boolean tags for overwriting the categorization of some units: - ConsideredNaval - ConsideredVehicle --- src/Ext/Team/Body.cpp | 137 ++++++++++++++++++++++++++++-------- src/Ext/Team/Body.h | 1 + src/Ext/TechnoType/Body.cpp | 5 ++ src/Ext/TechnoType/Body.h | 5 ++ 4 files changed, 120 insertions(+), 28 deletions(-) diff --git a/src/Ext/Team/Body.cpp b/src/Ext/Team/Body.cpp index d07c4e67e4..76698abb15 100644 --- a/src/Ext/Team/Body.cpp +++ b/src/Ext/Team/Body.cpp @@ -248,6 +248,26 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) activeTeams = activeTeamsList.Count; + /*Debug::Log("=====================\n[%s] ACTIVE TEAMS: %d / %d\n", pHouse->Type->ID, activeTeams, maxTeamsLimit); + for (auto team : activeTeamsList) + { + Debug::Log("[%s](%d) : %s\n", team->Type->ID, team->TotalObjects, team->Type->Name); + Debug::Log(" IsMoving: %d, IsFullStrength: %d, IsUnderStrength: %d\n", team->IsMoving, team->IsFullStrength, team->IsUnderStrength); + int i = 0; + + for (auto entry : team->Type->TaskForce->Entries) + { + if (entry.Amount > 0) + { + if (entry.Type) + Debug::Log("\t[%s]: %d / %d\n", entry.Type->ID, team->CountObjects[i], entry.Amount); + } + + i++; + } + } + Debug::Log("=====================\n");*/ + // We will use these values for discarding triggers bool hasReachedMaxTeamsLimit = activeTeams < maxTeamsLimit ? false : true; bool hasReachedMaxDefensiveTeamsLimit = activeDefenseTeamsCount < maxBaseDefenseTeams ? false : true; @@ -365,7 +385,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 1) { - // Simulate case 0: "house owns" + // Simulate case 1: "house owns" if (!pTrigger->ConditionObject) continue; @@ -521,7 +541,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else { - // Other cases from vanilla game + // Other cases from vanilla game if (!pTrigger->ConditionMet(pHouse, targetHouse, hasReachedMaxDefensiveTeamsLimit)) continue; } @@ -559,48 +579,69 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) // Analyze what kind of category is this main team if the feature is enabled if (splitTriggersByCategory) { - for (auto entry : pTriggerTeam1Type->TaskForce->Entries) + //Debug::Log("DEBUG: TaskForce [%s] members:\n", pTriggerTeam1Type->TaskForce->ID); + // TaskForces are limited to 6 entries + for (int i = 0; i < 6; i++) { - // If the team have mixed members there is no need to continue - if (teamIsCategory == teamCategory::Unclassified) - { - if (mergeUnclassifiedCategoryWith >= 0) - teamIsCategory = (teamCategory)mergeUnclassifiedCategoryWith; - - break; - } + auto entry = pTriggerTeam1Type->TaskForce->Entries[i]; + teamCategory entryIsCategory = teamCategory::Ground; if (entry.Amount > 0) { - if (entry.Type) + if (entry.Type->WhatAmI() == AbstractType::AircraftType + || entry.Type->ConsideredAircraft) { - if (entry.Type->WhatAmI() == AbstractType::AircraftType - || entry.Type->ConsideredAircraft) - { - // For now the team is from air category - teamIsCategory = teamIsCategory == teamCategory::None || teamIsCategory == teamCategory::Air ? teamCategory::Air : teamCategory::Unclassified; - } - else if (entry.Type->Naval - && (entry.Type->MovementZone != MovementZone::Amphibious - && entry.Type->MovementZone != MovementZone::AmphibiousDestroyer - && entry.Type->MovementZone != MovementZone::AmphibiousCrusher)) + // This unit is from air category + entryIsCategory = teamCategory::Air; + //Debug::Log("\t[%s](%d) is in AIR category.\n", entry.Type->ID, entry.Amount); + } + else + { + auto pTechnoTypeExt = TechnoTypeExt::ExtMap.Find(entry.Type); + if (!pTechnoTypeExt) + continue; + + if (pTechnoTypeExt->ConsideredNaval + || (entry.Type->Naval + && (entry.Type->MovementZone != MovementZone::Amphibious + && entry.Type->MovementZone != MovementZone::AmphibiousDestroyer + && entry.Type->MovementZone != MovementZone::AmphibiousCrusher))) { - // For now the team is from naval category - teamIsCategory = teamIsCategory == teamCategory::None || teamIsCategory == teamCategory::Naval ? teamCategory::Naval : teamCategory::Unclassified; + // This unit is from naval category + entryIsCategory = teamCategory::Naval; + //Debug::Log("\t[%s](%d) is in NAVAL category.\n", entry.Type->ID, entry.Amount); } - else if (teamIsCategory != teamCategory::Naval - && teamIsCategory != teamCategory::Air) + + if (pTechnoTypeExt->ConsideredVehicle + || (entryIsCategory != teamCategory::Naval + && entryIsCategory != teamCategory::Air)) { - // For now the team doesn't belong to the previous categories - teamIsCategory = teamIsCategory != teamCategory::Unclassified ? teamCategory::Ground : teamCategory::Unclassified; + // This unit is from ground category + entryIsCategory = teamCategory::Ground; + //Debug::Log("\t[%s](%d) is in GROUND category.\n", entry.Type->ID, entry.Amount); } } + + // if a team have multiple categories it will be a mixed category + teamIsCategory = teamIsCategory == teamCategory::None || teamIsCategory == entryIsCategory ? entryIsCategory : teamCategory::Unclassified; + + if (teamIsCategory == teamCategory::Unclassified) + break; } else { break; } } + + //Debug::Log("DEBUG: This team is a category %d (1:Ground, 2:Air, 3:Naval, 4:Mixed).\n", teamIsCategory); + // Si existe este valor y el team es MIXTO se sobreescribe el tipo de categoría + if (teamIsCategory == teamCategory::Unclassified + && mergeUnclassifiedCategoryWith >= 0) + { + //Debug::Log("DEBUG: MIXED category forced to work as category %d.\n", mergeUnclassifiedCategoryWith); + teamIsCategory = (teamCategory)mergeUnclassifiedCategoryWith; + } } bool allObjectsCanBeBuiltOrRecruited = true; @@ -817,6 +858,14 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) { case teamCategory::None: weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeight) * 1.0; + /*Debug::Log("Weight Dice: %f\n", weightDice); + + // Debug + Debug::Log("DEBUG: Candidate AI triggers list:\n"); + for (TriggerElementWeight element : validTriggerCandidates) + { + Debug::Log("Weight: %f, [%s][%s]: %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Team1->Name); + }*/ for (auto element : validTriggerCandidates) { @@ -832,6 +881,14 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) case teamCategory::Ground: weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightGroundOnly) * 1.0; + /*Debug::Log("Weight Dice: %f\n", weightDice); + + // Debug + Debug::Log("DEBUG: Candidate AI triggers list:\n"); + for (TriggerElementWeight element : validTriggerCandidatesGroundOnly) + { + Debug::Log("Weight: %f, [%s][%s]: %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Team1->Name); + }*/ for (auto element : validTriggerCandidatesGroundOnly) { @@ -847,6 +904,14 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) case teamCategory::Unclassified: weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightUnclassifiedOnly) * 1.0; + /*Debug::Log("Weight Dice: %f\n", weightDice); + + // Debug + Debug::Log("DEBUG: Candidate AI triggers list:\n"); + for (TriggerElementWeight element : validTriggerCandidatesUnclassifiedOnly) + { + Debug::Log("Weight: %f, [%s][%s]: %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Team1->Name); + }*/ for (auto element : validTriggerCandidatesUnclassifiedOnly) { @@ -862,6 +927,14 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) case teamCategory::Naval: weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightNavalOnly) * 1.0; + /*Debug::Log("Weight Dice: %f\n", weightDice); + + // Debug + Debug::Log("DEBUG: Candidate AI triggers list:\n"); + for (TriggerElementWeight element : validTriggerCandidatesNavalOnly) + { + Debug::Log("Weight: %f, [%s][%s]: %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Team1->Name); + }*/ for (auto element : validTriggerCandidatesNavalOnly) { @@ -877,6 +950,14 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) case teamCategory::Air: weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightAirOnly) * 1.0; + /*Debug::Log("Weight Dice: %f\n", weightDice); + + // Debug + Debug::Log("DEBUG: Candidate AI triggers list:\n"); + for (TriggerElementWeight element : validTriggerCandidatesAirOnly) + { + Debug::Log("Weight: %f, [%s][%s]: %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Team1->Name); + }*/ for (auto element : validTriggerCandidatesAirOnly) { diff --git a/src/Ext/Team/Body.h b/src/Ext/Team/Body.h index 559e39b185..89c9957248 100644 --- a/src/Ext/Team/Body.h +++ b/src/Ext/Team/Body.h @@ -9,6 +9,7 @@ #include #include #include +#include #include enum teamCategory diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index 6a2fff6a98..e63078eb02 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -273,6 +273,9 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->DeployedPrimaryFireFLH.Read(exArtINI, pArtSection, "DeployedPrimaryFireFLH"); this->DeployedSecondaryFireFLH.Read(exArtINI, pArtSection, "DeployedSecondaryFireFLH"); + this->ConsideredNaval.Read(exINI, pSection, "ConsideredNaval"); + this->ConsideredVehicle.Read(exINI, pSection, "ConsideredVehicle"); + // Prerequisite.RequiredTheaters contains a list of theader names char* key = "Prerequisite.RequiredTheaters"; char* context = nullptr; @@ -493,6 +496,8 @@ void TechnoTypeExt::ExtData::Serialize(T& Stm) .Process(this->Prerequisite_Negative) .Process(this->Prerequisite_Lists) .Process(this->Prerequisite_ListVector) + .Process(this->ConsideredNaval) + .Process(this->ConsideredVehicle) ; } void TechnoTypeExt::ExtData::LoadFromStream(PhobosStreamReader& Stm) diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h index 3ebfb6bcf5..1951273b68 100644 --- a/src/Ext/TechnoType/Body.h +++ b/src/Ext/TechnoType/Body.h @@ -158,6 +158,9 @@ class TechnoTypeExt std::vector> DeployedWeaponBurstFLHs; std::vector> EliteDeployedWeaponBurstFLHs; + Nullable ConsideredNaval; + Nullable ConsideredVehicle; + // Ares 0.1 ValueableVector Prerequisite_RequiredTheaters; ValueableVector Prerequisite; @@ -277,6 +280,8 @@ class TechnoTypeExt , Prerequisite { } , Prerequisite_Negative { } , Prerequisite_Lists { 0 } + , ConsideredNaval { } + , ConsideredVehicle { } { } virtual ~ExtData() = default; From 9712713e27907f5911e080cb8af02a34aebba362 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Wed, 11 Jan 2023 16:32:16 +0100 Subject: [PATCH 11/59] Updated documentation --- docs/AI-Scripting-and-Mapping.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/AI-Scripting-and-Mapping.md b/docs/AI-Scripting-and-Mapping.md index 02cb910cc7..1a323788b7 100644 --- a/docs/AI-Scripting-and-Mapping.md +++ b/docs/AI-Scripting-and-Mapping.md @@ -26,6 +26,7 @@ RepairBaseNodes=no,no,no ; 3 booleans indicating whether AI repair basenodes in - 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 understand Ares tags like `Prerequisite.RequiredTheaters`, `Prerequisite.Negative`, `Prerequisite.Lists` & `Generic prerequisites` section. - If it finds a trigger with 5000 current probability weight then discard valid triggers all and start searching all valid triggers with weight 5000. AI will pick 1 randomly and decrease by 1 the current weight of the selected trigger (so if nothing happens in the next teams selection loop it won't appear in this special list). Under this scenario categories are disabled. +- Units can override the category using `ConsideredVehicle` and `ConsideredNaval` boolean tags In `rulesmd.ini`: ```ini @@ -45,6 +46,10 @@ NewTeamsSelector.UnclassifiedCategoryPercentage= ; floating point value, percen NewTeamsSelector.GroundCategoryPercentage= ; floating point value, percents or absolute NewTeamsSelector.AirCategoryPercentage ; floating point value, percents or absolute NewTeamsSelector.NavalCategoryPercentage ; floating point value, percents or absolute + +[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: From 08691d5aae3b56a9878019f21955cf7e1202da7d Mon Sep 17 00:00:00 2001 From: FS-21 Date: Tue, 7 Feb 2023 06:49:03 +0100 Subject: [PATCH 12/59] Secret Lab support for the AI and bug fixes Now AI will be able to unlock secret lab units and use them in Teams. Other bugfixes and improvements. --- docs/AI-Scripting-and-Mapping.md | 3 +- src/Ext/Building/Body.cpp | 123 +++++++++++++++++++++++++++++++ src/Ext/Building/Body.h | 4 + src/Ext/Building/Hooks.cpp | 13 ++++ src/Ext/BuildingType/Body.cpp | 19 +++++ src/Ext/BuildingType/Body.h | 5 ++ src/Ext/House/Body.cpp | 22 +++++- src/Ext/House/Body.h | 2 +- src/Ext/Team/Body.cpp | 32 ++++---- src/Ext/TechnoType/Body.cpp | 44 +++++++++-- src/Ext/TechnoType/Body.h | 6 ++ 11 files changed, 247 insertions(+), 26 deletions(-) diff --git a/docs/AI-Scripting-and-Mapping.md b/docs/AI-Scripting-and-Mapping.md index 1a323788b7..64063ee263 100644 --- a/docs/AI-Scripting-and-Mapping.md +++ b/docs/AI-Scripting-and-Mapping.md @@ -26,7 +26,8 @@ RepairBaseNodes=no,no,no ; 3 booleans indicating whether AI repair basenodes in - 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 understand Ares tags like `Prerequisite.RequiredTheaters`, `Prerequisite.Negative`, `Prerequisite.Lists` & `Generic prerequisites` section. - If it finds a trigger with 5000 current probability weight then discard valid triggers all and start searching all valid triggers with weight 5000. AI will pick 1 randomly and decrease by 1 the current weight of the selected trigger (so if nothing happens in the next teams selection loop it won't appear in this special list). Under this scenario categories are disabled. -- Units can override the category using `ConsideredVehicle` and `ConsideredNaval` boolean tags +- Units can override the category using `ConsideredVehicle` and `ConsideredNaval` boolean tags. +- AI is be able to use unlocked units in captured Secret Labs. In `rulesmd.ini`: ```ini diff --git a/src/Ext/Building/Body.cpp b/src/Ext/Building/Body.cpp index 835f771bf0..7f9cb682bc 100644 --- a/src/Ext/Building/Body.cpp +++ b/src/Ext/Building/Body.cpp @@ -326,6 +326,128 @@ bool BuildingExt::HandleInfiltrate(BuildingClass* pBuilding, HouseClass* pInfilt return true; } +// Assigns a secret production option to the AI building. +void BuildingExt::ExtData::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::ExtMap.Find(pType); + + // If Secret Lab already picked a techno and isn't allowed to recalculate it again the function ends + if (this->SecretLab_Placed && !pDataType->Secret_RecalcOnCapture) + return; + + DynamicVectorClass validCandidates; + DynamicVectorClass possibleCandidates; + + if (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) + { + DynamicVectorClass ownedBuildingTypes; + + if (ownedBuildingTypes.Count == 0) + { + for (auto building : pOwner->Buildings) + { + ownedBuildingTypes.AddUnique(building->Type); + } + } + + for (const auto& boon : possibleCandidates) + { + auto pExt = TechnoTypeExt::ExtMap.Find(boon); + bool isRequiredHouse = true; // Default value if Secret.RequiredHouses isn't declared + + if (pExt->Secret_RequiredHouses.size() > 0) + isRequiredHouse = false; + + 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, ownedBuildingTypes, 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); + } +} + // ============================= // load / save @@ -339,6 +461,7 @@ void BuildingExt::ExtData::Serialize(T& Stm) .Process(this->GrindingWeapon_LastFiredFrame) .Process(this->CurrentAirFactory) .Process(this->AccumulatedGrindingRefund) + .Process(this->SecretLab_Placed) ; } diff --git a/src/Ext/Building/Body.h b/src/Ext/Building/Body.h index 0424bbd2a9..a30068e775 100644 --- a/src/Ext/Building/Body.h +++ b/src/Ext/Building/Body.h @@ -12,6 +12,7 @@ #include #include #include +#include class BuildingExt { @@ -27,6 +28,7 @@ class BuildingExt int GrindingWeapon_LastFiredFrame; BuildingClass* CurrentAirFactory; int AccumulatedGrindingRefund; + bool SecretLab_Placed; ExtData(BuildingClass* OwnerObject) : Extension(OwnerObject) , TypeExtData { nullptr } @@ -35,11 +37,13 @@ class BuildingExt , GrindingWeapon_LastFiredFrame { 0 } , CurrentAirFactory { nullptr } , AccumulatedGrindingRefund { 0 } + , SecretLab_Placed { false } { } void DisplayGrinderRefund(); void ApplyPoweredKillSpawns(); bool HasSuperWeapon(int index, bool withUpgrades) const; + void UpdateSecretLabAI(); virtual ~ExtData() = default; diff --git a/src/Ext/Building/Hooks.cpp b/src/Ext/Building/Hooks.cpp index 153240cc80..aa55a3c3e9 100644 --- a/src/Ext/Building/Hooks.cpp +++ b/src/Ext/Building/Hooks.cpp @@ -301,3 +301,16 @@ DEFINE_HOOK(0x4575A2, BuildingClass_Infiltrate_AfterAres, 0xE) return 0; } #undef INFILTRATE_HOOK_MAGIC + +DEFINE_HOOK(0x445F80, BuildingClass_GrandOpening_UpdateSecretLabAI, 0x5) +{ + GET(BuildingClass*, pThis, ECX); + + if (pThis->Type->SecretLab && !pThis->Owner->IsControlledByHuman()) + { + auto pExt = BuildingExt::ExtMap.Find(pThis); + pExt->UpdateSecretLabAI(); + } + + return 0; +} diff --git a/src/Ext/BuildingType/Body.cpp b/src/Ext/BuildingType/Body.cpp index eb6648ed09..a002bb902f 100644 --- a/src/Ext/BuildingType/Body.cpp +++ b/src/Ext/BuildingType/Body.cpp @@ -142,6 +142,23 @@ void BuildingTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->Grinding_DisplayRefund_Houses.Read(exINI, pSection, "Grinding.DisplayRefund.Houses"); this->Grinding_DisplayRefund_Offset.Read(exINI, pSection, "Grinding.DisplayRefund.Offset"); + this->Secret_RecalcOnCapture.Read(exINI, pSection, "SecretLab.GenerateOnCapture"); + //this->PossibleBoons.Read(exINI, pSection, "SecretLab.PossibleBoons"); + + // Secret.Boons contains a list of TechnoTypeClass IDs + 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; + // Ares tag this->SpyEffect_Custom.Read(exINI, pSection, "SpyEffect.Custom"); if (SuperWeaponTypeClass::Array->Count > 0) @@ -220,6 +237,8 @@ void BuildingTypeExt::ExtData::Serialize(T& Stm) .Process(this->SpyEffect_Custom) .Process(this->SpyEffect_VictimSuperWeapon) .Process(this->SpyEffect_InfiltratorSuperWeapon) + .Process(this->Secret_RecalcOnCapture) + .Process(this->PossibleBoons) ; } diff --git a/src/Ext/BuildingType/Body.h b/src/Ext/BuildingType/Body.h index d9a6a48032..5d43a63229 100644 --- a/src/Ext/BuildingType/Body.h +++ b/src/Ext/BuildingType/Body.h @@ -51,6 +51,9 @@ class BuildingTypeExt NullableIdx SpyEffect_VictimSuperWeapon; NullableIdx SpyEffect_InfiltratorSuperWeapon; + Valueable Secret_RecalcOnCapture; + NullableVector PossibleBoons; + ExtData(BuildingTypeClass* OwnerObject) : Extension(OwnerObject) , PowersUp_Owner { AffectedHouse::Owner } , PowersUp_Buildings {} @@ -81,6 +84,8 @@ class BuildingTypeExt , SpyEffect_Custom { false } , SpyEffect_VictimSuperWeapon {} , SpyEffect_InfiltratorSuperWeapon {} + , Secret_RecalcOnCapture { false } + , PossibleBoons {} { } // Ares 0.A functions diff --git a/src/Ext/House/Body.cpp b/src/Ext/House/Body.cpp index 71a49380ed..e26aed0847 100644 --- a/src/Ext/House/Body.cpp +++ b/src/Ext/House/Body.cpp @@ -73,7 +73,7 @@ HouseClass* HouseExt::GetHouseKind(OwnerHouseKind const kind, bool const allowRa } } -bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const pItem, const DynamicVectorClass ownedBuildingTypes) +bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const pItem, const DynamicVectorClass ownedBuildingTypes, bool skipSecretLabChecks) { if (!pThis || !pItem) return false; @@ -86,6 +86,15 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const if (!pItemExt) 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; + + // Check if it appears in Owner=, RequiredHouses= and ForbiddenHouses= + // Note: if RequiredHouses = tag doesn't exist InRequiredHouses() always returns TRUE + if (!pThis->InOwners(pItem) || !pThis->InRequiredHouses(pItem) || pThis->InForbiddenHouses(pItem)) + return false; + // Prerequisite.RequiredTheaters check if (pItemExt->Prerequisite_RequiredTheaters.size() > 0) { @@ -121,9 +130,6 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const { for (int idx : pItemExt->Prerequisite_Negative) { - if (prerequisiteNegativeMet) - return false; - if (idx < 0) // Can be used generic prerequisites in this Ares tag? I have to investigate it but for now we support it... { // Default prerequisites like POWER, PROC, BARRACKS, FACTORY, ... @@ -140,9 +146,16 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const prerequisiteNegativeMet = true; } } + + if (prerequisiteNegativeMet) + return false; } } + // Main prerequisite checks are skipped if a new secret lab object is in process to be unlocked + if (skipSecretLabChecks) + return true; + DynamicVectorClass prerequisiteOverride = pItem->PrerequisiteOverride; bool prerequisiteMet = false; // All buildings must appear in the buildings list owner by the house @@ -261,6 +274,7 @@ bool HouseExt::HasGenericPrerequisite(int idx, const DynamicVectorClass selectedPrerequisite = RulesExt::Global()->GenericPrerequisites.GetItem(std::abs(idx)); + //auto selectedPrerequisiteName = RulesExt::Global()->GenericPrerequisitesNames.GetItem(std::abs(idx));// Only used for easy debug if (selectedPrerequisite.Count == 0) return false; diff --git a/src/Ext/House/Body.h b/src/Ext/House/Body.h index 9bf1ef4649..ce7d261784 100644 --- a/src/Ext/House/Body.h +++ b/src/Ext/House/Body.h @@ -74,7 +74,7 @@ class HouseExt static int ActiveHarvesterCount(HouseClass* pThis); static int TotalHarvesterCount(HouseClass* pThis); static HouseClass* GetHouseKind(OwnerHouseKind kind, bool allowRandom, HouseClass* pDefault, HouseClass* pInvoker = nullptr, HouseClass* pVictim = nullptr); - static bool PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const pItem, const DynamicVectorClass ownedBuildingTypes); + static bool PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const pItem, const DynamicVectorClass ownedBuildingTypes, bool skipSecretLabChecks); static bool HasGenericPrerequisite(int idx, const DynamicVectorClass ownedBuildingTypes); static int FindGenericPrerequisite(const char* id); }; diff --git a/src/Ext/Team/Body.cpp b/src/Ext/Team/Body.cpp index 76698abb15..be51dfbc17 100644 --- a/src/Ext/Team/Body.cpp +++ b/src/Ext/Team/Body.cpp @@ -185,33 +185,39 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (totalPercengates > 1.0 || totalPercengates <= 0.0) splitTriggersByCategory = false; - // Note: if the sum of all percentages is less than 100% then that empty space will work like "no categories" + if (splitTriggersByCategory) { int categoryDice = ScenarioClass::Instance->Random.RandomRanged(1, 100); + int unclassifiedValue = (int)(percentageUnclassifiedTriggers * 100.0); + int groundValue = (int)(percentageGroundTriggers * 100.0); + int airValue = (int)(percentageAirTriggers * 100.0); + int navalValue = (int)(percentageNavalTriggers * 100.0); - if (categoryDice == 0) - { - splitTriggersByCategory = false; - } - else if (categoryDice <= (int)(percentageUnclassifiedTriggers * 100.0)) + // Pick what type of team will be selected in this round + if (percentageUnclassifiedTriggers > 0.0 && categoryDice <= unclassifiedValue) { validCategory = teamCategory::Unclassified; + Debug::Log("New AI team category selection: dice %d <= %d (MIXED)\n", categoryDice, unclassifiedValue); } - else if (categoryDice <= (int)((percentageUnclassifiedTriggers + percentageGroundTriggers) * 100.0)) + else if (percentageGroundTriggers > 0.0 && categoryDice <= (unclassifiedValue + groundValue)) { validCategory = teamCategory::Ground; + Debug::Log("New AI team category selection: dice %d <= %d (mixed: %d%% + GROUND: %d%%)\n", categoryDice, (unclassifiedValue + groundValue), unclassifiedValue, groundValue); } - else if (categoryDice <= (int)((percentageUnclassifiedTriggers + percentageGroundTriggers + percentageNavalTriggers) * 100.0)) + else if (percentageAirTriggers > 0.0 && categoryDice <= (unclassifiedValue + groundValue + airValue)) { - validCategory = teamCategory::Naval; + validCategory = teamCategory::Air; + Debug::Log("New AI team category selection: dice %d <= %d (mixed: %d%% + ground: %d%% + AIR: %d%%)\n", categoryDice, (unclassifiedValue + groundValue + airValue), unclassifiedValue, groundValue, airValue); } - else if (categoryDice <= (int)((percentageUnclassifiedTriggers + percentageGroundTriggers + percentageNavalTriggers + percentageAirTriggers) * 100.0)) + else if (percentageNavalTriggers > 0.0 && categoryDice <= (unclassifiedValue + groundValue + airValue + navalValue)) { - validCategory = teamCategory::Air; + validCategory = teamCategory::Naval; + Debug::Log("New AI team category selection: dice %d <= %d (mixed: %d%% + ground: %d%% + air: %d%% + NAVAL: %d%%)\n", categoryDice, (unclassifiedValue + groundValue + airValue + navalValue), unclassifiedValue, groundValue, airValue, navalValue); } else { + // If the sum of all percentages is less than 100% then that empty space will work like "no categories" splitTriggersByCategory = false; } } @@ -657,7 +663,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) continue; TechnoTypeClass* object = entry.Type; - bool canBeBuilt = HouseExt::PrerequisitesMet(pHouse, object, ownedBuildingTypes); + bool canBeBuilt = HouseExt::PrerequisitesMet(pHouse, object, ownedBuildingTypes, false); if (!canBeBuilt) { @@ -845,7 +851,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (!isFallbackEnabled) return SkipCode; - Debug::Log("... BUT fallback mode is enabled so now will be checked all available triggers.\n"); + Debug::Log("... but fallback mode is enabled so now will be checked all available triggers.\n"); validCategory = teamCategory::None; } diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index e63078eb02..40b2797fb8 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -275,17 +275,44 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->ConsideredNaval.Read(exINI, pSection, "ConsideredNaval"); this->ConsideredVehicle.Read(exINI, pSection, "ConsideredVehicle"); + this->ConsideredSecretLabTech.Read(exINI, pSection, "ConsideredSecretLabTech"); - // Prerequisite.RequiredTheaters contains a list of theader names - char* key = "Prerequisite.RequiredTheaters"; + // Secret.RequiredHouses contains a list of HouseTypeClass indexes + char* key = "SecretLab.RequiredHouses"; 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)) + { + std::string item(cur); + this->Secret_RequiredHouses.push_back(item); + } + + key = nullptr; + + // Secret.ForbiddenHouses contains a list of HouseTypeClass indexes + key = "SecretLab.ForbiddenHouses"; + 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)) + { + std::string item(cur); + this->Secret_ForbiddenHouses.push_back(item); + } + + key = nullptr; + + // Prerequisite.RequiredTheaters contains a list of theader names + key = "Prerequisite.RequiredTheaters"; + 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 = Theater::FindIndex(cur); if (index != -1) - Prerequisite_RequiredTheaters.push_back(index); + this->Prerequisite_RequiredTheaters.push_back(index); } key = nullptr; @@ -301,13 +328,13 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) int idx = TechnoTypeClass::FindIndex(cur); if (idx >= 0) { - Prerequisite.push_back(idx); + this->Prerequisite.push_back(idx); } else { int index = HouseExt::FindGenericPrerequisite(cur); if (index < 0) - Prerequisite.push_back(index); + this->Prerequisite.push_back(index); } } @@ -323,13 +350,13 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) int idx = TechnoTypeClass::FindIndex(cur); if (idx >= 0) { - Prerequisite_Negative.push_back(idx); + this->Prerequisite_Negative.push_back(idx); } else { int index = HouseExt::FindGenericPrerequisite(cur); if (index < 0) - Prerequisite_Negative.push_back(index); + this->Prerequisite_Negative.push_back(index); } } @@ -498,6 +525,9 @@ void TechnoTypeExt::ExtData::Serialize(T& Stm) .Process(this->Prerequisite_ListVector) .Process(this->ConsideredNaval) .Process(this->ConsideredVehicle) + .Process(this->ConsideredSecretLabTech) + .Process(this->Secret_RequiredHouses) + .Process(this->Secret_ForbiddenHouses) ; } void TechnoTypeExt::ExtData::LoadFromStream(PhobosStreamReader& Stm) diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h index 1951273b68..61fe193658 100644 --- a/src/Ext/TechnoType/Body.h +++ b/src/Ext/TechnoType/Body.h @@ -160,6 +160,9 @@ class TechnoTypeExt Nullable ConsideredNaval; Nullable ConsideredVehicle; + Valueable ConsideredSecretLabTech; + std::vector Secret_RequiredHouses; + std::vector Secret_ForbiddenHouses; // Ares 0.1 ValueableVector Prerequisite_RequiredTheaters; @@ -282,6 +285,9 @@ class TechnoTypeExt , Prerequisite_Lists { 0 } , ConsideredNaval { } , ConsideredVehicle { } + , ConsideredSecretLabTech { false } + , Secret_RequiredHouses { } + , Secret_ForbiddenHouses { } { } virtual ~ExtData() = default; From c2877737790d5d97c6d57513ea9b7466de22b5a7 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Wed, 8 Feb 2023 18:25:49 +0100 Subject: [PATCH 13/59] Point to the latest YRpp that can compile... --- YRpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/YRpp b/YRpp index a41eb1a449..9db1fd96f1 160000 --- a/YRpp +++ b/YRpp @@ -1 +1 @@ -Subproject commit a41eb1a4491f8256459a1c3e4a25af5ebded45db +Subproject commit 9db1fd96f1bf709c40c9cd304b276ed7578998ca From 57a948750d28e7960ea21c4dbe97171dcf523448 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Fri, 10 Feb 2023 19:35:59 +0100 Subject: [PATCH 14/59] Fix compilation after the develop merge --- src/Ext/Building/Body.cpp | 1 + src/Ext/Building/Body.h | 1 - src/Ext/BuildingType/Body.cpp | 2 +- src/Ext/Rules/Body.h | 2 +- src/Ext/Team/Body.cpp | 70 +++++++++++++++++------------------ src/Ext/Team/Body.h | 12 +++--- src/Ext/TechnoType/Body.cpp | 2 +- 7 files changed, 45 insertions(+), 45 deletions(-) diff --git a/src/Ext/Building/Body.cpp b/src/Ext/Building/Body.cpp index 8afd960452..8511964ccb 100644 --- a/src/Ext/Building/Body.cpp +++ b/src/Ext/Building/Body.cpp @@ -4,6 +4,7 @@ #include #include +#include template<> const DWORD Extension::Canary = 0x87654321; BuildingExt::ExtContainer BuildingExt::ExtMap; diff --git a/src/Ext/Building/Body.h b/src/Ext/Building/Body.h index 35788d8201..d5401f5141 100644 --- a/src/Ext/Building/Body.h +++ b/src/Ext/Building/Body.h @@ -12,7 +12,6 @@ #include #include #include -#include class BuildingExt { diff --git a/src/Ext/BuildingType/Body.cpp b/src/Ext/BuildingType/Body.cpp index 350c8e4749..fb152f8837 100644 --- a/src/Ext/BuildingType/Body.cpp +++ b/src/Ext/BuildingType/Body.cpp @@ -147,7 +147,7 @@ void BuildingTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) //this->PossibleBoons.Read(exINI, pSection, "SecretLab.PossibleBoons"); // Secret.Boons contains a list of TechnoTypeClass IDs - char* key = "SecretLab.PossibleBoons"; + const char* key = "SecretLab.PossibleBoons"; char* context = nullptr; pINI->ReadString(pSection, key, "", Phobos::readBuffer); diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h index b69df70c77..df467331a5 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -75,7 +75,7 @@ class RulesExt Valueable RadialIndicatorVisibility; DynamicVectorClass> GenericPrerequisites; - DynamicVectorClass GenericPrerequisitesNames; + DynamicVectorClass GenericPrerequisitesNames; Valueable NewTeamsSelector; Valueable NewTeamsSelector_SplitTriggersByCategory; diff --git a/src/Ext/Team/Body.cpp b/src/Ext/Team/Body.cpp index be51dfbc17..d165d0ce03 100644 --- a/src/Ext/Team/Body.cpp +++ b/src/Ext/Team/Body.cpp @@ -382,8 +382,8 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (!pTrigger->ConditionObject) continue; - DynamicVectorClass list; - list.AddItem(pTrigger->ConditionObject); + std::vector list; + list.push_back(pTrigger->ConditionObject); bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, targetHouse, true, list); if (!isConditionMet) @@ -395,8 +395,8 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (!pTrigger->ConditionObject) continue; - DynamicVectorClass list; - list.AddItem(pTrigger->ConditionObject); + std::vector list; + list.push_back(pTrigger->ConditionObject); bool isConditionMet = TeamExt::HouseOwns(pTrigger, pHouse, false, list); if (!isConditionMet) @@ -408,8 +408,8 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (!pTrigger->ConditionObject) continue; - DynamicVectorClass list; - list.AddItem(pTrigger->ConditionObject); + std::vector list; + list.push_back(pTrigger->ConditionObject); bool isConditionMet = TeamExt::NeutralOwns(pTrigger, list); if (!isConditionMet) @@ -421,8 +421,8 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (!pTrigger->ConditionObject) continue; - DynamicVectorClass list; - list.AddItem(pTrigger->ConditionObject); + std::vector list; + list.push_back(pTrigger->ConditionObject); bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, nullptr, false, list); if (!isConditionMet) @@ -430,11 +430,11 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 9) { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) + if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) { // New case 9: Like in case 0 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the enemy. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, targetHouse, false, list); if (!isConditionMet) @@ -443,11 +443,11 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 10) { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) + if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) { // New case 10: Like in case 1 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the house. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::HouseOwns(pTrigger, pHouse, false, list); if (!isConditionMet) @@ -456,11 +456,11 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 11) { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) + if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) { // New case 11: Like in case 7 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the Civilians. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::NeutralOwns(pTrigger, list); if (!isConditionMet) @@ -469,11 +469,11 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 12) { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) + if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) { // New case 12: Like in case 0 & 9 but instead of a specific enemy this checks in all enemies. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, nullptr, false, list); if (!isConditionMet) @@ -482,11 +482,11 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 13) { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) + if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) { // New case 13: Like in case 1 & 10 but instead checking the house now checks the allies. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::HouseOwns(pTrigger, pHouse, true, list); if (!isConditionMet) @@ -495,11 +495,11 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 14) { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) + if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) { // New case 14: Like in case 9 but instead of meet any comparison now is required all. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::EnemyOwnsAll(pTrigger, pHouse, targetHouse, list); if (!isConditionMet) @@ -508,11 +508,11 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 15) { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) + if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) { // New case 15: Like in case 10 but instead of meet any comparison now is required all. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::HouseOwnsAll(pTrigger, pHouse, list); if (!isConditionMet) @@ -521,11 +521,11 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 16) { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) + if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) { // New case 16: Like in case 11 but instead of meet any comparison now is required all. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::NeutralOwnsAll(pTrigger, list); if (!isConditionMet) @@ -534,11 +534,11 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 17) { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) + if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) { // New case 17: Like in case 14 but instead of meet any comparison now is required all. Check all enemies // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::EnemyOwnsAll(pTrigger, pHouse, nullptr, list); if (!isConditionMet) @@ -1039,7 +1039,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) return SkipCode; } -bool TeamExt::HouseOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, bool allies, DynamicVectorClass list) +bool TeamExt::HouseOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, bool allies, std::vector list) { bool result = false; int counter = 0; @@ -1088,7 +1088,7 @@ bool TeamExt::HouseOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, bool alli return result; } -bool TeamExt::EnemyOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, bool onlySelectedEnemy, DynamicVectorClass list) +bool TeamExt::EnemyOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, bool onlySelectedEnemy, std::vector list) { bool result = false; int counter = 0; @@ -1141,7 +1141,7 @@ bool TeamExt::EnemyOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClas return result; } -bool TeamExt::NeutralOwns(AITriggerTypeClass* pThis, DynamicVectorClass list) +bool TeamExt::NeutralOwns(AITriggerTypeClass* pThis, std::vector list) { bool result = false; int counter = 0; @@ -1195,11 +1195,11 @@ bool TeamExt::NeutralOwns(AITriggerTypeClass* pThis, DynamicVectorClass list) +bool TeamExt::HouseOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, std::vector list) { bool result = true; - if (list.Count == 0) + if (list.size() == 0) return false; // Count all objects of the list, like an AND operator @@ -1251,14 +1251,14 @@ bool TeamExt::HouseOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, Dynami return result; } -bool TeamExt::EnemyOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, DynamicVectorClass list) +bool TeamExt::EnemyOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, std::vector list) { bool result = true; if (pEnemy && pHouse->IsAlliedWith(pEnemy)) pEnemy = nullptr; - if (list.Count == 0) + if (list.size() == 0) return false; // Count all objects of the list, like an AND operator @@ -1312,11 +1312,11 @@ bool TeamExt::EnemyOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseC return result; } -bool TeamExt::NeutralOwnsAll(AITriggerTypeClass* pThis, DynamicVectorClass list) +bool TeamExt::NeutralOwnsAll(AITriggerTypeClass* pThis, std::vector list) { bool result = true; - if (list.Count == 0) + if (list.size() == 0) return false; // Any neutral house should be capable to meet the prerequisites diff --git a/src/Ext/Team/Body.h b/src/Ext/Team/Body.h index 89c9957248..f6d06a12ff 100644 --- a/src/Ext/Team/Body.h +++ b/src/Ext/Team/Body.h @@ -144,11 +144,11 @@ class TeamExt static ExtContainer ExtMap; - static bool HouseOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, bool allies, DynamicVectorClass list); - static bool HouseOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, DynamicVectorClass list); - static bool EnemyOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, bool onlySelectedEnemy, DynamicVectorClass list); - static bool EnemyOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, DynamicVectorClass list); - static bool NeutralOwns(AITriggerTypeClass* pThis, DynamicVectorClass list); - static bool NeutralOwnsAll(AITriggerTypeClass* pThis, DynamicVectorClass list); + static bool HouseOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, bool allies, std::vector list); + static bool HouseOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, std::vector list); + static bool EnemyOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, bool onlySelectedEnemy, std::vector list); + static bool EnemyOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, std::vector list); + static bool NeutralOwns(AITriggerTypeClass* pThis, std::vector list); + static bool NeutralOwnsAll(AITriggerTypeClass* pThis, std::vector list); }; diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index 0b65900d22..e9bf0632d0 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -285,7 +285,7 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->ConsideredSecretLabTech.Read(exINI, pSection, "ConsideredSecretLabTech"); // Secret.RequiredHouses contains a list of HouseTypeClass indexes - char* key = "SecretLab.RequiredHouses"; + const char* key = "SecretLab.RequiredHouses"; char* context = nullptr; pINI->ReadString(pSection, key, "", Phobos::readBuffer); From 035abe83556806209c09954be7cca0d7c6402b27 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 18 Feb 2023 11:53:00 +0100 Subject: [PATCH 15/59] crash fix --- src/Ext/Team/Body.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Ext/Team/Body.cpp b/src/Ext/Team/Body.cpp index d165d0ce03..ec09d79692 100644 --- a/src/Ext/Team/Body.cpp +++ b/src/Ext/Team/Body.cpp @@ -263,7 +263,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) for (auto entry : team->Type->TaskForce->Entries) { - if (entry.Amount > 0) + if (entry.Type && entry.Amount > 0) { if (entry.Type) Debug::Log("\t[%s]: %d / %d\n", entry.Type->ID, team->CountObjects[i], entry.Amount); @@ -594,6 +594,9 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (entry.Amount > 0) { + if (!entry.Type) + continue; + if (entry.Type->WhatAmI() == AbstractType::AircraftType || entry.Type->ConsideredAircraft) { @@ -689,7 +692,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) for (auto entry : pTriggerTeam1Type->TaskForce->Entries) { // Check if each unit in the taskforce has the available recruitable units in the map - if (allObjectsCanBeBuiltOrRecruited && entry.Amount > 0) + if (allObjectsCanBeBuiltOrRecruited && entry.Type && entry.Amount > 0) { bool canBeRecruited = false; From 9cc658d5e224d4dd495434e4ec7a2cda92e122d7 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Fri, 26 May 2023 19:09:10 +0200 Subject: [PATCH 16/59] Code improvements and additions Trigger conditions 18 and 19 for checking Bridges status --- Phobos.vcxproj | 1 + docs/AI-Scripting-and-Mapping.md | 2 + src/Ext/Building/Body.cpp | 11 +- src/Ext/House/Body.cpp | 72 ++++------ src/Ext/House/Body.h | 6 +- src/Ext/Scenario/Hooks.cpp | 36 +++++ src/Ext/Team/Body.cpp | 226 +++++++++++++++++-------------- src/Ext/Team/Body.h | 13 +- 8 files changed, 200 insertions(+), 167 deletions(-) create mode 100644 src/Ext/Scenario/Hooks.cpp diff --git a/Phobos.vcxproj b/Phobos.vcxproj index 62e4b8ca77..b54dbe9ded 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -33,6 +33,7 @@ + diff --git a/docs/AI-Scripting-and-Mapping.md b/docs/AI-Scripting-and-Mapping.md index 0cc30fd74c..d516cacff3 100644 --- a/docs/AI-Scripting-and-Mapping.md +++ b/docs/AI-Scripting-and-Mapping.md @@ -71,6 +71,8 @@ ConsideredVehicle= ; boolean | 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` diff --git a/src/Ext/Building/Body.cpp b/src/Ext/Building/Body.cpp index 25245adaff..462e94fc32 100644 --- a/src/Ext/Building/Body.cpp +++ b/src/Ext/Building/Body.cpp @@ -379,14 +379,11 @@ void BuildingExt::ExtData::UpdateSecretLabAI() if (possibleCandidates.Count > 0) { - DynamicVectorClass ownedBuildingTypes; + std::map ownedBuildings; - if (ownedBuildingTypes.Count == 0) + for (auto building : pOwner->Buildings) { - for (auto building : pOwner->Buildings) - { - ownedBuildingTypes.AddUnique(building->Type); - } + ++ownedBuildings[building->Type]; } for (const auto& boon : possibleCandidates) @@ -405,7 +402,7 @@ void BuildingExt::ExtData::UpdateSecretLabAI() if (pOwner->Type->ArrayIndex == houseIdx) { - bool canBeBuilt = HouseExt::PrerequisitesMet(pOwner, boon, ownedBuildingTypes, true); + bool canBeBuilt = HouseExt::PrerequisitesMet(pOwner, boon, ownedBuildings, true); if (canBeBuilt) { diff --git a/src/Ext/House/Body.cpp b/src/Ext/House/Body.cpp index 27d2dbde71..1c5dfb6f5b 100644 --- a/src/Ext/House/Body.cpp +++ b/src/Ext/House/Body.cpp @@ -74,7 +74,7 @@ HouseClass* HouseExt::GetHouseKind(OwnerHouseKind const kind, bool const allowRa } } -bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const pItem, const DynamicVectorClass ownedBuildingTypes, bool skipSecretLabChecks) +bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const pItem, std::map ownedBuildings, bool skipSecretLabChecks) { if (!pThis || !pItem) return false; @@ -115,13 +115,17 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const { if (pTechno->Owner == pThis && pTechno->GetTechnoType() == pItem - && pTechno->IsAlive && pTechno->Health > 0) + && pTechno->IsAlive + && pTechno->Health > 0) { nInstances++; + + if (nInstances >= pItem->BuildLimit) + return false; } } - if (nInstances >= pItem->BuildLimit) + if (pItem->BuildLimit < 1) return false; bool prerequisiteNegativeMet = false; // Only one coincidence is needed @@ -134,18 +138,12 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const if (idx < 0) // Can be used generic prerequisites in this Ares tag? I have to investigate it but for now we support it... { // Default prerequisites like POWER, PROC, BARRACKS, FACTORY, ... - prerequisiteNegativeMet = HouseExt::HasGenericPrerequisite(idx, ownedBuildingTypes); + prerequisiteNegativeMet = HouseExt::HasGenericPrerequisite(idx, ownedBuildings); } else { - for (auto pObject : ownedBuildingTypes) - { - if (prerequisiteNegativeMet) - break; - - if (idx == pObject->ArrayIndex) - prerequisiteNegativeMet = true; - } + if (ownedBuildings[TechnoTypeClass::Array->GetItem(idx)] > 0) + prerequisiteNegativeMet = true; } if (prerequisiteNegativeMet) @@ -172,18 +170,12 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const if (idx < 0) { // Default prerequisites like POWER, PROC, BARRACKS, FACTORY, ... - prerequisiteOverrideMet = HouseExt::HasGenericPrerequisite(idx, ownedBuildingTypes); + prerequisiteOverrideMet = HouseExt::HasGenericPrerequisite(idx, ownedBuildings); } else { - for (auto pObject : ownedBuildingTypes) - { - if (prerequisiteOverrideMet) - break; - - if (idx == pObject->ArrayIndex) - prerequisiteOverrideMet = true; - } + if (ownedBuildings[TechnoTypeClass::Array->GetItem(idx)] > 0) + prerequisiteOverrideMet = true; } } } @@ -199,18 +191,12 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const if (idx < 0) { // Default prerequisites like POWER, PROC, BARRACKS, FACTORY, ... - found = HouseExt::HasGenericPrerequisite(idx, ownedBuildingTypes); + found = HouseExt::HasGenericPrerequisite(idx, ownedBuildings); } else { - for (auto pObject : ownedBuildingTypes) - { - if (found) - break; - - if (idx == pObject->ArrayIndex) - found = true; - } + if (ownedBuildings[TechnoTypeClass::Array->GetItem(idx)] > 0) + found = true; } if (!found) @@ -242,20 +228,14 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const if (idx < 0) { // Default prerequisites like POWER, PROC, BARRACKS, FACTORY, ... - found = HouseExt::HasGenericPrerequisite(idx, ownedBuildingTypes); + found = HouseExt::HasGenericPrerequisite(idx, ownedBuildings); } else { found = false; - for (auto pObject : ownedBuildingTypes) - { - if (idx == pObject->ArrayIndex) - found = true; - - if (found) - break; - } + if (ownedBuildings[TechnoTypeClass::Array->GetItem(idx)] > 0) + found = true; } if (!found) @@ -269,7 +249,8 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const return prerequisiteMet || prerequisiteListsMet || prerequisiteOverrideMet; } -bool HouseExt::HasGenericPrerequisite(int idx, const DynamicVectorClass ownedBuildingTypes) +bool HouseExt::HasGenericPrerequisite(int idx, std::map ownedBuildings) + { if (idx >= 0) return false; @@ -287,14 +268,8 @@ bool HouseExt::HasGenericPrerequisite(int idx, const DynamicVectorClassArrayIndex) - found = true; - } + if (ownedBuildings[TechnoTypeClass::Array->GetItem(idxItem)] > 0) + found = true; } return found; @@ -369,6 +344,7 @@ void HouseExt::ExtData::Serialize(T& Stm) .Process(this->Factory_NavyType) .Process(this->Factory_AircraftType) .Process(this->RepairBaseNodes) + .Process(this->AITriggers_ValidList) ; } diff --git a/src/Ext/House/Body.h b/src/Ext/House/Body.h index 915a2d29bc..3aa3c51509 100644 --- a/src/Ext/House/Body.h +++ b/src/Ext/House/Body.h @@ -26,6 +26,8 @@ class HouseExt BuildingClass* Factory_NavyType; BuildingClass* Factory_AircraftType; + std::vector AITriggers_ValidList; + //Read from INI bool RepairBaseNodes[3]; @@ -78,7 +80,7 @@ class HouseExt static int ActiveHarvesterCount(HouseClass* pThis); static int TotalHarvesterCount(HouseClass* pThis); static HouseClass* GetHouseKind(OwnerHouseKind kind, bool allowRandom, HouseClass* pDefault, HouseClass* pInvoker = nullptr, HouseClass* pVictim = nullptr); - static bool PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const pItem, const DynamicVectorClass ownedBuildingTypes, bool skipSecretLabChecks); - static bool HasGenericPrerequisite(int idx, const DynamicVectorClass ownedBuildingTypes); + static bool PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const pItem, const std::map ownedBuildings, bool skipSecretLabChecks); + static bool HasGenericPrerequisite(int idx, std::map ownedBuildings); static int FindGenericPrerequisite(const char* id); }; diff --git a/src/Ext/Scenario/Hooks.cpp b/src/Ext/Scenario/Hooks.cpp new file mode 100644 index 0000000000..f2bfc4a021 --- /dev/null +++ b/src/Ext/Scenario/Hooks.cpp @@ -0,0 +1,36 @@ +#include +#include + +#include "Body.h" + +#include + +DEFINE_HOOK(0x687C9B, ReadScenarioINI_AITeamSelector_PreloadValidTriggers, 0x7) +{ + // For each house save a list with only AI Triggers that can be used + for (HouseClass* pHouse : *HouseClass::Array) + { + auto pHouseExt = HouseExt::ExtMap.Find(pHouse); + pHouseExt->AITriggers_ValidList.clear(); + int houseIdx = pHouse->ArrayIndex; + int sideIdx = pHouse->SideIndex + 1; + + for (int i = 0; i < AITriggerTypeClass::Array->Count; i++) + { + auto pTrigger = AITriggerTypeClass::Array->GetItem(i); + if (!pTrigger) + continue; + + int triggerHouse = pTrigger->HouseIndex; + int triggerSide = pTrigger->SideIndex; + + // The trigger must be compatible with the owner + if ((triggerHouse == -1 || houseIdx == triggerHouse) && (triggerSide == 0 || sideIdx == triggerSide)) + pHouseExt->AITriggers_ValidList.push_back(i); + } + + Debug::Log("House %d [%s] could use %d AI triggers in this map.\n", pHouse->ArrayIndex, pHouse->Type->ID, pHouseExt->AITriggers_ValidList.size()); + } + + return 0; +} diff --git a/src/Ext/Team/Body.cpp b/src/Ext/Team/Body.cpp index ec09d79692..0ce99d3969 100644 --- a/src/Ext/Team/Body.cpp +++ b/src/Ext/Team/Body.cpp @@ -242,7 +242,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) { totalActiveTeams++; int teamHouseIdx = pRunningTeam->Owner->ArrayIndex; - //auto teamHouse = pRunningTeam->GetOwningHouse(); + if (teamHouseIdx != houseIdx) continue; @@ -254,11 +254,11 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) activeTeams = activeTeamsList.Count; - /*Debug::Log("=====================\n[%s] ACTIVE TEAMS: %d / %d\n", pHouse->Type->ID, activeTeams, maxTeamsLimit); + Debug::Log("=====================\n[%s] ACTIVE TEAMS: %d / %d\n", pHouse->Type->ID, activeTeams, maxTeamsLimit); for (auto team : activeTeamsList) { Debug::Log("[%s](%d) : %s\n", team->Type->ID, team->TotalObjects, team->Type->Name); - Debug::Log(" IsMoving: %d, IsFullStrength: %d, IsUnderStrength: %d\n", team->IsMoving, team->IsFullStrength, team->IsUnderStrength); + Debug::Log(" IsMoving: %d, IsFullStrength: %d, IsUnderStrength: %d\n", team->IsMoving, team->IsFullStrength, team->IsUnderStrength); int i = 0; for (auto entry : team->Type->TaskForce->Entries) @@ -272,7 +272,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) i++; } } - Debug::Log("=====================\n");*/ + Debug::Log("=====================\n"); // We will use these values for discarding triggers bool hasReachedMaxTeamsLimit = activeTeams < maxTeamsLimit ? false : true; @@ -287,38 +287,37 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) return SkipCode; } - // Obtain the real list of structures the house have - DynamicVectorClass ownedBuildingTypes; - for (auto building : pHouse->Buildings) - { - ownedBuildingTypes.AddUnique(building->Type); - } + int destroyedBridgesCount = 0; + int undamagedBridgesCount = 0; + std::map ownedRecruitables; + std::map ownedBuildings; - struct recruitableUnit + for (auto pTechno : *TechnoClass::Array) { - TechnoTypeClass* object = nullptr; - int count = 1; - - bool operator==(const TechnoTypeClass* other) const - { - return (object == other); - } - - bool operator==(const recruitableUnit other) const + if (pTechno->WhatAmI() == AbstractType::Building) { - return (object == other.object); - } - }; + if (pTechno->Owner == pHouse) + { + ++ownedBuildings[pTechno->GetTechnoType()]; + } + else + { + auto pBuilding = static_cast(pTechno); + if (pBuilding && pBuilding->Type->BridgeRepairHut) + { + CellStruct cell = pTechno->GetCell()->MapCoords; - // Build a list of recruitable units by the house - DynamicVectorClass recruitableUnits; + if (MapClass::Instance->IsLinkedBridgeDestroyed(cell)) + destroyedBridgesCount++; + else + undamagedBridgesCount++; + } + } - for (auto pTechno : *TechnoClass::Array) - { - if (pTechno->WhatAmI() == AbstractType::Building) continue; + } - FootClass* pFoot = static_cast(pTechno); + auto* pFoot = static_cast(pTechno); if (!pFoot || !pTechno->IsAlive @@ -331,25 +330,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) continue; } - auto pTechnoType = pTechno->GetTechnoType(); - bool found = false; - - for (int i = 0; i < recruitableUnits.Count; i++) - { - if (recruitableUnits[i].object == pTechnoType) - { - recruitableUnits[i].count++; - found = true; - break; - } - } - - if (!found) - { - recruitableUnit newRecruitable; - newRecruitable.object = pTechnoType; - recruitableUnits.AddItem(newRecruitable); - } + ++ownedRecruitables[pTechno->GetTechnoType()]; } HouseClass* targetHouse = nullptr; @@ -382,8 +363,8 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (!pTrigger->ConditionObject) continue; - std::vector list; - list.push_back(pTrigger->ConditionObject); + DynamicVectorClass list; + list.AddItem(pTrigger->ConditionObject); bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, targetHouse, true, list); if (!isConditionMet) @@ -395,8 +376,8 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (!pTrigger->ConditionObject) continue; - std::vector list; - list.push_back(pTrigger->ConditionObject); + DynamicVectorClass list; + list.AddItem(pTrigger->ConditionObject); bool isConditionMet = TeamExt::HouseOwns(pTrigger, pHouse, false, list); if (!isConditionMet) @@ -408,8 +389,8 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (!pTrigger->ConditionObject) continue; - std::vector list; - list.push_back(pTrigger->ConditionObject); + DynamicVectorClass list; + list.AddItem(pTrigger->ConditionObject); bool isConditionMet = TeamExt::NeutralOwns(pTrigger, list); if (!isConditionMet) @@ -421,8 +402,8 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (!pTrigger->ConditionObject) continue; - std::vector list; - list.push_back(pTrigger->ConditionObject); + DynamicVectorClass list; + list.AddItem(pTrigger->ConditionObject); bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, nullptr, false, list); if (!isConditionMet) @@ -430,11 +411,11 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 9) { - if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) { // New case 9: Like in case 0 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the enemy. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); + DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, targetHouse, false, list); if (!isConditionMet) @@ -443,11 +424,11 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 10) { - if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) { // New case 10: Like in case 1 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the house. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); + DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::HouseOwns(pTrigger, pHouse, false, list); if (!isConditionMet) @@ -456,11 +437,11 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 11) { - if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) { // New case 11: Like in case 7 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the Civilians. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); + DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::NeutralOwns(pTrigger, list); if (!isConditionMet) @@ -469,11 +450,11 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 12) { - if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) { // New case 12: Like in case 0 & 9 but instead of a specific enemy this checks in all enemies. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); + DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, nullptr, false, list); if (!isConditionMet) @@ -482,11 +463,11 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 13) { - if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) { // New case 13: Like in case 1 & 10 but instead checking the house now checks the allies. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); + DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::HouseOwns(pTrigger, pHouse, true, list); if (!isConditionMet) @@ -495,11 +476,11 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 14) { - if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) { // New case 14: Like in case 9 but instead of meet any comparison now is required all. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); + DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::EnemyOwnsAll(pTrigger, pHouse, targetHouse, list); if (!isConditionMet) @@ -508,11 +489,11 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 15) { - if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) { // New case 15: Like in case 10 but instead of meet any comparison now is required all. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); + DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::HouseOwnsAll(pTrigger, pHouse, list); if (!isConditionMet) @@ -521,11 +502,11 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 16) { - if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) { // New case 16: Like in case 11 but instead of meet any comparison now is required all. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); + DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::NeutralOwnsAll(pTrigger, list); if (!isConditionMet) @@ -534,17 +515,33 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 17) { - if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) { // New case 17: Like in case 14 but instead of meet any comparison now is required all. Check all enemies // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); + DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::EnemyOwnsAll(pTrigger, pHouse, nullptr, list); if (!isConditionMet) continue; } } + else if ((int)pTrigger->ConditionType == 18) + { + // New case 18: Check destroyed bridges + bool isConditionMet = TeamExt::CountConditionMet(pTrigger, destroyedBridgesCount); + + if (!isConditionMet) + continue; + } + else if ((int)pTrigger->ConditionType == 19) + { + // New case 19: Check undamaged bridges + bool isConditionMet = TeamExt::CountConditionMet(pTrigger, undamagedBridgesCount); + + if (!isConditionMet) + continue; + } else { // Other cases from vanilla game @@ -666,7 +663,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) continue; TechnoTypeClass* object = entry.Type; - bool canBeBuilt = HouseExt::PrerequisitesMet(pHouse, object, ownedBuildingTypes, false); + bool canBeBuilt = HouseExt::PrerequisitesMet(pHouse, object, ownedBuildings, false); if (!canBeBuilt) { @@ -694,20 +691,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) // Check if each unit in the taskforce has the available recruitable units in the map if (allObjectsCanBeBuiltOrRecruited && entry.Type && entry.Amount > 0) { - bool canBeRecruited = false; - - for (auto item : recruitableUnits) - { - if (item.object == entry.Type) - { - if (item.count >= entry.Amount) - canBeRecruited = true; - - break; - } - } - - if (!canBeRecruited) + if (ownedRecruitables[entry.Type] < entry.Amount) { allObjectsCanBeBuiltOrRecruited = false; break; @@ -986,7 +970,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (!selectedTrigger) { - Debug::Log("DEBUG: House [%s] (idx: %d) failed to select Trigger. A new attempt Will be done later...\n", pHouse->Type->ID, pHouse->ArrayIndex); + Debug::Log("AI Team Selector: House [%s] (idx: %d) failed to select Trigger. A new attempt Will be done later...\n", pHouse->Type->ID, pHouse->ArrayIndex); return SkipCode; } @@ -998,7 +982,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } // We have a winner trigger here - Debug::Log("DEBUG: House [%s] (idx: %d) selected trigger [%s].\n", pHouse->Type->ID, pHouse->ArrayIndex, selectedTrigger->ID); + Debug::Log("AI Team Selector: House [%s] (idx: %d) selected trigger [%s].\n", pHouse->Type->ID, pHouse->ArrayIndex, selectedTrigger->ID); // Team 1 creation auto pTriggerTeam1Type = selectedTrigger->Team1; @@ -1014,8 +998,8 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (count < pTriggerTeam1Type->Max) { - if (auto newTeam = pTriggerTeam1Type->CreateTeam(pHouse)) - newTeam->NeedsToDisappear = false; + if (TeamClass* newTeam1 = pTriggerTeam1Type->CreateTeam(pHouse)) + newTeam1->NeedsToDisappear = false; } } @@ -1033,8 +1017,8 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (count < pTriggerTeam2Type->Max) { - if (auto newTeam = pTriggerTeam2Type->CreateTeam(pHouse)) - newTeam->NeedsToDisappear = false; + if (TeamClass* newTeam2 = pTriggerTeam2Type->CreateTeam(pHouse)) + newTeam2->NeedsToDisappear = false; } } } @@ -1042,7 +1026,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) return SkipCode; } -bool TeamExt::HouseOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, bool allies, std::vector list) +bool TeamExt::HouseOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, bool allies, DynamicVectorClass list) { bool result = false; int counter = 0; @@ -1091,7 +1075,7 @@ bool TeamExt::HouseOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, bool alli return result; } -bool TeamExt::EnemyOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, bool onlySelectedEnemy, std::vector list) +bool TeamExt::EnemyOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, bool onlySelectedEnemy, DynamicVectorClass list) { bool result = false; int counter = 0; @@ -1144,7 +1128,7 @@ bool TeamExt::EnemyOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClas return result; } -bool TeamExt::NeutralOwns(AITriggerTypeClass* pThis, std::vector list) +bool TeamExt::NeutralOwns(AITriggerTypeClass* pThis, DynamicVectorClass list) { bool result = false; int counter = 0; @@ -1198,11 +1182,11 @@ bool TeamExt::NeutralOwns(AITriggerTypeClass* pThis, std::vector list) +bool TeamExt::HouseOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, DynamicVectorClass list) { bool result = true; - if (list.size() == 0) + if (list.Count == 0) return false; // Count all objects of the list, like an AND operator @@ -1254,14 +1238,14 @@ bool TeamExt::HouseOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, std::v return result; } -bool TeamExt::EnemyOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, std::vector list) +bool TeamExt::EnemyOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, DynamicVectorClass list) { bool result = true; if (pEnemy && pHouse->IsAlliedWith(pEnemy)) pEnemy = nullptr; - if (list.size() == 0) + if (list.Count == 0) return false; // Count all objects of the list, like an AND operator @@ -1315,11 +1299,11 @@ bool TeamExt::EnemyOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseC return result; } -bool TeamExt::NeutralOwnsAll(AITriggerTypeClass* pThis, std::vector list) +bool TeamExt::NeutralOwnsAll(AITriggerTypeClass* pThis, DynamicVectorClass list) { bool result = true; - if (list.size() == 0) + if (list.Count == 0) return false; // Any neutral house should be capable to meet the prerequisites @@ -1384,3 +1368,37 @@ bool TeamExt::NeutralOwnsAll(AITriggerTypeClass* pThis, std::vectorConditions->ComparatorOperand) + { + case 0: + result = nObjects < pThis->Conditions->ComparatorType; + break; + case 1: + result = nObjects <= pThis->Conditions->ComparatorType; + break; + case 2: + result = nObjects == pThis->Conditions->ComparatorType; + break; + case 3: + result = nObjects >= pThis->Conditions->ComparatorType; + break; + case 4: + result = nObjects > pThis->Conditions->ComparatorType; + break; + case 5: + result = nObjects != pThis->Conditions->ComparatorType; + break; + default: + break; + } + + return result; +} diff --git a/src/Ext/Team/Body.h b/src/Ext/Team/Body.h index f6d06a12ff..c35506eb6b 100644 --- a/src/Ext/Team/Body.h +++ b/src/Ext/Team/Body.h @@ -144,11 +144,12 @@ class TeamExt static ExtContainer ExtMap; - static bool HouseOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, bool allies, std::vector list); - static bool HouseOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, std::vector list); - static bool EnemyOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, bool onlySelectedEnemy, std::vector list); - static bool EnemyOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, std::vector list); - static bool NeutralOwns(AITriggerTypeClass* pThis, std::vector list); - static bool NeutralOwnsAll(AITriggerTypeClass* pThis, std::vector list); + static bool HouseOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, bool allies, DynamicVectorClass list); + static bool HouseOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, DynamicVectorClass list); + static bool EnemyOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, bool onlySelectedEnemy, DynamicVectorClass list); + static bool EnemyOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, DynamicVectorClass list); + static bool NeutralOwns(AITriggerTypeClass* pThis, DynamicVectorClass list); + static bool NeutralOwnsAll(AITriggerTypeClass* pThis, DynamicVectorClass list); + static bool CountConditionMet(AITriggerTypeClass* pThis, int nObjects); }; From 28e715b8fd212a0cbf9c7422d0d2c0ec6eb00a40 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Fri, 26 May 2023 23:51:58 +0200 Subject: [PATCH 17/59] fixed the object type I changed by mistake --- src/Ext/Team/Body.cpp | 70 +++++++++++++++++++++---------------------- src/Ext/Team/Body.h | 12 ++++---- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/src/Ext/Team/Body.cpp b/src/Ext/Team/Body.cpp index 0ce99d3969..1d12fa9e25 100644 --- a/src/Ext/Team/Body.cpp +++ b/src/Ext/Team/Body.cpp @@ -363,8 +363,8 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (!pTrigger->ConditionObject) continue; - DynamicVectorClass list; - list.AddItem(pTrigger->ConditionObject); + std::vector list; + list.push_back(pTrigger->ConditionObject); bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, targetHouse, true, list); if (!isConditionMet) @@ -376,8 +376,8 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (!pTrigger->ConditionObject) continue; - DynamicVectorClass list; - list.AddItem(pTrigger->ConditionObject); + std::vector list; + list.push_back(pTrigger->ConditionObject); bool isConditionMet = TeamExt::HouseOwns(pTrigger, pHouse, false, list); if (!isConditionMet) @@ -389,8 +389,8 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (!pTrigger->ConditionObject) continue; - DynamicVectorClass list; - list.AddItem(pTrigger->ConditionObject); + std::vector list; + list.push_back(pTrigger->ConditionObject); bool isConditionMet = TeamExt::NeutralOwns(pTrigger, list); if (!isConditionMet) @@ -402,8 +402,8 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (!pTrigger->ConditionObject) continue; - DynamicVectorClass list; - list.AddItem(pTrigger->ConditionObject); + std::vector list; + list.push_back(pTrigger->ConditionObject); bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, nullptr, false, list); if (!isConditionMet) @@ -411,11 +411,11 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 9) { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) { // New case 9: Like in case 0 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the enemy. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, targetHouse, false, list); if (!isConditionMet) @@ -424,11 +424,11 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 10) { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) { // New case 10: Like in case 1 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the house. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::HouseOwns(pTrigger, pHouse, false, list); if (!isConditionMet) @@ -437,11 +437,11 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 11) { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) { // New case 11: Like in case 7 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the Civilians. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::NeutralOwns(pTrigger, list); if (!isConditionMet) @@ -450,11 +450,11 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 12) { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) { // New case 12: Like in case 0 & 9 but instead of a specific enemy this checks in all enemies. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, nullptr, false, list); if (!isConditionMet) @@ -463,11 +463,11 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 13) { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) { // New case 13: Like in case 1 & 10 but instead checking the house now checks the allies. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::HouseOwns(pTrigger, pHouse, true, list); if (!isConditionMet) @@ -476,11 +476,11 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 14) { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) { // New case 14: Like in case 9 but instead of meet any comparison now is required all. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::EnemyOwnsAll(pTrigger, pHouse, targetHouse, list); if (!isConditionMet) @@ -489,11 +489,11 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 15) { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) { // New case 15: Like in case 10 but instead of meet any comparison now is required all. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::HouseOwnsAll(pTrigger, pHouse, list); if (!isConditionMet) @@ -502,11 +502,11 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 16) { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) { // New case 16: Like in case 11 but instead of meet any comparison now is required all. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::NeutralOwnsAll(pTrigger, list); if (!isConditionMet) @@ -515,11 +515,11 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 17) { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.Count) + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) { // New case 17: Like in case 14 but instead of meet any comparison now is required all. Check all enemies // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - DynamicVectorClass list = RulesExt::Global()->AITargetTypesLists.GetItem(pTrigger->Conditions[3].ComparatorOperand); + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); bool isConditionMet = TeamExt::EnemyOwnsAll(pTrigger, pHouse, nullptr, list); if (!isConditionMet) @@ -1026,7 +1026,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) return SkipCode; } -bool TeamExt::HouseOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, bool allies, DynamicVectorClass list) +bool TeamExt::HouseOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, bool allies, std::vector list) { bool result = false; int counter = 0; @@ -1075,7 +1075,7 @@ bool TeamExt::HouseOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, bool alli return result; } -bool TeamExt::EnemyOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, bool onlySelectedEnemy, DynamicVectorClass list) +bool TeamExt::EnemyOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, bool onlySelectedEnemy, std::vector list) { bool result = false; int counter = 0; @@ -1128,7 +1128,7 @@ bool TeamExt::EnemyOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClas return result; } -bool TeamExt::NeutralOwns(AITriggerTypeClass* pThis, DynamicVectorClass list) +bool TeamExt::NeutralOwns(AITriggerTypeClass* pThis, std::vector list) { bool result = false; int counter = 0; @@ -1182,11 +1182,11 @@ bool TeamExt::NeutralOwns(AITriggerTypeClass* pThis, DynamicVectorClass list) +bool TeamExt::HouseOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, std::vector list) { bool result = true; - if (list.Count == 0) + if (list.size() == 0) return false; // Count all objects of the list, like an AND operator @@ -1238,14 +1238,14 @@ bool TeamExt::HouseOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, Dynami return result; } -bool TeamExt::EnemyOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, DynamicVectorClass list) +bool TeamExt::EnemyOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, std::vector list) { bool result = true; if (pEnemy && pHouse->IsAlliedWith(pEnemy)) pEnemy = nullptr; - if (list.Count == 0) + if (list.size() == 0) return false; // Count all objects of the list, like an AND operator @@ -1299,11 +1299,11 @@ bool TeamExt::EnemyOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseC return result; } -bool TeamExt::NeutralOwnsAll(AITriggerTypeClass* pThis, DynamicVectorClass list) +bool TeamExt::NeutralOwnsAll(AITriggerTypeClass* pThis, std::vector list) { bool result = true; - if (list.Count == 0) + if (list.size() == 0) return false; // Any neutral house should be capable to meet the prerequisites diff --git a/src/Ext/Team/Body.h b/src/Ext/Team/Body.h index c35506eb6b..be1bd9f578 100644 --- a/src/Ext/Team/Body.h +++ b/src/Ext/Team/Body.h @@ -144,12 +144,12 @@ class TeamExt static ExtContainer ExtMap; - static bool HouseOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, bool allies, DynamicVectorClass list); - static bool HouseOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, DynamicVectorClass list); - static bool EnemyOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, bool onlySelectedEnemy, DynamicVectorClass list); - static bool EnemyOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, DynamicVectorClass list); - static bool NeutralOwns(AITriggerTypeClass* pThis, DynamicVectorClass list); - static bool NeutralOwnsAll(AITriggerTypeClass* pThis, DynamicVectorClass list); + static bool HouseOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, bool allies, std::vector list); + static bool HouseOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, std::vector list); + static bool EnemyOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, bool onlySelectedEnemy, std::vector list); + static bool EnemyOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, std::vector list); + static bool NeutralOwns(AITriggerTypeClass* pThis, std::vector list); + static bool NeutralOwnsAll(AITriggerTypeClass* pThis, std::vector list); static bool CountConditionMet(AITriggerTypeClass* pThis, int nObjects); }; From 809e022be4d9f7c980d585cb06f1b2412dabdf66 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Fri, 26 May 2023 23:55:24 +0200 Subject: [PATCH 18/59] Comment a debug block --- src/Ext/Team/Body.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ext/Team/Body.cpp b/src/Ext/Team/Body.cpp index 1d12fa9e25..ec3ffae80c 100644 --- a/src/Ext/Team/Body.cpp +++ b/src/Ext/Team/Body.cpp @@ -254,7 +254,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) activeTeams = activeTeamsList.Count; - Debug::Log("=====================\n[%s] ACTIVE TEAMS: %d / %d\n", pHouse->Type->ID, activeTeams, maxTeamsLimit); + /*Debug::Log("=====================\n[%s] ACTIVE TEAMS: %d / %d\n", pHouse->Type->ID, activeTeams, maxTeamsLimit); for (auto team : activeTeamsList) { Debug::Log("[%s](%d) : %s\n", team->Type->ID, team->TotalObjects, team->Type->Name); @@ -272,7 +272,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) i++; } } - Debug::Log("=====================\n"); + Debug::Log("=====================\n");*/ // We will use these values for discarding triggers bool hasReachedMaxTeamsLimit = activeTeams < maxTeamsLimit ? false : true; From 7e1796080100630e5b610ddc756998f8d8ae6441 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 27 May 2023 00:15:57 +0200 Subject: [PATCH 19/59] Point to latest YRpp --- YRpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/YRpp b/YRpp index 5ad1070ce1..bf7425e1d2 160000 --- a/YRpp +++ b/YRpp @@ -1 +1 @@ -Subproject commit 5ad1070ce1446eb69d205703159cd70f9f580952 +Subproject commit bf7425e1d2cf2353e53919362ab9e4c0c664e9b1 From 2687a956863ddebd7a37e81bf98e7754b715870c Mon Sep 17 00:00:00 2001 From: FS-21 Date: Wed, 13 Dec 2023 17:40:45 +0100 Subject: [PATCH 20/59] Clear some vectors before filling with elements Fix a small bug in custom maps --- src/Ext/TechnoType/Body.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index 1db6f95a35..d5f14c59c3 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -411,6 +411,7 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->ConsideredSecretLabTech.Read(exINI, pSection, "ConsideredSecretLabTech"); // Secret.RequiredHouses contains a list of HouseTypeClass indexes + this->Secret_RequiredHouses.clear(); const char* key = "SecretLab.RequiredHouses"; char* context = nullptr; pINI->ReadString(pSection, key, "", Phobos::readBuffer); @@ -424,6 +425,7 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) key = nullptr; // Secret.ForbiddenHouses contains a list of HouseTypeClass indexes + this->Secret_ForbiddenHouses.clear(); key = "SecretLab.ForbiddenHouses"; context = nullptr; pINI->ReadString(pSection, key, "", Phobos::readBuffer); @@ -437,6 +439,7 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) key = nullptr; // Prerequisite.RequiredTheaters contains a list of theader names + this->Prerequisite_RequiredTheaters.clear(); key = "Prerequisite.RequiredTheaters"; context = nullptr; pINI->ReadString(pSection, key, "", Phobos::readBuffer); @@ -452,6 +455,7 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) // Prerequisite with Generic Prerequistes support. // Note: I have no idea of what could happen in all the game engine logics if I push the negative indexes of the Ares generic prerequisites directly into the original Prerequisite tag... for that reason this tag is duplicated for working with it + this->Prerequisite.clear(); key = "Prerequisite"; context = nullptr; pINI->ReadString(pSection, key, "", Phobos::readBuffer); @@ -474,6 +478,7 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) key = nullptr; // Prerequisite.Negative with Generic Prerequistes support + this->Prerequisite_Negative.clear(); key = "Prerequisite.Negative"; context = nullptr; pINI->ReadString(pSection, key, "", Phobos::readBuffer); @@ -496,6 +501,7 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) key = nullptr; // Prerequisite.ListX with Generic Prerequistes support + this->Prerequisite_ListVector.clear(); this->Prerequisite_Lists.Read(exINI, pSection, "Prerequisite.Lists"); if (Prerequisite_Lists.Get() > 0) @@ -524,7 +530,7 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) } } - Prerequisite_ListVector.push_back(objectsList); + this->Prerequisite_ListVector.push_back(objectsList); objectsList.Clear(); } } From 8477602a85457946f60c17d6007eae58ca949e55 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Fri, 22 Dec 2023 09:37:44 +0100 Subject: [PATCH 21/59] ParentCountry support Necessary for Single player missions --- src/Ext/Team/Body.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Ext/Team/Body.cpp b/src/Ext/Team/Body.cpp index e12445571e..06ea5203fd 100644 --- a/src/Ext/Team/Body.cpp +++ b/src/Ext/Team/Body.cpp @@ -227,8 +227,14 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } } + int parentCountryTypeIdx = pHouse->Type->FindParentCountryIndex(); // ParentCountry can change the House in a SP map + int houseTypeIdx = parentCountryTypeIdx >= 0 ? parentCountryTypeIdx : pHouse->Type->ArrayIndex; // Indexes in AITriggers section are 1-based int houseIdx = pHouse->ArrayIndex; - int sideIdx = pHouse->SideIndex + 1; + + int parentCountrySideTypeIdx = pHouse->Type->FindParentCountry()->SideIndex; + int sideTypeIdx = parentCountrySideTypeIdx >= 0 ? parentCountrySideTypeIdx + 1 : pHouse->Type->SideIndex + 1; // Side indexes in AITriggers section are 1-based + int sideIdx = pHouse->SideIndex + 1; // Side indexes in AITriggers section are 1-based + auto houseDifficulty = pHouse->AIDifficulty; int maxBaseDefenseTeams = RulesClass::Instance->MaximumAIDefensiveTeams.GetItem((int)houseDifficulty); int activeDefenseTeamsCount = 0; @@ -356,7 +362,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (pTrigger->IsEnabled) { // The trigger must be compatible with the owner - if ((triggerHouse == -1 || houseIdx == triggerHouse) && (triggerSide == 0 || sideIdx == triggerSide)) + if ((triggerHouse == -1 || houseTypeIdx == triggerHouse) && (triggerSide == 0 || sideTypeIdx == triggerSide)) { // "ConditionType=-1" will be skipped, always is valid if ((int)pTrigger->ConditionType >= 0) From 0a8284fa24a7d75dd5a9278ade00c39650ec16e4 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Mon, 11 Mar 2024 19:38:43 +0100 Subject: [PATCH 22/59] Fix compilation --- src/Ext/HouseType/Body.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Ext/HouseType/Body.cpp b/src/Ext/HouseType/Body.cpp index 9dfeccf13b..9f990dbc92 100644 --- a/src/Ext/HouseType/Body.cpp +++ b/src/Ext/HouseType/Body.cpp @@ -2,7 +2,7 @@ #include -template<> const DWORD Extension::Canary = 0x1111111A; +static constexpr DWORD Canary = 0x1111111A; HouseTypeExt::ExtContainer HouseTypeExt::ExtMap; void HouseTypeExt::ExtData::Initialize() @@ -89,7 +89,7 @@ DEFINE_HOOK(0x511635, HouseTypeClass_CTOR_1, 0x5) { GET(HouseTypeClass*, pItem, EAX); - HouseTypeExt::ExtMap.FindOrAllocate(pItem); + HouseTypeExt::ExtMap.Allocate(pItem); return 0; } @@ -98,7 +98,7 @@ DEFINE_HOOK(0x511643, HouseTypeClass_CTOR_2, 0x5) { GET(HouseTypeClass*, pItem, EAX); - HouseTypeExt::ExtMap.FindOrAllocate(pItem); + HouseTypeExt::ExtMap.Allocate(pItem); return 0; } From 5a2fa0f0bc3bb88481ca13531125db51865d2484 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Mon, 29 Apr 2024 07:12:02 +0200 Subject: [PATCH 23/59] Some prerequisites Fixes --- src/Ext/Building/Body.cpp | 2 +- src/Ext/House/Body.cpp | 18 ++++++++++-------- src/Ext/House/Body.h | 4 ++-- src/Ext/Rules/Body.cpp | 19 +++++++++++++++++++ src/Ext/Team/Body.cpp | 6 +++--- 5 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/Ext/Building/Body.cpp b/src/Ext/Building/Body.cpp index 19e2427633..043715f36e 100644 --- a/src/Ext/Building/Body.cpp +++ b/src/Ext/Building/Body.cpp @@ -380,7 +380,7 @@ void BuildingExt::ExtData::UpdateSecretLabAI() if (possibleCandidates.Count > 0) { - std::map ownedBuildings; + std::map ownedBuildings; for (auto building : pOwner->Buildings) { diff --git a/src/Ext/House/Body.cpp b/src/Ext/House/Body.cpp index a5bb9cd139..09cee3da73 100644 --- a/src/Ext/House/Body.cpp +++ b/src/Ext/House/Body.cpp @@ -380,7 +380,7 @@ HouseClass* HouseExt::GetHouseKind(OwnerHouseKind const kind, bool const allowRa } } -bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const pItem, std::map ownedBuildings, bool skipSecretLabChecks) +bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const pItem, std::map ownedBuildings, bool skipSecretLabChecks) { if (!pThis || !pItem) return false; @@ -448,7 +448,7 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const } else { - if (ownedBuildings[TechnoTypeClass::Array->GetItem(idx)] > 0) + if (ownedBuildings.count(BuildingTypeClass::Array->GetItem(idx)) > 0) prerequisiteNegativeMet = true; } @@ -480,7 +480,7 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const } else { - if (ownedBuildings[TechnoTypeClass::Array->GetItem(idx)] > 0) + if (ownedBuildings.count(BuildingTypeClass::Array->GetItem(idx)) > 0) prerequisiteOverrideMet = true; } } @@ -501,7 +501,8 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const } else { - if (ownedBuildings[TechnoTypeClass::Array->GetItem(idx)] > 0) + auto debugType = BuildingTypeClass::Array->GetItem(idx); + if (ownedBuildings.count(BuildingTypeClass::Array->GetItem(idx)) > 0) found = true; } @@ -540,7 +541,7 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const { found = false; - if (ownedBuildings[TechnoTypeClass::Array->GetItem(idx)] > 0) + if (ownedBuildings.count(BuildingTypeClass::Array->GetItem(idx)) > 0) found = true; } @@ -555,14 +556,14 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const return prerequisiteMet || prerequisiteListsMet || prerequisiteOverrideMet; } -bool HouseExt::HasGenericPrerequisite(int idx, std::map ownedBuildings) +bool HouseExt::HasGenericPrerequisite(int idx, std::map ownedBuildings) { if (idx >= 0) return false; DynamicVectorClass selectedPrerequisite = RulesExt::Global()->GenericPrerequisites.GetItem(std::abs(idx)); - //auto selectedPrerequisiteName = RulesExt::Global()->GenericPrerequisitesNames.GetItem(std::abs(idx));// Only used for easy debug + const char* selectedPrerequisiteName = RulesExt::Global()->GenericPrerequisitesNames[std::abs(idx)];// Only used for easy debug if (selectedPrerequisite.Count == 0) return false; @@ -574,7 +575,8 @@ bool HouseExt::HasGenericPrerequisite(int idx, std::map o if (found) break; - if (ownedBuildings[TechnoTypeClass::Array->GetItem(idxItem)] > 0) + auto debugType = BuildingTypeClass::Array->GetItem(idxItem); + if (ownedBuildings.count(BuildingTypeClass::Array->GetItem(idxItem)) > 0) found = true; } diff --git a/src/Ext/House/Body.h b/src/Ext/House/Body.h index 0317ac6857..ebcb2e3600 100644 --- a/src/Ext/House/Body.h +++ b/src/Ext/House/Body.h @@ -115,8 +115,8 @@ class HouseExt static int TotalHarvesterCount(HouseClass* pThis); static HouseClass* GetHouseKind(OwnerHouseKind kind, bool allowRandom, HouseClass* pDefault, HouseClass* pInvoker = nullptr, HouseClass* pVictim = nullptr); static CellClass* GetEnemyBaseGatherCell(HouseClass* pTargetHouse, HouseClass* pCurrentHouse, CoordStruct defaultCurrentCoords, SpeedType speedTypeZone, int extraDistance = 0); - static bool PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const pItem, const std::map ownedBuildings, bool skipSecretLabChecks); - static bool HasGenericPrerequisite(int idx, std::map ownedBuildings); + static bool PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const pItem, const std::map ownedBuildings, bool skipSecretLabChecks); + static bool HasGenericPrerequisite(int idx, std::map ownedBuildings); static int FindGenericPrerequisite(const char* id); static void SetSkirmishHouseName(HouseClass* pHouse); diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index 4da6eaa086..06ac0830bd 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -281,6 +281,25 @@ void RulesExt::FillDefaultPrerequisites() objectsList.Clear(); } + + // Debug + /*if (RulesExt::Global()->GenericPrerequisites.Count > 0) + { + Debug::Log("[GenericPrerequisites] = %d\n", RulesExt::Global()->GenericPrerequisites.Count); + Debug::Log("[GenericPrerequisites] (ID names) = %d\n\n", RulesExt::Global()->GenericPrerequisitesNames.Count); + + for (int i = 0; i < RulesExt::Global()->GenericPrerequisites.Count; i++) + { + auto list = RulesExt::Global()->GenericPrerequisites.GetItem(i); + const auto listName = RulesExt::Global()->GenericPrerequisitesNames[i]; + + for (int j : list) + { + auto pTypeName = BuildingTypeClass::Array->GetItem(j)->ID; + Debug::Log("[GenericPrerequisites][%s](%d) -> [%s](%d)\n", listName, i, pTypeName, j); + } + } + }*/ } // ============================= diff --git a/src/Ext/Team/Body.cpp b/src/Ext/Team/Body.cpp index 06ea5203fd..b809226c31 100644 --- a/src/Ext/Team/Body.cpp +++ b/src/Ext/Team/Body.cpp @@ -300,7 +300,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) int destroyedBridgesCount = 0; int undamagedBridgesCount = 0; std::map ownedRecruitables; - std::map ownedBuildings; + std::map ownedBuildings; for (auto pTechno : *TechnoClass::Array) { @@ -308,7 +308,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) { if (pTechno->Owner == pHouse) { - ++ownedBuildings[pTechno->GetTechnoType()]; + ++ownedBuildings[static_cast(pTechno->GetTechnoType())]; } else { @@ -701,7 +701,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) // Check if each unit in the taskforce has the available recruitable units in the map if (allObjectsCanBeBuiltOrRecruited && entry.Type && entry.Amount > 0) { - if (ownedRecruitables[entry.Type] < entry.Amount) + if (ownedRecruitables.count(entry.Type) < entry.Amount) { allObjectsCanBeBuiltOrRecruited = false; break; From 149a718643fb32bbb1337b5012d3a4cfeaa21da3 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Mon, 29 Apr 2024 08:57:11 +0200 Subject: [PATCH 24/59] Moved code into its own file --- Phobos.vcxproj | 2 + src/Ext/House/Body.Prerequisites.cpp | 224 ++++ src/Ext/House/Body.cpp | 223 ---- src/Ext/Team/Body.NewTeamsSelector.cpp | 1320 ++++++++++++++++++++++++ src/Ext/Team/Body.cpp | 1319 ----------------------- 5 files changed, 1546 insertions(+), 1542 deletions(-) create mode 100644 src/Ext/House/Body.Prerequisites.cpp create mode 100644 src/Ext/Team/Body.NewTeamsSelector.cpp diff --git a/Phobos.vcxproj b/Phobos.vcxproj index 6210e9bd25..cf9aedf0cc 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -38,6 +38,7 @@ + @@ -53,6 +54,7 @@ + diff --git a/src/Ext/House/Body.Prerequisites.cpp b/src/Ext/House/Body.Prerequisites.cpp new file mode 100644 index 0000000000..7d190fe450 --- /dev/null +++ b/src/Ext/House/Body.Prerequisites.cpp @@ -0,0 +1,224 @@ +#include "body.h" + +bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const pItem, std::map ownedBuildings, bool skipSecretLabChecks) +{ + if (!pThis || !pItem) + return false; + + auto pHouseExt = HouseExt::ExtMap.Find(pThis); + if (!pHouseExt) + return false; + + auto pItemExt = TechnoTypeExt::ExtMap.Find(pItem); + if (!pItemExt) + 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; + + // Check if it appears in Owner=, RequiredHouses= and ForbiddenHouses= + // Note: if RequiredHouses = tag doesn't exist InRequiredHouses() always returns TRUE + if (!pThis->InOwners(pItem) || !pThis->InRequiredHouses(pItem) || pThis->InForbiddenHouses(pItem)) + return false; + + // Prerequisite.RequiredTheaters check + if (pItemExt->Prerequisite_RequiredTheaters.size() > 0) + { + int currentTheaterIndex = (int)ScenarioClass::Instance->Theater; + if (pItemExt->Prerequisite_RequiredTheaters.IndexOf(currentTheaterIndex) < 0) + return false; + } + + // TechLevel check + if (pThis->TechLevel < pItem->TechLevel) + return false; + + // BuildLimit checks + int nInstances = 0; + + for (auto pTechno : *TechnoClass::Array) + { + if (pTechno->Owner == pThis + && pTechno->GetTechnoType() == pItem + && pTechno->IsAlive + && pTechno->Health > 0) + { + nInstances++; + + if (nInstances >= pItem->BuildLimit) + return false; + } + } + + if (pItem->BuildLimit < 1) + return false; + + bool prerequisiteNegativeMet = false; // Only one coincidence is needed + + // Ares Prerequisite.Negative list + if (pItemExt->Prerequisite_Negative.size() > 0) + { + for (int idx : pItemExt->Prerequisite_Negative) + { + if (idx < 0) // Can be used generic prerequisites in this Ares tag? I have to investigate it but for now we support it... + { + // Default prerequisites like POWER, PROC, BARRACKS, FACTORY, ... + prerequisiteNegativeMet = HouseExt::HasGenericPrerequisite(idx, ownedBuildings); + } + else + { + if (ownedBuildings.count(BuildingTypeClass::Array->GetItem(idx)) > 0) + prerequisiteNegativeMet = true; + } + + if (prerequisiteNegativeMet) + return false; + } + } + + // Main prerequisite checks are skipped if a new secret lab object is in process to be unlocked + if (skipSecretLabChecks) + return true; + + DynamicVectorClass prerequisiteOverride = pItem->PrerequisiteOverride; + + bool prerequisiteMet = false; // All buildings must appear in the buildings list owner by the house + bool prerequisiteOverrideMet = false; // This tag uses an OR comparator: Only one coincidence is needed + + if (prerequisiteOverride.Count > 0) + { + for (int idx : prerequisiteOverride) + { + if (prerequisiteOverrideMet) + break; + + if (idx < 0) + { + // Default prerequisites like POWER, PROC, BARRACKS, FACTORY, ... + prerequisiteOverrideMet = HouseExt::HasGenericPrerequisite(idx, ownedBuildings); + } + else + { + if (ownedBuildings.count(BuildingTypeClass::Array->GetItem(idx)) > 0) + prerequisiteOverrideMet = true; + } + } + } + + if (pItemExt->Prerequisite.size() > 0) + { + bool found = false; + + for (int idx : pItemExt->Prerequisite) + { + found = false; + + if (idx < 0) + { + // Default prerequisites like POWER, PROC, BARRACKS, FACTORY, ... + found = HouseExt::HasGenericPrerequisite(idx, ownedBuildings); + } + else + { + auto debugType = BuildingTypeClass::Array->GetItem(idx); + if (ownedBuildings.count(BuildingTypeClass::Array->GetItem(idx)) > 0) + found = true; + } + + if (!found) + break; + } + + prerequisiteMet = found; + } + else + { + // No prerequisites list means that always is buildable + prerequisiteMet = true; + } + + bool prerequisiteListsMet = false; + + // Ares Prerequisite lists + if (pItemExt->Prerequisite_Lists.Get() > 0) + { + bool found = false; + + for (auto list : pItemExt->Prerequisite_ListVector) + { + if (found) + break; + + for (int idx : list) + { + if (idx < 0) + { + // Default prerequisites like POWER, PROC, BARRACKS, FACTORY, ... + found = HouseExt::HasGenericPrerequisite(idx, ownedBuildings); + } + else + { + found = false; + + if (ownedBuildings.count(BuildingTypeClass::Array->GetItem(idx)) > 0) + found = true; + } + + if (!found) + break; + } + } + + prerequisiteListsMet = found; + } + + return prerequisiteMet || prerequisiteListsMet || prerequisiteOverrideMet; +} + +bool HouseExt::HasGenericPrerequisite(int idx, std::map ownedBuildings) + +{ + if (idx >= 0) + return false; + + DynamicVectorClass selectedPrerequisite = RulesExt::Global()->GenericPrerequisites.GetItem(std::abs(idx)); + const char* selectedPrerequisiteName = RulesExt::Global()->GenericPrerequisitesNames[std::abs(idx)];// Only used for easy debug + + if (selectedPrerequisite.Count == 0) + return false; + + bool found = false; + + for (auto idxItem : selectedPrerequisite) + { + if (found) + break; + + auto debugType = BuildingTypeClass::Array->GetItem(idxItem); + if (ownedBuildings.count(BuildingTypeClass::Array->GetItem(idxItem)) > 0) + found = true; + } + + return found; +} + +int HouseExt::FindGenericPrerequisite(const char* id) +{ + if (TechnoTypeClass::FindIndex(id) >= 0) + return 0; + + if (RulesExt::Global()->GenericPrerequisitesNames.Count == 0) + RulesExt::FillDefaultPrerequisites(); // needed! + + int i = 0; + for (auto str : RulesExt::Global()->GenericPrerequisitesNames) + { + if (_strcmpi(id, str) == 0) + return (-1 * i); + + ++i; + } + + return 0; +} diff --git a/src/Ext/House/Body.cpp b/src/Ext/House/Body.cpp index 09cee3da73..1422813550 100644 --- a/src/Ext/House/Body.cpp +++ b/src/Ext/House/Body.cpp @@ -380,229 +380,6 @@ HouseClass* HouseExt::GetHouseKind(OwnerHouseKind const kind, bool const allowRa } } -bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const pItem, std::map ownedBuildings, bool skipSecretLabChecks) -{ - if (!pThis || !pItem) - return false; - - auto pHouseExt = HouseExt::ExtMap.Find(pThis); - if (!pHouseExt) - return false; - - auto pItemExt = TechnoTypeExt::ExtMap.Find(pItem); - if (!pItemExt) - 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; - - // Check if it appears in Owner=, RequiredHouses= and ForbiddenHouses= - // Note: if RequiredHouses = tag doesn't exist InRequiredHouses() always returns TRUE - if (!pThis->InOwners(pItem) || !pThis->InRequiredHouses(pItem) || pThis->InForbiddenHouses(pItem)) - return false; - - // Prerequisite.RequiredTheaters check - if (pItemExt->Prerequisite_RequiredTheaters.size() > 0) - { - int currentTheaterIndex = (int)ScenarioClass::Instance->Theater; - if (pItemExt->Prerequisite_RequiredTheaters.IndexOf(currentTheaterIndex) < 0) - return false; - } - - // TechLevel check - if (pThis->TechLevel < pItem->TechLevel) - return false; - - // BuildLimit checks - int nInstances = 0; - - for (auto pTechno : *TechnoClass::Array) - { - if (pTechno->Owner == pThis - && pTechno->GetTechnoType() == pItem - && pTechno->IsAlive - && pTechno->Health > 0) - { - nInstances++; - - if (nInstances >= pItem->BuildLimit) - return false; - } - } - - if (pItem->BuildLimit < 1) - return false; - - bool prerequisiteNegativeMet = false; // Only one coincidence is needed - - // Ares Prerequisite.Negative list - if (pItemExt->Prerequisite_Negative.size() > 0) - { - for (int idx : pItemExt->Prerequisite_Negative) - { - if (idx < 0) // Can be used generic prerequisites in this Ares tag? I have to investigate it but for now we support it... - { - // Default prerequisites like POWER, PROC, BARRACKS, FACTORY, ... - prerequisiteNegativeMet = HouseExt::HasGenericPrerequisite(idx, ownedBuildings); - } - else - { - if (ownedBuildings.count(BuildingTypeClass::Array->GetItem(idx)) > 0) - prerequisiteNegativeMet = true; - } - - if (prerequisiteNegativeMet) - return false; - } - } - - // Main prerequisite checks are skipped if a new secret lab object is in process to be unlocked - if (skipSecretLabChecks) - return true; - - DynamicVectorClass prerequisiteOverride = pItem->PrerequisiteOverride; - - bool prerequisiteMet = false; // All buildings must appear in the buildings list owner by the house - bool prerequisiteOverrideMet = false; // This tag uses an OR comparator: Only one coincidence is needed - - if (prerequisiteOverride.Count > 0) - { - for (int idx : prerequisiteOverride) - { - if (prerequisiteOverrideMet) - break; - - if (idx < 0) - { - // Default prerequisites like POWER, PROC, BARRACKS, FACTORY, ... - prerequisiteOverrideMet = HouseExt::HasGenericPrerequisite(idx, ownedBuildings); - } - else - { - if (ownedBuildings.count(BuildingTypeClass::Array->GetItem(idx)) > 0) - prerequisiteOverrideMet = true; - } - } - } - - if (pItemExt->Prerequisite.size() > 0) - { - bool found = false; - - for (int idx : pItemExt->Prerequisite) - { - found = false; - - if (idx < 0) - { - // Default prerequisites like POWER, PROC, BARRACKS, FACTORY, ... - found = HouseExt::HasGenericPrerequisite(idx, ownedBuildings); - } - else - { - auto debugType = BuildingTypeClass::Array->GetItem(idx); - if (ownedBuildings.count(BuildingTypeClass::Array->GetItem(idx)) > 0) - found = true; - } - - if (!found) - break; - } - - prerequisiteMet = found; - } - else - { - // No prerequisites list means that always is buildable - prerequisiteMet = true; - } - - bool prerequisiteListsMet = false; - - // Ares Prerequisite lists - if (pItemExt->Prerequisite_Lists.Get() > 0) - { - bool found = false; - - for (auto list : pItemExt->Prerequisite_ListVector) - { - if (found) - break; - - for (int idx : list) - { - if (idx < 0) - { - // Default prerequisites like POWER, PROC, BARRACKS, FACTORY, ... - found = HouseExt::HasGenericPrerequisite(idx, ownedBuildings); - } - else - { - found = false; - - if (ownedBuildings.count(BuildingTypeClass::Array->GetItem(idx)) > 0) - found = true; - } - - if (!found) - break; - } - } - - prerequisiteListsMet = found; - } - - return prerequisiteMet || prerequisiteListsMet || prerequisiteOverrideMet; -} - -bool HouseExt::HasGenericPrerequisite(int idx, std::map ownedBuildings) - -{ - if (idx >= 0) - return false; - - DynamicVectorClass selectedPrerequisite = RulesExt::Global()->GenericPrerequisites.GetItem(std::abs(idx)); - const char* selectedPrerequisiteName = RulesExt::Global()->GenericPrerequisitesNames[std::abs(idx)];// Only used for easy debug - - if (selectedPrerequisite.Count == 0) - return false; - - bool found = false; - - for (auto idxItem : selectedPrerequisite) - { - if (found) - break; - - auto debugType = BuildingTypeClass::Array->GetItem(idxItem); - if (ownedBuildings.count(BuildingTypeClass::Array->GetItem(idxItem)) > 0) - found = true; - } - - return found; -} - -int HouseExt::FindGenericPrerequisite(const char* id) -{ - if (TechnoTypeClass::FindIndex(id) >= 0) - return 0; - - if (RulesExt::Global()->GenericPrerequisitesNames.Count == 0) - RulesExt::FillDefaultPrerequisites(); // needed! - - int i = 0; - for (auto str : RulesExt::Global()->GenericPrerequisitesNames) - { - if (_strcmpi(id, str) == 0) - return (-1 * i); - - ++i; - } - - return 0; -} - void HouseExt::ExtData::UpdateAutoDeathObjectsInLimbo() { for (auto const pExt : this->OwnedAutoDeathObjects) diff --git a/src/Ext/Team/Body.NewTeamsSelector.cpp b/src/Ext/Team/Body.NewTeamsSelector.cpp new file mode 100644 index 0000000000..938bd2fbed --- /dev/null +++ b/src/Ext/Team/Body.NewTeamsSelector.cpp @@ -0,0 +1,1320 @@ +#include "Body.h" + +DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) +{ + enum { UseOriginalSelector = 0x4F8A63, SkipCode = 0x4F8B08 }; + + GET(HouseClass*, pHouse, ESI); + + bool houseIsHuman = pHouse->IsHumanPlayer; + + if (SessionClass::IsCampaign()) + houseIsHuman = pHouse->IsHumanPlayer || pHouse->IsInPlayerControl; + + if (houseIsHuman || pHouse->Type->MultiplayPassive) + return SkipCode; + + auto pHouseTypeExt = HouseTypeExt::ExtMap.Find(pHouse->Type); + if (!pHouseTypeExt) + return SkipCode; + + if (!RulesExt::Global()->NewTeamsSelector) + return UseOriginalSelector; + + // Reset Team selection countdown + int countdown = RulesClass::Instance->TeamDelays[(int)pHouse->AIDifficulty]; + pHouse->TeamDelayTimer.Start(countdown); + + int totalActiveTeams = 0; + int activeTeams = 0; + + int totalGroundCategoryTriggers = 0; + int totalUnclassifiedCategoryTriggers = 0; + int totalNavalCategoryTriggers = 0; + int totalAirCategoryTriggers = 0; + + DynamicVectorClass validTriggerCandidates; + DynamicVectorClass validTriggerCandidatesGroundOnly; + DynamicVectorClass validTriggerCandidatesNavalOnly; + DynamicVectorClass validTriggerCandidatesAirOnly; + DynamicVectorClass validTriggerCandidatesUnclassifiedOnly; + + int dice = ScenarioClass::Instance->Random.RandomRanged(1, 100); + + // This house must have the triggers enabled + if (dice <= pHouse->RatioAITriggerTeam && pHouse->AITriggersActive) + { + 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; + + 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 + 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 + + // 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; + + default: + break; + } + + percentageUnclassifiedTriggers = 0.0; + } + + percentageUnclassifiedTriggers = percentageUnclassifiedTriggers < 0.0 || percentageUnclassifiedTriggers > 1.0 ? 0.0 : percentageUnclassifiedTriggers; + percentageGroundTriggers = percentageGroundTriggers < 0.0 || percentageGroundTriggers > 1.0 ? 0.0 : percentageGroundTriggers; + percentageNavalTriggers = percentageNavalTriggers < 0.0 || percentageNavalTriggers > 1.0 ? 0.0 : percentageNavalTriggers; + percentageAirTriggers = percentageAirTriggers < 0.0 || percentageAirTriggers > 1.0 ? 0.0 : percentageAirTriggers; + + double totalPercengates = percentageUnclassifiedTriggers + percentageGroundTriggers + percentageNavalTriggers + percentageAirTriggers; + if (totalPercengates > 1.0 || totalPercengates <= 0.0) + splitTriggersByCategory = false; + + + if (splitTriggersByCategory) + { + int categoryDice = ScenarioClass::Instance->Random.RandomRanged(1, 100); + int unclassifiedValue = (int)(percentageUnclassifiedTriggers * 100.0); + int groundValue = (int)(percentageGroundTriggers * 100.0); + int airValue = (int)(percentageAirTriggers * 100.0); + int navalValue = (int)(percentageNavalTriggers * 100.0); + + // Pick what type of team will be selected in this round + if (percentageUnclassifiedTriggers > 0.0 && categoryDice <= unclassifiedValue) + { + validCategory = teamCategory::Unclassified; + Debug::Log("New AI team category selection: dice %d <= %d (MIXED)\n", categoryDice, unclassifiedValue); + } + else if (percentageGroundTriggers > 0.0 && categoryDice <= (unclassifiedValue + groundValue)) + { + validCategory = teamCategory::Ground; + Debug::Log("New AI team category selection: dice %d <= %d (mixed: %d%% + GROUND: %d%%)\n", categoryDice, (unclassifiedValue + groundValue), unclassifiedValue, groundValue); + } + else if (percentageAirTriggers > 0.0 && categoryDice <= (unclassifiedValue + groundValue + airValue)) + { + validCategory = teamCategory::Air; + Debug::Log("New AI team category selection: dice %d <= %d (mixed: %d%% + ground: %d%% + AIR: %d%%)\n", categoryDice, (unclassifiedValue + groundValue + airValue), unclassifiedValue, groundValue, airValue); + } + else if (percentageNavalTriggers > 0.0 && categoryDice <= (unclassifiedValue + groundValue + airValue + navalValue)) + { + validCategory = teamCategory::Naval; + Debug::Log("New AI team category selection: dice %d <= %d (mixed: %d%% + ground: %d%% + air: %d%% + NAVAL: %d%%)\n", categoryDice, (unclassifiedValue + groundValue + airValue + navalValue), unclassifiedValue, groundValue, airValue, navalValue); + } + else + { + // If the sum of all percentages is less than 100% then that empty space will work like "no categories" + splitTriggersByCategory = false; + } + } + } + + int parentCountryTypeIdx = pHouse->Type->FindParentCountryIndex(); // ParentCountry can change the House in a SP map + int houseTypeIdx = parentCountryTypeIdx >= 0 ? parentCountryTypeIdx : pHouse->Type->ArrayIndex; // Indexes in AITriggers section are 1-based + int houseIdx = pHouse->ArrayIndex; + + int parentCountrySideTypeIdx = pHouse->Type->FindParentCountry()->SideIndex; + int sideTypeIdx = parentCountrySideTypeIdx >= 0 ? parentCountrySideTypeIdx + 1 : pHouse->Type->SideIndex + 1; // Side indexes in AITriggers section are 1-based + int sideIdx = pHouse->SideIndex + 1; // Side indexes in AITriggers section are 1-based + + auto houseDifficulty = pHouse->AIDifficulty; + int maxBaseDefenseTeams = RulesClass::Instance->MaximumAIDefensiveTeams.GetItem((int)houseDifficulty); + int activeDefenseTeamsCount = 0; + int maxTeamsLimit = RulesClass::Instance->TotalAITeamCap.GetItem((int)houseDifficulty); + double totalWeight = 0.0; + double totalWeightGroundOnly = 0.0; + double totalWeightNavalOnly = 0.0; + double totalWeightAirOnly = 0.0; + double totalWeightUnclassifiedOnly = 0.0; + + // Check if the running teams by the house already reached all the limits + DynamicVectorClass activeTeamsList; + + for (auto const pRunningTeam : *TeamClass::Array) + { + totalActiveTeams++; + int teamHouseIdx = pRunningTeam->Owner->ArrayIndex; + + if (teamHouseIdx != houseIdx) + continue; + + activeTeamsList.AddItem(pRunningTeam); + + if (pRunningTeam->Type->IsBaseDefense && activeDefenseTeamsCount < maxBaseDefenseTeams) + activeDefenseTeamsCount++; + } + + activeTeams = activeTeamsList.Count; + + /*Debug::Log("=====================\n[%s] ACTIVE TEAMS: %d / %d\n", pHouse->Type->ID, activeTeams, maxTeamsLimit); + for (auto team : activeTeamsList) + { + Debug::Log("[%s](%d) : %s\n", team->Type->ID, team->TotalObjects, team->Type->Name); + Debug::Log(" IsMoving: %d, IsFullStrength: %d, IsUnderStrength: %d\n", team->IsMoving, team->IsFullStrength, team->IsUnderStrength); + int i = 0; + + for (auto entry : team->Type->TaskForce->Entries) + { + if (entry.Type && entry.Amount > 0) + { + if (entry.Type) + Debug::Log("\t[%s]: %d / %d\n", entry.Type->ID, team->CountObjects[i], entry.Amount); + } + + i++; + } + } + Debug::Log("=====================\n");*/ + + // We will use these values for discarding triggers + bool hasReachedMaxTeamsLimit = activeTeams < maxTeamsLimit ? false : true; + bool hasReachedMaxDefensiveTeamsLimit = activeDefenseTeamsCount < maxBaseDefenseTeams ? false : true; + + if (hasReachedMaxDefensiveTeamsLimit) + Debug::Log("DEBUG: House [%s] (idx: %d) reached the MaximumAIDefensiveTeams value!\n", pHouse->Type->ID, pHouse->ArrayIndex); + + if (hasReachedMaxTeamsLimit) + { + Debug::Log("DEBUG: House [%s] (idx: %d) reached the TotalAITeamCap value!\n", pHouse->Type->ID, pHouse->ArrayIndex); + return SkipCode; + } + + int destroyedBridgesCount = 0; + int undamagedBridgesCount = 0; + std::map ownedRecruitables; + std::map ownedBuildings; + + for (auto pTechno : *TechnoClass::Array) + { + if (pTechno->WhatAmI() == AbstractType::Building) + { + if (pTechno->Owner == pHouse) + { + ++ownedBuildings[static_cast(pTechno->GetTechnoType())]; + } + else + { + auto pBuilding = static_cast(pTechno); + if (pBuilding && pBuilding->Type->BridgeRepairHut) + { + CellStruct cell = pTechno->GetCell()->MapCoords; + + if (MapClass::Instance->IsLinkedBridgeDestroyed(cell)) + destroyedBridgesCount++; + else + undamagedBridgesCount++; + } + } + + continue; + } + + auto* pFoot = static_cast(pTechno); + + if (!pFoot + || !pTechno->IsAlive + || pTechno->Health <= 0 + || !pTechno->IsOnMap // Note: underground movement is considered "IsOnMap == false" + || pTechno->Transporter + || pTechno->Absorbed + || !pFoot->CanBeRecruited(pHouse)) + { + continue; + } + + ++ownedRecruitables[pTechno->GetTechnoType()]; + } + + HouseClass* targetHouse = nullptr; + if (pHouse->EnemyHouseIndex >= 0) + targetHouse = HouseClass::Array->GetItem(pHouse->EnemyHouseIndex); + + bool onlyCheckImportantTriggers = false; + + // Gather all the trigger candidates into one place for posterior fast calculations + for (auto const pTrigger : *AITriggerTypeClass::Array) + { + if (!pTrigger) + continue; + + int triggerHouse = pTrigger->HouseIndex; + int triggerSide = pTrigger->SideIndex; + + // Ignore the deactivated triggers + if (pTrigger->IsEnabled) + { + // The trigger must be compatible with the owner + if ((triggerHouse == -1 || houseTypeIdx == triggerHouse) && (triggerSide == 0 || sideTypeIdx == triggerSide)) + { + // "ConditionType=-1" will be skipped, always is valid + if ((int)pTrigger->ConditionType >= 0) + { + if ((int)pTrigger->ConditionType == 0) + { + // Simulate case 0: "enemy owns" + if (!pTrigger->ConditionObject) + continue; + + std::vector list; + list.push_back(pTrigger->ConditionObject); + bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, targetHouse, true, list); + + if (!isConditionMet) + continue; + } + else if ((int)pTrigger->ConditionType == 1) + { + // Simulate case 1: "house owns" + if (!pTrigger->ConditionObject) + continue; + + std::vector list; + list.push_back(pTrigger->ConditionObject); + bool isConditionMet = TeamExt::HouseOwns(pTrigger, pHouse, false, list); + + if (!isConditionMet) + continue; + } + else if ((int)pTrigger->ConditionType == 7) + { + // Simulate case 7: "civilian owns" + if (!pTrigger->ConditionObject) + continue; + + std::vector list; + list.push_back(pTrigger->ConditionObject); + bool isConditionMet = TeamExt::NeutralOwns(pTrigger, list); + + if (!isConditionMet) + continue; + } + else if ((int)pTrigger->ConditionType == 8) + { + // Simulate case 0: "enemy owns" but instead of restrict it against the main enemy house it is done against all enemies + if (!pTrigger->ConditionObject) + continue; + + std::vector list; + list.push_back(pTrigger->ConditionObject); + bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, nullptr, false, list); + + if (!isConditionMet) + continue; + } + else if ((int)pTrigger->ConditionType == 9) + { + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) + { + // New case 9: Like in case 0 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the enemy. + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); + bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, targetHouse, false, list); + + if (!isConditionMet) + continue; + } + } + else if ((int)pTrigger->ConditionType == 10) + { + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) + { + // New case 10: Like in case 1 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the house. + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); + bool isConditionMet = TeamExt::HouseOwns(pTrigger, pHouse, false, list); + + if (!isConditionMet) + continue; + } + } + else if ((int)pTrigger->ConditionType == 11) + { + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) + { + // New case 11: Like in case 7 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the Civilians. + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); + bool isConditionMet = TeamExt::NeutralOwns(pTrigger, list); + + if (!isConditionMet) + continue; + } + } + else if ((int)pTrigger->ConditionType == 12) + { + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) + { + // New case 12: Like in case 0 & 9 but instead of a specific enemy this checks in all enemies. + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); + bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, nullptr, false, list); + + if (!isConditionMet) + continue; + } + } + else if ((int)pTrigger->ConditionType == 13) + { + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) + { + // New case 13: Like in case 1 & 10 but instead checking the house now checks the allies. + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); + bool isConditionMet = TeamExt::HouseOwns(pTrigger, pHouse, true, list); + + if (!isConditionMet) + continue; + } + } + else if ((int)pTrigger->ConditionType == 14) + { + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) + { + // New case 14: Like in case 9 but instead of meet any comparison now is required all. + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); + bool isConditionMet = TeamExt::EnemyOwnsAll(pTrigger, pHouse, targetHouse, list); + + if (!isConditionMet) + continue; + } + } + else if ((int)pTrigger->ConditionType == 15) + { + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) + { + // New case 15: Like in case 10 but instead of meet any comparison now is required all. + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); + bool isConditionMet = TeamExt::HouseOwnsAll(pTrigger, pHouse, list); + + if (!isConditionMet) + continue; + } + } + else if ((int)pTrigger->ConditionType == 16) + { + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) + { + // New case 16: Like in case 11 but instead of meet any comparison now is required all. + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); + bool isConditionMet = TeamExt::NeutralOwnsAll(pTrigger, list); + + if (!isConditionMet) + continue; + } + } + else if ((int)pTrigger->ConditionType == 17) + { + if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) + { + // New case 17: Like in case 14 but instead of meet any comparison now is required all. Check all enemies + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); + bool isConditionMet = TeamExt::EnemyOwnsAll(pTrigger, pHouse, nullptr, list); + + if (!isConditionMet) + continue; + } + } + else if ((int)pTrigger->ConditionType == 18) + { + // New case 18: Check destroyed bridges + bool isConditionMet = TeamExt::CountConditionMet(pTrigger, destroyedBridgesCount); + + if (!isConditionMet) + continue; + } + else if ((int)pTrigger->ConditionType == 19) + { + // New case 19: Check undamaged bridges + bool isConditionMet = TeamExt::CountConditionMet(pTrigger, undamagedBridgesCount); + + if (!isConditionMet) + continue; + } + else + { + // Other cases from vanilla game + if (!pTrigger->ConditionMet(pHouse, targetHouse, hasReachedMaxDefensiveTeamsLimit)) + continue; + } + } + + // All triggers below 5000 in current weight will get discarded if this mode is enabled + if (onlyCheckImportantTriggers) + { + if (pTrigger->Weight_Current < 5000) + continue; + } + + auto pTriggerTeam1Type = pTrigger->Team1; + if (!pTriggerTeam1Type) + continue; + + // No more defensive teams needed + if (pTriggerTeam1Type->IsBaseDefense && hasReachedMaxDefensiveTeamsLimit) + continue; + + // If this type of Team reached the max then skip it + int count = 0; + + for (auto team : activeTeamsList) + { + if (team->Type == pTriggerTeam1Type) + count++; + } + + if (count >= pTriggerTeam1Type->Max) + continue; + + teamCategory teamIsCategory = teamCategory::None; + + // Analyze what kind of category is this main team if the feature is enabled + if (splitTriggersByCategory) + { + //Debug::Log("DEBUG: TaskForce [%s] members:\n", pTriggerTeam1Type->TaskForce->ID); + // TaskForces are limited to 6 entries + for (int i = 0; i < 6; i++) + { + auto entry = pTriggerTeam1Type->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; + //Debug::Log("\t[%s](%d) is in AIR category.\n", entry.Type->ID, entry.Amount); + } + else + { + auto pTechnoTypeExt = TechnoTypeExt::ExtMap.Find(entry.Type); + if (!pTechnoTypeExt) + continue; + + if (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; + //Debug::Log("\t[%s](%d) is in NAVAL category.\n", entry.Type->ID, entry.Amount); + } + + if (pTechnoTypeExt->ConsideredVehicle + || (entryIsCategory != teamCategory::Naval + && entryIsCategory != teamCategory::Air)) + { + // This unit is from ground category + entryIsCategory = teamCategory::Ground; + //Debug::Log("\t[%s](%d) is in GROUND category.\n", entry.Type->ID, entry.Amount); + } + } + + // if a team have multiple categories it will be a mixed category + teamIsCategory = teamIsCategory == teamCategory::None || teamIsCategory == entryIsCategory ? entryIsCategory : teamCategory::Unclassified; + + if (teamIsCategory == teamCategory::Unclassified) + break; + } + else + { + break; + } + } + + //Debug::Log("DEBUG: This team is a category %d (1:Ground, 2:Air, 3:Naval, 4:Mixed).\n", teamIsCategory); + // Si existe este valor y el team es MIXTO se sobreescribe el tipo de categoría + if (teamIsCategory == teamCategory::Unclassified + && mergeUnclassifiedCategoryWith >= 0) + { + //Debug::Log("DEBUG: MIXED category forced to work as category %d.\n", mergeUnclassifiedCategoryWith); + teamIsCategory = (teamCategory)mergeUnclassifiedCategoryWith; + } + } + + bool allObjectsCanBeBuiltOrRecruited = true; + + if (pTriggerTeam1Type->Autocreate) + { + for (auto entry : pTriggerTeam1Type->TaskForce->Entries) + { + // Check if each unit in the taskforce meets the structure prerequisites + if (entry.Amount > 0) + { + if (!entry.Type) + continue; + + TechnoTypeClass* object = entry.Type; + bool canBeBuilt = HouseExt::PrerequisitesMet(pHouse, object, ownedBuildings, false); + + if (!canBeBuilt) + { + allObjectsCanBeBuiltOrRecruited = false; + break; + } + } + else + { + break; + } + } + } + else + { + allObjectsCanBeBuiltOrRecruited = false; + } + + if (!allObjectsCanBeBuiltOrRecruited && pTriggerTeam1Type->Recruiter) + { + allObjectsCanBeBuiltOrRecruited = true; + + for (auto entry : pTriggerTeam1Type->TaskForce->Entries) + { + // Check if each unit in the taskforce has the available recruitable units in the map + if (allObjectsCanBeBuiltOrRecruited && entry.Type && entry.Amount > 0) + { + if (ownedRecruitables.count(entry.Type) < entry.Amount) + { + allObjectsCanBeBuiltOrRecruited = false; + break; + } + } + } + } + + // We can't let AI cheat in this trigger because doesn't have the required tech tree available + if (!allObjectsCanBeBuiltOrRecruited) + continue; + + // Special case: triggers become very important if they reach the max priority (value 5000). + // They get stored in a elitist list and all previous triggers are discarded + if (pTrigger->Weight_Current >= 5000 && !onlyCheckImportantTriggers) + { + // First time only + if (validTriggerCandidates.Count > 0) + { + validTriggerCandidates.Clear(); + validTriggerCandidatesGroundOnly.Clear(); + validTriggerCandidatesNavalOnly.Clear(); + validTriggerCandidatesAirOnly.Clear(); + validTriggerCandidatesUnclassifiedOnly.Clear(); + validCategory = teamCategory::None; + } + + // Reset the current ones and now only will be added important triggers to the list + onlyCheckImportantTriggers = true; + totalWeight = 0.0; + splitTriggersByCategory = false; // VIP teams breaks the categories logic (on purpose) + } + + // Passed all checks, save this trigger for later. + // The idea behind this is to simulate an ordered list of weights and once we throw the dice we'll know the winner trigger: More weight means more possibilities to be selected. + totalWeight += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; + TriggerElementWeight item; + item.Trigger = pTrigger; + item.Weight = totalWeight; + item.Category = teamIsCategory; + + validTriggerCandidates.AddItem(item); + + if (splitTriggersByCategory) + { + TriggerElementWeight itemGroundOnly; + TriggerElementWeight itemAirOnly; + TriggerElementWeight itemNavalOnly; + TriggerElementWeight itemUnclassifiedOnly; + + switch (teamIsCategory) + { + case teamCategory::Ground: + totalWeightGroundOnly += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; + + itemGroundOnly.Trigger = pTrigger; + itemGroundOnly.Weight = totalWeightGroundOnly; + itemGroundOnly.Category = teamIsCategory; + + validTriggerCandidatesGroundOnly.AddItem(itemGroundOnly); + totalGroundCategoryTriggers++; + break; + + case teamCategory::Air: + totalWeightAirOnly += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; + + itemAirOnly.Trigger = pTrigger; + itemAirOnly.Weight = totalWeightAirOnly; + itemAirOnly.Category = teamIsCategory; + + validTriggerCandidatesAirOnly.AddItem(itemAirOnly); + totalAirCategoryTriggers++; + break; + + case teamCategory::Naval: + totalWeightNavalOnly += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; + + itemNavalOnly.Trigger = pTrigger; + itemNavalOnly.Weight = totalWeightNavalOnly; + itemNavalOnly.Category = teamIsCategory; + + validTriggerCandidatesNavalOnly.AddItem(itemNavalOnly); + totalNavalCategoryTriggers++; + break; + + case teamCategory::Unclassified: + totalWeightUnclassifiedOnly += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; + + itemUnclassifiedOnly.Trigger = pTrigger; + itemUnclassifiedOnly.Weight = totalWeightUnclassifiedOnly; + itemUnclassifiedOnly.Category = teamIsCategory; + + validTriggerCandidatesUnclassifiedOnly.AddItem(itemUnclassifiedOnly); + totalUnclassifiedCategoryTriggers++; + break; + + default: + break; + } + } + } + } + } + + if (splitTriggersByCategory) + { + switch (validCategory) + { + case teamCategory::Ground: + Debug::Log("DEBUG: This time only will be picked GROUND teams.\n"); + break; + + case teamCategory::Unclassified: + Debug::Log("DEBUG: This time only will be picked MIXED teams.\n"); + break; + + case teamCategory::Naval: + Debug::Log("DEBUG: This time only will be picked NAVAL teams.\n"); + break; + + case teamCategory::Air: + Debug::Log("DEBUG: This time only will be picked AIR teams.\n"); + break; + + default: + Debug::Log("DEBUG: This time teams categories are DISABLED.\n"); + break; + } + } + + if (validTriggerCandidates.Count == 0) + { + Debug::Log("DEBUG: [%s] (idx: %d) No valid triggers for now. A new attempt will be done later...\n", pHouse->Type->ID, pHouse->ArrayIndex); + return SkipCode; + } + + if ((validCategory == teamCategory::Ground && totalGroundCategoryTriggers == 0) + || (validCategory == teamCategory::Unclassified && totalUnclassifiedCategoryTriggers == 0) + || (validCategory == teamCategory::Air && totalAirCategoryTriggers == 0) + || (validCategory == teamCategory::Naval && totalNavalCategoryTriggers == 0)) + { + Debug::Log("DEBUG: [%s] (idx: %d) No valid triggers of this category. A new attempt should be done later...\n", pHouse->Type->ID, pHouse->ArrayIndex); + + if (!isFallbackEnabled) + return SkipCode; + + Debug::Log("... but fallback mode is enabled so now will be checked all available triggers.\n"); + validCategory = teamCategory::None; + } + + AITriggerTypeClass* selectedTrigger = nullptr; + double weightDice = 0.0; + double lastWeight = 0.0; + bool found = false; + + switch (validCategory) + { + case teamCategory::None: + weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeight) * 1.0; + /*Debug::Log("Weight Dice: %f\n", weightDice); + + // Debug + Debug::Log("DEBUG: Candidate AI triggers list:\n"); + for (TriggerElementWeight element : validTriggerCandidates) + { + Debug::Log("Weight: %f, [%s][%s]: %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Team1->Name); + }*/ + + for (auto element : validTriggerCandidates) + { + lastWeight = element.Weight; + + if (weightDice < element.Weight && !found) + { + selectedTrigger = element.Trigger; + found = true; + } + } + break; + + case teamCategory::Ground: + weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightGroundOnly) * 1.0; + /*Debug::Log("Weight Dice: %f\n", weightDice); + + // Debug + Debug::Log("DEBUG: Candidate AI triggers list:\n"); + for (TriggerElementWeight element : validTriggerCandidatesGroundOnly) + { + Debug::Log("Weight: %f, [%s][%s]: %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Team1->Name); + }*/ + + for (auto element : validTriggerCandidatesGroundOnly) + { + lastWeight = element.Weight; + + if (weightDice < element.Weight && !found) + { + selectedTrigger = element.Trigger; + found = true; + } + } + break; + + case teamCategory::Unclassified: + weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightUnclassifiedOnly) * 1.0; + /*Debug::Log("Weight Dice: %f\n", weightDice); + + // Debug + Debug::Log("DEBUG: Candidate AI triggers list:\n"); + for (TriggerElementWeight element : validTriggerCandidatesUnclassifiedOnly) + { + Debug::Log("Weight: %f, [%s][%s]: %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Team1->Name); + }*/ + + for (auto element : validTriggerCandidatesUnclassifiedOnly) + { + lastWeight = element.Weight; + + if (weightDice < element.Weight && !found) + { + selectedTrigger = element.Trigger; + found = true; + } + } + break; + + case teamCategory::Naval: + weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightNavalOnly) * 1.0; + /*Debug::Log("Weight Dice: %f\n", weightDice); + + // Debug + Debug::Log("DEBUG: Candidate AI triggers list:\n"); + for (TriggerElementWeight element : validTriggerCandidatesNavalOnly) + { + Debug::Log("Weight: %f, [%s][%s]: %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Team1->Name); + }*/ + + for (auto element : validTriggerCandidatesNavalOnly) + { + lastWeight = element.Weight; + + if (weightDice < element.Weight && !found) + { + selectedTrigger = element.Trigger; + found = true; + } + } + break; + + case teamCategory::Air: + weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightAirOnly) * 1.0; + /*Debug::Log("Weight Dice: %f\n", weightDice); + + // Debug + Debug::Log("DEBUG: Candidate AI triggers list:\n"); + for (TriggerElementWeight element : validTriggerCandidatesAirOnly) + { + Debug::Log("Weight: %f, [%s][%s]: %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Team1->Name); + }*/ + + for (auto element : validTriggerCandidatesAirOnly) + { + lastWeight = element.Weight; + + if (weightDice < element.Weight && !found) + { + selectedTrigger = element.Trigger; + found = true; + } + } + break; + + default: + break; + } + + if (!selectedTrigger) + { + Debug::Log("AI Team Selector: House [%s] (idx: %d) failed to select Trigger. A new attempt Will be done later...\n", pHouse->Type->ID, pHouse->ArrayIndex); + return SkipCode; + } + + if (selectedTrigger->Weight_Current >= 5000.0 + && selectedTrigger->Weight_Minimum <= 4999.0) + { + // Next time this trigger will be out of the elitist triggers list + selectedTrigger->Weight_Current = 4999.0; + } + + // We have a winner trigger here + Debug::Log("AI Team Selector: House [%s] (idx: %d) selected trigger [%s].\n", pHouse->Type->ID, pHouse->ArrayIndex, selectedTrigger->ID); + + // Team 1 creation + auto pTriggerTeam1Type = selectedTrigger->Team1; + if (pTriggerTeam1Type) + { + int count = 0; + + for (auto team : activeTeamsList) + { + if (team->Type == pTriggerTeam1Type) + count++; + } + + if (count < pTriggerTeam1Type->Max) + { + if (TeamClass* newTeam1 = pTriggerTeam1Type->CreateTeam(pHouse)) + newTeam1->NeedsToDisappear = false; + } + } + + // Team 2 creation (if set) + auto pTriggerTeam2Type = selectedTrigger->Team2; + if (pTriggerTeam2Type) + { + int count = 0; + + for (auto team : activeTeamsList) + { + if (team->Type == pTriggerTeam2Type) + count++; + } + + if (count < pTriggerTeam2Type->Max) + { + if (TeamClass* newTeam2 = pTriggerTeam2Type->CreateTeam(pHouse)) + newTeam2->NeedsToDisappear = false; + } + } + } + + return SkipCode; +} + +bool TeamExt::HouseOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, bool allies, std::vector list) +{ + bool result = false; + int counter = 0; + + // Count all objects of the list, like an OR operator + for (auto pItem : list) + { + for (auto pObject : *TechnoClass::Array) + { + if (pObject + && pObject->IsAlive + && pObject->Health > 0 + && ((!allies && pObject->Owner == pHouse) || (allies && pHouse != pObject->Owner && pHouse->IsAlliedWith(pObject->Owner))) + && !pObject->Owner->Type->MultiplayPassive + && pObject->GetTechnoType() == pItem) + { + counter++; + } + } + } + + switch (pThis->Conditions->ComparatorOperand) + { + case 0: + result = counter < pThis->Conditions->ComparatorType; + break; + case 1: + result = counter <= pThis->Conditions->ComparatorType; + break; + case 2: + result = counter == pThis->Conditions->ComparatorType; + break; + case 3: + result = counter >= pThis->Conditions->ComparatorType; + break; + case 4: + result = counter > pThis->Conditions->ComparatorType; + break; + case 5: + result = counter != pThis->Conditions->ComparatorType; + break; + default: + break; + } + + return result; +} + +bool TeamExt::EnemyOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, bool onlySelectedEnemy, std::vector list) +{ + bool result = false; + int counter = 0; + + if (pEnemy && pHouse->IsAlliedWith(pEnemy) && !onlySelectedEnemy) + pEnemy = nullptr; + + // Count all objects of the list, like an OR operator + for (auto pItem : list) + { + for (auto pObject : *TechnoClass::Array) + { + if (pObject + && pObject->IsAlive + && pObject->Health > 0 + && pObject->Owner != pHouse + && (!pEnemy || (pEnemy && !pHouse->IsAlliedWith(pEnemy))) + && !pObject->Owner->Type->MultiplayPassive + && pObject->GetTechnoType() == pItem) + { + counter++; + } + } + } + + switch (pThis->Conditions->ComparatorOperand) + { + case 0: + result = counter < pThis->Conditions->ComparatorType; + break; + case 1: + result = counter <= pThis->Conditions->ComparatorType; + break; + case 2: + result = counter == pThis->Conditions->ComparatorType; + break; + case 3: + result = counter >= pThis->Conditions->ComparatorType; + break; + case 4: + result = counter > pThis->Conditions->ComparatorType; + break; + case 5: + result = counter != pThis->Conditions->ComparatorType; + break; + default: + break; + } + + return result; +} + +bool TeamExt::NeutralOwns(AITriggerTypeClass* pThis, std::vector list) +{ + bool result = false; + int counter = 0; + + for (auto pHouse : *HouseClass::Array) + { + if (_stricmp(SideClass::Array->GetItem(pHouse->Type->SideIndex)->Name, "Civilian") != 0) + continue; + + // Count all objects of the list, like an OR operator + for (auto pItem : list) + { + for (auto pObject : *TechnoClass::Array) + { + if (pObject + && pObject->IsAlive + && pObject->Health > 0 + && pObject->Owner == pHouse + && pObject->GetTechnoType() == pItem) + { + counter++; + } + } + } + } + + switch (pThis->Conditions->ComparatorOperand) + { + case 0: + result = counter < pThis->Conditions->ComparatorType; + break; + case 1: + result = counter <= pThis->Conditions->ComparatorType; + break; + case 2: + result = counter == pThis->Conditions->ComparatorType; + break; + case 3: + result = counter >= pThis->Conditions->ComparatorType; + break; + case 4: + result = counter > pThis->Conditions->ComparatorType; + break; + case 5: + result = counter != pThis->Conditions->ComparatorType; + break; + default: + break; + } + + return result; +} + +bool TeamExt::HouseOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, std::vector list) +{ + bool result = true; + + if (list.size() == 0) + return false; + + // Count all objects of the list, like an AND operator + for (auto pItem : list) + { + if (!result) + break; + + int counter = 0; + result = true; + + for (auto pObject : *TechnoClass::Array) + { + if (pObject && + pObject->IsAlive && + pObject->Health > 0 && + pObject->Owner == pHouse && + pObject->GetTechnoType() == pItem) + { + counter++; + } + } + + switch (pThis->Conditions->ComparatorOperand) + { + case 0: + result = counter < pThis->Conditions->ComparatorType; + break; + case 1: + result = counter <= pThis->Conditions->ComparatorType; + break; + case 2: + result = counter == pThis->Conditions->ComparatorType; + break; + case 3: + result = counter >= pThis->Conditions->ComparatorType; + break; + case 4: + result = counter > pThis->Conditions->ComparatorType; + break; + case 5: + result = counter != pThis->Conditions->ComparatorType; + break; + default: + break; + } + } + + return result; +} + +bool TeamExt::EnemyOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, std::vector list) +{ + bool result = true; + + if (pEnemy && pHouse->IsAlliedWith(pEnemy)) + pEnemy = nullptr; + + if (list.size() == 0) + return false; + + // Count all objects of the list, like an AND operator + for (auto pItem : list) + { + if (!result) + break; + + int counter = 0; + result = true; + + for (auto pObject : *TechnoClass::Array) + { + if (pObject + && pObject->IsAlive + && pObject->Health > 0 + && pObject->Owner != pHouse + && (!pEnemy || (pEnemy && !pHouse->IsAlliedWith(pEnemy))) + && !pObject->Owner->Type->MultiplayPassive + && pObject->GetTechnoType() == pItem) + { + counter++; + } + } + + switch (pThis->Conditions->ComparatorOperand) + { + case 0: + result = counter < pThis->Conditions->ComparatorType; + break; + case 1: + result = counter <= pThis->Conditions->ComparatorType; + break; + case 2: + result = counter == pThis->Conditions->ComparatorType; + break; + case 3: + result = counter >= pThis->Conditions->ComparatorType; + break; + case 4: + result = counter > pThis->Conditions->ComparatorType; + break; + case 5: + result = counter != pThis->Conditions->ComparatorType; + break; + default: + break; + } + } + + return result; +} + +bool TeamExt::NeutralOwnsAll(AITriggerTypeClass* pThis, std::vector list) +{ + bool result = true; + + if (list.size() == 0) + return false; + + // Any neutral house should be capable to meet the prerequisites + for (auto pHouse : *HouseClass::Array) + { + if (!result) + break; + + bool foundAll = true; + + if (_stricmp(SideClass::Array->GetItem(pHouse->Type->SideIndex)->Name, "Civilian") != 0) + continue; + + // Count all objects of the list, like an AND operator + for (auto pItem : list) + { + if (!foundAll) + break; + + int counter = 0; + + for (auto pObject : *TechnoClass::Array) + { + if (pObject && + pObject->IsAlive && + pObject->Health > 0 && + pObject->Owner == pHouse && + pObject->GetTechnoType() == pItem) + { + counter++; + } + } + + switch (pThis->Conditions->ComparatorOperand) + { + case 0: + foundAll = counter < pThis->Conditions->ComparatorType; + break; + case 1: + foundAll = counter <= pThis->Conditions->ComparatorType; + break; + case 2: + foundAll = counter == pThis->Conditions->ComparatorType; + break; + case 3: + foundAll = counter >= pThis->Conditions->ComparatorType; + break; + case 4: + foundAll = counter > pThis->Conditions->ComparatorType; + break; + case 5: + foundAll = counter != pThis->Conditions->ComparatorType; + break; + default: + break; + } + } + + if (!foundAll) + result = false; + } + + return result; +} + +bool TeamExt::CountConditionMet(AITriggerTypeClass* pThis, int nObjects) +{ + bool result = true; + + if (nObjects < 0) + return false; + + switch (pThis->Conditions->ComparatorOperand) + { + case 0: + result = nObjects < pThis->Conditions->ComparatorType; + break; + case 1: + result = nObjects <= pThis->Conditions->ComparatorType; + break; + case 2: + result = nObjects == pThis->Conditions->ComparatorType; + break; + case 3: + result = nObjects >= pThis->Conditions->ComparatorType; + break; + case 4: + result = nObjects > pThis->Conditions->ComparatorType; + break; + case 5: + result = nObjects != pThis->Conditions->ComparatorType; + break; + default: + break; + } + + return result; +} diff --git a/src/Ext/Team/Body.cpp b/src/Ext/Team/Body.cpp index b809226c31..666d08e666 100644 --- a/src/Ext/Team/Body.cpp +++ b/src/Ext/Team/Body.cpp @@ -93,1322 +93,3 @@ DEFINE_HOOK(0x6EC55A, TeamClass_Save_Suffix, 0x5) TeamExt::ExtMap.SaveStatic(); return 0; } - -DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) -{ - enum { UseOriginalSelector = 0x4F8A63, SkipCode = 0x4F8B08 }; - - GET(HouseClass*, pHouse, ESI); - - bool houseIsHuman = pHouse->IsHumanPlayer; - - if (SessionClass::IsCampaign()) - houseIsHuman = pHouse->IsHumanPlayer || pHouse->IsInPlayerControl; - - if (houseIsHuman || pHouse->Type->MultiplayPassive) - return SkipCode; - - auto pHouseTypeExt = HouseTypeExt::ExtMap.Find(pHouse->Type); - if (!pHouseTypeExt) - return SkipCode; - - if (!RulesExt::Global()->NewTeamsSelector) - return UseOriginalSelector; - - // Reset Team selection countdown - int countdown = RulesClass::Instance->TeamDelays[(int)pHouse->AIDifficulty]; - pHouse->TeamDelayTimer.Start(countdown); - - int totalActiveTeams = 0; - int activeTeams = 0; - - int totalGroundCategoryTriggers = 0; - int totalUnclassifiedCategoryTriggers = 0; - int totalNavalCategoryTriggers = 0; - int totalAirCategoryTriggers = 0; - - DynamicVectorClass validTriggerCandidates; - DynamicVectorClass validTriggerCandidatesGroundOnly; - DynamicVectorClass validTriggerCandidatesNavalOnly; - DynamicVectorClass validTriggerCandidatesAirOnly; - DynamicVectorClass validTriggerCandidatesUnclassifiedOnly; - - int dice = ScenarioClass::Instance->Random.RandomRanged(1, 100); - - // This house must have the triggers enabled - if (dice <= pHouse->RatioAITriggerTeam && pHouse->AITriggersActive) - { - 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; - - 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 - 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 - - // 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; - - default: - break; - } - - percentageUnclassifiedTriggers = 0.0; - } - - percentageUnclassifiedTriggers = percentageUnclassifiedTriggers < 0.0 || percentageUnclassifiedTriggers > 1.0 ? 0.0 : percentageUnclassifiedTriggers; - percentageGroundTriggers = percentageGroundTriggers < 0.0 || percentageGroundTriggers > 1.0 ? 0.0 : percentageGroundTriggers; - percentageNavalTriggers = percentageNavalTriggers < 0.0 || percentageNavalTriggers > 1.0 ? 0.0 : percentageNavalTriggers; - percentageAirTriggers = percentageAirTriggers < 0.0 || percentageAirTriggers > 1.0 ? 0.0 : percentageAirTriggers; - - double totalPercengates = percentageUnclassifiedTriggers + percentageGroundTriggers + percentageNavalTriggers + percentageAirTriggers; - if (totalPercengates > 1.0 || totalPercengates <= 0.0) - splitTriggersByCategory = false; - - - if (splitTriggersByCategory) - { - int categoryDice = ScenarioClass::Instance->Random.RandomRanged(1, 100); - int unclassifiedValue = (int)(percentageUnclassifiedTriggers * 100.0); - int groundValue = (int)(percentageGroundTriggers * 100.0); - int airValue = (int)(percentageAirTriggers * 100.0); - int navalValue = (int)(percentageNavalTriggers * 100.0); - - // Pick what type of team will be selected in this round - if (percentageUnclassifiedTriggers > 0.0 && categoryDice <= unclassifiedValue) - { - validCategory = teamCategory::Unclassified; - Debug::Log("New AI team category selection: dice %d <= %d (MIXED)\n", categoryDice, unclassifiedValue); - } - else if (percentageGroundTriggers > 0.0 && categoryDice <= (unclassifiedValue + groundValue)) - { - validCategory = teamCategory::Ground; - Debug::Log("New AI team category selection: dice %d <= %d (mixed: %d%% + GROUND: %d%%)\n", categoryDice, (unclassifiedValue + groundValue), unclassifiedValue, groundValue); - } - else if (percentageAirTriggers > 0.0 && categoryDice <= (unclassifiedValue + groundValue + airValue)) - { - validCategory = teamCategory::Air; - Debug::Log("New AI team category selection: dice %d <= %d (mixed: %d%% + ground: %d%% + AIR: %d%%)\n", categoryDice, (unclassifiedValue + groundValue + airValue), unclassifiedValue, groundValue, airValue); - } - else if (percentageNavalTriggers > 0.0 && categoryDice <= (unclassifiedValue + groundValue + airValue + navalValue)) - { - validCategory = teamCategory::Naval; - Debug::Log("New AI team category selection: dice %d <= %d (mixed: %d%% + ground: %d%% + air: %d%% + NAVAL: %d%%)\n", categoryDice, (unclassifiedValue + groundValue + airValue + navalValue), unclassifiedValue, groundValue, airValue, navalValue); - } - else - { - // If the sum of all percentages is less than 100% then that empty space will work like "no categories" - splitTriggersByCategory = false; - } - } - } - - int parentCountryTypeIdx = pHouse->Type->FindParentCountryIndex(); // ParentCountry can change the House in a SP map - int houseTypeIdx = parentCountryTypeIdx >= 0 ? parentCountryTypeIdx : pHouse->Type->ArrayIndex; // Indexes in AITriggers section are 1-based - int houseIdx = pHouse->ArrayIndex; - - int parentCountrySideTypeIdx = pHouse->Type->FindParentCountry()->SideIndex; - int sideTypeIdx = parentCountrySideTypeIdx >= 0 ? parentCountrySideTypeIdx + 1 : pHouse->Type->SideIndex + 1; // Side indexes in AITriggers section are 1-based - int sideIdx = pHouse->SideIndex + 1; // Side indexes in AITriggers section are 1-based - - auto houseDifficulty = pHouse->AIDifficulty; - int maxBaseDefenseTeams = RulesClass::Instance->MaximumAIDefensiveTeams.GetItem((int)houseDifficulty); - int activeDefenseTeamsCount = 0; - int maxTeamsLimit = RulesClass::Instance->TotalAITeamCap.GetItem((int)houseDifficulty); - double totalWeight = 0.0; - double totalWeightGroundOnly = 0.0; - double totalWeightNavalOnly = 0.0; - double totalWeightAirOnly = 0.0; - double totalWeightUnclassifiedOnly = 0.0; - - // Check if the running teams by the house already reached all the limits - DynamicVectorClass activeTeamsList; - - for (auto const pRunningTeam : *TeamClass::Array) - { - totalActiveTeams++; - int teamHouseIdx = pRunningTeam->Owner->ArrayIndex; - - if (teamHouseIdx != houseIdx) - continue; - - activeTeamsList.AddItem(pRunningTeam); - - if (pRunningTeam->Type->IsBaseDefense && activeDefenseTeamsCount < maxBaseDefenseTeams) - activeDefenseTeamsCount++; - } - - activeTeams = activeTeamsList.Count; - - /*Debug::Log("=====================\n[%s] ACTIVE TEAMS: %d / %d\n", pHouse->Type->ID, activeTeams, maxTeamsLimit); - for (auto team : activeTeamsList) - { - Debug::Log("[%s](%d) : %s\n", team->Type->ID, team->TotalObjects, team->Type->Name); - Debug::Log(" IsMoving: %d, IsFullStrength: %d, IsUnderStrength: %d\n", team->IsMoving, team->IsFullStrength, team->IsUnderStrength); - int i = 0; - - for (auto entry : team->Type->TaskForce->Entries) - { - if (entry.Type && entry.Amount > 0) - { - if (entry.Type) - Debug::Log("\t[%s]: %d / %d\n", entry.Type->ID, team->CountObjects[i], entry.Amount); - } - - i++; - } - } - Debug::Log("=====================\n");*/ - - // We will use these values for discarding triggers - bool hasReachedMaxTeamsLimit = activeTeams < maxTeamsLimit ? false : true; - bool hasReachedMaxDefensiveTeamsLimit = activeDefenseTeamsCount < maxBaseDefenseTeams ? false : true; - - if (hasReachedMaxDefensiveTeamsLimit) - Debug::Log("DEBUG: House [%s] (idx: %d) reached the MaximumAIDefensiveTeams value!\n", pHouse->Type->ID, pHouse->ArrayIndex); - - if (hasReachedMaxTeamsLimit) - { - Debug::Log("DEBUG: House [%s] (idx: %d) reached the TotalAITeamCap value!\n", pHouse->Type->ID, pHouse->ArrayIndex); - return SkipCode; - } - - int destroyedBridgesCount = 0; - int undamagedBridgesCount = 0; - std::map ownedRecruitables; - std::map ownedBuildings; - - for (auto pTechno : *TechnoClass::Array) - { - if (pTechno->WhatAmI() == AbstractType::Building) - { - if (pTechno->Owner == pHouse) - { - ++ownedBuildings[static_cast(pTechno->GetTechnoType())]; - } - else - { - auto pBuilding = static_cast(pTechno); - if (pBuilding && pBuilding->Type->BridgeRepairHut) - { - CellStruct cell = pTechno->GetCell()->MapCoords; - - if (MapClass::Instance->IsLinkedBridgeDestroyed(cell)) - destroyedBridgesCount++; - else - undamagedBridgesCount++; - } - } - - continue; - } - - auto* pFoot = static_cast(pTechno); - - if (!pFoot - || !pTechno->IsAlive - || pTechno->Health <= 0 - || !pTechno->IsOnMap // Note: underground movement is considered "IsOnMap == false" - || pTechno->Transporter - || pTechno->Absorbed - || !pFoot->CanBeRecruited(pHouse)) - { - continue; - } - - ++ownedRecruitables[pTechno->GetTechnoType()]; - } - - HouseClass* targetHouse = nullptr; - if (pHouse->EnemyHouseIndex >= 0) - targetHouse = HouseClass::Array->GetItem(pHouse->EnemyHouseIndex); - - bool onlyCheckImportantTriggers = false; - - // Gather all the trigger candidates into one place for posterior fast calculations - for (auto const pTrigger : *AITriggerTypeClass::Array) - { - if (!pTrigger) - continue; - - int triggerHouse = pTrigger->HouseIndex; - int triggerSide = pTrigger->SideIndex; - - // Ignore the deactivated triggers - if (pTrigger->IsEnabled) - { - // The trigger must be compatible with the owner - if ((triggerHouse == -1 || houseTypeIdx == triggerHouse) && (triggerSide == 0 || sideTypeIdx == triggerSide)) - { - // "ConditionType=-1" will be skipped, always is valid - if ((int)pTrigger->ConditionType >= 0) - { - if ((int)pTrigger->ConditionType == 0) - { - // Simulate case 0: "enemy owns" - if (!pTrigger->ConditionObject) - continue; - - std::vector list; - list.push_back(pTrigger->ConditionObject); - bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, targetHouse, true, list); - - if (!isConditionMet) - continue; - } - else if ((int)pTrigger->ConditionType == 1) - { - // Simulate case 1: "house owns" - if (!pTrigger->ConditionObject) - continue; - - std::vector list; - list.push_back(pTrigger->ConditionObject); - bool isConditionMet = TeamExt::HouseOwns(pTrigger, pHouse, false, list); - - if (!isConditionMet) - continue; - } - else if ((int)pTrigger->ConditionType == 7) - { - // Simulate case 7: "civilian owns" - if (!pTrigger->ConditionObject) - continue; - - std::vector list; - list.push_back(pTrigger->ConditionObject); - bool isConditionMet = TeamExt::NeutralOwns(pTrigger, list); - - if (!isConditionMet) - continue; - } - else if ((int)pTrigger->ConditionType == 8) - { - // Simulate case 0: "enemy owns" but instead of restrict it against the main enemy house it is done against all enemies - if (!pTrigger->ConditionObject) - continue; - - std::vector list; - list.push_back(pTrigger->ConditionObject); - bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, nullptr, false, list); - - if (!isConditionMet) - continue; - } - else if ((int)pTrigger->ConditionType == 9) - { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) - { - // New case 9: Like in case 0 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the enemy. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); - bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, targetHouse, false, list); - - if (!isConditionMet) - continue; - } - } - else if ((int)pTrigger->ConditionType == 10) - { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) - { - // New case 10: Like in case 1 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the house. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); - bool isConditionMet = TeamExt::HouseOwns(pTrigger, pHouse, false, list); - - if (!isConditionMet) - continue; - } - } - else if ((int)pTrigger->ConditionType == 11) - { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) - { - // New case 11: Like in case 7 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the Civilians. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); - bool isConditionMet = TeamExt::NeutralOwns(pTrigger, list); - - if (!isConditionMet) - continue; - } - } - else if ((int)pTrigger->ConditionType == 12) - { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) - { - // New case 12: Like in case 0 & 9 but instead of a specific enemy this checks in all enemies. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); - bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, nullptr, false, list); - - if (!isConditionMet) - continue; - } - } - else if ((int)pTrigger->ConditionType == 13) - { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) - { - // New case 13: Like in case 1 & 10 but instead checking the house now checks the allies. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); - bool isConditionMet = TeamExt::HouseOwns(pTrigger, pHouse, true, list); - - if (!isConditionMet) - continue; - } - } - else if ((int)pTrigger->ConditionType == 14) - { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) - { - // New case 14: Like in case 9 but instead of meet any comparison now is required all. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); - bool isConditionMet = TeamExt::EnemyOwnsAll(pTrigger, pHouse, targetHouse, list); - - if (!isConditionMet) - continue; - } - } - else if ((int)pTrigger->ConditionType == 15) - { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) - { - // New case 15: Like in case 10 but instead of meet any comparison now is required all. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); - bool isConditionMet = TeamExt::HouseOwnsAll(pTrigger, pHouse, list); - - if (!isConditionMet) - continue; - } - } - else if ((int)pTrigger->ConditionType == 16) - { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) - { - // New case 16: Like in case 11 but instead of meet any comparison now is required all. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); - bool isConditionMet = TeamExt::NeutralOwnsAll(pTrigger, list); - - if (!isConditionMet) - continue; - } - } - else if ((int)pTrigger->ConditionType == 17) - { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) - { - // New case 17: Like in case 14 but instead of meet any comparison now is required all. Check all enemies - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); - bool isConditionMet = TeamExt::EnemyOwnsAll(pTrigger, pHouse, nullptr, list); - - if (!isConditionMet) - continue; - } - } - else if ((int)pTrigger->ConditionType == 18) - { - // New case 18: Check destroyed bridges - bool isConditionMet = TeamExt::CountConditionMet(pTrigger, destroyedBridgesCount); - - if (!isConditionMet) - continue; - } - else if ((int)pTrigger->ConditionType == 19) - { - // New case 19: Check undamaged bridges - bool isConditionMet = TeamExt::CountConditionMet(pTrigger, undamagedBridgesCount); - - if (!isConditionMet) - continue; - } - else - { - // Other cases from vanilla game - if (!pTrigger->ConditionMet(pHouse, targetHouse, hasReachedMaxDefensiveTeamsLimit)) - continue; - } - } - - // All triggers below 5000 in current weight will get discarded if this mode is enabled - if (onlyCheckImportantTriggers) - { - if (pTrigger->Weight_Current < 5000) - continue; - } - - auto pTriggerTeam1Type = pTrigger->Team1; - if (!pTriggerTeam1Type) - continue; - - // No more defensive teams needed - if (pTriggerTeam1Type->IsBaseDefense && hasReachedMaxDefensiveTeamsLimit) - continue; - - // If this type of Team reached the max then skip it - int count = 0; - - for (auto team : activeTeamsList) - { - if (team->Type == pTriggerTeam1Type) - count++; - } - - if (count >= pTriggerTeam1Type->Max) - continue; - - teamCategory teamIsCategory = teamCategory::None; - - // Analyze what kind of category is this main team if the feature is enabled - if (splitTriggersByCategory) - { - //Debug::Log("DEBUG: TaskForce [%s] members:\n", pTriggerTeam1Type->TaskForce->ID); - // TaskForces are limited to 6 entries - for (int i = 0; i < 6; i++) - { - auto entry = pTriggerTeam1Type->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; - //Debug::Log("\t[%s](%d) is in AIR category.\n", entry.Type->ID, entry.Amount); - } - else - { - auto pTechnoTypeExt = TechnoTypeExt::ExtMap.Find(entry.Type); - if (!pTechnoTypeExt) - continue; - - if (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; - //Debug::Log("\t[%s](%d) is in NAVAL category.\n", entry.Type->ID, entry.Amount); - } - - if (pTechnoTypeExt->ConsideredVehicle - || (entryIsCategory != teamCategory::Naval - && entryIsCategory != teamCategory::Air)) - { - // This unit is from ground category - entryIsCategory = teamCategory::Ground; - //Debug::Log("\t[%s](%d) is in GROUND category.\n", entry.Type->ID, entry.Amount); - } - } - - // if a team have multiple categories it will be a mixed category - teamIsCategory = teamIsCategory == teamCategory::None || teamIsCategory == entryIsCategory ? entryIsCategory : teamCategory::Unclassified; - - if (teamIsCategory == teamCategory::Unclassified) - break; - } - else - { - break; - } - } - - //Debug::Log("DEBUG: This team is a category %d (1:Ground, 2:Air, 3:Naval, 4:Mixed).\n", teamIsCategory); - // Si existe este valor y el team es MIXTO se sobreescribe el tipo de categoría - if (teamIsCategory == teamCategory::Unclassified - && mergeUnclassifiedCategoryWith >= 0) - { - //Debug::Log("DEBUG: MIXED category forced to work as category %d.\n", mergeUnclassifiedCategoryWith); - teamIsCategory = (teamCategory)mergeUnclassifiedCategoryWith; - } - } - - bool allObjectsCanBeBuiltOrRecruited = true; - - if (pTriggerTeam1Type->Autocreate) - { - for (auto entry : pTriggerTeam1Type->TaskForce->Entries) - { - // Check if each unit in the taskforce meets the structure prerequisites - if (entry.Amount > 0) - { - if (!entry.Type) - continue; - - TechnoTypeClass* object = entry.Type; - bool canBeBuilt = HouseExt::PrerequisitesMet(pHouse, object, ownedBuildings, false); - - if (!canBeBuilt) - { - allObjectsCanBeBuiltOrRecruited = false; - break; - } - } - else - { - break; - } - } - } - else - { - allObjectsCanBeBuiltOrRecruited = false; - } - - if (!allObjectsCanBeBuiltOrRecruited && pTriggerTeam1Type->Recruiter) - { - allObjectsCanBeBuiltOrRecruited = true; - - for (auto entry : pTriggerTeam1Type->TaskForce->Entries) - { - // Check if each unit in the taskforce has the available recruitable units in the map - if (allObjectsCanBeBuiltOrRecruited && entry.Type && entry.Amount > 0) - { - if (ownedRecruitables.count(entry.Type) < entry.Amount) - { - allObjectsCanBeBuiltOrRecruited = false; - break; - } - } - } - } - - // We can't let AI cheat in this trigger because doesn't have the required tech tree available - if (!allObjectsCanBeBuiltOrRecruited) - continue; - - // Special case: triggers become very important if they reach the max priority (value 5000). - // They get stored in a elitist list and all previous triggers are discarded - if (pTrigger->Weight_Current >= 5000 && !onlyCheckImportantTriggers) - { - // First time only - if (validTriggerCandidates.Count > 0) - { - validTriggerCandidates.Clear(); - validTriggerCandidatesGroundOnly.Clear(); - validTriggerCandidatesNavalOnly.Clear(); - validTriggerCandidatesAirOnly.Clear(); - validTriggerCandidatesUnclassifiedOnly.Clear(); - validCategory = teamCategory::None; - } - - // Reset the current ones and now only will be added important triggers to the list - onlyCheckImportantTriggers = true; - totalWeight = 0.0; - splitTriggersByCategory = false; // VIP teams breaks the categories logic (on purpose) - } - - // Passed all checks, save this trigger for later. - // The idea behind this is to simulate an ordered list of weights and once we throw the dice we'll know the winner trigger: More weight means more possibilities to be selected. - totalWeight += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; - TriggerElementWeight item; - item.Trigger = pTrigger; - item.Weight = totalWeight; - item.Category = teamIsCategory; - - validTriggerCandidates.AddItem(item); - - if (splitTriggersByCategory) - { - TriggerElementWeight itemGroundOnly; - TriggerElementWeight itemAirOnly; - TriggerElementWeight itemNavalOnly; - TriggerElementWeight itemUnclassifiedOnly; - - switch (teamIsCategory) - { - case teamCategory::Ground: - totalWeightGroundOnly += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; - - itemGroundOnly.Trigger = pTrigger; - itemGroundOnly.Weight = totalWeightGroundOnly; - itemGroundOnly.Category = teamIsCategory; - - validTriggerCandidatesGroundOnly.AddItem(itemGroundOnly); - totalGroundCategoryTriggers++; - break; - - case teamCategory::Air: - totalWeightAirOnly += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; - - itemAirOnly.Trigger = pTrigger; - itemAirOnly.Weight = totalWeightAirOnly; - itemAirOnly.Category = teamIsCategory; - - validTriggerCandidatesAirOnly.AddItem(itemAirOnly); - totalAirCategoryTriggers++; - break; - - case teamCategory::Naval: - totalWeightNavalOnly += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; - - itemNavalOnly.Trigger = pTrigger; - itemNavalOnly.Weight = totalWeightNavalOnly; - itemNavalOnly.Category = teamIsCategory; - - validTriggerCandidatesNavalOnly.AddItem(itemNavalOnly); - totalNavalCategoryTriggers++; - break; - - case teamCategory::Unclassified: - totalWeightUnclassifiedOnly += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; - - itemUnclassifiedOnly.Trigger = pTrigger; - itemUnclassifiedOnly.Weight = totalWeightUnclassifiedOnly; - itemUnclassifiedOnly.Category = teamIsCategory; - - validTriggerCandidatesUnclassifiedOnly.AddItem(itemUnclassifiedOnly); - totalUnclassifiedCategoryTriggers++; - break; - - default: - break; - } - } - } - } - } - - if (splitTriggersByCategory) - { - switch (validCategory) - { - case teamCategory::Ground: - Debug::Log("DEBUG: This time only will be picked GROUND teams.\n"); - break; - - case teamCategory::Unclassified: - Debug::Log("DEBUG: This time only will be picked MIXED teams.\n"); - break; - - case teamCategory::Naval: - Debug::Log("DEBUG: This time only will be picked NAVAL teams.\n"); - break; - - case teamCategory::Air: - Debug::Log("DEBUG: This time only will be picked AIR teams.\n"); - break; - - default: - Debug::Log("DEBUG: This time teams categories are DISABLED.\n"); - break; - } - } - - if (validTriggerCandidates.Count == 0) - { - Debug::Log("DEBUG: [%s] (idx: %d) No valid triggers for now. A new attempt will be done later...\n", pHouse->Type->ID, pHouse->ArrayIndex); - return SkipCode; - } - - if ((validCategory == teamCategory::Ground && totalGroundCategoryTriggers == 0) - || (validCategory == teamCategory::Unclassified && totalUnclassifiedCategoryTriggers == 0) - || (validCategory == teamCategory::Air && totalAirCategoryTriggers == 0) - || (validCategory == teamCategory::Naval && totalNavalCategoryTriggers == 0)) - { - Debug::Log("DEBUG: [%s] (idx: %d) No valid triggers of this category. A new attempt should be done later...\n", pHouse->Type->ID, pHouse->ArrayIndex); - - if (!isFallbackEnabled) - return SkipCode; - - Debug::Log("... but fallback mode is enabled so now will be checked all available triggers.\n"); - validCategory = teamCategory::None; - } - - AITriggerTypeClass* selectedTrigger = nullptr; - double weightDice = 0.0; - double lastWeight = 0.0; - bool found = false; - - switch (validCategory) - { - case teamCategory::None: - weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeight) * 1.0; - /*Debug::Log("Weight Dice: %f\n", weightDice); - - // Debug - Debug::Log("DEBUG: Candidate AI triggers list:\n"); - for (TriggerElementWeight element : validTriggerCandidates) - { - Debug::Log("Weight: %f, [%s][%s]: %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Team1->Name); - }*/ - - for (auto element : validTriggerCandidates) - { - lastWeight = element.Weight; - - if (weightDice < element.Weight && !found) - { - selectedTrigger = element.Trigger; - found = true; - } - } - break; - - case teamCategory::Ground: - weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightGroundOnly) * 1.0; - /*Debug::Log("Weight Dice: %f\n", weightDice); - - // Debug - Debug::Log("DEBUG: Candidate AI triggers list:\n"); - for (TriggerElementWeight element : validTriggerCandidatesGroundOnly) - { - Debug::Log("Weight: %f, [%s][%s]: %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Team1->Name); - }*/ - - for (auto element : validTriggerCandidatesGroundOnly) - { - lastWeight = element.Weight; - - if (weightDice < element.Weight && !found) - { - selectedTrigger = element.Trigger; - found = true; - } - } - break; - - case teamCategory::Unclassified: - weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightUnclassifiedOnly) * 1.0; - /*Debug::Log("Weight Dice: %f\n", weightDice); - - // Debug - Debug::Log("DEBUG: Candidate AI triggers list:\n"); - for (TriggerElementWeight element : validTriggerCandidatesUnclassifiedOnly) - { - Debug::Log("Weight: %f, [%s][%s]: %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Team1->Name); - }*/ - - for (auto element : validTriggerCandidatesUnclassifiedOnly) - { - lastWeight = element.Weight; - - if (weightDice < element.Weight && !found) - { - selectedTrigger = element.Trigger; - found = true; - } - } - break; - - case teamCategory::Naval: - weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightNavalOnly) * 1.0; - /*Debug::Log("Weight Dice: %f\n", weightDice); - - // Debug - Debug::Log("DEBUG: Candidate AI triggers list:\n"); - for (TriggerElementWeight element : validTriggerCandidatesNavalOnly) - { - Debug::Log("Weight: %f, [%s][%s]: %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Team1->Name); - }*/ - - for (auto element : validTriggerCandidatesNavalOnly) - { - lastWeight = element.Weight; - - if (weightDice < element.Weight && !found) - { - selectedTrigger = element.Trigger; - found = true; - } - } - break; - - case teamCategory::Air: - weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightAirOnly) * 1.0; - /*Debug::Log("Weight Dice: %f\n", weightDice); - - // Debug - Debug::Log("DEBUG: Candidate AI triggers list:\n"); - for (TriggerElementWeight element : validTriggerCandidatesAirOnly) - { - Debug::Log("Weight: %f, [%s][%s]: %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Team1->Name); - }*/ - - for (auto element : validTriggerCandidatesAirOnly) - { - lastWeight = element.Weight; - - if (weightDice < element.Weight && !found) - { - selectedTrigger = element.Trigger; - found = true; - } - } - break; - - default: - break; - } - - if (!selectedTrigger) - { - Debug::Log("AI Team Selector: House [%s] (idx: %d) failed to select Trigger. A new attempt Will be done later...\n", pHouse->Type->ID, pHouse->ArrayIndex); - return SkipCode; - } - - if (selectedTrigger->Weight_Current >= 5000.0 - && selectedTrigger->Weight_Minimum <= 4999.0) - { - // Next time this trigger will be out of the elitist triggers list - selectedTrigger->Weight_Current = 4999.0; - } - - // We have a winner trigger here - Debug::Log("AI Team Selector: House [%s] (idx: %d) selected trigger [%s].\n", pHouse->Type->ID, pHouse->ArrayIndex, selectedTrigger->ID); - - // Team 1 creation - auto pTriggerTeam1Type = selectedTrigger->Team1; - if (pTriggerTeam1Type) - { - int count = 0; - - for (auto team : activeTeamsList) - { - if (team->Type == pTriggerTeam1Type) - count++; - } - - if (count < pTriggerTeam1Type->Max) - { - if (TeamClass* newTeam1 = pTriggerTeam1Type->CreateTeam(pHouse)) - newTeam1->NeedsToDisappear = false; - } - } - - // Team 2 creation (if set) - auto pTriggerTeam2Type = selectedTrigger->Team2; - if (pTriggerTeam2Type) - { - int count = 0; - - for (auto team : activeTeamsList) - { - if (team->Type == pTriggerTeam2Type) - count++; - } - - if (count < pTriggerTeam2Type->Max) - { - if (TeamClass* newTeam2 = pTriggerTeam2Type->CreateTeam(pHouse)) - newTeam2->NeedsToDisappear = false; - } - } - } - - return SkipCode; -} - -bool TeamExt::HouseOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, bool allies, std::vector list) -{ - bool result = false; - int counter = 0; - - // Count all objects of the list, like an OR operator - for (auto pItem : list) - { - for (auto pObject : *TechnoClass::Array) - { - if (pObject - && pObject->IsAlive - && pObject->Health > 0 - && ((!allies && pObject->Owner == pHouse) || (allies && pHouse != pObject->Owner && pHouse->IsAlliedWith(pObject->Owner))) - && !pObject->Owner->Type->MultiplayPassive - && pObject->GetTechnoType() == pItem) - { - counter++; - } - } - } - - switch (pThis->Conditions->ComparatorOperand) - { - case 0: - result = counter < pThis->Conditions->ComparatorType; - break; - case 1: - result = counter <= pThis->Conditions->ComparatorType; - break; - case 2: - result = counter == pThis->Conditions->ComparatorType; - break; - case 3: - result = counter >= pThis->Conditions->ComparatorType; - break; - case 4: - result = counter > pThis->Conditions->ComparatorType; - break; - case 5: - result = counter != pThis->Conditions->ComparatorType; - break; - default: - break; - } - - return result; -} - -bool TeamExt::EnemyOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, bool onlySelectedEnemy, std::vector list) -{ - bool result = false; - int counter = 0; - - if (pEnemy && pHouse->IsAlliedWith(pEnemy) && !onlySelectedEnemy) - pEnemy = nullptr; - - // Count all objects of the list, like an OR operator - for (auto pItem : list) - { - for (auto pObject : *TechnoClass::Array) - { - if (pObject - && pObject->IsAlive - && pObject->Health > 0 - && pObject->Owner != pHouse - && (!pEnemy || (pEnemy && !pHouse->IsAlliedWith(pEnemy))) - && !pObject->Owner->Type->MultiplayPassive - && pObject->GetTechnoType() == pItem) - { - counter++; - } - } - } - - switch (pThis->Conditions->ComparatorOperand) - { - case 0: - result = counter < pThis->Conditions->ComparatorType; - break; - case 1: - result = counter <= pThis->Conditions->ComparatorType; - break; - case 2: - result = counter == pThis->Conditions->ComparatorType; - break; - case 3: - result = counter >= pThis->Conditions->ComparatorType; - break; - case 4: - result = counter > pThis->Conditions->ComparatorType; - break; - case 5: - result = counter != pThis->Conditions->ComparatorType; - break; - default: - break; - } - - return result; -} - -bool TeamExt::NeutralOwns(AITriggerTypeClass* pThis, std::vector list) -{ - bool result = false; - int counter = 0; - - for (auto pHouse : *HouseClass::Array) - { - if (_stricmp(SideClass::Array->GetItem(pHouse->Type->SideIndex)->Name, "Civilian") != 0) - continue; - - // Count all objects of the list, like an OR operator - for (auto pItem : list) - { - for (auto pObject : *TechnoClass::Array) - { - if (pObject - && pObject->IsAlive - && pObject->Health > 0 - && pObject->Owner == pHouse - && pObject->GetTechnoType() == pItem) - { - counter++; - } - } - } - } - - switch (pThis->Conditions->ComparatorOperand) - { - case 0: - result = counter < pThis->Conditions->ComparatorType; - break; - case 1: - result = counter <= pThis->Conditions->ComparatorType; - break; - case 2: - result = counter == pThis->Conditions->ComparatorType; - break; - case 3: - result = counter >= pThis->Conditions->ComparatorType; - break; - case 4: - result = counter > pThis->Conditions->ComparatorType; - break; - case 5: - result = counter != pThis->Conditions->ComparatorType; - break; - default: - break; - } - - return result; -} - -bool TeamExt::HouseOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, std::vector list) -{ - bool result = true; - - if (list.size() == 0) - return false; - - // Count all objects of the list, like an AND operator - for (auto pItem : list) - { - if (!result) - break; - - int counter = 0; - result = true; - - for (auto pObject : *TechnoClass::Array) - { - if (pObject && - pObject->IsAlive && - pObject->Health > 0 && - pObject->Owner == pHouse && - pObject->GetTechnoType() == pItem) - { - counter++; - } - } - - switch (pThis->Conditions->ComparatorOperand) - { - case 0: - result = counter < pThis->Conditions->ComparatorType; - break; - case 1: - result = counter <= pThis->Conditions->ComparatorType; - break; - case 2: - result = counter == pThis->Conditions->ComparatorType; - break; - case 3: - result = counter >= pThis->Conditions->ComparatorType; - break; - case 4: - result = counter > pThis->Conditions->ComparatorType; - break; - case 5: - result = counter != pThis->Conditions->ComparatorType; - break; - default: - break; - } - } - - return result; -} - -bool TeamExt::EnemyOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, std::vector list) -{ - bool result = true; - - if (pEnemy && pHouse->IsAlliedWith(pEnemy)) - pEnemy = nullptr; - - if (list.size() == 0) - return false; - - // Count all objects of the list, like an AND operator - for (auto pItem : list) - { - if (!result) - break; - - int counter = 0; - result = true; - - for (auto pObject : *TechnoClass::Array) - { - if (pObject - && pObject->IsAlive - && pObject->Health > 0 - && pObject->Owner != pHouse - && (!pEnemy || (pEnemy && !pHouse->IsAlliedWith(pEnemy))) - && !pObject->Owner->Type->MultiplayPassive - && pObject->GetTechnoType() == pItem) - { - counter++; - } - } - - switch (pThis->Conditions->ComparatorOperand) - { - case 0: - result = counter < pThis->Conditions->ComparatorType; - break; - case 1: - result = counter <= pThis->Conditions->ComparatorType; - break; - case 2: - result = counter == pThis->Conditions->ComparatorType; - break; - case 3: - result = counter >= pThis->Conditions->ComparatorType; - break; - case 4: - result = counter > pThis->Conditions->ComparatorType; - break; - case 5: - result = counter != pThis->Conditions->ComparatorType; - break; - default: - break; - } - } - - return result; -} - -bool TeamExt::NeutralOwnsAll(AITriggerTypeClass* pThis, std::vector list) -{ - bool result = true; - - if (list.size() == 0) - return false; - - // Any neutral house should be capable to meet the prerequisites - for (auto pHouse : *HouseClass::Array) - { - if (!result) - break; - - bool foundAll = true; - - if (_stricmp(SideClass::Array->GetItem(pHouse->Type->SideIndex)->Name, "Civilian") != 0) - continue; - - // Count all objects of the list, like an AND operator - for (auto pItem : list) - { - if (!foundAll) - break; - - int counter = 0; - - for (auto pObject : *TechnoClass::Array) - { - if (pObject && - pObject->IsAlive && - pObject->Health > 0 && - pObject->Owner == pHouse && - pObject->GetTechnoType() == pItem) - { - counter++; - } - } - - switch (pThis->Conditions->ComparatorOperand) - { - case 0: - foundAll = counter < pThis->Conditions->ComparatorType; - break; - case 1: - foundAll = counter <= pThis->Conditions->ComparatorType; - break; - case 2: - foundAll = counter == pThis->Conditions->ComparatorType; - break; - case 3: - foundAll = counter >= pThis->Conditions->ComparatorType; - break; - case 4: - foundAll = counter > pThis->Conditions->ComparatorType; - break; - case 5: - foundAll = counter != pThis->Conditions->ComparatorType; - break; - default: - break; - } - } - - if (!foundAll) - result = false; - } - - return result; -} - -bool TeamExt::CountConditionMet(AITriggerTypeClass* pThis, int nObjects) -{ - bool result = true; - - if (nObjects < 0) - return false; - - switch (pThis->Conditions->ComparatorOperand) - { - case 0: - result = nObjects < pThis->Conditions->ComparatorType; - break; - case 1: - result = nObjects <= pThis->Conditions->ComparatorType; - break; - case 2: - result = nObjects == pThis->Conditions->ComparatorType; - break; - case 3: - result = nObjects >= pThis->Conditions->ComparatorType; - break; - case 4: - result = nObjects > pThis->Conditions->ComparatorType; - break; - case 5: - result = nObjects != pThis->Conditions->ComparatorType; - break; - default: - break; - } - - return result; -} From 7e5f46166686b89217e4741ae7bb11ad03489159 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Mon, 29 Apr 2024 09:10:17 +0200 Subject: [PATCH 25/59] Moved code into its own file --- Phobos.vcxproj | 1 + src/Ext/Scenario/Hooks.NewTeamsSelector.cpp | 34 +++++++++++++++++++++ src/Ext/Scenario/Hooks.cpp | 32 ------------------- 3 files changed, 35 insertions(+), 32 deletions(-) create mode 100644 src/Ext/Scenario/Hooks.NewTeamsSelector.cpp diff --git a/Phobos.vcxproj b/Phobos.vcxproj index cf9aedf0cc..a0815f6861 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -44,6 +44,7 @@ + diff --git a/src/Ext/Scenario/Hooks.NewTeamsSelector.cpp b/src/Ext/Scenario/Hooks.NewTeamsSelector.cpp new file mode 100644 index 0000000000..2d31f85aa4 --- /dev/null +++ b/src/Ext/Scenario/Hooks.NewTeamsSelector.cpp @@ -0,0 +1,34 @@ +#include "Body.h" + +#include +#include + +DEFINE_HOOK(0x687C9B, ReadScenarioINI_AITeamSelector_PreloadValidTriggers, 0x7) +{ + // For each house save a list with only AI Triggers that can be used + for (HouseClass* pHouse : *HouseClass::Array) + { + auto pHouseExt = HouseExt::ExtMap.Find(pHouse); + pHouseExt->AITriggers_ValidList.clear(); + int houseIdx = pHouse->ArrayIndex; + int sideIdx = pHouse->SideIndex + 1; + + for (int i = 0; i < AITriggerTypeClass::Array->Count; i++) + { + auto pTrigger = AITriggerTypeClass::Array->GetItem(i); + if (!pTrigger) + continue; + + int triggerHouse = pTrigger->HouseIndex; + int triggerSide = pTrigger->SideIndex; + + // The trigger must be compatible with the owner + if ((triggerHouse == -1 || houseIdx == triggerHouse) && (triggerSide == 0 || sideIdx == triggerSide)) + pHouseExt->AITriggers_ValidList.push_back(i); + } + + Debug::Log("House %d [%s] could use %d AI triggers in this map.\n", pHouse->ArrayIndex, pHouse->Type->ID, pHouseExt->AITriggers_ValidList.size()); + } + + return 0; +} diff --git a/src/Ext/Scenario/Hooks.cpp b/src/Ext/Scenario/Hooks.cpp index e9f174c2bf..cfcbf8d43e 100644 --- a/src/Ext/Scenario/Hooks.cpp +++ b/src/Ext/Scenario/Hooks.cpp @@ -1,10 +1,8 @@ #include "Body.h" -#include #include #include #include -#include DEFINE_HOOK(0x6870D7, ReadScenario_LoadingScreens, 0x5) { @@ -29,33 +27,3 @@ DEFINE_HOOK(0x6870D7, ReadScenario_LoadingScreens, 0x5) return SkipGameCode; } - -DEFINE_HOOK(0x687C9B, ReadScenarioINI_AITeamSelector_PreloadValidTriggers, 0x7) -{ - // For each house save a list with only AI Triggers that can be used - for (HouseClass* pHouse : *HouseClass::Array) - { - auto pHouseExt = HouseExt::ExtMap.Find(pHouse); - pHouseExt->AITriggers_ValidList.clear(); - int houseIdx = pHouse->ArrayIndex; - int sideIdx = pHouse->SideIndex + 1; - - for (int i = 0; i < AITriggerTypeClass::Array->Count; i++) - { - auto pTrigger = AITriggerTypeClass::Array->GetItem(i); - if (!pTrigger) - continue; - - int triggerHouse = pTrigger->HouseIndex; - int triggerSide = pTrigger->SideIndex; - - // The trigger must be compatible with the owner - if ((triggerHouse == -1 || houseIdx == triggerHouse) && (triggerSide == 0 || sideIdx == triggerSide)) - pHouseExt->AITriggers_ValidList.push_back(i); - } - - Debug::Log("House %d [%s] could use %d AI triggers in this map.\n", pHouse->ArrayIndex, pHouse->Type->ID, pHouseExt->AITriggers_ValidList.size()); - } - - return 0; -} From 2280ae28bb3aa5b671d674f89aa5aa5adb93e934 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Mon, 29 Apr 2024 09:53:32 +0200 Subject: [PATCH 26/59] Moved code into the new file --- src/Ext/Building/Body.cpp | 2 +- src/Ext/Team/Body.NewTeamsSelector.cpp | 58 ++++++++++++++++++++++++++ src/Ext/Team/Body.h | 58 -------------------------- 3 files changed, 59 insertions(+), 59 deletions(-) diff --git a/src/Ext/Building/Body.cpp b/src/Ext/Building/Body.cpp index 043715f36e..ddd26dac18 100644 --- a/src/Ext/Building/Body.cpp +++ b/src/Ext/Building/Body.cpp @@ -329,7 +329,7 @@ bool BuildingExt::HandleInfiltrate(BuildingClass* pBuilding, HouseClass* pInfilt return true; } -// Assigns a secret production option to the AI building. +// Assigns a secret production option to the AI building (Ares doesn't handle the AI case). void BuildingExt::ExtData::UpdateSecretLabAI() { auto pThis = this->OwnerObject(); diff --git a/src/Ext/Team/Body.NewTeamsSelector.cpp b/src/Ext/Team/Body.NewTeamsSelector.cpp index 938bd2fbed..70a8ff6328 100644 --- a/src/Ext/Team/Body.NewTeamsSelector.cpp +++ b/src/Ext/Team/Body.NewTeamsSelector.cpp @@ -1,5 +1,63 @@ #include "Body.h" +enum teamCategory +{ + None = 0, // No category. Should be default value + Ground = 1, + Air = 2, + Naval = 3, + Unclassified = 4 +}; + +struct TriggerElementWeight +{ + double Weight = 0.0; + AITriggerTypeClass* Trigger = nullptr; + teamCategory Category = teamCategory::None; + + //need to define a == operator so it can be used in array classes + bool operator==(const TriggerElementWeight& other) const + { + return (Trigger == other.Trigger && Weight == other.Weight && Category == other.Category); + } + + //unequality + bool operator!=(const TriggerElementWeight& other) const + { + return (Trigger != other.Trigger || Weight != other.Weight || Category == other.Category); + } + + bool operator<(const TriggerElementWeight& other) const + { + return (Weight < other.Weight); + } + + bool operator<(const double other) const + { + return (Weight < other); + } + + bool operator>(const TriggerElementWeight& other) const + { + return (Weight > other.Weight); + } + + bool operator>(const double other) const + { + return (Weight > other); + } + + bool operator==(const double other) const + { + return (Weight == other); + } + + bool operator!=(const double other) const + { + return (Weight != other); + } +}; + DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) { enum { UseOriginalSelector = 0x4F8A63, SkipCode = 0x4F8B08 }; diff --git a/src/Ext/Team/Body.h b/src/Ext/Team/Body.h index e1a40ef478..c342baf81b 100644 --- a/src/Ext/Team/Body.h +++ b/src/Ext/Team/Body.h @@ -12,64 +12,6 @@ #include #include -enum teamCategory -{ - None = 0, // No category. Should be default value - Ground = 1, - Air = 2, - Naval = 3, - Unclassified = 4 -}; - -struct TriggerElementWeight -{ - double Weight = 0.0; - AITriggerTypeClass* Trigger = nullptr; - teamCategory Category = teamCategory::None; - - //need to define a == operator so it can be used in array classes - bool operator==(const TriggerElementWeight& other) const - { - return (Trigger == other.Trigger && Weight == other.Weight && Category == other.Category); - } - - //unequality - bool operator!=(const TriggerElementWeight& other) const - { - return (Trigger != other.Trigger || Weight != other.Weight || Category == other.Category); - } - - bool operator<(const TriggerElementWeight& other) const - { - return (Weight < other.Weight); - } - - bool operator<(const double other) const - { - return (Weight < other); - } - - bool operator>(const TriggerElementWeight& other) const - { - return (Weight > other.Weight); - } - - bool operator>(const double other) const - { - return (Weight > other); - } - - bool operator==(const double other) const - { - return (Weight == other); - } - - bool operator!=(const double other) const - { - return (Weight != other); - } -}; - class TeamExt { public: From 673109778f5502cbc991fc19911181b75a4e2c40 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Wed, 1 May 2024 08:18:30 +0200 Subject: [PATCH 27/59] Fix rare crash and tweaks --- src/Ext/Team/Body.NewTeamsSelector.cpp | 83 +++++++++++--------------- src/Ext/Techno/Body.cpp | 17 ++++++ src/Ext/Techno/Body.h | 1 + 3 files changed, 53 insertions(+), 48 deletions(-) diff --git a/src/Ext/Team/Body.NewTeamsSelector.cpp b/src/Ext/Team/Body.NewTeamsSelector.cpp index 70a8ff6328..da129c9a72 100644 --- a/src/Ext/Team/Body.NewTeamsSelector.cpp +++ b/src/Ext/Team/Body.NewTeamsSelector.cpp @@ -266,8 +266,10 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) std::map ownedRecruitables; std::map ownedBuildings; - for (auto pTechno : *TechnoClass::Array) + for (auto const pTechno : *TechnoClass::Array) { + if (!TechnoExt::IsValidTechno(pTechno)) continue; + if (pTechno->WhatAmI() == AbstractType::Building) { if (pTechno->Owner == pHouse) @@ -276,7 +278,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else { - auto pBuilding = static_cast(pTechno); + auto const pBuilding = static_cast(pTechno); if (pBuilding && pBuilding->Type->BridgeRepairHut) { CellStruct cell = pTechno->GetCell()->MapCoords; @@ -291,7 +293,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) continue; } - auto* pFoot = static_cast(pTechno); + auto const pFoot = static_cast(pTechno); if (!pFoot || !pTechno->IsAlive @@ -1010,10 +1012,9 @@ bool TeamExt::HouseOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, bool alli { for (auto pObject : *TechnoClass::Array) { - if (pObject - && pObject->IsAlive - && pObject->Health > 0 - && ((!allies && pObject->Owner == pHouse) || (allies && pHouse != pObject->Owner && pHouse->IsAlliedWith(pObject->Owner))) + if (!TechnoExt::IsValidTechno(pObject)) continue; + + if (((!allies && pObject->Owner == pHouse) || (allies && pHouse != pObject->Owner && pHouse->IsAlliedWith(pObject->Owner))) && !pObject->Owner->Type->MultiplayPassive && pObject->GetTechnoType() == pItem) { @@ -1058,14 +1059,13 @@ bool TeamExt::EnemyOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClas pEnemy = nullptr; // Count all objects of the list, like an OR operator - for (auto pItem : list) + for (auto const pItem : list) { - for (auto pObject : *TechnoClass::Array) + for (auto const pObject : *TechnoClass::Array) { - if (pObject - && pObject->IsAlive - && pObject->Health > 0 - && pObject->Owner != pHouse + if (!TechnoExt::IsValidTechno(pObject)) continue; + + if (pObject->Owner != pHouse && (!pEnemy || (pEnemy && !pHouse->IsAlliedWith(pEnemy))) && !pObject->Owner->Type->MultiplayPassive && pObject->GetTechnoType() == pItem) @@ -1107,24 +1107,20 @@ bool TeamExt::NeutralOwns(AITriggerTypeClass* pThis, std::vectorGetItem(pHouse->Type->SideIndex)->Name, "Civilian") != 0) continue; // Count all objects of the list, like an OR operator - for (auto pItem : list) + for (auto const pItem : list) { - for (auto pObject : *TechnoClass::Array) + for (auto const pObject : *TechnoClass::Array) { - if (pObject - && pObject->IsAlive - && pObject->Health > 0 - && pObject->Owner == pHouse - && pObject->GetTechnoType() == pItem) - { + if (!TechnoExt::IsValidTechno(pObject)) continue; + + if (pObject->Owner == pHouse && pObject->GetTechnoType() == pItem) counter++; - } } } } @@ -1164,7 +1160,7 @@ bool TeamExt::HouseOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, std::v return false; // Count all objects of the list, like an AND operator - for (auto pItem : list) + for (auto const pItem : list) { if (!result) break; @@ -1172,16 +1168,12 @@ bool TeamExt::HouseOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, std::v int counter = 0; result = true; - for (auto pObject : *TechnoClass::Array) + for (auto const pObject : *TechnoClass::Array) { - if (pObject && - pObject->IsAlive && - pObject->Health > 0 && - pObject->Owner == pHouse && - pObject->GetTechnoType() == pItem) - { + if (!TechnoExt::IsValidTechno(pObject)) continue; + + if (pObject->Owner == pHouse && pObject->GetTechnoType() == pItem) counter++; - } } switch (pThis->Conditions->ComparatorOperand) @@ -1223,7 +1215,7 @@ bool TeamExt::EnemyOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseC return false; // Count all objects of the list, like an AND operator - for (auto pItem : list) + for (auto const pItem : list) { if (!result) break; @@ -1231,12 +1223,11 @@ bool TeamExt::EnemyOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseC int counter = 0; result = true; - for (auto pObject : *TechnoClass::Array) + for (auto const pObject : *TechnoClass::Array) { - if (pObject - && pObject->IsAlive - && pObject->Health > 0 - && pObject->Owner != pHouse + if (!TechnoExt::IsValidTechno(pObject)) continue; + + if (pObject->Owner != pHouse && (!pEnemy || (pEnemy && !pHouse->IsAlliedWith(pEnemy))) && !pObject->Owner->Type->MultiplayPassive && pObject->GetTechnoType() == pItem) @@ -1281,7 +1272,7 @@ bool TeamExt::NeutralOwnsAll(AITriggerTypeClass* pThis, std::vectorIsAlive && - pObject->Health > 0 && - pObject->Owner == pHouse && - pObject->GetTechnoType() == pItem) - { + if (!TechnoExt::IsValidTechno(pObject)) continue; + + if (pObject->Owner == pHouse && pObject->GetTechnoType() == pItem) counter++; - } } switch (pThis->Conditions->ComparatorOperand) diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index 3b90925c0d..842522b0d0 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -5,6 +5,7 @@ #include #include +#include #include @@ -362,6 +363,22 @@ bool TechnoExt::IsTypeImmune(TechnoClass* pThis, TechnoClass* pSource) return false; } +bool TechnoExt::IsValidTechno(TechnoClass* pTechno) +{ + if (!pTechno) + return false; + + bool isValid = !pTechno->Dirty + && ScriptExt::IsUnitAvailable(pTechno, true) + && pTechno->Owner + && (pTechno->WhatAmI() == AbstractType::Infantry + || pTechno->WhatAmI() == AbstractType::Unit + || pTechno->WhatAmI() == AbstractType::Building + || pTechno->WhatAmI() == AbstractType::Aircraft); + + return isValid; +} + // ============================= // load / save diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h index 5bebb0c86a..114fe5c007 100644 --- a/src/Ext/Techno/Body.h +++ b/src/Ext/Techno/Body.h @@ -144,6 +144,7 @@ class TechnoExt static bool ConvertToType(FootClass* pThis, TechnoTypeClass* toType); static bool CanDeployIntoBuilding(UnitClass* pThis, bool noDeploysIntoDefaultValue = false); static bool IsTypeImmune(TechnoClass* pThis, TechnoClass* pSource); + static bool IsValidTechno(TechnoClass* pTechno); // WeaponHelpers.cpp static int PickWeaponIndex(TechnoClass* pThis, TechnoClass* pTargetTechno, AbstractClass* pTarget, int weaponIndexOne, int weaponIndexTwo, bool allowFallback = true, bool allowAAFallback = true); From 429d9572df4f594a73e33f74f1f4ab47f2c58fe7 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 11 May 2024 23:51:06 +0200 Subject: [PATCH 28/59] Added priority to defensive teams --- src/Ext/Team/Body.NewTeamsSelector.cpp | 30 +++++++++++++++++++------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/Ext/Team/Body.NewTeamsSelector.cpp b/src/Ext/Team/Body.NewTeamsSelector.cpp index da129c9a72..b465def51c 100644 --- a/src/Ext/Team/Body.NewTeamsSelector.cpp +++ b/src/Ext/Team/Body.NewTeamsSelector.cpp @@ -200,6 +200,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) int sideIdx = pHouse->SideIndex + 1; // Side indexes in AITriggers section are 1-based 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); @@ -222,16 +223,21 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) activeTeamsList.AddItem(pRunningTeam); - if (pRunningTeam->Type->IsBaseDefense && activeDefenseTeamsCount < maxBaseDefenseTeams) + if (pRunningTeam->Type->IsBaseDefense) activeDefenseTeamsCount++; } activeTeams = activeTeamsList.Count; - /*Debug::Log("=====================\n[%s] ACTIVE TEAMS: %d / %d\n", pHouse->Type->ID, activeTeams, maxTeamsLimit); + // We will use these values for discarding triggers + int defensiveTeamsLimit = RulesClass::Instance->UseMinDefenseRule ? minBaseDefenseTeams : maxBaseDefenseTeams; + bool hasReachedMaxTeamsLimit = activeTeams < maxTeamsLimit ? false : true; + bool hasReachedMaxDefensiveTeamsLimit = activeDefenseTeamsCount < defensiveTeamsLimit ? false : true; + + /*Debug::Log("\n=====================\n[%s] ACTIVE TEAMS: %d / %d (of them, defensive teams: %d / %d)\n", pHouse->Type->ID, activeTeams, maxTeamsLimit, activeDefenseTeamsCount, defensiveTeamsLimit); for (auto team : activeTeamsList) { - Debug::Log("[%s](%d) : %s\n", team->Type->ID, team->TotalObjects, team->Type->Name); + Debug::Log("[%s](%d) : %s%s\n", team->Type->ID, team->TotalObjects, team->Type->Name, team->Type->IsBaseDefense ? " -> is DEFENDER team" : ""); Debug::Log(" IsMoving: %d, IsFullStrength: %d, IsUnderStrength: %d\n", team->IsMoving, team->IsFullStrength, team->IsUnderStrength); int i = 0; @@ -246,11 +252,15 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) i++; } } - Debug::Log("=====================\n");*/ + Debug::Log("\n=====================\n");*/ - // We will use these values for discarding triggers - bool hasReachedMaxTeamsLimit = activeTeams < maxTeamsLimit ? false : true; - bool hasReachedMaxDefensiveTeamsLimit = activeDefenseTeamsCount < maxBaseDefenseTeams ? false : true; + // 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) + onlyPickDefensiveTeams = true; if (hasReachedMaxDefensiveTeamsLimit) Debug::Log("DEBUG: House [%s] (idx: %d) reached the MaximumAIDefensiveTeams value!\n", pHouse->Type->ID, pHouse->ArrayIndex); @@ -321,6 +331,10 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (!pTrigger) continue; + // Ignore offensive teams if the next trigger must be defensive + if (onlyPickDefensiveTeams && !pTrigger->IsForBaseDefense) + continue; + int triggerHouse = pTrigger->HouseIndex; int triggerSide = pTrigger->SideIndex; @@ -1294,7 +1308,7 @@ bool TeamExt::NeutralOwnsAll(AITriggerTypeClass* pThis, std::vectorOwner == pHouse && pObject->GetTechnoType() == pItem) + if (pObject->Owner == pHouse && pObject->GetTechnoType() == pItem) counter++; } From 6d473821208fdc4f8556890067b1bccdaccf3849 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 26 Oct 2024 22:49:47 +0200 Subject: [PATCH 29/59] Bug Fixes and some Trigger conditions implementations Also, now it won't ignore the AI difficulty and TechLevel. Fixes in Generic prerequisites --- src/Ext/House/Body.Prerequisites.cpp | 10 +- src/Ext/Rules/Body.cpp | 2 + src/Ext/Team/Body.NewTeamsSelector.cpp | 146 ++++++++++++++++--------- src/Ext/TechnoType/Body.cpp | 10 +- 4 files changed, 106 insertions(+), 62 deletions(-) diff --git a/src/Ext/House/Body.Prerequisites.cpp b/src/Ext/House/Body.Prerequisites.cpp index 7d190fe450..d0397999f9 100644 --- a/src/Ext/House/Body.Prerequisites.cpp +++ b/src/Ext/House/Body.Prerequisites.cpp @@ -205,8 +205,8 @@ bool HouseExt::HasGenericPrerequisite(int idx, std::map int HouseExt::FindGenericPrerequisite(const char* id) { - if (TechnoTypeClass::FindIndex(id) >= 0) - return 0; + if (BuildingTypeClass::FindIndex(id) >= 0) + return INT32_MAX; if (RulesExt::Global()->GenericPrerequisitesNames.Count == 0) RulesExt::FillDefaultPrerequisites(); // needed! @@ -215,10 +215,10 @@ int HouseExt::FindGenericPrerequisite(const char* id) for (auto str : RulesExt::Global()->GenericPrerequisitesNames) { if (_strcmpi(id, str) == 0) - return (-1 * i); + return i; - ++i; + --i; } - return 0; + return INT32_MAX; // Error } diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index 47d24628ca..012a07c682 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -267,6 +267,8 @@ void RulesExt::FillDefaultPrerequisites() CCINIClass::INI_Rules; 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); diff --git a/src/Ext/Team/Body.NewTeamsSelector.cpp b/src/Ext/Team/Body.NewTeamsSelector.cpp index b465def51c..92317b1197 100644 --- a/src/Ext/Team/Body.NewTeamsSelector.cpp +++ b/src/Ext/Team/Body.NewTeamsSelector.cpp @@ -166,22 +166,22 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (percentageUnclassifiedTriggers > 0.0 && categoryDice <= unclassifiedValue) { validCategory = teamCategory::Unclassified; - Debug::Log("New AI team category selection: dice %d <= %d (MIXED)\n", categoryDice, unclassifiedValue); + //Debug::Log("New AI team category selection: dice %d <= %d (MIXED)\n", categoryDice, unclassifiedValue); } else if (percentageGroundTriggers > 0.0 && categoryDice <= (unclassifiedValue + groundValue)) { validCategory = teamCategory::Ground; - Debug::Log("New AI team category selection: dice %d <= %d (mixed: %d%% + GROUND: %d%%)\n", categoryDice, (unclassifiedValue + groundValue), unclassifiedValue, groundValue); + //Debug::Log("New AI team category selection: dice %d <= %d (mixed: %d%% + GROUND: %d%%)\n", categoryDice, (unclassifiedValue + groundValue), unclassifiedValue, groundValue); } else if (percentageAirTriggers > 0.0 && categoryDice <= (unclassifiedValue + groundValue + airValue)) { validCategory = teamCategory::Air; - Debug::Log("New AI team category selection: dice %d <= %d (mixed: %d%% + ground: %d%% + AIR: %d%%)\n", categoryDice, (unclassifiedValue + groundValue + airValue), unclassifiedValue, groundValue, airValue); + //Debug::Log("New AI team category selection: dice %d <= %d (mixed: %d%% + ground: %d%% + AIR: %d%%)\n", categoryDice, (unclassifiedValue + groundValue + airValue), unclassifiedValue, groundValue, airValue); } else if (percentageNavalTriggers > 0.0 && categoryDice <= (unclassifiedValue + groundValue + airValue + navalValue)) { validCategory = teamCategory::Naval; - Debug::Log("New AI team category selection: dice %d <= %d (mixed: %d%% + ground: %d%% + air: %d%% + NAVAL: %d%%)\n", categoryDice, (unclassifiedValue + groundValue + airValue + navalValue), unclassifiedValue, groundValue, airValue, navalValue); + //Debug::Log("New AI team category selection: dice %d <= %d (mixed: %d%% + ground: %d%% + air: %d%% + NAVAL: %d%%)\n", categoryDice, (unclassifiedValue + groundValue + airValue + navalValue), unclassifiedValue, groundValue, airValue, navalValue); } else { @@ -197,7 +197,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) int parentCountrySideTypeIdx = pHouse->Type->FindParentCountry()->SideIndex; int sideTypeIdx = parentCountrySideTypeIdx >= 0 ? parentCountrySideTypeIdx + 1 : pHouse->Type->SideIndex + 1; // Side indexes in AITriggers section are 1-based - int sideIdx = pHouse->SideIndex + 1; // Side indexes in AITriggers section are 1-based + int sideIdx = pHouse->SideIndex + 1; // Side indexes in AITriggers section are 1-based -> unused variable!! auto houseDifficulty = pHouse->AIDifficulty; int minBaseDefenseTeams = RulesClass::Instance->MinimumAIDefensiveTeams.GetItem((int)houseDifficulty); @@ -263,11 +263,11 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) onlyPickDefensiveTeams = true; if (hasReachedMaxDefensiveTeamsLimit) - Debug::Log("DEBUG: House [%s] (idx: %d) reached the MaximumAIDefensiveTeams value!\n", pHouse->Type->ID, pHouse->ArrayIndex); + Debug::Log("AITeamsSelector - House [%s] (idx: %d) reached the MaximumAIDefensiveTeams value!\n", pHouse->Type->ID, pHouse->ArrayIndex); if (hasReachedMaxTeamsLimit) { - Debug::Log("DEBUG: House [%s] (idx: %d) reached the TotalAITeamCap value!\n", pHouse->Type->ID, pHouse->ArrayIndex); + Debug::Log("AITeamsSelector - House [%s] (idx: %d) reached the TotalAITeamCap value!\n", pHouse->Type->ID, pHouse->ArrayIndex); return SkipCode; } @@ -341,6 +341,18 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) // Ignore the deactivated triggers if (pTrigger->IsEnabled) { + //pTrigger->OwnerHouseType; + if (pTrigger->TechLevel > pHouse->TechLevel) + continue; + + // ignore it if isn't set for the house AI difficulty + if ((int)houseDifficulty == 0 && !pTrigger->Enabled_Hard + || (int)houseDifficulty == 1 && !pTrigger->Enabled_Normal + || (int)houseDifficulty == 2 && !pTrigger->Enabled_Easy) + { + continue; + } + // The trigger must be compatible with the owner if ((triggerHouse == -1 || houseTypeIdx == triggerHouse) && (triggerSide == 0 || sideTypeIdx == triggerSide)) { @@ -370,6 +382,30 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) list.push_back(pTrigger->ConditionObject); bool isConditionMet = TeamExt::HouseOwns(pTrigger, pHouse, false, list); + if (!isConditionMet) + continue; + }// TO-DO: Case 2 & 3: https://modenc.renegadeprojects.com/AITriggerTypes + else if ((int)pTrigger->ConditionType == 4) + { + // Simulate case 5: "Enemy house economy threshold?" + bool isConditionMet = pTrigger->HouseCredits(nullptr, targetHouse); + + if (!isConditionMet) + continue; + } + else if ((int)pTrigger->ConditionType == 5) + { + // Simulate case 5: "Iron Courtain is charged?" + bool isConditionMet = pTrigger->IronCurtainCharged(pHouse, nullptr); + + if (!isConditionMet) + continue; + } + else if ((int)pTrigger->ConditionType == 6) + { + // Simulate case 6: "Chronosphere is charged?" + bool isConditionMet = pTrigger->ChronoSphereCharged(pHouse, nullptr); + if (!isConditionMet) continue; } @@ -401,7 +437,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 9) { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) + if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) { // New case 9: Like in case 0 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the enemy. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) @@ -414,7 +450,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 10) { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) + if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) { // New case 10: Like in case 1 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the house. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) @@ -427,7 +463,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 11) { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) + if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) { // New case 11: Like in case 7 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the Civilians. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) @@ -440,7 +476,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 12) { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) + if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) { // New case 12: Like in case 0 & 9 but instead of a specific enemy this checks in all enemies. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) @@ -453,7 +489,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 13) { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) + if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) { // New case 13: Like in case 1 & 10 but instead checking the house now checks the allies. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) @@ -466,7 +502,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 14) { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) + if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) { // New case 14: Like in case 9 but instead of meet any comparison now is required all. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) @@ -479,7 +515,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 15) { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) + if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) { // New case 15: Like in case 10 but instead of meet any comparison now is required all. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) @@ -492,7 +528,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 16) { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) + if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) { // New case 16: Like in case 11 but instead of meet any comparison now is required all. // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) @@ -505,7 +541,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } else if ((int)pTrigger->ConditionType == 17) { - if (pTrigger->Conditions[3].ComparatorOperand < RulesExt::Global()->AITargetTypesLists.size()) + if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) { // New case 17: Like in case 14 but instead of meet any comparison now is required all. Check all enemies // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) @@ -646,12 +682,12 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) { for (auto entry : pTriggerTeam1Type->TaskForce->Entries) { + if (!entry.Type) + continue; + // Check if each unit in the taskforce meets the structure prerequisites if (entry.Amount > 0) { - if (!entry.Type) - continue; - TechnoTypeClass* object = entry.Type; bool canBeBuilt = HouseExt::PrerequisitesMet(pHouse, object, ownedBuildings, false); @@ -678,10 +714,15 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) for (auto entry : pTriggerTeam1Type->TaskForce->Entries) { + if (!entry.Type) + continue; + // Check if each unit in the taskforce has the available recruitable units in the map if (allObjectsCanBeBuiltOrRecruited && entry.Type && entry.Amount > 0) { - if (ownedRecruitables.count(entry.Type) < entry.Amount) + int recruits = ownedRecruitables.count(entry.Type) > 0 ? ownedRecruitables[entry.Type] : 0; + + if (recruits < entry.Amount) { allObjectsCanBeBuiltOrRecruited = false; break; @@ -791,30 +832,30 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) switch (validCategory) { case teamCategory::Ground: - Debug::Log("DEBUG: This time only will be picked GROUND teams.\n"); + Debug::Log("AITeamsSelector - This time only will be picked GROUND teams.\n"); break; case teamCategory::Unclassified: - Debug::Log("DEBUG: This time only will be picked MIXED teams.\n"); + Debug::Log("AITeamsSelector - This time only will be picked MIXED teams.\n"); break; case teamCategory::Naval: - Debug::Log("DEBUG: This time only will be picked NAVAL teams.\n"); + Debug::Log("AITeamsSelector - This time only will be picked NAVAL teams.\n"); break; case teamCategory::Air: - Debug::Log("DEBUG: This time only will be picked AIR teams.\n"); + Debug::Log("AITeamsSelector - This time only will be picked AIR teams.\n"); break; default: - Debug::Log("DEBUG: This time teams categories are DISABLED.\n"); + Debug::Log("AITeamsSelector - This time teams categories are DISABLED.\n"); break; } } if (validTriggerCandidates.Count == 0) { - Debug::Log("DEBUG: [%s] (idx: %d) No valid triggers for now. A new attempt will be done later...\n", pHouse->Type->ID, pHouse->ArrayIndex); + Debug::Log("AITeamsSelector - [%s] (idx: %d) No valid triggers for now. A new attempt will be done later...\n", pHouse->Type->ID, pHouse->ArrayIndex); return SkipCode; } @@ -823,12 +864,12 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) || (validCategory == teamCategory::Air && totalAirCategoryTriggers == 0) || (validCategory == teamCategory::Naval && totalNavalCategoryTriggers == 0)) { - Debug::Log("DEBUG: [%s] (idx: %d) No valid triggers of this category. A new attempt should be done later...\n", pHouse->Type->ID, pHouse->ArrayIndex); + Debug::Log("AITeamsSelector - [%s] (idx: %d) No valid triggers of this category. A new attempt should be done later...\n", pHouse->Type->ID, pHouse->ArrayIndex); if (!isFallbackEnabled) return SkipCode; - Debug::Log("... but fallback mode is enabled so now will be checked all available triggers.\n"); + Debug::Log("... but FALLBACK MODE is enabled so now will be checked all available triggers.\n"); validCategory = teamCategory::None; } @@ -841,13 +882,13 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) { case teamCategory::None: weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeight) * 1.0; - /*Debug::Log("Weight Dice: %f\n", weightDice); + Debug::Log("AITeamsSelector - Weight Dice: %f\n", weightDice); // Debug - Debug::Log("DEBUG: Candidate AI triggers list:\n"); + /*Debug::Log("DEBUG: Candidate AI triggers list:\n"); for (TriggerElementWeight element : validTriggerCandidates) { - Debug::Log("Weight: %f, [%s][%s]: %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Team1->Name); + Debug::Log("Selection weight: %f, [%s][%s](CW: %f): %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Weight_Current, element.Trigger->Team1->Name); }*/ for (auto element : validTriggerCandidates) @@ -864,13 +905,13 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) case teamCategory::Ground: weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightGroundOnly) * 1.0; - /*Debug::Log("Weight Dice: %f\n", weightDice); + Debug::Log("AITeamsSelector - Weight Dice: %f\n", weightDice); // Debug - Debug::Log("DEBUG: Candidate AI triggers list:\n"); + /*Debug::Log("DEBUG: Candidate AI triggers list:\n"); for (TriggerElementWeight element : validTriggerCandidatesGroundOnly) { - Debug::Log("Weight: %f, [%s][%s]: %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Team1->Name); + Debug::Log("Selection weight: %f, [%s][%s](CW: %f): %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Weight_Current, element.Trigger->Team1->Name); }*/ for (auto element : validTriggerCandidatesGroundOnly) @@ -887,13 +928,13 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) case teamCategory::Unclassified: weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightUnclassifiedOnly) * 1.0; - /*Debug::Log("Weight Dice: %f\n", weightDice); + Debug::Log("AITeamsSelector - Weight Dice: %f\n", weightDice); // Debug - Debug::Log("DEBUG: Candidate AI triggers list:\n"); + /*Debug::Log("DEBUG: Candidate AI triggers list:\n"); for (TriggerElementWeight element : validTriggerCandidatesUnclassifiedOnly) { - Debug::Log("Weight: %f, [%s][%s]: %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Team1->Name); + Debug::Log("Selection weight: %f, [%s][%s](CW: %f): %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Weight_Current, element.Trigger->Team1->Name); }*/ for (auto element : validTriggerCandidatesUnclassifiedOnly) @@ -910,13 +951,13 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) case teamCategory::Naval: weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightNavalOnly) * 1.0; - /*Debug::Log("Weight Dice: %f\n", weightDice); + Debug::Log("AITeamsSelector - Weight Dice: %f\n", weightDice); // Debug - Debug::Log("DEBUG: Candidate AI triggers list:\n"); + /*Debug::Log("DEBUG: Candidate AI triggers list:\n"); for (TriggerElementWeight element : validTriggerCandidatesNavalOnly) { - Debug::Log("Weight: %f, [%s][%s]: %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Team1->Name); + Debug::Log("Selection weight: %f, [%s][%s](CW: %f): %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Weight_Current, element.Trigger->Team1->Name); }*/ for (auto element : validTriggerCandidatesNavalOnly) @@ -933,13 +974,13 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) case teamCategory::Air: weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightAirOnly) * 1.0; - /*Debug::Log("Weight Dice: %f\n", weightDice); + Debug::Log("AITeamsSelector - Weight Dice: %f\n", weightDice); // Debug - Debug::Log("DEBUG: Candidate AI triggers list:\n"); + /*Debug::Log("DEBUG: Candidate AI triggers list:\n"); for (TriggerElementWeight element : validTriggerCandidatesAirOnly) { - Debug::Log("Weight: %f, [%s][%s]: %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Team1->Name); + Debug::Log("Selection weight: %f, [%s][%s](CW: %f): %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Weight_Current, element.Trigger->Team1->Name); }*/ for (auto element : validTriggerCandidatesAirOnly) @@ -960,7 +1001,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (!selectedTrigger) { - Debug::Log("AI Team Selector: House [%s] (idx: %d) failed to select Trigger. A new attempt Will be done later...\n", pHouse->Type->ID, pHouse->ArrayIndex); + Debug::Log("AITeamsSelector - House [%s] (idx: %d) failed to select Trigger. A new attempt Will be done later...\n", pHouse->Type->ID, pHouse->ArrayIndex); return SkipCode; } @@ -972,7 +1013,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } // We have a winner trigger here - Debug::Log("AI Team Selector: House [%s] (idx: %d) selected trigger [%s].\n", pHouse->Type->ID, pHouse->ArrayIndex, selectedTrigger->ID); + Debug::Log("AITeamsSelector - House [%s] (idx: %d) selected trigger [%s]: %s.\n", pHouse->Type->ID, pHouse->ArrayIndex, selectedTrigger->ID, selectedTrigger->Team1->Name); // Team 1 creation auto pTriggerTeam1Type = selectedTrigger->Team1; @@ -1064,13 +1105,13 @@ bool TeamExt::HouseOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, bool alli return result; } -bool TeamExt::EnemyOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, bool onlySelectedEnemy, std::vector list) +bool TeamExt::EnemyOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pSpecificEnemy, bool onlySelectedEnemy, std::vector list) { bool result = false; int counter = 0; - if (pEnemy && pHouse->IsAlliedWith(pEnemy) && !onlySelectedEnemy) - pEnemy = nullptr; + pSpecificEnemy = pSpecificEnemy && pHouse->IsAlliedWith(pSpecificEnemy) ? nullptr : pSpecificEnemy; + pSpecificEnemy = onlySelectedEnemy ? pSpecificEnemy : nullptr; // Count all objects of the list, like an OR operator for (auto const pItem : list) @@ -1079,10 +1120,11 @@ bool TeamExt::EnemyOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClas { if (!TechnoExt::IsValidTechno(pObject)) continue; - if (pObject->Owner != pHouse - && (!pEnemy || (pEnemy && !pHouse->IsAlliedWith(pEnemy))) - && !pObject->Owner->Type->MultiplayPassive - && pObject->GetTechnoType() == pItem) + const auto pItemType = pObject->GetTechnoType(); + if (pItemType == pItem + && pObject->Owner != pHouse + && (!pSpecificEnemy && !pHouse->IsAlliedWith(pObject->Owner) || (pSpecificEnemy && pSpecificEnemy == pObject->Owner)) + && !pObject->Owner->Type->MultiplayPassive) { counter++; } diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index 357b8880f1..2b4d028ae0 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -469,7 +469,7 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->ConsideredSecretLabTech.Read(exINI, pSection, "ConsideredSecretLabTech"); // Secret.RequiredHouses contains a list of HouseTypeClass indexes - this->Secret_RequiredHouses.clear(); + //this->Secret_RequiredHouses.clear(); const char* key = "SecretLab.RequiredHouses"; char* context = nullptr; pINI->ReadString(pSection, key, "", Phobos::readBuffer); @@ -483,7 +483,7 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) key = nullptr; // Secret.ForbiddenHouses contains a list of HouseTypeClass indexes - this->Secret_ForbiddenHouses.clear(); + //this->Secret_ForbiddenHouses.clear(); key = "SecretLab.ForbiddenHouses"; context = nullptr; pINI->ReadString(pSection, key, "", Phobos::readBuffer); @@ -513,7 +513,7 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) // Prerequisite with Generic Prerequistes support. // Note: I have no idea of what could happen in all the game engine logics if I push the negative indexes of the Ares generic prerequisites directly into the original Prerequisite tag... for that reason this tag is duplicated for working with it - this->Prerequisite.clear(); + //this->Prerequisite.clear(); key = "Prerequisite"; context = nullptr; pINI->ReadString(pSection, key, "", Phobos::readBuffer); @@ -536,7 +536,7 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) key = nullptr; // Prerequisite.Negative with Generic Prerequistes support - this->Prerequisite_Negative.clear(); + //this->Prerequisite_Negative.clear(); key = "Prerequisite.Negative"; context = nullptr; pINI->ReadString(pSection, key, "", Phobos::readBuffer); @@ -559,7 +559,7 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) key = nullptr; // Prerequisite.ListX with Generic Prerequistes support - this->Prerequisite_ListVector.clear(); + //this->Prerequisite_ListVector.clear(); this->Prerequisite_Lists.Read(exINI, pSection, "Prerequisite.Lists"); if (Prerequisite_Lists.Get() > 0) From 9f2a84ebd1c2968446114845377a541cde325e49 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Tue, 18 Feb 2025 13:45:59 +0100 Subject: [PATCH 30/59] Fixes & tweaks --- src/Ext/Scenario/Hooks.NewTeamsSelector.cpp | 4 +++- src/Ext/Team/Body.NewTeamsSelector.cpp | 18 +++++++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/Ext/Scenario/Hooks.NewTeamsSelector.cpp b/src/Ext/Scenario/Hooks.NewTeamsSelector.cpp index 2d31f85aa4..e9782b2758 100644 --- a/src/Ext/Scenario/Hooks.NewTeamsSelector.cpp +++ b/src/Ext/Scenario/Hooks.NewTeamsSelector.cpp @@ -5,6 +5,8 @@ DEFINE_HOOK(0x687C9B, ReadScenarioINI_AITeamSelector_PreloadValidTriggers, 0x7) { + bool ignoreGlobalAITriggers = ScenarioClass::Instance->IgnoreGlobalAITriggers; + // For each house save a list with only AI Triggers that can be used for (HouseClass* pHouse : *HouseClass::Array) { @@ -16,7 +18,7 @@ DEFINE_HOOK(0x687C9B, ReadScenarioINI_AITeamSelector_PreloadValidTriggers, 0x7) for (int i = 0; i < AITriggerTypeClass::Array->Count; i++) { auto pTrigger = AITriggerTypeClass::Array->GetItem(i); - if (!pTrigger) + if (!pTrigger || ignoreGlobalAITriggers == pTrigger->IsGlobal || !pTrigger->Team1) continue; int triggerHouse = pTrigger->HouseIndex; diff --git a/src/Ext/Team/Body.NewTeamsSelector.cpp b/src/Ext/Team/Body.NewTeamsSelector.cpp index 92317b1197..91488be316 100644 --- a/src/Ext/Team/Body.NewTeamsSelector.cpp +++ b/src/Ext/Team/Body.NewTeamsSelector.cpp @@ -66,7 +66,9 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) bool houseIsHuman = pHouse->IsHumanPlayer; - if (SessionClass::IsCampaign()) + bool isCampaign = SessionClass::IsCampaign(); + + if (isCampaign) houseIsHuman = pHouse->IsHumanPlayer || pHouse->IsInPlayerControl; if (houseIsHuman || pHouse->Type->MultiplayPassive) @@ -259,7 +261,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) int defensiveDice = ScenarioClass::Instance->Random.RandomRanged(0, 99); int defenseTeamSelectionThreshold = 50; - if ((defensiveDice < defenseTeamSelectionThreshold) && !hasReachedMaxDefensiveTeamsLimit) + if ((defensiveDice < defenseTeamSelectionThreshold) && !hasReachedMaxDefensiveTeamsLimit && !isCampaign) onlyPickDefensiveTeams = true; if (hasReachedMaxDefensiveTeamsLimit) @@ -324,11 +326,17 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) targetHouse = HouseClass::Array->GetItem(pHouse->EnemyHouseIndex); bool onlyCheckImportantTriggers = false; + bool ignoreGlobalAITriggers = ScenarioClass::Instance->IgnoreGlobalAITriggers; + + const auto pHouseExt = HouseExt::ExtMap.Find(pHouse); // Gather all the trigger candidates into one place for posterior fast calculations - for (auto const pTrigger : *AITriggerTypeClass::Array) + //for (auto const pTrigger : *AITriggerTypeClass::Array) + for (int triggerIdx : pHouseExt->AITriggers_ValidList) { - if (!pTrigger) + const auto pTrigger = AITriggerTypeClass::Array->GetItem(triggerIdx); + + if (!pTrigger || ignoreGlobalAITriggers == pTrigger->IsGlobal || !pTrigger->Team1) continue; // Ignore offensive teams if the next trigger must be defensive @@ -342,7 +350,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (pTrigger->IsEnabled) { //pTrigger->OwnerHouseType; - if (pTrigger->TechLevel > pHouse->TechLevel) + if (pTrigger->Team1->TechLevel > pHouse->TechLevel) continue; // ignore it if isn't set for the house AI difficulty From 15c5e979f91f95ecb25e946798854e863c09f4fa Mon Sep 17 00:00:00 2001 From: FS-21 Date: Tue, 18 Feb 2025 19:21:04 +0100 Subject: [PATCH 31/59] Fix --- src/Ext/TechnoType/Body.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index 1b2e4ca128..39c3037085 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -627,6 +627,7 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) const char* key = "SecretLab.RequiredHouses"; char* context = nullptr; pINI->ReadString(pSection, key, "", Phobos::readBuffer); + this->Secret_RequiredHouses.clear(); for (char* cur = strtok_s(Phobos::readBuffer, Phobos::readDelims, &context); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context)) { @@ -641,6 +642,7 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) key = "SecretLab.ForbiddenHouses"; context = nullptr; pINI->ReadString(pSection, key, "", Phobos::readBuffer); + this->Secret_ForbiddenHouses.clear(); for (char* cur = strtok_s(Phobos::readBuffer, Phobos::readDelims, &context); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context)) { @@ -655,6 +657,7 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) key = "Prerequisite.RequiredTheaters"; context = nullptr; pINI->ReadString(pSection, key, "", Phobos::readBuffer); + this->Prerequisite_RequiredTheaters.clear(); for (char* cur = strtok_s(Phobos::readBuffer, Phobos::readDelims, &context); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context)) { @@ -671,6 +674,7 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) key = "Prerequisite"; context = nullptr; pINI->ReadString(pSection, key, "", Phobos::readBuffer); + this->Prerequisite.clear(); for (char* cur = strtok_s(Phobos::readBuffer, Phobos::readDelims, &context); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context)) { @@ -694,6 +698,7 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) key = "Prerequisite.Negative"; context = nullptr; pINI->ReadString(pSection, key, "", Phobos::readBuffer); + this->Prerequisite_Negative.clear(); for (char* cur = strtok_s(Phobos::readBuffer, Phobos::readDelims, &context); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context)) { @@ -715,6 +720,7 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) // Prerequisite.ListX with Generic Prerequistes support //this->Prerequisite_ListVector.clear(); this->Prerequisite_Lists.Read(exINI, pSection, "Prerequisite.Lists"); + this->Prerequisite_ListVector.clear(); if (Prerequisite_Lists.Get() > 0) { From cd8a81f57fa72d6315b9f00a4e570fb202f73d05 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Thu, 20 Feb 2025 12:51:44 +0100 Subject: [PATCH 32/59] Tweak --- src/Ext/Techno/Body.cpp | 10 ++++++++-- src/Ext/Techno/Body.h | 3 ++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index 2994386e02..3d63e6f695 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -475,13 +475,19 @@ int TechnoExt::ExtData::GetAttachedEffectCumulativeCount(AttachEffectTypeClass* return foundCount; } -bool TechnoExt::IsValidTechno(TechnoClass* pTechno) +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, true) + && ScriptExt::IsUnitAvailable(pTechno, checkIfInTransportOrAbsorbed) && pTechno->Owner && (pTechno->WhatAmI() == AbstractType::Infantry || pTechno->WhatAmI() == AbstractType::Unit diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h index e83213c233..490728b2ba 100644 --- a/src/Ext/Techno/Body.h +++ b/src/Ext/Techno/Body.h @@ -176,7 +176,8 @@ class TechnoExt static Point2D GetBuildingSelectBracketPosition(TechnoClass* pThis, BuildingSelectBracketPosition bracketPosition); static void ProcessDigitalDisplays(TechnoClass* pThis); static void GetValuesForDisplay(TechnoClass* pThis, DisplayInfoType infoType, int& value, int& maxValue); - static bool IsValidTechno(TechnoClass* pTechno); + static bool IsValidTechno(TechnoClass* pTechno, bool checkIfInTransportOrAbsorbed = true); + static bool IsValidTechno(AbstractClass* pObject, bool checkIfInTransportOrAbsorbed = true); // WeaponHelpers.cpp static int PickWeaponIndex(TechnoClass* pThis, TechnoClass* pTargetTechno, AbstractClass* pTarget, int weaponIndexOne, int weaponIndexTwo, bool allowFallback = true, bool allowAAFallback = true); From 3a6f2e15ac182953cea6efec21f61cd6a576bd68 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sun, 10 Aug 2025 17:33:09 +0200 Subject: [PATCH 33/59] Fix AI in single player campaigns --- src/Ext/Scenario/Hooks.NewTeamsSelector.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Ext/Scenario/Hooks.NewTeamsSelector.cpp b/src/Ext/Scenario/Hooks.NewTeamsSelector.cpp index 24dbda755a..403a8edb82 100644 --- a/src/Ext/Scenario/Hooks.NewTeamsSelector.cpp +++ b/src/Ext/Scenario/Hooks.NewTeamsSelector.cpp @@ -12,8 +12,15 @@ DEFINE_HOOK(0x687C9B, ReadScenarioINI_AITeamSelector_PreloadValidTriggers, 0x7) { auto pHouseExt = HouseExt::ExtMap.Find(pHouse); pHouseExt->AITriggers_ValidList.clear(); + + int parentCountryTypeIdx = pHouse->Type->FindParentCountryIndex(); // ParentCountry can change the House in a SP map + int houseTypeIdx = parentCountryTypeIdx >= 0 ? parentCountryTypeIdx : pHouse->Type->ArrayIndex; // Indexes in AITriggers section are 1-based int houseIdx = pHouse->ArrayIndex; - int sideIdx = pHouse->SideIndex + 1; + + int parentCountrySideTypeIdx = pHouse->Type->FindParentCountry()->SideIndex; + int sideTypeIdx = parentCountrySideTypeIdx >= 0 ? parentCountrySideTypeIdx + 1 : pHouse->Type->SideIndex + 1; // Side indexes in AITriggers section are 1-based + int sideIdx = pHouse->SideIndex + 1; // Side indexes in AITriggers section are 1-based -> unused variable!! + for (int i = 0; i < AITriggerTypeClass::Array.Count; i++) { @@ -25,7 +32,8 @@ DEFINE_HOOK(0x687C9B, ReadScenarioINI_AITeamSelector_PreloadValidTriggers, 0x7) int triggerSide = pTrigger->SideIndex; // The trigger must be compatible with the owner - if ((triggerHouse == -1 || houseIdx == triggerHouse) && (triggerSide == 0 || sideIdx == triggerSide)) + //if ((triggerHouse == -1 || houseIdx == triggerHouse) && (triggerSide == 0 || sideIdx == triggerSide)) + if ((triggerHouse == -1 || houseTypeIdx == triggerHouse) && (triggerSide == 0 || sideTypeIdx == triggerSide)) pHouseExt->AITriggers_ValidList.push_back(i); } From b497b0a743a5b6ce3dc38a2efe201774aff419d5 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Mon, 11 Aug 2025 12:01:53 +0200 Subject: [PATCH 34/59] Fix crash --- src/Ext/Scenario/Hooks.NewTeamsSelector.cpp | 2 +- src/Ext/Team/Body.NewTeamsSelector.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ext/Scenario/Hooks.NewTeamsSelector.cpp b/src/Ext/Scenario/Hooks.NewTeamsSelector.cpp index 403a8edb82..6e947bfbba 100644 --- a/src/Ext/Scenario/Hooks.NewTeamsSelector.cpp +++ b/src/Ext/Scenario/Hooks.NewTeamsSelector.cpp @@ -17,7 +17,7 @@ DEFINE_HOOK(0x687C9B, ReadScenarioINI_AITeamSelector_PreloadValidTriggers, 0x7) int houseTypeIdx = parentCountryTypeIdx >= 0 ? parentCountryTypeIdx : pHouse->Type->ArrayIndex; // Indexes in AITriggers section are 1-based int houseIdx = pHouse->ArrayIndex; - int parentCountrySideTypeIdx = pHouse->Type->FindParentCountry()->SideIndex; + int parentCountrySideTypeIdx = parentCountryTypeIdx >= 0 ? pHouse->Type->FindParentCountry()->SideIndex : pHouse->Type->SideIndex; int sideTypeIdx = parentCountrySideTypeIdx >= 0 ? parentCountrySideTypeIdx + 1 : pHouse->Type->SideIndex + 1; // Side indexes in AITriggers section are 1-based int sideIdx = pHouse->SideIndex + 1; // Side indexes in AITriggers section are 1-based -> unused variable!! diff --git a/src/Ext/Team/Body.NewTeamsSelector.cpp b/src/Ext/Team/Body.NewTeamsSelector.cpp index 64ca8c54f9..dd27d9bd36 100644 --- a/src/Ext/Team/Body.NewTeamsSelector.cpp +++ b/src/Ext/Team/Body.NewTeamsSelector.cpp @@ -197,7 +197,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) int houseTypeIdx = parentCountryTypeIdx >= 0 ? parentCountryTypeIdx : pHouse->Type->ArrayIndex; // Indexes in AITriggers section are 1-based int houseIdx = pHouse->ArrayIndex; - int parentCountrySideTypeIdx = pHouse->Type->FindParentCountry()->SideIndex; + int parentCountrySideTypeIdx = parentCountryTypeIdx >= 0 ? pHouse->Type->FindParentCountry()->SideIndex : pHouse->Type->SideIndex; int sideTypeIdx = parentCountrySideTypeIdx >= 0 ? parentCountrySideTypeIdx + 1 : pHouse->Type->SideIndex + 1; // Side indexes in AITriggers section are 1-based int sideIdx = pHouse->SideIndex + 1; // Side indexes in AITriggers section are 1-based -> unused variable!! From f18e11d465d2be16954137e5ab97cc398a50fa99 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Thu, 30 Oct 2025 12:30:46 +0100 Subject: [PATCH 35/59] small improvements --- src/Ext/Team/Body.NewTeamsSelector.cpp | 85 +++++++++++++------------- 1 file changed, 44 insertions(+), 41 deletions(-) diff --git a/src/Ext/Team/Body.NewTeamsSelector.cpp b/src/Ext/Team/Body.NewTeamsSelector.cpp index dd27d9bd36..3fe8a60c4c 100644 --- a/src/Ext/Team/Body.NewTeamsSelector.cpp +++ b/src/Ext/Team/Body.NewTeamsSelector.cpp @@ -64,23 +64,22 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) GET(HouseClass*, pHouse, ESI); - bool houseIsHuman = pHouse->IsHumanPlayer; + 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) + if (houseIsHuman || pHouse->Type->MultiplayPassive || !pHouse->AITriggersActive) return SkipCode; auto pHouseTypeExt = HouseTypeExt::ExtMap.Find(pHouse->Type); if (!pHouseTypeExt) return SkipCode; - if (!RulesExt::Global()->NewTeamsSelector) - return UseOriginalSelector; - // Reset Team selection countdown int countdown = RulesClass::Instance->TeamDelays[(int)pHouse->AIDifficulty]; pHouse->TeamDelayTimer.Start(countdown); @@ -193,6 +192,32 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } } + if (splitTriggersByCategory) + { + switch (validCategory) + { + case teamCategory::Ground: + Debug::Log("AITeamsSelector - This time only GROUND 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; + } + } + int parentCountryTypeIdx = pHouse->Type->FindParentCountryIndex(); // ParentCountry can change the House in a SP map int houseTypeIdx = parentCountryTypeIdx >= 0 ? parentCountryTypeIdx : pHouse->Type->ArrayIndex; // Indexes in AITriggers section are 1-based int houseIdx = pHouse->ArrayIndex; @@ -329,6 +354,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) bool ignoreGlobalAITriggers = ScenarioClass::Instance->IgnoreGlobalAITriggers; const auto pHouseExt = HouseExt::ExtMap.Find(pHouse); + double maxPriority = 5000.0; // Gather all the trigger candidates into one place for posterior fast calculations //for (auto const pTrigger : *AITriggerTypeClass::Array) @@ -340,7 +366,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) continue; // Ignore offensive teams if the next trigger must be defensive - if (onlyPickDefensiveTeams && !pTrigger->IsForBaseDefense) + if ((onlyPickDefensiveTeams && !pTrigger->IsForBaseDefense) || (hasReachedMaxDefensiveTeamsLimit && pTrigger->IsForBaseDefense)) continue; int triggerHouse = pTrigger->HouseIndex; @@ -584,10 +610,10 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } } - // All triggers below 5000 in current weight will get discarded if this mode is enabled + // All triggers below maxPriority (usually 5000) in current weight will get discarded if this mode is enabled if (onlyCheckImportantTriggers) { - if (pTrigger->Weight_Current < 5000) + if (pTrigger->Weight_Current < maxPriority) continue; } @@ -682,6 +708,9 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) //Debug::Log("DEBUG: MIXED category forced to work as category %d.\n", mergeUnclassifiedCategoryWith); teamIsCategory = (teamCategory)mergeUnclassifiedCategoryWith; } + + if (validCategory != teamIsCategory) + continue; } bool allObjectsCanBeBuiltOrRecruited = true; @@ -693,7 +722,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (!entry.Type) continue; - // Check if each unit in the taskforce meets the structure prerequisites + // Check if each unit in the taskforce meets the tech level & structure prerequisites if (entry.Amount > 0) { TechnoTypeClass* object = entry.Type; @@ -743,9 +772,9 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (!allObjectsCanBeBuiltOrRecruited) continue; - // Special case: triggers become very important if they reach the max priority (value 5000). - // They get stored in a elitist list and all previous triggers are discarded - if (pTrigger->Weight_Current >= 5000 && !onlyCheckImportantTriggers) + // Special case: triggers become very important if they reach the max priority (usually 5000, see maxPriority). + // They get stored in an elitist list and all previous triggers are discarded + if (pTrigger->Weight_Current >= maxPriority && !onlyCheckImportantTriggers) { // First time only if (validTriggerCandidates.Count > 0) @@ -835,32 +864,6 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } } - if (splitTriggersByCategory) - { - switch (validCategory) - { - case teamCategory::Ground: - Debug::Log("AITeamsSelector - This time only will be picked GROUND teams.\n"); - break; - - case teamCategory::Unclassified: - Debug::Log("AITeamsSelector - This time only will be picked MIXED teams.\n"); - break; - - case teamCategory::Naval: - Debug::Log("AITeamsSelector - This time only will be picked NAVAL teams.\n"); - break; - - case teamCategory::Air: - Debug::Log("AITeamsSelector - This time only will be picked AIR teams.\n"); - break; - - default: - Debug::Log("AITeamsSelector - This time teams categories are DISABLED.\n"); - break; - } - } - if (validTriggerCandidates.Count == 0) { Debug::Log("AITeamsSelector - [%s] (idx: %d) No valid triggers for now. A new attempt will be done later...\n", pHouse->Type->ID, pHouse->ArrayIndex); @@ -1013,11 +1016,11 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) return SkipCode; } - if (selectedTrigger->Weight_Current >= 5000.0 - && selectedTrigger->Weight_Minimum <= 4999.0) + if (selectedTrigger->Weight_Current >= maxPriority + && selectedTrigger->Weight_Minimum <= (maxPriority - 1)) { // Next time this trigger will be out of the elitist triggers list - selectedTrigger->Weight_Current = 4999.0; + selectedTrigger->Weight_Current = maxPriority - 1; } // We have a winner trigger here From e9d9d4d222ead37b66c254a1b358cbf859c85c25 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Tue, 9 Dec 2025 14:30:08 +0100 Subject: [PATCH 36/59] Code tweaks & fixes - Prerequisite tags were cleaned after a change of map/restart. - More laxed when adding the AI triggers in their respective houses arrays. The main Team's selection loop will decide if the trigger is currently valid. - Some code tweaks & additional log lines in the AI selection loop. - Now AI teams are directly discarded from the selection loop if doesn't exist the required factories if Autocreate=yes --- src/Ext/Scenario/Hooks.NewTeamsSelector.cpp | 16 +- src/Ext/Team/Body.NewTeamsSelector.cpp | 1608 ++++++++++--------- src/Ext/TechnoType/Body.cpp | 147 +- 3 files changed, 921 insertions(+), 850 deletions(-) diff --git a/src/Ext/Scenario/Hooks.NewTeamsSelector.cpp b/src/Ext/Scenario/Hooks.NewTeamsSelector.cpp index 6e947bfbba..918cb56a06 100644 --- a/src/Ext/Scenario/Hooks.NewTeamsSelector.cpp +++ b/src/Ext/Scenario/Hooks.NewTeamsSelector.cpp @@ -5,9 +5,12 @@ DEFINE_HOOK(0x687C9B, ReadScenarioINI_AITeamSelector_PreloadValidTriggers, 0x7) { + if (!RulesExt::Global()->NewTeamsSelector) + return 0; + bool ignoreGlobalAITriggers = ScenarioClass::Instance->IgnoreGlobalAITriggers; - // For each house save a list with only AI Triggers that can be used + // For each house a list will be saved with Triggers that can be used by the AI for (HouseClass* pHouse : HouseClass::Array) { auto pHouseExt = HouseExt::ExtMap.Find(pHouse); @@ -21,11 +24,11 @@ DEFINE_HOOK(0x687C9B, ReadScenarioINI_AITeamSelector_PreloadValidTriggers, 0x7) int sideTypeIdx = parentCountrySideTypeIdx >= 0 ? parentCountrySideTypeIdx + 1 : pHouse->Type->SideIndex + 1; // Side indexes in AITriggers section are 1-based int sideIdx = pHouse->SideIndex + 1; // Side indexes in AITriggers section are 1-based -> unused variable!! - for (int i = 0; i < AITriggerTypeClass::Array.Count; i++) { auto pTrigger = AITriggerTypeClass::Array.GetItem(i); - if (!pTrigger || ignoreGlobalAITriggers == pTrigger->IsGlobal || !pTrigger->Team1) + + if (!pTrigger || (ignoreGlobalAITriggers && pTrigger->IsGlobal && !pTrigger->IsEnabled) || !pTrigger->Team1) continue; int triggerHouse = pTrigger->HouseIndex; @@ -37,7 +40,12 @@ DEFINE_HOOK(0x687C9B, ReadScenarioINI_AITeamSelector_PreloadValidTriggers, 0x7) pHouseExt->AITriggers_ValidList.push_back(i); } - Debug::Log("House %d [%s] could use %d AI triggers in this map.\n", pHouse->ArrayIndex, pHouse->Type->ID, pHouseExt->AITriggers_ValidList.size()); + 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()); + + /*for (int i : pHouseExt->AITriggers_ValidList) + { + Debug::Log(" => %s => %s\n", AITriggerTypeClass::Array.GetItem(i)->ID, AITriggerTypeClass::Array.GetItem(i)->Name); + }*/ } return 0; diff --git a/src/Ext/Team/Body.NewTeamsSelector.cpp b/src/Ext/Team/Body.NewTeamsSelector.cpp index 3fe8a60c4c..fd031fa0cc 100644 --- a/src/Ext/Team/Body.NewTeamsSelector.cpp +++ b/src/Ext/Team/Body.NewTeamsSelector.cpp @@ -101,967 +101,1033 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) int dice = ScenarioClass::Instance->Random.RandomRanged(1, 100); // This house must have the triggers enabled - if (dice <= pHouse->RatioAITriggerTeam && pHouse->AITriggersActive) - { - bool splitTriggersByCategory = RulesExt::Global()->NewTeamsSelector_SplitTriggersByCategory; - bool isFallbackEnabled = RulesExt::Global()->NewTeamsSelector_EnableFallback; - teamCategory validCategory = teamCategory::None; - int mergeUnclassifiedCategoryWith = -1; + if (dice > pHouse->RatioAITriggerTeam || !pHouse->AITriggersActive) + return SkipCode; - double percentageUnclassifiedTriggers = 0.0; - double percentageGroundTriggers = 0.0; - double percentageNavalTriggers = 0.0; - double percentageAirTriggers = 0.0; + bool splitTriggersByCategory = RulesExt::Global()->NewTeamsSelector_SplitTriggersByCategory; + bool isFallbackEnabled = RulesExt::Global()->NewTeamsSelector_EnableFallback; + teamCategory validCategory = teamCategory::None; + int mergeUnclassifiedCategoryWith = -1; - if (splitTriggersByCategory) + double percentageUnclassifiedTriggers = 0.0; + double percentageGroundTriggers = 0.0; + double percentageNavalTriggers = 0.0; + double percentageAirTriggers = 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 + 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 + + // Merge mixed category with another category, if set + if (mergeUnclassifiedCategoryWith >= 0) { - 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 - 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 - - // Merge mixed category with another category, if set - if (mergeUnclassifiedCategoryWith >= 0) + switch (mergeUnclassifiedCategoryWith) { - switch (mergeUnclassifiedCategoryWith) - { - case (int)teamCategory::Ground: - percentageGroundTriggers += percentageUnclassifiedTriggers; - break; - - case (int)teamCategory::Air: - percentageAirTriggers += percentageUnclassifiedTriggers; - break; + case (int)teamCategory::Ground: + percentageGroundTriggers += percentageUnclassifiedTriggers; + break; - case (int)teamCategory::Naval: - percentageNavalTriggers += percentageUnclassifiedTriggers; - break; + case (int)teamCategory::Air: + percentageAirTriggers += percentageUnclassifiedTriggers; + break; - default: - break; - } + case (int)teamCategory::Naval: + percentageNavalTriggers += percentageUnclassifiedTriggers; + break; - percentageUnclassifiedTriggers = 0.0; + default: + break; } - percentageUnclassifiedTriggers = percentageUnclassifiedTriggers < 0.0 || percentageUnclassifiedTriggers > 1.0 ? 0.0 : percentageUnclassifiedTriggers; - percentageGroundTriggers = percentageGroundTriggers < 0.0 || percentageGroundTriggers > 1.0 ? 0.0 : percentageGroundTriggers; - percentageNavalTriggers = percentageNavalTriggers < 0.0 || percentageNavalTriggers > 1.0 ? 0.0 : percentageNavalTriggers; - percentageAirTriggers = percentageAirTriggers < 0.0 || percentageAirTriggers > 1.0 ? 0.0 : percentageAirTriggers; + percentageUnclassifiedTriggers = 0.0; + } - double totalPercengates = percentageUnclassifiedTriggers + percentageGroundTriggers + percentageNavalTriggers + percentageAirTriggers; - if (totalPercengates > 1.0 || totalPercengates <= 0.0) - splitTriggersByCategory = false; + percentageUnclassifiedTriggers = percentageUnclassifiedTriggers < 0.0 || percentageUnclassifiedTriggers > 1.0 ? 0.0 : percentageUnclassifiedTriggers; + percentageGroundTriggers = percentageGroundTriggers < 0.0 || percentageGroundTriggers > 1.0 ? 0.0 : percentageGroundTriggers; + percentageNavalTriggers = percentageNavalTriggers < 0.0 || percentageNavalTriggers > 1.0 ? 0.0 : percentageNavalTriggers; + percentageAirTriggers = percentageAirTriggers < 0.0 || percentageAirTriggers > 1.0 ? 0.0 : percentageAirTriggers; + double totalPercengates = percentageUnclassifiedTriggers + percentageGroundTriggers + percentageNavalTriggers + percentageAirTriggers; + if (totalPercengates > 1.0 || totalPercengates <= 0.0) + splitTriggersByCategory = false; - if (splitTriggersByCategory) + + if (splitTriggersByCategory) + { + int categoryDice = ScenarioClass::Instance->Random.RandomRanged(1, 100); + int unclassifiedValue = (int)(percentageUnclassifiedTriggers * 100.0); + int groundValue = (int)(percentageGroundTriggers * 100.0); + int airValue = (int)(percentageAirTriggers * 100.0); + int navalValue = (int)(percentageNavalTriggers * 100.0); + + // Pick what type of team will be selected in this round + if (percentageUnclassifiedTriggers > 0.0 && categoryDice <= unclassifiedValue) { - int categoryDice = ScenarioClass::Instance->Random.RandomRanged(1, 100); - int unclassifiedValue = (int)(percentageUnclassifiedTriggers * 100.0); - int groundValue = (int)(percentageGroundTriggers * 100.0); - int airValue = (int)(percentageAirTriggers * 100.0); - int navalValue = (int)(percentageNavalTriggers * 100.0); - - // Pick what type of team will be selected in this round - if (percentageUnclassifiedTriggers > 0.0 && categoryDice <= unclassifiedValue) - { - validCategory = teamCategory::Unclassified; - //Debug::Log("New AI team category selection: dice %d <= %d (MIXED)\n", categoryDice, unclassifiedValue); - } - else if (percentageGroundTriggers > 0.0 && categoryDice <= (unclassifiedValue + groundValue)) - { - validCategory = teamCategory::Ground; - //Debug::Log("New AI team category selection: dice %d <= %d (mixed: %d%% + GROUND: %d%%)\n", categoryDice, (unclassifiedValue + groundValue), unclassifiedValue, groundValue); - } - else if (percentageAirTriggers > 0.0 && categoryDice <= (unclassifiedValue + groundValue + airValue)) - { - validCategory = teamCategory::Air; - //Debug::Log("New AI team category selection: dice %d <= %d (mixed: %d%% + ground: %d%% + AIR: %d%%)\n", categoryDice, (unclassifiedValue + groundValue + airValue), unclassifiedValue, groundValue, airValue); - } - else if (percentageNavalTriggers > 0.0 && categoryDice <= (unclassifiedValue + groundValue + airValue + navalValue)) - { - validCategory = teamCategory::Naval; - //Debug::Log("New AI team category selection: dice %d <= %d (mixed: %d%% + ground: %d%% + air: %d%% + NAVAL: %d%%)\n", categoryDice, (unclassifiedValue + groundValue + airValue + navalValue), unclassifiedValue, groundValue, airValue, navalValue); - } - else - { - // If the sum of all percentages is less than 100% then that empty space will work like "no categories" - splitTriggersByCategory = false; - } + validCategory = teamCategory::Unclassified; + //Debug::Log("New AI team category selection: dice %d <= %d (MIXED)\n", categoryDice, unclassifiedValue); + } + else if (percentageGroundTriggers > 0.0 && categoryDice <= (unclassifiedValue + groundValue)) + { + validCategory = teamCategory::Ground; + //Debug::Log("New AI team category selection: dice %d <= %d (mixed: %d%% + GROUND: %d%%)\n", categoryDice, (unclassifiedValue + groundValue), unclassifiedValue, groundValue); + } + else if (percentageAirTriggers > 0.0 && categoryDice <= (unclassifiedValue + groundValue + airValue)) + { + validCategory = teamCategory::Air; + //Debug::Log("New AI team category selection: dice %d <= %d (mixed: %d%% + ground: %d%% + AIR: %d%%)\n", categoryDice, (unclassifiedValue + groundValue + airValue), unclassifiedValue, groundValue, airValue); + } + else if (percentageNavalTriggers > 0.0 && categoryDice <= (unclassifiedValue + groundValue + airValue + navalValue)) + { + validCategory = teamCategory::Naval; + //Debug::Log("New AI team category selection: dice %d <= %d (mixed: %d%% + ground: %d%% + air: %d%% + NAVAL: %d%%)\n", categoryDice, (unclassifiedValue + groundValue + airValue + navalValue), unclassifiedValue, groundValue, airValue, navalValue); + } + else + { + // If the sum of all percentages is less than 100% then that empty space will work like "no categories" + splitTriggersByCategory = false; } } + } - if (splitTriggersByCategory) + if (splitTriggersByCategory) + { + switch (validCategory) { - switch (validCategory) - { - case teamCategory::Ground: - Debug::Log("AITeamsSelector - This time only GROUND teams will be picked.\n"); - break; + case teamCategory::Ground: + Debug::Log("AITeamsSelector - This time only GROUND teams will be picked.\n"); + break; - case teamCategory::Unclassified: - Debug::Log("AITeamsSelector - This time only MIXED 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::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; + 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; - } + default: + Debug::Log("AITeamsSelector - This time teams categories are DISABLED. Anyone can be picked\n"); + break; } + } - int parentCountryTypeIdx = pHouse->Type->FindParentCountryIndex(); // ParentCountry can change the House in a SP map - int houseTypeIdx = parentCountryTypeIdx >= 0 ? parentCountryTypeIdx : pHouse->Type->ArrayIndex; // Indexes in AITriggers section are 1-based - int houseIdx = pHouse->ArrayIndex; - - int parentCountrySideTypeIdx = parentCountryTypeIdx >= 0 ? pHouse->Type->FindParentCountry()->SideIndex : pHouse->Type->SideIndex; - int sideTypeIdx = parentCountrySideTypeIdx >= 0 ? parentCountrySideTypeIdx + 1 : pHouse->Type->SideIndex + 1; // Side indexes in AITriggers section are 1-based - int sideIdx = pHouse->SideIndex + 1; // Side indexes in AITriggers section are 1-based -> unused variable!! - - 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); - double totalWeight = 0.0; - double totalWeightGroundOnly = 0.0; - double totalWeightNavalOnly = 0.0; - double totalWeightAirOnly = 0.0; - double totalWeightUnclassifiedOnly = 0.0; - - // Check if the running teams by the house already reached all the limits - DynamicVectorClass activeTeamsList; - - for (auto const pRunningTeam : TeamClass::Array) - { - totalActiveTeams++; - int teamHouseIdx = pRunningTeam->Owner->ArrayIndex; + int parentCountryTypeIdx = pHouse->Type->FindParentCountryIndex(); // ParentCountry can change the House in a SP map + int houseTypeIdx = parentCountryTypeIdx >= 0 ? parentCountryTypeIdx : pHouse->Type->ArrayIndex; // Indexes in AITriggers section are 1-based + int houseIdx = pHouse->ArrayIndex; + + int parentCountrySideTypeIdx = parentCountryTypeIdx >= 0 ? pHouse->Type->FindParentCountry()->SideIndex : pHouse->Type->SideIndex; + int sideTypeIdx = parentCountrySideTypeIdx >= 0 ? parentCountrySideTypeIdx + 1 : pHouse->Type->SideIndex + 1; // Side indexes in AITriggers section are 1-based + int sideIdx = pHouse->SideIndex + 1; // Side indexes in AITriggers section are 1-based -> unused variable!! + + 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); + double totalWeight = 0.0; + double totalWeightGroundOnly = 0.0; + double totalWeightNavalOnly = 0.0; + double totalWeightAirOnly = 0.0; + double totalWeightUnclassifiedOnly = 0.0; + + // Check if the running teams by the house already reached all the limits + DynamicVectorClass activeTeamsList; + + for (auto const pRunningTeam : TeamClass::Array) + { + totalActiveTeams++; + int teamHouseIdx = pRunningTeam->Owner->ArrayIndex; - if (teamHouseIdx != houseIdx) - continue; + if (teamHouseIdx != houseIdx) + continue; - activeTeamsList.AddItem(pRunningTeam); + activeTeamsList.AddItem(pRunningTeam); - if (pRunningTeam->Type->IsBaseDefense) - activeDefenseTeamsCount++; - } + if (pRunningTeam->Type->IsBaseDefense) + activeDefenseTeamsCount++; + } - activeTeams = activeTeamsList.Count; + activeTeams = activeTeamsList.Count; - // We will use these values for discarding triggers - int defensiveTeamsLimit = RulesClass::Instance->UseMinDefenseRule ? minBaseDefenseTeams : maxBaseDefenseTeams; - bool hasReachedMaxTeamsLimit = activeTeams < maxTeamsLimit ? false : true; - bool hasReachedMaxDefensiveTeamsLimit = activeDefenseTeamsCount < defensiveTeamsLimit ? false : true; + // We will use these values for discarding triggers + int defensiveTeamsLimit = RulesClass::Instance->UseMinDefenseRule ? minBaseDefenseTeams : maxBaseDefenseTeams; + bool hasReachedMaxTeamsLimit = activeTeams < maxTeamsLimit ? false : true; + bool hasReachedMaxDefensiveTeamsLimit = activeDefenseTeamsCount < defensiveTeamsLimit ? false : true; - /*Debug::Log("\n=====================\n[%s] ACTIVE TEAMS: %d / %d (of them, defensive teams: %d / %d)\n", pHouse->Type->ID, activeTeams, maxTeamsLimit, activeDefenseTeamsCount, defensiveTeamsLimit); - for (auto team : activeTeamsList) - { - Debug::Log("[%s](%d) : %s%s\n", team->Type->ID, team->TotalObjects, team->Type->Name, team->Type->IsBaseDefense ? " -> is DEFENDER team" : ""); - Debug::Log(" IsMoving: %d, IsFullStrength: %d, IsUnderStrength: %d\n", team->IsMoving, team->IsFullStrength, team->IsUnderStrength); - int i = 0; + /*Debug::Log("\n=====================\n[%s] ACTIVE TEAMS: %d / %d (of them, defensive teams: %d / %d)\n", pHouse->Type->ID, activeTeams, maxTeamsLimit, activeDefenseTeamsCount, defensiveTeamsLimit); + for (auto team : activeTeamsList) + { + Debug::Log("[%s](%d) : %s%s\n", team->Type->ID, team->TotalObjects, team->Type->Name, team->Type->IsBaseDefense ? " -> is DEFENDER team" : ""); + Debug::Log(" IsMoving: %d, IsFullStrength: %d, IsUnderStrength: %d\n", team->IsMoving, team->IsFullStrength, team->IsUnderStrength); + int i = 0; - for (auto entry : team->Type->TaskForce->Entries) + for (auto entry : team->Type->TaskForce->Entries) + { + if (entry.Type && entry.Amount > 0) { - if (entry.Type && entry.Amount > 0) - { - if (entry.Type) - Debug::Log("\t[%s]: %d / %d\n", entry.Type->ID, team->CountObjects[i], entry.Amount); - } - - i++; + if (entry.Type) + Debug::Log("\t[%s]: %d / %d\n", entry.Type->ID, team->CountObjects[i], entry.Amount); } + + i++; } - Debug::Log("\n=====================\n");*/ + } + Debug::Log("\n=====================\n");*/ - // Check if the next team must be a defensive team - bool onlyPickDefensiveTeams = false; - int defensiveDice = ScenarioClass::Instance->Random.RandomRanged(0, 99); - int defenseTeamSelectionThreshold = 50; + // 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 ((defensiveDice < defenseTeamSelectionThreshold) && !hasReachedMaxDefensiveTeamsLimit && !isCampaign) + onlyPickDefensiveTeams = true; - if (hasReachedMaxDefensiveTeamsLimit) - Debug::Log("AITeamsSelector - House [%s] (idx: %d) reached the MaximumAIDefensiveTeams value!\n", pHouse->Type->ID, pHouse->ArrayIndex); + //if (hasReachedMaxDefensiveTeamsLimit) + //Debug::Log("AITeamsSelector - House [%s] (idx: %d) reached the MaximumAIDefensiveTeams value!\n", pHouse->Type->ID, pHouse->ArrayIndex); - if (hasReachedMaxTeamsLimit) - { - Debug::Log("AITeamsSelector - House [%s] (idx: %d) reached the TotalAITeamCap value!\n", pHouse->Type->ID, pHouse->ArrayIndex); - return SkipCode; - } + if (hasReachedMaxTeamsLimit) + { + //Debug::Log("AITeamsSelector - House [%s] (idx: %d) reached the TotalAITeamCap value!\n", pHouse->Type->ID, pHouse->ArrayIndex); + Debug::Log("AITeamsSelector - House %d [%s](%s) reached the TotalAITeamCap (%d)! Skipping operation this time...\n", pHouse->ArrayIndex, pHouse->PlainName, pHouse->Type->ID, defenseTeamSelectionThreshold); + return SkipCode; + } - int destroyedBridgesCount = 0; - int undamagedBridgesCount = 0; - std::map ownedRecruitables; - std::map ownedBuildings; + 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::map ownedRecruitables; + std::map ownedBuildings; + 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)) continue; - for (auto const pTechno : TechnoClass::Array) + if (pTechno->WhatAmI() == AbstractType::Building) { - if (!TechnoExt::IsValidTechno(pTechno)) continue; + auto const pBuildingType = static_cast(pTechno->GetTechnoType()); - if (pTechno->WhatAmI() == AbstractType::Building) + if (!pBuildingType) + continue; + + if (pTechno->Owner == pHouse) { - if (pTechno->Owner == pHouse) + ++ownedBuildings[static_cast(pBuildingType)]; + + switch (pBuildingType->Factory) { - ++ownedBuildings[static_cast(pTechno->GetTechnoType())]; + 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 + } + else + { + if (pBuildingType->BridgeRepairHut) { - auto const pBuilding = static_cast(pTechno); - if (pBuilding && pBuilding->Type->BridgeRepairHut) - { - CellStruct cell = pTechno->GetCell()->MapCoords; + CellStruct cell = pTechno->GetCell()->MapCoords; - if (MapClass::Instance.IsLinkedBridgeDestroyed(cell)) - destroyedBridgesCount++; - else - undamagedBridgesCount++; - } + if (MapClass::Instance.IsLinkedBridgeDestroyed(cell)) + destroyedBridgesCount++; + else + undamagedBridgesCount++; } - - continue; } - auto const pFoot = static_cast(pTechno); + continue; + } - if (!pFoot - || !pTechno->IsAlive - || pTechno->Health <= 0 - || !pTechno->IsOnMap // Note: underground movement is considered "IsOnMap == false" - || pTechno->Transporter - || pTechno->Absorbed - || !pFoot->CanBeRecruited(pHouse)) - { - continue; - } + auto const pFoot = static_cast(pTechno); - ++ownedRecruitables[pTechno->GetTechnoType()]; + if (!pFoot + || !pTechno->IsAlive + || pTechno->Health <= 0 + || !pTechno->IsOnMap // Note: underground movement is considered "IsOnMap == false" + || pTechno->Transporter + || pTechno->Absorbed + || !pFoot->CanBeRecruited(pHouse)) + { + continue; } - HouseClass* targetHouse = nullptr; - if (pHouse->EnemyHouseIndex >= 0) - targetHouse = HouseClass::Array.GetItem(pHouse->EnemyHouseIndex); + ++ownedRecruitables[pTechno->GetTechnoType()]; + } - bool onlyCheckImportantTriggers = false; - bool ignoreGlobalAITriggers = ScenarioClass::Instance->IgnoreGlobalAITriggers; + if (hasInfantryFactory || hasUnitFactory || hasAircraftFactory || hasNavalFactory) + canAutocreate = true; - const auto pHouseExt = HouseExt::ExtMap.Find(pHouse); - double maxPriority = 5000.0; + /*Debug::Log("AITeamsSelector - The house %d [%s](%s) have these buildings:\n", pHouse->ArrayIndex, pHouse->PlainName, pHouse->Type->ID); - // Gather all the trigger candidates into one place for posterior fast calculations - //for (auto const pTrigger : *AITriggerTypeClass::Array) - for (int triggerIdx : pHouseExt->AITriggers_ValidList) - { - const auto pTrigger = AITriggerTypeClass::Array.GetItem(triggerIdx); + for (auto pBuilding : ownedBuildings) + { + Debug::Log("[%d] => %s : %d\n", pBuilding.first->ArrayIndex, pBuilding.first->ID, pBuilding.second); + }*/ - if (!pTrigger || ignoreGlobalAITriggers == pTrigger->IsGlobal || !pTrigger->Team1) - continue; + HouseClass* targetHouse = nullptr; + if (pHouse->EnemyHouseIndex >= 0) + targetHouse = HouseClass::Array.GetItem(pHouse->EnemyHouseIndex); - // Ignore offensive teams if the next trigger must be defensive - if ((onlyPickDefensiveTeams && !pTrigger->IsForBaseDefense) || (hasReachedMaxDefensiveTeamsLimit && pTrigger->IsForBaseDefense)) - continue; + bool onlyCheckImportantTriggers = false; + bool ignoreGlobalAITriggers = ScenarioClass::Instance->IgnoreGlobalAITriggers; - int triggerHouse = pTrigger->HouseIndex; - int triggerSide = pTrigger->SideIndex; + const auto pHouseExt = HouseExt::ExtMap.Find(pHouse); + double maxPriority = 5000.0; - // Ignore the deactivated triggers - if (pTrigger->IsEnabled) - { - //pTrigger->OwnerHouseType; - if (pTrigger->Team1->TechLevel > pHouse->TechLevel) - continue; + // Gather all the trigger candidates into one place for posterior fast calculations + //for (auto const pTrigger : *AITriggerTypeClass::Array) + for (int triggerIdx : pHouseExt->AITriggers_ValidList) + { + const auto pTrigger = AITriggerTypeClass::Array.GetItem(triggerIdx); - // ignore it if isn't set for the house AI difficulty - if ((int)houseDifficulty == 0 && !pTrigger->Enabled_Hard - || (int)houseDifficulty == 1 && !pTrigger->Enabled_Normal - || (int)houseDifficulty == 2 && !pTrigger->Enabled_Easy) - { - continue; - } + if (!pTrigger || !pTrigger->Team1 || !pTrigger->IsEnabled) + continue; - // The trigger must be compatible with the owner - if ((triggerHouse == -1 || houseTypeIdx == triggerHouse) && (triggerSide == 0 || sideTypeIdx == triggerSide)) - { - // "ConditionType=-1" will be skipped, always is valid - if ((int)pTrigger->ConditionType >= 0) - { - if ((int)pTrigger->ConditionType == 0) - { - // Simulate case 0: "enemy owns" - if (!pTrigger->ConditionObject) - continue; + // Ignore offensive teams if the next trigger must be defensive + if ((onlyPickDefensiveTeams && !pTrigger->IsForBaseDefense) || (hasReachedMaxDefensiveTeamsLimit && pTrigger->IsForBaseDefense)) + continue; - std::vector list; - list.push_back(pTrigger->ConditionObject); - bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, targetHouse, true, list); + int triggerHouse = pTrigger->HouseIndex; + int triggerSide = pTrigger->SideIndex; - if (!isConditionMet) - continue; - } - else if ((int)pTrigger->ConditionType == 1) - { - // Simulate case 1: "house owns" - if (!pTrigger->ConditionObject) - continue; + //pTrigger->OwnerHouseType; + if (pTrigger->Team1->TechLevel > pHouse->TechLevel) + continue; - std::vector list; - list.push_back(pTrigger->ConditionObject); - bool isConditionMet = TeamExt::HouseOwns(pTrigger, pHouse, false, list); + // ignore it if isn't set for the house AI difficulty + if ((int)houseDifficulty == 0 && !pTrigger->Enabled_Hard + || (int)houseDifficulty == 1 && !pTrigger->Enabled_Normal + || (int)houseDifficulty == 2 && !pTrigger->Enabled_Easy) + { + continue; + } - if (!isConditionMet) - continue; - }// TO-DO: Case 2 & 3: https://modenc.renegadeprojects.com/AITriggerTypes - else if ((int)pTrigger->ConditionType == 4) - { - // Simulate case 5: "Enemy house economy threshold?" - bool isConditionMet = pTrigger->HouseCredits(nullptr, targetHouse); + // The trigger must be compatible with the owner + //if ((triggerHouse == -1 || houseTypeIdx == triggerHouse) && (triggerSide == 0 || sideTypeIdx == triggerSide)) + { + // "ConditionType=-1" will be skipped, always is valid + if ((int)pTrigger->ConditionType >= 0) + { + if ((int)pTrigger->ConditionType == 0) + { + // Simulate case 0: "enemy owns" + if (!pTrigger->ConditionObject) + continue; - if (!isConditionMet) - continue; - } - else if ((int)pTrigger->ConditionType == 5) - { - // Simulate case 5: "Iron Courtain is charged?" - bool isConditionMet = pTrigger->IronCurtainCharged(pHouse, nullptr); + std::vector list; + list.push_back(pTrigger->ConditionObject); + bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, targetHouse, true, list); - if (!isConditionMet) - continue; - } - else if ((int)pTrigger->ConditionType == 6) - { - // Simulate case 6: "Chronosphere is charged?" - bool isConditionMet = pTrigger->ChronoSphereCharged(pHouse, nullptr); + if (!isConditionMet) + continue; + } + else if ((int)pTrigger->ConditionType == 1) + { + // Simulate case 1: "house owns" + if (!pTrigger->ConditionObject) + continue; - if (!isConditionMet) - continue; - } - else if ((int)pTrigger->ConditionType == 7) - { - // Simulate case 7: "civilian owns" - if (!pTrigger->ConditionObject) - continue; + std::vector list; + list.push_back(pTrigger->ConditionObject); + bool isConditionMet = TeamExt::HouseOwns(pTrigger, pHouse, false, list); - std::vector list; - list.push_back(pTrigger->ConditionObject); - bool isConditionMet = TeamExt::NeutralOwns(pTrigger, list); + if (!isConditionMet) + continue; + }// TO-DO: Case 2 & 3: https://modenc.renegadeprojects.com/AITriggerTypes + else if ((int)pTrigger->ConditionType == 4) + { + // Simulate case 5: "Enemy house economy threshold?" + bool isConditionMet = pTrigger->HouseCredits(nullptr, targetHouse); - if (!isConditionMet) - continue; - } - else if ((int)pTrigger->ConditionType == 8) - { - // Simulate case 0: "enemy owns" but instead of restrict it against the main enemy house it is done against all enemies - if (!pTrigger->ConditionObject) - continue; + if (!isConditionMet) + continue; + } + else if ((int)pTrigger->ConditionType == 5) + { + // Simulate case 5: "Iron Courtain is charged?" + bool isConditionMet = pTrigger->IronCurtainCharged(pHouse, nullptr); - std::vector list; - list.push_back(pTrigger->ConditionObject); - bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, nullptr, false, list); + if (!isConditionMet) + continue; + } + else if ((int)pTrigger->ConditionType == 6) + { + // Simulate case 6: "Chronosphere is charged?" + bool isConditionMet = pTrigger->ChronoSphereCharged(pHouse, nullptr); - if (!isConditionMet) - continue; - } - else if ((int)pTrigger->ConditionType == 9) - { - if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) - { - // New case 9: Like in case 0 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the enemy. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); - bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, targetHouse, false, list); + if (!isConditionMet) + continue; + } + else if ((int)pTrigger->ConditionType == 7) + { + // Simulate case 7: "civilian owns" + if (!pTrigger->ConditionObject) + continue; - if (!isConditionMet) - continue; - } - } - else if ((int)pTrigger->ConditionType == 10) - { - if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) - { - // New case 10: Like in case 1 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the house. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); - bool isConditionMet = TeamExt::HouseOwns(pTrigger, pHouse, false, list); + std::vector list; + list.push_back(pTrigger->ConditionObject); + bool isConditionMet = TeamExt::NeutralOwns(pTrigger, list); - if (!isConditionMet) - continue; - } - } - else if ((int)pTrigger->ConditionType == 11) - { - if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) - { - // New case 11: Like in case 7 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the Civilians. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); - bool isConditionMet = TeamExt::NeutralOwns(pTrigger, list); + if (!isConditionMet) + continue; + } + else if ((int)pTrigger->ConditionType == 8) + { + // Simulate case 0: "enemy owns" but instead of restrict it against the main enemy house it is done against all enemies + if (!pTrigger->ConditionObject) + continue; - if (!isConditionMet) - continue; - } - } - else if ((int)pTrigger->ConditionType == 12) - { - if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) - { - // New case 12: Like in case 0 & 9 but instead of a specific enemy this checks in all enemies. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); - bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, nullptr, false, list); + std::vector list; + list.push_back(pTrigger->ConditionObject); + bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, nullptr, false, list); - if (!isConditionMet) - continue; - } - } - else if ((int)pTrigger->ConditionType == 13) - { - if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) - { - // New case 13: Like in case 1 & 10 but instead checking the house now checks the allies. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); - bool isConditionMet = TeamExt::HouseOwns(pTrigger, pHouse, true, list); + if (!isConditionMet) + continue; + } + else if ((int)pTrigger->ConditionType == 9) + { + if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) + { + // New case 9: Like in case 0 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the enemy. + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); + bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, targetHouse, false, list); - if (!isConditionMet) - continue; - } - } - else if ((int)pTrigger->ConditionType == 14) - { - if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) - { - // New case 14: Like in case 9 but instead of meet any comparison now is required all. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); - bool isConditionMet = TeamExt::EnemyOwnsAll(pTrigger, pHouse, targetHouse, list); + if (!isConditionMet) + continue; + } + } + else if ((int)pTrigger->ConditionType == 10) + { + if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) + { + // New case 10: Like in case 1 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the house. + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); + bool isConditionMet = TeamExt::HouseOwns(pTrigger, pHouse, false, list); - if (!isConditionMet) - continue; - } - } - else if ((int)pTrigger->ConditionType == 15) - { - if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) - { - // New case 15: Like in case 10 but instead of meet any comparison now is required all. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); - bool isConditionMet = TeamExt::HouseOwnsAll(pTrigger, pHouse, list); + if (!isConditionMet) + continue; + } + } + else if ((int)pTrigger->ConditionType == 11) + { + if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) + { + // New case 11: Like in case 7 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the Civilians. + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); + bool isConditionMet = TeamExt::NeutralOwns(pTrigger, list); - if (!isConditionMet) - continue; - } - } - else if ((int)pTrigger->ConditionType == 16) - { - if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) - { - // New case 16: Like in case 11 but instead of meet any comparison now is required all. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); - bool isConditionMet = TeamExt::NeutralOwnsAll(pTrigger, list); + if (!isConditionMet) + continue; + } + } + else if ((int)pTrigger->ConditionType == 12) + { + if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) + { + // New case 12: Like in case 0 & 9 but instead of a specific enemy this checks in all enemies. + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); + bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, nullptr, false, list); - if (!isConditionMet) - continue; - } - } - else if ((int)pTrigger->ConditionType == 17) - { - if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) - { - // New case 17: Like in case 14 but instead of meet any comparison now is required all. Check all enemies - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); - bool isConditionMet = TeamExt::EnemyOwnsAll(pTrigger, pHouse, nullptr, list); + if (!isConditionMet) + continue; + } + } + else if ((int)pTrigger->ConditionType == 13) + { + if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) + { + // New case 13: Like in case 1 & 10 but instead checking the house now checks the allies. + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); + bool isConditionMet = TeamExt::HouseOwns(pTrigger, pHouse, true, list); - if (!isConditionMet) - continue; - } - } - else if ((int)pTrigger->ConditionType == 18) - { - // New case 18: Check destroyed bridges - bool isConditionMet = TeamExt::CountConditionMet(pTrigger, destroyedBridgesCount); + if (!isConditionMet) + continue; + } + } + else if ((int)pTrigger->ConditionType == 14) + { + if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) + { + // New case 14: Like in case 9 but instead of meet any comparison now is required all. + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); + bool isConditionMet = TeamExt::EnemyOwnsAll(pTrigger, pHouse, targetHouse, list); - if (!isConditionMet) - continue; - } - else if ((int)pTrigger->ConditionType == 19) - { - // New case 19: Check undamaged bridges - bool isConditionMet = TeamExt::CountConditionMet(pTrigger, undamagedBridgesCount); + if (!isConditionMet) + continue; + } + } + else if ((int)pTrigger->ConditionType == 15) + { + if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) + { + // New case 15: Like in case 10 but instead of meet any comparison now is required all. + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); + bool isConditionMet = TeamExt::HouseOwnsAll(pTrigger, pHouse, list); - if (!isConditionMet) - continue; - } - else - { - // Other cases from vanilla game - if (!pTrigger->ConditionMet(pHouse, targetHouse, hasReachedMaxDefensiveTeamsLimit)) - continue; - } + if (!isConditionMet) + continue; } + } + else if ((int)pTrigger->ConditionType == 16) + { + if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) + { + // New case 16: Like in case 11 but instead of meet any comparison now is required all. + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); + bool isConditionMet = TeamExt::NeutralOwnsAll(pTrigger, list); - // All triggers below maxPriority (usually 5000) in current weight will get discarded if this mode is enabled - if (onlyCheckImportantTriggers) + if (!isConditionMet) + continue; + } + } + else if ((int)pTrigger->ConditionType == 17) + { + if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) { - if (pTrigger->Weight_Current < maxPriority) + // New case 17: Like in case 14 but instead of meet any comparison now is required all. Check all enemies + // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) + std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); + bool isConditionMet = TeamExt::EnemyOwnsAll(pTrigger, pHouse, nullptr, list); + + if (!isConditionMet) continue; } + } + else if ((int)pTrigger->ConditionType == 18) + { + // New case 18: Check destroyed bridges + bool isConditionMet = TeamExt::CountConditionMet(pTrigger, destroyedBridgesCount); - auto pTriggerTeam1Type = pTrigger->Team1; - if (!pTriggerTeam1Type) + if (!isConditionMet) continue; + } + else if ((int)pTrigger->ConditionType == 19) + { + // New case 19: Check undamaged bridges + bool isConditionMet = TeamExt::CountConditionMet(pTrigger, undamagedBridgesCount); - // No more defensive teams needed - if (pTriggerTeam1Type->IsBaseDefense && hasReachedMaxDefensiveTeamsLimit) + if (!isConditionMet) + continue; + } + else + { + // Other cases from vanilla game + if (!pTrigger->ConditionMet(pHouse, targetHouse, hasReachedMaxDefensiveTeamsLimit)) continue; + } + } - // If this type of Team reached the max then skip it - int count = 0; + // All triggers below maxPriority (usually 5000) in current weight will get discarded if this mode is enabled + if (onlyCheckImportantTriggers) + { + if (pTrigger->Weight_Current < maxPriority) + continue; + } - for (auto team : activeTeamsList) - { - if (team->Type == pTriggerTeam1Type) - count++; - } + // No more defensive teams needed + if (pTrigger->Team1->IsBaseDefense && hasReachedMaxDefensiveTeamsLimit) + continue; - if (count >= pTriggerTeam1Type->Max) - continue; + // If this type of Team reached the max then skip it + int count = 0; - teamCategory teamIsCategory = teamCategory::None; + for (auto team : activeTeamsList) + { + if (team->Type == pTrigger->Team1) + count++; + } - // Analyze what kind of category is this main team if the feature is enabled - if (splitTriggersByCategory) - { - //Debug::Log("DEBUG: TaskForce [%s] members:\n", pTriggerTeam1Type->TaskForce->ID); - // TaskForces are limited to 6 entries - for (int i = 0; i < 6; i++) - { - auto entry = pTriggerTeam1Type->TaskForce->Entries[i]; - teamCategory entryIsCategory = teamCategory::Ground; + if (count >= pTrigger->Team1->Max) + continue; - 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; - //Debug::Log("\t[%s](%d) is in AIR category.\n", entry.Type->ID, entry.Amount); - } - else - { - auto pTechnoTypeExt = TechnoTypeExt::ExtMap.Find(entry.Type); - if (!pTechnoTypeExt) - continue; - - if (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; - //Debug::Log("\t[%s](%d) is in NAVAL category.\n", entry.Type->ID, entry.Amount); - } - - if (pTechnoTypeExt->ConsideredVehicle - || (entryIsCategory != teamCategory::Naval - && entryIsCategory != teamCategory::Air)) - { - // This unit is from ground category - entryIsCategory = teamCategory::Ground; - //Debug::Log("\t[%s](%d) is in GROUND category.\n", entry.Type->ID, entry.Amount); - } - } - - // if a team have multiple categories it will be a mixed category - teamIsCategory = teamIsCategory == teamCategory::None || teamIsCategory == entryIsCategory ? entryIsCategory : teamCategory::Unclassified; - - if (teamIsCategory == teamCategory::Unclassified) - break; - } - else - { - break; - } - } + teamCategory teamIsCategory = teamCategory::None; - //Debug::Log("DEBUG: This team is a category %d (1:Ground, 2:Air, 3:Naval, 4:Mixed).\n", teamIsCategory); - // Si existe este valor y el team es MIXTO se sobreescribe el tipo de categoría - if (teamIsCategory == teamCategory::Unclassified - && mergeUnclassifiedCategoryWith >= 0) - { - //Debug::Log("DEBUG: MIXED category forced to work as category %d.\n", mergeUnclassifiedCategoryWith); - teamIsCategory = (teamCategory)mergeUnclassifiedCategoryWith; - } + // Analyze what kind of category is this main team if the feature is enabled + if (splitTriggersByCategory) + { + //Debug::Log("DEBUG: TaskForce [%s] members:\n", pTrigger->Team1->TaskForce->ID); + // TaskForces are limited to 6 entries + for (int i = 0; i < 6; i++) + { + auto entry = pTrigger->Team1->TaskForce->Entries[i]; + teamCategory entryIsCategory = teamCategory::Ground; - if (validCategory != teamIsCategory) + if (entry.Amount > 0) + { + if (!entry.Type) continue; - } - bool allObjectsCanBeBuiltOrRecruited = true; - - if (pTriggerTeam1Type->Autocreate) - { - for (auto entry : pTriggerTeam1Type->TaskForce->Entries) + if (entry.Type->WhatAmI() == AbstractType::AircraftType + || entry.Type->ConsideredAircraft) + { + // This unit is from air category + entryIsCategory = teamCategory::Air; + //Debug::Log("\t[%s](%d) is in AIR category.\n", entry.Type->ID, entry.Amount); + } + else { - if (!entry.Type) + auto pTechnoTypeExt = TechnoTypeExt::ExtMap.Find(entry.Type); + if (!pTechnoTypeExt) continue; - // Check if each unit in the taskforce meets the tech level & structure prerequisites - if (entry.Amount > 0) + if (pTechnoTypeExt->ConsideredNaval + || (entry.Type->Naval + && (entry.Type->MovementZone != MovementZone::Amphibious + && entry.Type->MovementZone != MovementZone::AmphibiousDestroyer + && entry.Type->MovementZone != MovementZone::AmphibiousCrusher))) { - TechnoTypeClass* object = entry.Type; - bool canBeBuilt = HouseExt::PrerequisitesMet(pHouse, object, ownedBuildings, false); - - if (!canBeBuilt) - { - allObjectsCanBeBuiltOrRecruited = false; - break; - } + // This unit is from naval category + entryIsCategory = teamCategory::Naval; + //Debug::Log("\t[%s](%d) is in NAVAL category.\n", entry.Type->ID, entry.Amount); } - else + + if (pTechnoTypeExt->ConsideredVehicle + || (entryIsCategory != teamCategory::Naval + && entryIsCategory != teamCategory::Air)) { - break; + // This unit is from ground category + entryIsCategory = teamCategory::Ground; + //Debug::Log("\t[%s](%d) is in GROUND category.\n", entry.Type->ID, entry.Amount); } } + + // if a team have multiple categories it will be a mixed category + teamIsCategory = teamIsCategory == teamCategory::None || teamIsCategory == entryIsCategory ? entryIsCategory : teamCategory::Unclassified; + + if (teamIsCategory == teamCategory::Unclassified) + break; } else { - allObjectsCanBeBuiltOrRecruited = false; + break; } + } - if (!allObjectsCanBeBuiltOrRecruited && pTriggerTeam1Type->Recruiter) - { - allObjectsCanBeBuiltOrRecruited = true; - - for (auto entry : pTriggerTeam1Type->TaskForce->Entries) - { - if (!entry.Type) - continue; + //Debug::Log("DEBUG: This team is a category %d (1:Ground, 2:Air, 3:Naval, 4:Mixed).\n", teamIsCategory); + // Si existe este valor y el team es MIXTO se sobreescribe el tipo de categoría + if (teamIsCategory == teamCategory::Unclassified + && mergeUnclassifiedCategoryWith >= 0) + { + //Debug::Log("DEBUG: MIXED category forced to work as category %d.\n", mergeUnclassifiedCategoryWith); + teamIsCategory = (teamCategory)mergeUnclassifiedCategoryWith; + } - // Check if each unit in the taskforce has the available recruitable units in the map - if (allObjectsCanBeBuiltOrRecruited && entry.Type && entry.Amount > 0) - { - int recruits = ownedRecruitables.count(entry.Type) > 0 ? ownedRecruitables[entry.Type] : 0; + if (validCategory != teamIsCategory) + continue; + } - if (recruits < entry.Amount) - { - allObjectsCanBeBuiltOrRecruited = false; - break; - } - } - } - } + bool allObjectsCanBeBuiltOrRecruited = true; - // We can't let AI cheat in this trigger because doesn't have the required tech tree available - if (!allObjectsCanBeBuiltOrRecruited) + if (pTrigger->Team1->Autocreate && canAutocreate) + { + for (auto entry : pTrigger->Team1->TaskForce->Entries) + { + if (!entry.Type) continue; - // Special case: triggers become very important if they reach the max priority (usually 5000, see maxPriority). - // They get stored in an elitist list and all previous triggers are discarded - if (pTrigger->Weight_Current >= maxPriority && !onlyCheckImportantTriggers) + if (entry.Amount == 0) // As soon as there are empty slots the check finish + break; + + bool canBeBuilt = true; + + // Exists the required factory for producing the checked unit? + switch (entry.Type->WhatAmI()) { - // First time only - if (validTriggerCandidates.Count > 0) + case AbstractType::InfantryType: + if (!hasInfantryFactory) + canBeBuilt = false; + break; + + case AbstractType::AircraftType: + if (!hasAircraftFactory) + canBeBuilt = false; + break; + + case AbstractType::UnitType: + if (entry.Type->Naval) { - validTriggerCandidates.Clear(); - validTriggerCandidatesGroundOnly.Clear(); - validTriggerCandidatesNavalOnly.Clear(); - validTriggerCandidatesAirOnly.Clear(); - validTriggerCandidatesUnclassifiedOnly.Clear(); - validCategory = teamCategory::None; + if (!hasNavalFactory) + canBeBuilt = false; } + else + { + if (!hasUnitFactory) + canBeBuilt = false; + } + break; - // Reset the current ones and now only will be added important triggers to the list - onlyCheckImportantTriggers = true; - totalWeight = 0.0; - splitTriggersByCategory = false; // VIP teams breaks the categories logic (on purpose) + default: + break; } - // Passed all checks, save this trigger for later. - // The idea behind this is to simulate an ordered list of weights and once we throw the dice we'll know the winner trigger: More weight means more possibilities to be selected. - totalWeight += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; - TriggerElementWeight item; - item.Trigger = pTrigger; - item.Weight = totalWeight; - item.Category = teamIsCategory; - - validTriggerCandidates.AddItem(item); + // Meets the production prerequisites? + if (canBeBuilt) + canBeBuilt = HouseExt::PrerequisitesMet(pHouse, entry.Type, ownedBuildings, false); - if (splitTriggersByCategory) + if (!canBeBuilt) { - TriggerElementWeight itemGroundOnly; - TriggerElementWeight itemAirOnly; - TriggerElementWeight itemNavalOnly; - TriggerElementWeight itemUnclassifiedOnly; + allObjectsCanBeBuiltOrRecruited = false; + break; + } + } + } + else + { + allObjectsCanBeBuiltOrRecruited = false; + } - switch (teamIsCategory) - { - case teamCategory::Ground: - totalWeightGroundOnly += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; + if (!allObjectsCanBeBuiltOrRecruited && pTrigger->Team1->Recruiter) + { + allObjectsCanBeBuiltOrRecruited = true; + + for (auto entry : pTrigger->Team1->TaskForce->Entries) + { + if (!entry.Type) + continue; - itemGroundOnly.Trigger = pTrigger; - itemGroundOnly.Weight = totalWeightGroundOnly; - itemGroundOnly.Category = teamIsCategory; + // Check if each unit in the taskforce has the available recruitable units in the map + if (allObjectsCanBeBuiltOrRecruited && entry.Amount > 0) + { + int recruits = ownedRecruitables.count(entry.Type) > 0 ? ownedRecruitables[entry.Type] : 0; - validTriggerCandidatesGroundOnly.AddItem(itemGroundOnly); - totalGroundCategoryTriggers++; + if (recruits < entry.Amount) + { + allObjectsCanBeBuiltOrRecruited = false; break; + } + } + } + } - case teamCategory::Air: - totalWeightAirOnly += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; + // We can't let AI cheat in this trigger because doesn't have the required tech tree available + if (!allObjectsCanBeBuiltOrRecruited) + continue; - itemAirOnly.Trigger = pTrigger; - itemAirOnly.Weight = totalWeightAirOnly; - itemAirOnly.Category = teamIsCategory; + // Special case: triggers become very important if they reach the max priority (usually 5000, see maxPriority). + // They get stored in an elitist list and all previous triggers are discarded + if (pTrigger->Weight_Current >= maxPriority && !onlyCheckImportantTriggers) + { + // First time only + if (validTriggerCandidates.Count > 0) + { + validTriggerCandidates.Clear(); + validTriggerCandidatesGroundOnly.Clear(); + validTriggerCandidatesNavalOnly.Clear(); + validTriggerCandidatesAirOnly.Clear(); + validTriggerCandidatesUnclassifiedOnly.Clear(); + validCategory = teamCategory::None; + } - validTriggerCandidatesAirOnly.AddItem(itemAirOnly); - totalAirCategoryTriggers++; - break; + // Reset the current ones and now only will be added important triggers to the list + onlyCheckImportantTriggers = true; + totalWeight = 0.0; + splitTriggersByCategory = false; // VIP teams breaks the categories logic (on purpose) + } - case teamCategory::Naval: - totalWeightNavalOnly += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; + // Passed all checks, save this trigger for later. + // The idea behind this is to simulate an ordered list of weights and once we throw the dice we'll know the winner trigger: More weight means more possibilities to be selected. + totalWeight += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; + TriggerElementWeight item; + item.Trigger = pTrigger; + item.Weight = totalWeight; + item.Category = teamIsCategory; - itemNavalOnly.Trigger = pTrigger; - itemNavalOnly.Weight = totalWeightNavalOnly; - itemNavalOnly.Category = teamIsCategory; + validTriggerCandidates.AddItem(item); - validTriggerCandidatesNavalOnly.AddItem(itemNavalOnly); - totalNavalCategoryTriggers++; - break; + if (splitTriggersByCategory) + { + TriggerElementWeight itemGroundOnly; + TriggerElementWeight itemAirOnly; + TriggerElementWeight itemNavalOnly; + TriggerElementWeight itemUnclassifiedOnly; - case teamCategory::Unclassified: - totalWeightUnclassifiedOnly += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; + switch (teamIsCategory) + { + case teamCategory::Ground: + totalWeightGroundOnly += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; - itemUnclassifiedOnly.Trigger = pTrigger; - itemUnclassifiedOnly.Weight = totalWeightUnclassifiedOnly; - itemUnclassifiedOnly.Category = teamIsCategory; + itemGroundOnly.Trigger = pTrigger; + itemGroundOnly.Weight = totalWeightGroundOnly; + itemGroundOnly.Category = teamIsCategory; - validTriggerCandidatesUnclassifiedOnly.AddItem(itemUnclassifiedOnly); - totalUnclassifiedCategoryTriggers++; - break; + validTriggerCandidatesGroundOnly.AddItem(itemGroundOnly); + totalGroundCategoryTriggers++; + break; - default: - break; - } - } - } - } - } + case teamCategory::Air: + totalWeightAirOnly += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; - if (validTriggerCandidates.Count == 0) - { - Debug::Log("AITeamsSelector - [%s] (idx: %d) No valid triggers for now. A new attempt will be done later...\n", pHouse->Type->ID, pHouse->ArrayIndex); - return SkipCode; - } + itemAirOnly.Trigger = pTrigger; + itemAirOnly.Weight = totalWeightAirOnly; + itemAirOnly.Category = teamIsCategory; - if ((validCategory == teamCategory::Ground && totalGroundCategoryTriggers == 0) - || (validCategory == teamCategory::Unclassified && totalUnclassifiedCategoryTriggers == 0) - || (validCategory == teamCategory::Air && totalAirCategoryTriggers == 0) - || (validCategory == teamCategory::Naval && totalNavalCategoryTriggers == 0)) - { - Debug::Log("AITeamsSelector - [%s] (idx: %d) No valid triggers of this category. A new attempt should be done later...\n", pHouse->Type->ID, pHouse->ArrayIndex); + validTriggerCandidatesAirOnly.AddItem(itemAirOnly); + totalAirCategoryTriggers++; + break; - if (!isFallbackEnabled) - return SkipCode; + case teamCategory::Naval: + totalWeightNavalOnly += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; - Debug::Log("... but FALLBACK MODE is enabled so now will be checked all available triggers.\n"); - validCategory = teamCategory::None; - } + itemNavalOnly.Trigger = pTrigger; + itemNavalOnly.Weight = totalWeightNavalOnly; + itemNavalOnly.Category = teamIsCategory; - AITriggerTypeClass* selectedTrigger = nullptr; - double weightDice = 0.0; - double lastWeight = 0.0; - bool found = false; + validTriggerCandidatesNavalOnly.AddItem(itemNavalOnly); + totalNavalCategoryTriggers++; + break; - switch (validCategory) - { - case teamCategory::None: - weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeight) * 1.0; - Debug::Log("AITeamsSelector - Weight Dice: %f\n", weightDice); + case teamCategory::Unclassified: + totalWeightUnclassifiedOnly += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; - // Debug - /*Debug::Log("DEBUG: Candidate AI triggers list:\n"); - for (TriggerElementWeight element : validTriggerCandidates) - { - Debug::Log("Selection weight: %f, [%s][%s](CW: %f): %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Weight_Current, element.Trigger->Team1->Name); - }*/ + itemUnclassifiedOnly.Trigger = pTrigger; + itemUnclassifiedOnly.Weight = totalWeightUnclassifiedOnly; + itemUnclassifiedOnly.Category = teamIsCategory; - for (auto element : validTriggerCandidates) - { - lastWeight = element.Weight; + validTriggerCandidatesUnclassifiedOnly.AddItem(itemUnclassifiedOnly); + totalUnclassifiedCategoryTriggers++; + break; - if (weightDice < element.Weight && !found) - { - selectedTrigger = element.Trigger; - found = true; + default: + break; } } - break; + } + } - case teamCategory::Ground: - weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightGroundOnly) * 1.0; - Debug::Log("AITeamsSelector - Weight Dice: %f\n", weightDice); + if (validTriggerCandidates.Count == 0) + { + Debug::Log("AITeamsSelector - The house %d [%s](%s) have no valid triggers for now. A new attempt will be done later...\n", pHouse->ArrayIndex, pHouse->PlainName, pHouse->Type->ID); + return SkipCode; + } - // Debug - /*Debug::Log("DEBUG: Candidate AI triggers list:\n"); - for (TriggerElementWeight element : validTriggerCandidatesGroundOnly) - { - Debug::Log("Selection weight: %f, [%s][%s](CW: %f): %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Weight_Current, element.Trigger->Team1->Name); - }*/ + if ((validCategory == teamCategory::Ground && totalGroundCategoryTriggers == 0) + || (validCategory == teamCategory::Unclassified && totalUnclassifiedCategoryTriggers == 0) + || (validCategory == teamCategory::Air && totalAirCategoryTriggers == 0) + || (validCategory == teamCategory::Naval && totalNavalCategoryTriggers == 0)) + { + Debug::Log("AITeamsSelector - The house %d [%s](%s) have no valid triggers of this category. A new attempt should be done later...\n", pHouse->ArrayIndex, pHouse->PlainName, pHouse->Type->ID); - for (auto element : validTriggerCandidatesGroundOnly) - { - lastWeight = element.Weight; + if (!isFallbackEnabled) + return SkipCode; - if (weightDice < element.Weight && !found) - { - selectedTrigger = element.Trigger; - found = true; - } - } - break; + Debug::Log("... but FALLBACK MODE is enabled so now will be checked all available triggers.\n"); + validCategory = teamCategory::None; + } - case teamCategory::Unclassified: - weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightUnclassifiedOnly) * 1.0; - Debug::Log("AITeamsSelector - Weight Dice: %f\n", weightDice); + AITriggerTypeClass* selectedTrigger = nullptr; + double weightDice = 0.0; + double lastWeight = 0.0; + bool found = false; - // Debug - /*Debug::Log("DEBUG: Candidate AI triggers list:\n"); - for (TriggerElementWeight element : validTriggerCandidatesUnclassifiedOnly) - { - Debug::Log("Selection weight: %f, [%s][%s](CW: %f): %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Weight_Current, element.Trigger->Team1->Name); - }*/ + switch (validCategory) + { + case teamCategory::None: + weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeight) * 1.0; + //Debug::Log("AITeamsSelector - Weight Dice: %f\n", weightDice); + Debug::Log("AITeamsSelector - Picking 1 team from the %d available...\n", validTriggerCandidates.Count); + // Debug + /*Debug::Log("DEBUG: Candidate AI triggers list:\n"); + for (TriggerElementWeight element : validTriggerCandidates) + { + Debug::Log("Selection weight: %f, [%s][%s](CW: %f): %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Weight_Current, element.Trigger->Team1->Name); + }*/ - for (auto element : validTriggerCandidatesUnclassifiedOnly) - { - lastWeight = element.Weight; + for (auto element : validTriggerCandidates) + { + lastWeight = element.Weight; - if (weightDice < element.Weight && !found) - { - selectedTrigger = element.Trigger; - found = true; - } + if (weightDice < element.Weight && !found) + { + selectedTrigger = element.Trigger; + found = true; } - break; + } + break; - case teamCategory::Naval: - weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightNavalOnly) * 1.0; - Debug::Log("AITeamsSelector - Weight Dice: %f\n", weightDice); + case teamCategory::Ground: + weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightGroundOnly) * 1.0; + //Debug::Log("AITeamsSelector - Weight Dice: %f\n", weightDice); + Debug::Log("AITeamsSelector - Picking 1 team (of the category 'GROUND') from the %d available...\n", validTriggerCandidatesGroundOnly.Count); - // Debug - /*Debug::Log("DEBUG: Candidate AI triggers list:\n"); - for (TriggerElementWeight element : validTriggerCandidatesNavalOnly) - { - Debug::Log("Selection weight: %f, [%s][%s](CW: %f): %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Weight_Current, element.Trigger->Team1->Name); - }*/ + // Debug + /*Debug::Log("DEBUG: Candidate AI triggers list:\n"); + for (TriggerElementWeight element : validTriggerCandidatesGroundOnly) + { + Debug::Log("Selection weight: %f, [%s][%s](CW: %f): %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Weight_Current, element.Trigger->Team1->Name); + }*/ - for (auto element : validTriggerCandidatesNavalOnly) - { - lastWeight = element.Weight; + for (auto element : validTriggerCandidatesGroundOnly) + { + lastWeight = element.Weight; - if (weightDice < element.Weight && !found) - { - selectedTrigger = element.Trigger; - found = true; - } + if (weightDice < element.Weight && !found) + { + selectedTrigger = element.Trigger; + found = true; } - break; + } + break; - case teamCategory::Air: - weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightAirOnly) * 1.0; - Debug::Log("AITeamsSelector - Weight Dice: %f\n", weightDice); + case teamCategory::Unclassified: + weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightUnclassifiedOnly) * 1.0; + //Debug::Log("AITeamsSelector - Weight Dice: %f\n", weightDice); + Debug::Log("AITeamsSelector - Picking 1 team (of the category 'UNCLASSIFIED') from the %d available...\n", validTriggerCandidatesUnclassifiedOnly.Count); - // Debug - /*Debug::Log("DEBUG: Candidate AI triggers list:\n"); - for (TriggerElementWeight element : validTriggerCandidatesAirOnly) - { - Debug::Log("Selection weight: %f, [%s][%s](CW: %f): %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Weight_Current, element.Trigger->Team1->Name); - }*/ + // Debug + /*Debug::Log("DEBUG: Candidate AI triggers list:\n"); + for (TriggerElementWeight element : validTriggerCandidatesUnclassifiedOnly) + { + Debug::Log("Selection weight: %f, [%s][%s](CW: %f): %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Weight_Current, element.Trigger->Team1->Name); + }*/ - for (auto element : validTriggerCandidatesAirOnly) - { - lastWeight = element.Weight; + for (auto element : validTriggerCandidatesUnclassifiedOnly) + { + lastWeight = element.Weight; - if (weightDice < element.Weight && !found) - { - selectedTrigger = element.Trigger; - found = true; - } + if (weightDice < element.Weight && !found) + { + selectedTrigger = element.Trigger; + found = true; } - break; - - default: - break; } + break; + + case teamCategory::Naval: + weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightNavalOnly) * 1.0; + //Debug::Log("AITeamsSelector - Weight Dice: %f\n", weightDice); + Debug::Log("AITeamsSelector - Picking 1 team (of the category 'NAVAL') from the %d available...\n", validTriggerCandidatesNavalOnly.Count); - if (!selectedTrigger) + // Debug + /*Debug::Log("DEBUG: Candidate AI triggers list:\n"); + for (TriggerElementWeight element : validTriggerCandidatesNavalOnly) { - Debug::Log("AITeamsSelector - House [%s] (idx: %d) failed to select Trigger. A new attempt Will be done later...\n", pHouse->Type->ID, pHouse->ArrayIndex); - return SkipCode; - } + Debug::Log("Selection weight: %f, [%s][%s](CW: %f): %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Weight_Current, element.Trigger->Team1->Name); + }*/ - if (selectedTrigger->Weight_Current >= maxPriority - && selectedTrigger->Weight_Minimum <= (maxPriority - 1)) + for (auto element : validTriggerCandidatesNavalOnly) { - // Next time this trigger will be out of the elitist triggers list - selectedTrigger->Weight_Current = maxPriority - 1; + lastWeight = element.Weight; + + if (weightDice < element.Weight && !found) + { + selectedTrigger = element.Trigger; + found = true; + } } + break; - // We have a winner trigger here - Debug::Log("AITeamsSelector - House [%s] (idx: %d) selected trigger [%s]: %s.\n", pHouse->Type->ID, pHouse->ArrayIndex, selectedTrigger->ID, selectedTrigger->Team1->Name); + case teamCategory::Air: + weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightAirOnly) * 1.0; + //Debug::Log("AITeamsSelector - Weight Dice: %f\n", weightDice); + Debug::Log("AITeamsSelector - Picking 1 team (of the category 'AIR') from the %d available...\n", validTriggerCandidatesAirOnly.Count); - // Team 1 creation - auto pTriggerTeam1Type = selectedTrigger->Team1; - if (pTriggerTeam1Type) + // Debug + /*Debug::Log("DEBUG: Candidate AI triggers list:\n"); + for (TriggerElementWeight element : validTriggerCandidatesAirOnly) { - int count = 0; + Debug::Log("Selection weight: %f, [%s][%s](CW: %f): %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Weight_Current, element.Trigger->Team1->Name); + }*/ - for (auto team : activeTeamsList) - { - if (team->Type == pTriggerTeam1Type) - count++; - } + for (auto element : validTriggerCandidatesAirOnly) + { + lastWeight = element.Weight; - if (count < pTriggerTeam1Type->Max) + if (weightDice < element.Weight && !found) { - if (TeamClass* newTeam1 = pTriggerTeam1Type->CreateTeam(pHouse)) - newTeam1->NeedsToDisappear = false; + selectedTrigger = element.Trigger; + found = true; } } + break; - // Team 2 creation (if set) - auto pTriggerTeam2Type = selectedTrigger->Team2; - if (pTriggerTeam2Type) - { - int count = 0; + default: + break; + } - for (auto team : activeTeamsList) - { - if (team->Type == pTriggerTeam2Type) - count++; - } + 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 (count < pTriggerTeam2Type->Max) - { - if (TeamClass* newTeam2 = pTriggerTeam2Type->CreateTeam(pHouse)) - newTeam2->NeedsToDisappear = false; - } + if (selectedTrigger->Weight_Current >= maxPriority + && selectedTrigger->Weight_Minimum <= (maxPriority - 1)) + { + // Next time this trigger will be out of the elitist triggers list + selectedTrigger->Weight_Current = maxPriority - 1; + } + + // 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 + int count = 0; + + for (auto team : activeTeamsList) + { + if (team->Type == selectedTrigger->Team1) + count++; + } + + if (count < selectedTrigger->Team1->Max) + { + if (TeamClass* newTeam1 = selectedTrigger->Team1->CreateTeam(pHouse)) + newTeam1->NeedsToDisappear = false; + } + + // Team 2 creation (if set) + auto pTriggerTeam2Type = selectedTrigger->Team2; + if (pTriggerTeam2Type) + { + int count = 0; + + for (auto team : activeTeamsList) + { + if (team->Type == pTriggerTeam2Type) + count++; + } + + if (count < pTriggerTeam2Type->Max) + { + if (TeamClass* newTeam2 = pTriggerTeam2Type->CreateTeam(pHouse)) + newTeam2->NeedsToDisappear = false; } } diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index 73aa5b1ab2..9a76c97bd0 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -1186,119 +1186,115 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->ConsideredSecretLabTech.Read(exINI, pSection, "ConsideredSecretLabTech"); // Secret.RequiredHouses contains a list of HouseTypeClass indexes - //this->Secret_RequiredHouses.clear(); - const char* key = "SecretLab.RequiredHouses"; - char* context = nullptr; - pINI->ReadString(pSection, key, "", Phobos::readBuffer); - this->Secret_RequiredHouses.clear(); - - for (char* cur = strtok_s(Phobos::readBuffer, Phobos::readDelims, &context); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context)) + if (pINI->ReadString(pSection, "SecretLab.RequiredHouses", "", Phobos::readBuffer) > 0) { - std::string item(cur); - this->Secret_RequiredHouses.push_back(item); - } + char* context = nullptr; + this->Secret_RequiredHouses.clear(); - key = nullptr; + 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 - //this->Secret_ForbiddenHouses.clear(); - key = "SecretLab.ForbiddenHouses"; - context = nullptr; - pINI->ReadString(pSection, key, "", Phobos::readBuffer); - this->Secret_ForbiddenHouses.clear(); - - for (char* cur = strtok_s(Phobos::readBuffer, Phobos::readDelims, &context); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context)) + if (pINI->ReadString(pSection, "SecretLab.ForbiddenHouses", "", Phobos::readBuffer) > 0) { - std::string item(cur); - this->Secret_ForbiddenHouses.push_back(item); - } + char* context = nullptr; + this->Secret_ForbiddenHouses.clear(); - key = nullptr; + 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 theader names - this->Prerequisite_RequiredTheaters.clear(); - key = "Prerequisite.RequiredTheaters"; - context = nullptr; - pINI->ReadString(pSection, key, "", Phobos::readBuffer); - this->Prerequisite_RequiredTheaters.clear(); - - for (char* cur = strtok_s(Phobos::readBuffer, Phobos::readDelims, &context); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context)) + if (pINI->ReadString(pSection, "Prerequisite.RequiredTheaters", "", Phobos::readBuffer) > 0) { - int index = Theater::FindIndex(cur); - if (index != -1) - this->Prerequisite_RequiredTheaters.push_back(index); - } + char* context = nullptr; + this->Prerequisite_RequiredTheaters.clear(); - key = nullptr; + for (char* cur = strtok_s(Phobos::readBuffer, Phobos::readDelims, &context); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context)) + { + int index = Theater::FindIndex(cur); + + if (index != -1) + this->Prerequisite_RequiredTheaters.push_back(index); + } + } // Prerequisite with Generic Prerequistes support. // Note: I have no idea of what could happen in all the game engine logics if I push the negative indexes of the Ares generic prerequisites directly into the original Prerequisite tag... for that reason this tag is duplicated for working with it - //this->Prerequisite.clear(); - key = "Prerequisite"; - context = nullptr; - pINI->ReadString(pSection, key, "", Phobos::readBuffer); - this->Prerequisite.clear(); - - for (char* cur = strtok_s(Phobos::readBuffer, Phobos::readDelims, &context); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context)) + if (pINI->ReadString(pSection, "Prerequisite", "", Phobos::readBuffer) > 0) { - int idx = TechnoTypeClass::FindIndex(cur); - if (idx >= 0) - { - this->Prerequisite.push_back(idx); - } - else + 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 index = HouseExt::FindGenericPrerequisite(cur); - if (index < 0) - this->Prerequisite.push_back(index); + 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); + } } } - key = nullptr; - // Prerequisite.Negative with Generic Prerequistes support - //this->Prerequisite_Negative.clear(); - key = "Prerequisite.Negative"; - context = nullptr; - pINI->ReadString(pSection, key, "", Phobos::readBuffer); - this->Prerequisite_Negative.clear(); - - for (char* cur = strtok_s(Phobos::readBuffer, Phobos::readDelims, &context); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context)) + if (pINI->ReadString(pSection, "Prerequisite.Negative", "", Phobos::readBuffer) > 0) { - int idx = TechnoTypeClass::FindIndex(cur); - if (idx >= 0) - { - this->Prerequisite_Negative.push_back(idx); - } - else + 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 index = HouseExt::FindGenericPrerequisite(cur); - if (index < 0) - this->Prerequisite_Negative.push_back(index); + 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); + } } } - key = nullptr; - // Prerequisite.ListX with Generic Prerequistes support - //this->Prerequisite_ListVector.clear(); this->Prerequisite_Lists.Read(exINI, pSection, "Prerequisite.Lists"); - this->Prerequisite_ListVector.clear(); - if (Prerequisite_Lists.Get() > 0) + 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* context2 = nullptr; + char* context = nullptr; pINI->ReadString(pSection, keySection, "", Phobos::readBuffer); - for (char* cur = strtok_s(Phobos::readBuffer, Phobos::readDelims, &context2); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context2)) + 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); @@ -1306,6 +1302,7 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) else { int index = HouseExt::FindGenericPrerequisite(cur); + if (index < 0) objectsList.AddItem(index); } From 01067aed4f6f2dd2f6676a9c13f2d34cf12a1f23 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Fri, 28 Aug 2026 23:53:08 +0200 Subject: [PATCH 37/59] Unify and optimize HouseExt::IsAvailableToHouse and PrerequisitesMet --- src/Ext/House/Body.Prerequisites.cpp | 42 ++++++++++++++++++++++++---- src/Ext/House/Body.h | 5 ++-- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/Ext/House/Body.Prerequisites.cpp b/src/Ext/House/Body.Prerequisites.cpp index 568d983005..9c191047be 100644 --- a/src/Ext/House/Body.Prerequisites.cpp +++ b/src/Ext/House/Body.Prerequisites.cpp @@ -1,6 +1,38 @@ #include "body.h" -bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const pItem, std::map ownedBuildings, bool skipSecretLabChecks) +bool HouseExt::IsAvailableToHouse(HouseClass* const pHouse, TechnoTypeClass* const pItem) +{ + if (!pHouse || !pItem) + return false; + + const auto pType = pHouse->Type; + 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, const std::map& ownedBuildings, bool skipSecretLabChecks) { if (!pThis || !pItem) return false; @@ -17,9 +49,8 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const if (!skipSecretLabChecks && pItemExt->ConsideredSecretLabTech && !pThis->HasFromSecretLab(pItem)) return false; - // Check if it appears in Owner=, RequiredHouses= and ForbiddenHouses= - // Note: if RequiredHouses = tag doesn't exist InRequiredHouses() always returns TRUE - if (!pThis->InOwners(pItem) || !pThis->InRequiredHouses(pItem) || pThis->InForbiddenHouses(pItem)) + // Check if it appears in Owner=, RequiredHouses= and ForbiddenHouses= (including ParentCountry support) + if (!HouseExt::IsAvailableToHouse(pThis, pItem)) return false; // Prerequisite.RequiredTheaters check @@ -176,8 +207,7 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const return prerequisiteMet || prerequisiteListsMet || prerequisiteOverrideMet; } -bool HouseExt::HasGenericPrerequisite(int idx, std::map ownedBuildings) - +bool HouseExt::HasGenericPrerequisite(int idx, const std::map& ownedBuildings) { if (idx >= 0) return false; diff --git a/src/Ext/House/Body.h b/src/Ext/House/Body.h index 2ccc7d1f53..1362e7daad 100644 --- a/src/Ext/House/Body.h +++ b/src/Ext/House/Body.h @@ -157,8 +157,9 @@ class HouseExt static HouseClass* GetHouseKind(OwnerHouseKind kind, bool allowRandom, HouseClass* pDefault, HouseClass* pInvoker = nullptr, HouseClass* pVictim = nullptr); static CellClass* GetEnemyBaseGatherCell(HouseClass* pTargetHouse, HouseClass* pCurrentHouse, CoordStruct defaultCurrentCoords, SpeedType speedTypeZone, int extraDistance = 0); static void GetAIChronoshiftSupers(HouseClass* pThis, SuperClass*& pSuperCSphere, SuperClass*& pSuperCWarp); - static bool PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const pItem, const std::map ownedBuildings, bool skipSecretLabChecks); - static bool HasGenericPrerequisite(int idx, std::map ownedBuildings); + static bool IsAvailableToHouse(HouseClass* const pHouse, TechnoTypeClass* const pItem); + static bool PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const pItem, const std::map& ownedBuildings, bool skipSecretLabChecks = false); + static bool HasGenericPrerequisite(int idx, const std::map& ownedBuildings); static int FindGenericPrerequisite(const char* id); static bool IsDisabledFromShell( From 4727c12ab16461976cdf7bd1b72a3845ba1d6ee9 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 29 Aug 2026 00:14:03 +0200 Subject: [PATCH 38/59] Eliminate ownedBuildings map and use pHouse->ActiveBuildingTypes in PrerequisitesMet --- src/Ext/Building/Body.cpp | 9 +- src/Ext/House/Body.Prerequisites.cpp | 248 +++++++++++++------------ src/Ext/House/Body.h | 4 +- src/Ext/Team/Body.NewTeamsSelector.cpp | 4 +- 4 files changed, 129 insertions(+), 136 deletions(-) diff --git a/src/Ext/Building/Body.cpp b/src/Ext/Building/Body.cpp index 1a0a3bf6bf..7009f4c22f 100644 --- a/src/Ext/Building/Body.cpp +++ b/src/Ext/Building/Body.cpp @@ -493,13 +493,6 @@ void BuildingExt::ExtData::UpdateSecretLabAI() if (possibleCandidates.Count > 0) { - std::map ownedBuildings; - - for (auto building : pOwner->Buildings) - { - ++ownedBuildings[building->Type]; - } - for (const auto& boon : possibleCandidates) { auto pExt = TechnoTypeExt::ExtMap.Find(boon); @@ -516,7 +509,7 @@ void BuildingExt::ExtData::UpdateSecretLabAI() if (pOwner->Type->ArrayIndex == houseIdx) { - bool canBeBuilt = HouseExt::PrerequisitesMet(pOwner, boon, ownedBuildings, true); + bool canBeBuilt = HouseExt::PrerequisitesMet(pOwner, boon, true); if (canBeBuilt) { diff --git a/src/Ext/House/Body.Prerequisites.cpp b/src/Ext/House/Body.Prerequisites.cpp index 9c191047be..afc05f2f4b 100644 --- a/src/Ext/House/Body.Prerequisites.cpp +++ b/src/Ext/House/Body.Prerequisites.cpp @@ -1,11 +1,56 @@ #include "body.h" +bool HouseExt::HasGenericPrerequisite(int idx, HouseClass* const pHouse) +{ + if (idx >= 0 || !pHouse) + return false; + + int absoluteIndex = std::abs(idx); + if (absoluteIndex >= RulesExt::Global()->GenericPrerequisites.Count) + return false; + + DynamicVectorClass selectedPrerequisite = RulesExt::Global()->GenericPrerequisites.GetItem(absoluteIndex); + if (selectedPrerequisite.Count == 0) + return false; + + for (auto idxItem : selectedPrerequisite) + { + if (pHouse->ActiveBuildingTypes.GetItemCount(idxItem) > 0) + return true; + } + + return false; +} + +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); @@ -32,7 +77,7 @@ bool HouseExt::IsAvailableToHouse(HouseClass* const pHouse, TechnoTypeClass* con return inOwners && inRequired && !inForbidden; } -bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const pItem, const std::map& ownedBuildings, bool skipSecretLabChecks) +bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const pItem, bool skipSecretLabChecks) { if (!pThis || !pItem) return false; @@ -45,14 +90,14 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const if (!pItemExt) 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; - // 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; + // Prerequisite.RequiredTheaters check if (pItemExt->Prerequisite_RequiredTheaters.size() > 0) { @@ -66,44 +111,44 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const return false; // BuildLimit checks - int nInstances = 0; - - for (const auto pTechno : TechnoClass::Array) + if (pItem->BuildLimit > 0) { - if (pTechno->Owner == pThis - && pTechno->GetTechnoType() == pItem - && pTechno->IsAlive - && pTechno->Health > 0) + int nInstances = 0; + for (const auto pTechno : TechnoClass::Array) { - nInstances++; + if (pTechno->Owner == pThis + && pTechno->GetTechnoType() == pItem + && pTechno->IsAlive + && pTechno->Health > 0) + { + nInstances++; - if (nInstances >= pItem->BuildLimit) - return false; + if (nInstances >= pItem->BuildLimit) + return false; + } } } - - if (pItem->BuildLimit < 1) + else if (pItem->BuildLimit == 0) + { return false; - - bool prerequisiteNegativeMet = false; // Only one coincidence is needed + } // Ares Prerequisite.Negative list if (pItemExt->Prerequisite_Negative.size() > 0) { for (int idx : pItemExt->Prerequisite_Negative) { - if (idx < 0) // Can be used generic prerequisites in this Ares tag? I have to investigate it but for now we support it... + bool negFound = false; + if (idx < 0) { - // Default prerequisites like POWER, PROC, BARRACKS, FACTORY, ... - prerequisiteNegativeMet = HouseExt::HasGenericPrerequisite(idx, ownedBuildings); + negFound = HouseExt::HasGenericPrerequisite(idx, pThis); } else { - if (ownedBuildings.count(BuildingTypeClass::Array.GetItem(idx)) > 0) - prerequisiteNegativeMet = true; + negFound = pThis->ActiveBuildingTypes.GetItemCount(idx) > 0; } - if (prerequisiteNegativeMet) + if (negFound) return false; } } @@ -112,143 +157,100 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const if (skipSecretLabChecks) return true; + // PrerequisiteOverride (OR logic: if ANY entry is owned, prerequisites are considered met) DynamicVectorClass prerequisiteOverride = pItem->PrerequisiteOverride; - - bool prerequisiteMet = false; // All buildings must appear in the buildings list owner by the house - bool prerequisiteOverrideMet = false; // This tag uses an OR comparator: Only one coincidence is needed - - if (prerequisiteOverride.Count > 0) + for (int idx : prerequisiteOverride) { - for (int idx : prerequisiteOverride) + if (idx < 0) { - if (prerequisiteOverrideMet) - break; - - if (idx < 0) - { - // Default prerequisites like POWER, PROC, BARRACKS, FACTORY, ... - prerequisiteOverrideMet = HouseExt::HasGenericPrerequisite(idx, ownedBuildings); - } - else - { - if (ownedBuildings.count(BuildingTypeClass::Array.GetItem(idx)) > 0) - prerequisiteOverrideMet = true; - } + if (HouseExt::HasGenericPrerequisite(idx, pThis)) + return true; + } + else + { + if (pThis->ActiveBuildingTypes.GetItemCount(idx) > 0) + return true; } } + // Main Prerequisite list (AND logic: ALL entries must be satisfied) + bool prerequisiteMet = true; if (pItemExt->Prerequisite.size() > 0) { - bool found = false; - for (int idx : pItemExt->Prerequisite) { - found = false; + bool found = false; if (idx < 0) { - // Default prerequisites like POWER, PROC, BARRACKS, FACTORY, ... - found = HouseExt::HasGenericPrerequisite(idx, ownedBuildings); + // Also check slave miner as alternative for PROC (-6) + if (idx == -6 && + RulesClass::Instance->PrerequisiteProcAlternate != nullptr && + pThis->ActiveUnitTypes.GetItemCount(RulesClass::Instance->PrerequisiteProcAlternate->ArrayIndex) > 0) + { + found = true; + } + else + { + found = HouseExt::HasGenericPrerequisite(idx, pThis); + } } else { - //auto debugType = BuildingTypeClass::Array.GetItem(idx); - if (ownedBuildings.count(BuildingTypeClass::Array.GetItem(idx)) > 0) - found = true; + found = pThis->ActiveBuildingTypes.GetItemCount(idx) > 0; } if (!found) + { + prerequisiteMet = false; break; + } } - - prerequisiteMet = found; - } - else - { - // No prerequisites list means that always is buildable - prerequisiteMet = true; } + // Ares Prerequisite lists (OR logic between lists, AND logic within each list) bool prerequisiteListsMet = false; - - // Ares Prerequisite lists - if (pItemExt->Prerequisite_Lists.Get() > 0) + if (pItemExt->Prerequisite_Lists.Get() > 0 && !pItemExt->Prerequisite_ListVector.empty()) { - bool found = false; - - for (auto list : pItemExt->Prerequisite_ListVector) + for (const auto& list : pItemExt->Prerequisite_ListVector) { - if (found) - break; - + bool listSatisfied = true; for (int idx : list) { + bool found = false; + if (idx < 0) { - // Default prerequisites like POWER, PROC, BARRACKS, FACTORY, ... - found = HouseExt::HasGenericPrerequisite(idx, ownedBuildings); + if (idx == -6 && + RulesClass::Instance->PrerequisiteProcAlternate != nullptr && + pThis->ActiveUnitTypes.GetItemCount(RulesClass::Instance->PrerequisiteProcAlternate->ArrayIndex) > 0) + { + found = true; + } + else + { + found = HouseExt::HasGenericPrerequisite(idx, pThis); + } } else { - found = false; - - if (ownedBuildings.count(BuildingTypeClass::Array.GetItem(idx)) > 0) - found = true; + found = pThis->ActiveBuildingTypes.GetItemCount(idx) > 0; } if (!found) + { + listSatisfied = false; break; + } } - } - - prerequisiteListsMet = found; - } - - return prerequisiteMet || prerequisiteListsMet || prerequisiteOverrideMet; -} - -bool HouseExt::HasGenericPrerequisite(int idx, const std::map& ownedBuildings) -{ - if (idx >= 0) - return false; - DynamicVectorClass selectedPrerequisite = RulesExt::Global()->GenericPrerequisites.GetItem(std::abs(idx)); - const char* selectedPrerequisiteName = RulesExt::Global()->GenericPrerequisitesNames[std::abs(idx)];// Only used for easy debug - - if (selectedPrerequisite.Count == 0) - return false; - - bool found = false; - - for (auto idxItem : selectedPrerequisite) - { - if (found) - break; - - //auto debugType = BuildingTypeClass::Array.GetItem(idxItem); - if (ownedBuildings.count(BuildingTypeClass::Array.GetItem(idxItem)) > 0) - found = true; - } - - return found; -} - -int HouseExt::FindGenericPrerequisite(const char* id) -{ - if (BuildingTypeClass::FindIndex(id) >= 0) - return INT32_MAX; - - if (RulesExt::Global()->GenericPrerequisitesNames.Count == 0) - RulesExt::FillDefaultPrerequisites(); // needed! - - int i = 0; - for (auto str : RulesExt::Global()->GenericPrerequisitesNames) - { - if (_strcmpi(id, str) == 0) - return i; - - --i; + if (listSatisfied) + { + prerequisiteListsMet = true; + break; + } + } } - return INT32_MAX; // Error + return prerequisiteMet || prerequisiteListsMet; } diff --git a/src/Ext/House/Body.h b/src/Ext/House/Body.h index 1362e7daad..b485bc7ec6 100644 --- a/src/Ext/House/Body.h +++ b/src/Ext/House/Body.h @@ -158,8 +158,8 @@ class HouseExt static CellClass* GetEnemyBaseGatherCell(HouseClass* pTargetHouse, HouseClass* pCurrentHouse, CoordStruct defaultCurrentCoords, SpeedType speedTypeZone, int extraDistance = 0); static void GetAIChronoshiftSupers(HouseClass* pThis, SuperClass*& pSuperCSphere, SuperClass*& pSuperCWarp); static bool IsAvailableToHouse(HouseClass* const pHouse, TechnoTypeClass* const pItem); - static bool PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const pItem, const std::map& ownedBuildings, bool skipSecretLabChecks = false); - static bool HasGenericPrerequisite(int idx, const std::map& ownedBuildings); + static bool PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const pItem, bool skipSecretLabChecks = false); + static bool HasGenericPrerequisite(int idx, HouseClass* const pHouse); static int FindGenericPrerequisite(const char* id); static bool IsDisabledFromShell( diff --git a/src/Ext/Team/Body.NewTeamsSelector.cpp b/src/Ext/Team/Body.NewTeamsSelector.cpp index fd031fa0cc..9281f53f4d 100644 --- a/src/Ext/Team/Body.NewTeamsSelector.cpp +++ b/src/Ext/Team/Body.NewTeamsSelector.cpp @@ -308,7 +308,6 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) int destroyedBridgesCount = 0; int undamagedBridgesCount = 0; std::map ownedRecruitables; - std::map ownedBuildings; bool hasInfantryFactory = false; bool hasUnitFactory = false; bool hasNavalFactory = false; @@ -328,7 +327,6 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (pTechno->Owner == pHouse) { - ++ownedBuildings[static_cast(pBuildingType)]; switch (pBuildingType->Factory) { @@ -799,7 +797,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) // Meets the production prerequisites? if (canBeBuilt) - canBeBuilt = HouseExt::PrerequisitesMet(pHouse, entry.Type, ownedBuildings, false); + canBeBuilt = HouseExt::PrerequisitesMet(pHouse, entry.Type, false); if (!canBeBuilt) { From b4461db0fab08f723f2d48e0613c25432440d0da Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 29 Aug 2026 00:23:01 +0200 Subject: [PATCH 39/59] Optimize PrerequisiteTheaters to DWORD bitmask across TechnoType --- src/Ext/House/Body.Prerequisites.cpp | 8 ++------ src/Ext/TechnoType/Body.cpp | 10 ++++++---- src/Ext/TechnoType/Body.h | 3 ++- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/Ext/House/Body.Prerequisites.cpp b/src/Ext/House/Body.Prerequisites.cpp index afc05f2f4b..c6be2ad2c5 100644 --- a/src/Ext/House/Body.Prerequisites.cpp +++ b/src/Ext/House/Body.Prerequisites.cpp @@ -99,12 +99,8 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const return false; // Prerequisite.RequiredTheaters check - if (pItemExt->Prerequisite_RequiredTheaters.size() > 0) - { - int currentTheaterIndex = (int)ScenarioClass::Instance->Theater; - if (pItemExt->Prerequisite_RequiredTheaters.IndexOf(currentTheaterIndex) < 0) - return false; - } + if (!(pItemExt->PrerequisiteTheaters & (1u << static_cast(ScenarioClass::Instance->Theater)))) + return false; // TechLevel check if (pThis->TechLevel < pItem->TechLevel) diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index 610afe4d98..c936c1b865 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -1284,18 +1284,20 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) } } - // Prerequisite.RequiredTheaters contains a list of theader names + // Prerequisite.RequiredTheaters contains a list of theater names if (pINI->ReadString(pSection, "Prerequisite.RequiredTheaters", "", Phobos::readBuffer) > 0) { char* context = nullptr; - this->Prerequisite_RequiredTheaters.clear(); + this->PrerequisiteTheaters = 0; for (char* cur = strtok_s(Phobos::readBuffer, Phobos::readDelims, &context); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context)) { int index = Theater::FindIndex(cur); if (index != -1) - this->Prerequisite_RequiredTheaters.push_back(index); + this->PrerequisiteTheaters |= (1u << index); + else + Debug::INIParseFailed(pSection, "Prerequisite.RequiredTheaters", cur); } } @@ -1624,7 +1626,7 @@ void TechnoTypeExt::ExtData::Serialize(T& Stm) .Process(this->Explodes_KillPassengers) .Process(this->Explodes_DuringBuildup) - .Process(this->Prerequisite_RequiredTheaters) + .Process(this->PrerequisiteTheaters) .Process(this->Prerequisite) .Process(this->Prerequisite_Negative) .Process(this->Prerequisite_Lists) diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h index f86caecf2a..df652120ea 100644 --- a/src/Ext/TechnoType/Body.h +++ b/src/Ext/TechnoType/Body.h @@ -444,7 +444,7 @@ class TechnoTypeExt std::vector Secret_ForbiddenHouses; // Ares 0.1 - ValueableVector Prerequisite_RequiredTheaters; + DWORD PrerequisiteTheaters; ValueableVector Prerequisite; ValueableVector Prerequisite_Negative; Valueable Prerequisite_Lists; @@ -858,6 +858,7 @@ class TechnoTypeExt , VoiceWeaponAttacks {} , VoiceEliteWeaponAttacks {} + , PrerequisiteTheaters { 0xFFFFFFFF } , InfantryAutoDeploy {} , TurretResponse {} From cc49ea088715c8f73ace285b803520d543a72c0c Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 29 Aug 2026 08:30:00 +0200 Subject: [PATCH 40/59] Add stolen tech checks and correct Prerequisite.Lists boolean logic in PrerequisitesMet --- src/Ext/House/Body.Prerequisites.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Ext/House/Body.Prerequisites.cpp b/src/Ext/House/Body.Prerequisites.cpp index c6be2ad2c5..8ee3b998f9 100644 --- a/src/Ext/House/Body.Prerequisites.cpp +++ b/src/Ext/House/Body.Prerequisites.cpp @@ -98,6 +98,14 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const 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; @@ -248,5 +256,17 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const } } + 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; } From e6aa412bb4127d104921278841a33472b2e3e4ef Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 29 Aug 2026 10:15:07 +0200 Subject: [PATCH 41/59] Add BuildLimit lifetime checks and Phobos BuildLimitGroup in PrerequisitesMet --- src/Ext/House/Body.Prerequisites.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Ext/House/Body.Prerequisites.cpp b/src/Ext/House/Body.Prerequisites.cpp index 8ee3b998f9..c21df68814 100644 --- a/src/Ext/House/Body.Prerequisites.cpp +++ b/src/Ext/House/Body.Prerequisites.cpp @@ -132,11 +132,20 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const } } } + 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.size() > 0) { From d2864560e1cd2f71b895e369a9995930a3ee15b2 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 29 Aug 2026 10:37:57 +0200 Subject: [PATCH 42/59] Add general support for PrerequisiteAlternate technos in GenericPrerequisites --- src/Ext/House/Body.Prerequisites.cpp | 43 +++++++++------------ src/Ext/Rules/Body.cpp | 57 ++++++++++++++++++++++++++-- src/Ext/Rules/Body.h | 2 + 3 files changed, 73 insertions(+), 29 deletions(-) diff --git a/src/Ext/House/Body.Prerequisites.cpp b/src/Ext/House/Body.Prerequisites.cpp index c21df68814..24c16a1c3c 100644 --- a/src/Ext/House/Body.Prerequisites.cpp +++ b/src/Ext/House/Body.Prerequisites.cpp @@ -6,19 +6,29 @@ bool HouseExt::HasGenericPrerequisite(int idx, HouseClass* const pHouse) return false; int absoluteIndex = std::abs(idx); - if (absoluteIndex >= RulesExt::Global()->GenericPrerequisites.Count) - return false; - - DynamicVectorClass selectedPrerequisite = RulesExt::Global()->GenericPrerequisites.GetItem(absoluteIndex); - if (selectedPrerequisite.Count == 0) + auto const pRulesExt = RulesExt::Global(); + if (absoluteIndex >= pRulesExt->GenericPrerequisites.Count) return false; + // Check buildings matching this generic prerequisite + DynamicVectorClass selectedPrerequisite = pRulesExt->GenericPrerequisites.GetItem(absoluteIndex); for (auto idxItem : selectedPrerequisite) { if (pHouse->ActiveBuildingTypes.GetItemCount(idxItem) > 0) return true; } + // Check alternate technos (vehicles, infantry, aircraft, etc.) + if (absoluteIndex < pRulesExt->GenericPrerequisitesAlternates.Count) + { + DynamicVectorClass selectedAlternates = pRulesExt->GenericPrerequisitesAlternates.GetItem(absoluteIndex); + for (auto pTechnoType : selectedAlternates) + { + if (pHouse->CountOwnedNow(pTechnoType) > 0) + return true; + } + } + return false; } @@ -196,17 +206,7 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const if (idx < 0) { - // Also check slave miner as alternative for PROC (-6) - if (idx == -6 && - RulesClass::Instance->PrerequisiteProcAlternate != nullptr && - pThis->ActiveUnitTypes.GetItemCount(RulesClass::Instance->PrerequisiteProcAlternate->ArrayIndex) > 0) - { - found = true; - } - else - { - found = HouseExt::HasGenericPrerequisite(idx, pThis); - } + found = HouseExt::HasGenericPrerequisite(idx, pThis); } else { @@ -234,16 +234,7 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const if (idx < 0) { - if (idx == -6 && - RulesClass::Instance->PrerequisiteProcAlternate != nullptr && - pThis->ActiveUnitTypes.GetItemCount(RulesClass::Instance->PrerequisiteProcAlternate->ArrayIndex) > 0) - { - found = true; - } - else - { - found = HouseExt::HasGenericPrerequisite(idx, pThis); - } + found = HouseExt::HasGenericPrerequisite(idx, pThis); } else { diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index 12cdaf098e..04034e5acf 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -389,13 +389,63 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) 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'); + } + + _snprintf_s(keyName, _TRUNCATE, "Prerequisite%sAlternate", formattedName); + + if (pINI->ReadString("General", keyName, "", Phobos::readBuffer) > 0) + { + 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 @@ -624,6 +674,7 @@ void RulesExt::ExtData::Serialize(T& Stm) .Process(this->ShowDesignatorRange) .Process(this->GenericPrerequisites) .Process(this->GenericPrerequisitesNames) + .Process(this->GenericPrerequisitesAlternates) .Process(this->NewTeamsSelector) .Process(this->NewTeamsSelector_SplitTriggersByCategory) .Process(this->NewTeamsSelector_EnableFallback) diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h index 38716c277c..bc14283bd2 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -264,6 +264,7 @@ class RulesExt DynamicVectorClass> GenericPrerequisites; DynamicVectorClass GenericPrerequisitesNames; + DynamicVectorClass> GenericPrerequisitesAlternates; Valueable NewTeamsSelector; Valueable NewTeamsSelector_SplitTriggersByCategory; @@ -433,6 +434,7 @@ class RulesExt , ShowDesignatorRange { true } , GenericPrerequisites { } , GenericPrerequisitesNames { } + , GenericPrerequisitesAlternates { } , NewTeamsSelector { false } , NewTeamsSelector_SplitTriggersByCategory { true } , NewTeamsSelector_EnableFallback { false } From b20b8a024f8f938a8c373fff7bd1861d68e90a9f Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 29 Aug 2026 10:58:18 +0200 Subject: [PATCH 43/59] Normalize prerequisite system with building upgrades, nested generic prerequisites and case-insensitive matching --- src/Ext/House/Body.Prerequisites.cpp | 119 +++++++++++++++------------ src/Ext/House/Body.h | 2 + src/Ext/Rules/Body.cpp | 69 +++++++++------- 3 files changed, 109 insertions(+), 81 deletions(-) diff --git a/src/Ext/House/Body.Prerequisites.cpp b/src/Ext/House/Body.Prerequisites.cpp index 24c16a1c3c..28d4c23745 100644 --- a/src/Ext/House/Body.Prerequisites.cpp +++ b/src/Ext/House/Body.Prerequisites.cpp @@ -1,4 +1,41 @@ #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::ExtMap.Find(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) { @@ -10,18 +47,26 @@ bool HouseExt::HasGenericPrerequisite(int idx, HouseClass* const pHouse) if (absoluteIndex >= pRulesExt->GenericPrerequisites.Count) return false; - // Check buildings matching this generic prerequisite - DynamicVectorClass selectedPrerequisite = pRulesExt->GenericPrerequisites.GetItem(absoluteIndex); + // Check buildings and nested generic prerequisites matching this generic prerequisite + DynamicVectorClass const& selectedPrerequisite = pRulesExt->GenericPrerequisites.GetItem(absoluteIndex); for (auto idxItem : selectedPrerequisite) { - if (pHouse->ActiveBuildingTypes.GetItemCount(idxItem) > 0) - return true; + 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 selectedAlternates = pRulesExt->GenericPrerequisitesAlternates.GetItem(absoluteIndex); + DynamicVectorClass const& selectedAlternates = pRulesExt->GenericPrerequisitesAlternates.GetItem(absoluteIndex); for (auto pTechnoType : selectedAlternates) { if (pHouse->CountOwnedNow(pTechnoType) > 0) @@ -32,6 +77,14 @@ bool HouseExt::HasGenericPrerequisite(int idx, HouseClass* const pHouse) 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) @@ -157,21 +210,11 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const return false; // Ares Prerequisite.Negative list - if (pItemExt->Prerequisite_Negative.size() > 0) + if (!pItemExt->Prerequisite_Negative.empty()) { for (int idx : pItemExt->Prerequisite_Negative) { - bool negFound = false; - if (idx < 0) - { - negFound = HouseExt::HasGenericPrerequisite(idx, pThis); - } - else - { - negFound = pThis->ActiveBuildingTypes.GetItemCount(idx) > 0; - } - - if (negFound) + if (HouseExt::HasPrerequisite(pThis, idx)) return false; } } @@ -181,39 +224,20 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const return true; // PrerequisiteOverride (OR logic: if ANY entry is owned, prerequisites are considered met) - DynamicVectorClass prerequisiteOverride = pItem->PrerequisiteOverride; + DynamicVectorClass const& prerequisiteOverride = pItem->PrerequisiteOverride; for (int idx : prerequisiteOverride) { - if (idx < 0) - { - if (HouseExt::HasGenericPrerequisite(idx, pThis)) - return true; - } - else - { - if (pThis->ActiveBuildingTypes.GetItemCount(idx) > 0) - return true; - } + if (HouseExt::HasPrerequisite(pThis, idx)) + return true; } // Main Prerequisite list (AND logic: ALL entries must be satisfied) bool prerequisiteMet = true; - if (pItemExt->Prerequisite.size() > 0) + if (!pItemExt->Prerequisite.empty()) { for (int idx : pItemExt->Prerequisite) { - bool found = false; - - if (idx < 0) - { - found = HouseExt::HasGenericPrerequisite(idx, pThis); - } - else - { - found = pThis->ActiveBuildingTypes.GetItemCount(idx) > 0; - } - - if (!found) + if (!HouseExt::HasPrerequisite(pThis, idx)) { prerequisiteMet = false; break; @@ -230,18 +254,7 @@ bool HouseExt::PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const bool listSatisfied = true; for (int idx : list) { - bool found = false; - - if (idx < 0) - { - found = HouseExt::HasGenericPrerequisite(idx, pThis); - } - else - { - found = pThis->ActiveBuildingTypes.GetItemCount(idx) > 0; - } - - if (!found) + if (!HouseExt::HasPrerequisite(pThis, idx)) { listSatisfied = false; break; diff --git a/src/Ext/House/Body.h b/src/Ext/House/Body.h index b485bc7ec6..6825a0dbd8 100644 --- a/src/Ext/House/Body.h +++ b/src/Ext/House/Body.h @@ -159,7 +159,9 @@ class HouseExt static void GetAIChronoshiftSupers(HouseClass* pThis, SuperClass*& pSuperCSphere, SuperClass*& pSuperCWarp); static bool IsAvailableToHouse(HouseClass* const pHouse, TechnoTypeClass* const pItem); static bool PrerequisitesMet(HouseClass* const pThis, TechnoTypeClass* const pItem, bool skipSecretLabChecks = false); + static bool HasBuildingPrerequisite(HouseClass* const pHouse, int idxBuildingType); static bool HasGenericPrerequisite(int idx, HouseClass* const pHouse); + static bool HasPrerequisite(HouseClass* const pHouse, int idx); static int FindGenericPrerequisite(const char* id); static bool IsDisabledFromShell( diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index 04034e5acf..7c862df4c4 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -410,9 +411,20 @@ void RulesExt::ExtData::LoadAfterTypeData(RulesClass* pThis, CCINIClass* pINI) 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)) @@ -469,7 +481,6 @@ void RulesExt::FillDefaultPrerequisites() if (RulesExt::Global()->GenericPrerequisitesNames.Count != 0) return; - CCINIClass::INI_Rules; DynamicVectorClass empty; RulesExt::Global()->GenericPrerequisitesNames.AddItem("-"); // Official index: 0 RulesExt::Global()->GenericPrerequisites.AddItem(empty); @@ -495,50 +506,52 @@ void RulesExt::FillDefaultPrerequisites() { DynamicVectorClass objectsList; char* context = nullptr; - CCINIClass::INI_Rules->ReadString("GenericPrerequisites", CCINIClass::INI_Rules->GetKeyName("GenericPrerequisites", i), "", Phobos::readBuffer); - char* name = (char*)CCINIClass::INI_Rules->GetKeyName("GenericPrerequisites", i); + 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); + } } - int index = RulesExt::Global()->GenericPrerequisitesNames.FindItemIndex(name); - if (index < 7 && index > 0) + // Find existing name using case-insensitive comparison + int existingIndex = -1; + for (int k = 0; k < RulesExt::Global()->GenericPrerequisitesNames.Count; ++k) { - // Overwrites a vanilla generic prerequisite - RulesExt::Global()->GenericPrerequisites[index] = objectsList; + 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(name); + RulesExt::Global()->GenericPrerequisitesNames.AddItem(keyName); RulesExt::Global()->GenericPrerequisites.AddItem(objectsList); } objectsList.Clear(); } - - // Debug - /*if (RulesExt::Global()->GenericPrerequisites.Count > 0) - { - Debug::Log("[GenericPrerequisites] = %d\n", RulesExt::Global()->GenericPrerequisites.Count); - Debug::Log("[GenericPrerequisites] (ID names) = %d\n\n", RulesExt::Global()->GenericPrerequisitesNames.Count); - - for (int i = 0; i < RulesExt::Global()->GenericPrerequisites.Count; i++) - { - auto list = RulesExt::Global()->GenericPrerequisites.GetItem(i); - const auto listName = RulesExt::Global()->GenericPrerequisitesNames[i]; - - for (int j : list) - { - auto pTypeName = BuildingTypeClass::Array->GetItem(j)->ID; - Debug::Log("[GenericPrerequisites][%s](%d) -> [%s](%d)\n", listName, i, pTypeName, j); - } - } - }*/ } // ============================= From a9efc10de21e9661dd1692d57ec7c4e1abfcba2d Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 29 Aug 2026 11:17:28 +0200 Subject: [PATCH 44/59] Optimize AI Team Selector data structures, eliminate temporary vector allocations, and use O(1) condition checks --- src/Ext/Scenario/Hooks.NewTeamsSelector.cpp | 2 - src/Ext/Team/Body.NewTeamsSelector.cpp | 696 ++++++-------------- src/Ext/Team/Body.h | 15 +- 3 files changed, 212 insertions(+), 501 deletions(-) diff --git a/src/Ext/Scenario/Hooks.NewTeamsSelector.cpp b/src/Ext/Scenario/Hooks.NewTeamsSelector.cpp index 918cb56a06..017df65820 100644 --- a/src/Ext/Scenario/Hooks.NewTeamsSelector.cpp +++ b/src/Ext/Scenario/Hooks.NewTeamsSelector.cpp @@ -18,11 +18,9 @@ DEFINE_HOOK(0x687C9B, ReadScenarioINI_AITeamSelector_PreloadValidTriggers, 0x7) int parentCountryTypeIdx = pHouse->Type->FindParentCountryIndex(); // ParentCountry can change the House in a SP map int houseTypeIdx = parentCountryTypeIdx >= 0 ? parentCountryTypeIdx : pHouse->Type->ArrayIndex; // Indexes in AITriggers section are 1-based - int houseIdx = pHouse->ArrayIndex; int parentCountrySideTypeIdx = parentCountryTypeIdx >= 0 ? pHouse->Type->FindParentCountry()->SideIndex : pHouse->Type->SideIndex; int sideTypeIdx = parentCountrySideTypeIdx >= 0 ? parentCountrySideTypeIdx + 1 : pHouse->Type->SideIndex + 1; // Side indexes in AITriggers section are 1-based - int sideIdx = pHouse->SideIndex + 1; // Side indexes in AITriggers section are 1-based -> unused variable!! for (int i = 0; i < AITriggerTypeClass::Array.Count; i++) { diff --git a/src/Ext/Team/Body.NewTeamsSelector.cpp b/src/Ext/Team/Body.NewTeamsSelector.cpp index 9281f53f4d..f672e5e887 100644 --- a/src/Ext/Team/Body.NewTeamsSelector.cpp +++ b/src/Ext/Team/Body.NewTeamsSelector.cpp @@ -1,4 +1,5 @@ #include "Body.h" +#include enum class teamCategory : int { @@ -168,22 +169,18 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (percentageUnclassifiedTriggers > 0.0 && categoryDice <= unclassifiedValue) { validCategory = teamCategory::Unclassified; - //Debug::Log("New AI team category selection: dice %d <= %d (MIXED)\n", categoryDice, unclassifiedValue); } else if (percentageGroundTriggers > 0.0 && categoryDice <= (unclassifiedValue + groundValue)) { validCategory = teamCategory::Ground; - //Debug::Log("New AI team category selection: dice %d <= %d (mixed: %d%% + GROUND: %d%%)\n", categoryDice, (unclassifiedValue + groundValue), unclassifiedValue, groundValue); } else if (percentageAirTriggers > 0.0 && categoryDice <= (unclassifiedValue + groundValue + airValue)) { validCategory = teamCategory::Air; - //Debug::Log("New AI team category selection: dice %d <= %d (mixed: %d%% + ground: %d%% + AIR: %d%%)\n", categoryDice, (unclassifiedValue + groundValue + airValue), unclassifiedValue, groundValue, airValue); } else if (percentageNavalTriggers > 0.0 && categoryDice <= (unclassifiedValue + groundValue + airValue + navalValue)) { validCategory = teamCategory::Naval; - //Debug::Log("New AI team category selection: dice %d <= %d (mixed: %d%% + ground: %d%% + air: %d%% + NAVAL: %d%%)\n", categoryDice, (unclassifiedValue + groundValue + airValue + navalValue), unclassifiedValue, groundValue, airValue, navalValue); } else { @@ -219,14 +216,8 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } } - int parentCountryTypeIdx = pHouse->Type->FindParentCountryIndex(); // ParentCountry can change the House in a SP map - int houseTypeIdx = parentCountryTypeIdx >= 0 ? parentCountryTypeIdx : pHouse->Type->ArrayIndex; // Indexes in AITriggers section are 1-based int houseIdx = pHouse->ArrayIndex; - int parentCountrySideTypeIdx = parentCountryTypeIdx >= 0 ? pHouse->Type->FindParentCountry()->SideIndex : pHouse->Type->SideIndex; - int sideTypeIdx = parentCountrySideTypeIdx >= 0 ? parentCountrySideTypeIdx + 1 : pHouse->Type->SideIndex + 1; // Side indexes in AITriggers section are 1-based - int sideIdx = pHouse->SideIndex + 1; // Side indexes in AITriggers section are 1-based -> unused variable!! - auto houseDifficulty = pHouse->AIDifficulty; int minBaseDefenseTeams = RulesClass::Instance->MinimumAIDefensiveTeams.GetItem((int)houseDifficulty); int maxBaseDefenseTeams = RulesClass::Instance->MaximumAIDefensiveTeams.GetItem((int)houseDifficulty); @@ -259,28 +250,8 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) // We will use these values for discarding triggers int defensiveTeamsLimit = RulesClass::Instance->UseMinDefenseRule ? minBaseDefenseTeams : maxBaseDefenseTeams; - bool hasReachedMaxTeamsLimit = activeTeams < maxTeamsLimit ? false : true; - bool hasReachedMaxDefensiveTeamsLimit = activeDefenseTeamsCount < defensiveTeamsLimit ? false : true; - - /*Debug::Log("\n=====================\n[%s] ACTIVE TEAMS: %d / %d (of them, defensive teams: %d / %d)\n", pHouse->Type->ID, activeTeams, maxTeamsLimit, activeDefenseTeamsCount, defensiveTeamsLimit); - for (auto team : activeTeamsList) - { - Debug::Log("[%s](%d) : %s%s\n", team->Type->ID, team->TotalObjects, team->Type->Name, team->Type->IsBaseDefense ? " -> is DEFENDER team" : ""); - Debug::Log(" IsMoving: %d, IsFullStrength: %d, IsUnderStrength: %d\n", team->IsMoving, team->IsFullStrength, team->IsUnderStrength); - int i = 0; - - for (auto entry : team->Type->TaskForce->Entries) - { - if (entry.Type && entry.Amount > 0) - { - if (entry.Type) - Debug::Log("\t[%s]: %d / %d\n", entry.Type->ID, team->CountObjects[i], entry.Amount); - } - - i++; - } - } - Debug::Log("\n=====================\n");*/ + bool hasReachedMaxTeamsLimit = activeTeams >= maxTeamsLimit; + bool hasReachedMaxDefensiveTeamsLimit = activeDefenseTeamsCount >= defensiveTeamsLimit; // Check if the next team must be a defensive team bool onlyPickDefensiveTeams = false; @@ -290,12 +261,8 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if ((defensiveDice < defenseTeamSelectionThreshold) && !hasReachedMaxDefensiveTeamsLimit && !isCampaign) onlyPickDefensiveTeams = true; - //if (hasReachedMaxDefensiveTeamsLimit) - //Debug::Log("AITeamsSelector - House [%s] (idx: %d) reached the MaximumAIDefensiveTeams value!\n", pHouse->Type->ID, pHouse->ArrayIndex); - if (hasReachedMaxTeamsLimit) { - //Debug::Log("AITeamsSelector - House [%s] (idx: %d) reached the TotalAITeamCap value!\n", pHouse->Type->ID, pHouse->ArrayIndex); Debug::Log("AITeamsSelector - House %d [%s](%s) reached the TotalAITeamCap (%d)! Skipping operation this time...\n", pHouse->ArrayIndex, pHouse->PlainName, pHouse->Type->ID, defenseTeamSelectionThreshold); return SkipCode; } @@ -307,7 +274,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) int destroyedBridgesCount = 0; int undamagedBridgesCount = 0; - std::map ownedRecruitables; + std::unordered_map ownedRecruitables; bool hasInfantryFactory = false; bool hasUnitFactory = false; bool hasNavalFactory = false; @@ -327,7 +294,6 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (pTechno->Owner == pHouse) { - switch (pBuildingType->Factory) { case AbstractType::InfantryType: @@ -384,25 +350,16 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (hasInfantryFactory || hasUnitFactory || hasAircraftFactory || hasNavalFactory) canAutocreate = true; - /*Debug::Log("AITeamsSelector - The house %d [%s](%s) have these buildings:\n", pHouse->ArrayIndex, pHouse->PlainName, pHouse->Type->ID); - - for (auto pBuilding : ownedBuildings) - { - Debug::Log("[%d] => %s : %d\n", pBuilding.first->ArrayIndex, pBuilding.first->ID, pBuilding.second); - }*/ - HouseClass* targetHouse = nullptr; if (pHouse->EnemyHouseIndex >= 0) targetHouse = HouseClass::Array.GetItem(pHouse->EnemyHouseIndex); bool onlyCheckImportantTriggers = false; - bool ignoreGlobalAITriggers = ScenarioClass::Instance->IgnoreGlobalAITriggers; const auto pHouseExt = HouseExt::ExtMap.Find(pHouse); double maxPriority = 5000.0; // Gather all the trigger candidates into one place for posterior fast calculations - //for (auto const pTrigger : *AITriggerTypeClass::Array) for (int triggerIdx : pHouseExt->AITriggers_ValidList) { const auto pTrigger = AITriggerTypeClass::Array.GetItem(triggerIdx); @@ -414,10 +371,6 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if ((onlyPickDefensiveTeams && !pTrigger->IsForBaseDefense) || (hasReachedMaxDefensiveTeamsLimit && pTrigger->IsForBaseDefense)) continue; - int triggerHouse = pTrigger->HouseIndex; - int triggerSide = pTrigger->SideIndex; - - //pTrigger->OwnerHouseType; if (pTrigger->Team1->TechLevel > pHouse->TechLevel) continue; @@ -430,7 +383,6 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } // The trigger must be compatible with the owner - //if ((triggerHouse == -1 || houseTypeIdx == triggerHouse) && (triggerSide == 0 || sideTypeIdx == triggerSide)) { // "ConditionType=-1" will be skipped, always is valid if ((int)pTrigger->ConditionType >= 0) @@ -438,210 +390,145 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if ((int)pTrigger->ConditionType == 0) { // Simulate case 0: "enemy owns" - if (!pTrigger->ConditionObject) - continue; - - std::vector list; - list.push_back(pTrigger->ConditionObject); - bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, targetHouse, true, list); - - if (!isConditionMet) + if (!pTrigger->ConditionObject || !TeamExt::EnemyOwns(pTrigger, pHouse, targetHouse, true, pTrigger->ConditionObject)) continue; } else if ((int)pTrigger->ConditionType == 1) { // Simulate case 1: "house owns" - if (!pTrigger->ConditionObject) - continue; - - std::vector list; - list.push_back(pTrigger->ConditionObject); - bool isConditionMet = TeamExt::HouseOwns(pTrigger, pHouse, false, list); - - if (!isConditionMet) + if (!pTrigger->ConditionObject || !TeamExt::HouseOwns(pTrigger, pHouse, false, pTrigger->ConditionObject)) continue; - }// TO-DO: Case 2 & 3: https://modenc.renegadeprojects.com/AITriggerTypes + } else if ((int)pTrigger->ConditionType == 4) { - // Simulate case 5: "Enemy house economy threshold?" - bool isConditionMet = pTrigger->HouseCredits(nullptr, targetHouse); - - if (!isConditionMet) + // Simulate case 4: "Enemy house economy threshold?" + if (!pTrigger->HouseCredits(nullptr, targetHouse)) continue; } else if ((int)pTrigger->ConditionType == 5) { - // Simulate case 5: "Iron Courtain is charged?" - bool isConditionMet = pTrigger->IronCurtainCharged(pHouse, nullptr); - - if (!isConditionMet) + // Simulate case 5: "Iron Curtain is charged?" + if (!pTrigger->IronCurtainCharged(pHouse, nullptr)) continue; } else if ((int)pTrigger->ConditionType == 6) { // Simulate case 6: "Chronosphere is charged?" - bool isConditionMet = pTrigger->ChronoSphereCharged(pHouse, nullptr); - - if (!isConditionMet) + if (!pTrigger->ChronoSphereCharged(pHouse, nullptr)) continue; } else if ((int)pTrigger->ConditionType == 7) { // Simulate case 7: "civilian owns" - if (!pTrigger->ConditionObject) - continue; - - std::vector list; - list.push_back(pTrigger->ConditionObject); - bool isConditionMet = TeamExt::NeutralOwns(pTrigger, list); - - if (!isConditionMet) + if (!pTrigger->ConditionObject || !TeamExt::NeutralOwns(pTrigger, pTrigger->ConditionObject)) continue; } else if ((int)pTrigger->ConditionType == 8) { - // Simulate case 0: "enemy owns" but instead of restrict it against the main enemy house it is done against all enemies - if (!pTrigger->ConditionObject) - continue; - - std::vector list; - list.push_back(pTrigger->ConditionObject); - bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, nullptr, false, list); - - if (!isConditionMet) + // Simulate case 8: "enemy owns" across all enemies + if (!pTrigger->ConditionObject || !TeamExt::EnemyOwns(pTrigger, pHouse, nullptr, false, pTrigger->ConditionObject)) continue; } else if ((int)pTrigger->ConditionType == 9) { - if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) + auto const listIdx = pTrigger->Conditions[3].ComparatorOperand; + if (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) { - // New case 9: Like in case 0 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the enemy. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); - bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, targetHouse, false, list); - - if (!isConditionMet) + const auto& list = RulesExt::Global()->AITargetTypesLists[listIdx]; + if (!TeamExt::EnemyOwns(pTrigger, pHouse, targetHouse, false, list)) continue; } } else if ((int)pTrigger->ConditionType == 10) { - if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) + auto const listIdx = pTrigger->Conditions[3].ComparatorOperand; + if (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) { - // New case 10: Like in case 1 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the house. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); - bool isConditionMet = TeamExt::HouseOwns(pTrigger, pHouse, false, list); - - if (!isConditionMet) + const auto& list = RulesExt::Global()->AITargetTypesLists[listIdx]; + if (!TeamExt::HouseOwns(pTrigger, pHouse, false, list)) continue; } } else if ((int)pTrigger->ConditionType == 11) { - if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) + auto const listIdx = pTrigger->Conditions[3].ComparatorOperand; + if (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) { - // New case 11: Like in case 7 but instead of 1 unit for comparisons there is a full list from [AITargetTypes] owned by the Civilians. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); - bool isConditionMet = TeamExt::NeutralOwns(pTrigger, list); - - if (!isConditionMet) + const auto& list = RulesExt::Global()->AITargetTypesLists[listIdx]; + if (!TeamExt::NeutralOwns(pTrigger, list)) continue; } } else if ((int)pTrigger->ConditionType == 12) { - if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) + auto const listIdx = pTrigger->Conditions[3].ComparatorOperand; + if (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) { - // New case 12: Like in case 0 & 9 but instead of a specific enemy this checks in all enemies. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); - bool isConditionMet = TeamExt::EnemyOwns(pTrigger, pHouse, nullptr, false, list); - - if (!isConditionMet) + const auto& list = RulesExt::Global()->AITargetTypesLists[listIdx]; + if (!TeamExt::EnemyOwns(pTrigger, pHouse, nullptr, false, list)) continue; } } else if ((int)pTrigger->ConditionType == 13) { - if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) + auto const listIdx = pTrigger->Conditions[3].ComparatorOperand; + if (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) { - // New case 13: Like in case 1 & 10 but instead checking the house now checks the allies. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); - bool isConditionMet = TeamExt::HouseOwns(pTrigger, pHouse, true, list); - - if (!isConditionMet) + const auto& list = RulesExt::Global()->AITargetTypesLists[listIdx]; + if (!TeamExt::HouseOwns(pTrigger, pHouse, true, list)) continue; } } else if ((int)pTrigger->ConditionType == 14) { - if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) + auto const listIdx = pTrigger->Conditions[3].ComparatorOperand; + if (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) { - // New case 14: Like in case 9 but instead of meet any comparison now is required all. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); - bool isConditionMet = TeamExt::EnemyOwnsAll(pTrigger, pHouse, targetHouse, list); - - if (!isConditionMet) + const auto& list = RulesExt::Global()->AITargetTypesLists[listIdx]; + if (!TeamExt::EnemyOwnsAll(pTrigger, pHouse, targetHouse, list)) continue; } } else if ((int)pTrigger->ConditionType == 15) { - if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) + auto const listIdx = pTrigger->Conditions[3].ComparatorOperand; + if (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) { - // New case 15: Like in case 10 but instead of meet any comparison now is required all. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); - bool isConditionMet = TeamExt::HouseOwnsAll(pTrigger, pHouse, list); - - if (!isConditionMet) + const auto& list = RulesExt::Global()->AITargetTypesLists[listIdx]; + if (!TeamExt::HouseOwnsAll(pTrigger, pHouse, list)) continue; } } else if ((int)pTrigger->ConditionType == 16) { - if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) + auto const listIdx = pTrigger->Conditions[3].ComparatorOperand; + if (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) { - // New case 16: Like in case 11 but instead of meet any comparison now is required all. - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); - bool isConditionMet = TeamExt::NeutralOwnsAll(pTrigger, list); - - if (!isConditionMet) + const auto& list = RulesExt::Global()->AITargetTypesLists[listIdx]; + if (!TeamExt::NeutralOwnsAll(pTrigger, list)) continue; } } else if ((int)pTrigger->ConditionType == 17) { - if (pTrigger->Conditions[3].ComparatorOperand < (int)RulesExt::Global()->AITargetTypesLists.size()) + auto const listIdx = pTrigger->Conditions[3].ComparatorOperand; + if (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) { - // New case 17: Like in case 14 but instead of meet any comparison now is required all. Check all enemies - // Caution: Little Endian hexadecimal value stored here: 00000000000000000000000000000000000000000000000000000000AABBCCDD; examples: 255 is 0xFF (in AA) and 256 is 0x0001 (in AABB) - std::vector list = RulesExt::Global()->AITargetTypesLists.at(pTrigger->Conditions[3].ComparatorOperand); - bool isConditionMet = TeamExt::EnemyOwnsAll(pTrigger, pHouse, nullptr, list); - - if (!isConditionMet) + const auto& list = RulesExt::Global()->AITargetTypesLists[listIdx]; + if (!TeamExt::EnemyOwnsAll(pTrigger, pHouse, nullptr, list)) continue; } } else if ((int)pTrigger->ConditionType == 18) { // New case 18: Check destroyed bridges - bool isConditionMet = TeamExt::CountConditionMet(pTrigger, destroyedBridgesCount); - - if (!isConditionMet) + if (!TeamExt::CountConditionMet(pTrigger, destroyedBridgesCount)) continue; } else if ((int)pTrigger->ConditionType == 19) { // New case 19: Check undamaged bridges - bool isConditionMet = TeamExt::CountConditionMet(pTrigger, undamagedBridgesCount); - - if (!isConditionMet) + if (!TeamExt::CountConditionMet(pTrigger, undamagedBridgesCount)) continue; } else @@ -680,7 +567,6 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) // Analyze what kind of category is this main team if the feature is enabled if (splitTriggersByCategory) { - //Debug::Log("DEBUG: TaskForce [%s] members:\n", pTrigger->Team1->TaskForce->ID); // TaskForces are limited to 6 entries for (int i = 0; i < 6; i++) { @@ -697,7 +583,6 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) { // This unit is from air category entryIsCategory = teamCategory::Air; - //Debug::Log("\t[%s](%d) is in AIR category.\n", entry.Type->ID, entry.Amount); } else { @@ -713,7 +598,6 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) { // This unit is from naval category entryIsCategory = teamCategory::Naval; - //Debug::Log("\t[%s](%d) is in NAVAL category.\n", entry.Type->ID, entry.Amount); } if (pTechnoTypeExt->ConsideredVehicle @@ -722,7 +606,6 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) { // This unit is from ground category entryIsCategory = teamCategory::Ground; - //Debug::Log("\t[%s](%d) is in GROUND category.\n", entry.Type->ID, entry.Amount); } } @@ -738,12 +621,10 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } } - //Debug::Log("DEBUG: This team is a category %d (1:Ground, 2:Air, 3:Naval, 4:Mixed).\n", teamIsCategory); - // Si existe este valor y el team es MIXTO se sobreescribe el tipo de categoría + // If this value is set and the team is MIXED, override category type if (teamIsCategory == teamCategory::Unclassified && mergeUnclassifiedCategoryWith >= 0) { - //Debug::Log("DEBUG: MIXED category forced to work as category %d.\n", mergeUnclassifiedCategoryWith); teamIsCategory = (teamCategory)mergeUnclassifiedCategoryWith; } @@ -823,7 +704,8 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) // Check if each unit in the taskforce has the available recruitable units in the map if (allObjectsCanBeBuiltOrRecruited && entry.Amount > 0) { - int recruits = ownedRecruitables.count(entry.Type) > 0 ? ownedRecruitables[entry.Type] : 0; + auto const it = ownedRecruitables.find(entry.Type); + int const recruits = (it != ownedRecruitables.end()) ? it->second : 0; if (recruits < entry.Amount) { @@ -860,7 +742,6 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } // Passed all checks, save this trigger for later. - // The idea behind this is to simulate an ordered list of weights and once we throw the dice we'll know the winner trigger: More weight means more possibilities to be selected. totalWeight += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; TriggerElementWeight item; item.Trigger = pTrigger; @@ -951,26 +832,16 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) AITriggerTypeClass* selectedTrigger = nullptr; double weightDice = 0.0; - double lastWeight = 0.0; bool found = false; switch (validCategory) { case teamCategory::None: weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeight) * 1.0; - //Debug::Log("AITeamsSelector - Weight Dice: %f\n", weightDice); Debug::Log("AITeamsSelector - Picking 1 team from the %d available...\n", validTriggerCandidates.Count); - // Debug - /*Debug::Log("DEBUG: Candidate AI triggers list:\n"); - for (TriggerElementWeight element : validTriggerCandidates) - { - Debug::Log("Selection weight: %f, [%s][%s](CW: %f): %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Weight_Current, element.Trigger->Team1->Name); - }*/ for (auto element : validTriggerCandidates) { - lastWeight = element.Weight; - if (weightDice < element.Weight && !found) { selectedTrigger = element.Trigger; @@ -981,20 +852,10 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) case teamCategory::Ground: weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightGroundOnly) * 1.0; - //Debug::Log("AITeamsSelector - Weight Dice: %f\n", weightDice); Debug::Log("AITeamsSelector - Picking 1 team (of the category 'GROUND') from the %d available...\n", validTriggerCandidatesGroundOnly.Count); - // Debug - /*Debug::Log("DEBUG: Candidate AI triggers list:\n"); - for (TriggerElementWeight element : validTriggerCandidatesGroundOnly) - { - Debug::Log("Selection weight: %f, [%s][%s](CW: %f): %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Weight_Current, element.Trigger->Team1->Name); - }*/ - for (auto element : validTriggerCandidatesGroundOnly) { - lastWeight = element.Weight; - if (weightDice < element.Weight && !found) { selectedTrigger = element.Trigger; @@ -1005,20 +866,10 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) case teamCategory::Unclassified: weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightUnclassifiedOnly) * 1.0; - //Debug::Log("AITeamsSelector - Weight Dice: %f\n", weightDice); Debug::Log("AITeamsSelector - Picking 1 team (of the category 'UNCLASSIFIED') from the %d available...\n", validTriggerCandidatesUnclassifiedOnly.Count); - // Debug - /*Debug::Log("DEBUG: Candidate AI triggers list:\n"); - for (TriggerElementWeight element : validTriggerCandidatesUnclassifiedOnly) - { - Debug::Log("Selection weight: %f, [%s][%s](CW: %f): %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Weight_Current, element.Trigger->Team1->Name); - }*/ - for (auto element : validTriggerCandidatesUnclassifiedOnly) { - lastWeight = element.Weight; - if (weightDice < element.Weight && !found) { selectedTrigger = element.Trigger; @@ -1029,20 +880,10 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) case teamCategory::Naval: weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightNavalOnly) * 1.0; - //Debug::Log("AITeamsSelector - Weight Dice: %f\n", weightDice); Debug::Log("AITeamsSelector - Picking 1 team (of the category 'NAVAL') from the %d available...\n", validTriggerCandidatesNavalOnly.Count); - // Debug - /*Debug::Log("DEBUG: Candidate AI triggers list:\n"); - for (TriggerElementWeight element : validTriggerCandidatesNavalOnly) - { - Debug::Log("Selection weight: %f, [%s][%s](CW: %f): %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Weight_Current, element.Trigger->Team1->Name); - }*/ - for (auto element : validTriggerCandidatesNavalOnly) { - lastWeight = element.Weight; - if (weightDice < element.Weight && !found) { selectedTrigger = element.Trigger; @@ -1053,20 +894,10 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) case teamCategory::Air: weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightAirOnly) * 1.0; - //Debug::Log("AITeamsSelector - Weight Dice: %f\n", weightDice); Debug::Log("AITeamsSelector - Picking 1 team (of the category 'AIR') from the %d available...\n", validTriggerCandidatesAirOnly.Count); - // Debug - /*Debug::Log("DEBUG: Candidate AI triggers list:\n"); - for (TriggerElementWeight element : validTriggerCandidatesAirOnly) - { - Debug::Log("Selection weight: %f, [%s][%s](CW: %f): %s\n", element.Weight, element.Trigger->ID, element.Trigger->Team1->ID, element.Trigger->Weight_Current, element.Trigger->Team1->Name); - }*/ - for (auto element : validTriggerCandidatesAirOnly) { - lastWeight = element.Weight; - if (weightDice < element.Weight && !found) { selectedTrigger = element.Trigger; @@ -1096,15 +927,15 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) 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 - int count = 0; + int count1 = 0; for (auto team : activeTeamsList) { if (team->Type == selectedTrigger->Team1) - count++; + count1++; } - if (count < selectedTrigger->Team1->Max) + if (count1 < selectedTrigger->Team1->Max) { if (TeamClass* newTeam1 = selectedTrigger->Team1->CreateTeam(pHouse)) newTeam1->NeedsToDisappear = false; @@ -1114,15 +945,15 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) auto pTriggerTeam2Type = selectedTrigger->Team2; if (pTriggerTeam2Type) { - int count = 0; + int count2 = 0; for (auto team : activeTeamsList) { if (team->Type == pTriggerTeam2Type) - count++; + count2++; } - if (count < pTriggerTeam2Type->Max) + if (count2 < pTriggerTeam2Type->Max) { if (TeamClass* newTeam2 = pTriggerTeam2Type->CreateTeam(pHouse)) newTeam2->NeedsToDisappear = false; @@ -1132,365 +963,244 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) return SkipCode; } -bool TeamExt::HouseOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, bool allies, std::vector list) +bool TeamExt::HouseOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, bool allies, TechnoTypeClass* pItem) { - bool result = false; 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); +} - // Count all objects of the list, like an OR operator - for (auto pItem : list) +bool TeamExt::HouseOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, bool allies, const std::vector& list) +{ + int counter = 0; + if (pHouse) { - for (auto pObject : TechnoClass::Array) + for (auto const pItem : list) { - if (!TechnoExt::IsValidTechno(pObject)) continue; + if (!pItem) continue; - if (((!allies && pObject->Owner == pHouse) || (allies && pHouse != pObject->Owner && pHouse->IsAlliedWith(pObject->Owner))) - && !pObject->Owner->Type->MultiplayPassive - && pObject->GetTechnoType() == pItem) + if (!allies) + { + counter += pHouse->CountOwnedAndPresent(pItem); + } + else { - counter++; + for (auto const pOtherHouse : HouseClass::Array) + { + if (pOtherHouse != pHouse && pHouse->IsAlliedWith(pOtherHouse) && !pOtherHouse->Type->MultiplayPassive) + { + counter += pOtherHouse->CountOwnedAndPresent(pItem); + } + } } } } + return TeamExt::CountConditionMet(pThis, counter); +} - switch (pThis->Conditions->ComparatorOperand) +bool TeamExt::EnemyOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, bool onlySelectedEnemy, TechnoTypeClass* pItem) +{ + int counter = 0; + if (pItem && pHouse) { - case 0: - result = counter < pThis->Conditions->ComparatorType; - break; - case 1: - result = counter <= pThis->Conditions->ComparatorType; - break; - case 2: - result = counter == pThis->Conditions->ComparatorType; - break; - case 3: - result = counter >= pThis->Conditions->ComparatorType; - break; - case 4: - result = counter > pThis->Conditions->ComparatorType; - break; - case 5: - result = counter != pThis->Conditions->ComparatorType; - break; - default: - break; + 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 result; + return TeamExt::CountConditionMet(pThis, counter); } -bool TeamExt::EnemyOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pSpecificEnemy, bool onlySelectedEnemy, std::vector list) +bool TeamExt::EnemyOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, bool onlySelectedEnemy, const std::vector& list) { - bool result = false; int counter = 0; - - pSpecificEnemy = pSpecificEnemy && pHouse->IsAlliedWith(pSpecificEnemy) ? nullptr : pSpecificEnemy; - pSpecificEnemy = onlySelectedEnemy ? pSpecificEnemy : nullptr; - - // Count all objects of the list, like an OR operator - for (auto const pItem : list) + if (pHouse) { - for (auto const pObject : TechnoClass::Array) + for (auto const pItem : list) { - if (!TechnoExt::IsValidTechno(pObject)) continue; + if (!pItem) continue; - const auto pItemType = pObject->GetTechnoType(); - if (pItemType == pItem - && pObject->Owner != pHouse - && (!pSpecificEnemy && !pHouse->IsAlliedWith(pObject->Owner) || (pSpecificEnemy && pSpecificEnemy == pObject->Owner)) - && !pObject->Owner->Type->MultiplayPassive) + if (onlySelectedEnemy && pEnemy && !pHouse->IsAlliedWith(pEnemy)) { - counter++; + 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); +} - switch (pThis->Conditions->ComparatorOperand) +bool TeamExt::NeutralOwns(AITriggerTypeClass* pThis, TechnoTypeClass* pItem) +{ + int counter = 0; + if (pItem) { - case 0: - result = counter < pThis->Conditions->ComparatorType; - break; - case 1: - result = counter <= pThis->Conditions->ComparatorType; - break; - case 2: - result = counter == pThis->Conditions->ComparatorType; - break; - case 3: - result = counter >= pThis->Conditions->ComparatorType; - break; - case 4: - result = counter > pThis->Conditions->ComparatorType; - break; - case 5: - result = counter != pThis->Conditions->ComparatorType; - break; - default: - break; + for (auto const pOtherHouse : HouseClass::Array) + { + if (_stricmp(SideClass::Array.GetItem(pOtherHouse->Type->SideIndex)->Name, "Civilian") == 0) + { + counter += pOtherHouse->CountOwnedAndPresent(pItem); + } + } } - - return result; + return TeamExt::CountConditionMet(pThis, counter); } -bool TeamExt::NeutralOwns(AITriggerTypeClass* pThis, std::vector list) +bool TeamExt::NeutralOwns(AITriggerTypeClass* pThis, const std::vector& list) { - bool result = false; int counter = 0; - - for (auto const pHouse : HouseClass::Array) + for (auto const pItem : list) { - if (_stricmp(SideClass::Array.GetItem(pHouse->Type->SideIndex)->Name, "Civilian") != 0) - continue; + if (!pItem) continue; - // Count all objects of the list, like an OR operator - for (auto const pItem : list) + for (auto const pOtherHouse : HouseClass::Array) { - for (auto const pObject : TechnoClass::Array) + if (_stricmp(SideClass::Array.GetItem(pOtherHouse->Type->SideIndex)->Name, "Civilian") == 0) { - if (!TechnoExt::IsValidTechno(pObject)) continue; - - if (pObject->Owner == pHouse && pObject->GetTechnoType() == pItem) - counter++; + counter += pOtherHouse->CountOwnedAndPresent(pItem); } } } - - switch (pThis->Conditions->ComparatorOperand) - { - case 0: - result = counter < pThis->Conditions->ComparatorType; - break; - case 1: - result = counter <= pThis->Conditions->ComparatorType; - break; - case 2: - result = counter == pThis->Conditions->ComparatorType; - break; - case 3: - result = counter >= pThis->Conditions->ComparatorType; - break; - case 4: - result = counter > pThis->Conditions->ComparatorType; - break; - case 5: - result = counter != pThis->Conditions->ComparatorType; - break; - default: - break; - } - - return result; + return TeamExt::CountConditionMet(pThis, counter); } -bool TeamExt::HouseOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, std::vector list) +bool TeamExt::HouseOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, const std::vector& list) { - bool result = true; - - if (list.size() == 0) + if (list.empty() || !pHouse) return false; - // Count all objects of the list, like an AND operator for (auto const pItem : list) { - if (!result) - break; - - int counter = 0; - result = true; - - for (auto const pObject : TechnoClass::Array) - { - if (!TechnoExt::IsValidTechno(pObject)) continue; - - if (pObject->Owner == pHouse && pObject->GetTechnoType() == pItem) - counter++; - } + if (!pItem) continue; - switch (pThis->Conditions->ComparatorOperand) - { - case 0: - result = counter < pThis->Conditions->ComparatorType; - break; - case 1: - result = counter <= pThis->Conditions->ComparatorType; - break; - case 2: - result = counter == pThis->Conditions->ComparatorType; - break; - case 3: - result = counter >= pThis->Conditions->ComparatorType; - break; - case 4: - result = counter > pThis->Conditions->ComparatorType; - break; - case 5: - result = counter != pThis->Conditions->ComparatorType; - break; - default: - break; - } + int const counter = pHouse->CountOwnedAndPresent(pItem); + if (!TeamExt::CountConditionMet(pThis, counter)) + return false; } - return result; + return true; } -bool TeamExt::EnemyOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, std::vector list) +bool TeamExt::EnemyOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, const std::vector& list) { - bool result = true; + if (list.empty() || !pHouse) + return false; if (pEnemy && pHouse->IsAlliedWith(pEnemy)) pEnemy = nullptr; - if (list.size() == 0) - return false; - - // Count all objects of the list, like an AND operator for (auto const pItem : list) { - if (!result) - break; + if (!pItem) continue; int counter = 0; - result = true; - - for (auto const pObject : TechnoClass::Array) + if (pEnemy) { - if (!TechnoExt::IsValidTechno(pObject)) continue; - - if (pObject->Owner != pHouse - && (!pEnemy || (pEnemy && !pHouse->IsAlliedWith(pEnemy))) - && !pObject->Owner->Type->MultiplayPassive - && pObject->GetTechnoType() == pItem) + counter = pEnemy->CountOwnedAndPresent(pItem); + } + else + { + for (auto const pOtherHouse : HouseClass::Array) { - counter++; + if (pOtherHouse != pHouse && !pHouse->IsAlliedWith(pOtherHouse) && !pOtherHouse->Type->MultiplayPassive) + { + counter += pOtherHouse->CountOwnedAndPresent(pItem); + } } } - switch (pThis->Conditions->ComparatorOperand) - { - case 0: - result = counter < pThis->Conditions->ComparatorType; - break; - case 1: - result = counter <= pThis->Conditions->ComparatorType; - break; - case 2: - result = counter == pThis->Conditions->ComparatorType; - break; - case 3: - result = counter >= pThis->Conditions->ComparatorType; - break; - case 4: - result = counter > pThis->Conditions->ComparatorType; - break; - case 5: - result = counter != pThis->Conditions->ComparatorType; - break; - default: - break; - } + if (!TeamExt::CountConditionMet(pThis, counter)) + return false; } - return result; + return true; } -bool TeamExt::NeutralOwnsAll(AITriggerTypeClass* pThis, std::vector list) +bool TeamExt::NeutralOwnsAll(AITriggerTypeClass* pThis, const std::vector& list) { - bool result = true; - - if (list.size() == 0) + if (list.empty()) return false; - // Any neutral house should be capable to meet the prerequisites - for (auto const pHouse : HouseClass::Array) + for (auto const pOtherHouse : HouseClass::Array) { - if (!result) - break; - - bool foundAll = true; - - if (_stricmp(SideClass::Array.GetItem(pHouse->Type->SideIndex)->Name, "Civilian") != 0) + if (_stricmp(SideClass::Array.GetItem(pOtherHouse->Type->SideIndex)->Name, "Civilian") != 0) continue; - // Count all objects of the list, like an AND operator + bool allMet = true; for (auto const pItem : list) { - if (!foundAll) - break; + if (!pItem) continue; - int counter = 0; - - for (auto const pObject : TechnoClass::Array) + int const counter = pOtherHouse->CountOwnedAndPresent(pItem); + if (!TeamExt::CountConditionMet(pThis, counter)) { - if (!TechnoExt::IsValidTechno(pObject)) continue; - - if (pObject->Owner == pHouse && pObject->GetTechnoType() == pItem) - counter++; - } - - switch (pThis->Conditions->ComparatorOperand) - { - case 0: - foundAll = counter < pThis->Conditions->ComparatorType; - break; - case 1: - foundAll = counter <= pThis->Conditions->ComparatorType; - break; - case 2: - foundAll = counter == pThis->Conditions->ComparatorType; - break; - case 3: - foundAll = counter >= pThis->Conditions->ComparatorType; - break; - case 4: - foundAll = counter > pThis->Conditions->ComparatorType; - break; - case 5: - foundAll = counter != pThis->Conditions->ComparatorType; - break; - default: + allMet = false; break; } } - if (!foundAll) - result = false; + if (allMet) + return true; } - return result; + return false; } bool TeamExt::CountConditionMet(AITriggerTypeClass* pThis, int nObjects) { - bool result = true; - - if (nObjects < 0) + if (!pThis || nObjects < 0) return false; - switch (pThis->Conditions->ComparatorOperand) + switch (pThis->Conditions[0].ComparatorOperand) { case 0: - result = nObjects < pThis->Conditions->ComparatorType; - break; + return nObjects < pThis->Conditions[0].ComparatorType; case 1: - result = nObjects <= pThis->Conditions->ComparatorType; - break; + return nObjects <= pThis->Conditions[0].ComparatorType; case 2: - result = nObjects == pThis->Conditions->ComparatorType; - break; + return nObjects == pThis->Conditions[0].ComparatorType; case 3: - result = nObjects >= pThis->Conditions->ComparatorType; - break; + return nObjects >= pThis->Conditions[0].ComparatorType; case 4: - result = nObjects > pThis->Conditions->ComparatorType; - break; + return nObjects > pThis->Conditions[0].ComparatorType; case 5: - result = nObjects != pThis->Conditions->ComparatorType; - break; + return nObjects != pThis->Conditions[0].ComparatorType; default: - break; + return true; } - - return result; } diff --git a/src/Ext/Team/Body.h b/src/Ext/Team/Body.h index 7012cec63f..c580cde897 100644 --- a/src/Ext/Team/Body.h +++ b/src/Ext/Team/Body.h @@ -90,12 +90,15 @@ class TeamExt static ExtContainer ExtMap; - static bool HouseOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, bool allies, std::vector list); - static bool HouseOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, std::vector list); - static bool EnemyOwns(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, bool onlySelectedEnemy, std::vector list); - static bool EnemyOwnsAll(AITriggerTypeClass* pThis, HouseClass* pHouse, HouseClass* pEnemy, std::vector list); - static bool NeutralOwns(AITriggerTypeClass* pThis, std::vector list); - static bool NeutralOwnsAll(AITriggerTypeClass* pThis, std::vector list); + 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); }; From 84b6cf3759868c0e400973de3c06c6b7ead27c6b Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 29 Aug 2026 11:29:31 +0200 Subject: [PATCH 45/59] Refactor AI Team Selector weight discrete distribution and eliminate redundant candidate vectors --- src/Ext/Team/Body.NewTeamsSelector.cpp | 273 ++++++------------------- 1 file changed, 62 insertions(+), 211 deletions(-) diff --git a/src/Ext/Team/Body.NewTeamsSelector.cpp b/src/Ext/Team/Body.NewTeamsSelector.cpp index f672e5e887..402dd230df 100644 --- a/src/Ext/Team/Body.NewTeamsSelector.cpp +++ b/src/Ext/Team/Body.NewTeamsSelector.cpp @@ -10,53 +10,11 @@ enum class teamCategory : int Unclassified = 4 }; -struct TriggerElementWeight +struct CandidateTrigger { - double Weight = 0.0; AITriggerTypeClass* Trigger = nullptr; + double Weight = 0.0; teamCategory Category = teamCategory::None; - - //need to define a == operator so it can be used in array classes - bool operator==(const TriggerElementWeight& other) const - { - return (Trigger == other.Trigger && Weight == other.Weight && Category == other.Category); - } - - //unequality - bool operator!=(const TriggerElementWeight& other) const - { - return (Trigger != other.Trigger || Weight != other.Weight || Category == other.Category); - } - - bool operator<(const TriggerElementWeight& other) const - { - return (Weight < other.Weight); - } - - bool operator<(const double other) const - { - return (Weight < other); - } - - bool operator>(const TriggerElementWeight& other) const - { - return (Weight > other.Weight); - } - - bool operator>(const double other) const - { - return (Weight > other); - } - - bool operator==(const double other) const - { - return (Weight == other); - } - - bool operator!=(const double other) const - { - return (Weight != other); - } }; DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) @@ -88,16 +46,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) int totalActiveTeams = 0; int activeTeams = 0; - int totalGroundCategoryTriggers = 0; - int totalUnclassifiedCategoryTriggers = 0; - int totalNavalCategoryTriggers = 0; - int totalAirCategoryTriggers = 0; - - DynamicVectorClass validTriggerCandidates; - DynamicVectorClass validTriggerCandidatesGroundOnly; - DynamicVectorClass validTriggerCandidatesNavalOnly; - DynamicVectorClass validTriggerCandidatesAirOnly; - DynamicVectorClass validTriggerCandidatesUnclassifiedOnly; + std::vector validCandidates; int dice = ScenarioClass::Instance->Random.RandomRanged(1, 100); @@ -223,11 +172,6 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) int maxBaseDefenseTeams = RulesClass::Instance->MaximumAIDefensiveTeams.GetItem((int)houseDifficulty); int activeDefenseTeamsCount = 0; int maxTeamsLimit = RulesClass::Instance->TotalAITeamCap.GetItem((int)houseDifficulty); - double totalWeight = 0.0; - double totalWeightGroundOnly = 0.0; - double totalWeightNavalOnly = 0.0; - double totalWeightAirOnly = 0.0; - double totalWeightUnclassifiedOnly = 0.0; // Check if the running teams by the house already reached all the limits DynamicVectorClass activeTeamsList; @@ -724,195 +668,102 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) // They get stored in an elitist list and all previous triggers are discarded if (pTrigger->Weight_Current >= maxPriority && !onlyCheckImportantTriggers) { - // First time only - if (validTriggerCandidates.Count > 0) - { - validTriggerCandidates.Clear(); - validTriggerCandidatesGroundOnly.Clear(); - validTriggerCandidatesNavalOnly.Clear(); - validTriggerCandidatesAirOnly.Clear(); - validTriggerCandidatesUnclassifiedOnly.Clear(); - validCategory = teamCategory::None; - } - - // Reset the current ones and now only will be added important triggers to the list + validCandidates.clear(); onlyCheckImportantTriggers = true; - totalWeight = 0.0; - splitTriggersByCategory = false; // VIP teams breaks the categories logic (on purpose) + validCategory = teamCategory::None; + splitTriggersByCategory = false; // VIP teams break the categories logic (on purpose) } // Passed all checks, save this trigger for later. - totalWeight += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; - TriggerElementWeight item; - item.Trigger = pTrigger; - item.Weight = totalWeight; - item.Category = teamIsCategory; - - validTriggerCandidates.AddItem(item); - - if (splitTriggersByCategory) - { - TriggerElementWeight itemGroundOnly; - TriggerElementWeight itemAirOnly; - TriggerElementWeight itemNavalOnly; - TriggerElementWeight itemUnclassifiedOnly; - - switch (teamIsCategory) - { - case teamCategory::Ground: - totalWeightGroundOnly += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; - - itemGroundOnly.Trigger = pTrigger; - itemGroundOnly.Weight = totalWeightGroundOnly; - itemGroundOnly.Category = teamIsCategory; - - validTriggerCandidatesGroundOnly.AddItem(itemGroundOnly); - totalGroundCategoryTriggers++; - break; - - case teamCategory::Air: - totalWeightAirOnly += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; - - itemAirOnly.Trigger = pTrigger; - itemAirOnly.Weight = totalWeightAirOnly; - itemAirOnly.Category = teamIsCategory; - - validTriggerCandidatesAirOnly.AddItem(itemAirOnly); - totalAirCategoryTriggers++; - break; - - case teamCategory::Naval: - totalWeightNavalOnly += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; - - itemNavalOnly.Trigger = pTrigger; - itemNavalOnly.Weight = totalWeightNavalOnly; - itemNavalOnly.Category = teamIsCategory; - - validTriggerCandidatesNavalOnly.AddItem(itemNavalOnly); - totalNavalCategoryTriggers++; - break; - - case teamCategory::Unclassified: - totalWeightUnclassifiedOnly += pTrigger->Weight_Current < 1.0 ? 1.0 : pTrigger->Weight_Current; - - itemUnclassifiedOnly.Trigger = pTrigger; - itemUnclassifiedOnly.Weight = totalWeightUnclassifiedOnly; - itemUnclassifiedOnly.Category = teamIsCategory; - - validTriggerCandidatesUnclassifiedOnly.AddItem(itemUnclassifiedOnly); - totalUnclassifiedCategoryTriggers++; - break; - - default: - break; - } - } + double const weight = (pTrigger->Weight_Current < 1.0) ? 1.0 : pTrigger->Weight_Current; + validCandidates.push_back({ pTrigger, weight, teamIsCategory }); } } - if (validTriggerCandidates.Count == 0) + if (validCandidates.empty()) { - Debug::Log("AITeamsSelector - The house %d [%s](%s) have no valid triggers for now. A new attempt will be done later...\n", pHouse->ArrayIndex, pHouse->PlainName, pHouse->Type->ID); + 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; } - if ((validCategory == teamCategory::Ground && totalGroundCategoryTriggers == 0) - || (validCategory == teamCategory::Unclassified && totalUnclassifiedCategoryTriggers == 0) - || (validCategory == teamCategory::Air && totalAirCategoryTriggers == 0) - || (validCategory == teamCategory::Naval && totalNavalCategoryTriggers == 0)) + // Calculate category stats and candidate counts + double categoryTotalWeight = 0.0; + int categoryCandidatesCount = 0; + if (validCategory != teamCategory::None) { - Debug::Log("AITeamsSelector - The house %d [%s](%s) have no valid triggers of this category. A new attempt should be done later...\n", pHouse->ArrayIndex, pHouse->PlainName, pHouse->Type->ID); + for (const auto& candidate : validCandidates) + { + if (candidate.Category == validCategory) + { + categoryTotalWeight += candidate.Weight; + categoryCandidatesCount++; + } + } + } + + if (validCategory != teamCategory::None && categoryCandidatesCount == 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 will be checked all available triggers.\n"); + Debug::Log("... but FALLBACK MODE is enabled so now all available triggers will be checked.\n"); validCategory = teamCategory::None; } AITriggerTypeClass* selectedTrigger = nullptr; - double weightDice = 0.0; - bool found = false; - switch (validCategory) + // Roulette Wheel Selection (Discrete Distribution) + if (validCategory != teamCategory::None && categoryTotalWeight > 0.0) { - case teamCategory::None: - weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeight) * 1.0; - Debug::Log("AITeamsSelector - Picking 1 team from the %d available...\n", validTriggerCandidates.Count); - - for (auto element : validTriggerCandidates) - { - if (weightDice < element.Weight && !found) - { - selectedTrigger = element.Trigger; - found = true; - } - } - break; - - case teamCategory::Ground: - weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightGroundOnly) * 1.0; - Debug::Log("AITeamsSelector - Picking 1 team (of the category 'GROUND') from the %d available...\n", validTriggerCandidatesGroundOnly.Count); + double const weightDice = ScenarioClass::Instance->Random.RandomDouble() * categoryTotalWeight; + Debug::Log("AITeamsSelector - Picking 1 team (of category %d) from the %d available (Total Weight: %f, Roll: %f)...\n", + static_cast(validCategory), categoryCandidatesCount, categoryTotalWeight, weightDice); - for (auto element : validTriggerCandidatesGroundOnly) + double accumulated = 0.0; + for (const auto& candidate : validCandidates) { - if (weightDice < element.Weight && !found) + if (candidate.Category == validCategory) { - selectedTrigger = element.Trigger; - found = true; - } - } - break; - - case teamCategory::Unclassified: - weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightUnclassifiedOnly) * 1.0; - Debug::Log("AITeamsSelector - Picking 1 team (of the category 'UNCLASSIFIED') from the %d available...\n", validTriggerCandidatesUnclassifiedOnly.Count); - - for (auto element : validTriggerCandidatesUnclassifiedOnly) - { - if (weightDice < element.Weight && !found) - { - selectedTrigger = element.Trigger; - found = true; + accumulated += candidate.Weight; + if (weightDice < accumulated) + { + selectedTrigger = candidate.Trigger; + break; + } } } - break; - - case teamCategory::Naval: - weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightNavalOnly) * 1.0; - Debug::Log("AITeamsSelector - Picking 1 team (of the category 'NAVAL') from the %d available...\n", validTriggerCandidatesNavalOnly.Count); + } + else + { + double totalWeight = 0.0; + for (const auto& candidate : validCandidates) + totalWeight += candidate.Weight; - for (auto element : validTriggerCandidatesNavalOnly) + if (totalWeight > 0.0) { - if (weightDice < element.Weight && !found) - { - selectedTrigger = element.Trigger; - found = true; - } - } - break; - - case teamCategory::Air: - weightDice = ScenarioClass::Instance->Random.RandomRanged(0, (int)totalWeightAirOnly) * 1.0; - Debug::Log("AITeamsSelector - Picking 1 team (of the category 'AIR') from the %d available...\n", validTriggerCandidatesAirOnly.Count); + double const weightDice = ScenarioClass::Instance->Random.RandomDouble() * totalWeight; + Debug::Log("AITeamsSelector - Picking 1 team from the %d available (Total Weight: %f, Roll: %f)...\n", + validCandidates.size(), totalWeight, weightDice); - for (auto element : validTriggerCandidatesAirOnly) - { - if (weightDice < element.Weight && !found) + double accumulated = 0.0; + for (const auto& candidate : validCandidates) { - selectedTrigger = element.Trigger; - found = true; + accumulated += candidate.Weight; + if (weightDice < accumulated) + { + selectedTrigger = candidate.Trigger; + break; + } } } - break; - - default: - 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); + 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; } From 4e608a74bd515765ff05cea75c2f2463b910f7cb Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 29 Aug 2026 12:57:59 +0200 Subject: [PATCH 46/59] Add dedicated Infantry category to AI Team Selector and fix fallback non-matching category candidate evaluation --- src/Ext/HouseType/Body.cpp | 2 + src/Ext/HouseType/Body.h | 2 + src/Ext/Rules/Body.cpp | 2 + src/Ext/Rules/Body.h | 10 +++-- src/Ext/Team/Body.NewTeamsSelector.cpp | 53 +++++++++++++++++--------- 5 files changed, 47 insertions(+), 22 deletions(-) diff --git a/src/Ext/HouseType/Body.cpp b/src/Ext/HouseType/Body.cpp index 9f990dbc92..4e5bd6b44c 100644 --- a/src/Ext/HouseType/Body.cpp +++ b/src/Ext/HouseType/Body.cpp @@ -27,6 +27,7 @@ void HouseTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) 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"); } void HouseTypeExt::ExtData::CompleteInitialization() @@ -44,6 +45,7 @@ void HouseTypeExt::ExtData::Serialize(T& Stm) .Process(this->NewTeamsSelector_GroundCategoryPercentage) .Process(this->NewTeamsSelector_AirCategoryPercentage) .Process(this->NewTeamsSelector_NavalCategoryPercentage) + .Process(this->NewTeamsSelector_InfantryCategoryPercentage) ; } diff --git a/src/Ext/HouseType/Body.h b/src/Ext/HouseType/Body.h index 76ffee06e7..03dfd56877 100644 --- a/src/Ext/HouseType/Body.h +++ b/src/Ext/HouseType/Body.h @@ -19,6 +19,7 @@ class HouseTypeExt Nullable NewTeamsSelector_GroundCategoryPercentage; Nullable NewTeamsSelector_NavalCategoryPercentage; Nullable NewTeamsSelector_AirCategoryPercentage; + Nullable NewTeamsSelector_InfantryCategoryPercentage; ExtData(HouseTypeClass* OwnerObject) : Extension(OwnerObject) , NewTeamsSelector_MergeUnclassifiedCategoryWith { } @@ -26,6 +27,7 @@ class HouseTypeExt , NewTeamsSelector_GroundCategoryPercentage { } , NewTeamsSelector_NavalCategoryPercentage { } , NewTeamsSelector_AirCategoryPercentage { } + , NewTeamsSelector_InfantryCategoryPercentage { } { } virtual ~ExtData() = default; diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index 7c862df4c4..b6afc2caf7 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -385,6 +385,7 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) 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"); // Section Generic Prerequisites FillDefaultPrerequisites(); @@ -696,6 +697,7 @@ void RulesExt::ExtData::Serialize(T& Stm) .Process(this->NewTeamsSelector_GroundCategoryPercentage) .Process(this->NewTeamsSelector_AirCategoryPercentage) .Process(this->NewTeamsSelector_NavalCategoryPercentage) + .Process(this->NewTeamsSelector_InfantryCategoryPercentage) .Process(this->DropPodTrailer) .Process(this->DropPodDefaultTrailer) .Process(this->PodImage) diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h index bc14283bd2..59f4bd686c 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -274,6 +274,7 @@ class RulesExt Valueable NewTeamsSelector_GroundCategoryPercentage; Valueable NewTeamsSelector_NavalCategoryPercentage; Valueable NewTeamsSelector_AirCategoryPercentage; + Valueable NewTeamsSelector_InfantryCategoryPercentage; Valueable BerzerkTargeting; @@ -439,10 +440,11 @@ class RulesExt , NewTeamsSelector_SplitTriggersByCategory { true } , NewTeamsSelector_EnableFallback { false } , NewTeamsSelector_MergeUnclassifiedCategoryWith { -1 } - , NewTeamsSelector_UnclassifiedCategoryPercentage { 0.25 } - , NewTeamsSelector_GroundCategoryPercentage { 0.25 } - , NewTeamsSelector_NavalCategoryPercentage { 0.25 } - , NewTeamsSelector_AirCategoryPercentage { 0.25 } + , NewTeamsSelector_UnclassifiedCategoryPercentage { 0.20 } + , NewTeamsSelector_GroundCategoryPercentage { 0.20 } + , NewTeamsSelector_NavalCategoryPercentage { 0.20 } + , NewTeamsSelector_AirCategoryPercentage { 0.20 } + , NewTeamsSelector_InfantryCategoryPercentage { 0.20 } , DropPodTrailer { } , DropPodDefaultTrailer { } , PodImage { } diff --git a/src/Ext/Team/Body.NewTeamsSelector.cpp b/src/Ext/Team/Body.NewTeamsSelector.cpp index 402dd230df..bb6ff259de 100644 --- a/src/Ext/Team/Body.NewTeamsSelector.cpp +++ b/src/Ext/Team/Body.NewTeamsSelector.cpp @@ -4,10 +4,11 @@ enum class teamCategory : int { None = 0, // No category. Should be default value - Ground = 1, - Air = 2, - Naval = 3, - Unclassified = 4 + Ground = 1, // Ground vehicles (Tanks, etc.) + Air = 2, // Aircrafts & jumpjets + Naval = 3, // Naval units + Infantry = 4, // Infantry units + Unclassified = 5 // Mixed teams }; struct CandidateTrigger @@ -63,14 +64,16 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) 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 + 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) @@ -89,6 +92,10 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) percentageNavalTriggers += percentageUnclassifiedTriggers; break; + case (int)teamCategory::Infantry: + percentageInfantryTriggers += percentageUnclassifiedTriggers; + break; + default: break; } @@ -100,12 +107,12 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) percentageGroundTriggers = percentageGroundTriggers < 0.0 || percentageGroundTriggers > 1.0 ? 0.0 : percentageGroundTriggers; percentageNavalTriggers = percentageNavalTriggers < 0.0 || percentageNavalTriggers > 1.0 ? 0.0 : percentageNavalTriggers; percentageAirTriggers = percentageAirTriggers < 0.0 || percentageAirTriggers > 1.0 ? 0.0 : percentageAirTriggers; + percentageInfantryTriggers = percentageInfantryTriggers < 0.0 || percentageInfantryTriggers > 1.0 ? 0.0 : percentageInfantryTriggers; - double totalPercengates = percentageUnclassifiedTriggers + percentageGroundTriggers + percentageNavalTriggers + percentageAirTriggers; + double totalPercengates = percentageUnclassifiedTriggers + percentageGroundTriggers + percentageNavalTriggers + percentageAirTriggers + percentageInfantryTriggers; if (totalPercengates > 1.0 || totalPercengates <= 0.0) splitTriggersByCategory = false; - if (splitTriggersByCategory) { int categoryDice = ScenarioClass::Instance->Random.RandomRanged(1, 100); @@ -113,6 +120,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) int groundValue = (int)(percentageGroundTriggers * 100.0); int airValue = (int)(percentageAirTriggers * 100.0); int navalValue = (int)(percentageNavalTriggers * 100.0); + int infantryValue = (int)(percentageInfantryTriggers * 100.0); // Pick what type of team will be selected in this round if (percentageUnclassifiedTriggers > 0.0 && categoryDice <= unclassifiedValue) @@ -131,6 +139,10 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) { validCategory = teamCategory::Naval; } + else if (percentageInfantryTriggers > 0.0 && categoryDice <= (unclassifiedValue + groundValue + airValue + navalValue + infantryValue)) + { + validCategory = teamCategory::Infantry; + } else { // If the sum of all percentages is less than 100% then that empty space will work like "no categories" @@ -144,7 +156,11 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) switch (validCategory) { case teamCategory::Ground: - Debug::Log("AITeamsSelector - This time only GROUND teams will be picked.\n"); + 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: @@ -531,24 +547,25 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) else { auto pTechnoTypeExt = TechnoTypeExt::ExtMap.Find(entry.Type); - if (!pTechnoTypeExt) - continue; - if (pTechnoTypeExt->ConsideredNaval + if (pTechnoTypeExt && (pTechnoTypeExt->ConsideredNaval || (entry.Type->Naval && (entry.Type->MovementZone != MovementZone::Amphibious && entry.Type->MovementZone != MovementZone::AmphibiousDestroyer - && entry.Type->MovementZone != MovementZone::AmphibiousCrusher))) + && entry.Type->MovementZone != MovementZone::AmphibiousCrusher)))) { // This unit is from naval category entryIsCategory = teamCategory::Naval; } - - if (pTechnoTypeExt->ConsideredVehicle - || (entryIsCategory != teamCategory::Naval - && entryIsCategory != teamCategory::Air)) + 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 category + // This unit is from ground vehicles category entryIsCategory = teamCategory::Ground; } } @@ -572,7 +589,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) teamIsCategory = (teamCategory)mergeUnclassifiedCategoryWith; } - if (validCategory != teamIsCategory) + if (!isFallbackEnabled && validCategory != teamCategory::None && validCategory != teamIsCategory) continue; } From bbd623174860b272968bd31729713c01410f35ce Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 29 Aug 2026 14:55:00 +0200 Subject: [PATCH 47/59] Optimize AI Team Selector with O(1) active team type counting, robust proportional category rolling, and integer side comparison --- src/Ext/Team/Body.NewTeamsSelector.cpp | 98 ++++++++++---------------- 1 file changed, 39 insertions(+), 59 deletions(-) diff --git a/src/Ext/Team/Body.NewTeamsSelector.cpp b/src/Ext/Team/Body.NewTeamsSelector.cpp index bb6ff259de..b1e8f05d07 100644 --- a/src/Ext/Team/Body.NewTeamsSelector.cpp +++ b/src/Ext/Team/Body.NewTeamsSelector.cpp @@ -103,49 +103,47 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) percentageUnclassifiedTriggers = 0.0; } - percentageUnclassifiedTriggers = percentageUnclassifiedTriggers < 0.0 || percentageUnclassifiedTriggers > 1.0 ? 0.0 : percentageUnclassifiedTriggers; - percentageGroundTriggers = percentageGroundTriggers < 0.0 || percentageGroundTriggers > 1.0 ? 0.0 : percentageGroundTriggers; - percentageNavalTriggers = percentageNavalTriggers < 0.0 || percentageNavalTriggers > 1.0 ? 0.0 : percentageNavalTriggers; - percentageAirTriggers = percentageAirTriggers < 0.0 || percentageAirTriggers > 1.0 ? 0.0 : percentageAirTriggers; - percentageInfantryTriggers = percentageInfantryTriggers < 0.0 || percentageInfantryTriggers > 1.0 ? 0.0 : percentageInfantryTriggers; - - double totalPercengates = percentageUnclassifiedTriggers + percentageGroundTriggers + percentageNavalTriggers + percentageAirTriggers + percentageInfantryTriggers; - if (totalPercengates > 1.0 || totalPercengates <= 0.0) - splitTriggersByCategory = false; + 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 (splitTriggersByCategory) + if (totalPercentages <= 0.0) + { + splitTriggersByCategory = false; + } + else { - int categoryDice = ScenarioClass::Instance->Random.RandomRanged(1, 100); - int unclassifiedValue = (int)(percentageUnclassifiedTriggers * 100.0); - int groundValue = (int)(percentageGroundTriggers * 100.0); - int airValue = (int)(percentageAirTriggers * 100.0); - int navalValue = (int)(percentageNavalTriggers * 100.0); - int infantryValue = (int)(percentageInfantryTriggers * 100.0); + 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 && categoryDice <= unclassifiedValue) + if (percentageUnclassifiedTriggers > 0.0 && categoryRoll < (accumulated += percentageUnclassifiedTriggers)) { validCategory = teamCategory::Unclassified; } - else if (percentageGroundTriggers > 0.0 && categoryDice <= (unclassifiedValue + groundValue)) + else if (percentageGroundTriggers > 0.0 && categoryRoll < (accumulated += percentageGroundTriggers)) { validCategory = teamCategory::Ground; } - else if (percentageAirTriggers > 0.0 && categoryDice <= (unclassifiedValue + groundValue + airValue)) + else if (percentageAirTriggers > 0.0 && categoryRoll < (accumulated += percentageAirTriggers)) { validCategory = teamCategory::Air; } - else if (percentageNavalTriggers > 0.0 && categoryDice <= (unclassifiedValue + groundValue + airValue + navalValue)) + else if (percentageNavalTriggers > 0.0 && categoryRoll < (accumulated += percentageNavalTriggers)) { validCategory = teamCategory::Naval; } - else if (percentageInfantryTriggers > 0.0 && categoryDice <= (unclassifiedValue + groundValue + airValue + navalValue + infantryValue)) + else if (percentageInfantryTriggers > 0.0 && categoryRoll < (accumulated += percentageInfantryTriggers)) { validCategory = teamCategory::Infantry; } else { - // If the sum of all percentages is less than 100% then that empty space will work like "no categories" splitTriggersByCategory = false; } } @@ -181,33 +179,28 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } } - int houseIdx = pHouse->ArrayIndex; - 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 if the running teams by the house already reached all the limits - DynamicVectorClass activeTeamsList; + // Check running teams owned by this house with O(1) type lookup + std::unordered_map activeTeamCounts; for (auto const pRunningTeam : TeamClass::Array) { totalActiveTeams++; - int teamHouseIdx = pRunningTeam->Owner->ArrayIndex; - - if (teamHouseIdx != houseIdx) + if (pRunningTeam->Owner != pHouse) continue; - activeTeamsList.AddItem(pRunningTeam); + activeTeams++; + activeTeamCounts[pRunningTeam->Type]++; if (pRunningTeam->Type->IsBaseDefense) activeDefenseTeamsCount++; } - activeTeams = activeTeamsList.Count; - // We will use these values for discarding triggers int defensiveTeamsLimit = RulesClass::Instance->UseMinDefenseRule ? minBaseDefenseTeams : maxBaseDefenseTeams; bool hasReachedMaxTeamsLimit = activeTeams >= maxTeamsLimit; @@ -511,15 +504,10 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) continue; // If this type of Team reached the max then skip it - int count = 0; + auto const itTeam1 = activeTeamCounts.find(pTrigger->Team1); + int const currentTeam1Count = (itTeam1 != activeTeamCounts.end()) ? itTeam1->second : 0; - for (auto team : activeTeamsList) - { - if (team->Type == pTrigger->Team1) - count++; - } - - if (count >= pTrigger->Team1->Max) + if (currentTeam1Count >= pTrigger->Team1->Max) continue; teamCategory teamIsCategory = teamCategory::None; @@ -795,13 +783,8 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) 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 - int count1 = 0; - - for (auto team : activeTeamsList) - { - if (team->Type == selectedTrigger->Team1) - count1++; - } + auto const itTeam1 = activeTeamCounts.find(selectedTrigger->Team1); + int const count1 = (itTeam1 != activeTeamCounts.end()) ? itTeam1->second : 0; if (count1 < selectedTrigger->Team1->Max) { @@ -810,16 +793,10 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } // Team 2 creation (if set) - auto pTriggerTeam2Type = selectedTrigger->Team2; - if (pTriggerTeam2Type) + if (auto const pTriggerTeam2Type = selectedTrigger->Team2) { - int count2 = 0; - - for (auto team : activeTeamsList) - { - if (team->Type == pTriggerTeam2Type) - count2++; - } + auto const itTeam2 = activeTeamCounts.find(pTriggerTeam2Type); + int const count2 = (itTeam2 != activeTeamCounts.end()) ? itTeam2->second : 0; if (count2 < pTriggerTeam2Type->Max) { @@ -938,9 +915,10 @@ 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 (_stricmp(SideClass::Array.GetItem(pOtherHouse->Type->SideIndex)->Name, "Civilian") == 0) + if (pOtherHouse->Type->SideIndex == civilianSideIndex) { counter += pOtherHouse->CountOwnedAndPresent(pItem); } @@ -952,13 +930,14 @@ bool TeamExt::NeutralOwns(AITriggerTypeClass* pThis, TechnoTypeClass* pItem) 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 (_stricmp(SideClass::Array.GetItem(pOtherHouse->Type->SideIndex)->Name, "Civilian") == 0) + if (pOtherHouse->Type->SideIndex == civilianSideIndex) { counter += pOtherHouse->CountOwnedAndPresent(pItem); } @@ -1024,9 +1003,10 @@ bool TeamExt::NeutralOwnsAll(AITriggerTypeClass* pThis, const std::vectorType->SideIndex)->Name, "Civilian") != 0) + if (pOtherHouse->Type->SideIndex != civilianSideIndex) continue; bool allMet = true; From 33e4e9988db7af4c025dd2aa8e67a45814a1ae4b Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 29 Aug 2026 14:58:56 +0200 Subject: [PATCH 48/59] Clean up code comments in AI Team Selector --- src/Ext/Team/Body.NewTeamsSelector.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ext/Team/Body.NewTeamsSelector.cpp b/src/Ext/Team/Body.NewTeamsSelector.cpp index b1e8f05d07..5b10abd1da 100644 --- a/src/Ext/Team/Body.NewTeamsSelector.cpp +++ b/src/Ext/Team/Body.NewTeamsSelector.cpp @@ -185,7 +185,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) int activeDefenseTeamsCount = 0; int maxTeamsLimit = RulesClass::Instance->TotalAITeamCap.GetItem((int)houseDifficulty); - // Check running teams owned by this house with O(1) type lookup + // Check running teams owned by this house std::unordered_map activeTeamCounts; for (auto const pRunningTeam : TeamClass::Array) From 355b7a7bef8e7c1596dbe44e65d82fd776c5e7a7 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 29 Aug 2026 15:19:53 +0200 Subject: [PATCH 49/59] Filter dead and destroying objects early in AI Team Selector map scan loop --- src/Ext/Team/Body.NewTeamsSelector.cpp | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/Ext/Team/Body.NewTeamsSelector.cpp b/src/Ext/Team/Body.NewTeamsSelector.cpp index 5b10abd1da..ac67fe7cd1 100644 --- a/src/Ext/Team/Body.NewTeamsSelector.cpp +++ b/src/Ext/Team/Body.NewTeamsSelector.cpp @@ -236,7 +236,8 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) for (auto const pTechno : TechnoClass::Array) { - if (!TechnoExt::IsValidTechno(pTechno)) continue; + if (!TechnoExt::IsValidTechno(pTechno) || !pTechno->IsAlive || pTechno->Health <= 0) + continue; if (pTechno->WhatAmI() == AbstractType::Building) { @@ -268,17 +269,14 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) break; } } - else + else if (pBuildingType->BridgeRepairHut) { - if (pBuildingType->BridgeRepairHut) - { - CellStruct cell = pTechno->GetCell()->MapCoords; + CellStruct cell = pTechno->GetCell()->MapCoords; - if (MapClass::Instance.IsLinkedBridgeDestroyed(cell)) - destroyedBridgesCount++; - else - undamagedBridgesCount++; - } + if (MapClass::Instance.IsLinkedBridgeDestroyed(cell)) + destroyedBridgesCount++; + else + undamagedBridgesCount++; } continue; @@ -287,8 +285,6 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) auto const pFoot = static_cast(pTechno); if (!pFoot - || !pTechno->IsAlive - || pTechno->Health <= 0 || !pTechno->IsOnMap // Note: underground movement is considered "IsOnMap == false" || pTechno->Transporter || pTechno->Absorbed From ea59bf081c4caf1b7de7b85184ffcd6610847e37 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 29 Aug 2026 15:28:57 +0200 Subject: [PATCH 50/59] Refactor AI trigger candidate loop with early exits, switch jump table, and optimized VIP checks --- src/Ext/Team/Body.NewTeamsSelector.cpp | 538 ++++++++++++------------- 1 file changed, 254 insertions(+), 284 deletions(-) diff --git a/src/Ext/Team/Body.NewTeamsSelector.cpp b/src/Ext/Team/Body.NewTeamsSelector.cpp index ac67fe7cd1..ed14a35e93 100644 --- a/src/Ext/Team/Body.NewTeamsSelector.cpp +++ b/src/Ext/Team/Body.NewTeamsSelector.cpp @@ -316,369 +316,339 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (!pTrigger || !pTrigger->Team1 || !pTrigger->IsEnabled) continue; - // Ignore offensive teams if the next trigger must be defensive - if ((onlyPickDefensiveTeams && !pTrigger->IsForBaseDefense) || (hasReachedMaxDefensiveTeamsLimit && pTrigger->IsForBaseDefense)) + // If VIP triggers were found, discard any non-VIP trigger immediately + if (onlyCheckImportantTriggers && pTrigger->Weight_Current < maxPriority) continue; if (pTrigger->Team1->TechLevel > pHouse->TechLevel) continue; - // ignore it if isn't set for the house AI difficulty - if ((int)houseDifficulty == 0 && !pTrigger->Enabled_Hard - || (int)houseDifficulty == 1 && !pTrigger->Enabled_Normal - || (int)houseDifficulty == 2 && !pTrigger->Enabled_Easy) + // Ignore it if isn't set for the house AI difficulty + if (((int)houseDifficulty == 0 && !pTrigger->Enabled_Hard) + || ((int)houseDifficulty == 1 && !pTrigger->Enabled_Normal) + || ((int)houseDifficulty == 2 && !pTrigger->Enabled_Easy)) { continue; } - // The trigger must be compatible with the owner + // 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) { - // "ConditionType=-1" will be skipped, always is valid - if ((int)pTrigger->ConditionType >= 0) + // TaskForces are limited to 6 entries + for (int i = 0; i < 6; i++) { - if ((int)pTrigger->ConditionType == 0) - { - // Simulate case 0: "enemy owns" - if (!pTrigger->ConditionObject || !TeamExt::EnemyOwns(pTrigger, pHouse, targetHouse, true, pTrigger->ConditionObject)) - continue; - } - else if ((int)pTrigger->ConditionType == 1) - { - // Simulate case 1: "house owns" - if (!pTrigger->ConditionObject || !TeamExt::HouseOwns(pTrigger, pHouse, false, pTrigger->ConditionObject)) - continue; - } - else if ((int)pTrigger->ConditionType == 4) - { - // Simulate case 4: "Enemy house economy threshold?" - if (!pTrigger->HouseCredits(nullptr, targetHouse)) - continue; - } - else if ((int)pTrigger->ConditionType == 5) - { - // Simulate case 5: "Iron Curtain is charged?" - if (!pTrigger->IronCurtainCharged(pHouse, nullptr)) - continue; - } - else if ((int)pTrigger->ConditionType == 6) - { - // Simulate case 6: "Chronosphere is charged?" - if (!pTrigger->ChronoSphereCharged(pHouse, nullptr)) - continue; - } - else if ((int)pTrigger->ConditionType == 7) - { - // Simulate case 7: "civilian owns" - if (!pTrigger->ConditionObject || !TeamExt::NeutralOwns(pTrigger, pTrigger->ConditionObject)) - continue; - } - else if ((int)pTrigger->ConditionType == 8) + const auto& entry = pTrigger->Team1->TaskForce->Entries[i]; + teamCategory entryIsCategory = teamCategory::Ground; + + if (entry.Amount > 0) { - // Simulate case 8: "enemy owns" across all enemies - if (!pTrigger->ConditionObject || !TeamExt::EnemyOwns(pTrigger, pHouse, nullptr, false, pTrigger->ConditionObject)) + if (!entry.Type) continue; - } - else if ((int)pTrigger->ConditionType == 9) - { - auto const listIdx = pTrigger->Conditions[3].ComparatorOperand; - if (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) + + if (entry.Type->WhatAmI() == AbstractType::AircraftType + || entry.Type->ConsideredAircraft) { - const auto& list = RulesExt::Global()->AITargetTypesLists[listIdx]; - if (!TeamExt::EnemyOwns(pTrigger, pHouse, targetHouse, false, list)) - continue; + // This unit is from air category + entryIsCategory = teamCategory::Air; } - } - else if ((int)pTrigger->ConditionType == 10) - { - auto const listIdx = pTrigger->Conditions[3].ComparatorOperand; - if (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) + else { - const auto& list = RulesExt::Global()->AITargetTypesLists[listIdx]; - if (!TeamExt::HouseOwns(pTrigger, pHouse, false, list)) - continue; + auto pTechnoTypeExt = TechnoTypeExt::ExtMap.Find(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 if ((int)pTrigger->ConditionType == 11) + else { - auto const listIdx = pTrigger->Conditions[3].ComparatorOperand; - if (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) - { - const auto& list = RulesExt::Global()->AITargetTypesLists[listIdx]; - if (!TeamExt::NeutralOwns(pTrigger, list)) - continue; - } + break; } - else if ((int)pTrigger->ConditionType == 12) + } + + // 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; + } + + // The trigger must be compatible with the owner ("ConditionType=-1" is always valid) + if ((int)pTrigger->ConditionType >= 0) + { + bool conditionMet = true; + + switch ((int)pTrigger->ConditionType) + { + case 0: + // Case 0: "enemy owns" + conditionMet = pTrigger->ConditionObject && TeamExt::EnemyOwns(pTrigger, pHouse, targetHouse, true, pTrigger->ConditionObject); + break; + + case 1: + // Case 1: "house owns" + conditionMet = pTrigger->ConditionObject && TeamExt::HouseOwns(pTrigger, pHouse, false, pTrigger->ConditionObject); + break; + + case 4: + // Case 4: "Enemy house economy threshold?" + conditionMet = pTrigger->HouseCredits(nullptr, targetHouse); + break; + + case 5: + // Case 5: "Iron Curtain is charged?" + conditionMet = pTrigger->IronCurtainCharged(pHouse, nullptr); + break; + + case 6: + // Case 6: "Chronosphere is charged?" + conditionMet = pTrigger->ChronoSphereCharged(pHouse, nullptr); + break; + + case 7: + // Case 7: "civilian owns" + conditionMet = pTrigger->ConditionObject && TeamExt::NeutralOwns(pTrigger, pTrigger->ConditionObject); + break; + + case 8: + // Case 8: "enemy owns" across all enemies + conditionMet = pTrigger->ConditionObject && TeamExt::EnemyOwns(pTrigger, pHouse, nullptr, false, pTrigger->ConditionObject); + break; + + case 9: { auto const listIdx = pTrigger->Conditions[3].ComparatorOperand; - if (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) - { - const auto& list = RulesExt::Global()->AITargetTypesLists[listIdx]; - if (!TeamExt::EnemyOwns(pTrigger, pHouse, nullptr, false, list)) - continue; - } + conditionMet = (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) + && TeamExt::EnemyOwns(pTrigger, pHouse, targetHouse, false, RulesExt::Global()->AITargetTypesLists[listIdx]); } - else if ((int)pTrigger->ConditionType == 13) + break; + + case 10: { auto const listIdx = pTrigger->Conditions[3].ComparatorOperand; - if (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) - { - const auto& list = RulesExt::Global()->AITargetTypesLists[listIdx]; - if (!TeamExt::HouseOwns(pTrigger, pHouse, true, list)) - continue; - } + conditionMet = (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) + && TeamExt::HouseOwns(pTrigger, pHouse, false, RulesExt::Global()->AITargetTypesLists[listIdx]); } - else if ((int)pTrigger->ConditionType == 14) + break; + + case 11: { auto const listIdx = pTrigger->Conditions[3].ComparatorOperand; - if (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) - { - const auto& list = RulesExt::Global()->AITargetTypesLists[listIdx]; - if (!TeamExt::EnemyOwnsAll(pTrigger, pHouse, targetHouse, list)) - continue; - } + conditionMet = (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) + && TeamExt::NeutralOwns(pTrigger, RulesExt::Global()->AITargetTypesLists[listIdx]); } - else if ((int)pTrigger->ConditionType == 15) + break; + + case 12: { auto const listIdx = pTrigger->Conditions[3].ComparatorOperand; - if (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) - { - const auto& list = RulesExt::Global()->AITargetTypesLists[listIdx]; - if (!TeamExt::HouseOwnsAll(pTrigger, pHouse, list)) - continue; - } + conditionMet = (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) + && TeamExt::EnemyOwns(pTrigger, pHouse, nullptr, false, RulesExt::Global()->AITargetTypesLists[listIdx]); } - else if ((int)pTrigger->ConditionType == 16) + break; + + case 13: { auto const listIdx = pTrigger->Conditions[3].ComparatorOperand; - if (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) - { - const auto& list = RulesExt::Global()->AITargetTypesLists[listIdx]; - if (!TeamExt::NeutralOwnsAll(pTrigger, list)) - continue; - } + conditionMet = (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) + && TeamExt::HouseOwns(pTrigger, pHouse, true, RulesExt::Global()->AITargetTypesLists[listIdx]); } - else if ((int)pTrigger->ConditionType == 17) + break; + + case 14: { auto const listIdx = pTrigger->Conditions[3].ComparatorOperand; - if (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) - { - const auto& list = RulesExt::Global()->AITargetTypesLists[listIdx]; - if (!TeamExt::EnemyOwnsAll(pTrigger, pHouse, nullptr, list)) - continue; - } + conditionMet = (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) + && TeamExt::EnemyOwnsAll(pTrigger, pHouse, targetHouse, RulesExt::Global()->AITargetTypesLists[listIdx]); } - else if ((int)pTrigger->ConditionType == 18) + break; + + case 15: { - // New case 18: Check destroyed bridges - if (!TeamExt::CountConditionMet(pTrigger, destroyedBridgesCount)) - continue; + auto const listIdx = pTrigger->Conditions[3].ComparatorOperand; + conditionMet = (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) + && TeamExt::HouseOwnsAll(pTrigger, pHouse, RulesExt::Global()->AITargetTypesLists[listIdx]); } - else if ((int)pTrigger->ConditionType == 19) + break; + + case 16: { - // New case 19: Check undamaged bridges - if (!TeamExt::CountConditionMet(pTrigger, undamagedBridgesCount)) - continue; + auto const listIdx = pTrigger->Conditions[3].ComparatorOperand; + conditionMet = (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) + && TeamExt::NeutralOwnsAll(pTrigger, RulesExt::Global()->AITargetTypesLists[listIdx]); } - else + break; + + case 17: { - // Other cases from vanilla game - if (!pTrigger->ConditionMet(pHouse, targetHouse, hasReachedMaxDefensiveTeamsLimit)) - continue; + auto const listIdx = pTrigger->Conditions[3].ComparatorOperand; + conditionMet = (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) + && TeamExt::EnemyOwnsAll(pTrigger, pHouse, nullptr, RulesExt::Global()->AITargetTypesLists[listIdx]); } - } + break; - // All triggers below maxPriority (usually 5000) in current weight will get discarded if this mode is enabled - if (onlyCheckImportantTriggers) - { - if (pTrigger->Weight_Current < maxPriority) - continue; - } + case 18: + // Case 18: Check destroyed bridges + conditionMet = TeamExt::CountConditionMet(pTrigger, destroyedBridgesCount); + break; - // No more defensive teams needed - if (pTrigger->Team1->IsBaseDefense && hasReachedMaxDefensiveTeamsLimit) - continue; + case 19: + // Case 19: Check undamaged bridges + conditionMet = TeamExt::CountConditionMet(pTrigger, undamagedBridgesCount); + break; - // If this type of Team reached the max then skip it - auto const itTeam1 = activeTeamCounts.find(pTrigger->Team1); - int const currentTeam1Count = (itTeam1 != activeTeamCounts.end()) ? itTeam1->second : 0; + default: + // Other cases from vanilla game + conditionMet = pTrigger->ConditionMet(pHouse, targetHouse, hasReachedMaxDefensiveTeamsLimit); + break; + } - if (currentTeam1Count >= pTrigger->Team1->Max) + if (!conditionMet) continue; + } - teamCategory teamIsCategory = teamCategory::None; + bool allObjectsCanBeBuiltOrRecruited = true; - // Analyze what kind of category is this main team if the feature is enabled - if (splitTriggersByCategory) + if (pTrigger->Team1->Autocreate && canAutocreate) + { + for (const auto& entry : pTrigger->Team1->TaskForce->Entries) { - // TaskForces are limited to 6 entries - for (int i = 0; i < 6; i++) - { - auto entry = pTrigger->Team1->TaskForce->Entries[i]; - teamCategory entryIsCategory = teamCategory::Ground; + if (!entry.Type) + continue; - if (entry.Amount > 0) - { - if (!entry.Type) - continue; + if (entry.Amount == 0) // As soon as there are empty slots the check finish + break; - if (entry.Type->WhatAmI() == AbstractType::AircraftType - || entry.Type->ConsideredAircraft) - { - // This unit is from air category - entryIsCategory = teamCategory::Air; - } - else - { - auto pTechnoTypeExt = TechnoTypeExt::ExtMap.Find(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; - } - } + bool canBeBuilt = true; - // if a team have multiple categories it will be a mixed category - teamIsCategory = teamIsCategory == teamCategory::None || teamIsCategory == entryIsCategory ? entryIsCategory : teamCategory::Unclassified; + // Exists the required factory for producing the checked unit? + switch (entry.Type->WhatAmI()) + { + case AbstractType::InfantryType: + if (!hasInfantryFactory) + canBeBuilt = false; + break; - if (teamIsCategory == teamCategory::Unclassified) - break; + case AbstractType::AircraftType: + if (!hasAircraftFactory) + canBeBuilt = false; + break; + + case AbstractType::UnitType: + if (entry.Type->Naval) + { + if (!hasNavalFactory) + canBeBuilt = false; } else { - break; + if (!hasUnitFactory) + canBeBuilt = false; } - } + break; - // If this value is set and the team is MIXED, override category type - if (teamIsCategory == teamCategory::Unclassified - && mergeUnclassifiedCategoryWith >= 0) - { - teamIsCategory = (teamCategory)mergeUnclassifiedCategoryWith; + default: + break; } - if (!isFallbackEnabled && validCategory != teamCategory::None && validCategory != teamIsCategory) - continue; - } - - bool allObjectsCanBeBuiltOrRecruited = true; + // Meets the production prerequisites? + if (canBeBuilt) + canBeBuilt = HouseExt::PrerequisitesMet(pHouse, entry.Type, false); - if (pTrigger->Team1->Autocreate && canAutocreate) - { - for (auto entry : pTrigger->Team1->TaskForce->Entries) + if (!canBeBuilt) { - if (!entry.Type) - continue; - - if (entry.Amount == 0) // As soon as there are empty slots the check finish - break; - - bool canBeBuilt = true; - - // Exists the required factory for producing the checked unit? - switch (entry.Type->WhatAmI()) - { - case AbstractType::InfantryType: - if (!hasInfantryFactory) - canBeBuilt = false; - break; - - case AbstractType::AircraftType: - if (!hasAircraftFactory) - canBeBuilt = false; - break; - - case AbstractType::UnitType: - if (entry.Type->Naval) - { - if (!hasNavalFactory) - canBeBuilt = false; - } - else - { - if (!hasUnitFactory) - canBeBuilt = false; - } - break; - - default: - break; - } - - // Meets the production prerequisites? - if (canBeBuilt) - canBeBuilt = HouseExt::PrerequisitesMet(pHouse, entry.Type, false); - - if (!canBeBuilt) - { - allObjectsCanBeBuiltOrRecruited = false; - break; - } + allObjectsCanBeBuiltOrRecruited = false; + break; } } - else - { - allObjectsCanBeBuiltOrRecruited = false; - } + } + else + { + allObjectsCanBeBuiltOrRecruited = false; + } + + if (!allObjectsCanBeBuiltOrRecruited && pTrigger->Team1->Recruiter) + { + allObjectsCanBeBuiltOrRecruited = true; - if (!allObjectsCanBeBuiltOrRecruited && pTrigger->Team1->Recruiter) + for (const auto& entry : pTrigger->Team1->TaskForce->Entries) { - allObjectsCanBeBuiltOrRecruited = true; + if (!entry.Type) + continue; - for (auto entry : pTrigger->Team1->TaskForce->Entries) + // Check if each unit in the taskforce has the available recruitable units in the map + if (allObjectsCanBeBuiltOrRecruited && entry.Amount > 0) { - if (!entry.Type) - continue; + auto const it = ownedRecruitables.find(entry.Type); + int const recruits = (it != ownedRecruitables.end()) ? it->second : 0; - // Check if each unit in the taskforce has the available recruitable units in the map - if (allObjectsCanBeBuiltOrRecruited && entry.Amount > 0) + if (recruits < entry.Amount) { - auto const it = ownedRecruitables.find(entry.Type); - int const recruits = (it != ownedRecruitables.end()) ? it->second : 0; - - if (recruits < entry.Amount) - { - allObjectsCanBeBuiltOrRecruited = false; - break; - } + allObjectsCanBeBuiltOrRecruited = false; + break; } } } + } - // We can't let AI cheat in this trigger because doesn't have the required tech tree available - if (!allObjectsCanBeBuiltOrRecruited) - continue; - - // Special case: triggers become very important if they reach the max priority (usually 5000, see maxPriority). - // They get stored in an elitist list and all previous triggers are discarded - if (pTrigger->Weight_Current >= maxPriority && !onlyCheckImportantTriggers) - { - validCandidates.clear(); - onlyCheckImportantTriggers = true; - validCategory = teamCategory::None; - splitTriggersByCategory = false; // VIP teams break the categories logic (on purpose) - } + // We can't let AI cheat in this trigger because doesn't have the required tech tree available + if (!allObjectsCanBeBuiltOrRecruited) + continue; - // 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 }); + // Special case: triggers become very important if they reach the max priority (usually 5000, see maxPriority). + // They get stored in an elitist list and all previous triggers are discarded + if (pTrigger->Weight_Current >= maxPriority && !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()) From e97a7d669a994a99c6941b582a9d653fb2fd1ad1 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 29 Aug 2026 15:42:49 +0200 Subject: [PATCH 51/59] Simplify Autocreate and Recruiter validation logic with early exits --- src/Ext/Team/Body.NewTeamsSelector.cpp | 76 ++++++++++---------------- 1 file changed, 28 insertions(+), 48 deletions(-) diff --git a/src/Ext/Team/Body.NewTeamsSelector.cpp b/src/Ext/Team/Body.NewTeamsSelector.cpp index ed14a35e93..d7f44f6774 100644 --- a/src/Ext/Team/Body.NewTeamsSelector.cpp +++ b/src/Ext/Team/Body.NewTeamsSelector.cpp @@ -548,92 +548,72 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) continue; } - bool allObjectsCanBeBuiltOrRecruited = true; + 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) - continue; - - if (entry.Amount == 0) // As soon as there are empty slots the check finish + if (!entry.Type || entry.Amount == 0) break; - bool canBeBuilt = true; - - // Exists the required factory for producing the checked unit? + bool hasFactory = false; switch (entry.Type->WhatAmI()) { case AbstractType::InfantryType: - if (!hasInfantryFactory) - canBeBuilt = false; + hasFactory = hasInfantryFactory; break; case AbstractType::AircraftType: - if (!hasAircraftFactory) - canBeBuilt = false; + hasFactory = hasAircraftFactory; break; case AbstractType::UnitType: - if (entry.Type->Naval) - { - if (!hasNavalFactory) - canBeBuilt = false; - } - else - { - if (!hasUnitFactory) - canBeBuilt = false; - } + hasFactory = entry.Type->Naval ? hasNavalFactory : hasUnitFactory; break; default: break; } - // Meets the production prerequisites? - if (canBeBuilt) - canBeBuilt = HouseExt::PrerequisitesMet(pHouse, entry.Type, false); - - if (!canBeBuilt) + if (!hasFactory || !HouseExt::PrerequisitesMet(pHouse, entry.Type, false)) { - allObjectsCanBeBuiltOrRecruited = false; + canProduce = false; break; } } } - else - { - allObjectsCanBeBuiltOrRecruited = false; - } - if (!allObjectsCanBeBuiltOrRecruited && pTrigger->Team1->Recruiter) + // Check if the team can be recruited from existing map units + if (!canProduce && pTrigger->Team1->Recruiter) { - allObjectsCanBeBuiltOrRecruited = true; + bool canRecruit = true; for (const auto& entry : pTrigger->Team1->TaskForce->Entries) { - if (!entry.Type) - continue; + if (!entry.Type || entry.Amount == 0) + break; - // Check if each unit in the taskforce has the available recruitable units in the map - if (allObjectsCanBeBuiltOrRecruited && entry.Amount > 0) - { - auto const it = ownedRecruitables.find(entry.Type); - int const recruits = (it != ownedRecruitables.end()) ? it->second : 0; + auto const it = ownedRecruitables.find(entry.Type); + int const availableRecruits = (it != ownedRecruitables.end()) ? it->second : 0; - if (recruits < entry.Amount) - { - allObjectsCanBeBuiltOrRecruited = false; - break; - } + if (availableRecruits < entry.Amount) + { + canRecruit = false; + break; } } + + canProduce = canRecruit; } - // We can't let AI cheat in this trigger because doesn't have the required tech tree available - if (!allObjectsCanBeBuiltOrRecruited) + if (!canProduce) continue; // Special case: triggers become very important if they reach the max priority (usually 5000, see maxPriority). From 501dac87418a8dc183224938ab39e3b5364cd464 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 29 Aug 2026 15:47:15 +0200 Subject: [PATCH 52/59] Unify roulette wheel selection to eliminate duplicated selection loops --- src/Ext/Team/Body.NewTeamsSelector.cpp | 62 +++++++++++--------------- 1 file changed, 25 insertions(+), 37 deletions(-) diff --git a/src/Ext/Team/Body.NewTeamsSelector.cpp b/src/Ext/Team/Body.NewTeamsSelector.cpp index d7f44f6774..9f21979cd5 100644 --- a/src/Ext/Team/Body.NewTeamsSelector.cpp +++ b/src/Ext/Team/Body.NewTeamsSelector.cpp @@ -638,21 +638,19 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) } // Calculate category stats and candidate counts - double categoryTotalWeight = 0.0; - int categoryCandidatesCount = 0; - if (validCategory != teamCategory::None) + double totalPoolWeight = 0.0; + int poolCandidatesCount = 0; + + for (const auto& candidate : validCandidates) { - for (const auto& candidate : validCandidates) + if (validCategory == teamCategory::None || candidate.Category == validCategory) { - if (candidate.Category == validCategory) - { - categoryTotalWeight += candidate.Weight; - categoryCandidatesCount++; - } + totalPoolWeight += candidate.Weight; + poolCandidatesCount++; } } - if (validCategory != teamCategory::None && categoryCandidatesCount == 0) + 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)); @@ -662,45 +660,35 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) 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 (validCategory != teamCategory::None && categoryTotalWeight > 0.0) + if (totalPoolWeight > 0.0) { - double const weightDice = ScenarioClass::Instance->Random.RandomDouble() * categoryTotalWeight; - Debug::Log("AITeamsSelector - Picking 1 team (of category %d) from the %d available (Total Weight: %f, Roll: %f)...\n", - static_cast(validCategory), categoryCandidatesCount, categoryTotalWeight, weightDice); + double const weightDice = ScenarioClass::Instance->Random.RandomDouble() * totalPoolWeight; - double accumulated = 0.0; - for (const auto& candidate : validCandidates) + if (validCategory != teamCategory::None) { - if (candidate.Category == validCategory) - { - accumulated += candidate.Weight; - if (weightDice < accumulated) - { - selectedTrigger = candidate.Trigger; - break; - } - } + 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 - { - double totalWeight = 0.0; - for (const auto& candidate : validCandidates) - totalWeight += candidate.Weight; - - if (totalWeight > 0.0) + else { - double const weightDice = ScenarioClass::Instance->Random.RandomDouble() * totalWeight; Debug::Log("AITeamsSelector - Picking 1 team from the %d available (Total Weight: %f, Roll: %f)...\n", - validCandidates.size(), totalWeight, weightDice); + poolCandidatesCount, totalPoolWeight, weightDice); + } - double accumulated = 0.0; - for (const auto& candidate : validCandidates) + double accumulated = 0.0; + for (const auto& candidate : validCandidates) + { + if (validCategory == teamCategory::None || candidate.Category == validCategory) { accumulated += candidate.Weight; if (weightDice < accumulated) From c056c794313069d4107cbeaa8505a6caf0798c92 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 29 Aug 2026 15:58:12 +0200 Subject: [PATCH 53/59] Optimize trigger preload with difficulty filtering and single-pass trigger scan --- src/Ext/Scenario/Hooks.NewTeamsSelector.cpp | 72 +++++++++++++++------ src/Ext/Team/Body.NewTeamsSelector.cpp | 8 --- 2 files changed, 52 insertions(+), 28 deletions(-) diff --git a/src/Ext/Scenario/Hooks.NewTeamsSelector.cpp b/src/Ext/Scenario/Hooks.NewTeamsSelector.cpp index 017df65820..dad887e431 100644 --- a/src/Ext/Scenario/Hooks.NewTeamsSelector.cpp +++ b/src/Ext/Scenario/Hooks.NewTeamsSelector.cpp @@ -10,40 +10,72 @@ DEFINE_HOOK(0x687C9B, ReadScenarioINI_AITeamSelector_PreloadValidTriggers, 0x7) bool ignoreGlobalAITriggers = ScenarioClass::Instance->IgnoreGlobalAITriggers; - // For each house a list will be saved with Triggers that can be used by the AI + struct HousePreloadInfo + { + HouseExt::ExtData* 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::ExtMap.Find(pHouse); + if (!pHouseExt) + continue; + pHouseExt->AITriggers_ValidList.clear(); - int parentCountryTypeIdx = pHouse->Type->FindParentCountryIndex(); // ParentCountry can change the House in a SP map - int houseTypeIdx = parentCountryTypeIdx >= 0 ? parentCountryTypeIdx : pHouse->Type->ArrayIndex; // Indexes in AITriggers section are 1-based + 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; // Side indexes in AITriggers section are 1-based + int sideTypeIdx = parentCountrySideTypeIdx >= 0 ? parentCountrySideTypeIdx + 1 : pHouse->Type->SideIndex + 1; - for (int i = 0; i < AITriggerTypeClass::Array.Count; i++) - { - auto pTrigger = AITriggerTypeClass::Array.GetItem(i); + houses.push_back({ pHouseExt, houseTypeIdx, sideTypeIdx, pHouse->AIDifficulty }); + } - if (!pTrigger || (ignoreGlobalAITriggers && pTrigger->IsGlobal && !pTrigger->IsEnabled) || !pTrigger->Team1) - continue; + // Single pass over all triggers + for (int i = 0; i < AITriggerTypeClass::Array.Count; i++) + { + auto pTrigger = AITriggerTypeClass::Array.GetItem(i); - int triggerHouse = pTrigger->HouseIndex; - int triggerSide = pTrigger->SideIndex; + if (!pTrigger || (ignoreGlobalAITriggers && pTrigger->IsGlobal && !pTrigger->IsEnabled) || !pTrigger->Team1) + continue; - // The trigger must be compatible with the owner - //if ((triggerHouse == -1 || houseIdx == triggerHouse) && (triggerSide == 0 || sideIdx == triggerSide)) - if ((triggerHouse == -1 || houseTypeIdx == triggerHouse) && (triggerSide == 0 || sideTypeIdx == triggerSide)) - pHouseExt->AITriggers_ValidList.push_back(i); - } + int triggerHouse = pTrigger->HouseIndex; + int triggerSide = pTrigger->SideIndex; - 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()); + 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 (int i : pHouseExt->AITriggers_ValidList) + for (HouseClass* pHouse : HouseClass::Array) + { + auto pHouseExt = HouseExt::ExtMap.Find(pHouse); + if (pHouseExt) { - Debug::Log(" => %s => %s\n", AITriggerTypeClass::Array.GetItem(i)->ID, AITriggerTypeClass::Array.GetItem(i)->Name); - }*/ + 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 index 9f21979cd5..60fedb715a 100644 --- a/src/Ext/Team/Body.NewTeamsSelector.cpp +++ b/src/Ext/Team/Body.NewTeamsSelector.cpp @@ -323,14 +323,6 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (pTrigger->Team1->TechLevel > pHouse->TechLevel) continue; - // Ignore it if isn't set for the house AI difficulty - if (((int)houseDifficulty == 0 && !pTrigger->Enabled_Hard) - || ((int)houseDifficulty == 1 && !pTrigger->Enabled_Normal) - || ((int)houseDifficulty == 2 && !pTrigger->Enabled_Easy)) - { - continue; - } - // Ignore offensive teams if the next trigger must be defensive if ((onlyPickDefensiveTeams && !pTrigger->IsForBaseDefense) || (hasReachedMaxDefensiveTeamsLimit && pTrigger->IsForBaseDefense)) continue; From 48320ef90f81f1ecd69fd00a645b95c692183bc9 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 29 Aug 2026 21:33:02 +0200 Subject: [PATCH 54/59] Filter strictly ground-only offensive triggers when no enemy targets are reachable from factories --- src/Ext/Team/Body.NewTeamsSelector.cpp | 93 ++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/src/Ext/Team/Body.NewTeamsSelector.cpp b/src/Ext/Team/Body.NewTeamsSelector.cpp index 60fedb715a..c075a6af2b 100644 --- a/src/Ext/Team/Body.NewTeamsSelector.cpp +++ b/src/Ext/Team/Body.NewTeamsSelector.cpp @@ -228,6 +228,9 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) int destroyedBridgesCount = 0; int undamagedBridgesCount = 0; std::unordered_map ownedRecruitables; + std::vector landUnitFactories; + std::vector infantryFactories; + std::vector enemyTargets; bool hasInfantryFactory = false; bool hasUnitFactory = false; bool hasNavalFactory = false; @@ -252,6 +255,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) { case AbstractType::InfantryType: hasInfantryFactory = true; + infantryFactories.push_back(pTechno->GetCell()->MapCoords); break; case AbstractType::AircraftType: @@ -260,15 +264,24 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) case AbstractType::UnitType: if (pBuildingType->Naval) + { hasNavalFactory = true; + } else + { hasUnitFactory = true; + landUnitFactories.push_back(pTechno->GetCell()->MapCoords); + } break; default: break; } } + else if (pTechno->Owner != pHouse && !pHouse->IsAlliedWith(pTechno->Owner) && !pTechno->Owner->Type->MultiplayPassive) + { + enemyTargets.push_back(pTechno->GetCell()->MapCoords); + } else if (pBuildingType->BridgeRepairHut) { CellStruct cell = pTechno->GetCell()->MapCoords; @@ -282,6 +295,11 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) continue; } + if (pTechno->Owner != pHouse && !pHouse->IsAlliedWith(pTechno->Owner) && !pTechno->Owner->Type->MultiplayPassive) + { + enemyTargets.push_back(pTechno->GetCell()->MapCoords); + } + auto const pFoot = static_cast(pTechno); if (!pFoot @@ -296,6 +314,46 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) ++ownedRecruitables[pTechno->GetTechnoType()]; } + bool hasReachableEnemyForVehicles = false; + bool hasReachableEnemyForInfantry = false; + + for (const auto& enemyCoord : enemyTargets) + { + int const enemyZone = MapClass::Instance.GetMovementZoneType(enemyCoord, MovementZone::Normal, false); + + if (enemyZone != -1) + { + if (!hasReachableEnemyForVehicles) + { + for (const auto& factoryCoord : landUnitFactories) + { + int const factoryZone = MapClass::Instance.GetMovementZoneType(factoryCoord, MovementZone::Normal, false); + if (factoryZone != -1 && factoryZone == enemyZone) + { + hasReachableEnemyForVehicles = true; + break; + } + } + } + + if (!hasReachableEnemyForInfantry) + { + for (const auto& barracksCoord : infantryFactories) + { + int const barracksZone = MapClass::Instance.GetMovementZoneType(barracksCoord, MovementZone::Normal, false); + if (barracksZone != -1 && barracksZone == enemyZone) + { + hasReachableEnemyForInfantry = true; + break; + } + } + } + } + + if (hasReachableEnemyForVehicles && hasReachableEnemyForInfantry) + break; + } + if (hasInfantryFactory || hasUnitFactory || hasAircraftFactory || hasNavalFactory) canAutocreate = true; @@ -335,6 +393,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) continue; teamCategory teamIsCategory = teamCategory::None; + bool isStrictlyGroundOnly = true; // Analyze what kind of category is this main team if the feature is enabled if (splitTriggersByCategory) @@ -350,6 +409,22 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (!entry.Type) continue; + if (entry.Type->WhatAmI() == AbstractType::AircraftType + || entry.Type->ConsideredAircraft + || entry.Type->Naval + || entry.Type->SpeedType == SpeedType::Hover + || entry.Type->SpeedType == SpeedType::Amphibious + || entry.Type->SpeedType == SpeedType::Winged + || entry.Type->MovementZone == MovementZone::Amphibious + || entry.Type->MovementZone == MovementZone::AmphibiousCrusher + || entry.Type->MovementZone == MovementZone::AmphibiousDestroyer + || entry.Type->MovementZone == MovementZone::WaterBeach + || entry.Type->MovementZone == MovementZone::Subterrannean + || entry.Type->MovementZone == MovementZone::Fly) + { + isStrictlyGroundOnly = false; + } + if (entry.Type->WhatAmI() == AbstractType::AircraftType || entry.Type->ConsideredAircraft) { @@ -406,6 +481,24 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) continue; } + // Discard strictly ground-only offensive triggers if no enemy targets are reachable by land from factories + if (!pTrigger->IsForBaseDefense && isStrictlyGroundOnly) + { + if (teamIsCategory == teamCategory::Ground && !hasReachableEnemyForVehicles) + { + Debug::Log("AITeamsSelector - House %d [%s](%s) trigger [%s]: %s discarded because no enemy targets are reachable by land from unit factories.\n", + pHouse->ArrayIndex, pHouse->PlainName, pHouse->Type->ID, pTrigger->ID, pTrigger->Team1->Name); + continue; + } + + if (teamIsCategory == teamCategory::Infantry && !hasReachableEnemyForInfantry) + { + Debug::Log("AITeamsSelector - House %d [%s](%s) trigger [%s]: %s discarded because no enemy targets are reachable by land from infantry factories.\n", + pHouse->ArrayIndex, pHouse->PlainName, pHouse->Type->ID, pTrigger->ID, pTrigger->Team1->Name); + continue; + } + } + // The trigger must be compatible with the owner ("ConditionType=-1" is always valid) if ((int)pTrigger->ConditionType >= 0) { From e1a1634827cb3389fb7e39f50d54a49f0fb74db1 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 29 Aug 2026 23:17:24 +0200 Subject: [PATCH 55/59] Revert "Filter strictly ground-only offensive triggers when no enemy targets are reachable from factories" This reverts commit 48320ef90f81f1ecd69fd00a645b95c692183bc9. --- src/Ext/Team/Body.NewTeamsSelector.cpp | 93 -------------------------- 1 file changed, 93 deletions(-) diff --git a/src/Ext/Team/Body.NewTeamsSelector.cpp b/src/Ext/Team/Body.NewTeamsSelector.cpp index c075a6af2b..60fedb715a 100644 --- a/src/Ext/Team/Body.NewTeamsSelector.cpp +++ b/src/Ext/Team/Body.NewTeamsSelector.cpp @@ -228,9 +228,6 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) int destroyedBridgesCount = 0; int undamagedBridgesCount = 0; std::unordered_map ownedRecruitables; - std::vector landUnitFactories; - std::vector infantryFactories; - std::vector enemyTargets; bool hasInfantryFactory = false; bool hasUnitFactory = false; bool hasNavalFactory = false; @@ -255,7 +252,6 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) { case AbstractType::InfantryType: hasInfantryFactory = true; - infantryFactories.push_back(pTechno->GetCell()->MapCoords); break; case AbstractType::AircraftType: @@ -264,24 +260,15 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) case AbstractType::UnitType: if (pBuildingType->Naval) - { hasNavalFactory = true; - } else - { hasUnitFactory = true; - landUnitFactories.push_back(pTechno->GetCell()->MapCoords); - } break; default: break; } } - else if (pTechno->Owner != pHouse && !pHouse->IsAlliedWith(pTechno->Owner) && !pTechno->Owner->Type->MultiplayPassive) - { - enemyTargets.push_back(pTechno->GetCell()->MapCoords); - } else if (pBuildingType->BridgeRepairHut) { CellStruct cell = pTechno->GetCell()->MapCoords; @@ -295,11 +282,6 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) continue; } - if (pTechno->Owner != pHouse && !pHouse->IsAlliedWith(pTechno->Owner) && !pTechno->Owner->Type->MultiplayPassive) - { - enemyTargets.push_back(pTechno->GetCell()->MapCoords); - } - auto const pFoot = static_cast(pTechno); if (!pFoot @@ -314,46 +296,6 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) ++ownedRecruitables[pTechno->GetTechnoType()]; } - bool hasReachableEnemyForVehicles = false; - bool hasReachableEnemyForInfantry = false; - - for (const auto& enemyCoord : enemyTargets) - { - int const enemyZone = MapClass::Instance.GetMovementZoneType(enemyCoord, MovementZone::Normal, false); - - if (enemyZone != -1) - { - if (!hasReachableEnemyForVehicles) - { - for (const auto& factoryCoord : landUnitFactories) - { - int const factoryZone = MapClass::Instance.GetMovementZoneType(factoryCoord, MovementZone::Normal, false); - if (factoryZone != -1 && factoryZone == enemyZone) - { - hasReachableEnemyForVehicles = true; - break; - } - } - } - - if (!hasReachableEnemyForInfantry) - { - for (const auto& barracksCoord : infantryFactories) - { - int const barracksZone = MapClass::Instance.GetMovementZoneType(barracksCoord, MovementZone::Normal, false); - if (barracksZone != -1 && barracksZone == enemyZone) - { - hasReachableEnemyForInfantry = true; - break; - } - } - } - } - - if (hasReachableEnemyForVehicles && hasReachableEnemyForInfantry) - break; - } - if (hasInfantryFactory || hasUnitFactory || hasAircraftFactory || hasNavalFactory) canAutocreate = true; @@ -393,7 +335,6 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) continue; teamCategory teamIsCategory = teamCategory::None; - bool isStrictlyGroundOnly = true; // Analyze what kind of category is this main team if the feature is enabled if (splitTriggersByCategory) @@ -409,22 +350,6 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (!entry.Type) continue; - if (entry.Type->WhatAmI() == AbstractType::AircraftType - || entry.Type->ConsideredAircraft - || entry.Type->Naval - || entry.Type->SpeedType == SpeedType::Hover - || entry.Type->SpeedType == SpeedType::Amphibious - || entry.Type->SpeedType == SpeedType::Winged - || entry.Type->MovementZone == MovementZone::Amphibious - || entry.Type->MovementZone == MovementZone::AmphibiousCrusher - || entry.Type->MovementZone == MovementZone::AmphibiousDestroyer - || entry.Type->MovementZone == MovementZone::WaterBeach - || entry.Type->MovementZone == MovementZone::Subterrannean - || entry.Type->MovementZone == MovementZone::Fly) - { - isStrictlyGroundOnly = false; - } - if (entry.Type->WhatAmI() == AbstractType::AircraftType || entry.Type->ConsideredAircraft) { @@ -481,24 +406,6 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) continue; } - // Discard strictly ground-only offensive triggers if no enemy targets are reachable by land from factories - if (!pTrigger->IsForBaseDefense && isStrictlyGroundOnly) - { - if (teamIsCategory == teamCategory::Ground && !hasReachableEnemyForVehicles) - { - Debug::Log("AITeamsSelector - House %d [%s](%s) trigger [%s]: %s discarded because no enemy targets are reachable by land from unit factories.\n", - pHouse->ArrayIndex, pHouse->PlainName, pHouse->Type->ID, pTrigger->ID, pTrigger->Team1->Name); - continue; - } - - if (teamIsCategory == teamCategory::Infantry && !hasReachableEnemyForInfantry) - { - Debug::Log("AITeamsSelector - House %d [%s](%s) trigger [%s]: %s discarded because no enemy targets are reachable by land from infantry factories.\n", - pHouse->ArrayIndex, pHouse->PlainName, pHouse->Type->ID, pTrigger->ID, pTrigger->Team1->Name); - continue; - } - } - // The trigger must be compatible with the owner ("ConditionType=-1" is always valid) if ((int)pTrigger->ConditionType >= 0) { From 0208b24007c39acfb486f228c59c5ba7b9e5ac14 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sat, 29 Aug 2026 23:29:52 +0200 Subject: [PATCH 56/59] Fix maxTeamsLimit variable passed in TotalAITeamCap log message --- src/Ext/Team/Body.NewTeamsSelector.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ext/Team/Body.NewTeamsSelector.cpp b/src/Ext/Team/Body.NewTeamsSelector.cpp index 60fedb715a..0201866aaa 100644 --- a/src/Ext/Team/Body.NewTeamsSelector.cpp +++ b/src/Ext/Team/Body.NewTeamsSelector.cpp @@ -216,7 +216,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) 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, defenseTeamSelectionThreshold); + 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; } From b78dc9f0249d42eea553a6e1f33327924f44e4a7 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sun, 30 Aug 2026 13:53:35 +0200 Subject: [PATCH 57/59] Refactor trigger condition evaluation into TeamExt::EvaluateTriggerCondition --- src/Ext/Team/Body.NewTeamsSelector.cpp | 246 ++++++++++++------------- src/Ext/Team/Body.h | 7 + 2 files changed, 120 insertions(+), 133 deletions(-) diff --git a/src/Ext/Team/Body.NewTeamsSelector.cpp b/src/Ext/Team/Body.NewTeamsSelector.cpp index 5265762880..9830175430 100644 --- a/src/Ext/Team/Body.NewTeamsSelector.cpp +++ b/src/Ext/Team/Body.NewTeamsSelector.cpp @@ -407,139 +407,8 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) continue; } - // The trigger must be compatible with the owner ("ConditionType=-1" is always valid) - if ((int)pTrigger->ConditionType >= 0) - { - bool conditionMet = true; - - switch ((int)pTrigger->ConditionType) - { - case 0: - // Case 0: "enemy owns" - conditionMet = pTrigger->ConditionObject && TeamExt::EnemyOwns(pTrigger, pHouse, targetHouse, true, pTrigger->ConditionObject); - break; - - case 1: - // Case 1: "house owns" - conditionMet = pTrigger->ConditionObject && TeamExt::HouseOwns(pTrigger, pHouse, false, pTrigger->ConditionObject); - break; - - case 4: - // Case 4: "Enemy house economy threshold?" - conditionMet = pTrigger->HouseCredits(nullptr, targetHouse); - break; - - case 5: - // Case 5: "Iron Curtain is charged?" - conditionMet = pTrigger->IronCurtainCharged(pHouse, nullptr); - break; - - case 6: - // Case 6: "Chronosphere is charged?" - conditionMet = pTrigger->ChronoSphereCharged(pHouse, nullptr); - break; - - case 7: - // Case 7: "civilian owns" - conditionMet = pTrigger->ConditionObject && TeamExt::NeutralOwns(pTrigger, pTrigger->ConditionObject); - break; - - case 8: - // Case 8: "enemy owns" across all enemies - conditionMet = pTrigger->ConditionObject && TeamExt::EnemyOwns(pTrigger, pHouse, nullptr, false, pTrigger->ConditionObject); - break; - - case 9: - { - auto const listIdx = pTrigger->Conditions[3].ComparatorOperand; - conditionMet = (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) - && TeamExt::EnemyOwns(pTrigger, pHouse, targetHouse, false, RulesExt::Global()->AITargetTypesLists[listIdx]); - } - break; - - case 10: - { - auto const listIdx = pTrigger->Conditions[3].ComparatorOperand; - conditionMet = (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) - && TeamExt::HouseOwns(pTrigger, pHouse, false, RulesExt::Global()->AITargetTypesLists[listIdx]); - } - break; - - case 11: - { - auto const listIdx = pTrigger->Conditions[3].ComparatorOperand; - conditionMet = (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) - && TeamExt::NeutralOwns(pTrigger, RulesExt::Global()->AITargetTypesLists[listIdx]); - } - break; - - case 12: - { - auto const listIdx = pTrigger->Conditions[3].ComparatorOperand; - conditionMet = (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) - && TeamExt::EnemyOwns(pTrigger, pHouse, nullptr, false, RulesExt::Global()->AITargetTypesLists[listIdx]); - } - break; - - case 13: - { - auto const listIdx = pTrigger->Conditions[3].ComparatorOperand; - conditionMet = (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) - && TeamExt::HouseOwns(pTrigger, pHouse, true, RulesExt::Global()->AITargetTypesLists[listIdx]); - } - break; - - case 14: - { - auto const listIdx = pTrigger->Conditions[3].ComparatorOperand; - conditionMet = (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) - && TeamExt::EnemyOwnsAll(pTrigger, pHouse, targetHouse, RulesExt::Global()->AITargetTypesLists[listIdx]); - } - break; - - case 15: - { - auto const listIdx = pTrigger->Conditions[3].ComparatorOperand; - conditionMet = (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) - && TeamExt::HouseOwnsAll(pTrigger, pHouse, RulesExt::Global()->AITargetTypesLists[listIdx]); - } - break; - - case 16: - { - auto const listIdx = pTrigger->Conditions[3].ComparatorOperand; - conditionMet = (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) - && TeamExt::NeutralOwnsAll(pTrigger, RulesExt::Global()->AITargetTypesLists[listIdx]); - } - break; - - case 17: - { - auto const listIdx = pTrigger->Conditions[3].ComparatorOperand; - conditionMet = (listIdx >= 0 && (size_t)listIdx < RulesExt::Global()->AITargetTypesLists.size()) - && TeamExt::EnemyOwnsAll(pTrigger, pHouse, nullptr, RulesExt::Global()->AITargetTypesLists[listIdx]); - } - break; - - case 18: - // Case 18: Check destroyed bridges - conditionMet = TeamExt::CountConditionMet(pTrigger, destroyedBridgesCount); - break; - - case 19: - // Case 19: Check undamaged bridges - conditionMet = TeamExt::CountConditionMet(pTrigger, undamagedBridgesCount); - break; - - default: - // Other cases from vanilla game - conditionMet = pTrigger->ConditionMet(pHouse, targetHouse, hasReachedMaxDefensiveTeamsLimit); - break; - } - - if (!conditionMet) - continue; - } + if (!TeamExt::EvaluateTriggerCondition(pTrigger, pHouse, targetHouse, hasReachedMaxDefensiveTeamsLimit, destroyedBridgesCount, undamagedBridgesCount)) + continue; if (!pTrigger->Team1->Autocreate && !pTrigger->Team1->Recruiter) continue; @@ -979,3 +848,114 @@ bool TeamExt::CountConditionMet(AITriggerTypeClass* pThis, int nObjects) 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: + return pTrigger->ConditionObject && TeamExt::EnemyOwns(pTrigger, pHouse, pTargetHouse, true, pTrigger->ConditionObject); + + case 1: + return pTrigger->ConditionObject && TeamExt::HouseOwns(pTrigger, pHouse, false, pTrigger->ConditionObject); + + case 4: + return pTrigger->HouseCredits(nullptr, pTargetHouse); + + case 5: + return pTrigger->IronCurtainCharged(pHouse, nullptr); + + case 6: + return pTrigger->ChronoSphereCharged(pHouse, nullptr); + + case 7: + return pTrigger->ConditionObject && TeamExt::NeutralOwns(pTrigger, pTrigger->ConditionObject); + + case 8: + return pTrigger->ConditionObject && TeamExt::EnemyOwns(pTrigger, pHouse, nullptr, false, pTrigger->ConditionObject); + + case 9: + { + 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: + { + 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: + { + 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: + { + 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: + { + 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: + { + 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: + { + 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: + { + 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: + { + 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: + return TeamExt::CountConditionMet(pTrigger, destroyedBridgesCount); + + case 19: + return TeamExt::CountConditionMet(pTrigger, undamagedBridgesCount); + + default: + return pTrigger->ConditionMet(pHouse, pTargetHouse, hasReachedMaxDefensiveTeamsLimit); + } +} diff --git a/src/Ext/Team/Body.h b/src/Ext/Team/Body.h index 16413528df..cc37f0073b 100644 --- a/src/Ext/Team/Body.h +++ b/src/Ext/Team/Body.h @@ -91,6 +91,13 @@ class TeamExt final : public AbstractExt, public Detach::Listener 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) { From b8e69ade1bb60f08d04282fa55085e893a3134ff Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sun, 30 Aug 2026 14:56:49 +0200 Subject: [PATCH 58/59] comments --- src/Ext/Team/Body.NewTeamsSelector.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Ext/Team/Body.NewTeamsSelector.cpp b/src/Ext/Team/Body.NewTeamsSelector.cpp index 9830175430..fc39bb52f1 100644 --- a/src/Ext/Team/Body.NewTeamsSelector.cpp +++ b/src/Ext/Team/Body.NewTeamsSelector.cpp @@ -866,27 +866,35 @@ bool TeamExt::EvaluateTriggerCondition( 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()) @@ -894,6 +902,7 @@ bool TeamExt::EvaluateTriggerCondition( } 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()) @@ -901,6 +910,7 @@ bool TeamExt::EvaluateTriggerCondition( } 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()) @@ -908,6 +918,7 @@ bool TeamExt::EvaluateTriggerCondition( } 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()) @@ -915,6 +926,7 @@ bool TeamExt::EvaluateTriggerCondition( } 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()) @@ -922,6 +934,7 @@ bool TeamExt::EvaluateTriggerCondition( } 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()) @@ -929,6 +942,7 @@ bool TeamExt::EvaluateTriggerCondition( } 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()) @@ -936,6 +950,7 @@ bool TeamExt::EvaluateTriggerCondition( } 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()) @@ -943,6 +958,7 @@ bool TeamExt::EvaluateTriggerCondition( } 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()) @@ -950,12 +966,15 @@ bool TeamExt::EvaluateTriggerCondition( } 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); } } From eced11e0d280e6de62d2e819b3b29a03f73ba3f7 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Sun, 30 Aug 2026 16:58:04 +0200 Subject: [PATCH 59/59] Add NewTeamsSelector.VIPWeight tag supporting custom thresholds and dynamic max weight mode --- docs/AI-Scripting-and-Mapping.md | 33 +++++++++++++---------- src/Ext/HouseType/Body.cpp | 2 ++ src/Ext/HouseType/Body.h | 2 ++ src/Ext/Rules/Body.cpp | 2 ++ src/Ext/Rules/Body.h | 2 ++ src/Ext/Team/Body.NewTeamsSelector.cpp | 36 +++++++++++++++++++------- 6 files changed, 55 insertions(+), 22 deletions(-) diff --git a/docs/AI-Scripting-and-Mapping.md b/docs/AI-Scripting-and-Mapping.md index 988a23da94..cf835b463d 100644 --- a/docs/AI-Scripting-and-Mapping.md +++ b/docs/AI-Scripting-and-Mapping.md @@ -169,14 +169,17 @@ ShowBriefing=true ; boolean ## 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 4 categories: ground, 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 25% chance to be selected. +- 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 understand Ares tags like `Prerequisite.RequiredTheaters`, `Prerequisite.Negative`, `Prerequisite.Lists` & `Generic prerequisites` section. -- If it finds a trigger with 5000 current probability weight then discard valid triggers all and start searching all valid triggers with weight 5000. AI will pick 1 randomly and decrease by 1 the current weight of the selected trigger (so if nothing happens in the next teams selection loop it won't appear in this special list). Under this scenario categories are 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 be able to use unlocked units in captured Secret Labs. +- AI is able to use unlocked units in captured Secret Labs. In `rulesmd.ini`: ```ini @@ -184,18 +187,22 @@ In `rulesmd.ini`: NewTeamsSelector=false ; boolean NewTeamsSelector.SplitTriggersByCategory=true ; boolean NewTeamsSelector.EnableFallback=false ; boolean -NewTeamsSelector.GroundCategoryPercentage=0.25 ; floating point value, percents or absolute -NewTeamsSelector.NavalCategoryPercentage=0.25 ; floating point value, percents or absolute -NewTeamsSelector.AirCategoryPercentage=0.25 ; floating point value, percents or absolute -NewTeamsSelector.UnclassifiedCategoryPercentage=0.25 ; floating point value, percents or absolute -NewTeamsSelector.MergeUnclassifiedCategoryWith=-1 ; Integer - Ground: 1, Air: 2, Naval: 3 +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= ; boolean +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.AirCategoryPercentage ; floating point value, percents or absolute -NewTeamsSelector.NavalCategoryPercentage ; 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 diff --git a/src/Ext/HouseType/Body.cpp b/src/Ext/HouseType/Body.cpp index 587bd34209..a95ac3658a 100644 --- a/src/Ext/HouseType/Body.cpp +++ b/src/Ext/HouseType/Body.cpp @@ -25,6 +25,7 @@ void HouseTypeExt::LoadFromINIFile(CCINIClass* pINI) 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 @@ -38,6 +39,7 @@ void HouseTypeExt::Serialize(T& Stm) .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 8e6d04cdaa..8a520ab196 100644 --- a/src/Ext/HouseType/Body.h +++ b/src/Ext/HouseType/Body.h @@ -33,6 +33,7 @@ class HouseTypeExt final : public AbstractTypeExt Nullable NewTeamsSelector_NavalCategoryPercentage; Nullable NewTeamsSelector_AirCategoryPercentage; Nullable NewTeamsSelector_InfantryCategoryPercentage; + Nullable NewTeamsSelector_VIPWeight; HouseTypeExt(HouseTypeClass* OwnerObject) : AbstractTypeExt(OwnerObject) , EVATag { -2 } @@ -42,6 +43,7 @@ class HouseTypeExt final : public AbstractTypeExt , NewTeamsSelector_NavalCategoryPercentage { } , NewTeamsSelector_AirCategoryPercentage { } , NewTeamsSelector_InfantryCategoryPercentage { } + , NewTeamsSelector_VIPWeight { } { } virtual ~HouseTypeExt() = default; diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index b116d5287d..aaf24a2c99 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -677,6 +677,7 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) 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(); @@ -1049,6 +1050,7 @@ void RulesExt::ExtData::Serialize(T& Stm) .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 4cfe7e1b4e..f3a1c70795 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -346,6 +346,7 @@ class RulesExt Valueable NewTeamsSelector_NavalCategoryPercentage; Valueable NewTeamsSelector_AirCategoryPercentage; Valueable NewTeamsSelector_InfantryCategoryPercentage; + Valueable NewTeamsSelector_VIPWeight; Valueable BerzerkTargeting; Valueable AllowBerzerkOnAllies; @@ -772,6 +773,7 @@ class RulesExt , NewTeamsSelector_NavalCategoryPercentage { 0.20 } , NewTeamsSelector_AirCategoryPercentage { 0.20 } , NewTeamsSelector_InfantryCategoryPercentage { 0.20 } + , NewTeamsSelector_VIPWeight { 5000.0 } , DropPodTrailer { } , DropPodDefaultTrailer { } , PodImage { } diff --git a/src/Ext/Team/Body.NewTeamsSelector.cpp b/src/Ext/Team/Body.NewTeamsSelector.cpp index fc39bb52f1..ec5a044a0b 100644 --- a/src/Ext/Team/Body.NewTeamsSelector.cpp +++ b/src/Ext/Team/Body.NewTeamsSelector.cpp @@ -307,7 +307,18 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) bool onlyCheckImportantTriggers = false; const auto pHouseExt = HouseExt::Fetch(pHouse); - double maxPriority = 5000.0; + 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) @@ -318,7 +329,7 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) continue; // If VIP triggers were found, discard any non-VIP trigger immediately - if (onlyCheckImportantTriggers && pTrigger->Weight_Current < maxPriority) + if (onlyCheckImportantTriggers && !isTriggerVIP(pTrigger)) continue; if (pTrigger->Team1->TechLevel > pHouse->TechLevel) @@ -478,9 +489,9 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) if (!canProduce) continue; - // Special case: triggers become very important if they reach the max priority (usually 5000, see maxPriority). - // They get stored in an elitist list and all previous triggers are discarded - if (pTrigger->Weight_Current >= maxPriority && !onlyCheckImportantTriggers) + // 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; @@ -568,11 +579,18 @@ DEFINE_HOOK(0x4F8A27, TeamTypeClass_SuggestedNewTeam_NewTeamsSelector, 0x5) return SkipCode; } - if (selectedTrigger->Weight_Current >= maxPriority - && selectedTrigger->Weight_Minimum <= (maxPriority - 1)) + if (isTriggerVIP(selectedTrigger)) { - // Next time this trigger will be out of the elitist triggers list - selectedTrigger->Weight_Current = maxPriority - 1; + 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