241 lines
7.6 KiB
C#
241 lines
7.6 KiB
C#
#region Copyright & License Information
|
|
/*
|
|
* Copyright (c) The OpenRA Developers and Contributors
|
|
* This file is part of OpenRA, which is free software. It is made
|
|
* available to you under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation, either version 3 of
|
|
* the License, or (at your option) any later version. For more
|
|
* information, see COPYING.
|
|
*/
|
|
#endregion
|
|
|
|
using System.Collections.Generic;
|
|
using System.Collections.Immutable;
|
|
using System.Linq;
|
|
using OpenRA.Traits;
|
|
|
|
namespace OpenRA.Mods.Common.Traits
|
|
{
|
|
[Desc("Controls AI unit production.")]
|
|
public class UnitBuilderBotModuleInfo : ConditionalTraitInfo
|
|
{
|
|
// TODO: Investigate whether this might the (or at least one) reason why bots occasionally get into a state of doing nothing.
|
|
// Reason: If this is less than SquadSize, the bot might get stuck between not producing more units due to this,
|
|
// but also not creating squads since there aren't enough idle units.
|
|
[Desc("Only produce units as long as there are less than this amount of units idling inside the base.")]
|
|
public readonly int IdleBaseUnitsMaximum = 12;
|
|
|
|
[Desc("Production queues AI uses for producing units.")]
|
|
public readonly HashSet<string> UnitQueues = new() { "Vehicle", "Infantry", "Plane", "Ship", "Aircraft" };
|
|
|
|
[Desc("What units to the AI should build.", "What relative share of the total army must be this type of unit.")]
|
|
public readonly Dictionary<string, int> UnitsToBuild = null;
|
|
|
|
[Desc("What units should the AI have a maximum limit to train.")]
|
|
public readonly Dictionary<string, int> UnitLimits = null;
|
|
|
|
[Desc("When should the AI start train specific units.")]
|
|
public readonly Dictionary<string, int> UnitDelays = null;
|
|
|
|
public override object Create(ActorInitializer init) { return new UnitBuilderBotModule(init.Self, this); }
|
|
}
|
|
|
|
public class UnitBuilderBotModule : ConditionalTrait<UnitBuilderBotModuleInfo>, IBotTick, IBotNotifyIdleBaseUnits, IBotRequestUnitProduction, IGameSaveTraitData
|
|
{
|
|
public const int FeedbackTime = 30; // ticks; = a bit over 1s. must be >= netlag.
|
|
|
|
readonly World world;
|
|
readonly Player player;
|
|
|
|
readonly List<string> queuedBuildRequests = new();
|
|
|
|
IBotRequestPauseUnitProduction[] requestPause;
|
|
int idleUnitCount;
|
|
|
|
int ticks;
|
|
|
|
public UnitBuilderBotModule(Actor self, UnitBuilderBotModuleInfo info)
|
|
: base(info)
|
|
{
|
|
world = self.World;
|
|
player = self.Owner;
|
|
}
|
|
|
|
protected override void Created(Actor self)
|
|
{
|
|
requestPause = self.Owner.PlayerActor.TraitsImplementing<IBotRequestPauseUnitProduction>().ToArray();
|
|
}
|
|
|
|
void IBotNotifyIdleBaseUnits.UpdatedIdleBaseUnits(List<Actor> idleUnits)
|
|
{
|
|
idleUnitCount = idleUnits.Count;
|
|
}
|
|
|
|
void IBotTick.BotTick(IBot bot)
|
|
{
|
|
if (requestPause.Any(rp => rp.PauseUnitProduction))
|
|
return;
|
|
|
|
ticks++;
|
|
|
|
if (ticks % FeedbackTime == 0)
|
|
{
|
|
var buildRequest = queuedBuildRequests.FirstOrDefault();
|
|
if (buildRequest != null)
|
|
{
|
|
BuildUnit(bot, buildRequest);
|
|
queuedBuildRequests.Remove(buildRequest);
|
|
}
|
|
|
|
foreach (var q in Info.UnitQueues)
|
|
BuildUnit(bot, q, idleUnitCount < Info.IdleBaseUnitsMaximum);
|
|
}
|
|
}
|
|
|
|
void IBotRequestUnitProduction.RequestUnitProduction(IBot bot, string requestedActor)
|
|
{
|
|
queuedBuildRequests.Add(requestedActor);
|
|
}
|
|
|
|
int IBotRequestUnitProduction.RequestedProductionCount(IBot bot, string requestedActor)
|
|
{
|
|
return queuedBuildRequests.Count(r => r == requestedActor);
|
|
}
|
|
|
|
void BuildUnit(IBot bot, string category, bool buildRandom)
|
|
{
|
|
// Pick a free queue
|
|
var queue = AIUtils.FindQueues(player, category).FirstOrDefault(q => !q.AllQueued().Any());
|
|
if (queue == null)
|
|
return;
|
|
|
|
var unit = buildRandom ?
|
|
ChooseRandomUnitToBuild(queue) :
|
|
ChooseUnitToBuild(queue);
|
|
|
|
if (unit == null)
|
|
return;
|
|
|
|
var name = unit.Name;
|
|
|
|
if (Info.UnitsToBuild != null && !Info.UnitsToBuild.ContainsKey(name))
|
|
return;
|
|
|
|
if (Info.UnitDelays != null &&
|
|
Info.UnitDelays.TryGetValue(name, out var delay) &&
|
|
delay > world.WorldTick)
|
|
return;
|
|
|
|
if (Info.UnitLimits != null &&
|
|
Info.UnitLimits.TryGetValue(name, out var limit) &&
|
|
world.Actors.Count(a => a.Owner == player && a.Info.Name == name) >= limit)
|
|
return;
|
|
|
|
bot.QueueOrder(Order.StartProduction(queue.Actor, name, 1));
|
|
}
|
|
|
|
// In cases where we want to build a specific unit but don't know the queue name (because there's more than one possibility)
|
|
void BuildUnit(IBot bot, string name)
|
|
{
|
|
var actorInfo = world.Map.Rules.Actors[name];
|
|
if (actorInfo == null)
|
|
return;
|
|
|
|
var buildableInfo = actorInfo.TraitInfoOrDefault<BuildableInfo>();
|
|
if (buildableInfo == null)
|
|
return;
|
|
|
|
ProductionQueue queue = null;
|
|
foreach (var pq in buildableInfo.Queue)
|
|
{
|
|
queue = AIUtils.FindQueues(player, pq).FirstOrDefault(q => !q.AllQueued().Any());
|
|
if (queue != null)
|
|
break;
|
|
}
|
|
|
|
if (queue != null)
|
|
{
|
|
bot.QueueOrder(Order.StartProduction(queue.Actor, name, 1));
|
|
AIUtils.BotDebug("{0} decided to build {1} (external request)", queue.Actor.Owner, name);
|
|
}
|
|
}
|
|
|
|
ActorInfo ChooseRandomUnitToBuild(ProductionQueue queue)
|
|
{
|
|
var buildableThings = queue.BuildableItems();
|
|
var unit = buildableThings.RandomOrDefault(world.LocalRandom);
|
|
return unit != null && HasAdequateAirUnitReloadBuildings(unit) ? unit : null;
|
|
}
|
|
|
|
ActorInfo ChooseUnitToBuild(ProductionQueue queue)
|
|
{
|
|
var buildableThings = queue.BuildableItems().Select(b => b.Name).ToHashSet();
|
|
if (buildableThings.Count == 0)
|
|
return null;
|
|
|
|
var myUnits = player.World
|
|
.ActorsHavingTrait<IPositionable>()
|
|
.Where(a => a.Owner == player)
|
|
.Select(a => a.Info.Name)
|
|
.ToList();
|
|
|
|
foreach (var unit in Info.UnitsToBuild.Shuffle(world.LocalRandom))
|
|
if (buildableThings.Contains(unit.Key))
|
|
if (myUnits.Count(a => a == unit.Key) * 100 < unit.Value * myUnits.Count)
|
|
if (HasAdequateAirUnitReloadBuildings(world.Map.Rules.Actors[unit.Key]))
|
|
return world.Map.Rules.Actors[unit.Key];
|
|
|
|
return null;
|
|
}
|
|
|
|
// For mods like RA (number of RearmActors must match the number of aircraft)
|
|
bool HasAdequateAirUnitReloadBuildings(ActorInfo actorInfo)
|
|
{
|
|
var aircraftInfo = actorInfo.TraitInfoOrDefault<AircraftInfo>();
|
|
if (aircraftInfo == null)
|
|
return true;
|
|
|
|
// If actor isn't Rearmable, it doesn't need a RearmActor to reload
|
|
var rearmableInfo = actorInfo.TraitInfoOrDefault<RearmableInfo>();
|
|
if (rearmableInfo == null)
|
|
return true;
|
|
|
|
var countOwnAir = AIUtils.CountActorsWithTrait<IPositionable>(actorInfo.Name, player);
|
|
var countBuildings = rearmableInfo.RearmActors.Sum(b => AIUtils.CountActorsWithTrait<Building>(b, player));
|
|
if (countOwnAir >= countBuildings)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
List<MiniYamlNode> IGameSaveTraitData.IssueTraitData(Actor self)
|
|
{
|
|
if (IsTraitDisabled)
|
|
return null;
|
|
|
|
return new List<MiniYamlNode>()
|
|
{
|
|
new MiniYamlNode("QueuedBuildRequests", FieldSaver.FormatValue(queuedBuildRequests.ToArray())),
|
|
new MiniYamlNode("IdleUnitCount", FieldSaver.FormatValue(idleUnitCount))
|
|
};
|
|
}
|
|
|
|
void IGameSaveTraitData.ResolveTraitData(Actor self, ImmutableArray<MiniYamlNode> data)
|
|
{
|
|
if (self.World.IsReplay)
|
|
return;
|
|
|
|
var queuedBuildRequestsNode = data.FirstOrDefault(n => n.Key == "QueuedBuildRequests");
|
|
if (queuedBuildRequestsNode != null)
|
|
{
|
|
queuedBuildRequests.Clear();
|
|
queuedBuildRequests.AddRange(FieldLoader.GetValue<string[]>("QueuedBuildRequests", queuedBuildRequestsNode.Value.Value));
|
|
}
|
|
|
|
var idleUnitCountNode = data.FirstOrDefault(n => n.Key == "IdleUnitCount");
|
|
if (idleUnitCountNode != null)
|
|
idleUnitCount = FieldLoader.GetValue<int>("IdleUnitCount", idleUnitCountNode.Value.Value);
|
|
}
|
|
}
|
|
}
|