Files
OpenRA/OpenRA.Mods.Common/Traits/BotModules/UnitBuilderBotModule.cs
RoosterDragon 949ba589c0 MiniYaml becomes an immutable data structure.
This changeset is motivated by a simple concept - get rid of the MiniYaml.Clone and MiniYamlNode.Clone methods to avoid deep copying yaml trees during merging. MiniYaml becoming immutable allows the merge function to reuse existing yaml trees rather than cloning them, saving on memory and improving merge performance. On initial loading the YAML for all maps is processed, so this provides a small reduction in initial loading time.

The rest of the changeset is dealing with the change in the exposed API surface. Some With* helper methods are introduced to allow creating new YAML from existing YAML. Areas of code that generated small amounts of YAML are able to transition directly to the immutable model without too much ceremony. Some use cases are far less ergonomic even with these helper methods and so a MiniYamlBuilder is introduced to retain mutable creation functionality. This allows those areas to continue to use the old mutable structures. The main users are the update rules and linting capabilities.
2023-08-07 21:57:10 +03:00

243 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();
if (!buildableThings.Any())
return null;
var unit = buildableThings.Random(world.LocalRandom);
return HasAdequateAirUnitReloadBuildings(unit) ? unit : null;
}
ActorInfo ChooseUnitToBuild(ProductionQueue queue)
{
var buildableThings = queue.BuildableItems();
if (!buildableThings.Any())
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.Any(b => b.Name == 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);
}
}
}