Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CREDITS.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ This page lists all the individual contributions to the project by their author.
- Unit & infantry auto-conversion on ammo change
- Restore the ScriptType action#24 `Play speech` from Tiberian Sun
- Modify ammo on impact
- Superweapon cooldown groups
- Randomize AI superweapon priority
- **Starkku**:
- Misc. minor bugfixes & improvements
- AI script actions:
Expand Down
26 changes: 26 additions & 0 deletions docs/New-or-Enhanced-Logics.md
Original file line number Diff line number Diff line change
Expand Up @@ -1447,6 +1447,21 @@ Message.LinkedSWAcquired= ; CSF entry key
EVA.LinkedSWAcquired= ; EVA entry
```

### Cooldown groups

- Superweapons can be grouped together using `SW.CooldownGroup`. When any superweapon belonging to a group is launched, all other currently active (`IsPresent`) superweapons belonging to that group for the firer house will have their countdown timers reset (`RechargeTimer`), implementing a shared cooldown mechanism.
- `SW.CooldownGroup` accepts a comma-separated list of group names, allowing a superweapon to belong to multiple groups simultaneously.
- The reset is non-transitive: launching a superweapon only resets superweapons that share at least one group with the *launched* superweapon. Superweapons reset collaterally do not propagate resets to other groups.
- `SW.CooldownGroup.SyncLongest` controls whether all present superweapons in the affected group(s) synchronize their cooldown to the longest `RechargeTime` among the superweapons currently active (built) in the group for that player. If any active superweapon in the group has this set to `true`, the synchronized longest cooldown applies to the entire group. If all active superweapons in the group have this set to `false`, each superweapon resets to its own individual recharge time.
- If the AI has multiple superweapons in the same group ready to fire simultaneously, it automatically and fairly selects one candidate at random each frame rather than always defaulting to the first superweapons defined in `[SuperWeaponTypes]`.

In `rulesmd.ini`:
```ini
[SOMESW] ; SuperWeaponType
SW.CooldownGroup= ; List of group names (strings separated by commas)
SW.CooldownGroup.SyncLongest=false ; boolean
```

### Next

![image](_static/images/swnext.gif)
Expand All @@ -1471,6 +1486,17 @@ SW.Next.RollChances= ; List of percentages.
SW.Next.RandomWeightsN= ; List of integers.
```

### Randomize AI superweapon priority

- AI houses now can evaluate their ready superweapons in a randomized order each firing cycle rather than always checking them in fixed array order.
- `RandomizeSuperWeaponPriority` controls whether AI houses randomize the evaluation order of all currently ready superweapons each firing cycle. When set to `true`, the AI shuffles all ready superweapons (including individual superweapons and group representatives) using the synchronized engine PRNG, eliminating the bias where superweapons defined earlier in `[SuperWeaponTypes]` are always prioritized. If the first randomly chosen candidate has no valid tactical target, the AI gracefully falls back to evaluate the remaining ready candidates in shuffled order. Defaults to `false`.

In `rulesmd.ini`:
```ini
[AI]
RandomizeSuperWeaponPriority=false ; boolean
```

### Recipient-specific message and EVA on superweapon activation

- Superweapons can now display messages and play EVA voices for specific recipient groups when activated.
Expand Down
2 changes: 2 additions & 0 deletions docs/Whats-New.md
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,8 @@ 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)
- [Superweapon cooldown groups](New-or-Enhanced-Logics.md#cooldown-groups) (by FS-21)
- [Randomize AI superweapon priority](New-or-Enhanced-Logics.md#randomize-ai-superweapon-priority) (by FS-21)
- [Customize ivan bomb visibility](Fixed-or-Improved-Logics.md#customize-ivan-bomb-visibility) (by NetsuNegi)
- [Customize whether mind-controlled `Insignificant` technos can be auto-targeted](Fixed-or-Improved-Logics.md#customize-whether-mind-controlled-insignificant-technos-can-be-auto-targeted) (by Noble_Fish)
- [AutoDeath based on player power status and player credits](New-or-Enhanced-Logics.md#kill-object-automatically) (by Flactine)
Expand Down
1 change: 1 addition & 0 deletions src/Ext/House/Body.h
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ class HouseExt final : public AbstractExt, public Detach::Listener<BuildingClass
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 void AI_TryFireSW_CooldownGroupAware(HouseClass* pThis);

static void SetForceOnlyTargetHouseEnemy(HouseClass* pThis, int mode = -1);
static void SetSkirmishHouseName(HouseClass* pHouse);
Expand Down
214 changes: 213 additions & 1 deletion src/Ext/House/Hooks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <Ext/Aircraft/Body.h>
#include <Ext/Scenario/Body.h>
#include <Ext/SWType/Body.h>
#include "Ext/Techno/Body.h"
#include "Ext/Building/Body.h"
#include <Ext/Event/Body.h>
Expand Down Expand Up @@ -453,6 +454,208 @@ DEFINE_HOOK(0x50B669, HouseClass_ShouldDisableCameo_GreyCameo, 0x3)
return 0;
}

// Wrapper around Ares' HouseClass::AI_TryFireSW (0x5098F0).
// When multiple superweapons in the same SW.CooldownGroup are ready at once,
// the AI picks one candidate at random via the synchronized PRNG and temporarily masks
// the others so Ares doesn't always default to the first one in the vector.
void HouseExt::AI_TryFireSW_CooldownGroupAware(HouseClass* pThis)
{
if (!pThis)
return;

if (pThis->IsControlledByHuman())
{
pThis->AI_TryFireSW();
return;
}

std::vector<SuperClass*> allReadySupers;

for (int i = 0; i < pThis->Supers.Count; ++i)
{
SuperClass* pSuper = pThis->Supers.GetItemOrDefault(i);

if (!pSuper || !pSuper->IsPresent || !pSuper->IsReady || !pSuper->Type)
continue;

if (pSuper->ChargeDrainState == ChargeDrainState::Draining)
continue;

auto const pExt = SWTypeExt::Fetch(pSuper->Type);

if (pExt && pExt->SW_Shots > 0)
{
auto const pHouseExt = HouseExt::Fetch(pThis);

if (pHouseExt && pHouseExt->SuperExts[pSuper->Type->ArrayIndex].ShotCount >= pExt->SW_Shots)
continue;
}

allReadySupers.push_back(pSuper);
}

if (allReadySupers.empty())
return;

std::vector<bool> inGroupCluster(allReadySupers.size(), false);
std::vector<SuperClass*> candidates;

for (size_t i = 0; i < allReadySupers.size(); ++i)
{
if (inGroupCluster[i])
continue;

SWTypeExt* pExtI = SWTypeExt::Fetch(allReadySupers[i]->Type);

if (!pExtI || pExtI->SW_CooldownGroup.empty())
{
candidates.push_back(allReadySupers[i]);
continue;
}

std::vector<SuperClass*> cluster;
cluster.push_back(allReadySupers[i]);
inGroupCluster[i] = true;

for (size_t j = i + 1; j < allReadySupers.size(); ++j)
{
if (inGroupCluster[j])
continue;

SWTypeExt* pExtJ = SWTypeExt::Fetch(allReadySupers[j]->Type);

if (!pExtJ || pExtJ->SW_CooldownGroup.empty())
continue;

bool sharesGroup = false;

for (SuperClass* pMember : cluster)
{
SWTypeExt* pMemberExt = SWTypeExt::Fetch(pMember->Type);

for (const std::string& group1 : pMemberExt->SW_CooldownGroup)
{
for (const std::string& group2 : pExtJ->SW_CooldownGroup)
{
if (_stricmp(group1.c_str(), group2.c_str()) == 0)
{
sharesGroup = true;
break;
}
}

if (sharesGroup)
break;
}

if (sharesGroup)
break;
}

if (sharesGroup)
{
cluster.push_back(allReadySupers[j]);
inGroupCluster[j] = true;
}
}

if (cluster.size() > 1)
{
int chosenIndex = ScenarioClass::Instance->Random.RandomRanged(0, static_cast<int>(cluster.size()) - 1);

std::string groupNames;

for (const std::string& groupName : pExtI->SW_CooldownGroup)
{
if (!groupNames.empty())
groupNames += ", ";

groupNames += groupName;
}

Debug::Log("[SW.CooldownGroup] House '%s' has %d ready superweapons in group '%s'; randomly selected [%s].\n",
pThis->get_ID(), static_cast<int>(cluster.size()), groupNames.c_str(), cluster[chosenIndex]->Type->get_ID());

for (size_t k = 0; k < cluster.size(); ++k)
{
if (static_cast<int>(k) != chosenIndex)
cluster[k]->IsReady = false;
}

candidates.push_back(cluster[chosenIndex]);
}
else
{
candidates.push_back(allReadySupers[i]);
}
}

SuperClass* pFired = nullptr;
const bool randomizePriority = RulesExt::Global()->RandomizeSuperWeaponPriority.Get();

if (!randomizePriority || candidates.size() <= 1)
{
pThis->AI_TryFireSW();

for (SuperClass* pCand : candidates)
{
if (!pCand->IsReady || pCand->RechargeTimer.TimeLeft > 0)
{
pFired = pCand;
break;
}
}
}
else
{
// Fisher-Yates shuffle of candidates using synchronized engine PRNG
for (int i = static_cast<int>(candidates.size()) - 1; i > 0; --i)
{
int j = ScenarioClass::Instance->Random.RandomRanged(0, i);
std::swap(candidates[i], candidates[j]);
}

for (size_t i = 0; i < candidates.size(); ++i)
{
SuperClass* pCandidate = candidates[i];

for (size_t j = 0; j < candidates.size(); ++j)
{
if (j != i)
candidates[j]->IsReady = false;
}

pCandidate->IsReady = true;

pThis->AI_TryFireSW();

if (!pCandidate->IsReady || pCandidate->RechargeTimer.TimeLeft > 0)
{
pFired = pCandidate;
break;
}
}
}

auto const pHouseExt = HouseExt::Fetch(pThis);

for (SuperClass* pSuper : allReadySupers)
{
if (pSuper != pFired && pSuper->IsPresent && pSuper->RechargeTimer.TimeLeft == 0)
{
auto const pExt = SWTypeExt::Fetch(pSuper->Type);

if (pExt && pExt->SW_Shots > 0 && pHouseExt)
{
if (pHouseExt->SuperExts[pSuper->Type->ArrayIndex].ShotCount >= pExt->SW_Shots)
continue;
}

pSuper->IsReady = true;
}
}
}

DEFINE_HOOK(0x4FD77C, HouseClass_ExpertAI_Superweapons, 0x5)
{
enum { SkipSWProcess = 0x4FD7A0 };
Expand All @@ -463,6 +666,15 @@ DEFINE_HOOK(0x4FD77C, HouseClass_ExpertAI_Superweapons, 0x5)
return 0;
}

DEFINE_HOOK(0x4FD799, HouseClass_ExpertAI_TryFireSW, 0x7)
{
GET(HouseClass*, pThis, EBX);

HouseExt::AI_TryFireSW_CooldownGroupAware(pThis);

return 0x4FD7A0;
}

DEFINE_HOOK(0x4F9038, HouseClass_AI_Superweapons, 0x5)
{
GET(HouseClass*, pThis, ESI);
Expand All @@ -483,7 +695,7 @@ DEFINE_HOOK(0x4F9038, HouseClass_AI_Superweapons, 0x5)
}

if (!SessionClass::IsCampaign() || pThis->IQLevel2 >= RulesClass::Instance->SuperWeapons)
pThis->AI_TryFireSW();
HouseExt::AI_TryFireSW_CooldownGroupAware(pThis);

return 0;
}
Expand Down
2 changes: 2 additions & 0 deletions src/Ext/Rules/Body.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI)
this->AIForbidConYard.Read(exINI, GameStrings::AI, "AIForbidConYard");
this->AINodeWallsOnly.Read(exINI, GameStrings::AI, "AINodeWallsOnly");
this->AICleanWallNode.Read(exINI, GameStrings::AI, "AICleanWallNode");
this->RandomizeSuperWeaponPriority.Read(exINI, GameStrings::AI, "RandomizeSuperWeaponPriority");

this->AttackMove_Aggressive.Read(exINI, GameStrings::General, "AttackMove.Aggressive");
this->AttackMove_UpdateTarget.Read(exINI, GameStrings::General, "AttackMove.UpdateTarget");
Expand Down Expand Up @@ -935,6 +936,7 @@ void RulesExt::ExtData::Serialize(T& Stm)
.Process(this->AIForbidConYard)
.Process(this->AINodeWallsOnly)
.Process(this->AICleanWallNode)
.Process(this->RandomizeSuperWeaponPriority)
.Process(this->AttackMove_Aggressive)
.Process(this->AttackMove_UpdateTarget)
.Process(this->MindControl_ThreatDelay)
Expand Down
2 changes: 2 additions & 0 deletions src/Ext/Rules/Body.h
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ class RulesExt
Valueable<bool> AIForbidConYard;
Valueable<bool> AINodeWallsOnly;
Valueable<bool> AICleanWallNode;
Valueable<bool> RandomizeSuperWeaponPriority;

Valueable<bool> AttackMove_Aggressive;
Valueable<bool> AttackMove_UpdateTarget;
Expand Down Expand Up @@ -789,6 +790,7 @@ class RulesExt
, AIForbidConYard { false }
, AINodeWallsOnly { false }
, AICleanWallNode { false }
, RandomizeSuperWeaponPriority { false }
, AttackMove_Aggressive { false }
, AttackMove_UpdateTarget { false }
, MindControl_ThreatDelay { 0 }
Expand Down
5 changes: 5 additions & 0 deletions src/Ext/SWType/Body.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ void SWTypeExt::Serialize(T& Stm)
.Process(this->EVA_Activated_Owner)
.Process(this->EVA_Activated_Allies)
.Process(this->EVA_Activated_Enemies)
.Process(this->SW_CooldownGroup)
.Process(this->SW_CooldownGroup_SyncLongest)
;
}

Expand Down Expand Up @@ -242,6 +244,9 @@ void SWTypeExt::LoadFromINIFile(CCINIClass* const pINI)
this->EVA_LinkedSWAcquired.Read(exINI, pSection, "EVA.LinkedSWAcquired");
this->SW_Link_RollChances.Read(exINI, pSection, "SW.Link.RollChances");

exINI.ParseStringList(this->SW_CooldownGroup, pSection, "SW.CooldownGroup");
this->SW_CooldownGroup_SyncLongest.Read(exINI, pSection, "SW.CooldownGroup.SyncLongest");

this->Message_Activated_Owner.Read(exINI, pSection, "Message.Activated.Owner");
this->Message_Activated_Allies.Read(exINI, pSection, "Message.Activated.Allies");
this->Message_Activated_Enemies.Read(exINI, pSection, "Message.Activated.Enemies");
Expand Down
7 changes: 7 additions & 0 deletions src/Ext/SWType/Body.h
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ class SWTypeExt final : public AbstractTypeExt
ValueableIdx<VoxClass> EVA_Activated_Allies;
ValueableIdx<VoxClass> EVA_Activated_Enemies;

std::vector<std::string> SW_CooldownGroup;
Valueable<bool> SW_CooldownGroup_SyncLongest;

SWTypeExt(SuperWeaponTypeClass* OwnerObject) : AbstractTypeExt(OwnerObject)
, TypeID { "" }
, Money_Amount { 0 }
Expand Down Expand Up @@ -213,6 +216,8 @@ class SWTypeExt final : public AbstractTypeExt
, EVA_Activated_Owner { -1 }
, EVA_Activated_Allies { -1 }
, EVA_Activated_Enemies { -1 }
, SW_CooldownGroup { }
, SW_CooldownGroup_SyncLongest { false }
{ }

// Ares 0.A functions
Expand All @@ -239,6 +244,8 @@ class SWTypeExt final : public AbstractTypeExt

void ApplyLinkedSW(SuperClass* pSW);

void ApplyCooldownGroupReset(SuperClass* pSW);

void ApplyActivatedMessage(SuperClass* pSW) const;
void ApplyActivatedEva(SuperClass* pSW) const;

Expand Down
Loading
Loading