Files
OpenRA/OpenRA.Mods.Common/Traits/BotModules/HarvesterBotModule.cs
RoosterDragon dc0f26a1cd Improve BotModule performance.
Several parts of bot module logic, often through the AIUtils helper class, will query or count over all actors in the world. This is not a fast operation and the AI tends to repeat it often.

Introduce some ActorIndex classes that can maintain an index of actors in the world that match a query based on a mix of actor name, owner or trait. These indexes introduce some overhead to maintain, but allow the queries or counts that bot modules needs to perform to be greatly sped up, as the index means there is a much smaller starting set of actors to consider. This is beneficial to the bot logic as the TraitDictionary index maintained by the world works only in terms of traits and doesn't allow the bot logic to perform a sufficiently selective lookup. This is because the bot logic is usually defined in terms of actor names rather than traits.
2024-03-12 16:14:29 +02:00

176 lines
6.5 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;
using System.Collections.Generic;
using System.Linq;
using OpenRA.Mods.Common.Activities;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits
{
[TraitLocation(SystemActors.Player)]
[Desc("Put this on the Player actor. Manages bot harvesters to ensure they always continue harvesting as long as there are resources on the map.")]
public class HarvesterBotModuleInfo : ConditionalTraitInfo
{
[Desc("Actor types that are considered harvesters. If harvester count drops below RefineryTypes count, a new harvester is built.",
"Leave empty to disable harvester replacement. Currently only needed by harvester replacement system.")]
public readonly HashSet<string> HarvesterTypes = new();
[Desc("Actor types that are counted as refineries. Currently only needed by harvester replacement system.")]
public readonly HashSet<string> RefineryTypes = new();
[Desc("Interval (in ticks) between giving out orders to idle harvesters.")]
public readonly int ScanForIdleHarvestersInterval = 50;
[Desc("Avoid enemy actors nearby when searching for a new resource patch. Should be somewhere near the max weapon range.")]
public readonly WDist HarvesterEnemyAvoidanceRadius = WDist.FromCells(8);
public override object Create(ActorInitializer init) { return new HarvesterBotModule(init.Self, this); }
}
public class HarvesterBotModule : ConditionalTrait<HarvesterBotModuleInfo>, IBotTick, INotifyActorDisposing
{
sealed class HarvesterTraitWrapper
{
public readonly Actor Actor;
public readonly Harvester Harvester;
public readonly Parachutable Parachutable;
public readonly Mobile Mobile;
public HarvesterTraitWrapper(Actor actor)
{
Actor = actor;
Harvester = actor.Trait<Harvester>();
Parachutable = actor.TraitOrDefault<Parachutable>();
Mobile = actor.TraitOrDefault<Mobile>();
}
}
readonly World world;
readonly Player player;
readonly Func<Actor, bool> unitCannotBeOrdered;
readonly Dictionary<Actor, HarvesterTraitWrapper> harvesters = new();
readonly ActorIndex.OwnerAndNamesAndTrait<Building> refineries;
readonly ActorIndex.OwnerAndNamesAndTrait<Harvester> harvestersIndex;
IResourceLayer resourceLayer;
ResourceClaimLayer claimLayer;
IBotRequestUnitProduction[] requestUnitProduction;
int scanForIdleHarvestersTicks;
public HarvesterBotModule(Actor self, HarvesterBotModuleInfo info)
: base(info)
{
world = self.World;
player = self.Owner;
unitCannotBeOrdered = a => a.Owner != self.Owner || a.IsDead || !a.IsInWorld;
refineries = new ActorIndex.OwnerAndNamesAndTrait<Building>(world, info.RefineryTypes, player);
harvestersIndex = new ActorIndex.OwnerAndNamesAndTrait<Harvester>(world, info.HarvesterTypes, player);
}
protected override void Created(Actor self)
{
requestUnitProduction = self.Owner.PlayerActor.TraitsImplementing<IBotRequestUnitProduction>().ToArray();
}
protected override void TraitEnabled(Actor self)
{
resourceLayer = world.WorldActor.TraitOrDefault<IResourceLayer>();
claimLayer = world.WorldActor.TraitOrDefault<ResourceClaimLayer>();
// Avoid all AIs scanning for idle harvesters on the same tick, randomize their initial scan delay.
scanForIdleHarvestersTicks = world.LocalRandom.Next(Info.ScanForIdleHarvestersInterval, Info.ScanForIdleHarvestersInterval * 2);
}
void IBotTick.BotTick(IBot bot)
{
if (resourceLayer == null || resourceLayer.IsEmpty)
return;
if (--scanForIdleHarvestersTicks > 0)
return;
var toRemove = harvesters.Keys.Where(unitCannotBeOrdered).ToList();
foreach (var a in toRemove)
harvesters.Remove(a);
scanForIdleHarvestersTicks = Info.ScanForIdleHarvestersInterval;
// Find new harvesters
var newHarvesters = world.ActorsHavingTrait<Harvester>().Where(a => !unitCannotBeOrdered(a) && !harvesters.ContainsKey(a));
foreach (var a in newHarvesters)
harvesters[a] = new HarvesterTraitWrapper(a);
// Find idle harvesters and give them orders:
foreach (var h in harvesters)
{
if (h.Value.Mobile == null)
continue;
if (!h.Key.IsIdle)
{
// Ignore this actor if FindAndDeliverResources is working fine or it is performing a different activity
if (h.Key.CurrentActivity is not FindAndDeliverResources act || !act.LastSearchFailed)
continue;
}
if (h.Value.Parachutable != null && h.Value.Parachutable.IsInAir)
continue;
// Tell the idle harvester to quit slacking:
var newSafeResourcePatch = FindNextResource(h.Key, h.Value);
AIUtils.BotDebug($"AI: Harvester {h.Key} is idle. Ordering to {newSafeResourcePatch} in search for new resources.");
bot.QueueOrder(new Order("Harvest", h.Key, newSafeResourcePatch, false));
}
// Less harvesters than refineries - build a new harvester
var unitBuilder = requestUnitProduction.FirstEnabledTraitOrDefault();
if (unitBuilder != null && Info.HarvesterTypes.Count > 0)
{
var harvCountTooLow =
AIUtils.CountActorByCommonName(harvestersIndex) <
AIUtils.CountActorByCommonName(refineries);
if (harvCountTooLow)
{
var harvesterType = Info.HarvesterTypes.Random(world.LocalRandom);
if (unitBuilder.RequestedProductionCount(bot, harvesterType) == 0)
unitBuilder.RequestUnitProduction(bot, harvesterType);
}
}
}
Target FindNextResource(Actor actor, HarvesterTraitWrapper harv)
{
bool IsValidResource(CPos cell) =>
harv.Harvester.CanHarvestCell(cell) &&
claimLayer.CanClaimCell(actor, cell);
var path = harv.Mobile.PathFinder.FindPathToTargetCellByPredicate(
actor, new[] { actor.Location }, IsValidResource, BlockedByActor.Stationary,
loc => world.FindActorsInCircle(world.Map.CenterOfCell(loc), Info.HarvesterEnemyAvoidanceRadius)
.Where(u => !u.IsDead && actor.Owner.RelationshipWith(u.Owner) == PlayerRelationship.Enemy)
.Sum(u => Math.Max(WDist.Zero.Length, Info.HarvesterEnemyAvoidanceRadius.Length - (world.Map.CenterOfCell(loc) - u.CenterPosition).Length)));
if (path.Count == 0)
return Target.Invalid;
return Target.FromCell(world, path[0]);
}
void INotifyActorDisposing.Disposing(Actor self)
{
refineries.Dispose();
harvestersIndex.Dispose();
}
}
}