diff --git a/CREDITS.md b/CREDITS.md
index 0b5547141b..5d94c251c8 100644
--- a/CREDITS.md
+++ b/CREDITS.md
@@ -289,6 +289,7 @@ This page lists all the individual contributions to the project by their author.
- Guard range customizations
- Wall overlay unit sell exploit fix
- Fix vehicles disguised as trees incorrectly displaying veterancy insignia when they shouldn't
+ - AI trigger condition type extension
- **Morton (MortonPL)**:
- `XDrawOffset` for animations
- Shield passthrough & absorption
diff --git a/Phobos.vcxproj b/Phobos.vcxproj
index d45a6ac91f..ad001cec00 100644
--- a/Phobos.vcxproj
+++ b/Phobos.vcxproj
@@ -18,6 +18,8 @@
+
+
@@ -219,6 +221,7 @@
+
diff --git a/YRpp b/YRpp
index 6aa702acd4..5da505f7dd 160000
--- a/YRpp
+++ b/YRpp
@@ -1 +1 @@
-Subproject commit 6aa702acd40d9b166644e4e0b6a3b510130493fc
+Subproject commit 5da505f7dd39fdbcd4090dd3eb583e723fa9a3eb
diff --git a/docs/AI-Scripting-and-Mapping.md b/docs/AI-Scripting-and-Mapping.md
index 79bb79626b..c56ebee434 100644
--- a/docs/AI-Scripting-and-Mapping.md
+++ b/docs/AI-Scripting-and-Mapping.md
@@ -514,6 +514,25 @@ x=i,n ; where 18048 <= i <= 18071, n is made up of two parts, the lo
This category is empty for now.
+## AI Trigger Condition Types
+
+Phobos adds new AI trigger condition types. AI trigger's condition type is defined as the number in fifth comma-separated parameter in `AITriggerType` declaration.
+
+F.ex
+```ini
+ID=Name,Team1,OwnerHouse,TechLevel,ConditionType,ConditionObject,Comparator,StartingWeight,MinimumWeight,MaximumWeight,IsForSkirmish,unused,Side,IsBaseDefense,Team2,EnabledInE,EnabledInM,EnabledInH
+```
+
+Some condition types operate on or with `ConditionObject` and `Comperator` as well. More information on defining AITriggerTypes can be found on [ModEnc](https://modenc.renegadeprojects.com/AITriggerTypes).
+
+### `8` Amount of Capturable Tech Buildings Exists
+
+Trigger can spring if a number of `Capturable=true` + `NeedsEngineer=true` buildings owned in total by all houses the trigger owner is not allied with satisfies `Comparator`.
+
+### `9` Amount of Bridge Repair Huts Exists
+
+Trigger can spring if a number of `BridgeRepairHut=true` buildings linked to broken bridges owned by first house belonging to `Civilian` side satisfies `Comparator`.
+
## Trigger Actions
### `500` Save Game
diff --git a/docs/Whats-New.md b/docs/Whats-New.md
index 096e7cd9c5..6d661cbfd9 100644
--- a/docs/Whats-New.md
+++ b/docs/Whats-New.md
@@ -542,6 +542,7 @@ New:
- Allow jumpjet climbing ignore building height (by TaranDahl)
- [Allow draw SuperWeapon timer as percentage](User-Interface.md#allow-draw-superweapon-timer-as-percentage) (by NetsuNegi)
- Customize particle system of parasite logic (by NetsuNegi)
+- [New AITriggerType conditions for tech buildings & bridge repair huts](AI-Scripting-and-Mapping.md#ai-trigger-condition-types) (by Starkku)
Vanilla fixes:
- Fixed sidebar not updating queued unit numbers when adding or removing units when the production is on hold (by CrimRecya)
diff --git a/src/Ext/AITriggerType/Body.cpp b/src/Ext/AITriggerType/Body.cpp
new file mode 100644
index 0000000000..23b636ebb0
--- /dev/null
+++ b/src/Ext/AITriggerType/Body.cpp
@@ -0,0 +1,120 @@
+#include "Body.h"
+
+AITriggerTypeExt::ExtContainer AITriggerTypeExt::ExtMap;
+
+// =============================
+// load / save
+
+void AITriggerTypeExt::ExtData::LoadFromStream(PhobosStreamReader& Stm)
+{
+ // Nothing yet
+}
+
+void AITriggerTypeExt::ExtData::SaveToStream(PhobosStreamWriter& Stm)
+{
+ // Nothing yet
+}
+
+// container
+
+AITriggerTypeExt::ExtContainer::ExtContainer() : Container("AITriggerTypeClass")
+{ }
+
+AITriggerTypeExt::ExtContainer::~ExtContainer() = default;
+
+int AITriggerTypeExt::CheckConditions(AITriggerTypeClass* pThis, HouseClass* pOwner, HouseClass* pEnemy)
+{
+ int condition = (int)pThis->ConditionType;
+
+ if (condition < -1) // Invalid, bail out early.
+ return 0;
+ else if (condition < (int)PhobosAITriggerConditions::NumberOfTechBuildingsExist) // Not Phobos condition
+ return -1;
+
+ bool success = false;
+
+ switch ((PhobosAITriggerConditions)condition)
+ {
+ case PhobosAITriggerConditions::NumberOfTechBuildingsExist:
+ success = AITriggerTypeExt::NumberOfTechBuildingsExist(pThis, pOwner);
+ break;
+ case PhobosAITriggerConditions::NumberOfBridgeRepairHutsExist:
+ success = AITriggerTypeExt::NumberOfBridgeRepairHutsExist(pThis);
+ break;
+ }
+
+ return success;
+}
+
+bool AITriggerTypeExt::GetComparatorResult(int operand1, AITriggerConditionComparatorType operatorType, int operand2)
+{
+ switch (operatorType)
+ {
+ case AITriggerConditionComparatorType::Less:
+ return operand1 < operand2;
+ break;
+ case AITriggerConditionComparatorType::LessOrEqual:
+ return operand1 <= operand2;
+ break;
+ case AITriggerConditionComparatorType::Equal:
+ return operand1 == operand2;
+ break;
+ case AITriggerConditionComparatorType::GreaterOrEqual:
+ return operand1 >= operand2;
+ break;
+ case AITriggerConditionComparatorType::Greater:
+ return operand1 > operand2;
+ break;
+ case AITriggerConditionComparatorType::NotEqual:
+ return operand1 != operand2;
+ break;
+ default:
+ return false;
+ break;
+ }
+}
+
+bool AITriggerTypeExt::NumberOfTechBuildingsExist(AITriggerTypeClass* pThis, HouseClass* pOwner)
+{
+ int count = 0;
+
+ for (auto const pHouse : HouseClass::Array)
+ {
+ if (pHouse->IsAlliedWith(pOwner))
+ continue;
+
+ // Could possibly be optimized with bespoke tracking but
+ // it didn't seem to make much of a difference in testing.
+ for (auto const pBuilding : pHouse->Buildings)
+ {
+ if (!pBuilding->IsAlive || pBuilding->InLimbo)
+ continue;
+
+ auto const pType = pBuilding->Type;
+
+ if (pType->NeedsEngineer && pType->Capturable)
+ count++;
+ }
+ }
+
+ return AITriggerTypeExt::GetComparatorResult(count, pThis->Conditions[0].ComparatorType, pThis->Conditions[0].ComparatorOperand);
+}
+
+bool AITriggerTypeExt::NumberOfBridgeRepairHutsExist(AITriggerTypeClass* pThis)
+{
+ int count = 0;
+ auto const pHouse = HouseClass::FindCivilianSide();
+
+ for (auto const pBuilding : pHouse->Buildings)
+ {
+ if (!pBuilding->IsAlive || pBuilding->InLimbo)
+ continue;
+
+ auto const pType = pBuilding->Type;
+
+ if (pType->BridgeRepairHut && MapClass::Instance.IsLinkedBridgeDestroyed(pBuilding->GetMapCoords()))
+ count++;
+ }
+
+ return AITriggerTypeExt::GetComparatorResult(count, pThis->Conditions[0].ComparatorType, pThis->Conditions[0].ComparatorOperand);
+}
diff --git a/src/Ext/AITriggerType/Body.h b/src/Ext/AITriggerType/Body.h
new file mode 100644
index 0000000000..23eac1a0e1
--- /dev/null
+++ b/src/Ext/AITriggerType/Body.h
@@ -0,0 +1,50 @@
+#pragma once
+#include
+#include
+
+// Vanilla game ones range from -1 to 7, see AITriggerCondition in GeneralDefinitions.h in YRpp.
+enum class PhobosAITriggerConditions : unsigned int
+{
+ NumberOfTechBuildingsExist = 8,
+ NumberOfBridgeRepairHutsExist = 9,
+};
+
+class AITriggerTypeExt
+{
+public:
+ using base_type = AITriggerTypeClass;
+
+ static constexpr DWORD Canary = 0x9B9B9B9B;
+
+ class ExtData final : public Extension
+ {
+ public:
+ // Nothing yet
+
+ ExtData(AITriggerTypeClass* OwnerObject) : Extension(OwnerObject)
+ // Nothing yet
+ { }
+
+ virtual ~ExtData() = default;
+
+ virtual void InvalidatePointer(void* ptr, bool bRemoved) override { }
+
+ virtual void LoadFromStream(PhobosStreamReader& Stm) override;
+ virtual void SaveToStream(PhobosStreamWriter& Stm) override;
+
+ };
+
+ class ExtContainer final : public Container
+ {
+ public:
+ ExtContainer();
+ ~ExtContainer();
+ };
+
+ static ExtContainer ExtMap;
+
+ static int CheckConditions(AITriggerTypeClass* pThis, HouseClass* pOwner, HouseClass* pEnemy);
+ static bool GetComparatorResult(int operand1, AITriggerConditionComparatorType operatorType, int operand2);
+ static bool NumberOfTechBuildingsExist(AITriggerTypeClass* pThis, HouseClass* pOwner);
+ static bool NumberOfBridgeRepairHutsExist(AITriggerTypeClass* pThis);
+};
diff --git a/src/Ext/AITriggerType/Hooks.cpp b/src/Ext/AITriggerType/Hooks.cpp
new file mode 100644
index 0000000000..578989cb12
--- /dev/null
+++ b/src/Ext/AITriggerType/Hooks.cpp
@@ -0,0 +1,26 @@
+#include
+#include
+
+DEFINE_HOOK(0x41E8FF, AITriggerTypeClass_NewTeam_CheckConditions, 0x9) // ConditionMet() in YRpp
+{
+ enum { ContinueGameChecks = 0x41E908, ReturnFromFunction = 0x41E9E1, Success = 0x41E9EA };
+
+ GET(AITriggerTypeClass*, pThis, ESI);
+ GET(HouseClass*, pOwner, EDI);
+ GET(HouseClass*, pEnemy, EBX);
+
+ int result = AITriggerTypeExt::CheckConditions(pThis, pOwner, pEnemy);
+
+ switch (result)
+ {
+ case 0:
+ return ReturnFromFunction;
+ break;
+ case 1:
+ return Success;
+ break;
+ default:
+ return ContinueGameChecks;
+ break;
+ }
+}