#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, 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().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); } } /// In addition returns true if reservation was succesful or we have already been reserved at . 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 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 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 conditions) { requireForceMove = Info.RequireForceMoveCondition.Evaluate(conditions); } /// Do we have an enabled client with matching . public bool CanDock(BitSet type, bool forceEnter = false) { return !IsTraitDisabled && dockClients.Any(client => client.CanDock(type, forceEnter)); } /// Does this contain at least one enabled with maching . public bool CanDock(Actor target, bool forceEnter = false) { return !IsTraitDisabled && target.TraitsImplementing() .Any(host => dockClients.Any(client => client.CanDock(host.GetDockType, forceEnter))); } /// Can we dock to this . public bool CanDockAt(Actor hostActor, IDockHost host, bool forceEnter = false, bool ignoreOccupancy = false) { return !IsTraitDisabled && dockClients.Any( client => client.CanDockAt(hostActor, host, forceEnter, ignoreOccupancy)); } /// Can we dock to this . public bool CanDockAt(Actor target, bool forceEnter = false, bool ignoreOccupancy = false) { return !IsTraitDisabled && target.TraitsImplementing().Any( host => dockClients.Any(client => client.CanDockAt(target, host, forceEnter, ignoreOccupancy))); } /// Can we dock to this . public bool CanQueueDockAt(Actor target, bool forceEnter, bool isQueued) { return !IsTraitDisabled && target.TraitsImplementing() .Any(host => dockClients.Any(client => client.CanQueueDockAt(target, host, forceEnter, isQueued))); } /// Find the closest viable . /// If is not set, scans all clients. Does not check if is enabled. public TraitPair? ClosestDock(IDockHost ignore, BitSet type = default, bool forceEnter = false, bool ignoreOccupancy = false) { var clients = type.IsEmpty ? dockClients : AvailableDockClients(type, forceEnter); return self.World.ActorsWithTrait() .Where(host => host.Trait != ignore && clients.Any(client => client.CanDockAt(host.Actor, host.Trait, forceEnter, ignoreOccupancy))) .ClosestDock(self, this); } /// Get viable 's on the . /// If is not set, checks all clients. Does not check if is enabled. public IEnumerable> AvailableDockHosts(Actor target, BitSet type = default, bool forceEnter = false, bool ignoreOccupancy = false) { var clients = type.IsEmpty ? dockClients : AvailableDockClients(type, forceEnter); return target.TraitsImplementing() .Where(host => clients.Any(client => client.CanDockAt(target, host, forceEnter, ignoreOccupancy))) .Select(host => new TraitPair(target, host)); } /// Get clients of matching . /// Does not check if is enabled. public IEnumerable AvailableDockClients(BitSet 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 requireForceMove; readonly Func canTarget; readonly Func useEnterCursor; public DockActorTargeter(int priority, string enterCursor, string enterBlockedCursor, Func requireForceMove, Func canTarget, Func 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 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? ClosestDock(this IEnumerable> docks, Actor clientActor, DockClientManager client) { var mobile = clientActor.TraitOrDefault(); 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; } } }