diff --git a/CREDITS.md b/CREDITS.md index d486713e9d..e503538c13 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -134,10 +134,9 @@ This page lists all the individual contributions to the project by their author. - Power plant damage factor - **FS-21**: - Dump Object Info enhancements - - `Powered.KillSpawns` - - `Spawner.LimitRange` - Majority of ScriptType actions - ScriptType Action 14004: Force Global `OnlyTargetHouseEnemy` value in Teams + - Script actions for managing AI Triggers - MC deployer fixes - Help with docs - Automatic Passenger Deletion logic diff --git a/Phobos.vcxproj b/Phobos.vcxproj index 67dc28e6ee..37deaf25f5 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -132,6 +132,7 @@ + diff --git a/docs/AI-Scripting-and-Mapping.md b/docs/AI-Scripting-and-Mapping.md index 074d3917e2..8b2f9d0ee2 100644 --- a/docs/AI-Scripting-and-Mapping.md +++ b/docs/AI-Scripting-and-Mapping.md @@ -508,6 +508,87 @@ In `aimd.ini`: x=16005,0 ``` +### `16006` Set House Index For Managing AI Triggers + +- Set the house index for enabling and disabling triggers. The indexes aren't the ones used in [Countries], these are internal in-game House indexes. + +In `aimd.ini`: +```ini +[SOMESCRIPTTYPE] ; ScriptType +x=16006,n ; integer +``` + +- The possible argument values are: + +| *Argument* | *Description* | +| :--------: | :-------------------------------------------: | +| 4475-4482 | Multiplayer start location indexes | +| 8997 | Special case that returns the house index of the Team object | +| >= 0 | House index. Neutral houses can be used | +| -1 | Random non-neutral House | +| -2 | Find the first Neutral house available | +| -3 | Random Human Player alive | + +### `16007` Enable Or Disable All AI Triggers + +- All AI triggers will be enabled or disabled. +You must set the affected side with action `16012` and house with action `16006`. + +In `aimd.ini`: +```ini +[SOMESCRIPTTYPE] ; ScriptType +x=16007,n ; integer, 1 == enable, 0 == disable +``` + +### `16008` Enable AI Triggers From List + +- When executed this action enable all AI trigger types from the selected list in `AITriggersList`. The second parameter is a 0-based index from the new section `AITriggersList` explained below. + +In `aimd.ini`: +```ini +[SOMESCRIPTTYPE] ; ScriptType +x=16008,n +``` + +The second parameter is a 0-based index for the `AITriggersList` section that specifies the list of possible `AITriggerTypes` that can be evaluated. The new `AITriggersList` section must be declared in `rulesmd.ini` for making this script work: + +In `rulesmd.ini`: +```ini +[AITriggersList] ; List of AITriggerTypes lists +0=SOMEAITRIGGERTYPE,SOMEOTHERAITRIGGERTYPE,SAMPLETRIGGERTYPE +1=ANOTHERTRIGGERTYPE,YETANOTHERTRIGGERTYPE +; ... +``` + +### `16009` Disable AI Triggers From List + +- Works silimar to the Action `16008`. When executed this action disable all AI trigger types from the selected list in `AITriggersList`. The second parameter is a 0-based index from the new section `AITriggersList`. + +### `16010` Disable AI Triggers If Contains Any Objects From the List + +- Works silimar to the Action `16011`. When executed this action all AI Trigger Types that contains any unit of the selected list in `AITargetTypes` will be disabled. The second parameter is a 0-based index from the new section `AITargetTypes`. + +### `16011` Enable AI Triggers If Contains Any Objects From the List + +- When executed this action all AI trigger types that contains any unit of the selected list in `AITargetTypes` will be enabled. The second parameter is a 0-based index from the new section `AITargetTypes`. + +In `aimd.ini`: +```ini +[SOMESCRIPTTYPE] ; ScriptType +x=16011,n +``` + +### `16012` Set Side Index For Managing AI Triggers + +- Set the side index for enabling and disabling triggers. +0 means any side. + +In `aimd.ini`: +```ini +[SOMESCRIPTTYPE] ; ScriptType +x=16012,n ; integer, where 0 > n, default -1 +``` + ### `18000-18999` Variable Manipulation #### `18000-18023` Edit Variable diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 8225c1eaa0..121af88f39 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -328,6 +328,13 @@ HideShakeEffects=false ; boolean 10103=Load Onto Transports,0,0,1,[LONG DESC] 10104=Chronoshift to Enemy Base,20,0,1,[LONG DESC] 14004=Force Global OnlyTargetHouseEnemy value in Teams,20,0,1,[LONG DESC] + 16006=Set House Index For Managing AI Triggers,20,0,1,[LONG DESC] + 16007=Enable Or Disable All AI Triggers,21,0,1,[LONG DESC] + 16008=Enable AI Triggers From List,28,0,1,[LONG DESC] + 16009=Disable AI Triggers From List,28,0,1,[LONG DESC] + 16010=Disable AI Triggers If Contains Any Objects From the List,29,0,1,[LONG DESC] + 16011=Enable AI Triggers If Contains Any Objects From the List,29,0,1,[LONG DESC] + 16012=Set Side Index For Managing AI Triggers,20,0,1,[LONG DESC] 18000=Local variable set,22,0,1,[LONG DESC] 18001=Local variable add,22,0,1,[LONG DESC] 18002=Local variable minus,22,0,1,[LONG DESC] @@ -408,6 +415,12 @@ HideShakeEffects=false ; boolean 25=Local variables,-4 26=Global variables,-5 27=Global variables,-6 + 28=AI Scripts List, -7 + 29=AI Target Type,-8 + + [ScriptParamTypes] + 7=AIScriptsList,1,1,0 + 8=AITargetTypes,1,1,0 ``` ```` @@ -421,6 +434,7 @@ HideShakeEffects=false ; boolean #### New: - [Customized transport plane for teams](AI-Scripting-and-Mapping.md#customized-transport-plane-for-teams) (by FlyStar) - [Modify ammo on impact](New-or-Enhanced-Logics.md#modify-ammo-on-impact) (by FS-21) +- [Script actions for managing AI Triggers](AI-Scripting-and-Mapping.md#script-actions-for-managing-ai-triggers) (by FS-21) #### Vanilla fixes: - Fixed the bug where a building with `Factory=BuildingType` owned by the AI did not play `ProductionAnim` when placing a produced building (by Noble_Fish) @@ -951,6 +965,7 @@ HideShakeEffects=false ; boolean - Building airstrike target eligibility customization (by Starkku) - IvanBomb detonation & image display optionally centered on buildings (by Starkku) - Forcing specific weapon against cloaked or disguised targets (by Starkku) +- Script action for enabling & disabling AI Triggers (by FS-21) - Customizable ROF random delay (by Starkku) - Animation with `Tiled=yes` now supports `CustomPalette` (by ststl) - Toggleable `DieSound` when grinding (by Trsdy) diff --git a/src/Ext/House/Body.cpp b/src/Ext/House/Body.cpp index e53d1d2ad9..f638c21bc8 100644 --- a/src/Ext/House/Body.cpp +++ b/src/Ext/House/Body.cpp @@ -586,6 +586,73 @@ float HouseExt::GetRestrictedFactoryPlantMult(TechnoTypeClass* pTechnoType) cons return 1.0f - ((1.0f - mult) * pTechnoTypeExt->FactoryPlant_Multiplier); } +int HouseExt::GetHouseIndex(int param, TeamClass* pTeam, TActionClass* pTAction) +{ + if ((pTeam && pTAction) || (param == 8997 && !pTeam && !pTAction)) + return -1; + + int houseIdx = -1; + std::vector housesListIdx; + + // Check special cases + if (param < 0) + { + if (param < -3) + return -1; + + for (auto pHouse : HouseClass::Array) + { + if (param == -2 && pHouse->IsNeutral()) + { + houseIdx = pHouse->ArrayIndex; + break; + } + else if (!pHouse->Defeated && !pHouse->IsObserver()) + { + if ((param == -1 && !pHouse->Type->MultiplayPassive) // Random non-neutral player + || (param == -3 && !pHouse->IsObserver())) // Random human player + { + housesListIdx.push_back(pHouse->ArrayIndex); + } + + if (housesListIdx.size() > 0) + houseIdx = housesListIdx.at(ScenarioClass::Instance->Random.RandomRanged(0, housesListIdx.size() - 1)); + else + return -1; + } + } + + return houseIdx; + } + + // Check a specific house of the map + if (param >= HouseClass::PlayerAtA && param <= HouseClass::PlayerAtH) + { + // Is a multiplayer house index (Player@A - Player@H) ? + houseIdx = param - HouseClass::PlayerAtA; + } + else if (param == 8997) + { + // Is the owner of the trigger ? + houseIdx = pTeam ? pTeam->Owner->ArrayIndex : pTAction->TeamType->Owner->ArrayIndex; + } + else if (param > 8997 || HouseClass::Array.Count <= param) + { + // Is a invalid index value + if (pTAction) + Debug::Log(__FUNCTION__": Invalid house index '%d'. This action could be skipped.\n", (int)pTAction->ActionKind, param); + + return -1; + } + + HouseClass* pHouse = HouseClass::Array.GetItem(houseIdx); + + if (!pHouse->Defeated && !pHouse->IsObserver()) + return houseIdx; + + return -1; +} + void HouseExt::SetForceOnlyTargetHouseEnemy(HouseClass* pThis, int mode) { const auto pHouseExt = HouseExt::Fetch(pThis); diff --git a/src/Ext/House/Body.h b/src/Ext/House/Body.h index df5ba629b6..597ba4bd0d 100644 --- a/src/Ext/House/Body.h +++ b/src/Ext/House/Body.h @@ -1,5 +1,6 @@ #pragma once #include +#include #include @@ -171,6 +172,7 @@ class HouseExt final : public AbstractExt, public Detach::ListenerAIScriptsLists.emplace_back(std::move(objectsList)); } + + // Section AITriggersList + int triggerItemsCount = pINI->GetKeyCount("AITriggersList"); + for (int i = 0; i < triggerItemsCount; ++i) + { + std::vector objectsList; + + char* context = nullptr; + pINI->ReadString("AITriggersList", pINI->GetKeyName("AITriggersList", i), "", Phobos::readBuffer); + + for (char *cur = strtok_s(Phobos::readBuffer, Phobos::readDelims, &context); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context)) + { + AITriggerTypeClass* pNewTrigger = GameCreate(cur); // Note: Don't use ::FindOrAllocate(cur) here... + objectsList.emplace_back(pNewTrigger); + } + + this->AITriggersLists.emplace_back(std::move(objectsList)); + } } // this should load everything that TypeData is not dependant on @@ -695,6 +713,7 @@ void RulesExt::ExtData::Serialize(T& Stm) Stm .Process(this->AITargetTypesLists) .Process(this->AIScriptsLists) + .Process(this->AITriggersLists) .Process(this->Storage_TiberiumIndex) .Process(this->HarvesterDumpAmount) .Process(this->InfantryGainSelfHealCap) diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h index 2de689e591..933c030d24 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -24,6 +25,7 @@ class RulesExt public: std::vector> AITargetTypesLists; std::vector> AIScriptsLists; + std::vector> AITriggersLists; Valueable Storage_TiberiumIndex; Valueable HarvesterDumpAmount; diff --git a/src/Ext/Script/Body.ManageAITriggers.cpp b/src/Ext/Script/Body.ManageAITriggers.cpp new file mode 100644 index 0000000000..16993f2c74 --- /dev/null +++ b/src/Ext/Script/Body.ManageAITriggers.cpp @@ -0,0 +1,186 @@ +#include "Body.h" +#include + +void ScriptExt::ManageTriggersFromList(TeamClass* pTeam, int idxAITriggerType = -1, bool isEnabled = false) +{ + auto pScript = pTeam->CurrentScript; + + if (idxAITriggerType < 0) + idxAITriggerType = pScript->Type->ScriptActions[pScript->CurrentMission].Argument; + + if (idxAITriggerType < 0 || RulesExt::Global()->AITriggersLists.size() <= 0) + { + pTeam->StepCompleted = true; + ScriptExt::Log("AI Scripts - ManageTriggersFromList: [%s] [%s] (line: %d = %d,%d) Aborting script action because the [AITriggersLists] index %d is invalid.\n", + pTeam->Type->ID, + pScript->Type->ID, + pScript->CurrentMission, + pScript->Type->ScriptActions[pScript->CurrentMission].Action, + pScript->Type->ScriptActions[pScript->CurrentMission].Argument, + idxAITriggerType); + + return; + } + + DynamicVectorClass objectsList; + for (auto obj : RulesExt::Global()->AITriggersLists[idxAITriggerType]) + objectsList.AddUnique(obj); + + for (auto pTrigger : AITriggerTypeClass::Array) + { + if (objectsList.FindItemIndex(pTrigger) >= 0) + pTrigger->IsEnabled = isEnabled; + } + + // This action finished + pTeam->StepCompleted = true; +} + +void ScriptExt::ManageAllTriggersFromHouse(TeamClass* pTeam, HouseClass* pHouse = nullptr, int sideIdx = -1, int houseIdx = -1, bool isEnabled = true) +{ + // if pHouse is set then it overwrites any argument + if (pHouse) + { + houseIdx = pHouse->ArrayIndex; + sideIdx = pHouse->SideIndex; + } + + if (sideIdx < 0) + return; + + for (auto pTrigger : AITriggerTypeClass::Array) + { + if ((houseIdx == -1 || houseIdx == pTrigger->HouseIndex) && (sideIdx == 0 || sideIdx == pTrigger->SideIndex)) + pTrigger->IsEnabled = isEnabled; + } + + // This action finished + pTeam->StepCompleted = true; +} + +void ScriptExt::SetSideIdxForManagingTriggers(TeamClass* pTeam, int sideIdx = -1) +{ + auto pScript = pTeam->CurrentScript; + + if (sideIdx < 0) + sideIdx = pScript->Type->ScriptActions[pScript->CurrentMission].Argument; + + // Any negative value will mark it as "Any side" + if (sideIdx < 0) + sideIdx = -1; + + if (auto pTeamData = TeamExt::Fetch(pTeam)) + pTeamData->TriggersSideIdx = sideIdx; + + // This action finished + pTeam->StepCompleted = true; +} + +void ScriptExt::SetHouseIdxForManagingTriggers(TeamClass* pTeam, int houseIdx = 2147483647) +{ + auto pScript = pTeam->CurrentScript; + + // Note: this magic number is a default index value the game will never be able to use due to technical limitations + if (houseIdx == 2147483647) + houseIdx = pScript->Type->ScriptActions[pScript->CurrentMission].Argument; + + houseIdx = HouseExt::GetHouseIndex(houseIdx, pTeam, nullptr); + + if (auto pTeamData = TeamExt::Fetch(pTeam)) + pTeamData->TriggersHouseIdx = houseIdx; + + // This action finished + pTeam->StepCompleted = true; +} + +void ScriptExt::ManageAITriggers(TeamClass* pTeam, int enabled = -1) +{ + auto pTeamData = TeamExt::Fetch(pTeam); + if (!pTeamData) + { + // This action finished + pTeam->StepCompleted = true; + return; + } + + int sideIdx = pTeamData->TriggersSideIdx; + int houseIdx = pTeamData->TriggersHouseIdx; + pTeamData->TriggersSideIdx = -1; + pTeamData->TriggersHouseIdx = -1; + auto pScript = pTeam->CurrentScript; + bool isEnabled = false; + + if (enabled < 0) + enabled = pScript->Type->ScriptActions[pScript->CurrentMission].Argument; + else + isEnabled = true; + + ScriptExt::ManageAllTriggersFromHouse(pTeam, nullptr, sideIdx, houseIdx, isEnabled); + + // This action finished + pTeam->StepCompleted = true; +} + +void ScriptExt::ManageTriggersWithObjects(TeamClass* pTeam, int idxAITargetType = -1, bool isEnabled = false) +{ + auto pScript = pTeam->CurrentScript; + + if (idxAITargetType < 0) + idxAITargetType = pScript->Type->ScriptActions[pScript->CurrentMission].Argument; + + if (idxAITargetType < 0 || RulesExt::Global()->AITargetTypesLists.size() <= 0) + { + pTeam->StepCompleted = true; + ScriptExt::Log("AI Scripts - ManageTriggersWithObjects: [%s] [%s] (line: %d = %d,%d) Aborting script action because the [AITargetTypes] index %d is invalid.\n", + pTeam->Type->ID, + pScript->Type->ID, + pScript->CurrentMission, + pScript->Type->ScriptActions[pScript->CurrentMission].Action, + pScript->Type->ScriptActions[pScript->CurrentMission].Argument, + idxAITargetType); + + return; + } + + DynamicVectorClass objectsList; + for (auto obj : RulesExt::Global()->AITargetTypesLists[idxAITargetType]) + objectsList.AddUnique(obj); + + if (objectsList.Count == 0) + { + pTeam->StepCompleted = true; + return; + } + + auto AddTechnosFromTeam = [&](DynamicVectorClass& entriesList, TeamTypeClass* pTeam) + { + if (pTeam) + { + for (auto entry : pTeam->TaskForce->Entries) + { + if (entry.Amount > 0) + entriesList.AddItem(entry.Type); + } + } + }; + + for (auto pTrigger : AITriggerTypeClass::Array) + { + DynamicVectorClass entriesList; + + AddTechnosFromTeam(entriesList, pTrigger->Team1); + AddTechnosFromTeam(entriesList, pTrigger->Team2); + + for (auto entry : entriesList) + { + if (objectsList.FindItemIndex(entry) >= 0) + { + pTrigger->IsEnabled = isEnabled; + break; + } + } + } + + // This action finished + pTeam->StepCompleted = true; +} diff --git a/src/Ext/Script/Body.cpp b/src/Ext/Script/Body.cpp index 8380de3b5b..0294cf60a7 100644 --- a/src/Ext/Script/Body.cpp +++ b/src/Ext/Script/Body.cpp @@ -216,6 +216,27 @@ void ScriptExt::ProcessAction(TeamClass* pTeam) // Chronoshift to enemy base, argument is additional distance modifier ScriptExt::ChronoshiftToEnemyBase(pTeam, argument); break; + case PhobosScripts::SetSideIdxForManagingTriggers: + ScriptExt::SetSideIdxForManagingTriggers(pTeam, -1); + break; + case PhobosScripts::SetHouseIdxForManagingTriggers: + ScriptExt::SetHouseIdxForManagingTriggers(pTeam, 2147483647); + break; + case PhobosScripts::ManageAllAITriggers: + ScriptExt::ManageAITriggers(pTeam, -1); + break; + case PhobosScripts::EnableTriggersFromList: + ScriptExt::ManageTriggersFromList(pTeam, -1, true); + break; + case PhobosScripts::DisableTriggersFromList: + ScriptExt::ManageTriggersFromList(pTeam, -1, false); + break; + case PhobosScripts::EnableTriggersWithObjects: + ScriptExt::ManageTriggersWithObjects(pTeam, -1, true); + break; + case PhobosScripts::DisableTriggersWithObjects: + ScriptExt::ManageTriggersWithObjects(pTeam, -1, false); + break; case PhobosScripts::ForceGlobalOnlyTargetHouseEnemy: ScriptExt::ForceGlobalOnlyTargetHouseEnemy(pTeam, -1); break; diff --git a/src/Ext/Script/Body.h b/src/Ext/Script/Body.h index f1ce860587..88bb1055bc 100644 --- a/src/Ext/Script/Body.h +++ b/src/Ext/Script/Body.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -70,6 +71,13 @@ enum class PhobosScripts : unsigned int RandomSkipNextAction = 16003, PickRandomScript = 16004, JumpBackToPreviousScript = 16005, + SetHouseIdxForManagingTriggers = 16006, + ManageAllAITriggers = 16007, + EnableTriggersFromList = 16008, + DisableTriggersFromList = 16009, + DisableTriggersWithObjects = 16010, + EnableTriggersWithObjects = 16011, + SetSideIdxForManagingTriggers = 16012, // Range 18000-18999 are variable actions LocalVariableSet = 18000, @@ -219,6 +227,8 @@ class ScriptExt final : public AbstractExt static void Stop_ForceJump_Countdown(TeamClass* pTeam); static void JumpBackToPreviousScript(TeamClass* pTeam); static void ChronoshiftToEnemyBase(TeamClass* pTeam, int extraDistance); + static void ManageTriggersFromList(TeamClass* pTeam, int idxAITriggerType, bool isEnabled); + static void ManageAllTriggersFromHouse(TeamClass* pTeam, HouseClass* pHouse, int sideIdx, int houseIdx, bool isEnabled); static void ForceGlobalOnlyTargetHouseEnemy(TeamClass* pTeam, int mode); @@ -229,6 +239,10 @@ class ScriptExt final : public AbstractExt template static void VariableBinaryOperationHandler(TeamClass* pTeam, int nVariable, int nVarToOperate); static bool IsUnitAvailable(TechnoClass* pTechno, bool checkIfInTransportOrAbsorbed); + static void SetSideIdxForManagingTriggers(TeamClass* pTeam, int sideIdx); + static void SetHouseIdxForManagingTriggers(TeamClass* pTeam, int houseIdx); + static void ManageAITriggers(TeamClass* pTeam, int enabled); + static void ManageTriggersWithObjects(TeamClass* pTeam, int idxAITargetType, bool isEnabled); static void Log(const char* pFormat, ...); static void PlaySpeech(TeamClass* pTeam); diff --git a/src/Ext/Team/Body.cpp b/src/Ext/Team/Body.cpp index 1d8be2bc29..bc12bd3b52 100644 --- a/src/Ext/Team/Body.cpp +++ b/src/Ext/Team/Body.cpp @@ -22,6 +22,8 @@ void TeamExt::Serialize(T& Stm) .Process(this->ForceJump_RepeatMode) .Process(this->TeamLeader) .Process(this->PreviousScriptList) + .Process(this->TriggersSideIdx) + .Process(this->TriggersHouseIdx) ; } diff --git a/src/Ext/Team/Body.h b/src/Ext/Team/Body.h index 5d6b9916b5..a2fc28538f 100644 --- a/src/Ext/Team/Body.h +++ b/src/Ext/Team/Body.h @@ -37,6 +37,8 @@ class TeamExt final : public AbstractExt, public Detach::Listener bool ForceJump_RepeatMode; FootClass* TeamLeader; std::vector PreviousScriptList; + int TriggersSideIdx; + int TriggersHouseIdx; TeamExt(TeamClass* OwnerObject) : AbstractExt(OwnerObject) , WaitNoTargetAttempts { 0 } @@ -52,6 +54,8 @@ class TeamExt final : public AbstractExt, public Detach::Listener , ForceJump_RepeatMode { false } , TeamLeader { nullptr } , PreviousScriptList { } + , TriggersSideIdx { -1 } + , TriggersHouseIdx { -1 } { } virtual ~TeamExt() = default;