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.
294 lines
11 KiB
C#
294 lines
11 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("Manages AI base construction.")]
|
|
public class BaseBuilderBotModuleInfo : ConditionalTraitInfo
|
|
{
|
|
[Desc("Tells the AI what building types are considered construction yards.")]
|
|
public readonly HashSet<string> ConstructionYardTypes = new();
|
|
|
|
[Desc("Tells the AI what building types are considered vehicle production facilities.")]
|
|
public readonly HashSet<string> VehiclesFactoryTypes = new();
|
|
|
|
[Desc("Tells the AI what building types are considered refineries.")]
|
|
public readonly HashSet<string> RefineryTypes = new();
|
|
|
|
[Desc("Tells the AI what building types are considered power plants.")]
|
|
public readonly HashSet<string> PowerTypes = new();
|
|
|
|
[Desc("Tells the AI what building types are considered infantry production facilities.")]
|
|
public readonly HashSet<string> BarracksTypes = new();
|
|
|
|
[Desc("Tells the AI what building types are considered production facilities.")]
|
|
public readonly HashSet<string> ProductionTypes = new();
|
|
|
|
[Desc("Tells the AI what building types are considered naval production facilities.")]
|
|
public readonly HashSet<string> NavalProductionTypes = new();
|
|
|
|
[Desc("Tells the AI what building types are considered silos (resource storage).")]
|
|
public readonly HashSet<string> SiloTypes = new();
|
|
|
|
[Desc("Tells the AI what building types are considered defenses.")]
|
|
public readonly HashSet<string> DefenseTypes = new();
|
|
|
|
[Desc("Production queues AI uses for buildings.")]
|
|
public readonly HashSet<string> BuildingQueues = new() { "Building" };
|
|
|
|
[Desc("Production queues AI uses for defenses.")]
|
|
public readonly HashSet<string> DefenseQueues = new() { "Defense" };
|
|
|
|
[Desc("Minimum distance in cells from center of the base when checking for building placement.")]
|
|
public readonly int MinBaseRadius = 2;
|
|
|
|
[Desc("Radius in cells around the center of the base to expand.")]
|
|
public readonly int MaxBaseRadius = 20;
|
|
|
|
[Desc("Minimum excess power the AI should try to maintain.")]
|
|
public readonly int MinimumExcessPower = 0;
|
|
|
|
[Desc("The targeted excess power the AI tries to maintain cannot rise above this.")]
|
|
public readonly int MaximumExcessPower = 0;
|
|
|
|
[Desc("Increase maintained excess power by this amount for every ExcessPowerIncreaseThreshold of base buildings.")]
|
|
public readonly int ExcessPowerIncrement = 0;
|
|
|
|
[Desc("Increase maintained excess power by ExcessPowerIncrement for every N base buildings.")]
|
|
public readonly int ExcessPowerIncreaseThreshold = 1;
|
|
|
|
[Desc("Number of refineries to build before building a barracks.")]
|
|
public readonly int InititalMinimumRefineryCount = 1;
|
|
|
|
[Desc("Number of refineries to build additionally after building a barracks.")]
|
|
public readonly int AdditionalMinimumRefineryCount = 1;
|
|
|
|
[Desc("Additional delay (in ticks) between structure production checks when there is no active production.",
|
|
"StructureProductionRandomBonusDelay is added to this.")]
|
|
public readonly int StructureProductionInactiveDelay = 125;
|
|
|
|
[Desc("Additional delay (in ticks) added between structure production checks when actively building things.",
|
|
"Note: this should be at least as large as the typical order latency to avoid duplicated build choices.")]
|
|
public readonly int StructureProductionActiveDelay = 25;
|
|
|
|
[Desc("A random delay (in ticks) of up to this is added to active/inactive production delays.")]
|
|
public readonly int StructureProductionRandomBonusDelay = 10;
|
|
|
|
[Desc("Delay (in ticks) until retrying to build structure after the last 3 consecutive attempts failed.")]
|
|
public readonly int StructureProductionResumeDelay = 1500;
|
|
|
|
[Desc("After how many failed attempts to place a structure should AI give up and wait",
|
|
"for StructureProductionResumeDelay before retrying.")]
|
|
public readonly int MaximumFailedPlacementAttempts = 3;
|
|
|
|
[Desc("How many randomly chosen cells with resources to check when deciding refinery placement.")]
|
|
public readonly int MaxResourceCellsToCheck = 3;
|
|
|
|
[Desc("Delay (in ticks) until rechecking for new BaseProviders.")]
|
|
public readonly int CheckForNewBasesDelay = 1500;
|
|
|
|
[Desc("Chance that the AI will place the defenses in the direction of the closest enemy building.")]
|
|
public readonly int PlaceDefenseTowardsEnemyChance = 100;
|
|
|
|
[Desc("Minimum range at which to build defensive structures near a combat hotspot.")]
|
|
public readonly int MinimumDefenseRadius = 5;
|
|
|
|
[Desc("Maximum range at which to build defensive structures near a combat hotspot.")]
|
|
public readonly int MaximumDefenseRadius = 20;
|
|
|
|
[Desc("Try to build another production building if there is too much cash.")]
|
|
public readonly int NewProductionCashThreshold = 5000;
|
|
|
|
[Desc("Radius in cells around a factory scanned for rally points by the AI.")]
|
|
public readonly int RallyPointScanRadius = 8;
|
|
|
|
[Desc("Radius in cells around each building with ProvideBuildableArea",
|
|
"to check for a 3x3 area of water where naval structures can be built.",
|
|
"Should match maximum adjacency of naval structures.")]
|
|
public readonly int CheckForWaterRadius = 8;
|
|
|
|
[Desc("Terrain types which are considered water for base building purposes.")]
|
|
public readonly HashSet<string> WaterTerrainTypes = new() { "Water" };
|
|
|
|
[Desc("What buildings to the AI should build.", "What integer percentage of the total base must be this type of building.")]
|
|
public readonly Dictionary<string, int> BuildingFractions = null;
|
|
|
|
[Desc("What buildings should the AI have a maximum limit to build.")]
|
|
public readonly Dictionary<string, int> BuildingLimits = null;
|
|
|
|
[Desc("When should the AI start building specific buildings.")]
|
|
public readonly Dictionary<string, int> BuildingDelays = null;
|
|
|
|
public override object Create(ActorInitializer init) { return new BaseBuilderBotModule(init.Self, this); }
|
|
}
|
|
|
|
public class BaseBuilderBotModule : ConditionalTrait<BaseBuilderBotModuleInfo>, IGameSaveTraitData,
|
|
IBotTick, IBotPositionsUpdated, IBotRespondToAttack, IBotRequestPauseUnitProduction
|
|
{
|
|
public CPos GetRandomBaseCenter()
|
|
{
|
|
var randomConstructionYard = world.Actors.Where(a => a.Owner == player &&
|
|
Info.ConstructionYardTypes.Contains(a.Info.Name))
|
|
.RandomOrDefault(world.LocalRandom);
|
|
|
|
return randomConstructionYard?.Location ?? initialBaseCenter;
|
|
}
|
|
|
|
public CPos DefenseCenter { get; private set; }
|
|
|
|
readonly World world;
|
|
readonly Player player;
|
|
PowerManager playerPower;
|
|
PlayerResources playerResources;
|
|
IResourceLayer resourceLayer;
|
|
IBotPositionsUpdated[] positionsUpdatedModules;
|
|
CPos initialBaseCenter;
|
|
readonly List<BaseBuilderQueueManager> builders = new();
|
|
|
|
public BaseBuilderBotModule(Actor self, BaseBuilderBotModuleInfo info)
|
|
: base(info)
|
|
{
|
|
world = self.World;
|
|
player = self.Owner;
|
|
}
|
|
|
|
protected override void Created(Actor self)
|
|
{
|
|
playerPower = self.Owner.PlayerActor.TraitOrDefault<PowerManager>();
|
|
playerResources = self.Owner.PlayerActor.Trait<PlayerResources>();
|
|
resourceLayer = self.World.WorldActor.TraitOrDefault<IResourceLayer>();
|
|
positionsUpdatedModules = self.Owner.PlayerActor.TraitsImplementing<IBotPositionsUpdated>().ToArray();
|
|
}
|
|
|
|
protected override void TraitEnabled(Actor self)
|
|
{
|
|
foreach (var building in Info.BuildingQueues)
|
|
builders.Add(new BaseBuilderQueueManager(this, building, player, playerPower, playerResources, resourceLayer));
|
|
foreach (var defense in Info.DefenseQueues)
|
|
builders.Add(new BaseBuilderQueueManager(this, defense, player, playerPower, playerResources, resourceLayer));
|
|
}
|
|
|
|
void IBotPositionsUpdated.UpdatedBaseCenter(CPos newLocation)
|
|
{
|
|
initialBaseCenter = newLocation;
|
|
}
|
|
|
|
void IBotPositionsUpdated.UpdatedDefenseCenter(CPos newLocation)
|
|
{
|
|
DefenseCenter = newLocation;
|
|
}
|
|
|
|
bool IBotRequestPauseUnitProduction.PauseUnitProduction => !IsTraitDisabled && !HasAdequateRefineryCount;
|
|
|
|
void IBotTick.BotTick(IBot bot)
|
|
{
|
|
SetRallyPointsForNewProductionBuildings(bot);
|
|
|
|
foreach (var b in builders)
|
|
b.Tick(bot);
|
|
}
|
|
|
|
void IBotRespondToAttack.RespondToAttack(IBot bot, Actor self, AttackInfo e)
|
|
{
|
|
if (e.Attacker == null || e.Attacker.Disposed)
|
|
return;
|
|
|
|
if (e.Attacker.Owner.RelationshipWith(self.Owner) != PlayerRelationship.Enemy)
|
|
return;
|
|
|
|
if (!e.Attacker.Info.HasTraitInfo<ITargetableInfo>())
|
|
return;
|
|
|
|
// Protect buildings
|
|
if (self.Info.HasTraitInfo<BuildingInfo>())
|
|
foreach (var n in positionsUpdatedModules)
|
|
n.UpdatedDefenseCenter(e.Attacker.Location);
|
|
}
|
|
|
|
void SetRallyPointsForNewProductionBuildings(IBot bot)
|
|
{
|
|
foreach (var rp in world.ActorsWithTrait<RallyPoint>())
|
|
{
|
|
if (rp.Actor.Owner != player)
|
|
continue;
|
|
|
|
if (rp.Trait.Path.Count == 0 || !IsRallyPointValid(rp.Trait.Path[0], rp.Actor.Info.TraitInfoOrDefault<BuildingInfo>()))
|
|
{
|
|
bot.QueueOrder(new Order("SetRallyPoint", rp.Actor, Target.FromCell(world, ChooseRallyLocationNear(rp.Actor)), false)
|
|
{
|
|
SuppressVisualFeedback = true
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Won't work for shipyards...
|
|
CPos ChooseRallyLocationNear(Actor producer)
|
|
{
|
|
var possibleRallyPoints = world.Map.FindTilesInCircle(producer.Location, Info.RallyPointScanRadius)
|
|
.Where(c => IsRallyPointValid(c, producer.Info.TraitInfoOrDefault<BuildingInfo>()));
|
|
|
|
if (!possibleRallyPoints.Any())
|
|
{
|
|
AIUtils.BotDebug("{0} has no possible rallypoint near {1}", producer.Owner, producer.Location);
|
|
return producer.Location;
|
|
}
|
|
|
|
return possibleRallyPoints.Random(world.LocalRandom);
|
|
}
|
|
|
|
bool IsRallyPointValid(CPos x, BuildingInfo info)
|
|
{
|
|
return info != null && world.IsCellBuildable(x, null, info);
|
|
}
|
|
|
|
// Require at least one refinery, unless we can't build it.
|
|
public bool HasAdequateRefineryCount =>
|
|
Info.RefineryTypes.Count == 0 ||
|
|
AIUtils.CountBuildingByCommonName(Info.RefineryTypes, player) >= MinimumRefineryCount ||
|
|
AIUtils.CountBuildingByCommonName(Info.PowerTypes, player) == 0 ||
|
|
AIUtils.CountBuildingByCommonName(Info.ConstructionYardTypes, player) == 0;
|
|
|
|
int MinimumRefineryCount => AIUtils.CountBuildingByCommonName(Info.BarracksTypes, player) > 0 ? Info.InititalMinimumRefineryCount + Info.AdditionalMinimumRefineryCount : Info.InititalMinimumRefineryCount;
|
|
|
|
List<MiniYamlNode> IGameSaveTraitData.IssueTraitData(Actor self)
|
|
{
|
|
if (IsTraitDisabled)
|
|
return null;
|
|
|
|
return new List<MiniYamlNode>()
|
|
{
|
|
new MiniYamlNode("InitialBaseCenter", FieldSaver.FormatValue(initialBaseCenter)),
|
|
new MiniYamlNode("DefenseCenter", FieldSaver.FormatValue(DefenseCenter))
|
|
};
|
|
}
|
|
|
|
void IGameSaveTraitData.ResolveTraitData(Actor self, ImmutableArray<MiniYamlNode> data)
|
|
{
|
|
if (self.World.IsReplay)
|
|
return;
|
|
|
|
var initialBaseCenterNode = data.FirstOrDefault(n => n.Key == "InitialBaseCenter");
|
|
if (initialBaseCenterNode != null)
|
|
initialBaseCenter = FieldLoader.GetValue<CPos>("InitialBaseCenter", initialBaseCenterNode.Value.Value);
|
|
|
|
var defenseCenterNode = data.FirstOrDefault(n => n.Key == "DefenseCenter");
|
|
if (defenseCenterNode != null)
|
|
DefenseCenter = FieldLoader.GetValue<CPos>("DefenseCenter", defenseCenterNode.Value.Value);
|
|
}
|
|
}
|
|
}
|