Files
OpenRA/OpenRA.Mods.Common/Traits/DockClientManager.cs
Gustas 52102da839 Fix harvesters refusing to queue dock orders whenn empty
(cherry picked from commit 19040126046a53fd5092515f2a4895fdd5c9c87a)
2024-12-27 16:30:58 +02:00

391 lines
13 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.Primitives;
using OpenRA.Support;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits
{
[Desc("Manages DockClients on the actor.")]
public class DockClientManagerInfo : ConditionalTraitInfo, IDockClientManagerInfo
{
[Desc("How long (in ticks) to wait until (re-)checking for a nearby available DockHost.")]
public readonly int SearchForDockDelay = 125;
[Desc("The pathfinding cost penalty applied for each dock client waiting to unload at a DockHost.")]
public readonly int OccupancyCostModifier = 12;
[ConsumedConditionReference]
[Desc("Boolean expression defining the condition under which the regular (non-force) enter cursor is disabled.")]
public readonly BooleanExpression RequireForceMoveCondition = null;
[CursorReference]
[Desc("Cursor to display when able to dock at target actor.")]
public readonly string EnterCursor = "enter";
[CursorReference]
[Desc("Cursor to display when unable to dock at target actor.")]
public readonly string EnterBlockedCursor = "enter-blocked";
[VoiceReference]
[Desc("Voice to be played when ordered to dock.")]
public readonly string Voice = "Action";
[Desc("Color to use for the target line of docking orders.")]
public readonly Color DockLineColor = Color.Green;
public override object Create(ActorInitializer init) { return new DockClientManager(init.Self, this); }
}
public class DockClientManager : ConditionalTrait<DockClientManagerInfo>, IResolveOrder, IOrderVoice, IIssueOrder, INotifyKilled, INotifyActorDisposing
{
readonly Actor self;
protected IDockClient[] dockClients;
public Color DockLineColor => Info.DockLineColor;
public int OccupancyCostModifier => Info.OccupancyCostModifier;
bool requireForceMove;
public DockClientManager(Actor self, DockClientManagerInfo info)
: base(info)
{
this.self = self;
}
protected override void Created(Actor self)
{
base.Created(self);
dockClients = self.TraitsImplementing<IDockClient>().ToArray();
}
public Actor ReservedHostActor { get; protected set; }
public IDockHost ReservedHost { get; protected set; }
IDockHost lastReservedDockHost = null;
public IDockHost LastReservedHost
{
get
{
if (lastReservedDockHost != null)
{
if (!lastReservedDockHost.IsEnabledAndInWorld)
lastReservedDockHost = null;
else
return lastReservedDockHost;
}
return ReservedHost;
}
}
public void UnreserveHost()
{
if (ReservedHost != null)
{
lastReservedDockHost = ReservedHost;
ReservedHost = null;
ReservedHostActor = null;
lastReservedDockHost.Unreserve(this);
}
}
/// <summary>In addition returns true if reservation was succesful or we have already been reserved at <paramref name="host"/>.</summary>
public bool ReserveHost(Actor hostActor, IDockHost host)
{
if (host == null)
return false;
if (ReservedHost == host)
return true;
UnreserveHost();
if (host.Reserve(hostActor, this))
{
ReservedHost = host;
ReservedHostActor = hostActor;
// After we have reserved a new Host we want to forget our old host.
lastReservedDockHost = null;
return true;
}
return false;
}
public void OnDockStarted(Actor self, Actor hostActor, IDockHost host)
{
foreach (var client in dockClients)
client.OnDockStarted(self, hostActor, host);
}
public bool OnDockTick(Actor self, Actor hostActor, IDockHost host)
{
if (IsTraitDisabled)
return true;
var cancel = true;
foreach (var client in dockClients)
if (!client.OnDockTick(self, hostActor, host))
cancel = false;
return cancel;
}
public void OnDockCompleted(Actor self, Actor hostActor, IDockHost host)
{
foreach (var client in dockClients)
client.OnDockCompleted(self, hostActor, host);
UnreserveHost();
}
IEnumerable<IOrderTargeter> IIssueOrder.Orders
{
get
{
yield return new DockActorTargeter(
6,
Info.EnterCursor,
Info.EnterBlockedCursor,
() => requireForceMove,
CanQueueDockAt,
(target, forceEnter) => CanDockAt(target, forceEnter, true));
}
}
void IResolveOrder.ResolveOrder(Actor self, Order order)
{
if (order.OrderString == "Dock" || order.OrderString == "ForceDock")
{
var target = order.Target;
// Deliver orders are only valid for own/allied actors,
// which are guaranteed to never be frozen.
// TODO: support frozen actors
if (target.Type != TargetType.Actor)
return;
self.QueueActivity(order.Queued, new MoveToDock(
self,
target.Actor,
null,
order.OrderString == "ForceDock",
true,
DockLineColor));
self.ShowTargetLines();
}
}
string IOrderVoice.VoicePhraseForOrder(Actor self, Order order)
{
if (order.Target.Type != TargetType.Actor || IsTraitDisabled)
return null;
if (order.OrderString != "Dock" && order.OrderString != "ForceDock")
return null;
if (CanQueueDockAt(order.Target.Actor, order.OrderString == "ForceDock", order.Queued))
return Info.Voice;
return null;
}
Order IIssueOrder.IssueOrder(Actor self, IOrderTargeter order, in Target target, bool queued)
{
if (order.OrderID == "Dock" || order.OrderID == "ForceDock")
return new Order(order.OrderID, self, target, queued);
return null;
}
public override IEnumerable<VariableObserver> GetVariableObservers()
{
foreach (var observer in base.GetVariableObservers())
yield return observer;
if (Info.RequireForceMoveCondition != null)
yield return new VariableObserver(RequireForceMoveConditionChanged, Info.RequireForceMoveCondition.Variables);
}
void RequireForceMoveConditionChanged(Actor self, IReadOnlyDictionary<string, int> conditions)
{
requireForceMove = Info.RequireForceMoveCondition.Evaluate(conditions);
}
/// <summary>Do we have an enabled client with matching <paramref name="type"/>.</summary>
public bool CanDock(BitSet<DockType> type, bool forceEnter = false)
{
return !IsTraitDisabled && dockClients.Any(client => client.CanDock(type, forceEnter));
}
/// <summary>Does this <paramref name="target"/> contain at least one enabled <see cref="IDockHost"/> with maching <see cref="DockType"/>.</summary>
public bool CanDock(Actor target, bool forceEnter = false)
{
return !IsTraitDisabled &&
target.TraitsImplementing<IDockHost>()
.Any(host => dockClients.Any(client => client.CanDock(host.GetDockType, forceEnter)));
}
/// <summary>Can we dock to this <paramref name="host"/>.</summary>
public bool CanDockAt(Actor hostActor, IDockHost host, bool forceEnter = false, bool ignoreOccupancy = false)
{
return !IsTraitDisabled && dockClients.Any(
client => client.CanDockAt(hostActor, host, forceEnter, ignoreOccupancy));
}
/// <summary>Can we dock to this <paramref name="target"/>.</summary>
public bool CanDockAt(Actor target, bool forceEnter = false, bool ignoreOccupancy = false)
{
return !IsTraitDisabled && target.TraitsImplementing<IDockHost>().Any(
host => dockClients.Any(client => client.CanDockAt(target, host, forceEnter, ignoreOccupancy)));
}
/// <summary>Can we dock to this <paramref name="target"/>.</summary>
public bool CanQueueDockAt(Actor target, bool forceEnter, bool isQueued)
{
return !IsTraitDisabled
&& target.TraitsImplementing<IDockHost>()
.Any(host => dockClients.Any(client => client.CanQueueDockAt(target, host, forceEnter, isQueued)));
}
/// <summary>Find the closest viable <see cref="IDockHost"/>.</summary>
/// <remarks>If <paramref name="type"/> is not set, scans all clients. Does not check if <see cref="DockClientManager"/> is enabled.</remarks>
public TraitPair<IDockHost>? ClosestDock(IDockHost ignore, BitSet<DockType> type = default, bool forceEnter = false, bool ignoreOccupancy = false)
{
var clients = type.IsEmpty ? dockClients : AvailableDockClients(type, forceEnter);
return self.World.ActorsWithTrait<IDockHost>()
.Where(host =>
host.Trait != ignore &&
clients.Any(client => client.CanDockAt(host.Actor, host.Trait, forceEnter, ignoreOccupancy)))
.ClosestDock(self, this);
}
/// <summary>Get viable <see cref="IDockHost"/>'s on the <paramref name="target"/>.</summary>
/// <remarks>If <paramref name="type"/> is not set, checks all clients. Does not check if <see cref="DockClientManager"/> is enabled.</remarks>
public IEnumerable<TraitPair<IDockHost>> AvailableDockHosts(Actor target, BitSet<DockType> type = default,
bool forceEnter = false, bool ignoreOccupancy = false)
{
var clients = type.IsEmpty ? dockClients : AvailableDockClients(type, forceEnter);
return target.TraitsImplementing<IDockHost>()
.Where(host => clients.Any(client => client.CanDockAt(target, host, forceEnter, ignoreOccupancy)))
.Select(host => new TraitPair<IDockHost>(target, host));
}
/// <summary>Get clients of matching <paramref name="type"/>.</summary>
/// <remarks>Does not check if <see cref="DockClientManager"/> is enabled.</remarks>
public IEnumerable<IDockClient> AvailableDockClients(BitSet<DockType> type, bool forceEnter = false)
{
return dockClients.Where(client => client.CanDock(type, forceEnter));
}
void INotifyKilled.Killed(Actor self, AttackInfo e) { UnreserveHost(); }
void INotifyActorDisposing.Disposing(Actor self) { UnreserveHost(); }
}
public class DockActorTargeter : IOrderTargeter
{
readonly string enterCursor;
readonly string enterBlockedCursor;
readonly Func<bool> requireForceMove;
readonly Func<Actor, bool, bool, bool> canTarget;
readonly Func<Actor, bool, bool> useEnterCursor;
public DockActorTargeter(int priority, string enterCursor, string enterBlockedCursor,
Func<bool> requireForceMove, Func<Actor, bool, bool, bool> canTarget, Func<Actor, bool, bool> useEnterCursor)
{
OrderID = "Dock";
OrderPriority = priority;
this.enterCursor = enterCursor;
this.enterBlockedCursor = enterBlockedCursor;
this.requireForceMove = requireForceMove;
this.canTarget = canTarget;
this.useEnterCursor = useEnterCursor;
}
public string OrderID { get; private set; }
public int OrderPriority { get; }
public bool TargetOverridesSelection(Actor self, in Target target, List<Actor> actorsAt, CPos xy, TargetModifiers modifiers) { return true; }
public bool CanTarget(Actor self, in Target target, ref TargetModifiers modifiers, ref string cursor)
{
// TODO: support frozen actors
if (target.Type != TargetType.Actor)
return false;
cursor = enterCursor;
IsQueued = modifiers.HasModifier(TargetModifiers.ForceQueue);
var forceEnter = modifiers.HasModifier(TargetModifiers.ForceMove);
OrderID = forceEnter ? "ForceDock" : "Dock";
if (requireForceMove() && !forceEnter)
return false;
if (!canTarget(target.Actor, forceEnter, IsQueued))
return false;
cursor = IsQueued || useEnterCursor(target.Actor, forceEnter)
? enterCursor
: enterBlockedCursor;
return true;
}
public virtual bool IsQueued { get; protected set; }
}
public static class DockExts
{
public static TraitPair<IDockHost>? ClosestDock(this IEnumerable<TraitPair<IDockHost>> docks, Actor clientActor, DockClientManager client)
{
var mobile = clientActor.TraitOrDefault<Mobile>();
if (mobile != null)
{
// Overlapping hosts can become hidden.
var lookup = docks
.GroupBy(dock => clientActor.World.Map.CellContaining(dock.Trait.DockPosition))
.ToDictionary(group => group.Key, group => group.First());
// Start a search from each docks position:
var path = mobile.PathFinder.FindPathToTargetCell(
clientActor, lookup.Keys, clientActor.Location, BlockedByActor.None,
location =>
{
if (!lookup.TryGetValue(location, out var dock))
return 0;
// Prefer docks with less occupancy (multiplier is to offset distance cost):
// TODO: add custom weights. E.g. owner vs allied.
return dock.Trait.ReservationCount * client.OccupancyCostModifier;
});
if (path.Count > 0)
return lookup[path.Last()];
}
else
{
return docks
.OrderBy(dock =>
(clientActor.Location - clientActor.World.Map.CellContaining(dock.Trait.DockPosition)).LengthSquared +
dock.Trait.ReservationCount * client.OccupancyCostModifier)
.FirstOrDefault();
}
return null;
}
}
}