Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f3c24e5
Frozen Rift map fix.
darkademic Dec 25, 2025
febf152
Captured Nerve Centers in Banishment count towards objective.
darkademic Dec 25, 2025
a17b8d5
- Encyclopedia update.
darkademic Dec 24, 2025
b290df2
Speculative AI Nuke Cannon behaviour fix.
darkademic Dec 26, 2025
30b2f29
Encyclopedia effects fix.
darkademic Dec 27, 2025
36d1578
Treachery co-op fix.
darkademic Dec 27, 2025
a1821ba
- Tiger Guard will fire at buildings under fog without moving within …
darkademic Dec 28, 2025
c5f9ead
Reckoning co-op improvements.
darkademic Dec 28, 2025
67755cc
Banishment queue update.
darkademic Dec 30, 2025
9017a6c
Enable tech on Reckoning Co-op.
darkademic Dec 31, 2025
536b756
Add guards to Exterminators on Very Hard/Brutal.
darkademic Dec 31, 2025
bd4123d
Blind affects Exterminator (same as Void Engine).
darkademic Dec 31, 2025
965bbbb
Fix Juncture Co-op.
darkademic Jan 1, 2026
69bfedd
Fix AI aircraft limits.
darkademic Jan 1, 2026
a604229
Multipolarity fixes.
darkademic Jan 1, 2026
2f748b9
feat: add support powers and ifv variants to encyclopedia
schultzp2020 Dec 31, 2025
4e71446
feat: add dropdown for entity variants
schultzp2020 Jan 1, 2026
7f446c6
Encyclopedia corrections.
darkademic Jan 1, 2026
528291a
Multipolarity fixes.
darkademic Jan 1, 2026
2eaf4f6
Encyclopedia updates.
darkademic Jan 1, 2026
9e91d30
Campaign prerequisites correction.
darkademic Jan 2, 2026
24220a7
Campaign progress update.
darkademic Jan 2, 2026
23f4e61
Incapacitation respawn fix.
darkademic Jan 2, 2026
025b114
Fix for in-game encyclopedia preview position.
darkademic Jan 3, 2026
a0e9ebd
Speculative desync fix.
darkademic Jan 3, 2026
59cf917
Encyclopedia previews fix and power reordering.
darkademic Jan 3, 2026
d6b3b63
Emancipation & Duality fixes.
darkademic Jan 5, 2026
df16cf9
feat(i18n): add fluent translation support for prologue missions
schultzp2020 Jan 5, 2026
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
10 changes: 10 additions & 0 deletions OpenRA.Mods.CA/AIUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ public static ILookup<string, ProductionQueue> FindQueuesByCategory(Player playe
.ToLookup(pq => pq.Info.Type);
}

public static ILookup<string, ProductionQueue> FindQueuesByCategory(IEnumerable<Player> players)
{
var player = players.First();

return player.World.ActorsWithTrait<ProductionQueue>()
.Where(a => players.Contains(a.Actor.Owner) && a.Trait.Enabled)
.Select(a => a.Trait)
.ToLookup(pq => pq.Info.Type);
}

public static IEnumerable<Actor> GetActorsWithTrait<T>(World world)
{
return world.ActorsHavingTrait<T>();
Expand Down
38 changes: 20 additions & 18 deletions OpenRA.Mods.CA/Traits/BotModules/Squads/States/AirStatesCA.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/
#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using OpenRA.Mods.Common;
Expand Down Expand Up @@ -229,23 +230,6 @@ public void Tick(SquadCA owner)
return;
}

var firstUnit = owner.Units.FirstOrDefault();
var buildableInfo = firstUnit.Info.TraitInfoOrDefault<BuildableInfo>();
var limitOne = buildableInfo != null && buildableInfo.BuildLimit == 1;
var canBuildMoreOfAircraft = firstUnit != null && !limitOne && owner.SquadManager.CanBuildMoreOfAircraft(firstUnit.Info);
var waitingCount = owner.WaitingUnits.Count;

var waitingPatience = 99;
if (waitingCount > 7)
waitingPatience = 65;
else if (waitingCount > 3)
waitingPatience = 85;
else if (waitingCount > 1)
waitingPatience = 95;

var impatience = owner.World.LocalRandom.Next(100);
var noPatience = impatience > waitingPatience;

foreach (var a in owner.Units)
{
var currentActivity = a.CurrentActivity;
Expand Down Expand Up @@ -300,7 +284,25 @@ public void Tick(SquadCA owner)
owner.WaitingUnits.Add(a);
}

if ((!canBuildMoreOfAircraft || noPatience) && owner.WaitingUnits.Count > 0 && owner.WaitingUnits.Count >= owner.RearmingUnits.Count)
var firstUnit = owner.Units.FirstOrDefault();
var buildableInfo = firstUnit.Info.TraitInfoOrDefault<BuildableInfo>();
var limitOne = buildableInfo != null && buildableInfo.BuildLimit == 1;

var waitingCount = owner.WaitingUnits.Count;

var waitingPatience = 99;
if (waitingCount > 7)
waitingPatience = 65;
else if (waitingCount > 3)
waitingPatience = 85;
else if (waitingCount > 1)
waitingPatience = 95;

var impatience = owner.World.LocalRandom.Next(100);
var noPatience = impatience > waitingPatience;
Func<bool> canBuildMoreOfAircraft = () => firstUnit != null && !limitOne && owner.SquadManager.CanBuildMoreOfAircraft(firstUnit.Info);

if (owner.WaitingUnits.Count > 0 && owner.WaitingUnits.Count >= owner.RearmingUnits.Count && (noPatience || !canBuildMoreOfAircraft()))
{
foreach (var a in owner.WaitingUnits)
if (CanAttackTarget(a, owner.TargetActor))
Expand Down
66 changes: 53 additions & 13 deletions OpenRA.Mods.CA/Traits/BotModules/UnitBuilderBotModuleCA.cs
Original file line number Diff line number Diff line change
Expand Up @@ -302,28 +302,68 @@ bool CanBuildMoreOfAircraft(ActorInfo actorInfo)
if (attackAircraftInfo == null)
return true;

int currentCount;
var limit = Info.MaxAircraft;
var currentCount = 0;
var isAirToAir = Info.AirToAirUnits.Contains(actorInfo.Name);

if (Info.MaintainAirSuperiority)
if (Info.MaintainAirSuperiority && isAirToAir)
{
var numAirToAirUnits = AIUtils.GetActorsWithTrait<Aircraft>(player.World).Count(a => a.Owner == player && Info.AirToAirUnits.Contains(a.Info.Name));
// Get all production queues to count queued aircraft (for allies as well)
var queues = AIUtils.FindQueuesByCategory(player.World.Players.Where(p => p.IsAlliedWith(player)));

if (Info.AirToAirUnits.Contains(actorInfo.Name))
// Count queued air-to-air units across all queues
var queuedAirToAirCount = queues.SelectMany(g => g).SelectMany(q => q.AllQueued())
.Count(item => Info.AirToAirUnits.Contains(item.Item));

var friendlyAirToAirCount = player.World.Actors.Count(a => a.Owner.RelationshipWith(player) == PlayerRelationship.Ally
&& Info.AirToAirUnits.Contains(a.Info.Name));

var enemyAirThreatCount = player.World.Actors.Count(a => a.Owner.RelationshipWith(player) == PlayerRelationship.Enemy
&& Info.AirThreatUnits.Contains(a.Info.Name));

currentCount = friendlyAirToAirCount + queuedAirToAirCount;
limit = Math.Max(enemyAirThreatCount + 1, limit);

if (Info.MaxAirSuperiority > 0)
limit = Math.Min(Info.MaxAirSuperiority, limit);
}
else
{
// Get all production queues to count queued aircraft
var queues = AIUtils.FindQueuesByCategory(player);

// Non air-to-air aircraft uses the standard aircraft limit
if (Info.MaintainAirSuperiority)
{
currentCount = numAirToAirUnits;
var numFriendlyAirToAirUnits = player.World.Actors.Count(a => a.Owner.RelationshipWith(player) == PlayerRelationship.Ally && Info.AirToAirUnits.Contains(a.Info.Name));
var numEnemyAirThreatUnits = player.World.Actors.Count(a => a.Owner.RelationshipWith(player) == PlayerRelationship.Enemy && Info.AirThreatUnits.Contains(a.Info.Name));
limit = Math.Max(numEnemyAirThreatUnits - numFriendlyAirToAirUnits + 1, limit);
var existingNonAirToAirCount = player.World.Actors.Count(a =>
a.Owner == player &&
a.Info.HasTraitInfo<AircraftInfo>() &&
a.Info.HasTraitInfo<BuildableInfo>() &&
!Info.AirToAirUnits.Contains(a.Info.Name));

if (Info.MaxAirSuperiority > 0)
limit = Math.Min(Info.MaxAirSuperiority, limit);
var queuedNonAirToAirCount = queues.SelectMany(g => g).SelectMany(q => q.AllQueued())
.Count(item => !Info.AirToAirUnits.Contains(item.Item) &&
world.Map.Rules.Actors[item.Item].HasTraitInfo<AircraftInfo>());

currentCount = existingNonAirToAirCount + queuedNonAirToAirCount;
}
else
currentCount = AIUtils.GetActorsWithTrait<Aircraft>(player.World).Count(a => a.Owner == player && a.Info.HasTraitInfo<BuildableInfo>()) - numAirToAirUnits;
{
var existingAircraftCount = player.World.Actors.Count(a =>
a.Owner == player &&
a.Info.HasTraitInfo<AircraftInfo>() &&
a.Info.HasTraitInfo<BuildableInfo>());

// Count all queued aircraft
var queuedAircraft = queues.SelectMany(g => g).SelectMany(q => q.AllQueued())
.Count(item => world.Map.Rules.Actors[item.Item].HasTraitInfo<AircraftInfo>());

currentCount = existingAircraftCount + queuedAircraft;
}
}
else
currentCount = AIUtils.GetActorsWithTrait<Aircraft>(player.World).Count(a => a.Owner == player && a.Info.HasTraitInfo<BuildableInfo>());

// bot debug
TextNotificationsManager.Debug("AI: {0} aircraft count for {1}: {2}/{3}", player, actorInfo.Name, currentCount, limit);

return currentCount < limit;
}
Expand Down
10 changes: 5 additions & 5 deletions OpenRA.Mods.CA/Traits/DelayedWeaponAttachable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public class DelayedWeaponAttachableInfo : ConditionalTraitInfo

public class DelayedWeaponAttachable : ConditionalTrait<DelayedWeaponAttachableInfo>, ITick, INotifyKilled, ISelectionBar, INotifyTransform, INotifyRemovedFromWorld
{
public HashSet<DelayedWeaponTrigger> Container { get; private set; }
public List<DelayedWeaponTrigger> Container { get; private set; }

readonly Actor self;
readonly HashSet<Actor> detectors = new HashSet<Actor>();
Expand All @@ -52,7 +52,7 @@ public DelayedWeaponAttachable(Actor self, DelayedWeaponAttachableInfo info)
: base(info)
{
this.self = self;
Container = new HashSet<DelayedWeaponTrigger>();
Container = new List<DelayedWeaponTrigger>();
}

void ITick.Tick(Actor self)
Expand All @@ -62,7 +62,7 @@ void ITick.Tick(Actor self)
foreach (var trigger in Container)
trigger.Tick(self);

Container.RemoveWhere(p => !p.IsValid);
Container.RemoveAll(p => !p.IsValid);

while (tokens.Count > Container.Count)
self.RevokeCondition(tokens.Pop());
Expand All @@ -81,7 +81,7 @@ void INotifyKilled.Killed(Actor self, AttackInfo e)
trigger.Activate(self);
}

Container.RemoveWhere(p => !p.IsValid);
Container.RemoveAll(p => !p.IsValid);
}
}

Expand Down Expand Up @@ -150,7 +150,7 @@ void INotifyTransform.BeforeTransform(Actor self)
foreach (var trigger in Container)
trigger.Activate(self);

Container.RemoveWhere(p => !p.IsValid);
Container.RemoveAll(p => !p.IsValid);

while (tokens.Count > 0 && Container.Count == 0)
self.RevokeCondition(tokens.Pop());
Expand Down
10 changes: 10 additions & 0 deletions OpenRA.Mods.CA/Traits/EncyclopediaExtras.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ namespace OpenRA.Mods.CA.Traits
[Desc("To override encyclopedia preview.")]
public class EncyclopediaExtrasInfo : TraitInfo
{
[FluentReference]
[Desc("If set, will be used instead of the tooltip name in encyclopedia.")]
public readonly string Name = null;

[Desc("If set will override the preview with this actor.")]
public readonly string RenderPreviewActor;

Expand All @@ -31,6 +35,12 @@ public class EncyclopediaExtrasInfo : TraitInfo
[Desc("If no Buildable Description exists, this will be shown instead.")]
public readonly string Description = "";

[Desc("Actor name this entry is a variant of (e.g., 'IFV'). Hides entry from main list.")]
public readonly string VariantOf = null;

[Desc("Group name for variant dropdown (e.g., 'Allies Infantry').")]
public readonly string VariantGroup = null;

public override object Create(ActorInitializer init) { return new EncyclopediaExtras(init, this); }
}

Expand Down
18 changes: 9 additions & 9 deletions OpenRA.Mods.CA/Traits/Modifiers/WithColoredOverlayCA.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#endregion

using System.Collections.Generic;
using System.Linq;
using OpenRA.Graphics;
using OpenRA.Mods.Common.Traits;
using OpenRA.Primitives;
Expand Down Expand Up @@ -94,19 +95,18 @@ public WithColoredOverlayPreviewModifier(WithColoredOverlayCAInfo info)

public IEnumerable<IRenderable> ModifyPreviewRender(WorldRenderer wr, IEnumerable<IRenderable> renderables, Rectangle bounds)
{
var result = new List<IRenderable>();

foreach (var r in renderables)
foreach (var r in renderables.ToList())
{
result.Add(r);
yield return r;

// For preview rendering, we apply tint to all modifyable renderables
// (unlike in-game where we skip decorations)
if (r is IModifyableRenderable mr)
result.Add(mr.WithTint(tint, mr.TintModifiers | TintModifiers.ReplaceColor).WithAlpha(alpha));
{
var currentTintModifiers = mr.TintModifiers;
yield return mr.WithTint(tint, currentTintModifiers | TintModifiers.ReplaceColor)
.WithAlpha(alpha)
.AsDecoration();
}
}

return result;
}

public void Tick() { }
Expand Down
2 changes: 1 addition & 1 deletion OpenRA.Mods.CA/Traits/Modifiers/WithPalettedOverlay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ public IEnumerable<IRenderable> ModifyPreviewRender(WorldRenderer wr, IEnumerabl
yield return r;

// For preview rendering, apply palette swap to all paletted renderables
// (unlike in-game where we skip decorations)
// (unlike in-game where we skip decorations, because the colored overlay used for customising the actor color is a decoration)
if (palette != null && r is IPalettedRenderable pr)
yield return pr.WithPalette(palette)
.WithZOffset(r.ZOffset + 1)
Expand Down
15 changes: 11 additions & 4 deletions OpenRA.Mods.CA/Traits/Player/CampaignProgressTracker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ void INotifyWinStateChanged.OnPlayerWon(Player player)
if (!player.World.Map.Categories.Contains("Campaign"))
return;

var missionTitle = GetMapTileWithoutNumber(player.World.Map.Title);
var missionTitle = GetMapTitleWithoutNumber(player.World.Map.Title);
var worldActor = player.World.WorldActor;
var difficulty = worldActor.TraitsImplementing<ScriptLobbyDropdown>()
.FirstOrDefault(sld => sld.Info.ID == "difficulty");
Expand Down Expand Up @@ -124,16 +124,23 @@ void IResolveOrder.ResolveOrder(Actor self, Order order)
developerCommandUsed = true;
}

public static string GetMapTileWithoutNumber(string mapTitle)
public static string GetMapTitleWithoutNumber(string mapTitle)
{
var firstColonIndex = mapTitle.IndexOf(": ");
var firstPeriodIndex = mapTitle.IndexOf(". ");
var splitIndex = firstColonIndex >= 0 ? firstColonIndex : firstPeriodIndex;

string result;
if (splitIndex >= 0)
return mapTitle.Substring(splitIndex + 2);
result = mapTitle.Substring(splitIndex + 2);
else
result = mapTitle;

return mapTitle;
var bracketIndex = result.IndexOf('(');
if (bracketIndex >= 0)
result = result.Substring(0, bracketIndex);

return result.TrimEnd();
}

public static Dictionary<string, MissionVictoryResult> GetCampaignProgress()
Expand Down
1 change: 0 additions & 1 deletion OpenRA.Mods.CA/Traits/Render/WithPreviewDecoration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
using OpenRA.Graphics;
using OpenRA.Mods.Common.Traits.Render;
using OpenRA.Primitives;
using OpenRA.Traits;

namespace OpenRA.Mods.CA.Traits.Render
{
Expand Down
Loading