From d0974cfdd2702db9b52125467b8a5ca4ebffd588 Mon Sep 17 00:00:00 2001 From: Gustas <37534529+PunkPun@users.noreply.github.com> Date: Tue, 24 Jan 2023 19:33:42 +0200 Subject: [PATCH] Abstract docking logic from Harvester and Refinery --- .../Activities/FindAndDeliverResources.cs | 86 +++-- .../Activities/GenericDockSequence.cs | 67 ++-- OpenRA.Mods.Common/Activities/MoveToDock.cs | 92 ++--- .../Traits/Buildings/Refinery.cs | 96 +---- .../Traits/CarryableHarvester.cs | 5 +- OpenRA.Mods.Common/Traits/DockClientBase.cs | 54 +++ .../Traits/DockClientManager.cs | 334 ++++++++++++++++++ OpenRA.Mods.Common/Traits/DockHost.cs | 183 ++++++++++ OpenRA.Mods.Common/Traits/Harvester.cs | 166 ++------- OpenRA.Mods.Common/TraitsInterfaces.cs | 74 +++- .../Rules/20230801/AbstractDocking.cs | 166 +++++++++ OpenRA.Mods.Common/UpdateRules/UpdatePath.cs | 13 +- mods/cnc/rules/structures.yaml | 6 +- mods/cnc/rules/vehicles.yaml | 1 + mods/d2k/rules/structures.yaml | 6 +- mods/d2k/rules/vehicles.yaml | 1 + mods/ra/rules/structures.yaml | 4 +- mods/ra/rules/vehicles.yaml | 1 + mods/ts/rules/nod-structures.yaml | 4 +- mods/ts/rules/nod-vehicles.yaml | 5 +- mods/ts/rules/shared-structures.yaml | 6 +- mods/ts/rules/shared-vehicles.yaml | 4 +- 22 files changed, 1016 insertions(+), 358 deletions(-) create mode 100644 OpenRA.Mods.Common/Traits/DockClientBase.cs create mode 100644 OpenRA.Mods.Common/Traits/DockClientManager.cs create mode 100644 OpenRA.Mods.Common/Traits/DockHost.cs create mode 100644 OpenRA.Mods.Common/UpdateRules/Rules/20230801/AbstractDocking.cs diff --git a/OpenRA.Mods.Common/Activities/FindAndDeliverResources.cs b/OpenRA.Mods.Common/Activities/FindAndDeliverResources.cs index bea05a80f1..7a61cfbaea 100644 --- a/OpenRA.Mods.Common/Activities/FindAndDeliverResources.cs +++ b/OpenRA.Mods.Common/Activities/FindAndDeliverResources.cs @@ -24,8 +24,6 @@ namespace OpenRA.Mods.Common.Activities readonly HarvesterInfo harvInfo; readonly Mobile mobile; readonly ResourceClaimLayer claimLayer; - - Actor deliverActor; CPos? orderLocation; CPos? lastHarvestedCell; bool hasDeliveredLoad; @@ -34,19 +32,14 @@ namespace OpenRA.Mods.Common.Activities public bool LastSearchFailed { get; private set; } - public FindAndDeliverResources(Actor self, Actor deliverActor = null) + public FindAndDeliverResources(Actor self, CPos? orderLocation = null) { harv = self.Trait(); harvInfo = self.Info.TraitInfo(); mobile = self.Trait(); claimLayer = self.World.WorldActor.Trait(); - this.deliverActor = deliverActor; - } - - public FindAndDeliverResources(Actor self, CPos orderLocation) - : this(self, null) - { - this.orderLocation = orderLocation; + if (orderLocation.HasValue) + this.orderLocation = orderLocation.Value; } protected override void OnFirstRun(Actor self) @@ -63,14 +56,6 @@ namespace OpenRA.Mods.Common.Activities if (harv.IsFull) QueueChild(new MoveToDock(self)); } - - // If an explicit "deliver" order is given, the harvester goes immediately to the refinery. - if (deliverActor != null) - { - QueueChild(new MoveToDock(self, deliverActor)); - hasDeliveredLoad = true; - deliverActor = null; - } } public override bool Tick(Actor self) @@ -92,9 +77,12 @@ namespace OpenRA.Mods.Common.Activities // Are we full or have nothing more to gather? Deliver resources. if (harv.IsFull || (!harv.IsEmpty && LastSearchFailed)) { + // If we are reserved it means docking was already initiated and we should wait. + if (harv.DockClientManager.ReservedHost != null) + return false; + QueueChild(new MoveToDock(self)); hasDeliveredLoad = true; - return false; } // After a failed search, wait and sit still for a bit before searching again. @@ -128,13 +116,13 @@ namespace OpenRA.Mods.Common.Activities // of the refinery entrance. if (LastSearchFailed) { - var lastproc = harv.LastLinkedProc ?? harv.LinkedProc; - if (lastproc != null && !lastproc.Disposed) + var lastproc = harv.DockClientManager.LastReservedHost; + if (lastproc != null) { - var deliveryLoc = lastproc.Trait().DeliveryPosition; - if (self.CenterPosition == deliveryLoc && harv.IsEmpty) + var deliveryLoc = self.World.Map.CellContaining(lastproc.DockPosition); + if (self.Location == deliveryLoc && harv.IsEmpty) { - var unblockCell = self.World.Map.CellContaining(deliveryLoc) + harv.Info.UnblockCell; + var unblockCell = deliveryLoc + harv.Info.UnblockCell; var moveTo = mobile.NearestMoveableCell(unblockCell, 1, 5); QueueChild(mobile.MoveTo(moveTo, 1)); } @@ -171,14 +159,31 @@ namespace OpenRA.Mods.Common.Activities } // Determine where to search from and how far to search: - var procLoc = GetSearchFromProcLocation(); - var searchFromLoc = lastHarvestedCell ?? procLoc ?? self.Location; - var searchRadius = lastHarvestedCell.HasValue ? harvInfo.SearchFromHarvesterRadius : harvInfo.SearchFromProcRadius; + // Prioritise search by these locations in this order: lastHarvestedCell -> lastLinkedDock -> self. + CPos searchFromLoc; + int searchRadius; + WPos? dockPos = null; + if (lastHarvestedCell.HasValue) + { + searchRadius = harvInfo.SearchFromHarvesterRadius; + searchFromLoc = lastHarvestedCell.Value; + } + else + { + searchRadius = harvInfo.SearchFromProcRadius; + var dock = harv.DockClientManager.LastReservedHost; + if (dock != null) + { + dockPos = dock.DockPosition; + searchFromLoc = self.World.Map.CellContaining(dockPos.Value); + } + else + searchFromLoc = self.Location; + } var searchRadiusSquared = searchRadius * searchRadius; var map = self.World.Map; - var procPos = procLoc.HasValue ? (WPos?)map.CenterOfCell(procLoc.Value) : null; var harvPos = self.CenterPosition; // Find any harvestable resources: @@ -196,19 +201,19 @@ namespace OpenRA.Mods.Common.Activities // Add a cost modifier to harvestable cells to prefer resources that are closer to the refinery. // This reduces the tendency for harvesters to move in straight lines - if (procPos.HasValue && harvInfo.ResourceRefineryDirectionPenalty > 0 && harv.CanHarvestCell(loc)) + if (dockPos.HasValue && harvInfo.ResourceRefineryDirectionPenalty > 0 && harv.CanHarvestCell(loc)) { var pos = map.CenterOfCell(loc); // Calculate harv-cell-refinery angle (cosine rule) - var b = pos - procPos.Value; + var b = pos - dockPos.Value; if (b != WVec.Zero) { var c = pos - harvPos; if (c != WVec.Zero) { - var a = harvPos - procPos.Value; + var a = harvPos - dockPos.Value; var cosA = (int)(512 * (b.LengthSquared + c.LengthSquared - a.LengthSquared) / b.Length / c.Length); // Cost modifier varies between 0 and ResourceRefineryDirectionPenalty @@ -239,19 +244,12 @@ namespace OpenRA.Mods.Common.Activities if (orderLocation != null) yield return new TargetLineNode(Target.FromCell(self.World, orderLocation.Value), harvInfo.HarvestLineColor); - else if (deliverActor != null) - yield return new TargetLineNode(Target.FromActor(deliverActor), harvInfo.DeliverLineColor); - } - - CPos? GetSearchFromProcLocation() - { - if (harv.LastLinkedProc != null && !harv.LastLinkedProc.IsDead && harv.LastLinkedProc.IsInWorld) - return harv.LastLinkedProc.World.Map.CellContaining(harv.LastLinkedProc.Trait().DeliveryPosition); - - if (harv.LinkedProc != null && !harv.LinkedProc.IsDead && harv.LinkedProc.IsInWorld) - return harv.LinkedProc.World.Map.CellContaining(harv.LinkedProc.Trait().DeliveryPosition); - - return null; + else + { + var manager = harv.DockClientManager; + if (manager.ReservedHostActor != null) + yield return new TargetLineNode(Target.FromActor(manager.ReservedHostActor), manager.DockLineColor); + } } } } diff --git a/OpenRA.Mods.Common/Activities/GenericDockSequence.cs b/OpenRA.Mods.Common/Activities/GenericDockSequence.cs index df21458e77..7742a0b83e 100644 --- a/OpenRA.Mods.Common/Activities/GenericDockSequence.cs +++ b/OpenRA.Mods.Common/Activities/GenericDockSequence.cs @@ -24,12 +24,12 @@ namespace OpenRA.Mods.Common.Activities { protected enum DockingState { Wait, Drag, Dock, Loop, Undock, Complete } - protected readonly Actor RefineryActor; + protected readonly Actor DockHostActor; + protected readonly IDockHost DockHost; protected readonly WithDockingOverlay DockHostSpriteOverlay; - protected readonly Harvester Harv; + protected readonly DockClientManager DockClient; protected readonly IDockClientBody DockClientBody; protected readonly bool IsDragRequired; - protected readonly WVec DragOffset; protected readonly int DragLength; protected readonly WPos StartDrag; protected readonly WPos EndDrag; @@ -41,20 +41,30 @@ namespace OpenRA.Mods.Common.Activities bool dockInitiated = false; - public GenericDockSequence(Actor self, Actor refineryActor, Refinery refinery) + public GenericDockSequence(Actor self, DockClientManager client, Actor hostActor, IDockHost host) { dockingState = DockingState.Drag; - RefineryActor = refineryActor; - DockHostSpriteOverlay = refineryActor.TraitOrDefault(); - IsDragRequired = refinery.IsDragRequired; - DragOffset = refinery.DragOffset; - DragLength = refinery.DragLength; - Harv = self.Trait(); + + DockClient = client; DockClientBody = self.TraitOrDefault(); - StartDrag = self.CenterPosition; - EndDrag = refineryActor.CenterPosition + DragOffset; notifyDockClients = self.TraitsImplementing().ToArray(); - notifyDockHosts = refineryActor.TraitsImplementing().ToArray(); + + DockHost = host; + DockHostActor = hostActor; + DockHostSpriteOverlay = hostActor.TraitOrDefault(); + notifyDockHosts = hostActor.TraitsImplementing().ToArray(); + + if (host is IDockHostDrag sequence) + { + IsDragRequired = sequence.IsDragRequired; + DragLength = sequence.DragLength; + StartDrag = self.CenterPosition; + EndDrag = hostActor.CenterPosition + sequence.DragOffset; + } + else + IsDragRequired = false; + + QueueChild(new Wait(host.DockWait)); } public override bool Tick(Actor self) @@ -65,8 +75,11 @@ namespace OpenRA.Mods.Common.Activities return false; case DockingState.Drag: - if (IsCanceling || !RefineryActor.IsInWorld || RefineryActor.IsDead || Harv.IsTraitDisabled) + if (IsCanceling || DockHostActor.IsDead || !DockHostActor.IsInWorld || !DockClient.CanDockAt(DockHostActor, DockHost, false, true)) + { + DockClient.UnreserveHost(); return true; + } dockingState = DockingState.Dock; if (IsDragRequired) @@ -75,10 +88,12 @@ namespace OpenRA.Mods.Common.Activities return false; case DockingState.Dock: - if (!IsCanceling && RefineryActor.IsInWorld && !RefineryActor.IsDead && !Harv.IsTraitDisabled) + if (!IsCanceling && !DockHostActor.IsDead && DockHostActor.IsInWorld && DockClient.CanDockAt(DockHostActor, DockHost, false, true)) { dockInitiated = true; PlayDockAnimations(self); + DockHost.OnDockStarted(DockHostActor, self, DockClient); + DockClient.OnDockStarted(self, DockHostActor, DockHost); NotifyDocked(self); } else @@ -87,7 +102,7 @@ namespace OpenRA.Mods.Common.Activities return false; case DockingState.Loop: - if (IsCanceling || !RefineryActor.IsInWorld || RefineryActor.IsDead || Harv.IsTraitDisabled || Harv.TickUnload(self, RefineryActor)) + if (IsCanceling || DockHostActor.IsDead || !DockHostActor.IsInWorld || DockClient.OnDockTick(self, DockHostActor, DockHost)) dockingState = DockingState.Undock; return false; @@ -101,8 +116,8 @@ namespace OpenRA.Mods.Common.Activities return false; case DockingState.Complete: - Harv.LastLinkedProc = Harv.LinkedProc; - Harv.LinkProc(null); + DockHost.OnDockCompleted(DockHostActor, self, DockClient); + DockClient.OnDockCompleted(self, DockHostActor, DockHost); NotifyUndocked(self); if (IsDragRequired) QueueChild(new Drag(self, EndDrag, StartDrag, DragLength)); @@ -145,7 +160,7 @@ namespace OpenRA.Mods.Common.Activities public virtual void PlayUndockAnimations(Actor self) { - if (RefineryActor.IsInWorld && !RefineryActor.IsDead && DockHostSpriteOverlay != null && !DockHostSpriteOverlay.Visible) + if (DockHostActor.IsInWorld && !DockHostActor.IsDead && DockHostSpriteOverlay != null && !DockHostSpriteOverlay.Visible) { dockingState = DockingState.Wait; DockHostSpriteOverlay.Visible = true; @@ -176,30 +191,30 @@ namespace OpenRA.Mods.Common.Activities void NotifyDocked(Actor self) { foreach (var nd in notifyDockClients) - nd.Docked(self, RefineryActor); + nd.Docked(self, DockHostActor); foreach (var nd in notifyDockHosts) - nd.Docked(RefineryActor, self); + nd.Docked(DockHostActor, self); } void NotifyUndocked(Actor self) { foreach (var nd in notifyDockClients) - nd.Undocked(self, RefineryActor); + nd.Undocked(self, DockHostActor); - if (RefineryActor.IsInWorld && !RefineryActor.IsDead) + if (DockHostActor.IsInWorld && !DockHostActor.IsDead) foreach (var nd in notifyDockHosts) - nd.Undocked(RefineryActor, self); + nd.Undocked(DockHostActor, self); } public override IEnumerable GetTargets(Actor self) { - yield return Target.FromActor(RefineryActor); + yield return Target.FromActor(DockHostActor); } public override IEnumerable TargetLineNodes(Actor self) { - yield return new TargetLineNode(Target.FromActor(RefineryActor), Color.Green); + yield return new TargetLineNode(Target.FromActor(DockHostActor), Color.Green); } } } diff --git a/OpenRA.Mods.Common/Activities/MoveToDock.cs b/OpenRA.Mods.Common/Activities/MoveToDock.cs index c3c6ea01e8..8f07a7743c 100644 --- a/OpenRA.Mods.Common/Activities/MoveToDock.cs +++ b/OpenRA.Mods.Common/Activities/MoveToDock.cs @@ -19,66 +19,71 @@ namespace OpenRA.Mods.Common.Activities { public class MoveToDock : Activity { - readonly IMove movement; - readonly Harvester harv; - readonly Actor targetActor; + readonly DockClientManager dockClient; + Actor dockHostActor; + IDockHost dockHost; readonly INotifyHarvesterAction[] notifyHarvesterActions; - Actor proc; - - public MoveToDock(Actor self, Actor targetActor = null) + public MoveToDock(Actor self, Actor dockHostActor = null, IDockHost dockHost = null) { - movement = self.Trait(); - harv = self.Trait(); - this.targetActor = targetActor; + dockClient = self.Trait(); + this.dockHostActor = dockHostActor; + this.dockHost = dockHost; notifyHarvesterActions = self.TraitsImplementing().ToArray(); } - protected override void OnFirstRun(Actor self) - { - if (targetActor != null && targetActor.IsInWorld) - harv.LinkProc(targetActor); - } - public override bool Tick(Actor self) { - if (harv.IsTraitDisabled) - Cancel(self, true); - if (IsCanceling) return true; - // Find the nearest best refinery if not explicitly ordered to a specific refinery: - if (harv.LinkedProc == null || !harv.LinkedProc.IsInWorld) - harv.ChooseNewProc(self, null); - - // No refineries exist; check again after delay defined in Harvester. - if (harv.LinkedProc == null) + if (dockClient.IsTraitDisabled) { - QueueChild(new Wait(harv.Info.SearchForDeliveryBuildingDelay)); - return false; + Cancel(self, true); + return true; } - proc = harv.LinkedProc; - var iao = proc.Trait(); - - if (self.CenterPosition != iao.DeliveryPosition) + // Find the nearest DockHost if not explicitly ordered to a specific dock. + if (dockHost == null || !dockHost.IsEnabledAndInWorld) { - foreach (var n in notifyHarvesterActions) - n.MovingToRefinery(self, proc); - - var target = Target.FromActor(proc); - QueueChild(movement.MoveOntoTarget(self, target, iao.DeliveryPosition - proc.CenterPosition, iao.DeliveryAngle)); - return false; + var host = dockClient.ClosestDock(null); + if (host.HasValue) + { + dockHost = host.Value.Trait; + dockHostActor = host.Value.Actor; + } + else + { + // No docks exist; check again after delay defined in dockClient. + QueueChild(new Wait(dockClient.Info.SearchForDockDelay)); + return false; + } } - QueueChild(new Wait(10)); - iao.OnDock(self, this); - return true; + if (dockClient.ReserveHost(dockHostActor, dockHost)) + { + if (dockHost.QueueMoveActivity(this, dockHostActor, self, dockClient)) + { + foreach (var n in notifyHarvesterActions) + n.MovingToRefinery(self, dockHostActor); + + return false; + } + + dockHost.QueueDockActivity(this, dockHostActor, self, dockClient); + return true; + } + else + { + // The dock explicitely chosen by the user is currently occupied. Wait and check again. + QueueChild(new Wait(dockClient.Info.SearchForDockDelay)); + return false; + } } public override void Cancel(Actor self, bool keepQueue = false) { + dockClient.UnreserveHost(); foreach (var n in notifyHarvesterActions) n.MovementCancelled(self); @@ -87,10 +92,13 @@ namespace OpenRA.Mods.Common.Activities public override IEnumerable TargetLineNodes(Actor self) { - if (proc != null) - yield return new TargetLineNode(Target.FromActor(proc), harv.Info.DeliverLineColor); + if (dockHostActor != null) + yield return new TargetLineNode(Target.FromActor(dockHostActor), dockClient.DockLineColor); else - yield return new TargetLineNode(Target.FromActor(harv.LinkedProc), harv.Info.DeliverLineColor); + { + if (dockClient.ReservedHostActor != null) + yield return new TargetLineNode(Target.FromActor(dockClient.ReservedHostActor), dockClient.DockLineColor); + } } } } diff --git a/OpenRA.Mods.Common/Traits/Buildings/Refinery.cs b/OpenRA.Mods.Common/Traits/Buildings/Refinery.cs index 1b8f0c2545..10db8db5ff 100644 --- a/OpenRA.Mods.Common/Traits/Buildings/Refinery.cs +++ b/OpenRA.Mods.Common/Traits/Buildings/Refinery.cs @@ -12,32 +12,14 @@ using System; using System.Collections.Generic; using System.Linq; -using OpenRA.Activities; -using OpenRA.Mods.Common.Activities; using OpenRA.Mods.Common.Effects; using OpenRA.Mods.Common.Traits.Render; -using OpenRA.Primitives; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { - public class RefineryInfo : TraitInfo, Requires, IAcceptResourcesInfo + public class RefineryInfo : TraitInfo, Requires, Requires { - [Desc("Actual harvester facing when docking.")] - public readonly WAngle DockAngle = WAngle.Zero; - - [Desc("Docking cell relative to top-left cell.")] - public readonly CVec DockOffset = CVec.Zero; - - [Desc("Does the refinery require the harvester to be dragged in?")] - public readonly bool IsDragRequired = false; - - [Desc("Vector by which the harvester will be dragged when docking.")] - public readonly WVec DragOffset = WVec.Zero; - - [Desc("In how many steps to perform the dragging?")] - public readonly int DragLength = 0; - [Desc("Store resources in silos. Adds cash directly without storing if set to false.")] public readonly bool UseStorage = true; @@ -50,33 +32,16 @@ namespace OpenRA.Mods.Common.Traits public override object Create(ActorInitializer init) { return new Refinery(init.Self, this); } } - public class Refinery : INotifyCreated, ITick, IAcceptResources, INotifySold, INotifyCapture, - INotifyOwnerChanged, ISync, INotifyActorDisposing + public class Refinery : IAcceptResources, INotifyCreated, ITick, INotifyOwnerChanged { - readonly Actor self; readonly RefineryInfo info; PlayerResources playerResources; IEnumerable resourceValueModifiers; int currentDisplayTick = 0; int currentDisplayValue = 0; - - [Sync] - Actor dockedHarv = null; - - [Sync] - bool preventDock = false; - - public bool AllowDocking => !preventDock; - public WPos DeliveryPosition => self.World.Map.CenterOfCell(self.Location + info.DockOffset); - public WAngle DeliveryAngle => info.DockAngle; - public bool IsDragRequired => info.IsDragRequired; - public WVec DragOffset => info.DragOffset; - public int DragLength => info.DragLength; - public Refinery(Actor self, RefineryInfo info) { - this.self = self; this.info = info; playerResources = self.Owner.PlayerActor.Trait(); currentDisplayTick = info.TickRate; @@ -87,13 +52,7 @@ namespace OpenRA.Mods.Common.Traits resourceValueModifiers = self.TraitsImplementing().ToArray().Select(m => m.GetResourceValueModifier()); } - public IEnumerable> GetLinkedHarvesters() - { - return self.World.ActorsWithTrait() - .Where(a => a.Trait.LinkedProc == self); - } - - int IAcceptResources.AcceptResources(string resourceType, int count) + int IAcceptResources.AcceptResources(Actor self, string resourceType, int count) { if (!playerResources.Info.ResourceValues.TryGetValue(resourceType, out var resourceValue)) return 0; @@ -131,17 +90,8 @@ namespace OpenRA.Mods.Common.Traits return count; } - void CancelDock() - { - preventDock = true; - } - void ITick.Tick(Actor self) { - // Harvester was killed while unloading - if (dockedHarv != null && dockedHarv.IsDead) - dockedHarv = null; - if (info.ShowTicks && currentDisplayValue > 0 && --currentDisplayTick <= 0) { var temp = currentDisplayValue; @@ -152,49 +102,9 @@ namespace OpenRA.Mods.Common.Traits } } - void INotifyActorDisposing.Disposing(Actor self) - { - CancelDock(); - foreach (var harv in GetLinkedHarvesters()) - harv.Trait.UnlinkProc(harv.Actor, self); - } - - public void OnDock(Actor harv, MoveToDock dockOrder) - { - if (!preventDock) - { - dockOrder.QueueChild(new CallFunc(() => dockedHarv = harv, false)); - dockOrder.QueueChild(new HarvesterDockSequence(harv, self, this)); - dockOrder.QueueChild(new CallFunc(() => dockedHarv = null, false)); - } - } - void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) { - // Unlink any harvesters - foreach (var harv in GetLinkedHarvesters()) - harv.Trait.UnlinkProc(harv.Actor, self); - playerResources = newOwner.PlayerActor.Trait(); } - - void INotifyCapture.OnCapture(Actor self, Actor captor, Player oldOwner, Player newOwner, BitSet captureTypes) - { - // Steal any docked harv too - if (dockedHarv != null) - { - dockedHarv.ChangeOwner(newOwner); - - // Relink to this refinery - dockedHarv.Trait().LinkProc(self); - } - } - - void INotifySold.Selling(Actor self) { CancelDock(); } - void INotifySold.Sold(Actor self) - { - foreach (var harv in GetLinkedHarvesters()) - harv.Trait.UnlinkProc(harv.Actor, self); - } } } diff --git a/OpenRA.Mods.Common/Traits/CarryableHarvester.cs b/OpenRA.Mods.Common/Traits/CarryableHarvester.cs index 2485c96d5e..3b80bc02ca 100644 --- a/OpenRA.Mods.Common/Traits/CarryableHarvester.cs +++ b/OpenRA.Mods.Common/Traits/CarryableHarvester.cs @@ -36,10 +36,9 @@ namespace OpenRA.Mods.Common.Traits void INotifyHarvesterAction.MovingToRefinery(Actor self, Actor refineryActor) { - var iao = refineryActor.Trait(); - var location = self.World.Map.CellContaining(iao.DeliveryPosition); + var dock = refineryActor.Trait(); foreach (var t in transports) - t.RequestTransport(self, location); + t.RequestTransport(self, self.World.Map.CellContaining(dock.DockPosition)); } void INotifyHarvesterAction.MovementCancelled(Actor self) diff --git a/OpenRA.Mods.Common/Traits/DockClientBase.cs b/OpenRA.Mods.Common/Traits/DockClientBase.cs new file mode 100644 index 0000000000..4b974fa257 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/DockClientBase.cs @@ -0,0 +1,54 @@ +#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 OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + public abstract class DockClientBaseInfo : ConditionalTraitInfo, IDockClientInfo, Requires { } + + public abstract class DockClientBase : ConditionalTrait, IDockClient, INotifyCreated where InfoType : DockClientBaseInfo + { + readonly Actor self; + + public abstract BitSet GetDockType { get; } + public DockClientManager DockClientManager { get; } + + protected DockClientBase(Actor self, InfoType info) + : base(info) + { + this.self = self; + DockClientManager = self.Trait(); + } + + protected virtual bool CanDock() + { + return true; + } + + public virtual bool IsDockingPossible(BitSet type, bool forceEnter = false) + { + return !IsTraitDisabled && GetDockType.Overlaps(type) && (forceEnter || CanDock()); + } + + public virtual bool CanDockAt(Actor hostActor, IDockHost host, bool forceEnter = false, bool ignoreOccupancy = false) + { + return (forceEnter || self.Owner.IsAlliedWith(hostActor.Owner)) && IsDockingPossible(host.GetDockType, forceEnter) && host.IsDockingPossible(self, this, ignoreOccupancy); + } + + public virtual void OnDockStarted(Actor self, Actor hostActor, IDockHost host) { } + + public virtual bool OnDockTick(Actor self, Actor hostActor, IDockHost host) { return false; } + + public virtual void OnDockCompleted(Actor self, Actor hostActor, IDockHost host) { } + } +} diff --git a/OpenRA.Mods.Common/Traits/DockClientManager.cs b/OpenRA.Mods.Common/Traits/DockClientManager.cs new file mode 100644 index 0000000000..5ab8081791 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/DockClientManager.cs @@ -0,0 +1,334 @@ +#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.Linq; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Orders; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [Desc("Manages DockClients on the actor.")] + public class DockClientManagerInfo : ConditionalTraitInfo + { + [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; + + [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; + + 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 EnterAlliedActorTargeter( + "ForceDock", + 6, + Info.EnterCursor, + Info.EnterBlockedCursor, + DockingPossible, + target => CanDockAt(target, true, true)); + yield return new EnterAlliedActorTargeter( + "Dock", + 5, + Info.EnterCursor, + Info.EnterBlockedCursor, + (actor, modifiers) => DockingPossible(actor), + target => CanDockAt(target, false, true)); + } + } + + void IResolveOrder.ResolveOrder(Actor self, Order order) + { + if (order.OrderString == "Dock") + { + 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; + + if (IsTraitDisabled) + return; + + var dock = AvailableDockHosts(target.Actor, false, true).ClosestDock(self, this); + if (!dock.HasValue) + return; + + self.QueueActivity(order.Queued, new MoveToDock(self, dock.Value.Actor, dock.Value.Trait)); + self.ShowTargetLines(); + } + else if (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; + + if (IsTraitDisabled) + return; + + var dock = AvailableDockHosts(target.Actor, true, true).ClosestDock(self, this); + if (!dock.HasValue) + return; + + self.QueueActivity(order.Queued, new MoveToDock(self, dock.Value.Actor, dock.Value.Trait)); + self.ShowTargetLines(); + } + } + + string IOrderVoice.VoicePhraseForOrder(Actor self, Order order) + { + if (order.Target.Type != TargetType.Actor || IsTraitDisabled) + return null; + + if (order.OrderString == "Dock" && CanDockAt(order.Target.Actor, false, true)) + return Info.Voice; + else if (order.OrderString == "ForceDock" && CanDockAt(order.Target.Actor, true, true)) + 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; + } + + /// Do we have an enabled client with matching . + public bool DockingPossible(BitSet type, bool forceEnter = false) + { + return !IsTraitDisabled && dockClients.Any(client => client.IsDockingPossible(type, forceEnter)); + } + + /// Does this contain at least one enabled with maching . + public bool DockingPossible(Actor target) + { + return !IsTraitDisabled && target.TraitsImplementing().Any(host => dockClients.Any(client => client.IsDockingPossible(host.GetDockType))); + } + + /// Does this contain at least one enabled with maching . + public bool DockingPossible(Actor target, TargetModifiers modifiers) + { + var forceEnter = modifiers.HasModifier(TargetModifiers.ForceMove); + return !IsTraitDisabled && target.TraitsImplementing().Any(host => dockClients.Any(client => client.IsDockingPossible(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))); + } + + /// 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); + 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 . + /// Does not check if is enabled. + public IEnumerable> AvailableDockHosts(Actor target, bool forceEnter = false, bool ignoreOccupancy = false) + { + return target.TraitsImplementing() + .Where(host => dockClients.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) + { + return dockClients.Where(client => client.IsDockingPossible(type)); + } + + void INotifyKilled.Killed(Actor self, AttackInfo e) { UnreserveHost(); } + + void INotifyActorDisposing.Disposing(Actor self) { UnreserveHost(); } + } + + public static class DockExts + { + public static TraitPair? ClosestDock(this IEnumerable> docks, Actor clientActor, DockClientManager client) + { + var mobile = clientActor.TraitOrDefault(); + if (mobile != null) + { + // Overlapping docks can become hidden. + var lookup = docks.ToDictionary(dock => clientActor.World.Map.CellContaining(dock.Trait.DockPosition)); + + // Start a search from each docks position: + var path = mobile.PathFinder.FindPathToTargetCell( + clientActor, lookup.Keys, clientActor.Location, BlockedByActor.None, + location => + { + if (!lookup.ContainsKey(location)) + return 0; + + var dock = lookup[location]; + + // Prefer docks with less occupancy (multiplier is to offset distance cost): + // TODO: add custom wieghts. 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; + } + } +} diff --git a/OpenRA.Mods.Common/Traits/DockHost.cs b/OpenRA.Mods.Common/Traits/DockHost.cs new file mode 100644 index 0000000000..2c00b156f1 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/DockHost.cs @@ -0,0 +1,183 @@ +#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 OpenRA.Activities; +using OpenRA.Mods.Common.Activities; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + public sealed class DockType { DockType() { } } + + [Desc("A generic dock that services DockClients.")] + public class DockHostInfo : ConditionalTraitInfo, IDockHostInfo + { + [Desc("Docking type.")] + public readonly BitSet Type; + + [Desc("How many clients can this dock be reserved for?")] + public readonly int MaxQueueLength = 3; + + [Desc("How long should the client wait before starting the docking sequence.")] + public readonly int DockWait = 10; + + [Desc("Actual client facing when docking.")] + public readonly WAngle DockAngle = WAngle.Zero; + + [Desc("Docking cell relative to the centre of the actor.")] + public readonly WVec DockOffset = WVec.Zero; + + [Desc("Does client need to be dragged in?")] + public readonly bool IsDragRequired = false; + + [Desc("Vector by which the client will be dragged when docking.")] + public readonly WVec DragOffset = WVec.Zero; + + [Desc("In how many steps to perform the dragging?")] + public readonly int DragLength = 0; + + public override object Create(ActorInitializer init) { return new DockHost(init.Self, this); } + } + + public class DockHost : ConditionalTrait, IDockHost, IDockHostDrag, ITick, INotifySold, INotifyCapture, INotifyOwnerChanged, ISync, INotifyKilled, INotifyActorDisposing + { + readonly Actor self; + + public BitSet GetDockType => Info.Type; + public bool IsEnabledAndInWorld => !preventDock && !IsTraitDisabled && !self.IsDead && self.IsInWorld; + public int ReservationCount => ReservedDockClients.Count; + public bool CanBeReserved => ReservationCount < Info.MaxQueueLength; + protected readonly List ReservedDockClients = new(); + + public WPos DockPosition => self.CenterPosition + Info.DockOffset; + public int DockWait => Info.DockWait; + public WAngle DockAngle => Info.DockAngle; + + bool IDockHostDrag.IsDragRequired => Info.IsDragRequired; + WVec IDockHostDrag.DragOffset => Info.DragOffset; + int IDockHostDrag.DragLength => Info.DragLength; + + [Sync] + bool preventDock = false; + + [Sync] + protected Actor dockedClientActor = null; + protected DockClientManager dockedClient = null; + + public DockHost(Actor self, DockHostInfo info) + : base(info) + { + this.self = self; + } + + public virtual bool IsDockingPossible(Actor clientActor, IDockClient client, bool ignoreReservations = false) + { + return !IsTraitDisabled && (ignoreReservations || CanBeReserved || ReservedDockClients.Contains(client.DockClientManager)); + } + + public virtual bool Reserve(Actor self, DockClientManager client) + { + if (CanBeReserved && !ReservedDockClients.Contains(client)) + { + ReservedDockClients.Add(client); + client.ReserveHost(self, this); + return true; + } + + return false; + } + + public virtual void UnreserveAll() + { + while (ReservedDockClients.Count > 0) + Unreserve(ReservedDockClients[0]); + } + + public virtual void Unreserve(DockClientManager client) + { + if (ReservedDockClients.Contains(client)) + { + ReservedDockClients.Remove(client); + client.UnreserveHost(); + } + } + + public virtual void OnDockStarted(Actor self, Actor clientActor, DockClientManager client) + { + dockedClientActor = clientActor; + dockedClient = client; + } + + public virtual void OnDockCompleted(Actor self, Actor clientActor, DockClientManager client) + { + dockedClientActor = null; + dockedClient = null; + } + + void ITick.Tick(Actor self) + { + Tick(self); + } + + protected virtual void Tick(Actor self) + { + // Client was killed during docking. + if (dockedClientActor != null && (dockedClientActor.IsDead || !dockedClientActor.IsInWorld)) + OnDockCompleted(self, dockedClientActor, dockedClient); + } + + public virtual bool QueueMoveActivity(Activity moveToDockActivity, Actor self, Actor clientActor, DockClientManager client) + { + var move = clientActor.Trait(); + + // Make sure the actor is at dock, at correct facing, and aircraft are landed. + // Mobile cannot freely move in WPos, so when we calculate close enough we convert to CPos. + if ((move is Mobile ? clientActor.Location != clientActor.World.Map.CellContaining(DockPosition) : clientActor.CenterPosition != DockPosition) + || move is not IFacing facing || facing.Facing != DockAngle) + { + moveToDockActivity.QueueChild(move.MoveOntoTarget(clientActor, Target.FromActor(self), DockPosition - self.CenterPosition, DockAngle)); + return true; + } + + return false; + } + + public virtual void QueueDockActivity(Activity moveToDockActivity, Actor self, Actor clientActor, DockClientManager client) + { + moveToDockActivity.QueueChild(new GenericDockSequence(clientActor, client, self, this)); + } + + protected override void TraitDisabled(Actor self) { UnreserveAll(); } + + void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) { UnreserveAll(); } + + void INotifyCapture.OnCapture(Actor self, Actor captor, Player oldOwner, Player newOwner, BitSet captureTypes) + { + // Steal any docked unit too. + if (dockedClientActor != null && !dockedClientActor.IsDead && dockedClientActor.IsInWorld) + { + dockedClientActor.ChangeOwner(newOwner); + + // On capture OnOwnerChanged event is called first, so we need to re-reserve. + dockedClient.ReserveHost(self, this); + } + } + + void INotifySold.Selling(Actor self) { preventDock = true; } + + void INotifySold.Sold(Actor self) { UnreserveAll(); } + + void INotifyKilled.Killed(Actor self, AttackInfo e) { UnreserveAll(); } + + void INotifyActorDisposing.Disposing(Actor self) { preventDock = true; UnreserveAll(); } + } +} diff --git a/OpenRA.Mods.Common/Traits/Harvester.cs b/OpenRA.Mods.Common/Traits/Harvester.cs index e0d77f41b7..1df1c284b7 100644 --- a/OpenRA.Mods.Common/Traits/Harvester.cs +++ b/OpenRA.Mods.Common/Traits/Harvester.cs @@ -14,18 +14,15 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using OpenRA.Mods.Common.Activities; -using OpenRA.Mods.Common.Orders; using OpenRA.Primitives; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { - public class HarvesterInfo : ConditionalTraitInfo, Requires + public class HarvesterInfo : DockClientBaseInfo, Requires { - public readonly HashSet DeliveryBuildings = new(); - - [Desc("How long (in ticks) to wait until (re-)checking for a nearby available DeliveryBuilding if not yet linked to one.")] - public readonly int SearchForDeliveryBuildingDelay = 125; + [Desc("Docking type")] + public readonly BitSet Type = new("Unload"); [Desc("Cell to move to when automatically unblocking DeliveryBuilding.")] public readonly CVec UnblockCell = new(0, 4); @@ -61,12 +58,6 @@ namespace OpenRA.Mods.Common.Traits [Desc("Interval to wait between searches when there are no resources nearby.")] public readonly int WaitDuration = 25; - [Desc("Find a new refinery to unload at if more than this many harvesters are already waiting.")] - public readonly int MaxUnloadQueue = 3; - - [Desc("The pathfinding cost penalty applied for each harvester waiting to unload at a refinery.")] - public readonly int UnloadQueueCostModifier = 12; - [Desc("The pathfinding cost penalty applied for cells directly away from the refinery.")] public readonly int ResourceRefineryDirectionPenalty = 200; @@ -80,23 +71,9 @@ namespace OpenRA.Mods.Common.Traits [VoiceReference] public readonly string HarvestVoice = "Action"; - [VoiceReference] - public readonly string DeliverVoice = "Action"; - [Desc("Color to use for the target line of harvest orders.")] public readonly Color HarvestLineColor = Color.Crimson; - [Desc("Color to use for the target line of harvest orders.")] - public readonly Color DeliverLineColor = Color.Green; - - [CursorReference] - [Desc("Cursor to display when able to unload at target actor.")] - public readonly string EnterCursor = "enter"; - - [CursorReference] - [Desc("Cursor to display when unable to unload at target actor.")] - public readonly string EnterBlockedCursor = "enter-blocked"; - [CursorReference] [Desc("Cursor to display when ordering to harvest resources.")] public readonly string HarvestCursor = "harvest"; @@ -104,7 +81,7 @@ namespace OpenRA.Mods.Common.Traits public override object Create(ActorInitializer init) { return new Harvester(init.Self, this); } } - public class Harvester : ConditionalTrait, IIssueOrder, IResolveOrder, IOrderVoice, + public class Harvester : DockClientBase, IIssueOrder, IResolveOrder, IOrderVoice, ISpeedModifier, ISync, INotifyCreated { public readonly IReadOnlyDictionary Contents; @@ -115,11 +92,7 @@ namespace OpenRA.Mods.Common.Traits readonly Dictionary contents = new(); int conditionToken = Actor.InvalidConditionToken; - [Sync] - public Actor LastLinkedProc = null; - - [Sync] - public Actor LinkedProc = null; + public override BitSet GetDockType => Info.Type; [Sync] int currentUnloadTicks; @@ -137,7 +110,7 @@ namespace OpenRA.Mods.Common.Traits } public Harvester(Actor self, HarvesterInfo info) - : base(info) + : base(self, info) { Contents = new ReadOnlyDictionary(contents); mobile = self.Trait(); @@ -156,70 +129,15 @@ namespace OpenRA.Mods.Common.Traits base.Created(self); } - public void LinkProc(Actor proc) - { - LinkedProc = proc; - } - - public void UnlinkProc(Actor self, Actor proc) - { - if (LinkedProc == proc) - ChooseNewProc(self, proc); - } - - public void ChooseNewProc(Actor self, Actor ignore) - { - LastLinkedProc = null; - LinkProc(ClosestProc(self, ignore)); - } - - bool IsAcceptableProcType(Actor proc) - { - return Info.DeliveryBuildings.Count == 0 || - Info.DeliveryBuildings.Contains(proc.Info.Name); - } - - public Actor ClosestProc(Actor self, Actor ignore) - { - // Find all refineries and their occupancy count: - // Exclude refineries with too many harvesters clogging the delivery location. - var refineries = self.World.ActorsWithTrait() - .Where(r => r.Actor != ignore && r.Actor.Owner == self.Owner && IsAcceptableProcType(r.Actor)) - .Select(r => new - { - Location = r.Actor.World.Map.CellContaining(r.Trait.DeliveryPosition), - Actor = r.Actor, - Occupancy = self.World.ActorsHavingTrait(h => h.LinkedProc == r.Actor).Count() - }) - .Where(r => r.Occupancy < Info.MaxUnloadQueue) - .ToDictionary(r => r.Location); - - if (refineries.Count == 0) - return null; - - // Start a search from each refinery's delivery location: - var path = mobile.PathFinder.FindPathToTargetCells( - self, self.Location, refineries.Select(r => r.Key), BlockedByActor.None, - location => - { - if (!refineries.ContainsKey(location)) - return 0; - - // Prefer refineries with less occupancy (multiplier is to offset distance cost): - var occupancy = refineries[location].Occupancy; - return occupancy * Info.UnloadQueueCostModifier; - }); - - if (path.Count > 0) - return refineries[path[0]].Actor; - - return null; - } - public bool IsFull => contents.Values.Sum() == Info.Capacity; public bool IsEmpty => contents.Values.Sum() == 0; public int Fullness => contents.Values.Sum() * 100 / Info.Capacity; + protected override bool CanDock() + { + return !IsEmpty; + } + void UpdateCondition(Actor self) { if (string.IsNullOrEmpty(Info.EmptyCondition)) @@ -243,21 +161,29 @@ namespace OpenRA.Mods.Common.Traits UpdateCondition(self); } - // Returns true when unloading is complete - public virtual bool TickUnload(Actor self, Actor proc) + IAcceptResources acceptResources; + public override void OnDockStarted(Actor self, Actor hostActor, IDockHost host) { + if (IsDockingPossible(host.GetDockType)) + acceptResources = hostActor.TraitOrDefault(); + } + + public override bool OnDockTick(Actor self, Actor hostActor, IDockHost host) + { + if (acceptResources == null || IsTraitDisabled) + return true; + // Wait until the next bale is ready if (--currentUnloadTicks > 0) return false; if (contents.Keys.Count > 0) { - var acceptResources = proc.Trait(); foreach (var c in contents) { var resourceType = c.Key; var count = Math.Min(c.Value, Info.BaleUnloadAmount); - var accepted = acceptResources.AcceptResources(resourceType, count); + var accepted = acceptResources.AcceptResources(hostActor, resourceType, count); if (accepted == 0) continue; @@ -274,6 +200,19 @@ namespace OpenRA.Mods.Common.Traits return contents.Count == 0; } + public override void OnDockCompleted(Actor self, Actor hostActor, IDockHost dock) + { + acceptResources = null; + + // After having docked at a refinery make sure we are running FindAndDeliverResources activity. + if (GetDockType.Overlaps(dock.GetDockType)) + { + var currentActivity = self.CurrentActivity; + if (currentActivity == null || (currentActivity is not FindAndDeliverResources && currentActivity.NextActivity == null)) + self.QueueActivity(true, new FindAndDeliverResources(self)); + } + } + public bool CanHarvestCell(CPos cell) { // Resources only exist in the ground layer @@ -295,20 +234,13 @@ namespace OpenRA.Mods.Common.Traits if (IsTraitDisabled) yield break; - yield return new EnterAlliedActorTargeter( - "Deliver", - 5, - Info.EnterCursor, - Info.EnterBlockedCursor, - (proc, _) => IsAcceptableProcType(proc), - proc => proc.Trait().AllowDocking); yield return new HarvestOrderTargeter(); } } Order IIssueOrder.IssueOrder(Actor self, IOrderTargeter order, in Target target, bool queued) { - if (order.OrderID == "Deliver" || order.OrderID == "Harvest") + if (order.OrderID == "Harvest") return new Order(order.OrderID, self, target, queued); return null; @@ -319,9 +251,6 @@ namespace OpenRA.Mods.Common.Traits if (order.OrderString == "Harvest") return Info.HarvestVoice; - if (order.OrderString == "Deliver" && !IsEmpty) - return Info.DeliverVoice; - return null; } @@ -329,9 +258,6 @@ namespace OpenRA.Mods.Common.Traits { if (order.OrderString == "Harvest") { - // NOTE: An explicit harvest order allows the harvester to decide which refinery to deliver to. - LinkProc(null); - CPos loc; if (order.Target.Type != TargetType.Invalid) { @@ -349,21 +275,6 @@ namespace OpenRA.Mods.Common.Traits self.QueueActivity(order.Queued, new FindAndDeliverResources(self, loc)); self.ShowTargetLines(); } - else if (order.OrderString == "Deliver") - { - // Deliver orders are only valid for own/allied actors, - // which are guaranteed to never be frozen. - if (order.Target.Type != TargetType.Actor) - return; - - var targetActor = order.Target.Actor; - var iao = targetActor.TraitOrDefault(); - if (iao == null || !iao.AllowDocking || !IsAcceptableProcType(targetActor)) - return; - - self.QueueActivity(order.Queued, new FindAndDeliverResources(self, targetActor)); - self.ShowTargetLines(); - } } int ISpeedModifier.GetSpeedModifier() @@ -373,8 +284,7 @@ namespace OpenRA.Mods.Common.Traits protected override void TraitDisabled(Actor self) { - LastLinkedProc = null; - LinkedProc = null; + base.TraitDisabled(self); contents.Clear(); if (conditionToken != Actor.InvalidConditionToken) diff --git a/OpenRA.Mods.Common/TraitsInterfaces.cs b/OpenRA.Mods.Common/TraitsInterfaces.cs index ab0246c1d8..7407d2cc78 100644 --- a/OpenRA.Mods.Common/TraitsInterfaces.cs +++ b/OpenRA.Mods.Common/TraitsInterfaces.cs @@ -13,7 +13,6 @@ using System; using System.Collections.Generic; using OpenRA.Activities; using OpenRA.Graphics; -using OpenRA.Mods.Common.Activities; using OpenRA.Mods.Common.Graphics; using OpenRA.Mods.Common.Terrain; using OpenRA.Mods.Common.Widgets; @@ -211,6 +210,72 @@ namespace OpenRA.Mods.Common.Traits void Harvested(Actor self, string resourceType); } + public interface IDockClientInfo : ITraitInfoInterface { } + + public interface IDockClient + { + BitSet GetDockType { get; } + DockClientManager DockClientManager { get; } + void OnDockStarted(Actor self, Actor hostActor, IDockHost host); + bool OnDockTick(Actor self, Actor hostActor, IDockHost dock); + void OnDockCompleted(Actor self, Actor hostActor, IDockHost host); + + /// Is this client allowed to dock. + /// + /// Does not check if is enabled. + /// Function should only be called from within or . + /// + bool IsDockingPossible(BitSet type, bool forceEnter = false); + + /// Is this client allowed to dock to . + /// + /// Does not check if is enabled. + /// Function should only be called from within or . + /// + bool CanDockAt(Actor hostActor, IDockHost host, bool forceEnter = false, bool ignoreOccupancy = false); + } + + public interface IDockHostInfo : ITraitInfoInterface { } + + public interface IDockHost + { + BitSet GetDockType { get; } + + /// Use this function instead of ConditionalTrait.IsTraitDisabled. + bool IsEnabledAndInWorld { get; } + int ReservationCount { get; } + bool CanBeReserved { get; } + WPos DockPosition { get; } + int DockWait { get; } + WAngle DockAngle { get; } + + /// Can this dock at this . + /// + /// Does not check . + /// Does not check if is enabled. + /// Does not check if is enabled. + /// + bool IsDockingPossible(Actor clientActor, IDockClient client, bool ignoreReservations = false); + bool Reserve(Actor self, DockClientManager client); + void UnreserveAll(); + void Unreserve(DockClientManager client); + void OnDockStarted(Actor self, Actor clientActor, DockClientManager client); + void OnDockCompleted(Actor self, Actor clientActor, DockClientManager client); + + /// If is not in range of queues a child move activity and returns true. If in range returns false. + bool QueueMoveActivity(Activity moveToDockActivity, Actor self, Actor clientActor, DockClientManager client); + + /// Should be called when in range of . + void QueueDockActivity(Activity moveToDockActivity, Actor self, Actor clientActor, DockClientManager client); + } + + public interface IDockHostDrag + { + bool IsDragRequired { get; } + WVec DragOffset { get; } + int DragLength { get; } + } + [RequireExplicitImplementation] public interface INotifyLoadCargo { @@ -280,14 +345,9 @@ namespace OpenRA.Mods.Common.Traits void Undeploy(Actor self, bool skipMakeAnim); } - public interface IAcceptResourcesInfo : ITraitInfoInterface { } public interface IAcceptResources { - void OnDock(Actor harv, MoveToDock dockOrder); - int AcceptResources(string resourceType, int count = 1); - WPos DeliveryPosition { get; } - WAngle DeliveryAngle { get; } - bool AllowDocking { get; } + int AcceptResources(Actor self, string resourceType, int count = 1); } public interface IDockClientBody diff --git a/OpenRA.Mods.Common/UpdateRules/Rules/20230801/AbstractDocking.cs b/OpenRA.Mods.Common/UpdateRules/Rules/20230801/AbstractDocking.cs new file mode 100644 index 0000000000..017a6154c5 --- /dev/null +++ b/OpenRA.Mods.Common/UpdateRules/Rules/20230801/AbstractDocking.cs @@ -0,0 +1,166 @@ +#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.Linq; + +namespace OpenRA.Mods.Common.UpdateRules.Rules +{ + public class AbstractDocking : UpdateRule, IBeforeUpdateActors + { + readonly string[] moveRefineyValues = { "DockAngle", "IsDragRequired", "DragOffset", "DragLength" }; + readonly string[] moveHarvesterValues = { "EnterCursor", "EnterBlockedCursor" }; + readonly string[] buildings = { "Building", "D2kBuilding" }; + readonly string[,] moveAndRenameHarvesterValues = new string[4, 2] + { + { "DeliverVoice", "Voice" }, + { "DeliverLineColor", "DockLineColor" }, + { "UnloadQueueCostModifier", "OccupancyCostModifier" }, + { "SearchForDeliveryBuildingDelay", "SearchForDockDelay" } + }; + + readonly Dictionary> refineryNodes = new(); + public override string Name => "Docking was abstracted from Refinery & Harvester."; + + public override string Description => + "Fields moved from Refinery to new trait DockHost, fields moved from Harvester to new trait DockClientManager and to DockHost"; + + public IEnumerable BeforeUpdateActors(ModData modData, List resolvedActors) + { + grid = modData.Manifest.Get(); + var harvesters = new Dictionary>(); + var refineries = new List(); + foreach (var actorNode in resolvedActors) + { + var harvesterNode = actorNode.ChildrenMatching("Harvester", includeRemovals: false).FirstOrDefault(); + if (harvesterNode != null) + harvesters[actorNode.Key] = harvesterNode.ChildrenMatching("DeliveryBuildings", includeRemovals: false) + .FirstOrDefault()?.NodeValue>() ?? new HashSet(); + + if (actorNode.ChildrenMatching("Refinery", includeRemovals: false).FirstOrDefault() != null) + refineries.Add(actorNode.Key.ToLowerInvariant()); + } + + foreach (var harvester in harvesters) + { + foreach (var deliveryBuildingHigh in harvester.Value) + { + var deliveryBuilding = deliveryBuildingHigh.ToLowerInvariant(); + foreach (var refinery in refineries) + { + if (refinery == deliveryBuilding) + { + if (!refineryNodes.ContainsKey(refinery)) + refineryNodes[refinery] = new List(); + + var node = new MiniYamlNodeBuilder("Type", deliveryBuilding.ToString()); + if (!refineryNodes[refinery].Any(n => n.Key == node.Key)) + refineryNodes[refinery].Add(node); + } + } + } + } + + yield break; + } + + public override IEnumerable UpdateActorNode(ModData modData, MiniYamlNodeBuilder actorNode) + { + var refineryNode = actorNode.ChildrenMatching("Refinery", includeRemovals: false).FirstOrDefault(); + if (refineryNode != null) + { + var dockNode = new MiniYamlNodeBuilder("DockHost", ""); + + var lowActorName = actorNode.Key.ToLowerInvariant(); + if (!refineryNodes.ContainsKey(lowActorName) || !refineryNodes[lowActorName].Any(n => n.Key == "Type")) + dockNode.AddNode("Type", "Unload"); + else + dockNode.AddNode(refineryNodes[lowActorName].First(n => n.Key == "Type")); + + foreach (var value in moveRefineyValues) + { + foreach (var node in refineryNode.ChildrenMatching(value).ToList()) + { + dockNode.AddNode(node); + refineryNode.RemoveNode(node); + } + } + + var oldOffset = CVec.Zero; + var dockOffsetNode = refineryNode.ChildrenMatching("DockOffset", includeRemovals: false).FirstOrDefault(); + if (dockOffsetNode != null) + { + oldOffset = dockOffsetNode.NodeValue(); + refineryNode.RemoveNode(dockOffsetNode); + } + + var buildingNode = actorNode.Value.Nodes.FirstOrDefault(n => buildings.Any(b => n.KeyMatches(b, includeRemovals: false))); + if (buildingNode != null) + { + var dimensions = buildingNode.ChildrenMatching("Dimensions", includeRemovals: false).FirstOrDefault()?.NodeValue() ?? new CVec(1, 1); + var localCenterOffset = buildingNode.ChildrenMatching("LocalCenterOffset", includeRemovals: false).FirstOrDefault()?.NodeValue() ?? WVec.Zero; + + var offset = CenterOfCell(oldOffset) - CenterOfCell(CVec.Zero) - BuildingCenter(dimensions, localCenterOffset); + if (offset != WVec.Zero) + dockNode.AddNode("DockOffset", offset); + } + + actorNode.AddNode(dockNode); + } + + var harvesterNode = actorNode.ChildrenMatching("Harvester", includeRemovals: false).FirstOrDefault(); + if (harvesterNode != null) + { + var dockClientNode = new MiniYamlNodeBuilder("DockClientManager", ""); + + foreach (var value in moveHarvesterValues) + { + foreach (var node in harvesterNode.ChildrenMatching(value).ToList()) + { + dockClientNode.AddNode(node); + harvesterNode.RemoveNode(node); + } + } + + for (var i = 0; i < moveAndRenameHarvesterValues.GetLength(0); i++) + { + foreach (var node in harvesterNode.ChildrenMatching(moveAndRenameHarvesterValues[i, 0]).ToList()) + { + harvesterNode.RemoveNode(node); + node.RenameKey(moveAndRenameHarvesterValues[i, 1]); + dockClientNode.AddNode(node); + } + } + + harvesterNode.RenameChildrenMatching("DeliveryBuildings", "DockType"); + harvesterNode.RemoveNodes("MaxUnloadQueue"); + + actorNode.AddNode(dockClientNode); + } + + yield break; + } + + MapGrid grid; + public WVec CenterOfCell(CVec cell) + { + if (grid.Type == MapGridType.Rectangular) + return new WVec(1024 * cell.X + 512, 1024 * cell.Y + 512, 0); + + return new WVec(724 * (cell.X - cell.Y + 1), 724 * (cell.X + cell.Y + 1), 0); + } + + public WVec BuildingCenter(CVec dimensions, WVec localCenterOffset) + { + return (CenterOfCell(dimensions) - CenterOfCell(new CVec(1, 1))) / 2 + localCenterOffset; + } + } +} diff --git a/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs b/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs index fd9ef0a10e..627bd19746 100644 --- a/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs +++ b/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs @@ -88,9 +88,8 @@ namespace OpenRA.Mods.Common.UpdateRules new UnhardcodeBaseBuilderBotModule(), }), - new UpdatePath("release-20230225", new UpdateRule[] + new UpdatePath("release-20230225", "playtest-20230801", new UpdateRule[] { - // bleed only changes here new TextNotificationsDisplayWidgetRemoveTime(), new RenameEngineerRepair(), new ProductionTabsWidgetAddTabButtonCollection(), @@ -104,7 +103,15 @@ namespace OpenRA.Mods.Common.UpdateRules new ExplicitSequenceFilenames(), new RemoveSequenceHasEmbeddedPalette(), new RemoveNegativeSequenceLength(), - }) + }), + + new UpdatePath("playtest-20230801", new UpdateRule[] + { + // bleed only changes here. + + // Execute these rules last to avoid premature yaml merge crashes. + new AbstractDocking(), + }), }; public static IEnumerable FromSource(ObjectCreator objectCreator, string source, bool chain = true) diff --git a/mods/cnc/rules/structures.yaml b/mods/cnc/rules/structures.yaml index 93ddb2d69c..30eac98a8a 100644 --- a/mods/cnc/rules/structures.yaml +++ b/mods/cnc/rules/structures.yaml @@ -239,12 +239,14 @@ PROC: RevealsShroud: Range: 6c0 Refinery: + TickRate: 15 + DockHost: + Type: Unload DockAngle: 448 - DockOffset: 0,2 + DockOffset: -1c0, 1c0, 0 IsDragRequired: True DragOffset: -554,512,0 DragLength: 12 - TickRate: 15 StoresResources: Capacity: 1000 Selectable: diff --git a/mods/cnc/rules/vehicles.yaml b/mods/cnc/rules/vehicles.yaml index 9a7ab89817..474f5055de 100644 --- a/mods/cnc/rules/vehicles.yaml +++ b/mods/cnc/rules/vehicles.yaml @@ -63,6 +63,7 @@ HARV: SearchFromHarvesterRadius: 8 HarvestFacings: 8 EmptyCondition: no-tiberium + DockClientManager: Mobile: Speed: 72 Health: diff --git a/mods/d2k/rules/structures.yaml b/mods/d2k/rules/structures.yaml index 59e1b20168..5034fca269 100644 --- a/mods/d2k/rules/structures.yaml +++ b/mods/d2k/rules/structures.yaml @@ -282,9 +282,11 @@ refinery: RevealsShroud: Range: 3c768 Refinery: - DockAngle: 640 - DockOffset: 2,1 TickRate: 20 + DockHost: + Type: Unload + DockAngle: 640 + DockOffset: 1c0,512,0 StoresResources: Capacity: 2000 CustomSellValue: diff --git a/mods/d2k/rules/vehicles.yaml b/mods/d2k/rules/vehicles.yaml index 7e1c824969..295b03fdf4 100644 --- a/mods/d2k/rules/vehicles.yaml +++ b/mods/d2k/rules/vehicles.yaml @@ -76,6 +76,7 @@ harvester: BaleUnloadDelay: 5 SearchFromProcRadius: 30 SearchFromHarvesterRadius: 15 + DockClientManager: CarryableHarvester: Health: HP: 45000 diff --git a/mods/ra/rules/structures.yaml b/mods/ra/rules/structures.yaml index df5a338bda..51f7cc6cdb 100644 --- a/mods/ra/rules/structures.yaml +++ b/mods/ra/rules/structures.yaml @@ -1287,8 +1287,10 @@ PROC: RevealsShroud@GAPGEN: Range: 4c0 Refinery: + DockHost: + Type: Unload DockAngle: 256 - DockOffset: 1,2 + DockOffset: 0, 1c0, 0 StoresResources: Capacity: 2000 CustomSellValue: diff --git a/mods/ra/rules/vehicles.yaml b/mods/ra/rules/vehicles.yaml index bb43ebad6f..b29f060da9 100644 --- a/mods/ra/rules/vehicles.yaml +++ b/mods/ra/rules/vehicles.yaml @@ -327,6 +327,7 @@ HARV: SearchFromHarvesterRadius: 8 HarvestFacings: 8 EmptyCondition: no-ore + DockClientManager: Health: HP: 60000 Armor: diff --git a/mods/ts/rules/nod-structures.yaml b/mods/ts/rules/nod-structures.yaml index 6986b94333..e5a1f699dd 100644 --- a/mods/ts/rules/nod-structures.yaml +++ b/mods/ts/rules/nod-structures.yaml @@ -589,8 +589,10 @@ NAWAST: Range: 6c0 MaxHeightDelta: 3 Refinery: + DockHost: + Type: UnloadWeed DockAngle: 640 - DockOffset: 2,1 + DockOffset: 724,724,0 StoresResources: Capacity: 56 Power: diff --git a/mods/ts/rules/nod-vehicles.yaml b/mods/ts/rules/nod-vehicles.yaml index 3f1d90e72d..336d3a45a0 100644 --- a/mods/ts/rules/nod-vehicles.yaml +++ b/mods/ts/rules/nod-vehicles.yaml @@ -351,7 +351,7 @@ WEED: Prerequisites: ~naweap, nawast, ~techlevel.superweapons Description: Collects veins for processing.\n Unarmed Harvester: - DeliveryBuildings: nawast + Type: UnloadWeed Capacity: 7 Resources: Veins BaleUnloadDelay: 20 @@ -359,7 +359,8 @@ WEED: SearchFromProcRadius: 72 SearchFromHarvesterRadius: 36 HarvestVoice: Attack - DeliverVoice: Move + DockClientManager: + Voice: Move Mobile: Speed: 71 TurnSpeed: 20 diff --git a/mods/ts/rules/shared-structures.yaml b/mods/ts/rules/shared-structures.yaml index 466d4f6b35..1b58dae996 100644 --- a/mods/ts/rules/shared-structures.yaml +++ b/mods/ts/rules/shared-structures.yaml @@ -120,9 +120,11 @@ PROC: Range: 6c0 MaxHeightDelta: 3 Refinery: - DockAngle: 640 - DockOffset: 2,1 DiscardExcessResources: true + DockHost: + Type: Unload + DockAngle: 640 + DockOffset: 362,362,0 StoresResources: Capacity: 2000 CustomSellValue: diff --git a/mods/ts/rules/shared-vehicles.yaml b/mods/ts/rules/shared-vehicles.yaml index 524510aa08..5078c013a8 100644 --- a/mods/ts/rules/shared-vehicles.yaml +++ b/mods/ts/rules/shared-vehicles.yaml @@ -59,7 +59,6 @@ HARV: Bounds: 1086, 2172 DecorationBounds: 1086, 2172 Harvester: - DeliveryBuildings: proc Capacity: 28 Resources: Tiberium, BlueTiberium BaleLoadDelay: 15 @@ -68,8 +67,9 @@ HARV: SearchFromProcRadius: 36 SearchFromHarvesterRadius: 18 HarvestVoice: Attack - DeliverVoice: Move EmptyCondition: no-tiberium + DockClientManager: + Voice: Move Mobile: Speed: 71 Health: